From 3fe423cd49ffd5f465477a20f70445060ed75406 Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Tue, 22 Jul 2025 18:04:22 +0100 Subject: [PATCH 01/16] Migrate RoslynAnalyzers to slnx --- RoslynAnalyzers/RoslynAnalyzers.sln | 49 ---------------------------- RoslynAnalyzers/RoslynAnalyzers.slnx | 7 ++++ 2 files changed, 7 insertions(+), 49 deletions(-) delete mode 100644 RoslynAnalyzers/RoslynAnalyzers.sln create mode 100644 RoslynAnalyzers/RoslynAnalyzers.slnx diff --git a/RoslynAnalyzers/RoslynAnalyzers.sln b/RoslynAnalyzers/RoslynAnalyzers.sln deleted file mode 100644 index 99bf8406..00000000 --- a/RoslynAnalyzers/RoslynAnalyzers.sln +++ /dev/null @@ -1,49 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JonSkeet.RoslynAnalyzers", "JonSkeet.RoslynAnalyzers\JonSkeet.RoslynAnalyzers\JonSkeet.RoslynAnalyzers.csproj", "{08A37136-34DC-4E67-BE0F-C21675DE2C7F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JonSkeet.RoslynAnalyzers.CodeFixes", "JonSkeet.RoslynAnalyzers\JonSkeet.RoslynAnalyzers.CodeFixes\JonSkeet.RoslynAnalyzers.CodeFixes.csproj", "{463F0B5F-22CF-4B2F-9C4D-C83B4BC751EA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JonSkeet.RoslynAnalyzers.Package", "JonSkeet.RoslynAnalyzers\JonSkeet.RoslynAnalyzers.Package\JonSkeet.RoslynAnalyzers.Package.csproj", "{FCBBA14E-5CFA-4E59-B9ED-1DBB615FA741}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JonSkeet.RoslynAnalyzers.Test", "JonSkeet.RoslynAnalyzers\JonSkeet.RoslynAnalyzers.Test\JonSkeet.RoslynAnalyzers.Test.csproj", "{FA5F1645-F6CD-4DEF-99C0-84F2913875EC}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JonSkeet.RoslynAnalyzers.Vsix", "JonSkeet.RoslynAnalyzers\JonSkeet.RoslynAnalyzers.Vsix\JonSkeet.RoslynAnalyzers.Vsix.csproj", "{3D130F44-F121-45D1-AD49-A670EA70D4A1}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {08A37136-34DC-4E67-BE0F-C21675DE2C7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {08A37136-34DC-4E67-BE0F-C21675DE2C7F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {08A37136-34DC-4E67-BE0F-C21675DE2C7F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {08A37136-34DC-4E67-BE0F-C21675DE2C7F}.Release|Any CPU.Build.0 = Release|Any CPU - {463F0B5F-22CF-4B2F-9C4D-C83B4BC751EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {463F0B5F-22CF-4B2F-9C4D-C83B4BC751EA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {463F0B5F-22CF-4B2F-9C4D-C83B4BC751EA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {463F0B5F-22CF-4B2F-9C4D-C83B4BC751EA}.Release|Any CPU.Build.0 = Release|Any CPU - {FCBBA14E-5CFA-4E59-B9ED-1DBB615FA741}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FCBBA14E-5CFA-4E59-B9ED-1DBB615FA741}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FCBBA14E-5CFA-4E59-B9ED-1DBB615FA741}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FCBBA14E-5CFA-4E59-B9ED-1DBB615FA741}.Release|Any CPU.Build.0 = Release|Any CPU - {FA5F1645-F6CD-4DEF-99C0-84F2913875EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FA5F1645-F6CD-4DEF-99C0-84F2913875EC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FA5F1645-F6CD-4DEF-99C0-84F2913875EC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FA5F1645-F6CD-4DEF-99C0-84F2913875EC}.Release|Any CPU.Build.0 = Release|Any CPU - {3D130F44-F121-45D1-AD49-A670EA70D4A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3D130F44-F121-45D1-AD49-A670EA70D4A1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3D130F44-F121-45D1-AD49-A670EA70D4A1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3D130F44-F121-45D1-AD49-A670EA70D4A1}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {AE43714F-E6CF-4FF9-89E9-0F5263E6FCDB} - EndGlobalSection -EndGlobal diff --git a/RoslynAnalyzers/RoslynAnalyzers.slnx b/RoslynAnalyzers/RoslynAnalyzers.slnx new file mode 100644 index 00000000..3356d1ce --- /dev/null +++ b/RoslynAnalyzers/RoslynAnalyzers.slnx @@ -0,0 +1,7 @@ + + + + + + + From 5ee87554b42866fa6b2c6952abf384e865630bf9 Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Tue, 22 Jul 2025 19:30:15 +0100 Subject: [PATCH 02/16] Reword the "dangerous with" analyzer into two One analyzer checks that any dangerous record parameter has [DangerousWithTarget] (which would normally be declared as an internal attribute). The other checks that all parameters set in with operators *don't* have [DangerousWithTarget]. --- .../DangerousWithOperatorAnalyzerTest.cs | 128 ++------------ .../DangerousWithTargetAnalyzerTest.cs | 167 ++++++++++++++++++ .../DangerousWithOperatorAnalyzer.cs | 79 +++------ .../DangerousWithTargetAnalyzer.cs | 123 +++++++++++++ 4 files changed, 332 insertions(+), 165 deletions(-) create mode 100644 RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Test/DangerousWithTargetAnalyzerTest.cs create mode 100644 RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers/DangerousWithTargetAnalyzer.cs diff --git a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Test/DangerousWithOperatorAnalyzerTest.cs b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Test/DangerousWithOperatorAnalyzerTest.cs index 8a4026e5..03b4056a 100644 --- a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Test/DangerousWithOperatorAnalyzerTest.cs +++ b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Test/DangerousWithOperatorAnalyzerTest.cs @@ -7,30 +7,13 @@ namespace JonSkeet.RoslynAnalyzers.Test; public class DangerousWithOperatorAnalyzerTest { - [Test] - public async Task TestNoInitializers() - { - var test = @"public record Simple(int X, int Y); - - class Test - { - static void M() - { - Simple s1 = new Simple(10, 10); - Simple s2 = s1 with { X = 10 }; - } - }"; - - await VerifyCS.VerifyAnalyzerAsync(test); - } + private const string AttributeDeclaration = "[System.AttributeUsage(System.AttributeTargets.Parameter)] internal class DangerousWithTargetAttribute : System.Attribute {}\n"; + private const string OtherAttributeDeclaration = "[System.AttributeUsage(System.AttributeTargets.Parameter)] internal class OtherAttribute : System.Attribute {}\n"; [Test] - public async Task TestInitializerUsedInUnsetParameter() + public async Task NoDangerousParameters() { - var test = @"public record Simple(int X, int Y) - { - public int Z { get; } = Y * 2; - } + var test = AttributeDeclaration + @"public record Simple(int X, int Y); class Test { @@ -45,12 +28,9 @@ static void M() } [Test] - public async Task TestPropertyInitializerUsedInSetParameter() + public async Task DangerousParameterUnset() { - var test = @"public record Simple(int X, int Y) - { - public int Z { get; } = X * 2; - } + var test = AttributeDeclaration + @"public record Simple(int X, [DangerousWithTarget] int Y); class Test { @@ -61,44 +41,13 @@ static void M() } }"; - var diagnostic = new DiagnosticResult(DangerousWithOperatorAnalyzer.Rule) - .WithSpan(11, 29, 11, 47) - .WithArguments("X"); - await VerifyCS.VerifyAnalyzerAsync(test, diagnostic); - } - - [Test] - public async Task TestInitializerCallingMethodUsedInSetParameter() - { - var test = @"public record Simple(int X, int Y) - { - public int Z { get; } = M(X); - private static int M(int value) => value; - } - - class Test - { - static void M() - { - Simple s1 = new Simple(10, 10); - Simple s2 = s1 with { X = 10 }; - } - }"; - - var diagnostic = new DiagnosticResult(DangerousWithOperatorAnalyzer.Rule) - .WithSpan(12, 29, 12, 47) - .WithArguments("X"); - await VerifyCS.VerifyAnalyzerAsync(test, diagnostic); + await VerifyCS.VerifyAnalyzerAsync(test); } [Test] - public async Task TestMultipleInitializersSingleDiagnostic() + public async Task DangerousParameterSet() { - var test = @"public record Simple(int X, int Y) - { - public int Z1 { get; } = X * 2; - public int Z2 { get; } = X * 2; - } + var test = AttributeDeclaration + @"public record Simple([DangerousWithTarget] int X, int Y); class Test { @@ -110,86 +59,47 @@ static void M() }"; var diagnostic = new DiagnosticResult(DangerousWithOperatorAnalyzer.Rule) - .WithSpan(12, 29, 12, 47) + .WithSpan(9, 29, 9, 47) .WithArguments("X"); await VerifyCS.VerifyAnalyzerAsync(test, diagnostic); } [Test] - public async Task TestMultipleParametersDiagnostics() + public async Task MultipleDangerousParameters() { - var test = @"public record Simple(int X, int Y) - { - public int Z1 { get; } = X * 2; - public int Z2 { get; } = Y * 2; - } + var test = AttributeDeclaration + @"public record Simple([DangerousWithTarget] int X, [DangerousWithTarget] int Y); class Test { static void M() { Simple s1 = new Simple(10, 10); - Simple s2 = s1 with { X = 10, Y = 20 }; + Simple s2 = s1 with { X = 20, Y = 20 }; } }"; var d1 = new DiagnosticResult(DangerousWithOperatorAnalyzer.Rule) - .WithSpan(12, 29, 12, 55) + .WithSpan(9, 29, 9, 55) .WithArguments("X"); var d2 = new DiagnosticResult(DangerousWithOperatorAnalyzer.Rule) - .WithSpan(12, 29, 12, 55) + .WithSpan(9, 29, 9, 55) .WithArguments("Y"); await VerifyCS.VerifyAnalyzerAsync(test, d1, d2); } [Test] - public async Task TestFieldInitializerUsedInSetParameter() - { - var test = @"public record Simple(int X, int Y) - { - private readonly int z = X * 2; - - public int Z => z; - } - - class Test - { - static void M() - { - Simple s1 = new Simple(10, 10); - Simple s2 = s1 with { X = 10 }; - } - }"; - - var diagnostic = new DiagnosticResult(DangerousWithOperatorAnalyzer.Rule) - .WithSpan(13, 29, 13, 47) - .WithArguments("X"); - await VerifyCS.VerifyAnalyzerAsync(test, diagnostic); - } - - [Test] - public async Task TestMultipleFieldInitializerUsedInSetParameter() + public async Task OtherAttributesIgnored() { - var test = @"public record Simple(int X, int Y) - { - private readonly int z = Y * 2, zz = X * 2; - - public int Z => z; - public int ZZ => z; - } + var test = OtherAttributeDeclaration + @"public record Simple([Other] int X, int Y); class Test { static void M() { Simple s1 = new Simple(10, 10); - Simple s2 = s1 with { X = 10 }; + Simple s2 = s1 with { X = 10, Y = 20 }; } }"; - - var diagnostic = new DiagnosticResult(DangerousWithOperatorAnalyzer.Rule) - .WithSpan(14, 29, 14, 47) - .WithArguments("X"); - await VerifyCS.VerifyAnalyzerAsync(test, diagnostic); + await VerifyCS.VerifyAnalyzerAsync(test); } } diff --git a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Test/DangerousWithTargetAnalyzerTest.cs b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Test/DangerousWithTargetAnalyzerTest.cs new file mode 100644 index 00000000..a89cc1ce --- /dev/null +++ b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Test/DangerousWithTargetAnalyzerTest.cs @@ -0,0 +1,167 @@ +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; +using System.Threading.Tasks; +using VerifyCS = JonSkeet.RoslynAnalyzers.Test.Verifiers.CSharpAnalyzerVerifier; + +namespace JonSkeet.RoslynAnalyzers.Test; + +public class DangerousWithTargetAnalyzerTest +{ + private const string AttributeDeclaration = "[System.AttributeUsage(System.AttributeTargets.Parameter)] internal class DangerousWithTargetAttribute : System.Attribute {}\n"; + private const string OtherAttributeDeclaration = "[System.AttributeUsage(System.AttributeTargets.Parameter)] internal class OtherAttribute : System.Attribute {}\n"; + + [Test] + public async Task NoParameters() + { + var test = AttributeDeclaration + @"public record Simple;"; + await VerifyCS.VerifyAnalyzerAsync(test); + } + + [Test] + public async Task NoInitializers() + { + var test = AttributeDeclaration + @"public record Simple(int X, int Y);"; + await VerifyCS.VerifyAnalyzerAsync(test); + } + + [Test] + public async Task PropertyInitializer() + { + var test = AttributeDeclaration + @"public record Simple(int X, int Y) + { + public int Z { get; } = X * 2; + }"; + + var diagnostic = new DiagnosticResult(DangerousWithTargetAnalyzer.Rule) + .WithSpan(2, 22, 2, 27) + .WithArguments("X"); + await VerifyCS.VerifyAnalyzerAsync(test, diagnostic); + } + + [Test] + public async Task InitializerCallingMethod() + { + var test = AttributeDeclaration + @"public record Simple(int X, int Y) + { + public int Z { get; } = M(X); + private static int M(int value) => value; + }"; + + var diagnostic = new DiagnosticResult(DangerousWithTargetAnalyzer.Rule) + .WithSpan(2, 22, 2, 27) + .WithArguments("X"); + await VerifyCS.VerifyAnalyzerAsync(test, diagnostic); + } + + [Test] + public async Task MultipleInitializersSingleDiagnostic() + { + var test = AttributeDeclaration + @"public record Simple(int X, int Y) + { + public int Z1 { get; } = X * 2; + public int Z2 { get; } = X * 2; + }"; + + var diagnostic = new DiagnosticResult(DangerousWithTargetAnalyzer.Rule) + .WithSpan(2, 22, 2, 27) + .WithArguments("X"); + await VerifyCS.VerifyAnalyzerAsync(test, diagnostic); + } + + [Test] + public async Task MultipleInitializerMultipleDiagnostics() + { + var test = AttributeDeclaration + @"public record Simple(int X, int Y, int Safe) + { + public int Z1 { get; } = X * 2; + public int Z2 { get; } = Y * 2; + }"; + + var d1 = new DiagnosticResult(DangerousWithTargetAnalyzer.Rule) + .WithSpan(2, 22, 2, 27) + .WithArguments("X"); + var d2 = new DiagnosticResult(DangerousWithTargetAnalyzer.Rule) + .WithSpan(2, 29, 2, 34) + .WithArguments("Y"); + await VerifyCS.VerifyAnalyzerAsync(test, d1, d2); + } + + [Test] + public async Task SingleFieldInitializer() + { + var test = AttributeDeclaration + @"public record Simple(int X, int Y) + { + private readonly int z = X * 2; + + public int Z => z; + }"; + + var diagnostic = new DiagnosticResult(DangerousWithTargetAnalyzer.Rule) + .WithSpan(2, 22, 2, 27) + .WithArguments("X"); + await VerifyCS.VerifyAnalyzerAsync(test, diagnostic); + } + + [Test] + public async Task MultipleFieldInitializers() + { + var test = AttributeDeclaration + @"public record Simple(int X, int Y, int Safe) + { + private readonly int z = Y * 2, zz = X * 2; + + public int Z => z; + public int ZZ => z; + }"; + + var d1 = new DiagnosticResult(DangerousWithTargetAnalyzer.Rule) + .WithSpan(2, 29, 2, 34) + .WithArguments("Y"); + var d2 = new DiagnosticResult(DangerousWithTargetAnalyzer.Rule) + .WithSpan(2, 22, 2, 27) + .WithArguments("X"); + await VerifyCS.VerifyAnalyzerAsync(test, d1, d2); + } + + [Test] + public async Task AttributedParameter() + { + var test = AttributeDeclaration + @"public record Simple([DangerousWithTarget] int X, int Y) + { + public int Z { get; } = X * 2; + }"; + + var diagnostic = new DiagnosticResult(DangerousWithTargetAnalyzer.Rule) + .WithSpan(2, 22, 2, 27) + .WithArguments("X"); + await VerifyCS.VerifyAnalyzerAsync(test); + } + + [Test] + public async Task OtherAttributedParameter() + { + var test = AttributeDeclaration + OtherAttributeDeclaration + @"public record Simple([Other] int X, int Y) + { + public int Z { get; } = X * 2; + }"; + + var diagnostic = new DiagnosticResult(DangerousWithTargetAnalyzer.Rule) + .WithSpan(3, 22, 3, 35) + .WithArguments("X"); + await VerifyCS.VerifyAnalyzerAsync(test, diagnostic); + } + + [Test] + public async Task MixedAttribution() + { + var test = AttributeDeclaration + @"public record Simple([DangerousWithTarget] int X, int Y) + { + public int Z { get; } = X * 2; + public int ZZ { get; } = Y * 2; + }"; + + var diagnostic = new DiagnosticResult(DangerousWithTargetAnalyzer.Rule) + .WithSpan(2, 51, 2, 56) + .WithArguments("Y"); + await VerifyCS.VerifyAnalyzerAsync(test, diagnostic); + } +} diff --git a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers/DangerousWithOperatorAnalyzer.cs b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers/DangerousWithOperatorAnalyzer.cs index b6123384..bf9af881 100644 --- a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers/DangerousWithOperatorAnalyzer.cs +++ b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers/DangerousWithOperatorAnalyzer.cs @@ -2,28 +2,22 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics; using System.Linq; -using System.Reflection; -using System.Xml.Linq; namespace JonSkeet.RoslynAnalyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class DangerousWithOperatorAnalyzer : DiagnosticAnalyzer { - public const string DiagnosticId = "JS0001"; + public const string DiagnosticId = "JS0002"; - // You can change these strings in the Resources.resx file. If you do not want your analyzer to be localize-able, you can use regular strings for Title and MessageFormat. - // See https://github.com/dotnet/roslyn/blob/main/docs/analyzers/Localizing%20Analyzers.md for more on localization - private static readonly string Title = "With operator sets a record parameter used during initialization"; - private static readonly string MessageFormat = "Record parameter '{0}' is used during initialization"; - private static readonly string Description = "With operator sets a record parameter used during initialization."; + private const string Title = $"Record parameters annotated with [{DangerousWithTargetAnalyzer.DangerousWithTargetAttributeShortName}] should not be set using the 'with' operator."; + private const string MessageFormat = $"Record parameter '{{0}}' is annotated with [{DangerousWithTargetAnalyzer.DangerousWithTargetAttributeShortName}]"; + private const string Description = $"Record parameters are annotated with [{DangerousWithTargetAnalyzer.DangerousWithTargetAttributeShortName}] if they are dangerous to set using the 'with' operator." + + " This is usually due to computations during initialization using the parameter, which aren't performed again using the new value. Using the 'with' operator with such parameters can lead to inconsistent state."; private const string Category = "Reliability"; - // TODO: Should this actually be private? Harder to test. public static DiagnosticDescriptor Rule { get; } = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description); public override ImmutableArray SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } } @@ -42,59 +36,32 @@ private static void AnalyzeWithOperator(SyntaxNodeAnalysisContext context) var node = context.Node; var assignedParameters = syntax.Initializer .Expressions - .Select(exp => model.GetSymbolInfo(((AssignmentExpressionSyntax) exp).Left).Symbol?.Name) + .Select(exp => model.GetSymbolInfo(((AssignmentExpressionSyntax) exp).Left).Symbol) .ToList(); - var typeInfo = model.GetTypeInfo(syntax.Expression); - if (!typeInfo.ConvertedType.IsRecord) - { - return; - } - var recordMembers = typeInfo.ConvertedType.GetMembers(); - foreach (var recordMember in recordMembers) + + foreach (var assignedParameter in assignedParameters) { - var declaringReferences = recordMember.DeclaringSyntaxReferences; - foreach (var decl in declaringReferences) + // The assigned parameter refers to a property, but we need to get at the parameter declaration. + // We check each declaring syntax to see if it's actually declaring a parameter. + if (assignedParameter is not IPropertySymbol propertySymbol) { - var declNode = decl.GetSyntax(); - if (declNode is PropertyDeclarationSyntax prop && prop.Initializer is not null) - { - MaybeReportDiagnostic(context, assignedParameters, prop.Initializer); - } - else if (declNode is FieldDeclarationSyntax field) - { - foreach (var subDecl in field.Declaration.Variables) - { - MaybeReportDiagnostic(context, assignedParameters, subDecl.Initializer); - } - } - else if (declNode is VariableDeclaratorSyntax variableDeclarator) + continue; + } + foreach (var syntaxReference in propertySymbol.DeclaringSyntaxReferences) + { + var declarationSyntax = syntaxReference.GetSyntax(); + if (declarationSyntax is not ParameterSyntax parameterSyntax) { - MaybeReportDiagnostic(context, assignedParameters, variableDeclarator.Initializer); + continue; } - else + var declaredSymbol = model.GetDeclaredSymbol(parameterSyntax); + if (declaredSymbol is IParameterSymbol parameterSymbol && + DangerousWithTargetAnalyzer.HasDangerousWithTargetAttribute(parameterSymbol, model)) { - Debugger.Break(); + var diagnostic = Diagnostic.Create(Rule, context.Node.GetLocation(), parameterSymbol.Name); + context.ReportDiagnostic(diagnostic); } } } } - - private static void MaybeReportDiagnostic(SyntaxNodeAnalysisContext context, List assignedParameters, EqualsValueClauseSyntax initializer) - { - var dataFlow = context.SemanticModel.AnalyzeDataFlow(initializer.Value); - var readSymbols = dataFlow.ReadInside; - for (int i = 0; i < readSymbols.Length; i++) - { - var parameterIndex = assignedParameters.IndexOf(readSymbols[i].Name); - if (parameterIndex != -1) - { - // Avoid reporting the same parameter multiple times. - assignedParameters[parameterIndex] = null; - var diagnostic = Diagnostic.Create(Rule, context.Node.GetLocation(), readSymbols[i].Name); - context.ReportDiagnostic(diagnostic); - return; - } - } - return; - } } diff --git a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers/DangerousWithTargetAnalyzer.cs b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers/DangerousWithTargetAnalyzer.cs new file mode 100644 index 00000000..e11229db --- /dev/null +++ b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers/DangerousWithTargetAnalyzer.cs @@ -0,0 +1,123 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace JonSkeet.RoslynAnalyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class DangerousWithTargetAnalyzer : DiagnosticAnalyzer +{ + internal const string DangerousWithTargetAttributeShortName = "DangerousWithTarget"; + internal const string DangerousWithTargetAttributeFullName = "DangerousWithTargetAttribute"; + + public const string DiagnosticId = "JS0001"; + + private static readonly string Title = $"Record parameters used during initialization should be annotated with [{DangerousWithTargetAttributeShortName}]"; + private static readonly string MessageFormat = $"Record parameter '{{0}}' is used during initialization; it should be annotated with [{DangerousWithTargetAttributeShortName}]"; + private static readonly string Description = "Record parameters used during initialization can introduce inconsistencies when set using the 'with' operator." + + $" Such parameters should be annotated with a [{DangerousWithTargetAttributeShortName}] attribute (which may be internal) to indicate that this is deliberate." + + " Setting such parameters using the 'with' operator triggers a warning via another analyzer."; + private const string Category = "Reliability"; + + public static DiagnosticDescriptor Rule { get; } = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description); + + public override ImmutableArray SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } } + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.RegisterSyntaxNodeAction(AnalyzeRecordDeclaration, SyntaxKind.RecordDeclaration); + context.EnableConcurrentExecution(); + } + + private static void AnalyzeRecordDeclaration(SyntaxNodeAnalysisContext context) + { + var syntax = (RecordDeclarationSyntax) context.Node; + if (syntax.ParameterList is not ParameterListSyntax parameterList) + { + return; + } + var model = context.SemanticModel; + var parameterSymbols = parameterList.Parameters.Select(p => model.GetDeclaredSymbol(p)).ToList(); + + var node = context.Node; + var declaredSymbol = model.GetDeclaredSymbol(node); + if (declaredSymbol is not ITypeSymbol typeSymbol) + { + return; + } + if (!typeSymbol.IsRecord) + { + return; + } + // Null out any parameters which already have the annotation. + for (int i = 0; i < parameterSymbols.Count; i++) + { + if (HasDangerousWithTargetAttribute(parameterSymbols[i], model)) + { + parameterSymbols[i] = null; + } + } + + var recordMembers = typeSymbol.GetMembers(); + + foreach (var recordMember in recordMembers) + { + var declaringReferences = recordMember.DeclaringSyntaxReferences; + foreach (var decl in declaringReferences) + { + var declNode = decl.GetSyntax(); + // TODO: Figure out a nicer way of doing this. (And if we even need all of the options here.) + if (declNode is PropertyDeclarationSyntax prop && prop.Initializer is not null) + { + MaybeReportDiagnostic(context, parameterSymbols, prop.Initializer); + } + else if (declNode is FieldDeclarationSyntax field) + { + foreach (var subDecl in field.Declaration.Variables) + { + MaybeReportDiagnostic(context, parameterSymbols, subDecl.Initializer); + } + } + else if (declNode is VariableDeclaratorSyntax variableDeclarator) + { + MaybeReportDiagnostic(context, parameterSymbols, variableDeclarator.Initializer); + } + } + } + } + + private static void MaybeReportDiagnostic(SyntaxNodeAnalysisContext context, List parameterSymbols, EqualsValueClauseSyntax initializer) + { + var dataFlow = context.SemanticModel.AnalyzeDataFlow(initializer.Value); + var readSymbols = dataFlow.ReadInside; + for (int i = 0; i < readSymbols.Length; i++) + { + if (readSymbols[i] is not IParameterSymbol readParameterSymbol) + { + continue; + } + var parameterIndex = parameterSymbols.IndexOf(readParameterSymbol); + if (parameterIndex != -1) + { + // Avoid reporting the same parameter multiple times. + parameterSymbols[parameterIndex] = null; + var firstDeclaration = readParameterSymbol.DeclaringSyntaxReferences.FirstOrDefault(); + var diagnostic = Diagnostic.Create(Rule, firstDeclaration?.GetSyntax()?.GetLocation(), readParameterSymbol.Name); + context.ReportDiagnostic(diagnostic); + return; + } + } + return; + } + + internal static bool HasDangerousWithTargetAttribute(IParameterSymbol symbol, SemanticModel model) => + symbol.GetAttributes().Any(attr => attr.AttributeClass?.Name == DangerousWithTargetAttributeFullName); + + internal static bool HasDangerousWithTargetAttribute(IPropertySymbol symbol, SemanticModel model) => + symbol.GetAttributes().Any(attr => attr.AttributeClass?.Name == DangerousWithTargetAttributeFullName); +} From be2ec1ed6ce5c1afb376db49c3ef6adf561a11d5 Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Tue, 22 Jul 2025 19:31:06 +0100 Subject: [PATCH 03/16] Release 1.0.0-beta.4 of the Roslyn Analyzers --- .../JonSkeet.RoslynAnalyzers.Package.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Package/JonSkeet.RoslynAnalyzers.Package.csproj b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Package/JonSkeet.RoslynAnalyzers.Package.csproj index 54a93bcf..48c9249d 100644 --- a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Package/JonSkeet.RoslynAnalyzers.Package.csproj +++ b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Package/JonSkeet.RoslynAnalyzers.Package.csproj @@ -9,7 +9,7 @@ JonSkeet.RoslynAnalyzers - 1.0.0-beta.3 + 1.0.0-beta.4 Jon Skeet Apache-2.0 https://github.com/jskeet/democode From e9a9488bc73300cf4d9279c576b633c441caa8d9 Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Tue, 22 Jul 2025 20:10:56 +0100 Subject: [PATCH 04/16] Release JonSkeet.RoslynAnalyzers 1.0.0-beta.5 Previously I hadn't built properly. I probably need a script for that... --- .../JonSkeet.RoslynAnalyzers.Package.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Package/JonSkeet.RoslynAnalyzers.Package.csproj b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Package/JonSkeet.RoslynAnalyzers.Package.csproj index 48c9249d..3f4af255 100644 --- a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Package/JonSkeet.RoslynAnalyzers.Package.csproj +++ b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Package/JonSkeet.RoslynAnalyzers.Package.csproj @@ -9,7 +9,7 @@ JonSkeet.RoslynAnalyzers - 1.0.0-beta.4 + 1.0.0-beta.5 Jon Skeet Apache-2.0 https://github.com/jskeet/democode From 26caa098e30f24e9d7a26588f3b5cf0dca89a071 Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Tue, 22 Jul 2025 20:46:12 +0100 Subject: [PATCH 05/16] Attempt to fix the use of records within a different file/assembly --- RoslynAnalyzers/.gitignore | 1 + .../JonSkeet.RoslynAnalyzers.Package.csproj | 2 +- .../DangerousWithOperatorAnalyzerTest.cs | 29 +++++++++++ .../DangerousWithTargetAnalyzerTest.cs | 3 -- .../JonSkeet.RoslynAnalyzers.Vsix.csproj | 48 ------------------- .../source.extension.vsixmanifest | 24 ---------- .../DangerousWithOperatorAnalyzer.cs | 44 ++++++++++------- .../DangerousWithTargetAnalyzer.cs | 7 +-- RoslynAnalyzers/RoslynAnalyzers.slnx | 1 - RoslynAnalyzers/build.sh | 4 ++ 10 files changed, 64 insertions(+), 99 deletions(-) create mode 100644 RoslynAnalyzers/.gitignore delete mode 100644 RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Vsix/JonSkeet.RoslynAnalyzers.Vsix.csproj delete mode 100644 RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Vsix/source.extension.vsixmanifest create mode 100644 RoslynAnalyzers/build.sh diff --git a/RoslynAnalyzers/.gitignore b/RoslynAnalyzers/.gitignore new file mode 100644 index 00000000..a7aeaaa1 --- /dev/null +++ b/RoslynAnalyzers/.gitignore @@ -0,0 +1 @@ +*.nupkg diff --git a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Package/JonSkeet.RoslynAnalyzers.Package.csproj b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Package/JonSkeet.RoslynAnalyzers.Package.csproj index 3f4af255..df03bc95 100644 --- a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Package/JonSkeet.RoslynAnalyzers.Package.csproj +++ b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Package/JonSkeet.RoslynAnalyzers.Package.csproj @@ -9,7 +9,7 @@ JonSkeet.RoslynAnalyzers - 1.0.0-beta.5 + 1.0.0-beta.6 Jon Skeet Apache-2.0 https://github.com/jskeet/democode diff --git a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Test/DangerousWithOperatorAnalyzerTest.cs b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Test/DangerousWithOperatorAnalyzerTest.cs index 03b4056a..b1fe9b38 100644 --- a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Test/DangerousWithOperatorAnalyzerTest.cs +++ b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Test/DangerousWithOperatorAnalyzerTest.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis.Testing; using NUnit.Framework; +using System.Threading; using System.Threading.Tasks; using VerifyCS = JonSkeet.RoslynAnalyzers.Test.Verifiers.CSharpAnalyzerVerifier; @@ -102,4 +103,32 @@ static void M() }"; await VerifyCS.VerifyAnalyzerAsync(test); } + + [Test] + public async Task SeparateSourceFiles() + { + var source1 = AttributeDeclaration + @"public record Simple([DangerousWithTarget] int X, int Y);"; + var source2 = @"class Test + { + static void M() + { + Simple s1 = new Simple(10, 10); + Simple s2 = s1 with { X = 20, Y = 20 }; + } + }"; + var diagnostic = new DiagnosticResult(DangerousWithOperatorAnalyzer.Rule) + .WithSpan("/0/Test1.cs", 6, 29, 6, 55) + .WithArguments("X"); + var test = new VerifyCS.Test + { + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + TestState = + { + Sources = { source1, source2 } + }, + ExpectedDiagnostics = { diagnostic } + }; + + await test.RunAsync(CancellationToken.None); + } } diff --git a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Test/DangerousWithTargetAnalyzerTest.cs b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Test/DangerousWithTargetAnalyzerTest.cs index a89cc1ce..08f69e99 100644 --- a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Test/DangerousWithTargetAnalyzerTest.cs +++ b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Test/DangerousWithTargetAnalyzerTest.cs @@ -130,9 +130,6 @@ public async Task AttributedParameter() public int Z { get; } = X * 2; }"; - var diagnostic = new DiagnosticResult(DangerousWithTargetAnalyzer.Rule) - .WithSpan(2, 22, 2, 27) - .WithArguments("X"); await VerifyCS.VerifyAnalyzerAsync(test); } diff --git a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Vsix/JonSkeet.RoslynAnalyzers.Vsix.csproj b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Vsix/JonSkeet.RoslynAnalyzers.Vsix.csproj deleted file mode 100644 index d23a692e..00000000 --- a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Vsix/JonSkeet.RoslynAnalyzers.Vsix.csproj +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - net472 - JonSkeet.RoslynAnalyzers.Vsix - JonSkeet.RoslynAnalyzers.Vsix - - - - false - false - false - false - false - false - Roslyn - - - - - - - - Program - $(DevEnvDir)devenv.exe - /rootsuffix $(VSSDKTargetPlatformRegRootSuffix) - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Vsix/source.extension.vsixmanifest b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Vsix/source.extension.vsixmanifest deleted file mode 100644 index 9ce1efb9..00000000 --- a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Vsix/source.extension.vsixmanifest +++ /dev/null @@ -1,24 +0,0 @@ - - - - - JonSkeet.RoslynAnalyzers - This is a sample diagnostic extension for the .NET Compiler Platform ("Roslyn"). - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers/DangerousWithOperatorAnalyzer.cs b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers/DangerousWithOperatorAnalyzer.cs index bf9af881..d556617c 100644 --- a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers/DangerousWithOperatorAnalyzer.cs +++ b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers/DangerousWithOperatorAnalyzer.cs @@ -34,34 +34,44 @@ private static void AnalyzeWithOperator(SyntaxNodeAnalysisContext context) WithExpressionSyntax syntax = (WithExpressionSyntax) context.Node; var model = context.SemanticModel; var node = context.Node; - var assignedParameters = syntax.Initializer - .Expressions - .Select(exp => model.GetSymbolInfo(((AssignmentExpressionSyntax) exp).Left).Symbol) - .ToList(); - foreach (var assignedParameter in assignedParameters) + var initalizerExpressions = syntax.Initializer.Expressions; + foreach (AssignmentExpressionSyntax initializerExpression in initalizerExpressions) { + var assignedParameter = model.GetSymbolInfo(initializerExpression.Left).Symbol; // The assigned parameter refers to a property, but we need to get at the parameter declaration. - // We check each declaring syntax to see if it's actually declaring a parameter. if (assignedParameter is not IPropertySymbol propertySymbol) { continue; } - foreach (var syntaxReference in propertySymbol.DeclaringSyntaxReferences) + if (IsDangerous(propertySymbol)) { - var declarationSyntax = syntaxReference.GetSyntax(); - if (declarationSyntax is not ParameterSyntax parameterSyntax) - { - continue; - } - var declaredSymbol = model.GetDeclaredSymbol(parameterSyntax); - if (declaredSymbol is IParameterSymbol parameterSymbol && - DangerousWithTargetAnalyzer.HasDangerousWithTargetAttribute(parameterSymbol, model)) + var diagnostic = Diagnostic.Create(Rule, context.Node.GetLocation(), assignedParameter.Name); + context.ReportDiagnostic(diagnostic); + } + } + + bool IsDangerous(IPropertySymbol propertySymbol) + { + // This is awful - there must be a better way of getting the parameter symbol for a record. + // I don't even known how to tell which constructor is the primary constructor... + var containingRecord = propertySymbol.ContainingType; + var constructors = containingRecord.InstanceConstructors; + foreach (var ctor in constructors) + { + foreach (var parameter in ctor.Parameters) { - var diagnostic = Diagnostic.Create(Rule, context.Node.GetLocation(), parameterSymbol.Name); - context.ReportDiagnostic(diagnostic); + if (parameter.Name != propertySymbol.Name) + { + continue; + } + if (DangerousWithTargetAnalyzer.HasDangerousWithTargetAttribute(parameter)) + { + return true; + } } } + return false; } } } diff --git a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers/DangerousWithTargetAnalyzer.cs b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers/DangerousWithTargetAnalyzer.cs index e11229db..076a455d 100644 --- a/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers/DangerousWithTargetAnalyzer.cs +++ b/RoslynAnalyzers/JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers/DangerousWithTargetAnalyzer.cs @@ -57,7 +57,7 @@ private static void AnalyzeRecordDeclaration(SyntaxNodeAnalysisContext context) // Null out any parameters which already have the annotation. for (int i = 0; i < parameterSymbols.Count; i++) { - if (HasDangerousWithTargetAttribute(parameterSymbols[i], model)) + if (HasDangerousWithTargetAttribute(parameterSymbols[i])) { parameterSymbols[i] = null; } @@ -115,9 +115,6 @@ private static void MaybeReportDiagnostic(SyntaxNodeAnalysisContext context, Lis return; } - internal static bool HasDangerousWithTargetAttribute(IParameterSymbol symbol, SemanticModel model) => - symbol.GetAttributes().Any(attr => attr.AttributeClass?.Name == DangerousWithTargetAttributeFullName); - - internal static bool HasDangerousWithTargetAttribute(IPropertySymbol symbol, SemanticModel model) => + internal static bool HasDangerousWithTargetAttribute(ISymbol symbol) => symbol.GetAttributes().Any(attr => attr.AttributeClass?.Name == DangerousWithTargetAttributeFullName); } diff --git a/RoslynAnalyzers/RoslynAnalyzers.slnx b/RoslynAnalyzers/RoslynAnalyzers.slnx index 3356d1ce..11d29b07 100644 --- a/RoslynAnalyzers/RoslynAnalyzers.slnx +++ b/RoslynAnalyzers/RoslynAnalyzers.slnx @@ -2,6 +2,5 @@ - diff --git a/RoslynAnalyzers/build.sh b/RoslynAnalyzers/build.sh new file mode 100644 index 00000000..a156d8c1 --- /dev/null +++ b/RoslynAnalyzers/build.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +dotnet build -c Release RoslynAnalyzers.slnx +dotnet pack -c Release -o $PWD JonSkeet.RoslynAnalyzers/JonSkeet.RoslynAnalyzers.Package From f9d6ea5196350828c696c2cfb7e1a78f48c30b39 Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Sat, 11 Oct 2025 09:59:44 +0100 Subject: [PATCH 06/16] Add NullExtensions for use more broadly (These are copied from election2029.uk) --- WpfUtil/JonSkeet.CoreAppUtil/NullExtensions.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 WpfUtil/JonSkeet.CoreAppUtil/NullExtensions.cs diff --git a/WpfUtil/JonSkeet.CoreAppUtil/NullExtensions.cs b/WpfUtil/JonSkeet.CoreAppUtil/NullExtensions.cs new file mode 100644 index 00000000..2a9e3405 --- /dev/null +++ b/WpfUtil/JonSkeet.CoreAppUtil/NullExtensions.cs @@ -0,0 +1,18 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace JonSkeet.CoreAppUtil; + +#nullable enable + +/// +/// Extension methods to make working with null values (and nullable reference types) simpler. +/// +public static class NullExtensions +{ + public static T OrThrow([NotNull] this T? text, [CallerArgumentExpression(nameof(text))] string? message = null) where T : class => + text ?? throw new InvalidDataException($"No value for '{message}'"); + + public static T OrThrow([NotNull] this T? text, [CallerArgumentExpression(nameof(text))] string? message = null) where T : struct => + text ?? throw new InvalidDataException($"No value for '{message}'"); +} From 51e2d72314ee44259105b0ad48cc606a9c48014b Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Sat, 11 Oct 2025 10:00:34 +0100 Subject: [PATCH 07/16] Support common operations for selectable collections: move next and previous --- .../SelectableCollection.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/WpfUtil/JonSkeet.CoreAppUtil/SelectableCollection.cs b/WpfUtil/JonSkeet.CoreAppUtil/SelectableCollection.cs index 980f2615..ef61b445 100644 --- a/WpfUtil/JonSkeet.CoreAppUtil/SelectableCollection.cs +++ b/WpfUtil/JonSkeet.CoreAppUtil/SelectableCollection.cs @@ -117,6 +117,28 @@ public void MaybeSelectFirst() } } + /// + /// Selects the next item, if there is one. + /// + public void MaybeSelectNext() + { + if (SelectedIndex + 1 < Count) + { + SelectedIndex++; + } + } + + /// + /// Selects the previous item, if there is one. + /// + public void MaybeSelectPrevious() + { + if (SelectedIndex > 0) + { + SelectedIndex--; + } + } + /// /// Clears any existing selection. (This is equivalent to setting /// to -1, or to null.) From 86141b2266df4ffd13ca4f4d7231f80799fabafc Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Sat, 24 May 2025 10:46:06 +0100 Subject: [PATCH 08/16] Initial project setup for Yamaha TF series This is completely non-functional, but means future commits will be about the actual code/protocol. --- .../DigiMixer.AppCore/DigiMixerConfig.cs | 3 + .../DigiMixer.Hardware.csproj | 2 + .../DigiMixer.TfSeries.Core.csproj | 13 ++++ .../DigiMixer.TfSeries.Core/TfMessage.cs | 6 ++ .../DigiMixer.TfSeries.Tools.csproj | 15 ++++ DigiMixer/DigiMixer.TfSeries.Tools/Program.cs | 2 + .../DigiMixer.TfSeries.csproj | 14 ++++ DigiMixer/DigiMixer.TfSeries/TfMixer.cs | 71 +++++++++++++++++++ DigiMixer/DigiMixer.sln | 31 ++++++++ DigiMixer/Protocols/yamaha-tf.md | 5 ++ 10 files changed, 162 insertions(+) create mode 100644 DigiMixer/DigiMixer.TfSeries.Core/DigiMixer.TfSeries.Core.csproj create mode 100644 DigiMixer/DigiMixer.TfSeries.Core/TfMessage.cs create mode 100644 DigiMixer/DigiMixer.TfSeries.Tools/DigiMixer.TfSeries.Tools.csproj create mode 100644 DigiMixer/DigiMixer.TfSeries.Tools/Program.cs create mode 100644 DigiMixer/DigiMixer.TfSeries/DigiMixer.TfSeries.csproj create mode 100644 DigiMixer/DigiMixer.TfSeries/TfMixer.cs create mode 100644 DigiMixer/Protocols/yamaha-tf.md diff --git a/DigiMixer/DigiMixer.AppCore/DigiMixerConfig.cs b/DigiMixer/DigiMixer.AppCore/DigiMixerConfig.cs index a225d201..a8c5a446 100644 --- a/DigiMixer/DigiMixer.AppCore/DigiMixerConfig.cs +++ b/DigiMixer/DigiMixer.AppCore/DigiMixerConfig.cs @@ -5,6 +5,7 @@ using DigiMixer.Mackie; using DigiMixer.Osc; using DigiMixer.QuSeries; +using DigiMixer.TfSeries; using DigiMixer.UCNet; using DigiMixer.UiHttp; using Microsoft.Extensions.Logging; @@ -104,6 +105,7 @@ public IMixerApi CreateMixerApi(ILogger logger, MixerApiOptions options = null) MixerHardwareType.StudioLive => StudioLive.CreateMixerApi(logger, Address, Port ?? 53000, options), MixerHardwareType.AllenHeathCq => CqMixer.CreateMixerApi(logger, Address, Port ?? 51326, options), MixerHardwareType.YamahaDm => DmMixer.CreateMixerApi(logger, Address, Port ?? 50368, options), + MixerHardwareType.YamahaTf => TfMixer.CreateMixerApi(logger, Address, Port ?? 50368, options), MixerHardwareType.BehringerWing => WingMixer.CreateMixerApi(logger, Address, Port ?? 2222, options), _ => throw new InvalidOperationException($"Unknown mixer type: {HardwareType}") }; @@ -126,6 +128,7 @@ public enum MixerHardwareType StudioLive, AllenHeathCq, YamahaDm, + YamahaTf, BehringerWing } } diff --git a/DigiMixer/DigiMixer.Hardware/DigiMixer.Hardware.csproj b/DigiMixer/DigiMixer.Hardware/DigiMixer.Hardware.csproj index e3037925..aa06868e 100644 --- a/DigiMixer/DigiMixer.Hardware/DigiMixer.Hardware.csproj +++ b/DigiMixer/DigiMixer.Hardware/DigiMixer.Hardware.csproj @@ -39,6 +39,8 @@ + + diff --git a/DigiMixer/DigiMixer.TfSeries.Core/DigiMixer.TfSeries.Core.csproj b/DigiMixer/DigiMixer.TfSeries.Core/DigiMixer.TfSeries.Core.csproj new file mode 100644 index 00000000..bb6bad2f --- /dev/null +++ b/DigiMixer/DigiMixer.TfSeries.Core/DigiMixer.TfSeries.Core.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + latest + + + + + + diff --git a/DigiMixer/DigiMixer.TfSeries.Core/TfMessage.cs b/DigiMixer/DigiMixer.TfSeries.Core/TfMessage.cs new file mode 100644 index 00000000..cb54f909 --- /dev/null +++ b/DigiMixer/DigiMixer.TfSeries.Core/TfMessage.cs @@ -0,0 +1,6 @@ +namespace DigiMixer.TfSeries.Core; + +public class TfMessage +{ + +} diff --git a/DigiMixer/DigiMixer.TfSeries.Tools/DigiMixer.TfSeries.Tools.csproj b/DigiMixer/DigiMixer.TfSeries.Tools/DigiMixer.TfSeries.Tools.csproj new file mode 100644 index 00000000..3a220ab6 --- /dev/null +++ b/DigiMixer/DigiMixer.TfSeries.Tools/DigiMixer.TfSeries.Tools.csproj @@ -0,0 +1,15 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + diff --git a/DigiMixer/DigiMixer.TfSeries.Tools/Program.cs b/DigiMixer/DigiMixer.TfSeries.Tools/Program.cs new file mode 100644 index 00000000..3751555c --- /dev/null +++ b/DigiMixer/DigiMixer.TfSeries.Tools/Program.cs @@ -0,0 +1,2 @@ +// See https://aka.ms/new-console-template for more information +Console.WriteLine("Hello, World!"); diff --git a/DigiMixer/DigiMixer.TfSeries/DigiMixer.TfSeries.csproj b/DigiMixer/DigiMixer.TfSeries/DigiMixer.TfSeries.csproj new file mode 100644 index 00000000..986fe01d --- /dev/null +++ b/DigiMixer/DigiMixer.TfSeries/DigiMixer.TfSeries.csproj @@ -0,0 +1,14 @@ + + + + net9.0 + enable + enable + latest + + + + + + + diff --git a/DigiMixer/DigiMixer.TfSeries/TfMixer.cs b/DigiMixer/DigiMixer.TfSeries/TfMixer.cs new file mode 100644 index 00000000..0461be7a --- /dev/null +++ b/DigiMixer/DigiMixer.TfSeries/TfMixer.cs @@ -0,0 +1,71 @@ +using DigiMixer.Core; +using Microsoft.Extensions.Logging; + +namespace DigiMixer.TfSeries; + +public static class TfMixer +{ + public static IMixerApi CreateMixerApi(ILogger logger, string host, int port = 50368, MixerApiOptions? options = null) => + new TfMixerApi(logger, host, port, options); +} + +internal class TfMixerApi : IMixerApi +{ + public TfMixerApi(ILogger logger, string host, int port, MixerApiOptions? options) + { + } + + public TimeSpan KeepAliveInterval => throw new NotImplementedException(); + + public IFaderScale FaderScale => throw new NotImplementedException(); + + public Task CheckConnection(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task Connect(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task DetectConfiguration(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + throw new NotImplementedException(); + } + + public void RegisterReceiver(IMixerReceiver receiver) + { + throw new NotImplementedException(); + } + + public Task RequestAllData(IReadOnlyList channelIds) + { + throw new NotImplementedException(); + } + + public Task SendKeepAlive() + { + throw new NotImplementedException(); + } + + public Task SetFaderLevel(ChannelId inputId, ChannelId outputId, FaderLevel level) + { + throw new NotImplementedException(); + } + + public Task SetFaderLevel(ChannelId outputId, FaderLevel level) + { + throw new NotImplementedException(); + } + + public Task SetMuted(ChannelId channelId, bool muted) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DigiMixer/DigiMixer.sln b/DigiMixer/DigiMixer.sln index 94388aa3..320b28cb 100644 --- a/DigiMixer/DigiMixer.sln +++ b/DigiMixer/DigiMixer.sln @@ -37,6 +37,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Protocols", "Protocols", "{ ProjectSection(SolutionItems) = preProject Protocols\behringer-wing.md = Protocols\behringer-wing.md Protocols\mackie.md = Protocols\mackie.md + Protocols\yamaha-tf.md = Protocols\yamaha-tf.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.Ssc", "DigiMixer.Ssc\DigiMixer.Ssc.csproj", "{6C3761B7-E526-4DE8-9EA8-9B3F67EDFA11}" @@ -87,6 +88,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.Hardware", "DigiM EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.BehringerWing.WingExplorer", "DigiMixer.BehringerWing.WingExplorer\DigiMixer.BehringerWing.WingExplorer.csproj", "{20705AD5-B614-413D-9BB9-8308A958E4F4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.TfSeries", "DigiMixer.TfSeries\DigiMixer.TfSeries.csproj", "{603E2343-6C02-4A68-A434-BC029AB6F788}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.TfSeries.Core", "DigiMixer.TfSeries.Core\DigiMixer.TfSeries.Core.csproj", "{7E0C4891-63AD-4E59-9717-402DDEAA585A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.TfSeries.Tools", "DigiMixer.TfSeries.Tools\DigiMixer.TfSeries.Tools.csproj", "{4A66EF70-036A-4AA3-BE57-D56645FEE3CD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -407,6 +414,30 @@ Global {20705AD5-B614-413D-9BB9-8308A958E4F4}.Release|Any CPU.Build.0 = Release|Any CPU {20705AD5-B614-413D-9BB9-8308A958E4F4}.Release|x64.ActiveCfg = Release|Any CPU {20705AD5-B614-413D-9BB9-8308A958E4F4}.Release|x64.Build.0 = Release|Any CPU + {603E2343-6C02-4A68-A434-BC029AB6F788}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {603E2343-6C02-4A68-A434-BC029AB6F788}.Debug|Any CPU.Build.0 = Debug|Any CPU + {603E2343-6C02-4A68-A434-BC029AB6F788}.Debug|x64.ActiveCfg = Debug|Any CPU + {603E2343-6C02-4A68-A434-BC029AB6F788}.Debug|x64.Build.0 = Debug|Any CPU + {603E2343-6C02-4A68-A434-BC029AB6F788}.Release|Any CPU.ActiveCfg = Release|Any CPU + {603E2343-6C02-4A68-A434-BC029AB6F788}.Release|Any CPU.Build.0 = Release|Any CPU + {603E2343-6C02-4A68-A434-BC029AB6F788}.Release|x64.ActiveCfg = Release|Any CPU + {603E2343-6C02-4A68-A434-BC029AB6F788}.Release|x64.Build.0 = Release|Any CPU + {7E0C4891-63AD-4E59-9717-402DDEAA585A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7E0C4891-63AD-4E59-9717-402DDEAA585A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7E0C4891-63AD-4E59-9717-402DDEAA585A}.Debug|x64.ActiveCfg = Debug|Any CPU + {7E0C4891-63AD-4E59-9717-402DDEAA585A}.Debug|x64.Build.0 = Debug|Any CPU + {7E0C4891-63AD-4E59-9717-402DDEAA585A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7E0C4891-63AD-4E59-9717-402DDEAA585A}.Release|Any CPU.Build.0 = Release|Any CPU + {7E0C4891-63AD-4E59-9717-402DDEAA585A}.Release|x64.ActiveCfg = Release|Any CPU + {7E0C4891-63AD-4E59-9717-402DDEAA585A}.Release|x64.Build.0 = Release|Any CPU + {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Debug|x64.ActiveCfg = Debug|Any CPU + {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Debug|x64.Build.0 = Debug|Any CPU + {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Release|Any CPU.Build.0 = Release|Any CPU + {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Release|x64.ActiveCfg = Release|Any CPU + {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/DigiMixer/Protocols/yamaha-tf.md b/DigiMixer/Protocols/yamaha-tf.md new file mode 100644 index 00000000..e65a834b --- /dev/null +++ b/DigiMixer/Protocols/yamaha-tf.md @@ -0,0 +1,5 @@ +# Yamaha TF Rack protocol + +Currently looks very similar to the DM protocol... but rather than assume this, +I'll implement it separately (copying code where appropriate), then see if I +can combine the two and abstract any differences. \ No newline at end of file From e46191fee22c92c34026ade7e6e26daedbdcb20d Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Sun, 25 May 2025 16:30:39 +0100 Subject: [PATCH 09/16] Refactoring for range operators etc --- DigiMixer/DigiMixer.Diagnostics/IPV4Packet.cs | 20 ++++++++++++----- DigiMixer/DigiMixer.Diagnostics/Tool.cs | 2 +- .../DmBinarySegment.cs | 6 ++--- .../DigiMixer.DmSeries.Core/DmMessage.cs | 22 +++++++++---------- .../DmUInt32Segment.cs | 10 ++++----- .../DmMessageExtensions.cs | 2 +- .../MmixFullDataDiff.cs | 2 +- DigiMixer/DigiMixer.DmSeries/DmMessages.cs | 4 ++-- DigiMixer/DigiMixer.DmSeries/DmMixer.cs | 4 ++-- .../FullChannelDataMessage.cs | 2 +- DigiMixer/DigiMixer.Mackie.Tools/Message.cs | 2 +- 11 files changed, 43 insertions(+), 33 deletions(-) diff --git a/DigiMixer/DigiMixer.Diagnostics/IPV4Packet.cs b/DigiMixer/DigiMixer.Diagnostics/IPV4Packet.cs index 8c46ce43..d1b00b52 100644 --- a/DigiMixer/DigiMixer.Diagnostics/IPV4Packet.cs +++ b/DigiMixer/DigiMixer.Diagnostics/IPV4Packet.cs @@ -25,6 +25,10 @@ private IPV4Packet(ProtocolType type, IPEndPoint source, IPEndPoint dest, byte[] this.dataOffset = offset; this.dataLength = length; Timestamp = timestamp; + if (data.Length < offset + length || offset < 0 || length < 0) + { + throw new ArgumentOutOfRangeException($"Invalid data/length/offset. Data length: {data.Length}; offset: {offset}; length: {length}; type={type}"); + } } public static IPV4Packet? TryConvert(BlockBase block) @@ -44,12 +48,12 @@ private IPV4Packet(ProtocolType type, IPEndPoint source, IPEndPoint dest, byte[] { return null; } - var ipLength = BinaryPrimitives.ReadUInt16BigEndian(dataSpan.Slice(16)); + var ipLength = BinaryPrimitives.ReadUInt16BigEndian(dataSpan[16..]); var type = (ProtocolType) data[23]; - IPAddress sourceAddress = new IPAddress(dataSpan.Slice(26, 4)); - IPAddress destAddress = new IPAddress(dataSpan.Slice(30, 4)); - int sourcePort = BinaryPrimitives.ReadUInt16BigEndian(dataSpan.Slice(34)); - int destPort = BinaryPrimitives.ReadUInt16BigEndian(dataSpan.Slice(36)); + IPAddress sourceAddress = new(dataSpan.Slice(26, 4)); + IPAddress destAddress = new(dataSpan.Slice(30, 4)); + int sourcePort = BinaryPrimitives.ReadUInt16BigEndian(dataSpan[34..]); + int destPort = BinaryPrimitives.ReadUInt16BigEndian(dataSpan[36..]); int dataOffset; int dataLength; @@ -63,6 +67,12 @@ private IPV4Packet(ProtocolType type, IPEndPoint source, IPEndPoint dest, byte[] int headerLength = (data[46] & 0xf0) >> 2; dataOffset = 34 + headerLength; dataLength = ipLength - (dataOffset - 14); + + // Handle TCP segmentation offload + if (ipLength == 0) + { + dataLength = data.Length - dataOffset; + } } else { diff --git a/DigiMixer/DigiMixer.Diagnostics/Tool.cs b/DigiMixer/DigiMixer.Diagnostics/Tool.cs index a14c226a..758355fa 100644 --- a/DigiMixer/DigiMixer.Diagnostics/Tool.cs +++ b/DigiMixer/DigiMixer.Diagnostics/Tool.cs @@ -37,7 +37,7 @@ public static async Task ExecuteFromCommandLine(string[] args, Type typeInA } return 1; } - var tool = (Tool) ctor.Invoke(args.Skip(1).ToArray()); + var tool = (Tool) ctor.Invoke([..args.Skip(1)]); return await tool.Execute(); } diff --git a/DigiMixer/DigiMixer.DmSeries.Core/DmBinarySegment.cs b/DigiMixer/DigiMixer.DmSeries.Core/DmBinarySegment.cs index fdeede2f..d7900889 100644 --- a/DigiMixer/DigiMixer.DmSeries.Core/DmBinarySegment.cs +++ b/DigiMixer/DigiMixer.DmSeries.Core/DmBinarySegment.cs @@ -42,13 +42,13 @@ public static DmBinarySegment FromHex(string text) public override void WriteTo(Span buffer) { buffer[0] = (byte) Format; - BinaryPrimitives.WriteInt32BigEndian(buffer.Slice(1), data.Length); - Data.CopyTo(buffer.Slice(5)); + BinaryPrimitives.WriteInt32BigEndian(buffer[1..], data.Length); + Data.CopyTo(buffer[5..]); } public static DmBinarySegment Parse(ReadOnlySpan buffer) { - var dataLength = BinaryPrimitives.ReadInt32BigEndian(buffer.Slice(1)); + var dataLength = BinaryPrimitives.ReadInt32BigEndian(buffer[1..]); var bytes = new byte[dataLength]; buffer.Slice(5, dataLength).CopyTo(bytes); return new DmBinarySegment(bytes); diff --git a/DigiMixer/DigiMixer.DmSeries.Core/DmMessage.cs b/DigiMixer/DigiMixer.DmSeries.Core/DmMessage.cs index 3cbd614e..7a42674b 100644 --- a/DigiMixer/DigiMixer.DmSeries.Core/DmMessage.cs +++ b/DigiMixer/DigiMixer.DmSeries.Core/DmMessage.cs @@ -51,14 +51,14 @@ public DmMessage(string type, uint flags, ImmutableList segments) { throw new InvalidDataException("Expected overall container with format 0x11"); } - var containerLength = BinaryPrimitives.ReadInt32BigEndian(body.Slice(1)); + var containerLength = BinaryPrimitives.ReadInt32BigEndian(body[1..]); if (containerLength != bodyLength - 5) { throw new InvalidDataException($"Expected overall container internal length {bodyLength - 5}; was {containerLength}"); } var segments = new List(); - var flags = BinaryPrimitives.ReadUInt32BigEndian(body.Slice(5)); - var nextSegmentData = body.Slice(9); + var flags = BinaryPrimitives.ReadUInt32BigEndian(body[5..]); + var nextSegmentData = body[9..]; while (nextSegmentData.Length > 0) { var format = (DmSegmentFormat) nextSegmentData[0]; @@ -72,26 +72,26 @@ public DmMessage(string type, uint flags, ImmutableList segments) _ => throw new InvalidDataException($"Unexpected segment format {nextSegmentData[0]:x2}") }; segments.Add(segment); - nextSegmentData = nextSegmentData.Slice(segment.Length); + nextSegmentData = nextSegmentData[segment.Length..]; } - return new DmMessage(type, flags, segments.ToImmutableList()); + return new DmMessage(type, flags, [.. segments]); } public void CopyTo(Span buffer) { // Note: we assume the span is right-sized to Length. Encoding.ASCII.GetBytes(Type, buffer); - BinaryPrimitives.WriteInt32BigEndian(buffer.Slice(4), buffer.Length - 8); + BinaryPrimitives.WriteInt32BigEndian(buffer[4..], buffer.Length - 8); buffer[8] = (byte) DmSegmentFormat.Binary; - BinaryPrimitives.WriteInt32BigEndian(buffer.Slice(9), buffer.Length - 13); - BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(13), Flags); - buffer = buffer.Slice(17); + BinaryPrimitives.WriteInt32BigEndian(buffer[9..], buffer.Length - 13); + BinaryPrimitives.WriteUInt32BigEndian(buffer[13..], Flags); + buffer = buffer[17..]; foreach (var segment in Segments) { segment.WriteTo(buffer); - buffer = buffer.Slice(segment.Length); + buffer = buffer[segment.Length..]; } } - public override string ToString() => $"{Type.PadRight(4)}: Flags={Flags:x8}; Segments={Segments.Count}"; + public override string ToString() => $"{Type,-4}: Flags={Flags:x8}; Segments={Segments.Count}"; } diff --git a/DigiMixer/DigiMixer.DmSeries.Core/DmUInt32Segment.cs b/DigiMixer/DigiMixer.DmSeries.Core/DmUInt32Segment.cs index 123c3502..5263d237 100644 --- a/DigiMixer/DigiMixer.DmSeries.Core/DmUInt32Segment.cs +++ b/DigiMixer/DigiMixer.DmSeries.Core/DmUInt32Segment.cs @@ -19,21 +19,21 @@ public DmUInt32Segment(ImmutableList values) public override void WriteTo(Span buffer) { buffer[0] = (byte) Format; - BinaryPrimitives.WriteInt32BigEndian(buffer.Slice(1), Values.Count); + BinaryPrimitives.WriteInt32BigEndian(buffer[1..], Values.Count); for (int i = 0; i < Values.Count; i++) { - BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(5 + i * 4), Values[i]); + BinaryPrimitives.WriteUInt32BigEndian(buffer[(5 + i * 4)..], Values[i]); } } public static DmUInt32Segment Parse(ReadOnlySpan buffer) { - var valueCount = BinaryPrimitives.ReadInt32BigEndian(buffer.Slice(1)); + var valueCount = BinaryPrimitives.ReadInt32BigEndian(buffer[1..]); var values = new uint[valueCount]; for (int i = 0; i < valueCount; i++) { - values[i] = BinaryPrimitives.ReadUInt32BigEndian(buffer.Slice(5 + i * 4)); + values[i] = BinaryPrimitives.ReadUInt32BigEndian(buffer[(5 + i * 4)..]); } - return new DmUInt32Segment(values.ToImmutableList()); + return new DmUInt32Segment([.. values]); } } diff --git a/DigiMixer/DigiMixer.DmSeries.Tools/DmMessageExtensions.cs b/DigiMixer/DigiMixer.DmSeries.Tools/DmMessageExtensions.cs index 548eba05..03bbcc71 100644 --- a/DigiMixer/DigiMixer.DmSeries.Tools/DmMessageExtensions.cs +++ b/DigiMixer/DigiMixer.DmSeries.Tools/DmMessageExtensions.cs @@ -36,7 +36,7 @@ static string DescribeSegment(DmSegment segment) case DmBinarySegment binary: var data = binary.Data; var hexLength = Math.Min(data.Length, 16); - var hex = Formatting.ToHex(data.Slice(0, hexLength)) + (hexLength == data.Length ? "" : " [...]"); + var hex = Formatting.ToHex(data[..hexLength]) + (hexLength == data.Length ? "" : " [...]"); return $"Binary[{data.Length}]: {hex}"; case DmTextSegment text: return $"Text: '{text.Text}'"; diff --git a/DigiMixer/DigiMixer.DmSeries.Tools/MmixFullDataDiff.cs b/DigiMixer/DigiMixer.DmSeries.Tools/MmixFullDataDiff.cs index 2fb4d37c..7f313b73 100644 --- a/DigiMixer/DigiMixer.DmSeries.Tools/MmixFullDataDiff.cs +++ b/DigiMixer/DigiMixer.DmSeries.Tools/MmixFullDataDiff.cs @@ -38,7 +38,7 @@ private static void DiffSnapshot(byte[]? currentSnapshot, byte[] newSnapshot) return; } - List differences = new(); + List differences = []; for (int i = 0; i < currentSnapshot.Length; i++) { if (newSnapshot[i] != currentSnapshot[i]) diff --git a/DigiMixer/DigiMixer.DmSeries/DmMessages.cs b/DigiMixer/DigiMixer.DmSeries/DmMessages.cs index 3cf80adc..7ba51d5a 100644 --- a/DigiMixer/DigiMixer.DmSeries/DmMessages.cs +++ b/DigiMixer/DigiMixer.DmSeries/DmMessages.cs @@ -35,10 +35,10 @@ public static class Subtypes public static DmBinarySegment Empty16ByteBinarySegment { get; } = new DmBinarySegment(new byte[16]); - public static DmMessage RequestData(string type, string subtype) => new DmMessage( + public static DmMessage RequestData(string type, string subtype) => new( type, flags: 0x01010102, [new DmTextSegment(subtype), new DmBinarySegment([0x80])]); - public static DmMessage UnrequestData(string type, string subtype) => new DmMessage( + public static DmMessage UnrequestData(string type, string subtype) => new( type, flags: 0x01010102, [new DmTextSegment(subtype), new DmBinarySegment([0x00])]); internal static bool IsKeepAlive(DmMessage message) => diff --git a/DigiMixer/DigiMixer.DmSeries/DmMixer.cs b/DigiMixer/DigiMixer.DmSeries/DmMixer.cs index 6dc31ff6..cf8d1fc2 100644 --- a/DigiMixer/DigiMixer.DmSeries/DmMixer.cs +++ b/DigiMixer/DigiMixer.DmSeries/DmMixer.cs @@ -86,11 +86,11 @@ public async Task DetectConfiguration(CancellationTok { throw new InvalidOperationException("Cannot detect configuration until connected"); } - var data = await fullDataTask; + await fullDataTask; // TODO: Lots more! Including the stereo flags... we may not get everything we need here. var inputs = DmChannels.AllInputs; var outputs = DmChannels.AllOutputs; - return new MixerChannelConfiguration(inputs, outputs, new[] { StereoPair.FromLeft(ChannelId.MainOutputLeft, StereoFlags.FullyIndependent) }); + return new MixerChannelConfiguration(inputs, outputs, [StereoPair.FromLeft(ChannelId.MainOutputLeft, StereoFlags.FullyIndependent)]); } public void Dispose() diff --git a/DigiMixer/DigiMixer.DmSeries/FullChannelDataMessage.cs b/DigiMixer/DigiMixer.DmSeries/FullChannelDataMessage.cs index f67bf815..4f239108 100644 --- a/DigiMixer/DigiMixer.DmSeries/FullChannelDataMessage.cs +++ b/DigiMixer/DigiMixer.DmSeries/FullChannelDataMessage.cs @@ -16,7 +16,7 @@ internal FullChannelDataMessage(DmMessage message) data = (DmBinarySegment) message.Segments[7]; } - private int GetStartOffset(ChannelId channel) => channel switch + private static int GetStartOffset(ChannelId channel) => channel switch { { IsInput: true, Value: int ch } => (ch - 1) * 0x1cf + 0x6088, { IsMainOutput: true, Value: int ch } => (ch - ChannelId.MainOutputLeft.Value) * 0x190 + 0x959e, diff --git a/DigiMixer/DigiMixer.Mackie.Tools/Message.cs b/DigiMixer/DigiMixer.Mackie.Tools/Message.cs index da8b07f3..1108a4c9 100644 --- a/DigiMixer/DigiMixer.Mackie.Tools/Message.cs +++ b/DigiMixer/DigiMixer.Mackie.Tools/Message.cs @@ -6,7 +6,7 @@ namespace DigiMixer.Mackie.Tools; public partial class Message { public static Message FromMackieMessage(MackieMessage message, bool outbound, DateTimeOffset? timestamp) => - new Message + new() { Outbound = outbound, Command = (int) message.Command, From 9bdd6405ccd9ac5cc57e4d43814a52adae411d18 Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Sun, 25 May 2025 16:31:09 +0100 Subject: [PATCH 10/16] Tooling to process messages directly from Wireshark files --- .../DigiMixer.Diagnostics/AnnotatedMessage.cs | 13 +++++ DigiMixer/DigiMixer.Diagnostics/Hex.cs | 15 +++++ .../DigiMixer.Diagnostics/MessageDirection.cs | 7 +++ .../DigiMixer.Diagnostics/NullExtensions.cs | 16 ++++++ .../DigiMixer.Diagnostics/WiresharkDump.cs | 57 +++++++++++++++++-- 5 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 DigiMixer/DigiMixer.Diagnostics/AnnotatedMessage.cs create mode 100644 DigiMixer/DigiMixer.Diagnostics/MessageDirection.cs create mode 100644 DigiMixer/DigiMixer.Diagnostics/NullExtensions.cs diff --git a/DigiMixer/DigiMixer.Diagnostics/AnnotatedMessage.cs b/DigiMixer/DigiMixer.Diagnostics/AnnotatedMessage.cs new file mode 100644 index 00000000..4d226599 --- /dev/null +++ b/DigiMixer/DigiMixer.Diagnostics/AnnotatedMessage.cs @@ -0,0 +1,13 @@ +using DigiMixer.Core; +using System.Net; + +namespace DigiMixer.Diagnostics; + +public record AnnotatedMessage( + TMessage Message, + DateTimeOffset Timestamp, + MessageDirection Direction, + IPAddress SourceAddress, + IPAddress DestinationAddress) where TMessage : class, IMixerMessage +{ +} diff --git a/DigiMixer/DigiMixer.Diagnostics/Hex.cs b/DigiMixer/DigiMixer.Diagnostics/Hex.cs index 93b93a62..3d7a12a7 100644 --- a/DigiMixer/DigiMixer.Diagnostics/Hex.cs +++ b/DigiMixer/DigiMixer.Diagnostics/Hex.cs @@ -1,4 +1,5 @@ using DigiMixer.Core; +using System.Data; using System.Text; namespace DigiMixer.Diagnostics; @@ -61,6 +62,20 @@ public static HexDumpLine ParseHexDumpLine(string line) return new HexDumpLine(direction, offset, data); } + /// + /// Parses hex values, ignoring any spaces. + /// + public static byte[] ParseHex(string text) + { + text = text.Replace(" ", ""); + byte[] data = new byte[text.Length / 2]; + for (int i = 0; i < data.Length; i++) + { + data[i] = Convert.ToByte(text[(i * 2)..(i * 2 + 2)], 16); + } + return data; + } + public record HexDumpLine(Direction Direction, ulong Offset, byte[] Data) { public override string ToString() => diff --git a/DigiMixer/DigiMixer.Diagnostics/MessageDirection.cs b/DigiMixer/DigiMixer.Diagnostics/MessageDirection.cs new file mode 100644 index 00000000..a24a753a --- /dev/null +++ b/DigiMixer/DigiMixer.Diagnostics/MessageDirection.cs @@ -0,0 +1,7 @@ +namespace DigiMixer.Diagnostics; + +public enum MessageDirection +{ + ClientToMixer = 0, + MixerToClient = 1 +} diff --git a/DigiMixer/DigiMixer.Diagnostics/NullExtensions.cs b/DigiMixer/DigiMixer.Diagnostics/NullExtensions.cs new file mode 100644 index 00000000..c5e682ec --- /dev/null +++ b/DigiMixer/DigiMixer.Diagnostics/NullExtensions.cs @@ -0,0 +1,16 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace DigiMixer.Diagnostics; + +/// +/// Extension methods to make working with null values (and nullable reference types) simpler. +/// +public static class NullExtensions +{ + public static T OrThrow([NotNull] this T? text, [CallerArgumentExpression(nameof(text))] string? message = null) where T : class => + text ?? throw new InvalidDataException($"No value for '{message}'"); + + public static T OrThrow([NotNull] this T? text, [CallerArgumentExpression(nameof(text))] string? message = null) where T : struct => + text ?? throw new InvalidDataException($"No value for '{message}'"); +} diff --git a/DigiMixer/DigiMixer.Diagnostics/WiresharkDump.cs b/DigiMixer/DigiMixer.Diagnostics/WiresharkDump.cs index e7af65fc..7e5b1994 100644 --- a/DigiMixer/DigiMixer.Diagnostics/WiresharkDump.cs +++ b/DigiMixer/DigiMixer.Diagnostics/WiresharkDump.cs @@ -1,4 +1,9 @@ -using PcapngFile; +using DigiMixer.Core; +using PcapngFile; +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Net; +using System.Net.Sockets; namespace DigiMixer.Diagnostics; @@ -15,10 +20,54 @@ private WiresharkDump(IReadOnlyList blocks) public static WiresharkDump Load(string filename) { - using (var reader = new Reader(filename)) + using var reader = new Reader(filename); + var blocks = reader.AllBlocks.ToList(); + return new WiresharkDump(blocks); + } + + public async Task>> ProcessMessages(string mixerIpAddress, string clientIpAddress) where TMessage : class, IMixerMessage + { + // Currently we assume a single client, a single mixer, and a single stream of messages between the two. + IPAddress clientAddr = IPAddress.Parse(clientIpAddress); + IPAddress mixerAddr = IPAddress.Parse(mixerIpAddress); + + DateTime? currentTimestamp = null; + IPAddress? currentSource = null; + IPAddress? currentDestination = null; + + List> messages = []; + + var outboundProcessor = new MessageProcessor(AddMessage, 1024 * 1024); + var inboundProcessor = new MessageProcessor(AddMessage, 1024 * 1024); + + foreach (var packet in IPV4Packets) + { + if (packet.Type != ProtocolType.Tcp) + { + continue; + } + + currentTimestamp = packet.Timestamp; + currentSource = packet.Source.Address; + currentDestination = packet.Dest.Address; + if (packet.Source.Address.Equals(clientAddr) && packet.Dest.Address.Equals(mixerAddr)) + { + await outboundProcessor.Process(packet.Data, default); + } + else if (packet.Source.Address.Equals(mixerAddr) && packet.Dest.Address.Equals(clientAddr)) + { + await inboundProcessor.Process(packet.Data, default); + } + } + return [.. messages]; + + void AddMessage(TMessage message) { - var blocks = reader.AllBlocks.ToList(); - return new WiresharkDump(blocks); + var direction = currentSource.OrThrow().Equals(clientAddr) ? + MessageDirection.ClientToMixer : MessageDirection.MixerToClient; + var annotated = new AnnotatedMessage(message, currentTimestamp.OrThrow(), direction, + currentSource.OrThrow(), currentDestination.OrThrow()); + messages.Add(annotated); } } } From 2db558b0b6781e6b2859a1baf2e3237179e8c98d Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Tue, 27 May 2025 21:09:25 +0100 Subject: [PATCH 11/16] First pass of common DM.Yamaha.Core and DM.Yamaha projects This includes parsing the MMSXLIT format (which we now understand). --- .../DigiMixer.Diagnostics/AnnotatedMessage.cs | 1 + DigiMixer/DigiMixer.Diagnostics/IPV4Packet.cs | 5 + .../DigiMixer.Diagnostics/WiresharkDump.cs | 11 +- .../DigiMixer.Hardware.csproj | 3 +- .../DigiMixer.TfSeries.Core/TfMessage.cs | 6 - .../ClientInitialMessages.cs | 66 +++++++++++ .../ConvertAllWireshark.cs | 20 ++++ .../ConvertWireshark.cs | 23 ++++ .../DecodingOptions.cs | 7 ++ DigiMixer/DigiMixer.TfSeries.Tools/Program.cs | 5 +- .../RequestFullData.cs | 41 +++++++ .../YamahaMessageExtensions.cs | 109 +++++++++++++++++ .../DigiMixer.TfSeries.csproj | 2 +- DigiMixer/DigiMixer.TfSeries/TfMessages.cs | 7 ++ DigiMixer/DigiMixer.TfSeries/TfMixer.cs | 82 ++++++++++++- .../DigiMixer.Yamaha.Core.csproj | 12 ++ .../YamahaBinarySegment.cs | 57 +++++++++ .../DigiMixer.Yamaha.Core/YamahaClient.cs | 14 +++ .../YamahaInt32Segment.cs | 32 +++++ .../DigiMixer.Yamaha.Core/YamahaMessage.cs | 112 ++++++++++++++++++ .../YamahaMessageType.cs | 74 ++++++++++++ .../DigiMixer.Yamaha.Core/YamahaMessages.cs | 17 +++ .../DigiMixer.Yamaha.Core/YamahaSegment.cs | 18 +++ .../YamahaSegmentFormat.cs | 10 ++ .../YamahaTextSegment.cs | 31 +++++ .../YamahaUInt16Segment.cs | 32 +++++ .../YamahaUInt32Segment.cs | 32 +++++ DigiMixer/DigiMixer.Yamaha/DataSubtypes.cs | 9 ++ .../DigiMixer.Yamaha.csproj} | 4 +- .../DigiMixer.Yamaha/KeepAliveMessage.cs | 27 +++++ DigiMixer/DigiMixer.Yamaha/SchemaCol.cs | 80 +++++++++++++ DigiMixer/DigiMixer.Yamaha/SchemaProperty.cs | 58 +++++++++ .../DigiMixer.Yamaha/SchemaPropertyType.cs | 8 ++ .../DigiMixer.Yamaha/SectionSchemaAndData.cs | 38 ++++++ .../SectionSchemaAndDataMessage.cs | 46 +++++++ .../DigiMixer.Yamaha/SyncHashesMessage.cs | 35 ++++++ DigiMixer/DigiMixer.Yamaha/WrappedMessage.cs | 16 +++ DigiMixer/DigiMixer.Yamaha/YamahaMessages.cs | 18 +++ DigiMixer/DigiMixer.sln | 30 +++-- DigiMixer/Protocols/yamaha-tf.md | 9 +- 40 files changed, 1174 insertions(+), 33 deletions(-) delete mode 100644 DigiMixer/DigiMixer.TfSeries.Core/TfMessage.cs create mode 100644 DigiMixer/DigiMixer.TfSeries.Tools/ClientInitialMessages.cs create mode 100644 DigiMixer/DigiMixer.TfSeries.Tools/ConvertAllWireshark.cs create mode 100644 DigiMixer/DigiMixer.TfSeries.Tools/ConvertWireshark.cs create mode 100644 DigiMixer/DigiMixer.TfSeries.Tools/DecodingOptions.cs create mode 100644 DigiMixer/DigiMixer.TfSeries.Tools/RequestFullData.cs create mode 100644 DigiMixer/DigiMixer.TfSeries.Tools/YamahaMessageExtensions.cs create mode 100644 DigiMixer/DigiMixer.TfSeries/TfMessages.cs create mode 100644 DigiMixer/DigiMixer.Yamaha.Core/DigiMixer.Yamaha.Core.csproj create mode 100644 DigiMixer/DigiMixer.Yamaha.Core/YamahaBinarySegment.cs create mode 100644 DigiMixer/DigiMixer.Yamaha.Core/YamahaClient.cs create mode 100644 DigiMixer/DigiMixer.Yamaha.Core/YamahaInt32Segment.cs create mode 100644 DigiMixer/DigiMixer.Yamaha.Core/YamahaMessage.cs create mode 100644 DigiMixer/DigiMixer.Yamaha.Core/YamahaMessageType.cs create mode 100644 DigiMixer/DigiMixer.Yamaha.Core/YamahaMessages.cs create mode 100644 DigiMixer/DigiMixer.Yamaha.Core/YamahaSegment.cs create mode 100644 DigiMixer/DigiMixer.Yamaha.Core/YamahaSegmentFormat.cs create mode 100644 DigiMixer/DigiMixer.Yamaha.Core/YamahaTextSegment.cs create mode 100644 DigiMixer/DigiMixer.Yamaha.Core/YamahaUInt16Segment.cs create mode 100644 DigiMixer/DigiMixer.Yamaha.Core/YamahaUInt32Segment.cs create mode 100644 DigiMixer/DigiMixer.Yamaha/DataSubtypes.cs rename DigiMixer/{DigiMixer.TfSeries.Core/DigiMixer.TfSeries.Core.csproj => DigiMixer.Yamaha/DigiMixer.Yamaha.csproj} (67%) create mode 100644 DigiMixer/DigiMixer.Yamaha/KeepAliveMessage.cs create mode 100644 DigiMixer/DigiMixer.Yamaha/SchemaCol.cs create mode 100644 DigiMixer/DigiMixer.Yamaha/SchemaProperty.cs create mode 100644 DigiMixer/DigiMixer.Yamaha/SchemaPropertyType.cs create mode 100644 DigiMixer/DigiMixer.Yamaha/SectionSchemaAndData.cs create mode 100644 DigiMixer/DigiMixer.Yamaha/SectionSchemaAndDataMessage.cs create mode 100644 DigiMixer/DigiMixer.Yamaha/SyncHashesMessage.cs create mode 100644 DigiMixer/DigiMixer.Yamaha/WrappedMessage.cs create mode 100644 DigiMixer/DigiMixer.Yamaha/YamahaMessages.cs diff --git a/DigiMixer/DigiMixer.Diagnostics/AnnotatedMessage.cs b/DigiMixer/DigiMixer.Diagnostics/AnnotatedMessage.cs index 4d226599..27713d71 100644 --- a/DigiMixer/DigiMixer.Diagnostics/AnnotatedMessage.cs +++ b/DigiMixer/DigiMixer.Diagnostics/AnnotatedMessage.cs @@ -7,6 +7,7 @@ public record AnnotatedMessage( TMessage Message, DateTimeOffset Timestamp, MessageDirection Direction, + int StreamOffset, IPAddress SourceAddress, IPAddress DestinationAddress) where TMessage : class, IMixerMessage { diff --git a/DigiMixer/DigiMixer.Diagnostics/IPV4Packet.cs b/DigiMixer/DigiMixer.Diagnostics/IPV4Packet.cs index d1b00b52..7abadf15 100644 --- a/DigiMixer/DigiMixer.Diagnostics/IPV4Packet.cs +++ b/DigiMixer/DigiMixer.Diagnostics/IPV4Packet.cs @@ -61,6 +61,11 @@ private IPV4Packet(ProtocolType type, IPEndPoint source, IPEndPoint dest, byte[] { dataOffset = 42; dataLength = BinaryPrimitives.ReadUInt16BigEndian(dataSpan[38..]) - 8; // The header includes its own length + // Ignore fragmented packets + if (data.Length < dataOffset + dataLength) + { + return null; + } } else if (type == ProtocolType.Tcp) { diff --git a/DigiMixer/DigiMixer.Diagnostics/WiresharkDump.cs b/DigiMixer/DigiMixer.Diagnostics/WiresharkDump.cs index 7e5b1994..7bbdf9d8 100644 --- a/DigiMixer/DigiMixer.Diagnostics/WiresharkDump.cs +++ b/DigiMixer/DigiMixer.Diagnostics/WiresharkDump.cs @@ -37,8 +37,10 @@ public async Task>> ProcessMessages> messages = []; - var outboundProcessor = new MessageProcessor(AddMessage, 1024 * 1024); - var inboundProcessor = new MessageProcessor(AddMessage, 1024 * 1024); + int outboundOffset = 0; + int inboundOffset = 0; + var outboundProcessor = new MessageProcessor(m => AddMessage(m, ref outboundOffset), 1024 * 1024); + var inboundProcessor = new MessageProcessor(m => AddMessage(m, ref inboundOffset), 1024 * 1024); foreach (var packet in IPV4Packets) { @@ -61,13 +63,14 @@ public async Task>> ProcessMessages(message, currentTimestamp.OrThrow(), direction, - currentSource.OrThrow(), currentDestination.OrThrow()); + offset, currentSource.OrThrow(), currentDestination.OrThrow()); messages.Add(annotated); + offset += message.Length; } } } diff --git a/DigiMixer/DigiMixer.Hardware/DigiMixer.Hardware.csproj b/DigiMixer/DigiMixer.Hardware/DigiMixer.Hardware.csproj index aa06868e..c5ac37c2 100644 --- a/DigiMixer/DigiMixer.Hardware/DigiMixer.Hardware.csproj +++ b/DigiMixer/DigiMixer.Hardware/DigiMixer.Hardware.csproj @@ -40,11 +40,12 @@ - + + diff --git a/DigiMixer/DigiMixer.TfSeries.Core/TfMessage.cs b/DigiMixer/DigiMixer.TfSeries.Core/TfMessage.cs deleted file mode 100644 index cb54f909..00000000 --- a/DigiMixer/DigiMixer.TfSeries.Core/TfMessage.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace DigiMixer.TfSeries.Core; - -public class TfMessage -{ - -} diff --git a/DigiMixer/DigiMixer.TfSeries.Tools/ClientInitialMessages.cs b/DigiMixer/DigiMixer.TfSeries.Tools/ClientInitialMessages.cs new file mode 100644 index 00000000..4257312f --- /dev/null +++ b/DigiMixer/DigiMixer.TfSeries.Tools/ClientInitialMessages.cs @@ -0,0 +1,66 @@ +using DigiMixer.Diagnostics; +using DigiMixer.Yamaha.Core; +using Microsoft.Extensions.Logging.Abstractions; + +namespace DigiMixer.TfSeries.Tools; + +public class ClientInitialMessages : Tool +{ + private const string Host = "192.168.1.96"; + private const int Port = 50368; + + private static readonly YamahaMessage Message1 = ConvertHexToMessage( + "4d 50 52 4f 00 00 00 1d 11 00 00 00 18 01 01 01", + "02 31 00 00 00 09 50 72 6f 70 65 72 74 79 00 11", + "00 00 00 01 80"); + + private static readonly YamahaMessage Message2 = ConvertHexToMessage( + "4d 50 52 4f 00 00 00 47 11 00 00 00 42 01 10 01", + "04 11 00 00 00 01 00 31 00 00 00 09 50 72 6f 70", + "65 72 74 79 00 11 00 00 00 10 3a 7c 8d 4c 85 f8", + "9f 1e aa 83 4f 96 63 0c ec 3d 11 00 00 00 10 8b", + "76 f3 98 78 64 6e 83 15 f5 81 7c 06 cc b6 91"); + + private static readonly YamahaMessage Message3 = ConvertHexToMessage( + "4d 50 52 4f 00 00 00 09 11 00 00 00 04 01 04 01 00"); + + public override async Task Execute() + { + YamahaClient client = new(NullLogger.Instance, Host, Port, HandleMessage); + await client.Connect(default); + client.Start(); + + await Send(Message1); + await Send(Message2); + await Send(Message3); + await Task.Delay(10000); + return 0; + + Task HandleMessage(YamahaMessage message, CancellationToken cancellationToken) + { + message.DisplayStructure("<=", DecodingOptions.Simple, Console.Out); + return Task.CompletedTask; + } + + async Task Send(YamahaMessage message) + { + message.DisplayStructure("=>", DecodingOptions.Simple, Console.Out); + await client.SendAsync(message, default); + } + } + + private static YamahaMessage ConvertHexToMessage(params string[] hexLines) + { + var hex = string.Join("", hexLines); + byte[] data = Hex.ParseHex(hex); + if (YamahaMessage.TryParse(data) is not YamahaMessage message) + { + throw new ArgumentException("Couldn't parse message"); + } + if (data.Length != message.Length) + { + throw new ArgumentException($"Didn't use all of the bytes ({data.Length} vs {message.Length}"); + } + return message; + } +} diff --git a/DigiMixer/DigiMixer.TfSeries.Tools/ConvertAllWireshark.cs b/DigiMixer/DigiMixer.TfSeries.Tools/ConvertAllWireshark.cs new file mode 100644 index 00000000..f83a5aae --- /dev/null +++ b/DigiMixer/DigiMixer.TfSeries.Tools/ConvertAllWireshark.cs @@ -0,0 +1,20 @@ +using DigiMixer.Diagnostics; + +namespace DigiMixer.TfSeries.Tools; + +public class ConvertAllWireshark(string directory) : Tool +{ + public override async Task Execute() + { + foreach (var file in Directory.GetFiles(directory, "*.pcapng")) + { + var singleFileTool = new ConvertWireshark(file); + var result = await singleFileTool.Execute(); + if (result != 0) + { + return result; + } + } + return 0; + } +} diff --git a/DigiMixer/DigiMixer.TfSeries.Tools/ConvertWireshark.cs b/DigiMixer/DigiMixer.TfSeries.Tools/ConvertWireshark.cs new file mode 100644 index 00000000..28cb14b7 --- /dev/null +++ b/DigiMixer/DigiMixer.TfSeries.Tools/ConvertWireshark.cs @@ -0,0 +1,23 @@ +using DigiMixer.Diagnostics; +using DigiMixer.Yamaha.Core; + +namespace DigiMixer.TfSeries.Tools; + +public class ConvertWireshark(string file) : Tool +{ + public override async Task Execute() + { + var dump = WiresharkDump.Load(file); + var messages = await dump.ProcessMessages("192.168.1.96", "192.168.1.140"); + + var dir = Path.GetDirectoryName(file).OrThrow(); + var newName = Path.GetFileNameWithoutExtension(file) + " decoded.txt"; + using var writer = File.CreateText(Path.Combine(dir, newName)); + foreach (var message in messages) + { + message.DisplayStructure(DecodingOptions.Investigative, writer); + } + Console.WriteLine($"Saved {newName}"); + return 0; + } +} diff --git a/DigiMixer/DigiMixer.TfSeries.Tools/DecodingOptions.cs b/DigiMixer/DigiMixer.TfSeries.Tools/DecodingOptions.cs new file mode 100644 index 00000000..6d14c899 --- /dev/null +++ b/DigiMixer/DigiMixer.TfSeries.Tools/DecodingOptions.cs @@ -0,0 +1,7 @@ +namespace DigiMixer.TfSeries.Tools; + +internal record DecodingOptions(bool SkipKeepAlive, bool DecodeSchemaAndData) +{ + internal static DecodingOptions Simple { get; } = new(false, false); + internal static DecodingOptions Investigative { get; } = new(true, true); +} \ No newline at end of file diff --git a/DigiMixer/DigiMixer.TfSeries.Tools/Program.cs b/DigiMixer/DigiMixer.TfSeries.Tools/Program.cs index 3751555c..dda3891e 100644 --- a/DigiMixer/DigiMixer.TfSeries.Tools/Program.cs +++ b/DigiMixer/DigiMixer.TfSeries.Tools/Program.cs @@ -1,2 +1,3 @@ -// See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); +using DigiMixer.Diagnostics; + +await Tool.ExecuteFromCommandLine(args, typeof(Program)); diff --git a/DigiMixer/DigiMixer.TfSeries.Tools/RequestFullData.cs b/DigiMixer/DigiMixer.TfSeries.Tools/RequestFullData.cs new file mode 100644 index 00000000..c4f5ee5f --- /dev/null +++ b/DigiMixer/DigiMixer.TfSeries.Tools/RequestFullData.cs @@ -0,0 +1,41 @@ +using DigiMixer.Diagnostics; +using DigiMixer.Yamaha.Core; +using Microsoft.Extensions.Logging.Abstractions; + +namespace DigiMixer.TfSeries.Tools; + +public class RequestFullData : Tool +{ + private const string Host = "192.168.1.96"; + private const int Port = 50368; + + private static readonly YamahaMessage FullData = new(YamahaMessageType.MMIX, 0x01010102, + [new YamahaTextSegment("Mixing"), YamahaBinarySegment.Empty]); + + private static readonly YamahaMessage NoHashes = new(YamahaMessageType.MMIX, 0x01100104, + [new YamahaBinarySegment([00]), new YamahaTextSegment("Mixing"), YamahaBinarySegment.Zero16, YamahaBinarySegment.Zero16]); + + public override async Task Execute() + { + YamahaClient client = new(NullLogger.Instance, Host, Port, HandleMessage); + await client.Connect(default); + client.Start(); + + await Send(FullData); + await Send(NoHashes); + await Task.Delay(10000); + return 0; + + Task HandleMessage(YamahaMessage message, CancellationToken cancellationToken) + { + message.DisplayStructure("<=", DecodingOptions.Simple,Console.Out); + return Task.CompletedTask; + } + + async Task Send(YamahaMessage message) + { + message.DisplayStructure("=>", DecodingOptions.Simple, Console.Out); + await client.SendAsync(message, default); + } + } +} diff --git a/DigiMixer/DigiMixer.TfSeries.Tools/YamahaMessageExtensions.cs b/DigiMixer/DigiMixer.TfSeries.Tools/YamahaMessageExtensions.cs new file mode 100644 index 00000000..27a7fe13 --- /dev/null +++ b/DigiMixer/DigiMixer.TfSeries.Tools/YamahaMessageExtensions.cs @@ -0,0 +1,109 @@ +using DigiMixer.Core; +using DigiMixer.Diagnostics; +using DigiMixer.Yamaha; +using DigiMixer.Yamaha.Core; +using System.Buffers.Binary; +using System.Reflection.Metadata; +using System.Security.Cryptography; +using System.Text; +using System.Xml.Linq; + +namespace DigiMixer.TfSeries.Tools; + +internal static class YamahaMessageExtensions +{ + internal static void DisplaySummary(this YamahaMessage message, string direction) + { + Console.WriteLine($"{direction} {message}: {string.Join(", ", message.Segments.Select(SummarizeSegment))}"); + + static string SummarizeSegment(YamahaSegment segment) => segment switch + { + YamahaBinarySegment binary => $"Binary[{binary.Data.Length}]", + YamahaTextSegment text => $"Text['{text.Text}']", + YamahaInt32Segment int32 => $"Int32[*{int32.Values.Count}]", + YamahaUInt32Segment uint32 => $"UInt32[*{uint32.Values.Count}]", + YamahaUInt16Segment uint16 => $"UInt16[*{uint16.Values.Count}]", + _ => throw new InvalidOperationException("Unknown segment type") + }; + } + + internal static void DisplayStructure(this AnnotatedMessage annotatedMessage, DecodingOptions options, TextWriter writer) + { + var message = annotatedMessage.Message; + if (options.SkipKeepAlive && message.Type.Text == "EEVT" && message.Segments.Count > 3 && message.Segments[0] is YamahaTextSegment { Text: "KeepAlive" }) + { + return; + } + string directionIndicator = annotatedMessage.Direction == MessageDirection.ClientToMixer ? "=>" : "<="; + writer.WriteLine($"{directionIndicator} 0x{annotatedMessage.StreamOffset:x8} {message}"); + DisplaySegments(message, options, writer); + } + + internal static void DisplayStructure(this YamahaMessage message, string directionIndicator, DecodingOptions options, TextWriter writer) + { + if (options.SkipKeepAlive && message.Type.Text == "EEVT" && message.Segments.Count > 3 && message.Segments[0] is YamahaTextSegment { Text: "KeepAlive" }) + { + return; + } + writer.WriteLine($"{directionIndicator} {message}"); + DisplaySegments(message, options, writer); + } + + private static void DisplaySegments(YamahaMessage message, DecodingOptions options, TextWriter writer) + { + foreach (var segment in message.Segments) + { + writer.WriteLine($" {DescribeSegment(segment)}"); + } + if (SectionSchemaAndDataMessage.TryParse(message) is { } section && options.DecodeSchemaAndData) + { + var hash = MD5.HashData(((YamahaBinarySegment) message.Segments[7]).Data); + writer.WriteLine($" MD5: {Formatting.ToHex(hash)}"); + DescribeSchemaAndData(section.Data); + } + writer.WriteLine(); + + static string DescribeSegment(YamahaSegment segment) + { + switch (segment) + { + case YamahaBinarySegment binary: + var data = binary.Data; + var hexLength = Math.Min(data.Length, 16); + var hex = Formatting.ToHex(data[..hexLength]) + (hexLength == data.Length ? "" : " [...]"); + return $"Binary[{data.Length}]: {hex}"; + case YamahaTextSegment text: + return $"Text: '{text.Text}'"; + case YamahaInt32Segment int32: + return $"Int32[*{int32.Values.Count}]: {string.Join(" ", int32.Values.Select(v => $"0x{v:x8}"))} / {string.Join(" ", int32.Values)}"; + case YamahaUInt32Segment uint32: + return $"UInt32[*{uint32.Values.Count}]: {string.Join(" ", uint32.Values.Select(v => v.ToString("x8")))}"; + case YamahaUInt16Segment uint16: + return $"UInt16[*{uint16.Values.Count}]: {string.Join(" ", uint16.Values.Select(v => v.ToString("x4")))}"; + default: + throw new InvalidOperationException("Unknown segment type"); + } + } + + void DescribeSchemaAndData(SectionSchemaAndData section) + { + writer.WriteLine($" Hash text: {section.SchemaHash}"); + writer.WriteLine($" Schema:"); + DescribeCol(section.Schema, " "); + } + + void DescribeCol(SchemaCol col, string indent) + { + writer.WriteLine($"{indent}COL: {col.Name} Offset={col.RelativeOffset}; Data length={col.DataLength}; Count={col.Count}"); + var nestedIndent = indent + " "; + foreach (var property in col.Properties) + { + writer.WriteLine($"{nestedIndent} PR: {property.Name}; Type={property.Type}; Length={property.Length}; Count={property.Count}"); + } + foreach (var nested in col.Cols) + { + DescribeCol(nested, nestedIndent); + } + } + } +} diff --git a/DigiMixer/DigiMixer.TfSeries/DigiMixer.TfSeries.csproj b/DigiMixer/DigiMixer.TfSeries/DigiMixer.TfSeries.csproj index 986fe01d..c1bf5098 100644 --- a/DigiMixer/DigiMixer.TfSeries/DigiMixer.TfSeries.csproj +++ b/DigiMixer/DigiMixer.TfSeries/DigiMixer.TfSeries.csproj @@ -8,7 +8,7 @@ - + diff --git a/DigiMixer/DigiMixer.TfSeries/TfMessages.cs b/DigiMixer/DigiMixer.TfSeries/TfMessages.cs new file mode 100644 index 00000000..717ec08a --- /dev/null +++ b/DigiMixer/DigiMixer.TfSeries/TfMessages.cs @@ -0,0 +1,7 @@ +using DigiMixer.Yamaha.Core; + +namespace DigiMixer.TfSeries; + +public class TfMessages +{ +} diff --git a/DigiMixer/DigiMixer.TfSeries/TfMixer.cs b/DigiMixer/DigiMixer.TfSeries/TfMixer.cs index 0461be7a..af7130c3 100644 --- a/DigiMixer/DigiMixer.TfSeries/TfMixer.cs +++ b/DigiMixer/DigiMixer.TfSeries/TfMixer.cs @@ -1,4 +1,6 @@ using DigiMixer.Core; +using DigiMixer.Yamaha; +using DigiMixer.Yamaha.Core; using Microsoft.Extensions.Logging; namespace DigiMixer.TfSeries; @@ -9,11 +11,12 @@ public static IMixerApi CreateMixerApi(ILogger logger, string host, int port = 5 new TfMixerApi(logger, host, port, options); } -internal class TfMixerApi : IMixerApi +internal class TfMixerApi(ILogger logger, string host, int port, MixerApiOptions? options) : IMixerApi { - public TfMixerApi(ILogger logger, string host, int port, MixerApiOptions? options) - { - } + private YamahaClient? controlClient; + private CancellationTokenSource? cts; + private DateTimeOffset? lastKeepAliveReceived; + private readonly MixerApiOptions options = options ?? MixerApiOptions.Default; public TimeSpan KeepAliveInterval => throw new NotImplementedException(); @@ -24,9 +27,28 @@ public Task CheckConnection(CancellationToken cancellationToken) throw new NotImplementedException(); } - public Task Connect(CancellationToken cancellationToken) + public async Task Connect(CancellationToken cancellationToken) { - throw new NotImplementedException(); + Dispose(); + + cts = new CancellationTokenSource(); + //meterClient = new DmMeterClient(logger); + //meterClient.MessageReceived += HandleMeterMessage; + //meterClient.Start(); + controlClient = new YamahaClient(logger, host, port, HandleControlMessage); + await controlClient.Connect(cancellationToken); + controlClient.Start(); + + // Pretend we've seen a keep-alive message + lastKeepAliveReceived = DateTimeOffset.UtcNow; + + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cts.Token); + + // fullDataTask = RequestFullData("MMIX", "Mixing", cancellationToken); + // await fullDataTask; + + // Request live updates for channel information. + await Send(new YamahaMessage(YamahaMessageType.MMIX, 0x01041000, []), cancellationToken); } public Task DetectConfiguration(CancellationToken cancellationToken) @@ -68,4 +90,52 @@ public Task SetMuted(ChannelId channelId, bool muted) { throw new NotImplementedException(); } + + private async Task HandleControlMessage(YamahaMessage message, CancellationToken cancellationToken) + { + var wrapped = WrappedMessage.TryParse(message); + + if (wrapped is KeepAliveMessage) + { + lastKeepAliveReceived = DateTimeOffset.UtcNow; + return; + } + + // Handle responses to full requests. + if (message.Header == 0x01140109 && message.Segments is + [YamahaBinarySegment _, YamahaTextSegment { Text: string subtype }, YamahaTextSegment { Text: string subtype2 }, YamahaUInt16Segment _, YamahaUInt32Segment _, + YamahaUInt32Segment _, YamahaUInt32Segment _, YamahaBinarySegment _, YamahaBinarySegment _] && + subtype == subtype2) + { + /* + var node = temporaryListeners.First; + while (node is not null) + { + var listener = node.Value; + if (listener.Type == message.Type && listener.Subtype == subtype) + { + listener.SetResult(message); + listener.Dispose(); + temporaryListeners.Remove(node); + } + node = node.Next; + } + if (message.Type == TfMessages.Types.Channels) + { + HandleFullChannelData(new FullChannelDataMessage(message)); + fullDataTask = Task.FromResult(message); + }*/ + // Acknowledge the data + await Send(new YamahaMessage(message.Type, 0x01040100, []), cancellationToken); + } + } + + private async Task Send(YamahaMessage message, CancellationToken cancellationToken) + { + if (controlClient is null) + { + throw new InvalidOperationException("Client is not connected"); + } + await controlClient.SendAsync(message, cancellationToken); + } } \ No newline at end of file diff --git a/DigiMixer/DigiMixer.Yamaha.Core/DigiMixer.Yamaha.Core.csproj b/DigiMixer/DigiMixer.Yamaha.Core/DigiMixer.Yamaha.Core.csproj new file mode 100644 index 00000000..d63e8663 --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha.Core/DigiMixer.Yamaha.Core.csproj @@ -0,0 +1,12 @@ + + + + net9.0 + enable + enable + + + + + + diff --git a/DigiMixer/DigiMixer.Yamaha.Core/YamahaBinarySegment.cs b/DigiMixer/DigiMixer.Yamaha.Core/YamahaBinarySegment.cs new file mode 100644 index 00000000..39a88612 --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha.Core/YamahaBinarySegment.cs @@ -0,0 +1,57 @@ +using System.Buffers.Binary; + +namespace DigiMixer.Yamaha.Core; + +public sealed class YamahaBinarySegment : YamahaSegment +{ + public static YamahaBinarySegment Empty { get; } = new([]); + public static YamahaBinarySegment Zero16 { get; } = new(new byte[16]); + + internal override int Length => data.Length + 5; + + private readonly ReadOnlyMemory data; + public ReadOnlySpan Data => data.Span; + + private YamahaBinarySegment(byte[] data) + { + this.data = data; + } + + public YamahaBinarySegment(ReadOnlySpan data) : this(data.ToArray()) + { + } + + public static YamahaBinarySegment FromHex(string text) + { + text = text.Replace(" ", ""); + byte[] data = new byte[text.Length / 2]; + for (int i = 0; i < data.Length; i++) + { + data[i] = (byte) ((ParseNybble(text[i * 2]) << 4) + ParseNybble(text[i * 2 + 1])); + } + return new YamahaBinarySegment(data); + + static int ParseNybble(char c) => c switch + { + >= '0' and <= '9' => c - '0', + >= 'A' and <= 'F' => c - 'A' + 10, + >= 'a' and <= 'f' => c - 'a' + 10, + _ => throw new ArgumentException($"Invalid nybble '{c}'") + }; + } + + internal override void WriteTo(Span buffer) + { + buffer[0] = (byte) YamahaSegmentFormat.Binary; + BinaryPrimitives.WriteInt32BigEndian(buffer[1..], data.Length); + Data.CopyTo(buffer[5..]); + } + + public static YamahaBinarySegment Parse(ReadOnlySpan buffer) + { + var dataLength = BinaryPrimitives.ReadInt32BigEndian(buffer[1..]); + var bytes = new byte[dataLength]; + buffer.Slice(5, dataLength).CopyTo(bytes); + return new YamahaBinarySegment(bytes); + } +} diff --git a/DigiMixer/DigiMixer.Yamaha.Core/YamahaClient.cs b/DigiMixer/DigiMixer.Yamaha.Core/YamahaClient.cs new file mode 100644 index 00000000..70bae266 --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha.Core/YamahaClient.cs @@ -0,0 +1,14 @@ +using DigiMixer.Core; +using Microsoft.Extensions.Logging; + +namespace DigiMixer.Yamaha.Core; + +public class YamahaClient(ILogger logger, string host, int port, Func handler) + : TcpMessageProcessingControllerBase(logger, host, port, bufferSize: 1024 * 1024) +{ + protected override async Task ProcessMessage(YamahaMessage message, CancellationToken cancellationToken) + { + Logger.LogTrace("Received message: {message}", message); + await handler(message, cancellationToken); + } +} diff --git a/DigiMixer/DigiMixer.Yamaha.Core/YamahaInt32Segment.cs b/DigiMixer/DigiMixer.Yamaha.Core/YamahaInt32Segment.cs new file mode 100644 index 00000000..6e2c27d6 --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha.Core/YamahaInt32Segment.cs @@ -0,0 +1,32 @@ +using System.Buffers.Binary; +using System.Collections.Immutable; + +namespace DigiMixer.Yamaha.Core; + +public sealed class YamahaInt32Segment(ImmutableList values) : YamahaSegment +{ + internal override int Length => 5 + Values.Count * 4; + + public ImmutableList Values { get; } = values; + + internal override void WriteTo(Span buffer) + { + buffer[0] = (byte) YamahaSegmentFormat.Int32; + BinaryPrimitives.WriteInt32BigEndian(buffer[1..], Values.Count); + for (int i = 0; i < Values.Count; i++) + { + BinaryPrimitives.WriteInt32BigEndian(buffer[(5 + i * 4)..], Values[i]); + } + } + + public static YamahaInt32Segment Parse(ReadOnlySpan buffer) + { + var valueCount = BinaryPrimitives.ReadInt32BigEndian(buffer[1..]); + var values = new int[valueCount]; + for (int i = 0; i < valueCount; i++) + { + values[i] = BinaryPrimitives.ReadInt32BigEndian(buffer[(5 + i * 4)..]); + } + return new YamahaInt32Segment([.. values]); + } +} diff --git a/DigiMixer/DigiMixer.Yamaha.Core/YamahaMessage.cs b/DigiMixer/DigiMixer.Yamaha.Core/YamahaMessage.cs new file mode 100644 index 00000000..b6fd9eca --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha.Core/YamahaMessage.cs @@ -0,0 +1,112 @@ +using DigiMixer.Core; +using System.Buffers.Binary; +using System.Collections.Immutable; +using System.Text; + +namespace DigiMixer.Yamaha.Core; + +public class YamahaMessage : IMixerMessage +{ + /// + /// Message type, e.g. MPRO or EEVT. + /// + public YamahaMessageType Type { get; } + + /// + /// The 32-bit value between the message length and the segments. + /// + public uint Header { get; } + + public int Length => + 4 // Type + + 4 // Message length (excluding type and length) + + 1 // Binary segment + + 4 // Length of binary segment + + 4 // Header + + Segments.Sum(s => s.Length); // Nested segments + + public ImmutableList Segments { get; } + + public YamahaMessage(YamahaMessageType type, uint header, ImmutableList segments) + { + Type = type; + Header = header; + Segments = segments; + if ((Header & 0xff) != segments.Count) + { + throw new ArgumentException($"Header {Header:x8} incompatible with segment count of {Segments.Count}"); + } + if (((Header >> 24) & 0xff) != type.HeaderByte) + { + throw new ArgumentException($"Header {Header:x8} incompatible with message type {Type}"); + } + } + + public static YamahaMessage? TryParse(ReadOnlySpan data) + { + if (data.Length < 8) + { + return null; + } + int bodyLength = BinaryPrimitives.ReadInt32BigEndian(data[4..8]); + if (data.Length < bodyLength + 8) + { + return null; + } + if (bodyLength < 0) + { + throw new InvalidDataException($"Negative body length: {bodyLength}"); + } + if (YamahaMessageType.TryParse(data) is not YamahaMessageType type) + { + throw new InvalidDataException($"Unknown message type: {Encoding.ASCII.GetString(data[0..4]).Trim()}"); + } + var body = data.Slice(8, bodyLength); + if (body[0] != (byte) YamahaSegmentFormat.Binary) + { + throw new InvalidDataException("Expected overall container with format 0x11"); + } + var containerLength = BinaryPrimitives.ReadInt32BigEndian(body[1..]); + if (containerLength != bodyLength - 5) + { + throw new InvalidDataException($"Expected overall container internal length {bodyLength - 5}; was {containerLength}"); + } + var segments = new List(); + var header = BinaryPrimitives.ReadUInt32BigEndian(body[5..]); + var nextSegmentData = body[9..]; + while (nextSegmentData.Length > 0) + { + var format = (YamahaSegmentFormat)nextSegmentData[0]; + YamahaSegment segment = format switch + { + YamahaSegmentFormat.Text => YamahaTextSegment.Parse(nextSegmentData), + YamahaSegmentFormat.Binary => YamahaBinarySegment.Parse(nextSegmentData), + YamahaSegmentFormat.Int32 => YamahaInt32Segment.Parse(nextSegmentData), + YamahaSegmentFormat.UInt32 => YamahaUInt32Segment.Parse(nextSegmentData), + YamahaSegmentFormat.UInt16 => YamahaUInt16Segment.Parse(nextSegmentData), + _ => throw new InvalidDataException($"Unexpected segment format {nextSegmentData[0]:x2}") + }; + segments.Add(segment); + nextSegmentData = nextSegmentData[segment.Length..]; + } + return new YamahaMessage(type, header, [.. segments]); + } + + public void CopyTo(Span buffer) + { + // Note: we assume the span is right-sized to Length. + Type.WriteTo(buffer); + BinaryPrimitives.WriteInt32BigEndian(buffer[4..], buffer.Length - 8); + buffer[8] = (byte) YamahaSegmentFormat.Binary; + BinaryPrimitives.WriteInt32BigEndian(buffer[9..], buffer.Length - 13); + BinaryPrimitives.WriteUInt32BigEndian(buffer[13..], Header); + buffer = buffer[17..]; + foreach (var segment in Segments) + { + segment.WriteTo(buffer); + buffer = buffer[segment.Length..]; + } + } + + public override string ToString() => $"{Type.Text,-4}: Flag1={(Header>>16) & 0xff:x2}; Flag2={(Header >> 8) & 0xff:x2}; Segments={Segments.Count}"; +} diff --git a/DigiMixer/DigiMixer.Yamaha.Core/YamahaMessageType.cs b/DigiMixer/DigiMixer.Yamaha.Core/YamahaMessageType.cs new file mode 100644 index 00000000..dd26e222 --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha.Core/YamahaMessageType.cs @@ -0,0 +1,74 @@ +using System.Buffers.Binary; +using System.Collections.Immutable; +using System.Text; + +namespace DigiMixer.Yamaha.Core; + +/// +/// The type of a message. +/// +public sealed class YamahaMessageType +{ + public static YamahaMessageType D000 { get; } = new("d000", 2); + public static YamahaMessageType D010 { get; } = new("d010", 2); + public static YamahaMessageType D020 { get; } = new("d020", 2); + public static YamahaMessageType D030 { get; } = new("d030", 2); + public static YamahaMessageType D040 { get; } = new("d040", 2); + public static YamahaMessageType DL_A { get; } = new("DL_A", 4); + public static YamahaMessageType DL_B { get; } = new("DL_B", 4); + public static YamahaMessageType DL_C { get; } = new("DL_C", 4); + public static YamahaMessageType DL_D { get; } = new("DL_D", 4); + public static YamahaMessageType DS_A { get; } = new("DS_A", 4); + public static YamahaMessageType DS_B { get; } = new("DS_B", 4); + public static YamahaMessageType DS_C { get; } = new("DS_C", 4); + public static YamahaMessageType DS_D { get; } = new("DS_D", 4); + public static YamahaMessageType EEVT { get; } = new("EEVT", 3); + public static YamahaMessageType MCST { get; } = new("MCST", 1); + public static YamahaMessageType MFX { get; } = new("MFX", 1); + public static YamahaMessageType MMIX { get; } = new("MMIX", 1); + public static YamahaMessageType MPRC { get; } = new("MPRC", 1); + public static YamahaMessageType MPRO { get; } = new("MPRO", 1); + public static YamahaMessageType MSCL { get; } = new("MSCL", 1); + public static YamahaMessageType MSCS { get; } = new("MSCS", 1); + public static YamahaMessageType MSTS { get; } = new("MSTS", 1); + public static YamahaMessageType MSUP { get; } = new("MSUP", 1); + public static YamahaMessageType MVOL { get; } = new("MVOL", 1); + + private static readonly ImmutableList MessageTypes = [D000, D010, D020, D030, D040, DL_A, DL_B, DL_C, DL_D, DS_A, DS_B, DS_C, DS_D, EEVT, MCST, MFX, MMIX, MPRC, MPRO, MSCL, MSCS, MSTS, MSUP, MVOL]; + + /// + /// 1-4 character textual representation, always ASCII. + /// This is used as the first four bytes of the message. + /// + public string Text { get; } + + public byte HeaderByte { get; } + + private readonly uint magicNumber; + + private YamahaMessageType(string text, byte headerByte) + { + Text = text; + var bytes = new byte[4]; + Encoding.ASCII.GetBytes(text, bytes); + magicNumber = BinaryPrimitives.ReadUInt32LittleEndian(bytes); + HeaderByte = headerByte; + } + + internal static YamahaMessageType? TryParse(ReadOnlySpan bytes) + { + var magicNumber = BinaryPrimitives.ReadUInt32LittleEndian(bytes); + foreach (var type in MessageTypes) + { + if (magicNumber == type.magicNumber) + { + return type; + } + } + return null; + } + + internal void WriteTo(Span bytes) => BinaryPrimitives.WriteUInt32LittleEndian(bytes, magicNumber); + + public override string ToString() => Text; +} diff --git a/DigiMixer/DigiMixer.Yamaha.Core/YamahaMessages.cs b/DigiMixer/DigiMixer.Yamaha.Core/YamahaMessages.cs new file mode 100644 index 00000000..6c9a1c84 --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha.Core/YamahaMessages.cs @@ -0,0 +1,17 @@ +namespace DigiMixer.Yamaha.Core; + +public static class YamahaMessages +{ + public static YamahaMessage KeepAlive { get; } = new(YamahaMessageType.EEVT, + 0x03010104, + [ + new YamahaUInt32Segment([0x0000]), + new YamahaUInt32Segment([0x0000]), + new YamahaTextSegment("KeepAlive"), + new YamahaTextSegment("") + ]); + + internal static bool IsKeepAlive(YamahaMessage message) => + // We could check more than this, but why bother? + message.Type == KeepAlive.Type && (message.Header == 0x03011004 || message.Header == 0x03010104); +} diff --git a/DigiMixer/DigiMixer.Yamaha.Core/YamahaSegment.cs b/DigiMixer/DigiMixer.Yamaha.Core/YamahaSegment.cs new file mode 100644 index 00000000..375a4928 --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha.Core/YamahaSegment.cs @@ -0,0 +1,18 @@ +namespace DigiMixer.Yamaha.Core; + +/// +/// A segment within a . +/// +public abstract class YamahaSegment +{ + internal YamahaSegment() + { + } + + /// + /// The length of the segment, including the format. + /// + internal abstract int Length { get; } + + internal abstract void WriteTo(Span buffer); +} diff --git a/DigiMixer/DigiMixer.Yamaha.Core/YamahaSegmentFormat.cs b/DigiMixer/DigiMixer.Yamaha.Core/YamahaSegmentFormat.cs new file mode 100644 index 00000000..f556d76e --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha.Core/YamahaSegmentFormat.cs @@ -0,0 +1,10 @@ +namespace DigiMixer.Yamaha.Core; + +public enum YamahaSegmentFormat : byte +{ + Binary = 0x11, + UInt16 = 0x12, + UInt32 = 0x14, + Int32 = 0x24, + Text = 0x31, +} diff --git a/DigiMixer/DigiMixer.Yamaha.Core/YamahaTextSegment.cs b/DigiMixer/DigiMixer.Yamaha.Core/YamahaTextSegment.cs new file mode 100644 index 00000000..bd9dd45e --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha.Core/YamahaTextSegment.cs @@ -0,0 +1,31 @@ + +using System.Buffers.Binary; +using System.Text; + +namespace DigiMixer.Yamaha.Core; + +public sealed class YamahaTextSegment(string text) : YamahaSegment +{ + /// + /// The text of the segment, not including the null terminator. + /// + public string Text { get; } = text; + + internal override int Length => Text.Length + 6; + + internal override void WriteTo(Span buffer) + { + buffer[0] = (byte)YamahaSegmentFormat.Text; + BinaryPrimitives.WriteInt32BigEndian(buffer[1..], Text.Length + 1); + Encoding.ASCII.GetBytes(Text, buffer[5..]); + buffer[5 + Text.Length] = 0; + } + + public static YamahaTextSegment Parse(ReadOnlySpan buffer) + { + var textLength = BinaryPrimitives.ReadInt32BigEndian(buffer[1..]); + // We assume the null termination. + var text = Encoding.ASCII.GetString(buffer.Slice(5, textLength - 1)); + return new YamahaTextSegment(text); + } +} diff --git a/DigiMixer/DigiMixer.Yamaha.Core/YamahaUInt16Segment.cs b/DigiMixer/DigiMixer.Yamaha.Core/YamahaUInt16Segment.cs new file mode 100644 index 00000000..52bd4f48 --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha.Core/YamahaUInt16Segment.cs @@ -0,0 +1,32 @@ +using System.Buffers.Binary; +using System.Collections.Immutable; + +namespace DigiMixer.Yamaha.Core; + +public sealed class YamahaUInt16Segment(ImmutableList values) : YamahaSegment +{ + internal override int Length => 5 + Values.Count * 2; + + public ImmutableList Values { get; } = values; + + internal override void WriteTo(Span buffer) + { + buffer[0] = (byte) YamahaSegmentFormat.UInt16; + BinaryPrimitives.WriteInt32BigEndian(buffer[1..], Values.Count); + for (int i = 0; i < Values.Count; i++) + { + BinaryPrimitives.WriteUInt16BigEndian(buffer[(5 + i * 2)..], Values[i]); + } + } + + public static YamahaUInt16Segment Parse(ReadOnlySpan buffer) + { + var valueCount = BinaryPrimitives.ReadInt32BigEndian(buffer[1..]); + var values = new ushort[valueCount]; + for (int i = 0; i < valueCount; i++) + { + values[i] = BinaryPrimitives.ReadUInt16BigEndian(buffer[(5 + i * 2)..]); + } + return new YamahaUInt16Segment([.. values]); + } +} diff --git a/DigiMixer/DigiMixer.Yamaha.Core/YamahaUInt32Segment.cs b/DigiMixer/DigiMixer.Yamaha.Core/YamahaUInt32Segment.cs new file mode 100644 index 00000000..02c1681a --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha.Core/YamahaUInt32Segment.cs @@ -0,0 +1,32 @@ +using System.Buffers.Binary; +using System.Collections.Immutable; + +namespace DigiMixer.Yamaha.Core; + +public sealed class YamahaUInt32Segment(ImmutableList values) : YamahaSegment +{ + internal override int Length => 5 + Values.Count * 4; + + public ImmutableList Values { get; } = values; + + internal override void WriteTo(Span buffer) + { + buffer[0] = (byte) YamahaSegmentFormat.UInt32; + BinaryPrimitives.WriteInt32BigEndian(buffer[1..], Values.Count); + for (int i = 0; i < Values.Count; i++) + { + BinaryPrimitives.WriteUInt32BigEndian(buffer[(5 + i * 4)..], Values[i]); + } + } + + public static YamahaUInt32Segment Parse(ReadOnlySpan buffer) + { + var valueCount = BinaryPrimitives.ReadInt32BigEndian(buffer[1..]); + var values = new uint[valueCount]; + for (int i = 0; i < valueCount; i++) + { + values[i] = BinaryPrimitives.ReadUInt32BigEndian(buffer[(5 + i * 4)..]); + } + return new YamahaUInt32Segment([.. values]); + } +} diff --git a/DigiMixer/DigiMixer.Yamaha/DataSubtypes.cs b/DigiMixer/DigiMixer.Yamaha/DataSubtypes.cs new file mode 100644 index 00000000..eac260b6 --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha/DataSubtypes.cs @@ -0,0 +1,9 @@ +namespace DigiMixer.Yamaha; + +public static class DataSubtypes +{ + // For MMIX + public const string Mixing = "Mixing"; + // For MPRO + public const string Property = "Property"; +} diff --git a/DigiMixer/DigiMixer.TfSeries.Core/DigiMixer.TfSeries.Core.csproj b/DigiMixer/DigiMixer.Yamaha/DigiMixer.Yamaha.csproj similarity index 67% rename from DigiMixer/DigiMixer.TfSeries.Core/DigiMixer.TfSeries.Core.csproj rename to DigiMixer/DigiMixer.Yamaha/DigiMixer.Yamaha.csproj index bb6bad2f..4fa71ce4 100644 --- a/DigiMixer/DigiMixer.TfSeries.Core/DigiMixer.TfSeries.Core.csproj +++ b/DigiMixer/DigiMixer.Yamaha/DigiMixer.Yamaha.csproj @@ -4,10 +4,10 @@ net9.0 enable enable - latest - + + diff --git a/DigiMixer/DigiMixer.Yamaha/KeepAliveMessage.cs b/DigiMixer/DigiMixer.Yamaha/KeepAliveMessage.cs new file mode 100644 index 00000000..5c5c3b6f --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha/KeepAliveMessage.cs @@ -0,0 +1,27 @@ +using DigiMixer.Yamaha.Core; + +namespace DigiMixer.Yamaha; + +public sealed class KeepAliveMessage : WrappedMessage +{ + // TODO: Do we need to worry about the different headers here? Maybe we should have two instances? + public static KeepAliveMessage Instance { get; } = new(new (YamahaMessageType.EEVT, 0x03010104, + [ + new YamahaUInt32Segment([0x0000]), + new YamahaUInt32Segment([0x0000]), + new YamahaTextSegment("KeepAlive"), + new YamahaTextSegment("") + ])); + + private KeepAliveMessage(YamahaMessage rawMessage) : base(rawMessage) + { + } + + public static new KeepAliveMessage? TryParse(YamahaMessage rawMessage) => + IsKeepAlive(rawMessage) ? Instance : null; + + internal static bool IsKeepAlive(YamahaMessage message) => + // We could check more than this, but why bother? + message.Type == Instance.RawMessage.Type && (message.Header == 0x03011004 || message.Header == 0x03010104); + +} diff --git a/DigiMixer/DigiMixer.Yamaha/SchemaCol.cs b/DigiMixer/DigiMixer.Yamaha/SchemaCol.cs new file mode 100644 index 00000000..640ec8e3 --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha/SchemaCol.cs @@ -0,0 +1,80 @@ +using System.Buffers.Binary; +using System.Collections.Immutable; +using System.Text; + +namespace DigiMixer.Yamaha; + +public sealed class SchemaCol +{ + public string Name { get; } + public ImmutableList Cols { get; } + public ImmutableList Properties { get; } + + /// + /// The number of bytes a single instance of this Col takes up. + /// + public int DataLength { get; } + + /// + /// The number of types this Col is repeated in the parent. + /// + public int Count { get; } + + /// + /// The offset of the first instance of this Col, relative to the start of the parent Col. + /// + public int RelativeOffset { get; } + + /// + /// The offset of the first instance of this Col, relative to the start of the data. + /// + public int AbsoluteOffset { get; } + + public SchemaCol? Parent { get; } + + private int SchemaLength => 48 + Properties.Count * 32 + Cols.Sum(c => c.SchemaLength); + + internal SchemaCol(SchemaCol? parent, ReadOnlySpan schema) + { + if (schema.Length < 48) + { + throw new Exception($"Invalid schema length {schema.Length} for COL: {Encoding.ASCII.GetString(schema)}"); + } + if (schema[0] != 'C' || schema[1] != 'O' || schema[2] != 'L' || schema[3] != '0') + { + throw new ArgumentException($"Unexpected schema data; expected COL, got {Encoding.ASCII.GetString(schema[0..3])}"); + } + Name = Encoding.ASCII.GetString(schema.Slice(4, 24)).TrimEnd('\0'); + RelativeOffset = BinaryPrimitives.ReadInt32LittleEndian(schema.Slice(36, 4)); + AbsoluteOffset = (parent?.AbsoluteOffset ?? 0) + RelativeOffset; + DataLength = BinaryPrimitives.ReadInt32LittleEndian(schema.Slice(40, 4)); + Count = BinaryPrimitives.ReadInt32LittleEndian(schema.Slice(44, 4)); + + var propertiesLength = BinaryPrimitives.ReadInt32LittleEndian(schema.Slice(28, 4)); + var colsLength = BinaryPrimitives.ReadInt32LittleEndian(schema.Slice(32, 4)); + if (schema.Length < 48 + propertiesLength + colsLength) + { + throw new ArgumentException($"Invalid length for Col; total length={schema.Length}; properties length={propertiesLength}; cols length={colsLength}"); + } + + var propertiesBuilder = ImmutableList.CreateBuilder(); + int dataOffset = 0; + for (int i = 0; i < propertiesLength / 32; i++) + { + var prop = new SchemaProperty(this, dataOffset, schema.Slice(i * 32 + 48, 32)); + propertiesBuilder.Add(prop); + dataOffset += prop.Length * prop.Count; + } + Properties = propertiesBuilder.ToImmutable(); + + var colsBuilder = ImmutableList.CreateBuilder(); + int offset = 0; + while (offset < colsLength) + { + var col = new SchemaCol(this, schema[(48 + propertiesLength + offset)..]); + colsBuilder.Add(col); + offset += col.SchemaLength; + } + Cols = colsBuilder.ToImmutable(); + } +} diff --git a/DigiMixer/DigiMixer.Yamaha/SchemaProperty.cs b/DigiMixer/DigiMixer.Yamaha/SchemaProperty.cs new file mode 100644 index 00000000..ffa19e03 --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha/SchemaProperty.cs @@ -0,0 +1,58 @@ +using System.Buffers.Binary; +using System.Text; + +namespace DigiMixer.Yamaha; + +public sealed class SchemaProperty +{ + /// + /// The name of the property. + /// + public string Name { get; } + + /// + /// The type of the property. + /// + public SchemaPropertyType Type { get; } + + /// + /// How many times this property is repeated. + /// + public int Count { get; } + + /// + /// Number of bytes this takes up in a Col. + /// + public int Length { get; } + + public SchemaCol Parent { get; } + + /// + /// The offset of the first instance of this property, relative to the start of the Col. + /// + public int RelativeOffset { get; } + + /// + /// The offset of the first instance of this property, relative to the start of the data. + /// + public int AbsoluteOffset { get; } + + internal SchemaProperty(SchemaCol parent, int offset, ReadOnlySpan schema) + { + if (schema.Length != 32) + { + throw new Exception($"Invalid schema length {schema.Length} for property: {schema.Length}"); + } + if (schema[0] != 'P' || schema[1] != 'R' || schema[2] != ' ') + { + throw new ArgumentException($"Unexpected schema data; expected property, got {Encoding.ASCII.GetString(schema[0..3])}"); + } + Type = (SchemaPropertyType) schema[3]; + Length = BinaryPrimitives.ReadUInt16LittleEndian(schema[4..6]); + Count = BinaryPrimitives.ReadUInt16LittleEndian(schema[6..8]); + Name = Encoding.ASCII.GetString(schema.Slice(8, 24)).TrimEnd('\0'); + Parent = parent; + RelativeOffset = offset; + AbsoluteOffset = offset + parent.AbsoluteOffset; + } +} diff --git a/DigiMixer/DigiMixer.Yamaha/SchemaPropertyType.cs b/DigiMixer/DigiMixer.Yamaha/SchemaPropertyType.cs new file mode 100644 index 00000000..50ccb412 --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha/SchemaPropertyType.cs @@ -0,0 +1,8 @@ +namespace DigiMixer.Yamaha; + +public enum SchemaPropertyType : byte +{ + Text = 0, + Float = 1, + Integer= 2 +} diff --git a/DigiMixer/DigiMixer.Yamaha/SectionSchemaAndData.cs b/DigiMixer/DigiMixer.Yamaha/SectionSchemaAndData.cs new file mode 100644 index 00000000..8a83a8fd --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha/SectionSchemaAndData.cs @@ -0,0 +1,38 @@ +using DigiMixer.Yamaha.Core; +using System.Buffers.Binary; +using System.Text; + +namespace DigiMixer.Yamaha; + +/// +/// The schema and data for a section, parsed from a +/// +public sealed class SectionSchemaAndData +{ + private readonly YamahaBinarySegment segment; + private readonly int schemaLength; + + public string Name { get; } + public string SchemaHash { get; } + public SchemaCol Schema { get; } + + public SectionSchemaAndData(YamahaBinarySegment segment) + { + this.segment = segment; + var segmentData = segment.Data; + + var header = segmentData[..88]; + Name = Encoding.ASCII.GetString(header[8..44]).TrimEnd('\0'); + SchemaHash = Encoding.ASCII.GetString(header[44..76]); + schemaLength = BinaryPrimitives.ReadInt32LittleEndian(header[80..]); + var dataLength = BinaryPrimitives.ReadInt32LittleEndian(header[84..]); + + if (schemaLength + dataLength + 88 != segmentData.Length) + { + throw new ArgumentException($"Invalid section data length. Segment length: {segmentData.Length}; schema length: {schemaLength}; values length: {dataLength}"); + } + + // For now, assume that there's a single root Col. + Schema = new SchemaCol(null, segmentData.Slice(88, schemaLength)); + } +} diff --git a/DigiMixer/DigiMixer.Yamaha/SectionSchemaAndDataMessage.cs b/DigiMixer/DigiMixer.Yamaha/SectionSchemaAndDataMessage.cs new file mode 100644 index 00000000..e3b70a34 --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha/SectionSchemaAndDataMessage.cs @@ -0,0 +1,46 @@ +using DigiMixer.Yamaha.Core; + +namespace DigiMixer.Yamaha; + +public sealed class SectionSchemaAndDataMessage : WrappedMessage +{ + public SectionSchemaAndData Data { get; } + public string Subtype => ((YamahaTextSegment) RawMessage.Segments[2]).Text; + + private SectionSchemaAndDataMessage(YamahaMessage rawMessage) : base(rawMessage) + { + Data = new((YamahaBinarySegment) RawMessage.Segments[7]); + } + + public static new SectionSchemaAndDataMessage? TryParse(YamahaMessage rawMessage) + { + var segments = rawMessage.Segments; + if (segments.Count != 9 || + segments[0] is not YamahaBinarySegment seg0 || + segments[1] is not YamahaTextSegment seg1 || + segments[2] is not YamahaTextSegment seg2 || + segments[3] is not YamahaUInt16Segment seg3 || + segments[4] is not YamahaUInt32Segment seg4 || + segments[5] is not YamahaUInt32Segment seg5 || + segments[6] is not YamahaUInt32Segment seg6 || + segments[7] is not YamahaBinarySegment seg7 || + segments[8] is not YamahaBinarySegment seg8) + { + return null; + } + + // Basic shape validation... + if (seg0.Data.Length != 1 || seg0.Data[0] != 0 || + seg1.Text != seg2.Text || + seg3.Values.Count != 1 || seg3.Values[0] != 0 || + seg4.Values.Count != 0 || + seg5.Values.Count != 0 || + seg6.Values.Count != 1 || seg6.Values[0] != 0x000000f0 || + seg7.Data.Length < 88 || + seg8.Data.Length != 0) + { + return null; + } + return new(rawMessage); + } +} diff --git a/DigiMixer/DigiMixer.Yamaha/SyncHashesMessage.cs b/DigiMixer/DigiMixer.Yamaha/SyncHashesMessage.cs new file mode 100644 index 00000000..471981c1 --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha/SyncHashesMessage.cs @@ -0,0 +1,35 @@ +using DigiMixer.Yamaha.Core; + +namespace DigiMixer.Yamaha; + +/// +/// A message containing a subtype and two hashes: one for the schema and one for the data. +/// +public sealed class SyncHashesMessage : WrappedMessage +{ + public string Subtype => ((YamahaTextSegment) RawMessage.Segments[1]).Text; + public ReadOnlySpan SchemaHash => ((YamahaBinarySegment) RawMessage.Segments[2]).Data; + public ReadOnlySpan DataHash => ((YamahaBinarySegment) RawMessage.Segments[3]).Data; + + private SyncHashesMessage(YamahaMessage rawMessage) : base(rawMessage) + { + } + + public static new SyncHashesMessage? TryParse(YamahaMessage rawMessage) + { + if (rawMessage.Segments.Count != 4 || + rawMessage.Segments[0] is not YamahaBinarySegment seg0 || + rawMessage.Segments[1] is not YamahaTextSegment || + rawMessage.Segments[2] is not YamahaBinarySegment seg2 || + rawMessage.Segments[3] is not YamahaBinarySegment seg3) + { + return null; + } + if (seg0.Data.Length != 1 || seg0.Data[0] != 0 || + seg2.Data.Length != 16 || seg3.Data.Length != 16) + { + return null; + } + return new(rawMessage); + } +} diff --git a/DigiMixer/DigiMixer.Yamaha/WrappedMessage.cs b/DigiMixer/DigiMixer.Yamaha/WrappedMessage.cs new file mode 100644 index 00000000..6295c911 --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha/WrappedMessage.cs @@ -0,0 +1,16 @@ +using DigiMixer.Yamaha.Core; + +namespace DigiMixer.Yamaha; + +/// +/// A wrapper for an original "raw" message, but with more semantics. +/// +public abstract class WrappedMessage(YamahaMessage rawMessage) +{ + public YamahaMessage RawMessage { get; } = rawMessage; + + public static WrappedMessage? TryParse(YamahaMessage message) => + (WrappedMessage?) SyncHashesMessage.TryParse(message) ?? + (WrappedMessage?) KeepAliveMessage.TryParse(message) ?? + SectionSchemaAndDataMessage.TryParse(message); +} diff --git a/DigiMixer/DigiMixer.Yamaha/YamahaMessages.cs b/DigiMixer/DigiMixer.Yamaha/YamahaMessages.cs new file mode 100644 index 00000000..0d0089db --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha/YamahaMessages.cs @@ -0,0 +1,18 @@ +using DigiMixer.Yamaha.Core; + +namespace DigiMixer.Yamaha; + +public static class YamahaMessages +{ + public static YamahaMessage KeepAlive { get; } = new(YamahaMessageType.EEVT, 0x03010104, + [ + new YamahaUInt32Segment([0x0000]), + new YamahaUInt32Segment([0x0000]), + new YamahaTextSegment("KeepAlive"), + new YamahaTextSegment("") + ]); + + internal static bool IsKeepAlive(YamahaMessage message) => + // We could check more than this, but why bother? + message.Type == KeepAlive.Type && (message.Header == 0x03011004 || message.Header == 0x03010104); +} diff --git a/DigiMixer/DigiMixer.sln b/DigiMixer/DigiMixer.sln index 320b28cb..f36b7712 100644 --- a/DigiMixer/DigiMixer.sln +++ b/DigiMixer/DigiMixer.sln @@ -90,10 +90,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.BehringerWing.Win EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.TfSeries", "DigiMixer.TfSeries\DigiMixer.TfSeries.csproj", "{603E2343-6C02-4A68-A434-BC029AB6F788}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.TfSeries.Core", "DigiMixer.TfSeries.Core\DigiMixer.TfSeries.Core.csproj", "{7E0C4891-63AD-4E59-9717-402DDEAA585A}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.TfSeries.Tools", "DigiMixer.TfSeries.Tools\DigiMixer.TfSeries.Tools.csproj", "{4A66EF70-036A-4AA3-BE57-D56645FEE3CD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.Yamaha.Core", "DigiMixer.Yamaha.Core\DigiMixer.Yamaha.Core.csproj", "{B400F62F-7AC4-49B4-AA76-4DA358DDE8B4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.Yamaha", "DigiMixer.Yamaha\DigiMixer.Yamaha.csproj", "{5EEC5BBA-67C7-4417-9283-53EEFB280FEB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -422,14 +424,6 @@ Global {603E2343-6C02-4A68-A434-BC029AB6F788}.Release|Any CPU.Build.0 = Release|Any CPU {603E2343-6C02-4A68-A434-BC029AB6F788}.Release|x64.ActiveCfg = Release|Any CPU {603E2343-6C02-4A68-A434-BC029AB6F788}.Release|x64.Build.0 = Release|Any CPU - {7E0C4891-63AD-4E59-9717-402DDEAA585A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7E0C4891-63AD-4E59-9717-402DDEAA585A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7E0C4891-63AD-4E59-9717-402DDEAA585A}.Debug|x64.ActiveCfg = Debug|Any CPU - {7E0C4891-63AD-4E59-9717-402DDEAA585A}.Debug|x64.Build.0 = Debug|Any CPU - {7E0C4891-63AD-4E59-9717-402DDEAA585A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7E0C4891-63AD-4E59-9717-402DDEAA585A}.Release|Any CPU.Build.0 = Release|Any CPU - {7E0C4891-63AD-4E59-9717-402DDEAA585A}.Release|x64.ActiveCfg = Release|Any CPU - {7E0C4891-63AD-4E59-9717-402DDEAA585A}.Release|x64.Build.0 = Release|Any CPU {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Debug|Any CPU.Build.0 = Debug|Any CPU {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -438,6 +432,22 @@ Global {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Release|Any CPU.Build.0 = Release|Any CPU {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Release|x64.ActiveCfg = Release|Any CPU {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Release|x64.Build.0 = Release|Any CPU + {B400F62F-7AC4-49B4-AA76-4DA358DDE8B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B400F62F-7AC4-49B4-AA76-4DA358DDE8B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B400F62F-7AC4-49B4-AA76-4DA358DDE8B4}.Debug|x64.ActiveCfg = Debug|Any CPU + {B400F62F-7AC4-49B4-AA76-4DA358DDE8B4}.Debug|x64.Build.0 = Debug|Any CPU + {B400F62F-7AC4-49B4-AA76-4DA358DDE8B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B400F62F-7AC4-49B4-AA76-4DA358DDE8B4}.Release|Any CPU.Build.0 = Release|Any CPU + {B400F62F-7AC4-49B4-AA76-4DA358DDE8B4}.Release|x64.ActiveCfg = Release|Any CPU + {B400F62F-7AC4-49B4-AA76-4DA358DDE8B4}.Release|x64.Build.0 = Release|Any CPU + {5EEC5BBA-67C7-4417-9283-53EEFB280FEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5EEC5BBA-67C7-4417-9283-53EEFB280FEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5EEC5BBA-67C7-4417-9283-53EEFB280FEB}.Debug|x64.ActiveCfg = Debug|Any CPU + {5EEC5BBA-67C7-4417-9283-53EEFB280FEB}.Debug|x64.Build.0 = Debug|Any CPU + {5EEC5BBA-67C7-4417-9283-53EEFB280FEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5EEC5BBA-67C7-4417-9283-53EEFB280FEB}.Release|Any CPU.Build.0 = Release|Any CPU + {5EEC5BBA-67C7-4417-9283-53EEFB280FEB}.Release|x64.ActiveCfg = Release|Any CPU + {5EEC5BBA-67C7-4417-9283-53EEFB280FEB}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/DigiMixer/Protocols/yamaha-tf.md b/DigiMixer/Protocols/yamaha-tf.md index e65a834b..44a90d06 100644 --- a/DigiMixer/Protocols/yamaha-tf.md +++ b/DigiMixer/Protocols/yamaha-tf.md @@ -2,4 +2,11 @@ Currently looks very similar to the DM protocol... but rather than assume this, I'll implement it separately (copying code where appropriate), then see if I -can combine the two and abstract any differences. \ No newline at end of file +can combine the two and abstract any differences. + +## Text protocol details + +There's a text protocol which doesn't do everything we need, but might help in terms of names to look for. + +https://github.com/BrenekH/yamaha-rcp-docs/ +https://github.com/bitfocus/companion-module-yamaha-rcp/ \ No newline at end of file From 38606230f15ed94ea65e2f2b269896d7e9714e1f Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Tue, 1 Jul 2025 19:53:22 +0100 Subject: [PATCH 12/16] More Yamaha TF experimentation (Still incomplete) --- .../ClientExperimentation.cs | 85 ++++++++++++ .../ConvertWireshark.cs | 9 +- .../DecodingOptions.cs | 6 +- .../YamahaMessageExtensions.cs | 128 +++++++++++++----- .../RequestResponseFlag.cs | 7 + .../DigiMixer.Yamaha.Core/YamahaMessage.cs | 23 +++- .../DigiMixer.Yamaha/KeepAliveMessage.cs | 26 ++-- .../DigiMixer.Yamaha/MonitorDataMessage.cs | 21 +++ DigiMixer/DigiMixer.Yamaha/SchemaCol.cs | 2 + DigiMixer/DigiMixer.Yamaha/SchemaProperty.cs | 6 + .../DigiMixer.Yamaha/SchemaPropertyType.cs | 4 +- .../DigiMixer.Yamaha/SectionSchemaAndData.cs | 52 ++++++- .../SectionSchemaAndDataMessage.cs | 52 +++---- .../DigiMixer.Yamaha/SingleValueMessage.cs | 88 ++++++++++++ .../DigiMixer.Yamaha/SyncHashesMessage.cs | 39 +++--- DigiMixer/DigiMixer.Yamaha/WrappedMessage.cs | 8 +- .../YamahaClientExtensions.cs | 9 ++ DigiMixer/DigiMixer.Yamaha/YamahaMessages.cs | 18 --- 18 files changed, 461 insertions(+), 122 deletions(-) create mode 100644 DigiMixer/DigiMixer.TfSeries.Tools/ClientExperimentation.cs create mode 100644 DigiMixer/DigiMixer.Yamaha.Core/RequestResponseFlag.cs create mode 100644 DigiMixer/DigiMixer.Yamaha/MonitorDataMessage.cs create mode 100644 DigiMixer/DigiMixer.Yamaha/SingleValueMessage.cs create mode 100644 DigiMixer/DigiMixer.Yamaha/YamahaClientExtensions.cs delete mode 100644 DigiMixer/DigiMixer.Yamaha/YamahaMessages.cs diff --git a/DigiMixer/DigiMixer.TfSeries.Tools/ClientExperimentation.cs b/DigiMixer/DigiMixer.TfSeries.Tools/ClientExperimentation.cs new file mode 100644 index 00000000..50b2b853 --- /dev/null +++ b/DigiMixer/DigiMixer.TfSeries.Tools/ClientExperimentation.cs @@ -0,0 +1,85 @@ +using DigiMixer.Diagnostics; +using DigiMixer.Yamaha; +using DigiMixer.Yamaha.Core; +using Microsoft.Extensions.Logging.Abstractions; + +namespace DigiMixer.TfSeries.Tools; + +/// +/// Tool expected to change over time: +/// - initializes a client +/// - sends experiment-specific messages +/// - sends keepalives automatically +/// +public class ClientExperimentation : Tool +{ + private const string TfRackHost = "192.168.1.96"; + private const string Dm3Host = "192.168.1.86"; + private const int Port = 50368; + + public override async Task Execute() + { + List messages = + [ + new YamahaMessage(YamahaMessageType.MMIX, 1, RequestResponseFlag.Request, [new YamahaTextSegment("Mixing"), new YamahaBinarySegment([0x80])]), + new MonitorDataMessage(YamahaMessageType.MMIX, RequestResponseFlag.Request).RawMessage, + new SyncHashesMessage(YamahaMessageType.MMIX, "Mixing", 0x10, RequestResponseFlag.Request, new byte[16], new byte[16]).RawMessage, + ]; + + TimeSpan delay = TimeSpan.FromSeconds(1); + DecodingOptions decodingOptions = new(SkipKeepAlive: true, DecodeSchema: false, DecodeData: false, ShowAllSegments: true); + YamahaClient client = null!; + client = new(NullLogger.Instance, TfRackHost, Port, HandleMessage); + await client.Connect(default); + client.Start(); + + foreach (var message in messages) + { + await Send(message); + await Task.Delay(delay); + await SendWrapped(KeepAliveMessage.Request); + await Task.Delay(delay); + } + + while (true) + { + await Task.Delay(delay); + await SendWrapped(KeepAliveMessage.Request); + } + + async Task HandleMessage(YamahaMessage message, CancellationToken cancellationToken) + { + message.DisplayStructure("<=", decodingOptions, Console.Out); + + // Respond to any messages we get, if we can. + if (message.RequestResponse == RequestResponseFlag.Request) + { + var response = GetResponse(message); + if (response is not null) + { + await Send(response); + } + } + } + + YamahaMessage? GetResponse(YamahaMessage request) => WrappedMessage.TryParse(request) switch + { + SyncHashesMessage shm => new SyncHashesMessage(request.Type, shm.Subtype, request.Flag1, RequestResponseFlag.Request, new byte[16], new byte[16]).RawMessage, + //SyncHashesMessage { RawMessage: var raw } shm => new SyncHashesMessage(raw.Type, shm.Subtype, raw.Flag1, RequestResponseFlag.Response, shm.DataHash, new byte[16]).RawMessage, + KeepAliveMessage or MonitorDataMessage => request.AsResponse(), + _ => null + }; + + async Task Send(YamahaMessage message) + { + message.DisplayStructure("=>", decodingOptions, Console.Out); + await client.SendAsync(message, default); + } + + async Task SendWrapped(WrappedMessage message) + { + message.RawMessage.DisplayStructure("=>", decodingOptions, Console.Out); + await client.SendAsync(message, default); + } + } +} diff --git a/DigiMixer/DigiMixer.TfSeries.Tools/ConvertWireshark.cs b/DigiMixer/DigiMixer.TfSeries.Tools/ConvertWireshark.cs index 28cb14b7..abc3be8f 100644 --- a/DigiMixer/DigiMixer.TfSeries.Tools/ConvertWireshark.cs +++ b/DigiMixer/DigiMixer.TfSeries.Tools/ConvertWireshark.cs @@ -1,4 +1,5 @@ using DigiMixer.Diagnostics; +using DigiMixer.Yamaha; using DigiMixer.Yamaha.Core; namespace DigiMixer.TfSeries.Tools; @@ -13,9 +14,15 @@ public override async Task Execute() var dir = Path.GetDirectoryName(file).OrThrow(); var newName = Path.GetFileNameWithoutExtension(file) + " decoded.txt"; using var writer = File.CreateText(Path.Combine(dir, newName)); + var schemaDictionary = new Dictionary(StringComparer.Ordinal); foreach (var message in messages) { - message.DisplayStructure(DecodingOptions.Investigative, writer); + message.DisplayStructure(DecodingOptions.Investigative, writer, schemaDictionary.GetValueOrDefault); + var schemaMessage = SectionSchemaAndDataMessage.TryParse(message.Message); + if (schemaMessage is not null) + { + schemaDictionary[schemaMessage.Subtype] = schemaMessage.Data.Schema; + } } Console.WriteLine($"Saved {newName}"); return 0; diff --git a/DigiMixer/DigiMixer.TfSeries.Tools/DecodingOptions.cs b/DigiMixer/DigiMixer.TfSeries.Tools/DecodingOptions.cs index 6d14c899..2993e7f2 100644 --- a/DigiMixer/DigiMixer.TfSeries.Tools/DecodingOptions.cs +++ b/DigiMixer/DigiMixer.TfSeries.Tools/DecodingOptions.cs @@ -1,7 +1,7 @@ namespace DigiMixer.TfSeries.Tools; -internal record DecodingOptions(bool SkipKeepAlive, bool DecodeSchemaAndData) +internal record DecodingOptions(bool SkipKeepAlive, bool DecodeSchema, bool DecodeData, bool ShowAllSegments) { - internal static DecodingOptions Simple { get; } = new(false, false); - internal static DecodingOptions Investigative { get; } = new(true, true); + internal static DecodingOptions Simple { get; } = new(false, false, false, false); + internal static DecodingOptions Investigative { get; } = new(false, false, false, true); } \ No newline at end of file diff --git a/DigiMixer/DigiMixer.TfSeries.Tools/YamahaMessageExtensions.cs b/DigiMixer/DigiMixer.TfSeries.Tools/YamahaMessageExtensions.cs index 27a7fe13..6533c5de 100644 --- a/DigiMixer/DigiMixer.TfSeries.Tools/YamahaMessageExtensions.cs +++ b/DigiMixer/DigiMixer.TfSeries.Tools/YamahaMessageExtensions.cs @@ -2,64 +2,74 @@ using DigiMixer.Diagnostics; using DigiMixer.Yamaha; using DigiMixer.Yamaha.Core; -using System.Buffers.Binary; -using System.Reflection.Metadata; using System.Security.Cryptography; -using System.Text; -using System.Xml.Linq; namespace DigiMixer.TfSeries.Tools; internal static class YamahaMessageExtensions { - internal static void DisplaySummary(this YamahaMessage message, string direction) - { - Console.WriteLine($"{direction} {message}: {string.Join(", ", message.Segments.Select(SummarizeSegment))}"); - - static string SummarizeSegment(YamahaSegment segment) => segment switch - { - YamahaBinarySegment binary => $"Binary[{binary.Data.Length}]", - YamahaTextSegment text => $"Text['{text.Text}']", - YamahaInt32Segment int32 => $"Int32[*{int32.Values.Count}]", - YamahaUInt32Segment uint32 => $"UInt32[*{uint32.Values.Count}]", - YamahaUInt16Segment uint16 => $"UInt16[*{uint16.Values.Count}]", - _ => throw new InvalidOperationException("Unknown segment type") - }; - } - - internal static void DisplayStructure(this AnnotatedMessage annotatedMessage, DecodingOptions options, TextWriter writer) + internal static void DisplayStructure(this AnnotatedMessage annotatedMessage, DecodingOptions options, TextWriter writer, Func? schemaProvider = null) { var message = annotatedMessage.Message; - if (options.SkipKeepAlive && message.Type.Text == "EEVT" && message.Segments.Count > 3 && message.Segments[0] is YamahaTextSegment { Text: "KeepAlive" }) + if (options.SkipKeepAlive && KeepAliveMessage.IsKeepAlive(message)) { return; } string directionIndicator = annotatedMessage.Direction == MessageDirection.ClientToMixer ? "=>" : "<="; - writer.WriteLine($"{directionIndicator} 0x{annotatedMessage.StreamOffset:x8} {message}"); - DisplaySegments(message, options, writer); + writer.WriteLine($"{DateTime.UtcNow:HH:mm:ss.fff}: {directionIndicator} 0x{annotatedMessage.StreamOffset:x8} {message}"); + DisplayBody(message, options, writer, schemaProvider); } - internal static void DisplayStructure(this YamahaMessage message, string directionIndicator, DecodingOptions options, TextWriter writer) + internal static void DisplayStructure(this YamahaMessage message, string directionIndicator, DecodingOptions options, TextWriter writer, Func? schemaProvider = null) { - if (options.SkipKeepAlive && message.Type.Text == "EEVT" && message.Segments.Count > 3 && message.Segments[0] is YamahaTextSegment { Text: "KeepAlive" }) + if (options.SkipKeepAlive && KeepAliveMessage.IsKeepAlive(message)) { return; } - writer.WriteLine($"{directionIndicator} {message}"); - DisplaySegments(message, options, writer); + writer.WriteLine($"{DateTime.UtcNow:HH:mm:ss.fff}: {directionIndicator} {message}"); + DisplayBody(message, options, writer, schemaProvider); } - private static void DisplaySegments(YamahaMessage message, DecodingOptions options, TextWriter writer) + private static void DisplayBody(YamahaMessage message, DecodingOptions options, TextWriter writer, Func? schemaProvider = null) { - foreach (var segment in message.Segments) + var wrappedMessage = WrappedMessage.TryParse(message); + + switch (wrappedMessage) { - writer.WriteLine($" {DescribeSegment(segment)}"); + case SectionSchemaAndDataMessage section: + writer.WriteLine($" SectionSchemaAndData ({section.Data.Name})"); + var hash = MD5.HashData(((YamahaBinarySegment) message.Segments[7]).Data); + writer.WriteLine($" MD5: {Formatting.ToHex(hash)}"); + if (options.DecodeSchema) + { + DescribeSchema(section.Data); + } + if (options.DecodeData) + { + DescribeData(section.Data); + } + break; + case SyncHashesMessage shm: + writer.WriteLine($" SyncHashes: {shm.Subtype}"); + break; + case KeepAliveMessage kam: + writer.WriteLine(" KeepAlive"); + break; + case SingleValueMessage svm: + writer.WriteLine($" SingleValue ({svm.SectionName}): Value={DescribeSegment(svm.ValueSegment)}"); + if (schemaProvider?.Invoke(svm.SectionName) is SchemaCol schema) + { + var property = svm.ResolveProperty(schema); + writer.WriteLine($" Property: {property.Path} (Indexes {string.Join(", ", svm.SchemaIndexes)})"); + } + break; } - if (SectionSchemaAndDataMessage.TryParse(message) is { } section && options.DecodeSchemaAndData) + if (wrappedMessage is null || options.ShowAllSegments) { - var hash = MD5.HashData(((YamahaBinarySegment) message.Segments[7]).Data); - writer.WriteLine($" MD5: {Formatting.ToHex(hash)}"); - DescribeSchemaAndData(section.Data); + foreach (var segment in message.Segments) + { + writer.WriteLine($" {DescribeSegment(segment)}"); + } } writer.WriteLine(); @@ -85,7 +95,7 @@ static string DescribeSegment(YamahaSegment segment) } } - void DescribeSchemaAndData(SectionSchemaAndData section) + void DescribeSchema(SectionSchemaAndData section) { writer.WriteLine($" Hash text: {section.SchemaHash}"); writer.WriteLine($" Schema:"); @@ -98,12 +108,58 @@ void DescribeCol(SchemaCol col, string indent) var nestedIndent = indent + " "; foreach (var property in col.Properties) { - writer.WriteLine($"{nestedIndent} PR: {property.Name}; Type={property.Type}; Length={property.Length}; Count={property.Count}"); + writer.WriteLine($"{nestedIndent}PR: {property.Name}; Type={property.Type}; Length={property.Length}; Count={property.Count}"); } foreach (var nested in col.Cols) { DescribeCol(nested, nestedIndent); } } + + void DescribeData(SectionSchemaAndData section) + { + DescribeColData(section, section.Schema, "", " ", 0); + } + + void DescribeColData(SectionSchemaAndData section, SchemaCol col, string colIndex, string indent, int additionalOffset) + { + writer.WriteLine($"{indent}COL{colIndex}: {col.Name}"); + foreach (var property in col.Properties) + { + for (int i = 0; i < property.Count; i++) + { + int propertyAdditionalOffset = additionalOffset + i * property.Length; + string description = property.Type switch + { + SchemaPropertyType.Text => section.GetString(property, propertyAdditionalOffset), + SchemaPropertyType.UnsignedInteger => property.Length switch + { + 1 => section.GetUInt8(property, propertyAdditionalOffset).ToString(), + 2 => section.GetUInt16(property, propertyAdditionalOffset).ToString(), + 4 => section.GetUInt32(property, propertyAdditionalOffset).ToString(), + _ => throw new InvalidOperationException($"Unexpected length {property.Length}") + }, + SchemaPropertyType.SignedInteger => property.Length switch + { + 1 => section.GetInt8(property, propertyAdditionalOffset).ToString(), + 2 => section.GetInt16(property, propertyAdditionalOffset).ToString(), + 4 => section.GetInt32(property, propertyAdditionalOffset).ToString(), + _ => throw new InvalidOperationException($"Unexpected length {property.Length}") + }, + _ => throw new InvalidOperationException($"Unexpected property type {property.Type}") + }; + string index = property.Count == 1 ? "" : $"[{i}]"; + writer.WriteLine($"{indent} {property.Name}{index}: {description}"); + } + } + foreach (var nestedCol in col.Cols) + { + for (int i = 0; i < nestedCol.Count; i++) + { + string nestedColIndex = nestedCol.Count == 1 ? "" : $"[{i}]"; + DescribeColData(section, nestedCol, nestedColIndex, indent + " ", additionalOffset + i * nestedCol.DataLength); + } + } + } } } diff --git a/DigiMixer/DigiMixer.Yamaha.Core/RequestResponseFlag.cs b/DigiMixer/DigiMixer.Yamaha.Core/RequestResponseFlag.cs new file mode 100644 index 00000000..55c8e800 --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha.Core/RequestResponseFlag.cs @@ -0,0 +1,7 @@ +namespace DigiMixer.Yamaha.Core; + +public enum RequestResponseFlag : byte +{ + Request = 0x01, + Response = 0x10 +} diff --git a/DigiMixer/DigiMixer.Yamaha.Core/YamahaMessage.cs b/DigiMixer/DigiMixer.Yamaha.Core/YamahaMessage.cs index b6fd9eca..79546c4e 100644 --- a/DigiMixer/DigiMixer.Yamaha.Core/YamahaMessage.cs +++ b/DigiMixer/DigiMixer.Yamaha.Core/YamahaMessage.cs @@ -17,6 +17,9 @@ public class YamahaMessage : IMixerMessage /// public uint Header { get; } + public byte Flag1 => (byte) (Header >> 16); + public RequestResponseFlag RequestResponse => (RequestResponseFlag) (byte) (Header >> 8); + public int Length => 4 // Type + 4 // Message length (excluding type and length) @@ -27,6 +30,18 @@ public class YamahaMessage : IMixerMessage public ImmutableList Segments { get; } + public YamahaMessage(YamahaMessageType type, byte flag1, RequestResponseFlag requestResponse, ImmutableList segments) : + this(type, ConstructHeader(type, flag1, requestResponse, segments.Count), segments) + { + } + + private static uint ConstructHeader(YamahaMessageType type, byte flag1, RequestResponseFlag requestResponse, int segmentCount) => + (uint) ((type.HeaderByte << 24) | + (flag1 << 16) | + (((byte) requestResponse) << 8) | + ((byte) segmentCount)); + + // Potentially deprecate? public YamahaMessage(YamahaMessageType type, uint header, ImmutableList segments) { Type = type; @@ -40,8 +55,14 @@ public YamahaMessage(YamahaMessageType type, uint header, ImmutableList new(Type, Flag1, RequestResponseFlag.Response, Segments); + public static YamahaMessage? TryParse(ReadOnlySpan data) { if (data.Length < 8) @@ -108,5 +129,5 @@ public void CopyTo(Span buffer) } } - public override string ToString() => $"{Type.Text,-4}: Flag1={(Header>>16) & 0xff:x2}; Flag2={(Header >> 8) & 0xff:x2}; Segments={Segments.Count}"; + public override string ToString() => $"{Type.Text,-4}: Flag1={Flag1:x2}; ReqResp={RequestResponse}; Segments={Segments.Count}"; } diff --git a/DigiMixer/DigiMixer.Yamaha/KeepAliveMessage.cs b/DigiMixer/DigiMixer.Yamaha/KeepAliveMessage.cs index 5c5c3b6f..228f5437 100644 --- a/DigiMixer/DigiMixer.Yamaha/KeepAliveMessage.cs +++ b/DigiMixer/DigiMixer.Yamaha/KeepAliveMessage.cs @@ -4,13 +4,20 @@ namespace DigiMixer.Yamaha; public sealed class KeepAliveMessage : WrappedMessage { - // TODO: Do we need to worry about the different headers here? Maybe we should have two instances? - public static KeepAliveMessage Instance { get; } = new(new (YamahaMessageType.EEVT, 0x03010104, + public static KeepAliveMessage Request { get; } = new(new (YamahaMessageType.EEVT, 0x01, RequestResponseFlag.Request, [ new YamahaUInt32Segment([0x0000]), - new YamahaUInt32Segment([0x0000]), - new YamahaTextSegment("KeepAlive"), - new YamahaTextSegment("") + new YamahaUInt32Segment([0x0000]), + new YamahaTextSegment("KeepAlive"), + new YamahaTextSegment("") + ])); + + public static KeepAliveMessage Response { get; } = new(new (YamahaMessageType.EEVT, 0x01, RequestResponseFlag.Response, + [ + new YamahaUInt32Segment([0x0000]), + new YamahaUInt32Segment([0x0000]), + new YamahaTextSegment("KeepAlive"), + new YamahaTextSegment("") ])); private KeepAliveMessage(YamahaMessage rawMessage) : base(rawMessage) @@ -18,10 +25,11 @@ private KeepAliveMessage(YamahaMessage rawMessage) : base(rawMessage) } public static new KeepAliveMessage? TryParse(YamahaMessage rawMessage) => - IsKeepAlive(rawMessage) ? Instance : null; + IsKeepAlive(rawMessage) ? (rawMessage.RequestResponse == RequestResponseFlag.Request ? Request : Response) + : null; - internal static bool IsKeepAlive(YamahaMessage message) => - // We could check more than this, but why bother? - message.Type == Instance.RawMessage.Type && (message.Header == 0x03011004 || message.Header == 0x03010104); + public static bool IsKeepAlive(YamahaMessage message) => + // We could check more than this, but why bother? + message.Type == Request.RawMessage.Type && message.Flag1 == Request.RawMessage.Flag1; } diff --git a/DigiMixer/DigiMixer.Yamaha/MonitorDataMessage.cs b/DigiMixer/DigiMixer.Yamaha/MonitorDataMessage.cs new file mode 100644 index 00000000..7f0f635c --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha/MonitorDataMessage.cs @@ -0,0 +1,21 @@ +using DigiMixer.Yamaha.Core; + +namespace DigiMixer.Yamaha; + +public sealed class MonitorDataMessage : WrappedMessage +{ + public MonitorDataMessage(YamahaMessageType type, RequestResponseFlag requestResponse) + : this(new(type, 0x4, requestResponse, [])) + { + } + + private MonitorDataMessage(YamahaMessage rawMessage) : base(rawMessage) + { + } + + public static new MonitorDataMessage? TryParse(YamahaMessage rawMessage) => + IsMonitorDataMessage(rawMessage) ? new(rawMessage) : null; + + private static bool IsMonitorDataMessage(YamahaMessage rawMessage) => + rawMessage.Flag1 == 0x04 && rawMessage.Segments.Count == 0; +} diff --git a/DigiMixer/DigiMixer.Yamaha/SchemaCol.cs b/DigiMixer/DigiMixer.Yamaha/SchemaCol.cs index 640ec8e3..3298ad69 100644 --- a/DigiMixer/DigiMixer.Yamaha/SchemaCol.cs +++ b/DigiMixer/DigiMixer.Yamaha/SchemaCol.cs @@ -7,6 +7,7 @@ namespace DigiMixer.Yamaha; public sealed class SchemaCol { public string Name { get; } + public string Path { get; } public ImmutableList Cols { get; } public ImmutableList Properties { get; } @@ -45,6 +46,7 @@ internal SchemaCol(SchemaCol? parent, ReadOnlySpan schema) throw new ArgumentException($"Unexpected schema data; expected COL, got {Encoding.ASCII.GetString(schema[0..3])}"); } Name = Encoding.ASCII.GetString(schema.Slice(4, 24)).TrimEnd('\0'); + Path = parent is null ? Name : $"{parent.Path}/{Name}"; RelativeOffset = BinaryPrimitives.ReadInt32LittleEndian(schema.Slice(36, 4)); AbsoluteOffset = (parent?.AbsoluteOffset ?? 0) + RelativeOffset; DataLength = BinaryPrimitives.ReadInt32LittleEndian(schema.Slice(40, 4)); diff --git a/DigiMixer/DigiMixer.Yamaha/SchemaProperty.cs b/DigiMixer/DigiMixer.Yamaha/SchemaProperty.cs index ffa19e03..908be7f3 100644 --- a/DigiMixer/DigiMixer.Yamaha/SchemaProperty.cs +++ b/DigiMixer/DigiMixer.Yamaha/SchemaProperty.cs @@ -10,6 +10,11 @@ public sealed class SchemaProperty /// public string Name { get; } + /// + /// The full path to the property (via ancestors). + /// + public string Path { get; } + /// /// The type of the property. /// @@ -51,6 +56,7 @@ internal SchemaProperty(SchemaCol parent, int offset, ReadOnlySpan schema) Length = BinaryPrimitives.ReadUInt16LittleEndian(schema[4..6]); Count = BinaryPrimitives.ReadUInt16LittleEndian(schema[6..8]); Name = Encoding.ASCII.GetString(schema.Slice(8, 24)).TrimEnd('\0'); + Path = $"{parent.Path}/{Name}"; Parent = parent; RelativeOffset = offset; AbsoluteOffset = offset + parent.AbsoluteOffset; diff --git a/DigiMixer/DigiMixer.Yamaha/SchemaPropertyType.cs b/DigiMixer/DigiMixer.Yamaha/SchemaPropertyType.cs index 50ccb412..2049db08 100644 --- a/DigiMixer/DigiMixer.Yamaha/SchemaPropertyType.cs +++ b/DigiMixer/DigiMixer.Yamaha/SchemaPropertyType.cs @@ -3,6 +3,6 @@ public enum SchemaPropertyType : byte { Text = 0, - Float = 1, - Integer= 2 + SignedInteger = 1, + UnsignedInteger= 2 } diff --git a/DigiMixer/DigiMixer.Yamaha/SectionSchemaAndData.cs b/DigiMixer/DigiMixer.Yamaha/SectionSchemaAndData.cs index 8a83a8fd..6776d88e 100644 --- a/DigiMixer/DigiMixer.Yamaha/SectionSchemaAndData.cs +++ b/DigiMixer/DigiMixer.Yamaha/SectionSchemaAndData.cs @@ -9,6 +9,8 @@ namespace DigiMixer.Yamaha; /// public sealed class SectionSchemaAndData { + private const int HeaderLength = 88; + private readonly YamahaBinarySegment segment; private readonly int schemaLength; @@ -16,23 +18,67 @@ public sealed class SectionSchemaAndData public string SchemaHash { get; } public SchemaCol Schema { get; } + public string GetString(SchemaProperty property, int additionalOffset = 0) + { + var offset = GetOffset(property, additionalOffset); + return Encoding.ASCII.GetString(segment.Data.Slice(offset, property.Length)).TrimEnd('\0'); + } + + public short GetInt16(SchemaProperty property, int additionalOffset = 0) + { + var offset = GetOffset(property, additionalOffset); + return BinaryPrimitives.ReadInt16LittleEndian(segment.Data[offset..]); + } + + public int GetInt32(SchemaProperty property, int additionalOffset = 0) + { + var offset = GetOffset(property, additionalOffset); + return BinaryPrimitives.ReadInt32LittleEndian(segment.Data[offset..]); + } + + public ushort GetUInt16(SchemaProperty property, int additionalOffset = 0) + { + var offset = GetOffset(property, additionalOffset); + return BinaryPrimitives.ReadUInt16LittleEndian(segment.Data[offset..]); + } + + public uint GetUInt32(SchemaProperty property, int additionalOffset = 0) + { + var offset = GetOffset(property, additionalOffset); + return BinaryPrimitives.ReadUInt32LittleEndian(segment.Data[offset..]); + } + + public byte GetUInt8(SchemaProperty property, int additionalOffset = 0) + { + var offset = GetOffset(property, additionalOffset); + return segment.Data[offset]; + } + + public sbyte GetInt8(SchemaProperty property, int additionalOffset = 0) + { + var offset = GetOffset(property, additionalOffset); + return (sbyte) segment.Data[offset]; + } + + private int GetOffset(SchemaProperty property, int additionalOffset = 0) => HeaderLength + schemaLength + property.AbsoluteOffset + additionalOffset; + public SectionSchemaAndData(YamahaBinarySegment segment) { this.segment = segment; var segmentData = segment.Data; - var header = segmentData[..88]; + var header = segmentData[..HeaderLength]; Name = Encoding.ASCII.GetString(header[8..44]).TrimEnd('\0'); SchemaHash = Encoding.ASCII.GetString(header[44..76]); schemaLength = BinaryPrimitives.ReadInt32LittleEndian(header[80..]); var dataLength = BinaryPrimitives.ReadInt32LittleEndian(header[84..]); - if (schemaLength + dataLength + 88 != segmentData.Length) + if (schemaLength + dataLength + HeaderLength != segmentData.Length) { throw new ArgumentException($"Invalid section data length. Segment length: {segmentData.Length}; schema length: {schemaLength}; values length: {dataLength}"); } // For now, assume that there's a single root Col. - Schema = new SchemaCol(null, segmentData.Slice(88, schemaLength)); + Schema = new SchemaCol(null, segmentData.Slice(HeaderLength, schemaLength)); } } diff --git a/DigiMixer/DigiMixer.Yamaha/SectionSchemaAndDataMessage.cs b/DigiMixer/DigiMixer.Yamaha/SectionSchemaAndDataMessage.cs index e3b70a34..509460b5 100644 --- a/DigiMixer/DigiMixer.Yamaha/SectionSchemaAndDataMessage.cs +++ b/DigiMixer/DigiMixer.Yamaha/SectionSchemaAndDataMessage.cs @@ -12,35 +12,27 @@ private SectionSchemaAndDataMessage(YamahaMessage rawMessage) : base(rawMessage) Data = new((YamahaBinarySegment) RawMessage.Segments[7]); } - public static new SectionSchemaAndDataMessage? TryParse(YamahaMessage rawMessage) - { - var segments = rawMessage.Segments; - if (segments.Count != 9 || - segments[0] is not YamahaBinarySegment seg0 || - segments[1] is not YamahaTextSegment seg1 || - segments[2] is not YamahaTextSegment seg2 || - segments[3] is not YamahaUInt16Segment seg3 || - segments[4] is not YamahaUInt32Segment seg4 || - segments[5] is not YamahaUInt32Segment seg5 || - segments[6] is not YamahaUInt32Segment seg6 || - segments[7] is not YamahaBinarySegment seg7 || - segments[8] is not YamahaBinarySegment seg8) - { - return null; - } + public static new SectionSchemaAndDataMessage? TryParse(YamahaMessage rawMessage) => + IsSectionSchemaAndDataMessage(rawMessage) ? new(rawMessage) : null; - // Basic shape validation... - if (seg0.Data.Length != 1 || seg0.Data[0] != 0 || - seg1.Text != seg2.Text || - seg3.Values.Count != 1 || seg3.Values[0] != 0 || - seg4.Values.Count != 0 || - seg5.Values.Count != 0 || - seg6.Values.Count != 1 || seg6.Values[0] != 0x000000f0 || - seg7.Data.Length < 88 || - seg8.Data.Length != 0) - { - return null; - } - return new(rawMessage); - } + private static bool IsSectionSchemaAndDataMessage(YamahaMessage rawMessage) => + rawMessage.Flag1 == 0x14 && + rawMessage.Segments.Count == 9 && + rawMessage.Segments[0] is YamahaBinarySegment seg0 && + seg0.Data.Length == 1 && seg0.Data[0] == 0 && + rawMessage.Segments[1] is YamahaTextSegment seg1 && + rawMessage.Segments[2] is YamahaTextSegment seg2 && + seg1.Text == seg2.Text && + rawMessage.Segments[3] is YamahaUInt16Segment seg3 && + seg3.Values.Count == 1 && seg3.Values[0] == 0 && + rawMessage.Segments[4] is YamahaUInt32Segment seg4 && + seg4.Values.Count == 0 && + rawMessage.Segments[5] is YamahaUInt32Segment seg5 && + seg5.Values.Count == 0 && + rawMessage.Segments[6] is YamahaUInt32Segment seg6 && + seg6.Values.Count == 1 && seg6.Values[0] == 0x000000f0 && + rawMessage.Segments[7] is YamahaBinarySegment seg7 && + seg7.Data.Length >= 88 && + rawMessage.Segments[8] is YamahaBinarySegment seg8 && + seg8.Data.Length == 0; } diff --git a/DigiMixer/DigiMixer.Yamaha/SingleValueMessage.cs b/DigiMixer/DigiMixer.Yamaha/SingleValueMessage.cs new file mode 100644 index 00000000..54f50f81 --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha/SingleValueMessage.cs @@ -0,0 +1,88 @@ +using DigiMixer.Yamaha.Core; +using System.Collections.Immutable; +using System.Security.Principal; + +namespace DigiMixer.Yamaha; + +/// +/// Reports/requests a change to a single value. +/// +public sealed class SingleValueMessage : WrappedMessage +{ + public string SectionName => ((YamahaTextSegment) RawMessage.Segments[1]).Text; + + /// + /// A path navigating from the top of the section schema to the relevant property. + /// Each value is the index of the property/col within a . + /// + public ImmutableList SchemaPath => ((YamahaUInt32Segment) RawMessage.Segments[4]).Values; + + /// + /// The indexes along the path from the root col, e.g. indicating which channel is being represented. + /// + public ImmutableList SchemaIndexes => ((YamahaUInt32Segment) RawMessage.Segments[5]).Values; + + // We don't know if this is really a client ID yet - it always seems to be 0xa0 or 0xa1. + public uint ClientId => ((YamahaUInt32Segment) RawMessage.Segments[6]).Values[0]; + + public YamahaSegment ValueSegment => RawMessage.Segments[7]; + + public SchemaProperty ResolveProperty(SchemaCol rootCol) + { + var currentCol = rootCol; + // Everything up until the last value is a col, then there's a property. + var colCount = SchemaPath.Count - 1; + + for (int i = 0; i < colCount; i++) + { + var schemaIndex = (int) SchemaPath[i]; + var colIndex = schemaIndex - currentCol.Properties.Count; + if (colIndex < 0 || colIndex >= currentCol.Cols.Count) + { + throw new ArgumentException($"Invalid schema path: {string.Join(".", SchemaPath)} at index {i}"); + } + currentCol = currentCol.Cols[colIndex]; + if (SchemaIndexes[i] >= currentCol.Count) + { + throw new ArgumentException($"Invalid schema index {SchemaIndexes[i]} in schema path: {string.Join(".", SchemaPath)} at index {i}"); + } + } + + var propertyIndex = (int) SchemaPath[colCount]; + if (propertyIndex < 0 || propertyIndex >= currentCol.Properties.Count) + { + throw new ArgumentException($"Invalid schema path: {string.Join(".", SchemaPath)} at final index for property"); + } + var property = currentCol.Properties[propertyIndex]; + if (SchemaIndexes[colCount] >= currentCol.Count) + { + throw new ArgumentException($"Invalid schema index {SchemaIndexes[colCount]} in schema path: {string.Join(".", SchemaPath)} for property"); + } + return property; + } + + private SingleValueMessage(YamahaMessage rawMessage) : base(rawMessage) + { + } + + public static new SingleValueMessage? TryParse(YamahaMessage rawMessage) => + IsSingleValueMessage(rawMessage) ? new SingleValueMessage(rawMessage) : null; + + private static bool IsSingleValueMessage(YamahaMessage rawMessage) => + rawMessage.Segments.Count == 8 && + rawMessage.Flag1 == 0x11 && + // From MixingStation, it's a UInt16[*1] - and then the response has text + // for the fifth segment... + // rawMessage.Segments[0] is YamahaBinarySegment seg0 && + // seg0.Data.Length == 1 && seg0.Data[0] == 0 && + rawMessage.Segments[1] is YamahaTextSegment seg1 && + rawMessage.Segments[2] is YamahaTextSegment seg2 && + seg1.Text == seg2.Text && + rawMessage.Segments[3] is YamahaUInt16Segment seg3 && + seg3.Values.Count == 1 && + rawMessage.Segments[4] is YamahaUInt32Segment seg4 && + rawMessage.Segments[5] is YamahaUInt32Segment seg5 && + seg4.Values.Count == seg5.Values.Count && + seg4.Values.Count == seg3.Values[0] && + rawMessage.Segments[6] is YamahaUInt32Segment; +} diff --git a/DigiMixer/DigiMixer.Yamaha/SyncHashesMessage.cs b/DigiMixer/DigiMixer.Yamaha/SyncHashesMessage.cs index 471981c1..1d3d33db 100644 --- a/DigiMixer/DigiMixer.Yamaha/SyncHashesMessage.cs +++ b/DigiMixer/DigiMixer.Yamaha/SyncHashesMessage.cs @@ -11,25 +11,32 @@ public sealed class SyncHashesMessage : WrappedMessage public ReadOnlySpan SchemaHash => ((YamahaBinarySegment) RawMessage.Segments[2]).Data; public ReadOnlySpan DataHash => ((YamahaBinarySegment) RawMessage.Segments[3]).Data; - private SyncHashesMessage(YamahaMessage rawMessage) : base(rawMessage) + public SyncHashesMessage(YamahaMessageType type, string subtype, byte flag1, RequestResponseFlag requestResponse, ReadOnlySpan schemaHash, ReadOnlySpan dataHash) + : this(new YamahaMessage(type, flag1, requestResponse, + [ + new YamahaBinarySegment([0]), + new YamahaTextSegment(subtype), + new YamahaBinarySegment(schemaHash), + new YamahaBinarySegment(dataHash), + ])) { } - public static new SyncHashesMessage? TryParse(YamahaMessage rawMessage) + private SyncHashesMessage(YamahaMessage rawMessage) : base(rawMessage) { - if (rawMessage.Segments.Count != 4 || - rawMessage.Segments[0] is not YamahaBinarySegment seg0 || - rawMessage.Segments[1] is not YamahaTextSegment || - rawMessage.Segments[2] is not YamahaBinarySegment seg2 || - rawMessage.Segments[3] is not YamahaBinarySegment seg3) - { - return null; - } - if (seg0.Data.Length != 1 || seg0.Data[0] != 0 || - seg2.Data.Length != 16 || seg3.Data.Length != 16) - { - return null; - } - return new(rawMessage); } + + public static new SyncHashesMessage? TryParse(YamahaMessage rawMessage) => + IsSyncHashesMessage(rawMessage) ? new(rawMessage) : null; + + private static bool IsSyncHashesMessage(YamahaMessage rawMessage) => + rawMessage.Flag1 == 0x10 && + rawMessage.Segments.Count == 4 && + rawMessage.Segments[0] is YamahaBinarySegment seg0 && + seg0.Data.Length == 1 && seg0.Data[0] == 0 && + rawMessage.Segments[1] is YamahaTextSegment && + rawMessage.Segments[2] is YamahaBinarySegment seg2 && + seg2.Data.Length == 16 && + rawMessage.Segments[3] is YamahaBinarySegment seg3 && + seg3.Data.Length == 16; } diff --git a/DigiMixer/DigiMixer.Yamaha/WrappedMessage.cs b/DigiMixer/DigiMixer.Yamaha/WrappedMessage.cs index 6295c911..f7843e85 100644 --- a/DigiMixer/DigiMixer.Yamaha/WrappedMessage.cs +++ b/DigiMixer/DigiMixer.Yamaha/WrappedMessage.cs @@ -10,7 +10,9 @@ public abstract class WrappedMessage(YamahaMessage rawMessage) public YamahaMessage RawMessage { get; } = rawMessage; public static WrappedMessage? TryParse(YamahaMessage message) => - (WrappedMessage?) SyncHashesMessage.TryParse(message) ?? - (WrappedMessage?) KeepAliveMessage.TryParse(message) ?? - SectionSchemaAndDataMessage.TryParse(message); + SyncHashesMessage.TryParse(message) ?? + KeepAliveMessage.TryParse(message) ?? + SectionSchemaAndDataMessage.TryParse(message) ?? + MonitorDataMessage.TryParse(message) ?? + (WrappedMessage?) SingleValueMessage.TryParse(message); } diff --git a/DigiMixer/DigiMixer.Yamaha/YamahaClientExtensions.cs b/DigiMixer/DigiMixer.Yamaha/YamahaClientExtensions.cs new file mode 100644 index 00000000..8257000d --- /dev/null +++ b/DigiMixer/DigiMixer.Yamaha/YamahaClientExtensions.cs @@ -0,0 +1,9 @@ +using DigiMixer.Yamaha.Core; + +namespace DigiMixer.Yamaha; + +public static class YamahaClientExtensions +{ + public static Task SendAsync(this YamahaClient client, WrappedMessage message, CancellationToken cancellationToken) => + client.SendAsync(message.RawMessage, cancellationToken); +} diff --git a/DigiMixer/DigiMixer.Yamaha/YamahaMessages.cs b/DigiMixer/DigiMixer.Yamaha/YamahaMessages.cs deleted file mode 100644 index 0d0089db..00000000 --- a/DigiMixer/DigiMixer.Yamaha/YamahaMessages.cs +++ /dev/null @@ -1,18 +0,0 @@ -using DigiMixer.Yamaha.Core; - -namespace DigiMixer.Yamaha; - -public static class YamahaMessages -{ - public static YamahaMessage KeepAlive { get; } = new(YamahaMessageType.EEVT, 0x03010104, - [ - new YamahaUInt32Segment([0x0000]), - new YamahaUInt32Segment([0x0000]), - new YamahaTextSegment("KeepAlive"), - new YamahaTextSegment("") - ]); - - internal static bool IsKeepAlive(YamahaMessage message) => - // We could check more than this, but why bother? - message.Type == KeepAlive.Type && (message.Header == 0x03011004 || message.Header == 0x03010104); -} From 7f51643133aa8d19ce8f79281efa1a4653e1ebc0 Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Tue, 1 Jul 2025 20:32:52 +0100 Subject: [PATCH 13/16] First steps towards the SQ protocol, which looks very similar to CQ so far The intention is to have a generalised Allen and Heath project which the SQ, CQ and QU utilise - but we're not there yet. --- .../AHControlClient.cs | 20 +++ .../AHMessageFormat.cs | 16 +++ .../AHMeterClient.cs | 49 +++++++ .../AHRawMessage.cs | 128 +++++++++++++++++ .../DigiMixer.AllenAndHeath.Core.csproj | 12 ++ .../DigiMixer.Hardware.csproj | 2 + .../DigiMixer.SqSeries.Core.csproj | 12 ++ .../SqControlClient.cs | 5 + .../SqMessageFormat.cs | 16 +++ .../DigiMixer.SqSeries.Core/SqRawMessage.cs | 129 ++++++++++++++++++ .../ClientInitialMessages.cs | 42 ++++++ .../ConvertAllWireshark.cs | 20 +++ .../ConvertWireshark.cs | 22 +++ .../DigiMixer.SqSeries.Tools.csproj | 15 ++ DigiMixer/DigiMixer.SqSeries.Tools/Program.cs | 3 + .../SqRawMessageExtensions.cs | 15 ++ DigiMixer/DigiMixer.SqSeries/AssemblyInfo.cs | 3 + .../DigiMixer.SqSeries.csproj | 13 ++ .../SqClientInitRequestMessage.cs | 19 +++ .../SqClientInitResponseMessage.cs | 15 ++ DigiMixer/DigiMixer.SqSeries/SqMessage.cs | 53 +++++++ DigiMixer/DigiMixer.SqSeries/SqMessageType.cs | 17 +++ DigiMixer/DigiMixer.SqSeries/SqMixer.cs | 10 ++ .../SqSimpleRequestMessage.cs | 17 +++ .../SqUdpHandshakeMessage.cs | 18 +++ .../DigiMixer.SqSeries/SqUnknownMessage.cs | 10 ++ .../SqVersionRequestMessage.cs | 16 +++ .../SqVersionResponseMessage.cs | 18 +++ DigiMixer/DigiMixer.sln | 31 +++++ DigiMixer/Protocols/ah-sq.md | 2 + 30 files changed, 748 insertions(+) create mode 100644 DigiMixer/DigiMixer.AllenAndHeath.Core/AHControlClient.cs create mode 100644 DigiMixer/DigiMixer.AllenAndHeath.Core/AHMessageFormat.cs create mode 100644 DigiMixer/DigiMixer.AllenAndHeath.Core/AHMeterClient.cs create mode 100644 DigiMixer/DigiMixer.AllenAndHeath.Core/AHRawMessage.cs create mode 100644 DigiMixer/DigiMixer.AllenAndHeath.Core/DigiMixer.AllenAndHeath.Core.csproj create mode 100644 DigiMixer/DigiMixer.SqSeries.Core/DigiMixer.SqSeries.Core.csproj create mode 100644 DigiMixer/DigiMixer.SqSeries.Core/SqControlClient.cs create mode 100644 DigiMixer/DigiMixer.SqSeries.Core/SqMessageFormat.cs create mode 100644 DigiMixer/DigiMixer.SqSeries.Core/SqRawMessage.cs create mode 100644 DigiMixer/DigiMixer.SqSeries.Tools/ClientInitialMessages.cs create mode 100644 DigiMixer/DigiMixer.SqSeries.Tools/ConvertAllWireshark.cs create mode 100644 DigiMixer/DigiMixer.SqSeries.Tools/ConvertWireshark.cs create mode 100644 DigiMixer/DigiMixer.SqSeries.Tools/DigiMixer.SqSeries.Tools.csproj create mode 100644 DigiMixer/DigiMixer.SqSeries.Tools/Program.cs create mode 100644 DigiMixer/DigiMixer.SqSeries.Tools/SqRawMessageExtensions.cs create mode 100644 DigiMixer/DigiMixer.SqSeries/AssemblyInfo.cs create mode 100644 DigiMixer/DigiMixer.SqSeries/DigiMixer.SqSeries.csproj create mode 100644 DigiMixer/DigiMixer.SqSeries/SqClientInitRequestMessage.cs create mode 100644 DigiMixer/DigiMixer.SqSeries/SqClientInitResponseMessage.cs create mode 100644 DigiMixer/DigiMixer.SqSeries/SqMessage.cs create mode 100644 DigiMixer/DigiMixer.SqSeries/SqMessageType.cs create mode 100644 DigiMixer/DigiMixer.SqSeries/SqMixer.cs create mode 100644 DigiMixer/DigiMixer.SqSeries/SqSimpleRequestMessage.cs create mode 100644 DigiMixer/DigiMixer.SqSeries/SqUdpHandshakeMessage.cs create mode 100644 DigiMixer/DigiMixer.SqSeries/SqUnknownMessage.cs create mode 100644 DigiMixer/DigiMixer.SqSeries/SqVersionRequestMessage.cs create mode 100644 DigiMixer/DigiMixer.SqSeries/SqVersionResponseMessage.cs create mode 100644 DigiMixer/Protocols/ah-sq.md diff --git a/DigiMixer/DigiMixer.AllenAndHeath.Core/AHControlClient.cs b/DigiMixer/DigiMixer.AllenAndHeath.Core/AHControlClient.cs new file mode 100644 index 00000000..21c0b1c0 --- /dev/null +++ b/DigiMixer/DigiMixer.AllenAndHeath.Core/AHControlClient.cs @@ -0,0 +1,20 @@ +using DigiMixer.Core; +using Microsoft.Extensions.Logging; + +namespace DigiMixer.AllenAndHeath.Core; + +public sealed class AHControlClient : TcpMessageProcessingControllerBase +{ + public event EventHandler? MessageReceived; + + public AHControlClient(ILogger logger, string host, int port) : base(logger, host, port, bufferSize: 65540) + { + } + + protected override Task ProcessMessage(AHRawMessage message, CancellationToken cancellationToken) + { + Logger.LogTrace("Received control message: {message}", message); + MessageReceived?.Invoke(this, message); + return Task.CompletedTask; + } +} diff --git a/DigiMixer/DigiMixer.AllenAndHeath.Core/AHMessageFormat.cs b/DigiMixer/DigiMixer.AllenAndHeath.Core/AHMessageFormat.cs new file mode 100644 index 00000000..1634bd94 --- /dev/null +++ b/DigiMixer/DigiMixer.AllenAndHeath.Core/AHMessageFormat.cs @@ -0,0 +1,16 @@ +namespace DigiMixer.AllenAndHeath.Core; + +public enum AHMessageFormat +{ + VariableLength, + + /// + /// Total of 8 bytes: the fixed-length indicator and 7 bytes of data + /// + FixedLength8, + + /// + /// Total of 9 bytes: the fixed-length indicator and 8 bytes of data + /// + FixedLength9 +} diff --git a/DigiMixer/DigiMixer.AllenAndHeath.Core/AHMeterClient.cs b/DigiMixer/DigiMixer.AllenAndHeath.Core/AHMeterClient.cs new file mode 100644 index 00000000..997af6a4 --- /dev/null +++ b/DigiMixer/DigiMixer.AllenAndHeath.Core/AHMeterClient.cs @@ -0,0 +1,49 @@ +using DigiMixer.AllenAndHeath.Core; +using DigiMixer.Core; +using Microsoft.Extensions.Logging; +using System.Buffers; +using System.Net; + +namespace AllenAndHeath.Core; + +public class AHMeterClient : UdpControllerBase, IDisposable +{ + private readonly MemoryPool SendingPool = MemoryPool.Shared; + + public ushort LocalUdpPort { get; } + public event EventHandler? MessageReceived; + + private AHMeterClient(ILogger logger, ushort localUdpPort) : base(logger, localUdpPort) + { + LocalUdpPort = localUdpPort; + } + + public AHMeterClient(ILogger logger) : this(logger, FindAvailableUdpPort()) + { + } + + public async Task SendAsync(AHRawMessage message, IPEndPoint mixerUdpEndPoint, CancellationToken cancellationToken) + { + if (Logger.IsEnabled(LogLevel.Trace)) + { + Logger.LogTrace("Sending keep-alive message"); + } + using var memoryOwner = SendingPool.Rent(message.Length); + var memory = memoryOwner.Memory[..message.Length]; + message.CopyTo(memory.Span); + await Send(memory, mixerUdpEndPoint, cancellationToken); + } + + protected override void ProcessData(ReadOnlySpan data) + { + if (AHRawMessage.TryParse(data) is not AHRawMessage message) + { + return; + } + if (Logger.IsEnabled(LogLevel.Trace)) + { + Logger.LogTrace("Received meter message: {message}", message); + } + MessageReceived?.Invoke(this, message); + } +} diff --git a/DigiMixer/DigiMixer.AllenAndHeath.Core/AHRawMessage.cs b/DigiMixer/DigiMixer.AllenAndHeath.Core/AHRawMessage.cs new file mode 100644 index 00000000..f6804295 --- /dev/null +++ b/DigiMixer/DigiMixer.AllenAndHeath.Core/AHRawMessage.cs @@ -0,0 +1,128 @@ +using DigiMixer.Core; +using System.Buffers.Binary; + +namespace DigiMixer.AllenAndHeath.Core; + +/// +/// A raw, uninterpreted (other than format and type) Allen and Heath message. +/// +public sealed class AHRawMessage : IMixerMessage +{ + private const byte VariableLengthPrefix = 0x7f; + private const byte FixedLengthPrefix = 0xf7; + + public AHMessageFormat Format { get; } + + /// + /// The message type, which is null if and only if the format is FixedLength8 or FixedLength9. + /// + public byte? Type { get; } + + private ReadOnlyMemory data; + + public ReadOnlySpan Data => data.Span; + + private AHRawMessage(AHMessageFormat format, byte? type, ReadOnlyMemory data) + { + Format = format; + Type = type; + this.data = data; + } + + public static AHRawMessage ForFixedLength(ReadOnlyMemory data) + { + var format = data.Length switch + { + 7 => AHMessageFormat.FixedLength8, + 8 => AHMessageFormat.FixedLength9, + _ => throw new ArgumentException() + }; + return new(format, null, data); + } + + public static AHRawMessage ForVariableLength(byte type, ReadOnlyMemory data) => + new(AHMessageFormat.VariableLength, type, data); + + /// + /// Length of the total message, including header. + /// + public int Length => Format switch + { + AHMessageFormat.VariableLength => Data.Length + 6, + AHMessageFormat.FixedLength8 => 8, + AHMessageFormat.FixedLength9 => 9, + _ => throw new InvalidOperationException() + }; + + public static AHRawMessage? TryParse(ReadOnlySpan data) + { + if (data.Length == 0) + { + return null; + } + return data[0] switch + { + VariableLengthPrefix => TryParseVariableLength(data.ToArray()), + FixedLengthPrefix => TryParseFixedLength(data.ToArray()), + _ => throw new ArgumentException($"Invalid data: first byte is 0x{data[0]:x2}") + }; + } + + private static AHRawMessage? TryParseVariableLength(ReadOnlyMemory data) + { + if (data.Length < 6) + { + return null; + } + byte type = data.Span[1]; + int dataLength = BinaryPrimitives.ReadInt32LittleEndian(data[2..6].Span); + if (data.Length < dataLength + 6) + { + return null; + } + return new AHRawMessage(AHMessageFormat.VariableLength, type, data[6..(dataLength + 6)].ToArray()); + } + + private static AHRawMessage? TryParseFixedLength(ReadOnlyMemory data) + { + if (data.Length < 8) + { + return null; + } + // Last of these has only been seen on the SQ... + if ((data.Span[1] == 0x12 && data.Span[3] == 0x23) || + (data.Span[1] == 0x13 && data.Span[3] == 0x16) || + (data.Span[1] == 0x1a && data.Span[2] == 0x1a && data.Span[3] == 0x26)) + { + if (data.Length < 9) + { + return null; + } + return ForFixedLength(data[1..9]); + } + + return ForFixedLength(data[1..8]); + } + + public override string ToString() => $"Type={Type}; Length={Data.Length}"; + + public void CopyTo(Span buffer) + { + switch (Format) + { + case AHMessageFormat.VariableLength: + buffer[0] = VariableLengthPrefix; + buffer[1] = Type!.Value; + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(2, 4), Data.Length); + data.Span.CopyTo(buffer.Slice(6)); + break; + case AHMessageFormat.FixedLength8: + case AHMessageFormat.FixedLength9: + buffer[0] = FixedLengthPrefix; + data.Span.CopyTo(buffer.Slice(1)); + break; + default: + throw new InvalidOperationException(); + } + } +} diff --git a/DigiMixer/DigiMixer.AllenAndHeath.Core/DigiMixer.AllenAndHeath.Core.csproj b/DigiMixer/DigiMixer.AllenAndHeath.Core/DigiMixer.AllenAndHeath.Core.csproj new file mode 100644 index 00000000..d63e8663 --- /dev/null +++ b/DigiMixer/DigiMixer.AllenAndHeath.Core/DigiMixer.AllenAndHeath.Core.csproj @@ -0,0 +1,12 @@ + + + + net9.0 + enable + enable + + + + + + diff --git a/DigiMixer/DigiMixer.Hardware/DigiMixer.Hardware.csproj b/DigiMixer/DigiMixer.Hardware/DigiMixer.Hardware.csproj index c5ac37c2..7e216ea5 100644 --- a/DigiMixer/DigiMixer.Hardware/DigiMixer.Hardware.csproj +++ b/DigiMixer/DigiMixer.Hardware/DigiMixer.Hardware.csproj @@ -28,6 +28,7 @@ + @@ -39,6 +40,7 @@ + diff --git a/DigiMixer/DigiMixer.SqSeries.Core/DigiMixer.SqSeries.Core.csproj b/DigiMixer/DigiMixer.SqSeries.Core/DigiMixer.SqSeries.Core.csproj new file mode 100644 index 00000000..a4886fab --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries.Core/DigiMixer.SqSeries.Core.csproj @@ -0,0 +1,12 @@ + + + + net9.0 + enable + enable + + + + + + diff --git a/DigiMixer/DigiMixer.SqSeries.Core/SqControlClient.cs b/DigiMixer/DigiMixer.SqSeries.Core/SqControlClient.cs new file mode 100644 index 00000000..1dc7411c --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries.Core/SqControlClient.cs @@ -0,0 +1,5 @@ +namespace DigiMixer.SqSeries.Core; + +public sealed class SqControlClient +{ +} diff --git a/DigiMixer/DigiMixer.SqSeries.Core/SqMessageFormat.cs b/DigiMixer/DigiMixer.SqSeries.Core/SqMessageFormat.cs new file mode 100644 index 00000000..79065ed4 --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries.Core/SqMessageFormat.cs @@ -0,0 +1,16 @@ +namespace DigiMixer.SqSeries.Core; + +public enum SqMessageFormat +{ + VariableLength, + + /// + /// Total of 8 bytes: the fixed-length indicator and 7 bytes of data + /// + FixedLength8, + + /// + /// Total of 9 bytes: the fixed-length indicator and 8 bytes of data + /// + FixedLength9 +} diff --git a/DigiMixer/DigiMixer.SqSeries.Core/SqRawMessage.cs b/DigiMixer/DigiMixer.SqSeries.Core/SqRawMessage.cs new file mode 100644 index 00000000..b4a33cb6 --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries.Core/SqRawMessage.cs @@ -0,0 +1,129 @@ +using DigiMixer.Core; +using System.Buffers.Binary; + +namespace DigiMixer.SqSeries.Core; + +/// +/// A raw, uninterpreted (other than format and type) Sq message. +/// +public sealed class AHRawMessage : IMixerMessage +{ + private const byte VariableLengthPrefix = 0x7f; + private const byte FixedLengthPrefix = 0xf7; + + public SqMessageFormat Format { get; } + + /// + /// The message type, which is null if and only if the format is VariableLengthmessages. + /// + public SqMessageType? Type { get; } + + private ReadOnlyMemory data; + + public ReadOnlySpan Data => data.Span; + + private AHRawMessage(SqMessageFormat format, SqMessageType? type, ReadOnlyMemory data) + { + Format = format; + Type = type; + this.data = data; + } + + internal static AHRawMessage ForFixedLength(ReadOnlyMemory data) + { + var format = data.Length switch + { + 7 => SqMessageFormat.FixedLength8, + 8 => SqMessageFormat.FixedLength9, + }; + return new(format, null, data); + } + + internal static AHRawMessage ForVariableLength(SqMessageType type, ReadOnlyMemory data) => + new(SqMessageFormat.VariableLength, type, data); + + /// + /// Length of the total message, including header. + /// + public int Length => Format switch + { + SqMessageFormat.VariableLength => Data.Length + 6, + SqMessageFormat.FixedLength8 => 8, + SqMessageFormat.FixedLength9 => 9, + _ => throw new InvalidOperationException() + }; + + public static AHRawMessage? TryParse(ReadOnlySpan data) + { + Console.WriteLine($"Parsing {data.Length} bytes"); + if (data.Length == 0) + { + return null; + } + return data[0] switch + { + VariableLengthPrefix => TryParseVariableLength(data.ToArray()), + FixedLengthPrefix => TryParseFixedLength(data.ToArray()), + _ => throw new ArgumentException($"Invalid data: first byte is 0x{data[0]:x2}") + }; + } + + private static AHRawMessage? TryParseVariableLength(ReadOnlyMemory data) + { + if (data.Length < 6) + { + return null; + } + SqMessageType type = (SqMessageType) data.Span[1]; + int dataLength = BinaryPrimitives.ReadInt32LittleEndian(data[2..6].Span); + if (data.Length < dataLength + 6) + { + return null; + } + return new AHRawMessage(SqMessageFormat.VariableLength, type, data[6..(dataLength + 6)].ToArray()); + } + + private static AHRawMessage? TryParseFixedLength(ReadOnlyMemory data) + { + if (data.Length < 8) + { + return null; + } + // Last of these has only been seen on the SQ... + if ((data.Span[1] == 0x12 && data.Span[3] == 0x23) || + (data.Span[1] == 0x13 && data.Span[3] == 0x16) || + (data.Span[1] == 0x1a && data.Span[2] == 0x1a && data.Span[3] == 0x26)) + { + if (data.Length < 9) + { + return null; + } + return ForFixedLength(data[1..9]); + } + + return ForFixedLength(data[1..8]); + } + + public override string ToString() => $"Type={Type}; Length={Data.Length}"; + + public void CopyTo(Span buffer) + { + switch (Format) + { + case SqMessageFormat.VariableLength: + buffer[0] = VariableLengthPrefix; + buffer[1] = (byte) Type.Value; + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(2, 4), Data.Length); + data.Span.CopyTo(buffer.Slice(6)); + break; + case SqMessageFormat.FixedLength8: + case SqMessageFormat.FixedLength9: + buffer[0] = FixedLengthPrefix; + data.Span.CopyTo(buffer.Slice(1)); + break; + default: + throw new InvalidOperationException(); + } + + } +} diff --git a/DigiMixer/DigiMixer.SqSeries.Tools/ClientInitialMessages.cs b/DigiMixer/DigiMixer.SqSeries.Tools/ClientInitialMessages.cs new file mode 100644 index 00000000..2b620aa8 --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries.Tools/ClientInitialMessages.cs @@ -0,0 +1,42 @@ +using AllenAndHeath.Core; +using DigiMixer.AllenAndHeath.Core; +using DigiMixer.Diagnostics; +using Microsoft.Extensions.Logging.Abstractions; + +namespace DigiMixer.SqSeries.Tools; + +public class ClientInitialMessages : Tool +{ + public override async Task Execute() + { + var meterClient = new AHMeterClient(NullLogger.Instance); + var controlClient = new AHControlClient(NullLogger.Instance, "192.168.1.56", 51326); + + controlClient.MessageReceived += LogMessage; + await controlClient.Connect(default); + controlClient.Start(); + + await SendAsync(new SqUdpHandshakeMessage(meterClient.LocalUdpPort)); + await Task.Delay(500); + await SendAsync(new SqVersionRequestMessage()); + await Task.Delay(500); + await SendAsync(new SqClientInitRequestMessage()); + await Task.Delay(500); + await SendAsync(new SqSimpleRequestMessage(SqMessageType.FullDataRequest)); + await Task.Delay(500); + await SendAsync(new SqSimpleRequestMessage(SqMessageType.Type15Request)); + await Task.Delay(500); + await SendAsync(new SqSimpleRequestMessage(SqMessageType.Type17Request)); + await Task.Delay(5000); + + return 0; + + Task SendAsync (SqMessage message) => controlClient.SendAsync(message.RawMessage, default); + } + + void LogMessage(object? sender, AHRawMessage message) + { + var sqMessage = SqMessage.FromRawMessage(message); + Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss.fff}: {sqMessage}"); + } +} diff --git a/DigiMixer/DigiMixer.SqSeries.Tools/ConvertAllWireshark.cs b/DigiMixer/DigiMixer.SqSeries.Tools/ConvertAllWireshark.cs new file mode 100644 index 00000000..c78300d3 --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries.Tools/ConvertAllWireshark.cs @@ -0,0 +1,20 @@ +using DigiMixer.Diagnostics; + +namespace DigiMixer.SqSeries.Tools; + +public class ConvertAllWireshark(string directory) : Tool +{ + public override async Task Execute() + { + foreach (var file in Directory.GetFiles(directory, "*.pcapng")) + { + var singleFileTool = new ConvertWireshark(file); + var result = await singleFileTool.Execute(); + if (result != 0) + { + return result; + } + } + return 0; + } +} diff --git a/DigiMixer/DigiMixer.SqSeries.Tools/ConvertWireshark.cs b/DigiMixer/DigiMixer.SqSeries.Tools/ConvertWireshark.cs new file mode 100644 index 00000000..8cae3f9c --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries.Tools/ConvertWireshark.cs @@ -0,0 +1,22 @@ +using DigiMixer.Diagnostics; +using DigiMixer.AllenAndHeath.Core; +namespace DigiMixer.SqSeries.Tools; + +public class ConvertWireshark(string file) : Tool +{ + public override async Task Execute() + { + var dump = WiresharkDump.Load(file); + var messages = await dump.ProcessMessages("192.168.1.56", "192.168.1.140"); + + var dir = Path.GetDirectoryName(file).OrThrow(); + var newName = Path.GetFileNameWithoutExtension(file) + " decoded.txt"; + using var writer = File.CreateText(Path.Combine(dir, newName)); + foreach (var message in messages) + { + message.DisplayStructure(writer); + } + Console.WriteLine($"Saved {newName}"); + return 0; + } +} diff --git a/DigiMixer/DigiMixer.SqSeries.Tools/DigiMixer.SqSeries.Tools.csproj b/DigiMixer/DigiMixer.SqSeries.Tools/DigiMixer.SqSeries.Tools.csproj new file mode 100644 index 00000000..39193e9f --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries.Tools/DigiMixer.SqSeries.Tools.csproj @@ -0,0 +1,15 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + diff --git a/DigiMixer/DigiMixer.SqSeries.Tools/Program.cs b/DigiMixer/DigiMixer.SqSeries.Tools/Program.cs new file mode 100644 index 00000000..dda3891e --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries.Tools/Program.cs @@ -0,0 +1,3 @@ +using DigiMixer.Diagnostics; + +await Tool.ExecuteFromCommandLine(args, typeof(Program)); diff --git a/DigiMixer/DigiMixer.SqSeries.Tools/SqRawMessageExtensions.cs b/DigiMixer/DigiMixer.SqSeries.Tools/SqRawMessageExtensions.cs new file mode 100644 index 00000000..705466c0 --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries.Tools/SqRawMessageExtensions.cs @@ -0,0 +1,15 @@ +using DigiMixer.Diagnostics; +using DigiMixer.AllenAndHeath.Core; + +namespace DigiMixer.SqSeries.Tools; + +internal static class AHRawMessageExtensions +{ + internal static void DisplayStructure(this AnnotatedMessage annotatedMessage, TextWriter writer) + { + var message = annotatedMessage.Message; + string directionIndicator = annotatedMessage.Direction == MessageDirection.ClientToMixer ? "=>" : "<="; + var sqMessage = SqMessage.FromRawMessage(message); + writer.WriteLine($"{DateTime.UtcNow:HH:mm:ss.fff}: {directionIndicator} 0x{annotatedMessage.StreamOffset:x8} {sqMessage}"); + } +} diff --git a/DigiMixer/DigiMixer.SqSeries/AssemblyInfo.cs b/DigiMixer/DigiMixer.SqSeries/AssemblyInfo.cs new file mode 100644 index 00000000..359712f2 --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("DigiMixer.SqSeries.Tools")] \ No newline at end of file diff --git a/DigiMixer/DigiMixer.SqSeries/DigiMixer.SqSeries.csproj b/DigiMixer/DigiMixer.SqSeries/DigiMixer.SqSeries.csproj new file mode 100644 index 00000000..258223a1 --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries/DigiMixer.SqSeries.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + + + + + + + diff --git a/DigiMixer/DigiMixer.SqSeries/SqClientInitRequestMessage.cs b/DigiMixer/DigiMixer.SqSeries/SqClientInitRequestMessage.cs new file mode 100644 index 00000000..6307f1d0 --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries/SqClientInitRequestMessage.cs @@ -0,0 +1,19 @@ +using DigiMixer.AllenAndHeath.Core; + +namespace DigiMixer.SqSeries; + +internal class SqClientInitRequestMessage : SqMessage +{ + // We don't know what this means at the moment, but it's always 1... + public ushort ClientValue => GetUInt16(0); + + internal SqClientInitRequestMessage() : base(SqMessageType.ClientInitRequest, [2, 0]) + { + } + + internal SqClientInitRequestMessage(AHRawMessage rawMessage) : base(rawMessage) + { + } + + public override string ToString() => $"Type={Type}; ClientValue={ClientValue}"; +} diff --git a/DigiMixer/DigiMixer.SqSeries/SqClientInitResponseMessage.cs b/DigiMixer/DigiMixer.SqSeries/SqClientInitResponseMessage.cs new file mode 100644 index 00000000..0b2e57e7 --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries/SqClientInitResponseMessage.cs @@ -0,0 +1,15 @@ +using DigiMixer.AllenAndHeath.Core; + +namespace DigiMixer.SqSeries; + +public class SqClientInitResponseMessage : SqMessage +{ + // We don't know what this means at the moment, but it's always 1... + public ushort MixerValue => GetUInt16(0); + + internal SqClientInitResponseMessage(AHRawMessage rawMessage) : base(rawMessage) + { + } + + public override string ToString() => $"Type={Type}; MixerValue={MixerValue}"; +} diff --git a/DigiMixer/DigiMixer.SqSeries/SqMessage.cs b/DigiMixer/DigiMixer.SqSeries/SqMessage.cs new file mode 100644 index 00000000..76097337 --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries/SqMessage.cs @@ -0,0 +1,53 @@ +using DigiMixer.AllenAndHeath.Core; +using DigiMixer.Core; +using System.Buffers.Binary; +using System.Text; + +namespace DigiMixer.SqSeries; + +/// +/// Base class for type-specific messages. These wrap . +/// +public abstract class SqMessage(AHRawMessage rawMessage) +{ + public AHRawMessage RawMessage { get; } = rawMessage; + + public ReadOnlySpan Data => RawMessage.Data; + public SqMessageType? Type => (SqMessageType?) RawMessage.Type; + + public override string ToString() => Type is null ? $"Fixed: {Formatting.ToHex(Data)}" : $"Type={Type}; Length={Data.Length}"; + + protected SqMessage(SqMessageType type, byte[] data) : this(AHRawMessage.ForVariableLength((byte) type, data)) + { + } + + internal ushort GetUInt16(int index) => BinaryPrimitives.ReadUInt16LittleEndian(Data[index..]); + + internal string? GetString(int offset, int maxLength) + { + int length = Data.Slice(offset, maxLength).IndexOf((byte) 0); + if (length == -1) + { + length = maxLength; + } + return length == 0 ? null : Encoding.ASCII.GetString(Data.Slice(offset, length)); + } + + public static SqMessage FromRawMessage(AHRawMessage rawMessage) => (SqMessageType?) rawMessage.Type switch + { + SqMessageType.UdpHandshake => new SqUdpHandshakeMessage(rawMessage), + //SqMessageType.Regular => new SqRegularMessage(rawMessage), + //SqMessageType.KeepAlive => new SqKeepAliveMessage(rawMessage), + SqMessageType.VersionRequest => new SqVersionRequestMessage(rawMessage), + SqMessageType.VersionResponse => new SqVersionResponseMessage(rawMessage), + SqMessageType.ClientInitRequest => new SqClientInitRequestMessage(rawMessage), + SqMessageType.ClientInitResponse => new SqClientInitResponseMessage(rawMessage), + SqMessageType.FullDataRequest => new SqSimpleRequestMessage(rawMessage), + SqMessageType.Type15Request => new SqSimpleRequestMessage(rawMessage), + SqMessageType.Type17Request => new SqSimpleRequestMessage(rawMessage), + //SqMessageType.FullDataResponse => new SqFullDataResponseMessage(rawMessage), + //SqMessageType.InputMeters => new SqInputMetersMessage(rawMessage), + //SqMessageType.OutputMeters => new SqOutputMetersMessage(rawMessage), + _ => new SqUnknownMessage(rawMessage) + }; +} diff --git a/DigiMixer/DigiMixer.SqSeries/SqMessageType.cs b/DigiMixer/DigiMixer.SqSeries/SqMessageType.cs new file mode 100644 index 00000000..ca8a45d8 --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries/SqMessageType.cs @@ -0,0 +1,17 @@ +namespace DigiMixer.SqSeries; + +public enum SqMessageType : byte +{ + UdpHandshake = 0, + VersionRequest = 1, + VersionResponse = 2, + FullDataRequest = 3, + FullDataResponse = 4, + ClientInitRequest = 11, + ClientInitResponse = 12, + UsersRequest = 20, + UsersResponse = 21, + Type13Request = 13, + Type15Request = 15, + Type17Request = 17, +} diff --git a/DigiMixer/DigiMixer.SqSeries/SqMixer.cs b/DigiMixer/DigiMixer.SqSeries/SqMixer.cs new file mode 100644 index 00000000..ff597032 --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries/SqMixer.cs @@ -0,0 +1,10 @@ +using DigiMixer.Core; +using Microsoft.Extensions.Logging; + +namespace DigiMixer.SqSeries; + +public class SqMixer +{ + public static IMixerApi CreateMixerApi(ILogger logger, string host, int port = 51326, MixerApiOptions? options = null) => + throw new NotImplementedException(); +} diff --git a/DigiMixer/DigiMixer.SqSeries/SqSimpleRequestMessage.cs b/DigiMixer/DigiMixer.SqSeries/SqSimpleRequestMessage.cs new file mode 100644 index 00000000..dfbf521e --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries/SqSimpleRequestMessage.cs @@ -0,0 +1,17 @@ +using DigiMixer.AllenAndHeath.Core; + +namespace DigiMixer.SqSeries; + +/// +/// A simple request message with no additional data. +/// +public class SqSimpleRequestMessage : SqMessage +{ + internal SqSimpleRequestMessage(SqMessageType type) : base(type, Array.Empty()) + { + } + + internal SqSimpleRequestMessage(AHRawMessage rawMessage) : base(rawMessage) + { + } +} diff --git a/DigiMixer/DigiMixer.SqSeries/SqUdpHandshakeMessage.cs b/DigiMixer/DigiMixer.SqSeries/SqUdpHandshakeMessage.cs new file mode 100644 index 00000000..a4574ba3 --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries/SqUdpHandshakeMessage.cs @@ -0,0 +1,18 @@ +using DigiMixer.AllenAndHeath.Core; + +namespace DigiMixer.SqSeries; + +internal sealed class SqUdpHandshakeMessage : SqMessage +{ + public ushort UdpPort => GetUInt16(0); + + public SqUdpHandshakeMessage(ushort udpPort) : base(SqMessageType.UdpHandshake, [(byte) udpPort, (byte) (udpPort >> 8)]) + { + } + + internal SqUdpHandshakeMessage(AHRawMessage message) : base(message) + { + } + + public override string ToString() => $"Type={Type}; UdpPort={UdpPort}"; +} diff --git a/DigiMixer/DigiMixer.SqSeries/SqUnknownMessage.cs b/DigiMixer/DigiMixer.SqSeries/SqUnknownMessage.cs new file mode 100644 index 00000000..1752f72a --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries/SqUnknownMessage.cs @@ -0,0 +1,10 @@ +using DigiMixer.AllenAndHeath.Core; + +namespace DigiMixer.SqSeries; + +internal sealed class SqUnknownMessage : SqMessage +{ + internal SqUnknownMessage(AHRawMessage rawMessage) : base(rawMessage) + { + } +} diff --git a/DigiMixer/DigiMixer.SqSeries/SqVersionRequestMessage.cs b/DigiMixer/DigiMixer.SqSeries/SqVersionRequestMessage.cs new file mode 100644 index 00000000..1a7e444e --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries/SqVersionRequestMessage.cs @@ -0,0 +1,16 @@ +using DigiMixer.AllenAndHeath.Core; + +namespace DigiMixer.SqSeries; + +internal class SqVersionRequestMessage : SqMessage +{ + public SqVersionRequestMessage() : base(SqMessageType.VersionRequest, []) + { + } + + internal SqVersionRequestMessage(AHRawMessage rawMessage) : base(rawMessage) + { + } + + public override string ToString() => $"Type={Type}"; +} diff --git a/DigiMixer/DigiMixer.SqSeries/SqVersionResponseMessage.cs b/DigiMixer/DigiMixer.SqSeries/SqVersionResponseMessage.cs new file mode 100644 index 00000000..e032d5d4 --- /dev/null +++ b/DigiMixer/DigiMixer.SqSeries/SqVersionResponseMessage.cs @@ -0,0 +1,18 @@ +using DigiMixer.AllenAndHeath.Core; + +namespace DigiMixer.SqSeries; + +internal class SqVersionResponseMessage : SqMessage +{ + public string Version => $"{Data[1]}.{Data[2]}.{Data[3]} r{GetUInt16(4)}"; + + public SqVersionResponseMessage(byte[] data) : base(SqMessageType.VersionResponse, data) + { + } + + internal SqVersionResponseMessage(AHRawMessage rawMessage) : base(rawMessage) + { + } + + public override string ToString() => $"Type={Type}; Version={Version}"; +} diff --git a/DigiMixer/DigiMixer.sln b/DigiMixer/DigiMixer.sln index f36b7712..c96994f6 100644 --- a/DigiMixer/DigiMixer.sln +++ b/DigiMixer/DigiMixer.sln @@ -35,6 +35,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.UCNet.Core", "Dig EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Protocols", "Protocols", "{0B03065C-EBBD-446F-9BC4-1D26FD96B439}" ProjectSection(SolutionItems) = preProject + Protocols\ah-sq.md = Protocols\ah-sq.md Protocols\behringer-wing.md = Protocols\behringer-wing.md Protocols\mackie.md = Protocols\mackie.md Protocols\yamaha-tf.md = Protocols\yamaha-tf.md @@ -96,6 +97,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.Yamaha.Core", "Di EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.Yamaha", "DigiMixer.Yamaha\DigiMixer.Yamaha.csproj", "{5EEC5BBA-67C7-4417-9283-53EEFB280FEB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.SqSeries", "DigiMixer.SqSeries\DigiMixer.SqSeries.csproj", "{25452C06-6C94-4E3F-9978-DC42CE9CAD9C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.SqSeries.Tools", "DigiMixer.SqSeries.Tools\DigiMixer.SqSeries.Tools.csproj", "{E1E0C51C-D634-4945-A310-16438CA78797}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.AllenAndHeath.Core", "DigiMixer.AllenAndHeath.Core\DigiMixer.AllenAndHeath.Core.csproj", "{082C4376-E6AC-45AA-B985-65E7142F171C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -448,6 +455,30 @@ Global {5EEC5BBA-67C7-4417-9283-53EEFB280FEB}.Release|Any CPU.Build.0 = Release|Any CPU {5EEC5BBA-67C7-4417-9283-53EEFB280FEB}.Release|x64.ActiveCfg = Release|Any CPU {5EEC5BBA-67C7-4417-9283-53EEFB280FEB}.Release|x64.Build.0 = Release|Any CPU + {25452C06-6C94-4E3F-9978-DC42CE9CAD9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25452C06-6C94-4E3F-9978-DC42CE9CAD9C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25452C06-6C94-4E3F-9978-DC42CE9CAD9C}.Debug|x64.ActiveCfg = Debug|Any CPU + {25452C06-6C94-4E3F-9978-DC42CE9CAD9C}.Debug|x64.Build.0 = Debug|Any CPU + {25452C06-6C94-4E3F-9978-DC42CE9CAD9C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25452C06-6C94-4E3F-9978-DC42CE9CAD9C}.Release|Any CPU.Build.0 = Release|Any CPU + {25452C06-6C94-4E3F-9978-DC42CE9CAD9C}.Release|x64.ActiveCfg = Release|Any CPU + {25452C06-6C94-4E3F-9978-DC42CE9CAD9C}.Release|x64.Build.0 = Release|Any CPU + {E1E0C51C-D634-4945-A310-16438CA78797}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1E0C51C-D634-4945-A310-16438CA78797}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1E0C51C-D634-4945-A310-16438CA78797}.Debug|x64.ActiveCfg = Debug|Any CPU + {E1E0C51C-D634-4945-A310-16438CA78797}.Debug|x64.Build.0 = Debug|Any CPU + {E1E0C51C-D634-4945-A310-16438CA78797}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1E0C51C-D634-4945-A310-16438CA78797}.Release|Any CPU.Build.0 = Release|Any CPU + {E1E0C51C-D634-4945-A310-16438CA78797}.Release|x64.ActiveCfg = Release|Any CPU + {E1E0C51C-D634-4945-A310-16438CA78797}.Release|x64.Build.0 = Release|Any CPU + {082C4376-E6AC-45AA-B985-65E7142F171C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {082C4376-E6AC-45AA-B985-65E7142F171C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {082C4376-E6AC-45AA-B985-65E7142F171C}.Debug|x64.ActiveCfg = Debug|Any CPU + {082C4376-E6AC-45AA-B985-65E7142F171C}.Debug|x64.Build.0 = Debug|Any CPU + {082C4376-E6AC-45AA-B985-65E7142F171C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {082C4376-E6AC-45AA-B985-65E7142F171C}.Release|Any CPU.Build.0 = Release|Any CPU + {082C4376-E6AC-45AA-B985-65E7142F171C}.Release|x64.ActiveCfg = Release|Any CPU + {082C4376-E6AC-45AA-B985-65E7142F171C}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/DigiMixer/Protocols/ah-sq.md b/DigiMixer/Protocols/ah-sq.md new file mode 100644 index 00000000..0e020e72 --- /dev/null +++ b/DigiMixer/Protocols/ah-sq.md @@ -0,0 +1,2 @@ +# Allen and Heath SQ protocol + From 6a3123d55a0b06bffd349c2f39e5f013886e6deb Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Sat, 15 Nov 2025 13:59:10 +0000 Subject: [PATCH 14/16] Update dependencies --- .../CameraControl.Visca/CameraControl.Visca.csproj | 2 +- DigiMixer/DigiMixer.Core/DigiMixer.Core.csproj | 2 +- DigiMixer/DigiMixer.Hardware/DigiMixer.Hardware.csproj | 2 +- DigiMixer/DigiMixer.Ssc/DigiMixer.Ssc.csproj | 2 +- IconPlatform/IconPlatform.Model/IconPlatform.Model.csproj | 2 +- WpfUtil/JonSkeet.CoreAppUtil/JonSkeet.CoreAppUtil.csproj | 8 ++++---- XTouchMini/XTouchMini.Model/XTouchMini.Model.csproj | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CameraControl/CameraControl.Visca/CameraControl.Visca.csproj b/CameraControl/CameraControl.Visca/CameraControl.Visca.csproj index 9a18874d..e9530236 100644 --- a/CameraControl/CameraControl.Visca/CameraControl.Visca.csproj +++ b/CameraControl/CameraControl.Visca/CameraControl.Visca.csproj @@ -6,6 +6,6 @@ 10 - + diff --git a/DigiMixer/DigiMixer.Core/DigiMixer.Core.csproj b/DigiMixer/DigiMixer.Core/DigiMixer.Core.csproj index f70b70aa..aac5142d 100644 --- a/DigiMixer/DigiMixer.Core/DigiMixer.Core.csproj +++ b/DigiMixer/DigiMixer.Core/DigiMixer.Core.csproj @@ -9,7 +9,7 @@ - + diff --git a/DigiMixer/DigiMixer.Hardware/DigiMixer.Hardware.csproj b/DigiMixer/DigiMixer.Hardware/DigiMixer.Hardware.csproj index 7e216ea5..908e2e68 100644 --- a/DigiMixer/DigiMixer.Hardware/DigiMixer.Hardware.csproj +++ b/DigiMixer/DigiMixer.Hardware/DigiMixer.Hardware.csproj @@ -22,7 +22,7 @@ - + diff --git a/DigiMixer/DigiMixer.Ssc/DigiMixer.Ssc.csproj b/DigiMixer/DigiMixer.Ssc/DigiMixer.Ssc.csproj index e70cb993..7da00322 100644 --- a/DigiMixer/DigiMixer.Ssc/DigiMixer.Ssc.csproj +++ b/DigiMixer/DigiMixer.Ssc/DigiMixer.Ssc.csproj @@ -7,7 +7,7 @@ - + diff --git a/IconPlatform/IconPlatform.Model/IconPlatform.Model.csproj b/IconPlatform/IconPlatform.Model/IconPlatform.Model.csproj index bcf2be1e..87724e09 100644 --- a/IconPlatform/IconPlatform.Model/IconPlatform.Model.csproj +++ b/IconPlatform/IconPlatform.Model/IconPlatform.Model.csproj @@ -7,7 +7,7 @@ - + diff --git a/WpfUtil/JonSkeet.CoreAppUtil/JonSkeet.CoreAppUtil.csproj b/WpfUtil/JonSkeet.CoreAppUtil/JonSkeet.CoreAppUtil.csproj index 4c41d62a..89219df9 100644 --- a/WpfUtil/JonSkeet.CoreAppUtil/JonSkeet.CoreAppUtil.csproj +++ b/WpfUtil/JonSkeet.CoreAppUtil/JonSkeet.CoreAppUtil.csproj @@ -5,9 +5,9 @@ - - - - + + + + diff --git a/XTouchMini/XTouchMini.Model/XTouchMini.Model.csproj b/XTouchMini/XTouchMini.Model/XTouchMini.Model.csproj index bcf2be1e..87724e09 100644 --- a/XTouchMini/XTouchMini.Model/XTouchMini.Model.csproj +++ b/XTouchMini/XTouchMini.Model/XTouchMini.Model.csproj @@ -7,7 +7,7 @@ - + From 6d3817dab9e2fed8703e99bc51452d761c9ee4bc Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Sat, 15 Nov 2025 14:22:29 +0000 Subject: [PATCH 15/16] Convert DigiMixer to centralised package management --- .../DigiMixer.BehringerWing.Tools.csproj | 2 +- .../DigiMixer.Core/DigiMixer.Core.csproj | 2 +- .../DigiMixer.Diagnostics.csproj | 4 +- .../DigiMixer.Hardware.csproj | 16 +- .../DigiMixer.Mackie.Tools.csproj | 2 +- .../DigiMixer.Msix/DigiMixer.Msix.wapproj | 2 +- DigiMixer/DigiMixer.Osc/DigiMixer.Osc.csproj | 5 +- .../DigiMixer.PeripheralConsole.csproj | 2 +- .../DigiMixer.RcfProxy.csproj | 14 +- DigiMixer/DigiMixer.RcfProxy/Program.cs | 35 +- DigiMixer/DigiMixer.Ssc/DigiMixer.Ssc.csproj | 2 +- .../DigiMixer.Tests/DigiMixer.Tests.csproj | 38 +- .../Ssc/SscMessageHandlerTest.cs | 2 +- .../DigiMixer.Tests/Ssc/SscMessageTest.cs | 16 +- .../DigiMixer.UCNet.Core.csproj | 4 +- DigiMixer/DigiMixer.sln | 489 ------------------ DigiMixer/DigiMixer.slnx | 65 +++ DigiMixer/Directory.Packages.props | 27 + 18 files changed, 164 insertions(+), 563 deletions(-) delete mode 100644 DigiMixer/DigiMixer.sln create mode 100644 DigiMixer/DigiMixer.slnx create mode 100644 DigiMixer/Directory.Packages.props diff --git a/DigiMixer/DigiMixer.BehringerWing.Tools/DigiMixer.BehringerWing.Tools.csproj b/DigiMixer/DigiMixer.BehringerWing.Tools/DigiMixer.BehringerWing.Tools.csproj index c7f475b4..a7fd7215 100644 --- a/DigiMixer/DigiMixer.BehringerWing.Tools/DigiMixer.BehringerWing.Tools.csproj +++ b/DigiMixer/DigiMixer.BehringerWing.Tools/DigiMixer.BehringerWing.Tools.csproj @@ -10,6 +10,6 @@ - + diff --git a/DigiMixer/DigiMixer.Core/DigiMixer.Core.csproj b/DigiMixer/DigiMixer.Core/DigiMixer.Core.csproj index aac5142d..5c92d6b9 100644 --- a/DigiMixer/DigiMixer.Core/DigiMixer.Core.csproj +++ b/DigiMixer/DigiMixer.Core/DigiMixer.Core.csproj @@ -9,7 +9,7 @@ - + diff --git a/DigiMixer/DigiMixer.Diagnostics/DigiMixer.Diagnostics.csproj b/DigiMixer/DigiMixer.Diagnostics/DigiMixer.Diagnostics.csproj index a6251a3b..bcb140d4 100644 --- a/DigiMixer/DigiMixer.Diagnostics/DigiMixer.Diagnostics.csproj +++ b/DigiMixer/DigiMixer.Diagnostics/DigiMixer.Diagnostics.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/DigiMixer/DigiMixer.Hardware/DigiMixer.Hardware.csproj b/DigiMixer/DigiMixer.Hardware/DigiMixer.Hardware.csproj index 908e2e68..e1c66bb7 100644 --- a/DigiMixer/DigiMixer.Hardware/DigiMixer.Hardware.csproj +++ b/DigiMixer/DigiMixer.Hardware/DigiMixer.Hardware.csproj @@ -15,16 +15,14 @@ - - - - + + + + + + - - - - - + diff --git a/DigiMixer/DigiMixer.Mackie.Tools/DigiMixer.Mackie.Tools.csproj b/DigiMixer/DigiMixer.Mackie.Tools/DigiMixer.Mackie.Tools.csproj index 83f81bc2..2c39aee2 100644 --- a/DigiMixer/DigiMixer.Mackie.Tools/DigiMixer.Mackie.Tools.csproj +++ b/DigiMixer/DigiMixer.Mackie.Tools/DigiMixer.Mackie.Tools.csproj @@ -10,7 +10,7 @@ - + diff --git a/DigiMixer/DigiMixer.Msix/DigiMixer.Msix.wapproj b/DigiMixer/DigiMixer.Msix/DigiMixer.Msix.wapproj index 01e72a99..2fccd388 100644 --- a/DigiMixer/DigiMixer.Msix/DigiMixer.Msix.wapproj +++ b/DigiMixer/DigiMixer.Msix/DigiMixer.Msix.wapproj @@ -89,7 +89,7 @@ - + diff --git a/DigiMixer/DigiMixer.Osc/DigiMixer.Osc.csproj b/DigiMixer/DigiMixer.Osc/DigiMixer.Osc.csproj index e587987f..e3992c0c 100644 --- a/DigiMixer/DigiMixer.Osc/DigiMixer.Osc.csproj +++ b/DigiMixer/DigiMixer.Osc/DigiMixer.Osc.csproj @@ -6,9 +6,8 @@ - - - + + diff --git a/DigiMixer/DigiMixer.PeripheralConsole/DigiMixer.PeripheralConsole.csproj b/DigiMixer/DigiMixer.PeripheralConsole/DigiMixer.PeripheralConsole.csproj index 30ae36ed..71f8988d 100644 --- a/DigiMixer/DigiMixer.PeripheralConsole/DigiMixer.PeripheralConsole.csproj +++ b/DigiMixer/DigiMixer.PeripheralConsole/DigiMixer.PeripheralConsole.csproj @@ -8,7 +8,7 @@ - + diff --git a/DigiMixer/DigiMixer.RcfProxy/DigiMixer.RcfProxy.csproj b/DigiMixer/DigiMixer.RcfProxy/DigiMixer.RcfProxy.csproj index 864ff827..080e0d7b 100644 --- a/DigiMixer/DigiMixer.RcfProxy/DigiMixer.RcfProxy.csproj +++ b/DigiMixer/DigiMixer.RcfProxy/DigiMixer.RcfProxy.csproj @@ -8,16 +8,10 @@ - - - - - - - - - - + + + + diff --git a/DigiMixer/DigiMixer.RcfProxy/Program.cs b/DigiMixer/DigiMixer.RcfProxy/Program.cs index e5fa0aba..1a9e81bb 100644 --- a/DigiMixer/DigiMixer.RcfProxy/Program.cs +++ b/DigiMixer/DigiMixer.RcfProxy/Program.cs @@ -31,34 +31,37 @@ var mixerAddressOption = new Option("--mixerAddress") { Description = "The address of the mixer", - IsRequired = true + Required = true }; -var mixerPortOption = new Option("--mixerPort", getDefaultValue: () => 8000) +var mixerPortOption = new Option("--mixerPort") { + DefaultValueFactory = _ => 8000, Description = "The port to connect to on the mixer", - IsRequired = false + Required = false }; -var localPortForMixerOption = new Option("--localPortForMixer", getDefaultValue: () => 9000) +var localPortForMixerOption = new Option("--localPortForMixer") { + DefaultValueFactory = _ => 9000, Description = "The local port the mixer connects to", - IsRequired = false + Required = false }; -var localPortForClientsOption = new Option("--localPortForClients", getDefaultValue: () => 8001) +var localPortForClientsOption = new Option("--localPortForClients") { + DefaultValueFactory = _ => 8001, Description = "The local port for clients to connect to", - IsRequired = false + Required = false }; var factory = LoggerFactory.Create(builder => builder.AddConsole().AddSystemdConsole(options => { options.UseUtcTimestamp = true; options.TimestampFormat = "yyyy-MM-dd'T'HH:mm:ss.FFFFFF'Z'"; }) .SetMinimumLevel(LogLevel.Debug)); var logger = factory.CreateLogger("Proxy"); -var rootCommand = new RootCommand(); -rootCommand.AddOption(mixerAddressOption); -rootCommand.AddOption(mixerPortOption); -rootCommand.AddOption(localPortForMixerOption); -rootCommand.AddOption(localPortForClientsOption); -rootCommand.SetHandler((mixerAddress, mixerPort, localPortForMixer, localPortForClients) => - Proxy.Start(mixerAddress, mixerPort, localPortForMixer, localPortForClients, logger), - mixerAddressOption, mixerPortOption, localPortForMixerOption, localPortForClientsOption); -await rootCommand.InvokeAsync(args); +var rootCommand = new RootCommand +{ + mixerAddressOption, mixerPortOption, localPortForMixerOption, localPortForClientsOption +}; +var parseResult = rootCommand.Parse(args); +await Proxy.Start( + parseResult.GetRequiredValue(mixerAddressOption), parseResult.GetValue(mixerPortOption), + parseResult.GetValue(localPortForMixerOption), parseResult.GetValue(localPortForClientsOption), + logger); diff --git a/DigiMixer/DigiMixer.Ssc/DigiMixer.Ssc.csproj b/DigiMixer/DigiMixer.Ssc/DigiMixer.Ssc.csproj index 7da00322..7eca5d4a 100644 --- a/DigiMixer/DigiMixer.Ssc/DigiMixer.Ssc.csproj +++ b/DigiMixer/DigiMixer.Ssc/DigiMixer.Ssc.csproj @@ -7,7 +7,7 @@ - + diff --git a/DigiMixer/DigiMixer.Tests/DigiMixer.Tests.csproj b/DigiMixer/DigiMixer.Tests/DigiMixer.Tests.csproj index 93bbc347..1b5a4892 100644 --- a/DigiMixer/DigiMixer.Tests/DigiMixer.Tests.csproj +++ b/DigiMixer/DigiMixer.Tests/DigiMixer.Tests.csproj @@ -1,23 +1,27 @@ - - net9.0 - enable - enable - latest - false - + + net9.0 + enable + enable + latest + false + - - - - - - - + + + + + + + + + + + + + + - - - diff --git a/DigiMixer/DigiMixer.Tests/Ssc/SscMessageHandlerTest.cs b/DigiMixer/DigiMixer.Tests/Ssc/SscMessageHandlerTest.cs index 79a6677f..b336759e 100644 --- a/DigiMixer/DigiMixer.Tests/Ssc/SscMessageHandlerTest.cs +++ b/DigiMixer/DigiMixer.Tests/Ssc/SscMessageHandlerTest.cs @@ -57,7 +57,7 @@ public void ErrorHandlerCalled() var response = new SscMessage(SscProperty.FromErrors(error)); handler.HandleMessage(response); - Assert.Null(receivedValue); + Assert.That(receivedValue, Is.Null); Assert.That(receivedError, Is.EqualTo(error)); } } diff --git a/DigiMixer/DigiMixer.Tests/Ssc/SscMessageTest.cs b/DigiMixer/DigiMixer.Tests/Ssc/SscMessageTest.cs index c9200f99..cd278147 100644 --- a/DigiMixer/DigiMixer.Tests/Ssc/SscMessageTest.cs +++ b/DigiMixer/DigiMixer.Tests/Ssc/SscMessageTest.cs @@ -50,7 +50,7 @@ public void ParseJson() new SscProperty("/a/b1", 1.5), new SscProperty("/a/b2", "test") }; - CollectionAssert.AreEqual(expected, message.Properties); + Assert.That(message.Properties, Is.EqualTo(expected).AsCollection); } [Test] @@ -59,7 +59,7 @@ public void WithId_NullId() var original = new SscMessage(new SscProperty("/a/b/c", 10), new SscProperty(SscAddresses.Osc.Xid, "id")); Assert.That(original.Id, Is.EqualTo("id")); var noId = original.WithId(null); - Assert.Null(noId.Id); + Assert.That(noId.Id, Is.Null); Assert.That(noId.Properties.Count, Is.EqualTo(1)); } @@ -84,7 +84,7 @@ public void WithId_NonNullId() new SscProperty("/a/b/c", 10), new SscProperty(SscAddresses.Osc.Xid, "id2") }; - CollectionAssert.AreEquivalent(expected, withId.Properties); + Assert.That(withId.Properties, Is.EquivalentTo(expected)); } [Test] @@ -101,8 +101,8 @@ public void ParseJson_ObservedErrors() """); // The error address still shows up as a property; Errors is just a convenience. - CollectionAssert.Contains(message.Properties, new SscProperty("/device/name", "EWDXEM2")); - Assert.NotNull(message.GetProperty(SscAddresses.Osc.Error)); + Assert.That(message.Properties, Has.Member(new SscProperty("/device/name", "EWDXEM2"))); + Assert.That(message.GetProperty(SscAddresses.Osc.Error), Is.Not.Null); var expected = new[] { @@ -110,7 +110,7 @@ public void ParseJson_ObservedErrors() new SscError("/mates/tx1/battery/xyz", 404, "address not found"), new SscError("/mates/tx1/battery/type", 424, "failed dependency") }; - CollectionAssert.AreEqual(expected, message.Errors); + Assert.That(message.Errors, Is.EqualTo(expected).AsCollection); } [Test] @@ -130,7 +130,7 @@ public void ParseJson_MultipleTopLevelErrors() new SscError("/abc", 404, "address not found"), new SscError("/x/y", 404, "address not found") }; - CollectionAssert.AreEqual(expected, message.Errors); + Assert.That(message.Errors, Is.EqualTo(expected).AsCollection); } [Test] @@ -151,7 +151,7 @@ public void ParseJson_PartialErrors() new SscError("/e2", 404, null), new SscError("/e3", 404, null) }; - CollectionAssert.AreEqual(expected, message.Errors); + Assert.That(message.Errors, Is.EqualTo(expected).AsCollection); } [Test] diff --git a/DigiMixer/DigiMixer.UCNet.Core/DigiMixer.UCNet.Core.csproj b/DigiMixer/DigiMixer.UCNet.Core/DigiMixer.UCNet.Core.csproj index 3cd6f27c..3cd2e243 100644 --- a/DigiMixer/DigiMixer.UCNet.Core/DigiMixer.UCNet.Core.csproj +++ b/DigiMixer/DigiMixer.UCNet.Core/DigiMixer.UCNet.Core.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/DigiMixer/DigiMixer.sln b/DigiMixer/DigiMixer.sln deleted file mode 100644 index c96994f6..00000000 --- a/DigiMixer/DigiMixer.sln +++ /dev/null @@ -1,489 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer", "DigiMixer\DigiMixer.csproj", "{BB919A98-376E-44D3-9B47-DD9A6A6B7C52}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.Osc", "DigiMixer.Osc\DigiMixer.Osc.csproj", "{1059F21C-34BB-4B4D-A276-DD92AFD71A2F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.UiHttp", "DigiMixer.UiHttp\DigiMixer.UiHttp.csproj", "{CB42BF97-A6FB-43E3-A22D-5346C9A8892B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.Wpf", "DigiMixer.Wpf\DigiMixer.Wpf.csproj", "{9C8B189F-0658-4B14-9A31-BBC58CD2B223}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.Core", "DigiMixer.Core\DigiMixer.Core.csproj", "{2EBEB007-4969-4D92-B8D1-E6BEE2DBFD68}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.RcfProxy", "DigiMixer.RcfProxy\DigiMixer.RcfProxy.csproj", "{8A8F9D0B-A96C-4BE1-882D-42CA18D4D140}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.Mackie", "DigiMixer.Mackie\DigiMixer.Mackie.csproj", "{5CBA7394-B2E3-414E-9B55-0F2D7A7266D9}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.Mackie.Core", "DigiMixer.Mackie.Core\DigiMixer.Mackie.Core.csproj", "{C2BDFCA9-6CE9-42C5-87F8-B2BDBABE115C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.Diagnostics", "DigiMixer.Diagnostics\DigiMixer.Diagnostics.csproj", "{7288E17E-8DD0-4696-9ED6-265D4369677F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.QuSeries", "DigiMixer.QuSeries\DigiMixer.QuSeries.csproj", "{94FECD12-299A-478A-BD36-D3CB052EAEE4}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.QuSeries.Tools", "DigiMixer.QuSeries.Tools\DigiMixer.QuSeries.Tools.csproj", "{B70280C0-272F-4F85-8D81-F2238AD1193F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.QuSeries.Core", "DigiMixer.QuSeries.Core\DigiMixer.QuSeries.Core.csproj", "{8A99DC16-0CC6-4711-92D5-D6759C7C8744}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.UCNet", "DigiMixer.UCNet\DigiMixer.UCNet.csproj", "{36493FCE-C777-413F-B3BC-1E6C25D177EB}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.UCNet.Tools", "DigiMixer.UCNet.Tools\DigiMixer.UCNet.Tools.csproj", "{369161C0-8D09-441B-8027-6642C6E60008}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.UCNet.Core", "DigiMixer.UCNet.Core\DigiMixer.UCNet.Core.csproj", "{1F479E07-BBED-4F80-93D4-A55154861E6E}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Protocols", "Protocols", "{0B03065C-EBBD-446F-9BC4-1D26FD96B439}" - ProjectSection(SolutionItems) = preProject - Protocols\ah-sq.md = Protocols\ah-sq.md - Protocols\behringer-wing.md = Protocols\behringer-wing.md - Protocols\mackie.md = Protocols\mackie.md - Protocols\yamaha-tf.md = Protocols\yamaha-tf.md - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.Ssc", "DigiMixer.Ssc\DigiMixer.Ssc.csproj", "{6C3761B7-E526-4DE8-9EA8-9B3F67EDFA11}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.Tests", "DigiMixer.Tests\DigiMixer.Tests.csproj", "{2B6225BE-5E46-422B-B365-B5FCA746545D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.Mackie.Tools", "DigiMixer.Mackie.Tools\DigiMixer.Mackie.Tools.csproj", "{92FF4EA6-D626-41CD-B950-A153A32B7ABD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.CqSeries.Core", "DigiMixer.CqSeries.Core\DigiMixer.CqSeries.Core.csproj", "{FA82282A-6123-4C2D-8D50-CA3B2C819E65}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.CqSeries", "DigiMixer.CqSeries\DigiMixer.CqSeries.csproj", "{84AC4953-474C-4EDD-8EA2-F8FE822FBF65}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.CqSeries.Tools", "DigiMixer.CqSeries.Tools\DigiMixer.CqSeries.Tools.csproj", "{718C7332-061F-4E64-B93F-BFE8DDDB3797}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.DmSeries.Core", "DigiMixer.DmSeries.Core\DigiMixer.DmSeries.Core.csproj", "{AA0CFF1C-5EFC-4B96-B850-7E227BE10504}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.DmSeries", "DigiMixer.DmSeries\DigiMixer.DmSeries.csproj", "{907940D4-C253-4D33-B98C-538EA8C368E5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.DmSeries.Tools", "DigiMixer.DmSeries.Tools\DigiMixer.DmSeries.Tools.csproj", "{9E447C1B-FBDE-4BBE-92A8-278FA45B38B8}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.UiHttp.Tools", "DigiMixer.UiHttp.Tools\DigiMixer.UiHttp.Tools.csproj", "{12AE60D1-4565-4165-8CFA-C4C9A0E75FA0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.Controls", "DigiMixer.Controls\DigiMixer.Controls.csproj", "{336197AD-E6FA-4285-A3BA-2B29CE4292ED}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JonSkeet.WpfUtil", "..\WpfUtil\JonSkeet.WpfUtil\JonSkeet.WpfUtil.csproj", "{B1D194DD-09A0-41B7-BF7D-4965FA680B64}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JonSkeet.WpfLogging", "..\WpfUtil\JonSkeet.WpfLogging\JonSkeet.WpfLogging.csproj", "{8D0D113B-1208-4F86-8E27-2C2F04AC4A17}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IconPlatform.Model", "..\IconPlatform\IconPlatform.Model\IconPlatform.Model.csproj", "{6F4D2966-31FC-48C9-986E-3B6C66A7A736}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XTouchMini.Model", "..\XTouchMini\XTouchMini.Model\XTouchMini.Model.csproj", "{7BCE73E5-0F10-4F46-B3B1-AF7381899560}" -EndProject -Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "DigiMixer.Msix", "DigiMixer.Msix\DigiMixer.Msix.wapproj", "{41D5CFAA-0691-4561-AE18-C2C14EE9C55A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.AppCore", "DigiMixer.AppCore\DigiMixer.AppCore.csproj", "{B5A51266-596D-4138-A62A-6C5B86E7C619}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.PeripheralConsole", "DigiMixer.PeripheralConsole\DigiMixer.PeripheralConsole.csproj", "{7B9C23C7-E656-4137-B01A-C1A1AE9E7B02}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JonSkeet.CoreAppUtil", "..\WpfUtil\JonSkeet.CoreAppUtil\JonSkeet.CoreAppUtil.csproj", "{160D0BEC-5CF6-4472-9BBF-498E2E07D5E3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.BehringerWing", "DigiMixer.BehringerWing\DigiMixer.BehringerWing.csproj", "{E183BBE5-6F28-42D9-BD3B-4B56922556FC}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.BehringerWing.Core", "DigiMixer.BehringerWing.Core\DigiMixer.BehringerWing.Core.csproj", "{26BD3DDF-BD51-4B02-A39F-E4817CA14723}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.BehringerWing.Tools", "DigiMixer.BehringerWing.Tools\DigiMixer.BehringerWing.Tools.csproj", "{56E5E913-B701-4E9D-90D7-53862C5EB3D5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigiMixer.Hardware", "DigiMixer.Hardware\DigiMixer.Hardware.csproj", "{731B08FA-CF37-4CB4-9C7D-617E93E9E97E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.BehringerWing.WingExplorer", "DigiMixer.BehringerWing.WingExplorer\DigiMixer.BehringerWing.WingExplorer.csproj", "{20705AD5-B614-413D-9BB9-8308A958E4F4}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.TfSeries", "DigiMixer.TfSeries\DigiMixer.TfSeries.csproj", "{603E2343-6C02-4A68-A434-BC029AB6F788}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.TfSeries.Tools", "DigiMixer.TfSeries.Tools\DigiMixer.TfSeries.Tools.csproj", "{4A66EF70-036A-4AA3-BE57-D56645FEE3CD}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.Yamaha.Core", "DigiMixer.Yamaha.Core\DigiMixer.Yamaha.Core.csproj", "{B400F62F-7AC4-49B4-AA76-4DA358DDE8B4}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.Yamaha", "DigiMixer.Yamaha\DigiMixer.Yamaha.csproj", "{5EEC5BBA-67C7-4417-9283-53EEFB280FEB}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.SqSeries", "DigiMixer.SqSeries\DigiMixer.SqSeries.csproj", "{25452C06-6C94-4E3F-9978-DC42CE9CAD9C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.SqSeries.Tools", "DigiMixer.SqSeries.Tools\DigiMixer.SqSeries.Tools.csproj", "{E1E0C51C-D634-4945-A310-16438CA78797}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigiMixer.AllenAndHeath.Core", "DigiMixer.AllenAndHeath.Core\DigiMixer.AllenAndHeath.Core.csproj", "{082C4376-E6AC-45AA-B985-65E7142F171C}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {BB919A98-376E-44D3-9B47-DD9A6A6B7C52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BB919A98-376E-44D3-9B47-DD9A6A6B7C52}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BB919A98-376E-44D3-9B47-DD9A6A6B7C52}.Debug|x64.ActiveCfg = Debug|Any CPU - {BB919A98-376E-44D3-9B47-DD9A6A6B7C52}.Debug|x64.Build.0 = Debug|Any CPU - {BB919A98-376E-44D3-9B47-DD9A6A6B7C52}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BB919A98-376E-44D3-9B47-DD9A6A6B7C52}.Release|Any CPU.Build.0 = Release|Any CPU - {BB919A98-376E-44D3-9B47-DD9A6A6B7C52}.Release|x64.ActiveCfg = Release|Any CPU - {BB919A98-376E-44D3-9B47-DD9A6A6B7C52}.Release|x64.Build.0 = Release|Any CPU - {1059F21C-34BB-4B4D-A276-DD92AFD71A2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1059F21C-34BB-4B4D-A276-DD92AFD71A2F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1059F21C-34BB-4B4D-A276-DD92AFD71A2F}.Debug|x64.ActiveCfg = Debug|Any CPU - {1059F21C-34BB-4B4D-A276-DD92AFD71A2F}.Debug|x64.Build.0 = Debug|Any CPU - {1059F21C-34BB-4B4D-A276-DD92AFD71A2F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1059F21C-34BB-4B4D-A276-DD92AFD71A2F}.Release|Any CPU.Build.0 = Release|Any CPU - {1059F21C-34BB-4B4D-A276-DD92AFD71A2F}.Release|x64.ActiveCfg = Release|Any CPU - {1059F21C-34BB-4B4D-A276-DD92AFD71A2F}.Release|x64.Build.0 = Release|Any CPU - {CB42BF97-A6FB-43E3-A22D-5346C9A8892B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CB42BF97-A6FB-43E3-A22D-5346C9A8892B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CB42BF97-A6FB-43E3-A22D-5346C9A8892B}.Debug|x64.ActiveCfg = Debug|Any CPU - {CB42BF97-A6FB-43E3-A22D-5346C9A8892B}.Debug|x64.Build.0 = Debug|Any CPU - {CB42BF97-A6FB-43E3-A22D-5346C9A8892B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CB42BF97-A6FB-43E3-A22D-5346C9A8892B}.Release|Any CPU.Build.0 = Release|Any CPU - {CB42BF97-A6FB-43E3-A22D-5346C9A8892B}.Release|x64.ActiveCfg = Release|Any CPU - {CB42BF97-A6FB-43E3-A22D-5346C9A8892B}.Release|x64.Build.0 = Release|Any CPU - {9C8B189F-0658-4B14-9A31-BBC58CD2B223}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9C8B189F-0658-4B14-9A31-BBC58CD2B223}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9C8B189F-0658-4B14-9A31-BBC58CD2B223}.Debug|x64.ActiveCfg = Debug|Any CPU - {9C8B189F-0658-4B14-9A31-BBC58CD2B223}.Debug|x64.Build.0 = Debug|Any CPU - {9C8B189F-0658-4B14-9A31-BBC58CD2B223}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9C8B189F-0658-4B14-9A31-BBC58CD2B223}.Release|Any CPU.Build.0 = Release|Any CPU - {9C8B189F-0658-4B14-9A31-BBC58CD2B223}.Release|x64.ActiveCfg = Release|Any CPU - {9C8B189F-0658-4B14-9A31-BBC58CD2B223}.Release|x64.Build.0 = Release|Any CPU - {2EBEB007-4969-4D92-B8D1-E6BEE2DBFD68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2EBEB007-4969-4D92-B8D1-E6BEE2DBFD68}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2EBEB007-4969-4D92-B8D1-E6BEE2DBFD68}.Debug|x64.ActiveCfg = Debug|Any CPU - {2EBEB007-4969-4D92-B8D1-E6BEE2DBFD68}.Debug|x64.Build.0 = Debug|Any CPU - {2EBEB007-4969-4D92-B8D1-E6BEE2DBFD68}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2EBEB007-4969-4D92-B8D1-E6BEE2DBFD68}.Release|Any CPU.Build.0 = Release|Any CPU - {2EBEB007-4969-4D92-B8D1-E6BEE2DBFD68}.Release|x64.ActiveCfg = Release|Any CPU - {2EBEB007-4969-4D92-B8D1-E6BEE2DBFD68}.Release|x64.Build.0 = Release|Any CPU - {8A8F9D0B-A96C-4BE1-882D-42CA18D4D140}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8A8F9D0B-A96C-4BE1-882D-42CA18D4D140}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8A8F9D0B-A96C-4BE1-882D-42CA18D4D140}.Debug|x64.ActiveCfg = Debug|Any CPU - {8A8F9D0B-A96C-4BE1-882D-42CA18D4D140}.Debug|x64.Build.0 = Debug|Any CPU - {8A8F9D0B-A96C-4BE1-882D-42CA18D4D140}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8A8F9D0B-A96C-4BE1-882D-42CA18D4D140}.Release|Any CPU.Build.0 = Release|Any CPU - {8A8F9D0B-A96C-4BE1-882D-42CA18D4D140}.Release|x64.ActiveCfg = Release|Any CPU - {8A8F9D0B-A96C-4BE1-882D-42CA18D4D140}.Release|x64.Build.0 = Release|Any CPU - {5CBA7394-B2E3-414E-9B55-0F2D7A7266D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5CBA7394-B2E3-414E-9B55-0F2D7A7266D9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5CBA7394-B2E3-414E-9B55-0F2D7A7266D9}.Debug|x64.ActiveCfg = Debug|Any CPU - {5CBA7394-B2E3-414E-9B55-0F2D7A7266D9}.Debug|x64.Build.0 = Debug|Any CPU - {5CBA7394-B2E3-414E-9B55-0F2D7A7266D9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5CBA7394-B2E3-414E-9B55-0F2D7A7266D9}.Release|Any CPU.Build.0 = Release|Any CPU - {5CBA7394-B2E3-414E-9B55-0F2D7A7266D9}.Release|x64.ActiveCfg = Release|Any CPU - {5CBA7394-B2E3-414E-9B55-0F2D7A7266D9}.Release|x64.Build.0 = Release|Any CPU - {C2BDFCA9-6CE9-42C5-87F8-B2BDBABE115C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C2BDFCA9-6CE9-42C5-87F8-B2BDBABE115C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C2BDFCA9-6CE9-42C5-87F8-B2BDBABE115C}.Debug|x64.ActiveCfg = Debug|Any CPU - {C2BDFCA9-6CE9-42C5-87F8-B2BDBABE115C}.Debug|x64.Build.0 = Debug|Any CPU - {C2BDFCA9-6CE9-42C5-87F8-B2BDBABE115C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C2BDFCA9-6CE9-42C5-87F8-B2BDBABE115C}.Release|Any CPU.Build.0 = Release|Any CPU - {C2BDFCA9-6CE9-42C5-87F8-B2BDBABE115C}.Release|x64.ActiveCfg = Release|Any CPU - {C2BDFCA9-6CE9-42C5-87F8-B2BDBABE115C}.Release|x64.Build.0 = Release|Any CPU - {7288E17E-8DD0-4696-9ED6-265D4369677F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7288E17E-8DD0-4696-9ED6-265D4369677F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7288E17E-8DD0-4696-9ED6-265D4369677F}.Debug|x64.ActiveCfg = Debug|Any CPU - {7288E17E-8DD0-4696-9ED6-265D4369677F}.Debug|x64.Build.0 = Debug|Any CPU - {7288E17E-8DD0-4696-9ED6-265D4369677F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7288E17E-8DD0-4696-9ED6-265D4369677F}.Release|Any CPU.Build.0 = Release|Any CPU - {7288E17E-8DD0-4696-9ED6-265D4369677F}.Release|x64.ActiveCfg = Release|Any CPU - {7288E17E-8DD0-4696-9ED6-265D4369677F}.Release|x64.Build.0 = Release|Any CPU - {94FECD12-299A-478A-BD36-D3CB052EAEE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {94FECD12-299A-478A-BD36-D3CB052EAEE4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {94FECD12-299A-478A-BD36-D3CB052EAEE4}.Debug|x64.ActiveCfg = Debug|Any CPU - {94FECD12-299A-478A-BD36-D3CB052EAEE4}.Debug|x64.Build.0 = Debug|Any CPU - {94FECD12-299A-478A-BD36-D3CB052EAEE4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {94FECD12-299A-478A-BD36-D3CB052EAEE4}.Release|Any CPU.Build.0 = Release|Any CPU - {94FECD12-299A-478A-BD36-D3CB052EAEE4}.Release|x64.ActiveCfg = Release|Any CPU - {94FECD12-299A-478A-BD36-D3CB052EAEE4}.Release|x64.Build.0 = Release|Any CPU - {B70280C0-272F-4F85-8D81-F2238AD1193F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B70280C0-272F-4F85-8D81-F2238AD1193F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B70280C0-272F-4F85-8D81-F2238AD1193F}.Debug|x64.ActiveCfg = Debug|Any CPU - {B70280C0-272F-4F85-8D81-F2238AD1193F}.Debug|x64.Build.0 = Debug|Any CPU - {B70280C0-272F-4F85-8D81-F2238AD1193F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B70280C0-272F-4F85-8D81-F2238AD1193F}.Release|Any CPU.Build.0 = Release|Any CPU - {B70280C0-272F-4F85-8D81-F2238AD1193F}.Release|x64.ActiveCfg = Release|Any CPU - {B70280C0-272F-4F85-8D81-F2238AD1193F}.Release|x64.Build.0 = Release|Any CPU - {8A99DC16-0CC6-4711-92D5-D6759C7C8744}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8A99DC16-0CC6-4711-92D5-D6759C7C8744}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8A99DC16-0CC6-4711-92D5-D6759C7C8744}.Debug|x64.ActiveCfg = Debug|Any CPU - {8A99DC16-0CC6-4711-92D5-D6759C7C8744}.Debug|x64.Build.0 = Debug|Any CPU - {8A99DC16-0CC6-4711-92D5-D6759C7C8744}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8A99DC16-0CC6-4711-92D5-D6759C7C8744}.Release|Any CPU.Build.0 = Release|Any CPU - {8A99DC16-0CC6-4711-92D5-D6759C7C8744}.Release|x64.ActiveCfg = Release|Any CPU - {8A99DC16-0CC6-4711-92D5-D6759C7C8744}.Release|x64.Build.0 = Release|Any CPU - {36493FCE-C777-413F-B3BC-1E6C25D177EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {36493FCE-C777-413F-B3BC-1E6C25D177EB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {36493FCE-C777-413F-B3BC-1E6C25D177EB}.Debug|x64.ActiveCfg = Debug|Any CPU - {36493FCE-C777-413F-B3BC-1E6C25D177EB}.Debug|x64.Build.0 = Debug|Any CPU - {36493FCE-C777-413F-B3BC-1E6C25D177EB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {36493FCE-C777-413F-B3BC-1E6C25D177EB}.Release|Any CPU.Build.0 = Release|Any CPU - {36493FCE-C777-413F-B3BC-1E6C25D177EB}.Release|x64.ActiveCfg = Release|Any CPU - {36493FCE-C777-413F-B3BC-1E6C25D177EB}.Release|x64.Build.0 = Release|Any CPU - {369161C0-8D09-441B-8027-6642C6E60008}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {369161C0-8D09-441B-8027-6642C6E60008}.Debug|Any CPU.Build.0 = Debug|Any CPU - {369161C0-8D09-441B-8027-6642C6E60008}.Debug|x64.ActiveCfg = Debug|Any CPU - {369161C0-8D09-441B-8027-6642C6E60008}.Debug|x64.Build.0 = Debug|Any CPU - {369161C0-8D09-441B-8027-6642C6E60008}.Release|Any CPU.ActiveCfg = Release|Any CPU - {369161C0-8D09-441B-8027-6642C6E60008}.Release|Any CPU.Build.0 = Release|Any CPU - {369161C0-8D09-441B-8027-6642C6E60008}.Release|x64.ActiveCfg = Release|Any CPU - {369161C0-8D09-441B-8027-6642C6E60008}.Release|x64.Build.0 = Release|Any CPU - {1F479E07-BBED-4F80-93D4-A55154861E6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1F479E07-BBED-4F80-93D4-A55154861E6E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1F479E07-BBED-4F80-93D4-A55154861E6E}.Debug|x64.ActiveCfg = Debug|Any CPU - {1F479E07-BBED-4F80-93D4-A55154861E6E}.Debug|x64.Build.0 = Debug|Any CPU - {1F479E07-BBED-4F80-93D4-A55154861E6E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1F479E07-BBED-4F80-93D4-A55154861E6E}.Release|Any CPU.Build.0 = Release|Any CPU - {1F479E07-BBED-4F80-93D4-A55154861E6E}.Release|x64.ActiveCfg = Release|Any CPU - {1F479E07-BBED-4F80-93D4-A55154861E6E}.Release|x64.Build.0 = Release|Any CPU - {6C3761B7-E526-4DE8-9EA8-9B3F67EDFA11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6C3761B7-E526-4DE8-9EA8-9B3F67EDFA11}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6C3761B7-E526-4DE8-9EA8-9B3F67EDFA11}.Debug|x64.ActiveCfg = Debug|Any CPU - {6C3761B7-E526-4DE8-9EA8-9B3F67EDFA11}.Debug|x64.Build.0 = Debug|Any CPU - {6C3761B7-E526-4DE8-9EA8-9B3F67EDFA11}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6C3761B7-E526-4DE8-9EA8-9B3F67EDFA11}.Release|Any CPU.Build.0 = Release|Any CPU - {6C3761B7-E526-4DE8-9EA8-9B3F67EDFA11}.Release|x64.ActiveCfg = Release|Any CPU - {6C3761B7-E526-4DE8-9EA8-9B3F67EDFA11}.Release|x64.Build.0 = Release|Any CPU - {2B6225BE-5E46-422B-B365-B5FCA746545D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2B6225BE-5E46-422B-B365-B5FCA746545D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2B6225BE-5E46-422B-B365-B5FCA746545D}.Debug|x64.ActiveCfg = Debug|Any CPU - {2B6225BE-5E46-422B-B365-B5FCA746545D}.Debug|x64.Build.0 = Debug|Any CPU - {2B6225BE-5E46-422B-B365-B5FCA746545D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2B6225BE-5E46-422B-B365-B5FCA746545D}.Release|Any CPU.Build.0 = Release|Any CPU - {2B6225BE-5E46-422B-B365-B5FCA746545D}.Release|x64.ActiveCfg = Release|Any CPU - {2B6225BE-5E46-422B-B365-B5FCA746545D}.Release|x64.Build.0 = Release|Any CPU - {92FF4EA6-D626-41CD-B950-A153A32B7ABD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {92FF4EA6-D626-41CD-B950-A153A32B7ABD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {92FF4EA6-D626-41CD-B950-A153A32B7ABD}.Debug|x64.ActiveCfg = Debug|Any CPU - {92FF4EA6-D626-41CD-B950-A153A32B7ABD}.Debug|x64.Build.0 = Debug|Any CPU - {92FF4EA6-D626-41CD-B950-A153A32B7ABD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {92FF4EA6-D626-41CD-B950-A153A32B7ABD}.Release|Any CPU.Build.0 = Release|Any CPU - {92FF4EA6-D626-41CD-B950-A153A32B7ABD}.Release|x64.ActiveCfg = Release|Any CPU - {92FF4EA6-D626-41CD-B950-A153A32B7ABD}.Release|x64.Build.0 = Release|Any CPU - {FA82282A-6123-4C2D-8D50-CA3B2C819E65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FA82282A-6123-4C2D-8D50-CA3B2C819E65}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FA82282A-6123-4C2D-8D50-CA3B2C819E65}.Debug|x64.ActiveCfg = Debug|Any CPU - {FA82282A-6123-4C2D-8D50-CA3B2C819E65}.Debug|x64.Build.0 = Debug|Any CPU - {FA82282A-6123-4C2D-8D50-CA3B2C819E65}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FA82282A-6123-4C2D-8D50-CA3B2C819E65}.Release|Any CPU.Build.0 = Release|Any CPU - {FA82282A-6123-4C2D-8D50-CA3B2C819E65}.Release|x64.ActiveCfg = Release|Any CPU - {FA82282A-6123-4C2D-8D50-CA3B2C819E65}.Release|x64.Build.0 = Release|Any CPU - {84AC4953-474C-4EDD-8EA2-F8FE822FBF65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {84AC4953-474C-4EDD-8EA2-F8FE822FBF65}.Debug|Any CPU.Build.0 = Debug|Any CPU - {84AC4953-474C-4EDD-8EA2-F8FE822FBF65}.Debug|x64.ActiveCfg = Debug|Any CPU - {84AC4953-474C-4EDD-8EA2-F8FE822FBF65}.Debug|x64.Build.0 = Debug|Any CPU - {84AC4953-474C-4EDD-8EA2-F8FE822FBF65}.Release|Any CPU.ActiveCfg = Release|Any CPU - {84AC4953-474C-4EDD-8EA2-F8FE822FBF65}.Release|Any CPU.Build.0 = Release|Any CPU - {84AC4953-474C-4EDD-8EA2-F8FE822FBF65}.Release|x64.ActiveCfg = Release|Any CPU - {84AC4953-474C-4EDD-8EA2-F8FE822FBF65}.Release|x64.Build.0 = Release|Any CPU - {718C7332-061F-4E64-B93F-BFE8DDDB3797}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {718C7332-061F-4E64-B93F-BFE8DDDB3797}.Debug|Any CPU.Build.0 = Debug|Any CPU - {718C7332-061F-4E64-B93F-BFE8DDDB3797}.Debug|x64.ActiveCfg = Debug|Any CPU - {718C7332-061F-4E64-B93F-BFE8DDDB3797}.Debug|x64.Build.0 = Debug|Any CPU - {718C7332-061F-4E64-B93F-BFE8DDDB3797}.Release|Any CPU.ActiveCfg = Release|Any CPU - {718C7332-061F-4E64-B93F-BFE8DDDB3797}.Release|Any CPU.Build.0 = Release|Any CPU - {718C7332-061F-4E64-B93F-BFE8DDDB3797}.Release|x64.ActiveCfg = Release|Any CPU - {718C7332-061F-4E64-B93F-BFE8DDDB3797}.Release|x64.Build.0 = Release|Any CPU - {AA0CFF1C-5EFC-4B96-B850-7E227BE10504}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AA0CFF1C-5EFC-4B96-B850-7E227BE10504}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AA0CFF1C-5EFC-4B96-B850-7E227BE10504}.Debug|x64.ActiveCfg = Debug|Any CPU - {AA0CFF1C-5EFC-4B96-B850-7E227BE10504}.Debug|x64.Build.0 = Debug|Any CPU - {AA0CFF1C-5EFC-4B96-B850-7E227BE10504}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AA0CFF1C-5EFC-4B96-B850-7E227BE10504}.Release|Any CPU.Build.0 = Release|Any CPU - {AA0CFF1C-5EFC-4B96-B850-7E227BE10504}.Release|x64.ActiveCfg = Release|Any CPU - {AA0CFF1C-5EFC-4B96-B850-7E227BE10504}.Release|x64.Build.0 = Release|Any CPU - {907940D4-C253-4D33-B98C-538EA8C368E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {907940D4-C253-4D33-B98C-538EA8C368E5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {907940D4-C253-4D33-B98C-538EA8C368E5}.Debug|x64.ActiveCfg = Debug|Any CPU - {907940D4-C253-4D33-B98C-538EA8C368E5}.Debug|x64.Build.0 = Debug|Any CPU - {907940D4-C253-4D33-B98C-538EA8C368E5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {907940D4-C253-4D33-B98C-538EA8C368E5}.Release|Any CPU.Build.0 = Release|Any CPU - {907940D4-C253-4D33-B98C-538EA8C368E5}.Release|x64.ActiveCfg = Release|Any CPU - {907940D4-C253-4D33-B98C-538EA8C368E5}.Release|x64.Build.0 = Release|Any CPU - {9E447C1B-FBDE-4BBE-92A8-278FA45B38B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9E447C1B-FBDE-4BBE-92A8-278FA45B38B8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9E447C1B-FBDE-4BBE-92A8-278FA45B38B8}.Debug|x64.ActiveCfg = Debug|Any CPU - {9E447C1B-FBDE-4BBE-92A8-278FA45B38B8}.Debug|x64.Build.0 = Debug|Any CPU - {9E447C1B-FBDE-4BBE-92A8-278FA45B38B8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9E447C1B-FBDE-4BBE-92A8-278FA45B38B8}.Release|Any CPU.Build.0 = Release|Any CPU - {9E447C1B-FBDE-4BBE-92A8-278FA45B38B8}.Release|x64.ActiveCfg = Release|Any CPU - {9E447C1B-FBDE-4BBE-92A8-278FA45B38B8}.Release|x64.Build.0 = Release|Any CPU - {12AE60D1-4565-4165-8CFA-C4C9A0E75FA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {12AE60D1-4565-4165-8CFA-C4C9A0E75FA0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {12AE60D1-4565-4165-8CFA-C4C9A0E75FA0}.Debug|x64.ActiveCfg = Debug|Any CPU - {12AE60D1-4565-4165-8CFA-C4C9A0E75FA0}.Debug|x64.Build.0 = Debug|Any CPU - {12AE60D1-4565-4165-8CFA-C4C9A0E75FA0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {12AE60D1-4565-4165-8CFA-C4C9A0E75FA0}.Release|Any CPU.Build.0 = Release|Any CPU - {12AE60D1-4565-4165-8CFA-C4C9A0E75FA0}.Release|x64.ActiveCfg = Release|Any CPU - {12AE60D1-4565-4165-8CFA-C4C9A0E75FA0}.Release|x64.Build.0 = Release|Any CPU - {336197AD-E6FA-4285-A3BA-2B29CE4292ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {336197AD-E6FA-4285-A3BA-2B29CE4292ED}.Debug|Any CPU.Build.0 = Debug|Any CPU - {336197AD-E6FA-4285-A3BA-2B29CE4292ED}.Debug|x64.ActiveCfg = Debug|Any CPU - {336197AD-E6FA-4285-A3BA-2B29CE4292ED}.Debug|x64.Build.0 = Debug|Any CPU - {336197AD-E6FA-4285-A3BA-2B29CE4292ED}.Release|Any CPU.ActiveCfg = Release|Any CPU - {336197AD-E6FA-4285-A3BA-2B29CE4292ED}.Release|Any CPU.Build.0 = Release|Any CPU - {336197AD-E6FA-4285-A3BA-2B29CE4292ED}.Release|x64.ActiveCfg = Release|Any CPU - {336197AD-E6FA-4285-A3BA-2B29CE4292ED}.Release|x64.Build.0 = Release|Any CPU - {B1D194DD-09A0-41B7-BF7D-4965FA680B64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B1D194DD-09A0-41B7-BF7D-4965FA680B64}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B1D194DD-09A0-41B7-BF7D-4965FA680B64}.Debug|x64.ActiveCfg = Debug|Any CPU - {B1D194DD-09A0-41B7-BF7D-4965FA680B64}.Debug|x64.Build.0 = Debug|Any CPU - {B1D194DD-09A0-41B7-BF7D-4965FA680B64}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B1D194DD-09A0-41B7-BF7D-4965FA680B64}.Release|Any CPU.Build.0 = Release|Any CPU - {B1D194DD-09A0-41B7-BF7D-4965FA680B64}.Release|x64.ActiveCfg = Release|Any CPU - {B1D194DD-09A0-41B7-BF7D-4965FA680B64}.Release|x64.Build.0 = Release|Any CPU - {8D0D113B-1208-4F86-8E27-2C2F04AC4A17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8D0D113B-1208-4F86-8E27-2C2F04AC4A17}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8D0D113B-1208-4F86-8E27-2C2F04AC4A17}.Debug|x64.ActiveCfg = Debug|Any CPU - {8D0D113B-1208-4F86-8E27-2C2F04AC4A17}.Debug|x64.Build.0 = Debug|Any CPU - {8D0D113B-1208-4F86-8E27-2C2F04AC4A17}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8D0D113B-1208-4F86-8E27-2C2F04AC4A17}.Release|Any CPU.Build.0 = Release|Any CPU - {8D0D113B-1208-4F86-8E27-2C2F04AC4A17}.Release|x64.ActiveCfg = Release|Any CPU - {8D0D113B-1208-4F86-8E27-2C2F04AC4A17}.Release|x64.Build.0 = Release|Any CPU - {6F4D2966-31FC-48C9-986E-3B6C66A7A736}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6F4D2966-31FC-48C9-986E-3B6C66A7A736}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6F4D2966-31FC-48C9-986E-3B6C66A7A736}.Debug|x64.ActiveCfg = Debug|Any CPU - {6F4D2966-31FC-48C9-986E-3B6C66A7A736}.Debug|x64.Build.0 = Debug|Any CPU - {6F4D2966-31FC-48C9-986E-3B6C66A7A736}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6F4D2966-31FC-48C9-986E-3B6C66A7A736}.Release|Any CPU.Build.0 = Release|Any CPU - {6F4D2966-31FC-48C9-986E-3B6C66A7A736}.Release|x64.ActiveCfg = Release|Any CPU - {6F4D2966-31FC-48C9-986E-3B6C66A7A736}.Release|x64.Build.0 = Release|Any CPU - {7BCE73E5-0F10-4F46-B3B1-AF7381899560}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7BCE73E5-0F10-4F46-B3B1-AF7381899560}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7BCE73E5-0F10-4F46-B3B1-AF7381899560}.Debug|x64.ActiveCfg = Debug|Any CPU - {7BCE73E5-0F10-4F46-B3B1-AF7381899560}.Debug|x64.Build.0 = Debug|Any CPU - {7BCE73E5-0F10-4F46-B3B1-AF7381899560}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7BCE73E5-0F10-4F46-B3B1-AF7381899560}.Release|Any CPU.Build.0 = Release|Any CPU - {7BCE73E5-0F10-4F46-B3B1-AF7381899560}.Release|x64.ActiveCfg = Release|Any CPU - {7BCE73E5-0F10-4F46-B3B1-AF7381899560}.Release|x64.Build.0 = Release|Any CPU - {41D5CFAA-0691-4561-AE18-C2C14EE9C55A}.Debug|Any CPU.ActiveCfg = Debug|x64 - {41D5CFAA-0691-4561-AE18-C2C14EE9C55A}.Debug|x64.ActiveCfg = Debug|x64 - {41D5CFAA-0691-4561-AE18-C2C14EE9C55A}.Debug|x64.Build.0 = Debug|x64 - {41D5CFAA-0691-4561-AE18-C2C14EE9C55A}.Debug|x64.Deploy.0 = Debug|x64 - {41D5CFAA-0691-4561-AE18-C2C14EE9C55A}.Release|Any CPU.ActiveCfg = Release|x64 - {41D5CFAA-0691-4561-AE18-C2C14EE9C55A}.Release|x64.ActiveCfg = Release|x64 - {41D5CFAA-0691-4561-AE18-C2C14EE9C55A}.Release|x64.Build.0 = Release|x64 - {41D5CFAA-0691-4561-AE18-C2C14EE9C55A}.Release|x64.Deploy.0 = Release|x64 - {B5A51266-596D-4138-A62A-6C5B86E7C619}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B5A51266-596D-4138-A62A-6C5B86E7C619}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B5A51266-596D-4138-A62A-6C5B86E7C619}.Debug|x64.ActiveCfg = Debug|Any CPU - {B5A51266-596D-4138-A62A-6C5B86E7C619}.Debug|x64.Build.0 = Debug|Any CPU - {B5A51266-596D-4138-A62A-6C5B86E7C619}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B5A51266-596D-4138-A62A-6C5B86E7C619}.Release|Any CPU.Build.0 = Release|Any CPU - {B5A51266-596D-4138-A62A-6C5B86E7C619}.Release|x64.ActiveCfg = Release|Any CPU - {B5A51266-596D-4138-A62A-6C5B86E7C619}.Release|x64.Build.0 = Release|Any CPU - {7B9C23C7-E656-4137-B01A-C1A1AE9E7B02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7B9C23C7-E656-4137-B01A-C1A1AE9E7B02}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7B9C23C7-E656-4137-B01A-C1A1AE9E7B02}.Debug|x64.ActiveCfg = Debug|Any CPU - {7B9C23C7-E656-4137-B01A-C1A1AE9E7B02}.Debug|x64.Build.0 = Debug|Any CPU - {7B9C23C7-E656-4137-B01A-C1A1AE9E7B02}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7B9C23C7-E656-4137-B01A-C1A1AE9E7B02}.Release|Any CPU.Build.0 = Release|Any CPU - {7B9C23C7-E656-4137-B01A-C1A1AE9E7B02}.Release|x64.ActiveCfg = Release|Any CPU - {7B9C23C7-E656-4137-B01A-C1A1AE9E7B02}.Release|x64.Build.0 = Release|Any CPU - {160D0BEC-5CF6-4472-9BBF-498E2E07D5E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {160D0BEC-5CF6-4472-9BBF-498E2E07D5E3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {160D0BEC-5CF6-4472-9BBF-498E2E07D5E3}.Debug|x64.ActiveCfg = Debug|Any CPU - {160D0BEC-5CF6-4472-9BBF-498E2E07D5E3}.Debug|x64.Build.0 = Debug|Any CPU - {160D0BEC-5CF6-4472-9BBF-498E2E07D5E3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {160D0BEC-5CF6-4472-9BBF-498E2E07D5E3}.Release|Any CPU.Build.0 = Release|Any CPU - {160D0BEC-5CF6-4472-9BBF-498E2E07D5E3}.Release|x64.ActiveCfg = Release|Any CPU - {160D0BEC-5CF6-4472-9BBF-498E2E07D5E3}.Release|x64.Build.0 = Release|Any CPU - {E183BBE5-6F28-42D9-BD3B-4B56922556FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E183BBE5-6F28-42D9-BD3B-4B56922556FC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E183BBE5-6F28-42D9-BD3B-4B56922556FC}.Debug|x64.ActiveCfg = Debug|Any CPU - {E183BBE5-6F28-42D9-BD3B-4B56922556FC}.Debug|x64.Build.0 = Debug|Any CPU - {E183BBE5-6F28-42D9-BD3B-4B56922556FC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E183BBE5-6F28-42D9-BD3B-4B56922556FC}.Release|Any CPU.Build.0 = Release|Any CPU - {E183BBE5-6F28-42D9-BD3B-4B56922556FC}.Release|x64.ActiveCfg = Release|Any CPU - {E183BBE5-6F28-42D9-BD3B-4B56922556FC}.Release|x64.Build.0 = Release|Any CPU - {26BD3DDF-BD51-4B02-A39F-E4817CA14723}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {26BD3DDF-BD51-4B02-A39F-E4817CA14723}.Debug|Any CPU.Build.0 = Debug|Any CPU - {26BD3DDF-BD51-4B02-A39F-E4817CA14723}.Debug|x64.ActiveCfg = Debug|Any CPU - {26BD3DDF-BD51-4B02-A39F-E4817CA14723}.Debug|x64.Build.0 = Debug|Any CPU - {26BD3DDF-BD51-4B02-A39F-E4817CA14723}.Release|Any CPU.ActiveCfg = Release|Any CPU - {26BD3DDF-BD51-4B02-A39F-E4817CA14723}.Release|Any CPU.Build.0 = Release|Any CPU - {26BD3DDF-BD51-4B02-A39F-E4817CA14723}.Release|x64.ActiveCfg = Release|Any CPU - {26BD3DDF-BD51-4B02-A39F-E4817CA14723}.Release|x64.Build.0 = Release|Any CPU - {56E5E913-B701-4E9D-90D7-53862C5EB3D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {56E5E913-B701-4E9D-90D7-53862C5EB3D5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {56E5E913-B701-4E9D-90D7-53862C5EB3D5}.Debug|x64.ActiveCfg = Debug|Any CPU - {56E5E913-B701-4E9D-90D7-53862C5EB3D5}.Debug|x64.Build.0 = Debug|Any CPU - {56E5E913-B701-4E9D-90D7-53862C5EB3D5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {56E5E913-B701-4E9D-90D7-53862C5EB3D5}.Release|Any CPU.Build.0 = Release|Any CPU - {56E5E913-B701-4E9D-90D7-53862C5EB3D5}.Release|x64.ActiveCfg = Release|Any CPU - {56E5E913-B701-4E9D-90D7-53862C5EB3D5}.Release|x64.Build.0 = Release|Any CPU - {731B08FA-CF37-4CB4-9C7D-617E93E9E97E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {731B08FA-CF37-4CB4-9C7D-617E93E9E97E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {731B08FA-CF37-4CB4-9C7D-617E93E9E97E}.Debug|x64.ActiveCfg = Debug|Any CPU - {731B08FA-CF37-4CB4-9C7D-617E93E9E97E}.Debug|x64.Build.0 = Debug|Any CPU - {731B08FA-CF37-4CB4-9C7D-617E93E9E97E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {731B08FA-CF37-4CB4-9C7D-617E93E9E97E}.Release|Any CPU.Build.0 = Release|Any CPU - {731B08FA-CF37-4CB4-9C7D-617E93E9E97E}.Release|x64.ActiveCfg = Release|Any CPU - {731B08FA-CF37-4CB4-9C7D-617E93E9E97E}.Release|x64.Build.0 = Release|Any CPU - {20705AD5-B614-413D-9BB9-8308A958E4F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {20705AD5-B614-413D-9BB9-8308A958E4F4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {20705AD5-B614-413D-9BB9-8308A958E4F4}.Debug|x64.ActiveCfg = Debug|Any CPU - {20705AD5-B614-413D-9BB9-8308A958E4F4}.Debug|x64.Build.0 = Debug|Any CPU - {20705AD5-B614-413D-9BB9-8308A958E4F4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {20705AD5-B614-413D-9BB9-8308A958E4F4}.Release|Any CPU.Build.0 = Release|Any CPU - {20705AD5-B614-413D-9BB9-8308A958E4F4}.Release|x64.ActiveCfg = Release|Any CPU - {20705AD5-B614-413D-9BB9-8308A958E4F4}.Release|x64.Build.0 = Release|Any CPU - {603E2343-6C02-4A68-A434-BC029AB6F788}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {603E2343-6C02-4A68-A434-BC029AB6F788}.Debug|Any CPU.Build.0 = Debug|Any CPU - {603E2343-6C02-4A68-A434-BC029AB6F788}.Debug|x64.ActiveCfg = Debug|Any CPU - {603E2343-6C02-4A68-A434-BC029AB6F788}.Debug|x64.Build.0 = Debug|Any CPU - {603E2343-6C02-4A68-A434-BC029AB6F788}.Release|Any CPU.ActiveCfg = Release|Any CPU - {603E2343-6C02-4A68-A434-BC029AB6F788}.Release|Any CPU.Build.0 = Release|Any CPU - {603E2343-6C02-4A68-A434-BC029AB6F788}.Release|x64.ActiveCfg = Release|Any CPU - {603E2343-6C02-4A68-A434-BC029AB6F788}.Release|x64.Build.0 = Release|Any CPU - {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Debug|x64.ActiveCfg = Debug|Any CPU - {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Debug|x64.Build.0 = Debug|Any CPU - {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Release|Any CPU.Build.0 = Release|Any CPU - {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Release|x64.ActiveCfg = Release|Any CPU - {4A66EF70-036A-4AA3-BE57-D56645FEE3CD}.Release|x64.Build.0 = Release|Any CPU - {B400F62F-7AC4-49B4-AA76-4DA358DDE8B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B400F62F-7AC4-49B4-AA76-4DA358DDE8B4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B400F62F-7AC4-49B4-AA76-4DA358DDE8B4}.Debug|x64.ActiveCfg = Debug|Any CPU - {B400F62F-7AC4-49B4-AA76-4DA358DDE8B4}.Debug|x64.Build.0 = Debug|Any CPU - {B400F62F-7AC4-49B4-AA76-4DA358DDE8B4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B400F62F-7AC4-49B4-AA76-4DA358DDE8B4}.Release|Any CPU.Build.0 = Release|Any CPU - {B400F62F-7AC4-49B4-AA76-4DA358DDE8B4}.Release|x64.ActiveCfg = Release|Any CPU - {B400F62F-7AC4-49B4-AA76-4DA358DDE8B4}.Release|x64.Build.0 = Release|Any CPU - {5EEC5BBA-67C7-4417-9283-53EEFB280FEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5EEC5BBA-67C7-4417-9283-53EEFB280FEB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5EEC5BBA-67C7-4417-9283-53EEFB280FEB}.Debug|x64.ActiveCfg = Debug|Any CPU - {5EEC5BBA-67C7-4417-9283-53EEFB280FEB}.Debug|x64.Build.0 = Debug|Any CPU - {5EEC5BBA-67C7-4417-9283-53EEFB280FEB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5EEC5BBA-67C7-4417-9283-53EEFB280FEB}.Release|Any CPU.Build.0 = Release|Any CPU - {5EEC5BBA-67C7-4417-9283-53EEFB280FEB}.Release|x64.ActiveCfg = Release|Any CPU - {5EEC5BBA-67C7-4417-9283-53EEFB280FEB}.Release|x64.Build.0 = Release|Any CPU - {25452C06-6C94-4E3F-9978-DC42CE9CAD9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {25452C06-6C94-4E3F-9978-DC42CE9CAD9C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {25452C06-6C94-4E3F-9978-DC42CE9CAD9C}.Debug|x64.ActiveCfg = Debug|Any CPU - {25452C06-6C94-4E3F-9978-DC42CE9CAD9C}.Debug|x64.Build.0 = Debug|Any CPU - {25452C06-6C94-4E3F-9978-DC42CE9CAD9C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {25452C06-6C94-4E3F-9978-DC42CE9CAD9C}.Release|Any CPU.Build.0 = Release|Any CPU - {25452C06-6C94-4E3F-9978-DC42CE9CAD9C}.Release|x64.ActiveCfg = Release|Any CPU - {25452C06-6C94-4E3F-9978-DC42CE9CAD9C}.Release|x64.Build.0 = Release|Any CPU - {E1E0C51C-D634-4945-A310-16438CA78797}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E1E0C51C-D634-4945-A310-16438CA78797}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E1E0C51C-D634-4945-A310-16438CA78797}.Debug|x64.ActiveCfg = Debug|Any CPU - {E1E0C51C-D634-4945-A310-16438CA78797}.Debug|x64.Build.0 = Debug|Any CPU - {E1E0C51C-D634-4945-A310-16438CA78797}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E1E0C51C-D634-4945-A310-16438CA78797}.Release|Any CPU.Build.0 = Release|Any CPU - {E1E0C51C-D634-4945-A310-16438CA78797}.Release|x64.ActiveCfg = Release|Any CPU - {E1E0C51C-D634-4945-A310-16438CA78797}.Release|x64.Build.0 = Release|Any CPU - {082C4376-E6AC-45AA-B985-65E7142F171C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {082C4376-E6AC-45AA-B985-65E7142F171C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {082C4376-E6AC-45AA-B985-65E7142F171C}.Debug|x64.ActiveCfg = Debug|Any CPU - {082C4376-E6AC-45AA-B985-65E7142F171C}.Debug|x64.Build.0 = Debug|Any CPU - {082C4376-E6AC-45AA-B985-65E7142F171C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {082C4376-E6AC-45AA-B985-65E7142F171C}.Release|Any CPU.Build.0 = Release|Any CPU - {082C4376-E6AC-45AA-B985-65E7142F171C}.Release|x64.ActiveCfg = Release|Any CPU - {082C4376-E6AC-45AA-B985-65E7142F171C}.Release|x64.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {B1BA6457-82A7-479C-9193-BFAADC667883} - EndGlobalSection -EndGlobal diff --git a/DigiMixer/DigiMixer.slnx b/DigiMixer/DigiMixer.slnx new file mode 100644 index 00000000..013ccdb4 --- /dev/null +++ b/DigiMixer/DigiMixer.slnx @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DigiMixer/Directory.Packages.props b/DigiMixer/Directory.Packages.props new file mode 100644 index 00000000..30ec7fe3 --- /dev/null +++ b/DigiMixer/Directory.Packages.props @@ -0,0 +1,27 @@ + + + true + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 6f88ed5872e1f9f772d8e26c17237d577743efea Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Sat, 15 Nov 2025 14:22:42 +0000 Subject: [PATCH 16/16] Convert WpfUtil to centralised package management and slnx --- WpfUtil/Directory.Packages.props | 11 ++++++ .../JonSkeet.CoreAppUtil.csproj | 8 ++-- WpfUtil/WpfUtil.sln | 37 ------------------- WpfUtil/WpfUtil.slnx | 8 ++++ 4 files changed, 23 insertions(+), 41 deletions(-) create mode 100644 WpfUtil/Directory.Packages.props delete mode 100644 WpfUtil/WpfUtil.sln create mode 100644 WpfUtil/WpfUtil.slnx diff --git a/WpfUtil/Directory.Packages.props b/WpfUtil/Directory.Packages.props new file mode 100644 index 00000000..3f0d3c27 --- /dev/null +++ b/WpfUtil/Directory.Packages.props @@ -0,0 +1,11 @@ + + + true + + + + + + + + \ No newline at end of file diff --git a/WpfUtil/JonSkeet.CoreAppUtil/JonSkeet.CoreAppUtil.csproj b/WpfUtil/JonSkeet.CoreAppUtil/JonSkeet.CoreAppUtil.csproj index 89219df9..f6e10da9 100644 --- a/WpfUtil/JonSkeet.CoreAppUtil/JonSkeet.CoreAppUtil.csproj +++ b/WpfUtil/JonSkeet.CoreAppUtil/JonSkeet.CoreAppUtil.csproj @@ -5,9 +5,9 @@ - - - - + + + + diff --git a/WpfUtil/WpfUtil.sln b/WpfUtil/WpfUtil.sln deleted file mode 100644 index 27fbeb4a..00000000 --- a/WpfUtil/WpfUtil.sln +++ /dev/null @@ -1,37 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JonSkeet.WpfUtil", "JonSkeet.WpfUtil\JonSkeet.WpfUtil.csproj", "{3E6FAC79-B48A-4EB4-9746-75D061A46E61}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JonSkeet.WpfLogging", "JonSkeet.WpfLogging\JonSkeet.WpfLogging.csproj", "{00D5CA3E-B233-4465-AC8F-590A39C69B8F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JonSkeet.CoreAppUtil", "JonSkeet.CoreAppUtil\JonSkeet.CoreAppUtil.csproj", "{D62CEFB0-9BAD-43E2-B245-9CA70ECEA87B}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {3E6FAC79-B48A-4EB4-9746-75D061A46E61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3E6FAC79-B48A-4EB4-9746-75D061A46E61}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3E6FAC79-B48A-4EB4-9746-75D061A46E61}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3E6FAC79-B48A-4EB4-9746-75D061A46E61}.Release|Any CPU.Build.0 = Release|Any CPU - {00D5CA3E-B233-4465-AC8F-590A39C69B8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {00D5CA3E-B233-4465-AC8F-590A39C69B8F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {00D5CA3E-B233-4465-AC8F-590A39C69B8F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {00D5CA3E-B233-4465-AC8F-590A39C69B8F}.Release|Any CPU.Build.0 = Release|Any CPU - {D62CEFB0-9BAD-43E2-B245-9CA70ECEA87B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D62CEFB0-9BAD-43E2-B245-9CA70ECEA87B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D62CEFB0-9BAD-43E2-B245-9CA70ECEA87B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D62CEFB0-9BAD-43E2-B245-9CA70ECEA87B}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {EBE2CA9E-9847-4DFD-A9BA-6474FE4F0429} - EndGlobalSection -EndGlobal diff --git a/WpfUtil/WpfUtil.slnx b/WpfUtil/WpfUtil.slnx new file mode 100644 index 00000000..c2a4dc7c --- /dev/null +++ b/WpfUtil/WpfUtil.slnx @@ -0,0 +1,8 @@ + + + + + + + +