Added COM cassette features

This commit is contained in:
Felix Weiß
2023-06-29 18:13:28 +02:00
parent 15cc2e245d
commit c332cd9f86
8 changed files with 474 additions and 148 deletions

View File

@@ -10,6 +10,9 @@ using MewtocolNet.Registers;
using System.Diagnostics; using System.Diagnostics;
using System.Text; using System.Text;
using Microsoft.Win32; using Microsoft.Win32;
using MewtocolNet.ComCassette;
using System.Linq;
using System.Net;
namespace Examples; 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")] [Scenario("Dispose and disconnect connection")]
public async Task RunDisposalAndDisconnectAsync () { 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")] [Scenario("Read all kinds of example registers")]
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")]
public async Task RunReadTest () { public async Task RunReadTest () {
//setting up a new PLC interface and register collection //setting up a new PLC interface and register collection
@@ -260,8 +147,8 @@ public class ExampleScenarios {
} }
[Scenario("Test multi frame")] [Scenario("Test read speed 100 R registers")]
public async Task MultiFrameTest() { public async Task ReadRSpeedTest() {
var preLogLevel = Logger.LogLevel; var preLogLevel = Logger.LogLevel;
Logger.LogLevel = LogLevel.Critical; Logger.LogLevel = LogLevel.Critical;
@@ -273,11 +160,7 @@ public class ExampleScenarios {
//auto add all built registers to the interface //auto add all built registers to the interface
var builder = RegBuilder.ForInterface(interf); var builder = RegBuilder.ForInterface(interf);
var r0reg = builder.FromPlcRegName("R0").Build(); for (int i = 0; i < 100; i++) {
builder.FromPlcRegName("R1").Build();
builder.FromPlcRegName("DT0").AsBytes(100).Build();
for (int i = 1; i < 100; i++) {
builder.FromPlcRegName($"R{i}A").Build(); builder.FromPlcRegName($"R{i}A").Build();
@@ -295,7 +178,7 @@ public class ExampleScenarios {
Console.WriteLine("Poller cycle finished"); 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(); 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();
}
} }

View File

@@ -7,6 +7,8 @@ using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using MewtocolNet.Logging; using MewtocolNet.Logging;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Globalization;
using System.Threading;
namespace Examples; namespace Examples;
@@ -16,13 +18,21 @@ class Program {
static void Main(string[] args) { static void Main(string[] args) {
Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-us");
Console.Clear();
AppDomain.CurrentDomain.UnhandledException += (s,e) => { 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) => { //TaskScheduler.UnobservedTaskException += (s,e) => {
Console.WriteLine(e.Exception.ToString()); // Console.ForegroundColor = ConsoleColor.Magenta;
}; // Console.WriteLine($"Unobserved Task Uncatched exception: {e.Exception.ToString()}");
// Console.ResetColor();
//};
ExampleSzenarios.SetupLogger(); ExampleSzenarios.SetupLogger();

View File

@@ -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 {
/// <summary>
/// Provides a interface to modify and find PLC network cassettes also known as COM5
/// </summary>
public class CassetteFinder {
public static async Task<IEnumerable<CassetteInformation>> FindClientsAsync (string ipSource = null, int timeoutMs = 100) {
var from = new IPEndPoint(IPAddress.Any, 0);
List<CassetteInformation> cassettesFound = new List<CassetteInformation>();
List<Task<List<CassetteInformation>>> interfacesTasks = new List<Task<List<CassetteInformation>>>();
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<NetworkInterface> 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<List<CassetteInformation>> FindClientsForEndpoint (IPEndPoint from, int timeoutMs, string ipEndpointName) {
var cassettesFound = new List<CassetteInformation>();
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;
}
}
}

View File

@@ -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 {
/// <summary>
/// Information about the COM cassette
/// </summary>
public class CassetteInformation {
/// <summary>
/// Indicates if the cassette is currently configurating
/// </summary>
public bool IsConfigurating { get; private set; }
/// <summary>
/// Name of the COM cassette
/// </summary>
public string Name { get; set; }
/// <summary>
/// Indicates the usage of DHCP
/// </summary>
public bool UsesDHCP { get; set; }
/// <summary>
/// IP Address of the COM cassette
/// </summary>
public IPAddress IPAddress { get; set; }
/// <summary>
/// Subnet mask of the cassette
/// </summary>
public IPAddress SubnetMask { get; set; }
/// <summary>
/// Default gateway of the cassette
/// </summary>
public IPAddress GatewayAddress { get; set; }
/// <summary>
/// Mac address of the cassette
/// </summary>
public byte[] MacAddress { get; private set; }
/// <summary>
/// The source endpoint the cassette is reachable from
/// </summary>
public IPEndPoint Endpoint { get; private set; }
/// <summary>
/// The name of the endpoint the device is reachable from, or null if not specifically defined
/// </summary>
public string EndpointName { get; private set; }
/// <summary>
/// Firmware version as string
/// </summary>
public string FirmwareVersion { get; private set; }
/// <summary>
/// The tcp port of the cassette
/// </summary>
public int Port { get; private set; }
/// <summary>
/// Status of the cassette
/// </summary>
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<byte> sendBytes = new List<byte>();
//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);
}
}
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace MewtocolNet.ComCassette {
/// <summary>
/// Needs a list of all status codes.. hard to reverse engineer
/// </summary>
public enum CassetteStatus {
/// <summary>
/// Cassette is running as intended
/// </summary>
Normal = 0,
/// <summary>
/// Cassette DHCP resolution error
/// </summary>
DHCPError = 2,
}
}

View File

@@ -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<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken) {
var tcs = new TaskCompletionSource<bool>();
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs)) {
if (task != await Task.WhenAny(task, tcs.Task)) {
throw new OperationCanceledException(cancellationToken);
}
}
return task.Result;
}
}
}

View File

@@ -59,6 +59,29 @@ namespace MewtocolNet {
#region Byte and string operation helpers #region Byte and string operation helpers
/// <summary>
/// Searches a byte array for a pattern
/// </summary>
/// <param name="src"></param>
/// <param name="pattern"></param>
/// <returns>The start index of the found pattern or -1</returns>
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;
}
/// <summary> /// <summary>
/// Converts a string (after converting to upper case) to ascii bytes /// Converts a string (after converting to upper case) to ascii bytes
/// </summary> /// </summary>
@@ -204,7 +227,7 @@ namespace MewtocolNet {
/// </summary> /// </summary>
/// <param name="seperator">Seperator between the hex numbers</param> /// <param name="seperator">Seperator between the hex numbers</param>
/// <param name="arr">The byte array</param> /// <param name="arr">The byte array</param>
internal static string ToHexString (this byte[] arr, string seperator = "") { public static string ToHexString (this byte[] arr, string seperator = "") {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();

View File

@@ -466,7 +466,7 @@ namespace MewtocolNet {
await stream.ReadAsync(responseBuffer, 0, responseBuffer.Length); await stream.ReadAsync(responseBuffer, 0, responseBuffer.Length);
bool terminatorReceived = responseBuffer.Any(x => x == (byte)CR); 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) { if (terminatorReceived && delimiterTerminatorIdx == -1) {
cmdState = CommandState.Complete; 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 #endregion
#region Disposing #region Disposing