feat: MEMS microphone software handling for TX15 and TX16SMK3 (#7328)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
3djc
2026-06-03 13:31:40 +10:00
committed by GitHub
co-authored by Claude Sonnet 4.6
parent 13bba22a4d
commit a7d730fded
39 changed files with 1385 additions and 4 deletions
+1
View File
@@ -98,6 +98,7 @@ set(GUI_SRC
radio/radio_trainer.cpp
radio/radio_version.cpp
radio/radio_gps_tool.cpp
radio/radio_mic_recorder.cpp
)
macro(add_gui_src src)
@@ -0,0 +1,282 @@
/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#include "radio_mic_recorder.h"
#if defined(PDM_CLOCK)
#include <stdio.h>
#include "button.h"
#include "dialog.h"
#include "edgetx.h"
#include "ff.h"
#include "sdcard.h"
#include "static.h"
#include "timers_driver.h"
static constexpr coord_t MIC_BTN_W = 260;
static constexpr coord_t MIC_BTN_H = EdgeTxStyles::UI_ELEMENT_HEIGHT * 2 + PAD_LARGE;
RadioMicRecorder::RadioMicRecorder() :
Page(ICON_RADIO_TOOLS)
{
// Bring the PDM hardware up only while this tool is open. Capture is off
// outside of this scope so the mic clock and DMA stream are idle.
pdmStart();
buildHeader(header);
buildBody(body);
enterIdle();
}
RadioMicRecorder::~RadioMicRecorder()
{
// An async call scheduled from the LabelDialog confirm path may still be
// pending when the page is closed (e.g. user long-presses EXIT while the
// dialog is being dismissed). Cancelling ensures the callback never fires
// on a destroyed instance.
lv_async_call_cancel(&RadioMicRecorder::asyncProcessPendingRename, this);
if (recorder.isRecording()) recorder.stop();
pdmStop();
}
void RadioMicRecorder::asyncProcessPendingRename(void* ctx)
{
static_cast<RadioMicRecorder*>(ctx)->processPendingRename();
}
void RadioMicRecorder::buildHeader(Window* window)
{
header->setTitle(STR_MENUTOOLS);
header->setTitle2(STR_MIC_RECORDER);
}
void RadioMicRecorder::buildBody(Window* window)
{
window->padAll(PAD_ZERO);
const coord_t w = window->width();
const coord_t h = window->height();
bigLabel = new StaticText(
window,
{0, h / 3 - 20, w, 40},
"", COLOR_THEME_PRIMARY1_INDEX, FONT(XL));
lv_obj_set_style_text_align(bigLabel->getLvObj(), LV_TEXT_ALIGN_CENTER, 0);
infoLabel = new StaticText(
window,
{0, h / 3 + 30, w, 24},
"", COLOR_THEME_SECONDARY1_INDEX, FONT(STD));
lv_obj_set_style_text_align(infoLabel->getLvObj(), LV_TEXT_ALIGN_CENTER, 0);
actionButton = new TextButton(
window,
{(w - MIC_BTN_W) / 2,
h - MIC_BTN_H - PAD_LARGE * 2,
MIC_BTN_W, MIC_BTN_H},
"", [this]() {
onActionPressed();
return 0;
});
actionButton->setFont(FONT_XL_INDEX);
}
void RadioMicRecorder::onEvent(event_t event)
{
if (event == EVT_KEY_LONG(KEY_EXIT)) {
killEvents(event);
if (recorder.isRecording()) recorder.stop();
onCancel();
}
}
void RadioMicRecorder::onActionPressed()
{
switch (state) {
case State::IDLE: enterCountdown(); break;
case State::COUNTDOWN: enterIdle(); break; // cancel
case State::RECORDING: stopRecording(); break;
}
}
void RadioMicRecorder::enterIdle()
{
if (recorder.isRecording()) recorder.stop();
state = State::IDLE;
stateStart = get_tmr10ms();
refreshUI();
}
void RadioMicRecorder::enterCountdown()
{
state = State::COUNTDOWN;
stateStart = get_tmr10ms();
refreshUI();
}
void RadioMicRecorder::enterRecording()
{
pickNextFilename();
FRESULT res = recorder.start(filename, 0);
if (res != FR_OK) {
char msg[40];
snprintf(msg, sizeof(msg), "%s %u", STR_OPEN_ERROR, (unsigned)res);
bigLabel->setText(msg);
state = State::IDLE;
stateStart = get_tmr10ms();
actionButton->setText(STR_RECORD);
return;
}
state = State::RECORDING;
stateStart = get_tmr10ms();
refreshUI();
}
void RadioMicRecorder::stopRecording()
{
recorder.stop();
PdmWavRecorder::trimSilence(filename);
state = State::IDLE;
stateStart = get_tmr10ms();
refreshUI();
const char* base = strrchr(filename, '/');
base = base ? base + 1 : filename;
char baseName[PATH_MAX_LEN] = {};
strncpy(baseName, base, sizeof(baseName) - 1);
char* dot = strrchr(baseName, '.');
if (dot) *dot = '\0';
new LabelDialog(baseName, LEN_FUNCTION_NAME, STR_SAVE_AS, [this](std::string newName) {
if (newName.empty()) return;
char dir[sizeof(SOUNDS_PATH) + 1];
strcpy(dir, SOUNDS_PATH "/");
strncpy(dir + SOUNDS_PATH_LNG_OFS, currentLanguagePack->id, 2);
snprintf(pendingRename, sizeof(pendingRename), "%s%s.wav", dir, newName.c_str());
if (strcmp(pendingRename, filename) == 0) { refreshUI(); return; }
// Defer the overwrite check until LabelDialog has finished closing —
// creating a modal now would leave us stacked on top of LabelDialog,
// which corrupts the lv_group chain when LabelDialog deletes itself.
// The page dtor cancels this async call if the user bails out before
// it fires (see ~RadioMicRecorder).
lv_async_call(&RadioMicRecorder::asyncProcessPendingRename, this);
});
}
void RadioMicRecorder::processPendingRename()
{
FILINFO info;
if (f_stat(pendingRename, &info) == FR_OK) {
new ConfirmDialog(STR_FILE_EXISTS, STR_ASK_OVERWRITE,
[this]() { applyRename(); });
} else {
applyRename();
}
}
void RadioMicRecorder::applyRename()
{
f_unlink(pendingRename);
f_rename(filename, pendingRename);
strncpy(filename, pendingRename, sizeof(filename) - 1);
filename[sizeof(filename) - 1] = '\0';
refreshUI();
}
void RadioMicRecorder::refreshUI()
{
char buf[64];
switch (state) {
case State::IDLE:
bigLabel->setText(STR_PUSH_TO_RECORD);
if (recorder.getSamplesWritten() > 0) {
snprintf(buf, sizeof(buf), "%s %s (%us)", STR_SAVED,
filename, (unsigned)recorder.getElapsedSeconds());
infoLabel->setText(buf);
} else {
infoLabel->setText("");
}
actionButton->setText(STR_RECORD);
break;
case State::COUNTDOWN: {
const uint32_t elapsed10 = (uint32_t)(get_tmr10ms() - stateStart);
int remaining = (int)COUNTDOWN_SECONDS - (int)(elapsed10 / 100U);
if (remaining < 0) remaining = 0;
snprintf(buf, sizeof(buf), "%s %d", STR_STARTING_IN, remaining);
bigLabel->setText(buf);
infoLabel->setText(STR_GET_READY);
actionButton->setText(STR_CANCEL);
break;
}
case State::RECORDING: {
const uint32_t s = recorder.getElapsedSeconds();
snprintf(buf, sizeof(buf), "%s %02u:%02u", STR_REC,
(unsigned)(s / 60U), (unsigned)(s % 60U));
bigLabel->setText(buf);
snprintf(buf, sizeof(buf), "%s %u KB",
filename, (unsigned)(recorder.getBytesWritten() / 1024U));
infoLabel->setText(buf);
actionButton->setText(STR_STOP);
break;
}
}
}
void RadioMicRecorder::checkEvents()
{
Page::checkEvents();
if (state == State::COUNTDOWN) {
const uint32_t elapsed10 = (uint32_t)(get_tmr10ms() - stateStart);
if (elapsed10 >= COUNTDOWN_SECONDS * 100U) {
enterRecording();
} else {
refreshUI();
}
} else if (state == State::RECORDING) {
if (!recorder.isRecording()) {
state = State::IDLE;
stateStart = get_tmr10ms();
}
refreshUI();
}
}
void RadioMicRecorder::pickNextFilename()
{
char dir[sizeof(SOUNDS_PATH) + 1];
strcpy(dir, SOUNDS_PATH "/");
strncpy(dir + SOUNDS_PATH_LNG_OFS, currentLanguagePack->id, 2);
FILINFO info;
for (int i = 0; i < 100; i++) {
snprintf(filename, sizeof(filename), "%srec_%02d.wav", dir, i);
if (f_stat(filename, &info) != FR_OK) return;
}
snprintf(filename, sizeof(filename), "%srec.wav", dir);
}
#endif // PDM_CLOCK
@@ -0,0 +1,75 @@
/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#pragma once
#include "page.h"
#include "pdm_wav_recorder.h"
#include "sdcard.h"
#if defined(PDM_CLOCK)
class TextButton;
class StaticText;
class RadioMicRecorder : public Page
{
public:
RadioMicRecorder();
~RadioMicRecorder() override;
protected:
enum class State : uint8_t { IDLE, COUNTDOWN, RECORDING };
static constexpr uint32_t COUNTDOWN_SECONDS = 5;
static constexpr int PATH_MAX_LEN = sizeof(SOUNDS_PATH) + 14; // "/lang/rec_00.wav\0"
State state = State::IDLE;
tmr10ms_t stateStart = 0;
char filename[PATH_MAX_LEN] = {0};
char pendingRename[PATH_MAX_LEN] = {0};
PdmWavRecorder recorder;
StaticText* bigLabel = nullptr;
StaticText* infoLabel = nullptr;
TextButton* actionButton = nullptr;
void buildHeader(Window* window);
void buildBody(Window* window);
void checkEvents() override;
void onEvent(event_t event) override;
void onActionPressed();
void enterIdle();
void enterCountdown();
void enterRecording();
void stopRecording();
void processPendingRename();
void applyRename();
void refreshUI();
void pickNextFilename();
// Stable callback address so the dtor can cancel a pending lv_async call
// scheduled from the LabelDialog confirm path.
static void asyncProcessPendingRename(void* ctx);
};
#endif // PDM_CLOCK
@@ -29,6 +29,7 @@
#include "radio_ghost_module_config.h"
#include "radio_spectrum_analyser.h"
#include "radio_gps_tool.h"
#include "radio_mic_recorder.h"
#include "standalone_lua.h"
#include "etx_lv_theme.h"
#include "lib_file.h"
@@ -211,6 +212,13 @@ static void run_ghost_config(const std::string&)
}
#endif
#if defined(PDM_CLOCK)
static void run_mic_recorder(const std::string&)
{
new RadioMicRecorder();
}
#endif
//-----------------------------------------------------------------------------
struct ToolButton : public TextButton {
@@ -349,6 +357,10 @@ void RadioToolsPage::rebuild(Window* window)
}
#endif
#if defined(PDM_CLOCK)
tools.emplace_back(ToolEntry{STR_MIC_RECORDER, "", run_mic_recorder});
#endif
#if defined(LUA)
scanLuaTools(tools);
#endif