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
mkdir manual-diagnosis-demo
cd manual-diagnosis-demo
npm init -y
npm install axios node-fetch moment2. Configuration
Create a config.js file:
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:
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:
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();Step 2: User Consent Management
Implement Consent Control
Create consentManager.js:
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;Test Consent Management
Create test-consent.js:
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:
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:
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:
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:
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:
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:
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:
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:
node diagnosisApp.jsExpected 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 completeChallenge Exercises
Advanced Diagnostics: Implement support for advanced diagnostic protocols like UDS (Unified Diagnostic Services).
Predictive Maintenance: Add machine learning models to predict potential failures based on historical data.
Multi-ECU Support: Extend the system to handle multiple ECUs simultaneously with parallel diagnostics.
Mobile Technician Interface: Create a mobile-friendly interface for technicians to perform diagnostics on-site.
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.