diff --git a/docs/docs/usage/GenericComm.md b/docs/docs/usage/GenericComm.md
index 243536e5b..bb2dc11ed 100644
--- a/docs/docs/usage/GenericComm.md
+++ b/docs/docs/usage/GenericComm.md
@@ -183,11 +183,12 @@ namespace PepperDash.Core
Cresnet = 8,
Cec = 9,
Udp = 10,
+ UdpServer = 11,
}
}
```
-These enumerations are not case sensitive. Not all methods are valid for a ```genericComm``` device. For a comport, the only valid type would be ```Com```. For a direct network socket, valid options are ```Ssh```, ```Tcpip```, ```Telnet```, and ```Udp```.
+These enumerations are not case sensitive. Not all methods are valid for a ```genericComm``` device. For a comport, the only valid type would be ```Com```. For a direct network socket, valid options are ```Ssh```, ```Tcpip```, ```Telnet```, ```Udp```, and ```UdpServer```.
##### ComParams
@@ -287,7 +288,7 @@ This property maps to the number of the port on the device you have mapped the r
##### TcpSshParams
-A ```Ssh```, ```TcpIp```, or ```Udp``` device requires a ```tcpSshProperties``` object to set the propeties of the socket.
+A ```Ssh```, ```TcpIp```, ```Udp```, or ```UdpServer``` device requires a ```tcpSshProperties``` object to set the properties of the socket.
```Json
{
@@ -304,7 +305,7 @@ A ```Ssh```, ```TcpIp```, or ```Udp``` device requires a ```tcpSshProperties```
**```address```**
-This is the IP address, hostname, or FQDN of the resource you wish to open a socket to. In the case of a UDP device, you can set either a single whitelist address with this data, or an appropriate broadcast address.
+This is the IP address, hostname, or FQDN of the resource you wish to open a socket to. Use ```Udp``` for outbound UDP to a remote endpoint. Use ```UdpServer``` when you need Essentials to bind a local UDP listener.
**```port```**
diff --git a/src/PepperDash.Core/Comm/GenericUdpClient.cs b/src/PepperDash.Core/Comm/GenericUdpClient.cs
new file mode 100644
index 000000000..2918e0060
--- /dev/null
+++ b/src/PepperDash.Core/Comm/GenericUdpClient.cs
@@ -0,0 +1,415 @@
+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; }
+ }
+
+ ///
+ /// Will be set and unset by Connect and Disconnect only
+ ///
+ public bool ConnectEnabled
+ {
+ get { lock (stateLock) { return _connectEnabled; } }
+ private set { lock (stateLock) { _connectEnabled = value; } }
+ }
+
+ ///
+ /// 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;
+ }
+
+ ConnectEnabled = true;
+
+ lock (stateLock)
+ {
+ if (client != null)
+ return;
+
+ try
+ {
+ receiveCancellationTokenSource = new CancellationTokenSource();
+ client = new UdpClient();
+ client.Client.ReceiveBufferSize = BufferSize;
+ client.Client.SendBufferSize = BufferSize;
+ client.Connect(Hostname, Port);
+ ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED;
+ reconnectTimer.Change(ThreadingTimeout.Infinite, ThreadingTimeout.Infinite);
+ StartReceive(receiveCancellationTokenSource.Token);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogMessage(ex, "Error connecting UDP client {0}", this, Key);
+ CleanupClient();
+ ClientStatus = SocketStatus.SOCKET_STATUS_NO_CONNECT;
+ StartReconnectTimer();
+ }
+ }
+ }
+
+ ///
+ /// Disconnect method
+ ///
+ public void Disconnect()
+ {
+ ConnectEnabled = false;
+
+ lock (stateLock)
+ {
+ reconnectTimer.Change(ThreadingTimeout.Infinite, ThreadingTimeout.Infinite);
+ CleanupClient();
+ ClientStatus = SocketStatus.SOCKET_STATUS_NO_CONNECT;
+ }
+ }
+
+ ///
+ /// SendText method
+ ///
+ public void SendText(string 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)
+ 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);
+ HandleDisconnected();
+ return;
+ }
+ catch (Exception ex)
+ {
+ Debug.LogMessage(ex, "Unexpected UDP receive error for {0}", this, Key);
+ HandleDisconnected();
+ return;
+ }
+ }
+ }, 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;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Comm/eControlMethods.cs b/src/PepperDash.Core/Comm/eControlMethods.cs
index b807fdc53..65ae03fa0 100644
--- a/src/PepperDash.Core/Comm/eControlMethods.cs
+++ b/src/PepperDash.Core/Comm/eControlMethods.cs
@@ -52,10 +52,14 @@ public enum eControlMethod
///
Cec,
///
- /// UDP Server
+ /// UDP client
///
Udp,
///
+ /// UDP server
+ ///
+ UdpServer,
+ ///
/// HTTP client
///
Http,
diff --git a/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs b/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs
index 9318e04b2..f2c3fb0aa 100644
--- a/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs
+++ b/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs
@@ -92,10 +92,21 @@ public static IBasicCommunication CreateCommForDevice(DeviceConfig deviceConfig)
}
case eControlMethod.Udp:
{
- var udp = new GenericUdpServer(deviceConfig.Key + "-udp", c.Address, c.Port, c.BufferSize);
+ var udp = new GenericUdpClient(deviceConfig.Key + "-udp", c.Address, c.Port, c.BufferSize)
+ {
+ AutoReconnect = c.AutoReconnect
+ };
+ if (udp.AutoReconnect)
+ udp.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs;
comm = udp;
break;
}
+ case eControlMethod.UdpServer:
+ {
+ var udpServer = new GenericUdpServer(deviceConfig.Key + "-udpServer", c.Address, c.Port, c.BufferSize);
+ comm = udpServer;
+ break;
+ }
case eControlMethod.Telnet:
break;
case eControlMethod.SecureTcpIp: