Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ The UI code needs to be built using Webpack. Run `npx webpack` to do so, or `npx

Run `npm run dev` to run the app.

### Logging and replaying telemetry

Use environment variables to log or replay telemetry files.

`STUART_WRITE_LOG=./telemetry.log npm run dev` to write telemetry data to a file
`STUART_READ_LOG=./telemetry.log npm run dev` to read telemetry data in from a file. (will not attach to iRacing)

## Issues with Node Module Version?

Run `.\node_modules\.bin\electron-rebuild.cmd` and it should fix it.
76 changes: 38 additions & 38 deletions app/api-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,45 @@ const { contextBridge, ipcRenderer } = require('electron');
// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld(
"api", {
receive: (channel, func) => {
ipcRenderer.on(channel, (event, ...args) => func(...args));
},
replay: (carNumber, sessionNum, sessionTime) => {
ipcRenderer.send('replay', { carNumber, sessionNum, sessionTime })
},
focusCamera: (carNumber) => {
ipcRenderer.send('focus-camera', { carNumber });
},
jumpToTime: (sessionNum, sessionTime) => {
ipcRenderer.send('jump-to-time', { sessionNum, sessionTime });
},
acknowledgeIncident: (incidentId) => {
ipcRenderer.send('acknowledge-incident', { incidentId });
},
dismissIncident: (incidentId) => {
ipcRenderer.send('dismiss-incident', { incidentId });
},
unresolveIncident: (incidentId) => {
ipcRenderer.send('unresolve-incident', { incidentId });
},
connect: () => {
ipcRenderer.send('connect-window', {});
},
clearIncidents: () => {
ipcRenderer.send('clear-incidents', {});
},
pauseReplay: () => {
ipcRenderer.send('replay-pause', {});
},
playReplay: () => {
ipcRenderer.send('replay-play', {});
},
liveReplay: () => {
ipcRenderer.send('replay-live', {});
}
"api",
{
// returns a function that can be used to "unsubscribe" from this event
receive: (channel, func) => {

function listener(event, ...args) {
func(...args);
}

ipcRenderer.on(channel, listener);

}
return () => { ipcRenderer.removeListener(listener);}
},
replay: (carNumber, sessionNum, sessionTime) => {
ipcRenderer.send('replay', { carNumber, sessionNum, sessionTime })
},
focusCamera: (carNumber, cameraGroup) => {
ipcRenderer.send('focus-camera', { carNumber, cameraGroup });
},
jumpToTime: (sessionNum, sessionTime) => {
ipcRenderer.send('jump-to-time', { sessionNum, sessionTime });
},
pauseReplay: () => {
ipcRenderer.send('replay-pause', {});
},
playReplay: () => {
ipcRenderer.send('replay-play', {});
},
liveReplay: () => {
ipcRenderer.send('replay-live', {});
},
sendChatMessages: (msgs) => {
return ipcRenderer.invoke('send-chat-message', msgs);
},
replaySpeed: (speed) => {
ipcRenderer.send('replay-speed', speed);
},
replaySearch: (searchMode) => {
ipcRenderer.send('replay-search', searchMode);
}
}
);
255 changes: 255 additions & 0 deletions app/appState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@

import { DriverCar } from '@common/DriverState';
import _ from 'lodash';
import iracing from 'node-irsdk-2021';
import { HexColor, CarColors } from '../common/util'

export type CarSessionFlag = "Servicible" | "Black" | "Repair" | "Disqualify"
export type TrackSurface ="OnTrack" | "OffTrack" | "NotInWorld" | "AproachingPits" | "InPitStall" ; // sic: "AproachingPits" is spelled wrong in the telemetry data
export type SessionFlag = "StartHidden" | "StartGo" | "StartReady" | "OneLapToGreen" | "Caution" | "CautionWaving";
/**
* AppState is a more convenient view of the iRacing game state
* that combines information from the Session data feed and the
* Telemetry data feed.
*
* To make use of a property from iRacing, first add an appropriate
* definition for it to irsdk.d.ts and here. Then update "toAppState"
* in app/state/streams.ts to copy the values from the game state.
*/

export function getTrackLength(sesh: iracing.SessionData): number {
const trackLengthStr = sesh.data.WeekendInfo.TrackLength;
const trackLengthRegex = new RegExp("^(\\d+(\\.\\d+)?) (\\w+)$");
const match = trackLengthRegex.exec(trackLengthStr);

if (match) {
const trackLength = +(match[1]);
const distanceUnit = match[3];

return trackLength * (distanceUnit == "km" ? 1000 : 1609.344)
}

return 0;
}

export type TimeStamp = {
session: number,
time: number,
}

export function toDriverCar(car: CarState) : DriverCar {
return {
class: {
color: car.classColor,
name: car.className,
},
color: car.carColors,
driverName: car.drivers[0].name, // TODO figure out how to get the current driver of the car
idx: car.idx,
number: car.number,
teamName: car.teamName,
}
}

export type AppState = {
/**
* The current live time in sim.
*/
live: TimeStamp,
sessionFlags: SessionFlag[],
replay: {
carIdx: number,
speed: number,
time: TimeStamp,
cameraGroupNum: number,
cameraNum: number,
},
weekend: {
/** A human-readable display name for the current track */
trackName: string,
/** a machine-readable track ID */
trackId: string,
/** the overall length of the track in meters */
trackLength: number,
/** e.g., '0.9 mi' or '1.7 km' */
trackDisplayLength: string,
},
cameraInfo: {
cameraGroups: {
num: number,
name: string,
cameras: {
num: number,
name: string,
}[]
}[]
},
cars: CarState[],
findCarByIdx: (_:number) => CarState | undefined,
findCarByNumber: (_:string) => CarState | undefined,
sessions: {
num: number,
name: string,
type: string,
}[],
}

export type CarState = {
// session

idx: number,
number: string,
/**
* Human-readable class name
*/
className: string,
/**
* Machine readable class ID
*/
classId: number,
classColor: HexColor,
carName: string,
carNameShort: string,
carColors: CarColors,
teamName: string,
drivers: {
customerId: number,
name: string,
shortName: string,
incidentCount: number,
}[],
teamIncidentCount: number,

// telemetry

lap: number,
trackPositionPct: number,
officialPosition: number,
officialClassPosition: number,
trackSurface: TrackSurface,
isAI: boolean,
isPaceCar: boolean,
paceRow: number,
paceLine: number,
flags: CarSessionFlag[]
}

function fromInt(hexColor: number) : HexColor {
return "#" + hexColor.toString(16).padStart(6, '0');
}

function fromDesignString(designStr: string) : CarColors {
// TODO regex this string in the form "12,111111,222222,333333"
return {
primary: "#000000",
secondary: "#000000",
tertiary: "#000000",
}
}

function createLookupFunction<K,V>(values: V[], getKey:(_:V) => K): (_:K) => V | undefined {
const lookupTable: Map<K, V> = new Map();
values.forEach(value => {
lookupTable.set(getKey(value), value);
});
return lookupTable.get.bind(lookupTable);
}

/**
* Generates a new AppState based on the latest session and telemetry data
*/
export function toAppState(session: iracing.SessionData, telemetry: iracing.TelemetryData): AppState {

// Iterate over drivers to start building up car array as new cars are discovered
let cars: Map<number, CarState> = new Map();

for(let driver of session.data.DriverInfo.Drivers) {
const idx = driver.CarIdx;

let car = cars.get(idx);
if(!car) {
car = {
carName: driver.CarScreenName,
carNameShort: driver.CarScreenNameShort,
classColor: fromInt(driver.CarClassColor),
classId: driver.CarClassID,
className: driver.CarClassShortName,
drivers: [],
idx: idx,
carColors: fromDesignString(driver.CarDesignStr),
isAI: driver.CarIsAI != 0,
isPaceCar: driver.CarIsPaceCar != 0,
number: driver.CarNumber,
teamName: driver.TeamName,
teamIncidentCount: driver.TeamIncidentCount,

lap: telemetry.values.CarIdxLap[idx],
officialClassPosition: telemetry.values.CarIdxPosition[idx],
officialPosition: telemetry.values.CarIdxPosition[idx],
paceLine: telemetry.values.CarIdxPaceLine[idx],
paceRow: telemetry.values.CarIdxPaceRow[idx],
trackPositionPct: telemetry.values.CarIdxLapDistPct[idx],
trackSurface: telemetry.values.CarIdxTrackSurface[idx] as TrackSurface,
flags: telemetry.values.CarIdxSessionFlags[idx]
};
cars.set(idx, car);
}

car.drivers.push({
customerId: driver.UserID,
incidentCount: driver.CurDriverIncidentCount,
name: driver.UserName,
shortName: driver.AbbrevName,
});
}

let sessions = session.data.SessionInfo.Sessions;

return {
cars: [...cars.values()],
replay: {
carIdx: telemetry.values.CamCarIdx,
time: {
session: telemetry.values.ReplaySessionNum,
time: telemetry.values.ReplaySessionTime,
},
speed: telemetry.values.ReplayPlaySpeed,
cameraGroupNum: telemetry.values.CamGroupNumber,
cameraNum: telemetry.values.CamCameraNumber,
},
live: {
session: telemetry.values.SessionNum,
time: telemetry.values.SessionTime,
},
weekend: {
trackDisplayLength: session.data.WeekendInfo.TrackLength,
trackLength: getTrackLength(session),
trackId: session.data.WeekendInfo.TrackConfigName,
trackName: session.data.WeekendInfo.TrackDisplayName,
},
cameraInfo: {
cameraGroups: session.data.CameraInfo.Groups.map((group) => {
return {
name: group.GroupName,
num: group.GroupNum,
cameras: group.Cameras.map((cam) => {
return {
name: cam.CameraName,
num: cam.CameraNum,
};
})
};
}),
},
sessionFlags: telemetry.values.SessionFlags,
sessions: sessions.map(session => ({
name: session.SessionName,
num: session.SessionNum,
type: session.SessionType,
})),
findCarByIdx: (idx: number) => cars.get(idx),
findCarByNumber: createLookupFunction([...cars.values()], (car) => car.number),
};
}


39 changes: 0 additions & 39 deletions app/application.ts

This file was deleted.

Loading