2026-04-06 21:33:52 -04:00

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)