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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Views/MainWindow.xaml.cs b/Views/MainWindow.xaml.cs
new file mode 100644
index 0000000..1eba338
--- /dev/null
+++ b/Views/MainWindow.xaml.cs
@@ -0,0 +1,192 @@
+namespace Lside_Mixture.Views
+{
+ using System;
+ using System.ComponentModel;
+ using System.Diagnostics;
+ using System.Windows;
+ using System.Windows.Controls;
+ using System.Windows.Input;
+ using System.Windows.Interop;
+ using Gauge;
+ using Lside_Mixture.Models;
+ using Lside_Mixture.Services;
+ using Lside_Mixture.ViewModels;
+ using Lside_Mixture.Views;
+ using Microsoft.Extensions.DependencyInjection;
+ using Octokit;
+
+ ///
+ /// Interaction logic for MainWindow.xaml
+ ///
+ public partial class MainWindow : Window
+ {
+ private static string updateUri;
+ private static MixtureChartWindow openGraphWindow = null;
+ private static PeakEGTWindow peakEGTWindow = null;
+
+ private readonly ISimService simservice = App.Current.Services.GetService();
+
+ private readonly MainWindowViewModel viewModel;
+
+ // main window - drag & drop
+ private bool mouseDown;
+
+ public MainWindow()
+ {
+ InitializeComponent();
+
+ this.viewModel = new MainWindowViewModel();
+ this.viewModel.SampleViewModel = new SampleViewModel();
+
+ this.viewModel.GaugeViewModel = new GaugeViewModel(800.0, 1200.0);
+
+ // defined within the MainWindow.xml file
+ SampleStackPanel.DataContext = this.viewModel.SampleViewModel;
+ GuageStackPanel.DataContext = this.viewModel.GaugeViewModel;
+ MainWindowStack.DataContext = this.viewModel;
+
+ simservice.SampleModel.PropertyChanged += this.SampleViewModel_PropertyChanged;
+ }
+
+ private void SampleViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ switch (e.PropertyName)
+ {
+ case "PlaneInfoResponse":
+ {
+ var pir = ((SampleModel)sender).planeInfoResponse;
+ this.viewModel.GaugeViewModel.ScaledValue = Convert.ToInt32(pir.EGT);
+ }
+ break;
+ }
+ }
+
+ // displays Graph
+ private void ButtonGraph_Click(object sender, RoutedEventArgs rea)
+ {
+
+ if (openGraphWindow == null)
+ {
+ // create window & let it do its thing.
+ openGraphWindow = new MixtureChartWindow();
+
+ // set as child
+ WindowInteropHelper child = new WindowInteropHelper(openGraphWindow);
+ WindowInteropHelper parent = new WindowInteropHelper(this);
+ child.Owner = parent.Handle;
+
+ openGraphWindow.Show();
+ openGraphWindow.Focus();
+ openGraphWindow.Closed += (s, e) => openGraphWindow = null;
+ }
+ else
+ {
+ openGraphWindow.Focus();
+ }
+ }
+
+ // Leans the plane at optimum mixture
+ private void ButtonPeakEGT_Click(object sender, RoutedEventArgs rea)
+ {
+ if (openGraphWindow == null)
+ {
+ // create window & let it do its thing.
+ peakEGTWindow = new PeakEGTWindow();
+
+ // set as child
+ WindowInteropHelper child = new WindowInteropHelper(peakEGTWindow);
+ WindowInteropHelper parent = new WindowInteropHelper(this);
+ child.Owner = parent.Handle;
+
+ peakEGTWindow.Show();
+ peakEGTWindow.Focus();
+ peakEGTWindow.Closed += (s, e) => peakEGTWindow = null;
+ }
+ else
+ {
+ peakEGTWindow.Focus();
+ }
+ }
+
+ private void ButtonUpdate_Click(object sender, RoutedEventArgs e)
+ {
+ Process.Start(updateUri);
+ }
+
+ /** Git Hub Updater, amends displayed URL in the Main Window. **/
+ private void BackgroundWorkerUpdate_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
+ {
+ var client = new GitHubClient(new Octokit.ProductHeaderValue("Lside-Mixture"));
+ var releases = client.Repository.Release.GetAll("jj-blip", "lside-Mixture").Result;
+ var latest = releases[0];
+ var tagVersion = latest.TagName.Remove(0, 1);
+
+ var currentVersion = new Version(this.viewModel.Version);
+ var latestGitVersion = new Version(tagVersion);
+ var versionDifference = latestGitVersion.CompareTo(currentVersion);
+
+ this.viewModel.UpdateAvailable = versionDifference > 0;
+ updateUri = latest.HtmlUrl;
+ }
+
+ /** MainWindow drag and drop **/
+ private void Header_LoadedHandler(object sender, RoutedEventArgs e)
+ {
+ this.InitHeader(sender as TextBlock);
+ }
+
+ private void InitHeader(TextBlock header)
+ {
+ header.MouseUp += new MouseButtonEventHandler(this.OnMouseUp);
+ header.MouseLeftButtonDown += new MouseButtonEventHandler(this.MouseLeftButtonDown);
+ header.MouseMove += new MouseEventHandler(this.OnMouseMove);
+ }
+
+ private new void MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ if (this.WindowState == WindowState.Maximized)
+ {
+ this.mouseDown = true;
+ }
+
+ this.DragMove();
+ }
+
+ private void OnMouseUp(object sender, MouseButtonEventArgs e)
+ {
+ this.mouseDown = false;
+ }
+
+ private void OnMouseMove(object sender, MouseEventArgs e)
+ {
+ if (this.mouseDown)
+ {
+ var mouseX = e.GetPosition(this).X;
+ var width = this.RestoreBounds.Width;
+ var x = mouseX - (width / 2);
+
+ if (x < 0)
+ {
+ x = 0;
+ }
+ else
+ if (x + width > SystemParameters.PrimaryScreenWidth)
+ {
+ x = SystemParameters.PrimaryScreenWidth - width;
+ }
+
+ this.WindowState = WindowState.Normal;
+ this.Left = x;
+ this.Top = 0;
+ this.DragMove();
+ }
+ }
+
+ /* Handlers for UI */
+
+ private void Button_Hide_Click(object sender, RoutedEventArgs e)
+ {
+ this.Hide();
+ }
+ }
+}
diff --git a/Views/MixtureChartWindow.xaml b/Views/MixtureChartWindow.xaml
new file mode 100644
index 0000000..f7ba12f
--- /dev/null
+++ b/Views/MixtureChartWindow.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
diff --git a/Views/MixtureChartWindow.xaml.cs b/Views/MixtureChartWindow.xaml.cs
new file mode 100644
index 0000000..a3b871c
--- /dev/null
+++ b/Views/MixtureChartWindow.xaml.cs
@@ -0,0 +1,118 @@
+namespace Lside_Mixture.Views
+{
+ using System;
+ using System.Linq;
+ using System.ComponentModel;
+ using System.Drawing;
+ using System.Windows;
+ using Lside_Mixture.ViewModels;
+ using ScottPlot;
+ using ScottPlot.Plottable;
+ using System.Windows.Controls;
+
+ ///
+ /// Interaction logic for MixtureChart.xaml
+ ///
+ public partial class MixtureChartWindow : Window
+ {
+ private readonly BackgroundWorker backgroundWorker = new BackgroundWorker();
+
+ private ScatterPlot mixturePlot;
+ private ScatterPlot rpmPlot;
+
+ // rpm
+ private ScottPlot.Renderable.Axis yAxis2;
+
+ public MixtureChartWindow()
+ {
+ InitializeComponent();
+
+ this.backgroundWorker.DoWork += this.BackgroundWorker_DoWork;
+ this.backgroundWorker.RunWorkerCompleted += this.backgroundWorker_Completed;
+
+ if (!this.backgroundWorker.IsBusy)
+ {
+ this.backgroundWorker.RunWorkerAsync();
+ }
+ }
+
+ private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
+ {
+ // long running
+ new MixtureChartViewModel(this);
+ }
+
+ private void backgroundWorker_Completed(object sender, RunWorkerCompletedEventArgs e)
+ {
+ //update ui once worker complete his work
+
+ if (this.EGT.Data.Length > 1)
+ {
+ var inProcess = (TextBox)this.FindName("InProcessTextBlock");
+ inProcess.Visibility = Visibility.Collapsed;
+
+ var plt = this.wpfPlot.Plot;
+
+ plt.YLabel("EGT F");
+ plt.YAxis2.Color(Color.Red);
+ plt.XLabel($"Mixture %");
+
+ // Mixture data is plotted inverted with display sign inverted
+ this.Mixture.Data = this.Mixture.Data.Select(val => -1 * val).ToArray();
+ plt.XAxis.TickLabelNotation(invertSign: true);
+
+ this.mixturePlot = this.AddScatter(plt, this.Mixture, this.EGT);
+ this.mixturePlot.XAxisIndex = 0;
+ this.mixturePlot.Color = Color.Red;
+
+ this.rpmPlot = this.AddScatter(plt, this.Mixture, this.RPM);
+ this.rpmPlot.Color = Color.Blue;
+ this.yAxis2 = this.wpfPlot.Plot.AddAxis(ScottPlot.Renderable.Edge.Left, null, "RPM");
+ this.rpmPlot.YAxisIndex = yAxis2.AxisIndex;
+ this.yAxis2.Color(this.rpmPlot.Color);
+ this.rpmPlot.MarkerShape = MarkerShape.triDown;
+
+ double peakEGT = this.EGT.Data.Max();
+ double mixtureAtPeakEGT = this.Mixture.Data[this.EGT.Data.ToList().IndexOf(peakEGT)];
+ plt.AddHorizontalLine(peakEGT - 50, this.mixturePlot.Color, style: LineStyle.Dash, label: "EGT - 50 F");
+
+ var hline = plt.AddHorizontalLine(peakEGT);
+ hline.Color = this.mixturePlot.Color;
+ hline.LineWidth = 1;
+ hline.PositionLabel = true;
+ hline.PositionLabelBackground = hline.Color;
+ hline.DragEnabled = true;
+
+ var vline = plt.AddVerticalLine(mixtureAtPeakEGT);
+ vline.LineWidth = 1;
+ vline.PositionLabel = true;
+ vline.PositionLabelBackground = hline.Color;
+ vline.DragEnabled = true;
+
+ var ypos = Convert.ToInt32(this.EGT.Data.Min()) + 20;
+ plt.AddText("Rich", -95, ypos, size: 14, color: Color.Red);
+ plt.AddText("Lean", -30, ypos, size: 14, color: Color.Red);
+
+ plt.Legend();
+
+ this.wpfPlot.Plot.AxisAuto();
+ this.wpfPlot.Refresh();
+ }
+ }
+
+ public PlotData Altitude { get; internal set; } = new PlotData("Altitude", new double[1]);
+
+ public PlotData EGT { get; internal set; } = new PlotData("EGT", new double[1]);
+
+ public PlotData Throttle { get; internal set; } = new PlotData("Throttle", new double[1]);
+
+ public PlotData Mixture { get; internal set; } = new PlotData("Mixture", new double[1]);
+
+ public PlotData RPM { get; internal set; } = new PlotData("RPM", new double[1]);
+
+ private ScatterPlot AddScatter(Plot plot, PlotData xPlotData, PlotData yPlotData)
+ {
+ return plot.AddScatter(xPlotData.Data, yPlotData.Data, label: yPlotData.Title);
+ }
+ }
+}
diff --git a/MainWindow.xaml b/Views/PeakEGTWindow.xaml
similarity index 67%
rename from MainWindow.xaml
rename to Views/PeakEGTWindow.xaml
index a9f25cd..360b98a 100644
--- a/MainWindow.xaml
+++ b/Views/PeakEGTWindow.xaml
@@ -1,11 +1,11 @@
-
+ Title="PeakEGTWindow" Height="450" Width="800">
diff --git a/Views/PeakEGTWindow.xaml.cs b/Views/PeakEGTWindow.xaml.cs
new file mode 100644
index 0000000..701696c
--- /dev/null
+++ b/Views/PeakEGTWindow.xaml.cs
@@ -0,0 +1,42 @@
+namespace Lside_Mixture.Views
+{
+ using System.Drawing;
+ using System.Windows;
+ using Lside_Mixture.ViewModels;
+ using ScottPlot;
+ using ScottPlot.Plottable;
+ using System.Windows.Controls;
+ using System.ComponentModel;
+
+
+ ///
+ /// Interaction logic for PeakEGTWindow.xaml
+ ///
+ public partial class PeakEGTWindow : Window
+ {
+ private readonly BackgroundWorker backgroundWorker = new BackgroundWorker();
+
+ public PeakEGTWindow()
+ {
+ InitializeComponent();
+
+ this.backgroundWorker.DoWork += this.BackgroundWorker_DoWork;
+ this.backgroundWorker.RunWorkerCompleted += this.backgroundWorker_Completed;
+
+ if (!this.backgroundWorker.IsBusy)
+ {
+ this.backgroundWorker.RunWorkerAsync();
+ }
+ }
+
+ private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
+ {
+ // long running
+ new PeakEGTViewModel(this);
+ }
+
+ private void backgroundWorker_Completed(object sender, RunWorkerCompletedEventArgs e)
+ {
+ }
+ }
+}
diff --git a/common/BindableBase.cs b/common/BindableBase.cs
new file mode 100644
index 0000000..d23138c
--- /dev/null
+++ b/common/BindableBase.cs
@@ -0,0 +1,43 @@
+namespace Lside_Mixture.Services
+{
+ using System.ComponentModel;
+ using System.Runtime.CompilerServices;
+
+ ///
+ /// Provides a standard change-notification implementation.
+ ///
+ public abstract class BindableBase : INotifyPropertyChanged
+ {
+ ///
+ /// Occurs when a property value changes.
+ ///
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ ///
+ /// Raises the PropertyChanged event for the property with the specified
+ /// name, or the calling property if no name is specified.
+ ///
+ public void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ ///
+ /// Checks whether the value of the specified field is different than the specified value, and
+ /// if they are different, updates the field and raises the PropertyChanged event for
+ /// the property with the specified name, or the calling property if no name is specified.
+ ///
+ /// if property changed.
+ protected bool SetProperty(ref T storage, T value, [CallerMemberName] string propertyName = null)
+ {
+ if (object.Equals(storage, value))
+ {
+ return false;
+ }
+
+ storage = value;
+ this.OnPropertyChanged(propertyName);
+ return true;
+ }
+ }
+}
diff --git a/common/PlaneInfoResponse.cs b/common/PlaneInfoResponse.cs
new file mode 100644
index 0000000..470b145
--- /dev/null
+++ b/common/PlaneInfoResponse.cs
@@ -0,0 +1,36 @@
+namespace Lside_Mixture.Services
+{
+ using System.Runtime.InteropServices;
+ using CTrue.FsConnect;
+
+ ///
+ /// This structure must match 1 : 1 with the SimService.definition list contents.
+ ///
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
+ public struct PlaneInfoResponse
+ {
+ // Title
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
+ public string Title;
+
+ [SimVar(NameId = FsSimVar.PlaneAltitude, UnitId = FsUnit.Feet)]
+ public double Altitude;
+
+ [SimVar(NameId = FsSimVar.GeneralEngRpm, Instance = 1, UnitId = FsUnit.Rpm)]
+ public double RPM;
+
+ [SimVar(NameId = FsSimVar.EngExhaustGasTemperature, Instance = 1, UnitId = FsUnit.Fahrenheit)]
+ public double EGT;
+
+ [SimVar(NameId = FsSimVar.GeneralEngThrottleLeverPosition, Instance = 1, UnitId = FsUnit.Percentage)]
+ public double Throttle;
+
+ [SimVar(NameId = FsSimVar.GeneralEngMixtureLeverPosition, Instance = 1, UnitId = FsUnit.Percentage)]
+ public double Mixture;
+
+ public override string ToString()
+ {
+ return $"response Altitude:{this.Altitude}, RPM:{this.RPM}, EGT:{this.EGT}, Throttle:{this.Throttle}, Mixture:{this.Mixture}, ";
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages.config b/packages.config
new file mode 100644
index 0000000..9d73a08
--- /dev/null
+++ b/packages.config
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/services/ISimService.cs b/services/ISimService.cs
new file mode 100644
index 0000000..0ce2e10
--- /dev/null
+++ b/services/ISimService.cs
@@ -0,0 +1,34 @@
+using Lside_Mixture.Managers;
+using Lside_Mixture.Models;
+
+namespace Lside_Mixture.Services
+{
+ public interface ISimService
+ {
+ bool Connected
+ {
+ get;
+ }
+
+ bool Crashed
+ {
+ get;
+ }
+
+ SampleModel SampleModel
+ {
+ get;
+ }
+
+ ///
+ /// Sets the engine Mixture, in %.
+ ///
+ ///
+ void SetMixture(double mixture);
+
+ PlaneInfoResponse MostRecentSample
+ {
+ get;
+ }
+ }
+}
diff --git a/services/SimService.cs b/services/SimService.cs
new file mode 100644
index 0000000..1674459
--- /dev/null
+++ b/services/SimService.cs
@@ -0,0 +1,223 @@
+namespace Lside_Mixture.Services
+{
+ using System;
+ using System.ComponentModel;
+ using System.Linq;
+ using System.Runtime.CompilerServices;
+ using System.Windows.Threading;
+ using CTrue.FsConnect;
+ using Lside_Mixture.Managers;
+ using Lside_Mixture.Models;
+ using Serilog;
+
+ ///
+ ///
+ /// Exposes:
+ /// Connected, true if connected to MSFS.
+ ///
+ ///
+ public class SimService : BindableBase, ISimService, INotifyPropertyChanged
+ {
+ // ms
+ private const int SampleRate = 20;
+
+ // flag to ensure only one SimConnect data packet being processed at a time
+ private static bool safeToRead = true;
+
+ private static bool simulationIsPaused = false;
+
+ private static SampleModel sampleModel = new SampleModel();
+
+ // timer, task reads data from a SimConnection
+ private readonly DispatcherTimer dataReadDispatchTimer = new DispatcherTimer();
+
+ // timer, task establishes connection if disconnected
+ private readonly DispatcherTimer connectionDispatchTimer = new DispatcherTimer();
+
+ // Establishes simConnect connection & Update scaneris
+ private readonly BackgroundWorker backgroundWorkerConnection = new BackgroundWorker();
+
+ private readonly FsConnect fsConnect = new FsConnect();
+
+ private readonly EngineManager engineManager;
+
+ private int planeInfoDefinitionId;
+
+ private bool running = false;
+
+ private bool connected = false;
+
+ private bool crashed = false;
+
+ public SimService()
+ {
+ // do a 'Connection check' every 1 sec
+ this.connectionDispatchTimer.Interval = new TimeSpan(0, 0, 0, 0, 1000);
+ this.connectionDispatchTimer.Tick += new EventHandler(this.ConnectionCheckEventHandler_OnTick);
+
+ // establishes simConnect connection, when required
+ this.backgroundWorkerConnection.DoWork += this.BackgroundWorkerConnection_DoWork;
+ this.connectionDispatchTimer.Start();
+
+ // Read SimConnect Data every 20 msec
+ this.dataReadDispatchTimer.Interval = new TimeSpan(0, 0, 0, 0, SampleRate);
+ this.dataReadDispatchTimer.Tick += new EventHandler(this.DataReadEventHandler_OnTick);
+
+ // register the read SimConnect data callback procedure
+ this.fsConnect.FsDataReceived += HandleReceivedFsData;
+ this.fsConnect.PauseStateChanged += FsConnect_PauseStateChanged;
+
+ this.fsConnect.Crashed += this.FsConnect_Crashed;
+ this.fsConnect.FlightLoaded += this.FsConnect_Loaded;
+
+ FsConnect managerfsConnect = new FsConnect();
+ managerfsConnect.Connect("TestApp", "localhost", 500, SimConnectProtocol.Ipv4);
+ this.engineManager = new EngineManager(managerfsConnect);
+ this.engineManager.Initialize();
+ }
+
+ private enum Requests
+ {
+ PlaneInfoRequest = 0,
+ }
+
+ public bool Crashed
+ {
+ get { return this.crashed; }
+ private set { this.crashed = value; }
+ }
+
+ public bool Connected
+ {
+ get { return this.connected; }
+ private set { this.connected = value; }
+ }
+
+ public SampleModel SampleModel {
+ get { return sampleModel; }
+ }
+
+ public PlaneInfoResponse MostRecentSample
+ {
+ get { return mostRecentplaneInfoResponse; }
+ }
+
+ ///
+ /// Sets the engine Mixture, in %.
+ ///
+ ///
+ public void SetMixture(double mixture)
+ {
+ engineManager.SetMixture(mixture);
+ engineManager.Update();
+ }
+
+ private static PlaneInfoResponse mostRecentplaneInfoResponse;
+
+ private static void HandleReceivedFsData(object sender, FsDataReceivedEventArgs e)
+ {
+ if (simulationIsPaused)
+ {
+ return;
+ }
+
+ if (!safeToRead)
+ {
+ // already processing a packet, skip this one
+ Log.Debug("lost one");
+ return;
+ }
+
+ safeToRead = false;
+ try
+ {
+ if (e.RequestId == (uint)Requests.PlaneInfoRequest)
+ {
+ var planeInfoResponse = (PlaneInfoResponse)e.Data.FirstOrDefault();
+ mostRecentplaneInfoResponse = planeInfoResponse;
+ sampleModel.Handle(planeInfoResponse);
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Debug(ex.Message);
+ }
+
+ safeToRead = true;
+ }
+
+ private static void FsConnect_PauseStateChanged(object sender, PauseStateChangedEventArgs e)
+ {
+ simulationIsPaused = e.Paused;
+ }
+
+ private void FsConnect_Crashed(object sender, EventArgs e)
+ {
+ this.crashed = true;
+ }
+
+ private void FsConnect_Loaded(object sender, EventArgs e)
+ {
+ this.crashed = false;
+ }
+
+ private void DataReadEventHandler_OnTick(object sender, EventArgs e)
+ {
+ try
+ {
+ this.fsConnect.RequestData((int)Requests.PlaneInfoRequest, this.planeInfoDefinitionId);
+ }
+ catch
+ {
+ }
+ }
+
+ private void ConnectionCheckEventHandler_OnTick(object sender, EventArgs e)
+ {
+ if (!this.backgroundWorkerConnection.IsBusy)
+ {
+ this.backgroundWorkerConnection.RunWorkerAsync();
+ }
+
+ bool oldConnected = this.Connected;
+
+ if (this.fsConnect.Connected)
+ {
+ if (!this.running)
+ {
+ this.running = true;
+ this.dataReadDispatchTimer.Start();
+ }
+
+ this.Connected = true;
+ }
+ else
+ {
+ this.Connected = false;
+ }
+
+ if (this.Connected != oldConnected)
+ {
+ this.OnPropertyChanged(nameof(this.Connected));
+ }
+ }
+
+ // If not connected , Connect
+ private void BackgroundWorkerConnection_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
+ {
+ if (!this.fsConnect.Connected)
+ {
+ try
+ {
+ // connect & register data of interest
+ this.fsConnect.Connect("TestApp", "localhost", 500, SimConnectProtocol.Ipv4);
+ this.planeInfoDefinitionId = this.fsConnect.RegisterDataDefinition();
+ }
+ catch
+ {
+ // ignore
+ }
+ }
+ }
+ }
+}
diff --git a/viewModels/PlotData.cs b/viewModels/PlotData.cs
new file mode 100644
index 0000000..a3a0688
--- /dev/null
+++ b/viewModels/PlotData.cs
@@ -0,0 +1,15 @@
+namespace Lside_Mixture.ViewModels
+{
+ public class PlotData
+ {
+ public PlotData(string title, T[] data)
+ {
+ this.Title = title;
+ this.Data = data;
+ }
+
+ public string Title { get; set; }
+
+ public T[] Data { get; set; }
+ }
+}
diff --git a/viewModels/SampleViewModel.cs b/viewModels/SampleViewModel.cs
new file mode 100644
index 0000000..8f54ecc
--- /dev/null
+++ b/viewModels/SampleViewModel.cs
@@ -0,0 +1,82 @@
+namespace Lside_Mixture.ViewModels
+{
+ using System;
+ using System.ComponentModel;
+ using Lside_Mixture.Models;
+ using Lside_Mixture.Services;
+ using Microsoft.Extensions.DependencyInjection;
+
+ public class SampleViewModel : BindableBase
+ {
+ private readonly ISimService simservice = App.Current.Services.GetService();
+
+ public SampleViewModel()
+ {
+ simservice.SampleModel.PropertyChanged += this.ModelPropertyChanged;
+ }
+
+ public string Name
+ {
+ get { return name; }
+ set { SetProperty(ref name, value, "Name"); }
+ }
+
+ public int Altitude
+ {
+ get { return altitude; }
+ set { SetProperty(ref altitude, value, "Altitude"); }
+ }
+
+ public int RPM
+ {
+ get { return rpm; }
+ set { SetProperty(ref rpm, value, "RPM"); }
+ }
+ public int EGT
+ {
+ get { return egt; }
+ set { SetProperty(ref egt, value, "EGT"); }
+ }
+
+ public int Throttle
+ {
+ get { return throttle; }
+ set { SetProperty(ref throttle, value, "Throttle"); }
+ }
+
+ public int Mixture
+ {
+ get { return mixture; }
+ set { SetProperty(ref mixture, value, "Mixture"); }
+ }
+
+ public void update(PlaneInfoResponse planeInfo)
+ {
+ this.Name = planeInfo.Title;
+ this.Altitude = Convert.ToInt32(planeInfo.Altitude);
+ this.RPM = Convert.ToInt32(planeInfo.RPM);
+ this.EGT = Convert.ToInt32(planeInfo.EGT);
+ this.Throttle = Convert.ToInt32(planeInfo.Throttle);
+ this.Mixture = Convert.ToInt32(planeInfo.Mixture);
+ }
+
+ private void ModelPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ switch (e.PropertyName)
+ {
+ case "PlaneInfoResponse":
+ {
+ this.update(((SampleModel)sender).planeInfoResponse);
+ }
+ break;
+ }
+ }
+
+ private string name;
+ private int altitude;
+ private int rpm;
+ private int egt;
+ private int throttle;
+ private int mixture;
+ }
+}
diff --git a/views/Mixture.xaml b/views/Mixture.xaml
new file mode 100644
index 0000000..942e8d8
--- /dev/null
+++ b/views/Mixture.xaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/views/Mixture.xaml.cs b/views/Mixture.xaml.cs
new file mode 100644
index 0000000..91140be
--- /dev/null
+++ b/views/Mixture.xaml.cs
@@ -0,0 +1,15 @@
+using System.Windows;
+
+namespace Lside_Mixture.Views
+{
+ ///
+ /// Interaction logic for Mixture.xaml
+ ///
+ public partial class Mixture : Window
+ {
+ public Mixture()
+ {
+ InitializeComponent();
+ }
+ }
+}