mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-04-20 07:56:50 +00:00
feat: handle timers correctly and migrate to native Timer instead of CTimer
This commit is contained in:
parent
4f86009209
commit
18088d37a1
1 changed files with 107 additions and 45 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Crestron.SimplSharp;
|
using System.Timers;
|
||||||
using PepperDash.Core;
|
using PepperDash.Core;
|
||||||
using PepperDash.Essentials.Core.Config;
|
using PepperDash.Essentials.Core.Config;
|
||||||
|
|
||||||
|
|
@ -21,7 +21,12 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Debounce timers for each sink device to prevent rapid successive updates
|
/// Debounce timers for each sink device to prevent rapid successive updates
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly Dictionary<string, CTimer> updateTimers = new Dictionary<string, CTimer>();
|
private readonly Dictionary<string, Timer> updateTimers = new Dictionary<string, Timer>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lock object protecting all access to <see cref="updateTimers"/>.
|
||||||
|
/// </summary>
|
||||||
|
private readonly object _timerLock = new object();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Debounce delay in milliseconds
|
/// Debounce delay in milliseconds
|
||||||
|
|
@ -50,7 +55,6 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
midpointToSinksMap = new Dictionary<string, HashSet<string>>();
|
midpointToSinksMap = new Dictionary<string, HashSet<string>>();
|
||||||
|
|
||||||
var sinks = DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
|
var sinks = DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
|
||||||
var midpoints = DeviceManager.AllDevices.OfType<IRoutingWithFeedback>();
|
|
||||||
|
|
||||||
foreach (var sink in sinks)
|
foreach (var sink in sinks)
|
||||||
{
|
{
|
||||||
|
|
@ -211,9 +215,46 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a sink from every midpoint set in the map and re-adds it based on its
|
||||||
|
/// current input port. Call this whenever a sink's selected input changes so that
|
||||||
|
/// HandleMidpointUpdate always sees an up-to-date downstream set.
|
||||||
|
/// </summary>
|
||||||
|
private void RebuildMapForSink(IRoutingSinkWithSwitchingWithInputPort sink)
|
||||||
|
{
|
||||||
|
if (midpointToSinksMap == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Remove this sink from all existing midpoint sets
|
||||||
|
foreach (var set in midpointToSinksMap.Values)
|
||||||
|
set.Remove(sink.Key);
|
||||||
|
|
||||||
|
// Drop any midpoint entries that are now empty
|
||||||
|
var emptyKeys = midpointToSinksMap
|
||||||
|
.Where(kvp => kvp.Value.Count == 0)
|
||||||
|
.Select(kvp => kvp.Key)
|
||||||
|
.ToList();
|
||||||
|
foreach (var k in emptyKeys)
|
||||||
|
midpointToSinksMap.Remove(k);
|
||||||
|
|
||||||
|
// Re-add the sink under every midpoint that is upstream of its new input
|
||||||
|
if (sink.CurrentInputPort == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var upstreamMidpoints = GetUpstreamMidpoints(sink);
|
||||||
|
foreach (var midpointKey in upstreamMidpoints)
|
||||||
|
{
|
||||||
|
if (!midpointToSinksMap.ContainsKey(midpointKey))
|
||||||
|
midpointToSinksMap[midpointKey] = new HashSet<string>();
|
||||||
|
|
||||||
|
midpointToSinksMap[midpointKey].Add(sink.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the InputChanged event from a sink device.
|
/// Handles the InputChanged event from a sink device.
|
||||||
/// Triggers an update for the specific sink device.
|
/// Updates the midpoint-to-sink map for the new input path, then triggers
|
||||||
|
/// a source-info update for the sink.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sender">The sink device that reported an input change.</param>
|
/// <param name="sender">The sink device that reported an input change.</param>
|
||||||
/// <param name="currentInputPort">The new input port selected on the sink device.</param>
|
/// <param name="currentInputPort">The new input port selected on the sink device.</param>
|
||||||
|
|
@ -224,6 +265,10 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Keep the map current so HandleMidpointUpdate can find this sink
|
||||||
|
if (sender is IRoutingSinkWithSwitchingWithInputPort sinkWithInputPort)
|
||||||
|
RebuildMapForSink(sinkWithInputPort);
|
||||||
|
|
||||||
UpdateDestination(sender, currentInputPort);
|
UpdateDestination(sender, currentInputPort);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -255,15 +300,13 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
|
|
||||||
var key = destination.Key;
|
var key = destination.Key;
|
||||||
|
|
||||||
// Cancel existing timer for this sink
|
// Cancel and replace any existing timer under the lock so no callback
|
||||||
if (updateTimers.TryGetValue(key, out var existingTimer))
|
// can race with us while we swap the entry.
|
||||||
{
|
Timer timerToDispose = null;
|
||||||
existingTimer.Stop();
|
Timer newTimer = null;
|
||||||
existingTimer.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start new debounced timer
|
newTimer = new Timer(DEBOUNCE_MS) { AutoReset = false };
|
||||||
updateTimers[key] = new CTimer(_ =>
|
newTimer.Elapsed += (s, e) =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -281,13 +324,36 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if (updateTimers.ContainsKey(key))
|
// Remove the entry first so a concurrent UpdateDestination call
|
||||||
|
// cannot re-dispose whatever timer we're about to dispose.
|
||||||
|
Timer selfTimer = null;
|
||||||
|
lock (_timerLock)
|
||||||
{
|
{
|
||||||
updateTimers[key]?.Dispose();
|
if (updateTimers.TryGetValue(key, out var current) && ReferenceEquals(current, newTimer))
|
||||||
updateTimers.Remove(key);
|
{
|
||||||
|
selfTimer = current;
|
||||||
|
updateTimers.Remove(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
selfTimer?.Dispose();
|
||||||
}
|
}
|
||||||
}, null, DEBOUNCE_MS);
|
};
|
||||||
|
|
||||||
|
lock (_timerLock)
|
||||||
|
{
|
||||||
|
if (updateTimers.TryGetValue(key, out var existingTimer))
|
||||||
|
timerToDispose = existingTimer;
|
||||||
|
|
||||||
|
updateTimers[key] = newTimer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispose the old timer outside the lock to avoid holding the lock during disposal.
|
||||||
|
// Dispose implicitly stops the timer, preventing its Elapsed event from firing.
|
||||||
|
timerToDispose?.Dispose();
|
||||||
|
|
||||||
|
// Start after the lock is released so the Elapsed callback cannot deadlock
|
||||||
|
// trying to acquire _timerLock while we still hold it.
|
||||||
|
newTimer.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -384,7 +450,8 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Debug.LogMessage(ex, "Error getting sourceTieLine: {Exception}", this, ex);
|
Debug.LogError(this, "Error getting sourceTieLine: {message}", ex.Message);
|
||||||
|
Debug.LogDebug(ex, "StackTrace: ");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -408,6 +475,11 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
return roomDefaultDisplay.DefaultDisplay.Key == destination.Key;
|
return roomDefaultDisplay.DefaultDisplay.Key == destination.Key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ConfigReader.ConfigObject.GetDestinationListForKey(r.DestinationListKey)?.FirstOrDefault(d => d.Value.SinkKey == destination.Key) != null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -429,10 +501,8 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
|
|
||||||
if (sourceList == null)
|
if (sourceList == null)
|
||||||
{
|
{
|
||||||
Debug.LogMessage(
|
Debug.LogDebug(this,
|
||||||
Serilog.Events.LogEventLevel.Debug,
|
|
||||||
"No source list found for source list key {key}. Unable to find source for tieLine {sourceTieLine}",
|
"No source list found for source list key {key}. Unable to find source for tieLine {sourceTieLine}",
|
||||||
this,
|
|
||||||
room.SourceListKey,
|
room.SourceListKey,
|
||||||
sourceTieLine
|
sourceTieLine
|
||||||
);
|
);
|
||||||
|
|
@ -461,10 +531,8 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
|
|
||||||
if (source == null)
|
if (source == null)
|
||||||
{
|
{
|
||||||
Debug.LogMessage(
|
Debug.LogDebug(this,
|
||||||
Serilog.Events.LogEventLevel.Debug,
|
|
||||||
"No source found for device {key}. Creating transient source for {destination}",
|
"No source found for device {key}. Creating transient source for {destination}",
|
||||||
this,
|
|
||||||
sourceTieLine.SourcePort.ParentDevice.Key,
|
sourceTieLine.SourcePort.ParentDevice.Key,
|
||||||
destination
|
destination
|
||||||
);
|
);
|
||||||
|
|
@ -498,10 +566,8 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
{
|
{
|
||||||
if (!(tieLine.DestinationPort.ParentDevice is IRoutingInputs sink))
|
if (!(tieLine.DestinationPort.ParentDevice is IRoutingInputs sink))
|
||||||
{
|
{
|
||||||
Debug.LogMessage(
|
Debug.LogDebug(this,
|
||||||
Serilog.Events.LogEventLevel.Debug,
|
|
||||||
"TieLine destination {device} is not IRoutingInputs",
|
"TieLine destination {device} is not IRoutingInputs",
|
||||||
this,
|
|
||||||
tieLine.DestinationPort.ParentDevice.Key
|
tieLine.DestinationPort.ParentDevice.Key
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -540,17 +606,21 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
|
|
||||||
if (route != null && route.Routes != null && route.Routes.Count > 0)
|
if (route != null && route.Routes != null && route.Routes.Count > 0)
|
||||||
{
|
{
|
||||||
// Found a valid route - return the source TieLine
|
// Routes[0] is the hop nearest the source: its InputPort is the
|
||||||
|
// port on the first switching device that receives the signal from
|
||||||
|
// the source side. The TieLine whose DestinationPort matches that
|
||||||
|
// port is the exact tie that was traversed, giving us the precise
|
||||||
|
// source output port via SourcePort — regardless of how many output
|
||||||
|
// ports the source device has.
|
||||||
|
var firstHop = route.Routes[0];
|
||||||
var sourceTieLine = TieLineCollection.Default.FirstOrDefault(tl =>
|
var sourceTieLine = TieLineCollection.Default.FirstOrDefault(tl =>
|
||||||
tl.SourcePort.ParentDevice.Key == source.Key &&
|
tl.DestinationPort.Key == firstHop.InputPort.Key &&
|
||||||
tl.Type.HasFlag(signalType));
|
tl.DestinationPort.ParentDevice.Key == firstHop.InputPort.ParentDevice.Key);
|
||||||
|
|
||||||
if (sourceTieLine != null)
|
if (sourceTieLine != null)
|
||||||
{
|
{
|
||||||
Debug.LogMessage(
|
Debug.LogDebug(this,
|
||||||
Serilog.Events.LogEventLevel.Debug,
|
"Found route from {source} to {sink} with {count} hops",
|
||||||
"Found route from {source} to {sink} with {count} hops",
|
|
||||||
this,
|
|
||||||
source.Key,
|
source.Key,
|
||||||
sink.Key,
|
sink.Key,
|
||||||
route.Routes.Count
|
route.Routes.Count
|
||||||
|
|
@ -561,22 +631,14 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.LogMessage(
|
Debug.LogDebug(this, "No route found to any source from {sink}", sink.Key);
|
||||||
Serilog.Events.LogEventLevel.Debug,
|
|
||||||
"No route found to any source from {sink}",
|
|
||||||
this,
|
|
||||||
sink.Key
|
|
||||||
);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Debug.LogMessage(
|
Debug.LogError(this, "Error getting root tieLine: {message}", ex.Message);
|
||||||
ex,
|
Debug.LogDebug(ex, "StackTrace: ");
|
||||||
"Error getting root tieLine: {Exception}",
|
|
||||||
this,
|
|
||||||
ex
|
|
||||||
);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue