Initial commit
This commit is contained in:
@@ -0,0 +1,528 @@
|
||||
using Sick.ColaB.Commands;
|
||||
using Sick.ColaB.Parsers;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace Sick.ColaB;
|
||||
|
||||
/// <summary>
|
||||
/// High-level client for communicating with SICK scanners
|
||||
/// Supports both ColaB (binary) and ColaA (ASCII) protocols with auto-detection
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new scanner client
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">Scanner IP address</param>
|
||||
/// <param name="port">Scanner port (default: 2112)</param>
|
||||
/// <param name="protocol">Protocol to use: "ColaB", "ColaA", or "Auto" (default: "Auto")</param>
|
||||
/// <param name="connectTimeoutMs">Connection timeout in milliseconds (default: 5000)</param>
|
||||
/// <param name="commandTimeoutMs">Command timeout in milliseconds (default: 5000)</param>
|
||||
/// <param name="readTimeoutMs">Read timeout in milliseconds (default: 5000)</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the client is currently connected
|
||||
/// </summary>
|
||||
public bool IsConnected => _session?.IsOpen ?? false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the protocol currently in use
|
||||
/// </summary>
|
||||
public string ActualProtocol => _session?.ProtocolType ?? (_useBinaryProtocol ? "ColaB" : "ColaA");
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the scanner with auto protocol detection
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects from the scanner
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the device identification from the scanner
|
||||
/// </summary>
|
||||
public Task<string> ReadDeviceIdentAsync(CancellationToken cancellationToken = default)
|
||||
=> SendCommandAsync("sRN DeviceIdent", cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Reads the current device state from the scanner
|
||||
/// </summary>
|
||||
public Task<string> ReadDeviceStateAsync(CancellationToken cancellationToken = default)
|
||||
=> SendCommandAsync("sRN SCdevicestate", cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Reads the scan data configuration from the scanner
|
||||
/// </summary>
|
||||
public Task<string> ReadScanDataConfigAsync(CancellationToken cancellationToken = default)
|
||||
=> SendCommandAsync("sRN LMDscandatacfg", cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the access mode for the scanner
|
||||
/// </summary>
|
||||
/// <param name="level">Access level (3 = Authorized Client, 4 = Service)</param>
|
||||
/// <param name="passwordHash">Password hash (hex string)</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
public Task<string> SetAccessModeAsync(int level, string passwordHash, CancellationToken cancellationToken = default)
|
||||
=> SendCommandAsync($"sMN SetAccessMode {level} {passwordHash}", cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Starts the measurement (scanning) process
|
||||
/// </summary>
|
||||
public Task<string> StartMeasurementAsync(CancellationToken cancellationToken = default)
|
||||
=> SendCommandAsync("sMN LMCstartmeas", cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Stops the measurement (scanning) process
|
||||
/// </summary>
|
||||
public Task<string> StopMeasurementAsync(CancellationToken cancellationToken = default)
|
||||
=> SendCommandAsync("sMN LMCstopmeas", cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Applies settings and runs the scanner
|
||||
/// </summary>
|
||||
public Task<string> RunAsync(CancellationToken cancellationToken = default)
|
||||
=> SendCommandAsync("sMN Run", cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables scan data events (ColaA only - ColaB requires polling)
|
||||
/// </summary>
|
||||
/// <param name="enable">True to enable events, false to disable</param>
|
||||
public Task<string> EnableScanDataEventsAsync(bool enable, CancellationToken cancellationToken = default)
|
||||
=> SendCommandAsync($"sEN LMDscandata {(enable ? 1 : 0)}", cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a SOPAS command and returns the reply (internal use)
|
||||
/// </summary>
|
||||
private async Task<string> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads scan data from the scanner
|
||||
/// </summary>
|
||||
public async Task<ScanDataResult?> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the scanner (authentication, start measurement, enable scan data)
|
||||
/// </summary>
|
||||
/// <returns>True if initialization completed successfully, false otherwise</returns>
|
||||
public async Task<bool> 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<string> SendCommandColaBAsync(string command)
|
||||
{
|
||||
if (_session == null || !_session.IsOpen)
|
||||
throw new InvalidOperationException("Session not initialized");
|
||||
|
||||
return await _session.SendCommandAsync(command, __commandTimeoutMs);
|
||||
}
|
||||
|
||||
private async Task<string> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user