Skip to content

BikeComputer Part I

Introduction

BikeComputer program

In this codelab, we consider the implementation of a bike computer for spinning bikes. Spinning bike only have a flywheel and one needs to continuously pedal to turn the wheel. Our spinning bike is original, because it also has a gear system (not sure whether this is mechanically feasible…).

Our bike_computer program implements the following functionalities:

  • Bike gear: the bike’s gear system provides a value representing the current gear that the bike_computer program can read. On our system, gear may be changed using the buttons.
  • Pedal rotation: the bike_computer program simulates pedal rotation at a given speed. On our system, rotation speed can be modified using the buttons.
  • Reset: with a button, the user can reset the counters.
  • Speedometer: based on the current gear and pedal rotation, it computes the current speed and the traveled distance.
  • Temperature: our bike has a temperature sensor that is used for displaying the current environmental temperature.
  • Display: the user can read the information provided by the bike_computer program on an LCD screen.

In this and the following codelabs, we will explore different approaches to implementing the bike_computer program. We will analyze the advantages and disadvantages of these implementations. We will also study some important performance indicators, such as the reset response time, which is the delay between the user pressing the reset button and the LCD displaying the reset values.

What you’ll build

In this codelab, you will program a simple bike_computer application using a a Timeline Cyclic Scheduling algorithm with a Super Loop mechanism. In the next codelab, we will implement Timeline Cyclic Scheduling scheduling using the Time-Triggered Cyclic Executive (TTCE)_ mechanism.

What you’ll learn

  • How to develop a embedded program using a Super Loop mechanism.
  • How to schedule the different tasks of an embedded program using {{ ttce_scheduling }}.
  • The limitations of the proposed Super Loop and Time-Triggered Cyclic Executive mechanisms.
  • Together with the following codelabs, the advantages and disadvantages of the different programming models and scheduling algorithms.

What you’ll need

  • The Zephyr Development Environment for developing and debugging C++ code snippets.
  • All preceding codelabs are prerequisites for this codelab.

The different implementations

The bike_computer codelab is divided into three different parts, in which we will realize different implementations of the same program:

  • The first implementations (codelab parts 1 and 2) are the simplest: a Super Loop and a Time-Triggered Cyclic Executive program with Timeline Cyclic Scheduling of tasks. In these implementations, the system never generates any event and all tasks are executed periodically.
  • We will analyze the limitations of this approach, and, based on these, implement several event-driven programs (parts 2 and 3 of the bike_computer codelab).

To start the different implementations, you must create a new Zephyr RTOS bike_computer application under the bike_computer folder. This can be done with

just create-app bike_computer

We will implement the different versions of the bike_computer computer app program using the following approach. We implement the different versions into a single program and keep them in separate subfolders, distinguishing the entry point in the main() function. This ensures that all versions are implemented within the same project. To clearly differentiate between the versions within the program, we use different namespaces for each one.

Conceive and Implement the bike_computer Modules

It is a good practice to modularize the implementation of a program. In C++, this naturally involves the creation of C++ classes, each of which is responsible for implementing a specific feature. First, we implement these C++ classes, and then we integrate them into our bike_computer program.

We start by implementing the classes that will be used by all bike_computer program implementations: the SensorDevice and Speedometer classes. As these classes will be used by all bike_computer program implementations, they are added to a subfolder named common and are defined within the bike_computer namespace.

The Constants Used in the Program

The constants are defined as follows:

Definition of constants
bike_computer/src/common/constants.hpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file constants.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Constants definition used for implementing the bike system
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

#pragma once

// std
#include <chrono>

namespace bike_computer {

// gear related constants
static constexpr uint8_t kMinGear = 1;
static constexpr uint8_t kMaxGear = 9;
// smallest gear (= 1) corresponds to a gear size of 20
// when the gear increases, the gear size descreases
static constexpr uint8_t kMaxGearSize = 20;
static constexpr uint8_t kMinGearSize = kMaxGearSize - kMaxGear;

// pedal related constants
// When compiling and linking with gcc, we get a link error when using static
// constexpr. The error is related to template instantiation.
using std::literals::chrono_literals::operator""ms;

// definition of pedal rotation initial time (corresponds to 80 turn / min)
static constexpr std::chrono::milliseconds kInitialPedalRotationTime = 750ms;
// definition of pedal minimal rotation time (corresponds to 160 turn / min)
static constexpr std::chrono::milliseconds kMinPedalRotationTime = 375ms;
// definition of pedal maximal rotation time (corresponds to 10 turn / min)
static constexpr std::chrono::milliseconds kMaxPedalRotationTime = 1500ms;
// definition of pedal rotation time change upon acceleration/deceleration
static constexpr std::chrono::milliseconds kDeltaPedalRotationTime = 25ms;

}  // namespace bike_computer

The SensorDevice Class

To simplify the integration of the BME280 sensor and hide some hardware details, our bike_computer program includes a SensorDevice class. The class declaration is given below:

SensorDevice declaration
bike_computer/src/common/sensor_device.hpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file sensor_device.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief SensorDevice header file
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

#pragma once

// zephyr
#include <zephyr/kernel.h>

// zpp_lib
#include "zpp_include/non_copyable.hpp"
#include "zpp_include/zephyr_result.hpp"

namespace bike_computer {

class SensorDevice : private zpp_lib::NonCopyable {
public:
  // constructor
  SensorDevice() = default;

  // method for initializing the device
  [[nodiscard]] zpp_lib::ZephyrResult initialize();

  // methods used for reading sensor measurements
  [[nodiscard]] zpp_lib::ZephyrResult read_temperature(float& temperature);
  [[nodiscard]] zpp_lib::ZephyrResult read_humidity(float& humidity);

private:
  // data members
  const struct device* _sensor_device = nullptr;
};

}  // namespace bike_computer

It uses the namespace bike_computer. Based on the class declaration, you must implement it in the bike_computer/src/common/sensor_device.cpp file. Once you have implemented it, you may test your implementation by using the following test program:

SensorDevice test program
bike_computer/tests/bike_computer/sensor_device/src/main.cpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file test_sensor_device.cpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Test program for the SensorDevice class
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

// zephyr

// bike_computer
#include "common/sensor_device.hpp"

// zpp_lib
#include "zpp_include/zpp_assert.hpp"
#include "zpp_include/zpp_log.hpp"
#include "zpp_include/zpp_test.hpp"

ZPP_LOG_MODULE_REGISTER(bike_computer, CONFIG_APP_LOG_LEVEL);

ZPP_ZTEST(sensor_device, test_sensor_device) {
  // create the SensorDevice instance
  bike_computer::SensorDevice sensor_device;

  auto res = sensor_device.initialize();
  if (!res) {
    zpp_zassert_true(res, "Cannot initialize sensor device: %d", static_cast<int>(res.error()));
  }

  float temperature = 0.0F;
  res               = sensor_device.read_temperature(temperature);
  if (!res) {
    zpp_zassert_true(res, "Cannot initialize sensor device: %d", static_cast<int>(res.error()));
  }
  static constexpr float kTemperatureRange = 20.0F;
  static constexpr float kMeanTemperature  = 15.0F;
  zpp_zassert_within(temperature, kMeanTemperature, kTemperatureRange, "Temperature outside range: %f", static_cast<double>(temperature));

  float humidity = 0.0F;
  res            = sensor_device.read_humidity(humidity);
  if (!res) {
    zpp_zassert_true(res, "Cannot initialize sensor device: %d", static_cast<int>(res.error()));
  }
  static constexpr float kHumidityRange = 45.0F;
  static constexpr float kMeanHumidity  = 50.0F;
  zpp_zassert_within(humidity, kMeanHumidity, kHumidityRange, "Humidity outside range: %f", static_cast<double>(humidity));
}

ZPP_ZTEST_SUITE(sensor_device, nullptr, nullptr, nullptr, nullptr, nullptr);

If you integrate this program into the tests folder of your bike_computer program in the appropriate subfolder, you may validate its correct implementation by using the command

just test bike_computer/tests "your_map_file.yaml"

Emulation on QEMU_X86

To run the SensorDevice test on qemu_x86, the BME280 sensor must be fully emulated on this platform. Since the sensor functionality is encapsulated in the SensorDevice class, we can emulate the sensor more easily. In the class implementation, use the #if !CONFIG_EMUL pragma to differentiate the implementation for the qemu_x86 device and implement a simple emulation that delivers sensor values without interacting with a physical sensor. Although a full emulation would be preferable, emulating the sensor in this manner is sufficient for our purposes.

If the test program succeeds (assuming that you have connected your BME280 sensor and screen properly), then it is time to commit and push your changes.

Small commits

Remember:

  • Small commits are better than large commits that include many unrelated changes.
  • Before committing the changes, you must also integrate the related application in the precommit phase by adding the following hook:
    .pre-commit-config.yaml
    - id: clang-tidy-bike-computer-sensor-device
      name: clang-tidy-bike-computer-sensor-device
      entry: python deps/zpp_lib/scripts/run_clang_tidy.py --app bike_computer/tests/sensor_device --configs "log+debug+gpio+display+sensor+test+phase_a" --wd "."
      language: system
      pass_filenames: true
      require_serial: true
      files: ^(bike_computer/tests/sensor_device)/.*\.(c|cc|cpp|cxx)$
    
    Once the precommit phase succeeds, you may commit and push your changes.

The Speedometer Class

Another class that will be used by all bike_computer implementations is the Speedometer class. This class is responsible to compute the current speed and traveled distance, given the gear size, the wheel circumference and the pedal rotation time. The class declaration is given below:

Speedometer declaration
bike_computer/src/common/speedometer.hpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file speedometer.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Speedometer header file
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

#pragma once

// std
#include <chrono>

// local
#include "constants.hpp"

// zpp_lib
#include "zpp_include/mutex.hpp"
#include "zpp_include/non_copyable.hpp"
#include "zpp_include/thread.hpp"

// stl
#if CONFIG_TEST
#include <functional>
#endif  // CONFIG_TEST

namespace bike_computer {

using std::literals::chrono_literals::operator""ms;
using std::literals::chrono_literals::operator""us;

class Speedometer : private zpp_lib::NonCopyable {
public:
  Speedometer();

  // method used for setting the current pedal rotation time
  void set_current_pedal_rotation_time(const std::chrono::milliseconds& current_rotation_time);

  // method used for setting/getting the current gear
  void set_gear_size(uint8_t gear_size);

  // method called for getting the current speed (expressed in km / h)
  [[nodiscard]] float get_current_speed() const;

  // method called for getting the current traveled distance (expressed in km)
  [[nodiscard]] float get_traveled_distance();

  // method called for resetting the traveled distance
  void reset();

  // methods used for tests only
#if CONFIG_TEST == 1
  [[nodiscard]] uint8_t get_gear_size() const {
    return _gear_size;
  }
  static float s_get_wheel_circumference() {
    return kWheelCircumference;
  }
  static uint8_t s_get_tray_size() {
    return kTraySize;
  }
  [[nodiscard]] std::chrono::milliseconds get_current_pedal_rotation_time() const {
    return _pedal_rotation_time;
  }
  using CallbackFunction = std::function<void()>;
  void set_on_reset_callback(CallbackFunction cb) {
    _cb = std::move(cb);
  }
#endif  // CONFIG_TEST == 1

private:
  // private methods
  void compute_speed();
  float compute_traveled_distance();

  // definition of task period time
  static constexpr std::chrono::milliseconds kTaskPeriod = 400ms;
  // definition of task execution time
  static constexpr std::chrono::microseconds kTaskRunTime = 200000us;

  // constants related to speed computation
  static constexpr float kWheelCircumference     = 2.1F;
  static constexpr uint8_t kTraySize             = 50;
  std::chrono::microseconds _last_time           = std::chrono::microseconds::zero();
  std::chrono::milliseconds _pedal_rotation_time = kInitialPedalRotationTime;

  // data members
  // LowPowerTicker _ticker;
  float _current_speed = 0.0F;
  zpp_lib::Mutex _total_distance_mutex;
  float _total_distance = 0.0F;
  uint8_t _gear_size    = 1;

#if CONFIG_TEST == 1
  std::function<void()> _cb;
#endif  // CONFIG_TEST == 1
};

}  // namespace bike_computer

The test program for this class is given below:

Speedometer test program
bike_computer/tests/bike_computer/speedometer/src/test_speedometer.cpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file test_speedometer.cpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Test program for the Speedometer class
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

// zephyr

// std
#include <chrono>
#include <cstdio>

// zpp_lib
#include "zpp_include/this_thread.hpp"
#include "zpp_include/zpp_assert.hpp"
#include "zpp_include/zpp_log.hpp"
#include "zpp_include/zpp_test.hpp"

// bike_computer
#include "common/speedometer.hpp"

ZPP_LOG_MODULE_REGISTER(bike_computer, CONFIG_APP_LOG_LEVEL);

// allow for 0.1 km/h difference
static constexpr float kAllowedSpeedDelta = 0.1F;
// allow for 1m difference
static constexpr float kAllowedDistanceDelta = 1.0F / 1000.0F;

// function called by test handler functions for verifying the current speed
// Internal function
void check_current_speed(const std::chrono::milliseconds& pedal_rotation_time,
                         uint8_t tray_size,
                         uint8_t gear_size,          // NOLINT(bugprone-easily-swappable-parameters)
                         float wheel_circumference,  // NOLINT(bugprone-easily-swappable-parameters)
                         float current_speed) {
  // compute the number of pedal rotation per hour
  static constexpr auto kMillisecondsPerSecond = 1000;
  static constexpr auto kSecondsPerHour        = 3600;
  static constexpr auto kMillisecondsPerHour   = kMillisecondsPerSecond * kSecondsPerHour;
  float pedal_rotations_per_hour               = static_cast<float>(kMillisecondsPerHour) / static_cast<float>(pedal_rotation_time.count());

  // compute the expected speed in km / h
  // first compute the distance in meter for each pedal turn
  float tray_gear_ratio         = static_cast<float>(tray_size) / static_cast<float>(gear_size);
  float distance_per_pedal_turn = tray_gear_ratio * wheel_circumference;
  float expected_speed          = (distance_per_pedal_turn / kMillisecondsPerSecond) * pedal_rotations_per_hour;

  ZPP_LOG_INF("  Expected speed is %f, current speed is %f\n", static_cast<double>(expected_speed), static_cast<double>(current_speed));
  zpp_zassert_within(current_speed, expected_speed, kAllowedSpeedDelta, "Current speed is not within bounds");
}

// compute the traveled distance for a time interval
// Internal function
float compute_distance(const std::chrono::milliseconds& pedal_rotation_time,
                       uint8_t tray_size,
                       uint8_t gear_size,  // NOLINT(bugprone-easily-swappable-parameters)
                       float wheel_circumference,
                       const std::chrono::milliseconds& travel_time) {
  // compute the number of pedal rotation during travel time
  // both times are expressed in ms
  float pedal_rotations = static_cast<float>(travel_time.count()) / static_cast<float>(pedal_rotation_time.count());

  // compute the distance in meter for each pedal turn
  float tray_gear_ratio         = static_cast<float>(tray_size) / static_cast<float>(gear_size);
  float distance_per_pedal_turn = tray_gear_ratio * wheel_circumference;

  // distancePerPedalTurn is expressed in m, divide per kMetersPerKilometer for a distance in km
  static constexpr float kMetersPerKilometer = 1000.0F;
  return (distance_per_pedal_turn * pedal_rotations) / kMetersPerKilometer;
}

// function called by test handler functions for verifying the distance traveled
void check_distance(const std::chrono::milliseconds& pedal_rotation_time,
                    uint8_t tray_size,
                    uint8_t gear_size,
                    float wheel_circumference,
                    const std::chrono::milliseconds& travel_time,
                    float distance) {
  // distancePerPedalTurn is expressed in m, divide per 1000 for a distance in km
  float expected_distance = compute_distance(pedal_rotation_time, tray_size, gear_size, wheel_circumference, travel_time);
  ZPP_LOG_INF("  Expected distance is %f, current distance is %f\n", static_cast<double>(expected_distance), static_cast<double>(distance));
  zpp_zassert_within(distance, expected_distance, kAllowedDistanceDelta, "Current distance is not within bounds");
}

// test the speedometer by modifying the gear
ZPP_ZTEST(speedometer, test_gear_size) {
  // create a speedometer instance
  bike_computer::Speedometer speedometer;

  // get speedometer constant values (for this test)
  auto tray_size           = bike_computer::Speedometer::s_get_tray_size();
  auto wheel_circumference = bike_computer::Speedometer::s_get_wheel_circumference();
  auto pedal_rotation_time = speedometer.get_current_pedal_rotation_time();

  for (uint8_t gear_size = bike_computer::kMinGearSize; gear_size <= bike_computer::kMaxGearSize; gear_size++) {
    // set the gear
    ZPP_LOG_INF("Testing gear size %d\n", gear_size);
    speedometer.set_gear_size(gear_size);

    // get the current speed
    auto current_speed = speedometer.get_current_speed();

    // check the speed against the expected one
    check_current_speed(pedal_rotation_time, tray_size, gear_size, wheel_circumference, current_speed);
  }
}

// test the speedometer by modifying the pedal rotation speed
ZPP_ZTEST(speedometer, test_rotation_speed) {
  // create a speedometer instance
  bike_computer::Speedometer speedometer;

  // set the gear size
  speedometer.set_gear_size(bike_computer::kMaxGearSize);

  // get speedometer constant values
  auto tray_size           = bike_computer::Speedometer::s_get_tray_size();
  auto wheel_circumference = bike_computer::Speedometer::s_get_wheel_circumference();
  auto gear_size           = speedometer.get_gear_size();

  // first test increasing rotation speed (decreasing rotation time)
  auto pedal_rotation_time = speedometer.get_current_pedal_rotation_time();
  while (pedal_rotation_time > bike_computer::kMinPedalRotationTime) {
    // decrease the pedal rotation time
    pedal_rotation_time -= bike_computer::kDeltaPedalRotationTime;
    speedometer.set_current_pedal_rotation_time(pedal_rotation_time);

    // get the current speed
    auto current_speed = speedometer.get_current_speed();

    // check the speed against the expected one
    check_current_speed(pedal_rotation_time, tray_size, gear_size, wheel_circumference, current_speed);
  }

  // second test decreasing rotation speed (increasing rotation time)
  pedal_rotation_time = speedometer.get_current_pedal_rotation_time();
  while (pedal_rotation_time < bike_computer::kMaxPedalRotationTime) {
    // increase the pedal rotation time
    pedal_rotation_time += bike_computer::kDeltaPedalRotationTime;
    speedometer.set_current_pedal_rotation_time(pedal_rotation_time);

    // get the current speed
    auto current_speed = speedometer.get_current_speed();

    // check the speed against the expected one
    check_current_speed(pedal_rotation_time, tray_size, gear_size, wheel_circumference, current_speed);
  }
}

// test the speedometer by modifying the pedal rotation speed
ZPP_ZTEST(speedometer, test_distance) {
  // create a speedometer instance
  bike_computer::Speedometer speedometer;

  // set the gear size
  speedometer.set_gear_size(bike_computer::kMaxGearSize);

  // get speedometer constant values
  auto tray_size           = bike_computer::Speedometer::s_get_tray_size();
  auto wheel_circumference = bike_computer::Speedometer::s_get_wheel_circumference();
  auto gear_size           = speedometer.get_gear_size();
  auto pedal_rotation_time = speedometer.get_current_pedal_rotation_time();

  // test different travel times
  using std::literals::chrono_literals::operator""s;
  using std::literals::chrono_literals::operator""ms;
  static constexpr std::array<std::chrono::milliseconds, 4> kTravelTimes = {500ms, 1000ms, 5s, 10s};

  // first check travel distance without changing gear and rotation speed
  auto total_travel_time = std::chrono::milliseconds::zero();
  for (auto travel_time : kTravelTimes) {
    // run for the travel time and get the distance
    zpp_lib::ThisThread::sleep_for(travel_time);

    // get the traveled distance
    auto traveled_distance = speedometer.get_traveled_distance();

    // accumulate travel time
    total_travel_time += travel_time;

    // check the distance vs the expected one
    check_distance(pedal_rotation_time, tray_size, gear_size, wheel_circumference, total_travel_time, traveled_distance);
  }

  // now change gear at each time interval
  auto expected_distance = speedometer.get_traveled_distance();
  for (auto travel_time : kTravelTimes) {
    // update the gear size
    gear_size++;
    speedometer.set_gear_size(gear_size);

    // run for the travel time and get the distance
    zpp_lib::ThisThread::sleep_for(travel_time);

    // compute the expected distance for this time segment
    float distance = compute_distance(pedal_rotation_time, tray_size, gear_size, wheel_circumference, travel_time);
    expected_distance += distance;

    // get the distance traveled
    auto traveled_distance = speedometer.get_traveled_distance();

    printk("  Expected distance is %f, current distance is %f\n",
           static_cast<double>(expected_distance),
           static_cast<double>(traveled_distance));
    zpp_zassert_within(traveled_distance, expected_distance, kAllowedDistanceDelta);
  }

  // now change rotation speed at each time interval
  expected_distance = speedometer.get_traveled_distance();
  for (auto travel_time : kTravelTimes) {
    // update the rotation speed
    pedal_rotation_time += bike_computer::kDeltaPedalRotationTime;
    speedometer.set_current_pedal_rotation_time(pedal_rotation_time);

    // run for the travel time and get the distance
    zpp_lib::ThisThread::sleep_for(travel_time);

    // compute the expected distance for this time segment
    float distance = compute_distance(pedal_rotation_time, tray_size, gear_size, wheel_circumference, travel_time);
    expected_distance += distance;

    // get the distance traveled
    auto traveled_distance = speedometer.get_traveled_distance();

    printk("  Expected distance is %f, current distance is %f\n",
           static_cast<double>(expected_distance),
           static_cast<double>(traveled_distance));
    zpp_zassert_within(traveled_distance, expected_distance, kAllowedDistanceDelta);
  }
}

// test the speedometer by modifying the pedal rotation speed
ZPP_ZTEST(speedometer, test_reset) {
  // create a speedometer instance
  bike_computer::Speedometer speedometer;

  // set the gear size
  speedometer.set_gear_size(bike_computer::kMinGearSize);

  // get speedometer constant values
  auto tray_size           = bike_computer::Speedometer::s_get_tray_size();
  auto wheel_circumference = bike_computer::Speedometer::s_get_wheel_circumference();
  auto gear_size           = speedometer.get_gear_size();
  auto pedal_rotation_time = speedometer.get_current_pedal_rotation_time();

  // travel for 5 seconds
  using std::literals::chrono_literals::operator""ms;
  static constexpr auto kTravelTime = 5000ms;
  zpp_lib::ThisThread::sleep_for(kTravelTime);

  // check the expected distaance traveled
  auto expected_distance = compute_distance(pedal_rotation_time, tray_size, gear_size, wheel_circumference, kTravelTime);

  // get the distance traveled
  auto traveled_distance = speedometer.get_traveled_distance();

  ZPP_LOG_INF("  Expected distance is %f, current distance is %f\n",
              static_cast<double>(expected_distance),
              static_cast<double>(traveled_distance));
  zpp_zassert_within(traveled_distance, expected_distance, kAllowedDistanceDelta);

  // reset the speedometer
  speedometer.reset();

  // traveled distance should now be zero
  traveled_distance = speedometer.get_traveled_distance();

  ZPP_LOG_INF("  Expected distance is %f, current distance is %f\n", 0.0, static_cast<double>(traveled_distance));
  zpp_zassert_within(0.0F, traveled_distance, kAllowedDistanceDelta);

  // travel again for 5 seconds
  zpp_lib::ThisThread::sleep_for(kTravelTime);

  // reset the speedometer without getting the distance
  speedometer.reset();

  // travel again for 5 seconds
  zpp_lib::ThisThread::sleep_for(kTravelTime);

  // get the distance traveled
  traveled_distance = speedometer.get_traveled_distance();

  ZPP_LOG_INF("  Expected distance is %f, current distance is %f\n",
              static_cast<double>(expected_distance),
              static_cast<double>(traveled_distance));
  zpp_zassert_within(traveled_distance, expected_distance, kAllowedDistanceDelta);
}

ZPP_ZTEST_SUITE(speedometer, nullptr, nullptr, nullptr, nullptr, nullptr);

The implementation of the Speedometer class is given below, at the exception of the reset(), computeSpeed() and computeDistance() methods that you must implement yourselves.

Speedometer implementation (partial)
bike_computer/src/common/speedometer.cpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file speedometer_device.cpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Speedometer implementation
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

#include "speedometer.hpp"

// zephyr

// std
#include <chrono>

// zpp_lib
#include "zpp_include/time.hpp"
#include "zpp_include/zpp_assert.hpp"
#include "zpp_include/zpp_log.hpp"

ZPP_LOG_MODULE_DECLARE(bike_computer, CONFIG_APP_LOG_LEVEL);

namespace bike_computer {

Speedometer::Speedometer() : _last_time(zpp_lib::Time::get_uptime()) {}

void Speedometer::set_current_pedal_rotation_time(const std::chrono::milliseconds& current_rotation_time) {
  if (_pedal_rotation_time != current_rotation_time) {
    // compute distance before changing the rotation time
    compute_traveled_distance();

    // change pedal rotation time
    _pedal_rotation_time = current_rotation_time;

    // compute speed with the new pedal rotation time
    compute_speed();
  }
}

void Speedometer::set_gear_size(uint8_t gear_size) {
  if (_gear_size != gear_size) {
    // compute distance before changing the gear size
    compute_traveled_distance();

    // change gear size
    _gear_size = gear_size;

    // compute speed with the new gear size
    compute_speed();
  }
}

float Speedometer::get_current_speed() const {
  return _current_speed;
}

float Speedometer::get_traveled_distance() {
  // make sure to update the distance traveled
  return compute_traveled_distance();
}

void Speedometer::reset() {
#if CONFIG_TEST
  if (_cb != nullptr) {
    _cb();
  }
#endif  // CONFIG_TEST
  // TODO(Student)

}

#if CONFIG_TEST
uint8_t Speedometer::get_gear_size() const {
  return _gear_size;
}

float Speedometer::get_wheel_circumference() const {
  return kWheelCircumference;
}

uint8_t Speedometer::get_tray_size() const {
  return kTraySize;
}

std::chrono::milliseconds Speedometer::get_current_pedal_rotation_time() const {
  return _pedal_rotation_time;
}

void Speedometer::set_on_reset_callback(CallbackFunction cb) {
  _cb = cb;
}

#endif  // CONFIG_TEST

void Speedometer::compute_speed() {
  // For computing the speed given a rear gear (braquet), one must divide the size of
  // the tray (plateau) by the size of the rear gear (pignon arrière), and then multiply
  // the result by the circumference of the wheel. Example: tray = 50, rear gear = 15.
  // Distance run with one pedal turn (wheel circumference = 2.10 m) = 50/15 * 2.1 m
  // = 6.99m If you ride at 80 pedal turns / min, you run a distance of 6.99 * 80 / min
  // ~= 560 m / min = 33.6 km/h

  // TODO(Student)

float Speedometer::compute_traveled_distance() {
  // For computing the speed given a rear gear (braquet), one must divide the size of
  // the tray (plateau) by the size of the rear gear (pignon arrière), and then multiply
  // the result by the circumference of the wheel. Example: tray = 50, rear gear = 15.
  // Distance run with one pedal turn (wheel circumference = 2.10 m) = 50/15 * 2.1 m
  // = 6.99m If you ride at 80 pedal turns / min, you run a distance of 6.99 * 80 / min
  // ~= 560 m / min = 33.6 km/h. We then multiply the speed by the time for getting the
  // distance traveled.

  // TODO(Student)


}  // namespace bike_computer

If you implement these methods correctly and run the following command:

just test bike_computer/tests/speedometer "your_map_file.yaml"
then the four test cases should succeed. If some test cases fail, check the twister-out/twister.log file to understand and fix the problem with your implementation.

Once your implementation has been successfully tested, you may commit and push the changes as documented above. Remember to add the appropriate hook in the pre-commit configuration file.

Integrate the Other Required Software Components

In addition to the classes described above and below, you need to integrate further classes to implement the bike_computer application:

  • The BikeDisplay class provides an API to display information on the LCD screen. It is used in the BikeSystem class. The .cpp/.hpp files can be downloaded here and here.
  • Resources used in the BikeDisplay task can be downloaded here and copied to the bike_computer/src/common/resources folder.
  • The TaskManager class allows to monitor and test that tasks are respecting their timing constraints. When used in test programs (with CONFIG_TEST=y), the class will assert when timing constraints are not respected. You can download the .cpp/.hpp files here and here.

All files downloaded here must be copied to the appropriate folders.

The BikeDisplay class encapsulates the zpp_lib::Display class. To compile any application that uses this class on your nrf5340dk/nrf5340/cpuapp device, the provided scripts automatically add the corresponding shield by adding the --shield adafruit_2_8_tft_touch_v2 argument to the build commands.

In the context of Zephyr RTOS, a shield is an add-on board that attaches to the main board to extend its features, as explained here. In our case, the shield is the Adafruit 2.8" TFT Touch v2 screen. Files describing the shield can be found under deps/zephyr/boards/shields/adafruit_2_8_tft_touch_v2. The shield.yml file describes the shield’s name and main features, which can be used for display and input (touch screen) purposes, as well as for an SDHC (Secure Digital Host Controller) interface to connect an SD card.

The bike_computer Program Tasks

It is usually a good, simple approach to analyze a program’s requirements by describing the tasks it must perform. These tasks can then be implemented independently of each other in separate classes, methods, or functions. Of course, tasks often depend on each other, so the implementation must account for these dependencies. For our bike_computer program, and for the sake of simplicity, we will minimize these dependencies.

The tasks of our bike_computer program are defined as follows:

  • Gear task: the bike_computer program reads the current gear and gear size from the gear system. The task run time is \(100\,\mathsf{ms}\) and its period is \(800\,\mathsf{ms}\).
  • Speed and distance task: the bike_computer program reads the pedal rotation time and updates the speed and traveled distance. The task run time is $200\,\mathsf{ms} and it period is \(400\,\mathsf{ms}\)
  • Temperature task: the bike_computer program reads the temperature from the sensor device. The task run time is \(100\,\mathsf{ms}\) and its period is \(1600\,\mathsf{ms}\)
  • Reset task: the user may press a reset button for resetting the traveled distance. The task run time is \(100\,\mathsf{ms}\) and its period is \(800\,\mathsf{ms}\).
  • Display task: The bike_computer program updates the information displayed on the LCD screen. The task run time is \(300\,\mathsf{ms}\) and its period is \(1600\,\mathsf{ms}\). The task may be splitted in two different subtasks.

The task run times used in this example are of course not very realistic. They should be much shorter. However, since we are interested in analyzing the different application behaviors without introducing idle times, using these times makes the analysis simpler.

The figure below shows the tasks and their run times. This figure simply shows the relative run times of all the tasks and does not show how they may be scheduled.

BikeComputer tasks

BikeComputer Tasks

We use specific classes to implement different tasks and simulate certain behaviors:

  • The gear task is implemented using the GearDevice class, which allows to read the current gear and check for gear changes. Gear changes are implemented by pressing the buttons.
  • The speed and distance task is implemented using the PedalDevice class, which allows to read the current speed and traveled distance, as well as to check speed changes. Speed changes are implemented by pressing the buttons.
  • The display task uses the BikeDisplay class that provides an API to display the information on the LCD screen (encapsulating the zpp_lib::Display class).
  • The reset task is implemented using the ResetDevice class, which allows to check for reset. Reset is implemented by pressing a button.

The GearDevice Class

The GearDevice class code is given below:

GearDevice declaration
bike_computer/src/static_scheduling/gear_device.hpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file gear_device.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief GearDevice header file (static scheduling)
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

#pragma once

// local
#include "common/constants.hpp"

// zpp_lib
#include "zpp_include/interrupt_in.hpp"
#include "zpp_include/non_copyable.hpp"

namespace bike_computer::static_scheduling {

class GearDevice : private zpp_lib::NonCopyable {
public:
  GearDevice();

  // method called for updating the bike system
  [[nodiscard]] uint8_t get_current_gear();
  [[nodiscard]] uint8_t get_current_gear_size() const;

private:
  // data members
  uint8_t _current_gear = bike_computer::kMinGear;

  // buttons
  zpp_lib::InterruptIn _button2;
  zpp_lib::InterruptIn _button3;
  zpp_lib::InterruptIn _button4;
};

}  // namespace bike_computer::static_scheduling
GearDevice implementation
bike_computer/src/static_scheduling/gear_device.cpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file gear_device.cpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief GearDevice implementation (static scheduling)
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

#include "gear_device.hpp"

// from common
#include "common/task_manager.hpp"

// zpp_lib
#include "zpp_include/time.hpp"

namespace bike_computer::static_scheduling {

GearDevice::GearDevice()
    : _button2(zpp_lib::InterruptIn::PinName::BUTTON2), _button3(zpp_lib::InterruptIn::PinName::BUTTON3),
      _button4(zpp_lib::InterruptIn::PinName::BUTTON4) {}

uint8_t GearDevice::get_current_gear() {
  std::chrono::microseconds initial_time = zpp_lib::Time::get_uptime();
  std::chrono::microseconds elapsed_time = std::chrono::microseconds::zero();

  // we bound the change to one decrement/increment per call
  // we increment/decrement rotation speed when button3/button4 is pressed
  // while button2 is pressed
  bool has_changed = false;
  while (elapsed_time < TaskManager::get_task_computation_time(TaskManager::TaskType::GearTaskType)) {
    if (!has_changed) {
      if (_button2.read() == zpp_lib::kPolarityPressed) {
        if (_button3.read() == zpp_lib::kPolarityPressed) {
          if (_current_gear > bike_computer::kMinGear) {
            _current_gear--;
          }
          has_changed = true;
        }

        if (_button4.read() == zpp_lib::kPolarityPressed) {
          if (_current_gear < bike_computer::kMaxGear) {
            _current_gear++;
          }
          has_changed = true;
        }
      }
    }
    elapsed_time = zpp_lib::Time::get_uptime() - initial_time;
  }
  return _current_gear;
}

uint8_t GearDevice::get_current_gear_size() const {
  // simulate task computation by waiting for the required task run time
  // wait_us(kTaskRunTime.count());
  return bike_computer::kMaxGearSize - _current_gear;
}

}  // namespace bike_computer::static_scheduling

This code uses the static_scheduling namespace because the first implementation of the bike_computer program implements static scheduling. In the get_current_gear() method, the program checks the state of the buttons to see if Button 3 (“Gear Down”) or Button 4 (“Gear Up”) has been pressed while Button 2 is pressed. If so, the current gear is modified and further changes are not allowed in the current call. The method loops until the task run time elapses. Finally, the method returns the current gear.

The PedalDevice and ResetDevice Classes

Based on the GearDevice class, you must implement the PedalDevice and ResetDevice classes that implement the classes as declared below:

PedalDevice declaration
bike_computer/src/static_scheduling/pedal_device.hpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file pedal_device.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief PedalDevice header file (static scheduling)
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

#pragma once

// local
#include "common/constants.hpp"

// zpp_lib
#include "zpp_include/interrupt_in.hpp"
#include "zpp_include/non_copyable.hpp"

namespace bike_computer::static_scheduling {

class PedalDevice : private zpp_lib::NonCopyable {
public:
  PedalDevice();

  // method called for updating the bike system
  std::chrono::milliseconds get_current_rotation_time();

private:
  // private methods
  void increase_rotation_speed();
  void decrease_rotation_speed();

  // data members
  std::chrono::milliseconds _pedal_rotation_time = bike_computer::kInitialPedalRotationTime;

  // buttons
  zpp_lib::InterruptIn _button2;
  zpp_lib::InterruptIn _button3;
  zpp_lib::InterruptIn _button4;
};

}  // namespace bike_computer::static_scheduling

The PedalDevice implementation must modify the speed when the user presses the Button 3 or Button 4 without pressing Button 2.

ResetDevice declaration
bike_computer/src/static_scheduling/reset_device.hpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file reset_device.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief ResetDevice header file (static scheduling)
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

#pragma once

// std
#include <chrono>

// zpp_lib
#include "zpp_include/interrupt_in.hpp"
#include "zpp_include/non_copyable.hpp"
#include "zpp_include/registration_token.hpp"

namespace bike_computer::static_scheduling {

class ResetDevice : private zpp_lib::NonCopyable {
public:
  // constructor and destructor
  ResetDevice();

  // method called for checking the reset status
  bool check_reset();

  // for computing the response time
  std::chrono::microseconds get_press_time();

private:
  // called when one of the buttons is pressed
  void on_fall_button1();

  // data members
  zpp_lib::InterruptIn _button1;
  zpp_lib::RegistrationToken _button1_token;
  std::chrono::microseconds _press_time{std::chrono::microseconds::zero()};
};

}  // namespace bike_computer::static_scheduling

The ResetDevice must implement a reset when the user presses Button 1. In this implementation, the program must check the polarity of the input pin.

In the ResetDevice::on_fall_button1() method, the press time is registered using

_pressTime = zpp_lib::Time::get_uptime();
Since the press time is registered using an ISR method, it is a good approximation of when the button was actually pressed. This allows to compute a good approximation of the reset response time in the BikeComputer class.

For more details about the Zephyr RTOS timer, you may read the related documentation.

Integration of Components into a BikeSystem Class

Super-Loop Implementation with Static Cyclic Scheduling

The very first implementation of the bike_computer program is the simplest one. It implements a timeline cyclic scheduling of tasks, without event handling. In this implementation, no event is ever generated by the system and all tasks are executed within the infinite super-loop executed in the BikeSystem::start() method (see the description below).

Before completing the BikeSystem class and running your program, you must first understand how tasks will be precisely scheduled in your super-loop. You can do this by accomplishing the related exercise. The task periods and computation times are defined above. Based on this value, the shortest repeating cycle defined as the least common multiple of task periods is easily computed to be \(1600\,\mathsf{ms}\) in this case.

Implementation of the BikeSystem Class

After implementing all device-related classes and establishing a cyclic scheduling of the tasks, the next step is to declare and define a BikeSystem class that stores all bike variables and update methods. The BikeSystem class owns an instance of each of the classes implemented above and its interface is defined as follows:

BikeSystem declaration
static_scheduling/bike_system.hpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file bike_system.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Bike System header file (static scheduling)
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

#pragma once

// stl
#include <atomic>

// local
#include "gear_device.hpp"
#include "pedal_device.hpp"
#include "reset_device.hpp"

// zpp_lib
#include "zpp_include/display.hpp"

// from common
#include "common/bike_display.hpp"
#include "common/sensor_device.hpp"
#include "common/speedometer.hpp"
#include "common/task_manager.hpp"

namespace bike_computer::static_scheduling {

class BikeSystem {
public:
  // constructor
  BikeSystem() = default;

  // destructor
  ~BikeSystem();

  /** Explicity prevent (move) copy and assignment
      rather than inheriting from NonCopyable. This avoids
      cppcoreguidelines-special-member-functions warning by clang-tidy.
  */
  BikeSystem(const BikeSystem&)            = delete;
  BikeSystem(BikeSystem&&)                 = delete;
  BikeSystem& operator=(const BikeSystem&) = delete;
  BikeSystem& operator=(BikeSystem&&)      = delete;

  // method called in main() for starting the system
  [[nodiscard]] zpp_lib::ZephyrResult start();

  // method called for stopping the system
  void stop();

private:
  // private methods
  [[nodiscard]] zpp_lib::ZephyrResult initialize();
  void gear_task();
  void speed_distance_task();
  void temperature_task();
  void reset_task();
  void display_task1();
  void display_task2();

  // flag stating whether sleep is allowed when simulating computation times
  static constexpr bool kAllowSleep = false;
  // stop flag, used for stopping the super-loop (set in stop())
  volatile std::atomic<bool> _stop_flag = false;
  // data member that represents the device for manipulating the gear
  GearDevice _gear_device;
  uint8_t _current_gear      = bike_computer::kMinGear;
  uint8_t _current_gear_size = bike_computer::kMinGearSize;
  // data member that represents the device for manipulating the pedal rotation
  // speed/time
  PedalDevice _pedal_device;
  float _current_speed     = 0.0F;
  float _traveled_distance = 0.0F;
  // data member that represents the device used for resetting
  ResetDevice _reset_device;
  // data member that represents the display
  BikeDisplay _bike_display;
  // data member that represents the device for counting wheel rotations
  Speedometer _speedometer;
  // data member that represents the sensor device
  SensorDevice _sensor_device;
  float _current_temperature = 0.0F;

  // used for managing tasks info
  TaskManager _task_manager;
};

}  // namespace bike_computer::static_scheduling

The implementation of most of the BikeSystem class is given below:

BikeSystem implementation (partial)
static_scheduling/bike_system.cpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file bike_system.cpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Bike System implementation (static scheduling)
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

#include "bike_system.hpp"

// std
#include <chrono>

// zephyr

// zpp_lib
#include "zpp_include/this_thread.hpp"
#include "zpp_include/time.hpp"
#include "zpp_include/utils.hpp"
#include "zpp_include/work_queue.hpp"
#include "zpp_include/zpp_assert.hpp"
#include "zpp_include/zpp_log.hpp"

// common
#include "common/ttce.hpp"

ZPP_LOG_MODULE_DECLARE(bike_computer, CONFIG_APP_LOG_LEVEL);

namespace bike_computer::static_scheduling {

// The complexity is increased by zephyr macros
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
zpp_lib::ZephyrResult BikeSystem::start() {
  ZPP_LOG_INF("Starting Super-Loop without event handling");

  zpp_lib::Utils::log_threads_summary();

  auto res = initialize();
  if (!res) {
    ZPP_LOG_ERR("Init failed: %d", (int)res.error());
    return res;
  }

  ZPP_LOG_DBG("Starting super-loop");

  // initialize the task manager phase
  _task_manager.initialize_phase();

  while (true) {
#if CONFIG_APP_LOG_LEVEL_DEBUG
    auto start_time = zpp_lib::Time::get_uptime();
#endif  // CONFIG_APP_LOG_LEVEL_DEBUG

    // TODO(Student): implement calls to different tasks based on computed schedule

    // register the time at the end of the cyclic schedule period and print the
    // elapsed time for the period
#if CONFIG_APP_LOG_LEVEL_DEBUG
    std::chrono::microseconds end_time = zpp_lib::Time::get_uptime();
    auto cycle                         = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
    ZPP_LOG_DBG("Repeating cycle time is %" PRIu64 " milliseconds", cycle.count());
#endif  // CONFIG_APP_LOG_LEVEL_DEBUG

    if (_stop_flag.load()) {
      break;
    }

#ifdef CONFIG_CPU_LOAD
    zpp_lib::Utils::log_cpu_load();
#endif
  }
  return res;
}

void BikeSystem::stop() {
  _stop_flag.store(true);
}

zpp_lib::ZephyrResult BikeSystem::initialize() {
  // initialize the display
  auto res = _bike_display.initialize();
  if (!res) {
    ZPP_LOG_ERR("Cannot initialize display: %d", (int)res.error());
    return res;
  }

  // initialize the sensor device
  res = _sensor_device.initialize();
  if (!res) {
    ZPP_LOG_ERR("Sensor not present or initialization failed: %d", (int)res.error());
  }

  return {};
}

void BikeSystem::gear_task() {
  // gear task
  _task_manager.register_task_start(TaskManager::TaskType::GearTaskType);

  // no need to protect access to data members (single threaded)
  _current_gear      = _gear_device.get_current_gear();
  _current_gear_size = _gear_device.get_current_gear_size();

  _task_manager.simulate_computation_time(TaskManager::TaskType::GearTaskType, kAllowSleep);
}

void BikeSystem::speed_distance_task() {
  // speed and distance task
  _task_manager.register_task_start(TaskManager::TaskType::SpeedTaskType);

  auto pedal_rotation_time = _pedal_device.get_current_rotation_time();
  _speedometer.set_current_pedal_rotation_time(pedal_rotation_time);
  _speedometer.set_gear_size(_current_gear_size);
  // no need to protect access to data members (single threaded)
  _current_speed     = _speedometer.get_current_speed();
  _traveled_distance = _speedometer.get_traveled_distance();

  _task_manager.simulate_computation_time(TaskManager::TaskType::SpeedTaskType, kAllowSleep);
}

void BikeSystem::temperature_task() {
  _task_manager.register_task_start(TaskManager::TaskType::TemperatureTaskType);

  // no need to protect access to data members (single threaded)
  zpp_lib::ZephyrResult res = _sensor_device.read_temperature(_current_temperature);
  if (!res) {
    ZPP_LOG_ERR("Cannot read temperature: %d", (int)res.error());
  }

  // simulate task computation by waiting for the required task computation time
  _task_manager.simulate_computation_time(TaskManager::TaskType::TemperatureTaskType, kAllowSleep);
}

void BikeSystem::reset_task() {
  _task_manager.register_task_start(TaskManager::TaskType::ResetTaskType);

  if (_reset_device.check_reset()) {
#if CONFIG_APP_LOG_LEVEL >= CONFIG_LOG_LEVEL_INFO
    std::chrono::microseconds response_time = // TODO(Student): compute response time
    ZPP_LOG_INF("Reset task: response time is %" PRIu64 " usecs", response_time.count());
#endif  // CONFIG_APP_LOG_LEVEL >= CONFIG_LOG_LEVEL_INFO
    _speedometer.reset();
  }

  _task_manager.simulate_computation_time(TaskManager::TaskType::ResetTaskType, kAllowSleep);
}

void BikeSystem::display_task1() {
  _task_manager.register_task_start(TaskManager::TaskType::DisplayTask1Type);

  // TODO(Student): update gear, speed and distance displayed on screen

  _task_manager.simulate_computation_time(TaskManager::TaskType::DisplayTask1Type, kAllowSleep);
}

void BikeSystem::display_task2() {
  _task_manager.register_task_start(TaskManager::TaskType::DisplayTask2Type);

  // TODO(Student): update temperature on screen

  _task_manager.simulate_computation_time(TaskManager::TaskType::DisplayTask2Type, kAllowSleep);
}

}  // namespace bike_computer::static_scheduling

First understand how the class is written, what the data members are and what the methods do. Then, complete the class implementation marked as TODO in the code.

Integration of your BikeSystem class in your main() function

Your main() function must create a BikeSystem instance on the stack and it must start it as shown below:

bike_computer/src/main.cpp
#if CONFIG_BIKE_COMPUTER_STATIC_SCHEDULING
  bike_computer::static_scheduling::BikeSystem bike_system;
#endif

  bikeSystem.start();
Note that the program will never return from the start() method call and that the bike_computer program will thus be executed by the main thread.

The CONFIG_BIKE_COMPUTER_STATIC_SCHEDULING configuration parameter must be defined in the application Kconfig file as follows:

bike_computer/Kconfig
menu "Zephyr"
source "Kconfig.zephyr"
endmenu

module = APP
module-str = APP
source "subsys/logging/Kconfig.template.log_config"

config BIKE_COMPUTER_STATIC_SCHEDULING
  bool "Build system with static scheduling"
  default n
  help
    This option must be enabled to use the static scheduling version.

Code Instrumentation to Measure Effective Task Periods and Computation Times

To verify that period and computation times are correct, the tasks must be instrumented. At the start of each method representing a task, such as the BikeSystem::gear_task() method, the current time is registered by calling _taskManager.register_task_start(). Then, the task runs, after which the method calls the _taskManager.simulate_computation_time() method. This registers task time information in the TaskManager instance.

By default, the TaskManager class does not perform any logging, because logging has a significant impact on the system. If you wish to add logging to better understand the program behavior at the time of conception, you may do the following:

  • Add the following lines in the Kconfig file:

    bike_computer/Kconfig
    config LOG_TASK_TIMES
      bool "Log task times for debugging purposes"
      default n
      help
        This option must be enabled to log task times with the TaskManager class
        This option must be disabled for test and production
    

  • Create a prj_log_task_times.conf file with the following content

    bike_computer/prj_log_task_times.conf
    # enable logging of task times in TaskManager class
    CONFIG_LOG_TASK_TIMES=y
    

  • Build your application with the following command:

    just build bike_computer log_app+gpio+display+sensor+phase_a prj_log_yes prj_log_task_times.conf
    
    This command build the application with logging of task times enabled.

  • Flash your board.

You should see an output similar to the one below on the console:

Log with the various task times

```txt title=”Serial Terminal”

[00:00:01.025,360] <dbg> bike_computer: start: Starting super-loop
[00:00:01.131,896] <dbg> bike_computer: simulate_computation_time: Task Gear: start time 0 (bounds 0 - 700000), computation time 100006
[00:00:01.344,421] <dbg> bike_computer: simulate_computation_time: Task Speed: start time 112488 (bounds 0 - 200000), computation time 200042
[00:00:01.457,427] <dbg> bike_computer: simulate_computation_time: Task Reset: start time 325531 (bounds 0 - 700000), computation time 100006
[00:00:01.670,440] <dbg> bike_computer: simulate_computation_time: Task Display(1): start time 438537 (bounds 0 - 1400000), computation time 200013
[00:00:01.884,002] <dbg> bike_computer: simulate_computation_time: Task Speed: start time 652069 (bounds 0 - 200000), computation time 200012
[00:00:01.997,009] <dbg> bike_computer: simulate_computation_time: Task Gear: start time 865112 (bounds 0 - 700000), computation time 100006
[00:00:02.209,960] <dbg> bike_computer: simulate_computation_time: Task Speed: start time 978027 (bounds 0 - 200000), computation time 200012
[00:00:02.322,937] <dbg> bike_computer: simulate_computation_time: Task Temperature: start time 1191040 (bounds 0 - 1500000), computation time 100006
[00:00:02.436,645] <dbg> bike_computer: simulate_computation_time: Task Display(2): start time 1304748 (bounds 0 - 1500000), computation time 100006
[00:00:02.650,299] <dbg> bike_computer: simulate_computation_time: Task Speed: start time 1418365 (bounds 0 - 200000), computation time 200013
[00:00:02.763,366] <dbg> bike_computer: simulate_computation_time: Task Reset: start time 1631470 (bounds 0 - 700000), computation time 100006
[00:00:02.776,458] <dbg> bike_computer: start: Repeating cycle time is 1744 milliseconds
```

From this log, one can confirm that:

  • The Major Cycle or Repeating Cycle Time is close to the expected \(1600\,\mathsf{ms}\). The overshoot is caused by logging.
  • The periods and computation times of the Gear, Speed, Temperature and Reset tasks are close to expected, with overshoot also caused by logging.
  • The Display task is splitted into 2 subtasks, with correct periods for a total computation time of \(300\,\mathsf{ms}\), as expected.

From this experiment, it is clear that logging information affects computation times, so logging should be disabled to test that computation and period times are respected. This is confirmed as follows:

  • Build your application without task logging with:

    just build bike_computer log_app+gpio+display+sensor+phase_a
    

  • Flash your board.

With this configuration, you should see an output similar to the one below on the console:

Log with the various task times
Serial Terminal
[00:00:01.024,261] <dbg> bike_computer: start: Starting super-loop
[00:00:02.630,920] <dbg> bike_computer: start: Repeating cycle time is 1600 milliseconds
[00:00:04.239,440] <dbg> bike_computer: start: Repeating cycle time is 1600 milliseconds
[00:00:05.847,961] <dbg> bike_computer: start: Repeating cycle time is 1600 milliseconds
[00:00:07.456,481] <dbg> bike_computer: start: Repeating cycle time is 1600 milliseconds

As you can see, the Major Cycle is now \(1600\,\mathsf{ms}\). The overshoot caused by logging has disappeared. Note that in test mode, logging is entirely disabled, as explained below.

Run the Test Program

To check the implementation of the BikeSystem::start() method, you must successfully run the test program available here. This program validates that the tasks run at the correct periods with the correct computation times.

It is important to note the following points regarding Zephyr RTOS test programs and their configurations:

  • Zephyr RTOS test programs can be built and flashed as standard Zephyr RTOS programs, for instance with the command

    just build bike_computer/tests/bike_system_part1/ gpio+sensor+display+test+phase_a
    
    If you flash your board, the test program will run. The main difference as compared with a twister run is that test results are not collected and that the application simply hangs at the end of the tests.

  • When running test programs with twister, configuration parameters cannot be passed through the command line. Instead configuration parameters are specified in the testcase.yaml file. For this test program, the testcase.yaml specifies a number of configuration files to be used with extra_conf_files and a number of arguments specific to each platform on which tests are run with extra_args.

  • With twister, a prj.conf file must be present at the root directory of the test program. Since we use default application parameters by adding the prj.conf file from deps/zpp_lib/configs/prj.conf, the prj.conf file of the test program is left empty.

  • The support for display cannot be specified in the twister command and must also be specified in the testcase.yaml file.

To run the test, you must execute the following command:

just test bike_computer/tests/bike_computer_part1 "my_map_file.yaml"
Recall that any serial connection to your board must be closed before running the test.

Expected Test Results

When the CONFIG_TEST configuration is enabled, the TaskManager class verifies that the computation times and periods of tasks are correct. If any of these values are outside the expected range, an assertion error is generated. To account for variations due to imprecision and extra statements, the TaskManager class allows for a variation of kAllowedDelta. This constant is set to 100 ticks.

Given that the CPU utilization factor is 1, the test will fail after a certain amount of time. This is expected, and the goal is to determine how long it takes for the test to fail. Modify test duration accordingly so that the test succeed on the different platforms. The test duration can be adjusted in the testcase.yaml file by modifying the value for each platform. It is expected that the test will fail earlier on a physical board than on an emulated device. If the test fails before reaching a duration of 20s, you must identify and resolve any scheduling errors. To run this test successfully, logging must be disabled entirely.

The expected results demonstrate that static scheduling is sensitive to CPU load.

Code Instrumentation to Measure the Reset Response Time

In theBikeSystem::reset_task() method, the speedometer resets when the user presses the button. You must also compute the task response time in this method (marked as TODO(Student)). The response time is the time elapsed between the button being pressed (or, more precisely, the button press being detected) and the reset request being handled.

After implementing the response time calculation, you can build the application with


Flash your device. Then press the button and observe the different response times obtained from this computation. If you do so, you should observe traces on the console similar to:

Log with multiple response times for the reset task

Serial Monitor
...
[00:00:02.634,307] <inf> bike_computer: Reset task: response time is 348968 usecs
...
[00:00:05.859,222] <inf> bike_computer: Reset task: response time is 1005402 usecs
...
[00:00:09.084,228] <inf> bike_computer: Reset task: response time is 360504 usecs

When running the program, you should also ensure that the push button is pressed for long enough to allow the event to be detected in the main program.

Question 1: Response time of the reset event

When running multiple tests to compute the response time of the reset event, you should observe the following:

  • There is a large variation in the response time values, from a few milliseconds to hundreds of milliseconds.
  • If you do not press long enough on the push button, the event may be missed and no reset happens.

Based on the program itself and on the task scheduling, explain these two behaviors. Explain also why such behaviors may be problematic.

Software Architecture

The class documented in this codelab follow the follow class architecture:

Class Diagram Class Diagram for the bike_computer program

Make sure that you have not deviated from this architecture. Please note that this class diagram does not document the constructors and method arguments.

Limitations of this Implementation

Varying and large response times are a problem that needs to be solved. In the next part of the codelab, we will implement an asynchronous button press mechanism to allow instantaneous handling of the event. We will then start making the next version of the program event-driven!

Wrap-Up

Before moving to the next implementation of the bike_computer program in the next codelab, make sure that you have accomplished the following:

  • All required classes are implemented and functional.
  • The main function is implemented and functional.
  • There is no significant deviations from the class architecture as documented above.
  • The three test programs (test_sensor_device, test_speedometer and test_bike_system_part1) run successfully, as documented in the codelab.
  • The pre-commit configuration file has been modified to include the required additional hooks. All software quality tools succeed, changes are committed and pushed to your repository.

Deliverables

Deliverables/Requirements (Project Phase B)

The deliverables/requirements for project Phase B include the following:

  • The implementation of the bike_computer using a a Timeline Cyclic Scheduling algorithm with a Super Loop mechanism is functional. All related files are available in the static_scheduling folder and all related code is developed under the static_scheduling namespace.
  • A diagram showing how tasks are scheduled during one major cycle is added to your repository and included in the README file of the project.

  • The developed code is compliant to the clang-tidy rules and just run-clang-tidy bike_computer log+debug+gpio+display+sensor+phase_a runs successfully.

  • The following hooks are added to the pre-commit configuration:

    • id: clang-tidy-bike-computer
    • id: clang-tidy-bike-computer-speedometer
    • id: clang-tidy-bike-computer-sensor-device
    • id: clang-tidy-bike-computer-bike-system-part1

    pre-commit runs successfully.

  • The bike_computer/tests/bike_system_part1 test runs successfully on your board and on qemu_x86. The CONFIG_TEST_DURATION_IN_MS parameter have been estimated and updated in the bike_computer/tests/bike_system_part1/testcase.yaml file. The ci/applications_for_build.yaml and ci/applications_for_test.yaml files are updated. The following commands run successfully:

    • just test bike_computer/tests/bike_system_part1 "you_map_file.yaml"
    • just test-qemu bike_computer/tests/bike_system_part1
    • just test-all "your_map_file.yaml", including the bike_computer/tests/bike_system_part1 test.
    • just test-all-qemu, including the bike_computer/tests/bike_system_part1 test.

    All build and tests pass on the GitHub repository.

  • The answer to question 1 is documented in the README.md file.