Modularize into interfaces with real + mock implementations

This commit is contained in:
pgdalmeida 2026-06-22 12:51:57 +02:00
parent 16ab54484d
commit 11a2384c53
Signed by: pedro.almeida
GPG Key ID: D4A6C394DF13F1D7
30 changed files with 1381 additions and 4580 deletions

View File

@ -46,6 +46,7 @@ add_library(fgc_core STATIC
src/core/Logger.cpp
src/core/TelemetryParser.cpp
src/core/CaptureScheduler.cpp
src/core/CommandParser.cpp
ini.c
)
target_include_directories(fgc_core PUBLIC
@ -55,14 +56,23 @@ target_include_directories(fgc_core PUBLIC
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.
# Application executable. Camera image pipeline + serial controller always
# build (OpenCV/libjxl/Boost.Asio are available everywhere); MQTT and Vimba
# sources are added only when their option is ON.
# ---------------------------------------------------------------------------
set(FGC_SOURCES
main.cpp
MQTT.cpp
Camera.cpp
src/core/Application.cpp
src/camera/JpegXlEncoder.cpp
src/camera/ImagePipeline.cpp
src/serial/SerialMotorController.cpp
)
if(WITH_MQTT)
list(APPEND FGC_SOURCES src/mqtt/MqttControlChannel.cpp)
endif()
if(WITH_VIMBA)
list(APPEND FGC_SOURCES src/camera/VimbaCameraSource.cpp)
endif()
add_executable(fire_gimbal_control ${FGC_SOURCES})
target_include_directories(fire_gimbal_control PRIVATE ${CMAKE_SOURCE_DIR})

View File

@ -1,433 +0,0 @@
#include "Camera.h"
#include "JPEG_XL.h"
#include "timing.h"
#include <iostream>
#include <mutex>
#include <filesystem>
#include <opencv2/opencv.hpp>
class FrameObserver : public IFrameObserver
{
public:
typedef std::function<void(int)> Callback;
Callback call;
int cam_id = 0;
std::atomic<int> settle = { 3 };
void registerCallback(Callback f) {
call = f;
}
FrameObserver(CameraPtr camera) : IFrameObserver(camera) {};
void FrameReceived(const FramePtr pFrame)
{
int old_val = settle.fetch_sub(1);
std::cout << "[FrameObserver cam=" << cam_id << "] FrameReceived settle=" << old_val << " → new=" << old_val - 1 << std::endl;
if (old_val > 0) {
std::cout << "[FrameObserver cam=" << cam_id << "] DUMP (settling)" << std::endl;
m_pCamera->QueueFrame(pFrame);
return;
}
bool bQueueDirectly = true;
VmbFrameStatusType eReceiveStatus;
if (VmbErrorSuccess == pFrame->GetReceiveStatus(eReceiveStatus))
{
/* ignore any incompletely frame */
if (VmbFrameStatusComplete != eReceiveStatus)
{
VmbUint64_t id;
std::string name;
std::string serial;
pFrame->GetFrameID(id);
m_pCamera->GetModel(name);
m_pCamera->GetSerialNumber(serial);
std::cout << "!!!incomplete frame, rcvd error: " << eReceiveStatus << " id: " << id << " camera: " << name << " + " << serial << std::endl;
m_pCamera->QueueFrame(pFrame);
return;
}
VmbUint64_t id;
pFrame->GetFrameID(id);
std::cout << "[FrameObserver cam=" << cam_id << "] rcvd frame ID=" << id << " → ENQUEUE" << std::endl;
// Lock the frame queue
m_FramesMutex.lock();
// Add frame to queue
m_Frames.push(pFrame);
// Unlock frame queue
m_FramesMutex.unlock();
// Emit the frame received signal
call(cam_id);
// callback!
bQueueDirectly = false;
}
else {
std::cout << "[FrameObserver cam=" << cam_id << "] frame rcvd error" << std::endl;
}
// If any error occurred we queue the frame without notification
if (true == bQueueDirectly)
{
m_pCamera->QueueFrame(pFrame);
}
};
FramePtr GetFrame() {
FramePtr res;
// Lock the frame queue
m_FramesMutex.lock();
// Pop frame from queue
if (!m_Frames.empty())
{
res = m_Frames.front();
m_Frames.pop();
}
// Unlock frame queue
m_FramesMutex.unlock();
return res;
}
void QueueF(FramePtr& frame) {
m_pCamera->QueueFrame(frame);
}
void ClearFrameQueue() {
// Lock the frame queue
m_FramesMutex.lock();
std::queue<FramePtr> empty;
std::swap(m_Frames, empty);
m_FramesMutex.unlock();
}
private:
std::queue<FramePtr> m_Frames;
std::mutex m_FramesMutex;
};
VimbaHandler::VimbaHandler(std::vector<std::string> cameraIds, MQTTClient* mqtt_c, motor_info* motor_i, bool demo) :
m_vmbSystem(VmbSystem::GetInstance()), mqtt_client(mqtt_c), gimbal_data(motor_i), demo_flag(demo)
{
VmbErrorType err = m_vmbSystem.Startup();
if (err != VmbErrorSuccess)
{
throw std::runtime_error("Could not start API, err=" + std::to_string(err));
}
//CameraPtrVector cameras;
//err = m_vmbSystem.GetCameras(cameras);
//if (err != VmbErrorSuccess)
//{
// m_vmbSystem.Shutdown();
// throw std::runtime_error("Could not get cameras, err=" + std::to_string(err));
//}
//if (cameras.empty())
//{
// m_vmbSystem.Shutdown();
// throw std::runtime_error("No cameras found.");
//}
if (cameraIds.size() > 0)
{
int id = 0;
for (std::string cameraId : cameraIds)
{
CameraPtr cam;
err = m_vmbSystem.GetCameraByID(cameraId.c_str(), cam);
if (err != VmbErrorSuccess)
{
m_vmbSystem.Shutdown();
throw std::runtime_error("No camera found with ID=" + std::string(cameraId) + ", err = " + std::to_string(err));
}
else {
m_cameras.push_back(cam);
}
}
}
//else
//{
// m_camera = cameras[0];
//}
for (CameraPtr camptr : m_cameras)
{
err = camptr->Open(VmbAccessModeFull);
if (err != VmbErrorSuccess)
{
m_vmbSystem.Shutdown();
throw std::runtime_error("Could not open camera, err=" + std::to_string(err));
}
else {
std::string name;
if (camptr->GetName(name) == VmbErrorSuccess)
{
std::cout << "Opened Camera " << name << std::endl;
}
}
}
}
VimbaHandler::~VimbaHandler()
{
try
{
Stop();
}
catch (std::runtime_error& e)
{
std::cout << e.what() << std::endl;
}
m_vmbSystem.Shutdown();
}
void VimbaHandler::Open()
{
}
void VimbaHandler::Close()
{
}
void VimbaHandler::Start()
{
save_thread_running = true;
std::cout << "starting Camera thread" << std::endl;
image_saver_thread = std::thread(&VimbaHandler::SaveImage, this);
int cam_id = 0;
for (CameraPtr cam : m_cameras) {
IFrameObserverPtr FO_ptr;
ImageQueue8 image_queue;
m_ImageQueue_vec.push_back(image_queue);
SP_SET(FO_ptr, new FrameObserver(cam));
SP_DYN_CAST<FrameObserver>(FO_ptr)->cam_id = cam_id;
cam_id++;
VmbErrorType err = cam->StartContinuousImageAcquisition(5, FO_ptr);
FO_ptr_vec.push_back(FO_ptr);
if (err != VmbErrorSuccess)
{
throw std::runtime_error("Could not start acquisition, err=" + std::to_string(err));
}
else {
SP_DYN_CAST<FrameObserver>(FO_ptr)->registerCallback(std::bind(&VimbaHandler::EnqueueToStoreStruct, this, std::placeholders::_1));
}
}
cam_started = true;
}
void VimbaHandler::Stop()
{
for (CameraPtr cam : m_cameras) {
VmbErrorType err = cam->StopContinuousImageAcquisition();
if (err != VmbErrorSuccess)
{
throw std::runtime_error("Could not stop acquisition, err=" + std::to_string(err));
}
}
cam_started = false;
save_thread_running = false;
if (image_saver_thread.joinable()) {
image_saver_thread.join();
}
}
void VimbaHandler::evaluateCommand(std::string cmd, double val)
{
if (cmd == "fps" && val > 0)
ChangeFramerate(val);
else if (cmd == "jxlq" && val > 0)
jxlq = val;
else if (cmd == "jxle" && val > 0)
jxle = val;
else if (cmd == "display" && val >= 0)
display_image = int(val);
}
void VimbaHandler::EnqueueToStoreStruct(int cam_id)
{
FramePtr frame = SP_DYN_CAST<FrameObserver>(FO_ptr_vec[cam_id])->GetFrame();
VmbUint32_t Width;
VmbUint32_t Height;
VmbUint32_t BufferSize;
VmbPixelFormatType PixelFormat;
const VmbUchar_t* pBuffer(NULL);
if (VmbErrorSuccess == frame->GetPixelFormat(PixelFormat)
&& VmbErrorSuccess == frame->GetWidth(Width)
&& VmbErrorSuccess == frame->GetHeight(Height)
&& VmbErrorSuccess == frame->GetBufferSize(BufferSize)
&& VmbErrorSuccess == frame->GetBuffer(pBuffer))
{
NanoUnixTimer time;
long long tstamp = time.Stamp_longlong();
std::lock_guard<std::mutex> lg(queue_mut);
ImageStore8Ptr pFrame;
// in case we reached the maximum number of queued frames
// take of the oldest and reuse it to store the newly arriving frame
std::vector<VmbUchar_t> data_in = std::vector<VmbUchar_t>(pBuffer, pBuffer + BufferSize);
if (m_ImageQueue_vec[cam_id].size() >= 100)
{
pFrame = m_ImageQueue_vec[cam_id].front();
m_ImageQueue_vec[cam_id].pop();
if (!pFrame->equal(Width, Height, PixelFormat))
{
pFrame.reset();
}
}
if (pFrame == NULL)
{
pFrame = ImageStore8Ptr(new image_store_8bit(data_in.data(), data_in.size(), Width, Height, PixelFormat, tstamp, cam_id));
}
else
{
pFrame->setData(data_in.data(), data_in.size(), tstamp);
}
m_ImageQueue_vec[cam_id].push(pFrame);
SP_DYN_CAST<FrameObserver>(FO_ptr_vec[cam_id])->QueueF(frame);
std::cout << "Enqueue finished Camera:" << cam_id << " -- Queue size: " << m_ImageQueue_vec[cam_id].size() << std::endl;
if (cam_id == 0)
cv_proc.notify_one();
}
}
void VimbaHandler::SaveImage()
{
while (save_thread_running) {
std::unique_lock<std::mutex> lock(proc_wait_mut);
cv_proc.wait(lock);
ImageStore8Ptr pFrame;
for (ImageQueue8& imageQueue : m_ImageQueue_vec) {
while (imageQueue.size() > 0 && save_jxl)
{
Timer time;
queue_mut.lock();
pFrame = imageQueue.front();
imageQueue.pop();
queue_count_rgb = imageQueue.size();
queue_mut.unlock();
int imagetype;
int imagechannels;
if (pFrame->dataSize() / (pFrame->height() * pFrame->width()) == 1) {
imagetype = CV_8UC1;
imagechannels = 1;
}
else if (pFrame->dataSize() / (pFrame->height() * pFrame->width()) == 3) {
imagetype = CV_8UC3;
imagechannels = 3;
}
cv::Mat img_to_rotate = cv::Mat(pFrame->height(), pFrame->width(), imagetype, pFrame->data());
cv::Mat img;
cv::rotate(img_to_rotate, img, cv::ROTATE_90_COUNTERCLOCKWISE);
if (display_image) {
cv::namedWindow("Display Image", cv::WINDOW_NORMAL);
cv::resizeWindow("Display Image", img.cols / 4, img.rows / 4);
cv::imshow("Display Image", img);
cv::waitKey(10);
}
std::string cameraname;
if (pFrame->getCamId() == 0)
cameraname = "RGB";
if (pFrame->getCamId() == 1)
cameraname = "ACR";
if (pFrame->getCamId() == 2)
cameraname = "NIR";
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();
// Create directories if they don't exist
if (!dir.empty() && !std::filesystem::exists(dir)) {
std::filesystem::create_directories(dir);
std::cout << "Created directories: " << dir << std::endl;
}
if (!demo_flag) {
JPEGXL jxl_writer(img.cols, img.rows, img.data, imagechannels, (float)jxlq, (int)jxle);
jxl_writer.WriteFile(filename.c_str());
std::cout << "Camera: " + std::to_string(pFrame->getCamId()) + " -- Compress TIME:" << std::to_string(time.ElapsedMillis()) << " -- SaveQueue size:" << imageQueue.size() << std::endl;
}
else {
std::filesystem::copy("test_smoke.jxl", filename);
}
time.Reset();
// 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);
}
}
}
}
bool VimbaHandler::ChangeFramerate(double fr)
{
VmbErrorType result;
for (CameraPtr cam : m_cameras) {
FeaturePtr pFeature;
result = SP_ACCESS(cam)->GetFeatureByName("AcquisitionFrameRate", pFeature);
if (result == VmbErrorSuccess)
result = pFeature->SetValue(fr);
if (result == VmbErrorSuccess) {
std::cout << "camera fps changed: " << fr << std::endl;
}
}
if (result == VmbErrorSuccess) {
return true;
}
else {
std::cout << "camera fps change failed with error:" << result << std::endl;
return false;
}
}
void VimbaHandler::TriggerSettle()
{
for (auto& fo : FO_ptr_vec) {
SP_DYN_CAST<FrameObserver>(fo)->settle.store(3);
}
}
bool VimbaHandler::TriggerCamera()
{
TriggerSettle();
std::cout << "[TriggerCamera] settle reset to 3, firing 4 triggers (3 settle + 1 real)..." << std::endl;
FeaturePtr pFeature;
VmbErrorType result;
for (int i = 0; i < 4; i++) {
result = SP_ACCESS(m_cameras[0])->GetFeatureByName("TriggerSoftware", pFeature);
if (result == VmbErrorSuccess)
result = pFeature->RunCommand();
int sl = SP_DYN_CAST<FrameObserver>(FO_ptr_vec[0])->settle.load();
std::cout << "[TriggerCamera] trigger #" << (i+1) << " settle_before=" << sl << " result=" << result << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(400));
}
std::cout << "[TriggerCamera] done, 4th frame should be real image" << std::endl;
return (result == VmbErrorSuccess);
}
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);
}

110
Camera.h
View File

@ -1,110 +0,0 @@
#pragma once
#include <VmbCPP/VmbCPP.h>
#include <thread>
#include <mutex>
#include <memory>
#include <queue>
#include <condition_variable>
#include <atomic>
#include "MQTT.h"
#include "Serial.h"
using namespace VmbCPP;
struct image_store_8bit
{
private:
typedef std::vector<VmbUchar_t> data_vec;
data_vec m_Data;
VmbUint32_t m_Width; // frame width
VmbUint32_t m_Height; // frame height
VmbPixelFormat_t m_PixelFormat; // frame pixel format
long long m_timestamp;
int m_cam_id;
public:
image_store_8bit(const VmbUchar_t* pBuffer, VmbUint32_t BufferByteSize, VmbUint32_t Width, VmbUint32_t Height, VmbPixelFormatType PixelFormat, long long stamp, int cam_id)
: m_Data(pBuffer, pBuffer + BufferByteSize)
, m_Width(Width)
, m_Height(Height)
, m_PixelFormat(PixelFormat)
, m_timestamp(stamp)
, m_cam_id(cam_id)
{
}
long long getTimestamp() {
return(m_timestamp);
}
bool equal(VmbUint32_t Width, VmbUint32_t Height, VmbPixelFormat_t PixelFormat) const
{
return m_Width == Width
&& m_Height == Height
&& m_PixelFormat == PixelFormat;
}
bool setData(const VmbUchar_t* Buffer, VmbUint32_t BufferSize, long long stamp)
{
if (BufferSize == dataSize())
{
std::copy(Buffer, Buffer + BufferSize, m_Data.begin());
m_timestamp = stamp;
return true;
}
return false;
}
VmbPixelFormat_t pixelFormat() const { return m_PixelFormat; }
VmbUint32_t width() const { return m_Width; }
VmbUint32_t height() const { return m_Height; }
VmbUint32_t dataSize() const { return static_cast<VmbUint32_t>(m_Data.size()); }
const VmbUchar_t* data() const { return &*m_Data.begin(); }
VmbUchar_t* data() { return &*m_Data.begin(); }
int getCamId() { return m_cam_id; }
};
class VimbaHandler
{
public:
VimbaHandler(std::vector<std::string> cameraIds, MQTTClient* mqtt_c, motor_info* motor_i, bool demo);
~VimbaHandler();
void Open();
void Close();
void Start();
void Stop();
void evaluateCommand(std::string cmd, double val);
void EnqueueToStoreStruct(int cam_id);
void SaveImage();
bool ChangeFramerate(double fr);
bool TriggerCamera();
void TriggerSettle();
void SetTowerName(std::string name);
void SetOutputDir(std::string dir);
bool cam_started = false;
int queue_count_rgb = 0;
private:
bool demo_flag = false;
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;
typedef std::shared_ptr<image_store_8bit> ImageStore8Ptr;
typedef std::queue<ImageStore8Ptr> ImageQueue8;
std::vector <ImageQueue8> m_ImageQueue_vec;
std::mutex queue_mut;
std::mutex proc_wait_mut;
std::condition_variable cv_proc;
bool save_thread_running = false;
bool save_jxl = true;
bool display_image = false;
std::thread image_saver_thread;
std::vector<IFrameObserverPtr> FO_ptr_vec;
VmbSystem& m_vmbSystem;
CameraPtrVector m_cameras;
};

110
JPEG_XL.h
View File

@ -1,110 +0,0 @@
#pragma once
#include <iostream>
#include <cstdio> // for fopen, fclose
#include <vector>
#include <jxl/codestream_header.h>
#include <jxl/color_encoding.h>
#include <jxl/encode.h>
#include <jxl/encode_cxx.h>
#include <jxl/thread_parallel_runner.h>
#include <jxl/thread_parallel_runner_cxx.h>
#include <jxl/types.h>
class JPEGXL
{
public:
JPEGXL(int width, int height, const unsigned char* img, int num_channels, float q, int e, int th = 3) :m_width(width), m_height(height), m_num_channels(num_channels), m_quality(q), m_efford(e)
{
auto enc = JxlEncoderMake(/*memory_manager=*/nullptr);
auto runner = JxlThreadParallelRunnerMake(nullptr, th);
if (JXL_ENC_SUCCESS != JxlEncoderSetParallelRunner(enc.get(),
JxlThreadParallelRunner,
runner.get())) {
fprintf(stderr, "JxlEncoderSetParallelRunner failed\n");
}
JxlPixelFormat pixel_format = { m_num_channels, JXL_TYPE_UINT8, JXL_NATIVE_ENDIAN, 0 };
JxlBasicInfo basic_info;
JxlEncoderInitBasicInfo(&basic_info);
basic_info.xsize = m_width;
basic_info.ysize = m_height;
basic_info.alpha_bits = 0;
basic_info.bits_per_sample = 8;
basic_info.num_color_channels = m_num_channels;
basic_info.uses_original_profile = false;
if (JXL_ENC_SUCCESS != JxlEncoderSetBasicInfo(enc.get(), &basic_info)) {
fprintf(stderr, "JxlEncoderSetBasicInfo failed\n");
return;
}
JxlColorEncoding color_encoding = {};
JxlColorEncodingSetToSRGB(&color_encoding,/*is_gray=*/pixel_format.num_channels < 3);
if (JXL_ENC_SUCCESS != JxlEncoderSetColorEncoding(enc.get(), &color_encoding)) {
fprintf(stderr, "JxlEncoderSetColorEncoding failed\n");
return;
}
JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), nullptr);
JxlEncoderFrameSettingsSetOption(frame_settings, JXL_ENC_FRAME_SETTING_EFFORT, e);
JxlEncoderFrameSettingsSetOption(frame_settings, JXL_ENC_FRAME_SETTING_DECODING_SPEED, 0);
if (q == 0) {//if lossless
JxlEncoderSetFrameDistance(frame_settings, 0);
JxlEncoderSetFrameLossless(frame_settings, true);
}
else {
JxlEncoderSetFrameLossless(frame_settings, false);
JxlEncoderSetFrameDistance(frame_settings, q);
}
if (JXL_ENC_SUCCESS != JxlEncoderAddImageFrame(frame_settings, &pixel_format, (void*)img, sizeof(uint8_t) * m_width * m_height * m_num_channels)) {
fprintf(stderr, "JxlEncoderAddImageFrame failed\n");
return;
}
JxlEncoderCloseInput(enc.get());
std::vector<uint8_t>* compressed = &compressed_data;
compressed->resize(64);
uint8_t* next_out = compressed->data();
size_t avail_out = compressed->size() - (next_out - compressed->data());
JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT;
while (process_result == JXL_ENC_NEED_MORE_OUTPUT) {
process_result = JxlEncoderProcessOutput(enc.get(), &next_out, &avail_out);
if (process_result == JXL_ENC_NEED_MORE_OUTPUT) {
size_t offset = next_out - compressed->data();
compressed->resize(compressed->size() * 2);
next_out = compressed->data() + offset;
avail_out = compressed->size() - offset;
}
}
compressed->resize(next_out - compressed->data());
if (JXL_ENC_SUCCESS != process_result) {
fprintf(stderr, "JxlEncoderProcessOutput failed\n");
return;
}
}
~JPEGXL() {}
bool WriteFile(const char* filename) {
FILE* file = fopen(filename, "wb");
if (!file) {
fprintf(stderr, "Could not open %s for writing\n", filename);
return false;
}
if (fwrite(compressed_data.data(), sizeof(uint8_t), compressed_data.size(), file) !=
compressed_data.size()) {
fprintf(stderr, "Could not write bytes to %s\n", filename);
fclose(file);
return false;
}
if (fclose(file) != 0) {
fprintf(stderr, "Could not close %s\n", filename);
return false;
}
return true;
}
private:
int m_width;
int m_height;
int m_num_channels;
float m_quality;
int m_efford;
std::vector<uint8_t> compressed_data;
};

3
Log.h
View File

@ -1,3 +0,0 @@
#pragma once
// Legacy shim: the project's logging now lives in fgc/Logger.h.
#include "fgc/Logger.h"

155
MQTT.cpp
View File

@ -1,155 +0,0 @@
#include "MQTT.h"
const int QOS = 1;
const int N_RETRY_ATTEMPTS = 5;
void MQTTCallback::on_failure(const mqtt::token& tok)
{
std::cout << "Connection failed" << std::endl;
if (++nretry_ > N_RETRY_ATTEMPTS)
std::cout << "Connection attempt failed already a few times" << std::endl;
reconnect();
}
// (Re)connection success
void MQTTCallback::connected(const std::string& cause) {
std::cout << "\nConnection success" << std::endl;
std::cout << "\nSubscribing to topics.." << std::endl;
cli_.subscribe(tpoic_target_hdg, QOS, nullptr, subListener_);
cli_.subscribe(topic_control_mode, QOS, nullptr, subListener_);
}
void MQTTCallback::message_arrived(mqtt::const_message_ptr msg) {
std::cout << "Message arrived" << std::endl;
std::cout << "\ttopic: '" << msg->get_topic() << "'" << std::endl;
std::cout << "\tpayload: '" << msg->to_string() << "'\n" << std::endl;
if (msg->get_topic() == "GGS/FWT/" + fwt_name + "/target_HDG") {
try {
int value = static_cast<int>(std::stoi(msg->to_string()));
std::unique_lock<std::mutex> ul(mqtt_mut);
sub_data.set_target_heading(msg->to_string());
}
catch (const std::invalid_argument& e) {
std::cerr << "MQTT type convertion invalid argument: " << e.what() << std::endl;
}
catch (const std::out_of_range& e) {
std::cerr << "MQTT type convertion out of range: " << e.what() << std::endl;
}
}
else if (msg->get_topic() == "GGS/FWT/" + fwt_name + "/ControlCode") {
try {
std::unique_lock<std::mutex> ul(mqtt_mut);
sub_data.set_control_code(static_cast<int>(std::stoi(msg->to_string())));
}
catch (const std::invalid_argument& e) {
std::cerr << "MQTT type convertion invalid argument: " << e.what() << std::endl;
}
catch (const std::out_of_range& e) {
std::cerr << "MQTT type convertion out of range: " << e.what() << std::endl;
}
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////7x
MQTTClient::MQTTClient(const std::string& serverURI, const std::string& clientId, std::string tower_name, std::string login_user, std::string login_pw)
: client(serverURI, clientId), callback(client, connOpts, tower_name)
{
connOpts.set_keep_alive_interval(20);
connOpts.set_clean_session(true);
connOpts.set_user_name(login_user);
connOpts.set_password(login_pw);
std::cout << "MQTT object created" << std::endl;
client.set_callback(callback);
std::cout << "MQTT callback set" << std::endl;
}
MQTTClient::~MQTTClient() {
if (client.is_connected())
disconnect();
}
void MQTTClient::connect_client() {
std::cout << "connecting MQTT..." << std::endl;
running = true;
mqtt_thread = std::thread(&MQTTClient::run, this);
}
void MQTTClient::run() {
const auto TIMEOUT = std::chrono::seconds(5);
try {
conToken = client.connect(connOpts, nullptr, callback);
conToken->wait_for(TIMEOUT);
connected = true;
}
catch (const mqtt::exception& exc) {
std::cerr << "Error: " << exc.what() << std::endl;
running = false;
connected = false;
return;
}
std::cout << "MQTT connected" << std::endl;
while (running)
;
}
void MQTTClient::disconnect() {
// Disconnect
try {
std::cout << "\nDisconnecting from the MQTT server..." << std::flush;
client.disconnect()->wait();
std::cout << "OK" << std::endl;
}
catch (const mqtt::exception& exc) {
std::cerr << exc.what() << std::endl;
}
running = false;
mqtt_thread.join();
}
void MQTTClient::publish(const std::string& topic, const std::string& payload) {
const auto TIMEOUT = std::chrono::seconds(3);
mqtt::delivery_token_ptr pubtok;
mqtt::message_ptr msg = mqtt::make_message(topic, payload);
msg->set_qos(1);
msg->set_retained(true);
try {
pubtok = client.publish(msg);
pubtok->wait_for(TIMEOUT);
}
catch (const mqtt::exception& exc) {
std::cerr << "Error: " << exc.what() << std::endl;
}
}
void MQTTClient::subscribe(const std::string& topic) {
try {
subToken = client.subscribe(topic, 1);
subToken->wait();
}
catch (const mqtt::exception& exc) {
std::cerr << "Error: " << exc.what() << std::endl;
}
}
void MQTTClient::receiveMessages() {
try {
client.start_consuming();
}
catch (const mqtt::exception& exc) {
std::cerr << "Error: " << exc.what() << std::endl;
}
while (true) {
}
}
bool MQTTClient::is_connected_to_server()
{
return client.is_connected();
}

157
MQTT.h
View File

@ -1,157 +0,0 @@
#pragma once
#include <iostream>
#include <mqtt/async_client.h>
#include <thread>
#include <mutex>
struct mqtt_sub_data
{
bool ctl_avail = false;
bool hdg_avail = false;
std::string target_heading="";
int control_code = 0;
void set_control_code(int c) {
ctl_avail = true;
control_code = c;
}
void set_target_heading(std::string target) {
hdg_avail = true;
target_heading = target;
}
};
// Callbacks for the success or failures of requested actions.
// This could be used to initiate further action, but here we just log the
// results to the console.
class action_listener : public virtual mqtt::iaction_listener
{
std::string name_;
void on_failure(const mqtt::token& tok) override {
std::cout << name_ << " failure";
if (tok.get_message_id() != 0)
std::cout << " for token: [" << tok.get_message_id() << "]" << std::endl;
std::cout << std::endl;
}
void on_success(const mqtt::token& tok) override {
std::cout << name_ << " success";
if (tok.get_message_id() != 0)
std::cout << " for token: [" << tok.get_message_id() << "]" << std::endl;
auto top = tok.get_topics();
if (top && !top->empty())
std::cout << "\ttoken topic: '" << (*top)[0] << "', ..." << std::endl;
std::cout << std::endl;
}
public:
action_listener(const std::string& name) : name_(name) {}
};
/////////////////////////////////////////////////////////////////////////////
/**
* Local callback & listener class for use with the client connection.
* This is primarily intended to receive messages, but it will also monitor
* the connection to the broker. If the connection is lost, it will attempt
* to restore the connection and re-subscribe to the topic.
*/
class MQTTCallback : public virtual mqtt::callback,
public virtual mqtt::iaction_listener
{
const std::string tpoic_target_hdg;
const std::string topic_control_mode;
mqtt_sub_data sub_data;
std::mutex mqtt_mut;
std::string fwt_name;
// Counter for the number of connection retries
int nretry_;
// The MQTT client
mqtt::async_client& cli_;
// Options to use if we need to reconnect
mqtt::connect_options& connOpts_;
// An action listener to display the result of actions.
action_listener subListener_;
// This deomonstrates manually reconnecting to the broker by calling
// connect() again. This is a possibility for an application that keeps
// a copy of it's original connect_options, or if the app wants to
// reconnect with different options.
// Another way this can be done manually, if using the same options, is
// to just call the async_client::reconnect() method.
void reconnect() {
std::this_thread::sleep_for(std::chrono::milliseconds(2500));
try {
cli_.connect(connOpts_, nullptr, *this);
}
catch (const mqtt::exception& exc) {
std::cerr << "Error: " << exc.what() << std::endl;
}
}
// Re-connection failure
void on_failure(const mqtt::token& tok) override;
// (Re)connection success
// Either this or connected() can be used for callbacks.
void on_success(const mqtt::token& tok) override {}
// Re-connection success
void connected(const std::string& cause) override;
// Callback for when the connection is lost.
// This will initiate the attempt to manually reconnect.
void connection_lost(const std::string& cause) override {
std::cout << "\nConnection lost" << std::endl;
if (!cause.empty())
std::cout << "\tcause: " << cause << std::endl;
std::cout << "Reconnecting..." << std::endl;
nretry_ = 0;
reconnect();
}
// Callback for when a message arrives.
void message_arrived(mqtt::const_message_ptr msg) override;
void delivery_complete(mqtt::delivery_token_ptr token) override {
std::cout << "MQTT delivery complete" << std::endl;
}
public:
MQTTCallback(mqtt::async_client& cli, mqtt::connect_options& connOpts, std::string tower_name)
: nretry_(0), cli_(cli), connOpts_(connOpts), subListener_("Subscription"), fwt_name(tower_name), tpoic_target_hdg("GGS/FWT/" + tower_name + "/target_HDG"), topic_control_mode("GGS/FWT/" + tower_name + "/ControlCode") {}
mqtt_sub_data get_sub_data() {
std::unique_lock<std::mutex> ul(mqtt_mut);
mqtt_sub_data tmp = sub_data;
sub_data.hdg_avail = false;
sub_data.ctl_avail = false;
return tmp;
}
};
class MQTTClient {
public:
MQTTClient(const std::string& serverURI, const std::string& clientId, std::string tower_name, std::string login_user, std::string login_pw);
~MQTTClient();
void connect_client();
void run();
void disconnect();
void publish(const std::string& topic, const std::string& payload);
void subscribe(const std::string& topic);
void receiveMessages();
bool is_connected_to_server();
bool running = false;
bool connected = false;
MQTTCallback callback;
private:
std::string fwt_name;
mqtt::async_client client;
mqtt::token_ptr subToken;
mqtt::token_ptr conToken;
mqtt::connect_options connOpts;
std::thread mqtt_thread;
};

102
Parser.h
View File

@ -1,102 +0,0 @@
#pragma once
#include <mutex>
#include <string>
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/phoenix.hpp>
namespace qi = boost::spirit::qi;
enum InputCommands
{
no_cmd,
startcamera,
stopcamera,
setcamera,
setimagerate,
setgimbal,
setmotorcontrol,
setdebug
};
struct parser_data {
std::string command="";
std::string device = "";
std::string option = "";
double command_val=0.0;
};
struct Parser
{
parser_data p_data;
std::mutex mut;
void set_parser_data(parser_data& data) {
std::cout << "parser set data..." << std::endl;
std::lock_guard<std::mutex> lg(mut);
p_data = data;
}
void clear_parser_data() {
//std::cout << "parser clear data..." << std::endl;
std::lock_guard<std::mutex> lg(mut);
p_data.command = "";
}
parser_data get_parser_data() {
//std::cout << "parser get data..." << std::endl;
std::lock_guard<std::mutex> lg(mut);
parser_data temp= p_data;
//clear_parser_data();
return temp;
}
void parse_input(std::string input) {
parser_data temp_data;
qi::parse(
input.begin(), input.end(),
(*qi::char_("a-zA-Z") >> ' ' >> *qi::char_("a-zA-Z") >> ' ' >> *qi::char_("a-zA-Z") >> ' ' >> qi::double_),
temp_data.command, temp_data.device, temp_data.option, temp_data.command_val
);
std::cout << temp_data.command << " ... " << temp_data.device << " ... " << temp_data.option << " ... " << temp_data.command_val << std::endl;
set_parser_data(temp_data);
std::cout << "parser data set" << temp_data.command_val << std::endl;
}
};
struct CMD_eval {
InputCommands eval(Parser& parse) {
if (parse.p_data.command == "start") {
std::cout << "starting Camera" << std::endl;
parse.clear_parser_data();
return startcamera;
}
else if (parse.p_data.command == "stop") {
std::cout << "stopping Camera" << std::endl;
parse.clear_parser_data();
return stopcamera;
}
else if (parse.p_data.command == "debug") {
parse.clear_parser_data();
return setdebug;
}
else if (parse.p_data.command == "set") {
//std::cout << "setting" << std::endl;
if (parse.p_data.device == "camera") {
parse.clear_parser_data();
return setcamera;
}
else if (parse.p_data.device == "fps") {
parse.clear_parser_data();
return setimagerate;
}
else if (parse.p_data.device == "motorctl") {
parse.clear_parser_data();
return setmotorcontrol;
}
}
else {
return no_cmd;
}
}
};

144
Serial.h
View File

@ -1,144 +0,0 @@
#include <boost/asio.hpp>
#include <iostream>
#include <thread>
#include <mutex>
#include <string>
struct motor_info
{
int Xenc;
int Xerr;
int sgt_val;
int sgt_stat;
int is_moving;
int control_status;
float hdg;
int deviation_warn;
int temp;
int humid;
int fan_pwm;
};
class SerialPort {
public:
SerialPort(const std::string& port, unsigned int baud_rate)
: io_service_(), serial_(io_service_) {
boost::system::error_code ec;
serial_.open(port, ec);
if (ec) {
throw std::runtime_error("Failed to open port: " + ec.message());
}
setOption(boost::asio::serial_port_base::baud_rate(baud_rate), "baud_rate");
setOption(boost::asio::serial_port_base::character_size(8), "character_size");
setOption(boost::asio::serial_port_base::parity(boost::asio::serial_port_base::parity::none), "parity");
setOption(boost::asio::serial_port_base::stop_bits(boost::asio::serial_port_base::stop_bits::one), "stop_bits");
setOption(boost::asio::serial_port_base::flow_control(boost::asio::serial_port_base::flow_control::none), "flow_control");
}
void startReading() {
read();
}
void sendCommand(std::string cmd) {
write(cmd);
}
void write(const std::string& data) {
boost::asio::async_write(serial_, boost::asio::buffer(data),
[](const boost::system::error_code& error, std::size_t) {
if (error) {
std::cerr << "Write failed: " << error.message() << std::endl;
}
}
);
}
void run() {
io_service_.run();
}
void stop() {
io_service_.stop();
}
void set_controller_info(motor_info& inf) {
std::lock_guard<std::mutex> lg(mut);
motorcontroller_information = inf;
}
motor_info get_controller_info() {
std::lock_guard<std::mutex> lg(mut);
return motorcontroller_information;
}
bool parser(std::string& message)
{
if (message[0] == '$') {
//std::cout << message;
std::vector<std::string> values;
std::string delimiter = ";";
size_t pos = 0;
while ((pos = message.find(delimiter)) != std::string::npos) {
values.push_back(message.substr(0, pos));
message.erase(0, pos + delimiter.length());
}
//for (int i = 0; i < values.size(); i++) {
// std::cout << values[i] << std::endl;
//}
if (values.size() == 12) {
motor_info tmp_info;
tmp_info.Xenc = stoi(values[1]);
tmp_info.Xerr = stoi(values[2]);
tmp_info.sgt_val = stoi(values[3]);
tmp_info.sgt_stat = stoi(values[4]);
tmp_info.is_moving = stoi(values[5]);
tmp_info.control_status = stoi(values[6]);
tmp_info.hdg = stof(values[7]);
tmp_info.deviation_warn = stoi(values[8]);
tmp_info.humid = stoi(values[9]);
tmp_info.temp = stoi(values[10]);
tmp_info.fan_pwm = stoi(values[11]);
set_controller_info(tmp_info);
return true;
}
return false;
//else
//std::cout << values.size() << std::endl;
}
}
private:
std::mutex mut;
motor_info motorcontroller_information;
template <typename Option>
void setOption(const Option& option, const std::string& option_name) {
boost::system::error_code ec;
serial_.set_option(option, ec);
if (ec) {
throw std::runtime_error("Failed to set " + option_name + ": " + ec.message());
}
}
void read() {
boost::asio::async_read_until(serial_, buffer_, '\n',
[this](const boost::system::error_code& error, std::size_t bytes_transferred) {
if (!error) {
std::istream is(&buffer_);
std::string line;
std::getline(is, line);
//std::cout << "Received: " << line << std::endl;
parser(line);
read(); // Continue reading
}
else {
std::cerr << "Read failed: " << error.message() << std::endl;
}
}
);
}
boost::asio::io_service io_service_;
boost::asio::serial_port serial_;
boost::asio::streambuf buffer_;
};

File diff suppressed because it is too large Load Diff

41
include/fgc/Application.h Normal file
View File

@ -0,0 +1,41 @@
#pragma once
#include "fgc/Config.h"
#include <memory>
#include <optional>
#include <string>
namespace fgc {
// Command-line / runtime options that override config at launch.
struct RuntimeOptions {
bool init = false; // run the endstop-finding init sequence
bool start = false; // begin capture automatically
bool demo = false; // demo mode (copy placeholder image instead of encoding)
// Tri-state feature overrides (unset => use the config value).
std::optional<bool> use_mqtt;
std::optional<bool> mock_camera;
std::optional<bool> mock_serial;
std::string log_level; // empty => default
};
// Owns the component lifecycle and the control loop. Builds the concrete
// motor controller, control channel and camera source from config + options
// (real or mock), wires them to the ImagePipeline and CaptureScheduler, and
// runs until "exit" / SIGINT.
class Application {
public:
Application(AppConfig config, RuntimeOptions options);
~Application();
int run();
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace fgc

View File

@ -0,0 +1,28 @@
#pragma once
#include <string>
namespace fgc {
// A parsed console command line: "<verb> [device] [option] [value]".
// Examples:
// "start" -> verb=start
// "set fps 5" -> verb=set device=fps value=5
// "set camera jxlq 2" -> verb=set device=camera option=jxlq value=2
// "set motorctl kd180" -> verb=set device=motorctl option=kd180
struct Command {
std::string verb;
std::string device;
std::string option;
double value = 0.0;
bool has_value = false;
bool empty() const { return verb.empty(); }
};
// Whitespace-tokenizing parser. Robust to commands with or without a trailing
// numeric value and to non-numeric option strings (e.g. raw motor commands).
// Replaces the fragile Boost.Spirit Qi grammar.
Command parseCommand(const std::string& line);
} // namespace fgc

View File

@ -35,6 +35,9 @@ public:
// Software-trigger a capture. Returns true on success.
virtual bool trigger() = 0;
// Optional: change the acquisition frame rate. Default no-op (e.g. mocks).
virtual bool setFrameRate(double /*fps*/) { return false; }
virtual void setFrameCallback(FrameCallback cb) = 0;
// Number of cameras this source manages.

View File

@ -0,0 +1,66 @@
#pragma once
#include "fgc/ICameraSource.h"
#include "fgc/IControlChannel.h"
#include <atomic>
#include <condition_variable>
#include <functional>
#include <mutex>
#include <queue>
#include <string>
#include <thread>
#include <vector>
namespace fgc {
// Consumes captured frames and turns them into stored artifacts: rotate 90 deg
// CCW, encode to JPEG XL, write to <output_dir>/<label>/<timestamp>.jxl, and
// publish a CamEvent. Runs a background worker so encoding never blocks
// acquisition. Split out of the old VimbaHandler::SaveImage so any camera
// source (real or mock) can feed it.
class ImagePipeline {
public:
struct Params {
std::string output_dir;
std::vector<std::string> labels = {"RGB", "ACR", "NIR"}; // by camera index
std::string tower;
double jxl_distance = 2.0; // 0 = lossless
int jxl_effort = 3;
bool demo = false; // copy demo_image instead of encoding
std::string demo_image = "test_smoke.jxl";
bool display = false; // show an OpenCV preview window
};
// heading: returns the current gimbal heading (degrees) for CamEvent.
ImagePipeline(IControlChannel& channel, std::function<float()> heading, Params params);
~ImagePipeline();
void start();
void stop();
// Enqueue a frame for processing (thread-safe; copies the frame).
void submit(const Frame& frame);
// Runtime-adjustable encoder settings (console "set camera ..." commands).
void setDistance(double d) { params_.jxl_distance = d; }
void setEffort(int e) { params_.jxl_effort = e; }
void setDisplay(bool on) { params_.display = on; }
private:
void run();
void process(const Frame& frame);
std::string labelFor(int cam_id) const;
IControlChannel& channel_;
std::function<float()> heading_;
Params params_;
std::queue<Frame> queue_;
std::mutex mutex_;
std::condition_variable cv_;
std::atomic<bool> running_{false};
std::thread worker_;
};
} // namespace fgc

View File

@ -0,0 +1,20 @@
#pragma once
#include <cstdint>
#include <string>
namespace fgc {
// Thin wrapper around libjxl for encoding an 8-bit interleaved image buffer
// to a JPEG XL file. Replaces the header-only JPEG_XL.h class.
class JpegXlEncoder {
public:
// Encode `img` (width*height*channels bytes, 8-bit, 1 or 3 channels) to
// `path`. `distance` is the JPEG XL frame distance (0 = lossless, higher =
// lossier); `effort` is the libjxl effort level. Returns true on success.
static bool encodeToFile(const std::string& path, const uint8_t* img, int width,
int height, int channels, float distance, int effort,
int threads = 3);
};
} // namespace fgc

View File

@ -0,0 +1,62 @@
#pragma once
#include "fgc/IControlChannel.h"
#include <atomic>
#include <memory>
#include <mutex>
#include <string>
#include <mqtt/async_client.h>
namespace fgc {
// Real control channel over MQTT (Eclipse Paho C++). Subscribes to the tower's
// target_HDG / ControlCode topics and publishes StatusCode / CamEvent. Topic
// scheme: GGS/FWT/<tower>/... (see docs/mqtt-api.md). Publishes are QoS 1,
// retained.
//
// Unlike the old MQTTClient it does NOT spin a busy-wait thread; Paho's async
// client delivers callbacks on its own threads.
class MqttControlChannel : public IControlChannel,
public virtual mqtt::callback,
public virtual mqtt::iaction_listener {
public:
MqttControlChannel(const std::string& broker_ip, const std::string& tower,
const std::string& user, const std::string& password);
~MqttControlChannel() override;
bool connect() override;
void disconnect() override;
bool connected() const override;
void publishStatus(int code) override;
void publishCamEvent(const CamEvent& event) override;
ControlCommand poll() override;
private:
// mqtt::callback
void connected(const std::string& cause) override;
void connection_lost(const std::string& cause) override;
void message_arrived(mqtt::const_message_ptr msg) override;
// mqtt::iaction_listener
void on_failure(const mqtt::token& tok) override;
void on_success(const mqtt::token& tok) override {}
void publish(const std::string& topic, const std::string& payload);
std::string tower_;
std::string topic_target_hdg_;
std::string topic_control_code_;
std::string topic_status_;
std::string topic_cam_event_;
mqtt::async_client client_;
mqtt::connect_options conn_opts_;
std::atomic<bool> connected_{false};
std::mutex mutex_;
ControlCommand sub_data_;
};
} // namespace fgc

View File

@ -0,0 +1,30 @@
#pragma once
#include "fgc/IMotorController.h"
#include <memory>
#include <string>
namespace fgc {
// Real motor controller over a serial port (Boost.Asio). Async-reads '$'-
// prefixed telemetry lines and parses them into MotorTelemetry; writes command
// strings. Wraps the logic of the old Serial.h SerialPort. Implementation
// detail (Boost.Asio) is hidden behind a pImpl.
class SerialMotorController : public IMotorController {
public:
SerialMotorController(std::string device, unsigned int baud);
~SerialMotorController() override;
void start() override;
void stop() override;
void sendCommand(const std::string& cmd) override;
MotorTelemetry telemetry() override;
bool connected() const override;
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace fgc

View File

@ -0,0 +1,34 @@
#pragma once
#include "fgc/ICameraSource.h"
#include <memory>
#include <string>
#include <vector>
namespace fgc {
// Real camera source backed by the Allied Vision Vimba X SDK (VmbCPP). Opens
// cameras by ID, runs continuous acquisition with a settle-then-keep frame
// observer, and delivers completed frames to the callback. The encode/save
// step lives in ImagePipeline, so this class only produces Frames.
class VimbaCameraSource : public ICameraSource {
public:
explicit VimbaCameraSource(std::vector<std::string> camera_ids);
~VimbaCameraSource() override;
void open() override;
void close() override;
void start() override;
void stop() override;
bool trigger() override;
bool setFrameRate(double fps) override;
void setFrameCallback(FrameCallback cb) override;
int cameraCount() const override;
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace fgc

View File

@ -0,0 +1,57 @@
#pragma once
#include "fgc/ICameraSource.h"
#include "fgc/Logger.h"
#include <chrono>
namespace fgc {
// Synthetic camera for development without hardware. On trigger() it generates
// a small RGB gradient frame (varying with time so successive captures differ)
// and delivers it to the frame callback.
class MockCameraSource : public ICameraSource {
public:
explicit MockCameraSource(int count = 1, uint32_t width = 640, uint32_t height = 480)
: count_(count), width_(width), height_(height) {}
void open() override { LOG_INFO << "[mock] camera opened (" << count_ << ")"; }
void close() override {}
void start() override { LOG_INFO << "[mock] camera acquisition started"; }
void stop() override {}
bool trigger() override {
if (!callback_) return false;
Frame f;
f.width = width_;
f.height = height_;
f.channels = 3;
f.cam_id = 0;
f.timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
f.data.resize(static_cast<size_t>(width_) * height_ * 3);
const uint8_t phase = static_cast<uint8_t>(f.timestamp_ms);
for (uint32_t y = 0; y < height_; ++y) {
for (uint32_t x = 0; x < width_; ++x) {
size_t i = (static_cast<size_t>(y) * width_ + x) * 3;
f.data[i + 0] = static_cast<uint8_t>(x + phase);
f.data[i + 1] = static_cast<uint8_t>(y + phase);
f.data[i + 2] = phase;
}
}
callback_(f);
return true;
}
void setFrameCallback(FrameCallback cb) override { callback_ = std::move(cb); }
int cameraCount() const override { return count_; }
private:
int count_;
uint32_t width_;
uint32_t height_;
FrameCallback callback_;
};
} // namespace fgc

View File

@ -0,0 +1,47 @@
#pragma once
#include "fgc/IMotorController.h"
#include "fgc/Logger.h"
#include <mutex>
#include <string>
namespace fgc {
// Simulated motor controller for development without hardware. Pretends the
// gimbal is continuously sweeping: heading advances on each telemetry read and
// is_moving stays 1 so the capture cycle fires. "kd<heading>" sets the target.
class MockMotorController : public IMotorController {
public:
void start() override { LOG_INFO << "[mock] motor controller started"; }
void stop() override { LOG_INFO << "[mock] motor controller stopped"; }
void sendCommand(const std::string& cmd) override {
LOG_DEBUG << "[mock] motor cmd: " << cmd;
std::lock_guard<std::mutex> lock(mutex_);
if (cmd.rfind("kd", 0) == 0) {
try {
tel_.heading = std::stof(cmd.substr(2));
} catch (const std::exception&) {}
}
}
MotorTelemetry telemetry() override {
std::lock_guard<std::mutex> lock(mutex_);
tel_.heading += 6.0f; // simulate a sweep step
if (tel_.heading >= 360.f) tel_.heading -= 360.f;
tel_.is_moving = 1;
tel_.temperature = 20;
tel_.humidity = 50;
tel_.fan_pwm = 0;
return tel_;
}
bool connected() const override { return true; }
private:
std::mutex mutex_;
MotorTelemetry tel_;
};
} // namespace fgc

View File

@ -0,0 +1,26 @@
#pragma once
#include "fgc/IControlChannel.h"
#include "fgc/Logger.h"
namespace fgc {
// No-op control channel for running without an MQTT broker. Publishes are
// dropped (logged at debug) and poll() never supplies a control command, so
// the scheduler stays in automatic-sweep mode (ControlCode 0).
class NullControlChannel : public IControlChannel {
public:
bool connect() override { return true; }
void disconnect() override {}
bool connected() const override { return true; }
void publishStatus(int code) override { LOG_DEBUG << "[null] status " << code; }
void publishCamEvent(const CamEvent& e) override {
LOG_DEBUG << "[null] cam event " << e.camera << " hdg=" << e.heading_decideg
<< " t=" << e.timestamp_ms;
}
ControlCommand poll() override { return {}; }
};
} // namespace fgc

407
main.cpp
View File

@ -1,338 +1,69 @@
#include <exception>
#include <iostream>
#include "cxxopts.hpp"
#include <thread>
#include <string>
#include <atomic>
#include <VmbC/VmbC.h>
#include <boost/program_options.hpp>
#include "Parser.h"
#include "Camera.h"
#include "MQTT.h"
#include "timing.h"
#include "fgc/Config.h"
#include "fgc/Paths.h"
namespace po = boost::program_options;
std::atomic<bool> running(true);
Timer loop_timer;
bool motorctl_info_out=false;
bool init_gimbal = false;
bool start_gimbal = false;
bool start_demo = false;
bool trigger_after_stopping = false;
double imagerate = 0.1;
int ctl_code = 0;
std::string mqtt_hdg = "0";
Parser parse_cmd;
std::string fwt_name = "";
std::string server_ip = "";
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" };
void readInput() {
std::string input;
while (running) {
if (!std::getline(std::cin, input)) {
// stdin geschlossen oder EOF (z.B. unter systemd)
// Einfach Thread schlafen legen und nichts tun
std::this_thread::sleep_for(std::chrono::milliseconds(500));
continue;
}
if (input.empty()) continue; // leere Zeilen ignorieren
if (input == "exit") {
running = false;
break;
}
std::cout << "Received input: " << input << "\n";
parse_cmd.parse_input(input);
}
}
int main(int argc, char* argv[])
{
// 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;
}
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
// Declare the supported options
po::options_description desc("Allowed options");
desc.add_options()
("help,h", "produce help message")
("init,i", po::value<bool>(), "find endstops")
("start,s", po::value<bool>(), "start gimbal system automatically")
("demo,d", po::value<bool>(), "demo mode (simulation)");
po::variables_map vm;
try {
// Parse command-line options
po::store(po::parse_command_line(argc, argv, desc), vm);
po::notify(vm);
if (vm.count("help")) {
std::cout << desc << "\n";
return 0;
}
if (vm.count("init")) {
std::cout << "find endstops " << vm["init"].as<bool>() << ".\n";
init_gimbal = true;
}
else {
std::cout << "init was not set.\n"; init_gimbal = false;
}
if (vm.count("start")) {
std::cout << "starting gimbal is: " << vm["start"].as<bool>() << ".\n";
start_gimbal = true;
}
else {
std::cout << "Not starting Gimbal automatically.\n"; start_gimbal = false;
}
if (vm.count("demo")) {
std::cout << "demomode is: " << vm["demo"].as<bool>() << ".\n";
start_demo = true;
}
else {
std::cout << "demomode is not set.\n"; start_demo = false;
}
}
catch (std::exception& e) {
std::cerr << "ARGS Error: " << e.what() << "\n";
return 1;
}
catch (...) {
std::cerr << "Unknown ARGS error!" << "\n";
return 1;
}
//main vars
SerialPort serial(cfg.serial.device, cfg.serial.baud);
std::thread io_thread;
const std::string mqtt_STATUS = "GGS/FWT/" + fwt_name + "/StatusCode";
mqtt_client =new MQTTClient(server_ip, fwt_name, fwt_name, mqtt_user, mqtt_pw);
mqtt_client->connect_client();
std::this_thread::sleep_for(std::chrono::milliseconds(500));
if (!mqtt_client->connected) {
std::cerr << "MQTT not connected, exit!" << "\n";
return 1;
}
mqtt_client->publish(mqtt_STATUS, "0");
//cmd parser
/*
try {
cxxopts::Options options("Firewatch Gimbal Software", "Base Gimbal Software C++");
options.add_options()
("h,help", "Print usage")
("d,debug", "Enable debugging")
("f,file", "File name", cxxopts::value<std::string>())
("i,integer", "An integer", cxxopts::value<int>()->default_value("10"))
;
auto result = options.parse(argc, argv);
if (result.count("help")) {
std::cout << options.help() << std::endl;
return 0;
}
if (result.count("debug")) {
std::cout << "Debugging enabled" << std::endl;
}
if (result.count("file")) {
std::cout << "File: " << result["file"].as<std::string>() << std::endl;
}
std::cout << "Integer: " << result["integer"].as<int>() << std::endl;
}
catch (const cxxopts::exceptions::exception& e) {
std::cerr << "Error parsing options: " << e.what() << std::endl;
return 1;
}
*/
//serial communication
try {
// Start reading from the serial port
serial.startReading();
// Run Boost.Asio's I/O service in a separate thread
io_thread=std::thread([&serial]() {
serial.run();
});
// Send commands in the main thread or from another thread
}
catch (const std::exception& e) {
std::cerr << "Serial Exception: " << e.what() << std::endl;
}
//start cin thread
std::thread inputThread(readInput);
const std::chrono::milliseconds interval(100);
motor_info act_info;
//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));
serial.sendCommand("r");
std::this_thread::sleep_for(std::chrono::milliseconds(500));
serial.sendCommand("e");
std::this_thread::sleep_for(std::chrono::milliseconds(500));
serial.sendCommand("q");
std::this_thread::sleep_for(std::chrono::milliseconds(60000));
act_info = serial.get_controller_info();
//while (act_info.is_moving==0) {
// std::this_thread::sleep_for(std::chrono::milliseconds(500));
// act_info = serial.get_controller_info();
// if (act_info.is_moving == 1) {
// std::this_thread::sleep_for(std::chrono::milliseconds(2000));
// act_info = serial.get_controller_info();
// }
//}
serial.sendCommand("y");
std::this_thread::sleep_for(std::chrono::milliseconds(500));
serial.sendCommand("ud56"); // set number of imtervals per turn
std::cout << "Gimbal init finished." << std::endl;
}
//loop
while (running) {
//check parser and evaluate commands
parser_data cmd_data = parse_cmd.get_parser_data();
CMD_eval eval;
InputCommands cmds;
cmds=eval.eval(parse_cmd);
if (cmds != no_cmd) {
if (cmds == startcamera) {
std::cout << "starting Camera" << std::endl;
cam_handler.Start();
}
else if (cmds == stopcamera) {
std::cout << "stopping Camera" << std::endl;
cam_handler.Stop();
}
else if (cmds == setcamera) {
cam_handler.evaluateCommand(cmd_data.option, cmd_data.command_val);
}
else if (cmds == setmotorcontrol) {
std::cout << "send command: " << cmd_data.option << std::endl;
serial.sendCommand(cmd_data.option);
}
else if (cmds == setimagerate) {
imagerate = cmd_data.command_val;
std::cout << "imagerate is: " << imagerate << std::endl;
}
else if (cmds == setdebug) {
motorctl_info_out = !motorctl_info_out;
std::cout << "debug is: " << motorctl_info_out << std::endl;
}
}if (start_gimbal) {
std::cout << "starting Camera" << std::endl;
cam_handler.Start();
start_gimbal = false;
}
act_info= serial.get_controller_info();
if(motorctl_info_out)
std::cout << "_________________________________\nEncoder act: "<< act_info.Xenc << "\nHDG: " << act_info.hdg << "\nEncoder err: " << act_info.Xerr << "\nsgt_stat/sgt_val/not_moving: " << act_info.sgt_stat <<" / "<<act_info.sgt_val << " / " << act_info.is_moving << "\nDriver Status: " << act_info.control_status << "\nDeviation Warning: " << act_info.deviation_warn << "\ntemp/humid/fan_pwm(0-255): " << act_info.temp << " / " << act_info.humid << " / " << act_info.fan_pwm << std::endl;
//mqtt
mqtt_in = mqtt_client->callback.get_sub_data();
if (mqtt_in.ctl_avail) {
ctl_code = mqtt_in.control_code;
mqtt_client->publish(mqtt_STATUS, std::to_string(ctl_code));
}
if (mqtt_in.hdg_avail) {
mqtt_hdg = mqtt_in.target_heading;
}
//gimbal control
if (ctl_code == 0) {
if (loop_timer.ElapsedMillis() > (1000 / imagerate) && cam_handler.cam_started) {
if (act_info.is_moving == 1) {
loop_timer.Reset();
trigger_after_stopping = true;
serial.sendCommand("p");
//if (cam_handler.TriggerCamera()) {
//}
//wait for stop and trigger
}
}
if (trigger_after_stopping && loop_timer.ElapsedMillis() > 100) {
if (act_info.is_moving == 1) {
if (cam_handler.TriggerCamera()) {
std::cout << "Temperature:" << act_info.temp << ", Humid:" << act_info.humid << ", FAN RPM:" << act_info.fan_pwm << std::endl;
trigger_after_stopping = false;
}
}
}
}
else if (ctl_code == 1) {
if (loop_timer.ElapsedMillis() > (1000 / imagerate)&& cam_handler.cam_started) {
if (act_info.is_moving == 1) {
loop_timer.Reset();
trigger_after_stopping = true;
serial.sendCommand("kd"+mqtt_hdg);
std::cout << "ctl" << ctl_code << std::endl;
std::cout << "hdg" << mqtt_hdg << std::endl;
//if (cam_handler.TriggerCamera()) {
//}
//wait for stop and trigger
}
}
if (trigger_after_stopping && loop_timer.ElapsedMillis() > 100) {
if (act_info.is_moving == 1) {
if (cam_handler.TriggerCamera()) {
trigger_after_stopping = false;
}
}
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
//stop serial thread
serial.stop();
io_thread.join();
//stop cin thread
inputThread.join();
// Clean up
delete mqtt_client;
mqtt_client = nullptr;
return 0;
}
#include "fgc/Application.h"
#include "fgc/Config.h"
#include "fgc/Logger.h"
#include "fgc/Paths.h"
#include <iostream>
#include <boost/program_options.hpp>
namespace po = boost::program_options;
using namespace fgc;
int main(int argc, char* argv[]) {
po::options_description desc("Fire Gimbal Control options");
desc.add_options()
("help,h", "show this help")
("config,c", po::value<std::string>(), "path to config.ini")
("init,i", po::bool_switch(), "run the endstop-finding init sequence")
("start,s", po::bool_switch(), "start capture automatically")
("demo,d", po::bool_switch(), "demo mode (copy placeholder image instead of encoding)")
("no-mqtt", po::bool_switch(), "disable MQTT (use a null control channel)")
("mock-camera", po::bool_switch(), "use a simulated camera (no hardware)")
("mock-serial", po::bool_switch(), "use a simulated motor controller (no hardware)")
("log-level", po::value<std::string>(), "trace|debug|info|warn|error|off");
po::variables_map vm;
try {
po::store(po::parse_command_line(argc, argv, desc), vm);
po::notify(vm);
} catch (const std::exception& e) {
std::cerr << "Argument error: " << e.what() << "\n";
return 1;
}
if (vm.count("help")) {
std::cout << desc << "\n";
return 0;
}
const std::string cli_cfg = vm.count("config") ? vm["config"].as<std::string>() : "";
auto cfg_path = paths::resolveConfigPath(cli_cfg);
if (!cfg_path) {
std::cerr << "No config.ini found. Searched:\n";
for (const auto& p : paths::configSearchPaths(cli_cfg)) std::cerr << " - " << p << "\n";
std::cerr << "Copy config/config.example.ini to one of these, pass --config, "
"or set $FGC_CONFIG.\n";
return 1;
}
AppConfig cfg;
try {
cfg = ConfigLoader::loadFromFile(*cfg_path);
} catch (const std::exception& e) {
std::cerr << "Config error (" << *cfg_path << "): " << e.what() << "\n";
return 1;
}
LOG_INFO << "Loaded config: " << *cfg_path;
RuntimeOptions opts;
opts.init = vm["init"].as<bool>();
opts.start = vm["start"].as<bool>();
opts.demo = vm["demo"].as<bool>();
if (vm["no-mqtt"].as<bool>()) opts.use_mqtt = false;
if (vm["mock-camera"].as<bool>()) opts.mock_camera = true;
if (vm["mock-serial"].as<bool>()) opts.mock_serial = true;
if (vm.count("log-level")) opts.log_level = vm["log-level"].as<std::string>();
Application app(std::move(cfg), std::move(opts));
return app.run();
}

View File

@ -0,0 +1,106 @@
#include "fgc/ImagePipeline.h"
#include "fgc/JpegXlEncoder.h"
#include "fgc/Logger.h"
#include <filesystem>
#include <opencv2/opencv.hpp>
namespace fs = std::filesystem;
namespace fgc {
ImagePipeline::ImagePipeline(IControlChannel& channel, std::function<float()> heading,
Params params)
: channel_(channel), heading_(std::move(heading)), params_(std::move(params)) {}
ImagePipeline::~ImagePipeline() { stop(); }
void ImagePipeline::start() {
if (running_.exchange(true)) return;
worker_ = std::thread(&ImagePipeline::run, this);
}
void ImagePipeline::stop() {
if (!running_.exchange(false)) return;
cv_.notify_all();
if (worker_.joinable()) worker_.join();
}
void ImagePipeline::submit(const Frame& frame) {
{
std::lock_guard<std::mutex> lock(mutex_);
queue_.push(frame);
}
cv_.notify_one();
}
void ImagePipeline::run() {
while (running_) {
Frame frame;
{
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this] { return !queue_.empty() || !running_; });
if (!running_ && queue_.empty()) break;
frame = std::move(queue_.front());
queue_.pop();
}
process(frame);
}
}
std::string ImagePipeline::labelFor(int cam_id) const {
if (cam_id >= 0 && cam_id < static_cast<int>(params_.labels.size()))
return params_.labels[cam_id];
return "cam" + std::to_string(cam_id);
}
void ImagePipeline::process(const Frame& frame) {
if (frame.channels != 1 && frame.channels != 3) {
LOG_WARN << "Dropping frame with unsupported channel count " << frame.channels;
return;
}
const int type = frame.channels == 1 ? CV_8UC1 : CV_8UC3;
cv::Mat src(frame.height, frame.width, type, const_cast<uint8_t*>(frame.data.data()));
cv::Mat img;
cv::rotate(src, img, cv::ROTATE_90_COUNTERCLOCKWISE);
if (params_.display) {
cv::namedWindow("Display Image", cv::WINDOW_NORMAL);
cv::resizeWindow("Display Image", img.cols / 4, img.rows / 4);
cv::imshow("Display Image", img);
cv::waitKey(10);
}
const std::string label = labelFor(frame.cam_id);
const fs::path dir = fs::path(params_.output_dir) / label;
std::error_code ec;
fs::create_directories(dir, ec);
const fs::path file = dir / (std::to_string(frame.timestamp_ms) + ".jxl");
if (params_.demo) {
fs::copy_file(params_.demo_image, file, fs::copy_options::overwrite_existing, ec);
if (ec) LOG_ERROR << "Demo copy failed: " << ec.message();
} else {
bool ok = JpegXlEncoder::encodeToFile(file.string(), img.data, img.cols, img.rows,
frame.channels, params_.jxl_distance,
params_.jxl_effort);
if (!ok) {
LOG_ERROR << "Failed to encode " << file.string();
return;
}
}
CamEvent ev;
ev.tower = params_.tower;
ev.camera = label;
ev.heading_decideg = static_cast<int>((heading_ ? heading_() : 0.f) * 10);
ev.timestamp_ms = frame.timestamp_ms;
channel_.publishCamEvent(ev);
LOG_DEBUG << "Saved " << file.string() << " (" << img.cols << "x" << img.rows << ")";
}
} // namespace fgc

View File

@ -0,0 +1,98 @@
#include "fgc/JpegXlEncoder.h"
#include "fgc/Logger.h"
#include <cstdio>
#include <vector>
#include <jxl/encode.h>
#include <jxl/encode_cxx.h>
#include <jxl/thread_parallel_runner.h>
#include <jxl/thread_parallel_runner_cxx.h>
#include <jxl/types.h>
namespace fgc {
bool JpegXlEncoder::encodeToFile(const std::string& path, const uint8_t* img, int width,
int height, int channels, float distance, int effort,
int threads) {
auto enc = JxlEncoderMake(nullptr);
auto runner = JxlThreadParallelRunnerMake(nullptr, threads);
if (JXL_ENC_SUCCESS !=
JxlEncoderSetParallelRunner(enc.get(), JxlThreadParallelRunner, runner.get())) {
LOG_ERROR << "JxlEncoderSetParallelRunner failed";
return false;
}
JxlPixelFormat pixel_format = {static_cast<uint32_t>(channels), JXL_TYPE_UINT8,
JXL_NATIVE_ENDIAN, 0};
JxlBasicInfo basic_info;
JxlEncoderInitBasicInfo(&basic_info);
basic_info.xsize = width;
basic_info.ysize = height;
basic_info.alpha_bits = 0;
basic_info.bits_per_sample = 8;
basic_info.num_color_channels = channels;
basic_info.uses_original_profile = JXL_FALSE;
if (JXL_ENC_SUCCESS != JxlEncoderSetBasicInfo(enc.get(), &basic_info)) {
LOG_ERROR << "JxlEncoderSetBasicInfo failed";
return false;
}
JxlColorEncoding color_encoding = {};
JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/channels < 3);
if (JXL_ENC_SUCCESS != JxlEncoderSetColorEncoding(enc.get(), &color_encoding)) {
LOG_ERROR << "JxlEncoderSetColorEncoding failed";
return false;
}
JxlEncoderFrameSettings* fs = JxlEncoderFrameSettingsCreate(enc.get(), nullptr);
JxlEncoderFrameSettingsSetOption(fs, JXL_ENC_FRAME_SETTING_EFFORT, effort);
JxlEncoderFrameSettingsSetOption(fs, JXL_ENC_FRAME_SETTING_DECODING_SPEED, 0);
if (distance == 0.0f) {
JxlEncoderSetFrameDistance(fs, 0);
JxlEncoderSetFrameLossless(fs, JXL_TRUE);
} else {
JxlEncoderSetFrameLossless(fs, JXL_FALSE);
JxlEncoderSetFrameDistance(fs, distance);
}
const size_t byte_size = static_cast<size_t>(width) * height * channels;
if (JXL_ENC_SUCCESS !=
JxlEncoderAddImageFrame(fs, &pixel_format, img, byte_size)) {
LOG_ERROR << "JxlEncoderAddImageFrame failed";
return false;
}
JxlEncoderCloseInput(enc.get());
std::vector<uint8_t> compressed(64);
uint8_t* next_out = compressed.data();
size_t avail_out = compressed.size();
JxlEncoderStatus result = JXL_ENC_NEED_MORE_OUTPUT;
while (result == JXL_ENC_NEED_MORE_OUTPUT) {
result = JxlEncoderProcessOutput(enc.get(), &next_out, &avail_out);
if (result == JXL_ENC_NEED_MORE_OUTPUT) {
size_t offset = next_out - compressed.data();
compressed.resize(compressed.size() * 2);
next_out = compressed.data() + offset;
avail_out = compressed.size() - offset;
}
}
if (JXL_ENC_SUCCESS != result) {
LOG_ERROR << "JxlEncoderProcessOutput failed";
return false;
}
compressed.resize(next_out - compressed.data());
FILE* file = std::fopen(path.c_str(), "wb");
if (!file) {
LOG_ERROR << "Could not open " << path << " for writing";
return false;
}
bool ok = std::fwrite(compressed.data(), 1, compressed.size(), file) == compressed.size();
if (std::fclose(file) != 0) ok = false;
if (!ok) LOG_ERROR << "Could not write " << path;
return ok;
}
} // namespace fgc

View File

@ -0,0 +1,161 @@
#include "fgc/VimbaCameraSource.h"
#include "fgc/Logger.h"
#include <atomic>
#include <chrono>
#include <stdexcept>
#include <thread>
#include <VmbCPP/VmbCPP.h>
using namespace VmbCPP;
namespace fgc {
namespace {
long long nowMs() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
}
// Frame observer: dumps the first few frames after each trigger (sensor
// settling), then delivers completed frames to the source callback.
class FrameObserver : public IFrameObserver {
public:
FrameObserver(CameraPtr camera, int cam_id, ICameraSource::FrameCallback cb)
: IFrameObserver(camera), cam_id_(cam_id), cb_(std::move(cb)) {}
void resetSettle() { settle_.store(3); }
void FrameReceived(const FramePtr pFrame) override {
if (settle_.fetch_sub(1) > 0) { // still settling -> discard
m_pCamera->QueueFrame(pFrame);
return;
}
VmbFrameStatusType status;
if (pFrame->GetReceiveStatus(status) == VmbErrorSuccess &&
status == VmbFrameStatusComplete) {
VmbUint32_t w = 0, h = 0, sz = 0;
const VmbUchar_t* buf = nullptr;
if (pFrame->GetWidth(w) == VmbErrorSuccess &&
pFrame->GetHeight(h) == VmbErrorSuccess &&
pFrame->GetBufferSize(sz) == VmbErrorSuccess &&
pFrame->GetBuffer(buf) == VmbErrorSuccess && buf && w && h) {
Frame f;
f.width = w;
f.height = h;
f.channels = static_cast<int>(sz / (static_cast<size_t>(w) * h));
f.cam_id = cam_id_;
f.timestamp_ms = nowMs();
f.data.assign(buf, buf + sz);
if (cb_) cb_(f);
}
}
m_pCamera->QueueFrame(pFrame);
}
private:
int cam_id_;
ICameraSource::FrameCallback cb_;
std::atomic<int> settle_{3};
};
} // namespace
struct VimbaCameraSource::Impl {
explicit Impl(std::vector<std::string> ids)
: camera_ids(std::move(ids)), sys(VmbSystem::GetInstance()) {}
std::vector<std::string> camera_ids;
VmbSystem& sys;
std::vector<CameraPtr> cameras;
std::vector<IFrameObserverPtr> observers;
ICameraSource::FrameCallback callback;
bool started = false;
};
VimbaCameraSource::VimbaCameraSource(std::vector<std::string> camera_ids)
: impl_(std::make_unique<Impl>(std::move(camera_ids))) {}
VimbaCameraSource::~VimbaCameraSource() {
try {
stop();
close();
} catch (const std::exception& e) {
LOG_WARN << "VimbaCameraSource shutdown: " << e.what();
}
}
void VimbaCameraSource::open() {
if (impl_->sys.Startup() != VmbErrorSuccess)
throw std::runtime_error("Could not start Vimba X API");
for (const auto& id : impl_->camera_ids) {
CameraPtr cam;
if (impl_->sys.GetCameraByID(id.c_str(), cam) != VmbErrorSuccess)
throw std::runtime_error("Camera not found: " + id);
if (cam->Open(VmbAccessModeFull) != VmbErrorSuccess)
throw std::runtime_error("Could not open camera: " + id);
impl_->cameras.push_back(cam);
LOG_INFO << "Opened camera " << id;
}
}
void VimbaCameraSource::close() {
for (auto& cam : impl_->cameras) cam->Close();
impl_->cameras.clear();
impl_->sys.Shutdown();
}
void VimbaCameraSource::start() {
int cam_id = 0;
for (auto& cam : impl_->cameras) {
IFrameObserverPtr observer(new FrameObserver(cam, cam_id, impl_->callback));
if (cam->StartContinuousImageAcquisition(5, observer) != VmbErrorSuccess)
throw std::runtime_error("Could not start acquisition");
impl_->observers.push_back(observer);
++cam_id;
}
impl_->started = true;
}
void VimbaCameraSource::stop() {
for (auto& cam : impl_->cameras) cam->StopContinuousImageAcquisition();
impl_->observers.clear();
impl_->started = false;
}
bool VimbaCameraSource::trigger() {
if (impl_->cameras.empty()) return false;
for (auto& obs : impl_->observers)
SP_DYN_CAST<FrameObserver>(obs)->resetSettle();
VmbErrorType result = VmbErrorSuccess;
for (int i = 0; i < 4; ++i) { // 3 settle frames + 1 real
FeaturePtr feature;
if (SP_ACCESS(impl_->cameras[0])->GetFeatureByName("TriggerSoftware", feature) ==
VmbErrorSuccess)
result = feature->RunCommand();
std::this_thread::sleep_for(std::chrono::milliseconds(400));
}
return result == VmbErrorSuccess;
}
bool VimbaCameraSource::setFrameRate(double fps) {
VmbErrorType result = VmbErrorSuccess;
for (auto& cam : impl_->cameras) {
FeaturePtr feature;
if (SP_ACCESS(cam)->GetFeatureByName("AcquisitionFrameRate", feature) == VmbErrorSuccess)
result = feature->SetValue(fps);
}
if (result == VmbErrorSuccess) LOG_INFO << "camera fps set to " << fps;
return result == VmbErrorSuccess;
}
void VimbaCameraSource::setFrameCallback(FrameCallback cb) { impl_->callback = std::move(cb); }
int VimbaCameraSource::cameraCount() const { return static_cast<int>(impl_->cameras.size()); }
} // namespace fgc

238
src/core/Application.cpp Normal file
View File

@ -0,0 +1,238 @@
#include "fgc/Application.h"
#include "fgc/CaptureScheduler.h"
#include "fgc/CommandParser.h"
#include "fgc/ICameraSource.h"
#include "fgc/IControlChannel.h"
#include "fgc/IMotorController.h"
#include "fgc/ImagePipeline.h"
#include "fgc/Logger.h"
#include "fgc/SerialMotorController.h"
#include "fgc/mock/MockCameraSource.h"
#include "fgc/mock/MockMotorController.h"
#include "fgc/mock/NullControlChannel.h"
#include <atomic>
#include <chrono>
#include <iostream>
#include <memory>
#include <mutex>
#include <queue>
#include <thread>
#if FGC_WITH_MQTT
#include "fgc/MqttControlChannel.h"
#endif
#if FGC_WITH_VIMBA
#include "fgc/VimbaCameraSource.h"
#endif
namespace fgc {
struct Application::Impl {
Impl(AppConfig c, RuntimeOptions o) : cfg(std::move(c)), opts(std::move(o)) {}
AppConfig cfg;
RuntimeOptions opts;
std::unique_ptr<IControlChannel> channel;
std::unique_ptr<IMotorController> motor;
std::unique_ptr<ICameraSource> camera;
std::unique_ptr<ImagePipeline> pipeline;
std::unique_ptr<CaptureScheduler> scheduler;
std::atomic<bool> running{true};
std::mutex cmd_mutex;
std::queue<std::string> cmd_queue;
std::thread input_thread;
std::string statusTopicTower() const { return cfg.general.tower_name; }
std::unique_ptr<IControlChannel> makeChannel() {
bool want = opts.use_mqtt.value_or(cfg.features.enable_mqtt);
#if FGC_WITH_MQTT
if (want) {
return std::make_unique<MqttControlChannel>(cfg.network.broker_ip,
cfg.general.tower_name,
cfg.network.mqtt_user,
cfg.network.mqtt_pw);
}
#else
if (want) LOG_WARN << "MQTT requested but binary built without MQTT; using null channel";
#endif
return std::make_unique<NullControlChannel>();
}
std::unique_ptr<IMotorController> makeMotor() {
bool mock = opts.mock_serial.value_or(cfg.features.mock_serial);
if (mock) return std::make_unique<MockMotorController>();
return std::make_unique<SerialMotorController>(cfg.serial.device, cfg.serial.baud);
}
std::unique_ptr<ICameraSource> makeCamera() {
bool mock = opts.mock_camera.value_or(cfg.features.mock_camera);
#if !FGC_WITH_VIMBA
if (!mock) {
LOG_WARN << "Camera requested but binary built without Vimba X; using mock camera";
mock = true;
}
#endif
if (mock) {
int count = cfg.camera.ids.empty() ? 1 : static_cast<int>(cfg.camera.ids.size());
return std::make_unique<MockCameraSource>(count);
}
#if FGC_WITH_VIMBA
return std::make_unique<VimbaCameraSource>(cfg.camera.ids);
#else
return std::make_unique<MockCameraSource>(1);
#endif
}
void runInitSequence() {
using namespace std::chrono_literals;
LOG_INFO << "Running gimbal init sequence (find endstops)";
motor->sendCommand("r");
std::this_thread::sleep_for(500ms);
motor->sendCommand("e");
std::this_thread::sleep_for(500ms);
motor->sendCommand("q");
std::this_thread::sleep_for(60s);
motor->sendCommand("y");
std::this_thread::sleep_for(500ms);
motor->sendCommand("ud56"); // intervals per turn
LOG_INFO << "Gimbal init finished";
}
void startCapture() {
camera->start();
scheduler->setCaptureActive(true);
LOG_INFO << "Capture started";
}
void stopCapture() {
camera->stop();
scheduler->setCaptureActive(false);
LOG_INFO << "Capture stopped";
}
void handleCommand(const std::string& line) {
Command c = parseCommand(line);
if (c.empty()) return;
if (c.verb == "exit") {
running = false;
} else if (c.verb == "start") {
startCapture();
} else if (c.verb == "stop") {
stopCapture();
} else if (c.verb == "debug") {
bool on = Logger::level() != LogLevel::Debug;
Logger::setLevel(on ? LogLevel::Debug : LogLevel::Info);
LOG_INFO << "debug logging " << (on ? "on" : "off");
} else if (c.verb == "set") {
if (c.device == "camera") {
if (c.option == "fps" && c.has_value) camera->setFrameRate(c.value);
else if (c.option == "jxlq" && c.has_value) pipeline->setDistance(c.value);
else if (c.option == "jxle" && c.has_value) pipeline->setEffort(static_cast<int>(c.value));
else if (c.option == "display") pipeline->setDisplay(c.value != 0.0);
else LOG_WARN << "unknown 'set camera' option: " << c.option;
} else if (c.device == "fps" && c.has_value) {
scheduler->setImageRate(c.value);
LOG_INFO << "capture rate set to " << c.value << " img/s";
} else if (c.device == "motorctl") {
motor->sendCommand(c.option);
} else {
LOG_WARN << "unknown 'set' target: " << c.device;
}
} else {
LOG_WARN << "unknown command: " << c.verb;
}
}
void inputLoop() {
std::string line;
while (running) {
if (!std::getline(std::cin, line)) {
std::this_thread::sleep_for(std::chrono::milliseconds(500));
continue;
}
if (line.empty()) continue;
std::lock_guard<std::mutex> lock(cmd_mutex);
cmd_queue.push(line);
}
}
void drainCommands() {
std::queue<std::string> local;
{
std::lock_guard<std::mutex> lock(cmd_mutex);
std::swap(local, cmd_queue);
}
while (!local.empty()) {
handleCommand(local.front());
local.pop();
}
}
int run() {
if (!opts.log_level.empty() && !Logger::setLevelFromString(opts.log_level))
LOG_WARN << "unknown log level '" << opts.log_level << "', keeping default";
channel = makeChannel();
motor = makeMotor();
camera = makeCamera();
if (!channel->connect())
LOG_WARN << "Control channel not connected; continuing in degraded mode";
ImagePipeline::Params pp;
pp.output_dir = cfg.paths.output_dir;
pp.labels = cfg.camera.labels;
pp.tower = cfg.general.tower_name;
pp.demo = opts.demo;
pipeline = std::make_unique<ImagePipeline>(
*channel, [this] { return motor->telemetry().heading; }, pp);
IMotorController* motor_ptr = motor.get();
(void)motor_ptr;
ImagePipeline* pipe_ptr = pipeline.get();
camera->setFrameCallback([pipe_ptr](const Frame& f) { pipe_ptr->submit(f); });
scheduler = std::make_unique<CaptureScheduler>(*motor, *camera, *channel,
cfg.image_rate());
motor->start();
camera->open();
pipeline->start();
channel->publishStatus(0);
if (opts.init) runInitSequence();
if (opts.start) startCapture();
input_thread = std::thread(&Impl::inputLoop, this);
LOG_INFO << "Entering control loop (type 'exit' to quit)";
while (running) {
drainCommands();
scheduler->tick();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
LOG_INFO << "Shutting down";
if (input_thread.joinable()) input_thread.join();
pipeline->stop();
camera->stop();
camera->close();
motor->stop();
channel->disconnect();
return 0;
}
};
Application::Application(AppConfig config, RuntimeOptions options)
: impl_(std::make_unique<Impl>(std::move(config), std::move(options))) {}
Application::~Application() = default;
int Application::run() { return impl_->run(); }
} // namespace fgc

View File

@ -0,0 +1,56 @@
#include "fgc/CommandParser.h"
#include <sstream>
#include <vector>
namespace fgc {
namespace {
bool parseNumber(const std::string& s, double& out) {
if (s.empty()) return false;
try {
size_t pos = 0;
out = std::stod(s, &pos);
return pos == s.size(); // entire token must be numeric
} catch (const std::exception&) {
return false;
}
}
} // namespace
Command parseCommand(const std::string& line) {
std::vector<std::string> tok;
std::istringstream iss(line);
std::string t;
while (iss >> t) tok.push_back(t);
Command cmd;
if (tok.empty()) return cmd;
cmd.verb = tok[0];
size_t idx = 1;
if (idx < tok.size()) cmd.device = tok[idx++];
if (idx < tok.size()) {
// If the last token is numeric, treat it as the value.
double v = 0.0;
size_t end = tok.size();
if (parseNumber(tok.back(), v)) {
cmd.value = v;
cmd.has_value = true;
end = tok.size() - 1;
}
// Anything between device and the value becomes the option (joined).
std::string option;
for (size_t i = idx; i < end; ++i) {
if (!option.empty()) option += " ";
option += tok[i];
}
cmd.option = option;
}
return cmd;
}
} // namespace fgc

View File

@ -0,0 +1,123 @@
#include "fgc/MqttControlChannel.h"
#include "fgc/Logger.h"
#include <chrono>
namespace fgc {
namespace {
constexpr int kQos = 1;
const auto kConnectTimeout = std::chrono::seconds(5);
const auto kPublishTimeout = std::chrono::seconds(3);
} // namespace
MqttControlChannel::MqttControlChannel(const std::string& broker_ip, const std::string& tower,
const std::string& user, const std::string& password)
: tower_(tower),
topic_target_hdg_("GGS/FWT/" + tower + "/target_HDG"),
topic_control_code_("GGS/FWT/" + tower + "/ControlCode"),
topic_status_("GGS/FWT/" + tower + "/StatusCode"),
topic_cam_event_("GGS/FWT/" + tower + "/CamEvent"),
client_(broker_ip, tower) {
conn_opts_.set_keep_alive_interval(20);
conn_opts_.set_clean_session(true);
conn_opts_.set_automatic_reconnect(true); // replaces the old manual reconnect loop
if (!user.empty()) conn_opts_.set_user_name(user);
if (!password.empty()) conn_opts_.set_password(password);
client_.set_callback(*this);
}
MqttControlChannel::~MqttControlChannel() { disconnect(); }
bool MqttControlChannel::connect() {
try {
client_.connect(conn_opts_, nullptr, *this)->wait_for(kConnectTimeout);
connected_ = client_.is_connected();
} catch (const mqtt::exception& e) {
LOG_ERROR << "MQTT connect failed: " << e.what();
connected_ = false;
}
if (connected_) LOG_INFO << "MQTT connected to broker as '" << tower_ << "'";
return connected_;
}
void MqttControlChannel::disconnect() {
if (!client_.is_connected()) return;
try {
client_.disconnect()->wait();
} catch (const mqtt::exception& e) {
LOG_WARN << "MQTT disconnect: " << e.what();
}
connected_ = false;
}
bool MqttControlChannel::connected() const { return connected_; }
void MqttControlChannel::connected(const std::string& /*cause*/) {
LOG_INFO << "MQTT (re)connected; subscribing";
client_.subscribe(topic_target_hdg_, kQos);
client_.subscribe(topic_control_code_, kQos);
connected_ = true;
}
void MqttControlChannel::connection_lost(const std::string& cause) {
LOG_WARN << "MQTT connection lost" << (cause.empty() ? "" : ": " + cause);
connected_ = false;
// Paho will auto-reconnect when configured; the broker callback re-subscribes.
}
void MqttControlChannel::on_failure(const mqtt::token& /*tok*/) {
LOG_WARN << "MQTT action failed";
}
void MqttControlChannel::message_arrived(mqtt::const_message_ptr msg) {
const std::string topic = msg->get_topic();
const std::string payload = msg->to_string();
std::lock_guard<std::mutex> lock(mutex_);
try {
if (topic == topic_target_hdg_) {
(void)std::stoi(payload); // validate it is an integer
sub_data_.target_heading = payload;
sub_data_.heading_available = true;
} else if (topic == topic_control_code_) {
sub_data_.control_code = std::stoi(payload);
sub_data_.control_code_available = true;
}
} catch (const std::exception& e) {
LOG_WARN << "MQTT payload conversion error on " << topic << ": " << e.what();
}
}
ControlCommand MqttControlChannel::poll() {
std::lock_guard<std::mutex> lock(mutex_);
ControlCommand out = sub_data_;
sub_data_.control_code_available = false;
sub_data_.heading_available = false;
return out;
}
void MqttControlChannel::publish(const std::string& topic, const std::string& payload) {
if (!client_.is_connected()) return;
try {
auto msg = mqtt::make_message(topic, payload);
msg->set_qos(kQos);
msg->set_retained(true);
client_.publish(msg)->wait_for(kPublishTimeout);
} catch (const mqtt::exception& e) {
LOG_WARN << "MQTT publish failed: " << e.what();
}
}
void MqttControlChannel::publishStatus(int code) {
publish(topic_status_, std::to_string(code));
}
void MqttControlChannel::publishCamEvent(const CamEvent& e) {
std::string payload = "{ \"fwt\":\"" + e.tower + "\" ,\"cam\":\"" + e.camera +
"\", \"hdg\":" + std::to_string(e.heading_decideg) +
", \"time\":" + std::to_string(e.timestamp_ms) + " }";
publish(topic_cam_event_, payload);
}
} // namespace fgc

View File

@ -0,0 +1,102 @@
#include "fgc/SerialMotorController.h"
#include "fgc/Logger.h"
#include "fgc/TelemetryParser.h"
#include <atomic>
#include <istream>
#include <mutex>
#include <thread>
#include <boost/asio.hpp>
namespace fgc {
struct SerialMotorController::Impl {
Impl(std::string dev, unsigned int b) : device(std::move(dev)), baud(b), serial(io) {}
std::string device;
unsigned int baud;
boost::asio::io_context io;
boost::asio::serial_port serial;
boost::asio::streambuf buffer;
std::thread io_thread;
std::mutex mutex;
MotorTelemetry latest;
std::atomic<bool> connected{false};
void doRead() {
boost::asio::async_read_until(
serial, buffer, '\n',
[this](const boost::system::error_code& ec, std::size_t) {
if (ec) {
LOG_WARN << "Serial read failed: " << ec.message();
return;
}
std::istream is(&buffer);
std::string line;
std::getline(is, line);
if (auto t = parseTelemetryLine(line)) {
std::lock_guard<std::mutex> lock(mutex);
latest = *t;
}
doRead();
});
}
};
SerialMotorController::SerialMotorController(std::string device, unsigned int baud)
: impl_(std::make_unique<Impl>(std::move(device), baud)) {}
SerialMotorController::~SerialMotorController() { stop(); }
void SerialMotorController::start() {
namespace asio = boost::asio;
boost::system::error_code ec;
impl_->serial.open(impl_->device, ec);
if (ec) {
LOG_ERROR << "Failed to open serial port " << impl_->device << ": " << ec.message();
impl_->connected = false;
return;
}
impl_->serial.set_option(asio::serial_port_base::baud_rate(impl_->baud));
impl_->serial.set_option(asio::serial_port_base::character_size(8));
impl_->serial.set_option(
asio::serial_port_base::parity(asio::serial_port_base::parity::none));
impl_->serial.set_option(
asio::serial_port_base::stop_bits(asio::serial_port_base::stop_bits::one));
impl_->serial.set_option(
asio::serial_port_base::flow_control(asio::serial_port_base::flow_control::none));
impl_->connected = true;
impl_->doRead();
impl_->io_thread = std::thread([this] { impl_->io.run(); });
LOG_INFO << "Serial controller started on " << impl_->device << " @ " << impl_->baud;
}
void SerialMotorController::stop() {
if (!impl_) return;
impl_->io.stop();
if (impl_->io_thread.joinable()) impl_->io_thread.join();
boost::system::error_code ec;
if (impl_->serial.is_open()) impl_->serial.close(ec);
impl_->connected = false;
}
void SerialMotorController::sendCommand(const std::string& cmd) {
if (!impl_->connected) return;
auto data = std::make_shared<std::string>(cmd);
boost::asio::async_write(impl_->serial, boost::asio::buffer(*data),
[data](const boost::system::error_code& ec, std::size_t) {
if (ec) LOG_WARN << "Serial write failed: " << ec.message();
});
}
MotorTelemetry SerialMotorController::telemetry() {
std::lock_guard<std::mutex> lock(impl_->mutex);
return impl_->latest;
}
bool SerialMotorController::connected() const { return impl_->connected; }
} // namespace fgc

116
timing.h
View File

@ -1,116 +0,0 @@
#pragma once
#include <iostream>
#include <string>
#include <chrono>
#include <ctime>
class Timer
{
public:
Timer()
{
Reset();
}
void Reset()
{
m_Start = std::chrono::high_resolution_clock::now();
}
float Elapsed()
{
return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now() - m_Start).count() * 0.001f * 0.001f * 0.001f;
}
float ElapsedMillis()
{
return Elapsed() * 1000.0f;
}
long long Stamp_millis()
{
// Get the current time point with nanosecond precision
auto now = std::chrono::system_clock::now();
// Calculate the number of nanoseconds since the epoch
auto since_epoch = now.time_since_epoch();
long long milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(since_epoch).count();
return milliseconds;
}
std::string GetFileStamp()
{
time_t now = time(nullptr);
struct tm* ltm = localtime(&now);
std::string stamp = std::to_string(1900 + ltm->tm_year) + "_" + std::to_string(1 + ltm->tm_mon) + "_" + std::to_string(ltm->tm_mday) + "_" + std::to_string(ltm->tm_hour) + "_" + std::to_string(ltm->tm_min);
//printf(stamp.c_str());
return stamp;
}
std::string GetStringStamp()
{
time_t now = time(nullptr);
struct tm* ltm = localtime(&now);
std::string stamp = std::to_string(1900 + ltm->tm_year) + "_" + std::to_string(1 + ltm->tm_mon) + "_" + std::to_string(ltm->tm_mday) + "_" + std::to_string(ltm->tm_hour) + "_" + std::to_string(ltm->tm_min) + "_" + std::to_string(ltm->tm_sec);
//printf(stamp.c_str());
return stamp;
}
std::string GetDirStamp()
{
time_t now = time(nullptr);
struct tm* ltm = localtime(&now);
std::string stamp = std::to_string(1900 + ltm->tm_year) + "_" + std::to_string(1 + ltm->tm_mon) + "_" + std::to_string(ltm->tm_mday) + "_" + std::to_string(ltm->tm_hour);
//printf(stamp.c_str());
return stamp;
}
private:
std::chrono::time_point<std::chrono::high_resolution_clock> m_Start;
};
class ScopedTimer
{
public:
ScopedTimer(const std::string& name)
: m_Name(name) {}
~ScopedTimer()
{
float time = m_Timer.ElapsedMillis();
std::cout << "[TIMER] " << m_Name << " - " << time << "ms\n";
}
private:
std::string m_Name;
Timer m_Timer;
};
class NanoUnixTimer
{
public:
std::string Stamp_string()
{
// Get the current time point with nanosecond precision
auto now = std::chrono::system_clock::now();
// Calculate the number of nanoseconds since the epoch
auto since_epoch = now.time_since_epoch();
auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(since_epoch).count();
// Convert the nanoseconds to a string
return std::to_string(milliseconds);
}
long long Stamp_longlong()
{
// Get the current time point with nanosecond precision
auto now = std::chrono::system_clock::now();
// Calculate the number of nanoseconds since the epoch
auto since_epoch = now.time_since_epoch();
long long milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(since_epoch).count();
return milliseconds;
}
};