hotel_pi/control-service
2026-04-06 21:33:52 -04:00
..
2026-04-06 21:33:52 -04:00
2026-04-06 21:33:52 -04:00
2026-04-06 21:33:52 -04:00
2026-04-06 21:33:52 -04:00
2026-04-06 21:33:52 -04:00

Control Service Development Guide

Overview

The Hotel Pi Control Service is a Node.js WebSocket server that:

  • Listens for HDMI-CEC remote input
  • Translates input to navigation events
  • Executes system commands (launch apps, etc.)
  • Communicates with the frontend via WebSocket

Architecture

src/
├── server.js          # Main server & WebSocket handler
├── cec-handler.js     # HDMI-CEC input processing
├── commands.js        # System command execution
└── ...

package.json           # Dependencies
.eslintrc.json         # Linting config
Dockerfile             # Container image

Setup & Development

Local Setup

cd control-service
npm install
npm run dev

Server runs on http://localhost:3001

Commands

npm run dev    # Start with hot reload
npm run start  # Run production
npm run lint   # Check code style

Core Modules

server.js

Main HTTP/WebSocket server.

Features:

  • HTTP server on port 3001
  • WebSocket server for client connections
  • Health check endpoint (GET /health)
  • Message routing to handlers

HTTP Routes:

GET  /              - Server info
GET  /health        - Health status
WS   /              - WebSocket connection

WebSocket Messages:

Client → Server:

  • launch-plex - Launch Plex media center
  • return-to-kiosk - Kill current app, return to kiosk
  • restart-kiosk - Restart kiosk application
  • execute - Execute shell command
  • ping - Ping for heartbeat

Server → Client:

  • connected - Connection confirmed
  • input - Remote input event
  • error - Error message
  • Response to commands

cec-handler.js

HDMI-CEC input listener.

Class: CECHandler

const cec = new CECHandler(devicePath);

await cec.init();              // Initialize CEC
cec.on('input', callback);     // Listen for input
cec.startMonitoring(callback); // Start listening

Input Event Mapping:

CEC Button → Event Type
OK/Select → select
Up arrow  → up
Down arrow → down
Left arrow → left
Right arrow → right
Exit/Back → back

Notes:

  • Requires cec-client system package
  • Device path: /dev/ttyAMA0 (Raspberry Pi UART)
  • Gracefully falls back if cec-client unavailable

commands.js

System command executor.

Class: CommandExecutor

const executor = new CommandExecutor(config);

await executor.launchPlex();        // Launch Plex
await executor.restartKiosk();      // Restart kiosk
await executor.returnToKiosk();     // Kill Plex, return to kiosk
await executor.executeCommand(cmd); // Execute arbitrary command
executor.getHealth();               // Get service health

Configuration:

{
  plexLaunchCommand: '/usr/bin/plex-htpc',
  kioskLaunchCommand: '/home/pi/scripts/launch-kiosk.sh'
}

Executing Commands:

// Launch Plex
const result = await executor.launchPlex();
// Returns: { success: true, message: '...' }

// Execute custom command
const result = await executor.executeCommand('ls -la /tmp');
// Returns: { success: true, stdout: '...', stderr: '' }

WebSocket Protocol

Connection Lifecycle

  1. Client connects to ws://localhost:3001
  2. Server sends { type: 'connected', payload: {...} }
  3. Client can send commands
  4. Server processes and responds
  5. Client can emit input events

Example Client Usage

const ws = new WebSocket('ws://localhost:3001');

ws.onopen = () => {
  console.log('Connected');
  ws.send(JSON.stringify({
    type: 'launch-plex',
    payload: {}
  }));
};

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Response:', data);
};

ws.onerror = (error) => {
  console.error('Error:', error);
};

ws.onclose = () => {
  console.log('Disconnected');
};

Message Format

All messages are JSON:

{
  "type": "command-name",
  "payload": {
    "key": "value"
  }
}

Configuration

Environment Variables

PORT=3001                              # Server port
CEC_DEVICE=/dev/ttyAMA0                # CEC serial device
PLEX_LAUNCH_COMMAND=/usr/bin/plex-htpc # Plex executable
KIOSK_LAUNCH_COMMAND=/home/pi/...      # Kiosk script path

Runtime Options

Edit config object in server.js:

const executor = new CommandExecutor({
  plexLaunchCommand: process.env.PLEX_LAUNCH_COMMAND,
  kioskLaunchCommand: process.env.KIOSK_LAUNCH_COMMAND,
});

Building & Deployment

Docker Build

docker build -t hotel-pi-control .
docker run -p 3001:3001 hotel-pi-control

Production Deployment

# Build for production
npm run build

# Start service
npm run start

Systemd Service

Create /etc/systemd/system/hotel-pi-control.service:

[Unit]
Description=Hotel Pi Control Service
After=network.target

[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/hotel-pi
ExecStart=/usr/bin/node src/server.js
Restart=always
RestartSec=10
Environment="PATH=/usr/local/bin:/usr/bin"

[Install]
WantedBy=multi-user.target

Enable:

sudo systemctl daemon-reload
sudo systemctl enable hotel-pi-control
sudo systemctl start hotel-pi-control

Error Handling

Connection Errors

// Client disconnected
ws.on('close', () => {
  console.log('Client disconnected');
  clients.delete(ws);
});

// WebSocket error
ws.on('error', (error) => {
  console.error('WebSocket error:', error.message);
});

Command Execution Errors

Commands wrap in try-catch and return success flag:

try {
  await executor.launchPlex();
  // Send success response
} catch (error) {
  ws.send(JSON.stringify({
    type: 'error',
    payload: { message: error.message }
  }));
}

Logging

Service logs to console with emoji indicators:

✓ Success
✗ Error
❌ Critical error
📡 Connection event
🎮 Input event
🎬 Plex launch
🔙 Return to kiosk
⚙ Command execution
🛑 Shutdown

For persistent logs, redirect output:

npm run start > /var/log/hotel-pi-control.log 2>&1 &

Testing

Health Check

curl http://localhost:3001/health

{
  "status": "healthy",
  "timestamp": "2024-03-20T12:34:56Z",
  "processes": ["plex"]
}

WebSocket Test

Using wscat:

npm install -g wscat
wscat -c ws://localhost:3001

# Type messages:
> {"type":"ping","payload":{}}

< {"type":"pong","timestamp":"..."}

Command Testing

# Execute command
curl -X POST http://localhost:3001 \
  -H "Content-Type: application/json" \
  -d '{"type":"execute","payload":{"command":"echo hello"}}'

# Test launch (won't actually launch without Plex installed)
curl -X POST http://localhost:3001 \
  -H "Content-Type: application/json" \
  -d '{"type":"launch-plex","payload":{}}'

Troubleshooting

Port Already in Use

# Find process using port 3001
lsof -i :3001

# Kill process
kill -9 <PID>

CEC Not Working

# Check if cec-client is installed
which cec-client

# Install if missing (Ubuntu/Debian)
sudo apt-get install libcec-dev

# Test CEC connection
echo "as" | cec-client -s

WebSocket Connection Fails

  • Verify server is running: curl http://localhost:3001
  • Check firewall: sudo ufw allow 3001
  • Check browser console for CORS/connection errors
  • Verify WebSocket URL in frontend .env

Memory Leaks

Check active connections:

# In health response
curl http://localhost:3001/health | jq '.processes'

# Monitor over time
watch curl http://localhost:3001/health

Performance Optimization

  1. Connection Pooling:

    • Reuse WebSocket connections
    • Don't create new connection per message
  2. Message Batching:

    • Send multiple events in one message if possible
    • Avoid rapid successive messages
  3. Resource Cleanup:

    • Properly close WebSocket connections
    • Kill child processes when done
  4. Monitoring:

    • Log important events
    • Track connection count
    • Monitor memory usage

Security Considerations

  1. Input Validation:

    • Validate command strings
    • Prevent shell injection
    • Whitelist allowed commands
  2. Authentication:

    • In production, add auth before executing commands
    • Use JWT or similar for WebSocket auth
  3. CORS:

    • Configure CORS_ORIGIN for specific domains
    • Don't allow all origins in production
  4. Network:

    • Firewall port 3001 to local network only
    • Use HTTPS/WSS in production
    • Disable debug endpoints in production

Code Quality

Linting

npm run lint

Best Practices

  • Use async/await (not callbacks)
  • Handle errors in try-catch
  • Log all important events
  • Close connections properly
  • Validate input data

Resources