commit 89be503edb1adfcafecbee614657d2d91f45caf6 Author: pgdalmeida Date: Mon Jun 22 11:23:01 2026 +0200 Initial commit diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..2fb7d86 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(git add *)", + "Bash(grep -iE \"config\\\\.ini|\\\\.out|\\\\.o$|compile_commands\")" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b1f263 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# ---- Build output ---- +/build/ +/obj/ +*.o +*.out +bin/**/*.out + +# CMake +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +/_deps/ + +# Editor / tooling +compile_commands.json +.cache/ +.vscode/ +*.swp + +# ---- Captured images ---- +*.jxl +bin/x64/Release/RGB/ +bin/x64/Release/ACR/ +bin/x64/Release/NIR/ +# but keep the demo-mode placeholder image (force-tracked) +!bin/x64/Release/test_smoke.jxl + +# ---- Secrets / machine-specific config ---- +# Real configs hold plaintext MQTT credentials. Commit config/config.example.ini instead. +config.ini +bin/x64/Release/config.ini diff --git a/Camera.cpp b/Camera.cpp new file mode 100644 index 0000000..aecc6cb --- /dev/null +++ b/Camera.cpp @@ -0,0 +1,449 @@ +#include "Camera.h" +#include "JPEG_XL.h" +#include "timing.h" +#include +#include +#include +#include + + +class FrameObserver : public IFrameObserver +{ +public: + typedef std::function Callback; + Callback call; + int cam_id = 0; + std::atomic 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 empty; + std::swap(m_Frames, empty); + m_FramesMutex.unlock(); + } +private: + std::queue m_Frames; + std::mutex m_FramesMutex; +}; + +VimbaHandler::VimbaHandler(std::vector 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(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(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(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 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 data_in = std::vector(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(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 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 homedir = getenv("HOME"); + std::string cameraname; + if (pFrame->getCamId() == 0) + cameraname = "RGB"; + if (pFrame->getCamId() == 1) + 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::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(); + //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); + 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(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(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"; +} diff --git a/Camera.h b/Camera.h new file mode 100644 index 0000000..72a389e --- /dev/null +++ b/Camera.h @@ -0,0 +1,108 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "MQTT.h" +#include "Serial.h" +using namespace VmbCPP; + +struct image_store_8bit +{ +private: + typedef std::vector 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(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 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); + + 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 mqtt_RGB = "GGS/FWT/" + fwt_name + "/CamEvent"; + motor_info* gimbal_data; + MQTTClient* mqtt_client; + typedef std::shared_ptr ImageStore8Ptr; + typedef std::queue ImageQueue8; + std::vector 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 FO_ptr_vec; + VmbSystem& m_vmbSystem; + CameraPtrVector m_cameras; + +}; + diff --git a/JPEG_XL.h b/JPEG_XL.h new file mode 100644 index 0000000..c47df81 --- /dev/null +++ b/JPEG_XL.h @@ -0,0 +1,110 @@ +#pragma once +#include +#include // for fopen, fclose +#include +#include +#include +#include +#include +#include +#include +#include +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* 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 compressed_data; +}; diff --git a/Log.h b/Log.h new file mode 100644 index 0000000..50e9667 --- /dev/null +++ b/Log.h @@ -0,0 +1 @@ +#pragma once diff --git a/MQTT.cpp b/MQTT.cpp new file mode 100644 index 0000000..6fe55d2 --- /dev/null +++ b/MQTT.cpp @@ -0,0 +1,155 @@ +#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(std::stoi(msg->to_string())); + std::unique_lock 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 ul(mqtt_mut); + sub_data.set_control_code(static_cast(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(); +} diff --git a/MQTT.h b/MQTT.h new file mode 100644 index 0000000..e286b52 --- /dev/null +++ b/MQTT.h @@ -0,0 +1,157 @@ +#pragma once + +#include +#include +#include +#include +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 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; +}; diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c85dcc7 --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ +CXX := g++ +CC := gcc +CXXFLAGS := -std=c++17 -g -Wall -I. +CFLAGS := -g -Wall -I. +LDFLAGS := -lpaho-mqttpp3 -lpaho-mqtt3a \ + -lopencv_core -lopencv_highgui \ + -ljxl -ljxl_threads \ + -lboost_program_options \ + -lVmbC -lVmbCPP + +TARGET := bin/Fire_Gimbal_Control.out +OBJDIR := obj + +CXX_SRCS := main.cpp MQTT.cpp Camera.cpp +C_SRCS := ini.c + +CXX_OBJS := $(CXX_SRCS:%.cpp=$(OBJDIR)/%.o) +C_OBJS := $(C_SRCS:%.c=$(OBJDIR)/%.o) +OBJS := $(CXX_OBJS) $(C_OBJS) + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJS) + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) + +$(OBJDIR)/%.o: %.cpp + $(CXX) $(CXXFLAGS) -c -o $@ $< + +$(OBJDIR)/%.o: %.c + $(CC) $(CFLAGS) -c -o $@ $< + +clean: + rm -f $(OBJS) $(TARGET) diff --git a/Parser.h b/Parser.h new file mode 100644 index 0000000..6413d46 --- /dev/null +++ b/Parser.h @@ -0,0 +1,102 @@ +#pragma once +#include +#include +#include +#include + +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 lg(mut); + p_data = data; + } + + void clear_parser_data() { + //std::cout << "parser clear data..." << std::endl; + std::lock_guard lg(mut); + p_data.command = ""; + } + + parser_data get_parser_data() { + //std::cout << "parser get data..." << std::endl; + std::lock_guard 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; + } + } +}; diff --git a/README.md b/README.md new file mode 100644 index 0000000..6b9e19b --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# Fire Gimbal Control (Staeffelsberg) + +Real-time control software for an automated **fire-watch gimbal**. A single C++17 binary runs on a +tower-mounted PC, rotates a pan gimbal carrying up to four industrial cameras, captures a 360° panorama +for wildfire detection, compresses each frame to JPEG XL, and reports to a ground station over MQTT. + +This is the deployment for the **Staeffelsberg** fire-watch tower (FWT). The same codebase is used across +towers; the tower identity comes from `config.ini`. + +State is held in memory (mutex-guarded structs), configuration is read once from an INI file, images are +written to the filesystem as `.jxl`, and telemetry flows over MQTT. See [docs/architecture.md](docs/architecture.md) +for the full picture. + +## What it does (at a glance) + +``` + motor controller (MCU) this program (Fire_Gimbal_Control.out) ground station + ┌───────────────────┐ serial ┌──────────────────────────────────────┐ MQTT ┌──────────────┐ + │ gimbal + sensors │ ─────────▶ │ read telemetry → decide when to stop │ ──────▶ │ broker / ZKMS │ + │ /dev/ttyACM0 │ ◀───────── │ send move/stop commands │ ◀────── │ control UI │ + └───────────────────┘ │ trigger cameras → encode JXL → save │ └──────────────┘ + └──────────────┬───────────────────────┘ + Vimba X (GigE/USB) │ files + cameras ───────────────────▶ ▼ RGB/ACR/NIR/.jxl +``` + +## Quick start + +```bash +# 1. Install dependencies (see docs/build-and-setup.md for details + Vimba X SDK) +# 2. Build +make # produces bin/Fire_Gimbal_Control.out +# 3. Run (requires camera(s), motor MCU on /dev/ttyACM0, and a reachable MQTT broker) +./bin/Fire_Gimbal_Control.out --start 1 # auto-start capture +./bin/Fire_Gimbal_Control.out --init 1 --start 1 # also find endstops first +``` + +> **Before it will run on this machine**, several paths are hardcoded to `/home/ggs/projects/Fire_Gimbal_Control/...` +> and the program exits if MQTT can't connect. Read **[docs/known-issues.md](docs/known-issues.md)** first — it +> lists every reproduction blocker and the deployed layout the binary expects. + +## Documentation + +| Document | Contents | +|----------|----------| +| [docs/architecture.md](docs/architecture.md) | System overview, threading model, end-to-end data flow, capture state machine | +| [docs/build-and-setup.md](docs/build-and-setup.md) | Toolchain, dependencies, build, serial/MQTT setup, directory layout | +| [docs/configuration.md](docs/configuration.md) | `config.ini` keys, CLI flags, console command grammar | +| [docs/mqtt-api.md](docs/mqtt-api.md) | MQTT topic catalog, payloads, QoS/retain, ControlCode semantics | +| [docs/modules-reference.md](docs/modules-reference.md) | Per-file reference and key data structures | +| [docs/known-issues.md](docs/known-issues.md) | Reproduction blockers and recommended follow-ups | + +## Repository layout + +``` +. +├── main.cpp Entry point: config, CLI args, threads, main control loop +├── Serial.h SerialPort + motor_info telemetry parser (Boost.Asio) +├── MQTT.h / MQTT.cpp MQTTClient + callbacks (Eclipse Paho C++) +├── Camera.h / Camera.cpp VimbaHandler: acquisition, queue, JXL save (Vimba X + OpenCV) +├── JPEG_XL.h JPEG XL encoder wrapper (libjxl) +├── Parser.h Console command parser (Boost.Spirit Qi) + command evaluator +├── timing.h Timer / timestamp helpers +├── ini.c / ini.h inih INI parser (third-party) +├── cxxopts.hpp Third-party CLI parser (legacy/unused — Boost is used instead) +├── Log.h Empty stub +├── config.ini Configuration (also a separate copy under bin/x64/Release/) +├── Makefile Build definition +└── bin/x64/Release/ Deployed/runtime directory (binary, config, startup scripts, image folders) +``` + +## License / ownership + +Internal tooling for the GGS fire-watch tower network. No license file is present in the repository. diff --git a/Serial.h b/Serial.h new file mode 100644 index 0000000..48e6281 --- /dev/null +++ b/Serial.h @@ -0,0 +1,144 @@ +#include +#include +#include +#include +#include +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 lg(mut); + motorcontroller_information = inf; + } + + motor_info get_controller_info() { + std::lock_guard lg(mut); + return motorcontroller_information; + } + + bool parser(std::string& message) + { + + if (message[0] == '$') { + //std::cout << message; + std::vector 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 + 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_; +}; \ No newline at end of file diff --git a/bin/x64/Release/startup_gimbal.sh b/bin/x64/Release/startup_gimbal.sh new file mode 100644 index 0000000..a3c16a7 --- /dev/null +++ b/bin/x64/Release/startup_gimbal.sh @@ -0,0 +1 @@ +~/projects/Fire_Gimbal_Control/bin/x64/Release/Fire_Gimbal_Control.out -s 1 diff --git a/bin/x64/Release/startup_gimbal_with_init.sh b/bin/x64/Release/startup_gimbal_with_init.sh new file mode 100644 index 0000000..78d1b03 --- /dev/null +++ b/bin/x64/Release/startup_gimbal_with_init.sh @@ -0,0 +1 @@ +~/projects/Fire_Gimbal_Control/bin/x64/Release/Fire_Gimbal_Control.out -i 1 -s 1 diff --git a/bin/x64/Release/test_smoke.jxl b/bin/x64/Release/test_smoke.jxl new file mode 100644 index 0000000..3d3604d Binary files /dev/null and b/bin/x64/Release/test_smoke.jxl differ diff --git a/config/config.example.ini b/config/config.example.ini new file mode 100644 index 0000000..a6a4ad3 --- /dev/null +++ b/config/config.example.ini @@ -0,0 +1,31 @@ +; Fire Gimbal Control - example configuration. +; +; Copy this file to "config.ini" and edit for your deployment: +; cp config/config.example.ini config.ini +; +; The real config.ini is gitignored because it contains credentials. +; MQTT credentials may instead be supplied via the environment variables +; FGC_MQTT_USER and FGC_MQTT_PW, which take precedence over the values here. + +[General] +; Tower identity, substituted into all MQTT topics and CamEvent payloads. +tower_name = ExampleTower +; Seconds between captures. +image_interval = 5 +; 1 = print motor telemetry each loop tick, 0 = quiet. +debug = 0 + +[Network] +; MQTT broker address (the ground-station / ZKMS server). +zkms_server_ip = 127.0.0.1 +; Prefer the FGC_MQTT_USER / FGC_MQTT_PW environment variables over these. +mqtt_user = CHANGE_ME +mqtt_pw = CHANGE_ME + +[Camera] +; Camera IDs: GigE IP (e.g. 192.168.11.101) or USB device ID (e.g. DEV_1AB22C0AADED). +; Leave entries blank/absent for cameras you do not have. Order maps to RGB, ACR, NIR. +id_Cam1 = DEV_0000000000 +id_Cam2 = +id_Cam3 = +id_Cam4 = diff --git a/cxxopts.hpp b/cxxopts.hpp new file mode 100644 index 0000000..d339314 --- /dev/null +++ b/cxxopts.hpp @@ -0,0 +1,2908 @@ +/* + +Copyright (c) 2014-2022 Jarryd Beck + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// vim: ts=2:sw=2:expandtab + +#ifndef CXXOPTS_HPP_INCLUDED +#define CXXOPTS_HPP_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef CXXOPTS_NO_EXCEPTIONS +#include +#endif + +#if defined(__GNUC__) && !defined(__clang__) +# if (__GNUC__ * 10 + __GNUC_MINOR__) < 49 +# define CXXOPTS_NO_REGEX true +# endif +#endif +#if defined(_MSC_VER) && !defined(__clang__) +#define CXXOPTS_LINKONCE_CONST __declspec(selectany) extern +#define CXXOPTS_LINKONCE __declspec(selectany) extern +#else +#define CXXOPTS_LINKONCE_CONST +#define CXXOPTS_LINKONCE +#endif + +#ifndef CXXOPTS_NO_REGEX +# include +#endif // CXXOPTS_NO_REGEX + +// Nonstandard before C++17, which is coincidentally what we also need for +#ifdef __has_include +# if __has_include() +# include +# ifdef __cpp_lib_optional +# define CXXOPTS_HAS_OPTIONAL +# endif +# endif +#endif + +#define CXXOPTS_FALLTHROUGH +#ifdef __has_cpp_attribute +#if __has_cpp_attribute(fallthrough) +#undef CXXOPTS_FALLTHROUGH +#define CXXOPTS_FALLTHROUGH [[fallthrough]] +#endif +#endif + +#if __cplusplus >= 201603L +#define CXXOPTS_NODISCARD [[nodiscard]] +#else +#define CXXOPTS_NODISCARD +#endif + +#ifndef CXXOPTS_VECTOR_DELIMITER +#define CXXOPTS_VECTOR_DELIMITER ',' +#endif + +#define CXXOPTS__VERSION_MAJOR 3 +#define CXXOPTS__VERSION_MINOR 2 +#define CXXOPTS__VERSION_PATCH 1 + +#if (__GNUC__ < 10 || (__GNUC__ == 10 && __GNUC_MINOR__ < 1)) && __GNUC__ >= 6 +#define CXXOPTS_NULL_DEREF_IGNORE +#endif + +#if defined(__GNUC__) +#define DO_PRAGMA(x) _Pragma(#x) +#define CXXOPTS_DIAGNOSTIC_PUSH DO_PRAGMA(GCC diagnostic push) +#define CXXOPTS_DIAGNOSTIC_POP DO_PRAGMA(GCC diagnostic pop) +#define CXXOPTS_IGNORE_WARNING(x) DO_PRAGMA(GCC diagnostic ignored x) +#else +// define other compilers here if needed +#define CXXOPTS_DIAGNOSTIC_PUSH +#define CXXOPTS_DIAGNOSTIC_POP +#define CXXOPTS_IGNORE_WARNING(x) +#endif + +#ifdef CXXOPTS_NO_RTTI +#define CXXOPTS_RTTI_CAST static_cast +#else +#define CXXOPTS_RTTI_CAST dynamic_cast +#endif + +namespace cxxopts { + static constexpr struct { + uint8_t major, minor, patch; + } version = { + CXXOPTS__VERSION_MAJOR, + CXXOPTS__VERSION_MINOR, + CXXOPTS__VERSION_PATCH + }; +} // namespace cxxopts + +//when we ask cxxopts to use Unicode, help strings are processed using ICU, +//which results in the correct lengths being computed for strings when they +//are formatted for the help output +//it is necessary to make sure that can be found by the +//compiler, and that icu-uc is linked in to the binary. + +#ifdef CXXOPTS_USE_UNICODE +#include + +namespace cxxopts { + + using String = icu::UnicodeString; + + inline + String + toLocalString(std::string s) + { + return icu::UnicodeString::fromUTF8(std::move(s)); + } + + // GNU GCC with -Weffc++ will issue a warning regarding the upcoming class, we want to silence it: + // warning: base class 'class std::enable_shared_from_this' has accessible non-virtual destructor + CXXOPTS_DIAGNOSTIC_PUSH + CXXOPTS_IGNORE_WARNING("-Wnon-virtual-dtor") + // This will be ignored under other compilers like LLVM clang. + class UnicodeStringIterator + { + public: + + using iterator_category = std::forward_iterator_tag; + using value_type = int32_t; + using difference_type = std::ptrdiff_t; + using pointer = value_type*; + using reference = value_type&; + + UnicodeStringIterator(const icu::UnicodeString* string, int32_t pos) + : s(string) + , i(pos) + { + } + + value_type + operator*() const + { + return s->char32At(i); + } + + bool + operator==(const UnicodeStringIterator& rhs) const + { + return s == rhs.s && i == rhs.i; + } + + bool + operator!=(const UnicodeStringIterator& rhs) const + { + return !(*this == rhs); + } + + UnicodeStringIterator& + operator++() + { + ++i; + return *this; + } + + UnicodeStringIterator + operator+(int32_t v) + { + return UnicodeStringIterator(s, i + v); + } + + private: + const icu::UnicodeString* s; + int32_t i; + }; + CXXOPTS_DIAGNOSTIC_POP + + inline + String& + stringAppend(String& s, String a) + { + return s.append(std::move(a)); + } + + inline + String& + stringAppend(String& s, std::size_t n, UChar32 c) + { + for (std::size_t i = 0; i != n; ++i) + { + s.append(c); + } + + return s; + } + + template + String& + stringAppend(String& s, Iterator begin, Iterator end) + { + while (begin != end) + { + s.append(*begin); + ++begin; + } + + return s; + } + + inline + size_t + stringLength(const String& s) + { + return static_cast(s.length()); + } + + inline + std::string + toUTF8String(const String& s) + { + std::string result; + s.toUTF8String(result); + + return result; + } + + inline + bool + empty(const String& s) + { + return s.isEmpty(); + } + +} // namespace cxxopts + +namespace std { + + inline + cxxopts::UnicodeStringIterator + begin(const icu::UnicodeString& s) + { + return cxxopts::UnicodeStringIterator(&s, 0); + } + + inline + cxxopts::UnicodeStringIterator + end(const icu::UnicodeString& s) + { + return cxxopts::UnicodeStringIterator(&s, s.length()); + } + +} // namespace std + +//ifdef CXXOPTS_USE_UNICODE +#else + +namespace cxxopts { + + using String = std::string; + + template + T + toLocalString(T&& t) + { + return std::forward(t); + } + + inline + std::size_t + stringLength(const String& s) + { + return s.length(); + } + + inline + String& + stringAppend(String& s, const String& a) + { + return s.append(a); + } + + inline + String& + stringAppend(String& s, std::size_t n, char c) + { + return s.append(n, c); + } + + template + String& + stringAppend(String& s, Iterator begin, Iterator end) + { + return s.append(begin, end); + } + + template + std::string + toUTF8String(T&& t) + { + return std::forward(t); + } + + inline + bool + empty(const std::string& s) + { + return s.empty(); + } + +} // namespace cxxopts + +//ifdef CXXOPTS_USE_UNICODE +#endif + +namespace cxxopts { + + namespace { + CXXOPTS_LINKONCE_CONST std::string LQUOTE("\'"); + CXXOPTS_LINKONCE_CONST std::string RQUOTE("\'"); + } // namespace + + // GNU GCC with -Weffc++ will issue a warning regarding the upcoming class, we + // want to silence it: warning: base class 'class + // std::enable_shared_from_this' has accessible non-virtual + // destructor This will be ignored under other compilers like LLVM clang. + CXXOPTS_DIAGNOSTIC_PUSH + CXXOPTS_IGNORE_WARNING("-Wnon-virtual-dtor") + + // some older versions of GCC warn under this warning + CXXOPTS_IGNORE_WARNING("-Weffc++") + class Value : public std::enable_shared_from_this + { + public: + + virtual ~Value() = default; + + virtual + std::shared_ptr + clone() const = 0; + + virtual void + add(const std::string& text) const = 0; + + virtual void + parse(const std::string& text) const = 0; + + virtual void + parse() const = 0; + + virtual bool + has_default() const = 0; + + virtual bool + is_container() const = 0; + + virtual bool + has_implicit() const = 0; + + virtual std::string + get_default_value() const = 0; + + virtual std::string + get_implicit_value() const = 0; + + virtual std::shared_ptr + default_value(const std::string& value) = 0; + + virtual std::shared_ptr + implicit_value(const std::string& value) = 0; + + virtual std::shared_ptr + no_implicit_value() = 0; + + virtual bool + is_boolean() const = 0; + }; + + CXXOPTS_DIAGNOSTIC_POP + + namespace exceptions { + + class exception : public std::exception + { + public: + explicit exception(std::string message) + : m_message(std::move(message)) + { + } + + CXXOPTS_NODISCARD + const char* + what() const noexcept override + { + return m_message.c_str(); + } + + private: + std::string m_message; + }; + + class specification : public exception + { + public: + + explicit specification(const std::string& message) + : exception(message) + { + } + }; + + class parsing : public exception + { + public: + explicit parsing(const std::string& message) + : exception(message) + { + } + }; + + class option_already_exists : public specification + { + public: + explicit option_already_exists(const std::string& option) + : specification("Option " + LQUOTE + option + RQUOTE + " already exists") + { + } + }; + + class invalid_option_format : public specification + { + public: + explicit invalid_option_format(const std::string& format) + : specification("Invalid option format " + LQUOTE + format + RQUOTE) + { + } + }; + + class invalid_option_syntax : public parsing { + public: + explicit invalid_option_syntax(const std::string& text) + : parsing("Argument " + LQUOTE + text + RQUOTE + + " starts with a - but has incorrect syntax") + { + } + }; + + class no_such_option : public parsing + { + public: + explicit no_such_option(const std::string& option) + : parsing("Option " + LQUOTE + option + RQUOTE + " does not exist") + { + } + }; + + class missing_argument : public parsing + { + public: + explicit missing_argument(const std::string& option) + : parsing( + "Option " + LQUOTE + option + RQUOTE + " is missing an argument" + ) + { + } + }; + + class option_requires_argument : public parsing + { + public: + explicit option_requires_argument(const std::string& option) + : parsing( + "Option " + LQUOTE + option + RQUOTE + " requires an argument" + ) + { + } + }; + + class gratuitous_argument_for_option : public parsing + { + public: + gratuitous_argument_for_option + ( + const std::string& option, + const std::string& arg + ) + : parsing( + "Option " + LQUOTE + option + RQUOTE + + " does not take an argument, but argument " + + LQUOTE + arg + RQUOTE + " given" + ) + { + } + }; + + class requested_option_not_present : public parsing + { + public: + explicit requested_option_not_present(const std::string& option) + : parsing("Option " + LQUOTE + option + RQUOTE + " not present") + { + } + }; + + class option_has_no_value : public exception + { + public: + explicit option_has_no_value(const std::string& option) + : exception( + !option.empty() ? + ("Option " + LQUOTE + option + RQUOTE + " has no value") : + "Option has no value") + { + } + }; + + class incorrect_argument_type : public parsing + { + public: + explicit incorrect_argument_type + ( + const std::string& arg + ) + : parsing( + "Argument " + LQUOTE + arg + RQUOTE + " failed to parse" + ) + { + } + }; + + } // namespace exceptions + + + template + void throw_or_mimic(const std::string& text) + { + static_assert(std::is_base_of::value, + "throw_or_mimic only works on std::exception and " + "deriving classes"); + +#ifndef CXXOPTS_NO_EXCEPTIONS + // If CXXOPTS_NO_EXCEPTIONS is not defined, just throw + throw T{ text }; +#else + // Otherwise manually instantiate the exception, print what() to stderr, + // and exit + T exception{ text }; + std::cerr << exception.what() << std::endl; + std::exit(EXIT_FAILURE); +#endif + } + + using OptionNames = std::vector; + + namespace values { + + namespace parser_tool { + + struct IntegerDesc + { + std::string negative = ""; + std::string base = ""; + std::string value = ""; + }; + struct ArguDesc { + std::string arg_name = ""; + bool grouping = false; + bool set_value = false; + std::string value = ""; + }; + +#ifdef CXXOPTS_NO_REGEX + inline IntegerDesc SplitInteger(const std::string& text) + { + if (text.empty()) + { + throw_or_mimic(text); + } + IntegerDesc desc; + const char* pdata = text.c_str(); + if (*pdata == '-') + { + pdata += 1; + desc.negative = "-"; + } + if (strncmp(pdata, "0x", 2) == 0) + { + pdata += 2; + desc.base = "0x"; + } + if (*pdata != '\0') + { + desc.value = std::string(pdata); + } + else + { + throw_or_mimic(text); + } + return desc; + } + + inline bool IsTrueText(const std::string& text) + { + const char* pdata = text.c_str(); + if (*pdata == 't' || *pdata == 'T') + { + pdata += 1; + if (strncmp(pdata, "rue\0", 4) == 0) + { + return true; + } + } + else if (strncmp(pdata, "1\0", 2) == 0) + { + return true; + } + return false; + } + + inline bool IsFalseText(const std::string& text) + { + const char* pdata = text.c_str(); + if (*pdata == 'f' || *pdata == 'F') + { + pdata += 1; + if (strncmp(pdata, "alse\0", 5) == 0) + { + return true; + } + } + else if (strncmp(pdata, "0\0", 2) == 0) + { + return true; + } + return false; + } + + inline OptionNames split_option_names(const std::string& text) + { + OptionNames split_names; + + std::string::size_type token_start_pos = 0; + auto length = text.length(); + + if (length == 0) + { + throw_or_mimic(text); + } + + while (token_start_pos < length) { + const auto& npos = std::string::npos; + auto next_non_space_pos = text.find_first_not_of(' ', token_start_pos); + if (next_non_space_pos == npos) { + throw_or_mimic(text); + } + token_start_pos = next_non_space_pos; + auto next_delimiter_pos = text.find(',', token_start_pos); + if (next_delimiter_pos == token_start_pos) { + throw_or_mimic(text); + } + if (next_delimiter_pos == npos) { + next_delimiter_pos = length; + } + auto token_length = next_delimiter_pos - token_start_pos; + // validate the token itself matches the regex /([:alnum:][-_[:alnum:]]*/ + { + const char* option_name_valid_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "_-.?"; + + if (!std::isalnum(text[token_start_pos], std::locale::classic()) || + text.find_first_not_of(option_name_valid_chars, token_start_pos) < next_delimiter_pos) { + throw_or_mimic(text); + } + } + split_names.emplace_back(text.substr(token_start_pos, token_length)); + token_start_pos = next_delimiter_pos + 1; + } + return split_names; + } + + inline ArguDesc ParseArgument(const char* arg, bool& matched) + { + ArguDesc argu_desc; + const char* pdata = arg; + matched = false; + if (strncmp(pdata, "--", 2) == 0) + { + pdata += 2; + if (isalnum(*pdata, std::locale::classic())) + { + argu_desc.arg_name.push_back(*pdata); + pdata += 1; + while (isalnum(*pdata, std::locale::classic()) || *pdata == '-' || *pdata == '_') + { + argu_desc.arg_name.push_back(*pdata); + pdata += 1; + } + if (argu_desc.arg_name.length() > 1) + { + if (*pdata == '=') + { + argu_desc.set_value = true; + pdata += 1; + if (*pdata != '\0') + { + argu_desc.value = std::string(pdata); + } + matched = true; + } + else if (*pdata == '\0') + { + matched = true; + } + } + } + } + else if (strncmp(pdata, "-", 1) == 0) + { + pdata += 1; + argu_desc.grouping = true; + while (isalnum(*pdata, std::locale::classic())) + { + argu_desc.arg_name.push_back(*pdata); + pdata += 1; + } + matched = !argu_desc.arg_name.empty() && *pdata == '\0'; + } + return argu_desc; + } + +#else // CXXOPTS_NO_REGEX + + namespace { + CXXOPTS_LINKONCE + const char* const integer_pattern = + "(-)?(0x)?([0-9a-zA-Z]+)|((0x)?0)"; + CXXOPTS_LINKONCE + const char* const truthy_pattern = + "(t|T)(rue)?|1"; + CXXOPTS_LINKONCE + const char* const falsy_pattern = + "(f|F)(alse)?|0"; + CXXOPTS_LINKONCE + const char* const option_pattern = + "--([[:alnum:]][-_[:alnum:]\\.]+)(=(.*))?|-([[:alnum:]].*)"; + CXXOPTS_LINKONCE + const char* const option_specifier_pattern = + "([[:alnum:]][-_[:alnum:]\\.]*)(,[ ]*[[:alnum:]][-_[:alnum:]]*)*"; + CXXOPTS_LINKONCE + const char* const option_specifier_separator_pattern = ", *"; + + } // namespace + + inline IntegerDesc SplitInteger(const std::string& text) + { + static const std::basic_regex integer_matcher(integer_pattern); + + std::smatch match; + std::regex_match(text, match, integer_matcher); + + if (match.length() == 0) + { + throw_or_mimic(text); + } + + IntegerDesc desc; + desc.negative = match[1]; + desc.base = match[2]; + desc.value = match[3]; + + if (match.length(4) > 0) + { + desc.base = match[5]; + desc.value = "0"; + return desc; + } + + return desc; + } + + inline bool IsTrueText(const std::string& text) + { + static const std::basic_regex truthy_matcher(truthy_pattern); + std::smatch result; + std::regex_match(text, result, truthy_matcher); + return !result.empty(); + } + + inline bool IsFalseText(const std::string& text) + { + static const std::basic_regex falsy_matcher(falsy_pattern); + std::smatch result; + std::regex_match(text, result, falsy_matcher); + return !result.empty(); + } + + // Gets the option names specified via a single, comma-separated string, + // and returns the separate, space-discarded, non-empty names + // (without considering which or how many are single-character) + inline OptionNames split_option_names(const std::string& text) + { + static const std::basic_regex option_specifier_matcher(option_specifier_pattern); + if (!std::regex_match(text.c_str(), option_specifier_matcher)) + { + throw_or_mimic(text); + } + + OptionNames split_names; + + static const std::basic_regex option_specifier_separator_matcher(option_specifier_separator_pattern); + constexpr int use_non_matches{ -1 }; + auto token_iterator = std::sregex_token_iterator( + text.begin(), text.end(), option_specifier_separator_matcher, use_non_matches); + std::copy(token_iterator, std::sregex_token_iterator(), std::back_inserter(split_names)); + return split_names; + } + + inline ArguDesc ParseArgument(const char* arg, bool& matched) + { + static const std::basic_regex option_matcher(option_pattern); + std::match_results result; + std::regex_match(arg, result, option_matcher); + matched = !result.empty(); + + ArguDesc argu_desc; + if (matched) { + argu_desc.arg_name = result[1].str(); + argu_desc.set_value = result[2].length() > 0; + argu_desc.value = result[3].str(); + if (result[4].length() > 0) + { + argu_desc.grouping = true; + argu_desc.arg_name = result[4].str(); + } + } + + return argu_desc; + } + +#endif // CXXOPTS_NO_REGEX +#undef CXXOPTS_NO_REGEX + } // namespace parser_tool + + namespace detail { + + template + struct SignedCheck; + + template + struct SignedCheck + { + template + void + operator()(bool negative, U u, const std::string& text) + { + if (negative) + { + if (u > static_cast((std::numeric_limits::min)())) + { + throw_or_mimic(text); + } + } + else + { + if (u > static_cast((std::numeric_limits::max)())) + { + throw_or_mimic(text); + } + } + } + }; + + template + struct SignedCheck + { + template + void + operator()(bool, U, const std::string&) const {} + }; + + template + void + check_signed_range(bool negative, U value, const std::string& text) + { + SignedCheck::is_signed>()(negative, value, text); + } + + } // namespace detail + + template + void + checked_negate(R& r, T&& t, const std::string&, std::true_type) + { + // if we got to here, then `t` is a positive number that fits into + // `R`. So to avoid MSVC C4146, we first cast it to `R`. + // See https://github.com/jarro2783/cxxopts/issues/62 for more details. + r = static_cast(-static_cast(t - 1) - 1); + } + + template + void + checked_negate(R&, T&&, const std::string& text, std::false_type) + { + throw_or_mimic(text); + } + + template + void + integer_parser(const std::string& text, T& value) + { + parser_tool::IntegerDesc int_desc = parser_tool::SplitInteger(text); + + using US = typename std::make_unsigned::type; + constexpr bool is_signed = std::numeric_limits::is_signed; + + const bool negative = int_desc.negative.length() > 0; + const uint8_t base = int_desc.base.length() > 0 ? 16 : 10; + const std::string& value_match = int_desc.value; + + US result = 0; + + for (char ch : value_match) + { + US digit = 0; + + if (ch >= '0' && ch <= '9') + { + digit = static_cast(ch - '0'); + } + else if (base == 16 && ch >= 'a' && ch <= 'f') + { + digit = static_cast(ch - 'a' + 10); + } + else if (base == 16 && ch >= 'A' && ch <= 'F') + { + digit = static_cast(ch - 'A' + 10); + } + else + { + throw_or_mimic(text); + } + + US limit = 0; + if (negative) + { + limit = static_cast(std::abs(static_cast((std::numeric_limits::min)()))); + } + else + { + limit = (std::numeric_limits::max)(); + } + + if (base != 0 && result > limit / base) + { + throw_or_mimic(text); + } + if (result * base > limit - digit) + { + throw_or_mimic(text); + } + + result = static_cast(result * base + digit); + } + + detail::check_signed_range(negative, result, text); + + if (negative) + { + checked_negate(value, result, text, std::integral_constant()); + } + else + { + value = static_cast(result); + } + } + + template + void stringstream_parser(const std::string& text, T& value) + { + std::stringstream in(text); + in >> value; + if (!in) { + throw_or_mimic(text); + } + } + + template ::value>::type* = nullptr + > + void parse_value(const std::string& text, T& value) + { + integer_parser(text, value); + } + + inline + void + parse_value(const std::string& text, bool& value) + { + if (parser_tool::IsTrueText(text)) + { + value = true; + return; + } + + if (parser_tool::IsFalseText(text)) + { + value = false; + return; + } + + throw_or_mimic(text); + } + + inline + void + parse_value(const std::string& text, std::string& value) + { + value = text; + } + + // The fallback parser. It uses the stringstream parser to parse all types + // that have not been overloaded explicitly. It has to be placed in the + // source code before all other more specialized templates. + template ::value>::type* = nullptr + > + void + parse_value(const std::string& text, T& value) { + stringstream_parser(text, value); + } + +#ifdef CXXOPTS_HAS_OPTIONAL + template + void + parse_value(const std::string& text, std::optional& value) + { + T result; + parse_value(text, result); + value = std::move(result); + } +#endif + + inline + void parse_value(const std::string& text, char& c) + { + if (text.length() != 1) + { + throw_or_mimic(text); + } + + c = text[0]; + } + + template + void + parse_value(const std::string& text, std::vector& value) + { + if (text.empty()) { + T v; + parse_value(text, v); + value.emplace_back(std::move(v)); + return; + } + std::stringstream in(text); + std::string token; + while (!in.eof() && std::getline(in, token, CXXOPTS_VECTOR_DELIMITER)) { + T v; + parse_value(token, v); + value.emplace_back(std::move(v)); + } + } + + template + void + add_value(const std::string& text, T& value) + { + parse_value(text, value); + } + + template + void + add_value(const std::string& text, std::vector& value) + { + T v; + add_value(text, v); + value.emplace_back(std::move(v)); + } + + template + struct type_is_container + { + static constexpr bool value = false; + }; + + template + struct type_is_container> + { + static constexpr bool value = true; + }; + + template + class abstract_value : public Value + { + using Self = abstract_value; + + public: + abstract_value() + : m_result(std::make_shared()) + , m_store(m_result.get()) + { + } + + explicit abstract_value(T* t) + : m_store(t) + { + } + + ~abstract_value() override = default; + + abstract_value& operator=(const abstract_value&) = default; + + abstract_value(const abstract_value& rhs) + { + if (rhs.m_result) + { + m_result = std::make_shared(); + m_store = m_result.get(); + } + else + { + m_store = rhs.m_store; + } + + m_default = rhs.m_default; + m_implicit = rhs.m_implicit; + m_default_value = rhs.m_default_value; + m_implicit_value = rhs.m_implicit_value; + } + + void + add(const std::string& text) const override + { + add_value(text, *m_store); + } + + void + parse(const std::string& text) const override + { + parse_value(text, *m_store); + } + + bool + is_container() const override + { + return type_is_container::value; + } + + void + parse() const override + { + parse_value(m_default_value, *m_store); + } + + bool + has_default() const override + { + return m_default; + } + + bool + has_implicit() const override + { + return m_implicit; + } + + std::shared_ptr + default_value(const std::string& value) override + { + m_default = true; + m_default_value = value; + return shared_from_this(); + } + + std::shared_ptr + implicit_value(const std::string& value) override + { + m_implicit = true; + m_implicit_value = value; + return shared_from_this(); + } + + std::shared_ptr + no_implicit_value() override + { + m_implicit = false; + return shared_from_this(); + } + + std::string + get_default_value() const override + { + return m_default_value; + } + + std::string + get_implicit_value() const override + { + return m_implicit_value; + } + + bool + is_boolean() const override + { + return std::is_same::value; + } + + const T& + get() const + { + if (m_store == nullptr) + { + return *m_result; + } + return *m_store; + } + + protected: + std::shared_ptr m_result{}; + T* m_store{}; + + bool m_default = false; + bool m_implicit = false; + + std::string m_default_value{}; + std::string m_implicit_value{}; + }; + + template + class standard_value : public abstract_value + { + public: + using abstract_value::abstract_value; + + CXXOPTS_NODISCARD + std::shared_ptr + clone() const override + { + return std::make_shared>(*this); + } + }; + + template <> + class standard_value : public abstract_value + { + public: + ~standard_value() override = default; + + standard_value() + { + set_default_and_implicit(); + } + + explicit standard_value(bool* b) + : abstract_value(b) + { + m_implicit = true; + m_implicit_value = "true"; + } + + std::shared_ptr + clone() const override + { + return std::make_shared>(*this); + } + + private: + + void + set_default_and_implicit() + { + m_default = true; + m_default_value = "false"; + m_implicit = true; + m_implicit_value = "true"; + } + }; + + } // namespace values + + template + std::shared_ptr + value() + { + return std::make_shared>(); + } + + template + std::shared_ptr + value(T& t) + { + return std::make_shared>(&t); + } + + class OptionAdder; + + CXXOPTS_NODISCARD + inline + const std::string& + first_or_empty(const OptionNames& long_names) + { + static const std::string empty{ "" }; + return long_names.empty() ? empty : long_names.front(); + } + + class OptionDetails + { + public: + OptionDetails + ( + std::string short_, + OptionNames long_, + String desc, + std::shared_ptr val + ) + : m_short(std::move(short_)) + , m_long(std::move(long_)) + , m_desc(std::move(desc)) + , m_value(std::move(val)) + , m_count(0) + { + m_hash = std::hash{}(first_long_name() + m_short); + } + + OptionDetails(const OptionDetails& rhs) + : m_desc(rhs.m_desc) + , m_value(rhs.m_value->clone()) + , m_count(rhs.m_count) + { + } + + OptionDetails(OptionDetails&& rhs) = default; + + CXXOPTS_NODISCARD + const String& + description() const + { + return m_desc; + } + + CXXOPTS_NODISCARD + const Value& + value() const { + return *m_value; + } + + CXXOPTS_NODISCARD + std::shared_ptr + make_storage() const + { + return m_value->clone(); + } + + CXXOPTS_NODISCARD + const std::string& + short_name() const + { + return m_short; + } + + CXXOPTS_NODISCARD + const std::string& + first_long_name() const + { + return first_or_empty(m_long); + } + + CXXOPTS_NODISCARD + const std::string& + essential_name() const + { + return m_long.empty() ? m_short : m_long.front(); + } + + CXXOPTS_NODISCARD + const OptionNames& + long_names() const + { + return m_long; + } + + std::size_t + hash() const + { + return m_hash; + } + + private: + std::string m_short{}; + OptionNames m_long{}; + String m_desc{}; + std::shared_ptr m_value{}; + int m_count; + + std::size_t m_hash{}; + }; + + struct HelpOptionDetails + { + std::string s; + OptionNames l; + String desc; + bool has_default; + std::string default_value; + bool has_implicit; + std::string implicit_value; + std::string arg_help; + bool is_container; + bool is_boolean; + }; + + struct HelpGroupDetails + { + std::string name{}; + std::string description{}; + std::vector options{}; + }; + + class OptionValue + { + public: + void + add + ( + const std::shared_ptr& details, + const std::string& text + ) + { + ensure_value(details); + ++m_count; + m_value->add(text); + m_long_names = &details->long_names(); + } + + void + parse + ( + const std::shared_ptr& details, + const std::string& text + ) + { + ensure_value(details); + ++m_count; + m_value->parse(text); + m_long_names = &details->long_names(); + } + + void + parse_default(const std::shared_ptr& details) + { + ensure_value(details); + m_default = true; + m_long_names = &details->long_names(); + m_value->parse(); + } + + void + parse_no_value(const std::shared_ptr& details) + { + m_long_names = &details->long_names(); + } + +#if defined(CXXOPTS_NULL_DEREF_IGNORE) + CXXOPTS_DIAGNOSTIC_PUSH + CXXOPTS_IGNORE_WARNING("-Wnull-dereference") +#endif + + CXXOPTS_NODISCARD + std::size_t + count() const noexcept + { + return m_count; + } + +#if defined(CXXOPTS_NULL_DEREF_IGNORE) + CXXOPTS_DIAGNOSTIC_POP +#endif + + // TODO: maybe default options should count towards the number of arguments + CXXOPTS_NODISCARD + bool + has_default() const noexcept + { + return m_default; + } + + template + const T& + as() const + { + if (m_value == nullptr) { + throw_or_mimic( + m_long_names == nullptr ? "" : first_or_empty(*m_long_names)); + } + + return CXXOPTS_RTTI_CAST&>(*m_value).get(); + } + +#ifdef CXXOPTS_HAS_OPTIONAL + template + std::optional + as_optional() const + { + if (m_value == nullptr) { + return std::nullopt; + } + return as(); + } +#endif + + private: + void + ensure_value(const std::shared_ptr& details) + { + if (m_value == nullptr) + { + m_value = details->make_storage(); + } + } + + + const OptionNames* m_long_names = nullptr; + // Holding this pointer is safe, since OptionValue's only exist in key-value pairs, + // where the key has the string we point to. + std::shared_ptr m_value{}; + std::size_t m_count = 0; + bool m_default = false; + }; + + class KeyValue + { + public: + KeyValue(std::string key_, std::string value_) noexcept + : m_key(std::move(key_)) + , m_value(std::move(value_)) + { + } + + CXXOPTS_NODISCARD + const std::string& + key() const + { + return m_key; + } + + CXXOPTS_NODISCARD + const std::string& + value() const + { + return m_value; + } + + template + T + as() const + { + T result; + values::parse_value(m_value, result); + return result; + } + + private: + std::string m_key; + std::string m_value; + }; + + using ParsedHashMap = std::unordered_map; + using NameHashMap = std::unordered_map; + + class ParseResult + { + public: + class Iterator + { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = KeyValue; + using difference_type = void; + using pointer = const KeyValue*; + using reference = const KeyValue&; + + Iterator() = default; + Iterator(const Iterator&) = default; + + // GCC complains about m_iter not being initialised in the member + // initializer list + CXXOPTS_DIAGNOSTIC_PUSH + CXXOPTS_IGNORE_WARNING("-Weffc++") + Iterator(const ParseResult* pr, bool end = false) + : m_pr(pr) + { + if (end) + { + m_sequential = false; + m_iter = m_pr->m_defaults.end(); + } + else + { + m_sequential = true; + m_iter = m_pr->m_sequential.begin(); + + if (m_iter == m_pr->m_sequential.end()) + { + m_sequential = false; + m_iter = m_pr->m_defaults.begin(); + } + } + } + CXXOPTS_DIAGNOSTIC_POP + + Iterator& operator++() + { + ++m_iter; + if (m_sequential && m_iter == m_pr->m_sequential.end()) + { + m_sequential = false; + m_iter = m_pr->m_defaults.begin(); + return *this; + } + return *this; + } + + Iterator operator++(int) + { + Iterator retval = *this; + ++(*this); + return retval; + } + + bool operator==(const Iterator& other) const + { + return (m_sequential == other.m_sequential) && (m_iter == other.m_iter); + } + + bool operator!=(const Iterator& other) const + { + return !(*this == other); + } + + const KeyValue& operator*() + { + return *m_iter; + } + + const KeyValue* operator->() + { + return m_iter.operator->(); + } + + private: + const ParseResult* m_pr; + std::vector::const_iterator m_iter; + bool m_sequential = true; + }; + + ParseResult() = default; + ParseResult(const ParseResult&) = default; + + ParseResult(NameHashMap&& keys, ParsedHashMap&& values, std::vector sequential, + std::vector default_opts, std::vector&& unmatched_args) + : m_keys(std::move(keys)) + , m_values(std::move(values)) + , m_sequential(std::move(sequential)) + , m_defaults(std::move(default_opts)) + , m_unmatched(std::move(unmatched_args)) + { + } + + ParseResult& operator=(ParseResult&&) = default; + ParseResult& operator=(const ParseResult&) = default; + + Iterator + begin() const + { + return Iterator(this); + } + + Iterator + end() const + { + return Iterator(this, true); + } + + std::size_t + count(const std::string& o) const + { + auto iter = m_keys.find(o); + if (iter == m_keys.end()) + { + return 0; + } + + auto viter = m_values.find(iter->second); + + if (viter == m_values.end()) + { + return 0; + } + + return viter->second.count(); + } + + const OptionValue& + operator[](const std::string& option) const + { + auto iter = m_keys.find(option); + + if (iter == m_keys.end()) + { + throw_or_mimic(option); + } + + auto viter = m_values.find(iter->second); + + if (viter == m_values.end()) + { + throw_or_mimic(option); + } + + return viter->second; + } + +#ifdef CXXOPTS_HAS_OPTIONAL + template + std::optional + as_optional(const std::string& option) const + { + auto iter = m_keys.find(option); + if (iter != m_keys.end()) + { + auto viter = m_values.find(iter->second); + if (viter != m_values.end()) + { + return viter->second.as_optional(); + } + } + return std::nullopt; + } +#endif + + const std::vector& + arguments() const + { + return m_sequential; + } + + const std::vector& + unmatched() const + { + return m_unmatched; + } + + const std::vector& + defaults() const + { + return m_defaults; + } + + const std::string + arguments_string() const + { + std::string result; + for (const auto& kv : m_sequential) + { + result += kv.key() + " = " + kv.value() + "\n"; + } + for (const auto& kv : m_defaults) + { + result += kv.key() + " = " + kv.value() + " " + "(default)" + "\n"; + } + return result; + } + + private: + NameHashMap m_keys{}; + ParsedHashMap m_values{}; + std::vector m_sequential{}; + std::vector m_defaults{}; + std::vector m_unmatched{}; + }; + + struct Option + { + Option + ( + std::string opts, + std::string desc, + std::shared_ptr value = ::cxxopts::value(), + std::string arg_help = "" + ) + : opts_(std::move(opts)) + , desc_(std::move(desc)) + , value_(std::move(value)) + , arg_help_(std::move(arg_help)) + { + } + + std::string opts_; + std::string desc_; + std::shared_ptr value_; + std::string arg_help_; + }; + + using OptionMap = std::unordered_map>; + using PositionalList = std::vector; + using PositionalListIterator = PositionalList::const_iterator; + + class OptionParser + { + public: + OptionParser(const OptionMap& options, const PositionalList& positional, bool allow_unrecognised) + : m_options(options) + , m_positional(positional) + , m_allow_unrecognised(allow_unrecognised) + { + } + + ParseResult + parse(int argc, const char* const* argv); + + bool + consume_positional(const std::string& a, PositionalListIterator& next); + + void + checked_parse_arg + ( + int argc, + const char* const* argv, + int& current, + const std::shared_ptr& value, + const std::string& name + ); + + void + add_to_option(const std::shared_ptr& value, const std::string& arg); + + void + parse_option + ( + const std::shared_ptr& value, + const std::string& name, + const std::string& arg = "" + ); + + void + parse_default(const std::shared_ptr& details); + + void + parse_no_value(const std::shared_ptr& details); + + private: + + void finalise_aliases(); + + const OptionMap& m_options; + const PositionalList& m_positional; + + std::vector m_sequential{}; + std::vector m_defaults{}; + bool m_allow_unrecognised; + + ParsedHashMap m_parsed{}; + NameHashMap m_keys{}; + }; + + class Options + { + public: + + explicit Options(std::string program_name, std::string help_string = "") + : m_program(std::move(program_name)) + , m_help_string(toLocalString(std::move(help_string))) + , m_custom_help("[OPTION...]") + , m_positional_help("positional parameters") + , m_show_positional(false) + , m_allow_unrecognised(false) + , m_width(76) + , m_tab_expansion(false) + , m_options(std::make_shared()) + { + } + + Options& + positional_help(std::string help_text) + { + m_positional_help = std::move(help_text); + return *this; + } + + Options& + custom_help(std::string help_text) + { + m_custom_help = std::move(help_text); + return *this; + } + + Options& + show_positional_help() + { + m_show_positional = true; + return *this; + } + + Options& + allow_unrecognised_options() + { + m_allow_unrecognised = true; + return *this; + } + + Options& + set_width(std::size_t width) + { + m_width = width; + return *this; + } + + Options& + set_tab_expansion(bool expansion = true) + { + m_tab_expansion = expansion; + return *this; + } + + ParseResult + parse(int argc, const char* const* argv); + + OptionAdder + add_options(std::string group = ""); + + void + add_options + ( + const std::string& group, + std::initializer_list