-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WIP on supporting RMonitor protocol.
In particular, Race Monitor, but also eventually for the IMSA replay files.
- Loading branch information
1 parent
6284f7e
commit 625123b
Showing
6 changed files
with
286 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,10 @@ | ||
import dayjs from 'dayjs'; | ||
import customParseFormat from 'dayjs/plugin/customParseFormat.js'; | ||
import duration from 'dayjs/plugin/duration.js'; | ||
import toObject from 'dayjs/plugin/toObject.js'; | ||
|
||
dayjs.extend(customParseFormat); | ||
dayjs.extend(duration); | ||
dayjs.extend(toObject); | ||
|
||
export default dayjs; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import { EventEmitter } from '../../eventEmitter.js'; | ||
|
||
export class RMonitorClient extends EventEmitter { | ||
constructor() { | ||
super(); | ||
|
||
this.clear = this.clear.bind(this); | ||
this.handleMessages = this.handleMessages.bind(this); | ||
this.handleMessage = this.handleMessage.bind(this); | ||
|
||
this.clear(); | ||
} | ||
|
||
clear() { | ||
this.state = { | ||
classes: {}, | ||
competitors: {}, | ||
session: {}, | ||
settings: {} | ||
}; | ||
} | ||
|
||
handlers = { | ||
// Competitor information 1 | ||
'A': (regNumber, number, transponder, firstName, lastName, nationality, classNumber) => { | ||
this.state.competitors[regNumber] = { | ||
...this.state.competitors[regNumber] || {}, | ||
regNumber, | ||
number, | ||
transponder, | ||
firstName, | ||
lastName, | ||
nationality, | ||
classNumber | ||
}; | ||
}, | ||
// Competitor information 2 | ||
'COMP': (regNumber, number, classNumber, firstName, lastName, nationality, additional) => { | ||
this.state.competitors[regNumber] = { | ||
...this.state.competitors[regNumber] || {}, | ||
number, | ||
firstName, | ||
lastName, | ||
nationality, | ||
classNumber, | ||
additional | ||
}; | ||
}, | ||
// Run/session information | ||
'B': (number, description) => { | ||
this.state.session = { | ||
...this.state.session || {}, | ||
number, | ||
description | ||
}; | ||
}, | ||
// Class information | ||
'C': (id, description) => { | ||
this.state.classes[id] = description; | ||
}, | ||
// Track settings | ||
'E': (name, value) => { | ||
this.state.settings[name] = value; | ||
}, | ||
// Heartbeat | ||
'F': (lapsRemain, timeRemain, timestamp, timeElapsed, flagState) => { | ||
this.state.session = { | ||
...this.state.session || {}, | ||
flagState, | ||
lapsRemain, | ||
timeElapsed, | ||
timeRemain, | ||
timestamp | ||
}; | ||
}, | ||
// Race information | ||
'G': (position, regNumber, laps, totalTime) => { | ||
this.state.competitors[regNumber] = { | ||
...this.state.competitors[regNumber] || {}, | ||
position, | ||
laps, | ||
totalTime | ||
}; | ||
}, | ||
// Practice/Qualifying information | ||
'H': (bestPosition, regNumber, bestLapNum, bestLapTime) => { | ||
this.state.competitors[regNumber] = { | ||
...this.state.competitors[regNumber] || {}, | ||
bestPosition, | ||
bestLapNum, | ||
bestLapTime | ||
}; | ||
}, | ||
// Clear | ||
'I': () => { | ||
this.clear(); | ||
}, | ||
// Start/finish crossing information | ||
'J': (regNumber, laptime, totalTime) => { | ||
this.state.competitors[regNumber] = { | ||
...this.state.competitors[regNumber] || {}, | ||
lastLapTime: laptime, | ||
totalTime | ||
}; | ||
}, | ||
// Race Monitor: s/f crossing | ||
'RMHL': (regNumber, laps, position, lastLapTime, lastFlag, totalTime) => { | ||
this.state.competitors[regNumber] = { | ||
...this.state.competitors[regNumber] || {}, | ||
laps, | ||
lastFlag, | ||
lastLapTime, | ||
position, | ||
totalTime | ||
}; | ||
}, | ||
// Race Monitor: transponder last seen | ||
'RMLT': (regNumber, timestamp) => { | ||
this.state.competitors[regNumber] = { | ||
...this.state.competitors[regNumber] || {}, | ||
lastSeen: timestamp | ||
}; | ||
} | ||
}; | ||
|
||
handleMessages(messages) { | ||
messages.split('\r\n').forEach( | ||
this.handleMessage | ||
); | ||
return this.state; | ||
} | ||
|
||
handleMessage(message) { | ||
if (message.startsWith('$')) { | ||
const [msgType, ...args] = message.trim().slice(1).split(','); | ||
|
||
if (this.handlers[msgType]) { | ||
this.handlers[msgType](...args.map(a => a.replaceAll('"', '').trim())); | ||
this.emit('update', { ...this.state }); | ||
} | ||
else { | ||
console.log('Unknown: ', msgType, args); | ||
} | ||
return this.state; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { RaceMonitor } from './service.js'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { Service } from '../services.js'; | ||
import { RMonitorClient } from './client.js'; | ||
import { getManifest, getState } from './translate.js'; | ||
|
||
export class RaceMonitor extends Service { | ||
start(connectionService) { | ||
const { groups: { raceID } } = this.service.source.match(RaceMonitor.regex); | ||
|
||
connectionService.fetch(`https://api.race-monitor.com/Info/WebRaceList?raceID=${raceID}`).then( | ||
(wrlText) => { | ||
const wrl = JSON.parse(wrlText); | ||
|
||
const token = wrl.LiveTimingToken; | ||
const host = wrl.LiveTimingHost; | ||
const currentRace = wrl.CurrentRaces.find(r => r.ReceivingData); | ||
|
||
if (!currentRace) { | ||
this.emitError('Could not find an active session'); | ||
} | ||
else { | ||
const wsUrl = `wss://${host}/instance/${currentRace.Instance}/${token}`; | ||
|
||
this.socket = connectionService.createWebsocket(wsUrl); | ||
|
||
const client = new RMonitorClient(); | ||
|
||
this.socket.on('connect', () => { | ||
this.emitInfo('Connected to upstream timing source'); | ||
this.socket.send(`$JOIN,${currentRace.Instance},${token}`); | ||
}); | ||
|
||
this.socket.on('message', (msg) => { | ||
const data = msg.data || msg.toString; | ||
client.handleMessages(data); | ||
}); | ||
|
||
client.on('update', (state) => { | ||
this._pendingState = state; | ||
}); | ||
|
||
setInterval( | ||
() => { | ||
if (this._pendingState) { | ||
console.log(this._pendingState); | ||
|
||
const manifest = getManifest(this._pendingState); | ||
this.onManifestChange(manifest); | ||
|
||
const state = getState(this._pendingState); | ||
console.log(state); | ||
this.onStateChange(state); | ||
|
||
this._pendingState = null; | ||
} | ||
}, | ||
1000 | ||
); | ||
} | ||
} | ||
); | ||
} | ||
|
||
stop() { | ||
this.socket?.close(); | ||
this._updateInterval && clearInterval(this._updateInterval); | ||
} | ||
} | ||
|
||
RaceMonitor.regex = /http(s?):\/\/(www\.)?race-monitor.com\/Live\/Race\/(?<raceID>[0-9]+)/; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import dayjs from '../../dates.js'; | ||
import { Stat } from '../../racing.js'; | ||
|
||
export const getManifest = (state) => { | ||
const descParts = (state.session.description || '').split(' - '); | ||
|
||
const name = descParts.length === 1 ? 'RMonitor' : descParts[0]; | ||
const description = descParts.length === 1 ? descParts : descParts[1]; | ||
|
||
return { | ||
name, | ||
description, | ||
colSpec: [ | ||
Stat.NUM, | ||
Stat.DRIVER, | ||
Stat.LAPS, | ||
Stat.LAST_LAP, | ||
Stat.BEST_LAP | ||
] | ||
}; | ||
}; | ||
|
||
export const getState = (state) => { | ||
return { | ||
cars: mapCars(Object.values(state.competitors)), | ||
session: {} | ||
}; | ||
}; | ||
|
||
const mapCars = (competitors) => { | ||
const sortedCompetitors = competitors.sort( | ||
(a, b) => { | ||
const positionDiff = parseInt(a.position || 0, 10) - parseInt(b.position || 0, 10); | ||
if (positionDiff === 0) { | ||
return (b.lastSeen || 0) - (a.lastSeen || 0); | ||
} | ||
return positionDiff; | ||
} | ||
); | ||
|
||
return sortedCompetitors.map( | ||
car => { | ||
const lastLap = parseTime(car.lastLapTime); | ||
const bestLap = parseTime(car.bestLapTime); | ||
return [ | ||
car.number, | ||
`${car.lastName.toUpperCase()}, ${car.firstName}`, | ||
car.laps, | ||
lastLap > 0 ? lastLap : null, | ||
bestLap > 0 ? bestLap : null, | ||
car.regNumber, | ||
car.position | ||
]; | ||
} | ||
); | ||
}; | ||
|
||
const parseTime = (time) => { | ||
if (!time) { | ||
return null; | ||
} | ||
const parsed = dayjs(time, 'HH:mm:ss.SSS').toObject(); | ||
const duration = dayjs.duration({ hours: parsed.hours, minutes: parsed.minutes, seconds: parsed.seconds, milliseconds: parsed.milliseconds }); | ||
return duration.asSeconds(); | ||
}; |