Remote Engine Control API Codelab
Overview
This codelab guides you through implementing the Remote Engine Control API, which allows users to remotely start or stop their vehicle's engine through a mobile application. You'll learn how to integrate engine control functionality, monitor status in real-time, and handle safety requirements.
Prerequisites
- Basic understanding of REST APIs
- Familiarity with JavaScript/TypeScript
- Node.js and npm installed
- Text editor or IDE
Learning Objectives
- Implement remote engine start/stop functionality
- Handle real-time status updates via SSE
- Manage engine control history and settings
- Implement safety and security measures
- Handle error scenarios and edge cases
Setup
1. Project Initialization
mkdir remote-engine-control-demo
cd remote-engine-control-demo
npm init -y
npm install axios node-fetch2. Configuration
Create a config.js file:
export const API_CONFIG = {
baseURL: 'https://api.ecarus.run/api/v1/remotecontrol',
authToken: 'sk_4f9c7b8e2d1a6c0f3e7a9b5d8c1e4f2a7c6d9e0b3f5a8c1d4e7f9b2c6a1e3d',
sampleVIN: 'KMHSH81C7LU123456',
timeout: 30000 // 30 seconds
};Step 1: Basic Engine Control
Implement Engine Start
Create engineControl.js:
import axios from 'axios';
import { API_CONFIG } from './config.js';
class EngineController {
constructor() {
this.client = axios.create({
baseURL: API_CONFIG.baseURL,
headers: {
'Authorization': `Bearer ${API_CONFIG.authToken}`,
'Content-Type': 'application/json'
},
timeout: API_CONFIG.timeout
});
}
async startEngine(vin, options = {}) {
try {
const payload = {
targetTemperature: options.targetTemperature || 22.0,
climateControl: options.climateControl !== false,
duration: options.duration || 1800
};
const response = await this.client.post(
`/vehicles/${vin}/engine/start`,
payload
);
console.log('Engine start initiated:', response.data);
return response.data;
} catch (error) {
console.error('Failed to start engine:', error.response?.data || error.message);
throw error;
}
}
async stopEngine(vin, options = {}) {
try {
const payload = {
force: options.force || false,
reason: options.reason || 'User stop',
preserveClimate: options.preserveClimate || false
};
const response = await this.client.post(
`/vehicles/${vin}/engine/stop`,
payload
);
console.log('Engine stop initiated:', response.data);
return response.data;
} catch (error) {
console.error('Failed to stop engine:', error.response?.data || error.message);
throw error;
}
}
async cancelCommand(vin, correlationId, options = {}) {
try {
const payload = {
correlationId,
reason: options.reason || 'User cancellation',
force: options.force || false
};
const response = await this.client.post(
`/vehicles/${vin}/engine/cancel`,
payload
);
console.log('Command cancelled:', response.data);
return response.data;
} catch (error) {
console.error('Failed to cancel command:', error.response?.data || error.message);
throw error;
}
}
}
export default EngineController;Test Basic Engine Control
Create test-basic.js:
import EngineController from './engineControl.js';
const controller = new EngineController();
async function testBasicControl() {
const vin = API_CONFIG.sampleVIN;
try {
// Test engine start
console.log('Testing engine start...');
const startResult = await controller.startEngine(vin, {
targetTemperature: 24.0,
climateControl: true,
duration: 1800
});
// Wait a bit then test engine stop
setTimeout(async () => {
console.log('Testing engine stop...');
await controller.stopEngine(vin, {
reason: 'Test completion'
});
}, 5000);
} catch (error) {
console.error('Test failed:', error.message);
}
}
testBasicControl();Run the test:
node test-basic.jsStep 2: Status Monitoring
Implement Status Checking
Add to engineControl.js:
async getEngineStatus(vin, includeDetails = true) {
try {
const response = await this.client.get(
`/vehicles/${vin}/engine/status`,
{ params: { includeDetails } }
);
console.log('Engine status:', response.data);
return response.data;
} catch (error) {
console.error('Failed to get engine status:', error.response?.data || error.message);
throw error;
}
}
async getEngineHistory(vin, options = {}) {
try {
const params = {
period: options.period || '24h',
limit: options.limit || 50,
offset: options.offset || 0,
action: options.action || undefined
};
const response = await this.client.get(
`/vehicles/${vin}/engine/history`,
{ params }
);
console.log('Engine history:', response.data);
return response.data;
} catch (error) {
console.error('Failed to get engine history:', error.response?.data || error.message);
throw error;
}
}Test Status Monitoring
Create test-status.js:
import EngineController from './engineControl.js';
const controller = new EngineController();
async function testStatusMonitoring() {
const vin = API_CONFIG.sampleVIN;
try {
// Get current status
console.log('Getting current engine status...');
const status = await controller.getEngineStatus(vin);
// Get history
console.log('Getting engine history...');
const history = await controller.getEngineHistory(vin, {
period: '7d',
limit: 10
});
console.log('Status monitoring test completed successfully');
} catch (error) {
console.error('Status monitoring test failed:', error.message);
}
}
testStatusMonitoring();Step 3: Real-time Updates with SSE
Implement SSE Client
Create sseClient.js:
export class SSEClient {
constructor(url, authToken) {
this.url = url;
this.authToken = authToken;
this.eventSource = null;
this.listeners = new Map();
}
connect() {
return new Promise((resolve, reject) => {
try {
this.eventSource = new EventSource(this.url, {
headers: {
'Authorization': `Bearer ${this.authToken}`
}
});
this.eventSource.onopen = () => {
console.log('SSE connection opened');
resolve();
};
this.eventSource.onerror = (error) => {
console.error('SSE connection error:', error);
if (this.eventSource.readyState === EventSource.CLOSED) {
reject(new Error('SSE connection closed'));
}
};
this.eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleEvent(data);
} catch (error) {
console.error('Failed to parse SSE data:', error);
}
};
} catch (error) {
reject(error);
}
});
}
on(eventType, callback) {
if (!this.listeners.has(eventType)) {
this.listeners.set(eventType, []);
}
this.listeners.get(eventType).push(callback);
}
handleEvent(data) {
const eventType = data.type || 'default';
const callbacks = this.listeners.get(eventType) || [];
callbacks.forEach(callback => {
try {
callback(data);
} catch (error) {
console.error('Error in event callback:', error);
}
});
}
disconnect() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
console.log('SSE connection closed');
}
}
}Test Real-time Updates
Create test-sse.js:
import { SSEClient } from './sseClient.js';
import { API_CONFIG } from './config.js';
async function testRealTimeUpdates() {
const vin = API_CONFIG.sampleVIN;
const sseUrl = `${API_CONFIG.baseURL}/vehicles/${vin}/engine/updates/stream`;
const sseClient = new SSEClient(sseUrl, API_CONFIG.authToken);
try {
await sseClient.connect();
// Listen for engine status updates
sseClient.on('ENGINE_STATUS_UPDATE', (data) => {
console.log('Engine status update:', data);
if (data.data.changeType === 'STARTED') {
console.log('🚗 Engine started successfully!');
} else if (data.data.changeType === 'STOPPED') {
console.log('🛑 Engine stopped');
}
});
// Listen for command status updates
sseClient.on('COMMAND_STATUS_UPDATE', (data) => {
console.log('Command status update:', data);
});
// Listen for system alerts
sseClient.on('SYSTEM_ALERT', (data) => {
console.log('System alert:', data);
});
console.log('Listening for real-time updates... Press Ctrl+C to stop');
// Keep the connection alive
process.on('SIGINT', () => {
console.log('\nDisconnecting...');
sseClient.disconnect();
process.exit(0);
});
} catch (error) {
console.error('Failed to connect to SSE:', error.message);
}
}
testRealTimeUpdates();Step 4: Settings Management
Implement Settings Control
Add to engineControl.js:
async getEngineConfig(vin) {
try {
const response = await this.client.get(`/vehicles/${vin}/engine/config`);
console.log('Engine config:', response.data);
return response.data;
} catch (error) {
console.error('Failed to get engine config:', error.response?.data || error.message);
throw error;
}
}
async updateEngineConfig(vin, config) {
try {
const payload = {
remoteStartEnabled: config.remoteStartEnabled !== false,
maxRunTime: config.maxRunTime || 1800,
autoStopEnabled: config.autoStopEnabled !== false,
securityRequired: config.securityRequired !== false,
climateControl: config.climateControl !== false,
defaultTemperature: config.defaultTemperature || 22.0,
lowFuelThreshold: config.lowFuelThreshold || 15.0,
highTemperatureThreshold: config.highTemperatureThreshold || 105.0
};
const response = await this.client.put(
`/vehicles/${vin}/engine/config`,
payload
);
console.log('Engine config updated:', response.data);
return response.data;
} catch (error) {
console.error('Failed to update engine config:', error.response?.data || error.message);
throw error;
}
}
async getEngineDiagnostics(vin) {
try {
const response = await this.client.get(`/vehicles/${vin}/engine/diagnostics`);
console.log('Engine diagnostics:', response.data);
return response.data;
} catch (error) {
console.error('Failed to get engine diagnostics:', error.response?.data || error.message);
throw error;
}
}Test Settings Management
Create test-settings.js:
import EngineController from './engineControl.js';
const controller = new EngineController();
async function testSettingsManagement() {
const vin = API_CONFIG.sampleVIN;
try {
// Get current settings
console.log('Getting current engine settings...');
const currentConfig = await controller.getEngineConfig(vin);
// Update settings
console.log('Updating engine settings...');
await controller.updateEngineConfig(vin, {
maxRunTime: 2400, // 40 minutes
defaultTemperature: 23.0,
lowFuelThreshold: 20.0
});
// Get diagnostics
console.log('Getting engine diagnostics...');
const diagnostics = await controller.getEngineDiagnostics(vin);
console.log('Settings management test completed successfully');
} catch (error) {
console.error('Settings management test failed:', error.message);
}
}
testSettingsManagement();Step 5: Error Handling and Safety
Implement Robust Error Handling
Create errorHandler.js:
export class EngineControlError extends Error {
constructor(message, code, details = null) {
super(message);
this.name = 'EngineControlError';
this.code = code;
this.details = details;
}
}
export class SafetyErrorHandler {
static handleEngineError(error) {
if (error.response) {
const status = error.response.status;
const data = error.response.data;
switch (status) {
case 400:
throw new EngineControlError(
'Invalid request parameters',
'INVALID_REQUEST',
data
);
case 401:
throw new EngineControlError(
'Authentication failed',
'AUTHENTICATION_ERROR',
data
);
case 403:
throw new EngineControlError(
'Permission denied for vehicle control',
'PERMISSION_DENIED',
data
);
case 409:
throw new EngineControlError(
'Engine control conflict - another command in progress',
'COMMAND_CONFLICT',
data
);
case 422:
throw new EngineControlError(
'Safety conditions not met for engine control',
'SAFETY_VIOLATION',
data
);
case 503:
throw new EngineControlError(
'Vehicle offline or unavailable',
'VEHICLE_OFFLINE',
data
);
default:
throw new EngineControlError(
'Engine control operation failed',
'UNKNOWN_ERROR',
data
);
}
} else if (error.code === 'ECONNABORTED') {
throw new EngineControlError(
'Request timeout - operation may still be processing',
'TIMEOUT',
null
);
} else {
throw new EngineControlError(
'Network error occurred',
'NETWORK_ERROR',
error.message
);
}
}
static getSafetyRecommendations(errorCode) {
const recommendations = {
'SAFETY_VIOLATION': [
'Ensure vehicle is parked with transmission in Park',
'Check that all doors are closed',
'Verify vehicle security system status',
'Make sure engine is not currently running'
],
'VEHICLE_OFFLINE': [
'Check vehicle network connectivity',
'Verify vehicle ignition status',
'Try again when vehicle has better signal',
'Contact support if issue persists'
],
'COMMAND_CONFLICT': [
'Wait for current command to complete',
'Cancel existing command before issuing new one',
'Check current engine status before retrying'
],
'TIMEOUT': [
'Command may still be processing',
'Check engine status after a few moments',
'Do not retry immediately to avoid conflicts'
]
};
return recommendations[errorCode] || ['Contact support for assistance'];
}
}Update Engine Controller with Error Handling
Modify engineControl.js to include error handling:
import { SafetyErrorHandler, EngineControlError } from './errorHandler.js';
// Add error handling to existing methods
async startEngine(vin, options = {}) {
try {
// ... existing code ...
} catch (error) {
throw SafetyErrorHandler.handleEngineError(error);
}
}
// Add safety check method
async performSafetyCheck(vin) {
try {
const status = await this.getEngineStatus(vin);
const safetyChecks = {
isVehicleSafe: !status.isRunning || status.isRemoteStarted,
isTransmissionInPark: status.transmissionGear === 'PARK',
areDoorsClosed: status.doorsLocked || !status.doorsOpen,
hasFuel: status.fuelLevel > 10.0,
isTemperatureSafe: status.engineTemperature < 105.0
};
const allSafe = Object.values(safetyChecks).every(check => check);
if (!allSafe) {
const failedChecks = Object.entries(safetyChecks)
.filter(([key, value]) => !value)
.map(([key]) => key);
throw new EngineControlError(
'Safety conditions not met',
'SAFETY_VIOLATION',
{ failedChecks, recommendations: SafetyErrorHandler.getSafetyRecommendations('SAFETY_VIOLATION') }
);
}
return safetyChecks;
} catch (error) {
throw SafetyErrorHandler.handleEngineError(error);
}
}Step 6: Complete Integration Example
Create Complete Application
Create app.js:
import EngineController from './engineControl.js';
import { SSEClient } from './sseClient.js';
import { SafetyErrorHandler } from './errorHandler.js';
import { API_CONFIG } from './config.js';
class RemoteEngineApp {
constructor() {
this.controller = new EngineController();
this.sseClient = null;
this.vin = API_CONFIG.sampleVIN;
}
async initialize() {
try {
console.log('🚗 Initializing Remote Engine Control App...');
// Connect to SSE for real-time updates
await this.connectSSE();
// Get initial status
await this.getStatus();
console.log('✅ App initialized successfully');
} catch (error) {
console.error('❌ Failed to initialize app:', error.message);
}
}
async connectSSE() {
const sseUrl = `${API_CONFIG.baseURL}/vehicles/${this.vin}/engine/updates/stream`;
this.sseClient = new SSEClient(sseUrl, API_CONFIG.authToken);
await this.sseClient.connect();
this.sseClient.on('ENGINE_STATUS_UPDATE', (data) => {
this.handleStatusUpdate(data);
});
this.sseClient.on('SYSTEM_ALERT', (data) => {
this.handleSystemAlert(data);
});
}
async startEngine(options = {}) {
try {
console.log('🔑 Starting engine...');
// Perform safety check first
await this.controller.performSafetyCheck(this.vin);
// Start engine
const result = await this.controller.startEngine(this.vin, options);
console.log('⏳ Engine start command sent:', result.commandId);
return result;
} catch (error) {
console.error('❌ Failed to start engine:', error.message);
if (error.details && error.details.recommendations) {
console.log('💡 Recommendations:');
error.details.recommendations.forEach(rec => console.log(` - ${rec}`));
}
throw error;
}
}
async stopEngine(options = {}) {
try {
console.log('🛑 Stopping engine...');
const result = await this.controller.stopEngine(this.vin, options);
console.log('⏳ Engine stop command sent:', result.commandId);
return result;
} catch (error) {
console.error('❌ Failed to stop engine:', error.message);
throw error;
}
}
async getStatus() {
try {
const status = await this.controller.getEngineStatus(this.vin);
console.log('📊 Current Engine Status:');
console.log(` Running: ${status.isRunning ? '✅' : '❌'}`);
console.log(` Remote Started: ${status.isRemoteStarted ? '✅' : '❌'}`);
console.log(` Remaining Time: ${status.remainingTimeSec}s`);
console.log(` Climate Active: ${status.climateActive ? '✅' : '❌'}`);
console.log(` Fuel Level: ${status.fuelLevel}%`);
console.log(` Engine Temp: ${status.engineTemperature}°C`);
return status;
} catch (error) {
console.error('❌ Failed to get status:', error.message);
throw error;
}
}
handleStatusUpdate(data) {
const changeType = data.data.changeType;
switch (changeType) {
case 'STARTED':
console.log('🚀 Engine started successfully!');
break;
case 'STOPPED':
console.log('🛑 Engine stopped');
break;
case 'TIMEOUT_WARNING':
console.log('⚠️ Engine will auto-stop soon');
break;
case 'LOW_FUEL_WARNING':
console.log('⛽ Low fuel warning');
break;
}
// Display updated status
console.log('📊 Updated Status:', {
running: data.data.isRunning,
remainingTime: data.data.remainingTimeSec,
temperature: data.data.currentTemperature
});
}
handleSystemAlert(data) {
console.log('🚨 System Alert:', data);
}
async shutdown() {
console.log('🔄 Shutting down app...');
if (this.sseClient) {
this.sseClient.disconnect();
}
console.log('✅ App shutdown complete');
}
}
// Example usage
async function main() {
const app = new RemoteEngineApp();
try {
await app.initialize();
// Example: Start engine with climate control
await app.startEngine({
targetTemperature: 24.0,
climateControl: true,
duration: 1800
});
// Wait and then stop
setTimeout(async () => {
await app.stopEngine({ reason: 'Demo completion' });
// Shutdown after a short delay
setTimeout(() => {
app.shutdown();
process.exit(0);
}, 2000);
}, 10000);
} catch (error) {
console.error('Application error:', error.message);
process.exit(1);
}
}
// Handle graceful shutdown
process.on('SIGINT', async () => {
console.log('\n🔄 Received SIGINT, shutting down gracefully...');
if (app) {
await app.shutdown();
}
process.exit(0);
});
main();Testing Your Implementation
Run the complete application:
node app.jsExpected output:
🚗 Initializing Remote Engine Control App...
SSE connection opened
📊 Current Engine Status:
Running: ❌
Remote Started: ❌
Remaining Time: 0s
Climate Active: ❌
Fuel Level: 65.5%
Engine Temp: 45.2°C
✅ App initialized successfully
🔑 Starting engine...
⏳ Engine start command sent: cmd-uuid-12345
🚀 Engine started successfully!
📊 Updated Status: { running: true, remainingTime: 1800, temperature: 18.5 }
🛑 Stopping engine...
⏳ Engine stop command sent: cmd-uuid-67890
🛑 Engine stopped
🔄 Shutting down app...
SSE connection closed
✅ App shutdown completeChallenge Exercises
Enhanced Safety Features: Implement additional safety checks like parking brake status and hood position.
Command Queue Management: Create a queue system to handle multiple engine control requests safely.
Analytics Dashboard: Build a simple dashboard showing engine usage statistics and patterns.
Mobile Integration: Adapt the code for a mobile app environment with offline support.
Multi-Vehicle Support: Extend the application to handle multiple vehicles simultaneously.
Summary
In this codelab, you learned how to:
- ✅ Implement remote engine start/stop functionality
- ✅ Monitor real-time engine status via SSE
- ✅ Handle safety requirements and error scenarios
- ✅ Manage engine settings and diagnostics
- ✅ Build a complete, production-ready application
The Remote Engine Control API provides a secure and reliable way to control vehicle engines remotely while maintaining strict safety requirements. The implementation patterns you learned here can be applied to other vehicle control APIs as well.