Add CMake build and typed config with path resolution (remove hardcoded paths)
This commit is contained in:
parent
89be503edb
commit
c4b24c0fbf
|
|
@ -2,7 +2,10 @@
|
|||
"permissions": {
|
||||
"allow": [
|
||||
"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)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 "------------------------------------------------------------")
|
||||
34
Camera.cpp
34
Camera.cpp
|
|
@ -337,7 +337,6 @@ void VimbaHandler::SaveImage()
|
|||
cv::imshow("Display Image", img);
|
||||
cv::waitKey(10);
|
||||
}
|
||||
std::string homedir = getenv("HOME");
|
||||
std::string cameraname;
|
||||
if (pFrame->getCamId() == 0)
|
||||
cameraname = "RGB";
|
||||
|
|
@ -345,7 +344,7 @@ void VimbaHandler::SaveImage()
|
|||
cameraname = "ACR";
|
||||
if (pFrame->getCamId() == 2)
|
||||
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);
|
||||
// Extract the directory part of the path
|
||||
std::filesystem::path dir = path.parent_path();
|
||||
|
|
@ -363,29 +362,9 @@ void VimbaHandler::SaveImage()
|
|||
std::filesystem::copy("test_smoke.jxl", filename);
|
||||
}
|
||||
time.Reset();
|
||||
//cv::imwrite(filename, img, compression_params);
|
||||
std::string net_path = "/mnt/ggs-smb/FirewatchTowers/FWT_Podrosche/";
|
||||
std::string backup_net_path = "/home/ggs/fwt_image_backup/";
|
||||
|
||||
|
||||
|
||||
//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);
|
||||
// NOTE: optional network upload of saved images to the ground
|
||||
// station was removed here; it had hardcoded NFS/SMB paths. If
|
||||
// reintroduced it should read its destination from config.
|
||||
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);
|
||||
}
|
||||
|
|
@ -447,3 +426,8 @@ void VimbaHandler::SetTowerName(std::string name)
|
|||
fwt_name = name;
|
||||
mqtt_RGB = "GGS/FWT/" + fwt_name + "/CamEvent";
|
||||
}
|
||||
|
||||
void VimbaHandler::SetOutputDir(std::string dir)
|
||||
{
|
||||
output_dir = std::move(dir);
|
||||
}
|
||||
|
|
|
|||
2
Camera.h
2
Camera.h
|
|
@ -78,6 +78,7 @@ public:
|
|||
bool TriggerCamera();
|
||||
void TriggerSettle();
|
||||
void SetTowerName(std::string name);
|
||||
void SetOutputDir(std::string dir);
|
||||
|
||||
bool cam_started = false;
|
||||
int queue_count_rgb = 0;
|
||||
|
|
@ -86,6 +87,7 @@ private:
|
|||
double jxlq = 2.0;
|
||||
double jxle = 3.0;
|
||||
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";
|
||||
motor_info* gimbal_data;
|
||||
MQTTClient* mqtt_client;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -29,3 +29,23 @@ id_Cam1 = DEV_0000000000
|
|||
id_Cam2 =
|
||||
id_Cam3 =
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
84
main.cpp
84
main.cpp
|
|
@ -10,7 +10,8 @@
|
|||
#include "Camera.h"
|
||||
#include "MQTT.h"
|
||||
#include "timing.h"
|
||||
#include "ini.h"
|
||||
#include "fgc/Config.h"
|
||||
#include "fgc/Paths.h"
|
||||
|
||||
namespace po = boost::program_options;
|
||||
|
||||
|
|
@ -31,14 +32,6 @@ mqtt_sub_data mqtt_in;
|
|||
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" };
|
||||
|
||||
// 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() {
|
||||
std::string input;
|
||||
while (running) {
|
||||
|
|
@ -60,44 +53,42 @@ void readInput() {
|
|||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
//ini handling with ini.h
|
||||
std::map<std::string, std::string> config;
|
||||
// Parse the INI file
|
||||
if (ini_parse("/home/ggs/projects/Fire_Gimbal_Control/bin/x64/Release/config.ini", handler, &config) < 0) {
|
||||
std::cerr << "Can't load config.ini file!" << std::endl;
|
||||
// Resolve the config file from a search order (no hardcoded paths):
|
||||
// --config (added in a later phase) -> $FGC_CONFIG -> ./config.ini
|
||||
// -> <exe dir>/config.ini -> $XDG_CONFIG_HOME/fire_gimbal_control/config.ini
|
||||
auto config_path = fgc::paths::resolveConfigPath();
|
||||
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;
|
||||
}
|
||||
// Access configuration values
|
||||
std::string tower_name = config["General.tower_name"];
|
||||
int rate = std::stoi(config["General.image_interval"]);
|
||||
int debug = std::stoi(config["General.debug"]);
|
||||
std::string zkms_server_ip = config["Network.zkms_server_ip"];
|
||||
std::string mqtt_user = config["Network.mqtt_user"];
|
||||
std::string mqtt_pw = config["Network.mqtt_pw"];
|
||||
std::string cam_id1 = config["Camera.id_Cam1"];
|
||||
std::string cam_id2 = config["Camera.id_Cam2"];
|
||||
std::string cam_id3 = config["Camera.id_Cam3"];
|
||||
std::string cam_id4 = config["Camera.id_Cam4"];
|
||||
if (cam_id1 != "")
|
||||
camera_id_vec.push_back(cam_id1);
|
||||
if (cam_id2 != "")
|
||||
camera_id_vec.push_back(cam_id2);
|
||||
if (cam_id3 != "")
|
||||
camera_id_vec.push_back(cam_id3);
|
||||
if (cam_id4 != "")
|
||||
camera_id_vec.push_back(cam_id4);
|
||||
std::cout << camera_id_vec.size() << " camera(s) should be connected according the config file." << "\n";
|
||||
|
||||
//set globals
|
||||
fwt_name = tower_name;
|
||||
imagerate = 1/(double)rate;
|
||||
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;
|
||||
|
||||
fgc::AppConfig cfg;
|
||||
try {
|
||||
cfg = fgc::ConfigLoader::loadFromFile(*config_path);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Config error (" << *config_path << "): " << e.what() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Bridge typed config into the existing globals/locals.
|
||||
camera_id_vec = cfg.camera.ids;
|
||||
fwt_name = cfg.general.tower_name;
|
||||
imagerate = cfg.image_rate();
|
||||
motorctl_info_out = cfg.general.debug;
|
||||
server_ip = cfg.network.broker_ip;
|
||||
std::string mqtt_user = cfg.network.mqtt_user;
|
||||
std::string mqtt_pw = cfg.network.mqtt_pw;
|
||||
|
||||
std::cout << "Loaded config: " << *config_path << "\n";
|
||||
std::cout << camera_id_vec.size() << " camera(s) configured.\n";
|
||||
std::cout << "tower_name: " << fwt_name << "\n";
|
||||
std::cout << "image_interval: " << cfg.general.image_interval << " s\n";
|
||||
std::cout << "debug: " << cfg.general.debug << "\n";
|
||||
std::cout << "broker: " << server_ip << std::endl;
|
||||
|
||||
|
||||
//args
|
||||
|
|
@ -152,7 +143,7 @@ int main(int argc, char* argv[])
|
|||
return 1;
|
||||
}
|
||||
//main vars
|
||||
SerialPort serial("/dev/ttyACM0", 115200);
|
||||
SerialPort serial(cfg.serial.device, cfg.serial.baud);
|
||||
std::thread io_thread;
|
||||
|
||||
const std::string mqtt_STATUS = "GGS/FWT/" + fwt_name + "/StatusCode";
|
||||
|
|
@ -216,6 +207,7 @@ int main(int argc, char* argv[])
|
|||
//Camera Obj
|
||||
VimbaHandler cam_handler(camera_id_vec, mqtt_client, &act_info, start_demo);
|
||||
cam_handler.SetTowerName(fwt_name);
|
||||
cam_handler.SetOutputDir(cfg.paths.output_dir);
|
||||
//init Gimbal
|
||||
if (init_gimbal) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue