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) {