Skip to content

Manual Diagnosis API Codelab

Overview

This codelab guides you through implementing the Manual Diagnosis API, which enables remote vehicle diagnostics through expert technician control. You'll learn how to manage diagnosis sessions, handle real-time data streaming, and process diagnostic results.

Prerequisites

  • Basic understanding of REST APIs
  • Familiarity with JavaScript/TypeScript
  • Node.js and npm installed
  • Understanding of vehicle diagnostics and ECU communication
  • Text editor or IDE

Learning Objectives

  • Implement remote diagnosis session management
  • Handle real-time diagnostic data streaming via SSE
  • Manage user consent and technician control
  • Process and analyze diagnostic results
  • Generate comprehensive diagnosis reports

Setup

1. Project Initialization

bash
mkdir manual-diagnosis-demo
cd manual-diagnosis-demo
npm init -y
npm install axios node-fetch moment

2. Configuration

Create a config.js file:

javascript
export const API_CONFIG = {
  baseURL: 'https://api.ecarus.run/api/v1/diagnosis',
  authToken: 'sk_4f9c7b8e2d1a6c0f3e7a9b5d8c1e4f2a7c6d9e0b3f5a8c1d4e7f9b2c6a1e3d',
  sampleVIN: 'KMHSH81C7LU123456',
  sampleTechnicianId: 'TECH_001',
  timeout: 30000 // 30 seconds for diagnosis operations
};

Step 1: Diagnosis Session Management

Implement Session Control

Create diagnosisSession.js:

javascript
import axios from 'axios';
import { API_CONFIG } from './config.js';

class DiagnosisSession {
  constructor() {
    this.client = axios.create({
      baseURL: API_CONFIG.baseURL,
      headers: {
        'Authorization': `Bearer ${API_CONFIG.authToken}`,
        'Content-Type': 'application/json'
      },
      timeout: API_CONFIG.timeout
    });
  }

  async requestSession(vin, options = {}) {
    try {
      const payload = {
        reason: options.reason || 'Routine Check',
        technicianId: options.technicianId || API_CONFIG.sampleTechnicianId
      };

      const response = await this.client.post(
        `/vehicles/${vin}/manual/session-request`,
        payload
      );

      console.log('Diagnosis session requested:', response.data);
      return response.data;
    } catch (error) {
      console.error('Failed to request diagnosis session:', error.response?.data || error.message);
      throw error;
    }
  }

  async stopSession(vin, sessionId, options = {}) {
    try {
      const payload = {
        sessionId,
        reason: options.reason || 'DIAGNOSIS_COMPLETED'
      };

      const response = await this.client.post(
        `/vehicles/${vin}/manual/session-stop`,
        payload
      );

      console.log('Diagnosis session stopped:', response.data);
      return response.data;
    } catch (error) {
      console.error('Failed to stop diagnosis session:', error.response?.data || error.message);
      throw error;
    }
  }

  async getSessionStatus(vin, sessionId) {
    try {
      const response = await this.client.get(
        `/vehicles/${vin}/manual/session/${sessionId}/status`
      );

      console.log('Session status:', response.data);
      return response.data;
    } catch (error) {
      console.error('Failed to get session status:', error.response?.data || error.message);
      throw error;
    }
  }

  async getActiveSessions(vin) {
    try {
      const response = await this.client.get(
        `/vehicles/${vin}/manual/sessions`
      );

      console.log('Active sessions:', response.data);
      return response.data;
    } catch (error) {
      console.error('Failed to get active sessions:', error.response?.data || error.message);
      throw error;
    }
  }

  formatSessionDuration(startTime, endTime = null) {
    const start = new Date(startTime);
    const end = endTime ? new Date(endTime) : new Date();
    const duration = Math.floor((end - start) / 1000);
    
    const hours = Math.floor(duration / 3600);
    const minutes = Math.floor((duration % 3600) / 60);
    const seconds = duration % 60;
    
    return { hours, minutes, seconds, totalSeconds: duration };
  }

  assessSessionStatus(sessionData) {
    const status = sessionData.status;
    const consent = sessionData.consent;
    
    return {
      isActive: status === 'ACTIVE',
      needsConsent: consent.status === 'PENDING',
      isReady: status === 'ACTIVE' && consent.status === 'GRANTED',
      canExecuteCommands: status === 'ACTIVE' && consent.status === 'GRANTED',
      currentCommand: sessionData.currentCommand || null
    };
  }
}

export default DiagnosisSession;

Test Session Management

Create test-session.js:

javascript
import DiagnosisSession from './diagnosisSession.js';
import { API_CONFIG } from './config.js';

const sessionManager = new DiagnosisSession();

async function testSessionManagement() {
  const vin = API_CONFIG.sampleVIN;

  try {
    console.log('🔧 Testing Diagnosis Session Management...');
    
    // Request a new session
    console.log('Requesting new diagnosis session...');
    const sessionRequest = await sessionManager.requestSession(vin, {
      reason: 'Engine Performance Check',
      technicianId: API_CONFIG.sampleTechnicianId
    });
    
    // Get active sessions
    console.log('Getting active sessions...');
    const activeSessions = await sessionManager.getActiveSessions(vin);
    
    if (activeSessions.sessions && activeSessions.sessions.length > 0) {
      const sessionId = activeSessions.sessions[0].sessionId;
      
      // Get session status
      console.log('Getting session status...');
      const sessionStatus = await sessionManager.getSessionStatus(vin, sessionId);
      
      const assessment = sessionManager.assessSessionStatus(sessionStatus);
      console.log('Session Assessment:', assessment);
      
      // Stop the session (for demo)
      setTimeout(async () => {
        console.log('Stopping session...');
        await sessionManager.stopSession(vin, sessionId, {
          reason: 'DEMO_COMPLETED'
        });
      }, 5000);
    }
    
  } catch (error) {
    console.error('❌ Session management test failed:', error.message);
  }
}

testSessionManagement();

Create consentManager.js:

javascript
import axios from 'axios';
import { API_CONFIG } from './config.js';

class ConsentManager {
  constructor() {
    this.client = axios.create({
      baseURL: API_CONFIG.baseURL,
      headers: {
        'Authorization': `Bearer ${API_CONFIG.authToken}`,
        'Content-Type': 'application/json'
      },
      timeout: API_CONFIG.timeout
    });
  }

  async provideConsent(vin, consentData) {
    try {
      const payload = {
        allow: consentData.allow !== false,
        requestId: consentData.requestId,
        timestamp: consentData.timestamp || new Date().toISOString()
      };

      const response = await this.client.post(
        `/vehicles/${vin}/manual/consent`,
        payload
      );

      console.log('Consent provided:', response.data);
      return response.data;
    } catch (error) {
      console.error('Failed to provide consent:', error.response?.data || error.message);
      throw error;
    }
  }

  async checkConsentStatus(vin, requestId) {
    try {
      // This would typically be part of session status check
      const response = await this.client.get(
        `/vehicles/${vin}/manual/consent/${requestId}`
      );

      console.log('Consent status:', response.data);
      return response.data;
    } catch (error) {
      console.error('Failed to check consent status:', error.response?.data || error.message);
      throw error;
    }
  }

  validateConsentRequest(consentData) {
    const errors = [];
    
    if (!consentData.requestId) {
      errors.push('Request ID is required');
    }
    
    if (typeof consentData.allow !== 'boolean') {
      errors.push('Consent decision (allow) must be boolean');
    }
    
    if (consentData.allow && !consentData.purpose) {
      errors.push('Purpose is required when consent is granted');
    }
    
    return {
      isValid: errors.length === 0,
      errors
    };
  }

  generateConsentMessage(sessionData) {
    return {
      title: 'Remote Diagnosis Request',
      message: `Technician ${sessionData.technicianId} is requesting to perform remote diagnosis on your vehicle.`,
      purpose: sessionData.reason,
      duration: 'Estimated 15-30 minutes',
      dataCollected: [
        'Engine performance data',
        'Diagnostic trouble codes (DTC)',
        'Sensor readings',
        'System status information'
      ],
      timestamp: new Date().toISOString()
    };
  }

  async processConsentFlow(vin, sessionId, consentDecision) {
    try {
      console.log('🔄 Processing consent flow...');
      
      // Validate consent decision
      const validation = this.validateConsentRequest(consentDecision);
      if (!validation.isValid) {
        throw new Error(`Invalid consent request: ${validation.errors.join(', ')}`);
      }
      
      // Provide consent
      const consentResult = await this.provideConsent(vin, consentDecision);
      
      // Check session status after consent
      // This would typically be done via SSE or polling
      console.log('✅ Consent processed successfully');
      
      return {
        consented: consentDecision.allow,
        sessionId,
        timestamp: consentResult.timestamp
      };
      
    } catch (error) {
      console.error('Failed to process consent flow:', error.message);
      throw error;
    }
  }
}

export default ConsentManager;

Create test-consent.js:

javascript
import ConsentManager from './consentManager.js';
import DiagnosisSession from './diagnosisSession.js';
import { API_CONFIG } from './config.js';

const consentManager = new ConsentManager();
const sessionManager = new DiagnosisSession();

async function testConsentManagement() {
  const vin = API_CONFIG.sampleVIN;

  try {
    console.log('🔐 Testing Consent Management...');
    
    // Request a session first
    const sessionRequest = await sessionManager.requestSession(vin, {
      reason: 'Engine Diagnostic Check',
      technicianId: API_CONFIG.sampleTechnicianId
    });
    
    // Generate consent message
    const consentMessage = consentManager.generateConsentMessage(sessionRequest);
    console.log('📱 Consent Message:', consentMessage);
    
    // Simulate user granting consent
    const consentDecision = {
      allow: true,
      requestId: sessionRequest.requestId || 'REQ_001',
      purpose: 'Engine Diagnostic Check'
    };
    
    // Process consent
    const consentResult = await consentManager.processConsentFlow(vin, sessionRequest.sessionId, consentDecision);
    console.log('✅ Consent Result:', consentResult);
    
  } catch (error) {
    console.error('❌ Consent management test failed:', error.message);
  }
}

testConsentManagement();

Step 3: Diagnostic Command Control

Implement Command Execution

Create diagnosisCommand.js:

javascript
import axios from 'axios';
import { API_CONFIG } from './config.js';

class DiagnosisCommand {
  constructor() {
    this.client = axios.create({
      baseURL: API_CONFIG.baseURL,
      headers: {
        'Authorization': `Bearer ${API_CONFIG.authToken}`,
        'Content-Type': 'application/json'
      },
      timeout: API_CONFIG.timeout
    });
  }

  async sendCommand(vin, commandData) {
    try {
      const payload = {
        cmd: commandData.cmd,
        pids: commandData.pids || [],
        sessionId: commandData.sessionId
      };

      const response = await this.client.post(
        `/vehicles/${vin}/manual/command`,
        payload
      );

      console.log('Command sent:', response.data);
      return response.data;
    } catch (error) {
      console.error('Failed to send command:', error.response?.data || error.message);
      throw error;
    }
  }

  async startLiveDataStream(vin, sessionId, pids = []) {
    try {
      const defaultPids = ['RPM', 'COOLANT_TEMP', 'SPEED', 'FUEL_PRESSURE'];
      const selectedPids = pids.length > 0 ? pids : defaultPids;

      const command = {
        cmd: 'CMD_LIVE_STREAM',
        pids: selectedPids,
        sessionId
      };

      return await this.sendCommand(vin, command);
    } catch (error) {
      console.error('Failed to start live data stream:', error.message);
      throw error;
    }
  }

  async readDTC(vin, sessionId) {
    try {
      const command = {
        cmd: 'CMD_READ_DTC',
        sessionId
      };

      return await this.sendCommand(vin, command);
    } catch (error) {
      console.error('Failed to read DTC codes:', error.message);
      throw error;
    }
  }

  async clearDTC(vin, sessionId) {
    try {
      const command = {
        cmd: 'CMD_CLEAR_DTC',
        sessionId
      };

      return await this.sendCommand(vin, command);
    } catch (error) {
      console.error('Failed to clear DTC codes:', error.message);
      throw error;
    }
  }

  validateCommand(commandData) {
    const validCommands = [
      'CMD_LIVE_STREAM',
      'CMD_READ_DTC',
      'CMD_CLEAR_DTC',
      'CMD_RUN_TEST',
      'CMD_GET_VIN',
      'CMD_GET_CALIBRATION'
    ];

    const validPIDs = [
      'RPM', 'COOLANT_TEMP', 'SPEED', 'FUEL_PRESSURE',
      'THROTTLE_POS', 'O2_SENSOR', 'MAF', 'MAP',
      'IGNITION_TIMING', 'FUEL_INJECTOR', 'EVAP_SYSTEM'
    ];

    const errors = [];

    if (!validCommands.includes(commandData.cmd)) {
      errors.push(`Invalid command: ${commandData.cmd}`);
    }

    if (commandData.cmd === 'CMD_LIVE_STREAM' && (!commandData.pids || commandData.pids.length === 0)) {
      errors.push('PIDs are required for live stream command');
    }

    if (commandData.pids) {
      const invalidPids = commandData.pids.filter(pid => !validPIDs.includes(pid));
      if (invalidPids.length > 0) {
        errors.push(`Invalid PIDs: ${invalidPids.join(', ')}`);
      }
    }

    return {
      isValid: errors.length === 0,
      errors
    };
  }

  getCommandDescription(command) {
    const descriptions = {
      'CMD_LIVE_STREAM': 'Start real-time data streaming from vehicle sensors',
      'CMD_READ_DTC': 'Read diagnostic trouble codes from vehicle ECUs',
      'CMD_CLEAR_DTC': 'Clear diagnostic trouble codes (requires confirmation)',
      'CMD_RUN_TEST': 'Run specific diagnostic tests on vehicle systems',
      'CMD_GET_VIN': 'Retrieve vehicle identification number',
      'CMD_GET_CALIBRATION': 'Get ECU calibration information'
    };

    return descriptions[command] || 'Unknown command';
  }

  formatPIDData(pid, value) {
    const units = {
      'RPM': 'RPM',
      'COOLANT_TEMP': '°C',
      'SPEED': 'km/h',
      'FUEL_PRESSURE': 'kPa',
      'THROTTLE_POS': '%',
      'O2_SENSOR': 'V',
      'MAF': 'g/s',
      'MAP': 'kPa',
      'IGNITION_TIMING': '°',
      'FUEL_INJECTOR': 'ms',
      'EVAP_SYSTEM': 'status'
    };

    return {
      pid,
      value,
      unit: units[pid] || '',
      formatted: `${value} ${units[pid] || ''}`
    };
  }
}

export default DiagnosisCommand;

Test Command Control

Create test-command.js:

javascript
import DiagnosisCommand from './diagnosisCommand.js';
import DiagnosisSession from './diagnosisSession.js';
import { API_CONFIG } from './config.js';

const commandManager = new DiagnosisCommand();
const sessionManager = new DiagnosisSession();

async function testCommandControl() {
  const vin = API_CONFIG.sampleVIN;

  try {
    console.log('⚙️ Testing Diagnosis Command Control...');
    
    // Request a session
    const sessionRequest = await sessionManager.requestSession(vin, {
      reason: 'Command Testing',
      technicianId: API_CONFIG.sampleTechnicianId
    });
    
    const sessionId = sessionRequest.sessionId;
    
    // Test live data stream command
    console.log('Starting live data stream...');
    const streamCommand = await commandManager.startLiveDataStream(vin, sessionId, [
      'RPM', 'COOLANT_TEMP', 'SPEED'
    ]);
    
    console.log('Stream command sent:', streamCommand);
    
    // Test DTC read command
    console.log('Reading DTC codes...');
    const dtcCommand = await commandManager.readDTC(vin, sessionId);
    console.log('DTC command sent:', dtcCommand);
    
    // Validate commands
    const testCommand = {
      cmd: 'CMD_LIVE_STREAM',
      pids: ['RPM', 'INVALID_PID']
    };
    
    const validation = commandManager.validateCommand(testCommand);
    console.log('Command validation:', validation);
    
    // Clean up
    setTimeout(async () => {
      await sessionManager.stopSession(vin, sessionId, {
        reason: 'TEST_COMPLETED'
      });
    }, 3000);
    
  } catch (error) {
    console.error('❌ Command control test failed:', error.message);
  }
}

testCommandControl();

Step 4: Real-time Data Streaming

Implement SSE Client for Diagnosis Data

Create diagnosisSSE.js:

javascript
export class DiagnosisSSEClient {
  constructor(url, authToken) {
    this.url = url;
    this.authToken = authToken;
    this.eventSource = null;
    this.listeners = new Map();
    this.dataBuffer = new Map();
    this.isStreaming = false;
  }

  connect() {
    return new Promise((resolve, reject) => {
      try {
        this.eventSource = new EventSource(this.url, {
          headers: {
            'Authorization': `Bearer ${this.authToken}`
          }
        });

        this.eventSource.onopen = () => {
          console.log('📡 Diagnosis SSE connection opened');
          this.isStreaming = true;
          resolve();
        };

        this.eventSource.onerror = (error) => {
          console.error('Diagnosis SSE connection error:', error);
          this.isStreaming = false;
          if (this.eventSource.readyState === EventSource.CLOSED) {
            reject(new Error('Diagnosis SSE connection closed'));
          }
        };

        this.eventSource.onmessage = (event) => {
          try {
            const data = JSON.parse(event.data);
            this.handleEvent(data);
          } catch (error) {
            console.error('Failed to parse diagnosis 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.eventType || 'default';
    const callbacks = this.listeners.get(eventType) || [];
    
    callbacks.forEach(callback => {
      try {
        callback(data);
      } catch (error) {
        console.error('Error in diagnosis event callback:', error);
      }
    });

    // Handle specific data events
    if (eventType === 'LIVE_DATA') {
      this.handleLiveData(data);
    } else if (eventType === 'DTC_RESULT') {
      this.handleDTCResult(data);
    } else if (eventType === 'COMMAND_STATUS') {
      this.handleCommandStatus(data);
    }
  }

  handleLiveData(data) {
    const { sessionId, timestamp, data: liveData } = data;
    
    // Store in buffer for analysis
    if (!this.dataBuffer.has(sessionId)) {
      this.dataBuffer.set(sessionId, []);
    }
    
    const buffer = this.dataBuffer.get(sessionId);
    buffer.push({
      timestamp,
      data: liveData,
      formatted: this.formatLiveData(liveData)
    });
    
    // Keep only last 100 data points
    if (buffer.length > 100) {
      buffer.shift();
    }
    
    console.log('📊 Live Data Received:', {
      sessionId,
      timestamp,
      rpm: liveData.rpm,
      coolantTemp: liveData.coolantTemp,
      speed: liveData.speed
    });
  }

  handleDTCResult(data) {
    console.log('🔧 DTC Result Received:', data);
    
    const { dtcCodes, sessionId } = data;
    
    if (dtcCodes && dtcCodes.length > 0) {
      console.log(`Found ${dtcCodes.length} DTC codes:`);
      dtcCodes.forEach((code, index) => {
        console.log(`  ${index + 1}. ${code.code}: ${code.description} (${code.severity})`);
      });
    } else {
      console.log('✅ No DTC codes found');
    }
  }

  handleCommandStatus(data) {
    console.log('⚙️ Command Status Update:', data);
    
    const { command, status, sessionId } = data;
    
    if (status === 'COMPLETED') {
      console.log(`✅ Command ${command} completed successfully`);
    } else if (status === 'FAILED') {
      console.log(`❌ Command ${command} failed`);
    } else if (status === 'EXECUTING') {
      console.log(`⏳ Command ${command} is executing...`);
    }
  }

  formatLiveData(rawData) {
    const formatted = {};
    
    Object.entries(rawData).forEach(([key, value]) => {
      switch (key) {
        case 'rpm':
          formatted[key] = { value, unit: 'RPM', display: `${value} RPM` };
          break;
        case 'coolantTemp':
          formatted[key] = { value, unit: '°C', display: `${value}°C` };
          break;
        case 'speed':
          formatted[key] = { value, unit: 'km/h', display: `${value} km/h` };
          break;
        case 'fuelPressure':
          formatted[key] = { value, unit: 'kPa', display: `${value} kPa` };
          break;
        case 'throttlePosition':
          formatted[key] = { value, unit: '%', display: `${value}%` };
          break;
        default:
          formatted[key] = { value, unit: '', display: `${value}` };
      }
    });
    
    return formatted;
  }

  getDataBuffer(sessionId) {
    return this.dataBuffer.get(sessionId) || [];
  }

  clearDataBuffer(sessionId) {
    this.dataBuffer.delete(sessionId);
  }

  analyzeDataTrends(sessionId) {
    const buffer = this.getDataBuffer(sessionId);
    if (buffer.length < 10) {
      return null; // Not enough data for analysis
    }
    
    const analysis = {
      rpm: this.analyzeTrend(buffer.map(d => d.data.rpm)),
      coolantTemp: this.analyzeTrend(buffer.map(d => d.data.coolantTemp)),
      speed: this.analyzeTrend(buffer.map(d => d.data.speed))
    };
    
    return analysis;
  }

  analyzeTrend(values) {
    if (values.length < 2) return null;
    
    const validValues = values.filter(v => v !== null && v !== undefined);
    if (validValues.length < 2) return null;
    
    const avg = validValues.reduce((sum, val) => sum + val, 0) / validValues.length;
    const max = Math.max(...validValues);
    const min = Math.min(...validValues);
    
    // Simple trend calculation (last vs first)
    const trend = validValues[validValues.length - 1] > validValues[0] ? 'increasing' : 
                  validValues[validValues.length - 1] < validValues[0] ? 'decreasing' : 'stable';
    
    return { avg, max, min, trend, samples: validValues.length };
  }

  disconnect() {
    if (this.eventSource) {
      this.eventSource.close();
      this.eventSource = null;
      this.isStreaming = false;
      console.log('Diagnosis SSE connection closed');
    }
  }
}

Test Real-time Data Streaming

Create test-streaming.js:

javascript
import { DiagnosisSSEClient } from './diagnosisSSE.js';
import DiagnosisCommand from './diagnosisCommand.js';
import DiagnosisSession from './diagnosisSession.js';
import { API_CONFIG } from './config.js';

const sseClient = new DiagnosisSSEClient(
  `${API_CONFIG.baseURL}/vehicles/${API_CONFIG.sampleVIN}/manual/live-data`,
  API_CONFIG.authToken
);

const commandManager = new DiagnosisCommand();
const sessionManager = new DiagnosisSession();

async function testRealTimeStreaming() {
  const vin = API_CONFIG.sampleVIN;

  try {
    console.log('📡 Testing Real-time Data Streaming...');
    
    // Connect to SSE
    await sseClient.connect();
    
    // Set up event listeners
    sseClient.on('LIVE_DATA', (data) => {
      console.log('📊 Live Data:', data.data);
    });
    
    sseClient.on('DTC_RESULT', (data) => {
      console.log('🔧 DTC Result:', data);
    });
    
    sseClient.on('COMMAND_STATUS', (data) => {
      console.log('⚙️ Command Status:', data);
    });
    
    // Start a session and commands
    const sessionRequest = await sessionManager.requestSession(vin, {
      reason: 'Real-time Streaming Test',
      technicianId: API_CONFIG.sampleTechnicianId
    });
    
    const sessionId = sessionRequest.sessionId;
    
    // Start live data stream
    await commandManager.startLiveDataStream(vin, sessionId, [
      'RPM', 'COOLANT_TEMP', 'SPEED', 'FUEL_PRESSURE'
    ]);
    
    // Read DTC codes
    await commandManager.readDTC(vin, sessionId);
    
    console.log('👂 Listening for real-time data... Press Ctrl+C to stop');
    
    // Analyze trends periodically
    setInterval(() => {
      const trends = sseClient.analyzeDataTrends(sessionId);
      if (trends) {
        console.log('📈 Data Trends:', trends);
      }
    }, 10000); // Every 10 seconds
    
    // Handle graceful shutdown
    process.on('SIGINT', async () => {
      console.log('\n🔄 Shutting down...');
      await sessionManager.stopSession(vin, sessionId, {
        reason: 'TEST_COMPLETED'
      });
      sseClient.disconnect();
      process.exit(0);
    });
    
  } catch (error) {
    console.error('❌ Real-time streaming test failed:', error.message);
  }
}

testRealTimeStreaming();

Step 5: Report Generation

Implement Diagnosis Reporting

Create diagnosisReport.js:

javascript
import axios from 'axios';
import { API_CONFIG } from './config.js';

class DiagnosisReport {
  constructor() {
    this.client = axios.create({
      baseURL: API_CONFIG.baseURL,
      headers: {
        'Authorization': `Bearer ${API_CONFIG.authToken}`,
        'Content-Type': 'application/json'
      },
      timeout: API_CONFIG.timeout
    });
  }

  async generateReport(vin, sessionId) {
    try {
      const response = await this.client.get(
        `/vehicles/${vin}/manual/session/${sessionId}/report`
      );

      console.log('Diagnosis report generated:', response.data);
      return response.data;
    } catch (error) {
      console.error('Failed to generate diagnosis report:', error.response?.data || error.message);
      throw error;
    }
  }

  async getDiagnosisHistory(vin, options = {}) {
    try {
      const params = {
        limit: options.limit || 50,
        offset: options.offset || 0,
        startDate: options.startDate,
        endDate: options.endDate
      };

      const response = await this.client.get(
        `/vehicles/${vin}/manual/history`,
        { params }
      );

      console.log('Diagnosis history:', response.data);
      return response.data;
    } catch (error) {
      console.error('Failed to get diagnosis history:', error.response?.data || error.message);
      throw error;
    }
  }

  formatReport(reportData) {
    const { summary, dtc, liveData, recommendations } = reportData;
    
    return {
      reportId: reportData.reportId,
      timestamp: reportData.timestamp,
      overallStatus: summary.overallStatus,
      summary: {
        status: summary.overallStatus,
        dtcCount: summary.dtcCount,
        criticalIssues: summary.criticalIssues,
        hasRecommendations: summary.recommendations && summary.recommendations.length > 0
      },
      issues: dtc.map(code => ({
        code: code.code,
        description: code.description,
        severity: code.severity,
        occurrences: code.occurrenceCount
      })),
      performance: {
        avgRpm: liveData.averageRpm,
        maxTemp: liveData.maxCoolantTemp,
        dataPoints: liveData.dataPoints
      },
      recommendations: recommendations.map(rec => ({
        priority: rec.priority,
        action: rec.action,
        description: rec.description
      }))
    };
  }

  assessReportSeverity(reportData) {
    const { summary, dtc } = reportData;
    
    let severity = 'NORMAL';
    let priority = 'LOW';
    
    if (summary.criticalIssues > 0) {
      severity = 'CRITICAL';
      priority = 'HIGH';
    } else if (dtc.some(code => code.severity === 'HIGH')) {
      severity = 'WARNING';
      priority = 'MEDIUM';
    } else if (summary.dtcCount > 0) {
      severity = 'INFO';
      priority = 'LOW';
    }
    
    return { severity, priority };
  }

  generateActionItems(reportData) {
    const { dtc, recommendations } = reportData;
    const actionItems = [];
    
    // Add DTC-based actions
    dtc.forEach(code => {
      if (code.severity === 'HIGH' || code.severity === 'CRITICAL') {
        actionItems.push({
          type: 'IMMEDIATE',
          description: `Address critical DTC: ${code.code}`,
          details: code.description
        });
      }
    });
    
    // Add recommendation-based actions
    recommendations.forEach(rec => {
      actionItems.push({
        type: rec.priority,
        description: rec.action,
        details: rec.description
      });
    });
    
    // Sort by priority
    const priorityOrder = { 'HIGH': 0, 'MEDIUM': 1, 'LOW': 2, 'IMMEDIATE': -1 };
    actionItems.sort((a, b) => priorityOrder[a.type] - priorityOrder[b.type]);
    
    return actionItems;
  }

  exportReport(reportData, format = 'json') {
    switch (format.toLowerCase()) {
      case 'json':
        return JSON.stringify(reportData, null, 2);
      
      case 'text':
        return this.generateTextReport(reportData);
      
      case 'csv':
        return this.generateCSVReport(reportData);
      
      default:
        throw new Error(`Unsupported export format: ${format}`);
    }
  }

  generateTextReport(reportData) {
    const { summary, dtc, liveData, recommendations } = reportData;
    
    let report = `DIAGNOSIS REPORT\n`;
    report += `================\n\n`;
    report += `Report ID: ${reportData.reportId}\n`;
    report += `Timestamp: ${reportData.timestamp}\n`;
    report += `Vehicle: ${reportData.vin}\n\n`;
    
    report += `SUMMARY\n`;
    report += `-------\n`;
    report += `Overall Status: ${summary.overallStatus}\n`;
    report += `DTC Count: ${summary.dtcCount}\n`;
    report += `Critical Issues: ${summary.criticalIssues}\n\n`;
    
    if (dtc.length > 0) {
      report += `DIAGNOSTIC TROUBLE CODES\n`;
      report += `-----------------------\n`;
      dtc.forEach((code, index) => {
        report += `${index + 1}. ${code.code} (${code.severity})\n`;
        report += `   ${code.description}\n`;
        report += `   Occurrences: ${code.occurrenceCount}\n\n`;
      });
    }
    
    if (recommendations.length > 0) {
      report += `RECOMMENDATIONS\n`;
      report += `---------------\n`;
      recommendations.forEach((rec, index) => {
        report += `${index + 1}. [${rec.priority}] ${rec.action}\n`;
        report += `   ${rec.description}\n\n`;
      });
    }
    
    return report;
  }

  generateCSVReport(reportData) {
    const { dtc } = reportData;
    
    let csv = 'Code,Description,Severity,Occurrences\n';
    dtc.forEach(code => {
      csv += `"${code.code}","${code.description}","${code.severity}",${code.occurrenceCount}\n`;
    });
    
    return csv;
  }
}

export default DiagnosisReport;

Test Report Generation

Create test-report.js:

javascript
import DiagnosisReport from './diagnosisReport.js';
import DiagnosisSession from './diagnosisSession.js';
import { API_CONFIG } from './config.js';

const reportManager = new DiagnosisReport();
const sessionManager = new DiagnosisSession();

async function testReportGeneration() {
  const vin = API_CONFIG.sampleVIN;

  try {
    console.log('📄 Testing Report Generation...');
    
    // Get diagnosis history
    const history = await reportManager.getDiagnosisHistory(vin, { limit: 5 });
    console.log(`Found ${history.sessions.length} past diagnosis sessions`);
    
    // If there are past sessions, generate a report for the most recent one
    if (history.sessions && history.sessions.length > 0) {
      const latestSession = history.sessions[0];
      
      console.log('Generating report for session:', latestSession.sessionId);
      const report = await reportManager.generateReport(vin, latestSession.sessionId);
      
      // Format and assess the report
      const formattedReport = reportManager.formatReport(report);
      const severity = reportManager.assessReportSeverity(report);
      const actionItems = reportManager.generateActionItems(report);
      
      console.log('📊 Formatted Report:', formattedReport);
      console.log('🚨 Severity Assessment:', severity);
      console.log('📋 Action Items:', actionItems);
      
      // Export in different formats
      console.log('\n📄 Text Report:');
      console.log(reportManager.exportReport(report, 'text'));
      
      console.log('\n📊 CSV Report:');
      console.log(reportManager.exportReport(report, 'csv'));
    } else {
      console.log('No past diagnosis sessions found');
    }
    
  } catch (error) {
    console.error('❌ Report generation test failed:', error.message);
  }
}

testReportGeneration();

Step 6: Complete Diagnosis Application

Create Complete Diagnosis System

Create diagnosisApp.js:

javascript
import DiagnosisSession from './diagnosisSession.js';
import ConsentManager from './consentManager.js';
import DiagnosisCommand from './diagnosisCommand.js';
import { DiagnosisSSEClient } from './diagnosisSSE.js';
import DiagnosisReport from './diagnosisReport.js';
import { API_CONFIG } from './config.js';

class DiagnosisApplication {
  constructor() {
    this.sessionManager = new DiagnosisSession();
    this.consentManager = new ConsentManager();
    this.commandManager = new DiagnosisCommand();
    this.reportManager = new DiagnosisReport();
    this.sseClient = null;
    this.currentSession = null;
    this.isStreaming = false;
  }

  async initialize() {
    try {
      console.log('🔧 Initializing Diagnosis Application...');
      
      // Connect to real-time events
      await this.connectRealTimeEvents();
      
      console.log('✅ Diagnosis Application initialized successfully');
    } catch (error) {
      console.error('❌ Failed to initialize Diagnosis Application:', error.message);
    }
  }

  async connectRealTimeEvents() {
    console.log('📡 Connecting to real-time diagnosis events...');
    
    const sseUrl = `${API_CONFIG.baseURL}/vehicles/${API_CONFIG.sampleVIN}/manual/live-data`;
    this.sseClient = new DiagnosisSSEClient(sseUrl, API_CONFIG.authToken);
    
    await this.sseClient.connect();
    
    this.sseClient.on('LIVE_DATA', (data) => {
      this.handleLiveData(data);
    });
    
    this.sseClient.on('DTC_RESULT', (data) => {
      this.handleDTCResult(data);
    });
    
    this.sseClient.on('COMMAND_STATUS', (data) => {
      this.handleCommandStatus(data);
    });
  }

  async startDiagnosisSession(options = {}) {
    try {
      console.log('🚀 Starting diagnosis session...');
      
      // Request session
      const sessionRequest = await this.sessionManager.requestSession(
        API_CONFIG.sampleVIN,
        {
          reason: options.reason || 'Comprehensive Diagnosis',
          technicianId: options.technicianId || API_CONFIG.sampleTechnicianId
        }
      );
      
      this.currentSession = sessionRequest;
      console.log('✅ Session requested:', sessionRequest.sessionId);
      
      // Handle consent if needed
      if (options.autoConsent) {
        await this.handleConsentFlow(sessionRequest);
      }
      
      return sessionRequest;
      
    } catch (error) {
      console.error('❌ Failed to start diagnosis session:', error.message);
      throw error;
    }
  }

  async handleConsentFlow(sessionData) {
    try {
      console.log('🔐 Processing consent flow...');
      
      // Generate consent message
      const consentMessage = this.consentManager.generateConsentMessage(sessionData);
      console.log('📱 Consent Message:', consentMessage);
      
      // Auto-grant consent for demo
      const consentDecision = {
        allow: true,
        requestId: sessionData.requestId || 'AUTO_CONSENT',
        purpose: sessionData.reason
      };
      
      const consentResult = await this.consentManager.processConsentFlow(
        API_CONFIG.sampleVIN,
        sessionData.sessionId,
        consentDecision
      );
      
      console.log('✅ Consent granted:', consentResult);
      
    } catch (error) {
      console.error('❌ Consent flow failed:', error.message);
    }
  }

  async runComprehensiveDiagnosis() {
    try {
      if (!this.currentSession) {
        throw new Error('No active session. Start a session first.');
      }
      
      console.log('🔍 Running comprehensive diagnosis...');
      
      const sessionId = this.currentSession.sessionId;
      
      // Start live data streaming
      console.log('📊 Starting live data stream...');
      await this.commandManager.startLiveDataStream(API_CONFIG.sampleVIN, sessionId, [
        'RPM', 'COOLANT_TEMP', 'SPEED', 'FUEL_PRESSURE', 'THROTTLE_POS'
      ]);
      
      this.isStreaming = true;
      
      // Read DTC codes
      console.log('🔧 Reading DTC codes...');
      await this.commandManager.readDTC(API_CONFIG.sampleVIN, sessionId);
      
      // Wait for data collection
      console.log('⏳ Collecting data for 15 seconds...');
      await this.sleep(15000);
      
      // Stop live streaming
      console.log('⏹️ Stopping data collection...');
      this.isStreaming = false;
      
      // Generate report
      console.log('📄 Generating diagnosis report...');
      const report = await this.reportManager.generateReport(API_CONFIG.sampleVIN, sessionId);
      
      // Process and display results
      await this.processDiagnosisResults(report);
      
      return report;
      
    } catch (error) {
      console.error('❌ Comprehensive diagnosis failed:', error.message);
      throw error;
    }
  }

  async processDiagnosisResults(report) {
    console.log('\n📊 DIAGNOSIS RESULTS');
    console.log('===================');
    
    const formattedReport = this.reportManager.formatReport(report);
    const severity = this.reportManager.assessReportSeverity(report);
    const actionItems = this.reportManager.generateActionItems(report);
    
    console.log(`🚨 Overall Status: ${formattedReport.summary.status} (${severity.severity})`);
    console.log(`🔧 DTC Codes Found: ${formattedReport.summary.dtcCount}`);
    console.log(`⚠️ Critical Issues: ${formattedReport.summary.criticalIssues}`);
    
    if (formattedReport.issues.length > 0) {
      console.log('\n🔧 ISSUES FOUND:');
      formattedReport.issues.forEach((issue, index) => {
        console.log(`  ${index + 1}. ${issue.code} (${issue.severity})`);
        console.log(`     ${issue.description}`);
      });
    }
    
    if (actionItems.length > 0) {
      console.log('\n📋 RECOMMENDED ACTIONS:');
      actionItems.forEach((action, index) => {
        console.log(`  ${index + 1}. [${action.type}] ${action.description}`);
        if (action.details) {
          console.log(`     ${action.details}`);
        }
      });
    }
    
    console.log('\n📈 PERFORMANCE DATA:');
    console.log(`  Average RPM: ${formattedReport.performance.avgRpm}`);
    console.log(`  Max Temperature: ${formattedReport.performance.maxTemp}°C`);
    console.log(`  Data Points Collected: ${formattedReport.performance.dataPoints}`);
    
    console.log('===================\n');
  }

  handleLiveData(data) {
    if (this.isStreaming) {
      const { rpm, coolantTemp, speed } = data.data;
      console.log(`📊 Live: ${rpm} RPM | ${coolantTemp}°C | ${speed} km/h`);
    }
  }

  handleDTCResult(data) {
    console.log('🔧 DTC Analysis Complete');
    if (data.dtcCodes && data.dtcCodes.length > 0) {
      data.dtcCodes.forEach(code => {
        console.log(`  ${code.code}: ${code.description} (${code.severity})`);
      });
    } else {
      console.log('  ✅ No DTC codes detected');
    }
  }

  handleCommandStatus(data) {
    const { command, status } = data;
    const statusIcon = status === 'COMPLETED' ? '✅' : 
                       status === 'FAILED' ? '❌' : 
                       status === 'EXECUTING' ? '⏳' : '❓';
    
    console.log(`${statusIcon} ${command}: ${status}`);
  }

  async stopDiagnosisSession() {
    try {
      if (this.currentSession) {
        console.log('🛑 Stopping diagnosis session...');
        
        await this.sessionManager.stopSession(
          API_CONFIG.sampleVIN,
          this.currentSession.sessionId,
          { reason: 'DIAGNOSIS_COMPLETED' }
        );
        
        this.currentSession = null;
        this.isStreaming = false;
        
        console.log('✅ Session stopped successfully');
      }
    } catch (error) {
      console.error('❌ Failed to stop diagnosis session:', error.message);
    }
  }

  async getDiagnosisHistory() {
    try {
      console.log('📜 Getting diagnosis history...');
      
      const history = await this.reportManager.getDiagnosisHistory(API_CONFIG.sampleVIN, {
        limit: 10
      });
      
      console.log(`Found ${history.sessions.length} past sessions`);
      
      history.sessions.forEach((session, index) => {
        console.log(`  ${index + 1}. ${session.sessionId} - ${session.timestamp} (${session.status})`);
      });
      
      return history;
      
    } catch (error) {
      console.error('❌ Failed to get diagnosis history:', error.message);
    }
  }

  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  async shutdown() {
    console.log('🔄 Shutting down Diagnosis Application...');
    
    // Stop current session if active
    await this.stopDiagnosisSession();
    
    // Disconnect SSE
    if (this.sseClient) {
      this.sseClient.disconnect();
    }
    
    console.log('✅ Diagnosis Application shutdown complete');
  }
}

// Example usage
async function main() {
  const app = new DiagnosisApplication();
  
  try {
    await app.initialize();
    
    // Start a comprehensive diagnosis session
    await app.startDiagnosisSession({
      reason: 'Demo Comprehensive Diagnosis',
      autoConsent: true
    });
    
    // Run the diagnosis
    const report = await app.runComprehensiveDiagnosis();
    
    // Get history
    await app.getDiagnosisHistory();
    
    // Clean up
    setTimeout(async () => {
      await app.shutdown();
      process.exit(0);
    }, 2000);
    
  } 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 diagnosis application:

bash
node diagnosisApp.js

Expected output:

🔧 Initializing Diagnosis Application...
📡 Connecting to real-time diagnosis events...
📡 Diagnosis SSE connection opened
✅ Diagnosis Application initialized successfully
🚀 Starting diagnosis session...
✅ Session requested: sess_001
🔐 Processing consent flow...
📱 Consent Message: { title: 'Remote Diagnosis Request', ... }
✅ Consent granted: { consented: true, sessionId: 'sess_001', ... }
🔍 Running comprehensive diagnosis...
📊 Starting live data stream...
🔧 Reading DTC codes...
⏳ Collecting data for 15 seconds...
📊 Live: 2500 RPM | 85.5°C | 0 km/h
📊 Live: 1800 RPM | 87.2°C | 0 km/h
🔧 DTC Analysis Complete
  P0520: Engine Oil Pressure Sensor/Switch Circuit (MEDIUM)
⏹️ Stopping data collection...
📄 Generating diagnosis report...

📊 DIAGNOSIS RESULTS
===================
🚨 Overall Status: WARNING (WARNING)
🔧 DTC Codes Found: 1
⚠️ Critical Issues: 0

🔧 ISSUES FOUND:
  1. P0520 (MEDIUM)
     Engine Oil Pressure Sensor/Switch Circuit

📋 RECOMMENDED ACTIONS:
  1. [MEDIUM] INSPECT_OIL_PRESSURE_SENSOR
     Check oil pressure sensor and wiring

📈 PERFORMANCE DATA:
  Average RPM: 2150
  Max Temperature: 92.5°C
  Data Points Collected: 150
===================

📜 Getting diagnosis history...
Found 3 past sessions
  1. sess_001 - 2026-01-13T14:30:00Z (COMPLETED)
  2. sess_002 - 2026-01-10T09:15:00Z (COMPLETED)
  3. sess_003 - 2026-01-05T16:45:00Z (COMPLETED)

🔄 Shutting down Diagnosis Application...
🛑 Stopping diagnosis session...
✅ Session stopped successfully
📡 Diagnosis SSE connection closed
✅ Diagnosis Application shutdown complete

Challenge Exercises

  1. Advanced Diagnostics: Implement support for advanced diagnostic protocols like UDS (Unified Diagnostic Services).

  2. Predictive Maintenance: Add machine learning models to predict potential failures based on historical data.

  3. Multi-ECU Support: Extend the system to handle multiple ECUs simultaneously with parallel diagnostics.

  4. Mobile Technician Interface: Create a mobile-friendly interface for technicians to perform diagnostics on-site.

  5. Automated Test Sequences: Implement predefined test sequences for common diagnostic scenarios.

Summary

In this codelab, you learned how to:

  • ✅ Implement remote diagnosis session management
  • ✅ Handle user consent and security requirements
  • ✅ Execute diagnostic commands and process results
  • ✅ Stream real-time diagnostic data via SSE
  • ✅ Generate comprehensive diagnosis reports
  • ✅ Build a complete remote diagnostic system

The Manual Diagnosis API provides powerful capabilities for remote vehicle diagnostics while maintaining security and user consent. The implementation patterns you learned here can be applied to various remote diagnostic and monitoring scenarios.

Additional Resources

Released under the MIT License.