├── .github └── workflows │ ├── build-test.yml │ └── npm-publish.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── binding.gyp ├── doc ├── README.hbs └── examples.hbs ├── examples ├── cam-example.js ├── print-names.js ├── recorder.js └── sample-writer.js ├── package.json ├── prettier.config.js ├── sample-data ├── sessioninfo.json ├── telemetry-desc.json └── telemetry.json ├── src ├── JsIrSdk.js ├── consts │ └── IrSdkConsts.js ├── cpp │ ├── IRSDKWrapper.cpp │ ├── IRSDKWrapper.h │ ├── IrSdkBindingHelpers.cpp │ ├── IrSdkBindingHelpers.h │ ├── IrSdkCommand.cpp │ ├── IrSdkCommand.h │ ├── IrSdkNodeBindings.cpp │ ├── IrSdkNodeBindings.h │ ├── irsdk │ │ └── irsdk_defines.h │ └── platform │ │ ├── platform.cpp │ │ └── platform.h ├── iracing-sdk-js.js └── utils │ ├── createSessionInfoParser.js │ ├── padCarNum.js │ └── stringToEnum.js ├── test ├── IrsdkNodeWrapper-stub.js ├── JsIrSdk-spec.js ├── iracing-sdk-js-spec.js ├── smoke-test.js └── utils │ └── stringToEnum-spec.js └── yarn.lock /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: windows-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Use Node.js 21.x 12 | uses: actions/setup-node@v3 13 | with: 14 | node-version: '21.x' 15 | - name: Use Python 3.12 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: 3.12 19 | env: 20 | PYTHON_VERSION: 3.12 21 | - name: Set Windows Env 22 | if: runner.os == 'Windows' 23 | run: | 24 | echo 'GYP_MSVS_VERSION=2015' >> $Env:GITHUB_ENV 25 | echo 'GYP_MSVS_OVERRIDE_PATH=C:\\Dummy' >> $Env:GITHUB_ENV 26 | - run: yarn --frozen-lockfile 27 | - run: yarn test 28 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: windows-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Use Node.js 21.x 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: '21.x' 19 | - name: Use Python 3.12 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: 3.12 23 | env: 24 | PYTHON_VERSION: 3.12 25 | - name: Set Windows Env 26 | if: runner.os == 'Windows' 27 | run: | 28 | echo 'GYP_MSVS_VERSION=2015' >> $Env:GITHUB_ENV 29 | echo 'GYP_MSVS_OVERRIDE_PATH=C:\\Dummy' >> $Env:GITHUB_ENV 30 | - run: yarn --frozen-lockfile 31 | - run: yarn test 32 | 33 | publish-npm: 34 | needs: build 35 | runs-on: windows-latest 36 | steps: 37 | - uses: actions/checkout@v4 38 | - name: Use Node.js 21.x 39 | uses: actions/setup-node@v3 40 | with: 41 | node-version: '21.x' 42 | registry-url: 'https://registry.npmjs.org' 43 | - name: Use Python 3.12 44 | uses: actions/setup-python@v4 45 | with: 46 | python-version: 3.12 47 | env: 48 | PYTHON_VERSION: 3.12 49 | - name: Set Windows Env 50 | if: runner.os == 'Windows' 51 | run: | 52 | echo 'GYP_MSVS_VERSION=2015' >> $Env:GITHUB_ENV 53 | echo 'GYP_MSVS_OVERRIDE_PATH=C:\\Dummy' >> $Env:GITHUB_ENV 54 | - run: yarn --frozen-lockfile 55 | - run: yarn test 56 | - run: npm ci 57 | - run: npm publish --provenance --access public 58 | env: 59 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio 2 | 3 | *.njsproj 4 | *.vcxproj.filters 5 | *.sln 6 | *.vcxproj 7 | .ntvs_analysis.dat 8 | *.sdf 9 | *.suo 10 | *.vcxproj.user 11 | 12 | # Misc 13 | .vscode 14 | node_modules/ 15 | build/ 16 | prebuilds/ 17 | npm-debug.log 18 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Visual Studio 2 | 3 | *.njsproj 4 | *.vcxproj.filters 5 | *.sln 6 | *.vcxproj 7 | .ntvs_analysis.dat 8 | *.sdf 9 | *.suo 10 | *.vcxproj.user 11 | 12 | # Misc 13 | 14 | prebuilds/ 15 | build/ 16 | sample-data/ 17 | examples/ 18 | test/ 19 | doc/ 20 | .eslintrc 21 | npm-debug.log 22 | .vscode 23 | prettier.config.js 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Zachary Friss 4 | 5 | Original node-irsdk Copyright (c) 2015-2017 Antti Pihlaja 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iracing-sdk-js 2 | 3 | (Another) Unofficial [iRacing](http://www.iracing.com/) SDK implementation for Node.js. 4 | 5 | [![npm version](https://img.shields.io/npm/v/iracing-sdk-js.svg)](https://www.npmjs.com/package/iracing-sdk-js) 6 | 7 | **iracing-sdk-js** provides data access (live telemetry and session info) and most of the available commands. You can find some usage examples in the [examples](https://github.com/friss/iracing-sdk-js/tree/main/examples) directory, and there are some [data samples](https://github.com/friss/iracing-sdk-js/tree/main/sample-data) too. 8 | 9 | ## Installing 10 | 11 | Make sure you are running [Node.js](https://nodejs.org/) v21 x64 or later. (Currently requires Node 21 for latest node test runner implementation, but should work with Node 20 too.) 12 | 13 | `npm install --save iracing-sdk-js` 14 | 15 | `yarn add iracing-sdk-js --save` 16 | 17 | 18 | ## API documentation 19 | 20 | 21 | 22 | ### irsdk 23 | 24 | * [irsdk](#module_irsdk) 25 | * [.init([opts])](#module_irsdk.init) ⇒ [iracing](#iracing) 26 | * [.getInstance()](#module_irsdk.getInstance) ⇒ [iracing](#iracing) 27 | 28 | 29 | 30 | #### irsdk.init([opts]) ⇒ [iracing](#iracing) 31 | Initialize JsIrSdk, can be done once before using getInstance first time. 32 | 33 | **Kind**: static method of [irsdk](#module_irsdk) 34 | **Returns**: [iracing](#iracing) - Running instance of JsIrSdk 35 | 36 | | Param | Type | Default | Description | 37 | | --- | --- | --- | --- | 38 | | [opts] | Object | | Options | 39 | | [opts.telemetryUpdateInterval] | Integer | 0 | Telemetry update interval, milliseconds | 40 | | [opts.sessionInfoUpdateInterval] | Integer | 0 | SessionInfo update interval, milliseconds | 41 | | [opts.sessionInfoParser] | [sessionInfoParser](#iracing..sessionInfoParser) | | Custom parser for session info | 42 | 43 | 44 | ```js 45 | const irsdk = require('iracing-sdk-js') // look for telemetry updates only once per 100 ms const iracing = irsdk.init({telemetryUpdateInterval: 100}) 46 | ``` 47 | 48 | 49 | #### irsdk.getInstance() ⇒ [iracing](#iracing) 50 | Get initialized instance of JsIrSdk 51 | 52 | **Kind**: static method of [irsdk](#module_irsdk) 53 | **Returns**: [iracing](#iracing) - Running instance of JsIrSdk 54 | 55 | ```js 56 | const irsdk = require('iracing-sdk-js') const iracing = irsdk.getInstance() 57 | ``` 58 | 59 | 60 | ### iracing ⇐ events.EventEmitter 61 | JsIrSdk is javascript implementation of iRacing SDK. 62 | 63 | Don't use constructor directly, use [getInstance](#module_irsdk.getInstance). 64 | 65 | **Kind**: global class 66 | **Extends**: events.EventEmitter 67 | **Emits**: [Connected](#iracing+event_Connected), [Disconnected](#iracing+event_Disconnected), [Telemetry](#iracing+event_Telemetry), [TelemetryDescription](#iracing+event_TelemetryDescription), [SessionInfo](#iracing+event_SessionInfo) 68 | **See**: [EventEmitter API](https://nodejs.org/api/events.html#events_class_eventemitter) 69 | 70 | * [iracing](#iracing) ⇐ events.EventEmitter 71 | * [new JsIrSdk()](#new_iracing_new) 72 | * _instance_ 73 | * [.telemetry](#iracing+telemetry) 74 | * [.telemetryDescription](#iracing+telemetryDescription) 75 | * [.sessionInfo](#iracing+sessionInfo) 76 | * [.Consts](#iracing+Consts) : [IrSdkConsts](#IrSdkConsts) 77 | * [.camControls](#iracing+camControls) : Object 78 | * [.playbackControls](#iracing+playbackControls) : Object 79 | * [.execCmd(msgId, [arg1], [arg2], [arg3])](#iracing+execCmd) 80 | * [.reloadTextures()](#iracing+reloadTextures) 81 | * [.reloadTexture(carIdx)](#iracing+reloadTexture) 82 | * [.execChatCmd(cmd, [arg])](#iracing+execChatCmd) 83 | * [.execChatMacro(num)](#iracing+execChatMacro) 84 | * [.execPitCmd(cmd, [arg])](#iracing+execPitCmd) 85 | * [.execTelemetryCmd(cmd)](#iracing+execTelemetryCmd) 86 | * ["TelemetryDescription"](#iracing+event_TelemetryDescription) 87 | * ["Telemetry"](#iracing+event_Telemetry) 88 | * ["SessionInfo"](#iracing+event_SessionInfo) 89 | * ["update"](#iracing+event_update) 90 | * ["Connected"](#iracing+event_Connected) 91 | * ["Disconnected"](#iracing+event_Disconnected) 92 | * _inner_ 93 | * [~sessionInfoParser](#iracing..sessionInfoParser) ⇒ Object 94 | 95 | 96 | 97 | #### new JsIrSdk() 98 | 99 | ```js 100 | const iracing = require('iracing-sdk-js').getInstance() 101 | ``` 102 | 103 | 104 | #### iracing.telemetry 105 | Latest telemetry, may be null or undefined 106 | 107 | **Kind**: instance property of [iracing](#iracing) 108 | 109 | 110 | #### iracing.telemetryDescription 111 | Latest telemetry, may be null or undefined 112 | 113 | **Kind**: instance property of [iracing](#iracing) 114 | 115 | 116 | #### iracing.sessionInfo 117 | Latest telemetry, may be null or undefined 118 | 119 | **Kind**: instance property of [iracing](#iracing) 120 | 121 | 122 | #### iracing.Consts : [IrSdkConsts](#IrSdkConsts) 123 | iRacing SDK related constants 124 | 125 | **Kind**: instance property of [iracing](#iracing) 126 | 127 | 128 | #### iracing.camControls : Object 129 | Camera controls 130 | 131 | **Kind**: instance property of [iracing](#iracing) 132 | 133 | 134 | #### iracing.playbackControls : Object 135 | Replay and playback controls 136 | 137 | **Kind**: instance property of [iracing](#iracing) 138 | 139 | 140 | #### iracing.execCmd(msgId, [arg1], [arg2], [arg3]) 141 | Execute any of available commands, excl. FFB command 142 | 143 | **Kind**: instance method of [iracing](#iracing) 144 | 145 | | Param | Type | Description | 146 | | --- | --- | --- | 147 | | msgId | Integer | Message id | 148 | | [arg1] | Integer | 1st argument | 149 | | [arg2] | Integer | 2nd argument | 150 | | [arg3] | Integer | 3rd argument | 151 | 152 | 153 | 154 | #### iracing.reloadTextures() 155 | Reload all car textures 156 | 157 | **Kind**: instance method of [iracing](#iracing) 158 | 159 | ```js 160 | iracing.reloadTextures() // reload all paints 161 | ``` 162 | 163 | 164 | #### iracing.reloadTexture(carIdx) 165 | Reload car's texture 166 | 167 | **Kind**: instance method of [iracing](#iracing) 168 | 169 | | Param | Type | Description | 170 | | --- | --- | --- | 171 | | carIdx | Integer | car to reload | 172 | 173 | 174 | ```js 175 | iracing.reloadTexture(1) // reload paint of carIdx=1 176 | ``` 177 | 178 | 179 | #### iracing.execChatCmd(cmd, [arg]) 180 | Execute chat command 181 | 182 | **Kind**: instance method of [iracing](#iracing) 183 | 184 | | Param | Type | Description | 185 | | --- | --- | --- | 186 | | cmd | [ChatCommand](#IrSdkConsts.ChatCommand) | | 187 | | [arg] | Integer | Command argument, if needed | 188 | 189 | 190 | ```js 191 | iracing.execChatCmd('cancel') // close chat window 192 | ``` 193 | 194 | 195 | #### iracing.execChatMacro(num) 196 | Execute chat macro 197 | 198 | **Kind**: instance method of [iracing](#iracing) 199 | 200 | | Param | Type | Description | 201 | | --- | --- | --- | 202 | | num | Integer | Macro's number (0-15) | 203 | 204 | 205 | ```js 206 | iracing.execChatMacro(1) // macro 1 207 | ``` 208 | 209 | 210 | #### iracing.execPitCmd(cmd, [arg]) 211 | Execute pit command 212 | 213 | **Kind**: instance method of [iracing](#iracing) 214 | 215 | | Param | Type | Description | 216 | | --- | --- | --- | 217 | | cmd | [PitCommand](#IrSdkConsts.PitCommand) | | 218 | | [arg] | Integer | Command argument, if needed | 219 | 220 | 221 | ```js 222 | // 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 223 | ``` 224 | 225 | 226 | #### iracing.execTelemetryCmd(cmd) 227 | Control telemetry logging (ibt file) 228 | 229 | **Kind**: instance method of [iracing](#iracing) 230 | 231 | | Param | Type | Description | 232 | | --- | --- | --- | 233 | | cmd | [TelemCommand](#IrSdkConsts.TelemCommand) | Command: start/stop/restart | 234 | 235 | 236 | ```js 237 | iracing.execTelemetryCmd('restart') 238 | ``` 239 | 240 | 241 | #### "TelemetryDescription" 242 | Telemetry description, contains description of available telemetry values 243 | 244 | **Kind**: event emitted by [iracing](#iracing) 245 | 246 | ```js 247 | iracing.on('TelemetryDescription', function (data) { console.log(evt) }) 248 | ``` 249 | 250 | 251 | #### "Telemetry" 252 | Telemetry update 253 | 254 | **Kind**: event emitted by [iracing](#iracing) 255 | 256 | ```js 257 | iracing.on('Telemetry', function (evt) { console.log(evt) }) 258 | ``` 259 | 260 | 261 | #### "SessionInfo" 262 | SessionInfo update 263 | 264 | **Kind**: event emitted by [iracing](#iracing) 265 | 266 | ```js 267 | iracing.on('SessionInfo', function (evt) { console.log(evt) }) 268 | ``` 269 | 270 | 271 | #### "update" 272 | any update event 273 | 274 | **Kind**: event emitted by [iracing](#iracing) 275 | 276 | ```js 277 | iracing.on('update', function (evt) { console.log(evt) }) 278 | ``` 279 | 280 | 281 | #### "Connected" 282 | iRacing, sim, is started 283 | 284 | **Kind**: event emitted by [iracing](#iracing) 285 | 286 | ```js 287 | iracing.on('Connected', function (evt) { console.log(evt) }) 288 | ``` 289 | 290 | 291 | #### "Disconnected" 292 | iRacing, sim, was closed 293 | 294 | **Kind**: event emitted by [iracing](#iracing) 295 | 296 | ```js 297 | iracing.on('Disconnected', function (evt) { console.log(evt) }) 298 | ``` 299 | 300 | 301 | #### iracing~sessionInfoParser ⇒ Object 302 | Parser for SessionInfo YAML 303 | 304 | **Kind**: inner typedef of [iracing](#iracing) 305 | **Returns**: Object - parsed session info 306 | 307 | | Param | Type | Description | 308 | | --- | --- | --- | 309 | | sessionInfo | String | SessionInfo YAML | 310 | 311 | 312 | 313 | ### IrSdkConsts 314 | IrSdkConsts, iRacing SDK constants/enums. 315 | 316 | **Kind**: global constant 317 | 318 | ```js 319 | var IrSdkConsts = require('node-irsdk').getInstance().Consts 320 | ``` 321 | 322 | * [IrSdkConsts](#IrSdkConsts) 323 | * [.BroadcastMsg](#IrSdkConsts.BroadcastMsg) 324 | * [.CameraState](#IrSdkConsts.CameraState) 325 | * [.RpyPosMode](#IrSdkConsts.RpyPosMode) 326 | * [.RpySrchMode](#IrSdkConsts.RpySrchMode) 327 | * [.RpyStateMode](#IrSdkConsts.RpyStateMode) 328 | * [.ReloadTexturesMode](#IrSdkConsts.ReloadTexturesMode) 329 | * [.ChatCommand](#IrSdkConsts.ChatCommand) 330 | * [.PitCommand](#IrSdkConsts.PitCommand) 331 | * [.TelemCommand](#IrSdkConsts.TelemCommand) 332 | * [.CamFocusAt](#IrSdkConsts.CamFocusAt) 333 | 334 | 335 | 336 | #### IrSdkConsts.BroadcastMsg 337 | Available command messages. 338 | 339 | **Kind**: static enum of [IrSdkConsts](#IrSdkConsts) 340 | **Properties** 341 | 342 | | Name | Default | Description | 343 | | --- | --- | --- | 344 | | CamSwitchPos | 0 | Switch cam, args: car position, group, camera | 345 | | CamSwitchNum | 1 | Switch cam, args, driver #, group, camera | 346 | | CamSetState | 2 | Set cam state, args: CameraState, unused, unused | 347 | | ReplaySetPlaySpeed | 3 | Set replay speed, args: speed, slowMotion, unused | 348 | | ReplaySetPlayPosition | 4 | Jump to frame, args: RpyPosMode, Frame Number (high, low) | 349 | | ReplaySearch | 5 | Search things from replay, args: RpySrchMode, unused, unused | 350 | | ReplaySetState | 6 | Set replay state, args: RpyStateMode, unused, unused | 351 | | ReloadTextures | 7 | Reload textures, args: ReloadTexturesMode, carIdx, unused | 352 | | ChatComand | 8 | Chat commands, args: ChatCommand, subCommand, unused | 353 | | PitCommand | 9 | Pit commands, args: PitCommand, parameter | 354 | | TelemCommand | 10 | Disk telemetry commands, args: TelemCommand, unused, unused | 355 | | FFBCommand | 11 | *not supported by node-irsdk**: Change FFB settings, args: FFBCommandMode, value (float, high, low) | 356 | | ReplaySearchSessionTime | 12 | Search by timestamp, args: sessionNum, sessionTimeMS (high, low) | 357 | 358 | 359 | 360 | #### IrSdkConsts.CameraState 361 | Camera state 362 | Camera state is bitfield; use these values to compose a new state. 363 | 364 | **Kind**: static enum of [IrSdkConsts](#IrSdkConsts) 365 | **Properties** 366 | 367 | | Name | Default | Description | 368 | | --- | --- | --- | 369 | | IsSessionScreen | 1 | Is driver out of the car | 370 | | IsScenicActive | 2 | The scenic camera is active (no focus car) | 371 | | CamToolActive | 4 | Activate camera tool | 372 | | UIHidden | 8 | Hide UI | 373 | | UseAutoShotSelection | 16 | Enable auto shot selection | 374 | | UseTemporaryEdits | 32 | Enable temporary edits | 375 | | UseKeyAcceleration | 64 | Enable key acceleration | 376 | | UseKey10xAcceleration | 128 | Enable 10x key acceleration | 377 | | UseMouseAimMode | 256 | Enable mouse aim | 378 | 379 | 380 | 381 | #### IrSdkConsts.RpyPosMode 382 | **Kind**: static enum of [IrSdkConsts](#IrSdkConsts) 383 | **Properties** 384 | 385 | | Name | Default | Description | 386 | | --- | --- | --- | 387 | | Begin | 0 | Frame number is relative to beginning | 388 | | Current | 1 | Frame number is relative to current frame | 389 | | End | 2 | Frame number is relative to end | 390 | 391 | 392 | 393 | #### IrSdkConsts.RpySrchMode 394 | **Kind**: static enum of [IrSdkConsts](#IrSdkConsts) 395 | **Properties** 396 | 397 | | Name | Default | 398 | | --- | --- | 399 | | ToStart | 0 | 400 | | ToEnd | 1 | 401 | | PrevSession | 2 | 402 | | NextSession | 3 | 403 | | PrevLap | 4 | 404 | | NextLap | 5 | 405 | | PrevFrame | 6 | 406 | | NextFrame | 7 | 407 | | PrevIncident | 8 | 408 | | NextIncident | 9 | 409 | 410 | 411 | 412 | #### IrSdkConsts.RpyStateMode 413 | **Kind**: static enum of [IrSdkConsts](#IrSdkConsts) 414 | **Properties** 415 | 416 | | Name | Default | Description | 417 | | --- | --- | --- | 418 | | EraseTape | 0 | Clear any data in the replay tape (works only if replay spooling disabled) | 419 | 420 | 421 | 422 | #### IrSdkConsts.ReloadTexturesMode 423 | **Kind**: static enum of [IrSdkConsts](#IrSdkConsts) 424 | **Properties** 425 | 426 | | Name | Default | 427 | | --- | --- | 428 | | All | 0 | 429 | | CarIdx | 1 | 430 | 431 | 432 | 433 | #### IrSdkConsts.ChatCommand 434 | **Kind**: static enum of [IrSdkConsts](#IrSdkConsts) 435 | **Properties** 436 | 437 | | Name | Default | Description | 438 | | --- | --- | --- | 439 | | Macro | 0 | Macro, give macro num (0-15) as argument | 440 | | BeginChat | 1 | Open up a new chat window | 441 | | Reply | 2 | Reply to last private chat | 442 | | Cancel | 3 | Close chat window | 443 | 444 | 445 | 446 | #### IrSdkConsts.PitCommand 447 | **Kind**: static enum of [IrSdkConsts](#IrSdkConsts) 448 | **Properties** 449 | 450 | | Name | Default | Description | 451 | | --- | --- | --- | 452 | | Clear | 0 | Clear all pit checkboxes | 453 | | WS | 1 | Clean the winshield, using one tear off | 454 | | Fuel | 2 | Request fuel, optional argument: liters | 455 | | LF | 3 | Request new left front, optional argument: pressure in kPa | 456 | | RF | 4 | Request new right front, optional argument: pressure in kPa | 457 | | LR | 5 | Request new left rear, optional argument: pressure in kPa | 458 | | RR | 6 | Request new right rear, optional argument: pressure in kPa | 459 | | ClearTires | 7 | Clear tire pit checkboxes | 460 | | FR | 8 | Request a fast repair | 461 | | ClearWS | 9 | Disable clear windshield | 462 | | ClearFR | 10 | Disable fast repair | 463 | | ClearFuel | 11 | Disable refueling | 464 | 465 | 466 | 467 | #### IrSdkConsts.TelemCommand 468 | **Kind**: static enum of [IrSdkConsts](#IrSdkConsts) 469 | **Properties** 470 | 471 | | Name | Default | Description | 472 | | --- | --- | --- | 473 | | Stop | 0 | Turn telemetry recording off | 474 | | Start | 1 | Turn telemetry recording on | 475 | | Restart | 2 | Write current file to disk and start a new one | 476 | 477 | 478 | 479 | #### IrSdkConsts.CamFocusAt 480 | When switching camera, these can be used instead of car number / position 481 | 482 | **Kind**: static enum of [IrSdkConsts](#IrSdkConsts) 483 | **Properties** 484 | 485 | | Name | Default | Description | 486 | | --- | --- | --- | 487 | | Incident | -3 | | 488 | | Leader | -2 | | 489 | | Exciting | -1 | | 490 | | Driver | 0 | Use car number / position instead of this | 491 | 492 | 493 | 494 | ### setState(state) 495 | Change camera tool state 496 | 497 | **Kind**: global function 498 | 499 | | Param | Type | Description | 500 | | --- | --- | --- | 501 | | state | [CameraState](#IrSdkConsts.CameraState) | new state | 502 | 503 | 504 | ```js 505 | // hide UI and enable mouse aim var States = iracing.Consts.CameraState var state = States.CamToolActive | States.UIHidden | States.UseMouseAimMode iracing.camControls.setState(state) 506 | ``` 507 | 508 | 509 | ### switchToCar(carNum, [camGroupNum], [camNum]) 510 | Switch camera, focus on car 511 | 512 | **Kind**: global function 513 | 514 | | Param | Type | Description | 515 | | --- | --- | --- | 516 | | carNum | Integer \| String \| [CamFocusAt](#IrSdkConsts.CamFocusAt) | Car to focus on | 517 | | [camGroupNum] | Integer | Select camera group | 518 | | [camNum] | Integer | Select camera | 519 | 520 | 521 | ```js 522 | // show car #2 iracing.camControls.switchToCar(2) 523 | 524 | ``` 525 | 526 | ```js 527 | // show car #02 iracing.camControls.switchToCar('02') 528 | 529 | ``` 530 | 531 | ```js 532 | // show leader iracing.camControls.switchToCar('leader') 533 | 534 | ``` 535 | 536 | ```js 537 | // show car #2 using cam group 3 iracing.camControls.switchToCar(2, 3) 538 | ``` 539 | 540 | 541 | ### switchToPos(position, [camGroupNum], [camNum]) 542 | Switch camera, focus on position 543 | 544 | **Kind**: global function 545 | 546 | | Param | Type | Description | 547 | | --- | --- | --- | 548 | | position | Integer \| [CamFocusAt](#IrSdkConsts.CamFocusAt) | Position to focus on | 549 | | [camGroupNum] | Integer | Select camera group | 550 | | [camNum] | Integer | Select camera | 551 | 552 | 553 | ```js 554 | iracing.camControls.switchToPos(2) // show P2 555 | ``` 556 | 557 | 558 | ### play() 559 | Play replay 560 | 561 | **Kind**: global function 562 | 563 | ```js 564 | iracing.playbackControls.play() 565 | ``` 566 | 567 | 568 | ### pause() 569 | Pause replay 570 | 571 | **Kind**: global function 572 | 573 | ```js 574 | iracing.playbackControls.pause() 575 | ``` 576 | 577 | 578 | ### fastForward([speed]) 579 | fast-forward replay 580 | 581 | **Kind**: global function 582 | 583 | | Param | Type | Default | Description | 584 | | --- | --- | --- | --- | 585 | | [speed] | Integer | 2 | FF speed, something between 2-16 works | 586 | 587 | 588 | ```js 589 | iracing.playbackControls.fastForward() // double speed FF 590 | ``` 591 | 592 | 593 | ### rewind([speed]) 594 | rewind replay 595 | 596 | **Kind**: global function 597 | 598 | | Param | Type | Default | Description | 599 | | --- | --- | --- | --- | 600 | | [speed] | Integer | 2 | RW speed, something between 2-16 works | 601 | 602 | 603 | ```js 604 | iracing.playbackControls.rewind() // double speed RW 605 | ``` 606 | 607 | 608 | ### slowForward([divider]) 609 | slow-forward replay, slow motion 610 | 611 | **Kind**: global function 612 | 613 | | Param | Type | Default | Description | 614 | | --- | --- | --- | --- | 615 | | [divider] | Integer | 2 | divider of speed, something between 2-17 works | 616 | 617 | 618 | ```js 619 | iracing.playbackControls.slowForward(2) // half speed 620 | ``` 621 | 622 | 623 | ### slowBackward([divider]) 624 | slow-backward replay, reverse slow motion 625 | 626 | **Kind**: global function 627 | 628 | | Param | Type | Default | Description | 629 | | --- | --- | --- | --- | 630 | | [divider] | Integer | 2 | divider of speed, something between 2-17 works | 631 | 632 | 633 | ```js 634 | iracing.playbackControls.slowBackward(2) // half speed RW 635 | ``` 636 | 637 | 638 | ### search(searchMode) 639 | Search things from replay 640 | 641 | **Kind**: global function 642 | 643 | | Param | Type | Description | 644 | | --- | --- | --- | 645 | | searchMode | [RpySrchMode](#IrSdkConsts.RpySrchMode) | what to search | 646 | 647 | 648 | ```js 649 | iracing.playbackControls.search('nextIncident') 650 | ``` 651 | 652 | 653 | ### searchTs(sessionNum, sessionTimeMS) 654 | Search timestamp 655 | 656 | **Kind**: global function 657 | 658 | | Param | Type | Description | 659 | | --- | --- | --- | 660 | | sessionNum | Integer | Session number | 661 | | sessionTimeMS | Integer | Session time in milliseconds | 662 | 663 | 664 | ```js 665 | // jump to 2nd minute of 3rd session iracing.playbackControls.searchTs(2, 2*60*1000) 666 | ``` 667 | 668 | 669 | ### searchFrame(frameNum, rpyPosMode) 670 | Go to frame. Frame counting can be relative to begin, end or current. 671 | 672 | **Kind**: global function 673 | 674 | | Param | Type | Description | 675 | | --- | --- | --- | 676 | | frameNum | Integer | Frame number | 677 | | rpyPosMode | [RpyPosMode](#IrSdkConsts.RpyPosMode) | Is frame number relative to begin, end or current frame | 678 | 679 | 680 | ```js 681 | iracing.playbackControls.searchFrame(1, 'current') // go to 1 frame forward 682 | ``` 683 | 684 | 685 | ## Development 686 | 687 | To develop `iracing-sdk-js` itself, you need working working installation of [node-gyp](https://github.com/nodejs/node-gyp#on-windows). 688 | 689 | Useful commands: 690 | 691 | * `yarn rebuild` rebuilds binary addon 692 | * `yarn test` runs mocked tests 693 | * `yarn smoke-tests` runs tests that requires iRacing to be running 694 | * `yarn ready` runs all tests and updates docs 695 | 696 | ## License 697 | 698 | Released under the [MIT License](https://github.com/friss/iracing-sdk-js/blob/main/LICENSE.md). 699 | 700 | 701 | ## Credits 702 | Originally based on [node-irsdk](https://github.com/apihlaja/node-irsdk) by [apihlaja](https://github.com/apihlaja). 703 | 704 | Parts of original irsdk used, license available here: https://github.com/friss/iracing-sdk-js/blob/main/src/cpp/irsdk/irsdk_defines.h (BSD-3-Clause) 705 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "IrSdkNodeBindings", 5 | "cflags": [ 6 | "-Wall", 7 | "-std=c++17" 8 | ], 9 | 10 | "sources": [ 11 | "src/cpp/IrSdkNodeBindings.cpp", 12 | "src/cpp/IrSdkCommand.cpp", 13 | "src/cpp/IRSDKWrapper.cpp", 14 | "src/cpp/IrSdkBindingHelpers.cpp", 15 | "src/cpp/platform/platform.cpp" 16 | ], 17 | "include_dirs": [ 18 | "main}} 21 | 22 | 23 | ## Development 24 | 25 | To develop `iracing-sdk-js` itself, you need working working installation of [node-gyp](https://github.com/nodejs/node-gyp#on-windows). 26 | 27 | Useful commands: 28 | 29 | * `yarn rebuild` rebuilds binary addon 30 | * `yarn test` runs mocked tests 31 | * `yarn smoke-tests` runs tests that requires iRacing to be running 32 | * `yarn ready` runs all tests and updates docs 33 | 34 | ## License 35 | 36 | Released under the [MIT License](https://github.com/friss/iracing-sdk-js/blob/main/LICENSE.md). 37 | 38 | 39 | ## Credits 40 | Originally based on [node-irsdk](https://github.com/apihlaja/node-irsdk) by [apihlaja](https://github.com/apihlaja). 41 | 42 | Parts of original irsdk used, license available here: https://github.com/friss/iracing-sdk-js/blob/main/src/cpp/irsdk/irsdk_defines.h (BSD-3-Clause) 43 | -------------------------------------------------------------------------------- /doc/examples.hbs: -------------------------------------------------------------------------------- 1 | {{#examples}} 2 | {{#if caption}} **Example:** {{caption}} {{else}} {{/if}} 3 | {{{inlineLinks example}}} 4 | {{/examples}} 5 | -------------------------------------------------------------------------------- /examples/cam-example.js: -------------------------------------------------------------------------------- 1 | // camera / command example 2 | 3 | const irsdk = require('../'); 4 | 5 | irsdk.init({ 6 | telemetryUpdateInterval: 1000, 7 | sessionInfoUpdateInterval: 1000, 8 | }); 9 | 10 | const iracing = irsdk.getInstance(); 11 | 12 | console.log('\nwaiting for iRacing...'); 13 | 14 | iracing.on('Connected', function () { 15 | console.log('Connected to iRacing.'); 16 | 17 | iracing.once('Disconnected', function () { 18 | console.log('iRacing shut down.'); 19 | process.exit(); 20 | }); 21 | 22 | iracing.once('SessionInfo', function (sessionInfo) { 23 | console.log('SessionInfo event received\n'); 24 | 25 | // try to find rollbar cam group 26 | const camGroup = sessionInfo.data.CameraInfo.Groups.find( 27 | function (camGroup) { 28 | return camGroup.GroupName === 'Roll Bar'; 29 | } 30 | ); 31 | const camGroupNum = camGroup ? camGroup.GroupNum : 1; 32 | 33 | // loop thru top10, switch every 5 second 34 | const currentPosition = 1; 35 | 36 | setInterval(function () { 37 | console.log('showing P' + currentPosition); 38 | iracing.camControls.switchToPos(currentPosition++, camGroupNum, 0); 39 | 40 | if (currentPosition > 10) { 41 | currentPosition = 1; 42 | } 43 | }, 5000); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /examples/print-names.js: -------------------------------------------------------------------------------- 1 | // prints team names 2 | 3 | const irsdk = require('../'); 4 | 5 | irsdk.init({ 6 | telemetryUpdateInterval: 1000, 7 | sessionInfoUpdateInterval: 1000, 8 | }); 9 | 10 | const iracing = irsdk.getInstance(); 11 | 12 | console.log('\nwaiting for iRacing...'); 13 | 14 | iracing.on('Connected', function () { 15 | console.log('\nConnected to iRacing.'); 16 | 17 | iracing.once('Disconnected', function () { 18 | console.log('iRacing shut down.'); 19 | }); 20 | 21 | iracing.once('SessionInfo', function (sessionInfo) { 22 | console.log('SessionInfo event received\n'); 23 | sessionInfo.data.DriverInfo.Drivers.forEach(function (driver) { 24 | console.log(driver.TeamName + ' - ' + driver.UserName); 25 | }); 26 | process.exit(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /examples/recorder.js: -------------------------------------------------------------------------------- 1 | const irsdk = require('../'); 2 | const fs = require('fs'); 3 | 4 | irsdk.init({ 5 | telemetryUpdateInterval: 100, 6 | sessionInfoUpdateInterval: 2000, 7 | }); 8 | 9 | const iracing = irsdk.getInstance(); 10 | 11 | function saveSample(type, time, data) { 12 | const fileName = './sample-data/rec/' + time + '-' + type + '.json'; 13 | fs.writeFile(fileName, JSON.stringify(data), function (err) { 14 | if (err) throw err; 15 | }); 16 | } 17 | 18 | console.log('waiting for iRacing...'); 19 | 20 | iracing.on('Connected', function () { 21 | console.log('connected to iRacing..'); 22 | }); 23 | 24 | iracing.on('Disconnected', function () { 25 | console.log('iRacing shut down, exiting.\n'); 26 | process.exit(); 27 | }); 28 | 29 | iracing.on('TelemetryDescription', function (data) { 30 | console.log('got TelemetryDescription'); 31 | 32 | saveSample('TelemetryDescription', Date.now(), data); 33 | }); 34 | 35 | iracing.on('Telemetry', function (data) { 36 | console.log('got Telemetry'); 37 | saveSample('Telemetry', Date.now(), data); 38 | }); 39 | 40 | iracing.on('SessionInfo', function (data) { 41 | console.log('got SessionInfo'); 42 | 43 | saveSample('SessionInfo', Date.now(), data); 44 | }); 45 | -------------------------------------------------------------------------------- /examples/sample-writer.js: -------------------------------------------------------------------------------- 1 | const irsdk = require('../'); 2 | const fs = require('fs'); 3 | 4 | // kill the process when enough is done.. 5 | const done = (function () { 6 | var tasks = []; 7 | var totalTasks = 3; 8 | 9 | return function (taskName) { 10 | tasks.push(taskName); 11 | if (tasks.length >= totalTasks) { 12 | console.log(); 13 | console.log('checks done', new Date()); 14 | process.exit(); 15 | } 16 | }; 17 | })(); 18 | 19 | irsdk.init({ 20 | telemetryUpdateInterval: 100, 21 | sessionInfoUpdateInterval: 100, 22 | }); 23 | 24 | const iracing = irsdk.getInstance(); 25 | 26 | console.log('waiting for iRacing...'); 27 | 28 | iracing.on('Connected', function () { 29 | console.log('connected to iRacing..'); 30 | }); 31 | 32 | iracing.on('Disconnected', function () { 33 | console.log('iRacing shut down.\n'); 34 | }); 35 | 36 | iracing.once('TelemetryDescription', function (data) { 37 | console.log('got TelemetryDescription'); 38 | const fileName = './sample-data/telemetry-desc.json'; 39 | 40 | fs.writeFile(fileName, JSON.stringify(data, null, 2), function (err) { 41 | if (err) throw err; 42 | done('TelemetryDescription'); 43 | }); 44 | }); 45 | 46 | iracing.once('Telemetry', function (data) { 47 | console.log('got Telemetry'); 48 | const fileName = './sample-data/telemetry.json'; 49 | 50 | fs.writeFile(fileName, JSON.stringify(data, null, 2), function (err) { 51 | if (err) throw err; 52 | done('Telemetry'); 53 | }); 54 | }); 55 | 56 | iracing.once('SessionInfo', function (data) { 57 | console.log('got SessionInfo'); 58 | const jsonFileName = './sample-data/sessioninfo.json'; 59 | 60 | fs.writeFile(jsonFileName, JSON.stringify(data, null, 2), function (err) { 61 | if (err) throw err; 62 | done('SessionInfo'); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iracing-sdk-js", 3 | "version": "1.4.0", 4 | "description": "iRacing SDK implementation for Node.js", 5 | "main": "src/iracing-sdk-js.js", 6 | "scripts": { 7 | "install": "yarn rebuild", 8 | "rebuild": "node-gyp rebuild", 9 | "smoke-test": "node test/smoke-test.js", 10 | "write-samples": "node examples/sample-writer.js", 11 | "test": "node --test test/**/*-spec.js", 12 | "doc": "jsdoc2md -d 3 -t doc/README.hbs --partial doc/examples.hbs -m none -g none src/iracing-sdk-js.js src/JsIrSdk.js src/consts/IrSdkConsts.js > README.md", 13 | "ready": "yarn rebuild && yarn test && yarn smoke-test && yarn write-samples && yarn doc" 14 | }, 15 | "keywords": [ 16 | "iracing" 17 | ], 18 | "author": "Zachary Friss", 19 | "license": "MIT", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/Friss/iracing-sdk-js" 23 | }, 24 | "engines": { 25 | "node": ">=21" 26 | }, 27 | "dependencies": { 28 | "js-yaml": "4.1.0", 29 | "nan": "2.22.0" 30 | }, 31 | "devDependencies": { 32 | "jsdoc-to-markdown": "9.1.1", 33 | "node-gyp": "11.0.0", 34 | "prettier": "3.4.2", 35 | "sandboxed-module": "2.0.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: true, 3 | printWidth: 80, 4 | proseWrap: 'never', 5 | semi: true, 6 | singleQuote: true, 7 | tabWidth: 2, 8 | trailingComma: 'es5', 9 | useTabs: false, 10 | }; 11 | -------------------------------------------------------------------------------- /sample-data/sessioninfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "2024-12-13T01:47:04.527Z", 3 | "data": { 4 | "WeekendInfo": { 5 | "TrackName": "virginia 2022 full", 6 | "TrackID": 465, 7 | "TrackLength": "5.22 km", 8 | "TrackLengthOfficial": "5.26 km", 9 | "TrackDisplayName": "Virginia International Raceway", 10 | "TrackDisplayShortName": "VIR", 11 | "TrackConfigName": "Full Course", 12 | "TrackCity": "Alton", 13 | "TrackCountry": "USA", 14 | "TrackAltitude": "166.18 m", 15 | "TrackLatitude": "36.568814 m", 16 | "TrackLongitude": "-79.206660 m", 17 | "TrackNorthOffset": "4.2700 rad", 18 | "TrackNumTurns": 20, 19 | "TrackPitSpeedLimit": "60.00 kph", 20 | "TrackType": "road course", 21 | "TrackDirection": "neutral", 22 | "TrackWeatherType": "Static", 23 | "TrackSkies": "Partly Cloudy", 24 | "TrackSurfaceTemp": "39.81 C", 25 | "TrackAirTemp": "25.56 C", 26 | "TrackAirPressure": "29.36 Hg", 27 | "TrackWindVel": "0.89 m/s", 28 | "TrackWindDir": "0.00 rad", 29 | "TrackRelativeHumidity": "45 %", 30 | "TrackFogLevel": "0 %", 31 | "TrackPrecipitation": "0 %", 32 | "TrackCleanup": 0, 33 | "TrackDynamicTrack": 1, 34 | "TrackVersion": "2024.11.26.01", 35 | "SeriesID": 0, 36 | "SeasonID": 0, 37 | "SessionID": 0, 38 | "SubSessionID": 0, 39 | "LeagueID": 0, 40 | "Official": 0, 41 | "RaceWeek": 0, 42 | "EventType": "Test", 43 | "Category": "Road", 44 | "SimMode": "full", 45 | "TeamRacing": 0, 46 | "MinDrivers": 0, 47 | "MaxDrivers": 0, 48 | "DCRuleSet": "None", 49 | "QualifierMustStartRace": 0, 50 | "NumCarClasses": 1, 51 | "NumCarTypes": 1, 52 | "HeatRacing": 0, 53 | "BuildType": "Release", 54 | "BuildTarget": "Members", 55 | "BuildVersion": "2024.12.11.01", 56 | "RaceFarm": null, 57 | "WeekendOptions": { 58 | "NumStarters": 0, 59 | "StartingGrid": "single file", 60 | "QualifyScoring": "best lap", 61 | "CourseCautions": "off", 62 | "StandingStart": 0, 63 | "ShortParadeLap": 0, 64 | "Restarts": "single file", 65 | "WeatherType": "Static", 66 | "Skies": "Partly Cloudy", 67 | "WindDirection": "N", 68 | "WindSpeed": "3.22 km/h", 69 | "WeatherTemp": "25.56 C", 70 | "RelativeHumidity": "45 %", 71 | "FogLevel": "0 %", 72 | "TimeOfDay": "12:00 pm", 73 | "Date": "2024-05-15T00:00:00.000Z", 74 | "EarthRotationSpeedupFactor": 1, 75 | "Unofficial": 1, 76 | "CommercialMode": "consumer", 77 | "NightMode": "variable", 78 | "IsFixedSetup": 0, 79 | "StrictLapsChecking": "default", 80 | "HasOpenRegistration": 0, 81 | "HardcoreLevel": 1, 82 | "NumJokerLaps": 0, 83 | "IncidentLimit": "unlimited", 84 | "FastRepairsLimit": "unlimited", 85 | "GreenWhiteCheckeredLimit": 0 86 | }, 87 | "TelemetryOptions": { 88 | "TelemetryDiskFile": "" 89 | } 90 | }, 91 | "SessionInfo": { 92 | "Sessions": [ 93 | { 94 | "SessionNum": 0, 95 | "SessionLaps": "unlimited", 96 | "SessionTime": "unlimited", 97 | "SessionNumLapsToAvg": 0, 98 | "SessionType": "Offline Testing", 99 | "SessionTrackRubberState": "moderately low usage", 100 | "SessionName": "TESTING", 101 | "SessionSubType": null, 102 | "SessionSkipped": 0, 103 | "SessionRunGroupsUsed": 0, 104 | "SessionEnforceTireCompoundChange": 0, 105 | "ResultsPositions": null, 106 | "ResultsFastestLap": [ 107 | { 108 | "CarIdx": 255, 109 | "FastestLap": 0, 110 | "FastestTime": -1 111 | } 112 | ], 113 | "ResultsAverageLapTime": -1, 114 | "ResultsNumCautionFlags": 0, 115 | "ResultsNumCautionLaps": 0, 116 | "ResultsNumLeadChanges": 0, 117 | "ResultsLapsComplete": -1, 118 | "ResultsOfficial": 0 119 | } 120 | ] 121 | }, 122 | "CameraInfo": { 123 | "Groups": [ 124 | { 125 | "GroupNum": 1, 126 | "GroupName": "Nose", 127 | "Cameras": [ 128 | { 129 | "CameraNum": 1, 130 | "CameraName": "CamNose" 131 | } 132 | ] 133 | }, 134 | { 135 | "GroupNum": 2, 136 | "GroupName": "Gearbox", 137 | "Cameras": [ 138 | { 139 | "CameraNum": 1, 140 | "CameraName": "CamGearbox" 141 | } 142 | ] 143 | }, 144 | { 145 | "GroupNum": 3, 146 | "GroupName": "Roll Bar", 147 | "Cameras": [ 148 | { 149 | "CameraNum": 1, 150 | "CameraName": "CamRoll Bar" 151 | } 152 | ] 153 | }, 154 | { 155 | "GroupNum": 4, 156 | "GroupName": "LF Susp", 157 | "Cameras": [ 158 | { 159 | "CameraNum": 1, 160 | "CameraName": "CamLF Susp" 161 | } 162 | ] 163 | }, 164 | { 165 | "GroupNum": 5, 166 | "GroupName": "LR Susp", 167 | "Cameras": [ 168 | { 169 | "CameraNum": 1, 170 | "CameraName": "CamLR Susp" 171 | } 172 | ] 173 | }, 174 | { 175 | "GroupNum": 6, 176 | "GroupName": "Gyro", 177 | "Cameras": [ 178 | { 179 | "CameraNum": 1, 180 | "CameraName": "CamGyro" 181 | } 182 | ] 183 | }, 184 | { 185 | "GroupNum": 7, 186 | "GroupName": "RF Susp", 187 | "Cameras": [ 188 | { 189 | "CameraNum": 1, 190 | "CameraName": "CamRF Susp" 191 | } 192 | ] 193 | }, 194 | { 195 | "GroupNum": 8, 196 | "GroupName": "RR Susp", 197 | "Cameras": [ 198 | { 199 | "CameraNum": 1, 200 | "CameraName": "CamRR Susp" 201 | } 202 | ] 203 | }, 204 | { 205 | "GroupNum": 9, 206 | "GroupName": "Cockpit", 207 | "Cameras": [ 208 | { 209 | "CameraNum": 1, 210 | "CameraName": "CamCockpit" 211 | } 212 | ] 213 | }, 214 | { 215 | "GroupNum": 10, 216 | "GroupName": "Scenic", 217 | "IsScenic": true, 218 | "Cameras": [ 219 | { 220 | "CameraNum": 1, 221 | "CameraName": "Scenic_07" 222 | }, 223 | { 224 | "CameraNum": 2, 225 | "CameraName": "Scenic_08" 226 | }, 227 | { 228 | "CameraNum": 3, 229 | "CameraName": "Scenic_09" 230 | }, 231 | { 232 | "CameraNum": 4, 233 | "CameraName": "Scenic_10" 234 | }, 235 | { 236 | "CameraNum": 5, 237 | "CameraName": "Scenic_02" 238 | }, 239 | { 240 | "CameraNum": 6, 241 | "CameraName": "Scenic_03" 242 | }, 243 | { 244 | "CameraNum": 7, 245 | "CameraName": "Scenic_05" 246 | }, 247 | { 248 | "CameraNum": 8, 249 | "CameraName": "Scenic_04" 250 | }, 251 | { 252 | "CameraNum": 9, 253 | "CameraName": "Scenic_06" 254 | }, 255 | { 256 | "CameraNum": 10, 257 | "CameraName": "Scenic_01" 258 | } 259 | ] 260 | }, 261 | { 262 | "GroupNum": 11, 263 | "GroupName": "TV1", 264 | "Cameras": [ 265 | { 266 | "CameraNum": 1, 267 | "CameraName": "CamTV1_00" 268 | }, 269 | { 270 | "CameraNum": 2, 271 | "CameraName": "CamTV1_01" 272 | }, 273 | { 274 | "CameraNum": 3, 275 | "CameraName": "CamTV1_02" 276 | }, 277 | { 278 | "CameraNum": 4, 279 | "CameraName": "CamTV1_03" 280 | }, 281 | { 282 | "CameraNum": 5, 283 | "CameraName": "CamTV1_05" 284 | }, 285 | { 286 | "CameraNum": 6, 287 | "CameraName": "CamTV1_04" 288 | }, 289 | { 290 | "CameraNum": 7, 291 | "CameraName": "CamTV1_06" 292 | }, 293 | { 294 | "CameraNum": 8, 295 | "CameraName": "CamTV1_07" 296 | }, 297 | { 298 | "CameraNum": 9, 299 | "CameraName": "CamTV1_08" 300 | }, 301 | { 302 | "CameraNum": 10, 303 | "CameraName": "CamTV1_09" 304 | } 305 | ] 306 | }, 307 | { 308 | "GroupNum": 12, 309 | "GroupName": "TV2", 310 | "Cameras": [ 311 | { 312 | "CameraNum": 1, 313 | "CameraName": "CamTV2_04" 314 | }, 315 | { 316 | "CameraNum": 2, 317 | "CameraName": "CamTV2_01" 318 | }, 319 | { 320 | "CameraNum": 3, 321 | "CameraName": "CamTV2_02" 322 | }, 323 | { 324 | "CameraNum": 4, 325 | "CameraName": "CamTV2_03" 326 | }, 327 | { 328 | "CameraNum": 5, 329 | "CameraName": "CamTV2_05" 330 | }, 331 | { 332 | "CameraNum": 6, 333 | "CameraName": "CamTV2_07" 334 | }, 335 | { 336 | "CameraNum": 7, 337 | "CameraName": "CamTV2_08" 338 | }, 339 | { 340 | "CameraNum": 8, 341 | "CameraName": "CamTV2_09" 342 | }, 343 | { 344 | "CameraNum": 9, 345 | "CameraName": "CamTV2_10" 346 | }, 347 | { 348 | "CameraNum": 10, 349 | "CameraName": "CamTV2_15" 350 | }, 351 | { 352 | "CameraNum": 11, 353 | "CameraName": "CamTV2_12" 354 | }, 355 | { 356 | "CameraNum": 12, 357 | "CameraName": "CamTV2_13" 358 | }, 359 | { 360 | "CameraNum": 13, 361 | "CameraName": "CamTV2_14" 362 | }, 363 | { 364 | "CameraNum": 14, 365 | "CameraName": "CamTV2_11" 366 | }, 367 | { 368 | "CameraNum": 15, 369 | "CameraName": "CamTV2_00" 370 | } 371 | ] 372 | }, 373 | { 374 | "GroupNum": 13, 375 | "GroupName": "TV3", 376 | "Cameras": [ 377 | { 378 | "CameraNum": 1, 379 | "CameraName": "CamTV3_03" 380 | }, 381 | { 382 | "CameraNum": 2, 383 | "CameraName": "CamTV3_01" 384 | }, 385 | { 386 | "CameraNum": 3, 387 | "CameraName": "CamTV3_02" 388 | }, 389 | { 390 | "CameraNum": 4, 391 | "CameraName": "CamTV3_04" 392 | }, 393 | { 394 | "CameraNum": 5, 395 | "CameraName": "CamTV3_05" 396 | }, 397 | { 398 | "CameraNum": 6, 399 | "CameraName": "CamTV3_06" 400 | }, 401 | { 402 | "CameraNum": 7, 403 | "CameraName": "CamTV3_07" 404 | }, 405 | { 406 | "CameraNum": 8, 407 | "CameraName": "CamTV3_08" 408 | }, 409 | { 410 | "CameraNum": 9, 411 | "CameraName": "CamTV3_09" 412 | }, 413 | { 414 | "CameraNum": 10, 415 | "CameraName": "CamTV3_10" 416 | } 417 | ] 418 | }, 419 | { 420 | "GroupNum": 14, 421 | "GroupName": "TV Static", 422 | "Cameras": [ 423 | { 424 | "CameraNum": 1, 425 | "CameraName": "CamTV4_00" 426 | }, 427 | { 428 | "CameraNum": 2, 429 | "CameraName": "CamTV4_01" 430 | }, 431 | { 432 | "CameraNum": 3, 433 | "CameraName": "CamTV4_02" 434 | }, 435 | { 436 | "CameraNum": 4, 437 | "CameraName": "CamTV4_03" 438 | }, 439 | { 440 | "CameraNum": 5, 441 | "CameraName": "CamTV4_04" 442 | }, 443 | { 444 | "CameraNum": 6, 445 | "CameraName": "CamTV4_05" 446 | }, 447 | { 448 | "CameraNum": 7, 449 | "CameraName": "CamTV4_06" 450 | }, 451 | { 452 | "CameraNum": 8, 453 | "CameraName": "CamTV4_08" 454 | }, 455 | { 456 | "CameraNum": 9, 457 | "CameraName": "CamTV4_07" 458 | }, 459 | { 460 | "CameraNum": 10, 461 | "CameraName": "CamTV4_10" 462 | }, 463 | { 464 | "CameraNum": 11, 465 | "CameraName": "CamTV4_09" 466 | }, 467 | { 468 | "CameraNum": 12, 469 | "CameraName": "CamTV4_11" 470 | }, 471 | { 472 | "CameraNum": 13, 473 | "CameraName": "CamTV4_12" 474 | }, 475 | { 476 | "CameraNum": 14, 477 | "CameraName": "CamTV4_13" 478 | }, 479 | { 480 | "CameraNum": 15, 481 | "CameraName": "CamTV4_14" 482 | }, 483 | { 484 | "CameraNum": 16, 485 | "CameraName": "CamTV4_15" 486 | }, 487 | { 488 | "CameraNum": 17, 489 | "CameraName": "CamTV4_16" 490 | }, 491 | { 492 | "CameraNum": 18, 493 | "CameraName": "CamTV4_17" 494 | } 495 | ] 496 | }, 497 | { 498 | "GroupNum": 15, 499 | "GroupName": "TV Mixed", 500 | "Cameras": [ 501 | { 502 | "CameraNum": 1, 503 | "CameraName": "CamTV3_09b" 504 | }, 505 | { 506 | "CameraNum": 2, 507 | "CameraName": "CamTV1_00" 508 | }, 509 | { 510 | "CameraNum": 3, 511 | "CameraName": "CamTV1_01" 512 | }, 513 | { 514 | "CameraNum": 4, 515 | "CameraName": "CamTV1_02" 516 | }, 517 | { 518 | "CameraNum": 5, 519 | "CameraName": "CamTV1_04" 520 | }, 521 | { 522 | "CameraNum": 6, 523 | "CameraName": "CamTV1_05" 524 | }, 525 | { 526 | "CameraNum": 7, 527 | "CameraName": "CamTV1_06" 528 | }, 529 | { 530 | "CameraNum": 8, 531 | "CameraName": "CamTV1_07" 532 | }, 533 | { 534 | "CameraNum": 9, 535 | "CameraName": "CamTV1_09" 536 | }, 537 | { 538 | "CameraNum": 10, 539 | "CameraName": "CamTV2_00" 540 | }, 541 | { 542 | "CameraNum": 11, 543 | "CameraName": "CamTV2_01" 544 | }, 545 | { 546 | "CameraNum": 12, 547 | "CameraName": "CamTV2_02" 548 | }, 549 | { 550 | "CameraNum": 13, 551 | "CameraName": "CamTV2_03" 552 | }, 553 | { 554 | "CameraNum": 14, 555 | "CameraName": "CamTV2_04" 556 | }, 557 | { 558 | "CameraNum": 15, 559 | "CameraName": "CamTV2_05" 560 | }, 561 | { 562 | "CameraNum": 16, 563 | "CameraName": "CamTV2_06" 564 | }, 565 | { 566 | "CameraNum": 17, 567 | "CameraName": "CamTV2_07" 568 | }, 569 | { 570 | "CameraNum": 18, 571 | "CameraName": "CamTV2_08" 572 | }, 573 | { 574 | "CameraNum": 19, 575 | "CameraName": "CamTV2_09" 576 | }, 577 | { 578 | "CameraNum": 20, 579 | "CameraName": "CamTV2_10" 580 | }, 581 | { 582 | "CameraNum": 21, 583 | "CameraName": "CamTV2_11" 584 | }, 585 | { 586 | "CameraNum": 22, 587 | "CameraName": "CamTV2_12" 588 | }, 589 | { 590 | "CameraNum": 23, 591 | "CameraName": "CamTV2_14" 592 | }, 593 | { 594 | "CameraNum": 24, 595 | "CameraName": "CamTV2_15" 596 | }, 597 | { 598 | "CameraNum": 25, 599 | "CameraName": "CamTV3_01" 600 | }, 601 | { 602 | "CameraNum": 26, 603 | "CameraName": "CamTV3_02" 604 | }, 605 | { 606 | "CameraNum": 27, 607 | "CameraName": "CamTV3_03" 608 | }, 609 | { 610 | "CameraNum": 28, 611 | "CameraName": "CamTV3_04" 612 | }, 613 | { 614 | "CameraNum": 29, 615 | "CameraName": "CamTV3_05" 616 | }, 617 | { 618 | "CameraNum": 30, 619 | "CameraName": "CamTV3_05b" 620 | }, 621 | { 622 | "CameraNum": 31, 623 | "CameraName": "CamTV3_06" 624 | }, 625 | { 626 | "CameraNum": 32, 627 | "CameraName": "CamTV3_07" 628 | }, 629 | { 630 | "CameraNum": 33, 631 | "CameraName": "CamTV3_07b" 632 | }, 633 | { 634 | "CameraNum": 34, 635 | "CameraName": "CamTV3_08" 636 | }, 637 | { 638 | "CameraNum": 35, 639 | "CameraName": "CamTV3_09" 640 | }, 641 | { 642 | "CameraNum": 36, 643 | "CameraName": "CamTV3_10" 644 | }, 645 | { 646 | "CameraNum": 37, 647 | "CameraName": "CamTV4_00" 648 | } 649 | ] 650 | }, 651 | { 652 | "GroupNum": 16, 653 | "GroupName": "Pit Lane", 654 | "Cameras": [ 655 | { 656 | "CameraNum": 1, 657 | "CameraName": "CamPit Lane" 658 | } 659 | ] 660 | }, 661 | { 662 | "GroupNum": 17, 663 | "GroupName": "Pit Lane 2", 664 | "Cameras": [ 665 | { 666 | "CameraNum": 1, 667 | "CameraName": "CamPit Lane 2" 668 | } 669 | ] 670 | }, 671 | { 672 | "GroupNum": 18, 673 | "GroupName": "Blimp", 674 | "Cameras": [ 675 | { 676 | "CameraNum": 1, 677 | "CameraName": "CamBlimp" 678 | } 679 | ] 680 | }, 681 | { 682 | "GroupNum": 19, 683 | "GroupName": "Chopper", 684 | "Cameras": [ 685 | { 686 | "CameraNum": 1, 687 | "CameraName": "CamChopper" 688 | } 689 | ] 690 | }, 691 | { 692 | "GroupNum": 20, 693 | "GroupName": "Chase", 694 | "Cameras": [ 695 | { 696 | "CameraNum": 1, 697 | "CameraName": "CamChase" 698 | } 699 | ] 700 | }, 701 | { 702 | "GroupNum": 21, 703 | "GroupName": "Far Chase", 704 | "Cameras": [ 705 | { 706 | "CameraNum": 1, 707 | "CameraName": "CamFar Chase" 708 | } 709 | ] 710 | }, 711 | { 712 | "GroupNum": 22, 713 | "GroupName": "Rear Chase", 714 | "Cameras": [ 715 | { 716 | "CameraNum": 1, 717 | "CameraName": "CamRear Chase" 718 | } 719 | ] 720 | } 721 | ] 722 | }, 723 | "RadioInfo": { 724 | "SelectedRadioNum": 0, 725 | "Radios": [ 726 | { 727 | "RadioNum": 0, 728 | "HopCount": 2, 729 | "NumFrequencies": 7, 730 | "TunedToFrequencyNum": 0, 731 | "ScanningIsOn": 1, 732 | "Frequencies": [ 733 | { 734 | "FrequencyNum": 0, 735 | "FrequencyName": "@ALLTEAMS", 736 | "Priority": 12, 737 | "CarIdx": -1, 738 | "EntryIdx": -1, 739 | "ClubID": 0, 740 | "CanScan": 1, 741 | "CanSquawk": 1, 742 | "Muted": 0, 743 | "IsMutable": 1, 744 | "IsDeletable": 0 745 | }, 746 | { 747 | "FrequencyNum": 1, 748 | "FrequencyName": "@DRIVERS", 749 | "Priority": 15, 750 | "CarIdx": -1, 751 | "EntryIdx": -1, 752 | "ClubID": 0, 753 | "CanScan": 1, 754 | "CanSquawk": 1, 755 | "Muted": 0, 756 | "IsMutable": 1, 757 | "IsDeletable": 0 758 | }, 759 | { 760 | "FrequencyNum": 2, 761 | "FrequencyName": "@TEAM", 762 | "Priority": 60, 763 | "CarIdx": 0, 764 | "EntryIdx": -1, 765 | "ClubID": 0, 766 | "CanScan": 1, 767 | "CanSquawk": 1, 768 | "Muted": 0, 769 | "IsMutable": 0, 770 | "IsDeletable": 0 771 | }, 772 | { 773 | "FrequencyNum": 3, 774 | "FrequencyName": "@CLUB", 775 | "Priority": 20, 776 | "CarIdx": -1, 777 | "EntryIdx": -1, 778 | "ClubID": 25, 779 | "CanScan": 1, 780 | "CanSquawk": 1, 781 | "Muted": 0, 782 | "IsMutable": 1, 783 | "IsDeletable": 0 784 | }, 785 | { 786 | "FrequencyNum": 4, 787 | "FrequencyName": "@ADMIN", 788 | "Priority": 90, 789 | "CarIdx": -1, 790 | "EntryIdx": -1, 791 | "ClubID": 0, 792 | "CanScan": 1, 793 | "CanSquawk": 1, 794 | "Muted": 0, 795 | "IsMutable": 0, 796 | "IsDeletable": 0 797 | }, 798 | { 799 | "FrequencyNum": 5, 800 | "FrequencyName": "@RACECONTROL", 801 | "Priority": 80, 802 | "CarIdx": -1, 803 | "EntryIdx": -1, 804 | "ClubID": 0, 805 | "CanScan": 1, 806 | "CanSquawk": 1, 807 | "Muted": 0, 808 | "IsMutable": 0, 809 | "IsDeletable": 0 810 | }, 811 | { 812 | "FrequencyNum": 6, 813 | "FrequencyName": "@PRIVATE", 814 | "Priority": 70, 815 | "CarIdx": -1, 816 | "EntryIdx": 0, 817 | "ClubID": 0, 818 | "CanScan": 1, 819 | "CanSquawk": 1, 820 | "Muted": 0, 821 | "IsMutable": 0, 822 | "IsDeletable": 0 823 | } 824 | ] 825 | } 826 | ] 827 | }, 828 | "DriverInfo": { 829 | "DriverCarIdx": 0, 830 | "DriverUserID": 697587, 831 | "PaceCarIdx": -1, 832 | "DriverHeadPosX": -0.105, 833 | "DriverHeadPosY": 0.325, 834 | "DriverHeadPosZ": 0.535, 835 | "DriverCarIsElectric": 0, 836 | "DriverCarIdleRPM": 1950, 837 | "DriverCarRedLine": 8000, 838 | "DriverCarEngCylinderCount": 8, 839 | "DriverCarFuelKgPerLtr": 0.75, 840 | "DriverCarFuelMaxLtr": 104, 841 | "DriverCarMaxFuelPct": 1, 842 | "DriverCarGearNumForward": 6, 843 | "DriverCarGearNeutral": 1, 844 | "DriverCarGearReverse": 1, 845 | "DriverCarSLFirstRPM": 7000, 846 | "DriverCarSLShiftRPM": 7700, 847 | "DriverCarSLLastRPM": 7600, 848 | "DriverCarSLBlinkRPM": 7950, 849 | "DriverCarVersion": "2024.11.26.01", 850 | "DriverPitTrkPct": 0.990978, 851 | "DriverCarEstLapTime": 105.0762, 852 | "DriverSetupName": "baseline.sto", 853 | "DriverSetupIsModified": 0, 854 | "DriverSetupLoadTypeName": "baseline", 855 | "DriverSetupPassedTech": 1, 856 | "DriverIncidentCount": 0, 857 | "Drivers": [ 858 | { 859 | "CarIdx": 0, 860 | "UserName": "Zachary Friss", 861 | "AbbrevName": null, 862 | "Initials": null, 863 | "UserID": 697587, 864 | "TeamID": 0, 865 | "TeamName": "Zachary Friss", 866 | "CarNumber": "64", 867 | "CarNumberRaw": 64, 868 | "CarPath": "chevyvettez06rgt3", 869 | "CarClassID": 0, 870 | "CarID": 184, 871 | "CarIsPaceCar": 0, 872 | "CarIsAI": 0, 873 | "CarIsElectric": 0, 874 | "CarScreenName": "Chevrolet Corvette Z06 GT3.R", 875 | "CarScreenNameShort": "Corvette GT3.R", 876 | "CarClassShortName": null, 877 | "CarClassRelSpeed": 0, 878 | "CarClassLicenseLevel": 0, 879 | "CarClassMaxFuelPct": "1.000 %", 880 | "CarClassWeightPenalty": "0.000 kg", 881 | "CarClassPowerAdjust": "0.000 %", 882 | "CarClassDryTireSetLimit": "0 %", 883 | "CarClassColor": 16777215, 884 | "CarClassEstLapTime": 105.0762, 885 | "IRating": 1, 886 | "LicLevel": 1, 887 | "LicSubLevel": 1, 888 | "LicString": "R 0.01", 889 | "LicColor": "0xundefined", 890 | "IsSpectator": 0, 891 | "CarDesignStr": "10,FFFFFF,FF7A59,33475B", 892 | "HelmetDesignStr": "7,ffffff,ff7a59,33475b", 893 | "SuitDesignStr": "21,ff7a59,33475b,ffffff", 894 | "BodyType": 0, 895 | "FaceType": 4, 896 | "HelmetType": 0, 897 | "CarNumberDesignStr": "0,0,FFFFFF,777777,000000", 898 | "CarSponsor_1": 0, 899 | "CarSponsor_2": 0, 900 | "CurDriverIncidentCount": 0, 901 | "TeamIncidentCount": 0 902 | } 903 | ] 904 | }, 905 | "SplitTimeInfo": { 906 | "Sectors": [ 907 | { 908 | "SectorNum": 0, 909 | "SectorStartPct": 0 910 | }, 911 | { 912 | "SectorNum": 1, 913 | "SectorStartPct": 0.184456 914 | }, 915 | { 916 | "SectorNum": 2, 917 | "SectorStartPct": 0.337214 918 | }, 919 | { 920 | "SectorNum": 3, 921 | "SectorStartPct": 0.504637 922 | }, 923 | { 924 | "SectorNum": 4, 925 | "SectorStartPct": 0.734279 926 | }, 927 | { 928 | "SectorNum": 5, 929 | "SectorStartPct": 0.829332 930 | } 931 | ] 932 | }, 933 | "CarSetup": { 934 | "UpdateCount": 1, 935 | "TiresAero": { 936 | "TireType": { 937 | "TireType": "Dry" 938 | }, 939 | "LeftFront": { 940 | "StartingPressure": "165 kPa", 941 | "LastHotPressure": "165 kPa", 942 | "LastTempsOMI": "44C, 44C, 44C", 943 | "TreadRemaining": "100%, 100%, 100%" 944 | }, 945 | "LeftRear": { 946 | "StartingPressure": "165 kPa", 947 | "LastHotPressure": "165 kPa", 948 | "LastTempsOMI": "44C, 44C, 44C", 949 | "TreadRemaining": "100%, 100%, 100%" 950 | }, 951 | "RightFront": { 952 | "StartingPressure": "165 kPa", 953 | "LastHotPressure": "165 kPa", 954 | "LastTempsIMO": "44C, 44C, 44C", 955 | "TreadRemaining": "100%, 100%, 100%" 956 | }, 957 | "RightRear": { 958 | "StartingPressure": "165 kPa", 959 | "LastHotPressure": "165 kPa", 960 | "LastTempsIMO": "44C, 44C, 44C", 961 | "TreadRemaining": "100%, 100%, 100%" 962 | }, 963 | "AeroBalanceCalc": { 964 | "FrontRhAtSpeed": "43 mm", 965 | "RearRhAtSpeed": "53 mm", 966 | "RearWingAngle": "9.5 degrees", 967 | "FrontDownforce": "38.1%" 968 | } 969 | }, 970 | "Chassis": { 971 | "FrontBrakes": { 972 | "ArbBlades": 3, 973 | "TotalToeIn": "-3.1 mm", 974 | "BrakePedalRatio": 4.67, 975 | "BrakePads": "Medium friction" 976 | }, 977 | "LeftFront": { 978 | "CornerWeight": "3162 N", 979 | "RideHeight": "51.0 mm", 980 | "BumpRubberGap": "12 mm", 981 | "SpringRate": "105 N/mm", 982 | "Camber": "-3.9 deg" 983 | }, 984 | "LeftRear": { 985 | "CornerWeight": "3973 N", 986 | "RideHeight": "71.3 mm", 987 | "BumpRubberGap": "53 mm", 988 | "SpringRate": "200 N/mm", 989 | "Camber": "-3.5 deg", 990 | "ToeIn": "+1.6 mm" 991 | }, 992 | "Rear": { 993 | "FuelLevel": "52.0 L", 994 | "ArbBlades": 0, 995 | "RearWingAngle": "9.5 degrees" 996 | }, 997 | "InCarAdjustments": { 998 | "BrakePressureBias": "55.0%", 999 | "AbsSetting": "3 (ABS)", 1000 | "TractionControlSetting": "3 (T/C)", 1001 | "DisplayPage": "Race", 1002 | "CrossWeight": "50.0%" 1003 | }, 1004 | "RightFront": { 1005 | "CornerWeight": "3162 N", 1006 | "RideHeight": "51.0 mm", 1007 | "BumpRubberGap": "12 mm", 1008 | "SpringRate": "105 N/mm", 1009 | "Camber": "-3.9 deg" 1010 | }, 1011 | "RightRear": { 1012 | "CornerWeight": "3973 N", 1013 | "RideHeight": "71.3 mm", 1014 | "BumpRubberGap": "53 mm", 1015 | "SpringRate": "200 N/mm", 1016 | "Camber": "-3.5 deg", 1017 | "ToeIn": "+1.6 mm" 1018 | }, 1019 | "GearsDifferential": { 1020 | "GearStack": "FIA", 1021 | "FrictionFaces": 8, 1022 | "DiffPreload": "100 Nm" 1023 | } 1024 | }, 1025 | "Dampers": { 1026 | "FrontDampers": { 1027 | "LowSpeedCompressionDamping": "5 clicks", 1028 | "HighSpeedCompressionDamping": "0 clicks", 1029 | "LowSpeedReboundDamping": "5 clicks", 1030 | "HighSpeedReboundDamping": "5 clicks" 1031 | }, 1032 | "RearDampers": { 1033 | "LowSpeedCompressionDamping": "5 clicks", 1034 | "HighSpeedCompressionDamping": "0 clicks", 1035 | "LowSpeedReboundDamping": "5 clicks", 1036 | "HighSpeedReboundDamping": "7 clicks" 1037 | } 1038 | } 1039 | } 1040 | } 1041 | } -------------------------------------------------------------------------------- /src/JsIrSdk.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('node:events'); 2 | const stringToEnum = require('./utils/stringToEnum'); 3 | const createSessionInfoParser = require('./utils/createSessionInfoParser'); 4 | const padCarNum = require('./utils/padCarNum'); 5 | const IrSdkConsts = require('./consts/IrSdkConsts'); 6 | const BroadcastMsg = IrSdkConsts.BroadcastMsg; 7 | 8 | /** 9 | JsIrSdk is javascript implementation of iRacing SDK. 10 | 11 | Don't use constructor directly, use {@link module:irsdk.getInstance}. 12 | 13 | @class 14 | @extends events.EventEmitter 15 | @see {@link https://nodejs.org/api/events.html#events_class_eventemitter|EventEmitter API} 16 | @alias iracing 17 | @fires iracing#Connected 18 | @fires iracing#Disconnected 19 | @fires iracing#Telemetry 20 | @fires iracing#TelemetryDescription 21 | @fires iracing#SessionInfo 22 | 23 | @example const iracing = require('iracing-sdk-js').getInstance() 24 | */ 25 | class JsIrSdk extends EventEmitter { 26 | constructor(IrSdkWrapper, opts) { 27 | super(); 28 | 29 | this.IrSdkWrapper = IrSdkWrapper; 30 | opts = opts || {}; 31 | 32 | /** Execute any of available commands, excl. FFB command 33 | @method 34 | @param {Integer} msgId Message id 35 | @param {Integer} [arg1] 1st argument 36 | @param {Integer} [arg2] 2nd argument 37 | @param {Integer} [arg3] 3rd argument 38 | */ 39 | this.execCmd = this.IrSdkWrapper.sendCmd; 40 | 41 | /** 42 | Parser for SessionInfo YAML 43 | @callback iracing~sessionInfoParser 44 | @param {String} sessionInfo SessionInfo YAML 45 | @returns {Object} parsed session info 46 | */ 47 | this.sessionInfoParser = opts.sessionInfoParser; 48 | if (!this.sessionInfoParser) { 49 | this.sessionInfoParser = createSessionInfoParser(); 50 | } 51 | 52 | this.connected = false; // if irsdk is available 53 | 54 | this.startIntervalId = setInterval(() => { 55 | if (!this.IrSdkWrapper.isInitialized()) { 56 | this.IrSdkWrapper.start(); 57 | } 58 | }, 10000); 59 | 60 | this.IrSdkWrapper.start(); 61 | 62 | /** Latest telemetry, may be null or undefined 63 | 64 | */ 65 | this.telemetry = null; 66 | 67 | /** Latest telemetry, may be null or undefined 68 | 69 | */ 70 | this.telemetryDescription = null; 71 | 72 | /** Latest telemetry, may be null or undefined 73 | 74 | */ 75 | this.sessionInfo = null; 76 | 77 | this.telemetryIntervalId = setInterval(() => { 78 | this.checkConnection(); 79 | if (this.connected && IrSdkWrapper.updateTelemetry()) { 80 | var now = new Date(); // date gives ms accuracy 81 | this.telemetry = IrSdkWrapper.getTelemetry(); 82 | // replace ctime timestamp 83 | this.telemetry.timestamp = now; 84 | setImmediate(() => { 85 | if (!this.telemetryDescription) { 86 | this.telemetryDescription = IrSdkWrapper.getTelemetryDescription(); 87 | /** 88 | Telemetry description, contains description of available telemetry values 89 | @event iracing#TelemetryDescription 90 | @type Object 91 | @example 92 | * iracing.on('TelemetryDescription', function (data) { 93 | * console.log(evt) 94 | * }) 95 | */ 96 | this.emit('update', { 97 | type: 'TelemetryDescription', 98 | data: this.telemetryDescription, 99 | timestamp: now, 100 | }); 101 | } 102 | /** 103 | Telemetry update 104 | @event iracing#Telemetry 105 | @type Object 106 | @example 107 | * iracing.on('Telemetry', function (evt) { 108 | * console.log(evt) 109 | * }) 110 | */ 111 | this.emit('update', { 112 | type: 'Telemetry', 113 | data: this.telemetry.values, 114 | timestamp: now, 115 | }); 116 | }); 117 | } 118 | }, opts.telemetryUpdateInterval); 119 | 120 | this.sessionInfoIntervalId = setInterval(() => { 121 | this.checkConnection(); 122 | if (this.connected && IrSdkWrapper.updateSessionInfo()) { 123 | var now = new Date(); 124 | var sessionInfo = IrSdkWrapper.getSessionInfo(); 125 | var doc; 126 | setImmediate(() => { 127 | try { 128 | doc = this.sessionInfoParser(sessionInfo); 129 | } catch (ex) { 130 | // TODO: log faulty yaml 131 | console.error('js-irsdk: yaml error: \n' + ex); 132 | } 133 | 134 | if (doc) { 135 | this.sessionInfo = { timestamp: now, data: doc }; 136 | /** 137 | SessionInfo update 138 | @event iracing#SessionInfo 139 | @type Object 140 | @example 141 | * iracing.on('SessionInfo', function (evt) { 142 | * console.log(evt) 143 | * }) 144 | */ 145 | this.emit('update', { 146 | type: 'SessionInfo', 147 | data: this.sessionInfo.data, 148 | timestamp: now, 149 | }); 150 | } 151 | }); 152 | } 153 | }, opts.sessionInfoUpdateInterval); 154 | 155 | /** 156 | any update event 157 | @event iracing#update 158 | @type Object 159 | @example 160 | * iracing.on('update', function (evt) { 161 | * console.log(evt) 162 | * }) 163 | */ 164 | this.on('update', (evt) => { 165 | // fire old events as well. 166 | const timestamp = evt.timestamp; 167 | const data = evt.data; 168 | const type = evt.type; 169 | 170 | switch (type) { 171 | case 'SessionInfo': 172 | this.emit('SessionInfo', { timestamp, data }); 173 | break; 174 | case 'Telemetry': 175 | this.emit('Telemetry', { timestamp, values: data }); 176 | break; 177 | case 'TelemetryDescription': 178 | this.emit('TelemetryDescription', data); 179 | break; 180 | case 'Connected': 181 | this.emit('Connected'); 182 | break; 183 | case 'Disconnected': 184 | this.emit('Disconnected'); 185 | break; 186 | default: 187 | break; 188 | } 189 | }); 190 | } 191 | 192 | checkConnection() { 193 | if (this.IrSdkWrapper.isInitialized() && this.IrSdkWrapper.isConnected()) { 194 | if (!this.connected) { 195 | this.connected = true; 196 | /** 197 | iRacing, sim, is started 198 | @event iracing#Connected 199 | @example 200 | * iracing.on('Connected', function (evt) { 201 | * console.log(evt) 202 | * }) 203 | */ 204 | this.emit('update', { type: 'Connected', timestamp: new Date() }); 205 | } 206 | } else { 207 | if (this.connected) { 208 | this.connected = false; 209 | /** 210 | iRacing, sim, was closed 211 | @event iracing#Disconnected 212 | @example 213 | * iracing.on('Disconnected', function (evt) { 214 | * console.log(evt) 215 | * }) 216 | */ 217 | this.emit('update', { type: 'Disconnected', timestamp: new Date() }); 218 | 219 | this.IrSdkWrapper.shutdown(); 220 | this.telemetryDescription = null; 221 | } 222 | } 223 | } 224 | 225 | /** iRacing SDK related constants 226 | @type IrSdkConsts 227 | @instance 228 | */ 229 | Consts = IrSdkConsts; 230 | 231 | /** Camera controls 232 | @type {Object} 233 | */ 234 | camControls = { 235 | /** Change camera tool state 236 | @method 237 | @param {IrSdkConsts.CameraState} state new state 238 | @example 239 | * // hide UI and enable mouse aim 240 | * var States = iracing.Consts.CameraState 241 | * var state = States.CamToolActive | States.UIHidden | States.UseMouseAimMode 242 | * iracing.camControls.setState(state) 243 | */ 244 | setState: (state) => { 245 | this.execCmd(BroadcastMsg.CamSetState, state); 246 | }, 247 | /** Switch camera, focus on car 248 | @method 249 | @param {Integer|String|IrSdkConsts.CamFocusAt} carNum Car to focus on 250 | @param {Integer} [camGroupNum] Select camera group 251 | @param {Integer} [camNum] Select camera 252 | 253 | @example 254 | * // show car #2 255 | * iracing.camControls.switchToCar(2) 256 | @example 257 | * // show car #02 258 | * iracing.camControls.switchToCar('02') 259 | @example 260 | * // show leader 261 | * iracing.camControls.switchToCar('leader') 262 | @example 263 | * // show car #2 using cam group 3 264 | * iracing.camControls.switchToCar(2, 3) 265 | */ 266 | switchToCar: (carNum, camGroupNum, camNum) => { 267 | camGroupNum = camGroupNum | 0; 268 | camNum = camNum | 0; 269 | 270 | if (typeof carNum === 'string') { 271 | if (isNaN(parseInt(carNum))) { 272 | carNum = stringToEnum(carNum, IrSdkConsts.CamFocusAt); 273 | } else { 274 | carNum = padCarNum(carNum); 275 | } 276 | } 277 | if (Number.isInteger(carNum)) { 278 | this.execCmd(BroadcastMsg.CamSwitchNum, carNum, camGroupNum, camNum); 279 | } 280 | }, 281 | /** Switch camera, focus on position 282 | @method 283 | @param {Integer|IrSdkConsts.CamFocusAt} position Position to focus on 284 | @param {Integer} [camGroupNum] Select camera group 285 | @param {Integer} [camNum] Select camera 286 | 287 | @example iracing.camControls.switchToPos(2) // show P2 288 | */ 289 | switchToPos: (position, camGroupNum, camNum) => { 290 | camGroupNum = camGroupNum | 0; 291 | camNum = camNum | 0; 292 | 293 | if (typeof position === 'string') { 294 | position = stringToEnum(position, IrSdkConsts.CamFocusAt); 295 | } 296 | if (Number.isInteger(position)) { 297 | this.execCmd(BroadcastMsg.CamSwitchPos, position, camGroupNum, camNum); 298 | } 299 | }, 300 | }; 301 | 302 | /** Replay and playback controls 303 | @type {Object} 304 | */ 305 | playbackControls = { 306 | /** Play replay 307 | @method 308 | @example iracing.playbackControls.play() 309 | */ 310 | play: () => { 311 | this.execCmd(BroadcastMsg.ReplaySetPlaySpeed, 1, 0); 312 | }, 313 | /** Pause replay 314 | @method 315 | @example iracing.playbackControls.pause() 316 | */ 317 | pause: () => { 318 | this.execCmd(BroadcastMsg.ReplaySetPlaySpeed, 0, 0); 319 | }, 320 | /** fast-forward replay 321 | @method 322 | @param {Integer} [speed=2] FF speed, something between 2-16 works 323 | @example iracing.playbackControls.fastForward() // double speed FF 324 | */ 325 | fastForward: (speed) => { 326 | speed = speed || 2; 327 | this.execCmd(BroadcastMsg.ReplaySetPlaySpeed, speed, 0); 328 | }, 329 | /** rewind replay 330 | @method 331 | @param {Integer} [speed=2] RW speed, something between 2-16 works 332 | @example iracing.playbackControls.rewind() // double speed RW 333 | */ 334 | rewind: (speed) => { 335 | speed = speed || 2; 336 | this.execCmd(BroadcastMsg.ReplaySetPlaySpeed, -1 * speed, 0); 337 | }, 338 | /** slow-forward replay, slow motion 339 | @method 340 | @param {Integer} [divider=2] divider of speed, something between 2-17 works 341 | @example iracing.playbackControls.slowForward(2) // half speed 342 | */ 343 | slowForward: (divider) => { 344 | divider = divider || 2; 345 | divider -= 1; 346 | this.execCmd(BroadcastMsg.ReplaySetPlaySpeed, divider, 1); 347 | }, 348 | /** slow-backward replay, reverse slow motion 349 | @method 350 | @param {Integer} [divider=2] divider of speed, something between 2-17 works 351 | @example iracing.playbackControls.slowBackward(2) // half speed RW 352 | */ 353 | slowBackward: (divider) => { 354 | divider = divider || 2; 355 | divider -= 1; 356 | this.execCmd(BroadcastMsg.ReplaySetPlaySpeed, -1 * divider, 1); 357 | }, 358 | /** Search things from replay 359 | @method 360 | @param {IrSdkConsts.RpySrchMode} searchMode what to search 361 | @example iracing.playbackControls.search('nextIncident') 362 | */ 363 | search: (searchMode) => { 364 | if (typeof searchMode === 'string') { 365 | searchMode = stringToEnum(searchMode, IrSdkConsts.RpySrchMode); 366 | } 367 | if (Number.isInteger(searchMode)) { 368 | this.execCmd(BroadcastMsg.ReplaySearch, searchMode); 369 | } 370 | }, 371 | /** Search timestamp 372 | @method 373 | @param {Integer} sessionNum Session number 374 | @param {Integer} sessionTimeMS Session time in milliseconds 375 | @example 376 | * // jump to 2nd minute of 3rd session 377 | * iracing.playbackControls.searchTs(2, 2*60*1000) 378 | */ 379 | searchTs: (sessionNum, sessionTimeMS) => { 380 | this.execCmd( 381 | BroadcastMsg.ReplaySearchSessionTime, 382 | sessionNum, 383 | sessionTimeMS 384 | ); 385 | }, 386 | /** Go to frame. Frame counting can be relative to begin, end or current. 387 | @method 388 | @param {Integer} frameNum Frame number 389 | @param {IrSdkConsts.RpyPosMode} rpyPosMode Is frame number relative to begin, end or current frame 390 | @example iracing.playbackControls.searchFrame(1, 'current') // go to 1 frame forward 391 | */ 392 | searchFrame: (frameNum, rpyPosMode) => { 393 | if (typeof rpyPosMode === 'string') { 394 | rpyPosMode = stringToEnum(rpyPosMode, IrSdkConsts.RpyPosMode); 395 | } 396 | if (Number.isInteger(rpyPosMode)) { 397 | this.execCmd(BroadcastMsg.ReplaySetPlayPosition, rpyPosMode, frameNum); 398 | } 399 | }, 400 | }; 401 | 402 | /** Reload all car textures 403 | @method 404 | @example iracing.reloadTextures() // reload all paints 405 | */ 406 | reloadTextures() { 407 | this.execCmd( 408 | BroadcastMsg.ReloadTextures, 409 | IrSdkConsts.ReloadTexturesMode.All 410 | ); 411 | } 412 | 413 | /** Reload car's texture 414 | @method 415 | @param {Integer} carIdx car to reload 416 | @example iracing.reloadTexture(1) // reload paint of carIdx=1 417 | */ 418 | reloadTexture(carIdx) { 419 | this.execCmd( 420 | BroadcastMsg.ReloadTextures, 421 | IrSdkConsts.ReloadTexturesMode.CarIdx, 422 | carIdx 423 | ); 424 | } 425 | 426 | /** Execute chat command 427 | @param {IrSdkConsts.ChatCommand} cmd 428 | @param {Integer} [arg] Command argument, if needed 429 | @example iracing.execChatCmd('cancel') // close chat window 430 | */ 431 | execChatCmd(cmd, arg) { 432 | arg = arg || 0; 433 | if (typeof cmd === 'string') { 434 | cmd = stringToEnum(cmd, IrSdkConsts.ChatCommand); 435 | } 436 | if (Number.isInteger(cmd)) { 437 | this.execCmd(BroadcastMsg.ChatComand, cmd, arg); 438 | } 439 | } 440 | 441 | /** Execute chat macro 442 | @param {Integer} num Macro's number (0-15) 443 | @example iracing.execChatMacro(1) // macro 1 444 | */ 445 | execChatMacro(num) { 446 | this.execChatCmd('macro', num); 447 | } 448 | 449 | /** Execute pit command 450 | @param {IrSdkConsts.PitCommand} cmd 451 | @param {Integer} [arg] Command argument, if needed 452 | @example 453 | * // full tank, no tires, no tear off 454 | * iracing.execPitCmd('clear') 455 | * iracing.execPitCmd('fuel', 999) // 999 liters 456 | * iracing.execPitCmd('lf') // new left front 457 | * iracing.execPitCmd('lr', 200) // new left rear, 200 kPa 458 | */ 459 | execPitCmd(cmd, arg) { 460 | arg = arg || 0; 461 | if (typeof cmd === 'string') { 462 | cmd = stringToEnum(cmd, IrSdkConsts.PitCommand); 463 | } 464 | if (Number.isInteger(cmd)) { 465 | this.execCmd(BroadcastMsg.PitCommand, cmd, arg); 466 | } 467 | } 468 | 469 | /** Control telemetry logging (ibt file) 470 | @param {IrSdkConsts.TelemCommand} cmd Command: start/stop/restart 471 | @example iracing.execTelemetryCmd('restart') 472 | */ 473 | execTelemetryCmd(cmd) { 474 | if (typeof cmd === 'string') { 475 | cmd = stringToEnum(cmd, IrSdkConsts.TelemCommand); 476 | } 477 | if (Number.isInteger(cmd)) { 478 | this.execCmd(BroadcastMsg.TelemCommand, cmd); 479 | } 480 | } 481 | 482 | /** 483 | Stops JsIrSdk, no new events are fired after calling this 484 | @method 485 | @private 486 | */ 487 | _stop() { 488 | clearInterval(this.telemetryIntervalId); 489 | clearInterval(this.sessionInfoIntervalId); 490 | clearInterval(this.startIntervalId); 491 | this.IrSdkWrapper.shutdown(); 492 | } 493 | } 494 | 495 | module.exports = JsIrSdk; 496 | -------------------------------------------------------------------------------- /src/consts/IrSdkConsts.js: -------------------------------------------------------------------------------- 1 | /** 2 | IrSdkConsts, iRacing SDK constants/enums. 3 | 4 | @namespace 5 | @constant 6 | @example var IrSdkConsts = require('node-irsdk').getInstance().Consts 7 | */ 8 | var IrSdkConsts = { 9 | /** 10 | Available command messages. 11 | @enum 12 | */ 13 | BroadcastMsg: { 14 | /** Switch cam, args: car position, group, camera */ 15 | CamSwitchPos: 0, 16 | /** Switch cam, args, driver #, group, camera */ 17 | CamSwitchNum: 1, 18 | /** Set cam state, args: CameraState, unused, unused */ 19 | CamSetState: 2, 20 | /** Set replay speed, args: speed, slowMotion, unused */ 21 | ReplaySetPlaySpeed: 3, 22 | /** Jump to frame, args: RpyPosMode, Frame Number (high, low) */ 23 | ReplaySetPlayPosition: 4, 24 | /** Search things from replay, args: RpySrchMode, unused, unused */ 25 | ReplaySearch: 5, 26 | /** Set replay state, args: RpyStateMode, unused, unused */ 27 | ReplaySetState: 6, 28 | /** Reload textures, args: ReloadTexturesMode, carIdx, unused */ 29 | ReloadTextures: 7, 30 | /** Chat commands, args: ChatCommand, subCommand, unused */ 31 | ChatComand: 8, 32 | /** Pit commands, args: PitCommand, parameter */ 33 | PitCommand: 9, 34 | /** Disk telemetry commands, args: TelemCommand, unused, unused */ 35 | TelemCommand: 10, 36 | /** **not supported by node-irsdk**: Change FFB settings, args: FFBCommandMode, value (float, high, low) */ 37 | FFBCommand: 11, 38 | /** Search by timestamp, args: sessionNum, sessionTimeMS (high, low) */ 39 | ReplaySearchSessionTime: 12, 40 | }, 41 | /** Camera state 42 | Camera state is bitfield; use these values to compose a new state. 43 | @enum 44 | */ 45 | CameraState: { 46 | /** Is driver out of the car */ 47 | IsSessionScreen: 0x0001, // 48 | /** The scenic camera is active (no focus car) */ 49 | IsScenicActive: 0x0002, // 50 | 51 | // these can be changed with a broadcast message 52 | /** Activate camera tool */ 53 | CamToolActive: 0x0004, 54 | /** Hide UI */ 55 | UIHidden: 0x0008, 56 | /** Enable auto shot selection */ 57 | UseAutoShotSelection: 0x0010, 58 | /** Enable temporary edits */ 59 | UseTemporaryEdits: 0x0020, 60 | /** Enable key acceleration */ 61 | UseKeyAcceleration: 0x0040, 62 | /** Enable 10x key acceleration */ 63 | UseKey10xAcceleration: 0x0080, 64 | /** Enable mouse aim */ 65 | UseMouseAimMode: 0x0100, 66 | }, 67 | /** @enum */ 68 | RpyPosMode: { 69 | /** Frame number is relative to beginning */ 70 | Begin: 0, 71 | /** Frame number is relative to current frame */ 72 | Current: 1, 73 | /** Frame number is relative to end */ 74 | End: 2, 75 | }, 76 | /** @enum */ 77 | RpySrchMode: { 78 | ToStart: 0, 79 | ToEnd: 1, 80 | PrevSession: 2, 81 | NextSession: 3, 82 | PrevLap: 4, 83 | NextLap: 5, 84 | PrevFrame: 6, 85 | NextFrame: 7, 86 | PrevIncident: 8, 87 | NextIncident: 9, 88 | }, 89 | /** @enum */ 90 | RpyStateMode: { 91 | /** Clear any data in the replay tape (works only if replay spooling disabled) */ 92 | EraseTape: 0, 93 | }, 94 | /** @enum */ 95 | ReloadTexturesMode: { 96 | All: 0, 97 | CarIdx: 1, 98 | }, 99 | /** @enum */ 100 | ChatCommand: { 101 | /** Macro, give macro num (0-15) as argument */ 102 | Macro: 0, 103 | /** Open up a new chat window */ 104 | BeginChat: 1, 105 | /** Reply to last private chat */ 106 | Reply: 2, 107 | /** Close chat window */ 108 | Cancel: 3, 109 | }, 110 | /** @enum */ 111 | PitCommand: { 112 | /** Clear all pit checkboxes */ 113 | Clear: 0, 114 | /** Clean the winshield, using one tear off */ 115 | WS: 1, 116 | /** Request fuel, optional argument: liters */ 117 | Fuel: 2, 118 | /** Request new left front, optional argument: pressure in kPa */ 119 | LF: 3, 120 | /** Request new right front, optional argument: pressure in kPa */ 121 | RF: 4, 122 | /** Request new left rear, optional argument: pressure in kPa */ 123 | LR: 5, 124 | /** Request new right rear, optional argument: pressure in kPa */ 125 | RR: 6, 126 | /** Clear tire pit checkboxes */ 127 | ClearTires: 7, 128 | /** Request a fast repair */ 129 | FR: 8, 130 | /** Disable clear windshield */ 131 | ClearWS: 9, 132 | /** Disable fast repair */ 133 | ClearFR: 10, 134 | /** Disable refueling */ 135 | ClearFuel: 11, 136 | }, 137 | /** @enum */ 138 | TelemCommand: { 139 | /** Turn telemetry recording off */ 140 | Stop: 0, 141 | /** Turn telemetry recording on */ 142 | Start: 1, 143 | /** Write current file to disk and start a new one */ 144 | Restart: 2, 145 | }, 146 | /** When switching camera, these can be used instead of car number / position 147 | @enum 148 | */ 149 | CamFocusAt: { 150 | Incident: -3, 151 | Leader: -2, 152 | Exciting: -1, 153 | /** Use car number / position instead of this */ 154 | Driver: 0, 155 | }, 156 | }; 157 | 158 | module.exports = IrSdkConsts; 159 | -------------------------------------------------------------------------------- /src/cpp/IRSDKWrapper.cpp: -------------------------------------------------------------------------------- 1 | #include "IRSDKWrapper.h" 2 | #include 3 | #include 4 | 5 | // npm install --debug enables debug prints 6 | #ifdef _DEBUG 7 | #define debug(x) std::cout << x << std::endl; 8 | #else 9 | #define debug(x) 10 | #endif 11 | 12 | NodeIrSdk::IRSDKWrapper::IRSDKWrapper() : pHeader(NULL), lastTickCount(INT_MIN), lastSessionInfoUpdate(INT_MIN), 13 | data(NULL), dataLen(-1), sessionInfoStr() 14 | { 15 | debug("IRSDKWrapper: constructing..."); 16 | } 17 | 18 | NodeIrSdk::IRSDKWrapper::~IRSDKWrapper() 19 | { 20 | debug("IRSDKWrapper: deconstructing..."); 21 | shutdown(); 22 | } 23 | 24 | bool NodeIrSdk::IRSDKWrapper::startup() 25 | { 26 | debug("IRSDKWrapper: starting up..."); 27 | 28 | if (!sharedMem_.isOpen()) 29 | { 30 | debug("IRSDKWrapper: opening mem map..."); 31 | if (!sharedMem_.open(IRSDK_MEMMAPFILENAME)) 32 | { 33 | return false; 34 | } 35 | pHeader = (irsdk_header *)sharedMem_.getData(); 36 | lastTickCount = INT_MIN; 37 | 38 | if (!dataEvent_.create(IRSDK_DATAVALIDEVENTNAME)) 39 | { 40 | shutdown(); 41 | return false; 42 | } 43 | } 44 | debug("IRSDKWrapper: start up ready."); 45 | return true; 46 | } 47 | 48 | bool NodeIrSdk::IRSDKWrapper::isInitialized() const 49 | { 50 | if (!sharedMem_.isOpen()) 51 | { 52 | debug("IRSDKWrapper: not initialized..."); 53 | return false; 54 | } 55 | debug("IRSDKWrapper: is initialized..."); 56 | return true; 57 | } 58 | 59 | bool NodeIrSdk::IRSDKWrapper::isConnected() const 60 | { 61 | bool status = pHeader && pHeader->status == irsdk_stConnected; 62 | debug("IRSDKWrapper: sim status: " << status); 63 | return status; 64 | } 65 | 66 | void NodeIrSdk::IRSDKWrapper::shutdown() 67 | { 68 | debug("IRSDKWrapper: shutting down..."); 69 | 70 | sharedMem_.close(); 71 | dataEvent_.close(); 72 | 73 | pHeader = NULL; 74 | lastTickCount = INT_MIN; 75 | lastSessionInfoUpdate = INT_MIN; 76 | delete[] data; 77 | data = NULL; 78 | lastValidTime = time(NULL); 79 | varHeadersArr.clear(); 80 | sessionInfoStr = ""; 81 | 82 | debug("IRSDKWrapper: shutdown ready."); 83 | } 84 | 85 | bool NodeIrSdk::IRSDKWrapper::updateSessionInfo() 86 | { 87 | debug("IRSDKWrapper: updating session info..."); 88 | if (startup()) 89 | { 90 | int counter = pHeader->sessionInfoUpdate; 91 | 92 | if (counter > lastSessionInfoUpdate) 93 | { 94 | sessionInfoStr = getSessionInfoStr(); 95 | lastSessionInfoUpdate = counter; 96 | return true; 97 | } 98 | return false; 99 | } 100 | return false; 101 | } 102 | 103 | const std::string NodeIrSdk::IRSDKWrapper::getSessionInfo() const 104 | { 105 | return sessionInfoStr; 106 | } 107 | 108 | bool NodeIrSdk::IRSDKWrapper::updateTelemetry() 109 | { 110 | debug("IRSDKWrapper: updating telemetry..."); 111 | if (isInitialized() && isConnected()) 112 | { 113 | if (varHeadersArr.empty()) 114 | { 115 | updateVarHeaders(); 116 | } 117 | // if sim is not active, then no new data 118 | if (pHeader->status != irsdk_stConnected) 119 | { 120 | debug("IRSDKWrapper: not connected, break"); 121 | lastTickCount = INT_MIN; 122 | return false; 123 | } 124 | 125 | debug("IRSDKWrapper: finding latest buffer"); 126 | int latest = 0; 127 | for (int i = 1; i < pHeader->numBuf; i++) 128 | if (pHeader->varBuf[latest].tickCount < pHeader->varBuf[i].tickCount) 129 | latest = i; 130 | 131 | debug("IRSDKWrapper: latest buffer " << latest); 132 | 133 | // if newer than last received, then report new data 134 | if (lastTickCount < pHeader->varBuf[latest].tickCount) 135 | { 136 | debug("IRSDKWrapper: new data, attempting to copy"); 137 | if (data == NULL || dataLen != pHeader->bufLen) 138 | { 139 | debug("IRSDKWrapper: create new data array"); 140 | if (data != NULL) 141 | delete[] data; 142 | data = NULL; 143 | 144 | if (pHeader->bufLen > 0) 145 | { 146 | dataLen = pHeader->bufLen; 147 | data = new char[dataLen]; 148 | } 149 | else 150 | { 151 | debug("IRSDKWrapper: weird bufferLen.. skipping"); 152 | return false; 153 | } 154 | } 155 | // try to get data 156 | // try twice to get the data out 157 | for (int count = 0; count < 2; count++) 158 | { 159 | debug("IRSDKWrapper: copy attempt " << count); 160 | int curTickCount = pHeader->varBuf[latest].tickCount; 161 | const char *sharedData = static_cast(sharedMem_.getData()); 162 | memcpy(data, sharedData + pHeader->varBuf[latest].bufOffset, pHeader->bufLen); 163 | if (curTickCount == pHeader->varBuf[latest].tickCount) 164 | { 165 | lastTickCount = curTickCount; 166 | lastValidTime = time(NULL); 167 | debug("IRSDKWrapper: copy complete"); 168 | return true; 169 | } 170 | } 171 | // if here, the data changed out from under us. 172 | debug("IRSDKWrapper: copy failed"); 173 | return false; 174 | } 175 | // if older than last received, then reset, we probably disconnected 176 | else if (lastTickCount > pHeader->varBuf[latest].tickCount) 177 | { 178 | debug("IRSDKWrapper: ???"); 179 | lastTickCount = INT_MIN; 180 | return false; 181 | } 182 | // else the same, and nothing changed this tick 183 | } 184 | debug("IRSDKWrapper: no new telemetry data"); 185 | return false; 186 | } 187 | 188 | double NodeIrSdk::IRSDKWrapper::getLastTelemetryUpdateTS() const 189 | { 190 | return 1000.0f * lastValidTime; 191 | } 192 | 193 | const char *NodeIrSdk::IRSDKWrapper::getSessionInfoStr() const 194 | { 195 | debug("IRSDKWrapper: getSessionInfoStr"); 196 | if (isInitialized() && pHeader) 197 | { 198 | return static_cast(sharedMem_.getData()) + pHeader->sessionInfoOffset; 199 | } 200 | 201 | return NULL; 202 | } 203 | 204 | void NodeIrSdk::IRSDKWrapper::updateVarHeaders() 205 | { 206 | debug("IRSDKWrapper: updating varHeaders..."); 207 | varHeadersArr.clear(); 208 | 209 | if (!pHeader) 210 | return; 211 | 212 | const char *sharedData = static_cast(sharedMem_.getData()); 213 | for (int index = 0; index < pHeader->numVars; ++index) 214 | { 215 | irsdk_varHeader *pVarHeader = &((irsdk_varHeader *)(sharedData + pHeader->varHeaderOffset))[index]; 216 | varHeadersArr.push_back(pVarHeader); 217 | } 218 | debug("IRSDKWrapper: varHeaders update done."); 219 | } 220 | 221 | NodeIrSdk::IRSDKWrapper::TelemetryVar::TelemetryVar(irsdk_varHeader *varHeader) : header(varHeader) 222 | { 223 | value = new char[irsdk_VarTypeBytes[varHeader->type] * varHeader->count]; 224 | type = (irsdk_VarType)varHeader->type; 225 | } 226 | 227 | NodeIrSdk::IRSDKWrapper::TelemetryVar::~TelemetryVar() 228 | { 229 | delete value; 230 | } 231 | 232 | const std::vector NodeIrSdk::IRSDKWrapper::getVarHeaders() const 233 | { 234 | debug("IRSDKWrapper: getVarHeaders"); 235 | return varHeadersArr; 236 | } 237 | 238 | bool NodeIrSdk::IRSDKWrapper::getVarVal(TelemetryVar &var) const 239 | { 240 | debug("IRSDKWrapper: getVarVal " << var.header->name); 241 | if (data == NULL) 242 | { 243 | debug("no data available.."); 244 | return false; 245 | } 246 | 247 | int valueBytes = irsdk_VarTypeBytes[var.header->type] * var.header->count; 248 | memcpy(var.value, data + var.header->offset, valueBytes); 249 | return true; 250 | } 251 | -------------------------------------------------------------------------------- /src/cpp/IRSDKWrapper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "irsdk/irsdk_defines.h" 4 | #include "platform/platform.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace NodeIrSdk 11 | { 12 | 13 | class IRSDKWrapper 14 | { 15 | public: 16 | IRSDKWrapper(); 17 | ~IRSDKWrapper(); 18 | 19 | bool startup(); 20 | void shutdown(); 21 | 22 | bool isInitialized() const; 23 | bool isConnected() const; 24 | 25 | bool updateTelemetry(); // returns true if telemetry update available 26 | bool updateSessionInfo(); // returns true if session info update available 27 | 28 | const std::string getSessionInfo() const; // returns yaml string 29 | 30 | struct TelemetryVar 31 | { 32 | irsdk_varHeader *header; 33 | irsdk_VarType type; 34 | 35 | union 36 | { // choose correct based on irsdk_VarType 37 | char *value; 38 | float *floatValue; 39 | int *intValue; 40 | bool *boolValue; 41 | double *doubleValue; 42 | }; 43 | 44 | TelemetryVar(irsdk_varHeader *varHeader); 45 | ~TelemetryVar(); 46 | }; 47 | 48 | const std::vector getVarHeaders() const; 49 | 50 | bool getVarVal(TelemetryVar &var) const; 51 | 52 | double getLastTelemetryUpdateTS() const; // returns JS compatible TS 53 | 54 | private: 55 | platform::SharedMemory sharedMem_; 56 | platform::Event dataEvent_; 57 | const irsdk_header *pHeader; 58 | int lastTickCount; 59 | int lastSessionInfoUpdate; 60 | time_t lastValidTime; 61 | char *data; 62 | int dataLen; 63 | std::string sessionInfoStr; 64 | 65 | std::vector varHeadersArr; 66 | 67 | void updateVarHeaders(); // updates map and vector 68 | const char *getSessionInfoStr() const; 69 | }; 70 | 71 | } // namespace NodeIrSdk 72 | -------------------------------------------------------------------------------- /src/cpp/IrSdkBindingHelpers.cpp: -------------------------------------------------------------------------------- 1 | #include "IrSdkBindingHelpers.h" 2 | #include 3 | #include 4 | 5 | using namespace v8; 6 | using namespace std; 7 | 8 | Local NodeIrSdk::convertTelemetryValueToObject(IRSDKWrapper::TelemetryVar &var, const int &index) 9 | { 10 | switch (var.type) 11 | { 12 | case irsdk_char: 13 | return Nan::New((var.value[index]) + "\0").ToLocalChecked(); 14 | case irsdk_bool: 15 | return Nan::New(var.boolValue[index]); 16 | case irsdk_int: 17 | if (strcmp(var.header->unit, "irsdk_SessionState") == 0) 18 | { 19 | return getStringValue(var.intValue[index], SESSION_STATES); 20 | } 21 | if (strcmp(var.header->unit, "irsdk_TrkLoc") == 0) 22 | { 23 | return getStringValue(var.intValue[index], TRACK_LOCS); 24 | } 25 | if (strcmp(var.header->unit, "irsdk_TrkSurf") == 0) 26 | { 27 | return getStringValue(var.intValue[index], TRACK_SURF); 28 | } 29 | if (strcmp(var.header->unit, "irsdk_PitSvStatus") == 0) 30 | { 31 | return getStringValue(var.intValue[index], PIT_SV_STATUS); 32 | } 33 | if (strcmp(var.header->unit, "irsdk_PaceMode") == 0) 34 | { 35 | return getStringValue(var.intValue[index], PACE_MODE); 36 | } 37 | if (strcmp(var.header->unit, "irsdk_CarLeftRight") == 0) 38 | { 39 | return getStringValue(var.intValue[index], CAR_BESIDE); 40 | } 41 | if (strcmp(var.header->unit, "irsdk_TrackWetness") == 0) 42 | { 43 | return getStringValue(var.intValue[index], TRACK_WETNESS); 44 | } 45 | return Nan::New(static_cast(var.intValue[index])); 46 | case irsdk_bitField: 47 | return getMaskedValues(var.intValue[index], var.header->unit); 48 | case irsdk_float: 49 | return Nan::New(static_cast(var.floatValue[index])); 50 | case irsdk_double: 51 | return Nan::New(var.doubleValue[index]); 52 | default: 53 | return Nan::Undefined(); 54 | } 55 | } 56 | 57 | Local NodeIrSdk::convertTelemetryVarToObject(IRSDKWrapper::TelemetryVar &var) 58 | { 59 | if (var.header->count > 1) 60 | { 61 | Local arr = Nan::New(var.header->count); 62 | for (int i = 0; i < var.header->count; ++i) 63 | { 64 | Nan::Set(arr, i, convertTelemetryValueToObject(var, i)); 65 | } 66 | return arr; 67 | } 68 | else 69 | { 70 | return convertTelemetryValueToObject(var, 0); 71 | } 72 | } 73 | 74 | void NodeIrSdk::convertVarHeaderToObject(IRSDKWrapper::TelemetryVar &var, Local &obj) 75 | { 76 | Nan::Set(obj, Nan::New("name").ToLocalChecked(), Nan::New(var.header->name).ToLocalChecked()); 77 | Nan::Set(obj, Nan::New("desc").ToLocalChecked(), Nan::New(var.header->desc).ToLocalChecked()); 78 | Nan::Set(obj, Nan::New("unit").ToLocalChecked(), Nan::New(var.header->unit).ToLocalChecked()); 79 | Nan::Set(obj, Nan::New("count").ToLocalChecked(), Nan::New(var.header->count)); 80 | 81 | switch (var.header->type) 82 | { 83 | case irsdk_char: 84 | Nan::Set(obj, Nan::New("type").ToLocalChecked(), Nan::New("char").ToLocalChecked()); 85 | break; 86 | case irsdk_bool: 87 | Nan::Set(obj, Nan::New("type").ToLocalChecked(), Nan::New("bool").ToLocalChecked()); 88 | break; 89 | case irsdk_int: 90 | Nan::Set(obj, Nan::New("type").ToLocalChecked(), Nan::New("int").ToLocalChecked()); 91 | break; 92 | case irsdk_bitField: 93 | Nan::Set(obj, Nan::New("type").ToLocalChecked(), Nan::New("bitField").ToLocalChecked()); 94 | break; 95 | case irsdk_float: 96 | Nan::Set(obj, Nan::New("type").ToLocalChecked(), Nan::New("float").ToLocalChecked()); 97 | break; 98 | case irsdk_double: 99 | Nan::Set(obj, Nan::New("type").ToLocalChecked(), Nan::New("double").ToLocalChecked()); 100 | break; 101 | default: 102 | Nan::Set(obj, Nan::New("type").ToLocalChecked(), Nan::New("UNKNOWN").ToLocalChecked()); 103 | break; 104 | } 105 | } 106 | 107 | Local NodeIrSdk::getMaskedValues(const int &val, char *unit) 108 | { 109 | if (strcmp(unit, "irsdk_Flags") == 0) 110 | { 111 | return getValueArr(val, FLAG_MASKS); 112 | } 113 | if (strcmp(unit, "irsdk_CameraState") == 0) 114 | { 115 | return getValueArr(val, CAMERA_STATE_MASKS); 116 | } 117 | if (strcmp(unit, "irsdk_EngineWarnings") == 0) 118 | { 119 | return getValueArr(val, ENGINE_WARNINGS_MASKS); 120 | } 121 | if (strcmp(unit, "irsdk_PitSvFlags") == 0) 122 | { 123 | return getValueArr(val, PIT_SV_MASKS); 124 | } 125 | if (strcmp(unit, "irsdk_CarLeftRight") == 0) 126 | { 127 | return getValueArr(val, CAR_BESIDE); 128 | } 129 | if (strcmp(unit, "irsdk_PaceFlags") == 0) 130 | { 131 | return getValueArr(val, PACE_FLAGS); 132 | } 133 | cerr << "Missing converter for bitField: " << unit << endl; 134 | return Nan::New(static_cast(val)); 135 | } 136 | 137 | Local NodeIrSdk::getValueArr(const int &val, const std::vector MASKS) 138 | { 139 | Local arr = Nan::New(); 140 | int counter = 0; 141 | for (const auto &mask : MASKS) 142 | { 143 | if ((mask.val & val) == mask.val) 144 | { 145 | Nan::Set(arr, counter++, Nan::New(mask.name).ToLocalChecked()); 146 | } 147 | } 148 | return arr; 149 | } 150 | 151 | Local NodeIrSdk::getStringValue(const int &val, const std::vector &map) 152 | { 153 | for (const auto &mask : map) 154 | { 155 | if (mask.val == val) 156 | { 157 | return Nan::New(mask.name).ToLocalChecked(); 158 | } 159 | } 160 | return Nan::Undefined(); 161 | } 162 | 163 | NodeIrSdk::MaskName::MaskName(int val, const char *name) : val(val), name(name) 164 | { 165 | } 166 | -------------------------------------------------------------------------------- /src/cpp/IrSdkBindingHelpers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "irsdk/irsdk_defines.h" 5 | #include "IRSDKWrapper.h" 6 | #include 7 | 8 | using namespace v8; 9 | 10 | namespace NodeIrSdk 11 | { 12 | 13 | Local convertTelemetryValueToObject(IRSDKWrapper::TelemetryVar &var, const int &index); 14 | Local convertTelemetryVarToObject(IRSDKWrapper::TelemetryVar &var); 15 | void convertVarHeaderToObject(IRSDKWrapper::TelemetryVar &var, Local &obj); 16 | 17 | struct MaskName 18 | { 19 | int val; 20 | const char *name; 21 | 22 | MaskName(int val, const char *name); 23 | }; 24 | 25 | Local getStringValue(const int &val, const std::vector &map); 26 | 27 | Local getMaskedValues(const int &val, char *unit); 28 | Local getValueArr(const int &val, const std::vector MASKS); 29 | 30 | const std::vector FLAG_MASKS = { 31 | MaskName((int)irsdk_checkered, "Checkered"), 32 | MaskName((int)irsdk_white, "White"), 33 | MaskName((int)irsdk_green, "Green"), 34 | MaskName((int)irsdk_yellow, "Yellow"), 35 | MaskName((int)irsdk_red, "Red"), 36 | MaskName((int)irsdk_blue, "Blue"), 37 | MaskName((int)irsdk_debris, "Debris"), 38 | MaskName((int)irsdk_crossed, "Crossed"), 39 | MaskName((int)irsdk_yellowWaving, "YellowWaving"), 40 | MaskName((int)irsdk_oneLapToGreen, "OneLapToGreen"), 41 | MaskName((int)irsdk_greenHeld, "GreenHeld"), 42 | MaskName((int)irsdk_tenToGo, "TenToGo"), 43 | MaskName((int)irsdk_fiveToGo, "FiveToGo"), 44 | MaskName((int)irsdk_randomWaving, "RandomWaving"), 45 | MaskName((int)irsdk_caution, "Caution"), 46 | MaskName((int)irsdk_cautionWaving, "CautionWaving"), 47 | 48 | // drivers black flags 49 | MaskName((int)irsdk_black, "Black"), 50 | MaskName((int)irsdk_disqualify, "Disqualify"), 51 | MaskName((int)irsdk_servicible, "Servicible"), 52 | MaskName((int)irsdk_furled, "Furled"), 53 | MaskName((int)irsdk_repair, "Repair"), 54 | 55 | // start lights 56 | MaskName((int)irsdk_startHidden, "StartHidden"), 57 | MaskName((int)irsdk_startReady, "StartReady"), 58 | MaskName((int)irsdk_startSet, "StartSet"), 59 | MaskName((int)irsdk_startGo, "StartGo")}; 60 | 61 | const std::vector PIT_SV_MASKS = { 62 | MaskName((int)irsdk_LFTireChange, "LFTireChange"), 63 | MaskName((int)irsdk_RFTireChange, "RFTireChange"), 64 | MaskName((int)irsdk_LRTireChange, "LRTireChange"), 65 | MaskName((int)irsdk_RRTireChange, "RRTireChange"), 66 | 67 | MaskName((int)irsdk_FuelFill, "FuelFill"), 68 | MaskName((int)irsdk_WindshieldTearoff, "WindshieldTearoff"), 69 | MaskName((int)irsdk_FastRepair, "FastRepair")}; 70 | 71 | const std::vector PIT_SV_STATUS = { 72 | MaskName((int)irsdk_PitSvNone, "PitSvNone"), 73 | MaskName((int)irsdk_PitSvInProgress, "PitSvInProgress"), 74 | MaskName((int)irsdk_PitSvComplete, "PitSvComplete"), 75 | MaskName((int)irsdk_PitSvTooFarLeft, "PitSvTooFarLeft"), 76 | MaskName((int)irsdk_PitSvTooFarRight, "PitSvTooFarRight"), 77 | MaskName((int)irsdk_PitSvTooFarForward, "PitSvTooFarForward"), 78 | MaskName((int)irsdk_PitSvTooFarBack, "PitSvTooFarBack"), 79 | MaskName((int)irsdk_PitSvBadAngle, "PitSvBadAngle"), 80 | MaskName((int)irsdk_PitSvCantFixThat, "PitSvCantFixThat"), 81 | }; 82 | 83 | const std::vector CAMERA_STATE_MASKS = { 84 | MaskName((int)irsdk_IsSessionScreen, "IsSessionScreen"), 85 | MaskName((int)irsdk_IsScenicActive, "IsScenicActive"), 86 | 87 | // these can be changed with a broadcast message 88 | MaskName((int)irsdk_CamToolActive, "CamToolActive"), 89 | MaskName((int)irsdk_UIHidden, "UIHidden"), 90 | MaskName((int)irsdk_UseAutoShotSelection, "UseAutoShotSelection"), 91 | MaskName((int)irsdk_UseTemporaryEdits, "UseTemporaryEdits"), 92 | MaskName((int)irsdk_UseKeyAcceleration, "UseKeyAcceleration"), 93 | MaskName((int)irsdk_UseKey10xAcceleration, "UseKey10xAcceleration"), 94 | MaskName((int)irsdk_UseMouseAimMode, "UseMouseAimMode")}; 95 | 96 | const std::vector ENGINE_WARNINGS_MASKS = { 97 | MaskName((int)irsdk_waterTempWarning, "WaterTempWarning"), 98 | MaskName((int)irsdk_fuelPressureWarning, "FuelPressureWarning"), 99 | MaskName((int)irsdk_oilPressureWarning, "OilPressureWarning"), 100 | MaskName((int)irsdk_engineStalled, "EngineStalled"), 101 | MaskName((int)irsdk_pitSpeedLimiter, "PitSpeedLimiter"), 102 | MaskName((int)irsdk_revLimiterActive, "RevLimiterActive"), 103 | MaskName((int)irsdk_oilTempWarning, "OilTempWarning"), 104 | }; 105 | 106 | const std::vector SESSION_STATES = { 107 | MaskName((int)irsdk_StateInvalid, "Invalid"), 108 | MaskName((int)irsdk_StateGetInCar, "GetInCar"), 109 | MaskName((int)irsdk_StateWarmup, "Warmup"), 110 | MaskName((int)irsdk_StateParadeLaps, "ParadeLaps"), 111 | MaskName((int)irsdk_StateRacing, "Racing"), 112 | MaskName((int)irsdk_StateCheckered, "Checkered"), 113 | MaskName((int)irsdk_StateCoolDown, "CoolDown")}; 114 | 115 | const std::vector TRACK_LOCS = { 116 | MaskName((int)irsdk_NotInWorld, "NotInWorld"), 117 | MaskName((int)irsdk_OffTrack, "OffTrack"), 118 | MaskName((int)irsdk_InPitStall, "InPitStall"), 119 | MaskName((int)irsdk_AproachingPits, "AproachingPits"), 120 | MaskName((int)irsdk_OnTrack, "OnTrack")}; 121 | 122 | const std::vector TRACK_SURF = { 123 | MaskName((int)irsdk_SurfaceNotInWorld, "SurfaceNotInWorld"), 124 | MaskName((int)irsdk_UndefinedMaterial, "UndefinedMaterial"), 125 | 126 | MaskName((int)irsdk_Asphalt1Material, "Asphalt1Material"), 127 | MaskName((int)irsdk_Asphalt2Material, "Asphalt2Material"), 128 | MaskName((int)irsdk_Asphalt3Material, "Asphalt3Material"), 129 | MaskName((int)irsdk_Asphalt4Material, "Asphalt4Material"), 130 | MaskName((int)irsdk_Concrete1Material, "Concrete1Material"), 131 | MaskName((int)irsdk_Concrete2Material, "Concrete2Material"), 132 | MaskName((int)irsdk_RacingDirt1Material, "RacingDirt1Material"), 133 | MaskName((int)irsdk_RacingDirt2Material, "RacingDirt2Material"), 134 | MaskName((int)irsdk_Paint1Material, "Paint1Material"), 135 | MaskName((int)irsdk_Paint2Material, "Paint2Material"), 136 | MaskName((int)irsdk_Rumble1Material, "Rumble1Material"), 137 | MaskName((int)irsdk_Rumble2Material, "Rumble2Material"), 138 | MaskName((int)irsdk_Rumble3Material, "Rumble3Material"), 139 | MaskName((int)irsdk_Rumble4Material, "Rumble4Material"), 140 | 141 | MaskName((int)irsdk_Grass1Material, "Grass1Material"), 142 | MaskName((int)irsdk_Grass2Material, "Grass2Material"), 143 | MaskName((int)irsdk_Grass3Material, "Grass3Material"), 144 | MaskName((int)irsdk_Grass4Material, "Grass4Material"), 145 | MaskName((int)irsdk_Dirt1Material, "Dirt1Material"), 146 | MaskName((int)irsdk_Dirt2Material, "Dirt2Material"), 147 | MaskName((int)irsdk_Dirt3Material, "Dirt3Material"), 148 | MaskName((int)irsdk_Dirt4Material, "Dirt4Material"), 149 | MaskName((int)irsdk_SandMaterial, "SandMaterial"), 150 | MaskName((int)irsdk_Gravel1Material, "Gravel1Material"), 151 | MaskName((int)irsdk_Gravel2Material, "Gravel2Material"), 152 | MaskName((int)irsdk_GrasscreteMaterial, "GrasscreteMaterial"), 153 | MaskName((int)irsdk_AstroturfMaterial, "AstroturfMaterial"), 154 | }; 155 | 156 | const std::vector CAR_BESIDE = { 157 | MaskName((int)irsdk_LROff, "LROff"), 158 | MaskName((int)irsdk_LRClear, "LRClear"), // no cars around us. 159 | MaskName((int)irsdk_LRCarLeft, "LRCarLeft"), // there is a car to our left. 160 | MaskName((int)irsdk_LRCarRight, "LRCarRight"), // there is a car to our right. 161 | MaskName((int)irsdk_LRCarLeftRight, "LRCarLeftRight"), // there are cars on each side. 162 | MaskName((int)irsdk_LR2CarsLeft, "LR2CarsLeft"), // there are two cars to our left. 163 | MaskName((int)irsdk_LR2CarsRight, "LR2CarsRight") // there are two cars to our right. 164 | }; 165 | 166 | const std::vector PACE_MODE = { 167 | MaskName((int)irsdk_PaceModeSingleFileStart, "PaceModeSingleFile"), 168 | MaskName((int)irsdk_PaceModeDoubleFileStart, "PaceModeDoubleFile"), 169 | MaskName((int)irsdk_PaceModeSingleFileRestart, "PaceModeSingleFileRestart"), 170 | MaskName((int)irsdk_PaceModeDoubleFileRestart, "PaceModeDoubleFileRestart"), 171 | MaskName((int)irsdk_PaceModeNotPacing, "PaceModeNotPacing"), 172 | }; 173 | 174 | const std::vector PACE_FLAGS = { 175 | MaskName((int)irsdk_PaceFlagsEndOfLine, "PaceFlagsEndOfLine"), 176 | MaskName((int)irsdk_PaceFlagsFreePass, "PaceFlagsFreePass"), 177 | MaskName((int)irsdk_PaceFlagsFreePass, "PaceFlagsFreePass"), 178 | }; 179 | 180 | const std::vector TRACK_WETNESS = { 181 | MaskName((int)irsdk_TrackWetness_UNKNOWN, "Unknown"), 182 | MaskName((int)irsdk_TrackWetness_Dry, "Dry"), 183 | MaskName((int)irsdk_TrackWetness_MostlyDry, "MostlyDry"), 184 | MaskName((int)irsdk_TrackWetness_VeryLightlyWet, "VeryLightlyWet"), 185 | MaskName((int)irsdk_TrackWetness_LightlyWet, "LightlyWet"), 186 | MaskName((int)irsdk_TrackWetness_ModeratelyWet, "ModeratelyWet"), 187 | MaskName((int)irsdk_TrackWetness_VeryWet, "VeryWet"), 188 | MaskName((int)irsdk_TrackWetness_ExtremelyWet, "ExtremelyWet"), 189 | }; 190 | 191 | }; // namespace NodeIrSdk 192 | -------------------------------------------------------------------------------- /src/cpp/IrSdkCommand.cpp: -------------------------------------------------------------------------------- 1 | #include "IrSdkCommand.h" 2 | 3 | #ifdef _WIN32 4 | #include "irsdk/irsdk_defines.h" 5 | #define WIN32_LEAN_AND_MEAN 6 | #include 7 | 8 | void NodeIrSdk::broadcastCmd(int cmd, int var1, int var2) 9 | { 10 | static unsigned int msgId = RegisterWindowMessageA((LPCSTR)IRSDK_BROADCASTMSGNAME); 11 | 12 | if (cmd >= 0 && cmd < irsdk_BroadcastLast) 13 | { 14 | SendNotifyMessage(HWND_BROADCAST, msgId, MAKELONG(cmd, var1), var2); 15 | } 16 | } 17 | 18 | void NodeIrSdk::broadcastCmd(int cmd, int var1, int var2, int var3) 19 | { 20 | broadcastCmd(cmd, var1, MAKELONG(var2, var3)); 21 | } 22 | #endif 23 | -------------------------------------------------------------------------------- /src/cpp/IrSdkCommand.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "irsdk/irsdk_defines.h" 4 | #include "platform/platform.h" 5 | 6 | namespace NodeIrSdk 7 | { 8 | #ifdef _WIN32 9 | void broadcastCmd(int cmd, int var1, int var2); 10 | void broadcastCmd(int cmd, int var1, int var2, int var3); 11 | #else 12 | // On non-Windows platforms, these are no-ops since iRacing only runs on Windows 13 | inline void broadcastCmd(int cmd, int var1, int var2) {} 14 | inline void broadcastCmd(int cmd, int var1, int var2, int var3) {} 15 | #endif 16 | } 17 | -------------------------------------------------------------------------------- /src/cpp/IrSdkNodeBindings.cpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "IRSDKWrapper.h" 4 | #include "IrSdkBindingHelpers.h" 5 | #include "IrSdkNodeBindings.h" 6 | #include "IrSdkCommand.h" 7 | 8 | #include 9 | #include 10 | 11 | using namespace v8; 12 | 13 | namespace NodeIrSdk 14 | { 15 | 16 | void start(const Nan::FunctionCallbackInfo &args) 17 | { 18 | args.GetReturnValue().Set(Nan::New(irsdk.startup())); 19 | } 20 | 21 | void shutdown(const Nan::FunctionCallbackInfo &args) 22 | { 23 | irsdk.shutdown(); 24 | args.GetReturnValue().Set(Nan::Undefined()); 25 | } 26 | 27 | void isInitialized(const Nan::FunctionCallbackInfo &args) 28 | { 29 | args.GetReturnValue().Set(Nan::New(irsdk.isInitialized())); 30 | } 31 | 32 | void isConnected(const Nan::FunctionCallbackInfo &args) 33 | { 34 | args.GetReturnValue().Set(Nan::New(irsdk.isConnected())); 35 | } 36 | 37 | void updateSessionInfo(const Nan::FunctionCallbackInfo &args) 38 | { 39 | args.GetReturnValue().Set(Nan::New(irsdk.updateSessionInfo())); 40 | } 41 | 42 | void getSessionInfo(const Nan::FunctionCallbackInfo &args) 43 | { 44 | args.GetReturnValue().Set( 45 | Nan::Encode(irsdk.getSessionInfo().c_str(), irsdk.getSessionInfo().length(), Nan::BINARY)); 46 | } 47 | 48 | void updateTelemetry(const Nan::FunctionCallbackInfo &args) 49 | { 50 | args.GetReturnValue().Set(Nan::New(irsdk.updateTelemetry())); 51 | } 52 | 53 | void getTelemetry(const Nan::FunctionCallbackInfo &args) 54 | { 55 | Local rootObj = Nan::New(); 56 | Local valuesObj = Nan::New(); 57 | Nan::Set(rootObj, Nan::New("timestamp").ToLocalChecked(), Nan::New(irsdk.getLastTelemetryUpdateTS()).ToLocalChecked()); 58 | 59 | std::vector headers = irsdk.getVarHeaders(); 60 | 61 | for (const auto item : headers) 62 | { 63 | IRSDKWrapper::TelemetryVar var(item); 64 | irsdk.getVarVal(var); 65 | Local varValue = convertTelemetryVarToObject(var); 66 | Nan::Set(valuesObj, Nan::New(var.header->name).ToLocalChecked(), varValue); 67 | } 68 | Nan::Set(rootObj, Nan::New("values").ToLocalChecked(), valuesObj); 69 | args.GetReturnValue().Set(rootObj); 70 | } 71 | 72 | void getTelemetryDescription(const Nan::FunctionCallbackInfo &args) 73 | { 74 | Local obj = Nan::New(); 75 | std::vector headers = irsdk.getVarHeaders(); 76 | 77 | for (const auto item : headers) 78 | { 79 | IRSDKWrapper::TelemetryVar var(item); 80 | irsdk.getVarVal(var); 81 | Local varObj = Nan::New(); 82 | convertVarHeaderToObject(var, varObj); 83 | Nan::Set(obj, Nan::New(var.header->name).ToLocalChecked(), varObj); 84 | } 85 | args.GetReturnValue().Set(obj); 86 | } 87 | 88 | NAN_METHOD(sendCmd) 89 | { 90 | 91 | if (!irsdk.isInitialized() || !irsdk.isConnected()) 92 | return; 93 | 94 | if (info.Length() > 4 || info.Length() < 1) 95 | { 96 | std::cerr << "sendCommand: invalid arguments (1 to 4 accepted)" << std::endl; 97 | return; 98 | } 99 | 100 | for (int i = 0; i < info.Length(); ++i) 101 | { 102 | if (!info[i]->IsInt32()) 103 | { 104 | std::cerr << "sendCommand: invalid argument type, int32 needed" << std::endl; 105 | return; 106 | } 107 | } 108 | 109 | switch (info.Length()) 110 | { 111 | case 1: 112 | broadcastCmd( 113 | info[0]->Int32Value(Nan::GetCurrentContext()).FromJust(), 114 | 0, 115 | 0); 116 | break; 117 | case 2: 118 | broadcastCmd( 119 | info[0]->Int32Value(Nan::GetCurrentContext()).FromJust(), 120 | info[1]->Int32Value(Nan::GetCurrentContext()).FromJust(), 121 | 0); 122 | break; 123 | case 3: 124 | broadcastCmd( 125 | info[0]->Int32Value(Nan::GetCurrentContext()).FromJust(), 126 | info[1]->Int32Value(Nan::GetCurrentContext()).FromJust(), 127 | info[2]->Int32Value(Nan::GetCurrentContext()).FromJust()); 128 | break; 129 | case 4: 130 | broadcastCmd( 131 | info[0]->Int32Value(Nan::GetCurrentContext()).FromJust(), 132 | info[1]->Int32Value(Nan::GetCurrentContext()).FromJust(), 133 | info[2]->Int32Value(Nan::GetCurrentContext()).FromJust(), 134 | info[3]->Int32Value(Nan::GetCurrentContext()).FromJust()); 135 | break; 136 | } 137 | } 138 | 139 | static void cleanUp(void *arg) 140 | { 141 | irsdk.shutdown(); 142 | } 143 | } // namespace NodeIrSdk 144 | -------------------------------------------------------------------------------- /src/cpp/IrSdkNodeBindings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | using namespace v8; 7 | 8 | namespace NodeIrSdk 9 | { 10 | 11 | IRSDKWrapper irsdk; 12 | 13 | void start(const Nan::FunctionCallbackInfo &args); 14 | 15 | void shutdown(const Nan::FunctionCallbackInfo &args); 16 | 17 | void isInitialized(const Nan::FunctionCallbackInfo &args); 18 | 19 | void isConnected(const Nan::FunctionCallbackInfo &args); 20 | 21 | void updateSessionInfo(const Nan::FunctionCallbackInfo &args); 22 | 23 | void getSessionInfo(const Nan::FunctionCallbackInfo &args); 24 | 25 | void updateTelemetry(const Nan::FunctionCallbackInfo &args); 26 | 27 | void getTelemetry(const Nan::FunctionCallbackInfo &args); 28 | 29 | void getTelemetryDescription(const Nan::FunctionCallbackInfo &args); 30 | 31 | NAN_METHOD(sendCmd); 32 | 33 | static void cleanUp(void *arg); 34 | 35 | // this defines public api of native addon 36 | NAN_MODULE_INIT(init) 37 | { 38 | irsdk.startup(); 39 | 40 | node::Environment *env = 41 | node::GetCurrentEnvironment(Nan::GetCurrentContext()); 42 | node::AtExit(env, cleanUp, NULL); 43 | 44 | NAN_EXPORT(target, start); 45 | NAN_EXPORT(target, shutdown); 46 | 47 | NAN_EXPORT(target, isInitialized); 48 | NAN_EXPORT(target, isConnected); 49 | 50 | NAN_EXPORT(target, updateSessionInfo); 51 | NAN_EXPORT(target, getSessionInfo); 52 | 53 | NAN_EXPORT(target, updateTelemetry); 54 | NAN_EXPORT(target, getTelemetryDescription); 55 | NAN_EXPORT(target, getTelemetry); 56 | 57 | NAN_EXPORT(target, sendCmd); 58 | } 59 | 60 | // name of native addon 61 | NODE_MODULE(IrSdkNodeBindings, init) 62 | } 63 | -------------------------------------------------------------------------------- /src/cpp/irsdk/irsdk_defines.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2013, iRacing.com Motorsport Simulations, LLC. 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of iRacing.com Motorsport Simulations nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | #ifndef IRSDK_DEFINES_H 29 | #define IRSDK_DEFINES_H 30 | 31 | #include 32 | #include 33 | 34 | /* 35 | The IRSDK is a simple api that lets clients access telemetry data from the 36 | iRacing simulator. It is broken down into several parts: 37 | 38 | - Live data 39 | Live data is output from the sim into a shared memory mapped file. Any 40 | application can open this memory mapped file and read the telemetry data 41 | out. The format of this data was laid out in such a way that it should be 42 | possible to access from any language that can open a windows memory mapped 43 | file, without needing an external api. 44 | 45 | There are two different types of data that the telemetry outputs, 46 | sessionInfo and variables: 47 | 48 | Session info is for data that only needs to be updated every once in a 49 | while. This data is output as a YAML formatted string. 50 | 51 | Variables, on the other hand, are output at a rate of 60 times a second. 52 | The varHeader struct defines each variable that the sim will output, while 53 | the varData struct gives details about the current line buffer that the vars 54 | are being written into. Each variable is packed into a binary array with 55 | an offset and length stored in the varHeader. The number of variables 56 | available can change depending on the car or session loaded. But once the 57 | sim is running the variable list is locked down and will not change during a 58 | session. 59 | 60 | The sim writes a new line of variables every 16 ms, and then signals any 61 | listeners in order to wake them up to read the data. Because the sim has no 62 | way of knowing when a listener is done reading the data, we triple buffer 63 | it in order to give all the clients enough time to read the data out. This 64 | gives you a minimum of 16 ms to read the data out and process it. So it is 65 | best to copy the data out before processing it. You can use the function 66 | irsdk_waitForDataReady() to both wait for new data and copy the data to a 67 | local buffer. 68 | 69 | - Logged data 70 | Detailed information about the local drivers car can be logged to disk in 71 | the form of an ibt binary file. This logging is enabled in the sim by 72 | typing alt-L at any time. The ibt file format directly mirrors the format 73 | of the live data. 74 | 75 | It is stored as an irsdk_header followed immediately by an irsdk_diskSubHeader. 76 | After that the offsets in the irsdk_header point to the sessionInfo string, 77 | the varHeader, and the varBuffer. 78 | 79 | - Remote Conrol 80 | You can control the camera selections and playback of a replay tape, from 81 | any external application by sending a windows message with the 82 | irsdk_broadcastMsg() function. 83 | */ 84 | 85 | // Constant Definitions 86 | 87 | #ifdef _WIN32 88 | #include 89 | 90 | static const _TCHAR IRSDK_DATAVALIDEVENTNAME[] = _T("Local\\IRSDKDataValidEvent"); 91 | static const _TCHAR IRSDK_MEMMAPFILENAME[] = _T("Local\\IRSDKMemMapFileName"); 92 | static const _TCHAR IRSDK_BROADCASTMSGNAME[] = _T("IRSDK_BROADCASTMSG"); 93 | 94 | #else 95 | 96 | static const char *IRSDK_DATAVALIDEVENTNAME = "Local\\IRSDKDataValidEvent"; 97 | static const char *IRSDK_MEMMAPFILENAME = "Local\\IRSDKMemMapFileName"; 98 | static const char *IRSDK_BROADCASTMSGNAME = "IRSDK_BROADCASTMSG"; 99 | #endif 100 | 101 | static const int IRSDK_MAX_BUFS = 4; 102 | static const int IRSDK_MAX_STRING = 32; 103 | // descriptions can be longer than max_string! 104 | static const int IRSDK_MAX_DESC = 64; 105 | 106 | // define markers for unlimited session lap and time 107 | static const int IRSDK_UNLIMITED_LAPS = 32767; 108 | static const float IRSDK_UNLIMITED_TIME = 604800.0f; 109 | 110 | // latest version of our telemetry headers 111 | static const int IRSDK_VER = 2; 112 | 113 | enum irsdk_StatusField 114 | { 115 | irsdk_stConnected = 1 116 | }; 117 | 118 | enum irsdk_VarType 119 | { 120 | // 1 byte 121 | irsdk_char = 0, 122 | irsdk_bool, 123 | 124 | // 4 bytes 125 | irsdk_int, 126 | irsdk_bitField, 127 | irsdk_float, 128 | 129 | // 8 bytes 130 | irsdk_double, 131 | 132 | // index, don't use 133 | irsdk_ETCount 134 | }; 135 | 136 | static const int irsdk_VarTypeBytes[irsdk_ETCount] = 137 | { 138 | 1, // irsdk_char 139 | 1, // irsdk_bool 140 | 141 | 4, // irsdk_int 142 | 4, // irsdk_bitField 143 | 4, // irsdk_float 144 | 145 | 8 // irsdk_double 146 | }; 147 | 148 | //--- 149 | 150 | // status 151 | enum irsdk_TrkLoc 152 | { 153 | irsdk_NotInWorld = -1, 154 | irsdk_OffTrack = 0, 155 | irsdk_InPitStall, 156 | // This indicates the lead in to pit road, as well as the pit road itself (where speed limits are enforced) 157 | // if you just want to know that your on the pit road surface look at the live value 'OnPitRoad' 158 | irsdk_AproachingPits, 159 | irsdk_OnTrack 160 | }; 161 | 162 | enum irsdk_TrkSurf 163 | { 164 | irsdk_SurfaceNotInWorld = -1, 165 | irsdk_UndefinedMaterial = 0, 166 | 167 | irsdk_Asphalt1Material, 168 | irsdk_Asphalt2Material, 169 | irsdk_Asphalt3Material, 170 | irsdk_Asphalt4Material, 171 | irsdk_Concrete1Material, 172 | irsdk_Concrete2Material, 173 | irsdk_RacingDirt1Material, 174 | irsdk_RacingDirt2Material, 175 | irsdk_Paint1Material, 176 | irsdk_Paint2Material, 177 | irsdk_Rumble1Material, 178 | irsdk_Rumble2Material, 179 | irsdk_Rumble3Material, 180 | irsdk_Rumble4Material, 181 | 182 | irsdk_Grass1Material, 183 | irsdk_Grass2Material, 184 | irsdk_Grass3Material, 185 | irsdk_Grass4Material, 186 | irsdk_Dirt1Material, 187 | irsdk_Dirt2Material, 188 | irsdk_Dirt3Material, 189 | irsdk_Dirt4Material, 190 | irsdk_SandMaterial, 191 | irsdk_Gravel1Material, 192 | irsdk_Gravel2Material, 193 | irsdk_GrasscreteMaterial, 194 | irsdk_AstroturfMaterial, 195 | }; 196 | 197 | enum irsdk_SessionState 198 | { 199 | irsdk_StateInvalid = 0, 200 | irsdk_StateGetInCar, 201 | irsdk_StateWarmup, 202 | irsdk_StateParadeLaps, 203 | irsdk_StateRacing, 204 | irsdk_StateCheckered, 205 | irsdk_StateCoolDown 206 | }; 207 | 208 | enum irsdk_CarLeftRight 209 | { 210 | irsdk_LROff = 0, 211 | irsdk_LRClear, // no cars around us. 212 | irsdk_LRCarLeft, // there is a car to our left. 213 | irsdk_LRCarRight, // there is a car to our right. 214 | irsdk_LRCarLeftRight, // there are cars on each side. 215 | irsdk_LR2CarsLeft, // there are two cars to our left. 216 | irsdk_LR2CarsRight // there are two cars to our right. 217 | }; 218 | 219 | enum irsdk_PitSvStatus 220 | { 221 | // status 222 | irsdk_PitSvNone = 0, 223 | irsdk_PitSvInProgress, 224 | irsdk_PitSvComplete, 225 | 226 | // errors 227 | irsdk_PitSvTooFarLeft = 100, 228 | irsdk_PitSvTooFarRight, 229 | irsdk_PitSvTooFarForward, 230 | irsdk_PitSvTooFarBack, 231 | irsdk_PitSvBadAngle, 232 | irsdk_PitSvCantFixThat, 233 | }; 234 | 235 | enum irsdk_PaceMode 236 | { 237 | irsdk_PaceModeSingleFileStart = 0, 238 | irsdk_PaceModeDoubleFileStart, 239 | irsdk_PaceModeSingleFileRestart, 240 | irsdk_PaceModeDoubleFileRestart, 241 | irsdk_PaceModeNotPacing, 242 | }; 243 | 244 | enum irsdk_TrackWetness 245 | { 246 | irsdk_TrackWetness_UNKNOWN = 0, 247 | irsdk_TrackWetness_Dry, 248 | irsdk_TrackWetness_MostlyDry, 249 | irsdk_TrackWetness_VeryLightlyWet, 250 | irsdk_TrackWetness_LightlyWet, 251 | irsdk_TrackWetness_ModeratelyWet, 252 | irsdk_TrackWetness_VeryWet, 253 | irsdk_TrackWetness_ExtremelyWet 254 | }; 255 | 256 | //--- 257 | 258 | // bit fields 259 | enum irsdk_EngineWarnings 260 | { 261 | irsdk_waterTempWarning = 0x0001, 262 | irsdk_fuelPressureWarning = 0x0002, 263 | irsdk_oilPressureWarning = 0x0004, 264 | irsdk_engineStalled = 0x0008, 265 | irsdk_pitSpeedLimiter = 0x0010, 266 | irsdk_revLimiterActive = 0x0020, 267 | irsdk_oilTempWarning = 0x0040, 268 | }; 269 | 270 | // global flags 271 | enum irsdk_Flags 272 | { 273 | // global flags 274 | irsdk_checkered = 0x00000001, 275 | irsdk_white = 0x00000002, 276 | irsdk_green = 0x00000004, 277 | irsdk_yellow = 0x00000008, 278 | irsdk_red = 0x00000010, 279 | irsdk_blue = 0x00000020, 280 | irsdk_debris = 0x00000040, 281 | irsdk_crossed = 0x00000080, 282 | irsdk_yellowWaving = 0x00000100, 283 | irsdk_oneLapToGreen = 0x00000200, 284 | irsdk_greenHeld = 0x00000400, 285 | irsdk_tenToGo = 0x00000800, 286 | irsdk_fiveToGo = 0x00001000, 287 | irsdk_randomWaving = 0x00002000, 288 | irsdk_caution = 0x00004000, 289 | irsdk_cautionWaving = 0x00008000, 290 | 291 | // drivers black flags 292 | irsdk_black = 0x00010000, 293 | irsdk_disqualify = 0x00020000, 294 | irsdk_servicible = 0x00040000, // car is allowed service (not a flag) 295 | irsdk_furled = 0x00080000, 296 | irsdk_repair = 0x00100000, 297 | 298 | // start lights 299 | irsdk_startHidden = 0x10000000, 300 | irsdk_startReady = 0x20000000, 301 | irsdk_startSet = 0x40000000, 302 | irsdk_startGo = 0x80000000, 303 | }; 304 | 305 | enum irsdk_CameraState 306 | { 307 | irsdk_IsSessionScreen = 0x0001, // the camera tool can only be activated if viewing the session screen (out of car) 308 | irsdk_IsScenicActive = 0x0002, // the scenic camera is active (no focus car) 309 | 310 | // these can be changed with a broadcast message 311 | irsdk_CamToolActive = 0x0004, 312 | irsdk_UIHidden = 0x0008, 313 | irsdk_UseAutoShotSelection = 0x0010, 314 | irsdk_UseTemporaryEdits = 0x0020, 315 | irsdk_UseKeyAcceleration = 0x0040, 316 | irsdk_UseKey10xAcceleration = 0x0080, 317 | irsdk_UseMouseAimMode = 0x0100 318 | }; 319 | 320 | enum irsdk_PitSvFlags 321 | { 322 | irsdk_LFTireChange = 0x0001, 323 | irsdk_RFTireChange = 0x0002, 324 | irsdk_LRTireChange = 0x0004, 325 | irsdk_RRTireChange = 0x0008, 326 | 327 | irsdk_FuelFill = 0x0010, 328 | irsdk_WindshieldTearoff = 0x0020, 329 | irsdk_FastRepair = 0x0040 330 | }; 331 | 332 | enum irsdk_PaceFlags 333 | { 334 | irsdk_PaceFlagsEndOfLine = 0x0001, 335 | irsdk_PaceFlagsFreePass = 0x0002, 336 | irsdk_PaceFlagsWavedAround = 0x0004, 337 | }; 338 | 339 | //---- 340 | // 341 | 342 | struct irsdk_varHeader 343 | { 344 | int type; // irsdk_VarType 345 | int offset; // offset fron start of buffer row 346 | int count; // number of entrys (array) 347 | // so length in bytes would be irsdk_VarTypeBytes[type] * count 348 | bool countAsTime; 349 | char pad[3]; // (16 byte align) 350 | 351 | char name[IRSDK_MAX_STRING]; 352 | char desc[IRSDK_MAX_DESC]; 353 | char unit[IRSDK_MAX_STRING]; // something like "kg/m^2" 354 | 355 | void clear() 356 | { 357 | type = 0; 358 | offset = 0; 359 | count = 0; 360 | countAsTime = false; 361 | memset(name, 0, sizeof(name)); 362 | memset(desc, 0, sizeof(name)); 363 | memset(unit, 0, sizeof(name)); 364 | } 365 | }; 366 | 367 | struct irsdk_varBuf 368 | { 369 | int tickCount; // used to detect changes in data 370 | int bufOffset; // offset from header 371 | int pad[2]; // (16 byte align) 372 | }; 373 | 374 | struct irsdk_header 375 | { 376 | int ver; // this api header version, see IRSDK_VER 377 | int status; // bitfield using irsdk_StatusField 378 | int tickRate; // ticks per second (60 or 360 etc) 379 | 380 | // session information, updated periodicaly 381 | int sessionInfoUpdate; // Incremented when session info changes 382 | int sessionInfoLen; // Length in bytes of session info string 383 | int sessionInfoOffset; // Session info, encoded in YAML format 384 | 385 | // State data, output at tickRate 386 | 387 | int numVars; // length of arra pointed to by varHeaderOffset 388 | int varHeaderOffset; // offset to irsdk_varHeader[numVars] array, Describes the variables received in varBuf 389 | 390 | int numBuf; // <= IRSDK_MAX_BUFS (3 for now) 391 | int bufLen; // length in bytes for one line 392 | //****ToDo, add these in 393 | // int curBufTickCount; // stashed copy of the current tickCount, can read this to see if new data is available 394 | // byte curBuf; // index of the most recently written buffer (0 to IRSDK_MAX_BUFS-1) 395 | // byte pad1[3]; // 16 byte align 396 | int pad1[2]; // (16 byte align) 397 | irsdk_varBuf varBuf[IRSDK_MAX_BUFS]; // buffers of data being written to 398 | }; 399 | 400 | // sub header used when writing telemetry to disk 401 | struct irsdk_diskSubHeader 402 | { 403 | time_t sessionStartDate; 404 | double sessionStartTime; 405 | double sessionEndTime; 406 | int sessionLapCount; 407 | int sessionRecordCount; 408 | }; 409 | 410 | //---- 411 | // Client function definitions 412 | 413 | bool irsdk_startup(); 414 | void irsdk_shutdown(); 415 | 416 | bool irsdk_getNewData(char *data); 417 | bool irsdk_waitForDataReady(int timeOut, char *data); 418 | bool irsdk_isConnected(); 419 | 420 | const irsdk_header *irsdk_getHeader(); 421 | const char *irsdk_getData(int index); 422 | const char *irsdk_getSessionInfoStr(); 423 | int irsdk_getSessionInfoStrUpdate(); // incrementing index that indicates new session info string 424 | 425 | const irsdk_varHeader *irsdk_getVarHeaderPtr(); 426 | const irsdk_varHeader *irsdk_getVarHeaderEntry(int index); 427 | 428 | int irsdk_varNameToIndex(const char *name); 429 | int irsdk_varNameToOffset(const char *name); 430 | 431 | //---- 432 | // Remote controll the sim by sending these windows messages 433 | // camera and replay commands only work when you are out of your car, 434 | // pit commands only work when in your car 435 | enum irsdk_BroadcastMsg 436 | { 437 | irsdk_BroadcastCamSwitchPos = 0, // car position, group, camera 438 | irsdk_BroadcastCamSwitchNum, // driver #, group, camera 439 | irsdk_BroadcastCamSetState, // irsdk_CameraState, unused, unused 440 | irsdk_BroadcastReplaySetPlaySpeed, // speed, slowMotion, unused 441 | irskd_BroadcastReplaySetPlayPosition, // irsdk_RpyPosMode, Frame Number (high, low) 442 | irsdk_BroadcastReplaySearch, // irsdk_RpySrchMode, unused, unused 443 | irsdk_BroadcastReplaySetState, // irsdk_RpyStateMode, unused, unused 444 | irsdk_BroadcastReloadTextures, // irsdk_ReloadTexturesMode, carIdx, unused 445 | irsdk_BroadcastChatComand, // irsdk_ChatCommandMode, subCommand, unused 446 | irsdk_BroadcastPitCommand, // irsdk_PitCommandMode, parameter 447 | irsdk_BroadcastTelemCommand, // irsdk_TelemCommandMode, unused, unused 448 | irsdk_BroadcastFFBCommand, // irsdk_FFBCommandMode, value (float, high, low) 449 | irsdk_BroadcastReplaySearchSessionTime, // sessionNum, sessionTimeMS (high, low) 450 | irsdk_BroadcastVideoCapture, // irsdk_VideoCaptureMode, unused, unused 451 | irsdk_BroadcastLast // unused placeholder 452 | }; 453 | 454 | enum irsdk_ChatCommandMode 455 | { 456 | irsdk_ChatCommand_Macro = 0, // pass in a number from 1-15 representing the chat macro to launch 457 | irsdk_ChatCommand_BeginChat, // Open up a new chat window 458 | irsdk_ChatCommand_Reply, // Reply to last private chat 459 | irsdk_ChatCommand_Cancel // Close chat window 460 | }; 461 | 462 | enum irsdk_PitCommandMode // this only works when the driver is in the car 463 | { 464 | irsdk_PitCommand_Clear = 0, // Clear all pit checkboxes 465 | irsdk_PitCommand_WS, // Clean the winshield, using one tear off 466 | irsdk_PitCommand_Fuel, // Add fuel, optionally specify the amount to add in liters or pass '0' to use existing amount 467 | irsdk_PitCommand_LF, // Change the left front tire, optionally specifying the pressure in KPa or pass '0' to use existing pressure 468 | irsdk_PitCommand_RF, // right front 469 | irsdk_PitCommand_LR, // left rear 470 | irsdk_PitCommand_RR, // right rear 471 | irsdk_PitCommand_ClearTires, // Clear tire pit checkboxes 472 | irsdk_PitCommand_FR, // Request a fast repair 473 | irsdk_PitCommand_ClearWS, // Uncheck Clean the winshield checkbox 474 | irsdk_PitCommand_ClearFR, // Uncheck request a fast repair 475 | irsdk_PitCommand_ClearFuel, // Uncheck add fuel 476 | irsdk_PitCommand_TC, // Change tire compound 477 | }; 478 | 479 | enum irsdk_TelemCommandMode // You can call this any time, but telemtry only records when driver is in there car 480 | { 481 | irsdk_TelemCommand_Stop = 0, // Turn telemetry recording off 482 | irsdk_TelemCommand_Start, // Turn telemetry recording on 483 | irsdk_TelemCommand_Restart, // Write current file to disk and start a new one 484 | }; 485 | 486 | enum irsdk_RpyStateMode 487 | { 488 | irsdk_RpyState_EraseTape = 0, // clear any data in the replay tape 489 | irsdk_RpyState_Last // unused place holder 490 | }; 491 | 492 | enum irsdk_ReloadTexturesMode 493 | { 494 | irsdk_ReloadTextures_All = 0, // reload all textuers 495 | irsdk_ReloadTextures_CarIdx // reload only textures for the specific carIdx 496 | }; 497 | 498 | // Search replay tape for events 499 | enum irsdk_RpySrchMode 500 | { 501 | irsdk_RpySrch_ToStart = 0, 502 | irsdk_RpySrch_ToEnd, 503 | irsdk_RpySrch_PrevSession, 504 | irsdk_RpySrch_NextSession, 505 | irsdk_RpySrch_PrevLap, 506 | irsdk_RpySrch_NextLap, 507 | irsdk_RpySrch_PrevFrame, 508 | irsdk_RpySrch_NextFrame, 509 | irsdk_RpySrch_PrevIncident, 510 | irsdk_RpySrch_NextIncident, 511 | irsdk_RpySrch_Last // unused placeholder 512 | }; 513 | 514 | enum irsdk_RpyPosMode 515 | { 516 | irsdk_RpyPos_Begin = 0, 517 | irsdk_RpyPos_Current, 518 | irsdk_RpyPos_End, 519 | irsdk_RpyPos_Last // unused placeholder 520 | }; 521 | 522 | enum irsdk_FFBCommandMode // You can call this any time 523 | { 524 | irsdk_FFBCommand_MaxForce = 0, // Set the maximum force when mapping steering torque force to direct input units (float in Nm) 525 | irsdk_FFBCommand_Last // unused placeholder 526 | }; 527 | 528 | // irsdk_BroadcastCamSwitchPos or irsdk_BroadcastCamSwitchNum camera focus defines 529 | // pass these in for the first parameter to select the 'focus at' types in the camera system. 530 | enum irsdk_csMode 531 | { 532 | irsdk_csFocusAtIncident = -3, 533 | irsdk_csFocusAtLeader = -2, 534 | irsdk_csFocusAtExiting = -1, 535 | // ctFocusAtDriver + car number... 536 | irsdk_csFocusAtDriver = 0 537 | }; 538 | 539 | enum irsdk_VideoCaptureMode 540 | { 541 | irsdk_VideoCapture_TriggerScreenShot = 0, // save a screenshot to disk 542 | irsdk_VideoCaptuer_StartVideoCapture, // start capturing video 543 | irsdk_VideoCaptuer_EndVideoCapture, // stop capturing video 544 | irsdk_VideoCaptuer_ToggleVideoCapture, // toggle video capture on/off 545 | irsdk_VideoCaptuer_ShowVideoTimer, // show video timer in upper left corner of display 546 | irsdk_VideoCaptuer_HideVideoTimer, // hide video timer 547 | }; 548 | 549 | // send a remote controll message to the sim 550 | // var1, var2, and var3 are all 16 bits signed 551 | void irsdk_broadcastMsg(irsdk_BroadcastMsg msg, int var1, int var2, int var3); 552 | // var2 can be a full 32 bits 553 | void irsdk_broadcastMsg(irsdk_BroadcastMsg msg, int var1, int var2); 554 | // var2 can be a full 32 bit float 555 | void irsdk_broadcastMsg(irsdk_BroadcastMsg msg, int var1, float var2); 556 | 557 | // add a leading zero (or zeros) to a car number 558 | // to encode car #001 call padCarNum(1,2) 559 | int irsdk_padCarNum(int num, int zero); 560 | 561 | #endif // IRSDK_DEFINES_H 562 | -------------------------------------------------------------------------------- /src/cpp/platform/platform.cpp: -------------------------------------------------------------------------------- 1 | #include "platform.h" 2 | #include 3 | #include 4 | 5 | #ifndef _WIN32 6 | #include 7 | #include 8 | #define INFINITE -1 9 | #endif 10 | 11 | namespace platform 12 | { 13 | 14 | SharedMemory::SharedMemory() : handle_(0), data_(nullptr), size_(0) 15 | #ifdef _WIN32 16 | , 17 | fileMapping_(NULL) 18 | #endif 19 | { 20 | } 21 | 22 | SharedMemory::~SharedMemory() 23 | { 24 | close(); 25 | } 26 | #ifdef _WIN32 27 | bool SharedMemory::open(const _TCHAR *name) 28 | { 29 | 30 | fileMapping_ = OpenFileMapping(FILE_MAP_READ, FALSE, name); 31 | if (fileMapping_ == NULL) 32 | { 33 | return false; 34 | } 35 | data_ = MapViewOfFile(fileMapping_, FILE_MAP_READ, 0, 0, 0); 36 | return data_ != nullptr; 37 | } 38 | #else 39 | 40 | bool SharedMemory::open(const char *name) 41 | { 42 | 43 | // On Unix systems, shared memory names must start with / 44 | std::string shmName = std::string("/") + (name[0] == '/' ? name + 1 : name); 45 | 46 | // Replace Windows path separator with underscore 47 | size_t pos = 0; 48 | while ((pos = shmName.find('\\', pos)) != std::string::npos) 49 | { 50 | shmName.replace(pos, 1, "_"); 51 | } 52 | 53 | handle_ = shm_open(shmName.c_str(), O_RDONLY, 0666); 54 | if (handle_ == -1) 55 | { 56 | return false; 57 | } 58 | 59 | struct stat sb; 60 | if (fstat(handle_, &sb) == -1) 61 | { 62 | close(); 63 | return false; 64 | } 65 | size_ = sb.st_size; 66 | 67 | data_ = mmap(nullptr, size_, PROT_READ, MAP_SHARED, handle_, 0); 68 | if (data_ == MAP_FAILED) 69 | { 70 | close(); 71 | return false; 72 | } 73 | return true; 74 | } 75 | #endif 76 | void SharedMemory::close() 77 | { 78 | #ifdef _WIN32 79 | if (data_) 80 | { 81 | UnmapViewOfFile(data_); 82 | data_ = nullptr; 83 | } 84 | if (fileMapping_) 85 | { 86 | CloseHandle(fileMapping_); 87 | fileMapping_ = NULL; 88 | } 89 | #else 90 | if (data_ && data_ != MAP_FAILED) 91 | { 92 | munmap(data_, size_); 93 | data_ = nullptr; 94 | } 95 | if (handle_ != -1) 96 | { 97 | ::close(handle_); 98 | handle_ = -1; 99 | } 100 | #endif 101 | } 102 | 103 | void *SharedMemory::getData() const 104 | { 105 | return data_; 106 | } 107 | 108 | bool SharedMemory::isOpen() const 109 | { 110 | #ifdef _WIN32 111 | return fileMapping_ != NULL && data_ != nullptr; 112 | #else 113 | return handle_ != -1 && data_ != nullptr && data_ != MAP_FAILED; 114 | #endif 115 | } 116 | 117 | Event::Event() : handle_( 118 | #ifdef _WIN32 119 | NULL 120 | #else 121 | SEM_FAILED 122 | #endif 123 | ) 124 | { 125 | } 126 | 127 | Event::~Event() 128 | { 129 | close(); 130 | } 131 | #ifdef _WIN32 132 | bool Event::create(const _TCHAR *name) 133 | { 134 | handle_ = OpenEvent(SYNCHRONIZE, FALSE, name); 135 | return handle_ != NULL; 136 | } 137 | #else 138 | 139 | bool Event::create(const char *name) 140 | { 141 | 142 | // On Unix systems we'll use a semaphore instead of an event 143 | std::string semName = std::string("/") + (name[0] == '/' ? name + 1 : name); 144 | 145 | // Replace Windows path separator with underscore 146 | size_t pos = 0; 147 | while ((pos = semName.find('\\', pos)) != std::string::npos) 148 | { 149 | semName.replace(pos, 1, "_"); 150 | } 151 | 152 | handle_ = sem_open(semName.c_str(), O_CREAT, 0666, 0); 153 | return handle_ != SEM_FAILED; 154 | } 155 | #endif 156 | void Event::close() 157 | { 158 | #ifdef _WIN32 159 | if (handle_) 160 | { 161 | CloseHandle(handle_); 162 | handle_ = NULL; 163 | } 164 | #else 165 | if (handle_ != SEM_FAILED) 166 | { 167 | sem_close(handle_); 168 | handle_ = SEM_FAILED; 169 | } 170 | #endif 171 | } 172 | 173 | bool Event::wait(int timeoutMs) 174 | { 175 | #ifdef _WIN32 176 | return WaitForSingleObject(handle_, timeoutMs) == WAIT_OBJECT_0; 177 | #else 178 | if (timeoutMs == INFINITE) 179 | { 180 | return sem_wait(handle_) == 0; 181 | } 182 | 183 | struct timespec ts; 184 | clock_gettime(CLOCK_REALTIME, &ts); 185 | ts.tv_sec += timeoutMs / 1000; 186 | ts.tv_nsec += (timeoutMs % 1000) * 1000000; 187 | 188 | return sem_timedwait(handle_, &ts) == 0; 189 | #endif 190 | } 191 | 192 | bool Event::isValid() const 193 | { 194 | #ifdef _WIN32 195 | return handle_ != NULL; 196 | #else 197 | return handle_ != SEM_FAILED; 198 | #endif 199 | } 200 | 201 | } // namespace platform 202 | -------------------------------------------------------------------------------- /src/cpp/platform/platform.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifdef _WIN32 4 | #define WIN32_LEAN_AND_MEAN 5 | #include 6 | #include 7 | #else 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #endif 15 | 16 | namespace platform 17 | { 18 | 19 | #ifdef _WIN32 20 | using handle_t = HANDLE; 21 | using sem_handle_t = HANDLE; 22 | #else 23 | using handle_t = int; 24 | using sem_handle_t = sem_t *; 25 | #endif 26 | 27 | class SharedMemory 28 | { 29 | public: 30 | SharedMemory(); 31 | ~SharedMemory(); 32 | #ifdef _WIN32 33 | bool open(const _TCHAR *name); 34 | #else 35 | bool open(const char *name); 36 | #endif 37 | void close(); 38 | void *getData() const; 39 | bool isOpen() const; 40 | 41 | private: 42 | handle_t handle_; 43 | void *data_; 44 | size_t size_; 45 | 46 | #ifdef _WIN32 47 | HANDLE fileMapping_; 48 | #endif 49 | }; 50 | 51 | class Event 52 | { 53 | public: 54 | Event(); 55 | ~Event(); 56 | #ifdef _WIN32 57 | bool create(const _TCHAR *name); 58 | #else 59 | bool create(const char *name); 60 | #endif 61 | void close(); 62 | bool wait(int timeoutMs); 63 | bool isValid() const; 64 | 65 | private: 66 | sem_handle_t handle_; 67 | }; 68 | 69 | } // namespace platform 70 | -------------------------------------------------------------------------------- /src/iracing-sdk-js.js: -------------------------------------------------------------------------------- 1 | const IrSdkNodeWrapper = require('../build/Release/IrSdkNodeBindings.node'); 2 | const JsIrSdk = require('./JsIrSdk'); 3 | 4 | /** 5 | @module irsdk 6 | */ 7 | module.exports = {}; 8 | 9 | let instance; 10 | 11 | /** 12 | Initialize JsIrSdk, can be done once before using getInstance first time. 13 | @function 14 | @static 15 | @param {Object} [opts] Options 16 | @param {Integer} [opts.telemetryUpdateInterval=0] Telemetry update interval, milliseconds 17 | @param {Integer} [opts.sessionInfoUpdateInterval=0] SessionInfo update interval, milliseconds 18 | @param {iracing~sessionInfoParser} [opts.sessionInfoParser] Custom parser for session info 19 | @returns {iracing} Running instance of JsIrSdk 20 | @example 21 | * const irsdk = require('iracing-sdk-js') 22 | * // look for telemetry updates only once per 100 ms 23 | * const iracing = irsdk.init({telemetryUpdateInterval: 100}) 24 | */ 25 | var init = (module.exports.init = function (opts) { 26 | if (!instance) { 27 | instance = new JsIrSdk( 28 | IrSdkNodeWrapper, 29 | Object.assign( 30 | { 31 | telemetryUpdateInterval: 0, 32 | sessionInfoUpdateInterval: 0, 33 | }, 34 | opts 35 | ) 36 | ); 37 | } 38 | return instance; 39 | }); 40 | 41 | /** 42 | Get initialized instance of JsIrSdk 43 | @function 44 | @static 45 | @returns {iracing} Running instance of JsIrSdk 46 | @example 47 | * const irsdk = require('iracing-sdk-js') 48 | * const iracing = irsdk.getInstance() 49 | */ 50 | module.exports.getInstance = function () { 51 | return init(); 52 | }; 53 | -------------------------------------------------------------------------------- /src/utils/createSessionInfoParser.js: -------------------------------------------------------------------------------- 1 | /** Default parser used for SessionInfo YAML 2 | Fixes TeamName issue, uses js-yaml for actual parsing 3 | @private 4 | @param {string} sessionInfoStr raw session info YAML string 5 | @returns {Object} parsed session info or falsy 6 | */ 7 | function createSessionInfoParser() { 8 | const yaml = require("js-yaml"); 9 | 10 | return function (sessionInfoStr) { 11 | const fixedYamlStr = sessionInfoStr.replace( 12 | /TeamName: ([^\n]+)/g, 13 | function (match, p1) { 14 | if ( 15 | (p1[0] === '"' && p1[p1.length - 1] === '"') || 16 | (p1[0] === "'" && p1[p1.length - 1] === "'") 17 | ) { 18 | return match; // skip if quoted already 19 | } else { 20 | // 2nd replace is unnecessary atm but its here just in case 21 | return "TeamName: '" + p1.replace(/'/g, "''") + "'"; 22 | } 23 | } 24 | ); 25 | return yaml.load(fixedYamlStr); 26 | }; 27 | } 28 | 29 | module.exports = createSessionInfoParser; 30 | -------------------------------------------------------------------------------- /src/utils/padCarNum.js: -------------------------------------------------------------------------------- 1 | /** pad car number 2 | @function 3 | @param {string} numStr - car number as string 4 | @returns {number} - padded car number 5 | */ 6 | function padCarNum(numStr) { 7 | if (typeof numStr === "string") { 8 | var num = parseInt(numStr); 9 | var zeros = numStr.length - num.toString().length; 10 | if (!zeros) return num; 11 | 12 | var numPlaces = 1; 13 | if (num > 9) numPlaces = 2; 14 | if (num > 99) numPlaces = 3; 15 | 16 | return (numPlaces + zeros) * 1000 + num; 17 | } 18 | } 19 | 20 | module.exports = padCarNum; 21 | -------------------------------------------------------------------------------- /src/utils/stringToEnum.js: -------------------------------------------------------------------------------- 1 | function stringToEnum(input, enumObj) { 2 | var enumKey = Object.keys(enumObj).find(function (key) { 3 | return key.toLowerCase() === input.toLowerCase(); 4 | }); 5 | if (enumKey) { 6 | return enumObj[enumKey]; 7 | } 8 | } 9 | 10 | module.exports = stringToEnum; 11 | -------------------------------------------------------------------------------- /test/IrsdkNodeWrapper-stub.js: -------------------------------------------------------------------------------- 1 | const mock = { 2 | start() { 3 | return true; 4 | }, 5 | shutdown() {}, 6 | isInitialized() { 7 | return true; 8 | }, 9 | isConnected() { 10 | return false; 11 | }, 12 | updateSessionInfo() { 13 | return false; 14 | }, 15 | getSessionInfo() {}, 16 | updateTelemetry() { 17 | return false; 18 | }, 19 | getTelemetryDescription() { 20 | return { telemetry: 'is telemetry' }; 21 | }, 22 | getTelemetry() { 23 | return {}; 24 | }, 25 | sendCmd() {}, 26 | }; 27 | 28 | module.exports = mock; 29 | -------------------------------------------------------------------------------- /test/JsIrSdk-spec.js: -------------------------------------------------------------------------------- 1 | const JsIrSdk = require('../src/JsIrSdk'); 2 | const IrSdkWrapper = require('./IrsdkNodeWrapper-stub'); 3 | const { describe, it, mock, beforeEach, afterEach } = require('node:test'); 4 | const assert = require('node:assert'); 5 | 6 | describe('JsIrSdk', function () { 7 | it('emits "Connected" when iRacing available', function (context) { 8 | context.mock.timers.enable(); 9 | 10 | const opts = { 11 | telemetryUpdateInterval: 1, 12 | sessionInfoUpdateInterval: 20000, 13 | }; 14 | const irSdkWrapperMockObj = Object.create(IrSdkWrapper); 15 | const start = mock.method(irSdkWrapperMockObj, 'start', () => true); 16 | 17 | irsdk = new JsIrSdk(irSdkWrapperMockObj, opts); 18 | const isConnected = mock.method( 19 | irSdkWrapperMockObj, 20 | 'isConnected', 21 | () => false 22 | ); 23 | 24 | const spy = context.mock.fn(); 25 | 26 | irsdk.on('Connected', spy); 27 | context.mock.timers.tick(2); 28 | assert.strictEqual(spy.mock.callCount(), 0); 29 | 30 | isConnected.mock.mockImplementation(() => true); 31 | 32 | context.mock.timers.tick(2); 33 | 34 | assert.strictEqual(spy.mock.callCount(), 1); 35 | assert.strictEqual(start.mock.callCount(), 1); 36 | }); 37 | 38 | it('emits "Disconnected" when iRacing shut down', function (context) { 39 | context.mock.timers.enable(); 40 | const opts = { 41 | telemetryUpdateInterval: 1, 42 | sessionInfoUpdateInterval: 20000, 43 | }; 44 | const irSdkWrapperMockObj = Object.create(IrSdkWrapper); 45 | const irsdk = new JsIrSdk(irSdkWrapperMockObj, opts); 46 | const isConnected = mock.method( 47 | irSdkWrapperMockObj, 48 | 'isConnected', 49 | () => true 50 | ); 51 | const spy = context.mock.fn(); 52 | irsdk.on('Disconnected', spy); 53 | context.mock.timers.tick(2); 54 | assert.strictEqual(spy.mock.callCount(), 0); 55 | isConnected.mock.mockImplementation(() => false); 56 | context.mock.timers.tick(2); 57 | assert.strictEqual(spy.mock.callCount(), 1); 58 | }); 59 | 60 | it('emits "Connected" again after reconnect', function (context) { 61 | context.mock.timers.enable(); 62 | const opts = { 63 | telemetryUpdateInterval: 2000, 64 | sessionInfoUpdateInterval: 20000, 65 | }; 66 | 67 | const irSdkWrapperMockObj = Object.create(IrSdkWrapper); 68 | const start = mock.method(irSdkWrapperMockObj, 'start', () => true); 69 | const isConnected = mock.method( 70 | irSdkWrapperMockObj, 71 | 'isConnected', 72 | () => true 73 | ); 74 | 75 | const irsdk = new JsIrSdk(irSdkWrapperMockObj, opts); 76 | 77 | assert.strictEqual(start.mock.callCount(), 1); 78 | 79 | context.mock.timers.tick(2500); 80 | isConnected.mock.mockImplementation(() => false); 81 | const isInitialized = mock.method( 82 | irSdkWrapperMockObj, 83 | 'isInitialized', 84 | () => false 85 | ); 86 | context.mock.timers.tick(11000); 87 | 88 | assert.strictEqual(start.mock.callCount(), 2); 89 | 90 | isConnected.mock.mockImplementation(() => true); 91 | isInitialized.mock.mockImplementation(() => true); 92 | 93 | const spy = context.mock.fn(); 94 | irsdk.on('Connected', spy); 95 | context.mock.timers.tick(2500); 96 | 97 | assert.strictEqual(spy.mock.callCount(), 1); 98 | }); 99 | 100 | it('emits "TelemetryDescription" once after "Connected"', async function (context) { 101 | context.mock.timers.enable(); 102 | 103 | const opts = { 104 | telemetryUpdateInterval: 1, 105 | sessionInfoUpdateInterval: 20000, 106 | }; 107 | const irSdkWrapperMockObj = Object.create(IrSdkWrapper); 108 | mock.method(irSdkWrapperMockObj, 'updateTelemetry', () => true); 109 | mock.method(irSdkWrapperMockObj, 'getTelemetryDescription', () => [ 110 | { RPM: 'engine revs per minute' }, 111 | ]); 112 | mock.method(irSdkWrapperMockObj, 'isConnected', () => true); 113 | const irsdk = new JsIrSdk(irSdkWrapperMockObj, opts); 114 | 115 | const spy = context.mock.fn(); 116 | irsdk.on('TelemetryDescription', spy); 117 | 118 | context.mock.timers.tick(5); 119 | assert.strictEqual(spy.mock.callCount(), 1); 120 | assert.strictEqual( 121 | spy.mock.calls[0].arguments[0][0].RPM, 122 | 'engine revs per minute' 123 | ); 124 | context.mock.timers.tick(5); 125 | assert.strictEqual(spy.mock.callCount(), 1); 126 | }); 127 | 128 | it('emits "Telemetry" when update available', function (context) { 129 | context.mock.timers.enable(); 130 | 131 | const opts = { 132 | telemetryUpdateInterval: 10, 133 | sessionInfoUpdateInterval: 20000, 134 | }; 135 | const irSdkWrapperMockObj = Object.create(IrSdkWrapper); 136 | mock.method(irSdkWrapperMockObj, 'updateTelemetry', () => true); 137 | mock.method(irSdkWrapperMockObj, 'getTelemetry', () => ({ 138 | values: { RPM: 1100 }, 139 | })); 140 | mock.method(irSdkWrapperMockObj, 'isConnected', () => true); 141 | const irsdk = new JsIrSdk(irSdkWrapperMockObj, opts); 142 | 143 | const spy = context.mock.fn(); 144 | irsdk.on('Telemetry', spy); 145 | 146 | context.mock.timers.tick(12); 147 | assert.strictEqual(spy.mock.callCount(), 1); 148 | assert.strictEqual(spy.mock.calls[0].arguments[0].values.RPM, 1100); 149 | 150 | mock.method(irSdkWrapperMockObj, 'updateTelemetry', () => false); 151 | context.mock.timers.tick(12); 152 | 153 | assert.strictEqual(spy.mock.callCount(), 1); 154 | 155 | mock.method(irSdkWrapperMockObj, 'updateTelemetry', () => true); 156 | context.mock.timers.tick(12); 157 | 158 | assert.strictEqual(spy.mock.callCount(), 2); 159 | }); 160 | 161 | it('emits "SessionInfo" when update available', function (context) { 162 | context.mock.timers.enable(); 163 | const opts = { telemetryUpdateInterval: 10, sessionInfoUpdateInterval: 10 }; 164 | const irSdkWrapperMockObj = Object.create(IrSdkWrapper); 165 | mock.method(irSdkWrapperMockObj, 'updateSessionInfo', () => true); 166 | mock.method(irSdkWrapperMockObj, 'getSessionInfo', () => { 167 | return '---\ntype: race\n'; 168 | }); 169 | mock.method(irSdkWrapperMockObj, 'isConnected', () => true); 170 | const irsdk = new JsIrSdk(irSdkWrapperMockObj, opts); 171 | const spy = context.mock.fn(); 172 | irsdk.on('SessionInfo', spy); 173 | context.mock.timers.tick(12); 174 | assert.strictEqual(spy.mock.callCount(), 1); 175 | assert.strictEqual(spy.mock.calls[0].arguments[0].data.type, 'race'); 176 | mock.method(irSdkWrapperMockObj, 'updateSessionInfo', () => false); 177 | context.mock.timers.tick(12); 178 | assert.strictEqual(spy.mock.callCount(), 1); 179 | mock.method(irSdkWrapperMockObj, 'updateSessionInfo', () => true); 180 | context.mock.timers.tick(12); 181 | assert.strictEqual(spy.mock.callCount(), 2); 182 | }); 183 | 184 | describe('All commands', function () { 185 | let sendCmd, irsdk; 186 | beforeEach(function () { 187 | const irsdkWrapperMockObj = Object.create(IrSdkWrapper); 188 | sendCmd = mock.method(irsdkWrapperMockObj, 'sendCmd', () => true); 189 | irsdk = new JsIrSdk(irsdkWrapperMockObj); 190 | }); 191 | afterEach(function () { 192 | irsdk._stop(); 193 | }); 194 | 195 | describe('.execCmd(cmd, [arg1, arg2, arg3])', function () { 196 | it('sends arbitrary broadcast message', function () { 197 | irsdk.execCmd(12, 13, 14, 15); 198 | assert.strictEqual(sendCmd.mock.callCount(), 1); 199 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 12); 200 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 13); 201 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 14); 202 | assert.strictEqual(sendCmd.mock.calls[0].arguments[3], 15); 203 | }); 204 | }); 205 | 206 | describe('.reloadTextures()', function () { 207 | it('sends reload all command', function () { 208 | irsdk.reloadTextures(); 209 | assert.strictEqual(sendCmd.mock.callCount(), 1); 210 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 7); 211 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 0); 212 | }); 213 | }); 214 | 215 | describe('.reloadTextures(carIdx)', function () { 216 | it('sends reload car command', function () { 217 | irsdk.reloadTexture(13); 218 | assert.strictEqual(sendCmd.mock.callCount(), 1); 219 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 7); 220 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 1); 221 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 13); 222 | }); 223 | }); 224 | 225 | describe('.execChatCmd(cmd, [arg])', function () { 226 | it('sends chat command when cmd given as integer', function () { 227 | irsdk.execChatCmd(2); 228 | assert.strictEqual(sendCmd.mock.callCount(), 1); 229 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 8); 230 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 2); 231 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 0); 232 | }); 233 | it('sends chat command when cmd given as string', function () { 234 | irsdk.execChatCmd('cancel'); 235 | assert.strictEqual(sendCmd.mock.callCount(), 1); 236 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 8); 237 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 3); 238 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 0); 239 | }); 240 | }); 241 | 242 | describe('.execChatMacro(num)', function () { 243 | it('sends chat macro command', function () { 244 | irsdk.execChatMacro(7); 245 | assert.strictEqual(sendCmd.mock.callCount(), 1); 246 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 8); 247 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 0); 248 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 7); 249 | }); 250 | }); 251 | 252 | describe('.execPitCmd(cmd, [arg])', function () { 253 | it('sends command when cmd given as integer', function () { 254 | irsdk.execPitCmd(1); 255 | assert.strictEqual(sendCmd.mock.callCount(), 1); 256 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 9); 257 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 1); 258 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 0); 259 | }); 260 | it('sends command when cmd given as string', function () { 261 | irsdk.execPitCmd('clearTires'); 262 | assert.strictEqual(sendCmd.mock.callCount(), 1); 263 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 9); 264 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 7); 265 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 0); 266 | }); 267 | it('passes thru integer argument', function () { 268 | irsdk.execPitCmd('fuel', 60); 269 | assert.strictEqual(sendCmd.mock.callCount(), 1); 270 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 9); 271 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 2); 272 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 60); 273 | }); 274 | }); 275 | 276 | describe('.execTelemetryCmd(cmd)', function () { 277 | it('sends command when cmd given as integer', function () { 278 | irsdk.execTelemetryCmd(1); 279 | assert.strictEqual(sendCmd.mock.callCount(), 1); 280 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 10); 281 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 1); 282 | }); 283 | it('sends command when cmd given as string', function () { 284 | irsdk.execTelemetryCmd('restart'); 285 | assert.strictEqual(sendCmd.mock.callCount(), 1); 286 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 10); 287 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 2); 288 | }); 289 | }); 290 | 291 | describe('.camControls', function () { 292 | describe('.setState(state)', function () { 293 | it('sends state cmd', function () { 294 | irsdk.camControls.setState(15); 295 | assert.strictEqual(sendCmd.mock.callCount(), 1); 296 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 2); 297 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 15); 298 | }); 299 | }); 300 | describe('.switchToCar(carNum, [camGroupNum], [camNum])', function () { 301 | it('sends switch cmd', function () { 302 | irsdk.camControls.switchToCar(12); 303 | assert.strictEqual(sendCmd.mock.callCount(), 1); 304 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 1); 305 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 12); 306 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 0); 307 | assert.strictEqual(sendCmd.mock.calls[0].arguments[3], 0); 308 | }); 309 | describe('leading zeros are padded if car num is given as string', function () { 310 | it('"1" -> 1', function () { 311 | irsdk.camControls.switchToCar('1'); 312 | assert.strictEqual(sendCmd.mock.callCount(), 1); 313 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 1); 314 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 1); 315 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 0); 316 | assert.strictEqual(sendCmd.mock.calls[0].arguments[3], 0); 317 | }); 318 | it('"100" -> 100', function () { 319 | irsdk.camControls.switchToCar('100'); 320 | assert.strictEqual(sendCmd.mock.callCount(), 1); 321 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 1); 322 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 100); 323 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 0); 324 | assert.strictEqual(sendCmd.mock.calls[0].arguments[3], 0); 325 | }); 326 | it('"110" -> 110', function () { 327 | irsdk.camControls.switchToCar('100'); 328 | assert.strictEqual(sendCmd.mock.callCount(), 1); 329 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 1); 330 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 100); 331 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 0); 332 | assert.strictEqual(sendCmd.mock.calls[0].arguments[3], 0); 333 | }); 334 | it('"01" -> 2001', function () { 335 | irsdk.camControls.switchToCar('01'); 336 | assert.strictEqual(sendCmd.mock.callCount(), 1); 337 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 1); 338 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 2001); 339 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 0); 340 | assert.strictEqual(sendCmd.mock.calls[0].arguments[3], 0); 341 | }); 342 | it('"001" -> 3001', function () { 343 | irsdk.camControls.switchToCar('001'); 344 | assert.strictEqual(sendCmd.mock.callCount(), 1); 345 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 1); 346 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 3001); 347 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 0); 348 | assert.strictEqual(sendCmd.mock.calls[0].arguments[3], 0); 349 | }); 350 | it('"011" -> 3011', function () { 351 | irsdk.camControls.switchToCar('011'); 352 | assert.strictEqual(sendCmd.mock.callCount(), 1); 353 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 1); 354 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 3011); 355 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 0); 356 | assert.strictEqual(sendCmd.mock.calls[0].arguments[3], 0); 357 | }); 358 | }); 359 | it('sends focus at cmd', function () { 360 | irsdk.camControls.switchToCar(-2); 361 | assert.strictEqual(sendCmd.mock.callCount(), 1); 362 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 1); 363 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], -2); 364 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 0); 365 | assert.strictEqual(sendCmd.mock.calls[0].arguments[3], 0); 366 | }); 367 | it('switches cam group and cam', function () { 368 | irsdk.camControls.switchToCar(12, 2, 3); 369 | assert.strictEqual(sendCmd.mock.callCount(), 1); 370 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 1); 371 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 12); 372 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 2); 373 | assert.strictEqual(sendCmd.mock.calls[0].arguments[3], 3); 374 | }); 375 | }); 376 | describe('.switchToPos(carNum, [camGroupNum], [camNum])', function () { 377 | it('sends switch cmd', function () { 378 | irsdk.camControls.switchToPos(12); 379 | assert.strictEqual(sendCmd.mock.callCount(), 1); 380 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 0); 381 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 12); 382 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 0); 383 | assert.strictEqual(sendCmd.mock.calls[0].arguments[3], 0); 384 | }); 385 | it('sends focus at cmd"', function () { 386 | irsdk.camControls.switchToPos(-2); 387 | assert.strictEqual(sendCmd.mock.callCount(), 1); 388 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 0); 389 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], -2); 390 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 0); 391 | assert.strictEqual(sendCmd.mock.calls[0].arguments[3], 0); 392 | }); 393 | it('switches cam group and cam', function () { 394 | irsdk.camControls.switchToPos(12, 2, 3); 395 | assert.strictEqual(sendCmd.mock.callCount(), 1); 396 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 0); 397 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 12); 398 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 2); 399 | assert.strictEqual(sendCmd.mock.calls[0].arguments[3], 3); 400 | }); 401 | }); 402 | 403 | describe('.playbackControls', function () { 404 | describe('.play()', function () { 405 | it('sends cmd', function () { 406 | irsdk.playbackControls.play(); 407 | assert.strictEqual(sendCmd.mock.callCount(), 1); 408 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 3); 409 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 1); 410 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 0); 411 | }); 412 | }); 413 | describe('.pause()', function () { 414 | it('sends cmd', function () { 415 | irsdk.playbackControls.pause(); 416 | assert.strictEqual(sendCmd.mock.callCount(), 1); 417 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 3); 418 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 0); 419 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 0); 420 | }); 421 | }); 422 | describe('.fastForward([speed])', function () { 423 | it('sends cmd', function () { 424 | irsdk.playbackControls.fastForward(); 425 | assert.strictEqual(sendCmd.mock.callCount(), 1); 426 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 3); 427 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 2); 428 | }); 429 | it('passes optional argument', function () { 430 | irsdk.playbackControls.fastForward(16); 431 | assert.strictEqual(sendCmd.mock.callCount(), 1); 432 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 3); 433 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 16); 434 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 0); 435 | }); 436 | }); 437 | describe('.rewind([speed])', function () { 438 | it('sends cmd', function () { 439 | irsdk.playbackControls.rewind(); 440 | assert.strictEqual(sendCmd.mock.callCount(), 1); 441 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 3); 442 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], -2); 443 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 0); 444 | }); 445 | it('passes optional argument', function () { 446 | irsdk.playbackControls.rewind(16); 447 | assert.strictEqual(sendCmd.mock.callCount(), 1); 448 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 3); 449 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], -16); 450 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 0); 451 | }); 452 | }); 453 | describe('.slowForward([divider])', function () { 454 | it('sends cmd', function () { 455 | irsdk.playbackControls.slowForward(); 456 | assert.strictEqual(sendCmd.mock.callCount(), 1); 457 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 3); 458 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 1); 459 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 1); 460 | }); 461 | it('passes optional argument', function () { 462 | irsdk.playbackControls.slowForward(16); 463 | assert.strictEqual(sendCmd.mock.callCount(), 1); 464 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 3); 465 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 15); 466 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 1); 467 | }); 468 | }); 469 | describe('.slowBackward([divider])', function () { 470 | it('sends cmd', function () { 471 | irsdk.playbackControls.slowBackward(); 472 | assert.strictEqual(sendCmd.mock.callCount(), 1); 473 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 3); 474 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], -1); 475 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 1); 476 | }); 477 | it('passes optional argument', function () { 478 | irsdk.playbackControls.slowBackward(16); 479 | assert.strictEqual(sendCmd.mock.callCount(), 1); 480 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 3); 481 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], -15); 482 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 1); 483 | }); 484 | }); 485 | 486 | describe('.searchTs(sessionNum, sessionTimeMS)', function () { 487 | it('sends cmd with args', function () { 488 | irsdk.playbackControls.searchTs(1, 5000); 489 | assert.strictEqual(sendCmd.mock.callCount(), 1); 490 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 12); 491 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 1); 492 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 5000); 493 | }); 494 | }); 495 | describe('.searchFrame(frameNum, rpyPosMode)', function () { 496 | it('sends cmd with args', function () { 497 | irsdk.playbackControls.searchFrame(5, 1); 498 | assert.strictEqual(sendCmd.mock.callCount(), 1); 499 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 4); 500 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 1); 501 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 5); 502 | }); 503 | it('rpyPosMode can be given as string', function () { 504 | irsdk.playbackControls.searchFrame(17, 'end'); 505 | assert.strictEqual(sendCmd.mock.callCount(), 1); 506 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 4); 507 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 2); 508 | assert.strictEqual(sendCmd.mock.calls[0].arguments[2], 17); 509 | }); 510 | }); 511 | describe('.search(searchMode)', function () { 512 | it('sends cmd with args', function () { 513 | irsdk.playbackControls.search(6); 514 | assert.strictEqual(sendCmd.mock.callCount(), 1); 515 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 5); 516 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 6); 517 | }); 518 | it('searchMode can be given as string', function () { 519 | irsdk.playbackControls.search('prevIncident'); 520 | assert.strictEqual(sendCmd.mock.callCount(), 1); 521 | assert.strictEqual(sendCmd.mock.calls[0].arguments[0], 5); 522 | assert.strictEqual(sendCmd.mock.calls[0].arguments[1], 8); 523 | }); 524 | }); 525 | }); 526 | }); 527 | }); 528 | }); 529 | -------------------------------------------------------------------------------- /test/iracing-sdk-js-spec.js: -------------------------------------------------------------------------------- 1 | const sandboxed = require('sandboxed-module'); 2 | const { describe, it } = require('node:test'); 3 | const assert = require('node:assert'); 4 | 5 | describe('iracing-sdk-js', function () { 6 | describe('#init', function () { 7 | it('instantiates JsIrSdk once', function (context) { 8 | const jsIrSdkSpy = context.mock.fn(); 9 | const nodeWrapperMock = {}; 10 | const opts = { 11 | telemetryUpdateInterval: 1, 12 | sessionInfoUpdateInterval: 2, 13 | }; 14 | const nodeIrSdk = sandboxed.require('../src/iracing-sdk-js', { 15 | requires: { 16 | '../build/Release/IrSdkNodeBindings.node': nodeWrapperMock, 17 | './JsIrSdk': jsIrSdkSpy, 18 | }, 19 | }); 20 | nodeIrSdk.init(opts); 21 | nodeIrSdk.init(opts); 22 | assert.strictEqual(jsIrSdkSpy.mock.callCount(), 1); 23 | assert.strictEqual( 24 | jsIrSdkSpy.mock.calls[0].arguments[0], 25 | nodeWrapperMock 26 | ); 27 | assert.strictEqual( 28 | jsIrSdkSpy.mock.calls[0].arguments[1].telemetryUpdateInterval, 29 | opts.telemetryUpdateInterval 30 | ); 31 | assert.strictEqual( 32 | jsIrSdkSpy.mock.calls[0].arguments[1].sessionInfoUpdateInterval, 33 | opts.sessionInfoUpdateInterval 34 | ); 35 | }); 36 | }); 37 | describe('#getInstance', function () { 38 | it('gives JsIrSdk singleton', function () { 39 | const nodeIrSdk = require('../src/iracing-sdk-js'); 40 | const instance1 = nodeIrSdk.getInstance(); 41 | const instance2 = nodeIrSdk.getInstance(); 42 | assert.strictEqual(instance1, instance2); 43 | instance2._stop(); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/smoke-test.js: -------------------------------------------------------------------------------- 1 | const assert = require('node:assert'); 2 | const irsdk = require('../'); 3 | 4 | irsdk.init({ 5 | telemetryUpdateInterval: 0, 6 | sessionInfoUpdateInterval: 1000, 7 | }); 8 | 9 | const iracing = irsdk.getInstance(); 10 | 11 | // kill the process when enough is done.. 12 | const done = (function () { 13 | var tasks = []; 14 | var totalTasks = 3; 15 | 16 | return function (taskName) { 17 | tasks.push(taskName); 18 | if (tasks.length >= totalTasks) { 19 | console.log(); 20 | console.log('checks done', new Date()); 21 | process.exit(); 22 | } 23 | }; 24 | })(); 25 | 26 | function validateValue(val, desc) { 27 | if (desc.type !== 'bitField') { 28 | if (desc.unit.substr(0, 5) === 'irsdk') { 29 | assert.strictEqual( 30 | typeof val, 31 | 'string', 32 | `enums should be converted to strings ${JSON.stringify(desc)}` 33 | ); 34 | } else { 35 | if (desc.type === 'bool') { 36 | assert.strictEqual(typeof val, 'boolean'); 37 | } 38 | if (desc.type === 'int') { 39 | assert.strictEqual(typeof val, 'number'); 40 | } 41 | if (desc.type === 'float') { 42 | assert.strictEqual(typeof val, 'number'); 43 | } 44 | if (desc.type === 'double') { 45 | assert.strictEqual(typeof val, 'number'); 46 | } 47 | if (desc.type === 'char') { 48 | assert.strictEqual(typeof val, 'string'); 49 | assert.strictEqual(val.length, 1); 50 | } 51 | } 52 | } else { 53 | // expect bitField to be converted to array 54 | assert.strictEqual( 55 | Array.isArray(val), 56 | true, 57 | 'bitField should be converted to array' 58 | ); 59 | 60 | val.forEach(function (bitFieldVal) { 61 | assert.strictEqual(typeof bitFieldVal, 'string'); 62 | }); 63 | } 64 | } 65 | 66 | // tests that C++ module is working 67 | // correctly when doing unit & type conversions 68 | function checkTelemetryValues(telemetry, desc) { 69 | if (!desc || !telemetry) { 70 | return; 71 | } 72 | 73 | console.log('got telemetry and its description, validating output..'); 74 | 75 | for (const telemetryVarName in desc) { 76 | if (desc.hasOwnProperty(telemetryVarName)) { 77 | console.log('checking ' + telemetryVarName); 78 | const varDesc = desc[telemetryVarName]; 79 | const value = telemetry.values[telemetryVarName]; 80 | 81 | assert.strictEqual(typeof varDesc, 'object'); 82 | assert.notEqual(value, undefined); 83 | 84 | if (varDesc.count > 1) { 85 | assert.strictEqual(Array.isArray(value), true); 86 | 87 | value.forEach(function (val) { 88 | validateValue(val, varDesc); 89 | }); 90 | } else { 91 | validateValue(value, varDesc); 92 | } 93 | } 94 | } 95 | } 96 | 97 | console.log(); 98 | console.log('waiting for iRacing...'); 99 | 100 | iracing.once('Connected', function () { 101 | console.log(); 102 | console.log('Connected to iRacing.'); 103 | 104 | let telemetry, desc; 105 | 106 | iracing.once('TelemetryDescription', function (data) { 107 | console.log('TelemetryDescription event received'); 108 | assert.strictEqual(typeof data, 'object'); 109 | desc = data; 110 | checkTelemetryValues(telemetry, desc); 111 | done('desc'); 112 | }); 113 | 114 | iracing.once('Telemetry', function (data) { 115 | console.log('Telemetry event received'); 116 | assert.strictEqual(typeof data, 'object'); 117 | assert.strictEqual(typeof data.timestamp, 'object'); 118 | assert.strictEqual(typeof data.values, 'object'); 119 | 120 | telemetry = data; 121 | checkTelemetryValues(telemetry, desc); 122 | done('telemetry'); 123 | }); 124 | 125 | iracing.once('SessionInfo', function (data) { 126 | console.log('SessionInfo event received'); 127 | assert.strictEqual(typeof data, 'object'); 128 | assert.strictEqual(typeof data.timestamp, 'object'); 129 | assert.strictEqual(typeof data.data, 'object'); 130 | done('sessioninfo'); 131 | }); 132 | }); 133 | 134 | setTimeout(function () { 135 | console.log(`Test hasn't finished in 3 seconds, exiting..`); 136 | process.exit(0); 137 | }, 3000); 138 | -------------------------------------------------------------------------------- /test/utils/stringToEnum-spec.js: -------------------------------------------------------------------------------- 1 | const { describe, it } = require('node:test'); 2 | const assert = require('node:assert'); 3 | const stringToEnum = require('../../src/utils/stringToEnum'); 4 | 5 | describe('stringToEnum', function () { 6 | const TEST_ENUM = { 7 | FIRST_VALUE: 1, 8 | SECOND_VALUE: 2, 9 | THIRD_VALUE: 3, 10 | }; 11 | 12 | it('converts matching string to enum value', function () { 13 | assert.strictEqual(stringToEnum('FIRST_VALUE', TEST_ENUM), 1); 14 | assert.strictEqual(stringToEnum('SECOND_VALUE', TEST_ENUM), 2); 15 | assert.strictEqual(stringToEnum('THIRD_VALUE', TEST_ENUM), 3); 16 | }); 17 | 18 | it('is case insensitive', function () { 19 | assert.strictEqual(stringToEnum('first_value', TEST_ENUM), 1); 20 | assert.strictEqual(stringToEnum('First_Value', TEST_ENUM), 1); 21 | assert.strictEqual(stringToEnum('FIRST_value', TEST_ENUM), 1); 22 | }); 23 | 24 | it('returns undefined for non-matching strings', function () { 25 | assert.strictEqual(stringToEnum('NON_EXISTENT', TEST_ENUM), undefined); 26 | assert.strictEqual(stringToEnum('', TEST_ENUM), undefined); 27 | }); 28 | 29 | it('handles special characters correctly', function () { 30 | const SPECIAL_ENUM = { 31 | 'SPECIAL-VALUE': 1, 32 | 'ANOTHER.VALUE': 2, 33 | }; 34 | assert.strictEqual(stringToEnum('SPECIAL-VALUE', SPECIAL_ENUM), 1); 35 | assert.strictEqual(stringToEnum('special-value', SPECIAL_ENUM), 1); 36 | assert.strictEqual(stringToEnum('ANOTHER.VALUE', SPECIAL_ENUM), 2); 37 | }); 38 | }); 39 | --------------------------------------------------------------------------------