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 centerreturn-to-kiosk- Kill current app, return to kioskrestart-kiosk- Restart kiosk applicationexecute- Execute shell commandping- Ping for heartbeat
Server → Client:
connected- Connection confirmedinput- Remote input eventerror- 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-clientsystem 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
- Client connects to
ws://localhost:3001 - Server sends
{ type: 'connected', payload: {...} } - Client can send commands
- Server processes and responds
- 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
-
Connection Pooling:
- Reuse WebSocket connections
- Don't create new connection per message
-
Message Batching:
- Send multiple events in one message if possible
- Avoid rapid successive messages
-
Resource Cleanup:
- Properly close WebSocket connections
- Kill child processes when done
-
Monitoring:
- Log important events
- Track connection count
- Monitor memory usage
Security Considerations
-
Input Validation:
- Validate command strings
- Prevent shell injection
- Whitelist allowed commands
-
Authentication:
- In production, add auth before executing commands
- Use JWT or similar for WebSocket auth
-
CORS:
- Configure CORS_ORIGIN for specific domains
- Don't allow all origins in production
-
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