From 54e329fbca8e942f468a5c3c38c6f6f41e2cc7c5 Mon Sep 17 00:00:00 2001 From: Martin Renner Date: Mon, 27 May 2024 19:41:31 +0200 Subject: [PATCH 1/2] Support TimeSpan and String types also for "state" properties and not just for "display" properties. In the same time, this combines the two different parsing methods into one single, because one was always an incomplete subset of the other. If an exception is thrown in the main loop, this must not cause a reconnect. Issue #114 --- .../PropertyLogic/PropertyComparer.cs | 12 ++-- .../SimHub/PropertyParser.cs | 3 +- .../SimHub/PropertyType.cs | 70 ++++--------------- .../SimHub/SimHubConnection.cs | 38 +++++++--- .../Actions/HotkeyActionTests.cs | 18 +++-- .../PropertyComparerEvaluateTests.cs | 36 +++++----- .../SimHub/PropertyTypeTests.cs | 61 ++++++++++++++++ 7 files changed, 134 insertions(+), 104 deletions(-) create mode 100644 StreamDeckSimHub.PluginTests/SimHub/PropertyTypeTests.cs diff --git a/StreamDeckSimHub.Plugin/PropertyLogic/PropertyComparer.cs b/StreamDeckSimHub.Plugin/PropertyLogic/PropertyComparer.cs index e8f37d0..7f95413 100644 --- a/StreamDeckSimHub.Plugin/PropertyLogic/PropertyComparer.cs +++ b/StreamDeckSimHub.Plugin/PropertyLogic/PropertyComparer.cs @@ -7,7 +7,7 @@ namespace StreamDeckSimHub.Plugin.PropertyLogic; /// -/// Parses expressions like some.property==5 and evaluates their resuls. +/// Parses expressions like some.property==5 and evaluates their results. /// public class PropertyComparer { @@ -86,14 +86,10 @@ public bool Evaluate(PropertyType propertyType, IComparable? propertyValue, Cond return false; } - // When we arrive here with a SimHub property of type "object", it could be interpreted - // as "double" or "string" (see PropertyType.ParseFromSimHub and .ParseLiberally). - // Both values (property value and compare value) have to be of the same type, otherwise they are unequal. - var compareFunction = expression.Operator.CompareFunction(); if (expression.Operator != ConditionOperator.Between) { - var compareValue = propertyType.ParseLiberally(expression.CompareValue); + var compareValue = propertyType.Parse(expression.CompareValue) ?? ""; if (propertyValue.GetType() != compareValue.GetType()) { _logger.LogDebug("Property value and compare value are of different types, returning 'false'"); @@ -109,8 +105,8 @@ public bool Evaluate(PropertyType propertyType, IComparable? propertyValue, Cond return false; } - var compareValue1 = propertyType.ParseLiberally(values[0]); - var compareValue2 = propertyType.ParseLiberally(values[1]); + var compareValue1 = propertyType.Parse(values[0]) ?? ""; + var compareValue2 = propertyType.Parse(values[1]) ?? ""; if (propertyValue.GetType() != compareValue1.GetType() || propertyValue.GetType() != compareValue2.GetType()) { _logger.LogDebug("Property value and compare value are of different types, returning 'false'"); diff --git a/StreamDeckSimHub.Plugin/SimHub/PropertyParser.cs b/StreamDeckSimHub.Plugin/SimHub/PropertyParser.cs index 10071de..40431c5 100644 --- a/StreamDeckSimHub.Plugin/SimHub/PropertyParser.cs +++ b/StreamDeckSimHub.Plugin/SimHub/PropertyParser.cs @@ -26,6 +26,7 @@ public class PropertyParser if (valueAsString == "(null)") valueAsString = null; // See https://github.com/pre-martin/SimHubPropertyServer/blob/main/PropertyServer.Plugin/Property/SimHubProperty.cs + // Keep PropertyTypeTests.cs in sync with this list. var type = typeAsString switch { "boolean" => PropertyType.Boolean, @@ -37,7 +38,7 @@ public class PropertyParser "object" => PropertyType.Object, _ => PropertyType.Double // Should not happen. But best guess should always be "double". }; - var value = type.ParseFromSimHub(valueAsString); + var value = type.Parse(valueAsString); return (name, type, value); } diff --git a/StreamDeckSimHub.Plugin/SimHub/PropertyType.cs b/StreamDeckSimHub.Plugin/SimHub/PropertyType.cs index 7f2d161..292660c 100644 --- a/StreamDeckSimHub.Plugin/SimHub/PropertyType.cs +++ b/StreamDeckSimHub.Plugin/SimHub/PropertyType.cs @@ -24,68 +24,15 @@ public enum PropertyType /// public static class PropertyTypeEx { - /// - /// Converts a string value into a typed value, which is returned as IComparable. The method assumes that the - /// string value was received from SimHub Property Plugin. - /// - public static IComparable? ParseFromSimHub(this PropertyType propertyType, string? propertyValue) + /// Converts a string value into a typed value, which is returned as IComparable. This method works for data + /// received from SimHub, but also for user supplied values, like compare values. + public static IComparable? Parse(this PropertyType propertyType, string? propertyValue) { if (propertyValue == null) { return null; } - switch (propertyType) - { - case PropertyType.Boolean: - { - var result = bool.TryParse(propertyValue, out var boolResult); - return result ? boolResult : false; - } - case PropertyType.Integer: - { - var result = int.TryParse(propertyValue, out var intResult); - return result ? intResult : 0; - } - case PropertyType.Long: - { - var result = long.TryParse(propertyValue, out var longResult); - return result ? longResult : 0L; - } - case PropertyType.Double: - { - var result = double.TryParse(propertyValue, NumberStyles.Any, CultureInfo.InvariantCulture, out var doubleResult); - return result ? doubleResult : 0.0d; - } - case PropertyType.TimeSpan: - { - var result = TimeSpan.TryParse(propertyValue, CultureInfo.InvariantCulture, out var timeSpanResult); - return result ? timeSpanResult : null; - } - case PropertyType.String: - { - return propertyValue; - } - case PropertyType.Object: - { - // Try to parse as double. - var result = double.TryParse(propertyValue, NumberStyles.Any, CultureInfo.InvariantCulture, out var doubleResult); - if (result) return doubleResult; - // If not possible, return as string - return propertyValue; - } - default: - throw new ArgumentOutOfRangeException(nameof(propertyType), propertyType, null); - } - } - - /// Converts a string value into a typed value, which is returned as IComparable. The method is more liberal - /// than ParseFromSimHub and accepts a wider range of property values. - /// - /// This method should be used to parse user input. - /// - public static IComparable ParseLiberally(this PropertyType propertyType, string propertyValue) - { switch (propertyType) { case PropertyType.Boolean: @@ -120,6 +67,15 @@ public static IComparable ParseLiberally(this PropertyType propertyType, string var result = double.TryParse(propertyValue, NumberStyles.Any, CultureInfo.InvariantCulture, out var doubleResult); return result ? doubleResult : 0.0d; } + case PropertyType.TimeSpan: + { + var result = TimeSpan.TryParse(propertyValue, CultureInfo.InvariantCulture, out var timeSpanResult); + return result ? timeSpanResult : null; + } + case PropertyType.String: + { + return propertyValue; + } case PropertyType.Object: { // Try to parse as double. @@ -129,7 +85,7 @@ public static IComparable ParseLiberally(this PropertyType propertyType, string return propertyValue; } default: - throw new ArgumentOutOfRangeException(nameof(propertyType), propertyType, null); + throw new ArgumentOutOfRangeException(nameof(propertyType), propertyType, "PropertyType parser not implemented for type"); } } } \ No newline at end of file diff --git a/StreamDeckSimHub.Plugin/SimHub/SimHubConnection.cs b/StreamDeckSimHub.Plugin/SimHub/SimHubConnection.cs index 74fb689..7f6e06a 100644 --- a/StreamDeckSimHub.Plugin/SimHub/SimHubConnection.cs +++ b/StreamDeckSimHub.Plugin/SimHub/SimHubConnection.cs @@ -141,20 +141,31 @@ private async Task ConnectAsync() { Logger.Info($"Established connection to {Sanitize(line)}"); Connected = true; - foreach (var propertyName in _subscriptions.Keys) - { - await SendSubscribe(propertyName); - } - - await ReadFromServer(); } } catch (Exception e) { - Logger.Info($"Connection failed: {e.Message}"); + Logger.Info($"Connection failed: {e}"); } - if (!Connected) + if (Connected) + { + Logger.Info("Sending queued subscriptions and starting poll loop"); + try + { + foreach (var propertyName in _subscriptions.Keys) + { + await SendSubscribe(propertyName); + } + } + catch (Exception e) + { + Logger.Error(e, "Exception while subscribing queued subscriptions"); + } + + await ReadFromServer(); + } + else { await Task.Delay(TimeSpan.FromSeconds(4)); } @@ -346,7 +357,14 @@ private async Task ReadFromServer() Logger.Debug($"Received from server: {Sanitize(line)}"); if (line.StartsWith("Property ")) { - await ParseProperty(line); + try + { + await ParseProperty(line); + } + catch (Exception e) + { + Logger.Error(e, $"Unhandled exception while processing data from server. Received line was: \"{Sanitize(line)}\""); + } } } @@ -356,7 +374,7 @@ private async Task ReadFromServer() catch (IOException ioe) { // IOException: Fall through to "CloseAndReconnect". - Logger.Warn($"Received IOException while waiting for data: {ioe.Message}"); + Logger.Warn($"Received IOException while waiting for data: {ioe}"); } await CloseAndReconnect(); diff --git a/StreamDeckSimHub.PluginTests/Actions/HotkeyActionTests.cs b/StreamDeckSimHub.PluginTests/Actions/HotkeyActionTests.cs index 32fbf59..5c30e60 100644 --- a/StreamDeckSimHub.PluginTests/Actions/HotkeyActionTests.cs +++ b/StreamDeckSimHub.PluginTests/Actions/HotkeyActionTests.cs @@ -25,14 +25,13 @@ public void TestOldComparisonInteger() Assert.That(ce.Operator, Is.EqualTo(ConditionOperator.Gt)); Assert.That(ce.CompareValue, Is.EqualTo("0")); - // If "EngineStarted" would be an "Integer" property, the old logic was: + // If "EngineStarted" is an "Integer" property, the old logic was: // - PropertyValue from SimHub > 0: True // - else: False - // We simulate also that we have received the values from SimHub. This ensures the old logic is still valid. - var propValueOne = PropertyType.Integer.ParseFromSimHub("1"); - var propValueTwo = PropertyType.Integer.ParseFromSimHub("2"); - var propValueZero = PropertyType.Integer.ParseFromSimHub("0"); - var propValueMinusOne = PropertyType.Integer.ParseFromSimHub("-1"); + var propValueOne = PropertyType.Integer.Parse("1"); + var propValueTwo = PropertyType.Integer.Parse("2"); + var propValueZero = PropertyType.Integer.Parse("0"); + var propValueMinusOne = PropertyType.Integer.Parse("-1"); Assert.That(_propertyComparer.Evaluate(PropertyType.Integer, propValueOne, ce), Is.True); Assert.That(_propertyComparer.Evaluate(PropertyType.Integer, propValueTwo, ce), Is.True); Assert.That(_propertyComparer.Evaluate(PropertyType.Integer, propValueZero, ce), Is.False); @@ -46,12 +45,11 @@ public void TestOldComparisonBoolean() Assert.That(ce.Operator, Is.EqualTo(ConditionOperator.Gt)); Assert.That(ce.CompareValue, Is.EqualTo("0")); - // If "EngineStarted" would be a "Boolean" property, the old logic was: + // If "EngineStarted" is a "Boolean" property, the old logic was: // - PropertyValue from SimHub = "True": > True // - else: False - // We simulate also that we have received the values from SimHub. This ensures the old logic is still valid. - var propValueTrue = PropertyType.Boolean.ParseFromSimHub("True"); - var propValueFalse = PropertyType.Boolean.ParseFromSimHub("False"); + var propValueTrue = PropertyType.Boolean.Parse("True"); + var propValueFalse = PropertyType.Boolean.Parse("False"); Assert.That(_propertyComparer.Evaluate(PropertyType.Boolean, propValueTrue, ce), Is.True); Assert.That(_propertyComparer.Evaluate(PropertyType.Boolean, propValueFalse, ce), Is.False); } diff --git a/StreamDeckSimHub.PluginTests/AttributeLogic/PropertyComparerEvaluateTests.cs b/StreamDeckSimHub.PluginTests/AttributeLogic/PropertyComparerEvaluateTests.cs index 19ea745..d541a11 100644 --- a/StreamDeckSimHub.PluginTests/AttributeLogic/PropertyComparerEvaluateTests.cs +++ b/StreamDeckSimHub.PluginTests/AttributeLogic/PropertyComparerEvaluateTests.cs @@ -10,15 +10,15 @@ namespace StreamDeckSimHub.PluginTests.AttributeLogic; public class PropertyComparerEvaluateTests { private PropertyComparer _propertyComparer; - private readonly IComparable? _propValueTrue = PropertyType.Boolean.ParseFromSimHub("True"); - private readonly IComparable? _propValueFalse = PropertyType.Boolean.ParseFromSimHub("False"); - private readonly IComparable? _propValueZero = PropertyType.Integer.ParseFromSimHub("0"); - private readonly IComparable? _propValueOne = PropertyType.Integer.ParseFromSimHub("1"); - private readonly IComparable? _propValueTwo = PropertyType.Integer.ParseFromSimHub("2"); - private readonly IComparable? _propValueThree = PropertyType.Integer.ParseFromSimHub("3"); - private readonly IComparable? _propValueLongZero = PropertyType.Long.ParseFromSimHub("0"); - private readonly IComparable? _propValueLongOne = PropertyType.Long.ParseFromSimHub("1"); - private readonly IComparable? _propValueLongTwo = PropertyType.Long.ParseFromSimHub("2"); + private readonly IComparable? _propValueTrue = PropertyType.Boolean.Parse("True"); + private readonly IComparable? _propValueFalse = PropertyType.Boolean.Parse("False"); + private readonly IComparable? _propValueZero = PropertyType.Integer.Parse("0"); + private readonly IComparable? _propValueOne = PropertyType.Integer.Parse("1"); + private readonly IComparable? _propValueTwo = PropertyType.Integer.Parse("2"); + private readonly IComparable? _propValueThree = PropertyType.Integer.Parse("3"); + private readonly IComparable? _propValueLongZero = PropertyType.Long.Parse("0"); + private readonly IComparable? _propValueLongOne = PropertyType.Long.Parse("1"); + private readonly IComparable? _propValueLongTwo = PropertyType.Long.Parse("2"); [SetUp] public void Init() @@ -115,8 +115,8 @@ public void LongPropWithIntegerValue() public void DoublePropWithDoubleValue() { var ce = _propertyComparer.Parse("acc.graphics.fuelEstimatedLaps>=3.5"); - var propValue5Dot9 = PropertyType.Double.ParseFromSimHub("5.9"); - var propValue3Dot4 = PropertyType.Double.ParseFromSimHub("3.4"); + var propValue5Dot9 = PropertyType.Double.Parse("5.9"); + var propValue3Dot4 = PropertyType.Double.Parse("3.4"); Assert.That(_propertyComparer.Evaluate(PropertyType.Double, propValue5Dot9, ce), Is.True); Assert.That(_propertyComparer.Evaluate(PropertyType.Double, propValue3Dot4, ce), Is.False); } @@ -145,12 +145,12 @@ public void IntegerPropInvalidBetween() public void ObjectProp() { var ce = _propertyComparer.Parse("DataCorePlugin.GameData.Gear>=3"); - var propValue2 = PropertyType.Object.ParseFromSimHub("2"); - var propValue3 = PropertyType.Object.ParseFromSimHub("3"); + var propValue2 = PropertyType.Object.Parse("2"); + var propValue3 = PropertyType.Object.Parse("3"); Assert.That(_propertyComparer.Evaluate(PropertyType.Object, propValue2, ce), Is.False); Assert.That(_propertyComparer.Evaluate(PropertyType.Object, propValue3, ce), Is.True); - var propValueN = PropertyType.Object.ParseFromSimHub("N"); + var propValueN = PropertyType.Object.Parse("N"); Assert.That(_propertyComparer.Evaluate(PropertyType.Object, propValueN, ce), Is.False); } @@ -158,14 +158,14 @@ public void ObjectProp() public void ObjectPropBetween() { var ce = _propertyComparer.Parse("DataCorePlugin.GameData.Gear~~3;4"); - var propValue2 = PropertyType.Object.ParseFromSimHub("2"); - var propValue3 = PropertyType.Object.ParseFromSimHub("3"); - var propValue5 = PropertyType.Object.ParseFromSimHub("5"); + var propValue2 = PropertyType.Object.Parse("2"); + var propValue3 = PropertyType.Object.Parse("3"); + var propValue5 = PropertyType.Object.Parse("5"); Assert.That(_propertyComparer.Evaluate(PropertyType.Object, propValue2, ce), Is.False); Assert.That(_propertyComparer.Evaluate(PropertyType.Object, propValue3, ce), Is.True); Assert.That(_propertyComparer.Evaluate(PropertyType.Object, propValue5, ce), Is.False); - var propValueN = PropertyType.Object.ParseFromSimHub("N"); + var propValueN = PropertyType.Object.Parse("N"); Assert.That(_propertyComparer.Evaluate(PropertyType.Object, propValueN, ce), Is.False); } } \ No newline at end of file diff --git a/StreamDeckSimHub.PluginTests/SimHub/PropertyTypeTests.cs b/StreamDeckSimHub.PluginTests/SimHub/PropertyTypeTests.cs new file mode 100644 index 0000000..413418d --- /dev/null +++ b/StreamDeckSimHub.PluginTests/SimHub/PropertyTypeTests.cs @@ -0,0 +1,61 @@ +// Copyright (C) 2024 Martin Renner +// LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) + +using StreamDeckSimHub.Plugin.SimHub; + +namespace StreamDeckSimHub.PluginTests.SimHub; + +/// +/// Tests to ensure that all known property types are really parsed correctly. +/// +public class PropertyTypeTests +{ + [Test] + public void TestBoolean() + { + var r1 = PropertyType.Boolean.Parse("True"); + Assert.That(r1, Is.True); + } + + [Test] + public void TestInteger() + { + var r1 = PropertyType.Integer.Parse("10"); + Assert.That(r1, Is.EqualTo(10)); + } + + [Test] + public void TestLong() + { + var r1 = PropertyType.Long.Parse("10"); + Assert.That(r1, Is.EqualTo(10)); + } + + [Test] + public void TestDouble() + { + var r1 = PropertyType.Double.Parse("10.1"); + Assert.That(r1, Is.EqualTo(10.1)); + } + + [Test] + public void TestTimespan() + { + var r1 = PropertyType.TimeSpan.Parse("1:05:03"); + Assert.That(r1, Is.EqualTo(TimeSpan.FromSeconds(1 * 3600 + 5 * 60 + 3))); + } + + [Test] + public void TestString() + { + var r1 = PropertyType.String.Parse("Hello"); + Assert.That(r1, Is.EqualTo("Hello")); + } + + [Test] + public void TestObject() + { + var r1 = PropertyType.Object.Parse("Hello"); + Assert.That(r1, Is.EqualTo("Hello")); + } +} \ No newline at end of file From cf543427db82cb4cabe8b853220753ed7144b21c Mon Sep 17 00:00:00 2001 From: Martin Renner Date: Tue, 28 May 2024 19:52:07 +0200 Subject: [PATCH 2/2] Updated documentation to reflect all available property types. --- doc/hotkey/Hotkey.adoc | 46 +++++++++++++++++------------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/doc/hotkey/Hotkey.adoc b/doc/hotkey/Hotkey.adoc index 7e53c9d..74e421a 100644 --- a/doc/hotkey/Hotkey.adoc +++ b/doc/hotkey/Hotkey.adoc @@ -104,12 +104,10 @@ After how many milliseconds of holding down the Stream Deck key, it will be reco This field allows to bind the button to a SimHub property, which will be used to determine the state of the button. -All properties, that are listed in SimHub under "Available properties" (around 2000+ properties) can be used. But the plugin allows access to even more properties, and for use in these Stream Deck actions, it is better to use "_typed_" properties instead of "_generic_" properties. +All properties, that are listed in SimHub under "Available properties" (around 2000+ properties) can be used. But the plugin allows access to even more properties, like the ShakeIt properties. TIP: Be sure to read the documentation of the https://github.com/pre-martin/SimHubPropertyServer[SimHub Property Server plugin], especially the section about the available properties! -TIP: "Generic" properties are received untyped as `object` (see SimHubPropertyServer plugin). This plugin tries to interpret them as a `double` value. If this is not possible, they are treated as strings. - === Simple SimHub Property Enter the name of a SimHub property. E.g. @@ -118,6 +116,12 @@ Enter the name of a SimHub property. E.g. dcp.gd.EngineIgnitionOn ---- +or (if you prefer the generic version of the property) + +---- +DataCorePlugin.GameData.EngineIgnitionOn +---- + The value of this SimHub property will update the state of the button. The logic for the action state depending on the SimHub property value is as follows: @@ -158,35 +162,21 @@ The following table shows the comparison logic for different SimHub property typ |=== | SimHub property type | supported comparison values | evaluation rules -| boolean -| "true", "false" -| should be self explanatory - -| -| any integer value -| comp. value == 0: "false" + -comp. value > 0: "true" +| boolean | "true", "false" | should be self-explanatory +| | any integer value | comp. value == 0: "false" + + comp. value > 0: "true" -| integer -| any integer value -| should be self explanatory +| integer | any integer value | should be self-explanatory +| | "true", "false" | prop. value == 1: "true" + + all other prop. values: "false" -| -| "true", "false" -| prop. value == 1: "true" + -all other prop. values: "false" +| long | same as "integer" | same as integer -| long -| same as "integer" -| same as integer +| double | any integer or floating | should be self-explanatory -| double -| any integer or floating -| should be self explanatory +| string | any string | should be self-explanatory -| object -| anything -| The types of the property value and the comparision value have to be the same, otherwise they are treated as "not equal". The plugin tries to interpret property values of type "object" as "double". If this is possible, the comparison value should be also of type "double", otherwise they are "not equal". +| object | anything | The types of the property value and the comparision value have to be the same, otherwise they are treated as "not equal". The plugin first tries to interpret property values of type "object" as "double". If this is possible, the comparison value should be also of type "double", otherwise they are "not equal". If interpretation as "double" is not possible, the plugin returns the property value as "string". |=== @@ -200,6 +190,8 @@ So the following expressions are all valid: `acc.physics.Gear~2;4` * Generic SimHub property: + `DataCorePlugin.GameData.SpotterCarLeft>0` or just `DataCorePlugin.GameData.SpotterCarLeft` +* Another generic property: + + `MotionPlugin.Global.DeviceState != Disabled` [#displayFormatForTitle]