diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 00000000..79512ebe --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "csharpier": { + "version": "1.2.4", + "commands": [ + "csharpier" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IAudioZone.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IAudioZone.cs new file mode 100644 index 00000000..593c2ea8 --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IAudioZone.cs @@ -0,0 +1,10 @@ +namespace PepperDash.Essentials.Core +{ + /// + /// Defines minimum functionality for an audio zone + /// + public interface IAudioZone : IBasicVolumeWithFeedback + { + void SelectInput(ushort input); + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IAudioZones.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IAudioZones.cs new file mode 100644 index 00000000..19c43fd8 --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IAudioZones.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace PepperDash.Essentials.Core +{ + /// + /// Identifies a device that contains audio zones + /// + public interface IAudioZones : IRouting + { + Dictionary Zone { get; } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IBasicVolumeControls.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IBasicVolumeControls.cs new file mode 100644 index 00000000..3767fa34 --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IBasicVolumeControls.cs @@ -0,0 +1,27 @@ +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + /// + /// Defines minimal volume and mute control methods + /// + public interface IBasicVolumeControls : IKeyName + { + /// + /// Increases the volume + /// + /// Indicates whether the volume change is a press and hold action + void VolumeUp(bool pressRelease); + + /// + /// Decreases the volume + /// + /// Indicates whether the volume change is a press and hold action + void VolumeDown(bool pressRelease); + + /// + /// Toggles the mute state + /// + void MuteToggle(); + } +} diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IBasicVolumeWithFeedback.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IBasicVolumeWithFeedback.cs new file mode 100644 index 00000000..addee08a --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IBasicVolumeWithFeedback.cs @@ -0,0 +1,14 @@ +namespace PepperDash.Essentials.Core +{ + /// + /// Defines the contract for IBasicVolumeWithFeedback + /// + public interface IBasicVolumeWithFeedback : IBasicVolumeControls + { + BoolFeedback MuteFeedback { get; } + void MuteOn(); + void MuteOff(); + void SetVolume(ushort level); + IntFeedback VolumeLevelFeedback { get; } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IBasicVolumeWithFeedbackAdvanced.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IBasicVolumeWithFeedbackAdvanced.cs new file mode 100644 index 00000000..5f51ce02 --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IBasicVolumeWithFeedbackAdvanced.cs @@ -0,0 +1,12 @@ +namespace PepperDash.Essentials.Core +{ + /// + /// Defines the contract for IBasicVolumeWithFeedbackAdvanced + /// + public interface IBasicVolumeWithFeedbackAdvanced : IBasicVolumeWithFeedback + { + int RawVolumeLevel { get; } + + eVolumeLevelUnits Units { get; } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IFullAudioSettings.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IFullAudioSettings.cs new file mode 100644 index 00000000..5344dd6f --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IFullAudioSettings.cs @@ -0,0 +1,41 @@ +namespace PepperDash.Essentials.Core +{ + /// + /// Defines the contract for IFullAudioSettings + /// + 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; } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasCurrentVolumeControls.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasCurrentVolumeControls.cs new file mode 100644 index 00000000..e6b1d7a3 --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasCurrentVolumeControls.cs @@ -0,0 +1,17 @@ +using System; + +namespace PepperDash.Essentials.Core +{ + /// + /// Defines the contract for IHasCurrentVolumeControls + /// + public interface IHasCurrentVolumeControls + { + IBasicVolumeControls CurrentVolumeControls { get; } + event EventHandler CurrentVolumeDeviceChange; + + void SetDefaultLevels(); + + bool ZeroVolumeWhenSwtichingVolumeDevices { get; } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasMuteControl.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasMuteControl.cs new file mode 100644 index 00000000..4a89d8cf --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasMuteControl.cs @@ -0,0 +1,10 @@ +namespace PepperDash.Essentials.Core +{ + /// + /// Defines basic mute control methods + /// + public interface IHasMuteControl + { + void MuteToggle(); + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasMuteControlWithFeedback.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasMuteControlWithFeedback.cs new file mode 100644 index 00000000..5278541a --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasMuteControlWithFeedback.cs @@ -0,0 +1,12 @@ +namespace PepperDash.Essentials.Core +{ + /// + /// Defines mute control methods and properties with feedback + /// + public interface IHasMuteControlWithFeedback : IHasMuteControl + { + BoolFeedback MuteFeedback { get; } + void MuteOn(); + void MuteOff(); + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasVolumeControl.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasVolumeControl.cs new file mode 100644 index 00000000..eae458a3 --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasVolumeControl.cs @@ -0,0 +1,11 @@ +namespace PepperDash.Essentials.Core +{ + /// + /// Defines the contract for IHasVolumeControl + /// + public interface IHasVolumeControl + { + void VolumeUp(bool pressRelease); + void VolumeDown(bool pressRelease); + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasVolumeControlWithFeedback.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasVolumeControlWithFeedback.cs new file mode 100644 index 00000000..a021edb1 --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasVolumeControlWithFeedback.cs @@ -0,0 +1,11 @@ +namespace PepperDash.Essentials.Core +{ + /// + /// Defines volume control methods and properties with feedback + /// + public interface IHasVolumeControlWithFeedback : IHasVolumeControl + { + void SetVolume(ushort level); + IntFeedback VolumeLevelFeedback { get; } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasVolumeDevice.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasVolumeDevice.cs new file mode 100644 index 00000000..706271aa --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasVolumeDevice.cs @@ -0,0 +1,10 @@ +namespace PepperDash.Essentials.Core +{ + /// + /// Defines the contract for IHasVolumeDevice + /// + public interface IHasVolumeDevice + { + IBasicVolumeControls VolumeDevice { get; } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/eVolumeLevelUnits.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/eVolumeLevelUnits.cs new file mode 100644 index 00000000..0ebb9270 --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/eVolumeLevelUnits.cs @@ -0,0 +1,10 @@ +namespace PepperDash.Essentials.Core +{ + public enum eVolumeLevelUnits + { + Decibels, + Percent, + Relative, + Absolute + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/IVolumeAndAudioInterfaces.cs b/src/PepperDash.Essentials.Core/Devices/IVolumeAndAudioInterfaces.cs deleted file mode 100644 index 32a28113..00000000 --- a/src/PepperDash.Essentials.Core/Devices/IVolumeAndAudioInterfaces.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; - -namespace PepperDash.Essentials.Core -{ - /// - /// Defines minimal volume and mute control methods - /// - public interface IBasicVolumeControls - { - void VolumeUp(bool pressRelease); - void VolumeDown(bool pressRelease); - void MuteToggle(); - } - - /// - /// Defines the contract for IHasVolumeControl - /// - public interface IHasVolumeControl - { - void VolumeUp(bool pressRelease); - void VolumeDown(bool pressRelease); - } - - /// - /// Defines volume control methods and properties with feedback - /// - public interface IHasVolumeControlWithFeedback : IHasVolumeControl - { - void SetVolume(ushort level); - IntFeedback VolumeLevelFeedback { get; } - } - - /// - /// Defines basic mute control methods - /// - public interface IHasMuteControl - { - void MuteToggle(); - } - - /// - /// Defines mute control methods and properties with feedback - /// - public interface IHasMuteControlWithFeedback : IHasMuteControl - { - BoolFeedback MuteFeedback { get; } - void MuteOn(); - void MuteOff(); - } - - /// - /// Defines the contract for IBasicVolumeWithFeedback - /// - public interface IBasicVolumeWithFeedback : IBasicVolumeControls - { - BoolFeedback MuteFeedback { get; } - void MuteOn(); - void MuteOff(); - void SetVolume(ushort level); - IntFeedback VolumeLevelFeedback { get; } - } - - /// - /// Defines the contract for IBasicVolumeWithFeedbackAdvanced - /// - public interface IBasicVolumeWithFeedbackAdvanced : IBasicVolumeWithFeedback - { - int RawVolumeLevel { get; } - - eVolumeLevelUnits Units { get; } - } - - public enum eVolumeLevelUnits - { - Decibels, - Percent, - Relative, - Absolute - } - - /// - /// Defines the contract for IHasCurrentVolumeControls - /// - public interface IHasCurrentVolumeControls - { - IBasicVolumeControls CurrentVolumeControls { get; } - event EventHandler CurrentVolumeDeviceChange; - - void SetDefaultLevels(); - - bool ZeroVolumeWhenSwtichingVolumeDevices { get; } - } - - - /// - /// Defines the contract for IFullAudioSettings - /// - 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; } - } - - /// - /// Defines the contract for IHasVolumeDevice - /// - public interface IHasVolumeDevice - { - IBasicVolumeControls VolumeDevice { get; } - } - - /// - /// Identifies a device that contains audio zones - /// - public interface IAudioZones : IRouting - { - Dictionary Zone { get; } - } - - /// - /// Defines minimum functionality for an audio zone - /// - public interface IAudioZone : IBasicVolumeWithFeedback - { - void SelectInput(ushort input); - } -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Fusion/FusionCustomPropertiesBridge.cs b/src/PepperDash.Essentials.Core/Fusion/FusionCustomPropertiesBridge.cs index a0cd8e6e..12f5d7e4 100644 --- a/src/PepperDash.Essentials.Core/Fusion/FusionCustomPropertiesBridge.cs +++ b/src/PepperDash.Essentials.Core/Fusion/FusionCustomPropertiesBridge.cs @@ -1,16 +1,10 @@  using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using Crestron.SimplSharp; using Newtonsoft.Json; using Newtonsoft.Json.Linq; - using PepperDash.Core; -using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.Devices; using Serilog.Events; @@ -25,17 +19,25 @@ namespace PepperDash.Essentials.Core.Fusion /// /// Evaluates the room info and custom properties from Fusion and updates the system properties aa needed /// - /// - public void EvaluateRoomInfo(string roomKey, RoomInformation roomInfo) + /// The room associated with this Fusion instance + /// The room information from Fusion + /// + public void EvaluateRoomInfo(IEssentialsRoom room, RoomInformation roomInfo, bool useFusionRoomName) { try { - var reconfigurableDevices = DeviceManager.AllDevices.Where(d => d is ReconfigurableDevice); + var reconfigurableDevices = DeviceManager.AllDevices.OfType(); foreach (var device in reconfigurableDevices) { // 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) { @@ -85,36 +87,49 @@ namespace PepperDash.Essentials.Core.Fusion 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(ToString()), helpMessage.CustomFieldValue); - deviceConfig.Properties["helpMessage"] = (string)helpMessage.CustomFieldValue; - } - } // 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) { - 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: "); } } } diff --git a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs index bcf6509e..308e5c12 100644 --- a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs +++ b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs @@ -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.CrestronXml; using Crestron.SimplSharp.CrestronXml.Serialization; @@ -10,11 +15,6 @@ using PepperDash.Core.Logging; using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.DeviceTypeInterfaces; using Serilog.Events; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Timers; namespace PepperDash.Essentials.Core.Fusion { @@ -1091,7 +1091,7 @@ namespace PepperDash.Essentials.Core.Fusion } RoomInfoChange?.Invoke(this, new EventArgs()); - CustomPropertiesBridge.EvaluateRoomInfo(Room.Key, roomInformation); + CustomPropertiesBridge.EvaluateRoomInfo(Room, roomInformation, _config.UseFusionRoomName); } } catch (Exception e) diff --git a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerPropertiesConfig.cs b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerPropertiesConfig.cs index c9dea4c3..98234e61 100644 --- a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerPropertiesConfig.cs +++ b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerPropertiesConfig.cs @@ -27,7 +27,7 @@ public class IEssentialsRoomFusionControllerPropertiesConfig } else { - Debug.LogWarning( "Failed to parse IpId '{0}' as UInt16", IpId); + Debug.LogWarning("Failed to parse IpId '{0}' as UInt16", IpId); return 0; } } @@ -45,6 +45,13 @@ public class IEssentialsRoomFusionControllerPropertiesConfig [JsonProperty("roomKey")] public string RoomKey { get; set; } + /// + /// Gets or sets whether to use the Fusion room name for this room + /// + /// Defaults to true to preserve current behavior. Set to false to skip updating the room name from Fusion + [JsonProperty("useFusionRoomName")] + public bool UseFusionRoomName { get; set; } = true; + /// /// Gets or sets whether to use HTML format for help requests /// diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs index c51cec58..e37d2d57 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs @@ -1,7 +1,7 @@ -using PepperDash.Core; -using PepperDash.Essentials.Core.Config; -using System; +using System; using System.Linq; +using PepperDash.Core; +using PepperDash.Essentials.Core.Config; 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, /// tracing the route back to the original source, and updating the CurrentSourceInfo on sink devices. /// - public class RoutingFeedbackManager:EssentialsDevice + public class RoutingFeedbackManager : EssentialsDevice { /// /// Initializes a new instance of the class. /// /// The unique key for this manager device. /// The name of this manager device. - public RoutingFeedbackManager(string key, string name): base(key, name) - { + public RoutingFeedbackManager(string key, string name) + : base(key, name) + { AddPreActivationAction(SubscribeForMidpointFeedback); AddPreActivationAction(SubscribeForSinkFeedback); } - /// /// Subscribes to the RouteChanged event on all devices implementing . /// @@ -41,12 +41,13 @@ namespace PepperDash.Essentials.Core.Routing /// private void SubscribeForSinkFeedback() { - var sinkDevices = DeviceManager.AllDevices.OfType(); + var sinkDevices = + DeviceManager.AllDevices.OfType(); - foreach (var device in sinkDevices) - { - device.InputChanged += HandleSinkUpdate; - } + foreach (var device in sinkDevices) + { + device.InputChanged += HandleSinkUpdate; + } } /// @@ -55,11 +56,15 @@ namespace PepperDash.Essentials.Core.Routing /// /// The midpoint device that reported a route change. /// The descriptor of the new route. - private void HandleMidpointUpdate(IRoutingWithFeedback midpoint, RouteSwitchDescriptor newRoute) + private void HandleMidpointUpdate( + IRoutingWithFeedback midpoint, + RouteSwitchDescriptor newRoute + ) { try { - var devices = DeviceManager.AllDevices.OfType(); + var devices = + DeviceManager.AllDevices.OfType(); foreach (var device in devices) { @@ -68,7 +73,13 @@ namespace PepperDash.Essentials.Core.Routing } 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 /// /// The sink device that reported an input change. /// The new input port selected on the sink device. - private void HandleSinkUpdate(IRoutingSinkWithSwitching sender, RoutingInputPort currentInputPort) + private void HandleSinkUpdate( + IRoutingSinkWithSwitching sender, + RoutingInputPort currentInputPort + ) { try { @@ -86,7 +100,13 @@ namespace PepperDash.Essentials.Core.Routing } 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 /// /// The destination sink device to update. /// The currently selected input port on the destination device. - private void UpdateDestination(IRoutingSinkWithSwitching destination, RoutingInputPort inputPort) - { - Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Updating destination {destination} with inputPort {inputPort}", this, destination?.Key, inputPort?.Key); + private void UpdateDestination( + IRoutingSinkWithSwitching destination, + 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; } @@ -111,11 +145,19 @@ namespace PepperDash.Essentials.Core.Routing { 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) { - 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 { @@ -123,12 +165,13 @@ namespace PepperDash.Essentials.Core.Routing Name = inputPort.Key, }; - - destination.CurrentSourceInfo = tempSourceListItem; ; + destination.CurrentSourceInfo = tempSourceListItem; + ; destination.CurrentSourceInfoKey = "$transient"; return; } - } catch (Exception ex) + } + catch (Exception ex) { Debug.LogMessage(ex, "Error getting first tieline: {Exception}", this, ex); return; @@ -143,7 +186,12 @@ namespace PepperDash.Essentials.Core.Routing 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 { @@ -155,32 +203,45 @@ namespace PepperDash.Essentials.Core.Routing destination.CurrentSourceInfoKey = string.Empty; return; } - } catch(Exception ex) + } + catch (Exception ex) { Debug.LogMessage(ex, "Error getting sourceTieLine: {Exception}", this, ex); 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. - var room = DeviceManager.AllDevices.OfType().FirstOrDefault((r) => { - if(r is IHasMultipleDisplays roomMultipleDisplays) - { - return roomMultipleDisplays.Displays.Any(d => d.Value.Key == destination.Key); - } + var room = DeviceManager + .AllDevices.OfType() + .FirstOrDefault( + (r) => + { + if (r is IHasMultipleDisplays roomMultipleDisplays) + { + return roomMultipleDisplays.Displays.Any(d => + d.Value.Key == destination.Key + ); + } - if(r is IHasDefaultDisplay roomDefaultDisplay) - { - return roomDefaultDisplay.DefaultDisplay.Key == destination.Key; - } + if (r is IHasDefaultDisplay roomDefaultDisplay) + { + return roomDefaultDisplay.DefaultDisplay.Key == destination.Key; + } - return false; - }); - - if(room == null) + return false; + } + ); + + 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; } @@ -190,29 +251,45 @@ namespace PepperDash.Essentials.Core.Routing 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; } // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found sourceList for room {room}", this, room.Key); - var sourceListItem = sourceList.FirstOrDefault(sli => { - //// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, - // "SourceListItem {sourceListItem}:{sourceKey} tieLine sourceport device key {sourcePortDeviceKey}", - // this, - // sli.Key, - // sli.Value.SourceKey, - // sourceTieLine.SourcePort.ParentDevice.Key); + var sourceListItem = sourceList.FirstOrDefault(sli => + { + //// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, + // "SourceListItem {sourceListItem}:{sourceKey} tieLine sourceport device key {sourcePortDeviceKey}", + // this, + // sli.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 sourceKey = sourceListItem.Key; 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 { @@ -221,7 +298,7 @@ namespace PepperDash.Essentials.Core.Routing }; destination.CurrentSourceInfoKey = "$transient"; - destination.CurrentSourceInfo = tempSourceListItem; + destination.CurrentSourceInfo = tempSourceListItem; return; } @@ -229,7 +306,6 @@ namespace PepperDash.Essentials.Core.Routing destination.CurrentSourceInfoKey = sourceKey; destination.CurrentSourceInfo = source; - } /// @@ -249,29 +325,49 @@ namespace PepperDash.Essentials.Core.Routing { // 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; } - 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); - 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) { - 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; } //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); - 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) { @@ -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 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); 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) { return GetRootTieLine(nextTieLine); } - } catch (Exception ex) + } + catch (Exception ex) { Debug.LogMessage(ex, "Error walking tieLines: {Exception}", this, ex); return null; diff --git a/src/PepperDash.Essentials.Devices.Common/DSP/DspBase.cs b/src/PepperDash.Essentials.Devices.Common/DSP/DspBase.cs index e20abc9c..051f3b1a 100644 --- a/src/PepperDash.Essentials.Devices.Common/DSP/DspBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/DSP/DspBase.cs @@ -61,13 +61,18 @@ namespace PepperDash.Essentials.Devices.Common.DSP /// /// Base class for DSP control points /// - public abstract class DspControlPoint : IKeyed + public abstract class DspControlPoint : IKeyName { /// /// Gets or sets the Key /// public string Key { get; } + /// + /// Gets or sets the Name + /// + public string Name { get; private set; } + /// /// Initializes a new instance of the DspControlPoint class /// diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs index 9163c2a3..f0e57de2 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs @@ -1,15 +1,27 @@ using System; using System.Collections.Generic; +using System.Timers; using Crestron.SimplSharp; using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.CrestronIO; using PepperDash.Essentials.Core.DeviceTypeInterfaces; -using Serilog.Events; +using PepperDash.Essentials.Devices.Common.Displays; namespace PepperDash.Essentials.Devices.Common.Shades { + /// + /// Enumeration for requested state + /// + enum RequestedState + { + None, + Raise, + Lower + } + /// /// Controls a single shade using three relays /// @@ -20,11 +32,16 @@ namespace PepperDash.Essentials.Devices.Common.Shades readonly ScreenLiftRelaysConfig LowerRelayConfig; readonly ScreenLiftRelaysConfig LatchedRelayConfig; - Displays.DisplayBase DisplayDevice; + DisplayBase DisplayDevice; ISwitchedOutput RaiseRelay; ISwitchedOutput LowerRelay; ISwitchedOutput LatchedRelay; + private bool _isMoving; + private RequestedState _requestedState; + private RequestedState _currentMovement; + private Timer _movementTimer; + /// /// Gets or sets the InUpPosition /// @@ -80,6 +97,11 @@ namespace PepperDash.Essentials.Devices.Common.Shades IsInUpPosition = new BoolFeedback("isInUpPosition", () => _isInUpPosition); + // Initialize movement timer for reuse + _movementTimer = new Timer(); + _movementTimer.Elapsed += OnMovementComplete; + _movementTimer.AutoReset = false; + switch (Mode) { case eScreenLiftControlMode.momentary: @@ -129,25 +151,25 @@ namespace PepperDash.Essentials.Devices.Common.Shades { case eScreenLiftControlMode.momentary: { - Debug.LogMessage(LogEventLevel.Debug, this, $"Getting relays for {Mode}"); + this.LogDebug("Getting relays for {mode}", Mode); RaiseRelay = GetSwitchedOutputFromDevice(RaiseRelayConfig.DeviceKey); LowerRelay = GetSwitchedOutputFromDevice(LowerRelayConfig.DeviceKey); break; } case eScreenLiftControlMode.latched: { - Debug.LogMessage(LogEventLevel.Debug, this, $"Getting relays for {Mode}"); + this.LogDebug("Getting relays for {mode}", Mode); LatchedRelay = GetSwitchedOutputFromDevice(LatchedRelayConfig.DeviceKey); break; } } - Debug.LogMessage(LogEventLevel.Debug, this, $"Getting display with key {DisplayDeviceKey}"); + this.LogDebug("Getting display with key {displayKey}", DisplayDeviceKey); DisplayDevice = GetDisplayBaseFromDevice(DisplayDeviceKey); 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.IsCoolingDownFeedback.OutputChange += IsCoolingDownFeedback_OutputChange; @@ -163,22 +185,49 @@ namespace PepperDash.Essentials.Devices.Common.Shades { 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) { case eScreenLiftControlMode.momentary: { 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; } case eScreenLiftControlMode.latched: { LatchedRelay.Off(); + InUpPosition = true; break; } } - InUpPosition = true; } /// @@ -188,59 +237,145 @@ namespace PepperDash.Essentials.Devices.Common.Shades { 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) { case eScreenLiftControlMode.momentary: { 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; } case eScreenLiftControlMode.latched: { LatchedRelay.On(); + InUpPosition = false; break; } } - InUpPosition = false; } - void PulseOutput(ISwitchedOutput output, int pulseTime) + private void DisposeMovementTimer() { - output.On(); - CTimer pulseTimer = new CTimer(new CTimerCallbackFunction((o) => output.Off()), pulseTime); + if (_movementTimer != null) + { + _movementTimer.Stop(); + _movementTimer.Elapsed -= OnMovementComplete; + _movementTimer.Dispose(); + _movementTimer = null; + } } /// - /// Attempts to get the port on teh specified device from config + /// Called when movement timer completes /// - /// - /// - ISwitchedOutput GetSwitchedOutputFromDevice(string relayKey) + private void OnMovementComplete(object sender, ElapsedEventArgs e) { - 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(relayKey); if (portDevice != null) { - return portDevice as ISwitchedOutput; + return portDevice; } 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; } } - Displays.DisplayBase GetDisplayBaseFromDevice(string displayKey) + private DisplayBase GetDisplayBaseFromDevice(string displayKey) { - var displayDevice = DeviceManager.GetDeviceForKey(displayKey); + var displayDevice = DeviceManager.GetDeviceForKey(displayKey); if (displayDevice != null) { - return displayDevice as Displays.DisplayBase; + return displayDevice; } 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; } } @@ -248,7 +383,7 @@ namespace PepperDash.Essentials.Devices.Common.Shades } /// - /// Represents a ScreenLiftControllerFactory + /// Factory for ScreenLiftController devices /// public class ScreenLiftControllerFactory : EssentialsDeviceFactory { @@ -260,14 +395,11 @@ namespace PepperDash.Essentials.Devices.Common.Shades TypeNames = new List() { "screenliftcontroller" }; } - /// - /// BuildDevice method - /// /// public override EssentialsDevice BuildDevice(DeviceConfig dc) { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Generic Comm Device"); - var props = Newtonsoft.Json.JsonConvert.DeserializeObject(dc.Properties.ToString()); + Debug.LogDebug("Factory Attempting to create new ScreenLiftController Device"); + var props = dc.Properties.ToObject(); return new ScreenLiftController(dc.Key, dc.Name, props); } diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftRelaysConfig.cs b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftRelaysConfig.cs index 4de9eb25..8890ac95 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftRelaysConfig.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftRelaysConfig.cs @@ -18,5 +18,11 @@ namespace PepperDash.Essentials.Devices.Common.Shades /// [JsonProperty("pulseTimeInMs")] public int PulseTimeInMs { get; set; } + + /// + /// Gets or sets the MoveTimeInMs - time in milliseconds for the movement to complete + /// + [JsonProperty("moveTimeInMs")] + public int MoveTimeInMs { get; set; } } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs index 80042352..81df6bac 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs @@ -22,7 +22,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// The message path. /// The device. public DeviceVolumeMessenger(string key, string messagePath, IBasicVolumeControls device) - : base(key, messagePath, device as IKeyName) + : base(key, messagePath, device) { this.device = device; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs index 3a1b6a27..ec718919 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs @@ -62,6 +62,14 @@ namespace PepperDash.Essentials.AppServer.Messengers 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)) + })); + }; } } diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 45ff814a..3adcaf87 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -1748,7 +1748,7 @@ namespace PepperDash.Essentials var clientNo = 1; 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( $"\r\nClient {clientNo}:\r\n" + diff --git a/src/PepperDash.Essentials.MobileControl/Utilities.cs b/src/PepperDash.Essentials.MobileControl/Utilities.cs index 8c2abf3e..ec9acd83 100644 --- a/src/PepperDash.Essentials.MobileControl/Utilities.cs +++ b/src/PepperDash.Essentials.MobileControl/Utilities.cs @@ -1,3 +1,4 @@ +using System.Threading; using PepperDash.Core; using PepperDash.Core.Logging; using WebSocketSharp; @@ -12,13 +13,12 @@ namespace PepperDash.Essentials private static int nextClientId = 0; /// - /// Get the next unique client ID + /// Get the next unique client ID (thread-safe) /// /// Client ID public static int GetNextClientId() { - nextClientId++; - return nextClientId; + return Interlocked.Increment(ref nextClientId); } /// /// Converts a WebSocketServer LogData object to Essentials logging calls. diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinResponse.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinResponse.cs index 1529d0fe..7f2266a5 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinResponse.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinResponse.cs @@ -64,6 +64,12 @@ namespace PepperDash.Essentials.WebSocketServer [JsonProperty("userAppUrl")] public string UserAppUrl { get; set; } + /// + /// Gets or sets the WebSocketUrl with clientId query parameter + /// + [JsonProperty("webSocketUrl")] + public string WebSocketUrl { get; set; } + /// /// Gets or sets the EnableDebug diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index 0f2fa388..252d2814 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.IO; @@ -59,12 +60,24 @@ namespace PepperDash.Essentials.WebSocketServer /// public Dictionary UiClientContexts { get; private set; } - private readonly Dictionary uiClients = new Dictionary(); + private readonly ConcurrentDictionary uiClients = new ConcurrentDictionary(); + + /// + /// Stores pending client registrations using composite key: token-clientId + /// This ensures the correct client ID is matched even when connections establish out of order + /// + private readonly ConcurrentDictionary pendingClientRegistrations = new ConcurrentDictionary(); + + /// + /// Stores queues of pending client IDs per token for legacy clients (FIFO) + /// This ensures thread-safety when multiple legacy clients use the same token + /// + private readonly ConcurrentDictionary> legacyClientIdQueues = new ConcurrentDictionary>(); /// /// Gets the collection of UI clients /// - public ReadOnlyDictionary UiClients => new ReadOnlyDictionary(uiClients); + public IReadOnlyDictionary UiClients => uiClients; private readonly MobileControlSystemController _parent; @@ -723,23 +736,95 @@ namespace PepperDash.Essentials.WebSocketServer private UiClient BuildUiClient(string roomKey, JoinToken token, string key) { - var c = new UiClient($"uiclient-{key}-{roomKey}-{token.Id}", token.Id, token.Token, token.TouchpanelKey); - this.LogInformation("Constructing UiClient with key {key} and ID {id}", key, token.Id); + // Dequeue the next clientId for legacy client support (FIFO per token) + // New clients will override this ID in OnOpen with the validated query parameter value + var clientId = "pending"; + if (legacyClientIdQueues.TryGetValue(key, out var queue) && queue.TryDequeue(out var dequeuedId)) + { + clientId = dequeuedId; + this.LogVerbose("Dequeued 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.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.Remove(token.Id); - } - uiClients.Add(token.Id, c); - // UiClients[key].SetClient(c); - c.ConnectionClosed += (o, a) => uiClients.Remove(a.ClientId); - token.Id = null; + uiClients.TryRemove(a.ClientId, out _); + // Clean up any pending registrations for this token + var keysToRemove = pendingClientRegistrations.Keys + .Where(k => k.StartsWith($"{key}-")) + .ToList(); + foreach (var k in keysToRemove) + { + pendingClientRegistrations.TryRemove(k, out _); + } + + // Clean up legacy queue if empty + if (legacyClientIdQueues.TryGetValue(key, out var legacyQueue) && legacyQueue.IsEmpty) + { + legacyClientIdQueues.TryRemove(key, out _); + } + }; return c; } + /// + /// Registers a UiClient with its validated client ID after WebSocket connection + /// + /// The UiClient to register + /// The validated client ID + /// The token key for validation + /// True if registration successful, false if validation failed + 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; + } + + /// + /// Registers a UiClient using legacy flow (for backwards compatibility with older clients) + /// + /// The UiClient to register + 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); + } + /// /// Prints out the session data for each path /// @@ -1046,10 +1131,22 @@ namespace PepperDash.Essentials.WebSocketServer }); } + // Generate a client ID for this join request var clientId = $"{Utilities.GetNextClientId()}"; - clientContext.Token.Id = clientId; + + // Store in pending registrations for new clients that send clientId via query param + var registrationKey = $"{token}-{clientId}"; + pendingClientRegistrations.TryAdd(registrationKey, clientId); + + // Also enqueue for legacy clients (thread-safe FIFO per token) + var queue = legacyClientIdQueues.GetOrAdd(token, _ => new ConcurrentQueue()); + queue.Enqueue(clientId); - this.LogVerbose("Assigning ClientId: {clientId}", clientId); + this.LogVerbose("Assigning ClientId: {clientId} for token: {token}", clientId, token); + + // 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 JoinResponse jRes = new JoinResponse @@ -1064,6 +1161,7 @@ namespace PepperDash.Essentials.WebSocketServer UserAppUrl = string.Format("http://{0}:{1}/mc/app", CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0), Port), + WebSocketUrl = wsUrl, EnableDebug = false, DeviceInterfaceSupport = deviceInterfaces }; diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs index cf8dd7f9..fd012bbe 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs @@ -31,6 +31,11 @@ namespace PepperDash.Essentials.WebSocketServer /// public string Token { get; private set; } + /// + /// The URL token key used to connect (from UiClientContexts dictionary key) + /// + public string TokenKey { get; set; } + /// /// Touchpanel Key associated with this client /// @@ -41,6 +46,11 @@ namespace PepperDash.Essentials.WebSocketServer /// public MobileControlSystemController Controller { get; set; } + /// + /// Gets or sets the server instance for client registration + /// + public MobileControlWebsocketServer Server { get; set; } + /// /// Gets or sets the room key that this client is associated with /// @@ -99,6 +109,50 @@ namespace PepperDash.Essentials.WebSocketServer Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this); 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) { Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Controller is null"); diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index 72d7c27d..ef19c955 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -662,6 +662,7 @@ namespace PepperDash.Essentials 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."); }