diff --git a/boards/tlora_pager/include/boards/tlora_pager/tlora_pager_board.h b/boards/tlora_pager/include/boards/tlora_pager/tlora_pager_board.h index e517b7ba..678cb26b 100644 --- a/boards/tlora_pager/include/boards/tlora_pager/tlora_pager_board.h +++ b/boards/tlora_pager/include/boards/tlora_pager/tlora_pager_board.h @@ -70,6 +70,16 @@ class SX1262Access : public SX1262 namespace boards::tlora_pager { +enum class PagerAudioOwner : uint8_t +{ + None, + MessageTone, + IncomingCallTone, + ReticulumCall, + Walkie, + Sstv, +}; + /** * @class TLoRaPagerBoard * @brief Main board class for T-LoRa-Pager hardware @@ -235,6 +245,17 @@ class TLoRaPagerBoard : public BoardBase, void setMessageToneVolume(uint8_t volume_percent) override; uint8_t getMessageToneVolume() const override; + int openAudioSession(PagerAudioOwner owner, uint8_t bits_per_sample, + uint8_t channels, uint32_t sample_rate, + bool speaker_enabled); + void closeAudioSession(PagerAudioOwner owner); + int audioRead(PagerAudioOwner owner, uint8_t* buffer, size_t size); + int audioWrite(PagerAudioOwner owner, const uint8_t* buffer, size_t size); + bool audioSetVolume(PagerAudioOwner owner, uint8_t level); + bool audioSetGain(PagerAudioOwner owner, float db_value); + bool audioSetMute(PagerAudioOwner owner, bool enabled); + bool audioSetOutMute(PagerAudioOwner owner, bool enabled); + /** * @brief Set haptic effect waveform * @param effects Effect number (0-127, see DRV2605 documentation) diff --git a/boards/tlora_pager/src/tlora_pager_board.cpp b/boards/tlora_pager/src/tlora_pager_board.cpp index e4521e23..b848799d 100644 --- a/boards/tlora_pager/src/tlora_pager_board.cpp +++ b/boards/tlora_pager/src/tlora_pager_board.cpp @@ -8,7 +8,9 @@ #include "freertos/task.h" #include "freertos/timers.h" +#include #include +#include #include #include #include @@ -76,6 +78,82 @@ static int s_gauge_error_streak = 0; static int s_gauge_reinit_count = 0; static uint32_t s_last_radio_spi_lock_timeout_log_ms = 0; static uint32_t s_suppressed_radio_spi_lock_timeout_logs = 0; +static portMUX_TYPE s_audio_owner_mux = portMUX_INITIALIZER_UNLOCKED; +static PagerAudioOwner s_audio_owner = PagerAudioOwner::None; +static uint8_t s_audio_logical_channels = 0; +static constexpr size_t kAudioConversionFrames = 256; +static int16_t s_audio_read_scratch[kAudioConversionFrames * 2] = {}; +static int16_t s_audio_write_scratch[kAudioConversionFrames * 2] = {}; + +const char* audioOwnerLabel(PagerAudioOwner owner) +{ + switch (owner) + { + case PagerAudioOwner::MessageTone: + return "message_tone"; + case PagerAudioOwner::IncomingCallTone: + return "incoming_call_tone"; + case PagerAudioOwner::ReticulumCall: + return "reticulum_call"; + case PagerAudioOwner::Walkie: + return "walkie"; + case PagerAudioOwner::Sstv: + return "sstv"; + case PagerAudioOwner::None: + default: + return "none"; + } +} + +PagerAudioOwner currentAudioOwner() +{ + portENTER_CRITICAL(&s_audio_owner_mux); + const PagerAudioOwner owner = s_audio_owner; + portEXIT_CRITICAL(&s_audio_owner_mux); + return owner; +} + +bool claimAudioOwner(PagerAudioOwner owner, uint8_t logical_channels) +{ + if (owner == PagerAudioOwner::None || logical_channels == 0) + { + return false; + } + + portENTER_CRITICAL(&s_audio_owner_mux); + const bool claimed = s_audio_owner == PagerAudioOwner::None; + if (claimed) + { + s_audio_owner = owner; + s_audio_logical_channels = logical_channels; + } + portEXIT_CRITICAL(&s_audio_owner_mux); + return claimed; +} + +bool audioOwnerMatches(PagerAudioOwner owner) +{ + return owner != PagerAudioOwner::None && currentAudioOwner() == owner; +} + +uint8_t audioLogicalChannels(PagerAudioOwner owner) +{ + portENTER_CRITICAL(&s_audio_owner_mux); + const uint8_t channels = s_audio_owner == owner ? s_audio_logical_channels : 0; + portEXIT_CRITICAL(&s_audio_owner_mux); + return channels; +} + +void releaseAudioOwner(PagerAudioOwner owner) +{ + portENTER_CRITICAL(&s_audio_owner_mux); + if (s_audio_owner == owner) + { + s_audio_owner = PagerAudioOwner::None; + s_audio_logical_channels = 0; + } + portEXIT_CRITICAL(&s_audio_owner_mux); +} // Temperature state (from BQ27220), used for safety / UI hints / brightness caps. static float s_last_temp_c = NAN; @@ -590,13 +668,9 @@ uint32_t TLoRaPagerBoard::begin(uint32_t disable_hw_init) if (codec.begin(Wire, 0x18, CODEC_TYPE_ES8311)) { devices_probe |= HW_CODEC_ONLINE; + powerControl(POWER_SPEAK, false); log_d("Audio codec (ES8311) initialized successfully"); Serial.printf("[TLoRaPagerBoard::begin] audio codec init end ok=1\n"); - - // Set power amplifier control callback - codec.setPaPinCallback([](bool enable, void* user_data) - { ((ExtensionIOXL9555*)user_data)->digitalWrite(EXPANDS_AMP_EN, enable); }, - &io); } else { @@ -1101,6 +1175,268 @@ void TLoRaPagerBoard::stopVibrator() log_d("[stopVibrator] Power disabled, function completed"); } +int TLoRaPagerBoard::openAudioSession(PagerAudioOwner owner, uint8_t bits_per_sample, + uint8_t channels, uint32_t sample_rate, + bool speaker_enabled) +{ +#ifndef USING_AUDIO_CODEC + (void)owner; + (void)bits_per_sample; + (void)channels; + (void)sample_rate; + (void)speaker_enabled; + return -1; +#else + if (!(devices_probe & HW_CODEC_ONLINE) || owner == PagerAudioOwner::None) + { + return -1; + } + if (!claimAudioOwner(owner, channels)) + { + Serial.printf("[Audio][Pager] busy requested=%s owner=%s\n", + audioOwnerLabel(owner), + audioOwnerLabel(currentAudioOwner())); + return -2; + } + + powerControl(POWER_SPEAK, false); + const uint8_t hardware_channels = channels == 1 ? 2 : channels; + int state = -1; + { + I2CGuard i2c; + state = codec.open(bits_per_sample, hardware_channels, sample_rate); + if (state == 0) + { + codec.setMute(true); + codec.setOutMute(true); + } + } + if (state != 0) + { + releaseAudioOwner(owner); + Serial.printf("[Audio][Pager] open failed owner=%s rc=%d rate=%lu logical_ch=%u hw_ch=%u\n", + audioOwnerLabel(owner), + state, + static_cast(sample_rate), + static_cast(channels), + static_cast(hardware_channels)); + return state; + } + + if (speaker_enabled) + { + powerControl(POWER_SPEAK, true); + delay(10); + } + Serial.printf("[Audio][Pager] open owner=%s rate=%lu logical_ch=%u hw_ch=%u speaker=%u\n", + audioOwnerLabel(owner), + static_cast(sample_rate), + static_cast(channels), + static_cast(hardware_channels), + speaker_enabled ? 1U : 0U); + return 0; +#endif +} + +void TLoRaPagerBoard::closeAudioSession(PagerAudioOwner owner) +{ +#ifdef USING_AUDIO_CODEC + if (!audioOwnerMatches(owner)) + { + return; + } + + { + I2CGuard i2c; + codec.setMute(true); + codec.setOutMute(true); + } + powerControl(POWER_SPEAK, false); + { + I2CGuard i2c; + codec.close(); + } + releaseAudioOwner(owner); + Serial.printf("[Audio][Pager] close owner=%s\n", audioOwnerLabel(owner)); +#else + (void)owner; +#endif +} + +int TLoRaPagerBoard::audioRead(PagerAudioOwner owner, uint8_t* buffer, size_t size) +{ +#ifdef USING_AUDIO_CODEC + const uint8_t logical_channels = audioLogicalChannels(owner); + if (!buffer || size == 0 || logical_channels == 0) + { + return -1; + } + if (logical_channels != 1) + { + return codec.read(buffer, size); + } + if ((size % sizeof(int16_t)) != 0) + { + return -1; + } + + auto* mono = reinterpret_cast(buffer); + const size_t frame_count = size / sizeof(int16_t); + size_t frame_offset = 0; + while (frame_offset < frame_count) + { + const size_t chunk_frames = + std::min(kAudioConversionFrames, frame_count - frame_offset); + const int state = codec.read( + reinterpret_cast(s_audio_read_scratch), + chunk_frames * 2 * sizeof(int16_t)); + if (state != 0) + { + return state; + } + + int64_t left_energy = 0; + int64_t right_energy = 0; + for (size_t index = 0; index < chunk_frames; ++index) + { + const int32_t left = s_audio_read_scratch[index * 2]; + const int32_t right = s_audio_read_scratch[(index * 2) + 1]; + left_energy += left < 0 ? -left : left; + right_energy += right < 0 ? -right : right; + } + const size_t active_channel = right_energy > left_energy ? 1 : 0; + for (size_t index = 0; index < chunk_frames; ++index) + { + mono[frame_offset + index] = + s_audio_read_scratch[(index * 2) + active_channel]; + } + frame_offset += chunk_frames; + } + return 0; +#else + (void)owner; + (void)buffer; + (void)size; + return -1; +#endif +} + +int TLoRaPagerBoard::audioWrite(PagerAudioOwner owner, const uint8_t* buffer, size_t size) +{ +#ifdef USING_AUDIO_CODEC + const uint8_t logical_channels = audioLogicalChannels(owner); + if (!buffer || size == 0 || logical_channels == 0) + { + return -1; + } + if (logical_channels != 1) + { + return codec.write(const_cast(buffer), size); + } + if ((size % sizeof(int16_t)) != 0) + { + return -1; + } + + const auto* mono = reinterpret_cast(buffer); + const size_t frame_count = size / sizeof(int16_t); + size_t frame_offset = 0; + while (frame_offset < frame_count) + { + const size_t chunk_frames = + std::min(kAudioConversionFrames, frame_count - frame_offset); + for (size_t index = 0; index < chunk_frames; ++index) + { + const int16_t sample = mono[frame_offset + index]; + s_audio_write_scratch[index * 2] = sample; + s_audio_write_scratch[(index * 2) + 1] = sample; + } + const int state = codec.write( + reinterpret_cast(s_audio_write_scratch), + chunk_frames * 2 * sizeof(int16_t)); + if (state != 0) + { + return state; + } + frame_offset += chunk_frames; + } + return 0; +#else + (void)owner; + (void)buffer; + (void)size; + return -1; +#endif +} + +bool TLoRaPagerBoard::audioSetVolume(PagerAudioOwner owner, uint8_t level) +{ +#ifdef USING_AUDIO_CODEC + if (!audioOwnerMatches(owner)) + { + return false; + } + I2CGuard i2c; + codec.setVolume(level); + return true; +#else + (void)owner; + (void)level; + return false; +#endif +} + +bool TLoRaPagerBoard::audioSetGain(PagerAudioOwner owner, float db_value) +{ +#ifdef USING_AUDIO_CODEC + if (!audioOwnerMatches(owner)) + { + return false; + } + I2CGuard i2c; + codec.setGain(db_value); + return true; +#else + (void)owner; + (void)db_value; + return false; +#endif +} + +bool TLoRaPagerBoard::audioSetMute(PagerAudioOwner owner, bool enabled) +{ +#ifdef USING_AUDIO_CODEC + if (!audioOwnerMatches(owner)) + { + return false; + } + I2CGuard i2c; + codec.setMute(enabled); + return true; +#else + (void)owner; + (void)enabled; + return false; +#endif +} + +bool TLoRaPagerBoard::audioSetOutMute(PagerAudioOwner owner, bool enabled) +{ +#ifdef USING_AUDIO_CODEC + if (!audioOwnerMatches(owner)) + { + return false; + } + I2CGuard i2c; + codec.setOutMute(enabled); + return true; +#else + (void)owner; + (void)enabled; + return false; +#endif +} + void TLoRaPagerBoard::playMessageTone() { #ifndef USING_AUDIO_CODEC @@ -1135,42 +1471,43 @@ void TLoRaPagerBoard::playMessageTone() static constexpr size_t kFramesPerChunk = 128; namespace pager_tone = ::platform::ui::audio::pager_notification; - int prev_volume = codec.getVolume(); - if (prev_volume < 0 || prev_volume > 100) - { - prev_volume = 50; - } - bool prev_out_mute = codec.getOutMute(); - - const int open_result = - codec.open(16, pager_tone::kChannels, pager_tone::kPlaybackSampleRateHz); + constexpr PagerAudioOwner kOwner = PagerAudioOwner::MessageTone; + const int open_result = openAudioSession( + kOwner, 16, 1, pager_tone::kPlaybackSampleRateHz, true); if (open_result != 0) { - Serial.printf("[Audio][PagerTone] open failed kind=message rc=%d rate=%lu channels=%u\n", + Serial.printf("[Audio][PagerTone] open failed kind=message rc=%d rate=%lu channels=1\n", open_result, - static_cast(pager_tone::kPlaybackSampleRateHz), - static_cast(pager_tone::kChannels)); + static_cast(pager_tone::kPlaybackSampleRateHz)); s_playing = false; return; } - codec.setOutMute(false); - codec.setVolume(_message_tone_volume); - delay(10); + if (!audioSetMute(kOwner, true) || + !audioSetVolume(kOwner, _message_tone_volume) || + !audioSetOutMute(kOwner, false)) + { + closeAudioSession(kOwner); + s_playing = false; + return; + } - int16_t pcm[kFramesPerChunk * pager_tone::kChannels]; + int16_t pcm[kFramesPerChunk]; pager_tone::AdpcmPlaybackState tone_state{}; while (pager_tone::hasMore(tone_state)) { - const uint16_t frames = pager_tone::fillStereoInterleaved( - tone_state, pcm, static_cast(kFramesPerChunk)); + uint16_t frames = 0; + while (frames < kFramesPerChunk && + pager_tone::nextPlaybackSample(tone_state, pcm[frames])) + { + ++frames; + } if (frames == 0) { break; } - const int write_result = codec.write(reinterpret_cast(pcm), - frames * pager_tone::kChannels * - sizeof(int16_t)); + const int write_result = audioWrite( + kOwner, reinterpret_cast(pcm), frames * sizeof(int16_t)); if (write_result != 0) { Serial.printf("[Audio][PagerTone] write failed kind=message rc=%d frames=%u\n", @@ -1178,12 +1515,14 @@ void TLoRaPagerBoard::playMessageTone() static_cast(frames)); break; } + delay(1); } - delay(5); - codec.setVolume(static_cast(prev_volume)); - codec.setOutMute(prev_out_mute); - codec.close(); + memset(pcm, 0, sizeof(pcm)); + (void)audioWrite(kOwner, reinterpret_cast(pcm), sizeof(pcm)); + (void)audioWrite(kOwner, reinterpret_cast(pcm), sizeof(pcm)); + delay(7); + closeAudioSession(kOwner); s_playing = false; #endif } @@ -1222,30 +1561,28 @@ void TLoRaPagerBoard::playIncomingCallTone(const volatile bool* stop_requested) static constexpr size_t kFramesPerChunk = 128; namespace call_tone = ::platform::ui::audio::call_notification; - int prev_volume = codec.getVolume(); - if (prev_volume < 0 || prev_volume > 100) - { - prev_volume = 50; - } - bool prev_out_mute = codec.getOutMute(); - - const int open_result = - codec.open(16, call_tone::kChannels, call_tone::kPlaybackSampleRateHz); + constexpr PagerAudioOwner kOwner = PagerAudioOwner::IncomingCallTone; + const int open_result = openAudioSession( + kOwner, 16, 1, call_tone::kPlaybackSampleRateHz, true); if (open_result != 0) { - Serial.printf("[Audio][PagerTone] open failed kind=call rc=%d rate=%lu channels=%u\n", + Serial.printf("[Audio][PagerTone] open failed kind=call rc=%d rate=%lu channels=1\n", open_result, - static_cast(call_tone::kPlaybackSampleRateHz), - static_cast(call_tone::kChannels)); + static_cast(call_tone::kPlaybackSampleRateHz)); s_playing = false; return; } - codec.setOutMute(false); - codec.setVolume(_message_tone_volume); - delay(10); + if (!audioSetMute(kOwner, true) || + !audioSetVolume(kOwner, _message_tone_volume) || + !audioSetOutMute(kOwner, false)) + { + closeAudioSession(kOwner); + s_playing = false; + return; + } - int16_t pcm[kFramesPerChunk * call_tone::kChannels]; + int16_t pcm[kFramesPerChunk]; call_tone::AdpcmPlaybackState tone_state{}; while (call_tone::hasMore(tone_state)) { @@ -1253,15 +1590,18 @@ void TLoRaPagerBoard::playIncomingCallTone(const volatile bool* stop_requested) { break; } - const uint16_t frames = call_tone::fillStereoInterleaved( - tone_state, pcm, static_cast(kFramesPerChunk)); + uint16_t frames = 0; + while (frames < kFramesPerChunk && + call_tone::nextPlaybackSample(tone_state, pcm[frames])) + { + ++frames; + } if (frames == 0) { break; } - const int write_result = codec.write(reinterpret_cast(pcm), - frames * call_tone::kChannels * - sizeof(int16_t)); + const int write_result = audioWrite( + kOwner, reinterpret_cast(pcm), frames * sizeof(int16_t)); if (write_result != 0) { Serial.printf("[Audio][PagerTone] write failed kind=call rc=%d frames=%u\n", @@ -1269,12 +1609,14 @@ void TLoRaPagerBoard::playIncomingCallTone(const volatile bool* stop_requested) static_cast(frames)); break; } + delay(1); } - delay(5); - codec.setVolume(static_cast(prev_volume)); - codec.setOutMute(prev_out_mute); - codec.close(); + memset(pcm, 0, sizeof(pcm)); + (void)audioWrite(kOwner, reinterpret_cast(pcm), sizeof(pcm)); + (void)audioWrite(kOwner, reinterpret_cast(pcm), sizeof(pcm)); + delay(7); + closeAudioSession(kOwner); s_playing = false; #endif } diff --git a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_adapter.cpp b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_adapter.cpp index 00d8d863..1fb1dba7 100644 --- a/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_adapter.cpp +++ b/platform/esp/arduino_common/src/chat/infra/lxmf/lxmf_adapter.cpp @@ -3930,14 +3930,21 @@ bool LxmfAdapter::handleLinkRequestPacket( return false; } - if (packet.transport_id && - !hashesEqual(packet.transport_id, identity_.identityHash(), reticulum::kTruncatedHashSize)) + LocalDestinationKind local_kind = LocalDestinationKind::Delivery; + const bool local_destination = + isLocalDestinationHash(packet.destination_hash, &local_kind); + const bool wifi_final_delivery = + local_destination && + ingress_interface == reticulum::interfaces::InterfaceKind::WifiGateway; + if (packet.transport_id && !wifi_final_delivery && + !hashesEqual(packet.transport_id, + identity_.identityHash(), + reticulum::kTruncatedHashSize)) { return false; } - LocalDestinationKind local_kind = LocalDestinationKind::Delivery; - if (isLocalDestinationHash(packet.destination_hash, &local_kind)) + if (local_destination) { if (local_kind == LocalDestinationKind::CallAudio && ingress_interface != reticulum::interfaces::InterfaceKind::WifiGateway) diff --git a/platform/esp/arduino_common/src/reticulum_call_audio_runtime.cpp b/platform/esp/arduino_common/src/reticulum_call_audio_runtime.cpp index 328c50a5..323acef8 100644 --- a/platform/esp/arduino_common/src/reticulum_call_audio_runtime.cpp +++ b/platform/esp/arduino_common/src/reticulum_call_audio_runtime.cpp @@ -19,14 +19,15 @@ namespace { constexpr uint8_t kBitsPerSample = 16; -constexpr uint8_t kCodecChannels = 2; +constexpr uint8_t kCodecChannels = 1; constexpr std::size_t kMaxCodec2SamplesPerFrame = 320; constexpr uint8_t kDefaultVolume = 78; constexpr float kDefaultGainDb = 36.0f; +constexpr auto kAudioOwner = + ::boards::tlora_pager::PagerAudioOwner::ReticulumCall; bool s_registered = false; ::boards::tlora_pager::TLoRaPagerBoard* s_board = nullptr; -int16_t s_codec_pcm[kMaxCodec2SamplesPerFrame * kCodecChannels] = {}; ::boards::tlora_pager::TLoRaPagerBoard* resolve_board() { @@ -49,16 +50,26 @@ bool open_audio(uint32_t sample_rate_hz) { s_board = resolve_board(); if (!s_board || - s_board->codec.open(kBitsPerSample, kCodecChannels, sample_rate_hz) != 0) + s_board->openAudioSession(kAudioOwner, + kBitsPerSample, + kCodecChannels, + sample_rate_hz, + true) != 0) { s_board = nullptr; return false; } - s_board->codec.setVolume(s_board->getMessageToneVolume()); - s_board->codec.setGain(kDefaultGainDb); - s_board->codec.setMute(false); - s_board->codec.setOutMute(false); + if (!s_board->audioSetVolume(kAudioOwner, + s_board->getMessageToneVolume()) || + !s_board->audioSetGain(kAudioOwner, kDefaultGainDb) || + !s_board->audioSetMute(kAudioOwner, false) || + !s_board->audioSetOutMute(kAudioOwner, false)) + { + s_board->closeAudioSession(kAudioOwner); + s_board = nullptr; + return false; + } return true; } @@ -66,9 +77,7 @@ void close_audio() { if (s_board) { - s_board->codec.setMute(true); - s_board->codec.setOutMute(true); - s_board->codec.close(); + s_board->closeAudioSession(kAudioOwner); } s_board = nullptr; } @@ -77,18 +86,12 @@ bool read_mono(int16_t* pcm, std::size_t sample_count) { if (!s_board || !pcm || sample_count == 0 || sample_count > kMaxCodec2SamplesPerFrame || - s_board->codec.read(reinterpret_cast(s_codec_pcm), - sample_count * kCodecChannels * sizeof(int16_t)) != 0) + s_board->audioRead(kAudioOwner, + reinterpret_cast(pcm), + sample_count * sizeof(int16_t)) != 0) { return false; } - - for (std::size_t index = 0; index < sample_count; ++index) - { - const int32_t left = s_codec_pcm[index * kCodecChannels]; - const int32_t right = s_codec_pcm[(index * kCodecChannels) + 1U]; - pcm[index] = static_cast((left + right) / 2); - } return true; } @@ -100,14 +103,9 @@ bool write_mono(const int16_t* pcm, std::size_t sample_count) return false; } - for (std::size_t index = 0; index < sample_count; ++index) - { - s_codec_pcm[index * kCodecChannels] = pcm[index]; - s_codec_pcm[(index * kCodecChannels) + 1U] = pcm[index]; - } - return s_board->codec.write(reinterpret_cast(s_codec_pcm), - sample_count * kCodecChannels * - sizeof(int16_t)) == 0; + return s_board->audioWrite(kAudioOwner, + reinterpret_cast(pcm), + sample_count * sizeof(int16_t)) == 0; } uint8_t speaker_volume() @@ -126,7 +124,7 @@ void set_speaker_volume(uint8_t volume_percent) } if (s_board) { - s_board->codec.setVolume(volume); + (void)s_board->audioSetVolume(kAudioOwner, volume); } } diff --git a/platform/esp/arduino_common/src/sstv/sstv_service.cpp b/platform/esp/arduino_common/src/sstv/sstv_service.cpp index 436e0df7..e6bee4a8 100644 --- a/platform/esp/arduino_common/src/sstv/sstv_service.cpp +++ b/platform/esp/arduino_common/src/sstv/sstv_service.cpp @@ -183,6 +183,13 @@ uint32_t millis() return sys::millis_now(); } #endif + +#if defined(TRAIL_MATE_ESP_BOARD_TAB5) +using SstvBoard = TLoRaPagerBoard; +#else +using SstvBoard = boards::tlora_pager::TLoRaPagerBoard; +#endif + constexpr uint32_t kTaskStack = 12288; constexpr uint32_t kTaskDelayMs = 2; constexpr uint32_t kCompleteHoldMs = 2000; @@ -966,17 +973,71 @@ void render_line_from_buffer(uint8_t line_rgb[320][4], e_mode mode, uint16_t lin s_has_image = true; } -void sstv_task(void*) +bool open_sstv_audio(SstvBoard* board) +{ + if (!board) + { + return false; + } +#if defined(TRAIL_MATE_ESP_BOARD_TAB5) + if (board->codec.open(kBitsPerSample, kChannels, kCodecSampleRate) != 0) + { + return false; + } + board->codec.setGain(kMicGainDb); + board->codec.setMute(false); + board->codec.setOutMute(true); + board->codec.setVolume(0); + board->powerControl(POWER_SPEAK, false); + return true; +#else + constexpr auto kOwner = boards::tlora_pager::PagerAudioOwner::Sstv; + if (board->openAudioSession( + kOwner, kBitsPerSample, kChannels, kCodecSampleRate, false) != 0) + { + return false; + } + if (!board->audioSetGain(kOwner, kMicGainDb) || + !board->audioSetMute(kOwner, false) || + !board->audioSetOutMute(kOwner, true) || + !board->audioSetVolume(kOwner, 0)) + { + board->closeAudioSession(kOwner); + return false; + } + return true; +#endif +} + +int read_sstv_audio(SstvBoard* board, uint8_t* buffer, size_t size) { #if defined(TRAIL_MATE_ESP_BOARD_TAB5) - TLoRaPagerBoard* board = TLoRaPagerBoard::getInstance(); + return board ? board->codec.read(buffer, size) : -1; #else - boards::tlora_pager::TLoRaPagerBoard* board = boards::tlora_pager::TLoRaPagerBoard::getInstance(); + return board ? board->audioRead( + boards::tlora_pager::PagerAudioOwner::Sstv, + buffer, + size) + : -1; #endif - int prev_volume = -1; - bool prev_out_mute = false; - bool restore_amp = false; - bool prev_amp = true; +} + +void close_sstv_audio(SstvBoard* board) +{ + if (!board) + { + return; + } +#if defined(TRAIL_MATE_ESP_BOARD_TAB5) + board->codec.close(); +#else + board->closeAudioSession(boards::tlora_pager::PagerAudioOwner::Sstv); +#endif +} + +void sstv_task(void*) +{ + SstvBoard* board = SstvBoard::getInstance(); if (!board) { set_error("Board not ready"); @@ -986,7 +1047,7 @@ void sstv_task(void*) return; } - if (board->codec.open(kBitsPerSample, kChannels, kCodecSampleRate) != 0) + if (!open_sstv_audio(board)) { set_error("Codec open failed"); set_status(sstv::State::Error, 0, 0.0f, 0.0f, false); @@ -996,18 +1057,6 @@ void sstv_task(void*) } s_codec_open = true; - board->codec.setGain(kMicGainDb); - board->codec.setMute(false); - prev_volume = board->codec.getVolume(); - prev_out_mute = board->codec.getOutMute(); - board->codec.setOutMute(true); - board->codec.setVolume(0); -#ifdef USING_XL9555_EXPANDS - prev_amp = board->io.digitalRead(EXPANDS_AMP_EN); - restore_amp = true; -#endif - // Disable speaker amplifier during RX to avoid feedback/noise. - board->powerControl(POWER_SPEAK, false); SSTV_LOG("[SSTV] codec open ok (rate=%lu, decode=%lu, bits=%u, ch=%u)\n", static_cast(kCodecSampleRate), static_cast(kSampleRate), @@ -1023,7 +1072,7 @@ void sstv_task(void*) set_error("No audio buffer"); set_status(sstv::State::Error, 0, 0.0f, 0.0f, false); s_codec_open = false; - board->codec.close(); + close_sstv_audio(board); s_active = false; vTaskDelete(nullptr); return; @@ -1040,7 +1089,7 @@ void sstv_task(void*) set_error("No decode buffers"); set_status(sstv::State::Error, 0, 0.0f, 0.0f, false); s_codec_open = false; - board->codec.close(); + close_sstv_audio(board); s_active = false; vTaskDelete(nullptr); return; @@ -1063,7 +1112,7 @@ void sstv_task(void*) set_error("No framebuffer"); set_status(sstv::State::Error, 0, 0.0f, 0.0f, false); s_codec_open = false; - board->codec.close(); + close_sstv_audio(board); s_active = false; vTaskDelete(nullptr); return; @@ -1123,8 +1172,9 @@ void sstv_task(void*) while (!s_stop) { - int read_state = board->codec.read(reinterpret_cast(buffer), - samples_per_read * sizeof(int16_t)); + int read_state = read_sstv_audio(board, + reinterpret_cast(buffer), + samples_per_read * sizeof(int16_t)); if (read_state != 0) { set_error("Audio read failed"); @@ -1489,16 +1539,7 @@ void sstv_task(void*) if (s_codec_open) { - if (prev_volume >= 0) - { - board->codec.setVolume(static_cast(prev_volume)); - } - board->codec.setOutMute(prev_out_mute); - if (restore_amp) - { - board->powerControl(POWER_SPEAK, prev_amp); - } - board->codec.close(); + close_sstv_audio(board); s_codec_open = false; } diff --git a/platform/esp/arduino_common/src/walkie_runtime.cpp b/platform/esp/arduino_common/src/walkie_runtime.cpp index dfc46d84..38106e8a 100644 --- a/platform/esp/arduino_common/src/walkie_runtime.cpp +++ b/platform/esp/arduino_common/src/walkie_runtime.cpp @@ -163,9 +163,16 @@ int codecOpen(Session* session, uint8_t bits_per_sample, uint8_t channels, uint3 { return -1; } - int state = board->codec.open(bits_per_sample, channels, sample_rate); + constexpr auto kOwner = ::boards::tlora_pager::PagerAudioOwner::Walkie; + int state = board->openAudioSession( + kOwner, bits_per_sample, channels, sample_rate, true); if (state == 0) { + if (!board->audioSetOutMute(kOwner, false)) + { + board->closeAudioSession(kOwner); + return -1; + } session->codec_open = true; } return state; @@ -178,20 +185,28 @@ void codecClose(Session* session) { return; } - board->codec.close(); + board->closeAudioSession(::boards::tlora_pager::PagerAudioOwner::Walkie); session->codec_open = false; } int codecRead(Session* session, uint8_t* buffer, size_t size) { ::boards::tlora_pager::TLoRaPagerBoard* board = resolveBoard(session); - return board ? board->codec.read(buffer, size) : -1; + return board ? board->audioRead( + ::boards::tlora_pager::PagerAudioOwner::Walkie, + buffer, + size) + : -1; } int codecWrite(Session* session, uint8_t* buffer, size_t size) { ::boards::tlora_pager::TLoRaPagerBoard* board = resolveBoard(session); - return board ? board->codec.write(buffer, size) : -1; + return board ? board->audioWrite( + ::boards::tlora_pager::PagerAudioOwner::Walkie, + buffer, + size) + : -1; } void codecSetVolume(Session* session, uint8_t level) @@ -199,7 +214,8 @@ void codecSetVolume(Session* session, uint8_t level) ::boards::tlora_pager::TLoRaPagerBoard* board = resolveBoard(session); if (board) { - board->codec.setVolume(level); + (void)board->audioSetVolume( + ::boards::tlora_pager::PagerAudioOwner::Walkie, level); } } @@ -208,7 +224,8 @@ void codecSetGain(Session* session, float db_value) ::boards::tlora_pager::TLoRaPagerBoard* board = resolveBoard(session); if (board) { - board->codec.setGain(db_value); + (void)board->audioSetGain( + ::boards::tlora_pager::PagerAudioOwner::Walkie, db_value); } } @@ -217,7 +234,9 @@ void codecSetMute(Session* session, bool enabled) ::boards::tlora_pager::TLoRaPagerBoard* board = resolveBoard(session); if (board) { - board->codec.setMute(enabled); + constexpr auto kOwner = ::boards::tlora_pager::PagerAudioOwner::Walkie; + (void)board->audioSetMute(kOwner, enabled); + (void)board->audioSetOutMute(kOwner, enabled); } } diff --git a/tests/reticulum_conformance/deviations.md b/tests/reticulum_conformance/deviations.md index 3e9c539f..85a82fde 100644 --- a/tests/reticulum_conformance/deviations.md +++ b/tests/reticulum_conformance/deviations.md @@ -33,3 +33,4 @@ Each entry must be classified before it drives code changes. | RCNF-006 | Announce validation | The conformance smoke now verifies `microReticulum` announce vectors structurally and cryptographically, including signature validation and destination-hash validation. | | RCNF-007 | Runtime state ownership | Trail Mate has explicit transport, link, resource, propagation, propagation-service, and delivery runtime helpers for the current embedded Reticulum runtime ownership contract. | | RCNF-008 | Supported-subset golden baseline | Trail Mate has deterministic product-subset golden vectors for the Reticulum/LXMF bytes currently used by supported business paths; canonical Python Reticulum/LXMF cross-check remains a later conformance upgrade. | +| RCNF-009 | Wi-Fi gateway final LinkRequest delivery | A Header 2 LinkRequest addressed to a local destination is now accepted as final delivery when it arrives from the configured Wi-Fi gateway, even when the retained transport id belongs to that gateway. Transport-id matching remains required for non-local forwarding and non-gateway ingress. |