Add CMake build and typed config with path resolution (remove hardcoded paths)

This commit is contained in:
pgdalmeida 2026-06-22 12:08:18 +02:00
parent 89be503edb
commit c4b24c0fbf
Signed by: pedro.almeida
GPG Key ID: D4A6C394DF13F1D7
12 changed files with 627 additions and 72 deletions

View File

@ -2,7 +2,10 @@
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(git add *)", "Bash(git add *)",
"Bash(grep -iE \"config\\\\.ini|\\\\.out|\\\\.o$|compile_commands\")" "Bash(grep -iE \"config\\\\.ini|\\\\.out|\\\\.o$|compile_commands\")",
"Bash(command -v cmake)",
"Bash(cmake --version)",
"Bash(cmake -B build -DWITH_VIMBA=OFF -DWITH_MQTT=OFF)"
] ]
} }
} }

117
CMakeLists.txt Normal file
View File

@ -0,0 +1,117 @@
cmake_minimum_required(VERSION 3.20)
project(fire_gimbal_control LANGUAGES C CXX)
# ---------------------------------------------------------------------------
# Options (runtime toggles still select mock vs real at launch; these control
# whether the heavy/proprietary dependencies are compiled in at all).
# ---------------------------------------------------------------------------
option(WITH_VIMBA "Build with Allied Vision Vimba X camera support" ON)
option(WITH_MQTT "Build with Eclipse Paho MQTT support" ON)
option(BUILD_TESTING "Build unit tests" OFF)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
# ---------------------------------------------------------------------------
# Always-required dependencies
# ---------------------------------------------------------------------------
find_package(Threads REQUIRED)
find_package(OpenCV REQUIRED COMPONENTS core highgui imgproc)
# Boost.Asio is header-only (Boost >= 1.69); only program_options is a compiled lib.
if(POLICY CMP0167)
cmake_policy(SET CMP0167 NEW)
endif()
find_package(Boost REQUIRED CONFIG COMPONENTS program_options)
# libjxl via pkg-config (Arch/Manjaro ship a .pc file)
find_package(PkgConfig REQUIRED)
pkg_check_modules(JXL REQUIRED IMPORTED_TARGET libjxl libjxl_threads)
# ---------------------------------------------------------------------------
# fgc_core: SDK-independent core (config, path resolution, and - as the
# refactor proceeds - logging, the capture scheduler and the I/O interfaces).
# Builds and unit-tests WITHOUT Vimba X or Paho, so the core logic is
# verifiable on any machine.
# ---------------------------------------------------------------------------
add_library(fgc_core STATIC
src/core/Config.cpp
src/core/Paths.cpp
ini.c
)
target_include_directories(fgc_core PUBLIC
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR} # for the bundled ini.h
)
target_link_libraries(fgc_core PUBLIC Threads::Threads)
# ---------------------------------------------------------------------------
# Application executable (current flat layout; restructured into src/ in a
# later phase). Pulls in the heavy/proprietary deps.
# ---------------------------------------------------------------------------
set(FGC_SOURCES
main.cpp
MQTT.cpp
Camera.cpp
)
add_executable(fire_gimbal_control ${FGC_SOURCES})
target_include_directories(fire_gimbal_control PRIVATE ${CMAKE_SOURCE_DIR})
target_link_libraries(fire_gimbal_control PRIVATE
fgc_core
${OpenCV_LIBS}
Boost::headers
Boost::program_options
PkgConfig::JXL
)
# ---------------------------------------------------------------------------
# Optional: MQTT (Eclipse Paho). Fetched + built from pinned, hash-verified
# upstream release tarballs (no AUR, no system install required).
# ---------------------------------------------------------------------------
if(WITH_MQTT)
include(cmake/Paho.cmake)
target_link_libraries(fire_gimbal_control PRIVATE PahoMqttCpp::paho-mqttpp3-static)
target_compile_definitions(fire_gimbal_control PRIVATE FGC_WITH_MQTT=1)
else()
target_compile_definitions(fire_gimbal_control PRIVATE FGC_WITH_MQTT=0)
message(STATUS "WITH_MQTT=OFF: building without MQTT support")
endif()
# ---------------------------------------------------------------------------
# Optional: Vimba X SDK (proprietary; install manually from Allied Vision).
# ---------------------------------------------------------------------------
if(WITH_VIMBA)
find_package(Vmb REQUIRED)
target_link_libraries(fire_gimbal_control PRIVATE Vmb::VmbC Vmb::VmbCPP)
target_compile_definitions(fire_gimbal_control PRIVATE FGC_WITH_VIMBA=1)
else()
target_compile_definitions(fire_gimbal_control PRIVATE FGC_WITH_VIMBA=0)
message(STATUS "WITH_VIMBA=OFF: building without Vimba X camera support (mock cameras only)")
endif()
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
if(BUILD_TESTING)
enable_testing()
add_subdirectory(tests)
endif()
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
message(STATUS "------------------------------------------------------------")
message(STATUS "fire_gimbal_control configuration")
message(STATUS " build type : ${CMAKE_BUILD_TYPE}")
message(STATUS " WITH_VIMBA : ${WITH_VIMBA}")
message(STATUS " WITH_MQTT : ${WITH_MQTT}")
message(STATUS " tests : ${BUILD_TESTING}")
message(STATUS "------------------------------------------------------------")

View File

@ -337,7 +337,6 @@ void VimbaHandler::SaveImage()
cv::imshow("Display Image", img); cv::imshow("Display Image", img);
cv::waitKey(10); cv::waitKey(10);
} }
std::string homedir = getenv("HOME");
std::string cameraname; std::string cameraname;
if (pFrame->getCamId() == 0) if (pFrame->getCamId() == 0)
cameraname = "RGB"; cameraname = "RGB";
@ -345,7 +344,7 @@ void VimbaHandler::SaveImage()
cameraname = "ACR"; cameraname = "ACR";
if (pFrame->getCamId() == 2) if (pFrame->getCamId() == 2)
cameraname = "NIR"; cameraname = "NIR";
std::string filename = homedir + "/projects/Fire_Gimbal_Control/bin/x64/Release/" + cameraname + "/" + std::to_string(pFrame->getTimestamp()) + ".jxl"; std::string filename = output_dir + "/" + cameraname + "/" + std::to_string(pFrame->getTimestamp()) + ".jxl";
std::filesystem::path path(filename); std::filesystem::path path(filename);
// Extract the directory part of the path // Extract the directory part of the path
std::filesystem::path dir = path.parent_path(); std::filesystem::path dir = path.parent_path();
@ -363,29 +362,9 @@ void VimbaHandler::SaveImage()
std::filesystem::copy("test_smoke.jxl", filename); std::filesystem::copy("test_smoke.jxl", filename);
} }
time.Reset(); time.Reset();
//cv::imwrite(filename, img, compression_params); // NOTE: optional network upload of saved images to the ground
std::string net_path = "/mnt/ggs-smb/FirewatchTowers/FWT_Podrosche/"; // station was removed here; it had hardcoded NFS/SMB paths. If
std::string backup_net_path = "/home/ggs/fwt_image_backup/"; // reintroduced it should read its destination from config.
//try {
// std::filesystem::copy_file(filename, net_path + filename);
// std::cout << "RGB Upload TIME:" << std::to_string(time.ElapsedMillis()) << std::endl;
//}
//catch (std::filesystem::filesystem_error& e)
//{
// std::cout << "Could not copy image to zkms: " << e.what() << '\n';
// std::cout << "Copy Backup to LattePanda Sigma" << '\n';
// try {
// std::filesystem::copy_file(filename, backup_net_path + filename);
// }
// catch (std::filesystem::filesystem_error& e)
// {
// std::cout << "Could not copy image to LattePanda Sigma: " << e.what() << '\n';
// }
//}
//std::filesystem::remove(filename);
std::string payload = "{ \"fwt\":\"" + fwt_name + "\" ,\"cam\":\"" + cameraname + "\", \"hdg\":" + std::to_string((int)(gimbal_data->hdg * 10)) + ", \"time\":" + std::to_string(pFrame->getTimestamp()) + " }"; std::string payload = "{ \"fwt\":\"" + fwt_name + "\" ,\"cam\":\"" + cameraname + "\", \"hdg\":" + std::to_string((int)(gimbal_data->hdg * 10)) + ", \"time\":" + std::to_string(pFrame->getTimestamp()) + " }";
mqtt_client->publish(mqtt_RGB, payload); mqtt_client->publish(mqtt_RGB, payload);
} }
@ -447,3 +426,8 @@ void VimbaHandler::SetTowerName(std::string name)
fwt_name = name; fwt_name = name;
mqtt_RGB = "GGS/FWT/" + fwt_name + "/CamEvent"; mqtt_RGB = "GGS/FWT/" + fwt_name + "/CamEvent";
} }
void VimbaHandler::SetOutputDir(std::string dir)
{
output_dir = std::move(dir);
}

View File

@ -78,6 +78,7 @@ public:
bool TriggerCamera(); bool TriggerCamera();
void TriggerSettle(); void TriggerSettle();
void SetTowerName(std::string name); void SetTowerName(std::string name);
void SetOutputDir(std::string dir);
bool cam_started = false; bool cam_started = false;
int queue_count_rgb = 0; int queue_count_rgb = 0;
@ -86,6 +87,7 @@ private:
double jxlq = 2.0; double jxlq = 2.0;
double jxle = 3.0; double jxle = 3.0;
std::string fwt_name = "Rietschen";//"Dev"; //TODO: put in config std::string fwt_name = "Rietschen";//"Dev"; //TODO: put in config
std::string output_dir; // base directory for saved images (set from config)
std::string mqtt_RGB = "GGS/FWT/" + fwt_name + "/CamEvent"; std::string mqtt_RGB = "GGS/FWT/" + fwt_name + "/CamEvent";
motor_info* gimbal_data; motor_info* gimbal_data;
MQTTClient* mqtt_client; MQTTClient* mqtt_client;

70
cmake/FindVmb.cmake Normal file
View File

@ -0,0 +1,70 @@
# FindVmb.cmake - locate the Allied Vision Vimba X SDK (VmbC + VmbCPP).
#
# The Vimba X SDK is proprietary and must be installed manually from Allied
# Vision (https://www.alliedvision.com/en/products/software/vimba-x-sdk/).
# It is NOT available through any package manager.
#
# Search hints (any one is enough):
# -DVMB_HOME=/opt/VimbaX_2024-1 (CMake cache variable)
# export VIMBA_X_HOME=/opt/VimbaX... (environment variable)
#
# Provides imported targets:
# Vmb::VmbC Vmb::VmbCPP
# and variables: Vmb_FOUND, Vmb_INCLUDE_DIR, Vmb_C_LIBRARY, Vmb_CPP_LIBRARY
set(_vmb_hints
"${VMB_HOME}"
"$ENV{VMB_HOME}"
"$ENV{VIMBA_X_HOME}"
"$ENV{VIMBAX_HOME}"
/opt/VimbaX
/opt/VimbaX_2025-1
/opt/VimbaX_2024-1
/opt/vimbax
/usr/local/VimbaX
)
find_path(Vmb_INCLUDE_DIR
NAMES VmbCPP/VmbCPP.h VmbC/VmbC.h
HINTS ${_vmb_hints}
PATH_SUFFIXES api/include include
)
find_library(Vmb_C_LIBRARY
NAMES VmbC
HINTS ${_vmb_hints}
PATH_SUFFIXES api/lib lib lib64
)
find_library(Vmb_CPP_LIBRARY
NAMES VmbCPP
HINTS ${_vmb_hints}
PATH_SUFFIXES api/lib lib lib64
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Vmb
REQUIRED_VARS Vmb_INCLUDE_DIR Vmb_C_LIBRARY Vmb_CPP_LIBRARY
FAIL_MESSAGE
"Vimba X SDK not found. Install it from Allied Vision and set -DVMB_HOME=<sdk_root> \
or the VIMBA_X_HOME environment variable. To build without camera support, \
configure with -DWITH_VIMBA=OFF."
)
if(Vmb_FOUND)
if(NOT TARGET Vmb::VmbC)
add_library(Vmb::VmbC UNKNOWN IMPORTED)
set_target_properties(Vmb::VmbC PROPERTIES
IMPORTED_LOCATION "${Vmb_C_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${Vmb_INCLUDE_DIR}")
endif()
if(NOT TARGET Vmb::VmbCPP)
add_library(Vmb::VmbCPP UNKNOWN IMPORTED)
set_target_properties(Vmb::VmbCPP PROPERTIES
IMPORTED_LOCATION "${Vmb_CPP_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${Vmb_INCLUDE_DIR}"
INTERFACE_LINK_LIBRARIES "Vmb::VmbC")
endif()
endif()
mark_as_advanced(Vmb_INCLUDE_DIR Vmb_C_LIBRARY Vmb_CPP_LIBRARY)

58
cmake/Paho.cmake Normal file
View File

@ -0,0 +1,58 @@
# Paho.cmake - provide Eclipse Paho MQTT C and C++ via FetchContent.
#
# Rationale (security): we build Paho from its OFFICIAL upstream Git repos
# rather than from the AUR. This removes the anonymous AUR packager from the
# trust chain - you trust only the Eclipse project source.
#
# HARDENING (recommended): the tags below are mutable refs. For maximum
# integrity, pin PAHO_C_TAG / PAHO_CPP_TAG to a full commit SHA (immutable),
# or switch to URL + URL_HASH (SHA256) of the signed release tarballs and
# verify the hash out-of-band. Bump the versions deliberately, not silently.
#
# NOTE: these exact tag strings and the cross-build wiring below must be
# verified on a machine with network access + cmake; they could not be built
# offline in the environment where this file was authored.
include(FetchContent)
set(PAHO_C_TAG "v1.3.14" CACHE STRING "Eclipse paho.mqtt.c git tag/commit")
set(PAHO_CPP_TAG "v1.4.1" CACHE STRING "Eclipse paho.mqtt.cpp git tag/commit")
# ---- Paho MQTT C (dependency of the C++ wrapper) ----
set(PAHO_BUILD_SHARED OFF CACHE BOOL "" FORCE)
set(PAHO_BUILD_STATIC ON CACHE BOOL "" FORCE)
set(PAHO_WITH_SSL OFF CACHE BOOL "" FORCE) # current code uses plain tcp://
set(PAHO_ENABLE_TESTING OFF CACHE BOOL "" FORCE)
set(PAHO_BUILD_SAMPLES OFF CACHE BOOL "" FORCE)
set(PAHO_BUILD_DOCUMENTATION OFF CACHE BOOL "" FORCE)
FetchContent_Declare(paho_mqtt_c
GIT_REPOSITORY https://github.com/eclipse-paho/paho.mqtt.c.git
GIT_TAG ${PAHO_C_TAG}
GIT_SHALLOW TRUE
)
# ---- Paho MQTT C++ ----
set(PAHO_BUILD_CPP_SHARED OFF CACHE BOOL "" FORCE)
set(PAHO_BUILD_CPP_STATIC ON CACHE BOOL "" FORCE)
FetchContent_Declare(paho_mqtt_cpp
GIT_REPOSITORY https://github.com/eclipse-paho/paho.mqtt.cpp.git
GIT_TAG ${PAHO_CPP_TAG}
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(paho_mqtt_c paho_mqtt_cpp)
# Provide a stable alias the top-level CMakeLists links against, regardless of
# the exact target name the upstream version exports.
if(NOT TARGET PahoMqttCpp::paho-mqttpp3-static)
if(TARGET paho-mqttpp3-static)
add_library(PahoMqttCpp::paho-mqttpp3-static ALIAS paho-mqttpp3-static)
elseif(TARGET paho-mqttpp3)
add_library(PahoMqttCpp::paho-mqttpp3-static ALIAS paho-mqttpp3)
else()
message(FATAL_ERROR "Paho C++ static target not found after FetchContent; "
"check PAHO_CPP_TAG and upstream target names.")
endif()
endif()

View File

@ -29,3 +29,23 @@ id_Cam1 = DEV_0000000000
id_Cam2 = id_Cam2 =
id_Cam3 = id_Cam3 =
id_Cam4 = id_Cam4 =
[Serial]
; Motor-controller serial device and baud rate.
device = /dev/ttyACM0
baud = 115200
[Paths]
; Directory for saved .jxl images. Supports leading ~ and $ENV expansion.
; If blank, defaults to $XDG_DATA_HOME/fire_gimbal_control/images
; (i.e. ~/.local/share/fire_gimbal_control/images).
output_dir =
[Features]
; Enable/disable subsystems and select mock (simulated) implementations.
; CLI flags (e.g. --no-mqtt, --mock-camera, --mock-serial) override these.
enable_mqtt = true
enable_camera = true
enable_serial = true
mock_camera = false
mock_serial = false

76
include/fgc/Config.h Normal file
View File

@ -0,0 +1,76 @@
#pragma once
#include <map>
#include <string>
#include <vector>
namespace fgc {
// Typed application configuration. Replaces the ad-hoc std::map<string,string>
// lookups that were scattered through main.cpp.
struct GeneralConfig {
std::string tower_name = "Unnamed";
int image_interval = 5; // seconds between captures
bool debug = false; // print motor telemetry each loop tick
};
struct NetworkConfig {
std::string broker_ip = "127.0.0.1"; // MQTT broker / ZKMS server
std::string mqtt_user; // see secrets note below
std::string mqtt_pw;
};
struct SerialConfig {
std::string device = "/dev/ttyACM0";
unsigned int baud = 115200;
};
struct CameraConfig {
std::vector<std::string> ids; // GigE IP or USB DEV_ id, in order
std::vector<std::string> labels = {"RGB", "ACR", "NIR"}; // index -> output subfolder
};
struct PathsConfig {
// Where captured .jxl images are written. Supports leading ~ and $ENV
// expansion. Empty => resolved to a sensible default at load time.
std::string output_dir;
};
struct FeaturesConfig {
bool enable_mqtt = true;
bool enable_camera = true;
bool enable_serial = true;
bool mock_camera = false; // use a simulated camera instead of Vimba X
bool mock_serial = false; // use a simulated motor controller
};
struct AppConfig {
GeneralConfig general;
NetworkConfig network;
SerialConfig serial;
CameraConfig camera;
PathsConfig paths;
FeaturesConfig features;
// Capture rate in images/second (derived from general.image_interval).
double image_rate() const;
};
// Loads and validates configuration.
//
// Secrets: mqtt_user / mqtt_pw are taken from the environment variables
// FGC_MQTT_USER / FGC_MQTT_PW when present; the INI values are a fallback.
class ConfigLoader {
public:
// Reads the INI file at `path` (via the bundled inih parser), maps it into
// a typed AppConfig, applies environment overrides, and validates it.
// Throws std::runtime_error with a clear message on failure.
static AppConfig loadFromFile(const std::string& path);
// Same mapping/validation logic, but from an already-parsed key->value map
// ("Section.name" => value). Exposed for unit testing without file IO.
static AppConfig fromMap(const std::map<std::string, std::string>& kv);
};
} // namespace fgc

29
include/fgc/Paths.h Normal file
View File

@ -0,0 +1,29 @@
#pragma once
#include <optional>
#include <string>
#include <vector>
namespace fgc::paths {
// Expand a leading "~" (to $HOME) and any "$VAR" / "${VAR}" environment
// references in `path`. Returns the expanded path (unchanged if nothing to do).
std::string expandUser(const std::string& path);
// Absolute directory containing the running executable (via /proc/self/exe).
// Returns empty string if it cannot be determined.
std::string executableDir();
// The ordered list of locations searched for a config file, given an optional
// explicit --config argument. Exposed so callers can report what was tried.
std::vector<std::string> configSearchPaths(const std::string& cliArg = "");
// Resolve the config file path using configSearchPaths(): the first existing,
// readable file wins. Returns nullopt if none is found.
std::optional<std::string> resolveConfigPath(const std::string& cliArg = "");
// Default image output directory when none is configured
// ($XDG_DATA_HOME/fire_gimbal_control/images, else ~/.local/share/...).
std::string defaultOutputDir();
} // namespace fgc::paths

View File

@ -10,7 +10,8 @@
#include "Camera.h" #include "Camera.h"
#include "MQTT.h" #include "MQTT.h"
#include "timing.h" #include "timing.h"
#include "ini.h" #include "fgc/Config.h"
#include "fgc/Paths.h"
namespace po = boost::program_options; namespace po = boost::program_options;
@ -31,14 +32,6 @@ mqtt_sub_data mqtt_in;
MQTTClient* mqtt_client = nullptr; MQTTClient* mqtt_client = nullptr;
std::vector<std::string> camera_id_vec; //= { "192.168.11.101" , "192.168.11.102", "192.168.11.103" }; //{ "DEV_1AB22C04F7A9" }; //{ "192.168.11.101" , "192.168.11.102", "192.168.11.103" }; std::vector<std::string> camera_id_vec; //= { "192.168.11.101" , "192.168.11.102", "192.168.11.103" }; //{ "DEV_1AB22C04F7A9" }; //{ "192.168.11.101" , "192.168.11.102", "192.168.11.103" };
// Callback function for parsing
static int handler(void* user, const char* section, const char* name, const char* value) {
auto* config = static_cast<std::map<std::string, std::string>*>(user);
std::string key = std::string(section) + "." + std::string(name);
(*config)[key] = value;
return 1; // Return 0 to stop parsing on error
}
void readInput() { void readInput() {
std::string input; std::string input;
while (running) { while (running) {
@ -60,44 +53,42 @@ void readInput() {
int main(int argc, char* argv[]) int main(int argc, char* argv[])
{ {
//ini handling with ini.h // Resolve the config file from a search order (no hardcoded paths):
std::map<std::string, std::string> config; // --config (added in a later phase) -> $FGC_CONFIG -> ./config.ini
// Parse the INI file // -> <exe dir>/config.ini -> $XDG_CONFIG_HOME/fire_gimbal_control/config.ini
if (ini_parse("/home/ggs/projects/Fire_Gimbal_Control/bin/x64/Release/config.ini", handler, &config) < 0) { auto config_path = fgc::paths::resolveConfigPath();
std::cerr << "Can't load config.ini file!" << std::endl; if (!config_path) {
std::cerr << "No config.ini found. Searched:\n";
for (const auto& p : fgc::paths::configSearchPaths())
std::cerr << " - " << p << "\n";
std::cerr << "Copy config/config.example.ini to one of these locations "
"or set $FGC_CONFIG." << std::endl;
return 1; return 1;
} }
// Access configuration values
std::string tower_name = config["General.tower_name"]; fgc::AppConfig cfg;
int rate = std::stoi(config["General.image_interval"]); try {
int debug = std::stoi(config["General.debug"]); cfg = fgc::ConfigLoader::loadFromFile(*config_path);
std::string zkms_server_ip = config["Network.zkms_server_ip"]; } catch (const std::exception& e) {
std::string mqtt_user = config["Network.mqtt_user"]; std::cerr << "Config error (" << *config_path << "): " << e.what() << std::endl;
std::string mqtt_pw = config["Network.mqtt_pw"]; return 1;
std::string cam_id1 = config["Camera.id_Cam1"]; }
std::string cam_id2 = config["Camera.id_Cam2"];
std::string cam_id3 = config["Camera.id_Cam3"]; // Bridge typed config into the existing globals/locals.
std::string cam_id4 = config["Camera.id_Cam4"]; camera_id_vec = cfg.camera.ids;
if (cam_id1 != "") fwt_name = cfg.general.tower_name;
camera_id_vec.push_back(cam_id1); imagerate = cfg.image_rate();
if (cam_id2 != "") motorctl_info_out = cfg.general.debug;
camera_id_vec.push_back(cam_id2); server_ip = cfg.network.broker_ip;
if (cam_id3 != "") std::string mqtt_user = cfg.network.mqtt_user;
camera_id_vec.push_back(cam_id3); std::string mqtt_pw = cfg.network.mqtt_pw;
if (cam_id4 != "")
camera_id_vec.push_back(cam_id4); std::cout << "Loaded config: " << *config_path << "\n";
std::cout << camera_id_vec.size() << " camera(s) should be connected according the config file." << "\n"; std::cout << camera_id_vec.size() << " camera(s) configured.\n";
std::cout << "tower_name: " << fwt_name << "\n";
//set globals std::cout << "image_interval: " << cfg.general.image_interval << " s\n";
fwt_name = tower_name; std::cout << "debug: " << cfg.general.debug << "\n";
imagerate = 1/(double)rate; std::cout << "broker: " << server_ip << std::endl;
motorctl_info_out = (bool)debug;
server_ip = zkms_server_ip;
// Print the parsed values
std::cout << "tower_name: " << tower_name << std::endl;
std::cout << "rate: " << rate << std::endl;
std::cout << "debug: " << debug << std::endl;
std::cout << "zkms_server_ip: " << zkms_server_ip << std::endl;
//args //args
@ -152,7 +143,7 @@ int main(int argc, char* argv[])
return 1; return 1;
} }
//main vars //main vars
SerialPort serial("/dev/ttyACM0", 115200); SerialPort serial(cfg.serial.device, cfg.serial.baud);
std::thread io_thread; std::thread io_thread;
const std::string mqtt_STATUS = "GGS/FWT/" + fwt_name + "/StatusCode"; const std::string mqtt_STATUS = "GGS/FWT/" + fwt_name + "/StatusCode";
@ -216,6 +207,7 @@ int main(int argc, char* argv[])
//Camera Obj //Camera Obj
VimbaHandler cam_handler(camera_id_vec, mqtt_client, &act_info, start_demo); VimbaHandler cam_handler(camera_id_vec, mqtt_client, &act_info, start_demo);
cam_handler.SetTowerName(fwt_name); cam_handler.SetTowerName(fwt_name);
cam_handler.SetOutputDir(cfg.paths.output_dir);
//init Gimbal //init Gimbal
if (init_gimbal) { if (init_gimbal) {
std::this_thread::sleep_for(std::chrono::milliseconds(500)); std::this_thread::sleep_for(std::chrono::milliseconds(500));

109
src/core/Config.cpp Normal file
View File

@ -0,0 +1,109 @@
#include "fgc/Config.h"
#include "fgc/Paths.h"
#include <cstdlib>
#include <stdexcept>
extern "C" {
#include "ini.h"
}
namespace fgc {
namespace {
std::string get(const std::map<std::string, std::string>& kv, const std::string& key,
const std::string& fallback = "") {
auto it = kv.find(key);
return (it != kv.end() && !it->second.empty()) ? it->second : fallback;
}
int getInt(const std::map<std::string, std::string>& kv, const std::string& key, int fallback) {
auto it = kv.find(key);
if (it == kv.end() || it->second.empty()) return fallback;
try {
return std::stoi(it->second);
} catch (const std::exception&) {
throw std::runtime_error("Config key '" + key + "' must be an integer, got '" +
it->second + "'");
}
}
bool getBool(const std::map<std::string, std::string>& kv, const std::string& key, bool fallback) {
auto it = kv.find(key);
if (it == kv.end() || it->second.empty()) return fallback;
const std::string& v = it->second;
if (v == "1" || v == "true" || v == "yes" || v == "on") return true;
if (v == "0" || v == "false" || v == "no" || v == "off") return false;
throw std::runtime_error("Config key '" + key + "' must be a boolean, got '" + v + "'");
}
std::string envOr(const char* name, const std::string& fallback) {
const char* v = std::getenv(name);
return (v && *v) ? std::string(v) : fallback;
}
// inih callback: flatten "Section.name" => value into the map.
int iniHandler(void* user, const char* section, const char* name, const char* value) {
auto* kv = static_cast<std::map<std::string, std::string>*>(user);
(*kv)[std::string(section) + "." + std::string(name)] = value ? value : "";
return 1;
}
} // namespace
double AppConfig::image_rate() const {
return general.image_interval > 0 ? 1.0 / general.image_interval : 0.0;
}
AppConfig ConfigLoader::fromMap(const std::map<std::string, std::string>& kv) {
AppConfig cfg;
cfg.general.tower_name = get(kv, "General.tower_name", cfg.general.tower_name);
cfg.general.image_interval = getInt(kv, "General.image_interval", cfg.general.image_interval);
cfg.general.debug = getBool(kv, "General.debug", cfg.general.debug);
cfg.network.broker_ip = get(kv, "Network.zkms_server_ip", cfg.network.broker_ip);
// Secrets: environment variables win over the file.
cfg.network.mqtt_user = envOr("FGC_MQTT_USER", get(kv, "Network.mqtt_user"));
cfg.network.mqtt_pw = envOr("FGC_MQTT_PW", get(kv, "Network.mqtt_pw"));
cfg.serial.device = get(kv, "Serial.device", cfg.serial.device);
cfg.serial.baud = static_cast<unsigned>(getInt(kv, "Serial.baud", cfg.serial.baud));
cfg.camera.ids.clear();
for (const char* key : {"Camera.id_Cam1", "Camera.id_Cam2", "Camera.id_Cam3", "Camera.id_Cam4"}) {
std::string id = get(kv, key);
if (!id.empty()) cfg.camera.ids.push_back(id);
}
std::string out = get(kv, "Paths.output_dir");
cfg.paths.output_dir = out.empty() ? paths::defaultOutputDir() : paths::expandUser(out);
cfg.features.enable_mqtt = getBool(kv, "Features.enable_mqtt", cfg.features.enable_mqtt);
cfg.features.enable_camera = getBool(kv, "Features.enable_camera", cfg.features.enable_camera);
cfg.features.enable_serial = getBool(kv, "Features.enable_serial", cfg.features.enable_serial);
cfg.features.mock_camera = getBool(kv, "Features.mock_camera", cfg.features.mock_camera);
cfg.features.mock_serial = getBool(kv, "Features.mock_serial", cfg.features.mock_serial);
// ---- validation ----
if (cfg.general.image_interval <= 0)
throw std::runtime_error("General.image_interval must be > 0 (got " +
std::to_string(cfg.general.image_interval) + ")");
if (cfg.serial.baud == 0)
throw std::runtime_error("Serial.baud must be > 0");
return cfg;
}
AppConfig ConfigLoader::loadFromFile(const std::string& path) {
std::map<std::string, std::string> kv;
int rc = ini_parse(path.c_str(), iniHandler, &kv);
if (rc < 0) throw std::runtime_error("Cannot read config file: " + path);
if (rc > 0)
throw std::runtime_error("Parse error in config file " + path + " at line " +
std::to_string(rc));
return fromMap(kv);
}
} // namespace fgc

95
src/core/Paths.cpp Normal file
View File

@ -0,0 +1,95 @@
#include "fgc/Paths.h"
#include <cstdlib>
#include <filesystem>
#include <unistd.h>
namespace fs = std::filesystem;
namespace fgc::paths {
namespace {
std::string envOr(const char* name, const std::string& fallback) {
const char* v = std::getenv(name);
return (v && *v) ? std::string(v) : fallback;
}
} // namespace
std::string expandUser(const std::string& path) {
if (path.empty()) return path;
std::string out;
out.reserve(path.size());
// Leading ~ -> $HOME
size_t i = 0;
if (path[0] == '~' && (path.size() == 1 || path[1] == '/')) {
out += envOr("HOME", "");
i = 1;
}
// $VAR and ${VAR} expansion
for (; i < path.size(); ++i) {
if (path[i] == '$') {
size_t start = i + 1;
bool braced = (start < path.size() && path[start] == '{');
if (braced) ++start;
size_t end = start;
while (end < path.size() &&
(std::isalnum(static_cast<unsigned char>(path[end])) || path[end] == '_')) {
++end;
}
std::string name = path.substr(start, end - start);
if (!name.empty()) {
out += envOr(name.c_str(), "");
i = braced && end < path.size() && path[end] == '}' ? end : end - 1;
continue;
}
}
out += path[i];
}
return out;
}
std::string executableDir() {
std::error_code ec;
fs::path self = fs::read_symlink("/proc/self/exe", ec);
if (ec) return {};
return self.parent_path().string();
}
std::vector<std::string> configSearchPaths(const std::string& cliArg) {
std::vector<std::string> paths;
if (!cliArg.empty()) paths.push_back(expandUser(cliArg));
if (const char* env = std::getenv("FGC_CONFIG"); env && *env)
paths.push_back(expandUser(env));
paths.push_back("config.ini"); // current working directory
if (std::string dir = executableDir(); !dir.empty())
paths.push_back((fs::path(dir) / "config.ini").string());
std::string xdg = envOr("XDG_CONFIG_HOME", expandUser("~/.config"));
if (!xdg.empty())
paths.push_back((fs::path(xdg) / "fire_gimbal_control" / "config.ini").string());
return paths;
}
std::optional<std::string> resolveConfigPath(const std::string& cliArg) {
std::error_code ec;
for (const auto& p : configSearchPaths(cliArg)) {
if (fs::is_regular_file(p, ec)) return p;
}
return std::nullopt;
}
std::string defaultOutputDir() {
std::string base = envOr("XDG_DATA_HOME", expandUser("~/.local/share"));
return (fs::path(base) / "fire_gimbal_control" / "images").string();
}
} // namespace fgc::paths