Adds heartbeat functionality using GPIO

Introduces a heartbeat mechanism to the robot node using GPIO.
This allows for external monitoring of the node's active state.

The heartbeat is implemented using a GPIO pin that toggles
at a regular interval, indicating that the system is running.

Updates CMakeLists to link the project with gpiod libraries
staged locally.
Adds .gitignore entries for local dependencies and PC build directories.
This commit is contained in:
Russell Gilbert 2026-02-08 12:55:44 +00:00
parent 6a8f4f364b
commit 957f789faa
6 changed files with 62 additions and 39 deletions

View file

@ -38,4 +38,10 @@
*.app *.app
# debug information files # debug information files
*.dwo *.dwo
# Local dependencies
extern/
# PC-side build directories
cmake-build-*/

View file

@ -1,5 +1,4 @@
# Sets the minimum CMake version required to handle modern C++ features cmake_minimum_required(VERSION 3.10)
cmake_minimum_required(VERSION 3.10) # Version 4.1 doesn't exist yet (standard is 3.x)
# Defines the project name # Defines the project name
project(Dixon) project(Dixon)
@ -7,14 +6,9 @@ project(Dixon)
# Forces the compiler to use C++20 features # Forces the compiler to use C++20 features
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
# Load the PkgConfig tool to find libraries automatically
find_package(PkgConfig REQUIRED)
# Search for libgpiod v2.x C++ bindings
pkg_check_modules(GPIOD REQUIRED libgpiodcxx)
# Define the executable and its source files # Define the executable and its source files
add_executable(Dixon main.cpp add_executable(Dixon
main.cpp
DixonNodeState.cpp DixonNodeState.cpp
DixonNodeState.h DixonNodeState.h
DixonBrain.cpp DixonBrain.cpp
@ -22,8 +16,14 @@ add_executable(Dixon main.cpp
CardioCenter/Heart.cpp CardioCenter/Heart.cpp
CardioCenter/Heart.h) CardioCenter/Heart.h)
# Add the Include directories found by PkgConfig (where the .hpp files live) # Header Path: Use the local v2.1 headers staged in the project
target_include_directories(Dixon PRIVATE ${GPIOD_INCLUDE_DIRS}) target_include_directories(Dixon PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/extern/libgpiod/include")
# Link the specific libraries found by PkgConfig
target_link_libraries(Dixon PRIVATE ${GPIOD_LIBRARIES}) # Link the gpiod libraries
target_link_libraries(Dixon PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/extern/libgpiod/aarch64/libgpiodcxx.so.2"
"${CMAKE_CURRENT_SOURCE_DIR}/extern/libgpiod/aarch64/libgpiod.so.3")
# Tell the linker: "Trust me, the Pi has the rest of the C++ and C libraries"
target_link_options(Dixon PRIVATE "-Wl,--allow-shlib-undefined")

View file

@ -3,33 +3,40 @@
namespace cardio namespace cardio
{ {
Heart::Heart(const char* chipName, unsigned int lineOffset) Heart::Heart(const char* chipName, unsigned int lineOffset)
: _isOn(false), : chip_(std::string("/dev/") + chipName),
_request(setup_request(chipName, lineOffset)) isOn_(false),
_lastBeat(std::chrono::steady_clock::now()),
request_(get_line_request(chip_, lineOffset))
{ {
} }
void Heart::beat() void Heart::beat()
{ {
_isOn = !_isOn; const auto now = std::chrono::steady_clock::now();
auto val = _isOn ? gpiod::line::value::ACTIVE : gpiod::line::value::INACTIVE; if (const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>
(now - _lastBeat); elapsed.count() >= 500)
_request.set_value(HEART_LINE_INDEX, val); {
isOn_ = !isOn_;
const auto val = isOn_ ? gpiod::line::value::ACTIVE : gpiod::line::value::INACTIVE;
request_.set_value(DEFAULT_HEART_PIN, val);
_lastBeat = now;
}
} }
gpiod::line_request Heart::setup_request(const char* chipName, const unsigned int lineOffset) void Heart::stop()
{ {
std::string chipPath = std::string("/dev/") + chipName; isOn_ = false;
gpiod::chip chip(chipPath); request_.set_value(DEFAULT_HEART_PIN, gpiod::line::value::INACTIVE);
}
auto settings = gpiod::line_settings() gpiod::line_request Heart::get_line_request(gpiod::chip& chip, const unsigned int lineOffset)
.set_direction(gpiod::line::direction::OUTPUT); {
const auto settings = gpiod::line_settings()
.set_direction(gpiod::line::direction::OUTPUT);
auto line_cfg = gpiod::line_config(); return chip.prepare_request()
line_cfg.add_line_settings(lineOffset, settings); .add_line_settings(lineOffset, settings)
.do_request();
return chip.prepare_request() }
.set_line_config(line_cfg) }
.do_request();
}
}

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <gpiod.hpp> #include <gpiod.hpp>
#include <chrono>
namespace cardio namespace cardio
{ {
@ -9,13 +10,16 @@ namespace cardio
public: public:
explicit Heart(const char* chipName = GPIO_CHIP_NAME, unsigned int lineOffset = DEFAULT_HEART_PIN); explicit Heart(const char* chipName = GPIO_CHIP_NAME, unsigned int lineOffset = DEFAULT_HEART_PIN);
void beat(); void beat();
void stop();
private: private:
static gpiod::line_request get_line_request(gpiod::chip&, unsigned int lineOffset);
static constexpr auto GPIO_CHIP_NAME = "gpiochip0"; static constexpr auto GPIO_CHIP_NAME = "gpiochip0";
static constexpr unsigned int DEFAULT_HEART_PIN = 17; static constexpr unsigned int DEFAULT_HEART_PIN = 17;
static constexpr unsigned int HEART_LINE_INDEX = 0; static constexpr unsigned int HEART_LINE_INDEX = 0;
static gpiod::line_request setup_request(const char* chipName, unsigned int lineOffset); std::chrono::steady_clock::time_point _lastBeat;
bool _isOn; gpiod::chip chip_;
gpiod::line_request _request; bool isOn_;
gpiod::line_request request_;
}; };
} }

View file

@ -4,7 +4,8 @@
#include <thread> #include <thread>
DixonBrain::DixonBrain() DixonBrain::DixonBrain()
: state_(DixonNodeState::instance()) : heart_(),
state_(DixonNodeState::instance())
{ {
std::cout << "DixonBrain initialised\n"; std::cout << "DixonBrain initialised\n";
} }
@ -37,6 +38,8 @@ void DixonBrain::stop()
if (loopThread_.joinable()) if (loopThread_.joinable())
loopThread_.join(); loopThread_.join();
heart_.stop();
std::cout << "DixonBrain stopped\n"; std::cout << "DixonBrain stopped\n";
} }
@ -44,6 +47,7 @@ void DixonBrain::runLoop()
{ {
while (state_.isBrainRunning()) while (state_.isBrainRunning())
{ {
heart_.beat();
// TODO: main control logic // TODO: main control logic
std::this_thread::sleep_for(std::chrono::milliseconds(10)); std::this_thread::sleep_for(std::chrono::milliseconds(10));
} }

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include "DixonNodeState.h" #include "DixonNodeState.h"
#include "CardioCenter/Heart.h"
#include <thread> #include <thread>
class DixonBrain class DixonBrain
@ -14,6 +15,7 @@ public:
private: private:
void runLoop(); void runLoop();
cardio::Heart heart_;
DixonNodeState& state_; DixonNodeState& state_;
std::thread loopThread_; std::thread loopThread_;
}; };