using System; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using DTS.Common.Enums.DASFactory; using DTS.Common.Utilities.Logging; using DTS.Common.Utils; namespace DTS.DASLib.Command.SLICE.MulticastCommands { public abstract class MulticastCommandBase : CommandBase { protected const int IP_ADDR_SIZE = 16; protected const int MAC_ADDR_SIZE = 18; protected const int DOUBLE_MAC_ADDR_SIZE = 2 * MAC_ADDR_SIZE; protected const int HOST_MAC_ADDRESS_OFFSET = 0; protected const int CLIENT_MAC_ADDRESS_OFFSET = MAC_ADDR_SIZE; protected const int FIRST_PARAMETER_OFFSET = CLIENT_MAC_ADDRESS_OFFSET + MAC_ADDR_SIZE; protected const int DEFAULT_COMMAND_PAYLOAD_SIZE = DOUBLE_MAC_ADDR_SIZE; protected const int DEFAULT_RESPONSE_PAYLOAD_SIZE = DOUBLE_MAC_ADDR_SIZE; protected enum Commands { Reserved = 0x00, AutoDiscover = 0x01, SetIpAddress = 0x02, GetIpAddress = 0x03, SetSubnetAddress = 0x04, GetSubnetAddress = 0x05, SetGatewayAddress = 0x06, GetGatewayAddress = 0x07, SetDnsAddress = 0x08, GetDnsAddress = 0x09, SetDhcp = 0x0A, GetDhcp = 0x0B, ResetMcu = 0x0C, WakeupBootloader = 0x0D, Identify = 0x0E, GetMACTable = 0x0F, GetMACTable_Reserved = 0x10, // created in FW but not being used. Just ignore this command for now. UdpQueryQTAS = 0x11, // query Arm-Trigger status via UDP multicast or unicast message via discovery route. UdpAutoQATS = 0x12, // message command ID for auto-arm self-start UDP multicast QATS UdpQATSInit = 0x13, // host command to send to DAS to start/stop UdpAutoQats message. UdpAutoTiltSensorData = 0x14, //Periodic UDP response when enabled by cmd ID 0x13 UdpPrepareToShutdown = 0x15, //Command UUT to preserve auto-arm configuration and go to idle state ready for power down. } public enum Ports { Command = 8501, Response = 8503, } public enum DeviceClasses { Unknown = 0, Slice6 = 1 << 0, SDB = 1 << 1, ECM = 1 << 2, S6DB = 1 << 3, Slice6Air = 1 << 4, PowerPro = 1 << 5, TsrAir = 1 << 6, S6DB3 = 1 << 7, Any = 0xFFFF, } public enum ResponseOptions { AlwaysRespond = 0, RespondIfNotBusy = 1, RespondIfNotConnected = 2 } public static int DEFAULT_RECEIVE_TIMEOUT_MS = 3000; public const string DEFAULT_MULTICAST_ADDRESS = "239.1.2.3"; public const string DEFAULT_RECEIVE_ADDRESS = "239.4.5.6"; public static string MulticastAddress = DEFAULT_MULTICAST_ADDRESS; public static string MulticastReceiveAddress = DEFAULT_RECEIVE_ADDRESS; public static int CommandPort = (int)Ports.Command; public static int ResponsePort = (int)Ports.Response; protected abstract Commands Command { get; } protected MulticastCommandBase(DTS.Common.Interface.DASFactory.ICommunication sock) : base(sock) { command.Type = CommandPacket.CommandType.Multicast; command.SetCommand((byte)Command, Command.ToString()); command.Parameter = new byte[DEFAULT_COMMAND_PAYLOAD_SIZE]; } protected MulticastCommandBase(DTS.Common.Interface.DASFactory.ICommunication sock, int timeoutMillisec) : base(sock, timeoutMillisec) { command.Type = CommandPacket.CommandType.Multicast; command.SetCommand((byte)Command, Command.ToString()); command.Parameter = new byte[DEFAULT_COMMAND_PAYLOAD_SIZE]; } public void BuildPacket() { baseCommand.ComputeCRCs(); } protected virtual bool StopAfterFirstMessage => true; public int ReceiveTimeoutMs { get; set; } = DEFAULT_RECEIVE_TIMEOUT_MS; private string _commandHostMac; protected string CommandClientMac; protected string ResponseHostMac; protected string ResponseClientMac; public string HostMac { get => _commandHostMac; set { _commandHostMac = value; command.SetParameter(HOST_MAC_ADDRESS_OFFSET, _commandHostMac); } } public string ClientMac { set { CommandClientMac = value; command.SetParameter(CLIENT_MAC_ADDRESS_OFFSET, CommandClientMac); } } protected override CommandReceiveAction WholePackage() { if (response.Status != DFConstantsAndEnums.CommandStatus.StatusNoError) return CommandReceiveAction.StopReceiving; response.GetParameter(HOST_MAC_ADDRESS_OFFSET, out ResponseHostMac); response.GetParameter(CLIENT_MAC_ADDRESS_OFFSET, out ResponseClientMac); if (!CommandClientMac.Equals(ResponseClientMac) || !HostMac.Equals(ResponseHostMac)) { response.Status = DFConstantsAndEnums.CommandStatus.StatusInvalidPacket; } return CommandReceiveAction.StopReceiving; } public IPAddress BindToAdapterIPAddress { get; set; } = IPAddress.Any; private void SendRequest() { var txGroupAddress = IPAddress.Parse(MulticastAddress); var remoteEndPoint = new IPEndPoint(txGroupAddress, CommandPort); UdpClient client = null; if (BindToAdapterIPAddress == IPAddress.Any) { client = new UdpClient(); } else { client = new UdpClient(new IPEndPoint(BindToAdapterIPAddress, CommandPort)); } client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.IpTimeToLive, 4); command.SequenceNumber = 0; command.ComputeCRCs(); var data = command.ToBytes(); client.Send(data, data.Length, remoteEndPoint); client.Close(); } /// /// alternative to MulticastExecute, /// just sends the multicast request, relies on /// StartListening to have been called to process any incoming data /// public void SendCommand() { SendRequest(); } /// /// used to signal Listen() function that it should terminate /// protected ManualResetEvent _stopListening = new ManualResetEvent(false); /// /// starts a listening thread /// public void StartListening() { CancellationToken ct; Task.Run(() => ReceiveThread(null, true, ct)).ConfigureAwait(false); } /// /// stops any listening thread /// public void StopListening() => _stopListening.Set(); public void MulticastExecute(CancellationToken ct, bool waitForResponse = true) { if (ct.IsCancellationRequested) { return; } //It's easier to kick off a worker thread to handle the receiver using (var signalComplete = new ManualResetEvent(true)) { if (true == waitForResponse) { signalComplete.Reset(); Task.Run(() => ReceiveThread(signalComplete, false, ct)); //Give it time to come up Thread.Sleep(100); } //Send request once receive socket is set up SendRequest(); WaitHandle.WaitAny(new[] { signalComplete, ct.WaitHandle }); } } protected virtual void ProcessResponse(byte[] data) { response = new CommandPacket(data); WholePackage(); } private const int UDP_ASYNC_RESPONSETIME_MS = 1000; /// /// Listens for UDP responses. Can signal when complete and can listen until _stopListening MRE is set. /// /// This MRE is set when the process is complete /// This will cause the listening loop to continue untill stopped by the _stopListening MRE private void ReceiveThread(ManualResetEvent signalComplete, bool listenUntilStopped, CancellationToken ct) { var rxGroupAddress = IPAddress.Parse(MulticastReceiveAddress); var endPoint = new IPEndPoint(BindToAdapterIPAddress, ResponsePort); var receiver = new UdpClient(); receiver.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); receiver.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, ReceiveTimeoutMs); _stopListening.Reset(); if (BindToAdapterIPAddress == IPAddress.Any) { receiver.ExclusiveAddressUse = false; } try { receiver.Client.Bind(endPoint); } catch (Exception ex) { APILogger.Log(ex); if (!ct.IsCancellationRequested) { signalComplete?.Set(); } return; } if (ct.IsCancellationRequested) { return; } try { if (BindToAdapterIPAddress == IPAddress.Any) { receiver.JoinMulticastGroup(rxGroupAddress); } else { receiver.JoinMulticastGroup(rxGroupAddress, BindToAdapterIPAddress); } } catch (Exception ex) { APILogger.Log(ex); } var timeToWait = TimeSpan.FromMilliseconds(UDP_ASYNC_RESPONSETIME_MS); byte[] data = new byte[0]; IAsyncResult asyncResult = null; do { if (BindToAdapterIPAddress != IPAddress.Any && !NetworkUtils.IsNetworkInterfaceUp(BindToAdapterIPAddress)) { Thread.Sleep(100); continue; } asyncResult = receiver.BeginReceive(null, null); asyncResult.AsyncWaitHandle.WaitOne(timeToWait); if (asyncResult.IsCompleted) { try { IPEndPoint remoteEP = null; data = receiver.EndReceive(asyncResult, ref remoteEP); // EndReceive worked and we have received data and remote endpoint System.Diagnostics.Debug.WriteLine("Got " + data.Length + " bytes from " + remoteEP); ProcessResponse(data); } catch (Exception ex) { APILogger.Log(ex); } } } while (!_stopListening.WaitOne(0, false) && (asyncResult == null || asyncResult.IsCompleted) && !ct.IsCancellationRequested); receiver.Close(); if (ct.IsCancellationRequested) { return; } if (null != signalComplete && !signalComplete.WaitOne(0)) { signalComplete.Set(); } } //This is a little hokey. It returns the first interface that's multicast compatible AND 'up'. It may not actually be the interface //that we're interested in. public static string GetMacAddress() { var interfaces = NetworkInterface.GetAllNetworkInterfaces(); foreach (var i in interfaces) { if (OperationalStatus.Up != i.OperationalStatus || !i.SupportsMulticast) continue; var address = BitConverter.ToString(i.GetPhysicalAddress().GetAddressBytes()); address = address.Replace("-", ":"); return address; } return null; } } }