Compare commits

...

56 Commits

Author SHA1 Message Date
Erik Meyer
a7c4e2fd60 feat: complete XML documentation in PD Core 2026-01-27 13:22:27 -05:00
Neil Dorin
4fa7a42330 Merge pull request #1376 from PepperDash/client-id-issues
fix: handle subsequent join calls and clientid/websocket client mismatches
2026-01-21 14:59:22 -07:00
Andrew Welker
9bad3ae21b fix: handle subsequent join calls and clientid/websocket client mismatches 2026-01-21 15:20:27 -06:00
Nick Genovese
3fb30d5561 Merge pull request #1373 from PepperDash/matrix-routing-isonline
Multiple fixes
2025-12-31 15:19:31 -05:00
Andrew Welker
57cd77f019 fix: implement IKeyName for DspControlPoint 2025-12-31 14:04:04 -06:00
Andrew Welker
7f2bb078c8 fix: revert prop name to inUpPosition for screenlift messenger
- refactor volume interfaces into separate files
- IBasicVolumeControl implements IKeyName
2025-12-31 12:20:40 -06:00
Andrew Welker
316bb849b4 fix: update matrix routing inputs if endpoint online status changes 2025-12-30 16:58:36 -06:00
Andrew Welker
a983e2c87f fix: save config only when values change 2025-12-30 14:34:11 -06:00
Nick Genovese
babb9a77df fix: a few logging updates 2025-12-29 16:48:13 -06:00
Nick Genovese
7629921113 fix: a few logging updates 2025-12-29 15:34:14 -06:00
Andrew Welker
3878c85a7a fix: use correct property name for isInUpPosition 2025-12-29 13:36:47 -06:00
Andrew Welker
7ad8218af0 fix: fusion controller now sets only associated room custom values 2025-12-29 13:03:47 -06:00
Andrew Welker
0c4aec14d1 fix: use .NET timers instead of CTimer 2025-12-29 11:53:52 -06:00
Andrew Welker
7d3f871460 Merge branch 'copilot/featureadd-raise-lower-time' into mc-connection-issues 2025-12-29 09:03:09 -06:00
Andrew Welker
78c9381108 fix: add clientId as qp for websocket for MC 2025-12-29 08:57:52 -06:00
Erik Meyer
a7ff2e8903 fix: move isInUpPosition to momvement complete method, remove conditionlal logic on raise/lower commands 2025-12-27 17:09:56 -06:00
copilot-swe-agent[bot]
ae0b2fe086 Refactor timer disposal and improve code readability
Co-authored-by: erikdred <88980320+erikdred@users.noreply.github.com>
2025-12-27 20:17:59 +00:00
copilot-swe-agent[bot]
bd11c827da Split movement time into separate raise/lower times and remove timing from latched mode
Co-authored-by: erikdred <88980320+erikdred@users.noreply.github.com>
2025-12-27 20:14:39 +00:00
copilot-swe-agent[bot]
7ea1efbabf Add raise/lower movement time configuration and banked command support
Co-authored-by: erikdred <88980320+erikdred@users.noreply.github.com>
2025-12-27 20:09:31 +00:00
copilot-swe-agent[bot]
9d6aaa2a0e Initial plan 2025-12-27 20:02:52 +00:00
Andrew Welker
53e7a30224 fix: handle threading issues for concurrent clients joining 2025-12-26 12:34:31 -06:00
Neil Dorin
39c1f60a4d Merge pull request #1370 from PepperDash/udp-eisc 2025-12-23 12:10:37 -05:00
Andrew Welker
2cc37a4e40 fix: ExecuteJoinAction now uses the correct Sig collections 2025-12-23 11:06:52 -06:00
Andrew Welker
076e5dfa9d chore: update comments, debug methods, and mark some things obsolete 2025-12-23 10:25:53 -06:00
Andrew Welker
092896bb25 fix: support for legacy UDP EISC 2025-12-23 09:57:49 -06:00
Neil Dorin
7c8f0586e6 Merge pull request #1369 from PepperDash/volume-messenger-issue 2025-12-16 17:15:46 -05:00
Andrew Welker
c5b0872a4c fix: DeviceVolumeMessenger only sends rawValue when device implements it 2025-12-16 15:41:33 -06:00
Andrew Welker
a7b88ec38d Merge pull request #1368 from PepperDash/generic-sink
generic sink
2025-12-16 11:09:40 -05:00
Andrew Welker
210b398a13 fix: implement PR Review suggestions 2025-12-16 10:04:01 -06:00
Nick Genovese
bce1e3610e fix: copy dictionaries
- fixed multiple enumeration exception
2025-12-10 16:58:41 -06:00
Andrew Welker
6a33e7c99d fix: initialize current sources dictionaries 2025-12-05 16:26:08 -06:00
Andrew Welker
2048e3f65d fix: GenericSink implements ICurrentSources 2025-12-05 16:26:02 -06:00
Neil Dorin
81df2738de Merge pull request #1363 from PepperDash/feature/add-on-off-dsp-preset-keys-to-room-config
feat: Add on/off dsp keys to EssentialsAvRoomPropertiesConfig
2025-11-26 18:00:18 -05:00
Neil Dorin
08fbec416f Update src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomConfig.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 15:53:45 -07:00
Neil Dorin
7594b22096 Update src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomConfig.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 15:53:39 -07:00
Neil Dorin
d1babf6b9b Update src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomConfig.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 15:53:31 -07:00
Neil Dorin
2187c9fb0d Update src/PepperDash.Essentials.Core/Devices/SourceListItem.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 15:53:22 -07:00
Neil Dorin
a5e6059160 Update src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomConfig.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 15:53:14 -07:00
Neil Dorin
9ef4aedcce Update src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomConfig.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 15:53:07 -07:00
Neil Dorin
f7c7160bf0 feat: Add on/off dsp keys to EssentialsAvRoomPropertiesConfig
clean up XML comments and improve property definitions in EssentialsRoomConfig
2025-11-26 15:48:14 -07:00
Neil Dorin
dbf5740841 Merge pull request #1362 from PepperDash/feature/add-IHasFeedback-to-IEssentialsRoomFusionController
fix: ensure proper disposal of help request timeout timer and improve…
2025-11-26 13:28:55 -05:00
Neil Dorin
c07e099a79 feat: add logging for help request timeout events in IEssentialsRoomFusionController 2025-11-26 11:10:21 -07:00
Neil Dorin
06cb508f3a Update src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 11:09:07 -07:00
Neil Dorin
e93b5b34cc Update src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 11:09:02 -07:00
Neil Dorin
4f5d4ef87a fix: ensure proper disposal of help request timeout timer and improve logging 2025-11-26 11:02:31 -07:00
Andrew Welker
636da8cc8c Merge pull request #1361 from PepperDash/feature/add-IHasFeedback-to-IEssentialsRoomFusionController
feat: add help request timeout functionality to IEssentialsRoomFusion…
2025-11-26 12:34:48 -05:00
Neil Dorin
5de1e2d7bb feat: add help request timeout functionality to IEssentialsRoomFusionController 2025-11-26 10:26:41 -07:00
Andrew Welker
03bbb84894 Merge pull request #1359 from PepperDash/feature/add-IHasFeedback-to-IEssentialsRoomFusionController
feat: implement IHasFeedback interface in IEssentialsRoomFusionContro…
2025-11-25 13:37:31 -05:00
Neil Dorin
d17394cdd7 feat: add logging to ExecuteSwitch method in GenericSink 2025-11-25 11:06:48 -07:00
Neil Dorin
8467afde38 Update src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-24 17:23:38 -07:00
Neil Dorin
5c016fb4b8 feat: implement IHasFeedback interface in IEssentialsRoomFusionController 2025-11-24 17:14:34 -07:00
Neil Dorin
2fbc32947c Merge pull request #1357 from PepperDash/url-parsing
fix: check for multiple URL patterns for both template & system URLS
2025-11-18 14:14:16 -05:00
Andrew Welker
c06b57a5f9 fix: check for multiple URL patterns for both template & system URLS 2025-11-18 12:30:54 -06:00
Neil Dorin
6d64fffc50 Merge pull request #1356 from PepperDash/mc-subscription-logging
Multiple Fixes
2025-11-14 15:44:58 -05:00
Andrew Welker
0c4ebdaf1d fix: change how subscription state is logged to reduce traffic to console 2025-11-13 09:56:29 -06:00
Andrew Welker
2c49fb9321 fix: parse current Portal URLS for system and template UUIDs correctly 2025-11-12 16:58:23 -06:00
48 changed files with 6632 additions and 554 deletions

13
.config/dotnet-tools.json Normal file
View File

@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.2.4",
"commands": [
"csharpier"
],
"rollForward": false
}
}
}

5243
buildFile.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -15,23 +15,23 @@ namespace PepperDash.Core
/// Unique Key /// Unique Key
/// </summary> /// </summary>
public string Key { get; protected set; } public string Key { get; protected set; }
/// <summary> /// <summary>
/// Gets or sets the Name /// Gets or sets the Name
/// </summary> /// </summary>
public string Name { get; protected set; } public string Name { get; protected set; }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public bool Enabled { get; protected set; } public bool Enabled { get; protected set; }
/// <summary> // /// <summary>
/// A place to store reference to the original config object, if any. These values should // /// A place to store reference to the original config object, if any. These values should
/// NOT be used as properties on the device as they are all publicly-settable values. // /// NOT be used as properties on the device as they are all publicly-settable values.
/// </summary> // /// </summary>
//public DeviceConfig Config { get; private set; } //public DeviceConfig Config { get; private set; }
/// <summary> // /// <summary>
/// Helper method to check if Config exists // /// Helper method to check if Config exists
/// </summary> // /// </summary>
//public bool HasConfig { get { return Config != null; } } //public bool HasConfig { get { return Config != null; } }
List<Action> _PreActivationActions; List<Action> _PreActivationActions;

View File

@@ -41,6 +41,9 @@ namespace PepperDash.Core
CrestronConsole.PrintLine(message); CrestronConsole.PrintLine(message);
} }
/// <summary>
/// Constructor for DebugConsoleSink
/// </summary>
public DebugConsoleSink(ITextFormatter formatProvider ) public DebugConsoleSink(ITextFormatter formatProvider )
{ {
_textFormatter = formatProvider ?? new JsonFormatter(); _textFormatter = formatProvider ?? new JsonFormatter();
@@ -48,6 +51,9 @@ namespace PepperDash.Core
} }
/// <summary>
/// Provides extension methods for DebugConsoleSink
/// </summary>
public static class DebugConsoleSinkExtensions public static class DebugConsoleSinkExtensions
{ {
/// <summary> /// <summary>

View File

@@ -27,6 +27,9 @@ namespace PepperDash.Core.Logging
CrestronLogger.WriteToLog(message, (uint)logEvent.Level); CrestronLogger.WriteToLog(message, (uint)logEvent.Level);
} }
/// <summary>
/// Constructor for DebugCrestronLoggerSink
/// </summary>
public DebugCrestronLoggerSink() public DebugCrestronLoggerSink()
{ {
CrestronLogger.Initialize(1, LoggerModeEnum.RM); CrestronLogger.Initialize(1, LoggerModeEnum.RM);

View File

@@ -63,6 +63,9 @@ namespace PepperDash.Core.Logging
handler(message); handler(message);
} }
/// <summary>
/// Constructor for DebugErrorLogSink
/// </summary>
public DebugErrorLogSink(ITextFormatter formatter = null) public DebugErrorLogSink(ITextFormatter formatter = null)
{ {
_formatter = formatter; _formatter = formatter;

View File

@@ -4,6 +4,9 @@ using Log = PepperDash.Core.Debug;
namespace PepperDash.Core.Logging namespace PepperDash.Core.Logging
{ {
/// <summary>
/// Provides extension methods for logging on IKeyed objects
/// </summary>
public static class DebugExtensions public static class DebugExtensions
{ {
/// <summary> /// <summary>

View File

@@ -32,6 +32,9 @@ namespace PepperDash.Core
private const string _certificateName = "selfCres"; private const string _certificateName = "selfCres";
private const string _certificatePassword = "cres12345"; private const string _certificatePassword = "cres12345";
/// <summary>
/// Gets the Port
/// </summary>
public int Port public int Port
{ get { get
{ {
@@ -41,6 +44,9 @@ namespace PepperDash.Core
} }
} }
/// <summary>
/// Gets the Url
/// </summary>
public string Url public string Url
{ {
get get
@@ -58,6 +64,9 @@ namespace PepperDash.Core
private readonly ITextFormatter _textFormatter; private readonly ITextFormatter _textFormatter;
/// <summary>
/// Constructor for DebugWebsocketSink
/// </summary>
public DebugWebsocketSink(ITextFormatter formatProvider) public DebugWebsocketSink(ITextFormatter formatProvider)
{ {
@@ -217,6 +226,9 @@ namespace PepperDash.Core
} }
} }
/// <summary>
/// Provides extension methods for DebugWebsocketSink
/// </summary>
public static class DebugWebsocketSinkExtensions public static class DebugWebsocketSinkExtensions
{ {
/// <summary> /// <summary>
@@ -237,6 +249,9 @@ namespace PepperDash.Core
{ {
private DateTime _connectionTime; private DateTime _connectionTime;
/// <summary>
/// Gets the ConnectedDuration
/// </summary>
public TimeSpan ConnectedDuration public TimeSpan ConnectedDuration
{ {
get get
@@ -252,11 +267,17 @@ namespace PepperDash.Core
} }
} }
/// <summary>
/// Constructor for DebugClient
/// </summary>
public DebugClient() public DebugClient()
{ {
Debug.Console(0, "DebugClient Created"); Debug.Console(0, "DebugClient Created");
} }
/// <summary>
/// OnOpen method
/// </summary>
protected override void OnOpen() protected override void OnOpen()
{ {
base.OnOpen(); base.OnOpen();
@@ -267,6 +288,9 @@ namespace PepperDash.Core
_connectionTime = DateTime.Now; _connectionTime = DateTime.Now;
} }
/// <summary>
/// OnMessage method
/// </summary>
protected override void OnMessage(MessageEventArgs e) protected override void OnMessage(MessageEventArgs e)
{ {
base.OnMessage(e); base.OnMessage(e);
@@ -274,6 +298,9 @@ namespace PepperDash.Core
Debug.Console(0, "WebSocket UiClient Message: {0}", e.Data); Debug.Console(0, "WebSocket UiClient Message: {0}", e.Data);
} }
/// <summary>
/// OnClose method
/// </summary>
protected override void OnClose(CloseEventArgs e) protected override void OnClose(CloseEventArgs e)
{ {
base.OnClose(e); base.OnClose(e);
@@ -282,6 +309,9 @@ namespace PepperDash.Core
} }
/// <summary>
/// OnError method
/// </summary>
protected override void OnError(WebSocketSharp.ErrorEventArgs e) protected override void OnError(WebSocketSharp.ErrorEventArgs e)
{ {
base.OnError(e); base.OnError(e);

View File

@@ -5,9 +5,16 @@ using System.Threading.Tasks;
namespace PepperDash.Core.Web.RequestHandlers namespace PepperDash.Core.Web.RequestHandlers
{ {
/// <summary>
/// CWS Base Async Handler, implements IHttpCwsHandler
/// </summary>
public abstract class WebApiBaseRequestAsyncHandler:IHttpCwsHandler public abstract class WebApiBaseRequestAsyncHandler:IHttpCwsHandler
{ {
private readonly Dictionary<string, Func<HttpCwsContext, Task>> _handlers; private readonly Dictionary<string, Func<HttpCwsContext, Task>> _handlers;
/// <summary>
/// Indicates whether CORS is enabled
/// </summary>
protected readonly bool EnableCors; protected readonly bool EnableCors;
/// <summary> /// <summary>

View File

@@ -10,6 +10,10 @@ namespace PepperDash.Core.Web.RequestHandlers
public abstract class WebApiBaseRequestHandler : IHttpCwsHandler public abstract class WebApiBaseRequestHandler : IHttpCwsHandler
{ {
private readonly Dictionary<string, Action<HttpCwsContext>> _handlers; private readonly Dictionary<string, Action<HttpCwsContext>> _handlers;
/// <summary>
/// Indicates whether CORS is enabled
/// </summary>
protected readonly bool EnableCors; protected readonly bool EnableCors;
/// <summary> /// <summary>

View File

@@ -45,9 +45,9 @@ namespace PepperDash.Core.Web
/// </summary> /// </summary>
public bool IsRegistered { get; private set; } public bool IsRegistered { get; private set; }
/// <summary> // /// <summary>
/// Http request handler // /// Http request handler
/// </summary> // /// </summary>
//public IHttpCwsHandler HttpRequestHandler //public IHttpCwsHandler HttpRequestHandler
//{ //{
// get { return _server.HttpRequestHandler; } // get { return _server.HttpRequestHandler; }
@@ -58,9 +58,9 @@ namespace PepperDash.Core.Web
// } // }
//} //}
/// <summary> // /// <summary>
/// Received request event handler // /// Received request event handler
/// </summary> // /// </summary>
//public event EventHandler<HttpCwsRequestEventArgs> ReceivedRequestEvent //public event EventHandler<HttpCwsRequestEventArgs> ReceivedRequestEvent
//{ //{
// add { _server.ReceivedRequestEvent += new HttpCwsRequestEventHandler(value); } // add { _server.ReceivedRequestEvent += new HttpCwsRequestEventHandler(value); }

View File

@@ -2,25 +2,29 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharpPro; using Crestron.SimplSharpPro;
using Crestron.SimplSharpPro.DeviceSupport; using Crestron.SimplSharpPro.DeviceSupport;
using Crestron.SimplSharpPro.EthernetCommunication; using Crestron.SimplSharpPro.EthernetCommunication;
using Newtonsoft.Json; using Newtonsoft.Json;
using PepperDash.Core; using PepperDash.Core;
using PepperDash.Core.Logging;
using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.Config;
using Serilog.Events; using Serilog.Events;
//using PepperDash.Essentials.Devices.Common.Cameras;
namespace PepperDash.Essentials.Core.Bridges namespace PepperDash.Essentials.Core.Bridges
{ {
/// <summary> /// <summary>
/// Base class for bridge API variants /// Base class for bridge API variants
/// </summary> /// </summary>
[Obsolete("Will be removed in v3.0.0")]
public abstract class BridgeApi : EssentialsDevice public abstract class BridgeApi : EssentialsDevice
{ {
/// <summary>
/// Constructor
/// </summary>
/// <param name="key">Device key</param>
protected BridgeApi(string key) : protected BridgeApi(string key) :
base(key) base(key)
{ {
@@ -29,23 +33,36 @@ namespace PepperDash.Essentials.Core.Bridges
} }
/// <summary> /// <summary>
/// Represents a EiscApiAdvanced /// Class to link devices and rooms to an EISC Instance
/// </summary> /// </summary>
public class EiscApiAdvanced : BridgeApi, ICommunicationMonitor public class EiscApiAdvanced : BridgeApi, ICommunicationMonitor
{ {
/// <summary>
/// Gets the PropertiesConfig
/// </summary>
public EiscApiPropertiesConfig PropertiesConfig { get; private set; } public EiscApiPropertiesConfig PropertiesConfig { get; private set; }
/// <summary>
/// Gets the JoinMaps dictionary
/// </summary>
public Dictionary<string, JoinMapBaseAdvanced> JoinMaps { get; private set; } public Dictionary<string, JoinMapBaseAdvanced> JoinMaps { get; private set; }
/// <summary>
/// Gets the EISC instance
/// </summary>
public BasicTriList Eisc { get; private set; } public BasicTriList Eisc { get; private set; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="dc">Device configuration</param>
/// <param name="eisc">EISC instance</param>
public EiscApiAdvanced(DeviceConfig dc, BasicTriList eisc) : public EiscApiAdvanced(DeviceConfig dc, BasicTriList eisc) :
base(dc.Key) base(dc.Key)
{ {
JoinMaps = new Dictionary<string, JoinMapBaseAdvanced>(); JoinMaps = new Dictionary<string, JoinMapBaseAdvanced>();
PropertiesConfig = dc.Properties.ToObject<EiscApiPropertiesConfig>(); PropertiesConfig = dc.Properties.ToObject<EiscApiPropertiesConfig>();
//PropertiesConfig = JsonConvert.DeserializeObject<EiscApiPropertiesConfig>(dc.Properties.ToString());
Eisc = eisc; Eisc = eisc;
@@ -61,7 +78,6 @@ namespace PepperDash.Essentials.Core.Bridges
/// <summary> /// <summary>
/// CustomActivate method /// CustomActivate method
/// </summary> /// </summary>
/// <inheritdoc />
public override bool CustomActivate() public override bool CustomActivate()
{ {
CommunicationMonitor.Start(); CommunicationMonitor.Start();
@@ -83,7 +99,7 @@ namespace PepperDash.Essentials.Core.Bridges
if (PropertiesConfig.Devices == null) if (PropertiesConfig.Devices == null)
{ {
Debug.LogMessage(LogEventLevel.Debug, this, "No devices linked to this bridge"); this.LogDebug("No devices linked to this bridge");
return; return;
} }
@@ -104,9 +120,7 @@ namespace PepperDash.Essentials.Core.Bridges
continue; continue;
} }
Debug.LogMessage(LogEventLevel.Information, this, this.LogWarning("{deviceKey} is not compatible with this bridge type. Please update the device.", device.Key);
"{0} is not compatible with this bridge type. Please use 'eiscapi' instead, or updae the device.",
device.Key);
} }
} }
@@ -121,34 +135,31 @@ namespace PepperDash.Essentials.Core.Bridges
if (registerResult != eDeviceRegistrationUnRegistrationResponse.Success) if (registerResult != eDeviceRegistrationUnRegistrationResponse.Success)
{ {
Debug.LogMessage(LogEventLevel.Verbose, this, "Registration result: {0}", registerResult); this.LogVerbose("Registration result: {registerResult}", registerResult);
return; return;
} }
Debug.LogMessage(LogEventLevel.Debug, this, "EISC registration successful"); this.LogDebug("EISC registration successful");
} }
/// <summary> /// <summary>
/// LinkRooms method /// Link rooms to this EISC. Rooms MUST implement IBridgeAdvanced
/// </summary> /// </summary>
public void LinkRooms() public void LinkRooms()
{ {
Debug.LogMessage(LogEventLevel.Debug, this, "Linking Rooms..."); this.LogDebug("Linking Rooms...");
if (PropertiesConfig.Rooms == null) if (PropertiesConfig.Rooms == null)
{ {
Debug.LogMessage(LogEventLevel.Debug, this, "No rooms linked to this bridge."); this.LogDebug("No rooms linked to this bridge.");
return; return;
} }
foreach (var room in PropertiesConfig.Rooms) foreach (var room in PropertiesConfig.Rooms)
{ {
var rm = DeviceManager.GetDeviceForKey(room.RoomKey) as IBridgeAdvanced; if (!(DeviceManager.GetDeviceForKey(room.RoomKey) is IBridgeAdvanced rm))
if (rm == null)
{ {
Debug.LogMessage(LogEventLevel.Debug, this, this.LogDebug("Room {roomKey} does not implement IBridgeAdvanced. Skipping...", room.RoomKey);
"Room {0} does not implement IBridgeAdvanced. Skipping...", room.RoomKey);
continue; continue;
} }
@@ -159,11 +170,8 @@ namespace PepperDash.Essentials.Core.Bridges
/// <summary> /// <summary>
/// Adds a join map /// Adds a join map
/// </summary> /// </summary>
/// <param name="deviceKey"></param> /// <param name="deviceKey">The key of the device to add the join map for</param>
/// <param name="joinMap"></param> /// <param name="joinMap">The join map to add</param>
/// <summary>
/// AddJoinMap method
/// </summary>
public void AddJoinMap(string deviceKey, JoinMapBaseAdvanced joinMap) public void AddJoinMap(string deviceKey, JoinMapBaseAdvanced joinMap)
{ {
if (!JoinMaps.ContainsKey(deviceKey)) if (!JoinMaps.ContainsKey(deviceKey))
@@ -172,14 +180,13 @@ namespace PepperDash.Essentials.Core.Bridges
} }
else else
{ {
Debug.LogMessage(LogEventLevel.Verbose, this, "Unable to add join map with key '{0}'. Key already exists in JoinMaps dictionary", deviceKey); this.LogWarning("Unable to add join map with key '{deviceKey}'. Key already exists in JoinMaps dictionary", deviceKey);
} }
} }
/// <summary> /// <summary>
/// PrintJoinMaps method /// PrintJoinMaps method
/// </summary> /// </summary>
/// <inheritdoc />
public virtual void PrintJoinMaps() public virtual void PrintJoinMaps()
{ {
CrestronConsole.ConsoleCommandResponse("Join Maps for EISC IPID: {0}\r\n", Eisc.ID.ToString("X")); CrestronConsole.ConsoleCommandResponse("Join Maps for EISC IPID: {0}\r\n", Eisc.ID.ToString("X"));
@@ -190,17 +197,17 @@ namespace PepperDash.Essentials.Core.Bridges
joinMap.Value.PrintJoinMapInfo(); joinMap.Value.PrintJoinMapInfo();
} }
} }
/// <summary> /// <summary>
/// MarkdownForBridge method /// MarkdownForBridge method
/// </summary> /// </summary>
/// <inheritdoc />
public virtual void MarkdownForBridge(string bridgeKey) public virtual void MarkdownForBridge(string bridgeKey)
{ {
Debug.LogMessage(LogEventLevel.Information, this, "Writing Joinmaps to files for EISC IPID: {0}", Eisc.ID.ToString("X")); this.LogInformation("Writing Joinmaps to files for EISC IPID: {eiscId}", Eisc.ID.ToString("X"));
foreach (var joinMap in JoinMaps) foreach (var joinMap in JoinMaps)
{ {
Debug.LogMessage(LogEventLevel.Information, "Generating markdown for device '{0}':", joinMap.Key); this.LogInformation("Generating markdown for device '{deviceKey}':", joinMap.Key);
joinMap.Value.MarkdownJoinMapInfo(joinMap.Key, bridgeKey); joinMap.Value.MarkdownJoinMapInfo(joinMap.Key, bridgeKey);
} }
} }
@@ -208,53 +215,45 @@ namespace PepperDash.Essentials.Core.Bridges
/// <summary> /// <summary>
/// Prints the join map for a device by key /// Prints the join map for a device by key
/// </summary> /// </summary>
/// <param name="deviceKey"></param> /// <param name="deviceKey">The key of the device to print the join map for</param>
/// <summary>
/// PrintJoinMapForDevice method
/// </summary>
public void PrintJoinMapForDevice(string deviceKey) public void PrintJoinMapForDevice(string deviceKey)
{ {
var joinMap = JoinMaps[deviceKey]; var joinMap = JoinMaps[deviceKey];
if (joinMap == null) if (joinMap == null)
{ {
Debug.LogMessage(LogEventLevel.Information, this, "Unable to find joinMap for device with key: '{0}'", deviceKey); this.LogInformation("Unable to find joinMap for device with key: '{deviceKey}'", deviceKey);
return; return;
} }
Debug.LogMessage(LogEventLevel.Information, "Join map for device '{0}' on EISC '{1}':", deviceKey, Key); this.LogInformation("Join map for device '{deviceKey}' on EISC '{eiscKey}':", deviceKey, Key);
joinMap.PrintJoinMapInfo(); joinMap.PrintJoinMapInfo();
} }
/// <summary> /// <summary>
/// Prints the join map for a device by key /// Prints the join map for a device by key in Markdown format
/// </summary>
/// <param name="deviceKey"></param>
/// <summary>
/// MarkdownJoinMapForDevice method
/// </summary> /// </summary>
/// <param name="deviceKey">The key of the device to print the join map for</param>
/// <param name="bridgeKey">The key of the bridge to use for the Markdown output</param>
public void MarkdownJoinMapForDevice(string deviceKey, string bridgeKey) public void MarkdownJoinMapForDevice(string deviceKey, string bridgeKey)
{ {
var joinMap = JoinMaps[deviceKey]; var joinMap = JoinMaps[deviceKey];
if (joinMap == null) if (joinMap == null)
{ {
Debug.LogMessage(LogEventLevel.Information, this, "Unable to find joinMap for device with key: '{0}'", deviceKey); this.LogInformation("Unable to find joinMap for device with key: '{deviceKey}'", deviceKey);
return; return;
} }
Debug.LogMessage(LogEventLevel.Information, "Join map for device '{0}' on EISC '{1}':", deviceKey, Key); this.LogInformation("Join map for device '{deviceKey}' on EISC '{eiscKey}':", deviceKey, Key);
joinMap.MarkdownJoinMapInfo(deviceKey, bridgeKey); joinMap.MarkdownJoinMapInfo(deviceKey, bridgeKey);
} }
/// <summary> /// <summary>
/// Used for debugging to trigger an action based on a join number and type /// Used for debugging to trigger an action based on a join number and type
/// </summary> /// </summary>
/// <param name="join"></param> /// <param name="join">The join number to execute the action for</param>
/// <param name="type"></param> /// <param name="type">The type of join (digital, analog, serial)</param>
/// <param name="state"></param> /// <param name="state">The state to pass to the action</param>
/// <summary>
/// ExecuteJoinAction method
/// </summary>
public void ExecuteJoinAction(uint join, string type, object state) public void ExecuteJoinAction(uint join, string type, object state)
{ {
try try
@@ -263,78 +262,87 @@ namespace PepperDash.Essentials.Core.Bridges
{ {
case "digital": case "digital":
{ {
var uo = Eisc.BooleanOutput[join].UserObject as Action<bool>; if (Eisc.BooleanOutput[join].UserObject is Action<bool> userObject)
if (uo != null)
{ {
Debug.LogMessage(LogEventLevel.Verbose, this, "Executing Action: {0}", uo.ToString()); this.LogVerbose("Executing Boolean Action");
uo(Convert.ToBoolean(state)); userObject(Convert.ToBoolean(state));
} }
else else
Debug.LogMessage(LogEventLevel.Verbose, this, "User Action is null. Nothing to Execute"); this.LogVerbose("User Object is null. Nothing to Execute");
break; break;
} }
case "analog": case "analog":
{ {
var uo = Eisc.BooleanOutput[join].UserObject as Action<ushort>; if (Eisc.UShortOutput[join].UserObject is Action<ushort> userObject)
if (uo != null)
{ {
Debug.LogMessage(LogEventLevel.Verbose, this, "Executing Action: {0}", uo.ToString()); this.LogVerbose("Executing Analog Action");
uo(Convert.ToUInt16(state)); userObject(Convert.ToUInt16(state));
} }
else else
Debug.LogMessage(LogEventLevel.Verbose, this, "User Action is null. Nothing to Execute"); break; this.LogVerbose("User Object is null. Nothing to Execute");
break;
} }
case "serial": case "serial":
{ {
var uo = Eisc.BooleanOutput[join].UserObject as Action<string>; if (Eisc.StringOutput[join].UserObject is Action<string> userObject)
if (uo != null)
{ {
Debug.LogMessage(LogEventLevel.Verbose, this, "Executing Action: {0}", uo.ToString()); this.LogVerbose("Executing Serial Action");
uo(Convert.ToString(state)); userObject(Convert.ToString(state));
} }
else else
Debug.LogMessage(LogEventLevel.Verbose, this, "User Action is null. Nothing to Execute"); this.LogVerbose("User Object is null. Nothing to Execute");
break; break;
} }
default: default:
{ {
Debug.LogMessage(LogEventLevel.Verbose, "Unknown join type. Use digital/serial/analog"); this.LogVerbose("Unknown join type. Use digital/serial/analog");
break; break;
} }
} }
} }
catch (Exception e) catch (Exception e)
{ {
Debug.LogMessage(LogEventLevel.Debug, this, "Error: {0}", e); this.LogError("ExecuteJoinAction error: {message}", e.Message);
this.LogDebug(e, "Stack Trace: ");
} }
} }
/// <summary> /// <summary>
/// Handles incoming sig changes /// Handle incoming sig changes
/// </summary> /// </summary>
/// <param name="currentDevice"></param> /// <param name="currentDevice">BasicTriList device that triggered the event</param>
/// <param name="args"></param> /// <param name="args">Event arguments containing the signal information</param>
protected void Eisc_SigChange(object currentDevice, SigEventArgs args) protected void Eisc_SigChange(object currentDevice, SigEventArgs args)
{ {
try try
{ {
Debug.LogMessage(LogEventLevel.Verbose, this, "EiscApiAdvanced change: {0} {1}={2}", args.Sig.Type, args.Sig.Number, args.Sig.StringValue); this.LogVerbose("EiscApiAdvanced change: {type} {number}={value}", args.Sig.Type, args.Sig.Number, args.Sig.StringValue);
var uo = args.Sig.UserObject; var userObject = args.Sig.UserObject;
if (uo == null) return; if (userObject == null) return;
Debug.LogMessage(LogEventLevel.Debug, this, "Executing Action: {0}", uo.ToString());
if (uo is Action<bool>) if (userObject is Action<bool>)
(uo as Action<bool>)(args.Sig.BoolValue); {
else if (uo is Action<ushort>) this.LogDebug("Executing Boolean Action");
(uo as Action<ushort>)(args.Sig.UShortValue); (userObject as Action<bool>)(args.Sig.BoolValue);
else if (uo is Action<string>) }
(uo as Action<string>)(args.Sig.StringValue); else if (userObject is Action<ushort>)
{
this.LogDebug("Executing Analog Action");
(userObject as Action<ushort>)(args.Sig.UShortValue);
}
else if (userObject is Action<string>)
{
this.LogDebug("Executing Serial Action");
(userObject as Action<string>)(args.Sig.StringValue);
}
} }
catch (Exception e) catch (Exception e)
{ {
Debug.LogMessage(LogEventLevel.Verbose, this, "Error in Eisc_SigChange handler: {0}", e); this.LogError("Eisc_SigChange handler error: {message}", e.Message);
this.LogDebug(e, "Stack Trace: ");
} }
} }
@@ -423,22 +431,33 @@ namespace PepperDash.Essentials.Core.Bridges
} }
/// <summary> /// <summary>
/// Represents a EiscApiAdvancedFactory /// Factory class for EiscApiAdvanced devices
/// </summary> /// </summary>
/// <remarks>
/// Supported types:
/// eiscapiadv - Create a standard EISC client over TCP/IP
/// eiscapiadvanced - Create a standard EISC client over TCP/IP
/// eiscapiadvancedserver - Create an EISC server
/// eiscapiadvancedclient - Create an EISC client
/// vceiscapiadv - Create a VC-4 EISC client
/// vceiscapiadvanced - Create a VC-4 EISC client
/// eiscapiadvudp - Create a standard EISC client over UDP
/// eiscapiadvancedudp - Create a standard EISC client over UDP
/// </remarks>
public class EiscApiAdvancedFactory : EssentialsDeviceFactory<EiscApiAdvanced> public class EiscApiAdvancedFactory : EssentialsDeviceFactory<EiscApiAdvanced>
{ {
/// <summary>
/// Constructor
/// </summary>
public EiscApiAdvancedFactory() public EiscApiAdvancedFactory()
{ {
TypeNames = new List<string> { "eiscapiadv", "eiscapiadvanced", "eiscapiadvancedserver", "eiscapiadvancedclient", "vceiscapiadv", "vceiscapiadvanced" }; TypeNames = new List<string> { "eiscapiadv", "eiscapiadvanced", "eiscapiadvancedserver", "eiscapiadvancedclient", "vceiscapiadv", "vceiscapiadvanced", "eiscapiadvudp", "eiscapiadvancedudp" };
} }
/// <summary>
/// BuildDevice method
/// </summary>
/// <inheritdoc /> /// <inheritdoc />
public override EssentialsDevice BuildDevice(DeviceConfig dc) public override EssentialsDevice BuildDevice(DeviceConfig dc)
{ {
Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new EiscApiAdvanced Device"); Debug.LogDebug("Attempting to create new EiscApiAdvanced Device");
var controlProperties = CommFactory.GetControlPropertiesConfig(dc); var controlProperties = CommFactory.GetControlPropertiesConfig(dc);
@@ -446,6 +465,13 @@ namespace PepperDash.Essentials.Core.Bridges
switch (dc.Type.ToLower()) switch (dc.Type.ToLower())
{ {
case "eiscapiadvudp":
case "eiscapiadvancedudp":
{
eisc = new EthernetIntersystemCommunications(controlProperties.IpIdInt,
controlProperties.TcpSshProperties.Address, Global.ControlSystem);
break;
}
case "eiscapiadv": case "eiscapiadv":
case "eiscapiadvanced": case "eiscapiadvanced":
{ {
@@ -468,7 +494,7 @@ namespace PepperDash.Essentials.Core.Bridges
{ {
if (string.IsNullOrEmpty(controlProperties.RoomId)) if (string.IsNullOrEmpty(controlProperties.RoomId))
{ {
Debug.LogMessage(LogEventLevel.Information, "Unable to build VC-4 EISC Client for device {0}. Room ID is missing or empty", dc.Key); Debug.LogInformation("Unable to build VC-4 EISC Client for device {deviceKey}. Room ID is missing or empty", dc.Key);
eisc = null; eisc = null;
break; break;
} }

View File

@@ -11,11 +11,11 @@ using PepperDash.Core;
namespace PepperDash.Essentials.Core.Config namespace PepperDash.Essentials.Core.Config
{ {
/// <summary> /// <summary>
/// Loads the ConfigObject from the file /// Loads the ConfigObject from the file
/// </summary> /// </summary>
public class EssentialsConfig : BasicConfig public class EssentialsConfig : BasicConfig
{ {
/// <summary> /// <summary>
/// Gets or sets the SystemUrl /// Gets or sets the SystemUrl
/// </summary> /// </summary>
@@ -32,24 +32,33 @@ namespace PepperDash.Essentials.Core.Config
/// Gets the SystemUuid extracted from the SystemUrl /// Gets the SystemUuid extracted from the SystemUrl
/// </summary> /// </summary>
[JsonProperty("systemUuid")] [JsonProperty("systemUuid")]
public string SystemUuid public string SystemUuid
{ {
get get
{ {
if (string.IsNullOrEmpty(SystemUrl)) string uuid;
return "missing url";
if (SystemUrl.Contains("#")) if (string.IsNullOrEmpty(SystemUrl))
{ {
var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/#.*"); uuid = "missing url";
string uuid = result.Groups[1].Value;
return uuid;
} else
{
var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/.*");
string uuid = result.Groups[1].Value;
return uuid;
} }
else if (SystemUrl.Contains("#"))
{
var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/#.*");
uuid = result.Groups[1].Value;
}
else if (SystemUrl.Contains("detail"))
{
var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/detail\/(.*)\/.*");
uuid = result.Groups[1].Value;
}
else
{
var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/.*");
uuid = result.Groups[1].Value;
}
return uuid;
} }
} }
@@ -57,24 +66,33 @@ namespace PepperDash.Essentials.Core.Config
/// Gets the TemplateUuid extracted from the TemplateUrl /// Gets the TemplateUuid extracted from the TemplateUrl
/// </summary> /// </summary>
[JsonProperty("templateUuid")] [JsonProperty("templateUuid")]
public string TemplateUuid public string TemplateUuid
{ {
get get
{ {
if (string.IsNullOrEmpty(TemplateUrl)) string uuid;
return "missing template url";
if (TemplateUrl.Contains("#")) if (string.IsNullOrEmpty(TemplateUrl))
{ {
var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/templates\/(.*)\/#.*"); uuid = "missing template url";
string uuid = result.Groups[1].Value;
return uuid;
} else
{
var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/system-templates\/(.*)\/system-template-versions\/(.*)\/.*");
string uuid = result.Groups[2].Value;
return uuid;
} }
else if (TemplateUrl.Contains("#"))
{
var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/templates\/(.*)\/#.*");
uuid = result.Groups[1].Value;
}
else if (TemplateUrl.Contains("detail"))
{
var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/system-templates\/detail\/(.*)\/system-template-versions\/detail\/(.*)\/.*");
uuid = result.Groups[2].Value;
}
else
{
var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/system-templates\/(.*)\/system-template-versions\/(.*)\/.*");
uuid = result.Groups[2].Value;
}
return uuid;
} }
} }
@@ -97,7 +115,7 @@ namespace PepperDash.Essentials.Core.Config
{ {
Rooms = new List<DeviceConfig>(); Rooms = new List<DeviceConfig>();
} }
} }
/// <summary> /// <summary>
/// Represents version data for Essentials and its packages /// Represents version data for Essentials and its packages
@@ -147,7 +165,7 @@ namespace PepperDash.Essentials.Core.Config
/// Represents a SystemTemplateConfigs /// Represents a SystemTemplateConfigs
/// </summary> /// </summary>
public class SystemTemplateConfigs public class SystemTemplateConfigs
{ {
/// <summary> /// <summary>
/// Gets or sets the System /// Gets or sets the System
/// </summary> /// </summary>
@@ -157,5 +175,5 @@ namespace PepperDash.Essentials.Core.Config
/// Gets or sets the Template /// Gets or sets the Template
/// </summary> /// </summary>
public EssentialsConfig Template { get; set; } public EssentialsConfig Template { get; set; }
} }
} }

View File

@@ -0,0 +1,10 @@
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Defines minimum functionality for an audio zone
/// </summary>
public interface IAudioZone : IBasicVolumeWithFeedback
{
void SelectInput(ushort input);
}
}

View File

@@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Identifies a device that contains audio zones
/// </summary>
public interface IAudioZones : IRouting
{
Dictionary<uint, IAudioZone> Zone { get; }
}
}

View File

@@ -0,0 +1,27 @@
using PepperDash.Core;
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Defines minimal volume and mute control methods
/// </summary>
public interface IBasicVolumeControls : IKeyName
{
/// <summary>
/// Increases the volume
/// </summary>
/// <param name="pressRelease">Indicates whether the volume change is a press and hold action</param>
void VolumeUp(bool pressRelease);
/// <summary>
/// Decreases the volume
/// </summary>
/// <param name="pressRelease">Indicates whether the volume change is a press and hold action</param>
void VolumeDown(bool pressRelease);
/// <summary>
/// Toggles the mute state
/// </summary>
void MuteToggle();
}
}

View File

@@ -0,0 +1,14 @@
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Defines the contract for IBasicVolumeWithFeedback
/// </summary>
public interface IBasicVolumeWithFeedback : IBasicVolumeControls
{
BoolFeedback MuteFeedback { get; }
void MuteOn();
void MuteOff();
void SetVolume(ushort level);
IntFeedback VolumeLevelFeedback { get; }
}
}

View File

@@ -0,0 +1,12 @@
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Defines the contract for IBasicVolumeWithFeedbackAdvanced
/// </summary>
public interface IBasicVolumeWithFeedbackAdvanced : IBasicVolumeWithFeedback
{
int RawVolumeLevel { get; }
eVolumeLevelUnits Units { get; }
}
}

View File

@@ -0,0 +1,41 @@
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Defines the contract for IFullAudioSettings
/// </summary>
public interface IFullAudioSettings : IBasicVolumeWithFeedback
{
void SetBalance(ushort level);
void BalanceLeft(bool pressRelease);
void BalanceRight(bool pressRelease);
void SetBass(ushort level);
void BassUp(bool pressRelease);
void BassDown(bool pressRelease);
void SetTreble(ushort level);
void TrebleUp(bool pressRelease);
void TrebleDown(bool pressRelease);
bool hasMaxVolume { get; }
void SetMaxVolume(ushort level);
void MaxVolumeUp(bool pressRelease);
void MaxVolumeDown(bool pressRelease);
bool hasDefaultVolume { get; }
void SetDefaultVolume(ushort level);
void DefaultVolumeUp(bool pressRelease);
void DefaultVolumeDown(bool pressRelease);
void LoudnessToggle();
void MonoToggle();
BoolFeedback LoudnessFeedback { get; }
BoolFeedback MonoFeedback { get; }
IntFeedback BalanceFeedback { get; }
IntFeedback BassFeedback { get; }
IntFeedback TrebleFeedback { get; }
IntFeedback MaxVolumeFeedback { get; }
IntFeedback DefaultVolumeFeedback { get; }
}
}

View File

@@ -0,0 +1,17 @@
using System;
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Defines the contract for IHasCurrentVolumeControls
/// </summary>
public interface IHasCurrentVolumeControls
{
IBasicVolumeControls CurrentVolumeControls { get; }
event EventHandler<VolumeDeviceChangeEventArgs> CurrentVolumeDeviceChange;
void SetDefaultLevels();
bool ZeroVolumeWhenSwtichingVolumeDevices { get; }
}
}

View File

@@ -0,0 +1,10 @@
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Defines basic mute control methods
/// </summary>
public interface IHasMuteControl
{
void MuteToggle();
}
}

View File

@@ -0,0 +1,12 @@
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Defines mute control methods and properties with feedback
/// </summary>
public interface IHasMuteControlWithFeedback : IHasMuteControl
{
BoolFeedback MuteFeedback { get; }
void MuteOn();
void MuteOff();
}
}

View File

@@ -0,0 +1,11 @@
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Defines the contract for IHasVolumeControl
/// </summary>
public interface IHasVolumeControl
{
void VolumeUp(bool pressRelease);
void VolumeDown(bool pressRelease);
}
}

View File

@@ -0,0 +1,11 @@
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Defines volume control methods and properties with feedback
/// </summary>
public interface IHasVolumeControlWithFeedback : IHasVolumeControl
{
void SetVolume(ushort level);
IntFeedback VolumeLevelFeedback { get; }
}
}

View File

@@ -0,0 +1,10 @@
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Defines the contract for IHasVolumeDevice
/// </summary>
public interface IHasVolumeDevice
{
IBasicVolumeControls VolumeDevice { get; }
}
}

View File

@@ -0,0 +1,10 @@
namespace PepperDash.Essentials.Core
{
public enum eVolumeLevelUnits
{
Decibels,
Percent,
Relative,
Absolute
}
}

View File

@@ -1,161 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Defines minimal volume and mute control methods
/// </summary>
public interface IBasicVolumeControls
{
void VolumeUp(bool pressRelease);
void VolumeDown(bool pressRelease);
void MuteToggle();
}
/// <summary>
/// Defines the contract for IHasVolumeControl
/// </summary>
public interface IHasVolumeControl
{
void VolumeUp(bool pressRelease);
void VolumeDown(bool pressRelease);
}
/// <summary>
/// Defines volume control methods and properties with feedback
/// </summary>
public interface IHasVolumeControlWithFeedback : IHasVolumeControl
{
void SetVolume(ushort level);
IntFeedback VolumeLevelFeedback { get; }
}
/// <summary>
/// Defines basic mute control methods
/// </summary>
public interface IHasMuteControl
{
void MuteToggle();
}
/// <summary>
/// Defines mute control methods and properties with feedback
/// </summary>
public interface IHasMuteControlWithFeedback : IHasMuteControl
{
BoolFeedback MuteFeedback { get; }
void MuteOn();
void MuteOff();
}
/// <summary>
/// Defines the contract for IBasicVolumeWithFeedback
/// </summary>
public interface IBasicVolumeWithFeedback : IBasicVolumeControls
{
BoolFeedback MuteFeedback { get; }
void MuteOn();
void MuteOff();
void SetVolume(ushort level);
IntFeedback VolumeLevelFeedback { get; }
}
/// <summary>
/// Defines the contract for IBasicVolumeWithFeedbackAdvanced
/// </summary>
public interface IBasicVolumeWithFeedbackAdvanced : IBasicVolumeWithFeedback
{
int RawVolumeLevel { get; }
eVolumeLevelUnits Units { get; }
}
public enum eVolumeLevelUnits
{
Decibels,
Percent,
Relative,
Absolute
}
/// <summary>
/// Defines the contract for IHasCurrentVolumeControls
/// </summary>
public interface IHasCurrentVolumeControls
{
IBasicVolumeControls CurrentVolumeControls { get; }
event EventHandler<VolumeDeviceChangeEventArgs> CurrentVolumeDeviceChange;
void SetDefaultLevels();
bool ZeroVolumeWhenSwtichingVolumeDevices { get; }
}
/// <summary>
/// Defines the contract for IFullAudioSettings
/// </summary>
public interface IFullAudioSettings : IBasicVolumeWithFeedback
{
void SetBalance(ushort level);
void BalanceLeft(bool pressRelease);
void BalanceRight(bool pressRelease);
void SetBass(ushort level);
void BassUp(bool pressRelease);
void BassDown(bool pressRelease);
void SetTreble(ushort level);
void TrebleUp(bool pressRelease);
void TrebleDown(bool pressRelease);
bool hasMaxVolume { get; }
void SetMaxVolume(ushort level);
void MaxVolumeUp(bool pressRelease);
void MaxVolumeDown(bool pressRelease);
bool hasDefaultVolume { get; }
void SetDefaultVolume(ushort level);
void DefaultVolumeUp(bool pressRelease);
void DefaultVolumeDown(bool pressRelease);
void LoudnessToggle();
void MonoToggle();
BoolFeedback LoudnessFeedback { get; }
BoolFeedback MonoFeedback { get; }
IntFeedback BalanceFeedback { get; }
IntFeedback BassFeedback { get; }
IntFeedback TrebleFeedback { get; }
IntFeedback MaxVolumeFeedback { get; }
IntFeedback DefaultVolumeFeedback { get; }
}
/// <summary>
/// Defines the contract for IHasVolumeDevice
/// </summary>
public interface IHasVolumeDevice
{
IBasicVolumeControls VolumeDevice { get; }
}
/// <summary>
/// Identifies a device that contains audio zones
/// </summary>
public interface IAudioZones : IRouting
{
Dictionary<uint, IAudioZone> Zone { get; }
}
/// <summary>
/// Defines minimum functionality for an audio zone
/// </summary>
public interface IAudioZone : IBasicVolumeWithFeedback
{
void SelectInput(ushort input);
}
}

View File

@@ -77,9 +77,6 @@ namespace PepperDash.Essentials.Core
/// A name that will override the source's name on the UI /// A name that will override the source's name on the UI
/// </summary> /// </summary>
[JsonProperty("name")] [JsonProperty("name")]
/// <summary>
/// Gets or sets the Name
/// </summary>
public string Name { get; set; } public string Name { get; set; }
/// <summary> /// <summary>

View File

@@ -1,16 +1,10 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using PepperDash.Core; using PepperDash.Core;
using PepperDash.Essentials.Core;
using PepperDash.Essentials.Core.Config;
using PepperDash.Essentials.Core.Devices; using PepperDash.Essentials.Core.Devices;
using Serilog.Events; using Serilog.Events;
@@ -25,17 +19,25 @@ namespace PepperDash.Essentials.Core.Fusion
/// <summary> /// <summary>
/// Evaluates the room info and custom properties from Fusion and updates the system properties aa needed /// Evaluates the room info and custom properties from Fusion and updates the system properties aa needed
/// </summary> /// </summary>
/// <param name="roomInfo"></param> /// <param name="room">The room associated with this Fusion instance</param>
public void EvaluateRoomInfo(string roomKey, RoomInformation roomInfo) /// <param name="roomInfo">The room information from Fusion</param>
/// <param name="useFusionRoomName"></param>
public void EvaluateRoomInfo(IEssentialsRoom room, RoomInformation roomInfo, bool useFusionRoomName)
{ {
try try
{ {
var reconfigurableDevices = DeviceManager.AllDevices.Where(d => d is ReconfigurableDevice); var reconfigurableDevices = DeviceManager.AllDevices.OfType<ReconfigurableDevice>();
foreach (var device in reconfigurableDevices) foreach (var device in reconfigurableDevices)
{ {
// Get the current device config so new values can be overwritten over existing // Get the current device config so new values can be overwritten over existing
var deviceConfig = (device as ReconfigurableDevice).Config; var deviceConfig = device.Config;
if (device is IEssentialsRoom)
{
// Skipping room name as this will affect ALL room instances in the configuration and cause unintended consequences when multiple rooms are present and multiple Fusion instances are used
continue;
}
if (device is RoomOnToDefaultSourceWhenOccupied) if (device is RoomOnToDefaultSourceWhenOccupied)
{ {
@@ -85,36 +87,49 @@ namespace PepperDash.Essentials.Core.Fusion
deviceConfig.Properties = JToken.FromObject(devProps); deviceConfig.Properties = JToken.FromObject(devProps);
} }
else if (device is IEssentialsRoom)
{
// Set the room name
if (!string.IsNullOrEmpty(roomInfo.Name))
{
Debug.LogMessage(LogEventLevel.Debug, "Current Room Name: {0}. New Room Name: {1}", deviceConfig.Name, roomInfo.Name);
// Set the name in config
deviceConfig.Name = roomInfo.Name;
Debug.LogMessage(LogEventLevel.Debug, "Room Name Successfully Changed.");
}
// Set the help message
var helpMessage = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("RoomHelpMessage"));
if (helpMessage != null)
{
//Debug.LogMessage(LogEventLevel.Debug, "Current Help Message: {0}. New Help Message: {1}", deviceConfig.Properties["help"]["message"].Value<string>(ToString()), helpMessage.CustomFieldValue);
deviceConfig.Properties["helpMessage"] = (string)helpMessage.CustomFieldValue;
}
}
// Set the config on the device // Set the config on the device
(device as ReconfigurableDevice).SetConfig(deviceConfig); device.SetConfig(deviceConfig);
} }
if (!(room is ReconfigurableDevice reconfigurable))
{
Debug.LogWarning("FusionCustomPropertiesBridge: Room is not a ReconfigurableDevice. Cannot map custom properties.");
return;
}
var roomConfig = reconfigurable.Config;
var updateConfig = false;
// Set the room name
if (!string.IsNullOrEmpty(roomInfo.Name) && useFusionRoomName)
{
Debug.LogDebug("Current Room Name: {currentName}. New Room Name: {fusionName}", roomConfig.Name, roomInfo.Name);
// Set the name in config
roomConfig.Name = roomInfo.Name;
updateConfig = true;
Debug.LogDebug("Room Name Successfully Changed.");
}
// Set the help message
var helpMessage = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("RoomHelpMessage"));
if (helpMessage != null)
{
roomConfig.Properties["helpMessage"] = helpMessage.CustomFieldValue;
updateConfig = true;
}
if (updateConfig)
{
reconfigurable.SetConfig(roomConfig);
}
} }
catch (Exception e) catch (Exception e)
{ {
Debug.LogMessage(LogEventLevel.Debug, "FusionCustomPropetiesBridge: Error mapping properties: {0}", e); Debug.LogError("FusionCustomPropetiesBridge: Exception mapping properties for {roomKey}: {message}", room.Key, e.Message);
Debug.LogDebug(e, "Stack Trace: ");
} }
} }
} }

View File

@@ -1,4 +1,9 @@
using Crestron.SimplSharp; using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Timers;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp.CrestronIO;
using Crestron.SimplSharp.CrestronXml; using Crestron.SimplSharp.CrestronXml;
using Crestron.SimplSharp.CrestronXml.Serialization; using Crestron.SimplSharp.CrestronXml.Serialization;
@@ -10,17 +15,13 @@ using PepperDash.Core.Logging;
using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.Config;
using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Core.DeviceTypeInterfaces;
using Serilog.Events; using Serilog.Events;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace PepperDash.Essentials.Core.Fusion namespace PepperDash.Essentials.Core.Fusion
{ {
/// <summary> /// <summary>
/// Represents a EssentialsHuddleSpaceFusionSystemControllerBase /// Represents a EssentialsHuddleSpaceFusionSystemControllerBase
/// </summary> /// </summary>
public class IEssentialsRoomFusionController : EssentialsDevice, IOccupancyStatusProvider, IFusionHelpRequest public class IEssentialsRoomFusionController : EssentialsDevice, IOccupancyStatusProvider, IFusionHelpRequest, IHasFeedback
{ {
private IEssentialsRoomFusionControllerPropertiesConfig _config; private IEssentialsRoomFusionControllerPropertiesConfig _config;
@@ -87,15 +88,17 @@ namespace PepperDash.Essentials.Core.Fusion
/// <inheritdoc /> /// <inheritdoc />
public StringFeedback HelpRequestStatusFeedback { get; private set; } public StringFeedback HelpRequestStatusFeedback { get; private set; }
private Timer _helpRequestTimeoutTimer;
#region System Info Sigs /// <summary>
/// Gets the DefaultHelpRequestTimeoutMs
/// </summary>
public int HelpRequestTimeoutMs => _config.HelpRequestTimeoutMs;
//StringSigData SystemName; /// <summary>
//StringSigData Model; /// Gets whether to use a timer for help requests
//StringSigData SerialNumber; /// </summary>
//StringSigData Uptime; public bool UseHelpRequestTimer => _config.UseTimeoutForHelpRequests;
#endregion
#region Processor Info Sigs #region Processor Info Sigs
@@ -240,6 +243,19 @@ namespace PepperDash.Essentials.Core.Fusion
this.LogDebug("Occupancy setup complete"); this.LogDebug("Occupancy setup complete");
HelpRequestResponseFeedback = new StringFeedback("HelpRequestResponse", () => FusionRoom.Help.OutputSig.StringValue);
HelpRequestSentFeedback = new BoolFeedback("HelpRequestSent", () => _helpRequestSent);
HelpRequestStatusFeedback = new StringFeedback("HelpRequestStatus", () => _helpRequestStatus.ToString());
Feedbacks.Add(HelpRequestResponseFeedback);
Feedbacks.Add(HelpRequestSentFeedback);
Feedbacks.Add(HelpRequestStatusFeedback);
if (RoomOccupancyRemoteStringFeedback != null)
Feedbacks.Add(RoomOccupancyRemoteStringFeedback);
if (RoomIsOccupiedFeedback != null)
Feedbacks.Add(RoomIsOccupiedFeedback);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -303,10 +319,6 @@ namespace PepperDash.Essentials.Core.Fusion
FusionRVI.GenerateFileForAllFusionDevices(); FusionRVI.GenerateFileForAllFusionDevices();
HelpRequestResponseFeedback = new StringFeedback("HelpRequestResponse", () => FusionRoom.Help.OutputSig.StringValue);
HelpRequestSentFeedback = new BoolFeedback("HelpRequestSent", () => _helpRequestSent);
HelpRequestStatusFeedback = new StringFeedback("HelpRequestStatus", () => _helpRequestStatus.ToString());
} }
/// <summary> /// <summary>
@@ -339,6 +351,11 @@ namespace PepperDash.Essentials.Core.Fusion
#endregion #endregion
/// <inheritdoc />
public FeedbackCollection<Feedback> Feedbacks { get; private set; } = new FeedbackCollection<Feedback>();
/// <summary> /// <summary>
/// ScheduleChange event /// ScheduleChange event
/// </summary> /// </summary>
@@ -1074,7 +1091,7 @@ namespace PepperDash.Essentials.Core.Fusion
} }
RoomInfoChange?.Invoke(this, new EventArgs()); RoomInfoChange?.Invoke(this, new EventArgs());
CustomPropertiesBridge.EvaluateRoomInfo(Room.Key, roomInformation); CustomPropertiesBridge.EvaluateRoomInfo(Room, roomInformation, _config.UseFusionRoomName);
} }
} }
catch (Exception e) catch (Exception e)
@@ -1772,7 +1789,7 @@ namespace PepperDash.Essentials.Core.Fusion
{ {
if (args.EventId == FusionEventIds.HelpMessageReceivedEventId) if (args.EventId == FusionEventIds.HelpMessageReceivedEventId)
{ {
this.LogInformation( "Help message received from Fusion for room '{0}'", this.LogInformation("Help message received from Fusion for room '{0}'",
Room.Name); Room.Name);
this.LogDebug("Help message content: {0}", FusionRoom.Help.OutputSig.StringValue); this.LogDebug("Help message content: {0}", FusionRoom.Help.OutputSig.StringValue);
@@ -1791,7 +1808,7 @@ namespace PepperDash.Essentials.Core.Fusion
break; break;
case "Please call the helpdesk.": case "Please call the helpdesk.":
// this.LogInformation("Please call the helpdesk."); // this.LogInformation("Please call the helpdesk.");
// _helpRequestStatus = eFusionHelpResponse.CallHelpDesk; _helpRequestStatus = eFusionHelpResponse.CallHelpDesk;
break; break;
case "Please wait, I will reschedule your meeting to a different room.": case "Please wait, I will reschedule your meeting to a different room.":
// this.LogInformation("Please wait, I will reschedule your meeting to a different room.", // this.LogInformation("Please wait, I will reschedule your meeting to a different room.",
@@ -1818,13 +1835,21 @@ namespace PepperDash.Essentials.Core.Fusion
_helpRequestStatus = eFusionHelpResponse.None; _helpRequestStatus = eFusionHelpResponse.None;
} }
if(_helpRequestStatus == eFusionHelpResponse.None) if (_helpRequestStatus == eFusionHelpResponse.None)
{ {
_helpRequestSent = false; _helpRequestSent = false;
HelpRequestSentFeedback.FireUpdate(); HelpRequestSentFeedback.FireUpdate();
} }
HelpRequestStatusFeedback.FireUpdate(); HelpRequestStatusFeedback.FireUpdate();
if (_helpRequestTimeoutTimer != null)
{
_helpRequestTimeoutTimer.Stop();
_helpRequestTimeoutTimer.Elapsed -= OnTimedEvent;
_helpRequestTimeoutTimer.Dispose();
_helpRequestTimeoutTimer = null;
}
} }
@@ -1895,10 +1920,34 @@ namespace PepperDash.Essentials.Core.Fusion
_helpRequestSent = true; _helpRequestSent = true;
HelpRequestSentFeedback.FireUpdate(); HelpRequestSentFeedback.FireUpdate();
if (UseHelpRequestTimer)
{
if (_helpRequestTimeoutTimer == null)
{
_helpRequestTimeoutTimer = new Timer(HelpRequestTimeoutMs);
_helpRequestTimeoutTimer.AutoReset = false;
_helpRequestTimeoutTimer.Enabled = true;
_helpRequestTimeoutTimer.Elapsed += OnTimedEvent;
}
_helpRequestTimeoutTimer.Interval = HelpRequestTimeoutMs;
_helpRequestTimeoutTimer.Start();
this.LogDebug("Help request timeout timer started for room '{0}' with timeout of {1} ms.",
Room.Name, HelpRequestTimeoutMs);
}
_helpRequestStatus = eFusionHelpResponse.HelpRequested; _helpRequestStatus = eFusionHelpResponse.HelpRequested;
HelpRequestStatusFeedback.FireUpdate(); HelpRequestStatusFeedback.FireUpdate();
} }
private void OnTimedEvent(object source, ElapsedEventArgs e)
{
this.LogInformation("Help request timeout reached for room '{0}'. Cancelling help request.", Room.Name);
CancelHelpRequest();
}
/// <inheritdoc /> /// <inheritdoc />
public void CancelHelpRequest() public void CancelHelpRequest()
{ {
@@ -1909,7 +1958,16 @@ namespace PepperDash.Essentials.Core.Fusion
HelpRequestSentFeedback.FireUpdate(); HelpRequestSentFeedback.FireUpdate();
_helpRequestStatus = eFusionHelpResponse.None; _helpRequestStatus = eFusionHelpResponse.None;
HelpRequestStatusFeedback.FireUpdate(); HelpRequestStatusFeedback.FireUpdate();
Debug.LogMessage(LogEventLevel.Information, this, "Help request cancelled in Fusion for room '{0}'", Room.Name); Debug.LogMessage(LogEventLevel.Information, this, "Help request cancelled for room '{0}'", Room.Name);
}
if (_helpRequestTimeoutTimer != null)
{
_helpRequestTimeoutTimer.Stop();
_helpRequestTimeoutTimer.Elapsed -= OnTimedEvent;
_helpRequestTimeoutTimer.Dispose();
_helpRequestTimeoutTimer = null;
this.LogDebug("Help request timeout timer stopped for room '{0}'.", Room.Name);
} }
} }

View File

@@ -27,7 +27,7 @@ public class IEssentialsRoomFusionControllerPropertiesConfig
} }
else else
{ {
Debug.LogWarning( "Failed to parse IpId '{0}' as UInt16", IpId); Debug.LogWarning("Failed to parse IpId '{0}' as UInt16", IpId);
return 0; return 0;
} }
} }
@@ -45,6 +45,13 @@ public class IEssentialsRoomFusionControllerPropertiesConfig
[JsonProperty("roomKey")] [JsonProperty("roomKey")]
public string RoomKey { get; set; } public string RoomKey { get; set; }
/// <summary>
/// Gets or sets whether to use the Fusion room name for this room
/// </summary>
/// <remarks>Defaults to true to preserve current behavior. Set to false to skip updating the room name from Fusion</remarks>
[JsonProperty("useFusionRoomName")]
public bool UseFusionRoomName { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets whether to use HTML format for help requests /// Gets or sets whether to use HTML format for help requests
/// </summary> /// </summary>
@@ -56,4 +63,16 @@ public class IEssentialsRoomFusionControllerPropertiesConfig
/// </summary> /// </summary>
[JsonProperty("use24HourTimeFormat")] [JsonProperty("use24HourTimeFormat")]
public bool Use24HourTimeFormat { get; set; } = false; public bool Use24HourTimeFormat { get; set; } = false;
/// <summary>
/// Gets or sets whether to use a timeout for help requests
/// </summary>
[JsonProperty("useTimeoutForHelpRequests")]
public bool UseTimeoutForHelpRequests { get; set; } = false;
/// <summary>
/// Gets or sets the timeout duration for help requests in milliseconds
/// </summary>
[JsonProperty("helpRequestTimeoutMs")]
public int HelpRequestTimeoutMs { get; set; } = 30000;
} }

View File

@@ -105,12 +105,21 @@ namespace PepperDash.Essentials.Room.Config
/// </summary> /// </summary>
public class EssentialsRoomPropertiesConfig public class EssentialsRoomPropertiesConfig
{ {
/// <summary>
/// Gets or sets the Addresses
/// </summary>
[JsonProperty("addresses")] [JsonProperty("addresses")]
public EssentialsRoomAddressPropertiesConfig Addresses { get; set; } public EssentialsRoomAddressPropertiesConfig Addresses { get; set; }
/// <summary>
/// Gets or sets the Description
/// </summary>
[JsonProperty("description")] [JsonProperty("description")]
public string Description { get; set; } public string Description { get; set; }
/// <summary>
/// Gets or sets the Emergency
/// </summary>
[JsonProperty("emergency")] [JsonProperty("emergency")]
public EssentialsRoomEmergencyConfig Emergency { get; set; } public EssentialsRoomEmergencyConfig Emergency { get; set; }
@@ -226,11 +235,11 @@ namespace PepperDash.Essentials.Room.Config
/// Indicates if this room represents a combination of other rooms /// Indicates if this room represents a combination of other rooms
/// </summary> /// </summary>
[JsonProperty("isRoomCombinationScenario")] [JsonProperty("isRoomCombinationScenario")]
/// <summary>
/// Gets or sets the IsRoomCombinationScenario
/// </summary>
public bool IsRoomCombinationScenario { get; set; } public bool IsRoomCombinationScenario { get; set; }
/// <summary>
/// Constructor
/// </summary>
public EssentialsRoomPropertiesConfig() public EssentialsRoomPropertiesConfig()
{ {
LogoLight = new EssentialsLogoPropertiesConfig(); LogoLight = new EssentialsLogoPropertiesConfig();
@@ -243,10 +252,10 @@ namespace PepperDash.Essentials.Room.Config
/// </summary> /// </summary>
public class EssentialsRoomUiBehaviorConfig public class EssentialsRoomUiBehaviorConfig
{ {
[JsonProperty("disableActivityButtonsWhileWarmingCooling")]
/// <summary> /// <summary>
/// Gets or sets the DisableActivityButtonsWhileWarmingCooling /// Gets or sets the DisableActivityButtonsWhileWarmingCooling
/// </summary> /// </summary>
[JsonProperty("disableActivityButtonsWhileWarmingCooling")]
public bool DisableActivityButtonsWhileWarmingCooling { get; set; } public bool DisableActivityButtonsWhileWarmingCooling { get; set; }
} }
@@ -255,74 +264,86 @@ namespace PepperDash.Essentials.Room.Config
/// </summary> /// </summary>
public class EssentialsAvRoomPropertiesConfig : EssentialsRoomPropertiesConfig public class EssentialsAvRoomPropertiesConfig : EssentialsRoomPropertiesConfig
{ {
[JsonProperty("defaultAudioKey")]
/// <summary> /// <summary>
/// Gets or sets the DefaultAudioKey /// Gets or sets the DefaultAudioKey
/// </summary> /// </summary>
[JsonProperty("defaultAudioKey")]
public string DefaultAudioKey { get; set; } public string DefaultAudioKey { get; set; }
[JsonProperty("sourceListKey")]
/// <summary>
/// Gets or sets the DefaultOnDspPresetKey
/// </summary>
[JsonProperty("defaultOnDspPresetKey")]
public string DefaultOnDspPresetKey { get; set; }
/// <summary>
/// Gets or sets the DefaultOffDspPresetKey
/// </summary>
[JsonProperty("defaultOffDspPresetKey")]
public string DefaultOffDspPresetKey { get; set; }
/// <summary> /// <summary>
/// Gets or sets the SourceListKey /// Gets or sets the SourceListKey
/// </summary> /// </summary>
/// </summary>
[JsonProperty("sourceListKey")]
public string SourceListKey { get; set; } public string SourceListKey { get; set; }
[JsonProperty("destinationListKey")]
/// <summary> /// <summary>
/// Gets or sets the DestinationListKey /// Gets or sets the DestinationListKey
/// </summary> /// </summary>
[JsonProperty("destinationListKey")]
public string DestinationListKey { get; set; } public string DestinationListKey { get; set; }
[JsonProperty("audioControlPointListKey")]
/// <summary> /// <summary>
/// Gets or sets the AudioControlPointListKey /// Gets or sets the AudioControlPointListKey
/// </summary> /// </summary>
[JsonProperty("audioControlPointListKey")]
public string AudioControlPointListKey { get; set; } public string AudioControlPointListKey { get; set; }
[JsonProperty("cameraListKey")]
/// <summary> /// <summary>
/// Gets or sets the CameraListKey /// Gets or sets the CameraListKey
/// </summary> /// </summary>
[JsonProperty("cameraListKey")]
public string CameraListKey { get; set; } public string CameraListKey { get; set; }
[JsonProperty("defaultSourceItem")]
/// <summary> /// <summary>
/// Gets or sets the DefaultSourceItem /// Gets or sets the DefaultSourceItem
/// </summary> /// </summary>
[JsonProperty("defaultSourceItem")]
public string DefaultSourceItem { get; set; } public string DefaultSourceItem { get; set; }
/// <summary> /// <summary>
/// Indicates if the room supports advanced sharing /// Indicates if the room supports advanced sharing
/// </summary> /// </summary>
[JsonProperty("supportsAdvancedSharing")] [JsonProperty("supportsAdvancedSharing")]
/// <summary>
/// Gets or sets the SupportsAdvancedSharing
/// </summary>
public bool SupportsAdvancedSharing { get; set; } public bool SupportsAdvancedSharing { get; set; }
/// <summary> /// <summary>
/// Indicates if non-tech users can change the share mode /// Indicates if non-tech users can change the share mode
/// </summary> /// </summary>
[JsonProperty("userCanChangeShareMode")] [JsonProperty("userCanChangeShareMode")]
/// <summary>
/// Gets or sets the UserCanChangeShareMode
/// </summary>
public bool UserCanChangeShareMode { get; set; } public bool UserCanChangeShareMode { get; set; }
[JsonProperty("matrixRoutingKey", NullValueHandling = NullValueHandling.Ignore)]
/// <summary> /// <summary>
/// Gets or sets the MatrixRoutingKey /// Gets or sets the MatrixRoutingKey
/// </summary> /// </summary>
[JsonProperty("matrixRoutingKey", NullValueHandling = NullValueHandling.Ignore)]
public string MatrixRoutingKey { get; set; } public string MatrixRoutingKey { get; set; }
} }
/// <summary>
/// Represents a EssentialsConferenceRoomPropertiesConfig
/// </summary>
public class EssentialsConferenceRoomPropertiesConfig : EssentialsAvRoomPropertiesConfig public class EssentialsConferenceRoomPropertiesConfig : EssentialsAvRoomPropertiesConfig
{ {
[JsonProperty("videoCodecKey")]
/// <summary> /// <summary>
/// Gets or sets the VideoCodecKey /// Gets or sets the VideoCodecKey
/// </summary> /// </summary>
[JsonProperty("videoCodecKey")]
public string VideoCodecKey { get; set; } public string VideoCodecKey { get; set; }
[JsonProperty("audioCodecKey")]
/// <summary> /// <summary>
/// Gets or sets the AudioCodecKey /// Gets or sets the AudioCodecKey
/// </summary> /// </summary>
[JsonProperty("audioCodecKey")]
public string AudioCodecKey { get; set; } public string AudioCodecKey { get; set; }
} }
@@ -337,12 +358,15 @@ namespace PepperDash.Essentials.Room.Config
/// </summary> /// </summary>
public bool Enabled { get; set; } public bool Enabled { get; set; }
[JsonProperty("deviceKeys")]
/// <summary> /// <summary>
/// Gets or sets the DeviceKeys /// Gets or sets the DeviceKeys
/// </summary> /// </summary>
[JsonProperty("deviceKeys")]
public List<string> DeviceKeys { get; set; } public List<string> DeviceKeys { get; set; }
/// <summary>
/// Constructor
/// </summary>
public EssentialsEnvironmentPropertiesConfig() public EssentialsEnvironmentPropertiesConfig()
{ {
DeviceKeys = new List<string>(); DeviceKeys = new List<string>();
@@ -355,6 +379,9 @@ namespace PepperDash.Essentials.Room.Config
/// </summary> /// </summary>
public class EssentialsRoomFusionConfig public class EssentialsRoomFusionConfig
{ {
/// <summary>
/// Gets the the IpId as a UInt16
/// </summary>
public uint IpIdInt public uint IpIdInt
{ {
get get
@@ -371,16 +398,16 @@ namespace PepperDash.Essentials.Room.Config
} }
} }
[JsonProperty("ipId")]
/// <summary> /// <summary>
/// Gets or sets the IpId /// Gets or sets the IpId
/// </summary> /// </summary>
[JsonProperty("ipId")]
public string IpId { get; set; } public string IpId { get; set; }
[JsonProperty("joinMapKey")]
/// <summary> /// <summary>
/// Gets or sets the JoinMapKey /// Gets or sets the JoinMapKey
/// </summary> /// </summary>
[JsonProperty("joinMapKey")]
public string JoinMapKey { get; set; } public string JoinMapKey { get; set; }
} }
@@ -390,16 +417,16 @@ namespace PepperDash.Essentials.Room.Config
/// </summary> /// </summary>
public class EssentialsRoomMicrophonePrivacyConfig public class EssentialsRoomMicrophonePrivacyConfig
{ {
[JsonProperty("deviceKey")]
/// <summary> /// <summary>
/// Gets or sets the DeviceKey /// Gets or sets the DeviceKey
/// </summary> /// </summary>
[JsonProperty("deviceKey")]
public string DeviceKey { get; set; } public string DeviceKey { get; set; }
[JsonProperty("behaviour")]
/// <summary> /// <summary>
/// Gets or sets the Behaviour /// Gets or sets the Behaviour
/// </summary> /// </summary>
[JsonProperty("behaviour")]
public string Behaviour { get; set; } public string Behaviour { get; set; }
} }
@@ -408,12 +435,15 @@ namespace PepperDash.Essentials.Room.Config
/// </summary> /// </summary>
public class EssentialsHelpPropertiesConfig public class EssentialsHelpPropertiesConfig
{ {
[JsonProperty("message")]
/// <summary> /// <summary>
/// Gets or sets the Message /// Gets or sets the Message
/// </summary> /// </summary>
[JsonProperty("message")]
public string Message { get; set; } public string Message { get; set; }
/// <summary>
/// Gets or sets the ShowCallButton
/// </summary>
[JsonProperty("showCallButton")] [JsonProperty("showCallButton")]
public bool ShowCallButton { get; set; } public bool ShowCallButton { get; set; }
@@ -421,11 +451,11 @@ namespace PepperDash.Essentials.Room.Config
/// Defaults to "Call Help Desk" /// Defaults to "Call Help Desk"
/// </summary> /// </summary>
[JsonProperty("callButtonText")] [JsonProperty("callButtonText")]
/// <summary>
/// Gets or sets the CallButtonText
/// </summary>
public string CallButtonText { get; set; } public string CallButtonText { get; set; }
/// <summary>
/// Constructor
/// </summary>
public EssentialsHelpPropertiesConfig() public EssentialsHelpPropertiesConfig()
{ {
CallButtonText = "Call Help Desk"; CallButtonText = "Call Help Desk";
@@ -437,22 +467,28 @@ namespace PepperDash.Essentials.Room.Config
/// </summary> /// </summary>
public class EssentialsOneButtonMeetingPropertiesConfig public class EssentialsOneButtonMeetingPropertiesConfig
{ {
[JsonProperty("enable")]
/// <summary> /// <summary>
/// Gets or sets the Enable /// Gets or sets the Enable
/// </summary> /// </summary>
[JsonProperty("enable")]
public bool Enable { get; set; } public bool Enable { get; set; }
} }
/// <summary>
/// Represents a EssentialsRoomAddressPropertiesConfig
/// </summary>
public class EssentialsRoomAddressPropertiesConfig public class EssentialsRoomAddressPropertiesConfig
{ {
/// <summary>
/// Gets or sets the PhoneNumber
/// </summary>
[JsonProperty("phoneNumber")] [JsonProperty("phoneNumber")]
public string PhoneNumber { get; set; } public string PhoneNumber { get; set; }
[JsonProperty("sipAddress")]
/// <summary> /// <summary>
/// Gets or sets the SipAddress /// Gets or sets the SipAddress
/// </summary> /// </summary>
[JsonProperty("sipAddress")]
public string SipAddress { get; set; } public string SipAddress { get; set; }
} }
@@ -462,14 +498,18 @@ namespace PepperDash.Essentials.Room.Config
/// </summary> /// </summary>
public class EssentialsLogoPropertiesConfig public class EssentialsLogoPropertiesConfig
{ {
[JsonProperty("type")]
/// <summary> /// <summary>
/// Gets or sets the Type /// Gets or sets the Type
/// </summary> /// </summary>
[JsonProperty("type")]
public string Type { get; set; } public string Type { get; set; }
/// <summary>
/// Gets or sets the Url
/// </summary>
[JsonProperty("url")] [JsonProperty("url")]
public string Url { get; set; } public string Url { get; set; }
/// <summary> /// <summary>
/// GetLogoUrlLight method /// GetLogoUrlLight method
/// </summary> /// </summary>
@@ -502,22 +542,28 @@ namespace PepperDash.Essentials.Room.Config
/// </summary> /// </summary>
public class EssentialsRoomOccSensorConfig public class EssentialsRoomOccSensorConfig
{ {
[JsonProperty("deviceKey")]
/// <summary> /// <summary>
/// Gets or sets the DeviceKey /// Gets or sets the DeviceKey
/// </summary> /// </summary>
[JsonProperty("deviceKey")]
public string DeviceKey { get; set; } public string DeviceKey { get; set; }
/// <summary>
/// Gets or sets the TimeoutMinutes
/// </summary>
[JsonProperty("timeoutMinutes")] [JsonProperty("timeoutMinutes")]
public int TimeoutMinutes { get; set; } public int TimeoutMinutes { get; set; }
} }
/// <summary>
/// Represents a EssentialsRoomTechConfig
/// </summary>
public class EssentialsRoomTechConfig public class EssentialsRoomTechConfig
{ {
[JsonProperty("password")]
/// <summary> /// <summary>
/// Gets or sets the Password /// Gets or sets the Password
/// </summary> /// </summary>
[JsonProperty("password")]
public string Password { get; set; } public string Password { get; set; }
} }
} }

View File

@@ -1,11 +1,11 @@
using Crestron.SimplSharpPro.Keypads; using System;
using PepperDash.Essentials.Core.Queues;
using PepperDash.Essentials.Core.Routing;
using Serilog.Events;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using Crestron.SimplSharpPro.Keypads;
using PepperDash.Essentials.Core.Queues;
using PepperDash.Essentials.Core.Routing;
using Serilog.Events;
using Debug = PepperDash.Core.Debug; using Debug = PepperDash.Core.Debug;
@@ -134,14 +134,15 @@ namespace PepperDash.Essentials.Core
} }
// otherwise, audioVideo needs to be handled as two steps. // otherwise, audioVideo needs to be handled as two steps.
Debug.LogMessage(LogEventLevel.Debug, "Attempting to build source route from {sourceKey} of type {type}", destination, source.Key); Debug.LogMessage(LogEventLevel.Debug, "Attempting to build source route from {destinationKey} to {sourceKey} of type {type}", destination, source.Key, signalType);
RouteDescriptor audioRouteDescriptor; RouteDescriptor audioRouteDescriptor;
if (signalType.HasFlag(eRoutingSignalType.SecondaryAudio)) if (signalType.HasFlag(eRoutingSignalType.SecondaryAudio))
{ {
audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.SecondaryAudio); audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.SecondaryAudio);
} else }
else
{ {
audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.Audio); audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.Audio);
} }
@@ -239,7 +240,7 @@ namespace PepperDash.Essentials.Core
Debug.LogMessage(LogEventLevel.Information, "Device: {destination} is NOT cooling down. Removing stored route request and routing to source key: {sourceKey}", null, destination.Key, routeRequest.Source.Key); Debug.LogMessage(LogEventLevel.Information, "Device: {destination} is NOT cooling down. Removing stored route request and routing to source key: {sourceKey}", null, destination.Key, routeRequest.Source.Key);
} }
routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination,destinationPort?.Key ?? string.Empty, false)); routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, destinationPort?.Key ?? string.Empty, false));
routeRequestQueue.Enqueue(new RouteRequestQueueItem(RunRouteRequest, routeRequest)); routeRequestQueue.Enqueue(new RouteRequestQueueItem(RunRouteRequest, routeRequest));
} }
@@ -272,7 +273,8 @@ namespace PepperDash.Essentials.Core
audioOrSingleRoute.ExecuteRoutes(); audioOrSingleRoute.ExecuteRoutes();
videoRoute?.ExecuteRoutes(); videoRoute?.ExecuteRoutes();
} catch(Exception ex) }
catch (Exception ex)
{ {
Debug.LogMessage(ex, "Exception Running Route Request {request}", null, request); Debug.LogMessage(ex, "Exception Running Route Request {request}", null, request);
} }
@@ -305,9 +307,10 @@ namespace PepperDash.Essentials.Core
Debug.LogMessage(LogEventLevel.Information, "Releasing current route: {0}", destination, current.Source.Key); Debug.LogMessage(LogEventLevel.Information, "Releasing current route: {0}", destination, current.Source.Key);
current.ReleaseRoutes(clearRoute); current.ReleaseRoutes(clearRoute);
} }
} catch (Exception ex) }
catch (Exception ex)
{ {
Debug.LogMessage(ex, "Exception releasing route for '{destination}':'{inputPortKey}'",null, destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey); Debug.LogMessage(ex, "Exception releasing route for '{destination}':'{inputPortKey}'", null, destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey);
} }
} }

View File

@@ -1,7 +1,7 @@
using PepperDash.Core; using System;
using PepperDash.Essentials.Core.Config;
using System;
using System.Linq; using System.Linq;
using PepperDash.Core;
using PepperDash.Essentials.Core.Config;
namespace PepperDash.Essentials.Core.Routing namespace PepperDash.Essentials.Core.Routing
{ {
@@ -9,20 +9,20 @@ namespace PepperDash.Essentials.Core.Routing
/// Manages routing feedback by subscribing to route changes on midpoint and sink devices, /// Manages routing feedback by subscribing to route changes on midpoint and sink devices,
/// tracing the route back to the original source, and updating the CurrentSourceInfo on sink devices. /// tracing the route back to the original source, and updating the CurrentSourceInfo on sink devices.
/// </summary> /// </summary>
public class RoutingFeedbackManager:EssentialsDevice public class RoutingFeedbackManager : EssentialsDevice
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RoutingFeedbackManager"/> class. /// Initializes a new instance of the <see cref="RoutingFeedbackManager"/> class.
/// </summary> /// </summary>
/// <param name="key">The unique key for this manager device.</param> /// <param name="key">The unique key for this manager device.</param>
/// <param name="name">The name of this manager device.</param> /// <param name="name">The name of this manager device.</param>
public RoutingFeedbackManager(string key, string name): base(key, name) public RoutingFeedbackManager(string key, string name)
: base(key, name)
{ {
AddPreActivationAction(SubscribeForMidpointFeedback); AddPreActivationAction(SubscribeForMidpointFeedback);
AddPreActivationAction(SubscribeForSinkFeedback); AddPreActivationAction(SubscribeForSinkFeedback);
} }
/// <summary> /// <summary>
/// Subscribes to the RouteChanged event on all devices implementing <see cref="IRoutingWithFeedback"/>. /// Subscribes to the RouteChanged event on all devices implementing <see cref="IRoutingWithFeedback"/>.
/// </summary> /// </summary>
@@ -41,12 +41,13 @@ namespace PepperDash.Essentials.Core.Routing
/// </summary> /// </summary>
private void SubscribeForSinkFeedback() private void SubscribeForSinkFeedback()
{ {
var sinkDevices = DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>(); var sinkDevices =
DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
foreach (var device in sinkDevices) foreach (var device in sinkDevices)
{ {
device.InputChanged += HandleSinkUpdate; device.InputChanged += HandleSinkUpdate;
} }
} }
/// <summary> /// <summary>
@@ -55,11 +56,15 @@ namespace PepperDash.Essentials.Core.Routing
/// </summary> /// </summary>
/// <param name="midpoint">The midpoint device that reported a route change.</param> /// <param name="midpoint">The midpoint device that reported a route change.</param>
/// <param name="newRoute">The descriptor of the new route.</param> /// <param name="newRoute">The descriptor of the new route.</param>
private void HandleMidpointUpdate(IRoutingWithFeedback midpoint, RouteSwitchDescriptor newRoute) private void HandleMidpointUpdate(
IRoutingWithFeedback midpoint,
RouteSwitchDescriptor newRoute
)
{ {
try try
{ {
var devices = DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>(); var devices =
DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
foreach (var device in devices) foreach (var device in devices)
{ {
@@ -68,7 +73,13 @@ namespace PepperDash.Essentials.Core.Routing
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.LogMessage(ex, "Error handling midpoint update from {midpointKey}:{Exception}", this, midpoint.Key, ex); Debug.LogMessage(
ex,
"Error handling midpoint update from {midpointKey}:{Exception}",
this,
midpoint.Key,
ex
);
} }
} }
@@ -78,7 +89,10 @@ namespace PepperDash.Essentials.Core.Routing
/// </summary> /// </summary>
/// <param name="sender">The sink device that reported an input change.</param> /// <param name="sender">The sink device that reported an input change.</param>
/// <param name="currentInputPort">The new input port selected on the sink device.</param> /// <param name="currentInputPort">The new input port selected on the sink device.</param>
private void HandleSinkUpdate(IRoutingSinkWithSwitching sender, RoutingInputPort currentInputPort) private void HandleSinkUpdate(
IRoutingSinkWithSwitching sender,
RoutingInputPort currentInputPort
)
{ {
try try
{ {
@@ -86,7 +100,13 @@ namespace PepperDash.Essentials.Core.Routing
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.LogMessage(ex, "Error handling Sink update from {senderKey}:{Exception}", this, sender.Key, ex); Debug.LogMessage(
ex,
"Error handling Sink update from {senderKey}:{Exception}",
this,
sender.Key,
ex
);
} }
} }
@@ -96,13 +116,27 @@ namespace PepperDash.Essentials.Core.Routing
/// </summary> /// </summary>
/// <param name="destination">The destination sink device to update.</param> /// <param name="destination">The destination sink device to update.</param>
/// <param name="inputPort">The currently selected input port on the destination device.</param> /// <param name="inputPort">The currently selected input port on the destination device.</param>
private void UpdateDestination(IRoutingSinkWithSwitching destination, RoutingInputPort inputPort) private void UpdateDestination(
IRoutingSinkWithSwitching destination,
RoutingInputPort inputPort
)
{ {
Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Updating destination {destination} with inputPort {inputPort}", this, destination?.Key, inputPort?.Key); Debug.LogMessage(
Serilog.Events.LogEventLevel.Debug,
"Updating destination {destination} with inputPort {inputPort}",
this,
destination?.Key,
inputPort?.Key
);
if(inputPort == null) if (inputPort == null)
{ {
Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "Destination {destination} has not reported an input port yet", this,destination.Key); Debug.LogMessage(
Serilog.Events.LogEventLevel.Debug,
"Destination {destination} has not reported an input port yet",
this,
destination.Key
);
return; return;
} }
@@ -111,11 +145,19 @@ namespace PepperDash.Essentials.Core.Routing
{ {
var tieLines = TieLineCollection.Default; var tieLines = TieLineCollection.Default;
firstTieLine = tieLines.FirstOrDefault(tl => tl.DestinationPort.Key == inputPort.Key && tl.DestinationPort.ParentDevice.Key == inputPort.ParentDevice.Key); firstTieLine = tieLines.FirstOrDefault(tl =>
tl.DestinationPort.Key == inputPort.Key
&& tl.DestinationPort.ParentDevice.Key == inputPort.ParentDevice.Key
);
if (firstTieLine == null) if (firstTieLine == null)
{ {
Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No tieline found for inputPort {inputPort}. Clearing current source", this, inputPort); Debug.LogMessage(
Serilog.Events.LogEventLevel.Debug,
"No tieline found for inputPort {inputPort}. Clearing current source",
this,
inputPort
);
var tempSourceListItem = new SourceListItem var tempSourceListItem = new SourceListItem
{ {
@@ -123,12 +165,13 @@ namespace PepperDash.Essentials.Core.Routing
Name = inputPort.Key, Name = inputPort.Key,
}; };
destination.CurrentSourceInfo = tempSourceListItem;
destination.CurrentSourceInfo = tempSourceListItem; ; ;
destination.CurrentSourceInfoKey = "$transient"; destination.CurrentSourceInfoKey = "$transient";
return; return;
} }
} catch (Exception ex) }
catch (Exception ex)
{ {
Debug.LogMessage(ex, "Error getting first tieline: {Exception}", this, ex); Debug.LogMessage(ex, "Error getting first tieline: {Exception}", this, ex);
return; return;
@@ -143,7 +186,12 @@ namespace PepperDash.Essentials.Core.Routing
if (sourceTieLine == null) if (sourceTieLine == null)
{ {
Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No route found to source for inputPort {inputPort}. Clearing current source", this, inputPort); Debug.LogMessage(
Serilog.Events.LogEventLevel.Debug,
"No route found to source for inputPort {inputPort}. Clearing current source",
this,
inputPort
);
var tempSourceListItem = new SourceListItem var tempSourceListItem = new SourceListItem
{ {
@@ -155,7 +203,8 @@ namespace PepperDash.Essentials.Core.Routing
destination.CurrentSourceInfoKey = string.Empty; destination.CurrentSourceInfoKey = string.Empty;
return; return;
} }
} catch(Exception ex) }
catch (Exception ex)
{ {
Debug.LogMessage(ex, "Error getting sourceTieLine: {Exception}", this, ex); Debug.LogMessage(ex, "Error getting sourceTieLine: {Exception}", this, ex);
return; return;
@@ -164,23 +213,35 @@ namespace PepperDash.Essentials.Core.Routing
// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root TieLine {tieLine}", this, sourceTieLine); // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root TieLine {tieLine}", this, sourceTieLine);
// Does not handle combinable scenarios or other scenarios where a display might be part of multiple rooms yet. // Does not handle combinable scenarios or other scenarios where a display might be part of multiple rooms yet.
var room = DeviceManager.AllDevices.OfType<IEssentialsRoom>().FirstOrDefault((r) => { var room = DeviceManager
if(r is IHasMultipleDisplays roomMultipleDisplays) .AllDevices.OfType<IEssentialsRoom>()
{ .FirstOrDefault(
return roomMultipleDisplays.Displays.Any(d => d.Value.Key == destination.Key); (r) =>
} {
if (r is IHasMultipleDisplays roomMultipleDisplays)
{
return roomMultipleDisplays.Displays.Any(d =>
d.Value.Key == destination.Key
);
}
if(r is IHasDefaultDisplay roomDefaultDisplay) if (r is IHasDefaultDisplay roomDefaultDisplay)
{ {
return roomDefaultDisplay.DefaultDisplay.Key == destination.Key; return roomDefaultDisplay.DefaultDisplay.Key == destination.Key;
} }
return false; return false;
}); }
);
if(room == null) if (room == null)
{ {
Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "No room found for display {destination}", this, destination.Key); Debug.LogMessage(
Serilog.Events.LogEventLevel.Debug,
"No room found for display {destination}",
this,
destination.Key
);
return; return;
} }
@@ -190,21 +251,31 @@ namespace PepperDash.Essentials.Core.Routing
if (sourceList == null) if (sourceList == null)
{ {
Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "No source list found for source list key {key}. Unable to find source for tieLine {sourceTieLine}", this, room.SourceListKey, sourceTieLine); Debug.LogMessage(
Serilog.Events.LogEventLevel.Debug,
"No source list found for source list key {key}. Unable to find source for tieLine {sourceTieLine}",
this,
room.SourceListKey,
sourceTieLine
);
return; return;
} }
// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found sourceList for room {room}", this, room.Key); // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found sourceList for room {room}", this, room.Key);
var sourceListItem = sourceList.FirstOrDefault(sli => { var sourceListItem = sourceList.FirstOrDefault(sli =>
//// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, {
// "SourceListItem {sourceListItem}:{sourceKey} tieLine sourceport device key {sourcePortDeviceKey}", //// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose,
// this, // "SourceListItem {sourceListItem}:{sourceKey} tieLine sourceport device key {sourcePortDeviceKey}",
// sli.Key, // this,
// sli.Value.SourceKey, // sli.Key,
// sourceTieLine.SourcePort.ParentDevice.Key); // sli.Value.SourceKey,
// sourceTieLine.SourcePort.ParentDevice.Key);
return sli.Value.SourceKey.Equals(sourceTieLine.SourcePort.ParentDevice.Key,StringComparison.InvariantCultureIgnoreCase); return sli.Value.SourceKey.Equals(
sourceTieLine.SourcePort.ParentDevice.Key,
StringComparison.InvariantCultureIgnoreCase
);
}); });
var source = sourceListItem.Value; var source = sourceListItem.Value;
@@ -212,7 +283,13 @@ namespace PepperDash.Essentials.Core.Routing
if (source == null) if (source == null)
{ {
Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No source found for device {key}. Creating transient source for {destination}", this, sourceTieLine.SourcePort.ParentDevice.Key, destination); Debug.LogMessage(
Serilog.Events.LogEventLevel.Debug,
"No source found for device {key}. Creating transient source for {destination}",
this,
sourceTieLine.SourcePort.ParentDevice.Key,
destination
);
var tempSourceListItem = new SourceListItem var tempSourceListItem = new SourceListItem
{ {
@@ -229,7 +306,6 @@ namespace PepperDash.Essentials.Core.Routing
destination.CurrentSourceInfoKey = sourceKey; destination.CurrentSourceInfoKey = sourceKey;
destination.CurrentSourceInfo = source; destination.CurrentSourceInfo = source;
} }
/// <summary> /// <summary>
@@ -249,29 +325,49 @@ namespace PepperDash.Essentials.Core.Routing
{ {
// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLine Source device {sourceDevice} is midpoint", this, midpoint); // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLine Source device {sourceDevice} is midpoint", this, midpoint);
if(midpoint.CurrentRoutes == null || midpoint.CurrentRoutes.Count == 0) if (midpoint.CurrentRoutes == null || midpoint.CurrentRoutes.Count == 0)
{ {
Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Midpoint {midpointKey} has no routes",this, midpoint.Key); Debug.LogMessage(
Serilog.Events.LogEventLevel.Debug,
"Midpoint {midpointKey} has no routes",
this,
midpoint.Key
);
return null; return null;
} }
var currentRoute = midpoint.CurrentRoutes.FirstOrDefault(route => { var currentRoute = midpoint.CurrentRoutes.FirstOrDefault(route =>
{
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Checking {route} against {tieLine}", this, route, tieLine); //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Checking {route} against {tieLine}", this, route, tieLine);
return route.OutputPort != null && route.InputPort != null && route.OutputPort?.Key == tieLine.SourcePort.Key && route.OutputPort?.ParentDevice.Key == tieLine.SourcePort.ParentDevice.Key; return route.OutputPort != null
&& route.InputPort != null
&& route.OutputPort?.Key == tieLine.SourcePort.Key
&& route.OutputPort?.ParentDevice.Key
== tieLine.SourcePort.ParentDevice.Key;
}); });
if (currentRoute == null) if (currentRoute == null)
{ {
Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No route through midpoint {midpoint} for outputPort {outputPort}", this, midpoint.Key, tieLine.SourcePort); Debug.LogMessage(
Serilog.Events.LogEventLevel.Debug,
"No route through midpoint {midpoint} for outputPort {outputPort}",
this,
midpoint.Key,
tieLine.SourcePort
);
return null; return null;
} }
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found currentRoute {currentRoute} through {midpoint}", this, currentRoute, midpoint); //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found currentRoute {currentRoute} through {midpoint}", this, currentRoute, midpoint);
nextTieLine = TieLineCollection.Default.FirstOrDefault(tl => { nextTieLine = TieLineCollection.Default.FirstOrDefault(tl =>
{
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Checking {route} against {tieLine}", tl.DestinationPort.Key, currentRoute.InputPort.Key); //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Checking {route} against {tieLine}", tl.DestinationPort.Key, currentRoute.InputPort.Key);
return tl.DestinationPort.Key == currentRoute.InputPort.Key && tl.DestinationPort.ParentDevice.Key == currentRoute.InputPort.ParentDevice.Key; }); return tl.DestinationPort.Key == currentRoute.InputPort.Key
&& tl.DestinationPort.ParentDevice.Key
== currentRoute.InputPort.ParentDevice.Key;
});
if (nextTieLine != null) if (nextTieLine != null)
{ {
@@ -286,19 +382,26 @@ namespace PepperDash.Essentials.Core.Routing
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLIne Source Device {sourceDeviceKey} is IRoutingSource: {isIRoutingSource}", this, tieLine.SourcePort.ParentDevice.Key, tieLine.SourcePort.ParentDevice is IRoutingSource); //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLIne Source Device {sourceDeviceKey} is IRoutingSource: {isIRoutingSource}", this, tieLine.SourcePort.ParentDevice.Key, tieLine.SourcePort.ParentDevice is IRoutingSource);
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLine Source Device interfaces: {typeFullName}:{interfaces}", this, tieLine.SourcePort.ParentDevice.GetType().FullName, tieLine.SourcePort.ParentDevice.GetType().GetInterfaces().Select(i => i.Name)); //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLine Source Device interfaces: {typeFullName}:{interfaces}", this, tieLine.SourcePort.ParentDevice.GetType().FullName, tieLine.SourcePort.ParentDevice.GetType().GetInterfaces().Select(i => i.Name));
if (tieLine.SourcePort.ParentDevice is IRoutingSource || tieLine.SourcePort.ParentDevice is IRoutingOutputs) //end of the chain if (
tieLine.SourcePort.ParentDevice is IRoutingSource
|| tieLine.SourcePort.ParentDevice is IRoutingOutputs
) //end of the chain
{ {
// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root: {tieLine}", this, tieLine); // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root: {tieLine}", this, tieLine);
return tieLine; return tieLine;
} }
nextTieLine = TieLineCollection.Default.FirstOrDefault(tl => tl.DestinationPort.Key == tieLine.SourcePort.Key && tl.DestinationPort.ParentDevice.Key == tieLine.SourcePort.ParentDevice.Key ); nextTieLine = TieLineCollection.Default.FirstOrDefault(tl =>
tl.DestinationPort.Key == tieLine.SourcePort.Key
&& tl.DestinationPort.ParentDevice.Key == tieLine.SourcePort.ParentDevice.Key
);
if (nextTieLine != null) if (nextTieLine != null)
{ {
return GetRootTieLine(nextTieLine); return GetRootTieLine(nextTieLine);
} }
} catch (Exception ex) }
catch (Exception ex)
{ {
Debug.LogMessage(ex, "Error walking tieLines: {Exception}", this, ex); Debug.LogMessage(ex, "Error walking tieLines: {Exception}", this, ex);
return null; return null;

View File

@@ -61,13 +61,18 @@ namespace PepperDash.Essentials.Devices.Common.DSP
/// <summary> /// <summary>
/// Base class for DSP control points /// Base class for DSP control points
/// </summary> /// </summary>
public abstract class DspControlPoint : IKeyed public abstract class DspControlPoint : IKeyName
{ {
/// <summary> /// <summary>
/// Gets or sets the Key /// Gets or sets the Key
/// </summary> /// </summary>
public string Key { get; } public string Key { get; }
/// <summary>
/// Gets or sets the Name
/// </summary>
public string Name { get; private set; }
/// <summary> /// <summary>
/// Initializes a new instance of the DspControlPoint class /// Initializes a new instance of the DspControlPoint class
/// </summary> /// </summary>

View File

@@ -1,15 +1,27 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Timers;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using PepperDash.Core; using PepperDash.Core;
using PepperDash.Core.Logging;
using PepperDash.Essentials.Core; using PepperDash.Essentials.Core;
using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.Config;
using PepperDash.Essentials.Core.CrestronIO; using PepperDash.Essentials.Core.CrestronIO;
using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Core.DeviceTypeInterfaces;
using Serilog.Events; using PepperDash.Essentials.Devices.Common.Displays;
namespace PepperDash.Essentials.Devices.Common.Shades namespace PepperDash.Essentials.Devices.Common.Shades
{ {
/// <summary>
/// Enumeration for requested state
/// </summary>
enum RequestedState
{
None,
Raise,
Lower
}
/// <summary> /// <summary>
/// Controls a single shade using three relays /// Controls a single shade using three relays
/// </summary> /// </summary>
@@ -20,11 +32,16 @@ namespace PepperDash.Essentials.Devices.Common.Shades
readonly ScreenLiftRelaysConfig LowerRelayConfig; readonly ScreenLiftRelaysConfig LowerRelayConfig;
readonly ScreenLiftRelaysConfig LatchedRelayConfig; readonly ScreenLiftRelaysConfig LatchedRelayConfig;
Displays.DisplayBase DisplayDevice; DisplayBase DisplayDevice;
ISwitchedOutput RaiseRelay; ISwitchedOutput RaiseRelay;
ISwitchedOutput LowerRelay; ISwitchedOutput LowerRelay;
ISwitchedOutput LatchedRelay; ISwitchedOutput LatchedRelay;
private bool _isMoving;
private RequestedState _requestedState;
private RequestedState _currentMovement;
private Timer _movementTimer;
/// <summary> /// <summary>
/// Gets or sets the InUpPosition /// Gets or sets the InUpPosition
/// </summary> /// </summary>
@@ -80,6 +97,11 @@ namespace PepperDash.Essentials.Devices.Common.Shades
IsInUpPosition = new BoolFeedback("isInUpPosition", () => _isInUpPosition); IsInUpPosition = new BoolFeedback("isInUpPosition", () => _isInUpPosition);
// Initialize movement timer for reuse
_movementTimer = new Timer();
_movementTimer.Elapsed += OnMovementComplete;
_movementTimer.AutoReset = false;
switch (Mode) switch (Mode)
{ {
case eScreenLiftControlMode.momentary: case eScreenLiftControlMode.momentary:
@@ -129,25 +151,25 @@ namespace PepperDash.Essentials.Devices.Common.Shades
{ {
case eScreenLiftControlMode.momentary: case eScreenLiftControlMode.momentary:
{ {
Debug.LogMessage(LogEventLevel.Debug, this, $"Getting relays for {Mode}"); this.LogDebug("Getting relays for {mode}", Mode);
RaiseRelay = GetSwitchedOutputFromDevice(RaiseRelayConfig.DeviceKey); RaiseRelay = GetSwitchedOutputFromDevice(RaiseRelayConfig.DeviceKey);
LowerRelay = GetSwitchedOutputFromDevice(LowerRelayConfig.DeviceKey); LowerRelay = GetSwitchedOutputFromDevice(LowerRelayConfig.DeviceKey);
break; break;
} }
case eScreenLiftControlMode.latched: case eScreenLiftControlMode.latched:
{ {
Debug.LogMessage(LogEventLevel.Debug, this, $"Getting relays for {Mode}"); this.LogDebug("Getting relays for {mode}", Mode);
LatchedRelay = GetSwitchedOutputFromDevice(LatchedRelayConfig.DeviceKey); LatchedRelay = GetSwitchedOutputFromDevice(LatchedRelayConfig.DeviceKey);
break; break;
} }
} }
Debug.LogMessage(LogEventLevel.Debug, this, $"Getting display with key {DisplayDeviceKey}"); this.LogDebug("Getting display with key {displayKey}", DisplayDeviceKey);
DisplayDevice = GetDisplayBaseFromDevice(DisplayDeviceKey); DisplayDevice = GetDisplayBaseFromDevice(DisplayDeviceKey);
if (DisplayDevice != null) if (DisplayDevice != null)
{ {
Debug.LogMessage(LogEventLevel.Debug, this, $"Subscribing to {DisplayDeviceKey} feedbacks"); this.LogDebug("Subscribing to {displayKey} feedbacks", DisplayDeviceKey);
DisplayDevice.IsWarmingUpFeedback.OutputChange += IsWarmingUpFeedback_OutputChange; DisplayDevice.IsWarmingUpFeedback.OutputChange += IsWarmingUpFeedback_OutputChange;
DisplayDevice.IsCoolingDownFeedback.OutputChange += IsCoolingDownFeedback_OutputChange; DisplayDevice.IsCoolingDownFeedback.OutputChange += IsCoolingDownFeedback_OutputChange;
@@ -163,22 +185,49 @@ namespace PepperDash.Essentials.Devices.Common.Shades
{ {
if (RaiseRelay == null && LatchedRelay == null) return; if (RaiseRelay == null && LatchedRelay == null) return;
Debug.LogMessage(LogEventLevel.Debug, this, $"Raising {Type}"); this.LogDebug("Raise called for {type}", Type);
// If device is moving, bank the command
if (_isMoving)
{
this.LogDebug("Device is moving, banking Raise command");
_requestedState = RequestedState.Raise;
return;
}
this.LogDebug("Raising {type}", Type);
switch (Mode) switch (Mode)
{ {
case eScreenLiftControlMode.momentary: case eScreenLiftControlMode.momentary:
{ {
PulseOutput(RaiseRelay, RaiseRelayConfig.PulseTimeInMs); PulseOutput(RaiseRelay, RaiseRelayConfig.PulseTimeInMs);
// Set moving flag and start timer if movement time is configured
if (RaiseRelayConfig.MoveTimeInMs > 0)
{
_isMoving = true;
_currentMovement = RequestedState.Raise;
if (_movementTimer.Enabled)
{
_movementTimer.Stop();
}
_movementTimer.Interval = RaiseRelayConfig.MoveTimeInMs;
_movementTimer.Start();
}
else
{
InUpPosition = true;
}
break; break;
} }
case eScreenLiftControlMode.latched: case eScreenLiftControlMode.latched:
{ {
LatchedRelay.Off(); LatchedRelay.Off();
InUpPosition = true;
break; break;
} }
} }
InUpPosition = true;
} }
/// <summary> /// <summary>
@@ -188,59 +237,145 @@ namespace PepperDash.Essentials.Devices.Common.Shades
{ {
if (LowerRelay == null && LatchedRelay == null) return; if (LowerRelay == null && LatchedRelay == null) return;
Debug.LogMessage(LogEventLevel.Debug, this, $"Lowering {Type}"); this.LogDebug("Lower called for {type}", Type);
// If device is moving, bank the command
if (_isMoving)
{
this.LogDebug("Device is moving, banking Lower command");
_requestedState = RequestedState.Lower;
return;
}
this.LogDebug("Lowering {type}", Type);
switch (Mode) switch (Mode)
{ {
case eScreenLiftControlMode.momentary: case eScreenLiftControlMode.momentary:
{ {
PulseOutput(LowerRelay, LowerRelayConfig.PulseTimeInMs); PulseOutput(LowerRelay, LowerRelayConfig.PulseTimeInMs);
// Set moving flag and start timer if movement time is configured
if (LowerRelayConfig.MoveTimeInMs > 0)
{
_isMoving = true;
_currentMovement = RequestedState.Lower;
if (_movementTimer.Enabled)
{
_movementTimer.Stop();
}
_movementTimer.Interval = LowerRelayConfig.MoveTimeInMs;
_movementTimer.Start();
}
else
{
InUpPosition = false;
}
break; break;
} }
case eScreenLiftControlMode.latched: case eScreenLiftControlMode.latched:
{ {
LatchedRelay.On(); LatchedRelay.On();
InUpPosition = false;
break; break;
} }
} }
InUpPosition = false;
} }
void PulseOutput(ISwitchedOutput output, int pulseTime) private void DisposeMovementTimer()
{ {
output.On(); if (_movementTimer != null)
CTimer pulseTimer = new CTimer(new CTimerCallbackFunction((o) => output.Off()), pulseTime); {
_movementTimer.Stop();
_movementTimer.Elapsed -= OnMovementComplete;
_movementTimer.Dispose();
_movementTimer = null;
}
} }
/// <summary> /// <summary>
/// Attempts to get the port on teh specified device from config /// Called when movement timer completes
/// </summary> /// </summary>
/// <param name="relayKey"></param> private void OnMovementComplete(object sender, ElapsedEventArgs e)
/// <returns></returns>
ISwitchedOutput GetSwitchedOutputFromDevice(string relayKey)
{ {
var portDevice = DeviceManager.GetDeviceForKey(relayKey); this.LogDebug("Movement complete");
// Update position based on completed movement
if (_currentMovement == RequestedState.Raise)
{
InUpPosition = true;
}
else if (_currentMovement == RequestedState.Lower)
{
InUpPosition = false;
}
_isMoving = false;
_currentMovement = RequestedState.None;
// Execute banked command if one exists
if (_requestedState != RequestedState.None)
{
this.LogDebug("Executing next command: {command}", _requestedState);
var commandToExecute = _requestedState;
_requestedState = RequestedState.None;
// Check if current state matches what the banked command would do and execute if different
switch (commandToExecute)
{
case RequestedState.Raise:
Raise();
break;
case RequestedState.Lower:
Lower();
break;
}
}
}
private void PulseOutput(ISwitchedOutput output, int pulseTime)
{
output.On();
var timer = new Timer(pulseTime)
{
AutoReset = false
};
timer.Elapsed += (sender, e) =>
{
output.Off();
timer.Dispose();
};
timer.Start();
}
private ISwitchedOutput GetSwitchedOutputFromDevice(string relayKey)
{
var portDevice = DeviceManager.GetDeviceForKey<ISwitchedOutput>(relayKey);
if (portDevice != null) if (portDevice != null)
{ {
return portDevice as ISwitchedOutput; return portDevice;
} }
else else
{ {
Debug.LogMessage(LogEventLevel.Debug, this, "Error: Unable to get relay device with key '{0}'", relayKey); this.LogWarning("Error: Unable to get relay device with key '{relayKey}'", relayKey);
return null; return null;
} }
} }
Displays.DisplayBase GetDisplayBaseFromDevice(string displayKey) private DisplayBase GetDisplayBaseFromDevice(string displayKey)
{ {
var displayDevice = DeviceManager.GetDeviceForKey(displayKey); var displayDevice = DeviceManager.GetDeviceForKey<DisplayBase>(displayKey);
if (displayDevice != null) if (displayDevice != null)
{ {
return displayDevice as Displays.DisplayBase; return displayDevice;
} }
else else
{ {
Debug.LogMessage(LogEventLevel.Debug, this, "Error: Unable to get display device with key '{0}'", displayKey); this.LogWarning("Error: Unable to get display device with key '{displayKey}'", displayKey);
return null; return null;
} }
} }
@@ -248,7 +383,7 @@ namespace PepperDash.Essentials.Devices.Common.Shades
} }
/// <summary> /// <summary>
/// Represents a ScreenLiftControllerFactory /// Factory for ScreenLiftController devices
/// </summary> /// </summary>
public class ScreenLiftControllerFactory : EssentialsDeviceFactory<RelayControlledShade> public class ScreenLiftControllerFactory : EssentialsDeviceFactory<RelayControlledShade>
{ {
@@ -260,14 +395,11 @@ namespace PepperDash.Essentials.Devices.Common.Shades
TypeNames = new List<string>() { "screenliftcontroller" }; TypeNames = new List<string>() { "screenliftcontroller" };
} }
/// <summary>
/// BuildDevice method
/// </summary>
/// <inheritdoc /> /// <inheritdoc />
public override EssentialsDevice BuildDevice(DeviceConfig dc) public override EssentialsDevice BuildDevice(DeviceConfig dc)
{ {
Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Generic Comm Device"); Debug.LogDebug("Factory Attempting to create new ScreenLiftController Device");
var props = Newtonsoft.Json.JsonConvert.DeserializeObject<ScreenLiftControllerConfigProperties>(dc.Properties.ToString()); var props = dc.Properties.ToObject<ScreenLiftControllerConfigProperties>();
return new ScreenLiftController(dc.Key, dc.Name, props); return new ScreenLiftController(dc.Key, dc.Name, props);
} }

View File

@@ -18,5 +18,11 @@ namespace PepperDash.Essentials.Devices.Common.Shades
/// </summary> /// </summary>
[JsonProperty("pulseTimeInMs")] [JsonProperty("pulseTimeInMs")]
public int PulseTimeInMs { get; set; } public int PulseTimeInMs { get; set; }
/// <summary>
/// Gets or sets the MoveTimeInMs - time in milliseconds for the movement to complete
/// </summary>
[JsonProperty("moveTimeInMs")]
public int MoveTimeInMs { get; set; }
} }
} }

View File

@@ -1,7 +1,10 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using PepperDash.Core; using PepperDash.Core;
using PepperDash.Core.Logging;
using PepperDash.Essentials.Core; using PepperDash.Essentials.Core;
using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.Config;
using PepperDash.Essentials.Core.Routing;
using Serilog.Events; using Serilog.Events;
namespace PepperDash.Essentials.Devices.Common.Generic namespace PepperDash.Essentials.Devices.Common.Generic
@@ -9,8 +12,17 @@ namespace PepperDash.Essentials.Devices.Common.Generic
/// <summary> /// <summary>
/// Represents a GenericSink /// Represents a GenericSink
/// </summary> /// </summary>
public class GenericSink : EssentialsDevice, IRoutingSinkWithSwitchingWithInputPort public class GenericSink : EssentialsDevice, IRoutingSinkWithSwitchingWithInputPort, ICurrentSources
{ {
/// <inheritdoc/>
public Dictionary<eRoutingSignalType, SourceListItem> CurrentSources { get; private set; }
/// <inheritdoc/>
public Dictionary<eRoutingSignalType, string> CurrentSourceKeys { get; private set; }
/// <inheritdoc />
public event EventHandler CurrentSourcesChanged;
/// <summary> /// <summary>
/// Initializes a new instance of the GenericSink class /// Initializes a new instance of the GenericSink class
/// </summary> /// </summary>
@@ -23,6 +35,49 @@ namespace PepperDash.Essentials.Devices.Common.Generic
var inputPort = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo | eRoutingSignalType.SecondaryAudio, eRoutingPortConnectionType.Hdmi, null, this); var inputPort = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo | eRoutingSignalType.SecondaryAudio, eRoutingPortConnectionType.Hdmi, null, this);
InputPorts.Add(inputPort); InputPorts.Add(inputPort);
CurrentSources = new Dictionary<eRoutingSignalType, SourceListItem>
{
{ eRoutingSignalType.Audio, null },
{ eRoutingSignalType.Video, null },
};
CurrentSourceKeys = new Dictionary<eRoutingSignalType, string>
{
{ eRoutingSignalType.Audio, string.Empty },
{ eRoutingSignalType.Video, string.Empty },
};
}
/// <inheritdoc />
public void SetCurrentSource(eRoutingSignalType signalType, string sourceListKey, SourceListItem sourceListItem)
{
foreach (eRoutingSignalType type in Enum.GetValues(typeof(eRoutingSignalType)))
{
var flagValue = Convert.ToInt32(type);
// Skip if flagValue is 0 or not a power of two (i.e., not a single-bit flag).
// (flagValue & (flagValue - 1)) != 0 checks if more than one bit is set.
if (flagValue == 0 || (flagValue & (flagValue - 1)) != 0)
{
this.LogDebug("Skipping {type}", type);
continue;
}
this.LogDebug("setting {type}", type);
if (signalType.HasFlag(type))
{
UpdateCurrentSources(type, sourceListKey, sourceListItem);
}
}
// Raise the CurrentSourcesChanged event
CurrentSourcesChanged?.Invoke(this, EventArgs.Empty);
}
private void UpdateCurrentSources(eRoutingSignalType signalType, string sourceListKey, SourceListItem sourceListItem)
{
CurrentSources[signalType] = sourceListItem;
CurrentSourceKeys[signalType] = sourceListKey;
} }
/// <summary> /// <summary>
@@ -73,7 +128,7 @@ namespace PepperDash.Essentials.Devices.Common.Generic
/// <inheritdoc /> /// <inheritdoc />
public void ExecuteSwitch(object inputSelector) public void ExecuteSwitch(object inputSelector)
{ {
throw new System.NotImplementedException(); this.LogDebug("GenericSink Executing Switch to: {inputSelector}", inputSelector);
} }
} }

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@@ -39,10 +40,14 @@ namespace PepperDash.Essentials.AppServer.Messengers
sourceDevice.CurrentSourcesChanged += (sender, e) => sourceDevice.CurrentSourcesChanged += (sender, e) =>
{ {
// need to copy the dictionaries to avoid enumeration issues
var currentSourceKeys = sourceDevice.CurrentSourceKeys.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
var currentSources = sourceDevice.CurrentSources.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
PostStatusMessage(JToken.FromObject(new PostStatusMessage(JToken.FromObject(new
{ {
currentSourceKeys = sourceDevice.CurrentSourceKeys, currentSourceKeys,
currentSources = sourceDevice.CurrentSources currentSources,
})); }));
}; };
} }

View File

@@ -22,7 +22,7 @@ namespace PepperDash.Essentials.AppServer.Messengers
/// <param name="messagePath">The message path.</param> /// <param name="messagePath">The message path.</param>
/// <param name="device">The device.</param> /// <param name="device">The device.</param>
public DeviceVolumeMessenger(string key, string messagePath, IBasicVolumeControls device) public DeviceVolumeMessenger(string key, string messagePath, IBasicVolumeControls device)
: base(key, messagePath, device as IKeyName) : base(key, messagePath, device)
{ {
this.device = device; this.device = device;
} }
@@ -130,34 +130,33 @@ namespace PepperDash.Essentials.AppServer.Messengers
feedback.MuteFeedback.OutputChange += (sender, args) => feedback.MuteFeedback.OutputChange += (sender, args) =>
{ {
PostStatusMessage(JToken.FromObject( var message = new VolumeStateMessage
new {
{ Volume = new Volume
volume = new {
{ Muted = args.BoolValue
muted = args.BoolValue }
} };
})
); PostStatusMessage(JToken.FromObject(message));
}; };
feedback.VolumeLevelFeedback.OutputChange += (sender, args) => feedback.VolumeLevelFeedback.OutputChange += (sender, args) =>
{ {
var rawValue = ""; var message = new VolumeStateMessage
if (feedback is IBasicVolumeWithFeedbackAdvanced volumeAdvanced)
{ {
rawValue = volumeAdvanced.RawVolumeLevel.ToString(); Volume = new Volume
}
var message = new
{
volume = new
{ {
level = args.IntValue, Level = args.IntValue,
rawValue
} }
}; };
if (device is IBasicVolumeWithFeedbackAdvanced volumeAdvanced)
{
message.Volume.RawValue = volumeAdvanced.RawVolumeLevel.ToString();
message.Volume.Units = volumeAdvanced.Units;
}
PostStatusMessage(JToken.FromObject(message)); PostStatusMessage(JToken.FromObject(message));
}; };
} }

View File

@@ -62,6 +62,14 @@ namespace PepperDash.Essentials.AppServer.Messengers
inputs = matrixDevice.InputSlots.ToDictionary(kvp => kvp.Key, kvp => new RoutingInput(kvp.Value)) inputs = matrixDevice.InputSlots.ToDictionary(kvp => kvp.Key, kvp => new RoutingInput(kvp.Value))
})); }));
}; };
inputSlot.IsOnline.OutputChange += (sender, args) =>
{
PostStatusMessage(JToken.FromObject(new
{
inputs = matrixDevice.InputSlots.ToDictionary(kvp => kvp.Key, kvp => new RoutingInput(kvp.Value))
}));
};
} }
} }

View File

@@ -194,7 +194,6 @@ namespace PepperDash.Essentials.AppServer.Messengers
{ {
if (!enableMessengerSubscriptions) if (!enableMessengerSubscriptions)
{ {
this.LogWarning("Messenger subscriptions not enabled");
return; return;
} }
@@ -218,7 +217,6 @@ namespace PepperDash.Essentials.AppServer.Messengers
{ {
if (!enableMessengerSubscriptions) if (!enableMessengerSubscriptions)
{ {
this.LogWarning("Messenger subscriptions not enabled");
return; return;
} }
@@ -238,7 +236,7 @@ namespace PepperDash.Essentials.AppServer.Messengers
return; return;
} }
this.LogInformation("Client with ID {clientId} unsubscribed", clientId); this.LogDebug("Client with ID {clientId} unsubscribed", clientId);
} }
/// <summary> /// <summary>
@@ -272,7 +270,8 @@ namespace PepperDash.Essentials.AppServer.Messengers
} }
catch (Exception ex) catch (Exception ex)
{ {
this.LogError(ex, "Exception posting status message for {messagePath} to {clientId}", MessagePath, clientId ?? "all clients"); this.LogError("Exception posting status message for {messagePath} to {clientId}: {message}", MessagePath, clientId ?? "all clients", ex.Message);
this.LogDebug(ex, "Stack trace: ");
} }
} }
@@ -301,7 +300,8 @@ namespace PepperDash.Essentials.AppServer.Messengers
} }
catch (Exception ex) catch (Exception ex)
{ {
this.LogError(ex, "Exception posting status message for {type} to {clientId}", type, clientId ?? "all clients"); this.LogError("Exception posting status message for {type} to {clientId}: {message}", type, clientId ?? "all clients", ex.Message);
this.LogDebug(ex, "Stack trace: ");
} }
} }

View File

@@ -1312,6 +1312,11 @@ namespace PepperDash.Essentials
/// <inheritdoc /> /// <inheritdoc />
public override void Initialize() public override void Initialize()
{ {
if (!Config.EnableMessengerSubscriptions)
{
this.LogWarning("Messenger subscriptions disabled. add \"enableMessengerSubscriptions\": true to config for {key} to enable.", Key);
}
foreach (var messenger in _messengers) foreach (var messenger in _messengers)
{ {
try try
@@ -1743,7 +1748,7 @@ namespace PepperDash.Essentials
var clientNo = 1; var clientNo = 1;
foreach (var clientContext in _directServer.UiClientContexts) foreach (var clientContext in _directServer.UiClientContexts)
{ {
var clients = _directServer.UiClients.Values.Where(c => c.Token == clientContext.Value.Token.Token); var clients = _directServer.UiClients.Values.Where(c => c.TokenKey == clientContext.Key);
CrestronConsole.ConsoleCommandResponse( CrestronConsole.ConsoleCommandResponse(
$"\r\nClient {clientNo}:\r\n" + $"\r\nClient {clientNo}:\r\n" +

View File

@@ -1,3 +1,4 @@
using System.Threading;
using PepperDash.Core; using PepperDash.Core;
using PepperDash.Core.Logging; using PepperDash.Core.Logging;
using WebSocketSharp; using WebSocketSharp;
@@ -12,13 +13,12 @@ namespace PepperDash.Essentials
private static int nextClientId = 0; private static int nextClientId = 0;
/// <summary> /// <summary>
/// Get the next unique client ID /// Get the next unique client ID (thread-safe)
/// </summary> /// </summary>
/// <returns>Client ID</returns> /// <returns>Client ID</returns>
public static int GetNextClientId() public static int GetNextClientId()
{ {
nextClientId++; return Interlocked.Increment(ref nextClientId);
return nextClientId;
} }
/// <summary> /// <summary>
/// Converts a WebSocketServer LogData object to Essentials logging calls. /// Converts a WebSocketServer LogData object to Essentials logging calls.

View File

@@ -64,6 +64,12 @@ namespace PepperDash.Essentials.WebSocketServer
[JsonProperty("userAppUrl")] [JsonProperty("userAppUrl")]
public string UserAppUrl { get; set; } public string UserAppUrl { get; set; }
/// <summary>
/// Gets or sets the WebSocketUrl with clientId query parameter
/// </summary>
[JsonProperty("webSocketUrl")]
public string WebSocketUrl { get; set; }
/// <summary> /// <summary>
/// Gets or sets the EnableDebug /// Gets or sets the EnableDebug

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
@@ -59,12 +60,25 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary> /// </summary>
public Dictionary<string, UiClientContext> UiClientContexts { get; private set; } public Dictionary<string, UiClientContext> UiClientContexts { get; private set; }
private readonly Dictionary<string, UiClient> uiClients = new Dictionary<string, UiClient>(); private readonly ConcurrentDictionary<string, UiClient> uiClients = new ConcurrentDictionary<string, UiClient>();
/// <summary>
/// Stores pending client registrations using composite key: token-clientId
/// This ensures the correct client ID is matched even when connections establish out of order
/// </summary>
private readonly ConcurrentDictionary<string, string> pendingClientRegistrations = new ConcurrentDictionary<string, string>();
/// <summary>
/// Stores pending client registrations with timestamp for legacy clients
/// Key is token, Value is list of (clientId, timestamp) tuples
/// Most recent registration is used to handle duplicate join requests
/// </summary>
private readonly ConcurrentDictionary<string, ConcurrentBag<(string clientId, DateTime timestamp)>> legacyClientRegistrations = new ConcurrentDictionary<string, ConcurrentBag<(string, DateTime)>>();
/// <summary> /// <summary>
/// Gets the collection of UI clients /// Gets the collection of UI clients
/// </summary> /// </summary>
public ReadOnlyDictionary<string, UiClient> UiClients => new ReadOnlyDictionary<string, UiClient>(uiClients); public IReadOnlyDictionary<string, UiClient> UiClients => uiClients;
private readonly MobileControlSystemController _parent; private readonly MobileControlSystemController _parent;
@@ -723,23 +737,155 @@ namespace PepperDash.Essentials.WebSocketServer
private UiClient BuildUiClient(string roomKey, JoinToken token, string key) private UiClient BuildUiClient(string roomKey, JoinToken token, string key)
{ {
var c = new UiClient($"uiclient-{key}-{roomKey}-{token.Id}", token.Id, token.Token, token.TouchpanelKey); // Get the most recent unused clientId for this token (legacy support)
this.LogInformation("Constructing UiClient with key {key} and ID {id}", key, token.Id); // New clients will override this ID in OnOpen with the validated query parameter value
var clientId = "pending";
if (legacyClientRegistrations.TryGetValue(key, out var registrations))
{
// Get most recent registration
var sorted = registrations.OrderByDescending(r => r.timestamp).ToList();
if (sorted.Any())
{
clientId = sorted.First().clientId;
// Remove it from the bag
var newBag = new ConcurrentBag<(string, DateTime)>(sorted.Skip(1));
legacyClientRegistrations.TryUpdate(key, newBag, registrations);
this.LogVerbose("Assigned most recent legacy clientId {clientId} for token {token}", clientId, key);
}
}
var c = new UiClient($"uiclient-{key}-{roomKey}-{clientId}", clientId, token.Token, token.TouchpanelKey);
this.LogInformation("Constructing UiClient with key {key} and temporary ID (will be set from query param)", key);
c.Controller = _parent; c.Controller = _parent;
c.RoomKey = roomKey; c.RoomKey = roomKey;
c.TokenKey = key; // Store the URL token key for filtering
c.Server = this; // Give UiClient access to server for ID registration
if (uiClients.ContainsKey(token.Id)) // Don't add to uiClients yet - will be added in OnOpen after ID is set from query param
c.ConnectionClosed += (o, a) =>
{ {
this.LogWarning("removing client with duplicate id {id}", token.Id); uiClients.TryRemove(a.ClientId, out _);
uiClients.Remove(token.Id); // Clean up any pending registrations for this token
} var keysToRemove = pendingClientRegistrations.Keys
uiClients.Add(token.Id, c); .Where(k => k.StartsWith($"{key}-"))
// UiClients[key].SetClient(c); .ToList();
c.ConnectionClosed += (o, a) => uiClients.Remove(a.ClientId); foreach (var k in keysToRemove)
token.Id = null; {
pendingClientRegistrations.TryRemove(k, out _);
}
// Clean up legacy registrations if empty
if (legacyClientRegistrations.TryGetValue(key, out var legacyBag) && legacyBag.IsEmpty)
{
legacyClientRegistrations.TryRemove(key, out _);
}
};
return c; return c;
} }
/// <summary>
/// Registers a UiClient with its validated client ID after WebSocket connection
/// </summary>
/// <param name="client">The UiClient to register</param>
/// <param name="clientId">The validated client ID</param>
/// <param name="tokenKey">The token key for validation</param>
/// <returns>True if registration successful, false if validation failed</returns>
public bool RegisterUiClient(UiClient client, string clientId, string tokenKey)
{
var registrationKey = $"{tokenKey}-{clientId}";
// Verify this clientId was generated during a join request for this token
if (!pendingClientRegistrations.TryRemove(registrationKey, out _))
{
this.LogWarning("Client attempted to connect with unregistered or expired clientId {clientId} for token {token}", clientId, tokenKey);
return false;
}
// Registration is valid - add to active clients
uiClients.AddOrUpdate(clientId, client, (id, existingClient) =>
{
this.LogWarning("Replacing existing client with duplicate id {id}", id);
return client;
});
this.LogInformation("Successfully registered UiClient with ID {clientId} for token {token}", clientId, tokenKey);
return true;
}
/// <summary>
/// Updates a client's ID when a mismatch is detected between stored ID and message ID
/// </summary>
/// <param name="oldClientId">The current/old client ID</param>
/// <param name="newClientId">The new client ID from the message</param>
/// <param name="tokenKey">The token key for validation</param>
/// <returns>True if update successful, false otherwise</returns>
public bool UpdateClientId(string oldClientId, string newClientId, string tokenKey)
{
if (string.IsNullOrEmpty(oldClientId) || string.IsNullOrEmpty(newClientId))
{
this.LogWarning("Cannot update client ID with null or empty values");
return false;
}
if (oldClientId == newClientId)
{
return true; // No update needed
}
// Verify the new clientId was registered for this token
var registrationKey = $"{tokenKey}-{newClientId}";
if (!pendingClientRegistrations.TryRemove(registrationKey, out _))
{
this.LogWarning("Cannot update to unregistered clientId {newClientId} for token {token}", newClientId, tokenKey);
return false;
}
// Get the existing client
if (!uiClients.TryRemove(oldClientId, out var client))
{
this.LogWarning("Cannot find client with old ID {oldClientId}", oldClientId);
return false;
}
// Update the client's ID
client.UpdateId(newClientId);
// Re-add with new ID
if (!uiClients.TryAdd(newClientId, client))
{
// If add fails, try to restore old entry
uiClients.TryAdd(oldClientId, client);
client.UpdateId(oldClientId);
this.LogError("Failed to update client ID from {oldClientId} to {newClientId}", oldClientId, newClientId);
return false;
}
this.LogInformation("Successfully updated client ID from {oldClientId} to {newClientId}", oldClientId, newClientId);
return true;
}
/// <summary>
/// Registers a UiClient using legacy flow (for backwards compatibility with older clients)
/// </summary>
/// <param name="client">The UiClient to register</param>
public void RegisterLegacyUiClient(UiClient client)
{
if (string.IsNullOrEmpty(client.Id))
{
this.LogError("Cannot register client with null or empty ID");
return;
}
uiClients.AddOrUpdate(client.Id, client, (id, existingClient) =>
{
this.LogWarning("Replacing existing client with duplicate id {id} (legacy flow)", id);
return client;
});
this.LogInformation("Successfully registered UiClient with ID {clientId} using legacy flow", client.Id);
}
/// <summary> /// <summary>
/// Prints out the session data for each path /// Prints out the session data for each path
/// </summary> /// </summary>
@@ -1046,10 +1192,23 @@ namespace PepperDash.Essentials.WebSocketServer
}); });
} }
// Generate a client ID for this join request
var clientId = $"{Utilities.GetNextClientId()}"; var clientId = $"{Utilities.GetNextClientId()}";
clientContext.Token.Id = clientId; var now = DateTime.UtcNow;
this.LogVerbose("Assigning ClientId: {clientId}", clientId); // Store in pending registrations for new clients that send clientId via query param
var registrationKey = $"{token}-{clientId}";
pendingClientRegistrations.TryAdd(registrationKey, clientId);
// For legacy clients, store with timestamp instead of FIFO queue
var legacyBag = legacyClientRegistrations.GetOrAdd(token, _ => new ConcurrentBag<(string, DateTime)>());
legacyBag.Add((clientId, now));
this.LogVerbose("Assigning ClientId: {clientId} for token: {token} at {timestamp}", clientId, token, now);
// Construct WebSocket URL with clientId query parameter
var wsProtocol = "ws";
var wsUrl = $"{wsProtocol}://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{Port}{_wsPath}{token}?clientId={clientId}";
// Construct the response object // Construct the response object
JoinResponse jRes = new JoinResponse JoinResponse jRes = new JoinResponse
@@ -1064,6 +1223,7 @@ namespace PepperDash.Essentials.WebSocketServer
UserAppUrl = string.Format("http://{0}:{1}/mc/app", UserAppUrl = string.Format("http://{0}:{1}/mc/app",
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0), CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
Port), Port),
WebSocketUrl = wsUrl,
EnableDebug = false, EnableDebug = false,
DeviceInterfaceSupport = deviceInterfaces DeviceInterfaceSupport = deviceInterfaces
}; };

View File

@@ -26,11 +26,25 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary> /// </summary>
public string Id { get; private set; } public string Id { get; private set; }
/// <summary>
/// Updates the client ID - only accessible from within the assembly (e.g., by the server)
/// </summary>
/// <param name="newId">The new client ID</param>
internal void UpdateId(string newId)
{
Id = newId;
}
/// <summary> /// <summary>
/// Token associated with this client /// Token associated with this client
/// </summary> /// </summary>
public string Token { get; private set; } public string Token { get; private set; }
/// <summary>
/// The URL token key used to connect (from UiClientContexts dictionary key)
/// </summary>
public string TokenKey { get; set; }
/// <summary> /// <summary>
/// Touchpanel Key associated with this client /// Touchpanel Key associated with this client
/// </summary> /// </summary>
@@ -41,6 +55,11 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary> /// </summary>
public MobileControlSystemController Controller { get; set; } public MobileControlSystemController Controller { get; set; }
/// <summary>
/// Gets or sets the server instance for client registration
/// </summary>
public MobileControlWebsocketServer Server { get; set; }
/// <summary> /// <summary>
/// Gets or sets the room key that this client is associated with /// Gets or sets the room key that this client is associated with
/// </summary> /// </summary>
@@ -99,6 +118,50 @@ namespace PepperDash.Essentials.WebSocketServer
Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this); Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this);
Log.Level = LogLevel.Trace; Log.Level = LogLevel.Trace;
// Get clientId from query parameter
var queryString = Context.QueryString;
var clientId = queryString["clientId"];
if (!string.IsNullOrEmpty(clientId))
{
// New behavior: Validate and register with the server using provided clientId
if (Server == null || !Server.RegisterUiClient(this, clientId, TokenKey))
{
this.LogError("Failed to register client with ID {clientId}. Invalid or expired registration.", clientId);
Context.WebSocket.Close(CloseStatusCode.PolicyViolation, "Invalid or expired clientId");
return;
}
// Update this client's ID to the validated one
Id = clientId;
Key = $"uiclient-{TokenKey}-{RoomKey}-{clientId}";
this.LogInformation("Client {clientId} successfully connected and registered (new flow)", clientId);
}
else
{
// Legacy behavior: Use clientId from Token.Id (generated in HandleJoinRequest)
this.LogInformation("Client connected without clientId query parameter. Using legacy registration flow.");
// Id is already set from Token in constructor, use it
if (string.IsNullOrEmpty(Id))
{
this.LogError("Legacy client has no ID from token. Connection will be closed.");
Context.WebSocket.Close(CloseStatusCode.PolicyViolation, "No client ID available");
return;
}
Key = $"uiclient-{TokenKey}-{RoomKey}-{Id}";
// Register directly to active clients (legacy flow)
if (Server != null)
{
Server.RegisterLegacyUiClient(this);
}
this.LogInformation("Client {clientId} registered using legacy flow", Id);
}
if (Controller == null) if (Controller == null)
{ {
Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Controller is null"); Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Controller is null");

View File

@@ -662,6 +662,7 @@ namespace PepperDash.Essentials
if (jsonFiles.Length > 1) if (jsonFiles.Length > 1)
{ {
Debug.LogError("Multiple configuration files found in application directory: {@jsonFiles}", jsonFiles.Select(f => f.FullName).ToArray());
throw new Exception("Multiple configuration files found. Cannot continue."); throw new Exception("Multiple configuration files found. Cannot continue.");
} }