FlightGear next
flarm.cxx
Go to the documentation of this file.
1/*
2 * SPDX-FileName: flarm.cxx
3 * SPDX-FileComment: Flarm protocol class
4 * SPDX-FileCopyrightText: Copyright (C) 2017 Thorsten Brehm - brehmt (at) gmail com
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8#include "config.h"
9
10#include <cstdlib>
11#include <cstring>
12#include <cstdio>
13
14#include <simgear/debug/logstream.hxx>
15#include <simgear/constants.h>
16#include <simgear/io/iochannel.hxx>
17#include <simgear/math/sg_geodesy.hxx>
18#include <simgear/sg_inlines.h>
19
21#include <Main/fg_props.hxx>
22#include <Main/globals.hxx>
23#include <flightgearBuildId.h>
24
25#include "flarm.hxx"
26
27using namespace NMEA;
28
29// #define FLARM_DEBUGGING
30
31#ifdef FLARM_DEBUGGING
32#warning Flarm debugging is enabled!
33#endif
34
35/*
36 * The Flarm protocol emulation reports multi-player and AI aircraft within the
37 * configured range using NMEA-style messages. The module supports bidirectional
38 * communication, i.e. its capable of sending messages, and also of receiving
39 * (and replying to) configuration commands. The emulation should be good enough
40 * to convince standard NAV/moving map clients (tablet APPs like skydemon, skymap,
41 * xcsoar etc) that they're connected to a Flarm device. The clients receive the
42 * GPS position and traffic information from the flight simulator.
43 *
44 * By default, the emulation reports all moving aircraft as targets of the lowest
45 * alert level ("traffic info only"). Parked aircraft are not reported.
46 *
47 * Higher alert levels (low-level/import/urgent alert) are only triggered
48 * when the FlightGear TCAS instrument is installed in the aircraft model (yes,
49 * Flarm is not TCAS, but reusing the TCAS threat-level for the emulation
50 * should be good enough :-) ). The TCAS instrument classifies all AI and MP
51 * aircraft into 4 threat levels (from invisible to high alert) - similar to
52 * the actual alert levels normally provided by a Flarm device.
53 *
54 * Supported NMEA messages:
55 * $GPRMC: own position information
56 *
57 * Supported Garmin proprietary messages:
58 * $PGRMZ: own barometric altitude information in feet
59 *
60 * Supported Flarm proprietary messages:
61 * $PFLAU: status and intruder data
62 * $PFLAA: data on targets within range
63 * $PFLAV: version information
64 * $PFLAE: device status information
65 * $PFLAC: configuration request
66 * $PFLAS: debug information
67 *
68 * Module properties:
69 * All Flarm configuration properties are mirrored to /sim/flarm/config.
70 * Useful properties:
71 * /sim/flarm/config/RANGE Range in meters when considering traffic targets
72 * /sim/flarm/config/NMEAOUT NMEA message mode (0=off, 1=all, 2=Garmin/NMEA messages only, 3=Flarm messages only)
73 * /sim/flarm/config/ACFT Aircraft type (1=glider, 3=helicopter, 8=motor aircraft, 9=jet)
74 *
75 */
76
78 FGGarmin(),
80 mFlarmConfig(fgGetNode("/sim/flarm/config", true)),
82{
83 // Flarm (and Garmin) devices normally report barometric altitude in feet
84 mMetric = false;
85 // disable all Garmin messages, except PGRMZ
87 // Allow processing more message lines per cycle than with FG's standard NMEA protocol,
88 // otherwise we won't reply quickly when a remote sends multiple configuration requests.
90 // allow bidirectional communication (we're sending data and accepting requests)
92
93#ifdef FLARM_DEBUGGING
94 // show I/O debug messages
95 sglog().set_log_classes(SG_IO);
96 sglog().set_log_priority(SG_DEBUG);
97#endif
98
99 // some default configuration data, to please XCSoar and other apps
100 const unsigned int zero=0;
101 setDefaultConfigValue("ACFT", 9);
102 setDefaultConfigValue("ADDWP", "");
103 setDefaultConfigValue("BAUD", 5);
104 setDefaultConfigValue("CFLAGS", zero);
105 setDefaultConfigValue("COMPID", "");
106 setDefaultConfigValue("COMPCLASS", "");
107 setDefaultConfigValue("COPIL", "");
108 setDefaultConfigValue("GLIDERID", "");
109 setDefaultConfigValue("GLIDERTYPE", "");
110 setDefaultConfigValue("ID", "0");
111 setDefaultConfigValue("LOGINT", 2);
112 setDefaultConfigValue("NEWTASK", "");
113 setDefaultConfigValue("NMEAOUT", 1); // all messages enabled
114 setDefaultConfigValue("NOTRACK", zero); // disabled
115 setDefaultConfigValue("PILOT", "Curt"); // :-)))
116 setDefaultConfigValue("PRIV", zero);
117 setDefaultConfigValue("RANGE", 25500);
118 setDefaultConfigValue("THRE", 2);
119 setDefaultConfigValue("UI", zero);
120}
121
122
125
126
127void FGFlarm::setDefaultConfigValue(const char* ConfigKey, const char* Value)
128{
129 if (!mFlarmConfig->hasValue(ConfigKey))
130 mFlarmConfig->setStringValue(ConfigKey, Value);
131}
132
133
134void FGFlarm::setDefaultConfigValue(const char* ConfigKey, unsigned int Value)
135{
136 if (!mFlarmConfig->hasValue(ConfigKey))
137 mFlarmConfig->setIntValue(ConfigKey, Value);
138}
139
140
141// generate Flarm NMEA messages
143{
144 // generate generic messages first
146
147 // traffic updates once per second only, independent of the normal protocol frequency
148 if ((get_count()-mLastUpdate)/get_hz() < 1.0)
149 {
150 return true;
151 }
153
154 char nmea[256];
155 int TargetCount=0;
156 double ClosestDistanceM2 = 99e9;
157 double ClosestLond=0.0, ClosestLatd=0.0;
158 int ClosestRelVerticalM=0;
159 int ClosestThreatLevel=-1;
160
161 // obtain own position
162 double latd = mFdm.get_Latitude() * SGD_RADIANS_TO_DEGREES;
163 double lond = mFdm.get_Longitude() * SGD_RADIANS_TO_DEGREES;
164
165 // PFLAA (Flarm proprietary)
167 {
168 double altitude_ft = mFdm.get_Altitude();
169
170 // check all AI/MP aircraft
171 SGPropertyNode* pAi = fgGetNode("/ai/models", true);
172 simgear::PropertyList aircraftList = pAi->getChildren("aircraft");
173 for (simgear::PropertyList::iterator i = aircraftList.begin(); i != aircraftList.end(); ++i)
174 {
175 SGPropertyNode* pModel = *i;
176 if ((pModel)&&(pModel->nChildren()))
177 {
178 double GroundSpeedKt = pModel->getDoubleValue("velocities/true-airspeed-kt", 0.0);
179 int threatLevel = pModel->getIntValue("tcas/threat-level", -99);
180 // threatLevel is undefined (-99) when no TCAS is installed
181 if (threatLevel == -99)
182 {
183 // set threat level to 0 (traffic info) when a/c is moving. Otherwise -1 (invisible).
184 threatLevel = (GroundSpeedKt>1) ? 0 : -1;
185 }
186
187 // report traffic, unless considered "invisible"
188 if (threatLevel >= 0)
189 {
190 // position data of current intruder
191 double targetLatd = pModel->getDoubleValue("position/latitude-deg", 0.0);
192 double targetLond = pModel->getDoubleValue("position/longitude-deg", 0.0);
193
194 // calculate the relative North and relative East distances in meters, as
195 // required by the Flarm protocol
196 double RelNorthAngleDeg = targetLatd - latd;
197 double RelNorth = ((2*SG_PI*SG_POLAR_RADIUS_M) / 360.0) * RelNorthAngleDeg;
198
199 double RelEastAngleDeg = targetLond - lond;
200 double RelEast = ((2*SG_PI*SG_EQUATORIAL_RADIUS_M) / 360.0) *
201 abs(cos(latd*SGD_DEGREES_TO_RADIANS)) * RelEastAngleDeg;
202
203#ifdef FLARM_DEBUGGING
204 {
205 double distanceM = sqrt(RelNorth*RelNorth+RelEast*RelEast);
206 pModel->setDoubleValue("flarm/distance", distanceM);
207 pModel->setIntValue("flarm/alive", pModel->getIntValue("flarm/alive",0)+1);
208 }
209#endif
210
211 // do not consider targets beyond 100km (1e5 meters)
212 double FlarmRangeM = mFlarmConfig->getIntValue("RANGE", 25500);
213 double DistanceM2 = RelNorth*RelNorth+RelEast*RelEast;
214 if (DistanceM2 < FlarmRangeM*FlarmRangeM)
215 {
216 TargetCount++;
217#if 0//def FLARM_DEBUGGING
218 {
219 double distanceM = sqrt(RelNorth*RelNorth+RelEast*RelEast);
220 printf("%3u: id %3u, %s, distance: %.1fkm, North: %.1f, East: %.1f, speed: %.1f kt\n",
221 pModel->getIndex(),
222 pModel->getIntValue("id"),
223 pModel->getStringValue("callsign", "<no name>"),
224 distanceM/1000.0, RelNorth/1e3, RelEast/1e3, GroundSpeedKt);
225 }
226#endif
227
228 int RelVerticalM = (pModel->getDoubleValue("position/altitude-ft", 0.0)-altitude_ft)* SG_FEET_TO_METER;
229 int Track = pModel->getDoubleValue("orientation/true-heading-deg", 0.0);
230 int ClimbRateMs = pModel->getDoubleValue("velocities/vertical-speed-fps", 0.0) * (SG_FPS_TO_KT * SG_KT_TO_MPS);
231 int AcftType = 9; // report as jet aircraft for now
232 // generate some fake 6-digit hex code
233 unsigned int ID = pModel->getIntValue("id") & 0x00FFFFFF;
234 //$PFLAA,AlarmLevel,RelNorth,RelEast,RelVertical,IDType,ID,Track,TurnRate,GroundSpeed,ClimbRate,AcftType
235 snprintf( nmea, 256, "$PFLAA,%u,%i,%i,%i,2,%06X,%u,,%i,%i,%u",
236 threatLevel, (int)RelNorth, (int)RelEast, RelVerticalM, ID,
237 Track, (int) (GroundSpeedKt*SG_KT_TO_MPS), ClimbRateMs, AcftType);
238 add_with_checksum(nmea, 256);
239
240 if (DistanceM2 < ClosestDistanceM2)
241 {
242 ClosestDistanceM2 = DistanceM2;
243 ClosestThreatLevel = threatLevel;
244 ClosestRelVerticalM = RelVerticalM;
245 ClosestLatd = targetLatd;
246 ClosestLond = targetLond;
247 }
248 }
249 }
250 }
251 }
252 }
253
254 // PFLAU (Flarm proprietary)
256 {
257 if (ClosestThreatLevel < 0)
258 {
259 // no threats, but maybe some targets
260 snprintf( nmea, 256, "$PFLAU,%u,1,1,1,0,,0,,", TargetCount);
261 }
262 else
263 {
264 // calculate the bearing and range of the closest target
265 double az2, bearing, distanceM;
266 geo_inverse_wgs_84(latd, lond, ClosestLatd, ClosestLond, &bearing, &az2, &distanceM);
267
268 // calculate relative bearing
269 double heading = mFdm.get_Psi_deg();
270 bearing -= heading;
271 SG_NORMALIZE_RANGE(bearing, -180.0, 180.0);
272
273 // set alert mode, depending on TCAS threat level
274 int AlertMode = 0; // no alert
275 if (ClosestThreatLevel >= 2) // TCAS RA alert
276 AlertMode = 2; // alarm!
277 else
278 if (ClosestThreatLevel == 1) // TCAS proximity alert
279 AlertMode = 1; // warning
280 snprintf( nmea, 256, "$PFLAU,%u,1,1,1,%u,%.0f,%u,%u,%u",
281 TargetCount, ClosestThreatLevel, bearing, AlertMode, ClosestRelVerticalM, (unsigned int) distanceM);
282 }
283 add_with_checksum(nmea, 256);
284 }
285
286 return true;
287}
288
289// process a Flarm sentence
290void FGFlarm::parse_message(const std::vector<std::string>& tokens)
291{
292 char nmea[256];
293
294 if ( tokens[0] == "PFLAE" )
295 {
296 if (tokens.size()<2)
297 return;
298
299 // #1: request
300 const std::string& request = tokens[1];
301 SG_LOG( SG_IO, SG_DEBUG, " PFLAE request = " << request );
302 if (request == "R")
303 {
304 // report "no errors"
305 snprintf( nmea, 256, "$PFLAE,A,0,0");
306 add_with_checksum(nmea, 256);
307 SG_LOG( SG_IO, SG_DEBUG, " PFLAE reply= " << nmea );
308 }
309 }
310 else
311 if ( tokens[0] == "PFLAV" )
312 {
313 if (tokens.size()<2)
314 return;
315
316 // #1: request
317 const std::string& request = tokens[1];
318 SG_LOG( SG_IO, SG_DEBUG, " PFLAV version request = " << request );
319 if (request == "R")
320 {
321 // report some fixed version to please the requesting device
322 snprintf( nmea, 256, "$PFLAV,A,2.00,6.00,");
323 add_with_checksum(nmea, 256);
324 SG_LOG( SG_IO, SG_DEBUG, " PFLAV reply= " << nmea );
325 }
326 }
327 else
328 if ( tokens[0] == "PFLAS" )
329 {
330 // status/debug information
331
332 if (tokens.size()<2)
333 return;
334
335 // #1: request
336 const std::string& request = tokens[1];
337 SG_LOG( SG_IO, SG_DEBUG, " PFLAS status/debug request = " << request );
338 if (request == "R")
339 {
340 // Report some debug data to please the requesting device.
341 // Apparently debug replies are not in NMEA format and contain plain text.
342 const char* FlrmDebugReply = (
343 "------------------------------------------\r\n"
344 "FlightGear " FLIGHTGEAR_VERSION "\r\n"
345 "Revision " REVISION "\r\n"
346 "------------------------------------------\r\n");
347 mNmeaSentence += FlrmDebugReply;
348 SG_LOG( SG_IO, SG_DEBUG, " PFLAS reply = " << FlrmDebugReply );
349 }
350 }
351 else
352 if ( tokens[0] == "PFLAC" )
353 {
354 // configuration command
355
356 if (tokens.size()<3)
357 return;
358
359 bool Error = true;
360
361 // #1: request
362 const std::string& request = tokens[1];
363
364 SG_LOG( SG_IO, SG_DEBUG, " PFLAC config request = " << request );
365
366 // #2: keyword
367 const std::string& keyword = tokens[2];
368 SG_LOG( SG_IO, SG_DEBUG, " PFLAC config request = " << keyword );
369
370 // check if the config element is supported
371 SGPropertyNode* configNode = mFlarmConfig->getChild(keyword,0,false);
372 if (!configNode)
373 {
374 // unsupported configuration element
375 }
376 else
377 if (request == "R")
378 {
379 // reply with config data
380 snprintf( nmea, 256, "$PFLAC,A,%s,%s",
381 keyword.c_str(), configNode->getStringValue().c_str());
382 add_with_checksum(nmea, 256);
383 Error = false;
384 }
385 else
386 if (request == "S")
387 {
388 if (tokens.size()<4)
389 return;
390 Error = false;
391
392 // special handling for some parameters
393 if (keyword == "NMEAOUT")
394 {
395 // #3: value
396 int value = (int) atof(tokens[3].c_str());
397 switch(value % 10)
398 {
399 case 0:
400 // disable all periodic messages
401 mNmeaMessages = 0;
402 mGarminMessages = 0;
403 mFlarmMessages = 0;
404 break;
405 case 1:
406 // enable all periodic messages
410 break;
411 case 2:
412 // enable all, except periodic Flarm messages
415 mFlarmMessages = 0;
416 break;
417 case 3:
418 // enable all, except periodic NMEA messages
419 mNmeaMessages = 0;
422 break;
423 default:
424 Error = true;
425 }
426 }
427
428 if (!Error)
429 {
430 // just store the config data as it is
431 configNode->setStringValue(tokens[3]);
432
433 // reply
434 snprintf( nmea, 256, "$PFLAC,A,%s,%s",
435 keyword.c_str(), configNode->getStringValue().c_str());
436 add_with_checksum(nmea, 256);
437 }
438 }
439
440 if (Error)
441 {
442 // report error for unsupported requests
443 snprintf( nmea, 256, "$PFLAC,A,ERROR");
444 add_with_checksum(nmea, 256);
445 }
446 }
447 else
448 {
449 // Invalid or unsupported message.
450 // In flarm mode, we only accept flarm messages as input, but no other messages, i.e. no position updates.
451 // Use the Garmin or NMEA basic protocols to feed position data into FG.
452 SG_LOG( SG_IO, SG_DEBUG, " Unsupported message = " << tokens[0] );
453 }
454}
#define i(x)
FGFlarm()
Definition flarm.cxx:77
unsigned int mFlarmMessages
Definition flarm.hxx:41
void setDefaultConfigValue(const char *ConfigKey, const char *Value)
Definition flarm.cxx:127
unsigned long mLastUpdate
Definition flarm.hxx:43
SGPropertyNode_ptr mFlarmConfig
Definition flarm.hxx:42
virtual bool gen_message()
Definition flarm.cxx:142
~FGFlarm()
Definition flarm.cxx:123
unsigned int mGarminMessages
Definition garmin.hxx:42
FGGarmin()
Definition garmin.cxx:34
virtual bool gen_message()
Definition garmin.cxx:56
bool mMetric
Definition garmin.hxx:43
unsigned int mMaxReceiveLines
Definition nmea.hxx:49
FlightProperties mFdm
Definition nmea.hxx:51
void add_with_checksum(char *sentence, unsigned int buf_size)
Definition nmea.cxx:43
std::string mNmeaSentence
Definition nmea.hxx:53
bool mBiDirectionalSupport
Definition nmea.hxx:50
unsigned int mNmeaMessages
Definition nmea.hxx:48
double get_hz() const
Definition protocol.hxx:68
unsigned long get_count()
Definition protocol.hxx:75
virtual bool parse_message()
Definition protocol.cxx:105
const unsigned int PFLAA
Definition flarm.hxx:34
const unsigned int PFLAU
Definition flarm.hxx:33
const unsigned int SET
Definition flarm.hxx:35
const unsigned int PGRMZ
Definition garmin.hxx:33
Definition flarm.hxx:29
const unsigned int SET
Definition nmea.hxx:40
static double atof(const string &str)
Definition options.cxx:107
SGPropertyNode * fgGetNode(const char *path, bool create)
Get a property node.
Definition proptest.cpp:27