using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Events;
using Inputs.ComSystem;
///
/// Основной класс-обертка для работы с COM-портом и периферийной платой.
/// Управляет всей коммуникацией с аппаратным обеспечением: прием монет, чтение кнопок, управление подсветкой, отправка сигналов.
///
public class ComWrapper : MonoBehaviour
{
//Настраиваем из инспектора
[SerializeField] bool saveMoneyBetweenSessions;
[SerializeField] bool enablePrizeChecking = false; // Если хопер с проверкой наличия приза ставим true
[SerializeField] bool keepAlive = true; // Периодическая проверка соединения с платами
[SerializeField] bool requireHandshake = true; // Двойное рукопожатие AES
public static UnityEvent onCoinAdded;
public static UnityEvent onCoinRemoved;
public static UnityEvent onInputRecieved;
public static System.Action OnInitialize;
// Событие, вызываемое непосредственно перед закрытием портов — слушатели могут послать команды выключения
public static System.Action OnBeforeClose;
// Очередь исходящих сообщений — все отправки пойдут через неё и будут обработаны в Update() или при завершении
private struct OutgoingMessage { public int playerId; public string message; }
private static readonly System.Collections.Concurrent.ConcurrentQueue outgoingQueue = new System.Collections.Concurrent.ConcurrentQueue();
#region Приватные поля
private const string REQUEST_INPUT = "91";
private const string LIGHTSIGNAL_PREFIX = "80";
private static int handle1; //идентификатор хэндла платы 1
private static int handle2; //идентификатор хэндла платы 2
// Асинхронная работа
private CancellationTokenSource pollCancellationTokenSource;
private int inputToggle = 0; // Флаг для проверки получения INPUT от плат
// Флаг для безопасного запроса переподключения, выставляется из фонового потока и обрабатывается в Update() на главном потоке
private volatile bool needReinit = false;
// Очередь для обработки COIN в главном потоке
private static readonly System.Collections.Concurrent.ConcurrentQueue coinQueue = new System.Collections.Concurrent.ConcurrentQueue();
// Защита от спама COIN: отслеживание времени последней монеты для каждой платы
private static long lastCoinTimePlate1 = 0; // в миллисекундах
private static long lastCoinTimePlate2 = 0; // в миллисекундах
private const long COIN_DEBOUNCE_MS = 250; // Минимальный интервал между монетами (250ms, больше чем 200ms спам от платы)
private static byte[] buf = new byte[1024]; //Байт-буфер куда будут приходить данные с порта
// StringBuilder для эффективного формирования строки INPUT без лишних аллокаций
private static System.Text.StringBuilder inputBuilder = new System.Text.StringBuilder(32);
// Кэш последних состояний INPUT для каждой платы (26 бит от каждой платы)
// ВАЖНО: 0 = кнопка нажата, 1 = кнопка отпущена, поэтому по умолчанию все единицы
// Каждая плата присылает 26 бит, из них используются только 10:
// - первые 8 бит = кнопки (биты 0-7)
// - последние 2 бита = хопер (биты 24-25)
// Итоговая строка: 8 кнопок платы1 + 8 кнопок платы2 + 2 хопера платы1 + 2 хопера платы2 = 20 бит
private static string lastInputBits1 = "11111111111111111111111111"; // последние 26 бит от платы 1 (все отпущены)
private static string lastInputBits2 = "11111111111111111111111111"; // последние 26 бит от платы 2 (все отпущены)
private static int CreditsPlayer1;
private static int CreditsPlayer2;
#endregion
//===========================start: PUBLIC API===========================//
///
/// Инициализирует систему работы с COM-портом.
/// Открывает соединение, настраивает события и запускает асинхронные запросы состояния.
///
public void Initialize()
{
//Создаем ивенты
onCoinAdded = new UnityEvent();
onCoinRemoved = new UnityEvent();
onInputRecieved = new();
//Начальная настройка
if (!saveMoneyBetweenSessions)
{
PlayerPrefs.SetInt("Player1_Coins", 0);
PlayerPrefs.SetInt("Player2_Coins", 0);
}
CreditsPlayer1 = PlayerPrefs.GetInt("Player1_Coins", 0);
CreditsPlayer2 = PlayerPrefs.GetInt("Player2_Coins", 0);
initPort(); //открываем порты
// Запускаем асинхронный опрос портов
pollCancellationTokenSource = new CancellationTokenSource();
StartAsyncPolling();
// Запускаем keep-alive механизм если включен
if (keepAlive || enablePrizeChecking)
{
StartKeepAliveProcess();
}
OnInitialize?.Invoke();
}
///
/// Добавляет один кредит в систему и сохраняет обновленное количество.
/// Вызывается автоматически событием при получении сигнала "COIN" из COM-порта.
///
///
/// Увеличивает счетчик кредитов и вызывает событие onCoinAdded для уведомления подписчиков.
/// Автоматически сохраняет состояние в PlayerPrefs.
///
public static void AddCredit(int coins = 1, int playerId = 1)
{
if (playerId == 1)
{
CreditsPlayer1 += coins;
PlayerPrefs.SetInt("Player1_Coins", CreditsPlayer1);
onCoinAdded?.Invoke(playerId, CreditsPlayer1); // передаём номер игрока!
}
else if (playerId == 2)
{
CreditsPlayer2 += coins;
PlayerPrefs.SetInt("Player2_Coins", CreditsPlayer2);
onCoinAdded?.Invoke(playerId, CreditsPlayer2); // передаём номер игрока!
}
PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + 1);
PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + 1);
}
///
/// Отправляет произвольную строковую команду на периферийную плату через COM-порт.
///
/// Команда для отправки
///
/// Используется для отправки пользовательских команд на плату.
/// Команда должна быть в формате, ожидаемом прошивкой платы.
/// В случае ошибки отправки выводит предупреждение в DebugWindow.
///
public static void Send(string message, int playerId)
{
// Публикуем в очередь — реальные записи будут выполнены в Update() (на главном потоке)
if (playerId != 1 && playerId != 2)
{
Debug.LogWarning("ComWrapper.Send: неверный playerId");
return;
}
outgoingQueue.Enqueue(new OutgoingMessage { playerId = playerId, message = message });
}
///
/// Отправляет строковую команду на все подключенные платы (broadcast).
/// Простая перегрузка, минимальное и безопасное изменение.
///
public static void Send(string message)
{
// Broadcast: ставим в очередь для обеих плат
outgoingQueue.Enqueue(new OutgoingMessage { playerId = 1, message = message });
outgoingQueue.Enqueue(new OutgoingMessage { playerId = 2, message = message });
}
///
/// Выполняет списание платы за запуск игры.
/// Проверяет наличие доступных кредитов или режим бесплатной игры.
/// При успешном списании увеличивает счетчики запусков.
///
///
/// true - если списание прошло успешно (бесплатный режим или есть кредиты),
/// false - если кредитов недостаточно и платный режим
///
///
/// В случае успеха:
/// - Увеличивает общие счетчики запусков (key_starts и key_startsNul)
/// - При платном режиме уменьшает количество кредитов на 1
/// - Вызывает событие onCoinRemoved при списании кредита
///
public static bool Pay(int coins = 1, int playerId = 1)
{
if (SettingsManager.instance.GetSettingValue("Бесплатная игра") == "Вкл") return true;
ref int credits = ref (playerId == 1 ? ref CreditsPlayer1 : ref CreditsPlayer2);
string key = playerId == 1 ? "Player1_Coins" : "Player2_Coins";
if (credits > 0)
{
credits -= coins;
PlayerPrefs.SetInt(key, credits);
onCoinRemoved?.Invoke(playerId, credits);
return true;
}
return false;
}
///
/// Получаем баланс игрока по id
///
///
public static int GetPlayerCredits(int playerId = 1)
{
return playerId == 1 ? CreditsPlayer1 : CreditsPlayer2;
}
//===========================end: PUBLIC API===========================//
private void Update()
{
// Выполняем переподключение на главном потоке, если было запрошено из фонового
if (needReinit)
{
needReinit = false;
Debug.LogWarning("ComWrapper: выполняю переподключение портов на главном потоке...");
ClosePort();
initPort();
}
// Обрабатываем COIN из очереди в главном потоке
int coinsPlayer1 = 0;
int coinsPlayer2 = 0;
while (coinQueue.TryDequeue(out int playerId))
{
AddCredit(1, playerId);
if (playerId == 1) coinsPlayer1++;
else coinsPlayer2++;
}
if (coinsPlayer1 > 0 || coinsPlayer2 > 0)
{
Debug.Log($"✓ Обработано монет: Игрок1={coinsPlayer1}, Игрок2={coinsPlayer2}, Итого={coinsPlayer1 + coinsPlayer2}");
}
// Отправляем накопленные исходящие сообщения (выходящие команды на платы)
ProcessOutgoingQueueOnce();
}
// Отправляет все сообщения, которые есть в очереди в данный момент (синхронно, на главном потоке)
private void ProcessOutgoingQueueOnce()
{
while (outgoingQueue.TryDequeue(out OutgoingMessage msg))
{
try
{
if (msg.playerId == 1 && handle1 > 0) ComNativeWrapper.Serial_WriteStringUTF8(handle1, msg.message, 1);
else if (msg.playerId == 2 && handle2 > 0) ComNativeWrapper.Serial_WriteStringUTF8(handle2, msg.message, 1);
}
catch (Exception ex)
{
Debug.LogWarning($"ComWrapper: ошибка отправки исходящего сообщения: {ex.Message}");
}
}
}
private void OnDisable()
{
try { OnBeforeClose?.Invoke(); } catch { }
// Даем шанс отправить накопленные команды (например, TurnAllOff). Ждем короткий таймаут.
try
{
var sw = System.Diagnostics.Stopwatch.StartNew();
const int maxWaitMs = 200; // ждем до 200 ms
while (!outgoingQueue.IsEmpty && sw.ElapsedMilliseconds < maxWaitMs)
{
// Обрабатываем очередь синхронно
ProcessOutgoingQueueOnce();
System.Threading.Thread.Sleep(10);
}
}
catch (Exception) { }
StopAsyncPolling();
ClosePort();
}
private void OnApplicationQuit()
{
try { OnBeforeClose?.Invoke(); } catch { }
try
{
var sw = System.Diagnostics.Stopwatch.StartNew();
const int maxWaitMs = 200;
while (!outgoingQueue.IsEmpty && sw.ElapsedMilliseconds < maxWaitMs)
{
ProcessOutgoingQueueOnce();
System.Threading.Thread.Sleep(10);
}
}
catch (Exception) { }
StopAsyncPolling();
ClosePort();
}
private void StopAsyncPolling()
{
// Только отменяем токен; не вызываем Dispose здесь — StartAsyncPolling() выполнит очистку.
try
{
pollCancellationTokenSource?.Cancel();
}
catch (Exception ex)
{
Debug.LogWarning($"StopAsyncPolling: ошибка при Cancel: {ex.Message}");
}
}
private async void StartAsyncPolling()
{
Debug.Log("Запуск асинхронного опроса портов");
// Локальная ссылка на CTS
var cts = pollCancellationTokenSource;
try
{
while (true)
{
// Всегда берём свежую ссылку в начале итерации
cts = pollCancellationTokenSource;
if (cts == null) break;
// Безопасно получаем токен: доступ к cts.Token может выбросить ObjectDisposedException
CancellationToken token;
try
{
token = cts.Token;
}
catch (ObjectDisposedException)
{
Debug.LogWarning("StartAsyncPolling: CTS disposed при попытке получить Token — выходим из цикла.");
break;
}
if (token.IsCancellationRequested) break;
try
{
// Используем локальный токен для задержек и чтения
await Task.Delay(10, token); // Опрос каждые 10ms
string comInput = await ReadBufferLineAsync(token);
if (!string.IsNullOrEmpty(comInput))
{
try
{
ProcessComInputImmediate(comInput);
}
catch (Exception ex)
{
Debug.LogError($"Ошибка обработки COM-входа: {ex}\nInput=[{comInput}]");
}
}
}
catch (OperationCanceledException)
{
// токен отменён — корректный выход из цикла
break;
}
catch (ObjectDisposedException odex)
{
Debug.LogWarning($"StartAsyncPolling: CancellationTokenSource был disposed во время ожидания: {odex.Message}");
break;
}
catch (Exception ex)
{
// Если источник был disposed — не считать это критической ошибкой
if (ex is ObjectDisposedException)
{
Debug.LogWarning($"StartAsyncPolling: ObjectDisposedException (обработано): {ex.Message}");
break;
}
Debug.LogError($"Внутренняя ошибка опроса порта: {ex}");
}
}
}
finally
{
// Убедимся, что мы корректно освободим CTS и обнулим поле только если оно всё ещё ссылается на тот объект
try
{
var tmp = pollCancellationTokenSource;
if (tmp != null)
{
try { tmp.Cancel(); } catch { }
try { tmp.Dispose(); } catch { }
}
}
finally
{
pollCancellationTokenSource = null;
Debug.Log("StartAsyncPolling: завершён и pollCancellationTokenSource очищен");
}
}
}
private void ProcessComInputImmediate(string comInput)
{
if (string.IsNullOrEmpty(comInput)) return;
Debug.Log(comInput);
// INPUT обработка
if (comInput.StartsWith("INPUT"))
{
System.Threading.Interlocked.Exchange(ref inputToggle, 1); // потокобезопасно выставляем флаг для keep-alive
if (comInput.Length > 5)
onInputRecieved?.Invoke(comInput.Substring(5));
else
onInputRecieved?.Invoke(string.Empty);
}
}
private void ToggleInputReceived(int value) => inputToggle = value;
private int GetInputToggle() => inputToggle;
#region Приватные методы
//Ну тут по дебаг логам все должно быть понятно
//Если интересно, то h - это хэндл. Что то на плюсовом, типа ID соединения или чтото. Если 0 то нету соед. если меньше то ошибка, больше 0 - норм.
private void initPort()
{
Debug.Log("Старт программы");
ComNativeWrapper.Serial_CloseAll();
var ports = ComNativeWrapper.ListPorts();
Debug.Log("Ports: " + string.Join(", ", ports));
if (ports.Length == 0) { Debug.Log("Нет портов, завершаю"); return; }
// НОВАЯ ПРОВЕРКА: нужно минимум 2 порта
if (ports.Length < 2)
{
Debug.Log($"ОШИБКА: Найден только {ports.Length} порт, а нужно минимум 2 для работы с двумя платами!");
return;
}
string port1 = ports[^1];
string port2 = ports[^2];
Debug.Log("Открываю первый с конца порт: " + port1);
Debug.Log("Открываю второй с конца порт: " + port2);
//вот эти настройки лучше не ковырять, но если очень хочется можно поменять readTimeoutMs и writeTimeoutMs
//Если read поставить слишком маленьким оно проебует кредиты когда они идут большой пачкой например при оплате через вендисту.
//Там в порт спамит "COIN" в больших количествах и процентов 10 из этих строк не успевают прочитаться при таймауте 1мс, соотв. монеты проебуются.
//Слишком большой таймаут будет тормозить игру потому что оно все выполняется синхронно и каждый кадр забирает столько мс сколько в переменной указано.
handle1 = ComNativeWrapper.Serial_Open(port1, 115200, 8, 0, 1, 5, 10);
handle2 = ComNativeWrapper.Serial_Open(port2, 115200, 8, 0, 1, 5, 10);
Debug.Log($"handle: {handle1}");
if (handle1 <= 0)
{
Debug.Log("Open failed: " + ComNativeWrapper.GetLastError());
return;
}
Debug.Log($"handle: {handle2}");
if (handle2 <= 0)
{
Debug.Log("Open failed: " + ComNativeWrapper.GetLastError());
return;
}
// ВАЖНО: Очищаем буферы портов от старых данных
Debug.Log("Очистка буферов портов от старых данных...");
FlushPortBuffers();
// Инициализируем lastCoinTime текущим временем чтобы не обработать старые COIN
long currentTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
lastCoinTimePlate1 = currentTime;
lastCoinTimePlate2 = currentTime;
Debug.Log($"Порты инициализированы, debounce активирован с текущего времени: {currentTime}");
// Выполняем handshake если требуется
if (requireHandshake)
{
PerformHandshakeForBothBoards(port1, port2);
}
}
///
/// Очищает буферы обоих портов от накопившихся старых данных
///
private static void FlushPortBuffers()
{
int flushedCount1 = 0;
int flushedCount2 = 0;
// Читаем все накопившиеся данные из порта 1
while (ComNativeWrapper.Serial_ReadLineUTF8_bytes(handle1, buf, buf.Length) > 0)
{
flushedCount1++;
if (flushedCount1 > 100) break; // Защита от бесконечного цикла
}
// Читаем все накопившиеся данные из порта 2
while (ComNativeWrapper.Serial_ReadLineUTF8_bytes(handle2, buf, buf.Length) > 0)
{
flushedCount2++;
if (flushedCount2 > 100) break; // Защита от бесконечного цикла
}
Debug.Log($"Очищено: Плата1={flushedCount1} сообщений, Плата2={flushedCount2} сообщений");
}
private static void ClosePort()
{
if (handle1 > 0)
{
ComNativeWrapper.Serial_Close(handle1);
handle1 = 0;
}
if (handle2 > 0)
{
ComNativeWrapper.Serial_Close(handle2);
handle2 = 0;
}
}
///
/// Конвертирует десятичное число в двоичную строку фиксированной длины (26 бит)
///
private static string DecimalToBinary(string decimalString)
{
if (int.TryParse(decimalString, out int decimalValue))
{
// Конвертируем в двоичную строку и дополняем нулями слева до 26 символов
string binary = Convert.ToString(decimalValue, 2).PadLeft(26, '0');
// Переворачиваем строку (младшие биты слева, старшие справа)
char[] chars = binary.ToCharArray();
Array.Reverse(chars);
string reversed = new string(chars);
return reversed;
}
// Если не удалось распарсить - возвращаем строку как есть (для обратной совместимости)
Debug.LogWarning($"Не удалось преобразовать '{decimalString}' в число. Используем как есть.");
return decimalString.PadLeft(26, '1'); // По умолчанию все кнопки отпущены
}
//Чтение с ДВУХ плат и объединение битов INPUT с использованием кэша
//COIN обрабатывается сразу, чтобы не потерять монеты с обеих плат
private static string ReadBufferLine()
{
try
{
string result1 = "";
string result2 = "";
bool hasNewInput1 = false;
bool hasNewInput2 = false;
// Читаем с первой платы (8 кнопок + 2 хопера)
int read1 = ComNativeWrapper.Serial_ReadLineUTF8_bytes(handle1, buf, buf.Length);
if (read1 > 0)
{
result1 = Encoding.UTF8.GetString(buf, 0, read1);
// Обрабатываем COIN сразу, но добавляем в очередь для обработки в главном потоке
if (result1 == "COIN")
{
long currentTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
long timeSinceLastCoin = currentTime - lastCoinTimePlate1;
// Проверяем debounce: если прошло меньше 250ms - это спам от той же монеты
if (timeSinceLastCoin < COIN_DEBOUNCE_MS)
{
Debug.Log($"COM1: COIN проигнорирован (спам через {timeSinceLastCoin}ms < 250ms) - подтверждение НЕ отправлено");
// НЕ отправляем подтверждение для спама!
// НЕ обновляем lastCoinTime для спама - это позволит следующей реальной монете пройти
}
else
{
// Это РЕАЛЬНАЯ новая монета - обрабатываем
lastCoinTimePlate1 = currentTime;
// ВАЖНО: Отправляем подтверждение ТОЛЬКО для реальных монет!
int confirmSent = ComNativeWrapper.Serial_WriteStringUTF8(handle1, "10", 1);
Debug.Log($"COM1: COIN принят → Подтверждение '10' отправлено (bytes: {confirmSent})");
// Добавляем в очередь для обработки в главном потоке
coinQueue.Enqueue(1);
}
}
// Если пришёл INPUT - обновляем кэш
else if (result1.StartsWith("INPUT"))
{
string rawData = result1.Substring(5);
// Конвертируем десятичное число в двоичную строку
lastInputBits1 = DecimalToBinary(rawData);
hasNewInput1 = true;
}
}
// Читаем со второй платы (8 кнопок + 2 хопера)
int read2 = ComNativeWrapper.Serial_ReadLineUTF8_bytes(handle2, buf, buf.Length);
if (read2 > 0)
{
result2 = Encoding.UTF8.GetString(buf, 0, read2);
// Обрабатываем COIN сразу, но добавляем в очередь для обработки в главном потоке
if (result2 == "COIN")
{
long currentTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
long timeSinceLastCoin = currentTime - lastCoinTimePlate2;
// Проверяем debounce: если прошло меньше 250ms - это спам от той же монеты
if (timeSinceLastCoin < COIN_DEBOUNCE_MS)
{
Debug.Log($"COM2: COIN проигнорирован (спам через {timeSinceLastCoin}ms < 250ms) - подтверждение НЕ отправлено");
// НЕ отправляем подтверждение для спама!
// НЕ обновляем lastCoinTime для спама - это позволит следующей реальной монете пройти
}
else
{
// Это РЕАЛЬНАЯ новая монета - обрабатываем
lastCoinTimePlate2 = currentTime;
// ВАЖНО: Отправляем подтверждение ТОЛЬКО для реальных монет!
int confirmSent = ComNativeWrapper.Serial_WriteStringUTF8(handle2, "10", 1);
Debug.Log($"COM2: COIN принят → Подтверждение '10' отправлено (bytes: {confirmSent})");
// Добавляем в очередь для обработки в главном потоке
coinQueue.Enqueue(2);
}
}
// Если пришёл INPUT - обновляем кэш
else if (result2.StartsWith("INPUT"))
{
string rawData = result2.Substring(5);
// Конвертируем десятичное число в двоичную строку
lastInputBits2 = DecimalToBinary(rawData);
hasNewInput2 = true;
}
}
// Если хотя бы с одной платы пришёл INPUT - объединяем с кэшем
if (hasNewInput1 || hasNewInput2)
{
// Берём нужные биты из каждой платы (из 26 бит):
// - от платы 1: первые 8 бит (кнопки 0-7)
// - от платы 2: первые 8 бит (кнопки 8-15)
// - от платы 1: последние 2 бита (хопер 16-17, биты 24-25)
// - от платы 2: последние 2 бита (хопер 18-19, биты 24-25)
// Итого: 20 бит (8+8+2+2)
string buttons1 = lastInputBits1.Substring(0, 8); // биты 0-7: кнопки платы 1
string buttons2 = lastInputBits2.Substring(0, 8); // биты 8-15: кнопки платы 2
string hopper1 = lastInputBits1.Substring(24, 2); // биты 16-17: хопер платы 1 (последние 2 из 26)
string hopper2 = lastInputBits2.Substring(24, 2); // биты 18-19: хопер платы 2 (последние 2 из 26)
// Формируем строку эффективно через StringBuilder
inputBuilder.Clear();
inputBuilder.Append("INPUT");
inputBuilder.Append(buttons1);
inputBuilder.Append(buttons2);
inputBuilder.Append(hopper1);
inputBuilder.Append(hopper2);
string combined = inputBuilder.ToString();
return combined;
}
}
catch (Exception ex)
{
Debug.LogError($"Ошибка в ReadBufferLine: {ex}");
}
return "";
}
///
/// Асинхронное чтение с двух портов. Выполняется в фоновом потоке.
///
private static async Task ReadBufferLineAsync(CancellationToken cancellationToken = default)
{
try
{
return await Task.Run(() =>
{
if (cancellationToken.IsCancellationRequested)
return string.Empty;
return ReadBufferLine();
}, cancellationToken);
}
catch (OperationCanceledException)
{
return string.Empty;
}
catch (Exception e)
{
Debug.LogError($"Ошибка асинхронного чтения портов: {e.Message}");
return string.Empty;
}
}
//просим прислать состояние цифровых входов. Приходит что то вроде "INPUT111111101"
private void requestInputs() => Send(REQUEST_INPUT);
///
/// Асинхронный keep-alive процесс для проверки соединения с платами
///
private async void StartKeepAliveProcess()
{
Debug.Log("Запуск асинхронной проверки соединения");
var cts = pollCancellationTokenSource;
try
{
while (true)
{
// Всегда берем свежую ссылку
cts = pollCancellationTokenSource;
if (cts == null) break;
CancellationToken token;
try { token = cts.Token; }
catch (ObjectDisposedException) { Debug.LogWarning("StartKeepAliveProcess: CTS disposed — выхожу."); break; }
if (token.IsCancellationRequested) break;
try
{
await Task.Delay(2000, token); // Ждем 2 секунды
// Запрашиваем INPUT и сбрасываем флаг ожидания
requestInputs();
System.Threading.Interlocked.Exchange(ref inputToggle, 0);
// Ждём ответа (1 сек)
await Task.Delay(1000, token);
// Проверяем, пришёл ли ответ (потокобезопасно)
if (System.Threading.Volatile.Read(ref inputToggle) == 0)
{
Debug.LogWarning("ComWrapper: платы не отвечают — устанавливаю запрос на переподключение (будет выполнено в Update).");
needReinit = true;
}
await Task.Delay(7000, token); // Итого ~10 секунд между проверками
}
catch (OperationCanceledException)
{
break;
}
catch (ObjectDisposedException)
{
Debug.LogWarning("StartKeepAliveProcess: CancellationTokenSource disposed — выхожу.");
break;
}
catch (Exception e)
{
Debug.LogError($"Ошибка проверки соединения: {e.Message}");
}
}
}
finally
{
// ничего не диспозим здесь — StopAsyncPolling/StartAsyncPolling управляют жизненным циклом CTS
}
}
///
/// Выполняет двойное рукопожатие для обеих плат через ComNativeWrapper
///
private void PerformHandshakeForBothBoards(string port1Name, string port2Name)
{
Debug.Log("=== НАЧАЛО ДВОЙНОГО РУКОПОЖАТИЯ ===");
bool success1 = PerformHandshakeSingle(handle1, 1);
bool success2 = PerformHandshakeSingle(handle2, 2);
if (success1 && success2)
{
Debug.Log("=== ОБЕ ПЛАТЫ АВТОРИЗОВАНЫ ===");
}
else
{
Debug.LogError($"=== HANDSHAKE ПРОВАЛЕН: Плата1={success1}, Плата2={success2} ===");
}
}
///
/// Выполняет handshake для одной платы (07→CHAL→08→RAES)
///
private bool PerformHandshakeSingle(int handle, int plateId)
{
if (handle <= 0)
{
Debug.LogError($"Handshake [{plateId}]: невалидный handle={handle}");
return false;
}
try
{
// 1. Генерируем случайный блок 16 байт
string randomHex = AESCrypto.GenerateRandomBlock16Hex();
string cmd1 = "07" + randomHex + "\r\n";
Debug.Log($"Handshake [{plateId}]: → 07+random...");
ComNativeWrapper.Serial_WriteStringUTF8(handle, cmd1, 1);
// 2. Ждем ответ CHAL
string chalResponse = WaitForResponse(handle, "CHAL", 3000);
if (string.IsNullOrEmpty(chalResponse) || chalResponse.Length < 36)
{
Debug.LogWarning($"Handshake [{plateId}]: нет ответа CHAL");
return false;
}
string serverChalHex = chalResponse.Substring(4, 32);
Debug.Log($"Handshake [{plateId}]: ← CHAL={serverChalHex.Substring(0, 8)}...");
// 3. Шифруем и отправляем 08+encrypted
string encryptedChal = AESCrypto.EncryptHexBlock16(serverChalHex);
string cmd2 = "08" + encryptedChal + "\r\n";
Debug.Log($"Handshake [{plateId}]: → 08+encrypted...");
ComNativeWrapper.Serial_WriteStringUTF8(handle, cmd2, 1);
// 4. Ждем ответ RAES
string raesResponse = WaitForResponse(handle, "RAES", 3000);
if (string.IsNullOrEmpty(raesResponse) || raesResponse.Length < 36)
{
Debug.LogWarning($"Handshake [{plateId}]: нет ответа RAES");
return false;
}
string raesHex = raesResponse.Substring(4, 32);
Debug.Log($"Handshake [{plateId}]: ← RAES={raesHex.Substring(0, 8)}...");
// 5. Расшифровываем и проверяем
string decryptedRaes = AESCrypto.DecryptHexBlock16(raesHex);
if (string.Equals(decryptedRaes, randomHex, StringComparison.OrdinalIgnoreCase))
{
Debug.Log($"Handshake [{plateId}]: ✓ УСПЕХ");
return true;
}
else
{
Debug.LogError($"Handshake [{plateId}]: ✗ Проверка провалена! Expected={randomHex.Substring(0, 8)}..., Got={decryptedRaes.Substring(0, 8)}...");
return false;
}
}
catch (Exception ex)
{
Debug.LogError($"Handshake [{plateId}]: Exception: {ex.Message}");
return false;
}
}
///
/// Ожидает ответ с определенным префиксом от платы
///
private string WaitForResponse(int handle, string prefix, int timeoutMs)
{
float elapsed = 0;
byte[] localBuf = new byte[1024];
while (elapsed < timeoutMs)
{
int read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(handle, localBuf, localBuf.Length);
if (read > 0)
{
string line = Encoding.UTF8.GetString(localBuf, 0, read).Trim();
if (line.StartsWith(prefix))
{
return line;
}
}
System.Threading.Thread.Sleep(10);
elapsed += 10;
}
return null;
}
#endregion
}