当前位置: 首页 > news >正文

Unity 串口通讯2 硬件SDK 开发[数据监听,按键监听]

关于如何进行串口通讯,可以查阅另一篇博客 Unity 串口通讯,本文介绍的是如何处理传的数据,这里提供一个十六进制数据的案例,供大家去学习。
数据案例 AA015500AA021407EEFF
数据说明

下标完整数值含义规则
00xAA固定数据头 1固定为 0xAA(帧起始标识,区分数据帧与干扰信号)
10x01帧序号从 0x01 开始自增
20x55固定数据头 2固定为 0x55(二次校验标识,与数据头 1 配合确认帧合法性)
30x01温度数据高位高位
40xAA温度数据低位低位
50x02湿度数据高位高位
60x14湿度数据低位低位
70x07按键值按键1为1,按键2为2,按键3为4等,按位标识按键
80xEE结束标识(高位)固定为 0xEE(帧结束高位,与低位配合确认帧完整性)
90xFF结束标识(低位)固定为 0xFF(帧结束低位)

先完成准备工作,串口通信中常用的 “高低位字节组合”,传2个8位16进制值,高位字节和低位字节,组合成一个16进制的数值,高位 0x01,低位0xAA
合并原理
‌高位字节左移8位‌:将 0x01 左移8位,得到 0x0100(即二进制 00000001 00000000)。
‌与低位字节相加‌:将移位后的结果与 0xAA 相加(或按位或 |),得到 0x01AA(二进制 00000001 10101010)。
‌转换为十进制‌:0x01AA = 1 × 256 + 10 × 16 + 10 × 1 = ‌426‌。
以此,用C#来实现

    /// <summary>/// 将高位和低位十六进制值组合成16位有符号整数/// </summary>public static float CombineHighLowBytes(string highByteHex, string lowByteHex){byte highByte = Convert.ToByte(highByteHex, 16);byte lowByte = Convert.ToByte(lowByteHex, 16);int combinedValue = (highByte << 8) | lowByte;return combinedValue;}

拓展一下需要的静态方法,比如收到的字节数组转化,字符分割,字符校验

    /// <summary>/// 将字节数组转换为十六进制字符串/// </summary>public static string BytesToHexString(byte[] bytes){if (bytes == null) return string.Empty;StringBuilder sb = new StringBuilder(bytes.Length * 2);foreach (byte b in bytes){sb.AppendFormat("{0:X2}", b);}return sb.ToString();}/// <summary>/// 将字符串分割为两个字符一组的数组/// </summary>public static string[] SplitIntoTwoCharParts(string input){if (string.IsNullOrEmpty(input)){return Array.Empty<string>();}List<string> parts = new List<string>();for (int i = 0; i < input.Length; i += 2){int length = (i + 2 <= input.Length) ? 2 : input.Length - i;parts.Add(input.Substring(i, length));}return parts.ToArray();}/// <summary>/// 检查字符串是否是有效的十六进制值/// </summary>public static bool IsHexString(string str){return str.All(c => char.IsDigit(c) || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'));}

根据提供的解析规则,处理数据包,解析除了按键之外的数据

    private const string PACKET_HEAD1 = "AA";         // 固定数据头1private const string PACKET_HEAD2 = "55";         // 固定数据头2private const string PACKET_TAIL_HIGH = "EE";     // 结束标识高位private const string PACKET_TAIL_LOW = "FF";      // 结束标识低位private const int EXPECTED_PACKET_PARTS = 10;     // 预期10个数据部分private void ProcessPacket(string packet){string[] parts = DataConversionUtility.SplitIntoTwoCharParts(packet);if (parts.Length != EXPECTED_PACKET_PARTS){Debug.LogError($"数据位数解析异常,期望{EXPECTED_PACKET_PARTS}个数据,实际{parts.Length}个");return;}try{// 验证数据头if (parts[0] != PACKET_HEAD1 || parts[2] != PACKET_HEAD2){Debug.LogError($"数据头验证失败,预期 {PACKET_HEAD1}{PACKET_HEAD2},实际 {parts[0]}{parts[2]}");return;}// 验证数据尾if (parts[8] != PACKET_TAIL_HIGH || parts[9] != PACKET_TAIL_LOW){Debug.LogError($"数据尾验证失败,预期 {PACKET_TAIL_HIGH}{PACKET_TAIL_LOW},实际 {parts[8]}{parts[9]}");return;}// 验证所有部分都是有效的十六进制if (!parts.All(DataConversionUtility.IsHexString)){Debug.LogError("数据包包含无效的十六进制值");return;}// 解析帧序号 (索引1)byte frameSequence = Convert.ToByte(parts[1], 16);OnFrameSequenceUpdated?.Invoke(frameSequence);// 解析温度数据 (索引3-4)float temperature = DataConversionUtility.CombineHighLowBytes(parts[3], parts[4]);// 解析湿度数据 (索引5-6)float humidity = DataConversionUtility.CombineHighLowBytes(parts[5], parts[6]);// 触发相应事件OnTemperatureUpdated?.Invoke(temperature);OnHumidityUpdated?.Invoke(humidity);// 输出日志Debug.Log($"帧序号:{frameSequence},温度:{temperature},湿度:{humidity}");}catch (Exception ex){Debug.LogError($"处理数据包时发生错误: {ex.Message}");}}

按键单独处理,先设定按键的常用状态

public enum KEYButtonState
{None,OnButtonDown,OnButtonUp,OnButtonClick
}

需要制定一个按键状态管理类,负责追踪和管理按键的状态变化,需要注意的是按键抖动和超时问题。

/// <summary>
/// 按键状态管理器,负责跟踪和管理按键状态变化
/// </summary>
public class KeyStateManager
{// 按键状态存储private Dictionary<int, KEYButtonState> _keyStates = new Dictionary<int, KEYButtonState>();// 上一次按下的按键记录private HashSet<int> _lastPressedKeys = new HashSet<int>();// 按键状态变化事件public event Action<int, KEYButtonState> OnKeyStateChanged;public KeyStateManager(){// 初始化1-8号按键状态为Nonefor (int i = 1; i <= 8; i++){_keyStates[i] = KEYButtonState.None;}}/// <summary>/// 更新按键状态并触发相应事件/// </summary>public void UpdateKeyStates(List<int> currentPressedKeys){HashSet<int> currentKeys = new HashSet<int>(currentPressedKeys);// 检查所有按键的状态变化for (int key = 1; key <= 8; key++){bool wasPressed = _lastPressedKeys.Contains(key);bool isPressed = currentKeys.Contains(key);KEYButtonState newState = _keyStates[key];// 状态转换逻辑if (!wasPressed && isPressed){newState = KEYButtonState.OnButtonDown;}else if (wasPressed && !isPressed){newState = KEYButtonState.OnButtonUp;}// 触发状态变化事件if (newState != _keyStates[key]){_keyStates[key] = newState;OnKeyStateChanged?.Invoke(key, newState);// 当按键松开时,触发点击事件if (newState == KEYButtonState.OnButtonUp){OnKeyStateChanged?.Invoke(key, KEYButtonState.OnButtonClick);}}}// 保存当前按键状态用于下次比较_lastPressedKeys = new HashSet<int>(currentKeys);// 重置瞬间状态ResetTransientStates();}/// <summary>/// 重置瞬间状态为None,避免持续触发/// </summary>private void ResetTransientStates(){foreach (int key in _keyStates.Keys.ToList()){if (_keyStates[key] != KEYButtonState.None){_keyStates[key] = KEYButtonState.None;}}}/// <summary>/// 处理超时情况,将所有按键视为松开/// </summary>public void HandleTimeout(){if (_lastPressedKeys.Count > 0){List<int> emptyKeys = new List<int>();UpdateKeyStates(emptyKeys);}}   
}

根据位运算规则,设定按键掩码和解析

    private const byte KEY_1_MASK = 0b00000001; // 1 按下1private const byte KEY_2_MASK = 0b00000010; // 2 按下2private const byte KEY_3_MASK = 0b00000100; // 4 按下3private const byte KEY_4_MASK = 0b00001000; // 8 按下4private const byte KEY_5_MASK = 0b00010000; // 16 按下5private const byte KEY_6_MASK = 0b00100000; // 32 以此类推private const byte KEY_7_MASK = 0b01000000;private const byte KEY_8_MASK = 0b10000000;/// <summary>/// 解析十六进制值,判断哪些按键被按下/// </summary>public List<int> ParseKeyPress(byte hexValue){List<int> pressedKeys = new List<int>();if ((hexValue & KEY_1_MASK) != 0) pressedKeys.Add(1);if ((hexValue & KEY_2_MASK) != 0) pressedKeys.Add(2);if ((hexValue & KEY_3_MASK) != 0) pressedKeys.Add(3);if ((hexValue & KEY_4_MASK) != 0) pressedKeys.Add(4);if ((hexValue & KEY_5_MASK) != 0) pressedKeys.Add(5);if ((hexValue & KEY_6_MASK) != 0) pressedKeys.Add(6);if ((hexValue & KEY_7_MASK) != 0) pressedKeys.Add(7);if ((hexValue & KEY_8_MASK) != 0) pressedKeys.Add(8);return pressedKeys;}

在数据包处理的方法中加入按键处理

    private readonly KeyStateManager _keyStateManager = new KeyStateManager();private float _lastMessageTime;private const float KEY_TIMEOUT = 1f;protected void Awake(){_keyStateManager.OnKeyStateChanged += LogKeyState;_lastMessageTime = Time.time;}/// <summary>/// 检查消息超时/// </summary>private void CheckMessageTimeout(){if (Time.time - _lastMessageTime > KEY_TIMEOUT){_keyStateManager.HandleTimeout();_lastMessageTime = Time.time;}}private void ProcessPacket(){//-------------从已有的逻辑部分增加byte keyValue = Convert.ToByte(parts[7], 16);List<int> pressedKeys = ParseKeyPress(keyValue);_keyStateManager.UpdateKeyStates(pressedKeys);Debug.Log($"按键:{string.Join(", ", pressedKeys)}");}/// <summary>/// 打印按键状态变化/// </summary>private void LogKeyState(int key, KEYButtonState state){string color = state switch{KEYButtonState.OnButtonDown => "green",KEYButtonState.OnButtonUp => "yellow",KEYButtonState.OnButtonClick => "blue",_ => "white"};Debug.Log($"<color={color}>[DataHandlerLog]按键[ {key} ]{GetKeyStateDescription(state)}</color>");}/// <summary>/// 获取按键状态的描述文本/// </summary>private string GetKeyStateDescription(KEYButtonState state){return state switch{KEYButtonState.OnButtonDown => "被按下",KEYButtonState.OnButtonUp => "被松开",KEYButtonState.OnButtonClick => "被点击",_ => "状态未知"};}

按键事件监听

    public event Action<float> OnTemperatureUpdated;public event Action<float> OnHumidityUpdated;public void SubscribeToKeyEvents(Action<int, KEYButtonState> listener){_keyStateManager.OnKeyStateChanged += listener;}public void UnsubscribeFromKeyEvents(Action<int, KEYButtonState> listener){_keyStateManager.OnKeyStateChanged -= listener;}

到这里位置,功能基本完善,但是还需要有个非常重要的处理,串口数据传输有个不可避免的问题,数据粘包。常用的解决方法是,添加一个缓冲区,接收到的数据不立刻处理,添加到缓冲区,然后从缓冲区去拿数据,拿一段去掉一段。

    private string _dataBuffer = "";/// <summary>/// 接收数据并加入缓冲区/// </summary>private void ReceiveData(string newData){_dataBuffer += newData;ParseBuffer();_lastMessageTime = Time.time;}/// <summary>/// 解析缓冲区中的数据,提取完整数据包/// </summary>private void ParseBuffer(){while (true){// 查找第一个数据头int head1Index = _dataBuffer.IndexOf(PACKET_HEAD1);if (head1Index == -1){_dataBuffer = "";break;}// 检查是否有足够的长度容纳第二个数据头if (head1Index + 2 >= _dataBuffer.Length){_dataBuffer = _dataBuffer.Substring(head1Index);break;}// 验证第二个数据头string potentialHead2 = _dataBuffer.Substring(head1Index + 4, 2); // 头1占2字节,帧序号占2字节if (potentialHead2 != PACKET_HEAD2){_dataBuffer = _dataBuffer.Substring(head1Index + 2);continue;}// 查找数据包尾部string remaining = _dataBuffer.Substring(head1Index);int tailIndex = remaining.IndexOf(PACKET_TAIL_HIGH + PACKET_TAIL_LOW);if (tailIndex == -1){_dataBuffer = _dataBuffer.Substring(head1Index);break;}// 提取完整数据包int packetLength = tailIndex + 4; // 尾部占4个字符(2字节)string completePacket = remaining.Substring(0, packetLength);ProcessPacket(completePacket);_dataBuffer = _dataBuffer.Substring(head1Index + packetLength);}}

模拟一下收到的信号数据,输出内容
在这里插入图片描述

实际上现在很多单片机已经支持直接传输中文字符串,处理起来更加简单,十六进制的话功耗会更小?
最后附上完整的数据解析代码

public class DataHandler : MonoSingleton<DataHandler>
{public bool isSimulation;// 事件定义public event Action<float> OnTemperatureUpdated;public event Action<float> OnHumidityUpdated;public event Action<byte> OnFrameSequenceUpdated;// 按键处理相关private readonly KeyStateManager _keyStateManager = new KeyStateManager();private float _lastMessageTime;private const float KEY_TIMEOUT = 1f;// 数据处理相关 - 根据新规则定义private string _dataBuffer = "";private const string PACKET_HEAD1 = "AA";         // 固定数据头1private const string PACKET_HEAD2 = "55";         // 固定数据头2private const string PACKET_TAIL_HIGH = "EE";     // 结束标识高位private const string PACKET_TAIL_LOW = "FF";      // 结束标识低位private const int EXPECTED_PACKET_PARTS = 10;     // 预期10个数据部分protected void Awake(){_keyStateManager.OnKeyStateChanged += LogKeyState;_lastMessageTime = Time.time;}private void Start(){//没有硬件的时候 unirx 模拟数据接收Observable.Interval(TimeSpan.FromSeconds(0.1f)).Subscribe(_ => { if (isSimulation) ReceiveData("AA015500AA021407EEFF"); }).AddTo(this);}protected virtual void Update(){CheckMessageTimeout();}/// <summary>/// 检查消息超时/// </summary>private void CheckMessageTimeout(){if (Time.time - _lastMessageTime > KEY_TIMEOUT){_keyStateManager.HandleTimeout();_lastMessageTime = Time.time;}}/// <summary>/// 处理接收到的完整数据/// </summary>public void ReadComplateString(object data){try{string hexString = data switch{byte[] byteData => DataConversionUtility.BytesToHexString(byteData),string stringData => stringData,_ => throw new ArgumentException($"未知的数据类型: {data.GetType()}")};if (!string.IsNullOrEmpty(hexString)){Debug.Log($"接收到数据: {hexString}");ReceiveData(hexString);}}catch (Exception ex){Debug.LogError($"处理接收数据时发生错误: {ex.Message}");}}/// <summary>/// 接收数据并加入缓冲区/// </summary>private void ReceiveData(string newData){_dataBuffer += newData;ParseBuffer();_lastMessageTime = Time.time;}/// <summary>/// 解析缓冲区中的数据,提取完整数据包/// </summary>private void ParseBuffer(){while (true){// 查找第一个数据头int head1Index = _dataBuffer.IndexOf(PACKET_HEAD1);if (head1Index == -1){_dataBuffer = "";break;}// 检查是否有足够的长度容纳第二个数据头if (head1Index + 2 >= _dataBuffer.Length){_dataBuffer = _dataBuffer.Substring(head1Index);break;}// 验证第二个数据头string potentialHead2 = _dataBuffer.Substring(head1Index + 4, 2); // 头1占2字节,帧序号占2字节if (potentialHead2 != PACKET_HEAD2){_dataBuffer = _dataBuffer.Substring(head1Index + 2);continue;}// 查找数据包尾部string remaining = _dataBuffer.Substring(head1Index);int tailIndex = remaining.IndexOf(PACKET_TAIL_HIGH + PACKET_TAIL_LOW);if (tailIndex == -1){_dataBuffer = _dataBuffer.Substring(head1Index);break;}// 提取完整数据包int packetLength = tailIndex + 4; // 尾部占4个字符(2字节)string completePacket = remaining.Substring(0, packetLength);ProcessPacket(completePacket);_dataBuffer = _dataBuffer.Substring(head1Index + packetLength);}}/// <summary>/// 处理完整的数据包/// </summary>private void ProcessPacket(string packet){string[] parts = DataConversionUtility.SplitIntoTwoCharParts(packet);if (parts.Length != EXPECTED_PACKET_PARTS){Debug.LogError($"数据位数解析异常,期望{EXPECTED_PACKET_PARTS}个数据,实际{parts.Length}个");return;}try{// 验证数据头if (parts[0] != PACKET_HEAD1 || parts[2] != PACKET_HEAD2){Debug.LogError($"数据头验证失败,预期 {PACKET_HEAD1}{PACKET_HEAD2},实际 {parts[0]}{parts[2]}");return;}// 验证数据尾if (parts[8] != PACKET_TAIL_HIGH || parts[9] != PACKET_TAIL_LOW){Debug.LogError($"数据尾验证失败,预期 {PACKET_TAIL_HIGH}{PACKET_TAIL_LOW},实际 {parts[8]}{parts[9]}");return;}// 验证所有部分都是有效的十六进制if (!parts.All(DataConversionUtility.IsHexString)){Debug.LogError("数据包包含无效的十六进制值");return;}// 解析帧序号 (索引1)byte frameSequence = Convert.ToByte(parts[1], 16);OnFrameSequenceUpdated?.Invoke(frameSequence);// 解析温度数据 (索引3-4)float temperature = DataConversionUtility.CombineHighLowBytes(parts[3], parts[4]);// 解析湿度数据 (索引5-6)float humidity = DataConversionUtility.CombineHighLowBytes(parts[5], parts[6]);// 处理按键数据 (索引7)byte keyValue = Convert.ToByte(parts[7], 16);List<int> pressedKeys = ParseKeyPress(keyValue);_keyStateManager.UpdateKeyStates(pressedKeys);// 触发相应事件OnTemperatureUpdated?.Invoke(temperature);OnHumidityUpdated?.Invoke(humidity);// 输出日志Debug.Log($"帧序号:{frameSequence},温度:{temperature},湿度:{humidity},按键:{string.Join(", ", pressedKeys)}");}catch (Exception ex){Debug.LogError($"处理数据包时发生错误: {ex.Message}");}}#region 按键解析// 按键掩码定义private const byte KEY_1_MASK = 0b00000001; // 1private const byte KEY_2_MASK = 0b00000010; // 2private const byte KEY_3_MASK = 0b00000100; // 4private const byte KEY_4_MASK = 0b00001000; // 8private const byte KEY_5_MASK = 0b00010000; // 16private const byte KEY_6_MASK = 0b00100000; // 32private const byte KEY_7_MASK = 0b01000000; private const byte KEY_8_MASK = 0b10000000; /// <summary>/// 解析十六进制值,判断哪些按键被按下/// </summary>public List<int> ParseKeyPress(byte hexValue){List<int> pressedKeys = new List<int>();if ((hexValue & KEY_1_MASK) != 0) pressedKeys.Add(1);if ((hexValue & KEY_2_MASK) != 0) pressedKeys.Add(2);if ((hexValue & KEY_3_MASK) != 0) pressedKeys.Add(3);if ((hexValue & KEY_4_MASK) != 0) pressedKeys.Add(4);if ((hexValue & KEY_5_MASK) != 0) pressedKeys.Add(5);if ((hexValue & KEY_6_MASK) != 0) pressedKeys.Add(6);if ((hexValue & KEY_7_MASK) != 0) pressedKeys.Add(7);if ((hexValue & KEY_8_MASK) != 0) pressedKeys.Add(8);return pressedKeys;}#endregion#region 事件订阅public void SubscribeToKeyEvents(Action<int, KEYButtonState> listener){_keyStateManager.OnKeyStateChanged += listener;}public void UnsubscribeFromKeyEvents(Action<int, KEYButtonState> listener){_keyStateManager.OnKeyStateChanged -= listener;}#endregion/// <summary>/// 打印按键状态变化/// </summary>private void LogKeyState(int key, KEYButtonState state){string color = state switch{KEYButtonState.OnButtonDown => "green",KEYButtonState.OnButtonUp => "yellow",KEYButtonState.OnButtonClick => "blue",_ => "white"};Debug.Log($"<color={color}>[DataHandlerLog]按键[ {key} ]{GetKeyStateDescription(state)}</color>");}/// <summary>/// 获取按键状态的描述文本/// </summary>private string GetKeyStateDescription(KEYButtonState state){return state switch{KEYButtonState.OnButtonDown => "被按下",KEYButtonState.OnButtonUp => "被松开",KEYButtonState.OnButtonClick => "被点击",_ => "状态未知"};}
}/// <summary>
/// 数据转换工具类,封装通用的数据转换方法
/// </summary>
public static class DataConversionUtility
{/// <summary>/// 将字节数组转换为十六进制字符串/// </summary>public static string BytesToHexString(byte[] bytes){if (bytes == null) return string.Empty;StringBuilder sb = new StringBuilder(bytes.Length * 2);foreach (byte b in bytes){sb.AppendFormat("{0:X2}", b);}return sb.ToString();}/// <summary>/// 将字符串分割为两个字符一组的数组/// </summary>public static string[] SplitIntoTwoCharParts(string input){if (string.IsNullOrEmpty(input)){return Array.Empty<string>();}List<string> parts = new List<string>();for (int i = 0; i < input.Length; i += 2){int length = (i + 2 <= input.Length) ? 2 : input.Length - i;parts.Add(input.Substring(i, length));}return parts.ToArray();}/// <summary>/// 检查字符串是否是有效的十六进制值/// </summary>public static bool IsHexString(string str){return str.All(c => char.IsDigit(c) || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'));}/// <summary>/// 将高位和低位十六进制值组合成16位有符号整数/// </summary>public static float CombineHighLowBytes(string highByteHex, string lowByteHex){byte highByte = Convert.ToByte(highByteHex, 16);byte lowByte = Convert.ToByte(lowByteHex, 16);int combinedValue = (highByte << 8) | lowByte;return combinedValue;}
}
http://www.dtcms.com/a/362106.html

相关文章:

  • 人工智能——课程考核
  • Python OpenCV图像处理与深度学习:Python OpenCV图像几何变换入门
  • 线程池发生了异常该怎么处理?
  • Groovy 的核心语法
  • 计算机视觉与深度学习 | 传统图像处理技术的未来发展前景分析
  • 算法练习——169.多数元素
  • 焦耳热技术助力顶刊研究:薄层质子交换膜实现高效水电解制氢
  • 深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)第八章知识点问答(18题)
  • 在工业质检中,机器视觉与人工检测的决策依据
  • Java类加载机制
  • 亚马逊云代理商:如何选择适合的AWS EC2实例类型?
  • ARM-SPI屏幕案例
  • 1. 叙述与命题
  • 【开题答辩全过程】以 基于JSP的养生网站设计与实现为例,包含答辩的问题和答案
  • 在JAVA中Mybatis的使用
  • GitHub每日最火火火项目(9.1)
  • TDengine 日期时间函数 DAYOFWEEK 使用手册
  • shell编程基础入门-3
  • 人工势场法(APF)路径规划 MATLAB
  • 战略进阶——解读92页培训_战略+概述与基本框架麦肯锡【附全文阅读】
  • 一个好的智能体框架应该是什么样子
  • Transformer的并行计算与长序列处理瓶颈总结
  • Solid Explorer文件管理器:功能强大的安卓文件管理器及网盘文件管理器
  • 2025年职场人士专业证书选择与分析
  • 从 “对话” 到 “共创”:生成式 AI 如何重塑内容创作全流程,普通人也能掌握的高效工具指南
  • Windows 安装配置解压版MongoDb
  • ‌ C++ 继承与派生类
  • 从DevOps到BizDevOps:哪些DevOps工具能够成为业务创新加速引擎?
  • Java网络编程基础 Socket通信入门指南
  • 『C++成长记』vector模拟实现