diff --git a/App.config b/App.config index 56efbc7..50ac543 100644 --- a/App.config +++ b/App.config @@ -1,6 +1,43 @@ - + + + +
+ + + + + + + + + + + + + + + + + + + + + + 200 + + + 100 + + + 2 + + + 10000 + + + \ No newline at end of file diff --git a/App.xaml b/App.xaml index 6a8721e..911b83c 100644 --- a/App.xaml +++ b/App.xaml @@ -1,9 +1,14 @@  + xmlns:local="clr-namespace:Lside_Mixture"> + - + + + + + + diff --git a/App.xaml.cs b/App.xaml.cs index 4edf894..128f92a 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -1,17 +1,79 @@ -using System; -using System.Collections.Generic; -using System.Configuration; -using System.Data; -using System.Linq; -using System.Threading.Tasks; -using System.Windows; - -namespace Lside_Mixture +namespace Lside_Mixture { - /// - /// Interaction logic for App.xaml - /// + using System; + using System.Threading.Tasks; + using System.Windows; + using Lside_Mixture.Services; + using Lside_Mixture.Views; + using Microsoft.Extensions.DependencyInjection; + using Serilog; + public partial class App : Application { + public App() + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .WriteTo.Console() + .CreateLogger(); + + this.Services = ConfigureServices(); + this.ShutdownMode = ShutdownMode.OnMainWindowClose; + + } + + /// + /// Gets the current application + /// note: Not a Lamdba, just a expression-bodied member 'syntax' defining a read-only Property. + /// + public static new App Current => (App)Application.Current; + + // IOC provider + public IServiceProvider Services { get; } + + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + this.SetupExceptionHandling(); + + MainWindow window = new MainWindow(); + window.Show(); + } + + private static IServiceProvider ConfigureServices() + { + var services = new ServiceCollection(); + + services.AddSingleton(); + + return services.BuildServiceProvider(); + } + + private void SetupExceptionHandling() + { + AppDomain.CurrentDomain.UnhandledException += (s, e) => + this.LogUnhandledException((Exception)e.ExceptionObject, "AppDomain.CurrentDomain.UnhandledException"); + + this.DispatcherUnhandledException += (s, e) => + { + this.LogUnhandledException(e.Exception, "Application.Current.DispatcherUnhandledException"); + e.Handled = true; + }; + + TaskScheduler.UnobservedTaskException += (s, e) => + { + this.LogUnhandledException(e.Exception, "TaskScheduler.UnobservedTaskException"); + e.SetObserved(); + }; + } + + private void LogUnhandledException(Exception e, string source) + { + MessageBox.Show($"{source} - {e.Message}"); + System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly(); + string logout = "\n\n" + DateTime.Now.ToString() + "\n" + System.Diagnostics.FileVersionInfo.GetVersionInfo(assembly.Location).FileVersion + "\n" + + e.Message + "\n" + e.Source + "\n" + e.StackTrace; + System.IO.File.AppendAllText(@"./log.txt", logout); + } } } diff --git a/Common/CaptureAndGraphLeaningMessage.cs b/Common/CaptureAndGraphLeaningMessage.cs new file mode 100644 index 0000000..1064971 --- /dev/null +++ b/Common/CaptureAndGraphLeaningMessage.cs @@ -0,0 +1,8 @@ +namespace Lside_Mixture.Common +{ + using CommunityToolkit.Mvvm.Messaging.Messages; + + public class CaptureAndGraphLeaningMessage : RequestMessage + { + } +} diff --git a/GuageUserControl/GaugeViewModel.cs b/GuageUserControl/GaugeViewModel.cs new file mode 100644 index 0000000..b7bc504 --- /dev/null +++ b/GuageUserControl/GaugeViewModel.cs @@ -0,0 +1,86 @@ +namespace Gauge +{ + using System; + using System.ComponentModel; + public class GaugeViewModel : INotifyPropertyChanged + { + + private double minValue; + private double maxValue; + + public event PropertyChangedEventHandler PropertyChanged; + private void NotifyPropertyChanged(string info) + { + if(PropertyChanged!= null) + { + PropertyChanged(this, new PropertyChangedEventArgs(info)); + } + } + + public GaugeViewModel() + { + Angle = -85; + Value = 0; + } + + public GaugeViewModel(double minValue, double maxValue) + { + Angle = -85; + Value = 0; + this.minValue = minValue; + this.maxValue = maxValue; + } + + int _angle; + + // 0 to 170 usage + public int Angle + { + get + { + return _angle; + } + + private set + { + _angle = value; + NotifyPropertyChanged("Angle"); + } + } + + // minValue to maxValue usage + public int ScaledValue + { + get + { + return _value; + } + + set + { + _value = value; + Angle = Convert.ToInt32((170.0 / this.maxValue) * (value - this.minValue)); + NotifyPropertyChanged("Value"); + } + } + + int _value; + public int Value + { + get + { + return _value; + } + + set + { + if (value >= 0 && value <= 170) + { + _value = value; + Angle = value - 85; + NotifyPropertyChanged("Value"); + } + } + } + } +} diff --git a/GuageUserControl/GuageUserControl.csproj b/GuageUserControl/GuageUserControl.csproj new file mode 100644 index 0000000..76658bf --- /dev/null +++ b/GuageUserControl/GuageUserControl.csproj @@ -0,0 +1,85 @@ + + + + + Debug + AnyCPU + {59BA7115-277A-4CB0-A039-B216AC0D87E1} + library + GuageUserControl + GuageUserControl + v4.7.2 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + true + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + 4.0 + + + + + + + + UserControlGauge.xaml + + + + MSBuild:Compile + Designer + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + \ No newline at end of file diff --git a/GuageUserControl/Properties/AssemblyInfo.cs b/GuageUserControl/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..17753f9 --- /dev/null +++ b/GuageUserControl/Properties/AssemblyInfo.cs @@ -0,0 +1,55 @@ +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Windows; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("GuageUserControl")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("GuageUserControl")] +[assembly: AssemblyCopyright("Copyright © 2023")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +//In order to begin building localizable applications, set +//CultureYouAreCodingWith in your .csproj file +//inside a . For example, if you are using US english +//in your source files, set the to en-US. Then uncomment +//the NeutralResourceLanguage attribute below. Update the "en-US" in +//the line below to match the UICulture setting in the project file. + +//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] + + +[assembly:ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] + + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/GuageUserControl/Properties/Resources.Designer.cs b/GuageUserControl/Properties/Resources.Designer.cs new file mode 100644 index 0000000..234208f --- /dev/null +++ b/GuageUserControl/Properties/Resources.Designer.cs @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace GuageUserControl.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GuageUserControl.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/GuageUserControl/Properties/Resources.resx b/GuageUserControl/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/GuageUserControl/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/GuageUserControl/Properties/Settings.Designer.cs b/GuageUserControl/Properties/Settings.Designer.cs new file mode 100644 index 0000000..929c2e2 --- /dev/null +++ b/GuageUserControl/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace GuageUserControl.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.5.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/GuageUserControl/Properties/Settings.settings b/GuageUserControl/Properties/Settings.settings new file mode 100644 index 0000000..033d7a5 --- /dev/null +++ b/GuageUserControl/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/GuageUserControl/UserControlGauge.xaml b/GuageUserControl/UserControlGauge.xaml new file mode 100644 index 0000000..5a85d7b --- /dev/null +++ b/GuageUserControl/UserControlGauge.xaml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/GuageUserControl/UserControlGauge.xaml.cs b/GuageUserControl/UserControlGauge.xaml.cs new file mode 100644 index 0000000..502e3b3 --- /dev/null +++ b/GuageUserControl/UserControlGauge.xaml.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace Gauge +{ + /// + /// Interaction logic for UserControlGauge.xaml + /// + public partial class UserControlGauge : UserControl + { + public UserControlGauge() + { + Angle = -85; + Value = 0; + InitializeComponent(); + } + + int _maxValue = 170; + public int maxValue + { + get + { + return _maxValue; + } + + set + { + _maxValue = value; + } + } + + int _value; + public int Value + { + get + { + return Convert.ToInt32(_value * maxValue / 170); + } + + set + { + if (value >= 0 && value <= maxValue) + { + _value = value; + Angle = Convert.ToInt32(value * 170 / maxValue); + } + } + } + + // dependent property + int _angle; + public int Angle + { + get + { + return _angle; + } + + private set + { + _angle = value; + } + } + } +} diff --git a/Lside-Mixture.csproj b/Lside-Mixture.csproj index f400568..a140bc2 100644 --- a/Lside-Mixture.csproj +++ b/Lside-Mixture.csproj @@ -14,6 +14,23 @@ 4 true true + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + + AnyCPU @@ -34,9 +51,95 @@ prompt 4 + + true + bin\x64\Debug\ + DEBUG;TRACE + full + x64 + 7.3 + prompt + true + + + bin\x64\Release\ + TRACE + true + pdbonly + x64 + 7.3 + prompt + true + + + packages\CommunityToolkit.Mvvm.8.2.1\lib\netstandard2.0\CommunityToolkit.Mvvm.dll + + + packages\CTrue.FsConnect.1.3.3\lib\net45\CTrue.FsConnect.dll + + + packages\Microsoft.Bcl.AsyncInterfaces.7.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll + + + packages\Microsoft.Extensions.DependencyInjection.7.0.0\lib\net462\Microsoft.Extensions.DependencyInjection.dll + + + packages\Microsoft.Extensions.DependencyInjection.Abstractions.7.0.0\lib\net462\Microsoft.Extensions.DependencyInjection.Abstractions.dll + + + packages\CTrue.FsConnect.1.3.3\lib\net45\Microsoft.FlightSimulator.SimConnect.dll + + + packages\Octokit.7.0.1\lib\netstandard2.0\Octokit.dll + + + + packages\ScottPlot.4.1.65\lib\net462\ScottPlot.dll + + + packages\ScottPlot.WPF.4.1.65\lib\net472\ScottPlot.WPF.dll + + + packages\Serilog.3.0.1\lib\net471\Serilog.dll + + + packages\Serilog.Sinks.Console.4.1.0\lib\net45\Serilog.Sinks.Console.dll + + + packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + + + packages\System.ComponentModel.Annotations.5.0.0\lib\net461\System.ComponentModel.Annotations.dll + + + + + packages\System.Drawing.Common.4.7.2\lib\net461\System.Drawing.Common.dll + + + packages\System.Memory.4.5.5\lib\net461\System.Memory.dll + + + + packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + + + packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll + True + True + + + packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + + + packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll + @@ -55,7 +158,20 @@ MSBuild:Compile Designer - + + + + + + + + + MixtureChartWindow.xaml + + + PeakEGTWindow.xaml + + MSBuild:Compile Designer @@ -63,12 +179,40 @@ App.xaml Code - + + + + MainWindow.xaml Code + + MSBuild:Compile + Designer + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + + + + + + + Mixture.xaml + Code @@ -86,6 +230,7 @@ ResXFileCodeGenerator Resources.Designer.cs + SettingsSingleFileGenerator Settings.Designer.cs @@ -94,5 +239,34 @@ + + + {59ba7115-277a-4cb0-a039-b216ac0d87e1} + GuageUserControl + + + + + False + Microsoft .NET Framework 4.7.2 %28x86 and x64%29 + true + + + False + .NET Framework 3.5 SP1 + false + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/Lside-Mixture.sln b/Lside-Mixture.sln index 1f34835..0699541 100644 --- a/Lside-Mixture.sln +++ b/Lside-Mixture.sln @@ -5,16 +5,37 @@ VisualStudioVersion = 17.5.33516.290 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lside-Mixture", "Lside-Mixture.csproj", "{CC802ADB-AEDF-4D54-8938-19D1C2C84A33}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GuageUserControl", "GuageUserControl\GuageUserControl.csproj", "{59BA7115-277A-4CB0-A039-B216AC0D87E1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2248D76B-BF6F-494A-ABD6-66441E7EF626}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {CC802ADB-AEDF-4D54-8938-19D1C2C84A33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CC802ADB-AEDF-4D54-8938-19D1C2C84A33}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC802ADB-AEDF-4D54-8938-19D1C2C84A33}.Debug|x64.ActiveCfg = Debug|x64 + {CC802ADB-AEDF-4D54-8938-19D1C2C84A33}.Debug|x64.Build.0 = Debug|x64 {CC802ADB-AEDF-4D54-8938-19D1C2C84A33}.Release|Any CPU.ActiveCfg = Release|Any CPU {CC802ADB-AEDF-4D54-8938-19D1C2C84A33}.Release|Any CPU.Build.0 = Release|Any CPU + {CC802ADB-AEDF-4D54-8938-19D1C2C84A33}.Release|x64.ActiveCfg = Release|x64 + {CC802ADB-AEDF-4D54-8938-19D1C2C84A33}.Release|x64.Build.0 = Release|x64 + {59BA7115-277A-4CB0-A039-B216AC0D87E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59BA7115-277A-4CB0-A039-B216AC0D87E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59BA7115-277A-4CB0-A039-B216AC0D87E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {59BA7115-277A-4CB0-A039-B216AC0D87E1}.Debug|x64.Build.0 = Debug|Any CPU + {59BA7115-277A-4CB0-A039-B216AC0D87E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59BA7115-277A-4CB0-A039-B216AC0D87E1}.Release|Any CPU.Build.0 = Release|Any CPU + {59BA7115-277A-4CB0-A039-B216AC0D87E1}.Release|x64.ActiveCfg = Release|Any CPU + {59BA7115-277A-4CB0-A039-B216AC0D87E1}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs deleted file mode 100644 index 908d89b..0000000 --- a/MainWindow.xaml.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; - -namespace Lside_Mixture -{ - /// - /// Interaction logic for MainWindow.xaml - /// - public partial class MainWindow : Window - { - public MainWindow() - { - InitializeComponent(); - } - } -} diff --git a/Managers/EngineManager.cs b/Managers/EngineManager.cs new file mode 100644 index 0000000..5ba8aa9 --- /dev/null +++ b/Managers/EngineManager.cs @@ -0,0 +1,100 @@ +namespace Lside_Mixture.Managers +{ + using System; + using System.Runtime.InteropServices; + using CTrue.FsConnect; + + /// + /// The controls the engine in the current aircraft. + /// + /// + /// Supports: + /// - Get and set heading bug. + /// + /// Usage: + /// Call to refresh properties with latest values from MSFS. + /// + public interface IEngineManager : IFsConnectManager + { + /// + /// Gets the current Mixture, in %. + /// + double Mixture { get; } + + /// + /// Sets the engine Mixture, in %. + /// + /// + void SetMixture(double mixture); + } + + public class EngineManager : FsConnectManager, IEngineManager + { + private int _eventGroupId; + + private EngineSimVars _engineSimVars = new EngineSimVars(); + private int _engineManagerSimVarsReqId; + private int _engineManagerSimVarsDefId; + + private int _mixtureSetEventId; + + /// + public double Mixture { get; private set; } + + /// + /// Creates a new instance. + /// + /// + public EngineManager(IFsConnect fsConnect) + : base(fsConnect) + { + } + + protected override void RegisterSimVars() + { + _engineManagerSimVarsReqId = _fsConnect.GetNextId(); + _engineManagerSimVarsDefId = _fsConnect.RegisterDataDefinition(); + } + + protected override void RegisterEvents() + { + _eventGroupId = _fsConnect.GetNextId(); + _mixtureSetEventId = _fsConnect.GetNextId(); + _fsConnect.MapClientEventToSimEvent(_eventGroupId, _mixtureSetEventId, FsEventNameId.Mixture1Set); + + _fsConnect.SetNotificationGroupPriority(_eventGroupId); + } + + protected override void OnFsDataReceived(object sender, FsDataReceivedEventArgs e) + { + if (e.Data.Count == 0) return; + if (!(e.Data[0] is EngineSimVars)) return; + + _engineSimVars = (EngineSimVars)e.Data[0]; + _resetEvent.Set(); + } + + /// + public override void Update() + { + _fsConnect.RequestData(_engineManagerSimVarsReqId, _engineManagerSimVarsDefId); + WaitForUpdate(); + + Mixture = _engineSimVars.Mixture; + } + + /// + public void SetMixture(double mixture) + { + int mix16k = Convert.ToInt32(mixture * 16383.0 / 100.0); + _fsConnect.TransmitClientEvent(_mixtureSetEventId, (uint)mix16k, _eventGroupId); + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)] + internal struct EngineSimVars + { + [SimVar(NameId = FsSimVar.GeneralEngMixtureLeverPosition, Instance = 1, UnitId = FsUnit.PercentScaler16k)] + public double Mixture; + } + } +} diff --git a/Managers/FsConnectManager.cs b/Managers/FsConnectManager.cs new file mode 100644 index 0000000..42d26ce --- /dev/null +++ b/Managers/FsConnectManager.cs @@ -0,0 +1,57 @@ + + +namespace Lside_Mixture.Managers +{ + using System; + using System.Threading; + using CTrue.FsConnect; + + public interface IFsConnectManager + { + void Update(); + } + + public abstract class FsConnectManager : IFsConnectManager + { + protected readonly IFsConnect _fsConnect; + protected AutoResetEvent _resetEvent = new AutoResetEvent(false); + private int _timeout = 5_000; + + protected FsConnectManager(IFsConnect fsConnect) + { + _fsConnect = fsConnect; + + _fsConnect.FsDataReceived += OnFsDataReceived; + } + + public virtual void Initialize() + { + RegisterSimVars(); + RegisterEvents(); + } + + protected virtual void OnFsDataReceived(object sender, FsDataReceivedEventArgs e) + { + } + + protected virtual void RegisterSimVars() + { + } + + protected virtual void RegisterEvents() + { + } + + protected void WaitForUpdate() + { + bool resetRes = _resetEvent.WaitOne(_timeout); + + if (!resetRes) + throw new TimeoutException("Updated data was not returned from MSFS within timeout"); + } + + public virtual void Update() + { + } + } +} diff --git a/Models/SampleModel.cs b/Models/SampleModel.cs new file mode 100644 index 0000000..496d36a --- /dev/null +++ b/Models/SampleModel.cs @@ -0,0 +1,16 @@ +namespace Lside_Mixture.Models +{ + using Lside_Mixture.Services; + + // Model expose data for consumption by WPF.It implements INotifyPropertyChanged. + public class SampleModel : BindableBase + { + public PlaneInfoResponse planeInfoResponse { get; private set; } + + public void Handle(PlaneInfoResponse response) + { + this.planeInfoResponse = response; + this.OnPropertyChanged("PlaneInfoResponse"); + } + } +} diff --git a/Properties/Settings.Designer.cs b/Properties/Settings.Designer.cs index bd02c59..c268b7b 100644 --- a/Properties/Settings.Designer.cs +++ b/Properties/Settings.Designer.cs @@ -8,23 +8,67 @@ // //------------------------------------------------------------------------------ -namespace Lside_Mixture.Properties -{ - - +namespace Lside_Mixture.Properties { + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase - { - + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.5.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default - { - get - { + + public static Settings Default { + get { return defaultInstance; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("200")] + public int RpmDropThreshold { + get { + return ((int)(this["RpmDropThreshold"])); + } + set { + this["RpmDropThreshold"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("100")] + public int EgtDropThreshold { + get { + return ((int)(this["EgtDropThreshold"])); + } + set { + this["EgtDropThreshold"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("2")] + public double TempStabilisedThreshold { + get { + return ((double)(this["TempStabilisedThreshold"])); + } + set { + this["TempStabilisedThreshold"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("10000")] + public int EgtChangeWaitDelayMSec { + get { + return ((int)(this["EgtChangeWaitDelayMSec"])); + } + set { + this["EgtChangeWaitDelayMSec"] = value; + } + } } } diff --git a/Properties/Settings.settings b/Properties/Settings.settings index 033d7a5..043c9a1 100644 --- a/Properties/Settings.settings +++ b/Properties/Settings.settings @@ -1,7 +1,18 @@  - - - - - + + + + + 200 + + + 100 + + + 2 + + + 10000 + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5aed42f --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +## Overview + +* C# Windows application that captures and displays Mixture data from the Microsoft Flight Simulator 2020. +* The application automatically connects to the running instance of MSFS. +* Its main control window can be hidden or moved to another screen. + +## Notes + +* A cold engine (or one started at height from the map) will not have stable EGT temperature until the engine is warmed. +* The application amends the mixture lever and could potentially starve the engine. Safety Thresholds are adjustable + +## Operation + +* The application status need to be Connected before it can function. +* Adjustments are only made to Engine 1 +* Graph button + - Takes the current Mixture value as its rich base + - decreases (leans) the mixture in 5% steps capturing engine EGT and RPM after 10 seconds + + EGT takes long time (10's of seconds) to stabilise if mixture changes significantly + + RPM stabilises pretty quickly (couple seconds) + - leaning continues to a 15% mixture, but halts if RPM drops more than a safe RPM from its peak + - The initial mixture % is restored after the computations & graph is shown + + +## Configuration + +* The file Lside-Mixture.exe.config contains configuration properties for the above. If you edit it, you need to follow the files obvious pattern. + - RpmDropThreshold + * max acceptable drop in RPM + - EgtDropThreshold + * max acceptable drom in Peak EGT, from observed peak + - TempStabilisedThreshold + * difference between 2 successive reading, need to be below this value to be 'stable' + - EgtChangeWaitDelayMSec + * max time we will wait for egt to stabilise + +## To Install (Tested on 64 bit windows only) + +Link to executable: https://github.com/JJ-blip/lside/releases/download/v1.0.0/Lside-Mixture.V100.zip + +* unzip the application zip (e.g. Lside-Mixture.V100.zip) +* execute Lside-Mixture.exe + +Lside.exe.config contains user changeable properties. + +## Known issues +* To early to say + +## License +Distributed under the GNU General Public License v3.0 License. \ No newline at end of file diff --git a/Styles/DataGrid.xaml b/Styles/DataGrid.xaml new file mode 100644 index 0000000..80c1b9c --- /dev/null +++ b/Styles/DataGrid.xaml @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Utils/BoundedQueue.cs b/Utils/BoundedQueue.cs new file mode 100644 index 0000000..551dd22 --- /dev/null +++ b/Utils/BoundedQueue.cs @@ -0,0 +1,77 @@ +namespace Lside_Mixture.Utils +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; + + public class BoundedQueue : IEnumerable + { + private readonly LinkedList internalList = new LinkedList(); + private readonly int maxQueueSize; + + public BoundedQueue(int queueSize) + { + if (queueSize < 0) + { + throw new ArgumentException("queueSize"); + } + + this.maxQueueSize = queueSize; + } + + public BoundedQueue(BoundedQueue responses) + { + this.maxQueueSize = responses.maxQueueSize; + foreach (T item in this.internalList) + { + this.internalList.AddLast(item); + } + } + + // adds to End, oldest will have at position [0] + public void Enqueue(T elem) + { + if (this.internalList.Count == this.maxQueueSize) + { + // make room + this.Dequeue(); + } + + this.internalList.AddLast(elem); + } + + public T Dequeue() + { + if (this.internalList.Count == 0) + { + throw new BoundedQueueException("Empty"); + } + + T elem = this.internalList.First.Value; + this.internalList.RemoveFirst(); + return elem; + } + + public T ElementAt(int v) + { + return this.internalList.ElementAt(v); + } + + public int Count() + { + return this.internalList.Count; + } + + public IEnumerator GetEnumerator() + { + return this.internalList.GetEnumerator(); + } + + // updates on the returned list will compromise the BoundedQueue + public LinkedList GetInternalLinkList() + { + return this.internalList; + } + } +} diff --git a/Utils/BoundedQueueException.cs b/Utils/BoundedQueueException.cs new file mode 100644 index 0000000..9479a24 --- /dev/null +++ b/Utils/BoundedQueueException.cs @@ -0,0 +1,28 @@ +namespace Lside_Mixture.Utils +{ + using System; + using System.Runtime.Serialization; + + [Serializable] + internal class BoundedQueueException : Exception + { + public BoundedQueueException() + { + } + + public BoundedQueueException(string message) + : base(message) + { + } + + public BoundedQueueException(string message, Exception innerException) + : base(message, innerException) + { + } + + protected BoundedQueueException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} \ No newline at end of file diff --git a/Utils/TaskUtil.cs b/Utils/TaskUtil.cs new file mode 100644 index 0000000..7e3230f --- /dev/null +++ b/Utils/TaskUtil.cs @@ -0,0 +1,40 @@ +namespace Lside_Mixture.Utils +{ + using System; + using System.Diagnostics; + using System.Threading; + using Serilog; + + public class TaskUtil + { + /// + /// Blocks until condition is true or timeout occurs. + /// + /// The break condition. + /// The frequency at which the condition will be checked. + /// The timeout in milliseconds. + /// minimum CYCLE delay in msec + /// How long it waited in seconds + public int WaitUntil(double timeSpan, ConditionCallback condition, int minDelaymsec = 500) + { + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); + + bool _waitUntilTrue = false; + System.Timers.Timer timer = new System.Timers.Timer(timeSpan); + timer.Elapsed += delegate { _waitUntilTrue = true; }; + timer.AutoReset = false; + timer.Start(); + + while (!(_waitUntilTrue || condition())) + { + Thread.Sleep(minDelaymsec); + } + stopwatch.Stop(); + + return Convert.ToInt32(stopwatch.Elapsed.TotalSeconds); + } + + public delegate bool ConditionCallback(); + } +} diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..f570e40 --- /dev/null +++ b/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,120 @@ +namespace Lside_Mixture.ViewModels +{ + using System.ComponentModel; + using Serilog; + using Gauge; + using Lside_Mixture.Services; + using Microsoft.Extensions.DependencyInjection; + + public class MainWindowViewModel : BindableBase + { + private readonly ISimService simService = App.Current.Services.GetService(); + + private bool updatable = false; + + private bool connected = false; + + public MainWindowViewModel() + { + this.connected = false; + this.updatable = false; + + ((INotifyPropertyChanged)this.simService).PropertyChanged += this.Connected_PropertyChanged; + } + + + public SampleViewModel SampleViewModel { get; set; } + + public GaugeViewModel GaugeViewModel { get; set; } + + public string Version + { + get + { + System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly(); + System.Diagnostics.FileVersionInfo fvi = System.Diagnostics.FileVersionInfo.GetVersionInfo(assembly.Location); + string myversion = fvi.FileVersion; + return myversion; + } + } + + public bool Connected + { + get + { + return this.connected; + } + + set + { + this.connected = value; + this.OnPropertyChanged("ConnectedColor"); + this.OnPropertyChanged("ConnectedString"); + } + } + + public string ConnectedString + { + get + { + if (this.Connected) + { + return "Connected"; + } + else + { + return "Disconnected"; + } + } + } + + public string ConnectedColor + { + get + { + if (!this.Connected) + { + return "#FFE63946"; + } + else + { + return "#ff02c39a"; + } + } + } + + public bool UpdateAvailable + { + get + { + return this.updatable; + } + + set + { + this.updatable = value; + this.OnPropertyChanged(); + } + } + + private void Connected_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case "Connected": + { + if (this.simService.Connected) + { + this.Connected = true; + } + else + { + this.Connected = false; + } + + break; + } + } + } + } +} diff --git a/ViewModels/MixtureChartViewModel.cs b/ViewModels/MixtureChartViewModel.cs new file mode 100644 index 0000000..eda46c5 --- /dev/null +++ b/ViewModels/MixtureChartViewModel.cs @@ -0,0 +1,183 @@ +namespace Lside_Mixture.ViewModels +{ + using System; + using System.Data; + using System.Drawing.Printing; + using System.Globalization; + using System.Linq; + using System.Threading; + using Lside_Mixture.Services; + using Lside_Mixture.Utils; + using Lside_Mixture.Views; + using Microsoft.Extensions.DependencyInjection; + using Serilog; + + public class MixtureChartViewModel : BindableBase + { + // max acceptable drop in RPM + private int RpmDropThreshold = Properties.Settings.Default.RpmDropThreshold; + // max acceptable drom in Peak EGT, from observed peak. + private int EgtDropThreshold = Properties.Settings.Default.EgtDropThreshold; + // difference between 2 successive reading, need to be below this value to be 'stable' + private double TempStabilisedThreshold = Properties.Settings.Default.TempStabilisedThreshold; + // max time we will wait for egt to stabilise + private int EgtChangeWaitDelayMSec = Properties.Settings.Default.EgtChangeWaitDelayMSec; + + private readonly ISimService simservice = App.Current.Services.GetService(); + + private BoundedQueue EgtSamples; + + public MixtureChartViewModel(MixtureChartWindow mixtureChartWindow) + { + this.MixtureChartWindow = mixtureChartWindow; + + this.CaptureAndGraphMixture(); + DataTable dt = this.GetDataTable(mixtureArray); + this.BuildArrays(dt); + } + + public MixtureChartWindow MixtureChartWindow { get; } + + public PlaneInfoResponse[] MixtureArray + { + get { return mixtureArray; } + private set { SetProperty(ref mixtureArray, value, "MixtureArray"); } + } + + public void BuildArrays(DataTable dt) + { + DataRow[] rows = dt.Select(); + + double[] altitude = rows.Select(row => row[1].ToString()).ToArray() + .Select(d => Convert.ToDouble(d)).ToArray(); + this.MixtureChartWindow.Altitude = new PlotData("Altitude", altitude); + + double[] rpm = rows.Select(row => row[2].ToString()).ToArray() + .Select(d => Convert.ToDouble(d)).ToArray(); + this.MixtureChartWindow.RPM = new PlotData("RPM", rpm); + + double[] egt = rows.Select(row => row[3].ToString()).ToArray() + .Select(d => Convert.ToDouble(d)).ToArray(); + this.MixtureChartWindow.EGT = new PlotData("EGT", egt); + + double[] throttle = rows.Select(row => row[4].ToString()).ToArray() + .Select(d => Convert.ToDouble(d)).ToArray(); + this.MixtureChartWindow.Throttle = new PlotData("Throttle", throttle); + + double[] mixture = rows.Select(row => row[5].ToString()).ToArray() + .Select(d => Convert.ToDouble(d)).ToArray(); + this.MixtureChartWindow.Mixture = new PlotData("Mixture", mixture); + + } + + private PlaneInfoResponse planeInfoResponseSample; + + private PlaneInfoResponse[] mixtureArray; + + private void CaptureAndGraphMixture() + { + // leans mixture in 5% steps capturing egt etc + // stops after peak (or rpm drops) + + MixtureArray = new PlaneInfoResponse[50]; + MixtureArray[0] = simservice.MostRecentSample; + var initialMixture = MixtureArray[0].Mixture; + var mix = initialMixture; + + int idx = 1; + while (mix > 15 && this.RpmOk() && this.EgtOk() && idx < (MixtureArray.Length - 1)) + { + EgtSamples = new BoundedQueue(5); + + mix = mix - 3; + simservice.SetMixture(mix); + + // practically never stabilises in 5 s , thus we always waits 5 seconds + int waitDelay = new TaskUtil().WaitUntil(EgtChangeWaitDelayMSec, EGTStabilised); + + { + Log.Information(String.Format("reading {0:0000} F at {1:00}% after {2} sec", simservice.MostRecentSample.EGT, simservice.MostRecentSample.Mixture, waitDelay)); + } + + MixtureArray[idx++] = simservice.MostRecentSample; + } + simservice.SetMixture(initialMixture); + MixtureArray = MixtureArray.Where(x => x.Mixture != 0).ToArray(); + } + + // return true if RPM has not dropped significantly from its peak + private bool RpmOk() + { + double[] rpmArray = MixtureArray.Where(x => x.Mixture != 0).ToArray().Select(item => item.RPM).ToArray(); + double peak = rpmArray.Max(); + if ((rpmArray.Length == 1) || peak - rpmArray[rpmArray.Length -1] < RpmDropThreshold) + { + // not enough data, or most current value has dropped too much + return true; + } + Log.Debug("RPM has dropped to {0:0} from its {1:0} peak", rpmArray[rpmArray.Length - 1], peak ); + return false; + } + + private bool EgtOk() + { + double[] egtArray = MixtureArray.Where(x => x.Mixture != 0).ToArray().Select(item => item.EGT).ToArray(); + double peak = egtArray.Max(); + if ((egtArray.Length == 1) || peak - egtArray[egtArray.Length - 1] < EgtDropThreshold) + { + // not enough data, or most current value has dropped too much + return true; + } + Log.Debug("EGT has dropped to {0:0} from its {1:0} peak", egtArray[egtArray.Length - 1], peak); + return false; + } + + // EGT takes a long time (too long) to stabalise, so in practice, waiting 5 seconds just hives an indicative temperature, RPM is quicker + private bool EGTStabilised() + { + bool stabilised = false; + + if (EgtSamples.Count() < 3) + { + // need 3 samples at least 1 second apart + EgtSamples.Enqueue(simservice.MostRecentSample.EGT); + return false; + } + + EgtSamples.Enqueue(simservice.MostRecentSample.EGT); + var diff = Math.Abs(EgtSamples.ElementAt(EgtSamples.Count() - 1) - EgtSamples.ElementAt(EgtSamples.Count() - 2)); + if (diff < TempStabilisedThreshold) + { + // last 2 + stabilised = true; + } + + return stabilised; + } + + private DataTable GetDataTable(PlaneInfoResponse[] mixtureArray) + { + DataTable dt = new DataTable(); + dt.Columns.Add(new DataColumn("Title")); + dt.Columns.Add(new DataColumn("Altitude")); + dt.Columns.Add(new DataColumn("RPM")); + dt.Columns.Add(new DataColumn("EGT")); + dt.Columns.Add(new DataColumn("Throttle")); + dt.Columns.Add(new DataColumn("Mixture")); + + for (int rowidx = 0; rowidx < MixtureArray.Length; rowidx++) + { + DataRow row = dt.NewRow(); + row["Title"] = mixtureArray[rowidx].Title; + row["Altitude"] = mixtureArray[rowidx].Altitude; + row["RPM"] = mixtureArray[rowidx].RPM; + row["EGT"] = mixtureArray[rowidx].EGT; + row["Throttle"] = mixtureArray[rowidx].Throttle; + row["Mixture"] = mixtureArray[rowidx].Mixture; + dt.Rows.Add(row); + } + return dt; + } + + } +} diff --git a/ViewModels/PeakEGTViewModel.cs b/ViewModels/PeakEGTViewModel.cs new file mode 100644 index 0000000..8236078 --- /dev/null +++ b/ViewModels/PeakEGTViewModel.cs @@ -0,0 +1,172 @@ +namespace Lside_Mixture.ViewModels +{ + using System; + using System.Threading; + using Lside_Mixture.Services; + using Lside_Mixture.Utils; + using Lside_Mixture.Views; + using Microsoft.Extensions.DependencyInjection; + using Serilog; + + public class PeakEGTViewModel : BindableBase + { + private readonly ISimService simservice = App.Current.Services.GetService(); + + private readonly int INITIAL_STEP_PERCENTAGE = -10; + private readonly int DELAY_MSEC = 10000; + private readonly double CONVERGENCE_EPSILON = 2.0; + + public PeakEGTViewModel(PeakEGTWindow peakEGTWindow) + { + this.PeakEGTWindow = peakEGTWindow; + + var peak = this.PeakEGT(); + + Log.Information(String.Format("Peak at {0:0} F", peak)); + } + + public PeakEGTWindow PeakEGTWindow { get; } + + + private double PeakEGT() + { + var samples = new BoundedQueue<(double temp, double mixture)>(3); + + var nowEGT = simservice.MostRecentSample.EGT; + var nowMixture = simservice.MostRecentSample.Mixture; + samples.Enqueue((nowEGT, nowMixture)); + + /* Preload samples with 2 values to enable SA to make decesion about next + */ + + // 1st: lean by step% + int step = INITIAL_STEP_PERCENTAGE; + var nextMixture = nowMixture + step; + simservice.SetMixture(nextMixture); + var nextEGT = NextEGT(); + samples.Enqueue((nextEGT, nextMixture)); + + // 2nd: lean by step% once more + nextMixture = samples.ElementAt(1).mixture + step; + simservice.SetMixture(nextMixture); + nextEGT = NextEGT(); + samples.Enqueue((nextEGT, nextMixture)); + + { + // t0 is the 1st change in EGT arising from a m0 change in mixture + var t0 = samples.ElementAt(1).temp - samples.ElementAt(0).temp; + var m0 = samples.ElementAt(1).mixture - samples.ElementAt(0).mixture; + Log.Information(String.Format("1st Mixture change {0:####.#}% (to {3,5:0.0}%) temperature change {1:##0.0} F (to {2:####} F)", m0, t0, samples.ElementAt(1).temp, samples.ElementAt(1).mixture)); + + // t1 is the next change in EGT arising from a m1 change in mixture + var t1 = samples.ElementAt(2).temp - samples.ElementAt(1).temp; + var m1 = samples.ElementAt(2).mixture - samples.ElementAt(1).mixture; + Log.Information(String.Format("Next Mixture change {0:####.#}% (to {3,5:0.0}%) temperature change {1:##0.0} F (to {2:####} F)", m1, t1, samples.ElementAt(2).temp, samples.ElementAt(2).mixture)); + } + + // define the convergence threshold + double epsilon = CONVERGENCE_EPSILON; + + return PeakEGTSucessiveApproxAlgorithm(samples, epsilon, step); + } + + private double PeakEGTSucessiveApproxAlgorithm(BoundedQueue<(double temp, double mixture)> samples, double epsilon, double step) + { + for (int i = 0; i < 15; i++) + { + var diff = difference(samples); + if (-epsilon < diff && diff < epsilon) + { + Log.Information(String.Format("Convergence at {0,4:00.0} F at {1:##} %", samples.ElementAt(2).temp, samples.ElementAt(2).mixture)); + return extractResult(samples); + } + else + { + (step, samples) = EstimateEGT(samples, step); + } + } + + Log.Information(String.Format("Exceeded iteration count at {0,4:00.0} F at {1:##} %", samples.ElementAt(2).temp, samples.ElementAt(2).mixture)); + return extractResult(samples); + } + + private (double step, BoundedQueue<(double temp, double mixture)>) EstimateEGT(BoundedQueue<(double temp, double mixture)> samples, double step) + { + // work with last 3 samples. + + // t1 is the most recent change in EGT arising from a m1 change in mixture + var t1 = samples.ElementAt(2).temp - samples.ElementAt(1).temp; + // var m1 = samples.ElementAt(1).mixture - samples.ElementAt(0).mixture; + + + // t0 is the earlier recent change in EGT arising from a m0 change in mixture + var t0 = samples.ElementAt(1).temp - samples.ElementAt(0).temp; + // var m0 = samples.ElementAt(1).mixture - samples.ElementAt(0).mixture; + + + string change = string.Empty; + + double nextEGT, nextMixture; //, nextStep; + if (t1 >0) + { + // most recent change rising - good + if (t1 > t0) + { + // rising faster & thus moving in right direction + // nextStep = - Math.Abs(samples.ElementAt(2).mixture - samples.ElementAt(1).mixture) * 1.0; + change = String.Format ("Rising, but faster {0:00.0}", t1 - t0); + } + else + { + // rising slower, convergence, slow down mixture updates + // nextStep = - (Math.Abs((samples.ElementAt(2).mixture - samples.ElementAt(1).mixture))) * 0.5; + change = String.Format("Rising, but slower {0:00.0}", t1 - t0); + } + + // using the computed step + // step = nextStep; + nextMixture = samples.ElementAt(2).mixture + step; + simservice.SetMixture(nextMixture); + nextEGT = NextEGT(); + samples.Enqueue((nextEGT, nextMixture)); + } + else + { + // falling, not good + // step back 2 samples & half step size + step = Math.Abs(samples.ElementAt(1).mixture - samples.ElementAt(0).mixture) * 0.5; + nextMixture = samples.ElementAt(0).mixture; + simservice.SetMixture(nextMixture); + nextEGT = NextEGT(); + samples.Enqueue((nextEGT, nextMixture)); + } + + { + // tn is the next change in EGT arising from a m1 change in mixture + var tn = samples.ElementAt(2).temp - samples.ElementAt(1).temp; + var mn = samples.ElementAt(2).mixture - samples.ElementAt(1).mixture; + Log.Information(String.Format("{4,25}, Next Mixture change {0,4:00.0}% (to {3,5:0.0}%) temperature change {1:##0.0} F (to {2:####} F)", mn, tn, samples.ElementAt(2).temp, samples.ElementAt(2).mixture, change)); + } + + return (step, samples); + } + + private double extractResult(BoundedQueue<(double temp, double mixture)> samples) + { + return samples.ElementAt(2).temp; + } + + private double difference(BoundedQueue<(double temp, double mixture)> samples) + { + return samples.ElementAt(2).temp - samples.ElementAt(1).temp; + } + + private double NextEGT(double last = 0) + { + // need better algorithim that a delay + Thread.Sleep(DELAY_MSEC); + var nextEGT = simservice.MostRecentSample.EGT; + return nextEGT; + } + } +} diff --git a/Views/MainWindow.xaml b/Views/MainWindow.xaml new file mode 100644 index 0000000..03c19bf --- /dev/null +++ b/Views/MainWindow.xaml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + Lside-Mixture - Mixture Monitor + + + + + + + + + + + + + + + + + + + +