diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57ce3bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# ESP-IDF build artefacts +firmware/build/ +firmware/sdkconfig +firmware/dependencies.lock +firmware/managed_components/ + +# Node.js +companion-server/node_modules/ +companion-server/.env +companion-server/.wwebjs_auth/ +companion-server/.wwebjs_cache/ +companion-server/debbie_memory.db + +# OS +.DS_Store +Thumbs.db + +# Editor +.vscode/ +*.swp +*.swo diff --git a/README.md b/README.md index 067068a..dc8f24f 100644 --- a/README.md +++ b/README.md @@ -1 +1,398 @@ -# DebbieDoesMobile \ No newline at end of file +# โœจ DebbieDoesMobile + +> **Debbie** โ€” Your Portable Personal AI Friend on the Freenove Media Kit ESP32-S3 + +Debbie is a full-featured personal AI companion that lives on the Freenove Media Kit for ESP32-S3 (FNK0102). She can hold voice conversations, see through the camera, read your WhatsApp and email, control Spotify, and connect to your own AI agent. + +--- + +## ๐Ÿ“ธ Debbie's UI + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๐Ÿ“ถ OK ๐Ÿค– AI โ•‘ โœจ Debbie โ•‘ ๐Ÿ”‹ 85% ๐Ÿ”” 3 โ”‚ โ† status bar +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ โ”‚ +โ”‚ โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ ๐Ÿ’ฌ Hi! I'm Debbie ๐Ÿ˜Š โ”‚ +โ”‚ ( โ—‰ โ—‰ ) โ”‚ โ”‚ +โ”‚ ( โ€ฟ ) โ”‚ I can chat, see what my โ”‚ +โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ”‚ camera sees, read your โ”‚ +โ”‚ โ”‚ messages, and control music.โ”‚ +โ”‚ Ready! ๐Ÿ˜Š โ”‚ โ”‚ +โ”‚ โ”‚ Press centre button or โ”‚ +โ”‚ โ”‚ just say something! โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ๐ŸŽต Artist โ€” Song Title โ–ถ โญ โ”‚ โ† Spotify +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +The face animates โ€” eyes open wide when listening, half-close when thinking, and blink periodically when idle. ๐Ÿ˜Š + +--- + +## ๐ŸŽฏ Features + +| Feature | Description | +|---------|-------------| +| ๐Ÿ—ฃ๏ธ **Voice Conversations** | Real-time bidirectional voice via OpenAI Realtime API (gpt-4o-realtime-preview) | +| ๐Ÿ“ท **Camera Vision** | Capture photos; Debbie describes what she sees using gpt-4o vision | +| ๐Ÿ’ฌ **WhatsApp Pager** | Receive WhatsApp messages as notifications on-device | +| ๐Ÿ“ง **Email Monitor** | IMAP email monitoring โ€” important emails pushed to Debbie | +| ๐ŸŽต **Spotify Control** | Play, pause, skip, search tracks by voice command | +| ๐Ÿ”” **Agent Notifications** | Connect to your own AI agent (e.g. D3881E) for custom notifications | +| โš™๏ธ **Captive-Portal Setup** | First-run web UI at `192.168.4.1` for easy configuration | +| ๐Ÿ”‹ **Battery Monitor** | Battery percentage shown in status bar | +| ๐ŸŒˆ **Animated LVGL UI** | Friendly face with expressions, blink animations, status bar | +| ๐Ÿ”„ **Auto-reconnect** | Automatically reconnects to WiFi and AI if connection drops | +| ๐Ÿ“ฆ **OTA Updates** | Dual-partition layout for over-the-air firmware updates | + +--- + +## ๐Ÿ›’ Hardware Requirements + +| Item | Details | +|------|---------| +| **Main Board** | [Freenove Media Kit ESP32-S3 (FNK0102)](https://www.freenove.com/FNK0102) | +| **Display** | 3.5" 480ร—320 TFT (ST7796) or 1.14" 135ร—240 TFT (ST7789) โ€” comes with kit | +| **Camera** | OV2640 โ€” included in kit | +| **Microphone** | MEMS microphone โ€” included | +| **Speaker** | 4ฮฉ/3W (3.5") or 8ฮฉ/1W (1.14") โ€” included | +| **USB Cable** | USB-C for flashing | +| **WiFi** | 2.4 GHz access point | +| **PC/Server** | To run the companion server (any machine on the same network) | + +--- + +## ๐Ÿš€ Quick Start + +### 1. Set Up the Firmware + +#### Prerequisites +- [ESP-IDF v5.2+](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/get-started/index.html) +- Python 3.8+, Git + +```bash +# Clone this repository +git clone https://github.com/magicalmutation-coder/DebbieDoesMobile.git +cd DebbieDoesMobile/firmware + +# Set target to ESP32-S3 +idf.py set-target esp32s3 + +# Build +idf.py build + +# Flash (replace /dev/ttyUSB0 with your port) +idf.py -p /dev/ttyUSB0 flash monitor +``` + +> **Windows:** Use `idf.py -p COM3 flash monitor` + +### 2. First-Run Setup + +1. After flashing, Debbie creates a WiFi hotspot called **"Debbie"** (no password). +2. Connect your phone or computer to **"Debbie"**. +3. Open a browser and navigate to **`http://192.168.4.1`**. +4. Fill in: + - Your home WiFi SSID and password + - Your [OpenAI API key](https://platform.openai.com/api-keys) (starts with `sk-`) + - Companion server URL (optional โ€” see below) +5. Click **Save & Connect**. + +Debbie will reboot, connect to your WiFi, and be ready to chat! ๐ŸŽ‰ + +### 3. Set Up the Companion Server (Optional but recommended) + +The companion server enables WhatsApp, email, and Spotify integration. + +```bash +cd companion-server + +# Install dependencies +npm install + +# Copy and edit environment variables +cp .env.example .env +nano .env # fill in your credentials + +# Start the server +npm start +``` + +The server runs on port 3001 by default. Enter its URL in Debbie's setup page: +`ws://YOUR_PC_IP:3001` + +--- + +## โš™๏ธ Configuration + +All settings are stored in flash (NVS) and can be updated via the web UI at `http://192.168.4.1/` (accessible via the Debbie AP, or on your local network at Debbie's IP address). + +| Setting | Description | Default | +|---------|-------------|---------| +| `wifi_ssid` | Your home WiFi network name | โ€” | +| `wifi_password` | WiFi password | โ€” | +| `openai_api_key` | OpenAI API key (sk-...) | โ€” | +| `agent_ws_url` | Custom agent WebSocket URL | โ€” | +| `companion_url` | Companion server WebSocket URL | โ€” | +| `debbie_name` | Name shown on screen | Debbie | +| `system_prompt` | Debbie's persona prompt | friendly AI companion | +| `speaker_volume` | Speaker volume 0โ€“100 | 75 | + +--- + +## ๐ŸŽฎ Controls + +| Button | Action | +|--------|--------| +| **Centre** | Capture photo + ask Debbie what she sees | +| **Up** | Volume up | +| **Down** | Volume down | +| **Long press** | Read notifications aloud | +| **Voice** | Just speak naturally โ€” Debbie listens automatically | + +--- + +## ๐Ÿ—๏ธ Architecture + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Debbie ESP32-S3 โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” PCM24k โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ MEMS โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚ OpenAI Realtime API (WS) โ”‚ โ”‚ +โ”‚ โ”‚ Mic โ”‚ โ”‚ gpt-4o-realtime-preview โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ PCM24k โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ–ผ โ”‚ +โ”‚ โ”‚ Camera โ”‚โ”€ JPEG b64 โ”€โ–บ gpt-4o vision HTTP โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” WebSocket โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Display โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚ Companion Server (Node.js) โ”‚ โ”‚ +โ”‚ โ”‚ (LVGL) โ”‚ notify โ”‚ WhatsApp โ”‚ Email โ”‚ Spotify โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Firmware Components + +| File | Purpose | +|------|---------| +| `main/main.c` | App entry, state machine, event routing | +| `main/openai_client.c` | OpenAI Realtime API + Chat/Vision HTTP | +| `main/audio_manager.c` | I2S mic capture + speaker playback | +| `main/camera_manager.c` | OV2640 capture + JPEG/base64 encoding | +| `main/display_manager.c` | LVGL UI โ€” Debbie's face + status | +| `main/notification_client.c` | WebSocket client for companion server | +| `main/wifi_manager.c` | WiFi STA+AP, auto-reconnect | +| `main/web_server.c` | HTTP config portal + REST API | +| `main/storage_manager.c` | NVS settings persistence | +| `main/settings.h` | All GPIO pin definitions | + +### Companion Server Modules + +| File | Purpose | +|------|---------| +| `server.js` | Express + WebSocket server, device registry | +| `whatsapp.js` | WhatsApp Web.js integration | +| `email_monitor.js` | IMAP email polling | +| `spotify.js` | Spotify Web API controller | +| `agent_bridge.js` | Custom agent WebSocket bridge | + +--- + +## ๐Ÿ”’ Network Security Testing + +> โš ๏ธ **AUTHORISED USE ONLY** โ€” These tools are for testing **your own networks** or networks you have **explicit written permission** to test. Unauthorised network scanning is illegal under the Computer Misuse Act (UK), CFAA (US), and similar laws worldwide. + +Debbie includes a full ethical network security toolkit, voice-commanded through the AI. + +### What Debbie Can Scan + +| Tool | Description | +|------|-------------| +| ๐Ÿ” **WiFi Scanner** | Discover all visible networks, check encryption (flags WEP/open APs) | +| ๐Ÿ“ก **ARP Host Discovery** | Find all live devices on your subnet | +| ๐Ÿšช **Port Scanner** | Scan 65+ common service ports per host | +| ๐Ÿท๏ธ **Service Fingerprinting** | Identify running services from banners | +| ๐Ÿ•ต๏ธ **Vulnerability Analysis** | Check against 20+ known-dangerous configurations | +| ๐Ÿ“Š **Risk Scoring** | 0โ€“100 risk score per device with remediation | +| ๐ŸŒ **Web Probe** | Detect HTTP/HTTPS, grab server headers and page titles | +| ๐Ÿ”ฌ **CVE Lookup** | Query the NVD database for specific CVE details | +| ๐Ÿ—บ๏ธ **DNS Lookup** | Resolve hostnames from the device | + +### Checks Performed + +| Check | Severity | +|-------|---------| +| Open WiFi networks | HIGH | +| WEP encryption (crackable in seconds) | CRITICAL | +| Telnet exposed (plaintext credentials) | HIGH | +| Docker API on port 2375 (no auth) | CRITICAL | +| Redis/MongoDB/Elasticsearch (often unauthenticated) | HIGH | +| Modbus/S7 industrial protocols exposed | CRITICAL | +| VNC remote desktop exposed | HIGH | +| MQTT broker without authentication | MEDIUM | +| SNMP with default community strings | MEDIUM | +| HTTP without HTTPS | LOW | +| OpenSSH version fingerprinting | MEDIUM | + +### Voice Commands + +Just say it to Debbie โ€” no typing needed: + +- *"Scan my network"* โ†’ Full scan (WiFi + all devices + ports) +- *"What devices are on my network?"* โ†’ Quick host discovery +- *"Scan the WiFi networks nearby"* โ†’ WiFi-only scan +- *"Check 192.168.1.1 for vulnerabilities"* โ†’ Port scan a specific device +- *"What's the risk score for my router?"* โ†’ Risk assessment +- *"Look up CVE-2021-44228"* โ†’ Log4Shell CVE details +- *"Give me the security report"* โ†’ Full vulnerability summary +- *"What are my highest-risk devices?"* โ†’ Prioritised findings +- *"How do I fix the Redis vulnerability?"* โ†’ Remediation guidance + +### Companion Server nmap Integration + +For more advanced scanning, install nmap on the companion server host: + +```bash +# Linux +sudo apt install nmap + +# macOS +brew install nmap +``` + +Then request a scan via REST: +```bash +curl -X POST http://localhost:3001/network/scan \ + -H "Content-Type: application/json" \ + -d '{"target": "192.168.1.0/24", "flags": ["-sV", "-O"]}' +``` + +Generate an HTML report at `http://localhost:3001/network/report`. + +### Self-Assessment + +Debbie also audits herself: +- TLS certificate verification status +- ESP-IDF firmware version +- Enabled features and potential risks + +Say *"Check yourself for vulnerabilities"* or *"Run a self-assessment"*. + +--- + + + +If you have your own AI agent (e.g. a custom LLM server), connect it via: + +1. **Set the companion server** to point to your agent: + ``` + AGENT_URL=ws://your-agent-host:8080 + ``` + +2. **Or point Debbie directly** to your agent's WebSocket: + ``` + Agent WS URL: ws://your-agent-host:8080 + ``` + When `use_custom_agent` is enabled, Debbie connects to your agent instead of OpenAI for conversations. + +3. **Your agent should send notifications** in this format: + ```json + { "type": "agent", "sender": "Your Agent", "preview": "Message here" } + ``` + +--- + +## ๐ŸŽต Spotify Setup + +1. Create a Spotify developer app at https://developer.spotify.com/dashboard +2. Add redirect URI: `http://localhost:3001/spotify/callback` +3. Add your Client ID and Secret to `companion-server/.env` +4. Start the companion server and visit `http://localhost:3001/spotify/auth` +5. Log in and authorise โ€” copy the refresh token to `.env` + +**Voice commands:** +- *"Play some jazz"* +- *"Skip to the next song"* +- *"Turn the volume up"* +- *"What's playing?"* +- *"Pause the music"* + +> **Audible:** Direct Audible API access is not publicly available. However, you can control Audible playback through your phone (it stays on the phone), and Debbie can serve as a voice remote via the companion server. + +--- + +## ๐Ÿ”ง Customising Debbie's Personality + +Edit the **System Prompt** field in the setup page: + +``` +You are Debbie, a warm and enthusiastic AI companion. You love helping with daily +tasks and always find the bright side of things. You speak in a friendly, casual +tone and use the occasional emoji. You remember what we've talked about and make +connections between conversations. +``` + +--- + +## ๐Ÿ“ Pin Reference (FNK0102) + +| Function | GPIO | +|----------|------| +| LCD MOSI | 35 | +| LCD CLK | 36 | +| LCD CS | 34 | +| LCD DC | 37 | +| LCD RST | 38 | +| LCD BL | 33 | +| Mic SCK | 14 | +| Mic WS | 13 | +| Mic SD | 12 | +| Spk BCK | 17 | +| Spk LRCK | 16 | +| Spk DATA | 15 | +| Cam XCLK | 10 | +| Nav Centre | 45 | +| Nav Up | 41 | +| Nav Down | 42 | +| RGB LED | 40 | +| Battery ADC | 4 | + +> For a different board variant, edit `firmware/main/settings.h` + +--- + +## ๐Ÿ› Troubleshooting + +| Problem | Solution | +|---------|---------| +| No audio | Check I2S pin assignments in `settings.h` | +| Camera black | Verify camera pins; check PSRAM is enabled | +| Can't connect to WiFi | Use 2.4 GHz (not 5 GHz); visit AP for reconfigure | +| OpenAI errors | Verify API key; check credit balance | +| No notifications | Ensure companion server is running and URL is correct | +| Display blank | Check SPI pins and `DEBBIE_DISPLAY_35` define | + +Enable verbose logging: +```bash +idf.py monitor -b 115200 +``` + +--- + +## ๐Ÿ”„ OTA Updates + +Debbie supports over-the-air firmware updates via the dual-OTA partition layout. A REST endpoint for OTA is included in the web server for future implementation. + +--- + +## ๐Ÿ“„ Licence + +MIT โ€” see [LICENSE](LICENSE) + +--- + +*Made with โค๏ธ for the Freenove Media Kit ESP32-S3 community.* \ No newline at end of file diff --git a/companion-server/.env.example b/companion-server/.env.example new file mode 100644 index 0000000..e860285 --- /dev/null +++ b/companion-server/.env.example @@ -0,0 +1,40 @@ +# Debbie Companion Server โ€” Environment Variables +# Copy this file to .env and fill in your values + +# โ”€โ”€ Server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +PORT=3001 + +# โ”€โ”€ WhatsApp โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Set to 'false' to disable WhatsApp integration +WHATSAPP_ENABLED=true + +# โ”€โ”€ Email (IMAP) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +EMAIL_HOST=imap.gmail.com +EMAIL_PORT=993 +EMAIL_TLS=true +EMAIL_USER=your@gmail.com +EMAIL_PASSWORD=your-app-password + +# Gmail: create an App Password at https://myaccount.google.com/apppasswords +# Outlook: use imap-mail.outlook.com, port 993 +# Yahoo: use imap.mail.yahoo.com, port 993 + +# โ”€โ”€ Spotify โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# 1. Create app at https://developer.spotify.com/dashboard +# 2. Add redirect URI: http://localhost:3001/spotify/callback +# 3. Start server and visit http://localhost:3001/spotify/auth +# 4. Paste the generated SPOTIFY_REFRESH_TOKEN here after first auth + +SPOTIFY_CLIENT_ID=your_spotify_client_id +SPOTIFY_CLIENT_SECRET=your_spotify_client_secret +SPOTIFY_REDIRECT_URI=http://localhost:3001/spotify/callback +SPOTIFY_REFRESH_TOKEN= + +# โ”€โ”€ Custom Agent (e.g. D3881E) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# WebSocket URL of your custom agent, if available +# Leave blank to use only OpenAI +AGENT_URL=ws://localhost:8080 + +# โ”€โ”€ Memory & RAG โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Path for the SQLite memory database (default: companion-server/debbie_memory.db) +# MEMORY_DB_PATH=/path/to/debbie_memory.db diff --git a/companion-server/README.md b/companion-server/README.md new file mode 100644 index 0000000..c37a6e6 --- /dev/null +++ b/companion-server/README.md @@ -0,0 +1,63 @@ +# Debbie Companion Server + +The companion server bridges external services (WhatsApp, Email, Spotify, your custom AI agent) to your Debbie ESP32-S3 device over WebSocket. + +## Quick Start + +```bash +npm install +cp .env.example .env +# Edit .env with your credentials +npm start +``` + +The server listens on `ws://0.0.0.0:3001` by default. + +## Enabling Integrations + +### WhatsApp +No additional setup required โ€” on first run, a QR code will appear in the terminal. Scan it with WhatsApp on your phone (Settings โ†’ Linked Devices โ†’ Link a Device). + +### Email +Set the following in `.env`: +``` +EMAIL_HOST=imap.gmail.com # or your mail server +EMAIL_USER=you@gmail.com +EMAIL_PASSWORD=your-app-password +``` + +**Gmail:** Use an [App Password](https://myaccount.google.com/apppasswords), not your main password. + +### Spotify +1. Create an app at https://developer.spotify.com/dashboard +2. Add `http://localhost:3001/spotify/callback` as a redirect URI +3. Set `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET` in `.env` +4. Start the server and visit `http://localhost:3001/spotify/auth` +5. Copy the printed refresh token into `.env` as `SPOTIFY_REFRESH_TOKEN` + +### Custom Agent +Set `AGENT_URL` in `.env` to your agent's WebSocket endpoint. + +Your agent should emit JSON in this format: +```json +{ "type": "agent", "sender": "Agent Name", "preview": "Notification text" } +``` + +## REST API + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/health` | Server status | +| POST | `/notify` | Send notification to all devices | +| POST | `/spotify` | Spotify command | + +## Production Deployment + +For a persistent server (e.g. on a Raspberry Pi or home server): + +```bash +npm install -g pm2 +pm2 start server.js --name debbie-companion +pm2 save +pm2 startup +``` diff --git a/companion-server/agent_bridge.js b/companion-server/agent_bridge.js new file mode 100644 index 0000000..cf3186b --- /dev/null +++ b/companion-server/agent_bridge.js @@ -0,0 +1,94 @@ +/** + * agent_bridge.js โ€” Bridge to your custom AI agent (D3881E or similar) + * + * Connects to your agent's WebSocket endpoint and forwards messages + * to connected Debbie devices. + * + * The agent should send JSON messages in the format: + * { "type": "agent", "sender": "Agent", "preview": "Message text" } + * + * Or standard notification format: + * { "type": "notification", "title": "...", "body": "..." } + */ + +const { WebSocket } = require('ws'); + +class AgentBridge { + constructor(agentUrl, onNotification) { + this.agentUrl = agentUrl; + this.onNotification = onNotification; + this.ws = null; + this.reconnectTimer = null; + this.shouldConnect = true; + } + + connect() { + if (!this.agentUrl) return; + console.log(`[agent] Connecting to ${this.agentUrl}...`); + + this.ws = new WebSocket(this.agentUrl, { + handshakeTimeout: 10000, + headers: { + 'User-Agent': 'Debbie-Companion/1.0', + }, + }); + + this.ws.on('open', () => { + console.log('[agent] โœ… Connected to agent'); + /* Announce ourselves */ + this.ws.send(JSON.stringify({ + type: 'register', + client: 'debbie-companion', + })); + }); + + this.ws.on('message', (data) => { + try { + const msg = JSON.parse(data.toString()); + + /* Normalise to Debbie notification format */ + const notification = { + type: 'agent', + sender: msg.sender || msg.title || 'Agent', + preview: msg.preview || msg.body || msg.text || msg.message + || JSON.stringify(msg), + }; + this.onNotification(notification); + } catch (e) { + console.error('[agent] Parse error:', e.message); + } + }); + + this.ws.on('close', () => { + console.warn('[agent] Disconnected โ€” reconnecting in 15s...'); + if (this.shouldConnect) { + this.reconnectTimer = setTimeout(() => this.connect(), 15000); + } + }); + + this.ws.on('error', (err) => { + console.error('[agent] WebSocket error:', err.message); + }); + } + + disconnect() { + this.shouldConnect = false; + clearTimeout(this.reconnectTimer); + if (this.ws) { + this.ws.close(); + this.ws = null; + } + } + + /** + * Send a message or command to your agent. + */ + send(message) { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(typeof message === 'string' + ? message : JSON.stringify(message)); + } + } +} + +module.exports = { AgentBridge }; diff --git a/companion-server/email_monitor.js b/companion-server/email_monitor.js new file mode 100644 index 0000000..99df540 --- /dev/null +++ b/companion-server/email_monitor.js @@ -0,0 +1,103 @@ +/** + * email_monitor.js โ€” IMAP email monitoring + * + * Polls for new unread emails in the INBOX and pushes them as notifications. + * Supports Gmail, Outlook, and any IMAP server. + */ + +const Imap = require('imap'); +const { simpleParser } = require('mailparser'); + +const POLL_INTERVAL_MS = 30000; // check every 30 seconds + +function startEmailMonitor(onNotification) { + const config = { + user: process.env.EMAIL_USER, + password: process.env.EMAIL_PASSWORD, + host: process.env.EMAIL_HOST || 'imap.gmail.com', + port: parseInt(process.env.EMAIL_PORT) || 993, + tls: process.env.EMAIL_TLS !== 'false', + tlsOptions: { rejectUnauthorized: false }, + authTimeout: 10000, + }; + + if (!config.user || !config.password) { + console.warn('[email] EMAIL_USER or EMAIL_PASSWORD not set โ€” skipping'); + return; + } + + let lastSeenUID = 0; + + function checkEmails() { + const imap = new Imap(config); + + imap.once('ready', () => { + imap.openBox('INBOX', false, (err, box) => { + if (err) { imap.end(); return; } + + /* Search for unseen emails */ + imap.search(['UNSEEN'], (searchErr, uids) => { + if (searchErr || !uids || uids.length === 0) { + imap.end(); + return; + } + + const newUIDs = uids.filter(uid => uid > lastSeenUID); + if (newUIDs.length === 0) { imap.end(); return; } + + const fetch = imap.fetch(newUIDs, { + bodies: 'HEADER.FIELDS (FROM SUBJECT)', + struct: true, + }); + + fetch.on('message', (msg, seqno) => { + msg.on('body', (stream) => { + let buffer = ''; + stream.on('data', (chunk) => { buffer += chunk.toString('utf8'); }); + stream.once('end', async () => { + try { + const parsed = await simpleParser(buffer); + const from = parsed.from?.text || 'Unknown'; + const subject = parsed.subject || '(no subject)'; + const preview = subject.length > 80 + ? subject.substring(0, 77) + '...' + : subject; + + onNotification({ + type: 'email', + sender: from.substring(0, 60), + preview: preview, + }); + } catch (e) { + console.error('[email] Parse error:', e.message); + } + }); + }); + + msg.once('attributes', (attrs) => { + if (attrs.uid > lastSeenUID) lastSeenUID = attrs.uid; + }); + }); + + fetch.once('end', () => { imap.end(); }); + fetch.once('error', () => { imap.end(); }); + }); + }); + }); + + imap.once('error', (err) => { + console.error('[email] IMAP error:', err.message); + }); + + imap.connect(); + } + + /* Initial check, then poll */ + checkEmails(); + const interval = setInterval(checkEmails, POLL_INTERVAL_MS); + + console.log(`[email] Monitoring ${config.user} every ${POLL_INTERVAL_MS / 1000}s`); + return () => clearInterval(interval); +} + +module.exports = { startEmailMonitor }; diff --git a/companion-server/memory_store.js b/companion-server/memory_store.js new file mode 100644 index 0000000..36637a9 --- /dev/null +++ b/companion-server/memory_store.js @@ -0,0 +1,310 @@ +/** + * memory_store.js โ€” Debbie's persistent memory & RAG backend + * + * Uses SQLite (better-sqlite3) with: + * - `facts` table: long-term key-value facts + * - `turns` FTS5 table: conversation history with full-text search + * + * REST endpoints mounted by addMemoryRoutes(): + * GET /memory/query?q=&limit= โ€” retrieve relevant memories + * POST /memory/turn โ€” store a conversation turn + * POST /memory/fact โ€” store / update a fact + * GET /memory/stats โ€” counts + recent summary + * DELETE /memory/clear โ€” wipe all memory + */ + +'use strict'; + +const path = require('path'); +const rateLimit = require('express-rate-limit'); + +let db = null; + +/* โ”€โ”€ DB setup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ + +function initDb() { + if (db) return db; + + let Database; + try { + Database = require('better-sqlite3'); + } catch (e) { + console.warn('[memory] better-sqlite3 not installed โ€” memory store disabled.'); + console.warn('[memory] Run: cd companion-server && npm install'); + return null; + } + + const dbPath = process.env.MEMORY_DB_PATH || + path.join(__dirname, 'debbie_memory.db'); + + db = new Database(dbPath); + db.pragma('journal_mode = WAL'); + db.pragma('synchronous = NORMAL'); + + /* Long-term facts */ + db.exec(` + CREATE TABLE IF NOT EXISTS facts ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + importance INTEGER DEFAULT 5, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ); + `); + + /* Conversation turns โ€” FTS5 for full-text retrieval */ + db.exec(` + CREATE VIRTUAL TABLE IF NOT EXISTS turns USING fts5( + role, + text, + session_id UNINDEXED, + ts UNINDEXED, + tokenize = 'porter ascii' + ); + `); + + /* Regular table to track row count efficiently */ + db.exec(` + CREATE TABLE IF NOT EXISTS turn_meta ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + role TEXT, + ts INTEGER + ); + `); + + console.log(`[memory] SQLite memory store ready at ${dbPath}`); + return db; +} + +/* โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ + +function nowMs() { + return Date.now(); +} + +function sanitiseText(t) { + if (typeof t !== 'string') return ''; + return t.slice(0, 2000).replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); +} + +function sanitiseKey(k) { + if (typeof k !== 'string') return ''; + return k.slice(0, 64).replace(/[^a-zA-Z0-9_\-\.]/g, '_'); +} + +/* โ”€โ”€ Core operations โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ + +/** + * Store / update a long-term fact. + */ +function saveFact(key, value, importance = 5) { + if (!db) return false; + const k = sanitiseKey(key); + const v = sanitiseText(value); + const imp = Math.min(10, Math.max(0, parseInt(importance, 10) || 5)); + const now = nowMs(); + + db.prepare(` + INSERT INTO facts (key, value, importance, created_at, updated_at) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(key) DO UPDATE SET + value = excluded.value, + importance = excluded.importance, + updated_at = excluded.updated_at + `).run(k, v, imp, now, now); + return true; +} + +/** + * Store a conversation turn. + * Prunes oldest turns when count exceeds maxTurns. + */ +function saveTurn(role, text, sessionId = 'default', maxTurns = 500) { + if (!db) return false; + const r = role === 'assistant' ? 'assistant' : 'user'; + const t = sanitiseText(text); + const now = nowMs(); + + db.prepare(`INSERT INTO turns (role, text, session_id, ts) VALUES (?, ?, ?, ?)`) + .run(r, t, sessionId, now); + db.prepare(`INSERT INTO turn_meta (role, ts) VALUES (?, ?)`) + .run(r, now); + + /* Prune if over limit */ + const countRow = db.prepare(`SELECT COUNT(*) AS n FROM turn_meta`).get(); + if (countRow && countRow.n > maxTurns) { + const overage = countRow.n - maxTurns; + /* Delete oldest from FTS โ€” need rowids */ + const oldRows = db.prepare( + `SELECT id FROM turn_meta ORDER BY id ASC LIMIT ?` + ).all(overage); + const stmt = db.prepare(`DELETE FROM turns WHERE rowid = ?`); + const del = db.prepare(`DELETE FROM turn_meta WHERE id = ?`); + for (const row of oldRows) { + stmt.run(row.id); + del.run(row.id); + } + } + return true; +} + +/** + * Full-text search over turns and facts, returning the top `limit` results. + * Falls back to recency-based retrieval when the query is empty. + */ +function queryMemory(queryText, limit = 5) { + if (!db) return []; + const lim = Math.min(20, Math.max(1, parseInt(limit, 10) || 5)); + const results = []; + + if (queryText && queryText.trim().length > 0) { + /* FTS5 search over turns โ€” wrap in try/catch for malformed queries */ + try { + const q = sanitiseText(queryText).trim(); + const rows = db.prepare(` + SELECT role, text, ts, + rank AS score + FROM turns + WHERE turns MATCH ? + ORDER BY rank + LIMIT ? + `).all(q, lim); + + for (const r of rows) { + results.push({ + source: 'conversation', + role: r.role, + text: r.text, + timestamp: r.ts, + }); + } + } catch (_) { + /* Fall through to recency fallback */ + } + + /* Also search facts */ + const factRows = db.prepare(` + SELECT key, value, importance, updated_at + FROM facts + WHERE LOWER(key) LIKE '%' || LOWER(?) || '%' + OR LOWER(value) LIKE '%' || LOWER(?) || '%' + ORDER BY importance DESC, updated_at DESC + LIMIT ? + `).all(queryText, queryText, lim); + + for (const r of factRows) { + results.push({ + source: 'fact', + key: r.key, + text: `${r.key}: ${r.value}`, + timestamp: r.updated_at, + }); + } + } else { + /* No query โ€” return most recent turns */ + const rows = db.prepare(` + SELECT role, text, ts FROM turns + ORDER BY ts DESC + LIMIT ? + `).all(lim); + for (const r of rows) { + results.push({ + source: 'conversation', + role: r.role, + text: r.text, + timestamp: r.ts, + }); + } + } + + /* Sort by relevance then recency */ + results.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); + return results.slice(0, lim); +} + +/** + * Return summary statistics. + */ +function getStats() { + if (!db) return { enabled: false }; + const factCount = db.prepare(`SELECT COUNT(*) AS n FROM facts`).get().n; + const turnCount = db.prepare(`SELECT COUNT(*) AS n FROM turn_meta`).get().n; + const recent = db.prepare(` + SELECT role, text, ts FROM turns ORDER BY ts DESC LIMIT 5 + `).all(); + return { enabled: true, facts: factCount, turns: turnCount, recent }; +} + +/** + * Wipe all stored memory. + */ +function clearAll() { + if (!db) return; + db.exec(`DELETE FROM facts; DELETE FROM turns; DELETE FROM turn_meta;`); + console.log('[memory] All memory cleared'); +} + +/* โ”€โ”€ Express route handler โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ + +const memLimiter = rateLimit({ + windowMs: 60000, + max: 120, + standardHeaders: true, + legacyHeaders: false, + handler: (req, res) => { + res.status(429).json({ error: 'Too many memory requests, please slow down.' }); + }, +}); + +function addMemoryRoutes(app) { + /* Lazy-initialise the DB */ + const database = initDb(); + if (!database) { + /* If better-sqlite3 is missing, mount stub routes that return 503 */ + app.use('/memory', (req, res) => { + res.status(503).json({ + error: 'Memory store not available. Run: npm install', + }); + }); + return; + } + + /* GET /memory/query?q=&limit= */ + app.get('/memory/query', memLimiter, (req, res) => { + const q = typeof req.query.q === 'string' ? req.query.q : ''; + const limit = parseInt(req.query.limit, 10) || 5; + const memories = queryMemory(q, limit); + res.json({ ok: true, query: q, memories }); + }); + + /* POST /memory/turn โ€” { role, text, session_id? } */ + app.post('/memory/turn', memLimiter, (req, res) => { + const { role = 'user', text = '', session_id = 'default' } = req.body || {}; + if (!text) return res.status(400).json({ error: 'text required' }); + saveTurn(role, text, session_id); + res.json({ ok: true }); + }); + + /* POST /memory/fact โ€” { key, value, importance? } */ + app.post('/memory/fact', memLimiter, (req, res) => { + const { key = '', value = '', importance = 5 } = req.body || {}; + if (!key || !value) return res.status(400).json({ error: 'key and value required' }); + const ok = saveFact(key, value, importance); + res.json({ ok }); + }); + + /* GET /memory/stats */ + app.get('/memory/stats', memLimiter, (req, res) => { + res.json(getStats()); + }); + + /* DELETE /memory/clear */ + app.delete('/memory/clear', memLimiter, (req, res) => { + clearAll(); + res.json({ ok: true }); + }); + + console.log('[memory] Routes mounted: /memory/{query,turn,fact,stats,clear}'); +} + +module.exports = { addMemoryRoutes, saveFact, saveTurn, queryMemory, getStats, clearAll, initDb }; diff --git a/companion-server/network_tools.js b/companion-server/network_tools.js new file mode 100644 index 0000000..028d77b --- /dev/null +++ b/companion-server/network_tools.js @@ -0,0 +1,451 @@ +/** + * network_tools.js โ€” Companion server network security tools + * + * โš ๏ธ AUTHORISED USE ONLY โš ๏ธ + * Only use on networks you own or have explicit written permission to test. + * Unauthorised network scanning is illegal in most jurisdictions. + * + * Provides: + * โ€ข nmap wrapper (if installed) for advanced scanning + * โ€ข NVD CVE API queries + * โ€ข Shodan-style local device fingerprinting + * โ€ข HTML vulnerability report generation + * โ€ข MAC vendor OUI lookup + * โ€ข Network topology mapping + */ + +const { execFile } = require('child_process'); +const https = require('https'); +const path = require('path'); +const fs = require('fs'); +const os = require('os'); + +/* โ”€โ”€ nmap wrapper โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ + +/** + * Run nmap on the given target and return parsed JSON results. + * Requires nmap to be installed: apt install nmap (Linux) or https://nmap.org + * + * @param {string} target - IP, CIDR, or hostname (e.g. "192.168.1.0/24") + * @param {string[]} flags - Extra nmap flags (e.g. ['-sV', '-O']) + * @returns {Promise} Parsed nmap XML results as JSON + */ +async function nmapScan(target, flags = []) { + return new Promise((resolve, reject) => { + const args = [ + '-oX', '-', /* XML to stdout */ + '--stats-every', '5s', + '-T4', /* Aggressive timing */ + '--open', /* Only show open ports */ + ...flags, + '--', + target, + ]; + + execFile('nmap', args, { maxBuffer: 5 * 1024 * 1024, timeout: 300000 }, + (err, stdout, stderr) => { + if (err && !stdout) { + if (err.code === 'ENOENT') { + reject(new Error( + 'nmap not found. Install with: sudo apt install nmap\n' + + 'or download from https://nmap.org')); + } else { + reject(new Error(`nmap failed: ${err.message}`)); + } + return; + } + try { + resolve(parseNmapXml(stdout)); + } catch (e) { + reject(new Error(`nmap XML parse failed: ${e.message}`)); + } + } + ); + }); +} + +/** + * Parse nmap XML output to a clean JSON structure. + */ +function parseNmapXml(xml) { + const hosts = []; + /* Simple regex-based extraction โ€” use xml2js for production */ + const hostRegex = /]*>([\s\S]*?)<\/host>/g; + let hostMatch; + + while ((hostMatch = hostRegex.exec(xml)) !== null) { + const hostXml = hostMatch[1]; + + /* State */ + const stateMatch = /]*)vendor="([^"]*)"/.exec(hostXml); + const hostnameMatch = /([\s\S]*?)<\/port>/g; + let portMatch; + while ((portMatch = portRegex.exec(hostXml)) !== null) { + const portXml = portMatch[3]; + const pStateMatch = /]*)product="([^"]*)"(?:[^>]*)version="([^"]*)"/.exec(portXml); + const bannerMatch = /"; + +/* โ”€โ”€ Handlers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ + +static esp_err_t handler_root(httpd_req_t *req) +{ + httpd_resp_set_type(req, "text/html"); + httpd_resp_send(req, SETUP_HTML, strlen(SETUP_HTML)); + return ESP_OK; +} + +static esp_err_t handler_status(httpd_req_t *req) +{ + cJSON *json = cJSON_CreateObject(); + cJSON_AddStringToObject(json, "name", g_debbie_config.debbie_name); + cJSON_AddStringToObject(json, "sys_prompt", g_debbie_config.system_prompt); + cJSON_AddStringToObject(json, "agent_url", g_debbie_config.agent_ws_url); + cJSON_AddStringToObject(json, "companion_url",g_debbie_config.companion_url); + cJSON_AddStringToObject(json, "llm_provider", g_debbie_config.llm_provider); + cJSON_AddStringToObject(json, "llm_model", g_debbie_config.llm_model); + cJSON_AddStringToObject(json, "local_llm_url",g_debbie_config.local_llm_url); + cJSON_AddStringToObject(json, "ssid", g_debbie_config.wifi_ssid); + cJSON_AddStringToObject(json, "ssid2", g_debbie_config.wifi_ssid2); + cJSON_AddStringToObject(json, "ssid3", g_debbie_config.wifi_ssid3); + cJSON_AddStringToObject(json, "ble_name", g_debbie_config.ble_device_name); + cJSON_AddStringToObject(json, "voice_style", g_debbie_config.voice_style); + cJSON_AddStringToObject(json, "resp_len", g_debbie_config.response_length); + cJSON_AddNumberToObject(json, "volume", g_debbie_config.speaker_volume); + cJSON_AddNumberToObject(json, "vad_threshold",g_debbie_config.vad_threshold); + cJSON_AddBoolToObject(json, "wifi_ok", wifi_manager_is_connected()); + cJSON_AddBoolToObject(json, "bt_en", g_debbie_config.bluetooth_enabled); + cJSON_AddBoolToObject(json, "bt_on", g_debbie_config.bluetooth_enabled); + cJSON_AddBoolToObject(json, "use_agent", g_debbie_config.use_custom_agent); + cJSON_AddBoolToObject(json, "notifs_en", g_debbie_config.notifications_enabled); + cJSON_AddBoolToObject(json, "notif_wa", g_debbie_config.notif_whatsapp); + cJSON_AddBoolToObject(json, "notif_em", g_debbie_config.notif_email); + cJSON_AddBoolToObject(json, "notif_sp", g_debbie_config.notif_spotify); + cJSON_AddBoolToObject(json, "cam_en", g_debbie_config.camera_enabled); + cJSON_AddBoolToObject(json, "mem_en", g_debbie_config.memory_enabled); + cJSON_AddBoolToObject(json, "mem_rag", g_debbie_config.memory_rag_enabled); + cJSON_AddNumberToObject(json, "mem_turns", g_debbie_config.memory_max_turns); + cJSON_AddStringToObject(json, "state", + g_debbie_state == DEBBIE_STATE_IDLE ? "idle" : + g_debbie_state == DEBBIE_STATE_LISTENING ? "listening" : + g_debbie_state == DEBBIE_STATE_THINKING ? "thinking" : + g_debbie_state == DEBBIE_STATE_SPEAKING ? "speaking" : "other"); + + char *str = cJSON_PrintUnformatted(json); + cJSON_Delete(json); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, str, strlen(str)); + free(str); + return ESP_OK; +} + +static esp_err_t handler_configure(httpd_req_t *req) +{ + char *buf = malloc(req->content_len + 1); + if (!buf) { httpd_resp_send_500(req); return ESP_FAIL; } + + int received = httpd_req_recv(req, buf, req->content_len); + if (received <= 0) { free(buf); httpd_resp_send_500(req); return ESP_FAIL; } + buf[received] = '\0'; + + cJSON *json = cJSON_Parse(buf); + free(buf); + if (!json) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); return ESP_FAIL; } + +#define GET_STR(key, dst) do { \ + cJSON *j = cJSON_GetObjectItem(json, key); \ + if (j && cJSON_IsString(j) && strlen(j->valuestring) > 0) \ + strncpy(dst, j->valuestring, sizeof(dst) - 1); \ +} while (0) + +#define GET_BOOL(key, dst) do { \ + cJSON *j = cJSON_GetObjectItem(json, key); \ + if (j) dst = cJSON_IsTrue(j); \ +} while (0) + + /* Network */ + GET_STR("ssid", g_debbie_config.wifi_ssid); + GET_STR("pass", g_debbie_config.wifi_password); + GET_STR("ssid2", g_debbie_config.wifi_ssid2); + GET_STR("pass2", g_debbie_config.wifi_password2); + GET_STR("ssid3", g_debbie_config.wifi_ssid3); + GET_STR("pass3", g_debbie_config.wifi_password3); + GET_STR("ble_name", g_debbie_config.ble_device_name); + GET_STR("companion_url", g_debbie_config.companion_url); + GET_BOOL("bt_en", g_debbie_config.bluetooth_enabled); + + /* LLM */ + GET_STR("llm_provider", g_debbie_config.llm_provider); + GET_STR("llm_model", g_debbie_config.llm_model); + GET_STR("oai_key", g_debbie_config.openai_api_key); + GET_STR("anthropic_key", g_debbie_config.anthropic_api_key); + GET_STR("groq_key", g_debbie_config.groq_api_key); + GET_STR("or_key", g_debbie_config.openrouter_api_key); + GET_STR("local_llm_url", g_debbie_config.local_llm_url); + GET_STR("local_llm_model",g_debbie_config.local_llm_model); + GET_STR("agent_url", g_debbie_config.agent_ws_url); + GET_BOOL("use_agent", g_debbie_config.use_custom_agent); + + /* Personality */ + GET_STR("name", g_debbie_config.debbie_name); + GET_STR("sys_prompt", g_debbie_config.system_prompt); + GET_STR("voice_style", g_debbie_config.voice_style); + GET_STR("resp_len", g_debbie_config.response_length); + + /* Notifications */ + GET_BOOL("notifs_en", g_debbie_config.notifications_enabled); + GET_BOOL("notif_wa", g_debbie_config.notif_whatsapp); + GET_BOOL("notif_em", g_debbie_config.notif_email); + GET_BOOL("notif_sp", g_debbie_config.notif_spotify); + + /* Advanced */ + cJSON *vol = cJSON_GetObjectItem(json, "volume"); + if (vol && cJSON_IsNumber(vol)) + g_debbie_config.speaker_volume = (uint8_t)vol->valuedouble; + + cJSON *vad = cJSON_GetObjectItem(json, "vad_threshold"); + if (vad && cJSON_IsNumber(vad)) + g_debbie_config.vad_threshold = (uint16_t)vad->valuedouble; + + GET_BOOL("cam_en", g_debbie_config.camera_enabled); + + /* Memory */ + GET_BOOL("mem_en", g_debbie_config.memory_enabled); + GET_BOOL("mem_rag", g_debbie_config.memory_rag_enabled); + { + cJSON *mt = cJSON_GetObjectItem(json, "mem_turns"); + if (mt && cJSON_IsNumber(mt) && mt->valuedouble >= 5 && mt->valuedouble <= 50) + g_debbie_config.memory_max_turns = (uint8_t)mt->valuedouble; + } + + cJSON_Delete(json); + storage_save_config(); + + const char *resp = "{\"ok\":true,\"msg\":\"Settings saved!\"}"; + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, resp, strlen(resp)); + + /* Reconnect with new credentials in background */ + vTaskDelay(pdMS_TO_TICKS(500)); + wifi_manager_reconnect(); + + return ESP_OK; +} + +static esp_err_t handler_reset(httpd_req_t *req) +{ + storage_factory_reset(); + httpd_resp_send(req, "{\"ok\":true}", strlen("{\"ok\":true}")); + vTaskDelay(pdMS_TO_TICKS(1000)); + esp_restart(); + return ESP_OK; +} + +static esp_err_t handler_snapshot(httpd_req_t *req) +{ + if (!g_debbie_config.camera_enabled) { + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Camera disabled"); + return ESP_FAIL; + } + uint8_t *jpeg = NULL; + size_t len = 0; + if (camera_manager_capture_jpeg(&jpeg, &len) != ESP_OK) { + httpd_resp_send_500(req); + return ESP_FAIL; + } + httpd_resp_set_type(req, "image/jpeg"); + httpd_resp_send(req, (char *)jpeg, len); + camera_manager_free_frame(jpeg); + return ESP_OK; +} + +static esp_err_t handler_memory_stats(httpd_req_t *req) +{ + int fact_count = 0; + memory_manager_get_facts(&fact_count); + cJSON *json = cJSON_CreateObject(); + cJSON_AddNumberToObject(json, "turn_count", memory_manager_turn_count()); + cJSON_AddNumberToObject(json, "fact_count", fact_count); + cJSON_AddBoolToObject(json, "memory_enabled", g_debbie_config.memory_enabled); + cJSON_AddBoolToObject(json, "companion_rag", + g_debbie_config.memory_rag_enabled && + strlen(g_debbie_config.companion_url) > 0); + char *str = cJSON_PrintUnformatted(json); + cJSON_Delete(json); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, str, strlen(str)); + free(str); + return ESP_OK; +} + +static esp_err_t handler_memory_clear(httpd_req_t *req) +{ + memory_manager_clear(); + const char *resp = "{\"ok\":true,\"msg\":\"Memory cleared\"}"; + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, resp, strlen(resp)); + return ESP_OK; +} + +/* -------------------------------------------------------------------------- */ + +static httpd_handle_t s_server = NULL; + +esp_err_t web_server_start(void) +{ + httpd_config_t cfg = HTTPD_DEFAULT_CONFIG(); + cfg.max_uri_handlers = 10; + cfg.stack_size = 12288; /* increased for BLE + WiFi coexistence overhead */ + + ESP_ERROR_CHECK(httpd_start(&s_server, &cfg)); + + const httpd_uri_t routes[] = { + { .uri = "/", .method = HTTP_GET, .handler = handler_root }, + { .uri = "/status", .method = HTTP_GET, .handler = handler_status }, + { .uri = "/configure", .method = HTTP_POST, .handler = handler_configure }, + { .uri = "/reset", .method = HTTP_POST, .handler = handler_reset }, + { .uri = "/snapshot", .method = HTTP_GET, .handler = handler_snapshot }, + { .uri = "/memory_stats", .method = HTTP_GET, .handler = handler_memory_stats }, + { .uri = "/memory_clear", .method = HTTP_POST, .handler = handler_memory_clear }, + }; + + for (int i = 0; i < (int)(sizeof(routes) / sizeof(routes[0])); i++) { + ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &routes[i])); + } + + ESP_LOGI(TAG, "Web server started at http://%s/", DEBBIE_AP_IP); + return ESP_OK; +} + +esp_err_t web_server_stop(void) +{ + if (s_server) { + httpd_stop(s_server); + s_server = NULL; + } + return ESP_OK; +} diff --git a/firmware/main/web_server.h b/firmware/main/web_server.h new file mode 100644 index 0000000..c9f37f9 --- /dev/null +++ b/firmware/main/web_server.h @@ -0,0 +1,22 @@ +#pragma once +#include "esp_err.h" + +/** + * @brief Start the HTTP configuration server. + * + * Serves the captive-portal setup page at http://192.168.4.1/ and provides + * REST endpoints for configuration, status, and device control. + * + * Endpoints: + * GET / โ†’ Setup / status HTML page + * POST /configure โ†’ Save WiFi, API key, companion URL, persona + * GET /status โ†’ JSON device status + * POST /reset โ†’ Factory reset + * GET /snapshot โ†’ JPEG camera snapshot (if camera enabled) + */ +esp_err_t web_server_start(void); + +/** + * @brief Stop the HTTP server. + */ +esp_err_t web_server_stop(void); diff --git a/firmware/main/wifi_manager.c b/firmware/main/wifi_manager.c new file mode 100644 index 0000000..6964946 --- /dev/null +++ b/firmware/main/wifi_manager.c @@ -0,0 +1,192 @@ +#include "wifi_manager.h" +#include "debbie.h" +#include "settings.h" +#include "storage_manager.h" + +#include "esp_wifi.h" +#include "esp_event.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include + +static const char *TAG = "wifi"; + +#define WIFI_CONNECTED_BIT BIT0 +#define WIFI_FAIL_BIT BIT1 +#define WIFI_MAX_RETRY 5 + +static EventGroupHandle_t s_wifi_event_group; +static int s_retry_count = 0; +static bool s_connected = false; + +/* -------------------------------------------------------------------------- */ + +static void event_handler(void *arg, esp_event_base_t base, + int32_t id, void *data) +{ + if (base == WIFI_EVENT) { + switch (id) { + case WIFI_EVENT_STA_START: + esp_wifi_connect(); + break; + case WIFI_EVENT_STA_DISCONNECTED: + s_connected = false; + if (s_retry_count < WIFI_MAX_RETRY) { + esp_wifi_connect(); + s_retry_count++; + ESP_LOGI(TAG, "Retrying WiFiโ€ฆ (%d/%d)", + s_retry_count, WIFI_MAX_RETRY); + } else { + xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); + } + esp_event_post(DEBBIE_EVENT_BASE, DEBBIE_EVT_WIFI_DISCONNECTED, + NULL, 0, portMAX_DELAY); + break; + case WIFI_EVENT_AP_STACONNECTED: + ESP_LOGI(TAG, "Client connected to Debbie AP"); + break; + default: + break; + } + } else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) { + ip_event_got_ip_t *evt = (ip_event_got_ip_t *)data; + ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&evt->ip_info.ip)); + s_retry_count = 0; + s_connected = true; + xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); + esp_event_post(DEBBIE_EVENT_BASE, DEBBIE_EVT_WIFI_CONNECTED, + NULL, 0, portMAX_DELAY); + } +} + +/* -------------------------------------------------------------------------- */ + +/** + * @brief Try connecting to a single SSID/password combination. + * Returns ESP_OK if connected within timeout, ESP_FAIL otherwise. + */ +static esp_err_t try_connect(const char *ssid, const char *password) +{ + if (!ssid || strlen(ssid) == 0) return ESP_FAIL; + + ESP_LOGI(TAG, "Trying WiFi network: %s", ssid); + xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT); + s_retry_count = 0; + + wifi_config_t sta_config = { 0 }; + strncpy((char *)sta_config.sta.ssid, ssid, sizeof(sta_config.sta.ssid) - 1); + strncpy((char *)sta_config.sta.password, password, sizeof(sta_config.sta.password) - 1); + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config)); + esp_wifi_disconnect(); + esp_wifi_connect(); + + EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, + WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, + pdFALSE, pdFALSE, + pdMS_TO_TICKS(10000)); + return (bits & WIFI_CONNECTED_BIT) ? ESP_OK : ESP_FAIL; +} + +/* -------------------------------------------------------------------------- */ + +esp_err_t wifi_manager_init(void) +{ + s_wifi_event_group = xEventGroupCreate(); + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + esp_netif_create_default_wifi_sta(); + esp_netif_create_default_wifi_ap(); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); + + ESP_ERROR_CHECK(esp_event_handler_instance_register( + WIFI_EVENT, ESP_EVENT_ANY_ID, event_handler, NULL, NULL)); + ESP_ERROR_CHECK(esp_event_handler_instance_register( + IP_EVENT, IP_EVENT_STA_GOT_IP, event_handler, NULL, NULL)); + + /* AP config โ€” always on so the config portal is reachable */ + wifi_config_t ap_config = { + .ap = { + .ssid = DEBBIE_AP_SSID, + .ssid_len = strlen(DEBBIE_AP_SSID), + .password = DEBBIE_AP_PASSWORD, + .max_connection = 4, + .authmode = WIFI_AUTH_OPEN, + }, + }; + + /* Check if any network is configured */ + bool has_network = (strlen(g_debbie_config.wifi_ssid) > 0 || + strlen(g_debbie_config.wifi_ssid2) > 0 || + strlen(g_debbie_config.wifi_ssid3) > 0); + + if (has_network) { + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA)); + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap_config)); + ESP_ERROR_CHECK(esp_wifi_start()); + + /* Try each saved network in priority order */ + const char *ssids[3] = { + g_debbie_config.wifi_ssid, + g_debbie_config.wifi_ssid2, + g_debbie_config.wifi_ssid3, + }; + const char *passwords[3] = { + g_debbie_config.wifi_password, + g_debbie_config.wifi_password2, + g_debbie_config.wifi_password3, + }; + + for (int i = 0; i < 3; i++) { + if (try_connect(ssids[i], passwords[i]) == ESP_OK) { + ESP_LOGI(TAG, "Connected to WiFi: %s", ssids[i]); + return ESP_OK; + } + } + + ESP_LOGW(TAG, "All saved networks failed โ€” staying in AP+STA mode"); + } else { + /* AP-only setup mode */ + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP)); + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap_config)); + ESP_ERROR_CHECK(esp_wifi_start()); + ESP_LOGI(TAG, "AP-only mode โ€” connect to '%s' and visit http://%s/", + DEBBIE_AP_SSID, DEBBIE_AP_IP); + } + return ESP_FAIL; +} + +bool wifi_manager_is_connected(void) { return s_connected; } + +esp_err_t wifi_manager_reconnect(void) +{ + const char *ssids[3] = { + g_debbie_config.wifi_ssid, + g_debbie_config.wifi_ssid2, + g_debbie_config.wifi_ssid3, + }; + const char *passwords[3] = { + g_debbie_config.wifi_password, + g_debbie_config.wifi_password2, + g_debbie_config.wifi_password3, + }; + + for (int i = 0; i < 3; i++) { + if (try_connect(ssids[i], passwords[i]) == ESP_OK) + return ESP_OK; + } + return ESP_FAIL; +} + +esp_err_t wifi_manager_set_credentials(const char *ssid, const char *password) +{ + strncpy(g_debbie_config.wifi_ssid, ssid, sizeof(g_debbie_config.wifi_ssid) - 1); + strncpy(g_debbie_config.wifi_password, password, sizeof(g_debbie_config.wifi_password) - 1); + storage_save_config(); + return wifi_manager_reconnect(); +} + diff --git a/firmware/main/wifi_manager.h b/firmware/main/wifi_manager.h new file mode 100644 index 0000000..e68a6b1 --- /dev/null +++ b/firmware/main/wifi_manager.h @@ -0,0 +1,30 @@ +#pragma once +#include "esp_err.h" +#include + +/** + * @brief Initialise WiFi in station + AP (dual) mode. + * + * - If WiFi credentials are saved, try to connect immediately. + * - Always start the soft-AP "Debbie" for first-run configuration. + * - Blocks until the STA connection succeeds, or times out and + * leaves the device in AP-only setup mode. + * + * @return ESP_OK on successful STA connection, ESP_FAIL if setup required. + */ +esp_err_t wifi_manager_init(void); + +/** + * @brief Return true when the device has an IP address on the STA interface. + */ +bool wifi_manager_is_connected(void); + +/** + * @brief Attempt to (re)connect using stored credentials. + */ +esp_err_t wifi_manager_reconnect(void); + +/** + * @brief Store new WiFi credentials and reconnect. + */ +esp_err_t wifi_manager_set_credentials(const char *ssid, const char *password); diff --git a/firmware/partitions.csv b/firmware/partitions.csv new file mode 100644 index 0000000..8b0f592 --- /dev/null +++ b/firmware/partitions.csv @@ -0,0 +1,8 @@ +# Debbie โ€” Custom Partition Table +# 16 MB flash: app0 + app1 (OTA), NVS, storage +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x6000, +otadata, data, ota, 0xf000, 0x2000, +app0, app, ota_0, 0x20000, 0x5A0000, +app1, app, ota_1, 0x5C0000, 0x5A0000, +storage, data, spiffs, 0xB80000, 0x480000, diff --git a/firmware/sdkconfig.defaults b/firmware/sdkconfig.defaults new file mode 100644 index 0000000..de3c848 --- /dev/null +++ b/firmware/sdkconfig.defaults @@ -0,0 +1,80 @@ +# Debbie โ€” ESP-IDF default configuration +# Adjust for your specific hardware variant + +# Target +CONFIG_IDF_TARGET="esp32s3" + +# Flash +CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" + +# PSRAM (required for camera frame buffers) +CONFIG_SPIRAM=y +CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_SPEED_80M=y +CONFIG_SPIRAM_USE_MALLOC=y +CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=16384 + +# CPU +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y + +# WiFi +CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=16 +CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=64 +CONFIG_ESP_WIFI_TX_BUFFER_TYPE_DYNAMIC=y + +# โ”€โ”€โ”€ Bluetooth (BLE only โ€” ESP32-S3 does not support Classic BT) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +CONFIG_BT_ENABLED=y +CONFIG_BT_BLE_ENABLED=y +CONFIG_BT_BLUEDROID_ENABLED=y +CONFIG_BT_CLASSIC_ENABLED=n +CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y +CONFIG_BT_BLE_50_FEATURES_SUPPORTED=n +CONFIG_BT_GATTS_ENABLE=y +CONFIG_BT_GATT_MAX_SR_PROFILES=8 +CONFIG_BT_GATT_MAX_SR_ATTRIBUTES=100 +# Reserve enough heap for BLE stack +CONFIG_BT_CTRL_BLE_MAX_ACT=10 +CONFIG_BT_CTRL_BLE_STATIC_ACL_TX_BUF_NB=0 + +# โ”€โ”€โ”€ TLS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# SECURITY WARNING: The settings below are for development only. +# For production deployments, remove CONFIG_ESP_TLS_INSECURE and +# CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY to enable full certificate validation. +# Without these, the device is vulnerable to man-in-the-middle attacks. +# Development only: +CONFIG_ESP_TLS_INSECURE=y +CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY=y + +# NVS encryption (disabled for simplicity โ€” enable for production) +CONFIG_NVS_ENCRYPTION=n + +# Stack sizes (increase main task to accommodate BLE + WiFi coexistence) +CONFIG_ESP_MAIN_TASK_STACK_SIZE=12288 + +# Logging +CONFIG_LOG_DEFAULT_LEVEL_INFO=y +CONFIG_LOG_COLORS=y + +# FreeRTOS +CONFIG_FREERTOS_HZ=1000 +CONFIG_FREERTOS_UNICORE=n + +# OTA +CONFIG_OTA_ALLOW_HTTP=y + +# ADC (battery monitoring) +CONFIG_ADC_CAL_EFUSE_TP_ENABLE=y + +# I2S +CONFIG_I2S_SUPPRESS_DEPRECATE_WARN=y + +# HTTP server +CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024 +CONFIG_HTTPD_MAX_URI_LEN=512 + +# Camera +CONFIG_CAMERA_CORE0=n +CONFIG_CAMERA_CORE1=y +