feat(protocol): add packet protocol
This commit is contained in:
136
client/src/protocol_parser.py
Normal file
136
client/src/protocol_parser.py
Normal file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
FR-1.4 Data Transmission Protocol Parser (v1)
|
||||
12-byte binary packet: [0xAA][TYPE=0x02][LEN=8][TIMESTAMP(4)][RMS_DB(2)][FREQ_HZ(2)][CRC8(1)]
|
||||
"""
|
||||
|
||||
import struct
|
||||
from typing import Optional, NamedTuple
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class AudioMetrics(NamedTuple):
|
||||
"""Parsed audio metrics packet"""
|
||||
timestamp_ms: int
|
||||
rms_db: float
|
||||
freq_hz: int
|
||||
valid: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProtocolStats:
|
||||
"""Protocol statistics"""
|
||||
packets_received: int = 0
|
||||
crc_errors: int = 0
|
||||
length_errors: int = 0
|
||||
range_errors: int = 0
|
||||
|
||||
|
||||
class ProtocolParser:
|
||||
"""
|
||||
Stream parser for FR-1.4 protocol with automatic resynchronization.
|
||||
"""
|
||||
|
||||
SOF = 0xAA
|
||||
TYPE_AUDIO_V1 = 0x02
|
||||
PAYLOAD_LEN = 0x08
|
||||
PACKET_SIZE = 12
|
||||
|
||||
def __init__(self):
|
||||
self.buffer = bytearray()
|
||||
self.stats = ProtocolStats()
|
||||
|
||||
@staticmethod
|
||||
def _crc8_atm(data: bytes) -> int:
|
||||
"""CRC-8/ATM: poly=0x07, init=0x00, refin=false, refout=false, xorout=0x00"""
|
||||
crc = 0x00
|
||||
for byte in data:
|
||||
crc ^= byte
|
||||
for _ in range(8):
|
||||
if crc & 0x80:
|
||||
crc = ((crc << 1) ^ 0x07) & 0xFF
|
||||
else:
|
||||
crc = (crc << 1) & 0xFF
|
||||
return crc
|
||||
|
||||
def feed(self, data: bytes) -> list[AudioMetrics]:
|
||||
"""
|
||||
Feed incoming bytes, return list of parsed packets.
|
||||
|
||||
Args:
|
||||
data: Raw bytes from serial port
|
||||
|
||||
Returns:
|
||||
List of successfully parsed AudioMetrics
|
||||
"""
|
||||
self.buffer.extend(data)
|
||||
packets = []
|
||||
|
||||
while len(self.buffer) >= self.PACKET_SIZE:
|
||||
# Find SOF
|
||||
sof_idx = self.buffer.find(self.SOF)
|
||||
if sof_idx == -1:
|
||||
# No SOF found, discard all but last byte
|
||||
self.buffer = self.buffer[-1:]
|
||||
break
|
||||
|
||||
# Discard bytes before SOF
|
||||
if sof_idx > 0:
|
||||
self.buffer = self.buffer[sof_idx:]
|
||||
|
||||
# Need at least 3 bytes for SOF + TYPE + LEN
|
||||
if len(self.buffer) < 3:
|
||||
break
|
||||
|
||||
packet_type = self.buffer[1]
|
||||
payload_len = self.buffer[2]
|
||||
|
||||
# Validate TYPE and LEN
|
||||
if packet_type != self.TYPE_AUDIO_V1 or payload_len != self.PAYLOAD_LEN:
|
||||
self.stats.length_errors += 1
|
||||
self.buffer.pop(0) # Remove false SOF, retry
|
||||
continue
|
||||
|
||||
# Full packet size = SOF(1) + TYPE(1) + LEN(1) + PAYLOAD(8) + CRC(1) = 12
|
||||
total_len = 3 + payload_len + 1
|
||||
if len(self.buffer) < total_len:
|
||||
break # Wait for more data
|
||||
|
||||
packet = bytes(self.buffer[:total_len])
|
||||
|
||||
# Verify CRC (over bytes 1..10: TYPE, LEN, payload)
|
||||
crc_data = packet[1:11]
|
||||
expected_crc = packet[11]
|
||||
calculated_crc = self._crc8_atm(crc_data)
|
||||
|
||||
if calculated_crc != expected_crc:
|
||||
self.stats.crc_errors += 1
|
||||
self.buffer.pop(0) # Remove bad packet, retry
|
||||
continue
|
||||
|
||||
# Parse payload (little-endian)
|
||||
timestamp_ms, rms_db_x10, freq_hz = struct.unpack_from('<IhH', packet, 3)
|
||||
|
||||
# Convert and validate ranges
|
||||
rms_db = rms_db_x10 / 10.0
|
||||
valid = True
|
||||
if not (-40.0 <= rms_db <= 80.0) or not (100 <= freq_hz <= 8000):
|
||||
self.stats.range_errors += 1
|
||||
valid = False
|
||||
|
||||
self.stats.packets_received += 1
|
||||
packets.append(AudioMetrics(
|
||||
timestamp_ms=timestamp_ms,
|
||||
rms_db=rms_db,
|
||||
freq_hz=freq_hz,
|
||||
valid=valid
|
||||
))
|
||||
|
||||
# Remove processed packet
|
||||
self.buffer = self.buffer[total_len:]
|
||||
|
||||
return packets
|
||||
|
||||
def get_stats(self) -> ProtocolStats:
|
||||
"""Get current statistics"""
|
||||
return self.stats
|
||||
56
client/src/receiver.py
Normal file
56
client/src/receiver.py
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example client for FR-1.4 protocol
|
||||
"""
|
||||
import serial
|
||||
import time
|
||||
from protocol_parser import ProtocolParser
|
||||
|
||||
|
||||
def main():
|
||||
SERIAL_PORT = "/dev/ttyACM0"
|
||||
BAUDRATE = 115200
|
||||
|
||||
parser = ProtocolParser()
|
||||
|
||||
try:
|
||||
with serial.Serial(SERIAL_PORT, BAUDRATE, timeout=1) as ser:
|
||||
print(f"Connected to {SERIAL_PORT}")
|
||||
|
||||
while True:
|
||||
# Read available data
|
||||
if ser.in_waiting > 0:
|
||||
data = ser.read(ser.in_waiting)
|
||||
|
||||
# Parse packets
|
||||
packets = parser.feed(data)
|
||||
for pkt in packets:
|
||||
if pkt.valid:
|
||||
print(
|
||||
f"[{pkt.timestamp_ms:010d}] "
|
||||
f"RMS: {pkt.rms_db:+6.1f} dB "
|
||||
f"Freq: {pkt.freq_hz:4d} Hz"
|
||||
)
|
||||
else:
|
||||
print(f"[WARN] Invalid packet: {pkt}")
|
||||
|
||||
# Show stats every 100 packets
|
||||
stats = parser.get_stats()
|
||||
if stats.packets_received % 100 == 0 and stats.packets_received > 0:
|
||||
print(
|
||||
f"Stats: RX={stats.packets_received} "
|
||||
f"CRC_err={stats.crc_errors} "
|
||||
f"LEN_err={stats.length_errors} "
|
||||
f"Range_err={stats.range_errors}"
|
||||
)
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nExiting...")
|
||||
except serial.SerialException as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,43 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import time
|
||||
|
||||
import serial
|
||||
|
||||
# Настройки порта
|
||||
SERIAL_PORT = "/dev/ttyACM0"
|
||||
BAUD_RATE = 115200
|
||||
|
||||
|
||||
def read_serial_data():
|
||||
try:
|
||||
# Открытие порта. Timeout позволяет прерывать блокирующее чтение.
|
||||
ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=1)
|
||||
print(f"Connected to {SERIAL_PORT}")
|
||||
|
||||
while True:
|
||||
if ser.in_waiting > 0:
|
||||
# Чтение строки, декодирование и удаление пробелов
|
||||
line = ser.readline().decode("utf-8", errors="replace").strip()
|
||||
if line:
|
||||
print(f"[RX]: {line}")
|
||||
else:
|
||||
# Небольшая пауза, чтобы не грузить CPU ПК
|
||||
time.sleep(0.01)
|
||||
|
||||
except serial.SerialException as e:
|
||||
print(f"Error opening serial port: {e}")
|
||||
print(
|
||||
"Hint: Check if /dev/ttyACM0 exists and you have permissions (group uucp)."
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("\nExiting...")
|
||||
except:
|
||||
print("\nAn error occure!")
|
||||
finally:
|
||||
if "ser" in locals() and ser.is_open:
|
||||
ser.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
read_serial_data()
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import serial
|
||||
import time
|
||||
|
||||
port = serial.Serial("/dev/ttyACM0", 115200, timeout=1)
|
||||
print(f"Connected to {port.name}")
|
||||
|
||||
port.write(b"test\n")
|
||||
time.sleep(0.1)
|
||||
|
||||
# Читаем 5 секунд
|
||||
start = time.time()
|
||||
while time.time() - start < 5:
|
||||
if port.in_waiting:
|
||||
data = port.read(port.in_waiting)
|
||||
print(f"RX: {data.decode('utf-8', errors='replace')}", end="")
|
||||
time.sleep(0.1)
|
||||
|
||||
port.close()
|
||||
40
firmware/App/Inc/protocol.h
Normal file
40
firmware/App/Inc/protocol.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#ifndef PROTOCOL_H
|
||||
#define PROTOCOL_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// Protocol Constants
|
||||
#define PROTOCOL_SOF 0xAA
|
||||
#define PACKET_TYPE_AUDIO 0x02
|
||||
#define PACKET_LEN_V1 0x08 // Payload length (excluding SOF, TYPE, LEN, CRC)
|
||||
#define PACKET_TOTAL_SIZE 12
|
||||
|
||||
// CRC8-ATM Constants
|
||||
#define CRC8_POLY 0x07
|
||||
#define CRC8_INIT 0x00
|
||||
|
||||
/**
|
||||
* @brief Calculates CRC-8/ATM over the data buffer.
|
||||
* Polynomial: x^8 + x^2 + x + 1 (0x07)
|
||||
* Init: 0x00, RefIn: false, RefOut: false, XorOut: 0x00
|
||||
* @param data Pointer to data buffer
|
||||
* @param len Length of data
|
||||
* @return Calculated CRC8
|
||||
*/
|
||||
uint8_t crc8_atm(const uint8_t *data, size_t len);
|
||||
|
||||
/**
|
||||
* @brief Encodes the audio metric packet into the wire format.
|
||||
* @param buf Output buffer (must be at least 12 bytes)
|
||||
* @param timestamp_ms Timestamp in milliseconds
|
||||
* @param rms_dbfs RMS value in dBFS (float)
|
||||
* @param freq_hz Peak frequency in Hz (float)
|
||||
*/
|
||||
void protocol_pack_v1(
|
||||
uint8_t *buf,
|
||||
uint32_t timestamp_ms,
|
||||
float rms_dbfs,
|
||||
float freq_hz);
|
||||
|
||||
#endif // PROTOCOL_H
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "FreeRTOS.h"
|
||||
#include "audio_adc.h"
|
||||
#include "audio_processor.h" // НОВОЕ
|
||||
#include "protocol.h"
|
||||
#include "queue.h"
|
||||
#include "stm32f1xx.h"
|
||||
#include "task.h"
|
||||
@@ -19,13 +20,12 @@ void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
|
||||
|
||||
// === Структуры данных ===
|
||||
|
||||
// НОВОЕ: пакет с результатами FFT
|
||||
typedef struct {
|
||||
float rms_dbfs;
|
||||
float peak_hz;
|
||||
float peak_mag;
|
||||
uint8_t clipped;
|
||||
uint32_t buffer_num;
|
||||
uint32_t timestamp_ms;
|
||||
} audio_metrics_packet_t;
|
||||
|
||||
static QueueHandle_t audio_metrics_queue = NULL;
|
||||
@@ -119,7 +119,8 @@ void audio_process_task(void *param) {
|
||||
.peak_hz = metrics.peak_hz,
|
||||
.peak_mag = metrics.peak_mag,
|
||||
.clipped = metrics.clipped,
|
||||
.buffer_num = buffer_counter};
|
||||
.timestamp_ms = xTaskGetTickCount(),
|
||||
};
|
||||
xQueueSend(audio_metrics_queue, &packet, 0);
|
||||
}
|
||||
}
|
||||
@@ -128,83 +129,37 @@ void audio_process_task(void *param) {
|
||||
|
||||
void cdc_task(void *param) {
|
||||
(void)param;
|
||||
|
||||
char tx_buffer[256];
|
||||
uint32_t heartbeat_counter = 0;
|
||||
// Buffer for the FR-1.4 packet (12 bytes)
|
||||
uint8_t tx_buffer[PACKET_TOTAL_SIZE];
|
||||
|
||||
while (1) {
|
||||
heartbeat_counter++;
|
||||
|
||||
// Heartbeat каждые 100 циклов
|
||||
if (heartbeat_counter % 100 == 0 && tud_cdc_connected()) {
|
||||
uint32_t current_buffer_count = audio_adc_get_buffer_count();
|
||||
|
||||
int len = snprintf(
|
||||
tx_buffer,
|
||||
sizeof(tx_buffer),
|
||||
"HB:%lu Q:%u BC:%lu\r\n",
|
||||
heartbeat_counter,
|
||||
(unsigned)uxQueueMessagesWaiting(audio_metrics_queue),
|
||||
current_buffer_count);
|
||||
|
||||
if (len > 0 && tud_cdc_write_available() >= (uint32_t)len) {
|
||||
tud_cdc_write(tx_buffer, (uint32_t)len);
|
||||
tud_cdc_write_flush();
|
||||
}
|
||||
}
|
||||
|
||||
// Метрики FFT
|
||||
// Check if USB is connected
|
||||
if (tud_cdc_connected()) {
|
||||
audio_metrics_packet_t packet;
|
||||
if (xQueueReceive(audio_metrics_queue, &packet, pdMS_TO_TICKS(10)) ==
|
||||
pdPASS) {
|
||||
// fixed-point:
|
||||
// RMS: dBFS * 10 (один знак после запятой)
|
||||
int32_t rms_x10 = (int32_t)(packet.rms_dbfs * 10.0f);
|
||||
int32_t rms_int = rms_x10 / 10;
|
||||
int32_t rms_frac = rms_x10 % 10;
|
||||
if (rms_frac < 0) rms_frac = -rms_frac;
|
||||
|
||||
// Freq: Hz * 10 (один знак после запятой)
|
||||
int32_t freq_x10 = (int32_t)(packet.peak_hz * 10.0f);
|
||||
int32_t freq_int = freq_x10 / 10;
|
||||
int32_t freq_frac = freq_x10 % 10;
|
||||
if (freq_frac < 0) freq_frac = -freq_frac;
|
||||
|
||||
// Mag: *1000 (три знака после запятой)
|
||||
int32_t mag_x1000 = (int32_t)(packet.peak_mag * 1000.0f);
|
||||
int32_t mag_int = mag_x1000 / 1000;
|
||||
int32_t mag_frac = mag_x1000 % 1000;
|
||||
if (mag_frac < 0) mag_frac = -mag_frac;
|
||||
|
||||
int len = snprintf(
|
||||
// Wait for data from DSP task
|
||||
if (xQueueReceive(
|
||||
audio_metrics_queue,
|
||||
&packet,
|
||||
pdMS_TO_TICKS(10)) == pdPASS) {
|
||||
// Pack data according to FR-1.4 spec
|
||||
protocol_pack_v1(
|
||||
tx_buffer,
|
||||
sizeof(tx_buffer),
|
||||
"Buf:%lu RMS:%ld.%01ld dBFS Freq:%ld.%01ld Hz Mag:%ld.%03ld "
|
||||
"Clip:%u\r\n",
|
||||
packet.buffer_num,
|
||||
(long)rms_int,
|
||||
(long)rms_frac,
|
||||
(long)freq_int,
|
||||
(long)freq_frac,
|
||||
(long)mag_int,
|
||||
(long)mag_frac,
|
||||
(unsigned)packet.clipped);
|
||||
packet.timestamp_ms,
|
||||
packet.rms_dbfs,
|
||||
packet.peak_hz);
|
||||
|
||||
if (len > 0 && tud_cdc_connected() &&
|
||||
tud_cdc_write_available() >= (uint32_t)len) {
|
||||
tud_cdc_write(tx_buffer, (uint32_t)len);
|
||||
// Write to USB CDC
|
||||
// Check available space just in case
|
||||
if (tud_cdc_write_available() >= sizeof(tx_buffer)) {
|
||||
tud_cdc_write(tx_buffer, sizeof(tx_buffer));
|
||||
tud_cdc_write_flush();
|
||||
}
|
||||
}
|
||||
|
||||
// Echo
|
||||
if (tud_cdc_available()) {
|
||||
uint8_t buf[64];
|
||||
uint32_t count = tud_cdc_read(buf, sizeof(buf));
|
||||
if (tud_cdc_connected() && count > 0) {
|
||||
tud_cdc_write(buf, count);
|
||||
tud_cdc_write_flush();
|
||||
}
|
||||
} else {
|
||||
// Flush queue if USB not connected to prevent stalling DSP task
|
||||
// or just sleep longer.
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
51
firmware/App/Src/protocol.c
Normal file
51
firmware/App/Src/protocol.c
Normal file
@@ -0,0 +1,51 @@
|
||||
#include "protocol.h"
|
||||
#include <math.h>
|
||||
|
||||
uint8_t crc8_atm(const uint8_t *data, size_t len) {
|
||||
uint8_t crc = CRC8_INIT;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
crc ^= data[i];
|
||||
for (uint8_t j = 0; j < 8; j++) {
|
||||
if (crc & 0x80) {
|
||||
crc = (crc << 1) ^ CRC8_POLY;
|
||||
} else {
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
void protocol_pack_v1(
|
||||
uint8_t *buf,
|
||||
uint32_t timestamp_ms,
|
||||
float rms_dbfs,
|
||||
float freq_hz) {
|
||||
// Header
|
||||
buf[0] = PROTOCOL_SOF;
|
||||
buf[1] = PACKET_TYPE_AUDIO;
|
||||
buf[2] = PACKET_LEN_V1;
|
||||
|
||||
// Payload: Timestamp (4 bytes, Little Endian)
|
||||
buf[3] = (uint8_t)(timestamp_ms & 0xFF);
|
||||
buf[4] = (uint8_t)((timestamp_ms >> 8) & 0xFF);
|
||||
buf[5] = (uint8_t)((timestamp_ms >> 16) & 0xFF);
|
||||
buf[6] = (uint8_t)((timestamp_ms >> 24) & 0xFF);
|
||||
|
||||
// Payload: RMS_DB (2 bytes, Little Endian, x10, int16)
|
||||
// Range check implicit by int16 cast, but clamping is safer
|
||||
// Spec: -40..80 dB -> -400..800
|
||||
// Note: Since DSP returns dBFS (negative), we just send it as is.
|
||||
// E.g. -60.5 dB -> -605.
|
||||
int16_t rms_fixed = (int16_t)(rms_dbfs * 10.0f);
|
||||
buf[7] = (uint8_t)(rms_fixed & 0xFF);
|
||||
buf[8] = (uint8_t)((rms_fixed >> 8) & 0xFF);
|
||||
|
||||
// Payload: FREQ_HZ (2 bytes, Little Endian, uint16)
|
||||
uint16_t freq_fixed = (uint16_t)freq_hz;
|
||||
buf[9] = (uint8_t)(freq_fixed & 0xFF);
|
||||
buf[10] = (uint8_t)((freq_fixed >> 8) & 0xFF);
|
||||
|
||||
// CRC8 (Calculated over bytes 1..10: TYPE, LEN, Payload)
|
||||
buf[11] = crc8_atm(&buf[1], 10);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ C_SOURCES = \
|
||||
App/Src/main.c \
|
||||
App/Src/audio_adc.c \
|
||||
App/Src/audio_processor.c \
|
||||
App/Src/protocol.c \
|
||||
App/Src/usb_descriptors.c \
|
||||
App/Src/system_stm32f1xx.c \
|
||||
|
||||
|
||||
Reference in New Issue
Block a user