diff --git a/docs/docs/usage/GenericComm.md b/docs/docs/usage/GenericComm.md index 16f467e2..243536e5 100644 --- a/docs/docs/usage/GenericComm.md +++ b/docs/docs/usage/GenericComm.md @@ -183,12 +183,11 @@ 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```, ```UdpClient```, and ```Udp```. +These enumerations are not case sensitive. Not all methods are valid for a ```genericComm``` device. For a comport, the only valid type would be ```Com```. For a direct network socket, valid options are ```Ssh```, ```Tcpip```, ```Telnet```, and ```Udp```. ##### ComParams @@ -288,7 +287,7 @@ This property maps to the number of the port on the device you have mapped the r ##### TcpSshParams -A ```Ssh```, ```TcpIp```, ```UdpClient```, or ```Udp``` device requires a ```tcpSshProperties``` object to set the propeties of the socket. +A ```Ssh```, ```TcpIp```, or ```Udp``` device requires a ```tcpSshProperties``` object to set the propeties of the socket. ```Json { @@ -305,7 +304,7 @@ A ```Ssh```, ```TcpIp```, ```UdpClient```, or ```Udp``` device requires a ```tcp **```address```** -This is the IP address, hostname, or FQDN of the resource you wish to open a socket to. Use ```UdpClient``` for outbound UDP to a remote endpoint. Use ```Udp``` when you need Essentials to bind a local UDP listener. +This is the IP address, hostname, or FQDN of the resource you wish to open a socket to. In the case of a UDP device, you can set either a single whitelist address with this data, or an appropriate broadcast address. **```port```** diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 235fee48..7435df6f 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,11 +1,11 @@ - 2.42.1-local + 2.29.0-local $(Version) PepperDash Technology PepperDash Technology PepperDash Essentials - Copyright © 2026 + Copyright © 2025 https://github.com/PepperDash/Essentials git Crestron; 4series @@ -20,7 +20,4 @@ - - - diff --git a/src/PepperDash.Core/Comm/GenericSshClient.cs b/src/PepperDash.Core/Comm/GenericSshClient.cs index 546a2a67..df44ab51 100644 --- a/src/PepperDash.Core/Comm/GenericSshClient.cs +++ b/src/PepperDash.Core/Comm/GenericSshClient.cs @@ -151,8 +151,6 @@ namespace PepperDash.Core // Thread-safety lock for state changes private readonly object _stateLock = new object(); - private volatile bool _isProgramStopping; - private bool disconnectLogged = false; /// @@ -209,9 +207,11 @@ namespace PepperDash.Core { if (programEventType == eProgramStatusEventType.Stopping) { - _isProgramStopping = true; - this.LogDebug("Program stopping. Closing connection"); - Disconnect(); + if (client != null) + { + this.LogDebug("Program stopping. Closing connection"); + Disconnect(); + } } } @@ -228,12 +228,6 @@ namespace PepperDash.Core return; } - if (_isProgramStopping) - { - this.LogDebug("Skipping connect because program is stopping"); - return; - } - ConnectEnabled = true; try @@ -293,7 +287,13 @@ namespace PepperDash.Core } catch (SshConnectionException e) { - var ie = e.InnerException; // The details are inside, when present - remote can close the connection with no inner exception at all + var ie = e.InnerException; // The details are inside!! + + if (ie is SocketException) + { + this.LogError("CONNECTION failure: Cannot reach host"); + this.LogVerbose(ie, "Exception details: "); + } if (ie is System.Net.Sockets.SocketException socketException) { @@ -301,20 +301,20 @@ namespace PepperDash.Core Hostname, Port); this.LogVerbose(socketException, "SocketException details: "); } - else if (ie is SshAuthenticationException) + 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 ?? e.Message); - this.LogVerbose(ie ?? e, "Exception details: "); + this.LogError("Error on connect: {error}", ie.Message); + this.LogVerbose(ie, "Exception details: "); } disconnectLogged = true; KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); - if (AutoReconnect && ConnectEnabled && !_isProgramStopping) + if (AutoReconnect) { 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 && ConnectEnabled && !_isProgramStopping) + if (AutoReconnect) { 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 && ConnectEnabled && !_isProgramStopping) + if (AutoReconnect) { this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); StartReconnectTimer(); @@ -473,7 +473,7 @@ namespace PepperDash.Core { connectLock.Release(); } - if (AutoReconnect && ConnectEnabled && !_isProgramStopping) + if (AutoReconnect && ConnectEnabled) { this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); StartReconnectTimer(); @@ -516,10 +516,7 @@ namespace PepperDash.Core this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim()); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); - if (AutoReconnect && ConnectEnabled && !_isProgramStopping) - { - StartReconnectTimer(); - } + StartReconnectTimer(); } catch (Exception ex) { @@ -552,10 +549,7 @@ namespace PepperDash.Core this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes)); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); - if (AutoReconnect && ConnectEnabled && !_isProgramStopping) - { - StartReconnectTimer(); - } + StartReconnectTimer(); } catch (Exception ex) { diff --git a/src/PepperDash.Core/Comm/GenericUdpClient.cs b/src/PepperDash.Core/Comm/GenericUdpClient.cs deleted file mode 100644 index 41792537..00000000 --- a/src/PepperDash.Core/Comm/GenericUdpClient.cs +++ /dev/null @@ -1,463 +0,0 @@ -using System; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Crestron.SimplSharp; -using Crestron.SimplSharp.CrestronSockets; -using ThreadingTimeout = System.Threading.Timeout; -using NetSocketException = System.Net.Sockets.SocketException; - -namespace PepperDash.Core -{ - /// - /// A class to handle basic UDP communications to a remote endpoint - /// - 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; - - /// - /// Object to enable stream debugging - /// - public CommunicationStreamDebugging StreamDebugging { get; private set; } - - /// - /// Fires when data is received from the remote endpoint and returns it as a byte array - /// - public event EventHandler BytesReceived; - - /// - /// Fires when data is received from the remote endpoint and returns it as text - /// - public event EventHandler TextReceived; - - /// - /// Fires when the socket status changes - /// - public event EventHandler ConnectionChange; - - /// - /// Address of remote endpoint - /// - public string Hostname { get; set; } - - /// - /// Port on remote endpoint - /// - public int Port { get; set; } - - /// - /// Another S+ helper because large port numbers can be treated as signed ints - /// - public ushort UPort - { - get { return Convert.ToUInt16(Port); } - set { Port = Convert.ToInt32(value); } - } - - /// - /// Defaults to 2000 - /// - public int BufferSize { get; set; } - - /// - /// True when the local socket is created and associated with the configured remote endpoint - /// - public bool IsConnected - { - get { return ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } - } - - /// - /// S+ helper for IsConnected - /// - public ushort UIsConnected - { - get { return (ushort)(IsConnected ? 1 : 0); } - } - - /// - /// The current socket status of the client - /// - 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)); - } - } - - /// - /// Ushort representation of client status - /// - public ushort UStatus - { - get { return (ushort)ClientStatus; } - } - - /// - /// Gets or sets the AutoReconnect - /// - public bool AutoReconnect { get; set; } - - /// - /// S+ helper for AutoReconnect - /// - public ushort UAutoReconnect - { - get { return (ushort)(AutoReconnect ? 1 : 0); } - set { AutoReconnect = value == 1; } - } - - /// - /// Milliseconds to wait before attempting to reconnect. Defaults to 5000 - /// - public int AutoReconnectIntervalMs { get; set; } - - /// - /// Constructor - /// - 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); - } - - /// - /// Constructor for S+ - /// - 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); - } - - /// - /// Initialize method - /// - 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(); - } - } - - /// - /// Deactivate method - /// - public override bool Deactivate() - { - Disconnect(); - return true; - } - - /// - /// Connect method - /// - 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(); - } - } - } - } - - /// - /// Disconnect method - /// - public void Disconnect() - { - lock (stateLock) - { - connectEnabled = false; - reconnectTimer.Change(ThreadingTimeout.Infinite, ThreadingTimeout.Infinite); - CleanupClient(); - ClientStatus = SocketStatus.SOCKET_STATUS_NO_CONNECT; - } - } - - /// - /// SendText method - /// - public void SendText(string text) - { - this.PrintSentText(text); - - var bytes = Encoding.GetEncoding(28591).GetBytes(text); - SendBytes(bytes); - } - - /// - /// SendBytes method - /// - 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; - } - } - } -} diff --git a/src/PepperDash.Core/Comm/eControlMethods.cs b/src/PepperDash.Core/Comm/eControlMethods.cs index a1a30734..b807fdc5 100644 --- a/src/PepperDash.Core/Comm/eControlMethods.cs +++ b/src/PepperDash.Core/Comm/eControlMethods.cs @@ -18,75 +18,70 @@ namespace PepperDash.Core /// /// RS232/422/485 /// - Com = 1, + Com, /// /// Crestron IpId (most Crestron ethernet devices) /// - IpId = 2, + IpId, /// /// Crestron IpIdTcp (HD-MD series, etc.) /// - IpidTcp = 3, + IpidTcp, /// /// Crestron IR control /// - IR = 4, + IR, /// /// SSH client /// - Ssh = 5, + Ssh, /// /// TCP/IP client /// - Tcpip = 6, + Tcpip, /// /// Telnet /// - Telnet = 7, + Telnet, /// /// Crestnet device /// - Cresnet = 8, + Cresnet, /// /// CEC Control, via a DM HDMI port /// - Cec = 9, + Cec, /// /// UDP Server /// - Udp = 10, - + Udp, /// /// HTTP client /// - Http = 11, + Http, /// /// HTTPS client /// - Https = 12, + Https, /// /// Websocket client /// - Ws = 13, + Ws, /// /// Secure Websocket client /// - Wss = 14, + Wss, /// /// Secure TCP/IP /// - SecureTcpIp = 15, + SecureTcpIp, /// /// Used when comms needs to be handled in SIMPL and bridged opposite the normal direction /// - ComBridge = 16, + ComBridge, /// /// InfinetEX control /// - InfinetEx = 17, - /// - /// UDP client - /// - UdpClient = 18, + InfinetEx } } \ No newline at end of file diff --git a/src/PepperDash.Core/Logging/DebugContext.cs b/src/PepperDash.Core/Logging/DebugContext.cs index 66e63634..e90ec86a 100644 --- a/src/PepperDash.Core/Logging/DebugContext.cs +++ b/src/PepperDash.Core/Logging/DebugContext.cs @@ -11,7 +11,6 @@ namespace PepperDash.Core /// /// Represents a debugging context /// - [Obsolete("DebugContext is no longer supported and will be removed in a future release.")] public class DebugContext { /// diff --git a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs index cfbf5785..fc3b0247 100644 --- a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs +++ b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs @@ -8,17 +8,10 @@ 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; @@ -72,20 +65,6 @@ namespace PepperDash.Core /// public bool IsRunning { get => _httpsServer?.IsListening ?? false; } - /// - /// Gets a value indicating whether there are active WebSocket connections. - /// - 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; @@ -135,70 +114,51 @@ namespace PepperDash.Core var subjectName = string.Format("CN={0}.{1}", hostName, domainName); var fqdn = string.Format("{0}.{1}", hostName, domainName); - 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)) + using (var rsa = RSA.Create(2048)) { - 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); + var request = new CertificateRequest( + subjectName, + rsa, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); - // 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 }); + // Subject Key Identifier + request.CertificateExtensions.Add( + new X509SubjectKeyIdentifierExtension(request.PublicKey, false)); - var separator = Path.DirectorySeparatorChar; - var outputPath = string.Format("{0}user{1}{2}.pfx", separator, separator, _certificateName); + // 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)); - using (var ms = new MemoryStream()) - { - var passwordChars = _certificatePassword.ToCharArray(); - try + // 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)) { - 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)); + var separator = Path.DirectorySeparatorChar; + var outputPath = string.Format("{0}user{1}{2}.pfx", separator, separator, _certificateName); + + var pfxBytes = cert.Export(X509ContentType.Pfx, _certificatePassword); + File.WriteAllBytes(outputPath, pfxBytes); + + CrestronConsole.PrintLine(string.Format("CreateCert: Certificate written to {0}", outputPath)); + } + } } catch (Exception ex) { @@ -231,66 +191,26 @@ 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 { - return LoadCertFromBouncyCastle(certPath, certPassword); + // EphemeralKeySet is required on Linux/OpenSSL (Crestron 4-series) to avoid + // key-container persistence failures, and avoids the private key export restriction. + return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet); } catch (Exception ex) { - // Cert is corrupt or was written by an incompatible tool — delete and regenerate once. + // Cert is stale or was generated by an incompatible library (e.g. old BouncyCastle output). + // Delete it, regenerate with the BCL path, and retry once. CrestronConsole.PrintLine(string.Format("SSL cert load failed ({0}); regenerating...", ex.Message)); try { File.Delete(certPath); } catch { } CreateCert(); - return LoadCertFromBouncyCastle(certPath, certPassword); - } - } - - /// - /// Loads a PKCS#12 file written by BouncyCastle and returns an with - /// private key attached. - /// The PFX is parsed and re-encoded by BouncyCastle (ensuring format compatibility), then passed as - /// raw bytes to so neither RSACryptoServiceProvider nor the - /// EphemeralKeySet flag (unsupported on the Crestron/Mono runtime) is needed. - /// - 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); + return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet); } } diff --git a/src/PepperDash.Core/PepperDash.Core.csproj b/src/PepperDash.Core/PepperDash.Core.csproj index 747aefb6..daa5c6da 100644 --- a/src/PepperDash.Core/PepperDash.Core.csproj +++ b/src/PepperDash.Core/PepperDash.Core.csproj @@ -43,7 +43,7 @@ - + diff --git a/src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs b/src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs index 131430a2..f8c815fd 100644 --- a/src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs +++ b/src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs @@ -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,25 +10,24 @@ using PepperDash.Core.JsonToSimpl; namespace PepperDash.Core.WebApi.Presets { - /// - /// Passcode client for the WebApi - /// - [Obsolete("WebApiPasscodeClient is no longer supported and will be removed in a future release.")] + /// + /// Passcode client for the WebApi + /// public class WebApiPasscodeClient : IKeyed { - /// - /// Notifies when user received - /// + /// + /// Notifies when user received + /// public event EventHandler UserReceived; - /// - /// Notifies when Preset received - /// + /// + /// Notifies when Preset received + /// public event EventHandler PresetReceived; - /// - /// Gets or sets the Key - /// + /// + /// Gets or sets the Key + /// public string Key { get; private set; } //string JsonMasterKey; @@ -55,13 +54,13 @@ namespace PepperDash.Core.WebApi.Presets { } - /// - /// Initializes the instance - /// - /// - /// - /// - /// + /// + /// Initializes the instance + /// + /// + /// + /// + /// public void Initialize(string key, string jsonMasterKey, string urlBase, string defaultPresetJsonFilePath) { Key = key; @@ -74,44 +73,44 @@ namespace PepperDash.Core.WebApi.Presets J2SMaster.Initialize(jsonMasterKey); } - /// - /// Gets the user for a passcode - /// - /// - /// - /// GetUserForPasscode method - /// + /// + /// Gets the user for a passcode + /// + /// + /// + /// GetUserForPasscode method + /// 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(resp.ContentString); - CurrentUser = user; - if (handler != null) - handler(this, new UserReceivedEventArgs(user, true)); - } - else - if (handler != null) - handler(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(resp.ContentString); + CurrentUser = user; + if (handler != null) + UserReceived(this, new UserReceivedEventArgs(user, true)); + } + else + if (handler != null) + UserReceived(this, new UserReceivedEventArgs(null, false)); } /// @@ -119,9 +118,9 @@ namespace PepperDash.Core.WebApi.Presets /// /// /// - /// - /// GetPresetForThisUser method - /// + /// + /// GetPresetForThisUser method + /// public void GetPresetForThisUser(int roomTypeId, int presetNumber) { if (CurrentUser == null) @@ -137,57 +136,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(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(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) - handler(this, new PresetReceivedEventArgs(preset, true)); - } - else // no existing preset - { - CurrentPreset = new Preset(); - LoadDefaultPresetData(); - if (handler != null) - handler(this, new PresetReceivedEventArgs(null, false)); - } + 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)); + } } 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)); } } @@ -219,14 +218,14 @@ namespace PepperDash.Core.WebApi.Presets /// /// /// - /// - /// SavePresetForThisUser method - /// + /// + /// SavePresetForThisUser method + /// 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) @@ -246,8 +245,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)); @@ -256,8 +255,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); diff --git a/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs b/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs index a5aa0d13..298bbc9b 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs @@ -39,8 +39,6 @@ namespace PepperDash.Essentials.Core ICec Port; - bool _cecSubscribed; - /// /// Constructor /// @@ -56,7 +54,7 @@ namespace PepperDash.Essentials.Core { Port = postActivationFunc(config); - TryEnsureCecSubscription(); + Port.StreamCec.CecChange += StreamCec_CecChange; }); } @@ -70,29 +68,7 @@ namespace PepperDash.Essentials.Core { Port = port; - TryEnsureCecSubscription(); - } - - /// - /// Subscribes to the CEC change event once 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. - /// - 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) @@ -128,9 +104,8 @@ namespace PepperDash.Essentials.Core /// public void SendText(string text) { - if (Port?.StreamCec == null) + if (Port == null) return; - TryEnsureCecSubscription(); this.PrintSentText(text); Port.StreamCec.Send.StringValue = text; } @@ -140,9 +115,8 @@ namespace PepperDash.Essentials.Core /// public void SendBytes(byte[] bytes) { - if (Port?.StreamCec == null) + if (Port == 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)); diff --git a/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs b/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs index b28331a0..9318e04b 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs @@ -96,17 +96,6 @@ 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: diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs index 37cd4eba..40c6c0cf 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs @@ -135,14 +135,12 @@ namespace PepperDash.Essentials.Core.Config { var parsedConfig = JObject.Parse(fs.ReadToEnd()); - // 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) + // 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) { Debug.LogMessage(LogEventLevel.Information, "Config file is a v2 format, no merge necessary."); ConfigObject = parsedConfig.ToObject(); @@ -150,8 +148,6 @@ 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(); @@ -164,13 +160,6 @@ namespace PepperDash.Essentials.Core.Config { ConfigObject.TemplateUrl = parsedConfig["template_url"].Value(); } - - // 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(); - } } Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded Merged Config"); diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigUpdater.cs b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigUpdater.cs index 2ba33df0..9df673bf 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigUpdater.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigUpdater.cs @@ -19,7 +19,6 @@ namespace PepperDash.Essentials.Core.Config /// /// ConfigUpdater class /// - [Obsolete("ConfigUpdater is no longer supported and will be removed in a future release.")] public static class ConfigUpdater { /// @@ -82,7 +81,7 @@ namespace PepperDash.Essentials.Core.Config { var handler = ConfigStatusChanged; - if (handler != null) + if(handler != null) { handler(typeof(ConfigUpdater), new ConfigStatusEventArgs(status)); } @@ -90,7 +89,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 { @@ -105,7 +104,7 @@ namespace PepperDash.Essentials.Core.Config Debug.LogMessage(LogEventLevel.Debug, "Error parsing new config: {0}", e); OnStatusUpdate(eUpdateStatus.UpdateFailed); - } + } } /// @@ -150,11 +149,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); @@ -171,9 +170,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); @@ -208,15 +207,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); } } - /// - /// Enumeration of eUpdateStatus values - /// - public enum eUpdateStatus + /// + /// Enumeration of eUpdateStatus values + /// + public enum eUpdateStatus { /// /// UpdateStarted status diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs index 44275ef0..6ffe07d2 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs @@ -105,7 +105,6 @@ namespace PepperDash.Essentials.Core.Config /// /// Gets or sets the Versions /// - [JsonProperty("versions")] public VersionData Versions { get; set; } /// @@ -135,25 +134,12 @@ namespace PepperDash.Essentials.Core.Config [JsonProperty("packages")] public List Packages { get; set; } - /// - /// Gets or sets the touchpanel wrapper app version - /// - [JsonProperty("touchpanelWrapperApp")] - public NugetVersion TouchpanelWrapperApp { get; set; } - - /// - /// Gets or sets the list of user interface packages - /// - [JsonProperty("userInterfaces")] - public List UserInterfaces { get; set; } - /// /// Initializes a new instance of the class. /// public VersionData() { Packages = new List(); - UserInterfaces = new List(); } } @@ -171,20 +157,8 @@ namespace PepperDash.Essentials.Core.Config /// /// Gets or sets the PackageId /// - [JsonProperty("packageId", NullValueHandling = NullValueHandling.Ignore)] + [JsonProperty("packageId")] public string PackageId { get; set; } - - /// - /// Gets or sets the RepoUrl - /// - [JsonProperty("repoUrl", NullValueHandling = NullValueHandling.Ignore)] - public string RepoUrl { get; set; } - - /// - /// Gets or sets the human-readable name - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; set; } } /// diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasWirelessSharing.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasWirelessSharing.cs deleted file mode 100644 index 6cd3835e..00000000 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasWirelessSharing.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; - -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces -{ - /// - /// 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. - /// - /// - /// This refers specifically to wireless screen/device mirroring, as distinct from in-call - /// content sharing on a video conference. - /// - public interface IHasWirelessSharing - { - /// - /// Reports whether a wireless sharing session is currently active (content is being presented). - /// - BoolFeedback IsSharingFeedback { get; } - - /// - /// Raised when wireless sharing starts or stops. The event args carry the new sharing state. - /// - event EventHandler SharingChanged; - } - - /// - /// Event arguments describing a change in wireless sharing state. - /// - public class WirelessSharingEventArgs : EventArgs - { - /// - /// True if a wireless sharing session is active (content is being presented), false otherwise. - /// - public bool IsSharing { get; private set; } - - /// - /// Creates a new . - /// - /// True if a wireless sharing session is active, false otherwise. - public WirelessSharingEventArgs(bool isSharing) - { - IsSharing = isSharing; - } - } -} diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessengerWithSubscriptions.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessengerWithSubscriptions.cs index 8603d5b6..887f1789 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessengerWithSubscriptions.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessengerWithSubscriptions.cs @@ -1,12 +1,10 @@ -using System; using PepperDash.Core; namespace PepperDash.Essentials.Core.DeviceTypeInterfaces { /// - /// Obsolete: messengers are subscription based by default; use IMobileControlMessenger instead. + /// Defines the contract for IMobileControlMessenger /// - [Obsolete("This interface is obsolete and will be removed in a future version. All messengers are now subscription based.")] public interface IMobileControlMessengerWithSubscriptions : IMobileControlMessenger { /// diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs deleted file mode 100644 index 6c6e5598..00000000 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System; - -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces -{ - /// - /// Interface for network switches that support VLAN assignment on individual ports. - /// - public interface INetworkSwitchVlanManager - { - /// - /// 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). - /// - /// Switch port identifier - /// VLAN ID or -1 when unavailable - int GetPortCurrentVlan(string port); - - /// - /// Changes the access VLAN of a single switch port. - /// The implementation is responsible for entering/exiting privileged/config mode. - /// - /// Switch port identifier (e.g. "1/0/3" for Netgear, "gi1/0/3" for Cisco) - /// Target VLAN ID (1-4093) - void SetPortVlan(string port, uint vlanId); - } - - /// - /// Interface for network switches that support Power over Ethernet (PoE) control on individual ports. - /// - public interface INetworkSwitchPoeManager - { - /// - /// Enables or disables PoE power delivery on a single switch port. - /// The implementation is responsible for entering/exiting privileged/config mode. - /// - /// Switch port identifier - /// True to enable PoE; false to disable PoE - void SetPortPoeState(string port, bool enabled); - } - - /// - /// Standardized interface for network switch devices that support per-port PoE control - /// and VLAN assignment. - /// - public interface INetworkSwitchPoeVlanManager : INetworkSwitchVlanManager, INetworkSwitchPoeManager - { - /// - /// Event that is raised when the state of a switch port changes, such as a VLAN change or PoE state change. - /// - event EventHandler PortStateChanged; - - } - - /// - /// Event arguments for port state changes on a network switch, such as VLAN changes or PoE state changes. - /// - public class NetworkSwitchPortEventArgs : EventArgs - { - /// - /// The identifier of the port that changed state (e.g. "1/0/3" for Netgear, "gi1/0/3" for Cisco). - /// - public string Port { get; private set; } - - /// - /// The type of event that occurred on the port (e.g. VLAN change, PoE enabled/disabled). - /// - public NetworkSwitchPortEventType EventType { get; private set; } - - /// - /// Constructor for NetworkSwitchPortEventArgs - /// - /// The identifier of the port that changed state - /// The type of event that occurred on the port - public NetworkSwitchPortEventArgs(string port, NetworkSwitchPortEventType eventType) - { - Port = port; - EventType = eventType; - } - } - - /// - /// Enumeration of network switch port state change event types (e.g. VLAN changes or PoE state changes). - /// - public enum NetworkSwitchPortEventType - { - /// - /// Indicates that the type of event is unknown or cannot be determined. - /// - Unknown, - - /// - /// Indicates that a VLAN change is in progress on the port, either through a call to SetPortVlan or an external change detected by polling. - /// - VlanChangeInProgress, - - /// - /// Indicates that the access VLAN on a port has changed, either through a successful call to SetPortVlan or an external change detected by polling. - /// - VlanChanged, - - /// - /// Indicates that PoE is being disabled on the port, either through a call to SetPortPoeState or an external change detected by polling. - /// - PoeDisableInProgress, - - /// - /// Indicates that PoE has been disabled on the port, either through a successful call to SetPortPoeState or an external change detected by polling. - /// - PoEDisabled, - - /// - /// Indicates that PoE is being enabled on the port, either through a call to SetPortPoeState or an external change detected by polling. - /// - PoeEnableInProgress, - - /// - /// Indicates that PoE has been enabled on the port, either through a successful call to SetPortPoeState or an external change detected by polling. - /// - PoEEnabled - } -} diff --git a/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs b/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs index c01d5e81..c4ec2ca9 100644 --- a/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs +++ b/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs @@ -7,7 +7,6 @@ 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 @@ -177,20 +176,7 @@ namespace PepperDash.Essentials.Core { if (!conversionType.IsEnum) { - 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; + return Convert.ChangeType(value, conversionType, System.Globalization.CultureInfo.InvariantCulture); } var stringValue = Convert.ToString(value); @@ -203,32 +189,6 @@ namespace PepperDash.Essentials.Core return Enum.Parse(conversionType, stringValue, true); } - /// - /// Processes escape sequences in a string, converting sequences like \r, \n, \t, \xHH - /// to their corresponding non-printable ASCII characters. - /// - 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(); - } - }); - } - /// /// Gets the properties on a device /// diff --git a/src/PepperDash.Essentials.Core/Devices/IHasDspPresetSave.cs b/src/PepperDash.Essentials.Core/Devices/IHasDspPresetSave.cs deleted file mode 100644 index c628129e..00000000 --- a/src/PepperDash.Essentials.Core/Devices/IHasDspPresetSave.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace PepperDash.Essentials.Core -{ - /// - /// Defines the contract for IHasDspPresetSave - /// - public interface IHasDspPresetSave : IDspPresets - { - /// - /// Saves the preset by key - /// - /// key of preset to save - void SavePreset(string key); - } -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Interfaces/ILogStrings.cs b/src/PepperDash.Essentials.Core/Interfaces/ILogStrings.cs index b41a9020..d55194fd 100644 --- a/src/PepperDash.Essentials.Core/Interfaces/ILogStrings.cs +++ b/src/PepperDash.Essentials.Core/Interfaces/ILogStrings.cs @@ -7,10 +7,9 @@ using PepperDash.Core; namespace PepperDash.Essentials.Core.Interfaces { - /// - /// Defines the contract for ILogStrings - /// - [Obsolete("ILogStrings is no longer supported and will be removed in a future release.")] + /// + /// Defines the contract for ILogStrings + /// public interface ILogStrings : IKeyed { /// diff --git a/src/PepperDash.Essentials.Core/Interfaces/ILogStringsWithLevel.cs b/src/PepperDash.Essentials.Core/Interfaces/ILogStringsWithLevel.cs index 32ef104a..bb835919 100644 --- a/src/PepperDash.Essentials.Core/Interfaces/ILogStringsWithLevel.cs +++ b/src/PepperDash.Essentials.Core/Interfaces/ILogStringsWithLevel.cs @@ -7,16 +7,15 @@ using PepperDash.Core; namespace PepperDash.Essentials.Core.Interfaces { - /// - /// Defines the contract for ILogStringsWithLevel - /// - [Obsolete("ILogStringsWithLevel is no longer supported and will be removed in a future release.")] + /// + /// Defines the contract for ILogStringsWithLevel + /// public interface ILogStringsWithLevel : IKeyed { /// /// Defines a class that is capable of logging a string with an int level /// - void SendToLog(IKeyed device, Debug.ErrorLogLevel level, string logMessage); + void SendToLog(IKeyed device, Debug.ErrorLogLevel level,string logMessage); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj index b7f333bd..251ba316 100644 --- a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj +++ b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj @@ -25,7 +25,7 @@ bin\$(Configuration)\PepperDash_Essentials_Core.xml - + diff --git a/src/PepperDash.Essentials.Core/Routing/Extensions.cs b/src/PepperDash.Essentials.Core/Routing/Extensions.cs index 303a0194..631ebd99 100644 --- a/src/PepperDash.Essentials.Core/Routing/Extensions.cs +++ b/src/PepperDash.Essentials.Core/Routing/Extensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -51,6 +52,13 @@ namespace PepperDash.Essentials.Core /// private static Dictionary> _tieLinesBySource; + /// + /// Cache of failed route attempts to avoid re-checking impossible paths. + /// Format: "sourceKey|destKey|signalType" + /// Uses ConcurrentDictionary as a thread-safe set (byte value is unused). + /// + private static readonly ConcurrentDictionary _impossibleRoutes = new ConcurrentDictionary(); + /// /// Indexes all TieLines by source and destination device keys for faster lookups. /// Should be called once at system startup after all TieLines are created. @@ -113,6 +121,29 @@ namespace PepperDash.Essentials.Core return TieLineCollection.Default.Where(t => t.SourcePort.ParentDevice.Key == sourceKey); } + /// + /// Creates a cache key for route impossibility tracking. + /// + /// Source device key + /// Destination device key + /// Source port key + /// Destination port key + /// Signal type + /// Cache key string + private static string GetRouteKey(string sourceKey, string destKey, string sourcePortKey, string destinationPortKey, eRoutingSignalType type) + { + return $"{sourceKey}|{destKey}|{sourcePortKey}|{destinationPortKey}|{type}"; + } + + /// + /// Clears the impossible routes cache. Should be called if TieLines are added/removed at runtime. + /// + public static void ClearImpossibleRoutesCache() + { + _impossibleRoutes.Clear(); + Debug.LogInformation("Impossible routes cache cleared"); + } + /// /// Gets any existing RouteDescriptor for a destination, clears it using ReleaseRoute /// and then attempts a new Route and if sucessful, stores that RouteDescriptor @@ -218,7 +249,7 @@ namespace PepperDash.Essentials.Core } // otherwise, audioVideo needs to be handled as two steps. - Debug.LogDebug(destination, "Attempting to build source route from {destinationKey} to {sourceKey} of type {type}", destination.Key, source.Key, signalType); + Debug.LogDebug(destination, "Attempting to build source route from {destinationKey} to {sourceKey} of type {type}", source.Key, signalType); RouteDescriptor audioRouteDescriptor; @@ -343,10 +374,8 @@ namespace PepperDash.Essentials.Core IndexTieLines(); } - var sinks = DeviceManager.AllDevices.OfType() - .Where(d => !(d is IRoutingInputsOutputs)).ToList(); - var sources = DeviceManager.AllDevices.OfType() - .Where(d => !(d is IRoutingInputsOutputs)).ToList(); + var sinks = DeviceManager.AllDevices.OfType().Where(d => !(d is IRoutingInputsOutputs)); + var sources = DeviceManager.AllDevices.OfType().Where(d => !(d is IRoutingInputsOutputs)); foreach (var sink in sinks) { @@ -356,17 +385,13 @@ namespace PepperDash.Essentials.Core { foreach (var outputPort in source.OutputPorts) { - var (audioOrSingleRoute, videoRoute) = sink.GetRouteToSource(source, outputPort.Type, inputPort, outputPort); + var (audioOrSingleRoute, videoRoute) = sink.GetRouteToSource(source, inputPort.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 @@ -375,10 +400,6 @@ 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)) @@ -410,10 +431,6 @@ 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); } } @@ -567,6 +584,14 @@ namespace PepperDash.Essentials.Core { cycle++; + // Check if this route has already been determined to be impossible + var routeKey = GetRouteKey(source.Key, destination.Key, sourcePort?.Key ?? "auto", destinationPort?.Key ?? "auto", signalType); + if (_impossibleRoutes.ContainsKey(routeKey)) + { + Debug.LogVerbose("Route {0} is cached as impossible, skipping", routeKey); + return false; + } + Debug.LogVerbose("GetRouteToSource: {cycle} {sourceKey}:{sourcePortKey}--> {destinationKey}:{destinationPortKey} {type}", null, cycle, source.Key, sourcePort?.Key ?? "auto", destination.Key, destinationPort?.Key ?? "auto", signalType.ToString()); RoutingInputPort goodInputPort = null; @@ -619,12 +644,8 @@ namespace PepperDash.Essentials.Core // 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) - .ToList(); + var midpointTieLines = destinationTieLines.Where(t => t.SourcePort.ParentDevice is IRoutingInputsOutputs); - 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(); @@ -664,7 +685,10 @@ namespace PepperDash.Essentials.Core if (goodInputPort == null) { - Debug.LogVerbose(destination, "No route found to {0} from destination {1} for type {2}", source.Key, destination.Key, signalType); + Debug.LogVerbose(destination, "No route found to {0}", source.Key); + + // Cache this as an impossible route + _impossibleRoutes.TryAdd(routeKey, 0); return false; } diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs index 7fe41c5c..16822ff2 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs @@ -66,9 +66,6 @@ namespace PepperDash.Essentials.Core.Routing foreach (var midpointKey in upstreamMidpoints) { - if (string.IsNullOrEmpty(midpointKey)) - continue; - if (!midpointToSinksMap.ContainsKey(midpointKey)) midpointToSinksMap[midpointKey] = new HashSet(); @@ -118,8 +115,7 @@ namespace PepperDash.Essentials.Core.Routing if (tieLine.SourcePort.ParentDevice is IRoutingWithFeedback midpoint) { - if (!string.IsNullOrEmpty(midpoint.Key)) - midpoints.Add(midpoint.Key); + midpoints.Add(midpoint.Key); // Find upstream TieLines connected to this midpoint's inputs var midpointInputs = (midpoint as IRoutingInputs)?.InputPorts; @@ -248,9 +244,6 @@ namespace PepperDash.Essentials.Core.Routing var upstreamMidpoints = GetUpstreamMidpoints(sink); foreach (var midpointKey in upstreamMidpoints) { - if (string.IsNullOrEmpty(midpointKey)) - continue; - if (!midpointToSinksMap.ContainsKey(midpointKey)) midpointToSinksMap[midpointKey] = new HashSet(); diff --git a/src/PepperDash.Essentials.Core/Routing/eRoutingSignalType.cs b/src/PepperDash.Essentials.Core/Routing/eRoutingSignalType.cs index 708b7448..9fce9c5b 100644 --- a/src/PepperDash.Essentials.Core/Routing/eRoutingSignalType.cs +++ b/src/PepperDash.Essentials.Core/Routing/eRoutingSignalType.cs @@ -27,19 +27,16 @@ namespace PepperDash.Essentials.Core /// /// Control signal type /// - [Obsolete("UsbOutput is no longer supported and will be removed in a future release.")] UsbOutput = 8, /// /// Control signal type /// - [Obsolete("UsbInput is no longer supported and will be removed in a future release.")] UsbInput = 16, /// /// Secondary audio signal type /// - [Obsolete("SecondaryAudio is no longer supported and will be removed in a future release.")] SecondaryAudio = 32 } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Secrets/SecretsManager.cs b/src/PepperDash.Essentials.Core/Secrets/SecretsManager.cs index ac4e80d8..382f5d55 100644 --- a/src/PepperDash.Essentials.Core/Secrets/SecretsManager.cs +++ b/src/PepperDash.Essentials.Core/Secrets/SecretsManager.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using Crestron.SimplSharp; using PepperDash.Core; -using PepperDash.Core.Logging; using Serilog.Events; namespace PepperDash.Essentials.Core @@ -297,14 +296,19 @@ namespace PepperDash.Essentials.Core { var secretPresent = provider.TestSecret(key); - provider.LogVerbose("SecretsProvider {0} {1} contain a secret entry for {2}", provider.Key, secretPresent ? "does" : "does not", key); + Debug.LogMessage(LogEventLevel.Verbose, provider, "SecretsProvider {0} {1} contain a secret entry for {2}", provider.Key, secretPresent ? "does" : "does not", key); if (!secretPresent) return - $"Unable to update secret for {provider.Key}:{key} - Please use the 'SetSecret' command to modify it"; + String.Format( + "Unable to update secret for {0}:{1} - Please use the 'SetSecret' command to modify it"); var response = provider.SetSecret(key, secret) - ? $"Secret successfully set for {provider.Key}:{key}" - : $"Unable to set secret for {provider.Key}:{key}"; + ? String.Format( + "Secret successfully set for {0}:{1}", + provider.Key, key) + : String.Format( + "Unable to set secret for {0}:{1}", + provider.Key, key); return response; } @@ -312,14 +316,19 @@ namespace PepperDash.Essentials.Core { var secretPresent = provider.TestSecret(key); - provider.LogVerbose("SecretsProvider {0} {1} contain a secret entry for {2}", provider.Key, secretPresent ? "does" : "does not", key); + Debug.LogMessage(LogEventLevel.Verbose, provider, "SecretsProvider {0} {1} contain a secret entry for {2}", provider.Key, secretPresent ? "does" : "does not", key); if (secretPresent) return - $"Unable to set secret for {provider.Key}:{key} - Please use the 'UpdateSecret' command to modify it"; + String.Format( + "Unable to set secret for {0}:{1} - Please use the 'UpdateSecret' command to modify it"); var response = provider.SetSecret(key, secret) - ? $"Secret successfully set for {provider.Key}:{key}" - : $"Unable to set secret for {provider.Key}:{key}"; + ? String.Format( + "Secret successfully set for {0}:{1}", + provider.Key, key) + : String.Format( + "Unable to set secret for {0}:{1}", + provider.Key, key); return response; } @@ -368,10 +377,15 @@ namespace PepperDash.Essentials.Core var key = args[1]; + provider.SetSecret(key, ""); response = provider.SetSecret(key, "") - ? $"Secret successfully deleted for {provider.Key}:{key}" - : $"Unable to delete secret for {provider.Key}:{key}"; + ? String.Format( + "Secret successfully deleted for {0}:{1}", + provider.Key, key) + : String.Format( + "Unable to delete secret for {0}:{1}", + provider.Key, key); CrestronConsole.ConsoleCommandResponse(response); return; diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs index cfbaa1df..9e781883 100644 --- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs +++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs @@ -20,8 +20,6 @@ namespace PepperDash.Essentials.Core.Web private readonly WebApiServer _debugServer; - - /// /// http(s)://{ipaddress}/cws/{basePath} /// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath} @@ -95,11 +93,6 @@ namespace PepperDash.Essentials.Core.Web Name = "ReportVersions", RouteHandler = new ReportVersionsRequestHandler() }, - new HttpCwsRoute("packageManifest") - { - Name = "GetPackageManifest", - RouteHandler = new GetPackageManifestRequestHandler() - }, new HttpCwsRoute("appdebug") { Name = "AppDebug", @@ -267,7 +260,7 @@ namespace PepperDash.Essentials.Core.Web _server.Start(); _debugServer.Start(); - + GetPaths(); } @@ -308,15 +301,7 @@ 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); } } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs index 59c662dc..01e38834 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs @@ -17,10 +17,7 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers /// Represents a DebugSessionRequestHandler /// public class DebugSessionRequestHandler : WebApiBaseRequestHandler - { - private CTimer _portForwardTimeoutTimer; - private readonly object _timerLock = new object(); - + { /// /// Constructor /// @@ -51,7 +48,6 @@ 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) { @@ -63,50 +59,14 @@ 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; - var data = new + object data = new { - url = Debug.WebsocketSink.Url, - fallbackUrl = csIp != null ? url.Replace(csIp, ip) : null + url = Debug.WebsocketSink.Url }; Debug.LogMessage(LogEventLevel.Information, "Debug Session URL: {0}", url); - Debug.LogMessage(LogEventLevel.Information, "Fallback Debug Session URL: {0}", data.fallbackUrl); // Return the port number with the full url of the WS Server var res = JsonConvert.SerializeObject(data); @@ -130,49 +90,8 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers /// 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(); @@ -180,55 +99,5 @@ 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); - } - } - - /// - /// Cancels the port forward timeout timer if a session is being explicitly stopped. - /// - private void CancelPortForwardTimeout() - { - lock (_timerLock) - { - _portForwardTimeoutTimer?.Dispose(); - _portForwardTimeoutTimer = null; - } - } - } } diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs index 9831fa91..ca9eeb81 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs @@ -1,14 +1,13 @@ -using System; -using System.Linq; +using System.Linq; using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; using PepperDash.Core.Web.RequestHandlers; namespace PepperDash.Essentials.Core.Web.RequestHandlers { - /// - /// Represents a GetFeedbacksForDeviceRequestHandler - /// + /// + /// Represents a GetFeedbacksForDeviceRequestHandler + /// public class GetFeedbacksForDeviceRequestHandler : WebApiBaseRequestHandler { /// @@ -77,7 +76,7 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers Value = feedback.IntValue }; - var stringFeedback = + var stringFeedback = from feedback in device.Feedbacks.OfType() where !string.IsNullOrEmpty(feedback.Key) select new diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetPackageManifestRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetPackageManifestRequestHandler.cs deleted file mode 100644 index eeea75cd..00000000 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetPackageManifestRequestHandler.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using Crestron.SimplSharp.WebScripting; -using Newtonsoft.Json; -using PepperDash.Core.Web.RequestHandlers; -using PepperDash.Essentials.Core.Config; - -namespace PepperDash.Essentials.Core.Web.RequestHandlers -{ - /// - /// Represents a GetPackageManifestRequestHandler - /// - public class GetPackageManifestRequestHandler : WebApiBaseRequestHandler - { - /// - /// Constructor - /// - /// - /// base(true) enables CORS support by default - /// - public GetPackageManifestRequestHandler() - : base(true) - { - } - - /// - /// Handles GET method requests - /// - /// - 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(); - } - } - - /// - /// Deep-copies the config's VersionData so the live config object is never mutated - /// - private static VersionData CloneVersionData(VersionData source) - { - if (source == null) - { - return null; - } - - var json = JsonConvert.SerializeObject(source); - return JsonConvert.DeserializeObject(json); - } - - /// - /// Enriches (or creates) the essentials entry from the loaded PepperDash.Essentials.Core assembly - /// - 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; - } - - /// - /// Merges reflection data from loaded plugin assemblies with the config's packages list - /// - 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()) - .Where(p => p != null) - .ToList(); - var matchedConfigPackages = new System.Collections.Generic.HashSet(); - var mergedPackages = new System.Collections.Generic.List(); - - 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 - // 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() - .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); - } - } -} diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs index 949f8bd5..5a4b7df7 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs @@ -1,6 +1,5 @@ using System; -using System.Collections.Generic; using Crestron.SimplSharp.CrestronAuthentication; using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; @@ -92,19 +91,7 @@ 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 = 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.Write(JsonConvert.SerializeObject(new { Token = token }, Formatting.Indented), false); context.Response.End(); } catch (System.Exception ex) @@ -134,40 +121,4 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers /// public string Password { get; set; } } - - /// - /// Represents a LoginResponse - /// - internal class LoginResponse - { - /// - /// Gets or sets the username. - /// - public string UserName { get; set; } - - /// - /// Gets or sets the access level. - /// - public Authentication.UserAuthenticationLevelEnum Access { get; set; } - - /// - /// Gets or sets the token authenticated state. - /// - public Authentication.eTokenAuthenticatedState State { get; set; } - - /// - /// Gets or sets the list of groups. - /// - public List Groups { get; set; } - - /// - /// Gets or sets the active directory connection flag. - /// - public int ADConnect { get; set; } - - /// - /// Gets or sets the valid flag indicating whether the token is valid. - /// - public bool Valid { get; set; } - } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/Cameras/CameraVisca.cs b/src/PepperDash.Essentials.Devices.Common/Cameras/CameraVisca.cs index f383dd0a..126bcd27 100644 --- a/src/PepperDash.Essentials.Devices.Common/Cameras/CameraVisca.cs +++ b/src/PepperDash.Essentials.Devices.Common/Cameras/CameraVisca.cs @@ -17,8 +17,6 @@ namespace PepperDash.Essentials.Devices.Common.Cameras /// /// Represents a CameraVisca /// - [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; diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs index d542f52a..80c1f8ee 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs @@ -152,15 +152,6 @@ 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 @@ -183,15 +174,6 @@ 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(); } } diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftControllerConfigProperties.cs b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftControllerConfigProperties.cs index 88113a08..1c4f9906 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftControllerConfigProperties.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftControllerConfigProperties.cs @@ -41,23 +41,5 @@ namespace PepperDash.Essentials.Devices.Common.Shades /// [JsonProperty("muteOnScreenUp")] public bool MuteOnScreenUp { get; set; } - - /// - /// 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. - /// - [JsonProperty("disableAutoLowerOnPowerOn")] - public bool DisableAutoLowerOnPowerOn { get; set; } - - /// - /// 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 ; together they make a controller fully - /// manual while leaving other controllers (e.g. the lift) on their default automatic behavior. - /// - [JsonProperty("disableAutoRaiseOnPowerOff")] - public bool DisableAutoRaiseOnPowerOff { get; set; } } } diff --git a/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj b/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj index 317fa2d3..7be4372d 100644 --- a/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj +++ b/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj @@ -29,6 +29,6 @@ - + \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs index bdefa09d..e9f7908c 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs @@ -21,7 +21,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec /// /// Represents a MockVC /// - public class MockVC : VideoCodecBase, IRoutingSource, IHasCallHistory, IHasScheduleAwareness, IHasCallFavorites, IHasDirectory, IHasCodecCameras, IHasCameraAutoMode, IHasCodecRoomPresets, IRoutingInputs + public class MockVC : VideoCodecBase, IRoutingSource, IHasCallHistory, IHasScheduleAwareness, IHasCallFavorites, IHasDirectory, IHasCodecCameras, IHasCameraAutoMode, IHasCodecRoomPresets { /// /// Gets or sets the PropertiesConfig diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs index 04c7e64d..b0b5ef55 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs @@ -26,7 +26,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec /// /// Base class for video codec devices /// - public abstract class VideoCodecBase : ReconfigurableDevice, + public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutputs, IUsageTracking, IHasDialer, IHasContentSharing, ICodecAudio, iVideoCodecInfo, IBridgeAdvanced, IHasStandbyMode { private const int XSigEncoding = 28591; diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceMessageBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceMessageBase.cs index 0198df2f..54a6ec36 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceMessageBase.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceMessageBase.cs @@ -10,7 +10,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// The device key /// - [JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)] + [JsonProperty("key")] /// /// Gets or sets the Key /// @@ -19,19 +19,19 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// The device name /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + [JsonProperty("name")] public string Name { get; set; } /// /// The type of the message class /// - [JsonProperty("messageType", NullValueHandling = NullValueHandling.Ignore)] + [JsonProperty("messageType")] public string MessageType => GetType().Name; /// /// Gets or sets the MessageBasePath /// - [JsonProperty("messageBasePath", NullValueHandling = NullValueHandling.Ignore)] + [JsonProperty("messageBasePath")] public string MessageBasePath { get; set; } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceStateMessageBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceStateMessageBase.cs index c5c0ab65..87f19e3f 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceStateMessageBase.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceStateMessageBase.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using Newtonsoft.Json; @@ -12,15 +11,13 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// The interfaces implmented by the device sending the messsage /// - [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")] + [JsonProperty("interfaces")] public List Interfaces { get; private set; } /// /// Sets the interfaces implemented by the device sending the message /// /// - [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 interfaces) { Interfaces = interfaces; diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs index eb3afec3..3031f4ba 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs @@ -264,9 +264,6 @@ namespace PepperDash.Essentials.AppServer.Messengers message.Name = _device.Name; - message.MessageBasePath = MessagePath; - - var token = JToken.FromObject(message); PostStatusMessage(token, MessagePath, clientId); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj index a9223061..d13d1a09 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj +++ b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj @@ -33,7 +33,7 @@ - + diff --git a/src/PepperDash.Essentials.MobileControl/ConnectedClientVersionInfo.cs b/src/PepperDash.Essentials.MobileControl/ConnectedClientVersionInfo.cs deleted file mode 100644 index 68f5e36e..00000000 --- a/src/PepperDash.Essentials.MobileControl/ConnectedClientVersionInfo.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace PepperDash.Essentials -{ - /// - /// Represents the version information reported by a connected Mobile Control UI client - /// - public class ConnectedClientVersionInfo - { - /// - /// Gets or sets the client id - /// - [JsonProperty("clientId")] - public string ClientId { get; set; } - - /// - /// Gets or sets the room key the client joined - /// - [JsonProperty("roomKey")] - public string RoomKey { get; set; } - - /// - /// Gets or sets the touchpanel key the client joined as, if any - /// - [JsonProperty("touchpanelKey")] - public string TouchpanelKey { get; set; } - - /// - /// Gets or sets the app version reported by the client (e.g. the React app's build-time APP_VERSION) - /// - [JsonProperty("appVersion")] - public string AppVersion { get; set; } - - /// - /// Gets or sets the expected app version from the system config's versions.touchpanelWrapperApp, if configured - /// - [JsonProperty("expectedAppVersion")] - public string ExpectedAppVersion { get; set; } - - /// - /// Gets or sets the UTC time the client last reported this version - /// - [JsonProperty("lastSeen")] - public DateTime LastSeen { get; set; } - - /// - /// Returns a copy of this instance, safe for callers outside the owning lock to hold/mutate - /// without affecting the internally tracked instance - /// - public ConnectedClientVersionInfo Clone() => new ConnectedClientVersionInfo - { - ClientId = ClientId, - RoomKey = RoomKey, - TouchpanelKey = TouchpanelKey, - AppVersion = AppVersion, - ExpectedAppVersion = ExpectedAppVersion, - LastSeen = LastSeen - }; - } -} diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs b/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs index 963e7fd5..ec7219a3 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -41,10 +40,9 @@ namespace PepperDash.Essentials public bool EnableApiServer { get; set; } = true; /// - /// Enables subscriptions for messengers + /// Enable subscriptions for Messengers /// [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; } } @@ -290,4 +288,4 @@ namespace PepperDash.Essentials /// NEO } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index c17cf3b9..3adcaf87 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -69,32 +69,11 @@ namespace PepperDash.Essentials private readonly Dictionary _defaultMessengers = new Dictionary(); - private readonly Dictionary _connectedClientVersions = - new Dictionary(StringComparer.InvariantCultureIgnoreCase); - - private readonly object _connectedClientVersionsLock = new object(); - /// /// Get the custom messengers with subscriptions /// public ReadOnlyDictionary Messengers => new ReadOnlyDictionary(_messengers.Values.OfType().ToDictionary(k => k.Key, v => v)); - /// - /// Gets the most recently reported UI app version for each connected client, keyed by clientId - /// - public ReadOnlyDictionary ConnectedClientVersions - { - get - { - lock (_connectedClientVersionsLock) - { - return new ReadOnlyDictionary( - _connectedClientVersions.ToDictionary(kv => kv.Key, kv => kv.Value.Clone()) - ); - } - } - } - /// /// Get the default messengers /// @@ -158,7 +137,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." @@ -1803,28 +1782,6 @@ 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" - ); - } - } } /// @@ -2224,8 +2181,6 @@ namespace PepperDash.Essentials var roomKey = content["roomKey"].Value(); var touchpanelKey = content.SelectToken("touchpanelKey"); - TrackClientAppVersion(clientId, roomKey, touchpanelKey?.Value(), content.SelectToken("appVersion")?.Value()); - if (_roomCombiner == null) { var message = new MobileControlMessage @@ -2297,50 +2252,6 @@ namespace PepperDash.Essentials SendTouchpanelKey(clientId, touchpanelKey); } - /// - /// 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. - /// - 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) diff --git a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj index b0e2dd9f..235e0899 100644 --- a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj +++ b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj @@ -38,7 +38,7 @@ - + diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs index 262cb34e..5830782d 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs @@ -25,28 +25,11 @@ namespace PepperDash.Essentials.Touchpanel /// Mobile Control touchpanel controller that provides app control, Zoom integration, /// and mobile control functionality for Crestron touchpanels. /// - public class MobileControlTouchpanelController : TouchpanelBase, IHasFeedback, ITswAppControl, ITswZoomControl, IDeviceInfoProvider, IMobileControlCrestronTouchpanelController, ITheme, ICommunicationMonitor + public class MobileControlTouchpanelController : TouchpanelBase, IHasFeedback, ITswAppControl, ITswZoomControl, IDeviceInfoProvider, IMobileControlCrestronTouchpanelController, ITheme { private readonly MobileControlTouchpanelProperties localConfig; private IMobileControlRoomMessenger _bridge; - /// - /// Gets the CommunicationMonitor tracking the panel's online/offline state - /// - 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; /// @@ -145,11 +128,6 @@ namespace PepperDash.Essentials.Touchpanel { localConfig = config; - if (panel != null) - { - CommunicationMonitor = new CrestronGenericBaseCommunicationMonitor(this, panel, 120000, 300000); - } - AddPostActivationAction(SubscribeForMobileControlUpdates); ThemeFeedback = new StringFeedback($"{Key}-theme", () => Theme); @@ -388,8 +366,6 @@ namespace PepperDash.Essentials.Touchpanel /// public override bool CustomActivate() { - CommunicationMonitor?.Start(); - var appMessenger = new ITswAppControlMessenger($"appControlMessenger-{Key}", $"/device/{Key}", this); var zoomMessenger = new ITswZoomControlMessenger($"zoomControlMessenger-{Key}", $"/device/{Key}", this); @@ -417,17 +393,6 @@ namespace PepperDash.Essentials.Touchpanel return base.CustomActivate(); } - /// - /// Stops the CommunicationMonitor on deactivation. - /// - /// True if deactivation was successful; otherwise, false. - public override bool Deactivate() - { - CommunicationMonitor?.Stop(); - - return base.Deactivate(); - } - /// /// Handles device extender signal changes for system reserved signals. /// @@ -554,8 +519,15 @@ namespace PepperDash.Essentials.Touchpanel return false; }) ? csIpAddress.ToString() : processorIp; - // replace the host but preserve whatever scheme (http/https) is already present in the URL - var updatedUrl = Regex.Replace(url, @"^(https?)://[^:/]+", $"$1://{ip}"); + 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}"); this.LogVerbose("Updated URL: {updatedUrl}", updatedUrl); @@ -768,7 +740,7 @@ namespace PepperDash.Essentials.Touchpanel /// public MobileControlTouchpanelControllerFactory() { - TypeNames = new List() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "mctsw1080", "mcts1080", "mcxpanel", "mcdge1000" }; + TypeNames = new List() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "mcxpanel", "mcdge1000" }; MinimumEssentialsFrameworkVersion = "2.0.0"; factories = new Dictionary> @@ -793,8 +765,6 @@ 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)} }; } diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index c96dc116..1c9ed37a 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -127,16 +127,6 @@ namespace PepperDash.Essentials.WebSocketServer /// public int Port { get; private set; } - /// - /// Gets the HTTP scheme to use for generated URLs, based on whether the direct server is configured as secure - /// - private string HttpScheme => _parent.Config.DirectServer.Secure ? "https" : "http"; - - /// - /// Gets the WebSocket scheme to use for generated URLs, based on whether the direct server is configured as secure - /// - private string WsScheme => _parent.Config.DirectServer.Secure ? "wss" : "ws"; - /// /// Gets the user app URL prefix /// @@ -144,8 +134,7 @@ namespace PepperDash.Essentials.WebSocketServer { get { - return string.Format("{0}://{1}:{2}{3}?token=", - HttpScheme, + return string.Format("http://{0}:{1}{2}?token=", CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0), Port, _userAppBaseHref); @@ -284,7 +273,7 @@ namespace PepperDash.Essentials.WebSocketServer { base.Initialize(); - _server = new HttpServer(Port, _parent.Config.DirectServer.Secure); + _server = new HttpServer(Port, false); _server.OnGet += Server_OnGet; @@ -302,7 +291,7 @@ namespace PepperDash.Essentials.WebSocketServer { ClientCertificateRequired = false, CheckCertificateRevocation = false, - EnabledSslProtocols = SslProtocols.Tls12 + EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 }; } @@ -414,11 +403,11 @@ namespace PepperDash.Essentials.WebSocketServer ip = csIpAddress.ToString(); } - var appUrl = $"{HttpScheme}://{ip}:{Port}/mc/app?token={touchpanel.Key}"; + var appUrl = $"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}"; this.LogVerbose("Sending URL {appUrl} to touchpanel {touchpanelKey}", appUrl, touchpanel.Touchpanel.Key); - touchpanel.Touchpanel.SetAppUrl(appUrl); + touchpanel.Touchpanel.SetAppUrl($"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}"); } } @@ -498,7 +487,7 @@ namespace PepperDash.Essentials.WebSocketServer { var config = new MobileControlApplicationConfig { - ApiPath = string.Format("{0}://{1}:{2}/mc/api", HttpScheme, processorIp, Port), + ApiPath = string.Format("http://{0}:{1}/mc/api", processorIp, _parent.Config.DirectServer.Port), GatewayAppPath = "", LogoPath = _parent.Config.ApplicationConfig?.LogoPath ?? "logo/logo.png", EnableDev = _parent.Config.ApplicationConfig?.EnableDev ?? false, @@ -1109,7 +1098,6 @@ 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"), @@ -1162,11 +1150,6 @@ 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; @@ -1230,7 +1213,8 @@ 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 wsUrl = $"{WsScheme}://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{Port}{_wsPath}{token}?clientId={clientId}"; + var wsProtocol = "ws"; + var wsUrl = $"{wsProtocol}://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{Port}{_wsPath}{token}?clientId={clientId}"; // Construct the response object JoinResponse jRes = new JoinResponse @@ -1242,8 +1226,7 @@ namespace PepperDash.Essentials.WebSocketServer Config = _parent.GetConfigWithPluginVersion(), CodeExpires = new DateTime().AddYears(1), UserCode = bridge.UserCode, - UserAppUrl = string.Format("{0}://{1}:{2}/mc/app", - HttpScheme, + UserAppUrl = string.Format("http://{0}:{1}/mc/app", CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0), Port), WebSocketUrl = wsUrl, @@ -1268,8 +1251,6 @@ 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); diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index 47de4d3e..e6c61c60 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -27,7 +27,7 @@ namespace PepperDash.Essentials private CEvent _initializeEvent; private const long StartupTime = 500; - // private const string minimumFirmwareVersion = "2.8006.00110"; + private const string minimumFirmwareVersion = "2.8006.00110"; /// /// Initializes a new instance of the ControlSystem class @@ -50,21 +50,21 @@ namespace PepperDash.Essentials { // 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; + var fwVersion = InitialParametersClass.FirmwareVersion; - // Debug.LogInformation("Control System Hardware Version: {fwVersion}", fwVersion); + 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; - // } + // 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. @@ -130,8 +130,14 @@ 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( - PrintPortalInfo, + 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), "portalinfo", "Shows portal URLS from configuration", ConsoleAccessLevelEnum.AccessOperator); @@ -154,29 +160,6 @@ namespace PepperDash.Essentials } } - private void PrintPortalInfo(string args) - { - if(ConfigReader.ConfigObject == null) - { - CrestronConsole.ConsoleCommandResponse("No configuration loaded. Cannot show portal URLs."); - return; - } - - if (string.IsNullOrEmpty(ConfigReader.ConfigObject.SystemUrl) && string.IsNullOrEmpty(ConfigReader.ConfigObject.TemplateUrl)) - { - CrestronConsole.ConsoleCommandResponse("No portal URLs defined in config."); - return; - } - - CrestronConsole.ConsoleCommandResponse( - "This system can be found at the following URLs:{2}" + - "System URL: {0}{2}" + - "Template URL: {1}{2}", - ConfigReader.ConfigObject?.SystemUrl, - ConfigReader.ConfigObject?.TemplateUrl, - CrestronEnvironment.NewLine); - } - /// /// DeterminePlatform method /// @@ -274,6 +257,11 @@ namespace PepperDash.Essentials PluginLoader.AddProgramAssemblies(); _ = new Core.DeviceFactory(); + // _ = new Devices.Common.DeviceFactory(); + // _ = new DeviceFactory(); + + // _ = new ProcessorExtensionDeviceFactory(); + // _ = new MobileControlFactory(); LoadAssets(Global.ApplicationDirectoryPathPrefix, Global.FilePathPrefix); @@ -286,9 +274,10 @@ namespace PepperDash.Essentials PluginLoader.LoadPlugins(); Debug.LogMessage(LogEventLevel.Information, "Folder structure verified. Loading config..."); - if (!ConfigReader.LoadConfig2() || ConfigReader.ConfigObject == null) + if (!ConfigReader.LoadConfig2()) { - Debug.LogMessage(LogEventLevel.Warning, "Unable to load config file."); + Debug.LogMessage(LogEventLevel.Information, "Essentials Load complete with errors"); + return; } Load(); @@ -410,12 +399,6 @@ 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; @@ -469,7 +452,7 @@ namespace PepperDash.Essentials var tlc = TieLineCollection.Default; - if (ConfigReader.ConfigObject?.TieLines == null) + if (ConfigReader.ConfigObject.TieLines == null) { return; } @@ -766,7 +749,7 @@ namespace PepperDash.Essentials /// 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; @@ -801,16 +784,15 @@ namespace PepperDash.Essentials /// /// Fires up a logo server if not already running /// - [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 (ConfigReader.ConfigObject?.Rooms == null || + if ( !ConfigReader.ConfigObject.Rooms.Any( CheckRoomConfig)) { diff --git a/src/PepperDash.Essentials/PepperDash.Essentials.csproj b/src/PepperDash.Essentials/PepperDash.Essentials.csproj index cb83bf84..20a42ffd 100644 --- a/src/PepperDash.Essentials/PepperDash.Essentials.csproj +++ b/src/PepperDash.Essentials/PepperDash.Essentials.csproj @@ -48,7 +48,7 @@ - +