Initial commit
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace Sick.ColaB;
|
||||
|
||||
/// <summary>
|
||||
/// ColaA (ASCII) session manager for handling send, receive and process telegrams
|
||||
/// Uses ASCII protocol with STX/ETX framing
|
||||
/// </summary>
|
||||
public sealed class ColaASession : SopasSession
|
||||
{
|
||||
private readonly string _ipAddress;
|
||||
private readonly ushort _port;
|
||||
private readonly int _readTimeoutMs;
|
||||
private readonly int _writeTimeoutMs;
|
||||
|
||||
private System.Net.Sockets.TcpClient? _tcpClient;
|
||||
private System.Net.Sockets.NetworkStream? _networkStream;
|
||||
private readonly Lock _lock = new();
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new ColaA session
|
||||
/// </summary>
|
||||
public ColaASession(string ipAddress, ushort port, int readTimeoutMs = 5000, int writeTimeoutMs = 5000)
|
||||
{
|
||||
_ipAddress = ipAddress ?? throw new ArgumentNullException(nameof(ipAddress));
|
||||
_port = port;
|
||||
_readTimeoutMs = readTimeoutMs;
|
||||
_writeTimeoutMs = writeTimeoutMs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the protocol type of this session
|
||||
/// </summary>
|
||||
public override string ProtocolType => "ColaA";
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the session is open
|
||||
/// </summary>
|
||||
public override bool IsOpen
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _tcpClient?.Connected ?? false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the session (connects to scanner)
|
||||
/// </summary>
|
||||
public override async Task OpenAsync(int connectTimeoutMs, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, nameof(ColaASession));
|
||||
|
||||
if (IsOpen)
|
||||
return;
|
||||
|
||||
var tcpClient = new System.Net.Sockets.TcpClient();
|
||||
try
|
||||
{
|
||||
// Connect with timeout
|
||||
var connectTask = tcpClient.ConnectAsync(_ipAddress, _port);
|
||||
var timeoutTask = Task.Delay(connectTimeoutMs, cancellationToken);
|
||||
var completedTask = await Task.WhenAny(connectTask, timeoutTask);
|
||||
|
||||
if (completedTask == timeoutTask)
|
||||
{
|
||||
tcpClient.Dispose();
|
||||
throw new TimeoutException($"Connection timeout after {connectTimeoutMs}ms");
|
||||
}
|
||||
|
||||
await connectTask;
|
||||
|
||||
var stream = tcpClient.GetStream();
|
||||
stream.ReadTimeout = _readTimeoutMs;
|
||||
stream.WriteTimeout = _writeTimeoutMs;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_tcpClient = tcpClient;
|
||||
_networkStream = stream;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
tcpClient?.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the session (disconnects from scanner)
|
||||
/// </summary>
|
||||
public override void Close()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
try
|
||||
{
|
||||
_networkStream?.Close();
|
||||
_networkStream?.Dispose();
|
||||
}
|
||||
catch { }
|
||||
_networkStream = null;
|
||||
|
||||
try
|
||||
{
|
||||
_tcpClient?.Close();
|
||||
_tcpClient?.Dispose();
|
||||
}
|
||||
catch { }
|
||||
_tcpClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a SOPAS command and waits for reply (ASCII format)
|
||||
/// Command format: "sRN DeviceIdent", "sMN LMCstartmeas", etc.
|
||||
/// </summary>
|
||||
public override async Task<string> SendCommandAsync(string command, int timeoutMs, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, nameof(ColaASession));
|
||||
|
||||
NetworkStream? stream;
|
||||
lock (_lock)
|
||||
{
|
||||
if (!IsOpen || _networkStream == null || _tcpClient == null || !_tcpClient.Connected)
|
||||
throw new InvalidOperationException("Session is not open");
|
||||
|
||||
stream = _networkStream;
|
||||
}
|
||||
|
||||
// Format SOPAS command (ASCII mode): STX + command + ETX
|
||||
var commandBytes = Encoding.ASCII.GetBytes(command);
|
||||
var message = new List<byte> { 0x02 }; // STX
|
||||
message.AddRange(commandBytes);
|
||||
message.Add(0x03); // ETX
|
||||
|
||||
// Send command
|
||||
await stream.WriteAsync(message.ToArray().AsMemory(0, message.Count), cancellationToken);
|
||||
await stream.FlushAsync(cancellationToken);
|
||||
|
||||
// Read reply with retry logic
|
||||
string reply = string.Empty;
|
||||
int retries = 3;
|
||||
|
||||
for (int i = 0; i < retries; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
reply = await ReadReplyAsync(stream, timeoutMs, cancellationToken);
|
||||
if (!string.IsNullOrEmpty(reply))
|
||||
break;
|
||||
}
|
||||
catch (TimeoutException) when (i < retries - 1)
|
||||
{
|
||||
// Retry on timeout
|
||||
await Task.Delay(100, cancellationToken);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(reply))
|
||||
{
|
||||
throw new TimeoutException($"No reply received after {retries} attempts");
|
||||
}
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a reply from the scanner (ASCII format)
|
||||
/// </summary>
|
||||
private async Task<string> ReadReplyAsync(NetworkStream stream, int timeoutMs, CancellationToken cancellationToken)
|
||||
{
|
||||
var buffer = new byte[4096];
|
||||
var totalBytes = 0;
|
||||
var startTime = DateTime.UtcNow;
|
||||
|
||||
while (DateTime.UtcNow - startTime < TimeSpan.FromMilliseconds(timeoutMs))
|
||||
{
|
||||
if (stream.DataAvailable)
|
||||
{
|
||||
var bytesRead = await stream.ReadAsync(
|
||||
buffer.AsMemory(totalBytes, buffer.Length - totalBytes),
|
||||
cancellationToken);
|
||||
|
||||
if (bytesRead == 0)
|
||||
break;
|
||||
|
||||
totalBytes += bytesRead;
|
||||
|
||||
// Check if we have complete message (ends with ETX)
|
||||
if (totalBytes > 0 && buffer[totalBytes - 1] == 0x03)
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(10, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
if (totalBytes == 0)
|
||||
throw new TimeoutException("No reply from scanner");
|
||||
|
||||
// Parse reply (skip STX, remove ETX)
|
||||
var replyStart = buffer[0] == 0x02 ? 1 : 0;
|
||||
var replyEnd = buffer[totalBytes - 1] == 0x03 ? totalBytes - 1 : totalBytes;
|
||||
var replyLength = replyEnd - replyStart;
|
||||
|
||||
if (replyLength <= 0)
|
||||
return string.Empty;
|
||||
|
||||
return Encoding.ASCII.GetString(buffer, replyStart, replyLength);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads scan data telegram from scanner (for event-based reception)
|
||||
/// </summary>
|
||||
public override async Task<byte[]?> ReceiveTelegramAsync(int timeoutMs, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, nameof(ColaASession));
|
||||
|
||||
NetworkStream? stream;
|
||||
lock (_lock)
|
||||
{
|
||||
if (!IsOpen || _networkStream == null)
|
||||
return null;
|
||||
|
||||
stream = _networkStream;
|
||||
}
|
||||
|
||||
var buffer = new byte[480000]; // Large buffer for scan data
|
||||
var totalBytes = 0;
|
||||
var startTime = DateTime.UtcNow;
|
||||
|
||||
while (DateTime.UtcNow - startTime < TimeSpan.FromMilliseconds(timeoutMs))
|
||||
{
|
||||
if (stream.DataAvailable)
|
||||
{
|
||||
var bytesRead = await stream.ReadAsync(
|
||||
buffer.AsMemory(totalBytes, buffer.Length - totalBytes),
|
||||
cancellationToken);
|
||||
|
||||
if (bytesRead == 0)
|
||||
break;
|
||||
|
||||
totalBytes += bytesRead;
|
||||
|
||||
// Check if we have complete message (ends with ETX)
|
||||
if (totalBytes > 0 && buffer[totalBytes - 1] == 0x03)
|
||||
{
|
||||
// Return complete telegram (including STX and ETX)
|
||||
return buffer[..totalBytes];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(10, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
if (totalBytes == 0)
|
||||
return null;
|
||||
|
||||
// Return partial data if available
|
||||
return buffer[..totalBytes];
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
Close();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user