From e49c69a12fc7599cfd0ede396d59039782d87b3a Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 15 Aug 2025 09:48:30 -0500 Subject: [PATCH] feat: add CommBridge class and enhance EssentialsBridgeableDevice with new constructors --- src/PepperDash.Core/Comm/eControlMethods.cs | 6 +- .../Bridges/IBridge.cs | 7 + .../Comm and IR/CommBridge.cs | 138 ++++++++ .../Comm and IR/CommFactory.cs | 307 +++++++++--------- .../Devices/EssentialsBridgeableDevice.cs | 15 +- 5 files changed, 319 insertions(+), 154 deletions(-) create mode 100644 src/PepperDash.Essentials.Core/Comm and IR/CommBridge.cs diff --git a/src/PepperDash.Core/Comm/eControlMethods.cs b/src/PepperDash.Core/Comm/eControlMethods.cs index 28a95b12..695594a1 100644 --- a/src/PepperDash.Core/Comm/eControlMethods.cs +++ b/src/PepperDash.Core/Comm/eControlMethods.cs @@ -74,6 +74,10 @@ namespace PepperDash.Core /// /// Secure TCP/IP /// - SecureTcpIp + SecureTcpIp, + /// + /// Used when comms needs to be handled in SIMPL and bridged opposite the normal direction + /// + ComBridge } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/IBridge.cs b/src/PepperDash.Essentials.Core/Bridges/IBridge.cs index 6e921921..34c11171 100644 --- a/src/PepperDash.Essentials.Core/Bridges/IBridge.cs +++ b/src/PepperDash.Essentials.Core/Bridges/IBridge.cs @@ -7,6 +7,13 @@ namespace PepperDash.Essentials.Core.Bridges /// public interface IBridgeAdvanced { + /// + /// Links the bridge to the API using the provided trilist, join start, join map key, and bridge. + /// + /// The trilist to link to. + /// The starting join number. + /// The key for the join map. + /// The EISC API bridge. void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Comm and IR/CommBridge.cs b/src/PepperDash.Essentials.Core/Comm and IR/CommBridge.cs new file mode 100644 index 00000000..b93a8212 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Comm and IR/CommBridge.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Crestron.SimplSharp.CrestronSockets; +using Crestron.SimplSharpPro.DeviceSupport; +using Newtonsoft.Json; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core.Bridges; +using PepperDash.Essentials.Core.Config; +using PepperDash.Essentials.Core.Devices; + + +namespace PepperDash.Essentials.Core +{ + /// + /// Implements IBasicCommunication and sends all communication through an EISC + /// + [Description("Generic communication wrapper class for any IBasicCommunication type")] + public class CommBridge : EssentialsBridgeableDevice, IBasicCommunication + { + private EiscApiAdvanced eisc; + + private IBasicCommunicationJoinMap joinMap; + + /// + /// Event triggered when text is received through the communication bridge. + /// + public event EventHandler TextReceived; + + /// + /// Event triggered when bytes are received through the communication bridge. + /// + public event EventHandler BytesReceived; + + /// + /// Indicates whether the communication bridge is currently connected. + /// + public bool IsConnected { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// The unique key for the communication bridge. + /// The display name for the communication bridge. + public CommBridge(string key, string name) + : base(key, name) + { + + } + + /// + /// Sends a byte array through the communication bridge. + /// + /// The byte array to send. + public void SendBytes(byte[] bytes) + { + if (eisc == null) + { + this.LogWarning("EISC is null, cannot send bytes."); + return; + } + eisc.Eisc.SetString(joinMap.SendText.JoinNumber, Encoding.ASCII.GetString(bytes, 0, bytes.Length)); + } + + /// + /// Sends a text string through the communication bridge. + /// + /// The text string to send. + public void SendText(string text) + { + if (eisc == null) + { + this.LogWarning("EISC is null, cannot send text."); + return; + } + eisc.Eisc.SetString(joinMap.SendText.JoinNumber, text); + } + + /// + /// Initiates a connection through the communication bridge. + /// + public void Connect() + { + if (eisc == null) + { + this.LogWarning("EISC is null, cannot connect."); + return; + } + eisc.Eisc.SetBool(joinMap.Connect.JoinNumber, true); + } + + /// + /// Terminates the connection through the communication bridge. + /// + public void Disconnect() + { + if (eisc == null) + { + this.LogWarning("EISC is null, cannot disconnect."); + return; + } + eisc.Eisc.SetBool(joinMap.Connect.JoinNumber, false); + } + + /// + public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) + { + joinMap = new IBasicCommunicationJoinMap(joinStart); + + var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey); + + if (!string.IsNullOrEmpty(joinMapSerialized)) + joinMap = JsonConvert.DeserializeObject(joinMapSerialized); + + if (bridge != null) + { + bridge.AddJoinMap(Key, joinMap); + } + else + { + this.LogWarning("Please update config to use 'eiscapiadvanced' to get all join map features for this device."); + } + + this.LogDebug("Linking to Trilist '{0}'", trilist.ID.ToString("X")); + + eisc = bridge; + + trilist.SetBoolSigAction(joinMap.Connected.JoinNumber, (b) => IsConnected = b); + + trilist.SetStringSigAction(joinMap.TextReceived.JoinNumber, (s) => + { + TextReceived?.Invoke(this, new GenericCommMethodReceiveTextArgs(s)); + BytesReceived?.Invoke(this, new GenericCommMethodReceiveBytesArgs(Encoding.ASCII.GetBytes(s))); + }); + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs b/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs index 2cd81a77..96f902f3 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs @@ -6,6 +6,7 @@ using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.DM; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Org.BouncyCastle.Tls; using PepperDash.Core; using PepperDash.Essentials.Core.Config; using Serilog.Events; @@ -38,9 +39,9 @@ namespace PepperDash.Essentials.Core /// Returns a comm method of either com port, TCP, SSH, and puts this into the DeviceManager /// /// The Device config object - /// - /// CreateCommForDevice method - /// + /// + /// CreateCommForDevice method + /// public static IBasicCommunication CreateCommForDevice(DeviceConfig deviceConfig) { EssentialsControlPropertiesConfig controlConfig = GetControlPropertiesConfig(deviceConfig); @@ -56,35 +57,38 @@ namespace PepperDash.Essentials.Core case eControlMethod.Com: comm = new ComPortController(deviceConfig.Key + "-com", GetComPort, controlConfig.ComParams.Value, controlConfig); break; - case eControlMethod.Cec: - comm = new CecPortController(deviceConfig.Key + "-cec", GetCecPort, controlConfig); - break; + case eControlMethod.ComBridge: + comm = new CommBridge(deviceConfig.Key + "-simpl", deviceConfig.Name + " Simpl"); + break; + case eControlMethod.Cec: + comm = new CecPortController(deviceConfig.Key + "-cec", GetCecPort, controlConfig); + break; case eControlMethod.IR: break; - case eControlMethod.Ssh: - { - var ssh = new GenericSshClient(deviceConfig.Key + "-ssh", c.Address, c.Port, c.Username, c.Password); - ssh.AutoReconnect = c.AutoReconnect; - if(ssh.AutoReconnect) - ssh.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs; - comm = ssh; - break; - } - case eControlMethod.Tcpip: - { - var tcp = new GenericTcpIpClient(deviceConfig.Key + "-tcp", c.Address, c.Port, c.BufferSize); - tcp.AutoReconnect = c.AutoReconnect; - if (tcp.AutoReconnect) - tcp.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs; - comm = tcp; - break; - } - case eControlMethod.Udp: - { - var udp = new GenericUdpServer(deviceConfig.Key + "-udp", c.Address, c.Port, c.BufferSize); - comm = udp; - break; - } + case eControlMethod.Ssh: + { + var ssh = new GenericSshClient(deviceConfig.Key + "-ssh", c.Address, c.Port, c.Username, c.Password); + ssh.AutoReconnect = c.AutoReconnect; + if (ssh.AutoReconnect) + ssh.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs; + comm = ssh; + break; + } + case eControlMethod.Tcpip: + { + var tcp = new GenericTcpIpClient(deviceConfig.Key + "-tcp", c.Address, c.Port, c.BufferSize); + tcp.AutoReconnect = c.AutoReconnect; + if (tcp.AutoReconnect) + tcp.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs; + comm = tcp; + break; + } + case eControlMethod.Udp: + { + var udp = new GenericUdpServer(deviceConfig.Key + "-udp", c.Address, c.Port, c.BufferSize); + comm = udp; + break; + } case eControlMethod.Telnet: break; case eControlMethod.SecureTcpIp: @@ -98,7 +102,7 @@ namespace PepperDash.Essentials.Core } default: break; - } + } } catch (Exception e) { @@ -107,15 +111,14 @@ namespace PepperDash.Essentials.Core } // put it in the device manager if it's the right flavor - var comDev = comm as Device; - if (comDev != null) + if (comm is Device comDev) DeviceManager.AddDevice(comDev); return comm; } - /// - /// GetComPort method - /// + /// + /// GetComPort method + /// public static ComPort GetComPort(EssentialsControlPropertiesConfig config) { var comPar = config.ComParams; @@ -126,74 +129,74 @@ namespace PepperDash.Essentials.Core return null; } - /// - /// Gets an ICec port from a RoutingInput or RoutingOutput on a device - /// - /// - /// - /// - /// GetCecPort method - /// - public static ICec GetCecPort(ControlPropertiesConfig config) - { - try - { - var dev = DeviceManager.GetDeviceForKey(config.ControlPortDevKey); + /// + /// Gets an ICec port from a RoutingInput or RoutingOutput on a device + /// + /// + /// + /// + /// GetCecPort method + /// + public static ICec GetCecPort(ControlPropertiesConfig config) + { + try + { + var dev = DeviceManager.GetDeviceForKey(config.ControlPortDevKey); - Debug.LogMessage(LogEventLevel.Information, "GetCecPort: device '{0}' {1}", config.ControlPortDevKey, dev == null - ? "is not valid, failed to get cec port" - : "found in device manager, attempting to get cec port"); + Debug.LogMessage(LogEventLevel.Information, "GetCecPort: device '{0}' {1}", config.ControlPortDevKey, dev == null + ? "is not valid, failed to get cec port" + : "found in device manager, attempting to get cec port"); - if (dev == null) - return null; + if (dev == null) + return null; - if (String.IsNullOrEmpty(config.ControlPortName)) - { - Debug.LogMessage(LogEventLevel.Information, "GetCecPort: '{0}' - Configuration missing 'ControlPortName'", config.ControlPortDevKey); - return null; - } + if (String.IsNullOrEmpty(config.ControlPortName)) + { + Debug.LogMessage(LogEventLevel.Information, "GetCecPort: '{0}' - Configuration missing 'ControlPortName'", config.ControlPortDevKey); + return null; + } - var inputsOutputs = dev as IRoutingInputsOutputs; - if (inputsOutputs == null) - { + var inputsOutputs = dev as IRoutingInputsOutputs; + if (inputsOutputs == null) + { Debug.LogMessage(LogEventLevel.Information, "GetCecPort: Device '{0}' does not support IRoutingInputsOutputs, failed to get CEC port called '{1}'", config.ControlPortDevKey, config.ControlPortName); - return null; - } + return null; + } var inputPort = inputsOutputs.InputPorts[config.ControlPortName]; - if (inputPort != null && inputPort.Port is ICec) + if (inputPort != null && inputPort.Port is ICec) return inputPort.Port as ICec; - - var outputPort = inputsOutputs.OutputPorts[config.ControlPortName]; - if (outputPort != null && outputPort.Port is ICec) - return outputPort.Port as ICec; - } - catch (Exception ex) - { - Debug.LogMessage(LogEventLevel.Debug, "GetCecPort Exception Message: {0}", ex.Message); - Debug.LogMessage(LogEventLevel.Verbose, "GetCecPort Exception StackTrace: {0}", ex.StackTrace); - if (ex.InnerException != null) - Debug.LogMessage(LogEventLevel.Information, "GetCecPort Exception InnerException: {0}", ex.InnerException); - } + + var outputPort = inputsOutputs.OutputPorts[config.ControlPortName]; + if (outputPort != null && outputPort.Port is ICec) + return outputPort.Port as ICec; + } + catch (Exception ex) + { + Debug.LogMessage(LogEventLevel.Debug, "GetCecPort Exception Message: {0}", ex.Message); + Debug.LogMessage(LogEventLevel.Verbose, "GetCecPort Exception StackTrace: {0}", ex.StackTrace); + if (ex.InnerException != null) + Debug.LogMessage(LogEventLevel.Information, "GetCecPort Exception InnerException: {0}", ex.InnerException); + } Debug.LogMessage(LogEventLevel.Information, "GetCecPort: Device '{0}' does not have a CEC port called '{1}'", config.ControlPortDevKey, config.ControlPortName); return null; - } + } /// /// Helper to grab the IComPorts device for this PortDeviceKey. Key "controlSystem" will /// return the ControlSystem object from the Global class. /// /// IComPorts device or null if the device is not found or does not implement IComPorts - /// - /// GetIComPortsDeviceFromManagedDevice method - /// + /// + /// GetIComPortsDeviceFromManagedDevice method + /// public static IComPorts GetIComPortsDeviceFromManagedDevice(string ComPortDevKey) { if ((ComPortDevKey.Equals("controlSystem", System.StringComparison.OrdinalIgnoreCase) @@ -210,81 +213,81 @@ namespace PepperDash.Essentials.Core } } - /// - /// Represents a EssentialsControlPropertiesConfig - /// - public class EssentialsControlPropertiesConfig : - ControlPropertiesConfig - { + /// + /// Represents a EssentialsControlPropertiesConfig + /// + public class EssentialsControlPropertiesConfig : + ControlPropertiesConfig + { - [JsonProperty("comParams", NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(ComSpecJsonConverter))] - public ComPort.ComPortSpec? ComParams { get; set; } + [JsonProperty("comParams", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(ComSpecJsonConverter))] + public ComPort.ComPortSpec? ComParams { get; set; } - [JsonProperty("cresnetId", NullValueHandling = NullValueHandling.Ignore)] - public string CresnetId { get; set; } + [JsonProperty("cresnetId", NullValueHandling = NullValueHandling.Ignore)] + public string CresnetId { get; set; } - /// - /// Attempts to provide uint conversion of string CresnetId - /// - [JsonIgnore] - public uint CresnetIdInt - { - get - { - try - { - return Convert.ToUInt32(CresnetId, 16); - } - catch (Exception) - { - throw new FormatException(string.Format("ERROR:Unable to convert Cresnet ID: {0} to hex. Error:\n{1}", CresnetId)); - } - } - } + /// + /// Attempts to provide uint conversion of string CresnetId + /// + [JsonIgnore] + public uint CresnetIdInt + { + get + { + try + { + return Convert.ToUInt32(CresnetId, 16); + } + catch (Exception) + { + throw new FormatException(string.Format("ERROR:Unable to convert Cresnet ID: {0} to hex. Error:\n{1}", CresnetId)); + } + } + } - [JsonProperty("infinetId", NullValueHandling = NullValueHandling.Ignore)] - /// - /// Gets or sets the InfinetId - /// - public string InfinetId { get; set; } + [JsonProperty("infinetId", NullValueHandling = NullValueHandling.Ignore)] + /// + /// Gets or sets the InfinetId + /// + public string InfinetId { get; set; } - /// - /// Attepmts to provide uiont conversion of string InifinetId - /// - [JsonIgnore] - public uint InfinetIdInt - { - get - { - try - { - return Convert.ToUInt32(InfinetId, 16); - } - catch (Exception) - { - throw new FormatException(string.Format("ERROR:Unable to conver Infinet ID: {0} to hex. Error:\n{1}", InfinetId)); - } - } - } - } + /// + /// Attepmts to provide uiont conversion of string InifinetId + /// + [JsonIgnore] + public uint InfinetIdInt + { + get + { + try + { + return Convert.ToUInt32(InfinetId, 16); + } + catch (Exception) + { + throw new FormatException(string.Format("ERROR:Unable to conver Infinet ID: {0} to hex. Error:\n{1}", InfinetId)); + } + } + } + } - /// - /// Represents a IrControlSpec - /// - public class IrControlSpec - { - /// - /// Gets or sets the PortDeviceKey - /// - public string PortDeviceKey { get; set; } - /// - /// Gets or sets the PortNumber - /// - public uint PortNumber { get; set; } - /// - /// Gets or sets the File - /// - public string File { get; set; } - } + /// + /// Represents a IrControlSpec + /// + public class IrControlSpec + { + /// + /// Gets or sets the PortDeviceKey + /// + public string PortDeviceKey { get; set; } + /// + /// Gets or sets the PortNumber + /// + public uint PortNumber { get; set; } + /// + /// Gets or sets the File + /// + public string File { get; set; } + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/EssentialsBridgeableDevice.cs b/src/PepperDash.Essentials.Core/Devices/EssentialsBridgeableDevice.cs index 0812ca4e..0b1a5ff7 100644 --- a/src/PepperDash.Essentials.Core/Devices/EssentialsBridgeableDevice.cs +++ b/src/PepperDash.Essentials.Core/Devices/EssentialsBridgeableDevice.cs @@ -3,16 +3,29 @@ using PepperDash.Essentials.Core.Bridges; namespace PepperDash.Essentials.Core { - public abstract class EssentialsBridgeableDevice:EssentialsDevice, IBridgeAdvanced + /// + /// Base class for devices that can be bridged to an EISC API. + /// + public abstract class EssentialsBridgeableDevice : EssentialsDevice, IBridgeAdvanced { + /// + /// Initializes a new instance of the class with the specified key. + /// + /// The unique key for the device. protected EssentialsBridgeableDevice(string key) : base(key) { } + /// + /// Initializes a new instance of the class with the specified key and name. + /// + /// The unique key for the device. + /// The display name for the device. protected EssentialsBridgeableDevice(string key, string name) : base(key, name) { } + /// public abstract void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge); } } \ No newline at end of file