Radio subida

This commit is contained in:
Álvaro Alcántara Ramírez 2026-08-03 21:38:32 +02:00
parent 5f81e54793
commit f0db3a8aeb
147 changed files with 11343 additions and 0 deletions

View file

@ -0,0 +1,109 @@
#include <Arduino.h>
// Evita que Arduino-ESP32 libere la memoria de Bluetooth Classic antes de setup().
#if __has_include("esp32-hal-alloc-bt-classic-mem.h")
#include "esp32-hal-alloc-bt-classic-mem.h"
#elif __has_include("esp32-hal-bt-mem.h")
#include "esp32-hal-bt-mem.h"
#else
#error "Actualiza esp32 by Espressif Systems a la version 3.3.8 o posterior."
#endif
#include "audio.hpp"
#include "config.hpp"
#include "ptt.hpp"
#include "uart_audio.hpp"
static uint8_t audio_frame_sequence = 0;
static TaskHandle_t pilot_tx_task_handle = nullptr;
static void pilot_tx_task(void *parameter)
{
(void)parameter;
bool microphone_enabled = false;
for (;;) {
const bool ptt_pressed = ptt_is_pressed();
if (ptt_pressed != microphone_enabled) {
microphone_enabled = ptt_pressed;
audio_set_microphone_enabled(microphone_enabled);
}
if (!ptt_pressed) {
vTaskDelay(pdMS_TO_TICKS(1));
continue;
}
const int8_t *audio = nullptr;
uint16_t audio_len = 0;
if (audio_capture_frame(&audio, &audio_len) && ptt_is_pressed()) {
uart_send_audio(audio_frame_sequence++, audio, audio_len);
}
vTaskDelay(pdMS_TO_TICKS(1));
}
}
static void stop_forever(const char *reason)
{
#if PILOT_DEBUG_SERIAL
Serial.println(reason);
#endif
while (true) {
delay(1000);
}
}
void setup()
{
Serial.begin(115200);
delay(500);
#if PILOT_DEBUG_SERIAL
Serial.println();
Serial.println("================================================");
Serial.println(" Radio piloto: INTERCOM BLUETOOTH <-> UART");
Serial.println("================================================");
#endif
if (!ptt_init()) {
stop_forever("FALLO: no se pudo iniciar el PTT KEY2");
}
if (!audio_init()) {
stop_forever("FALLO: no se pudo iniciar Bluetooth HFP");
}
if (!uart_audio_init()) {
stop_forever("FALLO: no se pudo iniciar el enlace UART");
}
const BaseType_t tx_task_result = xTaskCreatePinnedToCore(
pilot_tx_task,
"pilot_audio_tx",
PILOT_TX_TASK_STACK,
nullptr,
PILOT_TX_TASK_PRIORITY,
&pilot_tx_task_handle,
PILOT_TX_TASK_CORE
);
if (tx_task_result != pdPASS) {
stop_forever("FALLO: no se pudo iniciar la tarea de transmision del piloto");
}
#if PILOT_DEBUG_SERIAL
Serial.println("Radio lista. Enciende el V6 Pro+; se conectara automaticamente.");
Serial.println("KEY2 pulsado = enviar micro; la escucha del box sigue activa.");
#endif
}
void loop()
{
audio_process();
uart_process_received_audio(true);
uart_audio_debug_report();
delay(1);
}

View file

@ -0,0 +1,767 @@
#include "audio.hpp"
#include "config.hpp"
#include <Arduino.h>
#include <string.h>
#include "esp32-hal-bt.h"
#include "esp_bt.h"
#include "esp_bt_device.h"
#include "esp_bt_main.h"
#include "esp_gap_bt_api.h"
#include "esp_hf_ag_api.h"
#include "esp_timer.h"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/stream_buffer.h"
#if !defined(CONFIG_IDF_TARGET_ESP32)
#error "Bluetooth HFP necesita un ESP32 clasico, como el ESP32-A1S."
#endif
#if !defined(CONFIG_BT_HFP_AUDIO_DATA_PATH_HCI) || !(CONFIG_BT_HFP_AUDIO_DATA_PATH_HCI)
#error "Instala esp32 by Espressif Systems 3.3.8 o posterior para usar HFP por HCI."
#endif
/* ============================================================
ESTADO BLUETOOTH
============================================================ */
static esp_bd_addr_t intercom_addr = INTERCOM_BT_MAC;
static char test_number[] = "100";
static volatile bool hfp_ready = false;
static volatile bool slc_connected = false;
static volatile bool slc_connecting = false;
static volatile bool audio_connected = false;
static volatile bool audio_connecting = false;
static volatile bool call_active = false;
static volatile bool codec_msbc = false;
static uint32_t last_slc_attempt_ms = 0;
static uint32_t last_audio_attempt_ms = 0;
static uint32_t call_start_at_ms = 0;
static uint32_t audio_open_at_ms = 0;
/* ============================================================
BUFFERS DE AUDIO
mic_buffer:
PCM del micro ya convertido a 8 bits / 8 kHz para la radio.
speaker_buffer:
PCM de 16 bits a la frecuencia negociada por HFP para el intercom.
============================================================ */
static StreamBufferHandle_t mic_buffer = nullptr;
static StreamBufferHandle_t speaker_buffer = nullptr;
static esp_timer_handle_t hfp_audio_timer = nullptr;
static volatile bool microphone_enabled = false;
static volatile bool playback_enabled = false;
static volatile bool speaker_clear_requested = true;
static volatile bool downsample_phase = false;
static int8_t mic_frame[AUDIO_SAMPLES_PER_BLOCK];
/* ============================================================
UTILIDADES
============================================================ */
static void print_esp_result(const char *name, esp_err_t err)
{
#if PILOT_DEBUG_SERIAL
if (err == ESP_OK) {
Serial.printf("%s: OK\n", name);
} else {
Serial.printf("%s: %s (0x%04X)\n", name, esp_err_to_name(err), (unsigned)err);
}
#else
(void)name;
(void)err;
#endif
}
static void print_intercom_mac()
{
#if PILOT_DEBUG_SERIAL
Serial.printf(
"%02X:%02X:%02X:%02X:%02X:%02X",
intercom_addr[0], intercom_addr[1], intercom_addr[2],
intercom_addr[3], intercom_addr[4], intercom_addr[5]
);
#endif
}
static void drain_microphone_buffer()
{
if (mic_buffer == nullptr) {
return;
}
uint8_t discarded[128];
while (xStreamBufferReceive(mic_buffer, discarded, sizeof(discarded), 0) > 0) {
}
}
static void drain_speaker_buffer_from_reader(uint8_t *temporary, size_t temporary_len)
{
if (speaker_buffer == nullptr || temporary == nullptr || temporary_len == 0) {
return;
}
while (xStreamBufferReceive(speaker_buffer, temporary, temporary_len, 0) > 0) {
}
}
/* ============================================================
PIPELINE HFP
============================================================ */
static void audio_timer_callback(void *arg)
{
(void)arg;
if (audio_connected) {
esp_hf_ag_outgoing_data_ready();
}
}
static void stop_hfp_audio_timer()
{
if (hfp_audio_timer != nullptr) {
esp_timer_stop(hfp_audio_timer);
esp_timer_delete(hfp_audio_timer);
hfp_audio_timer = nullptr;
}
}
static bool start_hfp_audio_timer()
{
stop_hfp_audio_timer();
esp_timer_create_args_t timer_args = {};
timer_args.callback = audio_timer_callback;
timer_args.arg = nullptr;
timer_args.dispatch_method = ESP_TIMER_TASK;
timer_args.name = "hfp_audio";
timer_args.skip_unhandled_events = true;
esp_err_t err = esp_timer_create(&timer_args, &hfp_audio_timer);
if (err != ESP_OK) {
print_esp_result("esp_timer_create", err);
return false;
}
err = esp_timer_start_periodic(
hfp_audio_timer,
codec_msbc ? INTERCOM_MSBC_TRIGGER_US : INTERCOM_CVSD_TRIGGER_US
);
if (err != ESP_OK) {
print_esp_result("esp_timer_start_periodic", err);
stop_hfp_audio_timer();
return false;
}
return true;
}
/*
HFP entrega PCM mono de 16 bits.
- CVSD: 8 kHz -> se usa cada muestra.
- mSBC: 16 kHz -> se toma una de cada dos muestras.
No se aplica paso alto, paso bajo, puerta de ruido ni ganancia digital.
*/
static void incoming_audio_callback(const uint8_t *buf, uint32_t len)
{
if (buf == nullptr || len < sizeof(int16_t) || !microphone_enabled || mic_buffer == nullptr) {
return;
}
const int16_t *samples = reinterpret_cast<const int16_t *>(buf);
const size_t sample_count = len / sizeof(int16_t);
int8_t converted[128];
size_t converted_count = 0;
for (size_t i = 0; i < sample_count; ++i) {
bool keep = true;
if (codec_msbc) {
keep = !downsample_phase;
downsample_phase = !downsample_phase;
}
if (!keep) {
continue;
}
converted[converted_count++] = (int8_t)(samples[i] / 256);
if (converted_count == sizeof(converted)) {
xStreamBufferSend(mic_buffer, converted, converted_count, 0);
converted_count = 0;
}
}
if (converted_count > 0) {
xStreamBufferSend(mic_buffer, converted, converted_count, 0);
}
}
/*
El intercom solicita PCM mono de 16 bits.
Si no hay audio del box disponible, se devuelve silencio manteniendo
abierto el enlace HFP.
*/
static uint32_t outgoing_audio_callback(uint8_t *buf, uint32_t len)
{
if (buf == nullptr || len == 0 || !audio_connected) {
return 0;
}
if (speaker_clear_requested) {
drain_speaker_buffer_from_reader(buf, len);
speaker_clear_requested = false;
}
if (!playback_enabled || speaker_buffer == nullptr) {
memset(buf, 0, len);
return len;
}
const size_t received = xStreamBufferReceive(speaker_buffer, buf, len, 0);
if (received < len) {
memset(buf + received, 0, len - received);
}
return len;
}
/* ============================================================
CONEXION HFP
============================================================ */
static void request_slc_connect(bool force)
{
if (!hfp_ready || slc_connected || slc_connecting) {
return;
}
const uint32_t now = millis();
if (!force && (uint32_t)(now - last_slc_attempt_ms) < INTERCOM_SLC_RETRY_MS) {
return;
}
last_slc_attempt_ms = now;
slc_connecting = true;
#if PILOT_DEBUG_SERIAL
Serial.print("Conectando intercom HFP: ");
print_intercom_mac();
Serial.println();
#endif
const esp_err_t err = esp_hf_ag_slc_connect(intercom_addr);
if (err != ESP_OK) {
slc_connecting = false;
}
print_esp_result("esp_hf_ag_slc_connect", err);
}
static void start_fake_call()
{
if (!slc_connected || call_active) {
return;
}
call_active = true;
const esp_err_t err = esp_hf_ag_out_call(
intercom_addr,
1,
0,
ESP_HF_CALL_STATUS_CALL_IN_PROGRESS,
ESP_HF_CALL_SETUP_STATUS_IDLE,
test_number,
ESP_HF_CALL_ADDR_TYPE_UNKNOWN
);
print_esp_result("esp_hf_ag_out_call", err);
audio_open_at_ms = millis() + INTERCOM_AUDIO_OPEN_DELAY_MS;
}
static void request_audio_connect(bool force)
{
if (!slc_connected || audio_connected) {
return;
}
const uint32_t now = millis();
if (!force && audio_connecting &&
(uint32_t)(now - last_audio_attempt_ms) < INTERCOM_AUDIO_RETRY_MS) {
return;
}
last_audio_attempt_ms = now;
audio_connecting = true;
const esp_err_t err = esp_hf_ag_audio_connect(intercom_addr);
if (err != ESP_OK) {
audio_connecting = false;
}
print_esp_result("esp_hf_ag_audio_connect", err);
}
/* ============================================================
RESPUESTAS HFP BASICAS
============================================================ */
static void send_indicator_state(esp_bd_addr_t addr)
{
esp_hf_ag_ciev_report(addr, ESP_HF_IND_TYPE_CALL,
call_active ? ESP_HF_CALL_STATUS_CALL_IN_PROGRESS : ESP_HF_CALL_STATUS_NO_CALLS);
esp_hf_ag_ciev_report(addr, ESP_HF_IND_TYPE_CALLSETUP, ESP_HF_CALL_SETUP_STATUS_IDLE);
esp_hf_ag_ciev_report(addr, ESP_HF_IND_TYPE_SERVICE, ESP_HF_NETWORK_STATE_AVAILABLE);
esp_hf_ag_ciev_report(addr, ESP_HF_IND_TYPE_SIGNAL, 5);
esp_hf_ag_ciev_report(addr, ESP_HF_IND_TYPE_ROAM, ESP_HF_ROAMING_STATUS_INACTIVE);
esp_hf_ag_ciev_report(addr, ESP_HF_IND_TYPE_BATTCHG, 5);
esp_hf_ag_ciev_report(addr, ESP_HF_IND_TYPE_CALLHELD, ESP_HF_CALL_HELD_STATUS_NONE);
}
static void answer_current_call(esp_bd_addr_t addr)
{
call_active = true;
print_esp_result(
"esp_hf_ag_answer_call",
esp_hf_ag_answer_call(
addr,
1,
0,
ESP_HF_CALL_STATUS_CALL_IN_PROGRESS,
ESP_HF_CALL_SETUP_STATUS_IDLE,
test_number,
ESP_HF_CALL_ADDR_TYPE_UNKNOWN
)
);
audio_open_at_ms = millis() + INTERCOM_AUDIO_OPEN_DELAY_MS;
}
static void gap_callback(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param)
{
switch (event) {
case ESP_BT_GAP_AUTH_CMPL_EVT:
#if PILOT_DEBUG_SERIAL
Serial.println(param->auth_cmpl.stat == ESP_BT_STATUS_SUCCESS
? "Intercom emparejado correctamente"
: "ERROR emparejando el intercom");
#endif
break;
case ESP_BT_GAP_CFM_REQ_EVT:
esp_bt_gap_ssp_confirm_reply(param->cfm_req.bda, true);
break;
case ESP_BT_GAP_PIN_REQ_EVT: {
esp_bt_pin_code_t pin = {'0', '0', '0', '0'};
esp_bt_gap_pin_reply(param->pin_req.bda, true, 4, pin);
break;
}
default:
break;
}
}
static void hfp_callback(esp_hf_cb_event_t event, esp_hf_cb_param_t *param)
{
switch (event) {
case ESP_HF_PROF_STATE_EVT:
hfp_ready = (param->prof_stat.state == ESP_HF_INIT_SUCCESS ||
param->prof_stat.state == ESP_HF_INIT_ALREADY);
if (hfp_ready) {
request_slc_connect(true);
}
break;
case ESP_HF_CONNECTION_STATE_EVT: {
const esp_hf_connection_state_t state = param->conn_stat.state;
slc_connected = (state == ESP_HF_CONNECTION_STATE_SLC_CONNECTED);
slc_connecting = (state == ESP_HF_CONNECTION_STATE_CONNECTING ||
state == ESP_HF_CONNECTION_STATE_CONNECTED);
if (state == ESP_HF_CONNECTION_STATE_DISCONNECTED) {
slc_connected = false;
slc_connecting = false;
audio_connected = false;
audio_connecting = false;
call_active = false;
call_start_at_ms = 0;
audio_open_at_ms = 0;
microphone_enabled = false;
speaker_clear_requested = true;
stop_hfp_audio_timer();
#if PILOT_DEBUG_SERIAL
Serial.println("Intercom desconectado. Se reintentara automaticamente.");
#endif
} else if (slc_connected) {
slc_connecting = false;
#if PILOT_DEBUG_SERIAL
Serial.println("Intercom HFP conectado");
#endif
esp_hf_ag_volume_control(intercom_addr, ESP_HF_VOLUME_CONTROL_TARGET_SPK, 12);
esp_hf_ag_volume_control(intercom_addr, ESP_HF_VOLUME_CONTROL_TARGET_MIC, 15);
call_start_at_ms = millis() + INTERCOM_CALL_DELAY_MS;
}
break;
}
case ESP_HF_AUDIO_STATE_EVT: {
const esp_hf_audio_state_t state = param->audio_stat.state;
audio_connecting = (state == ESP_HF_AUDIO_STATE_CONNECTING);
audio_connected = (state == ESP_HF_AUDIO_STATE_CONNECTED ||
state == ESP_HF_AUDIO_STATE_CONNECTED_MSBC);
codec_msbc = (state == ESP_HF_AUDIO_STATE_CONNECTED_MSBC);
if (audio_connected) {
audio_connecting = false;
downsample_phase = false;
speaker_clear_requested = true;
esp_hf_ag_register_data_callback(incoming_audio_callback, outgoing_audio_callback);
start_hfp_audio_timer();
#if PILOT_DEBUG_SERIAL
Serial.printf("Audio del intercom conectado: %s\n",
codec_msbc ? "mSBC 16 kHz" : "CVSD 8 kHz");
#endif
} else if (state == ESP_HF_AUDIO_STATE_DISCONNECTED) {
audio_connecting = false;
stop_hfp_audio_timer();
#if PILOT_DEBUG_SERIAL
Serial.println("Audio del intercom desconectado");
#endif
}
break;
}
case ESP_HF_IND_UPDATE_EVT:
send_indicator_state(param->ind_upd.remote_addr);
break;
case ESP_HF_CIND_RESPONSE_EVT:
esp_hf_ag_cind_response(
param->cind_rep.remote_addr,
call_active ? ESP_HF_CALL_STATUS_CALL_IN_PROGRESS : ESP_HF_CALL_STATUS_NO_CALLS,
ESP_HF_CALL_SETUP_STATUS_IDLE,
ESP_HF_NETWORK_STATE_AVAILABLE,
5,
ESP_HF_ROAMING_STATUS_INACTIVE,
5,
ESP_HF_CALL_HELD_STATUS_NONE
);
break;
case ESP_HF_COPS_RESPONSE_EVT: {
static char operator_name[] = "FormulaGades";
esp_hf_ag_cops_response(param->cops_rep.remote_addr, operator_name);
break;
}
case ESP_HF_CLCC_RESPONSE_EVT:
if (call_active) {
esp_hf_ag_clcc_response(
param->clcc_rep.remote_addr,
1,
ESP_HF_CURRENT_CALL_DIRECTION_OUTGOING,
ESP_HF_CURRENT_CALL_STATUS_ACTIVE,
ESP_HF_CURRENT_CALL_MODE_VOICE,
ESP_HF_CURRENT_CALL_MPTY_TYPE_SINGLE,
test_number,
ESP_HF_CALL_ADDR_TYPE_UNKNOWN
);
}
esp_hf_ag_clcc_response(
param->clcc_rep.remote_addr,
0,
ESP_HF_CURRENT_CALL_DIRECTION_OUTGOING,
ESP_HF_CURRENT_CALL_STATUS_ACTIVE,
ESP_HF_CURRENT_CALL_MODE_VOICE,
ESP_HF_CURRENT_CALL_MPTY_TYPE_SINGLE,
test_number,
ESP_HF_CALL_ADDR_TYPE_UNKNOWN
);
break;
case ESP_HF_CNUM_RESPONSE_EVT:
esp_hf_ag_cnum_response(
param->cnum_rep.remote_addr,
test_number,
129,
ESP_HF_SUBSCRIBER_SERVICE_TYPE_VOICE
);
break;
case ESP_HF_ATA_RESPONSE_EVT:
answer_current_call(param->ata_rep.remote_addr);
break;
case ESP_HF_CHUP_RESPONSE_EVT:
call_active = false;
audio_connected = false;
audio_connecting = false;
stop_hfp_audio_timer();
break;
case ESP_HF_DIAL_EVT:
esp_hf_ag_cmee_send(
param->out_call.remote_addr,
ESP_HF_AT_RESPONSE_CODE_OK,
ESP_HF_CME_AG_FAILURE
);
answer_current_call(param->out_call.remote_addr);
break;
case ESP_HF_UNAT_RESPONSE_EVT: {
// El V6 Pro+ envia +SUGCODEC=1. Hay que responder OK para completar SLC.
static char ok_response[] = "OK";
esp_hf_ag_unknown_at_send(param->unat_rep.remote_addr, ok_response);
break;
}
default:
break;
}
}
static bool init_bluetooth()
{
if (mic_buffer == nullptr) {
mic_buffer = xStreamBufferCreate(INTERCOM_MIC_BUFFER_BYTES, 1);
}
if (speaker_buffer == nullptr) {
speaker_buffer = xStreamBufferCreate(INTERCOM_SPEAKER_BUFFER_BYTES, 1);
}
if (mic_buffer == nullptr || speaker_buffer == nullptr) {
#if PILOT_DEBUG_SERIAL
Serial.println("ERROR creando buffers de audio Bluetooth");
#endif
return false;
}
if (!btStarted() && !btStartMode(BT_MODE_CLASSIC_BT)) {
#if PILOT_DEBUG_SERIAL
Serial.println("ERROR iniciando Bluetooth Classic");
#endif
return false;
}
if (esp_bredr_tx_power_set(ESP_PWR_LVL_P9, ESP_PWR_LVL_P9) != ESP_OK) {
#if PILOT_DEBUG_SERIAL
Serial.println("ERROR configurando Bluetooth Classic a maxima potencia");
#endif
return false;
}
esp_bluedroid_status_t status = esp_bluedroid_get_status();
if (status == ESP_BLUEDROID_STATUS_UNINITIALIZED) {
if (esp_bluedroid_init() != ESP_OK) {
return false;
}
}
status = esp_bluedroid_get_status();
if (status != ESP_BLUEDROID_STATUS_ENABLED) {
if (esp_bluedroid_enable() != ESP_OK) {
return false;
}
}
if (esp_bt_gap_register_callback(gap_callback) != ESP_OK) {
return false;
}
esp_bt_io_cap_t io_capability = ESP_BT_IO_CAP_NONE;
esp_bt_gap_set_security_param(ESP_BT_SP_IOCAP_MODE, &io_capability, sizeof(io_capability));
esp_bt_pin_code_t unused_pin = {0};
esp_bt_gap_set_pin(ESP_BT_PIN_TYPE_VARIABLE, 0, unused_pin);
esp_bt_dev_set_device_name(INTERCOM_BT_NAME);
esp_bt_cod_t cod = {};
cod.major = ESP_BT_COD_MAJOR_DEV_PHONE;
cod.minor = 0x00;
cod.service = ESP_BT_COD_SRVC_AUDIO | ESP_BT_COD_SRVC_TELEPHONY;
esp_bt_gap_set_cod(cod, ESP_BT_SET_COD_ALL);
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
if (esp_bredr_sco_datapath_set(ESP_SCO_DATA_PATH_HCI) != ESP_OK) {
return false;
}
if (esp_hf_ag_register_callback(hfp_callback) != ESP_OK) {
return false;
}
if (esp_hf_ag_register_data_callback(incoming_audio_callback, outgoing_audio_callback) != ESP_OK) {
return false;
}
if (esp_hf_ag_init() != ESP_OK) {
return false;
}
return true;
}
/* ============================================================
API USADA POR RADIO_PILOTO
============================================================ */
bool audio_init()
{
playback_enabled = false;
speaker_clear_requested = true;
microphone_enabled = false;
#if PILOT_DEBUG_SERIAL
Serial.print("Intercom configurado: ");
print_intercom_mac();
Serial.println();
#endif
return init_bluetooth();
}
void audio_process()
{
const uint32_t now = millis();
if (hfp_ready && !slc_connected) {
request_slc_connect(false);
}
if (slc_connected && call_start_at_ms != 0 &&
(int32_t)(now - call_start_at_ms) >= 0) {
call_start_at_ms = 0;
start_fake_call();
}
if (slc_connected && call_active && audio_open_at_ms != 0 &&
(int32_t)(now - audio_open_at_ms) >= 0) {
audio_open_at_ms = 0;
request_audio_connect(true);
}
if (slc_connected && call_active && !audio_connected &&
(uint32_t)(now - last_audio_attempt_ms) >= INTERCOM_AUDIO_RETRY_MS) {
request_audio_connect(false);
}
}
void audio_set_microphone_enabled(bool enabled)
{
microphone_enabled = false;
drain_microphone_buffer();
downsample_phase = false;
microphone_enabled = enabled;
}
bool audio_capture_frame(const int8_t **out_audio, uint16_t *out_len)
{
if (out_audio == nullptr || out_len == nullptr || mic_buffer == nullptr ||
!microphone_enabled || !audio_connected) {
return false;
}
size_t total = 0;
const uint32_t started = millis();
while (total < AUDIO_SAMPLES_PER_BLOCK) {
if (!microphone_enabled || !audio_connected) {
return false;
}
const size_t received = xStreamBufferReceive(
mic_buffer,
mic_frame + total,
AUDIO_SAMPLES_PER_BLOCK - total,
pdMS_TO_TICKS(20)
);
total += received;
if ((uint32_t)(millis() - started) >= INTERCOM_MIC_FRAME_TIMEOUT_MS) {
return false;
}
}
*out_audio = mic_frame;
*out_len = AUDIO_SAMPLES_PER_BLOCK;
return true;
}
void audio_play_frame_s8(const int8_t *audio, uint16_t len)
{
if (audio == nullptr || len == 0 || speaker_buffer == nullptr ||
!playback_enabled || !audio_connected) {
return;
}
if (len > AUDIO_MAX_PAYLOAD) {
len = AUDIO_MAX_PAYLOAD;
}
const size_t required_bytes = (size_t)len * sizeof(int16_t) * (codec_msbc ? 2U : 1U);
if (xStreamBufferSpacesAvailable(speaker_buffer) < required_bytes) {
return;
}
constexpr uint16_t INPUT_CHUNK = 160;
int16_t converted[INPUT_CHUNK * 2];
uint16_t position = 0;
while (position < len) {
uint16_t chunk = (uint16_t)(len - position);
if (chunk > INPUT_CHUNK) {
chunk = INPUT_CHUNK;
}
size_t output_samples = 0;
for (uint16_t i = 0; i < chunk; ++i) {
const int16_t sample = (int16_t)((int32_t)audio[position + i] * 256);
converted[output_samples++] = sample;
if (codec_msbc) {
converted[output_samples++] = sample;
}
}
const size_t bytes = output_samples * sizeof(int16_t);
const size_t written = xStreamBufferSend(
speaker_buffer,
reinterpret_cast<const uint8_t *>(converted),
bytes,
0
);
if (written != bytes) {
return;
}
position = (uint16_t)(position + chunk);
}
}
void audio_request_playback_stop()
{
playback_enabled = false;
speaker_clear_requested = true;
}
void audio_resume_playback()
{
playback_enabled = true;
}
void audio_cut_playback()
{
playback_enabled = false;
speaker_clear_requested = true;
}

View file

@ -0,0 +1,14 @@
#pragma once
#include <Arduino.h>
bool audio_init();
void audio_process();
void audio_set_microphone_enabled(bool enabled);
bool audio_capture_frame(const int8_t **out_audio, uint16_t *out_len);
void audio_play_frame_s8(const int8_t *audio, uint16_t len);
void audio_request_playback_stop();
void audio_resume_playback();
void audio_cut_playback();

View file

@ -0,0 +1,87 @@
#pragma once
#include <Arduino.h>
/* ============================================================
AUDIO DE LA RADIO
El protocolo de radio sigue usando audio mono de 8 bits a 8 kHz.
El intercom negocia normalmente mSBC a 16 kHz; audio.cpp hace la
conversión sencilla 16 kHz <-> 8 kHz sin aplicar filtros.
============================================================ */
#define AUDIO_SAMPLE_RATE 8000
#define AUDIO_BLOCK_MS 200
#define AUDIO_SAMPLES_PER_BLOCK ((AUDIO_SAMPLE_RATE * AUDIO_BLOCK_MS) / 1000)
#define AUDIO_MAX_PAYLOAD AUDIO_SAMPLES_PER_BLOCK
/* ============================================================
INTERCOM BLUETOOTH HFP
ESP32-A1S = Audio Gateway (como un teléfono)
V6 Pro+ = manos libres HFP
============================================================ */
#define INTERCOM_BT_NAME "FORMULA_GADES_HFP"
#define INTERCOM_BT_MAC {0x10, 0xDC, 0xB6, 0x74, 0x2D, 0x23}
#define INTERCOM_SLC_RETRY_MS 5000
#define INTERCOM_AUDIO_RETRY_MS 3000
#define INTERCOM_CALL_DELAY_MS 1200
#define INTERCOM_AUDIO_OPEN_DELAY_MS 600
#define INTERCOM_MIC_BUFFER_BYTES 6400
#define INTERCOM_SPEAKER_BUFFER_BYTES 32768
#define INTERCOM_CVSD_TRIGGER_US 4000
#define INTERCOM_MSBC_TRIGGER_US 7500
#define INTERCOM_MIC_FRAME_TIMEOUT_MS 500
/* ============================================================
PTT CON EL BOTON KEY2 DE LA ESP32-AUDIO-KIT
Switch 1 (IO13-KEY2): ON
Switch 2 (IO13-DATA3): OFF
Switch 4 (IO13-MTCK): OFF
============================================================ */
#define PTT_BUTTON_GPIO GPIO_NUM_13
#define PTT_BUTTON_ACTIVE_LEVEL LOW
#define PTT_DEBOUNCE_MS 25
#define PTT_BUTTON_DEBUG 0
/* ============================================================
UART CON EL ESP32-WROOM-32U INTERMEDIARIO
Audio Kit GPIO23 (TX) -> WROOM-U GPIO16 (RX)
Audio Kit GPIO22 (RX) <- WROOM-U GPIO17 (TX)
GND Audio Kit <-> GND WROOM-U
============================================================ */
#define PILOT_UART_PORT 2
#define PILOT_UART_BAUD 460800
#define PILOT_UART_RX_GPIO 22
#define PILOT_UART_TX_GPIO 23
#define PILOT_UART_RX_BUFFER_SIZE 8192
#define PILOT_UART_PACKET_QUEUE_SIZE 32
#define PILOT_UART_TASK_STACK 4096
#define PILOT_UART_TASK_PRIORITY 4
#define PILOT_UART_TASK_CORE 1
#define PILOT_UART_PARSER_TIMEOUT_MS 120
#define PILOT_UART_WRITE_TIMEOUT_MS 120
#define PILOT_TX_TASK_STACK 6144
#define PILOT_TX_TASK_PRIORITY 3
#define PILOT_TX_TASK_CORE 1
#define RADIO_AUDIO_CHUNK 200
#define RADIO_PACKET_HEADER_LEN 11
#define RADIO_PACKET_MAX_LEN (RADIO_PACKET_HEADER_LEN + RADIO_AUDIO_CHUNK)
#define RADIO_REASSEMBLY_TIMEOUT_MS 600
#define BOX_AUDIO_AUTOSTART_GUARD_MS 150
#define RADIO_AUDIO_MAGIC_1 0xE5
#define RADIO_AUDIO_MAGIC_2 0x5E
#define RADIO_CONTROL_MAGIC_1 0xE5
#define RADIO_CONTROL_MAGIC_2 0x5F
#define RADIO_CONTROL_COMMAND_BOX_PTT 0x01
#define RADIO_CONTROL_PACKET_LEN 4
#define PILOT_DEBUG_SERIAL 1

View file

@ -0,0 +1,62 @@
#include "ptt.hpp"
#include "config.hpp"
#include <Arduino.h>
static bool stable_pressed = false;
static bool last_raw_pressed = false;
static uint32_t last_raw_change_ms = 0;
bool ptt_init()
{
// KEY2 no lleva resistencia pull-up externa, por lo que activamos
// la pull-up interna. Al pulsar KEY2, GPIO13 queda conectado a GND.
pinMode(PTT_BUTTON_GPIO, INPUT_PULLUP);
delay(10);
last_raw_pressed =
(digitalRead(PTT_BUTTON_GPIO) == PTT_BUTTON_ACTIVE_LEVEL);
stable_pressed = last_raw_pressed;
last_raw_change_ms = millis();
#if PILOT_DEBUG_SERIAL
Serial.println("PTT configurado en KEY2 (GPIO13, activo a nivel LOW)");
Serial.println("DIP: 1=ON, 2=OFF y 4=OFF para usar KEY2 sin conflictos");
#endif
return true;
}
bool ptt_is_pressed()
{
const bool raw_pressed =
(digitalRead(PTT_BUTTON_GPIO) == PTT_BUTTON_ACTIVE_LEVEL);
// Reinicia el temporizador cada vez que cambia la lectura instantanea.
if (raw_pressed != last_raw_pressed) {
last_raw_pressed = raw_pressed;
last_raw_change_ms = millis();
}
// Solo acepta el nuevo estado cuando se mantiene estable el tiempo
// indicado. Esto elimina los rebotes mecanicos del pulsador.
if (stable_pressed != last_raw_pressed &&
(uint32_t)(millis() - last_raw_change_ms) >= PTT_DEBOUNCE_MS) {
stable_pressed = last_raw_pressed;
#if PILOT_DEBUG_SERIAL
Serial.println(stable_pressed
? "PTT KEY2 ACTIVADO"
: "PTT KEY2 DESACTIVADO");
#endif
}
#if PTT_BUTTON_DEBUG
Serial.print("KEY2 raw=");
Serial.print(raw_pressed ? "PULSADO" : "LIBRE");
Serial.print(" estable=");
Serial.println(stable_pressed ? "PULSADO" : "LIBRE");
#endif
return stable_pressed;
}

View file

@ -0,0 +1,4 @@
#pragma once
bool ptt_init();
bool ptt_is_pressed();

View file

@ -0,0 +1,679 @@
#include "uart_audio.hpp"
#include "audio.hpp"
#include "config.hpp"
#include <HardwareSerial.h>
#include <stddef.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
struct __attribute__((packed)) AudioWirePacket {
uint8_t magic1;
uint8_t magic2;
uint8_t frame_seq;
uint8_t chunk_index;
uint8_t chunk_total;
uint16_t frame_len;
uint16_t offset;
uint16_t chunk_len;
int8_t audio[RADIO_AUDIO_CHUNK];
};
struct UartRxItem {
uint32_t epoch;
uint16_t packet_len;
uint8_t packet[RADIO_PACKET_MAX_LEN];
};
static constexpr size_t AUDIO_HEADER_SIZE = offsetof(AudioWirePacket, audio);
static constexpr uint8_t MAX_AUDIO_CHUNKS =
(AUDIO_MAX_PAYLOAD + RADIO_AUDIO_CHUNK - 1) / RADIO_AUDIO_CHUNK;
static HardwareSerial PilotUart(PILOT_UART_PORT);
static QueueHandle_t rx_packet_queue = nullptr;
static TaskHandle_t rx_task_handle = nullptr;
static portMUX_TYPE state_mux = portMUX_INITIALIZER_UNLOCKED;
static volatile bool box_ptt_active = false;
static volatile bool box_stop_pending = false;
static volatile uint32_t box_stream_epoch = 0;
static volatile uint32_t box_control_version = 0;
static volatile uint32_t dropped_uart_packets = 0;
static volatile uint32_t box_last_stop_ms = 0;
static volatile uint32_t debug_control_start = 0;
static volatile uint32_t debug_control_stop = 0;
static volatile uint32_t debug_audio_packets = 0;
static volatile uint32_t debug_reassembled_frames = 0;
static volatile uint32_t debug_audio_autostarts = 0;
static volatile uint32_t debug_guard_drops = 0;
static int8_t reassembly_buffer[AUDIO_MAX_PAYLOAD];
static bool chunk_received[MAX_AUDIO_CHUNKS];
static bool reassembly_active = false;
static uint8_t reassembly_seq = 0;
static uint8_t reassembly_total_chunks = 0;
static uint8_t reassembly_received_chunks = 0;
static uint16_t reassembly_frame_len = 0;
static uint32_t reassembly_epoch = 0;
static uint32_t reassembly_started_ms = 0;
static uint16_t read_le_u16(const uint8_t *p)
{
return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
}
static bool audio_packet_valid(const uint8_t *data, uint16_t len)
{
if (data == nullptr || len < RADIO_PACKET_HEADER_LEN || len > RADIO_PACKET_MAX_LEN) {
return false;
}
if (data[0] != RADIO_AUDIO_MAGIC_1 || data[1] != RADIO_AUDIO_MAGIC_2) {
return false;
}
const uint8_t chunk_index = data[3];
const uint8_t chunk_total = data[4];
const uint16_t frame_len = read_le_u16(&data[5]);
const uint16_t offset = read_le_u16(&data[7]);
const uint16_t chunk_len = read_le_u16(&data[9]);
if (chunk_total == 0 || chunk_total > MAX_AUDIO_CHUNKS) {
return false;
}
if (chunk_index >= chunk_total) {
return false;
}
if (frame_len == 0 || frame_len > AUDIO_MAX_PAYLOAD) {
return false;
}
if (chunk_len == 0 || chunk_len > RADIO_AUDIO_CHUNK) {
return false;
}
if ((uint32_t)offset + chunk_len > frame_len) {
return false;
}
return len == RADIO_PACKET_HEADER_LEN + chunk_len;
}
static bool control_packet_valid(const uint8_t *data, uint16_t len)
{
return data != nullptr &&
len == RADIO_CONTROL_PACKET_LEN &&
data[0] == RADIO_CONTROL_MAGIC_1 &&
data[1] == RADIO_CONTROL_MAGIC_2 &&
data[2] == RADIO_CONTROL_COMMAND_BOX_PTT &&
(data[3] == 0 || data[3] == 1);
}
static void snapshot_control_state(
bool *active,
bool *stop_pending,
uint32_t *epoch,
uint32_t *version)
{
portENTER_CRITICAL(&state_mux);
if (active != nullptr) {
*active = box_ptt_active;
}
if (stop_pending != nullptr) {
*stop_pending = box_stop_pending;
}
if (epoch != nullptr) {
*epoch = box_stream_epoch;
}
if (version != nullptr) {
*version = box_control_version;
}
portEXIT_CRITICAL(&state_mux);
}
static void handle_control_packet(const uint8_t *packet)
{
const bool start_requested = packet[3] != 0;
const uint32_t now = millis();
bool started = false;
bool stop_registered = false;
portENTER_CRITICAL(&state_mux);
if (start_requested) {
// Los START periódicos son latidos. No cambian el epoch si el stream ya
// está activo. También cancelan un cierre pendiente si vuelve a pulsarse.
if (!box_ptt_active) {
box_ptt_active = true;
++box_stream_epoch;
++box_control_version;
started = true;
}
box_stop_pending = false;
} else {
box_last_stop_ms = now;
if (box_ptt_active && !box_stop_pending) {
// No cortar todavía: todos los paquetes anteriores al STOP ya están
// en la cola UART y deben reconstruirse/reproducirse primero.
box_stop_pending = true;
stop_registered = true;
}
}
portEXIT_CRITICAL(&state_mux);
if (started) {
++debug_control_start;
audio_resume_playback();
}
if (stop_registered) {
++debug_control_stop;
}
}
static void enqueue_audio_packet(const uint8_t *packet, uint16_t len)
{
if (rx_packet_queue == nullptr || !audio_packet_valid(packet, len)) {
return;
}
bool active = false;
bool stop_pending = false;
uint32_t epoch = 0;
snapshot_control_state(&active, &stop_pending, &epoch, nullptr);
if (stop_pending) {
++debug_guard_drops;
return;
}
if (!active) {
const uint32_t now = millis();
uint32_t last_stop = 0;
portENTER_CRITICAL(&state_mux);
last_stop = box_last_stop_ms;
portEXIT_CRITICAL(&state_mux);
// Si acaba de llegar STOP, estos pueden ser fragmentos atrasados y se
// descartan. Pasado el margen, un paquete de audio válido puede recuperar
// por sí solo un START perdido o un reinicio del Audio Kit.
if ((uint32_t)(now - last_stop) < BOX_AUDIO_AUTOSTART_GUARD_MS) {
++debug_guard_drops;
return;
}
portENTER_CRITICAL(&state_mux);
if (!box_ptt_active) {
box_ptt_active = true;
box_stop_pending = false;
++box_stream_epoch;
++box_control_version;
++debug_audio_autostarts;
}
active = box_ptt_active;
epoch = box_stream_epoch;
portEXIT_CRITICAL(&state_mux);
if (active) {
audio_resume_playback();
}
}
++debug_audio_packets;
UartRxItem item = {};
item.epoch = epoch;
item.packet_len = len;
memcpy(item.packet, packet, len);
if (xQueueSend(rx_packet_queue, &item, 0) != pdTRUE) {
// Mantener audio reciente es preferible a acumular retraso. Si la cola
// se llena, eliminamos el paquete más antiguo e insertamos el nuevo.
UartRxItem discarded = {};
xQueueReceive(rx_packet_queue, &discarded, 0);
if (xQueueSend(rx_packet_queue, &item, 0) != pdTRUE) {
++dropped_uart_packets;
}
}
}
static void uart_rx_task(void *parameter)
{
(void)parameter;
enum ParserState {
WAIT_MAGIC_1,
WAIT_MAGIC_2,
READ_AUDIO_HEADER,
READ_AUDIO_BODY,
READ_CONTROL
};
ParserState state = WAIT_MAGIC_1;
uint8_t packet[RADIO_PACKET_MAX_LEN] = {};
size_t index = 0;
uint16_t expected_audio_len = 0;
uint32_t last_byte_ms = millis();
auto reset_parser = [&]() {
state = WAIT_MAGIC_1;
index = 0;
expected_audio_len = 0;
};
for (;;) {
bool read_anything = false;
while (PilotUart.available() > 0) {
const int value = PilotUart.read();
if (value < 0) {
break;
}
read_anything = true;
const uint8_t b = (uint8_t)value;
const uint32_t now = millis();
if (state != WAIT_MAGIC_1 &&
(uint32_t)(now - last_byte_ms) > PILOT_UART_PARSER_TIMEOUT_MS) {
reset_parser();
}
last_byte_ms = now;
switch (state) {
case WAIT_MAGIC_1:
if (b == RADIO_AUDIO_MAGIC_1) {
packet[0] = b;
index = 1;
state = WAIT_MAGIC_2;
}
break;
case WAIT_MAGIC_2:
if (b == RADIO_AUDIO_MAGIC_2) {
packet[1] = b;
index = 2;
state = READ_AUDIO_HEADER;
} else if (b == RADIO_CONTROL_MAGIC_2) {
packet[1] = b;
index = 2;
state = READ_CONTROL;
} else if (b == RADIO_AUDIO_MAGIC_1) {
packet[0] = b;
index = 1;
} else {
reset_parser();
}
break;
case READ_CONTROL:
if (index >= sizeof(packet)) {
reset_parser();
break;
}
packet[index++] = b;
if (index >= RADIO_CONTROL_PACKET_LEN) {
if (control_packet_valid(packet, (uint16_t)index)) {
handle_control_packet(packet);
}
reset_parser();
}
break;
case READ_AUDIO_HEADER:
if (index >= sizeof(packet)) {
reset_parser();
break;
}
packet[index++] = b;
if (index >= RADIO_PACKET_HEADER_LEN) {
const uint16_t frame_len = read_le_u16(&packet[5]);
const uint16_t offset = read_le_u16(&packet[7]);
expected_audio_len = read_le_u16(&packet[9]);
if (packet[4] == 0 || packet[4] > MAX_AUDIO_CHUNKS ||
packet[3] >= packet[4] ||
frame_len == 0 || frame_len > AUDIO_MAX_PAYLOAD ||
expected_audio_len == 0 || expected_audio_len > RADIO_AUDIO_CHUNK ||
(uint32_t)offset + expected_audio_len > frame_len) {
reset_parser();
break;
}
state = READ_AUDIO_BODY;
}
break;
case READ_AUDIO_BODY:
if (index >= sizeof(packet)) {
reset_parser();
break;
}
packet[index++] = b;
if (index >= RADIO_PACKET_HEADER_LEN + expected_audio_len) {
enqueue_audio_packet(packet, (uint16_t)index);
reset_parser();
}
break;
}
}
if (!read_anything) {
vTaskDelay(pdMS_TO_TICKS(1));
} else {
taskYIELD();
}
}
}
static bool uart_write_all(const uint8_t *data, size_t len)
{
if (data == nullptr || len == 0) {
return false;
}
size_t sent = 0;
const uint32_t started = millis();
while (sent < len) {
const size_t written = PilotUart.write(data + sent, len - sent);
sent += written;
if (sent >= len) {
return true;
}
if ((uint32_t)(millis() - started) >= PILOT_UART_WRITE_TIMEOUT_MS) {
return false;
}
delay(0);
}
return true;
}
static void reset_reassembly(uint8_t seq, uint8_t total_chunks, uint16_t frame_len, uint32_t epoch)
{
reassembly_active = true;
reassembly_seq = seq;
reassembly_total_chunks = total_chunks;
reassembly_received_chunks = 0;
reassembly_frame_len = frame_len;
reassembly_epoch = epoch;
reassembly_started_ms = millis();
memset(chunk_received, 0, sizeof(chunk_received));
}
bool uart_audio_init()
{
rx_packet_queue = xQueueCreate(
PILOT_UART_PACKET_QUEUE_SIZE,
sizeof(UartRxItem)
);
if (rx_packet_queue == nullptr) {
#if PILOT_DEBUG_SERIAL
Serial.println("ERROR: no se pudo crear la cola UART de audio");
#endif
return false;
}
// Debe configurarse antes de begin(), según la API HardwareSerial del ESP32.
PilotUart.setRxBufferSize(PILOT_UART_RX_BUFFER_SIZE);
PilotUart.begin(
PILOT_UART_BAUD,
SERIAL_8N1,
PILOT_UART_RX_GPIO,
PILOT_UART_TX_GPIO
);
const BaseType_t task_result = xTaskCreatePinnedToCore(
uart_rx_task,
"pilot_uart_rx",
PILOT_UART_TASK_STACK,
nullptr,
PILOT_UART_TASK_PRIORITY,
&rx_task_handle,
PILOT_UART_TASK_CORE
);
if (task_result != pdPASS) {
#if PILOT_DEBUG_SERIAL
Serial.println("ERROR: no se pudo crear la tarea UART");
#endif
rx_task_handle = nullptr;
return false;
}
#if PILOT_DEBUG_SERIAL
Serial.println("UART con intermediario iniciada");
Serial.printf("Baudios: %d | RX GPIO%d | TX GPIO%d\n",
PILOT_UART_BAUD,
PILOT_UART_RX_GPIO,
PILOT_UART_TX_GPIO);
#endif
return true;
}
bool uart_send_audio(uint8_t frame_seq, const int8_t *audio_data, uint16_t audio_len)
{
if (audio_data == nullptr || audio_len == 0 || audio_len > AUDIO_MAX_PAYLOAD) {
return false;
}
const uint8_t total_chunks =
(uint8_t)((audio_len + RADIO_AUDIO_CHUNK - 1) / RADIO_AUDIO_CHUNK);
for (uint8_t chunk_index = 0; chunk_index < total_chunks; ++chunk_index) {
const uint16_t offset = (uint16_t)chunk_index * RADIO_AUDIO_CHUNK;
const uint16_t remaining = (uint16_t)(audio_len - offset);
const uint16_t chunk_len = remaining > RADIO_AUDIO_CHUNK
? RADIO_AUDIO_CHUNK
: remaining;
AudioWirePacket packet = {};
packet.magic1 = RADIO_AUDIO_MAGIC_1;
packet.magic2 = RADIO_AUDIO_MAGIC_2;
packet.frame_seq = frame_seq;
packet.chunk_index = chunk_index;
packet.chunk_total = total_chunks;
packet.frame_len = audio_len;
packet.offset = offset;
packet.chunk_len = chunk_len;
memcpy(packet.audio, audio_data + offset, chunk_len);
const size_t packet_size = AUDIO_HEADER_SIZE + chunk_len;
if (!uart_write_all(reinterpret_cast<const uint8_t *>(&packet), packet_size)) {
#if PILOT_DEBUG_SERIAL
Serial.printf("ERROR UART enviando fragmento %u/%u\n",
(unsigned)chunk_index,
(unsigned)total_chunks);
#endif
return false;
}
}
return true;
}
void uart_discard_received_audio()
{
if (rx_packet_queue != nullptr) {
UartRxItem discarded = {};
while (xQueueReceive(rx_packet_queue, &discarded, 0) == pdTRUE) {
}
}
reassembly_active = false;
reassembly_received_chunks = 0;
memset(chunk_received, 0, sizeof(chunk_received));
}
static bool finalize_box_stop_if_drained()
{
if (rx_packet_queue == nullptr || uxQueueMessagesWaiting(rx_packet_queue) != 0 || reassembly_active) {
return false;
}
bool changed = false;
portENTER_CRITICAL(&state_mux);
if (box_stop_pending && box_ptt_active) {
box_stop_pending = false;
box_ptt_active = false;
++box_stream_epoch;
++box_control_version;
changed = true;
}
portEXIT_CRITICAL(&state_mux);
return changed;
}
bool uart_box_ptt_active()
{
bool active = false;
snapshot_control_state(&active, nullptr, nullptr, nullptr);
return active;
}
void uart_process_received_audio(bool reproduce)
{
static uint32_t processed_control_version = 0;
bool active = false;
uint32_t current_epoch = 0;
uint32_t current_version = 0;
snapshot_control_state(&active, nullptr, &current_epoch, &current_version);
if (current_version != processed_control_version) {
processed_control_version = current_version;
reassembly_active = false;
reassembly_received_chunks = 0;
memset(chunk_received, 0, sizeof(chunk_received));
if (!active) {
uart_discard_received_audio();
// El último frame ya se escribió al I2S. Se solicita el cierre sin
// vaciarlo, y el silencio final se añade tras el timeout normal.
audio_request_playback_stop();
} else if (reproduce) {
audio_resume_playback();
}
}
if (!reproduce || !active) {
uart_discard_received_audio();
return;
}
if (reassembly_active &&
(uint32_t)(millis() - reassembly_started_ms) > RADIO_REASSEMBLY_TIMEOUT_MS) {
reassembly_active = false;
}
UartRxItem item = {};
while (rx_packet_queue != nullptr &&
xQueueReceive(rx_packet_queue, &item, 0) == pdTRUE) {
snapshot_control_state(&active, nullptr, &current_epoch, nullptr);
if (!active || item.epoch != current_epoch) {
continue;
}
if (!audio_packet_valid(item.packet, item.packet_len)) {
continue;
}
AudioWirePacket packet = {};
memcpy(&packet, item.packet, item.packet_len);
if (!reassembly_active ||
reassembly_epoch != item.epoch ||
packet.frame_seq != reassembly_seq ||
packet.chunk_total != reassembly_total_chunks ||
packet.frame_len != reassembly_frame_len) {
reset_reassembly(
packet.frame_seq,
packet.chunk_total,
packet.frame_len,
item.epoch
);
}
if (!chunk_received[packet.chunk_index]) {
memcpy(
reassembly_buffer + packet.offset,
packet.audio,
packet.chunk_len
);
chunk_received[packet.chunk_index] = true;
++reassembly_received_chunks;
}
if (reassembly_received_chunks >= reassembly_total_chunks) {
++debug_reassembled_frames;
audio_play_frame_s8(reassembly_buffer, reassembly_frame_len);
reassembly_active = false;
}
}
// STOP se aplica únicamente cuando ya no queda ningún paquete ni un frame
// incompleto. Así los auriculares reproducen el final completo del mensaje.
if (finalize_box_stop_if_drained()) {
audio_request_playback_stop();
}
}
void uart_audio_debug_report()
{
#if PILOT_DEBUG_SERIAL
static uint32_t last_report_ms = 0;
const uint32_t now = millis();
if ((uint32_t)(now - last_report_ms) < 1000) {
return;
}
last_report_ms = now;
bool active = false;
bool stop_pending = false;
uint32_t epoch = 0;
uint32_t version = 0;
snapshot_control_state(&active, &stop_pending, &epoch, &version);
const UBaseType_t queued =
rx_packet_queue != nullptr ? uxQueueMessagesWaiting(rx_packet_queue) : 0;
Serial.printf(
"PILOTO BOX->INTERCOM: PTT=%s cierre_pendiente=%s ctrlON=%lu ctrlOFF=%lu paquetes=%lu frames=%lu autoSTART=%lu guard=%lu cola=%u drop=%lu epoch=%lu version=%lu\n",
active ? "ON" : "OFF",
stop_pending ? "SI" : "NO",
(unsigned long)debug_control_start,
(unsigned long)debug_control_stop,
(unsigned long)debug_audio_packets,
(unsigned long)debug_reassembled_frames,
(unsigned long)debug_audio_autostarts,
(unsigned long)debug_guard_drops,
(unsigned int)queued,
(unsigned long)dropped_uart_packets,
(unsigned long)epoch,
(unsigned long)version
);
#endif
}

View file

@ -0,0 +1,10 @@
#pragma once
#include <Arduino.h>
bool uart_audio_init();
bool uart_send_audio(uint8_t frame_seq, const int8_t *audio_data, uint16_t audio_len);
void uart_process_received_audio(bool reproduce);
void uart_discard_received_audio();
bool uart_box_ptt_active();
void uart_audio_debug_report();