Added serial port support

- complete restructure of codebase
This commit is contained in:
Felix Weiß
2023-06-30 18:39:19 +02:00
parent c332cd9f86
commit 8c2ba1f68f
37 changed files with 1135 additions and 564 deletions

View File

@@ -13,6 +13,7 @@ using Microsoft.Win32;
using MewtocolNet.ComCassette; using MewtocolNet.ComCassette;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.IO.Ports;
namespace Examples; namespace Examples;
@@ -21,7 +22,7 @@ public class ExampleScenarios {
public void SetupLogger () { public void SetupLogger () {
//attaching the logger //attaching the logger
Logger.LogLevel = LogLevel.Error; Logger.LogLevel = LogLevel.Verbose;
Logger.OnNewLogMessage((date, level, msg) => { Logger.OnNewLogMessage((date, level, msg) => {
if (level == LogLevel.Error) Console.ForegroundColor = ConsoleColor.Red; if (level == LogLevel.Error) Console.ForegroundColor = ConsoleColor.Red;
@@ -38,7 +39,7 @@ public class ExampleScenarios {
public async Task RunDisposalAndDisconnectAsync () { public async Task RunDisposalAndDisconnectAsync () {
//automatic disposal //automatic disposal
using (var interf = new MewtocolInterface("192.168.115.210")) { using (var interf = Mewtocol.Ethernet("192.168.115.210")) {
await interf.ConnectAsync(); await interf.ConnectAsync();
@@ -55,7 +56,7 @@ public class ExampleScenarios {
Console.WriteLine("Disposed, closed connection"); Console.WriteLine("Disposed, closed connection");
//manual close //manual close
var interf2 = new MewtocolInterface("192.168.115.210"); var interf2 = Mewtocol.Ethernet("192.168.115.210");
await interf2.ConnectAsync(); await interf2.ConnectAsync();
@@ -77,7 +78,7 @@ public class ExampleScenarios {
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
MewtocolInterface interf = new MewtocolInterface("192.168.115.210").WithPoller(); var interf = Mewtocol.Ethernet("192.168.115.210").WithPoller();
//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);
@@ -147,20 +148,18 @@ public class ExampleScenarios {
} }
[Scenario("Test read speed 100 R registers")] [Scenario("Test read speed TCP (n) R registers")]
public async Task ReadRSpeedTest() { public async Task ReadRSpeedTest (string registerCount) {
var preLogLevel = Logger.LogLevel; var preLogLevel = Logger.LogLevel;
Logger.LogLevel = LogLevel.Critical; Logger.LogLevel = LogLevel.Critical;
//setting up a new PLC interface and register collection //setting up a new PLC interface and register collection
MewtocolInterface interf = new MewtocolInterface("192.168.115.210") { using var interf = Mewtocol.Ethernet("192.168.115.210");
ConnectTimeout = 3000,
};
//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);
for (int i = 0; i < 100; i++) { for (int i = 0; i < int.Parse(registerCount); i++) {
builder.FromPlcRegName($"R{i}A").Build(); builder.FromPlcRegName($"R{i}A").Build();
@@ -169,6 +168,11 @@ public class ExampleScenarios {
//connect //connect
await interf.ConnectAsync(); await interf.ConnectAsync();
if(!interf.IsConnected) {
Console.WriteLine("Aborted, connection failed");
return;
}
Console.WriteLine("Poller cycle started"); Console.WriteLine("Poller cycle started");
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
@@ -180,12 +184,75 @@ public class ExampleScenarios {
Console.WriteLine($"Single frame excec time: {sw.ElapsedMilliseconds:N0}ms for {cmdCount} commands and {interf.Registers.Count()} R registers"); Console.WriteLine($"Single frame excec time: {sw.ElapsedMilliseconds:N0}ms for {cmdCount} commands and {interf.Registers.Count()} R registers");
interf.Disconnect();
await Task.Delay(1000); await Task.Delay(1000);
} }
[Scenario("Test read speed Serial (n) R registers")]
public async Task ReadRSpeedTestSerial (string registerCount) {
var preLogLevel = Logger.LogLevel;
Logger.LogLevel = LogLevel.Critical;
//setting up a new PLC interface and register collection
//MewtocolInterfaceShared interf = Mewtocol.SerialAuto("COM4");
using var interf = Mewtocol.Serial("COM4", BaudRate._115200, DataBits.Eight, Parity.Odd, StopBits.One);
//auto add all built registers to the interface
var builder = RegBuilder.ForInterface(interf);
for (int i = 0; i < int.Parse(registerCount); i++) {
builder.FromPlcRegName($"R{i}A").Build();
}
//connect
await interf.ConnectAsync();
if (!interf.IsConnected) {
Console.WriteLine("Aborted, connection failed");
return;
}
Console.WriteLine("Poller cycle started");
var sw = Stopwatch.StartNew();
int cmdCount = await interf.RunPollerCylceManual();
sw.Stop();
Console.WriteLine("Poller cycle finished");
Console.WriteLine($"Single frame excec time: {sw.ElapsedMilliseconds:N0}ms for {cmdCount} commands and {interf.Registers.Count()} R registers");
}
[Scenario("Test automatic serial port setup")]
public async Task TestAutoSerialSetup () {
var preLogLevel = Logger.LogLevel;
Logger.LogLevel = LogLevel.Critical;
//setting up a new PLC interface and register collection
var interf = Mewtocol.SerialAuto("COM4");
//connect
await interf.ConnectAsync();
if (!interf.IsConnected) {
Console.WriteLine("Aborted, connection failed");
return;
} else {
Console.WriteLine("Serial port settings found");
}
}
[Scenario("Find all COM5 cassettes in the network")] [Scenario("Find all COM5 cassettes in the network")]
public async Task FindCassettes () { public async Task FindCassettes () {
@@ -209,10 +276,10 @@ public class ExampleScenarios {
} }
await Task.Delay(5000);
var found = casettes.FirstOrDefault(x => x.Endpoint.Address.ToString() == "10.237.191.75"); var found = casettes.FirstOrDefault(x => x.Endpoint.Address.ToString() == "10.237.191.75");
if (found == null) return;
found.IPAddress = IPAddress.Parse($"192.168.1.{new Random().Next(20, 120)}"); found.IPAddress = IPAddress.Parse($"192.168.1.{new Random().Next(20, 120)}");
found.Name = $"Rand{new Random().Next(5, 15)}"; found.Name = $"Rand{new Random().Next(5, 15)}";

View File

@@ -54,7 +54,8 @@ class Program {
if(foundAtt != null && foundAtt is ScenarioAttribute att) { if(foundAtt != null && foundAtt is ScenarioAttribute att) {
Console.WriteLine($"[{j + 1}] {method.Name}() - {att.Description}"); string paramsStr = string.Join(" ", method.GetParameters().Select(x => x.Name));
Console.WriteLine($"[{j + 1}] {method.Name}({paramsStr}) - {att.Description}");
invokeableMethods.Add(method); invokeableMethods.Add(method);
j++; j++;
@@ -78,6 +79,8 @@ class Program {
var line = Console.ReadLine(); var line = Console.ReadLine();
var loggerMatch = Regex.Match(line, @"logger (?<level>[a-zA-Z]{0,})"); var loggerMatch = Regex.Match(line, @"logger (?<level>[a-zA-Z]{0,})");
var splitInput = Regex.Split(line, " ");
if (loggerMatch.Success && Enum.TryParse<LogLevel>(loggerMatch.Groups["level"].Value, out var loglevel)) { if (loggerMatch.Success && Enum.TryParse<LogLevel>(loggerMatch.Groups["level"].Value, out var loglevel)) {
@@ -93,13 +96,26 @@ class Program {
Console.Clear(); Console.Clear();
} else if (int.TryParse(line, out var lineNum)) { } else if (int.TryParse(splitInput[0], out var lineNum)) {
var index = Math.Clamp(lineNum - 1, 0, invokeableMethods.Count - 1); var index = Math.Clamp(lineNum - 1, 0, invokeableMethods.Count - 1);
var task = (Task)invokeableMethods.ElementAt(index).Invoke(ExampleSzenarios, null); object[] invParams = null;
task.Wait(); if(splitInput.Length > 1) {
invParams = splitInput.Skip(1).Cast<object>().ToArray();
}
try {
var task = (Task)invokeableMethods.ElementAt(index).Invoke(ExampleSzenarios, invParams);
task.Wait();
} catch (TargetParameterCountException) {
Console.WriteLine("Missing parameters");
}
Console.ForegroundColor = ConsoleColor.Green; Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("The program ran to completition"); Console.WriteLine("The program ran to completition");

View File

@@ -5,7 +5,7 @@ namespace MewtocolNet {
/// <summary> /// <summary>
/// Contains information about the plc and its cpu /// Contains information about the plc and its cpu
/// </summary> /// </summary>
public partial class CpuInfo { public struct CpuInfo {
/// <summary> /// <summary>
/// The cpu type of the plc /// The cpu type of the plc

View File

@@ -18,6 +18,8 @@ namespace MewtocolNet {
} }
} }
if(task.IsCanceled) return default(T);
return task.Result; return task.Result;
} }

View File

@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Text;
using System.Threading.Tasks;
namespace MewtocolNet {
internal static class SerialPortExtensions {
public async static Task WriteAsync (this SerialPort serialPort, byte[] buffer, int offset, int count) {
await serialPort.BaseStream.WriteAsync(buffer, 0, buffer.Length);
}
public async static Task ReadAsync (this SerialPort serialPort, byte[] buffer, int offset, int count) {
var bytesToRead = count;
var temp = new byte[count];
while (bytesToRead > 0) {
var readBytes = await serialPort.BaseStream.ReadAsync(temp, 0, bytesToRead);
Array.Copy(temp, 0, buffer, offset + count - bytesToRead, readBytes);
bytesToRead -= readBytes;
}
}
public async static Task<byte[]> ReadAsync (this SerialPort serialPort, int count) {
var buffer = new byte[count];
await serialPort.ReadAsync(buffer, 0, count);
return buffer;
}
}
}

View File

@@ -1,15 +1,18 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Linq;
namespace MewtocolNet.Queue { namespace MewtocolNet.Queue {
internal class SerialQueue { internal class AsyncQueue {
readonly object _locker = new object(); readonly object _locker = new object();
readonly WeakReference<Task> _lastTask = new WeakReference<Task>(null); readonly WeakReference<Task> _lastTask = new WeakReference<Task>(null);
internal Task<T> Enqueue<T>(Func<Task<T>> asyncFunction) { internal Task<T> Enqueue<T>(Func<Task<T>> asyncFunction) {
lock (_locker) { lock (_locker) {
Task lastTask; Task lastTask;
Task<T> resultTask; Task<T> resultTask;
@@ -22,6 +25,7 @@ namespace MewtocolNet.Queue {
_lastTask.SetTarget(resultTask); _lastTask.SetTarget(resultTask);
return resultTask; return resultTask;
} }
} }

96
MewtocolNet/IPlc.cs Normal file
View File

@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MewtocolNet {
/// <summary>
/// Provides a interface for Panasonic PLCs
/// </summary>
public interface IPlc : IDisposable {
/// <summary>
/// The current connection state of the interface
/// </summary>
bool IsConnected { get; }
/// <summary>
/// The current transmission speed in bytes per second
/// </summary>
int BytesPerSecondUpstream { get; }
/// <summary>
/// The current transmission speed in bytes per second
/// </summary>
int BytesPerSecondDownstream { get; }
/// <summary>
/// Current poller cycle duration
/// </summary>
int PollerCycleDurationMs { get; }
/// <summary>
/// Currently queued message count
/// </summary>
int QueuedMessages { get; }
/// <summary>
/// The registered data registers of the PLC
/// </summary>
IEnumerable<IRegister> Registers { get; }
/// <summary>
/// Generic information about the connected PLC
/// </summary>
PLCInfo PlcInfo { get; }
/// <summary>
/// The station number of the PLC
/// </summary>
int StationNumber { get; }
/// <summary>
/// The initial connection timeout in milliseconds
/// </summary>
int ConnectTimeout { get; set; }
/// <summary>
/// Tries to establish a connection with the device asynchronously
/// </summary>
Task ConnectAsync();
/// <summary>
/// Disconnects the devive from its current connection
/// </summary>
void Disconnect();
/// <summary>
/// Calculates the checksum automatically and sends a command to the PLC then awaits results
/// </summary>
/// <param name="_msg">MEWTOCOL Formatted request string ex: %01#RT</param>
/// <param name="withTerminator">Append the checksum and bcc automatically</param>
/// <param name="timeoutMs">Timout to wait for a response</param>
/// <returns>Returns the result</returns>
Task<MewtocolFrameResponse> SendCommandAsync(string _msg, bool withTerminator = true, int timeoutMs = -1);
/// <summary>
/// Use this to await the first poll iteration after connecting,
/// This also completes if the initial connection fails
/// </summary>
Task AwaitFirstDataAsync();
/// <summary>
/// Runs a single poller cycle manually,
/// useful if you want to use a custom update frequency
/// </summary>
/// <returns>The number of inidvidual mewtocol commands sent</returns>
Task<int> RunPollerCylceManual();
/// <summary>
/// Gets the connection info string
/// </summary>
string GetConnectionInfo();
}
}

View File

@@ -0,0 +1,33 @@
namespace MewtocolNet {
/// <summary>
/// Provides a interface for Panasonic PLCs over a ethernet connection
/// </summary>
public interface IPlcEthernet : IPlc {
/// <summary>
/// The current IP of the PLC connection
/// </summary>
string IpAddress { get; }
/// <summary>
/// The current port of the PLC connection
/// </summary>
int Port { get; }
/// <summary>
/// Attaches a poller to the interface
/// </summary>
public IPlcEthernet WithPoller();
/// <summary>
/// Configures the serial interface
/// </summary>
/// <param name="_ip">IP adress of the PLC</param>
/// <param name="_port">Port of the PLC</param>
/// <param name="_station">Station Number of the PLC</param>
void ConfigureConnection(string _ip, int _port = 9094, int _station = 1);
}
}

57
MewtocolNet/IPlcSerial.cs Normal file
View File

@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Text;
using System.Threading.Tasks;
namespace MewtocolNet {
/// <summary>
/// Provides a interface for Panasonic PLCs over a serial port connection
/// </summary>
public interface IPlcSerial : IPlc {
/// <summary>
/// Port name of the serial port that this device is configured for
/// </summary>
string PortName { get; }
/// <summary>
/// The serial connection baud rate that this device is configured for
/// </summary>
int SerialBaudRate { get; }
/// <summary>
/// The serial connection data bits
/// </summary>
int SerialDataBits { get; }
/// <summary>
/// The serial connection parity
/// </summary>
Parity SerialParity { get; }
/// <summary>
/// The serial connection stop bits
/// </summary>
StopBits SerialStopBits { get; }
/// <summary>
/// Attaches a poller to the interface
/// </summary>
public IPlcSerial WithPoller();
/// <summary>
/// Sets up the connection settings for the device
/// </summary>
/// <param name="_portName">Port name of COM port</param>
/// <param name="_baudRate">The serial connection baud rate</param>
/// <param name="_dataBits">The serial connection data bits</param>
/// <param name="_parity">The serial connection parity</param>
/// <param name="_stopBits">The serial connection stop bits</param>
/// <param name="_station">The station number of the PLC</param>
void ConfigureConnection(string _portName, int _baudRate = 19200, int _dataBits = 8, Parity _parity = Parity.Odd, StopBits _stopBits = StopBits.One, int _station = 1)
}
}

View File

@@ -0,0 +1,12 @@
namespace MewtocolNet {
internal enum CommandState {
Initial,
LineFeed,
RequestedNextFrame,
Complete
}
}

View File

@@ -31,7 +31,7 @@ namespace MewtocolNet.Logging {
if (sender == null) { if (sender == null) {
LogInvoked?.Invoke(DateTime.Now, loglevel, message); LogInvoked?.Invoke(DateTime.Now, loglevel, message);
} else { } else {
LogInvoked?.Invoke(DateTime.Now, loglevel, $"[{sender.GetConnectionPortInfo()}] {message}"); LogInvoked?.Invoke(DateTime.Now, loglevel, $"[{sender.GetConnectionInfo()}] {message}");
} }
} }

62
MewtocolNet/Mewtocol.cs Normal file
View File

@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Text;
namespace MewtocolNet {
/// <summary>
/// Builder helper for mewtocol interfaces
/// </summary>
public static class Mewtocol {
/// <summary>
/// Builds a ethernet based Mewtocol Interface
/// </summary>
/// <param name="_ip"></param>
/// <param name="_port"></param>
/// <param name="_station"></param>
/// <returns></returns>
public static IPlcEthernet Ethernet (string _ip, int _port = 9094, int _station = 1) {
var instance = new MewtocolInterfaceTcp();
instance.ConfigureConnection(_ip, _port, _station);
return instance;
}
/// <summary>
/// Builds a serial port based Mewtocol Interface
/// </summary>
/// <param name="_portName"></param>
/// <param name="_baudRate"></param>
/// <param name="_dataBits"></param>
/// <param name="_parity"></param>
/// <param name="_stopBits"></param>
/// <returns></returns>
public static IPlcSerial Serial (string _portName, BaudRate _baudRate = BaudRate._19200, DataBits _dataBits = DataBits.Eight, Parity _parity = Parity.Odd, StopBits _stopBits = StopBits.One, int _station = 1) {
var instance = new MewtocolInterfaceSerial();
instance.ConfigureConnection(_portName, (int)_baudRate, (int)_dataBits, _parity, _stopBits, _station);
return instance;
}
/// <summary>
/// Builds a serial mewtocol interface that finds the correct settings for the given port name automatically
/// </summary>
/// <param name="_portName"></param>
/// <param name="_station"></param>
/// <returns></returns>
public static IPlcSerial SerialAuto (string _portName, int _station = 1) {
var instance = new MewtocolInterfaceSerial();
instance.ConfigureConnection(_portName, _station);
instance.ConfigureConnectionAuto();
return instance;
}
}
}

View File

@@ -1,18 +1,12 @@
using MewtocolNet.Exceptions; using MewtocolNet.Logging;
using MewtocolNet.Logging;
using MewtocolNet.Queue; using MewtocolNet.Queue;
using MewtocolNet.RegisterAttributes;
using MewtocolNet.Registers; using MewtocolNet.Registers;
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.ComponentModel.Design;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -20,177 +14,119 @@ using System.Threading.Tasks;
namespace MewtocolNet { namespace MewtocolNet {
/// <summary> public partial class MewtocolInterface : IPlc, INotifyPropertyChanged, IDisposable {
/// The PLC com interface class
/// </summary>
public partial class MewtocolInterface : INotifyPropertyChanged, IDisposable {
/// <summary> #region Private fields
/// Gets triggered when the PLC connection was established
/// </summary> private protected Stream stream;
private int tcpMessagesSentThisCycle = 0;
private int pollerCycleDurationMs;
private volatile int queuedMessages;
private bool isConnected;
private PLCInfo plcInfo;
private protected int stationNumber;
private protected int bytesTotalCountedUpstream = 0;
private protected int bytesTotalCountedDownstream = 0;
private protected int cycleTimeMs = 25;
private protected int bytesPerSecondUpstream = 0;
private protected int bytesPerSecondDownstream = 0;
private protected AsyncQueue queue = new AsyncQueue();
private protected int RecBufferSize = 128;
private protected Stopwatch speedStopwatchUpstr;
private protected Stopwatch speedStopwatchDownstr;
private protected Task firstPollTask = new Task(() => { });
#endregion
#region Internal fields
internal event Action PolledCycle;
internal volatile bool pollerTaskStopped = true;
internal volatile bool pollerFirstCycle;
internal bool usePoller = false;
internal List<BaseRegister> RegistersUnderlying { get; private set; } = new List<BaseRegister>();
internal IEnumerable<IRegisterInternal> RegistersInternal => RegistersUnderlying.Cast<IRegisterInternal>();
#endregion
#region Public Read Only Properties / Fields
/// <inheritdoc/>
public event Action<PLCInfo> Connected; public event Action<PLCInfo> Connected;
/// <summary> /// <inheritdoc/>
/// Gets triggered when the PLC connection was closed or lost
/// </summary>
public event Action Disconnected; public event Action Disconnected;
/// <summary> /// <inheritdoc/>
/// Gets triggered when a registered data register changes its value
/// </summary>
public event Action<IRegister> RegisterChanged; public event Action<IRegister> RegisterChanged;
/// <summary> /// <inheritdoc/>
/// Gets triggered when a property of the interface changes
/// </summary>
public event PropertyChangedEventHandler PropertyChanged; public event PropertyChangedEventHandler PropertyChanged;
private int connectTimeout = 3000; /// <inheritdoc/>
/// <summary> public bool Disposed { get; private set; }
/// The initial connection timeout in milliseconds
/// </summary>
public int ConnectTimeout {
get { return connectTimeout; }
set { connectTimeout = value; }
}
private volatile int queuedMessages; /// <inheritdoc/>
/// <summary> public int QueuedMessages => queuedMessages;
/// Currently queued Messages
/// </summary>
public int QueuedMessages {
get => queuedMessages;
}
/// <summary> /// <inheritdoc/>
/// The host ip endpoint, leave it null to use an automatic interface
/// </summary>
public IPEndPoint HostEndpoint { get; set; }
private bool isConnected;
/// <summary>
/// The current connection state of the interface
/// </summary>
public bool IsConnected { public bool IsConnected {
get => isConnected; get => isConnected;
private set { private protected set {
isConnected = value; isConnected = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsConnected))); OnPropChange();
} }
} }
private bool disposed; /// <inheritdoc/>
/// <summary>
/// True if the current interface was disposed
/// </summary>
public bool Disposed {
get { return disposed; }
private set { disposed = value; }
}
private PLCInfo plcInfo;
/// <summary>
/// Generic information about the connected PLC
/// </summary>
public PLCInfo PlcInfo { public PLCInfo PlcInfo {
get => plcInfo; get => plcInfo;
private set { private set {
plcInfo = value; plcInfo = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PlcInfo))); OnPropChange();
} }
} }
/// <summary> /// <inheritdoc/>
/// The registered data registers of the PLC
/// </summary>
internal List<BaseRegister> RegistersUnderlying { get; private set; } = new List<BaseRegister>();
public IEnumerable<IRegister> Registers => RegistersUnderlying.Cast<IRegister>(); public IEnumerable<IRegister> Registers => RegistersUnderlying.Cast<IRegister>();
internal IEnumerable<IRegisterInternal> RegistersInternal => RegistersUnderlying.Cast<IRegisterInternal>(); /// <inheritdoc/>
private string ip;
private int port;
private int stationNumber;
private int cycleTimeMs = 25;
private int bytesTotalCountedUpstream = 0;
private int bytesTotalCountedDownstream = 0;
/// <summary>
/// The current IP of the PLC connection
/// </summary>
public string IpAddress => ip;
/// <summary>
/// The current port of the PLC connection
/// </summary>
public int Port => port;
/// <summary>
/// The station number of the PLC
/// </summary>
public int StationNumber => stationNumber; public int StationNumber => stationNumber;
/// <summary> /// <inheritdoc/>
/// The duration of the last message cycle
/// </summary>
public int CycleTimeMs {
get { return cycleTimeMs; }
private set {
cycleTimeMs = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CycleTimeMs)));
}
}
private int bytesPerSecondUpstream = 0;
/// <summary>
/// The current transmission speed in bytes per second
/// </summary>
public int BytesPerSecondUpstream { public int BytesPerSecondUpstream {
get { return bytesPerSecondUpstream; } get { return bytesPerSecondUpstream; }
private set { private protected set {
bytesPerSecondUpstream = value; bytesPerSecondUpstream = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BytesPerSecondUpstream))); OnPropChange();
} }
} }
private int bytesPerSecondDownstream = 0; /// <inheritdoc/>
/// <summary>
/// The current transmission speed in bytes per second
/// </summary>
public int BytesPerSecondDownstream { public int BytesPerSecondDownstream {
get { return bytesPerSecondDownstream; } get { return bytesPerSecondDownstream; }
private set { private protected set {
bytesPerSecondDownstream = value; bytesPerSecondDownstream = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BytesPerSecondDownstream))); OnPropChange();
} }
} }
internal NetworkStream stream; #endregion
internal TcpClient client;
internal readonly SerialQueue queue = new SerialQueue();
private int RecBufferSize = 128;
internal int SendExceptionsInRow = 0;
internal bool ImportantTaskRunning = false;
private Stopwatch speedStopwatchUpstr; #region Public read/write Properties / Fields
private Stopwatch speedStopwatchDownstr;
private Task firstPollTask = new Task(() => { }); /// <inheritdoc/>
public int ConnectTimeout { get; set; } = 3000;
#region Initialization #endregion
/// <summary> #region Methods
/// Builds a new Interfacer for a PLC
/// </summary>
/// <param name="_ip">IP adress of the PLC</param>
/// <param name="_port">Port of the PLC</param>
/// <param name="_station">Station Number of the PLC</param>
public MewtocolInterface(string _ip, int _port = 9094, int _station = 1) {
ip = _ip; private protected MewtocolInterface () {
port = _port;
stationNumber = _station;
Connected += MewtocolInterface_Connected; Connected += MewtocolInterface_Connected;
@@ -216,210 +152,60 @@ namespace MewtocolNet {
} }
#endregion /// <inheritdoc/>
public virtual Task ConnectAsync () => throw new NotImplementedException();
#region Setup /// <inheritdoc/>
public async Task AwaitFirstDataAsync() => await firstPollTask;
/// <summary> /// <inheritdoc/>
/// Trys to connect to the PLC by the IP given in the constructor
/// </summary>
/// <param name="OnConnected">
/// Gets called when a connection with a PLC was established
/// <para/>
/// If <see cref="WithPoller"/> is used it waits for the first data receive cycle to complete
/// </param>
/// <param name="OnFailed">Gets called when an error or timeout during connection occurs</param>
/// <returns></returns>
public async Task<MewtocolInterface> ConnectAsync(Action<PLCInfo> OnConnected = null, Action OnFailed = null) {
firstPollTask = new Task(() => { });
Logger.Log("Connecting to PLC...", LogLevel.Info, this);
var plcinf = await GetPLCInfoAsync();
if (plcinf != null) {
Logger.Log("Connected", LogLevel.Info, this);
Logger.Log($"\n\n{plcinf.ToString()}\n\n", LogLevel.Verbose, this);
Connected?.Invoke(plcinf);
if (!usePoller) {
if (OnConnected != null) OnConnected(plcinf);
firstPollTask.RunSynchronously();
return this;
}
PolledCycle += OnPollCycleDone;
void OnPollCycleDone() {
if (OnConnected != null) OnConnected(plcinf);
firstPollTask.RunSynchronously();
PolledCycle -= OnPollCycleDone;
}
} else {
if (OnFailed != null) {
OnFailed();
Disconnected?.Invoke();
firstPollTask.RunSynchronously();
Logger.Log("Initial connection failed", LogLevel.Info, this);
}
}
return this;
}
/// <summary>
/// Use this to await the first poll iteration after connecting,
/// This also completes if the initial connection fails
/// </summary>
public async Task AwaitFirstDataAsync () => await firstPollTask;
/// <summary>
/// Changes the connections parameters of the PLC, only applyable when the connection is offline
/// </summary>
/// <param name="_ip">Ip adress</param>
/// <param name="_port">Port number</param>
/// <param name="_station">Station number</param>
public void ChangeConnectionSettings(string _ip, int _port, int _station = 1) {
if (IsConnected)
throw new Exception("Cannot change the connection settings while the PLC is connected");
ip = _ip;
port = _port;
stationNumber = _station;
}
/// <summary>
/// Closes the connection all cyclic polling
/// </summary>
public void Disconnect() { public void Disconnect() {
if (!IsConnected) if (!IsConnected) return;
return;
pollCycleTask.Wait();
OnMajorSocketExceptionWhileConnected(); OnMajorSocketExceptionWhileConnected();
} }
/// <summary> /// <inheritdoc/>
/// Attaches a poller to the interface that continously public void Dispose() {
/// polls the registered data registers and writes the values to them
/// </summary>
public MewtocolInterface WithPoller() {
usePoller = true; if (Disposed) return;
return this; Disconnect();
//GC.SuppressFinalize(this);
Disposed = true;
} }
#endregion /// <inheritdoc/>
public virtual string GetConnectionInfo() => throw new NotImplementedException();
#region TCP connection state handling /// <inheritdoc/>
public async Task<MewtocolFrameResponse> SendCommandAsync(string _msg, bool withTerminator = true, int timeoutMs = -1) {
private async Task ConnectTCP() {
if (!IPAddress.TryParse(ip, out var targetIP)) {
throw new ArgumentException("The IP adress of the PLC was no valid format");
}
try {
if (HostEndpoint != null) {
client = new TcpClient(HostEndpoint) {
ReceiveBufferSize = RecBufferSize,
NoDelay = false,
};
var ep = (IPEndPoint)client.Client.LocalEndPoint;
Logger.Log($"Connecting [MAN] endpoint: {ep.Address}:{ep.Port}", LogLevel.Verbose, this);
} else {
client = new TcpClient() {
ReceiveBufferSize = RecBufferSize,
NoDelay = false,
ExclusiveAddressUse = true,
};
}
var result = client.BeginConnect(targetIP, port, null, null);
var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(ConnectTimeout));
if (!success || !client.Connected) {
OnMajorSocketExceptionWhileConnecting();
return;
}
if (HostEndpoint == null) {
var ep = (IPEndPoint)client.Client.LocalEndPoint;
Logger.Log($"Connecting [AUTO] endpoint: {ep.Address.MapToIPv4()}:{ep.Port}", LogLevel.Verbose, this);
}
stream = client.GetStream();
stream.ReadTimeout = 1000;
await Task.CompletedTask;
} catch (SocketException) {
OnMajorSocketExceptionWhileConnecting();
}
}
#endregion
#region Low level command handling
/// <summary>
/// Calculates the checksum automatically and sends a command to the PLC then awaits results
/// </summary>
/// <param name="_msg">MEWTOCOL Formatted request string ex: %01#RT</param>
/// <param name="withTerminator">Append the checksum and bcc automatically</param>
/// <returns>Returns the result</returns>
public async Task<MewtocolFrameResponse> SendCommandAsync(string _msg, bool withTerminator = true) {
//send request //send request
queuedMessages++; queuedMessages++;
var tempResponse = await queue.Enqueue(() => SendFrameAsync(_msg, withTerminator, withTerminator));
var tempResponse = queue.Enqueue(async () => await SendFrameAsync(_msg, withTerminator, withTerminator));
if (await Task.WhenAny(tempResponse, Task.Delay(timeoutMs)) != tempResponse) {
// timeout logic
return new MewtocolFrameResponse(403, "Timed out");
}
tcpMessagesSentThisCycle++; tcpMessagesSentThisCycle++;
queuedMessages--; queuedMessages--;
return tempResponse; return tempResponse.Result;
} }
private async Task<MewtocolFrameResponse> SendFrameAsync (string frame, bool useBcc = true, bool useCr = true) { private protected async Task<MewtocolFrameResponse> SendFrameAsync (string frame, bool useBcc = true, bool useCr = true) {
try { try {
//stop time
if (speedStopwatchUpstr == null) {
speedStopwatchUpstr = Stopwatch.StartNew();
}
if (speedStopwatchUpstr.Elapsed.TotalSeconds >= 1) {
speedStopwatchUpstr.Restart();
bytesTotalCountedUpstream = 0;
}
const char CR = '\r';
const char DELIMITER = '&';
if (client == null || !client.Connected) await ConnectTCP();
if (useBcc) if (useBcc)
frame = $"{frame.BuildBCCFrame()}"; frame = $"{frame.BuildBCCFrame()}";
@@ -428,82 +214,32 @@ namespace MewtocolNet {
//write inital command //write inital command
byte[] writeBuffer = Encoding.UTF8.GetBytes(frame); byte[] writeBuffer = Encoding.UTF8.GetBytes(frame);
await stream.WriteAsync(writeBuffer, 0, writeBuffer.Length); stream.Write(writeBuffer, 0, writeBuffer.Length);
//calc upstream speed
bytesTotalCountedUpstream += writeBuffer.Length;
var perSecUpstream = (double)((bytesTotalCountedUpstream / speedStopwatchUpstr.Elapsed.TotalMilliseconds) * 1000);
if (perSecUpstream <= 10000)
BytesPerSecondUpstream = (int)Math.Round(perSecUpstream, MidpointRounding.AwayFromZero);
Logger.Log($"[---------CMD START--------]", LogLevel.Critical, this); Logger.Log($"[---------CMD START--------]", LogLevel.Critical, this);
Logger.Log($"--> OUT MSG: {frame.Replace("\r", "(CR)")}", LogLevel.Critical, this); Logger.Log($"--> OUT MSG: {frame.Replace("\r", "(CR)")}", LogLevel.Critical, this);
//read var readResult = await ReadCommandAsync();
List<byte> totalResponse = new List<byte>();
byte[] responseBuffer = new byte[512];
bool wasMultiFramedResponse = false; //did not receive bytes but no errors, the com port was not configured right
CommandState cmdState = CommandState.Intial; if (readResult.Item1.Length == 0) {
//read until command complete return new MewtocolFrameResponse(402, "Receive buffer was empty");
while (cmdState != CommandState.Complete) {
//time measuring
if (speedStopwatchDownstr == null) {
speedStopwatchDownstr = Stopwatch.StartNew();
}
if (speedStopwatchDownstr.Elapsed.TotalSeconds >= 1) {
speedStopwatchDownstr.Restart();
bytesTotalCountedDownstream = 0;
}
responseBuffer = new byte[128];
await stream.ReadAsync(responseBuffer, 0, responseBuffer.Length);
bool terminatorReceived = responseBuffer.Any(x => x == (byte)CR);
var delimiterTerminatorIdx = responseBuffer.SearchBytePattern(new byte[] { (byte)DELIMITER, (byte)CR });
if (terminatorReceived && delimiterTerminatorIdx == -1) {
cmdState = CommandState.Complete;
} else if (delimiterTerminatorIdx != -1) {
cmdState = CommandState.RequestedNextFrame;
} else {
cmdState = CommandState.LineFeed;
}
//log message parts
var tempMsg = Encoding.UTF8.GetString(responseBuffer).Replace("\r", "(CR)");
Logger.Log($">> IN PART: {tempMsg}, Command state: {cmdState}", LogLevel.Critical, this);
//error response
int errorCode = CheckForErrorMsg(tempMsg);
if (errorCode != 0) return new MewtocolFrameResponse(errorCode);
//add complete response to collector without empty bytes
totalResponse.AddRange(responseBuffer.Where(x => x != (byte)0x0));
//request next part of the command if the delimiter was received
if (cmdState == CommandState.RequestedNextFrame) {
Logger.Log($"Requesting next frame...", LogLevel.Critical, this);
wasMultiFramedResponse = true;
writeBuffer = Encoding.UTF8.GetBytes("%01**&\r");
await stream.WriteAsync(writeBuffer, 0, writeBuffer.Length);
}
} }
//build final result //build final result
string resString = Encoding.UTF8.GetString(totalResponse.ToArray()); string resString = Encoding.UTF8.GetString(readResult.Item1);
if (wasMultiFramedResponse) { //check if the message had errors
//error response
var gotErrorcode = CheckForErrorMsg(resString);
if (gotErrorcode != 0) {
return new MewtocolFrameResponse(gotErrorcode);
}
//was multiframed response
if (readResult.Item2) {
var split = resString.Split('&'); var split = resString.Split('&');
@@ -519,13 +255,6 @@ namespace MewtocolNet {
} }
bytesTotalCountedDownstream += Encoding.ASCII.GetByteCount(resString);
var perSecDownstream = (double)((bytesTotalCountedDownstream / speedStopwatchDownstr.Elapsed.TotalMilliseconds) * 1000);
if (perSecUpstream <= 10000)
BytesPerSecondDownstream = (int)Math.Round(perSecUpstream, MidpointRounding.AwayFromZero);
Logger.Log($"<-- IN MSG: {resString.Replace("\r", "(CR)")}", LogLevel.Critical, this); Logger.Log($"<-- IN MSG: {resString.Replace("\r", "(CR)")}", LogLevel.Critical, this);
Logger.Log($"Total bytes parsed: {resString.Length}", LogLevel.Critical, this); Logger.Log($"Total bytes parsed: {resString.Length}", LogLevel.Critical, this);
Logger.Log($"[---------CMD END----------]", LogLevel.Critical, this); Logger.Log($"[---------CMD END----------]", LogLevel.Critical, this);
@@ -540,7 +269,73 @@ namespace MewtocolNet {
} }
private int CheckForErrorMsg (string msg) { private protected async Task<(byte[], bool)> ReadCommandAsync () {
//read total
List<byte> totalResponse = new List<byte>();
bool wasMultiFramedResponse = false;
try {
bool needsRead = false;
do {
byte[] buffer = new byte[128];
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
byte[] received = new byte[bytesRead];
Buffer.BlockCopy(buffer, 0, received, 0, bytesRead);
var commandRes = ParseBufferFrame(received);
needsRead = commandRes == CommandState.LineFeed || commandRes == CommandState.RequestedNextFrame;
var tempMsg = Encoding.UTF8.GetString(received).Replace("\r", "(CR)");
Logger.Log($">> IN PART: {tempMsg}, Command state: {commandRes}", LogLevel.Critical, this);
//add complete response to collector without empty bytes
totalResponse.AddRange(received.Where(x => x != (byte)0x0));
if (commandRes == CommandState.RequestedNextFrame) {
//request next frame
var writeBuffer = Encoding.UTF8.GetBytes("%01**&\r");
await stream.WriteAsync(writeBuffer, 0, writeBuffer.Length);
wasMultiFramedResponse = true;
}
} while (needsRead);
} catch (OperationCanceledException) { }
return (totalResponse.ToArray(), wasMultiFramedResponse);
}
private protected CommandState ParseBufferFrame(byte[] received) {
const char CR = '\r';
const char DELIMITER = '&';
CommandState cmdState;
bool terminatorReceived = received.Any(x => x == (byte)CR);
var delimiterTerminatorIdx = received.ToArray().SearchBytePattern(new byte[] { (byte)DELIMITER, (byte)CR });
if (terminatorReceived && delimiterTerminatorIdx == -1) {
cmdState = CommandState.Complete;
} else if (delimiterTerminatorIdx != -1) {
cmdState = CommandState.RequestedNextFrame;
} else {
cmdState = CommandState.LineFeed;
}
return cmdState;
}
private protected int CheckForErrorMsg (string msg) {
//error catching //error catching
Regex errorcheck = new Regex(@"\%[0-9]{2}\!([0-9]{2})", RegexOptions.IgnoreCase); Regex errorcheck = new Regex(@"\%[0-9]{2}\!([0-9]{2})", RegexOptions.IgnoreCase);
@@ -557,11 +352,7 @@ namespace MewtocolNet {
} }
#endregion private protected void OnMajorSocketExceptionWhileConnecting() {
#region Disposing
private void OnMajorSocketExceptionWhileConnecting() {
if (IsConnected) { if (IsConnected) {
@@ -572,7 +363,7 @@ namespace MewtocolNet {
} }
private void OnMajorSocketExceptionWhileConnected() { private protected void OnMajorSocketExceptionWhileConnected() {
if (IsConnected) { if (IsConnected) {
@@ -583,43 +374,44 @@ namespace MewtocolNet {
} }
private protected virtual void OnConnected (PLCInfo plcinf) {
/// <summary> Logger.Log("Connected", LogLevel.Info, this);
/// Disposes the current interface and clears all its members Logger.Log($"\n\n{plcinf.ToString()}\n\n", LogLevel.Verbose, this);
/// </summary>
public void Dispose() {
if (Disposed) return; IsConnected = true;
Disconnect(); Connected?.Invoke(plcinf);
//GC.SuppressFinalize(this); if (!usePoller) {
firstPollTask.RunSynchronously();
}
Disposed = true; PolledCycle += OnPollCycleDone;
void OnPollCycleDone() {
} firstPollTask.RunSynchronously();
PolledCycle -= OnPollCycleDone;
private void OnDisconnect () {
if (IsConnected) {
BytesPerSecondDownstream = 0;
BytesPerSecondUpstream = 0;
CycleTimeMs = 0;
IsConnected = false;
ClearRegisterVals();
Disconnected?.Invoke();
KillPoller();
client.Close();
} }
} }
private protected virtual void OnDisconnect () {
private void ClearRegisterVals() { BytesPerSecondDownstream = 0;
BytesPerSecondUpstream = 0;
CycleTimeMs = 0;
IsConnected = false;
ClearRegisterVals();
Disconnected?.Invoke();
KillPoller();
}
private protected void ClearRegisterVals() {
for (int i = 0; i < RegistersUnderlying.Count; i++) { for (int i = 0; i < RegistersUnderlying.Count; i++) {
@@ -630,28 +422,7 @@ namespace MewtocolNet {
} }
#endregion private protected void OnPropChange([CallerMemberName] string propertyName = null) {
#region Accessing Info
/// <summary>
/// Gets the connection info string
/// </summary>
public string GetConnectionPortInfo() {
return $"{IpAddress}:{Port}";
}
#endregion
#region Property change evnts
/// <summary>
/// Triggers a property changed event
/// </summary>
/// <param name="propertyName">Name of the property to trigger for</param>
private void OnPropChange ([CallerMemberName]string propertyName = null) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

View File

@@ -1,6 +1,7 @@
using MewtocolNet.Exceptions; using MewtocolNet.Exceptions;
using MewtocolNet.Logging; using MewtocolNet.Logging;
using MewtocolNet.RegisterAttributes; using MewtocolNet.RegisterAttributes;
using MewtocolNet.RegisterBuilding;
using MewtocolNet.Registers; using MewtocolNet.Registers;
using System; using System;
using System.Collections; using System.Collections;
@@ -12,24 +13,14 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace MewtocolNet namespace MewtocolNet {
{
/// <summary> /// <summary>
/// The PLC com interface class /// The PLC com interface class
/// </summary> /// </summary>
public partial class MewtocolInterface { public partial class MewtocolInterface {
internal event Action PolledCycle; internal Task pollCycleTask;
internal volatile bool pollerTaskStopped = true;
internal volatile bool pollerFirstCycle;
internal bool usePoller = false;
private int tcpMessagesSentThisCycle = 0;
private int pollerCycleDurationMs;
/// <summary> /// <summary>
/// True if the poller is actvice (can be paused) /// True if the poller is actvice (can be paused)
@@ -87,7 +78,8 @@ namespace MewtocolNet
tcpMessagesSentThisCycle = 0; tcpMessagesSentThisCycle = 0;
await OnMultiFrameCycle(); pollCycleTask = OnMultiFrameCycle();
await pollCycleTask;
return tcpMessagesSentThisCycle; return tcpMessagesSentThisCycle;
@@ -104,9 +96,10 @@ namespace MewtocolNet
tcpMessagesSentThisCycle = 0; tcpMessagesSentThisCycle = 0;
await OnMultiFrameCycle(); pollCycleTask = OnMultiFrameCycle();
await pollCycleTask;
if(!IsConnected) { if (!IsConnected) {
pollerTaskStopped = true; pollerTaskStopped = true;
return; return;
} }

View File

@@ -19,8 +19,9 @@ namespace MewtocolNet {
/// Gets generic information about the PLC /// Gets generic information about the PLC
/// </summary> /// </summary>
/// <returns>A PLCInfo class</returns> /// <returns>A PLCInfo class</returns>
public async Task<PLCInfo> GetPLCInfoAsync() { public async Task<PLCInfo> GetPLCInfoAsync(int timeout = -1) {
var resu = await SendCommandAsync("%01#RT");
var resu = await SendCommandAsync("%01#RT", true, timeout);
if (!resu.Success) return null; if (!resu.Success) return null;
var reg = new Regex(@"\%([0-9]{2})\$RT([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{4})..", RegexOptions.IgnoreCase); var reg = new Regex(@"\%([0-9]{2})\$RT([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{4})..", RegexOptions.IgnoreCase);

View File

@@ -0,0 +1,260 @@
using MewtocolNet.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Net.Sockets;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
namespace MewtocolNet {
public class MewtocolInterfaceSerial : MewtocolInterface, IPlcSerial {
private bool autoSerial;
//serial config
public string PortName { get; private set; }
public int SerialBaudRate { get; private set; }
public int SerialDataBits { get; private set; }
public Parity SerialParity { get; private set; }
public StopBits SerialStopBits { get; private set; }
//Serial
internal SerialPort serialClient;
internal MewtocolInterfaceSerial () : base() { }
/// <inheritdoc/>
public IPlcSerial WithPoller () {
usePoller = true;
return this;
}
/// <inheritdoc/>
public override string GetConnectionInfo() {
StringBuilder sb = new StringBuilder();
sb.Append($"{PortName}, ");
sb.Append($"{SerialBaudRate}, ");
sb.Append($"{SerialDataBits} ");
sb.Append($"{SerialParity.ToString().Substring(0, 1)} ");
switch (SerialStopBits) {
case StopBits.None:
sb.Append("0");
break;
case StopBits.One:
sb.Append("1");
break;
case StopBits.Two:
sb.Append("2");
break;
case StopBits.OnePointFive:
sb.Append("1.5");
break;
}
return sb.ToString();
}
/// <inheritdoc/>
public void ConfigureConnection (string _portName, int _baudRate = 19200, int _dataBits = 8, Parity _parity = Parity.Odd, StopBits _stopBits = StopBits.One, int _station = 1) {
PortName = _portName;
SerialBaudRate = _baudRate;
SerialDataBits = _dataBits;
SerialParity = _parity;
SerialStopBits = _stopBits;
stationNumber = _station;
OnSerialPropsChanged();
Disconnect();
}
internal void ConfigureConnectionAuto () {
autoSerial = true;
}
/// <inheritdoc/>
public override async Task ConnectAsync () {
try {
PLCInfo gotInfo = null;
if(autoSerial) {
gotInfo = await TryConnectAsyncMulti();
} else {
gotInfo = await TryConnectAsyncSingle(PortName, SerialBaudRate, SerialDataBits, SerialParity, SerialStopBits);
}
if(gotInfo != null) {
OnConnected(gotInfo);
} else {
Logger.Log("Initial connection failed", LogLevel.Info, this);
OnMajorSocketExceptionWhileConnecting();
}
await Task.CompletedTask;
} catch (SocketException) {
OnMajorSocketExceptionWhileConnecting();
}
}
private async Task<PLCInfo> TryConnectAsyncMulti () {
var baudRates = Enum.GetValues(typeof(BaudRate)).Cast<BaudRate>();
//ordered by most commonly used
baudRates = new List<BaudRate> {
//most common 3
BaudRate._19200,
BaudRate._115200,
BaudRate._9600,
//others
BaudRate._1200,
BaudRate._2400,
BaudRate._4800,
BaudRate._38400,
BaudRate._57600,
BaudRate._230400,
};
var dataBits = Enum.GetValues(typeof(DataBits)).Cast<DataBits>();
var parities = new List<Parity>() { Parity.None, Parity.Odd, Parity.Even, Parity.Mark };
var stopBits = new List<StopBits> { StopBits.One, StopBits.Two };
foreach (var baud in baudRates) {
foreach (var databit in dataBits) {
foreach (var parity in parities) {
foreach (var stopBit in stopBits) {
var res = await TryConnectAsyncSingle(PortName, (int)baud, (int)databit, parity, stopBit);
if(res != null) return res;
}
}
}
}
return null;
}
private async Task<PLCInfo> TryConnectAsyncSingle (string port, int baud, int dbits, Parity par, StopBits sbits) {
try {
serialClient = new SerialPort() {
PortName = port,
BaudRate = baud,
DataBits = dbits,
Parity = par,
StopBits = sbits,
ReadTimeout = 100,
Handshake = Handshake.None
};
PortName = port;
SerialBaudRate = baud;
SerialDataBits = dbits;
SerialParity = par;
SerialStopBits = sbits;
OnSerialPropsChanged();
serialClient.Open();
if (!serialClient.IsOpen) {
Logger.Log($"Failed to open [SERIAL]: {GetConnectionInfo()}", LogLevel.Verbose, this);
return null;
}
stream = serialClient.BaseStream;
Logger.Log($"Opened [SERIAL]: {GetConnectionInfo()}", LogLevel.Verbose, this);
var plcinf = await GetPLCInfoAsync(100);
if (plcinf == null) CloseClient();
return plcinf;
} catch (UnauthorizedAccessException) {
Logger.Log($"The port {serialClient.PortName} is currently in use. Close all accessing applications first", LogLevel.Error, this);
return null;
}
}
private void CloseClient () {
if(serialClient.IsOpen) {
serialClient.Close();
Logger.Log($"Closed [SERIAL]", LogLevel.Verbose, this);
}
}
private protected override void OnDisconnect() {
if (IsConnected) {
base.OnDisconnect();
CloseClient();
}
}
private void OnSerialPropsChanged () {
OnPropChange(nameof(PortName));
OnPropChange(nameof(SerialBaudRate));
OnPropChange(nameof(SerialDataBits));
OnPropChange(nameof(SerialParity));
OnPropChange(nameof(SerialStopBits));
}
}
}

View File

@@ -0,0 +1,164 @@
using MewtocolNet.Exceptions;
using MewtocolNet.Logging;
using MewtocolNet.Queue;
using MewtocolNet.RegisterAttributes;
using MewtocolNet.Registers;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace MewtocolNet {
/// <summary>
/// The PLC com interface class
/// </summary>
public class MewtocolInterfaceTcp : MewtocolInterface, IPlcEthernet {
/// <summary>
/// The host ip endpoint, leave it null to use an automatic interface
/// </summary>
public IPEndPoint HostEndpoint { get; set; }
//TCP
internal TcpClient client;
//tcp/ip config
private string ip;
private int port;
/// <inheritdoc/>
public string IpAddress => ip;
/// <inheritdoc/>
public int Port => port;
internal MewtocolInterfaceTcp () : base() { }
/// <inheritdoc/>
public IPlcEthernet WithPoller () {
usePoller = true;
return this;
}
#region TCP connection state handling
/// <inheritdoc/>
public void ConfigureConnection (string _ip, int _port = 9094, int _station = 1) {
ip = _ip;
port = _port;
stationNumber = _station;
Disconnect();
}
/// <inheritdoc/>
public override async Task ConnectAsync () {
if (!IPAddress.TryParse(ip, out var targetIP)) {
throw new ArgumentException("The IP adress of the PLC was no valid format");
}
try {
if (HostEndpoint != null) {
client = new TcpClient(HostEndpoint) {
ReceiveBufferSize = RecBufferSize,
NoDelay = false,
};
var ep = (IPEndPoint)client.Client.LocalEndPoint;
Logger.Log($"Connecting [MAN] endpoint: {ep.Address}:{ep.Port}", LogLevel.Verbose, this);
} else {
client = new TcpClient() {
ReceiveBufferSize = RecBufferSize,
NoDelay = false,
ExclusiveAddressUse = true,
};
}
var result = client.BeginConnect(targetIP, port, null, null);
var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(ConnectTimeout));
if (!success || !client.Connected) {
OnMajorSocketExceptionWhileConnecting();
return;
}
if (HostEndpoint == null) {
var ep = (IPEndPoint)client.Client.LocalEndPoint;
Logger.Log($"Connecting [AUTO] endpoint: {ep.Address.MapToIPv4()}:{ep.Port}", LogLevel.Verbose, this);
}
//get the stream
stream = client.GetStream();
stream.ReadTimeout = 1000;
//get plc info
var plcinf = await GetPLCInfoAsync();
if (plcinf != null) {
OnConnected(plcinf);
} else {
Logger.Log("Initial connection failed", LogLevel.Info, this);
OnDisconnect();
}
await Task.CompletedTask;
} catch (SocketException) {
OnMajorSocketExceptionWhileConnecting();
}
}
/// <summary>
/// Gets the connection info string
/// </summary>
public override string GetConnectionInfo() {
return $"{IpAddress}:{Port}";
}
private protected override void OnDisconnect() {
if (IsConnected) {
base.OnDisconnect();
client.Close();
}
}
#endregion
}
}

View File

@@ -23,4 +23,7 @@
<_Parameter1>MewtocolTests</_Parameter1> <_Parameter1>MewtocolTests</_Parameter1>
</AssemblyAttribute> </AssemblyAttribute>
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="System.IO.Ports" Version="7.0.0" />
</ItemGroup>
</Project> </Project>

View File

@@ -5,7 +5,7 @@ namespace MewtocolNet {
/// <summary> /// <summary>
/// All modes /// All modes
/// </summary> /// </summary>
public class PLCMode { public struct PLCMode {
/// <summary> /// <summary>
/// PLC is running /// PLC is running

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace MewtocolNet {
public enum BaudRate {
_1200 = 1200,
_2400 = 2400,
_4800 = 4800,
_9600 = 9600,
_19200 = 19200,
_38400 = 38400,
_57600 = 57600,
_115200 = 115200,
_230400 = 230400,
}
}

View File

@@ -1,4 +1,5 @@
namespace MewtocolNet.RegisterAttributes { namespace MewtocolNet {
/// <summary> /// <summary>
/// The size of the bitwise register /// The size of the bitwise register
/// </summary> /// </summary>

View File

@@ -0,0 +1,9 @@
namespace MewtocolNet {
public enum DataBits {
Seven = 7,
Eight = 8,
}
}

View File

@@ -0,0 +1,26 @@
namespace MewtocolNet {
// this is just used as syntactic sugar,
// when creating registers that are R/X/Y typed you dont need the DT types
/// <summary>
/// The type of an input/output register
/// </summary>
public enum IOType {
/// <summary>
/// Physical input as a bool (Relay)
/// </summary>
X = 0,
/// <summary>
/// Physical output as a bool (Relay)
/// </summary>
Y = 1,
/// <summary>
/// Internal relay
/// </summary>
R = 2,
}
}

View File

@@ -34,27 +34,4 @@ namespace MewtocolNet {
} }
// this is just used as syntactic sugar,
// when creating registers that are R/X/Y typed you dont need the DT types
/// <summary>
/// The type of an input/output register
/// </summary>
public enum IOType {
/// <summary>
/// Physical input as a bool (Relay)
/// </summary>
X = 0,
/// <summary>
/// Physical output as a bool (Relay)
/// </summary>
Y = 1,
/// <summary>
/// Internal relay
/// </summary>
R = 2,
}
} }

View File

@@ -21,10 +21,18 @@ namespace MewtocolNet.RegisterBuilding {
}; };
public static RegBuilder ForInterface (MewtocolInterface interf) { public static RegBuilder ForInterface (IPlcEthernet interf) {
var rb = new RegBuilder(); var rb = new RegBuilder();
rb.forInterface = interf; rb.forInterface = interf as MewtocolInterface;
return rb;
}
public static RegBuilder ForInterface(IPlcSerial interf) {
var rb = new RegBuilder();
rb.forInterface = interf as MewtocolInterface;
return rb; return rb;
} }

View File

@@ -3,7 +3,7 @@ using System;
using System.Collections; using System.Collections;
using System.Reflection; using System.Reflection;
namespace MewtocolNet { namespace MewtocolNet.RegisterBuilding {
internal struct RegisterBuildInfo { internal struct RegisterBuildInfo {

View File

@@ -1,27 +0,0 @@
namespace MewtocolNet {
/// <summary>
/// The formatted result of a ascii command
/// </summary>
public struct CommandResult {
/// <summary>
/// Success state of the message
/// </summary>
public bool Success { get; set; }
/// <summary>
/// Response text of the message
/// </summary>
public string Response { get; set; }
/// <summary>
/// Error code of the message
/// </summary>
public string Error { get; set; }
/// <summary>
/// Error text of the message
/// </summary>
public string ErrorDescription { get; set; }
}
}

View File

@@ -1,21 +0,0 @@
namespace MewtocolNet {
internal enum TCPMessageResult {
Waiting,
Success,
NotConnected,
FailedWithException,
FailedLineFeed,
}
internal enum CommandState {
Intial,
LineFeed,
RequestedNextFrame,
Complete
}
}

View File

@@ -109,7 +109,7 @@ namespace MewtocolTests {
[Fact(DisplayName = "Boolean R generation")] [Fact(DisplayName = "Boolean R generation")]
public void BooleanGen() { public void BooleanGen() {
var interf = new MewtocolInterface("192.168.0.1"); var interf = new MewtocolInterfaceShared("192.168.0.1");
interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller();
var register = interf.GetRegister(nameof(TestRegisterCollection.TestBool1)); var register = interf.GetRegister(nameof(TestRegisterCollection.TestBool1));
@@ -122,7 +122,7 @@ namespace MewtocolTests {
[Fact(DisplayName = "Boolean input XD generation")] [Fact(DisplayName = "Boolean input XD generation")]
public void BooleanInputGen() { public void BooleanInputGen() {
var interf = new MewtocolInterface("192.168.0.1"); var interf = new MewtocolInterfaceShared("192.168.0.1");
interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller();
var register = interf.GetRegister(nameof(TestRegisterCollection.TestBoolInputXD)); var register = interf.GetRegister(nameof(TestRegisterCollection.TestBoolInputXD));
@@ -135,7 +135,7 @@ namespace MewtocolTests {
[Fact(DisplayName = "Int16 generation")] [Fact(DisplayName = "Int16 generation")]
public void Int16Gen() { public void Int16Gen() {
var interf = new MewtocolInterface("192.168.0.1"); var interf = new MewtocolInterfaceShared("192.168.0.1");
interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller();
var register = interf.GetRegister(nameof(TestRegisterCollection.TestInt16)); var register = interf.GetRegister(nameof(TestRegisterCollection.TestInt16));
@@ -148,7 +148,7 @@ namespace MewtocolTests {
[Fact(DisplayName = "UInt16 generation")] [Fact(DisplayName = "UInt16 generation")]
public void UInt16Gen() { public void UInt16Gen() {
var interf = new MewtocolInterface("192.168.0.1"); var interf = new MewtocolInterfaceShared("192.168.0.1");
interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller();
var register = interf.GetRegister(nameof(TestRegisterCollection.TestUInt16)); var register = interf.GetRegister(nameof(TestRegisterCollection.TestUInt16));
@@ -161,7 +161,7 @@ namespace MewtocolTests {
[Fact(DisplayName = "Int32 generation")] [Fact(DisplayName = "Int32 generation")]
public void Int32Gen() { public void Int32Gen() {
var interf = new MewtocolInterface("192.168.0.1"); var interf = new MewtocolInterfaceShared("192.168.0.1");
interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller();
var register = interf.GetRegister(nameof(TestRegisterCollection.TestInt32)); var register = interf.GetRegister(nameof(TestRegisterCollection.TestInt32));
@@ -174,7 +174,7 @@ namespace MewtocolTests {
[Fact(DisplayName = "UInt32 generation")] [Fact(DisplayName = "UInt32 generation")]
public void UInt32Gen() { public void UInt32Gen() {
var interf = new MewtocolInterface("192.168.0.1"); var interf = new MewtocolInterfaceShared("192.168.0.1");
interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller();
var register = interf.GetRegister(nameof(TestRegisterCollection.TestUInt32)); var register = interf.GetRegister(nameof(TestRegisterCollection.TestUInt32));
@@ -187,7 +187,7 @@ namespace MewtocolTests {
[Fact(DisplayName = "Float32 generation")] [Fact(DisplayName = "Float32 generation")]
public void Float32Gen() { public void Float32Gen() {
var interf = new MewtocolInterface("192.168.0.1"); var interf = new MewtocolInterfaceShared("192.168.0.1");
interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller();
var register = interf.GetRegister(nameof(TestRegisterCollection.TestFloat32)); var register = interf.GetRegister(nameof(TestRegisterCollection.TestFloat32));
@@ -200,7 +200,7 @@ namespace MewtocolTests {
[Fact(DisplayName = "TimeSpan generation")] [Fact(DisplayName = "TimeSpan generation")]
public void TimespanGen() { public void TimespanGen() {
var interf = new MewtocolInterface("192.168.0.1"); var interf = new MewtocolInterfaceShared("192.168.0.1");
interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller(); interf.WithRegisterCollection(new TestRegisterCollection()).WithPoller();
var register = interf.GetRegister(nameof(TestRegisterCollection.TestTime)); var register = interf.GetRegister(nameof(TestRegisterCollection.TestTime));

View File

@@ -73,9 +73,9 @@ namespace MewtocolTests
output.WriteLine($"Testing: {plc.PLCName}"); output.WriteLine($"Testing: {plc.PLCName}");
var cycleClient = new MewtocolInterface(plc.PLCIP, plc.PLCPort); var cycleClient = new MewtocolInterfaceShared(plc.PLCIP, plc.PLCPort);
await cycleClient.ConnectAsync(); await cycleClient.ConnectAsyncOld();
Assert.True(cycleClient.IsConnected); Assert.True(cycleClient.IsConnected);
@@ -94,9 +94,9 @@ namespace MewtocolTests
output.WriteLine($"Testing: {plc.PLCName}\n"); output.WriteLine($"Testing: {plc.PLCName}\n");
var client = new MewtocolInterface(plc.PLCIP, plc.PLCPort); var client = new MewtocolInterfaceShared(plc.PLCIP, plc.PLCPort);
await client.ConnectAsync(); await client.ConnectAsyncOld();
output.WriteLine($"{client.PlcInfo}\n"); output.WriteLine($"{client.PlcInfo}\n");