diff --git a/Examples/ExampleScenarios.cs b/Examples/ExampleScenarios.cs index 24dcd95..7d62dff 100644 --- a/Examples/ExampleScenarios.cs +++ b/Examples/ExampleScenarios.cs @@ -10,6 +10,9 @@ using MewtocolNet.Registers; using System.Diagnostics; using System.Text; using Microsoft.Win32; +using MewtocolNet.ComCassette; +using System.Linq; +using System.Net; namespace Examples; @@ -31,61 +34,6 @@ public class ExampleScenarios { } - [Scenario("Permament connection with poller")] - public async Task RunCyclicPollerAsync () { - - Console.WriteLine("Starting poller scenario"); - - int runTime = 10000; - int remainingTime = runTime; - - //setting up a new PLC interface and register collection - MewtocolInterface interf = new MewtocolInterface("192.168.115.210"); - TestRegisters registers = new TestRegisters(); - - //attaching the register collection and an automatic poller - interf.WithRegisterCollection(registers).WithPoller(); - - await interf.ConnectAsync(); - await interf.AwaitFirstDataAsync(); - - _ = Task.Factory.StartNew(async () => { - - while (interf.IsConnected) { - - //flip the bool register each tick and wait for it to be registered - //await interf.SetRegisterAsync(nameof(registers.TestBool1), !registers.TestBool1); - - Console.Title = - $"Speed UP: {interf.BytesPerSecondUpstream} B/s, " + - $"Speed DOWN: {interf.BytesPerSecondDownstream} B/s, " + - $"Poll cycle: {interf.PollerCycleDurationMs} ms, " + - $"Queued MSGs: {interf.QueuedMessages}"; - - Console.Clear(); - Console.WriteLine("Underlying registers on tick: \n"); - - foreach (var register in interf.Registers) - Console.WriteLine($"{register.ToString(true)}"); - - Console.WriteLine($"{registers.TestBool1}"); - Console.WriteLine($"{registers.TestDuplicate}"); - - remainingTime -= 1000; - - Console.WriteLine($"\nStopping in: {remainingTime}ms"); - - await Task.Delay(1000); - - } - - }); - - await Task.Delay(runTime); - interf.Disconnect(); - - } - [Scenario("Dispose and disconnect connection")] public async Task RunDisposalAndDisconnectAsync () { @@ -125,68 +73,7 @@ public class ExampleScenarios { } - [Scenario("Test auto enums and bitwise, needs the example program from MewtocolNet/PLC_Test")] - public async Task RunEnumsBitwiseAsync () { - - Console.WriteLine("Starting auto enums and bitwise"); - - //setting up a new PLC interface and register collection - MewtocolInterface interf = new MewtocolInterface("192.168.115.210"); - TestRegistersEnumBitwise registers = new TestRegistersEnumBitwise(); - - //attaching the register collection and an automatic poller - interf.WithRegisterCollection(registers).WithPoller(); - - registers.PropertyChanged += (s, e) => { - - Console.Clear(); - - var props = registers.GetType().GetProperties(); - - foreach (var prop in props) { - - var val = prop.GetValue(registers); - string printVal = val?.ToString() ?? "null"; - - if (val is BitArray bitarr) { - printVal = bitarr.ToBitString(); - } - - Console.Write($"{prop.Name} - "); - - if(printVal == "True") { - Console.ForegroundColor = ConsoleColor.Green; - } - - Console.Write($"{printVal}"); - - Console.ResetColor(); - - Console.WriteLine(); - - } - - }; - - await interf.ConnectAsync(); - - //use the async method to make sure the cycling is stopped - //await interf.SetRegisterAsync(nameof(registers.StartCyclePLC), false); - - await Task.Delay(5000); - - //set the register without waiting for it async - registers.StartCyclePLC = true; - - await Task.Delay(5000); - - registers.StartCyclePLC = false; - - await Task.Delay(2000); - - } - - [Scenario("Read register test")] + [Scenario("Read all kinds of example registers")] public async Task RunReadTest () { //setting up a new PLC interface and register collection @@ -260,8 +147,8 @@ public class ExampleScenarios { } - [Scenario("Test multi frame")] - public async Task MultiFrameTest() { + [Scenario("Test read speed 100 R registers")] + public async Task ReadRSpeedTest() { var preLogLevel = Logger.LogLevel; Logger.LogLevel = LogLevel.Critical; @@ -273,11 +160,7 @@ public class ExampleScenarios { //auto add all built registers to the interface var builder = RegBuilder.ForInterface(interf); - var r0reg = builder.FromPlcRegName("R0").Build(); - builder.FromPlcRegName("R1").Build(); - builder.FromPlcRegName("DT0").AsBytes(100).Build(); - - for (int i = 1; i < 100; i++) { + for (int i = 0; i < 100; i++) { builder.FromPlcRegName($"R{i}A").Build(); @@ -295,7 +178,7 @@ public class ExampleScenarios { Console.WriteLine("Poller cycle finished"); - Console.WriteLine($"Single frame excec time: {sw.ElapsedMilliseconds:N0}ms for {cmdCount} commands"); + Console.WriteLine($"Single frame excec time: {sw.ElapsedMilliseconds:N0}ms for {cmdCount} commands and {interf.Registers.Count()} R registers"); interf.Disconnect(); @@ -303,4 +186,38 @@ public class ExampleScenarios { } + [Scenario("Find all COM5 cassettes in the network")] + public async Task FindCassettes () { + + Console.Clear(); + + var casettes = await CassetteFinder.FindClientsAsync(); + + foreach (var cassette in casettes) { + + Console.WriteLine($"{cassette.Name}"); + Console.WriteLine($"IP: {cassette.IPAddress}"); + Console.WriteLine($"Port: {cassette.Port}"); + Console.WriteLine($"DHCP: {cassette.UsesDHCP}"); + Console.WriteLine($"Subnet Mask: {cassette.SubnetMask}"); + Console.WriteLine($"Gateway: {cassette.GatewayAddress}"); + Console.WriteLine($"Mac: {cassette.MacAddress.ToHexString(":")}"); + Console.WriteLine($"Firmware: {cassette.FirmwareVersion}"); + Console.WriteLine($"Status: {cassette.Status}"); + Console.WriteLine($"Endpoint: {cassette.EndpointName} - {cassette.Endpoint.Address}"); + Console.WriteLine(); + + } + + await Task.Delay(5000); + + var found = casettes.FirstOrDefault(x => x.Endpoint.Address.ToString() == "10.237.191.75"); + + found.IPAddress = IPAddress.Parse($"192.168.1.{new Random().Next(20, 120)}"); + found.Name = $"Rand{new Random().Next(5, 15)}"; + + await found.SendNewConfigAsync(); + + } + } diff --git a/Examples/Program.cs b/Examples/Program.cs index 3b2a56e..dc1b27c 100644 --- a/Examples/Program.cs +++ b/Examples/Program.cs @@ -7,6 +7,8 @@ using System.Reflection; using System.Threading.Tasks; using MewtocolNet.Logging; using System.Text.RegularExpressions; +using System.Globalization; +using System.Threading; namespace Examples; @@ -16,13 +18,21 @@ class Program { static void Main(string[] args) { + Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-us"); + + Console.Clear(); + AppDomain.CurrentDomain.UnhandledException += (s,e) => { - Console.WriteLine(e.ExceptionObject.ToString()); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Uncatched exception: {e.ExceptionObject.ToString()}"); + Console.ResetColor(); }; - TaskScheduler.UnobservedTaskException += (s,e) => { - Console.WriteLine(e.Exception.ToString()); - }; + //TaskScheduler.UnobservedTaskException += (s,e) => { + // Console.ForegroundColor = ConsoleColor.Magenta; + // Console.WriteLine($"Unobserved Task Uncatched exception: {e.Exception.ToString()}"); + // Console.ResetColor(); + //}; ExampleSzenarios.SetupLogger(); diff --git a/MewtocolNet/ComCassette/CassetteFinder.cs b/MewtocolNet/ComCassette/CassetteFinder.cs new file mode 100644 index 0000000..346b212 --- /dev/null +++ b/MewtocolNet/ComCassette/CassetteFinder.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MewtocolNet.ComCassette { + + /// + /// Provides a interface to modify and find PLC network cassettes also known as COM5 + /// + public class CassetteFinder { + + public static async Task> FindClientsAsync (string ipSource = null, int timeoutMs = 100) { + + var from = new IPEndPoint(IPAddress.Any, 0); + + List cassettesFound = new List(); + List>> interfacesTasks = new List>>(); + + var usableInterfaces = GetUseableNetInterfaces(); + + if (ipSource == null) { + + var interfaces = NetworkInterface.GetAllNetworkInterfaces(); + + foreach (NetworkInterface netInterface in usableInterfaces) { + + IPInterfaceProperties ipProps = netInterface.GetIPProperties(); + var unicastInfo = ipProps.UnicastAddresses + .FirstOrDefault(x => x.Address.AddressFamily == AddressFamily.InterNetwork); + + var ep = new IPEndPoint(unicastInfo.Address, 0); + interfacesTasks.Add(FindClientsForEndpoint(ep, timeoutMs, netInterface.Name)); + + } + + } else { + + from = new IPEndPoint(IPAddress.Parse(ipSource), 0); + + var netInterface = usableInterfaces.FirstOrDefault(x => x.GetIPProperties().UnicastAddresses.Any(y => y.Address.ToString() == ipSource)); + + if (netInterface == null) + throw new NotSupportedException($"The host endpoint {ipSource}, is not available"); + + interfacesTasks.Add(FindClientsForEndpoint(from, timeoutMs, netInterface.Name)); + + } + + //run the interface querys + var grouped = await Task.WhenAll(interfacesTasks); + + foreach (var item in grouped) + cassettesFound.AddRange(item); + + return cassettesFound; + + } + + private static IEnumerable GetUseableNetInterfaces () { + + foreach (NetworkInterface netInterface in NetworkInterface.GetAllNetworkInterfaces()) { + + bool isEthernet = + netInterface.NetworkInterfaceType == NetworkInterfaceType.Ethernet || + netInterface.NetworkInterfaceType == NetworkInterfaceType.Ethernet3Megabit || + netInterface.NetworkInterfaceType == NetworkInterfaceType.FastEthernetFx || + netInterface.NetworkInterfaceType == NetworkInterfaceType.FastEthernetT || + netInterface.NetworkInterfaceType == NetworkInterfaceType.GigabitEthernet; + + bool isWlan = netInterface.NetworkInterfaceType == NetworkInterfaceType.Wireless80211; + + bool isUsable = netInterface.OperationalStatus == OperationalStatus.Up; + + if (!isUsable) continue; + if (!(isWlan || isEthernet)) continue; + + IPInterfaceProperties ipProps = netInterface.GetIPProperties(); + var hasUnicastInfo = ipProps.UnicastAddresses + .Any(x => x.Address.AddressFamily == AddressFamily.InterNetwork); + + if (!hasUnicastInfo) continue; + + yield return netInterface; + + } + + } + + private static async Task> FindClientsForEndpoint (IPEndPoint from, int timeoutMs, string ipEndpointName) { + + var cassettesFound = new List(); + + int plcPort = 9090; + + // Byte msg to request the status transmission of all plcs + byte[] requestCode = new byte[] { 0x88, 0x40, 0x00 }; + + // The start code of the status transmission response + byte[] startCode = new byte[] { 0x88, 0xC0, 0x00 }; + + using(var udpClient = new UdpClient()) { + + udpClient.EnableBroadcast = true; + + udpClient.Client.Bind(from); + + //broadcast packet to all devices (plc specific package) + udpClient.Send(requestCode, requestCode.Length, "255.255.255.255", plcPort); + + //canceling after no new data was read + CancellationTokenSource tSource = new CancellationTokenSource(); + var tm = new System.Timers.Timer(timeoutMs); + tm.Elapsed += (s, e) => { + tSource.Cancel(); + tm.Stop(); + }; + tm.Start(); + + //wait for devices to send response + try { + + byte[] recvBuffer = null; + + while (!tSource.Token.IsCancellationRequested) { + + var res = await udpClient.ReceiveAsync().WithCancellation(tSource.Token); + + if (res.Buffer == null) break; + + recvBuffer = res.Buffer; + + if (recvBuffer.SearchBytePattern(startCode) == 0) { + + tm.Stop(); + tm.Start(); + + var parsed = CassetteInformation.FromBytes(recvBuffer, from, ipEndpointName); + if (parsed != null) cassettesFound.Add(parsed); + + } + + } + + } catch (OperationCanceledException) { } catch (SocketException) { } + + } + + return cassettesFound; + + } + + } + +} diff --git a/MewtocolNet/ComCassette/CassetteInformation.cs b/MewtocolNet/ComCassette/CassetteInformation.cs new file mode 100644 index 0000000..fd5845f --- /dev/null +++ b/MewtocolNet/ComCassette/CassetteInformation.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +//WARNING! The whole UDP protocol was reverse engineered and is not fully implemented.. + +namespace MewtocolNet.ComCassette { + + /// + /// Information about the COM cassette + /// + public class CassetteInformation { + + /// + /// Indicates if the cassette is currently configurating + /// + public bool IsConfigurating { get; private set; } + + /// + /// Name of the COM cassette + /// + public string Name { get; set; } + + /// + /// Indicates the usage of DHCP + /// + public bool UsesDHCP { get; set; } + + /// + /// IP Address of the COM cassette + /// + public IPAddress IPAddress { get; set; } + + /// + /// Subnet mask of the cassette + /// + public IPAddress SubnetMask { get; set; } + + /// + /// Default gateway of the cassette + /// + public IPAddress GatewayAddress { get; set; } + + /// + /// Mac address of the cassette + /// + public byte[] MacAddress { get; private set; } + + /// + /// The source endpoint the cassette is reachable from + /// + public IPEndPoint Endpoint { get; private set; } + + /// + /// The name of the endpoint the device is reachable from, or null if not specifically defined + /// + public string EndpointName { get; private set; } + + /// + /// Firmware version as string + /// + public string FirmwareVersion { get; private set; } + + /// + /// The tcp port of the cassette + /// + public int Port { get; private set; } + + /// + /// Status of the cassette + /// + public CassetteStatus Status { get; private set; } + + internal static CassetteInformation FromBytes(byte[] bytes, IPEndPoint endpoint, string endpointName) { + + // Receive data package explained: + // 0 3 4 8 12 17 22 24 27 29 31 32 + // 88 C0 00 | 00 | C0 A8 73 D4 | FF FF FF 00 | C0 A8 73 3C | 00 | C0 8F 60 53 1C | 01 10 | 23 86 | 00 | 25 | 00 | 00 | 00 | 0D | (byte) * (n) NAME LEN + // Header |DHCP| IPv4 addr. | Subnet Mask | IPv4 Gatwy | | Mac Addr. | Ver. | Port | | | |STAT| | Name LEN | Name + // 1 or 0 Procuct Type? StatusCode Length of Name + + //get ips / mac + var dhcpOn = bytes.Skip(3).First() != 0x00; + var ipAdd = new IPAddress(bytes.Skip(4).Take(4).ToArray()); + var subnetMask = new IPAddress(bytes.Skip(8).Take(4).ToArray()); + var gateWaysAdd = new IPAddress(bytes.Skip(12).Take(4).ToArray()); + var macAdd = bytes.Skip(17).Take(5).ToArray(); + var firmwareV = string.Join(".", bytes.Skip(22).Take(2).Select(x => x.ToString("X1")).ToArray()); + var port = BitConverter.ToUInt16(bytes.Skip(24).Take(2).Reverse().ToArray(), 0); + var status = (CassetteStatus)bytes.Skip(29).First(); + + //missing blocks, later + + //get name + var name = Encoding.ASCII.GetString(bytes.Skip(32).ToArray()); + + return new CassetteInformation { + + Name = name, + UsesDHCP = dhcpOn, + IPAddress = ipAdd, + SubnetMask = subnetMask, + GatewayAddress = gateWaysAdd, + MacAddress = macAdd, + Endpoint = endpoint, + EndpointName = endpointName, + FirmwareVersion = firmwareV, + Port = port, + Status = status, + + }; + + } + + public async Task SendNewConfigAsync () { + + if (IsConfigurating) return; + + // this command gets sent to a specific plc ip address to overwrite the cassette config + // If dhcp is set to 1 the ip is ignored but still must be valid + + // 88 41 00 | 00 | C0 8F 61 07 1B | 05 | 54 65 73 74 31 | 05 | 46 50 58 45 54 | 00 | C0 A8 01 07 | FF FF FF 00 | C0 A8 73 3C + // Header | | | 5 | T e s t 1 | 05 | F P X E T |0||1| 192.168.1.7 | 255.255... | 192.168.115.60 + // Header | | Mac Address |LEN>| ASCII Name |LEN>| Static |DHCP| Target IP | Subnet Mask | Gateway + + IsConfigurating = true; + + List sendBytes = new List(); + + //add cmd header + sendBytes.AddRange(new byte[] { 0x88, 0x41, 0x00, 0x00 }); + + //add mac + sendBytes.AddRange(MacAddress); + + //add name length + sendBytes.Add((byte)Name.Length); + + //add name + sendBytes.AddRange(Encoding.ASCII.GetBytes(Name)); + + //FPXET + var subname = Encoding.ASCII.GetBytes("TESTFP"); + + //add sub name length + sendBytes.Add((byte)subname.Length); + + //add subname + sendBytes.AddRange(subname); + + //add dhcp 0 | 1 + sendBytes.Add((byte)(UsesDHCP ? 0x01 : 0x00)); + + //add ip address + sendBytes.AddRange(IPAddress.GetAddressBytes()); + + //add subnet mask ip address + sendBytes.AddRange(SubnetMask.GetAddressBytes()); + + //add gateway ip + sendBytes.AddRange(GatewayAddress.GetAddressBytes()); + + var sendBytesArr = sendBytes.ToArray(); + + using(var udpClient = new UdpClient()) { + + udpClient.Client.Bind(Endpoint); + + //broadcast packet to all devices (plc specific package) + await udpClient.SendAsync(sendBytesArr, sendBytesArr.Length, "255.255.255.255", 9090); + + } + + } + + } + +} diff --git a/MewtocolNet/ComCassette/CassetteStatus.cs b/MewtocolNet/ComCassette/CassetteStatus.cs new file mode 100644 index 0000000..4f84666 --- /dev/null +++ b/MewtocolNet/ComCassette/CassetteStatus.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MewtocolNet.ComCassette { + + /// + /// Needs a list of all status codes.. hard to reverse engineer + /// + public enum CassetteStatus { + + /// + /// Cassette is running as intended + /// + Normal = 0, + /// + /// Cassette DHCP resolution error + /// + DHCPError = 2, + + } + +} diff --git a/MewtocolNet/Extensions/AsyncExtensions.cs b/MewtocolNet/Extensions/AsyncExtensions.cs new file mode 100644 index 0000000..463ad8a --- /dev/null +++ b/MewtocolNet/Extensions/AsyncExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.Threading; +using System.Net.Sockets; + +namespace MewtocolNet { + + internal static class AsyncExtensions { + + public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) { + + var tcs = new TaskCompletionSource(); + using (cancellationToken.Register(s => ((TaskCompletionSource)s).TrySetResult(true), tcs)) { + if (task != await Task.WhenAny(task, tcs.Task)) { + throw new OperationCanceledException(cancellationToken); + } + } + + return task.Result; + + } + + } + +} diff --git a/MewtocolNet/MewtocolHelpers.cs b/MewtocolNet/MewtocolHelpers.cs index 99be553..1b4f071 100644 --- a/MewtocolNet/MewtocolHelpers.cs +++ b/MewtocolNet/MewtocolHelpers.cs @@ -59,6 +59,29 @@ namespace MewtocolNet { #region Byte and string operation helpers + /// + /// Searches a byte array for a pattern + /// + /// + /// + /// The start index of the found pattern or -1 + public static int SearchBytePattern(this byte[] src, byte[] pattern) { + + int maxFirstCharSlot = src.Length - pattern.Length + 1; + for (int i = 0; i < maxFirstCharSlot; i++) { + if (src[i] != pattern[0]) // compare only first byte + continue; + + // found a match on first byte, now try to match rest of the pattern + for (int j = pattern.Length - 1; j >= 1; j--) { + if (src[i + j] != pattern[j]) break; + if (j == 1) return i; + } + } + return -1; + + } + /// /// Converts a string (after converting to upper case) to ascii bytes /// @@ -204,7 +227,7 @@ namespace MewtocolNet { /// /// Seperator between the hex numbers /// The byte array - internal static string ToHexString (this byte[] arr, string seperator = "") { + public static string ToHexString (this byte[] arr, string seperator = "") { StringBuilder sb = new StringBuilder(); diff --git a/MewtocolNet/MewtocolInterface.cs b/MewtocolNet/MewtocolInterface.cs index cb48916..793720c 100644 --- a/MewtocolNet/MewtocolInterface.cs +++ b/MewtocolNet/MewtocolInterface.cs @@ -466,7 +466,7 @@ namespace MewtocolNet { await stream.ReadAsync(responseBuffer, 0, responseBuffer.Length); bool terminatorReceived = responseBuffer.Any(x => x == (byte)CR); - var delimiterTerminatorIdx = SearchBytePattern(responseBuffer, new byte[] { (byte)DELIMITER, (byte)CR }); + var delimiterTerminatorIdx = responseBuffer.SearchBytePattern(new byte[] { (byte)DELIMITER, (byte)CR }); if (terminatorReceived && delimiterTerminatorIdx == -1) { cmdState = CommandState.Complete; @@ -557,23 +557,6 @@ namespace MewtocolNet { } - private int SearchBytePattern (byte[] src, byte[] pattern) { - - int maxFirstCharSlot = src.Length - pattern.Length + 1; - for (int i = 0; i < maxFirstCharSlot; i++) { - if (src[i] != pattern[0]) // compare only first byte - continue; - - // found a match on first byte, now try to match rest of the pattern - for (int j = pattern.Length - 1; j >= 1; j--) { - if (src[i + j] != pattern[j]) break; - if (j == 1) return i; - } - } - return -1; - - } - #endregion #region Disposing