mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-08-31 19:08:29 +00:00
Compare commits
128 commits
v2.29.0-ti
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f05654638 | ||
|
|
4ca59e3186 | ||
|
|
a55e4811b3 | ||
|
|
5cac0bb6c8 | ||
|
|
44e5ff4c3e | ||
|
|
ec9753b19e | ||
|
|
12e5ff752d | ||
|
|
85a4b635f6 | ||
|
|
aff4c1e897 | ||
|
|
3afc73a446 | ||
|
|
3c9f5e95e0 | ||
|
|
f70c78bcc5 | ||
|
|
5b5619764f | ||
|
|
d21cef0e60 | ||
|
|
1719de195f | ||
|
|
6949253b1a | ||
|
|
e23c987516 | ||
|
|
14cd082076 | ||
|
|
36c15b8d95 | ||
|
|
fbf4c56403 | ||
|
|
f4cb17314d | ||
|
|
aa76551500 | ||
|
|
12828e8dd4 | ||
|
|
4dca16e9bd | ||
|
|
d4284bd59f | ||
|
|
a8ec831064 | ||
|
|
6ceddcf571 | ||
|
|
02f507ccb1 | ||
|
|
22ef9cf7c2 | ||
|
|
02216372bc | ||
|
|
c323c872fc | ||
|
|
1e63b10aa4 | ||
|
|
47e186d8f4 | ||
|
|
2f7789b374 | ||
|
|
588057f3ba | ||
|
|
64d60dacc2 | ||
|
|
f34d7223e0 | ||
|
|
68c44e46ae | ||
|
|
4f1eb979d3 | ||
|
|
8ac4eb7584 | ||
|
|
0240887d93 | ||
|
|
640bd7a8a7 | ||
|
|
2fac0ca926 | ||
|
|
af5611e403 | ||
|
|
3286d27898 | ||
|
|
8d3edde28c | ||
|
|
a220474101 | ||
|
|
b982219e2f | ||
|
|
e03e45ccf5 | ||
|
|
a7b839296e | ||
|
|
a63da82cc3 | ||
|
|
3974455337 | ||
|
|
782bb6c057 | ||
|
|
907eb2f397 | ||
|
|
72a4c63a06 | ||
|
|
5f26cb98fd | ||
|
|
6a4e3d5be2 | ||
|
|
d18fca8a98 | ||
|
|
162a06f9e9 | ||
|
|
75587c361f | ||
|
|
c3511cd1a6 | ||
|
|
b32bab0d33 | ||
|
|
1ce86dab29 | ||
|
|
77c700c565 | ||
|
|
aa050121ae | ||
|
|
abef1d095f | ||
|
|
74a12c9671 | ||
|
|
92d13f29a9 | ||
|
|
f658fdf363 | ||
|
|
6587444970 | ||
|
|
94c9e75d8f | ||
|
|
469999b6f7 | ||
|
|
08aba35334 | ||
|
|
68fe205504 | ||
|
|
21217341b5 | ||
|
|
e1a5c32c1f | ||
|
|
18f7000d76 | ||
|
|
d47cfd5e62 | ||
|
|
4f2d2ca746 | ||
|
|
e6583f7824 | ||
|
|
e57bc43a10 | ||
|
|
b83af26b77 | ||
|
|
c9f5af184b | ||
|
|
beb77ec468 | ||
|
|
b318e7f365 | ||
|
|
a9dd57fdaf | ||
|
|
d879430616 | ||
|
|
1a5840c29a | ||
|
|
6fbdaa9ca0 | ||
|
|
e5e1802da9 | ||
|
|
17adac639e | ||
|
|
2607180ab5 | ||
|
|
f1ef479301 | ||
|
|
8ba993ed66 | ||
|
|
9fc866741e | ||
|
|
f3ea9e1d5a | ||
|
|
404728c708 | ||
|
|
c050bb4eb3 | ||
|
|
2530003a58 | ||
|
|
c52d585a0c | ||
|
|
89c23f5432 | ||
|
|
841279eb0c | ||
|
|
1b32761e8e | ||
|
|
507130c1ae | ||
|
|
1ecfca7e5a | ||
|
|
dd32f44e3d | ||
|
|
ba48f23a71 | ||
|
|
447af3883c | ||
|
|
3b57860123 | ||
|
|
b1a575e4d2 | ||
|
|
2fdd73498a | ||
|
|
ee240ca378 | ||
|
|
f31f0611f1 | ||
|
|
0a991cffb0 | ||
|
|
18088d37a1 | ||
|
|
4f86009209 | ||
|
|
2197dc489d | ||
|
|
db14a614bc | ||
|
|
4a59cf9f81 | ||
|
|
c820052b41 | ||
|
|
bf517ad8b8 | ||
|
|
e55ac38719 | ||
|
|
d0ab42ccb7 | ||
|
|
1b10910cc2 | ||
|
|
84c730b7a1 | ||
|
|
6db7581295 | ||
|
|
76311b83ff | ||
|
|
8817d70f07 |
58 changed files with 2768 additions and 577 deletions
|
|
@ -183,11 +183,12 @@ namespace PepperDash.Core
|
|||
Cresnet = 8,
|
||||
Cec = 9,
|
||||
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```, 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```, ```UdpClient```, and ```Udp```.
|
||||
|
||||
##### ComParams
|
||||
|
||||
|
|
@ -287,7 +288,7 @@ This property maps to the number of the port on the device you have mapped the r
|
|||
|
||||
##### TcpSshParams
|
||||
|
||||
A ```Ssh```, ```TcpIp```, or ```Udp``` device requires a ```tcpSshProperties``` object to set the propeties of the socket.
|
||||
A ```Ssh```, ```TcpIp```, ```UdpClient```, or ```Udp``` device requires a ```tcpSshProperties``` object to set the propeties of the socket.
|
||||
|
||||
```Json
|
||||
{
|
||||
|
|
@ -304,7 +305,7 @@ A ```Ssh```, ```TcpIp```, or ```Udp``` device requires a ```tcpSshProperties```
|
|||
|
||||
**```address```**
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
**```port```**
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>2.29.0-local</Version>
|
||||
<Version>2.42.1-local</Version>
|
||||
<InformationalVersion>$(Version)</InformationalVersion>
|
||||
<Authors>PepperDash Technology</Authors>
|
||||
<Company>PepperDash Technology</Company>
|
||||
<Product>PepperDash Essentials</Product>
|
||||
<Copyright>Copyright © 2025</Copyright>
|
||||
<Copyright>Copyright © 2026</Copyright>
|
||||
<RepositoryUrl>https://github.com/PepperDash/Essentials</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageTags>Crestron; 4series</PackageTags>
|
||||
|
|
@ -20,4 +20,7 @@
|
|||
<None Include="..\..\LICENSE.md" Pack="true" PackagePath=""/>
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath=""/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AssemblyMetadata Include="PackageId" Value="$(PackageId)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -151,6 +151,8 @@ namespace PepperDash.Core
|
|||
// Thread-safety lock for state changes
|
||||
private readonly object _stateLock = new object();
|
||||
|
||||
private volatile bool _isProgramStopping;
|
||||
|
||||
private bool disconnectLogged = false;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -207,11 +209,9 @@ namespace PepperDash.Core
|
|||
{
|
||||
if (programEventType == eProgramStatusEventType.Stopping)
|
||||
{
|
||||
if (client != null)
|
||||
{
|
||||
this.LogDebug("Program stopping. Closing connection");
|
||||
Disconnect();
|
||||
}
|
||||
_isProgramStopping = true;
|
||||
this.LogDebug("Program stopping. Closing connection");
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -228,6 +228,12 @@ namespace PepperDash.Core
|
|||
return;
|
||||
}
|
||||
|
||||
if (_isProgramStopping)
|
||||
{
|
||||
this.LogDebug("Skipping connect because program is stopping");
|
||||
return;
|
||||
}
|
||||
|
||||
ConnectEnabled = true;
|
||||
|
||||
try
|
||||
|
|
@ -287,13 +293,7 @@ namespace PepperDash.Core
|
|||
}
|
||||
catch (SshConnectionException e)
|
||||
{
|
||||
var ie = e.InnerException; // The details are inside!!
|
||||
|
||||
if (ie is SocketException)
|
||||
{
|
||||
this.LogError("CONNECTION failure: Cannot reach host");
|
||||
this.LogVerbose(ie, "Exception details: ");
|
||||
}
|
||||
var ie = e.InnerException; // The details are inside, when present - remote can close the connection with no inner exception at all
|
||||
|
||||
if (ie is System.Net.Sockets.SocketException socketException)
|
||||
{
|
||||
|
|
@ -301,20 +301,20 @@ namespace PepperDash.Core
|
|||
Hostname, Port);
|
||||
this.LogVerbose(socketException, "SocketException details: ");
|
||||
}
|
||||
if (ie is SshAuthenticationException)
|
||||
else if (ie is SshAuthenticationException)
|
||||
{
|
||||
this.LogError("Authentication failure for username {userName}", Username);
|
||||
this.LogVerbose(ie, "AuthenticationException details: ");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LogError("Error on connect: {error}", ie.Message);
|
||||
this.LogVerbose(ie, "Exception details: ");
|
||||
this.LogError("Error on connect: {error}", ie?.Message ?? e.Message);
|
||||
this.LogVerbose(ie ?? e, "Exception details: ");
|
||||
}
|
||||
|
||||
disconnectLogged = true;
|
||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||
if (AutoReconnect)
|
||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
||||
{
|
||||
this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||
StartReconnectTimer();
|
||||
|
|
@ -326,7 +326,7 @@ namespace PepperDash.Core
|
|||
|
||||
disconnectLogged = true;
|
||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||
if (AutoReconnect)
|
||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
||||
{
|
||||
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||
StartReconnectTimer();
|
||||
|
|
@ -338,7 +338,7 @@ namespace PepperDash.Core
|
|||
this.LogVerbose(e, "Exception details: ");
|
||||
disconnectLogged = true;
|
||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||
if (AutoReconnect)
|
||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
||||
{
|
||||
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||
StartReconnectTimer();
|
||||
|
|
@ -473,7 +473,7 @@ namespace PepperDash.Core
|
|||
{
|
||||
connectLock.Release();
|
||||
}
|
||||
if (AutoReconnect && ConnectEnabled)
|
||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
||||
{
|
||||
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||
StartReconnectTimer();
|
||||
|
|
@ -516,7 +516,10 @@ namespace PepperDash.Core
|
|||
this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim());
|
||||
|
||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||
StartReconnectTimer();
|
||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
||||
{
|
||||
StartReconnectTimer();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -549,7 +552,10 @@ namespace PepperDash.Core
|
|||
this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes));
|
||||
|
||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||
StartReconnectTimer();
|
||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
||||
{
|
||||
StartReconnectTimer();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
|||
463
src/PepperDash.Core/Comm/GenericUdpClient.cs
Normal file
463
src/PepperDash.Core/Comm/GenericUdpClient.cs
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
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,70 +18,75 @@ namespace PepperDash.Core
|
|||
/// <summary>
|
||||
/// RS232/422/485
|
||||
/// </summary>
|
||||
Com,
|
||||
Com = 1,
|
||||
/// <summary>
|
||||
/// Crestron IpId (most Crestron ethernet devices)
|
||||
/// </summary>
|
||||
IpId,
|
||||
IpId = 2,
|
||||
/// <summary>
|
||||
/// Crestron IpIdTcp (HD-MD series, etc.)
|
||||
/// </summary>
|
||||
IpidTcp,
|
||||
IpidTcp = 3,
|
||||
/// <summary>
|
||||
/// Crestron IR control
|
||||
/// </summary>
|
||||
IR,
|
||||
IR = 4,
|
||||
/// <summary>
|
||||
/// SSH client
|
||||
/// </summary>
|
||||
Ssh,
|
||||
Ssh = 5,
|
||||
/// <summary>
|
||||
/// TCP/IP client
|
||||
/// </summary>
|
||||
Tcpip,
|
||||
Tcpip = 6,
|
||||
/// <summary>
|
||||
/// Telnet
|
||||
/// </summary>
|
||||
Telnet,
|
||||
Telnet = 7,
|
||||
/// <summary>
|
||||
/// Crestnet device
|
||||
/// </summary>
|
||||
Cresnet,
|
||||
Cresnet = 8,
|
||||
/// <summary>
|
||||
/// CEC Control, via a DM HDMI port
|
||||
/// </summary>
|
||||
Cec,
|
||||
Cec = 9,
|
||||
/// <summary>
|
||||
/// UDP Server
|
||||
/// </summary>
|
||||
Udp,
|
||||
Udp = 10,
|
||||
|
||||
/// <summary>
|
||||
/// HTTP client
|
||||
/// </summary>
|
||||
Http,
|
||||
Http = 11,
|
||||
/// <summary>
|
||||
/// HTTPS client
|
||||
/// </summary>
|
||||
Https,
|
||||
Https = 12,
|
||||
/// <summary>
|
||||
/// Websocket client
|
||||
/// </summary>
|
||||
Ws,
|
||||
Ws = 13,
|
||||
/// <summary>
|
||||
/// Secure Websocket client
|
||||
/// </summary>
|
||||
Wss,
|
||||
Wss = 14,
|
||||
/// <summary>
|
||||
/// Secure TCP/IP
|
||||
/// </summary>
|
||||
SecureTcpIp,
|
||||
SecureTcpIp = 15,
|
||||
/// <summary>
|
||||
/// Used when comms needs to be handled in SIMPL and bridged opposite the normal direction
|
||||
/// </summary>
|
||||
ComBridge,
|
||||
ComBridge = 16,
|
||||
/// <summary>
|
||||
/// InfinetEX control
|
||||
/// </summary>
|
||||
InfinetEx
|
||||
InfinetEx = 17,
|
||||
/// <summary>
|
||||
/// UDP client
|
||||
/// </summary>
|
||||
UdpClient = 18,
|
||||
}
|
||||
}
|
||||
|
|
@ -5,9 +5,9 @@ using Serilog.Events;
|
|||
namespace PepperDash.Core
|
||||
{
|
||||
//*********************************************************************************************************
|
||||
/// <summary>
|
||||
/// Represents a Device
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Represents a Device
|
||||
/// </summary>
|
||||
public class Device : IKeyName
|
||||
{
|
||||
|
||||
|
|
@ -15,25 +15,15 @@ namespace PepperDash.Core
|
|||
/// Unique Key
|
||||
/// </summary>
|
||||
public string Key { get; protected set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the Name
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Gets or sets the Name
|
||||
/// </summary>
|
||||
public string Name { get; protected set; }
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool Enabled { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// A place to store reference to the original config object, if any. These values should
|
||||
/// NOT be used as properties on the device as they are all publicly-settable values.
|
||||
/// </summary>
|
||||
//public DeviceConfig Config { get; private set; }
|
||||
/// <summary>
|
||||
/// Helper method to check if Config exists
|
||||
/// </summary>
|
||||
//public bool HasConfig { get { return Config != null; } }
|
||||
|
||||
List<Action> _PreActivationActions;
|
||||
List<Action> _PostActivationActions;
|
||||
|
||||
|
|
@ -86,9 +76,9 @@ namespace PepperDash.Core
|
|||
/// Adds a post activation action
|
||||
/// </summary>
|
||||
/// <param name="act"></param>
|
||||
/// <summary>
|
||||
/// AddPostActivationAction method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// AddPostActivationAction method
|
||||
/// </summary>
|
||||
public void AddPostActivationAction(Action act)
|
||||
{
|
||||
if (_PostActivationActions == null)
|
||||
|
|
@ -96,9 +86,9 @@ namespace PepperDash.Core
|
|||
_PostActivationActions.Add(act);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PreActivate method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// PreActivate method
|
||||
/// </summary>
|
||||
public void PreActivate()
|
||||
{
|
||||
if (_PreActivationActions != null)
|
||||
|
|
@ -115,9 +105,9 @@ namespace PepperDash.Core
|
|||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activate method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Activate method
|
||||
/// </summary>
|
||||
public bool Activate()
|
||||
{
|
||||
//if (_PreActivationActions != null)
|
||||
|
|
@ -128,9 +118,9 @@ namespace PepperDash.Core
|
|||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PostActivate method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// PostActivate method
|
||||
/// </summary>
|
||||
public void PostActivate()
|
||||
{
|
||||
if (_PostActivationActions != null)
|
||||
|
|
@ -153,9 +143,9 @@ namespace PepperDash.Core
|
|||
/// do not need to call base.CustomActivate()
|
||||
/// </summary>
|
||||
/// <returns>true if device activated successfully.</returns>
|
||||
/// <summary>
|
||||
/// CustomActivate method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// CustomActivate method
|
||||
/// </summary>
|
||||
public virtual bool CustomActivate() { return true; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -182,15 +172,15 @@ namespace PepperDash.Core
|
|||
if (o is bool && !(bool)o) a();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the object, including its key and name.
|
||||
/// </summary>
|
||||
/// <remarks>The returned string is formatted as "{Key} - {Name}". If the <c>Name</c> property is
|
||||
/// null or empty, "---" is used in place of the name.</remarks>
|
||||
/// <returns>A string that represents the object, containing the key and name in the format "{Key} - {Name}".</returns>
|
||||
/// <summary>
|
||||
/// ToString method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Returns a string representation of the object, including its key and name.
|
||||
/// </summary>
|
||||
/// <remarks>The returned string is formatted as "{Key} - {Name}". If the <c>Name</c> property is
|
||||
/// null or empty, "---" is used in place of the name.</remarks>
|
||||
/// <returns>A string that represents the object, containing the key and name in the format "{Key} - {Name}".</returns>
|
||||
/// <summary>
|
||||
/// ToString method
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("{0} - {1}", Key, string.IsNullOrEmpty(Name) ? "---" : Name);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using Crestron.SimplSharp;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ namespace PepperDash.Core
|
|||
/// <summary>
|
||||
/// Represents a debugging context
|
||||
/// </summary>
|
||||
[Obsolete("DebugContext is no longer supported and will be removed in a future release.")]
|
||||
public class DebugContext
|
||||
{
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -8,10 +8,17 @@ using Crestron.SimplSharp;
|
|||
using WebSocketSharp;
|
||||
using System.Security.Authentication;
|
||||
using WebSocketSharp.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.IO;
|
||||
using Org.BouncyCastle.Asn1;
|
||||
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.Json;
|
||||
|
||||
|
|
@ -46,8 +53,17 @@ namespace PepperDash.Core
|
|||
{
|
||||
get
|
||||
{
|
||||
if (_httpsServer == null) return "";
|
||||
return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{_httpsServer.Port}{_httpsServer.WebSocketServices[_path].Path}";
|
||||
if (_httpsServer == null || !_httpsServer.IsListening) return "";
|
||||
var service = _httpsServer.WebSocketServices[_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}";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,6 +72,20 @@ namespace PepperDash.Core
|
|||
/// </summary>
|
||||
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;
|
||||
|
||||
|
|
@ -105,51 +135,70 @@ namespace PepperDash.Core
|
|||
var subjectName = string.Format("CN={0}.{1}", hostName, domainName);
|
||||
var fqdn = string.Format("{0}.{1}", hostName, domainName);
|
||||
|
||||
using (var rsa = RSA.Create(2048))
|
||||
var random = new SecureRandom();
|
||||
|
||||
// Generate RSA 2048 key pair
|
||||
var keyPairGenerator = new RsaKeyPairGenerator();
|
||||
keyPairGenerator.Init(new KeyGenerationParameters(random, 2048));
|
||||
var keyPair = keyPairGenerator.GenerateKeyPair();
|
||||
|
||||
// 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))
|
||||
{
|
||||
|
||||
var request = new CertificateRequest(
|
||||
subjectName,
|
||||
rsa,
|
||||
HashAlgorithmName.SHA256,
|
||||
RSASignaturePadding.Pkcs1);
|
||||
|
||||
// Subject Key Identifier
|
||||
request.CertificateExtensions.Add(
|
||||
new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
|
||||
|
||||
// Extended Key Usage: server + client auth
|
||||
request.CertificateExtensions.Add(
|
||||
new X509EnhancedKeyUsageExtension(
|
||||
new OidCollection
|
||||
{
|
||||
new Oid("1.3.6.1.5.5.7.3.1"), // id-kp-serverAuth
|
||||
new Oid("1.3.6.1.5.5.7.3.2") // id-kp-clientAuth
|
||||
},
|
||||
false));
|
||||
|
||||
// Subject Alternative Names: DNS + IP
|
||||
var sanBuilder = new SubjectAlternativeNameBuilder();
|
||||
sanBuilder.AddDnsName(fqdn);
|
||||
if (System.Net.IPAddress.TryParse(ipAddress, out var ip))
|
||||
sanBuilder.AddIpAddress(ip);
|
||||
request.CertificateExtensions.Add(sanBuilder.Build());
|
||||
|
||||
var notBefore = DateTimeOffset.UtcNow;
|
||||
var notAfter = notBefore.AddYears(2);
|
||||
|
||||
using (var cert = request.CreateSelfSigned(notBefore, notAfter))
|
||||
{
|
||||
|
||||
var separator = Path.DirectorySeparatorChar;
|
||||
var outputPath = string.Format("{0}user{1}{2}.pfx", separator, separator, _certificateName);
|
||||
|
||||
var pfxBytes = cert.Export(X509ContentType.Pfx, _certificatePassword);
|
||||
File.WriteAllBytes(outputPath, pfxBytes);
|
||||
|
||||
CrestronConsole.PrintLine(string.Format("CreateCert: Certificate written to {0}", outputPath));
|
||||
}
|
||||
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)
|
||||
{
|
||||
|
|
@ -182,26 +231,66 @@ namespace PepperDash.Core
|
|||
{
|
||||
Debug.LogInformation("Starting Websocket Server on port: {0}", port);
|
||||
|
||||
|
||||
|
||||
|
||||
Start(port, CertPath, _certificatePassword);
|
||||
}
|
||||
|
||||
private static X509Certificate2 LoadOrRecreateCert(string certPath, string certPassword)
|
||||
{
|
||||
if (!File.Exists(certPath))
|
||||
CreateCert();
|
||||
|
||||
try
|
||||
{
|
||||
// EphemeralKeySet is required on Linux/OpenSSL (Crestron 4-series) to avoid
|
||||
// key-container persistence failures, and avoids the private key export restriction.
|
||||
return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet);
|
||||
return LoadCertFromBouncyCastle(certPath, certPassword);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Cert is stale or was generated by an incompatible library (e.g. old BouncyCastle output).
|
||||
// Delete it, regenerate with the BCL path, and retry once.
|
||||
// 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 new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.90" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.274" />
|
||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||
<PackageReference Include="Serilog.Expressions" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using Crestron.SimplSharp;
|
|||
using Crestron.SimplSharp.WebScripting;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PepperDash.Core.Logging;
|
||||
using PepperDash.Core.Web.RequestHandlers;
|
||||
|
||||
namespace PepperDash.Core.Web
|
||||
|
|
@ -25,29 +26,26 @@ namespace PepperDash.Core.Web
|
|||
private readonly CCriticalSection _serverLock = new CCriticalSection();
|
||||
private HttpCwsServer _server;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Key
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Gets or sets the Key
|
||||
/// </summary>
|
||||
public string Key { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Name
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Gets or sets the Name
|
||||
/// </summary>
|
||||
public string Name { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BasePath
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Gets or sets the BasePath
|
||||
/// </summary>
|
||||
public string BasePath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the IsRegistered
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Gets or sets the IsRegistered
|
||||
/// </summary>
|
||||
public bool IsRegistered { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Http request handler
|
||||
/// </summary>
|
||||
//public IHttpCwsHandler HttpRequestHandler
|
||||
//{
|
||||
// get { return _server.HttpRequestHandler; }
|
||||
|
|
@ -58,9 +56,6 @@ namespace PepperDash.Core.Web
|
|||
// }
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// Received request event handler
|
||||
/// </summary>
|
||||
//public event EventHandler<HttpCwsRequestEventArgs> ReceivedRequestEvent
|
||||
//{
|
||||
// add { _server.ReceivedRequestEvent += new HttpCwsRequestEventHandler(value); }
|
||||
|
|
@ -91,7 +86,7 @@ namespace PepperDash.Core.Web
|
|||
/// <param name="key"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="basePath"></param>
|
||||
public WebApiServer(string key, string name, string basePath)
|
||||
public WebApiServer(string key, string name, string basePath)
|
||||
{
|
||||
Key = key;
|
||||
Name = string.IsNullOrEmpty(name) ? DefaultName : name;
|
||||
|
|
@ -116,7 +111,7 @@ namespace PepperDash.Core.Web
|
|||
{
|
||||
if (programEventType != eProgramStatusEventType.Stopping) return;
|
||||
|
||||
Debug.Console(DebugInfo, this, "Program stopping. stopping server");
|
||||
this.LogInformation("Program stopping. stopping server");
|
||||
|
||||
Stop();
|
||||
}
|
||||
|
|
@ -130,18 +125,18 @@ namespace PepperDash.Core.Web
|
|||
// Re-enable the server if the link comes back up and the status should be connected
|
||||
if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp && IsRegistered)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Ethernet link up. Server is alreedy registered.");
|
||||
this.LogInformation("Ethernet link up. Server is alreedy registered.");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Console(DebugInfo, this, "Ethernet link up. Starting server");
|
||||
this.LogInformation("Ethernet link up. Starting server");
|
||||
|
||||
Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Initialize method
|
||||
/// </summary>
|
||||
public void Initialize(string key, string basePath)
|
||||
{
|
||||
Key = key;
|
||||
|
|
@ -155,7 +150,7 @@ namespace PepperDash.Core.Web
|
|||
{
|
||||
if (route == null)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Failed to add route, route parameter is null");
|
||||
this.LogWarning("Failed to add route, route parameter is null");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -167,23 +162,36 @@ namespace PepperDash.Core.Web
|
|||
/// Removes a route from CWS
|
||||
/// </summary>
|
||||
/// <param name="route"></param>
|
||||
/// <summary>
|
||||
/// RemoveRoute method
|
||||
/// </summary>
|
||||
public void RemoveRoute(HttpCwsRoute route)
|
||||
{
|
||||
if (route == null)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Failed to remote route, orute parameter is null");
|
||||
this.LogWarning("Failed to remove route, route parameter is null");
|
||||
return;
|
||||
}
|
||||
|
||||
_server.Routes.Remove(route);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GetRouteCollection method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Sets the fallback request handler that is invoked when no registered route
|
||||
/// matches an incoming request. Must be called before <see cref="Start"/>.
|
||||
/// </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()
|
||||
{
|
||||
return _server.Routes;
|
||||
|
|
@ -200,26 +208,24 @@ namespace PepperDash.Core.Web
|
|||
|
||||
if (_server == null)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Server is null, unable to start");
|
||||
this.LogWarning("Server is null, unable to start");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsRegistered)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Server has already been started");
|
||||
this.LogWarning("Server has already been started");
|
||||
return;
|
||||
}
|
||||
|
||||
IsRegistered = _server.Register();
|
||||
|
||||
Debug.Console(DebugInfo, this, "Starting server, registration {0}", IsRegistered ? "was successful" : "failed");
|
||||
this.LogInformation("Starting server, registration {registrationResult}", IsRegistered ? "was successful" : "failed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Start Exception Message: {0}", ex.Message);
|
||||
Debug.Console(DebugVerbose, this, "Start Exception StackTrace: {0}", ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
Debug.Console(DebugVerbose, this, "Start Exception InnerException: {0}", ex.InnerException);
|
||||
this.LogError("Start Exception Message: {message}", ex.Message);
|
||||
this.LogDebug(ex, "Start Exception StackTrace");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -227,9 +233,9 @@ namespace PepperDash.Core.Web
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Stop method
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
try
|
||||
|
|
@ -238,23 +244,21 @@ namespace PepperDash.Core.Web
|
|||
|
||||
if (_server == null)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Server is null or has already been stopped");
|
||||
this.LogWarning("Server is null or has already been stopped");
|
||||
return;
|
||||
}
|
||||
|
||||
IsRegistered = _server.Unregister() == false;
|
||||
|
||||
Debug.Console(DebugInfo, this, "Stopping server, unregistration {0}", IsRegistered ? "failed" : "was successful");
|
||||
this.LogInformation("Stopping server, unregistration {unregistrationResult}", IsRegistered ? "failed" : "was successful");
|
||||
|
||||
_server.Dispose();
|
||||
_server = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Server Stop Exception Message: {0}", ex.Message);
|
||||
Debug.Console(DebugVerbose, this, "Server Stop Exception StackTrace: {0}", ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
Debug.Console(DebugVerbose, this, "Server Stop Exception InnerException: {0}", ex.InnerException);
|
||||
this.LogError("Server Stop Exception Message: {message}", ex.Message);
|
||||
this.LogDebug(ex, "Server Stop Exception StackTrace");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -275,14 +279,12 @@ namespace PepperDash.Core.Web
|
|||
try
|
||||
{
|
||||
var j = JsonConvert.SerializeObject(args.Context, Formatting.Indented);
|
||||
Debug.Console(DebugVerbose, this, "RecieveRequestEventHandler Context:\x0d\x0a{0}", j);
|
||||
this.LogVerbose("RecieveRequestEventHandler Context:\x0d\x0a{0}", j);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "ReceivedRequestEventHandler Exception Message: {0}", ex.Message);
|
||||
Debug.Console(DebugVerbose, this, "ReceivedRequestEventHandler Exception StackTrace: {0}", ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
Debug.Console(DebugVerbose, this, "ReceivedRequestEventHandler Exception InnerException: {0}", ex.InnerException);
|
||||
this.LogError("ReceivedRequestEventHandler Exception Message: {message}", ex.Message);
|
||||
this.LogDebug(ex, "ReceivedRequestEventHandler Exception StackTrace: {stackTrace}", ex.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using System;
|
||||
using Crestron.SimplSharp; // For Basic SIMPL# Classes
|
||||
using Crestron.SimplSharp; // For Basic SIMPL# Classes
|
||||
using Crestron.SimplSharp.CrestronIO;
|
||||
using Crestron.SimplSharp.Net.Http;
|
||||
using Crestron.SimplSharp.Net.Https;
|
||||
|
|
@ -10,24 +10,25 @@ using PepperDash.Core.JsonToSimpl;
|
|||
|
||||
namespace PepperDash.Core.WebApi.Presets
|
||||
{
|
||||
/// <summary>
|
||||
/// Passcode client for the WebApi
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Passcode client for the WebApi
|
||||
/// </summary>
|
||||
[Obsolete("WebApiPasscodeClient is no longer supported and will be removed in a future release.")]
|
||||
public class WebApiPasscodeClient : IKeyed
|
||||
{
|
||||
/// <summary>
|
||||
/// Notifies when user received
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Notifies when user received
|
||||
/// </summary>
|
||||
public event EventHandler<UserReceivedEventArgs> UserReceived;
|
||||
|
||||
/// <summary>
|
||||
/// Notifies when Preset received
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Notifies when Preset received
|
||||
/// </summary>
|
||||
public event EventHandler<PresetReceivedEventArgs> PresetReceived;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Key
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Gets or sets the Key
|
||||
/// </summary>
|
||||
public string Key { get; private set; }
|
||||
|
||||
//string JsonMasterKey;
|
||||
|
|
@ -54,13 +55,13 @@ namespace PepperDash.Core.WebApi.Presets
|
|||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the instance
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="jsonMasterKey"></param>
|
||||
/// <param name="urlBase"></param>
|
||||
/// <param name="defaultPresetJsonFilePath"></param>
|
||||
/// <summary>
|
||||
/// Initializes the instance
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="jsonMasterKey"></param>
|
||||
/// <param name="urlBase"></param>
|
||||
/// <param name="defaultPresetJsonFilePath"></param>
|
||||
public void Initialize(string key, string jsonMasterKey, string urlBase, string defaultPresetJsonFilePath)
|
||||
{
|
||||
Key = key;
|
||||
|
|
@ -73,44 +74,44 @@ namespace PepperDash.Core.WebApi.Presets
|
|||
J2SMaster.Initialize(jsonMasterKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user for a passcode
|
||||
/// </summary>
|
||||
/// <param name="passcode"></param>
|
||||
/// <summary>
|
||||
/// GetUserForPasscode method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Gets the user for a passcode
|
||||
/// </summary>
|
||||
/// <param name="passcode"></param>
|
||||
/// <summary>
|
||||
/// GetUserForPasscode method
|
||||
/// </summary>
|
||||
public void GetUserForPasscode(string passcode)
|
||||
{
|
||||
// Bullshit duplicate code here... These two cases should be the same
|
||||
// except for https/http and the certificate ignores
|
||||
if (!UrlBase.StartsWith("https"))
|
||||
return;
|
||||
var req = new HttpsClientRequest();
|
||||
req.Url = new UrlParser(UrlBase + "/api/users/dopin");
|
||||
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
|
||||
req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json"));
|
||||
req.Header.AddHeader(new HttpsHeader("Accept", "application/json"));
|
||||
var jo = new JObject();
|
||||
jo.Add("pin", passcode);
|
||||
req.ContentString = jo.ToString();
|
||||
// Bullshit duplicate code here... These two cases should be the same
|
||||
// except for https/http and the certificate ignores
|
||||
if (!UrlBase.StartsWith("https"))
|
||||
return;
|
||||
var req = new HttpsClientRequest();
|
||||
req.Url = new UrlParser(UrlBase + "/api/users/dopin");
|
||||
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
|
||||
req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json"));
|
||||
req.Header.AddHeader(new HttpsHeader("Accept", "application/json"));
|
||||
var jo = new JObject();
|
||||
jo.Add("pin", passcode);
|
||||
req.ContentString = jo.ToString();
|
||||
|
||||
var client = new HttpsClient();
|
||||
client.HostVerification = false;
|
||||
client.PeerVerification = false;
|
||||
var resp = client.Dispatch(req);
|
||||
var handler = UserReceived;
|
||||
if (resp.Code == 200)
|
||||
{
|
||||
//CrestronConsole.PrintLine("Received: {0}", resp.ContentString);
|
||||
var user = JsonConvert.DeserializeObject<User>(resp.ContentString);
|
||||
CurrentUser = user;
|
||||
if (handler != null)
|
||||
UserReceived(this, new UserReceivedEventArgs(user, true));
|
||||
}
|
||||
else
|
||||
if (handler != null)
|
||||
UserReceived(this, new UserReceivedEventArgs(null, false));
|
||||
var client = new HttpsClient();
|
||||
client.HostVerification = false;
|
||||
client.PeerVerification = false;
|
||||
var resp = client.Dispatch(req);
|
||||
var handler = UserReceived;
|
||||
if (resp.Code == 200)
|
||||
{
|
||||
//CrestronConsole.PrintLine("Received: {0}", resp.ContentString);
|
||||
var user = JsonConvert.DeserializeObject<User>(resp.ContentString);
|
||||
CurrentUser = user;
|
||||
if (handler != null)
|
||||
handler(this, new UserReceivedEventArgs(user, true));
|
||||
}
|
||||
else
|
||||
if (handler != null)
|
||||
handler(this, new UserReceivedEventArgs(null, false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -118,9 +119,9 @@ namespace PepperDash.Core.WebApi.Presets
|
|||
/// </summary>
|
||||
/// <param name="roomTypeId"></param>
|
||||
/// <param name="presetNumber"></param>
|
||||
/// <summary>
|
||||
/// GetPresetForThisUser method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// GetPresetForThisUser method
|
||||
/// </summary>
|
||||
public void GetPresetForThisUser(int roomTypeId, int presetNumber)
|
||||
{
|
||||
if (CurrentUser == null)
|
||||
|
|
@ -136,57 +137,57 @@ namespace PepperDash.Core.WebApi.Presets
|
|||
PresetNumber = presetNumber
|
||||
};
|
||||
|
||||
var handler = PresetReceived;
|
||||
var handler = PresetReceived;
|
||||
try
|
||||
{
|
||||
if (!UrlBase.StartsWith("https"))
|
||||
return;
|
||||
var req = new HttpsClientRequest();
|
||||
req.Url = new UrlParser(UrlBase + "/api/presets/userandroom");
|
||||
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
|
||||
req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json"));
|
||||
req.Header.AddHeader(new HttpsHeader("Accept", "application/json"));
|
||||
req.ContentString = JsonConvert.SerializeObject(msg);
|
||||
if (!UrlBase.StartsWith("https"))
|
||||
return;
|
||||
var req = new HttpsClientRequest();
|
||||
req.Url = new UrlParser(UrlBase + "/api/presets/userandroom");
|
||||
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
|
||||
req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json"));
|
||||
req.Header.AddHeader(new HttpsHeader("Accept", "application/json"));
|
||||
req.ContentString = JsonConvert.SerializeObject(msg);
|
||||
|
||||
var client = new HttpsClient();
|
||||
client.HostVerification = false;
|
||||
client.PeerVerification = false;
|
||||
var client = new HttpsClient();
|
||||
client.HostVerification = false;
|
||||
client.PeerVerification = false;
|
||||
|
||||
// ask for the preset
|
||||
var resp = client.Dispatch(req);
|
||||
if (resp.Code == 200) // got it
|
||||
{
|
||||
//Debug.Console(1, this, "Received: {0}", resp.ContentString);
|
||||
var preset = JsonConvert.DeserializeObject<Preset>(resp.ContentString);
|
||||
CurrentPreset = preset;
|
||||
// ask for the preset
|
||||
var resp = client.Dispatch(req);
|
||||
if (resp.Code == 200) // got it
|
||||
{
|
||||
//Debug.Console(1, this, "Received: {0}", resp.ContentString);
|
||||
var preset = JsonConvert.DeserializeObject<Preset>(resp.ContentString);
|
||||
CurrentPreset = preset;
|
||||
|
||||
//if there's no preset data, load the template
|
||||
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.");
|
||||
LoadDefaultPresetData();
|
||||
return;
|
||||
}
|
||||
//if there's no preset data, load the template
|
||||
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.");
|
||||
LoadDefaultPresetData();
|
||||
return;
|
||||
}
|
||||
|
||||
J2SMaster.LoadWithJson(preset.Data);
|
||||
if (handler != null)
|
||||
PresetReceived(this, new PresetReceivedEventArgs(preset, true));
|
||||
}
|
||||
else // no existing preset
|
||||
{
|
||||
CurrentPreset = new Preset();
|
||||
LoadDefaultPresetData();
|
||||
if (handler != null)
|
||||
PresetReceived(this, new PresetReceivedEventArgs(null, false));
|
||||
}
|
||||
J2SMaster.LoadWithJson(preset.Data);
|
||||
if (handler != null)
|
||||
handler(this, new PresetReceivedEventArgs(preset, true));
|
||||
}
|
||||
else // no existing preset
|
||||
{
|
||||
CurrentPreset = new Preset();
|
||||
LoadDefaultPresetData();
|
||||
if (handler != null)
|
||||
handler(this, new PresetReceivedEventArgs(null, false));
|
||||
}
|
||||
}
|
||||
catch (HttpException e)
|
||||
{
|
||||
var resp = e.Response;
|
||||
Debug.Console(1, this, "No preset received (code {0}). Loading default template", resp.Code);
|
||||
LoadDefaultPresetData();
|
||||
if (handler != null)
|
||||
PresetReceived(this, new PresetReceivedEventArgs(null, false));
|
||||
if (handler != null)
|
||||
PresetReceived(this, new PresetReceivedEventArgs(null, false));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -218,14 +219,14 @@ namespace PepperDash.Core.WebApi.Presets
|
|||
/// </summary>
|
||||
/// <param name="roomTypeId"></param>
|
||||
/// <param name="presetNumber"></param>
|
||||
/// <summary>
|
||||
/// SavePresetForThisUser method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// SavePresetForThisUser method
|
||||
/// </summary>
|
||||
public void SavePresetForThisUser(int roomTypeId, int presetNumber)
|
||||
{
|
||||
if (CurrentPreset == null)
|
||||
LoadDefaultPresetData();
|
||||
//return;
|
||||
//return;
|
||||
|
||||
//// A new preset needs to have its numbers set
|
||||
//if (CurrentPreset.IsNewPreset)
|
||||
|
|
@ -245,8 +246,8 @@ namespace PepperDash.Core.WebApi.Presets
|
|||
{
|
||||
CurrentPreset.Data = json;
|
||||
|
||||
if (!UrlBase.StartsWith("https"))
|
||||
return;
|
||||
if (!UrlBase.StartsWith("https"))
|
||||
return;
|
||||
var req = new HttpsClientRequest();
|
||||
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
|
||||
req.Url = new UrlParser(string.Format("{0}/api/presets/addorchange", UrlBase));
|
||||
|
|
@ -255,8 +256,8 @@ namespace PepperDash.Core.WebApi.Presets
|
|||
req.ContentString = JsonConvert.SerializeObject(CurrentPreset);
|
||||
|
||||
var client = new HttpsClient();
|
||||
client.HostVerification = false;
|
||||
client.PeerVerification = false;
|
||||
client.HostVerification = false;
|
||||
client.PeerVerification = false;
|
||||
try
|
||||
{
|
||||
var resp = client.Dispatch(req);
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ namespace PepperDash.Essentials.Core
|
|||
|
||||
ICec Port;
|
||||
|
||||
bool _cecSubscribed;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
|
|
@ -54,7 +56,7 @@ namespace PepperDash.Essentials.Core
|
|||
{
|
||||
Port = postActivationFunc(config);
|
||||
|
||||
Port.StreamCec.CecChange += StreamCec_CecChange;
|
||||
TryEnsureCecSubscription();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -68,7 +70,29 @@ namespace PepperDash.Essentials.Core
|
|||
{
|
||||
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);
|
||||
_cecSubscribed = true;
|
||||
Debug.LogMessage(LogEventLevel.Information, this, "Subscribed to CEC feedback");
|
||||
}
|
||||
|
||||
void StreamCec_CecChange(Cec cecDevice, CecEventArgs args)
|
||||
|
|
@ -104,8 +128,9 @@ namespace PepperDash.Essentials.Core
|
|||
/// </summary>
|
||||
public void SendText(string text)
|
||||
{
|
||||
if (Port == null)
|
||||
if (Port?.StreamCec == null)
|
||||
return;
|
||||
TryEnsureCecSubscription();
|
||||
this.PrintSentText(text);
|
||||
Port.StreamCec.Send.StringValue = text;
|
||||
}
|
||||
|
|
@ -115,8 +140,9 @@ namespace PepperDash.Essentials.Core
|
|||
/// </summary>
|
||||
public void SendBytes(byte[] bytes)
|
||||
{
|
||||
if (Port == null)
|
||||
if (Port?.StreamCec == null)
|
||||
return;
|
||||
TryEnsureCecSubscription();
|
||||
var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
|
||||
this.PrintSentBytes(bytes);
|
||||
Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
|
||||
|
|
|
|||
|
|
@ -96,6 +96,17 @@ namespace PepperDash.Essentials.Core
|
|||
comm = udp;
|
||||
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:
|
||||
break;
|
||||
case eControlMethod.SecureTcpIp:
|
||||
|
|
|
|||
|
|
@ -135,12 +135,14 @@ namespace PepperDash.Essentials.Core.Config
|
|||
{
|
||||
var parsedConfig = JObject.Parse(fs.ReadToEnd());
|
||||
|
||||
// Check if it's a v2 config (check for "version" node)
|
||||
// this means it's already merged by the Portal API
|
||||
// from the v2 config tool
|
||||
var isV2Config = parsedConfig["versions"] != null;
|
||||
|
||||
if (isV2Config)
|
||||
// A config is v1 if it has separate "system" and "template" nodes that
|
||||
// need to be merged. A v2 config is already merged by the Portal API and
|
||||
// will not have "system"/"template" nodes. This is independent of whether
|
||||
// a "versions" node is present, which only carries version metadata and
|
||||
// can appear on either a v1 or v2 config.
|
||||
var isV1Config = parsedConfig["system"] != null && parsedConfig["template"] != null;
|
||||
|
||||
if (!isV1Config)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Config file is a v2 format, no merge necessary.");
|
||||
ConfigObject = parsedConfig.ToObject<EssentialsConfig>();
|
||||
|
|
@ -148,6 +150,8 @@ namespace PepperDash.Essentials.Core.Config
|
|||
return true;
|
||||
}
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information, "Config file is a v1 format, merging system and template.");
|
||||
|
||||
// Extract SystemUrl and TemplateUrl into final config output
|
||||
ConfigObject = PortalConfigReader.MergeConfigs(parsedConfig).ToObject<EssentialsConfig>();
|
||||
|
||||
|
|
@ -160,6 +164,13 @@ namespace PepperDash.Essentials.Core.Config
|
|||
{
|
||||
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");
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ namespace PepperDash.Essentials.Core.Config
|
|||
/// <summary>
|
||||
/// ConfigUpdater class
|
||||
/// </summary>
|
||||
[Obsolete("ConfigUpdater is no longer supported and will be removed in a future release.")]
|
||||
public static class ConfigUpdater
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -81,7 +82,7 @@ namespace PepperDash.Essentials.Core.Config
|
|||
{
|
||||
var handler = ConfigStatusChanged;
|
||||
|
||||
if(handler != null)
|
||||
if (handler != null)
|
||||
{
|
||||
handler(typeof(ConfigUpdater), new ConfigStatusEventArgs(status));
|
||||
}
|
||||
|
|
@ -89,7 +90,7 @@ namespace PepperDash.Essentials.Core.Config
|
|||
|
||||
static void WriteConfigToFile(string configData)
|
||||
{
|
||||
var filePath = Global.FilePathPrefix+ "configurationFile-updated.json";
|
||||
var filePath = Global.FilePathPrefix + "configurationFile-updated.json";
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -104,7 +105,7 @@ namespace PepperDash.Essentials.Core.Config
|
|||
Debug.LogMessage(LogEventLevel.Debug, "Error parsing new config: {0}", e);
|
||||
|
||||
OnStatusUpdate(eUpdateStatus.UpdateFailed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -149,11 +150,11 @@ namespace PepperDash.Essentials.Core.Config
|
|||
// Directory exists, first clear any contents
|
||||
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);
|
||||
|
||||
for (int i = 0; i < archivedConfigFiles.Length; i++ )
|
||||
for (int i = 0; i < archivedConfigFiles.Length; i++)
|
||||
{
|
||||
var file = archivedConfigFiles[i];
|
||||
Debug.LogMessage(LogEventLevel.Information, "Deleting archived file: '{0}'", file.FullName);
|
||||
|
|
@ -170,9 +171,9 @@ namespace PepperDash.Essentials.Core.Config
|
|||
|
||||
// Moves the file and appends the .bak extension
|
||||
var fileDest = archiveDirectoryPath + "/" + file.Name + ".bak";
|
||||
if(!File.Exists(fileDest))
|
||||
if (!File.Exists(fileDest))
|
||||
{
|
||||
file.MoveTo(fileDest);
|
||||
file.MoveTo(fileDest);
|
||||
}
|
||||
else
|
||||
Debug.LogMessage(LogEventLevel.Information, "Cannot move file to archive folder. Existing file already exists with same name: '{0}'", fileDest);
|
||||
|
|
@ -207,15 +208,15 @@ namespace PepperDash.Essentials.Core.Config
|
|||
|
||||
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>
|
||||
/// Enumeration of eUpdateStatus values
|
||||
/// </summary>
|
||||
public enum eUpdateStatus
|
||||
/// <summary>
|
||||
/// Enumeration of eUpdateStatus values
|
||||
/// </summary>
|
||||
public enum eUpdateStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// UpdateStarted status
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ namespace PepperDash.Essentials.Core.Config
|
|||
/// <summary>
|
||||
/// Gets or sets the Versions
|
||||
/// </summary>
|
||||
[JsonProperty("versions")]
|
||||
public VersionData Versions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -134,12 +135,25 @@ namespace PepperDash.Essentials.Core.Config
|
|||
[JsonProperty("packages")]
|
||||
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>
|
||||
/// Initializes a new instance of the <see cref="VersionData"/> class.
|
||||
/// </summary>
|
||||
public VersionData()
|
||||
{
|
||||
Packages = new List<NugetVersion>();
|
||||
UserInterfaces = new List<NugetVersion>();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -157,8 +171,20 @@ namespace PepperDash.Essentials.Core.Config
|
|||
/// <summary>
|
||||
/// Gets or sets the PackageId
|
||||
/// </summary>
|
||||
[JsonProperty("packageId")]
|
||||
[JsonProperty("packageId", NullValueHandling = NullValueHandling.Ignore)]
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -3,9 +3,291 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the display mode for a webview event, with expected values of "Fullscreen", "Modal", or "Unknown".
|
||||
/// </summary>
|
||||
public enum eWebViewEventMode
|
||||
{
|
||||
/// <summary>
|
||||
/// The display mode for the webview event is unknown or not specified. This value can be used as a default or fallback when the display mode is not provided or cannot be parsed into a known value.
|
||||
/// </summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// The webview event should be displayed in fullscreen mode, covering the entire screen and typically used for immersive experiences or when maximum screen real estate is needed. When a webview event with this display mode is shown, it will typically trigger the WebViewStatusChanged event with a status of "Fullscreen", and when it is cleared/closed, it will trigger the WebViewStatusChanged event with a status of "Cleared".
|
||||
/// </summary>
|
||||
Fullscreen,
|
||||
|
||||
/// <summary>
|
||||
/// The webview event should be displayed in modal mode, which typically means it will be shown as a dialog or overlay on top of the existing content, allowing the user to interact with it while still being able to see the underlying content. This display mode is often used for alerts, confirmations, or when the webview content is related to the current context but does not require full immersion. When a webview event with this display mode is shown, it will typically trigger the WebViewStatusChanged event with a status of "Modal", and when it is cleared/closed, it will trigger the WebViewStatusChanged event with a status of "Cleared".
|
||||
/// </summary>
|
||||
Modal,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the target for a webview event, with expected values of "OSD", "Controller", "PersistentWebApp", or "RoomScheduler".
|
||||
/// </summary>
|
||||
|
||||
public enum eWebViewTarget
|
||||
{
|
||||
/// <summary>
|
||||
/// The target for the webview event is unknown or not specified. This value can be used as a default or fallback when the target is not provided or cannot be parsed into a known value.
|
||||
/// </summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// The webview event should be displayed on the On-Screen Display (OSD).
|
||||
/// </summary>
|
||||
OSD,
|
||||
|
||||
/// <summary>
|
||||
/// The webview event should be displayed on the controller.
|
||||
/// </summary>
|
||||
Controller,
|
||||
|
||||
/// <summary>
|
||||
/// The webview event should be displayed on the persistent web application.
|
||||
/// </summary>
|
||||
PersistentWebApp,
|
||||
|
||||
/// <summary>
|
||||
/// The webview event should be displayed on the room scheduler.
|
||||
/// </summary>
|
||||
RoomScheduler
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the reason for an error in a webview event, which can provide additional information about what went wrong. This class is typically only used in the Status property of a WebViewEvent when the status indicates an error, and may be null otherwise.
|
||||
/// </summary>
|
||||
public class Reason
|
||||
{
|
||||
/// <summary>
|
||||
/// The reason for an error in a webview event as a string, which can provide additional information about what went wrong. This property is typically only populated in case of an error, and may be null otherwise.
|
||||
/// </summary>
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the XPath of a webview event, which can provide information about where an error occurred in the webview. This class is typically only used in the Status property of a WebViewEvent when the status indicates an error, and may be null otherwise.
|
||||
/// </summary>
|
||||
public class XPath
|
||||
{
|
||||
/// <summary>
|
||||
/// The XPath of a webview event as a string, which can provide information about where an error occurred in the webview. This property is typically only populated in case of an error, and may be null otherwise.
|
||||
/// </summary>
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a base class for properties that have a string value and trigger an action when the value changes. This class can be used as a base for properties like DisplayMode and Target in the WebViewEvent, which have string values that can be set directly or parsed into enums for easier handling of expected values. The ValueChangedAction can be set to trigger any desired behavior when the value changes, such as updating the UI or triggering other events.
|
||||
/// </summary>
|
||||
public abstract class ValueProperty
|
||||
{
|
||||
/// <summary>
|
||||
/// Triggered when Value is set
|
||||
/// </summary>
|
||||
public Action ValueChangedAction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the ValueChangedAction if it is set. This method should be called whenever the Value property is set to ensure that any desired behavior associated with a change in value is executed.
|
||||
/// </summary>
|
||||
protected void OnValueChanged()
|
||||
{
|
||||
var a = ValueChangedAction;
|
||||
if (a != null)
|
||||
a();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a webview event, which can include information about the status of the webview, the display parameters for the webview, and any error information if applicable. This class can be used to represent both show and clear events for a webview, with the Status property indicating the current status of the webview (e.g., "Fullscreen", "Modal", "Cleared", "Error", or "Unknown"), the Display property providing details about how the webview is being displayed (e.g., mode, URL, target, title), and the Cleared property providing details about a cleared/closed webview event (e.g., target and ID). The Id property can be used to correlate show and clear events for the same webview instance.
|
||||
/// </summary>
|
||||
public class WebViewEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The unique identifier for the webview event, which can be used to correlate show and clear events for the same webview instance. This property is typically included in both show and clear events for a webview, allowing you to track the lifecycle of a specific webview instance from when it is shown to when it is cleared/closed. The Id can be any string value, but it should be unique for each webview instance to ensure proper correlation between show and clear events.
|
||||
/// </summary>
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The status of the webview event, which can indicate the current state of the webview (e.g., "Fullscreen", "Modal", "Cleared", "Error", or "Unknown") as well as any error information if applicable (XPath and Reason). The Value property can be used to get or set the current status of the webview, while the XPath and Reason properties can provide additional information in case of an error. The StatusString property can be used to get or set the raw status string from the event, but it is recommended to use the Value property for easier handling of expected values. Setting the Value property will trigger the ValueChangedAction if it is set, allowing you to respond to changes in the webview status as needed.
|
||||
/// </summary>
|
||||
[JsonProperty("status")]
|
||||
public Status Status { get; set; } // /Event/UserInterface/WebView/Status
|
||||
|
||||
/// <summary>
|
||||
/// The display parameters for the webview event, which can include the display mode (e.g., "Fullscreen", "Modal", or "Unknown"), the URL to display in the webview, the target for the webview (e.g., "OSD", "Controller", "PersistentWebApp", or "RoomScheduler"), and the title to display on the webview. This property is typically included in show events for a webview, providing details about how the webview is being displayed. When a webview event with these display parameters is shown, it will typically trigger the WebViewStatusChanged event with a status of "Fullscreen" or "Modal" depending on the specified display mode, and when it is cleared/closed, it will trigger the WebViewStatusChanged event with a status of "Cleared".
|
||||
/// </summary>
|
||||
[JsonProperty("display")]
|
||||
public WebViewDisplay Display { get; set; } // /Event/UserInterface/WebView/Display
|
||||
|
||||
/// <summary>
|
||||
/// The details for a cleared/closed webview event, which can include the target for the webview that was cleared (e.g., "OSD", "Controller", "PersistentWebApp", or "RoomScheduler") and the unique identifier for the webview event that was cleared. This property is typically included in clear events for a webview, providing details about which webview instance was cleared/closed. When a webview event with this property is cleared/closed, it will typically trigger the WebViewStatusChanged event with a status of "Cleared".
|
||||
/// </summary>
|
||||
[JsonProperty("cleared")]
|
||||
public WebViewClear Cleared { get; set; } // /Event/UserInterface/WebView/Cleared
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the display parameters for a webview event, which can include the display mode (e.g., "Fullscreen", "Modal", or "Unknown"), the URL to display in the webview, the target for the webview (e.g., "OSD", "Controller", "PersistentWebApp", or "RoomScheduler"), and the title to display on the webview. This class is typically used in the Display property of a WebViewEvent to provide details about how the webview is being displayed when a show event occurs. When a webview event with these display parameters is shown, it will typically trigger the WebViewStatusChanged event with a status of "Fullscreen" or "Modal" depending on the specified display mode, and when it is cleared/closed, it will trigger the WebViewStatusChanged event with a status of "Cleared".
|
||||
/// </summary>
|
||||
public class WebViewDisplay
|
||||
{
|
||||
/// <summary>
|
||||
/// The display mode for the webview event. Expected values are "Fullscreen", "Modal", or "Unknown".
|
||||
/// </summary>
|
||||
[JsonProperty("mode")]
|
||||
public DisplayMode Mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The URL to display in the webview.
|
||||
/// </summary>
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The target for the webview. Expected values are "OSD", "Controller", "PersistentWebApp", or "RoomScheduler".
|
||||
/// </summary>
|
||||
[JsonProperty("target")]
|
||||
public Target Target { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The title to display on the webview.
|
||||
/// </summary>
|
||||
[JsonProperty("title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The unique identifier for the webview event, used to correlate show and clear events for the same webview instance.
|
||||
/// </summary>
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the data for a webview cleared event, which indicates that a webview with the specified ID and target has been cleared/closed.
|
||||
/// </summary>
|
||||
public class WebViewClear
|
||||
{
|
||||
/// <summary>
|
||||
/// The target for the webview that was cleared. Expected values are "OSD", "Controller", "PersistentWebApp", or "RoomScheduler".
|
||||
/// </summary>
|
||||
[JsonProperty("target")]
|
||||
public Target Target { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The unique identifier for the webview event that was cleared, used to correlate show and clear events for the same webview instance.
|
||||
/// </summary>
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The URL that was displayed in the webview that was cleared.
|
||||
/// </summary>
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the display mode for a webview event, with a string value and a corresponding enum property for easier handling of expected values.
|
||||
/// </summary>
|
||||
public class DisplayMode : ValueProperty
|
||||
{
|
||||
private string _value;
|
||||
|
||||
/// <summary>
|
||||
/// The id of the webview event.
|
||||
/// </summary>
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The string value for the display mode, which can be set directly or parsed into the WebViewEventMode enum using the WebViewEventMode property. Setting this property will also trigger the ValueChangedAction if it is set.
|
||||
/// </summary>
|
||||
public string Value { get { return _value; } set { _value = value; OnValueChanged(); } }
|
||||
|
||||
/// <summary>
|
||||
/// The display mode for the webview event as an enum, which can be used for easier handling of expected values. Expected values are Fullscreen, Modal, or Unknown.
|
||||
/// </summary>
|
||||
public eWebViewEventMode WebViewEventMode
|
||||
{
|
||||
get
|
||||
{
|
||||
eWebViewEventMode mode;
|
||||
System.Enum.TryParse(Value, true, out mode);
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the target for a webview event, with a string value and a corresponding enum property for easier handling of expected values. Setting the Value property will also trigger the ValueChangedAction if it is set.
|
||||
/// </summary>
|
||||
public class Target : ValueProperty
|
||||
{
|
||||
private string _value;
|
||||
|
||||
/// <summary>
|
||||
/// The id of the webview event.
|
||||
/// </summary>
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The string value for the target, which can be set directly or parsed into the eWebViewTarget enum using the WebViewTarget property. Expected values are "OSD", "Controller", "PersistentWebApp", or "RoomScheduler". Setting this property will also trigger the ValueChangedAction if it is set.
|
||||
/// </summary>
|
||||
public string Value { get { return _value; } set { _value = value; OnValueChanged(); } }
|
||||
|
||||
/// <summary>
|
||||
/// The target for the webview event as an enum, which can be used for easier handling of expected values. Expected values are OSD, Controller, PersistentWebApp, or RoomScheduler.
|
||||
/// </summary>
|
||||
public eWebViewTarget WebViewTarget
|
||||
{
|
||||
get
|
||||
{
|
||||
eWebViewTarget target;
|
||||
System.Enum.TryParse(Value, true, out target);
|
||||
return target;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the status of a webview event, which can include error information (XPath and Reason) as well as the current status of the webview. The Value property can be used to get or set the current status of the webview, while the XPath and Reason properties can provide additional information in case of an error. The StatusString property can be used to get or set the raw status string from the event.
|
||||
/// </summary>
|
||||
public class Status
|
||||
{
|
||||
/// <summary>
|
||||
/// The XPath of the webview event, which can provide information about where an error occurred in the webview. This property is typically only populated in case of an error, and may be null otherwise.
|
||||
/// </summary>
|
||||
[JsonProperty("XPath", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public XPath XPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The reason for an error in the webview event, which can provide additional information about what went wrong. This property is typically only populated in case of an error, and may be null otherwise.
|
||||
/// </summary>
|
||||
[JsonProperty("Reason", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public Reason Reason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The raw status string from the webview event, which can provide information about the current status of the webview. This property can be used to get or set the status directly, but it is recommended to use the Value property for easier handling of expected values. Setting this property will not trigger any actions, while setting the Value property will trigger the ValueChangedAction if it is set.
|
||||
/// </summary>
|
||||
[JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string StatusString { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current status of the webview as a string, which can be set directly or parsed into a WebViewEventMode enum using the WebViewEventMode property. Expected values are "Fullscreen", "Modal", "Cleared", "Error", or "Unknown". Setting this property will trigger the ValueChangedAction if it is set.
|
||||
/// </summary>
|
||||
[JsonProperty("Value", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the contract for IHasWebView
|
||||
/// </summary>
|
||||
|
|
@ -34,6 +316,7 @@ namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
|
|||
/// Event raised when the webview status changes
|
||||
/// </summary>
|
||||
event EventHandler<WebViewStatusChangedEventArgs> WebViewStatusChanged;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -75,6 +358,11 @@ namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
|
|||
/// </summary>
|
||||
public string Status { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WebViewEvent associated with the status change, which can provide additional information about the webview event that triggered the status change, such as display parameters or error information. This property allows you to include the full WebViewEvent in the event args, giving you access to all relevant details about the webview event when handling the WebViewStatusChanged event.
|
||||
/// </summary>
|
||||
public WebViewEvent WebView { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for WebViewStatusChangedEventArgs
|
||||
/// </summary>
|
||||
|
|
@ -83,5 +371,16 @@ namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
|
|||
{
|
||||
Status = status;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for WebViewStatusChangedEventArgs with WebViewEvent parameter, which can provide additional information about the webview event that triggered the status change, such as display parameters or error information. This constructor allows you to include the full WebViewEvent in the event args, giving you access to all relevant details about the webview event when handling the WebViewStatusChanged event.
|
||||
/// </summary>
|
||||
/// <param name="status">the new status of the webview</param>
|
||||
/// <param name="webview">the WebViewEvent associated with the status change</param>
|
||||
public WebViewStatusChangedEventArgs(string status, WebViewEvent webview)
|
||||
{
|
||||
Status = status;
|
||||
WebView = webview;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
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,10 +1,12 @@
|
|||
using System;
|
||||
using PepperDash.Core;
|
||||
|
||||
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the contract for IMobileControlMessenger
|
||||
/// Obsolete: messengers are subscription based by default; use IMobileControlMessenger instead.
|
||||
/// </summary>
|
||||
[Obsolete("This interface is obsolete and will be removed in a future version. All messengers are now subscription based.")]
|
||||
public interface IMobileControlMessengerWithSubscriptions : IMobileControlMessenger
|
||||
{
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
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,6 +7,7 @@ using System.Collections;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PepperDash.Essentials.Core
|
||||
|
|
@ -176,7 +177,20 @@ namespace PepperDash.Essentials.Core
|
|||
{
|
||||
if (!conversionType.IsEnum)
|
||||
{
|
||||
return Convert.ChangeType(value, conversionType, System.Globalization.CultureInfo.InvariantCulture);
|
||||
if (conversionType == typeof(byte[]) && value is string byteString)
|
||||
{
|
||||
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);
|
||||
|
|
@ -189,6 +203,32 @@ namespace PepperDash.Essentials.Core
|
|||
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>
|
||||
/// Gets the properties on a device
|
||||
/// </summary>
|
||||
|
|
|
|||
14
src/PepperDash.Essentials.Core/Devices/IHasDspPresetSave.cs
Normal file
14
src/PepperDash.Essentials.Core/Devices/IHasDspPresetSave.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
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,9 +7,10 @@ using PepperDash.Core;
|
|||
|
||||
namespace PepperDash.Essentials.Core.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the contract for ILogStrings
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Defines the contract for ILogStrings
|
||||
/// </summary>
|
||||
[Obsolete("ILogStrings is no longer supported and will be removed in a future release.")]
|
||||
public interface ILogStrings : IKeyed
|
||||
{
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -7,15 +7,16 @@ using PepperDash.Core;
|
|||
|
||||
namespace PepperDash.Essentials.Core.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the contract for ILogStringsWithLevel
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Defines the contract for ILogStringsWithLevel
|
||||
/// </summary>
|
||||
[Obsolete("ILogStringsWithLevel is no longer supported and will be removed in a future release.")]
|
||||
public interface ILogStringsWithLevel : IKeyed
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a class that is capable of logging a string with an int level
|
||||
/// </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>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.274" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Crestron\CrestronGenericBaseDevice.cs.orig" />
|
||||
|
|
|
|||
|
|
@ -2,10 +2,8 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Crestron.SimplSharpPro.Keypads;
|
||||
using PepperDash.Essentials.Core.Queues;
|
||||
using PepperDash.Essentials.Core.Routing;
|
||||
using Serilog.Events;
|
||||
using Debug = PepperDash.Core.Debug;
|
||||
|
||||
|
||||
|
|
@ -53,12 +51,6 @@ namespace PepperDash.Essentials.Core
|
|||
/// </summary>
|
||||
private static Dictionary<string, List<TieLine>> _tieLinesBySource;
|
||||
|
||||
/// <summary>
|
||||
/// Cache of failed route attempts to avoid re-checking impossible paths.
|
||||
/// Format: "sourceKey|destKey|signalType"
|
||||
/// </summary>
|
||||
private static readonly HashSet<string> _impossibleRoutes = new HashSet<string>();
|
||||
|
||||
/// <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.
|
||||
|
|
@ -67,7 +59,7 @@ namespace PepperDash.Essentials.Core
|
|||
{
|
||||
try
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Indexing TieLines for faster route discovery");
|
||||
Debug.LogInformation("Indexing TieLines for faster route discovery");
|
||||
|
||||
_tieLinesByDestination = TieLineCollection.Default
|
||||
.GroupBy(t => t.DestinationPort.ParentDevice.Key)
|
||||
|
|
@ -77,8 +69,8 @@ namespace PepperDash.Essentials.Core
|
|||
.GroupBy(t => t.SourcePort.ParentDevice.Key)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information, "TieLine indexing complete. {0} destination keys, {1} source keys",
|
||||
null, _tieLinesByDestination.Count, _tieLinesBySource.Count);
|
||||
Debug.LogInformation("TieLine indexing complete. {0} destination keys, {1} source keys",
|
||||
_tieLinesByDestination.Count, _tieLinesBySource.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -121,27 +113,6 @@ namespace PepperDash.Essentials.Core
|
|||
return TieLineCollection.Default.Where(t => t.SourcePort.ParentDevice.Key == sourceKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a cache key for route impossibility tracking.
|
||||
/// </summary>
|
||||
/// <param name="sourceKey">Source device key</param>
|
||||
/// <param name="destKey">Destination device key</param>
|
||||
/// <param name="type">Signal type</param>
|
||||
/// <returns>Cache key string</returns>
|
||||
private static string GetRouteKey(string sourceKey, string destKey, eRoutingSignalType type)
|
||||
{
|
||||
return string.Format("{0}|{1}|{2}", sourceKey, destKey, type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the impossible routes cache. Should be called if TieLines are added/removed at runtime.
|
||||
/// </summary>
|
||||
public static void ClearImpossibleRoutesCache()
|
||||
{
|
||||
_impossibleRoutes.Clear();
|
||||
Debug.LogMessage(LogEventLevel.Information, "Impossible routes cache cleared");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets any existing RouteDescriptor for a destination, clears it using ReleaseRoute
|
||||
/// and then attempts a new Route and if sucessful, stores that RouteDescriptor
|
||||
|
|
@ -151,7 +122,7 @@ namespace PepperDash.Essentials.Core
|
|||
{
|
||||
// Remove this line before committing!!!!!
|
||||
var frame = new StackFrame(1, true);
|
||||
Debug.LogMessage(LogEventLevel.Information, "ReleaseAndMakeRoute Called from {method} with params {destinationKey}:{sourceKey}:{signalType}:{destinationPortKey}:{sourcePortKey}", frame.GetMethod().Name, destination.Key, source.Key, signalType.ToString(), destinationPortKey, sourcePortKey);
|
||||
Debug.LogInformation("ReleaseAndMakeRoute Called from {method} with params {destinationKey}:{sourceKey}:{signalType}:{destinationPortKey}:{sourcePortKey}", frame.GetMethod().Name, destination.Key, source.Key, signalType.ToString(), destinationPortKey, sourcePortKey);
|
||||
|
||||
var inputPort = string.IsNullOrEmpty(destinationPortKey) ? null : destination.InputPorts.FirstOrDefault(p => p.Key == destinationPortKey);
|
||||
var outputPort = string.IsNullOrEmpty(sourcePortKey) ? null : source.OutputPorts.FirstOrDefault(p => p.Key == sourcePortKey);
|
||||
|
|
@ -209,13 +180,13 @@ namespace PepperDash.Essentials.Core
|
|||
/// <param name="destinationKey">destination device key</param>
|
||||
public static void RemoveRouteRequestForDestination(string destinationKey)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Removing route request for {destination}", null, destinationKey);
|
||||
Debug.LogInformation("Removing route request for {destination}", destinationKey);
|
||||
|
||||
var result = RouteRequests.Remove(destinationKey);
|
||||
|
||||
var messageTemplate = result ? "Route Request for {destination} removed" : "Route Request for {destination} not found";
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information, messageTemplate, null, destinationKey);
|
||||
Debug.LogInformation(messageTemplate, destinationKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -231,8 +202,8 @@ namespace PepperDash.Essentials.Core
|
|||
if (!signalType.HasFlag(eRoutingSignalType.AudioVideo) &&
|
||||
!(signalType.HasFlag(eRoutingSignalType.Video) && signalType.HasFlag(eRoutingSignalType.SecondaryAudio)))
|
||||
{
|
||||
var singleTypeRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, signalType);
|
||||
Debug.LogMessage(LogEventLevel.Debug, "Attempting to build source route from {sourceKey} of type {type}", destination, source.Key, signalType);
|
||||
var singleTypeRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, sourcePort, signalType);
|
||||
Debug.LogDebug(destination, "Attempting to build source route from {sourceKey} of type {type}", source.Key, signalType);
|
||||
|
||||
if (!destination.GetRouteToSource(source, null, null, signalType, 0, singleTypeRouteDescriptor, destinationPort, sourcePort))
|
||||
singleTypeRouteDescriptor = null;
|
||||
|
|
@ -240,46 +211,46 @@ namespace PepperDash.Essentials.Core
|
|||
var routes = singleTypeRouteDescriptor?.Routes ?? new List<RouteSwitchDescriptor>();
|
||||
foreach (var route in routes)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Route for device: {route}", destination, route.ToString());
|
||||
Debug.LogVerbose(destination, "Route for device: {route}", route.ToString());
|
||||
}
|
||||
|
||||
return (singleTypeRouteDescriptor, null);
|
||||
}
|
||||
// otherwise, audioVideo needs to be handled as two steps.
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Debug, "Attempting to build source route from {destinationKey} to {sourceKey} of type {type}", destination, source.Key, signalType);
|
||||
Debug.LogDebug(destination, "Attempting to build source route from {destinationKey} to {sourceKey} of type {type}", destination.Key, source.Key, signalType);
|
||||
|
||||
RouteDescriptor audioRouteDescriptor;
|
||||
|
||||
if (signalType.HasFlag(eRoutingSignalType.SecondaryAudio))
|
||||
{
|
||||
audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.SecondaryAudio);
|
||||
audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, sourcePort, eRoutingSignalType.SecondaryAudio);
|
||||
}
|
||||
else
|
||||
{
|
||||
audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.Audio);
|
||||
audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, sourcePort, eRoutingSignalType.Audio);
|
||||
}
|
||||
|
||||
var audioSuccess = destination.GetRouteToSource(source, null, null, signalType.HasFlag(eRoutingSignalType.SecondaryAudio) ? eRoutingSignalType.SecondaryAudio : eRoutingSignalType.Audio, 0, audioRouteDescriptor, destinationPort, sourcePort);
|
||||
|
||||
if (!audioSuccess)
|
||||
Debug.LogMessage(LogEventLevel.Debug, "Cannot find audio route to {0}", destination, source.Key);
|
||||
Debug.LogDebug(destination, "Cannot find audio route to {0}", source.Key);
|
||||
|
||||
var videoRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.Video);
|
||||
var videoRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, sourcePort, eRoutingSignalType.Video);
|
||||
|
||||
var videoSuccess = destination.GetRouteToSource(source, null, null, eRoutingSignalType.Video, 0, videoRouteDescriptor, destinationPort, sourcePort);
|
||||
|
||||
if (!videoSuccess)
|
||||
Debug.LogMessage(LogEventLevel.Debug, "Cannot find video route to {0}", destination, source.Key);
|
||||
Debug.LogDebug(destination, "Cannot find video route to {0}", source.Key);
|
||||
|
||||
foreach (var route in audioRouteDescriptor.Routes)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Audio route for device: {route}", destination, route.ToString());
|
||||
Debug.LogVerbose(destination, "Audio route for device: {route}", route.ToString());
|
||||
}
|
||||
|
||||
foreach (var route in videoRouteDescriptor.Routes)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Video route for device: {route}", destination, route.ToString());
|
||||
Debug.LogVerbose(destination, "Video route for device: {route}", route.ToString());
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -304,8 +275,8 @@ namespace PepperDash.Essentials.Core
|
|||
{
|
||||
if (destination == null) throw new ArgumentNullException(nameof(destination));
|
||||
if (source == null) throw new ArgumentNullException(nameof(source));
|
||||
if (destinationPort == null) Debug.LogMessage(LogEventLevel.Information, "Destination port is null");
|
||||
if (sourcePort == null) Debug.LogMessage(LogEventLevel.Information, "Source port is null");
|
||||
if (destinationPort == null) Debug.LogDebug("Destination port is null");
|
||||
if (sourcePort == null) Debug.LogDebug("Source port is null");
|
||||
|
||||
var routeRequest = new RouteRequest
|
||||
{
|
||||
|
|
@ -327,7 +298,7 @@ namespace PepperDash.Essentials.Core
|
|||
|
||||
RouteRequests[destination.Key] = routeRequest;
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information, "Device: {destination} is cooling down and already has a routing request stored. Storing new route request to route to source key: {sourceKey}", null, destination.Key, routeRequest.Source.Key);
|
||||
Debug.LogInformation("Device: {destination} is cooling down and already has a routing request stored. Storing new route request to route to source key: {sourceKey}", destination.Key, routeRequest.Source.Key);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -339,7 +310,7 @@ namespace PepperDash.Essentials.Core
|
|||
|
||||
RouteRequests.Add(destination.Key, routeRequest);
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information, "Device: {destination} is cooling down. Storing route request to route to source key: {sourceKey}", null, destination.Key, routeRequest.Source.Key);
|
||||
Debug.LogInformation("Device: {destination} is cooling down. Storing route request to route to source key: {sourceKey}", destination.Key, routeRequest.Source.Key);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -351,7 +322,7 @@ namespace PepperDash.Essentials.Core
|
|||
|
||||
RouteRequests.Remove(destination.Key);
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information, "Device: {destination} is NOT cooling down. Removing stored route request and routing to source key: {sourceKey}", null, destination.Key, routeRequest.Source.Key);
|
||||
Debug.LogInformation("Device: {destination} is NOT cooling down. Removing stored route request and routing to source key: {sourceKey}", destination.Key, routeRequest.Source.Key);
|
||||
}
|
||||
|
||||
routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, destinationPort?.Key ?? string.Empty, false));
|
||||
|
|
@ -372,8 +343,10 @@ namespace PepperDash.Essentials.Core
|
|||
IndexTieLines();
|
||||
}
|
||||
|
||||
var sinks = DeviceManager.AllDevices.OfType<IRoutingInputs>().Where(d => !(d is IRoutingInputsOutputs));
|
||||
var sources = DeviceManager.AllDevices.OfType<IRoutingOutputs>().Where(d => !(d is IRoutingInputsOutputs));
|
||||
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)
|
||||
{
|
||||
|
|
@ -383,13 +356,17 @@ namespace PepperDash.Essentials.Core
|
|||
{
|
||||
foreach (var outputPort in source.OutputPorts)
|
||||
{
|
||||
var (audioOrSingleRoute, videoRoute) = sink.GetRouteToSource(source, inputPort.Type, inputPort, outputPort);
|
||||
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
|
||||
|
|
@ -398,6 +375,10 @@ namespace PepperDash.Essentials.Core
|
|||
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))
|
||||
|
|
@ -429,6 +410,10 @@ namespace PepperDash.Essentials.Core
|
|||
continue;
|
||||
}
|
||||
|
||||
Debug.LogVerbose("Video route mapped: {source} -> {sink} via {input}/{output}",
|
||||
source.Key, sink.Key,
|
||||
inputPort.Key, outputPort.Key);
|
||||
|
||||
RouteDescriptors[eRoutingSignalType.Video].AddRouteDescriptor(videoRoute);
|
||||
}
|
||||
}
|
||||
|
|
@ -467,7 +452,8 @@ namespace PepperDash.Essentials.Core
|
|||
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.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))
|
||||
|
|
@ -475,7 +461,8 @@ namespace PepperDash.Essentials.Core
|
|||
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.DestinationPort == null || d.InputPort?.Key == request.DestinationPort.Key) &&
|
||||
(request.SourcePort == null || d.OutputPort?.Key == request.SourcePort.Key));
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -490,14 +477,15 @@ namespace PepperDash.Essentials.Core
|
|||
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.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.LogMessage(LogEventLevel.Debug, "No pre-loaded route found, building dynamically", request.Destination);
|
||||
Debug.LogDebug(request.Destination, "No pre-loaded route found, building dynamically");
|
||||
(audioOrSingleRoute, videoRoute) = request.Destination.GetRouteToSource(request.Source, request.SignalType, request.DestinationPort, request.SourcePort);
|
||||
}
|
||||
|
||||
|
|
@ -511,14 +499,15 @@ namespace PepperDash.Essentials.Core
|
|||
RouteDescriptorCollection.DefaultCollection.AddRouteDescriptor(videoRoute);
|
||||
}
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Executing full route", request.Destination);
|
||||
Debug.LogVerbose(request.Destination, "Executing full route");
|
||||
|
||||
audioOrSingleRoute.ExecuteRoutes();
|
||||
videoRoute?.ExecuteRoutes();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogMessage(ex, "Exception Running Route Request {request}", null, request);
|
||||
Debug.LogError("Exception Running Route Request {request}: {exception}", request, ex.Message);
|
||||
Debug.LogDebug(ex, "Stack Trace: ");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -532,7 +521,7 @@ namespace PepperDash.Essentials.Core
|
|||
{
|
||||
try
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Release route for '{destination}':'{inputPortKey}'", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey);
|
||||
Debug.LogInformation(destination, "Release route for '{destination}':'{inputPortKey}'", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey);
|
||||
|
||||
if (RouteRequests.TryGetValue(destination.Key, out RouteRequest existingRequest) && destination is IWarmingCooling)
|
||||
{
|
||||
|
|
@ -546,13 +535,14 @@ namespace PepperDash.Essentials.Core
|
|||
var current = RouteDescriptorCollection.DefaultCollection.RemoveRouteDescriptor(destination, inputPortKey);
|
||||
if (current != null)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Releasing current route: {0}", destination, current.Source.Key);
|
||||
Debug.LogInformation(destination, "Releasing current route: {0}", current.Source.Key);
|
||||
current.ReleaseRoutes(clearRoute);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogMessage(ex, "Exception releasing route for '{destination}':'{inputPortKey}'", null, destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey);
|
||||
Debug.LogError("Exception releasing route for '{destination}':'{inputPortKey}': {exception}", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey, ex.Message);
|
||||
Debug.LogDebug(ex, "Stack Trace: ");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -577,15 +567,7 @@ namespace PepperDash.Essentials.Core
|
|||
{
|
||||
cycle++;
|
||||
|
||||
// Check if this route has already been determined to be impossible
|
||||
var routeKey = GetRouteKey(source.Key, destination.Key, signalType);
|
||||
if (_impossibleRoutes.Contains(routeKey))
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Route {0} is cached as impossible, skipping", null, routeKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
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());
|
||||
Debug.LogVerbose("GetRouteToSource: {cycle} {sourceKey}:{sourcePortKey}--> {destinationKey}:{destinationPortKey} {type}", null, cycle, source.Key, sourcePort?.Key ?? "auto", destination.Key, destinationPort?.Key ?? "auto", signalType.ToString());
|
||||
|
||||
RoutingInputPort goodInputPort = null;
|
||||
|
||||
|
|
@ -633,12 +615,16 @@ namespace PepperDash.Essentials.Core
|
|||
}
|
||||
else // no direct-connect. Walk back devices.
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "is not directly connected to {sourceKey}. Walking down tie lines", destination, source.Key);
|
||||
Debug.LogVerbose(destination, "is not directly connected to {sourceKey}. Walking down tie lines", source.Key);
|
||||
|
||||
// No direct tie? Run back out on the inputs' attached devices...
|
||||
// Only the ones that are routing devices
|
||||
var midpointTieLines = destinationTieLines.Where(t => t.SourcePort.ParentDevice is IRoutingInputsOutputs);
|
||||
var midpointTieLines = destinationTieLines
|
||||
.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
|
||||
if (alreadyCheckedDevices == null)
|
||||
alreadyCheckedDevices = new List<IRoutingInputsOutputs>();
|
||||
|
|
@ -651,13 +637,13 @@ namespace PepperDash.Essentials.Core
|
|||
// Check if this previous device has already been walked
|
||||
if (alreadyCheckedDevices.Contains(midpointDevice))
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Skipping input {midpointDeviceKey} on {destinationKey}, this was already checked", destination, midpointDevice.Key, destination.Key);
|
||||
Debug.LogVerbose(destination, "Skipping input {midpointDeviceKey} on {destinationKey}, this was already checked", midpointDevice.Key, destination.Key);
|
||||
continue;
|
||||
}
|
||||
|
||||
var midpointOutputPort = tieLine.SourcePort;
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Trying to find route on {midpointDeviceKey}", destination, midpointDevice.Key);
|
||||
Debug.LogVerbose(destination, "Trying to find route on {midpointDeviceKey}", midpointDevice.Key);
|
||||
|
||||
// haven't seen this device yet. Do it. Pass the output port to the next
|
||||
// level to enable switching on success
|
||||
|
|
@ -666,9 +652,9 @@ namespace PepperDash.Essentials.Core
|
|||
|
||||
if (upstreamRoutingSuccess)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Upstream device route found", destination);
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Route found on {midpointDeviceKey}", destination, midpointDevice.Key);
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "TieLine: SourcePort: {SourcePort} DestinationPort: {DestinationPort}", destination, tieLine.SourcePort, tieLine.DestinationPort);
|
||||
Debug.LogVerbose(destination, "Upstream device route found");
|
||||
Debug.LogVerbose(destination, "Route found on {midpointDeviceKey}", midpointDevice.Key);
|
||||
Debug.LogVerbose(destination, "TieLine: SourcePort: {SourcePort} DestinationPort: {DestinationPort}", tieLine.SourcePort, tieLine.DestinationPort);
|
||||
goodInputPort = tieLine.DestinationPort;
|
||||
break; // Stop looping the inputs in this cycle
|
||||
}
|
||||
|
|
@ -678,10 +664,7 @@ namespace PepperDash.Essentials.Core
|
|||
|
||||
if (goodInputPort == null)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "No route found to {0}", destination, source.Key);
|
||||
|
||||
// Cache this as an impossible route
|
||||
_impossibleRoutes.Add(routeKey);
|
||||
Debug.LogVerbose(destination, "No route found to {0} from destination {1} for type {2}", source.Key, destination.Key, signalType);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
@ -698,7 +681,7 @@ namespace PepperDash.Essentials.Core
|
|||
routeTable.Routes.Add(new RouteSwitchDescriptor(outputPortToUse, goodInputPort));
|
||||
}
|
||||
else // device is merely IRoutingInputOutputs
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "No routing. Passthrough device", destination);
|
||||
Debug.LogVerbose(destination, "No routing. Passthrough device");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,22 +20,27 @@ namespace PepperDash.Essentials.Core
|
|||
public IRoutingInputs Destination { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the InputPort
|
||||
/// The InputPort on the destination device for this route, if applicable. May be null if the route is not for a specific input port.
|
||||
/// </summary>
|
||||
public RoutingInputPort InputPort { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Source
|
||||
/// Gets the source device (sink or midpoint) for the route.
|
||||
/// </summary>
|
||||
public IRoutingOutputs Source { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SignalType
|
||||
/// Gets the OutputPort on the source device for this route, if applicable. May be null if the route is not for a specific output port.
|
||||
/// </summary>
|
||||
public RoutingOutputPort OutputPort { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the signal type for this route.
|
||||
/// </summary>
|
||||
public eRoutingSignalType SignalType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Routes
|
||||
/// Gets the collection of route switch descriptors for this route.
|
||||
/// </summary>
|
||||
public List<RouteSwitchDescriptor> Routes { get; private set; }
|
||||
|
||||
|
|
@ -56,11 +61,24 @@ namespace PepperDash.Essentials.Core
|
|||
/// <param name="destination">The destination device.</param>
|
||||
/// <param name="inputPort">The destination input port (optional).</param>
|
||||
/// <param name="signalType">The signal type for this route.</param>
|
||||
public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, RoutingInputPort inputPort, eRoutingSignalType signalType)
|
||||
public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, RoutingInputPort inputPort, eRoutingSignalType signalType) : this(source, destination, inputPort, null, signalType)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RouteDescriptor"/> class for a route with specific destination input and source output ports.
|
||||
/// </summary>
|
||||
/// <param name="source"></param>
|
||||
/// <param name="destination"></param>
|
||||
/// <param name="inputPort"></param>
|
||||
/// <param name="outputPort"></param>
|
||||
/// <param name="signalType"></param>
|
||||
public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, RoutingInputPort inputPort, RoutingOutputPort outputPort, eRoutingSignalType signalType)
|
||||
{
|
||||
Destination = destination;
|
||||
InputPort = inputPort;
|
||||
Source = source;
|
||||
OutputPort = outputPort;
|
||||
SignalType = signalType;
|
||||
Routes = new List<RouteSwitchDescriptor>();
|
||||
}
|
||||
|
|
@ -72,7 +90,7 @@ namespace PepperDash.Essentials.Core
|
|||
{
|
||||
foreach (var route in Routes)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "ExecuteRoutes: {0}", null, route.ToString());
|
||||
Debug.LogVerbose("ExecuteRoutes: {0}", route.ToString());
|
||||
|
||||
if (route.SwitchingDevice is IRoutingSinkWithSwitching sink)
|
||||
{
|
||||
|
|
@ -86,7 +104,7 @@ namespace PepperDash.Essentials.Core
|
|||
|
||||
route.OutputPort.InUseTracker.AddUser(Destination, "destination-" + SignalType);
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Output port {0} routing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue);
|
||||
Debug.LogVerbose("Output port {0} routing. Count={1}", route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -112,6 +130,7 @@ namespace PepperDash.Essentials.Core
|
|||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError("Error executing switch: {exception}", e.Message);
|
||||
Debug.LogDebug(e, "Stack Trace: ");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -123,11 +142,11 @@ namespace PepperDash.Essentials.Core
|
|||
if (route.OutputPort.InUseTracker != null)
|
||||
{
|
||||
route.OutputPort.InUseTracker.RemoveUser(Destination, "destination-" + SignalType);
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Port {0} releasing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue);
|
||||
Debug.LogVerbose("Port {0} releasing. Count={1}", route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Error, "InUseTracker is null for OutputPort {0}", null, route.OutputPort.Key);
|
||||
Debug.LogVerbose("InUseTracker is null for OutputPort {0}", route.OutputPort.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,20 +51,25 @@ namespace PepperDash.Essentials.Core
|
|||
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.InputPort != null && descriptor.InputPort != null && t.InputPort.Key == descriptor.InputPort.Key)) &&
|
||||
((t.OutputPort == null && descriptor.OutputPort == null) ||
|
||||
(t.OutputPort != null && descriptor.OutputPort != null && t.OutputPort.Key == descriptor.OutputPort.Key)));
|
||||
|
||||
if (existingRoute != null)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, descriptor.Destination,
|
||||
"Route from {0} to {1}:{2} ({3}) already exists in this collection",
|
||||
Debug.LogInformation(descriptor.Destination,
|
||||
"Route from {source}:{outputPort} to {destination}:{inputPort} ({signalType}) already exists in this collection",
|
||||
descriptor?.Source?.Key,
|
||||
descriptor?.OutputPort?.Key ?? "auto",
|
||||
descriptor?.Destination?.Key,
|
||||
descriptor?.InputPort?.Key ?? "auto",
|
||||
descriptor?.SignalType);
|
||||
descriptor?.SignalType
|
||||
);
|
||||
return;
|
||||
}
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Adding route descriptor: {0} -> {1}:{2} ({3})",
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Crestron.SimplSharp;
|
||||
using System.Timers;
|
||||
using PepperDash.Core;
|
||||
using PepperDash.Essentials.Core.Config;
|
||||
|
||||
|
|
@ -21,7 +21,12 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
/// <summary>
|
||||
/// Debounce timers for each sink device to prevent rapid successive updates
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, CTimer> updateTimers = new Dictionary<string, CTimer>();
|
||||
private readonly Dictionary<string, Timer> updateTimers = new Dictionary<string, Timer>();
|
||||
|
||||
/// <summary>
|
||||
/// Lock object protecting all access to <see cref="updateTimers"/>.
|
||||
/// </summary>
|
||||
private readonly object _timerLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Debounce delay in milliseconds
|
||||
|
|
@ -50,7 +55,6 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
midpointToSinksMap = new Dictionary<string, HashSet<string>>();
|
||||
|
||||
var sinks = DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
|
||||
var midpoints = DeviceManager.AllDevices.OfType<IRoutingWithFeedback>();
|
||||
|
||||
foreach (var sink in sinks)
|
||||
{
|
||||
|
|
@ -62,6 +66,9 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
|
||||
foreach (var midpointKey in upstreamMidpoints)
|
||||
{
|
||||
if (string.IsNullOrEmpty(midpointKey))
|
||||
continue;
|
||||
|
||||
if (!midpointToSinksMap.ContainsKey(midpointKey))
|
||||
midpointToSinksMap[midpointKey] = new HashSet<string>();
|
||||
|
||||
|
|
@ -111,7 +118,8 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
|
||||
if (tieLine.SourcePort.ParentDevice is IRoutingWithFeedback midpoint)
|
||||
{
|
||||
midpoints.Add(midpoint.Key);
|
||||
if (!string.IsNullOrEmpty(midpoint.Key))
|
||||
midpoints.Add(midpoint.Key);
|
||||
|
||||
// Find upstream TieLines connected to this midpoint's inputs
|
||||
var midpointInputs = (midpoint as IRoutingInputs)?.InputPorts;
|
||||
|
|
@ -211,9 +219,49 @@ 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>
|
||||
/// Handles the InputChanged event from a sink device.
|
||||
/// Triggers an update for the specific sink device.
|
||||
/// Updates the midpoint-to-sink map for the new input path, then triggers
|
||||
/// a source-info update for the sink.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sink device that reported an input change.</param>
|
||||
/// <param name="currentInputPort">The new input port selected on the sink device.</param>
|
||||
|
|
@ -224,6 +272,10 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
{
|
||||
try
|
||||
{
|
||||
// Keep the map current so HandleMidpointUpdate can find this sink
|
||||
if (sender is IRoutingSinkWithSwitchingWithInputPort sinkWithInputPort)
|
||||
RebuildMapForSink(sinkWithInputPort);
|
||||
|
||||
UpdateDestination(sender, currentInputPort);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -255,15 +307,13 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
|
||||
var key = destination.Key;
|
||||
|
||||
// Cancel existing timer for this sink
|
||||
if (updateTimers.TryGetValue(key, out var existingTimer))
|
||||
{
|
||||
existingTimer.Stop();
|
||||
existingTimer.Dispose();
|
||||
}
|
||||
// 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;
|
||||
|
||||
// Start new debounced timer
|
||||
updateTimers[key] = new CTimer(_ =>
|
||||
newTimer = new Timer(DEBOUNCE_MS) { AutoReset = false };
|
||||
newTimer.Elapsed += (s, e) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -281,13 +331,36 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
}
|
||||
finally
|
||||
{
|
||||
if (updateTimers.ContainsKey(key))
|
||||
// Remove the entry first so a concurrent UpdateDestination call
|
||||
// cannot re-dispose whatever timer we're about to dispose.
|
||||
Timer selfTimer = null;
|
||||
lock (_timerLock)
|
||||
{
|
||||
updateTimers[key]?.Dispose();
|
||||
updateTimers.Remove(key);
|
||||
if (updateTimers.TryGetValue(key, out var current) && ReferenceEquals(current, newTimer))
|
||||
{
|
||||
selfTimer = current;
|
||||
updateTimers.Remove(key);
|
||||
}
|
||||
}
|
||||
selfTimer?.Dispose();
|
||||
}
|
||||
}, null, DEBOUNCE_MS);
|
||||
};
|
||||
|
||||
lock (_timerLock)
|
||||
{
|
||||
if (updateTimers.TryGetValue(key, out var existingTimer))
|
||||
timerToDispose = existingTimer;
|
||||
|
||||
updateTimers[key] = newTimer;
|
||||
}
|
||||
|
||||
// Dispose the old timer outside the lock to avoid holding the lock during disposal.
|
||||
// Dispose implicitly stops the timer, preventing its Elapsed event from firing.
|
||||
timerToDispose?.Dispose();
|
||||
|
||||
// Start after the lock is released so the Elapsed callback cannot deadlock
|
||||
// trying to acquire _timerLock while we still hold it.
|
||||
newTimer.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -384,7 +457,8 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogMessage(ex, "Error getting sourceTieLine: {Exception}", this, ex);
|
||||
Debug.LogError(this, "Error getting sourceTieLine: {message}", ex.Message);
|
||||
Debug.LogDebug(ex, "StackTrace: ");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -408,6 +482,11 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
return roomDefaultDisplay.DefaultDisplay.Key == destination.Key;
|
||||
}
|
||||
|
||||
if (ConfigReader.ConfigObject.GetDestinationListForKey(r.DestinationListKey)?.FirstOrDefault(d => d.Value.SinkKey == destination.Key) != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
|
@ -429,10 +508,8 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
|
||||
if (sourceList == null)
|
||||
{
|
||||
Debug.LogMessage(
|
||||
Serilog.Events.LogEventLevel.Debug,
|
||||
Debug.LogDebug(this,
|
||||
"No source list found for source list key {key}. Unable to find source for tieLine {sourceTieLine}",
|
||||
this,
|
||||
room.SourceListKey,
|
||||
sourceTieLine
|
||||
);
|
||||
|
|
@ -461,10 +538,8 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
|
||||
if (source == null)
|
||||
{
|
||||
Debug.LogMessage(
|
||||
Serilog.Events.LogEventLevel.Debug,
|
||||
Debug.LogDebug(this,
|
||||
"No source found for device {key}. Creating transient source for {destination}",
|
||||
this,
|
||||
sourceTieLine.SourcePort.ParentDevice.Key,
|
||||
destination
|
||||
);
|
||||
|
|
@ -498,10 +573,8 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
{
|
||||
if (!(tieLine.DestinationPort.ParentDevice is IRoutingInputs sink))
|
||||
{
|
||||
Debug.LogMessage(
|
||||
Serilog.Events.LogEventLevel.Debug,
|
||||
Debug.LogDebug(this,
|
||||
"TieLine destination {device} is not IRoutingInputs",
|
||||
this,
|
||||
tieLine.DestinationPort.ParentDevice.Key
|
||||
);
|
||||
return null;
|
||||
|
|
@ -540,17 +613,21 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
|
||||
if (route != null && route.Routes != null && route.Routes.Count > 0)
|
||||
{
|
||||
// Found a valid route - return the source TieLine
|
||||
// Routes[0] is the hop nearest the source: its InputPort is the
|
||||
// port on the first switching device that receives the signal from
|
||||
// the source side. The TieLine whose DestinationPort matches that
|
||||
// port is the exact tie that was traversed, giving us the precise
|
||||
// source output port via SourcePort — regardless of how many output
|
||||
// ports the source device has.
|
||||
var firstHop = route.Routes[0];
|
||||
var sourceTieLine = TieLineCollection.Default.FirstOrDefault(tl =>
|
||||
tl.SourcePort.ParentDevice.Key == source.Key &&
|
||||
tl.Type.HasFlag(signalType));
|
||||
tl.DestinationPort.Key == firstHop.InputPort.Key &&
|
||||
tl.DestinationPort.ParentDevice.Key == firstHop.InputPort.ParentDevice.Key);
|
||||
|
||||
if (sourceTieLine != null)
|
||||
{
|
||||
Debug.LogMessage(
|
||||
Serilog.Events.LogEventLevel.Debug,
|
||||
"Found route from {source} to {sink} with {count} hops",
|
||||
this,
|
||||
Debug.LogDebug(this,
|
||||
"Found route from {source} to {sink} with {count} hops",
|
||||
source.Key,
|
||||
sink.Key,
|
||||
route.Routes.Count
|
||||
|
|
@ -561,22 +638,14 @@ namespace PepperDash.Essentials.Core.Routing
|
|||
}
|
||||
}
|
||||
|
||||
Debug.LogMessage(
|
||||
Serilog.Events.LogEventLevel.Debug,
|
||||
"No route found to any source from {sink}",
|
||||
this,
|
||||
sink.Key
|
||||
);
|
||||
Debug.LogDebug(this, "No route found to any source from {sink}", sink.Key);
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogMessage(
|
||||
ex,
|
||||
"Error getting root tieLine: {Exception}",
|
||||
this,
|
||||
ex
|
||||
);
|
||||
Debug.LogError(this, "Error getting root tieLine: {message}", ex.Message);
|
||||
Debug.LogDebug(ex, "StackTrace: ");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,16 +27,19 @@ namespace PepperDash.Essentials.Core
|
|||
/// <summary>
|
||||
/// Control signal type
|
||||
/// </summary>
|
||||
[Obsolete("UsbOutput is no longer supported and will be removed in a future release.")]
|
||||
UsbOutput = 8,
|
||||
|
||||
/// <summary>
|
||||
/// Control signal type
|
||||
/// </summary>
|
||||
[Obsolete("UsbInput is no longer supported and will be removed in a future release.")]
|
||||
UsbInput = 16,
|
||||
|
||||
/// <summary>
|
||||
/// Secondary audio signal type
|
||||
/// </summary>
|
||||
[Obsolete("SecondaryAudio is no longer supported and will be removed in a future release.")]
|
||||
SecondaryAudio = 32
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using Crestron.SimplSharp;
|
||||
using PepperDash.Core;
|
||||
using PepperDash.Core.Logging;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace PepperDash.Essentials.Core
|
||||
|
|
@ -296,19 +297,14 @@ namespace PepperDash.Essentials.Core
|
|||
{
|
||||
var secretPresent = provider.TestSecret(key);
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Verbose, provider, "SecretsProvider {0} {1} contain a secret entry for {2}", provider.Key, secretPresent ? "does" : "does not", key);
|
||||
provider.LogVerbose("SecretsProvider {0} {1} contain a secret entry for {2}", provider.Key, secretPresent ? "does" : "does not", key);
|
||||
|
||||
if (!secretPresent)
|
||||
return
|
||||
String.Format(
|
||||
"Unable to update secret for {0}:{1} - Please use the 'SetSecret' command to modify it");
|
||||
$"Unable to update secret for {provider.Key}:{key} - Please use the 'SetSecret' command to modify it";
|
||||
var response = provider.SetSecret(key, secret)
|
||||
? String.Format(
|
||||
"Secret successfully set for {0}:{1}",
|
||||
provider.Key, key)
|
||||
: String.Format(
|
||||
"Unable to set secret for {0}:{1}",
|
||||
provider.Key, key);
|
||||
? $"Secret successfully set for {provider.Key}:{key}"
|
||||
: $"Unable to set secret for {provider.Key}:{key}";
|
||||
return response;
|
||||
}
|
||||
|
||||
|
|
@ -316,19 +312,14 @@ namespace PepperDash.Essentials.Core
|
|||
{
|
||||
var secretPresent = provider.TestSecret(key);
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Verbose, provider, "SecretsProvider {0} {1} contain a secret entry for {2}", provider.Key, secretPresent ? "does" : "does not", key);
|
||||
provider.LogVerbose("SecretsProvider {0} {1} contain a secret entry for {2}", provider.Key, secretPresent ? "does" : "does not", key);
|
||||
|
||||
if (secretPresent)
|
||||
return
|
||||
String.Format(
|
||||
"Unable to set secret for {0}:{1} - Please use the 'UpdateSecret' command to modify it");
|
||||
$"Unable to set secret for {provider.Key}:{key} - Please use the 'UpdateSecret' command to modify it";
|
||||
var response = provider.SetSecret(key, secret)
|
||||
? String.Format(
|
||||
"Secret successfully set for {0}:{1}",
|
||||
provider.Key, key)
|
||||
: String.Format(
|
||||
"Unable to set secret for {0}:{1}",
|
||||
provider.Key, key);
|
||||
? $"Secret successfully set for {provider.Key}:{key}"
|
||||
: $"Unable to set secret for {provider.Key}:{key}";
|
||||
return response;
|
||||
|
||||
}
|
||||
|
|
@ -377,15 +368,10 @@ namespace PepperDash.Essentials.Core
|
|||
|
||||
var key = args[1];
|
||||
|
||||
|
||||
provider.SetSecret(key, "");
|
||||
response = provider.SetSecret(key, "")
|
||||
? String.Format(
|
||||
"Secret successfully deleted for {0}:{1}",
|
||||
provider.Key, key)
|
||||
: String.Format(
|
||||
"Unable to delete secret for {0}:{1}",
|
||||
provider.Key, key);
|
||||
? $"Secret successfully deleted for {provider.Key}:{key}"
|
||||
: $"Unable to delete secret for {provider.Key}:{key}";
|
||||
CrestronConsole.ConsoleCommandResponse(response);
|
||||
return;
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ namespace PepperDash.Essentials.Core.Web
|
|||
{
|
||||
private readonly WebApiServer _server;
|
||||
|
||||
private readonly WebApiServer _debugServer;
|
||||
|
||||
|
||||
|
||||
|
||||
///<example>
|
||||
/// http(s)://{ipaddress}/cws/{basePath}
|
||||
/// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
|
||||
|
|
@ -70,6 +75,9 @@ namespace PepperDash.Essentials.Core.Web
|
|||
|
||||
_server = new WebApiServer(Key, Name, BasePath);
|
||||
|
||||
_debugServer = new WebApiServer($"{key}-debug-app", $"{name} Debug App", "/debug");
|
||||
_debugServer.SetFallbackHandler(new ServeDebugAppRequestHandler());
|
||||
|
||||
SetupRoutes();
|
||||
}
|
||||
|
||||
|
|
@ -87,6 +95,11 @@ namespace PepperDash.Essentials.Core.Web
|
|||
Name = "ReportVersions",
|
||||
RouteHandler = new ReportVersionsRequestHandler()
|
||||
},
|
||||
new HttpCwsRoute("packageManifest")
|
||||
{
|
||||
Name = "GetPackageManifest",
|
||||
RouteHandler = new GetPackageManifestRequestHandler()
|
||||
},
|
||||
new HttpCwsRoute("appdebug")
|
||||
{
|
||||
Name = "AppDebug",
|
||||
|
|
@ -242,6 +255,7 @@ namespace PepperDash.Essentials.Core.Web
|
|||
Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on {0} Appliance", is4Series ? "4-series" : "3-series");
|
||||
|
||||
_server.Start();
|
||||
_debugServer.Start();
|
||||
|
||||
GetPaths();
|
||||
|
||||
|
|
@ -252,12 +266,13 @@ namespace PepperDash.Essentials.Core.Web
|
|||
Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on Virtual Control Server");
|
||||
|
||||
_server.Start();
|
||||
_debugServer.Start();
|
||||
|
||||
GetPaths();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Print the available pahts
|
||||
/// Print the available paths
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// http(s)://{ipaddress}/cws/{basePath}
|
||||
|
|
@ -293,7 +308,15 @@ namespace PepperDash.Essentials.Core.Web
|
|||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, this, "{routeName:l}: {routePath:l}/{routeUrl:l}", route.Name, path, route.Url);
|
||||
}
|
||||
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,7 +36,7 @@ namespace PepperDash.Essentials.Core.Web
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// MapToDeviceListObject method
|
||||
|
|
@ -119,9 +119,5 @@ namespace PepperDash.Essentials.Core.Web
|
|||
};
|
||||
}
|
||||
|
||||
internal static bool IsAuthenticated(HttpCwsContext context)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,10 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
|||
/// Represents a DebugSessionRequestHandler
|
||||
/// </summary>
|
||||
public class DebugSessionRequestHandler : WebApiBaseRequestHandler
|
||||
{
|
||||
{
|
||||
private CTimer _portForwardTimeoutTimer;
|
||||
private readonly object _timerLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
|
|
@ -48,6 +51,7 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
|||
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
|
||||
|
||||
var port = 0;
|
||||
string csIp = null;
|
||||
|
||||
if (!Debug.WebsocketSink.IsRunning)
|
||||
{
|
||||
|
|
@ -59,14 +63,50 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
|||
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;
|
||||
|
||||
object data = new
|
||||
var 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, "Fallback Debug Session URL: {0}", data.fallbackUrl);
|
||||
|
||||
// Return the port number with the full url of the WS Server
|
||||
var res = JsonConvert.SerializeObject(data);
|
||||
|
|
@ -90,8 +130,49 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
|||
/// <param name="context"></param>
|
||||
protected override void HandlePost(HttpCwsContext context)
|
||||
{
|
||||
CancelPortForwardTimeout();
|
||||
|
||||
var port = Debug.WebsocketSink.Port;
|
||||
|
||||
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.StatusDescription = "OK";
|
||||
context.Response.End();
|
||||
|
|
@ -99,5 +180,55 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
|||
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,13 +1,14 @@
|
|||
using System.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Crestron.SimplSharp.WebScripting;
|
||||
using Newtonsoft.Json;
|
||||
using PepperDash.Core.Web.RequestHandlers;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a GetFeedbacksForDeviceRequestHandler
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Represents a GetFeedbacksForDeviceRequestHandler
|
||||
/// </summary>
|
||||
public class GetFeedbacksForDeviceRequestHandler : WebApiBaseRequestHandler
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -76,7 +77,7 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
|||
Value = feedback.IntValue
|
||||
};
|
||||
|
||||
var stringFeedback =
|
||||
var stringFeedback =
|
||||
from feedback in device.Feedbacks.OfType<StringFeedback>()
|
||||
where !string.IsNullOrEmpty(feedback.Key)
|
||||
select new
|
||||
|
|
|
|||
|
|
@ -0,0 +1,259 @@
|
|||
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,5 +1,6 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Crestron.SimplSharp.CrestronAuthentication;
|
||||
using Crestron.SimplSharp.WebScripting;
|
||||
using Newtonsoft.Json;
|
||||
|
|
@ -71,8 +72,10 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
|||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +92,19 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
|||
context.Response.StatusDescription = "OK";
|
||||
context.Response.ContentType = "application/json";
|
||||
context.Response.ContentEncoding = System.Text.Encoding.UTF8;
|
||||
context.Response.Write(JsonConvert.SerializeObject(new { Token = token }, Formatting.Indented), false);
|
||||
context.Response.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)
|
||||
|
|
@ -119,4 +134,40 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
|||
/// </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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
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,6 +17,8 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
|
|||
/// <summary>
|
||||
/// Represents a CameraVisca
|
||||
/// </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
|
||||
{
|
||||
private readonly CameraViscaPropertiesConfig PropertiesConfig;
|
||||
|
|
|
|||
|
|
@ -152,6 +152,15 @@ namespace PepperDash.Essentials.Devices.Common.Shades
|
|||
|
||||
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 (
|
||||
!DisplayDevice.IsCoolingDownFeedback.BoolValue
|
||||
&& Type == eScreenLiftControlType.lift
|
||||
|
|
@ -174,6 +183,15 @@ namespace PepperDash.Essentials.Devices.Common.Shades
|
|||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,5 +41,23 @@ namespace PepperDash.Essentials.Devices.Common.Shades
|
|||
/// </summary>
|
||||
[JsonProperty("muteOnScreenUp")]
|
||||
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" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.274" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -21,7 +21,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
|
|||
/// <summary>
|
||||
/// Represents a MockVC
|
||||
/// </summary>
|
||||
public class MockVC : VideoCodecBase, IRoutingSource, IHasCallHistory, IHasScheduleAwareness, IHasCallFavorites, IHasDirectory, IHasCodecCameras, IHasCameraAutoMode, IHasCodecRoomPresets
|
||||
public class MockVC : VideoCodecBase, IRoutingSource, IHasCallHistory, IHasScheduleAwareness, IHasCallFavorites, IHasDirectory, IHasCodecCameras, IHasCameraAutoMode, IHasCodecRoomPresets, IRoutingInputs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the PropertiesConfig
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
|
|||
/// <summary>
|
||||
/// Base class for video codec devices
|
||||
/// </summary>
|
||||
public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutputs,
|
||||
public abstract class VideoCodecBase : ReconfigurableDevice,
|
||||
IUsageTracking, IHasDialer, IHasContentSharing, ICodecAudio, iVideoCodecInfo, IBridgeAdvanced, IHasStandbyMode
|
||||
{
|
||||
private const int XSigEncoding = 28591;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
|||
/// <summary>
|
||||
/// The device key
|
||||
/// </summary>
|
||||
[JsonProperty("key")]
|
||||
[JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)]
|
||||
/// <summary>
|
||||
/// Gets or sets the Key
|
||||
/// </summary>
|
||||
|
|
@ -19,19 +19,19 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
|||
/// <summary>
|
||||
/// The device name
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of the message class
|
||||
/// </summary>
|
||||
[JsonProperty("messageType")]
|
||||
[JsonProperty("messageType", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string MessageType => GetType().Name;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MessageBasePath
|
||||
/// </summary>
|
||||
[JsonProperty("messageBasePath")]
|
||||
[JsonProperty("messageBasePath", NullValueHandling = NullValueHandling.Ignore)]
|
||||
|
||||
public string MessageBasePath { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
|
|
@ -11,13 +12,15 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
|||
/// <summary>
|
||||
/// The interfaces implmented by the device sending the messsage
|
||||
/// </summary>
|
||||
[JsonProperty("interfaces")]
|
||||
[JsonProperty("interfaces", NullValueHandling = NullValueHandling.Ignore)]
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the interfaces implemented by the device sending the message
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
Interfaces = interfaces;
|
||||
|
|
|
|||
|
|
@ -264,6 +264,9 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
|||
|
||||
message.Name = _device.Name;
|
||||
|
||||
message.MessageBasePath = MessagePath;
|
||||
|
||||
|
||||
var token = JToken.FromObject(message);
|
||||
|
||||
PostStatusMessage(token, MessagePath, clientId);
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
<Compile Remove="Messengers\SIMPLVtcMessenger.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.274" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
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,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
|
|
@ -40,9 +41,10 @@ namespace PepperDash.Essentials
|
|||
public bool EnableApiServer { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Enable subscriptions for Messengers
|
||||
/// Enables subscriptions for messengers
|
||||
/// </summary>
|
||||
[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; }
|
||||
}
|
||||
|
||||
|
|
@ -288,4 +290,4 @@ namespace PepperDash.Essentials
|
|||
/// </summary>
|
||||
NEO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,11 +69,32 @@ namespace PepperDash.Essentials
|
|||
private readonly Dictionary<string, IMobileControlMessenger> _defaultMessengers =
|
||||
new Dictionary<string, IMobileControlMessenger>();
|
||||
|
||||
private readonly Dictionary<string, ConnectedClientVersionInfo> _connectedClientVersions =
|
||||
new Dictionary<string, ConnectedClientVersionInfo>(StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
private readonly object _connectedClientVersionsLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Get the custom messengers with subscriptions
|
||||
/// </summary>
|
||||
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>
|
||||
/// Get the default messengers
|
||||
/// </summary>
|
||||
|
|
@ -137,7 +158,7 @@ namespace PepperDash.Essentials
|
|||
"No system_url value defined in config. Checking for value from SIMPL Bridge."
|
||||
);
|
||||
|
||||
if (!string.IsNullOrEmpty(SystemUrl))
|
||||
if (string.IsNullOrEmpty(SystemUrl))
|
||||
{
|
||||
this.LogError(
|
||||
"No system_url value defined in config or SIMPL Bridge. Unable to connect to Mobile Control."
|
||||
|
|
@ -1782,6 +1803,28 @@ namespace PepperDash.Essentials
|
|||
" 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>
|
||||
|
|
@ -2181,6 +2224,8 @@ namespace PepperDash.Essentials
|
|||
var roomKey = content["roomKey"].Value<string>();
|
||||
var touchpanelKey = content.SelectToken("touchpanelKey");
|
||||
|
||||
TrackClientAppVersion(clientId, roomKey, touchpanelKey?.Value<string>(), content.SelectToken("appVersion")?.Value<string>());
|
||||
|
||||
if (_roomCombiner == null)
|
||||
{
|
||||
var message = new MobileControlMessage
|
||||
|
|
@ -2252,6 +2297,50 @@ namespace PepperDash.Essentials
|
|||
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)
|
||||
{
|
||||
if (touchpanelKeyToken == null)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
<Compile Remove="RoomBridges\SourceDeviceMapDictionary.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.274" />
|
||||
<PackageReference Include="WebSocketSharp-netstandard" Version="1.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -25,11 +25,28 @@ namespace PepperDash.Essentials.Touchpanel
|
|||
/// Mobile Control touchpanel controller that provides app control, Zoom integration,
|
||||
/// and mobile control functionality for Crestron touchpanels.
|
||||
/// </summary>
|
||||
public class MobileControlTouchpanelController : TouchpanelBase, IHasFeedback, ITswAppControl, ITswZoomControl, IDeviceInfoProvider, IMobileControlCrestronTouchpanelController, ITheme
|
||||
public class MobileControlTouchpanelController : TouchpanelBase, IHasFeedback, ITswAppControl, ITswZoomControl, IDeviceInfoProvider, IMobileControlCrestronTouchpanelController, ITheme, ICommunicationMonitor
|
||||
{
|
||||
private readonly MobileControlTouchpanelProperties localConfig;
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -128,6 +145,11 @@ namespace PepperDash.Essentials.Touchpanel
|
|||
{
|
||||
localConfig = config;
|
||||
|
||||
if (panel != null)
|
||||
{
|
||||
CommunicationMonitor = new CrestronGenericBaseCommunicationMonitor(this, panel, 120000, 300000);
|
||||
}
|
||||
|
||||
AddPostActivationAction(SubscribeForMobileControlUpdates);
|
||||
|
||||
ThemeFeedback = new StringFeedback($"{Key}-theme", () => Theme);
|
||||
|
|
@ -366,6 +388,8 @@ namespace PepperDash.Essentials.Touchpanel
|
|||
/// </summary>
|
||||
public override bool CustomActivate()
|
||||
{
|
||||
CommunicationMonitor?.Start();
|
||||
|
||||
var appMessenger = new ITswAppControlMessenger($"appControlMessenger-{Key}", $"/device/{Key}", this);
|
||||
|
||||
var zoomMessenger = new ITswZoomControlMessenger($"zoomControlMessenger-{Key}", $"/device/{Key}", this);
|
||||
|
|
@ -393,6 +417,17 @@ namespace PepperDash.Essentials.Touchpanel
|
|||
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>
|
||||
/// Handles device extender signal changes for system reserved signals.
|
||||
/// </summary>
|
||||
|
|
@ -519,15 +554,8 @@ namespace PepperDash.Essentials.Touchpanel
|
|||
return false;
|
||||
}) ? csIpAddress.ToString() : processorIp;
|
||||
|
||||
var match = Regex.Match(url, @"^http://([^:/]+):\d+/mc/app\?token=.+$");
|
||||
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}");
|
||||
// replace the host but preserve whatever scheme (http/https) is already present in the URL
|
||||
var updatedUrl = Regex.Replace(url, @"^(https?)://[^:/]+", $"$1://{ip}");
|
||||
|
||||
this.LogVerbose("Updated URL: {updatedUrl}", updatedUrl);
|
||||
|
||||
|
|
@ -740,7 +768,7 @@ namespace PepperDash.Essentials.Touchpanel
|
|||
/// </summary>
|
||||
public MobileControlTouchpanelControllerFactory()
|
||||
{
|
||||
TypeNames = new List<string>() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "mcxpanel", "mcdge1000" };
|
||||
TypeNames = new List<string>() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "mctsw1080", "mcts1080", "mcxpanel", "mcdge1000" };
|
||||
MinimumEssentialsFrameworkVersion = "2.0.0";
|
||||
|
||||
factories = new Dictionary<string, Func<uint, CrestronControlSystem, string, BasicTriListWithSmartObject>>
|
||||
|
|
@ -765,6 +793,8 @@ namespace PepperDash.Essentials.Touchpanel
|
|||
{"ts770", (id, controlSystem, projectName) => new Ts770(id, controlSystem)},
|
||||
{"tsw1070", (id, controlSystem, projectName) => new Tsw1070(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)}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
using Crestron.SimplSharp.WebScripting;
|
||||
using Newtonsoft.Json;
|
||||
using PepperDash.Core;
|
||||
using PepperDash.Core.Web.RequestHandlers;
|
||||
using PepperDash.Essentials.Core.Web;
|
||||
using PepperDash.Essentials.WebSocketServer;
|
||||
using Serilog.Events;
|
||||
|
||||
|
||||
namespace PepperDash.Essentials.WebApiHandlers
|
||||
{
|
||||
|
|
|
|||
|
|
@ -127,6 +127,16 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||
/// </summary>
|
||||
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>
|
||||
/// Gets the user app URL prefix
|
||||
/// </summary>
|
||||
|
|
@ -134,7 +144,8 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||
{
|
||||
get
|
||||
{
|
||||
return string.Format("http://{0}:{1}{2}?token=",
|
||||
return string.Format("{0}://{1}:{2}{3}?token=",
|
||||
HttpScheme,
|
||||
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
|
||||
Port,
|
||||
_userAppBaseHref);
|
||||
|
|
@ -273,7 +284,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||
{
|
||||
base.Initialize();
|
||||
|
||||
_server = new HttpServer(Port, false);
|
||||
_server = new HttpServer(Port, _parent.Config.DirectServer.Secure);
|
||||
|
||||
_server.OnGet += Server_OnGet;
|
||||
|
||||
|
|
@ -291,7 +302,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||
{
|
||||
ClientCertificateRequired = false,
|
||||
CheckCertificateRevocation = false,
|
||||
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11
|
||||
EnabledSslProtocols = SslProtocols.Tls12
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -403,11 +414,11 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||
ip = csIpAddress.ToString();
|
||||
}
|
||||
|
||||
var appUrl = $"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}";
|
||||
var appUrl = $"{HttpScheme}://{ip}:{Port}/mc/app?token={touchpanel.Key}";
|
||||
|
||||
this.LogVerbose("Sending URL {appUrl} to touchpanel {touchpanelKey}", appUrl, touchpanel.Touchpanel.Key);
|
||||
|
||||
touchpanel.Touchpanel.SetAppUrl($"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}");
|
||||
touchpanel.Touchpanel.SetAppUrl(appUrl);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -487,7 +498,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||
{
|
||||
var config = new MobileControlApplicationConfig
|
||||
{
|
||||
ApiPath = string.Format("http://{0}:{1}/mc/api", processorIp, _parent.Config.DirectServer.Port),
|
||||
ApiPath = string.Format("{0}://{1}:{2}/mc/api", HttpScheme, processorIp, Port),
|
||||
GatewayAppPath = "",
|
||||
LogoPath = _parent.Config.ApplicationConfig?.LogoPath ?? "logo/logo.png",
|
||||
EnableDev = _parent.Config.ApplicationConfig?.EnableDev ?? false,
|
||||
|
|
@ -1098,6 +1109,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||
res.StatusCode = 200;
|
||||
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")
|
||||
{
|
||||
Content = new StringContent(body, Encoding.UTF8, "application/json"),
|
||||
|
|
@ -1150,6 +1162,11 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||
var qp = req.QueryString;
|
||||
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);
|
||||
|
||||
byte[] body;
|
||||
|
|
@ -1213,8 +1230,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||
this.LogVerbose("Assigning ClientId: {clientId} for token: {token} at {timestamp}", clientId, token, now);
|
||||
|
||||
// Construct WebSocket URL with clientId query parameter
|
||||
var wsProtocol = "ws";
|
||||
var wsUrl = $"{wsProtocol}://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{Port}{_wsPath}{token}?clientId={clientId}";
|
||||
var wsUrl = $"{WsScheme}://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{Port}{_wsPath}{token}?clientId={clientId}";
|
||||
|
||||
// Construct the response object
|
||||
JoinResponse jRes = new JoinResponse
|
||||
|
|
@ -1226,7 +1242,8 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||
Config = _parent.GetConfigWithPluginVersion(),
|
||||
CodeExpires = new DateTime().AddYears(1),
|
||||
UserCode = bridge.UserCode,
|
||||
UserAppUrl = string.Format("http://{0}:{1}/mc/app",
|
||||
UserAppUrl = string.Format("{0}://{1}:{2}/mc/app",
|
||||
HttpScheme,
|
||||
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
|
||||
Port),
|
||||
WebSocketUrl = wsUrl,
|
||||
|
|
@ -1251,6 +1268,8 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||
{
|
||||
res.StatusCode = 200;
|
||||
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 message = JsonConvert.SerializeObject(version);
|
||||
this.LogVerbose("{message}", message);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Crestron.SimplSharp;
|
||||
|
|
@ -29,6 +27,8 @@ namespace PepperDash.Essentials
|
|||
private CEvent _initializeEvent;
|
||||
private const long StartupTime = 500;
|
||||
|
||||
// private const string minimumFirmwareVersion = "2.8006.00110";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the ControlSystem class
|
||||
/// </summary>
|
||||
|
|
@ -48,6 +48,24 @@ namespace PepperDash.Essentials
|
|||
/// <inheritdoc />
|
||||
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
|
||||
// to allow any HD-BaseT DM endpoints to register first.
|
||||
bool preventInitializationComplete = Global.ControlSystemIsDmpsType;
|
||||
|
|
@ -112,14 +130,8 @@ namespace PepperDash.Essentials
|
|||
(ConfigReader.ConfigObject, Newtonsoft.Json.Formatting.Indented).Replace(Environment.NewLine, "\r\n"));
|
||||
}, "showconfig", "Shows the current running merged config", ConsoleAccessLevelEnum.AccessOperator);
|
||||
|
||||
CrestronConsole.AddNewConsoleCommand(s =>
|
||||
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),
|
||||
CrestronConsole.AddNewConsoleCommand(
|
||||
PrintPortalInfo,
|
||||
"portalinfo",
|
||||
"Shows portal URLS from configuration",
|
||||
ConsoleAccessLevelEnum.AccessOperator);
|
||||
|
|
@ -142,6 +154,29 @@ 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>
|
||||
/// DeterminePlatform method
|
||||
/// </summary>
|
||||
|
|
@ -239,11 +274,6 @@ namespace PepperDash.Essentials
|
|||
PluginLoader.AddProgramAssemblies();
|
||||
|
||||
_ = new Core.DeviceFactory();
|
||||
// _ = new Devices.Common.DeviceFactory();
|
||||
// _ = new DeviceFactory();
|
||||
|
||||
// _ = new ProcessorExtensionDeviceFactory();
|
||||
// _ = new MobileControlFactory();
|
||||
|
||||
LoadAssets(Global.ApplicationDirectoryPathPrefix, Global.FilePathPrefix);
|
||||
|
||||
|
|
@ -256,10 +286,9 @@ namespace PepperDash.Essentials
|
|||
PluginLoader.LoadPlugins();
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information, "Folder structure verified. Loading config...");
|
||||
if (!ConfigReader.LoadConfig2())
|
||||
if (!ConfigReader.LoadConfig2() || ConfigReader.ConfigObject == null)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Essentials Load complete with errors");
|
||||
return;
|
||||
Debug.LogMessage(LogEventLevel.Warning, "Unable to load config file.");
|
||||
}
|
||||
|
||||
Load();
|
||||
|
|
@ -381,6 +410,12 @@ namespace PepperDash.Essentials
|
|||
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)
|
||||
{
|
||||
IKeyed newDev = null;
|
||||
|
|
@ -434,7 +469,7 @@ namespace PepperDash.Essentials
|
|||
|
||||
var tlc = TieLineCollection.Default;
|
||||
|
||||
if (ConfigReader.ConfigObject.TieLines == null)
|
||||
if (ConfigReader.ConfigObject?.TieLines == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -462,7 +497,7 @@ namespace PepperDash.Essentials
|
|||
{
|
||||
try
|
||||
{
|
||||
if (args.Contains("?"))
|
||||
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");
|
||||
|
|
@ -508,7 +543,7 @@ namespace PepperDash.Essentials
|
|||
{
|
||||
try
|
||||
{
|
||||
if (args.Contains("?"))
|
||||
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");
|
||||
|
|
@ -557,7 +592,7 @@ namespace PepperDash.Essentials
|
|||
{
|
||||
try
|
||||
{
|
||||
if (args.Contains("?"))
|
||||
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");
|
||||
|
|
@ -731,7 +766,7 @@ namespace PepperDash.Essentials
|
|||
/// </summary>
|
||||
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.");
|
||||
return;
|
||||
|
|
@ -766,15 +801,16 @@ namespace PepperDash.Essentials
|
|||
/// <summary>
|
||||
/// Fires up a logo server if not already running
|
||||
/// </summary>
|
||||
[Obsolete("Logo server is no longer supported and will be removed in a future release.")]
|
||||
void LoadLogoServer()
|
||||
{
|
||||
if (ConfigReader.ConfigObject.Rooms == null)
|
||||
if (ConfigReader.ConfigObject?.Rooms == null)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "No rooms configured. Bypassing Logo server startup.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
if (ConfigReader.ConfigObject?.Rooms == null ||
|
||||
!ConfigReader.ConfigObject.Rooms.Any(
|
||||
CheckRoomConfig))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.Program" Version="2.21.90" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.Program" Version="2.21.274" />
|
||||
<PackageReference Include="System.IO.Compression" Version="4.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue