mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-07-02 10:38:16 +00:00
Merge pull request #1374 from PepperDash/tieline-visualisation
tieline visualisation
This commit is contained in:
commit
f31f0611f1
19 changed files with 2040 additions and 844 deletions
|
|
@ -1,6 +1,6 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>2.19.4-local</Version>
|
||||
<Version>2.29.0-local</Version>
|
||||
<InformationalVersion>$(Version)</InformationalVersion>
|
||||
<Authors>PepperDash Technology</Authors>
|
||||
<Company>PepperDash Technology</Company>
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ using Serilog.Events;
|
|||
namespace PepperDash.Core
|
||||
{
|
||||
//*********************************************************************************************************
|
||||
/// <summary>
|
||||
/// Represents a Device
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Represents a Device
|
||||
/// </summary>
|
||||
public class Device : IKeyName
|
||||
{
|
||||
|
||||
|
|
@ -15,25 +15,15 @@ namespace PepperDash.Core
|
|||
/// Unique Key
|
||||
/// </summary>
|
||||
public string Key { get; protected set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the Name
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Gets or sets the Name
|
||||
/// </summary>
|
||||
public string Name { get; protected set; }
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool Enabled { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// A place to store reference to the original config object, if any. These values should
|
||||
/// NOT be used as properties on the device as they are all publicly-settable values.
|
||||
/// </summary>
|
||||
//public DeviceConfig Config { get; private set; }
|
||||
/// <summary>
|
||||
/// Helper method to check if Config exists
|
||||
/// </summary>
|
||||
//public bool HasConfig { get { return Config != null; } }
|
||||
|
||||
List<Action> _PreActivationActions;
|
||||
List<Action> _PostActivationActions;
|
||||
|
||||
|
|
@ -86,9 +76,9 @@ namespace PepperDash.Core
|
|||
/// Adds a post activation action
|
||||
/// </summary>
|
||||
/// <param name="act"></param>
|
||||
/// <summary>
|
||||
/// AddPostActivationAction method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// AddPostActivationAction method
|
||||
/// </summary>
|
||||
public void AddPostActivationAction(Action act)
|
||||
{
|
||||
if (_PostActivationActions == null)
|
||||
|
|
@ -96,9 +86,9 @@ namespace PepperDash.Core
|
|||
_PostActivationActions.Add(act);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PreActivate method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// PreActivate method
|
||||
/// </summary>
|
||||
public void PreActivate()
|
||||
{
|
||||
if (_PreActivationActions != null)
|
||||
|
|
@ -115,9 +105,9 @@ namespace PepperDash.Core
|
|||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activate method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Activate method
|
||||
/// </summary>
|
||||
public bool Activate()
|
||||
{
|
||||
//if (_PreActivationActions != null)
|
||||
|
|
@ -128,9 +118,9 @@ namespace PepperDash.Core
|
|||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PostActivate method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// PostActivate method
|
||||
/// </summary>
|
||||
public void PostActivate()
|
||||
{
|
||||
if (_PostActivationActions != null)
|
||||
|
|
@ -153,9 +143,9 @@ namespace PepperDash.Core
|
|||
/// do not need to call base.CustomActivate()
|
||||
/// </summary>
|
||||
/// <returns>true if device activated successfully.</returns>
|
||||
/// <summary>
|
||||
/// CustomActivate method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// CustomActivate method
|
||||
/// </summary>
|
||||
public virtual bool CustomActivate() { return true; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -182,15 +172,15 @@ namespace PepperDash.Core
|
|||
if (o is bool && !(bool)o) a();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the object, including its key and name.
|
||||
/// </summary>
|
||||
/// <remarks>The returned string is formatted as "{Key} - {Name}". If the <c>Name</c> property is
|
||||
/// null or empty, "---" is used in place of the name.</remarks>
|
||||
/// <returns>A string that represents the object, containing the key and name in the format "{Key} - {Name}".</returns>
|
||||
/// <summary>
|
||||
/// ToString method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Returns a string representation of the object, including its key and name.
|
||||
/// </summary>
|
||||
/// <remarks>The returned string is formatted as "{Key} - {Name}". If the <c>Name</c> property is
|
||||
/// null or empty, "---" is used in place of the name.</remarks>
|
||||
/// <returns>A string that represents the object, containing the key and name in the format "{Key} - {Name}".</returns>
|
||||
/// <summary>
|
||||
/// ToString method
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("{0} - {1}", Key, string.IsNullOrEmpty(Name) ? "---" : Name);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
|
|
@ -12,11 +8,11 @@ using Crestron.SimplSharp;
|
|||
using WebSocketSharp;
|
||||
using System.Security.Authentication;
|
||||
using WebSocketSharp.Net;
|
||||
using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.IO;
|
||||
using Org.BouncyCastle.Asn1.X509;
|
||||
using Serilog.Formatting;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog.Formatting.Json;
|
||||
|
||||
namespace PepperDash.Core
|
||||
|
|
@ -27,18 +23,23 @@ namespace PepperDash.Core
|
|||
public class DebugWebsocketSink : ILogEventSink
|
||||
{
|
||||
private HttpServer _httpsServer;
|
||||
|
||||
|
||||
private string _path = "/debug/join/";
|
||||
private const string _certificateName = "selfCres";
|
||||
private const string _certificatePassword = "cres12345";
|
||||
|
||||
public int Port
|
||||
{ get
|
||||
{
|
||||
|
||||
if(_httpsServer == null) return 0;
|
||||
private static string CertPath =>
|
||||
$"{Path.DirectorySeparatorChar}user{Path.DirectorySeparatorChar}{_certificateName}.pfx";
|
||||
|
||||
|
||||
public int Port
|
||||
{
|
||||
get
|
||||
{
|
||||
|
||||
if (_httpsServer == null) return 0;
|
||||
return _httpsServer.Port;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string Url
|
||||
|
|
@ -54,60 +55,105 @@ namespace PepperDash.Core
|
|||
/// Gets or sets the IsRunning
|
||||
/// </summary>
|
||||
public bool IsRunning { get => _httpsServer?.IsListening ?? false; }
|
||||
|
||||
|
||||
|
||||
private readonly ITextFormatter _textFormatter;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DebugWebsocketSink"/> class with the specified text formatter.
|
||||
/// </summary>
|
||||
/// <remarks>This constructor initializes the WebSocket sink and ensures that a certificate is
|
||||
/// available for secure communication. If the required certificate does not exist, it will be created
|
||||
/// automatically. Additionally, the sink is configured to stop the server when the program is
|
||||
/// stopping.</remarks>
|
||||
/// <param name="formatProvider">The text formatter used to format log messages. If null, a default JSON formatter is used.</param>
|
||||
public DebugWebsocketSink(ITextFormatter formatProvider)
|
||||
{
|
||||
|
||||
_textFormatter = formatProvider ?? new JsonFormatter();
|
||||
|
||||
if (!File.Exists($"\\user\\{_certificateName}.pfx"))
|
||||
CreateCert(null);
|
||||
if (!File.Exists(CertPath))
|
||||
CreateCert();
|
||||
|
||||
CrestronEnvironment.ProgramStatusEventHandler += type =>
|
||||
{
|
||||
if (type == eProgramStatusEventType.Stopping)
|
||||
{
|
||||
StopServer();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void CreateCert(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
//Debug.Console(0,"CreateCert Creating Utility");
|
||||
CrestronConsole.PrintLine("CreateCert Creating Utility");
|
||||
//var utility = new CertificateUtility();
|
||||
var utility = new BouncyCertificate();
|
||||
//Debug.Console(0, "CreateCert Calling CreateCert");
|
||||
CrestronConsole.PrintLine("CreateCert Calling CreateCert");
|
||||
//utility.CreateCert();
|
||||
CrestronEnvironment.ProgramStatusEventHandler += type =>
|
||||
{
|
||||
if (type == eProgramStatusEventType.Stopping)
|
||||
StopServer();
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
// CrestronEnvironment is not available in test / dev environments — safe to skip.
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateCert()
|
||||
{
|
||||
// NOTE: This method is called from the constructor, which is itself called during Debug's static
|
||||
// constructor before _logger is assigned. Do NOT call any Debug.Log* methods here — use
|
||||
// CrestronConsole.PrintLine only, to avoid a NullReferenceException that would poison the Debug type.
|
||||
try
|
||||
{
|
||||
var ipAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
|
||||
var hostName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0);
|
||||
var domainName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, 0);
|
||||
|
||||
//Debug.Console(0, "DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress);
|
||||
CrestronConsole.PrintLine(string.Format("DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress));
|
||||
CrestronConsole.PrintLine(string.Format("CreateCert: DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress));
|
||||
|
||||
var certificate = utility.CreateSelfSignedCertificate(string.Format("CN={0}.{1}", hostName, domainName), new[] { string.Format("{0}.{1}", hostName, domainName), ipAddress }, new[] { KeyPurposeID.id_kp_serverAuth, KeyPurposeID.id_kp_clientAuth });
|
||||
//Crestron fails to let us do this...perhaps it should be done through their Dll's but haven't tested
|
||||
//Debug.Print($"CreateCert Storing Certificate To My.LocalMachine");
|
||||
//utility.AddCertToStore(certificate, StoreName.My, StoreLocation.LocalMachine);
|
||||
//Debug.Console(0, "CreateCert Saving Cert to \\user\\");
|
||||
CrestronConsole.PrintLine("CreateCert Saving Cert to \\user\\");
|
||||
utility.CertificatePassword = _certificatePassword;
|
||||
utility.WriteCertificate(certificate, @"\user\", _certificateName);
|
||||
//Debug.Console(0, "CreateCert Ending CreateCert");
|
||||
CrestronConsole.PrintLine("CreateCert Ending CreateCert");
|
||||
var subjectName = string.Format("CN={0}.{1}", hostName, domainName);
|
||||
var fqdn = string.Format("{0}.{1}", hostName, domainName);
|
||||
|
||||
using (var rsa = RSA.Create(2048))
|
||||
{
|
||||
|
||||
var request = new CertificateRequest(
|
||||
subjectName,
|
||||
rsa,
|
||||
HashAlgorithmName.SHA256,
|
||||
RSASignaturePadding.Pkcs1);
|
||||
|
||||
// Subject Key Identifier
|
||||
request.CertificateExtensions.Add(
|
||||
new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
|
||||
|
||||
// Extended Key Usage: server + client auth
|
||||
request.CertificateExtensions.Add(
|
||||
new X509EnhancedKeyUsageExtension(
|
||||
new OidCollection
|
||||
{
|
||||
new Oid("1.3.6.1.5.5.7.3.1"), // id-kp-serverAuth
|
||||
new Oid("1.3.6.1.5.5.7.3.2") // id-kp-clientAuth
|
||||
},
|
||||
false));
|
||||
|
||||
// Subject Alternative Names: DNS + IP
|
||||
var sanBuilder = new SubjectAlternativeNameBuilder();
|
||||
sanBuilder.AddDnsName(fqdn);
|
||||
if (System.Net.IPAddress.TryParse(ipAddress, out var ip))
|
||||
sanBuilder.AddIpAddress(ip);
|
||||
request.CertificateExtensions.Add(sanBuilder.Build());
|
||||
|
||||
var notBefore = DateTimeOffset.UtcNow;
|
||||
var notAfter = notBefore.AddYears(2);
|
||||
|
||||
using (var cert = request.CreateSelfSigned(notBefore, notAfter))
|
||||
{
|
||||
|
||||
var separator = Path.DirectorySeparatorChar;
|
||||
var outputPath = string.Format("{0}user{1}{2}.pfx", separator, separator, _certificateName);
|
||||
|
||||
var pfxBytes = cert.Export(X509ContentType.Pfx, _certificatePassword);
|
||||
File.WriteAllBytes(outputPath, pfxBytes);
|
||||
|
||||
CrestronConsole.PrintLine(string.Format("CreateCert: Certificate written to {0}", outputPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Debug.Console(0, "WSS CreateCert Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace);
|
||||
CrestronConsole.PrintLine(string.Format("WSS CreateCert Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace));
|
||||
CrestronConsole.PrintLine(string.Format("WSS CreateCert Failed: {0}\r\n{1}", ex.Message, ex.StackTrace));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -126,14 +172,37 @@ namespace PepperDash.Core
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// StartServerAndSetPort method
|
||||
/// Starts the WebSocket server on the specified port and configures it with the appropriate certificate.
|
||||
/// </summary>
|
||||
/// <remarks>This method initializes the WebSocket server and binds it to the specified port. It
|
||||
/// also applies the server's certificate for secure communication. Ensure that the port is not already in use
|
||||
/// and that the certificate file is accessible.</remarks>
|
||||
/// <param name="port">The port number on which the WebSocket server will listen. Must be a valid, non-negative port number.</param>
|
||||
public void StartServerAndSetPort(int port)
|
||||
{
|
||||
Debug.Console(0, "Starting Websocket Server on port: {0}", port);
|
||||
Debug.LogInformation("Starting Websocket Server on port: {0}", port);
|
||||
|
||||
|
||||
Start(port, $"\\user\\{_certificateName}.pfx", _certificatePassword);
|
||||
Start(port, CertPath, _certificatePassword);
|
||||
}
|
||||
|
||||
private static X509Certificate2 LoadOrRecreateCert(string certPath, string certPassword)
|
||||
{
|
||||
try
|
||||
{
|
||||
// EphemeralKeySet is required on Linux/OpenSSL (Crestron 4-series) to avoid
|
||||
// key-container persistence failures, and avoids the private key export restriction.
|
||||
return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Cert is stale or was generated by an incompatible library (e.g. old BouncyCastle output).
|
||||
// Delete it, regenerate with the BCL path, and retry once.
|
||||
CrestronConsole.PrintLine(string.Format("SSL cert load failed ({0}); regenerating...", ex.Message));
|
||||
try { File.Delete(certPath); } catch { }
|
||||
CreateCert();
|
||||
return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet);
|
||||
}
|
||||
}
|
||||
|
||||
private void Start(int port, string certPath = "", string certPassword = "")
|
||||
|
|
@ -142,66 +211,37 @@ namespace PepperDash.Core
|
|||
{
|
||||
_httpsServer = new HttpServer(port, true);
|
||||
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(certPath))
|
||||
{
|
||||
Debug.Console(0, "Assigning SSL Configuration");
|
||||
_httpsServer.SslConfiguration = new ServerSslConfiguration(new X509Certificate2(certPath, certPassword))
|
||||
{
|
||||
ClientCertificateRequired = false,
|
||||
CheckCertificateRevocation = false,
|
||||
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls,
|
||||
//this is just to test, you might want to actually validate
|
||||
ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
|
||||
{
|
||||
Debug.Console(0, "HTTPS ClientCerticateValidation Callback triggered");
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
Debug.Console(0, "Adding Debug Client Service");
|
||||
_httpsServer.AddWebSocketService<DebugClient>(_path);
|
||||
Debug.Console(0, "Assigning Log Info");
|
||||
_httpsServer.Log.Level = LogLevel.Trace;
|
||||
_httpsServer.Log.Output = (d, s) =>
|
||||
{
|
||||
uint level;
|
||||
Debug.LogInformation("Assigning SSL Configuration");
|
||||
|
||||
switch(d.Level)
|
||||
{
|
||||
case WebSocketSharp.LogLevel.Fatal:
|
||||
level = 3;
|
||||
break;
|
||||
case WebSocketSharp.LogLevel.Error:
|
||||
level = 2;
|
||||
break;
|
||||
case WebSocketSharp.LogLevel.Warn:
|
||||
level = 1;
|
||||
break;
|
||||
case WebSocketSharp.LogLevel.Info:
|
||||
level = 0;
|
||||
break;
|
||||
case WebSocketSharp.LogLevel.Debug:
|
||||
level = 4;
|
||||
break;
|
||||
case WebSocketSharp.LogLevel.Trace:
|
||||
level = 5;
|
||||
break;
|
||||
default:
|
||||
level = 4;
|
||||
break;
|
||||
}
|
||||
|
||||
Debug.Console(level, "{1} {0}\rCaller:{2}\rMessage:{3}\rs:{4}", d.Level.ToString(), d.Date.ToString(), d.Caller.ToString(), d.Message, s);
|
||||
};
|
||||
Debug.Console(0, "Starting");
|
||||
_httpsServer.SslConfiguration.ServerCertificate = LoadOrRecreateCert(certPath, certPassword);
|
||||
_httpsServer.SslConfiguration.ClientCertificateRequired = false;
|
||||
_httpsServer.SslConfiguration.CheckCertificateRevocation = false;
|
||||
_httpsServer.SslConfiguration.EnabledSslProtocols = SslProtocols.Tls12;
|
||||
//this is just to test, you might want to actually validate
|
||||
_httpsServer.SslConfiguration.ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
|
||||
{
|
||||
Debug.LogInformation("HTTPS ClientCerticateValidation Callback triggered");
|
||||
return true;
|
||||
};
|
||||
}
|
||||
Debug.LogInformation("Adding Debug Client Service");
|
||||
_httpsServer.AddWebSocketService<DebugClient>(_path);
|
||||
Debug.LogInformation("Assigning Log Info");
|
||||
_httpsServer.Log.Level = LogLevel.Trace;
|
||||
_httpsServer.Log.Output = WriteWebSocketInternalLog;
|
||||
Debug.LogInformation("Starting");
|
||||
|
||||
_httpsServer.Start();
|
||||
Debug.Console(0, "Ready");
|
||||
Debug.LogInformation("Ready");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.Console(0, "WebSocket Failed to start {0}", ex.Message);
|
||||
Debug.LogError(ex, "WebSocket Failed to start {0}", ex.Message);
|
||||
Debug.LogVerbose("Stack Trace:\r{0}", ex.StackTrace);
|
||||
// Null out the server so callers can detect failure via IsRunning / Url null guards.
|
||||
_httpsServer = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -210,10 +250,68 @@ namespace PepperDash.Core
|
|||
/// </summary>
|
||||
public void StopServer()
|
||||
{
|
||||
Debug.Console(0, "Stopping Websocket Server");
|
||||
_httpsServer?.Stop();
|
||||
Debug.LogInformation("Stopping Websocket Server");
|
||||
|
||||
_httpsServer = null;
|
||||
try
|
||||
{
|
||||
if (_httpsServer == null || !_httpsServer.IsListening)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent close-sequence internal websocket logs from re-entering the logging pipeline.
|
||||
_httpsServer.Log.Output = (d, s) => { };
|
||||
|
||||
var serviceHost = _httpsServer.WebSocketServices[_path];
|
||||
|
||||
if (serviceHost == null)
|
||||
{
|
||||
_httpsServer.Stop();
|
||||
_httpsServer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
serviceHost.Sessions.Broadcast("Server is stopping");
|
||||
|
||||
foreach (var session in serviceHost.Sessions.Sessions)
|
||||
{
|
||||
if (session?.Context?.WebSocket != null && session.Context.WebSocket.IsAlive)
|
||||
{
|
||||
session.Context.WebSocket.Close(1001, "Server is stopping");
|
||||
}
|
||||
}
|
||||
|
||||
_httpsServer.Stop();
|
||||
|
||||
_httpsServer = null;
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError(ex, "WebSocket Failed to stop gracefully {0}", ex.Message);
|
||||
Debug.LogVerbose("Stack Trace\r\n{0}", ex.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteWebSocketInternalLog(LogData data, string supplemental)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var message = string.IsNullOrWhiteSpace(data.Message) ? "<none>" : data.Message;
|
||||
var details = string.IsNullOrWhiteSpace(supplemental) ? string.Empty : string.Format(" | details: {0}", supplemental);
|
||||
|
||||
// Use direct console output to avoid recursive log sink calls.
|
||||
CrestronConsole.PrintLine(string.Format("WS[{0}] {1} | message: {2}{3}", data.Level, data.Date, message, details));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Never throw from websocket log callback.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using Crestron.SimplSharp;
|
|||
using Crestron.SimplSharp.WebScripting;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PepperDash.Core.Logging;
|
||||
using PepperDash.Core.Web.RequestHandlers;
|
||||
|
||||
namespace PepperDash.Core.Web
|
||||
|
|
@ -25,29 +26,26 @@ namespace PepperDash.Core.Web
|
|||
private readonly CCriticalSection _serverLock = new CCriticalSection();
|
||||
private HttpCwsServer _server;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Key
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Gets or sets the Key
|
||||
/// </summary>
|
||||
public string Key { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Name
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Gets or sets the Name
|
||||
/// </summary>
|
||||
public string Name { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BasePath
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Gets or sets the BasePath
|
||||
/// </summary>
|
||||
public string BasePath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the IsRegistered
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Gets or sets the IsRegistered
|
||||
/// </summary>
|
||||
public bool IsRegistered { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Http request handler
|
||||
/// </summary>
|
||||
//public IHttpCwsHandler HttpRequestHandler
|
||||
//{
|
||||
// get { return _server.HttpRequestHandler; }
|
||||
|
|
@ -58,9 +56,6 @@ namespace PepperDash.Core.Web
|
|||
// }
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// Received request event handler
|
||||
/// </summary>
|
||||
//public event EventHandler<HttpCwsRequestEventArgs> ReceivedRequestEvent
|
||||
//{
|
||||
// add { _server.ReceivedRequestEvent += new HttpCwsRequestEventHandler(value); }
|
||||
|
|
@ -91,7 +86,7 @@ namespace PepperDash.Core.Web
|
|||
/// <param name="key"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="basePath"></param>
|
||||
public WebApiServer(string key, string name, string basePath)
|
||||
public WebApiServer(string key, string name, string basePath)
|
||||
{
|
||||
Key = key;
|
||||
Name = string.IsNullOrEmpty(name) ? DefaultName : name;
|
||||
|
|
@ -99,6 +94,8 @@ namespace PepperDash.Core.Web
|
|||
|
||||
if (_server == null) _server = new HttpCwsServer(BasePath);
|
||||
|
||||
_server.AuthenticateAllRoutes = false;
|
||||
|
||||
_server.setProcessName(Key);
|
||||
_server.HttpRequestHandler = new DefaultRequestHandler();
|
||||
|
||||
|
|
@ -114,7 +111,7 @@ namespace PepperDash.Core.Web
|
|||
{
|
||||
if (programEventType != eProgramStatusEventType.Stopping) return;
|
||||
|
||||
Debug.Console(DebugInfo, this, "Program stopping. stopping server");
|
||||
this.LogInformation("Program stopping. stopping server");
|
||||
|
||||
Stop();
|
||||
}
|
||||
|
|
@ -128,18 +125,18 @@ namespace PepperDash.Core.Web
|
|||
// Re-enable the server if the link comes back up and the status should be connected
|
||||
if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp && IsRegistered)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Ethernet link up. Server is alreedy registered.");
|
||||
this.LogInformation("Ethernet link up. Server is alreedy registered.");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Console(DebugInfo, this, "Ethernet link up. Starting server");
|
||||
this.LogInformation("Ethernet link up. Starting server");
|
||||
|
||||
Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Initialize method
|
||||
/// </summary>
|
||||
public void Initialize(string key, string basePath)
|
||||
{
|
||||
Key = key;
|
||||
|
|
@ -153,7 +150,7 @@ namespace PepperDash.Core.Web
|
|||
{
|
||||
if (route == null)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Failed to add route, route parameter is null");
|
||||
this.LogWarning("Failed to add route, route parameter is null");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -165,23 +162,20 @@ namespace PepperDash.Core.Web
|
|||
/// Removes a route from CWS
|
||||
/// </summary>
|
||||
/// <param name="route"></param>
|
||||
/// <summary>
|
||||
/// RemoveRoute method
|
||||
/// </summary>
|
||||
public void RemoveRoute(HttpCwsRoute route)
|
||||
{
|
||||
if (route == null)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Failed to remote route, orute parameter is null");
|
||||
this.LogWarning("Failed to remove route, route parameter is null");
|
||||
return;
|
||||
}
|
||||
|
||||
_server.Routes.Remove(route);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GetRouteCollection method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// GetRouteCollection method
|
||||
/// </summary>
|
||||
public HttpCwsRouteCollection GetRouteCollection()
|
||||
{
|
||||
return _server.Routes;
|
||||
|
|
@ -198,26 +192,24 @@ namespace PepperDash.Core.Web
|
|||
|
||||
if (_server == null)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Server is null, unable to start");
|
||||
this.LogWarning("Server is null, unable to start");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsRegistered)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Server has already been started");
|
||||
this.LogWarning("Server has already been started");
|
||||
return;
|
||||
}
|
||||
|
||||
IsRegistered = _server.Register();
|
||||
|
||||
Debug.Console(DebugInfo, this, "Starting server, registration {0}", IsRegistered ? "was successful" : "failed");
|
||||
this.LogInformation("Starting server, registration {registrationResult}", IsRegistered ? "was successful" : "failed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Start Exception Message: {0}", ex.Message);
|
||||
Debug.Console(DebugVerbose, this, "Start Exception StackTrace: {0}", ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
Debug.Console(DebugVerbose, this, "Start Exception InnerException: {0}", ex.InnerException);
|
||||
this.LogError("Start Exception Message: {message}", ex.Message);
|
||||
this.LogDebug(ex, "Start Exception StackTrace");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -225,9 +217,9 @@ namespace PepperDash.Core.Web
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Stop method
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
try
|
||||
|
|
@ -236,23 +228,21 @@ namespace PepperDash.Core.Web
|
|||
|
||||
if (_server == null)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Server is null or has already been stopped");
|
||||
this.LogWarning("Server is null or has already been stopped");
|
||||
return;
|
||||
}
|
||||
|
||||
IsRegistered = _server.Unregister() == false;
|
||||
|
||||
Debug.Console(DebugInfo, this, "Stopping server, unregistration {0}", IsRegistered ? "failed" : "was successful");
|
||||
this.LogInformation("Stopping server, unregistration {unregistrationResult}", IsRegistered ? "failed" : "was successful");
|
||||
|
||||
_server.Dispose();
|
||||
_server = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Server Stop Exception Message: {0}", ex.Message);
|
||||
Debug.Console(DebugVerbose, this, "Server Stop Exception StackTrace: {0}", ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
Debug.Console(DebugVerbose, this, "Server Stop Exception InnerException: {0}", ex.InnerException);
|
||||
this.LogError("Server Stop Exception Message: {message}", ex.Message);
|
||||
this.LogDebug(ex, "Server Stop Exception StackTrace");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -273,14 +263,12 @@ namespace PepperDash.Core.Web
|
|||
try
|
||||
{
|
||||
var j = JsonConvert.SerializeObject(args.Context, Formatting.Indented);
|
||||
Debug.Console(DebugVerbose, this, "RecieveRequestEventHandler Context:\x0d\x0a{0}", j);
|
||||
this.LogVerbose("RecieveRequestEventHandler Context:\x0d\x0a{0}", j);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "ReceivedRequestEventHandler Exception Message: {0}", ex.Message);
|
||||
Debug.Console(DebugVerbose, this, "ReceivedRequestEventHandler Exception StackTrace: {0}", ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
Debug.Console(DebugVerbose, this, "ReceivedRequestEventHandler Exception InnerException: {0}", ex.InnerException);
|
||||
this.LogError("ReceivedRequestEventHandler Exception Message: {message}", ex.Message);
|
||||
this.LogDebug(ex, "ReceivedRequestEventHandler Exception StackTrace: {stackTrace}", ex.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -436,14 +436,14 @@ namespace PepperDash.Essentials.Core
|
|||
CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Input Ports:{2}", s, inputPorts.Count, CrestronEnvironment.NewLine);
|
||||
foreach (var routingInputPort in inputPorts)
|
||||
{
|
||||
CrestronConsole.ConsoleCommandResponse("{0}{1}", routingInputPort.Key, CrestronEnvironment.NewLine);
|
||||
CrestronConsole.ConsoleCommandResponse("key: {0} signalType: {1}{2}", routingInputPort.Key, routingInputPort.Type, CrestronEnvironment.NewLine);
|
||||
}
|
||||
}
|
||||
if (outputPorts == null) return;
|
||||
CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Output Ports:{2}", s, outputPorts.Count, CrestronEnvironment.NewLine);
|
||||
foreach (var routingOutputPort in outputPorts)
|
||||
{
|
||||
CrestronConsole.ConsoleCommandResponse("{0}{1}", routingOutputPort.Key, CrestronEnvironment.NewLine);
|
||||
CrestronConsole.ConsoleCommandResponse("key: {0} signalType: {1}{2}", routingOutputPort.Key, routingOutputPort.Type, CrestronEnvironment.NewLine);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Crestron.SimplSharpPro.Keypads;
|
||||
using PepperDash.Essentials.Core.Queues;
|
||||
using PepperDash.Essentials.Core.Routing;
|
||||
using Serilog.Events;
|
||||
using Debug = PepperDash.Core.Debug;
|
||||
|
||||
|
||||
|
|
@ -18,6 +17,20 @@ namespace PepperDash.Essentials.Core
|
|||
/// </summary>
|
||||
public static class Extensions
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// A collection of RouteDescriptors for each signal type.
|
||||
/// </summary>
|
||||
public static readonly Dictionary<eRoutingSignalType, RouteDescriptorCollection> RouteDescriptors = new Dictionary<eRoutingSignalType, RouteDescriptorCollection>()
|
||||
{
|
||||
{ eRoutingSignalType.Audio, new RouteDescriptorCollection() },
|
||||
{ eRoutingSignalType.Video, new RouteDescriptorCollection() },
|
||||
{ eRoutingSignalType.SecondaryAudio, new RouteDescriptorCollection() },
|
||||
{ eRoutingSignalType.AudioVideo, new RouteDescriptorCollection() },
|
||||
{ eRoutingSignalType.UsbInput, new RouteDescriptorCollection() },
|
||||
{ eRoutingSignalType.UsbOutput, new RouteDescriptorCollection() }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Stores pending route requests, keyed by the destination device key.
|
||||
/// Used primarily to handle routing requests while a device is cooling down.
|
||||
|
|
@ -29,6 +42,108 @@ namespace PepperDash.Essentials.Core
|
|||
/// </summary>
|
||||
private static readonly GenericQueue routeRequestQueue = new GenericQueue("routingQueue");
|
||||
|
||||
/// <summary>
|
||||
/// Indexed lookup of TieLines by destination device key for faster queries.
|
||||
/// </summary>
|
||||
private static Dictionary<string, List<TieLine>> _tieLinesByDestination;
|
||||
|
||||
/// <summary>
|
||||
/// Indexed lookup of TieLines by source device key for faster queries.
|
||||
/// </summary>
|
||||
private static Dictionary<string, List<TieLine>> _tieLinesBySource;
|
||||
|
||||
/// <summary>
|
||||
/// Cache of failed route attempts to avoid re-checking impossible paths.
|
||||
/// Format: "sourceKey|destKey|signalType"
|
||||
/// Uses ConcurrentDictionary as a thread-safe set (byte value is unused).
|
||||
/// </summary>
|
||||
private static readonly ConcurrentDictionary<string, byte> _impossibleRoutes = new ConcurrentDictionary<string, byte>();
|
||||
|
||||
/// <summary>
|
||||
/// Indexes all TieLines by source and destination device keys for faster lookups.
|
||||
/// Should be called once at system startup after all TieLines are created.
|
||||
/// </summary>
|
||||
public static void IndexTieLines()
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.LogInformation("Indexing TieLines for faster route discovery");
|
||||
|
||||
_tieLinesByDestination = TieLineCollection.Default
|
||||
.GroupBy(t => t.DestinationPort.ParentDevice.Key)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
_tieLinesBySource = TieLineCollection.Default
|
||||
.GroupBy(t => t.SourcePort.ParentDevice.Key)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
Debug.LogInformation("TieLine indexing complete. {0} destination keys, {1} source keys",
|
||||
_tieLinesByDestination.Count, _tieLinesBySource.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError("Exception indexing TieLines: {exception}", ex.Message);
|
||||
Debug.LogDebug(ex, "Stack Trace: ");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets TieLines connected to a destination device.
|
||||
/// Uses indexed lookup if available, otherwise falls back to LINQ query.
|
||||
/// </summary>
|
||||
/// <param name="destinationKey">The destination device key</param>
|
||||
/// <returns>List of TieLines connected to the destination</returns>
|
||||
private static IEnumerable<TieLine> GetTieLinesForDestination(string destinationKey)
|
||||
{
|
||||
if (_tieLinesByDestination != null && _tieLinesByDestination.TryGetValue(destinationKey, out List<TieLine> tieLines))
|
||||
{
|
||||
return tieLines;
|
||||
}
|
||||
|
||||
// Fallback to LINQ if index not available
|
||||
return TieLineCollection.Default.Where(t => t.DestinationPort.ParentDevice.Key == destinationKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets TieLines connected to a source device.
|
||||
/// Uses indexed lookup if available, otherwise falls back to LINQ query.
|
||||
/// </summary>
|
||||
/// <param name="sourceKey">The source device key</param>
|
||||
/// <returns>List of TieLines connected to the source</returns>
|
||||
private static IEnumerable<TieLine> GetTieLinesForSource(string sourceKey)
|
||||
{
|
||||
if (_tieLinesBySource != null && _tieLinesBySource.TryGetValue(sourceKey, out List<TieLine> tieLines))
|
||||
{
|
||||
return tieLines;
|
||||
}
|
||||
|
||||
// Fallback to LINQ if index not available
|
||||
return TieLineCollection.Default.Where(t => t.SourcePort.ParentDevice.Key == sourceKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a cache key for route impossibility tracking.
|
||||
/// </summary>
|
||||
/// <param name="sourceKey">Source device key</param>
|
||||
/// <param name="destKey">Destination device key</param>
|
||||
/// <param name="sourcePortKey">Source port key</param>
|
||||
/// <param name="destinationPortKey">Destination port key</param>
|
||||
/// <param name="type">Signal type</param>
|
||||
/// <returns>Cache key string</returns>
|
||||
private static string GetRouteKey(string sourceKey, string destKey, string sourcePortKey, string destinationPortKey, eRoutingSignalType type)
|
||||
{
|
||||
return $"{sourceKey}|{destKey}|{sourcePortKey}|{destinationPortKey}|{type}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the impossible routes cache. Should be called if TieLines are added/removed at runtime.
|
||||
/// </summary>
|
||||
public static void ClearImpossibleRoutesCache()
|
||||
{
|
||||
_impossibleRoutes.Clear();
|
||||
Debug.LogInformation("Impossible routes cache cleared");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets any existing RouteDescriptor for a destination, clears it using ReleaseRoute
|
||||
/// and then attempts a new Route and if sucessful, stores that RouteDescriptor
|
||||
|
|
@ -38,7 +153,7 @@ namespace PepperDash.Essentials.Core
|
|||
{
|
||||
// Remove this line before committing!!!!!
|
||||
var frame = new StackFrame(1, true);
|
||||
Debug.LogMessage(LogEventLevel.Information, "ReleaseAndMakeRoute Called from {method} with params {destinationKey}:{sourceKey}:{signalType}:{destinationPortKey}:{sourcePortKey}", frame.GetMethod().Name, destination.Key, source.Key, signalType.ToString(), destinationPortKey, sourcePortKey);
|
||||
Debug.LogInformation("ReleaseAndMakeRoute Called from {method} with params {destinationKey}:{sourceKey}:{signalType}:{destinationPortKey}:{sourcePortKey}", frame.GetMethod().Name, destination.Key, source.Key, signalType.ToString(), destinationPortKey, sourcePortKey);
|
||||
|
||||
var inputPort = string.IsNullOrEmpty(destinationPortKey) ? null : destination.InputPorts.FirstOrDefault(p => p.Key == destinationPortKey);
|
||||
var outputPort = string.IsNullOrEmpty(sourcePortKey) ? null : source.OutputPorts.FirstOrDefault(p => p.Key == sourcePortKey);
|
||||
|
|
@ -96,13 +211,13 @@ namespace PepperDash.Essentials.Core
|
|||
/// <param name="destinationKey">destination device key</param>
|
||||
public static void RemoveRouteRequestForDestination(string destinationKey)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Removing route request for {destination}", null, destinationKey);
|
||||
Debug.LogInformation("Removing route request for {destination}", destinationKey);
|
||||
|
||||
var result = RouteRequests.Remove(destinationKey);
|
||||
|
||||
var messageTemplate = result ? "Route Request for {destination} removed" : "Route Request for {destination} not found";
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information, messageTemplate, null, destinationKey);
|
||||
Debug.LogInformation(messageTemplate, destinationKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -118,8 +233,8 @@ namespace PepperDash.Essentials.Core
|
|||
if (!signalType.HasFlag(eRoutingSignalType.AudioVideo) &&
|
||||
!(signalType.HasFlag(eRoutingSignalType.Video) && signalType.HasFlag(eRoutingSignalType.SecondaryAudio)))
|
||||
{
|
||||
var singleTypeRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, signalType);
|
||||
Debug.LogMessage(LogEventLevel.Debug, "Attempting to build source route from {sourceKey} of type {type}", destination, source.Key, signalType);
|
||||
var singleTypeRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, sourcePort, signalType);
|
||||
Debug.LogDebug(destination, "Attempting to build source route from {sourceKey} of type {type}", source.Key, signalType);
|
||||
|
||||
if (!destination.GetRouteToSource(source, null, null, signalType, 0, singleTypeRouteDescriptor, destinationPort, sourcePort))
|
||||
singleTypeRouteDescriptor = null;
|
||||
|
|
@ -127,54 +242,55 @@ namespace PepperDash.Essentials.Core
|
|||
var routes = singleTypeRouteDescriptor?.Routes ?? new List<RouteSwitchDescriptor>();
|
||||
foreach (var route in routes)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Route for device: {route}", destination, route.ToString());
|
||||
Debug.LogVerbose(destination, "Route for device: {route}", route.ToString());
|
||||
}
|
||||
|
||||
return (singleTypeRouteDescriptor, null);
|
||||
}
|
||||
// otherwise, audioVideo needs to be handled as two steps.
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Debug, "Attempting to build source route from {destinationKey} to {sourceKey} of type {type}", destination, source.Key, signalType);
|
||||
Debug.LogDebug(destination, "Attempting to build source route from {destinationKey} to {sourceKey} of type {type}", source.Key, signalType);
|
||||
|
||||
RouteDescriptor audioRouteDescriptor;
|
||||
|
||||
if (signalType.HasFlag(eRoutingSignalType.SecondaryAudio))
|
||||
{
|
||||
audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.SecondaryAudio);
|
||||
audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, sourcePort, eRoutingSignalType.SecondaryAudio);
|
||||
}
|
||||
else
|
||||
{
|
||||
audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.Audio);
|
||||
audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, sourcePort, eRoutingSignalType.Audio);
|
||||
}
|
||||
|
||||
var audioSuccess = destination.GetRouteToSource(source, null, null, signalType.HasFlag(eRoutingSignalType.SecondaryAudio) ? eRoutingSignalType.SecondaryAudio : eRoutingSignalType.Audio, 0, audioRouteDescriptor, destinationPort, sourcePort);
|
||||
|
||||
if (!audioSuccess)
|
||||
Debug.LogMessage(LogEventLevel.Debug, "Cannot find audio route to {0}", destination, source.Key);
|
||||
Debug.LogDebug(destination, "Cannot find audio route to {0}", source.Key);
|
||||
|
||||
var videoRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.Video);
|
||||
var videoRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, sourcePort, eRoutingSignalType.Video);
|
||||
|
||||
var videoSuccess = destination.GetRouteToSource(source, null, null, eRoutingSignalType.Video, 0, videoRouteDescriptor, destinationPort, sourcePort);
|
||||
|
||||
if (!videoSuccess)
|
||||
Debug.LogMessage(LogEventLevel.Debug, "Cannot find video route to {0}", destination, source.Key);
|
||||
Debug.LogDebug(destination, "Cannot find video route to {0}", source.Key);
|
||||
|
||||
foreach (var route in audioRouteDescriptor.Routes)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Audio route for device: {route}", destination, route.ToString());
|
||||
Debug.LogVerbose(destination, "Audio route for device: {route}", route.ToString());
|
||||
}
|
||||
|
||||
foreach (var route in videoRouteDescriptor.Routes)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Video route for device: {route}", destination, route.ToString());
|
||||
Debug.LogVerbose(destination, "Video route for device: {route}", route.ToString());
|
||||
}
|
||||
|
||||
|
||||
if (!audioSuccess && !videoSuccess)
|
||||
return (null, null);
|
||||
|
||||
|
||||
return (audioRouteDescriptor, videoRouteDescriptor);
|
||||
// Return null for descriptors that have no routes
|
||||
return (audioSuccess && audioRouteDescriptor.Routes.Count > 0 ? audioRouteDescriptor : null,
|
||||
videoSuccess && videoRouteDescriptor.Routes.Count > 0 ? videoRouteDescriptor : null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -190,8 +306,8 @@ namespace PepperDash.Essentials.Core
|
|||
{
|
||||
if (destination == null) throw new ArgumentNullException(nameof(destination));
|
||||
if (source == null) throw new ArgumentNullException(nameof(source));
|
||||
if (destinationPort == null) Debug.LogMessage(LogEventLevel.Information, "Destination port is null");
|
||||
if (sourcePort == null) Debug.LogMessage(LogEventLevel.Information, "Source port is null");
|
||||
if (destinationPort == null) Debug.LogDebug("Destination port is null");
|
||||
if (sourcePort == null) Debug.LogDebug("Source port is null");
|
||||
|
||||
var routeRequest = new RouteRequest
|
||||
{
|
||||
|
|
@ -213,7 +329,7 @@ namespace PepperDash.Essentials.Core
|
|||
|
||||
RouteRequests[destination.Key] = routeRequest;
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information, "Device: {destination} is cooling down and already has a routing request stored. Storing new route request to route to source key: {sourceKey}", null, destination.Key, routeRequest.Source.Key);
|
||||
Debug.LogInformation("Device: {destination} is cooling down and already has a routing request stored. Storing new route request to route to source key: {sourceKey}", destination.Key, routeRequest.Source.Key);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -225,7 +341,7 @@ namespace PepperDash.Essentials.Core
|
|||
|
||||
RouteRequests.Add(destination.Key, routeRequest);
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information, "Device: {destination} is cooling down. Storing route request to route to source key: {sourceKey}", null, destination.Key, routeRequest.Source.Key);
|
||||
Debug.LogInformation("Device: {destination} is cooling down. Storing route request to route to source key: {sourceKey}", destination.Key, routeRequest.Source.Key);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -237,7 +353,7 @@ namespace PepperDash.Essentials.Core
|
|||
|
||||
RouteRequests.Remove(destination.Key);
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information, "Device: {destination} is NOT cooling down. Removing stored route request and routing to source key: {sourceKey}", null, destination.Key, routeRequest.Source.Key);
|
||||
Debug.LogInformation("Device: {destination} is NOT cooling down. Removing stored route request and routing to source key: {sourceKey}", destination.Key, routeRequest.Source.Key);
|
||||
}
|
||||
|
||||
routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, destinationPort?.Key ?? string.Empty, false));
|
||||
|
|
@ -245,6 +361,90 @@ namespace PepperDash.Essentials.Core
|
|||
routeRequestQueue.Enqueue(new RouteRequestQueueItem(RunRouteRequest, routeRequest));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps destination input ports to source output ports for all routing devices.
|
||||
/// </summary>
|
||||
public static void MapDestinationsToSources()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Index TieLines before mapping if not already done
|
||||
if (_tieLinesByDestination == null || _tieLinesBySource == null)
|
||||
{
|
||||
IndexTieLines();
|
||||
}
|
||||
|
||||
var sinks = DeviceManager.AllDevices.OfType<IRoutingInputs>().Where(d => !(d is IRoutingInputsOutputs));
|
||||
var sources = DeviceManager.AllDevices.OfType<IRoutingOutputs>().Where(d => !(d is IRoutingInputsOutputs));
|
||||
|
||||
foreach (var sink in sinks)
|
||||
{
|
||||
foreach (var source in sources)
|
||||
{
|
||||
foreach (var inputPort in sink.InputPorts)
|
||||
{
|
||||
foreach (var outputPort in source.OutputPorts)
|
||||
{
|
||||
var (audioOrSingleRoute, videoRoute) = sink.GetRouteToSource(source, inputPort.Type, inputPort, outputPort);
|
||||
|
||||
if (audioOrSingleRoute == null && videoRoute == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (audioOrSingleRoute != null)
|
||||
{
|
||||
// Only add routes that have actual switching steps
|
||||
if (audioOrSingleRoute.Routes == null || audioOrSingleRoute.Routes.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add to the appropriate collection(s) based on signal type
|
||||
// Note: A single route descriptor with combined flags (e.g., AudioVideo) will be added once per matching signal type
|
||||
if (audioOrSingleRoute.SignalType.HasFlag(eRoutingSignalType.Audio))
|
||||
{
|
||||
RouteDescriptors[eRoutingSignalType.Audio].AddRouteDescriptor(audioOrSingleRoute);
|
||||
}
|
||||
if (audioOrSingleRoute.SignalType.HasFlag(eRoutingSignalType.Video))
|
||||
{
|
||||
RouteDescriptors[eRoutingSignalType.Video].AddRouteDescriptor(audioOrSingleRoute);
|
||||
}
|
||||
if (audioOrSingleRoute.SignalType.HasFlag(eRoutingSignalType.SecondaryAudio))
|
||||
{
|
||||
RouteDescriptors[eRoutingSignalType.SecondaryAudio].AddRouteDescriptor(audioOrSingleRoute);
|
||||
}
|
||||
if (audioOrSingleRoute.SignalType.HasFlag(eRoutingSignalType.UsbInput))
|
||||
{
|
||||
RouteDescriptors[eRoutingSignalType.UsbInput].AddRouteDescriptor(audioOrSingleRoute);
|
||||
}
|
||||
if (audioOrSingleRoute.SignalType.HasFlag(eRoutingSignalType.UsbOutput))
|
||||
{
|
||||
RouteDescriptors[eRoutingSignalType.UsbOutput].AddRouteDescriptor(audioOrSingleRoute);
|
||||
}
|
||||
}
|
||||
if (videoRoute != null)
|
||||
{
|
||||
// Only add routes that have actual switching steps
|
||||
if (videoRoute.Routes == null || videoRoute.Routes.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
RouteDescriptors[eRoutingSignalType.Video].AddRouteDescriptor(videoRoute);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError("Exception mapping routes: {exception}", ex.Message);
|
||||
Debug.LogDebug(ex, "Stack Trace: ");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the actual routing based on a <see cref="RouteRequest"/>.
|
||||
/// Finds the route path, adds it to the collection, and executes the switches.
|
||||
|
|
@ -257,7 +457,54 @@ namespace PepperDash.Essentials.Core
|
|||
if (request.Source == null)
|
||||
return;
|
||||
|
||||
var (audioOrSingleRoute, videoRoute) = request.Destination.GetRouteToSource(request.Source, request.SignalType, request.DestinationPort, request.SourcePort);
|
||||
RouteDescriptor audioOrSingleRoute = null;
|
||||
RouteDescriptor videoRoute = null;
|
||||
|
||||
// Try to use pre-loaded route descriptors first
|
||||
if (request.SignalType.HasFlag(eRoutingSignalType.AudioVideo))
|
||||
{
|
||||
// For AudioVideo routes, check both Audio and Video collections
|
||||
if (RouteDescriptors.TryGetValue(eRoutingSignalType.Audio, out RouteDescriptorCollection audioCollection))
|
||||
{
|
||||
audioOrSingleRoute = audioCollection.Descriptors.FirstOrDefault(d =>
|
||||
d.Source.Key == request.Source.Key &&
|
||||
d.Destination.Key == request.Destination.Key &&
|
||||
(request.DestinationPort == null || d.InputPort?.Key == request.DestinationPort.Key) &&
|
||||
(request.SourcePort == null || d.OutputPort?.Key == request.SourcePort.Key));
|
||||
}
|
||||
|
||||
if (RouteDescriptors.TryGetValue(eRoutingSignalType.Video, out RouteDescriptorCollection videoCollection))
|
||||
{
|
||||
videoRoute = videoCollection.Descriptors.FirstOrDefault(d =>
|
||||
d.Source.Key == request.Source.Key &&
|
||||
d.Destination.Key == request.Destination.Key &&
|
||||
(request.DestinationPort == null || d.InputPort?.Key == request.DestinationPort.Key) &&
|
||||
(request.SourcePort == null || d.OutputPort?.Key == request.SourcePort.Key));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// For single signal type routes
|
||||
var signalTypeToCheck = request.SignalType.HasFlag(eRoutingSignalType.SecondaryAudio)
|
||||
? eRoutingSignalType.SecondaryAudio
|
||||
: request.SignalType;
|
||||
|
||||
if (RouteDescriptors.TryGetValue(signalTypeToCheck, out RouteDescriptorCollection collection))
|
||||
{
|
||||
audioOrSingleRoute = collection.Descriptors.FirstOrDefault(d =>
|
||||
d.Source.Key == request.Source.Key &&
|
||||
d.Destination.Key == request.Destination.Key &&
|
||||
(request.DestinationPort == null || d.InputPort?.Key == request.DestinationPort.Key) &&
|
||||
(request.SourcePort == null || d.OutputPort?.Key == request.SourcePort.Key));
|
||||
}
|
||||
}
|
||||
|
||||
// If no pre-loaded route found, build it dynamically
|
||||
if (audioOrSingleRoute == null && videoRoute == null)
|
||||
{
|
||||
Debug.LogDebug(request.Destination, "No pre-loaded route found, building dynamically");
|
||||
(audioOrSingleRoute, videoRoute) = request.Destination.GetRouteToSource(request.Source, request.SignalType, request.DestinationPort, request.SourcePort);
|
||||
}
|
||||
|
||||
if (audioOrSingleRoute == null && videoRoute == null)
|
||||
return;
|
||||
|
|
@ -269,14 +516,15 @@ namespace PepperDash.Essentials.Core
|
|||
RouteDescriptorCollection.DefaultCollection.AddRouteDescriptor(videoRoute);
|
||||
}
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Executing full route", request.Destination);
|
||||
Debug.LogVerbose(request.Destination, "Executing full route");
|
||||
|
||||
audioOrSingleRoute.ExecuteRoutes();
|
||||
videoRoute?.ExecuteRoutes();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogMessage(ex, "Exception Running Route Request {request}", null, request);
|
||||
Debug.LogError("Exception Running Route Request {request}: {exception}", request, ex.Message);
|
||||
Debug.LogDebug(ex, "Stack Trace: ");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -290,7 +538,7 @@ namespace PepperDash.Essentials.Core
|
|||
{
|
||||
try
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Release route for '{destination}':'{inputPortKey}'", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey);
|
||||
Debug.LogInformation(destination, "Release route for '{destination}':'{inputPortKey}'", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey);
|
||||
|
||||
if (RouteRequests.TryGetValue(destination.Key, out RouteRequest existingRequest) && destination is IWarmingCooling)
|
||||
{
|
||||
|
|
@ -304,13 +552,14 @@ namespace PepperDash.Essentials.Core
|
|||
var current = RouteDescriptorCollection.DefaultCollection.RemoveRouteDescriptor(destination, inputPortKey);
|
||||
if (current != null)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Releasing current route: {0}", destination, current.Source.Key);
|
||||
Debug.LogInformation(destination, "Releasing current route: {0}", current.Source.Key);
|
||||
current.ReleaseRoutes(clearRoute);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogMessage(ex, "Exception releasing route for '{destination}':'{inputPortKey}'", null, destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey);
|
||||
Debug.LogError("Exception releasing route for '{destination}':'{inputPortKey}': {exception}", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey, ex.Message);
|
||||
Debug.LogDebug(ex, "Stack Trace: ");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -321,13 +570,13 @@ namespace PepperDash.Essentials.Core
|
|||
/// </summary>
|
||||
/// <param name="destination"></param>
|
||||
/// <param name="source"></param>
|
||||
/// <param name="destinationPort">The RoutingOutputPort whose link is being checked for a route</param>
|
||||
/// <param name="outputPortToUse">The RoutingOutputPort whose link is being checked for a route</param>
|
||||
/// <param name="alreadyCheckedDevices">Prevents Devices from being twice-checked</param>
|
||||
/// <param name="signalType">This recursive function should not be called with AudioVideo</param>
|
||||
/// <param name="cycle">Just an informational counter</param>
|
||||
/// <param name="routeTable">The RouteDescriptor being populated as the route is discovered</param>
|
||||
/// <param name="outputPortToUse">The RoutingOutputPort to use for the route</param>
|
||||
/// <param name="sourcePort">The specific source output port to use (optional)</param>
|
||||
/// <param name="destinationPort">The RoutingOutputPort whose link is being checked for a route</param>
|
||||
/// <param name="sourcePort">The source output port (optional)</param>
|
||||
/// <returns>true if source is hit</returns>
|
||||
private static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source,
|
||||
RoutingOutputPort outputPortToUse, List<IRoutingInputsOutputs> alreadyCheckedDevices,
|
||||
|
|
@ -335,42 +584,54 @@ namespace PepperDash.Essentials.Core
|
|||
{
|
||||
cycle++;
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "GetRouteToSource: {cycle} {sourceKey}:{sourcePortKey}--> {destinationKey}:{destinationPortKey} {type}", null, cycle, source.Key, sourcePort?.Key ?? "auto", destination.Key, destinationPort?.Key ?? "auto", signalType.ToString());
|
||||
// Check if this route has already been determined to be impossible
|
||||
var routeKey = GetRouteKey(source.Key, destination.Key, sourcePort?.Key ?? "auto", destinationPort?.Key ?? "auto", signalType);
|
||||
if (_impossibleRoutes.ContainsKey(routeKey))
|
||||
{
|
||||
Debug.LogVerbose("Route {0} is cached as impossible, skipping", routeKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
Debug.LogVerbose("GetRouteToSource: {cycle} {sourceKey}:{sourcePortKey}--> {destinationKey}:{destinationPortKey} {type}", null, cycle, source.Key, sourcePort?.Key ?? "auto", destination.Key, destinationPort?.Key ?? "auto", signalType.ToString());
|
||||
|
||||
RoutingInputPort goodInputPort = null;
|
||||
|
||||
// Use indexed lookup instead of LINQ query
|
||||
var allDestinationTieLines = GetTieLinesForDestination(destination.Key);
|
||||
|
||||
IEnumerable<TieLine> destinationTieLines;
|
||||
TieLine directTie = null;
|
||||
|
||||
if (destinationPort == null)
|
||||
{
|
||||
destinationTieLines = TieLineCollection.Default.Where(t =>
|
||||
t.DestinationPort.ParentDevice.Key == destination.Key && (t.Type.HasFlag(signalType) || signalType == eRoutingSignalType.AudioVideo));
|
||||
destinationTieLines = allDestinationTieLines.Where(t =>
|
||||
t.Type.HasFlag(signalType) || signalType == eRoutingSignalType.AudioVideo);
|
||||
}
|
||||
else
|
||||
{
|
||||
destinationTieLines = TieLineCollection.Default.Where(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.DestinationPort.Key == destinationPort.Key && (t.Type.HasFlag(signalType)));
|
||||
destinationTieLines = allDestinationTieLines.Where(t =>
|
||||
t.DestinationPort.Key == destinationPort.Key && t.Type.HasFlag(signalType));
|
||||
}
|
||||
|
||||
// find the TieLine without a port
|
||||
if (destinationPort == null && sourcePort == null)
|
||||
{
|
||||
directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.SourcePort.ParentDevice.Key == source.Key);
|
||||
directTie = destinationTieLines.FirstOrDefault(t => t.SourcePort.ParentDevice.Key == source.Key);
|
||||
}
|
||||
// find a tieLine to a specific destination port without a specific source port
|
||||
else if (destinationPort != null && sourcePort == null)
|
||||
{
|
||||
directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.DestinationPort.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key);
|
||||
directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key);
|
||||
}
|
||||
// find a tieline to a specific source port without a specific destination port
|
||||
else if (destinationPort == null & sourcePort != null)
|
||||
{
|
||||
directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.SourcePort.ParentDevice.Key == source.Key && t.SourcePort.Key == sourcePort.Key);
|
||||
directTie = destinationTieLines.FirstOrDefault(t => t.SourcePort.ParentDevice.Key == source.Key && t.SourcePort.Key == sourcePort.Key);
|
||||
}
|
||||
// find a tieline to a specific source port and destination port
|
||||
else if (destinationPort != null && sourcePort != null)
|
||||
{
|
||||
directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.DestinationPort.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key && t.SourcePort.Key == sourcePort.Key);
|
||||
directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key && t.SourcePort.Key == sourcePort.Key);
|
||||
}
|
||||
|
||||
if (directTie != null) // Found a tie directly to the source
|
||||
|
|
@ -379,7 +640,7 @@ namespace PepperDash.Essentials.Core
|
|||
}
|
||||
else // no direct-connect. Walk back devices.
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "is not directly connected to {sourceKey}. Walking down tie lines", destination, source.Key);
|
||||
Debug.LogVerbose(destination, "is not directly connected to {sourceKey}. Walking down tie lines", source.Key);
|
||||
|
||||
// No direct tie? Run back out on the inputs' attached devices...
|
||||
// Only the ones that are routing devices
|
||||
|
|
@ -397,13 +658,13 @@ namespace PepperDash.Essentials.Core
|
|||
// Check if this previous device has already been walked
|
||||
if (alreadyCheckedDevices.Contains(midpointDevice))
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Skipping input {midpointDeviceKey} on {destinationKey}, this was already checked", destination, midpointDevice.Key, destination.Key);
|
||||
Debug.LogVerbose(destination, "Skipping input {midpointDeviceKey} on {destinationKey}, this was already checked", midpointDevice.Key, destination.Key);
|
||||
continue;
|
||||
}
|
||||
|
||||
var midpointOutputPort = tieLine.SourcePort;
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Trying to find route on {midpointDeviceKey}", destination, midpointDevice.Key);
|
||||
Debug.LogVerbose(destination, "Trying to find route on {midpointDeviceKey}", midpointDevice.Key);
|
||||
|
||||
// haven't seen this device yet. Do it. Pass the output port to the next
|
||||
// level to enable switching on success
|
||||
|
|
@ -412,9 +673,9 @@ namespace PepperDash.Essentials.Core
|
|||
|
||||
if (upstreamRoutingSuccess)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Upstream device route found", destination);
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Route found on {midpointDeviceKey}", destination, midpointDevice.Key);
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "TieLine: SourcePort: {SourcePort} DestinationPort: {DestinationPort}", destination, tieLine.SourcePort, tieLine.DestinationPort);
|
||||
Debug.LogVerbose(destination, "Upstream device route found");
|
||||
Debug.LogVerbose(destination, "Route found on {midpointDeviceKey}", midpointDevice.Key);
|
||||
Debug.LogVerbose(destination, "TieLine: SourcePort: {SourcePort} DestinationPort: {DestinationPort}", tieLine.SourcePort, tieLine.DestinationPort);
|
||||
goodInputPort = tieLine.DestinationPort;
|
||||
break; // Stop looping the inputs in this cycle
|
||||
}
|
||||
|
|
@ -424,7 +685,11 @@ namespace PepperDash.Essentials.Core
|
|||
|
||||
if (goodInputPort == null)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "No route found to {0}", destination, source.Key);
|
||||
Debug.LogVerbose(destination, "No route found to {0}", source.Key);
|
||||
|
||||
// Cache this as an impossible route
|
||||
_impossibleRoutes.TryAdd(routeKey, 0);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -440,7 +705,7 @@ namespace PepperDash.Essentials.Core
|
|||
routeTable.Routes.Add(new RouteSwitchDescriptor(outputPortToUse, goodInputPort));
|
||||
}
|
||||
else // device is merely IRoutingInputOutputs
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "No routing. Passthrough device", destination);
|
||||
Debug.LogVerbose(destination, "No routing. Passthrough device");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,22 +20,27 @@ namespace PepperDash.Essentials.Core
|
|||
public IRoutingInputs Destination { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the InputPort
|
||||
/// The InputPort on the destination device for this route, if applicable. May be null if the route is not for a specific input port.
|
||||
/// </summary>
|
||||
public RoutingInputPort InputPort { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Source
|
||||
/// Gets the source device (sink or midpoint) for the route.
|
||||
/// </summary>
|
||||
public IRoutingOutputs Source { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SignalType
|
||||
/// Gets the OutputPort on the source device for this route, if applicable. May be null if the route is not for a specific output port.
|
||||
/// </summary>
|
||||
public RoutingOutputPort OutputPort { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the signal type for this route.
|
||||
/// </summary>
|
||||
public eRoutingSignalType SignalType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Routes
|
||||
/// Gets the collection of route switch descriptors for this route.
|
||||
/// </summary>
|
||||
public List<RouteSwitchDescriptor> Routes { get; private set; }
|
||||
|
||||
|
|
@ -56,11 +61,24 @@ namespace PepperDash.Essentials.Core
|
|||
/// <param name="destination">The destination device.</param>
|
||||
/// <param name="inputPort">The destination input port (optional).</param>
|
||||
/// <param name="signalType">The signal type for this route.</param>
|
||||
public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, RoutingInputPort inputPort, eRoutingSignalType signalType)
|
||||
public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, RoutingInputPort inputPort, eRoutingSignalType signalType) : this(source, destination, inputPort, null, signalType)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RouteDescriptor"/> class for a route with specific destination input and source output ports.
|
||||
/// </summary>
|
||||
/// <param name="source"></param>
|
||||
/// <param name="destination"></param>
|
||||
/// <param name="inputPort"></param>
|
||||
/// <param name="outputPort"></param>
|
||||
/// <param name="signalType"></param>
|
||||
public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, RoutingInputPort inputPort, RoutingOutputPort outputPort, eRoutingSignalType signalType)
|
||||
{
|
||||
Destination = destination;
|
||||
InputPort = inputPort;
|
||||
Source = source;
|
||||
OutputPort = outputPort;
|
||||
SignalType = signalType;
|
||||
Routes = new List<RouteSwitchDescriptor>();
|
||||
}
|
||||
|
|
@ -72,7 +90,7 @@ namespace PepperDash.Essentials.Core
|
|||
{
|
||||
foreach (var route in Routes)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "ExecuteRoutes: {0}", null, route.ToString());
|
||||
Debug.LogVerbose("ExecuteRoutes: {0}", route.ToString());
|
||||
|
||||
if (route.SwitchingDevice is IRoutingSinkWithSwitching sink)
|
||||
{
|
||||
|
|
@ -86,7 +104,7 @@ namespace PepperDash.Essentials.Core
|
|||
|
||||
route.OutputPort.InUseTracker.AddUser(Destination, "destination-" + SignalType);
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Output port {0} routing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue);
|
||||
Debug.LogVerbose("Output port {0} routing. Count={1}", route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -95,15 +113,15 @@ namespace PepperDash.Essentials.Core
|
|||
/// Releases the usage tracking for the route and optionally clears the route on the switching devices.
|
||||
/// </summary>
|
||||
/// <param name="clearRoute">If true, attempts to clear the route on the switching devices (e.g., set input to null/0).</param>
|
||||
|
||||
|
||||
|
||||
|
||||
public void ReleaseRoutes(bool clearRoute = false)
|
||||
{
|
||||
foreach (var route in Routes.Where(r => r.SwitchingDevice is IRouting))
|
||||
{
|
||||
if (route.SwitchingDevice is IRouting switchingDevice)
|
||||
{
|
||||
if(clearRoute)
|
||||
if (clearRoute)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -112,6 +130,7 @@ namespace PepperDash.Essentials.Core
|
|||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError("Error executing switch: {exception}", e.Message);
|
||||
Debug.LogDebug(e, "Stack Trace: ");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -123,11 +142,11 @@ namespace PepperDash.Essentials.Core
|
|||
if (route.OutputPort.InUseTracker != null)
|
||||
{
|
||||
route.OutputPort.InUseTracker.RemoveUser(Destination, "destination-" + SignalType);
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Port {0} releasing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue);
|
||||
Debug.LogVerbose("Port {0} releasing. Count={1}", route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Error, "InUseTracker is null for OutputPort {0}", null, route.OutputPort.Key);
|
||||
Debug.LogVerbose("InUseTracker is null for OutputPort {0}", route.OutputPort.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -137,98 +156,11 @@ namespace PepperDash.Essentials.Core
|
|||
/// Returns a string representation of the route descriptor, including source, destination, and individual route steps.
|
||||
/// </summary>
|
||||
/// <returns>A string describing the route.</returns>
|
||||
|
||||
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var routesText = Routes.Select(r => r.ToString()).ToArray();
|
||||
return string.Format("Route table from {0} to {1}:\r{2}", Source.Key, Destination.Key, string.Join("\r", routesText));
|
||||
return $"Route table from {Source.Key} to {Destination.Key} for {SignalType}:\r\n {string.Join("\r\n ", routesText)}";
|
||||
}
|
||||
}
|
||||
|
||||
/*/// <summary>
|
||||
/// Represents an collection of individual route steps between Source and Destination
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Represents a RouteDescriptor
|
||||
/// </summary>
|
||||
public class RouteDescriptor<TInputSelector, TOutputSelector>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Destination
|
||||
/// </summary>
|
||||
public IRoutingInputs<TInputSelector> Destination { get; private set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the Source
|
||||
/// </summary>
|
||||
public IRoutingOutputs<TOutputSelector> Source { get; private set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the SignalType
|
||||
/// </summary>
|
||||
public eRoutingSignalType SignalType { get; private set; }
|
||||
public List<RouteSwitchDescriptor<TInputSelector, TOutputSelector>> Routes { get; private set; }
|
||||
|
||||
|
||||
public RouteDescriptor(IRoutingOutputs<TOutputSelector> source, IRoutingInputs<TInputSelector> destination, eRoutingSignalType signalType)
|
||||
{
|
||||
Destination = destination;
|
||||
Source = source;
|
||||
SignalType = signalType;
|
||||
Routes = new List<RouteSwitchDescriptor<TInputSelector, TOutputSelector>>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ExecuteRoutes method
|
||||
/// </summary>
|
||||
public void ExecuteRoutes()
|
||||
{
|
||||
foreach (var route in Routes)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "ExecuteRoutes: {0}", null, route.ToString());
|
||||
|
||||
if (route.SwitchingDevice is IRoutingSinkWithSwitching<TInputSelector> sink)
|
||||
{
|
||||
sink.ExecuteSwitch(route.InputPort.Selector);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (route.SwitchingDevice is IRouting switchingDevice)
|
||||
{
|
||||
switchingDevice.ExecuteSwitch(route.InputPort.Selector, route.OutputPort.Selector, SignalType);
|
||||
|
||||
route.OutputPort.InUseTracker.AddUser(Destination, "destination-" + SignalType);
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Output port {0} routing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ReleaseRoutes method
|
||||
/// </summary>
|
||||
public void ReleaseRoutes()
|
||||
{
|
||||
foreach (var route in Routes)
|
||||
{
|
||||
if (route.SwitchingDevice is IRouting<TInputSelector, TOutputSelector>)
|
||||
{
|
||||
// Pull the route from the port. Whatever is watching the output's in use tracker is
|
||||
// responsible for responding appropriately.
|
||||
route.OutputPort.InUseTracker.RemoveUser(Destination, "destination-" + SignalType);
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Port {0} releasing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ToString method
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
var routesText = Routes.Select(r => r.ToString()).ToArray();
|
||||
return string.Format("Route table from {0} to {1}:\r{2}", Source.Key, Destination.Key, string.Join("\r", routesText));
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using PepperDash.Core;
|
||||
using Serilog.Events;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using PepperDash.Core;
|
||||
using Serilog.Events;
|
||||
|
||||
|
||||
namespace PepperDash.Essentials.Core
|
||||
|
|
@ -12,7 +12,7 @@ namespace PepperDash.Essentials.Core
|
|||
public class RouteDescriptorCollection
|
||||
{
|
||||
/// <summary>
|
||||
/// DefaultCollection static property
|
||||
/// Gets the default collection of RouteDescriptors.
|
||||
/// </summary>
|
||||
public static RouteDescriptorCollection DefaultCollection
|
||||
{
|
||||
|
|
@ -27,6 +27,11 @@ namespace PepperDash.Essentials.Core
|
|||
|
||||
private readonly List<RouteDescriptor> RouteDescriptors = new List<RouteDescriptor>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets an enumerable collection of all RouteDescriptors in this collection.
|
||||
/// </summary>
|
||||
public IEnumerable<RouteDescriptor> Descriptors => RouteDescriptors.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a RouteDescriptor to the list. If an existing RouteDescriptor for the
|
||||
/// destination exists already, it will not be added - in order to preserve
|
||||
|
|
@ -40,13 +45,34 @@ namespace PepperDash.Essentials.Core
|
|||
return;
|
||||
}
|
||||
|
||||
if (RouteDescriptors.Any(t => t.Destination == descriptor.Destination)
|
||||
&& RouteDescriptors.Any(t => t.Destination == descriptor.Destination && t.InputPort != null && descriptor.InputPort != null && t.InputPort.Key == descriptor.InputPort.Key))
|
||||
// Check if a route already exists with the same source, destination, input port, AND signal type
|
||||
var existingRoute = RouteDescriptors.FirstOrDefault(t =>
|
||||
t.Source == descriptor.Source &&
|
||||
t.Destination == descriptor.Destination &&
|
||||
t.SignalType == descriptor.SignalType &&
|
||||
((t.InputPort == null && descriptor.InputPort == null) ||
|
||||
(t.InputPort != null && descriptor.InputPort != null && t.InputPort.Key == descriptor.InputPort.Key)) &&
|
||||
((t.OutputPort == null && descriptor.OutputPort == null) ||
|
||||
(t.OutputPort != null && descriptor.OutputPort != null && t.OutputPort.Key == descriptor.OutputPort.Key)));
|
||||
|
||||
if (existingRoute != null)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Debug, descriptor.Destination,
|
||||
"Route to [{0}] already exists in global routes table", descriptor?.Source?.Key);
|
||||
Debug.LogInformation(descriptor.Destination,
|
||||
"Route from {source}:{outputPort} to {destination}:{inputPort} ({signalType}) already exists in this collection",
|
||||
descriptor?.Source?.Key,
|
||||
descriptor?.OutputPort?.Key ?? "auto",
|
||||
descriptor?.Destination?.Key,
|
||||
descriptor?.InputPort?.Key ?? "auto",
|
||||
descriptor?.SignalType
|
||||
);
|
||||
return;
|
||||
}
|
||||
Debug.LogVerbose("Adding route descriptor: {source}:{outputPort} -> {destination}:{inputPort} ({signalType})",
|
||||
descriptor?.Source?.Key,
|
||||
descriptor?.OutputPort?.Key ?? "auto",
|
||||
descriptor?.Destination?.Key,
|
||||
descriptor?.InputPort?.Key ?? "auto",
|
||||
descriptor?.SignalType);
|
||||
RouteDescriptors.Add(descriptor);
|
||||
}
|
||||
|
||||
|
|
@ -61,11 +87,11 @@ namespace PepperDash.Essentials.Core
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the RouteDescriptor for a destination and input port key. Returns null if no matching RouteDescriptor exists.
|
||||
/// Gets the route descriptor for a specific destination and input port
|
||||
/// </summary>
|
||||
/// <param name="destination"></param>
|
||||
/// <param name="inputPortKey"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="destination">The destination device</param>
|
||||
/// <param name="inputPortKey">The input port key</param>
|
||||
/// <returns>The matching RouteDescriptor or null if not found</returns>
|
||||
public RouteDescriptor GetRouteDescriptorForDestinationAndInputPort(IRoutingInputs destination, string inputPortKey)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Getting route descriptor for '{destination}':'{inputPortKey}'", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey);
|
||||
|
|
@ -82,7 +108,7 @@ namespace PepperDash.Essentials.Core
|
|||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Removing route descriptor for '{destination}':'{inputPortKey}'", destination.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey);
|
||||
|
||||
var descr = string.IsNullOrEmpty(inputPortKey)
|
||||
var descr = string.IsNullOrEmpty(inputPortKey)
|
||||
? GetRouteDescriptorForDestination(destination)
|
||||
: GetRouteDescriptorForDestinationAndInputPort(destination, inputPortKey);
|
||||
if (descr != null)
|
||||
|
|
@ -93,70 +119,4 @@ namespace PepperDash.Essentials.Core
|
|||
return descr;
|
||||
}
|
||||
}
|
||||
|
||||
/*/// <summary>
|
||||
/// A collection of RouteDescriptors - typically the static DefaultCollection is used
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Represents a RouteDescriptorCollection
|
||||
/// </summary>
|
||||
public class RouteDescriptorCollection<TInputSelector, TOutputSelector>
|
||||
{
|
||||
public static RouteDescriptorCollection<TInputSelector, TOutputSelector> DefaultCollection
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_DefaultCollection == null)
|
||||
_DefaultCollection = new RouteDescriptorCollection<TInputSelector, TOutputSelector>();
|
||||
return _DefaultCollection;
|
||||
}
|
||||
}
|
||||
private static RouteDescriptorCollection<TInputSelector, TOutputSelector> _DefaultCollection;
|
||||
|
||||
private readonly List<RouteDescriptor> RouteDescriptors = new List<RouteDescriptor>();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a RouteDescriptor to the list. If an existing RouteDescriptor for the
|
||||
/// destination exists already, it will not be added - in order to preserve
|
||||
/// proper route releasing.
|
||||
/// </summary>
|
||||
/// <param name="descriptor"></param>
|
||||
/// <summary>
|
||||
/// AddRouteDescriptor method
|
||||
/// </summary>
|
||||
public void AddRouteDescriptor(RouteDescriptor descriptor)
|
||||
{
|
||||
if (RouteDescriptors.Any(t => t.Destination == descriptor.Destination))
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Debug, descriptor.Destination,
|
||||
"Route to [{0}] already exists in global routes table", descriptor.Source.Key);
|
||||
return;
|
||||
}
|
||||
RouteDescriptors.Add(descriptor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the RouteDescriptor for a destination
|
||||
/// </summary>
|
||||
/// <returns>null if no RouteDescriptor for a destination exists</returns>
|
||||
/// <summary>
|
||||
/// GetRouteDescriptorForDestination method
|
||||
/// </summary>
|
||||
public RouteDescriptor GetRouteDescriptorForDestination(IRoutingInputs<TInputSelector> destination)
|
||||
{
|
||||
return RouteDescriptors.FirstOrDefault(rd => rd.Destination == destination);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the RouteDescriptor for a given destination AND removes it from collection.
|
||||
/// Returns null if no route with the provided destination exists.
|
||||
/// </summary>
|
||||
public RouteDescriptor RemoveRouteDescriptor(IRoutingInputs<TInputSelector> destination)
|
||||
{
|
||||
var descr = GetRouteDescriptorForDestination(destination);
|
||||
if (descr != null)
|
||||
RouteDescriptors.Remove(descr);
|
||||
return descr;
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
|
@ -4,96 +4,51 @@
|
|||
/// Represents a RouteSwitchDescriptor
|
||||
/// </summary>
|
||||
public class RouteSwitchDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the SwitchingDevice
|
||||
/// </summary>
|
||||
public IRoutingInputs SwitchingDevice { get { return InputPort?.ParentDevice; } }
|
||||
/// <summary>
|
||||
/// The output port being switched from (relevant for matrix switchers). Null for sink devices.
|
||||
/// </summary>
|
||||
public RoutingOutputPort OutputPort { get; set; }
|
||||
/// <summary>
|
||||
/// The input port being switched to.
|
||||
/// </summary>
|
||||
public RoutingInputPort InputPort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RouteSwitchDescriptor"/> class for sink devices (no output port).
|
||||
/// </summary>
|
||||
/// <param name="inputPort">The input port being switched to.</param>
|
||||
public RouteSwitchDescriptor(RoutingInputPort inputPort)
|
||||
{
|
||||
InputPort = inputPort;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RouteSwitchDescriptor"/> class for matrix switchers.
|
||||
/// </summary>
|
||||
/// <param name="outputPort">The output port being switched from.</param>
|
||||
/// <param name="inputPort">The input port being switched to.</param>
|
||||
public RouteSwitchDescriptor(RoutingOutputPort outputPort, RoutingInputPort inputPort)
|
||||
{
|
||||
InputPort = inputPort;
|
||||
OutputPort = outputPort;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the route switch descriptor.
|
||||
/// </summary>
|
||||
/// <returns>A string describing the switch operation.</returns>
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
if (SwitchingDevice is IRouting)
|
||||
return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches output {(OutputPort != null ? OutputPort.Key : "No output port")} to input {(InputPort != null ? InputPort.Key : "No input port")}";
|
||||
else
|
||||
return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches to input {(InputPort != null ? InputPort.Key : "No input port")}";
|
||||
}
|
||||
}
|
||||
|
||||
/*/// <summary>
|
||||
/// Represents an individual link for a route
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Represents a RouteSwitchDescriptor
|
||||
/// </summary>
|
||||
public class RouteSwitchDescriptor<TInputSelector, TOutputSelector>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the SwitchingDevice
|
||||
/// </summary>
|
||||
public IRoutingInputs<TInputSelector> SwitchingDevice { get { return InputPort.ParentDevice; } }
|
||||
public IRoutingInputs SwitchingDevice { get { return InputPort?.ParentDevice; } }
|
||||
/// <summary>
|
||||
/// Gets or sets the OutputPort
|
||||
/// The output port being switched from (relevant for matrix switchers). Null for sink devices.
|
||||
/// </summary>
|
||||
public RoutingOutputPort<TOutputSelector> OutputPort { get; set; }
|
||||
public RoutingOutputPort OutputPort { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the InputPort
|
||||
/// The input port being switched to.
|
||||
/// </summary>
|
||||
public RoutingInputPort<TInputSelector> InputPort { get; set; }
|
||||
public RoutingInputPort InputPort { get; set; }
|
||||
|
||||
public RouteSwitchDescriptor(RoutingInputPort<TInputSelector> inputPort)
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RouteSwitchDescriptor"/> class for sink devices (no output port).
|
||||
/// </summary>
|
||||
/// <param name="inputPort">The input port being switched to.</param>
|
||||
public RouteSwitchDescriptor(RoutingInputPort inputPort)
|
||||
{
|
||||
InputPort = inputPort;
|
||||
}
|
||||
|
||||
public RouteSwitchDescriptor(RoutingOutputPort<TOutputSelector> outputPort, RoutingInputPort<TInputSelector> inputPort)
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RouteSwitchDescriptor"/> class for matrix switchers.
|
||||
/// </summary>
|
||||
/// <param name="outputPort">The output port being switched from.</param>
|
||||
/// <param name="inputPort">The input port being switched to.</param>
|
||||
public RouteSwitchDescriptor(RoutingOutputPort outputPort, RoutingInputPort inputPort)
|
||||
{
|
||||
InputPort = inputPort;
|
||||
OutputPort = outputPort;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ToString method
|
||||
/// Returns a string representation of the route switch descriptor.
|
||||
/// </summary>
|
||||
/// <returns>A string describing the switch operation.</returns>
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
if (SwitchingDevice is IRouting)
|
||||
return string.Format("{0} switches output '{1}' to input '{2}'", SwitchingDevice.Key, OutputPort.Selector, InputPort.Selector);
|
||||
return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches output {(OutputPort != null ? OutputPort.Key : "No output port")} to input {(InputPort != null ? InputPort.Key : "No input port")}";
|
||||
else
|
||||
return string.Format("{0} switches to input '{1}'", SwitchingDevice.Key, InputPort.Selector);
|
||||
return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches to input {(InputPort != null ? InputPort.Key : "No input port")}";
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Timers;
|
||||
using PepperDash.Core;
|
||||
using PepperDash.Essentials.Core.Config;
|
||||
|
||||
|
|
@ -11,6 +13,26 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
/// </summary>
|
||||
public class RoutingFeedbackManager : EssentialsDevice
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps midpoint device keys to the set of sink device keys that are downstream
|
||||
/// </summary>
|
||||
private Dictionary<string, HashSet<string>> midpointToSinksMap;
|
||||
|
||||
/// <summary>
|
||||
/// Debounce timers for each sink device to prevent rapid successive updates
|
||||
/// </summary>
|
||||
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>
|
||||
/// Debounce delay in milliseconds
|
||||
/// </summary>
|
||||
private const long DEBOUNCE_MS = 500;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RoutingFeedbackManager"/> class.
|
||||
/// </summary>
|
||||
|
|
@ -19,10 +41,99 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
public RoutingFeedbackManager(string key, string name)
|
||||
: base(key, name)
|
||||
{
|
||||
AddPreActivationAction(BuildMidpointSinkMap);
|
||||
AddPreActivationAction(SubscribeForMidpointFeedback);
|
||||
AddPreActivationAction(SubscribeForSinkFeedback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a map of which sink devices are downstream of each midpoint device
|
||||
/// for performance optimization in HandleMidpointUpdate
|
||||
/// </summary>
|
||||
private void BuildMidpointSinkMap()
|
||||
{
|
||||
midpointToSinksMap = new Dictionary<string, HashSet<string>>();
|
||||
|
||||
var sinks = DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
|
||||
|
||||
foreach (var sink in sinks)
|
||||
{
|
||||
if (sink.CurrentInputPort == null)
|
||||
continue;
|
||||
|
||||
// Find all upstream midpoints for this sink
|
||||
var upstreamMidpoints = GetUpstreamMidpoints(sink);
|
||||
|
||||
foreach (var midpointKey in upstreamMidpoints)
|
||||
{
|
||||
if (!midpointToSinksMap.ContainsKey(midpointKey))
|
||||
midpointToSinksMap[midpointKey] = new HashSet<string>();
|
||||
|
||||
midpointToSinksMap[midpointKey].Add(sink.Key);
|
||||
}
|
||||
}
|
||||
|
||||
Debug.LogMessage(
|
||||
Serilog.Events.LogEventLevel.Information,
|
||||
"Built midpoint-to-sink map with {count} midpoints",
|
||||
this,
|
||||
midpointToSinksMap.Count
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all upstream midpoint device keys for a given sink
|
||||
/// </summary>
|
||||
private HashSet<string> GetUpstreamMidpoints(IRoutingSinkWithSwitchingWithInputPort sink)
|
||||
{
|
||||
var result = new HashSet<string>();
|
||||
var visited = new HashSet<string>();
|
||||
|
||||
if (sink.CurrentInputPort == null)
|
||||
return result;
|
||||
|
||||
var tieLine = TieLineCollection.Default.FirstOrDefault(tl =>
|
||||
tl.DestinationPort.Key == sink.CurrentInputPort.Key &&
|
||||
tl.DestinationPort.ParentDevice.Key == sink.CurrentInputPort.ParentDevice.Key);
|
||||
|
||||
if (tieLine == null)
|
||||
return result;
|
||||
|
||||
TraceUpstreamMidpoints(tieLine, result, visited);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively traces upstream to find all midpoint devices
|
||||
/// </summary>
|
||||
private void TraceUpstreamMidpoints(TieLine tieLine, HashSet<string> midpoints, HashSet<string> visited)
|
||||
{
|
||||
if (tieLine == null || visited.Contains(tieLine.SourcePort.ParentDevice.Key))
|
||||
return;
|
||||
|
||||
visited.Add(tieLine.SourcePort.ParentDevice.Key);
|
||||
|
||||
if (tieLine.SourcePort.ParentDevice is IRoutingWithFeedback midpoint)
|
||||
{
|
||||
midpoints.Add(midpoint.Key);
|
||||
|
||||
// Find upstream TieLines connected to this midpoint's inputs
|
||||
var midpointInputs = (midpoint as IRoutingInputs)?.InputPorts;
|
||||
if (midpointInputs != null)
|
||||
{
|
||||
foreach (var inputPort in midpointInputs)
|
||||
{
|
||||
var upstreamTieLine = TieLineCollection.Default.FirstOrDefault(tl =>
|
||||
tl.DestinationPort.Key == inputPort.Key &&
|
||||
tl.DestinationPort.ParentDevice.Key == inputPort.ParentDevice.Key);
|
||||
|
||||
if (upstreamTieLine != null)
|
||||
TraceUpstreamMidpoints(upstreamTieLine, midpoints, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the RouteChanged event on all devices implementing <see cref="IRoutingWithFeedback"/>.
|
||||
/// </summary>
|
||||
|
|
@ -52,7 +163,7 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
|
||||
/// <summary>
|
||||
/// Handles the RouteChanged event from a midpoint device.
|
||||
/// Triggers an update for all sink devices.
|
||||
/// Only triggers updates for sink devices that are downstream of this midpoint.
|
||||
/// </summary>
|
||||
/// <param name="midpoint">The midpoint device that reported a route change.</param>
|
||||
/// <param name="newRoute">The descriptor of the new route.</param>
|
||||
|
|
@ -63,12 +174,33 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
{
|
||||
try
|
||||
{
|
||||
var devices =
|
||||
DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
|
||||
|
||||
foreach (var device in devices)
|
||||
// Only update affected sinks (performance optimization)
|
||||
if (midpointToSinksMap != null && midpointToSinksMap.TryGetValue(midpoint.Key, out var affectedSinkKeys))
|
||||
{
|
||||
UpdateDestination(device, device.CurrentInputPort);
|
||||
Debug.LogMessage(
|
||||
Serilog.Events.LogEventLevel.Debug,
|
||||
"Midpoint {midpoint} changed, updating {count} downstream sinks",
|
||||
this,
|
||||
midpoint.Key,
|
||||
affectedSinkKeys.Count
|
||||
);
|
||||
|
||||
foreach (var sinkKey in affectedSinkKeys)
|
||||
{
|
||||
if (DeviceManager.GetDeviceForKey(sinkKey) is IRoutingSinkWithSwitchingWithInputPort sink)
|
||||
{
|
||||
UpdateDestination(sink, sink.CurrentInputPort);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogMessage(
|
||||
Serilog.Events.LogEventLevel.Debug,
|
||||
"Midpoint {midpoint} changed but has no downstream sinks in map",
|
||||
this,
|
||||
midpoint.Key
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -83,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>
|
||||
/// 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>
|
||||
/// <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>
|
||||
|
|
@ -96,6 +265,10 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
{
|
||||
try
|
||||
{
|
||||
// Keep the map current so HandleMidpointUpdate can find this sink
|
||||
if (sender is IRoutingSinkWithSwitchingWithInputPort sinkWithInputPort)
|
||||
RebuildMapForSink(sinkWithInputPort);
|
||||
|
||||
UpdateDestination(sender, currentInputPort);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -113,6 +286,7 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
/// <summary>
|
||||
/// Updates the CurrentSourceInfo and CurrentSourceInfoKey properties on a destination (sink) device
|
||||
/// based on its currently selected input port by tracing the route back through tie lines.
|
||||
/// Uses debouncing to prevent rapid successive updates.
|
||||
/// </summary>
|
||||
/// <param name="destination">The destination sink device to update.</param>
|
||||
/// <param name="inputPort">The currently selected input port on the destination device.</param>
|
||||
|
|
@ -120,6 +294,76 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
IRoutingSinkWithSwitching destination,
|
||||
RoutingInputPort inputPort
|
||||
)
|
||||
{
|
||||
if (destination == null)
|
||||
return;
|
||||
|
||||
var key = destination.Key;
|
||||
|
||||
// Cancel and replace any existing timer under the lock so no callback
|
||||
// can race with us while we swap the entry.
|
||||
Timer timerToDispose = null;
|
||||
Timer newTimer = null;
|
||||
|
||||
newTimer = new Timer(DEBOUNCE_MS) { AutoReset = false };
|
||||
newTimer.Elapsed += (s, e) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
UpdateDestinationImmediate(destination, inputPort);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogMessage(
|
||||
ex,
|
||||
"Error in debounced update for destination {destinationKey}: {message}",
|
||||
this,
|
||||
destination.Key,
|
||||
ex.Message
|
||||
);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Remove the entry first so a concurrent UpdateDestination call
|
||||
// cannot re-dispose whatever timer we're about to dispose.
|
||||
Timer selfTimer = null;
|
||||
lock (_timerLock)
|
||||
{
|
||||
if (updateTimers.TryGetValue(key, out var current) && ReferenceEquals(current, newTimer))
|
||||
{
|
||||
selfTimer = current;
|
||||
updateTimers.Remove(key);
|
||||
}
|
||||
}
|
||||
selfTimer?.Dispose();
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
/// Immediately updates the CurrentSourceInfo for a destination device.
|
||||
/// Called after debounce delay.
|
||||
/// </summary>
|
||||
private void UpdateDestinationImmediate(
|
||||
IRoutingSinkWithSwitching destination,
|
||||
RoutingInputPort inputPort
|
||||
)
|
||||
{
|
||||
Debug.LogMessage(
|
||||
Serilog.Events.LogEventLevel.Debug,
|
||||
|
|
@ -206,7 +450,8 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -230,6 +475,11 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
return roomDefaultDisplay.DefaultDisplay.Key == destination.Key;
|
||||
}
|
||||
|
||||
if (ConfigReader.ConfigObject.GetDestinationListForKey(r.DestinationListKey)?.FirstOrDefault(d => d.Value.SinkKey == destination.Key) != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
|
@ -251,10 +501,8 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
|
||||
if (sourceList == null)
|
||||
{
|
||||
Debug.LogMessage(
|
||||
Serilog.Events.LogEventLevel.Debug,
|
||||
Debug.LogDebug(this,
|
||||
"No source list found for source list key {key}. Unable to find source for tieLine {sourceTieLine}",
|
||||
this,
|
||||
room.SourceListKey,
|
||||
sourceTieLine
|
||||
);
|
||||
|
|
@ -283,10 +531,8 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
|
||||
if (source == null)
|
||||
{
|
||||
Debug.LogMessage(
|
||||
Serilog.Events.LogEventLevel.Debug,
|
||||
Debug.LogDebug(this,
|
||||
"No source found for device {key}. Creating transient source for {destination}",
|
||||
this,
|
||||
sourceTieLine.SourcePort.ParentDevice.Key,
|
||||
destination
|
||||
);
|
||||
|
|
@ -309,105 +555,92 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively traces a route back from a given tie line to find the root source tie line.
|
||||
/// It navigates through midpoint devices (<see cref="IRoutingWithFeedback"/>) by checking their current routes.
|
||||
/// Traces a route back from a given tie line to find the root source tie line.
|
||||
/// Leverages the existing Extensions.GetRouteToSource method with loop protection.
|
||||
/// </summary>
|
||||
/// <param name="tieLine">The starting tie line (typically connected to a sink or midpoint).</param>
|
||||
/// <returns>The <see cref="TieLine"/> connected to the original source device, or null if the source cannot be determined.</returns>
|
||||
private TieLine GetRootTieLine(TieLine tieLine)
|
||||
{
|
||||
TieLine nextTieLine = null;
|
||||
try
|
||||
{
|
||||
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "**Following tieLine {tieLine}**", this, tieLine);
|
||||
|
||||
if (tieLine.SourcePort.ParentDevice is IRoutingWithFeedback midpoint)
|
||||
if (!(tieLine.DestinationPort.ParentDevice is IRoutingInputs sink))
|
||||
{
|
||||
// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLine Source device {sourceDevice} is midpoint", this, midpoint);
|
||||
Debug.LogDebug(this,
|
||||
"TieLine destination {device} is not IRoutingInputs",
|
||||
tieLine.DestinationPort.ParentDevice.Key
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (midpoint.CurrentRoutes == null || midpoint.CurrentRoutes.Count == 0)
|
||||
// Get all potential sources (devices that only have outputs, not inputs+outputs)
|
||||
var sources = DeviceManager.AllDevices
|
||||
.OfType<IRoutingOutputs>()
|
||||
.Where(s => !(s is IRoutingInputsOutputs));
|
||||
|
||||
// Try each signal type that this TieLine supports
|
||||
var signalTypes = new[]
|
||||
{
|
||||
eRoutingSignalType.Audio,
|
||||
eRoutingSignalType.Video,
|
||||
eRoutingSignalType.AudioVideo,
|
||||
eRoutingSignalType.SecondaryAudio,
|
||||
eRoutingSignalType.UsbInput,
|
||||
eRoutingSignalType.UsbOutput
|
||||
};
|
||||
|
||||
foreach (var signalType in signalTypes)
|
||||
{
|
||||
if (!tieLine.Type.HasFlag(signalType))
|
||||
continue;
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
Debug.LogMessage(
|
||||
Serilog.Events.LogEventLevel.Debug,
|
||||
"Midpoint {midpointKey} has no routes",
|
||||
this,
|
||||
midpoint.Key
|
||||
// Use the optimized route discovery with loop protection
|
||||
var (route, _) = sink.GetRouteToSource(
|
||||
source,
|
||||
signalType,
|
||||
tieLine.DestinationPort,
|
||||
null
|
||||
);
|
||||
return null;
|
||||
|
||||
if (route != null && route.Routes != null && route.Routes.Count > 0)
|
||||
{
|
||||
// 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 =>
|
||||
tl.DestinationPort.Key == firstHop.InputPort.Key &&
|
||||
tl.DestinationPort.ParentDevice.Key == firstHop.InputPort.ParentDevice.Key);
|
||||
|
||||
if (sourceTieLine != null)
|
||||
{
|
||||
Debug.LogDebug(this,
|
||||
"Found route from {source} to {sink} with {count} hops",
|
||||
source.Key,
|
||||
sink.Key,
|
||||
route.Routes.Count
|
||||
);
|
||||
return sourceTieLine;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
if (currentRoute == null)
|
||||
{
|
||||
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 =>
|
||||
{
|
||||
//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;
|
||||
});
|
||||
|
||||
if (nextTieLine != null)
|
||||
{
|
||||
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found next tieLine {tieLine}. Walking the chain", this, nextTieLine);
|
||||
return GetRootTieLine(nextTieLine);
|
||||
}
|
||||
|
||||
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root tieLine {tieLine}", this,nextTieLine);
|
||||
return nextTieLine;
|
||||
}
|
||||
|
||||
//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
|
||||
{
|
||||
// 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
|
||||
);
|
||||
|
||||
if (nextTieLine != null)
|
||||
{
|
||||
return GetRootTieLine(nextTieLine);
|
||||
}
|
||||
Debug.LogDebug(this, "No route found to any source from {sink}", sink.Key);
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogMessage(ex, "Error walking tieLines: {Exception}", this, ex);
|
||||
Debug.LogError(this, "Error getting root tieLine: {message}", ex.Message);
|
||||
Debug.LogDebug(ex, "StackTrace: ");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,73 +10,78 @@ using Serilog.Events;
|
|||
|
||||
namespace PepperDash.Essentials.Core.Web
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a EssentialsWebApi
|
||||
/// </summary>
|
||||
public class EssentialsWebApi : EssentialsDevice
|
||||
{
|
||||
private readonly WebApiServer _server;
|
||||
/// <summary>
|
||||
/// Represents a EssentialsWebApi
|
||||
/// </summary>
|
||||
public class EssentialsWebApi : EssentialsDevice
|
||||
{
|
||||
private readonly WebApiServer _server;
|
||||
|
||||
///<example>
|
||||
/// http(s)://{ipaddress}/cws/{basePath}
|
||||
/// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
|
||||
/// </example>
|
||||
private readonly string _defaultBasePath = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance
|
||||
? string.Format("/app{0:00}/api", InitialParametersClass.ApplicationNumber)
|
||||
: "/api";
|
||||
///<example>
|
||||
/// http(s)://{ipaddress}/cws/{basePath}
|
||||
/// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
|
||||
/// </example>
|
||||
private readonly string _defaultBasePath = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance
|
||||
? string.Format("/app{0:00}/api", InitialParametersClass.ApplicationNumber)
|
||||
: "/api";
|
||||
|
||||
private const int DebugTrace = 0;
|
||||
private const int DebugInfo = 1;
|
||||
private const int DebugVerbose = 2;
|
||||
private const int DebugTrace = 0;
|
||||
private const int DebugInfo = 1;
|
||||
private const int DebugVerbose = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BasePath
|
||||
/// </summary>
|
||||
public string BasePath { get; private set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the BasePath
|
||||
/// </summary>
|
||||
public string BasePath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tracks if CWS is registered
|
||||
/// </summary>
|
||||
public bool IsRegistered
|
||||
{
|
||||
get { return _server.IsRegistered; }
|
||||
}
|
||||
/// <summary>
|
||||
/// Tracks if CWS is registered
|
||||
/// </summary>
|
||||
public bool IsRegistered
|
||||
{
|
||||
get { return _server.IsRegistered; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="name"></param>
|
||||
public EssentialsWebApi(string key, string name)
|
||||
: this(key, name, null)
|
||||
{
|
||||
}
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="name"></param>
|
||||
public EssentialsWebApi(string key, string name)
|
||||
: this(key, name, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="config"></param>
|
||||
public EssentialsWebApi(string key, string name, EssentialsWebApiPropertiesConfig config)
|
||||
: base(key, name)
|
||||
{
|
||||
Key = key;
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="config"></param>
|
||||
public EssentialsWebApi(string key, string name, EssentialsWebApiPropertiesConfig config)
|
||||
: base(key, name)
|
||||
{
|
||||
Key = key;
|
||||
|
||||
if (config == null)
|
||||
BasePath = _defaultBasePath;
|
||||
else
|
||||
BasePath = string.IsNullOrEmpty(config.BasePath) ? _defaultBasePath : config.BasePath;
|
||||
if (config == null)
|
||||
BasePath = _defaultBasePath;
|
||||
else
|
||||
BasePath = string.IsNullOrEmpty(config.BasePath) ? _defaultBasePath : config.BasePath;
|
||||
|
||||
_server = new WebApiServer(Key, Name, BasePath);
|
||||
_server = new WebApiServer(Key, Name, BasePath);
|
||||
|
||||
SetupRoutes();
|
||||
}
|
||||
SetupRoutes();
|
||||
}
|
||||
|
||||
private void SetupRoutes()
|
||||
{
|
||||
private void SetupRoutes()
|
||||
{
|
||||
var routes = new List<HttpCwsRoute>
|
||||
{
|
||||
new HttpCwsRoute("login")
|
||||
{
|
||||
Name = "Root",
|
||||
RouteHandler = new LoginRequestHandler()
|
||||
},
|
||||
new HttpCwsRoute("versions")
|
||||
{
|
||||
Name = "ReportVersions",
|
||||
|
|
@ -177,6 +182,11 @@ namespace PepperDash.Essentials.Core.Web
|
|||
Name = "Get Routing Ports for a device",
|
||||
RouteHandler = new GetRoutingPortsHandler()
|
||||
},
|
||||
new HttpCwsRoute("routingDevicesAndTieLines")
|
||||
{
|
||||
Name = "Get Routing Devices and TieLines",
|
||||
RouteHandler = new GetRoutingDevicesAndTieLinesHandler()
|
||||
},
|
||||
};
|
||||
|
||||
AddRoute(routes);
|
||||
|
|
@ -211,78 +221,79 @@ namespace PepperDash.Essentials.Core.Web
|
|||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public override void Initialize()
|
||||
{
|
||||
AddRoute(new HttpCwsRoute("apiPaths") {
|
||||
{
|
||||
AddRoute(new HttpCwsRoute("apiPaths")
|
||||
{
|
||||
Name = "GetPaths",
|
||||
RouteHandler = new GetRoutesHandler(_server.GetRouteCollection(), BasePath)
|
||||
});
|
||||
|
||||
// If running on an appliance
|
||||
if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance)
|
||||
{
|
||||
/*
|
||||
{
|
||||
/*
|
||||
WEBSERVER [ON | OFF | TIMEOUT <VALUE IN SECONDS> | MAXSESSIONSPERUSER <Number of sessions>]
|
||||
*/
|
||||
var response = string.Empty;
|
||||
CrestronConsole.SendControlSystemCommand("webserver", ref response);
|
||||
if (response.Contains("OFF")) return;
|
||||
var response = string.Empty;
|
||||
CrestronConsole.SendControlSystemCommand("webserver", ref response);
|
||||
if (response.Contains("OFF")) return;
|
||||
|
||||
var is4Series = eCrestronSeries.Series4 == (Global.ProcessorSeries & eCrestronSeries.Series4);
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on {0} Appliance", is4Series ? "4-series" : "3-series");
|
||||
var is4Series = eCrestronSeries.Series4 == (Global.ProcessorSeries & eCrestronSeries.Series4);
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on {0} Appliance", is4Series ? "4-series" : "3-series");
|
||||
|
||||
_server.Start();
|
||||
_server.Start();
|
||||
|
||||
GetPaths();
|
||||
GetPaths();
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Automatically start CWS when running on a server (Linux OS, Virtual Control)
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on Virtual Control Server");
|
||||
// Automatically start CWS when running on a server (Linux OS, Virtual Control)
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on Virtual Control Server");
|
||||
|
||||
_server.Start();
|
||||
_server.Start();
|
||||
|
||||
GetPaths();
|
||||
}
|
||||
GetPaths();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Print the available pahts
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// http(s)://{ipaddress}/cws/{basePath}
|
||||
/// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
|
||||
/// </example>
|
||||
/// <summary>
|
||||
/// GetPaths method
|
||||
/// </summary>
|
||||
public void GetPaths()
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
|
||||
/// <summary>
|
||||
/// Print the available paths
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// http(s)://{ipaddress}/cws/{basePath}
|
||||
/// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
|
||||
/// </example>
|
||||
/// <summary>
|
||||
/// GetPaths method
|
||||
/// </summary>
|
||||
public void GetPaths()
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
|
||||
|
||||
var currentIp = CrestronEthernetHelper.GetEthernetParameter(
|
||||
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
|
||||
|
||||
var hostname = CrestronEthernetHelper.GetEthernetParameter(
|
||||
var currentIp = CrestronEthernetHelper.GetEthernetParameter(
|
||||
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
|
||||
|
||||
var hostname = CrestronEthernetHelper.GetEthernetParameter(
|
||||
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0);
|
||||
|
||||
var path = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server
|
||||
? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws{BasePath}"
|
||||
: $"https://{currentIp}/cws{BasePath}";
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information, this, "Server:{path:l}", path);
|
||||
|
||||
var routeCollection = _server.GetRouteCollection();
|
||||
if (routeCollection == null)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, this, "Server route collection is null");
|
||||
return;
|
||||
}
|
||||
Debug.LogMessage(LogEventLevel.Information, this, "Configured Routes:");
|
||||
foreach (var route in routeCollection)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, this, "{routeName:l}: {routePath:l}/{routeUrl:l}", route.Name, path, route.Url);
|
||||
}
|
||||
Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
|
||||
}
|
||||
}
|
||||
Debug.LogMessage(LogEventLevel.Information, this, "Server:{path:l}", path);
|
||||
|
||||
var routeCollection = _server.GetRouteCollection();
|
||||
if (routeCollection == null)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, this, "Server route collection is null");
|
||||
return;
|
||||
}
|
||||
Debug.LogMessage(LogEventLevel.Information, this, "Configured Routes:");
|
||||
foreach (var route in routeCollection)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, this, "{routeName:l}: {routePath:l}/{routeUrl:l}", route.Name, path, route.Url);
|
||||
}
|
||||
Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -36,17 +36,25 @@ namespace PepperDash.Essentials.Core.Web
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// MapToDeviceListObject method
|
||||
/// </summary>
|
||||
public static object MapToDeviceListObject(IKeyed device)
|
||||
{
|
||||
var interfaces = device.GetType()
|
||||
.GetInterfaces()
|
||||
.Select(i => i.Name)
|
||||
.ToList();
|
||||
|
||||
return new
|
||||
{
|
||||
device.Key,
|
||||
Name = (device is IKeyName)
|
||||
? (device as IKeyName).Name
|
||||
: "---"
|
||||
: "---",
|
||||
Interfaces = interfaces
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -110,5 +118,10 @@ namespace PepperDash.Essentials.Core.Web
|
|||
CType = device.Value.Type == null ? "---": device.Value.Type.ToString()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsAuthenticated(HttpCwsContext context)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Crestron.SimplSharp.WebScripting;
|
||||
using Newtonsoft.Json;
|
||||
using PepperDash.Core;
|
||||
using PepperDash.Core.Web.RequestHandlers;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles HTTP requests to retrieve routing devices and tielines information
|
||||
/// </summary>
|
||||
public class GetRoutingDevicesAndTieLinesHandler : WebApiBaseRequestHandler
|
||||
{
|
||||
public GetRoutingDevicesAndTieLinesHandler() : base(true) { }
|
||||
|
||||
protected override void HandleGet(HttpCwsContext context)
|
||||
{
|
||||
var devices = new List<RoutingDeviceInfo>();
|
||||
|
||||
// Get all devices from DeviceManager
|
||||
foreach (var device in DeviceManager.AllDevices)
|
||||
{
|
||||
var deviceInfo = new RoutingDeviceInfo
|
||||
{
|
||||
Key = device.Key,
|
||||
Name = (device as IKeyName)?.Name ?? device.Key
|
||||
};
|
||||
|
||||
// Check if device implements IRoutingInputs
|
||||
if (device is IRoutingInputs inputDevice)
|
||||
{
|
||||
deviceInfo.HasInputs = true;
|
||||
deviceInfo.InputPorts = inputDevice.InputPorts.Select(p => new PortInfo
|
||||
{
|
||||
Key = p.Key,
|
||||
SignalType = p.Type.ToString(),
|
||||
ConnectionType = p.ConnectionType.ToString(),
|
||||
IsInternal = p.IsInternal
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
// Check if device implements IRoutingOutputs
|
||||
if (device is IRoutingOutputs outputDevice)
|
||||
{
|
||||
deviceInfo.HasOutputs = true;
|
||||
deviceInfo.OutputPorts = outputDevice.OutputPorts.Select(p => new PortInfo
|
||||
{
|
||||
Key = p.Key,
|
||||
SignalType = p.Type.ToString(),
|
||||
ConnectionType = p.ConnectionType.ToString(),
|
||||
IsInternal = p.IsInternal
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
// Check if device implements IRoutingInputsOutputs
|
||||
if (device is IRoutingInputsOutputs)
|
||||
{
|
||||
deviceInfo.HasInputsAndOutputs = true;
|
||||
}
|
||||
|
||||
// Only include devices that have routing capabilities
|
||||
if (deviceInfo.HasInputs || deviceInfo.HasOutputs)
|
||||
{
|
||||
devices.Add(deviceInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Get all tielines
|
||||
var tielines = TieLineCollection.Default.Select(tl => new TieLineInfo
|
||||
{
|
||||
SourceDeviceKey = tl.SourcePort.ParentDevice.Key,
|
||||
SourcePortKey = tl.SourcePort.Key,
|
||||
DestinationDeviceKey = tl.DestinationPort.ParentDevice.Key,
|
||||
DestinationPortKey = tl.DestinationPort.Key,
|
||||
SignalType = tl.Type.ToString(),
|
||||
IsInternal = tl.IsInternal
|
||||
}).ToList();
|
||||
|
||||
var response = new RoutingSystemInfo
|
||||
{
|
||||
Devices = devices,
|
||||
TieLines = tielines
|
||||
};
|
||||
|
||||
var jsonResponse = JsonConvert.SerializeObject(response, Formatting.Indented);
|
||||
|
||||
context.Response.StatusCode = 200;
|
||||
context.Response.StatusDescription = "OK";
|
||||
context.Response.ContentType = "application/json";
|
||||
context.Response.ContentEncoding = Encoding.UTF8;
|
||||
context.Response.Write(jsonResponse, false);
|
||||
context.Response.End();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the complete routing system information including devices and tielines
|
||||
/// </summary>
|
||||
public class RoutingSystemInfo
|
||||
{
|
||||
[JsonProperty("devices")]
|
||||
public List<RoutingDeviceInfo> Devices { get; set; }
|
||||
|
||||
[JsonProperty("tieLines")]
|
||||
public List<TieLineInfo> TieLines { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a routing device with its ports information
|
||||
/// </summary>
|
||||
public class RoutingDeviceInfo
|
||||
{
|
||||
[JsonProperty("key")]
|
||||
public string Key { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("hasInputs")]
|
||||
public bool HasInputs { get; set; }
|
||||
|
||||
[JsonProperty("hasOutputs")]
|
||||
public bool HasOutputs { get; set; }
|
||||
|
||||
[JsonProperty("hasInputsAndOutputs")]
|
||||
public bool HasInputsAndOutputs { get; set; }
|
||||
|
||||
[JsonProperty("inputPorts", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public List<PortInfo> InputPorts { get; set; }
|
||||
|
||||
[JsonProperty("outputPorts", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public List<PortInfo> OutputPorts { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a routing port with its properties
|
||||
/// </summary>
|
||||
public class PortInfo
|
||||
{
|
||||
[JsonProperty("key")]
|
||||
public string Key { get; set; }
|
||||
|
||||
[JsonProperty("signalType")]
|
||||
public string SignalType { get; set; }
|
||||
|
||||
[JsonProperty("connectionType")]
|
||||
public string ConnectionType { get; set; }
|
||||
|
||||
[JsonProperty("isInternal")]
|
||||
public bool IsInternal { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a tieline connection between two ports
|
||||
/// </summary>
|
||||
public class TieLineInfo
|
||||
{
|
||||
[JsonProperty("sourceDeviceKey")]
|
||||
public string SourceDeviceKey { get; set; }
|
||||
|
||||
[JsonProperty("sourcePortKey")]
|
||||
public string SourcePortKey { get; set; }
|
||||
|
||||
[JsonProperty("destinationDeviceKey")]
|
||||
public string DestinationDeviceKey { get; set; }
|
||||
|
||||
[JsonProperty("destinationPortKey")]
|
||||
public string DestinationPortKey { get; set; }
|
||||
|
||||
[JsonProperty("signalType")]
|
||||
public string SignalType { get; set; }
|
||||
|
||||
[JsonProperty("isInternal")]
|
||||
public bool IsInternal { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
|
||||
using System;
|
||||
using Crestron.SimplSharp.CrestronAuthentication;
|
||||
using Crestron.SimplSharp.WebScripting;
|
||||
using Newtonsoft.Json;
|
||||
using PepperDash.Core.Web.RequestHandlers;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a LoginRequestHandler
|
||||
/// </summary>
|
||||
public class LoginRequestHandler : WebApiBaseRequestHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// base(true) enables CORS support by default
|
||||
/// </remarks>
|
||||
public LoginRequestHandler()
|
||||
: base(true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles POST method requests for user login and token generation
|
||||
/// </summary>
|
||||
/// <param name="context">The HTTP context for the request.</param>
|
||||
protected override void HandlePost(HttpCwsContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (context.Request.ContentLength < 0)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
context.Response.StatusDescription = "Bad Request";
|
||||
context.Response.End();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var data = context.Request.GetRequestBody();
|
||||
if (string.IsNullOrEmpty(data))
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
context.Response.StatusDescription = "Bad Request";
|
||||
context.Response.End();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var loginRequest = JsonConvert.DeserializeObject<LoginRequest>(data);
|
||||
|
||||
if (loginRequest == null || string.IsNullOrEmpty(loginRequest.Username) || string.IsNullOrEmpty(loginRequest.Password))
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
context.Response.StatusDescription = "Bad Request";
|
||||
context.Response.End();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Authentication.UserToken token;
|
||||
|
||||
try
|
||||
{
|
||||
token = Authentication.GetAuthenticationToken(loginRequest.Username, loginRequest.Password);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
context.Response.StatusCode = 401;
|
||||
context.Response.StatusDescription = "Bad Request";
|
||||
context.Response.End();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!token.Valid)
|
||||
{
|
||||
context.Response.StatusCode = 401;
|
||||
context.Response.StatusDescription = "Unauthorized";
|
||||
context.Response.End();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
context.Response.StatusCode = 200;
|
||||
context.Response.StatusDescription = "OK";
|
||||
context.Response.ContentType = "application/json";
|
||||
context.Response.ContentEncoding = System.Text.Encoding.UTF8;
|
||||
context.Response.Write(JsonConvert.SerializeObject(new { Token = token }, Formatting.Indented), false);
|
||||
context.Response.End();
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
context.Response.StatusCode = 500;
|
||||
context.Response.StatusDescription = "Internal Server Error";
|
||||
context.Response.ContentType = "application/json";
|
||||
context.Response.ContentEncoding = System.Text.Encoding.UTF8;
|
||||
context.Response.Write(JsonConvert.SerializeObject(new { Error = ex.Message }, Formatting.Indented), false);
|
||||
context.Response.End();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a LoginRequest
|
||||
/// </summary>
|
||||
public class LoginRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the username.
|
||||
/// </summary>
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password.
|
||||
/// </summary>
|
||||
public string Password { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
using Crestron.SimplSharp.WebScripting;
|
||||
using Newtonsoft.Json;
|
||||
using PepperDash.Core.Web.RequestHandlers;
|
||||
using PepperDash.Essentials.WebSocketServer;
|
||||
|
||||
|
||||
namespace PepperDash.Essentials.WebApiHandlers
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a DeleteAllUiClientsHandler
|
||||
/// </summary>
|
||||
public class DeleteAllUiClientsHandler : WebApiBaseRequestHandler
|
||||
{
|
||||
private readonly MobileControlWebsocketServer server;
|
||||
|
||||
/// <summary>
|
||||
/// Essentials CWS API handler for the MC Direct Server
|
||||
/// </summary>
|
||||
/// <param name="directServer">Direct Server instance</param>
|
||||
public DeleteAllUiClientsHandler(MobileControlWebsocketServer directServer) : base(true)
|
||||
{
|
||||
server = directServer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all clients from the Direct Server
|
||||
/// </summary>
|
||||
/// <param name="context">HTTP Context for this request</param>
|
||||
protected override void HandleDelete(HttpCwsContext context)
|
||||
{
|
||||
server.RemoveAllTokens("confirm");
|
||||
|
||||
var res = context.Response;
|
||||
res.StatusCode = 200;
|
||||
res.ContentType = "application/json";
|
||||
res.Headers.Add("Content-Type", "application/json");
|
||||
res.Write(JsonConvert.SerializeObject(new { success = true }), false);
|
||||
res.End();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -238,11 +238,17 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||
|
||||
var routes = new List<HttpCwsRoute>
|
||||
{
|
||||
new HttpCwsRoute($"devices/{Key}/client")
|
||||
new HttpCwsRoute($"device/{Key}/client")
|
||||
{
|
||||
Name = "ClientHandler",
|
||||
RouteHandler = new UiClientHandler(this)
|
||||
},
|
||||
|
||||
new HttpCwsRoute($"device/{Key}/deleteAllUiClients")
|
||||
{
|
||||
Name = "DeleteAllClientsHandler",
|
||||
RouteHandler = new DeleteAllUiClientsHandler(this)
|
||||
},
|
||||
};
|
||||
|
||||
apiServer.AddRoute(routes);
|
||||
|
|
@ -908,7 +914,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||
/// <summary>
|
||||
/// Removes all clients from the server
|
||||
/// </summary>
|
||||
private void RemoveAllTokens(string s)
|
||||
public void RemoveAllTokens(string s)
|
||||
{
|
||||
if (s == "?" || string.IsNullOrEmpty(s))
|
||||
{
|
||||
|
|
|
|||
257
src/PepperDash.Essentials/AssetLoader.cs
Normal file
257
src/PepperDash.Essentials/AssetLoader.cs
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using PepperDash.Core;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace PepperDash.Essentials
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles extracting embedded asset bundles and moving configuration files from the
|
||||
/// application directory to the program file-path prefix at startup.
|
||||
/// Implemented using <c>System.IO</c> types so it can run (and be tested) outside
|
||||
/// of a Crestron runtime environment.
|
||||
/// </summary>
|
||||
internal static class AssetLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// Scans <paramref name="applicationDirectoryPath"/> for well-known zip bundles and
|
||||
/// JSON configuration files and deploys them to <paramref name="filePathPrefix"/>.
|
||||
/// </summary>
|
||||
/// <param name="applicationDirectoryPath">
|
||||
/// The directory to scan (typically the Crestron application root).
|
||||
/// </param>
|
||||
/// <param name="filePathPrefix">
|
||||
/// The program's runtime working directory (e.g. <c>/nvram/program1/</c>).
|
||||
/// </param>
|
||||
internal static void Load(string applicationDirectoryPath, string filePathPrefix)
|
||||
{
|
||||
var applicationDirectory = new DirectoryInfo(applicationDirectoryPath);
|
||||
Debug.LogMessage(LogEventLevel.Information,
|
||||
"Searching: {applicationDirectory:l} for embedded assets - {Destination}",
|
||||
applicationDirectory.FullName, filePathPrefix);
|
||||
|
||||
ExtractAssetsZip(applicationDirectory, filePathPrefix);
|
||||
ExtractHtmlAssetsZip(applicationDirectory, filePathPrefix);
|
||||
ExtractDevToolsZip(applicationDirectory, filePathPrefix);
|
||||
MoveConfigurationFile(applicationDirectory, filePathPrefix);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static void ExtractAssetsZip(DirectoryInfo applicationDirectory, string filePathPrefix)
|
||||
{
|
||||
var zipFiles = applicationDirectory.GetFiles("assets*.zip");
|
||||
|
||||
if (zipFiles.Length > 1)
|
||||
throw new Exception("Multiple assets zip files found. Cannot continue.");
|
||||
|
||||
if (zipFiles.Length == 1)
|
||||
{
|
||||
var zipFile = zipFiles[0];
|
||||
var assetsRoot = Path.GetFullPath(filePathPrefix);
|
||||
if (!assetsRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) &&
|
||||
!assetsRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
|
||||
{
|
||||
assetsRoot += Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information,
|
||||
"Found assets zip file: {zipFile:l}... Unzipping...", zipFile.FullName);
|
||||
|
||||
using (var archive = ZipFile.OpenRead(zipFile.FullName))
|
||||
{
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
var destinationPath = Path.Combine(filePathPrefix, entry.FullName);
|
||||
var fullDest = Path.GetFullPath(destinationPath);
|
||||
if (!fullDest.StartsWith(assetsRoot, StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidOperationException(
|
||||
$"Entry '{entry.FullName}' is trying to extract outside of the target directory.");
|
||||
|
||||
if (string.IsNullOrEmpty(entry.Name))
|
||||
{
|
||||
Directory.CreateDirectory(destinationPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Directory.Exists(destinationPath))
|
||||
Directory.Delete(destinationPath, recursive: true);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath));
|
||||
entry.ExtractToFile(destinationPath, overwrite: true);
|
||||
Debug.LogMessage(LogEventLevel.Information,
|
||||
"Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var file in zipFiles)
|
||||
File.Delete(file.FullName);
|
||||
}
|
||||
|
||||
private static void ExtractHtmlAssetsZip(DirectoryInfo applicationDirectory, string filePathPrefix)
|
||||
{
|
||||
var htmlZipFiles = applicationDirectory.GetFiles("htmlassets*.zip");
|
||||
|
||||
if (htmlZipFiles.Length > 1)
|
||||
throw new Exception(
|
||||
"Multiple htmlassets zip files found in application directory. " +
|
||||
"Please ensure only one htmlassets*.zip file is present and retry.");
|
||||
|
||||
if (htmlZipFiles.Length == 1)
|
||||
{
|
||||
var htmlZipFile = htmlZipFiles[0];
|
||||
var programDir = new DirectoryInfo(
|
||||
filePathPrefix.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
||||
var userOrNvramDir = programDir.Parent;
|
||||
var rootDir = userOrNvramDir?.Parent;
|
||||
if (rootDir == null)
|
||||
throw new Exception(
|
||||
$"Unable to determine root directory for html extraction. Current path: {filePathPrefix}");
|
||||
|
||||
var htmlDir = Path.Combine(rootDir.FullName, "html");
|
||||
var htmlRoot = Path.GetFullPath(htmlDir);
|
||||
if (!htmlRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) &&
|
||||
!htmlRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
|
||||
{
|
||||
htmlRoot += Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information,
|
||||
"Found htmlassets zip file: {zipFile:l}... Unzipping...", htmlZipFile.FullName);
|
||||
|
||||
using (var archive = ZipFile.OpenRead(htmlZipFile.FullName))
|
||||
{
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
var destinationPath = Path.Combine(htmlDir, entry.FullName);
|
||||
var fullDest = Path.GetFullPath(destinationPath);
|
||||
if (!fullDest.StartsWith(htmlRoot, StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidOperationException(
|
||||
$"Entry '{entry.FullName}' is trying to extract outside of the target directory.");
|
||||
|
||||
if (string.IsNullOrEmpty(entry.Name))
|
||||
{
|
||||
Directory.CreateDirectory(destinationPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (File.Exists(destinationPath))
|
||||
File.Delete(destinationPath);
|
||||
|
||||
var parentDir = Path.GetDirectoryName(destinationPath);
|
||||
if (!string.IsNullOrEmpty(parentDir))
|
||||
Directory.CreateDirectory(parentDir);
|
||||
|
||||
entry.ExtractToFile(destinationPath, overwrite: true);
|
||||
Debug.LogMessage(LogEventLevel.Information,
|
||||
"Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var file in htmlZipFiles)
|
||||
File.Delete(file.FullName);
|
||||
}
|
||||
|
||||
private static void ExtractDevToolsZip(DirectoryInfo applicationDirectory, string filePathPrefix)
|
||||
{
|
||||
var devToolsZipFiles = applicationDirectory.GetFiles("essentials-devtools*.zip");
|
||||
|
||||
if (devToolsZipFiles.Length > 1)
|
||||
throw new Exception(
|
||||
"Multiple essentials-devtools zip files found in application directory. " +
|
||||
"Please ensure only one essentials-devtools*.zip file is present and retry.");
|
||||
|
||||
if (devToolsZipFiles.Length == 1)
|
||||
{
|
||||
var devToolsZipFile = devToolsZipFiles[0];
|
||||
var programDir = new DirectoryInfo(
|
||||
filePathPrefix.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
||||
var userOrNvramDir = programDir.Parent;
|
||||
var rootDir = userOrNvramDir?.Parent;
|
||||
if (rootDir == null)
|
||||
throw new Exception(
|
||||
$"Unable to determine root directory for debug html extraction. Current path: {filePathPrefix}");
|
||||
|
||||
var debugDir = Path.Combine(rootDir.FullName, "html", "debug");
|
||||
var debugRoot = Path.GetFullPath(debugDir);
|
||||
if (!debugRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) &&
|
||||
!debugRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
|
||||
{
|
||||
debugRoot += Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information,
|
||||
"Found essentials-devtools zip file: {zipFile:l}... Unzipping to {Destination}...",
|
||||
devToolsZipFile.FullName, debugDir);
|
||||
|
||||
using (var archive = ZipFile.OpenRead(devToolsZipFile.FullName))
|
||||
{
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
var destinationPath = Path.Combine(debugDir, entry.FullName);
|
||||
var fullDest = Path.GetFullPath(destinationPath);
|
||||
if (!fullDest.StartsWith(debugRoot, StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidOperationException(
|
||||
$"Entry '{entry.FullName}' is trying to extract outside of the target directory.");
|
||||
|
||||
if (string.IsNullOrEmpty(entry.Name))
|
||||
{
|
||||
Directory.CreateDirectory(destinationPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (File.Exists(destinationPath))
|
||||
File.Delete(destinationPath);
|
||||
|
||||
var parentDir = Path.GetDirectoryName(destinationPath);
|
||||
if (!string.IsNullOrEmpty(parentDir))
|
||||
Directory.CreateDirectory(parentDir);
|
||||
|
||||
entry.ExtractToFile(destinationPath, overwrite: true);
|
||||
Debug.LogMessage(LogEventLevel.Information,
|
||||
"Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var file in devToolsZipFiles)
|
||||
File.Delete(file.FullName);
|
||||
}
|
||||
|
||||
private static void MoveConfigurationFile(DirectoryInfo applicationDirectory, string filePathPrefix)
|
||||
{
|
||||
var jsonFiles = applicationDirectory.GetFiles("*configurationFile*.json");
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
if (jsonFiles.Length == 1)
|
||||
{
|
||||
var jsonFile = jsonFiles[0];
|
||||
var finalPath = Path.Combine(filePathPrefix, jsonFile.Name);
|
||||
Debug.LogMessage(LogEventLevel.Information,
|
||||
"Found configuration file: {jsonFile:l}... Moving to: {Destination}",
|
||||
jsonFile.FullName, finalPath);
|
||||
|
||||
if (File.Exists(finalPath))
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information,
|
||||
"Removing existing configuration file: {Destination}", finalPath);
|
||||
File.Delete(finalPath);
|
||||
}
|
||||
|
||||
jsonFile.MoveTo(finalPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -92,12 +92,16 @@ namespace PepperDash.Essentials
|
|||
|
||||
CrestronConsole.AddNewConsoleCommand(s => Debug.LogMessage(LogEventLevel.Information, "CONSOLE MESSAGE: {0}", s), "appdebugmessage", "Writes message to log", ConsoleAccessLevelEnum.AccessOperator);
|
||||
|
||||
CrestronConsole.AddNewConsoleCommand(s =>
|
||||
{
|
||||
foreach (var tl in TieLineCollection.Default)
|
||||
CrestronConsole.ConsoleCommandResponse(" {0}{1}", tl, CrestronEnvironment.NewLine);
|
||||
},
|
||||
"listtielines", "Prints out all tie lines", ConsoleAccessLevelEnum.AccessOperator);
|
||||
CrestronConsole.AddNewConsoleCommand(ListTieLines,
|
||||
"listtielines", "Prints out all tie lines. Usage: listtielines [signaltype]", ConsoleAccessLevelEnum.AccessOperator);
|
||||
|
||||
CrestronConsole.AddNewConsoleCommand(VisualizeRoutes, "visualizeroutes",
|
||||
"Visualizes routes by signal type",
|
||||
ConsoleAccessLevelEnum.AccessOperator);
|
||||
|
||||
CrestronConsole.AddNewConsoleCommand(VisualizeCurrentRoutes, "visualizecurrentroutes",
|
||||
"Visualizes current active routes from DefaultCollection",
|
||||
ConsoleAccessLevelEnum.AccessOperator);
|
||||
|
||||
CrestronConsole.AddNewConsoleCommand(s =>
|
||||
{
|
||||
|
|
@ -240,7 +244,7 @@ namespace PepperDash.Essentials
|
|||
// _ = new ProcessorExtensionDeviceFactory();
|
||||
// _ = new MobileControlFactory();
|
||||
|
||||
LoadAssets();
|
||||
LoadAssets(Global.ApplicationDirectoryPathPrefix, Global.FilePathPrefix);
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information, "Starting Essentials load from configuration");
|
||||
|
||||
|
|
@ -443,6 +447,282 @@ namespace PepperDash.Essentials
|
|||
|
||||
Debug.LogMessage(LogEventLevel.Information, "All Tie Lines Loaded.");
|
||||
|
||||
Extensions.MapDestinationsToSources();
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information, "All Routes Mapped.");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Visualizes routes in a tree format for better understanding of signal paths
|
||||
/// </summary>
|
||||
private void ListTieLines(string args)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(args) && args.Contains("?"))
|
||||
{
|
||||
CrestronConsole.ConsoleCommandResponse("Usage: listtielines [signaltype]\r\n");
|
||||
CrestronConsole.ConsoleCommandResponse("Signal types: Audio, Video, SecondaryAudio, AudioVideo, UsbInput, UsbOutput\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
eRoutingSignalType? signalTypeFilter = null;
|
||||
if (!string.IsNullOrEmpty(args))
|
||||
{
|
||||
eRoutingSignalType parsedType;
|
||||
if (Enum.TryParse(args.Trim(), true, out parsedType))
|
||||
{
|
||||
signalTypeFilter = parsedType;
|
||||
}
|
||||
else
|
||||
{
|
||||
CrestronConsole.ConsoleCommandResponse("Invalid signal type: {0}\r\n", args.Trim());
|
||||
CrestronConsole.ConsoleCommandResponse("Valid types: Audio, Video, SecondaryAudio, AudioVideo, UsbInput, UsbOutput\r\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var tielines = signalTypeFilter.HasValue
|
||||
? TieLineCollection.Default.Where(tl => tl.Type.HasFlag(signalTypeFilter.Value))
|
||||
: TieLineCollection.Default;
|
||||
|
||||
var count = 0;
|
||||
foreach (var tl in tielines)
|
||||
{
|
||||
CrestronConsole.ConsoleCommandResponse(" {0}{1}", tl, CrestronEnvironment.NewLine);
|
||||
count++;
|
||||
}
|
||||
|
||||
CrestronConsole.ConsoleCommandResponse("\r\nTotal: {0} tieline{1}{2}", count, count == 1 ? "" : "s", CrestronEnvironment.NewLine);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CrestronConsole.ConsoleCommandResponse("Error listing tielines: {0}\r\n", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void VisualizeRoutes(string args)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(args) && args.Contains("?"))
|
||||
{
|
||||
CrestronConsole.ConsoleCommandResponse("Usage: visualizeroutes [signaltype] [-s source] [-d destination]\r\n");
|
||||
CrestronConsole.ConsoleCommandResponse(" signaltype: Audio, Video, AudioVideo, etc.\r\n");
|
||||
CrestronConsole.ConsoleCommandResponse(" -s: Filter by source key (partial match)\r\n");
|
||||
CrestronConsole.ConsoleCommandResponse(" -d: Filter by destination key (partial match)\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
ParseRouteFilters(args, out eRoutingSignalType? signalTypeFilter, out string sourceFilter, out string destFilter);
|
||||
|
||||
CrestronConsole.ConsoleCommandResponse("\r\n+===========================================================================+\r\n");
|
||||
CrestronConsole.ConsoleCommandResponse("| ROUTE VISUALIZATION |\r\n");
|
||||
CrestronConsole.ConsoleCommandResponse("+===========================================================================+\r\n\r\n");
|
||||
|
||||
foreach (var descriptorCollection in Extensions.RouteDescriptors.Where(kv => kv.Value.Descriptors.Count() > 0))
|
||||
{
|
||||
// Filter by signal type if specified
|
||||
if (signalTypeFilter.HasValue && descriptorCollection.Key != signalTypeFilter.Value)
|
||||
continue;
|
||||
|
||||
CrestronConsole.ConsoleCommandResponse("\r\n+--- Signal Type: {0} ({1} routes) ---\r\n",
|
||||
descriptorCollection.Key,
|
||||
descriptorCollection.Value.Descriptors.Count());
|
||||
|
||||
foreach (var descriptor in descriptorCollection.Value.Descriptors)
|
||||
{
|
||||
// Filter by source/dest if specified
|
||||
if (sourceFilter != null && !descriptor.Source.Key.ToLower().Contains(sourceFilter))
|
||||
continue;
|
||||
if (destFilter != null && !descriptor.Destination.Key.ToLower().Contains(destFilter))
|
||||
continue;
|
||||
|
||||
VisualizeRouteDescriptor(descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
CrestronConsole.ConsoleCommandResponse("\r\n");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CrestronConsole.ConsoleCommandResponse("Error visualizing routes: {0}\r\n", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void VisualizeCurrentRoutes(string args)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(args) && args.Contains("?"))
|
||||
{
|
||||
CrestronConsole.ConsoleCommandResponse("Usage: visualizecurrentroutes [signaltype] [-s source] [-d destination]\r\n");
|
||||
CrestronConsole.ConsoleCommandResponse(" signaltype: Audio, Video, AudioVideo, etc.\r\n");
|
||||
CrestronConsole.ConsoleCommandResponse(" -s: Filter by source key (partial match)\r\n");
|
||||
CrestronConsole.ConsoleCommandResponse(" -d: Filter by destination key (partial match)\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
ParseRouteFilters(args, out eRoutingSignalType? signalTypeFilter, out string sourceFilter, out string destFilter);
|
||||
|
||||
CrestronConsole.ConsoleCommandResponse("\r\n+===========================================================================+\r\n");
|
||||
CrestronConsole.ConsoleCommandResponse("| CURRENT ROUTES VISUALIZATION |\r\n");
|
||||
CrestronConsole.ConsoleCommandResponse("+===========================================================================+\r\n\r\n");
|
||||
|
||||
var hasRoutes = false;
|
||||
|
||||
// Get all descriptors from DefaultCollection
|
||||
var allDescriptors = RouteDescriptorCollection.DefaultCollection.Descriptors;
|
||||
|
||||
// Group by signal type
|
||||
var groupedByType = allDescriptors.GroupBy(d => d.SignalType);
|
||||
|
||||
foreach (var group in groupedByType)
|
||||
{
|
||||
var signalType = group.Key;
|
||||
|
||||
// Filter by signal type if specified
|
||||
if (signalTypeFilter.HasValue && signalType != signalTypeFilter.Value)
|
||||
continue;
|
||||
|
||||
var filteredDescriptors = group.Where(d =>
|
||||
{
|
||||
if (sourceFilter != null && !d.Source.Key.ToLower().Contains(sourceFilter))
|
||||
return false;
|
||||
if (destFilter != null && !d.Destination.Key.ToLower().Contains(destFilter))
|
||||
return false;
|
||||
return true;
|
||||
}).ToList();
|
||||
|
||||
if (filteredDescriptors.Count == 0)
|
||||
continue;
|
||||
|
||||
hasRoutes = true;
|
||||
CrestronConsole.ConsoleCommandResponse("\r\n+--- Signal Type: {0} ({1} routes) ---\r\n",
|
||||
signalType,
|
||||
filteredDescriptors.Count);
|
||||
|
||||
foreach (var descriptor in filteredDescriptors)
|
||||
{
|
||||
VisualizeRouteDescriptor(descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasRoutes)
|
||||
{
|
||||
CrestronConsole.ConsoleCommandResponse("\r\nNo active routes found in current state.\r\n");
|
||||
}
|
||||
|
||||
CrestronConsole.ConsoleCommandResponse("\r\n");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CrestronConsole.ConsoleCommandResponse("Error visualizing current state: {0}\r\n", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses route filter arguments from command line
|
||||
/// </summary>
|
||||
/// <param name="args">Command line arguments</param>
|
||||
/// <param name="signalTypeFilter">Parsed signal type filter (if any)</param>
|
||||
/// <param name="sourceFilter">Parsed source filter (if any)</param>
|
||||
/// <param name="destFilter">Parsed destination filter (if any)</param>
|
||||
private void ParseRouteFilters(string args, out eRoutingSignalType? signalTypeFilter, out string sourceFilter, out string destFilter)
|
||||
{
|
||||
signalTypeFilter = null;
|
||||
sourceFilter = null;
|
||||
destFilter = null;
|
||||
|
||||
if (string.IsNullOrEmpty(args))
|
||||
return;
|
||||
|
||||
var parts = args.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
for (int i = 0; i < parts.Length; i++)
|
||||
{
|
||||
var part = parts[i];
|
||||
|
||||
// Check for flags
|
||||
if (part == "-s" && i + 1 < parts.Length)
|
||||
{
|
||||
sourceFilter = parts[++i].ToLower();
|
||||
}
|
||||
else if (part == "-d" && i + 1 < parts.Length)
|
||||
{
|
||||
destFilter = parts[++i].ToLower();
|
||||
}
|
||||
// Try to parse as signal type if not a flag and no signal type set yet
|
||||
else if (!part.StartsWith("-") && !signalTypeFilter.HasValue)
|
||||
{
|
||||
if (Enum.TryParse(part, true, out eRoutingSignalType parsedType))
|
||||
{
|
||||
signalTypeFilter = parsedType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visualizes a single route descriptor in a tree format
|
||||
/// </summary>
|
||||
private void VisualizeRouteDescriptor(RouteDescriptor descriptor)
|
||||
{
|
||||
CrestronConsole.ConsoleCommandResponse("|\r\n");
|
||||
CrestronConsole.ConsoleCommandResponse("|-- {0} --> {1}\r\n",
|
||||
descriptor.Source.Key,
|
||||
descriptor.Destination.Key);
|
||||
|
||||
if (descriptor.Routes == null || descriptor.Routes.Count == 0)
|
||||
{
|
||||
CrestronConsole.ConsoleCommandResponse("| +-- (No switching steps)\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < descriptor.Routes.Count; i++)
|
||||
{
|
||||
var route = descriptor.Routes[i];
|
||||
var isLast = i == descriptor.Routes.Count - 1;
|
||||
var prefix = isLast ? "+" : "|";
|
||||
var continuation = isLast ? " " : "|";
|
||||
|
||||
if (route.SwitchingDevice != null)
|
||||
{
|
||||
CrestronConsole.ConsoleCommandResponse("| {0}-- [{1}] {2}\r\n",
|
||||
prefix,
|
||||
route.SwitchingDevice.Key,
|
||||
GetSwitchDescription(route));
|
||||
|
||||
// Add visual connection line for non-last items
|
||||
if (!isLast)
|
||||
CrestronConsole.ConsoleCommandResponse("| {0} |\r\n", continuation);
|
||||
}
|
||||
else
|
||||
{
|
||||
CrestronConsole.ConsoleCommandResponse("| {0}-- {1}\r\n", prefix, route.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a readable description of the switching operation
|
||||
/// </summary>
|
||||
private string GetSwitchDescription(RouteSwitchDescriptor route)
|
||||
{
|
||||
if (route.OutputPort != null && route.InputPort != null)
|
||||
{
|
||||
return string.Format("{0} -> {1}", route.OutputPort.Key, route.InputPort.Key);
|
||||
}
|
||||
else if (route.InputPort != null)
|
||||
{
|
||||
return string.Format("-> {0}", route.InputPort.Key);
|
||||
}
|
||||
else
|
||||
{
|
||||
return "(passthrough)";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -544,142 +824,8 @@ namespace PepperDash.Essentials
|
|||
}
|
||||
}
|
||||
|
||||
private static void LoadAssets()
|
||||
{
|
||||
var applicationDirectory = new DirectoryInfo(Global.ApplicationDirectoryPathPrefix);
|
||||
Debug.LogMessage(LogEventLevel.Information, "Searching: {applicationDirectory:l} for embedded assets - {Destination}", applicationDirectory.FullName, Global.FilePathPrefix);
|
||||
internal static void LoadAssets(string applicationDirectoryPath, string filePathPrefix) =>
|
||||
AssetLoader.Load(applicationDirectoryPath, filePathPrefix);
|
||||
|
||||
var zipFiles = applicationDirectory.GetFiles("assets*.zip");
|
||||
|
||||
if (zipFiles.Length > 1)
|
||||
{
|
||||
throw new Exception("Multiple assets zip files found. Cannot continue.");
|
||||
}
|
||||
|
||||
if (zipFiles.Length == 1)
|
||||
{
|
||||
var zipFile = zipFiles[0];
|
||||
var assetsRoot = System.IO.Path.GetFullPath(Global.FilePathPrefix);
|
||||
if (!assetsRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && !assetsRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
|
||||
{
|
||||
assetsRoot += Path.DirectorySeparatorChar;
|
||||
}
|
||||
Debug.LogMessage(LogEventLevel.Information, "Found assets zip file: {zipFile:l}... Unzipping...", zipFile.FullName);
|
||||
using (var archive = ZipFile.OpenRead(zipFile.FullName))
|
||||
{
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
var destinationPath = Path.Combine(Global.FilePathPrefix, entry.FullName);
|
||||
var fullDest = System.IO.Path.GetFullPath(destinationPath);
|
||||
if (!fullDest.StartsWith(assetsRoot, StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidOperationException($"Entry '{entry.FullName}' is trying to extract outside of the target directory.");
|
||||
|
||||
if (string.IsNullOrEmpty(entry.Name))
|
||||
{
|
||||
Directory.CreateDirectory(destinationPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If a directory exists where a file should go, delete it
|
||||
if (Directory.Exists(destinationPath))
|
||||
Directory.Delete(destinationPath, true);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath));
|
||||
entry.ExtractToFile(destinationPath, true);
|
||||
Debug.LogMessage(LogEventLevel.Information, "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleaning up zip files
|
||||
foreach (var file in zipFiles)
|
||||
{
|
||||
File.Delete(file.FullName);
|
||||
}
|
||||
|
||||
var htmlZipFiles = applicationDirectory.GetFiles("htmlassets*.zip");
|
||||
|
||||
if (htmlZipFiles.Length > 1)
|
||||
{
|
||||
throw new Exception("Multiple htmlassets zip files found in application directory. Please ensure only one htmlassets*.zip file is present and retry.");
|
||||
}
|
||||
|
||||
|
||||
if (htmlZipFiles.Length == 1)
|
||||
{
|
||||
var htmlZipFile = htmlZipFiles[0];
|
||||
var programDir = new DirectoryInfo(Global.FilePathPrefix.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
||||
var userOrNvramDir = programDir.Parent;
|
||||
var rootDir = userOrNvramDir?.Parent;
|
||||
if (rootDir == null)
|
||||
{
|
||||
throw new Exception($"Unable to determine root directory for html extraction. Current path: {Global.FilePathPrefix}");
|
||||
}
|
||||
var htmlDir = Path.Combine(rootDir.FullName, "html");
|
||||
var htmlRoot = System.IO.Path.GetFullPath(htmlDir);
|
||||
if (!htmlRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) &&
|
||||
!htmlRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
|
||||
{
|
||||
htmlRoot += Path.DirectorySeparatorChar;
|
||||
}
|
||||
Debug.LogMessage(LogEventLevel.Information, "Found htmlassets zip file: {zipFile:l}... Unzipping...", htmlZipFile.FullName);
|
||||
using (var archive = ZipFile.OpenRead(htmlZipFile.FullName))
|
||||
{
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
var destinationPath = Path.Combine(htmlDir, entry.FullName);
|
||||
var fullDest = System.IO.Path.GetFullPath(destinationPath);
|
||||
if (!fullDest.StartsWith(htmlRoot, StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidOperationException($"Entry '{entry.FullName}' is trying to extract outside of the target directory.");
|
||||
|
||||
if (string.IsNullOrEmpty(entry.Name))
|
||||
{
|
||||
Directory.CreateDirectory(destinationPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only delete the file if it exists and is a file, not a directory
|
||||
if (File.Exists(destinationPath))
|
||||
File.Delete(destinationPath);
|
||||
|
||||
var parentDir = Path.GetDirectoryName(destinationPath);
|
||||
if (!string.IsNullOrEmpty(parentDir))
|
||||
Directory.CreateDirectory(parentDir);
|
||||
|
||||
entry.ExtractToFile(destinationPath, true);
|
||||
Debug.LogMessage(LogEventLevel.Information, "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleaning up html zip files
|
||||
foreach (var file in htmlZipFiles)
|
||||
{
|
||||
File.Delete(file.FullName);
|
||||
}
|
||||
|
||||
var jsonFiles = applicationDirectory.GetFiles("*configurationFile*.json");
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
if (jsonFiles.Length == 1)
|
||||
{
|
||||
var jsonFile = jsonFiles[0];
|
||||
var finalPath = Path.Combine(Global.FilePathPrefix, jsonFile.Name);
|
||||
Debug.LogMessage(LogEventLevel.Information, "Found configuration file: {jsonFile:l}... Moving to: {Destination}", jsonFile.FullName, finalPath);
|
||||
|
||||
if (File.Exists(finalPath))
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Removing existing configuration file: {Destination}", finalPath);
|
||||
File.Delete(finalPath);
|
||||
}
|
||||
|
||||
jsonFile.MoveTo(finalPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue