Add 3-pin whistle sound sensor digital H/L detect on GPIO22.

Touch+LVGL UI shows QUIET/SOUND, pin level, hit count; no dB (digital only).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-23 13:47:52 +08:00
commit b2052a7771
23 changed files with 1875 additions and 0 deletions

13
.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
.pio/
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
*.pyc
__pycache__/
.DS_Store
Thumbs.db
assets/
*.elf
# 构建目录里的 bin 不入库;firmware/ 预编译包要入库
!firmware/**/*.bin

35
README.md Normal file
View File

@@ -0,0 +1,35 @@
# 口哨 / 声音传感器(CYD)
触控 + LVGL 参考 `activeBuzzerTest` / `hw269test`。三针口哨模块 **高低电平** 接 **GPIO22**:有声触发显示、累计 hits。
## 硬件
| 项目 | 说明 |
|------|------|
| 板子 | CYD ESP32-2432S028R |
| 模块 | 三针口哨/声音传感器(黑头麦克风,OUT 高低电平) |
| 信号 | **GPIO22** |
接线:`VCC→3.3V`(或模块标注电压),`GND→GND`,`OUT / DO / S→GPIO22`。
默认按 **高电平 = 有声**。若实际相反,改 `include/pins.h` 里 `SOUND_ACTIVE_HIGH` 为 `0`。板上电位器可调灵敏度。
## 功能
- 实时显示 QUIET / SOUND!、引脚 HIGH/LOW
- 边沿计数 hits(带消抖);触控 **Reset** 清零
- **无分贝**:本模块只有数字口,GPIO22 也非 ADC,无法测 dB
## 构建 / 烧录
```bash
pio run -e cyd -t upload
```
或预编译包:
```powershell
cd firmware\cyd
.\flash.ps1
# 或 .\flash.ps1 -Port COMx
```

13
copy_lv_conf.py Normal file
View File

@@ -0,0 +1,13 @@
Import("env") # noqa: F821
from pathlib import Path
src = Path(env["PROJECT_DIR"]) / "include" / "lv_conf.h" # noqa: F821
dst_dir = Path(env["PROJECT_LIBDEPS_DIR"]) / env["PIOENV"] # noqa: F821
dst_dir.mkdir(parents=True, exist_ok=True)
dst = dst_dir / "lv_conf.h"
if src.exists():
dst.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
lvgl = dst_dir / "lvgl" / "lv_conf.h"
if (dst_dir / "lvgl").exists():
lvgl.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
print(f"copy_lv_conf: synced -> {dst}")

21
firmware/README.md Normal file
View File

@@ -0,0 +1,21 @@
# 预编译固件
目录:[`cyd/`](cyd/)
板子:**CYD ESP32-2432S028R**(ILI9341)
功能:三针口哨传感器(GPIO22 高低电平)+ SoftSPI 触控 + LVGL
## 刷写
```powershell
cd firmware\cyd
.\flash.ps1
# 或 .\flash.ps1 -Port COM3
```
## 重新打包
```powershell
.\firmware\export_from_build.ps1
.\firmware\export_from_build.ps1 -SkipBuild
```

16
firmware/cyd/VERSION.txt Normal file
View File

@@ -0,0 +1,16 @@
name: CYD Whistle Sound Sensor (cyd)
git: unknown
date: 2026-10-23
board: ESP32-2432S028R (ILI9341)
features: SoftSPI touch, LVGL, whistle/sound digital H/L on GPIO22
flash_size: 4MB
offsets:
0x1000 bootloader.bin
0x8000 partitions.bin
0xe000 boot_app0.bin
0x10000 firmware.bin
sha256:
bootloader.bin 3D234A7471F67B013686DABD4DEE7C1FA915C9928463616A94BC9297ACF1ABF8
boot_app0.bin F94C5D786A7A8FAB06AC5D10E33BF37711A6697636DC037559EA19CC410A17F0
firmware.bin 5CCE5B8CF5A31950C109E477A563FD31B1848ABE26C98D5AE70AC60450288634
partitions.bin 148B959CBFF1C38AA8E1D5C0BA9D612C54997B945E56A63F41223EEF650653A1

BIN
firmware/cyd/boot_app0.bin Normal file

Binary file not shown.

BIN
firmware/cyd/bootloader.bin Normal file

Binary file not shown.

BIN
firmware/cyd/firmware.bin Normal file

Binary file not shown.

5
firmware/cyd/flash.bat Normal file
View File

@@ -0,0 +1,5 @@
@echo off
setlocal
cd /d "%~dp0"
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0flash.ps1" %*
exit /b %ERRORLEVEL%

98
firmware/cyd/flash.ps1 Normal file
View File

@@ -0,0 +1,98 @@
# Flash archived CYD TF+Speaker firmware (no rebuild needed)
# Usage:
# .\flash.ps1
# .\flash.ps1 -Port COM5
# .\flash.ps1 -Port COM5 -Baud 460800
param(
[string]$Port = "",
[int]$Baud = 921600
)
$ErrorActionPreference = "Stop"
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
Set-Location $here
function Find-Esptool {
$pio = Get-Command pio -ErrorAction SilentlyContinue
if (-not $pio) {
$pioPath = Join-Path $env:USERPROFILE "AppData\Local\Programs\Python\Python312\Scripts\pio.exe"
if (Test-Path $pioPath) {
$env:Path = "$(Split-Path $pioPath);$env:Path"
$pio = Get-Command pio -ErrorAction SilentlyContinue
}
}
if ($pio) {
$pkg = Join-Path $env:USERPROFILE ".platformio\packages\tool-esptoolpy\esptool.py"
if (Test-Path $pkg) { return @{ Kind = "py"; Path = $pkg } }
}
$py = Get-Command python -ErrorAction SilentlyContinue
if ($py) {
try {
& python -c "import esptool" 2>$null
if ($LASTEXITCODE -eq 0) { return @{ Kind = "mod"; Path = "esptool" } }
} catch {}
}
$esp = Get-Command esptool.py -ErrorAction SilentlyContinue
if ($esp) { return @{ Kind = "py"; Path = $esp.Source } }
return $null
}
if (-not $Port) {
$ports = @()
try {
Add-Type -AssemblyName System.IO.Ports -ErrorAction SilentlyContinue | Out-Null
$ports = [System.IO.Ports.SerialPort]::GetPortNames() | Sort-Object
} catch {}
if (-not $ports -or $ports.Count -eq 0) {
$ports = Get-CimInstance Win32_SerialPort -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty DeviceID | Sort-Object
}
if (-not $ports -or $ports.Count -eq 0) {
Write-Error "No serial ports found. Plug in the CYD USB cable, then retry with -Port COMx"
}
if ($ports.Count -eq 1) {
$Port = $ports[0]
} else {
Write-Host "Available ports: $($ports -join ', ')"
$Port = $ports[-1]
Write-Host "Using last port: $Port (override with -Port)"
}
}
$tool = Find-Esptool
if (-not $tool) {
Write-Error "esptool not found. Install PlatformIO (`pio`) or: pip install esptool"
}
$needed = @("bootloader.bin", "partitions.bin", "boot_app0.bin", "firmware.bin")
foreach ($f in $needed) {
if (-not (Test-Path (Join-Path $here $f))) {
Write-Error "Missing $f — run ..\export_from_build.ps1 from repo root first"
}
}
$argsCommon = @(
"--chip", "esp32",
"--port", $Port,
"--baud", "$Baud",
"--before", "default_reset",
"--after", "hard_reset",
"write_flash", "-z",
"--flash_mode", "dio",
"--flash_freq", "40m",
"--flash_size", "4MB",
"0x1000", "bootloader.bin",
"0x8000", "partitions.bin",
"0xe000", "boot_app0.bin",
"0x10000", "firmware.bin"
)
Write-Host "Flashing $here -> $Port @ $Baud"
if ($tool.Kind -eq "mod") {
& python -m esptool @argsCommon
} else {
& python $tool.Path @argsCommon
}
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Host "Done. Open serial monitor at 115200 if needed."

7
firmware/cyd/meta.json Normal file
View File

@@ -0,0 +1,7 @@
{
"boardId": "esp32-2432s028",
"name": "CYD Whistle Sound Sensor",
"summary": "3-pin whistle/sound module on GPIO22: digital H/L detect + hit count",
"description": "SoftSPI touch + LVGL. OUT->GPIO22 high/low. Default active HIGH. No dB (digital only).",
"tags": ["sound", "whistle", "digital", "gpio22", "touch", "lvgl"]
}

BIN
firmware/cyd/partitions.bin Normal file

Binary file not shown.

View File

@@ -0,0 +1,57 @@
# Rebuild (optional) and refresh firmware/cyd from PlatformIO build output.
# Usage:
# .\firmware\export_from_build.ps1
# .\firmware\export_from_build.ps1 -SkipBuild
param([switch]$SkipBuild)
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
Set-Location $root
$env:Path = "C:\Users\dsx2016\AppData\Local\Programs\Python\Python312\Scripts;" + $env:Path
if (-not $SkipBuild) {
pio run -e cyd
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
$src = Join-Path $root ".pio\build\cyd"
$dst = Join-Path $root "firmware\cyd"
New-Item -ItemType Directory -Force -Path $dst | Out-Null
foreach ($f in @("bootloader.bin", "partitions.bin", "firmware.bin")) {
$p = Join-Path $src $f
if (-not (Test-Path $p)) { Write-Error "Missing build artifact: $p" }
Copy-Item $p $dst -Force
}
$bootApp = Join-Path $env:USERPROFILE ".platformio\packages\framework-arduinoespressif32\tools\partitions\boot_app0.bin"
if (-not (Test-Path $bootApp)) {
Write-Error "boot_app0.bin not found under PlatformIO packages"
}
Copy-Item $bootApp (Join-Path $dst "boot_app0.bin") -Force
$git = ""
try { $git = (git -C $root rev-parse --short HEAD).Trim() } catch { $git = "unknown" }
$date = Get-Date -Format "yyyy-MM-dd"
$hashes = Get-ChildItem (Join-Path $dst "*.bin") | Get-FileHash -Algorithm SHA256
$lines = @(
"name: CYD Whistle Sound Sensor (cyd)",
"git: $git",
"date: $date",
"board: ESP32-2432S028R (ILI9341)",
"features: SoftSPI touch, LVGL, whistle/sound digital H/L on GPIO22",
"flash_size: 4MB",
"offsets:",
" 0x1000 bootloader.bin",
" 0x8000 partitions.bin",
" 0xe000 boot_app0.bin",
" 0x10000 firmware.bin",
"sha256:"
)
foreach ($h in $hashes) {
$lines += (" {0,-15} {1}" -f (Split-Path $h.Path -Leaf), $h.Hash)
}
$lines | Set-Content -Encoding utf8 (Join-Path $dst "VERSION.txt")
Write-Host "Exported -> $dst (git $git)"

1127
include/lv_conf.h Normal file

File diff suppressed because it is too large Load Diff

21
include/pins.h Normal file
View File

@@ -0,0 +1,21 @@
#pragma once
// CYD ESP32-2432S028R — 触控/屏脚与 tftest 一致
#define TFT_BL_PIN 21
#define CYD_TOUCH_IRQ 36
#define CYD_TOUCH_MOSI 32
#define CYD_TOUCH_MISO 39
#define CYD_TOUCH_CLK 25
#define CYD_TOUCH_CS 33
// 三针口哨/声音传感器:OUT 高低电平(常见:有声=HIGH)
#define SOUND_PIN 22
// 1 = 高电平表示检测到声音;若模块相反改成 0
#define SOUND_ACTIVE_HIGH 1
#define SCREEN_WIDTH 240
#define SCREEN_HEIGHT 320
#define UI_HOR_RES 320
#define UI_VER_RES 240

20
include/sound.h Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include <stdint.h>
void sound_init();
void sound_poll();
/** 当前是否检测到声音(已按 SOUND_ACTIVE_HIGH 解释) */
bool sound_detected();
/** 原始引脚电平:true=HIGH */
bool sound_raw_high();
/** 累计触发次数(边沿,带消抖) */
uint32_t sound_event_count();
/** 最近一次触发后经过的 ms;未触发过为 UINT32_MAX */
uint32_t sound_ms_since_last();
void sound_reset_count();

12
include/touch.h Normal file
View File

@@ -0,0 +1,12 @@
#pragma once
#include <lvgl.h>
#include <stdint.h>
void touch_init();
void touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data);
// 调试:最近一次触点(物理 / LVGL 旋转后逻辑)
extern int16_t g_dbg_phys_x, g_dbg_phys_y;
extern int16_t g_dbg_logic_x, g_dbg_logic_y;
extern bool g_dbg_touch_down;

4
include/ui.h Normal file
View File

@@ -0,0 +1,4 @@
#pragma once
void ui_create();
void ui_tick();

56
platformio.ini Normal file
View File

@@ -0,0 +1,56 @@
; CYD ESP32-2432S028 — 口哨传感器(GPIO22 H/L) + SoftSPI 触控 + LVGL
; 触控/屏配置参考 tftest / hw269test / activeBuzzerTest
[env:cyd]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 460800
upload_speed = 460800
board_build.partitions = default.csv
lib_deps =
bodmer/TFT_eSPI@^2.5.43
lvgl/lvgl@9.2.2
build_flags =
-DCORE_DEBUG_LEVEL=0
-DUSER_SETUP_LOADED=1
-DILI9341_2_DRIVER=1
-DTFT_WIDTH=240
-DTFT_HEIGHT=320
-DTFT_MISO=12
-DTFT_MOSI=13
-DTFT_SCLK=14
-DTFT_CS=15
-DTFT_DC=2
-DTFT_RST=-1
-DTFT_BL=21
-DTFT_BACKLIGHT_ON=HIGH
-DTOUCH_CS=-1
-DLOAD_GLCD=1
-DLOAD_FONT2=1
-DLOAD_FONT4=1
-DLOAD_FONT6=1
-DLOAD_FONT7=1
-DLOAD_FONT8=1
-DLOAD_GFXFF=1
-DSMOOTH_FONT=1
-DSPI_FREQUENCY=40000000
-DSPI_READ_FREQUENCY=20000000
-DLV_CONF_SKIP
-DLV_COLOR_DEPTH=16
-DLV_MEM_SIZE=49152
-DLV_FONT_MONTSERRAT_14=1
-DLV_FONT_MONTSERRAT_16=1
-DLV_FONT_MONTSERRAT_18=1
-DLV_FONT_MONTSERRAT_20=1
-DLV_FONT_MONTSERRAT_28=1
-DLV_USE_TFT_ESPI=1
-DLV_USE_FLEX=1
-DLV_USE_BUTTON=1
-DLV_USE_LABEL=1
-DLV_USE_BAR=1
-DLV_USE_SLIDER=1
-I${PROJECT_DIR}/include
extra_scripts = pre:copy_lv_conf.py

46
src/main.cpp Normal file
View File

@@ -0,0 +1,46 @@
#include <Arduino.h>
#include <lvgl.h>
#include "pins.h"
#include "sound.h"
#include "touch.h"
#include "ui.h"
static constexpr uint32_t DRAW_BUF_SIZE =
(SCREEN_WIDTH * SCREEN_HEIGHT / 10 * (LV_COLOR_DEPTH / 8));
static uint32_t draw_buf[DRAW_BUF_SIZE / 4];
void setup() {
Serial.begin(460800);
delay(200);
Serial.println();
Serial.printf("LVGL %d.%d.%d | Whistle sound GPIO%d H/L\n", lv_version_major(),
lv_version_minor(), lv_version_patch(), SOUND_PIN);
Serial.println("BOOT_OK");
pinMode(TFT_BL_PIN, OUTPUT);
digitalWrite(TFT_BL_PIN, HIGH);
touch_init();
sound_init();
lv_init();
lv_display_t *disp =
lv_tft_espi_create(SCREEN_WIDTH, SCREEN_HEIGHT, draw_buf, sizeof(draw_buf));
lv_display_set_rotation(disp, LV_DISPLAY_ROTATION_270);
lv_indev_t *indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev, touch_read_cb);
lv_indev_set_display(indev, disp);
ui_create();
}
void loop() {
ui_tick();
lv_timer_handler();
lv_tick_inc(5);
delay(5);
}

61
src/sound.cpp Normal file
View File

@@ -0,0 +1,61 @@
#include "sound.h"
#include "pins.h"
#include <Arduino.h>
#include <limits.h>
static bool g_detected = false;
static bool g_raw_high = false;
static bool g_last_active = false;
static uint32_t g_events = 0;
static uint32_t g_last_event_ms = 0;
static uint32_t g_last_edge_ms = 0;
static constexpr uint32_t DEBOUNCE_MS = 40;
void sound_init() {
pinMode(SOUND_PIN, INPUT);
g_raw_high = digitalRead(SOUND_PIN) == HIGH;
g_detected = SOUND_ACTIVE_HIGH ? g_raw_high : !g_raw_high;
g_last_active = g_detected;
g_events = 0;
g_last_event_ms = 0;
g_last_edge_ms = millis();
Serial.printf("Sound sensor GPIO%d digital (active=%s)\n", SOUND_PIN,
SOUND_ACTIVE_HIGH ? "HIGH" : "LOW");
}
void sound_poll() {
g_raw_high = digitalRead(SOUND_PIN) == HIGH;
const bool active = SOUND_ACTIVE_HIGH ? g_raw_high : !g_raw_high;
g_detected = active;
const uint32_t now = millis();
if (active && !g_last_active && (now - g_last_edge_ms) >= DEBOUNCE_MS) {
g_events++;
g_last_event_ms = now;
g_last_edge_ms = now;
Serial.printf("SOUND_HIT #%lu\n", (unsigned long)g_events);
} else if (active != g_last_active) {
g_last_edge_ms = now;
}
g_last_active = active;
}
bool sound_detected() { return g_detected; }
bool sound_raw_high() { return g_raw_high; }
uint32_t sound_event_count() { return g_events; }
uint32_t sound_ms_since_last() {
if (g_last_event_ms == 0) {
return UINT32_MAX;
}
return millis() - g_last_event_ms;
}
void sound_reset_count() {
g_events = 0;
g_last_event_ms = 0;
Serial.println("SOUND_COUNT_RESET");
}

125
src/touch.cpp Normal file
View File

@@ -0,0 +1,125 @@
#include "touch.h"
#include "pins.h"
#include <Arduino.h>
/**
* SoftSPI XPT2046
* BL↔TR 是矩形主对角线镜像(XY 对调),不是 180°(180° 会搞成 TL↔BR)。
* 先按 rot2 得到旧 UI,再做对角修正,反变换喂给 LVGL 270。
*/
int16_t g_dbg_phys_x = -1;
int16_t g_dbg_phys_y = -1;
int16_t g_dbg_logic_x = -1;
int16_t g_dbg_logic_y = -1;
bool g_dbg_touch_down = false;
static void soft_write(uint8_t data) {
for (int i = 7; i >= 0; --i) {
digitalWrite(CYD_TOUCH_MOSI, (data >> i) & 1);
digitalWrite(CYD_TOUCH_CLK, HIGH);
delayMicroseconds(2);
digitalWrite(CYD_TOUCH_CLK, LOW);
delayMicroseconds(2);
}
}
static uint16_t soft_read12() {
uint16_t v = 0;
digitalWrite(CYD_TOUCH_CLK, HIGH);
delayMicroseconds(2);
digitalWrite(CYD_TOUCH_CLK, LOW);
delayMicroseconds(2);
for (int i = 0; i < 12; ++i) {
digitalWrite(CYD_TOUCH_CLK, HIGH);
delayMicroseconds(2);
v = (uint16_t)((v << 1) | (digitalRead(CYD_TOUCH_MISO) ? 1u : 0u));
digitalWrite(CYD_TOUCH_CLK, LOW);
delayMicroseconds(2);
}
return v;
}
static uint16_t xpt_read(uint8_t cmd) {
digitalWrite(CYD_TOUCH_CS, LOW);
soft_write(cmd);
uint16_t v = soft_read12();
digitalWrite(CYD_TOUCH_CS, HIGH);
return v;
}
static int16_t clampi(int32_t v, int32_t lo, int32_t hi) {
if (v < lo) {
return (int16_t)lo;
}
if (v > hi) {
return (int16_t)hi;
}
return (int16_t)v;
}
void touch_init() {
pinMode(CYD_TOUCH_CS, OUTPUT);
pinMode(CYD_TOUCH_CLK, OUTPUT);
pinMode(CYD_TOUCH_MOSI, OUTPUT);
pinMode(CYD_TOUCH_MISO, INPUT);
pinMode(CYD_TOUCH_IRQ, INPUT);
digitalWrite(CYD_TOUCH_CS, HIGH);
digitalWrite(CYD_TOUCH_CLK, LOW);
Serial.println("Touch: SoftSPI + diagonal fix (BL/TR)");
}
void touch_read_cb(lv_indev_t * /*indev*/, lv_indev_data_t *data) {
uint16_t adc_x = xpt_read(0x90);
uint16_t adc_y = xpt_read(0xD0);
adc_x = (uint16_t)((adc_x + xpt_read(0x90)) / 2);
adc_y = (uint16_t)((adc_y + xpt_read(0xD0)) / 2);
const bool irq = (digitalRead(CYD_TOUCH_IRQ) == LOW);
const bool in_range =
(adc_x > 80 && adc_x < 4000 && adc_y > 80 && adc_y < 4000);
if (!in_range || (!irq && (adc_x < 200 || adc_y < 200))) {
data->state = LV_INDEV_STATE_RELEASED;
g_dbg_touch_down = false;
return;
}
const int16_t p_x = (int16_t)adc_x;
const int16_t p_y = (int16_t)(4095 - adc_y);
int16_t x = map(p_x, 200, 3700, 1, SCREEN_WIDTH);
int16_t y = map(p_y, 240, 3800, 1, SCREEN_HEIGHT);
x = clampi(x, 0, SCREEN_WIDTH - 1);
y = clampi(y, 0, SCREEN_HEIGHT - 1);
// 旧 UI(LVGL270):(y, 239-x) — 该空间里 BL↔TR,做主对角线镜像
// (ux,uy) -> (uy*(319/239), ux*(239/319))
const int32_t old_ux = y;
const int32_t old_uy = (SCREEN_WIDTH - 1) - x;
int16_t ui_x = clampi((old_uy * (UI_HOR_RES - 1)) / (UI_VER_RES - 1), 0,
UI_HOR_RES - 1);
int16_t ui_y = clampi((old_ux * (UI_VER_RES - 1)) / (UI_HOR_RES - 1), 0,
UI_VER_RES - 1);
// 反变换喂 LVGL,使最终落在 (ui_x, ui_y)
const int16_t feed_x = (int16_t)((SCREEN_WIDTH - 1) - ui_y);
const int16_t feed_y = ui_x;
g_dbg_phys_x = x;
g_dbg_phys_y = y;
g_dbg_logic_x = ui_x;
g_dbg_logic_y = ui_y;
g_dbg_touch_down = true;
static uint32_t last_log = 0;
if (millis() - last_log > 80) {
last_log = millis();
Serial.printf("Touch UI(%d,%d)\n", ui_x, ui_y);
}
data->point.x = feed_x;
data->point.y = feed_y;
data->state = LV_INDEV_STATE_PRESSED;
}

138
src/ui.cpp Normal file
View File

@@ -0,0 +1,138 @@
#include "ui.h"
#include "pins.h"
#include "sound.h"
#include <Arduino.h>
#include <lvgl.h>
#include <stdio.h>
static constexpr uint32_t COL_BG = 0x0A1016;
static constexpr uint32_t COL_INK = 0xEAF0F6;
static constexpr uint32_t COL_MUTED = 0x7A8B9C;
static constexpr uint32_t COL_BTN = 0x243140;
static constexpr uint32_t COL_HIT = 0x1B5E4A;
static constexpr uint32_t COL_QUIET = 0x2A3340;
static lv_obj_t *lbl_title;
static lv_obj_t *lbl_pin;
static lv_obj_t *lbl_state;
static lv_obj_t *lbl_level;
static lv_obj_t *lbl_count;
static lv_obj_t *lbl_hint;
static lv_obj_t *btn_reset;
static lv_obj_t *bar_pulse;
static uint32_t g_flash_until = 0;
static void style_btn(lv_obj_t *btn, uint32_t bg) {
lv_obj_set_style_radius(btn, 10, 0);
lv_obj_set_style_bg_color(btn, lv_color_hex(bg), 0);
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
lv_obj_set_style_shadow_width(btn, 0, 0);
lv_obj_set_style_border_width(btn, 0, 0);
lv_obj_set_style_pad_all(btn, 0, 0);
}
static void on_reset(lv_event_t *e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) {
return;
}
sound_reset_count();
}
void ui_create() {
lv_obj_t *scr = lv_screen_active();
lv_obj_set_style_bg_color(scr, lv_color_hex(COL_BG), 0);
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
lbl_title = lv_label_create(scr);
lv_label_set_text(lbl_title, "Whistle / Sound");
lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_INK), 0);
lv_obj_set_style_text_font(lbl_title, &lv_font_montserrat_28, 0);
lv_obj_align(lbl_title, LV_ALIGN_TOP_MID, 0, 12);
lbl_pin = lv_label_create(scr);
lv_label_set_text_fmt(lbl_pin, "OUT -> GPIO%d (H/L)", SOUND_PIN);
lv_obj_set_style_text_color(lbl_pin, lv_color_hex(COL_MUTED), 0);
lv_obj_set_style_text_font(lbl_pin, &lv_font_montserrat_14, 0);
lv_obj_align(lbl_pin, LV_ALIGN_TOP_MID, 0, 48);
lbl_state = lv_label_create(scr);
lv_label_set_text(lbl_state, "QUIET");
lv_obj_set_style_text_color(lbl_state, lv_color_hex(COL_MUTED), 0);
lv_obj_set_style_text_font(lbl_state, &lv_font_montserrat_28, 0);
lv_obj_align(lbl_state, LV_ALIGN_TOP_MID, 0, 78);
lbl_level = lv_label_create(scr);
lv_label_set_text(lbl_level, "pin: LOW");
lv_obj_set_style_text_color(lbl_level, lv_color_hex(COL_MUTED), 0);
lv_obj_set_style_text_font(lbl_level, &lv_font_montserrat_16, 0);
lv_obj_align(lbl_level, LV_ALIGN_TOP_MID, 0, 114);
bar_pulse = lv_bar_create(scr);
lv_obj_set_size(bar_pulse, UI_HOR_RES - 48, 18);
lv_obj_align(bar_pulse, LV_ALIGN_TOP_MID, 0, 144);
lv_bar_set_range(bar_pulse, 0, 100);
lv_bar_set_value(bar_pulse, 0, LV_ANIM_OFF);
lv_obj_set_style_bg_color(bar_pulse, lv_color_hex(COL_QUIET), 0);
lv_obj_set_style_bg_opa(bar_pulse, LV_OPA_COVER, 0);
lv_obj_set_style_radius(bar_pulse, 6, 0);
lv_obj_set_style_bg_color(bar_pulse, lv_color_hex(0x3DDC97),
LV_PART_INDICATOR);
lv_obj_set_style_bg_opa(bar_pulse, LV_OPA_COVER, LV_PART_INDICATOR);
lv_obj_set_style_radius(bar_pulse, 6, LV_PART_INDICATOR);
lbl_count = lv_label_create(scr);
lv_label_set_text(lbl_count, "hits: 0");
lv_obj_set_style_text_color(lbl_count, lv_color_hex(COL_INK), 0);
lv_obj_set_style_text_font(lbl_count, &lv_font_montserrat_20, 0);
lv_obj_align(lbl_count, LV_ALIGN_TOP_MID, 0, 174);
lbl_hint = lv_label_create(scr);
lv_label_set_text(lbl_hint, "dB N/A (digital only)");
lv_obj_set_style_text_color(lbl_hint, lv_color_hex(COL_MUTED), 0);
lv_obj_set_style_text_font(lbl_hint, &lv_font_montserrat_14, 0);
lv_obj_align(lbl_hint, LV_ALIGN_TOP_MID, 0, 204);
btn_reset = lv_button_create(scr);
lv_obj_set_size(btn_reset, 120, 40);
lv_obj_align(btn_reset, LV_ALIGN_BOTTOM_MID, 0, -12);
style_btn(btn_reset, COL_BTN);
lv_obj_add_event_cb(btn_reset, on_reset, LV_EVENT_CLICKED, nullptr);
lv_obj_t *lbl = lv_label_create(btn_reset);
lv_label_set_text(lbl, "Reset");
lv_obj_set_style_text_color(lbl, lv_color_hex(COL_INK), 0);
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_16, 0);
lv_obj_center(lbl);
}
void ui_tick() {
sound_poll();
const bool hit = sound_detected();
const uint32_t now = millis();
if (hit) {
g_flash_until = now + 180;
}
const bool flash = hit || (now < g_flash_until);
if (lbl_state) {
lv_label_set_text(lbl_state, flash ? "SOUND!" : "QUIET");
lv_obj_set_style_text_color(
lbl_state, lv_color_hex(flash ? 0x3DDC97 : COL_MUTED), 0);
}
if (lbl_level) {
lv_label_set_text_fmt(lbl_level, "pin: %s -> %s",
sound_raw_high() ? "HIGH" : "LOW",
hit ? "active" : "idle");
}
if (bar_pulse) {
lv_bar_set_value(bar_pulse, flash ? 100 : 0, LV_ANIM_OFF);
lv_obj_set_style_bg_color(bar_pulse,
lv_color_hex(flash ? COL_HIT : COL_QUIET), 0);
}
if (lbl_count) {
lv_label_set_text_fmt(lbl_count, "hits: %lu",
(unsigned long)sound_event_count());
}
}