├── .gitignore ├── INSTALL.txt ├── README.txt ├── Resources.txt ├── VERSION.txt ├── __init__.py ├── api.py ├── benchmark.py ├── runtests.py ├── setup.py └── tests ├── api.txt ├── memorydump.dmp └── mmap.txt /.gitignore: -------------------------------------------------------------------------------- 1 | example code/* 2 | *.pyc 3 | build/* 4 | -------------------------------------------------------------------------------- /INSTALL.txt: -------------------------------------------------------------------------------- 1 | python setup.py install -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | iRacing Python API client 2 | ========================= 3 | 4 | Will build this as I learn how it works as there's no worked Python example. 5 | 6 | Description 7 | ----------- 8 | 9 | Uses memory-mapped files, has slow-updating YAML data and fast (60Hz) updating 10 | telemetry data. 11 | 12 | API 13 | --- 14 | 15 | This file api.py provides read-only access to the iRacing memory mapped file 16 | session and telemetry API. 17 | 18 | To get all meta, the api.py has an API of it's own. It's a very simple dict 19 | interface: 20 | 21 | api.API()[KEY] 22 | 23 | And there's a dict-like .keys() helper: 24 | 25 | api.API().keys() 26 | 27 | I'll do my best to support this as a minimum, going forward, but I'm hoping to 28 | add more clevers as well of course. 29 | 30 | Tests 31 | ----- 32 | 33 | Run using: 34 | python runtests.py 35 | 36 | Benchmarking 37 | ------------ 38 | 39 | To check telemetry read performance on your machine, run: 40 | python benchmark.py 41 | 42 | Requires 43 | -------- 44 | 45 | Python 2.7, PyYAML -------------------------------------------------------------------------------- /Resources.txt: -------------------------------------------------------------------------------- 1 | As they are few and damn far between... 2 | 3 | http://members.iracing.com/jforum/posts/list/1470675.page 4 | https://github.com/meltingice/node-iracing 5 | -------------------------------------------------------------------------------- /VERSION.txt: -------------------------------------------------------------------------------- 1 | Version info 2 | ------------ 3 | 4 | Numbers match tags in https://github.com/thisismyrobot/python-iracing-api 5 | 6 | Versions 7 | -------- 8 | 9 | 1.2 Added setuptools install script 10 | 11 | 1.11 Bug fix to demo usage. 12 | 13 | 1.1 Moved to dict-based telemetry access due to problems with nested YAML 14 | dicts. 15 | 16 | 1.0 Initial Version with basic session and telemetry accessors. -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thisismyrobot/python-iracing-api/faede3285983108058e1d3bdbc60dbc7c414653f/__init__.py -------------------------------------------------------------------------------- /api.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import mmap 3 | import os 4 | import struct 5 | import yaml # Requires PyYAML 6 | 7 | 8 | MEMMAPFILE = 'Local\\IRSDKMemMapFileName' 9 | MEMMAPFILESIZE = 798720 # Hopefully this is fairly static... 10 | MEMMAPFILE_START = '\x01' # Used to detect memory mapped file exists 11 | 12 | HEADER_LEN = 144 13 | 14 | # How far into a header the name sits, and its max length 15 | TELEM_NAME_OFFSET = 16 16 | TELEM_NAME_MAX_LEN = 32 17 | 18 | # There appears to be triple-buffering of data 19 | VAL_BUFFERS = 3 20 | 21 | # The mapping between the type integer in memory mapped file and Python's struct 22 | TYPEMAP = ['c', '?', 'i', 'I', 'f', 'd'] 23 | 24 | 25 | class API(object): 26 | """ A basic read-only iRacing Session and Telemetry API client. 27 | """ 28 | def __init__(self, mmap_object = None): 29 | """ Sets up a lot of internal variables, they are populated when first 30 | accessed by their non underscore-prepended versions. This makes the 31 | first access to a method like telemetry() slower, but massively 32 | tidies up the codebase. 33 | """ 34 | self.__var_types = None 35 | self.__buffer_offsets = None 36 | self.__sizes = None 37 | self.__mmp = mmap_object 38 | self.__var_offsets = None 39 | self.__telemetry_names = None 40 | self.__yaml_names = None 41 | 42 | if not self._iracing_alive(): 43 | raise Exception("iRacing memory mapped file could not be found") 44 | 45 | def __getitem__(self, key): 46 | """ Helper to allow for API()['Speed'] to work. 47 | """ 48 | if key in self._telemetry_names: 49 | return self.telemetry(key) 50 | else: 51 | return self._yaml_dict[key] 52 | 53 | def _iracing_alive(self): 54 | """ Returns true if iRacing is running, determined by whether we have a 55 | memory mapped file or not. 56 | """ 57 | try: 58 | self._mmp.seek(0) 59 | return self._mmp.read(1) == MEMMAPFILE_START 60 | except: 61 | return False 62 | 63 | @property 64 | def _telemetry_header_start(self): 65 | """ Returns the index of the telemetry header, searching from the end of 66 | the yaml. 67 | """ 68 | self._mmp.seek(self._yaml_end) 69 | dat = '\x00' 70 | while dat.strip() == '\x00': 71 | dat = self._mmp.read(1) 72 | return self._mmp.tell() - 1 73 | 74 | @property 75 | def _yaml_end(self): 76 | """ Returns the index of the end of the YAML in memory. 77 | """ 78 | self._mmp.seek(0) 79 | offset = 0 80 | headers = self._mmp.readline() 81 | while True: 82 | line = self._mmp.readline() 83 | if line.strip() == '...': 84 | break 85 | else: 86 | offset += len(line) 87 | return offset + len(headers) + 4 88 | 89 | @property 90 | def _mmp(self): 91 | """ Create the memory map. 92 | """ 93 | if self.__mmp is None: 94 | self.__mmp = mmap.mmap(-1, MEMMAPFILESIZE, MEMMAPFILE, 95 | access=mmap.ACCESS_READ) 96 | return self.__mmp 97 | 98 | @property 99 | def _sizes(self): 100 | """ Find the size for each variable, cache the results. 101 | """ 102 | if self.__sizes is None: 103 | self.__sizes = {} 104 | for key, var_type in self._var_types.items(): 105 | self.__sizes[key] = struct.calcsize(var_type) 106 | return self.__sizes 107 | 108 | @property 109 | def _buffer_offsets(self): 110 | """ Find the offsets for the value array(s), cache the result. 111 | """ 112 | if self.__buffer_offsets is None: 113 | self.__buffer_offsets = [self._get(52 + (i * 16), 'i') 114 | for i 115 | in range(VAL_BUFFERS)] 116 | return self.__buffer_offsets 117 | 118 | @property 119 | def _telemetry_names(self): 120 | """ The names of the telemetry variables, in order in memory, cached. 121 | TODO: Make less clunky... 122 | """ 123 | if self.__telemetry_names is None: 124 | self.__telemetry_names = [] 125 | self._mmp.seek(self._telemetry_header_start) 126 | while True: 127 | pos = self._mmp.tell() + TELEM_NAME_OFFSET 128 | start = TELEM_NAME_OFFSET 129 | end = TELEM_NAME_OFFSET + TELEM_NAME_MAX_LEN 130 | header = self._mmp.read(HEADER_LEN) 131 | name = header[start:end].replace('\x00','') 132 | if name == '': 133 | break 134 | self.__telemetry_names.append(name) 135 | return self.__telemetry_names 136 | 137 | @property 138 | def _var_types(self): 139 | """ Set up the type map based on the headers, cache the results. 140 | """ 141 | if self.__var_types is None: 142 | self.__var_types = {} 143 | for i, name in enumerate(self._telemetry_names): 144 | type_loc = self._telemetry_header_start + (i * HEADER_LEN) 145 | self.__var_types[name] = TYPEMAP[int(self._get(type_loc, 'i'))] 146 | return self.__var_types 147 | 148 | @property 149 | def _var_offsets(self): 150 | """ Find the offsets between the variables - used to find values in real 151 | time. Results are cached. 152 | """ 153 | if self.__var_offsets is None: 154 | self.__var_offsets = {} 155 | offsets_seek = self._get(28, 'i') 156 | for i, name in enumerate(self._telemetry_names): 157 | offset = self._get(offsets_seek + (i * HEADER_LEN) + 4, 'i') 158 | self.__var_offsets[name] = offset 159 | return self.__var_offsets 160 | 161 | @property 162 | def _yaml_dict(self): 163 | """ Returns the session yaml as a nested dict. 164 | """ 165 | ymltxt = '' 166 | self._mmp.seek(0) 167 | headers = self._mmp.readline() 168 | return yaml.load(self._mmp[self._mmp.tell():self._yaml_end], 169 | Loader=yaml.CLoader) 170 | 171 | def _get(self, position, type): 172 | """ Gets a value from the mmp, based on a position and struct var type. 173 | """ 174 | size = struct.calcsize(type) 175 | val = struct.unpack(type, self._mmp[position:position + size])[0] 176 | if val is None: 177 | val = 0 178 | return val 179 | 180 | def keys(self): 181 | """ Helper to allow this to be semi-dict-like by allowing .keys() calls. 182 | """ 183 | return sorted(self._yaml_dict.keys() + self._telemetry_names) 184 | 185 | def telemetry(self, key): 186 | """ Return the data for a telemetry key. There are three buffers and 187 | this returns the first one with a valid value. 188 | 189 | TODO: Use the "tick" indicator to show which one to use instead of 190 | this brute-force method. 191 | """ 192 | val_o = self._var_offsets[key] 193 | for buf_o in self._buffer_offsets: 194 | data = self._mmp[val_o + buf_o: val_o + buf_o + self._sizes[key]] 195 | if len(data.replace('\x00','')) != 0: 196 | return struct.unpack(self._var_types[key], data)[0] 197 | 198 | if __name__ == '__main__': 199 | """ Simple test usage. 200 | """ 201 | client = API() 202 | for key in client.keys(): 203 | print key, client[key] 204 | -------------------------------------------------------------------------------- /benchmark.py: -------------------------------------------------------------------------------- 1 | """ Benchmarker. 2 | 3 | Current results on my (mid performance) machine: 4 | 5 | Press any key to begin telemetry access benchmark... 6 | Setting up... 7 | Attempting to read current all telemetry keys 10000 times each... Done! 8 | 10000 reads of all keys executed in 6.1890001297 seconds 9 | Read rate for all 91 telementry values is: 1615.7698805 Hz 10 | Read rate for a single telementry value is: 147035.059126 Hz 11 | 12 | """ 13 | 14 | import api 15 | import time 16 | 17 | 18 | ITER = 10000 19 | 20 | print "Press any key to begin telemetry access benchmark...", 21 | raw_input() 22 | 23 | print "Setting up..." 24 | 25 | client = api.API() 26 | client['Speed'] 27 | keys = client._telemetry_names 28 | 29 | print "Attempting to read current all telemetry keys {0} times each...".format(ITER), 30 | 31 | start = time.time() 32 | 33 | for i in range(ITER): 34 | for k in keys: 35 | _ = client[k] 36 | 37 | end = time.time() 38 | duration = end - start 39 | 40 | all_telem_hz = 1 / (duration / ITER) 41 | telem_hz = all_telem_hz * len(keys) 42 | 43 | print "Done!" 44 | print "{0} reads of all keys executed in {1} seconds".format(ITER, duration) 45 | print "Read rate for all {0} telementry values is: {1} Hz".format(len(keys), 46 | all_telem_hz) 47 | print "Read rate for a single telementry value is: {0} Hz".format(telem_hz) -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | import doctest 2 | import glob 3 | import os 4 | 5 | 6 | files = glob.glob(os.path.join('tests','*.txt')) 7 | opts = (doctest.REPORT_ONLY_FIRST_FAILURE|doctest.ELLIPSIS| 8 | doctest.NORMALIZE_WHITESPACE) 9 | 10 | for f in files: 11 | doctest.testfile(f, optionflags=opts) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup(name='api', 4 | version='1.2', 5 | description='Python iRacing API client', 6 | author='Robert Wallhead', 7 | author_email='robert@thisismyrobot.com', 8 | url='https://github.com/thisismyrobot/python-iracing-api', 9 | py_modules=['api'], 10 | ) -------------------------------------------------------------------------------- /tests/api.txt: -------------------------------------------------------------------------------- 1 | iRacing API client 2 | ================== 3 | 4 | Tests for the iRacing API client. 5 | 6 | Data file 7 | --------- 8 | 9 | This series of tests will work with a cached copy of the memory file, so we need 10 | to get that. 11 | 12 | >>> import os 13 | >>> mmap_file = os.path.join('tests', 'memorydump.dmp') 14 | >>> mmap_f = open(mmap_file, 'r+b') 15 | 16 | And we need to convince the API client that it should actually be using this 17 | file instead of vainly looking in memory for iRacing's. We do this by creating a 18 | file with the correct tag for the API client to inspect. 19 | 20 | >>> import api 21 | >>> import mmap 22 | >>> mocked_mmap = mmap.mmap(mmap_f.fileno(), api.MEMMAPFILESIZE, 23 | ... 'Local\\IRSDKMemMapFileName') 24 | >>> api_client = api.API() 25 | 26 | YAML (session) 27 | -------------- 28 | 29 | There is YAML generated for the session, and it updates slowly. The YAML has 30 | is accessable through nested keys. 31 | 32 | >>> api_client['DriverInfo']['DriverHeadPosX'] 33 | -0.025000000000000001 34 | 35 | >>> api_client['SessionInfo']['Sessions'][0]['ResultsFastestLap'][0]['FastestTime'] 36 | 0.0 37 | 38 | >>> api_client['SessionInfo']['Sessions'][0]['ResultsFastestLap'][0]['CarIdx'] 39 | 255 40 | 41 | Telemetry 42 | --------- 43 | 44 | There is telemetry updated 60 times a second, we can access that in the same 45 | way. 46 | 47 | >>> api_client['Speed'] 48 | 1.7011767625808716 49 | 50 | Helpers 51 | ------- 52 | 53 | There are some convenience helpers too - you can access the client like a dict 54 | so the keys are available too: 55 | 56 | >>> keys = api_client.keys() 57 | >>> len(keys) 58 | 101 59 | 60 | >>> 'SessionInfo' in keys 61 | True 62 | 63 | >>> 'Speed' in keys 64 | True -------------------------------------------------------------------------------- /tests/memorydump.dmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thisismyrobot/python-iracing-api/faede3285983108058e1d3bdbc60dbc7c414653f/tests/memorydump.dmp -------------------------------------------------------------------------------- /tests/mmap.txt: -------------------------------------------------------------------------------- 1 | Memory file tests 2 | ----------------- 3 | 4 | Specifically, if it's not there. 5 | 6 | If iRacing is not running, the memory mapped file will not exist. We handle this 7 | by raising an exception - this happens when there is no file, or the file is 8 | there, but empty. 9 | 10 | >>> import api 11 | >>> api_client = api.API() 12 | Traceback (most recent call last): 13 | File "", line 1, in ? 14 | Exception: iRacing memory mapped file could not be found --------------------------------------------------------------------------------