Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions src/PepperDash.Essentials.Core/Routing/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -710,13 +710,30 @@ private static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOu
// it's a sink device
routeTable.Routes.Add(new RouteSwitchDescriptor(goodInputPort));
}
else if (destination is IRoutingMidpointWithFeedback)
else if (destination is IRoutingMidpointWithFeedback midpoint)
{
routeTable.Routes.Add(new RouteSwitchDescriptor(outputPortToUse, goodInputPort));
// outputPortToUse is only null when the midpoint is the route's terminal destination, so no
// downstream device asked for a particular output. Keep the hop so the midpoint still selects
// its input, and use its output only when that choice is unambiguous.
var outputPort = outputPortToUse ?? GetOnlyOutputPortForSignalType(midpoint, signalType);

if (outputPort == null)
{
Debug.LogMessage(LogEventLevel.Debug, "Route terminates on midpoint {midpointKey} with no single output for {signalType}; switching input {inputPortKey} with no output selector", destination, destination.Key, signalType, goodInputPort.Key);
}

routeTable.Routes.Add(new RouteSwitchDescriptor(outputPort, goodInputPort));
}
else // device is merely IRoutingInputOutputs
Debug.LogMessage(LogEventLevel.Verbose, "No routing. Passthrough device", destination);

return true;
}

private static RoutingOutputPort GetOnlyOutputPortForSignalType(IRoutingOutputs device, eRoutingSignalType signalType)
{
var candidates = device.OutputPorts?.Where(p => p.Type.HasFlag(signalType)).Take(2).ToList();

return candidates?.Count == 1 ? candidates[0] : null;
}
}
21 changes: 15 additions & 6 deletions src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,14 @@ public void ExecuteRoutes()

if (route.SwitchingDevice is IRoutingMidpointWithFeedback switchingDevice)
{
switchingDevice.ExecuteSwitch(route.InputPort.Selector, route.OutputPort.Selector, SignalType);
// OutputPort is null when the route terminates on this midpoint and it has no single
// output for the signal type; the device decides what a null output selector means.
switchingDevice.ExecuteSwitch(route.InputPort?.Selector, route.OutputPort?.Selector, SignalType);

if (route.OutputPort?.InUseTracker == null)
{
continue;
}

route.OutputPort.InUseTracker.AddUser(Destination, "destination-" + SignalType);

Expand All @@ -121,6 +128,13 @@ public void ReleaseRoutes(bool clearRoute = false)
{
if (route.SwitchingDevice is IRoutingMidpointWithFeedback switchingDevice)
{
// With no output port (a terminal midpoint with no single output), there is nothing to
// clear: a null input and null output selector would carry no meaning for the device.
if (route.OutputPort == null)
{
continue;
}

if (clearRoute)
{
try
Expand All @@ -134,11 +148,6 @@ public void ReleaseRoutes(bool clearRoute = false)
}
}

if (route.OutputPort == null)
{
continue;
}

if (route.OutputPort.InUseTracker != null)
{
route.OutputPort.InUseTracker.RemoveUser(Destination, "destination-" + SignalType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ public class RouteSwitchDescriptor
/// </summary>
public RoutingInputPort InputPort { get; set; }

/// <summary>
/// The signal type this switch carries, for devices that route signal types independently.
/// Null means the switch carries whatever the input port supports.
/// </summary>
public eRoutingSignalType? SignalType { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="RouteSwitchDescriptor"/> class for sink devices (no output port).
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ internal string GetSnapshotMessage()
{
InputPortKey = r.InputPort.Key,
OutputPortKey = r.OutputPort?.Key,
SignalType = r.InputPort.Type.ToString()
SignalType = (r.SignalType ?? r.InputPort.Type).ToString()
})
.ToList();
}
Expand Down Expand Up @@ -330,7 +330,7 @@ private void HandleMidpointRouteChanged(IRoutingMidpointWithFeedback midpoint, R
{
InputPortKey = r.InputPort.Key,
OutputPortKey = r.OutputPort?.Key,
SignalType = r.InputPort.Type.ToString()
SignalType = (r.SignalType ?? r.InputPort.Type).ToString()
})
.ToList() ?? new List<MidpointRouteDto>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ namespace PepperDash.Essentials.Devices.Common;
/// with ramping volume up/down while pressed and mute on/off/toggle, all backed by fake in-memory
/// state rather than any real hardware communication.
/// </summary>
public class MockAudioDevice : EssentialsDevice, IBasicVolumeWithFeedback
public class MockAudioDevice : EssentialsDevice, IBasicVolumeWithFeedback, ICommunicationMonitor
{
/// <inheritdoc />
/// <remarks>Always online: there's no real connection behind this mock to lose.</remarks>
public StatusMonitorBase CommunicationMonitor { get; }

private const int VolumeHeldRepeatIntervalMs = 100;
private const ushort VolumeStep = 655;

Expand All @@ -38,6 +42,8 @@ public class MockAudioDevice : EssentialsDevice, IBasicVolumeWithFeedback
public MockAudioDevice(string key, string name)
: base(key, name)
{
CommunicationMonitor = new MockCommunicationMonitor(this);

VolumeLevelFeedback = new IntFeedback("volume", () => _volumeLevel);
MuteFeedback = new BoolFeedback("muteOn", () => _isMuted);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ namespace PepperDash.Essentials.Devices.Common.Displays;
/// <summary>
/// Represents a mock display device for testing and simulation purposes.
/// </summary>
public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeAdvanced, IHasInputs<string>, IHasPowerControlWithFeedback
public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeAdvanced, IHasInputs<string>, IHasPowerControlWithFeedback, ICommunicationMonitor
{
/// <inheritdoc />
/// <remarks>Always online: there's no real connection behind this mock to lose.</remarks>
public StatusMonitorBase CommunicationMonitor { get; }

/// <inheritdoc />
public ISelectableItems<string> Inputs { get; private set; }

Expand Down Expand Up @@ -77,6 +81,8 @@ protected override Func<bool> IsWarmingUpFeedbackFunc
public MockDisplay(string key, string name)
: base(key, name)
{
CommunicationMonitor = new MockCommunicationMonitor(this);

Inputs = new MockDisplayInputs
{
Items = new Dictionary<string, ISelectableItem>
Expand Down
26 changes: 22 additions & 4 deletions src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using PepperDash.Core;
using PepperDash.Core.Logging;
using PepperDash.Essentials.Core;
Expand Down Expand Up @@ -32,8 +33,9 @@ public GenericSink(string key, string name) : base(key, name)
{
InputPorts = new RoutingPortCollection<RoutingInputPort>();

var inputPort = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, null, this);
var audioInputPort = new RoutingInputPort(RoutingPortNames.AnyAudioIn, eRoutingSignalType.Audio, eRoutingPortConnectionType.LineAudio, null, this);
// Each port's selector is its own key, so ExecuteSwitch can tell which one was switched to.
var inputPort = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, RoutingPortNames.AnyVideoIn, this);
var audioInputPort = new RoutingInputPort(RoutingPortNames.AnyAudioIn, eRoutingSignalType.Audio, eRoutingPortConnectionType.LineAudio, RoutingPortNames.AnyAudioIn, this);

InputPorts.Add(inputPort);
InputPorts.Add(audioInputPort);
Expand Down Expand Up @@ -111,10 +113,12 @@ private void UpdateCurrentSources(eRoutingSignalType signalType, IRoutingSource

private SourceListItem _currentSource;

private RoutingInputPort _currentInputPort;

/// <summary>
/// Gets the current input port
/// Gets the input port last switched to, or the first input port before any switch
/// </summary>
public RoutingInputPort CurrentInputPort => InputPorts[0];
public RoutingInputPort CurrentInputPort => _currentInputPort ?? InputPorts[0];

/// <inheritdoc />
public event InputChangedEventHandler InputChanged;
Expand All @@ -123,6 +127,20 @@ private void UpdateCurrentSources(eRoutingSignalType signalType, IRoutingSource
public void ExecuteSwitch(object inputSelector)
{
this.LogDebug("GenericSink Executing Switch to: {inputSelector}", inputSelector);

var port = InputPorts.FirstOrDefault(p => Equals(p.Selector, inputSelector));

if (port == null)
{
this.LogWarning("GenericSink has no input port with selector {inputSelector}", inputSelector);
return;
}

if (_currentInputPort == port) return;

_currentInputPort = port;

InputChanged?.Invoke(this, _currentInputPort);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
using System.Collections.Generic;
using System.Linq;
using Crestron.SimplSharpPro.DeviceSupport;
using Newtonsoft.Json;
using PepperDash.Core.Logging;
using PepperDash.Essentials.Core;
using PepperDash.Essentials.Core.Bridges;
using PepperDash.Essentials.Core.Config;
using PepperDash.Essentials.Core.Lighting;

namespace PepperDash.Essentials.Devices.Common.Lighting;

/// <summary>
/// Scenes to simulate for a <see cref="MockLightingDevice"/>.
/// </summary>
public class MockLightingConfig
{
/// <summary>
/// Scenes to simulate, in display order. Defaults to a generic 3-scene set (On/Dim/Off) when
/// omitted so the device works with no config at all.
/// </summary>
[JsonProperty("scenes", NullValueHandling = NullValueHandling.Ignore)]
public List<MockLightingSceneConfig> Scenes { get; set; }

/// <summary>
/// ID of the scene active on startup. Falls back to the first configured scene when unset
/// or when it does not match any configured scene.
/// </summary>
[JsonProperty("defaultSceneId", NullValueHandling = NullValueHandling.Ignore)]
public string DefaultSceneId { get; set; }
}

/// <summary>
/// One configured scene.
/// </summary>
public class MockLightingSceneConfig
{
/// <summary>
/// Scene ID, referenced by the client's /selectScene request
/// </summary>
[JsonProperty("id")]
public string Id { get; set; }

/// <summary>
/// Display name for the scene
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
}

/// <summary>
/// Simulated lighting scene controller. No hardware behind it - selecting a scene just updates
/// <see cref="LightingBase.CurrentLightingScene"/> and fires feedback, exactly like a real DSP
/// or lighting processor would once the scene recall is acknowledged.
/// </summary>
public class MockLightingDevice : LightingBase, ICommunicationMonitor
{
/// <inheritdoc />
/// <remarks>Always online: there's no real connection behind this mock to lose.</remarks>
public StatusMonitorBase CommunicationMonitor { get; }

private static readonly List<MockLightingSceneConfig> DefaultScenes = new List<MockLightingSceneConfig>
{
new MockLightingSceneConfig { Id = "on", Name = "On" },
new MockLightingSceneConfig { Id = "dim", Name = "Dim" },
new MockLightingSceneConfig { Id = "off", Name = "Off" },
};

/// <summary>
/// Initializes a new instance of the MockLightingDevice class
/// </summary>
/// <param name="key">The device key</param>
/// <param name="name">The device name</param>
/// <param name="config">Scenes to simulate; null falls back to a generic On/Dim/Off set</param>
public MockLightingDevice(string key, string name, MockLightingConfig config)
: base(key, name)
{
CommunicationMonitor = new MockCommunicationMonitor(this);

var scenesConfig = config?.Scenes != null && config.Scenes.Count > 0
? config.Scenes
: DefaultScenes;

LightingScenes = scenesConfig
.Select(s => new LightingScene { ID = s.Id, Name = s.Name })
.ToList();

CurrentLightingScene = LightingScenes.FirstOrDefault(s => s.ID == config?.DefaultSceneId)
?? LightingScenes.FirstOrDefault()
?? new LightingScene();

CurrentLightingSceneFeedback = new IntFeedback("currentLightingScene",
() => LightingScenes.IndexOf(CurrentLightingScene));
CurrentLightingSceneFeedback.FireUpdate();

OnLightingSceneChange();
Comment thread
Copilot marked this conversation as resolved.
}

/// <inheritdoc />
public override void SelectScene(LightingScene scene)
{
var match = LightingScenes.FirstOrDefault(s => s.ID == scene?.ID);

if (match == null)
{
this.LogWarning("No lighting scene with id '{sceneId}'", scene?.ID);
return;
}

this.LogInformation("Selecting lighting scene '{sceneId}'", match.ID);

CurrentLightingScene = match;
CurrentLightingSceneFeedback.FireUpdate();
OnLightingSceneChange();
}

/// <inheritdoc />
public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) =>
LinkLightingToApi(this, trilist, joinStart, joinMapKey, bridge);
}

/// <summary>
/// Builds a <see cref="MockLightingDevice"/> for any config entry typed "mockLightingDevice".
/// </summary>
public class MockLightingDeviceFactory : EssentialsDeviceFactory<MockLightingDevice>
{
/// <summary>
/// Initializes a new instance of the MockLightingDeviceFactory class
/// </summary>
public MockLightingDeviceFactory()
{
TypeNames = new List<string> { "mockLightingDevice" };
}

/// <inheritdoc />
public override EssentialsDevice BuildDevice(DeviceConfig dc)
{
var config = dc.Properties?.ToObject<MockLightingConfig>();

return new MockLightingDevice(dc.Key, dc.Name, config);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using PepperDash.Core;
using PepperDash.Essentials.Core;

namespace PepperDash.Essentials.Devices.Common;

/// <summary>
/// A <see cref="StatusMonitorBase"/> for mock devices, whose status is set directly rather than
/// derived from polling a real connection. Reports <see cref="MonitorStatus.IsOk"/> until told otherwise.
/// </summary>
public class MockCommunicationMonitor : StatusMonitorBase
{
/// <summary>
/// Initializes a new instance of the <see cref="MockCommunicationMonitor"/> class, reporting online.
/// </summary>
public MockCommunicationMonitor(IKeyed parent) : base(parent, 30000, 60000)
{
Status = MonitorStatus.IsOk;
}

/// <inheritdoc />
public override void Start() { }

/// <inheritdoc />
public override void Stop() { }

/// <summary>
/// Sets the monitor's current status.
/// </summary>
public void SetStatus(MonitorStatus status) => Status = status;
}
Loading
Loading