mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-07-02 10:38:16 +00:00
Merge pull request #1420 from PepperDash/hotfix-udpClient-clean
feat: add support for UdpClient in communication methods
This commit is contained in:
commit
e1a5c32c1f
4 changed files with 482 additions and 3 deletions
|
|
@ -183,11 +183,12 @@ namespace PepperDash.Core
|
||||||
Cresnet = 8,
|
Cresnet = 8,
|
||||||
Cec = 9,
|
Cec = 9,
|
||||||
Udp = 10,
|
Udp = 10,
|
||||||
|
UdpClient = 11,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
These enumerations are not case sensitive. Not all methods are valid for a ```genericComm``` device. For a comport, the only valid type would be ```Com```. For a direct network socket, valid options are ```Ssh```, ```Tcpip```, ```Telnet```, and ```Udp```.
|
These enumerations are not case sensitive. Not all methods are valid for a ```genericComm``` device. For a comport, the only valid type would be ```Com```. For a direct network socket, valid options are ```Ssh```, ```Tcpip```, ```Telnet```, ```UdpClient```, and ```Udp```.
|
||||||
|
|
||||||
##### ComParams
|
##### ComParams
|
||||||
|
|
||||||
|
|
@ -287,7 +288,7 @@ This property maps to the number of the port on the device you have mapped the r
|
||||||
|
|
||||||
##### TcpSshParams
|
##### TcpSshParams
|
||||||
|
|
||||||
A ```Ssh```, ```TcpIp```, or ```Udp``` device requires a ```tcpSshProperties``` object to set the propeties of the socket.
|
A ```Ssh```, ```TcpIp```, ```UdpClient```, or ```Udp``` device requires a ```tcpSshProperties``` object to set the propeties of the socket.
|
||||||
|
|
||||||
```Json
|
```Json
|
||||||
{
|
{
|
||||||
|
|
@ -304,7 +305,7 @@ A ```Ssh```, ```TcpIp```, or ```Udp``` device requires a ```tcpSshProperties```
|
||||||
|
|
||||||
**```address```**
|
**```address```**
|
||||||
|
|
||||||
This is the IP address, hostname, or FQDN of the resource you wish to open a socket to. In the case of a UDP device, you can set either a single whitelist address with this data, or an appropriate broadcast address.
|
This is the IP address, hostname, or FQDN of the resource you wish to open a socket to. Use ```UdpClient``` for outbound UDP to a remote endpoint. Use ```Udp``` when you need Essentials to bind a local UDP listener.
|
||||||
|
|
||||||
**```port```**
|
**```port```**
|
||||||
|
|
||||||
|
|
|
||||||
463
src/PepperDash.Core/Comm/GenericUdpClient.cs
Normal file
463
src/PepperDash.Core/Comm/GenericUdpClient.cs
Normal file
|
|
@ -0,0 +1,463 @@
|
||||||
|
using System;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Crestron.SimplSharp;
|
||||||
|
using Crestron.SimplSharp.CrestronSockets;
|
||||||
|
using ThreadingTimeout = System.Threading.Timeout;
|
||||||
|
using NetSocketException = System.Net.Sockets.SocketException;
|
||||||
|
|
||||||
|
namespace PepperDash.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A class to handle basic UDP communications to a remote endpoint
|
||||||
|
/// </summary>
|
||||||
|
public class GenericUdpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
|
||||||
|
{
|
||||||
|
private const string SplusKey = "Uninitialized UdpClient";
|
||||||
|
|
||||||
|
private readonly object stateLock = new object();
|
||||||
|
private readonly Timer reconnectTimer;
|
||||||
|
|
||||||
|
private UdpClient client;
|
||||||
|
private CancellationTokenSource receiveCancellationTokenSource;
|
||||||
|
private bool connectEnabled;
|
||||||
|
private bool connectionRefusedLogged;
|
||||||
|
private SocketStatus clientStatus = SocketStatus.SOCKET_STATUS_NO_CONNECT;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Object to enable stream debugging
|
||||||
|
/// </summary>
|
||||||
|
public CommunicationStreamDebugging StreamDebugging { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fires when data is received from the remote endpoint and returns it as a byte array
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fires when data is received from the remote endpoint and returns it as text
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fires when the socket status changes
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Address of remote endpoint
|
||||||
|
/// </summary>
|
||||||
|
public string Hostname { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Port on remote endpoint
|
||||||
|
/// </summary>
|
||||||
|
public int Port { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Another S+ helper because large port numbers can be treated as signed ints
|
||||||
|
/// </summary>
|
||||||
|
public ushort UPort
|
||||||
|
{
|
||||||
|
get { return Convert.ToUInt16(Port); }
|
||||||
|
set { Port = Convert.ToInt32(value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defaults to 2000
|
||||||
|
/// </summary>
|
||||||
|
public int BufferSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when the local socket is created and associated with the configured remote endpoint
|
||||||
|
/// </summary>
|
||||||
|
public bool IsConnected
|
||||||
|
{
|
||||||
|
get { return ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S+ helper for IsConnected
|
||||||
|
/// </summary>
|
||||||
|
public ushort UIsConnected
|
||||||
|
{
|
||||||
|
get { return (ushort)(IsConnected ? 1 : 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current socket status of the client
|
||||||
|
/// </summary>
|
||||||
|
public SocketStatus ClientStatus
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (stateLock)
|
||||||
|
{
|
||||||
|
return clientStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
var shouldFireEvent = false;
|
||||||
|
|
||||||
|
lock (stateLock)
|
||||||
|
{
|
||||||
|
if (clientStatus != value)
|
||||||
|
{
|
||||||
|
clientStatus = value;
|
||||||
|
shouldFireEvent = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldFireEvent)
|
||||||
|
ConnectionChange?.Invoke(this, new GenericSocketStatusChageEventArgs(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ushort representation of client status
|
||||||
|
/// </summary>
|
||||||
|
public ushort UStatus
|
||||||
|
{
|
||||||
|
get { return (ushort)ClientStatus; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the AutoReconnect
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoReconnect { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S+ helper for AutoReconnect
|
||||||
|
/// </summary>
|
||||||
|
public ushort UAutoReconnect
|
||||||
|
{
|
||||||
|
get { return (ushort)(AutoReconnect ? 1 : 0); }
|
||||||
|
set { AutoReconnect = value == 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
|
||||||
|
/// </summary>
|
||||||
|
public int AutoReconnectIntervalMs { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor
|
||||||
|
/// </summary>
|
||||||
|
public GenericUdpClient(string key, string address, int port, int bufferSize)
|
||||||
|
: base(key)
|
||||||
|
{
|
||||||
|
StreamDebugging = new CommunicationStreamDebugging(key);
|
||||||
|
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
|
||||||
|
AutoReconnectIntervalMs = 5000;
|
||||||
|
Hostname = address;
|
||||||
|
Port = port;
|
||||||
|
BufferSize = bufferSize;
|
||||||
|
|
||||||
|
reconnectTimer = new Timer(o =>
|
||||||
|
{
|
||||||
|
if (connectEnabled)
|
||||||
|
Connect();
|
||||||
|
}, null, ThreadingTimeout.Infinite, ThreadingTimeout.Infinite);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor for S+
|
||||||
|
/// </summary>
|
||||||
|
public GenericUdpClient()
|
||||||
|
: base(SplusKey)
|
||||||
|
{
|
||||||
|
StreamDebugging = new CommunicationStreamDebugging(SplusKey);
|
||||||
|
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
|
||||||
|
AutoReconnectIntervalMs = 5000;
|
||||||
|
BufferSize = 2000;
|
||||||
|
|
||||||
|
reconnectTimer = new Timer(o =>
|
||||||
|
{
|
||||||
|
if (connectEnabled)
|
||||||
|
Connect();
|
||||||
|
}, null, ThreadingTimeout.Infinite, ThreadingTimeout.Infinite);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize method
|
||||||
|
/// </summary>
|
||||||
|
public void Initialize(string key)
|
||||||
|
{
|
||||||
|
Key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
|
||||||
|
{
|
||||||
|
if (programEventType == eProgramStatusEventType.Stopping)
|
||||||
|
{
|
||||||
|
Debug.Console(1, this, "Program stopping. Closing connection");
|
||||||
|
Deactivate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deactivate method
|
||||||
|
/// </summary>
|
||||||
|
public override bool Deactivate()
|
||||||
|
{
|
||||||
|
Disconnect();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connect method
|
||||||
|
/// </summary>
|
||||||
|
public void Connect()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(Hostname))
|
||||||
|
{
|
||||||
|
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpClient '{0}': No address set", Key);
|
||||||
|
ClientStatus = SocketStatus.SOCKET_STATUS_NO_CONNECT;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Port < 1 || Port > 65535)
|
||||||
|
{
|
||||||
|
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpClient '{0}': Invalid port", Key);
|
||||||
|
ClientStatus = SocketStatus.SOCKET_STATUS_NO_CONNECT;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostname = Hostname;
|
||||||
|
var port = Port;
|
||||||
|
var bufferSize = BufferSize;
|
||||||
|
UdpClient newClient = null;
|
||||||
|
CancellationTokenSource newReceiveCancellationTokenSource = null;
|
||||||
|
CancellationToken startReceiveToken = default(CancellationToken);
|
||||||
|
var shouldStartReceive = false;
|
||||||
|
|
||||||
|
lock (stateLock)
|
||||||
|
{
|
||||||
|
connectEnabled = true;
|
||||||
|
|
||||||
|
if (client != null)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
newReceiveCancellationTokenSource = new CancellationTokenSource();
|
||||||
|
newClient = new UdpClient();
|
||||||
|
newClient.Client.ReceiveBufferSize = bufferSize;
|
||||||
|
newClient.Client.SendBufferSize = bufferSize;
|
||||||
|
newClient.Connect(hostname, port);
|
||||||
|
|
||||||
|
lock (stateLock)
|
||||||
|
{
|
||||||
|
if (!connectEnabled || client != null)
|
||||||
|
{
|
||||||
|
newClient.Close();
|
||||||
|
newReceiveCancellationTokenSource.Cancel();
|
||||||
|
newReceiveCancellationTokenSource.Dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
receiveCancellationTokenSource = newReceiveCancellationTokenSource;
|
||||||
|
client = newClient;
|
||||||
|
ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED;
|
||||||
|
reconnectTimer.Change(ThreadingTimeout.Infinite, ThreadingTimeout.Infinite);
|
||||||
|
startReceiveToken = receiveCancellationTokenSource.Token;
|
||||||
|
shouldStartReceive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldStartReceive)
|
||||||
|
StartReceive(startReceiveToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogMessage(ex, "Error connecting UDP client {0}", this, Key);
|
||||||
|
|
||||||
|
if (newClient != null)
|
||||||
|
newClient.Close();
|
||||||
|
|
||||||
|
if (newReceiveCancellationTokenSource != null)
|
||||||
|
{
|
||||||
|
newReceiveCancellationTokenSource.Cancel();
|
||||||
|
newReceiveCancellationTokenSource.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (stateLock)
|
||||||
|
{
|
||||||
|
if (connectEnabled && client == null)
|
||||||
|
{
|
||||||
|
ClientStatus = SocketStatus.SOCKET_STATUS_NO_CONNECT;
|
||||||
|
StartReconnectTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disconnect method
|
||||||
|
/// </summary>
|
||||||
|
public void Disconnect()
|
||||||
|
{
|
||||||
|
lock (stateLock)
|
||||||
|
{
|
||||||
|
connectEnabled = false;
|
||||||
|
reconnectTimer.Change(ThreadingTimeout.Infinite, ThreadingTimeout.Infinite);
|
||||||
|
CleanupClient();
|
||||||
|
ClientStatus = SocketStatus.SOCKET_STATUS_NO_CONNECT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SendText method
|
||||||
|
/// </summary>
|
||||||
|
public void SendText(string text)
|
||||||
|
{
|
||||||
|
this.PrintSentText(text);
|
||||||
|
|
||||||
|
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
|
||||||
|
SendBytes(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SendBytes method
|
||||||
|
/// </summary>
|
||||||
|
public void SendBytes(byte[] bytes)
|
||||||
|
{
|
||||||
|
if (bytes == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.PrintSentBytes(bytes);
|
||||||
|
|
||||||
|
if (!IsConnected || client == null)
|
||||||
|
Connect();
|
||||||
|
|
||||||
|
var udpClient = client;
|
||||||
|
if (!IsConnected || udpClient == null)
|
||||||
|
{
|
||||||
|
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpClient '{0}': Cannot send bytes because the client is not connected", Key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
udpClient.Send(bytes, bytes.Length);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogMessage(ex, "Error sending UDP bytes for {0}", this, Key);
|
||||||
|
HandleDisconnected();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartReceive(CancellationToken token)
|
||||||
|
{
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var udpClient = client;
|
||||||
|
if (udpClient == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var result = await udpClient.ReceiveAsync().ConfigureAwait(false);
|
||||||
|
var bytes = result.Buffer;
|
||||||
|
if (bytes == null || bytes.Length == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
connectionRefusedLogged = false;
|
||||||
|
|
||||||
|
var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
|
||||||
|
|
||||||
|
this.PrintReceivedBytes(bytes);
|
||||||
|
this.PrintReceivedText(text);
|
||||||
|
|
||||||
|
BytesReceived?.Invoke(this, new GenericCommMethodReceiveBytesArgs(bytes));
|
||||||
|
TextReceived?.Invoke(this, new GenericCommMethodReceiveTextArgs(text));
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (NetSocketException ex)
|
||||||
|
{
|
||||||
|
if (ex.SocketErrorCode == SocketError.ConnectionRefused)
|
||||||
|
{
|
||||||
|
if (!connectionRefusedLogged)
|
||||||
|
{
|
||||||
|
Debug.Console(1, Debug.ErrorLogLevel.Warning,
|
||||||
|
"GenericUdpClient '{0}': Remote endpoint refused UDP traffic or is no longer listening",
|
||||||
|
Key);
|
||||||
|
connectionRefusedLogged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleDisconnected();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.LogMessage(ex, "UDP receive error for {0}", this, Key);
|
||||||
|
|
||||||
|
if (AutoReconnect)
|
||||||
|
{
|
||||||
|
HandleDisconnected();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogMessage(ex, "Unexpected UDP receive error for {0}", this, Key);
|
||||||
|
|
||||||
|
if (AutoReconnect)
|
||||||
|
{
|
||||||
|
HandleDisconnected();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleDisconnected()
|
||||||
|
{
|
||||||
|
lock (stateLock)
|
||||||
|
{
|
||||||
|
CleanupClient();
|
||||||
|
ClientStatus = SocketStatus.SOCKET_STATUS_NO_CONNECT;
|
||||||
|
StartReconnectTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartReconnectTimer()
|
||||||
|
{
|
||||||
|
if (AutoReconnect && connectEnabled)
|
||||||
|
reconnectTimer.Change(AutoReconnectIntervalMs, ThreadingTimeout.Infinite);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanupClient()
|
||||||
|
{
|
||||||
|
if (receiveCancellationTokenSource != null)
|
||||||
|
{
|
||||||
|
receiveCancellationTokenSource.Cancel();
|
||||||
|
receiveCancellationTokenSource.Dispose();
|
||||||
|
receiveCancellationTokenSource = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client != null)
|
||||||
|
{
|
||||||
|
client.Close();
|
||||||
|
client = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -56,6 +56,10 @@ namespace PepperDash.Core
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Udp,
|
Udp,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// UDP client
|
||||||
|
/// </summary>
|
||||||
|
UdpClient,
|
||||||
|
/// <summary>
|
||||||
/// HTTP client
|
/// HTTP client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Http,
|
Http,
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,17 @@ namespace PepperDash.Essentials.Core
|
||||||
comm = udp;
|
comm = udp;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case eControlMethod.UdpClient:
|
||||||
|
{
|
||||||
|
var udpClient = new GenericUdpClient(deviceConfig.Key + "-udpClient", c.Address, c.Port, c.BufferSize)
|
||||||
|
{
|
||||||
|
AutoReconnect = c.AutoReconnect
|
||||||
|
};
|
||||||
|
if (udpClient.AutoReconnect)
|
||||||
|
udpClient.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs;
|
||||||
|
comm = udpClient;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case eControlMethod.Telnet:
|
case eControlMethod.Telnet:
|
||||||
break;
|
break;
|
||||||
case eControlMethod.SecureTcpIp:
|
case eControlMethod.SecureTcpIp:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue