using System.Collections; using System.Text; using RR.Game2D.Core.Managers; using Unity.VisualScripting; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; /// /// Основной класс-обертка для работы с COM-портом и периферийной платой. /// Управляет всей коммуникацией с аппаратным обеспечением: прием монет, чтение кнопок, управление подсветкой, отправка сигналов. /// public class ComWrapper : MonoBehaviour { //Настраиваем из инспектора [SerializeField] bool saveMoneyBetweenSessions; [SerializeField] bool enablePrizeChecking = false; // Если хопер с проверкой наличия приза ставим true public static UnityEvent OnCreditAdded; public static UnityEvent OnCreditRemoved; public static UnityEvent OnInputReceived; #region Приватные поля private const string REQUEST_INPUT = "91"; private const string DEFAULT_INPUT = "INPUT1111111111111111"; private const string LIGHTSIGNAL_PREFIX = "80"; private static int h; // идентификатор хэндла private static byte[] buf = new byte[1024]; // Байт-буфер куда будут приходить данные с порта private static int CreditsCount; private string inputString = DEFAULT_INPUT; #endregion //===========================start: PUBLIC API===========================// /// /// Инициализирует систему работы с COM-портом. /// Открывает соединение, настраивает события и запускает периодические запросы состояния. /// /// /// Обязателен к вызову при старте приложения. Выполняет: /// - Создание UnityEvent'ов для уведомлений /// - Загрузку количества кредитов из PlayerPrefs /// - Открытие COM-порта /// - Запуск периодического опроса пинов (если включена проверка призов) /// public void Initialize() { //Создаем ивенты OnCreditAdded = new UnityEvent(); OnCreditRemoved = new UnityEvent(); OnInputReceived = new UnityEvent(); //Начальная настройка if (!saveMoneyBetweenSessions) PlayerPrefs.SetInt("CoinsCount", 0); CreditsCount = PlayerPrefs.GetInt("CoinsCount", 0); initPort(); //вот это важный метод который собственно порт открывает. //Каждые 5 сек запрашиваем инпуты у платы. Надо потому что при заходе в игру с пинов еще не пришла инфа if (enablePrizeChecking) StartCoroutine(KeepPortAliveProcess()); } /// /// Добавляет один кредит в систему и сохраняет обновленное количество. /// Вызывается автоматически событием при получении сигнала "COIN" из COM-порта. /// /// /// Увеличивает счетчик кредитов и вызывает событие onCoinAdded для уведомления подписчиков. /// Автоматически сохраняет состояние в PlayerPrefs. /// public static void AddCredit() { CreditsCount++; PlayerPrefs.SetInt("CoinsCount", CreditsCount); OnCreditAdded?.Invoke(CreditsCount); } /// /// Отправляет произвольную строковую команду на периферийную плату через COM-порт. /// /// Команда для отправки /// /// Используется для отправки пользовательских команд на плату. /// Команда должна быть в формате, ожидаемом прошивкой платы. /// В случае ошибки отправки выводит предупреждение в DebugWindow. /// public static void Send(string message) { try { int w = ComNativeWrapper.Serial_WriteStringUTF8(h, message, 1); //w-количество записаных символов в порт. можно обработать //GameManager.Logger.Log("Wrote: " + w); } catch { // DebugWindow.Message("Ошибка отправки строкового сигнала."); } } /// /// Отправляет команду управления подсветкой на периферийную плату. /// /// Режим подсветки из перечисления LightMode /// /// Формирует команду вида "80XXX" где XXX - код режима подсветки. /// Для добавления новых режимов необходимо обновить вложенный метод getLightString + добавить режим в перечисление. /// public static void Send(LightMode lightMode) { try { string signal = LIGHTSIGNAL_PREFIX + getLightString(lightMode); Send(signal); //ВОТЭТОТ МЕТОД ПОД СВОИ ПОДСВЕТКИ НАДО ПОДГОНЯТЬ string getLightString(LightMode mode) => mode switch { LightMode.Idle1 => "500", LightMode.Idle2 => "507", LightMode.Orange => "501", LightMode.Blue => "502", LightMode.Win => "503", LightMode.Lose => "504", LightMode.Prize => "505", LightMode.Tama => "513", LightMode.Dog => "514", LightMode.Sam => "515", LightMode.Mistress => "516", _ => "500" }; } catch { // DebugWindow.Message("Ошибка отправки светового сигнала."); } } /// /// Выполняет списание платы за запуск игры. /// Проверяет наличие доступных кредитов или режим бесплатной игры. /// При успешном списании увеличивает счетчики запусков. /// /// /// true - если списание прошло успешно (бесплатный режим или есть кредиты), /// false - если кредитов недостаточно и платный режим /// /// /// В случае успеха: /// - Увеличивает общие счетчики запусков (key_starts и key_startsNul) /// - При платном режиме уменьшает количество кредитов на 1 /// - Вызывает событие onCoinRemoved при списании кредита /// public static bool Pay() { // блок проверки оплаты // TODO: Сделать свою систему для проверки оплаты и бесплатной игры // if (SettingsManager.instance.GetSettingValue("Бесплатная игра") == "Вкл") // { // //Счетчики пробега PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + 1); // PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + 1); // // return true; // } if (CreditsCount > 0) { CreditsCount--; PlayerPrefs.SetInt("CoinsCount", CreditsCount); //счетчики пробега PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + 1); PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + 1); OnCreditRemoved?.Invoke(CreditsCount); return true; } return false; } //===========================end: PUBLIC API===========================// private void Update() => pollPort(); private void OnDisable() => ClosePort(); private void OnApplicationQuit() => ClosePort(); private IEnumerator KeepPortAliveProcess() { OnInputReceived?.AddListener(InputReceivedCather); GameManager.Logger.Log("Подключаю проверку порта."); while (true) { requestInputs(); yield return new WaitForSeconds(5f); //нет инпута if (GetInputToggle() == 0) { GameManager.Logger.Log("Плата отвалилась. Запускаю переподключение."); initPort(); } //есть инпут else { ToggleInputReceived(0); } yield return new WaitForSeconds(5); } } void ToggleInputReceived(int value) => PlayerPrefs.SetInt("inputToggle", value); int GetInputToggle() => PlayerPrefs.GetInt("inputToggle", 0); void InputReceivedCather(string input) { if (!string.IsNullOrEmpty(input)) { //пришел инпут ToggleInputReceived(1); } } #region Приватные методы //Ну тут по дебаг логам все должно быть понятно //Если интересно, то h - это хэндл. Что то на плюсовом, типа ID соединения или чтото. Если 0 то нету соед. если меньше то ошибка, больше 0 - норм. private void initPort() { GameManager.Logger.Log("ComWrapper", "Старт программы"); ComNativeWrapper.Serial_CloseAll(); var ports = ComNativeWrapper.ListPorts(); GameManager.Logger.Log("ComWrapper","Ports: " + string.Join(", ", ports)); if (ports.Length == 0) { GameManager.Logger.Log("Нет портов, завершаю"); return; } string port = ports[^1]; GameManager.Logger.Log("ComWrapper","Открываю первый с конца порт: " + port); //вот эти настройки лучше не ковырять, но если очень хочется можно поменять readTimeoutMs и writeTimeoutMs //Если read поставить слишком маленьким оно проебует кредиты когда они идут большой пачкой например при оплате через вендисту. //Там в порт спамит "COIN" в больших количествах и процентов 10 из этих строк не успевают прочитаться при таймауте 1мс, соотв. монеты проебуются. //Слишком большой таймаут будет тормозить игру потому что оно все выполняется синхронно и каждый кадр забирает столько мс сколько в переменной указано. h = ComNativeWrapper.Serial_Open(port, 115200, 8, 0, 1, 5, 10); GameManager.Logger.Log("ComWrapper",$"handle: {h}"); if (h <= 0) { GameManager.Logger.Log("ComWrapper","Open failed: " + ComNativeWrapper.GetLastError()); return; } else { GameManager.Logger.Log("ComWrapper",$"Открыл порт: {port}"); } } private static void ClosePort() { Debug.Log("Закрываю все порты"); 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) { string s = Encoding.UTF8.GetString(buf, 0, read); //GameManager.Logger.Log($"ReadLine len={read} data=[{s}]"); return s; } else if (read < 0) { GameManager.Logger.Log("ComWrapper",$"ReadLine err={read} :: {ComNativeWrapper.GetLastError()}"); } return ""; } //просим прислать состояние цифровых входов. Приходит что то вроде "INPUT111111101" private void requestInputs() => Send(REQUEST_INPUT); private void pollPort() { //Тут все просто, читаем что нам приходит с порта и сравниваем. string comInput = ReadBufferLine(); if (comInput != "") { // DebugWindow.Message("COM: " + comInput); if (comInput == "COIN") AddCredit(); if (comInput.StartsWith("INPUT")) OnInputReceived?.Invoke(comInput.Substring(5)); //инвокаю сразу без префикса "INPUT" } } #endregion } public enum LightMode { Idle1, Idle2, Orange, Blue, Win, Lose, Prize, Tama, Dog, Sam, Mistress }