Initial commit

This commit is contained in:
2026-07-03 16:37:12 +07:00
commit 63b8c1ea8b
1931 changed files with 640587 additions and 0 deletions

View File

@@ -0,0 +1,245 @@
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;
}
}