diff --git a/test_suite/FGTestApi/CMakeLists.txt b/test_suite/FGTestApi/CMakeLists.txt
index 34cb89f51..83074ce66 100644
--- a/test_suite/FGTestApi/CMakeLists.txt
+++ b/test_suite/FGTestApi/CMakeLists.txt
@@ -6,6 +6,7 @@ set(TESTSUITE_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/scene_graph.cxx
${CMAKE_CURRENT_SOURCE_DIR}/TestPilot.cxx
${CMAKE_CURRENT_SOURCE_DIR}/NasalUnitTesting_TestSuite.cxx
+ ${CMAKE_CURRENT_SOURCE_DIR}/TestDataLogger.cxx
PARENT_SCOPE
)
@@ -17,5 +18,6 @@ set(TESTSUITE_HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/PrivateAccessorFDM.hxx
${CMAKE_CURRENT_SOURCE_DIR}/scene_graph.hxx
${CMAKE_CURRENT_SOURCE_DIR}/TestPilot.hxx
+ ${CMAKE_CURRENT_SOURCE_DIR}/TestDataLogger.hxx
PARENT_SCOPE
)
diff --git a/test_suite/FGTestApi/TestDataLogger.cxx b/test_suite/FGTestApi/TestDataLogger.cxx
new file mode 100644
index 000000000..a349bb131
--- /dev/null
+++ b/test_suite/FGTestApi/TestDataLogger.cxx
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2020 James Turner
+ *
+ * This file is part of the program FlightGear.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "TestDataLogger.hxx"
+
+#include
+#include
+#include
+#include
+
+#include
+
+#include "Main/globals.hxx"
+
+namespace FGTestApi {
+
+using DoubleVec = std::vector;
+
+static std::unique_ptr static_instance;
+
+class DataLogger::DataLoggerPrivate
+{
+public:
+ sg_ofstream _stream;
+
+ struct SampleInfo {
+ int column;
+ std::string name;
+ // range / units info, later
+ SGPropertyNode_ptr property;
+ };
+
+ double _currentTimeBase;
+ std::vector _samples;
+ DoubleVec _openRow;
+ bool _didHeader = false;
+
+ void writeCurrentRow()
+ {
+ if (!_didHeader) {
+ writeHeader();
+ _didHeader = true;
+ }
+
+ // capture property values into the open row data
+ std::for_each(_samples.begin(), _samples.end(), [this](const SampleInfo& info) {
+ if (info.property) {
+ _openRow[info.column] = info.property->getDoubleValue();
+ }
+ });
+
+ // write time base
+ _stream << globals->get_sim_time_sec() << ",";
+
+ for (const auto v : _openRow) {
+ if (std::isnan(v)) {
+ _stream << ","; // skip this data point
+ } else {
+ _stream << v << ",";
+ }
+ }
+
+ _stream << "\n";
+
+ std::fill(_openRow.begin(), _openRow.end(), std::numeric_limits::quiet_NaN());
+ }
+
+ void writeHeader()
+ {
+ _stream << "sim-time, ";
+ std::for_each(_samples.begin(), _samples.end(), [this](const SampleInfo& info) {
+ _stream << info.name << ", ";
+ });
+
+ _stream << "\n";
+ }
+};
+
+DataLogger::DataLogger()
+{
+ d.reset(new DataLoggerPrivate);
+}
+
+DataLogger::~DataLogger()
+{
+ d->_stream.close();
+}
+
+bool DataLogger::isActive()
+{
+ return static_instance != nullptr;
+}
+
+DataLogger* DataLogger::instance()
+{
+ if (!static_instance) {
+ static_instance.reset(new DataLogger);
+ }
+
+ return static_instance.get();
+}
+
+void DataLogger::initTest(const std::string& testName)
+{
+ d->_stream = sg_ofstream(testName + "_trace.csv");
+}
+
+void DataLogger::tearDown()
+{
+ if (static_instance) {
+ static_instance.reset();
+ }
+}
+
+void DataLogger::writeRecord()
+{
+ d->writeCurrentRow();
+}
+
+void DataLogger::recordProperty(const std::string& name, SGPropertyNode_ptr prop)
+{
+ int index = static_cast(d->_samples.size());
+ DataLoggerPrivate::SampleInfo info{index, name, prop};
+ d->_samples.push_back(info);
+
+ if (d->_openRow.size() <= index) {
+ d->_openRow.resize(index + 1, std::numeric_limits::quiet_NaN());
+ }
+}
+
+void DataLogger::recordSamplePoint(const std::string& name, double value)
+{
+ auto it = std::find_if(d->_samples.begin(), d->_samples.end(), [&name](const DataLoggerPrivate::SampleInfo& sample) {
+ return name == sample.name;
+ });
+
+ int index = 0;
+ if (it == d->_samples.end()) {
+ index = static_cast(d->_samples.size());
+ DataLoggerPrivate::SampleInfo info{index, name};
+ d->_samples.push_back(info);
+ } else {
+ index = it->column;
+ }
+
+ // grow _openRow as required
+ if (d->_openRow.size() <= index) {
+ d->_openRow.resize(index + 1, std::numeric_limits::quiet_NaN());
+ }
+
+ d->_openRow[index] = value;
+}
+
+} // namespace FGTestApi
diff --git a/test_suite/FGTestApi/TestDataLogger.hxx b/test_suite/FGTestApi/TestDataLogger.hxx
new file mode 100644
index 000000000..86a18515e
--- /dev/null
+++ b/test_suite/FGTestApi/TestDataLogger.hxx
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 James Turner
+ *
+ * This file is part of the program FlightGear.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+
+#include
+
+namespace FGTestApi {
+
+class DataLogger
+{
+public:
+ ~DataLogger();
+
+ static DataLogger* instance();
+ static bool isActive();
+
+ void initTest(const std::string& testName);
+
+ void recordProperty(const std::string& name, SGPropertyNode_ptr prop);
+
+ void setUp();
+
+ void tearDown();
+
+ void recordSamplePoint(const std::string& name, double value);
+
+ void writeRecord();
+
+private:
+ DataLogger();
+
+ class DataLoggerPrivate;
+ std::unique_ptr d;
+};
+
+
+} // namespace FGTestApi
diff --git a/test_suite/FGTestApi/testGlobals.cxx b/test_suite/FGTestApi/testGlobals.cxx
index 4286cd28e..8238cb923 100644
--- a/test_suite/FGTestApi/testGlobals.cxx
+++ b/test_suite/FGTestApi/testGlobals.cxx
@@ -2,6 +2,7 @@
#include "test_suite/dataStore.hxx"
+#include "TestDataLogger.hxx"
#include "testGlobals.hxx"
#include
@@ -254,15 +255,30 @@ void setPositionAndStabilise(const SGGeod& g)
void runForTime(double t)
{
- int ticks = t * 120.0;
+ const int tickHz = 30;
+ const double tickDuration = 1.0 / tickHz;
+
+ int ticks = static_cast(t * tickHz);
assert(ticks > 0);
- const double dt = 1 / 120.0;
-
+
+ const int logInterval = 0.5 * tickHz;
+ int nextLog = 0;
+
for (int t = 0; t < ticks; ++t) {
- globals->inc_sim_time_sec(dt);
- globals->get_subsystem_mgr()->update(dt);
- if (global_loggingToKML) {
- logCoordinate(globals->get_aircraft_position());
+ globals->inc_sim_time_sec(tickDuration);
+ globals->get_subsystem_mgr()->update(tickDuration);
+
+ if (nextLog == 0) {
+ if (global_loggingToKML) {
+ logCoordinate(globals->get_aircraft_position());
+ }
+
+ if (DataLogger::isActive()) {
+ DataLogger::instance()->writeRecord();
+ }
+ nextLog = logInterval;
+ } else {
+ nextLog--;
}
}
}
@@ -274,22 +290,26 @@ bool runForTimeWithCheck(double t, RunCheck check)
int ticks = static_cast(t * tickHz);
assert(ticks > 0);
- const int logInterval = 2 * tickHz; // every two seconds
+ const int logInterval = 0.5 * tickHz;
int nextLog = 0;
for (int t = 0; t < ticks; ++t) {
globals->inc_sim_time_sec(tickDuration);
globals->get_subsystem_mgr()->update(tickDuration);
-
- if (global_loggingToKML) {
- if (nextLog == 0) {
+
+ if (nextLog == 0) {
+ if (global_loggingToKML) {
logCoordinate(globals->get_aircraft_position());
- nextLog = logInterval;
- } else {
- nextLog--;
}
+
+ if (DataLogger::isActive()) {
+ DataLogger::instance()->writeRecord();
+ }
+ nextLog = logInterval;
+ } else {
+ nextLog--;
}
-
+
bool done = check();
if (done) {
if (global_loggingToKML) {