605 lines
22 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.Collections;
using System.Text;
using UnityEngine;
using UnityEngine.Events;
//async
using System.Threading.Tasks;
using System.Threading;
/// <summary>
/// Основной класс-обертка для работы с COM-портом и периферийной платой.
/// Управляет всей коммуникацией с аппаратным обеспечением: прием монет, чтение кнопок, управление подсветкой, отправка сигналов.
/// </summary>
public class ComWrapper : MonoBehaviour
{
//Настраиваем из инспектора
[SerializeField] bool saveMoneyBetweenSessions;
[SerializeField] bool enablePrizeChecking = false; // Если хопер с проверкой наличия приза ставим true
[SerializeField] bool keepAlive = true;
public static UnityEvent onPulseRecieved;
public static UnityEvent<int> onCoinAdded;
public static UnityEvent<int> onCoinRemoved;
public static UnityEvent<string> onInputRecieved;
#region Приватные поля
private const string REQUEST_INPUT = "91";
private const string DEFAULT_INPUT = "INPUT1111111111111111";
private static int h;//идентификатор хэндла
private static byte[] buf = new byte[1024]; //Байт-буфер куда будут приходить данные с порта
public static int Balance;
bool boardEncrypted = false;
string encString;
string confirmString;
Coroutine keepAliveCoroutine;
//async
private CancellationTokenSource pollCancellationTokenSource;
private bool isPolling = false;
//сделать запрос инпутов. а если его нет то просим ребут? делаем если по другим факторам не пройдёт.
//private string inputString = DEFAULT_INPUT;
#endregion
//===========================start: PUBLIC API===========================//
/// <summary>
/// Инициализирует систему работы с COM-портом.
/// Открывает соединение, настраивает события и запускает периодические запросы состояния.
/// </summary>
/// <remarks>
/// Обязателен к вызову при старте приложения. Выполняет:
/// - Создание UnityEvent'ов для уведомлений
/// - Загрузку количества кредитов из PlayerPrefs
/// - Открытие COM-порта
/// - Запуск периодического опроса пинов (если включена проверка призов)
/// </remarks>
public void Initialize()
{
//Создаем ивенты
onCoinAdded = new UnityEvent<int>();
onCoinRemoved = new UnityEvent<int>();
onInputRecieved = new UnityEvent<string>();
onPulseRecieved = new UnityEvent();
//Начальная настройка
if (!saveMoneyBetweenSessions) PlayerPrefs.SetInt("CoinsCount", 0);
Balance = PlayerPrefs.GetInt("CoinsCount", 0);
onInputRecieved?.AddListener(InputRecievedCather);
StartCoroutine(SetupBoard());
}
/// <summary>
/// Добавляет один кредит в систему и сохраняет обновленное количество.
/// Вызывается автоматически событием при получении сигнала "COIN" из COM-порта.
/// </summary>
/// <remarks>
/// Увеличивает счетчик кредитов и вызывает событие onCoinAdded для уведомления подписчиков.
/// Автоматически сохраняет состояние в PlayerPrefs.
/// </remarks>
public static void AddCredit()
{
int recieved = PlayerPrefs.GetInt("ValutaPerImpulse", 10);
Balance = Balance + recieved;
PlayerPrefs.SetInt("CoinsCount", Balance);
//Счетчики выручки
PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + recieved);
PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + recieved);
if(SettingsManager.GetString("BaseBotDifficulty", "Фиксированная") == "Повышенная")
{
int nowPulses = PlayerPrefs.GetInt("PulsesCounter", 0) + 1;
PlayerPrefs.SetInt("PulsesCounter", nowPulses);
}
onPulseRecieved?.Invoke();
onCoinAdded?.Invoke(Balance);
}
/// <summary>
/// Отправляет произвольную строковую команду на периферийную плату через COM-порт.
/// </summary>
/// <param name="message">Команда для отправки</param>
/// <remarks>
/// Используется для отправки пользовательских команд на плату.
/// Команда должна быть в формате, ожидаемом прошивкой платы.
/// В случае ошибки отправки выводит предупреждение в DebugWindow.
/// </remarks>
public static void Send(string message)
{
try
{
int w = ComNativeWrapper.Serial_WriteStringUTF8(h, message, 1); //w-количество записаных символов в порт. можно обработать
//Debug.Log("Wrote: " + w);
}
catch { Debug.LogError("Ошибка отправки строкового сигнала."); }
}
/// <summary>
/// Отправляет команду управления подсветкой на периферийную плату.
/// </summary>
/// <param name="lightMode">Режим подсветки из перечисления LightMode</param>
/// <remarks>
/// Формирует команду вида "80XXX" где XXX - код режима подсветки.
/// Для добавления новых режимов необходимо обновить вложенный метод getLightString + добавить режим в перечисление.
/// </remarks>
public static void Send(LightMode lightMode)
{
try
{
string signal = LightUtility.GetLightString(lightMode);
Send(signal);
}
catch { Debug.LogError("Ошибка отправки светового сигнала."); }
}
/// <summary>
/// Выполняет списание платы за запуск игры.
/// Проверяет наличие доступных кредитов или режим бесплатной игры.
/// При успешном списании увеличивает счетчики запусков.
/// </summary>
/// <returns>
/// true - если списание прошло успешно (бесплатный режим или есть кредиты),
/// false - если кредитов недостаточно и платный режим
/// </returns>
/// <remarks>
/// В случае успеха:
/// - Увеличивает общие счетчики запусков (key_starts и key_startsNul)
/// - При платном режиме уменьшает количество кредитов на 1
/// - Вызывает событие onCoinRemoved при списании кредита
/// </remarks>
public static bool Pay(int amount)
{
//блок проверки оплаты
if (SettingsManager.GetBool("Freeplay", false))
{
return true;
}
if (Balance >= amount)
{
Balance -= amount;
PlayerPrefs.SetInt("CoinsCount", Balance);
onCoinRemoved?.Invoke(Balance);
return true;
}
return false;
}
//===========================end: PUBLIC API===========================//
//private void Update() => pollPort(); //async пока выключил это
private void OnDisable()
{
StopAsyncPolling();//async
ClosePort();
}
private void OnApplicationQuit()
{
StopAsyncPolling();//async
ClosePort();
}
private void StopAsyncPolling()
{
pollCancellationTokenSource?.Cancel();
pollCancellationTokenSource?.Dispose();
pollCancellationTokenSource = null;
}
private async void StartAsyncPolling()
{
try
{
Logger.Instance.Log("Запуск асинхронного опроса порта");
while (pollCancellationTokenSource != null &&
!pollCancellationTokenSource.Token.IsCancellationRequested)
{
await Task.Delay(10, pollCancellationTokenSource.Token);
string comInput = await ReadBufferLineAsync(pollCancellationTokenSource.Token);
if (!string.IsNullOrEmpty(comInput))
{
// Прямой вызов - мы уже в главном потоке после await
ProcessComInputImmediate(comInput);
}
}
}
catch (OperationCanceledException)
{
Logger.Instance.Log("Асинхронный опрос порта остановлен");
}
catch (Exception e)
{
Logger.Instance.Log($"Ошибка асинхронного опроса: {e.Message}");
}
}
private void ProcessComInputImmediate(string comInput)
{
Debug.LogError("COM: " + comInput);
if (comInput == "COIN") AddCredit();
if (comInput.StartsWith("INPUT")) onInputRecieved?.Invoke(comInput.Substring(5));
if (comInput.StartsWith("ENC:")) encString = comInput;
if (comInput.StartsWith("ACTIVATED:")) confirmString = comInput;
}
private IEnumerator KeepPortAliveProcess()
{
Logger.Instance.Log("Подключаю проверку порта.");
while (true)
{
requestInputs();
yield return new WaitForSeconds(2f);
//нет инпута
if (GetInputToggle() == 0)
{
Logger.Instance.Log("Плата отвалилась. Запускаю переподключение.");
initPort();
}
//есть инпут
else
{
ToggleInputRecieved(0);
}
yield return new WaitForSeconds(10f);
}
}
void ToggleInputRecieved(int value) => PlayerPrefs.SetInt("inputToggle", value);
int GetInputToggle() => PlayerPrefs.GetInt("inputToggle", 0);
void InputRecievedCather(string input)
{
if (!string.IsNullOrEmpty(input))
{
//пришел инпут
ToggleInputRecieved(1);
}
}
#region Приватные методы
//Ну тут по дебаг логам все должно быть понятно
//Если интересно, то h - это хэндл. Что то на плюсовом, типа ID соединения или чтото. Если 0 то нету соед. если меньше то ошибка, больше 0 - норм.
private void initPort()
{
Logger.Instance.Log("Инициализация порта");
ComNativeWrapper.Serial_CloseAll();
//ComNativeWrapper.Serial_Close(h);
var ports = ComNativeWrapper.ListPorts();
if (ports.Length == 0) { Debug.LogError("Нет портов, завершаю"); return; }
string port = ports[^1];
Logger.Instance.Log("Открываю порт: " + port);
//вот эти настройки лучше не ковырять, но если очень хочется можно поменять readTimeoutMs и writeTimeoutMs
//Если read поставить слишком маленьким оно проебует кредиты когда они идут большой пачкой например при оплате через вендисту.
//Там в порт спамит "COIN" в больших количествах и процентов 10 из этих строк не успевают прочитаться при таймауте 1мс, соотв. монеты проебуются.
//Слишком большой таймаут будет тормозить игру потому что оно все выполняется синхронно и каждый кадр забирает столько мс сколько в переменной указано.
h = ComNativeWrapper.Serial_Open(port, 115200, 8, 0, 1, 4, 10);
Logger.Instance.Log($"handle: {h}");
if (h <= 0)
{
Logger.Instance.Log("Open failed: " + ComNativeWrapper.GetLastError());
return;
}
else
{
Logger.Instance.Log($"Открыл порт: {port}");
}
}
private static void ClosePort()
{
Logger.Instance.Log($"Отпускаю порт handle:{h}");
if (h > 0)
{
ComNativeWrapper.Serial_Close(h);
h = 0;
}
}
//Тут чтение, не трогаем(опционально)
private static string ReadBufferLine()
{
int read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(h, buf, buf.Length);
if (read > 0)
{
var s = Encoding.UTF8.GetString(buf, 0, read);
//DebugWindow.Message($"ReadLine len={read} data=[{s}]");
return s;
}
else if (read < 0)
{
//DebugWindow.Message($"ReadLine err={read} :: {ComNativeWrapper.GetLastError()}");
}
return "";
}
//async
private static async Task<string> ReadBufferLineAsync(CancellationToken cancellationToken = default)
{
try
{
return await Task.Run(() =>
{
// Проверяем отмену внутри Task.Run
if (cancellationToken.IsCancellationRequested)
return string.Empty;
int read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(h, buf, buf.Length);
if (read > 0)
{
return Encoding.UTF8.GetString(buf, 0, read);
}
return string.Empty;
}, cancellationToken);
}
catch (OperationCanceledException)
{
Logger.Instance.Log("Чтение порта отменено");
return string.Empty;
}
catch (Exception e)
{
Logger.Instance.Log($"Ошибка асинхронного чтения: {e.Message}");
return string.Empty;
}
}
//просим прислать состояние цифровых входов. Приходит что то вроде "INPUT111111101"
private void requestInputs() => Send(REQUEST_INPUT);
private void pollPort()
{
//Тут все просто, читаем что нам приходит с порта и сравниваем.
string comInput = ReadBufferLine();
if (comInput != "")
{
Debug.Log("COM: " + comInput);
if (comInput == "COIN") AddCredit();
if (comInput.StartsWith("INPUT")) onInputRecieved?.Invoke(comInput.Substring(5)); //инвокаю сразу без префикса "INPUT"
if (comInput.StartsWith("ENC:")) encString = comInput;
if (comInput.StartsWith("ACTIVATED:")) confirmString = comInput;
}
}
private IEnumerator SetupBoard()
{
if (keepAliveCoroutine != null) StopCoroutine(keepAliveCoroutine);
initPort();
boardEncrypted = true; //предполагаем что зашифрована
//пробуем получить инпуты. Вдруг плата осталась расшифрована
ToggleInputRecieved(0);
requestInputs();
float timer = 1;
while (GetInputToggle() == 0 && timer > 0)
{
pollPort();
timer -= Time.deltaTime;
yield return null;
}
//Есть инпут. незашифровано
if (GetInputToggle() == 1)
{
boardEncrypted = false;
}
if (boardEncrypted)
{
Logger.Instance.Log("Шифрованная плата");
//запрос
encString = "";
Send("666");
//ждем пока придет ENC:
timer = 1;
while (string.IsNullOrEmpty(encString) && timer > 0)
{
pollPort();
timer -= Time.deltaTime;
yield return null;
}
//получили ENC: строку или вышли по таймеру
//Строка пришла
if (!string.IsNullOrEmpty(encString))
{
Logger.Instance.Log($"Расшифровываю {encString}");
string decr = BoardUtility.DecryptSerial(ExtractData(encString));
//подтверждение
confirmString = "";
Send("777" + decr);
}
else
{
Logger.Instance.Log($"Зашифрованная строка не пришла"); //отвал игры пускать
(new GameObject()).AddComponent(typeof(VfxPrefab));
}
//ждем подтверждение от платы
timer = 2;
while (string.IsNullOrEmpty(confirmString) && timer > 0)
{
pollPort();
timer -= Time.deltaTime;
yield return null;
}
if (string.IsNullOrEmpty(confirmString))
{
Application.Quit();
}
else
{
if (confirmString.IndexOf("OK") != -1)
{
Logger.Instance.Log("Активировано.");
}
else
{
Application.Quit();
}
}
}
else
{
Logger.Instance.Log("НЕшифрованная плата");
}
//async
//Каждые 5 сек запрашиваем инпуты у платы. Надо потому что при заходе в игру с пинов еще не пришла инфа
/* if (keepAlive)
keepAliveCoroutine = StartCoroutine(KeepPortAliveProcess());*/
//async
pollCancellationTokenSource = new CancellationTokenSource();
StartAsyncPolling();
StartKeepAliveProcess();
}
private async void StartKeepAliveProcess()
{
if (keepAlive)
{
await StartKeepAliveProcessAsync(pollCancellationTokenSource.Token);
}
}
private async Task StartKeepAliveProcessAsync(CancellationToken cancellationToken)
{
try
{
Logger.Instance.Log("Запуск асинхронной проверки порта");
while (!cancellationToken.IsCancellationRequested)
{
await SendRequestInputsAsync(cancellationToken);
await Task.Delay(2000, cancellationToken); // 2 секунды
if (!await CheckPortConnectionAsync(cancellationToken))
{
Logger.Instance.Log("Плата отвалилась. Запускаю переподключение.");
await ReinitializePortAsync(cancellationToken);
}
else
{
ToggleInputRecieved(0);
}
await Task.Delay(10000, cancellationToken); // 10 секунд
}
}
catch (OperationCanceledException)
{
Logger.Instance.Log("Асинхронная проверка порта остановлена");
}
catch (Exception e)
{
Logger.Instance.Log($"Ошибка проверки порта: {e.Message}");
}
}
private async Task SendRequestInputsAsync(CancellationToken cancellationToken)
{
try
{
requestInputs();
await Task.CompletedTask;
}
catch (Exception e)
{
Logger.Instance.Log($"Ошибка отправки запроса инпутов: {e.Message}");
}
}
private async Task<bool> CheckPortConnectionAsync(CancellationToken cancellationToken)
{
// Простая проверка, не требует Task.Run
return await Task.FromResult(GetInputToggle() != 0);
}
private async Task ReinitializePortAsync(CancellationToken cancellationToken)
{
await Task.Run(() => initPort(), cancellationToken);
}
public static string ExtractData(string input)
{
const string encPrefix = "ENC:";
int encIndex = input.IndexOf(encPrefix);
if (encIndex == -1)
{
Logger.Instance.Log($"Неверный формат шифрованной строки {input}");
throw new ArgumentException("Invalid input format");
}
int dataStart = encIndex + encPrefix.Length;
return input.Substring(4);
}
#endregion
}
public enum LightMode
{
Idle1,
Idle2,
Orange,
Blue,
Win,
Lose,
Prize,
Toaster,
Tama,
Dog,
Sam,
Mistress
}