mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-08-31 19:08:29 +00:00
Compare commits
No commits in common. "main" and "v2.29.0" have entirely different histories.
62 changed files with 1108 additions and 4130 deletions
|
|
@ -19,5 +19,4 @@ jobs:
|
||||||
version: ${{ needs.getVersion.outputs.version }}
|
version: ${{ needs.getVersion.outputs.version }}
|
||||||
tag: ${{ needs.getVersion.outputs.tag }}
|
tag: ${{ needs.getVersion.outputs.tag }}
|
||||||
channel: ${{ needs.getVersion.outputs.channel }}
|
channel: ${{ needs.getVersion.outputs.channel }}
|
||||||
bypassPackageCheck: true
|
bypassPackageCheck: true
|
||||||
devToolsVersion: ${{ vars.ESSENTIALSDEVTOOLSVERSION }}
|
|
||||||
|
|
@ -183,12 +183,11 @@ namespace PepperDash.Core
|
||||||
Cresnet = 8,
|
Cresnet = 8,
|
||||||
Cec = 9,
|
Cec = 9,
|
||||||
Udp = 10,
|
Udp = 10,
|
||||||
UdpClient = 11,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
These enumerations are not case sensitive. Not all methods are valid for a ```genericComm``` device. For a comport, the only valid type would be ```Com```. For a direct network socket, valid options are ```Ssh```, ```Tcpip```, ```Telnet```, ```UdpClient```, and ```Udp```.
|
These enumerations are not case sensitive. Not all methods are valid for a ```genericComm``` device. For a comport, the only valid type would be ```Com```. For a direct network socket, valid options are ```Ssh```, ```Tcpip```, ```Telnet```, and ```Udp```.
|
||||||
|
|
||||||
##### ComParams
|
##### ComParams
|
||||||
|
|
||||||
|
|
@ -288,7 +287,7 @@ This property maps to the number of the port on the device you have mapped the r
|
||||||
|
|
||||||
##### TcpSshParams
|
##### TcpSshParams
|
||||||
|
|
||||||
A ```Ssh```, ```TcpIp```, ```UdpClient```, or ```Udp``` device requires a ```tcpSshProperties``` object to set the propeties of the socket.
|
A ```Ssh```, ```TcpIp```, or ```Udp``` device requires a ```tcpSshProperties``` object to set the propeties of the socket.
|
||||||
|
|
||||||
```Json
|
```Json
|
||||||
{
|
{
|
||||||
|
|
@ -305,7 +304,7 @@ A ```Ssh```, ```TcpIp```, ```UdpClient```, or ```Udp``` device requires a ```tcp
|
||||||
|
|
||||||
**```address```**
|
**```address```**
|
||||||
|
|
||||||
This is the IP address, hostname, or FQDN of the resource you wish to open a socket to. Use ```UdpClient``` for outbound UDP to a remote endpoint. Use ```Udp``` when you need Essentials to bind a local UDP listener.
|
This is the IP address, hostname, or FQDN of the resource you wish to open a socket to. In the case of a UDP device, you can set either a single whitelist address with this data, or an appropriate broadcast address.
|
||||||
|
|
||||||
**```port```**
|
**```port```**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>2.42.1-local</Version>
|
<Version>2.19.4-local</Version>
|
||||||
<InformationalVersion>$(Version)</InformationalVersion>
|
<InformationalVersion>$(Version)</InformationalVersion>
|
||||||
<Authors>PepperDash Technology</Authors>
|
<Authors>PepperDash Technology</Authors>
|
||||||
<Company>PepperDash Technology</Company>
|
<Company>PepperDash Technology</Company>
|
||||||
<Product>PepperDash Essentials</Product>
|
<Product>PepperDash Essentials</Product>
|
||||||
<Copyright>Copyright © 2026</Copyright>
|
<Copyright>Copyright © 2025</Copyright>
|
||||||
<RepositoryUrl>https://github.com/PepperDash/Essentials</RepositoryUrl>
|
<RepositoryUrl>https://github.com/PepperDash/Essentials</RepositoryUrl>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<PackageTags>Crestron; 4series</PackageTags>
|
<PackageTags>Crestron; 4series</PackageTags>
|
||||||
|
|
@ -20,7 +20,4 @@
|
||||||
<None Include="..\..\LICENSE.md" Pack="true" PackagePath=""/>
|
<None Include="..\..\LICENSE.md" Pack="true" PackagePath=""/>
|
||||||
<None Include="..\..\README.md" Pack="true" PackagePath=""/>
|
<None Include="..\..\README.md" Pack="true" PackagePath=""/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
|
||||||
<AssemblyMetadata Include="PackageId" Value="$(PackageId)" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -151,8 +151,6 @@ namespace PepperDash.Core
|
||||||
// Thread-safety lock for state changes
|
// Thread-safety lock for state changes
|
||||||
private readonly object _stateLock = new object();
|
private readonly object _stateLock = new object();
|
||||||
|
|
||||||
private volatile bool _isProgramStopping;
|
|
||||||
|
|
||||||
private bool disconnectLogged = false;
|
private bool disconnectLogged = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -209,9 +207,11 @@ namespace PepperDash.Core
|
||||||
{
|
{
|
||||||
if (programEventType == eProgramStatusEventType.Stopping)
|
if (programEventType == eProgramStatusEventType.Stopping)
|
||||||
{
|
{
|
||||||
_isProgramStopping = true;
|
if (client != null)
|
||||||
this.LogDebug("Program stopping. Closing connection");
|
{
|
||||||
Disconnect();
|
this.LogDebug("Program stopping. Closing connection");
|
||||||
|
Disconnect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -228,12 +228,6 @@ namespace PepperDash.Core
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_isProgramStopping)
|
|
||||||
{
|
|
||||||
this.LogDebug("Skipping connect because program is stopping");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ConnectEnabled = true;
|
ConnectEnabled = true;
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
@ -293,7 +287,13 @@ namespace PepperDash.Core
|
||||||
}
|
}
|
||||||
catch (SshConnectionException e)
|
catch (SshConnectionException e)
|
||||||
{
|
{
|
||||||
var ie = e.InnerException; // The details are inside, when present - remote can close the connection with no inner exception at all
|
var ie = e.InnerException; // The details are inside!!
|
||||||
|
|
||||||
|
if (ie is SocketException)
|
||||||
|
{
|
||||||
|
this.LogError("CONNECTION failure: Cannot reach host");
|
||||||
|
this.LogVerbose(ie, "Exception details: ");
|
||||||
|
}
|
||||||
|
|
||||||
if (ie is System.Net.Sockets.SocketException socketException)
|
if (ie is System.Net.Sockets.SocketException socketException)
|
||||||
{
|
{
|
||||||
|
|
@ -301,20 +301,20 @@ namespace PepperDash.Core
|
||||||
Hostname, Port);
|
Hostname, Port);
|
||||||
this.LogVerbose(socketException, "SocketException details: ");
|
this.LogVerbose(socketException, "SocketException details: ");
|
||||||
}
|
}
|
||||||
else if (ie is SshAuthenticationException)
|
if (ie is SshAuthenticationException)
|
||||||
{
|
{
|
||||||
this.LogError("Authentication failure for username {userName}", Username);
|
this.LogError("Authentication failure for username {userName}", Username);
|
||||||
this.LogVerbose(ie, "AuthenticationException details: ");
|
this.LogVerbose(ie, "AuthenticationException details: ");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
this.LogError("Error on connect: {error}", ie?.Message ?? e.Message);
|
this.LogError("Error on connect: {error}", ie.Message);
|
||||||
this.LogVerbose(ie ?? e, "Exception details: ");
|
this.LogVerbose(ie, "Exception details: ");
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectLogged = true;
|
disconnectLogged = true;
|
||||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
if (AutoReconnect)
|
||||||
{
|
{
|
||||||
this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs);
|
this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||||
StartReconnectTimer();
|
StartReconnectTimer();
|
||||||
|
|
@ -326,7 +326,7 @@ namespace PepperDash.Core
|
||||||
|
|
||||||
disconnectLogged = true;
|
disconnectLogged = true;
|
||||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
if (AutoReconnect)
|
||||||
{
|
{
|
||||||
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||||
StartReconnectTimer();
|
StartReconnectTimer();
|
||||||
|
|
@ -338,7 +338,7 @@ namespace PepperDash.Core
|
||||||
this.LogVerbose(e, "Exception details: ");
|
this.LogVerbose(e, "Exception details: ");
|
||||||
disconnectLogged = true;
|
disconnectLogged = true;
|
||||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
if (AutoReconnect)
|
||||||
{
|
{
|
||||||
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||||
StartReconnectTimer();
|
StartReconnectTimer();
|
||||||
|
|
@ -473,7 +473,7 @@ namespace PepperDash.Core
|
||||||
{
|
{
|
||||||
connectLock.Release();
|
connectLock.Release();
|
||||||
}
|
}
|
||||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
if (AutoReconnect && ConnectEnabled)
|
||||||
{
|
{
|
||||||
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||||
StartReconnectTimer();
|
StartReconnectTimer();
|
||||||
|
|
@ -516,10 +516,7 @@ namespace PepperDash.Core
|
||||||
this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim());
|
this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim());
|
||||||
|
|
||||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
StartReconnectTimer();
|
||||||
{
|
|
||||||
StartReconnectTimer();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -552,10 +549,7 @@ namespace PepperDash.Core
|
||||||
this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes));
|
this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes));
|
||||||
|
|
||||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
StartReconnectTimer();
|
||||||
{
|
|
||||||
StartReconnectTimer();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,463 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Crestron.SimplSharp;
|
|
||||||
using Crestron.SimplSharp.CrestronSockets;
|
|
||||||
using ThreadingTimeout = System.Threading.Timeout;
|
|
||||||
using NetSocketException = System.Net.Sockets.SocketException;
|
|
||||||
|
|
||||||
namespace PepperDash.Core
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A class to handle basic UDP communications to a remote endpoint
|
|
||||||
/// </summary>
|
|
||||||
public class GenericUdpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
|
|
||||||
{
|
|
||||||
private const string SplusKey = "Uninitialized UdpClient";
|
|
||||||
|
|
||||||
private readonly object stateLock = new object();
|
|
||||||
private readonly Timer reconnectTimer;
|
|
||||||
|
|
||||||
private UdpClient client;
|
|
||||||
private CancellationTokenSource receiveCancellationTokenSource;
|
|
||||||
private bool connectEnabled;
|
|
||||||
private bool connectionRefusedLogged;
|
|
||||||
private SocketStatus clientStatus = SocketStatus.SOCKET_STATUS_NO_CONNECT;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Object to enable stream debugging
|
|
||||||
/// </summary>
|
|
||||||
public CommunicationStreamDebugging StreamDebugging { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fires when data is received from the remote endpoint and returns it as a byte array
|
|
||||||
/// </summary>
|
|
||||||
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fires when data is received from the remote endpoint and returns it as text
|
|
||||||
/// </summary>
|
|
||||||
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fires when the socket status changes
|
|
||||||
/// </summary>
|
|
||||||
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Address of remote endpoint
|
|
||||||
/// </summary>
|
|
||||||
public string Hostname { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Port on remote endpoint
|
|
||||||
/// </summary>
|
|
||||||
public int Port { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Another S+ helper because large port numbers can be treated as signed ints
|
|
||||||
/// </summary>
|
|
||||||
public ushort UPort
|
|
||||||
{
|
|
||||||
get { return Convert.ToUInt16(Port); }
|
|
||||||
set { Port = Convert.ToInt32(value); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Defaults to 2000
|
|
||||||
/// </summary>
|
|
||||||
public int BufferSize { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// True when the local socket is created and associated with the configured remote endpoint
|
|
||||||
/// </summary>
|
|
||||||
public bool IsConnected
|
|
||||||
{
|
|
||||||
get { return ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// S+ helper for IsConnected
|
|
||||||
/// </summary>
|
|
||||||
public ushort UIsConnected
|
|
||||||
{
|
|
||||||
get { return (ushort)(IsConnected ? 1 : 0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The current socket status of the client
|
|
||||||
/// </summary>
|
|
||||||
public SocketStatus ClientStatus
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
lock (stateLock)
|
|
||||||
{
|
|
||||||
return clientStatus;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private set
|
|
||||||
{
|
|
||||||
var shouldFireEvent = false;
|
|
||||||
|
|
||||||
lock (stateLock)
|
|
||||||
{
|
|
||||||
if (clientStatus != value)
|
|
||||||
{
|
|
||||||
clientStatus = value;
|
|
||||||
shouldFireEvent = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldFireEvent)
|
|
||||||
ConnectionChange?.Invoke(this, new GenericSocketStatusChageEventArgs(this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ushort representation of client status
|
|
||||||
/// </summary>
|
|
||||||
public ushort UStatus
|
|
||||||
{
|
|
||||||
get { return (ushort)ClientStatus; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the AutoReconnect
|
|
||||||
/// </summary>
|
|
||||||
public bool AutoReconnect { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// S+ helper for AutoReconnect
|
|
||||||
/// </summary>
|
|
||||||
public ushort UAutoReconnect
|
|
||||||
{
|
|
||||||
get { return (ushort)(AutoReconnect ? 1 : 0); }
|
|
||||||
set { AutoReconnect = value == 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
|
|
||||||
/// </summary>
|
|
||||||
public int AutoReconnectIntervalMs { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Constructor
|
|
||||||
/// </summary>
|
|
||||||
public GenericUdpClient(string key, string address, int port, int bufferSize)
|
|
||||||
: base(key)
|
|
||||||
{
|
|
||||||
StreamDebugging = new CommunicationStreamDebugging(key);
|
|
||||||
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
|
|
||||||
AutoReconnectIntervalMs = 5000;
|
|
||||||
Hostname = address;
|
|
||||||
Port = port;
|
|
||||||
BufferSize = bufferSize;
|
|
||||||
|
|
||||||
reconnectTimer = new Timer(o =>
|
|
||||||
{
|
|
||||||
if (connectEnabled)
|
|
||||||
Connect();
|
|
||||||
}, null, ThreadingTimeout.Infinite, ThreadingTimeout.Infinite);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Constructor for S+
|
|
||||||
/// </summary>
|
|
||||||
public GenericUdpClient()
|
|
||||||
: base(SplusKey)
|
|
||||||
{
|
|
||||||
StreamDebugging = new CommunicationStreamDebugging(SplusKey);
|
|
||||||
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
|
|
||||||
AutoReconnectIntervalMs = 5000;
|
|
||||||
BufferSize = 2000;
|
|
||||||
|
|
||||||
reconnectTimer = new Timer(o =>
|
|
||||||
{
|
|
||||||
if (connectEnabled)
|
|
||||||
Connect();
|
|
||||||
}, null, ThreadingTimeout.Infinite, ThreadingTimeout.Infinite);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialize method
|
|
||||||
/// </summary>
|
|
||||||
public void Initialize(string key)
|
|
||||||
{
|
|
||||||
Key = key;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
|
|
||||||
{
|
|
||||||
if (programEventType == eProgramStatusEventType.Stopping)
|
|
||||||
{
|
|
||||||
Debug.Console(1, this, "Program stopping. Closing connection");
|
|
||||||
Deactivate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deactivate method
|
|
||||||
/// </summary>
|
|
||||||
public override bool Deactivate()
|
|
||||||
{
|
|
||||||
Disconnect();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Connect method
|
|
||||||
/// </summary>
|
|
||||||
public void Connect()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(Hostname))
|
|
||||||
{
|
|
||||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpClient '{0}': No address set", Key);
|
|
||||||
ClientStatus = SocketStatus.SOCKET_STATUS_NO_CONNECT;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Port < 1 || Port > 65535)
|
|
||||||
{
|
|
||||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpClient '{0}': Invalid port", Key);
|
|
||||||
ClientStatus = SocketStatus.SOCKET_STATUS_NO_CONNECT;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostname = Hostname;
|
|
||||||
var port = Port;
|
|
||||||
var bufferSize = BufferSize;
|
|
||||||
UdpClient newClient = null;
|
|
||||||
CancellationTokenSource newReceiveCancellationTokenSource = null;
|
|
||||||
CancellationToken startReceiveToken = default(CancellationToken);
|
|
||||||
var shouldStartReceive = false;
|
|
||||||
|
|
||||||
lock (stateLock)
|
|
||||||
{
|
|
||||||
connectEnabled = true;
|
|
||||||
|
|
||||||
if (client != null)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
newReceiveCancellationTokenSource = new CancellationTokenSource();
|
|
||||||
newClient = new UdpClient();
|
|
||||||
newClient.Client.ReceiveBufferSize = bufferSize;
|
|
||||||
newClient.Client.SendBufferSize = bufferSize;
|
|
||||||
newClient.Connect(hostname, port);
|
|
||||||
|
|
||||||
lock (stateLock)
|
|
||||||
{
|
|
||||||
if (!connectEnabled || client != null)
|
|
||||||
{
|
|
||||||
newClient.Close();
|
|
||||||
newReceiveCancellationTokenSource.Cancel();
|
|
||||||
newReceiveCancellationTokenSource.Dispose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
receiveCancellationTokenSource = newReceiveCancellationTokenSource;
|
|
||||||
client = newClient;
|
|
||||||
ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED;
|
|
||||||
reconnectTimer.Change(ThreadingTimeout.Infinite, ThreadingTimeout.Infinite);
|
|
||||||
startReceiveToken = receiveCancellationTokenSource.Token;
|
|
||||||
shouldStartReceive = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldStartReceive)
|
|
||||||
StartReceive(startReceiveToken);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogMessage(ex, "Error connecting UDP client {0}", this, Key);
|
|
||||||
|
|
||||||
if (newClient != null)
|
|
||||||
newClient.Close();
|
|
||||||
|
|
||||||
if (newReceiveCancellationTokenSource != null)
|
|
||||||
{
|
|
||||||
newReceiveCancellationTokenSource.Cancel();
|
|
||||||
newReceiveCancellationTokenSource.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (stateLock)
|
|
||||||
{
|
|
||||||
if (connectEnabled && client == null)
|
|
||||||
{
|
|
||||||
ClientStatus = SocketStatus.SOCKET_STATUS_NO_CONNECT;
|
|
||||||
StartReconnectTimer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Disconnect method
|
|
||||||
/// </summary>
|
|
||||||
public void Disconnect()
|
|
||||||
{
|
|
||||||
lock (stateLock)
|
|
||||||
{
|
|
||||||
connectEnabled = false;
|
|
||||||
reconnectTimer.Change(ThreadingTimeout.Infinite, ThreadingTimeout.Infinite);
|
|
||||||
CleanupClient();
|
|
||||||
ClientStatus = SocketStatus.SOCKET_STATUS_NO_CONNECT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SendText method
|
|
||||||
/// </summary>
|
|
||||||
public void SendText(string text)
|
|
||||||
{
|
|
||||||
this.PrintSentText(text);
|
|
||||||
|
|
||||||
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
|
|
||||||
SendBytes(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SendBytes method
|
|
||||||
/// </summary>
|
|
||||||
public void SendBytes(byte[] bytes)
|
|
||||||
{
|
|
||||||
if (bytes == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
this.PrintSentBytes(bytes);
|
|
||||||
|
|
||||||
if (!IsConnected || client == null)
|
|
||||||
Connect();
|
|
||||||
|
|
||||||
var udpClient = client;
|
|
||||||
if (!IsConnected || udpClient == null)
|
|
||||||
{
|
|
||||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpClient '{0}': Cannot send bytes because the client is not connected", Key);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
udpClient.Send(bytes, bytes.Length);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogMessage(ex, "Error sending UDP bytes for {0}", this, Key);
|
|
||||||
HandleDisconnected();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StartReceive(CancellationToken token)
|
|
||||||
{
|
|
||||||
Task.Run(async () =>
|
|
||||||
{
|
|
||||||
while (!token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var udpClient = client;
|
|
||||||
if (udpClient == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var result = await udpClient.ReceiveAsync().ConfigureAwait(false);
|
|
||||||
var bytes = result.Buffer;
|
|
||||||
if (bytes == null || bytes.Length == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
connectionRefusedLogged = false;
|
|
||||||
|
|
||||||
var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
|
|
||||||
|
|
||||||
this.PrintReceivedBytes(bytes);
|
|
||||||
this.PrintReceivedText(text);
|
|
||||||
|
|
||||||
BytesReceived?.Invoke(this, new GenericCommMethodReceiveBytesArgs(bytes));
|
|
||||||
TextReceived?.Invoke(this, new GenericCommMethodReceiveTextArgs(text));
|
|
||||||
}
|
|
||||||
catch (ObjectDisposedException)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch (NetSocketException ex)
|
|
||||||
{
|
|
||||||
if (ex.SocketErrorCode == SocketError.ConnectionRefused)
|
|
||||||
{
|
|
||||||
if (!connectionRefusedLogged)
|
|
||||||
{
|
|
||||||
Debug.Console(1, Debug.ErrorLogLevel.Warning,
|
|
||||||
"GenericUdpClient '{0}': Remote endpoint refused UDP traffic or is no longer listening",
|
|
||||||
Key);
|
|
||||||
connectionRefusedLogged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
HandleDisconnected();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.LogMessage(ex, "UDP receive error for {0}", this, Key);
|
|
||||||
|
|
||||||
if (AutoReconnect)
|
|
||||||
{
|
|
||||||
HandleDisconnected();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogMessage(ex, "Unexpected UDP receive error for {0}", this, Key);
|
|
||||||
|
|
||||||
if (AutoReconnect)
|
|
||||||
{
|
|
||||||
HandleDisconnected();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleDisconnected()
|
|
||||||
{
|
|
||||||
lock (stateLock)
|
|
||||||
{
|
|
||||||
CleanupClient();
|
|
||||||
ClientStatus = SocketStatus.SOCKET_STATUS_NO_CONNECT;
|
|
||||||
StartReconnectTimer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StartReconnectTimer()
|
|
||||||
{
|
|
||||||
if (AutoReconnect && connectEnabled)
|
|
||||||
reconnectTimer.Change(AutoReconnectIntervalMs, ThreadingTimeout.Infinite);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CleanupClient()
|
|
||||||
{
|
|
||||||
if (receiveCancellationTokenSource != null)
|
|
||||||
{
|
|
||||||
receiveCancellationTokenSource.Cancel();
|
|
||||||
receiveCancellationTokenSource.Dispose();
|
|
||||||
receiveCancellationTokenSource = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (client != null)
|
|
||||||
{
|
|
||||||
client.Close();
|
|
||||||
client = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -18,75 +18,70 @@ namespace PepperDash.Core
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// RS232/422/485
|
/// RS232/422/485
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Com = 1,
|
Com,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Crestron IpId (most Crestron ethernet devices)
|
/// Crestron IpId (most Crestron ethernet devices)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IpId = 2,
|
IpId,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Crestron IpIdTcp (HD-MD series, etc.)
|
/// Crestron IpIdTcp (HD-MD series, etc.)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IpidTcp = 3,
|
IpidTcp,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Crestron IR control
|
/// Crestron IR control
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IR = 4,
|
IR,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// SSH client
|
/// SSH client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Ssh = 5,
|
Ssh,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// TCP/IP client
|
/// TCP/IP client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Tcpip = 6,
|
Tcpip,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Telnet
|
/// Telnet
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Telnet = 7,
|
Telnet,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Crestnet device
|
/// Crestnet device
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Cresnet = 8,
|
Cresnet,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// CEC Control, via a DM HDMI port
|
/// CEC Control, via a DM HDMI port
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Cec = 9,
|
Cec,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// UDP Server
|
/// UDP Server
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Udp = 10,
|
Udp,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// HTTP client
|
/// HTTP client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Http = 11,
|
Http,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// HTTPS client
|
/// HTTPS client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Https = 12,
|
Https,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Websocket client
|
/// Websocket client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Ws = 13,
|
Ws,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Secure Websocket client
|
/// Secure Websocket client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Wss = 14,
|
Wss,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Secure TCP/IP
|
/// Secure TCP/IP
|
||||||
/// </summary>
|
/// </summary>
|
||||||
SecureTcpIp = 15,
|
SecureTcpIp,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used when comms needs to be handled in SIMPL and bridged opposite the normal direction
|
/// Used when comms needs to be handled in SIMPL and bridged opposite the normal direction
|
||||||
/// </summary>
|
/// </summary>
|
||||||
ComBridge = 16,
|
ComBridge,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// InfinetEX control
|
/// InfinetEX control
|
||||||
/// </summary>
|
/// </summary>
|
||||||
InfinetEx = 17,
|
InfinetEx
|
||||||
/// <summary>
|
|
||||||
/// UDP client
|
|
||||||
/// </summary>
|
|
||||||
UdpClient = 18,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,9 +5,9 @@ using Serilog.Events;
|
||||||
namespace PepperDash.Core
|
namespace PepperDash.Core
|
||||||
{
|
{
|
||||||
//*********************************************************************************************************
|
//*********************************************************************************************************
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a Device
|
/// Represents a Device
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Device : IKeyName
|
public class Device : IKeyName
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
@ -15,15 +15,25 @@ namespace PepperDash.Core
|
||||||
/// Unique Key
|
/// Unique Key
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Key { get; protected set; }
|
public string Key { get; protected set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the Name
|
/// Gets or sets the Name
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Name { get; protected set; }
|
public string Name { get; protected set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Enabled { get; protected set; }
|
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> _PreActivationActions;
|
||||||
List<Action> _PostActivationActions;
|
List<Action> _PostActivationActions;
|
||||||
|
|
||||||
|
|
@ -76,9 +86,9 @@ namespace PepperDash.Core
|
||||||
/// Adds a post activation action
|
/// Adds a post activation action
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="act"></param>
|
/// <param name="act"></param>
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// AddPostActivationAction method
|
/// AddPostActivationAction method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void AddPostActivationAction(Action act)
|
public void AddPostActivationAction(Action act)
|
||||||
{
|
{
|
||||||
if (_PostActivationActions == null)
|
if (_PostActivationActions == null)
|
||||||
|
|
@ -86,9 +96,9 @@ namespace PepperDash.Core
|
||||||
_PostActivationActions.Add(act);
|
_PostActivationActions.Add(act);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// PreActivate method
|
/// PreActivate method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void PreActivate()
|
public void PreActivate()
|
||||||
{
|
{
|
||||||
if (_PreActivationActions != null)
|
if (_PreActivationActions != null)
|
||||||
|
|
@ -105,9 +115,9 @@ namespace PepperDash.Core
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Activate method
|
/// Activate method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Activate()
|
public bool Activate()
|
||||||
{
|
{
|
||||||
//if (_PreActivationActions != null)
|
//if (_PreActivationActions != null)
|
||||||
|
|
@ -118,9 +128,9 @@ namespace PepperDash.Core
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// PostActivate method
|
/// PostActivate method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void PostActivate()
|
public void PostActivate()
|
||||||
{
|
{
|
||||||
if (_PostActivationActions != null)
|
if (_PostActivationActions != null)
|
||||||
|
|
@ -143,9 +153,9 @@ namespace PepperDash.Core
|
||||||
/// do not need to call base.CustomActivate()
|
/// do not need to call base.CustomActivate()
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>true if device activated successfully.</returns>
|
/// <returns>true if device activated successfully.</returns>
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// CustomActivate method
|
/// CustomActivate method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual bool CustomActivate() { return true; }
|
public virtual bool CustomActivate() { return true; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -172,15 +182,15 @@ namespace PepperDash.Core
|
||||||
if (o is bool && !(bool)o) a();
|
if (o is bool && !(bool)o) a();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a string representation of the object, including its key and name.
|
/// Returns a string representation of the object, including its key and name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>The returned string is formatted as "{Key} - {Name}". If the <c>Name</c> property is
|
/// <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>
|
/// 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>
|
/// <returns>A string that represents the object, containing the key and name in the format "{Key} - {Name}".</returns>
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ToString method
|
/// ToString method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return string.Format("{0} - {1}", Key, string.IsNullOrEmpty(Name) ? "---" : Name);
|
return string.Format("{0} - {1}", Key, string.IsNullOrEmpty(Name) ? "---" : Name);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Crestron.SimplSharp;
|
using Crestron.SimplSharp;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ namespace PepperDash.Core
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a debugging context
|
/// Represents a debugging context
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("DebugContext is no longer supported and will be removed in a future release.")]
|
|
||||||
public class DebugContext
|
public class DebugContext
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Serilog.Core;
|
using Serilog.Core;
|
||||||
using Serilog.Events;
|
using Serilog.Events;
|
||||||
|
|
@ -8,18 +12,11 @@ using Crestron.SimplSharp;
|
||||||
using WebSocketSharp;
|
using WebSocketSharp;
|
||||||
using System.Security.Authentication;
|
using System.Security.Authentication;
|
||||||
using WebSocketSharp.Net;
|
using WebSocketSharp.Net;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Org.BouncyCastle.Asn1;
|
|
||||||
using Org.BouncyCastle.Asn1.X509;
|
using Org.BouncyCastle.Asn1.X509;
|
||||||
using Org.BouncyCastle.Crypto;
|
|
||||||
using Org.BouncyCastle.Crypto.Generators;
|
|
||||||
using Org.BouncyCastle.Crypto.Operators;
|
|
||||||
using Org.BouncyCastle.Math;
|
|
||||||
using Org.BouncyCastle.Pkcs;
|
|
||||||
using Org.BouncyCastle.Security;
|
|
||||||
using Org.BouncyCastle.X509;
|
|
||||||
using Serilog.Formatting;
|
using Serilog.Formatting;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using Serilog.Formatting.Json;
|
using Serilog.Formatting.Json;
|
||||||
|
|
||||||
namespace PepperDash.Core
|
namespace PepperDash.Core
|
||||||
|
|
@ -30,40 +27,26 @@ namespace PepperDash.Core
|
||||||
public class DebugWebsocketSink : ILogEventSink
|
public class DebugWebsocketSink : ILogEventSink
|
||||||
{
|
{
|
||||||
private HttpServer _httpsServer;
|
private HttpServer _httpsServer;
|
||||||
|
|
||||||
private string _path = "/debug/join/";
|
private string _path = "/debug/join/";
|
||||||
private const string _certificateName = "selfCres";
|
private const string _certificateName = "selfCres";
|
||||||
private const string _certificatePassword = "cres12345";
|
private const string _certificatePassword = "cres12345";
|
||||||
|
|
||||||
private static string CertPath =>
|
public int Port
|
||||||
$"{Path.DirectorySeparatorChar}user{Path.DirectorySeparatorChar}{_certificateName}.pfx";
|
{ get
|
||||||
|
{
|
||||||
|
|
||||||
public int Port
|
if(_httpsServer == null) return 0;
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
|
|
||||||
if (_httpsServer == null) return 0;
|
|
||||||
return _httpsServer.Port;
|
return _httpsServer.Port;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Url
|
public string Url
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (_httpsServer == null || !_httpsServer.IsListening) return "";
|
if (_httpsServer == null) return "";
|
||||||
var service = _httpsServer.WebSocketServices[_path];
|
return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{_httpsServer.Port}{_httpsServer.WebSocketServices[_path].Path}";
|
||||||
if (service == null) return "";
|
|
||||||
|
|
||||||
// Use CSLAN IP if available, otherwise fallback to primary IP. This ensures we provide a reachable URL in dual-stack environments.
|
|
||||||
var cslanIp = CrestronEthernetHelper.GetEthernetParameter(
|
|
||||||
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 1);
|
|
||||||
if (!string.IsNullOrEmpty(cslanIp) && cslanIp != "Invalid Value")
|
|
||||||
return $"wss://{cslanIp}:{_httpsServer.Port}{service.Path}";
|
|
||||||
else
|
|
||||||
return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{_httpsServer.Port}{service.Path}";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,138 +54,60 @@ namespace PepperDash.Core
|
||||||
/// Gets or sets the IsRunning
|
/// Gets or sets the IsRunning
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsRunning { get => _httpsServer?.IsListening ?? false; }
|
public bool IsRunning { get => _httpsServer?.IsListening ?? false; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a value indicating whether there are active WebSocket connections.
|
|
||||||
/// </summary>
|
|
||||||
public bool HasActiveConnections
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_httpsServer == null || !_httpsServer.IsListening) return false;
|
|
||||||
var service = _httpsServer.WebSocketServices[_path];
|
|
||||||
if (service == null) return false;
|
|
||||||
return service.Sessions.Count > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private readonly ITextFormatter _textFormatter;
|
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)
|
public DebugWebsocketSink(ITextFormatter formatProvider)
|
||||||
{
|
{
|
||||||
|
|
||||||
_textFormatter = formatProvider ?? new JsonFormatter();
|
_textFormatter = formatProvider ?? new JsonFormatter();
|
||||||
|
|
||||||
if (!File.Exists(CertPath))
|
if (!File.Exists($"\\user\\{_certificateName}.pfx"))
|
||||||
CreateCert();
|
CreateCert(null);
|
||||||
|
|
||||||
try
|
CrestronEnvironment.ProgramStatusEventHandler += type =>
|
||||||
{
|
{
|
||||||
CrestronEnvironment.ProgramStatusEventHandler += type =>
|
if (type == eProgramStatusEventType.Stopping)
|
||||||
{
|
{
|
||||||
if (type == eProgramStatusEventType.Stopping)
|
StopServer();
|
||||||
StopServer();
|
}
|
||||||
};
|
};
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// CrestronEnvironment is not available in test / dev environments — safe to skip.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CreateCert()
|
private void CreateCert(string[] args)
|
||||||
{
|
{
|
||||||
// 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
|
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();
|
||||||
var ipAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
|
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 hostName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0);
|
||||||
var domainName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, 0);
|
var domainName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, 0);
|
||||||
|
|
||||||
CrestronConsole.PrintLine(string.Format("CreateCert: DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress));
|
//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));
|
||||||
|
|
||||||
var subjectName = string.Format("CN={0}.{1}", hostName, domainName);
|
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 });
|
||||||
var fqdn = string.Format("{0}.{1}", hostName, domainName);
|
//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");
|
||||||
var random = new SecureRandom();
|
//utility.AddCertToStore(certificate, StoreName.My, StoreLocation.LocalMachine);
|
||||||
|
//Debug.Console(0, "CreateCert Saving Cert to \\user\\");
|
||||||
// Generate RSA 2048 key pair
|
CrestronConsole.PrintLine("CreateCert Saving Cert to \\user\\");
|
||||||
var keyPairGenerator = new RsaKeyPairGenerator();
|
utility.CertificatePassword = _certificatePassword;
|
||||||
keyPairGenerator.Init(new KeyGenerationParameters(random, 2048));
|
utility.WriteCertificate(certificate, @"\user\", _certificateName);
|
||||||
var keyPair = keyPairGenerator.GenerateKeyPair();
|
//Debug.Console(0, "CreateCert Ending CreateCert");
|
||||||
|
CrestronConsole.PrintLine("CreateCert Ending CreateCert");
|
||||||
// Build certificate
|
|
||||||
var certGenerator = new X509V3CertificateGenerator();
|
|
||||||
certGenerator.SetSerialNumber(BigInteger.ValueOf(Math.Abs(DateTime.UtcNow.Ticks)));
|
|
||||||
certGenerator.SetIssuerDN(new X509Name(subjectName));
|
|
||||||
certGenerator.SetSubjectDN(new X509Name(subjectName));
|
|
||||||
certGenerator.SetNotBefore(DateTime.UtcNow);
|
|
||||||
certGenerator.SetNotAfter(DateTime.UtcNow.AddYears(2));
|
|
||||||
certGenerator.SetPublicKey(keyPair.Public);
|
|
||||||
|
|
||||||
// Extended Key Usage: server + client auth
|
|
||||||
certGenerator.AddExtension(X509Extensions.ExtendedKeyUsage, false,
|
|
||||||
new ExtendedKeyUsage(new[] { KeyPurposeID.id_kp_serverAuth, KeyPurposeID.id_kp_clientAuth }));
|
|
||||||
|
|
||||||
// Subject Alternative Names: DNS + IP
|
|
||||||
System.Net.IPAddress parsedIp;
|
|
||||||
if (System.Net.IPAddress.TryParse(ipAddress, out parsedIp))
|
|
||||||
{
|
|
||||||
certGenerator.AddExtension(X509Extensions.SubjectAlternativeName, false,
|
|
||||||
new GeneralNames(new GeneralName[] {
|
|
||||||
new GeneralName(GeneralName.DnsName, fqdn),
|
|
||||||
new GeneralName(GeneralName.IPAddress, ipAddress)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
certGenerator.AddExtension(X509Extensions.SubjectAlternativeName, false,
|
|
||||||
new GeneralNames(new GeneralName(GeneralName.DnsName, fqdn)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign with SHA256withRSA
|
|
||||||
var signatureFactory = new Asn1SignatureFactory("SHA256WITHRSA", keyPair.Private, random);
|
|
||||||
var certificate = certGenerator.Generate(signatureFactory);
|
|
||||||
|
|
||||||
// Export as PKCS12/PFX
|
|
||||||
var pkcs12Store = new Pkcs12StoreBuilder().Build();
|
|
||||||
var certEntry = new X509CertificateEntry(certificate);
|
|
||||||
pkcs12Store.SetCertificateEntry(_certificateName, certEntry);
|
|
||||||
pkcs12Store.SetKeyEntry(_certificateName, new AsymmetricKeyEntry(keyPair.Private), new[] { certEntry });
|
|
||||||
|
|
||||||
var separator = Path.DirectorySeparatorChar;
|
|
||||||
var outputPath = string.Format("{0}user{1}{2}.pfx", separator, separator, _certificateName);
|
|
||||||
|
|
||||||
using (var ms = new MemoryStream())
|
|
||||||
{
|
|
||||||
var passwordChars = _certificatePassword.ToCharArray();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
pkcs12Store.Save(ms, passwordChars, random);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Array.Clear(passwordChars, 0, passwordChars.Length);
|
|
||||||
}
|
|
||||||
File.WriteAllBytes(outputPath, ms.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
CrestronConsole.PrintLine(string.Format("CreateCert: Certificate written to {0}", outputPath));
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
CrestronConsole.PrintLine(string.Format("WSS CreateCert Failed: {0}\r\n{1}", ex.Message, ex.StackTrace));
|
//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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -221,77 +126,14 @@ namespace PepperDash.Core
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts the WebSocket server on the specified port and configures it with the appropriate certificate.
|
/// StartServerAndSetPort method
|
||||||
/// </summary>
|
/// </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)
|
public void StartServerAndSetPort(int port)
|
||||||
{
|
{
|
||||||
Debug.LogInformation("Starting Websocket Server on port: {0}", port);
|
Debug.Console(0, "Starting Websocket Server on port: {0}", port);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Start(port, CertPath, _certificatePassword);
|
Start(port, $"\\user\\{_certificateName}.pfx", _certificatePassword);
|
||||||
}
|
|
||||||
|
|
||||||
private static X509Certificate2 LoadOrRecreateCert(string certPath, string certPassword)
|
|
||||||
{
|
|
||||||
if (!File.Exists(certPath))
|
|
||||||
CreateCert();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return LoadCertFromBouncyCastle(certPath, certPassword);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Cert is corrupt or was written by an incompatible tool — delete and regenerate once.
|
|
||||||
CrestronConsole.PrintLine(string.Format("SSL cert load failed ({0}); regenerating...", ex.Message));
|
|
||||||
try { File.Delete(certPath); } catch { }
|
|
||||||
CreateCert();
|
|
||||||
return LoadCertFromBouncyCastle(certPath, certPassword);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Loads a PKCS#12 file written by BouncyCastle and returns an <see cref="X509Certificate2"/> with
|
|
||||||
/// private key attached.
|
|
||||||
/// The PFX is parsed and re-encoded by BouncyCastle (ensuring format compatibility), then passed as
|
|
||||||
/// raw bytes to <see cref="X509Certificate2"/> so neither <c>RSACryptoServiceProvider</c> nor the
|
|
||||||
/// <c>EphemeralKeySet</c> flag (unsupported on the Crestron/Mono runtime) is needed.
|
|
||||||
/// </summary>
|
|
||||||
private static X509Certificate2 LoadCertFromBouncyCastle(string certPath, string certPassword)
|
|
||||||
{
|
|
||||||
var passwordChars = certPassword.ToCharArray();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var stream = File.OpenRead(certPath))
|
|
||||||
{
|
|
||||||
var store = new Pkcs12StoreBuilder().Build();
|
|
||||||
store.Load(stream, passwordChars);
|
|
||||||
|
|
||||||
// Re-encode through BouncyCastle to guarantee PKCS#12 format compatibility,
|
|
||||||
// then hand raw bytes to X509Certificate2 — no RSACryptoServiceProvider needed.
|
|
||||||
using (var ms = new MemoryStream())
|
|
||||||
{
|
|
||||||
store.Save(ms, passwordChars, new SecureRandom());
|
|
||||||
var cert = new X509Certificate2(ms.ToArray(), certPassword);
|
|
||||||
|
|
||||||
if (!cert.HasPrivateKey)
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
string.Format("Certificate loaded from '{0}' does not contain a private key and cannot be used as a server certificate.", certPath));
|
|
||||||
|
|
||||||
return cert;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Array.Clear(passwordChars, 0, passwordChars.Length);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Start(int port, string certPath = "", string certPassword = "")
|
private void Start(int port, string certPath = "", string certPassword = "")
|
||||||
|
|
@ -300,37 +142,66 @@ namespace PepperDash.Core
|
||||||
{
|
{
|
||||||
_httpsServer = new HttpServer(port, true);
|
_httpsServer = new HttpServer(port, true);
|
||||||
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(certPath))
|
if (!string.IsNullOrWhiteSpace(certPath))
|
||||||
{
|
{
|
||||||
Debug.LogInformation("Assigning SSL Configuration");
|
Debug.Console(0, "Assigning SSL Configuration");
|
||||||
|
_httpsServer.SslConfiguration = new ServerSslConfiguration(new X509Certificate2(certPath, certPassword))
|
||||||
_httpsServer.SslConfiguration.ServerCertificate = LoadOrRecreateCert(certPath, certPassword);
|
{
|
||||||
_httpsServer.SslConfiguration.ClientCertificateRequired = false;
|
ClientCertificateRequired = false,
|
||||||
_httpsServer.SslConfiguration.CheckCertificateRevocation = false;
|
CheckCertificateRevocation = false,
|
||||||
_httpsServer.SslConfiguration.EnabledSslProtocols = SslProtocols.Tls12;
|
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls,
|
||||||
//this is just to test, you might want to actually validate
|
//this is just to test, you might want to actually validate
|
||||||
_httpsServer.SslConfiguration.ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
|
ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
|
||||||
{
|
{
|
||||||
Debug.LogInformation("HTTPS ClientCerticateValidation Callback triggered");
|
Debug.Console(0, "HTTPS ClientCerticateValidation Callback triggered");
|
||||||
return true;
|
return true;
|
||||||
};
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
Debug.LogInformation("Adding Debug Client Service");
|
Debug.Console(0, "Adding Debug Client Service");
|
||||||
_httpsServer.AddWebSocketService<DebugClient>(_path);
|
_httpsServer.AddWebSocketService<DebugClient>(_path);
|
||||||
Debug.LogInformation("Assigning Log Info");
|
Debug.Console(0, "Assigning Log Info");
|
||||||
_httpsServer.Log.Level = LogLevel.Trace;
|
_httpsServer.Log.Level = LogLevel.Trace;
|
||||||
_httpsServer.Log.Output = WriteWebSocketInternalLog;
|
_httpsServer.Log.Output = (d, s) =>
|
||||||
Debug.LogInformation("Starting");
|
{
|
||||||
|
uint level;
|
||||||
|
|
||||||
|
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.Start();
|
_httpsServer.Start();
|
||||||
Debug.LogInformation("Ready");
|
Debug.Console(0, "Ready");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Debug.LogError(ex, "WebSocket Failed to start {0}", ex.Message);
|
Debug.Console(0, "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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -339,68 +210,10 @@ namespace PepperDash.Core
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void StopServer()
|
public void StopServer()
|
||||||
{
|
{
|
||||||
Debug.LogInformation("Stopping Websocket Server");
|
Debug.Console(0, "Stopping Websocket Server");
|
||||||
|
_httpsServer?.Stop();
|
||||||
|
|
||||||
try
|
_httpsServer = null;
|
||||||
{
|
|
||||||
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.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0" />
|
<PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0" />
|
||||||
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.274" />
|
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.90" />
|
||||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||||
<PackageReference Include="Serilog.Expressions" Version="4.0.0" />
|
<PackageReference Include="Serilog.Expressions" Version="4.0.0" />
|
||||||
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />
|
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ using Crestron.SimplSharp;
|
||||||
using Crestron.SimplSharp.WebScripting;
|
using Crestron.SimplSharp.WebScripting;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using PepperDash.Core.Logging;
|
|
||||||
using PepperDash.Core.Web.RequestHandlers;
|
using PepperDash.Core.Web.RequestHandlers;
|
||||||
|
|
||||||
namespace PepperDash.Core.Web
|
namespace PepperDash.Core.Web
|
||||||
|
|
@ -26,26 +25,29 @@ namespace PepperDash.Core.Web
|
||||||
private readonly CCriticalSection _serverLock = new CCriticalSection();
|
private readonly CCriticalSection _serverLock = new CCriticalSection();
|
||||||
private HttpCwsServer _server;
|
private HttpCwsServer _server;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the Key
|
/// Gets or sets the Key
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Key { get; private set; }
|
public string Key { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the Name
|
/// Gets or sets the Name
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Name { get; private set; }
|
public string Name { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the BasePath
|
/// Gets or sets the BasePath
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string BasePath { get; private set; }
|
public string BasePath { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the IsRegistered
|
/// Gets or sets the IsRegistered
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsRegistered { get; private set; }
|
public bool IsRegistered { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Http request handler
|
||||||
|
/// </summary>
|
||||||
//public IHttpCwsHandler HttpRequestHandler
|
//public IHttpCwsHandler HttpRequestHandler
|
||||||
//{
|
//{
|
||||||
// get { return _server.HttpRequestHandler; }
|
// get { return _server.HttpRequestHandler; }
|
||||||
|
|
@ -56,6 +58,9 @@ namespace PepperDash.Core.Web
|
||||||
// }
|
// }
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Received request event handler
|
||||||
|
/// </summary>
|
||||||
//public event EventHandler<HttpCwsRequestEventArgs> ReceivedRequestEvent
|
//public event EventHandler<HttpCwsRequestEventArgs> ReceivedRequestEvent
|
||||||
//{
|
//{
|
||||||
// add { _server.ReceivedRequestEvent += new HttpCwsRequestEventHandler(value); }
|
// add { _server.ReceivedRequestEvent += new HttpCwsRequestEventHandler(value); }
|
||||||
|
|
@ -86,7 +91,7 @@ namespace PepperDash.Core.Web
|
||||||
/// <param name="key"></param>
|
/// <param name="key"></param>
|
||||||
/// <param name="name"></param>
|
/// <param name="name"></param>
|
||||||
/// <param name="basePath"></param>
|
/// <param name="basePath"></param>
|
||||||
public WebApiServer(string key, string name, string basePath)
|
public WebApiServer(string key, string name, string basePath)
|
||||||
{
|
{
|
||||||
Key = key;
|
Key = key;
|
||||||
Name = string.IsNullOrEmpty(name) ? DefaultName : name;
|
Name = string.IsNullOrEmpty(name) ? DefaultName : name;
|
||||||
|
|
@ -94,8 +99,6 @@ namespace PepperDash.Core.Web
|
||||||
|
|
||||||
if (_server == null) _server = new HttpCwsServer(BasePath);
|
if (_server == null) _server = new HttpCwsServer(BasePath);
|
||||||
|
|
||||||
_server.AuthenticateAllRoutes = false;
|
|
||||||
|
|
||||||
_server.setProcessName(Key);
|
_server.setProcessName(Key);
|
||||||
_server.HttpRequestHandler = new DefaultRequestHandler();
|
_server.HttpRequestHandler = new DefaultRequestHandler();
|
||||||
|
|
||||||
|
|
@ -111,7 +114,7 @@ namespace PepperDash.Core.Web
|
||||||
{
|
{
|
||||||
if (programEventType != eProgramStatusEventType.Stopping) return;
|
if (programEventType != eProgramStatusEventType.Stopping) return;
|
||||||
|
|
||||||
this.LogInformation("Program stopping. stopping server");
|
Debug.Console(DebugInfo, this, "Program stopping. stopping server");
|
||||||
|
|
||||||
Stop();
|
Stop();
|
||||||
}
|
}
|
||||||
|
|
@ -125,18 +128,18 @@ namespace PepperDash.Core.Web
|
||||||
// Re-enable the server if the link comes back up and the status should be connected
|
// Re-enable the server if the link comes back up and the status should be connected
|
||||||
if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp && IsRegistered)
|
if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp && IsRegistered)
|
||||||
{
|
{
|
||||||
this.LogInformation("Ethernet link up. Server is alreedy registered.");
|
Debug.Console(DebugInfo, this, "Ethernet link up. Server is alreedy registered.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.LogInformation("Ethernet link up. Starting server");
|
Debug.Console(DebugInfo, this, "Ethernet link up. Starting server");
|
||||||
|
|
||||||
Start();
|
Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initialize method
|
/// Initialize method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Initialize(string key, string basePath)
|
public void Initialize(string key, string basePath)
|
||||||
{
|
{
|
||||||
Key = key;
|
Key = key;
|
||||||
|
|
@ -150,7 +153,7 @@ namespace PepperDash.Core.Web
|
||||||
{
|
{
|
||||||
if (route == null)
|
if (route == null)
|
||||||
{
|
{
|
||||||
this.LogWarning("Failed to add route, route parameter is null");
|
Debug.Console(DebugInfo, this, "Failed to add route, route parameter is null");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,36 +165,23 @@ namespace PepperDash.Core.Web
|
||||||
/// Removes a route from CWS
|
/// Removes a route from CWS
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="route"></param>
|
/// <param name="route"></param>
|
||||||
|
/// <summary>
|
||||||
|
/// RemoveRoute method
|
||||||
|
/// </summary>
|
||||||
public void RemoveRoute(HttpCwsRoute route)
|
public void RemoveRoute(HttpCwsRoute route)
|
||||||
{
|
{
|
||||||
if (route == null)
|
if (route == null)
|
||||||
{
|
{
|
||||||
this.LogWarning("Failed to remove route, route parameter is null");
|
Debug.Console(DebugInfo, this, "Failed to remote route, orute parameter is null");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_server.Routes.Remove(route);
|
_server.Routes.Remove(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the fallback request handler that is invoked when no registered route
|
/// GetRouteCollection method
|
||||||
/// matches an incoming request. Must be called before <see cref="Start"/>.
|
/// </summary>
|
||||||
/// </summary>
|
|
||||||
/// <param name="handler">The handler to use as the server-level fallback.</param>
|
|
||||||
public void SetFallbackHandler(IHttpCwsHandler handler)
|
|
||||||
{
|
|
||||||
if (handler == null)
|
|
||||||
{
|
|
||||||
this.LogWarning("SetFallbackHandler: handler parameter is null, ignoring");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_server.HttpRequestHandler = handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// GetRouteCollection method
|
|
||||||
/// </summary>
|
|
||||||
public HttpCwsRouteCollection GetRouteCollection()
|
public HttpCwsRouteCollection GetRouteCollection()
|
||||||
{
|
{
|
||||||
return _server.Routes;
|
return _server.Routes;
|
||||||
|
|
@ -208,24 +198,26 @@ namespace PepperDash.Core.Web
|
||||||
|
|
||||||
if (_server == null)
|
if (_server == null)
|
||||||
{
|
{
|
||||||
this.LogWarning("Server is null, unable to start");
|
Debug.Console(DebugInfo, this, "Server is null, unable to start");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsRegistered)
|
if (IsRegistered)
|
||||||
{
|
{
|
||||||
this.LogWarning("Server has already been started");
|
Debug.Console(DebugInfo, this, "Server has already been started");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
IsRegistered = _server.Register();
|
IsRegistered = _server.Register();
|
||||||
|
|
||||||
this.LogInformation("Starting server, registration {registrationResult}", IsRegistered ? "was successful" : "failed");
|
Debug.Console(DebugInfo, this, "Starting server, registration {0}", IsRegistered ? "was successful" : "failed");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
this.LogError("Start Exception Message: {message}", ex.Message);
|
Debug.Console(DebugInfo, this, "Start Exception Message: {0}", ex.Message);
|
||||||
this.LogDebug(ex, "Start Exception StackTrace");
|
Debug.Console(DebugVerbose, this, "Start Exception StackTrace: {0}", ex.StackTrace);
|
||||||
|
if (ex.InnerException != null)
|
||||||
|
Debug.Console(DebugVerbose, this, "Start Exception InnerException: {0}", ex.InnerException);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -233,9 +225,9 @@ namespace PepperDash.Core.Web
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stop method
|
/// Stop method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Stop()
|
public void Stop()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -244,21 +236,23 @@ namespace PepperDash.Core.Web
|
||||||
|
|
||||||
if (_server == null)
|
if (_server == null)
|
||||||
{
|
{
|
||||||
this.LogWarning("Server is null or has already been stopped");
|
Debug.Console(DebugInfo, this, "Server is null or has already been stopped");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
IsRegistered = _server.Unregister() == false;
|
IsRegistered = _server.Unregister() == false;
|
||||||
|
|
||||||
this.LogInformation("Stopping server, unregistration {unregistrationResult}", IsRegistered ? "failed" : "was successful");
|
Debug.Console(DebugInfo, this, "Stopping server, unregistration {0}", IsRegistered ? "failed" : "was successful");
|
||||||
|
|
||||||
_server.Dispose();
|
_server.Dispose();
|
||||||
_server = null;
|
_server = null;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
this.LogError("Server Stop Exception Message: {message}", ex.Message);
|
Debug.Console(DebugInfo, this, "Server Stop Exception Message: {0}", ex.Message);
|
||||||
this.LogDebug(ex, "Server Stop Exception StackTrace");
|
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);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -279,12 +273,14 @@ namespace PepperDash.Core.Web
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var j = JsonConvert.SerializeObject(args.Context, Formatting.Indented);
|
var j = JsonConvert.SerializeObject(args.Context, Formatting.Indented);
|
||||||
this.LogVerbose("RecieveRequestEventHandler Context:\x0d\x0a{0}", j);
|
Debug.Console(DebugVerbose, this, "RecieveRequestEventHandler Context:\x0d\x0a{0}", j);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
this.LogError("ReceivedRequestEventHandler Exception Message: {message}", ex.Message);
|
Debug.Console(DebugInfo, this, "ReceivedRequestEventHandler Exception Message: {0}", ex.Message);
|
||||||
this.LogDebug(ex, "ReceivedRequestEventHandler Exception StackTrace: {stackTrace}", ex.StackTrace);
|
Debug.Console(DebugVerbose, this, "ReceivedRequestEventHandler Exception StackTrace: {0}", ex.StackTrace);
|
||||||
|
if (ex.InnerException != null)
|
||||||
|
Debug.Console(DebugVerbose, this, "ReceivedRequestEventHandler Exception InnerException: {0}", ex.InnerException);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using Crestron.SimplSharp; // For Basic SIMPL# Classes
|
using Crestron.SimplSharp; // For Basic SIMPL# Classes
|
||||||
using Crestron.SimplSharp.CrestronIO;
|
using Crestron.SimplSharp.CrestronIO;
|
||||||
using Crestron.SimplSharp.Net.Http;
|
using Crestron.SimplSharp.Net.Http;
|
||||||
using Crestron.SimplSharp.Net.Https;
|
using Crestron.SimplSharp.Net.Https;
|
||||||
|
|
@ -10,25 +10,24 @@ using PepperDash.Core.JsonToSimpl;
|
||||||
|
|
||||||
namespace PepperDash.Core.WebApi.Presets
|
namespace PepperDash.Core.WebApi.Presets
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Passcode client for the WebApi
|
/// Passcode client for the WebApi
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("WebApiPasscodeClient is no longer supported and will be removed in a future release.")]
|
|
||||||
public class WebApiPasscodeClient : IKeyed
|
public class WebApiPasscodeClient : IKeyed
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Notifies when user received
|
/// Notifies when user received
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event EventHandler<UserReceivedEventArgs> UserReceived;
|
public event EventHandler<UserReceivedEventArgs> UserReceived;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Notifies when Preset received
|
/// Notifies when Preset received
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event EventHandler<PresetReceivedEventArgs> PresetReceived;
|
public event EventHandler<PresetReceivedEventArgs> PresetReceived;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the Key
|
/// Gets or sets the Key
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Key { get; private set; }
|
public string Key { get; private set; }
|
||||||
|
|
||||||
//string JsonMasterKey;
|
//string JsonMasterKey;
|
||||||
|
|
@ -55,13 +54,13 @@ namespace PepperDash.Core.WebApi.Presets
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes the instance
|
/// Initializes the instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key"></param>
|
/// <param name="key"></param>
|
||||||
/// <param name="jsonMasterKey"></param>
|
/// <param name="jsonMasterKey"></param>
|
||||||
/// <param name="urlBase"></param>
|
/// <param name="urlBase"></param>
|
||||||
/// <param name="defaultPresetJsonFilePath"></param>
|
/// <param name="defaultPresetJsonFilePath"></param>
|
||||||
public void Initialize(string key, string jsonMasterKey, string urlBase, string defaultPresetJsonFilePath)
|
public void Initialize(string key, string jsonMasterKey, string urlBase, string defaultPresetJsonFilePath)
|
||||||
{
|
{
|
||||||
Key = key;
|
Key = key;
|
||||||
|
|
@ -74,44 +73,44 @@ namespace PepperDash.Core.WebApi.Presets
|
||||||
J2SMaster.Initialize(jsonMasterKey);
|
J2SMaster.Initialize(jsonMasterKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the user for a passcode
|
/// Gets the user for a passcode
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="passcode"></param>
|
/// <param name="passcode"></param>
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// GetUserForPasscode method
|
/// GetUserForPasscode method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void GetUserForPasscode(string passcode)
|
public void GetUserForPasscode(string passcode)
|
||||||
{
|
{
|
||||||
// Bullshit duplicate code here... These two cases should be the same
|
// Bullshit duplicate code here... These two cases should be the same
|
||||||
// except for https/http and the certificate ignores
|
// except for https/http and the certificate ignores
|
||||||
if (!UrlBase.StartsWith("https"))
|
if (!UrlBase.StartsWith("https"))
|
||||||
return;
|
return;
|
||||||
var req = new HttpsClientRequest();
|
var req = new HttpsClientRequest();
|
||||||
req.Url = new UrlParser(UrlBase + "/api/users/dopin");
|
req.Url = new UrlParser(UrlBase + "/api/users/dopin");
|
||||||
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
|
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
|
||||||
req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json"));
|
req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json"));
|
||||||
req.Header.AddHeader(new HttpsHeader("Accept", "application/json"));
|
req.Header.AddHeader(new HttpsHeader("Accept", "application/json"));
|
||||||
var jo = new JObject();
|
var jo = new JObject();
|
||||||
jo.Add("pin", passcode);
|
jo.Add("pin", passcode);
|
||||||
req.ContentString = jo.ToString();
|
req.ContentString = jo.ToString();
|
||||||
|
|
||||||
var client = new HttpsClient();
|
var client = new HttpsClient();
|
||||||
client.HostVerification = false;
|
client.HostVerification = false;
|
||||||
client.PeerVerification = false;
|
client.PeerVerification = false;
|
||||||
var resp = client.Dispatch(req);
|
var resp = client.Dispatch(req);
|
||||||
var handler = UserReceived;
|
var handler = UserReceived;
|
||||||
if (resp.Code == 200)
|
if (resp.Code == 200)
|
||||||
{
|
{
|
||||||
//CrestronConsole.PrintLine("Received: {0}", resp.ContentString);
|
//CrestronConsole.PrintLine("Received: {0}", resp.ContentString);
|
||||||
var user = JsonConvert.DeserializeObject<User>(resp.ContentString);
|
var user = JsonConvert.DeserializeObject<User>(resp.ContentString);
|
||||||
CurrentUser = user;
|
CurrentUser = user;
|
||||||
if (handler != null)
|
if (handler != null)
|
||||||
handler(this, new UserReceivedEventArgs(user, true));
|
UserReceived(this, new UserReceivedEventArgs(user, true));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
if (handler != null)
|
if (handler != null)
|
||||||
handler(this, new UserReceivedEventArgs(null, false));
|
UserReceived(this, new UserReceivedEventArgs(null, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -119,9 +118,9 @@ namespace PepperDash.Core.WebApi.Presets
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="roomTypeId"></param>
|
/// <param name="roomTypeId"></param>
|
||||||
/// <param name="presetNumber"></param>
|
/// <param name="presetNumber"></param>
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// GetPresetForThisUser method
|
/// GetPresetForThisUser method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void GetPresetForThisUser(int roomTypeId, int presetNumber)
|
public void GetPresetForThisUser(int roomTypeId, int presetNumber)
|
||||||
{
|
{
|
||||||
if (CurrentUser == null)
|
if (CurrentUser == null)
|
||||||
|
|
@ -137,57 +136,57 @@ namespace PepperDash.Core.WebApi.Presets
|
||||||
PresetNumber = presetNumber
|
PresetNumber = presetNumber
|
||||||
};
|
};
|
||||||
|
|
||||||
var handler = PresetReceived;
|
var handler = PresetReceived;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!UrlBase.StartsWith("https"))
|
if (!UrlBase.StartsWith("https"))
|
||||||
return;
|
return;
|
||||||
var req = new HttpsClientRequest();
|
var req = new HttpsClientRequest();
|
||||||
req.Url = new UrlParser(UrlBase + "/api/presets/userandroom");
|
req.Url = new UrlParser(UrlBase + "/api/presets/userandroom");
|
||||||
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
|
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
|
||||||
req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json"));
|
req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json"));
|
||||||
req.Header.AddHeader(new HttpsHeader("Accept", "application/json"));
|
req.Header.AddHeader(new HttpsHeader("Accept", "application/json"));
|
||||||
req.ContentString = JsonConvert.SerializeObject(msg);
|
req.ContentString = JsonConvert.SerializeObject(msg);
|
||||||
|
|
||||||
var client = new HttpsClient();
|
var client = new HttpsClient();
|
||||||
client.HostVerification = false;
|
client.HostVerification = false;
|
||||||
client.PeerVerification = false;
|
client.PeerVerification = false;
|
||||||
|
|
||||||
// ask for the preset
|
// ask for the preset
|
||||||
var resp = client.Dispatch(req);
|
var resp = client.Dispatch(req);
|
||||||
if (resp.Code == 200) // got it
|
if (resp.Code == 200) // got it
|
||||||
{
|
{
|
||||||
//Debug.Console(1, this, "Received: {0}", resp.ContentString);
|
//Debug.Console(1, this, "Received: {0}", resp.ContentString);
|
||||||
var preset = JsonConvert.DeserializeObject<Preset>(resp.ContentString);
|
var preset = JsonConvert.DeserializeObject<Preset>(resp.ContentString);
|
||||||
CurrentPreset = preset;
|
CurrentPreset = preset;
|
||||||
|
|
||||||
//if there's no preset data, load the template
|
//if there's no preset data, load the template
|
||||||
if (preset.Data == null || preset.Data.Trim() == string.Empty || JObject.Parse(preset.Data).Count == 0)
|
if (preset.Data == null || preset.Data.Trim() == string.Empty || JObject.Parse(preset.Data).Count == 0)
|
||||||
{
|
{
|
||||||
//Debug.Console(1, this, "Loaded preset has no data. Loading default template.");
|
//Debug.Console(1, this, "Loaded preset has no data. Loading default template.");
|
||||||
LoadDefaultPresetData();
|
LoadDefaultPresetData();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
J2SMaster.LoadWithJson(preset.Data);
|
J2SMaster.LoadWithJson(preset.Data);
|
||||||
if (handler != null)
|
if (handler != null)
|
||||||
handler(this, new PresetReceivedEventArgs(preset, true));
|
PresetReceived(this, new PresetReceivedEventArgs(preset, true));
|
||||||
}
|
}
|
||||||
else // no existing preset
|
else // no existing preset
|
||||||
{
|
{
|
||||||
CurrentPreset = new Preset();
|
CurrentPreset = new Preset();
|
||||||
LoadDefaultPresetData();
|
LoadDefaultPresetData();
|
||||||
if (handler != null)
|
if (handler != null)
|
||||||
handler(this, new PresetReceivedEventArgs(null, false));
|
PresetReceived(this, new PresetReceivedEventArgs(null, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (HttpException e)
|
catch (HttpException e)
|
||||||
{
|
{
|
||||||
var resp = e.Response;
|
var resp = e.Response;
|
||||||
Debug.Console(1, this, "No preset received (code {0}). Loading default template", resp.Code);
|
Debug.Console(1, this, "No preset received (code {0}). Loading default template", resp.Code);
|
||||||
LoadDefaultPresetData();
|
LoadDefaultPresetData();
|
||||||
if (handler != null)
|
if (handler != null)
|
||||||
PresetReceived(this, new PresetReceivedEventArgs(null, false));
|
PresetReceived(this, new PresetReceivedEventArgs(null, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,14 +218,14 @@ namespace PepperDash.Core.WebApi.Presets
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="roomTypeId"></param>
|
/// <param name="roomTypeId"></param>
|
||||||
/// <param name="presetNumber"></param>
|
/// <param name="presetNumber"></param>
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// SavePresetForThisUser method
|
/// SavePresetForThisUser method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SavePresetForThisUser(int roomTypeId, int presetNumber)
|
public void SavePresetForThisUser(int roomTypeId, int presetNumber)
|
||||||
{
|
{
|
||||||
if (CurrentPreset == null)
|
if (CurrentPreset == null)
|
||||||
LoadDefaultPresetData();
|
LoadDefaultPresetData();
|
||||||
//return;
|
//return;
|
||||||
|
|
||||||
//// A new preset needs to have its numbers set
|
//// A new preset needs to have its numbers set
|
||||||
//if (CurrentPreset.IsNewPreset)
|
//if (CurrentPreset.IsNewPreset)
|
||||||
|
|
@ -246,8 +245,8 @@ namespace PepperDash.Core.WebApi.Presets
|
||||||
{
|
{
|
||||||
CurrentPreset.Data = json;
|
CurrentPreset.Data = json;
|
||||||
|
|
||||||
if (!UrlBase.StartsWith("https"))
|
if (!UrlBase.StartsWith("https"))
|
||||||
return;
|
return;
|
||||||
var req = new HttpsClientRequest();
|
var req = new HttpsClientRequest();
|
||||||
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
|
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
|
||||||
req.Url = new UrlParser(string.Format("{0}/api/presets/addorchange", UrlBase));
|
req.Url = new UrlParser(string.Format("{0}/api/presets/addorchange", UrlBase));
|
||||||
|
|
@ -256,8 +255,8 @@ namespace PepperDash.Core.WebApi.Presets
|
||||||
req.ContentString = JsonConvert.SerializeObject(CurrentPreset);
|
req.ContentString = JsonConvert.SerializeObject(CurrentPreset);
|
||||||
|
|
||||||
var client = new HttpsClient();
|
var client = new HttpsClient();
|
||||||
client.HostVerification = false;
|
client.HostVerification = false;
|
||||||
client.PeerVerification = false;
|
client.PeerVerification = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var resp = client.Dispatch(req);
|
var resp = client.Dispatch(req);
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,6 @@ namespace PepperDash.Essentials.Core
|
||||||
|
|
||||||
ICec Port;
|
ICec Port;
|
||||||
|
|
||||||
bool _cecSubscribed;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructor
|
/// Constructor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -56,7 +54,7 @@ namespace PepperDash.Essentials.Core
|
||||||
{
|
{
|
||||||
Port = postActivationFunc(config);
|
Port = postActivationFunc(config);
|
||||||
|
|
||||||
TryEnsureCecSubscription();
|
Port.StreamCec.CecChange += StreamCec_CecChange;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,29 +68,7 @@ namespace PepperDash.Essentials.Core
|
||||||
{
|
{
|
||||||
Port = port;
|
Port = port;
|
||||||
|
|
||||||
TryEnsureCecSubscription();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subscribes to the CEC change event once <see cref="ICec.StreamCec"/> is available.
|
|
||||||
/// Safe to call repeatedly; the subscription is only wired a single time.
|
|
||||||
/// If StreamCec is null during construction, this is retried when send methods invoke
|
|
||||||
/// this method later.
|
|
||||||
/// </summary>
|
|
||||||
void TryEnsureCecSubscription()
|
|
||||||
{
|
|
||||||
if (_cecSubscribed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (Port?.StreamCec == null)
|
|
||||||
{
|
|
||||||
Debug.LogMessage(LogEventLevel.Warning, this, "StreamCec is not available; CEC feedback is deferred until the device is ready");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Port.StreamCec.CecChange += new CecChangeEventHandler(StreamCec_CecChange);
|
Port.StreamCec.CecChange += new CecChangeEventHandler(StreamCec_CecChange);
|
||||||
_cecSubscribed = true;
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, this, "Subscribed to CEC feedback");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void StreamCec_CecChange(Cec cecDevice, CecEventArgs args)
|
void StreamCec_CecChange(Cec cecDevice, CecEventArgs args)
|
||||||
|
|
@ -128,9 +104,8 @@ namespace PepperDash.Essentials.Core
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SendText(string text)
|
public void SendText(string text)
|
||||||
{
|
{
|
||||||
if (Port?.StreamCec == null)
|
if (Port == null)
|
||||||
return;
|
return;
|
||||||
TryEnsureCecSubscription();
|
|
||||||
this.PrintSentText(text);
|
this.PrintSentText(text);
|
||||||
Port.StreamCec.Send.StringValue = text;
|
Port.StreamCec.Send.StringValue = text;
|
||||||
}
|
}
|
||||||
|
|
@ -140,9 +115,8 @@ namespace PepperDash.Essentials.Core
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SendBytes(byte[] bytes)
|
public void SendBytes(byte[] bytes)
|
||||||
{
|
{
|
||||||
if (Port?.StreamCec == null)
|
if (Port == null)
|
||||||
return;
|
return;
|
||||||
TryEnsureCecSubscription();
|
|
||||||
var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
|
var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
|
||||||
this.PrintSentBytes(bytes);
|
this.PrintSentBytes(bytes);
|
||||||
Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
|
Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
|
||||||
|
|
|
||||||
|
|
@ -96,17 +96,6 @@ namespace PepperDash.Essentials.Core
|
||||||
comm = udp;
|
comm = udp;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case eControlMethod.UdpClient:
|
|
||||||
{
|
|
||||||
var udpClient = new GenericUdpClient(deviceConfig.Key + "-udpClient", c.Address, c.Port, c.BufferSize)
|
|
||||||
{
|
|
||||||
AutoReconnect = c.AutoReconnect
|
|
||||||
};
|
|
||||||
if (udpClient.AutoReconnect)
|
|
||||||
udpClient.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs;
|
|
||||||
comm = udpClient;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case eControlMethod.Telnet:
|
case eControlMethod.Telnet:
|
||||||
break;
|
break;
|
||||||
case eControlMethod.SecureTcpIp:
|
case eControlMethod.SecureTcpIp:
|
||||||
|
|
|
||||||
|
|
@ -135,14 +135,12 @@ namespace PepperDash.Essentials.Core.Config
|
||||||
{
|
{
|
||||||
var parsedConfig = JObject.Parse(fs.ReadToEnd());
|
var parsedConfig = JObject.Parse(fs.ReadToEnd());
|
||||||
|
|
||||||
// A config is v1 if it has separate "system" and "template" nodes that
|
// Check if it's a v2 config (check for "version" node)
|
||||||
// need to be merged. A v2 config is already merged by the Portal API and
|
// this means it's already merged by the Portal API
|
||||||
// will not have "system"/"template" nodes. This is independent of whether
|
// from the v2 config tool
|
||||||
// a "versions" node is present, which only carries version metadata and
|
var isV2Config = parsedConfig["versions"] != null;
|
||||||
// can appear on either a v1 or v2 config.
|
|
||||||
var isV1Config = parsedConfig["system"] != null && parsedConfig["template"] != null;
|
if (isV2Config)
|
||||||
|
|
||||||
if (!isV1Config)
|
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Config file is a v2 format, no merge necessary.");
|
Debug.LogMessage(LogEventLevel.Information, "Config file is a v2 format, no merge necessary.");
|
||||||
ConfigObject = parsedConfig.ToObject<EssentialsConfig>();
|
ConfigObject = parsedConfig.ToObject<EssentialsConfig>();
|
||||||
|
|
@ -150,8 +148,6 @@ namespace PepperDash.Essentials.Core.Config
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Config file is a v1 format, merging system and template.");
|
|
||||||
|
|
||||||
// Extract SystemUrl and TemplateUrl into final config output
|
// Extract SystemUrl and TemplateUrl into final config output
|
||||||
ConfigObject = PortalConfigReader.MergeConfigs(parsedConfig).ToObject<EssentialsConfig>();
|
ConfigObject = PortalConfigReader.MergeConfigs(parsedConfig).ToObject<EssentialsConfig>();
|
||||||
|
|
||||||
|
|
@ -164,13 +160,6 @@ namespace PepperDash.Essentials.Core.Config
|
||||||
{
|
{
|
||||||
ConfigObject.TemplateUrl = parsedConfig["template_url"].Value<string>();
|
ConfigObject.TemplateUrl = parsedConfig["template_url"].Value<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// MergeConfigs does not carry the "versions" node forward, so it must be
|
|
||||||
// applied separately to ensure it's preserved in the merged config.
|
|
||||||
if (parsedConfig["versions"] != null)
|
|
||||||
{
|
|
||||||
ConfigObject.Versions = parsedConfig["versions"].ToObject<VersionData>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded Merged Config");
|
Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded Merged Config");
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ namespace PepperDash.Essentials.Core.Config
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ConfigUpdater class
|
/// ConfigUpdater class
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("ConfigUpdater is no longer supported and will be removed in a future release.")]
|
|
||||||
public static class ConfigUpdater
|
public static class ConfigUpdater
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -82,7 +81,7 @@ namespace PepperDash.Essentials.Core.Config
|
||||||
{
|
{
|
||||||
var handler = ConfigStatusChanged;
|
var handler = ConfigStatusChanged;
|
||||||
|
|
||||||
if (handler != null)
|
if(handler != null)
|
||||||
{
|
{
|
||||||
handler(typeof(ConfigUpdater), new ConfigStatusEventArgs(status));
|
handler(typeof(ConfigUpdater), new ConfigStatusEventArgs(status));
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +89,7 @@ namespace PepperDash.Essentials.Core.Config
|
||||||
|
|
||||||
static void WriteConfigToFile(string configData)
|
static void WriteConfigToFile(string configData)
|
||||||
{
|
{
|
||||||
var filePath = Global.FilePathPrefix + "configurationFile-updated.json";
|
var filePath = Global.FilePathPrefix+ "configurationFile-updated.json";
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -105,7 +104,7 @@ namespace PepperDash.Essentials.Core.Config
|
||||||
Debug.LogMessage(LogEventLevel.Debug, "Error parsing new config: {0}", e);
|
Debug.LogMessage(LogEventLevel.Debug, "Error parsing new config: {0}", e);
|
||||||
|
|
||||||
OnStatusUpdate(eUpdateStatus.UpdateFailed);
|
OnStatusUpdate(eUpdateStatus.UpdateFailed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -150,11 +149,11 @@ namespace PepperDash.Essentials.Core.Config
|
||||||
// Directory exists, first clear any contents
|
// Directory exists, first clear any contents
|
||||||
var archivedConfigFiles = ConfigReader.GetConfigFiles(archiveDirectoryPath + Global.DirectorySeparator + Global.ConfigFileName + ".bak");
|
var archivedConfigFiles = ConfigReader.GetConfigFiles(archiveDirectoryPath + Global.DirectorySeparator + Global.ConfigFileName + ".bak");
|
||||||
|
|
||||||
if (archivedConfigFiles != null && archivedConfigFiles.Length > 0)
|
if(archivedConfigFiles != null || archivedConfigFiles.Length > 0)
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, "{0} Existing files found in archive folder. Deleting.", archivedConfigFiles.Length);
|
Debug.LogMessage(LogEventLevel.Information, "{0} Existing files found in archive folder. Deleting.", archivedConfigFiles.Length);
|
||||||
|
|
||||||
for (int i = 0; i < archivedConfigFiles.Length; i++)
|
for (int i = 0; i < archivedConfigFiles.Length; i++ )
|
||||||
{
|
{
|
||||||
var file = archivedConfigFiles[i];
|
var file = archivedConfigFiles[i];
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Deleting archived file: '{0}'", file.FullName);
|
Debug.LogMessage(LogEventLevel.Information, "Deleting archived file: '{0}'", file.FullName);
|
||||||
|
|
@ -171,9 +170,9 @@ namespace PepperDash.Essentials.Core.Config
|
||||||
|
|
||||||
// Moves the file and appends the .bak extension
|
// Moves the file and appends the .bak extension
|
||||||
var fileDest = archiveDirectoryPath + "/" + file.Name + ".bak";
|
var fileDest = archiveDirectoryPath + "/" + file.Name + ".bak";
|
||||||
if (!File.Exists(fileDest))
|
if(!File.Exists(fileDest))
|
||||||
{
|
{
|
||||||
file.MoveTo(fileDest);
|
file.MoveTo(fileDest);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Cannot move file to archive folder. Existing file already exists with same name: '{0}'", fileDest);
|
Debug.LogMessage(LogEventLevel.Information, "Cannot move file to archive folder. Existing file already exists with same name: '{0}'", fileDest);
|
||||||
|
|
@ -208,15 +207,15 @@ namespace PepperDash.Essentials.Core.Config
|
||||||
|
|
||||||
CrestronConsole.SendControlSystemCommand(string.Format("progreset -p:{0}", InitialParametersClass.ApplicationNumber), ref response);
|
CrestronConsole.SendControlSystemCommand(string.Format("progreset -p:{0}", InitialParametersClass.ApplicationNumber), ref response);
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Debug, "Console Response: {0}", response);
|
Debug.LogMessage(LogEventLevel.Debug, "Console Response: {0}", response);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enumeration of eUpdateStatus values
|
/// Enumeration of eUpdateStatus values
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum eUpdateStatus
|
public enum eUpdateStatus
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// UpdateStarted status
|
/// UpdateStarted status
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,6 @@ namespace PepperDash.Essentials.Core.Config
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the Versions
|
/// Gets or sets the Versions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("versions")]
|
|
||||||
public VersionData Versions { get; set; }
|
public VersionData Versions { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -135,25 +134,12 @@ namespace PepperDash.Essentials.Core.Config
|
||||||
[JsonProperty("packages")]
|
[JsonProperty("packages")]
|
||||||
public List<NugetVersion> Packages { get; set; }
|
public List<NugetVersion> Packages { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the touchpanel wrapper app version
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("touchpanelWrapperApp")]
|
|
||||||
public NugetVersion TouchpanelWrapperApp { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the list of user interface packages
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("userInterfaces")]
|
|
||||||
public List<NugetVersion> UserInterfaces { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="VersionData"/> class.
|
/// Initializes a new instance of the <see cref="VersionData"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public VersionData()
|
public VersionData()
|
||||||
{
|
{
|
||||||
Packages = new List<NugetVersion>();
|
Packages = new List<NugetVersion>();
|
||||||
UserInterfaces = new List<NugetVersion>();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,20 +157,8 @@ namespace PepperDash.Essentials.Core.Config
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the PackageId
|
/// Gets or sets the PackageId
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("packageId", NullValueHandling = NullValueHandling.Ignore)]
|
[JsonProperty("packageId")]
|
||||||
public string PackageId { get; set; }
|
public string PackageId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the RepoUrl
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("repoUrl", NullValueHandling = NullValueHandling.Ignore)]
|
|
||||||
public string RepoUrl { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the human-readable name
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
|
|
||||||
public string Name { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Defines the contract for a wireless presentation endpoint that reports whether a wireless
|
|
||||||
/// sharing session is currently active. Implemented by platforms such as Crestron AirMedia,
|
|
||||||
/// Mersive Solstice, Barco ClickShare, Miracast/Teams receivers, etc. Allows consumers (e.g.
|
|
||||||
/// room plugins) to react to wireless sharing activity without taking a dependency on any
|
|
||||||
/// concrete device implementation.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This refers specifically to wireless screen/device mirroring, as distinct from in-call
|
|
||||||
/// content sharing on a video conference.
|
|
||||||
/// </remarks>
|
|
||||||
public interface IHasWirelessSharing
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Reports whether a wireless sharing session is currently active (content is being presented).
|
|
||||||
/// </summary>
|
|
||||||
BoolFeedback IsSharingFeedback { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Raised when wireless sharing starts or stops. The event args carry the new sharing state.
|
|
||||||
/// </summary>
|
|
||||||
event EventHandler<WirelessSharingEventArgs> SharingChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Event arguments describing a change in wireless sharing state.
|
|
||||||
/// </summary>
|
|
||||||
public class WirelessSharingEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// True if a wireless sharing session is active (content is being presented), false otherwise.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsSharing { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new <see cref="WirelessSharingEventArgs"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="isSharing">True if a wireless sharing session is active, false otherwise.</param>
|
|
||||||
public WirelessSharingEventArgs(bool isSharing)
|
|
||||||
{
|
|
||||||
IsSharing = isSharing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
using System;
|
|
||||||
using PepperDash.Core;
|
using PepperDash.Core;
|
||||||
|
|
||||||
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
|
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Obsolete: messengers are subscription based by default; use IMobileControlMessenger instead.
|
/// Defines the contract for IMobileControlMessenger
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("This interface is obsolete and will be removed in a future version. All messengers are now subscription based.")]
|
|
||||||
public interface IMobileControlMessengerWithSubscriptions : IMobileControlMessenger
|
public interface IMobileControlMessengerWithSubscriptions : IMobileControlMessenger
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Interface for network switches that support VLAN assignment on individual ports.
|
|
||||||
/// </summary>
|
|
||||||
public interface INetworkSwitchVlanManager
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the current access VLAN ID configured on the port.
|
|
||||||
/// Return -1 when the value is unavailable (e.g. the switch has not been polled yet
|
|
||||||
/// or the implementation does not support VLAN queries).
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="port">Switch port identifier</param>
|
|
||||||
/// <returns>VLAN ID or -1 when unavailable</returns>
|
|
||||||
int GetPortCurrentVlan(string port);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Changes the access VLAN of a single switch port.
|
|
||||||
/// The implementation is responsible for entering/exiting privileged/config mode.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="port">Switch port identifier (e.g. "1/0/3" for Netgear, "gi1/0/3" for Cisco)</param>
|
|
||||||
/// <param name="vlanId">Target VLAN ID (1-4093)</param>
|
|
||||||
void SetPortVlan(string port, uint vlanId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Interface for network switches that support Power over Ethernet (PoE) control on individual ports.
|
|
||||||
/// </summary>
|
|
||||||
public interface INetworkSwitchPoeManager
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Enables or disables PoE power delivery on a single switch port.
|
|
||||||
/// The implementation is responsible for entering/exiting privileged/config mode.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="port">Switch port identifier</param>
|
|
||||||
/// <param name="enabled">True to enable PoE; false to disable PoE</param>
|
|
||||||
void SetPortPoeState(string port, bool enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Standardized interface for network switch devices that support per-port PoE control
|
|
||||||
/// and VLAN assignment.
|
|
||||||
/// </summary>
|
|
||||||
public interface INetworkSwitchPoeVlanManager : INetworkSwitchVlanManager, INetworkSwitchPoeManager
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Event that is raised when the state of a switch port changes, such as a VLAN change or PoE state change.
|
|
||||||
/// </summary>
|
|
||||||
event EventHandler<NetworkSwitchPortEventArgs> PortStateChanged;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Event arguments for port state changes on a network switch, such as VLAN changes or PoE state changes.
|
|
||||||
/// </summary>
|
|
||||||
public class NetworkSwitchPortEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The identifier of the port that changed state (e.g. "1/0/3" for Netgear, "gi1/0/3" for Cisco).
|
|
||||||
/// </summary>
|
|
||||||
public string Port { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The type of event that occurred on the port (e.g. VLAN change, PoE enabled/disabled).
|
|
||||||
/// </summary>
|
|
||||||
public NetworkSwitchPortEventType EventType { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Constructor for NetworkSwitchPortEventArgs
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="port">The identifier of the port that changed state</param>
|
|
||||||
/// <param name="eventType">The type of event that occurred on the port</param>
|
|
||||||
public NetworkSwitchPortEventArgs(string port, NetworkSwitchPortEventType eventType)
|
|
||||||
{
|
|
||||||
Port = port;
|
|
||||||
EventType = eventType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Enumeration of network switch port state change event types (e.g. VLAN changes or PoE state changes).
|
|
||||||
/// </summary>
|
|
||||||
public enum NetworkSwitchPortEventType
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates that the type of event is unknown or cannot be determined.
|
|
||||||
/// </summary>
|
|
||||||
Unknown,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates that a VLAN change is in progress on the port, either through a call to SetPortVlan or an external change detected by polling.
|
|
||||||
/// </summary>
|
|
||||||
VlanChangeInProgress,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates that the access VLAN on a port has changed, either through a successful call to SetPortVlan or an external change detected by polling.
|
|
||||||
/// </summary>
|
|
||||||
VlanChanged,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates that PoE is being disabled on the port, either through a call to SetPortPoeState or an external change detected by polling.
|
|
||||||
/// </summary>
|
|
||||||
PoeDisableInProgress,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates that PoE has been disabled on the port, either through a successful call to SetPortPoeState or an external change detected by polling.
|
|
||||||
/// </summary>
|
|
||||||
PoEDisabled,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates that PoE is being enabled on the port, either through a call to SetPortPoeState or an external change detected by polling.
|
|
||||||
/// </summary>
|
|
||||||
PoeEnableInProgress,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates that PoE has been enabled on the port, either through a successful call to SetPortPoeState or an external change detected by polling.
|
|
||||||
/// </summary>
|
|
||||||
PoEEnabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -7,7 +7,6 @@ using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace PepperDash.Essentials.Core
|
namespace PepperDash.Essentials.Core
|
||||||
|
|
@ -177,20 +176,7 @@ namespace PepperDash.Essentials.Core
|
||||||
{
|
{
|
||||||
if (!conversionType.IsEnum)
|
if (!conversionType.IsEnum)
|
||||||
{
|
{
|
||||||
if (conversionType == typeof(byte[]) && value is string byteString)
|
return Convert.ChangeType(value, conversionType, System.Globalization.CultureInfo.InvariantCulture);
|
||||||
{
|
|
||||||
var unescaped = UnescapeString(byteString);
|
|
||||||
return System.Text.Encoding.GetEncoding(28591).GetBytes(unescaped);
|
|
||||||
}
|
|
||||||
|
|
||||||
var converted = Convert.ChangeType(value, conversionType, System.Globalization.CultureInfo.InvariantCulture);
|
|
||||||
|
|
||||||
if (conversionType == typeof(string) && converted is string s)
|
|
||||||
{
|
|
||||||
return UnescapeString(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
return converted;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var stringValue = Convert.ToString(value);
|
var stringValue = Convert.ToString(value);
|
||||||
|
|
@ -203,32 +189,6 @@ namespace PepperDash.Essentials.Core
|
||||||
return Enum.Parse(conversionType, stringValue, true);
|
return Enum.Parse(conversionType, stringValue, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Processes escape sequences in a string, converting sequences like \r, \n, \t, \xHH
|
|
||||||
/// to their corresponding non-printable ASCII characters.
|
|
||||||
/// </summary>
|
|
||||||
private static string UnescapeString(string input)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(input))
|
|
||||||
return input;
|
|
||||||
|
|
||||||
return Regex.Replace(input, @"\\(r|n|t|\\|x[0-9A-Fa-f]{2})", match =>
|
|
||||||
{
|
|
||||||
var seq = match.Groups[1].Value;
|
|
||||||
switch (seq)
|
|
||||||
{
|
|
||||||
case "r": return "\r";
|
|
||||||
case "n": return "\n";
|
|
||||||
case "t": return "\t";
|
|
||||||
case "\\": return "\\";
|
|
||||||
default:
|
|
||||||
// \xHH hex escape
|
|
||||||
var hex = seq.Substring(1);
|
|
||||||
return ((char)Convert.ToInt32(hex, 16)).ToString();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the properties on a device
|
/// Gets the properties on a device
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -436,14 +436,14 @@ namespace PepperDash.Essentials.Core
|
||||||
CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Input Ports:{2}", s, inputPorts.Count, CrestronEnvironment.NewLine);
|
CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Input Ports:{2}", s, inputPorts.Count, CrestronEnvironment.NewLine);
|
||||||
foreach (var routingInputPort in inputPorts)
|
foreach (var routingInputPort in inputPorts)
|
||||||
{
|
{
|
||||||
CrestronConsole.ConsoleCommandResponse("key: {0} signalType: {1}{2}", routingInputPort.Key, routingInputPort.Type, CrestronEnvironment.NewLine);
|
CrestronConsole.ConsoleCommandResponse("{0}{1}", routingInputPort.Key, CrestronEnvironment.NewLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (outputPorts == null) return;
|
if (outputPorts == null) return;
|
||||||
CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Output Ports:{2}", s, outputPorts.Count, CrestronEnvironment.NewLine);
|
CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Output Ports:{2}", s, outputPorts.Count, CrestronEnvironment.NewLine);
|
||||||
foreach (var routingOutputPort in outputPorts)
|
foreach (var routingOutputPort in outputPorts)
|
||||||
{
|
{
|
||||||
CrestronConsole.ConsoleCommandResponse("key: {0} signalType: {1}{2}", routingOutputPort.Key, routingOutputPort.Type, CrestronEnvironment.NewLine);
|
CrestronConsole.ConsoleCommandResponse("{0}{1}", routingOutputPort.Key, CrestronEnvironment.NewLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
namespace PepperDash.Essentials.Core
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Defines the contract for IHasDspPresetSave
|
|
||||||
/// </summary>
|
|
||||||
public interface IHasDspPresetSave : IDspPresets
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Saves the preset by key
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="key">key of preset to save</param>
|
|
||||||
void SavePreset(string key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -7,10 +7,9 @@ using PepperDash.Core;
|
||||||
|
|
||||||
namespace PepperDash.Essentials.Core.Interfaces
|
namespace PepperDash.Essentials.Core.Interfaces
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines the contract for ILogStrings
|
/// Defines the contract for ILogStrings
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("ILogStrings is no longer supported and will be removed in a future release.")]
|
|
||||||
public interface ILogStrings : IKeyed
|
public interface ILogStrings : IKeyed
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,15 @@ using PepperDash.Core;
|
||||||
|
|
||||||
namespace PepperDash.Essentials.Core.Interfaces
|
namespace PepperDash.Essentials.Core.Interfaces
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines the contract for ILogStringsWithLevel
|
/// Defines the contract for ILogStringsWithLevel
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("ILogStringsWithLevel is no longer supported and will be removed in a future release.")]
|
|
||||||
public interface ILogStringsWithLevel : IKeyed
|
public interface ILogStringsWithLevel : IKeyed
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines a class that is capable of logging a string with an int level
|
/// Defines a class that is capable of logging a string with an int level
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void SendToLog(IKeyed device, Debug.ErrorLogLevel level, string logMessage);
|
void SendToLog(IKeyed device, Debug.ErrorLogLevel level,string logMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
<DocumentationFile>bin\$(Configuration)\PepperDash_Essentials_Core.xml</DocumentationFile>
|
<DocumentationFile>bin\$(Configuration)\PepperDash_Essentials_Core.xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.274" />
|
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="Crestron\CrestronGenericBaseDevice.cs.orig" />
|
<None Include="Crestron\CrestronGenericBaseDevice.cs.orig" />
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Crestron.SimplSharpPro.Keypads;
|
||||||
using PepperDash.Essentials.Core.Queues;
|
using PepperDash.Essentials.Core.Queues;
|
||||||
using PepperDash.Essentials.Core.Routing;
|
using PepperDash.Essentials.Core.Routing;
|
||||||
|
using Serilog.Events;
|
||||||
using Debug = PepperDash.Core.Debug;
|
using Debug = PepperDash.Core.Debug;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -16,20 +18,6 @@ namespace PepperDash.Essentials.Core
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Extensions
|
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>
|
/// <summary>
|
||||||
/// Stores pending route requests, keyed by the destination device key.
|
/// Stores pending route requests, keyed by the destination device key.
|
||||||
/// Used primarily to handle routing requests while a device is cooling down.
|
/// Used primarily to handle routing requests while a device is cooling down.
|
||||||
|
|
@ -41,78 +29,6 @@ namespace PepperDash.Essentials.Core
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly GenericQueue routeRequestQueue = new GenericQueue("routingQueue");
|
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>
|
|
||||||
/// 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>
|
/// <summary>
|
||||||
/// Gets any existing RouteDescriptor for a destination, clears it using ReleaseRoute
|
/// Gets any existing RouteDescriptor for a destination, clears it using ReleaseRoute
|
||||||
/// and then attempts a new Route and if sucessful, stores that RouteDescriptor
|
/// and then attempts a new Route and if sucessful, stores that RouteDescriptor
|
||||||
|
|
@ -122,7 +38,7 @@ namespace PepperDash.Essentials.Core
|
||||||
{
|
{
|
||||||
// Remove this line before committing!!!!!
|
// Remove this line before committing!!!!!
|
||||||
var frame = new StackFrame(1, true);
|
var frame = new StackFrame(1, true);
|
||||||
Debug.LogInformation("ReleaseAndMakeRoute Called from {method} with params {destinationKey}:{sourceKey}:{signalType}:{destinationPortKey}:{sourcePortKey}", frame.GetMethod().Name, destination.Key, source.Key, signalType.ToString(), destinationPortKey, sourcePortKey);
|
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);
|
||||||
|
|
||||||
var inputPort = string.IsNullOrEmpty(destinationPortKey) ? null : destination.InputPorts.FirstOrDefault(p => p.Key == destinationPortKey);
|
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);
|
var outputPort = string.IsNullOrEmpty(sourcePortKey) ? null : source.OutputPorts.FirstOrDefault(p => p.Key == sourcePortKey);
|
||||||
|
|
@ -180,13 +96,13 @@ namespace PepperDash.Essentials.Core
|
||||||
/// <param name="destinationKey">destination device key</param>
|
/// <param name="destinationKey">destination device key</param>
|
||||||
public static void RemoveRouteRequestForDestination(string destinationKey)
|
public static void RemoveRouteRequestForDestination(string destinationKey)
|
||||||
{
|
{
|
||||||
Debug.LogInformation("Removing route request for {destination}", destinationKey);
|
Debug.LogMessage(LogEventLevel.Information, "Removing route request for {destination}", null, destinationKey);
|
||||||
|
|
||||||
var result = RouteRequests.Remove(destinationKey);
|
var result = RouteRequests.Remove(destinationKey);
|
||||||
|
|
||||||
var messageTemplate = result ? "Route Request for {destination} removed" : "Route Request for {destination} not found";
|
var messageTemplate = result ? "Route Request for {destination} removed" : "Route Request for {destination} not found";
|
||||||
|
|
||||||
Debug.LogInformation(messageTemplate, destinationKey);
|
Debug.LogMessage(LogEventLevel.Information, messageTemplate, null, destinationKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -202,8 +118,8 @@ namespace PepperDash.Essentials.Core
|
||||||
if (!signalType.HasFlag(eRoutingSignalType.AudioVideo) &&
|
if (!signalType.HasFlag(eRoutingSignalType.AudioVideo) &&
|
||||||
!(signalType.HasFlag(eRoutingSignalType.Video) && signalType.HasFlag(eRoutingSignalType.SecondaryAudio)))
|
!(signalType.HasFlag(eRoutingSignalType.Video) && signalType.HasFlag(eRoutingSignalType.SecondaryAudio)))
|
||||||
{
|
{
|
||||||
var singleTypeRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, sourcePort, signalType);
|
var singleTypeRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, signalType);
|
||||||
Debug.LogDebug(destination, "Attempting to build source route from {sourceKey} of type {type}", source.Key, signalType);
|
Debug.LogMessage(LogEventLevel.Debug, "Attempting to build source route from {sourceKey} of type {type}", destination, source.Key, signalType);
|
||||||
|
|
||||||
if (!destination.GetRouteToSource(source, null, null, signalType, 0, singleTypeRouteDescriptor, destinationPort, sourcePort))
|
if (!destination.GetRouteToSource(source, null, null, signalType, 0, singleTypeRouteDescriptor, destinationPort, sourcePort))
|
||||||
singleTypeRouteDescriptor = null;
|
singleTypeRouteDescriptor = null;
|
||||||
|
|
@ -211,55 +127,54 @@ namespace PepperDash.Essentials.Core
|
||||||
var routes = singleTypeRouteDescriptor?.Routes ?? new List<RouteSwitchDescriptor>();
|
var routes = singleTypeRouteDescriptor?.Routes ?? new List<RouteSwitchDescriptor>();
|
||||||
foreach (var route in routes)
|
foreach (var route in routes)
|
||||||
{
|
{
|
||||||
Debug.LogVerbose(destination, "Route for device: {route}", route.ToString());
|
Debug.LogMessage(LogEventLevel.Verbose, "Route for device: {route}", destination, route.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return (singleTypeRouteDescriptor, null);
|
return (singleTypeRouteDescriptor, null);
|
||||||
}
|
}
|
||||||
// otherwise, audioVideo needs to be handled as two steps.
|
// otherwise, audioVideo needs to be handled as two steps.
|
||||||
|
|
||||||
Debug.LogDebug(destination, "Attempting to build source route from {destinationKey} to {sourceKey} of type {type}", destination.Key, source.Key, signalType);
|
Debug.LogMessage(LogEventLevel.Debug, "Attempting to build source route from {destinationKey} to {sourceKey} of type {type}", destination, source.Key, signalType);
|
||||||
|
|
||||||
RouteDescriptor audioRouteDescriptor;
|
RouteDescriptor audioRouteDescriptor;
|
||||||
|
|
||||||
if (signalType.HasFlag(eRoutingSignalType.SecondaryAudio))
|
if (signalType.HasFlag(eRoutingSignalType.SecondaryAudio))
|
||||||
{
|
{
|
||||||
audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, sourcePort, eRoutingSignalType.SecondaryAudio);
|
audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.SecondaryAudio);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, sourcePort, eRoutingSignalType.Audio);
|
audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.Audio);
|
||||||
}
|
}
|
||||||
|
|
||||||
var audioSuccess = destination.GetRouteToSource(source, null, null, signalType.HasFlag(eRoutingSignalType.SecondaryAudio) ? eRoutingSignalType.SecondaryAudio : eRoutingSignalType.Audio, 0, audioRouteDescriptor, destinationPort, sourcePort);
|
var audioSuccess = destination.GetRouteToSource(source, null, null, signalType.HasFlag(eRoutingSignalType.SecondaryAudio) ? eRoutingSignalType.SecondaryAudio : eRoutingSignalType.Audio, 0, audioRouteDescriptor, destinationPort, sourcePort);
|
||||||
|
|
||||||
if (!audioSuccess)
|
if (!audioSuccess)
|
||||||
Debug.LogDebug(destination, "Cannot find audio route to {0}", source.Key);
|
Debug.LogMessage(LogEventLevel.Debug, "Cannot find audio route to {0}", destination, source.Key);
|
||||||
|
|
||||||
var videoRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, sourcePort, eRoutingSignalType.Video);
|
var videoRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.Video);
|
||||||
|
|
||||||
var videoSuccess = destination.GetRouteToSource(source, null, null, eRoutingSignalType.Video, 0, videoRouteDescriptor, destinationPort, sourcePort);
|
var videoSuccess = destination.GetRouteToSource(source, null, null, eRoutingSignalType.Video, 0, videoRouteDescriptor, destinationPort, sourcePort);
|
||||||
|
|
||||||
if (!videoSuccess)
|
if (!videoSuccess)
|
||||||
Debug.LogDebug(destination, "Cannot find video route to {0}", source.Key);
|
Debug.LogMessage(LogEventLevel.Debug, "Cannot find video route to {0}", destination, source.Key);
|
||||||
|
|
||||||
foreach (var route in audioRouteDescriptor.Routes)
|
foreach (var route in audioRouteDescriptor.Routes)
|
||||||
{
|
{
|
||||||
Debug.LogVerbose(destination, "Audio route for device: {route}", route.ToString());
|
Debug.LogMessage(LogEventLevel.Verbose, "Audio route for device: {route}", destination, route.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var route in videoRouteDescriptor.Routes)
|
foreach (var route in videoRouteDescriptor.Routes)
|
||||||
{
|
{
|
||||||
Debug.LogVerbose(destination, "Video route for device: {route}", route.ToString());
|
Debug.LogMessage(LogEventLevel.Verbose, "Video route for device: {route}", destination, route.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!audioSuccess && !videoSuccess)
|
if (!audioSuccess && !videoSuccess)
|
||||||
return (null, null);
|
return (null, null);
|
||||||
|
|
||||||
// Return null for descriptors that have no routes
|
|
||||||
return (audioSuccess && audioRouteDescriptor.Routes.Count > 0 ? audioRouteDescriptor : null,
|
return (audioRouteDescriptor, videoRouteDescriptor);
|
||||||
videoSuccess && videoRouteDescriptor.Routes.Count > 0 ? videoRouteDescriptor : null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -275,8 +190,8 @@ namespace PepperDash.Essentials.Core
|
||||||
{
|
{
|
||||||
if (destination == null) throw new ArgumentNullException(nameof(destination));
|
if (destination == null) throw new ArgumentNullException(nameof(destination));
|
||||||
if (source == null) throw new ArgumentNullException(nameof(source));
|
if (source == null) throw new ArgumentNullException(nameof(source));
|
||||||
if (destinationPort == null) Debug.LogDebug("Destination port is null");
|
if (destinationPort == null) Debug.LogMessage(LogEventLevel.Information, "Destination port is null");
|
||||||
if (sourcePort == null) Debug.LogDebug("Source port is null");
|
if (sourcePort == null) Debug.LogMessage(LogEventLevel.Information, "Source port is null");
|
||||||
|
|
||||||
var routeRequest = new RouteRequest
|
var routeRequest = new RouteRequest
|
||||||
{
|
{
|
||||||
|
|
@ -298,7 +213,7 @@ namespace PepperDash.Essentials.Core
|
||||||
|
|
||||||
RouteRequests[destination.Key] = routeRequest;
|
RouteRequests[destination.Key] = routeRequest;
|
||||||
|
|
||||||
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);
|
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);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -310,7 +225,7 @@ namespace PepperDash.Essentials.Core
|
||||||
|
|
||||||
RouteRequests.Add(destination.Key, routeRequest);
|
RouteRequests.Add(destination.Key, routeRequest);
|
||||||
|
|
||||||
Debug.LogInformation("Device: {destination} is cooling down. Storing route request to route to source key: {sourceKey}", destination.Key, routeRequest.Source.Key);
|
Debug.LogMessage(LogEventLevel.Information, "Device: {destination} is cooling down. Storing route request to route to source key: {sourceKey}", null, destination.Key, routeRequest.Source.Key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -322,7 +237,7 @@ namespace PepperDash.Essentials.Core
|
||||||
|
|
||||||
RouteRequests.Remove(destination.Key);
|
RouteRequests.Remove(destination.Key);
|
||||||
|
|
||||||
Debug.LogInformation("Device: {destination} is NOT cooling down. Removing stored route request and routing to source key: {sourceKey}", destination.Key, routeRequest.Source.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);
|
||||||
}
|
}
|
||||||
|
|
||||||
routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, destinationPort?.Key ?? string.Empty, false));
|
routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, destinationPort?.Key ?? string.Empty, false));
|
||||||
|
|
@ -330,104 +245,6 @@ namespace PepperDash.Essentials.Core
|
||||||
routeRequestQueue.Enqueue(new RouteRequestQueueItem(RunRouteRequest, routeRequest));
|
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)).ToList();
|
|
||||||
var sources = DeviceManager.AllDevices.OfType<IRoutingOutputs>()
|
|
||||||
.Where(d => !(d is IRoutingInputsOutputs)).ToList();
|
|
||||||
|
|
||||||
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, outputPort.Type, inputPort, outputPort);
|
|
||||||
|
|
||||||
if (audioOrSingleRoute == null && videoRoute == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.LogVerbose("AudioOrSingleRoute Found: {audioRoute}", audioOrSingleRoute);
|
|
||||||
|
|
||||||
Debug.LogVerbose("VideoRoute Found: {videoRoute}", videoRoute);
|
|
||||||
|
|
||||||
if (audioOrSingleRoute != null)
|
|
||||||
{
|
|
||||||
// Only add routes that have actual switching steps
|
|
||||||
if (audioOrSingleRoute.Routes == null || audioOrSingleRoute.Routes.Count == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.LogVerbose("Route mapped: {source} -> {sink} via {input}/{output}, type {type}",
|
|
||||||
source.Key, sink.Key,
|
|
||||||
inputPort.Key, outputPort.Key, audioOrSingleRoute.SignalType);
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.LogVerbose("Video route mapped: {source} -> {sink} via {input}/{output}",
|
|
||||||
source.Key, sink.Key,
|
|
||||||
inputPort.Key, outputPort.Key);
|
|
||||||
|
|
||||||
RouteDescriptors[eRoutingSignalType.Video].AddRouteDescriptor(videoRoute);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogError("Exception mapping routes: {exception}", ex.Message);
|
|
||||||
Debug.LogDebug(ex, "Stack Trace: ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Executes the actual routing based on a <see cref="RouteRequest"/>.
|
/// Executes the actual routing based on a <see cref="RouteRequest"/>.
|
||||||
/// Finds the route path, adds it to the collection, and executes the switches.
|
/// Finds the route path, adds it to the collection, and executes the switches.
|
||||||
|
|
@ -440,54 +257,7 @@ namespace PepperDash.Essentials.Core
|
||||||
if (request.Source == null)
|
if (request.Source == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
RouteDescriptor audioOrSingleRoute = null;
|
var (audioOrSingleRoute, videoRoute) = request.Destination.GetRouteToSource(request.Source, request.SignalType, request.DestinationPort, request.SourcePort);
|
||||||
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)
|
if (audioOrSingleRoute == null && videoRoute == null)
|
||||||
return;
|
return;
|
||||||
|
|
@ -499,15 +269,14 @@ namespace PepperDash.Essentials.Core
|
||||||
RouteDescriptorCollection.DefaultCollection.AddRouteDescriptor(videoRoute);
|
RouteDescriptorCollection.DefaultCollection.AddRouteDescriptor(videoRoute);
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.LogVerbose(request.Destination, "Executing full route");
|
Debug.LogMessage(LogEventLevel.Verbose, "Executing full route", request.Destination);
|
||||||
|
|
||||||
audioOrSingleRoute.ExecuteRoutes();
|
audioOrSingleRoute.ExecuteRoutes();
|
||||||
videoRoute?.ExecuteRoutes();
|
videoRoute?.ExecuteRoutes();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Debug.LogError("Exception Running Route Request {request}: {exception}", request, ex.Message);
|
Debug.LogMessage(ex, "Exception Running Route Request {request}", null, request);
|
||||||
Debug.LogDebug(ex, "Stack Trace: ");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -521,7 +290,7 @@ namespace PepperDash.Essentials.Core
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Debug.LogInformation(destination, "Release route for '{destination}':'{inputPortKey}'", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey);
|
Debug.LogMessage(LogEventLevel.Information, "Release route for '{destination}':'{inputPortKey}'", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey);
|
||||||
|
|
||||||
if (RouteRequests.TryGetValue(destination.Key, out RouteRequest existingRequest) && destination is IWarmingCooling)
|
if (RouteRequests.TryGetValue(destination.Key, out RouteRequest existingRequest) && destination is IWarmingCooling)
|
||||||
{
|
{
|
||||||
|
|
@ -535,14 +304,13 @@ namespace PepperDash.Essentials.Core
|
||||||
var current = RouteDescriptorCollection.DefaultCollection.RemoveRouteDescriptor(destination, inputPortKey);
|
var current = RouteDescriptorCollection.DefaultCollection.RemoveRouteDescriptor(destination, inputPortKey);
|
||||||
if (current != null)
|
if (current != null)
|
||||||
{
|
{
|
||||||
Debug.LogInformation(destination, "Releasing current route: {0}", current.Source.Key);
|
Debug.LogMessage(LogEventLevel.Information, "Releasing current route: {0}", destination, current.Source.Key);
|
||||||
current.ReleaseRoutes(clearRoute);
|
current.ReleaseRoutes(clearRoute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Debug.LogError("Exception releasing route for '{destination}':'{inputPortKey}': {exception}", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey, ex.Message);
|
Debug.LogMessage(ex, "Exception releasing route for '{destination}':'{inputPortKey}'", null, destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey);
|
||||||
Debug.LogDebug(ex, "Stack Trace: ");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -553,13 +321,13 @@ namespace PepperDash.Essentials.Core
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="destination"></param>
|
/// <param name="destination"></param>
|
||||||
/// <param name="source"></param>
|
/// <param name="source"></param>
|
||||||
/// <param name="outputPortToUse">The RoutingOutputPort whose link is being checked for a route</param>
|
/// <param name="destinationPort">The RoutingOutputPort whose link is being checked for a route</param>
|
||||||
/// <param name="alreadyCheckedDevices">Prevents Devices from being twice-checked</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="signalType">This recursive function should not be called with AudioVideo</param>
|
||||||
/// <param name="cycle">Just an informational counter</param>
|
/// <param name="cycle">Just an informational counter</param>
|
||||||
/// <param name="routeTable">The RouteDescriptor being populated as the route is discovered</param>
|
/// <param name="routeTable">The RouteDescriptor being populated as the route is discovered</param>
|
||||||
/// <param name="destinationPort">The RoutingOutputPort whose link is being checked for a route</param>
|
/// <param name="outputPortToUse">The RoutingOutputPort to use for the route</param>
|
||||||
/// <param name="sourcePort">The source output port (optional)</param>
|
/// <param name="sourcePort">The specific source output port to use (optional)</param>
|
||||||
/// <returns>true if source is hit</returns>
|
/// <returns>true if source is hit</returns>
|
||||||
private static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source,
|
private static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source,
|
||||||
RoutingOutputPort outputPortToUse, List<IRoutingInputsOutputs> alreadyCheckedDevices,
|
RoutingOutputPort outputPortToUse, List<IRoutingInputsOutputs> alreadyCheckedDevices,
|
||||||
|
|
@ -567,46 +335,42 @@ namespace PepperDash.Essentials.Core
|
||||||
{
|
{
|
||||||
cycle++;
|
cycle++;
|
||||||
|
|
||||||
Debug.LogVerbose("GetRouteToSource: {cycle} {sourceKey}:{sourcePortKey}--> {destinationKey}:{destinationPortKey} {type}", null, cycle, source.Key, sourcePort?.Key ?? "auto", destination.Key, destinationPort?.Key ?? "auto", signalType.ToString());
|
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());
|
||||||
|
|
||||||
RoutingInputPort goodInputPort = null;
|
RoutingInputPort goodInputPort = null;
|
||||||
|
|
||||||
// Use indexed lookup instead of LINQ query
|
|
||||||
var allDestinationTieLines = GetTieLinesForDestination(destination.Key);
|
|
||||||
|
|
||||||
IEnumerable<TieLine> destinationTieLines;
|
IEnumerable<TieLine> destinationTieLines;
|
||||||
TieLine directTie = null;
|
TieLine directTie = null;
|
||||||
|
|
||||||
if (destinationPort == null)
|
if (destinationPort == null)
|
||||||
{
|
{
|
||||||
destinationTieLines = allDestinationTieLines.Where(t =>
|
destinationTieLines = TieLineCollection.Default.Where(t =>
|
||||||
t.Type.HasFlag(signalType) || signalType == eRoutingSignalType.AudioVideo);
|
t.DestinationPort.ParentDevice.Key == destination.Key && (t.Type.HasFlag(signalType) || signalType == eRoutingSignalType.AudioVideo));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
destinationTieLines = allDestinationTieLines.Where(t =>
|
destinationTieLines = TieLineCollection.Default.Where(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.DestinationPort.Key == destinationPort.Key && (t.Type.HasFlag(signalType)));
|
||||||
t.DestinationPort.Key == destinationPort.Key && t.Type.HasFlag(signalType));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the TieLine without a port
|
// find the TieLine without a port
|
||||||
if (destinationPort == null && sourcePort == null)
|
if (destinationPort == null && sourcePort == null)
|
||||||
{
|
{
|
||||||
directTie = destinationTieLines.FirstOrDefault(t => t.SourcePort.ParentDevice.Key == source.Key);
|
directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.SourcePort.ParentDevice.Key == source.Key);
|
||||||
}
|
}
|
||||||
// find a tieLine to a specific destination port without a specific source port
|
// find a tieLine to a specific destination port without a specific source port
|
||||||
else if (destinationPort != null && sourcePort == null)
|
else if (destinationPort != null && sourcePort == null)
|
||||||
{
|
{
|
||||||
directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key);
|
directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.DestinationPort.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key);
|
||||||
}
|
}
|
||||||
// find a tieline to a specific source port without a specific destination port
|
// find a tieline to a specific source port without a specific destination port
|
||||||
else if (destinationPort == null & sourcePort != null)
|
else if (destinationPort == null & sourcePort != null)
|
||||||
{
|
{
|
||||||
directTie = destinationTieLines.FirstOrDefault(t => t.SourcePort.ParentDevice.Key == source.Key && t.SourcePort.Key == sourcePort.Key);
|
directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.SourcePort.ParentDevice.Key == source.Key && t.SourcePort.Key == sourcePort.Key);
|
||||||
}
|
}
|
||||||
// find a tieline to a specific source port and destination port
|
// find a tieline to a specific source port and destination port
|
||||||
else if (destinationPort != null && sourcePort != null)
|
else if (destinationPort != null && sourcePort != null)
|
||||||
{
|
{
|
||||||
directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key && t.SourcePort.Key == sourcePort.Key);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (directTie != null) // Found a tie directly to the source
|
if (directTie != null) // Found a tie directly to the source
|
||||||
|
|
@ -615,16 +379,12 @@ namespace PepperDash.Essentials.Core
|
||||||
}
|
}
|
||||||
else // no direct-connect. Walk back devices.
|
else // no direct-connect. Walk back devices.
|
||||||
{
|
{
|
||||||
Debug.LogVerbose(destination, "is not directly connected to {sourceKey}. Walking down tie lines", source.Key);
|
Debug.LogMessage(LogEventLevel.Verbose, "is not directly connected to {sourceKey}. Walking down tie lines", destination, source.Key);
|
||||||
|
|
||||||
// No direct tie? Run back out on the inputs' attached devices...
|
// No direct tie? Run back out on the inputs' attached devices...
|
||||||
// Only the ones that are routing devices
|
// Only the ones that are routing devices
|
||||||
var midpointTieLines = destinationTieLines
|
var midpointTieLines = destinationTieLines.Where(t => t.SourcePort.ParentDevice is IRoutingInputsOutputs);
|
||||||
.Where(t => t.SourcePort.ParentDevice is IRoutingInputsOutputs)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
Debug.LogVerbose(destination, "Found {tieLineCount} tie lines to walk for {destinationKey}", midpointTieLines.Count, destination.Key);
|
|
||||||
|
|
||||||
//Create a list for tracking already checked devices to avoid loops, if it doesn't already exist from previous iteration
|
//Create a list for tracking already checked devices to avoid loops, if it doesn't already exist from previous iteration
|
||||||
if (alreadyCheckedDevices == null)
|
if (alreadyCheckedDevices == null)
|
||||||
alreadyCheckedDevices = new List<IRoutingInputsOutputs>();
|
alreadyCheckedDevices = new List<IRoutingInputsOutputs>();
|
||||||
|
|
@ -637,13 +397,13 @@ namespace PepperDash.Essentials.Core
|
||||||
// Check if this previous device has already been walked
|
// Check if this previous device has already been walked
|
||||||
if (alreadyCheckedDevices.Contains(midpointDevice))
|
if (alreadyCheckedDevices.Contains(midpointDevice))
|
||||||
{
|
{
|
||||||
Debug.LogVerbose(destination, "Skipping input {midpointDeviceKey} on {destinationKey}, this was already checked", midpointDevice.Key, destination.Key);
|
Debug.LogMessage(LogEventLevel.Verbose, "Skipping input {midpointDeviceKey} on {destinationKey}, this was already checked", destination, midpointDevice.Key, destination.Key);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var midpointOutputPort = tieLine.SourcePort;
|
var midpointOutputPort = tieLine.SourcePort;
|
||||||
|
|
||||||
Debug.LogVerbose(destination, "Trying to find route on {midpointDeviceKey}", midpointDevice.Key);
|
Debug.LogMessage(LogEventLevel.Verbose, "Trying to find route on {midpointDeviceKey}", destination, midpointDevice.Key);
|
||||||
|
|
||||||
// haven't seen this device yet. Do it. Pass the output port to the next
|
// haven't seen this device yet. Do it. Pass the output port to the next
|
||||||
// level to enable switching on success
|
// level to enable switching on success
|
||||||
|
|
@ -652,9 +412,9 @@ namespace PepperDash.Essentials.Core
|
||||||
|
|
||||||
if (upstreamRoutingSuccess)
|
if (upstreamRoutingSuccess)
|
||||||
{
|
{
|
||||||
Debug.LogVerbose(destination, "Upstream device route found");
|
Debug.LogMessage(LogEventLevel.Verbose, "Upstream device route found", destination);
|
||||||
Debug.LogVerbose(destination, "Route found on {midpointDeviceKey}", midpointDevice.Key);
|
Debug.LogMessage(LogEventLevel.Verbose, "Route found on {midpointDeviceKey}", destination, midpointDevice.Key);
|
||||||
Debug.LogVerbose(destination, "TieLine: SourcePort: {SourcePort} DestinationPort: {DestinationPort}", tieLine.SourcePort, tieLine.DestinationPort);
|
Debug.LogMessage(LogEventLevel.Verbose, "TieLine: SourcePort: {SourcePort} DestinationPort: {DestinationPort}", destination, tieLine.SourcePort, tieLine.DestinationPort);
|
||||||
goodInputPort = tieLine.DestinationPort;
|
goodInputPort = tieLine.DestinationPort;
|
||||||
break; // Stop looping the inputs in this cycle
|
break; // Stop looping the inputs in this cycle
|
||||||
}
|
}
|
||||||
|
|
@ -664,8 +424,7 @@ namespace PepperDash.Essentials.Core
|
||||||
|
|
||||||
if (goodInputPort == null)
|
if (goodInputPort == null)
|
||||||
{
|
{
|
||||||
Debug.LogVerbose(destination, "No route found to {0} from destination {1} for type {2}", source.Key, destination.Key, signalType);
|
Debug.LogMessage(LogEventLevel.Verbose, "No route found to {0}", destination, source.Key);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -681,7 +440,7 @@ namespace PepperDash.Essentials.Core
|
||||||
routeTable.Routes.Add(new RouteSwitchDescriptor(outputPortToUse, goodInputPort));
|
routeTable.Routes.Add(new RouteSwitchDescriptor(outputPortToUse, goodInputPort));
|
||||||
}
|
}
|
||||||
else // device is merely IRoutingInputOutputs
|
else // device is merely IRoutingInputOutputs
|
||||||
Debug.LogVerbose(destination, "No routing. Passthrough device");
|
Debug.LogMessage(LogEventLevel.Verbose, "No routing. Passthrough device", destination);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,27 +20,22 @@ namespace PepperDash.Essentials.Core
|
||||||
public IRoutingInputs Destination { get; private set; }
|
public IRoutingInputs Destination { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The InputPort on the destination device for this route, if applicable. May be null if the route is not for a specific input port.
|
/// Gets or sets the InputPort
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public RoutingInputPort InputPort { get; private set; }
|
public RoutingInputPort InputPort { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the source device (sink or midpoint) for the route.
|
/// Gets or sets the Source
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IRoutingOutputs Source { get; private set; }
|
public IRoutingOutputs Source { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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.
|
/// Gets or sets the SignalType
|
||||||
/// </summary>
|
|
||||||
public RoutingOutputPort OutputPort { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the signal type for this route.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public eRoutingSignalType SignalType { get; private set; }
|
public eRoutingSignalType SignalType { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the collection of route switch descriptors for this route.
|
/// Gets or sets the Routes
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<RouteSwitchDescriptor> Routes { get; private set; }
|
public List<RouteSwitchDescriptor> Routes { get; private set; }
|
||||||
|
|
||||||
|
|
@ -61,24 +56,11 @@ namespace PepperDash.Essentials.Core
|
||||||
/// <param name="destination">The destination device.</param>
|
/// <param name="destination">The destination device.</param>
|
||||||
/// <param name="inputPort">The destination input port (optional).</param>
|
/// <param name="inputPort">The destination input port (optional).</param>
|
||||||
/// <param name="signalType">The signal type for this route.</param>
|
/// <param name="signalType">The signal type for this route.</param>
|
||||||
public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, RoutingInputPort inputPort, eRoutingSignalType signalType) : this(source, destination, inputPort, null, signalType)
|
public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, RoutingInputPort inputPort, eRoutingSignalType 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;
|
Destination = destination;
|
||||||
InputPort = inputPort;
|
InputPort = inputPort;
|
||||||
Source = source;
|
Source = source;
|
||||||
OutputPort = outputPort;
|
|
||||||
SignalType = signalType;
|
SignalType = signalType;
|
||||||
Routes = new List<RouteSwitchDescriptor>();
|
Routes = new List<RouteSwitchDescriptor>();
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +72,7 @@ namespace PepperDash.Essentials.Core
|
||||||
{
|
{
|
||||||
foreach (var route in Routes)
|
foreach (var route in Routes)
|
||||||
{
|
{
|
||||||
Debug.LogVerbose("ExecuteRoutes: {0}", route.ToString());
|
Debug.LogMessage(LogEventLevel.Verbose, "ExecuteRoutes: {0}", null, route.ToString());
|
||||||
|
|
||||||
if (route.SwitchingDevice is IRoutingSinkWithSwitching sink)
|
if (route.SwitchingDevice is IRoutingSinkWithSwitching sink)
|
||||||
{
|
{
|
||||||
|
|
@ -104,7 +86,7 @@ namespace PepperDash.Essentials.Core
|
||||||
|
|
||||||
route.OutputPort.InUseTracker.AddUser(Destination, "destination-" + SignalType);
|
route.OutputPort.InUseTracker.AddUser(Destination, "destination-" + SignalType);
|
||||||
|
|
||||||
Debug.LogVerbose("Output port {0} routing. Count={1}", route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue);
|
Debug.LogMessage(LogEventLevel.Verbose, "Output port {0} routing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -113,15 +95,15 @@ namespace PepperDash.Essentials.Core
|
||||||
/// Releases the usage tracking for the route and optionally clears the route on the switching devices.
|
/// Releases the usage tracking for the route and optionally clears the route on the switching devices.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="clearRoute">If true, attempts to clear the route on the switching devices (e.g., set input to null/0).</param>
|
/// <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)
|
public void ReleaseRoutes(bool clearRoute = false)
|
||||||
{
|
{
|
||||||
foreach (var route in Routes.Where(r => r.SwitchingDevice is IRouting))
|
foreach (var route in Routes.Where(r => r.SwitchingDevice is IRouting))
|
||||||
{
|
{
|
||||||
if (route.SwitchingDevice is IRouting switchingDevice)
|
if (route.SwitchingDevice is IRouting switchingDevice)
|
||||||
{
|
{
|
||||||
if (clearRoute)
|
if(clearRoute)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -130,7 +112,6 @@ namespace PepperDash.Essentials.Core
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Debug.LogError("Error executing switch: {exception}", e.Message);
|
Debug.LogError("Error executing switch: {exception}", e.Message);
|
||||||
Debug.LogDebug(e, "Stack Trace: ");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,11 +123,11 @@ namespace PepperDash.Essentials.Core
|
||||||
if (route.OutputPort.InUseTracker != null)
|
if (route.OutputPort.InUseTracker != null)
|
||||||
{
|
{
|
||||||
route.OutputPort.InUseTracker.RemoveUser(Destination, "destination-" + SignalType);
|
route.OutputPort.InUseTracker.RemoveUser(Destination, "destination-" + SignalType);
|
||||||
Debug.LogVerbose("Port {0} releasing. Count={1}", route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue);
|
Debug.LogMessage(LogEventLevel.Verbose, "Port {0} releasing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Debug.LogVerbose("InUseTracker is null for OutputPort {0}", route.OutputPort.Key);
|
Debug.LogMessage(LogEventLevel.Error, "InUseTracker is null for OutputPort {0}", null, route.OutputPort.Key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -156,11 +137,98 @@ namespace PepperDash.Essentials.Core
|
||||||
/// Returns a string representation of the route descriptor, including source, destination, and individual route steps.
|
/// Returns a string representation of the route descriptor, including source, destination, and individual route steps.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A string describing the route.</returns>
|
/// <returns>A string describing the route.</returns>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
var routesText = Routes.Select(r => r.ToString()).ToArray();
|
var routesText = Routes.Select(r => r.ToString()).ToArray();
|
||||||
return $"Route table from {Source.Key} to {Destination.Key} for {SignalType}:\r\n {string.Join("\r\n ", routesText)}";
|
return string.Format("Route table from {0} to {1}:\r{2}", Source.Key, Destination.Key, string.Join("\r", 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 System.Collections.Generic;
|
using PepperDash.Core;
|
||||||
using System.Linq;
|
|
||||||
using PepperDash.Core;
|
|
||||||
using Serilog.Events;
|
using Serilog.Events;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
|
||||||
namespace PepperDash.Essentials.Core
|
namespace PepperDash.Essentials.Core
|
||||||
|
|
@ -12,7 +12,7 @@ namespace PepperDash.Essentials.Core
|
||||||
public class RouteDescriptorCollection
|
public class RouteDescriptorCollection
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the default collection of RouteDescriptors.
|
/// DefaultCollection static property
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static RouteDescriptorCollection DefaultCollection
|
public static RouteDescriptorCollection DefaultCollection
|
||||||
{
|
{
|
||||||
|
|
@ -27,11 +27,6 @@ namespace PepperDash.Essentials.Core
|
||||||
|
|
||||||
private readonly List<RouteDescriptor> RouteDescriptors = new List<RouteDescriptor>();
|
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>
|
/// <summary>
|
||||||
/// Adds a RouteDescriptor to the list. If an existing RouteDescriptor for the
|
/// Adds a RouteDescriptor to the list. If an existing RouteDescriptor for the
|
||||||
/// destination exists already, it will not be added - in order to preserve
|
/// destination exists already, it will not be added - in order to preserve
|
||||||
|
|
@ -45,34 +40,13 @@ namespace PepperDash.Essentials.Core
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a route already exists with the same source, destination, input port, AND signal type
|
if (RouteDescriptors.Any(t => t.Destination == descriptor.Destination)
|
||||||
var existingRoute = RouteDescriptors.FirstOrDefault(t =>
|
&& RouteDescriptors.Any(t => t.Destination == descriptor.Destination && t.InputPort != null && descriptor.InputPort != null && t.InputPort.Key == descriptor.InputPort.Key))
|
||||||
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.LogInformation(descriptor.Destination,
|
Debug.LogMessage(LogEventLevel.Debug, descriptor.Destination,
|
||||||
"Route from {source}:{outputPort} to {destination}:{inputPort} ({signalType}) already exists in this collection",
|
"Route to [{0}] already exists in global routes table", descriptor?.Source?.Key);
|
||||||
descriptor?.Source?.Key,
|
|
||||||
descriptor?.OutputPort?.Key ?? "auto",
|
|
||||||
descriptor?.Destination?.Key,
|
|
||||||
descriptor?.InputPort?.Key ?? "auto",
|
|
||||||
descriptor?.SignalType
|
|
||||||
);
|
|
||||||
return;
|
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);
|
RouteDescriptors.Add(descriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,11 +61,11 @@ namespace PepperDash.Essentials.Core
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the route descriptor for a specific destination and input port
|
/// Gets the RouteDescriptor for a destination and input port key. Returns null if no matching RouteDescriptor exists.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="destination">The destination device</param>
|
/// <param name="destination"></param>
|
||||||
/// <param name="inputPortKey">The input port key</param>
|
/// <param name="inputPortKey"></param>
|
||||||
/// <returns>The matching RouteDescriptor or null if not found</returns>
|
/// <returns></returns>
|
||||||
public RouteDescriptor GetRouteDescriptorForDestinationAndInputPort(IRoutingInputs destination, string inputPortKey)
|
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);
|
Debug.LogMessage(LogEventLevel.Information, "Getting route descriptor for '{destination}':'{inputPortKey}'", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey);
|
||||||
|
|
@ -108,7 +82,7 @@ namespace PepperDash.Essentials.Core
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Removing route descriptor for '{destination}':'{inputPortKey}'", destination.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey);
|
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)
|
? GetRouteDescriptorForDestination(destination)
|
||||||
: GetRouteDescriptorForDestinationAndInputPort(destination, inputPortKey);
|
: GetRouteDescriptorForDestinationAndInputPort(destination, inputPortKey);
|
||||||
if (descr != null)
|
if (descr != null)
|
||||||
|
|
@ -119,4 +93,70 @@ namespace PepperDash.Essentials.Core
|
||||||
return descr;
|
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,51 +4,96 @@
|
||||||
/// Represents a RouteSwitchDescriptor
|
/// Represents a RouteSwitchDescriptor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RouteSwitchDescriptor
|
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>
|
/// <summary>
|
||||||
/// Gets or sets the SwitchingDevice
|
/// Gets or sets the SwitchingDevice
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IRoutingInputs SwitchingDevice { get { return InputPort?.ParentDevice; } }
|
public IRoutingInputs<TInputSelector> SwitchingDevice { get { return InputPort.ParentDevice; } }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The output port being switched from (relevant for matrix switchers). Null for sink devices.
|
/// Gets or sets the OutputPort
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public RoutingOutputPort OutputPort { get; set; }
|
public RoutingOutputPort<TOutputSelector> OutputPort { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The input port being switched to.
|
/// Gets or sets the InputPort
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public RoutingInputPort InputPort { get; set; }
|
public RoutingInputPort<TInputSelector> InputPort { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
public RouteSwitchDescriptor(RoutingInputPort<TInputSelector> inputPort)
|
||||||
/// 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;
|
InputPort = inputPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public RouteSwitchDescriptor(RoutingOutputPort<TOutputSelector> outputPort, RoutingInputPort<TInputSelector> inputPort)
|
||||||
/// 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;
|
InputPort = inputPort;
|
||||||
OutputPort = outputPort;
|
OutputPort = outputPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a string representation of the route switch descriptor.
|
/// ToString method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A string describing the switch operation.</returns>
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
if (SwitchingDevice is IRouting)
|
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")}";
|
return string.Format("{0} switches output '{1}' to input '{2}'", SwitchingDevice.Key, OutputPort.Selector, InputPort.Selector);
|
||||||
else
|
else
|
||||||
return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches to input {(InputPort != null ? InputPort.Key : "No input port")}";
|
return string.Format("{0} switches to input '{1}'", SwitchingDevice.Key, InputPort.Selector);
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Timers;
|
|
||||||
using PepperDash.Core;
|
using PepperDash.Core;
|
||||||
using PepperDash.Essentials.Core.Config;
|
using PepperDash.Essentials.Core.Config;
|
||||||
|
|
||||||
|
|
@ -13,26 +11,6 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RoutingFeedbackManager : EssentialsDevice
|
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>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="RoutingFeedbackManager"/> class.
|
/// Initializes a new instance of the <see cref="RoutingFeedbackManager"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -41,103 +19,10 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
public RoutingFeedbackManager(string key, string name)
|
public RoutingFeedbackManager(string key, string name)
|
||||||
: base(key, name)
|
: base(key, name)
|
||||||
{
|
{
|
||||||
AddPreActivationAction(BuildMidpointSinkMap);
|
|
||||||
AddPreActivationAction(SubscribeForMidpointFeedback);
|
AddPreActivationAction(SubscribeForMidpointFeedback);
|
||||||
AddPreActivationAction(SubscribeForSinkFeedback);
|
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 (string.IsNullOrEmpty(midpointKey))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(midpoint.Key))
|
|
||||||
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>
|
/// <summary>
|
||||||
/// Subscribes to the RouteChanged event on all devices implementing <see cref="IRoutingWithFeedback"/>.
|
/// Subscribes to the RouteChanged event on all devices implementing <see cref="IRoutingWithFeedback"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -167,7 +52,7 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the RouteChanged event from a midpoint device.
|
/// Handles the RouteChanged event from a midpoint device.
|
||||||
/// Only triggers updates for sink devices that are downstream of this midpoint.
|
/// Triggers an update for all sink devices.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="midpoint">The midpoint device that reported a route change.</param>
|
/// <param name="midpoint">The midpoint device that reported a route change.</param>
|
||||||
/// <param name="newRoute">The descriptor of the new route.</param>
|
/// <param name="newRoute">The descriptor of the new route.</param>
|
||||||
|
|
@ -178,33 +63,12 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Only update affected sinks (performance optimization)
|
var devices =
|
||||||
if (midpointToSinksMap != null && midpointToSinksMap.TryGetValue(midpoint.Key, out var affectedSinkKeys))
|
DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
|
||||||
{
|
|
||||||
Debug.LogMessage(
|
|
||||||
Serilog.Events.LogEventLevel.Debug,
|
|
||||||
"Midpoint {midpoint} changed, updating {count} downstream sinks",
|
|
||||||
this,
|
|
||||||
midpoint.Key,
|
|
||||||
affectedSinkKeys.Count
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach (var sinkKey in affectedSinkKeys)
|
foreach (var device in devices)
|
||||||
{
|
|
||||||
if (DeviceManager.GetDeviceForKey(sinkKey) is IRoutingSinkWithSwitchingWithInputPort sink)
|
|
||||||
{
|
|
||||||
UpdateDestination(sink, sink.CurrentInputPort);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
Debug.LogMessage(
|
UpdateDestination(device, device.CurrentInputPort);
|
||||||
Serilog.Events.LogEventLevel.Debug,
|
|
||||||
"Midpoint {midpoint} changed but has no downstream sinks in map",
|
|
||||||
this,
|
|
||||||
midpoint.Key
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -219,49 +83,9 @@ 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 (string.IsNullOrEmpty(midpointKey))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!midpointToSinksMap.ContainsKey(midpointKey))
|
|
||||||
midpointToSinksMap[midpointKey] = new HashSet<string>();
|
|
||||||
|
|
||||||
midpointToSinksMap[midpointKey].Add(sink.Key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the InputChanged event from a sink device.
|
/// Handles the InputChanged event from a sink device.
|
||||||
/// Updates the midpoint-to-sink map for the new input path, then triggers
|
/// Triggers an update for the specific sink device.
|
||||||
/// a source-info update for the sink.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sender">The sink device that reported an input change.</param>
|
/// <param name="sender">The sink device that reported an input change.</param>
|
||||||
/// <param name="currentInputPort">The new input port selected on the sink device.</param>
|
/// <param name="currentInputPort">The new input port selected on the sink device.</param>
|
||||||
|
|
@ -272,10 +96,6 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Keep the map current so HandleMidpointUpdate can find this sink
|
|
||||||
if (sender is IRoutingSinkWithSwitchingWithInputPort sinkWithInputPort)
|
|
||||||
RebuildMapForSink(sinkWithInputPort);
|
|
||||||
|
|
||||||
UpdateDestination(sender, currentInputPort);
|
UpdateDestination(sender, currentInputPort);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -293,7 +113,6 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the CurrentSourceInfo and CurrentSourceInfoKey properties on a destination (sink) device
|
/// 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.
|
/// based on its currently selected input port by tracing the route back through tie lines.
|
||||||
/// Uses debouncing to prevent rapid successive updates.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="destination">The destination sink device to update.</param>
|
/// <param name="destination">The destination sink device to update.</param>
|
||||||
/// <param name="inputPort">The currently selected input port on the destination device.</param>
|
/// <param name="inputPort">The currently selected input port on the destination device.</param>
|
||||||
|
|
@ -301,76 +120,6 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
IRoutingSinkWithSwitching destination,
|
IRoutingSinkWithSwitching destination,
|
||||||
RoutingInputPort inputPort
|
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(
|
Debug.LogMessage(
|
||||||
Serilog.Events.LogEventLevel.Debug,
|
Serilog.Events.LogEventLevel.Debug,
|
||||||
|
|
@ -457,8 +206,7 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Debug.LogError(this, "Error getting sourceTieLine: {message}", ex.Message);
|
Debug.LogMessage(ex, "Error getting sourceTieLine: {Exception}", this, ex);
|
||||||
Debug.LogDebug(ex, "StackTrace: ");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -482,11 +230,6 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
return roomDefaultDisplay.DefaultDisplay.Key == destination.Key;
|
return roomDefaultDisplay.DefaultDisplay.Key == destination.Key;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ConfigReader.ConfigObject.GetDestinationListForKey(r.DestinationListKey)?.FirstOrDefault(d => d.Value.SinkKey == destination.Key) != null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -508,8 +251,10 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
|
|
||||||
if (sourceList == null)
|
if (sourceList == null)
|
||||||
{
|
{
|
||||||
Debug.LogDebug(this,
|
Debug.LogMessage(
|
||||||
|
Serilog.Events.LogEventLevel.Debug,
|
||||||
"No source list found for source list key {key}. Unable to find source for tieLine {sourceTieLine}",
|
"No source list found for source list key {key}. Unable to find source for tieLine {sourceTieLine}",
|
||||||
|
this,
|
||||||
room.SourceListKey,
|
room.SourceListKey,
|
||||||
sourceTieLine
|
sourceTieLine
|
||||||
);
|
);
|
||||||
|
|
@ -538,8 +283,10 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
|
|
||||||
if (source == null)
|
if (source == null)
|
||||||
{
|
{
|
||||||
Debug.LogDebug(this,
|
Debug.LogMessage(
|
||||||
|
Serilog.Events.LogEventLevel.Debug,
|
||||||
"No source found for device {key}. Creating transient source for {destination}",
|
"No source found for device {key}. Creating transient source for {destination}",
|
||||||
|
this,
|
||||||
sourceTieLine.SourcePort.ParentDevice.Key,
|
sourceTieLine.SourcePort.ParentDevice.Key,
|
||||||
destination
|
destination
|
||||||
);
|
);
|
||||||
|
|
@ -562,92 +309,105 @@ namespace PepperDash.Essentials.Core.Routing
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Traces a route back from a given tie line to find the root source tie line.
|
/// Recursively 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.
|
/// It navigates through midpoint devices (<see cref="IRoutingWithFeedback"/>) by checking their current routes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="tieLine">The starting tie line (typically connected to a sink or midpoint).</param>
|
/// <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>
|
/// <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)
|
private TieLine GetRootTieLine(TieLine tieLine)
|
||||||
{
|
{
|
||||||
|
TieLine nextTieLine = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!(tieLine.DestinationPort.ParentDevice is IRoutingInputs sink))
|
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "**Following tieLine {tieLine}**", this, tieLine);
|
||||||
|
|
||||||
|
if (tieLine.SourcePort.ParentDevice is IRoutingWithFeedback midpoint)
|
||||||
{
|
{
|
||||||
Debug.LogDebug(this,
|
// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLine Source device {sourceDevice} is midpoint", this, midpoint);
|
||||||
"TieLine destination {device} is not IRoutingInputs",
|
|
||||||
tieLine.DestinationPort.ParentDevice.Key
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all potential sources (devices that only have outputs, not inputs+outputs)
|
if (midpoint.CurrentRoutes == null || midpoint.CurrentRoutes.Count == 0)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
// Use the optimized route discovery with loop protection
|
Debug.LogMessage(
|
||||||
var (route, _) = sink.GetRouteToSource(
|
Serilog.Events.LogEventLevel.Debug,
|
||||||
source,
|
"Midpoint {midpointKey} has no routes",
|
||||||
signalType,
|
this,
|
||||||
tieLine.DestinationPort,
|
midpoint.Key
|
||||||
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.LogDebug(this, "No route found to any source from {sink}", sink.Key);
|
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLIne Source Device {sourceDeviceKey} is IRoutingSource: {isIRoutingSource}", this, tieLine.SourcePort.ParentDevice.Key, tieLine.SourcePort.ParentDevice is IRoutingSource);
|
||||||
return null;
|
//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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Debug.LogError(this, "Error getting root tieLine: {message}", ex.Message);
|
Debug.LogMessage(ex, "Error walking tieLines: {Exception}", this, ex);
|
||||||
Debug.LogDebug(ex, "StackTrace: ");
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,19 +27,16 @@ namespace PepperDash.Essentials.Core
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Control signal type
|
/// Control signal type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("UsbOutput is no longer supported and will be removed in a future release.")]
|
|
||||||
UsbOutput = 8,
|
UsbOutput = 8,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Control signal type
|
/// Control signal type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("UsbInput is no longer supported and will be removed in a future release.")]
|
|
||||||
UsbInput = 16,
|
UsbInput = 16,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Secondary audio signal type
|
/// Secondary audio signal type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("SecondaryAudio is no longer supported and will be removed in a future release.")]
|
|
||||||
SecondaryAudio = 32
|
SecondaryAudio = 32
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Crestron.SimplSharp;
|
using Crestron.SimplSharp;
|
||||||
using PepperDash.Core;
|
using PepperDash.Core;
|
||||||
using PepperDash.Core.Logging;
|
|
||||||
using Serilog.Events;
|
using Serilog.Events;
|
||||||
|
|
||||||
namespace PepperDash.Essentials.Core
|
namespace PepperDash.Essentials.Core
|
||||||
|
|
@ -297,14 +296,19 @@ namespace PepperDash.Essentials.Core
|
||||||
{
|
{
|
||||||
var secretPresent = provider.TestSecret(key);
|
var secretPresent = provider.TestSecret(key);
|
||||||
|
|
||||||
provider.LogVerbose("SecretsProvider {0} {1} contain a secret entry for {2}", provider.Key, secretPresent ? "does" : "does not", key);
|
Debug.LogMessage(LogEventLevel.Verbose, provider, "SecretsProvider {0} {1} contain a secret entry for {2}", provider.Key, secretPresent ? "does" : "does not", key);
|
||||||
|
|
||||||
if (!secretPresent)
|
if (!secretPresent)
|
||||||
return
|
return
|
||||||
$"Unable to update secret for {provider.Key}:{key} - Please use the 'SetSecret' command to modify it";
|
String.Format(
|
||||||
|
"Unable to update secret for {0}:{1} - Please use the 'SetSecret' command to modify it");
|
||||||
var response = provider.SetSecret(key, secret)
|
var response = provider.SetSecret(key, secret)
|
||||||
? $"Secret successfully set for {provider.Key}:{key}"
|
? String.Format(
|
||||||
: $"Unable to set secret for {provider.Key}:{key}";
|
"Secret successfully set for {0}:{1}",
|
||||||
|
provider.Key, key)
|
||||||
|
: String.Format(
|
||||||
|
"Unable to set secret for {0}:{1}",
|
||||||
|
provider.Key, key);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -312,14 +316,19 @@ namespace PepperDash.Essentials.Core
|
||||||
{
|
{
|
||||||
var secretPresent = provider.TestSecret(key);
|
var secretPresent = provider.TestSecret(key);
|
||||||
|
|
||||||
provider.LogVerbose("SecretsProvider {0} {1} contain a secret entry for {2}", provider.Key, secretPresent ? "does" : "does not", key);
|
Debug.LogMessage(LogEventLevel.Verbose, provider, "SecretsProvider {0} {1} contain a secret entry for {2}", provider.Key, secretPresent ? "does" : "does not", key);
|
||||||
|
|
||||||
if (secretPresent)
|
if (secretPresent)
|
||||||
return
|
return
|
||||||
$"Unable to set secret for {provider.Key}:{key} - Please use the 'UpdateSecret' command to modify it";
|
String.Format(
|
||||||
|
"Unable to set secret for {0}:{1} - Please use the 'UpdateSecret' command to modify it");
|
||||||
var response = provider.SetSecret(key, secret)
|
var response = provider.SetSecret(key, secret)
|
||||||
? $"Secret successfully set for {provider.Key}:{key}"
|
? String.Format(
|
||||||
: $"Unable to set secret for {provider.Key}:{key}";
|
"Secret successfully set for {0}:{1}",
|
||||||
|
provider.Key, key)
|
||||||
|
: String.Format(
|
||||||
|
"Unable to set secret for {0}:{1}",
|
||||||
|
provider.Key, key);
|
||||||
return response;
|
return response;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -368,10 +377,15 @@ namespace PepperDash.Essentials.Core
|
||||||
|
|
||||||
var key = args[1];
|
var key = args[1];
|
||||||
|
|
||||||
|
|
||||||
provider.SetSecret(key, "");
|
provider.SetSecret(key, "");
|
||||||
response = provider.SetSecret(key, "")
|
response = provider.SetSecret(key, "")
|
||||||
? $"Secret successfully deleted for {provider.Key}:{key}"
|
? String.Format(
|
||||||
: $"Unable to delete secret for {provider.Key}:{key}";
|
"Secret successfully deleted for {0}:{1}",
|
||||||
|
provider.Key, key)
|
||||||
|
: String.Format(
|
||||||
|
"Unable to delete secret for {0}:{1}",
|
||||||
|
provider.Key, key);
|
||||||
CrestronConsole.ConsoleCommandResponse(response);
|
CrestronConsole.ConsoleCommandResponse(response);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,96 +10,78 @@ using Serilog.Events;
|
||||||
|
|
||||||
namespace PepperDash.Essentials.Core.Web
|
namespace PepperDash.Essentials.Core.Web
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a EssentialsWebApi
|
/// Represents a EssentialsWebApi
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class EssentialsWebApi : EssentialsDevice
|
public class EssentialsWebApi : EssentialsDevice
|
||||||
{
|
{
|
||||||
private readonly WebApiServer _server;
|
private readonly WebApiServer _server;
|
||||||
|
|
||||||
private readonly WebApiServer _debugServer;
|
///<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;
|
||||||
|
|
||||||
|
/// <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; }
|
||||||
|
}
|
||||||
|
|
||||||
///<example>
|
/// <summary>
|
||||||
/// http(s)://{ipaddress}/cws/{basePath}
|
/// Constructor
|
||||||
/// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
|
/// </summary>
|
||||||
/// </example>
|
/// <param name="key"></param>
|
||||||
private readonly string _defaultBasePath = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance
|
/// <param name="name"></param>
|
||||||
? string.Format("/app{0:00}/api", InitialParametersClass.ApplicationNumber)
|
public EssentialsWebApi(string key, string name)
|
||||||
: "/api";
|
: this(key, name, null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
private const int DebugTrace = 0;
|
/// <summary>
|
||||||
private const int DebugInfo = 1;
|
/// Constructor
|
||||||
private const int DebugVerbose = 2;
|
/// </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>
|
if (config == null)
|
||||||
/// Gets or sets the BasePath
|
BasePath = _defaultBasePath;
|
||||||
/// </summary>
|
else
|
||||||
public string BasePath { get; private set; }
|
BasePath = string.IsNullOrEmpty(config.BasePath) ? _defaultBasePath : config.BasePath;
|
||||||
|
|
||||||
/// <summary>
|
_server = new WebApiServer(Key, Name, BasePath);
|
||||||
/// Tracks if CWS is registered
|
|
||||||
/// </summary>
|
|
||||||
public bool IsRegistered
|
|
||||||
{
|
|
||||||
get { return _server.IsRegistered; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
SetupRoutes();
|
||||||
/// Constructor
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="key"></param>
|
|
||||||
/// <param name="name"></param>
|
|
||||||
public EssentialsWebApi(string key, string name)
|
|
||||||
: this(key, name, null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
private void SetupRoutes()
|
||||||
/// 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;
|
|
||||||
|
|
||||||
_server = new WebApiServer(Key, Name, BasePath);
|
|
||||||
|
|
||||||
_debugServer = new WebApiServer($"{key}-debug-app", $"{name} Debug App", "/debug");
|
|
||||||
_debugServer.SetFallbackHandler(new ServeDebugAppRequestHandler());
|
|
||||||
|
|
||||||
SetupRoutes();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetupRoutes()
|
|
||||||
{
|
|
||||||
var routes = new List<HttpCwsRoute>
|
var routes = new List<HttpCwsRoute>
|
||||||
{
|
{
|
||||||
new HttpCwsRoute("login")
|
|
||||||
{
|
|
||||||
Name = "Root",
|
|
||||||
RouteHandler = new LoginRequestHandler()
|
|
||||||
},
|
|
||||||
new HttpCwsRoute("versions")
|
new HttpCwsRoute("versions")
|
||||||
{
|
{
|
||||||
Name = "ReportVersions",
|
Name = "ReportVersions",
|
||||||
RouteHandler = new ReportVersionsRequestHandler()
|
RouteHandler = new ReportVersionsRequestHandler()
|
||||||
},
|
},
|
||||||
new HttpCwsRoute("packageManifest")
|
|
||||||
{
|
|
||||||
Name = "GetPackageManifest",
|
|
||||||
RouteHandler = new GetPackageManifestRequestHandler()
|
|
||||||
},
|
|
||||||
new HttpCwsRoute("appdebug")
|
new HttpCwsRoute("appdebug")
|
||||||
{
|
{
|
||||||
Name = "AppDebug",
|
Name = "AppDebug",
|
||||||
|
|
@ -195,11 +177,6 @@ namespace PepperDash.Essentials.Core.Web
|
||||||
Name = "Get Routing Ports for a device",
|
Name = "Get Routing Ports for a device",
|
||||||
RouteHandler = new GetRoutingPortsHandler()
|
RouteHandler = new GetRoutingPortsHandler()
|
||||||
},
|
},
|
||||||
new HttpCwsRoute("routingDevicesAndTieLines")
|
|
||||||
{
|
|
||||||
Name = "Get Routing Devices and TieLines",
|
|
||||||
RouteHandler = new GetRoutingDevicesAndTieLinesHandler()
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
AddRoute(routes);
|
AddRoute(routes);
|
||||||
|
|
@ -234,89 +211,78 @@ namespace PepperDash.Essentials.Core.Web
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
AddRoute(new HttpCwsRoute("apiPaths")
|
AddRoute(new HttpCwsRoute("apiPaths") {
|
||||||
{
|
|
||||||
Name = "GetPaths",
|
Name = "GetPaths",
|
||||||
RouteHandler = new GetRoutesHandler(_server.GetRouteCollection(), BasePath)
|
RouteHandler = new GetRoutesHandler(_server.GetRouteCollection(), BasePath)
|
||||||
});
|
});
|
||||||
|
|
||||||
// If running on an appliance
|
// If running on an appliance
|
||||||
if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance)
|
if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance)
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
WEBSERVER [ON | OFF | TIMEOUT <VALUE IN SECONDS> | MAXSESSIONSPERUSER <Number of sessions>]
|
WEBSERVER [ON | OFF | TIMEOUT <VALUE IN SECONDS> | MAXSESSIONSPERUSER <Number of sessions>]
|
||||||
*/
|
*/
|
||||||
var response = string.Empty;
|
var response = string.Empty;
|
||||||
CrestronConsole.SendControlSystemCommand("webserver", ref response);
|
CrestronConsole.SendControlSystemCommand("webserver", ref response);
|
||||||
if (response.Contains("OFF")) return;
|
if (response.Contains("OFF")) return;
|
||||||
|
|
||||||
var is4Series = eCrestronSeries.Series4 == (Global.ProcessorSeries & eCrestronSeries.Series4);
|
var is4Series = eCrestronSeries.Series4 == (Global.ProcessorSeries & eCrestronSeries.Series4);
|
||||||
Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on {0} Appliance", is4Series ? "4-series" : "3-series");
|
Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on {0} Appliance", is4Series ? "4-series" : "3-series");
|
||||||
|
|
||||||
_server.Start();
|
_server.Start();
|
||||||
_debugServer.Start();
|
|
||||||
|
|
||||||
GetPaths();
|
GetPaths();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatically start CWS when running on a server (Linux OS, Virtual Control)
|
// Automatically start CWS when running on a server (Linux OS, Virtual Control)
|
||||||
Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on Virtual Control Server");
|
Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on Virtual Control Server");
|
||||||
|
|
||||||
_server.Start();
|
_server.Start();
|
||||||
_debugServer.Start();
|
|
||||||
|
|
||||||
GetPaths();
|
GetPaths();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Print the available paths
|
/// Print the available pahts
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <example>
|
/// <example>
|
||||||
/// http(s)://{ipaddress}/cws/{basePath}
|
/// http(s)://{ipaddress}/cws/{basePath}
|
||||||
/// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
|
/// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
|
||||||
/// </example>
|
/// </example>
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// GetPaths method
|
/// GetPaths method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void GetPaths()
|
public void GetPaths()
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
|
Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
|
||||||
|
|
||||||
var currentIp = CrestronEthernetHelper.GetEthernetParameter(
|
var currentIp = CrestronEthernetHelper.GetEthernetParameter(
|
||||||
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
|
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
|
||||||
|
|
||||||
var hostname = CrestronEthernetHelper.GetEthernetParameter(
|
var hostname = CrestronEthernetHelper.GetEthernetParameter(
|
||||||
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0);
|
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0);
|
||||||
|
|
||||||
var path = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server
|
var path = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server
|
||||||
? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws{BasePath}"
|
? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws{BasePath}"
|
||||||
: $"https://{currentIp}/cws{BasePath}";
|
: $"https://{currentIp}/cws{BasePath}";
|
||||||
|
|
||||||
|
Debug.LogMessage(LogEventLevel.Information, this, "Server:{path:l}", path);
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, this, "Server:{path:l}", path);
|
var routeCollection = _server.GetRouteCollection();
|
||||||
|
if (routeCollection == null)
|
||||||
var routeCollection = _server.GetRouteCollection();
|
{
|
||||||
if (routeCollection == null)
|
Debug.LogMessage(LogEventLevel.Information, this, "Server route collection is null");
|
||||||
{
|
return;
|
||||||
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, "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, "{routeName:l}: {routePath:l}/{routeUrl:l}", route.Name, path, route.Url);
|
Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
|
||||||
}
|
}
|
||||||
Debug.LogInformation(this, "Web API initialized and ready to accept requests");
|
}
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
|
|
||||||
|
|
||||||
var debugAppUrl = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server
|
|
||||||
? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws/debug"
|
|
||||||
: $"https://{currentIp}/cws/debug";
|
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, this, "Developer Tools Web App available at: {debugAppUrl:l}", debugAppUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -36,25 +36,17 @@ namespace PepperDash.Essentials.Core.Web
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// MapToDeviceListObject method
|
/// MapToDeviceListObject method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static object MapToDeviceListObject(IKeyed device)
|
public static object MapToDeviceListObject(IKeyed device)
|
||||||
{
|
{
|
||||||
var interfaces = device.GetType()
|
|
||||||
.GetInterfaces()
|
|
||||||
.Select(i => i.Name)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return new
|
return new
|
||||||
{
|
{
|
||||||
device.Key,
|
device.Key,
|
||||||
Name = (device is IKeyName)
|
Name = (device is IKeyName)
|
||||||
? (device as IKeyName).Name
|
? (device as IKeyName).Name
|
||||||
: "---",
|
: "---"
|
||||||
Interfaces = interfaces
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,6 +110,5 @@ namespace PepperDash.Essentials.Core.Web
|
||||||
CType = device.Value.Type == null ? "---": device.Value.Type.ToString()
|
CType = device.Value.Type == null ? "---": device.Value.Type.ToString()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -17,10 +17,7 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||||
/// Represents a DebugSessionRequestHandler
|
/// Represents a DebugSessionRequestHandler
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DebugSessionRequestHandler : WebApiBaseRequestHandler
|
public class DebugSessionRequestHandler : WebApiBaseRequestHandler
|
||||||
{
|
{
|
||||||
private CTimer _portForwardTimeoutTimer;
|
|
||||||
private readonly object _timerLock = new object();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructor
|
/// Constructor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -51,7 +48,6 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||||
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
|
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
|
||||||
|
|
||||||
var port = 0;
|
var port = 0;
|
||||||
string csIp = null;
|
|
||||||
|
|
||||||
if (!Debug.WebsocketSink.IsRunning)
|
if (!Debug.WebsocketSink.IsRunning)
|
||||||
{
|
{
|
||||||
|
|
@ -63,50 +59,14 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||||
Debug.SetWebSocketMinimumDebugLevel(Serilog.Events.LogEventLevel.Verbose);
|
Debug.SetWebSocketMinimumDebugLevel(Serilog.Events.LogEventLevel.Verbose);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to get the CS LAN IP and forward the port
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(
|
|
||||||
EthernetAdapterType.EthernetCSAdapter);
|
|
||||||
csIp = CrestronEthernetHelper.GetEthernetParameter(
|
|
||||||
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId);
|
|
||||||
|
|
||||||
if (port > 0)
|
|
||||||
{
|
|
||||||
var result = CrestronEthernetHelper.AddPortForwarding(
|
|
||||||
(ushort)port, (ushort)port, csIp,
|
|
||||||
CrestronEthernetHelper.ePortMapTransport.TCP);
|
|
||||||
|
|
||||||
if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr)
|
|
||||||
{
|
|
||||||
Debug.LogMessage(LogEventLevel.Warning, "Error adding port forwarding for debug websocket: {0}", result);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Port {0} forwarded to CS LAN for debug websocket", port);
|
|
||||||
StartPortForwardTimeout(port, csIp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (ArgumentException)
|
|
||||||
{
|
|
||||||
Debug.LogMessage(LogEventLevel.Debug, "This processor does not have a CS LAN adapter; skipping port forwarding");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogMessage(LogEventLevel.Warning, "Error automatically forwarding debug websocket port to CS LAN: {0}", ex.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
var url = Debug.WebsocketSink.Url;
|
var url = Debug.WebsocketSink.Url;
|
||||||
|
|
||||||
var data = new
|
object data = new
|
||||||
{
|
{
|
||||||
url = Debug.WebsocketSink.Url,
|
url = Debug.WebsocketSink.Url
|
||||||
fallbackUrl = csIp != null ? url.Replace(csIp, ip) : null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Debug Session URL: {0}", url);
|
Debug.LogMessage(LogEventLevel.Information, "Debug Session URL: {0}", url);
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Fallback Debug Session URL: {0}", data.fallbackUrl);
|
|
||||||
|
|
||||||
// Return the port number with the full url of the WS Server
|
// Return the port number with the full url of the WS Server
|
||||||
var res = JsonConvert.SerializeObject(data);
|
var res = JsonConvert.SerializeObject(data);
|
||||||
|
|
@ -130,49 +90,8 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||||
/// <param name="context"></param>
|
/// <param name="context"></param>
|
||||||
protected override void HandlePost(HttpCwsContext context)
|
protected override void HandlePost(HttpCwsContext context)
|
||||||
{
|
{
|
||||||
CancelPortForwardTimeout();
|
|
||||||
|
|
||||||
var port = Debug.WebsocketSink.Port;
|
|
||||||
|
|
||||||
Debug.WebsocketSink.StopServer();
|
Debug.WebsocketSink.StopServer();
|
||||||
|
|
||||||
// Remove the port forwarding entry
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(
|
|
||||||
EthernetAdapterType.EthernetCSAdapter);
|
|
||||||
var csIp = CrestronEthernetHelper.GetEthernetParameter(
|
|
||||||
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId);
|
|
||||||
|
|
||||||
if (port <= 0)
|
|
||||||
{
|
|
||||||
Debug.LogMessage(LogEventLevel.Debug, "Debug websocket port is not set; skipping port forwarding removal");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var result = CrestronEthernetHelper.RemovePortForwarding(
|
|
||||||
(ushort)port, (ushort)port, csIp,
|
|
||||||
CrestronEthernetHelper.ePortMapTransport.TCP);
|
|
||||||
|
|
||||||
if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr)
|
|
||||||
{
|
|
||||||
Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding for debug websocket: {0}", result);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Port forwarding for port {0} removed", port);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (ArgumentException)
|
|
||||||
{
|
|
||||||
Debug.LogMessage(LogEventLevel.Debug, "This processor does not have a CS LAN adapter; skipping port forwarding removal");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogMessage(LogEventLevel.Warning, "Error removing debug websocket port forwarding: {0}", ex.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Response.StatusCode = 200;
|
context.Response.StatusCode = 200;
|
||||||
context.Response.StatusDescription = "OK";
|
context.Response.StatusDescription = "OK";
|
||||||
context.Response.End();
|
context.Response.End();
|
||||||
|
|
@ -180,55 +99,5 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Websocket Debug Session Stopped");
|
Debug.LogMessage(LogEventLevel.Information, "Websocket Debug Session Stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void StartPortForwardTimeout(int port, string csIp)
|
|
||||||
{
|
|
||||||
lock (_timerLock)
|
|
||||||
{
|
|
||||||
_portForwardTimeoutTimer?.Dispose();
|
|
||||||
_portForwardTimeoutTimer = new CTimer(_ =>
|
|
||||||
{
|
|
||||||
if (Debug.WebsocketSink.HasActiveConnections)
|
|
||||||
{
|
|
||||||
Debug.LogMessage(LogEventLevel.Debug, "Debug websocket has active connections; keeping port forward");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "No debug websocket connection within 30 seconds; removing port forward for port {0}", port);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = CrestronEthernetHelper.RemovePortForwarding(
|
|
||||||
(ushort)port, (ushort)port, csIp,
|
|
||||||
CrestronEthernetHelper.ePortMapTransport.TCP);
|
|
||||||
|
|
||||||
if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr)
|
|
||||||
{
|
|
||||||
Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding on timeout: {0}", result);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Port forwarding for port {0} removed due to timeout", port);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding on timeout: {0}", ex.Message);
|
|
||||||
}
|
|
||||||
}, 30000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cancels the port forward timeout timer if a session is being explicitly stopped.
|
|
||||||
/// </summary>
|
|
||||||
private void CancelPortForwardTimeout()
|
|
||||||
{
|
|
||||||
lock (_timerLock)
|
|
||||||
{
|
|
||||||
_portForwardTimeoutTimer?.Dispose();
|
|
||||||
_portForwardTimeoutTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
using System;
|
using System.Linq;
|
||||||
using System.Linq;
|
|
||||||
using Crestron.SimplSharp.WebScripting;
|
using Crestron.SimplSharp.WebScripting;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using PepperDash.Core.Web.RequestHandlers;
|
using PepperDash.Core.Web.RequestHandlers;
|
||||||
|
|
||||||
namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a GetFeedbacksForDeviceRequestHandler
|
/// Represents a GetFeedbacksForDeviceRequestHandler
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class GetFeedbacksForDeviceRequestHandler : WebApiBaseRequestHandler
|
public class GetFeedbacksForDeviceRequestHandler : WebApiBaseRequestHandler
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -77,7 +76,7 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||||
Value = feedback.IntValue
|
Value = feedback.IntValue
|
||||||
};
|
};
|
||||||
|
|
||||||
var stringFeedback =
|
var stringFeedback =
|
||||||
from feedback in device.Feedbacks.OfType<StringFeedback>()
|
from feedback in device.Feedbacks.OfType<StringFeedback>()
|
||||||
where !string.IsNullOrEmpty(feedback.Key)
|
where !string.IsNullOrEmpty(feedback.Key)
|
||||||
select new
|
select new
|
||||||
|
|
|
||||||
|
|
@ -1,259 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using Crestron.SimplSharp.WebScripting;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using PepperDash.Core.Web.RequestHandlers;
|
|
||||||
using PepperDash.Essentials.Core.Config;
|
|
||||||
|
|
||||||
namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a GetPackageManifestRequestHandler
|
|
||||||
/// </summary>
|
|
||||||
public class GetPackageManifestRequestHandler : WebApiBaseRequestHandler
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Constructor
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// base(true) enables CORS support by default
|
|
||||||
/// </remarks>
|
|
||||||
public GetPackageManifestRequestHandler()
|
|
||||||
: base(true)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles GET method requests
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context"></param>
|
|
||||||
protected override void HandleGet(HttpCwsContext context)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = CloneVersionData(ConfigReader.ConfigObject?.Versions) ?? new VersionData();
|
|
||||||
|
|
||||||
PopulateEssentials(result);
|
|
||||||
PopulatePackages(result);
|
|
||||||
|
|
||||||
var js = JsonConvert.SerializeObject(result, Formatting.Indented);
|
|
||||||
|
|
||||||
context.Response.StatusCode = 200;
|
|
||||||
context.Response.StatusDescription = "OK";
|
|
||||||
context.Response.ContentType = "application/json";
|
|
||||||
context.Response.ContentEncoding = System.Text.Encoding.UTF8;
|
|
||||||
context.Response.Write(js, false);
|
|
||||||
context.Response.End();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
PepperDash.Core.Debug.LogMessage(ex, "Exception handling GET /packageManifest request");
|
|
||||||
context.Response.StatusCode = 500;
|
|
||||||
context.Response.StatusDescription = "Internal Server Error";
|
|
||||||
context.Response.End();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deep-copies the config's VersionData so the live config object is never mutated
|
|
||||||
/// </summary>
|
|
||||||
private static VersionData CloneVersionData(VersionData source)
|
|
||||||
{
|
|
||||||
if (source == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var json = JsonConvert.SerializeObject(source);
|
|
||||||
return JsonConvert.DeserializeObject<VersionData>(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Enriches (or creates) the essentials entry from the loaded PepperDash.Essentials.Core assembly
|
|
||||||
/// </summary>
|
|
||||||
private static void PopulateEssentials(VersionData result)
|
|
||||||
{
|
|
||||||
var essentials = result.Essentials ?? new NugetVersion();
|
|
||||||
|
|
||||||
essentials.Version = Global.AssemblyVersion;
|
|
||||||
|
|
||||||
// The main program assembly (PackageId "PepperDashEssentials") is what's actually published
|
|
||||||
// to NuGet, but this handler lives in PepperDash.Essentials.Core, which can't reference that
|
|
||||||
// project's types directly (Essentials -> Core, not the reverse). PluginLoader.EssentialsAssembly.Assembly
|
|
||||||
// is unreliable (often left null - see PluginLoader.SetEssentialsAssembly), so look it up
|
|
||||||
// directly from the loaded AppDomain by its Directory.Build.props-embedded PackageId metadata
|
|
||||||
// (every project's .csproj sets its own PackageId explicitly), falling back to this handler's
|
|
||||||
// own (Core) assembly if it can't be found.
|
|
||||||
var essentialsAssembly = AppDomain.CurrentDomain.GetAssemblies()
|
|
||||||
.FirstOrDefault(a => string.Equals(GetAssemblyMetadataValue(a, "PackageId"), "PepperDashEssentials", StringComparison.OrdinalIgnoreCase))
|
|
||||||
?? typeof(GetPackageManifestRequestHandler).Assembly;
|
|
||||||
|
|
||||||
var repoUrl = TrimTrailingGit(GetAssemblyMetadataValue(essentialsAssembly, "RepositoryUrl"));
|
|
||||||
if (!string.IsNullOrEmpty(repoUrl))
|
|
||||||
{
|
|
||||||
essentials.RepoUrl = repoUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
var name = GetAssemblyProduct(essentialsAssembly);
|
|
||||||
if (!string.IsNullOrEmpty(name))
|
|
||||||
{
|
|
||||||
essentials.Name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefer the PackageId embedded via Directory.Build.props' AssemblyMetadata item (the
|
|
||||||
// authoritative source, matching the actual published PackageId) over any config-supplied
|
|
||||||
// or hardcoded value.
|
|
||||||
var reflectedPackageId = GetAssemblyMetadataValue(essentialsAssembly, "PackageId");
|
|
||||||
if (!string.IsNullOrEmpty(reflectedPackageId))
|
|
||||||
{
|
|
||||||
essentials.PackageId = reflectedPackageId;
|
|
||||||
}
|
|
||||||
else if (string.IsNullOrEmpty(essentials.PackageId))
|
|
||||||
{
|
|
||||||
essentials.PackageId = "PepperDashEssentials";
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Essentials = essentials;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Merges reflection data from loaded plugin assemblies with the config's packages list
|
|
||||||
/// </summary>
|
|
||||||
private static void PopulatePackages(VersionData result)
|
|
||||||
{
|
|
||||||
// Filter out null entries defensively - the packages list is deserialized from user-editable
|
|
||||||
// config JSON, so a malformed "packages": [null, ...] shouldn't throw and 500 the endpoint.
|
|
||||||
var configPackages = (result.Packages ?? new System.Collections.Generic.List<NugetVersion>())
|
|
||||||
.Where(p => p != null)
|
|
||||||
.ToList();
|
|
||||||
var matchedConfigPackages = new System.Collections.Generic.HashSet<NugetVersion>();
|
|
||||||
var mergedPackages = new System.Collections.Generic.List<NugetVersion>();
|
|
||||||
|
|
||||||
foreach (var loaded in PluginLoader.EssentialsPluginAssemblies.Where(a => a.Assembly != null))
|
|
||||||
{
|
|
||||||
var reflectedVersion = loaded.Version;
|
|
||||||
if (string.IsNullOrEmpty(reflectedVersion))
|
|
||||||
{
|
|
||||||
// Never emit an entry with no version - the extension's parser drops entries
|
|
||||||
// whose version isn't a string.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var reflectedRepoUrl = TrimTrailingGit(GetAssemblyMetadataValue(loaded.Assembly, "RepositoryUrl"));
|
|
||||||
var reflectedName = GetAssemblyProduct(loaded.Assembly);
|
|
||||||
|
|
||||||
// Plugins built from a Directory.Build.props that embeds
|
|
||||||
// <AssemblyMetadata Include="PackageId" Value="$(PackageId)" /> carry their PackageId
|
|
||||||
// directly - this is authoritative and should be preferred over the title/name fallback chain.
|
|
||||||
var reflectedPackageId = GetAssemblyMetadataValue(loaded.Assembly, "PackageId");
|
|
||||||
|
|
||||||
var assemblyTitle = GetAssemblyTitle(loaded.Assembly);
|
|
||||||
var assemblyName = loaded.Assembly.GetName().Name;
|
|
||||||
var assemblyNameNoSeriesSuffix = StripTrailingSeriesSuffix(assemblyName);
|
|
||||||
|
|
||||||
var match = configPackages.FirstOrDefault(p =>
|
|
||||||
!matchedConfigPackages.Contains(p) &&
|
|
||||||
!string.IsNullOrEmpty(p.PackageId) &&
|
|
||||||
(string.Equals(p.PackageId, reflectedPackageId, StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
string.Equals(p.PackageId, assemblyTitle, StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
string.Equals(p.PackageId, assemblyName, StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
string.Equals(p.PackageId, assemblyNameNoSeriesSuffix, StringComparison.OrdinalIgnoreCase)));
|
|
||||||
|
|
||||||
if (match != null)
|
|
||||||
{
|
|
||||||
matchedConfigPackages.Add(match);
|
|
||||||
|
|
||||||
mergedPackages.Add(new NugetVersion
|
|
||||||
{
|
|
||||||
Name = !string.IsNullOrEmpty(match.Name) ? match.Name : reflectedName,
|
|
||||||
RepoUrl = !string.IsNullOrEmpty(match.RepoUrl) ? match.RepoUrl : reflectedRepoUrl,
|
|
||||||
PackageId = !string.IsNullOrEmpty(reflectedPackageId) ? reflectedPackageId : match.PackageId,
|
|
||||||
Version = reflectedVersion
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Loaded but not present (or not matched) in config - emit the reflected PackageId
|
|
||||||
// when the assembly carries one, otherwise leave it null as before.
|
|
||||||
mergedPackages.Add(new NugetVersion
|
|
||||||
{
|
|
||||||
Name = reflectedName,
|
|
||||||
RepoUrl = reflectedRepoUrl,
|
|
||||||
PackageId = reflectedPackageId,
|
|
||||||
Version = reflectedVersion,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configured but not currently loaded - pass through unchanged
|
|
||||||
mergedPackages.AddRange(configPackages.Where(p => !matchedConfigPackages.Contains(p)));
|
|
||||||
|
|
||||||
result.Packages = mergedPackages;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetAssemblyMetadataValue(Assembly assembly, string key)
|
|
||||||
{
|
|
||||||
if (assembly == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var match = assembly.GetCustomAttributes(typeof(AssemblyMetadataAttribute), false)
|
|
||||||
.Cast<AssemblyMetadataAttribute>()
|
|
||||||
.FirstOrDefault(a => string.Equals(a.Key, key, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
return match?.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetAssemblyProduct(Assembly assembly)
|
|
||||||
{
|
|
||||||
if (assembly == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var attribute = assembly.GetCustomAttributes(typeof(AssemblyProductAttribute), false)
|
|
||||||
.FirstOrDefault() as AssemblyProductAttribute;
|
|
||||||
|
|
||||||
return attribute?.Product;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetAssemblyTitle(Assembly assembly)
|
|
||||||
{
|
|
||||||
if (assembly == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var attribute = assembly.GetCustomAttributes(typeof(AssemblyTitleAttribute), false)
|
|
||||||
.FirstOrDefault() as AssemblyTitleAttribute;
|
|
||||||
|
|
||||||
return attribute?.Title;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string StripTrailingSeriesSuffix(string assemblyName)
|
|
||||||
{
|
|
||||||
const string suffix = ".4Series";
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(assemblyName) || !assemblyName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return assemblyName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return assemblyName.Substring(0, assemblyName.Length - suffix.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string TrimTrailingGit(string repoUrl)
|
|
||||||
{
|
|
||||||
const string suffix = ".git";
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(repoUrl) || !repoUrl.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return repoUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
return repoUrl.Substring(0, repoUrl.Length - suffix.Length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,178 +0,0 @@
|
||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
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.ContentType = "application/json";
|
|
||||||
context.Response.ContentEncoding = System.Text.Encoding.UTF8;
|
|
||||||
context.Response.Write(JsonConvert.SerializeObject(new { Error = "Unauthorized" }, Formatting.Indented), false);
|
|
||||||
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 = new LoginResponse
|
|
||||||
{
|
|
||||||
UserName = token.UserName,
|
|
||||||
Access = token.Access,
|
|
||||||
State = token.State,
|
|
||||||
Groups = token.Groups,
|
|
||||||
ADConnect = token.ADConnect,
|
|
||||||
Valid = token.Valid
|
|
||||||
}
|
|
||||||
}, 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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a LoginResponse
|
|
||||||
/// </summary>
|
|
||||||
internal class LoginResponse
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the username.
|
|
||||||
/// </summary>
|
|
||||||
public string UserName { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the access level.
|
|
||||||
/// </summary>
|
|
||||||
public Authentication.UserAuthenticationLevelEnum Access { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the token authenticated state.
|
|
||||||
/// </summary>
|
|
||||||
public Authentication.eTokenAuthenticatedState State { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the list of groups.
|
|
||||||
/// </summary>
|
|
||||||
public List<string> Groups { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the active directory connection flag.
|
|
||||||
/// </summary>
|
|
||||||
public int ADConnect { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the valid flag indicating whether the token is valid.
|
|
||||||
/// </summary>
|
|
||||||
public bool Valid { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,224 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using Crestron.SimplSharp;
|
|
||||||
using Crestron.SimplSharp.CrestronIO;
|
|
||||||
using Crestron.SimplSharp.WebScripting;
|
|
||||||
using PepperDash.Core;
|
|
||||||
using PepperDash.Core.Web.RequestHandlers;
|
|
||||||
using Serilog.Events;
|
|
||||||
|
|
||||||
namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Serves the React debug app from the processor's /HTML/debug folder.
|
|
||||||
/// The root route (debug) and all sub-paths (debug/{*filePath}) are handled here.
|
|
||||||
/// Text assets are sent as UTF-8 strings; binary assets are written to the response
|
|
||||||
/// OutputStream. Any sub-path that does not match a real file falls back to
|
|
||||||
/// index.html so that client-side (React Router) routing continues to work.
|
|
||||||
/// </summary>
|
|
||||||
public class ServeDebugAppRequestHandler : WebApiBaseRequestHandler
|
|
||||||
{
|
|
||||||
private static readonly Dictionary<string, string> MimeTypes = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
{ ".html", "text/html; charset=utf-8" },
|
|
||||||
{ ".htm", "text/html; charset=utf-8" },
|
|
||||||
{ ".js", "application/javascript" },
|
|
||||||
{ ".mjs", "application/javascript" },
|
|
||||||
{ ".jsx", "application/javascript" },
|
|
||||||
{ ".css", "text/css" },
|
|
||||||
{ ".json", "application/json" },
|
|
||||||
{ ".map", "application/json" },
|
|
||||||
{ ".svg", "image/svg+xml" },
|
|
||||||
{ ".ico", "image/x-icon" },
|
|
||||||
{ ".png", "image/png" },
|
|
||||||
{ ".jpg", "image/jpeg" },
|
|
||||||
{ ".jpeg", "image/jpeg" },
|
|
||||||
{ ".gif", "image/gif" },
|
|
||||||
{ ".woff", "font/woff" },
|
|
||||||
{ ".woff2","font/woff2" },
|
|
||||||
{ ".ttf", "font/ttf" },
|
|
||||||
{ ".eot", "application/vnd.ms-fontobject" },
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly HashSet<string> TextExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
".html", ".htm", ".js", ".mjs", ".jsx", ".css", ".json", ".map", ".svg", ".txt", ".xml"
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Constructor. CORS is enabled so browser dev-tools requests succeed.
|
|
||||||
/// </summary>
|
|
||||||
public ServeDebugAppRequestHandler() : base(true) { }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles GET requests for the debug app and its static assets.
|
|
||||||
/// </summary>
|
|
||||||
protected override void HandleGet(HttpCwsContext context)
|
|
||||||
{
|
|
||||||
// When acting as the server-level fallback handler, only handle
|
|
||||||
// requests that are actually for the /debug path; defer everything
|
|
||||||
// else to the base class (which returns 501 Not Implemented).
|
|
||||||
var rawUrl = context.Request.RawUrl ?? string.Empty;
|
|
||||||
if (rawUrl.IndexOf("/debug", StringComparison.OrdinalIgnoreCase) < 0)
|
|
||||||
{
|
|
||||||
base.HandleGet(context);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var htmlDebugPath = GetHtmlDebugPath();
|
|
||||||
if (htmlDebugPath == null)
|
|
||||||
{
|
|
||||||
SendResponse(context, 500, "Internal Server Error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var requestedPath = GetRequestedFilePath(context);
|
|
||||||
|
|
||||||
// Paths with no file extension are SPA client-side routes — serve index.html
|
|
||||||
string candidate;
|
|
||||||
if (string.IsNullOrEmpty(requestedPath) || !System.IO.Path.HasExtension(requestedPath))
|
|
||||||
{
|
|
||||||
candidate = System.IO.Path.Combine(htmlDebugPath, "index.html");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var relativePart = requestedPath.Replace('/', System.IO.Path.DirectorySeparatorChar);
|
|
||||||
candidate = System.IO.Path.Combine(htmlDebugPath, relativePart);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve to an absolute path and guard against path-traversal attacks
|
|
||||||
var resolvedCandidate = System.IO.Path.GetFullPath(candidate);
|
|
||||||
var resolvedBase = System.IO.Path.GetFullPath(htmlDebugPath)
|
|
||||||
+ System.IO.Path.DirectorySeparatorChar;
|
|
||||||
|
|
||||||
if (!resolvedCandidate.StartsWith(resolvedBase, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
SendResponse(context, 403, "Forbidden");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Missing static asset → fall back to index.html (SPA deep-link support)
|
|
||||||
if (!File.Exists(resolvedCandidate) && System.IO.Path.HasExtension(requestedPath ?? string.Empty))
|
|
||||||
{
|
|
||||||
resolvedCandidate = System.IO.Path.Combine(htmlDebugPath, "index.html");
|
|
||||||
Debug.LogMessage(LogEventLevel.Debug,
|
|
||||||
"ServeDebugAppRequestHandler: '{requestedPath:l}' not found, falling back to index.html",
|
|
||||||
requestedPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!File.Exists(resolvedCandidate))
|
|
||||||
{
|
|
||||||
SendResponse(context, 404, "Not Found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ext = System.IO.Path.GetExtension(resolvedCandidate);
|
|
||||||
var contentType = MimeTypes.TryGetValue(ext, out var mime) ? mime : "application/octet-stream";
|
|
||||||
|
|
||||||
context.Response.StatusCode = 200;
|
|
||||||
context.Response.StatusDescription = "OK";
|
|
||||||
context.Response.ContentType = contentType;
|
|
||||||
|
|
||||||
if (TextExtensions.Contains(ext))
|
|
||||||
{
|
|
||||||
string content;
|
|
||||||
using (var reader = new StreamReader(resolvedCandidate, Encoding.UTF8))
|
|
||||||
content = reader.ReadToEnd();
|
|
||||||
|
|
||||||
context.Response.ContentEncoding = Encoding.UTF8;
|
|
||||||
context.Response.Write(content, false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var bytes = System.IO.File.ReadAllBytes(resolvedCandidate);
|
|
||||||
context.Response.OutputStream.Write(bytes, 0, bytes.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Response.End();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogMessage(LogEventLevel.Error, ex,
|
|
||||||
"ServeDebugAppRequestHandler: Unhandled error serving '{rawUrl:l}'",
|
|
||||||
context.Request.RawUrl);
|
|
||||||
try { SendResponse(context, 500, "Internal Server Error"); }
|
|
||||||
catch { /* best-effort */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Resolves the absolute path of the /HTML/debug folder on the processor.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <c>Global.FilePathPrefix</c> is always <c>{root}/user/programX/</c> (or
|
|
||||||
/// equivalent), so walking up two parents gives the processor root that
|
|
||||||
/// contains the <c>html</c> folder. This mirrors the two-hop strategy used
|
|
||||||
/// by <c>AssetLoader.ExtractDevToolsZip</c> so that serving and extraction
|
|
||||||
/// always resolve to the same directory.
|
|
||||||
/// </remarks>
|
|
||||||
private static string GetHtmlDebugPath()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var separators = new[] { System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar };
|
|
||||||
var programDir = new DirectoryInfo(Global.FilePathPrefix.TrimEnd(separators));
|
|
||||||
|
|
||||||
// Walk up two levels: {root}/user/programX/ → {root}/user/ → {root}
|
|
||||||
// This matches the path calculation used in AssetLoader.ExtractDevToolsZip.
|
|
||||||
var userOrNvramDir = programDir.Parent;
|
|
||||||
var rootDir = userOrNvramDir?.Parent;
|
|
||||||
|
|
||||||
if (rootDir == null)
|
|
||||||
{
|
|
||||||
Debug.LogMessage(LogEventLevel.Error,
|
|
||||||
"ServeDebugAppRequestHandler: Cannot resolve HTML root from FilePathPrefix '{prefix:l}'",
|
|
||||||
Global.FilePathPrefix);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return System.IO.Path.Combine(rootDir.FullName, "html", "debug");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogMessage(LogEventLevel.Error, ex,
|
|
||||||
"ServeDebugAppRequestHandler: Error resolving HTML debug path");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extracts the file sub-path from the request by parsing <c>RawUrl</c>.
|
|
||||||
/// Returns an empty string when the URL ends at <c>/debug</c> (root hit).
|
|
||||||
/// </summary>
|
|
||||||
private static string GetRequestedFilePath(HttpCwsContext context)
|
|
||||||
{
|
|
||||||
var rawUrl = context.Request.RawUrl ?? string.Empty;
|
|
||||||
|
|
||||||
// Locate the /debug segment in the URL
|
|
||||||
const string debugToken = "/debug";
|
|
||||||
var idx = rawUrl.IndexOf(debugToken, StringComparison.OrdinalIgnoreCase);
|
|
||||||
if (idx < 0)
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
var afterDebug = rawUrl.Substring(idx + debugToken.Length);
|
|
||||||
|
|
||||||
// Strip query string
|
|
||||||
var qIdx = afterDebug.IndexOf('?');
|
|
||||||
if (qIdx >= 0)
|
|
||||||
afterDebug = afterDebug.Substring(0, qIdx);
|
|
||||||
|
|
||||||
// Strip leading slash to get a relative file path
|
|
||||||
return afterDebug.TrimStart('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SendResponse(HttpCwsContext context, int statusCode, string statusDescription)
|
|
||||||
{
|
|
||||||
context.Response.StatusCode = statusCode;
|
|
||||||
context.Response.StatusDescription = statusDescription;
|
|
||||||
context.Response.End();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -17,8 +17,6 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a CameraVisca
|
/// Represents a CameraVisca
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("CameraVisca is no longer supported and will be removed in a future release. Use the CameraVisca plugin instead.")]
|
|
||||||
|
|
||||||
public class CameraVisca : CameraBase, IHasCameraPtzControl, ICommunicationMonitor, IHasCameraPresets, IHasPowerControlWithFeedback, IBridgeAdvanced, IHasCameraFocusControl, IHasAutoFocusMode
|
public class CameraVisca : CameraBase, IHasCameraPtzControl, ICommunicationMonitor, IHasCameraPresets, IHasPowerControlWithFeedback, IBridgeAdvanced, IHasCameraFocusControl, IHasAutoFocusMode
|
||||||
{
|
{
|
||||||
private readonly CameraViscaPropertiesConfig PropertiesConfig;
|
private readonly CameraViscaPropertiesConfig PropertiesConfig;
|
||||||
|
|
|
||||||
|
|
@ -152,15 +152,6 @@ namespace PepperDash.Essentials.Devices.Common.Shades
|
||||||
|
|
||||||
private void IsCoolingDownFeedback_OutputChange(object sender, FeedbackEventArgs e)
|
private void IsCoolingDownFeedback_OutputChange(object sender, FeedbackEventArgs e)
|
||||||
{
|
{
|
||||||
if (Config.DisableAutoRaiseOnPowerOff)
|
|
||||||
{
|
|
||||||
this.LogDebug(
|
|
||||||
"Auto-raise on power-off disabled for {type}; leaving position unchanged (manual control only)",
|
|
||||||
Type
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!DisplayDevice.IsCoolingDownFeedback.BoolValue
|
!DisplayDevice.IsCoolingDownFeedback.BoolValue
|
||||||
&& Type == eScreenLiftControlType.lift
|
&& Type == eScreenLiftControlType.lift
|
||||||
|
|
@ -183,15 +174,6 @@ namespace PepperDash.Essentials.Devices.Common.Shades
|
||||||
{
|
{
|
||||||
if (DisplayDevice.IsWarmingUpFeedback.BoolValue)
|
if (DisplayDevice.IsWarmingUpFeedback.BoolValue)
|
||||||
{
|
{
|
||||||
if (Config.DisableAutoLowerOnPowerOn)
|
|
||||||
{
|
|
||||||
this.LogDebug(
|
|
||||||
"Auto-lower on power-on disabled for {type}; leaving position unchanged (manual control only)",
|
|
||||||
Type
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Lower();
|
Lower();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,23 +41,5 @@ namespace PepperDash.Essentials.Devices.Common.Shades
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("muteOnScreenUp")]
|
[JsonProperty("muteOnScreenUp")]
|
||||||
public bool MuteOnScreenUp { get; set; }
|
public bool MuteOnScreenUp { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// When true, this controller does NOT automatically lower when its assigned display powers on
|
|
||||||
/// (warms up). Manual Raise/Lower still work, and the power-off auto-raise is unaffected. Intended
|
|
||||||
/// for a projector screen that must not auto-drop in a public space for safety, while the projector
|
|
||||||
/// lift (a separate controller) can still drop automatically.
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("disableAutoLowerOnPowerOn")]
|
|
||||||
public bool DisableAutoLowerOnPowerOn { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// When true, this controller does NOT automatically raise when its assigned display powers off
|
|
||||||
/// (cools down). Manual Raise/Lower still work, and the power-on auto-lower is unaffected. The
|
|
||||||
/// companion to <see cref="DisableAutoLowerOnPowerOn"/>; together they make a controller fully
|
|
||||||
/// manual while leaving other controllers (e.g. the lift) on their default automatic behavior.
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("disableAutoRaiseOnPowerOff")]
|
|
||||||
public bool DisableAutoRaiseOnPowerOff { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,6 @@
|
||||||
<ProjectReference Include="..\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj" />
|
<ProjectReference Include="..\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.274" />
|
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -21,7 +21,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a MockVC
|
/// Represents a MockVC
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MockVC : VideoCodecBase, IRoutingSource, IHasCallHistory, IHasScheduleAwareness, IHasCallFavorites, IHasDirectory, IHasCodecCameras, IHasCameraAutoMode, IHasCodecRoomPresets, IRoutingInputs
|
public class MockVC : VideoCodecBase, IRoutingSource, IHasCallHistory, IHasScheduleAwareness, IHasCallFavorites, IHasDirectory, IHasCodecCameras, IHasCameraAutoMode, IHasCodecRoomPresets
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the PropertiesConfig
|
/// Gets or sets the PropertiesConfig
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base class for video codec devices
|
/// Base class for video codec devices
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class VideoCodecBase : ReconfigurableDevice,
|
public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutputs,
|
||||||
IUsageTracking, IHasDialer, IHasContentSharing, ICodecAudio, iVideoCodecInfo, IBridgeAdvanced, IHasStandbyMode
|
IUsageTracking, IHasDialer, IHasContentSharing, ICodecAudio, iVideoCodecInfo, IBridgeAdvanced, IHasStandbyMode
|
||||||
{
|
{
|
||||||
private const int XSigEncoding = 28591;
|
private const int XSigEncoding = 28591;
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The device key
|
/// The device key
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)]
|
[JsonProperty("key")]
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the Key
|
/// Gets or sets the Key
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -19,19 +19,19 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The device name
|
/// The device name
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
|
[JsonProperty("name")]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The type of the message class
|
/// The type of the message class
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("messageType", NullValueHandling = NullValueHandling.Ignore)]
|
[JsonProperty("messageType")]
|
||||||
public string MessageType => GetType().Name;
|
public string MessageType => GetType().Name;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the MessageBasePath
|
/// Gets or sets the MessageBasePath
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("messageBasePath", NullValueHandling = NullValueHandling.Ignore)]
|
[JsonProperty("messageBasePath")]
|
||||||
|
|
||||||
public string MessageBasePath { get; set; }
|
public string MessageBasePath { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
|
@ -12,15 +11,13 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The interfaces implmented by the device sending the messsage
|
/// The interfaces implmented by the device sending the messsage
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("interfaces", NullValueHandling = NullValueHandling.Ignore)]
|
[JsonProperty("interfaces")]
|
||||||
[Obsolete("Interfaces are no longer supported and will be removed in a future release. Interfaces for all devices are now retrieved via the /joinroom endpoint in the MobileControlWebsocketServer")]
|
|
||||||
public List<string> Interfaces { get; private set; }
|
public List<string> Interfaces { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the interfaces implemented by the device sending the message
|
/// Sets the interfaces implemented by the device sending the message
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="interfaces"></param>
|
/// <param name="interfaces"></param>
|
||||||
[Obsolete("SetInterfaces is no longer supported and will be removed in a future release. Interfaces for all devices are now retrieved via the /joinroom endpoint in the MobileControlWebsocketServer")]
|
|
||||||
public void SetInterfaces(List<string> interfaces)
|
public void SetInterfaces(List<string> interfaces)
|
||||||
{
|
{
|
||||||
Interfaces = interfaces;
|
Interfaces = interfaces;
|
||||||
|
|
|
||||||
|
|
@ -264,9 +264,6 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||||
|
|
||||||
message.Name = _device.Name;
|
message.Name = _device.Name;
|
||||||
|
|
||||||
message.MessageBasePath = MessagePath;
|
|
||||||
|
|
||||||
|
|
||||||
var token = JToken.FromObject(message);
|
var token = JToken.FromObject(message);
|
||||||
|
|
||||||
PostStatusMessage(token, MessagePath, clientId);
|
PostStatusMessage(token, MessagePath, clientId);
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
<Compile Remove="Messengers\SIMPLVtcMessenger.cs" />
|
<Compile Remove="Messengers\SIMPLVtcMessenger.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.274" />
|
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />
|
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
using System;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace PepperDash.Essentials
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the version information reported by a connected Mobile Control UI client
|
|
||||||
/// </summary>
|
|
||||||
public class ConnectedClientVersionInfo
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the client id
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("clientId")]
|
|
||||||
public string ClientId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the room key the client joined
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("roomKey")]
|
|
||||||
public string RoomKey { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the touchpanel key the client joined as, if any
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("touchpanelKey")]
|
|
||||||
public string TouchpanelKey { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the app version reported by the client (e.g. the React app's build-time APP_VERSION)
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("appVersion")]
|
|
||||||
public string AppVersion { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the expected app version from the system config's versions.touchpanelWrapperApp, if configured
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("expectedAppVersion")]
|
|
||||||
public string ExpectedAppVersion { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the UTC time the client last reported this version
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("lastSeen")]
|
|
||||||
public DateTime LastSeen { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a copy of this instance, safe for callers outside the owning lock to hold/mutate
|
|
||||||
/// without affecting the internally tracked instance
|
|
||||||
/// </summary>
|
|
||||||
public ConnectedClientVersionInfo Clone() => new ConnectedClientVersionInfo
|
|
||||||
{
|
|
||||||
ClientId = ClientId,
|
|
||||||
RoomKey = RoomKey,
|
|
||||||
TouchpanelKey = TouchpanelKey,
|
|
||||||
AppVersion = AppVersion,
|
|
||||||
ExpectedAppVersion = ExpectedAppVersion,
|
|
||||||
LastSeen = LastSeen
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Converters;
|
using Newtonsoft.Json.Converters;
|
||||||
|
|
||||||
|
|
@ -41,10 +40,9 @@ namespace PepperDash.Essentials
|
||||||
public bool EnableApiServer { get; set; } = true;
|
public bool EnableApiServer { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enables subscriptions for messengers
|
/// Enable subscriptions for Messengers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("enableMessengerSubscriptions")]
|
[JsonProperty("enableMessengerSubscriptions")]
|
||||||
[Obsolete("This property is obsolete and will be removed in a future version. All messengers are now subscription based.")]
|
|
||||||
public bool EnableMessengerSubscriptions { get; set; }
|
public bool EnableMessengerSubscriptions { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -290,4 +288,4 @@ namespace PepperDash.Essentials
|
||||||
/// </summary>
|
/// </summary>
|
||||||
NEO
|
NEO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -69,32 +69,11 @@ namespace PepperDash.Essentials
|
||||||
private readonly Dictionary<string, IMobileControlMessenger> _defaultMessengers =
|
private readonly Dictionary<string, IMobileControlMessenger> _defaultMessengers =
|
||||||
new Dictionary<string, IMobileControlMessenger>();
|
new Dictionary<string, IMobileControlMessenger>();
|
||||||
|
|
||||||
private readonly Dictionary<string, ConnectedClientVersionInfo> _connectedClientVersions =
|
|
||||||
new Dictionary<string, ConnectedClientVersionInfo>(StringComparer.InvariantCultureIgnoreCase);
|
|
||||||
|
|
||||||
private readonly object _connectedClientVersionsLock = new object();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the custom messengers with subscriptions
|
/// Get the custom messengers with subscriptions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ReadOnlyDictionary<string, IMobileControlMessengerWithSubscriptions> Messengers => new ReadOnlyDictionary<string, IMobileControlMessengerWithSubscriptions>(_messengers.Values.OfType<IMobileControlMessengerWithSubscriptions>().ToDictionary(k => k.Key, v => v));
|
public ReadOnlyDictionary<string, IMobileControlMessengerWithSubscriptions> Messengers => new ReadOnlyDictionary<string, IMobileControlMessengerWithSubscriptions>(_messengers.Values.OfType<IMobileControlMessengerWithSubscriptions>().ToDictionary(k => k.Key, v => v));
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the most recently reported UI app version for each connected client, keyed by clientId
|
|
||||||
/// </summary>
|
|
||||||
public ReadOnlyDictionary<string, ConnectedClientVersionInfo> ConnectedClientVersions
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
lock (_connectedClientVersionsLock)
|
|
||||||
{
|
|
||||||
return new ReadOnlyDictionary<string, ConnectedClientVersionInfo>(
|
|
||||||
_connectedClientVersions.ToDictionary(kv => kv.Key, kv => kv.Value.Clone())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the default messengers
|
/// Get the default messengers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -158,7 +137,7 @@ namespace PepperDash.Essentials
|
||||||
"No system_url value defined in config. Checking for value from SIMPL Bridge."
|
"No system_url value defined in config. Checking for value from SIMPL Bridge."
|
||||||
);
|
);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(SystemUrl))
|
if (!string.IsNullOrEmpty(SystemUrl))
|
||||||
{
|
{
|
||||||
this.LogError(
|
this.LogError(
|
||||||
"No system_url value defined in config or SIMPL Bridge. Unable to connect to Mobile Control."
|
"No system_url value defined in config or SIMPL Bridge. Unable to connect to Mobile Control."
|
||||||
|
|
@ -1803,28 +1782,6 @@ namespace PepperDash.Essentials
|
||||||
" Not Enabled in Config.\r\n"
|
" Not Enabled in Config.\r\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var connectedClientVersions = ConnectedClientVersions;
|
|
||||||
|
|
||||||
if (connectedClientVersions.Count == 0)
|
|
||||||
{
|
|
||||||
CrestronConsole.ConsoleCommandResponse("\r\nUI Client App Versions: None reported yet\r\n");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CrestronConsole.ConsoleCommandResponse("\r\nUI Client App Versions:\r\n");
|
|
||||||
foreach (var kv in connectedClientVersions)
|
|
||||||
{
|
|
||||||
var v = kv.Value;
|
|
||||||
var match = string.IsNullOrEmpty(v.ExpectedAppVersion) || string.Equals(v.ExpectedAppVersion, v.AppVersion, StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
CrestronConsole.ConsoleCommandResponse(
|
|
||||||
$" Client: {v.ClientId} Touchpanel: {v.TouchpanelKey} Room: {v.RoomKey}\r\n" +
|
|
||||||
$" Reported: {v.AppVersion} Expected: {(string.IsNullOrEmpty(v.ExpectedAppVersion) ? "(not configured)" : v.ExpectedAppVersion)} Match: {(match ? "Yes" : "NO - MISMATCH")}\r\n" +
|
|
||||||
$" Last Seen (UTC): {v.LastSeen:yyyy-MM-dd HH:mm:ss}\r\n"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -2224,8 +2181,6 @@ namespace PepperDash.Essentials
|
||||||
var roomKey = content["roomKey"].Value<string>();
|
var roomKey = content["roomKey"].Value<string>();
|
||||||
var touchpanelKey = content.SelectToken("touchpanelKey");
|
var touchpanelKey = content.SelectToken("touchpanelKey");
|
||||||
|
|
||||||
TrackClientAppVersion(clientId, roomKey, touchpanelKey?.Value<string>(), content.SelectToken("appVersion")?.Value<string>());
|
|
||||||
|
|
||||||
if (_roomCombiner == null)
|
if (_roomCombiner == null)
|
||||||
{
|
{
|
||||||
var message = new MobileControlMessage
|
var message = new MobileControlMessage
|
||||||
|
|
@ -2297,50 +2252,6 @@ namespace PepperDash.Essentials
|
||||||
SendTouchpanelKey(clientId, touchpanelKey);
|
SendTouchpanelKey(clientId, touchpanelKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Records the app version reported by a connecting UI client (e.g. the mobile control React app's
|
|
||||||
/// build-time APP_VERSION) and compares it against the configured versions.touchpanelWrapperApp version.
|
|
||||||
/// </summary>
|
|
||||||
private void TrackClientAppVersion(string clientId, string roomKey, string touchpanelKey, string appVersion)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(appVersion))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var expectedVersion = ConfigReader.ConfigObject?.Versions?.TouchpanelWrapperApp?.Version;
|
|
||||||
|
|
||||||
var info = new ConnectedClientVersionInfo
|
|
||||||
{
|
|
||||||
ClientId = clientId,
|
|
||||||
RoomKey = roomKey,
|
|
||||||
TouchpanelKey = touchpanelKey,
|
|
||||||
AppVersion = appVersion,
|
|
||||||
ExpectedAppVersion = expectedVersion,
|
|
||||||
LastSeen = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
lock (_connectedClientVersionsLock)
|
|
||||||
{
|
|
||||||
_connectedClientVersions[clientId] = info;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(expectedVersion) && !string.Equals(expectedVersion, appVersion, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
this.LogWarning(
|
|
||||||
"Client {clientId} (touchpanel {touchpanelKey}) reported UI app version {appVersion}, which does not match configured versions.touchpanelWrapperApp version {expectedVersion}",
|
|
||||||
clientId, touchpanelKey, appVersion, expectedVersion
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this.LogVerbose(
|
|
||||||
"Client {clientId} (touchpanel {touchpanelKey}) reported UI app version {appVersion}",
|
|
||||||
clientId, touchpanelKey, appVersion
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SendTouchpanelKey(string clientId, JToken touchpanelKeyToken)
|
private void SendTouchpanelKey(string clientId, JToken touchpanelKeyToken)
|
||||||
{
|
{
|
||||||
if (touchpanelKeyToken == null)
|
if (touchpanelKeyToken == null)
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
<Compile Remove="RoomBridges\SourceDeviceMapDictionary.cs" />
|
<Compile Remove="RoomBridges\SourceDeviceMapDictionary.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.274" />
|
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
||||||
<PackageReference Include="WebSocketSharp-netstandard" Version="1.0.1" />
|
<PackageReference Include="WebSocketSharp-netstandard" Version="1.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -25,28 +25,11 @@ namespace PepperDash.Essentials.Touchpanel
|
||||||
/// Mobile Control touchpanel controller that provides app control, Zoom integration,
|
/// Mobile Control touchpanel controller that provides app control, Zoom integration,
|
||||||
/// and mobile control functionality for Crestron touchpanels.
|
/// and mobile control functionality for Crestron touchpanels.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MobileControlTouchpanelController : TouchpanelBase, IHasFeedback, ITswAppControl, ITswZoomControl, IDeviceInfoProvider, IMobileControlCrestronTouchpanelController, ITheme, ICommunicationMonitor
|
public class MobileControlTouchpanelController : TouchpanelBase, IHasFeedback, ITswAppControl, ITswZoomControl, IDeviceInfoProvider, IMobileControlCrestronTouchpanelController, ITheme
|
||||||
{
|
{
|
||||||
private readonly MobileControlTouchpanelProperties localConfig;
|
private readonly MobileControlTouchpanelProperties localConfig;
|
||||||
private IMobileControlRoomMessenger _bridge;
|
private IMobileControlRoomMessenger _bridge;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the CommunicationMonitor tracking the panel's online/offline state
|
|
||||||
/// </summary>
|
|
||||||
public StatusMonitorBase CommunicationMonitor { get; private set; }
|
|
||||||
|
|
||||||
private sealed class NullCommunicationMonitor : StatusMonitorBase
|
|
||||||
{
|
|
||||||
public NullCommunicationMonitor(IKeyed parent) : base(parent, 120000, 300000)
|
|
||||||
{
|
|
||||||
Status = MonitorStatus.InError;
|
|
||||||
Message = "Panel is not initialized";
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Start() { }
|
|
||||||
public override void Stop() { }
|
|
||||||
}
|
|
||||||
|
|
||||||
private string _appUrl;
|
private string _appUrl;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -145,11 +128,6 @@ namespace PepperDash.Essentials.Touchpanel
|
||||||
{
|
{
|
||||||
localConfig = config;
|
localConfig = config;
|
||||||
|
|
||||||
if (panel != null)
|
|
||||||
{
|
|
||||||
CommunicationMonitor = new CrestronGenericBaseCommunicationMonitor(this, panel, 120000, 300000);
|
|
||||||
}
|
|
||||||
|
|
||||||
AddPostActivationAction(SubscribeForMobileControlUpdates);
|
AddPostActivationAction(SubscribeForMobileControlUpdates);
|
||||||
|
|
||||||
ThemeFeedback = new StringFeedback($"{Key}-theme", () => Theme);
|
ThemeFeedback = new StringFeedback($"{Key}-theme", () => Theme);
|
||||||
|
|
@ -388,8 +366,6 @@ namespace PepperDash.Essentials.Touchpanel
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public override bool CustomActivate()
|
public override bool CustomActivate()
|
||||||
{
|
{
|
||||||
CommunicationMonitor?.Start();
|
|
||||||
|
|
||||||
var appMessenger = new ITswAppControlMessenger($"appControlMessenger-{Key}", $"/device/{Key}", this);
|
var appMessenger = new ITswAppControlMessenger($"appControlMessenger-{Key}", $"/device/{Key}", this);
|
||||||
|
|
||||||
var zoomMessenger = new ITswZoomControlMessenger($"zoomControlMessenger-{Key}", $"/device/{Key}", this);
|
var zoomMessenger = new ITswZoomControlMessenger($"zoomControlMessenger-{Key}", $"/device/{Key}", this);
|
||||||
|
|
@ -417,17 +393,6 @@ namespace PepperDash.Essentials.Touchpanel
|
||||||
return base.CustomActivate();
|
return base.CustomActivate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stops the CommunicationMonitor on deactivation.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>True if deactivation was successful; otherwise, false.</returns>
|
|
||||||
public override bool Deactivate()
|
|
||||||
{
|
|
||||||
CommunicationMonitor?.Stop();
|
|
||||||
|
|
||||||
return base.Deactivate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles device extender signal changes for system reserved signals.
|
/// Handles device extender signal changes for system reserved signals.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -554,8 +519,15 @@ namespace PepperDash.Essentials.Touchpanel
|
||||||
return false;
|
return false;
|
||||||
}) ? csIpAddress.ToString() : processorIp;
|
}) ? csIpAddress.ToString() : processorIp;
|
||||||
|
|
||||||
// replace the host but preserve whatever scheme (http/https) is already present in the URL
|
var match = Regex.Match(url, @"^http://([^:/]+):\d+/mc/app\?token=.+$");
|
||||||
var updatedUrl = Regex.Replace(url, @"^(https?)://[^:/]+", $"$1://{ip}");
|
if (match.Success)
|
||||||
|
{
|
||||||
|
string ipa = match.Groups[1].Value;
|
||||||
|
// ip will be "192.168.1.100"
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace ipa with ip but leave the rest of the string intact
|
||||||
|
var updatedUrl = Regex.Replace(url, @"^http://[^:/]+", $"http://{ip}");
|
||||||
|
|
||||||
this.LogVerbose("Updated URL: {updatedUrl}", updatedUrl);
|
this.LogVerbose("Updated URL: {updatedUrl}", updatedUrl);
|
||||||
|
|
||||||
|
|
@ -768,7 +740,7 @@ namespace PepperDash.Essentials.Touchpanel
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MobileControlTouchpanelControllerFactory()
|
public MobileControlTouchpanelControllerFactory()
|
||||||
{
|
{
|
||||||
TypeNames = new List<string>() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "mctsw1080", "mcts1080", "mcxpanel", "mcdge1000" };
|
TypeNames = new List<string>() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "mcxpanel", "mcdge1000" };
|
||||||
MinimumEssentialsFrameworkVersion = "2.0.0";
|
MinimumEssentialsFrameworkVersion = "2.0.0";
|
||||||
|
|
||||||
factories = new Dictionary<string, Func<uint, CrestronControlSystem, string, BasicTriListWithSmartObject>>
|
factories = new Dictionary<string, Func<uint, CrestronControlSystem, string, BasicTriListWithSmartObject>>
|
||||||
|
|
@ -793,8 +765,6 @@ namespace PepperDash.Essentials.Touchpanel
|
||||||
{"ts770", (id, controlSystem, projectName) => new Ts770(id, controlSystem)},
|
{"ts770", (id, controlSystem, projectName) => new Ts770(id, controlSystem)},
|
||||||
{"tsw1070", (id, controlSystem, projectName) => new Tsw1070(id, controlSystem)},
|
{"tsw1070", (id, controlSystem, projectName) => new Tsw1070(id, controlSystem)},
|
||||||
{"ts1070", (id, controlSystem, projectName) => new Ts1070(id, controlSystem)},
|
{"ts1070", (id, controlSystem, projectName) => new Ts1070(id, controlSystem)},
|
||||||
{"tsw1080", (id, controlSystem, projectName) => new Tsw1080(id, controlSystem)},
|
|
||||||
{"ts1080", (id, controlSystem, projectName) => new Ts1080(id, controlSystem)},
|
|
||||||
{"dge1000", (id, controlSystem, projectName) => new Dge1000(id, controlSystem)}
|
{"dge1000", (id, controlSystem, projectName) => new Dge1000(id, controlSystem)}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -127,16 +127,6 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Port { get; private set; }
|
public int Port { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the HTTP scheme to use for generated URLs, based on whether the direct server is configured as secure
|
|
||||||
/// </summary>
|
|
||||||
private string HttpScheme => _parent.Config.DirectServer.Secure ? "https" : "http";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the WebSocket scheme to use for generated URLs, based on whether the direct server is configured as secure
|
|
||||||
/// </summary>
|
|
||||||
private string WsScheme => _parent.Config.DirectServer.Secure ? "wss" : "ws";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the user app URL prefix
|
/// Gets the user app URL prefix
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -144,8 +134,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
return string.Format("{0}://{1}:{2}{3}?token=",
|
return string.Format("http://{0}:{1}{2}?token=",
|
||||||
HttpScheme,
|
|
||||||
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
|
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
|
||||||
Port,
|
Port,
|
||||||
_userAppBaseHref);
|
_userAppBaseHref);
|
||||||
|
|
@ -249,17 +238,11 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
|
|
||||||
var routes = new List<HttpCwsRoute>
|
var routes = new List<HttpCwsRoute>
|
||||||
{
|
{
|
||||||
new HttpCwsRoute($"device/{Key}/client")
|
new HttpCwsRoute($"devices/{Key}/client")
|
||||||
{
|
{
|
||||||
Name = "ClientHandler",
|
Name = "ClientHandler",
|
||||||
RouteHandler = new UiClientHandler(this)
|
RouteHandler = new UiClientHandler(this)
|
||||||
},
|
},
|
||||||
|
|
||||||
new HttpCwsRoute($"device/{Key}/deleteAllUiClients")
|
|
||||||
{
|
|
||||||
Name = "DeleteAllClientsHandler",
|
|
||||||
RouteHandler = new DeleteAllUiClientsHandler(this)
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
apiServer.AddRoute(routes);
|
apiServer.AddRoute(routes);
|
||||||
|
|
@ -284,7 +267,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
_server = new HttpServer(Port, _parent.Config.DirectServer.Secure);
|
_server = new HttpServer(Port, false);
|
||||||
|
|
||||||
_server.OnGet += Server_OnGet;
|
_server.OnGet += Server_OnGet;
|
||||||
|
|
||||||
|
|
@ -302,7 +285,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
{
|
{
|
||||||
ClientCertificateRequired = false,
|
ClientCertificateRequired = false,
|
||||||
CheckCertificateRevocation = false,
|
CheckCertificateRevocation = false,
|
||||||
EnabledSslProtocols = SslProtocols.Tls12
|
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -414,11 +397,11 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
ip = csIpAddress.ToString();
|
ip = csIpAddress.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
var appUrl = $"{HttpScheme}://{ip}:{Port}/mc/app?token={touchpanel.Key}";
|
var appUrl = $"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}";
|
||||||
|
|
||||||
this.LogVerbose("Sending URL {appUrl} to touchpanel {touchpanelKey}", appUrl, touchpanel.Touchpanel.Key);
|
this.LogVerbose("Sending URL {appUrl} to touchpanel {touchpanelKey}", appUrl, touchpanel.Touchpanel.Key);
|
||||||
|
|
||||||
touchpanel.Touchpanel.SetAppUrl(appUrl);
|
touchpanel.Touchpanel.SetAppUrl($"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -498,7 +481,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
{
|
{
|
||||||
var config = new MobileControlApplicationConfig
|
var config = new MobileControlApplicationConfig
|
||||||
{
|
{
|
||||||
ApiPath = string.Format("{0}://{1}:{2}/mc/api", HttpScheme, processorIp, Port),
|
ApiPath = string.Format("http://{0}:{1}/mc/api", processorIp, _parent.Config.DirectServer.Port),
|
||||||
GatewayAppPath = "",
|
GatewayAppPath = "",
|
||||||
LogoPath = _parent.Config.ApplicationConfig?.LogoPath ?? "logo/logo.png",
|
LogoPath = _parent.Config.ApplicationConfig?.LogoPath ?? "logo/logo.png",
|
||||||
EnableDev = _parent.Config.ApplicationConfig?.EnableDev ?? false,
|
EnableDev = _parent.Config.ApplicationConfig?.EnableDev ?? false,
|
||||||
|
|
@ -925,7 +908,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes all clients from the server
|
/// Removes all clients from the server
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void RemoveAllTokens(string s)
|
private void RemoveAllTokens(string s)
|
||||||
{
|
{
|
||||||
if (s == "?" || string.IsNullOrEmpty(s))
|
if (s == "?" || string.IsNullOrEmpty(s))
|
||||||
{
|
{
|
||||||
|
|
@ -1109,7 +1092,6 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
res.StatusCode = 200;
|
res.StatusCode = 200;
|
||||||
res.Close();
|
res.Close();
|
||||||
|
|
||||||
// remote log collector has no dedicated secure flag; keep it on http regardless of DirectServer.Secure
|
|
||||||
var logRequest = new HttpRequestMessage(HttpMethod.Post, $"http://{_parent.Config.DirectServer.Logging.Host}:{_parent.Config.DirectServer.Logging.Port}/logs")
|
var logRequest = new HttpRequestMessage(HttpMethod.Post, $"http://{_parent.Config.DirectServer.Logging.Host}:{_parent.Config.DirectServer.Logging.Port}/logs")
|
||||||
{
|
{
|
||||||
Content = new StringContent(body, Encoding.UTF8, "application/json"),
|
Content = new StringContent(body, Encoding.UTF8, "application/json"),
|
||||||
|
|
@ -1162,11 +1144,6 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
var qp = req.QueryString;
|
var qp = req.QueryString;
|
||||||
var token = qp["token"];
|
var token = qp["token"];
|
||||||
|
|
||||||
// Each join mints a single-use clientId; the panel webview must never replay a cached
|
|
||||||
// response, or it reconnects forever with an already-consumed id (1008 loop).
|
|
||||||
res.Headers.Add("Cache-Control", "no-store");
|
|
||||||
res.Headers.Add("Pragma", "no-cache");
|
|
||||||
|
|
||||||
this.LogVerbose("Join Room Request with token: {token}", token);
|
this.LogVerbose("Join Room Request with token: {token}", token);
|
||||||
|
|
||||||
byte[] body;
|
byte[] body;
|
||||||
|
|
@ -1230,7 +1207,8 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
this.LogVerbose("Assigning ClientId: {clientId} for token: {token} at {timestamp}", clientId, token, now);
|
this.LogVerbose("Assigning ClientId: {clientId} for token: {token} at {timestamp}", clientId, token, now);
|
||||||
|
|
||||||
// Construct WebSocket URL with clientId query parameter
|
// Construct WebSocket URL with clientId query parameter
|
||||||
var wsUrl = $"{WsScheme}://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{Port}{_wsPath}{token}?clientId={clientId}";
|
var wsProtocol = "ws";
|
||||||
|
var wsUrl = $"{wsProtocol}://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{Port}{_wsPath}{token}?clientId={clientId}";
|
||||||
|
|
||||||
// Construct the response object
|
// Construct the response object
|
||||||
JoinResponse jRes = new JoinResponse
|
JoinResponse jRes = new JoinResponse
|
||||||
|
|
@ -1242,8 +1220,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
Config = _parent.GetConfigWithPluginVersion(),
|
Config = _parent.GetConfigWithPluginVersion(),
|
||||||
CodeExpires = new DateTime().AddYears(1),
|
CodeExpires = new DateTime().AddYears(1),
|
||||||
UserCode = bridge.UserCode,
|
UserCode = bridge.UserCode,
|
||||||
UserAppUrl = string.Format("{0}://{1}:{2}/mc/app",
|
UserAppUrl = string.Format("http://{0}:{1}/mc/app",
|
||||||
HttpScheme,
|
|
||||||
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
|
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
|
||||||
Port),
|
Port),
|
||||||
WebSocketUrl = wsUrl,
|
WebSocketUrl = wsUrl,
|
||||||
|
|
@ -1268,8 +1245,6 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
{
|
{
|
||||||
res.StatusCode = 200;
|
res.StatusCode = 200;
|
||||||
res.ContentType = "application/json";
|
res.ContentType = "application/json";
|
||||||
res.Headers.Add("Cache-Control", "no-store");
|
|
||||||
res.Headers.Add("Pragma", "no-cache");
|
|
||||||
var version = new Version() { ServerVersion = _parent.GetConfigWithPluginVersion().RuntimeInfo.PluginVersion };
|
var version = new Version() { ServerVersion = _parent.GetConfigWithPluginVersion().RuntimeInfo.PluginVersion };
|
||||||
var message = JsonConvert.SerializeObject(version);
|
var message = JsonConvert.SerializeObject(version);
|
||||||
this.LogVerbose("{message}", message);
|
this.LogVerbose("{message}", message);
|
||||||
|
|
|
||||||
|
|
@ -1,257 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Crestron.SimplSharp;
|
using Crestron.SimplSharp;
|
||||||
|
|
@ -27,8 +28,6 @@ namespace PepperDash.Essentials
|
||||||
private CEvent _initializeEvent;
|
private CEvent _initializeEvent;
|
||||||
private const long StartupTime = 500;
|
private const long StartupTime = 500;
|
||||||
|
|
||||||
// private const string minimumFirmwareVersion = "2.8006.00110";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the ControlSystem class
|
/// Initializes a new instance of the ControlSystem class
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -48,24 +47,6 @@ namespace PepperDash.Essentials
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void InitializeSystem()
|
public override void InitializeSystem()
|
||||||
{
|
{
|
||||||
|
|
||||||
// Get FW version and stop if it's too low to run this version of Essentials. Must be greater than v2.8006.00110
|
|
||||||
// var fwVersion = InitialParametersClass.FirmwareVersion;
|
|
||||||
|
|
||||||
// Debug.LogInformation("Control System Hardware Version: {fwVersion}", fwVersion);
|
|
||||||
|
|
||||||
// // split the version into parts and compare against minimumFirmwareVersion
|
|
||||||
// var versionParts = fwVersion.Split('.').Select(int.Parse).ToArray();
|
|
||||||
// var minParts = minimumFirmwareVersion.Split('.').Select(int.Parse).ToArray();
|
|
||||||
// if (versionParts.Length < minParts.Length
|
|
||||||
// || versionParts[0] < minParts[0]
|
|
||||||
// || (versionParts[0] == minParts[0] && versionParts[1] < minParts[1])
|
|
||||||
// || (versionParts[0] == minParts[0] && versionParts[1] == minParts[1] && versionParts[2] <= minParts[2]))
|
|
||||||
// {
|
|
||||||
// Debug.LogFatal("Firmware version {fwVersion} is too low to run this version of Essentials. Please upgrade to greater than v{minimumFirmwareVersion}.", fwVersion, minimumFirmwareVersion);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// If the control system is a DMPS type, we need to wait to exit this method until all devices have had time to activate
|
// If the control system is a DMPS type, we need to wait to exit this method until all devices have had time to activate
|
||||||
// to allow any HD-BaseT DM endpoints to register first.
|
// to allow any HD-BaseT DM endpoints to register first.
|
||||||
bool preventInitializationComplete = Global.ControlSystemIsDmpsType;
|
bool preventInitializationComplete = Global.ControlSystemIsDmpsType;
|
||||||
|
|
@ -111,16 +92,12 @@ namespace PepperDash.Essentials
|
||||||
|
|
||||||
CrestronConsole.AddNewConsoleCommand(s => Debug.LogMessage(LogEventLevel.Information, "CONSOLE MESSAGE: {0}", s), "appdebugmessage", "Writes message to log", ConsoleAccessLevelEnum.AccessOperator);
|
CrestronConsole.AddNewConsoleCommand(s => Debug.LogMessage(LogEventLevel.Information, "CONSOLE MESSAGE: {0}", s), "appdebugmessage", "Writes message to log", ConsoleAccessLevelEnum.AccessOperator);
|
||||||
|
|
||||||
CrestronConsole.AddNewConsoleCommand(ListTieLines,
|
CrestronConsole.AddNewConsoleCommand(s =>
|
||||||
"listtielines", "Prints out all tie lines. Usage: listtielines [signaltype]", ConsoleAccessLevelEnum.AccessOperator);
|
{
|
||||||
|
foreach (var tl in TieLineCollection.Default)
|
||||||
CrestronConsole.AddNewConsoleCommand(VisualizeRoutes, "visualizeroutes",
|
CrestronConsole.ConsoleCommandResponse(" {0}{1}", tl, CrestronEnvironment.NewLine);
|
||||||
"Visualizes routes by signal type",
|
},
|
||||||
ConsoleAccessLevelEnum.AccessOperator);
|
"listtielines", "Prints out all tie lines", ConsoleAccessLevelEnum.AccessOperator);
|
||||||
|
|
||||||
CrestronConsole.AddNewConsoleCommand(VisualizeCurrentRoutes, "visualizecurrentroutes",
|
|
||||||
"Visualizes current active routes from DefaultCollection",
|
|
||||||
ConsoleAccessLevelEnum.AccessOperator);
|
|
||||||
|
|
||||||
CrestronConsole.AddNewConsoleCommand(s =>
|
CrestronConsole.AddNewConsoleCommand(s =>
|
||||||
{
|
{
|
||||||
|
|
@ -130,8 +107,14 @@ namespace PepperDash.Essentials
|
||||||
(ConfigReader.ConfigObject, Newtonsoft.Json.Formatting.Indented).Replace(Environment.NewLine, "\r\n"));
|
(ConfigReader.ConfigObject, Newtonsoft.Json.Formatting.Indented).Replace(Environment.NewLine, "\r\n"));
|
||||||
}, "showconfig", "Shows the current running merged config", ConsoleAccessLevelEnum.AccessOperator);
|
}, "showconfig", "Shows the current running merged config", ConsoleAccessLevelEnum.AccessOperator);
|
||||||
|
|
||||||
CrestronConsole.AddNewConsoleCommand(
|
CrestronConsole.AddNewConsoleCommand(s =>
|
||||||
PrintPortalInfo,
|
CrestronConsole.ConsoleCommandResponse(
|
||||||
|
"This system can be found at the following URLs:{2}" +
|
||||||
|
"System URL: {0}{2}" +
|
||||||
|
"Template URL: {1}{2}",
|
||||||
|
ConfigReader.ConfigObject.SystemUrl,
|
||||||
|
ConfigReader.ConfigObject.TemplateUrl,
|
||||||
|
CrestronEnvironment.NewLine),
|
||||||
"portalinfo",
|
"portalinfo",
|
||||||
"Shows portal URLS from configuration",
|
"Shows portal URLS from configuration",
|
||||||
ConsoleAccessLevelEnum.AccessOperator);
|
ConsoleAccessLevelEnum.AccessOperator);
|
||||||
|
|
@ -154,29 +137,6 @@ namespace PepperDash.Essentials
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PrintPortalInfo(string args)
|
|
||||||
{
|
|
||||||
if(ConfigReader.ConfigObject == null)
|
|
||||||
{
|
|
||||||
CrestronConsole.ConsoleCommandResponse("No configuration loaded. Cannot show portal URLs.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(ConfigReader.ConfigObject.SystemUrl) && string.IsNullOrEmpty(ConfigReader.ConfigObject.TemplateUrl))
|
|
||||||
{
|
|
||||||
CrestronConsole.ConsoleCommandResponse("No portal URLs defined in config.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CrestronConsole.ConsoleCommandResponse(
|
|
||||||
"This system can be found at the following URLs:{2}" +
|
|
||||||
"System URL: {0}{2}" +
|
|
||||||
"Template URL: {1}{2}",
|
|
||||||
ConfigReader.ConfigObject?.SystemUrl,
|
|
||||||
ConfigReader.ConfigObject?.TemplateUrl,
|
|
||||||
CrestronEnvironment.NewLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// DeterminePlatform method
|
/// DeterminePlatform method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -274,8 +234,13 @@ namespace PepperDash.Essentials
|
||||||
PluginLoader.AddProgramAssemblies();
|
PluginLoader.AddProgramAssemblies();
|
||||||
|
|
||||||
_ = new Core.DeviceFactory();
|
_ = new Core.DeviceFactory();
|
||||||
|
// _ = new Devices.Common.DeviceFactory();
|
||||||
|
// _ = new DeviceFactory();
|
||||||
|
|
||||||
LoadAssets(Global.ApplicationDirectoryPathPrefix, Global.FilePathPrefix);
|
// _ = new ProcessorExtensionDeviceFactory();
|
||||||
|
// _ = new MobileControlFactory();
|
||||||
|
|
||||||
|
LoadAssets();
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Starting Essentials load from configuration");
|
Debug.LogMessage(LogEventLevel.Information, "Starting Essentials load from configuration");
|
||||||
|
|
||||||
|
|
@ -286,9 +251,10 @@ namespace PepperDash.Essentials
|
||||||
PluginLoader.LoadPlugins();
|
PluginLoader.LoadPlugins();
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Folder structure verified. Loading config...");
|
Debug.LogMessage(LogEventLevel.Information, "Folder structure verified. Loading config...");
|
||||||
if (!ConfigReader.LoadConfig2() || ConfigReader.ConfigObject == null)
|
if (!ConfigReader.LoadConfig2())
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Warning, "Unable to load config file.");
|
Debug.LogMessage(LogEventLevel.Information, "Essentials Load complete with errors");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Load();
|
Load();
|
||||||
|
|
@ -410,12 +376,6 @@ namespace PepperDash.Essentials
|
||||||
new Core.Monitoring.SystemMonitorController("systemMonitor"));
|
new Core.Monitoring.SystemMonitorController("systemMonitor"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ConfigReader.ConfigObject is null)
|
|
||||||
{
|
|
||||||
Debug.LogMessage(LogEventLevel.Warning, "LoadDevices: ConfigObject is null. Cannot load devices.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var devConf in ConfigReader.ConfigObject.Devices)
|
foreach (var devConf in ConfigReader.ConfigObject.Devices)
|
||||||
{
|
{
|
||||||
IKeyed newDev = null;
|
IKeyed newDev = null;
|
||||||
|
|
@ -469,7 +429,7 @@ namespace PepperDash.Essentials
|
||||||
|
|
||||||
var tlc = TieLineCollection.Default;
|
var tlc = TieLineCollection.Default;
|
||||||
|
|
||||||
if (ConfigReader.ConfigObject?.TieLines == null)
|
if (ConfigReader.ConfigObject.TieLines == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -483,282 +443,6 @@ namespace PepperDash.Essentials
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "All Tie Lines Loaded.");
|
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>
|
/// <summary>
|
||||||
|
|
@ -766,7 +450,7 @@ namespace PepperDash.Essentials
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void LoadRooms()
|
public void LoadRooms()
|
||||||
{
|
{
|
||||||
if (ConfigReader.ConfigObject?.Rooms == null)
|
if (ConfigReader.ConfigObject.Rooms == null)
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Notice: Configuration contains no rooms - Is this intentional? This may be a valid configuration.");
|
Debug.LogMessage(LogEventLevel.Information, "Notice: Configuration contains no rooms - Is this intentional? This may be a valid configuration.");
|
||||||
return;
|
return;
|
||||||
|
|
@ -801,16 +485,15 @@ namespace PepperDash.Essentials
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fires up a logo server if not already running
|
/// Fires up a logo server if not already running
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Logo server is no longer supported and will be removed in a future release.")]
|
|
||||||
void LoadLogoServer()
|
void LoadLogoServer()
|
||||||
{
|
{
|
||||||
if (ConfigReader.ConfigObject?.Rooms == null)
|
if (ConfigReader.ConfigObject.Rooms == null)
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, "No rooms configured. Bypassing Logo server startup.");
|
Debug.LogMessage(LogEventLevel.Information, "No rooms configured. Bypassing Logo server startup.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ConfigReader.ConfigObject?.Rooms == null ||
|
if (
|
||||||
!ConfigReader.ConfigObject.Rooms.Any(
|
!ConfigReader.ConfigObject.Rooms.Any(
|
||||||
CheckRoomConfig))
|
CheckRoomConfig))
|
||||||
{
|
{
|
||||||
|
|
@ -861,8 +544,142 @@ namespace PepperDash.Essentials
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void LoadAssets(string applicationDirectoryPath, string filePathPrefix) =>
|
private static void LoadAssets()
|
||||||
AssetLoader.Load(applicationDirectoryPath, filePathPrefix);
|
{
|
||||||
|
var applicationDirectory = new DirectoryInfo(Global.ApplicationDirectoryPathPrefix);
|
||||||
|
Debug.LogMessage(LogEventLevel.Information, "Searching: {applicationDirectory:l} for embedded assets - {Destination}", applicationDirectory.FullName, Global.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Crestron.SimplSharp.SDK.Program" Version="2.21.274" />
|
<PackageReference Include="Crestron.SimplSharp.SDK.Program" Version="2.21.90" />
|
||||||
<PackageReference Include="System.IO.Compression" Version="4.0.0" />
|
<PackageReference Include="System.IO.Compression" Version="4.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue