ArrowSerialPort.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. using System;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using MySerialPortInterface;
  5. public class ArrowSerialPort : MonoBehaviour
  6. {
  7. //使用自定义串口
  8. private SerialPortInterface serialPortInterface;
  9. private int baudrate = 115200;
  10. //private int baudrate = 921600;
  11. //private int baudrate = 460800;
  12. private string PortName = "/dev/ttyS0";
  13. //private static Dictionary<string, int> _isInit = new();
  14. public bool testMode = true;
  15. private void Start()
  16. {
  17. //if (!_isInit.TryGetValue(gameObject.name, out int instanceID) && instanceID == 0)
  18. //{
  19. // DontDestroyOnLoad(gameObject);
  20. // Init();
  21. //}
  22. DontDestroyOnLoad(gameObject);
  23. Init();
  24. }
  25. private void LOG(string msg, bool warning = false)
  26. {
  27. if (!warning)
  28. Debug.Log($"<color=#00FF00>{msg}</color>");
  29. else
  30. Debug.LogWarning($"<color=#00FF00>{msg}</color>");
  31. }
  32. private void Init()
  33. {
  34. //_isInit[gameObject.name] = GetInstanceID();
  35. #if UNITY_ANDROID && !UNITY_EDITOR
  36. serialPortInterface = gameObject.GetComponent<SerialPortInterface>();
  37. serialPortInterface.SystemEventObject.AddListener(SystemEventObject);
  38. TrySerialOpenPort();
  39. #endif
  40. #if UNITY_EDITOR
  41. if (testMode)
  42. {
  43. var testBoard = Resources.Load<GameObject>("SerialPortTest");
  44. GameObject.Instantiate(testBoard);
  45. }
  46. //模拟连接设备
  47. //List<byte> data = new List<byte>();
  48. //data.Add(0xaa);
  49. //data.Add(0x80);
  50. //data.Add(0x07);
  51. //data.Add(0x02);
  52. //data.Add(0x01);
  53. //data.Add(0x84);
  54. //data.Add(0x55);
  55. //HandleSerialPortUpdate(data.ToArray(), 7);
  56. #endif
  57. }
  58. private void TrySerialOpenPort()
  59. {
  60. serialPortInterface?.Open();
  61. }
  62. private void SystemEventObject(SerialPortInterface arg0, string msg)
  63. {
  64. // || msg.Equals("PERMISSION_ERROR")
  65. if (msg.Equals("OPEN_ERROR"))//串口开启失败 定时重试
  66. {
  67. #if UNITY_ANDROID && !UNITY_EDITOR
  68. Invoke("TrySerialOpenPort", 1f);
  69. #endif
  70. // LOG($"{PortName} 串口打开失败 重试中", true);
  71. }
  72. else if (msg.Equals("OPENED"))
  73. {
  74. SerialPortHelper.ins.OnConnect(arg0.DeviceName);
  75. //应用启动 间隔十秒请求一次设备信息
  76. CancelInvoke("RequestDeviceIno");
  77. InvokeRepeating("RequestDeviceIno", 0f, 10f);
  78. //LOG($"{PortName} 串口打开成功!");
  79. ////激光默认打开
  80. //RequestLightState(true);
  81. }
  82. else if (msg.Equals("CLOSED"))//串口断开
  83. {
  84. SerialPortHelper.ins.OnDisConnect(arg0.DeviceName);
  85. LOG($"{PortName} 串口关闭!");
  86. }
  87. }
  88. private void OnDestroy()
  89. {
  90. //if (_isInit.TryGetValue(gameObject.name, out var instanceID) && instanceID == GetInstanceID())
  91. //{
  92. // LOG($"{PortName} 串口关闭");
  93. // SerialPortHelper.ins.OnDisConnect(serialPortInterface?.DeviceName);
  94. // serialPortInterface?.Close();
  95. // _isInit.Remove(gameObject.name);
  96. //}
  97. }
  98. /// <summary>
  99. /// 串口读取二进制流数据(界面挂载调用)
  100. /// </summary>
  101. /// <param name="data"></param>
  102. public void ReadStreamingBinary(object data)
  103. {
  104. var realData = new List<byte>();
  105. var tempData = data as byte[];
  106. for (int i = 0; i < tempData.Length; i++)
  107. {
  108. realData.Add(tempData[i]);
  109. if (tempData.Length > i + 1)
  110. {
  111. if (tempData[i + 1] == 0XAA) //多条消息 粘包位置
  112. {
  113. //把上一条发送
  114. PhraseData(realData.ToArray());
  115. realData.Clear();
  116. }
  117. }
  118. else
  119. PhraseData(realData.ToArray());
  120. }
  121. }
  122. //自定义端口使用
  123. public void HandleSerialPortUpdate(byte[] buffer, int size)
  124. {
  125. //Debug.Log($"Received Data: {BitConverter.ToString(buffer)} of size {size}");
  126. //PhraseData(buffer);
  127. // 将新的数据加入缓存
  128. dataCache.AddRange(buffer);
  129. // 尝试解析缓存中的数据
  130. while (TryParseNextFrame(ref dataCache, out byte[] frame))
  131. {
  132. // 如果成功解析一帧数据,处理该帧
  133. // Debug.Log($"[ArrowSerialPort] frame Data: {BitConverter.ToString(frame)}");
  134. PhraseData(frame);
  135. }
  136. }
  137. public void TestRead(byte[] bytes)
  138. {
  139. ReadStreamingBinary(bytes);
  140. }
  141. /// <summary>
  142. /// 解析串口数据
  143. /// </summary>
  144. /// <param name="bytes"></param>
  145. private void PhraseData(byte[] bytes)
  146. {
  147. if (bytes[0] == 0xAA)
  148. {
  149. string msg = string.Empty;
  150. for (int i = 0; i < bytes.Length; i++)
  151. {
  152. msg += bytes[i].ToString("x2") + " ";
  153. }
  154. // 获取当前时间
  155. DateTime currentTime = DateTime.Now;
  156. LOG($"[ArrowSerialPort] {PortName} 收到串口信息 msg={msg}!,接收时间:" + currentTime.ToString("yyyy-MM-dd HH:mm:ss.fff"));
  157. var cmdID = bytes[1];
  158. switch (cmdID)
  159. {
  160. case 0x80://设备信息响应
  161. OnDeviceInfoBack(bytes);
  162. break;
  163. case 0x81://射击消息
  164. if (CommonConfig.bDisableBluetooth) {
  165. // 获取当前时间
  166. DateTime currentTime1 = DateTime.Now;
  167. // 打印带毫秒的时间戳
  168. Debug.Log("射箭前:CurrentTime1: " + currentTime1.ToString("yyyy-MM-dd HH:mm:ss.fff"));
  169. //3 - APP在收到射箭消息后,立即回复相同的内容给设备端;
  170. serialPortInterface?.Write(bytes);
  171. DateTime currentTime3 = DateTime.Now;
  172. // 打印带毫秒的时间戳
  173. Debug.Log("*** 回复相同的内容给设备端 CurrentTime3: " + currentTime3.ToString("yyyy-MM-dd HH:mm:ss.fff"));
  174. }
  175. OnDeviceShoot(bytes);
  176. if (CommonConfig.bDisableBluetooth)
  177. {
  178. // 获取当前时间
  179. DateTime currentTime2 = DateTime.Now;
  180. // 打印带毫秒的时间戳
  181. Debug.Log("射箭后:CurrentTime2: " + currentTime2.ToString("yyyy-MM-dd HH:mm:ss.fff"));
  182. }
  183. break;
  184. case 0x82://按键消息
  185. OnDeviceButton(bytes);
  186. break;
  187. case 0x83://激光控制
  188. OnLightChange(bytes);
  189. break;
  190. case 0x84://弹夹消息(仅手枪)
  191. OnMagazinesInfo(bytes);
  192. break;
  193. }
  194. }
  195. else
  196. {
  197. if (CommonConfig.bDisableBluetooth) {
  198. //byte[] bytes = new byte[] { 0x0A, 0x00, 0x00, 0x00, // 1st timestamp
  199. // 0x0B, 0x00, 0x00, 0x00, // 2nd timestamp
  200. // 0x00, 0x01, 0x00, 0x00 // 3rd timestamp
  201. //};
  202. if (bytes.Length == 12)
  203. {
  204. // 解析并转换时间戳
  205. for (int i = 0; i < bytes.Length; i += 4)
  206. {
  207. // 提取 4 字节的时间戳
  208. uint timestamp = BitConverter.ToUInt32(bytes, i);
  209. // 转换为毫秒(单位是 100us)
  210. float timeInMilliseconds = timestamp * 0.1f;
  211. // 打印转换结果
  212. Debug.Log($"Timestamp (U32): {timestamp} => Time: {timeInMilliseconds} ms");
  213. }
  214. }
  215. }
  216. }
  217. }
  218. bool lightWaitClose = false;
  219. public void DelayCloseLight()
  220. {
  221. if (!lightWaitClose && hasOnceInceCoin)
  222. {
  223. LOG("游戏时间结束 延迟关闭激光");
  224. Invoke("DoDelayCloseLight", 60f);
  225. lightWaitClose = true;
  226. }
  227. }
  228. public void CancelDelayCloseLight()
  229. {
  230. LOG("关闭延迟关闭激光");
  231. lightWaitClose = false;
  232. CancelInvoke("DoDelayCloseLight");
  233. }
  234. private void DoDelayCloseLight()
  235. {
  236. RequestLightState(false);
  237. }
  238. //是否有过首次投币 没投币过 不关闭摄像头
  239. bool hasOnceInceCoin = false;
  240. #region APP请求
  241. // 异或校验内容:命令+长度+数据内容
  242. /// <summary>
  243. /// 设置激光状态
  244. /// </summary>
  245. public void RequestLightState(bool on, bool insertCoin = false)
  246. {
  247. if (insertCoin)
  248. hasOnceInceCoin = true;
  249. List<byte> data = new List<byte>();
  250. data.Add(0xAA);//起始码
  251. data.Add(0x83);//命令号
  252. data.Add(0x06);//长度
  253. data.Add((byte)(on ? 0x01 : 0x00));//长度
  254. byte temp = 0;
  255. for (int i = 1; i < data.Count; i++)
  256. {
  257. temp ^= data[i];
  258. }
  259. data.Add(temp);//异或校验
  260. data.Add(0x55);//结束码
  261. serialPortInterface?.Write(data.ToArray());
  262. LOG($"设置激光状态:{on}!");
  263. OnLightChange(data.ToArray());
  264. }
  265. /// <summary>
  266. /// app请求设备信息
  267. /// </summary>
  268. public void RequestDeviceIno()
  269. {
  270. List<byte> data = new List<byte>();
  271. data.Add(0xAA);//起始码
  272. data.Add(0x80);//命令号
  273. data.Add(0x05);//长度
  274. data.Add(0x85);//异或校验
  275. data.Add(0x55);//结束码
  276. serialPortInterface?.Write(data.ToArray());
  277. Debug.Log("发送心跳包");
  278. DevicesHolder.ins?.ShowConnectTip();
  279. }
  280. #endregion
  281. #region 返回消息处理
  282. /// <summary>
  283. /// 弹夹消息
  284. /// </summary>
  285. /// <param name="bytes"></param>
  286. private void OnMagazinesInfo(byte[] bytes)
  287. {
  288. Debug.Log($"{PortName} 收到弹夹消息!");
  289. SerialPortHelper.ins.OnMagazineChange(bytes);
  290. }
  291. private void OnLightChange(byte[] bytes)// 0xAA 0x83 0x06 0x01 0x84 0x55
  292. {
  293. lightWaitClose = false;
  294. Debug.Log($"{PortName} 激光状态变更回包!");
  295. var isOn = bytes[3] == 0x01;
  296. UserSettings.ins.lightState = isOn;
  297. }
  298. /// <summary>
  299. /// 设备信息响应
  300. /// </summary>
  301. private void OnDeviceInfoBack(byte[] bytes)// 0xAA 0x80 0x07 0x01 0x01 0x89 0x5D
  302. {
  303. Debug.Log($"{PortName} 收到设备信息响应!");
  304. var check = bytes[1] + bytes[2] + bytes[3] + bytes[4];//校验:命令+长度+数据内容
  305. if (HomeView.ins == null) {
  306. return;
  307. }
  308. //0x01 HOUYI Pro
  309. //0x02 ARTEMIS Pro
  310. //0x03 Pistol 1
  311. var deviceType = bytes[3];//设备类型
  312. Debug.Log("设备类型 Device Type: " + deviceType);
  313. switch (deviceType)
  314. {
  315. case 0x01:
  316. UserSettings.ins.selectDevicesName = "HOUYI Pro";
  317. DevicesHolder.ins.SwitchDeviceByType(AimDeviceType.HOUYIPRO);
  318. break;
  319. case 0x02:
  320. UserSettings.ins.selectDevicesName = "ARTEMIS Pro";
  321. DevicesHolder.ins.SwitchDeviceByType(AimDeviceType.ARTEMISPRO);
  322. break;
  323. case 0x03:
  324. UserSettings.ins.selectDevicesName = "Pistol M9";
  325. DevicesHolder.ins.SwitchDeviceByType(AimDeviceType.Gun);
  326. break;
  327. }
  328. UserSettings.ins.Save();
  329. //刷新界面
  330. var setting = FindAnyObjectByType<CustomUIView.BoxUserSettings>();
  331. setting?.FlushDeviceSelect();
  332. }
  333. /// <summary>
  334. /// 射击消息
  335. /// </summary>
  336. private void OnDeviceShoot(byte[] bytes)
  337. {
  338. LOG($"{PortName} 收到设备射击消息!");
  339. var check = bytes[1] + bytes[2] + bytes[3] + bytes[4];//校验:命令+长度+数据内容
  340. //if (check != bytes[5])
  341. //{
  342. // LOG("OnDeviceShoot 数据校验错误!");
  343. //}else
  344. SerialPortHelper.shoot?.Invoke(bytes);
  345. }
  346. /// <summary>
  347. /// 按键消息
  348. /// </summary>
  349. /// <param name="bytes"></param>
  350. private void OnDeviceButton(byte[] bytes)
  351. {
  352. Debug.Log($"{PortName} 收到设备按键消息!");
  353. var check = bytes[1] + bytes[2] + bytes[3];
  354. //校验:命令+长度+数据内容
  355. //if (check != bytes[4])
  356. //{
  357. // LOG("OnDeviceButton 数据校验错误!");
  358. //}
  359. //else
  360. if (string.IsNullOrEmpty(UserSettings.ins.selectDevicesName))
  361. //开机
  362. RequestDeviceIno();
  363. SerialPortHelper.aim?.Invoke(bytes);
  364. }
  365. #endregion
  366. #region 解析自定义缓存
  367. private List<byte> dataCache = new List<byte>(); // 用于缓存接收到的数据
  368. private const byte FrameHeader1 = 0xAA; // 帧头1
  369. private const byte FrameHeader2 = 0x55; // 帧头2
  370. // 尝试从缓存中解析出下一帧
  371. private bool TryParseNextFrame(ref List<byte> cache, out byte[] frame)
  372. {
  373. frame = null;
  374. // 检查缓存中是否有足够的数据进行解析(至少需要 3 个字节:帧头 + 命令 + 数据长度)
  375. if (cache.Count < 3)
  376. return false;
  377. // 查找帧头:0xAA
  378. for (int i = 0; i < cache.Count - 1; i++)
  379. {
  380. if (cache[i] == 0xAA)
  381. {
  382. // 如果找到帧头,确保后面有足够的数据(命令 + 数据长度字节)
  383. if (cache.Count < i + 3)
  384. continue;
  385. byte command = cache[i + 1]; // 命令字节
  386. byte dataLength = cache[i + 2]; // 数据长度字节(表示整个数据帧的总长度)
  387. // 如果数据长度不合理,跳过该帧
  388. if (dataLength == 0 || cache.Count < i + dataLength)
  389. continue;
  390. // 计算校验值(命令 + 数据长度 + 数据部分)
  391. byte calculatedChecksum = command;
  392. calculatedChecksum ^= dataLength;
  393. for (int j = i + 3; j < i + dataLength - 2; j++) // 从数据部分开始
  394. {
  395. calculatedChecksum ^= cache[j]; // 与数据内容进行异或
  396. }
  397. byte checksum = cache[i + dataLength - 2]; // 校验字节位于数据部分之后
  398. // 校验通过后,提取数据
  399. if (checksum == calculatedChecksum)
  400. {
  401. // 获取完整的数据帧,包括帧头、命令、数据长度、数据、校验和帧尾
  402. frame = cache.GetRange(i, dataLength).ToArray(); // 数据帧总长度 = 数据长度字节指定的长度
  403. // 移除已经解析的帧
  404. cache.RemoveRange(0, i + dataLength); // 更新缓存,移除已解析的部分
  405. return true;
  406. }
  407. }
  408. }
  409. // 如果没有找到符合条件的帧,返回 false
  410. return false;
  411. }
  412. #endregion
  413. }