using System.Net.Sockets; using System.Text; namespace Sick.ColaB; /// /// ColaA (ASCII) session manager for handling send, receive and process telegrams /// Uses ASCII protocol with STX/ETX framing /// public sealed class ColaASession : SopasSession { private readonly string _ipAddress; private readonly ushort _port; private readonly int _readTimeoutMs; private readonly int _writeTimeoutMs; private System.Net.Sockets.TcpClient? _tcpClient; private System.Net.Sockets.NetworkStream? _networkStream; private readonly Lock _lock = new(); private bool _disposed; /// /// Creates a new ColaA session /// public ColaASession(string ipAddress, ushort port, int readTimeoutMs = 5000, int writeTimeoutMs = 5000) { _ipAddress = ipAddress ?? throw new ArgumentNullException(nameof(ipAddress)); _port = port; _readTimeoutMs = readTimeoutMs; _writeTimeoutMs = writeTimeoutMs; } /// /// Gets the protocol type of this session /// public override string ProtocolType => "ColaA"; /// /// Gets whether the session is open /// public override bool IsOpen { get { lock (_lock) { return _tcpClient?.Connected ?? false; } } } /// /// Opens the session (connects to scanner) /// public override async Task OpenAsync(int connectTimeoutMs, CancellationToken cancellationToken = default) { ObjectDisposedException.ThrowIf(_disposed, nameof(ColaASession)); if (IsOpen) return; var tcpClient = new System.Net.Sockets.TcpClient(); try { // Connect with timeout var connectTask = tcpClient.ConnectAsync(_ipAddress, _port); var timeoutTask = Task.Delay(connectTimeoutMs, cancellationToken); var completedTask = await Task.WhenAny(connectTask, timeoutTask); if (completedTask == timeoutTask) { tcpClient.Dispose(); throw new TimeoutException($"Connection timeout after {connectTimeoutMs}ms"); } await connectTask; var stream = tcpClient.GetStream(); stream.ReadTimeout = _readTimeoutMs; stream.WriteTimeout = _writeTimeoutMs; lock (_lock) { _tcpClient = tcpClient; _networkStream = stream; } } catch { tcpClient?.Dispose(); throw; } } /// /// Closes the session (disconnects from scanner) /// public override void Close() { if (_disposed) return; lock (_lock) { try { _networkStream?.Close(); _networkStream?.Dispose(); } catch { } _networkStream = null; try { _tcpClient?.Close(); _tcpClient?.Dispose(); } catch { } _tcpClient = null; } } /// /// Sends a SOPAS command and waits for reply (ASCII format) /// Command format: "sRN DeviceIdent", "sMN LMCstartmeas", etc. /// public override async Task SendCommandAsync(string command, int timeoutMs, CancellationToken cancellationToken = default) { ObjectDisposedException.ThrowIf(_disposed, nameof(ColaASession)); NetworkStream? stream; lock (_lock) { if (!IsOpen || _networkStream == null || _tcpClient == null || !_tcpClient.Connected) throw new InvalidOperationException("Session is not open"); stream = _networkStream; } // Format SOPAS command (ASCII mode): STX + command + ETX var commandBytes = Encoding.ASCII.GetBytes(command); var message = new List { 0x02 }; // STX message.AddRange(commandBytes); message.Add(0x03); // ETX // Send command await stream.WriteAsync(message.ToArray().AsMemory(0, message.Count), cancellationToken); await stream.FlushAsync(cancellationToken); // Read reply with retry logic string reply = string.Empty; int retries = 3; for (int i = 0; i < retries; i++) { try { reply = await ReadReplyAsync(stream, timeoutMs, cancellationToken); if (!string.IsNullOrEmpty(reply)) break; } catch (TimeoutException) when (i < retries - 1) { // Retry on timeout await Task.Delay(100, cancellationToken); continue; } } if (string.IsNullOrEmpty(reply)) { throw new TimeoutException($"No reply received after {retries} attempts"); } return reply; } /// /// Reads a reply from the scanner (ASCII format) /// private async Task ReadReplyAsync(NetworkStream stream, int timeoutMs, CancellationToken cancellationToken) { var buffer = new byte[4096]; var totalBytes = 0; var startTime = DateTime.UtcNow; while (DateTime.UtcNow - startTime < TimeSpan.FromMilliseconds(timeoutMs)) { if (stream.DataAvailable) { var bytesRead = await stream.ReadAsync( buffer.AsMemory(totalBytes, buffer.Length - totalBytes), cancellationToken); if (bytesRead == 0) break; totalBytes += bytesRead; // Check if we have complete message (ends with ETX) if (totalBytes > 0 && buffer[totalBytes - 1] == 0x03) break; } else { await Task.Delay(10, cancellationToken); } } if (totalBytes == 0) throw new TimeoutException("No reply from scanner"); // Parse reply (skip STX, remove ETX) var replyStart = buffer[0] == 0x02 ? 1 : 0; var replyEnd = buffer[totalBytes - 1] == 0x03 ? totalBytes - 1 : totalBytes; var replyLength = replyEnd - replyStart; if (replyLength <= 0) return string.Empty; return Encoding.ASCII.GetString(buffer, replyStart, replyLength); } /// /// Reads scan data telegram from scanner (for event-based reception) /// public override async Task ReceiveTelegramAsync(int timeoutMs, CancellationToken cancellationToken = default) { ObjectDisposedException.ThrowIf(_disposed, nameof(ColaASession)); NetworkStream? stream; lock (_lock) { if (!IsOpen || _networkStream == null) return null; stream = _networkStream; } var buffer = new byte[480000]; // Large buffer for scan data var totalBytes = 0; var startTime = DateTime.UtcNow; while (DateTime.UtcNow - startTime < TimeSpan.FromMilliseconds(timeoutMs)) { if (stream.DataAvailable) { var bytesRead = await stream.ReadAsync( buffer.AsMemory(totalBytes, buffer.Length - totalBytes), cancellationToken); if (bytesRead == 0) break; totalBytes += bytesRead; // Check if we have complete message (ends with ETX) if (totalBytes > 0 && buffer[totalBytes - 1] == 0x03) { // Return complete telegram (including STX and ETX) return buffer[..totalBytes]; } } else { await Task.Delay(10, cancellationToken); } } if (totalBytes == 0) return null; // Return partial data if available return buffer[..totalBytes]; } public override void Dispose() { if (_disposed) return; Close(); _disposed = true; } }