From fdb64a02a890c67e335c3e1382394f1b71784575 Mon Sep 17 00:00:00 2001 From: ThorstenB Date: Sat, 10 Nov 2012 21:25:31 +0100 Subject: [PATCH] Extended replay and flight recording system Save/restore replay sessions. Replay system message support, so recorded flights can be turned into flight tutorials. --- src/Aircraft/flightrecorder.cxx | 67 +++- src/Aircraft/flightrecorder.hxx | 6 +- src/Aircraft/replay.cxx | 636 +++++++++++++++++++++++++++++--- src/Aircraft/replay.hxx | 36 +- src/Main/fg_commands.cxx | 26 ++ 5 files changed, 703 insertions(+), 68 deletions(-) diff --git a/src/Aircraft/flightrecorder.cxx b/src/Aircraft/flightrecorder.cxx index a2188540b..a229f6814 100644 --- a/src/Aircraft/flightrecorder.cxx +++ b/src/Aircraft/flightrecorder.cxx @@ -56,6 +56,21 @@ FGFlightRecorder::reinit(void) { m_ConfigNode = 0; + SGPropertyNode_ptr ConfigNode; + int Selected = m_RecorderNode->getIntValue(m_ConfigName, 0); + SG_LOG(SG_SYSTEMS, SG_INFO, "FlightRecorder: Recorder configuration #" << Selected); + if (Selected >= 0) + ConfigNode = m_RecorderNode->getChild("config", Selected); + + if (!ConfigNode.valid()) + ConfigNode = getDefault(); + + reinit(ConfigNode); +} + +void +FGFlightRecorder::reinit(SGPropertyNode_ptr ConfigNode) +{ m_TotalRecordSize = 0; m_CaptureDouble.clear(); @@ -65,13 +80,7 @@ FGFlightRecorder::reinit(void) m_CaptureInt8.clear(); m_CaptureBool.clear(); - int Selected = m_RecorderNode->getIntValue(m_ConfigName, 0); - SG_LOG(SG_SYSTEMS, SG_INFO, "FlightRecorder: Recorder configuration #" << Selected); - if (Selected >= 0) - m_ConfigNode = m_RecorderNode->getChild("config", Selected); - - if (!m_ConfigNode.valid()) - initDefault(); + m_ConfigNode = ConfigNode; if (!m_ConfigNode.valid()) { @@ -145,9 +154,11 @@ FGFlightRecorder::haveProperty(SGPropertyNode* pProperty) /** Read default flight-recorder configuration. * Default should match properties as hard coded for versions up to FG2.4.0. */ -void -FGFlightRecorder::initDefault(void) +SGPropertyNode_ptr +FGFlightRecorder::getDefault(void) { + SGPropertyNode_ptr ConfigNode; + // set name of active flight recorder type SG_LOG(SG_SYSTEMS, SG_INFO, "FlightRecorder: No custom configuration. Loading generic default recorder."); @@ -168,7 +179,7 @@ FGFlightRecorder::initDefault(void) try { readProperties(path.str(), m_RecorderNode->getChild("config", 0 ,true), 0); - m_ConfigNode = m_RecorderNode->getChild("config", 0 ,false); + ConfigNode = m_RecorderNode->getChild("config", 0 ,false); } catch (sg_io_exception &e) { SG_LOG(SG_SYSTEMS, SG_ALERT, "FlightRecorder: Error reading file '" << @@ -176,6 +187,8 @@ FGFlightRecorder::initDefault(void) } } } + + return ConfigNode; } /** Read signal list below given base node. @@ -548,3 +561,37 @@ FGFlightRecorder::replay(double SimTime, const FGReplayData* _pNextBuffer, const } } } + +int +FGFlightRecorder::getConfig(SGPropertyNode* root, const char* typeStr, const FlightRecorder::TSignalList& SignalList) +{ + static const char* InterpolationTypes[] = {"discrete", "linear", "angular-rad", "angular-deg"}; + size_t SignalCount = SignalList.size(); + SGPropertyNode* Signals = root->getNode("signals", true); + for (size_t i=0; iaddChild("signal"); + SignalProp->setStringValue("type", typeStr); + SignalProp->setStringValue("interpolation", InterpolationTypes[SignalList[i].Interpolation]); + SignalProp->setStringValue("property", SignalList[i].Signal->getPath()); + } + SG_LOG(SG_SYSTEMS, SG_DEBUG, "FlightRecorder: Have " << SignalCount << " signals of type " << typeStr); + root->setIntValue(typeStr, SignalCount); + return SignalCount; +} + +void +FGFlightRecorder::getConfig(SGPropertyNode* root) +{ + root->setStringValue("name", m_RecorderNode->getStringValue("active-config-name", "")); + int SignalCount = 0; + SignalCount += getConfig(root, "double", m_CaptureDouble); + SignalCount += getConfig(root, "float", m_CaptureFloat); + SignalCount += getConfig(root, "int", m_CaptureInteger); + SignalCount += getConfig(root, "int16", m_CaptureInt16); + SignalCount += getConfig(root, "int8", m_CaptureInt8); + SignalCount += getConfig(root, "bool", m_CaptureBool); + + root->setIntValue("recorder/record-size", getRecordSize()); + root->setIntValue("recorder/signal-count", SignalCount); +} diff --git a/src/Aircraft/flightrecorder.hxx b/src/Aircraft/flightrecorder.hxx index 7650db458..3fddc11b3 100644 --- a/src/Aircraft/flightrecorder.hxx +++ b/src/Aircraft/flightrecorder.hxx @@ -54,6 +54,7 @@ public: virtual ~FGFlightRecorder(); void reinit (void); + void reinit (SGPropertyNode_ptr ConfigNode); FGReplayData* createEmptyRecord (void); FGReplayData* capture (double SimTime, FGReplayData* pRecycledBuffer); void replay (double SimTime, const FGReplayData* pNextBuffer, @@ -61,9 +62,10 @@ public: void deleteRecord (FGReplayData* pRecord); int getRecordSize (void) { return m_TotalRecordSize;} + void getConfig (SGPropertyNode* root); private: - void initDefault(void); + SGPropertyNode_ptr getDefault(void); void initSignalList(const char* pSignalType, FlightRecorder::TSignalList& SignalList, SGPropertyNode_ptr BaseNode); void processSignalList(const char* pSignalType, FlightRecorder::TSignalList& SignalList, @@ -72,6 +74,8 @@ private: bool haveProperty(FlightRecorder::TSignalList& Capture,SGPropertyNode* pProperty); bool haveProperty(SGPropertyNode* pProperty); + int getConfig(SGPropertyNode* root, const char* typeStr, const FlightRecorder::TSignalList& SignalList); + SGPropertyNode_ptr m_RecorderNode; SGPropertyNode_ptr m_ConfigNode; diff --git a/src/Aircraft/replay.cxx b/src/Aircraft/replay.cxx index f44d38b88..959437ef9 100644 --- a/src/Aircraft/replay.cxx +++ b/src/Aircraft/replay.cxx @@ -1,7 +1,7 @@ // replay.cxx - a system to record and replay FlightGear flights // // Written by Curtis Olson, started July 2003. -// Updated by Thorsten Brehm, September 2011. +// Updated by Thorsten Brehm, September 2011 and November 2012. // // Copyright (C) 2003 Curtis L. Olson - http://www.flightgear.org/~curt // @@ -29,17 +29,55 @@ #include #include +#include +#include +#include +#include +#include #include
#include "replay.hxx" #include "flightrecorder.hxx" +using std::deque; +using std::vector; +using simgear::gzContainerReader; +using simgear::gzContainerWriter; + +#if 1 + #define MY_SG_DEBUG SG_DEBUG +#else + #define MY_SG_DEBUG SG_ALERT +#endif + +/** Magic string to verify valid FG flight recorder tapes. */ +static const char* const FlightRecorderFileMagic = "FlightGear Flight Recorder Tape"; + +namespace ReplayContainer +{ + enum Type + { + Invalid = -1, + Header = 0, /**< Used for initial file header (fixed identification string). */ + MetaData = 1, /**< XML data / properties with arbitrary data, such as description, aircraft type, ... */ + Properties = 2, /**< XML data describing the recorded flight recorder properties. + Format is identical to flight recorder XML configuration. Also contains some + extra data to verify flight recorder consistency. */ + RawData = 3 /**< Actual binary data blobs (the recorder's tape). + One "RawData" blob is used for each resolution. */ + }; +} + /** * Constructor */ FGReplay::FGReplay() : + sim_time(0), + last_mt_time(0.0), + last_lt_time(0.0), + last_msg_time(0), last_replay_state(0), m_high_res_time(60.0), m_medium_res_time(600.0), @@ -88,6 +126,9 @@ FGReplay::clear() m_pRecorder->deleteRecord(recycler.front()); recycler.pop_front(); } + + // clear messages belonging to old replay session + fgGetNode("/sim/replay/messages", 0, true)->removeChildren("msg", false); } /** @@ -98,11 +139,15 @@ void FGReplay::init() { disable_replay = fgGetNode("/sim/replay/disable", true); - replay_master = fgGetNode("/sim/freeze/replay-state", true); + replay_master = fgGetNode("/sim/replay/replay-state", true); replay_time = fgGetNode("/sim/replay/time", true); replay_time_str = fgGetNode("/sim/replay/time-str", true); replay_looped = fgGetNode("/sim/replay/looped", true); speed_up = fgGetNode("/sim/speed-up", true); + + // alias to keep backward compatibility + fgGetNode("/sim/freeze/replay-state", true)->alias(replay_master); + reinit(); } @@ -116,6 +161,7 @@ FGReplay::reinit() sim_time = 0.0; last_mt_time = 0.0; last_lt_time = 0.0; + last_msg_time = 0.0; // Flush queues clear(); @@ -128,20 +174,9 @@ FGReplay::reinit() m_medium_sample_rate = fgGetDouble("/sim/replay/buffer/medium-res-sample-dt", 0.5); // medium term sample rate (sec) m_long_sample_rate = fgGetDouble("/sim/replay/buffer/low-res-sample-dt", 5.0); // long term sample rate (sec) - // Create an estimated nr of required ReplayData objects - // 120 is an estimated maximum frame rate. - int estNrObjects = (int) ((m_high_res_time*120) + (m_medium_res_time*m_medium_sample_rate) + - (m_low_res_time*m_long_sample_rate)); - for (int i = 0; i < estNrObjects; i++) - { - FGReplayData* r = m_pRecorder->createEmptyRecord(); - if (r) - recycler.push_back(r); - else - { - SG_LOG(SG_SYSTEMS, SG_ALERT, "ReplaySystem: Out of memory!"); - } - } + fillRecycler(); + loadMessages(); + replay_master->setIntValue(0); disable_replay->setBoolValue(0); replay_time->setDoubleValue(0); @@ -157,7 +192,6 @@ FGReplay::bind() { } - /** * Unbind from the property tree */ @@ -168,6 +202,25 @@ FGReplay::unbind() // nothing to unbind } +void +FGReplay::fillRecycler() +{ + // Create an estimated nr of required ReplayData objects + // 120 is an estimated maximum frame rate. + int estNrObjects = (int) ((m_high_res_time*120) + (m_medium_res_time*m_medium_sample_rate) + + (m_low_res_time*m_long_sample_rate)); + for (int i = 0; i < estNrObjects; i++) + { + FGReplayData* r = m_pRecorder->createEmptyRecord(); + if (r) + recycler.push_back(r); + else + { + SG_LOG(SG_SYSTEMS, SG_ALERT, "ReplaySystem: Out of memory!"); + } + } +} + static void printTimeStr(char* pStrBuffer,double _Time, bool ShowDecimal=true) { @@ -195,14 +248,67 @@ printTimeStr(char* pStrBuffer,double _Time, bool ShowDecimal=true) sprintf(&pStrBuffer[len],".%u",d); } +void +FGReplay::guiMessage(const char* message) +{ + fgSetString("/sim/messages/copilot", message); +} + +void +FGReplay::loadMessages() +{ + // load messages + replay_messages.clear(); + simgear::PropertyList msgs = fgGetNode("/sim/replay/messages", true)->getChildren("msg"); + + for (simgear::PropertyList::iterator it = msgs.begin();it != msgs.end();++it) + { + const char* msgText = (*it)->getStringValue("text", ""); + const double msgTime = (*it)->getDoubleValue("time", -1.0); + const char* msgSpeaker = (*it)->getStringValue("speaker", "pilot"); + if ((msgText[0] != 0)&&(msgTime >= 0)) + { + FGReplayMessages data; + data.sim_time = msgTime; + data.message = msgText; + data.speaker = msgSpeaker; + replay_messages.push_back(data); + } + } + current_msg = replay_messages.begin(); +} + +void +FGReplay::replayMessage(double time) +{ + if (time < last_msg_time) + { + current_msg = replay_messages.begin(); + } + + // check if messages have to be triggered + while ((current_msg != replay_messages.end())&& + (time >= current_msg->sim_time)) + { + // don't trigger messages when too long ago (fast-forward/skipped replay) + if (time - current_msg->sim_time < 3.0) + { + fgGetNode("/sim/messages", true)->getNode(current_msg->speaker, true)->setStringValue(current_msg->message); + } + ++current_msg; + } + last_msg_time = time; +} + /** Start replay session */ bool -FGReplay::start() +FGReplay::start(bool NewTape) { // freeze the fdm, resume from sim pause double StartTime = get_start_time(); double EndTime = get_end_time(); + was_finished_already = false; fgSetDouble("/sim/replay/start-time", StartTime); fgSetDouble("/sim/replay/end-time", EndTime); char StrBuffer[30]; @@ -216,15 +322,26 @@ FGReplay::start() buffer_elements*m_pRecorder->getRecordSize() / (1024*1024.0)); if ((fgGetBool("/sim/freeze/master"))|| (0 == replay_master->getIntValue())) - fgSetString("/sim/messages/copilot", "Replay active. 'Esc' to stop."); + guiMessage("Replay active. 'Esc' to stop."); fgSetBool ("/sim/freeze/master", 0); fgSetBool ("/sim/freeze/clock", 0); if (0 == replay_master->getIntValue()) { replay_master->setIntValue(1); - replay_time->setDoubleValue(-1); + if (NewTape) + { + // start replay at initial time, when loading a new tape + replay_time->setDoubleValue(StartTime); + } + else + { + // start replay at "loop interval" when starting instant replay + replay_time->setDoubleValue(-1); + } replay_time_str->setStringValue(""); } + loadMessages(); + return true; } @@ -244,6 +361,7 @@ FGReplay::update( double dt ) if (fgGetBool("/sim/freeze/master",false)|| fgGetBool("/sim/freeze/clock",false)) { + // unpause - disable the replay system in next loop fgSetBool("/sim/freeze/master",false); fgSetBool("/sim/freeze/clock",false); last_replay_state = 1; @@ -252,6 +370,7 @@ FGReplay::update( double dt ) if ((replay_master->getIntValue() != 3)|| (last_replay_state == 3)) { + // disable the replay system current_replay_state = replay_master->getIntValue(); replay_master->setIntValue(0); replay_time->setDoubleValue(0); @@ -264,7 +383,7 @@ FGReplay::update( double dt ) fgSetBool("/sim/sound/enabled",true); fgSetBool("/sim/replay/mute",false); } - fgSetString("/sim/messages/copilot", "Replay stopped. Your controls!"); + guiMessage("Replay stopped. Your controls!"); } } @@ -310,7 +429,9 @@ FGReplay::update( double dt ) double endTime = get_end_time(); fgSetDouble( "/sim/replay/start-time", startTime ); fgSetDouble( "/sim/replay/end-time", endTime ); - double duration = fgGetDouble( "/sim/replay/duration" ); + double duration = 0; + if (replay_looped->getBoolValue()) + fgGetDouble("/sim/replay/duration"); if( duration && (duration < (endTime - startTime)) ) { current_time = endTime - duration; } else { @@ -319,9 +440,19 @@ FGReplay::update( double dt ) } bool IsFinished = replay( replay_time->getDoubleValue() ); if (IsFinished) + { + if (!was_finished_already) + { + guiMessage("End of tape. 'Esc' to return."); + was_finished_already = true; + } current_time = (replay_looped->getBoolValue()) ? -1 : get_end_time()+0.01; + } else + { current_time += dt * speed_up->getDoubleValue(); + was_finished_already = false; + } replay_time->setDoubleValue(current_time); char StrBuffer[30]; printTimeStr(StrBuffer,current_time); @@ -341,7 +472,6 @@ FGReplay::update( double dt ) // flight recording - //cerr << "Recording replay" << endl; sim_time += dt * speed_up->getDoubleValue(); // sanity check, don't collect data if FDM data isn't good @@ -357,9 +487,7 @@ FGReplay::update( double dt ) } // update the short term list - //stamp("point_06"); short_term.push_back( r ); - //stamp("point_07"); FGReplayData *st_front = short_term.front(); if (!st_front) @@ -367,39 +495,45 @@ FGReplay::update( double dt ) SG_LOG(SG_SYSTEMS, SG_ALERT, "ReplaySystem: Inconsistent data!"); } - if ( sim_time - st_front->sim_time > m_high_res_time ) { - while ( sim_time - st_front->sim_time > m_high_res_time ) { + if ( sim_time - st_front->sim_time > m_high_res_time ) + { + while ( sim_time - st_front->sim_time > m_high_res_time ) + { st_front = short_term.front(); recycler.push_back(st_front); short_term.pop_front(); } - //stamp("point_08"); + // update the medium term list - if ( sim_time - last_mt_time > m_medium_sample_rate ) { + if ( sim_time - last_mt_time > m_medium_sample_rate ) + { last_mt_time = sim_time; st_front = short_term.front(); medium_term.push_back( st_front ); short_term.pop_front(); FGReplayData *mt_front = medium_term.front(); - if ( sim_time - mt_front->sim_time > m_medium_res_time ) { - //stamp("point_09"); - while ( sim_time - mt_front->sim_time > m_medium_res_time ) { + if ( sim_time - mt_front->sim_time > m_medium_res_time ) + { + while ( sim_time - mt_front->sim_time > m_medium_res_time ) + { mt_front = medium_term.front(); recycler.push_back(mt_front); medium_term.pop_front(); } // update the long term list - if ( sim_time - last_lt_time > m_long_sample_rate ) { + if ( sim_time - last_lt_time > m_long_sample_rate ) + { last_lt_time = sim_time; mt_front = medium_term.front(); long_term.push_back( mt_front ); medium_term.pop_front(); FGReplayData *lt_front = long_term.front(); - if ( sim_time - lt_front->sim_time > m_low_res_time ) { - //stamp("point_10"); - while ( sim_time - lt_front->sim_time > m_low_res_time ) { + if ( sim_time - lt_front->sim_time > m_low_res_time ) + { + while ( sim_time - lt_front->sim_time > m_low_res_time ) + { lt_front = long_term.front(); recycler.push_back(lt_front); long_term.pop_front(); @@ -435,9 +569,7 @@ FGReplay::record(double time) recycler.pop_front(); } - r = m_pRecorder->capture(time, r); - - return r; + return m_pRecorder->capture(time, r); } /** @@ -496,6 +628,8 @@ FGReplay::replay( double time ) { // find the two frames to interpolate between double t1, t2; + replayMessage(time); + if ( short_term.size() > 0 ) { t1 = short_term.back()->sim_time; t2 = short_term.front()->sim_time; @@ -504,52 +638,43 @@ FGReplay::replay( double time ) { replay( time, short_term.back() ); // replay is finished now return true; - // cout << "first frame" << endl; } else if ( time <= t1 && time >= t2 ) { interpolate( time, short_term ); - // cout << "from short term" << endl; } else if ( medium_term.size() > 0 ) { t1 = short_term.front()->sim_time; t2 = medium_term.back()->sim_time; if ( time <= t1 && time >= t2 ) { replay(time, medium_term.back(), short_term.front()); - // cout << "from short/medium term" << endl; } else { t1 = medium_term.back()->sim_time; t2 = medium_term.front()->sim_time; if ( time <= t1 && time >= t2 ) { interpolate( time, medium_term ); - // cout << "from medium term" << endl; } else if ( long_term.size() > 0 ) { t1 = medium_term.front()->sim_time; t2 = long_term.back()->sim_time; if ( time <= t1 && time >= t2 ) { replay(time, long_term.back(), medium_term.front()); - // cout << "from medium/long term" << endl; } else { t1 = long_term.back()->sim_time; t2 = long_term.front()->sim_time; if ( time <= t1 && time >= t2 ) { interpolate( time, long_term ); - // cout << "from long term" << endl; } else { // replay the oldest long term frame replay(time, long_term.front()); - // cout << "oldest long term frame" << endl; } } } else { // replay the oldest medium term frame replay(time, medium_term.front()); - // cout << "oldest medium term frame" << endl; } } } else { // replay the oldest short term frame replay(time, short_term.front()); - // cout << "oldest short term frame" << endl; } } else { // nothing to replay @@ -596,3 +721,418 @@ FGReplay::get_end_time() return 0.0; } } + +/** Save raw replay data in a separate container */ +static bool +saveRawReplayData(gzContainerWriter& output, const replay_list_type& ReplayData, size_t RecordSize) +{ + // get number of records in this stream + size_t Count = ReplayData.size(); + + // write container header for raw data + if (!output.writeContainerHeader(ReplayContainer::RawData, Count * RecordSize)) + { + SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to save replay data. Cannot write data container. Disk full?"); + return false; + } + + // write the raw data (all records in the given list) + replay_list_type::const_iterator it = ReplayData.begin(); + size_t CheckCount = 0; + while ((it != ReplayData.end())&& + !output.fail()) + { + const FGReplayData* pRecord = *it++; + output.write((char*)pRecord, RecordSize); + CheckCount++; + } + + // Did we really write as much as we intended? + if (CheckCount != Count) + { + SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to save replay data. Expected to write " << Count << " records, but wrote " << CheckCount); + return false; + } + + SG_LOG(SG_SYSTEMS, MY_SG_DEBUG, "Saved " << CheckCount << " records of size " << RecordSize); + return !output.fail(); +} + +/** Load raw replay data from a separate container */ +static bool +loadRawReplayData(gzContainerReader& input, FGFlightRecorder* pRecorder, replay_list_type& ReplayData, size_t RecordSize) +{ + size_t Size = 0; + simgear::ContainerType Type = ReplayContainer::Invalid; + + // write container header for raw data + if (!input.readContainerHeader(&Type, &Size)) + { + SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to load replay data. Missing data container."); + return false; + } + else + if (Type != ReplayContainer::RawData) + { + SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to load replay data. Expected data container, got " << Type); + return false; + } + + // read the raw data + size_t Count = Size / RecordSize; + SG_LOG(SG_SYSTEMS, MY_SG_DEBUG, "Loading replay data. Container size is " << Size << ", record size " << RecordSize << + ", expected record count " << Count << "."); + + size_t CheckCount = 0; + for (CheckCount=0; (CheckCountcreateEmptyRecord(); + input.read((char*) pBuffer, RecordSize); + ReplayData.push_back(pBuffer); + } + + // did we get all we have hoped for? + if (CheckCount != Count) + { + if (input.eof()) + { + SG_LOG(SG_SYSTEMS, SG_ALERT, "Unexpected end of file."); + } + SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to load replay data. Expected " << Count << " records, but got " << CheckCount); + return false; + } + + SG_LOG(SG_SYSTEMS, MY_SG_DEBUG, "Loaded " << CheckCount << " records of size " << RecordSize); + return true; +} + +/** Write flight recorder tape with given filename and meta properties to disk */ +bool +FGReplay::saveTape(const char* Filename, SGPropertyNode* MetaDataProps) +{ + bool ok = true; + + /* open output stream *******************************************/ + gzContainerWriter output(Filename, FlightRecorderFileMagic); + if (!output.good()) + { + SG_LOG(SG_SYSTEMS, SG_ALERT, "Cannot open file" << Filename); + return false; + } + + /* write meta data **********************************************/ + ok &= output.writeContainer(ReplayContainer::MetaData, MetaDataProps); + + /* write flight recorder configuration **************************/ + SGPropertyNode_ptr Config; + if (ok) + { + Config = new SGPropertyNode(); + m_pRecorder->getConfig(Config.get()); + ok &= output.writeContainer(ReplayContainer::Properties, Config.get()); + } + + /* write raw data ***********************************************/ + if (Config) + { + size_t RecordSize = Config->getIntValue("recorder/record-size", 0); + SG_LOG(SG_SYSTEMS, MY_SG_DEBUG, "Total signal count: " << Config->getIntValue("recorder/signal-count", 0) + << ", record size: " << RecordSize); + if (ok) + ok &= saveRawReplayData(output, short_term, RecordSize); + if (ok) + ok &= saveRawReplayData(output, medium_term, RecordSize); + if (ok) + ok &= saveRawReplayData(output, long_term, RecordSize); + Config = 0; + } + + /* done *********************************************************/ + output.close(); + + return ok; +} + +/** Write flight recorder tape to disk. User/script command. */ +bool +FGReplay::saveTape(const SGPropertyNode* ConfigData) +{ + const char* tapeDirectory = fgGetString("/sim/replay/tape-directory", ""); + const char* aircraftType = fgGetString("/sim/aircraft", "unknown"); + + SGPropertyNode_ptr myMetaData = new SGPropertyNode(); + SGPropertyNode* meta = myMetaData->getNode("meta", 0, true); + + // add some data to the file - so we know for which aircraft/version it was recorded + meta->setStringValue("aircraft-type", aircraftType); + meta->setStringValue("aircraft-description", fgGetString("/sim/description", "")); + meta->setStringValue("aircraft-fdm", fgGetString("/sim/flight-model", "")); + meta->setStringValue("closest-airport-id", fgGetString("/sim/airport/closest-airport-id", "")); + const char* aircraft_version = fgGetString("/sim/aircraft-version", ""); + if (aircraft_version[0]==0) + aircraft_version = "(unknown aircraft version)"; + meta->setStringValue("aircraft-version", aircraft_version); + + // add information on the tape's recording duration + double Duration = get_end_time()-get_start_time(); + meta->setDoubleValue("tape-duration", Duration); + char StrBuffer[30]; + printTimeStr(StrBuffer, Duration, false); + meta->setStringValue("tape-duration-str", StrBuffer); + + // add simulator version + copyProperties(fgGetNode("/sim/version", 0, true), meta->getNode("version", 0, true)); + if (ConfigData->getNode("user-data")) + { + copyProperties(ConfigData->getNode("user-data"), meta->getNode("user-data", 0, true)); + } + + // store replay messages + copyProperties(fgGetNode("/sim/replay/messages", 0, true), myMetaData->getNode("messages", 0, true)); + + // generate file name (directory + aircraft type + date + time + suffix) + SGPath p(tapeDirectory); + p.append(aircraftType); + p.concat("-"); + time_t calendar_time = time(NULL); + struct tm *local_tm; + local_tm = localtime( &calendar_time ); + char time_str[256]; + strftime( time_str, 256, "%Y%02m%02d-%02H%02M%02S", local_tm); + p.concat(time_str); + p.concat(".fgtape"); + + bool ok = true; + // make sure we're not overwriting something + if (p.exists()) + { + // same timestamp!? + SG_LOG(SG_SYSTEMS, SG_ALERT, "Error, flight recorder tape file with same name already exists."); + ok = false; + } + + if (ok) + ok &= saveTape(p.c_str(), myMetaData.get()); + + if (ok) + guiMessage("Flight recorder tape saved successfully!"); + else + guiMessage("Failed to save tape! See log output."); + + return ok; +} + +/** Read a flight recorder tape with given filename from disk and return meta properties. + * Actual data and signal configuration is not read when in "Preview" mode. + */ +bool +FGReplay::loadTape(const char* Filename, bool Preview, SGPropertyNode* UserData) +{ + bool ok = true; + + /* open input stream ********************************************/ + gzContainerReader input(Filename, FlightRecorderFileMagic); + if (input.eof() || !input.good()) + { + SG_LOG(SG_SYSTEMS, SG_ALERT, "Cannot open file " << Filename); + ok = false; + } + + SGPropertyNode_ptr MetaDataProps = new SGPropertyNode(); + + /* read meta data ***********************************************/ + if (ok) + { + char* MetaData = NULL; + size_t Size = 0; + simgear::ContainerType Type = ReplayContainer::Invalid; + if (!input.readContainer(&Type, &MetaData, &Size) || Size<1) + { + SG_LOG(SG_SYSTEMS, SG_ALERT, "File not recognized. This is not a valid FlightGear flight recorder tape: " << Filename + << ". Invalid meta data."); + ok = false; + } + else + if (Type != ReplayContainer::MetaData) + { + SG_LOG(SG_SYSTEMS, MY_SG_DEBUG, "Invalid header. Container type " << Type); + SG_LOG(SG_SYSTEMS, SG_ALERT, "File not recognized. This is not a valid FlightGear flight recorder tape: " << Filename); + ok = false; + } + else + { + try + { + readProperties(MetaData, Size-1, MetaDataProps); + copyProperties(MetaDataProps->getNode("meta", 0, true), UserData); + } catch (const sg_exception &e) + { + SG_LOG(SG_SYSTEMS, SG_ALERT, "Error reading flight recorder tape: " << Filename + << ", XML parser message:" << e.getFormattedMessage()); + ok = false; + } + } + + if (MetaData) + { + //printf("%s\n", MetaData); + free(MetaData); + MetaData = NULL; + } + } + + /* read flight recorder configuration **************************/ + if ((ok)&&(!Preview)) + { + SG_LOG(SG_SYSTEMS, MY_SG_DEBUG, "Loading flight recorder data..."); + char* ConfigXML = NULL; + size_t Size = 0; + simgear::ContainerType Type = ReplayContainer::Invalid; + if (!input.readContainer(&Type, &ConfigXML, &Size) || Size<1) + { + SG_LOG(SG_SYSTEMS, SG_ALERT, "File not recognized. This is not a valid FlightGear flight recorder tape: " << Filename + << ". Invalid configuration container."); + ok = false; + } + else + if ((!ConfigXML)||(Type != ReplayContainer::Properties)) + { + SG_LOG(SG_SYSTEMS, SG_ALERT, "File not recognized. This is not a valid FlightGear flight recorder tape: " << Filename + << ". Unexpected container type, expected \"properties\"."); + ok = false; + } + + SGPropertyNode_ptr Config = new SGPropertyNode(); + if (ok) + { + try + { + readProperties(ConfigXML, Size-1, Config); + } catch (const sg_exception &e) + { + SG_LOG(SG_SYSTEMS, SG_ALERT, "Error reading flight recorder tape: " << Filename + << ", XML parser message:" << e.getFormattedMessage()); + ok = false; + } + if (ok) + { + // reconfigure the recorder - and wipe old data (no longer matches the current recorder) + m_pRecorder->reinit(Config); + clear(); + fillRecycler(); + } + } + + if (ConfigXML) + { + free(ConfigXML); + ConfigXML = NULL; + } + + /* read raw data ***********************************************/ + if (ok) + { + size_t RecordSize = m_pRecorder->getRecordSize(); + size_t OriginalSize = Config->getIntValue("recorder/record-size", 0); + // check consistency - ugly things happen when data vs signals mismatch + if ((OriginalSize != RecordSize)&& + (OriginalSize != 0)) + { + ok = false; + SG_LOG(SG_SYSTEMS, SG_ALERT, "Error: Data inconsistency. Flight recorder tape has record size " << RecordSize + << ", expected size was " << OriginalSize << "."); + } + + if (ok) + ok &= loadRawReplayData(input, m_pRecorder, short_term, RecordSize); + if (ok) + ok &= loadRawReplayData(input, m_pRecorder, medium_term, RecordSize); + if (ok) + ok &= loadRawReplayData(input, m_pRecorder, long_term, RecordSize); + + // restore replay messages + if (ok) + { + copyProperties(MetaDataProps->getNode("messages", 0, true), + fgGetNode("/sim/replay/messages", 0, true)); + } + sim_time = get_end_time(); + // TODO we could (re)store these too + last_mt_time = last_lt_time = sim_time; + } + /* done *********************************************************/ + } + + input.close(); + + if (!Preview) + { + if (ok) + { + guiMessage("Flight recorder tape loaded successfully!"); + start(true); + } + else + guiMessage("Failed to load tape. See log output."); + } + + return ok; +} + +/** List available tapes in current directory. + * Limits to tapes matching current aircraft when SameAircraftFilter is enabled. + */ +bool +FGReplay::listTapes(bool SameAircraftFilter, const SGPath& tapeDirectory) +{ + const std::string& aircraftType = simgear::strutils::uppercase(fgGetString("/sim/aircraft", "unknown")); + + // process directory listing of ".fgtape" files + simgear::Dir dir(tapeDirectory); + simgear::PathList list = dir.children(simgear::Dir::TYPE_FILE, ".fgtape"); + + SGPropertyNode* TapeList = fgGetNode("/sim/replay/tape-list", true); + TapeList->removeChildren("tape", false); + int Index = 0; + size_t l = aircraftType.size(); + for (simgear::PathList::iterator it = list.begin(); it!=list.end(); ++it) + { + SGPath file(it->file()); + std::string name(file.base()); + if ((!SameAircraftFilter)|| + (0==simgear::strutils::uppercase(name).compare(0,l, aircraftType))) + { + TapeList->getNode("tape", Index++, true)->setStringValue(name); + } + } + + return true; +} + +/** Load a flight recorder tape from disk. User/script command. */ +bool +FGReplay::loadTape(const SGPropertyNode* ConfigData) +{ + SGPath tapeDirectory(fgGetString("/sim/replay/tape-directory", "")); + + // see if shall really load the file - or just obtain the meta data for preview + bool Preview = ConfigData->getBoolValue("preview", 0); + + // file/tape to be loaded + std::string tape = ConfigData->getStringValue("tape", ""); + + if (tape.empty()) + { + if (!Preview) + return listTapes(ConfigData->getBoolValue("same-aircraft", 0), tapeDirectory); + return true; + } + else + { + SGPropertyNode* UserData = fgGetNode("/sim/gui/dialogs/flightrecorder/preview", true); + tapeDirectory.append(tape); + tapeDirectory.concat(".fgtape"); + SG_LOG(SG_SYSTEMS, MY_SG_DEBUG, "Checking flight recorder file " << tapeDirectory << ", preview: " << Preview); + return loadTape(tapeDirectory.c_str(), Preview, UserData); + } +} diff --git a/src/Aircraft/replay.hxx b/src/Aircraft/replay.hxx index 7eaa5913c..3cf3b5af1 100644 --- a/src/Aircraft/replay.hxx +++ b/src/Aircraft/replay.hxx @@ -30,13 +30,12 @@ #include -#include - #include #include #include -using std::deque; +#include +#include class FGFlightRecorder; @@ -46,9 +45,14 @@ typedef struct { /* more data here, hidden to the outside world */ } FGReplayData; -typedef deque < FGReplayData *> replay_list_type; - +typedef struct { + double sim_time; + std::string message; + std::string speaker; +} FGReplayMessages; +typedef std::deque < FGReplayData *> replay_list_type; +typedef std::vector < FGReplayMessages > replay_messages_type; /** * A recording/replay module for FlightGear flights @@ -57,9 +61,7 @@ typedef deque < FGReplayData *> replay_list_type; class FGReplay : public SGSubsystem { - public: - FGReplay (); virtual ~FGReplay(); @@ -68,27 +70,44 @@ public: virtual void bind(); virtual void unbind(); virtual void update( double dt ); - bool start(); + bool start(bool NewTape=false); + + bool saveTape(const SGPropertyNode* ConfigData); + bool loadTape(const SGPropertyNode* ConfigData); private: void clear(); FGReplayData* record(double time); void interpolate(double time, const replay_list_type &list); void replay(double time, FGReplayData* pCurrentFrame, FGReplayData* pOldFrame=NULL); + void guiMessage(const char* message); + void loadMessages(); + void fillRecycler(); bool replay( double time ); + void replayMessage( double time ); + double get_start_time(); double get_end_time(); + bool listTapes(bool SameAircraftFilter, const SGPath& tapeDirectory); + bool saveTape(const char* Filename, SGPropertyNode* MetaData); + bool loadTape(const char* Filename, bool Preview, SGPropertyNode* UserData); + double sim_time; double last_mt_time; double last_lt_time; + double last_msg_time; + replay_messages_type::iterator current_msg; int last_replay_state; + bool was_finished_already; replay_list_type short_term; replay_list_type medium_term; replay_list_type long_term; replay_list_type recycler; + replay_messages_type replay_messages; + SGPropertyNode_ptr disable_replay; SGPropertyNode_ptr replay_master; SGPropertyNode_ptr replay_time; @@ -106,5 +125,4 @@ private: FGFlightRecorder* m_pRecorder; }; - #endif // _FG_REPLAY_HXX diff --git a/src/Main/fg_commands.cxx b/src/Main/fg_commands.cxx index e5d72f8a9..75a5e2fbf 100644 --- a/src/Main/fg_commands.cxx +++ b/src/Main/fg_commands.cxx @@ -298,6 +298,30 @@ do_save (const SGPropertyNode * arg) } } +/** + * Built-in command: save flight recorder tape. + * + */ +static bool +do_save_tape (const SGPropertyNode * arg) +{ + FGReplay* replay = (FGReplay*) globals->get_subsystem("replay"); + replay->saveTape(arg); + + return true; +} +/** + * Built-in command: load flight recorder tape. + * + */ +static bool +do_load_tape (const SGPropertyNode * arg) +{ + FGReplay* replay = (FGReplay*) globals->get_subsystem("replay"); + replay->loadTape(arg); + + return true; +} /** * Built-in command: (re)load the panel. @@ -1445,6 +1469,8 @@ static struct { { "pause", do_pause }, { "load", do_load }, { "save", do_save }, + { "save-tape", do_save_tape }, + { "load-tape", do_load_tape }, { "panel-load", do_panel_load }, { "preferences-load", do_preferences_load }, { "view-cycle", do_view_cycle },