FlightGear next
flightrecorder.cxx
Go to the documentation of this file.
1/*
2 * SPDX-FileName: flightrecorder.cxx
3 * SPDX-FileCopyrightText: Copyright (C) 2011 Thorsten Brehm - brehmt (at) gmail com
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7#ifdef HAVE_CONFIG_H
8#include "config.h"
9#endif
10
11#include <assert.h>
12#include <stdio.h>
13#include <string.h>
14
15#include <Main/fg_props.hxx>
18#include <simgear/debug/logstream.hxx>
19#include <simgear/math/SGMath.hxx>
20#include <simgear/misc/ResourceManager.hxx>
21#include <simgear/misc/strutils.hxx>
22#include <simgear/props/props_io.hxx>
23#include <simgear/structure/exception.hxx>
24
25#include "flightrecorder.hxx"
26
27using namespace FlightRecorder;
28using std::string;
29
30
31// Appends raw data to <out>.
32static void s_Append(std::vector<char>& out, const void* data, size_t data_len)
33{
34 size_t pos = out.size();
35 out.resize(pos + data_len);
36 memcpy(&out[pos], data, data_len);
37}
38
39// Appends int16 number to <out>.
40static void s_AppendNumber(std::vector<char>& out, int16_t number)
41{
42 s_Append(out, &number, sizeof(number));
43}
44
45// Appends string to <out> as int16 length followed by chars.
46static void s_AppendString(std::vector<char>& out, const std::string& s)
47{
48 s_AppendNumber(out, s.size());
49 s_Append(out, s.c_str(), s.size());
50}
51
52// Updates property <b> so that it is identical to property <a> and writes
53// information on all differences (creation, value changes and deletion) to
54// <out>.
55//
56// We call ourselves recursively for child nodes.
57//
58// We are careful to preserve indices.
59//
60// We treat all property values as text.
61//
63 std::vector<char>& out,
64 SGPropertyNode* a,
65 SGPropertyNode* b,
66 const std::vector<const char*>* path_exclude_prefixes)
67{
68 assert(a);
69 assert(b);
70 assert(a->getNameString() == b->getNameString());
71 assert(a->getPath() == b->getPath());
72 assert(a->getIndex() == b->getIndex());
73
74 if (path_exclude_prefixes) {
75 for (const char* path_exclude_prefix : *path_exclude_prefixes) {
76 if (simgear::strutils::starts_with(a->getPath(true /*simplify*/), path_exclude_prefix)) {
77 SG_LOG(SG_SYSTEMS, SG_BULK, "Ignoring: " << a->getPath(true /*simplify*/));
78 return;
79 }
80 }
81 }
82 // If values differ, write a's value to out and change b's value to a's value.
83 const std::string a_value = a->getStringValue();
84 const std::string b_value = b->getStringValue();
85 if (a_value != b_value) {
86 // Values are different so write out node <a> and update b.
87 SG_LOG(SG_SYSTEMS, SG_DEBUG, "recording property change:" << a->getPath() << ": " << b->getStringValue() << " => " << a->getStringValue());
88 s_AppendString(out, a->getPath(true /*simplify*/));
89 s_AppendString(out, a->getStringValue());
90 b->setStringValue(a_value);
91 }
92
93 // Look at all child nodes of <b>, removing any that are not in <a>.
94 int bn = b->nChildren();
95 for (int i = 0; i < bn; ++i) {
96 SGPropertyNode* bc = b->getChild(i);
97 SGPropertyNode* ac = a->getChild(bc->getNameString(), bc->getIndex(), false /*create*/);
98 if (!ac) {
99 // Child node is in b but not in a; we write out special
100 // information about the deleted node and remove from b.
101 s_AppendString(out, "");
102 s_AppendString(out, bc->getPath(true /*simplify*/));
103 b->removeChild(bc);
104 }
105 }
106
107 // Look at all child nodes of <a>, copying across to <b> as required.
108 //
109 int an = a->nChildren();
110 for (int i = 0; i < an; ++i) {
111 SGPropertyNode* ac = a->getChild(i);
112 SGPropertyNode* bc = b->getChild(ac->getNameString(), ac->getIndex(), true /*create*/);
113 // Recurse.
114 s_RecordPropertyDiffs(out, ac, bc, path_exclude_prefixes);
115 }
116}
117
118
119// Takes care of writing extra-properties to FGReplayData if we are doing a
120// Continuous recording with extra-properties.
121//
122// We write different properties depending on properties such as
123// /sim/replay/record-main-view.
124//
127 : m_record_extra_properties(fgGetNode("/sim/replay/record-extra-properties", true /*create*/)),
128 m_record_main_window(fgGetNode("/sim/replay/record-main-window", true /*create*/)),
129 m_record_main_view(fgGetNode("/sim/replay/record-main-view", true /*create*/)),
130 m_record_extra_properties_paths(fgGetNode("/sim/replay/record-extra-properties-paths", true /*create*/))
131 {
132 }
133
134 // Added info about property changes to *ReplayData if we are recording
135 // them.
136 //
137 void capture(SGPropertyNode_ptr RecordExtraPropertiesReference, FGReplayData* ReplayData)
138 {
139 if (m_record_extra_properties->getBoolValue()) {
140 // Record extra properties specified in
141 // /sim/replay/record-extra-properties-paths/path[].
142 //
143 auto paths = m_record_extra_properties_paths->getChildren("path");
144 for (SGPropertyNode* n : paths) {
145 // We don't try to handle indices or deletion for top-level
146 // property node.
147 //
148 std::string path = n->getStringValue();
149 SGPropertyNode* a = globals->get_props()->getNode(path, false /*create*/);
150 if (!a) {
151 SG_LOG(SG_SYSTEMS, SG_ALERT, "property does not exist: " << path);
152 continue;
153 }
154 SGPropertyNode* b = RecordExtraPropertiesReference->getNode(path, true /*create*/);
155 s_RecordPropertyDiffs(ReplayData->extra_properties, a, b, NULL /*path_exclude_prefixes*/);
156 }
157 }
158
159 if (m_record_main_window->getBoolValue()) {
160 // Record size/position of main window.
161 //
162 static std::vector<const char*> s_paths = {
163 "sim/startup/xpos",
164 "sim/startup/ypos",
165 "sim/startup/xsize",
166 "sim/startup/ysize"};
167 for (const char* path : s_paths) {
168 SGPropertyNode* a = globals->get_props()->getNode(path, true /*create*/);
169 SGPropertyNode* b = RecordExtraPropertiesReference->getNode(path, true /*create*/);
170 s_RecordPropertyDiffs(ReplayData->extra_properties, a, b, NULL /*path_exclude_prefixes*/);
171 }
172 }
173
174 if (m_record_main_view->getBoolValue()) {
175 // Record main window view.
176 //
177 static std::vector<const char*> s_excludes = {
178 "/sim/current-view/debug/",
179 "/sim/current-view/raw-orientation",
180 "/sim/current-view/viewer-"};
181 const char* path = "sim/current-view";
182 SGPropertyNode* a = globals->get_props()->getNode(path, true /*create*/);
183 SGPropertyNode* b = RecordExtraPropertiesReference->getNode(path, true /*create*/);
184 s_RecordPropertyDiffs(ReplayData->extra_properties, a, b, &s_excludes);
185 }
186 }
187
188private:
189 SGPropertyNode_ptr m_record_extra_properties;
190 SGPropertyNode_ptr m_record_main_window;
191 SGPropertyNode_ptr m_record_main_view;
192 SGPropertyNode_ptr m_record_extra_properties_paths;
193};
194
195static std::shared_ptr<RecordExtraProperties> s_record_extra_properties;
196
197
198FGFlightRecorder::FGFlightRecorder(const char* pConfigName) : m_RecorderNode(fgGetNode("/sim/flight-recorder", true)),
199 m_ReplayMultiplayer(fgGetNode("/sim/replay/multiplayer", true)),
200 m_ReplayExtraProperties(fgGetNode("/sim/replay/replay-extra-properties", true)),
201 m_ReplayMainView(fgGetNode("/sim/replay/replay-main-view", true)),
202 m_ReplayMainWindowPosition(fgGetNode("/sim/replay/replay-main-window-position", true)),
203 m_ReplayMainWindowSize(fgGetNode("/sim/replay/replay-main-window-size", true)),
204 m_RecordContinuous(fgGetNode("/sim/replay/record-continuous", true)),
205 m_RecordExtraProperties(fgGetNode("/sim/replay/record-extra-properties", true)),
206 m_LogRawSpeed(fgGetNode("/sim/replay/log-raw-speed", true)),
207 m_TotalRecordSize(0),
208 m_ConfigName(pConfigName),
209 m_usingDefaultConfig(false),
210 m_MultiplayMgr(globals->get_subsystem<FGMultiplayMgr>())
211{
212}
213
218
220{
221 m_ConfigNode = 0;
222 m_usingDefaultConfig = false;
223
224 SGPropertyNode_ptr ConfigNode;
225 int Selected = m_RecorderNode->getIntValue(m_ConfigName, 0);
226 SG_LOG(SG_SYSTEMS, SG_INFO, "FlightRecorder: Recorder configuration #" << std::to_string(Selected));
227 if (Selected >= 0)
228 ConfigNode = m_RecorderNode->getChild("config", Selected);
229
230 if (!ConfigNode.valid())
231 ConfigNode = getDefault();
232
233 reinit(ConfigNode);
234}
235
236void FGFlightRecorder::reinit(SGPropertyNode_ptr ConfigNode)
237{
238 m_TotalRecordSize = 0;
239
240 m_CaptureDouble.clear();
241 m_CaptureFloat.clear();
242 m_CaptureInteger.clear();
243 m_CaptureInt16.clear();
244 m_CaptureInt8.clear();
245 m_CaptureBool.clear();
246
247 m_ConfigNode = ConfigNode;
248
249 if (!m_ConfigNode.valid()) {
250 SG_LOG(SG_SYSTEMS, SG_ALERT, "FlightRecorder: Configuration is invalid. Flight recorder disabled.");
251 } else {
252 // set name of active flight recorder type
253 const std::string pRecorderName =
254 m_ConfigNode->getStringValue("name",
255 "aircraft-specific flight recorder");
256 SG_LOG(SG_SYSTEMS, SG_INFO, "FlightRecorder: Using custom recorder configuration: " << pRecorderName);
257 m_RecorderNode->setStringValue("active-config-name", pRecorderName);
258
259 // get signals
260 initSignalList("double", m_CaptureDouble, m_ConfigNode);
261 initSignalList("float", m_CaptureFloat, m_ConfigNode);
262 initSignalList("int", m_CaptureInteger, m_ConfigNode);
263 initSignalList("int16", m_CaptureInt16, m_ConfigNode);
264 initSignalList("int8", m_CaptureInt8, m_ConfigNode);
265 initSignalList("bool", m_CaptureBool, m_ConfigNode);
266 }
267
268 // calculate size of a single record
269 m_TotalRecordSize = sizeof(double) * 1 /* sim time */ +
270 sizeof(double) * m_CaptureDouble.size() +
271 sizeof(float) * m_CaptureFloat.size() +
272 sizeof(int) * m_CaptureInteger.size() +
273 sizeof(short int) * m_CaptureInt16.size() +
274 sizeof(signed char) * m_CaptureInt8.size() +
275 sizeof(unsigned char) * ((m_CaptureBool.size() + 7) / 8); // 8 bools per byte
276
277 // expose size of actual flight recorder record
278 m_RecorderNode->setIntValue("record-size", m_TotalRecordSize);
279 SG_LOG(SG_SYSTEMS, SG_INFO, "FlightRecorder: record size is " << m_TotalRecordSize << " bytes");
280
282}
283
285bool FGFlightRecorder::haveProperty(FlightRecorder::TSignalList& SignalList, const SGPropertyNode* pProperty)
286{
287 unsigned int Count = SignalList.size();
288 for (unsigned int i = 0; i < Count; i++) {
289 if (SignalList[i].Signal.get() == pProperty) {
290 return true;
291 }
292 }
293 return false;
294}
295
297bool FGFlightRecorder::haveProperty(const SGPropertyNode* pProperty)
298{
299 if (haveProperty(m_CaptureDouble, pProperty))
300 return true;
301 if (haveProperty(m_CaptureFloat, pProperty))
302 return true;
303 if (haveProperty(m_CaptureInteger, pProperty))
304 return true;
305 if (haveProperty(m_CaptureInt16, pProperty))
306 return true;
307 if (haveProperty(m_CaptureInt8, pProperty))
308 return true;
309 if (haveProperty(m_CaptureBool, pProperty))
310 return true;
311 return false;
312}
313
316SGPropertyNode_ptr
317FGFlightRecorder::getDefault()
318{
319 SGPropertyNode_ptr ConfigNode;
320
321 // set name of active flight recorder type
322 SG_LOG(SG_SYSTEMS, SG_INFO, "FlightRecorder: No custom configuration. Loading generic default recorder.");
323
324 const std::string Path = m_RecorderNode->getStringValue("default-config", "");
325 if (Path.empty()) {
326 SG_LOG(SG_SYSTEMS, SG_ALERT, "FlightRecorder: No default flight recorder specified! Check defaults.xml!");
327 } else {
328 SGPath path = globals->resolve_aircraft_path(Path);
329 if (path.isNull()) {
330 SG_LOG(SG_SYSTEMS, SG_ALERT, "FlightRecorder: Cannot find file '" << Path << "'.");
331 } else {
332 try {
333 readProperties(path, m_RecorderNode->getChild("config", 0, true), 0);
334 ConfigNode = m_RecorderNode->getChild("config", 0, false);
335 } catch (sg_io_exception& e) {
336 SG_LOG(SG_SYSTEMS, SG_ALERT, "FlightRecorder: Error reading file '" << Path << ": " << e.getFormattedMessage());
337 }
338 }
339 }
340
341 m_usingDefaultConfig = true;
342 return ConfigNode;
343}
344
349void FGFlightRecorder::initSignalList(const char* pSignalType, TSignalList& SignalList, SGPropertyNode_ptr BaseNode)
350{
351 // clear old signals
352 SignalList.clear();
353
354 processSignalList(pSignalType, SignalList, BaseNode);
355
356 SG_LOG(SG_SYSTEMS, SG_DEBUG, "FlightRecorder: " << SignalList.size() << " signals of type " << pSignalType);
357}
358
363void FGFlightRecorder::processSignalList(const char* pSignalType, TSignalList& SignalList, SGPropertyNode_ptr SignalListNode,
364 string PropPrefix, int Count)
365{
366 // get the list of signal sources (property paths) for this signal type
367 SGPropertyNode_ptr SignalNode;
368 int Index = 0;
369
370 Count = SignalListNode->getIntValue("count", Count);
371 PropPrefix = simgear::strutils::strip(SignalListNode->getStringValue("prefix", PropPrefix.c_str()));
372 if ((!PropPrefix.empty()) && (PropPrefix[PropPrefix.size() - 1] != '/'))
373 PropPrefix += "/";
374
375 do {
376 SignalNode = SignalListNode->getChild("signal", Index, false);
377 if (SignalNode.valid() &&
378 (std::string(pSignalType) == SignalNode->getStringValue("type", "float"))) {
379 string PropertyPath = SignalNode->getStringValue("property", "");
380 if (!PropertyPath.empty()) {
381 PropertyPath = PropPrefix + PropertyPath;
382 const std::string pInterpolation = SignalNode->getStringValue("interpolation", "linear");
383
384 // Check if current signal has a "%i" place holder. Otherwise count is 1.
385 string::size_type IndexPos = PropertyPath.find("%i");
386 int SignalCount = Count;
387 if (IndexPos == string::npos)
388 SignalCount = 1;
389
390 for (int IndexValue = 0; IndexValue < SignalCount; IndexValue++) {
391 string PPath = PropertyPath;
392 if (IndexPos != string::npos) {
393 char strbuf[20];
394 snprintf(strbuf, 20, "%d", IndexValue);
395 PPath = PPath.replace(IndexPos, 2, strbuf);
396 }
397 TCapture Capture;
398 Capture.Signal = fgGetNode(PPath.c_str(), false);
399 if (!Capture.Signal.valid()) {
400 // JMT - only warn if using a custom config, not the default one. Since the generic config of
401 // course requests many props, but we do want this warning for custom configs tailored to the
402 // aircraft model.
403 if (!m_usingDefaultConfig) {
404 // warn user: we're maybe going to record useless data
405 // Or maybe the data is only initialized later. Warn anyway, so we can catch useless data.
406 SG_LOG(SG_SYSTEMS, SG_INFO, "FlightRecorder: Recording non-existent property '" << PPath << "'.");
407 }
408
409 Capture.Signal = fgGetNode(PPath.c_str(), true);
410 }
411
412 if (pInterpolation == "discrete")
413 Capture.Interpolation = discrete;
414 else if ((pInterpolation == "angular") ||
415 (pInterpolation == "angular-rad"))
416 Capture.Interpolation = angular_rad;
417 else if (pInterpolation == "angular-deg")
418 Capture.Interpolation = angular_deg;
419 else if (pInterpolation == "linear")
420 Capture.Interpolation = linear;
421 else {
422 SG_LOG(SG_SYSTEMS, SG_ALERT, "FlightRecorder: Unsupported interpolation type '" << pInterpolation << "' of signal '" << PPath << "'");
423 Capture.Interpolation = linear;
424 }
425 if (haveProperty(Capture.Signal)) {
426 SG_LOG(SG_SYSTEMS, SG_ALERT, "FlightRecorder: Property '" << PPath << "' specified multiple times. Check flight recorder configuration.");
427 } else
428 SignalList.push_back(Capture);
429 }
430 }
431 }
432 Index++;
433 } while (SignalNode.valid());
434
435 // allow recursive definition of signal lists
436 simgear::PropertyList Nodes = SignalListNode->getChildren("signals");
437 for (unsigned int i = 0; i < Nodes.size(); i++) {
438 processSignalList(pSignalType, SignalList, Nodes[i], PropPrefix, Count);
439 }
440}
441
447FGFlightRecorder::capture(double SimTime, FGReplayData* ReplayData)
448{
449 if (!ReplayData) {
450 ReplayData = new FGReplayData;
451 if (!ReplayData)
452 return NULL;
453 }
454
455 // When replaying, we are able to carry recording live multiplayer
456 // information. To make this work we need to record a stationary
457 // user aircraft, with information from the last live user aircraft
458 // FGReplayData. So we store the last live user aircraft information in
459 // this static vector.
460 //
461 static std::vector<char> s_recent_raw_data;
462
463 int in_replay = fgGetInt("/sim/replay/replay-state");
464
465 ReplayData->sim_time = SimTime;
466
467 if (in_replay && !s_recent_raw_data.empty()) {
468 // Record the fixed position of live user aircraft at the point at
469 // which we started replay.
470 //
471 ReplayData->raw_data = s_recent_raw_data;
472 } else {
473 // Find live information about the user aircraft.
474 //
475 int Offset = 0;
476 ReplayData->raw_data.resize(m_TotalRecordSize);
477 char* pBuffer = &ReplayData->raw_data.front();
478
479 // 64bit aligned data first!
480 {
481 // capture doubles
482 double* pDoubles = (double*)&pBuffer[Offset];
483 unsigned int SignalCount = m_CaptureDouble.size();
484 for (unsigned int i = 0; i < SignalCount; i++) {
485 pDoubles[i] = m_CaptureDouble[i].Signal->getDoubleValue();
486 }
487 Offset += SignalCount * sizeof(double);
488 }
489
490 // 32bit aligned data comes second...
491 {
492 // capture floats
493 float* pFloats = (float*)&pBuffer[Offset];
494 unsigned int SignalCount = m_CaptureFloat.size();
495 for (unsigned int i = 0; i < SignalCount; i++) {
496 pFloats[i] = m_CaptureFloat[i].Signal->getFloatValue();
497 }
498 Offset += SignalCount * sizeof(float);
499 }
500
501 {
502 // capture integers (32bit aligned)
503 int* pInt = (int*)&pBuffer[Offset];
504 unsigned int SignalCount = m_CaptureInteger.size();
505 for (unsigned int i = 0; i < SignalCount; i++) {
506 pInt[i] = m_CaptureInteger[i].Signal->getIntValue();
507 }
508 Offset += SignalCount * sizeof(int);
509 }
510
511 // 16bit aligned data is next...
512 {
513 // capture 16bit short integers
514 short int* pShortInt = (short int*)&pBuffer[Offset];
515 unsigned int SignalCount = m_CaptureInt16.size();
516 for (unsigned int i = 0; i < SignalCount; i++) {
517 pShortInt[i] = (short int)m_CaptureInt16[i].Signal->getIntValue();
518 }
519 Offset += SignalCount * sizeof(short int);
520 }
521
522 // finally: byte aligned data is last...
523 {
524 // capture 8bit chars
525 signed char* pChar = (signed char*)&pBuffer[Offset];
526 unsigned int SignalCount = m_CaptureInt8.size();
527 for (unsigned int i = 0; i < SignalCount; i++) {
528 pChar[i] = (signed char)m_CaptureInt8[i].Signal->getIntValue();
529 }
530 Offset += SignalCount * sizeof(signed char);
531 }
532
533 {
534 // capture 1bit booleans (8bit aligned)
535 unsigned char* pFlags = (unsigned char*)&pBuffer[Offset];
536 unsigned int SignalCount = m_CaptureBool.size();
537 int Size = (SignalCount + 7) / 8;
538 Offset += Size;
539 memset(pFlags, 0, Size);
540 for (unsigned int i = 0; i < SignalCount; i++) {
541 if (m_CaptureBool[i].Signal->getBoolValue())
542 pFlags[i >> 3] |= 1 << (i & 7);
543 }
544 }
545
546 assert(Offset + sizeof(double) == m_TotalRecordSize);
547
548 // Update s_recent_raw_data so that we will be able to carry recording
549 // while replaying.
550 //
551 s_recent_raw_data = ReplayData->raw_data;
552 }
553
554 // If m_ReplayMultiplayer is true, move all recent
555 // multiplayer messages from m_MultiplayMgr into
556 // ReplayData->multiplayer_messages. Otherwise clear m_MultiplayMgr's
557 // list of recent messages.
558 //
559 ReplayData->multiplayer_messages.clear();
560 bool replayMultiplayer = m_ReplayMultiplayer->getBoolValue();
561 for (;;) {
562 auto multiplayerMessage = m_MultiplayMgr->popMessageHistory();
563 if (!multiplayerMessage) {
564 break;
565 }
566 if (replayMultiplayer) {
567 // Attempt to ignore chat messages. Unfortunately it seems
568 // that CHAT_MSG_ID is unused, and instead chat messages are
569 // included in POS_DATA_ID messages as sim/multiplay/chat in
570 // src/MultiPlayer/multiplaymgr.cxx's sIdPropertyList.
571 //
572 auto message_header = reinterpret_cast<const T_MsgHdr*>(&multiplayerMessage->front());
573 xdr_data_t message_id = message_header->MsgId;
574 if (message_id == CHAT_MSG_ID) {
575 SG_LOG(SG_GENERAL, SG_ALERT, "Not recording chat message: " << std::string(reinterpret_cast<const T_ChatMsg*>(message_header + 1)->Text, message_header->MsgLen - sizeof(*message_header)));
576 } else {
577 ReplayData->multiplayer_messages.push_back(multiplayerMessage);
578 }
579 }
580 }
581
582 // Add extra properties if we are doing continuous recording.
583 //
584 if (m_RecordContinuous->getBoolValue()) {
585 if (!m_RecordExtraPropertiesReference) {
586 SG_LOG(SG_SYSTEMS, SG_DEBUG, "m_RecordPropertiesReference is null");
587 m_RecordExtraPropertiesReference = new SGPropertyNode;
588 }
589 s_record_extra_properties->capture(m_RecordExtraPropertiesReference, ReplayData);
590 }
591
592 ReplayData->UpdateStats();
593
594 // Note that if we are replaying, <ReplayData> will contain the last live
595 // position of the live user aircraft, plus any multiplayer packets that
596 // have arrived recently. If there were no recent multiplayer packets,
597 // then there's no actually no need to record anything (unless we're at the
598 // start of recording).
599 //
600 // But for now at least, we don't try to optimize the recording like that.
601 //
602
603 return ReplayData;
604}
605
607static double
608weighting(TInterpolation interpolation, double ratio, double v1, double v2)
609{
610 switch (interpolation) {
611 case linear:
612 return v1 + ratio * (v2 - v1);
613
614 case angular_deg: {
615 // special handling of angular data
616 double tmp = v2 - v1;
617 if (tmp > 180)
618 tmp -= 360;
619 else if (tmp < -180)
620 tmp += 360;
621 return v1 + tmp * ratio;
622 }
623
624 case angular_rad: {
625 // special handling of angular data
626 double tmp = v2 - v1;
627 if (tmp > SGD_PI)
628 tmp -= SGD_2PI;
629 else if (tmp < -SGD_PI)
630 tmp += SGD_2PI;
631 return v1 + tmp * ratio;
632 }
633
634 case discrete:
635 // fall through
636 default:
637 return v2;
638 }
639}
640
642{
643 SG_LOG(SG_SYSTEMS, SG_DEBUG, "Clearing m_RecordExtraPropertiesReference");
644 m_RecordExtraPropertiesReference = nullptr;
645}
646
647// Converts string to int, ignoring errors and doing nothing if out-para <out>
648// is null.
649static void setInt(const std::string& value, int* out)
650{
651 if (!out) return;
652 try {
653 *out = std::stoi(value);
654 } catch (std::exception& e) {
655 SG_LOG(SG_SYSTEMS, SG_ALERT, "Ignoring failed conversion of '" << value << "' to int: " << e.what());
656 }
657}
658
659/* Converts string to double, writing to out-para <out> and setting <ok> to
660true. If conversion fails or fails to use the entire input string, sets ok to
661false. */
662static void string_to_double(const std::string& s, double& out, bool& ok)
663{
664 errno = 0;
665 char* end;
666 out = strtod(s.c_str(), &end);
667 if (errno || !end || *end != 0) {
668 ok = false;
669 return;
670 }
671 ok = true;
672}
673
674/* Updates property <path> to <value_next_string>. If property is also in
675<frame_prev> and values look like floating point, we interpolate using <ratio>.
676*/
677static void replayProperty(
678 const std::string& path,
679 const std::string& value_next_string,
680 const FGReplayData* frame_prev,
681 double ratio)
682{
683 SGPropertyNode* p = globals->get_props()->getNode(path, true /*create*/);
684 bool done = false;
685 if (frame_prev) {
686 /* Check whether <path> is in frame_prev's list of property changes. */
687 SG_LOG(SG_SYSTEMS, SG_DEBUG, "p && _pLastBuffer");
688 auto p_prev_it = frame_prev->replay_extra_property_changes.find(path);
689 if (p_prev_it != frame_prev->replay_extra_property_changes.end()) {
690 /* Property <path> is also in frame_prev. */
691 SG_LOG(SG_SYSTEMS, SG_DEBUG, "property in frame_prev and frame_next:"
692 << " " << path);
693 const std::string& value_prev_string = p_prev_it->second;
694 bool value_prev_ok;
695 bool value_next_ok;
696 double value_prev;
697 double value_next;
698 string_to_double(value_prev_string, value_prev, value_prev_ok);
699 string_to_double(value_next_string, value_next, value_next_ok);
700 if (value_prev_ok && value_next_ok) {
701 /* Both values look like floating point so we interpolate. */
702 SG_LOG(SG_SYSTEMS, SG_DEBUG, "property is fp");
704 if (simgear::strutils::ends_with(path, "-deg")) {
705 interpolation = TInterpolation::angular_deg;
706 } else if (simgear::strutils::ends_with(path, "-rad")) {
707 interpolation = TInterpolation::angular_rad;
708 }
709 SG_LOG(SG_SYSTEMS, SG_DEBUG, "calling weighting() ratio=" << ratio);
710 double value_interpolated = weighting(
711 interpolation,
712 ratio,
713 value_prev,
714 value_next);
715 SG_LOG(SG_GENERAL, SG_DEBUG, "Interpolating " << path << ": [" << value_prev << " .. " << value_next << "] => " << value_interpolated);
716 globals->get_props()->setDoubleValue(path, value_interpolated);
717 done = true;
718 }
719 }
720 }
721 if (!done) p->setStringValue(path, value_next_string);
722}
723
726void FGFlightRecorder::replay(double SimTime, const FGReplayData* _pNextBuffer,
727 const FGReplayData* _pLastBuffer,
728 int* main_window_xpos,
729 int* main_window_ypos,
730 int* main_window_xsize,
731 int* main_window_ysize)
732{
733 const char* pLastBuffer = (_pLastBuffer && !_pLastBuffer->raw_data.empty()) ? &_pLastBuffer->raw_data.front() : nullptr;
734 const char* pBuffer = (_pNextBuffer && !_pNextBuffer->raw_data.empty()) ? &_pNextBuffer->raw_data.front() : nullptr;
735 double ratio = 1.0;
736 if (pBuffer) {
737 /* Replay signals. */
738 int Offset = 0;
739 if (pLastBuffer) {
740 double NextSimTime = _pNextBuffer->sim_time;
741 double LastSimTime = _pLastBuffer->sim_time;
742 double Numerator = SimTime - LastSimTime;
743 double dt = NextSimTime - LastSimTime;
744 // avoid divide by zero and other quirks
745 if ((Numerator > 0.0) && (dt != 0.0)) {
746 ratio = Numerator / dt;
747 if (ratio > 1.0)
748 ratio = 1.0;
749 }
750 }
751
752 // 64bit aligned data first!
753 {
754 // restore doubles
755 const double* pDoubles = (const double*)&pBuffer[Offset];
756 const double* pLastDoubles = (const double*)&pLastBuffer[Offset];
757 unsigned int SignalCount = m_CaptureDouble.size();
758
759 for (unsigned int i = 0; i < SignalCount; i++) {
760 double v = pDoubles[i];
761 if (pLastBuffer) {
762 v = weighting(m_CaptureDouble[i].Interpolation, ratio,
763 pLastDoubles[i], v);
764 }
765 m_CaptureDouble[i].Signal->setDoubleValue(v);
766 }
767
768 if (m_LogRawSpeed->getBoolValue() && _pNextBuffer && pLastBuffer) {
769 // Log raw speed values to
770 // /sim/replay/log-raw-speed-values/value[]. This is used by
771 // scripts/python/recordreplay.py --test-motion.
772 //
773 SGGeod pos_geod = SGGeod::fromDegFt(
774 fgGetDouble("/position/longitude-deg"),
775 fgGetDouble("/position/latitude-deg"),
776 fgGetDouble("/position/altitude-ft"));
777 SGVec3d pos = SGVec3d::fromGeod(pos_geod);
778 static SGVec3d pos_prev;
779 static double t_prev = -1;
780 double t = SimTime;
781 double dt = t - t_prev;
782 if (t_prev != -1 && dt > 0) {
783 double distance = length(pos - pos_prev);
784 double speed = dt ? distance / dt : -1;
785 SG_LOG(SG_GENERAL, SG_DEBUG, ""
786 << " User aircraft:"
787 << " pLastBuffer=" << ((void*)pLastBuffer) << " t_prev=" << std::setprecision(10) << t_prev << " t=" << std::setprecision(10) << t << " dt=" << dt << " distance=" << distance << " speed=" << speed);
788 SGPropertyNode* n = fgGetNode("/sim/replay/log-raw-speed-values", true /*create*/);
789 n->addChild("value")->setDoubleValue(speed);
790 }
791 pos_prev = pos;
792 t_prev = t;
793 }
794
795 Offset += SignalCount * sizeof(double);
796 }
797
798 // 32bit aligned data comes second...
799 {
800 // restore floats
801 const float* pFloats = (const float*)&pBuffer[Offset];
802 const float* pLastFloats = (const float*)&pLastBuffer[Offset];
803 unsigned int SignalCount = m_CaptureFloat.size();
804 for (unsigned int i = 0; i < SignalCount; i++) {
805 float v = pFloats[i];
806 if (pLastBuffer) {
807 v = weighting(m_CaptureFloat[i].Interpolation, ratio,
808 pLastFloats[i], v);
809 }
810 m_CaptureFloat[i].Signal->setDoubleValue(v); //setFloatValue
811 }
812 Offset += SignalCount * sizeof(float);
813 }
814
815 {
816 // restore integers (32bit aligned)
817 const int* pInt = (const int*)&pBuffer[Offset];
818 unsigned int SignalCount = m_CaptureInteger.size();
819 for (unsigned int i = 0; i < SignalCount; i++) {
820 m_CaptureInteger[i].Signal->setIntValue(pInt[i]);
821 }
822 Offset += SignalCount * sizeof(int);
823 }
824
825 // 16bit aligned data is next...
826 {
827 // restore 16bit short integers
828 const short int* pShortInt = (const short int*)&pBuffer[Offset];
829 unsigned int SignalCount = m_CaptureInt16.size();
830 for (unsigned int i = 0; i < SignalCount; i++) {
831 m_CaptureInt16[i].Signal->setIntValue(pShortInt[i]);
832 }
833 Offset += SignalCount * sizeof(short int);
834 }
835
836 // finally: byte aligned data is last...
837 {
838 // restore 8bit chars
839 const signed char* pChar = (const signed char*)&pBuffer[Offset];
840 unsigned int SignalCount = m_CaptureInt8.size();
841 for (unsigned int i = 0; i < SignalCount; i++) {
842 m_CaptureInt8[i].Signal->setIntValue(pChar[i]);
843 }
844 Offset += SignalCount * sizeof(signed char);
845 }
846
847 {
848 // restore 1bit booleans (8bit aligned)
849 const unsigned char* pFlags = (const unsigned char*)&pBuffer[Offset];
850 unsigned int SignalCount = m_CaptureBool.size();
851 int Size = (SignalCount + 7) / 8;
852 Offset += Size;
853 for (unsigned int i = 0; i < SignalCount; i++) {
854 m_CaptureBool[i].Signal->setBoolValue(0 != (pFlags[i >> 3] & (1 << (i & 7))));
855 }
856 }
857 }
858
859 // Replay any multiplayer messages.
860 for (auto multiplayer_message : _pNextBuffer->multiplayer_messages) {
861 SG_LOG(SG_SYSTEMS, SG_DEBUG, "Pushing multiplayer message to multiplay manager");
862 m_MultiplayMgr->pushMessageHistory(multiplayer_message);
863 }
864
865 // Replay extra property changes.
866 //
867 bool replay_extra_properties = m_ReplayExtraProperties->getBoolValue();
868 bool replay_main_view = m_ReplayMainView->getBoolValue();
869 bool replay_main_window_position = m_ReplayMainWindowPosition->getBoolValue();
870 bool replay_main_window_size = m_ReplayMainWindowSize->getBoolValue();
871
872 if (replay_extra_properties) {
873 for (auto extra_property_removed_path : _pNextBuffer->replay_extra_property_removals) {
874 SG_LOG(SG_SYSTEMS, SG_DEBUG, "replaying extra property removal: " << extra_property_removed_path);
875 globals->get_props()->removeChild(extra_property_removed_path);
876 }
877 }
878
879 // Apply any changes to /sim/current-view/view-number* first. This is a
880 // hack to avoid problems where setting view-number appears to make the
881 // view code change other things such as pitch-offset-deg internally. So if
882 // the recorded property changes list view-number after pitch-offset-deg,
883 // we will end up overwriting the recorded change to pitch-offset-deg.
884 //
885 // The ultimate cause of this problem is that pitch-offset-deg is a tied
886 // property so when recording we don't always pick up all changes.
887 //
888 if (replay_main_view) {
889 for (auto prop_change : _pNextBuffer->replay_extra_property_changes) {
890 const std::string& path = prop_change.first;
891 const std::string& value = prop_change.second;
892 if (simgear::strutils::starts_with(path, "/sim/current-view/view-number")) {
893 SG_LOG(SG_SYSTEMS, SG_DEBUG, "SimTime=" << SimTime << " replaying view " << path << "=" << value);
894 globals->get_props()->setStringValue(path, value);
895 }
896 }
897 }
898
899 for (auto prop_change : _pNextBuffer->replay_extra_property_changes) {
900 const std::string& path = prop_change.first;
901 const std::string& value = prop_change.second;
902
903 if (0) {
904 } else if (path == "/sim/startup/xpos") {
905 if (replay_main_window_position) setInt(value, main_window_xpos);
906 } else if (path == "/sim/startup/ypos") {
907 if (replay_main_window_position) setInt(value, main_window_ypos);
908 } else if (path == "/sim/startup/xsize") {
909 if (replay_main_window_size) setInt(value, main_window_xsize);
910 } else if (path == "/sim/startup/ysize") {
911 if (replay_main_window_size) setInt(value, main_window_ysize);
912 } else if (simgear::strutils::starts_with(path, "/sim/current-view/")) {
913 if (replay_main_view) {
914 SG_LOG(SG_SYSTEMS, SG_DEBUG, "SimTime=" << SimTime << " replaying view change: " << path << "=" << value);
915 /* Interpolate floating point values if possible. */
916 replayProperty(path, value, _pLastBuffer, ratio);
917 }
918 } else if (replay_extra_properties) {
919 SG_LOG(SG_SYSTEMS, SG_DEBUG, "SimTime=" << SimTime << " replaying extra_property change: " << path << "=" << value);
920 SGPropertyNode* p = globals->get_props()->getNode(path, true /*create*/);
921 bool done = false;
922 if (p && _pLastBuffer) {
923 SG_LOG(SG_SYSTEMS, SG_DEBUG, "p && _pLastBuffer");
924 auto p_prev_it = _pLastBuffer->replay_extra_property_changes.find(path);
925 if (p_prev_it != _pLastBuffer->replay_extra_property_changes.end()) {
926 /* Property <path> is in both _pLastBuffer and
927 _pNextBuffer, so if it is floating point, we will
928 interpolate. */
929 SG_LOG(SG_SYSTEMS, SG_DEBUG, "property in _pLastBuffer and _pNextBuffer:"
930 << " " << path);
931 const std::string& valus_prev = p_prev_it->second;
932 size_t value_prev_len;
933 size_t value_next_len;
934 double value_prev = std::stod(valus_prev, &value_prev_len);
935 double value_next = std::stod(value, &value_next_len);
936 if (value_prev_len == valus_prev.size() && value_next_len == value.size()) {
937 /* Both values look like floating point so we will
938 interpolate. */
939 SG_LOG(SG_SYSTEMS, SG_DEBUG, "property is fp");
941 if (simgear::strutils::ends_with(path, "-deg")) {
942 interpolation = TInterpolation::angular_deg;
943 } else if (simgear::strutils::ends_with(path, "-rad")) {
944 interpolation = TInterpolation::angular_rad;
945 }
946 SG_LOG(SG_SYSTEMS, SG_DEBUG, "calling weighting()");
947 double value_interpolated = weighting(
948 interpolation,
949 ratio,
950 value_prev,
951 value_next);
952 SG_LOG(SG_GENERAL, SG_DEBUG, "Interpolating " << path << ": [" << value_prev << " .. " << value_next << "] => " << value_interpolated);
953 globals->get_props()->setDoubleValue(path, value_interpolated);
954 done = true;
955 }
956 }
957 }
958 if (!done) p->setStringValue(path, value);
959 }
960 }
961}
962
963int FGFlightRecorder::getConfig(SGPropertyNode* root, const char* typeStr, const FlightRecorder::TSignalList& SignalList)
964{
965 static const char* InterpolationTypes[] = {"discrete", "linear", "angular-rad", "angular-deg"};
966 size_t SignalCount = SignalList.size();
967 SGPropertyNode* Signals = root->getNode("signals", true);
968 for (size_t i = 0; i < SignalCount; i++) {
969 SGPropertyNode* SignalProp = Signals->addChild("signal");
970 SignalProp->setStringValue("type", typeStr);
971 SignalProp->setStringValue("interpolation", InterpolationTypes[SignalList[i].Interpolation]);
972 SignalProp->setStringValue("property", SignalList[i].Signal->getPath());
973 }
974 SG_LOG(SG_SYSTEMS, SG_DEBUG, "FlightRecorder: Have " << SignalCount << " signals of type " << typeStr);
975 root->setIntValue(typeStr, SignalCount);
976 return SignalCount;
977}
978
979void FGFlightRecorder::getConfig(SGPropertyNode* root)
980{
981 root->setStringValue("name", m_RecorderNode->getStringValue("active-config-name", ""));
982 int SignalCount = 0;
983 SignalCount += getConfig(root, "double", m_CaptureDouble);
984 SignalCount += getConfig(root, "float", m_CaptureFloat);
985 SignalCount += getConfig(root, "int", m_CaptureInteger);
986 SignalCount += getConfig(root, "int16", m_CaptureInt16);
987 SignalCount += getConfig(root, "int8", m_CaptureInt8);
988 SignalCount += getConfig(root, "bool", m_CaptureBool);
989
990 root->setIntValue("recorder/record-size", getRecordSize());
991 root->setIntValue("recorder/signal-count", SignalCount);
992}
#define p(x)
#define i(x)
FGFlightRecorder(const char *pConfigName)
void getConfig(SGPropertyNode *root)
virtual ~FGFlightRecorder()
FGReplayData * capture(double SimTime, FGReplayData *pRecycledBuffer)
Capture data.
void replay(double SimTime, const FGReplayData *pNextBuffer, const FGReplayData *pLastBuffer, int *main_window_xpos, int *main_window_ypos, int *main_window_xsize, int *main_window_ysize)
Replay.
SGPath resolve_aircraft_path(const std::string &branch) const
Given a path to an aircraft-related resource file, resolve it against the appropriate root.
Definition globals.cxx:555
int fgGetInt(const char *name, int defaultValue)
Get an int value for a property.
Definition fg_props.cxx:532
static void s_Append(std::vector< char > &out, const void *data, size_t data_len)
static void s_RecordPropertyDiffs(std::vector< char > &out, SGPropertyNode *a, SGPropertyNode *b, const std::vector< const char * > *path_exclude_prefixes)
static double weighting(TInterpolation interpolation, double ratio, double v1, double v2)
Do interpolation as defined by given interpolation type and weighting ratio.
static void string_to_double(const std::string &s, double &out, bool &ok)
static void s_AppendNumber(std::vector< char > &out, int16_t number)
static void setInt(const std::string &value, int *out)
static void s_AppendString(std::vector< char > &out, const std::string &s)
static void replayProperty(const std::string &path, const std::string &value_next_string, const FGReplayData *frame_prev, double ratio)
static std::shared_ptr< RecordExtraProperties > s_record_extra_properties
FGGlobals * globals
Definition globals.cxx:142
#define CHAT_MSG_ID
std::vector< TCapture > TSignalList
double fgGetDouble(const char *name, double defaultValue)
Get a double value for a property.
Definition proptest.cpp:30
SGPropertyNode * fgGetNode(const char *path, bool create)
Get a property node.
Definition proptest.cpp:27
std::vector< std::shared_ptr< std::vector< char > > > multiplayer_messages
std::vector< char > extra_properties
std::map< std::string, std::string > replay_extra_property_changes
std::vector< std::string > replay_extra_property_removals
std::vector< char > raw_data
SGPropertyNode_ptr Signal
void capture(SGPropertyNode_ptr RecordExtraPropertiesReference, FGReplayData *ReplayData)
uint32_t xdr_data_t
Definition tiny_xdr.hxx:30