Skip to content

Bike Computer Part II

Introduction

Event-driven model

In part 1 of this codelab, we have implemented a version of the bike computer program that uses cyclic static scheduling using a Super-Loop mechanism. This implementation used polling for checking the button status. The limitations of this approach have been demonstrated and the need for an event-driven approach has been made clear.

In this part of the codelab, we will study how to implement an event-driven model for non periodic tasks. We will compare the event response time to the ones obtained in part 1 of the codelab.

Before starting the work on this codelab, make sure you carefully read the deliverable sections of Project Phase 2.

What you’ll build

In this codelab, you’re going to program a simple bike computer program that is handling non periodic events using an event-driven approach, with static cyclic scheduling of the periodic tasks.

What you’ll learn

  • How to use and log statistics about CPU usage.
  • How to use the Work and WorkQueue API (from zpp_lib) for programming periodic tasks.
  • How to develop a simple embedded program using an event-driven approach.

What you’ll need

  • The Zephyr Development Environment for developing and debugging C++ code snippets.
  • The Bike computer - part I codelab is a prerequisite to this codelab.

Start from BikeComputer Part 1

To implement the next version of the BikeComputer, start from the version developed in Part 1 of the codelab:

  • Copy the code from the “static_scheduling” folder into a new subfolder named “static_scheduling_with_event”. The code in this new folder will contain the changes described in this codelab.

  • Modify the namespace in all classes from static_scheduling to static_scheduling_with_event.

  • Modify your main program to use this new implementation (e.g. include path or namespace usage).

  • Compile and run your BikeComputer program. The result should be the same as in Part 1.

Implement the BikeComputer program using Time-Triggered Cyclic Executive Scheduling

The first change compared to Part 1 relates to the implementation of cyclic scheduling. Rather than using a Super-Loop, the program will now be implemented using TTCE Cyclic Scheduling. Task scheduling remains the same as that implemented in the Super-Loop, but will now be triggered using timers.

The TTCE mechanism is implemented using the TTCE class below:

bike_computer/src/common/ttce.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 ttce.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief TTCE implementation
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

#pragma once

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

// std
#include <chrono>
#include <string>

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

namespace bike_computer {

template <typename F, uint16_t NbrOfMinorCycles, uint16_t MaxMinorCycleSize>
class TTCE : private zpp_lib::NonCopyable<TTCE<F, NbrOfMinorCycles, MaxMinorCycleSize>> {
 public:
  explicit TTCE(std::chrono::milliseconds minorCycle) : _minorCycle(minorCycle) {
    k_timer_init(&_timer, &TTCE::_thunk, nullptr);
    // specify this instance as user data
    // this cast is ugly but the only way to pass a reference to this instance to the
    // timer
    // cppcheck-suppress cstyleCast
    _timer.user_data = (void*)this;  // NOLINT(readability/casting)
    k_work_init(&_work, &TTCE::_workHandler);
    // initialize the work queue
    k_work_queue_init(&_workQueue);
  }

  void start() {
    // first start the timer
    k_timeout_t period = zpp_lib::milliseconds_to_ticks(_minorCycle);
    k_timer_start(&_timer, period, period);

    // then run the work queue
    struct k_work_queue_config cfg = {
        .name     = "TTCE Work Queue",
        .no_yield = true,
    };
    _isStarted = true;
    k_work_queue_run(&_workQueue, &cfg);
  }

  void stop() {
    if (!_isStarted) {
      return;
    }
    // first stop the time
    k_timer_stop(&_timer);
    // drain the work queue
    auto ret = k_work_queue_drain(&_workQueue, true);
    if (ret < 0) {
      __ASSERT(false, "k_work_queue_drain failed with code %d", ret);
    }
    ret = k_work_queue_stop(&_workQueue, K_SECONDS(1));
    if (ret != 0) {
      __ASSERT(false, "k_work_queue_stop failed with code %d", ret);
    }
    _isStarted = false;
  }

  bool isStarted() { return _isStarted; }

  void addInitialTask(F f) { _initialTask = f; }

  [[nodiscard]] zpp_lib::ZephyrResult addTask(uint16_t minorCycleIndex, F f) {
    zpp_lib::ZephyrResult res;
    if (minorCycleIndex >= NbrOfMinorCycles) {
      __ASSERT(false, "Invalid minor cycle index %d", minorCycleIndex);
      res.assign_error(zpp_lib::ZephyrErrorCode::k_inval);
      return res;
    }
    if (_nbrOfTasksInMinorCycle[minorCycleIndex] >= MaxMinorCycleSize) {
      __ASSERT(false,
               "Too many tasks in minor cycle %d: %d",
               minorCycleIndex,
               _nbrOfTasksInMinorCycle[minorCycleIndex] + 1);
      res.assign_error(zpp_lib::ZephyrErrorCode::k_inval);
      return res;
    }

    _tasks[minorCycleIndex][_nbrOfTasksInMinorCycle[minorCycleIndex]] = f;
    _nbrOfTasksInMinorCycle[minorCycleIndex]++;

    return res;
  }

 private:
  static void _thunk(struct k_timer* timer_id) {
    // submit the periodic TTCE task
    if (timer_id != nullptr) {
      // get instance from user data
      // this cast is ugly but the only way to pass a reference to this instance to the
      // timer
      // cppcheck-suppress cstyleCast
      TTCE* pTTCE = (TTCE*)timer_id->user_data;  // NOLINT(readability/casting)
      auto ret    = k_work_submit_to_queue(&pTTCE->_workQueue, &pTTCE->_work);
      if (ret != 0 && ret != 1 && ret != 2) {
        __ASSERT(false, "Failed to submit work: %d", ret);
        return;
      }
    }
  }

  static void _workHandler(struct k_work* item) {
    // this ugly casting is the simplest way of getting the information
    // we need in the _workHandler method
    // CASTING IS POSSIBLE ONLY WHEN k_work IS THE FIRST ATTRIBUTE IN THE CLASS
    // cppcheck-suppress dangerousTypeCast
    TTCE* pTTCE = (TTCE*)item;  // NOLINT(readability/casting)

    // if an initial task is set, execute it and result it
    if (pTTCE->_initialTask != nullptr) {
      pTTCE->_initialTask();
      pTTCE->_initialTask = nullptr;
    }

    // execute tasks based on schedule table
    for (uint16_t taskIndex = 0; taskIndex < MaxMinorCycleSize; taskIndex++) {
      if (pTTCE->_tasks[pTTCE->_minorCycleIndex][taskIndex] != nullptr) {
        pTTCE->_tasks[pTTCE->_minorCycleIndex][taskIndex]();
      }
    }
    pTTCE->_minorCycleIndex = (pTTCE->_minorCycleIndex + 1) % NbrOfMinorCycles;
  }

  // _work MUST be the first attribute
  struct k_work _work;
  struct k_work_q _workQueue;
  bool _isStarted = false;
  struct k_timer _timer;
  std::chrono::milliseconds _minorCycle;
  uint16_t _minorCycleIndex                          = 0;
  F _tasks[NbrOfMinorCycles][MaxMinorCycleSize]      = {nullptr};
  uint16_t _nbrOfTasksInMinorCycle[NbrOfMinorCycles] = {0};
  F _initialTask                                     = nullptr;
};

}  // namespace bike_computer

To understand how the TTCE class works, you need to understand the following:

  • TTCE is a template class with three template parameters:

    • typename F is a type template parameter. F is a callable type meaning that one can call f() when f is an instance of type F.
    • uint16_t NbrOfMinorCycles is a constant template parameter, which specifies the number of minor cycles in the major cycle.
    • uint16_t MaxMinorCycleSize is a constant template parameter, which specifies the maximum number of tasks in each minor cycle.
  • The TTCE constructor performs the following actions:

    • It initializes the _minorCycle data member. This data member represents the duration of the minor cycle in milliseconds.
    • It initializes the _timer data member. The timer will call the TTCE::_thunk static method at regular time intervals. The timer is started in the TTCE::start() method. The k_timer data structure contains a user_data field that is used here to store a reference to the TTCE instance.
    • It initializes the _work data member. It allows to set the handler function that will be called when a work is submitted to a work queue. Here, the handler function is the static method TTCE::_workHandler. See below the description of the TTCE::_thunk() and TTCE::_workHandler() methods.
    • It initializes the _workQueue data member.
  • The TTCE::start() method performs the following actions:

    • It starts the timer at the _minorCycle period.
    • It runs the _workQueue work queue. It is important to note that the work queue will be run forever and that the k_work_queue_run call will not return, unless the work queue is stopped by calling the TTCE::stop() method. This means that the thread executing the TTCE::start() method will run the work queue forever and will not execute anything else. TTCE::stop() must be executed from another thread.
  • The TTCE::stop() method performs the following actions:

    • It stops the timer _timer.
    • It drains and stops the work queue _workQueue.
    • As stated above, it is important to note that the TTCE::stop() method must be called from a different thread than the thread calling TTCE::start().
  • The TTCE::addTask() method enables the addition of a task to a specific minor cycle. The task is specified as an instance of F. Tasks are added in FIFO order.

  • The private TTCE::_thunk() method is called by the timer and it performs the following actions:

    • It gets the TTCE instance from the user_data void pointer. This pointer is set in the class constructor.
    • Using the TTCE instance, it does submit the work to the work queue. This will invoke the _workHandler static method.
  • The private TTCE::_workHandler() method is invoked by the work queue and it performs the following actions:

    • It gets the TTCE instance from the k_work* parameter that is passed as parameter. Each time the work queue invokes the work handler attached to the work item, a pointer to the work item is passed to the work handler function. Since _work is the first attribute, it is possible to cast the work item pointer to a pointer to the containing class.
  • It is important to note that the C-style castings used in the TTCE class are used because no more robust and elegant solution exists. This is imposed by the Zephyr RTOS timer and work queue APIs.

Before proceeding further, ensure that you understand the basic principles of the TTCE class. The next step is to integrate it into your BikeComputer::start() method to replace the Super-Loop mechanism, as follows:

  • In C++, a class template is not a compilation unit in itself; it needs to be instantiated. This means that you must declare a variable with the template parameters specified. The code below demonstrates the declaration of the variable gTTCE that instantiates the template class TTCE. In this instance, the template parameter F is defined as a function with the signature void(), and kNbrOfMinorCycles and kMaxNbrOfTasksInMinorCycles are constants that define the TTCE parameters.

    using VoidFunction = std::function<void()>;
    static TTCE<VoidFunction, kNbrOfMinorCycles, kMaxNbrOfTasksInMinorCycles> gTTCE(kMinorCycle);
    
    You must define such a variable with the correct TTCE parameters.

  • After creating the variable, replace each call to a task function with a call to TTCE::addTask. The parameter F to be passed to the addTask() method is the task function applied to the BikeSystem instance, which is declared as std::bind(&BikeSystem::gearTask, this) for instance. For correct calls to TTCE::addTask, the task distribution over minor cycles needs to be known and applied correctly.

  • After adding all the tasks, initialize the task manager phase using the _taskManager.initializePhase() method and start your TTCE instance.

At this point, your BikeComputer program should now behave in the same way as it did with the Super-Loop implementation.

Use zpp_lib::Utils::logCPULoad() to Log CPU statistics

In order to understand the behaviour of the BikeComputer program, it is helpful to understand its CPU load. The zpp_lib library provides a wrapper to the Zephyr RTOS cpu_load_get() function to compute and log cpu load in the console. You may call this wrapper function with:

#ifdef CONFIG_CPU_LOAD
#include "zpp_include/utils.hpp"
#endif
...
#ifdef CONFIG_CPU_LOAD
zpp_lib::Utils::logCPULoad();
#endif
Note that you must also modify your prj.conf file to define the CONFIG_CPU_LOAD=y symbol. With the TTCE implementation, the call to zpp_lib::Utils::logCPULoad() must be implemented by adding one additional task to one minor cycle.

Question 1

If you log CPU statistics at the end of every major cycle (in the super-loop), what CPU usage do you observe? How can you explain the observed CPU uptime?

Solution

You should observe an uptime time of about 99-100%. The reason is simple: with the current implementation we poll constantly for checking for button status and we perform busy waiting loops for matching the periods of the different tasks. Given that the CPU utilization factor is 1, the CPU load must be around 100%.

Reduce CPU usage with sleep calls rather than busy wait

To match the expected task periods, the TaskManager::simulateComputationTime() method implements a busy wait mechanism. With this mechanism, the CPU remains active while waiting for a given time to elapse. This results in higher-than-necessary CPU usage. This can be improved by replacing the busy wait implementation with calls to the zpp_lib::ThisThread::sleep_for()method. The parameter passed to the method should specify the remaining time to be spent in the method specified as std::chrono::milliseconds.

Question 2

If you run the program after the change from busy wait to sleep calls, what CPU usage do you observe? How can you explain the observed CPU uptime?

Solution

You should observe an uptime time of about 75%. The reason is simple: the temperature and display tasks have a cumulated execution time of \(400\,\mathsf{ms}\). Given that the remaining execution time is each method is slightly smaller, this represents 25% of the major cycle time of \(1600\,\mathsf{ms}\). This thus reduces the CPU usage from approx. 99% to approx. 75%. Given that displaying information on the LCD display costs an overhead, you should see CPU usage of approx. 80-85%.

Test your TTCE Implementation

To test your TTCE implementation, add the test program given below in the “bike_computer/tests/bike_computer/bike_system_part2” folder and run the corresponding west twister command. The test program now consists of two test cases and the two test cases should run successfully.

test TTCE Implementation
bike_computer/tests/bike_computer/bike_system_part2/src/test_bike_system_part2.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_bike_system_part2.cpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Test program for the BikeSystem class (codelab part 2)
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

// zephyr
#include <zephyr/logging/log.h>
#include <zephyr/ztest.h>

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

// zpp_lib
#include "zpp_include/this_thread.hpp"
#include "zpp_include/thread.hpp"

// bike computer
#include "static_scheduling_with_event/bike_system.hpp"

LOG_MODULE_REGISTER(bike_system, CONFIG_APP_LOG_LEVEL);

// for ms or s literals
using namespace std::literals;

// Different modes
// nrf5340, busy: 48s
// nrf5340, sleep: 48s
// qemu_x86, busy: tested up to 120s
// qemy_x86, sleep: tested up to 120s
static constexpr std::chrono::milliseconds testDuration = 48s;

// test_bike_system_event_queue handler function
ZTEST(bike_system_part2, test_bike_system_ttce) {
  // create the BikeSystem instance
  static bike_computer::static_scheduling_with_event::BikeSystem bikeSystem;

  // run the bike system in a separate thread
  zpp_lib::Thread thread(zpp_lib::PreemptableThreadPriority::PriorityNormal,
                         "Test BS TTCE");
  auto res = thread.start(std::bind(
      &bike_computer::static_scheduling_with_event::BikeSystem::start, &bikeSystem));
  zassert_true(res, "Could not start thread");

  // let the bike system run for the test duration
  zpp_lib::ThisThread::sleep_for(testDuration);

  // stop the bike system
  bikeSystem.stop();

  res = thread.join();
  zassert_true(res, "Could not join thread");
}

ZTEST_SUITE(bike_system_part2, NULL, NULL, NULL, NULL, NULL);

It is expected that the test fails after a given time. You must establish the test duration before failure and document it. Note that this duration may be different depending on the target device and you must choose the smallest duration - the one with which the test succeeds on all platforms.

Static Scheduling Event-Driven Implementation

One of the major drawback of the implementation of our BikeComputer program so far, is that events can be missed. Moreover, polling continuously for detecting the button state consumes useless CPU time.

On embedded systems, this can be improved by handling events using interrupts. Interrupts are implemented using special dedicated hardware in the MCU that detects the event and runs an Interrupt Service Routine (ISR) in response. For handling asynchronous events that are by nature non-predictable, using interrupts is the most appropriate approach:

  • An interrupt-based routine only runs the code when it is called/initiated by the interrupt-service routine. This routine directly executes the addressed code, sometimes without any CPU interaction.
  • Therefore, it is fully scalable, because it is independent from the number of monitored events. In other words, the interrupt supervision doesn’t consume CPU time.
  • Using interrupts, the controller is prevented from doing unnecessary tasks by operating in a passive mode for as long as possible, responding mostly just to events and triggers.

When the interrupt trigger occurs, the processor does some hard-wired processing and then executes the ISR, including return-from-interrupt processing at the end, as depicted in the figure below.

Interrupt processing

Interrupt processing

Using the zpp_lib InterruptIn API for detecting an event on an input pin is very easy. to detect an event on an input pin is easy. Instead of polling for input status periodically and continuously, you only need to create an instance of the InterruptIn class and trigger an event when a digital input pin changes. You can trigger interrupts on the falling edge of signals by setting a callback function with the InterruptIn::fall() method. Note that the InterruptIn class is a template class, and the template parameter is the PinName of the button. In this context, PinName is an enum class type that defines the four buttons present on your nrf5340dk/nrf5340/cpuapp device.

Implement the BikeComputer Program using an Event-driven Approach

To implement a version of your program with an event-driven detection of the button press, you must apply some changes to the program developed in the part I of the codelab. The major change is that you must register a function that is called upon button press.

The steps for applying the changes are:

  • Modify the ResetDevice class so that it sets an internal flag upon button press. The checkReset() method used by the BikeComputer class must be modified accordingly. Be careful in implementing the internal state that is modified in a ISR context and can be queried from another thread.

Note that you must also modify the GearDevice and PedalDevice classes similarly for an event-driven implementation. Instead of polling to check the button state at regular intervals, register an event handler using the Button::fall() method. This will enable you to change the gear (GearDevice) or the pedal rotation speed (PedalDevice) without polling.

Implementation note for all device classes

For all device classes, the button fallback methods remain internal to each class and modify the device’s internal state. These classes continue to provide ‘getter’ methods for accessing this internal state.

Should internal state attributes be protected against concurrent access

In principle, attributes that can be modified in ISR routines and accessed from other threads should be protected. This is important because the attribute could be modified by the ISR at any time and in particular when a thread has started to read the attribute. In some situations, it may not be necessary to do so, if all following conditions are met:

  • The attribute is a single atomic machine word (e.g., uint32_t on a 32-bit MCU).
  • The ISR only writes/reads the attribute.
  • The thread only reads/writes the attribute.
  • The variable is declared atomic_t or volatile, or you access it with Zephyr RTOS’s atomic_*() APIs.

When these conditions are met, nested interrupts (e.g., a higher priority ISR interrupts a lower priority ISR) are not an issue. In particular, in our scenario, only one ISR can modify the shared data.

About the use of std::atomic<T>

Alternatively, one can use std::atomic<T> rather than Zephyr RTOS’s atomic_*() APIs. However, you need to consider that std::atomic<T> works correctly as long as:

  • T is a type that the underlying architecture supports for lock-free atomics (e.g., uint32_t on a 32-bit MCU). This can be checked with the std::atomic<T>::is_always_lock_free() function. For instance, under Zephyr RTOS, std::atomic<uint64_t> is not implemented for 32-bit architectures.
  • Your ISR and threads only use atomic stores/loads or atomic operations (no non-atomic accesses).
  • You use relaxed / acquire / release memory orders correctly. This means that when protecting data access with a flag, then the order is which data is written/accessed must be guaranted such as in
    // in ISR
    data = 123; // normal write
    flag.store(true, std::memory_order_release); // Prevents stores after this operation
    // in thread
    if (flag.load(std::memory_order_acquire)) { // Prevents loads before this operation
      auto x = data; // guaranteed correct
    }
    
  • Note that by default the memory order to std::atomic<T> methods is memory_order_seq_cst, which guarantees the strongest ordering but is also the slowest.

Once all classes have been changed accordingly, you may compile and test your application.

Run the test program

Once you have implemented all changes documented above, you can run the “bike_system_part2” test program again. Check whether the test duration before failure and document the changes.

Question 3

If you run the static_scheduling_with_event program, what CPU usage do you observe? How can you explain the observed CPU uptime?

Solution

The uptime should go down from approx 75% to about 1%. The explanation is simple: the GearDevice, PedalDevice and ResetDevice do NOT poll for button status anymore. The total polling time per major cycle was \(100\,\mathsf{ms} * 2\) + \(200\,\mathsf{ms} * 4\) + \(200\,\mathsf{ms} * 2\) for a total of \(1200\,\mathsf{ms}\). We thus reduce CPU usage by approx 75%! Again, displaying values on the screen costs some CPU time and an overhead of approx. 8-10% may be observed.

Volatile data

At this stage, it is important to introduce the concept of volatile data. Volatile data is created in your program by adding the volatile keyword to a variable declaration. This is sometimes required because compilers assume that variables in memory don’t change spontaneously, and optimize the code based on that belief. The reasons for optimizing the code are:

  • Don’t reload a variable from memory if the current function hasn’t changed it.
  • Read a variable from memory into a register for faster access.
  • Write back to memory at the end of the procedure, or before a procedure call, or when the compiler runs out of free registers, but not before, for saving memory write.

This optimization can fail in some circumstances. For example, while reading from input port or polling for key press using while (status);. The compiler may generate code that reads from status only once and reuse that value. This will generate an infinite loop triggered by status being true.

The variables for which it fails in all circumstances are:

  • Memory-mapped peripheral register: register changes on its own.
  • Global variables modified by an ISR: the ISR changes the variable.
  • Global variables in a multithreaded application: another thread or ISR changes the variable.

This is the reason why you should declare the reset variable modified in the ISR as volatile. However, Zephyr RTOS’s atomic_*() functions do not expect volatile parameters and as documented above it is sufficient to use these atomic_*() functions.

Static scheduling with interrupts

This implementation of the bike computer program can be described as cyclic static scheduling with interrupts. It essentially has two operating modes:

  • The main application code runs in the foreground and schedule all period tasks as expected. Some period tasks check for device status stored in global volatile memory for executing portions of the main application code.
  • The interrupt service routines run in the background with high priority. These routines are executed when an event is triggered, they handle the most urgent work and modify global volatile variables that will be queried for processing in the main application code.

This implementation is an improvement over the implementation that does not use events, since it guarantees that no event is ever missed.

Question 4: Response time of the reset event

When you run multiple tests for computing the response time of the reset event, what do you observe? Is there an improvement as compared to the static_scheduling::BikeSystem implementation?

Based on the program itself and on the task scheduling, explain the observed behavior.

Solution

The improvement is that you can no longer miss a keystroke, as the event is registered in interrupts in any case. However, there is no improvement in response time, and there is still a wide range of response time values, from a few milliseconds to about \(800\,\mathsf{ms}\). The explanation is that the reset task is still run as a periodic task with a period of \(800\,\mathsf{ms}\).

There is thus a clear need for more dynamic scheduling, which can account for task importance, improve response times and allow for more dynamic behaviors.

Wrap-Up

At the end of this codelab, make sure that you have accomplished the following:

  • The BikeSystem class has replace busy waits with sleep calls.
  • The ResetDevice, GearDevice and PedalDevice classes has been modified as specified, to support event-driven handling of the buttons.
  • The BikeSystem class has been modified for registering ISRs and handling events.
  • You have run all tests successfully.
  • You have answered all questions.
  • The pre-commit configuration file has been modified to include all files added in this codelab. All software quality tools succeed, changes are committed and pushed to your repository.