using Sick.ColaB.Commands; namespace Sick.ColaB; /// /// ColaB session manager for handling send, receive and process telegrams /// public sealed class ColaBSession(TcpClient _tcpClient) : SopasSession { private readonly Lock _lock = new(); private bool _disposed; /// /// Gets the protocol type of this session /// public override string ProtocolType => "ColaB"; public override bool IsOpen { get { lock (_lock) { return _tcpClient.IsConnected; } } } /// /// Opens the session (connects to scanner) /// public void Open(int timeoutMs = 5000) { ObjectDisposedException.ThrowIf(_disposed, nameof(ColaBSession)); if (IsOpen) return; _tcpClient.Connect(timeoutMs); } /// /// Opens the session asynchronously (connects to scanner) /// public override Task OpenAsync(int connectTimeoutMs, CancellationToken cancellationToken = default) { Open(connectTimeoutMs); return Task.CompletedTask; } /// /// Closes the session (disconnects from scanner) /// public override void Close() { if (_disposed) return; _tcpClient.Disconnect(); } /// /// Executes a command and waits for reply /// IMPORTANT: This method is thread-safe. Only one command can execute at a time. /// 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)])}"); } } } /// /// Sends a SOPAS command and waits for reply (string format) /// public override async Task 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; } /// /// Executes a read command and returns the command object (allowing access to RawReplyData for binary parsing) /// 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; } /// /// Receives a telegram from scanner (for event-based reception) /// public override Task ReceiveTelegramAsync(int timeoutMs, CancellationToken cancellationToken = default) { ObjectDisposedException.ThrowIf(_disposed, nameof(ColaBSession)); if (!IsOpen) return Task.FromResult(null); try { var frame = _tcpClient.Receive(timeoutMs); return Task.FromResult(frame); } catch (TimeoutException) { return Task.FromResult(null); } } public override void Dispose() { if (_disposed) return; Close(); _disposed = true; } }