464 lines
8.7 KiB
Markdown
464 lines
8.7 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
cd control-service
|
|
npm install
|
|
npm run dev
|
|
```
|
|
|
|
Server runs on http://localhost:3001
|
|
|
|
### Commands
|
|
|
|
```bash
|
|
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**
|
|
|
|
```javascript
|
|
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**
|
|
|
|
```javascript
|
|
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:**
|
|
```javascript
|
|
{
|
|
plexLaunchCommand: '/usr/bin/plex-htpc',
|
|
kioskLaunchCommand: '/home/pi/scripts/launch-kiosk.sh'
|
|
}
|
|
```
|
|
|
|
**Executing Commands:**
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```json
|
|
{
|
|
"type": "command-name",
|
|
"payload": {
|
|
"key": "value"
|
|
}
|
|
}
|
|
```
|
|
|
|
## Configuration
|
|
|
|
### Environment Variables
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```javascript
|
|
const executor = new CommandExecutor({
|
|
plexLaunchCommand: process.env.PLEX_LAUNCH_COMMAND,
|
|
kioskLaunchCommand: process.env.KIOSK_LAUNCH_COMMAND,
|
|
});
|
|
```
|
|
|
|
## Building & Deployment
|
|
|
|
### Docker Build
|
|
|
|
```bash
|
|
docker build -t hotel-pi-control .
|
|
docker run -p 3001:3001 hotel-pi-control
|
|
```
|
|
|
|
### Production Deployment
|
|
|
|
```bash
|
|
# Build for production
|
|
npm run build
|
|
|
|
# Start service
|
|
npm run start
|
|
```
|
|
|
|
### Systemd Service
|
|
|
|
Create `/etc/systemd/system/hotel-pi-control.service`:
|
|
|
|
```ini
|
|
[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:
|
|
```bash
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable hotel-pi-control
|
|
sudo systemctl start hotel-pi-control
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Connection Errors
|
|
|
|
```javascript
|
|
// 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:
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```bash
|
|
npm run start > /var/log/hotel-pi-control.log 2>&1 &
|
|
```
|
|
|
|
## Testing
|
|
|
|
### Health Check
|
|
|
|
```bash
|
|
curl http://localhost:3001/health
|
|
|
|
{
|
|
"status": "healthy",
|
|
"timestamp": "2024-03-20T12:34:56Z",
|
|
"processes": ["plex"]
|
|
}
|
|
```
|
|
|
|
### WebSocket Test
|
|
|
|
Using `wscat`:
|
|
```bash
|
|
npm install -g wscat
|
|
wscat -c ws://localhost:3001
|
|
|
|
# Type messages:
|
|
> {"type":"ping","payload":{}}
|
|
|
|
< {"type":"pong","timestamp":"..."}
|
|
```
|
|
|
|
### Command Testing
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# Find process using port 3001
|
|
lsof -i :3001
|
|
|
|
# Kill process
|
|
kill -9 <PID>
|
|
```
|
|
|
|
### CEC Not Working
|
|
|
|
```bash
|
|
# 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:
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
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
|
|
|
|
- [Node.js Docs](https://nodejs.org/docs/)
|
|
- [ws Library](https://github.com/websockets/ws)
|
|
- [libcec Documentation](https://github.com/libcec/libcec)
|