Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions extras/havpe-relay/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# ESPHome build artifacts
firmware/.esphome/

# Firmware secrets (generated by init.py)
firmware/secrets.yaml

# Audio recordings (debug mode)
audio_chunks/

# Python
__pycache__/
*.pyc
Comment on lines +1 to +12
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Missing .env from gitignore — secrets will be committed.

init.py generates a .env file containing AUTH_PASSWORD and other credentials. This file should be excluded from version control.

🐛 Proposed fix
 # Firmware secrets (generated by init.py)
 firmware/secrets.yaml
 
+# Environment file (generated by init.py, contains credentials)
+.env
+
 # Audio recordings (debug mode)
 audio_chunks/
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# ESPHome build artifacts
firmware/.esphome/
# Firmware secrets (generated by init.py)
firmware/secrets.yaml
# Audio recordings (debug mode)
audio_chunks/
# Python
__pycache__/
*.pyc
# ESPHome build artifacts
firmware/.esphome/
# Firmware secrets (generated by init.py)
firmware/secrets.yaml
# Environment file (generated by init.py, contains credentials)
.env
# Audio recordings (debug mode)
audio_chunks/
# Python
__pycache__/
*.pyc
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extras/havpe-relay/.gitignore` around lines 1 - 12, Add the generated .env to
.gitignore so secrets from init.py (e.g., AUTH_PASSWORD and other credentials)
are not committed; edit the repository .gitignore to include a line with ".env"
(and consider adding related patterns like ".env.local" or "*.env" if
appropriate) to ensure the generated environment file is excluded from version
control.

235 changes: 127 additions & 108 deletions extras/havpe-relay/README.md
Original file line number Diff line number Diff line change
@@ -1,164 +1,183 @@
# HAVPE Relay (Home Assistant Voice Preview Edition Relay)

TCP-to-WebSocket relay for ESPHome Voice-PE that connects to the Omi advanced backend.
TCP-to-WebSocket relay that bridges ESP32 Voice-PE devices to the Chronicle backend.

## Features

- **TCP Server**: Listens on port 8989 for ESP32 Voice-PE connections
- **Audio Format Conversion**: Converts 32-bit PCM to 16-bit PCM using easy-audio-interfaces
- **WebSocket Client**: Forwards converted audio to backend at `/ws?codec=pcm` endpoint
- **Graceful Handling**: Supports reconnections and proper cleanup
- **Configurable**: Command-line options for ports and endpoints
## Architecture

## Audio Processing
```
ESP32 Voice-PE ──TCP:8989──► HAVPE Relay ──WebSocket──► Chronicle Backend
(32-bit stereo) (16-bit mono) (/ws?codec=pcm)
```

- **Input Format**: 32-bit PCM, 16kHz, 2 channels (from ESP32 Voice-PE)
- **Output Format**: 16-bit PCM, 16kHz, 2 channels (to backend)
- **Conversion**: Uses easy-audio-interfaces for robust audio processing
The relay:
- Listens for raw TCP audio from an ESP32 running ESPHome
- Converts 32-bit stereo I2S data to 16-bit mono PCM
- Authenticates with the Chronicle backend (JWT)
- Streams audio over WebSocket using the Wyoming protocol

## Installation
## Quick Start

Make sure you're in the havpe-relay directory:
### 1. Configure

```bash
cd havpe-relay
cd extras/havpe-relay
./init.sh
```

Install dependencies (already configured in pyproject.toml):
The setup wizard configures:
- Backend URL and WebSocket URL
- Authentication credentials (reads defaults from backend `.env`)
- Device name and TCP port
- (Optional) ESP32 firmware WiFi and relay IP secrets

```bash
uv sync
```

## Usage
### 2. Flash the ESP32 Firmware

### Basic Usage
See [Firmware Flashing](#firmware-flashing) below.

Start the relay with default settings:
### 3. Start the Relay

```bash
uv run main.py
```
# With Docker
docker compose up --build -d

This will:
- Listen for TCP connections on port 8989
- Forward to WebSocket at `ws://127.0.0.1:8000/ws?codec=pcm`
# Or run directly
uv run python main.py
```

### Advanced Usage
## Firmware Flashing

```bash
# Custom TCP port
uv run main.py --tcp-port 9090
The `firmware/` directory contains the ESPHome configuration for the ESP32-S3 Voice-PE.

# Custom WebSocket URL
uv run main.py --ws-url "ws://192.168.1.100:8000/ws?codec=pcm"
### Configure Secrets

# Verbose logging
uv run main.py -v # INFO level
uv run main.py -vv # DEBUG level
If you didn't configure firmware during `./init.sh`, create the secrets file manually:

# Full configuration example
uv run main.py --tcp-port 8989 --ws-url "ws://localhost:8000/ws?codec=pcm" -v
```bash
cd firmware
cp secrets.template.yaml secrets.yaml
```

### Command Line Options
Edit `secrets.yaml` with your values:

| Option | Default | Description |
|--------|---------|-------------|
| `--tcp-port` | 8989 | TCP port to listen on for ESP32 connections |
| `--ws-url` | `ws://127.0.0.1:8000/ws?codec=pcm` | WebSocket URL to forward audio to |
| `-v` / `--verbose` | WARNING | Increase verbosity (-v: INFO, -vv: DEBUG) |
```yaml
wifi_ssid: "YourWiFiNetwork"
wifi_password: "YourWiFiPassword"
relay_ip_address: "192.168.0.108" # IP of the machine running this relay
```

## Architecture
### Flash

```
ESP32 Voice-PE → TCP:8989 → HAVPE Relay → WebSocket:/ws?codec=pcm → Omi Backend
(32-bit PCM) (16-bit PCM)
Connect the ESP32-S3 Voice-PE via USB, then:

```bash
./flash.sh
```

## Integration with Backend
This installs ESPHome via the `firmware` dependency group and runs `esphome run`. On first flash ESPHome will:
1. Download and compile the ESP-IDF framework (~5 min first time)
2. Build the firmware
3. Flash over USB (select the serial port when prompted)

The relay automatically includes the following WebSocket parameters when connecting to the backend:
Subsequent flashes are faster (incremental builds) and can be done over WiFi (OTA).

- `user_id=esp32_voice_pe` - Identifies the audio source
- `rate=16000` - Sample rate (16kHz)
- `width=2` - Sample width (16-bit = 2 bytes)
- `channels=2` - Stereo audio
- `src=voice_pe` - Source identifier
To view device logs:

Example WebSocket URL sent to backend:
```
ws://127.0.0.1:8000/ws?codec=pcm?user_id=esp32_voice_pe&rate=16000&width=2&channels=2&src=voice_pe
```bash
./flash.sh logs
```

## Development
### Hardware Wiring

### Project Structure
The ESPHome config (`voice-tcp.yaml`) expects an I2S microphone on these pins:

```
havpe-relay/
├── main.py # Main relay implementation
├── pyproject.toml # Project configuration
├── uv.lock # Dependency lock file
├── README.md # This file
├── .python-version # Python version (3.12)
└── .venv/ # Virtual environment
```
| Signal | GPIO |
|--------|------|
| BCLK | 13 |
| LRCLK | 14 |
| DIN | 15 |

### Dependencies
These match the default Voice-PE board pinout. If your board differs, edit the pin numbers in `voice-tcp.yaml`.

- `easy-audio-interfaces>=0.2.6` - Audio processing and format conversion
- `websockets>=15.0.1` - WebSocket client implementation
- Python 3.12+ required
### Verify Connection

### Audio Conversion Details
After flashing, the ESP32 will:
1. Connect to WiFi
2. Open a TCP socket to `relay_ip_address:8989`
3. Stream raw I2S audio data

The relay uses a two-step process for audio conversion:
Check the relay logs to confirm audio is flowing:

1. **Input Processing**: Wraps incoming TCP data in `AudioChunk` format
2. **Format Conversion**: Converts 32-bit float PCM to 16-bit integer PCM
- Clamps values to [-1, 1] range
- Scales to 16-bit integer range (-32767 to 32767)
- Maintains sample rate and channel count
```bash
# Docker
docker compose logs -f

## Troubleshooting
# Direct
uv run python main.py -v
```

### Common Issues
You should see `TCP client connected` followed by chunk processing messages.

1. **Connection Refused**: Ensure the backend is running on the specified WebSocket URL
2. **TCP Port in Use**: Another service might be using port 8989
3. **Audio Quality Issues**: Check that ESP32 is sending 32-bit PCM data
## Configuration

### Debug Mode
### Environment Variables (`.env`)

Run with debug logging to see detailed audio processing:
| Variable | Default | Description |
|----------|---------|-------------|
| `BACKEND_URL` | `http://host.docker.internal:8000` | Backend HTTP URL (for auth) |
| `BACKEND_WS_URL` | `ws://host.docker.internal:8000` | Backend WebSocket URL |
| `AUTH_USERNAME` | — | Email address for Chronicle login |
| `AUTH_PASSWORD` | — | Password for Chronicle login |
| `DEVICE_NAME` | `havpe` | Device identifier (becomes part of client ID) |
| `TCP_PORT` | `8989` | TCP port to listen on for ESP32 |

### Command Line Options

```bash
uv run main.py -vv
uv run python main.py --help
```

This will show:
- TCP connection details
- Audio chunk sizes and conversion rates
- WebSocket message sizes
- Error details

### Monitoring
| Option | Default | Description |
|--------|---------|-------------|
| `--port` | 8989 | TCP port for ESP32 connections |
| `--host` | `0.0.0.0` | Host address to bind to |
| `--backend-url` | from env | Backend API URL |
| `--backend-ws-url` | from env | Backend WebSocket URL |
| `--username` | from env | Auth username |
| `--password` | from env | Auth password |
| `--debug-audio` | off | Save raw audio to `audio_chunks/` |
| `-v` / `-vv` | WARNING | Increase log verbosity |

Watch the logs for:
- `TCP client connected` - ESP32 successfully connected
- `WebSocket connected` - Backend connection established
- `Relayed X bytes (32-bit) -> Y bytes (16-bit)` - Audio being processed
- Conversion ratio should be approximately 2:1 (32-bit to 16-bit)
## Project Structure

## Testing
```
havpe-relay/
├── main.py # Relay server
├── init.py # Setup wizard
├── init.sh # Setup wizard wrapper
├── flash.sh # Firmware flash wrapper
├── .env.template # Environment template
├── docker-compose.yml # Docker config
├── Dockerfile # Container build
├── firmware/
│ ├── voice-tcp.yaml # ESPHome config for ESP32-S3
│ ├── tcp_stream.h # lwIP socket header
│ ├── secrets.template.yaml # Secrets template
│ └── secrets.yaml # Your secrets (gitignored)
└── pyproject.toml # Python dependencies
```

You can test the relay using the provided test listener (if needed):
## Troubleshooting

1. Start the test WebSocket listener on port 8000
2. Start the relay: `uv run main.py -v`
3. Connect your ESP32 Voice-PE device to the relay on port 8989
### ESP32 won't connect to relay
- Verify `relay_ip_address` in `firmware/secrets.yaml` matches this machine's LAN IP
- Ensure the relay is running and port 8989 is not firewalled
- Check ESP32 serial logs: `esphome logs firmware/voice-tcp.yaml`

## License
### Authentication failures
- Verify credentials: try logging in at `BACKEND_URL/docs` with the same email/password
- Check the backend is reachable from the relay host

This project is part of the chronicle ecosystem.
### No audio in Chronicle
- Run with `-v` to confirm chunks are being sent
- Run with `--debug-audio` to save raw audio locally and verify it's not silence
- Check backend WebSocket logs for the connection
17 changes: 4 additions & 13 deletions extras/havpe-relay/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,14 @@ services:
dockerfile: Dockerfile
ports:
- "${TCP_PORT:-8989}:8989"
env_file: .env
environment:
# Connect to backend running on host (adjust as needed)
- WS_URL=${WS_URL:-ws://host.docker.internal:8000/ws?codec=pcm}
- TCP_PORT=${TCP_PORT:-8989}
Comment on lines 7 to 10
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Container-side port is hardcoded — changing TCP_PORT will break connectivity.

The port mapping "${TCP_PORT:-8989}:8989" binds the host side dynamically but the container side is always 8989. If a user sets TCP_PORT=9000, the application inside the container listens on 9000, but Docker still forwards traffic to container port 8989, causing a mismatch.

🐛 Proposed fix
     ports:
-      - "${TCP_PORT:-8989}:8989"
+      - "${TCP_PORT:-8989}:${TCP_PORT:-8989}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- "${TCP_PORT:-8989}:8989"
env_file: .env
environment:
# Connect to backend running on host (adjust as needed)
- WS_URL=${WS_URL:-ws://host.docker.internal:8000/ws?codec=pcm}
- TCP_PORT=${TCP_PORT:-8989}
- "${TCP_PORT:-8989}:${TCP_PORT:-8989}"
env_file: .env
environment:
- TCP_PORT=${TCP_PORT:-8989}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extras/havpe-relay/docker-compose.yml` around lines 7 - 10, The
host-to-container port mapping currently hardcodes the container side to 8989
causing a mismatch when TCP_PORT is overridden; update the port mapping in
docker-compose.yml to use the same variable for both sides (e.g., replace
"${TCP_PORT:-8989}:8989" with "${TCP_PORT:-8989}:${TCP_PORT:-8989}") so the
container port and the environment variable TCP_PORT remain consistent with the
existing environment entry (TCP_PORT=${TCP_PORT:-8989}).

# Authentication credentials for backend
- AUTH_USERNAME=${AUTH_USERNAME}
- AUTH_PASSWORD=${AUTH_PASSWORD}
# - VERBOSE=${VERBOSE:-1}
- DEBUG=${DEBUG:-0}
restart: unless-stopped
healthcheck:
test: ["CMD", "netstat", "-an", "|", "grep", "8989"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
command: ["uv", "run", "python3", "main.py"]
command: ["uv", "run", "python3", "main.py",
"--backend-url", "${BACKEND_URL:-http://host.docker.internal:8000}",
"--backend-ws-url", "${BACKEND_WS_URL:-ws://host.docker.internal:8000}"]
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
Expand Down
5 changes: 5 additions & 0 deletions extras/havpe-relay/firmware/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Gitignore settings for ESPHome
# This is an example and may include too much for your use-case.
# You can modify this file to suit your needs.
/.esphome/
/secrets.yaml
Loading