- Добавлен ComSystemManager как новый публичный компонент COM-системы для поэтапной замены ComWrapper - Реализовано получение роли контроллера только после успешного handshake в ComDeviceClient - Добавлено публичное поле ControllerRole в ComDeviceClient - Добавлен запрос роли по команде 97 и разбор ответа формата ROLE : <value> (с пробелами) - В ComMultiManager добавлена логика выбора DefaultDeviceId по роли main_controller - Добавлен доступ к роли устройства через GetDeviceRole(deviceId) в менеджере и фасаде - В ComWrapper добавлены настройки роли: roleRequestCommand, roleResponsePrefix, roleRequestTimeoutMs, mainControllerRole - Дефолтная команда запроса роли изменена на 97 - Выполнена кодовая миграция зависимостей с ComWrapper на ComSystemManager в GameManager, GlobalsManager, PaymentManager, BalanceManager, InactivityManager, COMInput, LightController, PrizeSystem - В GlobalsManager исправлена инициализация ссылки после AddComponent<ComSystemManager>(), чтобы исключить NullReference
724 lines
22 KiB
C#
724 lines
22 KiB
C#
using System;
|
||
using System.Security.Cryptography;
|
||
using System.Text;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using RR.Game2D.Core.Managers;
|
||
|
||
namespace RR.Common.Controls
|
||
{
|
||
internal sealed class ComDeviceClient
|
||
{
|
||
private const string RequestInput = "91";
|
||
private const string DefaultInputBits = "11111111111111111111111111";
|
||
private const long CoinDebounceMs = 250;
|
||
|
||
private static readonly byte[] AesEncryptedKey =
|
||
{
|
||
0x69, 0x40, 0xC7, 0x0E, 0xF2, 0xC7, 0x45, 0x70,
|
||
0x72, 0xCD, 0x28, 0x58, 0x99, 0x8F, 0x73, 0xA8,
|
||
0x5F, 0x5F, 0xED, 0xB4, 0xC1, 0x62, 0xB4, 0x24,
|
||
0xAC, 0xD7, 0xA3, 0xD6, 0xD8, 0x0B, 0x50, 0x37
|
||
};
|
||
|
||
private readonly object _ioLock = new object();
|
||
|
||
private readonly bool _enableEncryptedHandshake;
|
||
private readonly int _handshakeTimeoutMs;
|
||
private readonly bool _keepAlive;
|
||
private readonly string _roleRequestCommand;
|
||
private readonly string _roleResponsePrefix;
|
||
private readonly int _roleRequestTimeoutMs;
|
||
|
||
private CancellationTokenSource _lifecycleCts;
|
||
private Task _pollTask;
|
||
private Task _keepAliveTask;
|
||
|
||
private int _handle;
|
||
private long _lastCoinTime;
|
||
private int _inputToggle;
|
||
|
||
private bool _isAuthorized;
|
||
private bool _isOperational;
|
||
private bool _handshakeInProgress;
|
||
|
||
private string _requestHex = string.Empty;
|
||
private string _lastInputBits = DefaultInputBits;
|
||
private byte[] _aesKey32;
|
||
|
||
private HandshakeStage _handshakeStage = HandshakeStage.None;
|
||
private TaskCompletionSource<bool> _handshakeCompletion;
|
||
private TaskCompletionSource<string> _roleCompletion;
|
||
|
||
private enum HandshakeStage
|
||
{
|
||
None,
|
||
WaitingRaes,
|
||
WaitingChal,
|
||
WaitingReady,
|
||
Completed,
|
||
Failed
|
||
}
|
||
|
||
public string DeviceId { get; }
|
||
public string PortName { get; }
|
||
|
||
public bool IsOperational => _isOperational;
|
||
public string LastInputBits => _lastInputBits;
|
||
public string ControllerRole { get; private set; } = string.Empty;
|
||
|
||
public event Action<string> CoinAccepted;
|
||
public event Action<string, string> InputReceived;
|
||
public event Action<string> Authorized;
|
||
public event Action<string> Disconnected;
|
||
|
||
public ComDeviceClient(
|
||
string portName,
|
||
bool enableEncryptedHandshake,
|
||
int handshakeTimeoutMs,
|
||
bool keepAlive,
|
||
string roleRequestCommand,
|
||
string roleResponsePrefix,
|
||
int roleRequestTimeoutMs)
|
||
{
|
||
PortName = portName;
|
||
DeviceId = portName;
|
||
_enableEncryptedHandshake = enableEncryptedHandshake;
|
||
_handshakeTimeoutMs = Math.Max(500, handshakeTimeoutMs);
|
||
_keepAlive = keepAlive;
|
||
_roleRequestCommand = roleRequestCommand?.Trim() ?? string.Empty;
|
||
_roleResponsePrefix = string.IsNullOrWhiteSpace(roleResponsePrefix) ? "ROLE" : roleResponsePrefix.Trim();
|
||
_roleRequestTimeoutMs = Math.Max(200, roleRequestTimeoutMs);
|
||
|
||
if (_enableEncryptedHandshake)
|
||
{
|
||
_aesKey32 = BuildAesKey();
|
||
}
|
||
}
|
||
|
||
public bool Start()
|
||
{
|
||
if (!OpenPort()) return false;
|
||
|
||
_lifecycleCts = new CancellationTokenSource();
|
||
_pollTask = Task.Run(() => PollLoopAsync(_lifecycleCts.Token), _lifecycleCts.Token);
|
||
|
||
if (_enableEncryptedHandshake)
|
||
{
|
||
_ = RunHandshakeAsync(_lifecycleCts.Token);
|
||
}
|
||
else
|
||
{
|
||
MarkOperational();
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
public void Stop(bool sendShutdownCommands)
|
||
{
|
||
try
|
||
{
|
||
_lifecycleCts?.Cancel();
|
||
}
|
||
catch
|
||
{
|
||
// ignored
|
||
}
|
||
|
||
if (sendShutdownCommands)
|
||
{
|
||
SendRaw("2170", true);
|
||
SendRaw("2130", true);
|
||
}
|
||
|
||
lock (_ioLock)
|
||
{
|
||
if (_handle > 0)
|
||
{
|
||
ComNativeWrapper.Serial_Close(_handle);
|
||
_handle = 0;
|
||
}
|
||
}
|
||
|
||
_isOperational = false;
|
||
_isAuthorized = false;
|
||
_handshakeInProgress = false;
|
||
_handshakeStage = HandshakeStage.None;
|
||
_handshakeCompletion?.TrySetCanceled();
|
||
_roleCompletion?.TrySetCanceled();
|
||
ControllerRole = string.Empty;
|
||
|
||
Disconnected?.Invoke(DeviceId);
|
||
|
||
_lifecycleCts?.Dispose();
|
||
_lifecycleCts = null;
|
||
}
|
||
|
||
public void Send(string message)
|
||
{
|
||
SendRaw(message, false);
|
||
}
|
||
|
||
private bool OpenPort()
|
||
{
|
||
int handle = ComNativeWrapper.Serial_Open(PortName, 115200, 8, 0, 1, 5, 10);
|
||
if (handle <= 0)
|
||
{
|
||
GameManager.Logger?.LogError("ComDeviceClient", $"[{DeviceId}] Open failed: {ComNativeWrapper.GetLastError()}");
|
||
return false;
|
||
}
|
||
|
||
lock (_ioLock)
|
||
{
|
||
_handle = handle;
|
||
}
|
||
|
||
_lastCoinTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||
FlushPortBuffer();
|
||
return true;
|
||
}
|
||
|
||
private async Task PollLoopAsync(CancellationToken token)
|
||
{
|
||
while (!token.IsCancellationRequested)
|
||
{
|
||
try
|
||
{
|
||
await Task.Delay(1, token);
|
||
string line = ReadBufferLine();
|
||
if (string.IsNullOrEmpty(line)) continue;
|
||
ProcessLine(line);
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
break;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
GameManager.Logger?.LogError("ComDeviceClient", $"[{DeviceId}] Poll error: {e.Message}");
|
||
}
|
||
}
|
||
}
|
||
|
||
private void ProcessLine(string line)
|
||
{
|
||
line = line.Trim();
|
||
if (string.IsNullOrEmpty(line)) return;
|
||
|
||
if (_handshakeInProgress)
|
||
{
|
||
TryProcessHandshakeLine(line);
|
||
return;
|
||
}
|
||
|
||
if (!_isOperational) return;
|
||
|
||
if (TryHandleRoleLine(line))
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (line.StartsWith("INPUT", StringComparison.Ordinal))
|
||
{
|
||
_inputToggle = 1;
|
||
if (line.Length <= 5) return;
|
||
|
||
string rawData = line.Substring(5);
|
||
if (string.IsNullOrWhiteSpace(rawData)) return;
|
||
|
||
string bits = DecimalToBinary(rawData);
|
||
_lastInputBits = bits;
|
||
InputReceived?.Invoke(DeviceId, bits);
|
||
}
|
||
}
|
||
|
||
private async Task RunHandshakeAsync(CancellationToken token)
|
||
{
|
||
_handshakeInProgress = true;
|
||
_isAuthorized = false;
|
||
_isOperational = false;
|
||
_handshakeStage = HandshakeStage.WaitingRaes;
|
||
_handshakeCompletion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||
|
||
SendRaw("00", true);
|
||
await Task.Delay(1000, token);
|
||
|
||
_requestHex = GenerateRandomBlock16Hex();
|
||
SendRaw("07" + _requestHex, true);
|
||
|
||
Task timeoutTask = Task.Delay(_handshakeTimeoutMs, token);
|
||
Task finished = await Task.WhenAny(_handshakeCompletion.Task, timeoutTask);
|
||
|
||
if (finished == _handshakeCompletion.Task && _handshakeCompletion.Task.Result)
|
||
{
|
||
MarkOperational();
|
||
return;
|
||
}
|
||
|
||
FailHandshake("Таймаут рукопожатия");
|
||
}
|
||
|
||
private void TryProcessHandshakeLine(string line)
|
||
{
|
||
if (line.Equals("READY", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
if (_handshakeStage != HandshakeStage.WaitingReady)
|
||
{
|
||
FailHandshake($"READY в неверной фазе: {_handshakeStage}");
|
||
return;
|
||
}
|
||
|
||
_handshakeStage = HandshakeStage.Completed;
|
||
_isAuthorized = true;
|
||
_handshakeCompletion?.TrySetResult(true);
|
||
return;
|
||
}
|
||
|
||
if (line.StartsWith("RAES", StringComparison.Ordinal))
|
||
{
|
||
HandleRaes(line);
|
||
return;
|
||
}
|
||
|
||
if (line.StartsWith("CHAL", StringComparison.Ordinal))
|
||
{
|
||
HandleChal(line);
|
||
}
|
||
}
|
||
|
||
private void HandleRaes(string line)
|
||
{
|
||
try
|
||
{
|
||
if (_handshakeStage != HandshakeStage.WaitingRaes)
|
||
{
|
||
FailHandshake($"RAES в неверной фазе: {_handshakeStage}");
|
||
return;
|
||
}
|
||
|
||
if (line.Length < 36)
|
||
{
|
||
FailHandshake("Некорректная длина RAES");
|
||
return;
|
||
}
|
||
|
||
string hex = line.Substring(4, 32);
|
||
if (!IsHexString(hex, 32))
|
||
{
|
||
FailHandshake("RAES содержит некорректный HEX");
|
||
return;
|
||
}
|
||
|
||
string decrypted = DecryptHexBlock16(hex);
|
||
if (!string.Equals(decrypted, _requestHex, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
FailHandshake("RAES не совпадает с запросом 07");
|
||
return;
|
||
}
|
||
|
||
_handshakeStage = HandshakeStage.WaitingChal;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
FailHandshake($"Ошибка RAES: {e.Message}");
|
||
}
|
||
}
|
||
|
||
private void HandleChal(string line)
|
||
{
|
||
try
|
||
{
|
||
if (_handshakeStage != HandshakeStage.WaitingChal)
|
||
{
|
||
FailHandshake($"CHAL в неверной фазе: {_handshakeStage}");
|
||
return;
|
||
}
|
||
|
||
if (line.Length < 36)
|
||
{
|
||
FailHandshake("Некорректная длина CHAL");
|
||
return;
|
||
}
|
||
|
||
string hex = line.Substring(4, 32);
|
||
if (!IsHexString(hex, 32))
|
||
{
|
||
FailHandshake("CHAL содержит некорректный HEX");
|
||
return;
|
||
}
|
||
|
||
string encrypted = EncryptHexBlock16(hex);
|
||
SendRaw("08" + encrypted, true);
|
||
_handshakeStage = HandshakeStage.WaitingReady;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
FailHandshake($"Ошибка CHAL: {e.Message}");
|
||
}
|
||
}
|
||
|
||
private void MarkOperational()
|
||
{
|
||
_handshakeInProgress = false;
|
||
_isAuthorized = true;
|
||
_isOperational = true;
|
||
_handshakeStage = HandshakeStage.Completed;
|
||
ControllerRole = string.Empty;
|
||
|
||
SendRaw("2171", true);
|
||
|
||
if (_keepAlive)
|
||
{
|
||
_keepAliveTask = Task.Run(() => KeepAliveLoopAsync(_lifecycleCts.Token), _lifecycleCts.Token);
|
||
}
|
||
|
||
_ = Task.Run(() => ResolveRoleAfterAuthorizationAsync(_lifecycleCts.Token), _lifecycleCts.Token);
|
||
}
|
||
|
||
private void FailHandshake(string message)
|
||
{
|
||
_isAuthorized = false;
|
||
_isOperational = false;
|
||
_handshakeInProgress = false;
|
||
_handshakeStage = HandshakeStage.Failed;
|
||
_handshakeCompletion?.TrySetResult(false);
|
||
GameManager.Logger?.LogError("ComDeviceClient", $"[{DeviceId}] {message}");
|
||
}
|
||
|
||
private async Task KeepAliveLoopAsync(CancellationToken token)
|
||
{
|
||
while (!token.IsCancellationRequested)
|
||
{
|
||
try
|
||
{
|
||
await Task.Delay(2000, token);
|
||
|
||
if (!_isOperational)
|
||
{
|
||
await Task.Delay(8000, token);
|
||
continue;
|
||
}
|
||
|
||
SendRaw(RequestInput, false);
|
||
_inputToggle = 0;
|
||
|
||
await Task.Delay(1000, token);
|
||
if (_inputToggle == 0)
|
||
{
|
||
await ReconnectAsync(token);
|
||
}
|
||
|
||
await Task.Delay(7000, token);
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
break;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
GameManager.Logger?.LogError("ComDeviceClient", $"[{DeviceId}] KeepAlive error: {e.Message}");
|
||
}
|
||
}
|
||
}
|
||
|
||
private async Task ReconnectAsync(CancellationToken token)
|
||
{
|
||
_isOperational = false;
|
||
_isAuthorized = false;
|
||
_handshakeInProgress = false;
|
||
_handshakeStage = HandshakeStage.None;
|
||
_lastInputBits = DefaultInputBits;
|
||
_inputToggle = 0;
|
||
ControllerRole = string.Empty;
|
||
|
||
lock (_ioLock)
|
||
{
|
||
if (_handle > 0)
|
||
{
|
||
ComNativeWrapper.Serial_Close(_handle);
|
||
_handle = 0;
|
||
}
|
||
}
|
||
|
||
if (!OpenPort())
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (_enableEncryptedHandshake)
|
||
{
|
||
await RunHandshakeAsync(token);
|
||
}
|
||
else
|
||
{
|
||
MarkOperational();
|
||
}
|
||
}
|
||
|
||
private void FlushPortBuffer()
|
||
{
|
||
lock (_ioLock)
|
||
{
|
||
if (_handle <= 0) return;
|
||
|
||
byte[] localBuf = new byte[1024];
|
||
int flushedCount = 0;
|
||
|
||
int read;
|
||
while ((read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(_handle, localBuf, localBuf.Length)) > 0)
|
||
{
|
||
flushedCount++;
|
||
if (flushedCount > 100) break;
|
||
}
|
||
}
|
||
}
|
||
|
||
private string ReadBufferLine()
|
||
{
|
||
lock (_ioLock)
|
||
{
|
||
if (_handle <= 0) return string.Empty;
|
||
|
||
byte[] localBuf = new byte[1024];
|
||
int read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(_handle, localBuf, localBuf.Length);
|
||
if (read <= 0) return string.Empty;
|
||
|
||
if (read > localBuf.Length) read = localBuf.Length;
|
||
|
||
string line = Encoding.UTF8.GetString(localBuf, 0, read).Trim();
|
||
if (line != "COIN") return line;
|
||
|
||
long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||
if (now - _lastCoinTime < CoinDebounceMs) return string.Empty;
|
||
|
||
_lastCoinTime = now;
|
||
|
||
if (_isOperational)
|
||
{
|
||
ComNativeWrapper.Serial_WriteStringUTF8(_handle, "10", 1);
|
||
CoinAccepted?.Invoke(DeviceId);
|
||
}
|
||
|
||
return string.Empty;
|
||
}
|
||
}
|
||
|
||
private void SendRaw(string message, bool bypassAuthorization)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(message)) return;
|
||
if (!bypassAuthorization && !_isOperational) return;
|
||
|
||
lock (_ioLock)
|
||
{
|
||
if (_handle <= 0) return;
|
||
ComNativeWrapper.Serial_WriteStringUTF8(_handle, message, 1);
|
||
}
|
||
}
|
||
|
||
private bool TryHandleRoleLine(string line)
|
||
{
|
||
if (_roleCompletion == null) return false;
|
||
if (!_roleCompletion.Task.IsCompleted && TryExtractRole(line, out string role))
|
||
{
|
||
ControllerRole = role;
|
||
_roleCompletion.TrySetResult(role);
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private async Task ResolveRoleAfterAuthorizationAsync(CancellationToken token)
|
||
{
|
||
// Роль считываем только после завершенной авторизации рукопожатия.
|
||
if (!_enableEncryptedHandshake || !_isAuthorized || string.IsNullOrWhiteSpace(_roleRequestCommand))
|
||
{
|
||
Authorized?.Invoke(DeviceId);
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
_roleCompletion = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||
SendRaw(_roleRequestCommand, false);
|
||
|
||
Task timeoutTask = Task.Delay(_roleRequestTimeoutMs, token);
|
||
Task finished = await Task.WhenAny(_roleCompletion.Task, timeoutTask);
|
||
if (finished == _roleCompletion.Task &&
|
||
_roleCompletion.Task.IsCompletedSuccessfully &&
|
||
!string.IsNullOrWhiteSpace(_roleCompletion.Task.Result))
|
||
{
|
||
ControllerRole = _roleCompletion.Task.Result;
|
||
}
|
||
else
|
||
{
|
||
GameManager.Logger?.LogWarning("ComDeviceClient",
|
||
$"[{DeviceId}] Не удалось получить роль контроллера за {_roleRequestTimeoutMs}ms");
|
||
}
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
// ignore
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
GameManager.Logger?.LogError("ComDeviceClient", $"[{DeviceId}] Role resolve error: {e.Message}");
|
||
}
|
||
finally
|
||
{
|
||
Authorized?.Invoke(DeviceId);
|
||
}
|
||
}
|
||
|
||
private bool TryExtractRole(string line, out string role)
|
||
{
|
||
role = string.Empty;
|
||
if (string.IsNullOrWhiteSpace(line)) return false;
|
||
|
||
string trimmed = line.Trim();
|
||
if (!trimmed.StartsWith(_roleResponsePrefix, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
string tail = trimmed.Substring(_roleResponsePrefix.Length).Trim();
|
||
if (tail.StartsWith(":") || tail.StartsWith("="))
|
||
{
|
||
tail = tail.Substring(1).Trim();
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(tail)) return false;
|
||
role = tail;
|
||
return true;
|
||
}
|
||
|
||
private static string DecimalToBinary(string decimalString)
|
||
{
|
||
try
|
||
{
|
||
if (!long.TryParse(decimalString, out long value)) return DefaultInputBits;
|
||
|
||
string binary = Convert.ToString(value, 2);
|
||
char[] chars = binary.ToCharArray();
|
||
Array.Reverse(chars);
|
||
return new string(chars);
|
||
}
|
||
catch
|
||
{
|
||
return DefaultInputBits;
|
||
}
|
||
}
|
||
|
||
private static byte[] BuildAesKey()
|
||
{
|
||
byte[] key = new byte[32];
|
||
for (int i = 0; i < key.Length; i++)
|
||
{
|
||
key[i] = (byte)(AesEncryptedKey[i] ^ 0x37);
|
||
}
|
||
|
||
return key;
|
||
}
|
||
|
||
private string EncryptHexBlock16(string plainHex)
|
||
{
|
||
byte[] plain = HexToBytes(plainHex);
|
||
if (plain.Length != 16) throw new InvalidOperationException("Plain must be 16 bytes.");
|
||
return BytesToHex(Aes256EcbEncrypt(plain));
|
||
}
|
||
|
||
private string DecryptHexBlock16(string cipherHex)
|
||
{
|
||
byte[] cipher = HexToBytes(cipherHex);
|
||
if (cipher.Length != 16) throw new InvalidOperationException("Cipher must be 16 bytes.");
|
||
return BytesToHex(Aes256EcbDecrypt(cipher));
|
||
}
|
||
|
||
private byte[] Aes256EcbEncrypt(byte[] data)
|
||
{
|
||
using (Aes aes = Aes.Create())
|
||
{
|
||
aes.Key = _aesKey32;
|
||
aes.Mode = CipherMode.ECB;
|
||
aes.Padding = PaddingMode.None;
|
||
|
||
using (ICryptoTransform encryptor = aes.CreateEncryptor())
|
||
{
|
||
return encryptor.TransformFinalBlock(data, 0, data.Length);
|
||
}
|
||
}
|
||
}
|
||
|
||
private byte[] Aes256EcbDecrypt(byte[] data)
|
||
{
|
||
using (Aes aes = Aes.Create())
|
||
{
|
||
aes.Key = _aesKey32;
|
||
aes.Mode = CipherMode.ECB;
|
||
aes.Padding = PaddingMode.None;
|
||
|
||
using (ICryptoTransform decryptor = aes.CreateDecryptor())
|
||
{
|
||
return decryptor.TransformFinalBlock(data, 0, data.Length);
|
||
}
|
||
}
|
||
}
|
||
|
||
private static string GenerateRandomBlock16Hex()
|
||
{
|
||
byte[] buffer = new byte[16];
|
||
RandomNumberGenerator.Fill(buffer);
|
||
return BytesToHex(buffer);
|
||
}
|
||
|
||
private static byte[] HexToBytes(string hex)
|
||
{
|
||
if (hex == null) throw new ArgumentNullException(nameof(hex));
|
||
hex = hex.Trim();
|
||
|
||
if (hex.Length % 2 != 0)
|
||
{
|
||
throw new InvalidOperationException("Hex string length must be even.");
|
||
}
|
||
|
||
byte[] result = new byte[hex.Length / 2];
|
||
for (int i = 0; i < result.Length; i++)
|
||
{
|
||
result[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
private static string BytesToHex(byte[] bytes)
|
||
{
|
||
StringBuilder builder = new StringBuilder(bytes.Length * 2);
|
||
for (int i = 0; i < bytes.Length; i++)
|
||
{
|
||
builder.AppendFormat("{0:X2}", bytes[i]);
|
||
}
|
||
|
||
return builder.ToString();
|
||
}
|
||
|
||
private static bool IsHexString(string value, int expectedLength)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(value) || value.Length != expectedLength)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
for (int i = 0; i < value.Length; i++)
|
||
{
|
||
if (!Uri.IsHexDigit(value[i]))
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
}
|
||
}
|