feat(face): tap-to-reveal volume controls

Tapping the face now brings up an overlay with a microphone button and
volume down/up, plus a live level bar showing the current output volume.
Icons are drawn from a subset of the Phosphor glyph font. Tapping the dim
scrim behind the controls dismisses them; they also auto-hide after a few
seconds of inactivity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
This commit is contained in:
2026-07-15 20:24:04 +02:00
co-authored by Claude Opus 4.8
parent ebdb1f0457
commit 23f001ceb5
6 changed files with 1540 additions and 2 deletions
+6
View File
@@ -8,6 +8,12 @@ until the first tagged release.
## [Unreleased]
### Added
- Tap the screen to reveal on-screen controls — a microphone button plus volume
down/up and a live level bar, using Phosphor icon glyphs. Tapping the dimmed
backdrop dismisses them.
### Fixed
- Fixed the blue-screen flicker during voice activity — the matrix rain now
+1
View File
@@ -10,6 +10,7 @@ idf_component_register(
"fonts/font_face_140.c"
"fonts/font_rain_22.c"
"fonts/font_rage_64.c"
"fonts/font_phosphor_64.c"
INCLUDE_DIRS "."
EMBED_FILES "c6_slave.bin"
PRIV_REQUIRES nvs_flash esp_wifi esp_event esp_netif json esp_timer esp_app_format esp_http_server esp-sr
+20 -1
View File
@@ -34,6 +34,7 @@ static const char *TAG = "audio";
static esp_codec_dev_handle_t s_spk;
static esp_codec_dev_handle_t s_mic;
static int s_vol = GONG_VOLUME; /* speaker out volume 0..100 */
static int16_t *s_gong;
static uint8_t *s_reply;
@@ -276,7 +277,7 @@ void audio_init(void)
};
s_spk = bsp_audio_codec_speaker_init();
if (s_spk != NULL) {
esp_codec_dev_set_out_vol(s_spk, GONG_VOLUME);
esp_codec_dev_set_out_vol(s_spk, s_vol);
esp_codec_dev_open(s_spk, &fs);
}
s_mic = bsp_audio_codec_microphone_init();
@@ -348,3 +349,21 @@ bool audio_is_playing(void)
{
return s_playing;
}
void audio_volume_set(int vol)
{
if (vol < 0) {
vol = 0;
} else if (vol > 100) {
vol = 100;
}
s_vol = vol;
if (s_spk != NULL) {
esp_codec_dev_set_out_vol(s_spk, s_vol);
}
}
int audio_volume_get(void)
{
return s_vol;
}
+2
View File
@@ -34,6 +34,8 @@ void audio_playback_begin(void);
void audio_playback_feed(const uint8_t *data, size_t len);
void audio_playback_end(void);
bool audio_is_playing(void);
void audio_volume_set(int vol); /* 0..100 */
int audio_volume_get(void);
/* net.c */
void net_start(void);
+131 -1
View File
@@ -16,6 +16,17 @@
LV_FONT_DECLARE(font_face_140);
LV_FONT_DECLARE(font_rain_22);
LV_FONT_DECLARE(font_phosphor_64);
/* Phosphor icon glyphs (Private Use Area, UTF-8) baked into font_phosphor_64. */
#define ICON_MIC "\xEE\x8C\xA6" /* ph-microphone U+E326 */
#define ICON_MIC_OFF "\xEE\x8C\xA8" /* ph-microphone-slash U+E328 */
#define ICON_VOL_UP "\xEE\x91\x8A" /* ph-speaker-high U+E44A */
#define ICON_VOL_DOWN "\xEE\x91\x8C" /* ph-speaker-low U+E44C */
#define ICON_VOL_MUTE "\xEE\x91\x9A" /* ph-speaker-slash U+E45A */
#define ICON_EQ "\xEE\xAE\xBC" /* ph-equalizer U+EBBC */
#define ICON_SLIDERS "\xEE\x90\xB2" /* ph-sliders U+E432 */
#define ICON_SLIDERS_H "\xEE\x90\xB4" /* ph-sliders-horizontal U+E434 */
LV_FONT_DECLARE(font_rage_64);
#define SCREEN 800
@@ -111,6 +122,10 @@ static struct {
lv_obj_t *status;
lv_obj_t *elapsed;
lv_obj_t *arc;
lv_obj_t *controls; /* tap-to-reveal control overlay (mic + volume) */
lv_obj_t *vol_bar;
lv_obj_t *vol_lbl; /* "NN%" */
lv_timer_t *ctl_timer; /* one-shot auto-hide for the controls */
stream_t streams[RAIN_MAX];
int arc_rot;
uint32_t state_since;
@@ -407,10 +422,88 @@ static void clock_cb(lv_timer_t *timer)
}
}
/* --- tap-to-reveal controls: mic + volume down/up + level bar --- */
static void controls_update_vol(void)
{
int v = audio_volume_get();
lv_bar_set_value(F.vol_bar, v, LV_ANIM_OFF);
char buf[8];
snprintf(buf, sizeof(buf), "%d%%", v);
lv_label_set_text(F.vol_lbl, buf);
}
static void controls_hide(void)
{
lv_obj_add_flag(F.controls, LV_OBJ_FLAG_HIDDEN);
lv_timer_pause(F.ctl_timer);
}
static void controls_show(void)
{
controls_update_vol();
lv_obj_clear_flag(F.controls, LV_OBJ_FLAG_HIDDEN);
face_activity(); /* wake the screen from any dim state */
lv_timer_reset(F.ctl_timer);
lv_timer_resume(F.ctl_timer);
}
static void ctl_autohide_cb(lv_timer_t *t) { (void)t; controls_hide(); }
static void mic_cb(lv_event_t *e)
{
(void)e;
controls_hide();
app_on_touch(); /* the existing tap-to-talk action */
}
static void volup_cb(lv_event_t *e)
{
(void)e;
audio_volume_set(audio_volume_get() + 10);
controls_update_vol();
lv_timer_reset(F.ctl_timer);
}
static void voldown_cb(lv_event_t *e)
{
(void)e;
audio_volume_set(audio_volume_get() - 10);
controls_update_vol();
lv_timer_reset(F.ctl_timer);
}
/* tap anywhere (the underlying catcher, reachable only while controls are hidden)
* reveals the controls; a tap on the dim scrim behind them dismisses. */
static void touch_cb(lv_event_t *e)
{
(void)e;
app_on_touch();
controls_show();
}
static void scrim_cb(lv_event_t *e)
{
(void)e;
controls_hide();
}
static lv_obj_t *icon_btn(lv_obj_t *parent, const char *icon, int diam, lv_event_cb_t cb)
{
lv_obj_t *b = lv_button_create(parent);
lv_obj_set_size(b, diam, diam);
lv_obj_set_style_radius(b, LV_RADIUS_CIRCLE, 0);
lv_obj_set_style_bg_color(b, lv_color_hex(0x0A140E), 0);
lv_obj_set_style_bg_opa(b, LV_OPA_COVER, 0);
lv_obj_set_style_border_color(b, lv_color_hex(0x00A050), 0);
lv_obj_set_style_border_width(b, 2, 0);
lv_obj_set_style_shadow_width(b, 0, 0);
lv_obj_t *lbl = lv_label_create(b);
lv_obj_set_style_text_font(lbl, &font_phosphor_64, 0);
lv_obj_set_style_text_color(lbl, lv_color_hex(0xC3FFD7), 0);
lv_label_set_text(lbl, icon);
lv_obj_center(lbl);
lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, NULL);
return b;
}
void face_init(void)
@@ -507,6 +600,43 @@ void face_init(void)
lv_obj_add_flag(touch, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(touch, touch_cb, LV_EVENT_CLICKED, NULL);
/* --- tap-to-reveal control overlay: dim scrim + mic + volume down/up + bar --- */
F.controls = lv_obj_create(scr); /* above the touch catcher */
lv_obj_remove_style_all(F.controls);
lv_obj_set_size(F.controls, SCREEN, SCREEN);
lv_obj_set_style_bg_color(F.controls, lv_color_black(), 0);
lv_obj_set_style_bg_opa(F.controls, 200, 0); /* dim the face behind */
lv_obj_add_flag(F.controls, LV_OBJ_FLAG_CLICKABLE);
lv_obj_clear_flag(F.controls, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_event_cb(F.controls, scrim_cb, LV_EVENT_CLICKED, NULL);
lv_obj_add_flag(F.controls, LV_OBJ_FLAG_HIDDEN); /* hidden until tapped */
lv_obj_t *mic = icon_btn(F.controls, ICON_MIC, 150, mic_cb);
lv_obj_align(mic, LV_ALIGN_CENTER, 0, -30);
lv_obj_t *vdn = icon_btn(F.controls, ICON_VOL_DOWN, 110, voldown_cb);
lv_obj_align(vdn, LV_ALIGN_CENTER, -230, -30);
lv_obj_t *vup = icon_btn(F.controls, ICON_VOL_UP, 110, volup_cb);
lv_obj_align(vup, LV_ALIGN_CENTER, 230, -30);
F.vol_bar = lv_bar_create(F.controls);
lv_obj_set_size(F.vol_bar, 360, 16);
lv_obj_align(F.vol_bar, LV_ALIGN_CENTER, 0, 140);
lv_bar_set_range(F.vol_bar, 0, 100);
lv_obj_set_style_bg_color(F.vol_bar, lv_color_hex(0x0A140E), LV_PART_MAIN);
lv_obj_set_style_bg_opa(F.vol_bar, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_bg_color(F.vol_bar, lv_color_hex(0x00C860), LV_PART_INDICATOR);
lv_obj_set_style_radius(F.vol_bar, 8, LV_PART_MAIN);
lv_obj_set_style_radius(F.vol_bar, 8, LV_PART_INDICATOR);
F.vol_lbl = lv_label_create(F.controls);
lv_obj_set_style_text_font(F.vol_lbl, &lv_font_montserrat_28, 0);
lv_obj_set_style_text_color(F.vol_lbl, lv_color_hex(0xC3FFD7), 0);
lv_label_set_text(F.vol_lbl, "");
lv_obj_align(F.vol_lbl, LV_ALIGN_CENTER, 0, 180);
F.ctl_timer = lv_timer_create(ctl_autohide_cb, 5000, NULL);
lv_timer_pause(F.ctl_timer);
/* Boot straight into the calm idle face (dry — rain gate holds it off until
* the gateway connects). No "CONNECTING" banner churn on every recovery. */
F.state = FACE_IDLE;
File diff suppressed because it is too large Load Diff