diff --git a/scripts/python/recordreplay.py b/scripts/python/recordreplay.py index 89cf55f4d..fd4b49767 100755 --- a/scripts/python/recordreplay.py +++ b/scripts/python/recordreplay.py @@ -26,6 +26,11 @@ Args: A second fgfs executable. If specified we run all tests twice, first using to create the recording and to replay it, second the other way round. + --test-motion + Checks that speed of aircraft on replay is not affected by frame rate. + + We deliberately change frame rate while recording UFO moving at + constant speed. BOOLS is comma-sparated list of 0 or 1, with 1 activating the particular feature. So for example '--continuous 0' tests normal recording/replay', @@ -77,20 +82,23 @@ class Fg: args += f' --telnet={port}' args2 = args.split() + environ = os.environ.copy() if isinstance(env, str): - env2 = dict() for nv in env.split(): n, v = nv.split('=', 1) - env2[n] = v - env = env2 - environ = os.environ.copy() - if env: - environ.update(env) + environ[n] = v if 'DISPLAY' not in environ: environ['DISPLAY'] = ':0' # Run flightgear in new process, telling it to open telnet server. - self.child = subprocess.Popen(args2) + # + # We run not in a shell, otherwise self.child.terminate() doesn't + # work - it would kill the shell but leave fgfs running (there are + # workarounds for this, such as prefixing the command with 'exec'). + # + log(f'Command is: {args}') + log(f'Running: {args2}') + self.child = subprocess.Popen(args2, env=environ) # Connect to flightgear's telnet server. timeout = 15 @@ -278,11 +286,80 @@ def test_record_replay( log('Test passed') +def test_motion(fgfs): + + aircraft = 'ufo' + fg = Fg( aircraft, f'{fgfs} --aircraft={aircraft}') + path = f'{fg.aircraft}-continuous.fgtape' + + if 0: + items = fg.fg.ls( '/sim') + log( '/sim is:') + for item in items: + log( f' {item}') + fg.close() + return + + fg.waitfor('/sim/fdm-initialized', 1, timeout=45) + + # Run UFO with constant speed, varying the framerate so we check whether + # recorded speeds are affected. + # + fg.fg['/controls/engines/engine[0]/throttle'] = 0.1 + fg.fg['/sim/frame-rate-throttle-hz'] = 5 + + # Delay to let frame rate settle. + time.sleep(10) + + # Start recording. + fg.fg['/sim/replay/record-continuous'] = 1 + time.sleep(5) + + # Change frame rate. + fg.fg['/sim/frame-rate-throttle-hz'] = 2 + time.sleep(5) + + # Restore original frame rate. + fg.fg['/sim/frame-rate-throttle-hz'] = 5 + time.sleep(5) + + # Stop recording. + fg.fg['/sim/replay/record-continuous'] = 0 + + fg.close() + + path2 = os.readlink( path) + g_cleanup.append(lambda: os.remove(path2)) + + fg = Fg( aircraft, f'{fgfs} --load-tape={path} --prop:/sim/replay/log-raw-speed=true') + fg.waitfor('/sim/fdm-initialized', 1, timeout=45) + fg.fg['/sim/frame-rate-throttle-hz'] = 10 + fg.waitfor('/sim/replay/replay-state', 1) + fg.waitfor('/sim/replay/replay-state-eof', 1) + + items0 = fg.fg.ls( '/sim/replay/log-raw-speed-values') + items = [] + for item in items0: + if item.name == 'value': + items.append(item) + num_errors = 0 + for item in items[:-1]: # Ignore last item because replay at end interpolates. + speed = float(item.value) + prefix = ' ' + if abs(speed - 200) > 0.5: + num_errors += 1 + prefix = '*' + log( f' {prefix} speed={speed} details: {item}') + fg.close() + assert num_errors == 0, 'Replay showed uneven speed.' + + if __name__ == '__main__': fgfs = f'./build-walk/fgfs.exe-run.sh' fgfs_old = None + do_test = 'all' continuous_s = [0, 1] extra_properties_s = [0, 1] main_view_s = [0, 1] @@ -319,60 +396,67 @@ if __name__ == '__main__': elif arg == '--f-old': fgfs_old = next(args) fgfs_reverse = [0, 1] + elif arg == '--test-motion': + do_test = 'motion' else: raise Exception(f'Unrecognised arg: {arg!r}') - try: - if fgfs_old: - for fgfs1, fgfs2 in [(fgfs, fgfs_old), (fgfs_old, fgfs)]: - for multiplayer in 0, 1: - test_record_replay( - fgfs1, - fgfs2, - multiplayer, - continuous=0, - extra_properties=0, - main_view=0, - length=10, - ) - else: - its_max = len(multiplayer_s) * len(continuous_s) * len(extra_properties_s) * len(main_view_s) * len(fgfs_reverse_s) - it = 0 - for multiplayer in multiplayer_s: - for continuous in continuous_s: - for extra_properties in extra_properties_s: - for main_view in main_view_s: - for fgfs_reverse in fgfs_reverse_s: - if fgfs_reverse: - fgfs_save = fgfs_old - fgfs_load = fgfs - else: - fgfs_save = fgfs - fgfs_load = fgfs_old + if do_test == 'motion': + test_motion( fgfs) + elif do_test == 'all': + try: + if fgfs_old: + for fgfs1, fgfs2 in [(fgfs, fgfs_old), (fgfs_old, fgfs)]: + for multiplayer in 0, 1: + test_record_replay( + fgfs1, + fgfs2, + multiplayer, + continuous=0, + extra_properties=0, + main_view=0, + length=10, + ) + else: + its_max = len(multiplayer_s) * len(continuous_s) * len(extra_properties_s) * len(main_view_s) * len(fgfs_reverse_s) + it = 0 + for multiplayer in multiplayer_s: + for continuous in continuous_s: + for extra_properties in extra_properties_s: + for main_view in main_view_s: + for fgfs_reverse in fgfs_reverse_s: + if fgfs_reverse: + fgfs_save = fgfs_old + fgfs_load = fgfs + else: + fgfs_save = fgfs + fgfs_load = fgfs_old - ok = True - if it_min is not None: - if it < it_min: - ok = False - if it_max is not None: - if it >= it_max: - ok = False - log('') - log(f'===') - log(f'=== {it}/{its_max}') - if ok: - test_record_replay( - fgfs_save, - fgfs_load, - multiplayer=multiplayer, - continuous=continuous, - extra_properties=extra_properties, - main_view=main_view, - length=10 - ) - it += 1 - finally: - pass + ok = True + if it_min is not None: + if it < it_min: + ok = False + if it_max is not None: + if it >= it_max: + ok = False + log('') + log(f'===') + log(f'=== {it}/{its_max}') + if ok: + test_record_replay( + fgfs_save, + fgfs_load, + multiplayer=multiplayer, + continuous=continuous, + extra_properties=extra_properties, + main_view=main_view, + length=10 + ) + it += 1 + finally: + pass + else: + assert 0, f'do_test={do_test}' # If everything passed, cleanup. Otherwise leave recordings in place, as # they can be useful for debugging. diff --git a/src/Aircraft/flightrecorder.cxx b/src/Aircraft/flightrecorder.cxx index 520f27b4d..da321e263 100644 --- a/src/Aircraft/flightrecorder.cxx +++ b/src/Aircraft/flightrecorder.cxx @@ -230,6 +230,7 @@ FGFlightRecorder::FGFlightRecorder(const char* pConfigName) : m_ReplayMainWindowSize (fgGetNode("/sim/replay/replay-main-window-size", true)), m_RecordContinuous (fgGetNode("/sim/replay/record-continuous", true)), m_RecordExtraProperties (fgGetNode("/sim/replay/record-extra-properties", true)), + m_LogRawSpeed (fgGetNode("/sim/replay/log-raw-speed", true)), m_TotalRecordSize(0), m_ConfigName(pConfigName), m_usingDefaultConfig(false), @@ -775,6 +776,7 @@ FGFlightRecorder::replay(double SimTime, const FGReplayData* _pNextBuffer, const double* pDoubles = (const double*) &pBuffer[Offset]; const double* pLastDoubles = (const double*) &pLastBuffer[Offset]; unsigned int SignalCount = m_CaptureDouble.size(); + for (unsigned int i=0; isetDoubleValue(v); } + + if (m_LogRawSpeed->getBoolValue()) { + // Log raw speed values to + // /sim/replay/log-raw-speed-values/value[]. This is used by + // scripts/python/recordreplay.py --test-motion. + // + SGGeod pos_geod = SGGeod::fromDegFt( + fgGetDouble("/position/longitude-deg"), + fgGetDouble("/position/latitude-deg"), + fgGetDouble("/position/altitude-ft") + ); + SGVec3d pos = SGVec3d::fromGeod(pos_geod); + static SGVec3d pos_prev; + static double simtime_prev = -1; + double dt = SimTime - simtime_prev; + if (simtime_prev != -1 && dt > 0) { + double distance = length(pos - pos_prev); + double speed = dt ? distance / dt : -1; + SG_LOG(SG_GENERAL, SG_DEBUG, "User aircraft:" + << " pLastBuffer=" << ((void*) pLastBuffer) + << " SimTime=" << SimTime + << " dt=" << dt + << " distance=" << distance + << " speed=" << speed + ); + SGPropertyNode* n = fgGetNode("/sim/replay/log-raw-speed-values", true /*create*/); + n->addChild("value")->setDoubleValue(speed); + } + pos_prev = pos; + simtime_prev = SimTime; + } + Offset += SignalCount * sizeof(double); } diff --git a/src/Aircraft/flightrecorder.hxx b/src/Aircraft/flightrecorder.hxx index 4f6267d15..3ab8ef3cb 100644 --- a/src/Aircraft/flightrecorder.hxx +++ b/src/Aircraft/flightrecorder.hxx @@ -95,6 +95,8 @@ private: SGPropertyNode_ptr m_RecordContinuous; SGPropertyNode_ptr m_RecordExtraProperties; + SGPropertyNode_ptr m_LogRawSpeed; + // This contains copy of all properties that we are recording, so that we // can send only differences. // diff --git a/src/Aircraft/replay.cxx b/src/Aircraft/replay.cxx index 8f89d868c..8bf8bad1e 100644 --- a/src/Aircraft/replay.cxx +++ b/src/Aircraft/replay.cxx @@ -356,6 +356,7 @@ FGReplay::init() { disable_replay = fgGetNode("/sim/replay/disable", true); replay_master = fgGetNode("/sim/replay/replay-state", true); + replay_master_eof = fgGetNode("/sim/replay/replay-state-eof", true); replay_time = fgGetNode("/sim/replay/time", true); replay_time_str = fgGetNode("/sim/replay/time-str", true); replay_looped = fgGetNode("/sim/replay/looped", true); @@ -552,6 +553,7 @@ FGReplay::start(bool NewTape) if (0 == replay_master->getIntValue()) { replay_master->setIntValue(1); + replay_master_eof->setIntValue(0); if (NewTape) { // start replay at initial time, when loading a new tape @@ -1010,6 +1012,7 @@ FGReplay::update( double dt ) { if (!was_finished_already) { + replay_master_eof->setIntValue(1); guiMessage("End of tape. 'Esc' to return."); was_finished_already = true; } diff --git a/src/Aircraft/replay.hxx b/src/Aircraft/replay.hxx index ab938b58c..c885b1815 100644 --- a/src/Aircraft/replay.hxx +++ b/src/Aircraft/replay.hxx @@ -223,6 +223,7 @@ private: SGPropertyNode_ptr disable_replay; SGPropertyNode_ptr replay_master; + SGPropertyNode_ptr replay_master_eof; SGPropertyNode_ptr replay_time; SGPropertyNode_ptr replay_time_str; SGPropertyNode_ptr replay_looped;