360 lines
12 KiB
C#
360 lines
12 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// COLA2 session manager for handling send, receive and process telegrams
|
|
/// Thread-safe implementation
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new COLA2 session
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|