UNPKG

node-irsdk

Version:

iRacing SDK implementation for Node.js

520 lines (473 loc) 15.3 kB
var util = require('util') var events = require('events') var Consts = require('./IrSdkConsts') var BroadcastMsg = Consts.BroadcastMsg /** Default parser used for SessionInfo YAML Fixes TeamName issue, uses js-yaml for actual parsing @private @param {string} sessionInfoStr raw session info YAML string @returns {Object} parsed session info or falsy */ function createSessionInfoParser () { var yaml = require('js-yaml') return function (sessionInfoStr) { var fixedYamlStr = sessionInfoStr.replace(/TeamName: ([^\n]+)/g, function (match, p1) { if ((p1[0] === '"' && p1[p1.length - 1] === '"') || (p1[0] === "'" && p1[p1.length - 1] === "'")) { return match // skip if quoted already } else { // 2nd replace is unnecessary atm but its here just in case return "TeamName: '" + p1.replace(/'/g, "''") + "'" } }) return yaml.safeLoad(fixedYamlStr) } } /** JsIrSdk is javascript implementation of iRacing SDK. Don't use constructor directly, use {@link module:irsdk.getInstance}. @class @extends events.EventEmitter @see {@link https://nodejs.org/api/events.html#events_class_eventemitter|EventEmitter API} @alias iracing @fires iracing#Connected @fires iracing#Disconnected @fires iracing#Telemetry @fires iracing#TelemetryDescription @fires iracing#SessionInfo @example var iracing = require('node-irsdk').getInstance() */ function JsIrSdk (IrSdkWrapper, opts) { events.EventEmitter.call(this) /** Execute any of available commands, excl. FFB command @method @param {Integer} msgId Message id @param {Integer} [arg1] 1st argument @param {Integer} [arg2] 2nd argument @param {Integer} [arg3] 3rd argument */ this.execCmd = IrSdkWrapper.sendCmd /** iRacing SDK related constants @type IrSdkConsts @instance */ this.Consts = Consts /** Camera controls @type {Object} */ this.camControls = { /** Change camera tool state @method @param {IrSdkConsts.CameraState} state new state @example * // hide UI and enable mouse aim * var States = iracing.Consts.CameraState * var state = States.CamToolActive | States.UIHidden | States.UseMouseAimMode * iracing.camControls.setState(state) */ setState: function (state) { self.execCmd(BroadcastMsg.CamSetState, state) }, /** Switch camera, focus on car @method @param {Integer|String|IrSdkConsts.CamFocusAt} carNum Car to focus on @param {Integer} [camGroupNum] Select camera group @param {Integer} [camNum] Select camera @example * // show car #2 * iracing.camControls.switchToCar(2) @example * // show car #02 * iracing.camControls.switchToCar('02') @example * // show leader * iracing.camControls.switchToCar('leader') @example * // show car #2 using cam group 3 * iracing.camControls.switchToCar(2, 3) */ switchToCar: function (carNum, camGroupNum, camNum) { camGroupNum = camGroupNum | 0 camNum = camNum | 0 if (typeof carNum === 'string') { if (isNaN(parseInt(carNum))) { carNum = stringToEnum(carNum, Consts.CamFocusAt) } else { carNum = padCarNum(carNum) } } if (Number.isInteger(carNum)) { self.execCmd(BroadcastMsg.CamSwitchNum, carNum, camGroupNum, camNum) } }, /** Switch camera, focus on position @method @param {Integer|IrSdkConsts.CamFocusAt} position Position to focus on @param {Integer} [camGroupNum] Select camera group @param {Integer} [camNum] Select camera @example iracing.camControls.switchToPos(2) // show P2 */ switchToPos: function (position, camGroupNum, camNum) { camGroupNum = camGroupNum | 0 camNum = camNum | 0 if (typeof position === 'string') { position = stringToEnum(position, Consts.CamFocusAt) } if (Number.isInteger(position)) { self.execCmd(BroadcastMsg.CamSwitchPos, position, camGroupNum, camNum) } } } /** Replay and playback controls @type {Object} */ this.playbackControls = { /** Play replay @method @example iracing.playbackControls.play() */ play: function () { self.execCmd(BroadcastMsg.ReplaySetPlaySpeed, 1, 0) }, /** Pause replay @method @example iracing.playbackControls.pause() */ pause: function () { self.execCmd(BroadcastMsg.ReplaySetPlaySpeed, 0, 0) }, /** fast-forward replay @method @param {Integer} [speed=2] FF speed, something between 2-16 works @example iracing.playbackControls.fastForward() // double speed FF */ fastForward: function (speed) { speed = speed || 2 self.execCmd(BroadcastMsg.ReplaySetPlaySpeed, speed, 0) }, /** rewind replay @method @param {Integer} [speed=2] RW speed, something between 2-16 works @example iracing.playbackControls.rewind() // double speed RW */ rewind: function (speed) { speed = speed || 2 self.execCmd(BroadcastMsg.ReplaySetPlaySpeed, -1 * speed, 0) }, /** slow-forward replay, slow motion @method @param {Integer} [divider=2] divider of speed, something between 2-17 works @example iracing.playbackControls.slowForward(2) // half speed */ slowForward: function (divider) { divider = divider || 2 divider -= 1 self.execCmd(BroadcastMsg.ReplaySetPlaySpeed, divider, 1) }, /** slow-backward replay, reverse slow motion @method @param {Integer} [divider=2] divider of speed, something between 2-17 works @example iracing.playbackControls.slowBackward(2) // half speed RW */ slowBackward: function (divider) { divider = divider || 2 divider -= 1 self.execCmd(BroadcastMsg.ReplaySetPlaySpeed, -1 * divider, 1) }, /** Search things from replay @method @param {IrSdkConsts.RpySrchMode} searchMode what to search @example iracing.playbackControls.search('nextIncident') */ search: function (searchMode) { if (typeof searchMode === 'string') { searchMode = stringToEnum(searchMode, Consts.RpySrchMode) } if (Number.isInteger(searchMode)) { self.execCmd(BroadcastMsg.ReplaySearch, searchMode) } }, /** Search timestamp @method @param {Integer} sessionNum Session number @param {Integer} sessionTimeMS Session time in milliseconds @example * // jump to 2nd minute of 3rd session * iracing.playbackControls.searchTs(2, 2*60*1000) */ searchTs: function (sessionNum, sessionTimeMS) { self.execCmd(BroadcastMsg.ReplaySearchSessionTime, sessionNum, sessionTimeMS) }, /** Go to frame. Frame counting can be relative to begin, end or current. @method @param {Integer} frameNum Frame number @param {IrSdkConsts.RpyPosMode} rpyPosMode Is frame number relative to begin, end or current frame @example iracing.playbackControls.searchFrame(1, 'current') // go to 1 frame forward */ searchFrame: function (frameNum, rpyPosMode) { if (typeof rpyPosMode === 'string') { rpyPosMode = stringToEnum(rpyPosMode, Consts.RpyPosMode) } if (Number.isInteger(rpyPosMode)) { self.execCmd(BroadcastMsg.ReplaySetPlayPosition, rpyPosMode, frameNum) } } } /** Reload all car textures @method @example iracing.reloadTextures() // reload all paints */ this.reloadTextures = function () { this.execCmd(BroadcastMsg.ReloadTextures, Consts.ReloadTexturesMode.All) } /** Reload car's texture @method @param {Integer} carIdx car to reload @example iracing.reloadTexture(1) // reload paint of carIdx=1 */ this.reloadTexture = function (carIdx) { this.execCmd(BroadcastMsg.ReloadTextures, Consts.ReloadTexturesMode.CarIdx, carIdx) } /** Execute chat command @param {IrSdkConsts.ChatCommand} cmd @param {Integer} [arg] Command argument, if needed @example iracing.execChatCmd('cancel') // close chat window */ this.execChatCmd = function (cmd, arg) { arg = arg || 0 if (typeof cmd === 'string') { cmd = stringToEnum(cmd, Consts.ChatCommand) } if (Number.isInteger(cmd)) { this.execCmd(BroadcastMsg.ChatComand, cmd, arg) } } /** Execute chat macro @param {Integer} num Macro's number (0-15) @example iracing.execChatMacro(1) // macro 1 */ this.execChatMacro = function (num) { this.execChatCmd('macro', num) } /** Execute pit command @param {IrSdkConsts.PitCommand} cmd @param {Integer} [arg] Command argument, if needed @example * // full tank, no tires, no tear off * iracing.execPitCmd('clear') * iracing.execPitCmd('fuel', 999) // 999 liters * iracing.execPitCmd('lf') // new left front * iracing.execPitCmd('lr', 200) // new left rear, 200 kPa */ this.execPitCmd = function (cmd, arg) { arg = arg || 0 if (typeof cmd === 'string') { cmd = stringToEnum(cmd, Consts.PitCommand) } if (Number.isInteger(cmd)) { this.execCmd(BroadcastMsg.PitCommand, cmd, arg) } } /** Control telemetry logging (ibt file) @param {IrSdkConsts.TelemCommand} cmd Command: start/stop/restart @example iracing.execTelemetryCmd('restart') */ this.execTelemetryCmd = function (cmd) { if (typeof cmd === 'string') { cmd = stringToEnum(cmd, Consts.TelemCommand) } if (Number.isInteger(cmd)) { this.execCmd(BroadcastMsg.TelemCommand, cmd) } } var self = this opts = opts || {} /** Parser for SessionInfo YAML @callback iracing~sessionInfoParser @param {String} sessionInfo SessionInfo YAML @returns {Object} parsed session info */ var parseSessionInfo = opts.sessionInfoParser if (!parseSessionInfo) parseSessionInfo = createSessionInfoParser() var connected = false // if irsdk is available var startIntervalId = setInterval(function () { if (!IrSdkWrapper.isInitialized()) { IrSdkWrapper.start() } }, 10000) IrSdkWrapper.start() /** Latest telemetry, may be null or undefined */ this.telemetry = null /** Latest telemetry, may be null or undefined */ this.telemetryDescription = null /** Latest telemetry, may be null or undefined */ this.sessionInfo = null var checkConnection = function () { if (IrSdkWrapper.isInitialized() && IrSdkWrapper.isConnected()) { if (!connected) { connected = true /** iRacing, sim, is started @event iracing#Connected @example * iracing.on('Connected', function (evt) { * console.log(evt) * }) */ self.emit('update', {type: 'Connected', timestamp: new Date()}) } } else { if (connected) { connected = false /** iRacing, sim, was closed @event iracing#Disconnected @example * iracing.on('Disconnected', function (evt) { * console.log(evt) * }) */ self.emit('update', {type: 'Disconnected', timestamp: new Date()}) IrSdkWrapper.shutdown() self.telemetryDescription = null } } } var telemetryIntervalId = setInterval(function () { checkConnection() if (connected && IrSdkWrapper.updateTelemetry()) { var now = new Date() // date gives ms accuracy self.telemetry = IrSdkWrapper.getTelemetry() // replace ctime timestamp self.telemetry.timestamp = now setImmediate(function () { if (!self.telemetryDescription) { self.telemetryDescription = IrSdkWrapper.getTelemetryDescription() /** Telemetry description, contains description of available telemetry values @event iracing#TelemetryDescription @type Object @example * iracing.on('TelemetryDescription', function (data) { * console.log(evt) * }) */ self.emit('update', {type: 'TelemetryDescription', data: self.telemetryDescription, timestamp: now}) } /** Telemetry update @event iracing#Telemetry @type Object @example * iracing.on('Telemetry', function (evt) { * console.log(evt) * }) */ self.emit('update', {type: 'Telemetry', data: self.telemetry.values, timestamp: now}) }) } }, opts.telemetryUpdateInterval) var sessionInfoIntervalId = setInterval(function () { checkConnection() if (connected && IrSdkWrapper.updateSessionInfo()) { var now = new Date() var sessionInfo = IrSdkWrapper.getSessionInfo() var doc setImmediate(function () { try { doc = parseSessionInfo(sessionInfo) } catch (ex) { // TODO: log faulty yaml console.error('js-irsdk: yaml error: \n' + ex) } if (doc) { self.sessionInfo = { timestamp: now, data: doc } /** SessionInfo update @event iracing#SessionInfo @type Object @example * iracing.on('SessionInfo', function (evt) { * console.log(evt) * }) */ self.emit('update', {type: 'SessionInfo', data: self.sessionInfo.data, timestamp: now}) } }) } }, opts.sessionInfoUpdateInterval) /** any update event @event iracing#update @type Object @example * iracing.on('update', function (evt) { * console.log(evt) * }) */ self.on('update', evt => { // fire old events as well. const timestamp = evt.timestamp const data = evt.data const type = evt.type switch (type) { case 'SessionInfo': self.emit('SessionInfo', {timestamp, data}) break case 'Telemetry': self.emit('Telemetry', {timestamp, values: data}) break case 'TelemetryDescription': self.emit('TelemetryDescription', data) break case 'Connected': self.emit('Connected') break case 'Disconnected': self.emit('Disconnected') break default: break } }) /** Stops JsIrSdk, no new events are fired after calling this @method @private */ this._stop = function () { clearInterval(telemetryIntervalId) clearInterval(sessionInfoIntervalId) clearInterval(startIntervalId) IrSdkWrapper.shutdown() } /** pad car number @function @private */ function padCarNum (numStr) { if (typeof numStr === 'string') { var num = parseInt(numStr) var zeros = numStr.length - num.toString().length if (!zeros) return num var numPlaces = 1 if (num > 9) numPlaces = 2 if (num > 99) numPlaces = 3 return (numPlaces + zeros) * 1000 + num } } } util.inherits(JsIrSdk, events.EventEmitter) module.exports = JsIrSdk function stringToEnum (input, enumObj) { var enumKey = Object.keys(enumObj).find(function (key) { return key.toLowerCase() === input.toLowerCase() }) if (enumKey) { return enumObj[enumKey] } }