diff --git a/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs b/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs
index b2565b3d..b3e6d48f 100644
--- a/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs
+++ b/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs
@@ -78,9 +78,7 @@ public class DeviceJsonApi
}
var mParams = method.GetParameters();
- var convertedParams = mParams
- .Select((p, i) => ConvertType(action.Params[i], p.ParameterType))
- .ToArray();
+ var convertedParams = BuildConvertedParams(mParams, action.Params);
Task.Run(() =>
{
@@ -142,9 +140,7 @@ public class DeviceJsonApi
}
var mParams = method.GetParameters();
- var convertedParams = mParams
- .Select((p, i) => ConvertType(action.Params[i], p.ParameterType))
- .ToArray();
+ var convertedParams = BuildConvertedParams(mParams, action.Params);
try
{
@@ -173,8 +169,56 @@ public class DeviceJsonApi
}
}
+ ///
+ /// Builds the ordered, converted argument array to pass to .
+ ///
+ ///
+ /// Handles the common positional case (one supplied param per method parameter), as well as a method
+ /// with a single array-typed parameter (e.g. void Foo(string[] items) ). For the latter, config
+ /// authors may either supply the params as a flat list matching the array's contents
+ /// ("params": ["a", "b", "c"] ) or as a single nested array ("params": [["a", "b", "c"]] ) -
+ /// both are converted into the single array argument correctly.
+ ///
+ /// The target method's parameters.
+ /// The raw parameter values supplied in the action.
+ /// The converted arguments, in order, ready to pass to Invoke.
+ private static object[] BuildConvertedParams(ParameterInfo[] mParams, object[] actionParams)
+ {
+ if (mParams.Length == 1 && mParams[0].ParameterType.IsArray && actionParams.Length != 1)
+ {
+ return new[] { ConvertType(actionParams, mParams[0].ParameterType) };
+ }
+
+ return mParams
+ .Select((p, i) => ConvertType(actionParams[i], p.ParameterType))
+ .ToArray();
+ }
+
private static object ConvertType(object value, Type conversionType)
{
+ if (conversionType.IsArray)
+ {
+ var elementType = conversionType.GetElementType();
+
+ // The supplied value is a collection of raw items (a JArray, or the whole flat params list) -
+ // convert each item to the array's element type.
+ if (value is IEnumerable enumerable && !(value is string))
+ {
+ var items = enumerable.Cast()
+ .Select(item => ConvertType(item, elementType))
+ .ToArray();
+
+ var typedArray = Array.CreateInstance(elementType, items.Length);
+ Array.Copy(items, typedArray, items.Length);
+ return typedArray;
+ }
+
+ // A single scalar value was supplied for an array parameter - wrap it in a single-element array.
+ var singleArray = Array.CreateInstance(elementType, 1);
+ singleArray.SetValue(ConvertType(value, elementType), 0);
+ return singleArray;
+ }
+
if (!conversionType.IsEnum)
{
return Convert.ChangeType(value, conversionType, System.Globalization.CultureInfo.InvariantCulture);
diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs b/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs
index f176c24c..a7e30660 100644
--- a/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs
+++ b/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs
@@ -65,7 +65,7 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA
int VolumeHeldRepeatInterval = 200;
ushort VolumeInterval = 655;
- ushort _FakeVolumeLevel = 31768;
+ ushort _FakeVolumeLevel = 32768;
bool _IsMuted;
Timer _volumeUpTimer;
Timer _volumeDownTimer;
@@ -107,6 +107,11 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA
VolumeLevelFeedback = new IntFeedback("volume", () => { return _FakeVolumeLevel; });
MuteFeedback = new BoolFeedback("muteOn", () => _IsMuted);
+ // Seed the feedback's cached value from _FakeVolumeLevel immediately. IntFeedback.IntValue only
+ // reflects the current backing value after FireUpdate has run once, so without this the UI would
+ // report 0 until the volume was first incremented/decremented.
+ VolumeLevelFeedback.FireUpdate();
+
WarmupTime = 10000;
CooldownTime = 10000;
}
diff --git a/src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpoint.cs b/src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpoint.cs
new file mode 100644
index 00000000..74853223
--- /dev/null
+++ b/src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpoint.cs
@@ -0,0 +1,177 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json;
+using PepperDash.Core;
+using PepperDash.Core.Logging;
+using PepperDash.Essentials.Core;
+using PepperDash.Essentials.Core.Config;
+using Serilog.Events;
+
+namespace PepperDash.Essentials.Devices.Common.Routing;
+
+///
+/// A mock midpoint routing device (e.g. a matrix switcher) that implements
+/// without any real hardware communication. Its input and output ports are configured via
+/// , each with a name, signal type, and physical port
+/// (connection) type - so it can stand in for a real switching device (such as a StreamSync matrix) for
+/// development and testing of routing logic.
+///
+[Description("A mock routing midpoint (e.g. matrix switcher) device for testing routing logic without real hardware")]
+public class MockRoutingMidpoint : EssentialsDevice, IRoutingMidpointWithFeedback
+{
+ ///
+ /// The configuration properties for this device.
+ ///
+ public MockRoutingMidpointPropertiesConfig PropertiesConfig { get; private set; }
+
+ ///
+ public RoutingPortCollection InputPorts { get; private set; }
+
+ ///
+ public RoutingPortCollection OutputPorts { get; private set; }
+
+ ///
+ public List CurrentRoutes { get; } = new List();
+
+ ///
+ public event RouteChangedEventHandler RouteChanged;
+
+ ///
+ /// Initializes a new instance of the class from a .
+ ///
+ /// The device configuration, whose Properties are deserialized as .
+ public MockRoutingMidpoint(DeviceConfig config)
+ : base(config.Key, config.Name)
+ {
+ PropertiesConfig = config.Properties != null
+ ? JsonConvert.DeserializeObject(config.Properties.ToString())
+ : null;
+ PropertiesConfig ??= new MockRoutingMidpointPropertiesConfig();
+
+ InputPorts = new RoutingPortCollection();
+ OutputPorts = new RoutingPortCollection();
+
+ BuildPorts();
+ }
+
+ ///
+ /// Builds the input and output ports from . Each port's Key and Selector
+ /// are both set to the configured port name, so selectors passed to and
+ /// can simply be looked up by matching against the port's Selector.
+ ///
+ private void BuildPorts()
+ {
+ try
+ {
+ foreach (var portConfig in PropertiesConfig.InputPorts)
+ {
+ if (string.IsNullOrEmpty(portConfig.Name))
+ {
+ this.LogWarning("Skipping input port with no name configured for {key}", Key);
+ continue;
+ }
+
+ var port = new RoutingInputPort(portConfig.Name, portConfig.SignalType, portConfig.PortType, portConfig.Name, this);
+ InputPorts.Add(port);
+ }
+
+ foreach (var portConfig in PropertiesConfig.OutputPorts)
+ {
+ if (string.IsNullOrEmpty(portConfig.Name))
+ {
+ this.LogWarning("Skipping output port with no name configured for {key}", Key);
+ continue;
+ }
+
+ var port = new RoutingOutputPort(portConfig.Name, portConfig.SignalType, portConfig.PortType, portConfig.Name, this);
+ OutputPorts.Add(port);
+ }
+
+ this.LogInformation("Built {inputCount} input port(s) and {outputCount} output port(s) for mock midpoint {key}",
+ InputPorts.Count, OutputPorts.Count, Key);
+ }
+ catch (Exception ex)
+ {
+ this.LogException(ex, "Error building ports for mock midpoint {0}", Key);
+ }
+ }
+
+ ///
+ public void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType)
+ {
+ try
+ {
+ var outputPort = OutputPorts.FirstOrDefault(p => Equals(p.Selector, outputSelector));
+
+ if (outputPort == null)
+ {
+ this.LogWarning("Unable to find output port for selector {selector} on {key}", outputSelector, Key);
+ return;
+ }
+
+ // Remove any existing route to this output before making or clearing the new one.
+ var existingRoute = CurrentRoutes.FirstOrDefault(r => r.OutputPort?.Key == outputPort.Key);
+ if (existingRoute != null)
+ {
+ CurrentRoutes.Remove(existingRoute);
+ }
+
+ if (inputSelector == null)
+ {
+ this.LogInformation("Clearing route to output {output} on {key}", outputPort.Key, Key);
+
+ var clearedDescriptor = new RouteSwitchDescriptor(outputPort, null);
+ RouteChanged?.Invoke(this, clearedDescriptor);
+ return;
+ }
+
+ var inputPort = InputPorts.FirstOrDefault(p => Equals(p.Selector, inputSelector));
+
+ if (inputPort == null)
+ {
+ this.LogWarning("Unable to find input port for selector {selector} on {key}", inputSelector, Key);
+ return;
+ }
+
+ var descriptor = new RouteSwitchDescriptor(outputPort, inputPort);
+ CurrentRoutes.Add(descriptor);
+
+ this.LogInformation("Executed switch: {input} -> {output} ({signalType}) on {key}",
+ inputPort.Key, outputPort.Key, signalType, Key);
+
+ RouteChanged?.Invoke(this, descriptor);
+ }
+ catch (Exception ex)
+ {
+ this.LogException(ex, "Error executing switch on mock midpoint {0}", Key);
+ }
+ }
+
+ ///
+ public void ClearRoute(object outputSelector, eRoutingSignalType signalType)
+ {
+ ExecuteSwitch(null, outputSelector, signalType);
+ }
+}
+
+///
+/// Factory for building devices.
+///
+public class MockRoutingMidpointFactory : EssentialsDeviceFactory
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MockRoutingMidpointFactory()
+ {
+ TypeNames = new List { "mockroutingmidpoint", "mockmidpoint" };
+ }
+
+ ///
+ public override EssentialsDevice BuildDevice(DeviceConfig dc)
+ {
+ Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new MockRoutingMidpoint Device");
+ return new MockRoutingMidpoint(dc);
+ }
+}
diff --git a/src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpointPropertiesConfig.cs b/src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpointPropertiesConfig.cs
new file mode 100644
index 00000000..7c7d4bc3
--- /dev/null
+++ b/src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpointPropertiesConfig.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using PepperDash.Essentials.Core;
+
+namespace PepperDash.Essentials.Devices.Common.Routing;
+
+///
+/// Configuration properties for a device.
+///
+public class MockRoutingMidpointPropertiesConfig
+{
+ ///
+ /// The input ports to build on the mock midpoint.
+ ///
+ [JsonProperty("inputPorts")]
+ public List InputPorts { get; set; } = new List();
+
+ ///
+ /// The output ports to build on the mock midpoint.
+ ///
+ [JsonProperty("outputPorts")]
+ public List OutputPorts { get; set; } = new List();
+}
+
+///
+/// Configuration for a single input or output port on a device.
+///
+public class MockRoutingMidpointPortConfig
+{
+ ///
+ /// The name of the port. Used as both the port's key and its selector value, so it must be unique
+ /// within the collection (input ports or output ports) it is configured under.
+ ///
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ ///
+ /// The routing signal type supported by this port (e.g. "Audio", "Video", "AudioVideo", "Usb").
+ /// Defaults to AudioVideo if not specified.
+ ///
+ [JsonProperty("signalType")]
+ [JsonConverter(typeof(StringEnumConverter))]
+ public eRoutingSignalType SignalType { get; set; } = eRoutingSignalType.AudioVideo;
+
+ ///
+ /// The physical connection type of this port (e.g. "Hdmi", "Dm", "DisplayPort"). Defaults to Hdmi if
+ /// not specified.
+ ///
+ [JsonProperty("portType")]
+ [JsonConverter(typeof(StringEnumConverter))]
+ public eRoutingPortConnectionType PortType { get; set; } = eRoutingPortConnectionType.Hdmi;
+}