Skip to content

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

bash
mkdir remote-engine-control-demo
cd remote-engine-control-demo
npm init -y
npm install axios node-fetch

2. Configuration

Create a config.js file:

javascript
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:

javascript
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:

javascript
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:

bash
node test-basic.js

Step 2: Status Monitoring

Implement Status Checking

Add to engineControl.js:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

bash
node app.js

Expected 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 complete

Challenge Exercises

  1. Enhanced Safety Features: Implement additional safety checks like parking brake status and hood position.

  2. Command Queue Management: Create a queue system to handle multiple engine control requests safely.

  3. Analytics Dashboard: Build a simple dashboard showing engine usage statistics and patterns.

  4. Mobile Integration: Adapt the code for a mobile app environment with offline support.

  5. 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.

Additional Resources

Released under the MIT License.