diff --git a/vSMR/Rimcas.cpp b/vSMR/Rimcas.cpp index d39eba3c0..cb3d1a657 100644 --- a/vSMR/Rimcas.cpp +++ b/vSMR/Rimcas.cpp @@ -1,5 +1,6 @@ #include "stdafx.h" #include "Rimcas.hpp" +#include "SMRRadar.hpp" #include "Logger.h" CRimcas::CRimcas() {} @@ -234,8 +235,7 @@ void CRimcas::OnRefreshEnd(CRadarScreen *instance, int threshold) { bool isAnotherAcApproaching = ApproachingAircrafts.count(it->first) > 0; - if (AcOnRunway.count(it->first) > 1 || isOnClosedRunway || - isAnotherAcApproaching) { + if (AcOnRunway.count(it->first) > 1 || isOnClosedRunway || (AcOnRunway.count(it->first) > 0 && isAnotherAcApproaching)) { auto AcOnRunwayRange = AcOnRunway.equal_range(it->first); @@ -356,7 +356,11 @@ void CRimcas::TrackDeparture(CRadarTarget Rt, CFlightPlan fp, string runwayArea = GetAcInRunwayArea(Rt, instance); bool onRunway = (runwayArea != string_false); - if (onRunway && DepartedAircraft.find(callsign) == DepartedAircraft.end()) { + // Only add if the aircraft is on its assigned departure runway, not crossing another + string depRwy = fp.GetFlightPlanData().GetDepartureRwy(); + bool onDepartureRunway = onRunway && depRwy.length() > 0 && runwayArea.find(depRwy) != string::npos; + + if (onDepartureRunway && DepartedAircraft.find(callsign) == DepartedAircraft.end()) { DepartureInfo info; info.callsign = callsign; if (info.callsign.length() > 8) { @@ -375,12 +379,13 @@ void CRimcas::TrackDeparture(CRadarTarget Rt, CFlightPlan fp, info.wakeTurbCat = ""; info.wakeTurbCat += fp.GetFlightPlanData().GetAircraftWtc(); - info.airborneFreq = fp.GetControllerAssignedData().GetFlightStripAnnotation(8); - if (info.airborneFreq.length() == 0) { - info.airborneFreq = "QSY"; - } - if (info.airborneFreq.length() > 7) { - info.airborneFreq = info.airborneFreq.substr(0, 7); + info.airborneFreq = "QSY"; + // Get frequency from UKCP integration socket + if (SMRPluginSharedData::ukcpIntegration != nullptr) { + std::string ukcpFreq = SMRPluginSharedData::ukcpIntegration->GetDepartureFrequency(callsign); + if (!ukcpFreq.empty()) { + info.airborneFreq = ukcpFreq; + } } info.groundAltitude = Rt.GetPosition().GetPressureAltitude(); @@ -394,11 +399,20 @@ void CRimcas::TrackDeparture(CRadarTarget Rt, CFlightPlan fp, // Update and start timer once aircraft is airborne if (DepartedAircraft.find(callsign) != DepartedAircraft.end()) { DepartureInfo &info = DepartedAircraft[callsign]; + + // (always check for updates) + if (SMRPluginSharedData::ukcpIntegration != nullptr) { + std::string ukcpFreq = SMRPluginSharedData::ukcpIntegration->GetDepartureFrequency(callsign); + if (!ukcpFreq.empty()) { + info.airborneFreq = ukcpFreq; + } + } + int currentAltitude = Rt.GetPosition().GetPressureAltitude(); - // Remove aircraft if above 6000 feet - if (currentAltitude > 6000) { - Logger::info("TrackDeparture: Removing " + callsign + " - above 6000ft (alt=" + std::to_string(currentAltitude) + ")"); + // Remove aircraft if above 11000 feet + if (currentAltitude > 11000) { + Logger::info("TrackDeparture: Removing " + callsign + " - above 11000ft (alt=" + std::to_string(currentAltitude) + ")"); DepartedAircraft.erase(callsign); return; } @@ -429,9 +443,9 @@ void CRimcas::UpdateDepartureTimer(int departureDisplayDurationSecs, CRadarScree continue; } - // Remove if above 6000 feet + // Remove if above 11000 feet CRadarTarget rt = instance->GetPlugIn()->RadarTargetSelect(pair.first.c_str()); - if (rt.IsValid() && rt.GetPosition().GetPressureAltitude() > 6000) { + if (rt.IsValid() && rt.GetPosition().GetPressureAltitude() > 11000) { toRemove.push_back(pair.first); continue; } diff --git a/vSMR/Rimcas.hpp b/vSMR/Rimcas.hpp index 589ca7ffa..1d2c12118 100644 --- a/vSMR/Rimcas.hpp +++ b/vSMR/Rimcas.hpp @@ -49,7 +49,8 @@ class CRimcas { string wakeTurbCat = ""; string airborneFreq = ""; // QSY frequency clock_t liftoffTime = 0; - bool dismissed = false; + bool handedOff = false; // First click: mark as handed off (green) + bool dismissed = false; // Second click: remove from list int groundAltitude = 0; // Altitude when on ground bool timerStarted = false; // Whether timer has begun }; diff --git a/vSMR/SMRPlugin.cpp b/vSMR/SMRPlugin.cpp index 0c4962295..68397c3f9 100644 --- a/vSMR/SMRPlugin.cpp +++ b/vSMR/SMRPlugin.cpp @@ -301,7 +301,7 @@ CSMRPlugin::CSMRPlugin(void) :CPlugIn(EuroScopePlugIn::COMPATIBILITY_CODE, MY_PL { Logger::DLL_PATH = ""; - Logger::ENABLED = true; + Logger::ENABLED = false; // // Adding the SMR Display type @@ -334,6 +334,12 @@ CSMRPlugin::CSMRPlugin(void) :CPlugIn(EuroScopePlugIn::COMPATIBILITY_CODE, MY_PL DllPath = DllPathFile; DllPath.resize(DllPath.size() - strlen("vSMR.dll")); Logger::DLL_PATH = DllPath; + + // Start UKCP integration socket client + if (SMRPluginSharedData::ukcpIntegration == nullptr) { + SMRPluginSharedData::ukcpIntegration = new UKCPIntegration(); + } + SMRPluginSharedData::ukcpIntegration->Start(); } CSMRPlugin::~CSMRPlugin() @@ -350,6 +356,13 @@ CSMRPlugin::~CSMRPlugin() { io_service.stop(); //vStripsThread.join(); + + // Stop UKCP integration + if (SMRPluginSharedData::ukcpIntegration != nullptr) { + SMRPluginSharedData::ukcpIntegration->Stop(); + delete SMRPluginSharedData::ukcpIntegration; + SMRPluginSharedData::ukcpIntegration = nullptr; + } } catch (std::exception& e) { diff --git a/vSMR/SMRRadar.cpp b/vSMR/SMRRadar.cpp index 013255c9d..bbf951545 100644 --- a/vSMR/SMRRadar.cpp +++ b/vSMR/SMRRadar.cpp @@ -815,12 +815,19 @@ void CSMRRadar::OnClickScreenObject(int ObjectType, const char *sObjectId, getActiveAirport().c_str()); } - // Handle click on QSY to dismiss the aircraft + // Handle click on QSY - two stage: first marks handed off (green), second dismisses if (ObjectType == RIMCAS_DEP_WINDOW_QSY) { string idStr(sObjectId); if (idStr.length() > 4 && idStr.substr(0, 4) == "QSY_") { string callsign = idStr.substr(4); - RimcasInstance->DismissDeparture(callsign); + auto it = RimcasInstance->DepartedAircraft.find(callsign); + if (it != RimcasInstance->DepartedAircraft.end()) { + if (!it->second.handedOff) { + it->second.handedOff = true; + } else { + RimcasInstance->DismissDeparture(callsign); + } + } RequestRefresh(); } } @@ -3243,7 +3250,11 @@ void CSMRRadar::OnRefresh(HDC hDC, int Phase) { // Draw each enabled column if (depWindowShowCallsign) { - dc.TextOutA(rowX, rowY, info.callsign.c_str()); + string displayCallsign = info.callsign; + if (info.timerStarted) { + displayCallsign = "^ " + displayCallsign; + } + dc.TextOutA(rowX, rowY, displayCallsign.c_str()); rowX += colCallsignWidth + colPadding; } @@ -3272,9 +3283,14 @@ void CSMRRadar::OnRefresh(HDC hDC, int Phase) { if (depWindowShowFreq) { CRect qsyRect = {rowX, rowY, rowX + colFreqWidth, rowY + TextHeight}; - // Draw box around QSY button - CPen ButtonPen(PS_SOLID, 1, RGB(100, 100, 100)); + // Draw box around QSY button - bright green fill if handed off + COLORREF boxColor = info.handedOff ? RGB(0, 255, 0) : RGB(100, 100, 100); + CPen ButtonPen(PS_SOLID, 1, boxColor); CPen *oldPen = dc.SelectObject(&ButtonPen); + if (info.handedOff) { + CBrush greenBrush(RGB(0, 190, 0)); + dc.FillRect(&qsyRect, &greenBrush); + } dc.Rectangle(&qsyRect); dc.SelectObject(oldPen); @@ -3282,10 +3298,11 @@ void CSMRRadar::OnRefresh(HDC hDC, int Phase) { int textWidth = dc.GetTextExtent(info.airborneFreq.c_str()).cx; int qsyX = rowX + colFreqWidth - textWidth - 2; // -2 for padding from edge dc.TextOutA(qsyX, rowY, info.airborneFreq.c_str()); - // Make QSY clickable to dismiss + // Make QSY clickable - first click hands off, second dismisses string qsyKey = "QSY_" + info.callsign; + const char* tooltip = info.handedOff ? "Click to dismiss" : "Click to mark handed off"; AddScreenObject(RIMCAS_DEP_WINDOW_QSY, qsyKey.c_str(), qsyRect, - true, "Click to dismiss"); + true, tooltip); rowX += colFreqWidth + colPadding; } diff --git a/vSMR/SMRRadar.hpp b/vSMR/SMRRadar.hpp index c8a4f96c1..998986ae1 100644 --- a/vSMR/SMRRadar.hpp +++ b/vSMR/SMRRadar.hpp @@ -17,6 +17,7 @@ #include "InsetWindow.h" #include "Logger.h" #include "Rimcas.hpp" +#include "UKCPIntegration.hpp" #include #include #include @@ -37,6 +38,7 @@ static vector ManuallyCorrelated; namespace SMRPluginSharedData { static asio::io_service io_service; +extern UKCPIntegration* ukcpIntegration; // The TCP handler is a shared resource } using namespace SMRSharedData; @@ -106,7 +108,7 @@ class CSMRRadar : public EuroScopePlugIn::CRadarScreen { // Departure Timer Window settings bool showDepartureWindow = true; - int departureDisplayDuration = 180; // 3 minutes in seconds (default) + int departureDisplayDuration = 300; // 5 minutes in seconds (default) bool depWindowShowCallsign = true; bool depWindowShowDest = true; bool depWindowShowAcType = true; diff --git a/vSMR/UKCPIntegration.cpp b/vSMR/UKCPIntegration.cpp new file mode 100644 index 000000000..3098d1460 --- /dev/null +++ b/vSMR/UKCPIntegration.cpp @@ -0,0 +1,259 @@ +#include "stdafx.h" +#include "UKCPIntegration.hpp" +#include "rapidjson/document.h" +#include "rapidjson/writer.h" +#include "rapidjson/stringbuffer.h" + +// Single definition of the shared pointer (declared extern in SMRRadar.hpp) +namespace SMRPluginSharedData { + UKCPIntegration* ukcpIntegration = nullptr; +} + +UKCPIntegration::UKCPIntegration() {} + +UKCPIntegration::~UKCPIntegration() { + Stop(); +} + +void UKCPIntegration::Start() { + if (m_running.load()) + return; + + m_running.store(true); + m_thread = std::thread(&UKCPIntegration::ConnectionThread, this); + Logger::info("UKCPIntegration: Started"); +} + +void UKCPIntegration::Stop() { + m_running.store(false); + if (m_thread.joinable()) { + m_thread.join(); + } + std::lock_guard lock(m_mutex); + m_frequencies.clear(); + m_connected.store(false); + Logger::info("UKCPIntegration: Stopped"); +} + +std::string UKCPIntegration::GetDepartureFrequency(const std::string& callsign) { + std::lock_guard lock(m_mutex); + auto it = m_frequencies.find(callsign); + if (it != m_frequencies.end()) { + return it->second; + } + return ""; +} + +void UKCPIntegration::ConnectionThread() { + while (m_running.load()) { + Logger::info("UKCPIntegration: Attempting connection to localhost:" + std::to_string(UKCP_PORT)); + + SOCKET sock = TryConnect(); + if (sock == INVALID_SOCKET) { + Logger::info("UKCPIntegration: Connection failed, retrying in " + std::to_string(RECONNECT_DELAY_MS / 1000) + "s"); + // Wait with periodic checks so we can stop quickly + for (int i = 0; i < RECONNECT_DELAY_MS / 100 && m_running.load(); i++) { + Sleep(100); + } + continue; + } + + Logger::info("UKCPIntegration: Connected to UKCP"); + + if (!SendHandshake(sock)) { + Logger::info("UKCPIntegration: Handshake failed"); + closesocket(sock); + m_connected.store(false); + continue; + } + + m_connected.store(true); + Logger::info("UKCPIntegration: Handshake sent, listening for messages"); + + // Read loop + while (m_running.load() && ReadAndProcess(sock)) { + // Keep reading + } + + Logger::info("UKCPIntegration: Connection lost"); + closesocket(sock); + m_connected.store(false); + m_recvBuffer.clear(); + + // Clear stale frequency data on disconnect + { + std::lock_guard lock(m_mutex); + m_frequencies.clear(); + } + + // Wait before reconnecting + for (int i = 0; i < RECONNECT_DELAY_MS / 100 && m_running.load(); i++) { + Sleep(100); + } + } +} + +SOCKET UKCPIntegration::TryConnect() { + struct addrinfo hints = {}; + struct addrinfo* result = nullptr; + + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + std::string port = std::to_string(UKCP_PORT); + int rc = getaddrinfo("127.0.0.1", port.c_str(), &hints, &result); + if (rc != 0) { + return INVALID_SOCKET; + } + + SOCKET sock = socket(result->ai_family, result->ai_socktype, result->ai_protocol); + if (sock == INVALID_SOCKET) { + freeaddrinfo(result); + return INVALID_SOCKET; + } + + rc = connect(sock, result->ai_addr, (int)result->ai_addrlen); + freeaddrinfo(result); + + if (rc == SOCKET_ERROR) { + closesocket(sock); + return INVALID_SOCKET; + } + + // Set a receive timeout so we can periodically check m_running + int timeout = 1000; // 1 second + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout)); + + return sock; +} + +bool UKCPIntegration::SendHandshake(SOCKET sock) { + // Build the init message using rapidjson + rapidjson::Document doc; + doc.SetObject(); + auto& alloc = doc.GetAllocator(); + + doc.AddMember("type", "initialise", alloc); + doc.AddMember("version", 1, alloc); + doc.AddMember("id", "vsmr-init-1", alloc); + + rapidjson::Value data(rapidjson::kObjectType); + data.AddMember("integration_name", "vSMR", alloc); + data.AddMember("integration_version", "1.0.0", alloc); + + rapidjson::Value subscriptions(rapidjson::kArrayType); + rapidjson::Value sub(rapidjson::kObjectType); + sub.AddMember("type", "departure_frequency_updated", alloc); + sub.AddMember("version", 1, alloc); + subscriptions.PushBack(sub, alloc); + + data.AddMember("event_subscriptions", subscriptions, alloc); + doc.AddMember("data", data, alloc); + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + doc.Accept(writer); + + std::string msg = buffer.GetString(); + msg += MESSAGE_DELIMITER; + + int sent = send(sock, msg.c_str(), (int)msg.length(), 0); + if (sent == SOCKET_ERROR) { + return false; + } + + Logger::info("UKCPIntegration: Sent handshake: " + std::string(buffer.GetString())); + return true; +} + +bool UKCPIntegration::ReadAndProcess(SOCKET sock) { + char buf[RECV_BUFFER_SIZE]; + + int bytesRead = recv(sock, buf, RECV_BUFFER_SIZE - 1, 0); + + if (bytesRead == SOCKET_ERROR) { + int err = WSAGetLastError(); + if (err == WSAETIMEDOUT) { + // should keep running? + return true; + } + Logger::info("UKCPIntegration: recv error: " + std::to_string(err)); + return false; + } + + if (bytesRead == 0) { + // closed by server + return false; + } + + m_recvBuffer.append(buf, bytesRead); + + size_t pos; + while ((pos = m_recvBuffer.find(MESSAGE_DELIMITER)) != std::string::npos) { + std::string message = m_recvBuffer.substr(0, pos); + m_recvBuffer.erase(0, pos + 1); + + if (!message.empty()) { + ProcessMessage(message); + } + } + + return true; +} + +void UKCPIntegration::ProcessMessage(const std::string& json) { + rapidjson::Document doc; + doc.Parse<0>(json.c_str()); + + if (doc.HasParseError() || !doc.IsObject()) { + Logger::info("UKCPIntegration: Failed to parse message: " + json); + return; + } + + if (!doc.HasMember("type") || !doc["type"].IsString()) { + return; + } + + std::string type = doc["type"].GetString(); + + if (type == "initialisation_success") { + Logger::info("UKCPIntegration: Initialisation successful"); + } + else if (type == "initialisation_failure") { + std::string errors = ""; + if (doc.HasMember("errors") && doc["errors"].IsArray()) { + for (rapidjson::SizeType i = 0; i < doc["errors"].Size(); i++) { + if (i > 0) errors += ", "; + errors += doc["errors"][i].GetString(); + } + } + Logger::info("UKCPIntegration: Initialisation FAILED: " + errors); + } + else if (type == "departure_frequency_updated") { + HandleDepartureFrequencyUpdated(json); + } +} + +void UKCPIntegration::HandleDepartureFrequencyUpdated(const std::string& json) { + rapidjson::Document doc; + doc.Parse<0>(json.c_str()); + + if (doc.HasParseError() || !doc.IsObject()) return; + if (!doc.HasMember("data") || !doc["data"].IsObject()) return; + + const rapidjson::Value& data = doc["data"]; + + if (!data.HasMember("callsign") || !data["callsign"].IsString()) return; + if (!data.HasMember("frequency") || !data["frequency"].IsString()) return; + + std::string callsign = data["callsign"].GetString(); + std::string frequency = data["frequency"].GetString(); + + { + std::lock_guard lock(m_mutex); + m_frequencies[callsign] = frequency; + } + + Logger::info("UKCPIntegration: Frequency update - " + callsign + " -> " + frequency); +} diff --git a/vSMR/UKCPIntegration.hpp b/vSMR/UKCPIntegration.hpp new file mode 100644 index 000000000..53231dc38 --- /dev/null +++ b/vSMR/UKCPIntegration.hpp @@ -0,0 +1,70 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include "Logger.h" + +#pragma comment(lib, "Ws2_32.lib") + +// Connects to the UK Controller Plugin integration socket (localhost:52814) +// and listens for departure_frequency_updated messages. +class UKCPIntegration { +public: + UKCPIntegration(); + ~UKCPIntegration(); + + // Start the background connection thread + void Start(); + + // Stop the background connection thread and clean up + void Stop(); + + // Get the departure frequency for a callsign (thread-safe) + // Returns empty string if no frequency is known + std::string GetDepartureFrequency(const std::string& callsign); + + // Check if connected to UKCP + bool IsConnected() const { return m_connected.load(); } + +private: + // Background thread entry point + void ConnectionThread(); + + // Attempt to connect to UKCP socket + SOCKET TryConnect(); + + // Send the initialisation handshake + bool SendHandshake(SOCKET sock); + + // Read and process messages from the socket + // Returns false if the connection was lost + bool ReadAndProcess(SOCKET sock); + + // Parse a single JSON message + void ProcessMessage(const std::string& json); + + // Parse the departure_frequency_updated message + void HandleDepartureFrequencyUpdated(const std::string& json); + + // Thread-safe frequency map + std::mutex m_mutex; + std::map m_frequencies; + + // Receive buffer for accumulating partial TCP data + std::string m_recvBuffer; + + // Connection state + std::atomic m_running{false}; + std::atomic m_connected{false}; + std::thread m_thread; + + static const int UKCP_PORT = 52814; + static const char MESSAGE_DELIMITER = '\x1F'; + static const int RECONNECT_DELAY_MS = 5000; + static const int RECV_BUFFER_SIZE = 4096; +}; diff --git a/vSMR/vSMR.vcxproj b/vSMR/vSMR.vcxproj index 845526a3d..a5767525a 100644 --- a/vSMR/vSMR.vcxproj +++ b/vSMR/vSMR.vcxproj @@ -142,6 +142,7 @@ Create Create + @@ -168,6 +169,7 @@ +