Hardware
Stikadoo AI Printer
What is Stikadoo?
Dataflow
- ESP32 gets WebSocket config from /challenge response (in ota.cc)
- ESP32 connects → handle_connect() in aws_lambda.py
- ESP32 sends hello → session created
- ESP32 sends listen start → audio buffer cleared
- ESP32 sends binary audio packets → handle_binary_message() buffers audio
- ESP32 sends listen stop → should trigger STT (currently placeholder)
- AWS Transcribe converts audio to text (not yet implemented)
- Text sent back to ESP32 as STT message (integration missing)
- LLM generates image from text (ready, but not called)
Display
QSPI
MIPI
Codec
Volume
Edit main/audio/audio_codec.h and change value:
int output_volume_ = 90;
Websocket Server
DynamoDB
#!/bin/bash
# Script to create DynamoDB table for WebSocket session persistence
# This uses the lowest cost settings (on-demand billing)
TABLE_NAME="stikadoo-websocket-sessions"
REGION="${AWS_REGION:-cn-northwest-1}" # Default to your region
echo "Creating DynamoDB table: $TABLE_NAME"
echo "Region: $REGION"
echo ""
# Check if table already exists
if aws dynamodb describe-table --table-name "$TABLE_NAME" --region "$REGION" 2>/dev/null; then
echo "Table $TABLE_NAME already exists!"
echo "To delete and recreate, run:"
echo " aws dynamodb delete-table --table-name $TABLE_NAME --region $REGION"
exit 1
fi
# Create table with on-demand billing (lowest cost)
aws dynamodb create-table \
--table-name "$TABLE_NAME" \
--region "$REGION" \
--attribute-definitions \
AttributeName=session_id,AttributeType=S \
--key-schema \
AttributeName=session_id,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--tags \
Key=Project,Value=Stikadoo \
Key=Environment,Value=Production \
Key=Purpose,Value=WebSocketSessionStorage
echo ""
echo "Waiting for table to be active..."
aws dynamodb wait table-exists --table-name "$TABLE_NAME" --region "$REGION"
echo ""
echo "Table created successfully!"
echo ""
echo "Enabling TTL (Time To Live) for automatic cleanup..."
aws dynamodb update-time-to-live \
--table-name "$TABLE_NAME" \
--region "$REGION" \
--time-to-live-specification \
Enabled=true,AttributeName=expires_at
echo ""
echo "DynamoDB table setup complete!"
echo ""
echo "Table name: $TABLE_NAME"
echo "Billing mode: PAY_PER_REQUEST (on-demand)"
echo "TTL: Enabled on 'expires_at' attribute"
echo ""
echo "Estimated cost: ~$0.10/month for typical usage"
echo ""
echo "To verify, run:"
echo " aws dynamodb describe-table --table-name $TABLE_NAME --region $REGION"
Minimal Policy
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DynamoDBSessionsTableAccess",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:Scan"
],
"Resource": "arn:aws-cn:dynamodb:*:*:table/stikadoo-websocket-sessions"
}
]
}
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DynamoDBSessionsTableAccess",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:Query",
"dynamodb:Scan"
],
"Resource": [
"arn:aws-cn:dynamodb:*:*:table/stikadoo-websocket-sessions",
"arn:aws-cn:dynamodb:*:*:table/stikadoo-websocket-sessions/index/*"
]
}
]
}
Lambda
Lambda Configurations
Change the Lambda function timeout to 10 seconds to ensure that the AWS Transcribe service can finish a transcription job. Adjust this configuration based on your needs.
aws lambda update-function-configuration --function-name stikadoo_websocket_handler --timeout 10 --region cn-northwest-1
Layer
python3 build_lambda_layer.py --python-version 3.12
aws lambda publish-layer-version \
--layer-name stikadoo-websocket-server-dependencies \
--zip-file fileb://stikadoo-websocket-server-dependencies-python3.12.zip \
--compatible-runtimes python3.12
Protocol Summary
Device → Server Messages
| Type | Description | Fields |
|---|---|---|
hello | Establish session | version, transport, features, audio_params |
listen | Listening state | session_id, state (start/stop/detect), mode |
abort | Abort operation | session_id, reason |
goodbye | Close session | session_id |
mcp | MCP protocol | session_id, payload |
| Binary | OPUS audio | Protocol version 1/2/3 format |
Server → Device Messages
| Type | Description | Fields |
|---|---|---|
hello | Session response | session_id, transport, audio_params |
tts | Text-to-speech | session_id, state, text |
stt | Speech-to-text | session_id, text |
llm | LLM emotion | session_id, emotion |
system | System command | session_id, command |
alert | Alert message | session_id, status, message, emotion |
Integration Example
# In server.py, add AI integration:
from aws_transcribe import start_transcription, stop_transcription
from llm import generate_image_from_text
# When audio is received:
async def process_audio_packet(session_id: str, audio_data: bytes):
# ... existing code ...
# When listen state is "stop", process audio
if session.get("listen_state") == "stop":
# Stop transcription and get final transcript
await stop_transcription(session_id)
# Process transcript through LLM to generate image
client_id = session.get("client_id")
if client_id:
image_data = await generate_image_from_text(transcript_text, client_id, session_id)
# Image is automatically sent back via callback
# Send TTS
if result.get("llm_response"):
tts_msg = send_tts_message(session_id, result["llm_response"], "sentence_start")
await websocket.send(json.dumps(tts_msg))
Troubleshooting
Connection Issues
- Check if WebSocket library is installed:
pip list | grep websockets - Verify server is running:
curl http://localhost:8765 - Check firewall/network rules
Message Parsing Errors
- Verify JSON format matches protocol specification
- Check protocol version compatibility
- Validate session_id is present in messages
Session Issues
- Check session timeout settings
- Verify session cleanup on disconnect
- Monitor session count in logs
Next Steps
- Read deployment guide for detailed deployment instructions
- Integrate with your AI service provider
- Test with actual ESP32 device
- Deploy to production environment
AWS API Gateway Deployment
This guide explains how to deploy the WebSocket server on AWS API Gateway with Lambda backend.
Route Selection Expression
When configuring your WebSocket API in AWS API Gateway, use the following Route Selection Expression:
$request.body.type
This expression extracts the type field from the JSON message body to route messages to the appropriate Lambda function.
Routes Configuration
Configure the following routes in your API Gateway WebSocket API:
| Route Key | Description | Lambda Function |
|---|---|---|
$connect | WebSocket connection established | Same Lambda |
$disconnect | WebSocket connection closed | Same Lambda |
$default | Binary messages (audio packets) | Same Lambda |
hello | Hello message (session establishment) | Same Lambda |
listen | Listen state changes | Same Lambda |
abort | Abort operation | Same Lambda |
goodbye | Close session | Same Lambda |
mcp | MCP protocol messages | Same Lambda |
Note: All routes can point to the same Lambda function. The handler will route based on the routeKey in the event.
Deployment Steps
1. Create Lambda Function
- Go to AWS Lambda Console
- Create a new function
- Choose Python 3.9 or later runtime
- Upload deployment package (see below)
2. Prepare Deployment Package
cd scripts/websocket_server
# Install dependencies
pip3 install boto3 -t .
pip3 install websockets -t .
# Create deployment package
zip -r ../websocket_lambda.zip .
Required files:
aws_lambda.py(main Lambda handler)aws_transcribe.py(Amazon Transcribe STT service)server.py(shared functions)llm.py(Qwen3 VL image generation service)boto3,aiohttpand dependencies
3. Configure Lambda Function
Handler: server.handler
Environment Variables:
Important: Set API_GATEWAY_ENDPOINT after creating the API Gateway (see step 4).
Memory: 512 MB (minimum recommended)
Timeout: 30 seconds (or longer for audio processing)
4. Create API Gateway WebSocket API
- Go to API Gateway Console
- Click Create API
- Select WebSocket API
- Configure:
- API name:
stickerbox-websocket(or your choice) - Route selection expression:
$request.body.type - Description: (optional)
- API name:
5. Configure Routes
For each route, create an integration:
- Click on the route (e.g.,
$connect) - Click Attach integration
- Select Lambda Function
- Choose your Lambda function
- Click Attach
Repeat for all routes:
$connect$disconnect$defaulthellolistenabortgoodbyemcp
6. Set API Gateway Endpoint
After deploying the API, get the WebSocket endpoint URL:
- Go to Stages in your API
- Copy the WebSocket URL (e.g.,
wss://abc123.execute-api.us-east-1.amazonaws.com/production) - Update Lambda environment variable
API_GATEWAY_ENDPOINT:
(Note: Usehttps://abc123.execute-api.us-east-1.amazonaws.com/productionhttps://notwss://)
7. Deploy API
- Go to Stages in your API
- Click Deploy API
- Select or create a stage (e.g.,
production) - Click Deploy
8. Configure Permissions
Ensure Lambda has permission to call API Gateway Management API:
- Go to Lambda function
- Go to Configuration → Permissions
- Click on the execution role
Editbutton. - Add inline policy:
** AWS Global **
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"execute-api:ManageConnections"
],
"Resource": "arn:aws:execute-api:*:*:*/*/@connections/*"
}
]
}
** AWS China **
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"execute-api:ManageConnections"
],
"Resource": "arn:aws-cn:execute-api:*:*:*/*/@connections/*"
}
]
}
9. Configure API Key (Optional but Recommended)
API Keys provide a simple way to control access to your WebSocket API. This is useful for preventing unauthorized access and tracking usage.
Step 1: Create API Key
- Go to API Gateway console
- In the left sidebar, click API Keys
- Click Create API Key
- Configure:
- Name:
stickerbox-websocket-key(or your choice) - Description: (optional)
- API key source: Select Auto-generate (recommended)
- Enabled: Check Enabled
- Name:
- Click Save
- Important: Copy the API Key value immediately (you won't be able to see it again)
- The API Key will look like:
abc123XYZ789...
- The API Key will look like:
Step 2: Create Usage Plan (Optional but Recommended)
Usage plans allow you to set throttling and quota limits:
- In API Gateway console, click Usage Plans in the left sidebar
- Click Create
- Configure:
- Name:
stickerbox-usage-plan(or your choice) - Description: (optional)
- Throttle: (optional) Set rate and burst limits
- Rate: Requests per second (e.g.,
100) - Burst: Maximum concurrent requests (e.g.,
200)
- Rate: Requests per second (e.g.,
- Quota: (optional) Set daily/monthly limits
- Quota: Total requests (e.g.,
10000) - Period:
DayorMonth
- Quota: Total requests (e.g.,
- Name:
- Click Next
- Add API stages:
- Select your WebSocket API
- Select your stage (e.g.,
production) - Click Add
- Click Next
- Add API keys:
- Select the API key created in Step 1
- Click Add
- Click Done
Step 3: Require API Key on Stage
- Go to your WebSocket API in API Gateway
- Click Stages in the left sidebar
- Select your stage (e.g.,
production) - Click on the $connect route (or any route you want to protect)
- Click Route Request tab
- Check API Key Required
- Click Save
- Other routes can not have API Key required for websocket protocol.
Note: You can require API Key on the $connect is usually sufficient.
Step 4: Redeploy API
After configuring API Key requirement:
- Go to Stages in your API
- Click Deploy API
- Select your stage
- Click Deploy
Using API Key in Requests
Header Name
API Gateway expects the API Key in the x-api-key header:
x-api-key: your-api-key-value
Python Example
import asyncio
import websockets
import json
async def test():
uri = "wss://websocket.aicenture.cn"
# Use additional_headers (list of tuples) for compatibility with older websockets versions
headers = [
("x-api-key", "11ipM9ePHT1V2GlwC..."),
("Device-Id", "b4:3a:45:9b:94:74"),
("Client-Id", "08450614-9d08-472e-811d-1988f761d376"),
("Protocol-Version", "3")
]
async with websockets.connect(uri, additional_headers=headers) as ws:
# Send hello
hello = {
"type": "hello",
"version": 3,
"transport": "websocket",
"features": {"mcp": True},
"audio_params": {
"format": "opus",
"sample_rate": 16000,
"channels": 1,
"frame_duration": 60
}
}
await ws.send(json.dumps(hello))
# Receive response
response = await ws.recv()
print(f"Received: {response}")
asyncio.run(test())
Postman Example
- Open Postman
- Create new WebSocket request
- Enter WebSocket URL
- Go to Headers tab
- Add header:
- Key:
x-api-key - Value:
your-api-key-value
- Key:
- Add other headers as needed
- Connect and test
cURL / websocat Example
# Using websocat with API Key
echo '{"type":"hello","version":3,"transport":"websocket","features":{"mcp":true},"audio_params":{"format":"opus","sample_rate":16000,"channels":1,"frame_duration":60}}' | \
websocat -H "x-api-key: your-api-key-value" \
-H "Device-Id: AA:BB:CC:DD:EE:FF" \
-H "Client-Id: test-client" \
-H "Protocol-Version: 3" \
wss://your-api-id.execute-api.region.amazonaws.com/stage
API Key Troubleshooting
403 Forbidden Error
Problem: Connection returns 403 Forbidden
Solutions:
- Verify API Key is correct (check for typos)
- Ensure API Key is enabled in API Gateway
- Check that API Key is associated with the usage plan
- Verify usage plan is linked to your API stage
- Ensure route has "API Key Required" enabled
- Check that API was redeployed after enabling API Key requirement
Missing API Key Error
Problem: Error message about missing API Key
Solutions:
- Verify
x-api-keyheader is included in request - Check header name is exactly
x-api-key(case-sensitive) - Ensure header is sent during WebSocket handshake (not in message body)
API Key Not Working
Problem: API Key is set but still getting errors
Solutions:
- Verify API Key is in the same region as API Gateway
- Check usage plan limits (throttle/quota) haven't been exceeded
- Ensure API Key hasn't been disabled
- Verify stage is deployed after configuration changes
- Check CloudWatch logs for detailed error messages
Testing API Key
You can test if API Key is working:
- Without API Key: Should get 403 Forbidden
- With Invalid API Key: Should get 403 Forbidden
- With Valid API Key: Should connect successfully
API Key Best Practices
Store Securely: Never commit API Keys to version control
- Use environment variables
- Use AWS Secrets Manager for production
- Use secure configuration management
Rotate Regularly: Periodically rotate API Keys
- Create new API Key
- Update clients
- Disable old API Key
- Delete old API Key after verification
Use Usage Plans: Set throttling and quotas to prevent abuse
- Set reasonable rate limits
- Monitor usage in CloudWatch
- Set up alarms for unusual activity
Per-Client Keys: Create separate API Keys for different clients/devices
- Easier to track usage per client
- Can revoke access per client
- Better security isolation
Monitor Usage: Track API Key usage
- Use CloudWatch metrics
- Set up billing alerts
- Review usage patterns regularly
Alternative: Custom Authorizer
For more advanced authentication, consider using a Lambda Authorizer instead of API Keys:
- Create Lambda function for authorization
- Configure as authorizer on
$connectroute - Validate tokens, JWT, or custom authentication
- Return IAM policy for authorization
API Keys are simpler but less flexible than custom authorizers.
Message Flow
1. Connection ($connect)
When device connects:
- API Gateway calls Lambda with
routeKey: "$connect" - Lambda stores connection info
- Device should send
hellomessage next
2. Hello Message
Device sends:
{
"type": "hello",
"version": 3,
"transport": "websocket",
"features": {"mcp": true},
"audio_params": {
"format": "opus",
"sample_rate": 16000,
"channels": 1,
"frame_duration": 60
}
}
Route selection: $request.body.type → "hello" → routes to hello route
Lambda responds:
{
"type": "hello",
"session_id": "...",
"transport": "websocket",
"audio_params": {
"sample_rate": 16000,
"frame_duration": 60
}
}
3. Binary Messages (Audio)
Device sends binary WebSocket frames:
- Route selection: No
typefield → routes to$defaultroute - Lambda processes as base64-encoded binary data
4. Text Messages
Device sends JSON messages with type field:
type: "listen"→ routes tolistenroutetype: "abort"→ routes toabortroutetype: "goodbye"→ routes togoodbyeroutetype: "mcp"→ routes tomcproute
5. Disconnection ($disconnect)
When device disconnects:
- API Gateway calls Lambda with
routeKey: "$disconnect" - Lambda cleans up session
Testing
Test WebSocket Connection
import asyncio
import websockets
import json
async def test():
uri = "wss://your-api-id.execute-api.region.amazonaws.com/stage"
headers = {
"x-api-key": "your-api-key-value", # Required if API Key is enabled
"Device-Id": "AA:BB:CC:DD:EE:FF",
"Client-Id": "test-client",
"Protocol-Version": "3",
"Authorization": "Bearer test-token"
}
async with websockets.connect(uri, extra_headers=headers) as ws:
# Send hello
hello = {
"type": "hello",
"version": 3,
"transport": "websocket",
"features": {"mcp": True},
"audio_params": {
"format": "opus",
"sample_rate": 16000,
"channels": 1,
"frame_duration": 60
}
}
await ws.send(json.dumps(hello))
# Receive response
response = await ws.recv()
print(f"Received: {response}")
asyncio.run(test())
Note: Include x-api-key header if you've configured API Key requirement (see Configure API Key section).
Troubleshooting
Route Selection Not Working
- Verify route selection expression:
$request.body.type - Check that messages include
typefield - Binary messages should route to
$default
Connection Issues
- Verify API Gateway endpoint URL
- Check Lambda permissions for
execute-api:ManageConnections - Verify
API_GATEWAY_ENDPOINTenvironment variable
Message Not Received
- Check CloudWatch logs for Lambda function
- Verify route is configured correctly
- Check that message format matches protocol
Binary Message Handling
- Binary messages are base64-encoded by API Gateway
- Handler automatically decodes before processing
- Ensure
$defaultroute is configured
Session Management
Sessions are stored in memory (Lambda function memory). For production:
- Use DynamoDB for session persistence
- Use ElastiCache for shared session state
- Use API Gateway connection management for connection tracking
Example DynamoDB integration:
import boto3
dynamodb = boto3.resource('dynamodb')
sessions_table = dynamodb.Table('websocket-sessions')
# Store session
sessions_table.put_item(Item={
'connection_id': connection_id,
'session_id': session_id,
'device_id': device_id,
'created_at': datetime.utcnow().isoformat(),
'ttl': int((datetime.utcnow() + timedelta(seconds=3600)).timestamp())
})
Cost Optimization
- Use Provisioned Concurrency for consistent performance
- Use Reserved Concurrency to limit costs
- Monitor API Gateway connection count
- Use DynamoDB TTL for automatic session cleanup
Security
- Authentication: Validate
Authorizationheader in$connect - Rate Limiting: Configure throttling in API Gateway
- CORS: Configure if needed (not required for WebSocket)
- VPC: Deploy Lambda in VPC if accessing private resources
Next Steps
- Integrate with AI services (STT/TTS/LLM)
- Add DynamoDB for session persistence
- Set up CloudWatch alarms
- Configure auto-scaling
- Add monitoring and logging
Custom Domain Configuration
This guide explains how to bind a custom domain to your WebSocket API Gateway, allowing you to use a friendly domain name instead of the default API Gateway endpoint.
Prerequisites
- Domain Name: You need to own a domain name (e.g.,
example.com) - Route 53 Hosted Zone (recommended) or access to your domain's DNS settings
- SSL/TLS Certificate: AWS Certificate Manager (ACM) certificate for your domain
Step 1: Request SSL/TLS Certificate in ACM
- Go to AWS Certificate Manager console
- Click Request a certificate
- Select Request a public certificate
- Enter your domain name:
- For root domain:
example.com - For subdomain:
ws.example.comorapi.example.com - You can also add wildcard:
*.example.com
- For root domain:
- Choose DNS validation (recommended) or Email validation
- If using DNS validation, add the CNAME records to your DNS provider
- Wait for certificate to be issued (status: Issued)
Important: The certificate must be in the same region as your API Gateway.
Step 2: Create Custom Domain in API Gateway
- Go to API Gateway console
- Select your WebSocket API
- In the left sidebar, click Custom domain names
- Click Create
- Configure:
- Domain name: Enter your domain (e.g.,
ws.example.com) - Certificate: Select the ACM certificate from Step 1
- Endpoint type: Choose Regional (recommended) or Edge-optimized
- Domain name: Enter your domain (e.g.,
- Click Create
Step 3: Configure API Mapping
After creating the custom domain:
- Click on your custom domain name
- Go to API mappings tab
- Click Configure API mappings
- Click Add new mapping
- Configure:
- API: Select your WebSocket API
- Stage: Select your stage (e.g.,
production) - Path: Leave empty (or use a path like
websocketif needed)
- Click Save
Step 4: Update DNS Records
You need to create a DNS record pointing to your custom domain.
Option A: Using Route 53 (Recommended)
- Go to Route 53 console
- Select your hosted zone
- Click Create record
- Configure:
- Record name: Enter subdomain (e.g.,
wsforws.example.com) - Record type: Select A - Routes traffic to an IPv4 address
- Alias: Enable Alias
- Route traffic to: Select Alias to API Gateway API
- Region: Select your API Gateway region
- API: Select your WebSocket API
- Stage: Select your stage
- Record name: Enter subdomain (e.g.,
- Click Create records
Option B: Using External DNS Provider
- Go to your domain's DNS provider (e.g., GoDaddy, Namecheap, Cloudflare)
- Get the Target domain name from API Gateway:
- Go to your custom domain in API Gateway
- Copy the API Gateway domain name (e.g.,
d-abc123xyz.execute-api.us-east-1.amazonaws.com)
- Create a CNAME record:
- Name:
ws(forws.example.com) - Type:
CNAME - Value: Paste the API Gateway domain name
- TTL:
300(or your preference)
- Name:
- Save the record
Note: DNS propagation can take 5-60 minutes.
Step 5: Verify Domain Configuration
- Wait for DNS propagation (check with
nslookupordig) - Test DNS resolution:
nslookup ws.example.com # or dig ws.example.com - Verify SSL certificate:
openssl s_client -connect ws.example.com:443 -servername ws.example.com
Step 6: Update WebSocket URL
After DNS propagation, update your WebSocket connection URL:
Before (API Gateway endpoint):
wss://abc123.execute-api.us-east-1.amazonaws.com/production
After (Custom domain):
wss://ws.example.com
Or with path mapping:
wss://ws.example.com/websocket
Step 7: Update Lambda Environment Variable
If your Lambda function uses the API Gateway endpoint, update the API_GATEWAY_ENDPOINT environment variable:
- Go to Lambda function
- Go to Configuration → Environment variables
- Update
API_GATEWAY_ENDPOINT:
(Usehttps://ws.example.comhttps://notwss://)
Testing Custom Domain
Test WebSocket Connection
import asyncio
import websockets
import json
async def test():
# Use custom domain instead of API Gateway endpoint
uri = "wss://ws.example.com"
headers = {
"Device-Id": "AA:BB:CC:DD:EE:FF",
"Client-Id": "test-client",
"Protocol-Version": "3",
"Authorization": "Bearer test-token"
}
async with websockets.connect(uri, extra_headers=headers) as ws:
# Send hello
hello = {
"type": "hello",
"version": 3,
"transport": "websocket",
"features": {"mcp": True},
"audio_params": {
"format": "opus",
"sample_rate": 16000,
"channels": 1,
"frame_duration": 60
}
}
await ws.send(json.dumps(hello))
# Receive response
response = await ws.recv()
print(f"Received: {response}")
asyncio.run(test())
Test with Postman
- Open Postman
- Create new WebSocket request
- Enter URL:
wss://ws.example.com - Add headers as before
- Connect and test
Troubleshooting
DNS Not Resolving
Problem: Custom domain doesn't resolve
Solutions:
- Wait for DNS propagation (can take up to 48 hours, usually 5-60 minutes)
- Verify DNS record is correct:
nslookup ws.example.com - Check Route 53 hosted zone or external DNS provider
- Ensure CNAME/Alias record points to correct API Gateway domain
SSL Certificate Issues
Problem: SSL certificate errors
Solutions:
- Verify certificate is issued and active in ACM
- Ensure certificate is in the same region as API Gateway
- Check certificate covers your domain (including subdomain)
- Verify DNS validation records are added correctly
Connection Refused
Problem: Cannot connect to custom domain
Solutions:
- Verify API mapping is configured correctly
- Check that stage is deployed
- Ensure custom domain is active (not in "Pending" state)
- Verify API Gateway domain name in DNS record is correct
403 Forbidden
Problem: Connection returns 403 error
Solutions:
- Check API mapping configuration
- Verify stage name matches
- Ensure API Gateway is deployed to the stage
- Check if path mapping is required
Best Practices
- Use Subdomain: Use a subdomain (e.g.,
ws.example.com) instead of root domain - Regional Endpoint: Use Regional endpoint type for better performance
- Wildcard Certificate: Use
*.example.comcertificate for multiple subdomains - Route 53: Use Route 53 for easier DNS management and faster propagation
- Monitor: Set up CloudWatch alarms for custom domain health
- Backup: Keep the original API Gateway endpoint as backup
Cost Considerations
- Custom Domain: Free
- ACM Certificate: Free (for public certificates)
- Route 53: Charges apply for hosted zones and queries
- API Gateway: Same pricing as without custom domain
Security Notes
- HTTPS Only: Custom domains automatically use HTTPS/WSS
- Certificate Validation: Always validate certificates through ACM
- DNS Security: Use DNSSEC if available
- Access Control: Custom domain doesn't change API Gateway access control
Next Steps
- Update device firmware to use custom domain
- Update documentation with new WebSocket URL
- Set up monitoring for custom domain
- Configure backup endpoint for failover
Route Selection Expression
Answer: Route Selection Expression
When configuring your WebSocket API in AWS API Gateway, use:
$request.body.type
This expression extracts the type field from the JSON message body to route messages to the appropriate handler.
How It Works
When a device sends a JSON message like:
{
"type": "hello",
"version": 3,
"transport": "websocket",
...
}
The route selection expression $request.body.type evaluates to "hello", which routes the message to the hello route.
Route Configuration
Configure these routes in API Gateway:
| Route Key | When Used | Example Message |
|---|---|---|
$connect | WebSocket connection established | (automatic) |
$disconnect | WebSocket connection closed | (automatic) |
$default | Binary messages (no type field) | Binary audio packets |
hello | type: "hello" | Session establishment |
listen | type: "listen" | Listening state changes |
abort | type: "abort" | Abort operation |
goodbye | type: "goodbye" | Close session |
mcp | type: "mcp" | MCP protocol messages |
Important Notes
Binary Messages: Binary WebSocket frames don't have a
typefield, so they route to$defaultAll Routes → Same Lambda: All routes can point to the same Lambda function. The handler (
websocket_server_aws.lambda_handler) will check therouteKeyin the event and handle accordingly.Route Selection Expression Syntax:
$request.body.type- extracts from JSON body$request.body.action- alternative if usingactionfield$request.body.routeKey- alternative if usingrouteKeyfield
Example Configuration
In AWS API Gateway Console:
- Route Selection Expression:
$request.body.type - Routes:
$connect→ Lambda function$disconnect→ Lambda function$default→ Lambda functionhello→ Lambda functionlisten→ Lambda functionabort→ Lambda functiongoodbye→ Lambda functionmcp→ Lambda function
All routes point to the same Lambda function (websocket_server_aws.lambda_handler).
Testing Route Selection
You can test the route selection expression in the API Gateway console:
- Go to your WebSocket API
- Click on a route (e.g.,
hello) - Use the test feature with a sample body:
{ "type": "hello", "version": 3 } - Verify it routes correctly
Troubleshooting
Problem: Messages not routing correctly
Solution:
- Verify route selection expression is exactly
$request.body.type - Check that messages include
typefield - Ensure routes are configured for all message types
- Check Lambda logs for routing information
Problem: Binary messages not working
Solution:
- Ensure
$defaultroute is configured - Binary messages don't have
typefield, so they route to$default - Handler automatically detects binary vs text messages
Testing with Postman
This guide explains how to test the WebSocket endpoint in Postman.
Steps to Test in Postman
1. Create New WebSocket Request
- Open Postman
- Click New → WebSocket Request
- Or use shortcut:
Ctrl+N(Windows/Linux) orCmd+N(Mac) → Select WebSocket Request
2. Enter WebSocket URL
In the URL field, enter:
wss://8ib3640062.execute-api.cn-northwest-1.amazonaws.com.cn/production
3. Add Headers (Optional but Recommended)
Click on Headers tab and add:
| Header Name | Value | Description |
|---|---|---|
x-api-key | your-api-key-value | API Key (required if API Key is enabled) |
Device-Id | AA:BB:CC:DD:EE:FF | Device MAC address (test value) |
Client-Id | test-client-123 | Client UUID (test value) |
Protocol-Version | 3 | Protocol version (1, 2, or 3) |
Authorization | Bearer your-token | Authentication token (if required) |
Note:
x-api-keyheader is required if you've configured API Key requirement (see Configure API Key section)- Other headers are optional for WebSocket connections in Postman, but your server may require them
4. Connect to WebSocket
- Click Connect button
- You should see connection status change to "Connected"
- Connection messages will appear in the message panel
5. Send Hello Message
After connecting, send a hello message to establish the session:
{
"type": "hello",
"version": 3,
"transport": "websocket",
"features": {
"aec": false,
"mcp": false
},
"audio_params": {
"format": "opus",
"sample_rate": 16000,
"channels": 1,
"frame_duration": 60
}
}
Steps:
- In the message input area, select Text format
- Paste the JSON above
- Click Send
6. Expected Response
You should receive a response like:
{
"type": "hello",
"session_id": "abc123...",
"transport": "websocket",
"audio_params": {
"sample_rate": 16000,
"frame_duration": 60
}
}
7. Test Other Messages
Send Listen Message
{
"type": "listen",
"session_id": "your-session-id-from-hello-response",
"state": "start",
"mode": "auto"
}
Send Listen Stop
{
"type": "listen",
"session_id": "your-session-id",
"state": "stop"
}
Send Goodbye
{
"type": "goodbye",
"session_id": "your-session-id"
}
8. Disconnect
Click Disconnect button to close the WebSocket connection.
Troubleshooting
Connection Issues
Problem: Cannot connect to WebSocket
Solutions:
- Verify the URL is correct (starts with
wss://) - Check if your network/firewall allows WebSocket connections
- Verify AWS API Gateway is deployed and accessible
- Check CloudWatch logs for connection errors
Problem: Connection closes immediately
Solutions:
- Check if
$connectroute is configured in API Gateway - Verify Lambda function has proper permissions
- Check Lambda function logs in CloudWatch
- If API Key is required, verify
x-api-keyheader is included and valid - Check if API Key is enabled and associated with usage plan
Message Issues
Problem: No response to hello message
Solutions:
- Verify
helloroute is configured in API Gateway - Check route selection expression:
$request.body.type - Verify Lambda function is handling the route correctly
- Check Lambda function logs
Problem: Invalid message format
Solutions:
- Ensure JSON is valid (no trailing commas, proper quotes)
- Verify message includes
typefield - Check that
session_idis included for messages after hello
Alternative: Using cURL
If Postman doesn't work, you can test with websocat (WebSocket client):
# Install websocat (if not installed)
# macOS: brew install websocat
# Linux: cargo install websocat
# Connect and send hello message (with API Key if required)
echo '{"type":"hello","version":3,"transport":"websocket","features":{"mcp":true},"audio_params":{"format":"opus","sample_rate":16000,"channels":1,"frame_duration":60}}' | \
websocat -H "x-api-key: your-api-key-value" \
-H "Device-Id: AA:BB:CC:DD:EE:FF" \
-H "Client-Id: test-client" \
-H "Protocol-Version: 3" \
wss://8ib3640062.execute-api.cn-northwest-1.amazonaws.com.cn/production
Alternative: Using Python Script
Create a test script:
import asyncio
import websockets
import json
async def test():
uri = "wss://8ib3640062.execute-api.cn-northwest-1.amazonaws.com.cn/production"
headers = {
"x-api-key": "your-api-key-value", # Required if API Key is enabled
"Device-Id": "AA:BB:CC:DD:EE:FF",
"Client-Id": "test-client-123",
"Protocol-Version": "3",
"Authorization": "Bearer test-token"
}
async with websockets.connect(uri, extra_headers=headers) as ws:
print("Connected!")
# Send hello
hello = {
"type": "hello",
"version": 3,
"transport": "websocket",
"features": {"mcp": True},
"audio_params": {
"format": "opus",
"sample_rate": 16000,
"channels": 1,
"frame_duration": 60
}
}
await ws.send(json.dumps(hello))
print(f"Sent: {json.dumps(hello)}")
# Receive response
response = await ws.recv()
print(f"Received: {response}")
# Parse response
data = json.loads(response)
session_id = data.get("session_id")
if session_id:
# Send listen message
listen = {
"type": "listen",
"session_id": session_id,
"state": "start",
"mode": "auto"
}
await ws.send(json.dumps(listen))
print(f"Sent: {json.dumps(listen)}")
# Wait for any responses
try:
response = await asyncio.wait_for(ws.recv(), timeout=5.0)
print(f"Received: {response}")
except asyncio.TimeoutError:
print("No response received")
# Send goodbye
goodbye = {
"type": "goodbye",
"session_id": session_id
}
await ws.send(json.dumps(goodbye))
print(f"Sent: {json.dumps(goodbye)}")
asyncio.run(test())
Testing Binary Messages
Postman supports binary messages, but you'll need to:
- Select Binary format in message input
- Send base64-encoded binary data or raw bytes
For testing audio packets, you can send a test binary message (though it won't be valid OPUS data):
00000000 # Protocol version 3 format: [type:1][reserved:1][payload_size:2][payload]
Postman Features
Message History
- All sent/received messages are saved in the message panel
- You can resend previous messages
- Messages are color-coded (sent/received)
Auto-reconnect
- Postman can auto-reconnect if connection drops
- Configure in connection settings
Save Collection
- Save your WebSocket requests in a Postman collection
- Share with team members
- Use for automated testing
Expected Message Flow
- Connect → API Gateway calls
$connectroute - Send hello → Routes to
helloroute → Receive session_id - Send listen → Routes to
listenroute - Send binary → Routes to
$defaultroute - Send goodbye → Routes to
goodbyeroute - Disconnect → API Gateway calls
$disconnectroute
Monitoring
While testing, monitor:
- Postman Console: View connection status and messages
- AWS CloudWatch: Check Lambda function logs
- API Gateway Logs: View connection and routing logs
- Lambda Metrics: Check invocations, errors, duration
Common Test Scenarios
Scenario 1: Basic Connection Test
- Connect → Should see connection established
- Disconnect → Should see clean disconnect
Scenario 2: Hello Handshake
- Connect → Send hello → Should receive hello response with session_id
Scenario 3: Full Session
- Connect → Hello → Listen start → Listen stop → Goodbye → Disconnect
Scenario 4: Error Handling
- Send invalid JSON → Should handle gracefully
- Send message without session_id → Should return error
- Send message with wrong session_id → Should return error
Notes
- Postman WebSocket support may vary by version
- Some advanced features (like binary message editing) may be limited
- For production testing, consider using dedicated WebSocket testing tools
- Always check CloudWatch logs for server-side issues