ACPlayer_Webpage/lib/telemetryBridge.ts

203 lines
5.3 KiB
TypeScript

// lib/telemetryBridge.ts
// Bridge between C++ Unix socket and Next.js SSE
import { Socket } from 'net';
const TELEMETRY_SOCKET_PATH = '/tmp/ACtelemetry_socket';
interface CarTelemetry {
carID: number;
driver_name: string;
driver_guid: string;
car_model: string;
normalizedSplinePos: number;
speed_kmh: number;
gear: number;
rpm: number;
last_lap_time: number;
best_lap_time: number;
current_lap: number;
position: number;
}
interface TelemetryPacket {
server_id: number;
car_count: number;
cars: CarTelemetry[];
}
type TelemetryCallback = (data: TelemetryPacket) => void;
class TelemetryBridge {
private socket: Socket | null = null;
private connected: boolean = false;
private callbacks: Set<TelemetryCallback> = new Set();
private reconnectTimeout: NodeJS.Timeout | null = null;
private buffer: Buffer = Buffer.alloc(0);
constructor() {
this.connect();
}
private connect() {
console.log('[Telemetry] Connecting to', TELEMETRY_SOCKET_PATH);
this.socket = new Socket();
this.socket.connect(TELEMETRY_SOCKET_PATH, () => {
console.log('[Telemetry] Connected to C++ socket');
this.connected = true;
this.buffer = Buffer.alloc(0); // Reset buffer on new connection
});
this.socket.on('data', (data: Buffer) => {
// Append new data to buffer
this.buffer = Buffer.concat([this.buffer, data]);
// Parse complete packets from buffer
this.parsePackets();
});
this.socket.on('error', (err) => {
console.error('[Telemetry] Socket error:', err.message);
this.connected = false;
});
this.socket.on('close', () => {
console.log('[Telemetry] Connection closed, reconnecting in 2s...');
this.connected = false;
this.socket = null;
// Reconnect after 2 seconds
if (this.reconnectTimeout) clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = setTimeout(() => this.connect(), 2000);
});
}
private parsePackets() {
// Telemetry packet structure from C++:
// uint8_t server_id (1 byte)
// uint8_t car_count (1 byte)
// car_telemetry cars[64] (each car = 158 bytes)
const HEADER_SIZE = 2;
const CAR_SIZE = 158; // Size of car_telemetry struct
while (this.buffer.length >= HEADER_SIZE) {
const server_id = this.buffer.readUInt8(0);
const car_count = this.buffer.readUInt8(1);
const expected_size = HEADER_SIZE + (car_count * CAR_SIZE);
if (this.buffer.length < expected_size) {
// Not enough data yet, wait for more
break;
}
// Parse the packet
const packet: TelemetryPacket = {
server_id,
car_count,
cars: [],
};
let offset = HEADER_SIZE;
for (let i = 0; i < car_count; i++) {
const carID = this.buffer.readUInt8(offset);
offset += 1;
const driver_name = this.buffer.toString('utf8', offset, offset + 64).replace(/\0.*$/g, '');
offset += 64;
const driver_guid = this.buffer.toString('utf8', offset, offset + 64).replace(/\0.*$/g, '');
offset += 64;
const car_model = this.buffer.toString('utf8', offset, offset + 64).replace(/\0.*$/g, '');
offset += 64;
const normalizedSplinePos = this.buffer.readFloatLE(offset);
offset += 4;
const speed_kmh = this.buffer.readFloatLE(offset);
offset += 4;
const gear = this.buffer.readUInt8(offset);
offset += 1;
const rpm = this.buffer.readUInt16LE(offset);
offset += 2;
const last_lap_time = this.buffer.readUInt32LE(offset);
offset += 4;
const best_lap_time = this.buffer.readUInt32LE(offset);
offset += 4;
const current_lap = this.buffer.readUInt16LE(offset);
offset += 2;
const position = this.buffer.readUInt8(offset);
offset += 1;
packet.cars.push({
carID,
driver_name,
driver_guid,
car_model,
normalizedSplinePos,
speed_kmh,
gear,
rpm,
last_lap_time,
best_lap_time,
current_lap,
position,
});
}
// Emit packet to all callbacks
this.callbacks.forEach(cb => cb(packet));
// Remove processed packet from buffer
this.buffer = this.buffer.subarray(expected_size);
}
}
public subscribe(callback: TelemetryCallback): () => void {
this.callbacks.add(callback);
console.log('[Telemetry] Subscriber added, total:', this.callbacks.size);
// Return unsubscribe function
return () => {
this.callbacks.delete(callback);
console.log('[Telemetry] Subscriber removed, total:', this.callbacks.size);
};
}
public isConnected(): boolean {
return this.connected;
}
public disconnect() {
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
}
if (this.socket) {
this.socket.destroy();
this.socket = null;
}
this.connected = false;
}
}
// Singleton instance
let bridge: TelemetryBridge | null = null;
export function getTelemetryBridge(): TelemetryBridge {
if (!bridge) {
bridge = new TelemetryBridge();
}
return bridge;
}