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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user