Snapshot Beep + WAV UI before Pomodoro rewrite.
Keep LEDC tones, TF WAV playback, and compact list layout as a tagged baseline. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
41
CHANGELOG.md
41
CHANGELOG.md
@@ -1,5 +1,46 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-10-18 — WAV via DAC2 only (touch-safe)
|
||||
|
||||
- Drop I2S INTERNAL_DAC (it enables GPIO25 and kills touch).
|
||||
- PCM WAV → RAM → dac_output on GPIO26 only; Beep tab unchanged.
|
||||
|
||||
## 2026-10-18 — Beep tab + WAV tab
|
||||
|
||||
- Keep 16 LEDC beeps on Beep tab.
|
||||
- WAV tab scans TF (/ and /wav); short files load to RAM then play via Internal DAC.
|
||||
- Core0 decode task; gain capped for onboard amp.
|
||||
|
||||
## 2026-10-18 — Built-in UI beep sounds
|
||||
|
||||
- Drop MP3; LEDC tone bank: Click/Key/Pop/Confirm/Cancel/Success/Error/...
|
||||
- Scrollable SOUNDS list; tap to play.
|
||||
|
||||
## 2026-10-18 — Smooth MP3 + keep UI alive
|
||||
|
||||
- MP3 decode on FreeRTOS core0; UI/touch on core1.
|
||||
- DAC1(GPIO25) disabled while playing so touch CLK works.
|
||||
- No SD rescan during playback (SPI contention).
|
||||
|
||||
## 2026-10-18 — Music list from TF
|
||||
|
||||
- Scan root + /music for .mp3; scrollable list; tap row to play.
|
||||
- PLAY uses selected track; auto-enables speaker on list tap.
|
||||
|
||||
## 2026-10-18 — Play /paomo.mp3 from TF
|
||||
|
||||
- PLAY streams `/paomo.mp3` via ESP8266Audio + internal DAC (GPIO26).
|
||||
- Serial `#PUT` + `tools/sd_put.py` to copy MP3 onto the TF card.
|
||||
|
||||
## 2026-10-18 — Canon in D melody
|
||||
|
||||
- Replace Twinkle Twinkle with Pachelbel Canon in D (monophonic).
|
||||
|
||||
## 2026-10-17 — ASCII glyphs + volume slider
|
||||
|
||||
- Replace ·/— (tofu boxes) with ASCII `-` / `|`.
|
||||
- Add VOL slider (0-100); live duty update while playing.
|
||||
|
||||
## 2026-10-17 — UI layout refresh
|
||||
|
||||
- Compact 320x240 layout: header status, capacity hero, usage bar, dual actions.
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <stdint.h>
|
||||
|
||||
void audio_init();
|
||||
|
||||
void audio_set_enabled(bool on);
|
||||
bool audio_is_enabled();
|
||||
|
||||
// 音量 0–100,默认 20
|
||||
void audio_set_volume(uint8_t percent);
|
||||
uint8_t audio_get_volume();
|
||||
|
||||
void audio_play_music();
|
||||
// 内置提示音
|
||||
uint8_t sound_count();
|
||||
const char *sound_name(uint8_t idx);
|
||||
int8_t sound_selected();
|
||||
void sound_set_selected(int8_t idx);
|
||||
void audio_play_sound(uint8_t idx);
|
||||
|
||||
// WAV(TF)
|
||||
void audio_play_wav_index(uint8_t idx);
|
||||
void audio_play_selected(); // 按当前 tab 语义由 UI 决定;保留播 beep 选中
|
||||
|
||||
void audio_stop();
|
||||
bool audio_is_playing();
|
||||
|
||||
void audio_tick();
|
||||
|
||||
const char *audio_last_status();
|
||||
|
||||
7
include/sd_put.h
Normal file
7
include/sd_put.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// 串口协议: "#PUT <path> <bytes>\n" -> READY -> raw -> DONE/FAIL
|
||||
// 在 loop 里调用;返回 true 表示刚处理完一次传输
|
||||
bool sd_put_poll();
|
||||
@@ -6,4 +6,8 @@
|
||||
void ui_create();
|
||||
void ui_update(const SdCardInfo &info);
|
||||
void ui_refresh_audio_buttons();
|
||||
void ui_refresh_lists();
|
||||
void ui_set_touch_debug(int16_t lx, int16_t ly, bool down);
|
||||
|
||||
// 0=Beep 1=WAV
|
||||
uint8_t ui_active_tab();
|
||||
|
||||
23
include/wav_list.h
Normal file
23
include/wav_list.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
static constexpr size_t WAV_MAX = 24;
|
||||
static constexpr size_t WAV_PATH_MAX = 64;
|
||||
static constexpr size_t WAV_NAME_MAX = 28;
|
||||
|
||||
struct WavTrack {
|
||||
char path[WAV_PATH_MAX];
|
||||
char name[WAV_NAME_MAX];
|
||||
uint32_t bytes;
|
||||
};
|
||||
|
||||
uint8_t wav_list_count();
|
||||
int8_t wav_list_selected();
|
||||
void wav_list_set_selected(int8_t idx);
|
||||
const WavTrack *wav_list_get(uint8_t idx);
|
||||
const char *wav_list_current_path();
|
||||
|
||||
bool wav_list_scan();
|
||||
12
include/wav_player.h
Normal file
12
include/wav_player.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <stdint.h>
|
||||
|
||||
void wav_player_init();
|
||||
void wav_player_stop();
|
||||
bool wav_player_is_playing();
|
||||
bool wav_player_play_path(const char *path);
|
||||
void wav_player_set_volume(uint8_t percent); // 0-100, maps to SetGain
|
||||
void wav_player_tick(); // no-op if task handles; kept for API
|
||||
const char *wav_player_status();
|
||||
@@ -3,7 +3,8 @@
|
||||
platform = espressif32
|
||||
board = esp32dev
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
monitor_speed = 460800
|
||||
upload_speed = 460800
|
||||
board_build.partitions = default.csv
|
||||
|
||||
lib_deps =
|
||||
@@ -47,6 +48,8 @@ build_flags =
|
||||
-DLV_USE_BUTTON=1
|
||||
-DLV_USE_LABEL=1
|
||||
-DLV_USE_BAR=1
|
||||
-DLV_USE_SLIDER=1
|
||||
-DLV_USE_TABVIEW=1
|
||||
-I${PROJECT_DIR}/include
|
||||
|
||||
extra_scripts = pre:copy_lv_conf.py
|
||||
|
||||
@@ -1,34 +1,92 @@
|
||||
#include "audio_test.h"
|
||||
#include "pins.h"
|
||||
#include "wav_list.h"
|
||||
#include "wav_player.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
static const char *g_status = "Speaker OFF";
|
||||
static bool g_enabled = false;
|
||||
static bool g_playing = false;
|
||||
static uint8_t g_volume = 20; // 默认 20%
|
||||
static bool g_beep_playing = false;
|
||||
static uint8_t g_volume = 40;
|
||||
static int8_t g_selected = 0;
|
||||
|
||||
static size_t g_note_i = 0;
|
||||
static uint32_t g_until_ms = 0;
|
||||
|
||||
enum Phase : uint8_t { kStartNote, kWaitNote, kWaitGap };
|
||||
static Phase g_phase = kStartNote;
|
||||
enum Phase : uint8_t { kStart, kWaitTone, kWaitGap };
|
||||
static Phase g_phase = kStart;
|
||||
|
||||
static constexpr int k_ledc_ch = 0;
|
||||
static constexpr int k_ledc_res = 8;
|
||||
|
||||
struct Note {
|
||||
struct ToneNote {
|
||||
uint16_t freq;
|
||||
uint16_t ms;
|
||||
};
|
||||
|
||||
static const Note k_music[] = {
|
||||
{262, 280}, {262, 280}, {392, 280}, {392, 280}, {440, 280}, {440, 280}, {392, 520},
|
||||
{349, 280}, {349, 280}, {330, 280}, {330, 280}, {294, 280}, {294, 280}, {262, 520},
|
||||
{392, 280}, {392, 280}, {349, 280}, {349, 280}, {330, 280}, {330, 280}, {294, 520},
|
||||
{392, 280}, {392, 280}, {349, 280}, {349, 280}, {330, 280}, {330, 280}, {294, 520},
|
||||
{262, 280}, {262, 280}, {392, 280}, {392, 280}, {440, 280}, {440, 280}, {392, 520},
|
||||
{349, 280}, {349, 280}, {330, 280}, {330, 280}, {294, 280}, {294, 280}, {262, 700},
|
||||
struct SoundDef {
|
||||
const char *name;
|
||||
const ToneNote *notes;
|
||||
uint8_t count;
|
||||
};
|
||||
|
||||
static constexpr size_t k_music_len = sizeof(k_music) / sizeof(k_music[0]);
|
||||
static const ToneNote k_click[] = {{1200, 40}};
|
||||
static const ToneNote k_confirm[] = {{880, 70}, {0, 30}, {1175, 90}};
|
||||
static const ToneNote k_cancel[] = {{600, 80}, {0, 40}, {440, 100}};
|
||||
static const ToneNote k_success[] = {
|
||||
{523, 80}, {0, 20}, {659, 80}, {0, 20}, {784, 120}};
|
||||
static const ToneNote k_error[] = {
|
||||
{400, 120}, {0, 40}, {400, 120}, {0, 40}, {300, 180}};
|
||||
static const ToneNote k_warn[] = {{900, 90}, {0, 70}, {900, 90}};
|
||||
static const ToneNote k_notify[] = {
|
||||
{1000, 50}, {0, 40}, {1000, 50}, {0, 40}, {1000, 50}};
|
||||
static const ToneNote k_boot[] = {
|
||||
{440, 70}, {0, 20}, {554, 70}, {0, 20}, {659, 100}};
|
||||
static const ToneNote k_shutdown[] = {
|
||||
{659, 80}, {0, 20}, {554, 80}, {0, 20}, {440, 120}};
|
||||
static const ToneNote k_alarm[] = {
|
||||
{880, 120}, {0, 80}, {880, 120}, {0, 80}, {880, 120},
|
||||
{0, 80}, {880, 200}};
|
||||
static const ToneNote k_connect[] = {{700, 60}, {0, 30}, {1050, 100}};
|
||||
static const ToneNote k_disconnect[] = {{1050, 60}, {0, 30}, {700, 100}};
|
||||
static const ToneNote k_lowbat[] = {{500, 150}, {0, 100}, {400, 200}};
|
||||
static const ToneNote k_chime[] = {
|
||||
{784, 100}, {0, 30}, {988, 100}, {0, 30}, {1175, 140}};
|
||||
static const ToneNote k_key[] = {{1400, 25}};
|
||||
static const ToneNote k_pop[] = {{1800, 30}, {0, 15}, {1200, 40}};
|
||||
|
||||
static const SoundDef k_sounds[] = {
|
||||
{"Click", k_click, (uint8_t)(sizeof(k_click) / sizeof(k_click[0]))},
|
||||
{"Key", k_key, (uint8_t)(sizeof(k_key) / sizeof(k_key[0]))},
|
||||
{"Pop", k_pop, (uint8_t)(sizeof(k_pop) / sizeof(k_pop[0]))},
|
||||
{"Confirm", k_confirm, (uint8_t)(sizeof(k_confirm) / sizeof(k_confirm[0]))},
|
||||
{"Cancel", k_cancel, (uint8_t)(sizeof(k_cancel) / sizeof(k_cancel[0]))},
|
||||
{"Success", k_success, (uint8_t)(sizeof(k_success) / sizeof(k_success[0]))},
|
||||
{"Error", k_error, (uint8_t)(sizeof(k_error) / sizeof(k_error[0]))},
|
||||
{"Warn", k_warn, (uint8_t)(sizeof(k_warn) / sizeof(k_warn[0]))},
|
||||
{"Notify", k_notify, (uint8_t)(sizeof(k_notify) / sizeof(k_notify[0]))},
|
||||
{"Boot", k_boot, (uint8_t)(sizeof(k_boot) / sizeof(k_boot[0]))},
|
||||
{"Shutdown", k_shutdown, (uint8_t)(sizeof(k_shutdown) / sizeof(k_shutdown[0]))},
|
||||
{"Alarm", k_alarm, (uint8_t)(sizeof(k_alarm) / sizeof(k_alarm[0]))},
|
||||
{"Connect", k_connect, (uint8_t)(sizeof(k_connect) / sizeof(k_connect[0]))},
|
||||
{"Disconnect", k_disconnect,
|
||||
(uint8_t)(sizeof(k_disconnect) / sizeof(k_disconnect[0]))},
|
||||
{"LowBat", k_lowbat, (uint8_t)(sizeof(k_lowbat) / sizeof(k_lowbat[0]))},
|
||||
{"Chime", k_chime, (uint8_t)(sizeof(k_chime) / sizeof(k_chime[0]))},
|
||||
};
|
||||
|
||||
static constexpr uint8_t k_sound_count =
|
||||
(uint8_t)(sizeof(k_sounds) / sizeof(k_sounds[0]));
|
||||
|
||||
static const SoundDef *g_active = nullptr;
|
||||
|
||||
static uint32_t volume_duty() {
|
||||
uint32_t duty = (127u * g_volume) / 100u;
|
||||
if (duty < 1 && g_volume > 0) {
|
||||
duty = 1;
|
||||
}
|
||||
return duty;
|
||||
}
|
||||
|
||||
static void speaker_off_pin() {
|
||||
ledcWrite(k_ledc_ch, 0);
|
||||
@@ -43,22 +101,51 @@ static void speaker_tone(uint16_t freq) {
|
||||
}
|
||||
ledcSetup(k_ledc_ch, freq, k_ledc_res);
|
||||
ledcAttachPin(SPEAKER_PIN, k_ledc_ch);
|
||||
// 方波占空比随音量;20% → duty≈25/255
|
||||
uint32_t duty = (127u * g_volume) / 100u;
|
||||
if (duty < 1) {
|
||||
duty = 1;
|
||||
ledcWrite(k_ledc_ch, volume_duty());
|
||||
}
|
||||
|
||||
static void stop_beep_only() {
|
||||
g_beep_playing = false;
|
||||
g_active = nullptr;
|
||||
g_note_i = 0;
|
||||
g_phase = kStart;
|
||||
speaker_off_pin();
|
||||
}
|
||||
|
||||
uint8_t sound_count() {
|
||||
return k_sound_count;
|
||||
}
|
||||
|
||||
const char *sound_name(uint8_t idx) {
|
||||
if (idx >= k_sound_count) {
|
||||
return "-";
|
||||
}
|
||||
ledcWrite(k_ledc_ch, duty);
|
||||
return k_sounds[idx].name;
|
||||
}
|
||||
|
||||
int8_t sound_selected() {
|
||||
return g_selected;
|
||||
}
|
||||
|
||||
void sound_set_selected(int8_t idx) {
|
||||
if (idx < 0 || idx >= (int8_t)k_sound_count) {
|
||||
g_selected = -1;
|
||||
return;
|
||||
}
|
||||
g_selected = idx;
|
||||
}
|
||||
|
||||
void audio_init() {
|
||||
pinMode(SPEAKER_PIN, OUTPUT);
|
||||
digitalWrite(SPEAKER_PIN, LOW);
|
||||
g_enabled = false;
|
||||
g_playing = false;
|
||||
g_volume = 20;
|
||||
g_beep_playing = false;
|
||||
g_volume = 40;
|
||||
g_selected = 0;
|
||||
g_status = "Speaker OFF";
|
||||
Serial.printf("Speaker GPIO%d vol=%u%%\n", SPEAKER_PIN, (unsigned)g_volume);
|
||||
wav_player_init();
|
||||
Serial.printf("Speaker GPIO%d vol=%u%% beeps=%u\n", SPEAKER_PIN,
|
||||
(unsigned)g_volume, (unsigned)k_sound_count);
|
||||
}
|
||||
|
||||
void audio_set_volume(uint8_t percent) {
|
||||
@@ -66,6 +153,7 @@ void audio_set_volume(uint8_t percent) {
|
||||
percent = 100;
|
||||
}
|
||||
g_volume = percent;
|
||||
wav_player_set_volume(percent);
|
||||
Serial.printf("Volume %u%%\n", (unsigned)g_volume);
|
||||
}
|
||||
|
||||
@@ -80,8 +168,9 @@ void audio_set_enabled(bool on) {
|
||||
g_status = "Speaker OFF";
|
||||
} else {
|
||||
g_status = "Speaker ON";
|
||||
wav_player_stop();
|
||||
speaker_tone(1000);
|
||||
delay(80);
|
||||
delay(60);
|
||||
speaker_off_pin();
|
||||
}
|
||||
Serial.printf("Speaker %s\n", on ? "ON" : "OFF");
|
||||
@@ -92,35 +181,69 @@ bool audio_is_enabled() {
|
||||
}
|
||||
|
||||
void audio_stop() {
|
||||
g_playing = false;
|
||||
g_note_i = 0;
|
||||
g_phase = kStartNote;
|
||||
speaker_off_pin();
|
||||
stop_beep_only();
|
||||
wav_player_stop();
|
||||
if (g_enabled) {
|
||||
g_status = "Speaker ON";
|
||||
}
|
||||
}
|
||||
|
||||
bool audio_is_playing() {
|
||||
return g_playing;
|
||||
return g_beep_playing || wav_player_is_playing();
|
||||
}
|
||||
|
||||
void audio_play_music() {
|
||||
if (!g_enabled) {
|
||||
g_status = "Turn ON first";
|
||||
Serial.println("Audio: blocked (speaker off)");
|
||||
void audio_play_sound(uint8_t idx) {
|
||||
if (idx >= k_sound_count) {
|
||||
g_status = "Bad sound";
|
||||
return;
|
||||
}
|
||||
g_playing = true;
|
||||
if (!g_enabled) {
|
||||
g_enabled = true;
|
||||
}
|
||||
wav_player_stop();
|
||||
sound_set_selected((int8_t)idx);
|
||||
g_active = &k_sounds[idx];
|
||||
g_note_i = 0;
|
||||
g_phase = kStartNote;
|
||||
g_phase = kStart;
|
||||
g_until_ms = 0;
|
||||
g_status = "Playing...";
|
||||
Serial.println("Audio: play music");
|
||||
g_beep_playing = true;
|
||||
g_status = g_active->name;
|
||||
Serial.printf("Beep: %s\n", g_active->name);
|
||||
}
|
||||
|
||||
void audio_play_wav_index(uint8_t idx) {
|
||||
const WavTrack *t = wav_list_get(idx);
|
||||
if (!t) {
|
||||
g_status = "No wav";
|
||||
return;
|
||||
}
|
||||
if (!g_enabled) {
|
||||
g_enabled = true;
|
||||
}
|
||||
stop_beep_only();
|
||||
wav_list_set_selected((int8_t)idx);
|
||||
if (wav_player_play_path(t->path)) {
|
||||
g_status = wav_player_status();
|
||||
} else {
|
||||
g_status = wav_player_status();
|
||||
}
|
||||
}
|
||||
|
||||
void audio_play_selected() {
|
||||
if (g_selected < 0) {
|
||||
g_status = "No sound";
|
||||
return;
|
||||
}
|
||||
audio_play_sound((uint8_t)g_selected);
|
||||
}
|
||||
|
||||
void audio_tick() {
|
||||
if (!g_playing || !g_enabled) {
|
||||
if (wav_player_is_playing()) {
|
||||
g_status = wav_player_status();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!g_beep_playing || !g_active || !g_enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -129,31 +252,33 @@ void audio_tick() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_phase == kWaitNote) {
|
||||
if (g_phase == kWaitTone) {
|
||||
speaker_off_pin();
|
||||
g_phase = kWaitGap;
|
||||
g_until_ms = now + 35;
|
||||
g_until_ms = now + 12;
|
||||
g_note_i++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_phase == kWaitGap) {
|
||||
g_phase = kStartNote;
|
||||
g_phase = kStart;
|
||||
}
|
||||
|
||||
if (g_note_i >= k_music_len) {
|
||||
audio_stop();
|
||||
g_status = "Music done";
|
||||
Serial.println("Audio: music done");
|
||||
if (g_note_i >= g_active->count) {
|
||||
stop_beep_only();
|
||||
g_status = g_enabled ? "Done" : "Speaker OFF";
|
||||
return;
|
||||
}
|
||||
|
||||
const Note &n = k_music[g_note_i];
|
||||
const ToneNote &n = g_active->notes[g_note_i];
|
||||
speaker_tone(n.freq);
|
||||
g_until_ms = now + n.ms;
|
||||
g_phase = kWaitNote;
|
||||
g_phase = kWaitTone;
|
||||
}
|
||||
|
||||
const char *audio_last_status() {
|
||||
if (wav_player_is_playing()) {
|
||||
return wav_player_status();
|
||||
}
|
||||
return g_status;
|
||||
}
|
||||
|
||||
30
src/main.cpp
30
src/main.cpp
@@ -4,8 +4,10 @@
|
||||
#include "audio_test.h"
|
||||
#include "pins.h"
|
||||
#include "sd_info.h"
|
||||
#include "sd_put.h"
|
||||
#include "touch.h"
|
||||
#include "ui.h"
|
||||
#include "wav_list.h"
|
||||
|
||||
static constexpr uint32_t DRAW_BUF_SIZE =
|
||||
(SCREEN_WIDTH * SCREEN_HEIGHT / 10 * (LV_COLOR_DEPTH / 8));
|
||||
@@ -13,7 +15,7 @@ static uint32_t draw_buf[DRAW_BUF_SIZE / 4];
|
||||
|
||||
static uint32_t last_refresh_ms = 0;
|
||||
static uint32_t last_btn_ui_ms = 0;
|
||||
static constexpr uint32_t REFRESH_MS = 3000;
|
||||
static constexpr uint32_t REFRESH_MS = 4000;
|
||||
|
||||
static void refresh_sd_ui() {
|
||||
SdCardInfo info = sd_read_info();
|
||||
@@ -24,22 +26,27 @@ static void refresh_sd_ui() {
|
||||
} else {
|
||||
Serial.printf("Type: %s FS: %s Cap: %s\n", info.type, info.filesystem,
|
||||
format_bytes(info.card_bytes).c_str());
|
||||
if (!audio_is_playing()) {
|
||||
wav_list_scan();
|
||||
ui_refresh_lists();
|
||||
}
|
||||
}
|
||||
|
||||
ui_update(info);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Serial.setRxBufferSize(16384);
|
||||
Serial.begin(460800);
|
||||
delay(200);
|
||||
Serial.println();
|
||||
Serial.printf("LVGL %d.%d.%d | CYD TF + Speaker\n", lv_version_major(),
|
||||
Serial.printf("LVGL %d.%d.%d | CYD Beep + WAV\n", lv_version_major(),
|
||||
lv_version_minor(), lv_version_patch());
|
||||
Serial.println("BOOT_OK");
|
||||
|
||||
pinMode(TFT_BL_PIN, OUTPUT);
|
||||
digitalWrite(TFT_BL_PIN, HIGH);
|
||||
|
||||
// 先触摸再 SD:触控 SoftSPI 与 SD VSPI 分脚,映射勿改 touch.cpp
|
||||
touch_init();
|
||||
audio_init();
|
||||
|
||||
@@ -66,20 +73,31 @@ void setup() {
|
||||
}
|
||||
|
||||
void loop() {
|
||||
const bool playing = audio_is_playing();
|
||||
|
||||
if (!playing && sd_put_poll()) {
|
||||
refresh_sd_ui();
|
||||
}
|
||||
|
||||
lv_timer_handler();
|
||||
lv_tick_inc(5);
|
||||
audio_tick();
|
||||
|
||||
const uint32_t now = millis();
|
||||
if (now - last_refresh_ms >= REFRESH_MS) {
|
||||
if (!playing && now - last_refresh_ms >= REFRESH_MS) {
|
||||
last_refresh_ms = now;
|
||||
refresh_sd_ui();
|
||||
}
|
||||
|
||||
if (now - last_btn_ui_ms >= 200) {
|
||||
last_btn_ui_ms = now;
|
||||
static bool was_playing = false;
|
||||
ui_refresh_audio_buttons();
|
||||
if (playing != was_playing) {
|
||||
was_playing = playing;
|
||||
ui_refresh_lists();
|
||||
}
|
||||
}
|
||||
|
||||
delay(5);
|
||||
delay(playing ? 2 : 5);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ static const char *card_type_name(uint8_t t) {
|
||||
static const char *detect_filesystem() {
|
||||
File root = SD.open("/");
|
||||
if (!root) {
|
||||
return "—";
|
||||
return "-";
|
||||
}
|
||||
root.close();
|
||||
|
||||
@@ -86,7 +86,7 @@ SdCardInfo sd_read_info() {
|
||||
info.mounted = false;
|
||||
info.present = false;
|
||||
info.type = "NONE";
|
||||
info.filesystem = "—";
|
||||
info.filesystem = "-";
|
||||
info.card_bytes = 0;
|
||||
info.total_bytes = 0;
|
||||
info.used_bytes = 0;
|
||||
|
||||
121
src/sd_put.cpp
Normal file
121
src/sd_put.cpp
Normal file
@@ -0,0 +1,121 @@
|
||||
#include "sd_put.h"
|
||||
#include "sd_info.h"
|
||||
|
||||
#include <SD.h>
|
||||
|
||||
static String read_line_timeout(uint32_t timeout_ms) {
|
||||
String line;
|
||||
const uint32_t start = millis();
|
||||
while (millis() - start < timeout_ms) {
|
||||
while (Serial.available()) {
|
||||
const char c = (char)Serial.read();
|
||||
if (c == '\n' || c == '\r') {
|
||||
if (line.length()) {
|
||||
return line;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
line += c;
|
||||
if (line.length() > 200) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
delay(1);
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
bool sd_put_poll() {
|
||||
if (!Serial.available()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 只在行首是 #PUT 时进入,避免吃掉普通日志
|
||||
if (Serial.peek() != '#') {
|
||||
// 丢弃非协议噪声到换行,防止堵死
|
||||
while (Serial.available() && Serial.peek() != '#') {
|
||||
const int c = Serial.read();
|
||||
if (c == '\n' || c == '\r') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const String line = read_line_timeout(500);
|
||||
if (!line.startsWith("#PUT ")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// #PUT /paomo.mp3 4143009
|
||||
int sp1 = line.indexOf(' ', 5);
|
||||
if (sp1 < 0) {
|
||||
Serial.println("PUT_ERR bad args");
|
||||
return true;
|
||||
}
|
||||
const String path = line.substring(5, sp1);
|
||||
const uint32_t size = (uint32_t)line.substring(sp1 + 1).toInt();
|
||||
if (path.length() < 2 || path[0] != '/' || size == 0 || size > 64UL * 1024UL * 1024UL) {
|
||||
Serial.println("PUT_ERR bad path/size");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!sd_init()) {
|
||||
Serial.println("PUT_ERR sd");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (SD.exists(path.c_str())) {
|
||||
SD.remove(path.c_str());
|
||||
}
|
||||
|
||||
File f = SD.open(path.c_str(), FILE_WRITE);
|
||||
if (!f) {
|
||||
Serial.println("PUT_ERR open");
|
||||
return true;
|
||||
}
|
||||
|
||||
Serial.println("READY");
|
||||
Serial.flush();
|
||||
|
||||
// 窗口协议:每收满一块回 ACK,避免 RX 溢出
|
||||
constexpr size_t k_win = 4096;
|
||||
uint8_t buf[k_win];
|
||||
uint32_t got = 0;
|
||||
while (got < size) {
|
||||
const uint32_t need = size - got;
|
||||
const size_t chunk = need > k_win ? k_win : (size_t)need;
|
||||
size_t n = 0;
|
||||
const uint32_t wait_start = millis();
|
||||
while (n < chunk && millis() - wait_start < 30000) {
|
||||
if (!Serial.available()) {
|
||||
delay(1);
|
||||
continue;
|
||||
}
|
||||
Serial.setTimeout(50);
|
||||
const int r = Serial.readBytes(buf + n, chunk - n);
|
||||
if (r > 0) {
|
||||
n += (size_t)r;
|
||||
}
|
||||
}
|
||||
if (n == 0) {
|
||||
f.close();
|
||||
SD.remove(path.c_str());
|
||||
Serial.println("PUT_ERR timeout");
|
||||
return true;
|
||||
}
|
||||
if (f.write(buf, n) != n) {
|
||||
f.close();
|
||||
SD.remove(path.c_str());
|
||||
Serial.println("PUT_ERR write");
|
||||
return true;
|
||||
}
|
||||
got += n;
|
||||
Serial.printf("ACK %u\n", (unsigned)got);
|
||||
Serial.flush();
|
||||
}
|
||||
f.flush();
|
||||
f.close();
|
||||
Serial.println("DONE");
|
||||
return true;
|
||||
}
|
||||
322
src/ui.cpp
322
src/ui.cpp
@@ -1,11 +1,12 @@
|
||||
#include "ui.h"
|
||||
#include "audio_test.h"
|
||||
#include "pins.h"
|
||||
#include "wav_list.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <lvgl.h>
|
||||
#include <string.h>
|
||||
|
||||
// 紧凑横屏仪表:顶栏状态 → 容量主信息 → 底栏操作(触控区保持大)
|
||||
static constexpr uint32_t COL_BG = 0x0A1016;
|
||||
static constexpr uint32_t COL_INK = 0xEAF0F6;
|
||||
static constexpr uint32_t COL_MUTED = 0x7A8B9C;
|
||||
@@ -17,17 +18,21 @@ static constexpr uint32_t COL_BTN_OFF = 0x243140;
|
||||
static constexpr uint32_t COL_BTN_ON = 0x1B5E4A;
|
||||
static constexpr uint32_t COL_BTN_PLAY = 0x1A4A6E;
|
||||
static constexpr uint32_t COL_BTN_STOP = 0x8B2E2E;
|
||||
static constexpr uint32_t COL_ROW = 0x15202C;
|
||||
static constexpr uint32_t COL_ROW_SEL = 0x1E3A4A;
|
||||
|
||||
static lv_obj_t *lbl_title;
|
||||
static lv_obj_t *lbl_chip;
|
||||
static lv_obj_t *lbl_capacity;
|
||||
static lv_obj_t *lbl_meta;
|
||||
static lv_obj_t *lbl_audio;
|
||||
static lv_obj_t *bar_usage;
|
||||
static lv_obj_t *lbl_vol;
|
||||
static lv_obj_t *slider_vol;
|
||||
static lv_obj_t *tabview;
|
||||
static lv_obj_t *list_beep;
|
||||
static lv_obj_t *list_wav;
|
||||
static lv_obj_t *btn_speaker;
|
||||
static lv_obj_t *btn_play;
|
||||
static lv_obj_t *lbl_speaker;
|
||||
static lv_obj_t *lbl_play;
|
||||
static uint8_t g_tab = 0;
|
||||
|
||||
static void style_btn(lv_obj_t *btn, uint32_t bg) {
|
||||
lv_obj_set_style_radius(btn, 10, 0);
|
||||
@@ -38,6 +43,29 @@ static void style_btn(lv_obj_t *btn, uint32_t bg) {
|
||||
lv_obj_set_style_pad_all(btn, 0, 0);
|
||||
}
|
||||
|
||||
static void sync_vol_label() {
|
||||
if (!lbl_vol) {
|
||||
return;
|
||||
}
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "%u%%", (unsigned)audio_get_volume());
|
||||
lv_label_set_text(lbl_vol, buf);
|
||||
}
|
||||
|
||||
static void style_row(lv_obj_t *row, bool selected, bool playing) {
|
||||
uint32_t bg = selected ? COL_ROW_SEL : COL_ROW;
|
||||
if (playing) {
|
||||
bg = COL_BTN_PLAY;
|
||||
}
|
||||
lv_obj_set_style_bg_color(row, lv_color_hex(bg), 0);
|
||||
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_radius(row, 8, 0);
|
||||
lv_obj_set_style_border_width(row, 0, 0);
|
||||
lv_obj_set_style_shadow_width(row, 0, 0);
|
||||
lv_obj_set_style_pad_left(row, 10, 0);
|
||||
lv_obj_set_style_pad_right(row, 10, 0);
|
||||
}
|
||||
|
||||
static void on_speaker_clicked(lv_event_t *e) {
|
||||
LV_UNUSED(e);
|
||||
audio_set_enabled(!audio_is_enabled());
|
||||
@@ -48,33 +76,159 @@ static void on_play_clicked(lv_event_t *e) {
|
||||
LV_UNUSED(e);
|
||||
if (audio_is_playing()) {
|
||||
audio_stop();
|
||||
} else if (g_tab == 1) {
|
||||
const int8_t sel = wav_list_selected();
|
||||
if (sel >= 0) {
|
||||
audio_play_wav_index((uint8_t)sel);
|
||||
}
|
||||
} else {
|
||||
audio_play_music();
|
||||
audio_play_selected();
|
||||
}
|
||||
ui_refresh_audio_buttons();
|
||||
ui_refresh_lists();
|
||||
}
|
||||
|
||||
static void on_vol_changed(lv_event_t *e) {
|
||||
lv_obj_t *slider = (lv_obj_t *)lv_event_get_target(e);
|
||||
audio_set_volume((uint8_t)lv_slider_get_value(slider));
|
||||
sync_vol_label();
|
||||
}
|
||||
|
||||
static void on_beep_clicked(lv_event_t *e) {
|
||||
const int idx = (int)(intptr_t)lv_event_get_user_data(e);
|
||||
if (idx < 0 || idx >= (int)sound_count()) {
|
||||
return;
|
||||
}
|
||||
audio_play_sound((uint8_t)idx);
|
||||
ui_refresh_audio_buttons();
|
||||
ui_refresh_lists();
|
||||
}
|
||||
|
||||
static void on_wav_clicked(lv_event_t *e) {
|
||||
const int idx = (int)(intptr_t)lv_event_get_user_data(e);
|
||||
if (idx < 0 || idx >= (int)wav_list_count()) {
|
||||
return;
|
||||
}
|
||||
audio_play_wav_index((uint8_t)idx);
|
||||
ui_refresh_audio_buttons();
|
||||
ui_refresh_lists();
|
||||
}
|
||||
|
||||
static void on_tab_changed(lv_event_t *e) {
|
||||
lv_obj_t *tv = (lv_obj_t *)lv_event_get_target(e);
|
||||
g_tab = (uint8_t)lv_tabview_get_tab_active(tv);
|
||||
ui_refresh_lists();
|
||||
}
|
||||
|
||||
static void fill_list_beep() {
|
||||
if (!list_beep) {
|
||||
return;
|
||||
}
|
||||
lv_obj_clean(list_beep);
|
||||
const uint8_t n = sound_count();
|
||||
const int8_t sel = sound_selected();
|
||||
const bool playing = audio_is_playing() && g_tab == 0;
|
||||
|
||||
for (uint8_t i = 0; i < n; i++) {
|
||||
lv_obj_t *row = lv_button_create(list_beep);
|
||||
lv_obj_set_width(row, lv_pct(100));
|
||||
lv_obj_set_height(row, 32);
|
||||
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_add_event_cb(row, on_beep_clicked, LV_EVENT_CLICKED, (void *)(intptr_t)i);
|
||||
style_row(row, sel == (int8_t)i, playing && sel == (int8_t)i);
|
||||
|
||||
lv_obj_t *lbl = lv_label_create(row);
|
||||
char line[36];
|
||||
snprintf(line, sizeof(line), "%u %s", (unsigned)(i + 1), sound_name(i));
|
||||
lv_label_set_text(lbl, line);
|
||||
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_text_color(lbl, lv_color_hex(COL_INK), 0);
|
||||
lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void fill_list_wav() {
|
||||
if (!list_wav) {
|
||||
return;
|
||||
}
|
||||
lv_obj_clean(list_wav);
|
||||
const uint8_t n = wav_list_count();
|
||||
if (n == 0) {
|
||||
lv_obj_t *empty = lv_label_create(list_wav);
|
||||
lv_label_set_text(empty, "No .wav on TF\nPut in / or /wav");
|
||||
lv_obj_set_style_text_font(empty, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_text_color(empty, lv_color_hex(COL_MUTED), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const int8_t sel = wav_list_selected();
|
||||
const bool playing = audio_is_playing() && g_tab == 1;
|
||||
|
||||
for (uint8_t i = 0; i < n; i++) {
|
||||
const WavTrack *t = wav_list_get(i);
|
||||
if (!t) {
|
||||
continue;
|
||||
}
|
||||
lv_obj_t *row = lv_button_create(list_wav);
|
||||
lv_obj_set_width(row, lv_pct(100));
|
||||
lv_obj_set_height(row, 32);
|
||||
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_add_event_cb(row, on_wav_clicked, LV_EVENT_CLICKED, (void *)(intptr_t)i);
|
||||
style_row(row, sel == (int8_t)i, playing && sel == (int8_t)i);
|
||||
|
||||
lv_obj_t *lbl = lv_label_create(row);
|
||||
char line[40];
|
||||
snprintf(line, sizeof(line), "%u %s", (unsigned)(i + 1), t->name);
|
||||
lv_label_set_text(lbl, line);
|
||||
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_text_color(lbl, lv_color_hex(COL_INK), 0);
|
||||
lv_label_set_long_mode(lbl, LV_LABEL_LONG_SCROLL_CIRCULAR);
|
||||
lv_obj_set_width(lbl, lv_pct(100));
|
||||
lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void ui_refresh_lists() {
|
||||
fill_list_beep();
|
||||
fill_list_wav();
|
||||
}
|
||||
|
||||
uint8_t ui_active_tab() {
|
||||
return g_tab;
|
||||
}
|
||||
|
||||
void ui_refresh_audio_buttons() {
|
||||
if (lbl_speaker) {
|
||||
char buf[24];
|
||||
snprintf(buf, sizeof(buf), "%s %u%%",
|
||||
audio_is_enabled() ? "ON" : "OFF", (unsigned)audio_get_volume());
|
||||
lv_label_set_text(lbl_speaker, buf);
|
||||
lv_label_set_text(lbl_speaker, audio_is_enabled() ? "ON" : "OFF");
|
||||
}
|
||||
if (btn_speaker) {
|
||||
style_btn(btn_speaker, audio_is_enabled() ? COL_BTN_ON : COL_BTN_OFF);
|
||||
}
|
||||
|
||||
if (lbl_play) {
|
||||
lv_label_set_text(lbl_play, audio_is_playing() ? "STOP" : "PLAY");
|
||||
}
|
||||
if (btn_play) {
|
||||
style_btn(btn_play, audio_is_playing() ? COL_BTN_STOP : COL_BTN_PLAY);
|
||||
}
|
||||
|
||||
if (lbl_audio) {
|
||||
lv_label_set_text(lbl_audio, audio_last_status());
|
||||
}
|
||||
if (slider_vol && (int32_t)audio_get_volume() != lv_slider_get_value(slider_vol)) {
|
||||
lv_slider_set_value(slider_vol, audio_get_volume(), LV_ANIM_OFF);
|
||||
}
|
||||
sync_vol_label();
|
||||
}
|
||||
|
||||
static void style_scroll_list(lv_obj_t *list) {
|
||||
lv_obj_set_size(list, lv_pct(100), lv_pct(100));
|
||||
lv_obj_set_style_bg_opa(list, LV_OPA_TRANSP, 0);
|
||||
lv_obj_set_style_border_width(list, 0, 0);
|
||||
lv_obj_set_style_pad_all(list, 4, 0);
|
||||
lv_obj_set_style_pad_row(list, 4, 0);
|
||||
lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_scroll_dir(list, LV_DIR_VER);
|
||||
lv_obj_add_flag(list, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_clear_flag(list, LV_OBJ_FLAG_CLICKABLE);
|
||||
}
|
||||
|
||||
void ui_create() {
|
||||
@@ -83,72 +237,78 @@ void ui_create() {
|
||||
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
|
||||
lv_obj_clear_flag(scr, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
// —— 顶栏 ——
|
||||
lbl_title = lv_label_create(scr);
|
||||
lv_obj_t *lbl_title = lv_label_create(scr);
|
||||
lv_label_set_text(lbl_title, "CYD");
|
||||
lv_obj_set_style_text_font(lbl_title, &lv_font_montserrat_20, 0);
|
||||
lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_ACCENT), 0);
|
||||
lv_obj_set_pos(lbl_title, 14, 10);
|
||||
lv_obj_set_pos(lbl_title, 12, 4);
|
||||
|
||||
lbl_chip = lv_label_create(scr);
|
||||
lv_label_set_text(lbl_chip, "—");
|
||||
lv_label_set_text(lbl_chip, "-");
|
||||
lv_obj_set_style_text_font(lbl_chip, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_text_color(lbl_chip, lv_color_hex(COL_MUTED), 0);
|
||||
lv_obj_align(lbl_chip, LV_ALIGN_TOP_RIGHT, -14, 14);
|
||||
lv_obj_align(lbl_chip, LV_ALIGN_TOP_RIGHT, -12, 8);
|
||||
|
||||
lv_obj_t *rule = lv_obj_create(scr);
|
||||
lv_obj_set_size(rule, UI_HOR_RES - 28, 1);
|
||||
lv_obj_set_pos(rule, 14, 40);
|
||||
lv_obj_set_style_bg_color(rule, lv_color_hex(COL_LINE), 0);
|
||||
lv_obj_set_style_bg_opa(rule, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(rule, 0, 0);
|
||||
lv_obj_set_style_radius(rule, 0, 0);
|
||||
lv_obj_clear_flag(rule, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_clear_flag(rule, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_t *vol_cap = lv_label_create(scr);
|
||||
lv_label_set_text(vol_cap, "VOL");
|
||||
lv_obj_set_style_text_font(vol_cap, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_text_color(vol_cap, lv_color_hex(COL_MUTED), 0);
|
||||
lv_obj_set_pos(vol_cap, 12, 30);
|
||||
|
||||
// —— 主信息:容量 ——
|
||||
lv_obj_t *cap_hint = lv_label_create(scr);
|
||||
lv_label_set_text(cap_hint, "CAPACITY");
|
||||
lv_obj_set_style_text_font(cap_hint, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_text_color(cap_hint, lv_color_hex(COL_MUTED), 0);
|
||||
lv_obj_set_pos(cap_hint, 14, 52);
|
||||
slider_vol = lv_slider_create(scr);
|
||||
lv_obj_set_size(slider_vol, UI_HOR_RES - 12 - 56 - 44, 14);
|
||||
lv_obj_set_pos(slider_vol, 52, 30);
|
||||
lv_slider_set_range(slider_vol, 0, 100);
|
||||
lv_slider_set_value(slider_vol, audio_get_volume(), LV_ANIM_OFF);
|
||||
lv_obj_set_style_bg_color(slider_vol, lv_color_hex(COL_LINE), LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_opa(slider_vol, LV_OPA_COVER, LV_PART_MAIN);
|
||||
lv_obj_set_style_radius(slider_vol, 4, LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_color(slider_vol, lv_color_hex(COL_OK), LV_PART_INDICATOR);
|
||||
lv_obj_set_style_bg_opa(slider_vol, LV_OPA_COVER, LV_PART_INDICATOR);
|
||||
lv_obj_set_style_bg_color(slider_vol, lv_color_hex(COL_INK), LV_PART_KNOB);
|
||||
lv_obj_set_style_pad_all(slider_vol, 4, LV_PART_KNOB);
|
||||
lv_obj_add_event_cb(slider_vol, on_vol_changed, LV_EVENT_VALUE_CHANGED, nullptr);
|
||||
|
||||
lbl_capacity = lv_label_create(scr);
|
||||
lv_label_set_text(lbl_capacity, "—");
|
||||
lv_obj_set_style_text_font(lbl_capacity, &lv_font_montserrat_20, 0);
|
||||
lv_obj_set_style_text_color(lbl_capacity, lv_color_hex(COL_INK), 0);
|
||||
lv_obj_set_pos(lbl_capacity, 14, 72);
|
||||
|
||||
lbl_meta = lv_label_create(scr);
|
||||
lv_label_set_text(lbl_meta, "Type — · FS —");
|
||||
lv_obj_set_style_text_font(lbl_meta, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_text_color(lbl_meta, lv_color_hex(COL_MUTED), 0);
|
||||
lv_obj_set_pos(lbl_meta, 14, 100);
|
||||
|
||||
bar_usage = lv_bar_create(scr);
|
||||
lv_obj_set_size(bar_usage, UI_HOR_RES - 28, 8);
|
||||
lv_obj_set_pos(bar_usage, 14, 124);
|
||||
lv_bar_set_range(bar_usage, 0, 100);
|
||||
lv_bar_set_value(bar_usage, 0, LV_ANIM_OFF);
|
||||
lv_obj_set_style_radius(bar_usage, 4, 0);
|
||||
lv_obj_set_style_bg_color(bar_usage, lv_color_hex(COL_LINE), 0);
|
||||
lv_obj_set_style_bg_color(bar_usage, lv_color_hex(COL_ACCENT), LV_PART_INDICATOR);
|
||||
lv_obj_set_style_radius(bar_usage, 4, LV_PART_INDICATOR);
|
||||
lbl_vol = lv_label_create(scr);
|
||||
lv_label_set_text(lbl_vol, "40%");
|
||||
lv_obj_set_style_text_font(lbl_vol, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_text_color(lbl_vol, lv_color_hex(COL_INK), 0);
|
||||
lv_obj_align(lbl_vol, LV_ALIGN_TOP_RIGHT, -12, 30);
|
||||
|
||||
lbl_audio = lv_label_create(scr);
|
||||
lv_label_set_text(lbl_audio, "Speaker OFF");
|
||||
lv_obj_set_style_text_font(lbl_audio, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_text_color(lbl_audio, lv_color_hex(COL_MUTED), 0);
|
||||
lv_obj_set_pos(lbl_audio, 14, 140);
|
||||
lv_obj_set_pos(lbl_audio, 12, 48);
|
||||
|
||||
// —— 底栏双按钮(大命中区)——
|
||||
const int btn_y = 168;
|
||||
const int btn_h = 58;
|
||||
// Tabs: Beep | WAV
|
||||
tabview = lv_tabview_create(scr);
|
||||
lv_obj_set_pos(tabview, 8, 66);
|
||||
lv_obj_set_size(tabview, UI_HOR_RES - 16, 110);
|
||||
lv_tabview_set_tab_bar_size(tabview, 28);
|
||||
lv_obj_set_style_bg_color(tabview, lv_color_hex(COL_BG), 0);
|
||||
lv_obj_add_event_cb(tabview, on_tab_changed, LV_EVENT_VALUE_CHANGED, nullptr);
|
||||
|
||||
lv_obj_t *tab_beep = lv_tabview_add_tab(tabview, "Beep");
|
||||
lv_obj_t *tab_wav = lv_tabview_add_tab(tabview, "WAV");
|
||||
lv_obj_set_style_bg_color(tab_beep, lv_color_hex(COL_BG), 0);
|
||||
lv_obj_set_style_bg_color(tab_wav, lv_color_hex(COL_BG), 0);
|
||||
lv_obj_set_style_pad_all(tab_beep, 0, 0);
|
||||
lv_obj_set_style_pad_all(tab_wav, 0, 0);
|
||||
|
||||
list_beep = lv_obj_create(tab_beep);
|
||||
style_scroll_list(list_beep);
|
||||
|
||||
list_wav = lv_obj_create(tab_wav);
|
||||
style_scroll_list(list_wav);
|
||||
|
||||
const int btn_y = 184;
|
||||
const int btn_h = 48;
|
||||
const int gap = 10;
|
||||
const int btn_w = (UI_HOR_RES - 28 - gap) / 2;
|
||||
const int btn_w = (UI_HOR_RES - 24 - gap) / 2;
|
||||
|
||||
btn_speaker = lv_button_create(scr);
|
||||
lv_obj_set_pos(btn_speaker, 14, btn_y);
|
||||
lv_obj_set_pos(btn_speaker, 12, btn_y);
|
||||
lv_obj_set_size(btn_speaker, btn_w, btn_h);
|
||||
lv_obj_add_flag(btn_speaker, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_add_event_cb(btn_speaker, on_speaker_clicked, LV_EVENT_CLICKED, nullptr);
|
||||
@@ -158,70 +318,48 @@ void ui_create() {
|
||||
lv_label_set_text(spk_cap, "SPEAKER");
|
||||
lv_obj_set_style_text_font(spk_cap, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_text_color(spk_cap, lv_color_hex(COL_MUTED), 0);
|
||||
lv_obj_align(spk_cap, LV_ALIGN_TOP_MID, 0, 8);
|
||||
lv_obj_align(spk_cap, LV_ALIGN_TOP_MID, 0, 4);
|
||||
|
||||
lbl_speaker = lv_label_create(btn_speaker);
|
||||
lv_label_set_text(lbl_speaker, "OFF 20%");
|
||||
lv_label_set_text(lbl_speaker, "OFF");
|
||||
lv_obj_set_style_text_font(lbl_speaker, &lv_font_montserrat_16, 0);
|
||||
lv_obj_set_style_text_color(lbl_speaker, lv_color_hex(COL_INK), 0);
|
||||
lv_obj_align(lbl_speaker, LV_ALIGN_BOTTOM_MID, 0, -10);
|
||||
lv_obj_align(lbl_speaker, LV_ALIGN_BOTTOM_MID, 0, -6);
|
||||
|
||||
btn_play = lv_button_create(scr);
|
||||
lv_obj_set_pos(btn_play, 14 + btn_w + gap, btn_y);
|
||||
lv_obj_set_pos(btn_play, 12 + btn_w + gap, btn_y);
|
||||
lv_obj_set_size(btn_play, btn_w, btn_h);
|
||||
lv_obj_add_flag(btn_play, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_add_event_cb(btn_play, on_play_clicked, LV_EVENT_CLICKED, nullptr);
|
||||
style_btn(btn_play, COL_BTN_PLAY);
|
||||
|
||||
lv_obj_t *play_cap = lv_label_create(btn_play);
|
||||
lv_label_set_text(play_cap, "MUSIC");
|
||||
lv_label_set_text(play_cap, "SOUND");
|
||||
lv_obj_set_style_text_font(play_cap, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_text_color(play_cap, lv_color_hex(COL_MUTED), 0);
|
||||
lv_obj_align(play_cap, LV_ALIGN_TOP_MID, 0, 8);
|
||||
lv_obj_align(play_cap, LV_ALIGN_TOP_MID, 0, 4);
|
||||
|
||||
lbl_play = lv_label_create(btn_play);
|
||||
lv_label_set_text(lbl_play, "PLAY");
|
||||
lv_obj_set_style_text_font(lbl_play, &lv_font_montserrat_16, 0);
|
||||
lv_obj_set_style_text_color(lbl_play, lv_color_hex(COL_INK), 0);
|
||||
lv_obj_align(lbl_play, LV_ALIGN_BOTTOM_MID, 0, -10);
|
||||
lv_obj_align(lbl_play, LV_ALIGN_BOTTOM_MID, 0, -6);
|
||||
|
||||
ui_refresh_audio_buttons();
|
||||
ui_refresh_lists();
|
||||
}
|
||||
|
||||
void ui_set_touch_debug(int16_t /*lx*/, int16_t /*ly*/, bool /*down*/) {
|
||||
// 触控已标定;界面不再显示调试光标
|
||||
}
|
||||
void ui_set_touch_debug(int16_t /*lx*/, int16_t /*ly*/, bool /*down*/) {}
|
||||
|
||||
void ui_update(const SdCardInfo &info) {
|
||||
if (!info.mounted || !info.present) {
|
||||
lv_label_set_text(lbl_chip, info.error.length() ? info.error.c_str() : "FAIL");
|
||||
lv_obj_set_style_text_color(lbl_chip, lv_color_hex(COL_BAD), 0);
|
||||
lv_label_set_text(lbl_capacity, "—");
|
||||
lv_label_set_text(lbl_meta, "No card");
|
||||
lv_bar_set_value(bar_usage, 0, LV_ANIM_OFF);
|
||||
} else {
|
||||
char chip[32];
|
||||
snprintf(chip, sizeof(chip), "OK · %s", info.type);
|
||||
char chip[40];
|
||||
snprintf(chip, sizeof(chip), "OK %s W%u", info.type, (unsigned)wav_list_count());
|
||||
lv_label_set_text(lbl_chip, chip);
|
||||
lv_obj_set_style_text_color(lbl_chip, lv_color_hex(COL_OK), 0);
|
||||
|
||||
String cap = format_bytes(info.card_bytes);
|
||||
lv_label_set_text(lbl_capacity, cap.c_str());
|
||||
|
||||
char meta[48];
|
||||
snprintf(meta, sizeof(meta), "%s · %s · %u files", info.type,
|
||||
info.filesystem, (unsigned)info.root_files);
|
||||
lv_label_set_text(lbl_meta, meta);
|
||||
|
||||
int pct = 0;
|
||||
if (info.total_bytes > 0) {
|
||||
pct = static_cast<int>((info.used_bytes * 100ULL) / info.total_bytes);
|
||||
if (pct > 100) {
|
||||
pct = 100;
|
||||
}
|
||||
}
|
||||
lv_bar_set_value(bar_usage, pct, LV_ANIM_ON);
|
||||
}
|
||||
|
||||
ui_refresh_audio_buttons();
|
||||
}
|
||||
|
||||
129
src/wav_list.cpp
Normal file
129
src/wav_list.cpp
Normal file
@@ -0,0 +1,129 @@
|
||||
#include "wav_list.h"
|
||||
#include "sd_info.h"
|
||||
|
||||
#include <SD.h>
|
||||
#include <string.h>
|
||||
|
||||
static WavTrack g_tracks[WAV_MAX];
|
||||
static uint8_t g_count = 0;
|
||||
static int8_t g_selected = -1;
|
||||
|
||||
static bool ends_with_wav(const char *name) {
|
||||
const size_t n = strlen(name);
|
||||
if (n < 4) {
|
||||
return false;
|
||||
}
|
||||
const char *e = name + n - 4;
|
||||
return (e[0] == '.' &&
|
||||
(e[1] == 'w' || e[1] == 'W') &&
|
||||
(e[2] == 'a' || e[2] == 'A') &&
|
||||
(e[3] == 'v' || e[3] == 'V'));
|
||||
}
|
||||
|
||||
static void make_display_name(const char *filename, char *out, size_t out_len) {
|
||||
size_t len = strlen(filename);
|
||||
if (len >= 4 && ends_with_wav(filename)) {
|
||||
len -= 4;
|
||||
}
|
||||
char tmp[WAV_NAME_MAX];
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; i < len && j + 1 < sizeof(tmp); i++) {
|
||||
const unsigned char c = (unsigned char)filename[i];
|
||||
tmp[j++] = (c >= 0x20 && c < 0x7F) ? (char)c : '_';
|
||||
}
|
||||
tmp[j] = '\0';
|
||||
if (tmp[0] == '\0') {
|
||||
snprintf(out, out_len, "wav");
|
||||
} else {
|
||||
snprintf(out, out_len, "%s", tmp);
|
||||
}
|
||||
}
|
||||
|
||||
static bool add_track(const char *dir, const char *filename, uint32_t bytes) {
|
||||
if (g_count >= WAV_MAX || !ends_with_wav(filename)) {
|
||||
return false;
|
||||
}
|
||||
WavTrack &t = g_tracks[g_count];
|
||||
if (dir && dir[0] && !(dir[0] == '/' && dir[1] == '\0')) {
|
||||
snprintf(t.path, sizeof(t.path), "%s/%s", dir, filename);
|
||||
} else {
|
||||
snprintf(t.path, sizeof(t.path), "/%s", filename);
|
||||
}
|
||||
make_display_name(filename, t.name, sizeof(t.name));
|
||||
t.bytes = bytes;
|
||||
g_count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void scan_dir(const char *dir) {
|
||||
File root = SD.open(dir);
|
||||
if (!root || !root.isDirectory()) {
|
||||
if (root) {
|
||||
root.close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
File f = root.openNextFile();
|
||||
while (f && g_count < WAV_MAX) {
|
||||
if (!f.isDirectory()) {
|
||||
const char *name = f.name();
|
||||
const char *base = strrchr(name, '/');
|
||||
base = base ? base + 1 : name;
|
||||
add_track(dir, base, (uint32_t)f.size());
|
||||
}
|
||||
f = root.openNextFile();
|
||||
}
|
||||
root.close();
|
||||
}
|
||||
|
||||
bool wav_list_scan() {
|
||||
g_count = 0;
|
||||
g_selected = -1;
|
||||
if (!sd_init()) {
|
||||
return false;
|
||||
}
|
||||
scan_dir("/");
|
||||
scan_dir("/wav");
|
||||
scan_dir("/WAV");
|
||||
scan_dir("/sounds");
|
||||
scan_dir("/Sounds");
|
||||
if (g_count > 0) {
|
||||
g_selected = 0;
|
||||
}
|
||||
Serial.printf("WAV list: %u\n", (unsigned)g_count);
|
||||
for (uint8_t i = 0; i < g_count; i++) {
|
||||
Serial.printf(" [%u] %s %uB\n", (unsigned)i, g_tracks[i].path,
|
||||
(unsigned)g_tracks[i].bytes);
|
||||
}
|
||||
return g_count > 0;
|
||||
}
|
||||
|
||||
uint8_t wav_list_count() {
|
||||
return g_count;
|
||||
}
|
||||
|
||||
int8_t wav_list_selected() {
|
||||
return g_selected;
|
||||
}
|
||||
|
||||
void wav_list_set_selected(int8_t idx) {
|
||||
if (idx < 0 || idx >= (int8_t)g_count) {
|
||||
g_selected = -1;
|
||||
return;
|
||||
}
|
||||
g_selected = idx;
|
||||
}
|
||||
|
||||
const WavTrack *wav_list_get(uint8_t idx) {
|
||||
if (idx >= g_count) {
|
||||
return nullptr;
|
||||
}
|
||||
return &g_tracks[idx];
|
||||
}
|
||||
|
||||
const char *wav_list_current_path() {
|
||||
if (g_selected < 0 || g_selected >= (int8_t)g_count) {
|
||||
return nullptr;
|
||||
}
|
||||
return g_tracks[g_selected].path;
|
||||
}
|
||||
365
src/wav_player.cpp
Normal file
365
src/wav_player.cpp
Normal file
@@ -0,0 +1,365 @@
|
||||
#include "wav_player.h"
|
||||
#include "pins.h"
|
||||
#include "sd_info.h"
|
||||
|
||||
#include <SD.h>
|
||||
#include <driver/dac.h>
|
||||
#include <esp32-hal.h>
|
||||
#include <esp_timer.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
// DAC2-only;大文件边读边降采样成 8bit mono,避免 "TooBig"
|
||||
|
||||
static constexpr uint32_t k_max_pcm = 120 * 1024; // ~7.5s @16k
|
||||
static constexpr uint32_t k_max_file = 3 * 1024 * 1024;
|
||||
static constexpr uint32_t k_target_hz = 22050;
|
||||
|
||||
static const char *g_status = "idle";
|
||||
static volatile bool g_playing = false;
|
||||
static volatile bool g_stop_req = false;
|
||||
static uint8_t g_volume = 40;
|
||||
|
||||
static uint8_t *g_pcm = nullptr;
|
||||
static uint32_t g_pcm_len = 0;
|
||||
static uint32_t g_rate = 16000;
|
||||
|
||||
static TaskHandle_t g_task = nullptr;
|
||||
|
||||
static void release_dac_pins() {
|
||||
dac_output_disable(DAC_CHANNEL_1);
|
||||
dac_output_disable(DAC_CHANNEL_2);
|
||||
pinMode(SPEAKER_PIN, OUTPUT);
|
||||
digitalWrite(SPEAKER_PIN, LOW);
|
||||
}
|
||||
|
||||
static void teardown_pcm() {
|
||||
if (g_pcm) {
|
||||
free(g_pcm);
|
||||
g_pcm = nullptr;
|
||||
}
|
||||
g_pcm_len = 0;
|
||||
}
|
||||
|
||||
static uint8_t scale_sample(uint8_t u8) {
|
||||
int c = (int)u8 - 128;
|
||||
c = (c * (int)g_volume) / 100;
|
||||
c = (c * 45) / 100;
|
||||
int o = 128 + c;
|
||||
if (o < 0) {
|
||||
o = 0;
|
||||
}
|
||||
if (o > 255) {
|
||||
o = 255;
|
||||
}
|
||||
return (uint8_t)o;
|
||||
}
|
||||
|
||||
static bool read_u16(File &f, uint16_t &v) {
|
||||
uint8_t b[2];
|
||||
if (f.read(b, 2) != 2) {
|
||||
return false;
|
||||
}
|
||||
v = (uint16_t)b[0] | ((uint16_t)b[1] << 8);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool read_u32(File &f, uint32_t &v) {
|
||||
uint8_t b[4];
|
||||
if (f.read(b, 4) != 4) {
|
||||
return false;
|
||||
}
|
||||
v = (uint32_t)b[0] | ((uint32_t)b[1] << 8) | ((uint32_t)b[2] << 16) |
|
||||
((uint32_t)b[3] << 24);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool read_frame_s16(File &f, uint16_t channels, uint16_t bits, int32_t &mono) {
|
||||
int32_t mix = 0;
|
||||
for (uint16_t c = 0; c < channels; c++) {
|
||||
if (bits == 8) {
|
||||
const int b = f.read();
|
||||
if (b < 0) {
|
||||
return false;
|
||||
}
|
||||
mix += (b - 128) * 256;
|
||||
} else {
|
||||
uint16_t s16;
|
||||
if (!read_u16(f, s16)) {
|
||||
return false;
|
||||
}
|
||||
mix += (int16_t)s16;
|
||||
}
|
||||
}
|
||||
if (channels == 2) {
|
||||
mix /= 2;
|
||||
}
|
||||
mono = mix;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool load_wav_pcm(const char *path) {
|
||||
teardown_pcm();
|
||||
|
||||
File f = SD.open(path, FILE_READ);
|
||||
if (!f) {
|
||||
g_status = "OpenFail";
|
||||
return false;
|
||||
}
|
||||
const uint32_t fsz = f.size();
|
||||
if (fsz > k_max_file) {
|
||||
f.close();
|
||||
g_status = "TooBig";
|
||||
Serial.printf("WAV reject size=%u\n", (unsigned)fsz);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t tag = 0;
|
||||
if (!read_u32(f, tag) || tag != 0x46464952) {
|
||||
f.close();
|
||||
g_status = "NotRIFF";
|
||||
return false;
|
||||
}
|
||||
uint32_t riff_size = 0;
|
||||
read_u32(f, riff_size);
|
||||
(void)riff_size;
|
||||
if (!read_u32(f, tag) || tag != 0x45564157) {
|
||||
f.close();
|
||||
g_status = "NotWAVE";
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t audio_fmt = 0, channels = 0, bits = 0;
|
||||
uint32_t sample_rate = 0;
|
||||
bool got_fmt = false;
|
||||
uint32_t data_size = 0;
|
||||
uint32_t data_pos = 0;
|
||||
bool got_data = false;
|
||||
|
||||
while (f.available()) {
|
||||
if (!read_u32(f, tag)) {
|
||||
break;
|
||||
}
|
||||
uint32_t chunk_size = 0;
|
||||
if (!read_u32(f, chunk_size)) {
|
||||
break;
|
||||
}
|
||||
if (tag == 0x20746d66) { // fmt
|
||||
uint16_t block_align = 0;
|
||||
uint32_t byte_rate = 0;
|
||||
if (!read_u16(f, audio_fmt) || !read_u16(f, channels) ||
|
||||
!read_u32(f, sample_rate) || !read_u32(f, byte_rate) ||
|
||||
!read_u16(f, block_align) || !read_u16(f, bits)) {
|
||||
f.close();
|
||||
g_status = "BadFmt";
|
||||
return false;
|
||||
}
|
||||
(void)byte_rate;
|
||||
(void)block_align;
|
||||
if (chunk_size > 16) {
|
||||
f.seek(f.position() + (chunk_size - 16));
|
||||
}
|
||||
// WAVE_FORMAT_EXTENSIBLE(0xFFFE) 也常见,当 PCM 用若 bits 合法
|
||||
got_fmt = true;
|
||||
} else if (tag == 0x61746164) { // data
|
||||
data_pos = f.position();
|
||||
data_size = chunk_size;
|
||||
got_data = true;
|
||||
break;
|
||||
} else {
|
||||
f.seek(f.position() + chunk_size + (chunk_size & 1));
|
||||
}
|
||||
}
|
||||
|
||||
// 1=PCM, 0xFFFE=extensible(很多导出工具)
|
||||
if (!got_fmt || !got_data || (audio_fmt != 1 && audio_fmt != 0xFFFE)) {
|
||||
f.close();
|
||||
g_status = "NeedPCM";
|
||||
Serial.printf("WAV fmt=%u\n", (unsigned)audio_fmt);
|
||||
return false;
|
||||
}
|
||||
if (channels < 1 || channels > 2 || (bits != 8 && bits != 16)) {
|
||||
f.close();
|
||||
g_status = "BadBits";
|
||||
return false;
|
||||
}
|
||||
if (sample_rate < 4000 || sample_rate > 96000) {
|
||||
f.close();
|
||||
g_status = "BadRate";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 降采样到 <= 22.05k,省 RAM、也更好定时
|
||||
uint32_t out_rate = sample_rate;
|
||||
uint32_t decim = 1;
|
||||
while (out_rate > k_target_hz) {
|
||||
out_rate /= 2;
|
||||
decim *= 2;
|
||||
}
|
||||
if (decim < 1) {
|
||||
decim = 1;
|
||||
}
|
||||
|
||||
const uint32_t bytes_per_frame = (bits / 8u) * channels;
|
||||
const uint32_t src_frames = data_size / bytes_per_frame;
|
||||
uint32_t out_frames = src_frames / decim;
|
||||
if (out_frames > k_max_pcm) {
|
||||
out_frames = k_max_pcm;
|
||||
}
|
||||
if (out_frames == 0) {
|
||||
f.close();
|
||||
g_status = "Empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
g_pcm = (uint8_t *)malloc(out_frames);
|
||||
if (!g_pcm) {
|
||||
f.close();
|
||||
g_status = "OOM";
|
||||
return false;
|
||||
}
|
||||
|
||||
f.seek(data_pos);
|
||||
uint32_t got = 0;
|
||||
for (uint32_t oi = 0; oi < out_frames; oi++) {
|
||||
int64_t acc = 0;
|
||||
uint32_t n = 0;
|
||||
for (uint32_t d = 0; d < decim; d++) {
|
||||
int32_t mono = 0;
|
||||
if (!read_frame_s16(f, channels, bits, mono)) {
|
||||
break;
|
||||
}
|
||||
acc += mono;
|
||||
n++;
|
||||
}
|
||||
if (n == 0) {
|
||||
break;
|
||||
}
|
||||
const int32_t mix = (int32_t)(acc / (int64_t)n);
|
||||
int v = (mix / 256) + 128;
|
||||
if (v < 0) {
|
||||
v = 0;
|
||||
}
|
||||
if (v > 255) {
|
||||
v = 255;
|
||||
}
|
||||
g_pcm[got++] = (uint8_t)v;
|
||||
}
|
||||
f.close();
|
||||
|
||||
if (got == 0) {
|
||||
teardown_pcm();
|
||||
g_status = "Empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
g_pcm_len = got;
|
||||
g_rate = out_rate;
|
||||
Serial.printf("WAV ok %uB -> %u samp @%u (decim %u)\n", (unsigned)fsz,
|
||||
(unsigned)g_pcm_len, (unsigned)g_rate, (unsigned)decim);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void wav_task(void * /*arg*/) {
|
||||
for (;;) {
|
||||
if (!g_playing || !g_pcm || g_pcm_len == 0) {
|
||||
vTaskDelay(pdMS_TO_TICKS(15));
|
||||
continue;
|
||||
}
|
||||
|
||||
g_stop_req = false;
|
||||
dac_output_disable(DAC_CHANNEL_1);
|
||||
dac_output_enable(DAC_CHANNEL_2);
|
||||
|
||||
const uint32_t period_us = 1000000UL / (g_rate ? g_rate : 16000);
|
||||
int64_t next = esp_timer_get_time();
|
||||
|
||||
for (uint32_t i = 0; i < g_pcm_len; i++) {
|
||||
if (g_stop_req) {
|
||||
break;
|
||||
}
|
||||
dac_output_voltage(DAC_CHANNEL_2, scale_sample(g_pcm[i]));
|
||||
next += (int64_t)period_us;
|
||||
const int64_t now = esp_timer_get_time();
|
||||
const int64_t wait = next - now;
|
||||
if (wait > 50) {
|
||||
ets_delay_us((uint32_t)wait);
|
||||
} else if (wait < -3000) {
|
||||
next = now;
|
||||
}
|
||||
}
|
||||
|
||||
release_dac_pins();
|
||||
g_playing = false;
|
||||
g_status = g_stop_req ? "Stop" : "Done";
|
||||
Serial.println(g_status);
|
||||
}
|
||||
}
|
||||
|
||||
void wav_player_init() {
|
||||
release_dac_pins();
|
||||
xTaskCreatePinnedToCore(wav_task, "wavdac", 4096, nullptr, 4, &g_task, 0);
|
||||
g_status = "idle";
|
||||
Serial.println("WAV: DAC2-only + downsample");
|
||||
}
|
||||
|
||||
void wav_player_set_volume(uint8_t percent) {
|
||||
if (percent > 100) {
|
||||
percent = 100;
|
||||
}
|
||||
g_volume = percent;
|
||||
}
|
||||
|
||||
void wav_player_stop() {
|
||||
if (g_playing) {
|
||||
g_stop_req = true;
|
||||
for (int i = 0; i < 40 && g_playing; i++) {
|
||||
delay(5);
|
||||
}
|
||||
}
|
||||
g_playing = false;
|
||||
g_stop_req = false;
|
||||
release_dac_pins();
|
||||
g_status = "idle";
|
||||
}
|
||||
|
||||
bool wav_player_is_playing() {
|
||||
return g_playing;
|
||||
}
|
||||
|
||||
const char *wav_player_status() {
|
||||
return g_status;
|
||||
}
|
||||
|
||||
bool wav_player_play_path(const char *path) {
|
||||
if (!path || !path[0]) {
|
||||
g_status = "NoFile";
|
||||
return false;
|
||||
}
|
||||
if (!sd_init()) {
|
||||
g_status = "SDFail";
|
||||
return false;
|
||||
}
|
||||
if (!SD.exists(path)) {
|
||||
g_status = "Missing";
|
||||
return false;
|
||||
}
|
||||
|
||||
wav_player_stop();
|
||||
ledcDetachPin(SPEAKER_PIN);
|
||||
release_dac_pins();
|
||||
|
||||
if (!load_wav_pcm(path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
g_stop_req = false;
|
||||
g_playing = true;
|
||||
g_status = "Play";
|
||||
Serial.printf("WAV play %s\n", path);
|
||||
return true;
|
||||
}
|
||||
|
||||
void wav_player_tick() {}
|
||||
150
tools/sd_put.py
Normal file
150
tools/sd_put.py
Normal file
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Upload a file to the CYD TF card over serial (#PUT + ACK window)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import serial
|
||||
from serial.tools import list_ports
|
||||
except ImportError:
|
||||
print("Need pyserial: pip install pyserial", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
WIN = 4096
|
||||
|
||||
|
||||
def pick_port(preferred: str | None) -> str:
|
||||
if preferred:
|
||||
return preferred
|
||||
ports = list(list_ports.comports())
|
||||
for p in ports:
|
||||
desc = (p.description or "") + (p.hwid or "")
|
||||
if "USB" in desc.upper() or "UART" in desc.upper() or "CP210" in desc.upper() or "CH340" in desc.upper():
|
||||
return p.device
|
||||
if ports:
|
||||
return ports[0].device
|
||||
raise SystemExit("No serial port found")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("file", type=Path, help="Local file to upload")
|
||||
ap.add_argument("--port", default=None)
|
||||
ap.add_argument("--baud", type=int, default=460800)
|
||||
ap.add_argument("--remote", default="/paomo.mp3")
|
||||
args = ap.parse_args()
|
||||
|
||||
path: Path = args.file
|
||||
if not path.is_file():
|
||||
print(f"Missing file: {path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
size = path.stat().st_size
|
||||
port = pick_port(args.port)
|
||||
print(f"Port {port} {path.name} -> {args.remote} ({size} bytes)", flush=True)
|
||||
|
||||
ser = serial.Serial()
|
||||
ser.port = port
|
||||
ser.baudrate = args.baud
|
||||
ser.timeout = 1
|
||||
ser.dtr = False
|
||||
ser.rts = False
|
||||
ser.open()
|
||||
# 硬复位 ESP32
|
||||
ser.rts = True
|
||||
time.sleep(0.1)
|
||||
ser.rts = False
|
||||
print("waiting BOOT_OK...", flush=True)
|
||||
deadline = time.time() + 12
|
||||
booted = False
|
||||
while time.time() < deadline:
|
||||
line = ser.readline().decode("utf-8", errors="ignore").strip()
|
||||
if line:
|
||||
print("<", line, flush=True)
|
||||
if "BOOT_OK" in line:
|
||||
booted = True
|
||||
break
|
||||
if not booted:
|
||||
print("warn: BOOT_OK not seen, continuing", flush=True)
|
||||
time.sleep(0.5)
|
||||
ser.reset_input_buffer()
|
||||
|
||||
cmd = f"#PUT {args.remote} {size}\n".encode("ascii")
|
||||
print(f"send {cmd!r}", flush=True)
|
||||
ser.write(cmd)
|
||||
ser.flush()
|
||||
|
||||
deadline = time.time() + 20
|
||||
while time.time() < deadline:
|
||||
line = ser.readline().decode("utf-8", errors="ignore").strip()
|
||||
if line:
|
||||
print("<", line, flush=True)
|
||||
if line == "READY":
|
||||
break
|
||||
if line.startswith("PUT_ERR"):
|
||||
return 2
|
||||
else:
|
||||
print("Timeout waiting READY", file=sys.stderr, flush=True)
|
||||
return 3
|
||||
|
||||
sent = 0
|
||||
t0 = time.time()
|
||||
with path.open("rb") as f:
|
||||
while sent < size:
|
||||
chunk = f.read(min(WIN, size - sent))
|
||||
if not chunk:
|
||||
break
|
||||
ser.write(chunk)
|
||||
ser.flush()
|
||||
sent += len(chunk)
|
||||
|
||||
# 等 ACK <sent>
|
||||
ack_deadline = time.time() + 30
|
||||
acked = False
|
||||
while time.time() < ack_deadline:
|
||||
line = ser.readline().decode("utf-8", errors="ignore").strip()
|
||||
if not line:
|
||||
continue
|
||||
print("<", line, flush=True)
|
||||
if line.startswith("PUT_ERR"):
|
||||
return 4
|
||||
if line.startswith("ACK "):
|
||||
try:
|
||||
got = int(line.split()[1])
|
||||
except (IndexError, ValueError):
|
||||
continue
|
||||
if got >= sent:
|
||||
acked = True
|
||||
break
|
||||
if line == "DONE" and sent >= size:
|
||||
print("OK", flush=True)
|
||||
return 0
|
||||
if not acked:
|
||||
print(f"Timeout waiting ACK after {sent}", file=sys.stderr, flush=True)
|
||||
return 4
|
||||
|
||||
if sent % (256 * 1024) < WIN or sent == size:
|
||||
rate = sent / max(time.time() - t0, 0.001)
|
||||
print(f"sent {sent}/{size} ({rate/1024:.1f} KB/s)", flush=True)
|
||||
|
||||
deadline = time.time() + 30
|
||||
while time.time() < deadline:
|
||||
line = ser.readline().decode("utf-8", errors="ignore").strip()
|
||||
if line:
|
||||
print("<", line, flush=True)
|
||||
if line == "DONE":
|
||||
print("OK", flush=True)
|
||||
return 0
|
||||
if line.startswith("PUT_ERR"):
|
||||
return 5
|
||||
|
||||
print("Timeout waiting DONE", file=sys.stderr, flush=True)
|
||||
return 6
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user