add esp32-S3
This commit is contained in:
@@ -110,35 +110,6 @@
|
||||
<pre id="espnowPeersJson"></pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Bluetooth</h3>
|
||||
<form id="btHfpConnectForm">
|
||||
<input type="text" id="btHfpAddr" placeholder="MAC téléphone HFP (AA:BB:CC:DD:EE:FF)">
|
||||
<button type="submit">HFP Connect</button>
|
||||
</form>
|
||||
<form id="btDialForm">
|
||||
<input type="text" id="btDialNumber" placeholder="Numéro (ex: 0612345678)">
|
||||
<button type="submit">Dial</button>
|
||||
</form>
|
||||
<div class="inline-actions">
|
||||
<button id="btHfpDisconnectBtn">HFP Disconnect</button>
|
||||
<button id="btRedialBtn">Redial</button>
|
||||
<button id="btAnswerBtn">Answer</button>
|
||||
<button id="btHangupBtn">Hangup</button>
|
||||
<button id="btCallsBtn">Calls Query</button>
|
||||
</div>
|
||||
<div class="inline-actions">
|
||||
<button id="btDiscoverableOnBtn">Discoverable ON</button>
|
||||
<button id="btDiscoverableOffBtn">Discoverable OFF</button>
|
||||
<button id="btAutoReconnectOnBtn">Auto Reconnect ON</button>
|
||||
<button id="btAutoReconnectOffBtn">Auto Reconnect OFF</button>
|
||||
<button id="btPbapSyncBtn">PBAP Sync</button>
|
||||
<button id="btBleStartBtn">BLE Start</button>
|
||||
<button id="btBleStopBtn">BLE Stop</button>
|
||||
<button id="btRefreshBtn">Rafraîchir BT</button>
|
||||
</div>
|
||||
<pre id="btJson"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
+2
-219
@@ -89,14 +89,10 @@ function applyStatusSnapshot(status) {
|
||||
const wifiState = status.wifi?.state || "n/a";
|
||||
const mqttConnected = status.mqtt?.connected ? "on" : "off";
|
||||
const peers = status.espnow?.peer_count ?? 0;
|
||||
const btCallState = status.bluetooth?.call_state || "n/a";
|
||||
const btConnected = status.bluetooth?.connected ? "on" : "off";
|
||||
const btAutoReconnect = status.bluetooth?.auto_reconnect_enabled ? "on" : "off";
|
||||
const pbapSupported = status.bluetooth?.pbap_supported ? "yes" : "no";
|
||||
const liveState = realtimeConnected ? "live=on" : "live=off";
|
||||
line.textContent =
|
||||
`board=${status.board_profile || "n/a"} telephony=${telephonyState} hook=${hook} ` +
|
||||
`wifi=${wifiState} mqtt=${mqttConnected} espnow_peers=${peers} bt=${btConnected} bt_auto=${btAutoReconnect} bt_call=${btCallState} pbap=${pbapSupported} ${liveState}`;
|
||||
`wifi=${wifiState} mqtt=${mqttConnected} espnow_peers=${peers} ${liveState}`;
|
||||
|
||||
setJson("statusJson", status);
|
||||
if (status.wifi) {
|
||||
@@ -109,9 +105,6 @@ function applyStatusSnapshot(status) {
|
||||
setJson("espnowJson", status.espnow);
|
||||
setJson("espnowPeersJson", { peers: status.espnow.peers || [] });
|
||||
}
|
||||
if (status.bluetooth) {
|
||||
setJson("btJson", status.bluetooth);
|
||||
}
|
||||
}
|
||||
|
||||
function connectRealtime() {
|
||||
@@ -235,15 +228,6 @@ async function refreshNetwork() {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshBluetooth() {
|
||||
try {
|
||||
const bt = await requestJson("/api/bluetooth");
|
||||
setJson("btJson", bt);
|
||||
} catch (error) {
|
||||
setJson("btJson", { error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async function sendControl(action) {
|
||||
const result = await requestJson("/api/control", {
|
||||
method: "POST",
|
||||
@@ -265,7 +249,6 @@ function bindEvents() {
|
||||
});
|
||||
document.getElementById("refreshConfigBtn").addEventListener("click", refreshConfig);
|
||||
document.getElementById("espnowRefreshBtn").addEventListener("click", refreshNetwork);
|
||||
document.getElementById("btRefreshBtn").addEventListener("click", refreshBluetooth);
|
||||
|
||||
document.getElementById("applyAudioConfigBtn").addEventListener("click", async () => {
|
||||
try {
|
||||
@@ -493,206 +476,6 @@ function bindEvents() {
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("btHfpConnectForm").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const addr = document.getElementById("btHfpAddr").value.trim();
|
||||
try {
|
||||
const result = await requestJson("/api/bluetooth/hfp/connect", {
|
||||
method: "POST",
|
||||
headers: jsonHeaders(),
|
||||
body: JSON.stringify({ addr }),
|
||||
});
|
||||
setJson("actionResult", result);
|
||||
await Promise.all([refreshBluetooth(), refreshStatus()]);
|
||||
} catch (error) {
|
||||
setJson("actionResult", { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("btHfpDisconnectBtn").addEventListener("click", async () => {
|
||||
try {
|
||||
const result = await requestJson("/api/bluetooth/hfp/disconnect", {
|
||||
method: "POST",
|
||||
headers: jsonHeaders(),
|
||||
body: "{}",
|
||||
});
|
||||
setJson("actionResult", result);
|
||||
await Promise.all([refreshBluetooth(), refreshStatus()]);
|
||||
} catch (error) {
|
||||
setJson("actionResult", { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("btDialForm").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const number = document.getElementById("btDialNumber").value.trim();
|
||||
try {
|
||||
const result = await requestJson("/api/bluetooth/hfp/dial", {
|
||||
method: "POST",
|
||||
headers: jsonHeaders(),
|
||||
body: JSON.stringify({ number }),
|
||||
});
|
||||
setJson("actionResult", result);
|
||||
await Promise.all([refreshBluetooth(), refreshStatus()]);
|
||||
} catch (error) {
|
||||
setJson("actionResult", { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("btRedialBtn").addEventListener("click", async () => {
|
||||
try {
|
||||
const result = await requestJson("/api/bluetooth/hfp/redial", {
|
||||
method: "POST",
|
||||
headers: jsonHeaders(),
|
||||
body: "{}",
|
||||
});
|
||||
setJson("actionResult", result);
|
||||
await Promise.all([refreshBluetooth(), refreshStatus()]);
|
||||
} catch (error) {
|
||||
setJson("actionResult", { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("btAnswerBtn").addEventListener("click", async () => {
|
||||
try {
|
||||
const result = await requestJson("/api/bluetooth/hfp/answer", {
|
||||
method: "POST",
|
||||
headers: jsonHeaders(),
|
||||
body: "{}",
|
||||
});
|
||||
setJson("actionResult", result);
|
||||
await Promise.all([refreshBluetooth(), refreshStatus()]);
|
||||
} catch (error) {
|
||||
setJson("actionResult", { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("btHangupBtn").addEventListener("click", async () => {
|
||||
try {
|
||||
const result = await requestJson("/api/bluetooth/hfp/hangup", {
|
||||
method: "POST",
|
||||
headers: jsonHeaders(),
|
||||
body: "{}",
|
||||
});
|
||||
setJson("actionResult", result);
|
||||
await Promise.all([refreshBluetooth(), refreshStatus()]);
|
||||
} catch (error) {
|
||||
setJson("actionResult", { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("btCallsBtn").addEventListener("click", async () => {
|
||||
try {
|
||||
const result = await requestJson("/api/bluetooth/hfp/calls", {
|
||||
method: "POST",
|
||||
headers: jsonHeaders(),
|
||||
body: "{}",
|
||||
});
|
||||
setJson("actionResult", result);
|
||||
await Promise.all([refreshBluetooth(), refreshStatus()]);
|
||||
} catch (error) {
|
||||
setJson("actionResult", { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("btDiscoverableOnBtn").addEventListener("click", async () => {
|
||||
try {
|
||||
const result = await requestJson("/api/bluetooth/discoverable/on", {
|
||||
method: "POST",
|
||||
headers: jsonHeaders(),
|
||||
body: "{}",
|
||||
});
|
||||
setJson("actionResult", result);
|
||||
await Promise.all([refreshBluetooth(), refreshStatus()]);
|
||||
} catch (error) {
|
||||
setJson("actionResult", { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("btDiscoverableOffBtn").addEventListener("click", async () => {
|
||||
try {
|
||||
const result = await requestJson("/api/bluetooth/discoverable/off", {
|
||||
method: "POST",
|
||||
headers: jsonHeaders(),
|
||||
body: "{}",
|
||||
});
|
||||
setJson("actionResult", result);
|
||||
await Promise.all([refreshBluetooth(), refreshStatus()]);
|
||||
} catch (error) {
|
||||
setJson("actionResult", { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("btAutoReconnectOnBtn").addEventListener("click", async () => {
|
||||
try {
|
||||
const result = await requestJson("/api/bluetooth/hfp/auto", {
|
||||
method: "POST",
|
||||
headers: jsonHeaders(),
|
||||
body: JSON.stringify({ enabled: true }),
|
||||
});
|
||||
setJson("actionResult", result);
|
||||
await Promise.all([refreshBluetooth(), refreshStatus()]);
|
||||
} catch (error) {
|
||||
setJson("actionResult", { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("btAutoReconnectOffBtn").addEventListener("click", async () => {
|
||||
try {
|
||||
const result = await requestJson("/api/bluetooth/hfp/auto", {
|
||||
method: "POST",
|
||||
headers: jsonHeaders(),
|
||||
body: JSON.stringify({ enabled: false }),
|
||||
});
|
||||
setJson("actionResult", result);
|
||||
await Promise.all([refreshBluetooth(), refreshStatus()]);
|
||||
} catch (error) {
|
||||
setJson("actionResult", { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("btPbapSyncBtn").addEventListener("click", async () => {
|
||||
try {
|
||||
const result = await requestJson("/api/bluetooth/pbap/sync", {
|
||||
method: "POST",
|
||||
headers: jsonHeaders(),
|
||||
body: "{}",
|
||||
});
|
||||
setJson("actionResult", result);
|
||||
await Promise.all([refreshBluetooth(), refreshStatus()]);
|
||||
} catch (error) {
|
||||
setJson("actionResult", { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("btBleStartBtn").addEventListener("click", async () => {
|
||||
try {
|
||||
const result = await requestJson("/api/bluetooth/ble/start", {
|
||||
method: "POST",
|
||||
headers: jsonHeaders(),
|
||||
body: "{}",
|
||||
});
|
||||
setJson("actionResult", result);
|
||||
await Promise.all([refreshBluetooth(), refreshStatus()]);
|
||||
} catch (error) {
|
||||
setJson("actionResult", { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("btBleStopBtn").addEventListener("click", async () => {
|
||||
try {
|
||||
const result = await requestJson("/api/bluetooth/ble/stop", {
|
||||
method: "POST",
|
||||
headers: jsonHeaders(),
|
||||
body: "{}",
|
||||
});
|
||||
setJson("actionResult", result);
|
||||
await Promise.all([refreshBluetooth(), refreshStatus()]);
|
||||
} catch (error) {
|
||||
setJson("actionResult", { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll("#controlSection button[data-action]").forEach((button) => {
|
||||
button.addEventListener("click", async () => {
|
||||
try {
|
||||
@@ -718,6 +501,6 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
bindEvents();
|
||||
connectRealtime();
|
||||
ensureFallbackPolling();
|
||||
await Promise.all([refreshStatus(), refreshConfig(), refreshNetwork(), refreshBluetooth()]);
|
||||
await Promise.all([refreshStatus(), refreshConfig(), refreshNetwork()]);
|
||||
showSection("dashboard");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
app0, app, ota_0, 0x10000, 0x640000,
|
||||
spiffs, data, spiffs, 0x650000, 0x9A0000,
|
||||
coredump, data, coredump,0xFF0000, 0x10000,
|
||||
|
@@ -0,0 +1,5 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
app0, app, ota_0, 0x10000, 0x640000,
|
||||
usbmsc, data, fat, 0x650000, 0x9A0000,
|
||||
coredump, data, coredump, 0xFF0000, 0x10000,
|
||||
|
+75
-22
@@ -1,7 +1,7 @@
|
||||
; PlatformIO Project Configuration File
|
||||
|
||||
[platformio]
|
||||
default_envs = esp32dev
|
||||
default_envs = esp32-s3-devkitc-1
|
||||
build_dir = .pio/build_live
|
||||
|
||||
[env]
|
||||
@@ -15,6 +15,7 @@ lib_deps =
|
||||
bblanchon/ArduinoJson@^7.0.4
|
||||
throwtheswitch/Unity@^2.6.1
|
||||
https://github.com/pschatzmann/arduino-audio-tools.git
|
||||
https://github.com/bitluni/OsciDisplay.git
|
||||
lib_ignore =
|
||||
ESPAsyncTCP
|
||||
RPAsyncTCP
|
||||
@@ -34,29 +35,11 @@ build_src_filter =
|
||||
+<audio/Es8388Driver.cpp>
|
||||
+<config/A252ConfigStore.cpp>
|
||||
+<core/CommandDispatcher.cpp>
|
||||
+<core/AgentSupervisor.cpp>
|
||||
+<core/PlatformProfile.cpp>
|
||||
+<props/EspNowBridge.cpp>
|
||||
+<slic/Ks0835SlicController.cpp>
|
||||
+<telephony/DtmfDecoder.cpp>
|
||||
+<telephony/TelephonyService.cpp>
|
||||
|
||||
[env:esp32dev-bt-migrate]
|
||||
board = esp32dev
|
||||
board_build.partitions = huge_app.csv
|
||||
upload_port = /dev/cu.usbserial
|
||||
monitor_port = /dev/cu.usbserial
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-DBOARD_PROFILE_A252
|
||||
-DBT_BACKEND_EXPERIMENTAL=1
|
||||
build_src_filter =
|
||||
+<main.cpp>
|
||||
+<audio/AudioEngine.cpp>
|
||||
+<audio/Es8388Driver.cpp>
|
||||
+<config/A252ConfigStore.cpp>
|
||||
+<core/CommandDispatcher.cpp>
|
||||
+<core/PlatformProfile.cpp>
|
||||
+<props/EspNowBridge.cpp>
|
||||
+<wifi/WifiManager.cpp>
|
||||
+<visual/ScopeDisplay.cpp>
|
||||
+<slic/Ks0835SlicController.cpp>
|
||||
+<telephony/DtmfDecoder.cpp>
|
||||
+<telephony/TelephonyService.cpp>
|
||||
@@ -77,6 +60,76 @@ lib_deps =
|
||||
|
||||
[env:esp32-s3-devkitc-1]
|
||||
board = esp32-s3-devkitc-1
|
||||
board_build.partitions = partitions/esp32s3_no_ota_spiffs_16MB.csv
|
||||
upload_port = /dev/cu.usbserial
|
||||
monitor_port = /dev/cu.usbserial
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-DBOARD_PROFILE_ESP32_S3
|
||||
build_src_filter =
|
||||
+<main.cpp>
|
||||
+<audio/AudioEngine.cpp>
|
||||
+<audio/Es8388Driver.cpp>
|
||||
+<config/A252ConfigStore.cpp>
|
||||
+<core/CommandDispatcher.cpp>
|
||||
+<core/AgentSupervisor.cpp>
|
||||
+<core/PlatformProfile.cpp>
|
||||
+<props/EspNowBridge.cpp>
|
||||
+<wifi/WifiManager.cpp>
|
||||
+<visual/ScopeDisplay.cpp>
|
||||
+<slic/Ks0835SlicController.cpp>
|
||||
+<telephony/DtmfDecoder.cpp>
|
||||
+<telephony/TelephonyService.cpp>
|
||||
|
||||
[env:esp32-s3-usb-host]
|
||||
board = esp32s3usbotg
|
||||
board_build.partitions = partitions/esp32s3_no_ota_spiffs_16MB.csv
|
||||
upload_port = /dev/cu.usbserial
|
||||
monitor_port = /dev/cu.usbserial
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-DBOARD_PROFILE_ESP32_S3
|
||||
-DUSB_HOST_BOOT_ENABLE
|
||||
build_src_filter =
|
||||
+<main.cpp>
|
||||
+<audio/AudioEngine.cpp>
|
||||
+<audio/Es8388Driver.cpp>
|
||||
+<config/A252ConfigStore.cpp>
|
||||
+<core/CommandDispatcher.cpp>
|
||||
+<core/AgentSupervisor.cpp>
|
||||
+<core/PlatformProfile.cpp>
|
||||
+<props/EspNowBridge.cpp>
|
||||
+<wifi/WifiManager.cpp>
|
||||
+<visual/ScopeDisplay.cpp>
|
||||
+<slic/Ks0835SlicController.cpp>
|
||||
+<telephony/DtmfDecoder.cpp>
|
||||
+<telephony/TelephonyService.cpp>
|
||||
|
||||
[env:esp32-s3-usb-msc]
|
||||
board = esp32-s3-devkitc-1
|
||||
board_build.partitions = partitions/esp32s3_no_ota_spiffs_usb_msc_16MB.csv
|
||||
extra_scripts = scripts/upload_usbmsc.py
|
||||
upload_port = /dev/cu.usbserial
|
||||
monitor_port = /dev/cu.usbserial
|
||||
build_unflags =
|
||||
-DARDUINO_USB_MODE=1
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-DBOARD_PROFILE_ESP32_S3
|
||||
-DARDUINO_USB_MODE=0
|
||||
-DUSB_MSC_BOOT_ENABLE
|
||||
build_src_filter =
|
||||
+<main.cpp>
|
||||
+<usb/UsbMassStorageRuntime.cpp>
|
||||
+<audio/AudioEngine.cpp>
|
||||
+<audio/Es8388Driver.cpp>
|
||||
+<config/A252ConfigStore.cpp>
|
||||
+<core/CommandDispatcher.cpp>
|
||||
+<core/AgentSupervisor.cpp>
|
||||
+<core/PlatformProfile.cpp>
|
||||
+<props/EspNowBridge.cpp>
|
||||
+<wifi/WifiManager.cpp>
|
||||
+<visual/ScopeDisplay.cpp>
|
||||
+<slic/Ks0835SlicController.cpp>
|
||||
+<telephony/DtmfDecoder.cpp>
|
||||
+<telephony/TelephonyService.cpp>
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
"""PlatformIO custom target: create and flash FFat USB-MSC image."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _parse_int(value: str) -> int:
|
||||
value = value.strip()
|
||||
if value.lower().startswith("0x"):
|
||||
return int(value, 16)
|
||||
return int(value)
|
||||
|
||||
|
||||
def _normalize_flash_freq(value: str) -> str:
|
||||
if not value:
|
||||
return "80m"
|
||||
normalized = value.strip().lower()
|
||||
if normalized.endswith("l"):
|
||||
normalized = normalized[:-1]
|
||||
if normalized.isdigit():
|
||||
freq_hz = int(normalized)
|
||||
if freq_hz % 1_000_000 == 0:
|
||||
return f"{freq_hz // 1_000_000}m"
|
||||
return str(freq_hz)
|
||||
return normalized
|
||||
|
||||
|
||||
def _run(command):
|
||||
print("[upload_usbmsc] $ {}".format(" ".join(command)))
|
||||
result = subprocess.run(command, check=False)
|
||||
if result.returncode:
|
||||
raise RuntimeError(f"Command failed ({command[0]}): {result.returncode}")
|
||||
|
||||
|
||||
def _find_tool(tool: str, project_dir: Path, env) -> str:
|
||||
path = shutil.which(tool)
|
||||
if path:
|
||||
return path
|
||||
|
||||
platformio_home = Path.home() / ".platformio" / "packages"
|
||||
project_package = Path(os.environ.get("PIO_PACKAGES_DIR", str(project_dir / ".pio" / "packages")))
|
||||
candidates = []
|
||||
|
||||
if tool == "mkfatfs":
|
||||
candidates.extend(
|
||||
[
|
||||
project_package / "tool-mkfatfs" / "mkfatfs",
|
||||
platformio_home / "tool-mkfatfs" / "mkfatfs",
|
||||
]
|
||||
)
|
||||
elif tool == "esptool.py":
|
||||
candidates.extend(
|
||||
[
|
||||
project_package / "tool-esptoolpy" / "esptool.py",
|
||||
platformio_home / "tool-esptoolpy" / "esptool.py",
|
||||
]
|
||||
)
|
||||
else:
|
||||
candidates.append(Path(tool))
|
||||
|
||||
for candidate in candidates:
|
||||
if candidate.exists():
|
||||
return str(candidate)
|
||||
|
||||
raise RuntimeError(f"Unable to locate '{tool}'. Install PlatformIO package dependencies first.")
|
||||
|
||||
|
||||
def _find_usbmsc_partition(partitions_path: Path) -> tuple[int, int]:
|
||||
if not partitions_path.exists():
|
||||
raise FileNotFoundError(f"Partition table not found: {partitions_path}")
|
||||
|
||||
for line in partitions_path.read_text(encoding="utf-8").splitlines():
|
||||
if not line.strip() or line.lstrip().startswith("#"):
|
||||
continue
|
||||
parts = [segment.strip() for segment in re.split(r",\s*", line) if segment.strip()]
|
||||
if len(parts) < 5:
|
||||
continue
|
||||
name, _part_type, _sub_type, offset, size = parts[:5]
|
||||
if name == "usbmsc":
|
||||
return _parse_int(offset), _parse_int(size)
|
||||
|
||||
raise RuntimeError("Could not find 'usbmsc' partition in partition table.")
|
||||
|
||||
|
||||
def _build_usbmsc_image(mkfatfs: str, source_dir: Path, image_path: Path, image_size: int) -> None:
|
||||
if not source_dir.is_dir():
|
||||
raise RuntimeError(f"WebUI folder not found: {source_dir}")
|
||||
if image_path.exists():
|
||||
image_path.unlink()
|
||||
_run([mkfatfs, "-c", str(source_dir), "-t", "fatfs", "-s", str(image_size), str(image_path)])
|
||||
|
||||
|
||||
def _upload_usbmsc_image(
|
||||
esptool: str,
|
||||
python_exec: str,
|
||||
port: str,
|
||||
flash_args: dict,
|
||||
offset: int,
|
||||
image_path: Path,
|
||||
) -> None:
|
||||
command = [
|
||||
python_exec,
|
||||
esptool,
|
||||
"--port",
|
||||
port,
|
||||
"--baud",
|
||||
flash_args["baud"],
|
||||
"--before",
|
||||
flash_args["before"],
|
||||
"--after",
|
||||
flash_args["after"],
|
||||
"write_flash",
|
||||
"-z",
|
||||
"--flash_mode",
|
||||
flash_args["mode"],
|
||||
"--flash_freq",
|
||||
flash_args["freq"],
|
||||
"--flash_size",
|
||||
flash_args["size"],
|
||||
hex(offset),
|
||||
str(image_path),
|
||||
]
|
||||
_run(command)
|
||||
|
||||
|
||||
def _target_upload_usbmsc(source, target, env):
|
||||
project_dir = Path(env.subst("${PROJECT_DIR}"))
|
||||
build_dir = Path(env.subst("${BUILD_DIR}"))
|
||||
build_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
board = env.BoardConfig()
|
||||
partition_path_raw = env.GetProjectConfig().get("env:%s" % env["PIOENV"], "board_build.partitions")
|
||||
if not partition_path_raw:
|
||||
partition_path_raw = board.get("build", {}).get("partitions")
|
||||
if not partition_path_raw:
|
||||
raise RuntimeError("No partition table configured for this environment.")
|
||||
|
||||
partitions_path = Path(partition_path_raw)
|
||||
if not partitions_path.is_absolute():
|
||||
partitions_path = project_dir / partitions_path
|
||||
|
||||
partition_offset, partition_size = _find_usbmsc_partition(partitions_path)
|
||||
if partition_size <= 0:
|
||||
raise RuntimeError("USB-MSC partition has invalid size.")
|
||||
|
||||
webui_dir = project_dir / "data" / "webui"
|
||||
image_path = build_dir / "usbmsc.bin"
|
||||
|
||||
mkfatfs = _find_tool("mkfatfs", project_dir, env)
|
||||
_build_usbmsc_image(mkfatfs, webui_dir, image_path, partition_size)
|
||||
print(
|
||||
"[upload_usbmsc] built image "
|
||||
f"{image_path} ({image_path.stat().st_size} bytes, partition 0x{partition_offset:06x} / 0x{partition_size:06x})"
|
||||
)
|
||||
|
||||
dry_run = os.environ.get("USB_MSC_DRY_RUN", "").lower() in {"1", "true", "yes", "on"}
|
||||
if dry_run:
|
||||
print("[upload_usbmsc] dry run enabled, skipping flash step")
|
||||
return
|
||||
|
||||
upload_port = (
|
||||
env.GetProjectOption("upload_port", None)
|
||||
or env.get("UPLOAD_PORT")
|
||||
or os.environ.get("PIO_UPLOAD_PORT")
|
||||
or os.environ.get("UPLOAD_PORT")
|
||||
)
|
||||
if not upload_port:
|
||||
print("[upload_usbmsc] skipped flash step (no upload port), image ready")
|
||||
return
|
||||
|
||||
esptool = _find_tool("esptool.py", project_dir, env)
|
||||
python_exec = env.get("PYTHONEXE", shutil.which("python3") or "python3")
|
||||
|
||||
board_upload = board.get("upload", {})
|
||||
board_build = board.get("build", {})
|
||||
|
||||
flash_args = {
|
||||
"baud": str(board_upload.get("speed", env.get("UPLOAD_SPEED", "460800"))),
|
||||
"before": env.get("UPLOAD_BEFORE", "default_reset"),
|
||||
"after": env.get("UPLOAD_AFTER", "hard_reset"),
|
||||
"mode": env.get("FLASH_MODE", board_build.get("flash_mode", "dio")),
|
||||
"freq": _normalize_flash_freq(board_build.get("f_flash", env.get("F_FLASH", "80m"))),
|
||||
"size": board_upload.get("flash_size", env.get("FLASH_SIZE", "8MB")),
|
||||
}
|
||||
_upload_usbmsc_image(esptool, python_exec, upload_port, flash_args, partition_offset, image_path)
|
||||
print(f"[upload_usbmsc] flashed {image_path.name} at 0x{partition_offset:06x}")
|
||||
|
||||
|
||||
def _target_upload_usbmsc_dryrun(source, target, env):
|
||||
previous = os.environ.get("USB_MSC_DRY_RUN")
|
||||
os.environ["USB_MSC_DRY_RUN"] = "1"
|
||||
try:
|
||||
_target_upload_usbmsc(source, target, env)
|
||||
finally:
|
||||
if previous is None:
|
||||
os.environ.pop("USB_MSC_DRY_RUN", None)
|
||||
else:
|
||||
os.environ["USB_MSC_DRY_RUN"] = previous
|
||||
|
||||
|
||||
Import("env")
|
||||
if hasattr(env, "AddCustomTarget"):
|
||||
env.AddCustomTarget(
|
||||
name="upload_usbmsc",
|
||||
dependencies=[],
|
||||
actions=[_target_upload_usbmsc],
|
||||
title="Upload USB-MSC",
|
||||
description="Build and flash FFat USB-MSC webui image",
|
||||
)
|
||||
env.AddCustomTarget(
|
||||
name="upload_usbmsc_dryrun",
|
||||
dependencies=[],
|
||||
actions=[_target_upload_usbmsc_dryrun],
|
||||
title="Upload USB-MSC (dry run)",
|
||||
description="Build FFat USB-MSC webui image without flashing",
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "audio/AudioEngine.h"
|
||||
|
||||
#include <SD_MMC.h>
|
||||
#include <esp_dsp.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@@ -15,11 +16,21 @@ constexpr int16_t kDialToneAmplitude = 32000;
|
||||
constexpr float kDialToneLinearGain = 1.14f;
|
||||
constexpr size_t kDialToneChunkFrames = 160;
|
||||
constexpr size_t kMaxChannels = 2;
|
||||
constexpr size_t kAdcDspFirTaps = 5U;
|
||||
constexpr float kDspDcBlockR = 0.995f;
|
||||
constexpr float kDspHighPassHz = 250.0f;
|
||||
constexpr float kDspLowPassHz = 3400.0f;
|
||||
constexpr float kDspAdcScale = 1.0f / 2048.0f;
|
||||
constexpr float kDspPostGain = 1.0f;
|
||||
constexpr uint8_t kAdcDspMinFftDownsample = 1U;
|
||||
constexpr uint8_t kAdcDspMaxFftDownsample = 64U;
|
||||
constexpr float kDialToneAttackMs = 25.0f;
|
||||
constexpr float kDialToneReleaseMs = 40.0f;
|
||||
constexpr TickType_t kI2sWriteTimeoutMs = 2;
|
||||
constexpr TickType_t kI2sReadTimeoutMs = 2;
|
||||
constexpr size_t kPlaybackCopyBytes = 1024;
|
||||
constexpr int16_t kAdcRawMax = 4095;
|
||||
constexpr int16_t kAdcMidScale = kAdcRawMax / 2;
|
||||
|
||||
int16_t clampInt16(float value) {
|
||||
if (value > static_cast<float>(std::numeric_limits<int16_t>::max())) {
|
||||
@@ -31,6 +42,77 @@ int16_t clampInt16(float value) {
|
||||
return static_cast<int16_t>(value);
|
||||
}
|
||||
|
||||
void biquadLowPassCoeff(float sample_rate_hz, float frequency_hz, float q, float& b0, float& b1, float& b2, float& a1, float& a2) {
|
||||
if (frequency_hz <= 0.0f || sample_rate_hz <= 0.0f || q <= 0.0f) {
|
||||
b0 = 1.0f;
|
||||
b1 = 0.0f;
|
||||
b2 = 0.0f;
|
||||
a1 = 0.0f;
|
||||
a2 = 0.0f;
|
||||
return;
|
||||
}
|
||||
const float omega = kTwoPi * frequency_hz / sample_rate_hz;
|
||||
const float sn = std::sin(omega);
|
||||
const float cs = std::cos(omega);
|
||||
const float alpha = sn / (2.0f * q);
|
||||
const float b0o = (1.0f - cs) / 2.0f;
|
||||
const float b1o = 1.0f - cs;
|
||||
const float b2o = (1.0f - cs) / 2.0f;
|
||||
const float a0 = 1.0f + alpha;
|
||||
const float a1o = -2.0f * cs;
|
||||
const float a2o = 1.0f - alpha;
|
||||
|
||||
b0 = b0o / a0;
|
||||
b1 = b1o / a0;
|
||||
b2 = b2o / a0;
|
||||
a1 = a1o / a0;
|
||||
a2 = a2o / a0;
|
||||
}
|
||||
|
||||
void biquadHighPassCoeff(float sample_rate_hz, float frequency_hz, float q, float& b0, float& b1, float& b2, float& a1, float& a2) {
|
||||
if (frequency_hz <= 0.0f || sample_rate_hz <= 0.0f || q <= 0.0f) {
|
||||
b0 = 1.0f;
|
||||
b1 = 0.0f;
|
||||
b2 = 0.0f;
|
||||
a1 = 0.0f;
|
||||
a2 = 0.0f;
|
||||
return;
|
||||
}
|
||||
const float omega = kTwoPi * frequency_hz / sample_rate_hz;
|
||||
const float sn = std::sin(omega);
|
||||
const float cs = std::cos(omega);
|
||||
const float alpha = sn / (2.0f * q);
|
||||
const float b0o = (1.0f + cs) / 2.0f;
|
||||
const float b1o = -(1.0f + cs);
|
||||
const float b2o = (1.0f + cs) / 2.0f;
|
||||
const float a0 = 1.0f + alpha;
|
||||
const float a1o = -2.0f * cs;
|
||||
const float a2o = 1.0f - alpha;
|
||||
|
||||
b0 = b0o / a0;
|
||||
b1 = b1o / a0;
|
||||
b2 = b2o / a0;
|
||||
a1 = a1o / a0;
|
||||
a2 = a2o / a0;
|
||||
}
|
||||
|
||||
float processBiquad(float input, float& b0, float& b1, float& b2, float& a1, float& a2, float& z1, float& z2) {
|
||||
const float y = b0 * input + z1;
|
||||
z1 = b1 * input - a1 * y + z2;
|
||||
z2 = b2 * input - a2 * y;
|
||||
return y;
|
||||
}
|
||||
|
||||
float clampFloat(float value, float lo, float hi) {
|
||||
if (value < lo) {
|
||||
return lo;
|
||||
}
|
||||
if (value > hi) {
|
||||
return hi;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
int bitsPerSampleToInt(i2s_bits_per_sample_t bits) {
|
||||
switch (bits) {
|
||||
case I2S_BITS_PER_SAMPLE_24BIT:
|
||||
@@ -141,9 +223,244 @@ bool AudioEngine::prepareWavPlayback(File& file, const char* path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void AudioEngine::initAdcDspChain(uint32_t sample_rate_hz) {
|
||||
const float sr = static_cast<float>(sample_rate_hz == 0U ? kAdcDspDefaultSampleRateHz : sample_rate_hz);
|
||||
|
||||
const float high_cut = std::min(sr * 0.45f - 20.0f, kDspLowPassHz);
|
||||
const float low_cut = std::min(std::max(kDspHighPassHz, 10.0f), sr * 0.45f - 100.0f);
|
||||
|
||||
biquadHighPassCoeff(sr, low_cut, 0.707f, adc_dsp_biquad_hp_b0_, adc_dsp_biquad_hp_b1_, adc_dsp_biquad_hp_b2_,
|
||||
adc_dsp_biquad_hp_a1_, adc_dsp_biquad_hp_a2_);
|
||||
biquadLowPassCoeff(sr, high_cut > 0.0f ? high_cut : 1.0f, 0.707f, adc_dsp_biquad_lp_b0_, adc_dsp_biquad_lp_b1_,
|
||||
adc_dsp_biquad_lp_b2_, adc_dsp_biquad_lp_a1_, adc_dsp_biquad_lp_a2_);
|
||||
resetAdcDspState();
|
||||
initAdcFftDspBackend();
|
||||
adc_dsp_chain_enabled_ = true;
|
||||
Serial.printf("[AudioEngine] ADC DSP chain enabled (sr=%u, hp=%.1fHz, lp=%.1fHz)\n",
|
||||
static_cast<unsigned>(sample_rate_hz),
|
||||
low_cut,
|
||||
high_cut > 0.0f ? high_cut : 1.0f);
|
||||
}
|
||||
|
||||
void AudioEngine::initAdcFftDspBackend() {
|
||||
adc_dsp_fft_probe_backend_ready_ = false;
|
||||
if (!adc_dsp_fft_probe_enabled_ || !adc_fft_enabled_ || kAdcDspFftWindowSamples == 0U) {
|
||||
return;
|
||||
}
|
||||
|
||||
const esp_err_t ret = dsps_fft2r_init_fc32(nullptr, CONFIG_DSP_MAX_FFT_SIZE);
|
||||
if (ret != ESP_OK) {
|
||||
Serial.printf("[AudioEngine] FFT backend init failed: %d\n", ret);
|
||||
return;
|
||||
}
|
||||
adc_dsp_fft_probe_backend_ready_ = true;
|
||||
}
|
||||
|
||||
void AudioEngine::deinitAdcFftDspBackend() {
|
||||
if (!adc_dsp_fft_probe_backend_ready_) {
|
||||
return;
|
||||
}
|
||||
|
||||
dsps_fft2r_deinit_fc32();
|
||||
adc_dsp_fft_probe_backend_ready_ = false;
|
||||
}
|
||||
|
||||
void AudioEngine::updateAdcDspConfig(const AudioConfig& cfg) {
|
||||
adc_dsp_chain_enabled_ = (use_adc_capture_ && cfg.adc_dsp_enabled);
|
||||
adc_fft_enabled_ = (adc_dsp_chain_enabled_ && cfg.adc_fft_enabled);
|
||||
adc_dsp_fft_probe_enabled_ = adc_fft_enabled_;
|
||||
adc_dsp_fft_downsample_ = cfg.adc_dsp_fft_downsample;
|
||||
if (adc_dsp_fft_downsample_ < kAdcDspMinFftDownsample) {
|
||||
adc_dsp_fft_downsample_ = kAdcDspMinFftDownsample;
|
||||
} else if (adc_dsp_fft_downsample_ > kAdcDspMaxFftDownsample) {
|
||||
adc_dsp_fft_downsample_ = kAdcDspMaxFftDownsample;
|
||||
}
|
||||
|
||||
const uint16_t max_ignore_bin = static_cast<uint16_t>((kAdcDspFftWindowSamples / 2U > 0U) ? (kAdcDspFftWindowSamples / 2U - 1U) : 0U);
|
||||
adc_fft_ignore_low_bin_ = std::min<uint16_t>(cfg.adc_fft_ignore_low_bin, max_ignore_bin);
|
||||
adc_fft_ignore_high_bin_ = std::min<uint16_t>(cfg.adc_fft_ignore_high_bin, max_ignore_bin);
|
||||
|
||||
if (!adc_dsp_chain_enabled_) {
|
||||
deinitAdcFftDspBackend();
|
||||
return;
|
||||
}
|
||||
|
||||
initAdcDspChain(cfg.sample_rate);
|
||||
}
|
||||
|
||||
void AudioEngine::resetAdcDspState() {
|
||||
std::memset(adc_dsp_fir_state_, 0, sizeof(adc_dsp_fir_state_));
|
||||
adc_dsp_fir_pos_ = 0U;
|
||||
adc_dsp_prev_input_ = 0.0f;
|
||||
adc_dsp_prev_output_ = 0.0f;
|
||||
adc_dsp_biquad_hp_z1_ = 0.0f;
|
||||
adc_dsp_biquad_hp_z2_ = 0.0f;
|
||||
adc_dsp_biquad_lp_z1_ = 0.0f;
|
||||
adc_dsp_biquad_lp_z2_ = 0.0f;
|
||||
std::memset(adc_dsp_fft_buffer_, 0, sizeof(adc_dsp_fft_buffer_));
|
||||
adc_dsp_fft_head_ = 0U;
|
||||
adc_dsp_fft_fill_ = 0U;
|
||||
adc_dsp_fft_decimator_ = 0U;
|
||||
metrics_.adc_fft_peak_bin = 0U;
|
||||
const uint32_t effective_downsample = std::max<uint32_t>(1U, static_cast<uint32_t>(adc_dsp_fft_downsample_));
|
||||
metrics_.adc_fft_probe_rate_hz =
|
||||
static_cast<uint16_t>(std::max(1U, _config.sample_rate / std::max(1U, effective_downsample)));
|
||||
metrics_.adc_fft_peak_freq_hz = 0.0f;
|
||||
metrics_.adc_fft_peak_magnitude = 0.0f;
|
||||
}
|
||||
|
||||
float AudioEngine::applyDcBlocker(float sample) {
|
||||
const float filtered = sample - adc_dsp_prev_input_ + (kDspDcBlockR * adc_dsp_prev_output_);
|
||||
adc_dsp_prev_input_ = sample;
|
||||
adc_dsp_prev_output_ = filtered;
|
||||
return filtered;
|
||||
}
|
||||
|
||||
float AudioEngine::applyFirNoiseReduction(float sample) {
|
||||
adc_dsp_fir_state_[adc_dsp_fir_pos_] = sample;
|
||||
|
||||
// FIR taps: 1/16 * [1, 4, 6, 4, 1] (soft anti-alias + anti-click).
|
||||
constexpr float kFirCoeff[kAdcDspFirTaps] = {0.0625f, 0.25f, 0.375f, 0.25f, 0.0625f};
|
||||
|
||||
float result = 0.0f;
|
||||
uint8_t idx = adc_dsp_fir_pos_;
|
||||
for (size_t tap = 0U; tap < kAdcDspFirTaps; ++tap) {
|
||||
const size_t fir_idx = (idx + kAdcDspFirTaps - tap) % kAdcDspFirTaps;
|
||||
result += kFirCoeff[tap] * adc_dsp_fir_state_[fir_idx];
|
||||
}
|
||||
|
||||
adc_dsp_fir_pos_ = static_cast<uint8_t>((adc_dsp_fir_pos_ + 1U) % kAdcDspFirTaps);
|
||||
return result;
|
||||
}
|
||||
|
||||
int16_t AudioEngine::applyBiquadChain(float sample) {
|
||||
const float hp = processBiquad(sample, adc_dsp_biquad_hp_b0_, adc_dsp_biquad_hp_b1_, adc_dsp_biquad_hp_b2_,
|
||||
adc_dsp_biquad_hp_a1_, adc_dsp_biquad_hp_a2_, adc_dsp_biquad_hp_z1_,
|
||||
adc_dsp_biquad_hp_z2_);
|
||||
const float lp = processBiquad(hp, adc_dsp_biquad_lp_b0_, adc_dsp_biquad_lp_b1_, adc_dsp_biquad_lp_b2_,
|
||||
adc_dsp_biquad_lp_a1_, adc_dsp_biquad_lp_a2_, adc_dsp_biquad_lp_z1_,
|
||||
adc_dsp_biquad_lp_z2_);
|
||||
return clampInt16(clampFloat(lp * kDspPostGain * 32768.0f, -32768.0f, 32767.0f));
|
||||
}
|
||||
|
||||
void AudioEngine::appendAdcFftSample(float sample) {
|
||||
if (!adc_dsp_fft_probe_enabled_ || adc_dsp_fft_downsample_ == 0U || kAdcDspFftWindowSamples == 0U) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (++adc_dsp_fft_decimator_ < adc_dsp_fft_downsample_) {
|
||||
return;
|
||||
}
|
||||
adc_dsp_fft_decimator_ = 0U;
|
||||
|
||||
adc_dsp_fft_buffer_[adc_dsp_fft_head_] = sample;
|
||||
adc_dsp_fft_head_ = static_cast<uint8_t>((adc_dsp_fft_head_ + 1U) % kAdcDspFftWindowSamples);
|
||||
if (adc_dsp_fft_fill_ < kAdcDspFftWindowSamples) {
|
||||
++adc_dsp_fft_fill_;
|
||||
return;
|
||||
}
|
||||
|
||||
runAdcFftProbe();
|
||||
}
|
||||
|
||||
void AudioEngine::runAdcFftProbe() {
|
||||
if (!adc_dsp_fft_probe_enabled_ || adc_dsp_fft_fill_ < kAdcDspFftWindowSamples || kAdcDspFftWindowSamples == 0U) {
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t kHalfBins = kAdcDspFftWindowSamples / 2U;
|
||||
if (kHalfBins < 2U) {
|
||||
return;
|
||||
}
|
||||
float best_power = 0.0f;
|
||||
uint16_t best_bin = 0U;
|
||||
const uint32_t effective_downsample = std::max<uint32_t>(1U, static_cast<uint32_t>(adc_dsp_fft_downsample_));
|
||||
const float probe_sr = static_cast<float>(std::max(1U, _config.sample_rate)) / static_cast<float>(effective_downsample);
|
||||
const uint16_t ignore_low = std::min<uint16_t>(adc_fft_ignore_low_bin_, static_cast<uint16_t>(kHalfBins - 1U));
|
||||
const uint16_t ignore_high = std::min<uint16_t>(adc_fft_ignore_high_bin_, static_cast<uint16_t>(kHalfBins));
|
||||
const uint16_t upper_limit = (ignore_high >= kHalfBins) ? 1U : static_cast<uint16_t>(kHalfBins - ignore_high);
|
||||
|
||||
for (size_t i = 0U; i < kAdcDspFftWindowSamples; ++i) {
|
||||
const size_t src_idx = (adc_dsp_fft_head_ + i) % kAdcDspFftWindowSamples;
|
||||
const float phase = static_cast<float>(i) / static_cast<float>(kAdcDspFftWindowSamples - 1U);
|
||||
const float sample = adc_dsp_fft_buffer_[src_idx] * (0.5f - 0.5f * std::cos(kTwoPi * phase));
|
||||
adc_dsp_fft_complex_buffer_[i * 2U] = sample;
|
||||
adc_dsp_fft_complex_buffer_[i * 2U + 1U] = 0.0f;
|
||||
}
|
||||
|
||||
if (adc_dsp_fft_probe_backend_ready_) {
|
||||
esp_err_t ret = dsps_fft2r_fc32(adc_dsp_fft_complex_buffer_, static_cast<int>(kAdcDspFftWindowSamples));
|
||||
if (ret != ESP_OK) {
|
||||
Serial.printf("[AudioEngine] dsps_fft2r_fc32 failed: %d\n", ret);
|
||||
return;
|
||||
}
|
||||
ret = dsps_bit_rev_fc32(adc_dsp_fft_complex_buffer_, static_cast<int>(kAdcDspFftWindowSamples));
|
||||
if (ret != ESP_OK) {
|
||||
Serial.printf("[AudioEngine] dsps_bit_rev_fc32 failed: %d\n", ret);
|
||||
return;
|
||||
}
|
||||
ret = dsps_cplx2reC_fc32(adc_dsp_fft_complex_buffer_, static_cast<int>(kAdcDspFftWindowSamples));
|
||||
if (ret != ESP_OK) {
|
||||
Serial.printf("[AudioEngine] dsps_cplx2reC_fc32 failed: %d\n", ret);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t bin = 1U; bin < kHalfBins; ++bin) {
|
||||
if (bin <= ignore_low || bin >= upper_limit) {
|
||||
continue;
|
||||
}
|
||||
const float re = adc_dsp_fft_complex_buffer_[bin * 2U];
|
||||
const float im = adc_dsp_fft_complex_buffer_[bin * 2U + 1U];
|
||||
const float power = (re * re) + (im * im);
|
||||
if (power > best_power) {
|
||||
best_power = power;
|
||||
best_bin = static_cast<uint16_t>(bin);
|
||||
}
|
||||
}
|
||||
|
||||
metrics_.adc_fft_peak_bin = best_bin;
|
||||
metrics_.adc_fft_peak_magnitude = std::sqrt(best_power);
|
||||
metrics_.adc_fft_peak_freq_hz = best_bin == 0U ? 0.0f
|
||||
: (static_cast<float>(best_bin) *
|
||||
(probe_sr / static_cast<float>(kAdcDspFftWindowSamples)));
|
||||
}
|
||||
|
||||
int16_t AudioEngine::processAdcSample(int16_t raw_sample) {
|
||||
float sample = static_cast<float>(raw_sample) * kDspAdcScale;
|
||||
if (!adc_dsp_chain_enabled_) {
|
||||
return clampInt16(sample * kDspPostGain * 32768.0f);
|
||||
}
|
||||
|
||||
sample = applyDcBlocker(sample);
|
||||
sample = applyFirNoiseReduction(sample);
|
||||
appendAdcFftSample(sample);
|
||||
return applyBiquadChain(sample);
|
||||
}
|
||||
|
||||
bool AudioEngine::begin(const AudioConfig& config) {
|
||||
end();
|
||||
_config = config;
|
||||
adc_capture_pin_ = config.capture_adc_pin;
|
||||
use_adc_capture_ = (adc_capture_pin_ >= 0);
|
||||
|
||||
if (use_adc_capture_) {
|
||||
if (adc_capture_pin_ < 0 || adc_capture_pin_ > 39) {
|
||||
Serial.printf("[AudioEngine] invalid ADC pin for capture: %d\n", adc_capture_pin_);
|
||||
return false;
|
||||
}
|
||||
|
||||
pinMode(adc_capture_pin_, INPUT);
|
||||
analogReadResolution(12);
|
||||
analogSetPinAttenuation(adc_capture_pin_, ADC_11db);
|
||||
adc_capture_sample_interval_us_ = std::max(1U, 1000000U / _config.sample_rate);
|
||||
} else {
|
||||
adc_capture_sample_interval_us_ = 0U;
|
||||
}
|
||||
updateAdcDspConfig(config);
|
||||
next_adc_capture_us_ = 0U;
|
||||
|
||||
const bool full_duplex = (_config.enable_capture && features_.has_full_duplex_i2s);
|
||||
const audio_tools::RxTxMode mode = full_duplex ? audio_tools::RXTX_MODE : audio_tools::TX_MODE;
|
||||
@@ -201,6 +518,7 @@ void AudioEngine::end() {
|
||||
if (!driver_installed_) {
|
||||
return;
|
||||
}
|
||||
deinitAdcFftDspBackend();
|
||||
stopTask();
|
||||
stopDialTone();
|
||||
stopPlaybackFile();
|
||||
@@ -309,8 +627,13 @@ bool AudioEngine::requestCapture(CaptureClient client) {
|
||||
}
|
||||
|
||||
portENTER_CRITICAL(&capture_lock_);
|
||||
const bool was_active = capture_active_;
|
||||
capture_clients_mask_ = static_cast<uint8_t>(capture_clients_mask_ | bit);
|
||||
capture_active_ = (capture_clients_mask_ != 0U);
|
||||
if (capture_active_ && !was_active && use_adc_capture_ && adc_dsp_chain_enabled_) {
|
||||
resetAdcDspState();
|
||||
next_adc_capture_us_ = 0U;
|
||||
}
|
||||
portEXIT_CRITICAL(&capture_lock_);
|
||||
return true;
|
||||
}
|
||||
@@ -334,6 +657,9 @@ size_t AudioEngine::readCaptureFrame(int16_t* dst, size_t samples) {
|
||||
if (!capture_active_ || !driver_installed_ || dst == nullptr || samples == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (use_adc_capture_) {
|
||||
return captureFromAdc(dst, samples, true);
|
||||
}
|
||||
if (!lockI2s()) {
|
||||
return 0;
|
||||
}
|
||||
@@ -366,6 +692,9 @@ size_t AudioEngine::readCaptureFrameNonBlocking(int16_t* dst, size_t samples) {
|
||||
if (!capture_active_ || !driver_installed_ || dst == nullptr || samples == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (use_adc_capture_) {
|
||||
return captureFromAdc(dst, samples, false);
|
||||
}
|
||||
if (!lockI2s()) {
|
||||
return 0;
|
||||
}
|
||||
@@ -414,6 +743,51 @@ void AudioEngine::stopCapture() {
|
||||
releaseCapture(CAPTURE_CLIENT_GENERIC);
|
||||
}
|
||||
|
||||
size_t AudioEngine::captureFromAdc(int16_t* dst, size_t samples, bool blocking) {
|
||||
if (dst == nullptr || samples == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint32_t start_ms = millis();
|
||||
metrics_.frames_requested += static_cast<uint32_t>(samples);
|
||||
size_t captured = 0;
|
||||
|
||||
if (next_adc_capture_us_ == 0U) {
|
||||
next_adc_capture_us_ = static_cast<uint64_t>(micros());
|
||||
}
|
||||
|
||||
if (adc_capture_sample_interval_us_ == 0U) {
|
||||
adc_capture_sample_interval_us_ = 1000000U / std::max(1U, _config.sample_rate);
|
||||
}
|
||||
|
||||
while (captured < samples) {
|
||||
const uint64_t target_us = next_adc_capture_us_;
|
||||
const uint64_t now_us = micros();
|
||||
if (!blocking && now_us < target_us) {
|
||||
break;
|
||||
}
|
||||
if (blocking && now_us < target_us) {
|
||||
delayMicroseconds(static_cast<unsigned long>(target_us - now_us));
|
||||
}
|
||||
|
||||
const int raw = analogRead(adc_capture_pin_);
|
||||
const int16_t centered = static_cast<int16_t>(raw - kAdcMidScale);
|
||||
dst[captured] = processAdcSample(centered);
|
||||
++captured;
|
||||
next_adc_capture_us_ = target_us + adc_capture_sample_interval_us_;
|
||||
}
|
||||
|
||||
metrics_.frames_read += static_cast<uint32_t>(captured);
|
||||
if (captured < samples) {
|
||||
metrics_.underrun_count++;
|
||||
metrics_.drop_frames += static_cast<uint32_t>(samples - captured);
|
||||
}
|
||||
|
||||
metrics_.last_latency_ms = millis() - start_ms;
|
||||
metrics_.max_latency_ms = std::max(metrics_.max_latency_ms, metrics_.last_latency_ms);
|
||||
return captured;
|
||||
}
|
||||
|
||||
bool AudioEngine::startDialTone() {
|
||||
if (!driver_installed_) {
|
||||
return false;
|
||||
|
||||
@@ -20,7 +20,13 @@ struct AudioConfig {
|
||||
int ws_pin = 25;
|
||||
int data_out_pin = 26;
|
||||
int data_in_pin = 35;
|
||||
int capture_adc_pin = -1;
|
||||
bool enable_capture = true;
|
||||
bool adc_dsp_enabled = true;
|
||||
bool adc_fft_enabled = true;
|
||||
uint8_t adc_dsp_fft_downsample = 2U;
|
||||
uint16_t adc_fft_ignore_low_bin = 1U;
|
||||
uint16_t adc_fft_ignore_high_bin = 1U;
|
||||
uint8_t dma_buf_count = 8;
|
||||
uint16_t dma_buf_len = 256;
|
||||
};
|
||||
@@ -32,6 +38,10 @@ struct AudioRuntimeMetrics {
|
||||
uint32_t underrun_count = 0;
|
||||
uint32_t last_latency_ms = 0;
|
||||
uint32_t max_latency_ms = 0;
|
||||
uint16_t adc_fft_peak_bin = 0;
|
||||
uint16_t adc_fft_probe_rate_hz = 0;
|
||||
float adc_fft_peak_freq_hz = 0.0f;
|
||||
float adc_fft_peak_magnitude = 0.0f;
|
||||
};
|
||||
|
||||
AudioConfig defaultAudioConfigForProfile(BoardProfile profile);
|
||||
@@ -70,6 +80,17 @@ public:
|
||||
private:
|
||||
static size_t activeChannelCount(i2s_channel_fmt_t channel_format);
|
||||
static void audioTaskFn(void* arg);
|
||||
size_t captureFromAdc(int16_t* dst, size_t samples, bool blocking);
|
||||
void initAdcDspChain(uint32_t sample_rate_hz);
|
||||
int16_t processAdcSample(int16_t raw_sample);
|
||||
void resetAdcDspState();
|
||||
float applyDcBlocker(float sample);
|
||||
float applyFirNoiseReduction(float sample);
|
||||
int16_t applyBiquadChain(float sample);
|
||||
void appendAdcFftSample(float sample);
|
||||
void runAdcFftProbe();
|
||||
void initAdcFftDspBackend();
|
||||
void deinitAdcFftDspBackend();
|
||||
void startTask();
|
||||
void stopTask();
|
||||
bool lockI2s() const;
|
||||
@@ -78,6 +99,7 @@ private:
|
||||
void stopPlaybackFile();
|
||||
bool prepareWavPlayback(File& file, const char* path);
|
||||
bool streamPlaybackChunk();
|
||||
void updateAdcDspConfig(const AudioConfig& cfg);
|
||||
|
||||
bool driver_installed_ = false;
|
||||
bool capture_active_ = false;
|
||||
@@ -97,6 +119,43 @@ private:
|
||||
AudioConfig _config;
|
||||
FeatureMatrix features_;
|
||||
AudioRuntimeMetrics metrics_;
|
||||
int adc_capture_pin_ = -1;
|
||||
uint32_t adc_capture_sample_interval_us_ = 0;
|
||||
uint64_t next_adc_capture_us_ = 0;
|
||||
bool use_adc_capture_ = false;
|
||||
bool adc_dsp_chain_enabled_ = false;
|
||||
bool adc_fft_enabled_ = false;
|
||||
uint8_t adc_dsp_fft_downsample_ = kAdcDspDefaultFftDownsample;
|
||||
uint16_t adc_fft_ignore_low_bin_ = 1U;
|
||||
uint16_t adc_fft_ignore_high_bin_ = 1U;
|
||||
static constexpr uint32_t kAdcDspDefaultSampleRateHz = 16000U;
|
||||
static constexpr uint8_t kAdcDspDefaultFftDownsample = 2U;
|
||||
float adc_dsp_prev_input_ = 0.0f;
|
||||
float adc_dsp_prev_output_ = 0.0f;
|
||||
float adc_dsp_fir_state_[5U] = {0.0f};
|
||||
uint8_t adc_dsp_fir_pos_ = 0U;
|
||||
float adc_dsp_biquad_hp_b0_ = 1.0f;
|
||||
float adc_dsp_biquad_hp_b1_ = 0.0f;
|
||||
float adc_dsp_biquad_hp_b2_ = 0.0f;
|
||||
float adc_dsp_biquad_hp_a1_ = 0.0f;
|
||||
float adc_dsp_biquad_hp_a2_ = 0.0f;
|
||||
float adc_dsp_biquad_hp_z1_ = 0.0f;
|
||||
float adc_dsp_biquad_hp_z2_ = 0.0f;
|
||||
float adc_dsp_biquad_lp_b0_ = 1.0f;
|
||||
float adc_dsp_biquad_lp_b1_ = 0.0f;
|
||||
float adc_dsp_biquad_lp_b2_ = 0.0f;
|
||||
float adc_dsp_biquad_lp_a1_ = 0.0f;
|
||||
float adc_dsp_biquad_lp_a2_ = 0.0f;
|
||||
float adc_dsp_biquad_lp_z1_ = 0.0f;
|
||||
float adc_dsp_biquad_lp_z2_ = 0.0f;
|
||||
static constexpr size_t kAdcDspFftWindowSamples = 64U;
|
||||
float adc_dsp_fft_buffer_[kAdcDspFftWindowSamples] = {0.0f};
|
||||
uint8_t adc_dsp_fft_head_ = 0U;
|
||||
uint8_t adc_dsp_fft_fill_ = 0U;
|
||||
uint8_t adc_dsp_fft_decimator_ = 0U;
|
||||
float adc_dsp_fft_complex_buffer_[kAdcDspFftWindowSamples * 2U] = {0.0f};
|
||||
bool adc_dsp_fft_probe_enabled_ = false;
|
||||
bool adc_dsp_fft_probe_backend_ready_ = false;
|
||||
audio_tools::I2SStream i2s_stream_;
|
||||
audio_tools::WAVDecoder wav_decoder_;
|
||||
audio_tools::EncodedAudioStream wav_stream_;
|
||||
|
||||
@@ -100,9 +100,8 @@ bool Es8388Driver::setRoute(const String& route) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Current hardware path uses DAC->mixer->line output for both RTC and BT.
|
||||
// Keep route as metadata and ensure output path is enabled.
|
||||
if (route_ == "bluetooth" || route_ == "rtc") {
|
||||
// Keep route as metadata and ensure output path is enabled for RTC.
|
||||
if (route_ == "rtc") {
|
||||
return writeReg(0x26, 0x00) && writeReg(0x27, 0x90) && writeReg(0x2A, 0x90) &&
|
||||
writeReg(0x04, 0x3C);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,116 +0,0 @@
|
||||
#ifndef BLUETOOTH_BLUETOOTH_MANAGER_H
|
||||
#define BLUETOOTH_BLUETOOTH_MANAGER_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <functional>
|
||||
|
||||
#include "core/PlatformProfile.h"
|
||||
|
||||
class AudioEngine;
|
||||
|
||||
class BluetoothManager {
|
||||
public:
|
||||
BluetoothManager();
|
||||
bool begin(BoardProfile profile);
|
||||
void setAudioBridge(AudioEngine* audio);
|
||||
bool connect(const char* mac);
|
||||
bool disconnect();
|
||||
bool isConnected() const;
|
||||
bool startHFP();
|
||||
bool stopHFP();
|
||||
bool startBLE();
|
||||
bool stopBLE();
|
||||
void tick();
|
||||
bool setDiscoverable(bool enabled);
|
||||
bool setAutoReconnectEnabled(bool enabled);
|
||||
bool autoReconnectEnabled() const;
|
||||
void suspendReconnect(uint32_t duration_ms);
|
||||
bool dial(const String& number);
|
||||
bool redial();
|
||||
bool answerCall();
|
||||
bool hangupCall();
|
||||
bool queryCurrentCalls();
|
||||
bool syncPbapContacts();
|
||||
void logStatus() const;
|
||||
void statusToJson(JsonObject obj) const;
|
||||
void setSecurity(bool enabled);
|
||||
void setBleCommandHandler(std::function<String(const String&)> handler);
|
||||
void publishBleStatus();
|
||||
String executeBleCommand(const String& cmd);
|
||||
void handleHfpEvent(int event, const void* param);
|
||||
void onHfpIncomingAudio(const uint8_t* buf, uint32_t len);
|
||||
uint32_t onHfpOutgoingAudio(uint8_t* buf, uint32_t len);
|
||||
void onBleClientConnected(bool connected);
|
||||
void onGapAuthComplete(const uint8_t remote_bda[6], bool success);
|
||||
void onGapAclDisconnected(const uint8_t remote_bda[6], uint16_t reason);
|
||||
bool isSecurityEnabled() const;
|
||||
bool isHfpActive() const;
|
||||
bool isBleActive() const;
|
||||
bool isDiscoverable() const;
|
||||
bool isPbapSupported() const;
|
||||
String callState() const;
|
||||
String peerMac() const;
|
||||
|
||||
private:
|
||||
bool ensureBtStackReady();
|
||||
bool ensureHfpClientReady();
|
||||
bool requireCallControlReady(const char* operation);
|
||||
bool requestHfpConnect(const char* reason);
|
||||
bool issueDialRequest(const String& dialed, const char* event_name);
|
||||
bool loadBondedPeerFromStack();
|
||||
bool parseMac(const String& mac, uint8_t out[6]) const;
|
||||
String formatMac(const uint8_t* mac) const;
|
||||
void applyDiscoverablePolicy();
|
||||
void onHfpAudioStateChanged(bool connected);
|
||||
void requestAudioIfNeeded(const char* reason);
|
||||
bool hasAnyBluetoothClientConnected() const;
|
||||
|
||||
FeatureMatrix features_;
|
||||
bool stack_ready_;
|
||||
bool hfp_initialized_;
|
||||
bool hfp_requested_;
|
||||
bool auto_reconnect_enabled_;
|
||||
bool ble_stack_initialized_;
|
||||
bool ble_service_ready_;
|
||||
bool ble_client_connected_;
|
||||
bool connected_;
|
||||
bool hfp_active_;
|
||||
bool slc_connected_;
|
||||
bool call_setup_active_;
|
||||
bool ble_active_;
|
||||
bool discoverable_;
|
||||
bool security_enabled_;
|
||||
bool pbap_supported_;
|
||||
bool pbap_synced_;
|
||||
bool hfp_data_callback_registered_;
|
||||
bool hfp_audio_link_up_;
|
||||
String peer_mac_;
|
||||
uint8_t peer_addr_[6];
|
||||
bool peer_addr_valid_;
|
||||
bool hfp_connect_inflight_;
|
||||
uint8_t hfp_reconnect_attempts_;
|
||||
uint32_t next_hfp_reconnect_ms_;
|
||||
uint32_t reconnect_suspended_until_ms_;
|
||||
uint32_t slc_wait_deadline_ms_;
|
||||
uint32_t next_audio_retry_ms_;
|
||||
uint32_t next_pending_dial_retry_ms_;
|
||||
volatile bool pending_status_publish_;
|
||||
volatile bool pending_discoverable_policy_;
|
||||
String call_state_;
|
||||
String pending_dial_number_;
|
||||
String last_dialed_number_;
|
||||
String pbap_last_error_;
|
||||
String last_hfp_event_;
|
||||
String last_ble_event_;
|
||||
String last_error_;
|
||||
String ble_last_command_;
|
||||
String ble_last_response_;
|
||||
uint32_t sco_rx_bytes_;
|
||||
uint32_t sco_tx_bytes_;
|
||||
uint32_t last_sco_activity_ms_;
|
||||
AudioEngine* audio_bridge_;
|
||||
std::function<String(const String&)> ble_command_handler_;
|
||||
};
|
||||
|
||||
#endif // BLUETOOTH_BLUETOOTH_MANAGER_H
|
||||
@@ -1,32 +0,0 @@
|
||||
#ifndef BLUETOOTH_I_BLUETOOTH_BACKEND_H
|
||||
#define BLUETOOTH_I_BLUETOOTH_BACKEND_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "core/PlatformProfile.h"
|
||||
|
||||
struct PbapContact {
|
||||
String display_name;
|
||||
String phone_number;
|
||||
};
|
||||
|
||||
class IBluetoothBackend {
|
||||
public:
|
||||
virtual ~IBluetoothBackend() = default;
|
||||
|
||||
virtual bool begin(BoardProfile profile) = 0;
|
||||
virtual bool connect(const String& mac) = 0;
|
||||
virtual bool disconnect() = 0;
|
||||
|
||||
virtual bool setDiscoverable(bool enabled) = 0;
|
||||
virtual bool dial(const String& number) = 0;
|
||||
virtual bool redial() = 0;
|
||||
virtual bool answer() = 0;
|
||||
virtual bool hangup() = 0;
|
||||
virtual bool queryCalls() = 0;
|
||||
|
||||
virtual bool syncPbap() = 0;
|
||||
virtual bool isPbapSupported() const = 0;
|
||||
};
|
||||
|
||||
#endif // BLUETOOTH_I_BLUETOOTH_BACKEND_H
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "core/PlatformProfile.h"
|
||||
|
||||
namespace {
|
||||
constexpr const char* kPinsNs = "a252-pins";
|
||||
constexpr const char* kAudioNs = "a252-audio";
|
||||
@@ -26,7 +28,26 @@ bool loadJsonArray(const String& raw, JsonDocument& doc) {
|
||||
} // namespace
|
||||
|
||||
A252PinsConfig A252ConfigStore::defaultPins() {
|
||||
return A252PinsConfig{};
|
||||
A252PinsConfig cfg;
|
||||
if (detectBoardProfile() == BoardProfile::ESP32_S3) {
|
||||
cfg.i2s_bck = 17;
|
||||
cfg.i2s_ws = 18;
|
||||
cfg.i2s_dout = 21;
|
||||
cfg.i2s_din = 39;
|
||||
cfg.es8388_sda = -1;
|
||||
cfg.es8388_scl = -1;
|
||||
cfg.slic_rm = 32;
|
||||
cfg.slic_fr = 5;
|
||||
cfg.slic_shk = 23;
|
||||
cfg.slic_pd = 14;
|
||||
cfg.slic_adc_in = 34;
|
||||
cfg.hook_active_high = true;
|
||||
cfg.pcm_flt = 25;
|
||||
cfg.pcm_demp = 26;
|
||||
cfg.pcm_xsmt = 27;
|
||||
cfg.pcm_fmt = 33;
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
A252AudioConfig A252ConfigStore::defaultAudio() {
|
||||
@@ -57,7 +78,12 @@ bool A252ConfigStore::loadPins(A252PinsConfig& out) {
|
||||
out.slic_shk = prefs.getInt("slic_shk", out.slic_shk);
|
||||
out.slic_line = prefs.getInt("slic_line", out.slic_line);
|
||||
out.slic_pd = prefs.getInt("slic_pd", out.slic_pd);
|
||||
out.slic_adc_in = prefs.getInt("slic_adc_in", out.slic_adc_in);
|
||||
out.hook_active_high = prefs.getBool("hook_hi", out.hook_active_high);
|
||||
out.pcm_flt = prefs.getInt("pcm_flt", out.pcm_flt);
|
||||
out.pcm_demp = prefs.getInt("pcm_demp", out.pcm_demp);
|
||||
out.pcm_xsmt = prefs.getInt("pcm_xsmt", out.pcm_xsmt);
|
||||
out.pcm_fmt = prefs.getInt("pcm_fmt", out.pcm_fmt);
|
||||
prefs.end();
|
||||
|
||||
String error;
|
||||
@@ -98,7 +124,12 @@ bool A252ConfigStore::savePins(const A252PinsConfig& cfg, String* error) {
|
||||
prefs.putInt("slic_shk", cfg.slic_shk);
|
||||
prefs.putInt("slic_line", cfg.slic_line);
|
||||
prefs.putInt("slic_pd", cfg.slic_pd);
|
||||
prefs.putInt("slic_adc_in", cfg.slic_adc_in);
|
||||
prefs.putBool("hook_hi", cfg.hook_active_high);
|
||||
prefs.putInt("pcm_flt", cfg.pcm_flt);
|
||||
prefs.putInt("pcm_demp", cfg.pcm_demp);
|
||||
prefs.putInt("pcm_xsmt", cfg.pcm_xsmt);
|
||||
prefs.putInt("pcm_fmt", cfg.pcm_fmt);
|
||||
prefs.end();
|
||||
return true;
|
||||
}
|
||||
@@ -276,15 +307,21 @@ bool A252ConfigStore::validatePins(const A252PinsConfig& cfg, String& error) {
|
||||
cfg.i2s_ws,
|
||||
cfg.i2s_dout,
|
||||
cfg.i2s_din,
|
||||
cfg.es8388_sda,
|
||||
cfg.es8388_scl,
|
||||
cfg.slic_rm,
|
||||
cfg.slic_fr,
|
||||
cfg.slic_shk,
|
||||
cfg.slic_pd,
|
||||
cfg.pcm_flt,
|
||||
cfg.pcm_demp,
|
||||
cfg.pcm_xsmt,
|
||||
cfg.pcm_fmt,
|
||||
cfg.slic_adc_in,
|
||||
};
|
||||
|
||||
for (int pin : required_pins) {
|
||||
if (pin < 0) {
|
||||
continue;
|
||||
}
|
||||
if (pin < 0 || pin > 39) {
|
||||
error = "invalid_pin_range";
|
||||
return false;
|
||||
@@ -296,6 +333,43 @@ bool A252ConfigStore::validatePins(const A252PinsConfig& cfg, String& error) {
|
||||
used.push_back(pin);
|
||||
}
|
||||
|
||||
if (detectBoardProfile() == BoardProfile::ESP32_A252) {
|
||||
if (cfg.es8388_sda < 0 || cfg.es8388_scl < 0) {
|
||||
error = "invalid_pin_range";
|
||||
return false;
|
||||
}
|
||||
if (cfg.es8388_sda == cfg.es8388_scl) {
|
||||
error = "pin_conflict";
|
||||
return false;
|
||||
}
|
||||
if (cfg.es8388_sda < 0 || cfg.es8388_sda > 39 || cfg.es8388_scl < 0 || cfg.es8388_scl > 39) {
|
||||
error = "invalid_pin_range";
|
||||
return false;
|
||||
}
|
||||
if (std::find(used.begin(), used.end(), cfg.es8388_sda) != used.end() ||
|
||||
std::find(used.begin(), used.end(), cfg.es8388_scl) != used.end()) {
|
||||
error = "pin_conflict";
|
||||
return false;
|
||||
}
|
||||
used.push_back(cfg.es8388_sda);
|
||||
used.push_back(cfg.es8388_scl);
|
||||
} else {
|
||||
if (cfg.es8388_sda >= 0) {
|
||||
if (cfg.es8388_sda > 39 || std::find(used.begin(), used.end(), cfg.es8388_sda) != used.end()) {
|
||||
error = cfg.es8388_sda > 39 ? "invalid_pin_range" : "pin_conflict";
|
||||
return false;
|
||||
}
|
||||
used.push_back(cfg.es8388_sda);
|
||||
}
|
||||
if (cfg.es8388_scl >= 0) {
|
||||
if (cfg.es8388_scl > 39 || std::find(used.begin(), used.end(), cfg.es8388_scl) != used.end()) {
|
||||
error = cfg.es8388_scl > 39 ? "invalid_pin_range" : "pin_conflict";
|
||||
return false;
|
||||
}
|
||||
used.push_back(cfg.es8388_scl);
|
||||
}
|
||||
}
|
||||
|
||||
// Optional legacy line-enable pin, retired by default (-1).
|
||||
if (cfg.slic_line != -1) {
|
||||
if (cfg.slic_line < 0 || cfg.slic_line > 39) {
|
||||
@@ -328,7 +402,7 @@ bool A252ConfigStore::validateAudio(const A252AudioConfig& cfg, String& error) {
|
||||
}
|
||||
|
||||
const String route = cfg.route;
|
||||
if (!(route == "rtc" || route == "bluetooth" || route == "none")) {
|
||||
if (!(route == "rtc" || route == "none")) {
|
||||
error = "invalid_route";
|
||||
return false;
|
||||
}
|
||||
@@ -369,7 +443,14 @@ void A252ConfigStore::pinsToJson(const A252PinsConfig& cfg, JsonObject obj) {
|
||||
slic["shk"] = cfg.slic_shk;
|
||||
slic["line"] = cfg.slic_line;
|
||||
slic["pd"] = cfg.slic_pd;
|
||||
slic["adc_in"] = cfg.slic_adc_in;
|
||||
slic["hook_active_high"] = cfg.hook_active_high;
|
||||
|
||||
JsonObject pcm = obj["pcm"].to<JsonObject>();
|
||||
pcm["flt"] = cfg.pcm_flt;
|
||||
pcm["demp"] = cfg.pcm_demp;
|
||||
pcm["xsmt"] = cfg.pcm_xsmt;
|
||||
pcm["fmt"] = cfg.pcm_fmt;
|
||||
}
|
||||
|
||||
void A252ConfigStore::audioToJson(const A252AudioConfig& cfg, JsonObject obj) {
|
||||
|
||||
@@ -21,13 +21,24 @@ struct A252PinsConfig {
|
||||
int slic_shk = 23;
|
||||
int slic_line = -1;
|
||||
int slic_pd = 19;
|
||||
int slic_adc_in = -1;
|
||||
bool hook_active_high = true;
|
||||
|
||||
int pcm_flt = -1;
|
||||
int pcm_demp = -1;
|
||||
int pcm_xsmt = -1;
|
||||
int pcm_fmt = -1;
|
||||
};
|
||||
|
||||
struct A252AudioConfig {
|
||||
uint32_t sample_rate = 16000;
|
||||
uint8_t bits_per_sample = 16;
|
||||
bool enable_capture = true;
|
||||
bool adc_dsp_enabled = true;
|
||||
bool adc_fft_enabled = true;
|
||||
uint8_t adc_dsp_fft_downsample = 2U;
|
||||
uint16_t adc_fft_ignore_low_bin = 1U;
|
||||
uint16_t adc_fft_ignore_high_bin = 1U;
|
||||
uint8_t volume = 90;
|
||||
bool mute = false;
|
||||
String route = "rtc";
|
||||
|
||||
@@ -12,24 +12,15 @@ FeatureMatrix getFeatureMatrix(BoardProfile profile) {
|
||||
switch (profile) {
|
||||
case BoardProfile::ESP32_A252:
|
||||
return FeatureMatrix{
|
||||
.has_bt_classic = true,
|
||||
.has_hfp = true,
|
||||
.has_full_duplex_i2s = true,
|
||||
.has_ble_control = true,
|
||||
};
|
||||
case BoardProfile::ESP32_S3:
|
||||
return FeatureMatrix{
|
||||
.has_bt_classic = false,
|
||||
.has_hfp = false,
|
||||
.has_full_duplex_i2s = false,
|
||||
.has_ble_control = true,
|
||||
};
|
||||
default:
|
||||
return FeatureMatrix{
|
||||
.has_bt_classic = false,
|
||||
.has_hfp = false,
|
||||
.has_full_duplex_i2s = false,
|
||||
.has_ble_control = false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,7 @@ enum class BoardProfile : uint8_t {
|
||||
};
|
||||
|
||||
struct FeatureMatrix {
|
||||
bool has_bt_classic;
|
||||
bool has_hfp;
|
||||
bool has_full_duplex_i2s;
|
||||
bool has_ble_control;
|
||||
};
|
||||
|
||||
BoardProfile detectBoardProfile();
|
||||
|
||||
+135
-12
@@ -11,7 +11,10 @@
|
||||
#include "props/EspNowBridge.h"
|
||||
#include "slic/Ks0835SlicController.h"
|
||||
#include "telephony/TelephonyService.h"
|
||||
#include "visual/ScopeDisplay.h"
|
||||
#include "wifi/WifiManagerInstance.h"
|
||||
#include "usb/UsbHostRuntime.h"
|
||||
#include "usb/UsbMassStorageRuntime.h"
|
||||
|
||||
#ifndef UNIT_TEST
|
||||
namespace {
|
||||
@@ -21,8 +24,8 @@ constexpr int kAudioAmpEnablePin = 21;
|
||||
constexpr char kBootLogTag[] = "RTC_BOOT";
|
||||
constexpr bool kPrintHelpOnBoot = false;
|
||||
|
||||
BoardProfile g_profile = BoardProfile::ESP32_A252;
|
||||
FeatureMatrix g_features = getFeatureMatrix(BoardProfile::ESP32_A252);
|
||||
BoardProfile g_profile = detectBoardProfile();
|
||||
FeatureMatrix g_features = getFeatureMatrix(g_profile);
|
||||
|
||||
A252PinsConfig g_pins_cfg = A252ConfigStore::defaultPins();
|
||||
A252AudioConfig g_audio_cfg = A252ConfigStore::defaultAudio();
|
||||
@@ -35,6 +38,7 @@ Es8388Driver g_codec;
|
||||
TelephonyService g_telephony;
|
||||
EspNowBridge g_espnow;
|
||||
CommandDispatcher g_dispatcher;
|
||||
ScopeDisplay g_scope_display;
|
||||
String g_serial_line;
|
||||
|
||||
DispatchResponse makeResponse(bool ok, const String& code) {
|
||||
@@ -308,12 +312,28 @@ AudioConfig buildI2sConfig(const A252PinsConfig& pins_cfg, const A252AudioConfig
|
||||
cfg.ws_pin = pins_cfg.i2s_ws;
|
||||
cfg.data_out_pin = pins_cfg.i2s_dout;
|
||||
cfg.data_in_pin = pins_cfg.i2s_din;
|
||||
cfg.capture_adc_pin = pins_cfg.slic_adc_in;
|
||||
cfg.enable_capture = audio_cfg.enable_capture;
|
||||
cfg.dma_buf_count = 8;
|
||||
cfg.dma_buf_len = 256;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
void applyPcm5102ControlPins(const A252PinsConfig& pins_cfg) {
|
||||
auto apply = [](int pin, int value) {
|
||||
if (pin < 0) {
|
||||
return;
|
||||
}
|
||||
pinMode(pin, OUTPUT);
|
||||
digitalWrite(pin, value);
|
||||
};
|
||||
|
||||
apply(pins_cfg.pcm_flt, LOW);
|
||||
apply(pins_cfg.pcm_demp, LOW);
|
||||
apply(pins_cfg.pcm_xsmt, HIGH);
|
||||
apply(pins_cfg.pcm_fmt, LOW);
|
||||
}
|
||||
|
||||
bool applyHardwareConfig() {
|
||||
const SlicPins slic_pins = {
|
||||
.pin_rm = static_cast<uint8_t>(g_pins_cfg.slic_rm),
|
||||
@@ -328,22 +348,26 @@ bool applyHardwareConfig() {
|
||||
g_slic.setPowerDown(false);
|
||||
g_slic.setRing(false);
|
||||
|
||||
const bool codec_ok = g_codec.begin(g_pins_cfg.es8388_sda, g_pins_cfg.es8388_scl);
|
||||
g_codec.setVolume(g_audio_cfg.volume);
|
||||
g_codec.setMute(g_audio_cfg.mute);
|
||||
g_codec.setRoute(g_audio_cfg.route);
|
||||
bool codec_ok = true;
|
||||
if (g_profile == BoardProfile::ESP32_A252) {
|
||||
codec_ok = g_codec.begin(g_pins_cfg.es8388_sda, g_pins_cfg.es8388_scl);
|
||||
g_codec.setVolume(g_audio_cfg.volume);
|
||||
g_codec.setMute(g_audio_cfg.mute);
|
||||
g_codec.setRoute(g_audio_cfg.route);
|
||||
}
|
||||
|
||||
applyPcm5102ControlPins(g_pins_cfg);
|
||||
const AudioConfig audio = buildI2sConfig(g_pins_cfg, g_audio_cfg);
|
||||
const bool audio_ok = g_audio.begin(audio);
|
||||
g_audio.resetMetrics();
|
||||
|
||||
g_telephony.begin(g_profile, g_slic, g_audio);
|
||||
g_telephony.setDialCallback([](const String& number) {
|
||||
Serial.printf("[Telephony] dial request ignored (BT disabled): %s\n", number.c_str());
|
||||
Serial.printf("[Telephony] dial callback disabled for routing: %s\n", number.c_str());
|
||||
return false;
|
||||
});
|
||||
g_telephony.setAnswerCallback([]() {
|
||||
Serial.println("[Telephony] answer request ignored (BT disabled)");
|
||||
Serial.println("[Telephony] answer callback disabled");
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -374,6 +398,9 @@ void appendAudioMetrics(JsonObject root) {
|
||||
audio["underrun"] = metrics.underrun_count;
|
||||
audio["drop"] = metrics.drop_frames;
|
||||
audio["latence_ms"] = metrics.last_latency_ms;
|
||||
audio["adc_fft_peak_bin"] = metrics.adc_fft_peak_bin;
|
||||
audio["adc_fft_peak_freq_hz"] = metrics.adc_fft_peak_freq_hz;
|
||||
audio["adc_fft_peak_mag"] = metrics.adc_fft_peak_magnitude;
|
||||
}
|
||||
|
||||
void fillStatusSnapshot(JsonObject root) {
|
||||
@@ -386,6 +413,12 @@ void fillStatusSnapshot(JsonObject root) {
|
||||
|
||||
appendAudioMetrics(root);
|
||||
|
||||
JsonObject scope = root["scope_display"].to<JsonObject>();
|
||||
scope["supported"] = g_scope_display.supported();
|
||||
scope["enabled"] = g_scope_display.enabled();
|
||||
scope["frequency"] = g_scope_display.frequency();
|
||||
scope["amplitude"] = g_scope_display.amplitude();
|
||||
|
||||
JsonObject espnow = root["espnow"].to<JsonObject>();
|
||||
g_espnow.statusToJson(espnow);
|
||||
|
||||
@@ -432,9 +465,24 @@ bool applyPinsPatch(JsonVariantConst patch, A252PinsConfig& target, String& erro
|
||||
if (patch["slic"]["pd"].is<int>()) {
|
||||
next.slic_pd = patch["slic"]["pd"].as<int>();
|
||||
}
|
||||
if (patch["slic"]["adc_in"].is<int>()) {
|
||||
next.slic_adc_in = patch["slic"]["adc_in"].as<int>();
|
||||
}
|
||||
if (patch["slic"]["hook_active_high"].is<bool>()) {
|
||||
next.hook_active_high = patch["slic"]["hook_active_high"].as<bool>();
|
||||
}
|
||||
if (patch["pcm"]["flt"].is<int>()) {
|
||||
next.pcm_flt = patch["pcm"]["flt"].as<int>();
|
||||
}
|
||||
if (patch["pcm"]["demp"].is<int>()) {
|
||||
next.pcm_demp = patch["pcm"]["demp"].as<int>();
|
||||
}
|
||||
if (patch["pcm"]["xsmt"].is<int>()) {
|
||||
next.pcm_xsmt = patch["pcm"]["xsmt"].as<int>();
|
||||
}
|
||||
if (patch["pcm"]["fmt"].is<int>()) {
|
||||
next.pcm_fmt = patch["pcm"]["fmt"].as<int>();
|
||||
}
|
||||
|
||||
if (patch["i2s_bck"].is<int>()) {
|
||||
next.i2s_bck = patch["i2s_bck"].as<int>();
|
||||
@@ -468,9 +516,24 @@ bool applyPinsPatch(JsonVariantConst patch, A252PinsConfig& target, String& erro
|
||||
if (patch["slic_pd"].is<int>()) {
|
||||
next.slic_pd = patch["slic_pd"].as<int>();
|
||||
}
|
||||
if (patch["slic_adc_in"].is<int>()) {
|
||||
next.slic_adc_in = patch["slic_adc_in"].as<int>();
|
||||
}
|
||||
if (patch["hook_active_high"].is<bool>()) {
|
||||
next.hook_active_high = patch["hook_active_high"].as<bool>();
|
||||
}
|
||||
if (patch["pcm_flt"].is<int>()) {
|
||||
next.pcm_flt = patch["pcm_flt"].as<int>();
|
||||
}
|
||||
if (patch["pcm_demp"].is<int>()) {
|
||||
next.pcm_demp = patch["pcm_demp"].as<int>();
|
||||
}
|
||||
if (patch["pcm_xsmt"].is<int>()) {
|
||||
next.pcm_xsmt = patch["pcm_xsmt"].as<int>();
|
||||
}
|
||||
if (patch["pcm_fmt"].is<int>()) {
|
||||
next.pcm_fmt = patch["pcm_fmt"].as<int>();
|
||||
}
|
||||
|
||||
next.slic_line = -1;
|
||||
|
||||
@@ -629,6 +692,53 @@ void registerCommands() {
|
||||
return makeResponse(true, "CAPTURE_STOP");
|
||||
});
|
||||
|
||||
g_dispatcher.registerCommand("OSC_START", [](const String& args) {
|
||||
String first;
|
||||
String rest;
|
||||
uint16_t freq = 1200U;
|
||||
uint8_t amp = 48U;
|
||||
|
||||
if (!args.isEmpty()) {
|
||||
if (!splitFirstToken(args, first, rest)) {
|
||||
return makeResponse(false, "OSC_START invalid_args");
|
||||
}
|
||||
const int parsed_freq = first.toInt();
|
||||
if (parsed_freq > 0) {
|
||||
freq = static_cast<uint16_t>(parsed_freq);
|
||||
}
|
||||
if (!rest.isEmpty()) {
|
||||
const int parsed_amp = rest.toInt();
|
||||
if (parsed_amp > 0) {
|
||||
amp = static_cast<uint8_t>(parsed_amp);
|
||||
}
|
||||
}
|
||||
if (!g_scope_display.configure(freq, amp)) {
|
||||
return makeResponse(false, "OSC_START invalid_config");
|
||||
}
|
||||
}
|
||||
|
||||
if (!g_scope_display.begin()) {
|
||||
return makeResponse(false, "OSC_START not_supported");
|
||||
}
|
||||
g_scope_display.enable(true);
|
||||
return makeResponse(true, "OSC_START");
|
||||
});
|
||||
|
||||
g_dispatcher.registerCommand("OSC_STOP", [](const String&) {
|
||||
g_scope_display.enable(false);
|
||||
return makeResponse(true, "OSC_STOP");
|
||||
});
|
||||
|
||||
g_dispatcher.registerCommand("OSC_STATUS", [](const String&) {
|
||||
JsonDocument out;
|
||||
JsonObject scope = out.to<JsonObject>();
|
||||
scope["supported"] = g_scope_display.supported();
|
||||
scope["enabled"] = g_scope_display.enabled();
|
||||
scope["frequency"] = g_scope_display.frequency();
|
||||
scope["amplitude"] = g_scope_display.amplitude();
|
||||
return jsonResponse(out);
|
||||
});
|
||||
|
||||
g_dispatcher.registerCommand("PLAY", [](const String& args) {
|
||||
const String path = args.isEmpty() ? "/welcome.wav" : args;
|
||||
return makeResponse(g_audio.playFile(path.c_str()), "PLAY");
|
||||
@@ -779,9 +889,11 @@ void registerCommands() {
|
||||
}
|
||||
|
||||
g_audio_cfg = next;
|
||||
g_codec.setVolume(g_audio_cfg.volume);
|
||||
g_codec.setMute(g_audio_cfg.mute);
|
||||
g_codec.setRoute(g_audio_cfg.route);
|
||||
if (g_profile == BoardProfile::ESP32_A252) {
|
||||
g_codec.setVolume(g_audio_cfg.volume);
|
||||
g_codec.setMute(g_audio_cfg.mute);
|
||||
g_codec.setRoute(g_audio_cfg.route);
|
||||
}
|
||||
const bool audio_ok = g_audio.begin(buildI2sConfig(g_pins_cfg, g_audio_cfg));
|
||||
return makeResponse(audio_ok, "AUDIO_CONFIG_SET");
|
||||
});
|
||||
@@ -910,9 +1022,19 @@ void setup() {
|
||||
printf("[RTC_BL_PHONE] stdio lock warmup\n");
|
||||
fflush(stdout);
|
||||
|
||||
g_profile = BoardProfile::ESP32_A252;
|
||||
g_profile = detectBoardProfile();
|
||||
g_features = getFeatureMatrix(g_profile);
|
||||
|
||||
#ifdef USB_HOST_BOOT_ENABLE
|
||||
const bool usb_host = usb_host_runtime::enableHostPortPower();
|
||||
Serial.printf("[RTC_BL_PHONE] USB host bootstrap: %s\n", usb_host ? "ok" : "not available");
|
||||
#endif
|
||||
|
||||
#ifdef USB_MSC_BOOT_ENABLE
|
||||
const bool usb_msc = usb_msc_runtime::beginUsbMassStorage();
|
||||
Serial.printf("[RTC_BL_PHONE] USB MSC bootstrap: %s\n", usb_msc ? "ok" : "failed");
|
||||
#endif
|
||||
|
||||
A252ConfigStore::loadPins(g_pins_cfg);
|
||||
g_pins_cfg.slic_line = -1;
|
||||
A252ConfigStore::loadAudio(g_audio_cfg);
|
||||
@@ -939,6 +1061,7 @@ void setup() {
|
||||
|
||||
void loop() {
|
||||
g_telephony.tick();
|
||||
g_scope_display.tick();
|
||||
g_espnow.tick();
|
||||
pollSerial();
|
||||
delay(1);
|
||||
|
||||
@@ -219,7 +219,7 @@ void TelephonyService::commitDialBuffer(const char* reason) {
|
||||
|
||||
const String number = dial_buffer_;
|
||||
const bool ok = dial_callback_ ? dial_callback_(number) : false;
|
||||
last_dial_error_ = ok ? "" : "bt_dial_failed";
|
||||
last_dial_error_ = ok ? "" : "dial_failed";
|
||||
Serial.printf("[Telephony] dial_trigger reason=%s number=%s ok=%s\n",
|
||||
reason != nullptr ? reason : "unknown",
|
||||
number.c_str(),
|
||||
@@ -282,10 +282,9 @@ void TelephonyService::tick() {
|
||||
ring_phase_on_ = false;
|
||||
slic_->setRing(false);
|
||||
const bool answered = answer_callback_ ? answer_callback_() : false;
|
||||
// While transitioning from incoming ring to call answer, keep dial tone muted
|
||||
// even if BT answer fails transiently.
|
||||
// Keep dial tone muted while transitioning from incoming ring to call answer.
|
||||
suppress_dial_tone_ = true;
|
||||
last_dial_error_ = answered ? "" : "bt_answer_failed";
|
||||
last_dial_error_ = answered ? "" : "answer_failed";
|
||||
state_ = TelephonyState::OFF_HOOK;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace usb_host_runtime {
|
||||
|
||||
inline bool enableHostPortPower() {
|
||||
#if defined(ARDUINO_ESP32_S3_USB_OTG) && defined(USB_HOST_EN) && \
|
||||
defined(USB_HOST_POWER_VBUS) && defined(USB_HOST_POWER_OFF)
|
||||
usbHostEnable(true);
|
||||
usbHostPower(USB_HOST_POWER_VBUS);
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace usb_host_runtime
|
||||
@@ -0,0 +1,228 @@
|
||||
#include <Arduino.h>
|
||||
#include <USB.h>
|
||||
#include <USBMSC.h>
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <esp_partition.h>
|
||||
|
||||
#include "usb/UsbMassStorageRuntime.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kUsbMscBlockSize = 512;
|
||||
constexpr uint32_t kSectorBytes = 4096;
|
||||
constexpr char kUsbMscPartitionLabel[] = "usbmsc";
|
||||
|
||||
constexpr char kUsbMscVendorId[] = "ESP32";
|
||||
constexpr char kUsbMscProductId[] = "USB_MSC";
|
||||
constexpr char kUsbMscProductRevision[] = "1.0";
|
||||
constexpr char kUsbMscLogTag[] = "USB_MSC";
|
||||
|
||||
USBMSC g_usb_msc;
|
||||
const esp_partition_t* g_msc_partition = nullptr;
|
||||
uint32_t g_msc_blocks = 0;
|
||||
bool g_msc_ready = false;
|
||||
|
||||
uint32_t alignDown(uint32_t value, uint32_t align) {
|
||||
return value & ~(align - 1U);
|
||||
}
|
||||
|
||||
uint32_t alignUp(uint32_t value, uint32_t align) {
|
||||
return (value + align - 1U) & ~(align - 1U);
|
||||
}
|
||||
|
||||
bool isInRange(uint32_t offset, uint32_t size) {
|
||||
if (g_msc_partition == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (offset > g_msc_partition->size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (static_cast<uint64_t>(offset) + size) <= g_msc_partition->size;
|
||||
}
|
||||
|
||||
bool eraseRange(uint32_t offset, uint32_t size) {
|
||||
if (!isInRange(offset, size)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t aligned_offset = alignDown(offset, kSectorBytes);
|
||||
const uint32_t aligned_size = alignUp(size, kSectorBytes);
|
||||
|
||||
const uint64_t partition_end = g_msc_partition->size;
|
||||
if (aligned_offset >= partition_end) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (aligned_offset + aligned_size > partition_end) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const esp_err_t err = esp_partition_erase_range(g_msc_partition, aligned_offset, aligned_size);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(kUsbMscLogTag, "erase_range failed off=%lu size=%lu err=%s", aligned_offset, aligned_size, esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool onStartStop(uint8_t power_condition, bool start, bool load_eject) {
|
||||
(void)power_condition;
|
||||
g_msc_ready = start || !load_eject;
|
||||
ESP_LOGI(kUsbMscLogTag, "start_stop power_condition=%u start=%d eject=%d", power_condition, static_cast<int>(start),
|
||||
static_cast<int>(load_eject));
|
||||
return true;
|
||||
}
|
||||
|
||||
int32_t onWrite(uint32_t lba, uint32_t offset, uint8_t* buffer, uint32_t bufsize) {
|
||||
if (!g_msc_ready || g_msc_partition == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (offset >= kUsbMscBlockSize) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint64_t block_offset = static_cast<uint64_t>(lba) * kUsbMscBlockSize;
|
||||
const uint64_t bytes_offset_64 = block_offset + offset;
|
||||
const uint64_t max_writable = g_msc_partition->size;
|
||||
if (bytes_offset_64 >= max_writable) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint32_t bytes_offset = static_cast<uint32_t>(bytes_offset_64);
|
||||
const uint32_t max_chunk = static_cast<uint32_t>(max_writable - bytes_offset);
|
||||
const uint32_t write_size = (bufsize > max_chunk) ? max_chunk : bufsize;
|
||||
if (!isInRange(bytes_offset, write_size)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t written = 0;
|
||||
while (written < write_size) {
|
||||
const uint32_t dst_offset = bytes_offset + written;
|
||||
const uint32_t sector_start = alignDown(dst_offset, kSectorBytes);
|
||||
const uint32_t sector_end = sector_start + kSectorBytes;
|
||||
const uint32_t chunk_end = (bytes_offset + write_size < sector_end) ? (bytes_offset + write_size) : sector_end;
|
||||
const uint32_t copy_len = chunk_end - dst_offset;
|
||||
const uint32_t sector_pos = dst_offset - sector_start;
|
||||
|
||||
uint8_t sector[kSectorBytes];
|
||||
const esp_err_t read_err = esp_partition_read(g_msc_partition, sector_start, sector, kSectorBytes);
|
||||
if (read_err != ESP_OK) {
|
||||
ESP_LOGE(kUsbMscLogTag, "sector read failed addr=%lu err=%s", sector_start, esp_err_to_name(read_err));
|
||||
return 0;
|
||||
}
|
||||
|
||||
memcpy(sector + sector_pos, buffer + written, copy_len);
|
||||
|
||||
if (!eraseRange(sector_start, kSectorBytes)) {
|
||||
return 0;
|
||||
}
|
||||
const esp_err_t write_err = esp_partition_write(g_msc_partition, sector_start, sector, kSectorBytes);
|
||||
if (write_err != ESP_OK) {
|
||||
ESP_LOGE(kUsbMscLogTag,
|
||||
"write failed lba=%lu offset=%lu size=%lu err=%s",
|
||||
lba,
|
||||
offset,
|
||||
write_size,
|
||||
esp_err_to_name(write_err));
|
||||
return static_cast<int32_t>(written);
|
||||
}
|
||||
|
||||
written += copy_len;
|
||||
}
|
||||
|
||||
return static_cast<int32_t>(written);
|
||||
}
|
||||
|
||||
int32_t onRead(uint32_t lba, uint32_t offset, void* buffer, uint32_t bufsize) {
|
||||
if (!g_msc_ready || g_msc_partition == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (offset >= kUsbMscBlockSize) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint64_t block_offset = static_cast<uint64_t>(lba) * kUsbMscBlockSize;
|
||||
const uint64_t bytes_offset_64 = block_offset + offset;
|
||||
if (bytes_offset_64 >= g_msc_partition->size) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint32_t bytes_offset = static_cast<uint32_t>(bytes_offset_64);
|
||||
const uint32_t max_chunk = static_cast<uint32_t>(g_msc_partition->size - bytes_offset);
|
||||
const uint32_t read_size = (bufsize > max_chunk) ? max_chunk : bufsize;
|
||||
|
||||
if (!isInRange(bytes_offset, read_size)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const esp_err_t err = esp_partition_read(g_msc_partition, bytes_offset, buffer, read_size);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(kUsbMscLogTag, "read failed lba=%lu offset=%lu size=%lu err=%s", lba, offset, read_size, esp_err_to_name(err));
|
||||
return 0;
|
||||
}
|
||||
return static_cast<int32_t>(read_size);
|
||||
}
|
||||
|
||||
void onUsbEvent(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
|
||||
(void)arg;
|
||||
(void)event_data;
|
||||
|
||||
if (event_base != ARDUINO_USB_EVENTS) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event_id) {
|
||||
case ARDUINO_USB_STARTED_EVENT:
|
||||
ESP_LOGI(kUsbMscLogTag, "USB plugged");
|
||||
break;
|
||||
case ARDUINO_USB_STOPPED_EVENT:
|
||||
ESP_LOGI(kUsbMscLogTag, "USB unplugged");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace usb_msc_runtime {
|
||||
|
||||
bool beginUsbMassStorage() {
|
||||
g_msc_partition = esp_partition_find_first(
|
||||
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT, kUsbMscPartitionLabel);
|
||||
if (g_msc_partition == nullptr) {
|
||||
ESP_LOGE(kUsbMscLogTag, "partition '%s' not found", kUsbMscPartitionLabel);
|
||||
return false;
|
||||
}
|
||||
|
||||
g_msc_blocks = static_cast<uint32_t>(g_msc_partition->size / kUsbMscBlockSize);
|
||||
g_usb_msc.onStartStop(onStartStop);
|
||||
g_usb_msc.onRead(onRead);
|
||||
g_usb_msc.onWrite(onWrite);
|
||||
g_usb_msc.vendorID(kUsbMscVendorId);
|
||||
g_usb_msc.productID(kUsbMscProductId);
|
||||
g_usb_msc.productRevision(kUsbMscProductRevision);
|
||||
g_usb_msc.mediaPresent(true);
|
||||
if (!g_usb_msc.begin(g_msc_blocks, kUsbMscBlockSize)) {
|
||||
ESP_LOGE(kUsbMscLogTag, "USBMSC begin failed");
|
||||
g_msc_partition = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
USB.onEvent(onUsbEvent);
|
||||
USB.begin();
|
||||
g_msc_ready = true;
|
||||
ESP_LOGI(kUsbMscLogTag,
|
||||
"started: blocks=%lu size=%luKB label=%s",
|
||||
g_msc_blocks,
|
||||
g_msc_partition->size / 1024U,
|
||||
g_msc_partition->label);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace usb_msc_runtime
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
namespace usb_msc_runtime {
|
||||
|
||||
bool beginUsbMassStorage();
|
||||
|
||||
} // namespace usb_msc_runtime
|
||||
@@ -0,0 +1,126 @@
|
||||
#include "ScopeDisplay.h"
|
||||
|
||||
#include <math.h>
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32)
|
||||
#include <driver/dac.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t kDefaultAmplitude = 48U;
|
||||
constexpr uint16_t kDefaultFrequencyHz = 1200U;
|
||||
constexpr uint16_t kMinFrequencyHz = 60U;
|
||||
constexpr uint16_t kMaxFrequencyHz = 5000U;
|
||||
} // namespace
|
||||
|
||||
ScopeDisplay::ScopeDisplay()
|
||||
: initialized_(false),
|
||||
configured_(false),
|
||||
enabled_(false),
|
||||
supported_(false),
|
||||
frequency_hz_(kDefaultFrequencyHz),
|
||||
amplitude_(kDefaultAmplitude),
|
||||
last_tick_us_(0),
|
||||
phase_(0.0f) {}
|
||||
|
||||
bool ScopeDisplay::supported() const {
|
||||
return supported_;
|
||||
}
|
||||
|
||||
bool ScopeDisplay::enabled() const {
|
||||
return initialized_ && enabled_;
|
||||
}
|
||||
|
||||
uint16_t ScopeDisplay::frequency() const {
|
||||
return frequency_hz_;
|
||||
}
|
||||
|
||||
uint8_t ScopeDisplay::amplitude() const {
|
||||
return amplitude_;
|
||||
}
|
||||
|
||||
bool ScopeDisplay::begin() {
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32)
|
||||
dac_output_enable(DAC_CHANNEL_1);
|
||||
dac_output_enable(DAC_CHANNEL_2);
|
||||
initialized_ = true;
|
||||
supported_ = true;
|
||||
configured_ = true;
|
||||
enabled_ = true;
|
||||
last_tick_us_ = micros();
|
||||
phase_ = 0.0f;
|
||||
return true;
|
||||
#else
|
||||
initialized_ = false;
|
||||
supported_ = false;
|
||||
configured_ = false;
|
||||
enabled_ = false;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void ScopeDisplay::end() {
|
||||
if (!initialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
enabled_ = false;
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32)
|
||||
dac_output_disable(DAC_CHANNEL_1);
|
||||
dac_output_disable(DAC_CHANNEL_2);
|
||||
#endif
|
||||
initialized_ = false;
|
||||
}
|
||||
|
||||
bool ScopeDisplay::configure(uint16_t frequency_hz, uint8_t amplitude) {
|
||||
if (frequency_hz < kMinFrequencyHz || frequency_hz > kMaxFrequencyHz) {
|
||||
return false;
|
||||
}
|
||||
if (amplitude == 0U) {
|
||||
return false;
|
||||
}
|
||||
frequency_hz_ = frequency_hz;
|
||||
amplitude_ = amplitude;
|
||||
configured_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScopeDisplay::enable(bool value) {
|
||||
if (!configured_ || !supported_) {
|
||||
return;
|
||||
}
|
||||
enabled_ = value;
|
||||
if (enabled_) {
|
||||
if (!initialized_) {
|
||||
begin();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScopeDisplay::tick() {
|
||||
if (!initialized_ || !enabled_ || !configured_) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32)
|
||||
const uint32_t now = micros();
|
||||
if ((now - last_tick_us_) < kTickIntervalUs) {
|
||||
return;
|
||||
}
|
||||
last_tick_us_ = now;
|
||||
|
||||
const float step = kTau * static_cast<float>(frequency_hz_) * (kTickIntervalUs / 1000000.0f);
|
||||
phase_ += step;
|
||||
if (phase_ >= kTau) {
|
||||
phase_ -= kTau;
|
||||
}
|
||||
|
||||
const float x = sinf(phase_);
|
||||
const float y = cosf(phase_);
|
||||
const int v1 = 128 + static_cast<int>(x * static_cast<float>(amplitude_));
|
||||
const int v2 = 128 + static_cast<int>(y * static_cast<float>(amplitude_));
|
||||
const uint8_t sample1 = static_cast<uint8_t>(constrain(v1, 0, 255));
|
||||
const uint8_t sample2 = static_cast<uint8_t>(constrain(v2, 0, 255));
|
||||
dac_output_voltage(DAC_CHANNEL_1, sample1);
|
||||
dac_output_voltage(DAC_CHANNEL_2, sample2);
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef VISUAL_SCOPE_DISPLAY_H
|
||||
#define VISUAL_SCOPE_DISPLAY_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
class ScopeDisplay {
|
||||
public:
|
||||
ScopeDisplay();
|
||||
|
||||
bool begin();
|
||||
void end();
|
||||
bool supported() const;
|
||||
bool enabled() const;
|
||||
bool configure(uint16_t frequency_hz, uint8_t amplitude);
|
||||
void enable(bool value);
|
||||
void tick();
|
||||
|
||||
uint16_t frequency() const;
|
||||
uint8_t amplitude() const;
|
||||
|
||||
private:
|
||||
static constexpr uint32_t kTickIntervalUs = 300;
|
||||
static constexpr float kTau = 6.283185307179586f;
|
||||
|
||||
bool initialized_;
|
||||
bool configured_;
|
||||
bool enabled_;
|
||||
bool supported_;
|
||||
uint16_t frequency_hz_;
|
||||
uint8_t amplitude_;
|
||||
uint32_t last_tick_us_;
|
||||
float phase_;
|
||||
};
|
||||
|
||||
#endif // VISUAL_SCOPE_DISPLAY_H
|
||||
@@ -1,6 +1,10 @@
|
||||
#include "web/WebServerManager.h"
|
||||
|
||||
#ifdef USB_MSC_BOOT_ENABLE
|
||||
#include <FFat.h>
|
||||
#else
|
||||
#include <SPIFFS.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
constexpr bool kForceAuthDisabled = true;
|
||||
@@ -27,11 +31,19 @@ WebServerManager::WebServerManager(uint16_t port)
|
||||
auth_pass_("admin") {}
|
||||
|
||||
void WebServerManager::begin() {
|
||||
#ifdef USB_MSC_BOOT_ENABLE
|
||||
if (FFat.begin(true, "/usbmsc", 10, "usbmsc")) {
|
||||
server_.serveStatic("/", FFat, "/webui/").setDefaultFile("index.html");
|
||||
} else {
|
||||
Serial.println("[WebServerManager] FFat mount failed (label usbmsc)");
|
||||
}
|
||||
#else
|
||||
if (!SPIFFS.begin(true)) {
|
||||
Serial.println("[WebServerManager] SPIFFS mount failed");
|
||||
} else {
|
||||
server_.serveStatic("/", SPIFFS, "/webui/").setDefaultFile("index.html");
|
||||
}
|
||||
#endif
|
||||
|
||||
registerRoutes();
|
||||
server_.begin();
|
||||
@@ -280,68 +292,6 @@ void WebServerManager::registerRoutes() {
|
||||
handleDispatch(request, "ESPNOW_SEND " + mac + " " + payload);
|
||||
});
|
||||
|
||||
// Bluetooth.
|
||||
server_.on("/api/bluetooth", HTTP_GET,
|
||||
[this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_STATUS"); });
|
||||
server_.on("/api/bluetooth/hfp/connect", HTTP_POST, [this](AsyncWebServerRequest* request) {
|
||||
JsonDocument doc;
|
||||
if (!extractJsonBody(request, doc)) {
|
||||
request->send(400, "application/json", "{\"error\":\"invalid json body\"}");
|
||||
return;
|
||||
}
|
||||
const String addr = doc["addr"] | "";
|
||||
if (!isValidInput(addr, 32)) {
|
||||
request->send(400, "application/json", "{\"error\":\"invalid addr\"}");
|
||||
return;
|
||||
}
|
||||
handleDispatch(request, "BT_HFP_CONNECT " + addr);
|
||||
});
|
||||
server_.on("/api/bluetooth/hfp/disconnect", HTTP_POST,
|
||||
[this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_HFP_DISCONNECT"); });
|
||||
server_.on("/api/bluetooth/hfp/auto", HTTP_GET,
|
||||
[this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_STATUS"); });
|
||||
server_.on("/api/bluetooth/hfp/auto", HTTP_POST, [this](AsyncWebServerRequest* request) {
|
||||
JsonDocument doc;
|
||||
if (!extractJsonBody(request, doc)) {
|
||||
request->send(400, "application/json", "{\"error\":\"invalid json body\"}");
|
||||
return;
|
||||
}
|
||||
const bool enabled = doc["enabled"] | true;
|
||||
handleDispatch(request, enabled ? "BT_AUTO_RECONNECT_ON" : "BT_AUTO_RECONNECT_OFF");
|
||||
});
|
||||
server_.on("/api/bluetooth/discoverable/on", HTTP_POST,
|
||||
[this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_DISCOVERABLE_ON"); });
|
||||
server_.on("/api/bluetooth/discoverable/off", HTTP_POST,
|
||||
[this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_DISCOVERABLE_OFF"); });
|
||||
server_.on("/api/bluetooth/hfp/dial", HTTP_POST, [this](AsyncWebServerRequest* request) {
|
||||
JsonDocument doc;
|
||||
if (!extractJsonBody(request, doc)) {
|
||||
request->send(400, "application/json", "{\"error\":\"invalid json body\"}");
|
||||
return;
|
||||
}
|
||||
const String number = doc["number"] | "";
|
||||
if (!isValidInput(number, 32)) {
|
||||
request->send(400, "application/json", "{\"error\":\"invalid number\"}");
|
||||
return;
|
||||
}
|
||||
handleDispatch(request, "BT_DIAL " + number);
|
||||
});
|
||||
server_.on("/api/bluetooth/hfp/redial", HTTP_POST,
|
||||
[this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_REDIAL"); });
|
||||
server_.on("/api/bluetooth/hfp/answer", HTTP_POST,
|
||||
[this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_ANSWER"); });
|
||||
server_.on("/api/bluetooth/hfp/hangup", HTTP_POST,
|
||||
[this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_HANGUP"); });
|
||||
server_.on("/api/bluetooth/hfp/calls", HTTP_POST,
|
||||
[this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_CALLS"); });
|
||||
server_.on("/api/bluetooth/pbap/sync", HTTP_POST,
|
||||
[this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_PBAP_SYNC"); });
|
||||
server_.on("/api/bluetooth/ble/start", HTTP_POST,
|
||||
[this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_BLE_START"); });
|
||||
server_.on("/api/bluetooth/ble/stop", HTTP_POST,
|
||||
[this](AsyncWebServerRequest* request) { handleDispatch(request, "BT_BLE_STOP"); });
|
||||
}
|
||||
|
||||
bool WebServerManager::authenticateRequest(AsyncWebServerRequest* request) const {
|
||||
if (kForceAuthDisabled || !auth_enabled_) {
|
||||
return true;
|
||||
@@ -389,9 +339,7 @@ bool WebServerManager::isEffectCommand(const String& command_line) {
|
||||
token.trim();
|
||||
token.toUpperCase();
|
||||
|
||||
return token == "CALL" || token == "PLAY" || token == "CAPTURE_START" || token == "CAPTURE_STOP" ||
|
||||
token == "BT_DIAL" || token == "DIAL" || token == "BT_REDIAL" || token == "BT_ANSWER" ||
|
||||
token == "BT_HANGUP";
|
||||
return token == "CALL" || token == "PLAY" || token == "CAPTURE_START" || token == "CAPTURE_STOP";
|
||||
}
|
||||
|
||||
void WebServerManager::refreshStatusCache() {
|
||||
|
||||
@@ -61,7 +61,7 @@ String buildFallbackApSsid() {
|
||||
return String(name);
|
||||
}
|
||||
|
||||
void enforceBtCoexModemSleep() {
|
||||
void enforceCoexModemSleep() {
|
||||
WiFi.setSleep(true);
|
||||
const esp_err_t err = esp_wifi_set_ps(WIFI_PS_MIN_MODEM);
|
||||
if (err != ESP_OK && err != ESP_ERR_WIFI_NOT_INIT && err != ESP_ERR_WIFI_NOT_STARTED) {
|
||||
@@ -84,7 +84,7 @@ WifiManager::WifiManager()
|
||||
next_coex_reassert_ms_(0) {}
|
||||
|
||||
void WifiManager::enforceCoexPolicy() const {
|
||||
enforceBtCoexModemSleep();
|
||||
enforceCoexModemSleep();
|
||||
}
|
||||
|
||||
bool WifiManager::begin(const char* ssid, const char* password, uint32_t timeout_ms) {
|
||||
@@ -104,10 +104,9 @@ bool WifiManager::connect(const String& ssid, const String& password, uint32_t t
|
||||
|
||||
stopFallbackAp();
|
||||
WiFi.mode(WIFI_STA);
|
||||
// Required by ESP32 WiFi+BT coexistence.
|
||||
// Keep reconnect policy manual to avoid repeated WiFi timer churn under BT load.
|
||||
// Keep reconnect policy manual to avoid repeated WiFi timer churn from external clients.
|
||||
WiFi.setAutoReconnect(false);
|
||||
enforceCoexPolicy(); // Re-assert after mode switch to avoid BT coex abort.
|
||||
enforceCoexPolicy();
|
||||
WiFi.disconnect(false, true);
|
||||
enforceCoexPolicy();
|
||||
delay(100);
|
||||
@@ -285,7 +284,7 @@ bool WifiManager::startFallbackAp() {
|
||||
}
|
||||
|
||||
WiFi.mode(WIFI_AP_STA);
|
||||
// Required by ESP32 WiFi+BT coexistence in AP+STA mode.
|
||||
// Keep a stable WiFi mode under AP+STA conditions.
|
||||
WiFi.setAutoReconnect(false);
|
||||
enforceCoexPolicy();
|
||||
const bool ok = WiFi.softAP(
|
||||
|
||||
Reference in New Issue
Block a user