using System; using System.Collections.Generic; using UnityEngine; using MySerialPortInterface; public class ArrowSerialPort : MonoBehaviour { //使用自定义串口 private SerialPortInterface serialPortInterface; private int baudrate = 115200; //private int baudrate = 921600; //private int baudrate = 460800; private string PortName = "/dev/ttyS0"; //private static Dictionary _isInit = new(); public bool testMode = true; private void Start() { //if (!_isInit.TryGetValue(gameObject.name, out int instanceID) && instanceID == 0) //{ // DontDestroyOnLoad(gameObject); // Init(); //} DontDestroyOnLoad(gameObject); Init(); } private void LOG(string msg, bool warning = false) { if (!warning) Debug.Log($"{msg}"); else Debug.LogWarning($"{msg}"); } private void Init() { //_isInit[gameObject.name] = GetInstanceID(); #if UNITY_ANDROID && !UNITY_EDITOR serialPortInterface = gameObject.GetComponent(); serialPortInterface.SystemEventObject.AddListener(SystemEventObject); TrySerialOpenPort(); #endif #if UNITY_EDITOR if (testMode) { var testBoard = Resources.Load("SerialPortTest"); GameObject.Instantiate(testBoard); } //模拟连接设备 //List data = new List(); //data.Add(0xaa); //data.Add(0x80); //data.Add(0x07); //data.Add(0x02); //data.Add(0x01); //data.Add(0x84); //data.Add(0x55); //HandleSerialPortUpdate(data.ToArray(), 7); #endif } private void TrySerialOpenPort() { serialPortInterface?.Open(); } private void SystemEventObject(SerialPortInterface arg0, string msg) { // || msg.Equals("PERMISSION_ERROR") if (msg.Equals("OPEN_ERROR"))//串口开启失败 定时重试 { #if UNITY_ANDROID && !UNITY_EDITOR Invoke("TrySerialOpenPort", 1f); #endif // LOG($"{PortName} 串口打开失败 重试中", true); } else if (msg.Equals("OPENED")) { SerialPortHelper.ins.OnConnect(arg0.DeviceName); //应用启动 间隔十秒请求一次设备信息 CancelInvoke("RequestDeviceIno"); InvokeRepeating("RequestDeviceIno", 0f, 10f); //LOG($"{PortName} 串口打开成功!"); if (CommonConfig.bOpenTheLaserWithoutQRCode) { //没有二维码直接开启激光 //激光默认打开 RequestLightState(true); } } else if (msg.Equals("CLOSED"))//串口断开 { SerialPortHelper.ins.OnDisConnect(arg0.DeviceName); LOG($"{PortName} 串口关闭!"); } } private void OnDestroy() { //if (_isInit.TryGetValue(gameObject.name, out var instanceID) && instanceID == GetInstanceID()) //{ // LOG($"{PortName} 串口关闭"); // SerialPortHelper.ins.OnDisConnect(serialPortInterface?.DeviceName); // serialPortInterface?.Close(); // _isInit.Remove(gameObject.name); //} } /// /// 串口读取二进制流数据(界面挂载调用) /// /// public void ReadStreamingBinary(object data) { var realData = new List(); var tempData = data as byte[]; for (int i = 0; i < tempData.Length; i++) { realData.Add(tempData[i]); if (tempData.Length > i + 1) { if (tempData[i + 1] == 0XAA) //多条消息 粘包位置 { //把上一条发送 PhraseData(realData.ToArray()); realData.Clear(); } } else PhraseData(realData.ToArray()); } } //自定义端口使用 public void HandleSerialPortUpdate(byte[] buffer, int size) { //Debug.Log($"Received Data: {BitConverter.ToString(buffer)} of size {size}"); //PhraseData(buffer); // 将新的数据加入缓存 dataCache.AddRange(buffer); // 尝试解析缓存中的数据 while (TryParseNextFrame(ref dataCache, out byte[] frame)) { // 如果成功解析一帧数据,处理该帧 // Debug.Log($"[ArrowSerialPort] frame Data: {BitConverter.ToString(frame)}"); PhraseData(frame); } } public void TestRead(byte[] bytes) { ReadStreamingBinary(bytes); } /// /// 解析串口数据 /// /// private void PhraseData(byte[] bytes) { if (bytes[0] == 0xAA) { string msg = string.Empty; for (int i = 0; i < bytes.Length; i++) { msg += bytes[i].ToString("x2") + " "; } // 获取当前时间 DateTime currentTime = DateTime.Now; LOG($"[ArrowSerialPort] {PortName} 收到串口信息 msg={msg}!,接收时间:" + currentTime.ToString("yyyy-MM-dd HH:mm:ss.fff")); var cmdID = bytes[1]; switch (cmdID) { case 0x80://设备信息响应 OnDeviceInfoBack(bytes); break; case 0x81://射击消息 if (CommonConfig.bDisableBluetooth) { // 获取当前时间 DateTime currentTime1 = DateTime.Now; // 打印带毫秒的时间戳 Debug.Log("射箭前:CurrentTime1: " + currentTime1.ToString("yyyy-MM-dd HH:mm:ss.fff")); //3 - APP在收到射箭消息后,立即回复相同的内容给设备端; serialPortInterface?.Write(bytes); DateTime currentTime3 = DateTime.Now; // 打印带毫秒的时间戳 Debug.Log("*** 回复相同的内容给设备端 CurrentTime3: " + currentTime3.ToString("yyyy-MM-dd HH:mm:ss.fff")); } OnDeviceShoot(bytes); if (CommonConfig.bDisableBluetooth) { // 获取当前时间 DateTime currentTime2 = DateTime.Now; // 打印带毫秒的时间戳 Debug.Log("射箭后:CurrentTime2: " + currentTime2.ToString("yyyy-MM-dd HH:mm:ss.fff")); } break; case 0x82://按键消息 OnDeviceButton(bytes); break; case 0x83://激光控制 OnLightChange(bytes); break; case 0x84://弹夹消息(仅手枪) OnMagazinesInfo(bytes); break; } } else { if (CommonConfig.bDisableBluetooth) { //byte[] bytes = new byte[] { 0x0A, 0x00, 0x00, 0x00, // 1st timestamp // 0x0B, 0x00, 0x00, 0x00, // 2nd timestamp // 0x00, 0x01, 0x00, 0x00 // 3rd timestamp //}; if (bytes.Length == 12) { // 解析并转换时间戳 for (int i = 0; i < bytes.Length; i += 4) { // 提取 4 字节的时间戳 uint timestamp = BitConverter.ToUInt32(bytes, i); // 转换为毫秒(单位是 100us) float timeInMilliseconds = timestamp * 0.1f; // 打印转换结果 Debug.Log($"Timestamp (U32): {timestamp} => Time: {timeInMilliseconds} ms"); } } } } } bool lightWaitClose = false; public void DelayCloseLight() { if (!lightWaitClose && hasOnceInceCoin) { LOG("游戏时间结束 延迟关闭激光"); Invoke("DoDelayCloseLight", 60f); lightWaitClose = true; } } public void CancelDelayCloseLight() { LOG("关闭延迟关闭激光"); lightWaitClose = false; CancelInvoke("DoDelayCloseLight"); } private void DoDelayCloseLight() { RequestLightState(false); } //是否有过首次投币 没投币过 不关闭摄像头 bool hasOnceInceCoin = false; #region APP请求 // 异或校验内容:命令+长度+数据内容 /// /// 设置激光状态 /// public void RequestLightState(bool on, bool insertCoin = false) { if (insertCoin) hasOnceInceCoin = true; List data = new List(); data.Add(0xAA);//起始码 data.Add(0x83);//命令号 data.Add(0x06);//长度 data.Add((byte)(on ? 0x01 : 0x00));//长度 byte temp = 0; for (int i = 1; i < data.Count; i++) { temp ^= data[i]; } data.Add(temp);//异或校验 data.Add(0x55);//结束码 serialPortInterface?.Write(data.ToArray()); LOG($"设置激光状态:{on}!"); OnLightChange(data.ToArray()); } /// /// app请求设备信息 /// public void RequestDeviceIno() { List data = new List(); data.Add(0xAA);//起始码 data.Add(0x80);//命令号 data.Add(0x05);//长度 data.Add(0x85);//异或校验 data.Add(0x55);//结束码 serialPortInterface?.Write(data.ToArray()); Debug.Log("发送心跳包"); DevicesHolder.ins?.ShowConnectTip(); } #endregion #region 返回消息处理 /// /// 弹夹消息 /// /// private void OnMagazinesInfo(byte[] bytes) { Debug.Log($"{PortName} 收到弹夹消息!"); List data = new List(); data.Add(0x5C);//起始码 data.Add(bytes[3]);//命令号 0x01 0x00 SerialPortHelper.ins.OnMagazineChange(data.ToArray()); } private void OnLightChange(byte[] bytes)// 0xAA 0x83 0x06 0x01 0x84 0x55 { lightWaitClose = false; Debug.Log($"{PortName} 激光状态变更回包!"); var isOn = bytes[3] == 0x01; UserSettings.ins.lightState = isOn; } /// /// 设备信息响应 /// private void OnDeviceInfoBack(byte[] bytes)// 0xAA 0x80 0x07 0x01 0x01 0x89 0x5D { Debug.Log($"{PortName} 收到设备信息响应!"); var check = bytes[1] + bytes[2] + bytes[3] + bytes[4];//校验:命令+长度+数据内容 if (HomeView.ins == null) { return; } //0x01 HOUYI Pro //0x02 ARTEMIS Pro //0x03 Pistol 1 var deviceType = bytes[3];//设备类型 Debug.Log("设备类型 Device Type: " + deviceType); switch (deviceType) { case 0x01: UserSettings.ins.selectDevicesName = "HOUYI Pro"; DevicesHolder.ins.SwitchDeviceByType(AimDeviceType.HOUYIPRO); break; case 0x02: UserSettings.ins.selectDevicesName = "ARTEMIS Pro"; DevicesHolder.ins.SwitchDeviceByType(AimDeviceType.ARTEMISPRO); break; case 0x03: UserSettings.ins.selectDevicesName = "Pistol M9"; DevicesHolder.ins.SwitchDeviceByType(AimDeviceType.Gun); break; } UserSettings.ins.Save(); //刷新界面 var setting = FindAnyObjectByType(); setting?.FlushDeviceSelect(); } /// /// 射击消息 /// private void OnDeviceShoot(byte[] bytes) { LOG($"{PortName} 收到设备射击消息!"); var check = bytes[1] + bytes[2] + bytes[3] + bytes[4];//校验:命令+长度+数据内容 //if (check != bytes[5]) //{ // LOG("OnDeviceShoot 数据校验错误!"); //}else SerialPortHelper.shoot?.Invoke(bytes); } /// /// 按键消息 /// /// private void OnDeviceButton(byte[] bytes) { Debug.Log($"{PortName} 收到设备按键消息!"); var check = bytes[1] + bytes[2] + bytes[3]; //校验:命令+长度+数据内容 //if (check != bytes[4]) //{ // LOG("OnDeviceButton 数据校验错误!"); //} //else if (string.IsNullOrEmpty(UserSettings.ins.selectDevicesName)) //开机 RequestDeviceIno(); SerialPortHelper.aim?.Invoke(bytes); } #endregion #region 解析自定义缓存 private List dataCache = new List(); // 用于缓存接收到的数据 private const byte FrameHeader1 = 0xAA; // 帧头1 private const byte FrameHeader2 = 0x55; // 帧头2 // 尝试从缓存中解析出下一帧 private bool TryParseNextFrame(ref List cache, out byte[] frame) { frame = null; // 检查缓存中是否有足够的数据进行解析(至少需要 3 个字节:帧头 + 命令 + 数据长度) if (cache.Count < 3) return false; // 查找帧头:0xAA for (int i = 0; i < cache.Count - 1; i++) { if (cache[i] == 0xAA) { // 如果找到帧头,确保后面有足够的数据(命令 + 数据长度字节) if (cache.Count < i + 3) continue; byte command = cache[i + 1]; // 命令字节 byte dataLength = cache[i + 2]; // 数据长度字节(表示整个数据帧的总长度) // 如果数据长度不合理,跳过该帧 if (dataLength == 0 || cache.Count < i + dataLength) continue; // 计算校验值(命令 + 数据长度 + 数据部分) byte calculatedChecksum = command; calculatedChecksum ^= dataLength; for (int j = i + 3; j < i + dataLength - 2; j++) // 从数据部分开始 { calculatedChecksum ^= cache[j]; // 与数据内容进行异或 } byte checksum = cache[i + dataLength - 2]; // 校验字节位于数据部分之后 // 校验通过后,提取数据 if (checksum == calculatedChecksum) { // 获取完整的数据帧,包括帧头、命令、数据长度、数据、校验和帧尾 frame = cache.GetRange(i, dataLength).ToArray(); // 数据帧总长度 = 数据长度字节指定的长度 // 移除已经解析的帧 cache.RemoveRange(0, i + dataLength); // 更新缓存,移除已解析的部分 return true; } } } // 如果没有找到符合条件的帧,返回 false return false; } #endregion }