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.");
}