FSFlightLogger/FSFlightLogger/SimConnectLogger.cs

247 lines
7.6 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using FlightLoggerLib;
using FlightLoggerLib.Concrete;
using Nito.AsyncEx;
using SimConnect;
using SimConnect.Attribute;
using SimConnect.Lib;
namespace FSFlightLogger
{
public class SimConnectLogger : IAsyncDisposable
{
private readonly ISimConnectClient client;
private SimConnectLoggerWorker worker;
public class Config
{
public string CSVOutputPath { get; set; }
public string KMLOutputPath { get; set; }
public int? KMLLivePort { get; set; }
public TimeSpan IntervalTime { get; set; }
public int IntervalDistance { get; set; }
public bool WaitForMovement { get; set; }
public TimeSpan? NewLogWhenIdleSeconds { get; set; }
}
public SimConnectLogger(ISimConnectClient client)
{
this.client = client;
}
public async ValueTask DisposeAsync()
{
// Do not dispose of client, it is managed by the caller
await Stop();
}
public async Task Start(Config config)
{
await Stop();
worker = new SimConnectLoggerWorker(client, config);
}
public async Task Stop()
{
if (worker != null)
{
await worker.DisposeAsync();
worker = null;
}
}
private class SimConnectLoggerWorker : IAsyncDisposable
{
private readonly Config config;
private readonly List<IFlightLogger> loggers = new List<IFlightLogger>();
private IAsyncDisposable definition;
private IAsyncDisposable simEvent;
private IAsyncDisposable pauseEvent;
private DateTime lastTime;
private PositionData lastData = PositionData.Empty();
private bool waitingForMovement;
private DateTime lastStopped;
private readonly AsyncLock simStateLock = new AsyncLock();
private volatile bool paused = true;
private volatile bool running = false;
public SimConnectLoggerWorker(ISimConnectClient client, Config config)
{
this.config = config;
if (!string.IsNullOrEmpty(config.CSVOutputPath))
{
Directory.CreateDirectory(config.CSVOutputPath);
loggers.Add(new CSVFlightLogger(config.CSVOutputPath));
}
if (!string.IsNullOrEmpty(config.KMLOutputPath))
{
Directory.CreateDirectory(config.KMLOutputPath);
loggers.Add(new KMLFlightLogger(config.KMLOutputPath, TimeSpan.FromSeconds(5)));
}
if (config.KMLLivePort.HasValue)
loggers.Add(new KMLLiveFlightLogger(config.KMLLivePort.Value));
waitingForMovement = config.WaitForMovement;
lastStopped = DateTime.Now;
pauseEvent = client.SubscribeToSystemEvent(SimConnectSystemEvent.Pause, HandlePauseEvent);
simEvent = client.SubscribeToSystemEvent(SimConnectSystemEvent.Sim, HandleSimEvent);
definition = client.AddDefinition<PositionData>(HandlePositionData);
}
public async ValueTask DisposeAsync()
{
if (definition != null)
{
await definition.DisposeAsync();
definition = null;
}
if (pauseEvent != null)
{
await pauseEvent.DisposeAsync();
pauseEvent = null;
}
if (simEvent != null)
{
await simEvent.DisposeAsync();
simEvent = null;
}
foreach (var logger in loggers)
await logger.DisposeAsync();
}
private async Task HandlePauseEvent(SimConnectSystemEventArgs args)
{
using (await simStateLock.LockAsync())
{
paused = ((SimConnectPauseSystemEventArgs)args).Paused;
}
}
private async Task HandleSimEvent(SimConnectSystemEventArgs args)
{
using (await simStateLock.LockAsync())
{
running = ((SimConnectSimSystemEventArgs)args).SimRunning;
}
}
private async Task HandlePositionData(PositionData data)
{
if (paused || !running)
return;
var moving = data.Airspeed > 0;
if (!moving && waitingForMovement)
return;
waitingForMovement = false;
// TODO take vertical position into account when going straight up or down (not a common use case, but still)
var distanceMeters = LatLon.DistanceBetweenInMeters(lastData.Latitude, lastData.Longitude, data.Latitude, data.Longitude);
if (distanceMeters < config.IntervalDistance)
{
if (data.Airspeed < 0.1)
data.Airspeed = 0;
// Make an exception if we were last moving and have now stopped, so the 0 velocity record is logged as well
if (data.Airspeed > 0 || lastData.Airspeed == 0)
return;
}
var now = DateTime.Now;
if (config.NewLogWhenIdleSeconds.HasValue)
{
if (moving)
{
if (now - lastStopped >= config.NewLogWhenIdleSeconds)
{
await Task.WhenAll(loggers.Select(logger =>
logger.NewLog()));
}
}
lastStopped = now;
}
var time = now - lastTime;
if (time < config.IntervalTime)
return;
lastTime = now;
lastData = data;
// ReSharper disable once AccessToDisposedClosure - covered by disposing the client first
await Task.WhenAll(loggers.Select(logger =>
logger.LogPosition(now, new FlightPosition
{
Latitude = data.Latitude,
Longitude = data.Longitude,
Altitude = data.Altitude,
Airspeed = data.Airspeed
})));
}
}
// ReSharper disable once ClassNeverInstantiated.Local - it is, by the SimConnect client
public class PositionData
{
[SimConnectVariable("PLANE LATITUDE", "degrees")]
public float Latitude;
[SimConnectVariable("PLANE LONGITUDE", "degrees")]
public float Longitude;
[SimConnectVariable("PLANE ALTITUDE", "feet")]
public float Altitude;
[SimConnectVariable("AIRSPEED INDICATED", "knots")]
public float Airspeed;
public static PositionData Empty()
{
return new PositionData
{
Latitude = 0,
Longitude = 0,
Altitude = 0,
Airspeed = 0
};
}
}
}
}