feat(protocol): add packet protocol

This commit is contained in:
2025-12-26 01:11:12 +03:00
parent 97c59cdda2
commit 2a14a36797
8 changed files with 314 additions and 137 deletions

View 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
View 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()

View File

@@ -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()

View File

@@ -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()

View 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

View File

@@ -3,6 +3,7 @@
#include "FreeRTOS.h" #include "FreeRTOS.h"
#include "audio_adc.h" #include "audio_adc.h"
#include "audio_processor.h" // НОВОЕ #include "audio_processor.h" // НОВОЕ
#include "protocol.h"
#include "queue.h" #include "queue.h"
#include "stm32f1xx.h" #include "stm32f1xx.h"
#include "task.h" #include "task.h"
@@ -19,13 +20,12 @@ void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
// === Структуры данных === // === Структуры данных ===
// НОВОЕ: пакет с результатами FFT
typedef struct { typedef struct {
float rms_dbfs; float rms_dbfs;
float peak_hz; float peak_hz;
float peak_mag; float peak_mag;
uint8_t clipped; uint8_t clipped;
uint32_t buffer_num; uint32_t timestamp_ms;
} audio_metrics_packet_t; } audio_metrics_packet_t;
static QueueHandle_t audio_metrics_queue = NULL; static QueueHandle_t audio_metrics_queue = NULL;
@@ -119,7 +119,8 @@ void audio_process_task(void *param) {
.peak_hz = metrics.peak_hz, .peak_hz = metrics.peak_hz,
.peak_mag = metrics.peak_mag, .peak_mag = metrics.peak_mag,
.clipped = metrics.clipped, .clipped = metrics.clipped,
.buffer_num = buffer_counter}; .timestamp_ms = xTaskGetTickCount(),
};
xQueueSend(audio_metrics_queue, &packet, 0); xQueueSend(audio_metrics_queue, &packet, 0);
} }
} }
@@ -128,83 +129,37 @@ void audio_process_task(void *param) {
void cdc_task(void *param) { void cdc_task(void *param) {
(void)param; (void)param;
// Buffer for the FR-1.4 packet (12 bytes)
char tx_buffer[256]; uint8_t tx_buffer[PACKET_TOTAL_SIZE];
uint32_t heartbeat_counter = 0;
while (1) { while (1) {
heartbeat_counter++; // Check if USB is connected
if (tud_cdc_connected()) {
// 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
audio_metrics_packet_t packet; 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 (один знак после запятой) // Wait for data from DSP task
int32_t freq_x10 = (int32_t)(packet.peak_hz * 10.0f); if (xQueueReceive(
int32_t freq_int = freq_x10 / 10; audio_metrics_queue,
int32_t freq_frac = freq_x10 % 10; &packet,
if (freq_frac < 0) freq_frac = -freq_frac; pdMS_TO_TICKS(10)) == pdPASS) {
// Pack data according to FR-1.4 spec
// Mag: *1000 (три знака после запятой) protocol_pack_v1(
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(
tx_buffer, tx_buffer,
sizeof(tx_buffer), packet.timestamp_ms,
"Buf:%lu RMS:%ld.%01ld dBFS Freq:%ld.%01ld Hz Mag:%ld.%03ld " packet.rms_dbfs,
"Clip:%u\r\n", packet.peak_hz);
packet.buffer_num,
(long)rms_int,
(long)rms_frac,
(long)freq_int,
(long)freq_frac,
(long)mag_int,
(long)mag_frac,
(unsigned)packet.clipped);
if (len > 0 && tud_cdc_connected() && // Write to USB CDC
tud_cdc_write_available() >= (uint32_t)len) { // Check available space just in case
tud_cdc_write(tx_buffer, (uint32_t)len); if (tud_cdc_write_available() >= sizeof(tx_buffer)) {
tud_cdc_write(tx_buffer, sizeof(tx_buffer));
tud_cdc_write_flush(); tud_cdc_write_flush();
} }
} }
} else {
// Echo // Flush queue if USB not connected to prevent stalling DSP task
if (tud_cdc_available()) { // or just sleep longer.
uint8_t buf[64]; vTaskDelay(pdMS_TO_TICKS(100));
uint32_t count = tud_cdc_read(buf, sizeof(buf));
if (tud_cdc_connected() && count > 0) {
tud_cdc_write(buf, count);
tud_cdc_write_flush();
}
} }
} }
} }

View 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);
}

View File

@@ -6,6 +6,7 @@ C_SOURCES = \
App/Src/main.c \ App/Src/main.c \
App/Src/audio_adc.c \ App/Src/audio_adc.c \
App/Src/audio_processor.c \ App/Src/audio_processor.c \
App/Src/protocol.c \
App/Src/usb_descriptors.c \ App/Src/usb_descriptors.c \
App/Src/system_stm32f1xx.c \ App/Src/system_stm32f1xx.c \