Initial commit
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
using Sick.ColaB.Commands;
|
||||
|
||||
namespace Sick.ColaB;
|
||||
|
||||
/// <summary>
|
||||
/// ColaB session manager for handling send, receive and process telegrams
|
||||
/// </summary>
|
||||
public sealed class ColaBSession(TcpClient _tcpClient) : SopasSession
|
||||
{
|
||||
private readonly Lock _lock = new();
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the protocol type of this session
|
||||
/// </summary>
|
||||
public override string ProtocolType => "ColaB";
|
||||
|
||||
public override bool IsOpen
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _tcpClient.IsConnected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the session (connects to scanner)
|
||||
/// </summary>
|
||||
public void Open(int timeoutMs = 5000)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, nameof(ColaBSession));
|
||||
|
||||
if (IsOpen)
|
||||
return;
|
||||
|
||||
_tcpClient.Connect(timeoutMs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the session asynchronously (connects to scanner)
|
||||
/// </summary>
|
||||
public override Task OpenAsync(int connectTimeoutMs, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Open(connectTimeoutMs);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the session (disconnects from scanner)
|
||||
/// </summary>
|
||||
public override void Close()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_tcpClient.Disconnect();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a command and waits for reply
|
||||
/// IMPORTANT: This method is thread-safe. Only one command can execute at a time.
|
||||
/// </summary>
|
||||
public void ExecuteCommand(CommandBase command, int timeoutMs = 5000)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, nameof(ColaBSession));
|
||||
|
||||
// CRITICAL: Lock to prevent concurrent send/receive operations
|
||||
// Without this lock, multiple threads can send commands simultaneously,
|
||||
// causing responses to get mixed up and frame parsing to fail
|
||||
lock (_lock)
|
||||
{
|
||||
if (!IsOpen)
|
||||
throw new InvalidOperationException("Session is not open");
|
||||
|
||||
// Create ColaB frame from command
|
||||
var commandData = command.GetCommandData();
|
||||
var frame = ColaBHelper.CreateFrame(commandData.Span);
|
||||
|
||||
// Send frame
|
||||
_tcpClient.Send(frame);
|
||||
|
||||
// Receive reply with retry logic
|
||||
byte[]? replyFrame = null;
|
||||
int retries = 3;
|
||||
for (int i = 0; i < retries; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
replyFrame = _tcpClient.Receive(timeoutMs);
|
||||
break;
|
||||
}
|
||||
catch (TimeoutException) when (i < retries - 1)
|
||||
{
|
||||
// Retry on timeout
|
||||
System.Threading.Thread.Sleep(100);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (replyFrame == null || replyFrame.Length == 0)
|
||||
throw new TimeoutException($"No reply received after {retries} attempts");
|
||||
|
||||
// Parse frame and extract command data
|
||||
if (ColaBHelper.TryParseFrame(replyFrame, out var replyData, out _))
|
||||
{
|
||||
var replyDataArray = replyData.ToArray();
|
||||
command.ProcessReply(replyDataArray);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to parse reply frame. Raw reply: {BitConverter.ToString([.. replyFrame.Take(64)])}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a SOPAS command and waits for reply (string format)
|
||||
/// </summary>
|
||||
public override async Task<string> SendCommandAsync(string command, int timeoutMs, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, nameof(ColaBSession));
|
||||
|
||||
if (!IsOpen)
|
||||
throw new InvalidOperationException("Session is not open");
|
||||
|
||||
// Parse SOPAS command string and create appropriate CommandBase
|
||||
CommandBase colaBCommand;
|
||||
if (command.StartsWith("sRN ", StringComparison.Ordinal))
|
||||
{
|
||||
// Read command
|
||||
var variableName = command[4..].Trim();
|
||||
colaBCommand = new ReadCommand(variableName);
|
||||
}
|
||||
else if (command.StartsWith("sWN ", StringComparison.Ordinal))
|
||||
{
|
||||
// Write command
|
||||
var parts = command[4..].Split(' ', 2);
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
colaBCommand = new WriteCommand(parts[0], parts[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"Invalid write command format: {command}");
|
||||
}
|
||||
}
|
||||
else if (command.StartsWith("sMN ", StringComparison.Ordinal))
|
||||
{
|
||||
// Method command
|
||||
var parts = command[4..].Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length > 0)
|
||||
{
|
||||
var methodName = parts[0];
|
||||
var parameters = parts.Skip(1).ToArray();
|
||||
colaBCommand = new MethodCommand(methodName, parameters);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"Invalid method command format: {command}");
|
||||
}
|
||||
}
|
||||
else if (command.StartsWith("sEN ", StringComparison.Ordinal))
|
||||
{
|
||||
// Event command
|
||||
var parts = command[4..].Split(' ', 2);
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
colaBCommand = new WriteCommand(parts[0], parts[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"Invalid event command format: {command}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"Unsupported command format: {command}");
|
||||
}
|
||||
|
||||
// Execute command
|
||||
ExecuteCommand(colaBCommand, timeoutMs);
|
||||
|
||||
// Return reply
|
||||
if (!colaBCommand.WasSuccessful)
|
||||
return string.Empty;
|
||||
|
||||
if (colaBCommand is ReadCommand readCmd)
|
||||
return readCmd.ReplyValue ?? string.Empty;
|
||||
else if (colaBCommand is MethodCommand methodCmd)
|
||||
return methodCmd.ReplyValue ?? string.Empty;
|
||||
else if (colaBCommand is WriteCommand)
|
||||
return "OK";
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a read command and returns the command object (allowing access to RawReplyData for binary parsing)
|
||||
/// </summary>
|
||||
public ReadCommand ExecuteReadCommand(string variableName, int timeoutMs = 5000)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, nameof(ColaBSession));
|
||||
|
||||
if (!IsOpen)
|
||||
throw new InvalidOperationException("Session is not open");
|
||||
|
||||
var readCmd = new ReadCommand(variableName);
|
||||
ExecuteCommand(readCmd, timeoutMs);
|
||||
return readCmd;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Receives a telegram from scanner (for event-based reception)
|
||||
/// </summary>
|
||||
public override Task<byte[]?> ReceiveTelegramAsync(int timeoutMs, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, nameof(ColaBSession));
|
||||
|
||||
if (!IsOpen)
|
||||
return Task.FromResult<byte[]?>(null);
|
||||
|
||||
try
|
||||
{
|
||||
var frame = _tcpClient.Receive(timeoutMs);
|
||||
return Task.FromResult<byte[]?>(frame);
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
return Task.FromResult<byte[]?>(null);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
Close();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user