2026-05-11 (3VO): C++: Inheritance (Mocking A Sensor)#

Initial State: Reading And Writing sysfs Files#

@startuml

class Sensor {
  + double get_temperature()
}

note bottom of Sensor: Linux/hwmon\n(though not reflected by its name)

class PWMPin {
  + void set_duty_cycle(uint64_t)
  + void set_period(uint64_t)
}

note bottom of PWMPin: Linux/sysfs\n(though not reflected by its name)

class Logic
{
  + void loop()
}

note bottom of Logic: each loop iteration:\n* read temperature\n*update PWM duty cycle accordingly

Logic -l-> Sensor
Logic -r-> PWMPin

@enduml

  • Hardware based implementation: we are reading and writing sysfs files

    • Sensor: hwmon style; reading, say, /sys/class/hwmon/hwmon2/temp1_input

    • PWM: writing, say, /sys/class/pwm/pwmchip0/pwm0/period and /sys/class/pwm/pwmchip0/pwm0/duty_cycle

Existing Pieces: PWM Pin#

#pragma once

#include <filesystem>

class PWMPin
{
public:
    PWMPin(std::filesystem::path basedir);

    void set_period(uint64_t);
    void set_duty_cycle(uint64_t);

private:
    std::filesystem::path _basedir;
    bool _ok;
};
#include "pwm.h"
#include "fileutil.h"

PWMPin::PWMPin(std::filesystem::path basedir)
: _basedir(basedir)
{
    if (!std::filesystem::exists(basedir))
        throw std::exception();
    if (!file_is_rwable(basedir / "period"))
        throw std::exception();
    if (!file_is_rwable(_basedir / "duty_cycle"))
        throw std::exception();
}

void PWMPin::set_period(uint64_t period)
{ 
    return write_uint64_t_to_file(_basedir / "period", period);
}

void PWMPin::set_duty_cycle(uint64_t duty_cycle)
{
    return write_uint64_t_to_file(_basedir / "duty_cycle", duty_cycle);
}

Existing Pieces: Sensor#

#pragma once

#include <filesystem>

class Sensor
{
public:
    Sensor(const std::filesystem::path& temperature_file);
    double get_temperature();

private:
    std::filesystem::path _temperature_file;
};
#include "sensor.h"

#include "fileutil.h"


Sensor::Sensor(const std::filesystem::path& temperature_file)
: _temperature_file(temperature_file) {}

double Sensor::get_temperature()
{
    return read_file_as_uint64_t(_temperature_file) / 1000;
}

Existing Pieces: Main Program#

code/2026-05-11/initial/temperature-display.cpp#
#include "sensor.h"
#include "pwm.h"
#include "logic.h"
#include "sensor.h"
#include "pwm.h"

#include <iostream>

int main(int argc, char** argv)
{
    if (argc != 3) {
        std::cerr << "Usage: " << argv[0] << " <SYSFS-HWMON-STYLE-TEMPERATURE-FILE> <SYSFS-PWMPIN-DIRECTORY>" << std::endl;
        return 1;
    }

    Sensor sensor(argv[1]);
    PWMPin pwmpin(argv[2]);
    pwmpin.set_period(30*1000*1000);
    pwmpin.set_duty_cycle(0);

    Logic logic(&sensor, &pwmpin);
    logic.loop();

    return 0;
}

Existing Pieces: Loop#

#pragma once

#include "sensor.h"
#include "pwm.h"

class Logic
{
public:
    Logic(
        Sensor* sensor, 
        PWMPin* pwmpin
    );
    void loop();
private:
    Sensor* _sensor;
    PWMPin* _pwmpin;
};
#include "logic.h"

#include <cassert>

Logic::Logic(
    Sensor* sensor, 
    PWMPin* pwmpin)
: _sensor(sensor),
  _pwmpin(pwmpin) {}

void Logic::loop()
{
    while (true) {
        double t = _sensor->get_temperature();
        uint64_t duty_cycle = t/50 * 30*1000*1000;
        _pwmpin->set_duty_cycle(duty_cycle);

        const timespec naptime = {
            .tv_sec = 1,
            .tv_nsec = 0,
        };
        int rv = nanosleep(&naptime, nullptr);
        assert(rv != -1);
    }
}

And Sensor/PWM Alternatives?#

  • Unplanned for alternatives: Logic hardwired to exactly these sysfs hardware interface

  • Possible alternative sensors

Possible Sensor Alternative (Brute Force Approach)#

  • Sensor type is determined at startup: choose alternative implementation instead

  • Let logic operate on that instead

  • Intended modification: instantiate alternatives

#include "alternative-sensor.h"
#include "pwm.h"
#include "logic.h"
#include <iostream>

int main(int argc, char** argv)
{
    if (argc != 2) {
        std::cerr << "Usage: " << argv[0] << " <SYSFS-PWMPIN-DIRECTORY>" << std::endl;
        return 1;
    }

    AlternativeSensor sensor;                          // <-- intended modification
    PWMPin pwmpin(argv[1]);
    pwmpin.set_period(30*1000*1000);
    pwmpin.set_duty_cycle(0);

    Logic logic(&sensor, &pwmpin);
    logic.loop();

    return 0;
}

Btw, The Alternative Sensor Implementation#

#pragma once

class AlternativeSensor
{
public:
    double get_temperature() { return 37.5; }
};

Alternative: Unintended Modifications#

#pragma once

#include "alternative-sensor.h"
#include "pwm.h"

class Logic
{
public:
    Logic(
        AlternativeSensor* sensor,                     // <-- unintended modification
        PWMPin* pwmpin
    );
    void loop();
private:
    AlternativeSensor* _sensor;                        // <-- unintended modification
    PWMPin* _pwmpin;
};

A Better Approach For Alternatives: Interfaces/Polymorphic Types#

  • SensorInterface does not implement anything

  • Just dictates how an implementation must look like

  • Abstract base class

  • Logic uses the interface - and not a concrete implementation

  • Implementations implement that interface

  • ⟶ Implementations can be exchanged, while leaving Logic unmodified

@startuml

interface SensorInterface {
  + double get_temperature()
}

note left of SensorInterface: interface

class Sensor {
  + double get_temperature()
}
SensorInterface <|.. Sensor

note bottom of Sensor: one concrete implementation

class AlternativeSensor {
  + double get_temperature()
}
SensorInterface <|.. AlternativeSensor

note bottom of AlternativeSensor: another concrete implementation

class PWMPin {
  + void set_duty_cycle(uint64_t)
  + void set_period(uint64_t)
}

note bottom of PWMPin: (not touched, to be done analogously)

class Logic
{
  + void loop()
}

note top of Logic: uses interface

Logic -l-> SensorInterface
Logic -r-> PWMPin

@enduml

Defining An Interface#

@startuml

interface SensorInterface {
  + double get_temperature()
}

@enduml

#pragma once

class SensorInterface
{
public:
    virtual ~SensorInterface() = default;              // <-- wtf?
    virtual double get_temperature() = 0;              // <-- "abstract", or "pure virtual"
};
  • Interfaces are only one usage of C++’s inheritance toolcase

  • virtual: dynamic dispatch

    Although called (by Logic) via the base class reference/pointer, the call is dispatched to the actual type

  • = 0: abstract method

    No implementation given ⟶ containing class cannot be instantiated

  • Virtual destructor ⟶ see Destructors And Interfaces

Implementing An Interface#

@startuml

interface SensorInterface {
  + double get_temperature()
}

class Sensor {
  + double get_temperature()
}
SensorInterface <|.. Sensor

class AlternativeSensor {
  + double get_temperature()
}
SensorInterface <|.. AlternativeSensor

@enduml

#pragma once

#include "sensor-interface.h"
#include <filesystem>

class Sensor : public SensorInterface                  // <-- *is-a* SensorInterface
{
public:
    Sensor(const std::filesystem::path& temperature_file);
    double get_temperature() override;                 // <-- override? wtf?

private:
    std::filesystem::path _temperature_file;
};
#pragma once

#include "sensor-interface.h"

class AlternativeSensor : public SensorInterface       // <-- *is-a* SensorInterface
{
public:
    double get_temperature() override                  // <-- override? wtf?
    { 
        return 37.5;
    }
};

Assembling Parts#

  • Instantiate concrete type

  • Abstract types cannot be instantiated

  • Pass concrete object as their base class

  • ⟶ Automatically converted to their base types

#include "alternative-sensor.h"
#include "pwm.h"
#include "logic.h"
#include <iostream>

int main(int argc, char** argv)
{
    if (argc != 2) {
        std::cerr << "Usage: " << argv[0] << " <SYSFS-PWMPIN-DIRECTORY>" << std::endl;
        return 1;
    }

    AlternativeSensor sensor;                          // <-- instantiation of concrete type
    PWMPin pwmpin(argv[1]);
    pwmpin.set_period(30*1000*1000);
    pwmpin.set_duty_cycle(0);

    Logic logic(
        &sensor,                                       // <-- AlternativeSensor* converted to SensorInterface*
        &pwmpin);
    logic.loop();

    return 0;
}

Final Polishing: Names Are Important#

  • Sensor does not say anything about its nature

    • It is actually Linux specific

    • Of the many Linux ways to talk to a sensor, it uses the hwmon (hardware monitoring) subsystem

    • LinuxHWMONSensor is probably a better name

  • AlternativeSensor has been chosen for didactic purposes, and also says nothing about the nature of the alternative

    • We want to use such a trivial implementation for automatic tests (Logic does not require actual hardware to be tested)

    • MockSensor describes that fact

  • SensorInterface. Interfaces are an important concept, and the name reflects it well.

    • It just does not roll well off the tongue

    • Why not just say Sensor?

Before

After

@startuml

interface SensorInterface {
  + double get_temperature()
}

class Sensor {
  + double get_temperature()
}
SensorInterface <|.. Sensor

class AlternativeSensor {
  + double get_temperature()
}
SensorInterface <|.. AlternativeSensor

@enduml

@startuml

interface Sensor {
  + double get_temperature()
}

class LinuxHWMONSensor {
  + double get_temperature()
}
Sensor <|.. LinuxHWMONSensor

class MockSensor {
  + double get_temperature()
}
Sensor <|.. MockSensor

@enduml