276 lines
6.8 KiB
TypeScript
276 lines
6.8 KiB
TypeScript
// 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<TelemetryCallback> = 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();
|
|
}
|
|
});
|
|
}
|