adding flasher

This commit is contained in:
Brent Perteet
2025-10-12 13:40:31 -05:00
parent a89fdc6843
commit 04d2c71d01
18 changed files with 1034 additions and 194 deletions

View File

@@ -0,0 +1,13 @@
{
"permissions": {
"allow": [
"Bash(idf.py build:*)",
"Bash(grep:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(pip install:*)",
"Bash(build_from_spec.bat)"
],
"deny": []
}
}

141
docker.md Normal file
View File

@@ -0,0 +1,141 @@
Perfect! Below is the updated `README.md` section with **complete instructions** for setting up the Dockerized ESP-IDF environment on **Windows**, **including installation of the official Espressif VS Code extension**.
You can paste this into your repos `README.md`. Its ready to go and designed to guide teammates or collaborators through cloning, launching, and building the project with confidence.
---
## 🐳 ESP-IDF Dockerized Development Environment (Windows)
This project includes a fully containerized [ESP-IDF](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/index.html) development environment using **Docker** and **Visual Studio Code**.
Follow these instructions to set up, build, and flash the firmware inside a VS Code **Dev Container** on **Windows**.
---
### ✅ Prerequisites
Ensure the following are installed on your system:
1. [**Docker Desktop for Windows**](https://www.docker.com/products/docker-desktop)
* Enable **WSL 2 backend** during installation.
2. [**Visual Studio Code**](https://code.visualstudio.com/)
3. [**Dev Containers Extension**](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
* In VS Code: `Extensions → Search "Dev Containers" → Install`
4. [**Espressif IDF Extension** for VS Code](https://marketplace.visualstudio.com/items?itemName=espressif.esp-idf-extension)
* In VS Code: `Extensions → Search "ESP-IDF" → Install`
> 💡 **Optional (recommended):** Install [WSL 2 with Ubuntu](https://learn.microsoft.com/en-us/windows/wsl/install) for improved Linux compatibility inside Docker.
---
### 🚀 Getting Started
#### 1. Clone this repository
```bash
git clone https://github.com/your-username/your-esp-idf-project.git
cd your-esp-idf-project
```
---
#### 2. Open in Visual Studio Code
From the project root:
```bash
code .
```
> Ensure you're opening the folder that contains `.devcontainer/`.
---
#### 3. Reopen in Dev Container
In VS Code:
* Press `F1` or `Ctrl+Shift+P`
* Run: **Dev Containers: Reopen in Container**
VS Code will:
* Build the Docker image (based on the provided `Dockerfile`)
* Set up the ESP-IDF environment
* Install extensions automatically
---
#### 4. Verify Environment
Once setup is complete:
* A terminal should launch inside the container
* Run:
```bash
idf.py --version
```
to confirm ESP-IDF is active and available.
---
#### 5. Build the Firmware
Inside the containers terminal:
```bash
idf.py build
```
You should see standard ESP-IDF build output and a `.bin` firmware file in the `build/` directory.
---
### 🔌 Flashing the ESP32 (Optional)
If you want to flash from inside the container:
1. Connect your ESP32 via USB.
2. Identify the COM port on Windows (e.g., `COM3`).
3. Pass the USB device into the container (this may require configuration).
4. Then run:
```bash
idf.py -p COM3 flash monitor
```
> ⚠️ **Note:** Docker Desktop for Windows doesnt always expose serial ports to containers directly. You can build firmware inside the container and flash it from your host as a fallback.
---
### 🧰 Notes
* The ESP-IDF version is pinned in the Dockerfile (e.g., `espressif/idf:v5.2.1`)
* The container automatically runs `source $IDF_PATH/export.sh` to prepare the environment.
* VS Code extensions (`.devcontainer.json`) include:
* `ms-vscode.cpptools`
* `ms-vscode.cmake-tools`
* `espressif.esp-idf-extension`
---
### 🛠 Troubleshooting
* If `idf.py` is not found, make sure the container terminal sources `export.sh`
* If USB flashing fails, flash from host or use WSL serial forwarding tools like `socat` or `usbip`
---
Would you like this `README.md` version to be packaged with:
* A **starter repo structure**
* A prebuilt `.devcontainer/` directory
* A sample `Dockerfile`, `main.c`, and `CMakeLists.txt`?
If yes, I can bundle that up for you — just say the word.

4
flash_gui.bat Normal file
View File

@@ -0,0 +1,4 @@
@echo off
echo Starting ESP32 Firmware Flash GUI...
python flash_tool/gui_flasher.py
pause

View File

@@ -0,0 +1,43 @@
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(
['gui_flasher.py'],
pathex=[],
binaries=[],
datas=[('requirements.txt', '.')],
hiddenimports=['serial.tools.list_ports'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='ESP32_Flasher',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True, # Set to False for windowed mode
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

7
flash_tool/build_exe.bat Normal file
View File

@@ -0,0 +1,7 @@
@echo off
echo Building ESP32 Flasher Executable...
cd /d "%~dp0"
python build_executable.py
echo.
echo Build complete! Check the 'dist' folder for ESP32_Flasher.exe
pause

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""
Build script to create standalone executable for ESP32 Flash GUI
"""
import subprocess
import sys
import os
from pathlib import Path
def install_requirements():
"""Install required packages"""
print("Installing requirements...")
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"])
def build_executable():
"""Build standalone executable using PyInstaller"""
print("Building standalone executable...")
# PyInstaller command
cmd = [
sys.executable, "-m", "PyInstaller",
"--onefile", # Single executable file
"--windowed", # No console window (remove if you want console)
"--name", "ESP32_Flasher",
"--icon", "icon.ico" if Path("icon.ico").exists() else None,
"--add-data", "requirements.txt;.", # Include requirements file
"gui_flasher.py"
]
# Remove None values
cmd = [arg for arg in cmd if arg is not None]
subprocess.check_call(cmd)
print("\nExecutable built successfully!")
print("Find it in the 'dist' folder as 'ESP32_Flasher.exe'")
def main():
os.chdir(Path(__file__).parent)
try:
install_requirements()
build_executable()
except subprocess.CalledProcessError as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,11 @@
@echo off
echo Installing dependencies...
pip install pyinstaller esptool pyserial
echo.
echo Building executable from spec file...
pyinstaller ESP32_Flasher.spec
echo.
echo Build complete! Find ESP32_Flasher.exe in the dist folder.
pause

View File

@@ -0,0 +1,14 @@
@echo off
echo Installing auto-py-to-exe...
pip install auto-py-to-exe esptool pyserial
echo.
echo Starting auto-py-to-exe GUI...
echo.
echo Configure the following settings:
echo - Script Location: gui_flasher.py
echo - Onefile: One File
echo - Console Window: Console Based (or Window Based if you prefer no console)
echo - Additional Files: Add requirements.txt
echo.
auto-py-to-exe

271
flash_tool/gui_flasher.py Normal file
View File

@@ -0,0 +1,271 @@
#!/usr/bin/env python3
"""
ESP32 Firmware Flash GUI Tool
Simple GUI interface for flashing firmware packages to ESP32 devices
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import subprocess
import sys
import threading
import zipfile
import tempfile
import os
from pathlib import Path
import serial.tools.list_ports
class ESP32FlasherGUI:
def __init__(self, root):
self.root = root
self.root.title("ESP32 Firmware Flasher")
self.root.geometry("600x500")
# Variables
self.port_var = tk.StringVar()
self.firmware_path_var = tk.StringVar()
self.temp_dir = None
self.setup_ui()
self.refresh_ports()
def setup_ui(self):
# Main frame
main_frame = ttk.Frame(self.root, padding="10")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Configure grid weights
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=1)
# Port selection
ttk.Label(main_frame, text="Serial Port:").grid(row=0, column=0, sticky=tk.W, pady=5)
port_frame = ttk.Frame(main_frame)
port_frame.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=5)
port_frame.columnconfigure(0, weight=1)
self.port_combo = ttk.Combobox(port_frame, textvariable=self.port_var, state="readonly")
self.port_combo.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 5))
ttk.Button(port_frame, text="Refresh", command=self.refresh_ports).grid(row=0, column=1)
# Firmware package selection
ttk.Label(main_frame, text="Firmware Package:").grid(row=1, column=0, sticky=tk.W, pady=5)
firmware_frame = ttk.Frame(main_frame)
firmware_frame.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=5)
firmware_frame.columnconfigure(0, weight=1)
self.firmware_entry = ttk.Entry(firmware_frame, textvariable=self.firmware_path_var, state="readonly")
self.firmware_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 5))
ttk.Button(firmware_frame, text="Browse", command=self.browse_firmware).grid(row=0, column=1)
# Flash settings frame
settings_frame = ttk.LabelFrame(main_frame, text="Flash Settings", padding="5")
settings_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=10)
settings_frame.columnconfigure(1, weight=1)
# Flash settings
ttk.Label(settings_frame, text="Chip:").grid(row=0, column=0, sticky=tk.W, pady=2)
self.chip_var = tk.StringVar(value="esp32")
ttk.Entry(settings_frame, textvariable=self.chip_var, width=15).grid(row=0, column=1, sticky=tk.W, pady=2)
ttk.Label(settings_frame, text="Baud Rate:").grid(row=1, column=0, sticky=tk.W, pady=2)
self.baud_var = tk.StringVar(value="460800")
ttk.Entry(settings_frame, textvariable=self.baud_var, width=15).grid(row=1, column=1, sticky=tk.W, pady=2)
ttk.Label(settings_frame, text="Flash Mode:").grid(row=2, column=0, sticky=tk.W, pady=2)
self.flash_mode_var = tk.StringVar(value="dio")
ttk.Entry(settings_frame, textvariable=self.flash_mode_var, width=15).grid(row=2, column=1, sticky=tk.W, pady=2)
ttk.Label(settings_frame, text="Flash Freq:").grid(row=3, column=0, sticky=tk.W, pady=2)
self.flash_freq_var = tk.StringVar(value="40m")
ttk.Entry(settings_frame, textvariable=self.flash_freq_var, width=15).grid(row=3, column=1, sticky=tk.W, pady=2)
ttk.Label(settings_frame, text="Flash Size:").grid(row=4, column=0, sticky=tk.W, pady=2)
self.flash_size_var = tk.StringVar(value="2MB")
ttk.Entry(settings_frame, textvariable=self.flash_size_var, width=15).grid(row=4, column=1, sticky=tk.W, pady=2)
# Flash button
self.flash_button = ttk.Button(main_frame, text="Flash Firmware", command=self.flash_firmware)
self.flash_button.grid(row=3, column=0, columnspan=2, pady=10)
# Progress bar
self.progress = ttk.Progressbar(main_frame, mode='indeterminate')
self.progress.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5)
# Log output
ttk.Label(main_frame, text="Output:").grid(row=5, column=0, sticky=tk.W, pady=(10, 0))
self.log_text = scrolledtext.ScrolledText(main_frame, height=15, width=70)
self.log_text.grid(row=6, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5)
main_frame.rowconfigure(6, weight=1)
def refresh_ports(self):
"""Refresh the list of available serial ports"""
ports = [port.device for port in serial.tools.list_ports.comports()]
self.port_combo['values'] = ports
if ports and not self.port_var.get():
self.port_var.set(ports[0])
def browse_firmware(self):
"""Browse for firmware package (.zip file)"""
filename = filedialog.askopenfilename(
title="Select Firmware Package",
filetypes=[("ZIP files", "*.zip"), ("All files", "*.*")]
)
if filename:
self.firmware_path_var.set(filename)
self.validate_firmware_package(filename)
def validate_firmware_package(self, zip_path):
"""Validate that the ZIP contains required firmware files"""
required_files = [
"bootloader.bin",
"soundshot.bin",
"ota_data_initial.bin",
"partition-table.bin"
]
try:
with zipfile.ZipFile(zip_path, 'r') as zip_file:
zip_contents = zip_file.namelist()
missing_files = []
for required_file in required_files:
if required_file not in zip_contents:
missing_files.append(required_file)
if missing_files:
messagebox.showwarning(
"Invalid Package",
f"Missing required files:\n{', '.join(missing_files)}"
)
return False
else:
self.log_message(f"✓ Valid firmware package: {Path(zip_path).name}")
return True
except zipfile.BadZipFile:
messagebox.showerror("Error", "Invalid ZIP file")
return False
def log_message(self, message):
"""Add message to log output"""
self.log_text.insert(tk.END, message + "\n")
self.log_text.see(tk.END)
self.root.update()
def extract_firmware_package(self, zip_path):
"""Extract firmware package to temporary directory"""
if self.temp_dir:
# Clean up previous temp dir
import shutil
shutil.rmtree(self.temp_dir, ignore_errors=True)
self.temp_dir = tempfile.mkdtemp(prefix="esp32_flash_")
with zipfile.ZipFile(zip_path, 'r') as zip_file:
zip_file.extractall(self.temp_dir)
return self.temp_dir
def flash_firmware(self):
"""Flash the firmware to ESP32"""
# Validate inputs
if not self.port_var.get():
messagebox.showerror("Error", "Please select a serial port")
return
if not self.firmware_path_var.get():
messagebox.showerror("Error", "Please select a firmware package")
return
# Disable flash button and start progress
self.flash_button.config(state="disabled")
self.progress.start()
self.log_text.delete(1.0, tk.END)
# Run flash in separate thread to prevent UI freezing
thread = threading.Thread(target=self._flash_worker)
thread.daemon = True
thread.start()
def _flash_worker(self):
"""Worker thread for flashing firmware"""
try:
# Extract firmware package
self.log_message("Extracting firmware package...")
temp_dir = self.extract_firmware_package(self.firmware_path_var.get())
# Define file paths
bootloader = os.path.join(temp_dir, "bootloader.bin")
firmware = os.path.join(temp_dir, "soundshot.bin")
ota_initial = os.path.join(temp_dir, "ota_data_initial.bin")
partition = os.path.join(temp_dir, "partition-table.bin")
# Build esptool command
cmd = [
sys.executable, "-m", "esptool",
"--chip", self.chip_var.get(),
"--port", self.port_var.get(),
"--baud", self.baud_var.get(),
"--before", "default_reset",
"--after", "hard_reset",
"write-flash",
"--flash-mode", self.flash_mode_var.get(),
"--flash-freq", self.flash_freq_var.get(),
"--flash-size", self.flash_size_var.get(),
"0x1000", bootloader,
"0x20000", firmware,
"0x11000", ota_initial,
"0x8000", partition
]
self.log_message(f"Running: {' '.join(cmd)}\n")
# Run esptool
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
universal_newlines=True
)
# Stream output
for line in process.stdout:
self.root.after(0, self.log_message, line.rstrip())
process.wait()
if process.returncode == 0:
self.root.after(0, self.log_message, "\n✓ Flash completed successfully!")
self.root.after(0, messagebox.showinfo, "Success", "Firmware flashed successfully!")
else:
self.root.after(0, self.log_message, f"\n✗ Flash failed with return code {process.returncode}")
self.root.after(0, messagebox.showerror, "Error", "Flash operation failed!")
except Exception as e:
self.root.after(0, self.log_message, f"\n✗ Error: {str(e)}")
self.root.after(0, messagebox.showerror, "Error", f"Flash operation failed: {str(e)}")
finally:
# Re-enable UI
self.root.after(0, self.progress.stop)
self.root.after(0, lambda: self.flash_button.config(state="normal"))
# Clean up temp directory
if self.temp_dir:
import shutil
shutil.rmtree(self.temp_dir, ignore_errors=True)
self.temp_dir = None
def main():
root = tk.Tk()
app = ESP32FlasherGUI(root)
root.mainloop()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,3 @@
esptool>=4.0
pyserial>=3.5
pyinstaller>=5.0

View File

@@ -109,24 +109,11 @@ static void bt_app_av_sm_hdlr(uint16_t event, void *param);
/* utils for transfer BLuetooth Deveice Address into string form */ /* utils for transfer BLuetooth Deveice Address into string form */
static char *bda2str(esp_bd_addr_t bda, char *str, size_t size); static char *bda2str(esp_bd_addr_t bda, char *str, size_t size);
/* NVS storage functions for paired devices */ static esp_err_t bt_try_connect_known_devices(void);
typedef struct { static void bt_debug_print_known_devices(void);
esp_bd_addr_t bda; static esp_err_t bt_add_discovered_device(esp_bd_addr_t bda, const char *name);
char name[ESP_BT_GAP_MAX_BDNAME_LEN + 1]; static esp_err_t bt_try_connect_all_known_devices(void);
uint32_t last_connected; static esp_err_t bt_try_next_known_device(void);
} paired_device_t;
static esp_err_t nvs_save_paired_device(const paired_device_t *device);
static esp_err_t nvs_load_paired_devices(paired_device_t *devices, size_t *count);
static esp_err_t nvs_remove_paired_device(esp_bd_addr_t bda);
static bool nvs_is_device_known(esp_bd_addr_t bda);
static esp_err_t nvs_get_known_device_count(size_t *count);
static esp_err_t nvs_try_connect_known_devices(void);
static void nvs_debug_print_known_devices(void);
static esp_err_t nvs_add_discovered_device(esp_bd_addr_t bda, const char *name);
static esp_err_t nvs_update_connection_timestamp(esp_bd_addr_t bda);
static esp_err_t nvs_try_connect_all_known_devices(void);
static esp_err_t nvs_try_next_known_device(void);
/* A2DP application state machine handler for each state */ /* A2DP application state machine handler for each state */
static void bt_app_av_state_unconnected_hdlr(uint16_t event, void *param); static void bt_app_av_state_unconnected_hdlr(uint16_t event, void *param);
@@ -167,145 +154,18 @@ static bool s_volume_control_available = false; /* Whether AVRC vo
* NVS STORAGE FUNCTION DEFINITIONS * NVS STORAGE FUNCTION DEFINITIONS
********************************/ ********************************/
static esp_err_t nvs_save_paired_device(const paired_device_t *device) // This function is no longer used - replaced by system_savePairedDevice
{
nvs_handle_t nvs_handle;
esp_err_t ret;
size_t count = 0;
char key[32];
if (!device) {
return ESP_ERR_INVALID_ARG;
}
ret = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs_handle);
if (ret != ESP_OK) {
ESP_LOGE(BT_AV_TAG, "Error opening NVS handle: %s", esp_err_to_name(ret));
return ret;
}
// Get current device count
size_t required_size = sizeof(size_t);
nvs_get_blob(nvs_handle, NVS_KEY_COUNT, &count, &required_size);
// Check if device already exists
paired_device_t existing_devices[MAX_PAIRED_DEVICES];
size_t existing_count = MAX_PAIRED_DEVICES;
nvs_load_paired_devices(existing_devices, &existing_count);
int device_index = -1;
for (int i = 0; i < existing_count; i++) {
if (memcmp(existing_devices[i].bda, device->bda, ESP_BD_ADDR_LEN) == 0) {
device_index = i;
break;
}
}
// If device not found and we have space, add it
if (device_index == -1) {
if (count >= MAX_PAIRED_DEVICES) {
ESP_LOGW(BT_AV_TAG, "Maximum paired devices reached");
nvs_close(nvs_handle);
return ESP_ERR_NO_MEM;
}
device_index = count;
count++;
}
// Save device data
snprintf(key, sizeof(key), "%s%d", NVS_KEY_PREFIX, device_index);
ret = nvs_set_blob(nvs_handle, key, device, sizeof(paired_device_t));
if (ret != ESP_OK) {
ESP_LOGE(BT_AV_TAG, "Error saving device: %s", esp_err_to_name(ret));
nvs_close(nvs_handle);
return ret;
}
// Save device count
ret = nvs_set_blob(nvs_handle, NVS_KEY_COUNT, &count, sizeof(size_t));
if (ret != ESP_OK) {
ESP_LOGE(BT_AV_TAG, "Error saving device count: %s", esp_err_to_name(ret));
nvs_close(nvs_handle);
return ret;
}
ret = nvs_commit(nvs_handle);
nvs_close(nvs_handle);
char bda_str[18];
ESP_LOGI(BT_AV_TAG, "Saved paired device: %s (%s)",
device->name, bda2str(device->bda, bda_str, sizeof(bda_str)));
return ret;
}
static esp_err_t nvs_load_paired_devices(paired_device_t *devices, size_t *count) // This function is no longer used - replaced by system_loadPairedDevices
{
nvs_handle_t nvs_handle;
esp_err_t ret;
size_t device_count = 0;
char key[32];
if (!devices || !count || *count == 0) {
return ESP_ERR_INVALID_ARG;
}
ret = nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs_handle);
if (ret != ESP_OK) {
ESP_LOGD(BT_AV_TAG, "NVS namespace not found (first run): %s", esp_err_to_name(ret));
*count = 0;
return ESP_OK;
}
// Get device count
size_t required_size = sizeof(size_t);
ret = nvs_get_blob(nvs_handle, NVS_KEY_COUNT, &device_count, &required_size);
if (ret != ESP_OK) {
ESP_LOGD(BT_AV_TAG, "No paired devices found");
nvs_close(nvs_handle);
*count = 0;
return ESP_OK;
}
// Load each device
size_t loaded = 0;
for (int i = 0; i < device_count && loaded < *count; i++) {
snprintf(key, sizeof(key), "%s%d", NVS_KEY_PREFIX, i);
required_size = sizeof(paired_device_t);
ret = nvs_get_blob(nvs_handle, key, &devices[loaded], &required_size);
if (ret == ESP_OK) {
loaded++;
}
}
nvs_close(nvs_handle);
*count = loaded;
return ESP_OK;
}
static bool nvs_is_device_known(esp_bd_addr_t bda) // This function is no longer used - replaced by system_isDeviceKnown
static esp_err_t bt_try_connect_known_devices(void)
{ {
paired_device_t devices[MAX_PAIRED_DEVICES]; paired_device_t devices[MAX_PAIRED_DEVICES];
size_t count = MAX_PAIRED_DEVICES; size_t count = MAX_PAIRED_DEVICES;
if (nvs_load_paired_devices(devices, &count) != ESP_OK) { esp_err_t ret = system_loadPairedDevices(devices, &count);
return false;
}
for (int i = 0; i < count; i++) {
if (memcmp(devices[i].bda, bda, ESP_BD_ADDR_LEN) == 0) {
return true;
}
}
return false;
}
static esp_err_t nvs_try_connect_known_devices(void)
{
paired_device_t devices[MAX_PAIRED_DEVICES];
size_t count = MAX_PAIRED_DEVICES;
esp_err_t ret = nvs_load_paired_devices(devices, &count);
if (ret != ESP_OK || count == 0) { if (ret != ESP_OK || count == 0) {
ESP_LOGI(BT_AV_TAG, "No known devices to connect to"); ESP_LOGI(BT_AV_TAG, "No known devices to connect to");
return ESP_ERR_NOT_FOUND; return ESP_ERR_NOT_FOUND;
@@ -355,14 +215,14 @@ static esp_err_t nvs_try_connect_known_devices(void)
return ret; return ret;
} }
static void nvs_debug_print_known_devices(void) static void bt_debug_print_known_devices(void)
{ {
paired_device_t devices[MAX_PAIRED_DEVICES]; const paired_device_t* devices;
size_t count = MAX_PAIRED_DEVICES; size_t count = 0;
esp_err_t ret = nvs_load_paired_devices(devices, &count); devices = system_getPairedDevices(&count);
if (ret != ESP_OK) { if (!devices) {
ESP_LOGE(BT_AV_TAG, "Failed to load devices for debug: %s", esp_err_to_name(ret)); ESP_LOGE(BT_AV_TAG, "Failed to load devices for debug");
return; return;
} }
@@ -377,12 +237,12 @@ static void nvs_debug_print_known_devices(void)
ESP_LOGI(BT_AV_TAG, "=== End Device List ==="); ESP_LOGI(BT_AV_TAG, "=== End Device List ===");
} }
static esp_err_t nvs_remove_paired_device(esp_bd_addr_t bda) static esp_err_t __attribute__((unused)) nvs_remove_paired_device(esp_bd_addr_t bda)
{ {
paired_device_t devices[MAX_PAIRED_DEVICES]; paired_device_t devices[MAX_PAIRED_DEVICES];
size_t count = MAX_PAIRED_DEVICES; size_t count = MAX_PAIRED_DEVICES;
esp_err_t ret = nvs_load_paired_devices(devices, &count); esp_err_t ret = system_loadPairedDevices(devices, &count);
if (ret != ESP_OK || count == 0) { if (ret != ESP_OK || count == 0) {
return ESP_ERR_NOT_FOUND; return ESP_ERR_NOT_FOUND;
} }
@@ -438,7 +298,7 @@ static esp_err_t nvs_remove_paired_device(esp_bd_addr_t bda)
return ret; return ret;
} }
static esp_err_t nvs_get_known_device_count(size_t *count) static esp_err_t __attribute__((unused)) nvs_get_known_device_count(size_t *count)
{ {
if (!count) { if (!count) {
return ESP_ERR_INVALID_ARG; return ESP_ERR_INVALID_ARG;
@@ -462,14 +322,14 @@ static esp_err_t nvs_get_known_device_count(size_t *count)
return ret; return ret;
} }
static esp_err_t nvs_add_discovered_device(esp_bd_addr_t bda, const char *name) static esp_err_t bt_add_discovered_device(esp_bd_addr_t bda, const char *name)
{ {
if (!bda || !name) { if (!bda || !name) {
return ESP_ERR_INVALID_ARG; return ESP_ERR_INVALID_ARG;
} }
// Check if device is already known // Check if device is already known
if (nvs_is_device_known(bda)) { if (system_isDeviceKnown(bda)) {
char bda_str[18]; char bda_str[18];
ESP_LOGD(BT_AV_TAG, "Device %s (%s) already known, skipping", ESP_LOGD(BT_AV_TAG, "Device %s (%s) already known, skipping",
name, bda2str(bda, bda_str, sizeof(bda_str))); name, bda2str(bda, bda_str, sizeof(bda_str)));
@@ -479,12 +339,12 @@ static esp_err_t nvs_add_discovered_device(esp_bd_addr_t bda, const char *name)
// Create paired_device_t structure for discovered device // Create paired_device_t structure for discovered device
paired_device_t device; paired_device_t device;
memcpy(device.bda, bda, ESP_BD_ADDR_LEN); memcpy(device.bda, bda, ESP_BD_ADDR_LEN);
strncpy(device.name, name, ESP_BT_GAP_MAX_BDNAME_LEN); strncpy(device.name, name, DEVICE_NAME_MAX_LEN - 1);
device.name[ESP_BT_GAP_MAX_BDNAME_LEN] = '\0'; device.name[DEVICE_NAME_MAX_LEN - 1] = '\0';
device.last_connected = 0; // Never connected, set to 0 device.last_connected = 0; // Never connected, set to 0
// Save to NVS // Save to NVS
esp_err_t ret = nvs_save_paired_device(&device); esp_err_t ret = system_savePairedDevice(&device);
if (ret == ESP_OK) { if (ret == ESP_OK) {
char bda_str[18]; char bda_str[18];
ESP_LOGI(BT_AV_TAG, "Added discovered device to NVS: %s (%s)", ESP_LOGI(BT_AV_TAG, "Added discovered device to NVS: %s (%s)",
@@ -496,7 +356,7 @@ static esp_err_t nvs_add_discovered_device(esp_bd_addr_t bda, const char *name)
return ret; return ret;
} }
static esp_err_t nvs_update_connection_timestamp(esp_bd_addr_t bda) static esp_err_t __attribute__((unused)) nvs_update_connection_timestamp(esp_bd_addr_t bda)
{ {
if (!bda) { if (!bda) {
return ESP_ERR_INVALID_ARG; return ESP_ERR_INVALID_ARG;
@@ -505,7 +365,7 @@ static esp_err_t nvs_update_connection_timestamp(esp_bd_addr_t bda)
paired_device_t devices[MAX_PAIRED_DEVICES]; paired_device_t devices[MAX_PAIRED_DEVICES];
size_t count = MAX_PAIRED_DEVICES; size_t count = MAX_PAIRED_DEVICES;
esp_err_t ret = nvs_load_paired_devices(devices, &count); esp_err_t ret = system_loadPairedDevices(devices, &count);
if (ret != ESP_OK || count == 0) { if (ret != ESP_OK || count == 0) {
ESP_LOGW(BT_AV_TAG, "No devices found to update timestamp"); ESP_LOGW(BT_AV_TAG, "No devices found to update timestamp");
return ESP_ERR_NOT_FOUND; return ESP_ERR_NOT_FOUND;
@@ -529,7 +389,7 @@ static esp_err_t nvs_update_connection_timestamp(esp_bd_addr_t bda)
devices[device_index].last_connected = (uint32_t)(esp_timer_get_time() / 1000000); // Convert to seconds devices[device_index].last_connected = (uint32_t)(esp_timer_get_time() / 1000000); // Convert to seconds
// Save updated device // Save updated device
ret = nvs_save_paired_device(&devices[device_index]); ret = system_savePairedDevice(&devices[device_index]);
if (ret == ESP_OK) { if (ret == ESP_OK) {
char bda_str[18]; char bda_str[18];
ESP_LOGI(BT_AV_TAG, "Updated connection timestamp for device: %s (%s)", ESP_LOGI(BT_AV_TAG, "Updated connection timestamp for device: %s (%s)",
@@ -541,11 +401,11 @@ static esp_err_t nvs_update_connection_timestamp(esp_bd_addr_t bda)
return ret; return ret;
} }
static esp_err_t nvs_try_connect_all_known_devices(void) static esp_err_t bt_try_connect_all_known_devices(void)
{ {
// Load all known devices into cache // Load all known devices into cache
s_known_device_count = MAX_PAIRED_DEVICES; s_known_device_count = MAX_PAIRED_DEVICES;
esp_err_t ret = nvs_load_paired_devices(s_known_devices, &s_known_device_count); esp_err_t ret = system_loadPairedDevices(s_known_devices, &s_known_device_count);
if (ret != ESP_OK || s_known_device_count == 0) { if (ret != ESP_OK || s_known_device_count == 0) {
ESP_LOGI(BT_AV_TAG, "No known devices to connect to"); ESP_LOGI(BT_AV_TAG, "No known devices to connect to");
s_current_device_index = -1; s_current_device_index = -1;
@@ -587,7 +447,7 @@ static esp_err_t nvs_try_connect_all_known_devices(void)
return ret; return ret;
} }
static esp_err_t nvs_try_next_known_device(void) static esp_err_t bt_try_next_known_device(void)
{ {
if (s_current_device_index < 0 || s_known_device_count == 0) { if (s_current_device_index < 0 || s_known_device_count == 0) {
ESP_LOGI(BT_AV_TAG, "No more devices to try"); ESP_LOGI(BT_AV_TAG, "No more devices to try");
@@ -714,7 +574,7 @@ static void filter_inquiry_scan_result(esp_bt_gap_cb_param_t *param)
get_name_from_eir(eir, s_peer_bdname, NULL); get_name_from_eir(eir, s_peer_bdname, NULL);
// Save discovered audio device to NVS (but don't connect to it) // Save discovered audio device to NVS (but don't connect to it)
nvs_add_discovered_device(param->disc_res.bda, (char *)s_peer_bdname); bt_add_discovered_device(param->disc_res.bda, (char *)s_peer_bdname);
// Add to device list for GUI // Add to device list for GUI
add_device_to_list(param->disc_res.bda, (char *)s_peer_bdname, false, rssi); add_device_to_list(param->disc_res.bda, (char *)s_peer_bdname, false, rssi);
@@ -754,7 +614,8 @@ static void bt_app_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *pa
} else { } else {
/* not discovered, continue to discover */ /* not discovered, continue to discover */
ESP_LOGI(BT_AV_TAG, "Device discovery failed, continue to discover..."); ESP_LOGI(BT_AV_TAG, "Device discovery failed, continue to discover...");
esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, 10, 0); s_a2d_state = APP_AV_STATE_UNCONNECTED;
//esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, 10, 0);
} }
} else if (param->disc_st_chg.state == ESP_BT_GAP_DISCOVERY_STARTED) { } else if (param->disc_st_chg.state == ESP_BT_GAP_DISCOVERY_STARTED) {
ESP_LOGI(BT_AV_TAG, "Discovery started."); ESP_LOGI(BT_AV_TAG, "Discovery started.");
@@ -854,10 +715,10 @@ static void bt_av_hdl_stack_evt(uint16_t event, void *p_param)
esp_bt_gap_get_device_name(); esp_bt_gap_get_device_name();
// Print list of saved devices from NVS // Print list of saved devices from NVS
nvs_debug_print_known_devices(); bt_debug_print_known_devices();
// Try to connect to all known devices sequentially // Try to connect to all known devices sequentially
esp_err_t connect_ret = nvs_try_connect_all_known_devices(); esp_err_t connect_ret = bt_try_connect_all_known_devices();
if (connect_ret != ESP_OK) { if (connect_ret != ESP_OK) {
// No known devices found, start discovery to find new devices // No known devices found, start discovery to find new devices
ESP_LOGI(BT_AV_TAG, "No known devices found, starting discovery..."); ESP_LOGI(BT_AV_TAG, "No known devices found, starting discovery...");
@@ -1222,7 +1083,8 @@ static void bt_app_av_state_unconnected_hdlr(uint16_t event, void *param)
break; break;
case BT_APP_HEART_BEAT_EVT: { case BT_APP_HEART_BEAT_EVT: {
// Try to connect to known devices, or start discovery if none available // Try to connect to known devices, or start discovery if none available
esp_err_t connect_ret = nvs_try_connect_all_known_devices(); esp_err_t connect_ret = bt_try_connect_all_known_devices();
if (connect_ret != ESP_OK) { if (connect_ret != ESP_OK) {
// No known devices, start discovery // No known devices, start discovery
ESP_LOGI(BT_AV_TAG, "No known devices available, starting discovery..."); ESP_LOGI(BT_AV_TAG, "No known devices available, starting discovery...");
@@ -1257,14 +1119,17 @@ static void bt_app_av_state_connecting_hdlr(uint16_t event, void *param)
s_media_state = APP_AV_MEDIA_STATE_IDLE; s_media_state = APP_AV_MEDIA_STATE_IDLE;
// Update connection timestamp for this device // Update connection timestamp for this device
nvs_update_connection_timestamp(s_peer_bda); system_updateConnectionTimestamp(s_peer_bda);
} else if (a2d->conn_stat.state == ESP_A2D_CONNECTION_STATE_DISCONNECTED) { } else if (a2d->conn_stat.state == ESP_A2D_CONNECTION_STATE_DISCONNECTED) {
ESP_LOGI(BT_AV_TAG, "Connection failed, trying next device..."); ESP_LOGI(BT_AV_TAG, "Connection failed, trying next device...");
// Try next known device before giving up // Try next known device before giving up
esp_err_t next_ret = nvs_try_next_known_device(); esp_err_t next_ret = bt_try_next_known_device();
if (next_ret != ESP_OK) { if (next_ret != ESP_OK) {
// No more devices to try, go to unconnected state // No more devices to try, go to unconnected state
s_a2d_state = APP_AV_STATE_UNCONNECTED; //s_a2d_state = APP_AV_STATE_UNCONNECTED;
ESP_LOGI(BT_AV_TAG, "No known devices available, starting discovery...");
s_a2d_state = APP_AV_STATE_DISCOVERING;
esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, 10, 0);
} }
} }
break; break;
@@ -1281,7 +1146,7 @@ static void bt_app_av_state_connecting_hdlr(uint16_t event, void *param)
if (++s_connecting_intv >= 2) { if (++s_connecting_intv >= 2) {
ESP_LOGI(BT_AV_TAG, "Connection timeout, trying next device..."); ESP_LOGI(BT_AV_TAG, "Connection timeout, trying next device...");
// Try next known device before giving up // Try next known device before giving up
esp_err_t next_ret = nvs_try_next_known_device(); esp_err_t next_ret = bt_try_next_known_device();
if (next_ret != ESP_OK) { if (next_ret != ESP_OK) {
// No more devices to try, go to unconnected state // No more devices to try, go to unconnected state
s_a2d_state = APP_AV_STATE_UNCONNECTED; s_a2d_state = APP_AV_STATE_UNCONNECTED;
@@ -1834,7 +1699,7 @@ static void load_paired_devices_to_list(void) {
size_t count = MAX_PAIRED_DEVICES; size_t count = MAX_PAIRED_DEVICES;
ESP_LOGI(BT_AV_TAG, "Attempting to load paired devices from NVS"); ESP_LOGI(BT_AV_TAG, "Attempting to load paired devices from NVS");
esp_err_t err = nvs_load_paired_devices(paired_devices, &count); esp_err_t err = system_loadPairedDevices(paired_devices, &count);
if (err == ESP_OK) { if (err == ESP_OK) {
ESP_LOGI(BT_AV_TAG, "Successfully loaded %d paired devices from NVS", (int)count); ESP_LOGI(BT_AV_TAG, "Successfully loaded %d paired devices from NVS", (int)count);
for (size_t i = 0; i < count; i++) { for (size_t i = 0; i < count; i++) {

View File

@@ -72,6 +72,13 @@ static lv_obj_t* create_volume_page(void);
static void update_volume_display(int volume); static void update_volume_display(int volume);
static void ensure_menu_styles(void); static void ensure_menu_styles(void);
// Menu stack management functions
static void menu_stack_push(lv_obj_t *page);
static lv_obj_t* menu_stack_pop(void);
static void menu_stack_clear(void);
static bool menu_stack_is_empty(void);
static void menu_go_back(void);
#define MAX_ITEMS 10 #define MAX_ITEMS 10
#define VISIBLE_ITEMS 3 #define VISIBLE_ITEMS 3
#define MENU_MAX_STRING_LENGTH 30 #define MENU_MAX_STRING_LENGTH 30
@@ -91,6 +98,15 @@ static lv_obj_t *_currentPage = NULL;
static GuiMode_t _mode = GUI_BUBBLE; static GuiMode_t _mode = GUI_BUBBLE;
// Menu navigation stack
#define MAX_MENU_STACK_SIZE 8
typedef struct {
lv_obj_t *pages[MAX_MENU_STACK_SIZE];
int count;
} menu_stack_t;
static menu_stack_t _menuStack = {0};
/* 1. Prepare a default (unfocused) style */ /* 1. Prepare a default (unfocused) style */
static lv_style_t _styleUnfocusedBtn; static lv_style_t _styleUnfocusedBtn;
@@ -135,6 +151,9 @@ static void show_menu(void) {
lv_obj_t* menu = create_menu_container(); lv_obj_t* menu = create_menu_container();
lv_obj_t* main_page = create_main_page(); lv_obj_t* main_page = create_main_page();
// Clear the menu stack when starting fresh menu navigation
menu_stack_clear();
lv_menu_set_page(menu, main_page); lv_menu_set_page(menu, main_page);
_currentPage = main_page; _currentPage = main_page;
@@ -180,7 +199,8 @@ static void lcd_init(void)
.miso_io_num = -1, .miso_io_num = -1,
.quadwp_io_num = -1, .quadwp_io_num = -1,
.quadhd_io_num = -1, .quadhd_io_num = -1,
.max_transfer_sz = LCD_H_RES * LCD_V_RES * 2 //.max_transfer_sz = LCD_H_RES * LCD_V_RES * 2
.max_transfer_sz = LCD_H_RES * 25 * (LCD_BITS_PER_PIXEL/8),
}; };
ESP_ERROR_CHECK(spi_bus_initialize(LCD_SPI_HOST, &buscfg, SPI_DMA_CH_AUTO)); ESP_ERROR_CHECK(spi_bus_initialize(LCD_SPI_HOST, &buscfg, SPI_DMA_CH_AUTO));
@@ -236,7 +256,8 @@ static void lvgl_init(void)
const lvgl_port_display_cfg_t disp_cfg = { const lvgl_port_display_cfg_t disp_cfg = {
.io_handle = io_handle, .io_handle = io_handle,
.panel_handle = panel_handle, .panel_handle = panel_handle,
.buffer_size = LCD_H_RES * LCD_V_RES * 2, // .buffer_size = LCD_H_RES * LCD_V_RES * 2,
.buffer_size = LCD_H_RES * 25 * (LCD_BITS_PER_PIXEL/8), // was full screen
.double_buffer = false, .double_buffer = false,
.hres = LCD_H_RES, .hres = LCD_H_RES,
.vres = LCD_V_RES, .vres = LCD_V_RES,
@@ -347,12 +368,20 @@ static void btn_click_cb(lv_event_t * e) {
// Handle specific menu items // Handle specific menu items
if (strcmp(txt, "Bluetooth") == 0) { if (strcmp(txt, "Bluetooth") == 0) {
LOCK(); LOCK();
// Push current page onto stack before navigating
menu_stack_push(_currentPage);
show_bt_device_list(); show_bt_device_list();
UNLOCK(); UNLOCK();
} else if (strcmp(txt, "Volume") == 0) { } else if (strcmp(txt, "Volume") == 0) {
LOCK(); LOCK();
// Push current page onto stack before navigating
menu_stack_push(_currentPage);
show_volume_control(); show_volume_control();
UNLOCK(); UNLOCK();
} else if (strcmp(txt, "Back") == 0) {
LOCK();
menu_go_back();
UNLOCK();
} else if (strcmp(txt, "Exit") == 0) { } else if (strcmp(txt, "Exit") == 0) {
LOCK(); LOCK();
_mode = GUI_BUBBLE; _mode = GUI_BUBBLE;
@@ -609,8 +638,7 @@ static void bt_device_click_cb(lv_event_t * e) {
if (strcmp(txt, "Back") == 0) { if (strcmp(txt, "Back") == 0) {
LOCK(); LOCK();
bt_stop_discovery(); bt_stop_discovery();
_mode = GUI_MENU; menu_go_back();
show_menu();
UNLOCK(); UNLOCK();
return; return;
} else if (strcmp(txt, "Refresh") == 0) { } else if (strcmp(txt, "Refresh") == 0) {
@@ -803,6 +831,10 @@ static lv_obj_t* create_volume_page(void) {
lv_obj_align(instr2, LV_ALIGN_BOTTOM_MID, 0, -10); lv_obj_align(instr2, LV_ALIGN_BOTTOM_MID, 0, -10);
#endif #endif
// Add Back button
lv_obj_t* back_btn = addMenuItem(_volume_page, "Back");
lv_obj_add_event_cb(back_btn, btn_click_cb, LV_EVENT_CLICKED, NULL);
return _volume_page; return _volume_page;
} }
@@ -831,6 +863,64 @@ static void show_volume_control(void) {
ESP_LOGI(TAG, "Volume control displayed"); ESP_LOGI(TAG, "Volume control displayed");
} }
// Menu stack management functions
static void menu_stack_push(lv_obj_t *page) {
if (_menuStack.count < MAX_MENU_STACK_SIZE && page != NULL) {
_menuStack.pages[_menuStack.count++] = page;
ESP_LOGI(TAG, "Menu stack push: page=%p, count=%d", page, _menuStack.count);
} else if (_menuStack.count >= MAX_MENU_STACK_SIZE) {
ESP_LOGW(TAG, "Menu stack overflow, cannot push more pages");
}
}
static lv_obj_t* menu_stack_pop(void) {
if (_menuStack.count > 0) {
lv_obj_t *page = _menuStack.pages[--_menuStack.count];
ESP_LOGI(TAG, "Menu stack pop: page=%p, count=%d", page, _menuStack.count);
return page;
}
ESP_LOGI(TAG, "Menu stack is empty, cannot pop");
return NULL;
}
static void menu_stack_clear(void) {
_menuStack.count = 0;
ESP_LOGI(TAG, "Menu stack cleared");
}
static bool menu_stack_is_empty(void) {
return (_menuStack.count == 0);
}
static void menu_go_back(void) {
ESP_LOGI(TAG, "Menu go back requested");
if (menu_stack_is_empty()) {
ESP_LOGI(TAG, "Menu stack empty, returning to bubble mode");
_mode = GUI_BUBBLE;
show_bubble();
return;
}
lv_obj_t *previous_page = menu_stack_pop();
if (previous_page != NULL) {
ESP_LOGI(TAG, "Returning to previous menu page");
lv_obj_t* menu = create_menu_container();
if (menu) {
lv_menu_set_page(menu, previous_page);
_currentPage = previous_page;
_mode = GUI_MENU;
lv_obj_remove_flag(menu, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(_bubble, LV_OBJ_FLAG_HIDDEN);
}
} else {
ESP_LOGI(TAG, "No previous page found, returning to bubble mode");
_mode = GUI_BUBBLE;
show_bubble();
}
}
static void update_volume_display(int volume) { static void update_volume_display(int volume) {
if (_volume_bar && _volume_label) { if (_volume_bar && _volume_label) {
LOCK(); LOCK();
@@ -1029,8 +1119,7 @@ static void gui_task(void *pvParameters)
LOCK(); LOCK();
if (_mode == GUI_MENU) if (_mode == GUI_MENU)
{ {
_mode = GUI_BUBBLE; menu_go_back(); // Use menu stack navigation
show_bubble(); // Cleanup menu and show bubble
} }
else else
{ {

View File

@@ -325,7 +325,8 @@ void app_main(void)
#else #else
while (1) while (1)
{ {
vTaskDelay(pdMS_TO_TICKS(1000)); system_processNvsRequests();
vTaskDelay(pdMS_TO_TICKS(10));
} }
#endif #endif
} }

View File

@@ -1,20 +1,34 @@
#include "system.h" #include "system.h"
#include "esp_log.h" #include "esp_log.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_timer.h"
#include <string.h>
static SystemState_t _systemState; static SystemState_t _systemState;
static EventGroupHandle_t _systemEvent; static EventGroupHandle_t _systemEvent;
static EventManager_t _eventManager; static EventManager_t _eventManager;
static QueueHandle_t _nvsRequestQueue;
static const char* NVS_NAMESPACE = "bt_devices";
static const char* NVS_KEY_COUNT = "count";
static esp_err_t nvs_load_devices_internal(paired_device_t *devices, size_t *count);
static esp_err_t nvs_save_devices_internal(const paired_device_t *devices, size_t count);
void system_init(void) void system_init(void)
{ {
_systemState.zeroAngle = 0.0f; _systemState.zeroAngle = 0.0f;
_systemState.primaryAxis = PRIMARY_AXIS; _systemState.primaryAxis = PRIMARY_AXIS;
_systemState.pairedDeviceCount = 0;
_systemEvent = xEventGroupCreate(); _systemEvent = xEventGroupCreate();
_eventManager.count = 0; _eventManager.count = 0;
_eventManager.mutex = xSemaphoreCreateMutex(); _eventManager.mutex = xSemaphoreCreateMutex();
system_initNvsService();
} }
int system_getPrimaryAxis(void) int system_getPrimaryAxis(void)
@@ -198,4 +212,271 @@ void system_requestVolumeUp(void) {
void system_requestVolumeDown(void) { void system_requestVolumeDown(void) {
ESP_LOGI("system", "Volume Down requested"); ESP_LOGI("system", "Volume Down requested");
system_notifyAll(EM_EVENT_VOLUME_DOWN); system_notifyAll(EM_EVENT_VOLUME_DOWN);
}
void system_initNvsService(void) {
_nvsRequestQueue = xQueueCreate(10, sizeof(nvs_request_t));
if (_nvsRequestQueue == NULL) {
ESP_LOGE("system", "Failed to create NVS request queue");
}
memset(_systemState.pairedDevices, 0, sizeof(_systemState.pairedDevices));
_systemState.pairedDeviceCount = 0;
paired_device_t devices[MAX_PAIRED_DEVICES];
size_t count = 0;
if (nvs_load_devices_internal(devices, &count) == ESP_OK) {
xSemaphoreTake(_eventManager.mutex, portMAX_DELAY);
memcpy(_systemState.pairedDevices, devices, count * sizeof(paired_device_t));
_systemState.pairedDeviceCount = count;
xSemaphoreGive(_eventManager.mutex);
ESP_LOGI("system", "Loaded %d paired devices from NVS", count);
}
}
static esp_err_t nvs_load_devices_internal(paired_device_t *devices, size_t *count) {
nvs_handle_t nvs_handle;
esp_err_t ret;
ret = nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs_handle);
if (ret != ESP_OK) {
ESP_LOGE("system", "Failed to open NVS namespace: %s", esp_err_to_name(ret));
*count = 0;
return ret;
}
size_t device_count = 0;
size_t required_size = sizeof(size_t);
ret = nvs_get_blob(nvs_handle, NVS_KEY_COUNT, &device_count, &required_size);
if (ret != ESP_OK) {
nvs_close(nvs_handle);
*count = 0;
return ESP_OK;
}
device_count = (device_count > MAX_PAIRED_DEVICES) ? MAX_PAIRED_DEVICES : device_count;
for (size_t i = 0; i < device_count; i++) {
char key[16];
snprintf(key, sizeof(key), "device_%zu", i);
required_size = sizeof(paired_device_t);
ret = nvs_get_blob(nvs_handle, key, &devices[i], &required_size);
if (ret != ESP_OK) {
ESP_LOGW("system", "Failed to load device %zu", i);
device_count = i;
break;
}
}
nvs_close(nvs_handle);
*count = device_count;
return ESP_OK;
}
static esp_err_t nvs_save_devices_internal(const paired_device_t *devices, size_t count) {
nvs_handle_t nvs_handle;
esp_err_t ret;
ret = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs_handle);
if (ret != ESP_OK) {
ESP_LOGE("system", "Failed to open NVS namespace for write: %s", esp_err_to_name(ret));
return ret;
}
ret = nvs_set_blob(nvs_handle, NVS_KEY_COUNT, &count, sizeof(size_t));
if (ret != ESP_OK) {
nvs_close(nvs_handle);
return ret;
}
for (size_t i = 0; i < count; i++) {
char key[16];
snprintf(key, sizeof(key), "device_%zu", i);
ret = nvs_set_blob(nvs_handle, key, &devices[i], sizeof(paired_device_t));
if (ret != ESP_OK) {
nvs_close(nvs_handle);
return ret;
}
}
ret = nvs_commit(nvs_handle);
nvs_close(nvs_handle);
return ret;
}
void system_processNvsRequests(void) {
nvs_request_t request;
while (xQueueReceive(_nvsRequestQueue, &request, 0) == pdTRUE) {
xSemaphoreTake(_eventManager.mutex, portMAX_DELAY);
switch (request.operation) {
case NVS_OP_SAVE_DEVICE:
request.result = nvs_save_devices_internal(_systemState.pairedDevices, _systemState.pairedDeviceCount);
break;
case NVS_OP_LOAD_DEVICES:
request.result = nvs_load_devices_internal(_systemState.pairedDevices, &_systemState.pairedDeviceCount);
break;
case NVS_OP_REMOVE_DEVICE: {
size_t removed_index = SIZE_MAX;
for (size_t i = 0; i < _systemState.pairedDeviceCount; i++) {
if (memcmp(_systemState.pairedDevices[i].bda, request.bda, ESP_BD_ADDR_LEN) == 0) {
removed_index = i;
break;
}
}
if (removed_index != SIZE_MAX) {
for (size_t i = removed_index; i < _systemState.pairedDeviceCount - 1; i++) {
_systemState.pairedDevices[i] = _systemState.pairedDevices[i + 1];
}
_systemState.pairedDeviceCount--;
request.result = nvs_save_devices_internal(_systemState.pairedDevices, _systemState.pairedDeviceCount);
} else {
request.result = ESP_ERR_NOT_FOUND;
}
break;
}
case NVS_OP_UPDATE_TIMESTAMP: {
for (size_t i = 0; i < _systemState.pairedDeviceCount; i++) {
if (memcmp(_systemState.pairedDevices[i].bda, request.bda, ESP_BD_ADDR_LEN) == 0) {
_systemState.pairedDevices[i].last_connected = (uint32_t)(esp_timer_get_time() / 1000000);
request.result = nvs_save_devices_internal(_systemState.pairedDevices, _systemState.pairedDeviceCount);
break;
}
}
break;
}
default:
request.result = ESP_ERR_INVALID_ARG;
break;
}
xSemaphoreGive(_eventManager.mutex);
request.response_ready = true;
if (request.requestor) {
xTaskNotify(request.requestor, (uint32_t)&request, eSetValueWithOverwrite);
}
}
}
esp_err_t system_savePairedDevice(const paired_device_t *device) {
if (!device) return ESP_ERR_INVALID_ARG;
xSemaphoreTake(_eventManager.mutex, portMAX_DELAY);
size_t existing_index = SIZE_MAX;
for (size_t i = 0; i < _systemState.pairedDeviceCount; i++) {
if (memcmp(_systemState.pairedDevices[i].bda, device->bda, ESP_BD_ADDR_LEN) == 0) {
existing_index = i;
break;
}
}
if (existing_index != SIZE_MAX) {
_systemState.pairedDevices[existing_index] = *device;
} else if (_systemState.pairedDeviceCount < MAX_PAIRED_DEVICES) {
_systemState.pairedDevices[_systemState.pairedDeviceCount++] = *device;
} else {
xSemaphoreGive(_eventManager.mutex);
return ESP_ERR_NO_MEM;
}
xSemaphoreGive(_eventManager.mutex);
nvs_request_t request = {
.operation = NVS_OP_SAVE_DEVICE,
.device = *device,
.requestor = xTaskGetCurrentTaskHandle()
};
if (xQueueSend(_nvsRequestQueue, &request, pdMS_TO_TICKS(NVS_TIMEOUT_MS)) == pdTRUE) {
uint32_t notification;
if (xTaskNotifyWait(0, UINT32_MAX, &notification, pdMS_TO_TICKS(NVS_TIMEOUT_MS)) == pdTRUE) {
nvs_request_t *response = (nvs_request_t*)notification;
return response->result;
}
}
return ESP_ERR_TIMEOUT;
}
esp_err_t system_loadPairedDevices(paired_device_t *devices, size_t *count) {
if (!devices || !count) return ESP_ERR_INVALID_ARG;
xSemaphoreTake(_eventManager.mutex, portMAX_DELAY);
size_t copy_count = (*count < _systemState.pairedDeviceCount) ? *count : _systemState.pairedDeviceCount;
memcpy(devices, _systemState.pairedDevices, copy_count * sizeof(paired_device_t));
*count = copy_count;
xSemaphoreGive(_eventManager.mutex);
return ESP_OK;
}
esp_err_t system_removePairedDevice(esp_bd_addr_t bda) {
nvs_request_t request = {
.operation = NVS_OP_REMOVE_DEVICE,
.requestor = xTaskGetCurrentTaskHandle()
};
memcpy(request.bda, bda, ESP_BD_ADDR_LEN);
if (xQueueSend(_nvsRequestQueue, &request, pdMS_TO_TICKS(NVS_TIMEOUT_MS)) == pdTRUE) {
uint32_t notification;
if (xTaskNotifyWait(0, UINT32_MAX, &notification, pdMS_TO_TICKS(NVS_TIMEOUT_MS)) == pdTRUE) {
nvs_request_t *response = (nvs_request_t*)notification;
return response->result;
}
}
return ESP_ERR_TIMEOUT;
}
bool system_isDeviceKnown(esp_bd_addr_t bda) {
bool known = false;
xSemaphoreTake(_eventManager.mutex, portMAX_DELAY);
for (size_t i = 0; i < _systemState.pairedDeviceCount; i++) {
if (memcmp(_systemState.pairedDevices[i].bda, bda, ESP_BD_ADDR_LEN) == 0) {
known = true;
break;
}
}
xSemaphoreGive(_eventManager.mutex);
return known;
}
esp_err_t system_getKnownDeviceCount(size_t *count) {
if (!count) return ESP_ERR_INVALID_ARG;
xSemaphoreTake(_eventManager.mutex, portMAX_DELAY);
*count = _systemState.pairedDeviceCount;
xSemaphoreGive(_eventManager.mutex);
return ESP_OK;
}
esp_err_t system_updateConnectionTimestamp(esp_bd_addr_t bda) {
nvs_request_t request = {
.operation = NVS_OP_UPDATE_TIMESTAMP,
.requestor = xTaskGetCurrentTaskHandle()
};
memcpy(request.bda, bda, ESP_BD_ADDR_LEN);
if (xQueueSend(_nvsRequestQueue, &request, pdMS_TO_TICKS(NVS_TIMEOUT_MS)) == pdTRUE) {
uint32_t notification;
if (xTaskNotifyWait(0, UINT32_MAX, &notification, pdMS_TO_TICKS(NVS_TIMEOUT_MS)) == pdTRUE) {
nvs_request_t *response = (nvs_request_t*)notification;
return response->result;
}
}
return ESP_ERR_TIMEOUT;
}
const paired_device_t* system_getPairedDevices(size_t *count) {
if (!count) return NULL;
xSemaphoreTake(_eventManager.mutex, portMAX_DELAY);
*count = _systemState.pairedDeviceCount;
xSemaphoreGive(_eventManager.mutex);
return (const paired_device_t*)_systemState.pairedDevices;
} }

View File

@@ -4,9 +4,22 @@
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "freertos/semphr.h" #include "freertos/semphr.h"
#include "freertos/queue.h"
#include "esp_bt_defs.h"
#define DISABLE_GUI #define DISABLE_GUI
#define MAX_PAIRED_DEVICES 10
#define DEVICE_NAME_MAX_LEN 32
// Forward declaration of paired_device_t
typedef struct {
char name[DEVICE_NAME_MAX_LEN];
esp_bd_addr_t bda;
uint32_t last_connected;
bool is_connected;
} paired_device_t;
enum enum
{ {
ANGLE_XY = 0, ANGLE_XY = 0,
@@ -34,6 +47,10 @@ typedef struct SystemState_s
// BT event data // BT event data
int btDeviceIndex; int btDeviceIndex;
// NVS cached data
paired_device_t pairedDevices[MAX_PAIRED_DEVICES];
size_t pairedDeviceCount;
} SystemState_t; } SystemState_t;
@@ -88,5 +105,36 @@ int system_getBtDeviceIndex(void);
void system_requestVolumeUp(void); void system_requestVolumeUp(void);
void system_requestVolumeDown(void); void system_requestVolumeDown(void);
// NVS Service
typedef enum {
NVS_OP_SAVE_DEVICE,
NVS_OP_LOAD_DEVICES,
NVS_OP_REMOVE_DEVICE,
NVS_OP_IS_DEVICE_KNOWN,
NVS_OP_GET_DEVICE_COUNT,
NVS_OP_UPDATE_TIMESTAMP
} nvs_operation_type_t;
typedef struct {
nvs_operation_type_t operation;
paired_device_t device;
esp_bd_addr_t bda;
TaskHandle_t requestor;
esp_err_t result;
bool response_ready;
} nvs_request_t;
void system_initNvsService(void);
void system_processNvsRequests(void);
esp_err_t system_savePairedDevice(const paired_device_t *device);
esp_err_t system_loadPairedDevices(paired_device_t *devices, size_t *count);
esp_err_t system_removePairedDevice(esp_bd_addr_t bda);
bool system_isDeviceKnown(esp_bd_addr_t bda);
esp_err_t system_getKnownDeviceCount(size_t *count);
esp_err_t system_updateConnectionTimestamp(esp_bd_addr_t bda);
const paired_device_t* system_getPairedDevices(size_t *count);
#define NVS_TIMEOUT_MS 5000
#endif #endif

View File

@@ -1,4 +1,4 @@
set(PROJECT_NAME "soundshot") set(PROJECT_NAME "soundshot")
set(CHIP "esp32") set(CHIP "esp32")
set(PORT "COM3") set(PORT "COM14")
set(BAUD "460800") set(BAUD "460800")

View File

@@ -449,7 +449,7 @@ CONFIG_BT_CONTROLLER_ENABLED=y
# #
# Bluedroid Options # Bluedroid Options
# #
CONFIG_BT_BTC_TASK_STACK_SIZE=3072 CONFIG_BT_BTC_TASK_STACK_SIZE=8192
CONFIG_BT_BLUEDROID_PINNED_TO_CORE_0=y CONFIG_BT_BLUEDROID_PINNED_TO_CORE_0=y
# CONFIG_BT_BLUEDROID_PINNED_TO_CORE_1 is not set # CONFIG_BT_BLUEDROID_PINNED_TO_CORE_1 is not set
CONFIG_BT_BLUEDROID_PINNED_TO_CORE=0 CONFIG_BT_BLUEDROID_PINNED_TO_CORE=0
@@ -2275,7 +2275,7 @@ CONFIG_LV_FONT_UNSCII_8=y
CONFIG_LV_FONT_UNSCII_16=y CONFIG_LV_FONT_UNSCII_16=y
# end of Enable built-in fonts # end of Enable built-in fonts
CONFIG_LV_FONT_DEFAULT_MONTSERRAT_8=y # CONFIG_LV_FONT_DEFAULT_MONTSERRAT_8 is not set
# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_10 is not set # CONFIG_LV_FONT_DEFAULT_MONTSERRAT_10 is not set
# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_12 is not set # CONFIG_LV_FONT_DEFAULT_MONTSERRAT_12 is not set
# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_14 is not set # CONFIG_LV_FONT_DEFAULT_MONTSERRAT_14 is not set
@@ -2300,7 +2300,7 @@ CONFIG_LV_FONT_DEFAULT_MONTSERRAT_8=y
# CONFIG_LV_FONT_DEFAULT_DEJAVU_16_PERSIAN_HEBREW is not set # CONFIG_LV_FONT_DEFAULT_DEJAVU_16_PERSIAN_HEBREW is not set
# CONFIG_LV_FONT_DEFAULT_SIMSUN_14_CJK is not set # CONFIG_LV_FONT_DEFAULT_SIMSUN_14_CJK is not set
# CONFIG_LV_FONT_DEFAULT_SIMSUN_16_CJK is not set # CONFIG_LV_FONT_DEFAULT_SIMSUN_16_CJK is not set
# CONFIG_LV_FONT_DEFAULT_UNSCII_8 is not set CONFIG_LV_FONT_DEFAULT_UNSCII_8=y
# CONFIG_LV_FONT_DEFAULT_UNSCII_16 is not set # CONFIG_LV_FONT_DEFAULT_UNSCII_16 is not set
# CONFIG_LV_FONT_FMT_TXT_LARGE is not set # CONFIG_LV_FONT_FMT_TXT_LARGE is not set
# CONFIG_LV_USE_FONT_COMPRESSED is not set # CONFIG_LV_USE_FONT_COMPRESSED is not set
@@ -2515,7 +2515,7 @@ CONFIG_ESP32_APPTRACE_DEST_NONE=y
CONFIG_ESP32_APPTRACE_LOCK_ENABLE=y CONFIG_ESP32_APPTRACE_LOCK_ENABLE=y
CONFIG_BLUEDROID_ENABLED=y CONFIG_BLUEDROID_ENABLED=y
# CONFIG_NIMBLE_ENABLED is not set # CONFIG_NIMBLE_ENABLED is not set
CONFIG_BTC_TASK_STACK_SIZE=3072 CONFIG_BTC_TASK_STACK_SIZE=8192
CONFIG_BLUEDROID_PINNED_TO_CORE_0=y CONFIG_BLUEDROID_PINNED_TO_CORE_0=y
# CONFIG_BLUEDROID_PINNED_TO_CORE_1 is not set # CONFIG_BLUEDROID_PINNED_TO_CORE_1 is not set
CONFIG_BLUEDROID_PINNED_TO_CORE=0 CONFIG_BLUEDROID_PINNED_TO_CORE=0

View File

@@ -1,7 +1,7 @@
{ {
"project_name": "soundshot", "project_name": "soundshot",
"chip": "esp32", "chip": "esp32",
"port": "COM3", "port": "COM14",
"monitor_baud": 115200, "monitor_baud": 115200,
"flash_baud": 460800, "flash_baud": 460800,
"flash_mode": "dio", "flash_mode": "dio",