990 lines
47 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Events;
using Inputs.ComSystem;
/// <summary>
/// Основной класс-обертка для работы с COM-портом и периферийной платой.
/// Управляет всей коммуникацией с аппаратным обеспечением: прием монет, чтение кнопок, управление подсветкой, отправка сигналов.
/// </summary>
public class ComWrapper : MonoBehaviour
{
//Настраиваем из инспектора
[SerializeField] bool enablePrizeChecking = false; // Если хопер с проверкой наличия приза ставим true
[SerializeField] bool keepAlive = true; // Периодическая проверка соединения с платами
[SerializeField] bool requireHandshake = true; // Двойное рукопожатие AES
// Монеты и кредиты — см. CreditSystem.cs (работает в любом режиме ввода)
public static UnityEvent<string> 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<OutgoingMessage> outgoingQueue = new System.Collections.Concurrent.ConcurrentQueue<OutgoingMessage>();
#region Приватные поля
private const string REQUEST_INPUT = "91";
private const string LIGHTSIGNAL_PREFIX = "80";
private static int handle1; //идентификатор хэндла платы 1
private static int handle2; //идентификатор хэндла платы 2
private static int handle3; //идентификатор хэндла платы 3 (опциональная витрина)
// Роль-ориентированные хэндлы (заполняются в AssignRoles после инициализации портов)
private static int handleLeft = 0; // левая игровая плата (player 1)
private static int handleRight = 0; // правая игровая плата (player 2)
private static int handleVitrine = 0; // витрина / showcase (опционально)
// Константы ролей — должны совпадать с тем, что плата возвращает на команду 97
// Ответ платы: "ROLE : <название>" (до 20 символов, регистр игнорируется)
public const string ROLE_LEFT = "left";
public const string ROLE_RIGHT = "right";
public const string ROLE_VITRINE = "vitrina";
private const string CMD_GET_ROLE = "97"; // команда запроса роли платы
// Асинхронная работа
private CancellationTokenSource pollCancellationTokenSource;
private int inputToggle = 0; // Флаг для проверки получения INPUT от плат
// Флаг для безопасного запроса переподключения, выставляется из фонового потока и обрабатывается в Update() на главном потоке
private volatile bool needReinit = false;
// Очередь для обработки COIN в главном потоке
private static readonly System.Collections.Concurrent.ConcurrentQueue<int> coinQueue = new System.Collections.Concurrent.ConcurrentQueue<int>();
// Защита от спама 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 (все отпущены)
#endregion
//===========================start: PUBLIC API===========================//
/// <summary>
/// Инициализирует систему работы с COM-портом.
/// Открывает соединение, настраивает события и запускает асинхронные запросы состояния.
/// </summary>
public void Initialize()
{
//Создаем ивенты
onInputRecieved = new();
initPort(); //открываем порты
// Запускаем асинхронный опрос портов
pollCancellationTokenSource = new CancellationTokenSource();
StartAsyncPolling();
// Запускаем keep-alive механизм если включен
if (keepAlive || enablePrizeChecking)
{
StartKeepAliveProcess();
}
OnInitialize?.Invoke();
}
/// <summary>
/// Добавляет один кредит в систему и сохраняет обновленное количество.
/// Вызывается автоматически событием при получении сигнала "COIN" из COM-порта.
/// </summary>
/// <remarks>
/// Вся логика кредитов перенесена в CreditSystem.cs.
/// Оставлено только для совместимости — удалено.
/// </remarks>
/// <summary>
/// Отправляет произвольную строковую команду на периферийную плату через COM-порт.
/// </summary>
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 });
}
/// <summary>
/// Отправляет строковую команду на все подключенные платы (broadcast).
/// </summary>
public static void Send(string message)
{
outgoingQueue.Enqueue(new OutgoingMessage { playerId = 1, message = message });
outgoingQueue.Enqueue(new OutgoingMessage { playerId = 2, message = message });
}
/// <summary>
/// Возвращает true, если витрина (showcase-плата) была обнаружена и подключена.
/// </summary>
public static bool HasVitrine => handleVitrine > 0;
/// <summary>
/// Отправляет команду на витрину (showcase-плату).
/// Если витрина не обнаружена — выводит предупреждение и ничего не делает.
/// </summary>
public static void SendToVitrine(string message)
{
if (handleVitrine > 0)
outgoingQueue.Enqueue(new OutgoingMessage { playerId = 3, message = message });
else
Debug.LogWarning("ComWrapper.SendToVitrine: витрина не подключена или роль не определена.");
}
/// <summary>
/// Возвращает хэндл платы по её строковой роли (используй ROLE_LEFT / ROLE_RIGHT / ROLE_VITRINE).
/// Возвращает 0, если плата с такой ролью не найдена.
/// </summary>
public static int GetHandleByRole(string role)
{
return role switch
{
ROLE_LEFT => handleLeft,
ROLE_RIGHT => handleRight,
ROLE_VITRINE => handleVitrine,
_ => 0
};
}
//===========================end: PUBLIC API===========================//
private void Update()
{
// Выполняем переподключение на главном потоке, если было запрошено из фонового
if (needReinit)
{
needReinit = false;
Debug.LogWarning("ComWrapper: выполняю переподключение портов на главном потоке...");
ClosePort();
initPort();
}
// Обрабатываем COIN из очереди — делегируем в CreditSystem
int coinsPlayer1 = 0;
int coinsPlayer2 = 0;
while (coinQueue.TryDequeue(out int playerId))
{
CreditSystem.AddImpulse(playerId);
if (playerId == 1) coinsPlayer1++;
else coinsPlayer2++;
}
if (coinsPlayer1 > 0 || coinsPlayer2 > 0)
{
Debug.Log($"<color=green>✓ Обработано монет: Игрок1={coinsPlayer1}, Игрок2={coinsPlayer2}, Итого={coinsPlayer1 + coinsPlayer2}</color>");
}
// Отправляем накопленные исходящие сообщения (выходящие команды на платы)
ProcessOutgoingQueueOnce();
}
// Отправляет все сообщения, которые есть в очереди в данный момент (синхронно, на главном потоке)
private void ProcessOutgoingQueueOnce()
{
while (outgoingQueue.TryDequeue(out OutgoingMessage msg))
{
try
{
// Используем роль-алиасы; если ещё не назначены — fallback на raw handle1/handle2
int targetHandle = msg.playerId switch
{
1 => handleLeft > 0 ? handleLeft : handle1, // левая / player1
2 => handleRight > 0 ? handleRight : handle2, // правая / player2
3 => handleVitrine, // витрина (только через SendToVitrine)
_ => 0
};
if (targetHandle > 0)
ComNativeWrapper.Serial_WriteStringUTF8(targetHandle, 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($"<color=red>ОШИБКА: Найден только {ports.Length} порт, а нужно минимум 2 для работы с двумя платами!</color>");
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;
}
// Опционально открываем 3-й порт (возможная витрина)
handle3 = 0;
if (ports.Length >= 3)
{
string port3 = ports[^3];
Debug.Log($"<color=cyan>Найден 3-й порт, открываю (возможная витрина): {port3}</color>");
handle3 = ComNativeWrapper.Serial_Open(port3, 115200, 8, 0, 1, 5, 10);
if (handle3 <= 0)
{
Debug.LogWarning($"Не удалось открыть порт {port3} (handle3={handle3}), витрина недоступна.");
handle3 = 0;
}
else
{
Debug.Log($"<color=cyan>handle3 (возможная витрина): {handle3}</color>");
}
}
// ВАЖНО: Очищаем буферы портов от старых данных
Debug.Log("<color=yellow>Очистка буферов портов от старых данных...</color>");
FlushPortBuffers();
// Инициализируем lastCoinTime текущим временем чтобы не обработать старые COIN
long currentTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
lastCoinTimePlate1 = currentTime;
lastCoinTimePlate2 = currentTime;
Debug.Log($"<color=green>Порты инициализированы, debounce активирован с текущего времени: {currentTime}</color>");
// Выполняем handshake если требуется
if (requireHandshake)
{
PerformHandshakeForBothBoards(port1, port2);
}
// Определяем роли плат через команду 97 (левая / правая / витрина)
AssignRoles();
}
/// <summary>
/// Очищает буферы обоих портов от накопившихся старых данных
/// </summary>
private static void FlushPortBuffers()
{
int flushedCount1 = 0;
int flushedCount2 = 0;
int flushedCount3 = 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; // Защита от бесконечного цикла
}
// Читаем накопившиеся данные из порта 3 (витрина), если открыт
if (handle3 > 0)
{
while (ComNativeWrapper.Serial_ReadLineUTF8_bytes(handle3, buf, buf.Length) > 0)
{
flushedCount3++;
if (flushedCount3 > 100) break;
}
}
Debug.Log($"<color=cyan>Очищено: Плата1={flushedCount1}, Плата2={flushedCount2}, Плата3={flushedCount3} сообщений</color>");
}
private static void ClosePort()
{
if (handle1 > 0) { ComNativeWrapper.Serial_Close(handle1); handle1 = 0; }
if (handle2 > 0) { ComNativeWrapper.Serial_Close(handle2); handle2 = 0; }
if (handle3 > 0) { ComNativeWrapper.Serial_Close(handle3); handle3 = 0; }
// Сбрасываем роль-ориентированные алиасы
handleLeft = 0; handleRight = 0; handleVitrine = 0;
}
/// <summary>
/// Конвертирует десятичное число в двоичную строку фиксированной длины (26 бит)
/// </summary>
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;
// Используем роль-алиасы, если уже назначены (иначе — raw handle1/handle2)
int hLeft = handleLeft > 0 ? handleLeft : handle1;
int hRight = handleRight > 0 ? handleRight : handle2;
// Читаем с левой платы (8 кнопок + 2 хопера)
int read1 = ComNativeWrapper.Serial_ReadLineUTF8_bytes(hLeft, 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($"<color=yellow>COM-ЛЕВАЯ: COIN проигнорирован (спам через {timeSinceLastCoin}ms < 250ms) - подтверждение НЕ отправлено</color>");
// НЕ отправляем подтверждение для спама!
// НЕ обновляем lastCoinTime для спама - это позволит следующей реальной монете пройти
}
else
{
// Это РЕАЛЬНАЯ новая монета - обрабатываем
lastCoinTimePlate1 = currentTime;
// ВАЖНО: Отправляем подтверждение ТОЛЬКО для реальных монет!
int confirmSent = ComNativeWrapper.Serial_WriteStringUTF8(hLeft, "10", 1);
Debug.Log($"<color=cyan>COM-ЛЕВАЯ: COIN принят → Подтверждение '10' отправлено (bytes: {confirmSent})</color>");
// Добавляем в очередь для обработки в главном потоке
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(hRight, 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($"<color=yellow>COM-ПРАВАЯ: COIN проигнорирован (спам через {timeSinceLastCoin}ms < 250ms) - подтверждение НЕ отправлено</color>");
// НЕ отправляем подтверждение для спама!
// НЕ обновляем lastCoinTime для спама - это позволит следующей реальной монете пройти
}
else
{
// Это РЕАЛЬНАЯ новая монета - обрабатываем
lastCoinTimePlate2 = currentTime;
// ВАЖНО: Отправляем подтверждение ТОЛЬКО для реальных монет!
int confirmSent = ComNativeWrapper.Serial_WriteStringUTF8(hRight, "10", 1);
Debug.Log($"<color=cyan>COM-ПРАВАЯ: COIN принят → Подтверждение '10' отправлено (bytes: {confirmSent})</color>");
// Добавляем в очередь для обработки в главном потоке
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 "";
}
/// <summary>
/// Асинхронное чтение с двух портов. Выполняется в фоновом потоке.
/// </summary>
private static async Task<string> 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);
/// <summary>
/// Асинхронный keep-alive процесс для проверки соединения с платами
/// </summary>
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
}
}
/// <summary>
/// Выполняет двойное рукопожатие для обеих плат через ComNativeWrapper
/// </summary>
private void PerformHandshakeForBothBoards(string port1Name, string port2Name)
{
Debug.Log("=== НАЧАЛО ДВОЙНОГО РУКОПОЖАТИЯ ===");
bool success1 = PerformHandshakeSingle(handle1, 1);
bool success2 = PerformHandshakeSingle(handle2, 2);
bool success3 = handle3 > 0 && PerformHandshakeSingle(handle3, 3);
if (success1 && success2)
{
Debug.Log("<color=green>=== ОБЕ ИГРОВЫЕ ПЛАТЫ АВТОРИЗОВАНЫ ===</color>");
}
else
{
Debug.LogError($"<color=red>=== HANDSHAKE ПРОВАЛЕН: Плата1={success1}, Плата2={success2} ===</color>");
}
if (handle3 > 0)
{
if (success3) Debug.Log("<color=cyan>=== ВИТРИНА АВТОРИЗОВАНА ===</color>");
else Debug.LogWarning("<color=yellow>=== HANDSHAKE ВИТРИНЫ ПРОВАЛЕН (продолжаем без витрины) ===</color>");
}
}
/// <summary>
/// Выполняет handshake для одной платы (07→CHAL→08→RAES)
/// </summary>
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($"<color=green>Handshake [{plateId}]: ✓ УСПЕХ</color>");
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;
}
}
/// <summary>
/// Ожидает ответ с определенным префиксом от платы
/// </summary>
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;
}
/// <summary>
/// Запрашивает роль платы командой 97 и возвращает её (в верхнем регистре, обрезанную).
/// Ответ платы ожидается в формате: "ROLE : ЛЕВАЯ" / "ROLE : ПРАВАЯ" / "ROLE : ВИТРИНА"
/// </summary>
private string RequestBoardRole(int handle)
{
if (handle <= 0) return string.Empty;
try
{
// Отправляем команду запроса роли
ComNativeWrapper.Serial_WriteStringUTF8(handle, CMD_GET_ROLE, 1);
// Ждём ответ с префиксом "ROLE" до 2 секунд
string response = WaitForResponse(handle, "ROLE", 2000);
if (string.IsNullOrEmpty(response))
{
Debug.LogWarning($"RequestBoardRole (handle={handle}): нет ответа на команду {CMD_GET_ROLE}");
return string.Empty;
}
// Парсим "ROLE : <название>" → ищем двоеточие
int colonIdx = response.IndexOf(':');
if (colonIdx >= 0 && colonIdx < response.Length - 1)
{
string role = response.Substring(colonIdx + 1).Trim().ToUpper();
return role;
}
Debug.LogWarning($"RequestBoardRole (handle={handle}): неожиданный формат ответа: '{response}'");
return string.Empty;
}
catch (Exception ex)
{
Debug.LogError($"RequestBoardRole (handle={handle}): Exception: {ex.Message}");
return string.Empty;
}
}
/// <summary>
/// Определяет роли плат (ЛЕВАЯ / ПРАВАЯ / ВИТРИНА) через команду 97.
/// Заполняет handleLeft, handleRight, handleVitrine.
/// Если роль платы не определена — выводит предупреждение, fallback на порядок портов.
/// </summary>
private void AssignRoles()
{
Debug.Log("<color=yellow>=== ОПРЕДЕЛЕНИЕ РОЛЕЙ ПЛАТ (команда 97) ===</color>");
handleLeft = 0;
handleRight = 0;
handleVitrine = 0;
// Перебираем все открытые хэндлы
int[] allHandles = { handle1, handle2, handle3 };
foreach (int h in allHandles)
{
if (h <= 0) continue;
string role = RequestBoardRole(h);
Debug.Log($"<color=cyan>Плата handle={h}: роль = '{role}'</color>");
switch (role)
{
case ROLE_LEFT:
handleLeft = h;
Debug.Log($"<color=green>→ handle={h} назначена как ЛЕВАЯ (player 1)</color>");
break;
case ROLE_RIGHT:
handleRight = h;
Debug.Log($"<color=green>→ handle={h} назначена как ПРАВАЯ (player 2)</color>");
break;
case ROLE_VITRINE:
handleVitrine = h;
Debug.Log($"<color=cyan>→ handle={h} назначена как ВИТРИНА (showcase)</color>");
break;
default:
Debug.LogWarning($"Плата handle={h}: неизвестная или пустая роль '{role}' — будет использован fallback");
break;
}
}
// Fallback: если игровые платы не ответили на запрос роли — используем порядок портов
if (handleLeft == 0 && handle1 > 0)
{
handleLeft = handle1;
Debug.LogWarning($"<color=orange>ЛЕВАЯ роль не определена — используем handle1={handle1} как fallback</color>");
}
if (handleRight == 0 && handle2 > 0)
{
handleRight = handle2;
Debug.LogWarning($"<color=orange>ПРАВАЯ роль не определена — используем handle2={handle2} как fallback</color>");
}
Debug.Log($"<color=green>=== РОЛИ: Левая={handleLeft}, Правая={handleRight}, Витрина={handleVitrine} ===</color>");
}
#endregion
}