Initial commit

This commit is contained in:
2026-07-03 16:31:37 +07:00
commit 899c7c637d
1939 changed files with 641750 additions and 0 deletions

View File

@@ -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;
}
}