mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-07-02 10:38:16 +00:00
388 lines
No EOL
12 KiB
C#
388 lines
No EOL
12 KiB
C#
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 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;
|
|
}
|
|
|
|
lock (stateLock)
|
|
{
|
|
connectEnabled = true;
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
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)
|
|
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;
|
|
|
|
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)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
} |