Initial commit
This commit is contained in:
@@ -0,0 +1,359 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user