程序代码篇---数据包解析

数据包解析是不同设备(如电脑、ESP32 等嵌入式设备)之间通信的核心环节。简单说,就是把收到的 "一串数据" 翻译成双方都能理解的 "具体信息"(比如温度、湿度、命令等)。下面介绍几种常见的数据包格式,以及 Python 和 ESP32(基于 Arduino 框架)的解析代码,尽量用通俗的语言解释。

一、文本格式(最容易理解)

文本格式的数据包由字符串组成,人类可以直接看懂,适合简单场景。常见的有CSV 格式JSON 格式

1. CSV 格式(逗号分隔值)

特点 :用逗号(或其他符号)分隔不同字段,结构简单,类似表格。
适用场景 :传感器批量数据(如温度、湿度、时间)、简单配置信息。
示例数据包"25.6,60.2,202310011200"(温度 = 25.6℃,湿度 = 60.2%,时间 = 2023-10-01 12:00)

Python 解析代码

用内置的split函数分割字符串即可。

python 复制代码
# 假设收到的CSV数据包
recv_data = "25.6,60.2,202310011200"

# 解析步骤
fields = recv_data.split(',')  # 用逗号分割
if len(fields) == 3:  # 检查字段数量是否正确
    temperature = float(fields[0])
    humidity = float(fields[1])
    time = fields[2]
    print(f"温度:{temperature}℃,湿度:{humidity}%,时间:{time}")
else:
    print("数据格式错误")

ESP32 解析代码

ESP32 通过串口 / 网络收到字符串后,用strtok函数分割(类似 Python 的 split)。

cpp 复制代码
#include <Arduino.h>

void setup() {
  Serial.begin(115200);  // 初始化串口
}

void loop() {
  if (Serial.available()) {  // 检查是否有数据
    String recv_str = Serial.readStringUntil('\n');  // 读取一行数据(假设以换行结束)
    recv_str.trim();  // 去掉首尾空格和换行
    
    // 用逗号分割字符串
    char* data_ptr = strtok((char*)recv_str.c_str(), ",");  // 第一个字段
    float temperature, humidity;
    String time_str;
    int count = 0;
    
    while (data_ptr != NULL) {
      if (count == 0) {
        temperature = atof(data_ptr);  // 转成浮点数(温度)
      } else if (count == 1) {
        humidity = atof(data_ptr);  // 转成浮点数(湿度)
      } else if (count == 2) {
        time_str = String(data_ptr);  // 时间字符串
      }
      data_ptr = strtok(NULL, ",");  // 下一个字段
      count++;
    }
    
    if (count == 3) {  // 确认解析到3个字段
      Serial.printf("温度:%.1f℃,湿度:%.1f%,时间:%s\n", temperature, humidity, time_str.c_str());
    } else {
      Serial.println("CSV格式错误");
    }
  }
}
2. JSON 格式(键值对结构)

特点 :用{键:值}的结构表示数据,支持嵌套(比如数组、对象),适合复杂数据。
适用场景 :API 接口、带层级的配置信息(如 "设备信息 + 传感器数据")。
示例数据包{"device_id":"esp32_01","data":{"temp":25.6,"hum":60.2},"time":"202310011200"}

Python 解析代码

用内置的json模块直接转成字典,方便取值。

python 复制代码
import json

# 假设收到的JSON数据包
recv_data = '{"device_id":"esp32_01","data":{"temp":25.6,"hum":60.2},"time":"202310011200"}'

try:
    # 解析JSON字符串为字典
    data_dict = json.loads(recv_data)
    # 提取数据
    device_id = data_dict["device_id"]
    temp = data_dict["data"]["temp"]
    hum = data_dict["data"]["hum"]
    time = data_dict["time"]
    print(f"设备:{device_id},温度:{temp}℃,湿度:{hum}%,时间:{time}")
except json.JSONDecodeError:
    print("JSON格式错误")
except KeyError as e:
    print(f"缺少字段:{e}")

ESP32 解析代码

需要用ArduinoJson库(需在 Arduino 库管理器中安装),处理 JSON 结构。

cpp 复制代码
#include <Arduino.h>
#include <ArduinoJson.h>  // 引入JSON库

void setup() {
  Serial.begin(115200);
}

void loop() {
  if (Serial.available()) {
    String recv_str = Serial.readStringUntil('\n');
    recv_str.trim();
    
    // 分配JSON缓冲区(根据数据大小调整,这里用1024字节)
    StaticJsonDocument<1024> doc;
    // 解析JSON
    DeserializationError error = deserializeJson(doc, recv_str);
    
    if (error) {  // 解析失败
      Serial.printf("JSON解析错误:%s\n", error.c_str());
      return;
    }
    
    // 提取数据(注意判断字段是否存在)
    if (doc.containsKey("device_id") && doc.containsKey("data") && doc.containsKey("time")) {
      const char* device_id = doc["device_id"];
      float temp = doc["data"]["temp"];
      float hum = doc["data"]["hum"];
      const char* time_str = doc["time"];
      
      Serial.printf("设备:%s,温度:%.1f℃,湿度:%.1f%,时间:%s\n", 
                   device_id, temp, hum, time_str);
    } else {
      Serial.println("JSON缺少必要字段");
    }
  }
}

二、二进制格式(更高效)

文本格式虽然易读,但占空间大(比如数字 "25.6" 要占 4 个字符),传输效率低。二进制格式直接用字节存储数据(比如 25.6℃可以用 2 个字节存储),更适合嵌入式设备(如 ESP32)之间的高速通信。常见的有固定长度格式TLV 格式

1. 固定长度格式

特点 :每个字段的长度固定(比如温度占 2 字节,湿度占 2 字节),解析时按固定位置取数据,速度快。
适用场景 :实时性要求高的场景(如传感器每秒传输一次数据)。
示例数据包结构(共 5 字节):

字段 长度(字节) 说明
温度 2 用 int16_t 存储(实际值 = 存储值 / 10,比如 0x00FA=250 → 25.0℃)
湿度 2 同上(0x0258=600 → 60.0%)
状态 1 0 = 正常,1 = 异常

对应的二进制数据:0x00 0xFA 0x02 0x58 0x00(即温度 25.0℃,湿度 60.0%,状态正常)

Python 解析代码

struct模块解析二进制数据(需要知道每个字段的类型和顺序)。

python 复制代码
import struct

# 假设收到的二进制数据(5字节)
recv_bytes = b'\x00\xFA\x02\x58\x00'

if len(recv_bytes) == 5:  # 检查长度是否正确
    # 解析:2字节温度(int16)、2字节湿度(int16)、1字节状态(uint8)
    temp_raw, hum_raw, status = struct.unpack('>hhu', recv_bytes)
    # 转换为实际值(除以10)
    temperature = temp_raw / 10.0
    humidity = hum_raw / 10.0
    print(f"温度:{temperature}℃,湿度:{humidity}%,状态:{'正常' if status == 0 else '异常'}")
else:
    print("二进制数据长度错误")

>hhu表示:>大端模式,hint16,hint16,uuint8)

ESP32 解析代码

直接操作字节数组,按固定位置取数据。

cpp 复制代码
#include <Arduino.h>

// 定义数据包结构(5字节)
struct DataPacket {
  int16_t temp_raw;  // 温度原始值(2字节)
  int16_t hum_raw;   // 湿度原始值(2字节)
  uint8_t status;    // 状态(1字节)
};

void setup() {
  Serial.begin(115200);
}

void loop() {
  if (Serial.available() >= 5) {  // 至少收到5字节
    uint8_t recv_buf[5];
    Serial.readBytes(recv_buf, 5);  // 读取5字节到缓冲区
    
    // 解析数据(大端模式:高位字节在前)
    DataPacket data;
    data.temp_raw = (recv_buf[0] << 8) | recv_buf[1];  // 第0-1字节 → 温度
    data.hum_raw = (recv_buf[2] << 8) | recv_buf[3];   // 第2-3字节 → 湿度
    data.status = recv_buf[4];                         // 第4字节 → 状态
    
    // 转换为实际值
    float temperature = data.temp_raw / 10.0;
    float humidity = data.hum_raw / 10.0;
    
    Serial.printf("温度:%.1f℃,湿度:%.1f%,状态:%s\n",
                 temperature, humidity, 
                 (data.status == 0) ? "正常" : "异常");
  }
}
2. TLV 格式(Type-Length-Value)

特点 :每个字段由 "类型(Type)+ 长度(Length)+ 值(Value)" 组成,字段数量和长度可以不固定,灵活性高。
适用场景 :需要扩展的协议(比如有时传温度,有时传温度 + 湿度 + 电量)。
示例数据包

类型(1 字节) 长度(1 字节) 值(n 字节) 说明
0x01 0x02 0x00 0xFA 温度(25.0℃)
0x02 0x02 0x02 0x58 湿度(60.0%)

对应的二进制数据:0x01 0x02 0x00 0xFA 0x02 0x02 0x02 0x58

Python 解析代码

循环解析每个 TLV 单元(先读类型,再读长度,再读对应的值)。

python 复制代码
def parse_tlv(data):
    parsed = {}
    index = 0
    while index < len(data):
        if index + 2 > len(data):  # 至少需要类型(1)+长度(1)
            break
        type_ = data[index]        # 类型(1字节)
        length = data[index + 1]   # 长度(1字节)
        index += 2
        
        if index + length > len(data):  # 检查值是否完整
            break
        value = data[index:index + length]  # 值(length字节)
        index += length
        
        # 解析具体值(根据类型转换)
        if type_ == 0x01:  # 温度
            temp_raw = (value[0] << 8) | value[1]
            parsed["temperature"] = temp_raw / 10.0
        elif type_ == 0x02:  # 湿度
            hum_raw = (value[0] << 8) | value[1]
            parsed["humidity"] = hum_raw / 10.0
    return parsed

# 假设收到的TLV二进制数据
recv_bytes = bytes([0x01, 0x02, 0x00, 0xFA, 0x02, 0x02, 0x02, 0x58])
result = parse_tlv(recv_bytes)
print(f"解析结果:{result}")  # 输出:{'temperature': 25.0, 'humidity': 60.0}

ESP32 解析代码

同样循环读取每个 TLV 单元,按类型处理值。

cpp 复制代码
#include <Arduino.h>

void parseTLV(uint8_t* data, int len) {
  int index = 0;
  float temp = -1, hum = -1;
  
  while (index < len) {
    if (index + 2 > len) break;  // 不够类型+长度的字节
    
    uint8_t type = data[index];
    uint8_t length = data[index + 1];
    index += 2;
    
    if (index + length > len) break;  // 不够值的字节
    
    // 解析值(根据类型)
    if (type == 0x01 && length == 2) {  // 温度(2字节)
      int16_t raw = (data[index] << 8) | data[index + 1];
      temp = raw / 10.0;
    } else if (type == 0x02 && length == 2) {  // 湿度(2字节)
      int16_t raw = (data[index] << 8) | data[index + 1];
      hum = raw / 10.0;
    }
    
    index += length;
  }
  
  // 打印结果
  if (temp != -1) Serial.printf("温度:%.1f℃ ", temp);
  if (hum != -1) Serial.printf("湿度:%.1f% ", hum);
  Serial.println();
}

void setup() {
  Serial.begin(115200);
}

void loop() {
  // 假设收到8字节TLV数据
  if (Serial.available() >= 8) {
    uint8_t recv_buf[8];
    Serial.readBytes(recv_buf, 8);
    parseTLV(recv_buf, 8);
  }
}

三、自定义格式(最灵活,适合嵌入式)

实际项目中,常自定义格式(结合文本或二进制),并加入帧头、帧尾、校验位(防止数据错乱)。比如:

格式定义帧头(0xAA) + 数据长度(1字节) + 数据内容 + 校验和(1字节) + 帧尾(0x55)

  • 帧头 / 帧尾:标记数据包的开始和结束(比如 0xAA 开始,0x55 结束)。
  • 数据长度:告诉接收方 "数据内容" 有多少字节,方便解析。
  • 校验和:所有数据字节的和(取低 8 位),用于检查数据是否传输错误。

示例数据包 (数据内容是温度 25.0℃,即 0x00FA):
0xAA 0x02 0x00 0xFA 0xFA 0x55

  • 帧头:0xAA
  • 数据长度:0x02(数据内容占 2 字节)
  • 数据内容:0x00 0xFA(温度原始值)
  • 校验和:0x00 + 0xFA = 0xFA
  • 帧尾:0x55

Python 解析代码

先找帧头,再验证帧尾和校验和,最后解析数据。

python 复制代码
def parse_custom(data):
    index = 0
    while index < len(data):
        # 找帧头(0xAA)
        if data[index] != 0xAA:
            index += 1
            continue
        
        # 检查是否有足够的字节(帧头+长度+校验+帧尾至少4字节)
        if index + 4 > len(data):
            break
        
        length = data[index + 1]  # 数据长度
        # 检查总长度是否足够(帧头+长度+数据+校验+帧尾)
        total_len = 2 + length + 1 + 1  # 2=帧头+长度,1=校验,1=帧尾
        if index + total_len > len(data):
            index += 1
            continue
        
        # 提取数据内容
        content = data[index + 2 : index + 2 + length]
        # 提取校验和
        checksum = data[index + 2 + length]
        # 提取帧尾
        tail = data[index + 2 + length + 1]
        
        # 验证帧尾和校验和
        if tail != 0x55:
            index += 1
            continue
        # 计算校验和(数据内容的和)
        calc_checksum = sum(content) & 0xFF
        if calc_checksum != checksum:
            index += 1
            continue
        
        # 解析数据内容(这里是温度)
        if length == 2:
            temp_raw = (content[0] << 8) | content[1]
            temperature = temp_raw / 10.0
            print(f"解析成功:温度={temperature}℃")
        
        # 移动到下一个数据包
        index += total_len
    return

# 假设收到的自定义格式数据
recv_bytes = bytes([0xAA, 0x02, 0x00, 0xFA, 0xFA, 0x55])
parse_custom(recv_bytes)  # 输出:解析成功:温度=25.0℃

ESP32 解析代码

逻辑和 Python 类似,先找帧头,再验证校验和和帧尾。

cpp 复制代码
#include <Arduino.h>

void parseCustom(uint8_t* data, int len) {
  int index = 0;
  while (index < len) {
    // 找帧头0xAA
    if (data[index] != 0xAA) {
      index++;
      continue;
    }
    
    // 检查最小长度(帧头+长度+校验+帧尾=4字节)
    if (index + 4 > len) break;
    
    uint8_t length = data[index + 1];  // 数据长度
    uint16_t total_len = 2 + length + 1 + 1;  // 总长度
    
    // 检查总长度是否足够
    if (index + total_len > len) {
      index++;
      continue;
    }
    
    // 提取各部分
    uint8_t* content = &data[index + 2];  // 数据内容
    uint8_t checksum = data[index + 2 + length];  // 校验和
    uint8_t tail = data[index + 2 + length + 1];  // 帧尾
    
    // 验证帧尾
    if (tail != 0x55) {
      index++;
      continue;
    }
    
    // 计算校验和
    uint8_t calc_checksum = 0;
    for (int i = 0; i < length; i++) {
      calc_checksum += content[i];
    }
    if (calc_checksum != checksum) {
      index++;
      continue;
    }
    
    // 解析数据(温度)
    if (length == 2) {
      int16_t temp_raw = (content[0] << 8) | content[1];
      float temperature = temp_raw / 10.0;
      Serial.printf("解析成功:温度=%.1f℃\n", temperature);
    }
    
    // 移动到下一个数据包
    index += total_len;
  }
}

void setup() {
  Serial.begin(115200);
}

void loop() {
  // 假设收到6字节自定义数据
  if (Serial.available() >= 6) {
    uint8_t recv_buf[6];
    Serial.readBytes(recv_buf, 6);
    parseCustom(recv_buf, 6);
  }
}

总结:如何选择格式?

  • 简单场景(少量数据,需要人看懂):用 CSV 或 JSON。
  • 实时性高、数据量大(如传感器每秒 10 次传输):用固定长度二进制。
  • 需要灵活扩展(字段不固定):用 TLV。
  • 嵌入式设备通信(怕数据错乱):用带帧头、校验的自定义格式。

核心原则:在 "易读性" 和 "效率 / 可靠性" 之间找平衡

相关推荐
LiuYiCheng1234561 小时前
Python游戏开发:Pygame全面指南与实战
python·pygame
魔障阿Q1 小时前
华为310P3模型转换及python推理
人工智能·python·深度学习·yolo·计算机视觉·华为
都叫我大帅哥2 小时前
决策树:从零开始的机器学习“算命大师”修炼手册
python·机器学习
这里有鱼汤2 小时前
首个支持A股的AI多智能体金融系统,来了
前端·python
云霄IT2 小时前
python使用ffmpeg录制rtmp/m3u8推流视频并按ctrl+c实现优雅退出
python·ffmpeg·音视频
都叫我大帅哥2 小时前
我给大模型装上“记忆黄金券”:LangChain的ConversationSummaryBufferMemory全解析
python·langchain·ai编程
桃子叔叔2 小时前
28天0基础前端工程师完成Flask接口编写
前端·python·flask
德育处主任3 小时前
『OpenCV-Python』配合 Matplotlib 显示图像
后端·python·opencv
仰望星空的凡人10 小时前
【JS逆向基础】数据库之MongoDB
javascript·数据库·python·mongodb