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

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.xdnf.cn/news/19671.html

相关文章:

  • 前端静态资源缓存与部署实践总结
  • 纯代码实现登录页面的DIY
  • 从零开始的python学习——函数(1)
  • uni-app支持单多选、搜索、查询、限制能否点击组件
  • SpringBoot @RefreshScope 注解的极致玩法
  • 从零开始的云计算生活——第五十五天,黑云压城,kubernetes模块之网络组件和CoreDNS组件
  • 一次诡异的报错排查:为什么时间戳变成了 ١٧٥٦٦٣٢٧٨
  • 云端虚拟手机:云手机的原理是什么?
  • SRE 系列(五)| MTTK/MTTF/MTTV:故障应急机制的三板斧
  • 低空经济的中国式进化:无人机与实时视频链路的未来五年
  • 后端笔试题-多线程JUC相关
  • 用滑动窗口与线性回归将音频信号转换为“Token”序列:一种简单的音频特征编码方法
  • 全栈智算系列直播回顾 | 智算中心对网络的需求与应对策略(下)
  • Linux开发必备:yum/vim/gcc/make全攻略
  • 大模型微调显存内存节约方法
  • 【ComfyUI】图像描述词润色总结
  • 基于若依框架前端学习VUE和TS的核心内容
  • 函数、数组与 grep + 正则表达式的 Linux Shell 编程进阶指南
  • windows10专业版系统安装本地化mysql服务端
  • AI公共数据分析完整实战教程:从原始数据到商业洞察【网络研讨会完整回放】
  • golang -- viper
  • Go语言运维实用入门:高效构建运维工具
  • 洽洽的“成本龙卷风”与渠道断层
  • MVC问题记录
  • Python备份实战专栏第5/6篇:Docker + Nginx 生产环境一键部署方案
  • 【机器学习入门】4.4 聚类的应用——从西瓜分类到防控,看无监督学习如何落地
  • Mac上如何安装mysql
  • 阿里云代理商:轻量应用服务器介绍及搭建个人博客教程参考
  • 【赵渝强老师】阿里云大数据MaxCompute的体系架构
  • Git基础使用和PR贡献