mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-08-31 10:58:28 +00:00
feat: refactor DeviceJsonApi parameter conversion and add MockRoutingMidpoint with configuration properties
This commit is contained in:
parent
9f18275f81
commit
d771320fff
4 changed files with 286 additions and 7 deletions
|
|
@ -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
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the ordered, converted argument array to pass to <see cref="MethodInfo.Invoke(object, object[])"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Handles the common positional case (one supplied param per method parameter), as well as a method
|
||||
/// with a single array-typed parameter (e.g. <c>void Foo(string[] items)</c>). For the latter, config
|
||||
/// authors may either supply the params as a flat list matching the array's contents
|
||||
/// (<c>"params": ["a", "b", "c"]</c>) or as a single nested array (<c>"params": [["a", "b", "c"]]</c>) -
|
||||
/// both are converted into the single array argument correctly.
|
||||
/// </remarks>
|
||||
/// <param name="mParams">The target method's parameters.</param>
|
||||
/// <param name="actionParams">The raw parameter values supplied in the action.</param>
|
||||
/// <returns>The converted arguments, in order, ready to pass to Invoke.</returns>
|
||||
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<object>()
|
||||
.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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A mock midpoint routing device (e.g. a matrix switcher) that implements <see cref="IRoutingMidpointWithFeedback"/>
|
||||
/// without any real hardware communication. Its input and output ports are configured via
|
||||
/// <see cref="MockRoutingMidpointPropertiesConfig"/>, 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.
|
||||
/// </summary>
|
||||
[Description("A mock routing midpoint (e.g. matrix switcher) device for testing routing logic without real hardware")]
|
||||
public class MockRoutingMidpoint : EssentialsDevice, IRoutingMidpointWithFeedback
|
||||
{
|
||||
/// <summary>
|
||||
/// The configuration properties for this device.
|
||||
/// </summary>
|
||||
public MockRoutingMidpointPropertiesConfig PropertiesConfig { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public RoutingPortCollection<RoutingInputPort> InputPorts { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public RoutingPortCollection<RoutingOutputPort> OutputPorts { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<RouteSwitchDescriptor> CurrentRoutes { get; } = new List<RouteSwitchDescriptor>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public event RouteChangedEventHandler RouteChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MockRoutingMidpoint"/> class from a <see cref="DeviceConfig"/>.
|
||||
/// </summary>
|
||||
/// <param name="config">The device configuration, whose Properties are deserialized as <see cref="MockRoutingMidpointPropertiesConfig"/>.</param>
|
||||
public MockRoutingMidpoint(DeviceConfig config)
|
||||
: base(config.Key, config.Name)
|
||||
{
|
||||
PropertiesConfig = config.Properties != null
|
||||
? JsonConvert.DeserializeObject<MockRoutingMidpointPropertiesConfig>(config.Properties.ToString())
|
||||
: null;
|
||||
PropertiesConfig ??= new MockRoutingMidpointPropertiesConfig();
|
||||
|
||||
InputPorts = new RoutingPortCollection<RoutingInputPort>();
|
||||
OutputPorts = new RoutingPortCollection<RoutingOutputPort>();
|
||||
|
||||
BuildPorts();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the input and output ports from <see cref="PropertiesConfig"/>. Each port's Key and Selector
|
||||
/// are both set to the configured port name, so selectors passed to <see cref="ExecuteSwitch"/> and
|
||||
/// <see cref="ClearRoute"/> can simply be looked up by matching against the port's Selector.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearRoute(object outputSelector, eRoutingSignalType signalType)
|
||||
{
|
||||
ExecuteSwitch(null, outputSelector, signalType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory for building <see cref="MockRoutingMidpoint"/> devices.
|
||||
/// </summary>
|
||||
public class MockRoutingMidpointFactory : EssentialsDeviceFactory<MockRoutingMidpoint>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MockRoutingMidpointFactory"/> class.
|
||||
/// </summary>
|
||||
public MockRoutingMidpointFactory()
|
||||
{
|
||||
TypeNames = new List<string> { "mockroutingmidpoint", "mockmidpoint" };
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override EssentialsDevice BuildDevice(DeviceConfig dc)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new MockRoutingMidpoint Device");
|
||||
return new MockRoutingMidpoint(dc);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration properties for a <see cref="MockRoutingMidpoint"/> device.
|
||||
/// </summary>
|
||||
public class MockRoutingMidpointPropertiesConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// The input ports to build on the mock midpoint.
|
||||
/// </summary>
|
||||
[JsonProperty("inputPorts")]
|
||||
public List<MockRoutingMidpointPortConfig> InputPorts { get; set; } = new List<MockRoutingMidpointPortConfig>();
|
||||
|
||||
/// <summary>
|
||||
/// The output ports to build on the mock midpoint.
|
||||
/// </summary>
|
||||
[JsonProperty("outputPorts")]
|
||||
public List<MockRoutingMidpointPortConfig> OutputPorts { get; set; } = new List<MockRoutingMidpointPortConfig>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for a single input or output port on a <see cref="MockRoutingMidpoint"/> device.
|
||||
/// </summary>
|
||||
public class MockRoutingMidpointPortConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The routing signal type supported by this port (e.g. "Audio", "Video", "AudioVideo", "Usb").
|
||||
/// Defaults to AudioVideo if not specified.
|
||||
/// </summary>
|
||||
[JsonProperty("signalType")]
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public eRoutingSignalType SignalType { get; set; } = eRoutingSignalType.AudioVideo;
|
||||
|
||||
/// <summary>
|
||||
/// The physical connection type of this port (e.g. "Hdmi", "Dm", "DisplayPort"). Defaults to Hdmi if
|
||||
/// not specified.
|
||||
/// </summary>
|
||||
[JsonProperty("portType")]
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public eRoutingPortConnectionType PortType { get; set; } = eRoutingPortConnectionType.Hdmi;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue