Compare commits

..

6 Commits

Author SHA1 Message Date
equinoy
c0971a4f8e Merge remote-tracking branch 'origin/screen-lift-controller-mute-logic' into feature/circuittype-property-versiport 2026-01-22 15:36:27 -06:00
equinoy
8bc6d4392b feat: add circuitType property to IOPortConfig and implement state inversion in digital input devices 2026-01-22 15:04:25 -06:00
Neil Dorin
4fa7a42330 Merge pull request #1376 from PepperDash/client-id-issues
fix: handle subsequent join calls and clientid/websocket client mismatches
2026-01-21 14:59:22 -07:00
Andrew Welker
9bad3ae21b fix: handle subsequent join calls and clientid/websocket client mismatches 2026-01-21 15:20:27 -06:00
Nick Genovese
f49901d3fa fix: Improve status messages in StatusMonitorCollection
Enhanced error and warning message generation to use monitor names when available, include counts, proper pluralization, and append "Offline" when issues are present. Avoided multiple enumerations by converting to lists. "Room Ok." is shown when no issues are detected.
2026-01-01 18:01:37 -06:00
Nick Genovese
7910b7931e feat: Add mute logic to ScreenLiftController
- adds a config value that mutes the display when the screen is in the up position
- screens will now mute/unmute based on their position if the config is set
2026-01-01 09:02:04 -06:00
8 changed files with 320 additions and 171 deletions

View File

@@ -21,6 +21,7 @@ namespace PepperDash.Essentials.Core.CrestronIO
public class GenericDigitalInputDevice : EssentialsBridgeableDevice, IDigitalInput, IHasFeedback
{
private DigitalInput inputPort;
private bool invertState;
/// <summary>
/// Gets or sets the InputStateFeedback
@@ -41,7 +42,10 @@ namespace PepperDash.Essentials.Core.CrestronIO
IOPortConfig config)
: base(key, name)
{
InputStateFeedback = new BoolFeedback("inputState", () => inputPort.State);
invertState = !string.IsNullOrEmpty(config.CircuitType) &&
config.CircuitType.Equals("NC", StringComparison.OrdinalIgnoreCase);
InputStateFeedback = new BoolFeedback("inputState", () => invertState ? !inputPort.State : inputPort.State);
AddPostActivationAction(() =>
{

View File

@@ -18,6 +18,7 @@ namespace PepperDash.Essentials.Core.CrestronIO
public class GenericVersiportDigitalInputDevice : EssentialsBridgeableDevice, IDigitalInput, IPartitionStateProvider, IHasFeedback
{
private Versiport inputPort;
private bool invertState;
/// <summary>
/// Gets or sets the InputStateFeedback
@@ -47,7 +48,10 @@ namespace PepperDash.Essentials.Core.CrestronIO
public GenericVersiportDigitalInputDevice(string key, string name, Func<IOPortConfig, Versiport> postActivationFunc, IOPortConfig config) :
base(key, name)
{
InputStateFeedback = new BoolFeedback("inputState", () => inputPort.DigitalIn);
invertState = !string.IsNullOrEmpty(config.CircuitType) &&
config.CircuitType.Equals("NC", StringComparison.OrdinalIgnoreCase);
InputStateFeedback = new BoolFeedback("inputState", () => invertState ? !inputPort.DigitalIn : inputPort.DigitalIn);
PartitionPresentFeedback = new BoolFeedback("partitionPresent", () => !inputPort.DigitalIn);
AddPostActivationAction(() =>

View File

@@ -37,5 +37,12 @@ namespace PepperDash.Essentials.Core.CrestronIO
/// </summary>
[JsonProperty("minimumChange")]
public int MinimumChange { get; set; }
/// <summary>
/// Gets or sets the circuit type: "NO" (Normally Open) or "NC" (Normally Closed)
/// If set to "NC", the input state will be inverted. Defaults to "NO" if not specified.
/// </summary>
[JsonProperty("circuitType")]
public string CircuitType { get; set; } = "NO";
}
}

View File

@@ -14,7 +14,7 @@ using PepperDash.Core;
namespace PepperDash.Essentials.Core
{
/// <summary>
///
///
/// </summary>
public class StatusMonitorCollection : IStatusMonitor
{
@@ -59,51 +59,61 @@ namespace PepperDash.Essentials.Core
void ProcessStatuses()
{
var InError = Monitors.Where(m => m.Status == MonitorStatus.InError);
var InWarning = Monitors.Where(m => m.Status == MonitorStatus.InWarning);
var IsOk = Monitors.Where(m => m.Status == MonitorStatus.IsOk);
var InError = Monitors.Where(m => m.Status == MonitorStatus.InError).ToList();
var InWarning = Monitors.Where(m => m.Status == MonitorStatus.InWarning).ToList();
var IsOk = Monitors.Where(m => m.Status == MonitorStatus.IsOk).ToList();
MonitorStatus initialStatus;
string prefix = "0:";
if (InError.Count() > 0)
if (InError.Any())
{
initialStatus = MonitorStatus.InError;
prefix = "3:";
}
else if (InWarning.Count() > 0)
else if (InWarning.Any())
{
initialStatus = MonitorStatus.InWarning;
prefix = "2:";
}
else if (IsOk.Count() > 0)
else if (IsOk.Any())
initialStatus = MonitorStatus.IsOk;
else
initialStatus = MonitorStatus.StatusUnknown;
// Build the error message string
if (InError.Count() > 0 || InWarning.Count() > 0)
{
StringBuilder sb = new StringBuilder(prefix);
if (InError.Count() > 0)
{
// Do string splits and joins
sb.Append(string.Format("{0} Errors:", InError.Count()));
foreach (var mon in InError)
sb.Append(string.Format("{0}, ", mon.Parent.Key));
}
if (InWarning.Count() > 0)
{
sb.Append(string.Format("{0} Warnings:", InWarning.Count()));
foreach (var mon in InWarning)
sb.Append(string.Format("{0}, ", mon.Parent.Key));
}
Message = sb.ToString();
}
else
{
Message = "Room Ok.";
}
if (InError.Any() || InWarning.Any())
{
var errorNames = InError
.Select(mon => mon.Parent is IKeyName keyName ? keyName.Name : mon.Parent.Key)
.ToList();
var warningNames = InWarning
.Select(mon => mon.Parent is IKeyName keyName ? keyName.Name : mon.Parent.Key)
.ToList();
var sb = new StringBuilder(prefix);
if (errorNames.Count > 0)
{
sb.Append($"{errorNames.Count} Error{(errorNames.Count > 1 ? "s" : "")}: ");
sb.Append(string.Join(", ", errorNames));
}
if (warningNames.Count > 0)
{
if (errorNames.Count > 0)
sb.Append("; ");
sb.Append($"{warningNames.Count} Warning{(warningNames.Count > 1 ? "s" : "")}: ");
sb.Append(string.Join(", ", warningNames));
}
sb.Append(" Offline");
Message = sb.ToString();
}
else
{
Message = "Room Ok.";
}
// Want to fire even if status doesn't change because the message may.
Status = initialStatus;

View File

@@ -19,7 +19,7 @@ namespace PepperDash.Essentials.Devices.Common.Shades
{
None,
Raise,
Lower
Lower,
}
/// <summary>
@@ -50,7 +50,8 @@ namespace PepperDash.Essentials.Devices.Common.Shades
get { return _isInUpPosition; }
set
{
if (value == _isInUpPosition) return;
if (value == _isInUpPosition)
return;
_isInUpPosition = value;
IsInUpPosition.FireUpdate();
PositionChanged?.Invoke(this, new EventArgs());
@@ -87,7 +88,11 @@ namespace PepperDash.Essentials.Devices.Common.Shades
/// <summary>
/// Constructor for ScreenLiftController
/// </summary>
public ScreenLiftController(string key, string name, ScreenLiftControllerConfigProperties config)
public ScreenLiftController(
string key,
string name,
ScreenLiftControllerConfigProperties config
)
: base(key, name)
{
Config = config;
@@ -105,27 +110,60 @@ namespace PepperDash.Essentials.Devices.Common.Shades
switch (Mode)
{
case eScreenLiftControlMode.momentary:
{
RaiseRelayConfig = Config.Relays["raise"];
LowerRelayConfig = Config.Relays["lower"];
break;
}
{
RaiseRelayConfig = Config.Relays["raise"];
LowerRelayConfig = Config.Relays["lower"];
break;
}
case eScreenLiftControlMode.latched:
{
LatchedRelayConfig = Config.Relays["latched"];
break;
}
{
LatchedRelayConfig = Config.Relays["latched"];
break;
}
}
IsInUpPosition.OutputChange += (sender, args) =>
{
this.LogDebug(
"ScreenLiftController '{name}' IsInUpPosition changed to {position}",
Name,
IsInUpPosition.BoolValue ? "Up" : "Down"
);
if (!Config.MuteOnScreenUp)
{
return;
}
if (args.BoolValue)
{
return;
}
if (DisplayDevice is IBasicVideoMuteWithFeedback videoMute)
{
this.LogInformation("Unmuting video because screen is down");
videoMute.VideoMuteOff();
}
};
IsInUpPosition.FireUpdate();
}
private void IsCoolingDownFeedback_OutputChange(object sender, FeedbackEventArgs e)
{
if (!DisplayDevice.IsCoolingDownFeedback.BoolValue && Type == eScreenLiftControlType.lift)
if (
!DisplayDevice.IsCoolingDownFeedback.BoolValue
&& Type == eScreenLiftControlType.lift
)
{
Raise();
return;
}
if (DisplayDevice.IsCoolingDownFeedback.BoolValue && Type == eScreenLiftControlType.screen)
if (
DisplayDevice.IsCoolingDownFeedback.BoolValue
&& Type == eScreenLiftControlType.screen
)
{
Raise();
return;
@@ -150,18 +188,18 @@ namespace PepperDash.Essentials.Devices.Common.Shades
switch (Mode)
{
case eScreenLiftControlMode.momentary:
{
this.LogDebug("Getting relays for {mode}", Mode);
RaiseRelay = GetSwitchedOutputFromDevice(RaiseRelayConfig.DeviceKey);
LowerRelay = GetSwitchedOutputFromDevice(LowerRelayConfig.DeviceKey);
break;
}
{
this.LogDebug("Getting relays for {mode}", Mode);
RaiseRelay = GetSwitchedOutputFromDevice(RaiseRelayConfig.DeviceKey);
LowerRelay = GetSwitchedOutputFromDevice(LowerRelayConfig.DeviceKey);
break;
}
case eScreenLiftControlMode.latched:
{
this.LogDebug("Getting relays for {mode}", Mode);
LatchedRelay = GetSwitchedOutputFromDevice(LatchedRelayConfig.DeviceKey);
break;
}
{
this.LogDebug("Getting relays for {mode}", Mode);
LatchedRelay = GetSwitchedOutputFromDevice(LatchedRelayConfig.DeviceKey);
break;
}
}
this.LogDebug("Getting display with key {displayKey}", DisplayDeviceKey);
@@ -172,7 +210,8 @@ namespace PepperDash.Essentials.Devices.Common.Shades
this.LogDebug("Subscribing to {displayKey} feedbacks", DisplayDeviceKey);
DisplayDevice.IsWarmingUpFeedback.OutputChange += IsWarmingUpFeedback_OutputChange;
DisplayDevice.IsCoolingDownFeedback.OutputChange += IsCoolingDownFeedback_OutputChange;
DisplayDevice.IsCoolingDownFeedback.OutputChange +=
IsCoolingDownFeedback_OutputChange;
}
return base.CustomActivate();
@@ -183,10 +222,17 @@ namespace PepperDash.Essentials.Devices.Common.Shades
/// </summary>
public void Raise()
{
if (RaiseRelay == null && LatchedRelay == null) return;
if (RaiseRelay == null && LatchedRelay == null)
return;
this.LogDebug("Raise called for {type}", Type);
if (Config.MuteOnScreenUp && DisplayDevice is IBasicVideoMuteWithFeedback videoMute)
{
this.LogInformation("Muting video because screen is going up");
videoMute.VideoMuteOn();
}
// If device is moving, bank the command
if (_isMoving)
{
@@ -200,33 +246,33 @@ namespace PepperDash.Essentials.Devices.Common.Shades
switch (Mode)
{
case eScreenLiftControlMode.momentary:
{
PulseOutput(RaiseRelay, RaiseRelayConfig.PulseTimeInMs);
{
PulseOutput(RaiseRelay, RaiseRelayConfig.PulseTimeInMs);
// Set moving flag and start timer if movement time is configured
if (RaiseRelayConfig.MoveTimeInMs > 0)
{
_isMoving = true;
_currentMovement = RequestedState.Raise;
if (_movementTimer.Enabled)
{
_movementTimer.Stop();
}
_movementTimer.Interval = RaiseRelayConfig.MoveTimeInMs;
_movementTimer.Start();
}
else
{
InUpPosition = true;
}
break;
}
case eScreenLiftControlMode.latched:
// Set moving flag and start timer if movement time is configured
if (RaiseRelayConfig.MoveTimeInMs > 0)
{
LatchedRelay.Off();
InUpPosition = true;
break;
_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;
}
}
}
@@ -235,7 +281,8 @@ namespace PepperDash.Essentials.Devices.Common.Shades
/// </summary>
public void Lower()
{
if (LowerRelay == null && LatchedRelay == null) return;
if (LowerRelay == null && LatchedRelay == null)
return;
this.LogDebug("Lower called for {type}", Type);
@@ -252,33 +299,33 @@ namespace PepperDash.Essentials.Devices.Common.Shades
switch (Mode)
{
case eScreenLiftControlMode.momentary:
{
PulseOutput(LowerRelay, LowerRelayConfig.PulseTimeInMs);
{
PulseOutput(LowerRelay, LowerRelayConfig.PulseTimeInMs);
// Set moving flag and start timer if movement time is configured
if (LowerRelayConfig.MoveTimeInMs > 0)
{
_isMoving = true;
_currentMovement = RequestedState.Lower;
if (_movementTimer.Enabled)
{
_movementTimer.Stop();
}
_movementTimer.Interval = LowerRelayConfig.MoveTimeInMs;
_movementTimer.Start();
}
else
{
InUpPosition = false;
}
break;
}
case eScreenLiftControlMode.latched:
// Set moving flag and start timer if movement time is configured
if (LowerRelayConfig.MoveTimeInMs > 0)
{
LatchedRelay.On();
InUpPosition = false;
break;
_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;
}
}
}
@@ -339,16 +386,13 @@ namespace PepperDash.Essentials.Devices.Common.Shades
{
output.On();
var timer = new Timer(pulseTime)
{
AutoReset = false
};
var timer = new Timer(pulseTime) { AutoReset = false };
timer.Elapsed += (sender, e) =>
{
output.Off();
timer.Dispose();
};
{
output.Off();
timer.Dispose();
};
timer.Start();
}
@@ -361,7 +405,10 @@ namespace PepperDash.Essentials.Devices.Common.Shades
}
else
{
this.LogWarning("Error: Unable to get relay device with key '{relayKey}'", relayKey);
this.LogWarning(
"Error: Unable to get relay device with key '{relayKey}'",
relayKey
);
return null;
}
}
@@ -375,11 +422,13 @@ namespace PepperDash.Essentials.Devices.Common.Shades
}
else
{
this.LogWarning("Error: Unable to get display device with key '{displayKey}'", displayKey);
this.LogWarning(
"Error: Unable to get display device with key '{displayKey}'",
displayKey
);
return null;
}
}
}
/// <summary>
@@ -387,7 +436,7 @@ namespace PepperDash.Essentials.Devices.Common.Shades
/// </summary>
public class ScreenLiftControllerFactory : EssentialsDeviceFactory<RelayControlledShade>
{
/// <summary>
/// <summary>
/// Constructor for ScreenLiftControllerFactory
/// </summary>
public ScreenLiftControllerFactory()
@@ -404,4 +453,4 @@ namespace PepperDash.Essentials.Devices.Common.Shades
return new ScreenLiftController(dc.Key, dc.Name, props);
}
}
}
}

View File

@@ -5,37 +5,41 @@ using PepperDash.Essentials.Core.DeviceTypeInterfaces;
namespace PepperDash.Essentials.Devices.Common.Shades
{
/// <summary>
/// Represents a ScreenLiftControllerConfigProperties
/// </summary>
public class ScreenLiftControllerConfigProperties
{
/// <summary>
/// Gets or sets the DisplayDeviceKey
/// Represents a ScreenLiftControllerConfigProperties
/// </summary>
[JsonProperty("displayDeviceKey")]
public string DisplayDeviceKey { get; set; }
public class ScreenLiftControllerConfigProperties
{
/// <summary>
/// Gets or sets the DisplayDeviceKey
/// </summary>
[JsonProperty("displayDeviceKey")]
public string DisplayDeviceKey { get; set; }
/// <summary>
/// Gets or sets the Type
/// </summary>
[JsonProperty("type")]
[JsonConverter(typeof(StringEnumConverter))]
public eScreenLiftControlType Type { get; set; }
/// <summary>
/// Gets or sets the Type
/// </summary>
[JsonProperty("type")]
[JsonConverter(typeof(StringEnumConverter))]
public eScreenLiftControlType Type { get; set; }
/// <summary>
/// Gets or sets the Mode
/// </summary>
[JsonProperty("mode")]
[JsonConverter(typeof(StringEnumConverter))]
public eScreenLiftControlMode Mode { get; set; }
/// <summary>
/// Gets or sets the Mode
/// </summary>
[JsonProperty("mode")]
[JsonConverter(typeof(StringEnumConverter))]
public eScreenLiftControlMode Mode { get; set; }
/// <summary>
/// Gets or sets the Relays
/// </summary>
[JsonProperty("relays")]
public Dictionary<string, ScreenLiftRelaysConfig> Relays { get; set; }
/// <summary>
/// Gets or sets the Relays
/// </summary>
[JsonProperty("relays")]
public Dictionary<string, ScreenLiftRelaysConfig> Relays { get; set; }
}
}
/// <summary>
/// Mutes the display when the screen is in the up position
/// </summary>
[JsonProperty("muteOnScreenUp")]
public bool MuteOnScreenUp { get; set; }
}
}

View File

@@ -69,10 +69,11 @@ namespace PepperDash.Essentials.WebSocketServer
private readonly ConcurrentDictionary<string, string> pendingClientRegistrations = new ConcurrentDictionary<string, string>();
/// <summary>
/// Stores queues of pending client IDs per token for legacy clients (FIFO)
/// This ensures thread-safety when multiple legacy clients use the same token
/// Stores pending client registrations with timestamp for legacy clients
/// Key is token, Value is list of (clientId, timestamp) tuples
/// Most recent registration is used to handle duplicate join requests
/// </summary>
private readonly ConcurrentDictionary<string, ConcurrentQueue<string>> legacyClientIdQueues = new ConcurrentDictionary<string, ConcurrentQueue<string>>();
private readonly ConcurrentDictionary<string, ConcurrentBag<(string clientId, DateTime timestamp)>> legacyClientRegistrations = new ConcurrentDictionary<string, ConcurrentBag<(string, DateTime)>>();
/// <summary>
/// Gets the collection of UI clients
@@ -736,15 +737,23 @@ namespace PepperDash.Essentials.WebSocketServer
private UiClient BuildUiClient(string roomKey, JoinToken token, string key)
{
// Dequeue the next clientId for legacy client support (FIFO per token)
// Get the most recent unused clientId for this token (legacy support)
// 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))
if (legacyClientRegistrations.TryGetValue(key, out var registrations))
{
clientId = dequeuedId;
this.LogVerbose("Dequeued legacy clientId {clientId} for token {token}", clientId, key);
// Get most recent registration
var sorted = registrations.OrderByDescending(r => r.timestamp).ToList();
if (sorted.Any())
{
clientId = sorted.First().clientId;
// Remove it from the bag
var newBag = new ConcurrentBag<(string, DateTime)>(sorted.Skip(1));
legacyClientRegistrations.TryUpdate(key, newBag, registrations);
this.LogVerbose("Assigned most recent legacy clientId {clientId} for token {token}", clientId, key);
}
}
var c = new UiClient($"uiclient-{key}-{roomKey}-{clientId}", clientId, token.Token, token.TouchpanelKey);
this.LogInformation("Constructing UiClient with key {key} and temporary ID (will be set from query param)", key);
c.Controller = _parent;
@@ -753,8 +762,8 @@ namespace PepperDash.Essentials.WebSocketServer
c.Server = this; // Give UiClient access to server for ID registration
// Don't add to uiClients yet - will be added in OnOpen after ID is set from query param
c.ConnectionClosed += (o, a) =>
c.ConnectionClosed += (o, a) =>
{
uiClients.TryRemove(a.ClientId, out _);
// Clean up any pending registrations for this token
@@ -765,11 +774,11 @@ namespace PepperDash.Essentials.WebSocketServer
{
pendingClientRegistrations.TryRemove(k, out _);
}
// Clean up legacy queue if empty
if (legacyClientIdQueues.TryGetValue(key, out var legacyQueue) && legacyQueue.IsEmpty)
// Clean up legacy registrations if empty
if (legacyClientRegistrations.TryGetValue(key, out var legacyBag) && legacyBag.IsEmpty)
{
legacyClientIdQueues.TryRemove(key, out _);
legacyClientRegistrations.TryRemove(key, out _);
}
};
return c;
@@ -785,7 +794,7 @@ namespace PepperDash.Essentials.WebSocketServer
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 _))
{
@@ -799,11 +808,63 @@ namespace PepperDash.Essentials.WebSocketServer
this.LogWarning("Replacing existing client with duplicate id {id}", id);
return client;
});
this.LogInformation("Successfully registered UiClient with ID {clientId} for token {token}", clientId, tokenKey);
return true;
}
/// <summary>
/// Updates a client's ID when a mismatch is detected between stored ID and message ID
/// </summary>
/// <param name="oldClientId">The current/old client ID</param>
/// <param name="newClientId">The new client ID from the message</param>
/// <param name="tokenKey">The token key for validation</param>
/// <returns>True if update successful, false otherwise</returns>
public bool UpdateClientId(string oldClientId, string newClientId, string tokenKey)
{
if (string.IsNullOrEmpty(oldClientId) || string.IsNullOrEmpty(newClientId))
{
this.LogWarning("Cannot update client ID with null or empty values");
return false;
}
if (oldClientId == newClientId)
{
return true; // No update needed
}
// Verify the new clientId was registered for this token
var registrationKey = $"{tokenKey}-{newClientId}";
if (!pendingClientRegistrations.TryRemove(registrationKey, out _))
{
this.LogWarning("Cannot update to unregistered clientId {newClientId} for token {token}", newClientId, tokenKey);
return false;
}
// Get the existing client
if (!uiClients.TryRemove(oldClientId, out var client))
{
this.LogWarning("Cannot find client with old ID {oldClientId}", oldClientId);
return false;
}
// Update the client's ID
client.UpdateId(newClientId);
// Re-add with new ID
if (!uiClients.TryAdd(newClientId, client))
{
// If add fails, try to restore old entry
uiClients.TryAdd(oldClientId, client);
client.UpdateId(oldClientId);
this.LogError("Failed to update client ID from {oldClientId} to {newClientId}", oldClientId, newClientId);
return false;
}
this.LogInformation("Successfully updated client ID from {oldClientId} to {newClientId}", oldClientId, newClientId);
return true;
}
/// <summary>
/// Registers a UiClient using legacy flow (for backwards compatibility with older clients)
/// </summary>
@@ -821,7 +882,7 @@ namespace PepperDash.Essentials.WebSocketServer
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);
}
@@ -1133,16 +1194,17 @@ namespace PepperDash.Essentials.WebSocketServer
// Generate a client ID for this join request
var clientId = $"{Utilities.GetNextClientId()}";
var now = DateTime.UtcNow;
// 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<string>());
queue.Enqueue(clientId);
this.LogVerbose("Assigning ClientId: {clientId} for token: {token}", clientId, token);
// For legacy clients, store with timestamp instead of FIFO queue
var legacyBag = legacyClientRegistrations.GetOrAdd(token, _ => new ConcurrentBag<(string, DateTime)>());
legacyBag.Add((clientId, now));
this.LogVerbose("Assigning ClientId: {clientId} for token: {token} at {timestamp}", clientId, token, now);
// Construct WebSocket URL with clientId query parameter
var wsProtocol = "ws";

View File

@@ -26,6 +26,15 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary>
public string Id { get; private set; }
/// <summary>
/// Updates the client ID - only accessible from within the assembly (e.g., by the server)
/// </summary>
/// <param name="newId">The new client ID</param>
internal void UpdateId(string newId)
{
Id = newId;
}
/// <summary>
/// Token associated with this client
/// </summary>