數據包解析是不同設備(如電腦、ESP32 等嵌入式設備)之間通信的核心環節。簡單說,就是把收到的 "一串數據" 翻譯成雙方都能理解的 "具體信息"(比如溫度、濕度、命令等)。下面介紹幾種常見的數據包格式,以及 Python 和 ESP32(基于 Arduino 框架)的解析代碼,盡量用通俗的語言解釋。
一、文本格式(最容易理解)
文本格式的數據包由字符串組成,人類可以直接看懂,適合簡單場景。常見的有CSV 格式和JSON 格式。
1. CSV 格式(逗號分隔值)
特點:用逗號(或其他符號)分隔不同字段,結構簡單,類似表格。
適用場景:傳感器批量數據(如溫度、濕度、時間)、簡單配置信息。
示例數據包:"25.6,60.2,202310011200"
(溫度 = 25.6℃,濕度 = 60.2%,時間 = 2023-10-01 12:00)
Python 解析代碼:
用內置的split
函數分割字符串即可。
# 假設收到的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)。
#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
模塊直接轉成字典,方便取值。
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 結構。
#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;// 解析JSONDeserializationError 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
模塊解析二進制數據(需要知道每個字段的類型和順序)。
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.0humidity = hum_raw / 10.0print(f"溫度:{temperature}℃,濕度:{humidity}%,狀態:{'正常' if status == 0 else '異常'}")
else:print("二進制數據長度錯誤")
(>hhu
表示:>
大端模式,h
int16,h
int16,u
uint8)
ESP32 解析代碼:
直接操作字節數組,按固定位置取數據。
#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 單元(先讀類型,再讀長度,再讀對應的值)。
def parse_tlv(data):parsed = {}index = 0while index < len(data):if index + 2 > len(data): # 至少需要類型(1)+長度(1)breaktype_ = data[index] # 類型(1字節)length = data[index + 1] # 長度(1字節)index += 2if index + length > len(data): # 檢查值是否完整breakvalue = data[index:index + length] # 值(length字節)index += length# 解析具體值(根據類型轉換)if type_ == 0x01: # 溫度temp_raw = (value[0] << 8) | value[1]parsed["temperature"] = temp_raw / 10.0elif type_ == 0x02: # 濕度hum_raw = (value[0] << 8) | value[1]parsed["humidity"] = hum_raw / 10.0return 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 單元,按類型處理值。
#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 解析代碼:
先找幀頭,再驗證幀尾和校驗和,最后解析數據。
def parse_custom(data):index = 0while index < len(data):# 找幀頭(0xAA)if data[index] != 0xAA:index += 1continue# 檢查是否有足夠的字節(幀頭+長度+校驗+幀尾至少4字節)if index + 4 > len(data):breaklength = data[index + 1] # 數據長度# 檢查總長度是否足夠(幀頭+長度+數據+校驗+幀尾)total_len = 2 + length + 1 + 1 # 2=幀頭+長度,1=校驗,1=幀尾if index + total_len > len(data):index += 1continue# 提取數據內容content = data[index + 2 : index + 2 + length]# 提取校驗和checksum = data[index + 2 + length]# 提取幀尾tail = data[index + 2 + length + 1]# 驗證幀尾和校驗和if tail != 0x55:index += 1continue# 計算校驗和(數據內容的和)calc_checksum = sum(content) & 0xFFif calc_checksum != checksum:index += 1continue# 解析數據內容(這里是溫度)if length == 2:temp_raw = (content[0] << 8) | content[1]temperature = temp_raw / 10.0print(f"解析成功:溫度={temperature}℃")# 移動到下一個數據包index += total_lenreturn# 假設收到的自定義格式數據
recv_bytes = bytes([0xAA, 0x02, 0x00, 0xFA, 0xFA, 0x55])
parse_custom(recv_bytes) # 輸出:解析成功:溫度=25.0℃
ESP32 解析代碼:
邏輯和 Python 類似,先找幀頭,再驗證校驗和和幀尾。
#include <Arduino.h>void parseCustom(uint8_t* data, int len) {int index = 0;while (index < len) {// 找幀頭0xAAif (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。
- 嵌入式設備通信(怕數據錯亂):用帶幀頭、校驗的自定義格式。
核心原則:在 "易讀性" 和 "效率 / 可靠性" 之間找平衡。