From 53e7a30224a2748a322e92e65e9c343cfb3cb685 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 26 Dec 2025 12:34:31 -0600 Subject: [PATCH 01/16] fix: handle threading issues for concurrent clients joining --- .../MobileControlSystemController.cs | 2 +- .../Utilities.cs | 6 +- .../MobileControlWebsocketServer.cs | 63 +++++++++++++++---- .../WebSocketServer/UiClient.cs | 5 ++ src/PepperDash.Essentials/ControlSystem.cs | 1 + 5 files changed, 60 insertions(+), 17 deletions(-) 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/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index 0f2fa388..25cd7ba7 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,18 @@ 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(); /// /// Gets the collection of UI clients /// - public ReadOnlyDictionary UiClients => new ReadOnlyDictionary(uiClients); + public IReadOnlyDictionary UiClients => uiClients; private readonly MobileControlSystemController _parent; @@ -723,20 +730,46 @@ 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); + // Try to retrieve a pending client ID that was registered during the join request + // Use the composite key: token-clientId + string clientId = null; + string registrationKey = null; + + // Find a registration for this token + var matchingRegistrations = pendingClientRegistrations.Keys + .Where(k => k.StartsWith($"{key}-")) + .OrderBy(k => k) // Process in order for FIFO behavior + .ToList(); + + if (matchingRegistrations.Any()) + { + registrationKey = matchingRegistrations.First(); + if (pendingClientRegistrations.TryRemove(registrationKey, out clientId)) + { + this.LogVerbose("Retrieved pending clientId {clientId} for token {key}", clientId, key); + } + } + + if (clientId == null) + { + // Fallback: generate a new client ID if none was pending + clientId = $"{Utilities.GetNextClientId()}"; + this.LogWarning("No pending clientId found for token {key}, generated new ID {clientId}", key, clientId); + } + + var c = new UiClient($"uiclient-{key}-{roomKey}-{clientId}", clientId, token.Token, token.TouchpanelKey); + this.LogInformation("Constructing UiClient with key {key} and ID {id}", key, clientId); c.Controller = _parent; c.RoomKey = roomKey; + c.TokenKey = key; // Store the URL token key for filtering - if (uiClients.ContainsKey(token.Id)) + uiClients.AddOrUpdate(clientId, c, (id, existingClient) => { - this.LogWarning("removing client with duplicate id {id}", token.Id); - uiClients.Remove(token.Id); - } - uiClients.Add(token.Id, c); + this.LogWarning("replacing client with duplicate id {id}", id); + return c; + }); // UiClients[key].SetClient(c); - c.ConnectionClosed += (o, a) => uiClients.Remove(a.ClientId); - token.Id = null; + c.ConnectionClosed += (o, a) => uiClients.TryRemove(a.ClientId, out _); return c; } @@ -1046,10 +1079,14 @@ namespace PepperDash.Essentials.WebSocketServer }); } + // Generate a client ID for this join request and store it with a composite key var clientId = $"{Utilities.GetNextClientId()}"; - clientContext.Token.Id = clientId; + + // Store registration with composite key: token-clientId so BuildUiClient can verify it + var registrationKey = $"{token}-{clientId}"; + pendingClientRegistrations.TryAdd(registrationKey, clientId); - this.LogVerbose("Assigning ClientId: {clientId}", clientId); + this.LogVerbose("Assigning ClientId: {clientId} for token: {token}", clientId, token); // Construct the response object JoinResponse jRes = new JoinResponse diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs index cf8dd7f9..61e5d042 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 /// 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."); } From 9d6aaa2a0ec75304183198a84c3904925e5f749b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 20:02:52 +0000 Subject: [PATCH 02/16] Initial plan From 7ea1efbabf25aba18e04a4479ab3e0fe05636582 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 20:09:31 +0000 Subject: [PATCH 03/16] Add raise/lower movement time configuration and banked command support Co-authored-by: erikdred <88980320+erikdred@users.noreply.github.com> --- .../Displays/ScreenLiftController.cs | 105 ++++++++++++++++++ .../Displays/ScreenLiftRelaysConfig.cs | 6 + 2 files changed, 111 insertions(+) diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs index 9163c2a3..5d25a58a 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs @@ -10,6 +10,16 @@ using Serilog.Events; namespace PepperDash.Essentials.Devices.Common.Shades { + /// + /// Enumeration for requested state + /// + enum RequestedState + { + None, + Raise, + Lower + } + /// /// Controls a single shade using three relays /// @@ -25,6 +35,10 @@ namespace PepperDash.Essentials.Devices.Common.Shades ISwitchedOutput LowerRelay; ISwitchedOutput LatchedRelay; + private bool _isMoving; + private RequestedState _requestedState; + private CTimer _movementTimer; + /// /// Gets or sets the InUpPosition /// @@ -163,6 +177,16 @@ namespace PepperDash.Essentials.Devices.Common.Shades { if (RaiseRelay == null && LatchedRelay == null) return; + Debug.LogMessage(LogEventLevel.Debug, this, $"Raise called for {Type}"); + + // If device is moving, bank the command + if (_isMoving) + { + Debug.LogMessage(LogEventLevel.Debug, this, $"Device is moving, banking Raise command"); + _requestedState = RequestedState.Raise; + return; + } + Debug.LogMessage(LogEventLevel.Debug, this, $"Raising {Type}"); switch (Mode) @@ -170,11 +194,25 @@ namespace PepperDash.Essentials.Devices.Common.Shades case eScreenLiftControlMode.momentary: { PulseOutput(RaiseRelay, RaiseRelayConfig.PulseTimeInMs); + + // Set moving flag and start timer if movement time is configured + if (RaiseRelayConfig.MovementTimeInMs > 0) + { + _isMoving = true; + _movementTimer = new CTimer(OnMovementComplete, RaiseRelayConfig.MovementTimeInMs); + } break; } case eScreenLiftControlMode.latched: { LatchedRelay.Off(); + + // Set moving flag and start timer if movement time is configured + if (LatchedRelayConfig.MovementTimeInMs > 0) + { + _isMoving = true; + _movementTimer = new CTimer(OnMovementComplete, LatchedRelayConfig.MovementTimeInMs); + } break; } } @@ -188,6 +226,16 @@ namespace PepperDash.Essentials.Devices.Common.Shades { if (LowerRelay == null && LatchedRelay == null) return; + Debug.LogMessage(LogEventLevel.Debug, this, $"Lower called for {Type}"); + + // If device is moving, bank the command + if (_isMoving) + { + Debug.LogMessage(LogEventLevel.Debug, this, $"Device is moving, banking Lower command"); + _requestedState = RequestedState.Lower; + return; + } + Debug.LogMessage(LogEventLevel.Debug, this, $"Lowering {Type}"); switch (Mode) @@ -195,17 +243,74 @@ namespace PepperDash.Essentials.Devices.Common.Shades case eScreenLiftControlMode.momentary: { PulseOutput(LowerRelay, LowerRelayConfig.PulseTimeInMs); + + // Set moving flag and start timer if movement time is configured + if (LowerRelayConfig.MovementTimeInMs > 0) + { + _isMoving = true; + _movementTimer = new CTimer(OnMovementComplete, LowerRelayConfig.MovementTimeInMs); + } break; } case eScreenLiftControlMode.latched: { LatchedRelay.On(); + + // Set moving flag and start timer if movement time is configured + if (LatchedRelayConfig.MovementTimeInMs > 0) + { + _isMoving = true; + _movementTimer = new CTimer(OnMovementComplete, LatchedRelayConfig.MovementTimeInMs); + } break; } } InUpPosition = false; } + /// + /// Called when movement timer completes + /// + void OnMovementComplete(object o) + { + Debug.LogMessage(LogEventLevel.Debug, this, $"Movement complete"); + + _isMoving = false; + + // Execute banked command if one exists + if (_requestedState != RequestedState.None) + { + Debug.LogMessage(LogEventLevel.Debug, this, $"Executing banked command: {_requestedState}"); + + var commandToExecute = _requestedState; + _requestedState = RequestedState.None; + + // Check if current state matches what the banked command would do + // If so, ignore it + if (commandToExecute == RequestedState.Raise && InUpPosition) + { + Debug.LogMessage(LogEventLevel.Debug, this, $"Already in up position, ignoring banked Raise command"); + return; + } + + if (commandToExecute == RequestedState.Lower && !InUpPosition) + { + Debug.LogMessage(LogEventLevel.Debug, this, $"Already in down position, ignoring banked Lower command"); + return; + } + + // Execute the banked command + if (commandToExecute == RequestedState.Raise) + { + Raise(); + } + else if (commandToExecute == RequestedState.Lower) + { + Lower(); + } + } + } + void PulseOutput(ISwitchedOutput output, int pulseTime) { output.On(); diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftRelaysConfig.cs b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftRelaysConfig.cs index 4de9eb25..a99f82d3 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 MovementTimeInMs - time in milliseconds for the movement to complete + /// + [JsonProperty("movementTimeInMs")] + public int MovementTimeInMs { get; set; } } } \ No newline at end of file From bd11c827daef5f6938c5cb89681de3649ebea126 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 20:14:39 +0000 Subject: [PATCH 04/16] Split movement time into separate raise/lower times and remove timing from latched mode Co-authored-by: erikdred <88980320+erikdred@users.noreply.github.com> --- .../Displays/ScreenLiftController.cs | 40 ++++++++++--------- .../Displays/ScreenLiftRelaysConfig.cs | 12 ++++-- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs index 5d25a58a..4494fdbf 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs @@ -196,23 +196,24 @@ namespace PepperDash.Essentials.Devices.Common.Shades PulseOutput(RaiseRelay, RaiseRelayConfig.PulseTimeInMs); // Set moving flag and start timer if movement time is configured - if (RaiseRelayConfig.MovementTimeInMs > 0) + if (RaiseRelayConfig.RaiseTimeInMs > 0) { _isMoving = true; - _movementTimer = new CTimer(OnMovementComplete, RaiseRelayConfig.MovementTimeInMs); + + // Dispose previous timer if exists + if (_movementTimer != null) + { + _movementTimer.Stop(); + _movementTimer.Dispose(); + } + + _movementTimer = new CTimer(OnMovementComplete, RaiseRelayConfig.RaiseTimeInMs); } break; } case eScreenLiftControlMode.latched: { LatchedRelay.Off(); - - // Set moving flag and start timer if movement time is configured - if (LatchedRelayConfig.MovementTimeInMs > 0) - { - _isMoving = true; - _movementTimer = new CTimer(OnMovementComplete, LatchedRelayConfig.MovementTimeInMs); - } break; } } @@ -245,23 +246,24 @@ namespace PepperDash.Essentials.Devices.Common.Shades PulseOutput(LowerRelay, LowerRelayConfig.PulseTimeInMs); // Set moving flag and start timer if movement time is configured - if (LowerRelayConfig.MovementTimeInMs > 0) + if (LowerRelayConfig.LowerTimeInMs > 0) { _isMoving = true; - _movementTimer = new CTimer(OnMovementComplete, LowerRelayConfig.MovementTimeInMs); + + // Dispose previous timer if exists + if (_movementTimer != null) + { + _movementTimer.Stop(); + _movementTimer.Dispose(); + } + + _movementTimer = new CTimer(OnMovementComplete, LowerRelayConfig.LowerTimeInMs); } break; } case eScreenLiftControlMode.latched: { LatchedRelay.On(); - - // Set moving flag and start timer if movement time is configured - if (LatchedRelayConfig.MovementTimeInMs > 0) - { - _isMoving = true; - _movementTimer = new CTimer(OnMovementComplete, LatchedRelayConfig.MovementTimeInMs); - } break; } } @@ -271,7 +273,7 @@ namespace PepperDash.Essentials.Devices.Common.Shades /// /// Called when movement timer completes /// - void OnMovementComplete(object o) + private void OnMovementComplete(object o) { Debug.LogMessage(LogEventLevel.Debug, this, $"Movement complete"); diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftRelaysConfig.cs b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftRelaysConfig.cs index a99f82d3..f39134a4 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftRelaysConfig.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftRelaysConfig.cs @@ -20,9 +20,15 @@ namespace PepperDash.Essentials.Devices.Common.Shades public int PulseTimeInMs { get; set; } /// - /// Gets or sets the MovementTimeInMs - time in milliseconds for the movement to complete + /// Gets or sets the RaiseTimeInMs - time in milliseconds for the raise movement to complete /// - [JsonProperty("movementTimeInMs")] - public int MovementTimeInMs { get; set; } + [JsonProperty("raiseTimeInMs")] + public int RaiseTimeInMs { get; set; } + + /// + /// Gets or sets the LowerTimeInMs - time in milliseconds for the lower movement to complete + /// + [JsonProperty("lowerTimeInMs")] + public int LowerTimeInMs { get; set; } } } \ No newline at end of file From ae0b2fe0864692fd85a8c9fffce8f02fc4b3a105 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 20:17:59 +0000 Subject: [PATCH 05/16] Refactor timer disposal and improve code readability Co-authored-by: erikdred <88980320+erikdred@users.noreply.github.com> --- .../Displays/ScreenLiftController.cs | 75 ++++++++++--------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs index 4494fdbf..459819af 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs @@ -199,14 +199,7 @@ namespace PepperDash.Essentials.Devices.Common.Shades if (RaiseRelayConfig.RaiseTimeInMs > 0) { _isMoving = true; - - // Dispose previous timer if exists - if (_movementTimer != null) - { - _movementTimer.Stop(); - _movementTimer.Dispose(); - } - + DisposeMovementTimer(); _movementTimer = new CTimer(OnMovementComplete, RaiseRelayConfig.RaiseTimeInMs); } break; @@ -249,14 +242,7 @@ namespace PepperDash.Essentials.Devices.Common.Shades if (LowerRelayConfig.LowerTimeInMs > 0) { _isMoving = true; - - // Dispose previous timer if exists - if (_movementTimer != null) - { - _movementTimer.Stop(); - _movementTimer.Dispose(); - } - + DisposeMovementTimer(); _movementTimer = new CTimer(OnMovementComplete, LowerRelayConfig.LowerTimeInMs); } break; @@ -270,6 +256,19 @@ namespace PepperDash.Essentials.Devices.Common.Shades InUpPosition = false; } + /// + /// Disposes the current movement timer if it exists + /// + private void DisposeMovementTimer() + { + if (_movementTimer != null) + { + _movementTimer.Stop(); + _movementTimer.Dispose(); + _movementTimer = null; + } + } + /// /// Called when movement timer completes /// @@ -287,28 +286,30 @@ namespace PepperDash.Essentials.Devices.Common.Shades var commandToExecute = _requestedState; _requestedState = RequestedState.None; - // Check if current state matches what the banked command would do - // If so, ignore it - if (commandToExecute == RequestedState.Raise && InUpPosition) + // Check if current state matches what the banked command would do and execute if different + switch (commandToExecute) { - Debug.LogMessage(LogEventLevel.Debug, this, $"Already in up position, ignoring banked Raise command"); - return; - } - - if (commandToExecute == RequestedState.Lower && !InUpPosition) - { - Debug.LogMessage(LogEventLevel.Debug, this, $"Already in down position, ignoring banked Lower command"); - return; - } - - // Execute the banked command - if (commandToExecute == RequestedState.Raise) - { - Raise(); - } - else if (commandToExecute == RequestedState.Lower) - { - Lower(); + case RequestedState.Raise: + if (InUpPosition) + { + Debug.LogMessage(LogEventLevel.Debug, this, $"Already in up position, ignoring banked Raise command"); + } + else + { + Raise(); + } + break; + + case RequestedState.Lower: + if (!InUpPosition) + { + Debug.LogMessage(LogEventLevel.Debug, this, $"Already in down position, ignoring banked Lower command"); + } + else + { + Lower(); + } + break; } } } From a7ff2e8903487c3aea66f136f2184693b11219b3 Mon Sep 17 00:00:00 2001 From: Erik Meyer Date: Sat, 27 Dec 2025 17:09:56 -0600 Subject: [PATCH 06/16] fix: move isInUpPosition to momvement complete method, remove conditionlal logic on raise/lower commands --- .../Displays/ScreenLiftController.cs | 56 +++++++++++-------- .../Displays/ScreenLiftRelaysConfig.cs | 12 +--- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs index 459819af..aad8f54a 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs @@ -37,6 +37,7 @@ namespace PepperDash.Essentials.Devices.Common.Shades private bool _isMoving; private RequestedState _requestedState; + private RequestedState _currentMovement; private CTimer _movementTimer; /// @@ -188,7 +189,7 @@ namespace PepperDash.Essentials.Devices.Common.Shades } Debug.LogMessage(LogEventLevel.Debug, this, $"Raising {Type}"); - + switch (Mode) { case eScreenLiftControlMode.momentary: @@ -196,21 +197,26 @@ namespace PepperDash.Essentials.Devices.Common.Shades PulseOutput(RaiseRelay, RaiseRelayConfig.PulseTimeInMs); // Set moving flag and start timer if movement time is configured - if (RaiseRelayConfig.RaiseTimeInMs > 0) + if (RaiseRelayConfig.MoveTimeInMs > 0) { _isMoving = true; + _currentMovement = RequestedState.Raise; DisposeMovementTimer(); - _movementTimer = new CTimer(OnMovementComplete, RaiseRelayConfig.RaiseTimeInMs); + _movementTimer = new CTimer(OnMovementComplete, RaiseRelayConfig.MoveTimeInMs); + } + else + { + InUpPosition = true; } break; } case eScreenLiftControlMode.latched: { LatchedRelay.Off(); + InUpPosition = true; break; } } - InUpPosition = true; } /// @@ -221,7 +227,7 @@ namespace PepperDash.Essentials.Devices.Common.Shades if (LowerRelay == null && LatchedRelay == null) return; Debug.LogMessage(LogEventLevel.Debug, this, $"Lower called for {Type}"); - + // If device is moving, bank the command if (_isMoving) { @@ -239,21 +245,26 @@ namespace PepperDash.Essentials.Devices.Common.Shades PulseOutput(LowerRelay, LowerRelayConfig.PulseTimeInMs); // Set moving flag and start timer if movement time is configured - if (LowerRelayConfig.LowerTimeInMs > 0) + if (LowerRelayConfig.MoveTimeInMs > 0) { _isMoving = true; + _currentMovement = RequestedState.Lower; DisposeMovementTimer(); - _movementTimer = new CTimer(OnMovementComplete, LowerRelayConfig.LowerTimeInMs); + _movementTimer = new CTimer(OnMovementComplete, LowerRelayConfig.MoveTimeInMs); + } + else + { + InUpPosition = false; } break; } case eScreenLiftControlMode.latched: { LatchedRelay.On(); + InUpPosition = false; break; } } - InUpPosition = false; } /// @@ -276,7 +287,18 @@ namespace PepperDash.Essentials.Devices.Common.Shades { Debug.LogMessage(LogEventLevel.Debug, this, $"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) @@ -290,25 +312,11 @@ namespace PepperDash.Essentials.Devices.Common.Shades switch (commandToExecute) { case RequestedState.Raise: - if (InUpPosition) - { - Debug.LogMessage(LogEventLevel.Debug, this, $"Already in up position, ignoring banked Raise command"); - } - else - { - Raise(); - } + Raise(); break; case RequestedState.Lower: - if (!InUpPosition) - { - Debug.LogMessage(LogEventLevel.Debug, this, $"Already in down position, ignoring banked Lower command"); - } - else - { - Lower(); - } + Lower(); break; } } diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftRelaysConfig.cs b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftRelaysConfig.cs index f39134a4..8890ac95 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftRelaysConfig.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftRelaysConfig.cs @@ -20,15 +20,9 @@ namespace PepperDash.Essentials.Devices.Common.Shades public int PulseTimeInMs { get; set; } /// - /// Gets or sets the RaiseTimeInMs - time in milliseconds for the raise movement to complete + /// Gets or sets the MoveTimeInMs - time in milliseconds for the movement to complete /// - [JsonProperty("raiseTimeInMs")] - public int RaiseTimeInMs { get; set; } - - /// - /// Gets or sets the LowerTimeInMs - time in milliseconds for the lower movement to complete - /// - [JsonProperty("lowerTimeInMs")] - public int LowerTimeInMs { get; set; } + [JsonProperty("moveTimeInMs")] + public int MoveTimeInMs { get; set; } } } \ No newline at end of file From 78c9381108b20d67eadc0538ece8bb5b5c1f844e Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Mon, 29 Dec 2025 08:57:52 -0600 Subject: [PATCH 07/16] fix: add clientId as qp for websocket for MC --- .../WebSocketServer/JoinResponse.cs | 6 + .../MobileControlWebsocketServer.cs | 127 +++++++++++++----- .../WebSocketServer/UiClient.cs | 49 +++++++ 3 files changed, 149 insertions(+), 33 deletions(-) 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 25cd7ba7..252d2814 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -68,6 +68,12 @@ namespace PepperDash.Essentials.WebSocketServer /// 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 /// @@ -730,49 +736,95 @@ namespace PepperDash.Essentials.WebSocketServer private UiClient BuildUiClient(string roomKey, JoinToken token, string key) { - // Try to retrieve a pending client ID that was registered during the join request - // Use the composite key: token-clientId - string clientId = null; - string registrationKey = null; - - // Find a registration for this token - var matchingRegistrations = pendingClientRegistrations.Keys - .Where(k => k.StartsWith($"{key}-")) - .OrderBy(k => k) // Process in order for FIFO behavior - .ToList(); - - if (matchingRegistrations.Any()) + // 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)) { - registrationKey = matchingRegistrations.First(); - if (pendingClientRegistrations.TryRemove(registrationKey, out clientId)) - { - this.LogVerbose("Retrieved pending clientId {clientId} for token {key}", clientId, key); - } - } - - if (clientId == null) - { - // Fallback: generate a new client ID if none was pending - clientId = $"{Utilities.GetNextClientId()}"; - this.LogWarning("No pending clientId found for token {key}, generated new ID {clientId}", key, clientId); + 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 ID {id}", key, clientId); + 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 - uiClients.AddOrUpdate(clientId, c, (id, existingClient) => + // Don't add to uiClients yet - will be added in OnOpen after ID is set from query param + + c.ConnectionClosed += (o, a) => { - this.LogWarning("replacing client with duplicate id {id}", id); - return c; - }); - // UiClients[key].SetClient(c); - c.ConnectionClosed += (o, a) => uiClients.TryRemove(a.ClientId, out _); + 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 /// @@ -1079,15 +1131,23 @@ namespace PepperDash.Essentials.WebSocketServer }); } - // Generate a client ID for this join request and store it with a composite key + // Generate a client ID for this join request var clientId = $"{Utilities.GetNextClientId()}"; - // Store registration with composite key: token-clientId so BuildUiClient can verify it + // 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} 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 { @@ -1101,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 61e5d042..fd012bbe 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs @@ -46,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 /// @@ -104,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"); From 0c4aec14d1c7f45c77df02972a11243e2f36ca73 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Mon, 29 Dec 2025 11:53:52 -0600 Subject: [PATCH 08/16] fix: use .NET timers instead of CTimer --- .../Displays/ScreenLiftController.cs | 124 ++++++++++-------- 1 file changed, 70 insertions(+), 54 deletions(-) diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs index aad8f54a..f0e57de2 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs @@ -1,12 +1,14 @@ 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 { @@ -30,7 +32,7 @@ namespace PepperDash.Essentials.Devices.Common.Shades readonly ScreenLiftRelaysConfig LowerRelayConfig; readonly ScreenLiftRelaysConfig LatchedRelayConfig; - Displays.DisplayBase DisplayDevice; + DisplayBase DisplayDevice; ISwitchedOutput RaiseRelay; ISwitchedOutput LowerRelay; ISwitchedOutput LatchedRelay; @@ -38,7 +40,7 @@ namespace PepperDash.Essentials.Devices.Common.Shades private bool _isMoving; private RequestedState _requestedState; private RequestedState _currentMovement; - private CTimer _movementTimer; + private Timer _movementTimer; /// /// Gets or sets the InUpPosition @@ -95,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: @@ -144,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; @@ -178,31 +185,35 @@ namespace PepperDash.Essentials.Devices.Common.Shades { if (RaiseRelay == null && LatchedRelay == null) return; - Debug.LogMessage(LogEventLevel.Debug, this, $"Raise called for {Type}"); + this.LogDebug("Raise called for {type}", Type); // If device is moving, bank the command if (_isMoving) { - Debug.LogMessage(LogEventLevel.Debug, this, $"Device is moving, banking Raise command"); + this.LogDebug("Device is moving, banking Raise command"); _requestedState = RequestedState.Raise; return; } - Debug.LogMessage(LogEventLevel.Debug, this, $"Raising {Type}"); - + 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; - DisposeMovementTimer(); - _movementTimer = new CTimer(OnMovementComplete, RaiseRelayConfig.MoveTimeInMs); + if (_movementTimer.Enabled) + { + _movementTimer.Stop(); + } + _movementTimer.Interval = RaiseRelayConfig.MoveTimeInMs; + _movementTimer.Start(); } else { @@ -226,31 +237,35 @@ namespace PepperDash.Essentials.Devices.Common.Shades { if (LowerRelay == null && LatchedRelay == null) return; - Debug.LogMessage(LogEventLevel.Debug, this, $"Lower called for {Type}"); - + this.LogDebug("Lower called for {type}", Type); + // If device is moving, bank the command if (_isMoving) { - Debug.LogMessage(LogEventLevel.Debug, this, $"Device is moving, banking Lower command"); + this.LogDebug("Device is moving, banking Lower command"); _requestedState = RequestedState.Lower; return; } - Debug.LogMessage(LogEventLevel.Debug, this, $"Lowering {Type}"); + 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; - DisposeMovementTimer(); - _movementTimer = new CTimer(OnMovementComplete, LowerRelayConfig.MoveTimeInMs); + if (_movementTimer.Enabled) + { + _movementTimer.Stop(); + } + _movementTimer.Interval = LowerRelayConfig.MoveTimeInMs; + _movementTimer.Start(); } else { @@ -267,14 +282,12 @@ namespace PepperDash.Essentials.Devices.Common.Shades } } - /// - /// Disposes the current movement timer if it exists - /// private void DisposeMovementTimer() { if (_movementTimer != null) { _movementTimer.Stop(); + _movementTimer.Elapsed -= OnMovementComplete; _movementTimer.Dispose(); _movementTimer = null; } @@ -283,10 +296,10 @@ namespace PepperDash.Essentials.Devices.Common.Shades /// /// Called when movement timer completes /// - private void OnMovementComplete(object o) + private void OnMovementComplete(object sender, ElapsedEventArgs e) { - Debug.LogMessage(LogEventLevel.Debug, this, $"Movement complete"); - + this.LogDebug("Movement complete"); + // Update position based on completed movement if (_currentMovement == RequestedState.Raise) { @@ -296,25 +309,25 @@ namespace PepperDash.Essentials.Devices.Common.Shades { InUpPosition = false; } - + _isMoving = false; _currentMovement = RequestedState.None; - + // Execute banked command if one exists if (_requestedState != RequestedState.None) { - Debug.LogMessage(LogEventLevel.Debug, this, $"Executing banked command: {_requestedState}"); - + 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; @@ -322,41 +335,47 @@ namespace PepperDash.Essentials.Devices.Common.Shades } } - void PulseOutput(ISwitchedOutput output, int pulseTime) + private void PulseOutput(ISwitchedOutput output, int pulseTime) { output.On(); - CTimer pulseTimer = new CTimer(new CTimerCallbackFunction((o) => output.Off()), pulseTime); + + var timer = new Timer(pulseTime) + { + AutoReset = false + }; + + timer.Elapsed += (sender, e) => + { + output.Off(); + timer.Dispose(); + }; + timer.Start(); } - /// - /// Attempts to get the port on teh specified device from config - /// - /// - /// - ISwitchedOutput GetSwitchedOutputFromDevice(string relayKey) + private ISwitchedOutput GetSwitchedOutputFromDevice(string relayKey) { - var portDevice = DeviceManager.GetDeviceForKey(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; } } @@ -364,7 +383,7 @@ namespace PepperDash.Essentials.Devices.Common.Shades } /// - /// Represents a ScreenLiftControllerFactory + /// Factory for ScreenLiftController devices /// public class ScreenLiftControllerFactory : EssentialsDeviceFactory { @@ -376,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); } From 7ad8218af0314fab558d08e6d09e70009875e4db Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Mon, 29 Dec 2025 13:03:47 -0600 Subject: [PATCH 09/16] fix: fusion controller now sets only associated room custom values --- .../Fusion/FusionCustomPropertiesBridge.cs | 72 ++++++++++--------- .../Fusion/IEssentialsRoomFusionController.cs | 14 ++-- ...alsRoomFusionControllerPropertiesConfig.cs | 9 ++- 3 files changed, 55 insertions(+), 40 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Fusion/FusionCustomPropertiesBridge.cs b/src/PepperDash.Essentials.Core/Fusion/FusionCustomPropertiesBridge.cs index a0cd8e6e..b170d28e 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,42 @@ 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; + + // 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; + + 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; + } + + 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 /// From 3878c85a7aef5fcf2722e00054fde08cdf12bc4f Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Mon, 29 Dec 2025 13:36:47 -0600 Subject: [PATCH 10/16] fix: use correct property name for isInUpPosition --- .../Messengers/IProjectorScreenLiftControlMessenger.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs index f63b4834..992de703 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs @@ -85,7 +85,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// Gets or sets the InUpPosition /// - [JsonProperty("inUpPosition", NullValueHandling = NullValueHandling.Ignore)] + [JsonProperty("isInUpPosition", NullValueHandling = NullValueHandling.Ignore)] public bool? InUpPosition { get; set; } /// From 7629921113c3260b2918471abfd465aa2e4e3d49 Mon Sep 17 00:00:00 2001 From: Nick Genovese Date: Mon, 29 Dec 2025 15:34:14 -0600 Subject: [PATCH 11/16] fix: a few logging updates --- .../Routing/RoutingFeedbackManager.cs | 237 +++++++++++++----- 1 file changed, 170 insertions(+), 67 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs index c51cec58..820b74e2 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.Verbose, + "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.Warning, + "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.Warning, + "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; From babb9a77dff06280acdca3bc6feae031231619d5 Mon Sep 17 00:00:00 2001 From: Nick Genovese Date: Mon, 29 Dec 2025 16:48:13 -0600 Subject: [PATCH 12/16] fix: a few logging updates --- .../Routing/RoutingFeedbackManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs index 820b74e2..e37d2d57 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs @@ -122,7 +122,7 @@ namespace PepperDash.Essentials.Core.Routing ) { Debug.LogMessage( - Serilog.Events.LogEventLevel.Verbose, + Serilog.Events.LogEventLevel.Debug, "Updating destination {destination} with inputPort {inputPort}", this, destination?.Key, @@ -237,7 +237,7 @@ namespace PepperDash.Essentials.Core.Routing if (room == null) { Debug.LogMessage( - Serilog.Events.LogEventLevel.Warning, + Serilog.Events.LogEventLevel.Debug, "No room found for display {destination}", this, destination.Key @@ -252,7 +252,7 @@ namespace PepperDash.Essentials.Core.Routing if (sourceList == null) { Debug.LogMessage( - Serilog.Events.LogEventLevel.Warning, + Serilog.Events.LogEventLevel.Debug, "No source list found for source list key {key}. Unable to find source for tieLine {sourceTieLine}", this, room.SourceListKey, From a983e2c87f68f31791d9c088ee9207fae9e6e3b5 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Tue, 30 Dec 2025 14:34:00 -0600 Subject: [PATCH 13/16] fix: save config only when values change --- .../Fusion/FusionCustomPropertiesBridge.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Core/Fusion/FusionCustomPropertiesBridge.cs b/src/PepperDash.Essentials.Core/Fusion/FusionCustomPropertiesBridge.cs index b170d28e..12f5d7e4 100644 --- a/src/PepperDash.Essentials.Core/Fusion/FusionCustomPropertiesBridge.cs +++ b/src/PepperDash.Essentials.Core/Fusion/FusionCustomPropertiesBridge.cs @@ -100,12 +100,15 @@ namespace PepperDash.Essentials.Core.Fusion 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."); } @@ -115,9 +118,13 @@ namespace PepperDash.Essentials.Core.Fusion if (helpMessage != null) { roomConfig.Properties["helpMessage"] = helpMessage.CustomFieldValue; + updateConfig = true; } - reconfigurable.SetConfig(roomConfig); + if (updateConfig) + { + reconfigurable.SetConfig(roomConfig); + } } catch (Exception e) { From 316bb849b4980905a8355a1eaf0dde400ac8d19d Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Tue, 30 Dec 2025 16:58:36 -0600 Subject: [PATCH 14/16] fix: update matrix routing inputs if endpoint online status changes --- .../Messengers/IMatrixRoutingMessenger.cs | 8 ++++++++ 1 file changed, 8 insertions(+) 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)) + })); + }; } } From 7f2bb078c8052c589bd986975b964eadfe2eb3fd Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 31 Dec 2025 12:20:40 -0600 Subject: [PATCH 15/16] fix: revert prop name to inUpPosition for screenlift messenger - refactor volume interfaces into separate files - IBasicVolumeControl implements IKeyName --- .config/dotnet-tools.json | 13 ++ .../DeviceTypeInterfaces/IAudioZone.cs | 10 ++ .../DeviceTypeInterfaces/IAudioZones.cs | 12 ++ .../IBasicVolumeControls.cs | 27 +++ .../IBasicVolumeWithFeedback.cs | 14 ++ .../IBasicVolumeWithFeedbackAdvanced.cs | 12 ++ .../IFullAudioSettings.cs | 41 +++++ .../IHasCurrentVolumeControls.cs | 17 ++ .../DeviceTypeInterfaces/IHasMuteControl.cs | 10 ++ .../IHasMuteControlWithFeedback.cs | 12 ++ .../DeviceTypeInterfaces/IHasVolumeControl.cs | 11 ++ .../IHasVolumeControlWithFeedback.cs | 11 ++ .../DeviceTypeInterfaces/IHasVolumeDevice.cs | 10 ++ .../DeviceTypeInterfaces/eVolumeLevelUnits.cs | 10 ++ .../Devices/IVolumeAndAudioInterfaces.cs | 161 ------------------ .../Messengers/DeviceVolumeMessenger.cs | 2 +- .../IProjectorScreenLiftControlMessenger.cs | 2 +- 17 files changed, 212 insertions(+), 163 deletions(-) create mode 100644 .config/dotnet-tools.json create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IAudioZone.cs create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IAudioZones.cs create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IBasicVolumeControls.cs create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IBasicVolumeWithFeedback.cs create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IBasicVolumeWithFeedbackAdvanced.cs create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IFullAudioSettings.cs create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasCurrentVolumeControls.cs create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasMuteControl.cs create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasMuteControlWithFeedback.cs create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasVolumeControl.cs create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasVolumeControlWithFeedback.cs create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasVolumeDevice.cs create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/eVolumeLevelUnits.cs delete mode 100644 src/PepperDash.Essentials.Core/Devices/IVolumeAndAudioInterfaces.cs 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.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/IProjectorScreenLiftControlMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs index 992de703..f63b4834 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs @@ -85,7 +85,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// Gets or sets the InUpPosition /// - [JsonProperty("isInUpPosition", NullValueHandling = NullValueHandling.Ignore)] + [JsonProperty("inUpPosition", NullValueHandling = NullValueHandling.Ignore)] public bool? InUpPosition { get; set; } /// From 57cd77f01949a9e5916d44171e5a0ad570b034ac Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 31 Dec 2025 14:04:04 -0600 Subject: [PATCH 16/16] fix: implement IKeyName for DspControlPoint --- src/PepperDash.Essentials.Devices.Common/DSP/DspBase.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 ///