Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
Система выдачи призов хоппером: - PrizeSystemManager теперь читает enableHopper из настройки prizes.giveSmallPrize вместо захардкоженного SerializeField (реагирует на изменения в реальном времени) - LevelFinish: исправлен ключ настройки на prizes.giveSmallPrize - Добавлен HopperCheckController для тестирования выдачи приза кнопкой в настройках - Проверка датчика хоппера перенесена в ComDeviceClient (2-й бит INPUT) - PrizeGivenBuffer заменён с PlayerPrefs на потокобезопасный Interlocked-буфер (исправлен краш при вызове PlayerPrefs из фонового потока платы) - Добавлен PrizeSystem.OnPrizeResult (Action<bool>) — событие результата выдачи Отображение ошибки выдачи приза: - Добавлен PrizeErrorDisplay — UI-компонент, подписанный на OnPrizeResult - Зависит от настройки prizes.showPrizeError (реагирует на изменения в реальном времени) QR-коды: - PrizeSystemManager подписан на изменение prizes.phoneNumber — QR-коды перегенерируются автоматически при смене номера - WinBannerController показывает QR-коды только при включённой prizes.giveBigPrize Прочее: - Добавлен Levitator — компонент плавной левитации объекта (высота, скорость, плавность) - Настройки: группы поддержки/тестов реорганизованы, добавлена кнопка btn.hopper_test
874 lines
30 KiB
C#
874 lines
30 KiB
C#
using System;
|
||
using System.Security.Cryptography;
|
||
using System.Text;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using RR.Game2D.Core.Managers;
|
||
using UnityEngine;
|
||
|
||
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 int _missedInputResponses;
|
||
|
||
private bool _isAuthorized;
|
||
private bool _isOperational;
|
||
private bool _handshakeInProgress;
|
||
|
||
private string _requestHex = string.Empty;
|
||
private string _lastHandshakeResponse = string.Empty;
|
||
private string _lastInputBits = DefaultInputBits;
|
||
private byte[] _aesKey32;
|
||
|
||
private HandshakeStage _handshakeStage = HandshakeStage.None;
|
||
private TaskCompletionSource<bool> _handshakeCompletion;
|
||
private TaskCompletionSource<bool> _chalReceivedCompletion;
|
||
private TaskCompletionSource<string> _roleCompletion;
|
||
private TaskCompletionSource<string> _macCompletion;
|
||
|
||
private enum HandshakeStage
|
||
{
|
||
None,
|
||
WaitingChal,
|
||
WaitingRaes,
|
||
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 string MacAddress { get; private set; } = string.Empty;
|
||
|
||
public event Action<string> CoinAccepted;
|
||
public event Action<string, string> InputReceived;
|
||
public event Action<string, string> RawLineReceived;
|
||
public event Action<string> Authorized;
|
||
public event Action<string> Disconnected;
|
||
public event Action<string> ConnectionLost;
|
||
|
||
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();
|
||
_chalReceivedCompletion?.TrySetCanceled();
|
||
_roleCompletion?.TrySetCanceled();
|
||
_macCompletion?.TrySetCanceled();
|
||
ControllerRole = string.Empty;
|
||
_keepAliveTask = null;
|
||
|
||
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 (TryHandleMacLine(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;
|
||
|
||
// Проверяем датчик хоппера: 2-й бит с начала в нормальном двоичном представлении
|
||
if (long.TryParse(rawData, out long rawValue))
|
||
{
|
||
string rawBinary = Convert.ToString(rawValue, 2);
|
||
if (rawBinary.Length >= 2 && rawBinary[1] == '0')
|
||
PrizeSystem.SignalPrizeGiven();
|
||
}
|
||
|
||
string bits = DecimalToBinary(rawData);
|
||
_lastInputBits = bits;
|
||
InputReceived?.Invoke(DeviceId, bits);
|
||
return;
|
||
}
|
||
|
||
RawLineReceived?.Invoke(DeviceId, line);
|
||
}
|
||
|
||
private async Task RunHandshakeAsync(CancellationToken token)
|
||
{
|
||
_handshakeInProgress = true;
|
||
_isAuthorized = false;
|
||
_isOperational = false;
|
||
_handshakeStage = HandshakeStage.WaitingChal;
|
||
_lastHandshakeResponse = string.Empty;
|
||
_handshakeCompletion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||
_chalReceivedCompletion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||
Debug.Log($"[COM AUTH][{DeviceId}] Handshake started. Stage={_handshakeStage}");
|
||
|
||
_requestHex = GenerateRandomBlock16Hex();
|
||
// Порядок handshake по протоколу платы:
|
||
// PC -> 07[random], board -> CHAL, PC -> 08[enc(CHAL)], board -> RAES, board -> AUTH_CNT_OK.
|
||
SendRaw("07" + _requestHex, true);
|
||
Debug.Log($"[COM AUTH][{DeviceId}] -> 07{_requestHex}");
|
||
|
||
// Если на 07 не получили ожидаемый CHAL быстро, сразу пропускаем устройство.
|
||
int chalFastTimeoutMs = Math.Min(1200, Math.Max(250, _handshakeTimeoutMs / 4));
|
||
Task chalFastTimeoutTask = Task.Delay(chalFastTimeoutMs, token);
|
||
Task chalFastFinished = await Task.WhenAny(_chalReceivedCompletion.Task, chalFastTimeoutTask);
|
||
if (chalFastFinished != _chalReceivedCompletion.Task)
|
||
{
|
||
Debug.LogWarning($"[COM AUTH][{DeviceId}] No CHAL after 07 in {chalFastTimeoutMs}ms. Skipping device.");
|
||
FailHandshake($"Нет ожидаемого CHAL на 07 за {chalFastTimeoutMs}ms");
|
||
return;
|
||
}
|
||
|
||
Task timeoutTask = Task.Delay(_handshakeTimeoutMs, token);
|
||
Task finished = await Task.WhenAny(_handshakeCompletion.Task, timeoutTask);
|
||
|
||
if (finished == _handshakeCompletion.Task && _handshakeCompletion.Task.Result)
|
||
{
|
||
Debug.Log($"[COM AUTH][{DeviceId}] Handshake completed successfully");
|
||
MarkOperational();
|
||
return;
|
||
}
|
||
|
||
GameManager.Logger.LogWarning($"COM AUTH [{DeviceId}]", $"Handshake timeout. Last board response: {_lastHandshakeResponse}");
|
||
FailHandshake($"Таймаут рукопожатия. Последний ответ платы: {_lastHandshakeResponse}");
|
||
}
|
||
|
||
private void TryProcessHandshakeLine(string line)
|
||
{
|
||
Debug.Log($"[COM AUTH][{DeviceId}] <- {line}; stage={_handshakeStage}");
|
||
_lastHandshakeResponse = line;
|
||
|
||
// Во время handshake обрабатываем только ожидаемые сообщения:
|
||
// WaitingChal -> CHAL
|
||
// WaitingRaes -> RAES
|
||
// Все остальные строки игнорируем.
|
||
if (_handshakeStage == HandshakeStage.WaitingChal)
|
||
{
|
||
if (line.StartsWith("CHAL", StringComparison.Ordinal))
|
||
{
|
||
HandleChal(line);
|
||
}
|
||
else
|
||
{
|
||
Debug.Log($"[COM AUTH][{DeviceId}] Ignored while WaitingChal: {line}");
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
if (_handshakeStage == HandshakeStage.WaitingRaes)
|
||
{
|
||
if (line.StartsWith("RAES", StringComparison.Ordinal))
|
||
{
|
||
HandleRaes(line);
|
||
}
|
||
else
|
||
{
|
||
Debug.Log($"[COM AUTH][{DeviceId}] Ignored while WaitingRaes: {line}");
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
Debug.Log($"[COM AUTH][{DeviceId}] Ignored in stage {_handshakeStage}: {line}");
|
||
}
|
||
|
||
private void HandleRaes(string line)
|
||
{
|
||
try
|
||
{
|
||
Debug.Log($"[COM AUTH][{DeviceId}] Handling RAES...");
|
||
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.Completed;
|
||
_isAuthorized = true;
|
||
Debug.Log($"[COM AUTH][{DeviceId}] RAES verified. Raw RAES={line}; decrypted={decrypted}; handshake complete");
|
||
_handshakeCompletion?.TrySetResult(true);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
FailHandshake($"Ошибка RAES: {e.Message}");
|
||
}
|
||
}
|
||
|
||
private void HandleChal(string line)
|
||
{
|
||
try
|
||
{
|
||
Debug.Log($"[COM AUTH][{DeviceId}] Handling CHAL...");
|
||
if (_handshakeStage != HandshakeStage.WaitingChal)
|
||
{
|
||
FailHandshake($"CHAL в неверной фазе: {_handshakeStage}");
|
||
return;
|
||
}
|
||
|
||
_chalReceivedCompletion?.TrySetResult(true);
|
||
|
||
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.WaitingRaes;
|
||
Debug.Log($"[COM AUTH][{DeviceId}] -> 08{encrypted}; stage={_handshakeStage}");
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
FailHandshake($"Ошибка CHAL: {e.Message}");
|
||
}
|
||
}
|
||
|
||
private void MarkOperational()
|
||
{
|
||
_handshakeInProgress = false;
|
||
_isAuthorized = true;
|
||
_isOperational = true;
|
||
_handshakeStage = HandshakeStage.Completed;
|
||
ControllerRole = string.Empty;
|
||
_missedInputResponses = 0;
|
||
Debug.Log($"[COM AUTH][{DeviceId}] Device marked operational");
|
||
|
||
SendRaw("2171", true);
|
||
SendRaw("890003", true);
|
||
SendRaw("8600", true);
|
||
|
||
if (_keepAlive && (_keepAliveTask == null || _keepAliveTask.IsCompleted))
|
||
{
|
||
_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);
|
||
Debug.LogWarning($"[COM AUTH][{DeviceId}] Handshake failed: {message}");
|
||
GameManager.Logger?.LogWarning("ComDeviceClient", $"[{DeviceId}] {message}");
|
||
|
||
// Если порт отвечает не по ожидаемому протоколу платы,
|
||
// отключаем устройство, чтобы менеджер выбрал другой порт.
|
||
Stop(false);
|
||
}
|
||
|
||
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)
|
||
{
|
||
_missedInputResponses++;
|
||
if (_missedInputResponses >= 3)
|
||
{
|
||
GameManager.Logger?.LogWarning("ComDeviceClient",
|
||
$"[{DeviceId}] Нет INPUT-ответа {_missedInputResponses} цикла подряд, выполняю reconnect");
|
||
_missedInputResponses = 0;
|
||
await ReconnectAsync(token);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
_missedInputResponses = 0;
|
||
}
|
||
|
||
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;
|
||
MacAddress = string.Empty;
|
||
|
||
ConnectionLost?.Invoke(DeviceId);
|
||
|
||
lock (_ioLock)
|
||
{
|
||
if (_handle > 0)
|
||
{
|
||
ComNativeWrapper.Serial_Close(_handle);
|
||
_handle = 0;
|
||
}
|
||
}
|
||
|
||
if (!OpenPort())
|
||
{
|
||
// Порт недоступен при переподключении: отключаем устройство и даем менеджеру
|
||
// переключиться на другие доступные порты.
|
||
Stop(false);
|
||
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 bool TryHandleMacLine(string line)
|
||
{
|
||
if (_macCompletion == null) return false;
|
||
if (!_macCompletion.Task.IsCompleted && TryExtractMac(line, out string mac))
|
||
{
|
||
MacAddress = mac;
|
||
_macCompletion.TrySetResult(mac);
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private static bool TryExtractMac(string line, out string mac)
|
||
{
|
||
mac = string.Empty;
|
||
if (string.IsNullOrWhiteSpace(line)) return false;
|
||
|
||
string trimmed = line.Trim();
|
||
|
||
// Ответ на "06" — голый MAC без двоеточий, 12 hex-символов
|
||
if (trimmed.Length == 12 && IsHexString(trimmed))
|
||
{
|
||
mac = trimmed.ToUpperInvariant();
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private static bool IsHexString(string s)
|
||
{
|
||
for (int i = 0; i < s.Length; i++)
|
||
{
|
||
char c = s[i];
|
||
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
private async Task ResolveRoleAfterAuthorizationAsync(CancellationToken token)
|
||
{
|
||
// Роль и MAC считываем только после завершенной авторизации рукопожатия.
|
||
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");
|
||
}
|
||
|
||
// --- Запрос MAC-адреса ---
|
||
_macCompletion = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||
SendRaw("06", false);
|
||
|
||
Task macTimeoutTask = Task.Delay(_roleRequestTimeoutMs, token);
|
||
Task macFinished = await Task.WhenAny(_macCompletion.Task, macTimeoutTask);
|
||
if (macFinished == _macCompletion.Task &&
|
||
_macCompletion.Task.IsCompletedSuccessfully &&
|
||
!string.IsNullOrWhiteSpace(_macCompletion.Task.Result))
|
||
{
|
||
MacAddress = _macCompletion.Task.Result;
|
||
GameManager.Logger?.Log("ComDeviceClient",
|
||
$"[{DeviceId}] MAC-адрес: {MacAddress}");
|
||
}
|
||
else
|
||
{
|
||
GameManager.Logger?.LogWarning("ComDeviceClient",
|
||
$"[{DeviceId}] Не удалось получить MAC-адрес за {_roleRequestTimeoutMs}ms");
|
||
}
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
// ignore
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
GameManager.Logger?.LogError("ComDeviceClient", $"[{DeviceId}] Role/MAC 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;
|
||
}
|
||
}
|
||
}
|