// lib/telemetryBridge.ts // Bridge between C++ Unix socket telemetry server and Next.js SSE clients import net from 'net'; const TELEMETRY_SOCKET_PATH = '/tmp/ACtelemetry_socket'; interface Position { x: number; y: number; z: number; } interface CarTelemetry { carID: number; driver_name: string; driver_guid: string; car_model: string; position: Position; normalizedSplinePos: number; speed_kmh: number; gear: number; rpm: number; last_lap_time: number; best_lap_time: number; current_lap: number; position_rank: number; } interface TelemetryPacket { server_id: number; car_count: number; cars: CarTelemetry[]; } type TelemetryCallback = (packet: TelemetryPacket) => void; class TelemetryBridge { private socket: net.Socket | null = null; private subscribers: Set = new Set(); private reconnectTimer: NodeJS.Timeout | null = null; private connected: boolean = false; private buffer: Buffer = Buffer.alloc(0); constructor() { this.connect(); } private connect() { if (this.socket) { this.socket.destroy(); } console.log('[Bridge] Connecting to telemetry socket...'); this.socket = net.createConnection(TELEMETRY_SOCKET_PATH); this.socket.on('connect', () => { console.log('[Bridge] Connected to telemetry server'); this.connected = true; this.buffer = Buffer.alloc(0); if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } }); this.socket.on('data', (data: Buffer) => { // Append new data to buffer this.buffer = Buffer.concat([this.buffer, data]); // Calculate expected packet size // server_id (1) + car_count (1) + cars (car_count * car_size) const HEADER_SIZE = 2; // server_id + car_count const CAR_SIZE = 1 + 64 + 64 + 64 + 4 + 4 + 1 + 2 + 4 + 4 + 2 + 1; // 211 bytes per car while (this.buffer.length >= HEADER_SIZE) { const server_id = this.buffer.readUInt8(0); const car_count = this.buffer.readUInt8(1); // Updated CAR_SIZE: 1 + 64 + 64 + 64 + 12 + 4 + 4 + 1 + 2 + 4 + 4 + 2 + 1 = 227 bytes const CAR_SIZE = 227; const expected_size = HEADER_SIZE + (car_count * CAR_SIZE); if (this.buffer.length >= expected_size) { // We have a complete packet const packet_data = this.buffer.slice(0, expected_size); this.buffer = this.buffer.slice(expected_size); // Parse packet const packet = this.parsePacket(packet_data); if (packet) { // Broadcast to all subscribers this.subscribers.forEach(callback => { try { callback(packet); } catch (error) { console.error('[Bridge] Error in subscriber callback:', error); } }); } } else { // Wait for more data break; } } }); this.socket.on('error', (err) => { console.error('[Bridge] Socket error:', err.message); this.connected = false; }); this.socket.on('close', () => { console.log('[Bridge] Connection closed, reconnecting in 5s...'); this.connected = false; if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); } this.reconnectTimer = setTimeout(() => { this.connect(); }, 5000); }); } private parsePacket(data: Buffer): TelemetryPacket | null { try { let offset = 0; const server_id = data.readUInt8(offset); offset += 1; const car_count = data.readUInt8(offset); offset += 1; const cars: CarTelemetry[] = []; for (let i = 0; i < car_count; i++) { const carID = data.readUInt8(offset); offset += 1; const driver_name = this.readString(data, offset, 64); offset += 64; const driver_guid = this.readString(data, offset, 64); offset += 64; const car_model = this.readString(data, offset, 64); offset += 64; const position: Position = { x: data.readFloatLE(offset), y: data.readFloatLE(offset + 4), z: data.readFloatLE(offset + 8), }; offset += 12; const normalizedSplinePos = data.readFloatLE(offset); offset += 4; const speed_kmh = data.readFloatLE(offset); offset += 4; const gear = data.readUInt8(offset); offset += 1; const rpm = data.readUInt16LE(offset); offset += 2; const last_lap_time = data.readUInt32LE(offset); offset += 4; const best_lap_time = data.readUInt32LE(offset); offset += 4; const current_lap = data.readUInt16LE(offset); offset += 2; const position_rank = data.readUInt8(offset); offset += 1; cars.push({ carID, driver_name, driver_guid, car_model, position, normalizedSplinePos, speed_kmh, gear, rpm, last_lap_time, best_lap_time, current_lap, position_rank, }); } return { server_id, car_count, cars, }; } catch (error) { console.error('[Bridge] Error parsing packet:', error); return null; } } private readString(buffer: Buffer, offset: number, length: number): string { const end = buffer.indexOf(0, offset); const strEnd = end === -1 || end >= offset + length ? offset + length : end; return buffer.toString('utf8', offset, strEnd).trim(); } public subscribe(callback: TelemetryCallback): () => void { this.subscribers.add(callback); console.log('[Bridge] Subscriber added (total:', this.subscribers.size, ')'); // Return unsubscribe function return () => { this.subscribers.delete(callback); console.log('[Bridge] Subscriber removed (total:', this.subscribers.size, ')'); }; } public getSubscriberCount(): number { return this.subscribers.size; } public isConnected(): boolean { return this.connected; } public destroy() { if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (this.socket) { this.socket.destroy(); this.socket = null; } this.subscribers.clear(); } } // Singleton instance let bridgeInstance: TelemetryBridge | null = null; export function getTelemetryBridge(): TelemetryBridge { if (!bridgeInstance) { bridgeInstance = new TelemetryBridge(); } return bridgeInstance; } // Cleanup on process exit if (typeof process !== 'undefined') { process.on('SIGTERM', () => { if (bridgeInstance) { bridgeInstance.destroy(); } }); process.on('SIGINT', () => { if (bridgeInstance) { bridgeInstance.destroy(); } }); }