Compare commits

...

55 Commits

Author SHA1 Message Date
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
38 changed files with 1317 additions and 538 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
}
}
}

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;
@@ -60,8 +77,7 @@ 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;
@@ -115,7 +115,7 @@ namespace PepperDash.Essentials.Core
public static (RouteDescriptor, RouteDescriptor) GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source, eRoutingSignalType signalType, RoutingInputPort destinationPort, RoutingOutputPort sourcePort) public static (RouteDescriptor, RouteDescriptor) GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source, eRoutingSignalType signalType, RoutingInputPort destinationPort, RoutingOutputPort sourcePort)
{ {
// if it's a single signal type, find the route // if it's a single signal type, find the route
if (!signalType.HasFlag(eRoutingSignalType.AudioVideo) && if (!signalType.HasFlag(eRoutingSignalType.AudioVideo) &&
!(signalType.HasFlag(eRoutingSignalType.Video) && signalType.HasFlag(eRoutingSignalType.SecondaryAudio))) !(signalType.HasFlag(eRoutingSignalType.Video) && signalType.HasFlag(eRoutingSignalType.SecondaryAudio)))
{ {
var singleTypeRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, signalType); var singleTypeRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, signalType);
@@ -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);
} }
@@ -199,13 +200,13 @@ namespace PepperDash.Essentials.Core
Source = source, Source = source,
SourcePort = sourcePort, SourcePort = sourcePort,
SignalType = signalType SignalType = signalType
}; };
var coolingDevice = destination as IWarmingCooling; var coolingDevice = destination as IWarmingCooling;
//We already have a route request for this device, and it's a cooling device and is cooling //We already have a route request for this device, and it's a cooling device and is cooling
if (RouteRequests.TryGetValue(destination.Key, out RouteRequest existingRouteRequest) && coolingDevice != null && coolingDevice.IsCoolingDownFeedback.BoolValue == true) if (RouteRequests.TryGetValue(destination.Key, out RouteRequest existingRouteRequest) && coolingDevice != null && coolingDevice.IsCoolingDownFeedback.BoolValue == true)
{ {
coolingDevice.IsCoolingDownFeedback.OutputChange -= existingRouteRequest.HandleCooldown; coolingDevice.IsCoolingDownFeedback.OutputChange -= existingRouteRequest.HandleCooldown;
coolingDevice.IsCoolingDownFeedback.OutputChange += routeRequest.HandleCooldown; coolingDevice.IsCoolingDownFeedback.OutputChange += routeRequest.HandleCooldown;
@@ -219,7 +220,7 @@ namespace PepperDash.Essentials.Core
//New Request //New Request
if (coolingDevice != null && coolingDevice.IsCoolingDownFeedback.BoolValue == true) if (coolingDevice != null && coolingDevice.IsCoolingDownFeedback.BoolValue == true)
{ {
coolingDevice.IsCoolingDownFeedback.OutputChange += routeRequest.HandleCooldown; coolingDevice.IsCoolingDownFeedback.OutputChange += routeRequest.HandleCooldown;
RouteRequests.Add(destination.Key, routeRequest); RouteRequests.Add(destination.Key, routeRequest);
@@ -239,9 +240,9 @@ 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));
} }
/// <summary> /// <summary>
@@ -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,
Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Updating destination {destination} with inputPort {inputPort}", this, destination?.Key, inputPort?.Key); RoutingInputPort inputPort
)
{
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,32 +203,45 @@ 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;
} }
// 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,29 +251,45 @@ 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;
var sourceKey = sourceListItem.Key; var sourceKey = sourceListItem.Key;
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
{ {
@@ -221,7 +298,7 @@ namespace PepperDash.Essentials.Core.Routing
}; };
destination.CurrentSourceInfoKey = "$transient"; destination.CurrentSourceInfoKey = "$transient";
destination.CurrentSourceInfo = tempSourceListItem; destination.CurrentSourceInfo = tempSourceListItem;
return; return;
} }
@@ -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.");
} }