diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..cdd5577 --- /dev/null +++ b/.claude/settings.local.json @@ -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": [] + } +} \ No newline at end of file diff --git a/docker.md b/docker.md new file mode 100644 index 0000000..8d4d899 --- /dev/null +++ b/docker.md @@ -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 repo’s `README.md`. It’s 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 container’s 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 doesn’t 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. diff --git a/flash_gui.bat b/flash_gui.bat new file mode 100644 index 0000000..3f1e226 --- /dev/null +++ b/flash_gui.bat @@ -0,0 +1,4 @@ +@echo off +echo Starting ESP32 Firmware Flash GUI... +python flash_tool/gui_flasher.py +pause \ No newline at end of file diff --git a/flash_tool/ESP32_Flasher.spec b/flash_tool/ESP32_Flasher.spec new file mode 100644 index 0000000..1cc65ad --- /dev/null +++ b/flash_tool/ESP32_Flasher.spec @@ -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, +) \ No newline at end of file diff --git a/flash_tool/build_exe.bat b/flash_tool/build_exe.bat new file mode 100644 index 0000000..1b4daff --- /dev/null +++ b/flash_tool/build_exe.bat @@ -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 \ No newline at end of file diff --git a/flash_tool/build_executable.py b/flash_tool/build_executable.py new file mode 100644 index 0000000..4ff124b --- /dev/null +++ b/flash_tool/build_executable.py @@ -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() \ No newline at end of file diff --git a/flash_tool/build_from_spec.bat b/flash_tool/build_from_spec.bat new file mode 100644 index 0000000..49cf1c1 --- /dev/null +++ b/flash_tool/build_from_spec.bat @@ -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 \ No newline at end of file diff --git a/flash_tool/build_with_auto_py_to_exe.bat b/flash_tool/build_with_auto_py_to_exe.bat new file mode 100644 index 0000000..fabc373 --- /dev/null +++ b/flash_tool/build_with_auto_py_to_exe.bat @@ -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 \ No newline at end of file diff --git a/flash_tool/gui_flasher.py b/flash_tool/gui_flasher.py new file mode 100644 index 0000000..0d79e7c --- /dev/null +++ b/flash_tool/gui_flasher.py @@ -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() \ No newline at end of file diff --git a/flash_tool/requirements.txt b/flash_tool/requirements.txt new file mode 100644 index 0000000..d77ef91 --- /dev/null +++ b/flash_tool/requirements.txt @@ -0,0 +1,3 @@ +esptool>=4.0 +pyserial>=3.5 +pyinstaller>=5.0 \ No newline at end of file diff --git a/main/bt_app.c b/main/bt_app.c index 16c7560..251fc44 100644 --- a/main/bt_app.c +++ b/main/bt_app.c @@ -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 */ static char *bda2str(esp_bd_addr_t bda, char *str, size_t size); -/* NVS storage functions for paired devices */ -typedef struct { - esp_bd_addr_t bda; - char name[ESP_BT_GAP_MAX_BDNAME_LEN + 1]; - uint32_t last_connected; -} 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); +static esp_err_t bt_try_connect_known_devices(void); +static void bt_debug_print_known_devices(void); +static esp_err_t bt_add_discovered_device(esp_bd_addr_t bda, const char *name); +static esp_err_t bt_try_connect_all_known_devices(void); +static esp_err_t bt_try_next_known_device(void); /* A2DP application state machine handler for each state */ 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 ********************************/ -static esp_err_t nvs_save_paired_device(const paired_device_t *device) -{ - 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; -} +// This function is no longer used - replaced by system_savePairedDevice -static esp_err_t nvs_load_paired_devices(paired_device_t *devices, size_t *count) -{ - 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; -} +// This function is no longer used - replaced by system_loadPairedDevices -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]; size_t count = MAX_PAIRED_DEVICES; - if (nvs_load_paired_devices(devices, &count) != ESP_OK) { - 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); + esp_err_t ret = system_loadPairedDevices(devices, &count); if (ret != ESP_OK || count == 0) { ESP_LOGI(BT_AV_TAG, "No known devices to connect to"); return ESP_ERR_NOT_FOUND; @@ -355,14 +215,14 @@ static esp_err_t nvs_try_connect_known_devices(void) 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]; - size_t count = MAX_PAIRED_DEVICES; + const paired_device_t* devices; + size_t count = 0; - esp_err_t ret = nvs_load_paired_devices(devices, &count); - if (ret != ESP_OK) { - ESP_LOGE(BT_AV_TAG, "Failed to load devices for debug: %s", esp_err_to_name(ret)); + devices = system_getPairedDevices(&count); + if (!devices) { + ESP_LOGE(BT_AV_TAG, "Failed to load devices for debug"); return; } @@ -377,12 +237,12 @@ static void nvs_debug_print_known_devices(void) 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]; 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) { return ESP_ERR_NOT_FOUND; } @@ -438,7 +298,7 @@ static esp_err_t nvs_remove_paired_device(esp_bd_addr_t bda) 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) { return ESP_ERR_INVALID_ARG; @@ -462,14 +322,14 @@ static esp_err_t nvs_get_known_device_count(size_t *count) 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) { return ESP_ERR_INVALID_ARG; } // Check if device is already known - if (nvs_is_device_known(bda)) { + if (system_isDeviceKnown(bda)) { char bda_str[18]; ESP_LOGD(BT_AV_TAG, "Device %s (%s) already known, skipping", 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 paired_device_t device; memcpy(device.bda, bda, ESP_BD_ADDR_LEN); - strncpy(device.name, name, ESP_BT_GAP_MAX_BDNAME_LEN); - device.name[ESP_BT_GAP_MAX_BDNAME_LEN] = '\0'; + strncpy(device.name, name, DEVICE_NAME_MAX_LEN - 1); + device.name[DEVICE_NAME_MAX_LEN - 1] = '\0'; device.last_connected = 0; // Never connected, set to 0 // Save to NVS - esp_err_t ret = nvs_save_paired_device(&device); + esp_err_t ret = system_savePairedDevice(&device); if (ret == ESP_OK) { char bda_str[18]; 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; } -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) { 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]; 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) { ESP_LOGW(BT_AV_TAG, "No devices found to update timestamp"); 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 // Save updated device - ret = nvs_save_paired_device(&devices[device_index]); + ret = system_savePairedDevice(&devices[device_index]); if (ret == ESP_OK) { char bda_str[18]; 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; } -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 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) { ESP_LOGI(BT_AV_TAG, "No known devices to connect to"); s_current_device_index = -1; @@ -587,7 +447,7 @@ static esp_err_t nvs_try_connect_all_known_devices(void) 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) { 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); // 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_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 { /* not discovered, 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) { 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(); // 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 - 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) { // No known devices found, start discovery to find new devices 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; case BT_APP_HEART_BEAT_EVT: { // 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) { // No known devices, start 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; // 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) { ESP_LOGI(BT_AV_TAG, "Connection failed, trying next device..."); // 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) { // 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; @@ -1281,7 +1146,7 @@ static void bt_app_av_state_connecting_hdlr(uint16_t event, void *param) if (++s_connecting_intv >= 2) { ESP_LOGI(BT_AV_TAG, "Connection timeout, trying next device..."); // 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) { // No more devices to try, go to unconnected state 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; 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) { ESP_LOGI(BT_AV_TAG, "Successfully loaded %d paired devices from NVS", (int)count); for (size_t i = 0; i < count; i++) { diff --git a/main/gui.c b/main/gui.c index cc330f8..33ec22b 100644 --- a/main/gui.c +++ b/main/gui.c @@ -72,6 +72,13 @@ static lv_obj_t* create_volume_page(void); static void update_volume_display(int volume); 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 VISIBLE_ITEMS 3 #define MENU_MAX_STRING_LENGTH 30 @@ -91,6 +98,15 @@ static lv_obj_t *_currentPage = NULL; 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 */ static lv_style_t _styleUnfocusedBtn; @@ -135,6 +151,9 @@ static void show_menu(void) { lv_obj_t* menu = create_menu_container(); 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); _currentPage = main_page; @@ -180,7 +199,8 @@ static void lcd_init(void) .miso_io_num = -1, .quadwp_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)); @@ -236,7 +256,8 @@ static void lvgl_init(void) const lvgl_port_display_cfg_t disp_cfg = { .io_handle = io_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, .hres = LCD_H_RES, .vres = LCD_V_RES, @@ -347,12 +368,20 @@ static void btn_click_cb(lv_event_t * e) { // Handle specific menu items if (strcmp(txt, "Bluetooth") == 0) { LOCK(); + // Push current page onto stack before navigating + menu_stack_push(_currentPage); show_bt_device_list(); UNLOCK(); } else if (strcmp(txt, "Volume") == 0) { LOCK(); + // Push current page onto stack before navigating + menu_stack_push(_currentPage); show_volume_control(); UNLOCK(); + } else if (strcmp(txt, "Back") == 0) { + LOCK(); + menu_go_back(); + UNLOCK(); } else if (strcmp(txt, "Exit") == 0) { LOCK(); _mode = GUI_BUBBLE; @@ -609,8 +638,7 @@ static void bt_device_click_cb(lv_event_t * e) { if (strcmp(txt, "Back") == 0) { LOCK(); bt_stop_discovery(); - _mode = GUI_MENU; - show_menu(); + menu_go_back(); UNLOCK(); return; } 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); #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; } @@ -831,6 +863,64 @@ static void show_volume_control(void) { 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) { if (_volume_bar && _volume_label) { LOCK(); @@ -1029,8 +1119,7 @@ static void gui_task(void *pvParameters) LOCK(); if (_mode == GUI_MENU) { - _mode = GUI_BUBBLE; - show_bubble(); // Cleanup menu and show bubble + menu_go_back(); // Use menu stack navigation } else { diff --git a/main/main.c b/main/main.c index 90828a8..89fbfec 100644 --- a/main/main.c +++ b/main/main.c @@ -325,7 +325,8 @@ void app_main(void) #else while (1) { - vTaskDelay(pdMS_TO_TICKS(1000)); + system_processNvsRequests(); + vTaskDelay(pdMS_TO_TICKS(10)); } #endif } diff --git a/main/system.c b/main/system.c index 78c187b..944b1d6 100644 --- a/main/system.c +++ b/main/system.c @@ -1,20 +1,34 @@ #include "system.h" #include "esp_log.h" +#include "nvs_flash.h" +#include "nvs.h" +#include "esp_timer.h" +#include static SystemState_t _systemState; static EventGroupHandle_t _systemEvent; 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) { _systemState.zeroAngle = 0.0f; _systemState.primaryAxis = PRIMARY_AXIS; + _systemState.pairedDeviceCount = 0; _systemEvent = xEventGroupCreate(); _eventManager.count = 0; _eventManager.mutex = xSemaphoreCreateMutex(); + + system_initNvsService(); } int system_getPrimaryAxis(void) @@ -198,4 +212,271 @@ void system_requestVolumeUp(void) { void system_requestVolumeDown(void) { ESP_LOGI("system", "Volume Down requested"); 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, ¬ification, 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, ¬ification, 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, ¬ification, 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; } \ No newline at end of file diff --git a/main/system.h b/main/system.h index 77d5cdd..3e6e029 100644 --- a/main/system.h +++ b/main/system.h @@ -4,9 +4,22 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" +#include "freertos/queue.h" +#include "esp_bt_defs.h" #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 { ANGLE_XY = 0, @@ -34,6 +47,10 @@ typedef struct SystemState_s // BT event data int btDeviceIndex; + + // NVS cached data + paired_device_t pairedDevices[MAX_PAIRED_DEVICES]; + size_t pairedDeviceCount; } SystemState_t; @@ -88,5 +105,36 @@ int system_getBtDeviceIndex(void); void system_requestVolumeUp(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 \ No newline at end of file diff --git a/project_settings.cmake b/project_settings.cmake index 1788cf6..79af8d7 100644 --- a/project_settings.cmake +++ b/project_settings.cmake @@ -1,4 +1,4 @@ set(PROJECT_NAME "soundshot") set(CHIP "esp32") -set(PORT "COM3") +set(PORT "COM14") set(BAUD "460800") diff --git a/sdkconfig b/sdkconfig index 154051c..568831a 100644 --- a/sdkconfig +++ b/sdkconfig @@ -449,7 +449,7 @@ CONFIG_BT_CONTROLLER_ENABLED=y # # 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_1 is not set CONFIG_BT_BLUEDROID_PINNED_TO_CORE=0 @@ -2275,7 +2275,7 @@ CONFIG_LV_FONT_UNSCII_8=y CONFIG_LV_FONT_UNSCII_16=y # 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_12 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_SIMSUN_14_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_FMT_TXT_LARGE 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_BLUEDROID_ENABLED=y # 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_1 is not set CONFIG_BLUEDROID_PINNED_TO_CORE=0 diff --git a/settings.json b/settings.json index d5349dd..2666604 100644 --- a/settings.json +++ b/settings.json @@ -1,7 +1,7 @@ { "project_name": "soundshot", "chip": "esp32", - "port": "COM3", + "port": "COM14", "monitor_baud": 115200, "flash_baud": 460800, "flash_mode": "dio",