246 lines
7.7 KiB
C#
246 lines
7.7 KiB
C#
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;
|
|
}
|
|
}
|
|
|