diff --git a/client/src/protocol_parser.py b/client/src/protocol_parser.py new file mode 100644 index 0000000..689a49b --- /dev/null +++ b/client/src/protocol_parser.py @@ -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(' ProtocolStats: + """Get current statistics""" + return self.stats diff --git a/client/src/receiver.py b/client/src/receiver.py new file mode 100644 index 0000000..14b3381 --- /dev/null +++ b/client/src/receiver.py @@ -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() diff --git a/client/src/reveier.py b/client/src/reveier.py deleted file mode 100644 index d2c5cbf..0000000 --- a/client/src/reveier.py +++ /dev/null @@ -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() diff --git a/client/src/test_usb.py b/client/src/test_usb.py deleted file mode 100644 index 2d95c35..0000000 --- a/client/src/test_usb.py +++ /dev/null @@ -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() diff --git a/firmware/App/Inc/protocol.h b/firmware/App/Inc/protocol.h new file mode 100644 index 0000000..825bcbf --- /dev/null +++ b/firmware/App/Inc/protocol.h @@ -0,0 +1,40 @@ +#ifndef PROTOCOL_H +#define PROTOCOL_H + +#include +#include + +// 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 diff --git a/firmware/App/Src/main.c b/firmware/App/Src/main.c index 7574b21..b91c013 100644 --- a/firmware/App/Src/main.c +++ b/firmware/App/Src/main.c @@ -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++; + // Check if USB is connected + if (tud_cdc_connected()) { + audio_metrics_packet_t packet; - // Heartbeat каждые 100 циклов - if (heartbeat_counter % 100 == 0 && tud_cdc_connected()) { - uint32_t current_buffer_count = audio_adc_get_buffer_count(); + // 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, + packet.timestamp_ms, + packet.rms_dbfs, + packet.peak_hz); - 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; - 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( - 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); - - if (len > 0 && tud_cdc_connected() && - tud_cdc_write_available() >= (uint32_t)len) { - tud_cdc_write(tx_buffer, (uint32_t)len); - 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(); + // 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(); + } } + } else { + // Flush queue if USB not connected to prevent stalling DSP task + // or just sleep longer. + vTaskDelay(pdMS_TO_TICKS(100)); } } } diff --git a/firmware/App/Src/protocol.c b/firmware/App/Src/protocol.c new file mode 100644 index 0000000..60c44b2 --- /dev/null +++ b/firmware/App/Src/protocol.c @@ -0,0 +1,51 @@ +#include "protocol.h" +#include + +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); +} diff --git a/firmware/Makefile b/firmware/Makefile index 7e9d7cd..cb5b94a 100644 --- a/firmware/Makefile +++ b/firmware/Makefile @@ -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 \