329 lines
14 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.Collections;
using System.Text;
using RR.Game2D.Core.Managers;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
/// <summary>
/// Основной класс-обертка для работы с COM-портом и периферийной платой.
/// Управляет всей коммуникацией с аппаратным обеспечением: прием монет, чтение кнопок, управление подсветкой, отправка сигналов.
/// </summary>
public class ComWrapper : MonoBehaviour
{
//Настраиваем из инспектора
[SerializeField] bool saveMoneyBetweenSessions;
[SerializeField] bool enablePrizeChecking = false; // Если хопер с проверкой наличия приза ставим true
public static UnityEvent<int> OnCreditAdded;
public static UnityEvent<int> OnCreditRemoved;
public static UnityEvent<string> 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===========================//
/// <summary>
/// Инициализирует систему работы с COM-портом.
/// Открывает соединение, настраивает события и запускает периодические запросы состояния.
/// </summary>
/// <remarks>
/// Обязателен к вызову при старте приложения. Выполняет:
/// - Создание UnityEvent'ов для уведомлений
/// - Загрузку количества кредитов из PlayerPrefs
/// - Открытие COM-порта
/// - Запуск периодического опроса пинов (если включена проверка призов)
/// </remarks>
public void Initialize()
{
//Создаем ивенты
OnCreditAdded = new UnityEvent<int>();
OnCreditRemoved = new UnityEvent<int>();
OnInputReceived = new UnityEvent<string>();
//Начальная настройка
if (!saveMoneyBetweenSessions) PlayerPrefs.SetInt("CoinsCount", 0);
CreditsCount = PlayerPrefs.GetInt("CoinsCount", 0);
initPort(); //вот это важный метод который собственно порт открывает.
//Каждые 5 сек запрашиваем инпуты у платы. Надо потому что при заходе в игру с пинов еще не пришла инфа
if (enablePrizeChecking) StartCoroutine(KeepPortAliveProcess());
}
/// <summary>
/// Добавляет один кредит в систему и сохраняет обновленное количество.
/// Вызывается автоматически событием при получении сигнала "COIN" из COM-порта.
/// </summary>
/// <remarks>
/// Увеличивает счетчик кредитов и вызывает событие onCoinAdded для уведомления подписчиков.
/// Автоматически сохраняет состояние в PlayerPrefs.
/// </remarks>
public static void AddCredit()
{
CreditsCount++;
PlayerPrefs.SetInt("CoinsCount", CreditsCount);
OnCreditAdded?.Invoke(CreditsCount);
}
/// <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-количество записаных символов в порт. можно обработать
//GameManager.Logger.Log("Wrote: " + w);
}
catch
{
// DebugWindow.Message("Ошибка отправки строкового сигнала.");
}
}
/// <summary>
/// Отправляет команду управления подсветкой на периферийную плату.
/// </summary>
/// <param name="lightMode">Режим подсветки из перечисления LightMode</param>
/// <remarks>
/// Формирует команду вида "80XXX" где XXX - код режима подсветки.
/// Для добавления новых режимов необходимо обновить вложенный метод getLightString + добавить режим в перечисление.
/// </remarks>
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("Ошибка отправки светового сигнала.");
}
}
/// <summary>
/// Выполняет списание платы за запуск игры.
/// Проверяет наличие доступных кредитов или режим бесплатной игры.
/// При успешном списании увеличивает счетчики запусков.
/// </summary>
/// <returns>
/// true - если списание прошло успешно (бесплатный режим или есть кредиты),
/// false - если кредитов недостаточно и платный режим
/// </returns>
/// <remarks>
/// В случае успеха:
/// - Увеличивает общие счетчики запусков (key_starts и key_startsNul)
/// - При платном режиме уменьшает количество кредитов на 1
/// - Вызывает событие onCoinRemoved при списании кредита
/// </remarks>
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
}