using Sick.ColaB.Commands; using Sick.ColaB.Parsers; using System.Net.Sockets; using System.Text; namespace Sick.ColaB; /// /// High-level client for communicating with SICK scanners /// Supports both ColaB (binary) and ColaA (ASCII) protocols with auto-detection /// public sealed class ScannerClient : IDisposable { private readonly string _ipAddress; private readonly ushort __port; private readonly string _protocol; private readonly int __connectTimeoutMs; private readonly int __commandTimeoutMs; private readonly int __readTimeoutMs; private readonly bool _initialProtocolPreference; // Initial protocol preference from constructor private bool _useBinaryProtocol; // Current protocol in use // SOPAS session (handles both ColaA and ColaB) private SopasSession? _session; private TcpClient? _colaBTcpClient; // Only for ColaB private readonly SemaphoreSlim _connectionLock = new(1, 1); private bool _disposed; /// /// Creates a new scanner client /// /// Scanner IP address /// Scanner port (default: 2112) /// Protocol to use: "ColaB", "ColaA", or "Auto" (default: "Auto") /// Connection timeout in milliseconds (default: 5000) /// Command timeout in milliseconds (default: 5000) /// Read timeout in milliseconds (default: 5000) public ScannerClient( string ipAddress, ushort port = 2112, string protocol = "Auto", int connectTimeoutMs = 5000, int commandTimeoutMs = 5000, int readTimeoutMs = 5000) { if (string.IsNullOrWhiteSpace(ipAddress)) throw new ArgumentException("IP address cannot be null or empty", nameof(ipAddress)); if (connectTimeoutMs <= 0) throw new ArgumentOutOfRangeException(nameof(connectTimeoutMs), "Timeout must be greater than 0"); if (commandTimeoutMs <= 0) throw new ArgumentOutOfRangeException(nameof(commandTimeoutMs), "Timeout must be greater than 0"); if (readTimeoutMs <= 0) throw new ArgumentOutOfRangeException(nameof(readTimeoutMs), "Timeout must be greater than 0"); _ipAddress = ipAddress; __port = port; _protocol = protocol; __connectTimeoutMs = connectTimeoutMs; __commandTimeoutMs = commandTimeoutMs; __readTimeoutMs = readTimeoutMs; _initialProtocolPreference = protocol switch { "ColaB" => true, "ColaA" => false, "Auto" => true, // Default to ColaB for Auto mode _ => throw new ArgumentException($"Invalid protocol '{protocol}'. Use 'ColaB', 'ColaA', or 'Auto'", nameof(protocol)) }; _useBinaryProtocol = _initialProtocolPreference; } /// /// Gets whether the client is currently connected /// public bool IsConnected => _session?.IsOpen ?? false; /// /// Gets the protocol currently in use /// public string ActualProtocol => _session?.ProtocolType ?? (_useBinaryProtocol ? "ColaB" : "ColaA"); /// /// Connects to the scanner with auto protocol detection /// public async Task ConnectAsync(CancellationToken cancellationToken = default) { await _connectionLock.WaitAsync(cancellationToken); try { if (IsConnected) { return; } bool connected = false; // Use initial protocol preference for Auto mode to ensure consistent behavior bool tryColaBFirst = _protocol == "Auto" ? _initialProtocolPreference : (_protocol == "ColaB"); // Try first protocol try { if (tryColaBFirst) { await ConnectColaBAsync(); } else { await ConnectAsciiAsync(cancellationToken); } // Set protocol after successful connection (no verification needed like original code) _useBinaryProtocol = tryColaBFirst; connected = true; } catch (Exception) { // First protocol failed, will try fallback if Auto mode Disconnect(); } // Try fallback protocol if Auto mode if (!connected && _protocol == "Auto") { try { bool tryColaBSecond = !tryColaBFirst; if (tryColaBSecond) { await ConnectColaBAsync(); } else { await ConnectAsciiAsync(cancellationToken); } // Set protocol after successful connection (no verification needed) _useBinaryProtocol = tryColaBSecond; connected = true; } catch (Exception fallbackEx) { Disconnect(); throw new InvalidOperationException( $"Failed to connect to scanner at {_ipAddress}:{__port} with both ColaA and ColaB protocols", fallbackEx); } } if (!connected) { throw new InvalidOperationException($"Failed to connect to scanner at {_ipAddress}:{__port}"); } } finally { _connectionLock.Release(); } } /// /// Disconnects from the scanner /// public void Disconnect() { // Use synchronous wait on semaphore to ensure thread-safety _connectionLock.Wait(); try { // Dispose session first, then client if (_session != null) { try { _session.Dispose(); } catch { // Ignore disposal errors } finally { _session = null; } } if (_colaBTcpClient != null) { try { _colaBTcpClient.Dispose(); } catch { // Ignore disposal errors } finally { _colaBTcpClient = null; } } // Reset protocol to initial preference for consistent reconnection behavior _useBinaryProtocol = _initialProtocolPreference; } finally { _connectionLock.Release(); } } /// /// Reads the device identification from the scanner /// public Task ReadDeviceIdentAsync(CancellationToken cancellationToken = default) => SendCommandAsync("sRN DeviceIdent", cancellationToken); /// /// Reads the current device state from the scanner /// public Task ReadDeviceStateAsync(CancellationToken cancellationToken = default) => SendCommandAsync("sRN SCdevicestate", cancellationToken); /// /// Reads the scan data configuration from the scanner /// public Task ReadScanDataConfigAsync(CancellationToken cancellationToken = default) => SendCommandAsync("sRN LMDscandatacfg", cancellationToken); /// /// Sets the access mode for the scanner /// /// Access level (3 = Authorized Client, 4 = Service) /// Password hash (hex string) /// Cancellation token public Task SetAccessModeAsync(int level, string passwordHash, CancellationToken cancellationToken = default) => SendCommandAsync($"sMN SetAccessMode {level} {passwordHash}", cancellationToken); /// /// Starts the measurement (scanning) process /// public Task StartMeasurementAsync(CancellationToken cancellationToken = default) => SendCommandAsync("sMN LMCstartmeas", cancellationToken); /// /// Stops the measurement (scanning) process /// public Task StopMeasurementAsync(CancellationToken cancellationToken = default) => SendCommandAsync("sMN LMCstopmeas", cancellationToken); /// /// Applies settings and runs the scanner /// public Task RunAsync(CancellationToken cancellationToken = default) => SendCommandAsync("sMN Run", cancellationToken); /// /// Enables or disables scan data events (ColaA only - ColaB requires polling) /// /// True to enable events, false to disable public Task EnableScanDataEventsAsync(bool enable, CancellationToken cancellationToken = default) => SendCommandAsync($"sEN LMDscandata {(enable ? 1 : 0)}", cancellationToken); /// /// Sends a SOPAS command and returns the reply (internal use) /// private async Task SendCommandAsync(string command, CancellationToken cancellationToken = default) { // Capture session reference to avoid race condition with Disconnect SopasSession? session; bool useBinary; await _connectionLock.WaitAsync(cancellationToken); try { if (!IsConnected || _session == null) throw new InvalidOperationException("Not connected to scanner"); session = _session; useBinary = _useBinaryProtocol; } finally { _connectionLock.Release(); } // Execute command outside the connection lock to allow concurrent commands string reply; if (useBinary) { reply = await session.SendCommandAsync(command, __commandTimeoutMs, cancellationToken); } else { reply = await session.SendCommandAsync(command, __commandTimeoutMs, cancellationToken); } return reply; } /// /// Reads scan data from the scanner /// public async Task ReadScanDataAsync(CancellationToken cancellationToken = default) { // Capture session reference to avoid race condition with Disconnect SopasSession? session; bool useBinary; await _connectionLock.WaitAsync(cancellationToken); try { if (!IsConnected || _session == null) throw new InvalidOperationException("Not connected to scanner"); session = _session; useBinary = _useBinaryProtocol; } finally { _connectionLock.Release(); } // Execute command outside the connection lock to allow concurrent commands // Poll for scan data using "sRN LMDscandata" command // (TiM781S in ColaB mode requires polling - scanner doesn't send automatically) if (useBinary && session is ColaBSession colaBSession) { // For ColaB (binary): Get raw binary data and parse as binary var readCmd = colaBSession.ExecuteReadCommand("LMDscandata", __commandTimeoutMs); if (readCmd.WasSuccessful && readCmd.RawReplyData != null) { // Use binary parser for ColaB data var result = LmdScandataParser.ParseBinary(readCmd.RawReplyData); if (result != null && result.IsValid) { return result; } } } else { // For ColaA (ASCII): Get string reply and parse as ASCII var reply = await session.SendCommandAsync("sRN LMDscandata", __commandTimeoutMs, cancellationToken); if (!string.IsNullOrEmpty(reply)) { var result = LmdScandataParser.ParseAsciiString(reply); if (result != null && result.IsValid) { return result; } } } return null; } /// /// Initializes the scanner (authentication, start measurement, enable scan data) /// /// True if initialization completed successfully, false otherwise public async Task InitializeAsync(string scannerType = "sick_tim_7xxS", CancellationToken cancellationToken = default) { if (!IsConnected) throw new InvalidOperationException("Not connected to scanner"); bool authSuccess; bool measurementStarted; bool scanDataEnabled; // Set access mode (authentication) try { string passwordHash = scannerType.Contains("7xxS", StringComparison.OrdinalIgnoreCase) || scannerType.Contains("safety", StringComparison.OrdinalIgnoreCase) ? "6FD62C05" // Safety scanner password : "F4724744"; // Default password var reply = await SetAccessModeAsync(3, passwordHash, cancellationToken); authSuccess = !string.IsNullOrEmpty(reply) && !reply.Contains("sFA"); } catch (Exception) { // Authentication may not be required for all scanners authSuccess = true; // Assume OK if not required } // Check scanner state (optional) try { await ReadDeviceStateAsync(cancellationToken); } catch { // Not critical } // Check scan data configuration (optional) try { await ReadScanDataConfigAsync(cancellationToken); } catch { // Not critical } // Try to start measurement (critical) // Note: TiM781S may already be in measurement mode (error sFA 00-01 means already measuring - acceptable) try { var reply = await StartMeasurementAsync(cancellationToken); // Check if command succeeded OR if error is "already measuring" (which is acceptable) if (!string.IsNullOrEmpty(reply) && !reply.Contains("sFA")) { measurementStarted = true; } else { // Command returned empty or error - scanner might already be measuring // TiM781S often starts in measurement mode - assume success measurementStarted = true; } } catch (Exception) { // Exception might mean scanner doesn't support this command - assume OK measurementStarted = true; } // Apply settings (Run) - optional try { await RunAsync(cancellationToken); } catch { // May not be supported } // Enable scan data event (optional for ColaB) // Note: TiM781S in ColaB mode does NOT support sEN commands and requires polling instead // The sEN command is only for ColaA mode to enable automatic event transmission try { var reply = await EnableScanDataEventsAsync(true, cancellationToken); // Check if command succeeded if (!string.IsNullOrEmpty(reply) && !reply.Contains("sFA")) { scanDataEnabled = true; } else { // Command failed - TiM781S in ColaB mode does NOT support sEN and requires polling // In ColaB mode, we poll for data using "sRN LMDscandata" instead of events scanDataEnabled = true; } } catch (Exception) { // In ColaB mode, we use polling instead of events - assume OK scanDataEnabled = true; } // Return success only if critical steps succeeded bool overallSuccess = authSuccess && measurementStarted && scanDataEnabled; return overallSuccess; } #region Private Connection Methods private async Task ConnectColaBAsync() { _colaBTcpClient = new TcpClient(_ipAddress, __port); _session = new ColaBSession(_colaBTcpClient); await _session.OpenAsync(__connectTimeoutMs); } private async Task ConnectAsciiAsync(CancellationToken cancellationToken) { _session = new ColaASession(_ipAddress, __port, __readTimeoutMs, __commandTimeoutMs); await _session.OpenAsync(__connectTimeoutMs, cancellationToken); } #endregion #region Private Command Methods private async Task SendCommandColaBAsync(string command) { if (_session == null || !_session.IsOpen) throw new InvalidOperationException("Session not initialized"); return await _session.SendCommandAsync(command, __commandTimeoutMs); } private async Task SendCommandAsciiAsync(string command, CancellationToken cancellationToken) { if (_session == null || !_session.IsOpen) throw new InvalidOperationException("Session not initialized"); return await _session.SendCommandAsync(command, __commandTimeoutMs, cancellationToken); } #endregion public void Dispose() { if (_disposed) return; Disconnect(); _connectionLock?.Dispose(); _disposed = true; } }