using Sick.SafetyScanners.Cola2.Commands; using Sick.SafetyScanners.DataProcessing; using Sick.SafetyScanners.DataStructures; using Sick.SafetyScanners.Exceptions; using Sick.SafetyScanners.Helpers; using Sick.SafetyScanners.Interfaces; using Sick.SafetyScanners.Types; namespace Sick.SafetyScanners.Cola2; /// /// COLA2 session manager for handling send, receive and process telegrams /// Thread-safe implementation /// public sealed class Cola2Session : ICola2Session { private readonly ITcpClient _tcpClient; private readonly ParseTcpPacket _packetParser; private uint? _sessionId; private ushort _requestId; private readonly object _lock = new(); private bool _disposed; public uint? SessionId { get { lock (_lock) { return _sessionId; } } private set { lock (_lock) { _sessionId = value; } } } public bool IsOpen { get { // Fast path: read without lock first (volatile-like behavior) var sessionId = _sessionId; var isConnected = _tcpClient.IsConnected; // Double-check with lock if needed if (!isConnected || !sessionId.HasValue) return false; // Verify with lock to ensure consistency lock (_lock) { return _tcpClient.IsConnected && _sessionId.HasValue; } } } /// /// Creates a new COLA2 session /// public Cola2Session(ITcpClient tcpClient) { _tcpClient = tcpClient ?? throw new ArgumentNullException(nameof(tcpClient)); _packetParser = new ParseTcpPacket(); _requestId = 0; } public ushort GetNextRequestId() { lock (_lock) { return ++_requestId; } } public void Open() { if (_disposed) ObjectDisposedException.ThrowIf(_disposed, nameof(Cola2Session)); // Close existing session first if open (avoid deadlock by not locking) if (IsOpen) { Close(); } // Connect TCP if not connected if (!_tcpClient.IsConnected) { _tcpClient.Connect(TimeDuration.FromSeconds(5)); } // Create session command var createSessionCmd = new CreateSessionCommand(); // Send command without session ID check if (!createSessionCmd.CanBeExecutedWithoutSessionId) { throw new InvalidOperationException("CreateSession command must be executable without session ID"); } // Manually set request ID createSessionCmd.RequestId = GetNextRequestId(); createSessionCmd.SessionId = 0; // No session ID for CreateSession // Assemble and send telegram (GỬI CreateSession command) AssembleAndSendTelegram(createSessionCmd); // Receive and process response (NHẬN response từ scanner) var response = ReceiveAndProcessResponse(createSessionCmd, TimeDuration.FromSeconds(5)); // Parse response var parseResult = _packetParser.ParseTcpSequence(response); // Process reply createSessionCmd.ProcessReply(parseResult.Data, parseResult.CommandType, parseResult.CommandMode); // Check for error code if (parseResult.ErrorCode.HasValue && parseResult.ErrorCode.Value != 0) { throw new CommandException( 0, parseResult.RequestId, parseResult.CommandType, parseResult.CommandMode, parseResult.ErrorCode.Value, $"CreateSession failed with error code: 0x{parseResult.ErrorCode.Value:X4}" ); } if (!createSessionCmd.WasSuccessful) { throw new SessionException("Failed to create COLA2 session"); } // Extract session ID from packet header (not from reply data) // In C++ reference, session ID is read from packet header (offset 10) by ParseTCPPacket // and set to Command via setCommandValuesFromPacket(), then retrieved via getSessionID() // In C# implementation, we get it directly from parseResult.SessionId if (parseResult.SessionId == 0) { throw new SessionException("Failed to extract session ID from CreateSession reply: Session ID is 0"); } SessionId = parseResult.SessionId; } public void Close() { ObjectDisposedException.ThrowIf(_disposed, nameof(Cola2Session)); lock (_lock) { if (!IsOpen) { // Already closed return; } } try { var closeSessionCmd = new CloseSessionCommand { SessionId = SessionId ?? 0 }; SendCommand(closeSessionCmd, TimeDuration.FromSeconds(5)); } catch (Exception) { // Ignore errors during close } finally { SessionId = null; _tcpClient.Disconnect(); } } public void SendCommand(ICola2Command command, TimeDuration? timeout = null) { ObjectDisposedException.ThrowIf(_disposed, nameof(Cola2Session)); ArgumentNullException.ThrowIfNull(command); // Check if session is required lock (_lock) { if (!command.CanBeExecutedWithoutSessionId && !IsOpen) { throw new SessionException("Attempt to send command in closed Cola2 session state"); } // Set session ID and request ID // In C++ reference: cmd.setSessionID(getSessionID().get_value_or(0)); // We must ensure session ID is set correctly before sending if (!_sessionId.HasValue && !command.CanBeExecutedWithoutSessionId) { throw new SessionException($"Cannot send command: Session ID is not set. Session is open: {IsOpen}"); } command.SessionId = _sessionId ?? 0; command.RequestId = GetNextRequestId(); // Debug: Verify session ID is set (only for commands that require session) if (!command.CanBeExecutedWithoutSessionId && command.SessionId == 0) { throw new SessionException($"Session ID is 0 for command that requires session. Current session state: IsOpen={IsOpen}, _sessionId={_sessionId}"); } } // Assemble and send telegram AssembleAndSendTelegram(command); // Receive and process response var response = ReceiveAndProcessResponse(command, timeout); // Parse response var parseResult = _packetParser.ParseTcpSequence(response); // Check error code FIRST - if error code is present and non-zero, it indicates an error // even if command type/mode is correct (R/A) // Error code format: 0x[VARIABLE_INDEX]00 typically means "variable index not found" if (parseResult.ErrorCode.HasValue && parseResult.ErrorCode.Value != 0) { var errorCode = parseResult.ErrorCode.Value; var errorMessage = GetErrorMessage(errorCode, command); throw new CommandException( SessionId ?? 0, parseResult.RequestId, parseResult.CommandType, parseResult.CommandMode, errorCode, errorMessage ); } // Process reply (this checks command type/mode and sets WasSuccessful) command.ProcessReply(parseResult.Data, parseResult.CommandType, parseResult.CommandMode); // Check if processReply failed (command type/mode mismatch) if (!command.WasSuccessful) { // When command type/mode is wrong, check for error code in data ushort? errorCode = null; if (parseResult.Data.Length >= 2) { // Error code might be at the start of data payload (offset 18 of packet) errorCode = ReadWriteHelper.ReadUint16BigEndian(parseResult.Data.Span, 0); } throw new CommandException(SessionId ?? 0, parseResult.RequestId, parseResult.CommandType, parseResult.CommandMode, parseResult.ErrorCode ?? 0, $"Command failed: processReply returned false. Expected 'R'/'A' (0x52/0x41), got 0x{parseResult.CommandType:X2}/0x{parseResult.CommandMode:X2}" ); } // Command was successful (command type/mode is 'R'/'A') // The 2 bytes at offset 18-19 are part of the data payload (variable index for VariableCommand) // NOT an error code - error code only exists when command fails // No need to check error code when command is acknowledged // Command was successful (processReply returned true and no error code or error code matches variable index) } private void AssembleAndSendTelegram(ICola2Command command) { if (command is not CommandBase cmdBase) throw new ArgumentException("Command must be derived from CommandBase", nameof(command)); var telegram = cmdBase.ConstructTelegram(); _tcpClient.Send(telegram); } private PacketBuffer ReceiveAndProcessResponse(ICola2Command command, TimeDuration? timeout) { var packetMerger = new TcpPacketMerger(); try { while (!packetMerger.IsComplete) { var packet = _tcpClient.Receive(timeout); if (packetMerger.IsEmpty) { var expectedLength = _packetParser.GetExpectedPacketLength(packet); packetMerger.SetTargetSize(expectedLength); } // AddTcpPacket returns true if complete var isComplete = packetMerger.AddTcpPacket(packet); if (isComplete) { break; // Exit loop early when complete } } return packetMerger.GetDeployedBuffer(); } finally { packetMerger.Dispose(); } } private string GetErrorMessage(ushort errorCode, ICola2Command command) { // Common COLA2 error codes // 0x0D00 = Variable index not found or not supported // 0x0001 = General error // 0x0002 = Invalid parameter // etc. var baseMessage = $"Command failed with error code: 0x{errorCode:X4}"; if (command is VariableCommand varCmd) { baseMessage += $" (Variable Index: 0x{varCmd.VariableIndex:X4})"; // Common error codes for variable commands if (errorCode == 0x0D00) { baseMessage += " - Variable index not found or not supported by this scanner model"; } else if (errorCode == 0x0001) { baseMessage += " - General error"; } else if (errorCode == 0x0002) { baseMessage += " - Invalid parameter"; } } return baseMessage; } public void Dispose() { if (_disposed) return; Close(); _tcpClient.Dispose(); _disposed = true; } }