Adds basic logging and refactors main loop

Adds a basic logging system using std::clog with color codes for different log levels.
Refactors the main loop to use the logging system and improve readability.
Changes DixonNodeState to use atomic exchange for setting node status and logs status changes.
Removes the old main.cpp and adds a new one that uses a separate thread for signal handling.
Updates the CMakeLists.txt to link against gpiod and spdlog.
Moves version information into a version.txt file and creates a custom target to bump the version using a python script.
This commit is contained in:
Russell Gilbert 2026-02-20 15:27:42 +00:00
parent e8c3e0f8c2
commit 3a4e00a409
11 changed files with 140 additions and 44 deletions

View file

@ -1,7 +1,7 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Dixon-Attach" type="CLion_Remote" version="1" remoteCommand="tcp:Dixon1:9090" symbolFile="$PROJECT_DIR$/cmake-build-pi/dixon" sysroot=""> <configuration default="false" name="Dixon-Attach" type="CLion_Remote" version="1" remoteCommand="tcp:Dixon1:9090" symbolFile="$PROJECT_DIR$/cmake-build-pi5-debug/dixon" sysroot="">
<debugger kind="GDB" isBundled="true" /> <debugger kind="GDB" isBundled="true" />
<pathMapping remote=":/mnt/C/source/Dixon/src/Robotnode" local="$PROJECT_DIR$" /> <pathMapping remote=":$USER_HOME$/dixon" local="$PROJECT_DIR$" />
<method v="2" /> <method v="2" />
</configuration> </configuration>
</component> </component>

View file

@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.10) cmake_minimum_required(VERSION 3.10)
# 1. Define the Version Bumper Target # Define the Version Bumper Target
# This target is like a "recipe" that isn't cooked until we ask for it. # This target is like a "recipe" that isn't cooked until we ask for it.
add_custom_target(BumpVersion add_custom_target(BumpVersion
COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/bump_version.py COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/bump_version.py
@ -8,7 +8,7 @@ add_custom_target(BumpVersion
COMMENT "Bumping version in version.txt..." COMMENT "Bumping version in version.txt..."
) )
# 2. Read the version for the Project metadata # Read the version for the Project metadata
# This still happens during "Reload" so CMake knows the project version. # This still happens during "Reload" so CMake knows the project version.
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/version.txt" RAW_VERSION) file(READ "${CMAKE_CURRENT_SOURCE_DIR}/version.txt" RAW_VERSION)
string(STRIP "${RAW_VERSION}" DIXON_VERSION_FROM_FILE) string(STRIP "${RAW_VERSION}" DIXON_VERSION_FROM_FILE)
@ -29,9 +29,10 @@ add_executable(dixon
DixonBrain.cpp DixonBrain.cpp
DixonBrain.h DixonBrain.h
CardioCenter/Heart.cpp CardioCenter/Heart.cpp
CardioCenter/Heart.h) CardioCenter/Heart.h
Logging/DixonLogger.h)
# 3. Create the Dependency # Create the Dependency
# This tells CMake: "Before you build Dixon, you MUST run BumpVersion." # This tells CMake: "Before you build Dixon, you MUST run BumpVersion."
add_dependencies(dixon BumpVersion) add_dependencies(dixon BumpVersion)
@ -41,15 +42,9 @@ configure_file(Version.h.in "${CMAKE_CURRENT_BINARY_DIR}/Version.h")
# Consolidated Include Paths # Consolidated Include Paths
target_include_directories(dixon PRIVATE target_include_directories(dixon PRIVATE
"${CMAKE_CURRENT_BINARY_DIR}" # For generated Version.h "${CMAKE_CURRENT_BINARY_DIR}" # For generated Version.h
# "${CMAKE_CURRENT_SOURCE_DIR}/extern/libgpiod/include" # For libgpiod headers
) )
# Link the gpiod libraries # Link the gpiod & spdlog 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")
# Link the gpiod libraries
target_link_libraries(dixon PRIVATE target_link_libraries(dixon PRIVATE
gpiodcxx gpiodcxx
gpiod) gpiod)

View file

@ -32,6 +32,7 @@ namespace cardio
void Heart::stop() void Heart::stop()
{ {
std::cout << "Heart::stop() called.\n";
isOn_ = false; isOn_ = false;
request_.set_value(DEFAULT_HEART_PIN, gpiod::line::value::INACTIVE); request_.set_value(DEFAULT_HEART_PIN, gpiod::line::value::INACTIVE);
} }

View file

@ -2,18 +2,20 @@
#include <iostream> #include <iostream>
#include <chrono> #include <chrono>
#include <thread> #include <thread>
#include "Logging/DixonLogger.h"
DixonBrain::DixonBrain() DixonBrain::DixonBrain()
: heart_(), : heart_(),
state_(DixonNodeState::instance()) state_(DixonNodeState::instance()),
logger_("DixonBrain")
{ {
std::cout << "DixonBrain initialised\n"; logger_.info("DixonBrain created.");
} }
DixonBrain::~DixonBrain() DixonBrain::~DixonBrain()
{ {
stop(); stop();
std::cout << "DixonBrain destroyed\n"; logger_.info("DixonBrain destroyed.");
} }
void DixonBrain::start() void DixonBrain::start()
@ -25,7 +27,7 @@ void DixonBrain::start()
loopThread_ = std::thread(&DixonBrain::runLoop, this); loopThread_ = std::thread(&DixonBrain::runLoop, this);
std::cout << "DixonBrain started\n"; logger_.info("DixonBrain started.");
} }
void DixonBrain::stop() void DixonBrain::stop()
@ -35,7 +37,6 @@ void DixonBrain::stop()
state_.setNodeStatus(NodeStatus::Stopping); state_.setNodeStatus(NodeStatus::Stopping);
if (loopThread_.joinable()) if (loopThread_.joinable())
loopThread_.join(); loopThread_.join();
@ -43,7 +44,7 @@ void DixonBrain::stop()
state_.setNodeStatus(NodeStatus::Stopped); state_.setNodeStatus(NodeStatus::Stopped);
std::cout << "DixonBrain stopped\n"; logger_.info("DixonBrain stopped.");
} }
void DixonBrain::runLoop() void DixonBrain::runLoop()

View file

@ -1,7 +1,8 @@
#pragma once #pragma once
#include <thread>
#include "DixonNodeState.h" #include "DixonNodeState.h"
#include "CardioCenter/Heart.h" #include "CardioCenter/Heart.h"
#include <thread> #include "Logging/DixonLogger.h"
class DixonBrain class DixonBrain
{ {
@ -15,7 +16,9 @@ public:
private: private:
void runLoop(); void runLoop();
cardio::Heart heart_; cardio::Heart heart_;
DixonNodeState& state_; DixonNodeState& state_;
std::thread loopThread_; std::thread loopThread_;
Dixon::DixonLogger logger_;
}; };

View file

@ -1,12 +1,15 @@
#include "DixonNodeState.h" #include "DixonNodeState.h"
#include <iostream>
#include <ostream>
DixonNodeState& DixonNodeState::instance() DixonNodeState& DixonNodeState::instance()
{ {
static DixonNodeState instance; static DixonNodeState instance;
return instance; return instance;
} }
void DixonNodeState::setConnected(bool value) void DixonNodeState::setConnected(const bool value)
{ {
connected_ = value; connected_ = value;
} }
@ -18,10 +21,14 @@ bool DixonNodeState::isConnected() const
void DixonNodeState::setNodeStatus(NodeStatus value) void DixonNodeState::setNodeStatus(NodeStatus value)
{ {
node_status_ = value; NodeStatus oldStatus = node_status_.exchange(value);
logger_.info("Node status changed: {} -> {}",
static_cast<int>(oldStatus),
static_cast<int>(value));
} }
NodeStatus DixonNodeState::getNodeStatus() const NodeStatus DixonNodeState::getNodeStatus() const
{ {
return node_status_; return node_status_.load();
} }

View file

@ -1,10 +1,12 @@
#pragma once #pragma once
#include<atomic> #include<atomic>
#include "Logging/DixonLogger.h"
enum class NodeStatus : int { enum class NodeStatus : int {
Starting, Starting,
Running, Running,
StopPending,
Stopping, Stopping,
Stopped Stopped
}; };
@ -26,9 +28,11 @@ public:
DixonNodeState& operator=(DixonNodeState&&) = delete; DixonNodeState& operator=(DixonNodeState&&) = delete;
private: private:
DixonNodeState() = default; DixonNodeState() : logger_("DixonNodeState"){}
~DixonNodeState() = default; ~DixonNodeState() = default;
Dixon::DixonLogger logger_;
std::atomic<bool> connected_ = false; std::atomic<bool> connected_ = false;
std::atomic<NodeStatus> node_status_ = NodeStatus::Stopped; std::atomic<NodeStatus> node_status_ = NodeStatus::Stopped;
}; };

View file

@ -0,0 +1,59 @@
#pragma once
#include <iostream>
#include <format>
#include <mutex>
#include <chrono>
#include <string_view>
namespace Dixon {
class DixonLogger {
private:
std::string_view m_name;
// Internal helper to keep the code DRY (Don't Repeat Yourself)
template<typename... Args>
void log_output(std::string_view level, std::string_view color, std::string_view fmt, Args&&... args) const {
try {
auto now = std::chrono::system_clock::now();
std::string message = std::vformat(fmt, std::make_format_args(args...));
static std::mutex logMutex;
std::lock_guard<std::mutex> lock(logMutex);
// \033[0m resets the color back to normal at the end
std::clog << std::format("[{:%T}] [{}] [{}{}{}] {}\n",
now, m_name, color, level, "\033[0m", message);
} catch (const std::exception& e) {
std::cerr << "Log Error: " << e.what() << std::endl;
}
}
public:
explicit DixonLogger(std::string_view name) : m_name(name) {}
template<typename... Args>
void debug(std::string_view fmt, Args&&... args) const {
log_output("DEBUG", "\033[36m", fmt, std::forward<Args>(args)...); // Cyan
}
template<typename... Args>
void info(std::string_view fmt, Args&&... args) const {
log_output("INFO", "\033[32m", fmt, std::forward<Args>(args)...); // Green
}
template<typename... Args>
void warn(std::string_view fmt, Args&&... args) const {
log_output("WARN", "\033[33m", fmt, std::forward<Args>(args)...); // Yellow
}
template<typename... Args>
void error(std::string_view fmt, Args&&... args) const {
log_output("ERROR", "\033[31m", fmt, std::forward<Args>(args)...); // Red
}
template<typename... Args>
void critical(std::string_view fmt, Args&&... args) const {
log_output("CRITICAL", "\033[1;31;47m", fmt, std::forward<Args>(args)...); // Bold Red on White
}
};
}

View file

@ -2,26 +2,43 @@
#include <iostream> #include <iostream>
#include "DixonBrain.h" #include "DixonBrain.h"
#include "Version.h" // The generated header #include "Version.h" // The generated header
#include "Logging/DixonLogger.h"
void signal_thread_func(sigset_t set) {
void signal_handler(int signal) { int sig;
if (signal == SIGINT || signal == SIGTERM) { // Loop so we can catch multiple signals if the shutdown takes a moment
std::cout << "\nShutdown signal received (" << signal << ")." << std::endl; while (DixonNodeState::instance().getNodeStatus() != NodeStatus::Stopping) {
if (sigwait(&set, &sig) == 0) {
// Use your atomic flag to tell the brain to stop looping std::cout << "\nShutdown signal received (" << sig << ")." << std::endl;
DixonNodeState::instance().setNodeStatus(NodeStatus::Stopping); DixonNodeState::instance().setNodeStatus(NodeStatus::Stopping);
// After setting 'Stopping', the loop will exit on next check
} }
}
}
sigset_t prepare_signal_set()
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGTERM);
return set;
} }
int main() int main()
{ {
// The "Splash Screen" Dixon::DixonLogger logger("main");
std::cout << "Starting Dixon v" << DIXON_VERSION << "...\n";
std::signal(SIGINT, signal_handler); // The "Splash Screen"
std::signal(SIGTERM, signal_handler); logger.info("Dixon core is starting. Version: {}", "1.x.x");
const sigset_t set = prepare_signal_set();
pthread_sigmask(SIG_BLOCK, &set, nullptr);
std::thread sig_thread(signal_thread_func, set);
sig_thread.detach(); // Let it run independently
// Create the brain controller // Create the brain controller
DixonBrain brain; DixonBrain brain;
@ -29,23 +46,32 @@ int main()
// Start the brain's control loop // Start the brain's control loop
brain.start(); brain.start();
std::cout << "Dixon is alive...\n"; logger.info("Dixon core is alive.");
const DixonNodeState& state = DixonNodeState::instance();
auto count = 1; auto count = 1;
while (DixonNodeState::instance().getNodeStatus() == NodeStatus::Running) while (state.getNodeStatus() != NodeStatus::Stopped)
{ {
if (state.getNodeStatus() == NodeStatus::StopPending)
{
logger.info("calling 'stop' on Dixon Brain.");
brain.stop();
}
count++; count++;
if (count>10) if (count>100)
{ {
count = 0; count = 0;
std::cout << "Dixon state is " << static_cast<int>( DixonNodeState::instance().getNodeStatus()) << "\n"; auto const currentState = static_cast<int>(DixonNodeState::instance().getNodeStatus());
logger.info("Dixon is alive, state is {}.", currentState);
//log->log(spdlog::level::info, "Dixon state is {}",
//static_cast<int>(D);
} }
std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::this_thread::sleep_for(std::chrono::milliseconds(100));
} }
// Stop the brain cleanly
brain.stop(); logger.info("Dixon has left the building.");
std::cout << "Dixon has left the building...\n";
return 0; return 0;
} }

View file

@ -1 +1 @@
1.0.33 1.0.47

View file

@ -2,7 +2,7 @@
# Configuration # Configuration
LOCAL_BIN="$HOME/dev/Dixon/src/RobotNode/cmake-build-pi5-debug/dixon" LOCAL_BIN="$HOME/dev/Dixon/src/RobotNode/cmake-build-pi5-debug/dixon"
REMOTE_TARGET="russellg59@192.168.1.224" REMOTE_TARGET="russellg59@dixon1"
REMOTE_DIR="/home/russellg59/dixon" REMOTE_DIR="/home/russellg59/dixon"
REMOTE_EXE="$REMOTE_DIR/dixon" REMOTE_EXE="$REMOTE_DIR/dixon"