UNPKG

node-irsdk-mjo

Version:

iRacing SDK implementation for nodejs

522 lines (473 loc) 17.4 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, "''") + "'"; } }); fixedYamlStr = fixedYamlStr.replace(/[\x80-\x9f]+/g, ""); return yaml.load(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]; } }