├── WasmModule ├── .gitignore └── WasmModule.h ├── SimVarScanner ├── .gitignore ├── .prettierrc ├── package.json └── README.md ├── BravoLights ├── install.bat ├── uninstall.bat ├── Resources │ ├── Bulb.png │ ├── Exit.png │ ├── Table.png │ ├── Debugger.png │ └── TrayIcon.ico ├── README.md ├── Connections │ ├── IWASMChannel.cs │ └── VariableHandlerUtils.cs ├── App.xaml ├── Properties │ ├── launchSettings.json │ ├── PublishProfiles │ │ └── FolderProfile.pubxml │ └── Resources.Designer.cs ├── Ast │ ├── LvarExpression.cs │ ├── SimVarExpression.cs │ └── MSFSExpressionParser.cs ├── UI │ ├── RedBrushConverter.cs │ ├── CountToVisibilityConverter.cs │ ├── ExpressionAndVariables.xaml.cs │ ├── RedGreenBrushConverter.cs │ ├── BBLSplashScreen.xaml │ ├── CorruptExeXmlErrorWindow.xaml │ ├── ExpressionAndVariablesViewModel.cs │ ├── CorruptExeXmlErrorWindow.xaml.cs │ ├── ExpressionAndVariables.xaml │ ├── ProgramInfo.cs │ ├── BBLSplashScreen.xaml.cs │ ├── VariableList.xaml │ ├── LightsWindow.xaml.cs │ └── VariableListViewModel.cs ├── AssemblyInfo.cs ├── CsvFile.cs ├── Config.User.ini ├── Installation │ ├── FileUtils.cs │ ├── ExeXmlFixer.cs │ └── FlightSimulatorPaths.cs ├── GlobalLightController.cs ├── LightExpressionConfig.cs ├── BravoLights.csproj ├── Config.cs ├── IniFile.cs └── MainViewModel.cs ├── MSFSWASMProject ├── .gitignore ├── PackageDefinitions │ ├── better-bravo-lights-lvar-module │ │ └── ContentInfo │ │ │ └── Thumbnail.jpg │ └── better-bravo-lights-lvar-module.xml └── BetterBravoLightsLVars.xml ├── docs └── VS2019Install.png ├── .gitignore ├── BravoLights.Common ├── ValueChangedEventArgs.cs ├── ILightsState.cs ├── IConnection.cs ├── IVariable.cs ├── Ast │ ├── LiteralNumericNode.cs │ ├── LiteralBoolNode.cs │ ├── IAstNode.cs │ ├── NodeDataType.cs │ ├── ErrorNode.cs │ ├── ConstantNode.cs │ ├── ExpressionToken.cs │ ├── VariableBase.cs │ ├── UnaryExpression.cs │ ├── BinaryExpression.cs │ ├── BinaryNumericExpression.cs │ ├── ComparisonExpression.cs │ └── BooleanLogicalExpression.cs ├── BravoLights.Common.csproj ├── ViewModelBase.cs ├── AssemblyInfo.cs ├── LightExpression.cs ├── LightNames.cs ├── UsbLogic.cs └── ExpressionParser.cs ├── .github └── ISSUE_TEMPLATE │ ├── config.yml │ ├── 30-support-question.yaml │ ├── 20-feature-request.yaml │ ├── 50-help-fix-exe-xml.yaml │ ├── 10-bug-report.yaml │ └── 40-suggested-config.yaml ├── DCSBravoLights ├── App.xaml ├── App.xaml.cs ├── DCSBravoLights.csproj ├── MainWindow.xaml ├── AssemblyInfo.cs ├── RedBrushConverter.cs ├── LightsState.cs ├── DcsVariableExpression.cs ├── DcsConnection.cs ├── VariableName.cs ├── DcsExpressionParser.cs ├── DebuggerUI.xaml ├── MainWindow.xaml.cs ├── DataDefinitions.cs ├── DcsDefinitions.cs └── DebuggerUI.xaml.cs ├── assemble-wasm-for-ide.cmd ├── BetterBravoLights.exe.nlog ├── BravoLights.Tests ├── GitHubVersionTests.cs ├── BravoLights.Tests.csproj ├── ExeXmlFixerTests.cs ├── ExpressionCombinationTests.cs ├── ConfigTests.cs └── LVarManagerTests.cs ├── LICENSE ├── README.md └── BravoLights.sln /WasmModule/.gitignore: -------------------------------------------------------------------------------- 1 | /MSFS/ 2 | -------------------------------------------------------------------------------- /SimVarScanner/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /BravoLights/install.bat: -------------------------------------------------------------------------------- 1 | "%~dp0Program\BetterBravoLights" /install 2 | -------------------------------------------------------------------------------- /BravoLights/uninstall.bat: -------------------------------------------------------------------------------- 1 | "%~dp0Program\BetterBravoLights" /uninstall 2 | -------------------------------------------------------------------------------- /MSFSWASMProject/.gitignore: -------------------------------------------------------------------------------- 1 | /Packages/ 2 | /_PackageInt/ 3 | /PackageSources/ -------------------------------------------------------------------------------- /SimVarScanner/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /docs/VS2019Install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoystonS/BetterBravoLights/HEAD/docs/VS2019Install.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | bin/ 3 | obj/ 4 | *.*proj.user 5 | *.pubxml.user 6 | BetterBravoLights/ 7 | BetterBravoLights.zip 8 | -------------------------------------------------------------------------------- /BravoLights/Resources/Bulb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoystonS/BetterBravoLights/HEAD/BravoLights/Resources/Bulb.png -------------------------------------------------------------------------------- /BravoLights/Resources/Exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoystonS/BetterBravoLights/HEAD/BravoLights/Resources/Exit.png -------------------------------------------------------------------------------- /BravoLights/Resources/Table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoystonS/BetterBravoLights/HEAD/BravoLights/Resources/Table.png -------------------------------------------------------------------------------- /BravoLights/Resources/Debugger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoystonS/BetterBravoLights/HEAD/BravoLights/Resources/Debugger.png -------------------------------------------------------------------------------- /BravoLights/Resources/TrayIcon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoystonS/BetterBravoLights/HEAD/BravoLights/Resources/TrayIcon.ico -------------------------------------------------------------------------------- /BravoLights.Common/ValueChangedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BravoLights.Common 4 | { 5 | public class ValueChangedEventArgs : EventArgs 6 | { 7 | public object NewValue; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MSFSWASMProject/PackageDefinitions/better-bravo-lights-lvar-module/ContentInfo/Thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoystonS/BetterBravoLights/HEAD/MSFSWASMProject/PackageDefinitions/better-bravo-lights-lvar-module/ContentInfo/Thumbnail.jpg -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 📖 Better Bravo Lights Documentation 4 | url: https://github.com/RoystonS/BetterBravoLights/wiki 5 | about: Full documentation for using and configuring Better Bravo Lights -------------------------------------------------------------------------------- /BravoLights.Common/ILightsState.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel; 3 | 4 | namespace BravoLights.Common 5 | { 6 | public interface ILightsState : INotifyPropertyChanged 7 | { 8 | public IEnumerable LitLights { get; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /BravoLights/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Image 3 | 4 | - Bulb: https://icon-icons.com/icon/bulb-idea-light/74600 5 | - Debugging: https://icon-icons.com/icon/bug-danger-internet-malware-search-security-virus/127085 6 | - Table - https://icon-icons.com/icon/table/70854 7 | - Exit: https://icon-icons.com/icon/input-exit/90426 8 | -------------------------------------------------------------------------------- /BravoLights/Connections/IWASMChannel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BravoLights.Connections 4 | { 5 | public interface IWASMChannel 6 | { 7 | void ClearSubscriptions(); 8 | void Subscribe(short id); 9 | void Unsubscribe(short id); 10 | 11 | SimState SimState { get; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /BravoLights.Common/IConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BravoLights.Common; 3 | 4 | namespace BravoLights.Common 5 | { 6 | public interface IConnection 7 | { 8 | void AddListener(IVariable variable, EventHandler handler); 9 | void RemoveListener(IVariable variable, EventHandler handler); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /WasmModule/WasmModule.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef __INTELLISENSE__ 4 | #define MODULE_EXPORT __attribute__( ( visibility( "default" ) ) ) 5 | #define MODULE_WASM_MODNAME(mod) __attribute__((import_module(mod))) 6 | #else 7 | #define MODULE_EXPORT 8 | #define MODULE_WASM_MODNAME(mod) 9 | #define __attribute__(x) 10 | #define __restrict__ 11 | #endif 12 | -------------------------------------------------------------------------------- /BravoLights/App.xaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /BravoLights/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "StartedBySimulator": { 4 | "commandName": "Project", 5 | "commandLineArgs": "/startedbysimulator" 6 | }, 7 | "StartedManually": { 8 | "commandName": "Project" 9 | }, 10 | "Install": { 11 | "commandName": "Project", 12 | "commandLineArgs": "/install" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /MSFSWASMProject/BetterBravoLightsLVars.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | . 4 | _PackageInt 5 | 6 | PackageDefinitions\better-bravo-lights-lvar-module.xml 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /DCSBravoLights/App.xaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /DCSBravoLights/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.Data; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | 9 | namespace DCSBravoLights 10 | { 11 | /// 12 | /// Interaction logic for App.xaml 13 | /// 14 | public partial class App : Application 15 | { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /BravoLights.Common/IVariable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BravoLights.Common.Ast; 3 | 4 | namespace BravoLights.Common 5 | { 6 | public interface IVariable : IAstNode, IEquatable 7 | { 8 | /// 9 | /// Gets the identifier for this variable. For an lvar this would be the simple name; for a sim var, it would be the name plus units. 10 | /// 11 | string Identifier { get; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /DCSBravoLights/DCSBravoLights.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WinExe 5 | net5.0-windows 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /SimVarScanner/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simvarscanner", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "build": "node index.js > ../BravoLights/KnownSimVars.csv" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "engines": { 13 | "node": ">=16.13" 14 | }, 15 | "volta": { 16 | "node": "16.13.0", 17 | "yarn": "1.22.17" 18 | }, 19 | "dependencies": { 20 | "jsdom": "^19.0.0", 21 | "node-fetch": "^3.2.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BravoLights.Common/Ast/LiteralNumericNode.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace BravoLights.Common.Ast 4 | { 5 | /// 6 | /// A node which represents a literal double number. 7 | /// 8 | class LiteralNumericNode : ConstantNode 9 | { 10 | public LiteralNumericNode(double value) : base(value) 11 | { 12 | } 13 | 14 | public override string ToString() 15 | { 16 | return Value.ToString(CultureInfo.InvariantCulture); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /DCSBravoLights/MainWindow.xaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /BravoLights/Ast/LvarExpression.cs: -------------------------------------------------------------------------------- 1 | using BravoLights.Common; 2 | using BravoLights.Common.Ast; 3 | using BravoLights.Connections; 4 | 5 | namespace BravoLights.Ast 6 | { 7 | class LvarExpression : VariableBase, IAstNode 8 | { 9 | public string LVarName { get; set; } 10 | 11 | protected override IConnection Connection => LVarManager.Connection; 12 | 13 | public override string Identifier => $"L:{LVarName}"; 14 | 15 | public override string ToString() 16 | { 17 | return Identifier; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /BravoLights.Common/Ast/LiteralBoolNode.cs: -------------------------------------------------------------------------------- 1 | namespace BravoLights.Common.Ast 2 | { 3 | /// 4 | /// A node which represents a literal bool. 5 | /// 6 | class LiteralBoolNode : ConstantNode 7 | { 8 | private LiteralBoolNode(bool value) : base(value) 9 | { 10 | } 11 | 12 | public override string ToString() 13 | { 14 | return Value ? "ON" : "OFF"; 15 | } 16 | 17 | public static readonly IAstNode On = new LiteralBoolNode(true); 18 | public static readonly IAstNode Off = new LiteralBoolNode(false); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /BravoLights.Common/BravoLights.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Royston Shufflebotham <royston@shufflebotham.org> 5 | Better Bravo Lights 6 | (C) 2021-2022 Royston Shufflebotham <royston@shufflebotham.org> 7 | net5.0 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /DCSBravoLights/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /BravoLights/UI/RedBrushConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Windows.Data; 4 | using System.Windows.Media; 5 | 6 | namespace BravoLights.UI 7 | { 8 | [ValueConversion(typeof(bool), typeof(Brush))] 9 | public class RedBrushConverter : IValueConverter 10 | { 11 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 12 | { 13 | return (bool)value ? Brushes.Red : Brushes.Black; 14 | } 15 | 16 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 17 | { 18 | throw new NotImplementedException(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DCSBravoLights/RedBrushConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Windows.Data; 4 | using System.Windows.Media; 5 | 6 | namespace DCSBravoLights 7 | { 8 | [ValueConversion(typeof(bool), typeof(Brush))] 9 | public class RedBrushConverter : IValueConverter 10 | { 11 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 12 | { 13 | return (bool)value ? Brushes.Red : Brushes.Black; 14 | } 15 | 16 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 17 | { 18 | throw new NotImplementedException(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /BravoLights/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Windows; 3 | 4 | [assembly: ThemeInfo( 5 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 6 | //(used if a resource is not found in the page, 7 | // or application resource dictionaries) 8 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 9 | //(used if a resource is not found in the page, 10 | // app, or any theme specific resource dictionaries) 11 | )] 12 | 13 | [assembly: InternalsVisibleTo("BravoLights.Tests")] -------------------------------------------------------------------------------- /assemble-wasm-for-ide.cmd: -------------------------------------------------------------------------------- 1 | rd/s/q BravoLights\bin\Debug\net5-windows\better-bravo-lights-lvar-module 2 | rd/s/q BravoLights\bin\Release\net5-windows\better-bravo-lights-lvar-module 3 | 4 | rem Builds the WASM module and installs it into the Debug output directory for the project. 5 | "%MSFS_SDK%Tools\bin\fspackagetool.exe" MSFSWASMProject\BetterBravoLightsLVars.xml -outputdir BravoLights\bin\Debug\net5.0-windows 6 | 7 | rem And another copy for the Release dir 8 | echo D | xcopy /c/d/e/h/k BravoLights\bin\Debug\net5.0-windows\Packages BravoLights\bin\Release\net5.0-windows\Packages 9 | 10 | rem And another copy for unit tests 11 | echo D | xcopy /c/d/e/h/k BravoLights\bin\Debug\net5.0-windows\Packages BravoLights.Tests\bin\Debug\net5.0-windows\Packages 12 | -------------------------------------------------------------------------------- /BravoLights/UI/CountToVisibilityConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Windows; 4 | using System.Windows.Data; 5 | 6 | namespace BravoLights.UI 7 | { 8 | [ValueConversion(typeof(int), typeof(Visibility))] 9 | public class CountToVisibilityConverter : IValueConverter 10 | { 11 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 12 | { 13 | var count = (int)value; 14 | return (count == 0) ? Visibility.Collapsed : Visibility.Visible; 15 | } 16 | 17 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 18 | { 19 | throw new NotImplementedException(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /BetterBravoLights.exe.nlog: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /BravoLights/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | bin\Release\net5.0-windows\publish\ 10 | FileSystem 11 | net5.0-windows 12 | win-x64 13 | true 14 | False 15 | False 16 | False 17 | 18 | -------------------------------------------------------------------------------- /BravoLights/Ast/SimVarExpression.cs: -------------------------------------------------------------------------------- 1 | using BravoLights.Common; 2 | using BravoLights.Common.Ast; 3 | using BravoLights.Connections; 4 | 5 | namespace BravoLights.Ast 6 | { 7 | class SimVarExpression : VariableBase, IAstNode 8 | { 9 | public readonly NameAndUnits NameAndUnits; 10 | 11 | public SimVarExpression(string simVarName, string units) 12 | { 13 | NameAndUnits = new NameAndUnits { Name = simVarName, Units = units }; 14 | } 15 | 16 | public override string ToString() 17 | { 18 | return Identifier; 19 | } 20 | 21 | protected override IConnection Connection => SimConnectConnection.Connection; 22 | 23 | public override string Identifier => $"A:{this.NameAndUnits.Name}, {this.NameAndUnits.Units}"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /BravoLights.Common/Ast/IAstNode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace BravoLights.Common.Ast 5 | { 6 | /// 7 | /// Represents a node in a parsed expression tree. 8 | /// 9 | public interface IAstNode 10 | { 11 | string ErrorText { get; } 12 | 13 | event EventHandler ValueChanged; 14 | 15 | IEnumerable Variables { get; } 16 | 17 | /// 18 | /// Gets the type of the overall value of this node. 19 | /// 20 | NodeDataType ValueType { get; } 21 | 22 | /// 23 | /// Calculates an optimized version of this expression, e.g. converting "A AND ON" to "A" and "A OR ON" to "ON". 24 | /// 25 | IAstNode Optimize(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /BravoLights/CsvFile.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace BravoLights 6 | { 7 | public class CsvFile 8 | { 9 | public string[] Headings; 10 | public string[][] Rows; 11 | 12 | public void Load(string filename) 13 | { 14 | var lines = File.ReadAllLines(filename); 15 | 16 | Headings = Split(lines[0]); 17 | 18 | Rows = lines.Skip(1).Select(line => Split(line)).ToArray(); 19 | } 20 | 21 | private static string[] Split(string line) 22 | { 23 | var regex = new Regex("\"(.*?)\",?\\s*"); 24 | var matches = regex.Matches(line); 25 | 26 | var values = matches.Select(m => m.Groups[1].Value).ToArray(); 27 | return values; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /BravoLights.Common/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace BravoLights.Common 5 | { 6 | public abstract class ViewModelBase : INotifyPropertyChanged 7 | { 8 | protected void SetProperty(ref T field, T value, [CallerMemberName] string propertyName = null) 9 | { 10 | if ((field == null && value != null) || (field != null && !field.Equals(value))) 11 | { 12 | field = value; 13 | RaisePropertyChanged(propertyName); 14 | } 15 | } 16 | 17 | protected void RaisePropertyChanged(string propertyName) 18 | { 19 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 20 | } 21 | 22 | public event PropertyChangedEventHandler PropertyChanged; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /BravoLights/Config.User.ini: -------------------------------------------------------------------------------- 1 | ############################################### 2 | # Better Bravo Lights user configuration file # 3 | ############################################### 4 | 5 | # Place your own personal configuration entries in this file 6 | # and they will override or add to the built in configuration. 7 | 8 | # The file Program\Config.BuiltIn.ini for many examples of what you can 9 | # do. Full configuration documentation is available at 10 | # https://roystons.github.io/BetterBravoLights/configuration.html 11 | 12 | [Default] 13 | ;AUTOPILOT = A:AUTOPILOT MASTER, percent over 100 == 1 14 | 15 | ;Uncomment the line below to invert the display logic for the top row of auto-pilot lights 16 | ;Invert = HDG, NAV, APR, REV, ALT, VS, IAS, AUTOPILOT 17 | 18 | [Aircraft.AircraftName] 19 | ;AUTOPILOT = L:XMLVAR_Autopilot_1_Status == 1 OR L:XMLVAR_Autopilot_2_Status == 1 20 | -------------------------------------------------------------------------------- /BravoLights/UI/ExpressionAndVariables.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | 4 | namespace BravoLights.UI 5 | { 6 | /// 7 | /// Interaction logic for ExpressionAndVariables.xaml 8 | /// 9 | public partial class ExpressionAndVariables : UserControl 10 | { 11 | public ExpressionAndVariables() 12 | { 13 | InitializeComponent(); 14 | } 15 | 16 | public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register("ViewModel", typeof(ExpressionAndVariablesViewModel), typeof(ExpressionAndVariables)); 17 | public ExpressionAndVariablesViewModel ViewModel 18 | { 19 | get { return (ExpressionAndVariablesViewModel)GetValue(ViewModelProperty); } 20 | set { SetValue(ViewModelProperty, value); } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /DCSBravoLights/LightsState.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using BravoLights.Common; 3 | 4 | namespace DCSBravoLights 5 | { 6 | public class LightsState : ViewModelBase, ILightsState 7 | { 8 | public IEnumerable LitLights 9 | { 10 | get { return litLights; } 11 | } 12 | 13 | private readonly HashSet litLights = new(); 14 | 15 | public void SetLight(string lightName, bool lit) 16 | { 17 | bool changed; 18 | if (lit) 19 | { 20 | changed = litLights.Add(lightName); 21 | } 22 | else 23 | { 24 | changed = litLights.Remove(lightName); 25 | } 26 | 27 | if (changed) 28 | { 29 | RaisePropertyChanged(lightName); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /BravoLights.Tests/GitHubVersionTests.cs: -------------------------------------------------------------------------------- 1 | using BravoLights.UI; 2 | using System; 3 | using System.IO; 4 | using System.Reflection; 5 | using Xunit; 6 | 7 | namespace BravoLights.Tests 8 | { 9 | public class GitHubVersionTests 10 | { 11 | 12 | [Fact] 13 | public void DetectsLatestVersionFromAnOutOfOrderAssetsList() 14 | { 15 | var codeBaseUrl = new Uri(Assembly.GetExecutingAssembly().Location); 16 | var codeBasePath = Uri.UnescapeDataString(codeBaseUrl.AbsolutePath); 17 | var dirPath = Path.GetDirectoryName(codeBasePath); 18 | var filename = Path.Combine(dirPath, "GitHubReleasesSample.json"); 19 | var text = File.ReadAllText(filename); 20 | 21 | var latestVersion = ProgramInfo.ExtractLatestVersionFromGitHubReleasesJson(text); 22 | Assert.Equal("0.6.0", latestVersion); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /DCSBravoLights/DcsVariableExpression.cs: -------------------------------------------------------------------------------- 1 | using BravoLights.Common; 2 | using BravoLights.Common.Ast; 3 | 4 | namespace DCSBravoLights 5 | { 6 | class DcsVariableExpression : VariableBase, IAstNode 7 | { 8 | private readonly VariableName variableName; 9 | 10 | public DcsVariableExpression(string category, string identifier) 11 | { 12 | variableName = new VariableName(category, identifier); 13 | } 14 | 15 | public override string ToString() 16 | { 17 | return $"[{variableName.DcsCategory}:{variableName.DcsIdentifier}]"; 18 | } 19 | 20 | protected override IConnection Connection => DcsConnection.Connection; 21 | 22 | public override string Identifier => $"{variableName.DcsCategory}:{variableName.DcsIdentifier}"; 23 | 24 | public VariableName VariableName { get { return variableName; } } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /BravoLights.Common/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Runtime.InteropServices; 3 | 4 | // In SDK-style projects such as this one, several assembly attributes that were historically 5 | // defined in this file are now automatically added during build and populated with 6 | // values defined in project properties. For details of which attributes are included 7 | // and how to customise this process see: https://aka.ms/assembly-info-properties 8 | 9 | 10 | // Setting ComVisible to false makes the types in this assembly not visible to COM 11 | // components. If you need to access a type in this assembly from COM, set the ComVisible 12 | // attribute to true on that type. 13 | 14 | [assembly: ComVisible(false)] 15 | 16 | // The following GUID is for the ID of the typelib if this project is exposed to COM. 17 | 18 | [assembly: Guid("203606e7-ef5f-4e09-96a2-949e0b7eb8cb")] 19 | 20 | [assembly: InternalsVisibleTo("BravoLights.Tests")] 21 | -------------------------------------------------------------------------------- /BravoLights.Common/Ast/NodeDataType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BravoLights.Common.Ast 4 | { 5 | public enum NodeDataType 6 | { 7 | /// 8 | /// Indicates that the node exposes a value of type Double. 9 | /// 10 | Double, 11 | 12 | /// 13 | /// Indicates that the node exposes a value of type Boolean. 14 | /// 15 | Boolean 16 | } 17 | 18 | public static class NodeDataTypeUtility 19 | { 20 | public static NodeDataType GetNodeDataType(Type valueType) 21 | { 22 | if (valueType ==typeof(bool)) 23 | { 24 | return NodeDataType.Boolean; 25 | } 26 | if (valueType == typeof(double)) 27 | { 28 | return NodeDataType.Double; 29 | } 30 | 31 | throw new Exception($"Unexpected node value type: {valueType.Name}"); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /DCSBravoLights/DcsConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BravoLights.Common; 3 | 4 | namespace DCSBravoLights 5 | { 6 | class DcsConnection : IConnection 7 | { 8 | public static readonly DcsConnection Connection = new(); 9 | 10 | public DcsVariablesManager DcsVariablesManager; 11 | 12 | public void AddListener(IVariable variable, EventHandler handler) 13 | { 14 | var dcsVariable = (DcsVariableExpression)variable; 15 | var variableName = dcsVariable.VariableName; 16 | 17 | DcsVariablesManager.AddHandler(variableName, handler); 18 | } 19 | 20 | public void RemoveListener(IVariable variable, EventHandler handler) 21 | { 22 | var dcsVariable = (DcsVariableExpression)variable; 23 | var variableName = dcsVariable.VariableName; 24 | 25 | DcsVariablesManager.RemoveHandler(variableName, handler); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /BravoLights.Common/Ast/ErrorNode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace BravoLights.Common.Ast 5 | { 6 | public class ErrorNode : IAstNode 7 | { 8 | public ErrorNode(string errorText) 9 | { 10 | ErrorText = errorText; 11 | } 12 | 13 | public string ErrorText { get; private set; } 14 | 15 | public IEnumerable Variables { 16 | get { return Array.Empty(); } 17 | } 18 | 19 | public NodeDataType ValueType 20 | { 21 | get { return NodeDataType.Double; } 22 | } 23 | 24 | event EventHandler IAstNode.ValueChanged 25 | { 26 | add { } 27 | remove { } 28 | } 29 | 30 | public override string ToString() 31 | { 32 | return ErrorText; 33 | } 34 | 35 | public IAstNode Optimize() 36 | { 37 | return this; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /BravoLights/Connections/VariableHandlerUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BravoLights.Common; 3 | 4 | namespace BravoLights.Connections 5 | { 6 | static class VariableHandlerUtils 7 | { 8 | /// 9 | /// Reports that, whilst we are connected to the server, we haven't yet received a value for this variable. 10 | /// 11 | public static void SendNoValueError(object sender, EventHandler handler) 12 | { 13 | handler(sender, new ValueChangedEventArgs { NewValue = new Exception("No value yet received from simulator") }); 14 | } 15 | 16 | /// 17 | /// Reports that a variable doesn't have a value because the simulator isn't connected. 18 | /// 19 | public static void SendNoConnectionError(object sender, EventHandler handler) 20 | { 21 | handler(sender, new ValueChangedEventArgs { NewValue = new Exception("No connection to simulator") }); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DCSBravoLights/VariableName.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DCSBravoLights 4 | { 5 | public class VariableName : IEquatable 6 | { 7 | private readonly string dcsCategory; 8 | private readonly string dcsIdentifier; 9 | 10 | public VariableName(string category, string identifier) 11 | { 12 | dcsCategory = category; 13 | dcsIdentifier = identifier; 14 | } 15 | 16 | public string DcsCategory { get => dcsCategory; } 17 | public string DcsIdentifier { get => dcsIdentifier; } 18 | 19 | public bool Equals(VariableName other) 20 | { 21 | return dcsCategory.Equals(other.dcsCategory) && dcsIdentifier.Equals(other.dcsIdentifier); 22 | } 23 | public override bool Equals(object obj) 24 | { 25 | return Equals(obj as VariableName); 26 | } 27 | 28 | public override int GetHashCode() 29 | { 30 | return dcsCategory.GetHashCode() + dcsIdentifier.GetHashCode(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /MSFSWASMProject/PackageDefinitions/better-bravo-lights-lvar-module.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | MISC 4 | Better Bravo Lights LVars Module 5 | 6 | Royston Shufflebotham 7 | 8 | 9 | true 10 | false 11 | 12 | 13 | 14 | ContentInfo 15 | 16 | false 17 | 18 | PackageDefinitions\better-bravo-lights-lvar-module\ContentInfo 19 | ContentInfo\better-bravo-lights-lvar-module 20 | 21 | 22 | Copy 23 | 24 | false 25 | 26 | PackageSources\modules\ 27 | modules\ 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /SimVarScanner/README.md: -------------------------------------------------------------------------------- 1 | # Sim Var Scanner 2 | 3 | This is a NodeJS project which scrapes the Microsoft 4 | SimVar web pages (https://docs.flightsimulator.com/html/index.htm#t=Programming_Tools%2FSimVars%2FSimulation_Variables.htm) to produce a CSV file containing all known `A:` variables. 5 | 6 | Two particularly tricky parts are determining units and ranges of `:index` variables. 7 | 8 | ## Determining units 9 | 10 | There are many problems in the Microsoft tables; whilst many of the 'Units' cells are perfectly readable, many are simply blank, and many simply contain the letter 's'. Most that reference feet have stray parentheses, which aren't always closed. 11 | 12 | So we have to have a big block of code to cope with the special cases. 13 | 14 | ## Determining index ranges 15 | 16 | Variables of the form `FOO BAR:index` can't be read directly, and indexed forms `FOO BAR:1`, `FOO BAR:2` need to be read. It's not always at all obvious what the ranges should be: for engines it's typically 1-4; for wings, 1-2; for contact points, 0-19. 17 | 18 | So we have to have a big block of code to cope with the special cases. 19 | -------------------------------------------------------------------------------- /BravoLights/UI/RedGreenBrushConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Windows.Data; 4 | using System.Windows.Media; 5 | 6 | namespace BravoLights.UI 7 | { 8 | public class MultiBrushConverter : IMultiValueConverter 9 | { 10 | public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) 11 | { 12 | try 13 | { 14 | var red = (bool)values[0]; 15 | var green = (bool)values[1]; 16 | 17 | if (red) 18 | { 19 | return green ? Brushes.Orange : Brushes.Red; 20 | } 21 | else 22 | { 23 | return green ? Brushes.Green : Brushes.Black; 24 | } 25 | } catch 26 | { 27 | return Brushes.Black; 28 | } 29 | } 30 | 31 | public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) 32 | { 33 | throw new NotImplementedException(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /BravoLights.Common/Ast/ConstantNode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace BravoLights.Common.Ast 5 | { 6 | /// 7 | /// A node which represents a constant value. 8 | /// 9 | abstract class ConstantNode: IAstNode 10 | { 11 | protected ConstantNode(T value) 12 | { 13 | Value = value; 14 | } 15 | 16 | public readonly T Value; 17 | 18 | public string ErrorText { get { return null; } } 19 | 20 | public IEnumerable Variables 21 | { 22 | get { return Array.Empty(); } 23 | } 24 | 25 | public NodeDataType ValueType 26 | { 27 | get { return NodeDataTypeUtility.GetNodeDataType(typeof(T)); } 28 | } 29 | 30 | public event EventHandler ValueChanged 31 | { 32 | add { value(this, new ValueChangedEventArgs { NewValue = Value }); } 33 | remove { } 34 | } 35 | 36 | public IAstNode Optimize() 37 | { 38 | return this; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /DCSBravoLights/DcsExpressionParser.cs: -------------------------------------------------------------------------------- 1 | using BravoLights.Common; 2 | using BravoLights.Common.Ast; 3 | using sly.lexer; 4 | using sly.parser.generator; 5 | 6 | namespace DCSBravoLights 7 | { 8 | class DcsExpressionParser : ExpressionParserBase 9 | { 10 | [Production("primary: DCS_VAR")] 11 | public IAstNode DcsVarExpression(Token token) 12 | { 13 | // [Category:Identifier] 14 | var text = token.Value[1..^1].Trim(); 15 | var bits = text.Split(":"); 16 | var category = bits[0]; 17 | var identifier = bits[1]; 18 | return new DcsVariableExpression(category, identifier); 19 | } 20 | 21 | [Operand] 22 | [Production("group : LPAREN DcsExpressionParser_expressions RPAREN")] 23 | public IAstNode Group(Token lparen, IAstNode child, Token rparen) 24 | { 25 | return child; 26 | } 27 | 28 | public static IAstNode Parse(string expression) 29 | { 30 | return Parse(expression); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 Royston Shufflebotham 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /BravoLights.Tests/BravoLights.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0-windows 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | all 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | PreserveNewest 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /BravoLights/Installation/FileUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace BravoLights.Installation 9 | { 10 | static class FileUtils 11 | { 12 | 13 | public static void CopyDirectory(string source, string destination) 14 | { 15 | CopyDirectory(new DirectoryInfo(source), new DirectoryInfo(destination)); 16 | } 17 | 18 | public static void CopyDirectory(DirectoryInfo source, DirectoryInfo destination) 19 | { 20 | if (!Directory.Exists(destination.FullName)) 21 | { 22 | Directory.CreateDirectory(destination.FullName); 23 | } 24 | 25 | foreach (var fi in source.GetFiles()) 26 | { 27 | fi.CopyTo(Path.Combine(destination.FullName, fi.Name), true); 28 | } 29 | 30 | foreach (var sourceChild in source.GetDirectories()) 31 | { 32 | var destinationChild = destination.CreateSubdirectory(sourceChild.Name); 33 | CopyDirectory(sourceChild, destinationChild); 34 | } 35 | } 36 | 37 | public static void RemoveDirectoryRecursively(string name) 38 | { 39 | if (Directory.Exists(name)) 40 | { 41 | Directory.Delete(name, true); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /BravoLights.Common/Ast/ExpressionToken.cs: -------------------------------------------------------------------------------- 1 | using sly.lexer; 2 | 3 | namespace BravoLights.Common.Ast 4 | { 5 | public enum ExpressionToken 6 | { 7 | [Lexeme("L:[A-Za-z0-9_ :]+")] 8 | LVAR = 50, 9 | 10 | [Lexeme("A:[:A-Za-z0-9 ]+(,\\s*([A-Za-z0-9 ]+))?")] 11 | SIMVAR = 52, 12 | 13 | [Lexeme("\\[[A-Za-z_0-9 ]+:[A-Za-z_0-9 ]+\\]")] 14 | DCS_VAR = 53, 15 | 16 | [Lexeme("OFF")] 17 | OFF = 0, 18 | 19 | [Lexeme("ON")] 20 | ON = 1, 21 | 22 | // Hex/decimal prefix? 23 | [Lexeme("0[xX][0-9a-fA-F]+")] 24 | HEX_NUMBER = 2, 25 | 26 | [Lexeme("[0-9]+(\\.[0-9]+)?")] 27 | DECIMAL_NUMBER = 3, 28 | 29 | [Lexeme("\\+")] 30 | PLUS = 4, 31 | 32 | [Lexeme("-")] 33 | MINUS = 5, 34 | 35 | [Lexeme("\\*")] 36 | TIMES = 6, 37 | 38 | [Lexeme("/")] 39 | DIVIDE = 7, 40 | 41 | [Lexeme("\\|")] 42 | BITWISE_OR = 8, 43 | 44 | [Lexeme("&")] 45 | BITWISE_AND = 9, 46 | 47 | [Lexeme("[ \\t]+", isSkippable: true)] 48 | WHITESPACE = 20, 49 | 50 | [Lexeme("OR")] 51 | LOGICAL_OR = 10, 52 | [Lexeme("AND")] 53 | LOGICAL_AND = 11, 54 | 55 | [Lexeme("NOT")] 56 | NOT = 12, 57 | 58 | [Lexeme("(<=?)|(==)|(!=)|(>=?)")] 59 | COMPARISON = 13, 60 | 61 | [Lexeme("\\(")] 62 | LPAREN = 30, 63 | 64 | [Lexeme("\\)")] 65 | RPAREN = 31, 66 | 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /BravoLights/GlobalLightController.cs: -------------------------------------------------------------------------------- 1 | using BravoLights.Common; 2 | using NLog; 3 | 4 | namespace BravoLights 5 | { 6 | public class GlobalLightController 7 | { 8 | private static readonly Logger logger = LogManager.GetCurrentClassLogger(); 9 | 10 | private readonly IUsbLogic usbLogic; 11 | 12 | public GlobalLightController(IUsbLogic usbLogic) 13 | { 14 | this.usbLogic = usbLogic; 15 | } 16 | 17 | private bool simulatorInMainMenu = false; 18 | public bool SimulatorInMainMenu { get => simulatorInMainMenu; set { simulatorInMainMenu = value; Check(); } } 19 | 20 | private bool readingConfiguration = false; 21 | public bool ReadingConfiguration { get => readingConfiguration; set { readingConfiguration = value; Check(); } } 22 | 23 | private bool applicationExiting; 24 | public bool ApplicationExiting { get => applicationExiting; set { applicationExiting = value; Check(); } } 25 | 26 | private bool simulatorConnected; 27 | public bool SimulatorConnected { get => simulatorConnected; set { simulatorConnected = value; Check(); } } 28 | 29 | private void Check() 30 | { 31 | logger.Debug("SimulatorInMainMenu={0}, ReadingConfiguration={1}, ApplicationExiting={2}, SimulatorConnected={3}", 32 | SimulatorInMainMenu, ReadingConfiguration, ApplicationExiting, SimulatorConnected); 33 | 34 | var lightsShouldBeOn = !SimulatorInMainMenu && !ReadingConfiguration && !ApplicationExiting && SimulatorConnected; 35 | usbLogic.LightsEnabled = lightsShouldBeOn; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /BravoLights.Common/LightExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BravoLights.Common.Ast; 3 | 4 | namespace BravoLights.Common 5 | { 6 | public class LightExpression 7 | { 8 | public LightExpression(string lightName, IAstNode expression, bool errorIfNotBooleanExpression) 9 | { 10 | LightName = lightName; 11 | Expression = expression; 12 | 13 | if (errorIfNotBooleanExpression && expression.ValueType != NodeDataType.Boolean) 14 | { 15 | Expression = new ErrorNode($"A boolean expression is needed to drive a light, not a numeric one."); 16 | } 17 | } 18 | 19 | public readonly string LightName; 20 | public readonly IAstNode Expression; 21 | 22 | private EventHandler handlers; 23 | 24 | public event EventHandler ValueChanged 25 | { 26 | add 27 | { 28 | var send = handlers == null; 29 | handlers += value; 30 | if (send) 31 | { 32 | Expression.ValueChanged += Expression_ValueChanged; 33 | } 34 | } 35 | remove 36 | { 37 | handlers -= value; 38 | if (handlers == null) 39 | { 40 | Expression.ValueChanged -= Expression_ValueChanged; 41 | } 42 | } 43 | } 44 | 45 | private void Expression_ValueChanged(object sender, ValueChangedEventArgs e) 46 | { 47 | handlers?.Invoke(this, e); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /BravoLights/LightExpressionConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using BravoLights.Ast; 4 | using BravoLights.Common; 5 | 6 | namespace BravoLights 7 | { 8 | internal class LightExpressionConfig 9 | { 10 | /// 11 | /// Computes the light expressions for a particular aircraft. 12 | /// 13 | /// 14 | /// Returns an expression for every known light. 15 | /// 16 | public static IEnumerable ComputeLightExpressions(IConfig config, string aircraft) 17 | { 18 | var invert = config.GetConfig(aircraft, "Invert") ?? ""; 19 | var masterEnable = config.GetConfig(aircraft, "MasterEnable") ?? "ON"; 20 | 21 | var lightNamesToInvert = new HashSet(invert.Split(',', ' ').Select(n => n.Trim())); 22 | 23 | var lightExpressions = LightNames.AllNames.Select(lightName => 24 | { 25 | var expressionText = config.GetConfig(aircraft, lightName); 26 | if (expressionText == null) 27 | { 28 | expressionText = "OFF"; 29 | } 30 | 31 | if (lightNamesToInvert.Contains(lightName)) 32 | { 33 | expressionText = $"NOT({expressionText})"; 34 | } 35 | 36 | expressionText = $"({masterEnable}) AND ({expressionText})"; 37 | 38 | var expression = MSFSExpressionParser.Parse(expressionText).Optimize(); 39 | 40 | return new LightExpression(lightName, expression, true); 41 | }); 42 | 43 | return lightExpressions; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/30-support-question.yaml: -------------------------------------------------------------------------------- 1 | name: "❓ Ask a support question" 2 | description: I can't figure out how to make BetterBravoLights do something in particular. 3 | labels: needs-triage 4 | assignees: RoystonS 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Please fill out this form with all the relevant information so we can help you out. 11 | 12 | We appreciate your questions as we can use them to improve the software and documentation! 13 | 14 | - type: dropdown 15 | id: bbl_version 16 | attributes: 17 | label: Software version 18 | description: Which version of BetterBravoLights are you using? 19 | options: 20 | - 0.9.0 21 | - 0.8.2 22 | - 0.8.1 23 | - 0.8.0 24 | - 0.7.0 25 | - 0.6.0 26 | validations: 27 | required: true 28 | 29 | - type: textarea 30 | id: bbl_what_doing 31 | attributes: 32 | label: What are you trying to do? 33 | description: Please tell us what you're trying to achieve 34 | placeholder: I can't figure out how to get the HDG light to light up in the Concorde. 35 | validations: 36 | required: true 37 | 38 | - type: textarea 39 | id: bbl_problem 40 | attributes: 41 | label: What's the problem? 42 | description: What is (or isn't) happening as you'd expect? 43 | placeholder: BBL is reporting that my HDG configuration line isn't valid 44 | 45 | - type: textarea 46 | id: bbl_tried 47 | attributes: 48 | label: What have you tried already? 49 | description: It'll save us some effort if you tell us the alternative approaches you've already tried. 50 | placeholder: I've tried setting `HDG` to be `L:CONC_HDG = 1` and `L:CONC_HDG` but both are reported as "not valid" 51 | 52 | - type: textarea 53 | id: bbl_extra 54 | attributes: 55 | label: Other information 56 | description: | 57 | Add other information here, such as other relevant software you have installed. 58 | -------------------------------------------------------------------------------- /BravoLights/Ast/MSFSExpressionParser.cs: -------------------------------------------------------------------------------- 1 | using BravoLights.Common; 2 | using BravoLights.Common.Ast; 3 | using sly.lexer; 4 | using sly.parser.generator; 5 | using System; 6 | 7 | namespace BravoLights.Ast 8 | { 9 | #pragma warning disable CA1822 // Mark members as static 10 | public class MSFSExpressionParser : ExpressionParserBase 11 | { 12 | [Operand] 13 | [Production("numeric_literal: LVAR")] 14 | public IAstNode Lvar(Token token) 15 | { 16 | var text = token.Value[2..]; 17 | 18 | return new LvarExpression 19 | { 20 | LVarName = text.Trim() 21 | }; 22 | } 23 | 24 | [Operand] 25 | [Production("numeric_literal: SIMVAR")] 26 | public IAstNode SimVarExpression(Token simvarToken) 27 | { 28 | // text will be 29 | // A:SIMVAR 30 | // or 31 | // A:SIMVAR, units 32 | // (The former isn't valid but we allow it up to this point so that we can give a nice error message about the missing units) 33 | 34 | var text = simvarToken.Value[2..]; 35 | var bits = text.Split(",", 2); 36 | var varName = bits[0].Trim(); 37 | 38 | if (bits.Length == 1) 39 | { 40 | throw new Exception($"Missing units for variable 'A:{varName}'."); 41 | } 42 | 43 | var type = bits[1].Trim(); 44 | return new SimVarExpression(varName, type); 45 | } 46 | 47 | [Operand] 48 | [Production("group : LPAREN MSFSExpressionParser_expressions RPAREN")] 49 | public IAstNode Group(Token _1, IAstNode child, Token _2) 50 | { 51 | return child; 52 | } 53 | 54 | public static IAstNode Parse(string expression) 55 | { 56 | return Parse(expression); 57 | } 58 | } 59 | #pragma warning restore CA1822 // Mark members as static 60 | } 61 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/20-feature-request.yaml: -------------------------------------------------------------------------------- 1 | name: "💡 Suggest a feature" 2 | description: I've got a great idea to help improve BetterBravoLights. 3 | labels: 4 | - needs-triage 5 | - enhancement 6 | assignees: RoystonS 7 | 8 | body: 9 | - type: markdown 10 | attributes: 11 | value: | 12 | Please fill out this form with all the relevant information to help us figure out how to improve the software. 13 | 14 | We appreciate all suggestions! 15 | 16 | - type: dropdown 17 | id: bbl_version 18 | attributes: 19 | label: Software version 20 | description: Which version of BetterBravoLights are you using? 21 | options: 22 | - 0.9.0 23 | - 0.8.2 24 | - 0.8.1 25 | - 0.8.0 26 | - 0.7.0 27 | - 0.6.0 28 | validations: 29 | required: true 30 | 31 | - type: textarea 32 | id: bbl_problem 33 | attributes: 34 | label: Problem 35 | description: Is your feature request related to a problem? Please describe the problem you'd like to solve. 36 | placeholder: Manually reticulating splines in BBL takes a long time. Could BBL do it automatically? 37 | validations: 38 | required: true 39 | 40 | - type: textarea 41 | id: bbl_solution 42 | attributes: 43 | label: Suggested solution 44 | description: Do you have a suggested solution in mind? 45 | placeholder: BBL could automatically reticulate splines by passing them through an external spline reticulation service when it starts up. 46 | 47 | - type: textarea 48 | id: bbl_tried 49 | attributes: 50 | label: What alternatives are there? 51 | description: Are there alternative solutions or features you've considered? 52 | placeholder: It would be even better if BBL didn't have to reticulate splines at all, but it obviously needs to do that given the limitations of MSFS. 53 | 54 | - type: textarea 55 | id: bbl_extra 56 | attributes: 57 | label: Other information 58 | description: | 59 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /BravoLights.Common/Ast/VariableBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace BravoLights.Common.Ast 5 | { 6 | /// 7 | /// Base class for a variable. 8 | /// 9 | public abstract class VariableBase : IVariable 10 | { 11 | public string ErrorText { get { return null; } } 12 | 13 | public IEnumerable Variables 14 | { 15 | get { yield return this; } 16 | } 17 | 18 | protected abstract IConnection Connection { get; } 19 | public abstract string Identifier { get; } 20 | 21 | public NodeDataType ValueType 22 | { 23 | get { return NodeDataType.Double; } 24 | } 25 | 26 | private readonly Dictionary, EventHandler> handlerMappings 27 | = new(); 28 | 29 | public event EventHandler ValueChanged 30 | { 31 | add 32 | { 33 | // For this incoming subscription we're just going to subscribe to the 34 | // underlying connection but with a callback that changes the sender 35 | // to be this variable. 36 | void mappedDelegate(object sender, ValueChangedEventArgs e) 37 | { 38 | value(this, e); 39 | } 40 | 41 | handlerMappings[value] = mappedDelegate; 42 | Connection.AddListener(this, mappedDelegate); 43 | } 44 | remove 45 | { 46 | var mappedDelegate = handlerMappings[value]; 47 | handlerMappings.Remove(value); 48 | Connection.RemoveListener(this, mappedDelegate); 49 | } 50 | } 51 | 52 | public bool Equals(IVariable other) 53 | { 54 | return Identifier.Equals(other.Identifier); 55 | } 56 | public override int GetHashCode() 57 | { 58 | return Identifier.GetHashCode(); 59 | } 60 | 61 | public IAstNode Optimize() 62 | { 63 | return this; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/50-help-fix-exe-xml.yaml: -------------------------------------------------------------------------------- 1 | name: "🧑‍🔧 Help me fix my corrupt exe.xml file" 2 | description: BetterBravoLights told me my existing exe.xml file is corrupt and I'd like some help. 3 | labels: exe-xml-help 4 | assignees: 5 | - RoystonS 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Use this form if you've tried to install BetterBravoLights but it refused to attempt to install because your MSFS `exe.xml` file was corrupt. 11 | 12 | However, rather than filling this form out by hand, BetterBravoLights v0.3.1 and above comes with a one-click option that will fill it all in for you automatically. 13 | 14 | - type: input 15 | id: bbl_version 16 | attributes: 17 | label: Software version 18 | description: Please indicate which version of BetterBravoLights you have 19 | placeholder: 0.9.0 20 | validations: 21 | required: true 22 | 23 | - type: input 24 | id: bbl_exe_xml_path 25 | attributes: 26 | label: Location of my exe.xml file 27 | placeholder: C:\Users\myuser\AppData\Local\Packages\Microsoft.FlightSimulator_8wekyb3d8bbwe\LocalCache 28 | validations: 29 | required: false 30 | 31 | - type: textarea 32 | id: bbl_exe_xml_contents 33 | attributes: 34 | label: Contents of my exe.xml file 35 | description: Paste the contents of your corrupted exe.xml file here 36 | render: xml 37 | placeholder: | 38 | 39 | Auto launch external applications on MSFS start 40 | exe.xml 41 | 42 | 43 | 44 | 45 | validations: 46 | required: true 47 | 48 | - type: textarea 49 | id: bbl_error 50 | attributes: 51 | label: The installation error 52 | description: Paste here the error that Better Bravo Lights gave you when attempting to install 53 | render: text 54 | placeholder: | 55 | The 'SimBase.Document' start tag on line 2 position 2 does not match the end tag of 'Launch.Addon'. Line 12, position 5. 56 | 57 | validations: 58 | required: false 59 | 60 | - type: markdown 61 | attributes: 62 | value: | 63 | I understand that BetterBravoLights didn't corrupt the file and that it's simply warning 64 | about the corrupted file. But could you help me fix it? 65 | -------------------------------------------------------------------------------- /BravoLights/UI/BBLSplashScreen.xaml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | by Royston Shufflebotham <royston@shufflebotham.org> 29 | 30 | 31 | 32 | 33 | 34 | 35 | Uses 36 | HidSharp by James F. Bellinger, 37 | sly by Olivier Duhart 38 | and 39 | NLog by Jaroslaw Kowalski, Kim Christensen & Julian Verdurmen. 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/10-bug-report.yaml: -------------------------------------------------------------------------------- 1 | name: "🐛 Report a bug" 2 | description: I'd like to report a problem with BetterBravoLights. 3 | labels: 4 | - needs-triage 5 | - bug 6 | assignees: RoystonS 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Please fill out this form with all the relevant information to so we can understand what's going on and fix the issue quickly. 12 | 13 | We appreciate all bugs filed and Pull Requests submitted! 14 | 15 | - type: dropdown 16 | id: bbl_version 17 | attributes: 18 | label: Software version 19 | description: Which version of BetterBravoLights are you using? If you don't have the latest version, please give that a try. 20 | options: 21 | - 0.9.0 22 | - 0.8.2 23 | - 0.8.1 24 | - 0.8.0 25 | - 0.7.0 26 | - 0.6.0 27 | validations: 28 | required: true 29 | 30 | - type: dropdown 31 | id: bbl_msfs_edition 32 | attributes: 33 | label: Flight Simulator installation type 34 | description: Are you using the Windows Store version of Microsoft Flight Simulator 2020 or the Steam version? 35 | options: 36 | - Windows Store 37 | - Steam 38 | 39 | - type: textarea 40 | id: bbl_problem 41 | attributes: 42 | label: Problem 43 | description: Please describe what the problem is. 44 | placeholder: Program shows the error "JUYED AWK YACC" doesn't find any results when I search for certain terms in the experimental variable list. 45 | validations: 46 | required: true 47 | 48 | - type: textarea 49 | id: bbl_reproduce 50 | attributes: 51 | label: How to reproduce the problem 52 | description: If you can indicate how to reproduce the problem, it's much easier for us to fix! 53 | placeholder: | 54 | Steps to reproduce the behavior: 55 | 56 | 1. Open the experimental variable list 57 | 2. Search for "Etaoin Shrdlu" or "Foobie Bletch" 58 | 3. BBL shows the error "JUYED AWK YACC" 59 | 4. BBL doesn't show any variables that match my search 60 | 61 | - type: textarea 62 | id: bbl_expected 63 | attributes: 64 | label: Expected behaviour 65 | description: What behaviour are you expecting from BetterBravoLights? 66 | placeholder: BBL should show all the variables that match my search term. I'm sure there are some. 67 | 68 | - type: textarea 69 | id: bbl_extra 70 | attributes: 71 | label: Other information 72 | description: | 73 | Add any other context or screenshots about the issue here. 74 | -------------------------------------------------------------------------------- /DCSBravoLights/DebuggerUI.xaml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /BravoLights.Tests/ExeXmlFixerTests.cs: -------------------------------------------------------------------------------- 1 | using BravoLights.Installation; 2 | using Xunit; 3 | 4 | namespace BravoLights.Tests 5 | { 6 | public class ExeXmlFixerTests 7 | { 8 | [Fact] 9 | public void InsertsMissingOpenSimBaseDocumentWithXmlHeaderPresent() 10 | { 11 | var input = @" 12 | 13 | AFCBridge 14 | False 15 | C:\Something\AFC_Bridge.exe 16 | 17 | "; 18 | 19 | var output = ExeXmlFixer.TryFix(input); 20 | 21 | Assert.Equal(@" 22 | 23 | 24 | AFCBridge 25 | False 26 | C:\Something\AFC_Bridge.exe 27 | 28 | ", output); 29 | } 30 | 31 | [Fact] 32 | public void InsertsMissingOpenSimBaseDocumentWithMissingXmlHeader() 33 | { 34 | var input = @" 35 | AFCBridge 36 | False 37 | C:\Something\AFC_Bridge.exe 38 | 39 | "; 40 | 41 | var output = ExeXmlFixer.TryFix(input); 42 | 43 | Assert.Equal(@" 44 | 45 | 46 | AFCBridge 47 | False 48 | C:\Something\AFC_Bridge.exe 49 | 50 | ", output); 51 | } 52 | 53 | [Fact] 54 | public void KeepsOriginalXmlEncoding() 55 | { 56 | var input = @" 57 | 58 | AFCBridge 59 | False 60 | C:\Something\AFC_Bridge.exe 61 | 62 | "; 63 | 64 | var output = ExeXmlFixer.TryFix(input); 65 | 66 | Assert.Equal(@" 67 | 68 | 69 | AFCBridge 70 | False 71 | C:\Something\AFC_Bridge.exe 72 | 73 | ", output); 74 | } 75 | 76 | [Fact] 77 | public void DoesNotTryToFixATotallyNonsensicalExeXmlFile() 78 | { 79 | var input = @" 80 | This is totally garbage"; 81 | var output = ExeXmlFixer.TryFix(input); 82 | 83 | Assert.Null(output); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /BravoLights/BravoLights.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WinExe 5 | net5.0-windows 6 | true 7 | true 8 | 0.9.0 9 | Royston Shufflebotham <royston@shufflebotham.org> 10 | Better Bravo Lights 11 | (C) 2021-2022 Royston Shufflebotham <royston@shufflebotham.org> 12 | BetterBravoLights 13 | BetterBravoLights 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | C:\MSFS SDK\SimConnect SDK\lib\managed\Microsoft.FlightSimulator.SimConnect.dll 30 | 31 | 32 | 33 | 34 | 35 | PreserveNewest 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | True 50 | True 51 | Resources.resx 52 | 53 | 54 | 55 | 56 | 57 | ResXFileCodeGenerator 58 | Resources.Designer.cs 59 | 60 | 61 | 62 | 63 | 64 | Always 65 | 66 | 67 | PreserveNewest 68 | 69 | 70 | PreserveNewest 71 | 72 | 73 | PreserveNewest 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /BravoLights/UI/CorruptExeXmlErrorWindow.xaml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | We've found an existing problem with one of your Flight Simulator configuration files. 17 | 18 | In order to install Better Bravo Lights we would need to modify that file, but because it's already corrupted we can't safely do that. 19 | The file needs to be repaired before Better Bravo Lights - or any other similar tool - can be installed. 20 | 21 | 22 | The file in question is the exe.xml file at: 23 | 24 | 25 | Show me that folder 26 | 27 | 28 | Your file is unfortunately not a valid XML file. You can find full details at our GitHub if you want to know more. 29 | 30 | 31 | If you're not sure how to repair the file, that's fine: we may be able to help you. 32 | 33 | Please raise a support question at our GitHub and 34 | we'll try to help you fix it. You'll need a GitHub account to raise the issue, but that link will automatically populate 35 | the entire issue for you, complete with the error details and contents of your file. 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /BravoLights/UI/ExpressionAndVariablesViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | using BravoLights.Common; 4 | 5 | namespace BravoLights.UI 6 | { 7 | public class ExpressionAndVariablesViewModel : ViewModelBase 8 | { 9 | private ObservableCollection variables = new(); 10 | public ObservableCollection Variables 11 | { 12 | get { return variables; } 13 | private set 14 | { 15 | SetProperty(ref variables, value); 16 | } 17 | } 18 | 19 | private string expressionText = ""; 20 | public string ExpressionText 21 | { 22 | get { return expressionText; } 23 | private set 24 | { 25 | SetProperty(ref expressionText, value); 26 | } 27 | } 28 | 29 | private bool expressionErrored = false; 30 | public bool ExpressionErrored 31 | { 32 | get { return expressionErrored; } 33 | private set 34 | { 35 | SetProperty(ref expressionErrored, value); 36 | } 37 | } 38 | 39 | private ISet monitoredVariables; 40 | 41 | public void Monitor(LightExpression lightExpression) 42 | { 43 | if (monitoredVariables != null) 44 | { 45 | foreach (var variable in monitoredVariables) 46 | { 47 | variable.ValueChanged -= Variable_ValueChanged; 48 | } 49 | } 50 | monitoredVariables = null; 51 | 52 | Variables = null; 53 | ExpressionText = ""; 54 | ExpressionErrored = false; 55 | 56 | if (lightExpression != null) 57 | { 58 | ExpressionText = lightExpression.Expression.ToString(); 59 | ExpressionErrored = lightExpression.Expression.ErrorText != null; 60 | 61 | var variables = new ObservableCollection(); 62 | Variables = variables; 63 | 64 | monitoredVariables = new HashSet(lightExpression.Expression.Variables); 65 | foreach (var variable in monitoredVariables) 66 | { 67 | var variableState = new VariableState { Name = variable.Identifier }; 68 | variables.Add(variableState); 69 | 70 | variable.ValueChanged += Variable_ValueChanged; 71 | } 72 | } 73 | } 74 | 75 | private void Variable_ValueChanged(object sender, ValueChangedEventArgs e) 76 | { 77 | var variable = sender as IVariable; 78 | 79 | foreach (var variableState in Variables) 80 | { 81 | if (variableState.Name == variable.Identifier) 82 | { 83 | variableState.Value = e.NewValue; 84 | } 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /BravoLights/UI/CorruptExeXmlErrorWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Web; 5 | using System.Windows; 6 | using System.Windows.Navigation; 7 | using BravoLights.Installation; 8 | 9 | namespace BravoLights.UI 10 | { 11 | /// 12 | /// Interaction logic for CorruptExeXmlErrorWindow.xaml 13 | /// 14 | public partial class CorruptExeXmlErrorWindow : Window 15 | { 16 | public CorruptExeXmlErrorWindow() 17 | { 18 | InitializeComponent(); 19 | } 20 | 21 | private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e) 22 | { 23 | e.Handled = true; 24 | Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri) { UseShellExecute = true }); 25 | } 26 | 27 | private void ShowExeXmlLocation(object sender, RequestNavigateEventArgs e) 28 | { 29 | e.Handled = true; 30 | ShowFolderLocation(Exception); 31 | } 32 | 33 | public static void ShowFolderLocation(CorruptExeXmlException exception) 34 | { 35 | Process.Start(new ProcessStartInfo(Path.GetDirectoryName(exception.ExeXmlFilename)) { UseShellExecute = true }); 36 | } 37 | 38 | private void RaiseExeXmlIssue(object sender, RequestNavigateEventArgs e) 39 | { 40 | e.Handled = true; 41 | RaiseGitHubIssue(Exception); 42 | } 43 | 44 | public static void RaiseGitHubIssue(CorruptExeXmlException exception) 45 | { 46 | var queryParams = HttpUtility.ParseQueryString(string.Empty); 47 | queryParams["template"] = "50-help-fix-exe-xml.yaml"; 48 | 49 | queryParams["labels"] = "exe-xml-help"; 50 | queryParams["title"] = "Can you help fix my corrupt exe.xml file?"; 51 | 52 | queryParams["bbl_error"] = exception.InnerException.Message; 53 | queryParams["bbl_version"] = ProgramInfo.VersionString; 54 | queryParams["bbl_exe_xml_path"] = exception.ExeXmlFilename; 55 | queryParams["bbl_exe_xml_contents"] = exception.OriginalContent; 56 | 57 | var uriBuilder = new UriBuilder("https://github.com/RoystonS/BetterBravoLights/issues/new") 58 | { 59 | Query = queryParams.ToString() 60 | }; 61 | 62 | var uri = uriBuilder.ToString(); 63 | 64 | Process.Start(new ProcessStartInfo(uri) { UseShellExecute = true }); 65 | } 66 | 67 | public static readonly DependencyProperty ExceptionProperty = DependencyProperty.Register("Exception", typeof(CorruptExeXmlException), typeof(CorruptExeXmlErrorWindow)); 68 | public CorruptExeXmlException Exception 69 | { 70 | get { return (CorruptExeXmlException)GetValue(ExceptionProperty); } 71 | set { SetValue(ExceptionProperty, value); } 72 | } 73 | 74 | private void Button_Click(object sender, RoutedEventArgs e) 75 | { 76 | Close(); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /BravoLights/UI/ExpressionAndVariables.xaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Expression 22 | 25 | 26 | Variables referenced by expression 27 | 30 | 31 | 35 | 36 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /BravoLights.Tests/ExpressionCombinationTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BravoLights.Ast; 3 | using BravoLights.Common; 4 | using BravoLights.Connections; 5 | using Moq; 6 | using Xunit; 7 | 8 | namespace BravoLights.Tests 9 | { 10 | public class ExpressionCombinationTests 11 | { 12 | private static void SetupLVarManager() 13 | { 14 | var mockWasmChannel = new Mock(); 15 | mockWasmChannel.SetupGet(c => c.SimState).Returns(SimState.SimRunning); 16 | LVarManager.Connection.SetWASMChannel(mockWasmChannel.Object); 17 | } 18 | 19 | [Fact] 20 | public void ORExpressionsShortcircuitCorrectly() 21 | { 22 | SetupLVarManager(); 23 | 24 | var parse = MSFSExpressionParser.Parse("L:Var1 > 3 OR 1 == 1"); 25 | 26 | object lastValue = null; 27 | parse.ValueChanged += (object sender, ValueChangedEventArgs e) => 28 | { 29 | lastValue = e.NewValue; 30 | }; 31 | 32 | // Even though the LVar does not exist we should still be able to short-circuit the result because one side of the OR is true. 33 | Assert.Equal(true, lastValue); 34 | } 35 | 36 | [Fact] 37 | public void ORExpressionsDoNotShortcircuitIfNeitherSideIsTrue() 38 | { 39 | SetupLVarManager(); 40 | 41 | var parse = MSFSExpressionParser.Parse("L:Var1 > 3 OR 1 == 0"); 42 | 43 | object lastValue = null; 44 | parse.ValueChanged += (object sender, ValueChangedEventArgs e) => 45 | { 46 | lastValue = e.NewValue; 47 | }; 48 | 49 | // One side of the OR is false, so we're dependent upon the other side, which is erroring. So the OR should error. 50 | Assert.IsType(lastValue); 51 | } 52 | 53 | [Fact] 54 | public void ANDExpressionsShortcircuitCorrectly() 55 | { 56 | SetupLVarManager(); 57 | 58 | var parse = MSFSExpressionParser.Parse("L:Var1 > 3 AND 1 == 0"); 59 | 60 | object lastValue = null; 61 | parse.ValueChanged += (object sender, ValueChangedEventArgs e) => 62 | { 63 | lastValue = e.NewValue; 64 | }; 65 | 66 | // Even though the LVar does not exist we should still be able to short-circuit the result because one side of the AND is false 67 | Assert.Equal(false, lastValue); 68 | } 69 | 70 | [Fact] 71 | public void ANDExpressionsDoNotShortcircuitIfEitherSideIsTrue() 72 | { 73 | SetupLVarManager(); 74 | 75 | var parse = MSFSExpressionParser.Parse("L:Var1 > 3 AND 1 == 1"); 76 | 77 | object lastValue = null; 78 | parse.ValueChanged += (object sender, ValueChangedEventArgs e) => 79 | { 80 | lastValue = e.NewValue; 81 | }; 82 | 83 | // One side of the AND is true, so we're dependent upon the other side, which is erroring. So the AND should error. 84 | Assert.IsType(lastValue); 85 | } 86 | 87 | 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/40-suggested-config.yaml: -------------------------------------------------------------------------------- 1 | name: "🔨 Share configuration" 2 | description: I've developed some cool BetterBravoLights configuration and would like to share it with the world. 3 | labels: needs-triage 4 | assignees: RoystonS 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Use this form if you've developed some BetterBravoLights configuration that you think would be useful for others. If we use your configuration we'll give you credit, of course! 10 | 11 | If you're submitting multiple separate pieces of configuration for separate aircraft, please submit this form once for each aircraft so that we can keep the changes and discussions separate. 12 | 13 | - type: dropdown 14 | id: bbl_version 15 | attributes: 16 | label: Software version 17 | description: Which version of BetterBravoLights are you using? 18 | options: 19 | - 0.9.0 20 | - 0.8.2 21 | - 0.8.1 22 | - 0.8.0 23 | - 0.7.0 24 | - 0.6.0 25 | validations: 26 | required: true 27 | 28 | - type: dropdown 29 | id: bbl_type 30 | attributes: 31 | label: Change type 32 | description: Is this adding a new aircraft or modifying one that BetterBravoLights already knows about? 33 | options: 34 | - New aircraft 35 | - Modify existing aircraft 36 | validations: 37 | required: true 38 | 39 | - type: textarea 40 | id: bbl_aircrafts 41 | attributes: 42 | label: Aircrafts 43 | description: | 44 | "Which aircrafts are you modifying/adding?" 45 | "Please include the manufacturer of the simulated aircraft, not just the aircraft name, and for separately-installed addons, the version, if possible." 46 | placeholder: "DC Designs Stearman 1.0.3" 47 | 48 | - type: textarea 49 | id: bbl_config 50 | attributes: 51 | label: Configuration 52 | description: | 53 | Please add your configuration here. 54 | It's really helpful if each configuration line is preceded with a comment explaining where that line comes from. This helps us figure out how 'authoritative' the configuration is. 55 | 56 | Checking the source code of the aircraft is _really_ reliable. Checking the documentation/POH is _quite_ reliable. Other educated guesses are _fairly_ reliable. 57 | 58 | See https://github.com/RoystonS/BetterBravoLights/blob/f4574ea871ac09f1936b48478ef1f9be8ea13ff9/BravoLights/Config.ini#L110-L117 for examples. 59 | render: ini 60 | placeholder: | 61 | ;; Cool New Aircraft 62 | [Aircraft.Cool_New_Aircraft, Aircraft.Cool_New_Aircraft_Variant1] 63 | ; From cool-new-aircraft\html_ui\Pages\VCockpit\Instruments\NavSystems\CoolNew\NavSystem.js 64 | HDG = L:CNA_AUTOPILOT_HDG == 1 65 | 66 | - type: textarea 67 | id: bbl_extra 68 | attributes: 69 | label: Other information 70 | description: | 71 | Add other information here, such as what they do, where you got these values from and how confident you are that they're correct. 72 | (I can't buy every possible addon aircraft and am only an amateur simulator pilot.) 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Better Bravo Lights 2 | 3 | This is the 'Better Bravo Lights' tool. It replaces the standard Honeycomb Bravo lights utility with a program which is easier to configure, more flexible and more responsive. 4 | 5 | If you want to _use_ Better Bravo Lights, all the documentation is available at the Better Bravo Lights wiki: 6 | 7 | https://github.com/RoystonS/BetterBravoLights/wiki 8 | 9 | --- 10 | 11 | If you want to work on the Better Bravo Lights code, read on: 12 | 13 | ## Developer Documentation 14 | 15 | If you'd like to work on the code, here's how to get going with it: 16 | 17 | 1. Install Visual Studio Community 2019 with C++ and C# support. 18 | 2. `git clone` the codebase. 19 | 3. Install the MSFS SDK. 20 | 4. Open the `.sln` file. 21 | 5. Build the project. 22 | 6. Run `BravoLights`. 23 | 7. Run the unit tests in `BravoLights.Tests`. The tests are mostly for configuration parsing and expression parsing. 24 | No unit tests cover simulator interactions yet. 25 | 26 | To work on the WASM module, build as above, but also: 27 | 28 | 1. Enter Dev mode in MSFS. 29 | 2. In the Dev mode File menu, open up the `MSFSWASMProject/BetterBravoLightsLVars.xml` project. 30 | 3. In the Project Editor window, click 'Build All'. This will assemble the WASM module (locking up MSFS for 10-15 seconds) and inject it into MSFS. 31 | 4. Develop the BetterBravoLights app against the injected WASM module. 32 | 5. To iterate with WASM changes, build the project and click 'Build All' again. That'll update the WASM module in MSFS. 33 | 34 | To work on installation/uninstallation you'll need a copy of the assembled WASM module in the BravoLights `Debug` directory: 35 | 36 | 1. Run `assemble-wasm-for-ide.cmd` to assemble the WASM module (note: it uses whichever was last built out of Debug or Release WASM build) 37 | and copy it to the IDE output directories. 38 | 39 | ## Releasing 40 | 41 | 1. Change the version number on the main BravoLights assembly as desired. Follow https://semver.org/ versioning. 42 | 2. If the WASM protocol has changed, change the WASM module version in MSFSWASMProject\PackageDefinitions\better-bravo-lights-lvar-module.xml. 43 | 3. Using PowerShell, run `build-and-publish.ps1` from the root directory. It will build the projects, assemble the WASM module and package 44 | it all together into a `BetterBravoLights.zip` file in the root directory, ready for release. 45 | 4. Test that .zip. 46 | 5. Commit and tag the code in Git, and push it. 47 | 6. Create a release in GitHub against that tag, and upload `BetterBravoLights.zip` to it, with an appropriate changelog. 48 | 7. Upload the new `.zip` to `flightsim.to` with the new changelog. 49 | 50 | ## Manual Testing 51 | 52 | ### Test with a non-installed BBL 53 | 54 | 1. Start BBL 55 | 2. Check tray tooltip + A: + L: variable descriptions; should indicate no connection 56 | 3. Start MSFS 57 | 4. When connected, all should update to show connection 58 | 5. Missing LVars should report as not present yet 59 | 6. Test an aircraft with no/few LVars, e.g. C172 60 | 7. Check that lights go out in main menu 61 | 8. Launch TBM 62 | 9. Check new LVars are detected and work 63 | 10. Launch Hawk 64 | 11. Check new LVars 65 | 12. Stop sim 66 | 13. Check variables report no connection 67 | -------------------------------------------------------------------------------- /BravoLights/UI/ProgramInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Net.Http.Headers; 4 | using System.Reflection; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | 8 | namespace BravoLights.UI 9 | { 10 | public class ProgramInfo 11 | { 12 | public static string ProductNameAndVersion 13 | { 14 | get 15 | { 16 | return $"Better Bravo Lights {VersionString}"; 17 | } 18 | } 19 | 20 | public static string VersionString 21 | { 22 | get { 23 | var version = Assembly.GetExecutingAssembly().GetName().Version; 24 | return $"{version.Major}.{version.Minor}.{version.Build}"; 25 | } 26 | } 27 | 28 | private static async Task FetchLatestVersionStringAsync() 29 | { 30 | var client = new HttpClient(); 31 | client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json")); 32 | client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("RoystonS-BetterBravoLights", VersionString)); 33 | 34 | // We're not going to bother paging so we'll assume that the latest version is somewhere in the first page. 35 | var response = await client.GetStringAsync("https://api.github.com/repos/RoystonS/BetterBravoLights/releases"); 36 | return ExtractLatestVersionFromGitHubReleasesJson(response); 37 | } 38 | 39 | internal static string ExtractLatestVersionFromGitHubReleasesJson(string json) 40 | { 41 | var doc = JsonDocument.Parse(json); 42 | 43 | Version latestVersion = null; 44 | foreach (var releaseEntry in doc.RootElement.EnumerateArray()) 45 | { 46 | try 47 | { 48 | // v0.6.0 49 | var releaseName = releaseEntry.GetProperty("name").GetString(); 50 | var versionString = releaseName[1..]; 51 | var version = new Version(versionString); 52 | if (latestVersion == null || version.CompareTo(latestVersion) > 0) 53 | { 54 | latestVersion = version; 55 | } 56 | } 57 | catch 58 | { 59 | } 60 | } 61 | 62 | return latestVersion.ToString(); 63 | } 64 | 65 | private static Task cachedLatestVersionFetch; 66 | 67 | public static Task GetLatestVersionStringAsync() 68 | { 69 | if (cachedLatestVersionFetch == null) 70 | { 71 | cachedLatestVersionFetch = FetchLatestVersionStringAsync(); 72 | } 73 | 74 | return cachedLatestVersionFetch; 75 | } 76 | 77 | public static async Task IsNewVersionAvailableAsync() 78 | { 79 | try 80 | { 81 | var latestVersion = await GetLatestVersionStringAsync(); 82 | return latestVersion != VersionString; 83 | } catch 84 | { 85 | return false; 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /BravoLights.Common/Ast/UnaryExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace BravoLights.Common.Ast 5 | { 6 | abstract class UnaryExpression : IAstNode 7 | { 8 | protected readonly IAstNode Child; 9 | 10 | private object lastChildValue; 11 | private object lastReportedValue; 12 | 13 | protected UnaryExpression(IAstNode child) 14 | { 15 | Child = child; 16 | } 17 | 18 | public string ErrorText => null; 19 | 20 | public IEnumerable Variables 21 | { 22 | get { return Child.Variables; } 23 | } 24 | 25 | public NodeDataType ValueType 26 | { 27 | get { return NodeDataTypeUtility.GetNodeDataType(typeof(TOutput)); } 28 | } 29 | 30 | protected abstract TOutput ComputeValue(TChildren child); 31 | 32 | private void HandleChildValueChanged(object sender, ValueChangedEventArgs e) 33 | { 34 | lastChildValue = e.NewValue; 35 | 36 | Recompute(); 37 | } 38 | 39 | private void Recompute() 40 | { 41 | if (lastChildValue == null) 42 | { 43 | return; 44 | } 45 | 46 | object newValue; 47 | if (lastChildValue is Exception) 48 | { 49 | newValue = lastChildValue; 50 | } 51 | else 52 | { 53 | var child = (TChildren)Convert.ChangeType(lastChildValue, typeof(TChildren)); 54 | newValue = ComputeValue(child); 55 | } 56 | 57 | if (lastReportedValue == null || !lastReportedValue.Equals(newValue)) // N.B. We must unbox before doing the comparison otherwise we'll be comparing boxed pointers 58 | { 59 | lastReportedValue = newValue; 60 | 61 | listeners?.Invoke(this, new ValueChangedEventArgs { NewValue = newValue }); 62 | } 63 | } 64 | 65 | public abstract IAstNode Optimize(); 66 | 67 | private EventHandler listeners; 68 | 69 | public event EventHandler ValueChanged 70 | { 71 | add 72 | { 73 | var subscribe = listeners == null; 74 | // It's important that we add the listener before subscribing to children 75 | // because the subscription may fire immediately 76 | listeners += value; 77 | if (subscribe) 78 | { 79 | Child.ValueChanged += HandleChildValueChanged; 80 | } 81 | if (lastReportedValue != null) 82 | { 83 | // New subscriber and we already have a valid computed value. Ship it. 84 | value(this, new ValueChangedEventArgs { NewValue = lastReportedValue }); 85 | } 86 | } 87 | remove 88 | { 89 | listeners -= value; 90 | if (listeners == null) 91 | { 92 | Child.ValueChanged -= HandleChildValueChanged; 93 | } 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /BravoLights/UI/BBLSplashScreen.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Timers; 4 | using System.Windows; 5 | 6 | namespace BravoLights.UI 7 | { 8 | /// 9 | /// Interaction logic for BBLSplashScreen.xaml 10 | /// 11 | public partial class BBLSplashScreen : Window 12 | { 13 | private const int NewCheckTimeoutMillis = 5000; 14 | private const int MinimumSplashShowMillis = 1500; 15 | private const int MinimumNewVersionShowMillis = 20000; 16 | 17 | private DateTime showStart; 18 | 19 | public BBLSplashScreen() 20 | { 21 | InitializeComponent(); 22 | 23 | showStart = DateTime.UtcNow; 24 | 25 | newVersionCheckTimeout = new Timer { Interval = NewCheckTimeoutMillis, AutoReset = false }; 26 | newVersionCheckTimeout.Elapsed += NewVersionCheckTimer_Elapsed; 27 | CheckForNewVersion(); 28 | } 29 | 30 | public static string ProductAndVersion 31 | { 32 | get { return ProgramInfo.ProductNameAndVersion; } 33 | } 34 | 35 | private Timer newVersionCheckTimeout; 36 | 37 | private async void CheckForNewVersion() 38 | { 39 | newVersionCheckTimeout.Start(); 40 | var latestVersion = await ProgramInfo.GetLatestVersionStringAsync(); 41 | if (this.Visibility != Visibility.Visible) 42 | { 43 | // Already hidden. 44 | return; 45 | } 46 | newVersionCheckTimeout.Stop(); 47 | 48 | if (latestVersion == ProgramInfo.VersionString) 49 | { 50 | // We have the latest version. Let's make sure the splash screen shows for our minimum time 51 | var remainingMinimumTime = MinimumSplashShowMillis - DateTime.UtcNow.Subtract(showStart).TotalMilliseconds; 52 | if (remainingMinimumTime > 0) 53 | { 54 | var timer = new Timer { Interval = remainingMinimumTime, AutoReset = false }; 55 | timer.Elapsed += delegate { HideOnCorrectThread(); }; 56 | timer.Start(); 57 | } else 58 | { 59 | // The splash screen has already been up for long enough 60 | HideOnCorrectThread(); 61 | } 62 | } 63 | else 64 | { 65 | // This is not the latest version; let the user know 66 | Dispatcher.Invoke(delegate 67 | { 68 | NewVersionLinkText.Text = $"A new version ({latestVersion}) is available"; 69 | }); 70 | 71 | // If there's a new version, we want to ensure that's shown for a bit 72 | var timer = new Timer { Interval = MinimumNewVersionShowMillis, AutoReset = false }; 73 | timer.Elapsed += delegate { HideOnCorrectThread(); }; 74 | timer.Start(); 75 | } 76 | } 77 | 78 | private void NewVersionCheckTimer_Elapsed(object sender, ElapsedEventArgs e) 79 | { 80 | // We waited for 5 seconds to hear back from the version check, but it didn't work. 81 | // That's long enough to have the splash screen up, so just give up now. 82 | HideOnCorrectThread(); 83 | } 84 | 85 | private void HideOnCorrectThread() 86 | { 87 | Dispatcher.Invoke(delegate { Hide(); }); 88 | } 89 | 90 | private void NewVersionLink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e) 91 | { 92 | e.Handled = true; 93 | Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri) { UseShellExecute = true }); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /BravoLights/Installation/ExeXmlFixer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using System.Text.RegularExpressions; 5 | using System.Xml.Linq; 6 | 7 | namespace BravoLights.Installation 8 | { 9 | internal static class ExeXmlFixer 10 | { 11 | /// 12 | /// Attempts to fix broken exe.xml file contents. 13 | /// 14 | /// exe.xml contents, already known to be broken. 15 | /// A fixed copy of the exe.xml contents if we can DEFINITELY 16 | /// fix it with zero risk, or null if it's not auto-fixable. 17 | /// 18 | public static string TryFix(string exeXml) 19 | { 20 | if (!exeXml.Contains(" 23 | // line. We just need to insert one back in, which is straightforward. 24 | 25 | if (!exeXml.StartsWith("" + Environment.NewLine + exeXml; 29 | } 30 | 31 | var endOfXmlHeader = exeXml.IndexOf("?>"); 32 | if (endOfXmlHeader > 15 && endOfXmlHeader < 60) 33 | { 34 | // That looks a reasonable place for the header to end. 35 | exeXml = exeXml.Substring(0, endOfXmlHeader+2) + Environment.NewLine + @"" + exeXml.Substring(endOfXmlHeader+2); 36 | } 37 | 38 | try 39 | { 40 | var doc = XDocument.Parse(exeXml); 41 | // Hooray. With those changes we can read the XML file. 42 | 43 | var windows1252EncodingRegex = new Regex("windows-1252", RegexOptions.IgnoreCase); 44 | var originalEncoding = (doc.Declaration != null && doc.Declaration.Encoding != null) ? doc.Declaration.Encoding : "Windows-1252"; 45 | 46 | var sw = new StringWriterWithEncoding(originalEncoding); 47 | doc.Save(sw); 48 | 49 | // The .NET CP-1252 Encoding calls itself 'windows-1252' but MSFS usually uses 'Windows-1252'. 50 | // Let's restore the original encoding name; 51 | if (windows1252EncodingRegex.IsMatch(originalEncoding)) 52 | { 53 | return sw.ToString().Replace("windows-1252", originalEncoding); 54 | } 55 | else 56 | { 57 | return sw.ToString(); 58 | } 59 | } 60 | catch 61 | { 62 | return null; 63 | } 64 | } 65 | 66 | return null; 67 | } 68 | 69 | static ExeXmlFixer() 70 | { 71 | Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); 72 | } 73 | } 74 | 75 | class StringWriterWithEncoding : StringWriter 76 | { 77 | private readonly Encoding mEncoding; 78 | 79 | public StringWriterWithEncoding(string encoding) 80 | { 81 | try 82 | { 83 | mEncoding = Encoding.GetEncoding(encoding); 84 | } 85 | catch (Exception) 86 | { 87 | mEncoding = Encoding.GetEncoding("windows-1252"); 88 | } 89 | } 90 | 91 | public override Encoding Encoding => mEncoding; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /DCSBravoLights/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | using BravoLights.Common; 4 | 5 | namespace DCSBravoLights 6 | { 7 | public partial class MainWindow : Window 8 | { 9 | public MainWindow() 10 | { 11 | InitializeComponent(); 12 | } 13 | 14 | private readonly DcsBiosState dcsBiosState = new(); 15 | private DebuggerUI debuggerUI; 16 | private LightsState lightsState; 17 | #pragma warning disable IDE0052 // Remove unread private members 18 | private UsbLogic usbLogic; 19 | #pragma warning restore IDE0052 // Remove unread private members 20 | 21 | protected override void OnInitialized(EventArgs e) 22 | { 23 | base.OnInitialized(e); 24 | 25 | dcsBiosState.StartListening(); 26 | 27 | var variablesManager = new DcsVariablesManager(dcsBiosState); 28 | 29 | DcsConnection.Connection.DcsVariablesManager = variablesManager; 30 | 31 | debuggerUI = new(); 32 | debuggerUI.Show(); 33 | 34 | lightsState = new LightsState(); 35 | usbLogic = new UsbLogic(lightsState) 36 | { 37 | LightsEnabled = true 38 | }; 39 | 40 | Monitor(LightNames.GearLGreen, "[Landing Gear and Flap Control Panel:GEAR_L_SAFE] == 1"); 41 | Monitor(LightNames.GearCGreen, "[Landing Gear and Flap Control Panel:GEAR_N_SAFE] == 1"); 42 | Monitor(LightNames.GearRGreen, "[Landing Gear and Flap Control Panel:GEAR_R_SAFE] == 1"); 43 | Monitor(LightNames.GearLRed, "[Landing Gear and Flap Control Panel:GEAR_L_SAFE] == 0 AND [Landing Gear and Flap Control Panel:HANDLE_GEAR_WARNING] == 1"); 44 | Monitor(LightNames.GearCRed, "[Landing Gear and Flap Control Panel:GEAR_N_SAFE] == 0 AND [Landing Gear and Flap Control Panel:HANDLE_GEAR_WARNING] == 1"); 45 | Monitor(LightNames.GearRRed, "[Landing Gear and Flap Control Panel:GEAR_R_SAFE] == 0 AND [Landing Gear and Flap Control Panel:HANDLE_GEAR_WARNING] == 1"); 46 | Monitor(LightNames.MasterCaution, "[UFC:MASTER_CAUTION] == 1"); 47 | Monitor(LightNames.LowOilPressure, "[Caution Lights Panel:CL_F2] == 1 OR [Caution Lights Panel:CL_F3] == 1"); 48 | Monitor(LightNames.LowHydPressure, "[Caution Lights Panel:CL_A2] == 1 OR [Caution Lights Panel:CL_A3] == 1"); 49 | Monitor(LightNames.LowFuelPressure, "[Caution Lights Panel:CL_J2] == 1 OR [Caution Lights Panel:CL_J3] == 1"); 50 | Monitor(LightNames.AuxFuelPump, "[Caution Lights Panel:CL_G2] == 1 OR [Caution Lights Panel:CL_G3] == 1 OR [Caution Lights Panel:CL_H2] == 1 OR [Caution Lights Panel:CL_H3] == 1"); 51 | Monitor(LightNames.EngineFire, "[Glare Shield:APU_FIRE] == 1 OR [Glare Shield:L_ENG_FIRE] == 1 OR [Glare Shield:R_ENG_FIRE] == 1"); 52 | Monitor(LightNames.StarterEngaged, "[Caution Lights Panel:CL_A1] == 1"); 53 | Monitor(LightNames.APU, "[Caution Lights Panel:CL_L1] == 1"); 54 | Monitor(LightNames.Door, "[Misc:CANOPY_VALUE] > 0"); 55 | Monitor(LightNames.AntiIce, "[Environment Control Panel:ENVCP_PITOT_HEAT] != 1"); 56 | Monitor(LightNames.LowVolts, "[Caution Lights Panel:CL_L2] == 1 OR [Caution Lights Panel:CL_L3] == 1"); 57 | } 58 | 59 | private void Monitor(string lightName, string expression) 60 | { 61 | var lightExpression = new LightExpression(lightName, DcsExpressionParser.Parse(expression), true); 62 | 63 | lightExpression.ValueChanged += LightExpression_ValueChanged; 64 | } 65 | 66 | private void LightExpression_ValueChanged(object sender, ValueChangedEventArgs e) 67 | { 68 | var lightExpression = (LightExpression)sender; 69 | 70 | var lit = e.NewValue is not Exception && (bool)e.NewValue; 71 | lightsState.SetLight(lightExpression.LightName, lit); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /DCSBravoLights/DataDefinitions.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace DCSBravoLights 4 | { 5 | public enum DataType 6 | { 7 | Integer, 8 | String 9 | } 10 | 11 | public abstract class DataDefinition 12 | { 13 | public VariableName VariableName { get; set; } 14 | 15 | public string Description { get; set; } 16 | 17 | public string Suffix { get; set; } 18 | 19 | public ushort Address { get; set; } 20 | 21 | public abstract DataType Type { get; } 22 | 23 | public abstract ushort Length { get; } 24 | 25 | public abstract object GetValue(byte[] rawData, bool[] dataValid); 26 | 27 | public abstract string GetStringValue(byte[] rawData, bool[] dataValid); 28 | 29 | public abstract bool WouldChange(byte[] oldData, ushort position, byte newByte); 30 | } 31 | 32 | public class IntegerDefinition : DataDefinition 33 | { 34 | public override DataType Type { get { return DataType.Integer; } } 35 | public override ushort Length { get { return 2; } } 36 | 37 | public ushort Mask; 38 | public ushort MaxValue; 39 | public ushort ShiftBy; 40 | 41 | public override object GetValue(byte[] rawData, bool[] dataValid) 42 | { 43 | if (!dataValid[Address] || !dataValid[Address + 1]) 44 | { 45 | return null; 46 | } 47 | var byte1 = rawData[Address]; 48 | var byte2 = rawData[Address + 1]; 49 | 50 | return Compute(byte1, byte2); 51 | } 52 | 53 | private ushort Compute(byte byte1, byte byte2) 54 | { 55 | var rawValue = (ushort)(byte1 + byte2 * 256); 56 | var masked = (ushort)(rawValue & Mask); 57 | var shifted = (ushort)(masked >> ShiftBy); 58 | return shifted; 59 | } 60 | 61 | public override bool WouldChange(byte[] oldData, ushort position, byte newByte) 62 | { 63 | var oldByte1 = oldData[Address]; 64 | var oldByte2 = oldData[Address + 1]; 65 | 66 | var oldValue = Compute(oldByte1, oldByte2); 67 | 68 | var newByte1 = oldByte1; 69 | var newByte2 = oldByte2; 70 | if (position == Address) 71 | { 72 | newByte1 = newByte; 73 | } 74 | else 75 | { 76 | newByte2 = newByte; 77 | } 78 | var newValue = Compute(newByte1, newByte2); 79 | return oldValue != newValue; 80 | } 81 | 82 | public override string GetStringValue(byte[] rawData, bool[] dataValid) 83 | { 84 | return GetValue(rawData, dataValid).ToString(); 85 | } 86 | } 87 | 88 | public class StringDefinition : DataDefinition 89 | { 90 | public override DataType Type { get { return DataType.String; } } 91 | public ushort MaxLength; 92 | public override ushort Length { get { return MaxLength; } } 93 | 94 | public override string GetValue(byte[] rawData, bool[] dataValid) 95 | { 96 | for (var i = Address; i < Address + Length; i++) 97 | { 98 | if (!dataValid[i]) 99 | { 100 | return null; 101 | } 102 | } 103 | 104 | var subset = rawData[Address..(Address + Length)]; 105 | return Encoding.ASCII.GetString(subset); 106 | } 107 | 108 | public override string GetStringValue(byte[] rawData, bool[] dataValid) 109 | { 110 | return GetValue(rawData, dataValid); 111 | } 112 | 113 | public override bool WouldChange(byte[] oldData, ushort position, byte newByte) 114 | { 115 | return oldData[position] != newByte; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /BravoLights/UI/VariableList.xaml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | This window shows all of the known A: and L: variables from the simulator. 25 | The Filter box can be used to filter down the list of variables. 26 | 27 | 28 | WARNING: this user interface is experimental and may cause the simulator to run more slowly or even crash. 29 | Note that the list of A: variables may be incomplete; please provide feedback if there are important ones missing. 30 | 31 | 32 | 33 | 36 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /BravoLights.Common/Ast/BinaryExpression.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace BravoLights.Common.Ast 6 | { 7 | abstract class BinaryExpression : IAstNode 8 | { 9 | internal readonly IAstNode Lhs; 10 | internal readonly IAstNode Rhs; 11 | 12 | private object lastLhsValue; 13 | private object lastRhsValue; 14 | private object lastReportedValue; 15 | 16 | protected BinaryExpression(IAstNode lhs, IAstNode rhs) 17 | { 18 | Lhs = lhs; 19 | Rhs = rhs; 20 | } 21 | 22 | public string ErrorText => null; 23 | 24 | public IEnumerable Variables 25 | { 26 | get 27 | { 28 | foreach (var variable in Lhs.Variables) { 29 | yield return variable; 30 | } 31 | foreach (var variable in Rhs.Variables) 32 | { 33 | yield return variable; 34 | } 35 | } 36 | } 37 | 38 | protected abstract string OperatorText { get; } 39 | 40 | public NodeDataType ValueType 41 | { 42 | get 43 | { 44 | return NodeDataTypeUtility.GetNodeDataType(typeof(TOutput)); 45 | } 46 | } 47 | 48 | protected abstract object ComputeValue(object lhsValue, object rhsValue); 49 | 50 | private void HandleLhsValueChanged(object sender, ValueChangedEventArgs e) 51 | { 52 | lastLhsValue = e.NewValue; 53 | 54 | Recompute(); 55 | } 56 | 57 | private void HandleRhsValueChanged(object sender, ValueChangedEventArgs e) 58 | { 59 | lastRhsValue = e.NewValue; 60 | 61 | Recompute(); 62 | } 63 | 64 | private void Recompute() 65 | { 66 | if (lastLhsValue == null || lastRhsValue == null) 67 | { 68 | return; 69 | } 70 | 71 | object newValue = ComputeValue(lastLhsValue, lastRhsValue); 72 | 73 | if (lastReportedValue == null || !lastReportedValue.Equals(newValue)) // N.B. We must unbox before doing the comparison otherwise we'll be comparing boxed pointers 74 | { 75 | lastReportedValue = newValue; 76 | 77 | listeners?.Invoke(this, new ValueChangedEventArgs { NewValue = newValue }); 78 | } 79 | } 80 | 81 | private EventHandler listeners; 82 | 83 | public event EventHandler ValueChanged 84 | { 85 | add 86 | { 87 | var subscribe = listeners == null; 88 | // It's important that we add the listener before subscribing to children 89 | // because the subscription may fire immediately 90 | listeners += value; 91 | if (subscribe) 92 | { 93 | Lhs.ValueChanged += HandleLhsValueChanged; 94 | Rhs.ValueChanged += HandleRhsValueChanged; 95 | } 96 | if (lastReportedValue != null) 97 | { 98 | // New subscriber and we already have a valid computed value. Ship it. 99 | value(this, new ValueChangedEventArgs { NewValue = lastReportedValue }); 100 | } 101 | } 102 | remove 103 | { 104 | listeners -= value; 105 | if (listeners == null) 106 | { 107 | Lhs.ValueChanged -= HandleLhsValueChanged; 108 | Rhs.ValueChanged -= HandleRhsValueChanged; 109 | } 110 | } 111 | } 112 | 113 | public override string ToString() 114 | { 115 | return $"({Lhs} {OperatorText} {Rhs})"; 116 | } 117 | 118 | public abstract IAstNode Optimize(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /BravoLights.Common/LightNames.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace BravoLights.Common 4 | { 5 | class LightInfo 6 | { 7 | public int Byte; 8 | public byte BitValue; 9 | } 10 | 11 | public static class LightNames 12 | { 13 | public const string HDG = "HDG"; 14 | public const string NAV = "NAV"; 15 | public const string APR = "APR"; 16 | public const string REV = "REV"; 17 | public const string ALT = "ALT"; 18 | public const string VS = "VS"; 19 | public const string IAS = "IAS"; 20 | public const string AUTOPILOT = "AUTOPILOT"; 21 | 22 | public const string GearLGreen = "GearLGreen"; 23 | public const string GearLRed = "GearLRed"; 24 | public const string GearCGreen = "GearCGreen"; 25 | public const string GearCRed = "GearCRed"; 26 | public const string GearRGreen = "GearRGreen"; 27 | public const string GearRRed = "GearRRed"; 28 | 29 | public const string MasterWarning = "MasterWarning"; 30 | public const string EngineFire = "EngineFire"; 31 | public const string LowOilPressure = "LowOilPressure"; 32 | public const string LowFuelPressure = "LowFuelPressure"; 33 | public const string AntiIce = "AntiIce"; 34 | public const string StarterEngaged = "StarterEngaged"; 35 | public const string APU = "APU"; 36 | 37 | public const string MasterCaution = "MasterCaution"; 38 | public const string Vacuum = "Vacuum"; 39 | public const string LowHydPressure = "LowHydPressure"; 40 | public const string AuxFuelPump = "AuxFuelPump"; 41 | public const string ParkingBrake = "ParkingBrake"; 42 | public const string LowVolts = "LowVolts"; 43 | public const string Door = "Door"; 44 | 45 | public static IEnumerable AllNames 46 | { 47 | get { return LightInfos.Keys; } 48 | } 49 | 50 | internal static IDictionary LightInfos = new Dictionary { 51 | { HDG, new LightInfo { Byte = 1, BitValue = 1 << 0 } }, 52 | { NAV, new LightInfo { Byte = 1, BitValue = 1 << 1 } }, 53 | { APR, new LightInfo { Byte = 1, BitValue = 1 << 2 } }, 54 | { REV, new LightInfo { Byte = 1, BitValue = 1 << 3 } }, 55 | { ALT, new LightInfo { Byte = 1, BitValue = 1 << 4 } }, 56 | { VS, new LightInfo { Byte = 1, BitValue = 1 << 5 } }, 57 | { IAS, new LightInfo { Byte = 1, BitValue = 1 << 6 } }, 58 | { AUTOPILOT, new LightInfo { Byte = 1, BitValue = 1 << 7 } }, 59 | 60 | { GearLGreen, new LightInfo { Byte = 2, BitValue = 1 << 0 } }, 61 | { GearLRed, new LightInfo { Byte = 2, BitValue = 1 << 1 } }, 62 | { GearCGreen, new LightInfo { Byte = 2, BitValue = 1 << 2 } }, 63 | { GearCRed, new LightInfo { Byte = 2, BitValue = 1 << 3 } }, 64 | { GearRGreen, new LightInfo { Byte = 2, BitValue = 1 << 4 } }, 65 | { GearRRed, new LightInfo { Byte = 2, BitValue = 1 << 5 } }, 66 | 67 | { MasterWarning, new LightInfo { Byte = 2, BitValue = 1 << 6 } }, 68 | { EngineFire, new LightInfo { Byte = 2, BitValue = 1 << 7 } }, 69 | { LowOilPressure, new LightInfo { Byte = 3, BitValue = 1 << 0 } }, 70 | { LowFuelPressure, new LightInfo { Byte = 3, BitValue = 1 << 1 } }, 71 | { AntiIce, new LightInfo { Byte = 3, BitValue = 1 << 2 } }, 72 | { StarterEngaged, new LightInfo { Byte = 3, BitValue = 1 << 3 } }, 73 | { APU, new LightInfo { Byte = 3, BitValue = 1 << 4 } }, 74 | 75 | { MasterCaution, new LightInfo { Byte = 3, BitValue = 1 << 5 } }, 76 | { Vacuum, new LightInfo { Byte = 3, BitValue = 1 << 6 } }, 77 | { LowHydPressure, new LightInfo { Byte = 3, BitValue = 1 << 7 } }, 78 | { AuxFuelPump, new LightInfo { Byte = 4, BitValue = 1 << 0 } }, 79 | { ParkingBrake, new LightInfo { Byte = 4, BitValue = 1 << 1 } }, 80 | { LowVolts, new LightInfo { Byte = 4, BitValue = 1 << 2 } }, 81 | { Door, new LightInfo { Byte = 4, BitValue = 1 << 3 } }, 82 | }; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /BravoLights.Common/UsbLogic.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using HidSharp; 4 | using NLog; 5 | 6 | namespace BravoLights.Common 7 | { 8 | public interface IUsbLogic : INotifyPropertyChanged 9 | { 10 | /// 11 | /// Gets or sets a value indicating whether we should be showing lights on the Bravo. 12 | /// If false, all lights will be switched off. If true, lights will be shown based on 13 | /// the contents of the . 14 | /// 15 | bool LightsEnabled { get; set; } 16 | 17 | /// 18 | /// Gets a value indicating whether a Bravo Throttle is actually present. 19 | /// 20 | bool BravoPresent { get; } 21 | } 22 | 23 | public class UsbLogic : ViewModelBase, IUsbLogic 24 | { 25 | private static readonly Logger logger = LogManager.GetCurrentClassLogger(); 26 | 27 | private static readonly int HoneycombVendorId = 0x294B; 28 | private static readonly int BravoProductId = 0x1901; 29 | 30 | private readonly ILightsState lightsState; 31 | 32 | private HidDevice bravoDevice; 33 | private HidStream bravoStream; 34 | 35 | public UsbLogic(ILightsState lightsState) 36 | { 37 | this.lightsState = lightsState; 38 | 39 | this.CheckBravo(); 40 | 41 | this.LightsState_Changed(null, EventArgs.Empty); 42 | 43 | DeviceList.Local.Changed += DeviceList_Changed; 44 | 45 | lightsState.PropertyChanged += LightsState_Changed; 46 | } 47 | 48 | private void CheckBravo() 49 | { 50 | var device = DeviceList.Local.GetHidDeviceOrNull(vendorID: HoneycombVendorId, productID: BravoProductId); 51 | if (device != bravoDevice) 52 | { 53 | // There's either no Bravo or a different one. Drop the old connection. 54 | if (bravoStream != null) 55 | { 56 | logger.Info("Disconnecting from Bravo"); 57 | bravoStream.Close(); 58 | bravoStream = null; 59 | bravoDevice = null; 60 | } 61 | } 62 | 63 | if (device == null) 64 | { 65 | logger.Warn("No Honeycomb Bravo device found"); 66 | this.BravoPresent = false; 67 | return; 68 | } 69 | 70 | logger.Debug("Found Honeycomb Bravo: {0}", device.DevicePath); 71 | 72 | bravoDevice = device; 73 | bravoStream = device.Open(); 74 | 75 | LightsState_Changed(null, EventArgs.Empty); 76 | this.BravoPresent = true; 77 | } 78 | 79 | private void DeviceList_Changed(object sender, DeviceListChangedEventArgs e) 80 | { 81 | this.CheckBravo(); 82 | } 83 | 84 | private bool bravoPresent = false; 85 | public bool BravoPresent 86 | { 87 | get { return bravoPresent; } 88 | private set 89 | { 90 | SetProperty(ref bravoPresent, value); 91 | } 92 | } 93 | 94 | private bool lightsEnabled = false; 95 | 96 | public bool LightsEnabled 97 | { 98 | get { return lightsEnabled; } 99 | set { 100 | if (lightsEnabled == value) 101 | { 102 | return; 103 | } 104 | 105 | logger.Debug("LightsEnabled = {0}", value); 106 | 107 | SetProperty(ref lightsEnabled, value); 108 | LightsState_Changed(null, EventArgs.Empty); 109 | } 110 | } 111 | 112 | private void LightsState_Changed(object sender, EventArgs e) 113 | { 114 | var data = new byte[] { 0, 0, 0, 0, 0 }; 115 | 116 | if (LightsEnabled) 117 | { 118 | foreach (var light in lightsState.LitLights) 119 | { 120 | var lightInfo = LightNames.LightInfos[light]; 121 | data[lightInfo.Byte] |= lightInfo.BitValue; 122 | } 123 | } 124 | 125 | if (bravoStream != null) 126 | { 127 | bravoStream.SetFeature(data); 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /BravoLights/Config.cs: -------------------------------------------------------------------------------- 1 | using NLog; 2 | using System; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Timers; 6 | 7 | namespace BravoLights 8 | { 9 | public interface IConfig 10 | { 11 | string GetConfig(string aircraft, string key); 12 | event EventHandler OnConfigChanged; 13 | } 14 | 15 | public class FileConfig : IConfig 16 | { 17 | /// 18 | /// A cooloff timer to prevent us reading the config file as soon as it changes. 19 | /// 20 | private readonly Timer backoffTimer = new() { AutoReset = false, Interval = 100 }; 21 | 22 | private FileSystemWatcher fsWatcher; 23 | private static readonly Logger logger = LogManager.GetCurrentClassLogger(); 24 | 25 | private readonly IniFile iniFile = new(); 26 | private readonly string filePath; 27 | 28 | public FileConfig(string filePath) 29 | { 30 | backoffTimer.Elapsed += BackoffTimer_Elapsed; 31 | this.filePath = Path.GetFullPath(filePath); 32 | } 33 | 34 | 35 | public event EventHandler OnConfigChanged; 36 | 37 | public void Monitor() 38 | { 39 | fsWatcher = new FileSystemWatcher(Path.GetDirectoryName(filePath)); 40 | fsWatcher.Changed += ConfigFileChanged; 41 | fsWatcher.Created += ConfigFileChanged; 42 | fsWatcher.EnableRaisingEvents = true; 43 | 44 | ReadConfig(); 45 | } 46 | 47 | private void BackoffTimer_Elapsed(object sender, ElapsedEventArgs e) 48 | { 49 | ReadConfig(); 50 | } 51 | 52 | private void ConfigFileChanged(object sender, FileSystemEventArgs e) 53 | { 54 | if (e.FullPath == filePath) 55 | { 56 | logger.Debug("Detected change to config file {0}", filePath); 57 | backoffTimer.Stop(); 58 | backoffTimer.Start(); 59 | } 60 | } 61 | 62 | public string GetConfig(string aircraft, string key) 63 | { 64 | lock (this) 65 | { 66 | var value = iniFile.GetValueOrNull($"Aircraft.{aircraft}", key); 67 | if (value != null) 68 | { 69 | return value; 70 | } 71 | 72 | value = iniFile.GetValueOrNull("Default", key); 73 | return value; 74 | } 75 | } 76 | 77 | private void ReadConfig() 78 | { 79 | logger.Debug("Reading config file {0}", filePath); 80 | try 81 | { 82 | lock (this) 83 | { 84 | iniFile.LoadConfigFromFile(filePath); 85 | } 86 | OnConfigChanged?.Invoke(this, EventArgs.Empty); 87 | } 88 | catch 89 | { 90 | logger.Warn("Failed to read config file {0}", filePath); 91 | return; 92 | } 93 | } 94 | 95 | public void LoadConfig(string[] lines) 96 | { 97 | lock (this) 98 | { 99 | iniFile.LoadConfigLines(lines); 100 | } 101 | } 102 | } 103 | 104 | public class ConfigChain : IConfig 105 | { 106 | private readonly IConfig[] configs; 107 | 108 | public ConfigChain(params IConfig[] configs) 109 | { 110 | this.configs = configs; 111 | } 112 | 113 | public event EventHandler OnConfigChanged 114 | { 115 | add 116 | { 117 | foreach (var config in configs) 118 | { 119 | config.OnConfigChanged += value; 120 | } 121 | } 122 | remove 123 | { 124 | foreach (var config in configs) 125 | { 126 | config.OnConfigChanged -= value; 127 | } 128 | } 129 | } 130 | 131 | public string GetConfig(string aircraft, string key) 132 | { 133 | foreach (var config in configs) 134 | { 135 | var result = config.GetConfig(aircraft, key); 136 | if (result != null) 137 | { 138 | return result; 139 | } 140 | } 141 | 142 | return null; 143 | } 144 | } 145 | } 146 | 147 | -------------------------------------------------------------------------------- /BravoLights.Common/Ast/BinaryNumericExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using sly.lexer; 3 | 4 | namespace BravoLights.Common.Ast 5 | { 6 | enum NumericOperator 7 | { 8 | Plus, 9 | Minus, 10 | Times, 11 | Divide, 12 | BinaryAnd, 13 | BinaryOr 14 | } 15 | 16 | /// 17 | /// A binary expression such as 'X + Y' or 'X / Y', which produces a number from two other numbers and an operator. 18 | /// 19 | abstract class BinaryNumericExpression : BinaryExpression 20 | { 21 | protected BinaryNumericExpression(IAstNode lhs, IAstNode rhs) : base(lhs, rhs) 22 | { 23 | } 24 | 25 | protected abstract double ComputeNumericValue(double lhs, double rhs); 26 | 27 | protected override object ComputeValue(object lhsValue, object rhsValue) 28 | { 29 | if (lhsValue is Exception) 30 | { 31 | return lhsValue; 32 | } 33 | if (rhsValue is Exception) 34 | { 35 | return rhsValue; 36 | } 37 | var lhs = Convert.ToDouble(lhsValue); 38 | var rhs = Convert.ToDouble(rhsValue); 39 | return ComputeNumericValue(lhs, rhs); 40 | } 41 | 42 | public static BinaryNumericExpression Create(IAstNode lhs, Token op, IAstNode rhs) 43 | { 44 | return op.Value switch 45 | { 46 | "+" => new PlusExpression(lhs, rhs), 47 | "-" => new MinusExpression(lhs, rhs), 48 | "*" => new TimesExpression(lhs, rhs), 49 | "/" => new DivideExpression(lhs, rhs), 50 | "&" => new BitwiseAndExpression(lhs, rhs), 51 | "|" => new BitwiseOrExpression(lhs, rhs), 52 | _ => throw new Exception($"Unexpected operator: {op.Value}"), 53 | }; 54 | } 55 | 56 | public override IAstNode Optimize() 57 | { 58 | return this; 59 | } 60 | } 61 | 62 | class PlusExpression : BinaryNumericExpression 63 | { 64 | public PlusExpression(IAstNode lhs, IAstNode rhs) : base(lhs, rhs) 65 | { 66 | } 67 | 68 | protected override string OperatorText => "+"; 69 | 70 | protected override double ComputeNumericValue(double lhs, double rhs) 71 | { 72 | return lhs + rhs; 73 | } 74 | } 75 | 76 | class MinusExpression : BinaryNumericExpression 77 | { 78 | public MinusExpression(IAstNode lhs, IAstNode rhs) : base(lhs, rhs) 79 | { 80 | } 81 | 82 | protected override string OperatorText => "-"; 83 | 84 | protected override double ComputeNumericValue(double lhs, double rhs) 85 | { 86 | return lhs - rhs; 87 | } 88 | } 89 | 90 | class TimesExpression : BinaryNumericExpression 91 | { 92 | public TimesExpression(IAstNode lhs, IAstNode rhs) : base(lhs, rhs) 93 | { 94 | } 95 | 96 | protected override string OperatorText => "*"; 97 | 98 | protected override double ComputeNumericValue(double lhs, double rhs) 99 | { 100 | return lhs * rhs; 101 | } 102 | } 103 | 104 | class DivideExpression : BinaryNumericExpression 105 | { 106 | public DivideExpression(IAstNode lhs, IAstNode rhs) : base(lhs, rhs) 107 | { 108 | } 109 | 110 | protected override string OperatorText => "/"; 111 | 112 | protected override double ComputeNumericValue(double lhs, double rhs) 113 | { 114 | return lhs / rhs; 115 | } 116 | } 117 | 118 | class BitwiseOrExpression : BinaryNumericExpression 119 | { 120 | public BitwiseOrExpression(IAstNode lhs, IAstNode rhs) : base(lhs, rhs) 121 | { 122 | } 123 | 124 | protected override string OperatorText => "|"; 125 | 126 | protected override double ComputeNumericValue(double lhs, double rhs) 127 | { 128 | return Convert.ToDouble(Convert.ToInt32(lhs) | Convert.ToInt32(rhs)); 129 | } 130 | } 131 | 132 | class BitwiseAndExpression : BinaryNumericExpression 133 | { 134 | public BitwiseAndExpression(IAstNode lhs, IAstNode rhs) : base(lhs, rhs) 135 | { 136 | } 137 | 138 | protected override string OperatorText => "&"; 139 | 140 | protected override double ComputeNumericValue(double lhs, double rhs) 141 | { 142 | return Convert.ToDouble(Convert.ToInt32(lhs) & Convert.ToInt32(rhs)); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /BravoLights/UI/LightsWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Windows; 4 | using System.Windows.Controls; 5 | using BravoLights.Common; 6 | 7 | namespace BravoLights.UI 8 | { 9 | /// 10 | /// Interaction logic for LightsWindow.xaml 11 | /// 12 | public partial class LightsWindow : Window 13 | { 14 | public LightsWindow() 15 | { 16 | InitializeComponent(); 17 | } 18 | 19 | protected override void OnInitialized(EventArgs e) 20 | { 21 | base.OnInitialized(e); 22 | Title = $"{ProgramInfo.ProductNameAndVersion} - Lights Monitor"; 23 | } 24 | 25 | private MainViewModel viewModel; 26 | 27 | public MainViewModel ViewModel 28 | { 29 | get 30 | { 31 | return (MainViewModel)DataContext; 32 | } 33 | set 34 | { 35 | viewModel = value; 36 | viewModel.PropertyChanged += ViewModel_PropertyChanged; 37 | DataContext = new CombinedDataContext 38 | { 39 | MainState = value, 40 | ExpressionAndVariablesViewModel = eavVM 41 | }; 42 | } 43 | } 44 | 45 | private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) 46 | { 47 | if (e.PropertyName == "LightExpressions") 48 | { 49 | UpdateMonitor(); 50 | } 51 | } 52 | 53 | private string monitoredLight = ""; 54 | 55 | private void Checkbox_Checked(object sender, RoutedEventArgs e) 56 | { 57 | monitoredLight = ((Control)e.OriginalSource).Tag as string; 58 | 59 | UpdateMonitor(); 60 | } 61 | 62 | private readonly ExpressionAndVariablesViewModel eavVM = new(); 63 | 64 | private void UpdateMonitor() 65 | { 66 | LightExpression lightExpression = null; 67 | 68 | if (monitoredLight != null && viewModel != null) 69 | { 70 | viewModel.LightExpressions.TryGetValue(monitoredLight, out lightExpression); 71 | } 72 | eavVM.Monitor(lightExpression); 73 | } 74 | 75 | private void Window_Closing(object sender, CancelEventArgs e) 76 | { 77 | // Hide instead of close 78 | e.Cancel = true; 79 | Hide(); 80 | 81 | // Unsubscribe whilst invisible 82 | UpdateMonitor(); 83 | } 84 | } 85 | 86 | class CombinedDataContext 87 | { 88 | private MainViewModel mainState; 89 | public MainViewModel MainState 90 | { 91 | get { return mainState; } 92 | set { mainState = value; } 93 | } 94 | 95 | private ExpressionAndVariablesViewModel eavVM; 96 | 97 | public ExpressionAndVariablesViewModel ExpressionAndVariablesViewModel 98 | { 99 | get { return eavVM; } 100 | set { eavVM = value; } 101 | } 102 | } 103 | 104 | public class VariableState : ViewModelBase 105 | { 106 | public VariableState() 107 | { 108 | ValueText = "No value received yet"; 109 | IsError = true; 110 | } 111 | 112 | public string Name { get; set; } 113 | 114 | private string val; 115 | public string ValueText 116 | { 117 | get { return val; } 118 | private set 119 | { 120 | SetProperty(ref val, value); 121 | } 122 | } 123 | 124 | private bool isError; 125 | public bool IsError 126 | { 127 | get { return isError; } 128 | private set 129 | { 130 | SetProperty(ref isError, value); 131 | } 132 | } 133 | 134 | private object valueObject; 135 | public object Value 136 | { 137 | get { return valueObject; } 138 | set 139 | { 140 | SetProperty(ref valueObject, value); 141 | 142 | var exception = value as Exception; 143 | 144 | IsError = exception != null; 145 | 146 | if (exception != null) 147 | { 148 | ValueText = exception.Message; 149 | } 150 | else 151 | { 152 | ValueText = value.ToString(); 153 | } 154 | } 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /BravoLights/IniFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace BravoLights 7 | { 8 | class IniFile 9 | { 10 | private Dictionary sections = new(); 11 | 12 | private static readonly Regex sectionRegex = new("\\[(.*)\\]"); 13 | private static readonly Regex keyValueRegex = new("^(.*?)\\s*=\\s*(.*)$"); 14 | 15 | public void LoadConfigFromFile(string filename) 16 | { 17 | try 18 | { 19 | var lines = File.ReadAllLines(filename); 20 | LoadConfigLines(lines); 21 | } 22 | catch 23 | { 24 | throw new Exception($"Failed to read {filename}"); 25 | } 26 | } 27 | 28 | public void LoadConfigLines(string[] configLines) 29 | { 30 | ICollection newSections = new List(); 31 | 32 | var sections = new Dictionary(); 33 | 34 | var continuation = ""; 35 | 36 | foreach (var rawLine in configLines) 37 | { 38 | var line = rawLine.Trim(); 39 | 40 | if (line.StartsWith(";")) 41 | { 42 | // Comment 43 | continue; 44 | } 45 | 46 | if (line.EndsWith("\\")) 47 | { 48 | // Line has continuation marker, to join it to the next line 49 | var trimmedLineWithoutMarker = line.Substring(0, line.Length - 1).Trim(); 50 | continuation = (continuation + " " + trimmedLineWithoutMarker).Trim(); 51 | continue; 52 | } 53 | 54 | line = (continuation + " " + line).Trim(); 55 | 56 | if (line.Length == 0) 57 | { 58 | // Empty line 59 | continue; 60 | } 61 | 62 | var sectionMatch = sectionRegex.Match(line); 63 | if (sectionMatch.Success) 64 | { 65 | var sectionNamesString = sectionMatch.Groups[1].Value; 66 | var sectionNames = sectionNamesString.Split(','); 67 | newSections.Clear(); 68 | 69 | foreach (var sectionName in sectionNames) 70 | { 71 | var trimmedSectionName = sectionName.Trim(); 72 | if (!sections.TryGetValue(trimmedSectionName, out IniSection section)) 73 | { 74 | section = new IniSection(); 75 | sections[trimmedSectionName] = section; 76 | } 77 | newSections.Add(section); 78 | } 79 | continue; 80 | } 81 | 82 | 83 | var keyValueMatch = keyValueRegex.Match(line); 84 | if (keyValueMatch.Success) 85 | { 86 | var key = keyValueMatch.Groups[1].Value; 87 | var value = keyValueMatch.Groups[2].Value; 88 | foreach (var section in newSections) 89 | { 90 | section.Set(key, value); 91 | } 92 | } 93 | } 94 | 95 | this.sections = sections; 96 | } 97 | 98 | public bool HasSection(string sectionName) 99 | { 100 | if (sections.TryGetValue(sectionName, out var section)) 101 | { 102 | return !section.IsEmpty; 103 | } 104 | return false; 105 | } 106 | 107 | public string GetValueOrNull(string sectionName, string key) 108 | { 109 | if (sections.TryGetValue(sectionName, out var section)) 110 | { 111 | if (section.TryGetValue(key, out var value)) 112 | { 113 | return value; 114 | } 115 | } 116 | 117 | return null; 118 | } 119 | } 120 | 121 | class IniSection 122 | { 123 | private readonly Dictionary storage = new(); 124 | 125 | public IniSection() 126 | { 127 | } 128 | 129 | public bool IsEmpty 130 | { 131 | get { return storage.Count == 0; } 132 | } 133 | 134 | public void Set(string key, string value) 135 | { 136 | storage[key] = value; 137 | } 138 | 139 | public bool TryGetValue(string key, out string value) 140 | { 141 | return storage.TryGetValue(key, out value); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /BravoLights.Common/Ast/ComparisonExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using sly.lexer; 3 | 4 | namespace BravoLights.Common.Ast 5 | { 6 | /// 7 | /// A binary expression such as 'X < Y' or 'X == Y', which produces a boolean from two other numbers and an operator. 8 | /// 9 | abstract class ComparisonExpression : BinaryExpression 10 | { 11 | protected ComparisonExpression(IAstNode lhs, IAstNode rhs) : base(lhs, rhs) 12 | { 13 | } 14 | 15 | protected abstract bool ComputeComparisonValue(double lhs, double rhs); 16 | 17 | protected override object ComputeValue(object lhsValue, object rhsValue) 18 | { 19 | if (lhsValue is Exception) 20 | { 21 | return lhsValue; 22 | } 23 | if (rhsValue is Exception) 24 | { 25 | return rhsValue; 26 | } 27 | var lhs = Convert.ToDouble(lhsValue); 28 | var rhs = Convert.ToDouble(rhsValue); 29 | return ComputeComparisonValue(lhs, rhs); 30 | } 31 | 32 | public static ComparisonExpression Create(IAstNode lhs, Token token, IAstNode rhs) 33 | { 34 | return token.Value switch 35 | { 36 | "<" => new LtComparison(lhs, rhs), 37 | "<=" => new LeqComparison(lhs, rhs), 38 | "==" => new EqComparison(lhs, rhs), 39 | "!=" or "<>" => new NeqComparison(lhs, rhs), 40 | ">=" => new GeqComparison(lhs, rhs), 41 | ">" => new GtComparison(lhs, rhs), 42 | _ => throw new Exception($"Unexpected operator {token.Value}"), 43 | }; 44 | } 45 | 46 | public override IAstNode Optimize() 47 | { 48 | return this; 49 | } 50 | } 51 | 52 | class LtComparison : ComparisonExpression 53 | { 54 | public LtComparison(IAstNode lhs, IAstNode rhs) : base(lhs, rhs) 55 | { 56 | } 57 | 58 | protected override bool ComputeComparisonValue(double lhs, double rhs) 59 | { 60 | return lhs < rhs; 61 | } 62 | protected override string OperatorText => "<"; 63 | } 64 | 65 | class LeqComparison : ComparisonExpression 66 | { 67 | public LeqComparison(IAstNode lhs, IAstNode rhs) : base(lhs, rhs) 68 | { 69 | } 70 | 71 | protected override bool ComputeComparisonValue(double lhs, double rhs) 72 | { 73 | return lhs <= rhs; 74 | } 75 | protected override string OperatorText => "<="; 76 | } 77 | 78 | class EqComparison : ComparisonExpression 79 | { 80 | public EqComparison(IAstNode lhs, IAstNode rhs) : base(lhs, rhs) 81 | { 82 | } 83 | 84 | protected override bool ComputeComparisonValue(double lhs, double rhs) 85 | { 86 | return lhs == rhs; 87 | } 88 | protected override string OperatorText => "=="; 89 | } 90 | 91 | class NeqComparison : ComparisonExpression 92 | { 93 | public NeqComparison(IAstNode lhs, IAstNode rhs) : base(lhs, rhs) 94 | { 95 | } 96 | 97 | protected override bool ComputeComparisonValue(double lhs, double rhs) 98 | { 99 | return lhs != rhs; 100 | } 101 | protected override string OperatorText => "!="; 102 | } 103 | 104 | class GeqComparison : ComparisonExpression 105 | { 106 | public GeqComparison(IAstNode lhs, IAstNode rhs) : base(lhs, rhs) 107 | { 108 | } 109 | 110 | protected override bool ComputeComparisonValue(double lhs, double rhs) 111 | { 112 | return lhs >= rhs; 113 | } 114 | protected override string OperatorText => ">="; 115 | } 116 | 117 | class GtComparison : ComparisonExpression 118 | { 119 | public GtComparison(IAstNode lhs, IAstNode rhs) : base(lhs, rhs) 120 | { 121 | } 122 | 123 | protected override bool ComputeComparisonValue(double lhs, double rhs) 124 | { 125 | return lhs > rhs; 126 | } 127 | protected override string OperatorText => ">"; 128 | } 129 | 130 | class UnaryMinusExpression : UnaryExpression 131 | { 132 | public UnaryMinusExpression(IAstNode child) : base(child) 133 | { 134 | } 135 | 136 | protected override double ComputeValue(double child) 137 | { 138 | return -child; 139 | } 140 | 141 | public override string ToString() 142 | { 143 | return $"-{Child}"; 144 | } 145 | 146 | public override IAstNode Optimize() 147 | { 148 | return new UnaryMinusExpression(Child.Optimize()); 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /BravoLights.Tests/ConfigTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace BravoLights.Tests 4 | { 5 | public class ConfigTests 6 | { 7 | private static IConfig CreateConfig(string file) 8 | { 9 | var config = new FileConfig("Config.ini"); 10 | config.LoadConfig(file.Split('\r', '\n')); 11 | return config; 12 | } 13 | 14 | [Fact] 15 | public void IgnoresComments() 16 | { 17 | var config = CreateConfig(@" 18 | ;This is a comment 19 | [Aircraft.Some_Aircraft] 20 | ;This is also a comment 21 | REV = A:AUTOPILOT BACKCOURSE HOLD, bool == 1 22 | ;Another comment 23 | "); 24 | Assert.Equal("A:AUTOPILOT BACKCOURSE HOLD, bool == 1", config.GetConfig("Some_Aircraft", "REV")); 25 | } 26 | 27 | [Fact] 28 | public void ReturnsSpecificConfigurationsForMatchingAircraft() 29 | { 30 | var config = CreateConfig(@" 31 | [Aircraft.Aircraft1] 32 | REV = A:AUTOPILOT BACKCOURSE HOLD, bool == 1 33 | 34 | [Aircraft.Aircraft2] 35 | REV = A:AUTOPILOT BACKCOURSE HOLD, bool == 2 36 | "); 37 | Assert.Equal("A:AUTOPILOT BACKCOURSE HOLD, bool == 1", config.GetConfig("Aircraft1", "REV")); 38 | Assert.Equal("A:AUTOPILOT BACKCOURSE HOLD, bool == 2", config.GetConfig("Aircraft2", "REV")); 39 | } 40 | 41 | [Fact] 42 | public void MergesMultipleSectionsForASingleAircraft() 43 | { 44 | var config = CreateConfig(@" 45 | [Aircraft.Aircraft1] 46 | LowFuelPressure = 1 < 2 47 | [Aircraft.Aircraft2] 48 | LowFuelPressure = 2 < 3 49 | [Aircraft.Aircraft1] 50 | HDG = 3 > 4 51 | "); 52 | Assert.Equal("1 < 2", config.GetConfig("Aircraft1", "LowFuelPressure")); 53 | Assert.Equal("2 < 3", config.GetConfig("Aircraft2", "LowFuelPressure")); 54 | Assert.Equal("3 > 4", config.GetConfig("Aircraft1", "HDG")); 55 | 56 | } 57 | [Fact] 58 | public void AllowsAircraftToOverrideOrFallbackToDefault() 59 | { 60 | var config = CreateConfig(@" 61 | [Default] 62 | LowVolts = A:ELECTRICAL MAIN BUS VOLTAGE:1, volts <= 28 63 | Vacuum = A:PARTIAL PANEL VACUUM, Enum == 1 64 | 65 | [Aircraft.Aircraft1] 66 | LowVolts = A:ELECTRICAL MAIN BUS VOLTAGE:3, volts <= 26 67 | "); 68 | Assert.Equal("A:ELECTRICAL MAIN BUS VOLTAGE:3, volts <= 26", config.GetConfig("Aircraft1", "LowVolts")); 69 | Assert.Equal("A:ELECTRICAL MAIN BUS VOLTAGE:1, volts <= 28", config.GetConfig("Aircraft2", "LowVolts")); 70 | Assert.Equal("A:PARTIAL PANEL VACUUM, Enum == 1", config.GetConfig("Aircraft1", "Vacuum")); 71 | } 72 | 73 | [Fact] 74 | public void AllowsCommaSeparatedAircraftAndSpecific() 75 | { 76 | var config = CreateConfig(@" 77 | [Default] 78 | LowVolts = A:ELECTRICAL MAIN BUS VOLTAGE:1, volts <= 28 79 | Vacuum = A:PARTIAL PANEL VACUUM, Enum == 1 80 | LowFuelPressure = OFF 81 | 82 | [Aircraft.Aircraft1, Aircraft.Aircraft2] 83 | LowVolts = A:ELECTRICAL MAIN BUS VOLTAGE:3, volts <= 26 84 | 85 | [Aircraft.Aircraft1] 86 | LowFuelPressure = A:GENERAL ENG FUEL PRESSURE:1, psf < 65 87 | "); 88 | Assert.Equal("A:ELECTRICAL MAIN BUS VOLTAGE:3, volts <= 26", config.GetConfig("Aircraft1", "LowVolts")); 89 | Assert.Equal("A:ELECTRICAL MAIN BUS VOLTAGE:3, volts <= 26", config.GetConfig("Aircraft2", "LowVolts")); 90 | Assert.Equal("A:GENERAL ENG FUEL PRESSURE:1, psf < 65", config.GetConfig("Aircraft1", "LowFuelPressure")); 91 | Assert.Equal("OFF", config.GetConfig("Aircraft2", "LowFuelPressure")); 92 | } 93 | 94 | [Fact] 95 | public void ReturnsNullForMissingConfiguration() 96 | { 97 | var config = CreateConfig(@" 98 | [Default] 99 | LowFuelPressure = OFF 100 | "); 101 | Assert.Equal("OFF", config.GetConfig("Aircraft1", "LowFuelPressure")); 102 | Assert.Null(config.GetConfig("Aircraft1", "EngineFire")); 103 | } 104 | 105 | [Fact] 106 | public void SupportsBackslashLineContinuationCharacters() 107 | { 108 | // We'll insert some extra spaces in to check that continuations work even if there's trailing whitespace, 109 | // and that whitespace is all trimmed out (and re-inserted) correctly. 110 | var extraSpaces = " "; 111 | 112 | var config = CreateConfig($@" 113 | [Default] 114 | LowFuelPressure = {extraSpaces}\{extraSpaces} 115 | L:I_OH_FUEL_CENTER_1_L == 1 \{extraSpaces} 116 | OR L:I_OH_FUEL_CENTER_1_U == 1 {extraSpaces}\ 117 | {extraSpaces} OR L:I_OH_FUEL_CENTER_2_L == 1 \ 118 | OR L:I_OH_FUEL_CENTER_2_U == 1{extraSpaces}"); 119 | 120 | Assert.Equal("L:I_OH_FUEL_CENTER_1_L == 1 OR L:I_OH_FUEL_CENTER_1_U == 1 OR L:I_OH_FUEL_CENTER_2_L == 1 OR L:I_OH_FUEL_CENTER_2_U == 1", config.GetConfig("Aircraft", "LowFuelPressure")); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /DCSBravoLights/DcsDefinitions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text.Json; 5 | 6 | namespace DCSBravoLights 7 | { 8 | class DcsDefinitions : IEnumerable 9 | { 10 | private readonly Dictionary definitions = new(); 11 | private readonly ISet[] definitionsByAddress = new ISet[65536]; 12 | 13 | public DcsDefinitions() 14 | { 15 | LoadDefinitionFile("MetadataStart"); 16 | LoadDefinitionFile("MetadataEnd"); 17 | } 18 | 19 | public void LoadDefinitionFile(string aliasName) 20 | { 21 | var commonDataJson = File.ReadAllText(Path.Join(DcsBiosState.DcsJsonDocFolder, aliasName + ".json")); 22 | 23 | var commonData = JsonDocument.Parse(commonDataJson); 24 | foreach (var rootElem in commonData.RootElement.EnumerateObject()) 25 | { 26 | var name = rootElem.Name; 27 | foreach (var nextElem in rootElem.Value.EnumerateObject()) 28 | { 29 | var name2 = nextElem.Name; 30 | var val = nextElem.Value; 31 | 32 | var outputs = val.GetProperty("outputs"); 33 | 34 | var category = val.GetProperty("category").GetString(); 35 | var identifier = val.GetProperty("identifier").GetString(); 36 | string description; 37 | try 38 | { 39 | description = val.GetProperty("description").GetString(); 40 | } 41 | catch (KeyNotFoundException) 42 | { 43 | description = string.Empty; 44 | } 45 | 46 | foreach (var output in outputs.EnumerateArray()) 47 | { 48 | if (outputs.GetArrayLength() > 1) 49 | { 50 | description += " - " + output.GetProperty("description").GetString(); 51 | } 52 | 53 | var address = output.GetProperty("address").GetUInt16(); 54 | var suffix = output.GetProperty("suffix").GetString(); 55 | 56 | DataDefinition def; 57 | 58 | if (output.GetProperty("type").GetString() == "integer") 59 | { 60 | def = new IntegerDefinition 61 | { 62 | VariableName = new VariableName(category, identifier), 63 | Description = description, 64 | Address = address, 65 | Suffix = suffix, 66 | Mask = output.GetProperty("mask").GetUInt16(), 67 | MaxValue = output.GetProperty("max_value").GetUInt16(), 68 | ShiftBy = output.GetProperty("shift_by").GetUInt16() 69 | }; 70 | } 71 | else 72 | { 73 | var maxLength = output.GetProperty("max_length").GetUInt16(); 74 | def = new StringDefinition 75 | { 76 | VariableName = new VariableName(category, identifier), 77 | Description = description, 78 | Address = address, 79 | Suffix = suffix, 80 | MaxLength = maxLength 81 | }; 82 | } 83 | 84 | AddDefinition(def); 85 | } 86 | } 87 | } 88 | } 89 | 90 | public DataDefinition GetDataDefinition(VariableName variableName) 91 | { 92 | if (definitions.TryGetValue(variableName, out var def)) 93 | { 94 | return def; 95 | } else 96 | { 97 | return null; 98 | } 99 | } 100 | 101 | private void AddDefinition(DataDefinition def) 102 | { 103 | definitions[def.VariableName] = def; 104 | 105 | for (var i = 0; i < def.Length; i++) 106 | { 107 | var set = definitionsByAddress[i + def.Address]; 108 | if (set == null) 109 | { 110 | set = new HashSet(); 111 | definitionsByAddress[i + def.Address] = set; 112 | } 113 | set.Add(def); 114 | } 115 | } 116 | 117 | public IEnumerator GetEnumerator() 118 | { 119 | return definitions.Values.GetEnumerator(); 120 | } 121 | 122 | IEnumerator IEnumerable.GetEnumerator() 123 | { 124 | return this.GetEnumerator(); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /BravoLights.Common/ExpressionParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using BravoLights.Common.Ast; 4 | using sly.lexer; 5 | using sly.parser; 6 | using sly.parser.generator; 7 | 8 | namespace BravoLights.Common 9 | { 10 | #pragma warning disable CA1822 // Mark members as static 11 | public abstract class ExpressionParserBase 12 | { 13 | [Operand] 14 | [Production("logical_literal: OFF")] 15 | [Production("logical_literal: ON")] 16 | public IAstNode LiteralBool(Token token) 17 | { 18 | return token.Value == "ON" ? LiteralBoolNode.On : LiteralBoolNode.Off; 19 | } 20 | 21 | [Operand] 22 | [Production("primary: HEX_NUMBER")] 23 | public IAstNode NumericExpressionFromLiteralNumber(Token offsetToken) 24 | { 25 | var text = offsetToken.Value; 26 | var num = text[2..]; 27 | var value = int.Parse(num, NumberStyles.HexNumber); 28 | return new LiteralNumericNode(value); 29 | } 30 | 31 | [Operand] 32 | [Production("primary: DECIMAL_NUMBER")] 33 | public IAstNode NumericExpressionFromDecimalNumber(Token offsetToken) 34 | { 35 | var text = offsetToken.Value; 36 | var value = double.Parse(text, CultureInfo.InvariantCulture); 37 | return new LiteralNumericNode(value); 38 | } 39 | 40 | [Infix((int)ExpressionToken.PLUS, Associativity.Left, 14)] 41 | [Infix((int)ExpressionToken.MINUS, Associativity.Left, 14)] 42 | [Infix((int)ExpressionToken.TIMES, Associativity.Left, 15)] 43 | [Infix((int)ExpressionToken.DIVIDE, Associativity.Left, 15)] 44 | [Infix((int)ExpressionToken.BITWISE_AND, Associativity.Left, 10)] 45 | [Infix((int)ExpressionToken.BITWISE_OR, Associativity.Left, 8)] 46 | public IAstNode NumberExpression(IAstNode lhs, Token token, IAstNode rhs) 47 | { 48 | return BinaryNumericExpression.Create(lhs, token, rhs); 49 | } 50 | 51 | [Infix((int)ExpressionToken.LOGICAL_AND, Associativity.Left, 7)] 52 | [Infix((int)ExpressionToken.LOGICAL_OR, Associativity.Left, 6)] 53 | public IAstNode LogicalExpression(IAstNode lhs, Token token, IAstNode rhs) 54 | { 55 | return BooleanLogicalExpression.Create(lhs, token, rhs); 56 | } 57 | 58 | [Prefix((int)ExpressionToken.MINUS, Associativity.Right, 17)] 59 | public IAstNode NumericExpression(Token _, IAstNode child) 60 | { 61 | return new UnaryMinusExpression(child); 62 | } 63 | 64 | // We want NOT to to bind tighter than AND/OR but looser than numeric comparison operations 65 | [Prefix((int)ExpressionToken.NOT, Associativity.Right, 11)] 66 | public IAstNode LogicalExpression(Token _, IAstNode child) 67 | { 68 | return new NotExpression(child); 69 | } 70 | 71 | 72 | [Infix((int)ExpressionToken.COMPARISON, Associativity.Left, 12)] 73 | public IAstNode Comparison(IAstNode lhs, Token token, IAstNode rhs) 74 | { 75 | return ComparisonExpression.Create(lhs, token, rhs); 76 | } 77 | 78 | private static Parser cachedParser; 79 | 80 | public static IAstNode Parse(string expression) where T : ExpressionParserBase, new() 81 | { 82 | if (cachedParser == null) 83 | { 84 | var startingRule = $"{typeof(T).Name}_expressions"; 85 | var parserInstance = new T(); 86 | var builder = new ParserBuilder(); 87 | var parser = builder.BuildParser(parserInstance, ParserType.EBNF_LL_RECURSIVE_DESCENT, startingRule); 88 | if (parser.IsError) 89 | { 90 | throw new Exception($"Could not create parser. BNF is not valid. {parser.Errors[0]}"); 91 | } 92 | cachedParser = parser.Result; 93 | } 94 | 95 | // To simplify an ambiguous lexer which would result from having both && and & as well as || and |, we'll 96 | // simplify the incoming expression by turning && into AND and || into OR: 97 | expression = expression.Replace("&&", " AND "); 98 | expression = expression.Replace("||", " OR "); 99 | 100 | // Similarly for <> which maps to != 101 | expression = expression.Replace("<>", "!="); 102 | 103 | try 104 | { 105 | var parseResult = cachedParser.Parse(expression); 106 | if (parseResult.IsError) 107 | { 108 | return new ErrorNode(parseResult.Errors[0].ErrorMessage); 109 | } 110 | return parseResult.Result; 111 | } catch (Exception ex) 112 | { 113 | return new ErrorNode(ex.Message); 114 | } 115 | } 116 | } 117 | #pragma warning restore CA1822 // Mark members as static 118 | } 119 | 120 | -------------------------------------------------------------------------------- /BravoLights/MainViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using BravoLights.Common; 4 | using BravoLights.Connections; 5 | using BravoLights.UI; 6 | 7 | namespace BravoLights 8 | { 9 | public class MainViewModel : ViewModelBase, ILightsState 10 | { 11 | private readonly ISet litLights = new HashSet(); 12 | 13 | private Dictionary lightExpressions = new(); 14 | 15 | public IReadOnlyDictionary LightExpressions 16 | { 17 | get { return lightExpressions; } 18 | } 19 | 20 | public void RegisterLights(IEnumerable lights) 21 | { 22 | var newExpressions = new Dictionary(); 23 | 24 | foreach (var light in lights) 25 | { 26 | var lightName = light.LightName; 27 | 28 | newExpressions[lightName] = light; 29 | light.ValueChanged += ExpressionValueChanged; 30 | } 31 | 32 | foreach (var light in lightExpressions) 33 | { 34 | var expression = light.Value; 35 | if (expression != null) 36 | { 37 | expression.ValueChanged -= ExpressionValueChanged; 38 | } 39 | } 40 | 41 | SetProperty(ref lightExpressions, newExpressions, nameof(LightExpressions)); 42 | } 43 | 44 | private void ExpressionValueChanged(object sender, ValueChangedEventArgs e) 45 | { 46 | var lightExpression = (LightExpression)sender; 47 | var lightName = lightExpression.LightName; 48 | 49 | 50 | var lit = e.NewValue is not Exception && (bool)e.NewValue; 51 | 52 | bool changed; 53 | if (lit) 54 | { 55 | changed = litLights.Add(lightName); 56 | } else 57 | { 58 | changed = litLights.Remove(lightName); 59 | } 60 | 61 | if (changed) 62 | { 63 | RaisePropertyChanged(lightName); 64 | } 65 | } 66 | 67 | private string aircraft = "General"; 68 | public string Aircraft 69 | { 70 | get { return aircraft; } 71 | set 72 | { 73 | SetProperty(ref aircraft, value); 74 | } 75 | } 76 | 77 | private SimState simState = SimState.SimExited; 78 | public SimState SimState 79 | { 80 | get { return simState; } 81 | set { SetProperty(ref simState, value); } 82 | } 83 | 84 | public IEnumerable LitLights 85 | { 86 | get { return litLights; } 87 | } 88 | 89 | public bool IsLit(string lightName) 90 | { 91 | return litLights.Contains(lightName); 92 | } 93 | public bool HDG { get { return IsLit(LightNames.HDG); } } 94 | public bool NAV { get { return IsLit(LightNames.NAV); } } 95 | public bool APR { get { return IsLit(LightNames.APR); } } 96 | public bool REV { get { return IsLit(LightNames.REV); } } 97 | public bool ALT { get { return IsLit(LightNames.ALT); } } 98 | public bool VS { get { return IsLit(LightNames.VS); } } 99 | public bool IAS { get { return IsLit(LightNames.IAS); } } 100 | public bool AUTOPILOT { get { return IsLit(LightNames.AUTOPILOT); } } 101 | 102 | public bool MasterWarning { get { return IsLit(LightNames.MasterWarning); } } 103 | public bool EngineFire { get { return IsLit(LightNames.EngineFire); } } 104 | public bool LowOilPressure { get { return IsLit(LightNames.LowOilPressure); } } 105 | public bool LowFuelPressure { get { return IsLit(LightNames.LowFuelPressure); } } 106 | public bool AntiIce { get { return IsLit(LightNames.AntiIce); } } 107 | public bool StarterEngaged { get { return IsLit(LightNames.StarterEngaged); } } 108 | public bool APU { get { return IsLit(LightNames.APU); } } 109 | public bool MasterCaution { get { return IsLit(LightNames.MasterCaution); } } 110 | public bool Vacuum { get { return IsLit(LightNames.Vacuum); } } 111 | public bool LowHydPressure { get { return IsLit(LightNames.LowHydPressure); } } 112 | public bool AuxFuelPump { get { return IsLit(LightNames.AuxFuelPump); } } 113 | public bool ParkingBrake { get { return IsLit(LightNames.ParkingBrake); } } 114 | public bool LowVolts { get { return IsLit(LightNames.LowVolts); } } 115 | public bool Door { get { return IsLit(LightNames.Door); } } 116 | 117 | public bool GearCRed { get { return IsLit(LightNames.GearCRed); } } 118 | public bool GearCGreen { get { return IsLit(LightNames.GearCGreen); } } 119 | public bool GearLRed { get { return IsLit(LightNames.GearLRed); } } 120 | public bool GearLGreen { get { return IsLit(LightNames.GearLGreen); } } 121 | public bool GearRRed { get { return IsLit(LightNames.GearRRed); } } 122 | public bool GearRGreen { get { return IsLit(LightNames.GearRGreen); } } 123 | 124 | 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /BravoLights/Installation/FlightSimulatorPaths.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text.RegularExpressions; 4 | using System.Windows.Forms; 5 | 6 | namespace BravoLights.Installation 7 | { 8 | static class FlightSimulatorPaths 9 | { 10 | /// 11 | /// Gets the location of the main Flight Simulator installation. 12 | /// 13 | public static string FlightSimulatorPath 14 | { 15 | get 16 | { 17 | var localAppData = (UnitTestRoot == null) ? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) : Path.Join(UnitTestRoot, "LOCALAPPDATA"); 18 | var windowsStoreLocation = Path.Join(localAppData, "Packages", "Microsoft.FlightSimulator_8wekyb3d8bbwe", "LocalCache"); 19 | 20 | var appData = (UnitTestRoot == null) ? Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) : Path.Join(UnitTestRoot, "APPDATA"); 21 | var steamLocation = Path.Join(appData, "Microsoft Flight Simulator"); 22 | 23 | var pathsToTry = new[] 24 | { 25 | windowsStoreLocation, 26 | steamLocation 27 | }; 28 | 29 | foreach (var path in pathsToTry) 30 | { 31 | if (File.Exists(Path.Join(path, "FlightSimulator.CFG")) || File.Exists(Path.Join(path, "UserCfg.opt"))) 32 | { 33 | return path; 34 | } 35 | } 36 | 37 | var pathsTried = String.Join(", ", pathsToTry); 38 | throw new Exception($"Could not locate main Flight Simulator path. Paths tried: {pathsTried}"); 39 | } 40 | } 41 | 42 | /// 43 | /// Gets the path that the BBL .exe is running from. 44 | /// 45 | public static string BetterBravoLightsPath 46 | { 47 | get 48 | { 49 | if (UnitTestRoot == null) 50 | { 51 | return Application.StartupPath; 52 | } 53 | 54 | return Path.Join(Application.StartupPath, "..", "..", "..", "..", "BravoLights", "bin", "Debug", "net5.0-windows"); 55 | } 56 | } 57 | 58 | /// 59 | /// Gets the location of the MSFS exe.xml file, which may not actually exist yet. 60 | /// 61 | public static string ExeXmlPath 62 | { 63 | get 64 | { 65 | return Path.Join(FlightSimulatorPath, "exe.xml"); 66 | } 67 | } 68 | 69 | /// 70 | /// Gets the location of the UserCfg.opt file. 71 | /// 72 | private static string UserCfgOptPath 73 | { 74 | get 75 | { 76 | return Path.Join(FlightSimulatorPath, "UserCfg.opt"); 77 | } 78 | } 79 | 80 | private static readonly Regex installedPackagesPathRegex = new("^InstalledPackagesPath \"(.*)\""); 81 | 82 | /// 83 | /// Gets the location of the Official and Community directories. 84 | /// 85 | public static string MSFSPackagesPath 86 | { 87 | get 88 | { 89 | var lines = File.ReadAllLines(UserCfgOptPath); 90 | foreach (var line in lines) 91 | { 92 | var match = installedPackagesPathRegex.Match(line); 93 | if (match.Success) 94 | { 95 | return match.Groups[1].Value; 96 | } 97 | } 98 | 99 | throw new Exception("Cannot locate FS packages path"); 100 | } 101 | } 102 | 103 | 104 | /// 105 | /// Gets the location of the Community directory. 106 | /// 107 | public static string CommunityPath 108 | { 109 | get { return Path.Join(MSFSPackagesPath, "Community"); } 110 | } 111 | 112 | 113 | private const string WasmModuleName = "better-bravo-lights-lvar-module"; 114 | 115 | public static string InstalledWasmModulePath 116 | { 117 | get { return Path.Join(CommunityPath, WasmModuleName); } 118 | } 119 | public static string IncludedWasmModulePath 120 | { 121 | get { return Path.Join(BetterBravoLightsPath, "Packages", WasmModuleName); } 122 | } 123 | 124 | public static string BuiltInConfigIniPath 125 | { 126 | get { return Path.Join(BetterBravoLightsPath, "Config.BuiltIn.ini"); } 127 | } 128 | 129 | public static string UserRuntimePath 130 | { 131 | get 132 | { 133 | return Path.Combine(new DirectoryInfo(BetterBravoLightsPath).Parent.FullName); 134 | } 135 | } 136 | public static string UserConfigIniPath 137 | { 138 | get 139 | { 140 | return Path.Combine(UserRuntimePath, "Config.ini"); 141 | } 142 | } 143 | 144 | internal static string UnitTestRoot = null; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /BravoLights.Common/Ast/BooleanLogicalExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using sly.lexer; 3 | 4 | namespace BravoLights.Common.Ast 5 | { 6 | abstract class BooleanLogicalExpression : BinaryExpression 7 | { 8 | protected BooleanLogicalExpression(IAstNode lhs, IAstNode rhs) : base(lhs, rhs) 9 | { 10 | } 11 | 12 | 13 | public static BooleanLogicalExpression Create(IAstNode lhs, Token token, IAstNode rhs) 14 | { 15 | return token.Value switch 16 | { 17 | "&&" or "AND" => new AndExpression(lhs, rhs), 18 | "||" or "OR" => new OrExpression(lhs, rhs), 19 | _ => throw new Exception($"Unexpected operator {token.Value}"), 20 | }; 21 | } 22 | } 23 | 24 | class AndExpression : BooleanLogicalExpression 25 | { 26 | public AndExpression(IAstNode lhs, IAstNode rhs) : base(lhs, rhs) 27 | { 28 | } 29 | 30 | protected override object ComputeValue(object lhsValue, object rhsValue) 31 | { 32 | if (lhsValue is bool lhs) 33 | { 34 | if (lhs == false) 35 | { 36 | return false; 37 | } 38 | } 39 | if (rhsValue is bool rhs) 40 | { 41 | if (rhs == false) 42 | { 43 | return false; 44 | } 45 | } 46 | if (lhsValue is Exception) 47 | { 48 | return lhsValue; 49 | } 50 | if (rhsValue is Exception) 51 | { 52 | return rhsValue; 53 | } 54 | 55 | // Neither side is false, and neither are Exceptions 56 | return true; 57 | } 58 | 59 | public override IAstNode Optimize() 60 | { 61 | var optimizedLeft = Lhs.Optimize(); 62 | var optimizedRight = Rhs.Optimize(); 63 | 64 | if (optimizedLeft is LiteralBoolNode lhsNode) 65 | { 66 | // ON AND A -> A 67 | // OFF AND A -> OFF 68 | return lhsNode.Value ? optimizedRight : LiteralBoolNode.Off; 69 | } 70 | if (optimizedRight is LiteralBoolNode rhsNode) 71 | { 72 | // A AND ON -> A 73 | // A AND OFF -> OFF 74 | return rhsNode.Value ? optimizedLeft : LiteralBoolNode.Off; 75 | } 76 | 77 | return new AndExpression(optimizedLeft, optimizedRight); 78 | } 79 | 80 | protected override string OperatorText => "AND"; 81 | } 82 | 83 | class OrExpression : BooleanLogicalExpression 84 | { 85 | public OrExpression(IAstNode lhs, IAstNode rhs) : base(lhs, rhs) 86 | { 87 | } 88 | 89 | protected override object ComputeValue(object lhsValue, object rhsValue) 90 | { 91 | if (lhsValue is bool lhs) 92 | { 93 | if (lhs == true) 94 | { 95 | return true; 96 | } 97 | } 98 | if (rhsValue is bool rhs) 99 | { 100 | if (rhs == true) 101 | { 102 | return true; 103 | } 104 | } 105 | if (lhsValue is Exception) 106 | { 107 | return lhsValue; 108 | } 109 | if (rhsValue is Exception) 110 | { 111 | return rhsValue; 112 | } 113 | 114 | // Neither side is true, and neither are Exceptions 115 | return false; 116 | } 117 | 118 | public override IAstNode Optimize() 119 | { 120 | var optimizedLeft = Lhs.Optimize(); 121 | var optimizedRight = Rhs.Optimize(); 122 | 123 | if (optimizedLeft is LiteralBoolNode lhsNode) 124 | { 125 | // ON OR A -> ON 126 | // OFF OR A -> A 127 | return lhsNode.Value ? LiteralBoolNode.On : optimizedRight; 128 | } 129 | if (optimizedRight is LiteralBoolNode rhsNode) 130 | { 131 | // A OR ON -> ON 132 | // A OR OFF -> A 133 | return rhsNode.Value ? LiteralBoolNode.On : optimizedLeft; 134 | } 135 | 136 | return new OrExpression(optimizedLeft, optimizedRight); 137 | } 138 | 139 | protected override string OperatorText => "OR"; 140 | } 141 | 142 | class NotExpression : UnaryExpression 143 | { 144 | public NotExpression(IAstNode child) : base(child) 145 | { 146 | } 147 | 148 | protected override bool ComputeValue(bool child) 149 | { 150 | return !child; 151 | } 152 | 153 | public override string ToString() 154 | { 155 | return $"(NOT {Child})"; 156 | } 157 | 158 | public override IAstNode Optimize() 159 | { 160 | if (Child is LiteralBoolNode node) 161 | { 162 | // NOT ON -> OFF 163 | // NOT OFF -> ON 164 | return node.Value ? LiteralBoolNode.Off : LiteralBoolNode.On; 165 | } 166 | 167 | return this; 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /BravoLights.Tests/LVarManagerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using BravoLights.Ast; 4 | using BravoLights.Common; 5 | using BravoLights.Connections; 6 | using Moq; 7 | using Xunit; 8 | 9 | namespace BravoLights.Tests 10 | { 11 | public class LVarManagerTests 12 | { 13 | class LVarData : ILVarData 14 | { 15 | public short ValueCount { get; set; } 16 | 17 | public short[] Ids { get; set; } 18 | 19 | public double[] Values { get; set; } 20 | } 21 | 22 | class TestListener 23 | { 24 | public object LastValue = null; 25 | 26 | public void Listener(object sender, ValueChangedEventArgs e) 27 | { 28 | LastValue = e.NewValue; 29 | if (LastValue is Exception exception) 30 | { 31 | LastValue = exception.Message; 32 | } 33 | } 34 | } 35 | 36 | [Fact] 37 | public void AllowSubscriptionBeforeLVarIsKnown() 38 | { 39 | var mgr = new LVarManager(); 40 | var mockWasmChannel = new Mock(MockBehavior.Strict); 41 | mockWasmChannel.SetupGet(c => c.SimState).Returns(SimState.SimRunning); 42 | 43 | mgr.SetWASMChannel(mockWasmChannel.Object); 44 | 45 | var expression = new LvarExpression { LVarName = "Var2" }; 46 | 47 | var testListener = new TestListener(); 48 | mgr.AddListener(expression, testListener.Listener); 49 | 50 | Assert.Equal("LVar does not exist yet; this aircraft may not support it", testListener.LastValue); 51 | 52 | mockWasmChannel.Setup(c => c.ClearSubscriptions()); 53 | mockWasmChannel.Setup(c => c.Subscribe(1)); // 0-indexed, so Var2 is 1 54 | // After this point if the LVar does appear, we should subscribe to it. 55 | mgr.UpdateLVarList(new List() { "Var1", "Var2", "Var3" }); 56 | mockWasmChannel.VerifyAll(); 57 | 58 | // The LVar is now known but we've not had an actual valuep 59 | Assert.Equal("No value yet received from simulator", testListener.LastValue); 60 | } 61 | 62 | [Fact] 63 | public void ReportsLackOfValueForNewlySubscribedLVar() 64 | { 65 | var mgr = new LVarManager(); 66 | var mockWasmChannel = new Mock(); 67 | mockWasmChannel.SetupGet(c => c.SimState).Returns(SimState.SimRunning); 68 | 69 | mgr.SetWASMChannel(mockWasmChannel.Object); 70 | mgr.UpdateLVarList(new List() { "Var1", "Var2", "Var3" }); 71 | 72 | var expression = new LvarExpression { LVarName = "Var2" }; 73 | 74 | var testListener = new TestListener(); 75 | mgr.AddListener(expression, testListener.Listener); 76 | 77 | Assert.Equal("No value yet received from simulator", testListener.LastValue); 78 | } 79 | 80 | [Fact] 81 | public void ReportsValueForExistingSubscribedLVar() 82 | { 83 | var mgr = new LVarManager(); 84 | var mockWasmChannel = new Mock(); 85 | mockWasmChannel.SetupGet(c => c.SimState).Returns(SimState.SimRunning); 86 | 87 | mgr.SetWASMChannel(mockWasmChannel.Object); 88 | mgr.UpdateLVarList(new List() { "Var1", "Var2", "Var3" }); 89 | 90 | var expression = new LvarExpression { LVarName = "Var2" }; 91 | 92 | // Add existing subscription and deliver a value for it 93 | var mockListener1 = new Mock>(); 94 | mgr.AddListener(expression, mockListener1.Object); 95 | var data = new LVarData { ValueCount = 1, Ids = new short[] { 1 }, Values = new double[] { 42 } }; 96 | mgr.UpdateLVarValues(data); 97 | 98 | // Make a new subscription; it should be given the latest value immediately 99 | var testListener = new TestListener(); 100 | mgr.AddListener(expression, testListener.Listener); 101 | Assert.Equal(42.0, testListener.LastValue); 102 | } 103 | 104 | [Fact] 105 | public void UnsubscribesWhenLastListenerRemovedForAnExpression() 106 | { 107 | var mgr = new LVarManager(); 108 | var mockWasmChannel = new Mock(); 109 | mockWasmChannel.SetupGet(c => c.SimState).Returns(SimState.SimRunning); 110 | 111 | mgr.SetWASMChannel(mockWasmChannel.Object); 112 | mgr.UpdateLVarList(new List() { "Var1", "Var2", "Var3" }); 113 | 114 | var expression = new LvarExpression { LVarName = "Var2" }; 115 | var mockListener1 = new Mock>(); 116 | var mockListener2 = new Mock>(); 117 | mgr.AddListener(expression, mockListener1.Object); 118 | mgr.AddListener(expression, mockListener2.Object); 119 | 120 | mockWasmChannel.Verify(c => c.Unsubscribe(It.IsAny()), Times.Never); 121 | mgr.RemoveListener(expression, mockListener1.Object); 122 | mockWasmChannel.Verify(c => c.Unsubscribe(It.IsAny()), Times.Never); 123 | mgr.RemoveListener(expression, mockListener2.Object); 124 | mockWasmChannel.Verify(c => c.Unsubscribe(1)); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /BravoLights.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31402.337 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BravoLights.Tests", "BravoLights.Tests\BravoLights.Tests.csproj", "{5A84220D-FF6D-4EB6-AD47-47A430AEBFDE}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BravoLights", "BravoLights\BravoLights.csproj", "{6FF6967E-5A00-4844-AA3D-FDECF9E7593E}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BravoLights.Common", "BravoLights.Common\BravoLights.Common.csproj", "{60426C1C-884E-4727-8BAC-794E601CDD15}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DCSBravoLights", "DCSBravoLights\DCSBravoLights.csproj", "{EF0B053E-4214-49B4-94A0-8B165981F822}" 13 | EndProject 14 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WasmModule", "WasmModule\WasmModule.vcxproj", "{A5468B35-BBBD-4C55-97ED-81BFE343B0E4}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7C55C68B-E475-4274-A0DB-37A629D3FE8A}" 17 | ProjectSection(SolutionItems) = preProject 18 | .gitignore = .gitignore 19 | assemble-wasm-for-ide.cmd = assemble-wasm-for-ide.cmd 20 | MSFSWASMProject\PackageDefinitions\better-bravo-lights-lvar-module.xml = MSFSWASMProject\PackageDefinitions\better-bravo-lights-lvar-module.xml 21 | BetterBravoLights.exe.nlog = BetterBravoLights.exe.nlog 22 | MSFSWASMProject\BetterBravoLightsLVars.xml = MSFSWASMProject\BetterBravoLightsLVars.xml 23 | build-and-publish.ps1 = build-and-publish.ps1 24 | LICENSE = LICENSE 25 | README.md = README.md 26 | EndProjectSection 27 | EndProject 28 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "github-issue-templates", "github-issue-templates", "{6134E965-26F3-4242-AE61-16D19412EE15}" 29 | ProjectSection(SolutionItems) = preProject 30 | .github\ISSUE_TEMPLATE\10-bug-report.yaml = .github\ISSUE_TEMPLATE\10-bug-report.yaml 31 | .github\ISSUE_TEMPLATE\20-feature-request.yaml = .github\ISSUE_TEMPLATE\20-feature-request.yaml 32 | .github\ISSUE_TEMPLATE\30-support-question.yaml = .github\ISSUE_TEMPLATE\30-support-question.yaml 33 | .github\ISSUE_TEMPLATE\40-suggested-config.yaml = .github\ISSUE_TEMPLATE\40-suggested-config.yaml 34 | .github\ISSUE_TEMPLATE\50-help-fix-exe-xml.yaml = .github\ISSUE_TEMPLATE\50-help-fix-exe-xml.yaml 35 | .github\ISSUE_TEMPLATE\config.yml = .github\ISSUE_TEMPLATE\config.yml 36 | EndProjectSection 37 | EndProject 38 | Global 39 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 40 | Debug|Any CPU = Debug|Any CPU 41 | Debug|MSFS = Debug|MSFS 42 | Release|Any CPU = Release|Any CPU 43 | Release|MSFS = Release|MSFS 44 | EndGlobalSection 45 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 46 | {5A84220D-FF6D-4EB6-AD47-47A430AEBFDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {5A84220D-FF6D-4EB6-AD47-47A430AEBFDE}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {5A84220D-FF6D-4EB6-AD47-47A430AEBFDE}.Debug|MSFS.ActiveCfg = Debug|Any CPU 49 | {5A84220D-FF6D-4EB6-AD47-47A430AEBFDE}.Debug|MSFS.Build.0 = Debug|Any CPU 50 | {5A84220D-FF6D-4EB6-AD47-47A430AEBFDE}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {5A84220D-FF6D-4EB6-AD47-47A430AEBFDE}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {5A84220D-FF6D-4EB6-AD47-47A430AEBFDE}.Release|MSFS.ActiveCfg = Release|Any CPU 53 | {5A84220D-FF6D-4EB6-AD47-47A430AEBFDE}.Release|MSFS.Build.0 = Release|Any CPU 54 | {6FF6967E-5A00-4844-AA3D-FDECF9E7593E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {6FF6967E-5A00-4844-AA3D-FDECF9E7593E}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {6FF6967E-5A00-4844-AA3D-FDECF9E7593E}.Debug|MSFS.ActiveCfg = Debug|Any CPU 57 | {6FF6967E-5A00-4844-AA3D-FDECF9E7593E}.Debug|MSFS.Build.0 = Debug|Any CPU 58 | {6FF6967E-5A00-4844-AA3D-FDECF9E7593E}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {6FF6967E-5A00-4844-AA3D-FDECF9E7593E}.Release|Any CPU.Build.0 = Release|Any CPU 60 | {6FF6967E-5A00-4844-AA3D-FDECF9E7593E}.Release|MSFS.ActiveCfg = Release|Any CPU 61 | {6FF6967E-5A00-4844-AA3D-FDECF9E7593E}.Release|MSFS.Build.0 = Release|Any CPU 62 | {60426C1C-884E-4727-8BAC-794E601CDD15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 63 | {60426C1C-884E-4727-8BAC-794E601CDD15}.Debug|Any CPU.Build.0 = Debug|Any CPU 64 | {60426C1C-884E-4727-8BAC-794E601CDD15}.Debug|MSFS.ActiveCfg = Debug|Any CPU 65 | {60426C1C-884E-4727-8BAC-794E601CDD15}.Debug|MSFS.Build.0 = Debug|Any CPU 66 | {60426C1C-884E-4727-8BAC-794E601CDD15}.Release|Any CPU.ActiveCfg = Release|Any CPU 67 | {60426C1C-884E-4727-8BAC-794E601CDD15}.Release|Any CPU.Build.0 = Release|Any CPU 68 | {60426C1C-884E-4727-8BAC-794E601CDD15}.Release|MSFS.ActiveCfg = Release|Any CPU 69 | {60426C1C-884E-4727-8BAC-794E601CDD15}.Release|MSFS.Build.0 = Release|Any CPU 70 | {EF0B053E-4214-49B4-94A0-8B165981F822}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 71 | {EF0B053E-4214-49B4-94A0-8B165981F822}.Debug|Any CPU.Build.0 = Debug|Any CPU 72 | {EF0B053E-4214-49B4-94A0-8B165981F822}.Debug|MSFS.ActiveCfg = Debug|Any CPU 73 | {EF0B053E-4214-49B4-94A0-8B165981F822}.Debug|MSFS.Build.0 = Debug|Any CPU 74 | {EF0B053E-4214-49B4-94A0-8B165981F822}.Release|Any CPU.ActiveCfg = Release|Any CPU 75 | {EF0B053E-4214-49B4-94A0-8B165981F822}.Release|Any CPU.Build.0 = Release|Any CPU 76 | {EF0B053E-4214-49B4-94A0-8B165981F822}.Release|MSFS.ActiveCfg = Release|Any CPU 77 | {EF0B053E-4214-49B4-94A0-8B165981F822}.Release|MSFS.Build.0 = Release|Any CPU 78 | {A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Debug|Any CPU.ActiveCfg = Debug|MSFS 79 | {A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Debug|Any CPU.Build.0 = Debug|MSFS 80 | {A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Debug|MSFS.ActiveCfg = Debug|MSFS 81 | {A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Debug|MSFS.Build.0 = Debug|MSFS 82 | {A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release|Any CPU.ActiveCfg = Release|MSFS 83 | {A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release|Any CPU.Build.0 = Release|MSFS 84 | {A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release|MSFS.ActiveCfg = Release|MSFS 85 | {A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release|MSFS.Build.0 = Release|MSFS 86 | EndGlobalSection 87 | GlobalSection(SolutionProperties) = preSolution 88 | HideSolutionNode = FALSE 89 | EndGlobalSection 90 | GlobalSection(NestedProjects) = preSolution 91 | {6134E965-26F3-4242-AE61-16D19412EE15} = {7C55C68B-E475-4274-A0DB-37A629D3FE8A} 92 | EndGlobalSection 93 | GlobalSection(ExtensibilityGlobals) = postSolution 94 | SolutionGuid = {6805C8D9-1B80-44D5-BF81-1DF3386EA961} 95 | EndGlobalSection 96 | EndGlobal 97 | -------------------------------------------------------------------------------- /BravoLights/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace BravoLights.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("BravoLights.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized resource of type System.Drawing.Bitmap. 65 | /// 66 | internal static System.Drawing.Bitmap DebuggerImage { 67 | get { 68 | object obj = ResourceManager.GetObject("DebuggerImage", resourceCulture); 69 | return ((System.Drawing.Bitmap)(obj)); 70 | } 71 | } 72 | 73 | /// 74 | /// Looks up a localized resource of type System.Drawing.Bitmap. 75 | /// 76 | internal static System.Drawing.Bitmap ExitImage { 77 | get { 78 | object obj = ResourceManager.GetObject("ExitImage", resourceCulture); 79 | return ((System.Drawing.Bitmap)(obj)); 80 | } 81 | } 82 | 83 | /// 84 | /// Looks up a localized resource of type System.Drawing.Bitmap. 85 | /// 86 | internal static System.Drawing.Bitmap TableImage { 87 | get { 88 | object obj = ResourceManager.GetObject("TableImage", resourceCulture); 89 | return ((System.Drawing.Bitmap)(obj)); 90 | } 91 | } 92 | 93 | /// 94 | /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). 95 | /// 96 | internal static System.Drawing.Icon TrayIcon { 97 | get { 98 | object obj = ResourceManager.GetObject("TrayIcon", resourceCulture); 99 | return ((System.Drawing.Icon)(obj)); 100 | } 101 | } 102 | 103 | /// 104 | /// Looks up a localized string similar to {0} - Connected to simulator. 105 | /// 106 | internal static string TrayIconConnectedToSimFormat { 107 | get { 108 | return ResourceManager.GetString("TrayIconConnectedToSimFormat", resourceCulture); 109 | } 110 | } 111 | 112 | /// 113 | /// Looks up a localized string similar to Debugger. 114 | /// 115 | internal static string TrayIconMenuDebugger { 116 | get { 117 | return ResourceManager.GetString("TrayIconMenuDebugger", resourceCulture); 118 | } 119 | } 120 | 121 | /// 122 | /// Looks up a localized string similar to Exit. 123 | /// 124 | internal static string TrayIconMenuExit { 125 | get { 126 | return ResourceManager.GetString("TrayIconMenuExit", resourceCulture); 127 | } 128 | } 129 | 130 | /// 131 | /// Looks up a localized string similar to {0} - No Bravo Throttle detected. 132 | /// 133 | internal static string TrayIconNoBravoThrottleConnectedFormat { 134 | get { 135 | return ResourceManager.GetString("TrayIconNoBravoThrottleConnectedFormat", resourceCulture); 136 | } 137 | } 138 | 139 | /// 140 | /// Looks up a localized string similar to {0} - Waiting for simulator. 141 | /// 142 | internal static string TrayIconWaitingForSimFormat { 143 | get { 144 | return ResourceManager.GetString("TrayIconWaitingForSimFormat", resourceCulture); 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /BravoLights/UI/VariableListViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.ComponentModel; 5 | using System.Windows.Data; 6 | using BravoLights.Common; 7 | 8 | namespace BravoLights.UI 9 | { 10 | 11 | /// 12 | /// Interaction logic for the variable list view. 13 | /// 14 | public class VariableListViewModel : ViewModelBase, IDisposable 15 | { 16 | private readonly Dictionary itemEntries = new(); 17 | private readonly ObservableCollection items = new(); 18 | 19 | private readonly ICollectionView filteredView; 20 | 21 | public VariableListViewModel() 22 | { 23 | filteredView = CollectionViewSource.GetDefaultView(items); 24 | } 25 | 26 | /// 27 | /// Gets the items that should be displayed in the variable list. 28 | /// 29 | public ObservableCollection Items 30 | { 31 | get { return items; } 32 | } 33 | 34 | public void UpdateDefinitions(IEnumerable definitions) 35 | { 36 | UnsubscribeAll(); 37 | 38 | foreach (var def in definitions) 39 | { 40 | var item = new Item(def); 41 | itemEntries.Add(def, item); 42 | Items.Add(item); 43 | } 44 | } 45 | 46 | private void UnsubscribeAll() 47 | { 48 | foreach (var item in itemEntries.Values) 49 | { 50 | item.Unsubscribe(); 51 | } 52 | 53 | itemEntries.Clear(); 54 | Items.Clear(); 55 | } 56 | 57 | private string aircraftName; 58 | public string AircraftName 59 | { 60 | get { return aircraftName; } 61 | set { SetProperty(ref aircraftName, value); } 62 | } 63 | 64 | private string filterText; 65 | public string FilterText 66 | { 67 | get { return filterText; } 68 | set 69 | { 70 | SetProperty(ref filterText, value); 71 | if (value.Length > 0) 72 | { 73 | filteredView.Filter = (x) => 74 | { 75 | var defn = ((Item)x).DataDefinition; 76 | var variableName = defn.VariableName; 77 | return variableName.Contains(value, StringComparison.CurrentCultureIgnoreCase) || 78 | defn.Description.Contains(value, StringComparison.CurrentCultureIgnoreCase); 79 | }; 80 | } 81 | else 82 | { 83 | filteredView.Filter = null; 84 | } 85 | filteredView.Refresh(); 86 | } 87 | } 88 | 89 | private int textSize = 12; 90 | public int TextSize 91 | { 92 | get { return textSize; } 93 | set 94 | { 95 | SetProperty(ref textSize, value); 96 | } 97 | } 98 | 99 | public void Dispose() 100 | { 101 | GC.SuppressFinalize(this); 102 | UnsubscribeAll(); 103 | } 104 | } 105 | 106 | /// 107 | /// Holds the description of an available variable. 108 | /// 109 | public class DataDefinition 110 | { 111 | public string Group { get; set; } 112 | public string VariableName { get; set; } 113 | public string Units { get; set; } 114 | public string Description { get; set; } 115 | public IVariable Variable { get; set; } 116 | } 117 | 118 | /// 119 | /// Represents a variable and its current value. 120 | /// 121 | public class Item : ViewModelBase 122 | { 123 | private static readonly Exception NoValueReceivedYetException = new Exception("No value received yet"); 124 | 125 | public Item(DataDefinition definition) 126 | { 127 | DataDefinition = definition; 128 | Value = NoValueReceivedYetException; 129 | } 130 | 131 | private DataDefinition key; 132 | public DataDefinition DataDefinition 133 | { 134 | get { return key; } 135 | private set { SetProperty(ref key, value); } 136 | } 137 | 138 | private object mValue; 139 | public object Value 140 | { 141 | get { return mValue; } 142 | private set 143 | { 144 | SetProperty(ref mValue, value); 145 | RaisePropertyChanged(nameof(ValueText)); 146 | RaisePropertyChanged(nameof(IsError)); 147 | } 148 | } 149 | 150 | public string ValueText 151 | { 152 | get 153 | { 154 | if (mValue is Exception ex) 155 | { 156 | return ex.Message; 157 | } 158 | return Convert.ToString(mValue); 159 | } 160 | } 161 | 162 | public bool IsError 163 | { 164 | get { return mValue is Exception; } 165 | } 166 | 167 | 168 | private bool isSubscribed = false; 169 | 170 | public void Subscribe() 171 | { 172 | DataDefinition.Variable.ValueChanged += Variable_ValueChanged; 173 | isSubscribed = true; 174 | } 175 | 176 | public void Unsubscribe() 177 | { 178 | if (isSubscribed) 179 | { 180 | DataDefinition.Variable.ValueChanged -= Variable_ValueChanged; 181 | 182 | // We deliberately don't advertise a property change, but we want to remember 183 | // that the current value is no longer valid. 184 | mValue = new Exception("Not subscribed"); 185 | isSubscribed = false; 186 | } 187 | } 188 | 189 | private void Variable_ValueChanged(object sender, ValueChangedEventArgs e) 190 | { 191 | Value = e.NewValue; 192 | } 193 | } 194 | 195 | public class ExpressionEditorViewModel : ViewModelBase 196 | { 197 | private string expressionText; 198 | public string ExpressionText 199 | { 200 | get { return expressionText; } 201 | set 202 | { 203 | SetProperty(ref expressionText, value); 204 | } 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /DCSBravoLights/DebuggerUI.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.ComponentModel; 5 | using System.Windows; 6 | using System.Windows.Data; 7 | using System.Windows.Threading; 8 | using BravoLights.Common; 9 | 10 | namespace DCSBravoLights 11 | { 12 | /// 13 | /// Interaction logic for DebuggerUI.xaml 14 | /// 15 | public partial class DebuggerUI : Window 16 | { 17 | private readonly DebuggerViewModel viewModel; 18 | 19 | public DebuggerUI() 20 | { 21 | viewModel = new DebuggerViewModel(VariablesManager.DcsBiosState); 22 | 23 | InitializeComponent(); 24 | } 25 | 26 | private DcsVariablesManager VariablesManager 27 | { 28 | get { return DcsConnection.Connection.DcsVariablesManager; } 29 | } 30 | 31 | public DebuggerViewModel ViewModel 32 | { 33 | get { return viewModel; } 34 | } 35 | 36 | protected override void OnInitialized(EventArgs e) 37 | { 38 | base.OnInitialized(e); 39 | 40 | VariablesManager.AircraftNameChanged += AircraftNameChanged; 41 | VariablesManager.DefinitionsChanged += DefinitionsChanged; 42 | 43 | viewModel.AircraftName = VariablesManager.AircraftName; 44 | DefinitionsChanged(null, EventArgs.Empty); 45 | } 46 | 47 | protected override void OnClosed(EventArgs e) 48 | { 49 | base.OnClosed(e); 50 | viewModel.Dispose(); 51 | } 52 | 53 | private void DefinitionsChanged(object sender, EventArgs e) 54 | { 55 | Dispatcher.InvokeAsync( () => { 56 | viewModel.UpdateDefinitions(VariablesManager.DataDefinitions); 57 | }); 58 | } 59 | 60 | private void AircraftNameChanged(object sender, EventArgs e) 61 | { 62 | Dispatcher.InvokeAsync(() => 63 | { 64 | viewModel.AircraftName = VariablesManager.AircraftName; 65 | }); 66 | } 67 | } 68 | 69 | public class DebuggerViewModel : ViewModelBase, IDisposable 70 | { 71 | private readonly Dictionary itemEntries = new(); 72 | private readonly ObservableCollection items = new(); 73 | 74 | private readonly DcsBiosState dcsBiosState; 75 | private readonly ICollectionView filteredView; 76 | 77 | public DebuggerViewModel(DcsBiosState dcsBiosState) 78 | { 79 | this.dcsBiosState = dcsBiosState; 80 | this.filteredView = CollectionViewSource.GetDefaultView(items); 81 | 82 | } 83 | 84 | public ICollectionView FilteredItems { get; private set; } 85 | 86 | public ObservableCollection Items 87 | { 88 | get { return items; } 89 | } 90 | 91 | public void UpdateDefinitions(IEnumerable definitions) 92 | { 93 | UnsubscribeAll(); 94 | 95 | foreach (var def in definitions) 96 | { 97 | var item = new Item { DataDefinition = def, Value = "Not yet received from simulator" }; 98 | itemEntries.Add(def, item); 99 | Items.Add(item); 100 | dcsBiosState.AddHandler(def, HandleValueChanged); 101 | } 102 | } 103 | 104 | private string aircraftName; 105 | public string AircraftName 106 | { 107 | get { return aircraftName; } 108 | set { SetProperty(ref aircraftName, value); } 109 | } 110 | 111 | private string filterText; 112 | public string FilterText 113 | { 114 | get { return filterText; } 115 | set 116 | { 117 | SetProperty(ref filterText, value); 118 | if (value.Length > 0) 119 | { 120 | filteredView.Filter = (x) => 121 | { 122 | var defn = ((Item)x).DataDefinition; 123 | var variableName = defn.VariableName; 124 | return variableName.DcsIdentifier.Contains(value, StringComparison.CurrentCultureIgnoreCase) || 125 | variableName.DcsCategory.Contains(value, StringComparison.CurrentCultureIgnoreCase) || 126 | defn.Description.Contains(value, StringComparison.CurrentCultureIgnoreCase); 127 | }; 128 | } 129 | else 130 | { 131 | filteredView.Filter = null; 132 | } 133 | filteredView.Refresh(); 134 | } 135 | } 136 | 137 | public void Dispose() 138 | { 139 | GC.SuppressFinalize(this); 140 | UnsubscribeAll(); 141 | } 142 | 143 | private void UnsubscribeAll() 144 | { 145 | foreach (var item in itemEntries.Keys) 146 | { 147 | dcsBiosState.RemoveHandler(item, HandleValueChanged); 148 | } 149 | 150 | itemEntries.Clear(); 151 | Items.Clear(); 152 | } 153 | 154 | private void HandleValueChanged(object sender, ValueChangedEventArgs e) 155 | { 156 | var def = (DataDefinition)sender; 157 | 158 | if (itemEntries.TryGetValue(def, out var item)) 159 | { 160 | item.Value = e.NewValue; 161 | } 162 | } 163 | } 164 | 165 | public class Item : ViewModelBase 166 | { 167 | private DataDefinition key; 168 | public DataDefinition DataDefinition 169 | { 170 | get { return key; } 171 | set { SetProperty(ref key, value); } 172 | } 173 | 174 | private object mValue; 175 | public object Value 176 | { 177 | get { return mValue; } 178 | set 179 | { 180 | SetProperty(ref mValue, value); 181 | RaisePropertyChanged(nameof(ValueText)); 182 | RaisePropertyChanged(nameof(IsError)); 183 | } 184 | } 185 | 186 | public string ValueText 187 | { 188 | get 189 | { 190 | if (mValue is Exception ex) 191 | { 192 | return ex.Message; 193 | } 194 | return Convert.ToString(mValue); 195 | } 196 | } 197 | 198 | public bool IsError 199 | { 200 | get { return mValue is Exception; } 201 | } 202 | } 203 | } 204 | --------------------------------------------------------------------------------