diff --git a/.gitignore b/.gitignore index bdd8f087..49e0d362 100644 --- a/.gitignore +++ b/.gitignore @@ -98,3 +98,4 @@ Software/GroundTruthAnnotator/yolo/labels/*.cache # Dev scripts cache Dev/scripts/__pycache__ packaging/pitrac +*.save diff --git a/PiTrac/Software/web-server/templates/dashboard.html b/PiTrac/Software/web-server/templates/dashboard.html new file mode 100644 index 00000000..3d2a35d8 --- /dev/null +++ b/PiTrac/Software/web-server/templates/dashboard.html @@ -0,0 +1,132 @@ +{% extends "base.html" %} + +{% block title %}PiTrac Launch Monitor{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+
+
+
+
+
System Status
+
Initializing...
+ + +
+ OpenGolfSim: + + {{ 'Connected' if ogs_connected else 'Disconnected' }} + +
+ +
+
+ +
+
+
Ball Speed
+
+ {{ shot.speed }} + mph +
+
+ +
+
Carry Distance
+
+ {{ shot.carry }} + yds +
+
+ +
+
Launch Angle
+
+ {{ shot.launch_angle }} + ° +
+
+ +
+
Side Angle
+
+ {{ shot.side_angle }} + ° +
+
+ +
+
Back Spin
+
+ {{ shot.back_spin }} + rpm +
+
+ +
+
Side Spin
+
+ {{ shot.side_spin }} + rpm +
+
+
+ + + + +{% if ogs_connected %} +
+

OpenGolfSim Controls

+ +
+ + +
+ + + + + + + + + + +
+ +
+
+ +
+ Tip: arrows support hold (down/up). Most other buttons use press. +
+
+{% endif %} + + + +
+ + +
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} \ No newline at end of file diff --git a/Software/LMSourceCode/ImageProcessing/golf_sim_config.json b/Software/LMSourceCode/ImageProcessing/golf_sim_config.json index 005108c4..42a0888f 100644 --- a/Software/LMSourceCode/ImageProcessing/golf_sim_config.json +++ b/Software/LMSourceCode/ImageProcessing/golf_sim_config.json @@ -295,6 +295,7 @@ "kCamera2ComparisonGain": "0.8", "kCamera2CalibrateOrLocationGain": "1.0", "kCamera2Contrast": "1.2", + "kCamera2Saturation": "0.0", "kCamera2PuttingGain": "1.5", "kCamera2PuttingContrast": "1.2", "kCamera1StillShutterTimeuS": "40000", @@ -475,10 +476,15 @@ "kGSProConnectPort": "0921" }, "E6": { - "kE6Comment": "USE CMD LINE - Exampe: --e6_host_address 10.0.0.29", + "kE6Comment": "USE CMD LINE - Example: --e6_host_address 10.0.0.29", "kE6ConnectAddress": "", "kE6ConnectPort": "2483", "kE6InterMessageDelayMs": "50" + }, + "OpenGolfSim":{ + "kOGSComment":"USE CMD LINE OPTION - Example: --open_golf_sim_host_address 10.0.0.25", + "kOGSConnectAddress": "", + "kOGSConnectPort": "3111" } }, "club_data": { diff --git a/Software/LMSourceCode/ImageProcessing/gs_config.cpp b/Software/LMSourceCode/ImageProcessing/gs_config.cpp index a2a763ce..edfc5ada 100644 --- a/Software/LMSourceCode/ImageProcessing/gs_config.cpp +++ b/Software/LMSourceCode/ImageProcessing/gs_config.cpp @@ -177,7 +177,8 @@ bool GolfSimConfiguration::ReadValues() { SetConstant("gs_config.cameras.kCamera1HighFPSGain", LibCameraInterface::kCamera1HighFPSGain); SetConstant("gs_config.cameras.kCamera1Contrast", LibCameraInterface::kCamera1Contrast); SetConstant("gs_config.cameras.kCamera2Gain", LibCameraInterface::kCamera2Gain); - + SetConstant("gs_config.comeras.kCamera2Saturations", LibCameraInterface::kCamera2Saturation); + // Let the command-line gain parameter override the .json config file parameter // TBD - May want to have separate gain options? if (GolfSimOptions::GetCommandLineOptions().camera_gain_ > 0.0) { diff --git a/Software/LMSourceCode/ImageProcessing/gs_opengolfsim_interface.cpp b/Software/LMSourceCode/ImageProcessing/gs_opengolfsim_interface.cpp new file mode 100644 index 00000000..ab7b32de --- /dev/null +++ b/Software/LMSourceCode/ImageProcessing/gs_opengolfsim_interface.cpp @@ -0,0 +1,196 @@ +#include +#include + +#include "logging_tools.h" +#include "gs_config.h" +#include "gs_options.h" +#include "gs_ui_system.h" + +#include "gs_opengolfsim_interface.h" +#include "gs_opengolfsim_results.h" + +namespace golf_sim { + +static const int kDefaultOGSPort = 3111; +static const char* kDefaultOGSHost = ""; // empty = disabled by default + +static std::string ToString(SimConnState s) { + switch (s) { + case SimConnState::kDisabled: return "Disabled"; + case SimConnState::kDisconnected: return "Disconnected"; + case SimConnState::kConnecting: return "Connecting"; + case SimConnState::kConnected: return "Connected"; + case SimConnState::kError: return "Error"; + } + return "Unknown"; +} + +void GsOpenGolfSimInterface::OnConnectionStateChanged( + SimConnState /*from*/, SimConnState to, const std::string& reason) +{ + GsIPCResultType uiType = GsIPCResultType::kWaitingForSimulatorArmed; + std::string human; + + switch (to) { + case SimConnState::kDisabled: + human = "OpenGolfSim disabled (no host/port configured)"; + break; + + case SimConnState::kConnecting: + human = "OpenGolfSim connecting to " + socket_connect_address_ + ":" + socket_connect_port_; + break; + + case SimConnState::kConnected: + break; + + case SimConnState::kDisconnected: + human = "OpenGolfSim disconnected" + (reason.empty() ? "" : (": " + reason)); + break; + + case SimConnState::kError: + default: + uiType = GsIPCResultType::kError; + human = "OpenGolfSim socket error" + (reason.empty() ? "" : (": " + reason)); + break; + } + + // machine tag (Python parses this) + std::string state = + (to == SimConnState::kConnected) ? "connected" : + (to == SimConnState::kConnecting) ? "connecting" : + (to == SimConnState::kDisabled) ? "disabled" : + (to == SimConnState::kDisconnected) ? "disconnected" : "error"; + + std::string tag = "SIM_CONN OpenGolfSim state=" + state; + if (!reason.empty()) tag += " reason=" + reason; + + // ONE message: easy parse + still readable + std::string combined = "[" + tag + "] " + human; + + GsUISystem::SendIPCStatusMessage(uiType, combined); +} + + +std::string GsOpenGolfSimInterface::EnsureNewline(const std::string& s) { + if (!s.empty() && s.back() == '\n') return s; + return s + "\n"; +} + +GsOpenGolfSimInterface::GsOpenGolfSimInterface() { + // Defaults + socket_connect_address_ = kDefaultOGSHost; + socket_connect_port_ = std::to_string(kDefaultOGSPort); // <-- port is a string in base class + last_device_status_.clear(); + + // Pull from config + GolfSimConfiguration::SetConstant( + "gs_config.golf_simulator_interfaces.OpenGolfSim.kOGSConnectAddress", + socket_connect_address_ + ); + GolfSimConfiguration::SetConstant( + "gs_config.golf_simulator_interfaces.OpenGolfSim.kOGSConnectPort", + socket_connect_port_ + ); +} + +GsOpenGolfSimInterface::~GsOpenGolfSimInterface() {} + +bool GsOpenGolfSimInterface::InterfaceIsPresent() { + // Read config into locals (static function!) + std::string addr = kDefaultOGSHost; + std::string port_str = std::to_string(kDefaultOGSPort); + + GolfSimConfiguration::SetConstant( + "gs_config.golf_simulator_interfaces.OpenGolfSim.kOGSConnectAddress", + addr + ); + GolfSimConfiguration::SetConstant( + "gs_config.golf_simulator_interfaces.OpenGolfSim.kOGSConnectPort", + port_str + ); + + int port = -1; + try { port = std::stoi(port_str); } catch (...) { port = -1; } + + // Treat empty/"disabled" as off + if (addr.empty() || addr == "disabled" || port <= 0) { + GS_LOG_TRACE_MSG(trace, "GsOpenGolfSimInterface::InterfaceIsPresent - Not Present"); + return false; + } + + GS_LOG_TRACE_MSG(trace, + "GsOpenGolfSimInterface::InterfaceIsPresent - Present (addr=" + addr + + ", port=" + std::to_string(port) + ")" + ); + return true; +} + +bool GsOpenGolfSimInterface::Initialize() { + GS_LOG_TRACE_MSG(trace, "GsOpenGolfSimInterface Initialize called."); + + // Log what we will actually try to connect to + GS_LOG_MSG(info, "OpenGolfSim connect target: " + socket_connect_address_ + ":" + socket_connect_port_); + + if (!GsSimSocketInterface::Initialize()) { + GS_LOG_MSG(error, "GsOpenGolfSimInterface could not Initialize."); + return false; + } + +#ifdef __unix__ + usleep(500); +#endif + + initialized_ = true; + + // Always ready on connect + SendSimMessage(EnsureNewline(R"({"type":"device","status":"ready"})")); + last_device_status_ = "ready"; // avoid immediate duplicate heartbeat + return true; +} + +void GsOpenGolfSimInterface::DeInitialize() { + GsSimSocketInterface::DeInitialize(); +} + +void GsOpenGolfSimInterface::SetSimSystemArmed(const bool /*is_armed*/) {} + +bool GsOpenGolfSimInterface::GetSimSystemArmed() { + return true; +} + +bool GsOpenGolfSimInterface::SendResults(const GsResults& r) { + if (!initialized_) return false; + + // Heartbeat -> device status + if (r.result_message_is_keepalive_) { + const char* status = r.heartbeat_ball_detected_ ? "ready" : "busy"; + if (last_device_status_ == status) return true; + last_device_status_ = status; + + std::string msg = std::string(R"({"type":"device","status":")") + status + R"("})"; + SendSimMessage(EnsureNewline(msg)); + return true; + } + + // Normal shot path + const std::string msg = GenerateResultsDataToSend(r); + if (msg.empty()) return true; + SendSimMessage(msg); + return true; +} + + +std::string GsOpenGolfSimInterface::GenerateResultsDataToSend(const GsResults& r) { + GsOpenGolfSimResults ogs(r); + const std::string payload = ogs.Format(); + if (payload.empty()) + return ""; + return EnsureNewline(payload); +} + +bool GsOpenGolfSimInterface::ProcessReceivedData(const std::string received_data) { + GS_LOG_MSG(info, "Received from OpenGolfSim: " + received_data); + return true; +} + +} // namespace golf_sim diff --git a/Software/LMSourceCode/ImageProcessing/gs_opengolfsim_interface.h b/Software/LMSourceCode/ImageProcessing/gs_opengolfsim_interface.h new file mode 100644 index 00000000..7aeca1bd --- /dev/null +++ b/Software/LMSourceCode/ImageProcessing/gs_opengolfsim_interface.h @@ -0,0 +1,35 @@ +// gs_opengolfsim_interface.h + +#pragma once +#include +#include "gs_sim_socket_interface.h" +#include "gs_results.h" + +namespace golf_sim { + +class GsOpenGolfSimInterface : public GsSimSocketInterface { +public: + GsOpenGolfSimInterface(); + virtual ~GsOpenGolfSimInterface(); + + static bool InterfaceIsPresent(); + + virtual bool Initialize() override; + virtual void DeInitialize() override; + + virtual bool SendResults(const GsResults& results) override; + + virtual void SetSimSystemArmed(const bool is_armed) override; + virtual bool GetSimSystemArmed() override; + +protected: + std::string EnsureNewline(const std::string& s); + virtual std::string GenerateResultsDataToSend(const GsResults& r); + virtual bool ProcessReceivedData(const std::string received_data) override; + void OnConnectionStateChanged(SimConnState from, SimConnState to, const std::string& reason) override; + +private: + std::string last_device_status_; +}; + +} // namespace golf_sim diff --git a/Software/LMSourceCode/ImageProcessing/gs_opengolfsim_results.cpp b/Software/LMSourceCode/ImageProcessing/gs_opengolfsim_results.cpp new file mode 100644 index 00000000..869c7d80 --- /dev/null +++ b/Software/LMSourceCode/ImageProcessing/gs_opengolfsim_results.cpp @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ + +#include +#include +#include + +#include "gs_opengolfsim_results.h" + +namespace golf_sim { + +std::string GsOpenGolfSimResults::Format() const +{ + // Don't ever send keepalives as shots + if (result_message_is_keepalive_) + return ""; + + // Basic sanity: ignore false triggers / mis-detections with near-zero speed + if (!std::isfinite(speed_mph_) || speed_mph_ < 3.0) + return ""; + + // Compute total spin magnitude from components + const double backSpinRpm = back_spin_rpm_; + const double sideSpinRpm = side_spin_rpm_; + const double spinSpeedRpm = std::sqrt(backSpinRpm * backSpinRpm + sideSpinRpm * sideSpinRpm); + + // "20% above normal high wedge spin" filter (12,000 * 1.2 = 14,400) + constexpr double kMaxSpinRpm = 14400.0; + if (!std::isfinite(spinSpeedRpm) || spinSpeedRpm > kMaxSpinRpm) + return ""; + + // Map PiTrac fields to OpenGolfSim fields + const double ballSpeedMph = speed_mph_; + const double vLaunchDeg = vla_deg_; + const double hLaunchDeg = hla_deg_; + const double spinAxisDeg = GetSpinAxis(); + + // Optional: extra sanity on angles/axis + if (!std::isfinite(vLaunchDeg) || !std::isfinite(hLaunchDeg) || !std::isfinite(spinAxisDeg)) + return ""; + + char buf[512]; + std::snprintf( + buf, sizeof(buf), + R"({"type":"shot","shot":{"ballSpeed":%.3f,"verticalLaunchAngle":%.3f,"horizontalLaunchAngle":%.3f,"spinAxis":%.3f,"spinSpeed":%.3f}})", + ballSpeedMph, vLaunchDeg, hLaunchDeg, spinAxisDeg, spinSpeedRpm + ); + + return std::string(buf); +} + +} // namespace golf_sim diff --git a/Software/LMSourceCode/ImageProcessing/gs_opengolfsim_results.h b/Software/LMSourceCode/ImageProcessing/gs_opengolfsim_results.h new file mode 100644 index 00000000..223b2af8 --- /dev/null +++ b/Software/LMSourceCode/ImageProcessing/gs_opengolfsim_results.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#pragma once + +#include "gs_results.h" + +namespace golf_sim { + +class GsOpenGolfSimResults : public GsResults { +public: + GsOpenGolfSimResults() = default; + explicit GsOpenGolfSimResults(const GsResults& results) : GsResults(results) {} + std::string Format() const; +}; + +} diff --git a/Software/LMSourceCode/ImageProcessing/gs_sim_interface.cpp b/Software/LMSourceCode/ImageProcessing/gs_sim_interface.cpp index c1250bb6..dbb87862 100644 --- a/Software/LMSourceCode/ImageProcessing/gs_sim_interface.cpp +++ b/Software/LMSourceCode/ImageProcessing/gs_sim_interface.cpp @@ -11,6 +11,7 @@ #include "gs_sim_interface.h" #include "gs_gspro_interface.h" #include "gs_e6_interface.h" +#include "gs_opengolfsim_interface.h" namespace golf_sim { @@ -81,6 +82,27 @@ namespace golf_sim { } } + if (GsOpenGolfSimInterface::InterfaceIsPresent()) { + GS_LOG_TRACE_MSG(trace, "OpenGolfSim simulator interface detected."); + + GsOpenGolfSimInterface* ogs_sim = new GsOpenGolfSimInterface(); + if (ogs_sim == nullptr) { + GS_LOG_MSG(error, "Could not create an OpenGolfSim simulator interface."); + return false; + } + + // You’ll probably want to add this enum value (see note below) + ogs_sim->simulator_type_ = GolfSimulatorType::kOpenGolfSim; + + interfaces_.push_back(ogs_sim); + + if (!ogs_sim->Initialize()) { + GS_LOG_MSG(error, "OpenGolfSim simulator interface could not be initialized."); + return false; + } +} + + if (interfaces_.size() == 0) { GS_LOG_TRACE_MSG(trace, "No simulator interface detected."); } diff --git a/Software/LMSourceCode/ImageProcessing/gs_sim_interface.h b/Software/LMSourceCode/ImageProcessing/gs_sim_interface.h index 7e12e237..7b86bea2 100644 --- a/Software/LMSourceCode/ImageProcessing/gs_sim_interface.h +++ b/Software/LMSourceCode/ImageProcessing/gs_sim_interface.h @@ -24,7 +24,8 @@ namespace golf_sim { enum GolfSimulatorType { kNone = 0, kGSPro = 1, - kE6 = 2 + kE6 = 2, + kOpenGolfSim = 3 }; GsSimInterface(); diff --git a/Software/LMSourceCode/ImageProcessing/gs_sim_socket_interface.cpp b/Software/LMSourceCode/ImageProcessing/gs_sim_socket_interface.cpp index 5e960c1a..4a0d6e72 100644 --- a/Software/LMSourceCode/ImageProcessing/gs_sim_socket_interface.cpp +++ b/Software/LMSourceCode/ImageProcessing/gs_sim_socket_interface.cpp @@ -50,6 +50,18 @@ namespace golf_sim { // that we don't have to repeatedly do so. May also want to // setup a keep-alive ping to the SimSocket system. GS_LOG_TRACE_MSG(trace, "GsSimSocketInterface Initialize called."); + + // Treat empty/disabled config as disabled + if (socket_connect_address_.empty() || socket_connect_address_ == "disabled" || + socket_connect_port_.empty() || socket_connect_port_ == "0") + { + SetConnectionState(SimConnState::kDisabled, "No sim host/port configured"); + GS_LOG_MSG(info, "Sim socket disabled (no host/port configured)."); + return false; // InterfaceIsPresent() should prevent this being called anyway + } + +SetConnectionState(SimConnState::kConnecting); +GS_LOG_MSG(info, "Connecting to SimSocketServer at: " + socket_connect_address_ + ":" + socket_connect_port_); try { @@ -76,6 +88,9 @@ namespace golf_sim { boost::asio::connect(*socket_, endpoints); + + SetConnectionState(SimConnState::kConnected); + GS_LOG_MSG(info, "Connected to SimSocketServer at: " + socket_connect_address_ + ":" + socket_connect_port_); receiver_thread_ = std::unique_ptr(new std::thread(&GsSimSocketInterface::ReceiveSocketData, this)); @@ -85,6 +100,7 @@ namespace golf_sim { } catch (std::exception& e) { + SetConnectionState(SimConnState::kError, e.what()); GS_LOG_MSG(error, "Failed TestSimSocketMessage - Error was: " + std::string(e.what())); return false; } @@ -105,72 +121,74 @@ namespace golf_sim { return true; } - void GsSimSocketInterface::ReceiveSocketData() { +void GsSimSocketInterface::ReceiveSocketData() +{ + receive_thread_exited_ = false; - receive_thread_exited_ = false; + std::array buf{}; // 2000 bytes payload + null terminator + boost::system::error_code error; + std::string received_data_string; - static std::array buf; - boost::system::error_code error; - std::string received_data_string; + while (GolfSimGlobals::golf_sim_running_) + { + GS_LOG_TRACE_MSG(trace, "Waiting to receive data from SimSocketserver."); - while (GolfSimGlobals::golf_sim_running_) - { - // We don't want to re-enter this while we're processing - // a received message - // boost::lock_guard lock(sim_socket_receive_mutex_); + size_t len = 0; - GS_LOG_TRACE_MSG(trace, "Waiting to receive data from SimSocketserver."); + try { + // IMPORTANT: read at most (size-1) so we can always null-terminate + len = socket_->read_some(boost::asio::buffer(buf.data(), buf.size() - 1), error); + } + catch (const std::exception& e) + { + GS_LOG_MSG(error, "GsSimSocketInterface::ReceiveSocketData failed to read from socket - Error was: " + std::string(e.what())); + SetConnectionState(SimConnState::kError, e.what()); + receive_thread_exited_ = true; + return; + } - size_t len = 0; + if (error == boost::asio::error::eof) { + GS_LOG_TRACE_MSG(trace, "GsSimSocketInterface::ReceiveSocketData Received EOF"); + SetConnectionState(SimConnState::kDisconnected, "EOF"); + receive_thread_exited_ = true; + return; + } - try { - // Read_some should be blocking, but if the socket has closed, it will return immediately - len = socket_->read_some(boost::asio::buffer(buf), error); - } - catch (std::exception& e) - { - GS_LOG_MSG(error, "GsSimSocketInterface::ReceiveSocketData failed to read from socket - Error was: " + std::string(e.what())); - } + if (error) { + GS_LOG_MSG(error, "Sim socket receive error: " + error.message()); + SetConnectionState(SimConnState::kError, error.message()); + receive_thread_exited_ = true; + return; + } - if (len == 0) { - GS_LOG_MSG(warning, "Received 0-length message from server. Will attempt to re-initialize"); - /// TBD - Are we sure we want to exit? - receive_thread_exited_ = true; - return; - } + if (len == 0) { + GS_LOG_MSG(warning, "Received 0-length message from server."); + SetConnectionState(SimConnState::kDisconnected, "0-length read"); + receive_thread_exited_ = true; + return; + } - // Null-terminate the string - buf[len] = (char)0; - received_data_string = std::string(buf.data()); - GS_LOG_TRACE_MSG(trace, " Read some data (" + std::to_string(len) + " bytes) : " + received_data_string); - - if (error == boost::asio::error::eof) { - // Connection closed cleanly by peer. - // In this case, we may want to de-initialize - GS_LOG_TRACE_MSG(trace, "GsSimSocketInterface::ReceiveSocketData Received EOF"); - receive_thread_exited_ = true; - return; - } - else if (error) { - GS_LOG_TRACE_MSG(trace, "GsSimSocketInterface::ReceiveSocketData Received Error"); - throw boost::system::system_error(error); // Some other error. - } - // Derived classes will, for example, parse the message and inject any - // relevant events into the FSM. + // Null-terminate and build string + buf[len] = '\0'; + received_data_string.assign(buf.data(), len); - GS_LOG_TRACE_MSG(trace, "Received SimSocket message of: \n" + received_data_string); + GS_LOG_TRACE_MSG(trace, " Read some data (" + std::to_string(len) + " bytes) : " + received_data_string); + GS_LOG_TRACE_MSG(trace, "Received SimSocket message of: \n" + received_data_string); - if (!ProcessReceivedData(received_data_string)) { - GS_LOG_MSG(error, "Failed GsSimSocketInterface::ReceiveSocketData - Could process data: " + received_data_string); - return; - } + if (!ProcessReceivedData(received_data_string)) { + GS_LOG_MSG(error, "ProcessReceivedData failed"); + SetConnectionState(SimConnState::kError, "ProcessReceivedData failed"); + receive_thread_exited_ = true; + return; } - - GS_LOG_MSG(error, "GsSimSocketInterface::ReceiveSocketData Exiting"); } - void GsSimSocketInterface::DeInitialize() { + GS_LOG_MSG(info, "GsSimSocketInterface::ReceiveSocketData Exiting"); +} + + void GsSimSocketInterface::DeInitialize() { + SetConnectionState(SimConnState::kDisconnected, "DeInitialize()"); GS_LOG_TRACE_MSG(trace, "GsSimSocketInterface::DeInitialize() called."); try { @@ -209,6 +227,11 @@ namespace golf_sim { size_t write_length = 0; boost::system::error_code error; + if (!socket_) { + SetConnectionState(SimConnState::kDisconnected, "socket_ was null on write"); + return -1; + } + GS_LOG_TRACE_MSG(trace, "GsSimSocketInterface::SendSimMessage - Message was: " + message); // We don't want to re-enter this while we're processing @@ -219,9 +242,18 @@ namespace golf_sim { try { write_length = socket_->write_some(boost::asio::buffer(message), error); + + if (error) { + SetConnectionState(SimConnState::kError, error.message()); + receive_thread_exited_ = true; // forces reconnect path in SendResults + GS_LOG_MSG(error, "Sim socket write failed: " + error.message()); + return -2; + } } catch (std::exception& e) { + SetConnectionState(SimConnState::kError, e.what()); + receive_thread_exited_ = true; GS_LOG_MSG(error, "Failed TestE6Message - Error was: " + std::string(e.what()) + ". Error code was:" + std::to_string(error.value()) ); return -2; } diff --git a/Software/LMSourceCode/ImageProcessing/gs_sim_socket_interface.h b/Software/LMSourceCode/ImageProcessing/gs_sim_socket_interface.h index 3abbbcb5..a0bd099c 100644 --- a/Software/LMSourceCode/ImageProcessing/gs_sim_socket_interface.h +++ b/Software/LMSourceCode/ImageProcessing/gs_sim_socket_interface.h @@ -5,6 +5,10 @@ #pragma once +#include +#include +#include +#include #include #include @@ -18,6 +22,14 @@ using ip::tcp; namespace golf_sim { + enum class SimConnState { + kDisabled = 0, + kDisconnected, + kConnecting, + kConnected, + kError + }; + class GsSimSocketInterface : public GsSimInterface { public: @@ -37,6 +49,20 @@ namespace golf_sim { virtual void ReceiveSocketData(); + // ---- connection state API ---- + SimConnState GetConnectionState() const { + return connection_state_.load(std::memory_order_relaxed); + } + + bool IsConnected() const { + return GetConnectionState() == SimConnState::kConnected; + } + + std::string GetLastConnectionError() const { + boost::lock_guard lock(conn_state_mutex_); + return last_connection_error_; + } + public: std::string socket_connect_address_; @@ -52,6 +78,33 @@ namespace golf_sim { // return the number of bytes written virtual int SendSimMessage(const std::string& message); + // ---- state transition helper + hook ---- + void SetConnectionState(SimConnState s, const std::string& reason = "") { + SimConnState prev = connection_state_.exchange(s, std::memory_order_relaxed); + + { + boost::lock_guard lock(conn_state_mutex_); + + // Clear stale error when we move into healthy-ish states + if (s == SimConnState::kConnected || s == SimConnState::kConnecting) { + last_connection_error_.clear(); + } + + // Store reason when provided (works for any state) + if (!reason.empty()) { + last_connection_error_ = reason; + } + } + + if (prev != s || !reason.empty()) { + OnConnectionStateChanged(prev, s, reason); + } +} + + + // Derived classes can override to publish to ActiveMQ / UI / logs + virtual void OnConnectionStateChanged(SimConnState /*from*/, SimConnState /*to*/, const std::string& /*reason*/) {} + protected: tcp::socket* socket_ = nullptr; @@ -64,6 +117,11 @@ namespace golf_sim { boost::mutex sim_socket_receive_mutex_; boost::mutex sim_socket_send_mutex_; + + // ---- state storage ---- + std::atomic connection_state_{SimConnState::kDisconnected}; + mutable boost::mutex conn_state_mutex_; + std::string last_connection_error_; }; } diff --git a/Software/LMSourceCode/ImageProcessing/libcamera_interface.cpp b/Software/LMSourceCode/ImageProcessing/libcamera_interface.cpp index 77332823..57a5cf0f 100644 --- a/Software/LMSourceCode/ImageProcessing/libcamera_interface.cpp +++ b/Software/LMSourceCode/ImageProcessing/libcamera_interface.cpp @@ -60,6 +60,7 @@ namespace golf_sim { double LibCameraInterface::kCamera2ComparisonGain = 0.8; double LibCameraInterface::kCamera2StrobedEnvironmentGain = 0.8; double LibCameraInterface::kCamera2Contrast = 1.0; + double LibCameraInterface::kCamera2Saturation = 0.0; double LibCameraInterface::kCamera2CalibrateOrLocationGain = 1.0; double LibCameraInterface::kCamera2PuttingGain = 4.0; double LibCameraInterface::kCamera2PuttingContrast = 1.0; @@ -1432,6 +1433,8 @@ bool WaitForCam2Trigger(cv::Mat& return_image) { GolfSimOptions::GetCommandLineOptions().system_mode_ == SystemMode::kCamera2AutoCalibrate) { options->gain = LibCameraInterface::kCamera2CalibrateOrLocationGain; + options->saturation = (float)LibCameraInterface::kCamera2Saturation; + } else if (GolfSimClubs::GetCurrentClubType() == GolfSimClubs::kPutter) { options->gain = LibCameraInterface::kCamera2PuttingGain; @@ -1440,9 +1443,11 @@ bool WaitForCam2Trigger(cv::Mat& return_image) { else { if (!GolfSimOptions::GetCommandLineOptions().lm_comparison_mode_) { options->gain = LibCameraInterface::kCamera2Gain; + options->saturation = (float)LibCameraInterface::kCamera2Saturation; } else { options->gain = LibCameraInterface::kCamera2ComparisonGain; + options->saturation = (float)LibCameraInterface::kCamera2Saturation; } options->contrast = LibCameraInterface::kCamera2Contrast; diff --git a/Software/LMSourceCode/ImageProcessing/libcamera_interface.h b/Software/LMSourceCode/ImageProcessing/libcamera_interface.h index 3e3e299e..f3fcc8ba 100644 --- a/Software/LMSourceCode/ImageProcessing/libcamera_interface.h +++ b/Software/LMSourceCode/ImageProcessing/libcamera_interface.h @@ -55,6 +55,7 @@ namespace golf_sim { static double kCamera1HighFPSGain; // 15.0 to TBD?? static double kCamera1Contrast; // 0.0 to 32.0 static double kCamera2Gain; // 0.0 to TBD?? + static double kCamera2Saturation; //0.0 = greyscale, 1.0 = normal static double kCamera2ComparisonGain; // 0.0 to TBD?? static double kCamera2CalibrateOrLocationGain; static double kCamera2StrobedEnvironmentGain; diff --git a/Software/LMSourceCode/ImageProcessing/meson.build b/Software/LMSourceCode/ImageProcessing/meson.build index c4bfe389..40c8339e 100644 --- a/Software/LMSourceCode/ImageProcessing/meson.build +++ b/Software/LMSourceCode/ImageProcessing/meson.build @@ -216,6 +216,8 @@ pitrac_lm_sources += ([ 'gs_ipc_system.cpp', 'gs_message_consumer.cpp', 'gs_message_producer.cpp', + 'gs_opengolfsim_interface.cpp', + 'gs_opengolfsim_results.cpp' ]) pitrac_lm_sources += ([ diff --git a/Software/web-server/configurations.json b/Software/web-server/configurations.json index b0615dcb..6ac5e658 100644 --- a/Software/web-server/configurations.json +++ b/Software/web-server/configurations.json @@ -590,6 +590,24 @@ "requiresRestart": true, "passedVia": "json", "passedTo": "both" + }, + "gs_config.cameras.kCamera2Saturation": { + "category": "Cameras", + "subcategory": "basic", + "basicSubcategory": "Cameras", + "displayName": "Camera 2 Saturation", + "description": "Saturation setting for Camera 2 - 0.0 for black and white images", + "visibleWhen": { + "system.mode": "single" + }, + "type": "number", + "min": 0.0, + "max": 2.0, + "step": 0.1, + "default": 0.0, + "requiresRestart": true, + "passedVia": "json", + "passedTo": "both" }, "gs_config.cameras.kCamera1SearchCenterX": { "category": "Cameras", @@ -675,6 +693,33 @@ "passedVia": "json", "passedTo": "both" }, + "gs_config.golf_simulator_interfaces.OpenGolfSim.kOGSConnectAddress": { + "category": "Simulators", + "subcategory": "basic", + "basicSubcategory": "Simulators", + "displayName": "Open Golf Sim Address", + "description": "IP address of Open Golf Sim simulator (e.g., 192.168.1.100)", + "type": "ip_address", + "default": "", + "requiresRestart": false, + "passedVia": "json", + "passedTo": "both", + "cliArgument": "--gspro_host_address" + }, + "gs_config.golf_simulator_interfaces.OpenGolfSim.kOGSConnectPort": { + "category": "Simulators", + "subcategory": "basic", + "basicSubcategory": "Simulators", + "displayName": "Open Golf Sim Port", + "description": "Port number for Open Golf simulator", + "type": "number", + "min": 1, + "max": 65535, + "default": 3111, + "requiresRestart": true, + "passedVia": "json", + "passedTo": "both" + }, "gs_config.ball_identification.kUseCLAHEProcessing": { "category": "Ball Detection", "subcategory": "basic", diff --git a/Software/web-server/static/css/dashboard.css b/Software/web-server/static/css/dashboard.css index c059549a..fe951b28 100644 --- a/Software/web-server/static/css/dashboard.css +++ b/Software/web-server/static/css/dashboard.css @@ -242,6 +242,37 @@ background-color: var(--error); } +/* Open Golf Sim Controls */ +.sim-badge { margin-top: 6px; font-size: 12px; opacity: 0.9; } +.sim-badge__state { font-weight: 700; margin-left: 6px; } + +.ogs-controls { margin-top: 18px; padding: 14px; border-radius: 10px; background: rgba(255,255,255,0.03); } +.ogs-controls h2 { margin: 0 0 12px 0; font-size: 16px; } + +.ogs-controls-grid { + display: grid; + grid-template-columns: repeat(3, minmax(90px, 1fr)); + gap: 10px; + align-items: stretch; +} + +.ogs-btn { + padding: 14px 10px; + border-radius: 10px; + border: 1px solid rgba(255,255,255,0.12); + background: rgba(255,255,255,0.06); + color: inherit; + font-weight: 700; + cursor: pointer; + user-select: none; + touch-action: manipulation; +} + +.ogs-btn:active { transform: scale(0.98); } + +.ogs-controls-hint { margin-top: 10px; font-size: 12px; opacity: 0.75; } +/* Open Golf Sim Controls */ + @media (max-width: 768px) { .metrics-grid { grid-template-columns: 1fr; diff --git a/packaging/src/boot/config.sh b/packaging/src/boot/config.sh new file mode 100644 index 00000000..070bd392 --- /dev/null +++ b/packaging/src/boot/config.sh @@ -0,0 +1,4 @@ +echo "# This file is located at 'src/./boot/config.sh'." +echo "# It contains the implementation for the 'pitrac boot config' command." +echo "# The code you write here will be wrapped by a function named 'pitrac_boot_config_command()'." +echo "# Feel free to edit this file; your changes will persist when regenerating." diff --git a/packaging/src/calibrate/auto.sh b/packaging/src/calibrate/auto.sh new file mode 100644 index 00000000..03b84a43 --- /dev/null +++ b/packaging/src/calibrate/auto.sh @@ -0,0 +1,4 @@ +echo "# This file is located at 'src/./calibrate/auto.sh'." +echo "# It contains the implementation for the 'pitrac calibrate auto' command." +echo "# The code you write here will be wrapped by a function named 'pitrac_calibrate_auto_command()'." +echo "# Feel free to edit this file; your changes will persist when regenerating." diff --git a/packaging/src/calibrate/camera.sh b/packaging/src/calibrate/camera.sh new file mode 100644 index 00000000..24f48fee --- /dev/null +++ b/packaging/src/calibrate/camera.sh @@ -0,0 +1,4 @@ +echo "# This file is located at 'src/./calibrate/camera.sh'." +echo "# It contains the implementation for the 'pitrac calibrate camera' command." +echo "# The code you write here will be wrapped by a function named 'pitrac_calibrate_camera_command()'." +echo "# Feel free to edit this file; your changes will persist when regenerating." diff --git a/packaging/src/calibrate/wizard.sh b/packaging/src/calibrate/wizard.sh new file mode 100644 index 00000000..4cbb18ac --- /dev/null +++ b/packaging/src/calibrate/wizard.sh @@ -0,0 +1,4 @@ +echo "# This file is located at 'src/./calibrate/wizard.sh'." +echo "# It contains the implementation for the 'pitrac calibrate wizard' command." +echo "# The code you write here will be wrapped by a function named 'pitrac_calibrate_wizard_command()'." +echo "# Feel free to edit this file; your changes will persist when regenerating." diff --git a/packaging/src/run.sh b/packaging/src/run.sh new file mode 100644 index 00000000..d22184e2 --- /dev/null +++ b/packaging/src/run.sh @@ -0,0 +1,4 @@ +echo "# This file is located at 'src/./run.sh'." +echo "# It contains the implementation for the 'pitrac run' command." +echo "# The code you write here will be wrapped by a function named 'pitrac_run_command()'." +echo "# Feel free to edit this file; your changes will persist when regenerating." diff --git a/packaging/src/setup.sh b/packaging/src/setup.sh new file mode 100644 index 00000000..2cb91018 --- /dev/null +++ b/packaging/src/setup.sh @@ -0,0 +1,4 @@ +echo "# This file is located at 'src/./setup.sh'." +echo "# It contains the implementation for the 'pitrac setup' command." +echo "# The code you write here will be wrapped by a function named 'pitrac_setup_command()'." +echo "# Feel free to edit this file; your changes will persist when regenerating." diff --git a/packaging/src/status.sh b/packaging/src/status.sh new file mode 100644 index 00000000..22767ca4 --- /dev/null +++ b/packaging/src/status.sh @@ -0,0 +1,4 @@ +echo "# This file is located at 'src/./status.sh'." +echo "# It contains the implementation for the 'pitrac status' command." +echo "# The code you write here will be wrapped by a function named 'pitrac_status_command()'." +echo "# Feel free to edit this file; your changes will persist when regenerating." diff --git a/packaging/src/stop.sh b/packaging/src/stop.sh new file mode 100644 index 00000000..91fd9bc5 --- /dev/null +++ b/packaging/src/stop.sh @@ -0,0 +1,4 @@ +echo "# This file is located at 'src/./stop.sh'." +echo "# It contains the implementation for the 'pitrac stop' command." +echo "# The code you write here will be wrapped by a function named 'pitrac_stop_command()'." +echo "# Feel free to edit this file; your changes will persist when regenerating."