From e4f79d951f121cee3a081ad9dcb558b06f1bb923 Mon Sep 17 00:00:00 2001 From: TwinFan Date: Mon, 30 Mar 2026 00:16:29 +0200 Subject: [PATCH 01/11] feat/LTAPI: Send cartesian coordinates and transp. mode See TwinFan/LTAPI#12 --- CMakeLists.txt | 2 +- Lib/LTAPI/LTAPI.h | 77 +++++++++++++++++++++++---- Lib/XPMP2 | 2 +- LiveTraffic.xcodeproj/project.pbxproj | 4 +- Src/LTAircraft.cpp | 11 +++- docs/readme.html | 23 ++++++-- 6 files changed, 98 insertions(+), 21 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ad1a7bce..ec44f9bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,7 +23,7 @@ endif() set(CACHE{CMAKE_BUILD_TYPE} TYPE STRING VALUE "RelWithDebInfo") project(LiveTraffic - VERSION 4.3.5 + VERSION 4.3.6 DESCRIPTION "LiveTraffic X-Plane plugin") set(VERSION_BETA 0) diff --git a/Lib/LTAPI/LTAPI.h b/Lib/LTAPI/LTAPI.h index d55f39ad..cf330732 100644 --- a/Lib/LTAPI/LTAPI.h +++ b/Lib/LTAPI/LTAPI.h @@ -10,7 +10,7 @@ /// textual info like type, registration, call sign, flight number. /// @see https://twinfan.github.io/LTAPI/ /// @author Birger Hoppe -/// @copyright (c) 2019-2025 Birger Hoppe +/// @copyright (c) 2019-2026 Birger Hoppe /// @copyright 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 @@ -36,6 +36,7 @@ #include #include #include +#include #include "XPLMDataAccess.h" #include "XPLMGraphics.h" @@ -85,6 +86,20 @@ class LTAPIAircraft FPH_STOPPED_ON_RWY ///< Stopped on runway because ran out of tracking data, plane will disappear soon }; + /// @brief These enumerations define the way the transponder of a given plane is operating. + /// @note as defined by dataRef `sim/cockpit2/tcas/targets/ssr_mode`: + /// "Transponder mode: off=0, stdby=1, on (mode A)=2, alt (mode C)=3, test=4, GND (mode S)=5, ta_only (mode S)=6, ta/ra=7" + enum XPMPTransponderMode { + xpmpTransponderMode_Off = 0, ///< transponder is off not currently sending -> aircraft not visible on TCAS + xpmpTransponderMode_Standby, ///< transponder is in standby, not currently sending -> aircraft not visible on TCAS + xpmpTransponderMode_ModeA, ///< transponder is on, Mode A + xpmpTransponderMode_ModeC, ///< transponder is on, Mode C (Alt) + xpmpTransponderMode_Test, ///< transponder is on, Test + xpmpTransponderMode_ModeS_Gnd, ///< transponder is on, Mode S (Gnd) + xpmpTransponderMode_ModeS_TAOnly, ///< transponder is on, Mode S (TA-Only) + xpmpTransponderMode_ModeS_TARA, ///< transponder is on, Mode S (TA/RA) + }; + /// @brief Bulk data transfer structur for communication with LTAPI /// @note Structure needs to be in synch with LiveTraffic, /// version differences are handled using a struct size "negotiation", @@ -126,20 +141,31 @@ class LTAPIAircraft bool camera : 1; ///< is LiveTraffic's camera on this aircraft? // Misc int multiIdx : 8; ///< multiplayer index if plane reported via sim/multiplayer/position dataRefs, 0 if not + // Transponder Mode (added in LT 4.4.0) + unsigned trspMode : 4; ///< Transponder mode, see enum XPMPTransponderMode (filled only as of LT 4.4.0) // Filler for 8-byte alignment - unsigned filler2 : 8; + unsigned filler2 : 4; unsigned filler3 : 32; } bits; ///< Flights phase, on-ground status, lights // V1.22 additions - double lat = 0.0f; ///< [°] latitude - double lon = 0.0f; ///< [°] longitude - double alt_ft = 0.0f; ///< [ft] altitude - + double lat = 0.0f; ///< [°] latitude + double lon = 0.0f; ///< [°] longitude + double alt_ft = 0.0f; ///< [ft] altitude + + // LT v4.4.0 additions + // Cartesian location in local coordinates + double x = NAN; ///< local Cartesian X coordinate (NAN indicates to delivered by master, e.g. because older version) + double y = NAN; ///< local Cartesian Y coordinate + double z = NAN; ///< local Cartesian Z coordinate + // Cartesian velocity in m/s per axis, updated at least once per second + double v_x = NAN; ///< [m/s] Cartesian velocity in X direction + double v_y = NAN; ///< [m/s] Cartesian velocity in Y direction + double v_z = NAN; ///< [m/s] Cartesian velocity in Z direction /// Constructor initializes some data without defaults LTAPIBulkData() - { memset(&bits, 0, sizeof(bits)); } + { memset(&bits, 0, sizeof(bits)); bits.trspMode = 4; } }; /// @brief Bulk text transfer structur for communication with LTAPI @@ -275,15 +301,31 @@ class LTAPIAircraft float getBearing() const { return bulk.bearing; } ///< [°] to current camera position float getDistNm() const { return bulk.dist_nm; } ///< [nm] distance to current camera int getMultiIdx() const { return bulk.bits.multiIdx; } ///< multiplayer index if plane reported via sim/multiplayer/position dataRefs, 0 if not + XPMPTransponderMode getTrspMode() const ///< Transponder mode, like off, Mode_C, Mode_S_TARA + { return XPMPTransponderMode(bulk.bits.trspMode); } + const char* getTrspModeTxt() const; ///< Transponder mode text, like "off", "Mode C", "Mode S TARA" - // calculated /// @brief `lat`/`lon`/`alt` converted to local coordinates + /// @see https://developer.x-plane.com/article/screencoordinates/#3-D_Coordinate_System + /// @param[out] v_x [m/s] Local cartesian velocity on the x axis of the local coordinate system (roughly "east") + /// @param[out] v_y [m/s] Local cartesian velocity on the y axis of the local coordinate system (roughly "up") + /// @param[out] v_z [m/s] Local cartesian velocity on the z axis of the local coordinate system (roughly "south") + void getLocalVelocities (double& v_x, double& v_y, double& v_z) const + { v_x = bulk.v_x; v_y = bulk.v_y; v_z = bulk.v_z; } + + /// @brief [m/s] Approximate ground speed based on local coordinates + double getLocalGndSpeed_ms () const { return std::hypot(bulk.v_x, bulk.v_z); } + /// @brief [kn] Approximate ground speed based on local coordinates + double getLocalGndSpeed_kn () const { return getLocalGndSpeed_ms() * 1.94384; } + + // calculated (or transferred in newer versions) + /// @brief Local coordinates (coverted from `lat`/`lon`/`alt` in older versions) + /// @see https://developer.x-plane.com/article/screencoordinates/#3-D_Coordinate_System /// @see https://developer.x-plane.com/sdk/XPLMGraphics/#XPLMWorldToLocal /// @param[out] x Local x coordinate /// @param[out] y Local y coordinate /// @param[out] z Local z coordinate - void getLocalCoord (double& x, double& y, double& z) const - { XPLMWorldToLocal(bulk.lat,bulk.lon,bulk.alt_ft*0.3048, &x,&y,&z); } + void getLocalCoord (double& x, double& y, double& z) const; public: /// @brief Standard object creation callback. @@ -509,13 +551,26 @@ class LTDataRef { /// Size of original bulk structure as per LiveTraffic v1.20 constexpr size_t LTAPIBulkData_v120 = 80; +/// Size of bulk structure as per LiveTraffic v1.22 +#if IBM +constexpr size_t LTAPIBulkData_v122 = 120; +#else +constexpr size_t LTAPIBulkData_v122 = 104; +#endif + /// Size of current bulk structure -constexpr size_t LTAPIBulkData_v122 = sizeof(LTAPIAircraft::LTAPIBulkData); +constexpr size_t LTAPIBulkData_v440 = sizeof(LTAPIAircraft::LTAPIBulkData); +#if IBM +static_assert(LTAPIBulkData_v440 == 168, "LTAPIBulkData size is not 152 as expected"); +#else +static_assert(LTAPIBulkData_v440 == 152, "LTAPIBulkData size is not 152 as expected"); +#endif /// Size of original bulk info structure as per previous versions of LiveTraffic constexpr size_t LTAPIBulkInfoTexts_v120 = 264; constexpr size_t LTAPIBulkInfoTexts_v122 = 288; /// Size of current bulk info structure constexpr size_t LTAPIBulkInfoTexts_v240 = sizeof(LTAPIAircraft::LTAPIBulkInfoTexts); +static_assert(LTAPIBulkInfoTexts_v240 == 304, "LTAPIBulkInfoTexts size is not 304 as expected"); #endif /* LTAPI_h */ diff --git a/Lib/XPMP2 b/Lib/XPMP2 index 89aba0ac..2c642d45 160000 --- a/Lib/XPMP2 +++ b/Lib/XPMP2 @@ -1 +1 @@ -Subproject commit 89aba0ac319b985256c4b69b2bdcf97777b34200 +Subproject commit 2c642d452fa09509131f29a11bfa0ba936218039 diff --git a/LiveTraffic.xcodeproj/project.pbxproj b/LiveTraffic.xcodeproj/project.pbxproj index 88417f1a..f5d27df1 100755 --- a/LiveTraffic.xcodeproj/project.pbxproj +++ b/LiveTraffic.xcodeproj/project.pbxproj @@ -835,7 +835,7 @@ LIVETRAFFIC_VERSION_BETA = 1; LIVETRAFFIC_VER_MAJOR = 4; LIVETRAFFIC_VER_MINOR = 3; - LIVETRAFFIC_VER_PATCH = 5; + LIVETRAFFIC_VER_PATCH = 6; LLVM_LTO = NO; MACH_O_TYPE = mh_dylib; MACOSX_DEPLOYMENT_TARGET = 10.15; @@ -956,7 +956,7 @@ LIVETRAFFIC_VERSION_BETA = 1; LIVETRAFFIC_VER_MAJOR = 4; LIVETRAFFIC_VER_MINOR = 3; - LIVETRAFFIC_VER_PATCH = 5; + LIVETRAFFIC_VER_PATCH = 6; LLVM_LTO = YES; MACH_O_TYPE = mh_dylib; MACOSX_DEPLOYMENT_TARGET = 10.15; diff --git a/Src/LTAircraft.cpp b/Src/LTAircraft.cpp index 3422d617..a9ebb027 100644 --- a/Src/LTAircraft.cpp +++ b/Src/LTAircraft.cpp @@ -6,7 +6,7 @@ /// LTAircraft calculates the current position and configuration of the aircraft /// in every flighloop cycle while being called from libxplanemp. /// @author Birger Hoppe -/// @copyright (c) 2018-2020 Birger Hoppe +/// @copyright (c) 2018-2026 Birger Hoppe /// @copyright 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 @@ -2453,6 +2453,7 @@ void LTAircraft::CopyBulkData (LTAPIAircraft::LTAPIBulkData* pOut, pOut->bits.hidden = !IsVisible(); pOut->bits.camera = IsInCameraView(); pOut->bits.multiIdx = tcasTargetIdx; + pOut->bits.trspMode = unsigned(acRadar.mode); // v4.4.0 addition pOut->bits.filler2 = 0; pOut->bits.filler3 = 0; @@ -2462,6 +2463,14 @@ void LTAircraft::CopyBulkData (LTAPIAircraft::LTAPIBulkData* pOut, pOut->lon = GetPPos().lon(); pOut->alt_ft = GetPPos().alt_ft(); } + + // v4.4.0 additions + pOut->x = double(drawInfo.x); // It is double because X-Plane is moving towards double already...we'll change LiveTraffic probably sometime soon, too + pOut->y = double(drawInfo.y); + pOut->z = double(drawInfo.z); + pOut->v_x = v_x; + pOut->v_y = v_y; + pOut->v_z = v_z; } // copies text information out into the bulk structure for LTAPI usage diff --git a/docs/readme.html b/docs/readme.html index 699331fb..6bcf2b81 100755 --- a/docs/readme.html +++ b/docs/readme.html @@ -133,15 +133,28 @@

Release Notes

v4

-

v4.3.5

- +

v4.3.6 Beta

+

- Mandatory Update for X-Plane 12.4.1 to prevent X-Plane from stopping. + Update: In case of doubt you can always just copy all files from the archive + over the files of your existing installation.

+

At least copy the following files, which have changed compared to v4.3.3:

+
    +
  • lin|mac|win_x64/LiveTraffic.xpl
  • +
+ +

Change log:

+ +
    +
  • Sends local cartesian coordinates and transponder mode to LTAPI.
  • +
+ +

v4.3.5

+

- Update: In case of doubt you can always just copy all files from the archive - over the files of your existing installation. + Mandatory Update for X-Plane 12.4.1 to prevent X-Plane from stopping.

At least copy the following files, which have changed compared to v4.3.3:

From 550110a738d874d775dd15f23e557e8c8b409382 Mon Sep 17 00:00:00 2001 From: TwinFan Date: Mon, 6 Apr 2026 17:18:06 +0200 Subject: [PATCH 02/11] fix: Stop prop/rotor when parked --- Src/LTAircraft.cpp | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/Src/LTAircraft.cpp b/Src/LTAircraft.cpp index a9ebb027..94dd9b70 100644 --- a/Src/LTAircraft.cpp +++ b/Src/LTAircraft.cpp @@ -1858,13 +1858,7 @@ bool LTAircraft::CalcPPos() // calculate timestamp can be a bit off, especially when acceleration is in progress, // overwrite with current value as of now ppos.ts() = currCycle.simTime; -/* -#warning Remove this - if (bIsSelected) { - LOG_MSG(logDEBUG,"f=%.4f, p={%s}, head=%.1f -> %.1f", - f, ppos.dbgTxt().c_str(), prevHead, ppos.heading()); - } -*/ + // if we are runnig beyond 'to' we might become invalid (especially too low, too high) // catch that case...likely the a/c is to be removed due to outdated data // soon anyway, we just speed up things a bit here @@ -2909,17 +2903,17 @@ void LTAircraft::UpdatePosition (float, int cycle) SetThrustReversRatio((float)reversers.get()); // for engine / prop rotation we derive a value based on flight model - if (pDoc8643->hasRotor()) + if (GetFlightPhase() == FPH_PARKED) + SetEngineRotRpm(0.0f); + else if (pDoc8643->hasRotor()) SetEngineRotRpm(float(pMdl->PROP_RPM_MAX)); else SetEngineRotRpm(float(pMdl->PROP_RPM_MAX/2 + GetThrustRatio() * pMdl->PROP_RPM_MAX/2)); SetPropRotRpm(GetEngineRotRpm()); // Make props and rotors move based on rotation speed and time passed since last cycle - SetEngineRotAngle(GetEngineRotAngle() + RpmToDegree(GetEngineRotRpm(), currCycle.diffTime)); - - while (GetEngineRotAngle() >= 360.0f) - SetEngineRotAngle(GetEngineRotAngle() - 360.0f); + SetEngineRotAngle(std::fmod(GetEngineRotAngle() + RpmToDegree(GetEngineRotRpm(), currCycle.diffTime), + 360.0f)); SetPropRotAngle(GetEngineRotAngle()); // Gear deflection - has an effect during touch-down only From 917105459a7b3c6b49afeaf8c498eef0eb0c4971 Mon Sep 17 00:00:00 2001 From: TwinFan Date: Mon, 6 Apr 2026 17:30:53 +0200 Subject: [PATCH 03/11] feat/RT/App: Buffered Traffic, Weather enabled --- Include/CoordCalc.h | 10 +++ Include/LTRealTraffic.h | 53 +++++++----- Src/LTOpenGlider.cpp | 8 +- Src/LTOpenSky.cpp | 8 +- Src/LTRealTraffic.cpp | 176 ++++++++++++++++++++++++++++------------ Src/LTWeather.cpp | 6 +- docs/readme.html | 6 ++ 7 files changed, 183 insertions(+), 84 deletions(-) diff --git a/Include/CoordCalc.h b/Include/CoordCalc.h index 547bd8a2..9c80de84 100644 --- a/Include/CoordCalc.h +++ b/Include/CoordCalc.h @@ -527,6 +527,16 @@ struct boundingBoxTy { positionTy sw () const { return positionTy(se.lat(), nw.lon()); } /// north-east corner ("maximum") positionTy ne () const { return positionTy(nw.lat(), se.lon()); } + + double& top () { return nw.lat(); } + double& bottom () { return se.lat(); } + double& left () { return nw.lon(); } + double& right () { return se.lon(); } + + double top () const { return nw.lat(); } + double bottom () const { return se.lat(); } + double left () const { return nw.lon(); } + double right () const { return se.lon(); } // standard string for any output purposes operator std::string() const; diff --git a/Include/LTRealTraffic.h b/Include/LTRealTraffic.h index ff8af194..1101fc88 100644 --- a/Include/LTRealTraffic.h +++ b/Include/LTRealTraffic.h @@ -56,7 +56,7 @@ #define RT_WEATHER_POST "GUID=%s&lat=%.2f&lon=%.2f&alt=%ld&airports=%s&querytype=locwx&toffset=%ld" #define RT_TRAFFIC_URL RT_BASE_URL "/traffic" #define RT_TRAFFIC_POST "GUID=%s&top=%.2f&bottom=%.2f&left=%.2f&right=%.2f&querytype=locationtraffic&toffset=%ld" -#define RT_TRAFFIC_POST_BUFFER "GUID=%s&top=%.2f&bottom=%.2f&left=%.2f&right=%.2f&querytype=locationtraffic&toffset=%ld&buffercount=%d&buffertime=10" +#define RT_TRAFFIC_POST_BUFFER "GUID=%s&top=%.2f&bottom=%.2f&left=%.2f&right=%.2f&querytype=locationtraffic&toffset=%ld&buffercount=%d&buffertime=%d" #define RT_TRAFFIC_POST_PARKED "GUID=%s&top=%.2f&bottom=%.2f&left=%.2f&right=%.2f&querytype=parkedtraffic&toffset=%ld" #define RT_LOCALHOST "0.0.0.0" @@ -90,7 +90,8 @@ constexpr std::chrono::seconds RT_DRCT_ERR_WAIT = std::chrono::seconds(5); ///< constexpr std::chrono::seconds RT_DRCT_ERR_RATE = std::chrono::seconds(10); ///< wait in case of rate violations, too many sessions constexpr std::chrono::minutes RT_DRCT_WX_WAIT = std::chrono::minutes(1); ///< How often to update weather? constexpr int RT_DRCT_MAX_WX_ERR = 5; ///< Max number of consecutive errors during initial weather requests we wait for...before not asking for weather any longer -constexpr int RT_CNT_SEND_TIMING = 240; ///< RT App: After how many position position message also to send a timing message? (with 250ms period, 240 means: every minute) +constexpr int RT_CNT_SEND_TIMING = 240; ///< RT App: After how many position messages also to send a timing message? (with 250ms period, 240 means: every minute) +constexpr int RT_BUFFER_PERIOD = 10; ///< [s] When requesting buffered traffic, how much time between two buffers? /// Fields in a response of a direct connection's request enum RT_DIRECT_FIELDS_TY { @@ -254,7 +255,7 @@ class RealTrafficConnection : public LTFlightDataChannel RT_STATUS_CONNECTED_FULL, // both connected to, and have received UDP data RT_STATUS_STOPPING }; - + protected: // general lock to synch thread access to object members std::recursive_mutex rtMutex; @@ -282,18 +283,18 @@ class RealTrafficConnection : public LTFlightDataChannel positionTy pos; ///< viewer position for which we receive Realtraffic data long tOff = 0; ///< [min] time offset for which we request data } curr; ///< Data for the current request - + /// What's the next time we could send a traffic request? std::chrono::time_point tNextTraffic; /// What's the next time we could send a weather request? std::chrono::time_point tNextWeather; - + /// METAR entry in the NearestMETAR response struct NearestMETAR { std::string ICAO = RT_METAR_UNKN; ///< ICAO code of METAR station double dist = NAN; ///< distance to station double brgTo = NAN; ///< bearing to station - + NearestMETAR() {} ///< Standard constructor, all empty NearestMETAR(const JSON_Object* pObj) { Parse (pObj); } ///< Fill from JSON @@ -328,13 +329,13 @@ class RealTrafficConnection : public LTFlightDataChannel long lTotalFlights = -1; /// Shall we check for parked traffic next time around? (Set from main thread after airport data updates) bool bDoParkedTraffic = false; - + // TCP connection to send current position std::thread thrTcpServer; ///< thread of the TCP listening thread (short-lived) XPMP2::TCPConnection tcpPosSender; ///< TCP connection to communicate with RealTraffic /// Status of the separate TCP listening thread volatile ThrStatusTy eTcpThrStatus = THR_NONE; - + // UDP sockets XPMP2::UDPReceiver udpTrafficData; ///< UDP receiver for traffic data (port 49005) XPMP2::UDPReceiver udpWeatherData; ///< UDP receiver for weather data (port 49004) @@ -342,19 +343,25 @@ class RealTrafficConnection : public LTFlightDataChannel // the self-pipe to shut down the UDP listener thread gracefully SOCKET udpPipe[2] = { INVALID_SOCKET, INVALID_SOCKET }; #endif - double lastReceivedTime = 0.0; // copy of simTime + /// last simtime that we received UDP traffic + double lastReceivedTime = 0.0; + /// last known position to detect fast movement (to request buffered traffic and the like) + positionTy lastKnownViewPos; + /// Expecting buffered traffic first? + bool bWaitForBuffers = true; + /// expected bu // map of last received datagrams for duplicate detection std::map mapDatagrams; /// rolling list of timestamp (diff to now) for detecting historic sending std::deque dequeTS; /// [s] current timestamp adjustment double tsAdjust = 0.0; - + public: RealTrafficConnection (); - + void Stop (bool bWaitJoin) override; ///< Stop the UDP listener gracefully - + // interface called from LTChannel // SetValid also sets internal status void SetValid (bool _valid, bool bMsg = true) override; @@ -362,12 +369,12 @@ class RealTrafficConnection : public LTFlightDataChannel /// Have connection read traffic data at next chance void DoReadParkedTraffic () { bDoParkedTraffic = true; } -// // shall data of this channel be subject to LTFlightData::DataSmoothing? -// bool DoDataSmoothing (double& gndRange, double& airbRange) const override -// { gndRange = RT_SMOOTH_GROUND; airbRange = RT_SMOOTH_AIRBORNE; return true; } + // // shall data of this channel be subject to LTFlightData::DataSmoothing? + // bool DoDataSmoothing (double& gndRange, double& airbRange) const override + // { gndRange = RT_SMOOTH_GROUND; airbRange = RT_SMOOTH_AIRBORNE; return true; } // shall data of this channel be subject to hovering flight detection? bool DoHoverDetection () const override { return true; } - + // Status std::string GetStatusText () const override; ///< return a human-readable status bool isHistoric () const { return curr.tOff > 0; } ///< serving historic data? @@ -382,7 +389,8 @@ class RealTrafficConnection : public LTFlightDataChannel /// Which request do we need next and when can we send it? std::chrono::time_point SetRequType (const positionTy& pos); public: - bool IsFirstTrafficRequ () const { return lTotalFlights < 0; } ///< Have not received any traffic data before? + int GetNumTrafficBuffers () const ///< How many buffers of buffered traffic would we request? + { return std::min(10, dataRefs.GetFdBufPeriod() / RT_BUFFER_PERIOD); } std::string GetURL (const positionTy&) override; ///< in direct mode return URL and set void ComputeBody (const positionTy& pos) override; ///< in direct mode puts together the POST request with the position data etc. bool ProcessFetchedData () override; ///< in direct mode process the received data @@ -415,17 +423,20 @@ class RealTrafficConnection : public LTFlightDataChannel void SendXPSimTime(bool bForce); ///< Send XP's current simulated time to RealTraffic, adapted to "today or earlier", every once in a while, or if `bForce` void SendPos (const positionTy& pos, double speed_m); ///< Send position/speed info for own ship to RealTraffic void SendUsersPlanePos(); ///< Send user's plane's position/speed to RealTraffic - void RequestBufferTraffic(); ///< Send request for initial traffic for buffering + void RequestBufferTraffic(const positionTy& pos, + double radius_m); ///< Send request for initial traffic for buffering // MARK: Data Processing // Process received datagrams bool ProcessRecvedTrafficData (const char* traffic); - bool ProcessRTTFC (LTFlightData::FDKeyTy& fdKey, const std::vector& tfc); ///< Process a RTTFC type message - bool ProcessAITFC (LTFlightData::FDKeyTy& fdKey, const std::vector& tfc); ///< Process a AITFC or XTRAFFICPSX type message + /// Process a RTTFC type message + bool ProcessRTTFC (LTFlightData::FDKeyTy& fdKey, const std::vector& tfc, int nBuffer); + ///< Process a AITFC or XTRAFFICPSX type message + bool ProcessAITFC (LTFlightData::FDKeyTy& fdKey, const std::vector& tfc, int nBuffer); bool ProcessRecvedWeatherData (const char* weather); ///< Process UDP weather JSON from RT Application /// Determine timestamp adjustment necessary in case of historic data - void AdjustTimestamp (double& ts); + void AdjustTimestamp (double& ts, int nBuffer); /// Return a string describing the current timestamp adjustment std::string GetAdjustTSText () const; diff --git a/Src/LTOpenGlider.cpp b/Src/LTOpenGlider.cpp index 41cf3b88..00aeddf2 100644 --- a/Src/LTOpenGlider.cpp +++ b/Src/LTOpenGlider.cpp @@ -241,10 +241,10 @@ std::string OpenGliderConnection::GetURL (const positionTy& pos) char url[128] = ""; snprintf(url, sizeof(url), OPGLIDER_URL, - box.nw.lat(), // lamax - box.se.lat(), // lamin - box.se.lon(), // lomax - box.nw.lon()); // lomin + box.top(), // lamax + box.bottom(), // lamin + box.right(), // lomax + box.left()); // lomin return std::string(url); } diff --git a/Src/LTOpenSky.cpp b/Src/LTOpenSky.cpp index 9045db81..63e752fa 100644 --- a/Src/LTOpenSky.cpp +++ b/Src/LTOpenSky.cpp @@ -221,10 +221,10 @@ std::string OpenSkyConnection::GetURL (const positionTy& pos) char url[128] = ""; snprintf(url, sizeof(url), OPSKY_URL_ALL, - box.se.lat(), // lamin - box.nw.lon(), // lomin - box.nw.lat(), // lamax - box.se.lon() ); // lomax + box.bottom(), // lamin + box.left(), // lomin + box.top(), // lamax + box.right() ); // lomax return std::string(url); } diff --git a/Src/LTRealTraffic.cpp b/Src/LTRealTraffic.cpp index 8690f657..d550bd27 100644 --- a/Src/LTRealTraffic.cpp +++ b/Src/LTRealTraffic.cpp @@ -136,6 +136,8 @@ std::string RealTrafficConnection::GetStatusText () const // Add extended information specifically on RealTraffic connection status s += " | "; s += GetStatusStr(); + if (bWaitForBuffers) + s += " | Waiting for buffered traffic"; if (IsConnected() && lastReceivedTime > 0.0) { // add when the last msg was received snprintf(sIntvl,sizeof(sIntvl),MSG_RT_LAST_RCVD, @@ -165,6 +167,23 @@ void RealTrafficConnection::Main () { // Loop to facilitate a change between connection types while (shallRun()) { + + // -- Init -- + + // Clear the list of historic time stamp differences + dequeTS.clear(); + // Some more data resets to make sure we start over with the series of requests + curr.eRequType = CurrTy::RT_REQU_AUTH; + curr.sGUID.clear(); + rtWx.QNH = NAN; + rtWx.nErr = 0; + rtWx.ResetFirstTime(); + lTotalFlights = -1; + + // reset last known values + lastReceivedTime = 0.0; + lastKnownViewPos = positionTy(); + // Just distinguish between direct R/R and UDP connection switch (dataRefs.GetRTConnType()) { @@ -192,15 +211,6 @@ void RealTrafficConnection::MainDirect () { // This is a communication thread's main function, set thread's name and C locale ThreadSettings TS ("LT_RT_Direct", LC_ALL_MASK); - // Clear the list of historic time stamp differences - dequeTS.clear(); - // Some more data resets to make sure we start over with the series of requests - curr.eRequType = CurrTy::RT_REQU_AUTH; - curr.sGUID.clear(); - rtWx.QNH = NAN; - rtWx.nErr = 0; - rtWx.ResetFirstTime(); - lTotalFlights = -1; // can right away read parked traffic if parked aircraft enabled and airport data is already available, otherwise we'll be triggered later when airport data has been processed bDoParkedTraffic = dataRefs.ShallKeepParkedAircraft() && LTAptAvailable(); // If we could theoretically set weather we prepare the interpolation settings @@ -386,27 +396,48 @@ void RealTrafficConnection::ComputeBody (const positionTy&) curr.tOff); break; case CurrTy::RT_REQU_PARKED: + { + // we add 10% to the bounding box to have some data ready once the plane is close enough for display + const boundingBoxTy box (curr.pos, double(dataRefs.GetFdStdDistance_m()) * 1.10); + snprintf(s,sizeof(s), + RT_TRAFFIC_POST_PARKED, + curr.sGUID.c_str(), + box.top(), box.bottom(), + box.left(), box.right(), + curr.tOff); + break; + } case CurrTy::RT_REQU_TRAFFIC: { // we add 10% to the bounding box to have some data ready once the plane is close enough for display const boundingBoxTy box (curr.pos, double(dataRefs.GetFdStdDistance_m()) * 1.10); - // If we request traffic for the very first time, then we ask for some buffer into the past for faster plane display - if ((curr.eRequType == CurrTy::RT_REQU_TRAFFIC) && IsFirstTrafficRequ()) { + // If we request traffic for the very first time or if user jumped far, then we ask for some buffer into the past for faster plane display + if (!lastKnownViewPos.isNormal() || + lastKnownViewPos.distRoughSqr(curr.pos) > sqr(dataRefs.GetFdStdDistance_m()/2)) + { + if (lastKnownViewPos.isNormal()) { + LOG_MSG(logDEBUG, "Moved far, by %.1fnm", + lastKnownViewPos.dist(curr.pos) / M_per_NM); + } + lastKnownViewPos = curr.pos; + + // Send buffered traffic request snprintf(s,sizeof(s), RT_TRAFFIC_POST_BUFFER, curr.sGUID.c_str(), - box.nw.lat(), box.se.lat(), - box.nw.lon(), box.se.lon(), + box.top(), box.bottom(), + box.left(), box.right(), curr.tOff, - std::min(10, dataRefs.GetFdBufPeriod() / 10)); // One buffer per 10s of buffering time, max of 10 buffers + GetNumTrafficBuffers(), + RT_BUFFER_PERIOD); } - // normal un-buffered request for traffic or parked aircraft + // normal un-buffered request for traffic else { snprintf(s,sizeof(s), - curr.eRequType == CurrTy::RT_REQU_TRAFFIC ? RT_TRAFFIC_POST : RT_TRAFFIC_POST_PARKED, + RT_TRAFFIC_POST, curr.sGUID.c_str(), - box.nw.lat(), box.se.lat(), - box.nw.lon(), box.se.lon(), + box.top(), box.bottom(), + box.left(), box.right(), curr.tOff); } break; @@ -1292,18 +1323,13 @@ void RealTrafficConnection::MainUDP () // This is a communication thread's main function, set thread's name and C locale ThreadSettings TS ("LT_RT_App", LC_ALL_MASK); - rtWx.QNH = NAN; - rtWx.nErr = 0; - rtWx.ResetFirstTime(); - lTotalFlights = -1; - // Top-level exception handling try { // set startup status SetStatus(RT_STATUS_STARTING); - - // Clear the list of historic time stamp differences - dequeTS.clear(); + + // When starting up we definitely want to request buffered traffic first, so make sure we don't process live traffic + bWaitForBuffers = true; // If we could theoretically set weather we prepare the interpolation settings if (WeatherCanSet()) { @@ -1394,17 +1420,14 @@ void RealTrafficConnection::MainUDP () if (retval > 0 && FD_ISSET(udpWeatherData.getSocket(), &sRead)) { // read UDP datagram - long rcvdBytes = udpWeatherData.recv(); + const long rcvdBytes = udpWeatherData.recv(); -/* TODO: Reenable once RT App Weather works - Currently disabled because what we get more often than not is a forwarded "tiny delta" notice // received something? if (rcvdBytes > 0) { // have it processed ProcessRecvedWeatherData(udpWeatherData.getBuf()); } - */ } // handling of errors, both from select and from recv if (retval < 0 && (errno != EAGAIN && errno != EWOULDBLOCK)) { @@ -1430,10 +1453,20 @@ void RealTrafficConnection::MainUDP () SendXPSimTime(nCountPosSent <= 0); // force every once in a while SendUsersPlanePos(); - - // Request initial traffic just once - if (nCountPosSent < 0) - RequestBufferTraffic(); + + // "fast move" detection + const positionTy viewPos = dataRefs.GetViewPos(); + if (!lastKnownViewPos.isNormal() || + lastKnownViewPos.distRoughSqr(viewPos) > sqr(dataRefs.GetFdStdDistance_m()/2)) + { + if (lastKnownViewPos.isNormal()) { + LOG_MSG(logDEBUG, "Moved far, by %.1fnm", + lastKnownViewPos.dist(viewPos) / M_per_NM); + } + RequestBufferTraffic(viewPos, dataRefs.GetFdStdDistance_m()); + lastKnownViewPos = viewPos; + bWaitForBuffers = true; + } // next time in about 200ms tNextPos = std::chrono::steady_clock::now() + @@ -1773,13 +1806,22 @@ void RealTrafficConnection::SendUsersPlanePos() // Request data for buffering from the App via the TCP channel -void RealTrafficConnection::RequestBufferTraffic () +// Qs999=sendbuffer=count,time,[bottom,left,top,right]\n +void RealTrafficConnection::RequestBufferTraffic (const positionTy& pos, + double radius_m) { + const boundingBoxTy box (pos, radius_m); // send the string to request buffer traffic char s[100]; - snprintf(s, sizeof(s), "Qs999=sendbuffer=%d", - std::min(10, dataRefs.GetFdBufPeriod() / 10)); + snprintf(s, sizeof(s), "Qs999=sendbuffer=%d,%d,%.2f,%.2f,%.2f,%.2f\n", + GetNumTrafficBuffers(), + RT_BUFFER_PERIOD, + box.bottom(), box.left(), + box.top(), box.right()); SendMsg(s); + LOG_MSG(logINFO, "Requested buffered traffic %ldnm around %s", + std::lround(radius_m / M_per_NM), + std::string(pos).c_str()); } @@ -1802,10 +1844,14 @@ bool RealTrafficConnection::ProcessRecvedTrafficData (const char* traffic) // not enough fields found for any message? if (tfc.size() < RT_MIN_TFC_FIELDS) { - // RealTraffic sends an "RTPARK_EOT"/"RTTFC_EOT" message when it is done sending one round of updates, - // but we don't need it and silently ignore any kind of "_EOT" message - if (std::strstr(traffic, "_EOT")) + // RealTraffic sends various markers that we mostly don't need...we just ignore them + if (tfc.size() == 1) { + if (tfc[0] == "RTBUF_EOT") { // but when done with buffers process normal traffic again + LOG_MSG(logINFO, "RTBUF_EOT Finished receiving buffered traffic"); + bWaitForBuffers = false; + } return true; + } // Otherwise it's worth a warning because it's unexpected LOG_MSG(logWARN, ERR_RT_DISCARDED_MSG, traffic); return false; @@ -1841,25 +1887,43 @@ bool RealTrafficConnection::ProcessRecvedTrafficData (const char* traffic) // *** Process different formats **** + // buffered traffic? + int nBuf = 0; + if (std::strncmp(tfc[RT_RTTFC_REC_TYPE].c_str(), "RTBUF=", 6) == 0) { + nBuf = std::atoi(tfc[RT_RTTFC_REC_TYPE].c_str()+6) + 1; + if (bWaitForBuffers) { + LOG_MSG(logDEBUG, "RTBUF Received first buffered traffic"); + bWaitForBuffers = false; // buffered traffic has arrived, we wait no longer + } + } + else { + // live traffic...but if we are waiting for buffered then we skip it silently + if (bWaitForBuffers) + return true; + } + // There are 3 formats we are _really_ interested in: RTTFC, AITFC, and XTRAFFICPSX // Check for them and their correct number of fields - if (tfc[RT_RTTFC_REC_TYPE] == RT_TRAFFIC_RTTFC) { + if (tfc[RT_RTTFC_REC_TYPE] == RT_TRAFFIC_RTTFC || // regular traffic + (nBuf && tfc.size() >= RT_RTTFC_MIN_TFC_FIELDS)) // buffered traffic with many fields + { if (tfc.size() < RT_RTTFC_MIN_TFC_FIELDS) { LOG_MSG(logWARN, ERR_RT_DISCARDED_MSG, traffic); return false; } - return ProcessRTTFC(fdKey, tfc); + return ProcessRTTFC(fdKey, tfc, nBuf); } - else if (tfc[RT_AITFC_REC_TYPE] == RT_TRAFFIC_AITFC) { + // Buffered traffic comes with few fields and is typically processed here as AITFC format + else if (tfc[RT_AITFC_REC_TYPE] == RT_TRAFFIC_AITFC || nBuf) { if (tfc.size() < RT_AITFC_NUM_FIELDS_MIN) { LOG_MSG(logWARN, ERR_RT_DISCARDED_MSG, traffic); return false; } - return ProcessAITFC(fdKey, tfc); + return ProcessAITFC(fdKey, tfc, nBuf); } else if (tfc[RT_AITFC_REC_TYPE] == RT_TRAFFIC_XTRAFFICPSX) { if (tfc.size() < RT_XTRAFFICPSX_NUM_FIELDS) { LOG_MSG(logWARN, ERR_RT_DISCARDED_MSG, traffic); return false; } - return ProcessAITFC(fdKey, tfc); + return ProcessAITFC(fdKey, tfc, false); } else { // other format than AITFC or XTRAFFICPSX @@ -1898,11 +1962,12 @@ double firstPositive (const std::vector& tfc, /// 35008,-1,71.02, autopilot|vnav|lnav|tcas,0.0,-21.9,223,24, /// -30,0,1,170124 bool RealTrafficConnection::ProcessRTTFC (LTFlightData::FDKeyTy& fdKey, - const std::vector& tfc) + const std::vector& tfc, + int nBuffer) { // *** position time *** double posTime = std::stod(tfc[RT_RTTFC_TIMESTAMP]); - AdjustTimestamp(posTime); + AdjustTimestamp(posTime, nBuffer); // *** Process received data *** @@ -2027,7 +2092,8 @@ bool RealTrafficConnection::ProcessRTTFC (LTFlightData::FDKeyTy& fdKey, /// XTRAFFICPSX,531917901,40.9145,-73.7625,1975,64,1,218,140,DAL9936(BCS1) /// bool RealTrafficConnection::ProcessAITFC (LTFlightData::FDKeyTy& fdKey, - const std::vector& tfc) + const std::vector& tfc, + int nBuffer) { // *** position time *** // There are 2 possibilities: @@ -2042,7 +2108,7 @@ bool RealTrafficConnection::ProcessAITFC (LTFlightData::FDKeyTy& fdKey, { // use that delivered timestamp and (potentially) adjust it if it is in the past posTime = std::stod(tfc[RT_AITFC_TIMESTAMP]); - AdjustTimestamp(posTime); + AdjustTimestamp(posTime, nBuffer); } else { @@ -2185,13 +2251,21 @@ bool RealTrafficConnection::ProcessAITFC (LTFlightData::FDKeyTy& fdKey, // Determine timestamp adjustment necessary in case of historic data -void RealTrafficConnection::AdjustTimestamp (double& ts) +void RealTrafficConnection::AdjustTimestamp (double& ts, int nBuffer) { + // If this is a buffered request then it is a timestamp further in the past, adjust for that + if (nBuffer) { + // buffer number starts with 1 for the oldest buffer all the way up to the GetNumTrafficBuffers + // for the last buffer before current time + nBuffer = (GetNumTrafficBuffers() - nBuffer + 1) * RT_BUFFER_PERIOD; + } + // the assumed 'now' is simTime + buffering period + // minus the buffering time if we are buffering const double now = dataRefs.GetSimTime() + dataRefs.GetFdBufPeriod(); // *** Keep the rolling list of timestamps diffs, max length: 11 *** - dequeTS.push_back(now - ts); + dequeTS.push_back(now - (ts + double(nBuffer))); while (dequeTS.size() > 11) dequeTS.pop_front(); diff --git a/Src/LTWeather.cpp b/Src/LTWeather.cpp index 907cf573..e34c95c9 100644 --- a/Src/LTWeather.cpp +++ b/Src/LTWeather.cpp @@ -1318,11 +1318,9 @@ bool WeatherFetch (float _lat, float _lon, float _radius_nm) // put together the URL, with a bounding box with _radius_nm in each direction const boundingBoxTy box (positionTy(_lat, _lon), _radius_nm * M_per_NM * 2.0); - const positionTy minPos = box.sw(); - const positionTy maxPos = box.ne(); snprintf(url, sizeof(url), WEATHER_URL, - minPos.lat(), minPos.lon(), - maxPos.lat(), maxPos.lon()); + box.bottom(), box.left(), + box.top(), box.right()); // prepare the handle with the right options readBuf.reserve(CURL_MAX_WRITE_SIZE); diff --git a/docs/readme.html b/docs/readme.html index 6bcf2b81..fcbae844 100755 --- a/docs/readme.html +++ b/docs/readme.html @@ -148,6 +148,12 @@

v4.3.6 Beta

Change log:

    +
  • RealTraffic: +
      +
    • App: Request buffered traffic for faster filling of the sky upon starting / switching airports.
    • +
    • App: Supports now processing RealTraffic's weather.
    • +
    +
  • Sends local cartesian coordinates and transponder mode to LTAPI.
From 5d8ce9d9005c95fdef1bf2625b2c6e360b46529e Mon Sep 17 00:00:00 2001 From: TwinFan Date: Mon, 6 Apr 2026 18:08:48 +0200 Subject: [PATCH 04/11] feat/RT: Link to aircraft tracking --- Include/LTRealTraffic.h | 2 ++ Src/LTRealTraffic.cpp | 16 +++++++++++++++- docs/readme.html | 3 +++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Include/LTRealTraffic.h b/Include/LTRealTraffic.h index 1101fc88..c9a9c06e 100644 --- a/Include/LTRealTraffic.h +++ b/Include/LTRealTraffic.h @@ -39,6 +39,7 @@ #define RT_CHECK_NAME "RealTraffic Web Site" #define RT_CHECK_URL "https://www.flyrealtraffic.com/" #define RT_CHECK_POPUP "Open RealTraffic's web site" +#define RT_SLUG "https://www.flyrealtraffic.com/livemap/?hex=%lx" #define REALTRAFFIC_NAME "RealTraffic" @@ -434,6 +435,7 @@ class RealTrafficConnection : public LTFlightDataChannel ///< Process a AITFC or XTRAFFICPSX type message bool ProcessAITFC (LTFlightData::FDKeyTy& fdKey, const std::vector& tfc, int nBuffer); bool ProcessRecvedWeatherData (const char* weather); ///< Process UDP weather JSON from RT Application + std::string GetSlug (unsigned long hex) const; ///< returns a slug string for a given hex id /// Determine timestamp adjustment necessary in case of historic data void AdjustTimestamp (double& ts, int nBuffer); diff --git a/Src/LTRealTraffic.cpp b/Src/LTRealTraffic.cpp index d550bd27..3448295e 100644 --- a/Src/LTRealTraffic.cpp +++ b/Src/LTRealTraffic.cpp @@ -758,6 +758,7 @@ bool RealTrafficConnection::ProcessTrafficBuffer (const JSON_Object* pBuf) std::string s = jag_s(pJAc, RT_DRCT_Category); stat.catDescr = GetADSBEmitterCat(s); + stat.slug = GetSlug(fdKey.num); // RealTraffic often sends ASW20 when it should be AS20, a glider if (stat.acTypeIcao == "ASW20") stat.acTypeIcao = "AS20"; @@ -950,7 +951,7 @@ bool RealTrafficConnection::ProcessParkedAcBuffer (const JSON_Object* pData) stat.acTypeIcao = std::move(dat.acType); stat.call = std::move(dat.call); stat.reg = std::move(dat.reg); - + // RealTraffic often sends ASW20 when it should be AS20, a glider if (stat.acTypeIcao == "ASW20") stat.acTypeIcao = "AS20"; @@ -2023,6 +2024,7 @@ bool RealTrafficConnection::ProcessRTTFC (LTFlightData::FDKeyTy& fdKey, stat.call = tfc[RT_RTTFC_CS_ICAO]; stat.reg = tfc[RT_RTTFC_AC_TAILNO]; stat.setOrigDest(tfc[RT_RTTFC_FROM_IATA], tfc[RT_RTTFC_TO_IATA]); + stat.slug = GetSlug(fdKey.num); const std::string& sCat = tfc[RT_RTTFC_CATEGORY]; stat.catDescr = GetADSBEmitterCat(sCat); @@ -2180,6 +2182,8 @@ bool RealTrafficConnection::ProcessAITFC (LTFlightData::FDKeyTy& fdKey, stat.reg = STATIC_OBJECT_TYPE; stat.catDescr = GetADSBEmitterCat("C3"); } + + stat.slug = GetSlug(fdKey.num); // -- dynamic data -- LTFlightData::FDDynamicData dyn; @@ -2250,6 +2254,16 @@ bool RealTrafficConnection::ProcessAITFC (LTFlightData::FDKeyTy& fdKey, } +// returns a slug string for a given hex id +std::string RealTrafficConnection::GetSlug (unsigned long hex) const +{ + char buf[100]; + snprintf(buf, sizeof(buf), RT_SLUG, hex); + return std::string(buf); +} + + + // Determine timestamp adjustment necessary in case of historic data void RealTrafficConnection::AdjustTimestamp (double& ts, int nBuffer) { diff --git a/docs/readme.html b/docs/readme.html index fcbae844..4a250d30 100755 --- a/docs/readme.html +++ b/docs/readme.html @@ -150,6 +150,9 @@

v4.3.6 Beta

  • RealTraffic:
      +
    • Link to RealTraffic's aircraft tracking from + Aircraft List / + Info Window.
    • App: Request buffered traffic for faster filling of the sky upon starting / switching airports.
    • App: Supports now processing RealTraffic's weather.
    From d5581b53af10414f4645ca1ee55dbc943f21e4a6 Mon Sep 17 00:00:00 2001 From: TwinFan Date: Tue, 7 Apr 2026 20:11:08 +0200 Subject: [PATCH 05/11] chore/OpnSky: Switched off Master File channel, closes #308 --- Src/DataRefs.cpp | 27 ++++++++++++++++----------- Src/SettingsUI.cpp | 1 - docs/readme.html | 5 +++++ 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/Src/DataRefs.cpp b/Src/DataRefs.cpp index bf3bce72..26f695e2 100644 --- a/Src/DataRefs.cpp +++ b/Src/DataRefs.cpp @@ -784,7 +784,6 @@ ILWrect (0, 400, 965, 0) bChannel[DR_CHANNEL_ADSB_FI_ONLINE - DR_CHANNEL_FIRST] = true; bChannel[DR_CHANNEL_OPEN_SKY_ONLINE - DR_CHANNEL_FIRST] = true; bChannel[DR_CHANNEL_OPEN_SKY_AC_MASTERDATA - DR_CHANNEL_FIRST] = true; - bChannel[DR_CHANNEL_OPEN_SKY_AC_MASTERFILE - DR_CHANNEL_FIRST] = true; bChannel[DR_CHANNEL_OPEN_GLIDER_NET - DR_CHANNEL_FIRST] = true; bChannel[DR_CHANNEL_SYNTHETIC - DR_CHANNEL_FIRST] = true; @@ -2028,7 +2027,7 @@ bool DataRefs::LoadConfigFile() // which conversion to do with the (older) version of the config file? unsigned long cfgFileVer = 0; - enum cfgFileConvE { CFG_NO_CONV=0, CFG_V3, CFG_V31, CFG_V331, CFG_V342, CFG_V350, CFG_V420 } conv = CFG_NO_CONV; + enum cfgFileConvE { CFG_NO_CONV=0, CFG_V3, CFG_V31, CFG_V331, CFG_V342, CFG_V350, CFG_V420, CFG_V436 } conv = CFG_NO_CONV; // open a config file std::string sFileName (LTCalcFullPath(PATH_CONFIG_FILE)); @@ -2088,18 +2087,20 @@ bool DataRefs::LoadConfigFile() cfgFileVer += std::stoul(m[3]); // any conversions required? - if (cfgFileVer < 30100) // < 3.1.0 - conv = CFG_V31; - if (cfgFileVer < 30301) // < 3.3.1 - conv = CFG_V331; - if (cfgFileVer < 30402) // < 3.4.2: Reset Force FMOD instance = 0, set network timeout to 5s - conv = CFG_V342; + if (cfgFileVer < 40306) // < 4.3.6: Switch off OpenSky Master File + conv = CFG_V436; + if (cfgFileVer < 40200) // < 4.2.0: Clear ADSBEx API key (switch to other service) + conv = CFG_V420; if (cfgFileVer < 30500) { // < 3.5.0 rtConnType = RT_CONN_APP; // Switch RealTraffic default to App as it was before conv = CFG_V350; } - if (cfgFileVer < 40200) // < 4.2.0: Clear ADSBEx API key (switch to other service) - conv = CFG_V420; + if (cfgFileVer < 30402) // < 3.4.2: Reset Force FMOD instance = 0, set network timeout to 5s + conv = CFG_V342; + if (cfgFileVer < 30301) // < 3.3.1 + conv = CFG_V331; + if (cfgFileVer < 30100) // < 3.1.0 + conv = CFG_V31; } } @@ -2176,6 +2177,11 @@ bool DataRefs::LoadConfigFile() // Switching to v4.2 we need to disable ADSBEx until a new API key is configured if (*i == DATA_REFS_LT[DR_CHANNEL_ADSB_EXCHANGE_ONLINE]) sVal = "0"; + [[fallthrough]]; + case CFG_V436: + // Switching off OpenSky Master File + if (*i == DATA_REFS_LT[DR_CHANNEL_OPEN_SKY_AC_MASTERFILE]) + sVal = "0"; break; } @@ -2471,7 +2477,6 @@ void DataRefs::SetChannelEnabled (dataRefsLT ch, bool bEnable) // If OpenSky Tracking is enabled then make sure OpenSky Master is also if (IsChannelEnabled(DR_CHANNEL_OPEN_SKY_ONLINE)) { bChannel[DR_CHANNEL_OPEN_SKY_AC_MASTERDATA - DR_CHANNEL_FIRST] = true; - bChannel[DR_CHANNEL_OPEN_SKY_AC_MASTERFILE - DR_CHANNEL_FIRST] = true; } // if a channel got disabled check if any tracking data channel is left diff --git a/Src/SettingsUI.cpp b/Src/SettingsUI.cpp index 680133a7..39b023f4 100644 --- a/Src/SettingsUI.cpp +++ b/Src/SettingsUI.cpp @@ -440,7 +440,6 @@ void LTSettingsUI::buildInterface() // we also make sure that OpenSky Master data is enabled if (!bWasADSBHubEnabled && dataRefs.IsChannelEnabled(DR_CHANNEL_ADSB_HUB)) { dataRefs.SetChannelEnabled(DR_CHANNEL_OPEN_SKY_AC_MASTERDATA, true); - dataRefs.SetChannelEnabled(DR_CHANNEL_OPEN_SKY_AC_MASTERFILE, true); } // ADSBHub's connection status details diff --git a/docs/readme.html b/docs/readme.html index 4a250d30..0d556a59 100755 --- a/docs/readme.html +++ b/docs/readme.html @@ -157,6 +157,11 @@

    v4.3.6 Beta

  • App: Supports now processing RealTraffic's weather.
+
  • + OpenSky: Switched off the "Masterdata File" channel to avoid + errors in the log as OpenSky is not currently maintaining that + data source. It could be re-activated manually. +
  • Sends local cartesian coordinates and transponder mode to LTAPI.
  • From c202465659056ab53ea930b146aade02433d6c69 Mon Sep 17 00:00:00 2001 From: TwinFan Date: Thu, 9 Apr 2026 20:45:47 +0200 Subject: [PATCH 06/11] chore: Enhance resiliance in LTApt even stricter locking, with timeout; element access by ".at" rather than [] --- Src/LTApt.cpp | 134 +++++++++++++++++++++++++++++++------------------- 1 file changed, 83 insertions(+), 51 deletions(-) diff --git a/Src/LTApt.cpp b/Src/LTApt.cpp index 67905194..226300cc 100644 --- a/Src/LTApt.cpp +++ b/Src/LTApt.cpp @@ -10,7 +10,7 @@ /// @see More information on reading from `apt.dat` is on [a separate page](@ref apt_dat). /// @see `apt.dat` file format specification is [here](https://developer.x-plane.com/article/airport-data-apt-dat-file-format-specification/) /// @author Birger Hoppe -/// @copyright (c) 2020 Birger Hoppe +/// @copyright (c) 2026 Birger Hoppe /// @copyright 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 @@ -395,9 +395,9 @@ class Apt { /// Is given node connected to a rwy? bool IsConnectedToRwy (size_t idxN) const { - const TaxiNode& n = vecTaxiNodes[idxN]; + const TaxiNode& n = vecTaxiNodes.at(idxN); for (size_t idxE: n.vecEdges) - if (vecTaxiEdges[idxE].GetType() == TaxiEdge::RUN_WAY) + if (vecTaxiEdges.at(idxE).GetType() == TaxiEdge::RUN_WAY) return true; return false; } @@ -407,7 +407,7 @@ class Apt { { const TaxiNode& a = vecTaxiNodes.at(idxA); for (size_t idxE: a.vecEdges) { - const TaxiEdge& e = vecTaxiEdges[idxE]; + const TaxiEdge& e = vecTaxiEdges.at(idxE); if (e.otherNode(idxA) == idxB) return idxE; } @@ -533,7 +533,7 @@ class Apt { if (insNode == e.startNode() || insNode == e.endNode()) return; size_t joinOrigB = e.endNode(); - TaxiNode& origB = vecTaxiNodes[joinOrigB]; + TaxiNode& origB = vecTaxiNodes.at(joinOrigB); // 2. Short-cut existing node at new joint const TaxiNode& a = e.GetA(*this); @@ -589,7 +589,7 @@ class Apt { // The edge to work on const size_t idxE = oldN.vecEdges.back(); oldN.vecEdges.pop_back(); - TaxiEdge& e = vecTaxiEdges[idxE]; + TaxiEdge& e = vecTaxiEdges.at(idxE); // Replace the node in the edge and recalculate the edge e.ReplaceNode(oldIdxN, newIdxN); @@ -618,7 +618,7 @@ class Apt { vecTaxiEdgesIdxHead.clear(); vecTaxiEdgesIdxHead.reserve(vecTaxiEdges.size()); for (size_t eIdx = 0; eIdx < vecTaxiEdges.size(); ++eIdx) - if (vecTaxiEdges[eIdx].isValid()) + if (vecTaxiEdges.at(eIdx).isValid()) vecTaxiEdgesIdxHead.push_back(eIdx); // Now sort the index array by the angle of the linked edge @@ -676,12 +676,12 @@ class Apt { rngPair.first, [&](const size_t& idx, double _angle) { return vecTaxiEdges[idx].angle < _angle; }); - iter != vecTaxiEdgesIdxHead.cend() && vecTaxiEdges[*iter].angle <= rngPair.second; + iter != vecTaxiEdgesIdxHead.cend() && vecTaxiEdges.at(*iter).angle <= rngPair.second; ++iter) { // Check for type limitation, then add to `vec` if (_restrictType == TaxiEdge::UNKNOWN_WAY || - _restrictType == vecTaxiEdges[*iter].GetType()) + _restrictType == vecTaxiEdges.at(*iter).GetType()) lst.push_back(*iter); } } @@ -750,7 +750,7 @@ class Apt { continue; // Skip edge if invalid - const TaxiEdge& e = vecTaxiEdges[eIdx]; + const TaxiEdge& e = vecTaxiEdges.at(eIdx); if (!e.isValid()) continue; @@ -979,7 +979,7 @@ class Apt { for (size_t idxN: vecPathEnds) { // The node we deal with - TaxiNode& n = vecTaxiNodes[idxN]; + TaxiNode& n = vecTaxiNodes.at(idxN); // The exclusion edge list: With these edges we don't want to join: // 1. All our direct edges @@ -990,7 +990,7 @@ class Apt { vecEdgeExclusions.erase(lastEExcl,vecEdgeExclusions.end()); // Try finding _another_ edge this one can connect to - positionTy pos(n.lat, n.lon, 0.0, NAN, vecTaxiEdges[n.vecEdges.front()].GetAngleFrom(idxN)); + positionTy pos(n.lat, n.lon, 0.0, NAN, vecTaxiEdges.at(n.vecEdges.front()).GetAngleFrom(idxN)); const TaxiEdge* pJoinE = FindClosestEdge(pos, pos, // larger distance allowed if I'm a single node, smaller only if I already have connections n.vecEdges.size() <= 1 ? APT_JOIN_MAX_DIST_M : APT_MAX_SIMILAR_NODE_DIST_M, @@ -1025,9 +1025,9 @@ class Apt { // node now with the already merged node, so that both taxiways // join with the rwy in one single joint node. size_t nearIdxN = ULONG_MAX; - if (n.IsCloseTo(vecTaxiNodes[pJoinE->startNode()], APT_MAX_SIMILAR_NODE_DIST_M)) + if (n.IsCloseTo(vecTaxiNodes.at(pJoinE->startNode()), APT_MAX_SIMILAR_NODE_DIST_M)) nearIdxN = pJoinE->startNode(); - else if (n.IsCloseTo(vecTaxiNodes[pJoinE->endNode()], APT_MAX_SIMILAR_NODE_DIST_M)) + else if (n.IsCloseTo(vecTaxiNodes.at(pJoinE->endNode()), APT_MAX_SIMILAR_NODE_DIST_M)) nearIdxN = pJoinE->endNode(); // One of the nodes is indeed nearby? @@ -1097,17 +1097,17 @@ class Apt { // is not simple either. I expect vecVisit to stay short // due to cut-off at _maxLen, so I've decided this way:) vecIdxTy::iterator shortestIter = vecVisit.begin(); - double shortestDist = vecTaxiNodes[*shortestIter].pathLen; + double shortestDist = vecTaxiNodes.at(*shortestIter).pathLen; for (vecIdxTy::iterator i = std::next(shortestIter); i != vecVisit.end(); ++i) { - if (vecTaxiNodes[*i].pathLen < shortestDist) { + if (vecTaxiNodes.at(*i).pathLen < shortestDist) { shortestIter = i; - shortestDist = vecTaxiNodes[*i].pathLen; + shortestDist = vecTaxiNodes.at(*i).pathLen; } } const size_t shortestNIdx = *shortestIter; - TaxiNode& shortestN = vecTaxiNodes[shortestNIdx]; + TaxiNode& shortestN = vecTaxiNodes.at(shortestNIdx); // To avoid too sharp corners we need to know the angle by which we reach this shortest node const size_t idxEdgeToShortestN = @@ -1116,7 +1116,7 @@ class Apt { // start heading for when leaving first node, otherwise heading between previous and current node const double angleToShortestN = idxEdgeToShortestN == EDGE_UNKNOWN ? _headingAtStart : - vecTaxiEdges[idxEdgeToShortestN].GetAngleFrom(shortestN.prevIdx); + vecTaxiEdges.at(idxEdgeToShortestN).GetAngleFrom(shortestN.prevIdx); // This one is now already counted as "visited" so no more updates to its pathLen! shortestN.bVisited = true; @@ -1125,11 +1125,11 @@ class Apt { // Update all connected nodes with best possible distance for (size_t eIdx: shortestN.vecEdges) { - const TaxiEdge& e = vecTaxiEdges[eIdx]; + const TaxiEdge& e = vecTaxiEdges.at(eIdx); if (!e.isValid()) continue; size_t updNIdx = e.otherNode(shortestNIdx); - TaxiNode& updN = vecTaxiNodes[updNIdx]; + TaxiNode& updN = vecTaxiNodes.at(updNIdx); // if aleady visited then no need to re-assess if (updN.bVisited) @@ -1177,7 +1177,7 @@ class Apt { vecVisit.clear(); for (size_t nIdx = _endN; nIdx < ULONG_MAX-1; // until nIdx becomes invalid - nIdx = vecTaxiNodes[nIdx].prevIdx) // move on to _previous_ node on shortest path + nIdx = vecTaxiNodes.at(nIdx).prevIdx) // move on to _previous_ node on shortest path { LOG_ASSERT(nIdx < vecTaxiNodes.size()); vecVisit.push_back(nIdx); @@ -1344,10 +1344,10 @@ class Apt { // previous edge's relevant node bool bSkipStart = false; - const TaxiEdge& prevE = vecTaxiEdges[pPrevPos->edgeIdx]; + const TaxiEdge& prevE = vecTaxiEdges.at(pPrevPos->edgeIdx); size_t prevErelN = prevE.endByHeading(pPrevPos->heading()); { - const TaxiNode& othN = vecTaxiNodes[prevE.otherNode(prevErelN)]; + const TaxiNode& othN = vecTaxiNodes.at(prevE.otherNode(prevErelN)); if (DistLatLonSqr(othN.lat, othN.lon, pPrevPos->lat(), pPrevPos->lon()) <= sqr(2*APT_MAX_SIMILAR_NODE_DIST_M)) { prevErelN = prevE.otherNode(prevErelN); bSkipStart = true; // this node is now _before_ prevPos, don't add that to the deque! @@ -1356,7 +1356,7 @@ class Apt { { // Sanity check: if the distance to reaching the first node // is more than we shall travel in total we're making a mistake - const TaxiNode& prevErelNode = vecTaxiNodes[prevErelN]; + const TaxiNode& prevErelNode = vecTaxiNodes.at(prevErelN); if (DistLatLon(pPrevPos->lat(), pPrevPos->lon(), prevErelNode.lat, prevErelNode.lon) > distPrevPosPos) // Then it is simpler to just go straight without any taxiway path @@ -1368,7 +1368,7 @@ class Apt { bool bSkipEnd = false; size_t currEstartN = pEdge->startByHeading(pos.heading()); { - const TaxiNode& othN = vecTaxiNodes[pEdge->otherNode(currEstartN)]; + const TaxiNode& othN = vecTaxiNodes.at(pEdge->otherNode(currEstartN)); if (DistLatLonSqr(othN.lat, othN.lon, pos.lat(), pos.lon()) <= sqr(2*APT_MAX_SIMILAR_NODE_DIST_M)) { currEstartN = pEdge->otherNode(currEstartN); bSkipEnd = true; // this node is now _beyond_ pos, don't add that to the deque! @@ -1377,7 +1377,7 @@ class Apt { { // Sanity check: if the distance to reaching the last node // is more than we shall travel in total we're making a mistake - const TaxiNode& currErelNode = vecTaxiNodes[currEstartN]; + const TaxiNode& currErelNode = vecTaxiNodes.at(currEstartN); if (DistLatLon(pos.lat(), pos.lon(), currErelNode.lat, currErelNode.lon) > distPrevPosPos) // Then it is simpler to just go straight without any taxiway path @@ -1425,17 +1425,17 @@ class Apt { // if we removed nodes from the start of the path then we need to adjust path len in the nodes now: // The start node has to have pathLen == 0.0 - if (vecPath.size() >= 2 && vecTaxiNodes[vecPath.back()].pathLen > 0.0) { - const double adjust = vecTaxiNodes[vecPath.back()].pathLen; + if (vecPath.size() >= 2 && vecTaxiNodes.at(vecPath.back()).pathLen > 0.0) { + const double adjust = vecTaxiNodes.at(vecPath.back()).pathLen; for (size_t nIdx: vecPath) - vecTaxiNodes[nIdx].pathLen -= adjust; + vecTaxiNodes.at(nIdx).pathLen -= adjust; } // Some path left? if (vecPath.size() >= 2) { - const TaxiNode& endN = vecTaxiNodes[vecPath.front()]; // end of path - const TaxiNode& startN = vecTaxiNodes[vecPath.back()]; // start of path + const TaxiNode& endN = vecTaxiNodes.at(vecPath.front()); // end of path + const TaxiNode& startN = vecTaxiNodes.at(vecPath.back()); // start of path // distance from prevPos to path's start const double distToStart = DistLatLon(pPrevPos->lat(), pPrevPos->lon(), startN.lat, startN.lon); @@ -1492,7 +1492,7 @@ class Apt { const bool bLastNode = std::next(iter) == vecPath.crend(); // create a proper position and insert it into fd's posDeque - const TaxiNode& n = vecTaxiNodes[*iter]; + const TaxiNode& n = vecTaxiNodes.at(*iter); positionTy insPos (n.lat, n.lon, NAN, // lat, lon, altitude startTS + timeStartToPos * n.pathLen / distStartToPos, NAN, // heading will be populated later @@ -1615,7 +1615,7 @@ class Apt { // Validate vecTaxiNodes and vecTaxiEdges for (size_t idxN = 0; idxN < vecTaxiNodes.size(); ++idxN) { - const TaxiNode& n = vecTaxiNodes[idxN]; + const TaxiNode& n = vecTaxiNodes.at(idxN); for (size_t idxE: n.vecEdges) { const TaxiEdge& e = vecTaxiEdges[idxE]; @@ -1895,7 +1895,7 @@ typedef std::map mapAptTy; static mapAptTy gmapApt; /// Lock to access global map of airports -static std::mutex mtxGMapApt; +static std::recursive_timed_mutex mtxGMapApt; // Temporary storage while reading an airport from apt.dat vecTaxiNodesTy Apt::vecRwyNodes; @@ -1954,7 +1954,7 @@ void Apt::AddApt (Apt&& apt) // Access to the list of airports is guarded by a lock const std::string key = apt.GetId(); // make a copy of the key, as `apt` gets moved soon: { - std::lock_guard lock(mtxGMapApt); + std::lock_guard lock(mtxGMapApt); gmapApt.emplace(key, std::move(apt)); } @@ -2358,7 +2358,7 @@ static void ReadOneAptFile (std::ifstream& fIn, const boundingBoxTy& box) void PurgeApt (const boundingBoxTy& _box) { // Access is guarded by a lock - std::lock_guard lock(mtxGMapApt); + std::lock_guard lock(mtxGMapApt); // loop all airports and remove those, whose center point is outside the box mapAptTy::iterator iter = gmapApt.begin(); @@ -2494,9 +2494,19 @@ void AsyncReadApt (positionTy ctr, double radius) /// Find airport, which contains passed-in position, can be `nullptr` Apt* LTAptFind (const positionTy& pos) { - for (auto& pair: gmapApt) - if (pair.second.Contains(pos)) - return &pair.second; + // Access to the list of airports is guarded by a lock + std::unique_lock lock(mtxGMapApt, + dataRefs.IsXPThread() ? + std::chrono::milliseconds(100) : + std::chrono::milliseconds(500)); + if (lock) { + for (auto& pair: gmapApt) + if (pair.second.Contains(pos)) + return &pair.second; + } + else { + LOG_MSG(logDEBUG, "Locking mtxGMapApt failed"); + } return nullptr; } @@ -2521,7 +2531,7 @@ static bool bAptAvailable = false; void LTAptUpdateRwyAltitudes () { // access is guarded by a lock - std::lock_guard lock(mtxGMapApt); + std::lock_guard lock(mtxGMapApt); // loop all airports and their runways for (mapAptTy::value_type& p: gmapApt) @@ -2610,7 +2620,14 @@ positionTy LTAptFindRwy (const LTAircraft::FlightModel& _mdl, // --- Iterate the airports --- // Access to the list of airports is guarded by a lock - std::lock_guard lock(mtxGMapApt); + std::unique_lock lock(mtxGMapApt, + dataRefs.IsXPThread() ? + std::chrono::milliseconds(100) : + std::chrono::milliseconds(500)); + if (!lock) { + LOG_MSG(logDEBUG, "Locking mtxGMapApt failed"); + return positionTy(); + } // loop over airports for (mapAptTy::const_iterator iterApt = gmapApt.cbegin(); @@ -2707,7 +2724,14 @@ positionTy LTAptFindStartupLoc (const positionTy& pos, double* outDist) { // Access to the list of airports is guarded by a lock - std::lock_guard lock(mtxGMapApt); + std::unique_lock lock(mtxGMapApt, + dataRefs.IsXPThread() ? + std::chrono::milliseconds(100) : + std::chrono::milliseconds(500)); + if (!lock) { + LOG_MSG(logDEBUG, "Locking mtxGMapApt failed"); + return positionTy(); + } // Which airport are we looking at? Apt* pApt = LTAptFind(pos); @@ -2741,15 +2765,23 @@ bool LTAptSnap (LTFlightData& fd, dequePositionTy::iterator& posIter, return false; // Access to the list of airports is guarded by a lock - std::lock_guard lock(mtxGMapApt); - - // Which airport are we looking at? - Apt* pApt = LTAptFind(*posIter); - if (!pApt) // not a position in any airport's bounding box + std::unique_lock lock(mtxGMapApt, + dataRefs.IsXPThread() ? + std::chrono::milliseconds(100) : + std::chrono::milliseconds(500)); + if (lock) { + // Which airport are we looking at? + Apt* pApt = LTAptFind(*posIter); + if (!pApt) // not a position in any airport's bounding box + return false; + + // Let's snap! + return pApt->SnapToTaxiway(fd, posIter, bInsertTaxiTurns); + } + else { + LOG_MSG(logDEBUG, "Locking mtxGMapApt failed"); return false; - - // Let's snap! - return pApt->SnapToTaxiway(fd, posIter, bInsertTaxiTurns); + } } From 735b2d5d5de4a269ec1f59eac3c45d4b57bb7d27 Mon Sep 17 00:00:00 2001 From: TwinFan Date: Thu, 9 Apr 2026 20:46:09 +0200 Subject: [PATCH 07/11] chore: Ensure logging around Aircraft::SetInvalid --- Lib/XPMP2 | 2 +- Src/LTAircraft.cpp | 4 +++- Src/LTFlightData.cpp | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Lib/XPMP2 b/Lib/XPMP2 index 2c642d45..1f4fc9ce 160000 --- a/Lib/XPMP2 +++ b/Lib/XPMP2 @@ -1 +1 @@ -Subproject commit 2c642d452fa09509131f29a11bfa0ba936218039 +Subproject commit 1f4fc9ce0536b97e55ccf390ce09105ecd38c3ef diff --git a/Src/LTAircraft.cpp b/Src/LTAircraft.cpp index 94dd9b70..80d2784c 100644 --- a/Src/LTAircraft.cpp +++ b/Src/LTAircraft.cpp @@ -2992,7 +2992,9 @@ void LTAircraft::UpdatePosition (float, int cycle) } catch (const std::exception& e) { LOG_MSG(logERR, ERR_TOP_LEVEL_EXCEPTION, e.what()); - } catch (...) {} + } catch (...) { + LOG_MSG(logERR, ERR_TOP_LEVEL_EXCEPTION, "(unknown)"); + } // for any kind of exception: don't use this object any more! SetInvalid(); diff --git a/Src/LTFlightData.cpp b/Src/LTFlightData.cpp index 32dc099c..0e346ab4 100644 --- a/Src/LTFlightData.cpp +++ b/Src/LTFlightData.cpp @@ -1400,6 +1400,7 @@ void LTFlightData::CalcNextPosMain () LOG_MSG(logERR, ERR_TOP_LEVEL_EXCEPTION " - on aircraft %s", e.what(), pair.first.c_str()); fd.SetInvalid(); } catch (...) { + LOG_MSG(logERR, ERR_TOP_LEVEL_EXCEPTION " - on aircraft %s", "(unknown)", pair.first.c_str()); fd.SetInvalid(); } @@ -1854,6 +1855,7 @@ void LTFlightData::AppendAllNewPos() LOG_MSG(logERR, ERR_TOP_LEVEL_EXCEPTION, e.what()); fd.SetInvalid(); } catch (...) { + LOG_MSG(logERR, ERR_TOP_LEVEL_EXCEPTION, "(unkown)"); fd.SetInvalid(); } } From ae607a87d9fe455da25a01e4c0f5e35971ff4170 Mon Sep 17 00:00:00 2001 From: TwinFan Date: Fri, 10 Apr 2026 20:23:10 +0200 Subject: [PATCH 08/11] feat: LTAPI doesLTControlCamera --- Include/DataRefs.h | 10 ++++++++++ Lib/LTAPI/LTAPI.h | 12 ++++++++++++ Lib/XPMP2 | 2 +- Src/DataRefs.cpp | 26 ++++++++++++++++++++++++++ Src/LTAircraft.cpp | 6 ++++++ Src/LTMain.cpp | 3 +++ 6 files changed, 58 insertions(+), 1 deletion(-) diff --git a/Include/DataRefs.h b/Include/DataRefs.h index cd171981..937436d7 100644 --- a/Include/DataRefs.h +++ b/Include/DataRefs.h @@ -355,6 +355,8 @@ enum dataRefsLT { DR_SIM_DATE, DR_SIM_TIME, + DR_CAMERA_CONTROL, ///< Does LiveTraffic have camera control? + DR_LT_VER, ///< LiveTraffic's version number, like 201 for v2.01 DR_LT_VER_DATE, ///< LiveTraffic's version date, like 20200430 for 30-APR-2020 @@ -783,6 +785,10 @@ class DataRefs std::string keyAc; // key (transpIcao) for a/c whose data is returned const LTAircraft* pAc = nullptr; // ptr to that a/c + // Track camera control + const int MAX_CYCLE_NO_CAMERA_CB = 6; + int nCycleWithoutCameraCB = MAX_CYCLE_NO_CAMERA_CB; ///< How many flight loop cycles did we do without receiving a camera callback? (Anything larger than 5 is considered "No camera control") + // Weather float lastWeatherAttempt = 0.0f; ///< last time we _tried_ to update the weather float lastWeatherUpd = 0.0f; ///< last time the weather was updated? (in XP's network time) @@ -918,7 +924,11 @@ class DataRefs static float LTGetAcInfoF(void* p); void SetCameraAc(const LTAircraft* pCamAc); ///< sets the data of the shared datarefs to point to `ac` as the current aircraft under the camera + void CntCyclesWithoutCamera(); ///< Count flight loop callbacks without camera callback + void CntCameraCallback(); ///< Count the fact that there was a camera callback -> resets `nCycleWithoutCameraCB` static void ClearCameraAc(void*); ///< shared dataRef callback: Whenever someone else writes to the shared dataRef we clear our a/c camera information + // livetraffic/camera/control + static int LTHasCameraControl(void*); ///< Does LT have camera control? // seconds since epoch including fractionals double GetSimTime() const { return lastSimTime; } diff --git a/Lib/LTAPI/LTAPI.h b/Lib/LTAPI/LTAPI.h index cf330732..ce65f217 100644 --- a/Lib/LTAPI/LTAPI.h +++ b/Lib/LTAPI/LTAPI.h @@ -445,6 +445,18 @@ class LTAPIConnect /// Avoid duplicates, just use LTAPI if doesLTControlAI() is `true`. static bool doesLTControlAI (); + /// @brief Does LiveTraffic control X-Plane's camera? + /// @details LiveTraffic controls the camera if a user activates + /// the camera view on a plane ( and no 3rd party plugin + /// takes over immediately) + /// @note This can still return `false` at the time `LTAPIAircraft::toggleCamera` is called + /// as at that time it is not yet clear if LiveTraffic will have camera control or a 3rd party plugin. + /// It is `true` as soon as LiveTraffic receives camera callback calls from X-Plane + /// and is reset to `false` as soon as LiveTraffic is informed of having lost camera control, + /// or 5 flight loop callbacks after the last camera callback (in case LiveTraffic wasn't + /// informed of having lost camera control, e.g. because another plugin took over directly). + static bool doesLTControlCamera (); + /// What is current simulated time in LiveTraffic (usually 'now' minus buffering period)? static time_t getLTSimTime (); diff --git a/Lib/XPMP2 b/Lib/XPMP2 index 1f4fc9ce..5471eafa 160000 --- a/Lib/XPMP2 +++ b/Lib/XPMP2 @@ -1 +1 @@ -Subproject commit 1f4fc9ce0536b97e55ccf390ce09105ecd38c3ef +Subproject commit 5471eafa9835524cb89b7fb89cd4261ad0e06a59 diff --git a/Src/DataRefs.cpp b/Src/DataRefs.cpp index 26f695e2..4400870b 100644 --- a/Src/DataRefs.cpp +++ b/Src/DataRefs.cpp @@ -508,6 +508,8 @@ DataRefs::dataRefDefinitionT DATA_REFS_LT[CNT_DATAREFS_LT] = { {"livetraffic/sim/date", DataRefs::LTGetSimDateTime, NULL, (void*)1, false }, {"livetraffic/sim/time", DataRefs::LTGetSimDateTime, NULL, (void*)2, false }, + + {"livetraffic/camera/control", DataRefs::LTHasCameraControl }, {"livetraffic/ver/nr", GetLTVerNum, NULL, NULL, false }, {"livetraffic/ver/date", GetLTVerDate, NULL, NULL, false }, @@ -1500,6 +1502,9 @@ float DataRefs::LTGetAcInfoF(void* p) // sets the data of the shared datarefs to point to `ac` as the current aircraft under the camera void DataRefs::SetCameraAc(const LTAircraft* pCamAc) { + // If the camera aircraft has just been reset then we also make sure we don't consider us having camera control + nCycleWithoutCameraCB = MAX_CYCLE_NO_CAMERA_CB; + // requires that we could define and find the shared dataRef if (!adrXP[DR_CAMERA_TCAS_IDX] || !adrXP[DR_CAMERA_AC_ID]) @@ -1517,6 +1522,20 @@ void DataRefs::SetCameraAc(const LTAircraft* pCamAc) gbIgnoreItsMe = false; } +// Count flight loop callbacks without camera callback +void DataRefs::CntCyclesWithoutCamera() +{ + if (nCycleWithoutCameraCB < MAX_CYCLE_NO_CAMERA_CB) + nCycleWithoutCameraCB++; +} + +// Count the fact that there was a camera callback +void DataRefs::CntCameraCallback() +{ + nCycleWithoutCameraCB = 0; +} + + // shared dataRef callback: Whenever someone else writes to the shared dataRef we clear our a/c camera information void DataRefs::ClearCameraAc(void*) { @@ -1590,6 +1609,13 @@ int DataRefs::LTGetSimDateTime(void* p) } } +// livetraffic/camera/control +int DataRefs::LTHasCameraControl(void*) +{ + return dataRefs.nCycleWithoutCameraCB >= dataRefs.MAX_CYCLE_NO_CAMERA_CB ? 0 : 1; +} + + // Enable/Disable display of aircraft void DataRefs::LTSetAircraftDisplayed(void*, int i) { dataRefs.SetAircraftDisplayed (i); } diff --git a/Src/LTAircraft.cpp b/Src/LTAircraft.cpp index 80d2784c..c2a64b4e 100644 --- a/Src/LTAircraft.cpp +++ b/Src/LTAircraft.cpp @@ -2710,6 +2710,7 @@ int LTAircraft::CameraCB (XPLMCameraPosition_t* outCameraPosition, { CameraRegisterCommands(false); pExtViewAc = nullptr; + dataRefs.SetCameraAc(nullptr); return 0; } @@ -2724,6 +2725,11 @@ int LTAircraft::CameraCB (XPLMCameraPosition_t* outCameraPosition, outCameraPosition->roll = extOffs.roll; outCameraPosition->zoom = extOffs.zoom; + // Reset the counter that counts flight loop calls w/o camera control. + // The "loosing control" part above works great if X-Plane itself takes over camera control, + // but reportedly not if a 3rd party plugin takes over, so we count ourselves. + dataRefs.CntCameraCallback(); + return 1; } diff --git a/Src/LTMain.cpp b/Src/LTMain.cpp index 4a24d8b4..ccf7d56a 100644 --- a/Src/LTMain.cpp +++ b/Src/LTMain.cpp @@ -1099,6 +1099,9 @@ void LTRegularUpdates() // handle new network data (that func has a short-cut exit if nothing to do) LTFlightData::AppendAllNewPos(); + + // Count flight loop callbacks without camera control + dataRefs.CntCyclesWithoutCamera(); // Flush out all non-written log messages FlushMsg(); From 59a337742d92dd594b11bd3c9c6acb9051e78fa5 Mon Sep 17 00:00:00 2001 From: TwinFan Date: Sun, 12 Apr 2026 18:21:41 +0200 Subject: [PATCH 09/11] feat: New channel Airplanes.live --- CMakeLists.txt | 2 +- .../AirplanesLive.sjson/797699435.417481 | Bin 0 -> 29408 bytes Data/AirplanesLive/AirplanesLive.sjson/data | Bin 0 -> 184 bytes .../AirplanesLive.sjson/metaData | Bin 0 -> 221 bytes Include/Constants.h | 1 + Include/DataRefs.h | 3 +- Include/LTADSBEx.h | 35 ++- LiveTraffic.xcodeproj/project.pbxproj | 8 +- .../xcschemes/LiveTraffic.xcscheme | 2 +- README.md | 45 ++-- Src/DataRefs.cpp | 6 +- Src/LTADSBEx.cpp | 79 ++++++- Src/LTChannel.cpp | 3 +- Src/SettingsUI.cpp | 204 ++++++++++-------- docs/readme.html | 7 +- 15 files changed, 264 insertions(+), 131 deletions(-) create mode 100644 Data/AirplanesLive/AirplanesLive.sjson/797699435.417481 create mode 100644 Data/AirplanesLive/AirplanesLive.sjson/data create mode 100644 Data/AirplanesLive/AirplanesLive.sjson/metaData diff --git a/CMakeLists.txt b/CMakeLists.txt index ec44f9bd..23933667 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,7 +23,7 @@ endif() set(CACHE{CMAKE_BUILD_TYPE} TYPE STRING VALUE "RelWithDebInfo") project(LiveTraffic - VERSION 4.3.6 + VERSION 4.4.0 DESCRIPTION "LiveTraffic X-Plane plugin") set(VERSION_BETA 0) diff --git a/Data/AirplanesLive/AirplanesLive.sjson/797699435.417481 b/Data/AirplanesLive/AirplanesLive.sjson/797699435.417481 new file mode 100644 index 0000000000000000000000000000000000000000..a6226207b4ac901e37bdcf5f3216e06b7e0b5487 GIT binary patch literal 29408 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 S)2i4Td@}u}GIasMhyMdWA*Fx- literal 0 HcmV?d00001 diff --git a/Data/AirplanesLive/AirplanesLive.sjson/data b/Data/AirplanesLive/AirplanesLive.sjson/data new file mode 100644 index 0000000000000000000000000000000000000000..12c4432ed0e6fb3ff22a2e2de6b9a23d095dda94 GIT binary patch literal 184 zcmV;p07w5HiwFP!000006K#+|4uUWcMfaRy(yd4eLoM|HuGt$G2&g55nzpV;?=CHk zLE~)Z&&&VQN#_KB=$60{=48<>_29YAuoT&QeQQS3rz@B|AhlLQ-&zvk&ck!i5R-q? z0S>bBmZGX07QE;Rf mEiUv+JUn=59KNH4$Pke-PTA*YuHi1EiF*S)?Nwen0RRAJ&QhoV literal 0 HcmV?d00001 diff --git a/Data/AirplanesLive/AirplanesLive.sjson/metaData b/Data/AirplanesLive/AirplanesLive.sjson/metaData new file mode 100644 index 0000000000000000000000000000000000000000..eb1b8c92c277f36e94e01c0464652193a7f73f7d GIT binary patch literal 221 zcmV<303!b%iwFP!000006NS$~3&J262H<+TC4|& **NOTE:** The Docker environment hasn't been maintained for a long time +> and is no longer guaranteed to work. + +Locally, LiveTraffic can be build for all platforms using the Docker cross compile environment +[`twinfan/focal-win-mac-lin-compile-env`](https://hub.docker.com/r/twinfan/focal-win-mac-lin-compile-env). +Tested on Mac as a host, should work the same way on Linux. + +- Install [Docker Desktop](https://www.docker.com/products/docker-desktop) and start it. +- `cd docker` +- `make` + +In the first run only, it will download the necessary Docker image. +The actual build takes only a few seconds. Results are written to `build-*/*_x64` folders. + +For more background info also see [`docker/README.md`](https://github.com/TwinFan/LiveTraffic/blob/master/docker/README.md). + +The `Makefile` also builds the `doc` target, ie. the Doxygen documentation. +That will only work on a Mac. Otherwise, you may want to remove `doc` from `all`. + ### Doxygen Documentation Newer files come with Doxygen-style documentation. All file headers are updated already diff --git a/Src/DataRefs.cpp b/Src/DataRefs.cpp index 4400870b..60b3f39b 100644 --- a/Src/DataRefs.cpp +++ b/Src/DataRefs.cpp @@ -605,8 +605,9 @@ DataRefs::dataRefDefinitionT DATA_REFS_LT[CNT_DATAREFS_LT] = { {"livetraffic/channel/open_sky/online", DataRefs::LTGetInt, DataRefs::LTSetBool, GET_VAR, true, true }, {"livetraffic/channel/open_sky/ac_masterdata", DataRefs::LTGetInt, DataRefs::LTSetBool, GET_VAR, true, true }, {"livetraffic/channel/open_sky/ac_masterfile", DataRefs::LTGetInt, DataRefs::LTSetBool, GET_VAR, true, true }, - {"livetraffic/channel/adsb_fi/online", DataRefs::LTGetInt, DataRefs::LTSetBool, GET_VAR, true, true }, {"livetraffic/channel/adsb_exchange/online", DataRefs::LTGetInt, DataRefs::LTSetBool, GET_VAR, true, true }, + {"livetraffic/channel/adsb_fi/online", DataRefs::LTGetInt, DataRefs::LTSetBool, GET_VAR, true, true }, + {"livetraffic/channel/airplanes_live/online", DataRefs::LTGetInt, DataRefs::LTSetBool, GET_VAR, true, true }, {"livetraffic/channel/real_traffic/online", DataRefs::LTGetInt, DataRefs::LTSetBool, GET_VAR, true, true }, }; @@ -782,7 +783,8 @@ ILWrect (0, 400, 965, 0) i = false; // enable all public/free channels by default: - // adsb.fi, OpenSky Tracking & Master Data, OGN, and Synthetic by default + // Airplanes.live, adsb.fi, OpenSky Tracking & Master Data, OGN, and Synthetic by default + bChannel[DR_CHANNEL_AIRPLANES_LIVE - DR_CHANNEL_FIRST] = true; bChannel[DR_CHANNEL_ADSB_FI_ONLINE - DR_CHANNEL_FIRST] = true; bChannel[DR_CHANNEL_OPEN_SKY_ONLINE - DR_CHANNEL_FIRST] = true; bChannel[DR_CHANNEL_OPEN_SKY_AC_MASTERDATA - DR_CHANNEL_FIRST] = true; diff --git a/Src/LTADSBEx.cpp b/Src/LTADSBEx.cpp index e0cd35ea..83c2a143 100644 --- a/Src/LTADSBEx.cpp +++ b/Src/LTADSBEx.cpp @@ -637,6 +637,79 @@ size_t ADSBExchangeConnection::DoTestADSBExAPIKeyCB (char *ptr, size_t, size_t n return nmemb; } +// +// MARK: Airplanes.live +// + +AirplanesLiveConnection::AirplanesLiveConnection () : +ADSBBase(DR_CHANNEL_AIRPLANES_LIVE, AIRPLANES_NAME, AIRPLANES_SLUG_BASE) +{ + // purely informational + urlName = AIRPLANES_CHECK_NAME; + urlLink = AIRPLANES_CHECK_URL; + urlPopup = AIRPLANES_CHECK_POPUP; +} + + +// put together the URL to fetch based on current view position +std::string AirplanesLiveConnection::GetURL (const positionTy& pos) +{ + char url[128] = ""; + snprintf(url, sizeof(url), AIRPLANES_URL, pos.lat(), pos.lon(), + dataRefs.GetFdStdDistance_nm()); + return std::string(url); +} + + +// virtual thread main function +void AirplanesLiveConnection::Main () +{ + // This is a communication thread's main function, set thread's name and C locale + ThreadSettings TS ("LT_AirplanesLive", LC_ALL_MASK); + + while ( shallRun() ) { + // LiveTraffic Top Level Exception Handling + try { + // basis for determining when to be called next + tNextWakeup = std::chrono::steady_clock::now(); + + // where are we right now? + const positionTy pos (dataRefs.GetViewPos()); + + // If the camera position is valid we can request data around it + if (pos.isNormal()) { + // Next wakeup is "refresh interval" from _now_ + tNextWakeup += std::chrono::seconds(dataRefs.GetFdRefreshIntvl()); + + // fetch data and process it + if (FetchAllData(pos) && ProcessFetchedData()) + // reduce error count if processed successfully + // as a chance to appear OK in the long run + DecErrCnt(); + } + else { + // Camera position is yet invalid, retry in a second + tNextWakeup += std::chrono::seconds(1); + } + + // sleep for FD_REFRESH_INTVL or if woken up for termination + // by condition variable trigger + { + std::unique_lock lk(FDThreadSynchMutex); + FDThreadSynchCV.wait_until(lk, tNextWakeup, + [this]{return !shallRun();}); + } + + } catch (const std::exception& e) { + LOG_MSG(logERR, ERR_TOP_LEVEL_EXCEPTION, e.what()); + IncErrCnt(); + } catch (...) { + LOG_MSG(logERR, ERR_TOP_LEVEL_EXCEPTION, "(unknown type)"); + IncErrCnt(); + } + } +} + // // MARK: adsb.fi // @@ -683,9 +756,9 @@ void ADSBfiConnection::Main () // fetch data and process it if (FetchAllData(pos) && ProcessFetchedData()) - // reduce error count if processed successfully - // as a chance to appear OK in the long run - DecErrCnt(); + // reduce error count if processed successfully + // as a chance to appear OK in the long run + DecErrCnt(); } else { // Camera position is yet invalid, retry in a second diff --git a/Src/LTChannel.cpp b/Src/LTChannel.cpp index baa207e6..4577c7a9 100644 --- a/Src/LTChannel.cpp +++ b/Src/LTChannel.cpp @@ -893,8 +893,9 @@ bool LTFlightDataEnable() // load live feed readers (in order of priority) listFDC.emplace_back(new RealTrafficConnection()); - listFDC.emplace_back(new ADSBExchangeConnection); + listFDC.emplace_back(new AirplanesLiveConnection); listFDC.emplace_back(new ADSBfiConnection); + listFDC.emplace_back(new ADSBExchangeConnection); listFDC.emplace_back(new OpenSkyConnection); listFDC.emplace_back(new ADSBHubConnection()); listFDC.emplace_back(new OpenGliderConnection); diff --git a/Src/SettingsUI.cpp b/Src/SettingsUI.cpp index 39b023f4..ed31e2b3 100644 --- a/Src/SettingsUI.cpp +++ b/Src/SettingsUI.cpp @@ -263,6 +263,28 @@ void LTSettingsUI::buildInterface() } } + // --- Airplanes.live --- + if (ImGui::TreeNodeCbxLinkHelp("Airplanes.live", nCol, + DR_CHANNEL_AIRPLANES_LIVE, "Connect to Airplanes.live for tracking data", + ICON_FA_EXTERNAL_LINK_SQUARE_ALT " " AIRPLANES_CHECK_NAME, + AIRPLANES_CHECK_URL, + AIRPLANES_CHECK_POPUP, + HELP_SET_CH_AIRPLANES, "Open Help on Airplanes.live in Browser", + sFilter, nOpCl)) + { + // Airplanes.live's connection status details + if (ImGui::FilteredLabel("Connection Status", sFilter)) { + if (const LTChannel* pAirplanesCh = LTFlightDataGetCh(DR_CHANNEL_AIRPLANES_LIVE)) { + ImGui::TextWrapped("%s", pAirplanesCh->GetStatusText().c_str()); + } else { + ImGui::TextUnformatted("Off"); + } + ImGui::TableNextCell(); + } + + if (!*sFilter) ImGui::TreePop(); + } + // --- adsb.fi --- if (ImGui::TreeNodeCbxLinkHelp("adsb.fi", nCol, DR_CHANNEL_ADSB_FI_ONLINE, "Connect to adsb.fi for tracking data", @@ -284,7 +306,7 @@ void LTSettingsUI::buildInterface() if (!*sFilter) ImGui::TreePop(); } - + // --- OpenSky --- if (ImGui::TreeNodeCbxLinkHelp("OpenSky Network", nCol, DR_CHANNEL_OPEN_SKY_ONLINE, "Enable OpenSky tracking data", @@ -455,96 +477,6 @@ void LTSettingsUI::buildInterface() if (!*sFilter) ImGui::TreePop(); } - // --- ADS-B Exchange --- - if (ImGui::TreeNodeCbxLinkHelp("ADS-B Exchange", nCol, - // we offer the enable checkbox only when an API key is defined - dataRefs.GetADSBExAPIKey().empty() ? dataRefsLT(-1) : DR_CHANNEL_ADSB_EXCHANGE_ONLINE, - dataRefs.GetADSBExAPIKey().empty() ? "ADS-B Exchange requires an API key" : "Enable ADS-B Exchange tracking data", - ICON_FA_EXTERNAL_LINK_SQUARE_ALT " " ADSBEX_CHECK_NAME, - ADSBEX_CHECK_URL, - ADSBEX_CHECK_POPUP, - HELP_SET_CH_ADSBEX, "Open Help on ADS-B Exchange in Browser", - sFilter, nOpCl)) - { - // Have no ADSBEx key? - if (dataRefs.GetADSBExAPIKey().empty()) { - if (ImGui::FilteredLabel("ADS-B Exchange", sFilter, false)) { - ImGui::TextDisabled("%s", "requires an API key:"); - ImGui::TableNextCell(); - } - } - - // ADS-B Exchange's API key - if (ImGui::FilteredLabel("API Key", sFilter)) { - // "Eye" button changes password flag - ImGui::Selectable(ICON_FA_EYE "##ADSBExKeyVisible", &bADSBExKeyClearText, - ImGuiSelectableFlags_None, ImVec2(ImGui::GetWidthIconBtn(),0)); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "Show/Hide key"); - ImGui::SameLine(); // make text entry the size of the remaining space in cell, but not larger - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if (ImGui::InputTextWithHint("##ADSBExKey", - "Enter or paste API key", - &sADSBExKeyEntry, - // clear text or password mode? - (bADSBExKeyClearText ? ImGuiInputTextFlags_None : ImGuiInputTextFlags_Password) | - // prohibit changes to the key while test is underway - (eADSBExKeyTest == ADSBX_KEY_TESTING ? ImGuiInputTextFlags_ReadOnly : ImGuiInputTextFlags_None))) - // when key is changing reset a potential previous result - eADSBExKeyTest = ADSBX_KEY_NO_ACTION; - // key is changed and different -> offer to test it - if (eADSBExKeyTest == ADSBX_KEY_NO_ACTION && - !sADSBExKeyEntry.empty() && - sADSBExKeyEntry != dataRefs.GetADSBExAPIKey()) - { - if (ImGui::ButtonTooltip(ICON_FA_UNDO " Reset to saved", "Resets the key to the previously saved key")) - { - sADSBExKeyEntry = dataRefs.GetADSBExAPIKey(); - } - ImGui::SameLine(); - if (ImGui::ButtonTooltip(ICON_FA_CHECK " Test and Save Key", "Sends a request to ADS-B Exchange using your entered key to test its validity.\nKey is saved only after a successful test.")) - { - ADSBExchangeConnection::TestADSBExAPIKey(sADSBExKeyEntry); - eADSBExKeyTest = ADSBX_KEY_TESTING; - } - } - // Test of key underway? -> check for result - if (eADSBExKeyTest == ADSBX_KEY_TESTING) { - bool bSuccess = false; - if (ADSBExchangeConnection::TestADSBExAPIKeyResult(bSuccess)) { - eADSBExKeyTest = bSuccess ? ADSBX_KEY_SUCCESS : ADSBX_KEY_FAILED; - if (bSuccess) { - dataRefs.SetADSBExAPIKey(sADSBExKeyEntry); - - } - } else { - ImGui::TextUnformatted(ICON_FA_SPINNER " Key is being tested..."); - } - } - // Key tested successfully - if (eADSBExKeyTest == ADSBX_KEY_SUCCESS) - ImGui::TextUnformatted(ICON_FA_CHECK_CIRCLE " Key tested successfully"); - else if (eADSBExKeyTest == ADSBX_KEY_FAILED) - ImGui::TextUnformatted(ICON_FA_EXCLAMATION_TRIANGLE " Key test failed!"); - - ImGui::TableNextCell(); - } - - // ADSBEx's connection status details (only if there is an API key defined) - if (!dataRefs.GetADSBExAPIKey().empty() && - ImGui::FilteredLabel("Connection Status", sFilter)) - { - if (const LTChannel* pADSBExbCh = LTFlightDataGetCh(DR_CHANNEL_ADSB_EXCHANGE_ONLINE)) { - ImGui::TextWrapped("%s", pADSBExbCh->GetStatusText().c_str()); - } else { - ImGui::TextUnformatted("Off"); - } - ImGui::TableNextCell(); - } - - if (!*sFilter) ImGui::TreePop(); - } - // --- Open Glider Network --- if (ImGui::TreeNodeCbxLinkHelp("Open Glider Network", nCol, DR_CHANNEL_OPEN_GLIDER_NET, "Enable OGN tracking data", @@ -772,6 +704,96 @@ void LTSettingsUI::buildInterface() if (!*sFilter) ImGui::TreePop(); } + // --- ADS-B Exchange --- + if (ImGui::TreeNodeCbxLinkHelp("ADS-B Exchange", nCol, + // we offer the enable checkbox only when an API key is defined + dataRefs.GetADSBExAPIKey().empty() ? dataRefsLT(-1) : DR_CHANNEL_ADSB_EXCHANGE_ONLINE, + dataRefs.GetADSBExAPIKey().empty() ? "ADS-B Exchange requires an API key" : "Enable ADS-B Exchange tracking data", + ICON_FA_EXTERNAL_LINK_SQUARE_ALT " " ADSBEX_CHECK_NAME, + ADSBEX_CHECK_URL, + ADSBEX_CHECK_POPUP, + HELP_SET_CH_ADSBEX, "Open Help on ADS-B Exchange in Browser", + sFilter, nOpCl)) + { + // Have no ADSBEx key? + if (dataRefs.GetADSBExAPIKey().empty()) { + if (ImGui::FilteredLabel("ADS-B Exchange", sFilter, false)) { + ImGui::TextDisabled("%s", "requires an API key:"); + ImGui::TableNextCell(); + } + } + + // ADS-B Exchange's API key + if (ImGui::FilteredLabel("API Key", sFilter)) { + // "Eye" button changes password flag + ImGui::Selectable(ICON_FA_EYE "##ADSBExKeyVisible", &bADSBExKeyClearText, + ImGuiSelectableFlags_None, ImVec2(ImGui::GetWidthIconBtn(),0)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Show/Hide key"); + ImGui::SameLine(); // make text entry the size of the remaining space in cell, but not larger + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputTextWithHint("##ADSBExKey", + "Enter or paste API key", + &sADSBExKeyEntry, + // clear text or password mode? + (bADSBExKeyClearText ? ImGuiInputTextFlags_None : ImGuiInputTextFlags_Password) | + // prohibit changes to the key while test is underway + (eADSBExKeyTest == ADSBX_KEY_TESTING ? ImGuiInputTextFlags_ReadOnly : ImGuiInputTextFlags_None))) + // when key is changing reset a potential previous result + eADSBExKeyTest = ADSBX_KEY_NO_ACTION; + // key is changed and different -> offer to test it + if (eADSBExKeyTest == ADSBX_KEY_NO_ACTION && + !sADSBExKeyEntry.empty() && + sADSBExKeyEntry != dataRefs.GetADSBExAPIKey()) + { + if (ImGui::ButtonTooltip(ICON_FA_UNDO " Reset to saved", "Resets the key to the previously saved key")) + { + sADSBExKeyEntry = dataRefs.GetADSBExAPIKey(); + } + ImGui::SameLine(); + if (ImGui::ButtonTooltip(ICON_FA_CHECK " Test and Save Key", "Sends a request to ADS-B Exchange using your entered key to test its validity.\nKey is saved only after a successful test.")) + { + ADSBExchangeConnection::TestADSBExAPIKey(sADSBExKeyEntry); + eADSBExKeyTest = ADSBX_KEY_TESTING; + } + } + // Test of key underway? -> check for result + if (eADSBExKeyTest == ADSBX_KEY_TESTING) { + bool bSuccess = false; + if (ADSBExchangeConnection::TestADSBExAPIKeyResult(bSuccess)) { + eADSBExKeyTest = bSuccess ? ADSBX_KEY_SUCCESS : ADSBX_KEY_FAILED; + if (bSuccess) { + dataRefs.SetADSBExAPIKey(sADSBExKeyEntry); + + } + } else { + ImGui::TextUnformatted(ICON_FA_SPINNER " Key is being tested..."); + } + } + // Key tested successfully + if (eADSBExKeyTest == ADSBX_KEY_SUCCESS) + ImGui::TextUnformatted(ICON_FA_CHECK_CIRCLE " Key tested successfully"); + else if (eADSBExKeyTest == ADSBX_KEY_FAILED) + ImGui::TextUnformatted(ICON_FA_EXCLAMATION_TRIANGLE " Key test failed!"); + + ImGui::TableNextCell(); + } + + // ADSBEx's connection status details (only if there is an API key defined) + if (!dataRefs.GetADSBExAPIKey().empty() && + ImGui::FilteredLabel("Connection Status", sFilter)) + { + if (const LTChannel* pADSBExbCh = LTFlightDataGetCh(DR_CHANNEL_ADSB_EXCHANGE_ONLINE)) { + ImGui::TextWrapped("%s", pADSBExbCh->GetStatusText().c_str()); + } else { + ImGui::TextUnformatted("Off"); + } + ImGui::TableNextCell(); + } + + if (!*sFilter) ImGui::TreePop(); + } + // --- FSCharter --- if (ImGui::TreeNodeCbxLinkHelp(FSC_NAME, nCol, DR_CHANNEL_FSCHARTER, diff --git a/docs/readme.html b/docs/readme.html index 0d556a59..1c25da9e 100755 --- a/docs/readme.html +++ b/docs/readme.html @@ -133,14 +133,14 @@

    Release Notes

    v4

    -

    v4.3.6 Beta

    +

    v4.4.0

    Update: In case of doubt you can always just copy all files from the archive over the files of your existing installation.

    -

    At least copy the following files, which have changed compared to v4.3.3:

    +

    At least copy the following files, which have changed compared to v4.3.5:

    • lin|mac|win_x64/LiveTraffic.xpl
    @@ -148,6 +148,9 @@

    v4.3.6 Beta

    Change log:

      +
    • Airplanes.live: New free traffic data channel, + see documentation. + On by default.
    • RealTraffic:
      • Link to RealTraffic's aircraft tracking from From 5bae71e98fedefc16d813e3a4ba822bee387d525 Mon Sep 17 00:00:00 2001 From: TwinFan Date: Tue, 14 Apr 2026 22:59:47 +0200 Subject: [PATCH 10/11] feat/Sound: Set Audio output Device --- Include/Constants.h | 2 ++ Include/DataRefs.h | 5 +++ Include/SettingsUI.h | 4 +++ Src/DataRefs.cpp | 78 +++++++++++++++++++++++++++++++++++++++----- Src/LiveTraffic.cpp | 3 ++ Src/SettingsUI.cpp | 22 ++++++++++++- docs/readme.html | 5 +++ 7 files changed, 109 insertions(+), 10 deletions(-) diff --git a/Include/Constants.h b/Include/Constants.h index e7c99798..5f17b65e 100644 --- a/Include/Constants.h +++ b/Include/Constants.h @@ -211,6 +211,8 @@ constexpr const char* REMOTE_SIGNATURE = "TwinFan.plugin.XPMP2.Remote"; #define CFG_DEFAULT_CAR_TYPE "DEFAULT_CAR_TYPE" #define CFG_DEFAULT_AC_TYP_INFO "Default a/c type is '%s'" #define CFG_DEFAULT_CAR_TYP_INFO "Default car type is '%s'" +#define CFG_SOUND_DEVICE "Sound_Device" +#define SOUND_DEV_XPLANE "X-Plane" #define CFG_OPENSKY_CLIENT "OpenSky_Client" #define CFG_OPENSKY_SECRET "OpenSky_Secret" #define CFG_ADSBEX_API_KEY "ADSBEX_API_KEY" diff --git a/Include/DataRefs.h b/Include/DataRefs.h index d50c136f..53ac5717 100644 --- a/Include/DataRefs.h +++ b/Include/DataRefs.h @@ -771,6 +771,7 @@ class DataRefs std::string sDefaultAcIcaoType = CSL_DEFAULT_ICAO_TYPE; std::string sDefaultCarIcaoType = CSL_CAR_ICAO_TYPE; + std::string sSoundDevice; ///< Output sound device name std::string sOpenSkyClient; ///< OpenSky Network Client ID std::string sOpenSkySecret; ///< OpenSky Network Client Secret std::string sADSBExAPIKey; ///< ADS-B Exchange API key @@ -974,6 +975,10 @@ class DataRefs inline bool GetAutoStart() const { return bAutoStart != 0; } int GetVolumeMaster() const { return volMaster; } bool ShallForceFmodInstance() const { return sndForceFmodInstance != 0; } + const std::string& GetSoundDevice () const { return sSoundDevice; } + bool SetSoundDevice (const std::string& dev); + std::vector GetAllSoundDeviceNames (bool bForceIncludeCurrent) const;; + void SetSound (); ///< Set sound according to volMaster and sSoundDevice inline bool IsAIonRequest() const { return bAIonRequest != 0; } bool IsAINotOnGnd() const { return bAINotOnGnd != 0; } static int HaveAIUnderControl(void* =NULL) { return XPMPHasControlOfAIAircraft(); } diff --git a/Include/SettingsUI.h b/Include/SettingsUI.h index d755cdec..cb8dff88 100644 --- a/Include/SettingsUI.h +++ b/Include/SettingsUI.h @@ -93,6 +93,10 @@ class LTSettingsUI : public LTImgWindow std::string gndVehicleEntry; ///< edit buffer for ground vehicle int gndVehicleOK = 0; ///< -1 error, 0 untested, 1 OK + // Advanced + std::vector vecSndDevs; ///< List of possible sound devices + float tsSndDevsLastUpd = 0.0f; ///< when was that list updated last? + // Debug options std::string txtDebugFilter; ///< filter for single aircraft std::string txtFixAcType; ///< fixed aircraft type diff --git a/Src/DataRefs.cpp b/Src/DataRefs.cpp index 60b3f39b..0b9c39fe 100644 --- a/Src/DataRefs.cpp +++ b/Src/DataRefs.cpp @@ -1779,15 +1779,8 @@ bool DataRefs::SetCfgValue (void* p, int val) else if (p == &fdLongRefrIntvl && fdCurrRefrIntvl == oldLongRefreshIntvl) fdCurrRefrIntvl = fdLongRefrIntvl; // Master Volume change to be forwarded to XPMP2, too - else if (p == &volMaster) { - if (volMaster == 0) { // Disable sound altogether - XPMPSoundEnable(false); - } else { // Sound is (to be) enabled - if (!XPMPSoundIsEnabled() && pluginState >= STATE_INIT) - XPMPSoundEnable(true); - XPMPSoundSetMasterVolume(float(volMaster) / 100.0f); - } - } + else if (p == &volMaster) + SetSound(); // Enable/disable sound if and only if volume > 0 // If weather is... if (p == &weatherCtl) { @@ -1903,6 +1896,69 @@ void DataRefs::GetLabelColor (float outColor[4]) const conv_color(labelColor, outColor); } +// Set the sound device name +bool DataRefs::SetSoundDevice (const std::string& dev) +{ + if (XPMPSoundSetAudioDeviceName(dev)) { + LOG_MSG(logINFO, "Using sound device '%s'", dev.c_str()); + sSoundDevice = dev; + return true; + } + if (dev != SOUND_DEV_XPLANE) { + LOG_MSG(logWARN, "Unable to select sound device '%s'", dev.c_str()); + } + return false; +} + +// Get all possible sound device names. +// Parameter determines if sSoundDevice is added to the list if needed +std::vector DataRefs::GetAllSoundDeviceNames (bool bForceIncludeCurrent) const +{ + // Fetch all device names from XPMP2 and check along the way if the current selected device name is included + bool bCurrDevIsIn = false; + std::vector vec; + std::string dev; + for (int i = 0; XPMPSoundGetAudioDeviceName(i, dev); ++i) { + if (sSoundDevice == dev) + bCurrDevIsIn = true; + vec.emplace_back(std::move(dev)); + } + // If we didn't get anything then it'll be X-Plane's sound system at work + if (vec.empty()) { + if (sSoundDevice == SOUND_DEV_XPLANE) + bCurrDevIsIn = true; + vec.emplace_back(SOUND_DEV_XPLANE); + } + // Add the current selected device name if not in and so wished + if (!bCurrDevIsIn && bForceIncludeCurrent && !sSoundDevice.empty()) + vec.emplace_back(sSoundDevice); + // return all + return vec; +} + +// Enable/disable sound +void DataRefs::SetSound () +{ + if (volMaster > 0) { + // Sound is (to be) enabled + if (!XPMPSoundIsEnabled() && pluginState >= STATE_INIT) + XPMPSoundEnable(true); + XPMPSoundSetMasterVolume(float(volMaster) / 100.0f); + // if setting the output device fails, then figure out what now that device is + if (!sSoundDevice.empty() && // there is a device to set + !SetSoundDevice(sSoundDevice) && // setting that dev didn't work + XPMPSoundGetActiveAudioDevice(&sSoundDevice) == 0 && // let's figure out what now the device is + sSoundDevice.empty()) // but if it is still index 0 and no name + { + sSoundDevice = SOUND_DEV_XPLANE; // then assume it is "X-Plane" itself + } + } + else { + // Disable sound altogether + XPMPSoundEnable(false); + } +} + // //MARK: Debug Options // @@ -2232,6 +2288,8 @@ bool DataRefs::LoadConfigFile() SetDefaultAcIcaoType(sVal); else if (sDataRef == CFG_DEFAULT_CAR_TYPE) SetDefaultCarIcaoType(sVal); + else if (sDataRef == CFG_SOUND_DEVICE) + sSoundDevice = sVal; // can't set device now, too early else if (sDataRef == CFG_OPENSKY_CLIENT) SetOpenSkyClient(sVal); else if (sDataRef == CFG_OPENSKY_SECRET) @@ -2382,6 +2440,8 @@ bool DataRefs::SaveConfigFile() // *** Strings *** fOut << CFG_DEFAULT_AC_TYPE << ' ' << GetDefaultAcIcaoType() << '\n'; fOut << CFG_DEFAULT_CAR_TYPE << ' ' << GetDefaultCarIcaoType() << '\n'; + if (!sSoundDevice.empty()) + fOut << CFG_SOUND_DEVICE << ' ' << sSoundDevice << '\n'; if (!sOpenSkyClient.empty()) fOut << CFG_OPENSKY_CLIENT << ' ' << sOpenSkyClient << '\n'; if (!sOpenSkySecret.empty()) diff --git a/Src/LiveTraffic.cpp b/Src/LiveTraffic.cpp index e9b9fa8e..4212bb55 100755 --- a/Src/LiveTraffic.cpp +++ b/Src/LiveTraffic.cpp @@ -530,6 +530,9 @@ PLUGIN_API int XPluginEnable(void) try { // Enable showing aircraft if (!LTMainEnable()) return 0; + + // Initialize sound and sound device + dataRefs.SetSound(); // Success return 1; diff --git a/Src/SettingsUI.cpp b/Src/SettingsUI.cpp index ed31e2b3..05fbac4b 100644 --- a/Src/SettingsUI.cpp +++ b/Src/SettingsUI.cpp @@ -1166,7 +1166,27 @@ void LTSettingsUI::buildInterface() { ImGui::FilteredCfgCheckbox("Own FMOD Instance", sFilter, DR_CFG_SND_FORCE_FMOD_INSTANCE, "Enforce using separate FMOD instance instead of X-Plane's.\n(Takes effect after restart only.)"); - + // Sound Device Selection + if (ImGui::FilteredLabel("Sound Device", sFilter)) { + const std::string& devCurr = dataRefs.GetSoundDevice(); + if (ImGui::BeginCombo("##SoundDevice", devCurr.c_str())) { + // Get list of sound devices every other second "only" + if (CheckEverySoOften(tsSndDevsLastUpd, 2.0f)) + vecSndDevs = dataRefs.GetAllSoundDeviceNames(true); + // List thems + for (const std::string& dev: vecSndDevs) { + bool isSelected = (dev == devCurr); + if (ImGui::Selectable(dev.c_str(), &isSelected)) { // new selecton made? + if (!dataRefs.SetSoundDevice(dev)) // try to use...didn't work? + isSelected = false; + } + if (isSelected) // if (now,still) the selected, make it the focused one + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + } + if (!*sFilter) ImGui::TreePop(); } diff --git a/docs/readme.html b/docs/readme.html index 1c25da9e..1b34c0f0 100755 --- a/docs/readme.html +++ b/docs/readme.html @@ -160,6 +160,11 @@

        v4.4.0

      • App: Supports now processing RealTraffic's weather.
    • +
    • + Sound: If forcing an Own FMOD instance, then you can now select + the Sound Device for sound produced by LiveTraffic in the + Advanced / Miscellaneous settings. +
    • OpenSky: Switched off the "Masterdata File" channel to avoid errors in the log as OpenSky is not currently maintaining that From 5c491f5218f0df0dc03eecebfc98a8ed9f8b378f Mon Sep 17 00:00:00 2001 From: TwinFan Date: Tue, 28 Apr 2026 21:21:33 +0200 Subject: [PATCH 11/11] feat/Sound: Add a "no change" option --- Include/Constants.h | 2 +- Include/DataRefs.h | 2 +- Src/DataRefs.cpp | 21 ++++++++++++--------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Include/Constants.h b/Include/Constants.h index 5f17b65e..00fff45d 100644 --- a/Include/Constants.h +++ b/Include/Constants.h @@ -212,7 +212,7 @@ constexpr const char* REMOTE_SIGNATURE = "TwinFan.plugin.XPMP2.Remote"; #define CFG_DEFAULT_AC_TYP_INFO "Default a/c type is '%s'" #define CFG_DEFAULT_CAR_TYP_INFO "Default car type is '%s'" #define CFG_SOUND_DEVICE "Sound_Device" -#define SOUND_DEV_XPLANE "X-Plane" +#define CFG_SND_NO_DEVICE "(no change)" #define CFG_OPENSKY_CLIENT "OpenSky_Client" #define CFG_OPENSKY_SECRET "OpenSky_Secret" #define CFG_ADSBEX_API_KEY "ADSBEX_API_KEY" diff --git a/Include/DataRefs.h b/Include/DataRefs.h index 53ac5717..d734a592 100644 --- a/Include/DataRefs.h +++ b/Include/DataRefs.h @@ -771,7 +771,7 @@ class DataRefs std::string sDefaultAcIcaoType = CSL_DEFAULT_ICAO_TYPE; std::string sDefaultCarIcaoType = CSL_CAR_ICAO_TYPE; - std::string sSoundDevice; ///< Output sound device name + std::string sSoundDevice = CFG_SND_NO_DEVICE; ///< Output sound device name std::string sOpenSkyClient; ///< OpenSky Network Client ID std::string sOpenSkySecret; ///< OpenSky Network Client Secret std::string sADSBExAPIKey; ///< ADS-B Exchange API key diff --git a/Src/DataRefs.cpp b/Src/DataRefs.cpp index 0b9c39fe..ab5e28ca 100644 --- a/Src/DataRefs.cpp +++ b/Src/DataRefs.cpp @@ -1899,12 +1899,19 @@ void DataRefs::GetLabelColor (float outColor[4]) const // Set the sound device name bool DataRefs::SetSoundDevice (const std::string& dev) { + // "no change" always works + if (dev == CFG_SND_NO_DEVICE) { + sSoundDevice = CFG_SND_NO_DEVICE; + return true; + } + + // Try to set the selected device if (XPMPSoundSetAudioDeviceName(dev)) { LOG_MSG(logINFO, "Using sound device '%s'", dev.c_str()); sSoundDevice = dev; return true; } - if (dev != SOUND_DEV_XPLANE) { + else { LOG_MSG(logWARN, "Unable to select sound device '%s'", dev.c_str()); } return false; @@ -1916,19 +1923,15 @@ std::vector DataRefs::GetAllSoundDeviceNames (bool bForceIncludeCur { // Fetch all device names from XPMP2 and check along the way if the current selected device name is included bool bCurrDevIsIn = false; - std::vector vec; + std::vector vec = { CFG_SND_NO_DEVICE }; // start with the extra "(no change)" entry + if (sSoundDevice == CFG_SND_NO_DEVICE) + bCurrDevIsIn = true; std::string dev; for (int i = 0; XPMPSoundGetAudioDeviceName(i, dev); ++i) { if (sSoundDevice == dev) bCurrDevIsIn = true; vec.emplace_back(std::move(dev)); } - // If we didn't get anything then it'll be X-Plane's sound system at work - if (vec.empty()) { - if (sSoundDevice == SOUND_DEV_XPLANE) - bCurrDevIsIn = true; - vec.emplace_back(SOUND_DEV_XPLANE); - } // Add the current selected device name if not in and so wished if (!bCurrDevIsIn && bForceIncludeCurrent && !sSoundDevice.empty()) vec.emplace_back(sSoundDevice); @@ -1950,7 +1953,7 @@ void DataRefs::SetSound () XPMPSoundGetActiveAudioDevice(&sSoundDevice) == 0 && // let's figure out what now the device is sSoundDevice.empty()) // but if it is still index 0 and no name { - sSoundDevice = SOUND_DEV_XPLANE; // then assume it is "X-Plane" itself + sSoundDevice = CFG_SND_NO_DEVICE; // then it is "(no change)" } } else {