diff --git a/CMakeLists.txt b/CMakeLists.txt index ad1a7bce..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.5 + 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 00000000..a6226207 Binary files /dev/null and b/Data/AirplanesLive/AirplanesLive.sjson/797699435.417481 differ diff --git a/Data/AirplanesLive/AirplanesLive.sjson/data b/Data/AirplanesLive/AirplanesLive.sjson/data new file mode 100644 index 00000000..12c4432e Binary files /dev/null and b/Data/AirplanesLive/AirplanesLive.sjson/data differ diff --git a/Data/AirplanesLive/AirplanesLive.sjson/metaData b/Data/AirplanesLive/AirplanesLive.sjson/metaData new file mode 100644 index 00000000..eb1b8c92 Binary files /dev/null and b/Data/AirplanesLive/AirplanesLive.sjson/metaData differ diff --git a/Include/Constants.h b/Include/Constants.h index f652bb49..00fff45d 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 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" @@ -266,6 +268,7 @@ constexpr const char* REMOTE_SIGNATURE = "TwinFan.plugin.XPMP2.Remote"; #define HELP_SET_CH_OPENSKY "setup/installation/opensky" #define HELP_SET_CH_ADSBHUB "setup/installation/adsbhub" #define HELP_SET_CH_ADSBEX "setup/installation/ads-b-exchange" +#define HELP_SET_CH_AIRPLANES "setup/installation/airplanes.live" #define HELP_SET_CH_ADSBFI "setup/installation/adsb.fi" #define HELP_SET_CH_OPENGLIDER "setup/installation/ogn" #define HELP_SET_CH_REALTRAFFIC "setup/installation/realtraffic-connectivity" 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/DataRefs.h b/Include/DataRefs.h index cd171981..d734a592 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 @@ -449,8 +451,9 @@ enum dataRefsLT { DR_CHANNEL_OPEN_SKY_ONLINE, DR_CHANNEL_OPEN_SKY_AC_MASTERDATA, DR_CHANNEL_OPEN_SKY_AC_MASTERFILE, - DR_CHANNEL_ADSB_FI_ONLINE, DR_CHANNEL_ADSB_EXCHANGE_ONLINE, + DR_CHANNEL_ADSB_FI_ONLINE, + DR_CHANNEL_AIRPLANES_LIVE, DR_CHANNEL_REAL_TRAFFIC_ONLINE, // currently highest-prio channel // always last, number of elements: CNT_DATAREFS_LT @@ -768,6 +771,7 @@ class DataRefs std::string sDefaultAcIcaoType = CSL_DEFAULT_ICAO_TYPE; std::string sDefaultCarIcaoType = CSL_CAR_ICAO_TYPE; + 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 @@ -783,6 +787,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 +926,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; } @@ -963,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/LTADSBEx.h b/Include/LTADSBEx.h index eedb981d..92f57072 100644 --- a/Include/LTADSBEx.h +++ b/Include/LTADSBEx.h @@ -1,19 +1,22 @@ /// @file LTADSBEx.h /// @brief ADS-B Exchange and adsb.fi: Requests and processes live tracking data +/// @see Airplanes.live: https://airplanes.live/api-guide/ +/// @see adsb.fi: https://github.com/adsbfi/opendata /// @see ADSBEx: https://www.adsbexchange.com/ /// RAPID API: https://rapidapi.com/adsbx/api/adsbexchange-com1 /// RAPID API Endpoint: https://rapidapi.com/adsbx/api/adsbexchange-com1/playground/endpoint_7dee5835-86b3-40ce-a402-f1ab43240884 /// ADSBEx v2 API documentation: /// ...on Swagger: https://adsbexchange.com/api/aircraft/v2/docs /// ...fields: https://www.adsbexchange.com/version-2-api-wip/ -/// @see adsb.fi: https://github.com/adsbfi/opendata /// @details Defines a base class handling the ADSBEx data format, /// which is shared by both ADS-B Exchange and adsb.fi. +/// @details Defines AirplanesLiveConnection:\n +/// - Provides a proper REST-conform URL +/// @details Defines ADSBfiConnection:\n +/// - Provides a proper REST-conform URL /// @details Defines ADSBExchangeConnection:\n /// - Handles the API key\n /// - Provides a proper REST-conform URL for both the original sevrer as well as for the Rapid API server. -/// @details Defines ADSBfiConnection:\n -/// - Provides a proper REST-conform URL /// @author Birger Hoppe /// @copyright (c) 2018-2024 Birger Hoppe /// @copyright Permission is hereby granted, free of charge, to any person obtaining a @@ -166,6 +169,30 @@ class ADSBExchangeConnection : public ADSBBase static size_t DoTestADSBExAPIKeyCB (char *ptr, size_t, size_t nmemb, void* userdata); }; +// +// MARK: Airplanes.live +// + +#define AIRPLANES_CHECK_NAME "Airplanes.live Map" +#define AIRPLANES_CHECK_URL "https://globe.airplanes.live/?lat=%.3f&lon=%.3f" +#define AIRPLANES_SLUG_BASE "https://globe.airplanes.live/?icao=" // + icao24 hex code +#define AIRPLANES_CHECK_POPUP "Check Airplane.live's coverage" + +#define AIRPLANES_NAME "Airplanes.live" +#define AIRPLANES_URL "https://api.airplanes.live/v2/point/%.3f/%.3f/%d" // lat/lon/radius + +class AirplanesLiveConnection : public ADSBBase +{ +public: + AirplanesLiveConnection (); ///< Constructor + std::string GetURL (const positionTy& pos) override; ///< Compile Airplanes.live request URL + +protected: + void Main () override; ///< virtual thread main function + bool ProcessErrors (const JSON_Object*) override ///< No specific error processing for Airplanes.live + { return true; } +}; + // // MARK: adsb.fi // @@ -176,7 +203,7 @@ class ADSBExchangeConnection : public ADSBBase #define ADSBFI_CHECK_POPUP "Check adsb.fi's coverage" #define ADSBFI_NAME "adsb.fi" -#define ADSBFI_URL "https://opendata.adsb.fi/api/v2/lat/%f/lon/%f/dist/%d/" +#define ADSBFI_URL "https://opendata.adsb.fi/api/v2/lat/%.3f/lon/%.3f/dist/%d/" #define ADSBFI_AIRCRAFT_ARR "aircraft" diff --git a/Include/LTRealTraffic.h b/Include/LTRealTraffic.h index ff8af194..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" @@ -56,7 +57,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 +91,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 +256,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 +284,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 +330,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 +344,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 +370,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 +390,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 +424,21 @@ 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 + 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); + void AdjustTimestamp (double& ts, int nBuffer); /// Return a string describing the current timestamp adjustment std::string GetAdjustTSText () const; 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/Lib/LTAPI/LTAPI.h b/Lib/LTAPI/LTAPI.h index d55f39ad..ce65f217 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. @@ -403,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 (); @@ -509,13 +563,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..5471eafa 160000 --- a/Lib/XPMP2 +++ b/Lib/XPMP2 @@ -1 +1 @@ -Subproject commit 89aba0ac319b985256c4b69b2bdcf97777b34200 +Subproject commit 5471eafa9835524cb89b7fb89cd4261ad0e06a59 diff --git a/LiveTraffic.xcodeproj/project.pbxproj b/LiveTraffic.xcodeproj/project.pbxproj index 88417f1a..a44babcc 100755 --- a/LiveTraffic.xcodeproj/project.pbxproj +++ b/LiveTraffic.xcodeproj/project.pbxproj @@ -834,8 +834,8 @@ LIBRARY_SEARCH_PATHS = Lib/fmod; LIVETRAFFIC_VERSION_BETA = 1; LIVETRAFFIC_VER_MAJOR = 4; - LIVETRAFFIC_VER_MINOR = 3; - LIVETRAFFIC_VER_PATCH = 5; + LIVETRAFFIC_VER_MINOR = 4; + LIVETRAFFIC_VER_PATCH = 0; LLVM_LTO = NO; MACH_O_TYPE = mh_dylib; MACOSX_DEPLOYMENT_TARGET = 10.15; @@ -955,8 +955,8 @@ LIBRARY_SEARCH_PATHS = Lib/fmod; LIVETRAFFIC_VERSION_BETA = 1; LIVETRAFFIC_VER_MAJOR = 4; - LIVETRAFFIC_VER_MINOR = 3; - LIVETRAFFIC_VER_PATCH = 5; + LIVETRAFFIC_VER_MINOR = 4; + LIVETRAFFIC_VER_PATCH = 0; LLVM_LTO = YES; MACH_O_TYPE = mh_dylib; MACOSX_DEPLOYMENT_TARGET = 10.15; diff --git a/LiveTraffic.xcodeproj/xcuserdata/birger.xcuserdatad/xcschemes/LiveTraffic.xcscheme b/LiveTraffic.xcodeproj/xcuserdata/birger.xcuserdatad/xcschemes/LiveTraffic.xcscheme index 81d9d04c..3cfcfd24 100644 --- a/LiveTraffic.xcodeproj/xcuserdata/birger.xcuserdatad/xcschemes/LiveTraffic.xcscheme +++ b/LiveTraffic.xcodeproj/xcuserdata/birger.xcuserdatad/xcschemes/LiveTraffic.xcscheme @@ -1,6 +1,6 @@ **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 bf3bce72..ab5e28ca 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 }, @@ -603,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 }, }; @@ -780,11 +783,11 @@ 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; - 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; @@ -1501,6 +1504,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]) @@ -1518,6 +1524,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*) { @@ -1591,6 +1611,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); } @@ -1752,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) { @@ -1876,6 +1896,72 @@ void DataRefs::GetLabelColor (float outColor[4]) const conv_color(labelColor, outColor); } +// 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; + } + else { + 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 = { 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)); + } + // 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 = CFG_SND_NO_DEVICE; // then it is "(no change)" + } + } + else { + // Disable sound altogether + XPMPSoundEnable(false); + } +} + // //MARK: Debug Options // @@ -2028,7 +2114,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 +2174,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 +2264,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; } @@ -2198,6 +2291,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) @@ -2348,6 +2443,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()) @@ -2471,7 +2568,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/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/LTAircraft.cpp b/Src/LTAircraft.cpp index 3422d617..c2a64b4e 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 @@ -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 @@ -2453,6 +2447,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 +2457,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 @@ -2707,6 +2710,7 @@ int LTAircraft::CameraCB (XPLMCameraPosition_t* outCameraPosition, { CameraRegisterCommands(false); pExtViewAc = nullptr; + dataRefs.SetCameraAc(nullptr); return 0; } @@ -2721,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; } @@ -2900,17 +2909,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 @@ -2989,7 +2998,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/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); + } } 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/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(); } } 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(); 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..3448295e 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; @@ -727,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"; @@ -919,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"; @@ -1292,18 +1324,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 +1421,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 +1454,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 +1807,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 +1845,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 +1888,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 +1963,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 *** @@ -1958,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); @@ -2027,7 +2094,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 +2110,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 { @@ -2114,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; @@ -2184,14 +2254,32 @@ 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) +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/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 680133a7..05fbac4b 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", @@ -440,7 +462,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 @@ -456,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", @@ -773,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, @@ -1145,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 699331fb..1b34c0f0 100755 --- a/docs/readme.html +++ b/docs/readme.html @@ -133,15 +133,50 @@

Release Notes

v4

-

v4.3.5

- +

v4.4.0

+

- 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.5:

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

Change log:

+ +
    +
  • Airplanes.live: New free traffic data channel, + see documentation. + On by default.
  • +
  • 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.
    • +
    +
  • +
  • + 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 + data source. It could be re-activated manually. +
  • +
  • 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: