Hardware

Stikadoo AI Printer

What is Stikadoo?


Dataflow

  1. ESP32 gets WebSocket config from /challenge response (in ota.cc)
  2. ESP32 connects → handle_connect() in aws_lambda.py
  3. ESP32 sends hello → session created
  4. ESP32 sends listen start → audio buffer cleared
  5. ESP32 sends binary audio packets → handle_binary_message() buffers audio
  6. ESP32 sends listen stop → should trigger STT (currently placeholder)
  7. AWS Transcribe converts audio to text (not yet implemented)
  8. Text sent back to ESP32 as STT message (integration missing)
  9. 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

TypeDescriptionFields
helloEstablish sessionversion, transport, features, audio_params
listenListening statesession_id, state (start/stop/detect), mode
abortAbort operationsession_id, reason
goodbyeClose sessionsession_id
mcpMCP protocolsession_id, payload
BinaryOPUS audioProtocol version 1/2/3 format

Server → Device Messages

TypeDescriptionFields
helloSession responsesession_id, transport, audio_params
ttsText-to-speechsession_id, state, text
sttSpeech-to-textsession_id, text
llmLLM emotionsession_id, emotion
systemSystem commandsession_id, command
alertAlert messagesession_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

  1. Read deployment guide for detailed deployment instructions
  2. Integrate with your AI service provider
  3. Test with actual ESP32 device
  4. 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 KeyDescriptionLambda Function
$connectWebSocket connection establishedSame Lambda
$disconnectWebSocket connection closedSame Lambda
$defaultBinary messages (audio packets)Same Lambda
helloHello message (session establishment)Same Lambda
listenListen state changesSame Lambda
abortAbort operationSame Lambda
goodbyeClose sessionSame Lambda
mcpMCP protocol messagesSame 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

  1. Go to AWS Lambda Console
  2. Create a new function
  3. Choose Python 3.9 or later runtime
  4. 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, aiohttp and 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

  1. Go to API Gateway Console
  2. Click Create API
  3. Select WebSocket API
  4. Configure:
    • API name: stickerbox-websocket (or your choice)
    • Route selection expression: $request.body.type
    • Description: (optional)

5. Configure Routes

For each route, create an integration:

  1. Click on the route (e.g., $connect)
  2. Click Attach integration
  3. Select Lambda Function
  4. Choose your Lambda function
  5. Click Attach

Repeat for all routes:

  • $connect
  • $disconnect
  • $default
  • hello
  • listen
  • abort
  • goodbye
  • mcp

6. Set API Gateway Endpoint

After deploying the API, get the WebSocket endpoint URL:

  1. Go to Stages in your API
  2. Copy the WebSocket URL (e.g., wss://abc123.execute-api.us-east-1.amazonaws.com/production)
  3. Update Lambda environment variable API_GATEWAY_ENDPOINT:
    https://abc123.execute-api.us-east-1.amazonaws.com/production
    
    (Note: Use https:// not wss://)

7. Deploy API

  1. Go to Stages in your API
  2. Click Deploy API
  3. Select or create a stage (e.g., production)
  4. Click Deploy

8. Configure Permissions

Ensure Lambda has permission to call API Gateway Management API:

  1. Go to Lambda function
  2. Go to ConfigurationPermissions
  3. Click on the execution role Edit button.
  4. 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/*"
        }
    ]
}

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

  1. Go to API Gateway console
  2. In the left sidebar, click API Keys
  3. Click Create API Key
  4. Configure:
    • Name: stickerbox-websocket-key (or your choice)
    • Description: (optional)
    • API key source: Select Auto-generate (recommended)
    • Enabled: Check Enabled
  5. Click Save
  6. Important: Copy the API Key value immediately (you won't be able to see it again)
    • The API Key will look like: abc123XYZ789...

Usage plans allow you to set throttling and quota limits:

  1. In API Gateway console, click Usage Plans in the left sidebar
  2. Click Create
  3. 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)
    • Quota: (optional) Set daily/monthly limits
      • Quota: Total requests (e.g., 10000)
      • Period: Day or Month
  4. Click Next
  5. Add API stages:
    • Select your WebSocket API
    • Select your stage (e.g., production)
    • Click Add
  6. Click Next
  7. Add API keys:
    • Select the API key created in Step 1
    • Click Add
  8. Click Done

Step 3: Require API Key on Stage

  1. Go to your WebSocket API in API Gateway
  2. Click Stages in the left sidebar
  3. Select your stage (e.g., production)
  4. Click on the $connect route (or any route you want to protect)
  5. Click Route Request tab
  6. Check API Key Required
  7. Click Save
  8. 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:

  1. Go to Stages in your API
  2. Click Deploy API
  3. Select your stage
  4. 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

  1. Open Postman
  2. Create new WebSocket request
  3. Enter WebSocket URL
  4. Go to Headers tab
  5. Add header:
    • Key: x-api-key
    • Value: your-api-key-value
  6. Add other headers as needed
  7. 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-key header 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:

  1. Without API Key: Should get 403 Forbidden
  2. With Invalid API Key: Should get 403 Forbidden
  3. With Valid API Key: Should connect successfully

API Key Best Practices

  1. Store Securely: Never commit API Keys to version control

    • Use environment variables
    • Use AWS Secrets Manager for production
    • Use secure configuration management
  2. Rotate Regularly: Periodically rotate API Keys

    • Create new API Key
    • Update clients
    • Disable old API Key
    • Delete old API Key after verification
  3. Use Usage Plans: Set throttling and quotas to prevent abuse

    • Set reasonable rate limits
    • Monitor usage in CloudWatch
    • Set up alarms for unusual activity
  4. 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
  5. 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:

  1. Create Lambda function for authorization
  2. Configure as authorizer on $connect route
  3. Validate tokens, JWT, or custom authentication
  4. 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 hello message 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 type field → routes to $default route
  • Lambda processes as base64-encoded binary data

4. Text Messages

Device sends JSON messages with type field:

  • type: "listen" → routes to listen route
  • type: "abort" → routes to abort route
  • type: "goodbye" → routes to goodbye route
  • type: "mcp" → routes to mcp route

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 type field
  • Binary messages should route to $default

Connection Issues

  • Verify API Gateway endpoint URL
  • Check Lambda permissions for execute-api:ManageConnections
  • Verify API_GATEWAY_ENDPOINT environment 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 $default route is configured

Session Management

Sessions are stored in memory (Lambda function memory). For production:

  1. Use DynamoDB for session persistence
  2. Use ElastiCache for shared session state
  3. 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

  1. Authentication: Validate Authorization header in $connect
  2. Rate Limiting: Configure throttling in API Gateway
  3. CORS: Configure if needed (not required for WebSocket)
  4. VPC: Deploy Lambda in VPC if accessing private resources

Next Steps

  1. Integrate with AI services (STT/TTS/LLM)
  2. Add DynamoDB for session persistence
  3. Set up CloudWatch alarms
  4. Configure auto-scaling
  5. 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

  1. Domain Name: You need to own a domain name (e.g., example.com)
  2. Route 53 Hosted Zone (recommended) or access to your domain's DNS settings
  3. SSL/TLS Certificate: AWS Certificate Manager (ACM) certificate for your domain

Step 1: Request SSL/TLS Certificate in ACM

  1. Go to AWS Certificate Manager console
  2. Click Request a certificate
  3. Select Request a public certificate
  4. Enter your domain name:
    • For root domain: example.com
    • For subdomain: ws.example.com or api.example.com
    • You can also add wildcard: *.example.com
  5. Choose DNS validation (recommended) or Email validation
  6. If using DNS validation, add the CNAME records to your DNS provider
  7. 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

  1. Go to API Gateway console
  2. Select your WebSocket API
  3. In the left sidebar, click Custom domain names
  4. Click Create
  5. 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
  6. Click Create

Step 3: Configure API Mapping

After creating the custom domain:

  1. Click on your custom domain name
  2. Go to API mappings tab
  3. Click Configure API mappings
  4. Click Add new mapping
  5. Configure:
    • API: Select your WebSocket API
    • Stage: Select your stage (e.g., production)
    • Path: Leave empty (or use a path like websocket if needed)
  6. Click Save

Step 4: Update DNS Records

You need to create a DNS record pointing to your custom domain.

  1. Go to Route 53 console
  2. Select your hosted zone
  3. Click Create record
  4. Configure:
    • Record name: Enter subdomain (e.g., ws for ws.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
  5. Click Create records

Option B: Using External DNS Provider

  1. Go to your domain's DNS provider (e.g., GoDaddy, Namecheap, Cloudflare)
  2. 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)
  3. Create a CNAME record:
    • Name: ws (for ws.example.com)
    • Type: CNAME
    • Value: Paste the API Gateway domain name
    • TTL: 300 (or your preference)
  4. Save the record

Note: DNS propagation can take 5-60 minutes.

Step 5: Verify Domain Configuration

  1. Wait for DNS propagation (check with nslookup or dig)
  2. Test DNS resolution:
    nslookup ws.example.com
    # or
    dig ws.example.com
    
  3. 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:

  1. Go to Lambda function
  2. Go to ConfigurationEnvironment variables
  3. Update API_GATEWAY_ENDPOINT:
    https://ws.example.com
    
    (Use https:// not wss://)

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

  1. Open Postman
  2. Create new WebSocket request
  3. Enter URL: wss://ws.example.com
  4. Add headers as before
  5. 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

  1. Use Subdomain: Use a subdomain (e.g., ws.example.com) instead of root domain
  2. Regional Endpoint: Use Regional endpoint type for better performance
  3. Wildcard Certificate: Use *.example.com certificate for multiple subdomains
  4. Route 53: Use Route 53 for easier DNS management and faster propagation
  5. Monitor: Set up CloudWatch alarms for custom domain health
  6. 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

  1. HTTPS Only: Custom domains automatically use HTTPS/WSS
  2. Certificate Validation: Always validate certificates through ACM
  3. DNS Security: Use DNSSEC if available
  4. Access Control: Custom domain doesn't change API Gateway access control

Next Steps

  1. Update device firmware to use custom domain
  2. Update documentation with new WebSocket URL
  3. Set up monitoring for custom domain
  4. 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 KeyWhen UsedExample Message
$connectWebSocket connection established(automatic)
$disconnectWebSocket connection closed(automatic)
$defaultBinary messages (no type field)Binary audio packets
hellotype: "hello"Session establishment
listentype: "listen"Listening state changes
aborttype: "abort"Abort operation
goodbyetype: "goodbye"Close session
mcptype: "mcp"MCP protocol messages

Important Notes

  1. Binary Messages: Binary WebSocket frames don't have a type field, so they route to $default

  2. All Routes → Same Lambda: All routes can point to the same Lambda function. The handler (websocket_server_aws.lambda_handler) will check the routeKey in the event and handle accordingly.

  3. Route Selection Expression Syntax:

    • $request.body.type - extracts from JSON body
    • $request.body.action - alternative if using action field
    • $request.body.routeKey - alternative if using routeKey field

Example Configuration

In AWS API Gateway Console:

  1. Route Selection Expression: $request.body.type
  2. Routes:
    • $connect → Lambda function
    • $disconnect → Lambda function
    • $default → Lambda function
    • hello → Lambda function
    • listen → Lambda function
    • abort → Lambda function
    • goodbye → Lambda function
    • mcp → 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:

  1. Go to your WebSocket API
  2. Click on a route (e.g., hello)
  3. Use the test feature with a sample body:
    {
      "type": "hello",
      "version": 3
    }
    
  4. Verify it routes correctly

Troubleshooting

Problem: Messages not routing correctly

Solution:

  • Verify route selection expression is exactly $request.body.type
  • Check that messages include type field
  • Ensure routes are configured for all message types
  • Check Lambda logs for routing information

Problem: Binary messages not working

Solution:

  • Ensure $default route is configured
  • Binary messages don't have type field, 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

  1. Open Postman
  2. Click NewWebSocket Request
  3. Or use shortcut: Ctrl+N (Windows/Linux) or Cmd+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

Click on Headers tab and add:

Header NameValueDescription
x-api-keyyour-api-key-valueAPI Key (required if API Key is enabled)
Device-IdAA:BB:CC:DD:EE:FFDevice MAC address (test value)
Client-Idtest-client-123Client UUID (test value)
Protocol-Version3Protocol version (1, 2, or 3)
AuthorizationBearer your-tokenAuthentication token (if required)

Note:

  • x-api-key header 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

  1. Click Connect button
  2. You should see connection status change to "Connected"
  3. 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:

  1. In the message input area, select Text format
  2. Paste the JSON above
  3. 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 $connect route 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-key header 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 hello route 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 type field
  • Check that session_id is 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:

  1. Select Binary format in message input
  2. 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

  1. Connect → API Gateway calls $connect route
  2. Send hello → Routes to hello route → Receive session_id
  3. Send listen → Routes to listen route
  4. Send binary → Routes to $default route
  5. Send goodbye → Routes to goodbye route
  6. Disconnect → API Gateway calls $disconnect route

Monitoring

While testing, monitor:

  1. Postman Console: View connection status and messages
  2. AWS CloudWatch: Check Lambda function logs
  3. API Gateway Logs: View connection and routing logs
  4. 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
Previous
Raspberry Pi