├── data └── .gitkeep ├── db └── .gitkeep ├── test_out └── .gitkeep ├── gt7dashboard ├── __init__.py ├── test │ ├── __init__.py │ ├── test_gt7communication.py │ ├── test_gt7diagrams.py │ └── test_gt7helper.py ├── gt7lap.py ├── gt7help.py ├── gt7communication.py ├── gt7helper.py └── gt7diagrams.py ├── pytest.ini ├── setup.cfg ├── README.assets ├── screenshot.png ├── screenshot_boost.png ├── screenshot_gear.png ├── screenshot_rpm.png ├── screenshot_speed.png ├── screenshot_yaw.png ├── screenshot_braking.png ├── screenshot_fuelmap.png ├── screenshot_header.png ├── screenshot_social.png ├── screenshot_coasting.png ├── screenshot_race_line.png ├── screenshot_raceline.png ├── screenshot_throttle.png ├── screenshot_timediff.png ├── screenshot_timetable.png ├── screenshot_tirespeed.png ├── screenshot_tuninginfo.png ├── screenshot_lapcontrols.png ├── screenshot_manualcontrols.png ├── screenshot_speeddeviation.png └── screenshot_peaks_and_valleys.png ├── requirements.txt ├── res └── image-20220805170136418.png ├── run.sh ├── run.ps1 ├── run.command ├── helper └── download_cars_csv.py ├── Dockerfile ├── brew.command ├── .github └── workflows │ ├── test.yaml │ └── deploy.yaml ├── Makefile ├── .gitignore ├── generate_doc.py ├── pyproject.toml ├── README.md ├── main.py └── LICENSE /data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /db/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_out/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gt7dashboard/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gt7dashboard/test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | minversion = 7.0 3 | pythonpath = src 4 | testpaths = 5 | tests 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = gt7dashboard 3 | version = 1.0.0 4 | 5 | [options] 6 | packages = find: 7 | -------------------------------------------------------------------------------- /README.assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bokeh~=3.6.3 2 | pycryptodome~=3.21.0 3 | tabulate~=0.8.10 4 | pandas~=2.2.3 5 | scipy~=1.15.1 6 | -------------------------------------------------------------------------------- /res/image-20220805170136418.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/res/image-20220805170136418.png -------------------------------------------------------------------------------- /README.assets/screenshot_boost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot_boost.png -------------------------------------------------------------------------------- /README.assets/screenshot_gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot_gear.png -------------------------------------------------------------------------------- /README.assets/screenshot_rpm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot_rpm.png -------------------------------------------------------------------------------- /README.assets/screenshot_speed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot_speed.png -------------------------------------------------------------------------------- /README.assets/screenshot_yaw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot_yaw.png -------------------------------------------------------------------------------- /README.assets/screenshot_braking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot_braking.png -------------------------------------------------------------------------------- /README.assets/screenshot_fuelmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot_fuelmap.png -------------------------------------------------------------------------------- /README.assets/screenshot_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot_header.png -------------------------------------------------------------------------------- /README.assets/screenshot_social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot_social.png -------------------------------------------------------------------------------- /README.assets/screenshot_coasting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot_coasting.png -------------------------------------------------------------------------------- /README.assets/screenshot_race_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot_race_line.png -------------------------------------------------------------------------------- /README.assets/screenshot_raceline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot_raceline.png -------------------------------------------------------------------------------- /README.assets/screenshot_throttle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot_throttle.png -------------------------------------------------------------------------------- /README.assets/screenshot_timediff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot_timediff.png -------------------------------------------------------------------------------- /README.assets/screenshot_timetable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot_timetable.png -------------------------------------------------------------------------------- /README.assets/screenshot_tirespeed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot_tirespeed.png -------------------------------------------------------------------------------- /README.assets/screenshot_tuninginfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot_tuninginfo.png -------------------------------------------------------------------------------- /README.assets/screenshot_lapcontrols.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot_lapcontrols.png -------------------------------------------------------------------------------- /README.assets/screenshot_manualcontrols.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot_manualcontrols.png -------------------------------------------------------------------------------- /README.assets/screenshot_speeddeviation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot_speeddeviation.png -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pip3 install -r requirements.txt 4 | 5 | python3 helper/download_cars_csv.py 6 | 7 | python3 -m bokeh serve . 8 | -------------------------------------------------------------------------------- /README.assets/screenshot_peaks_and_valleys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipem/gt7dashboard/HEAD/README.assets/screenshot_peaks_and_valleys.png -------------------------------------------------------------------------------- /run.ps1: -------------------------------------------------------------------------------- 1 | pip install -r requirements.txt 2 | 3 | python helper/download_cars_csv.py 4 | 5 | python -m bokeh serve . 6 | 7 | Read-Host -Prompt "Press Enter to continue..." 8 | -------------------------------------------------------------------------------- /run.command: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(dirname "$0")" 4 | 5 | pip3 install -r requirements.txt 6 | 7 | python3 helper/download_cars_csv.py 8 | 9 | python3 -m bokeh serve . 10 | -------------------------------------------------------------------------------- /helper/download_cars_csv.py: -------------------------------------------------------------------------------- 1 | import urllib.request 2 | 3 | url = 'https://raw.githubusercontent.com/ddm999/gt7info/web-new/_data/db/cars.csv' 4 | filename = 'db/cars.csv' 5 | 6 | urllib.request.urlretrieve(url, filename) 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY requirements.txt . 6 | 7 | RUN pip install --no-cache-dir -r requirements.txt 8 | 9 | COPY . . 10 | 11 | ADD https://raw.githubusercontent.com/ddm999/gt7info/web-new/_data/db/cars.csv db/cars.csv 12 | RUN chmod -R 755 db 13 | 14 | CMD [ "bokeh", "serve", "." ] 15 | -------------------------------------------------------------------------------- /brew.command: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script should be used on MacOS when Python is a managed environment; 4 | # i.e.: when Python is installed via Homebrew or similar package managers 5 | # 6 | # @author MBDesu 7 | 8 | cd "$(dirname "$0")" 9 | 10 | python3 -m venv ./venv 11 | source ./venv/bin/activate 12 | python3 -m pip install -r requirements.txt 13 | python3 helper/download_cars_csv.py 14 | python3 -m bokeh serve . 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [ ubuntu-latest, windows-latest ] 12 | python-version: [ "3.10", "3.11", "3.12", "3.13" ] 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: | 22 | make car_lists 23 | python -m pip install --upgrade pip 24 | pip install -r requirements.txt 25 | pip install pytest 26 | - name: Test with pytest 27 | run: | 28 | python -m pytest 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/bash -O expand_aliases 2 | 3 | limited: 4 | GT7_LIMITED=true python3 gt7telemetry.py 192.168.178.120 5 | 6 | race: 7 | BOKEH_LOG_LEVEL=fatal GT7_LIMITED=true GT7_HIDE_ANALYSIS=true GT7_HIDE_TUNING=true python3 gt7telemetry.py 192.168.178.120 8 | 9 | normal: 10 | python3 gt7telemetry.py 192.168.178.120 11 | 12 | doc: 13 | python3 generate_doc.py 14 | 15 | deps: 16 | python3 -m pip install -r requirements.txt 17 | 18 | test_deps: 19 | python3 -m pip install pytest 20 | 21 | test: test_deps deps 22 | python3 -m pytest . 23 | 24 | car_lists: 25 | python3 helper/download_cars_csv.py 26 | 27 | serve: 28 | bokeh serve . 29 | 30 | deploy: 31 | git push 32 | ssh ${MK_SERVER_USER}@${MK_SERVER_HOST} "cd work/gt7dashboard && git pull && git switch '$(shell git rev-parse --abbrev-ref HEAD)' && cd ~/git/conf/docker && sudo -S CONTAINER_NAME=gt7dashboard make build" 33 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Packages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | IMAGE_NAME: ${{ github.repository }} 11 | 12 | jobs: 13 | build-and-deploy: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | - name: Login to GitHub Packages 19 | uses: docker/login-action@v2 20 | with: 21 | registry: ${{ env.REGISTRY }} 22 | username: ${{ github.actor }} 23 | password: ${{ secrets.GITHUB_TOKEN }} 24 | - name: Extract metadata (tags, labels) for Docker 25 | id: meta 26 | uses: docker/metadata-action@v4 27 | with: 28 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 29 | - name: Build and push Docker image 30 | uses: docker/build-push-action@v2 31 | with: 32 | context: . 33 | push: true 34 | tags: ${{ steps.meta.outputs.tags }} 35 | labels: ${{ steps.meta.outputs.labels }} 36 | -------------------------------------------------------------------------------- /gt7dashboard/test/test_gt7communication.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import unittest 4 | 5 | from gt7dashboard import gt7communication 6 | from gt7dashboard.gt7lap import Lap 7 | 8 | PLAYSTATION_IP = "ps5wifi" 9 | 10 | 11 | # check if host is up 12 | def is_host_up(ip: str) -> bool: 13 | response = os.system("ping -c 1 " + PLAYSTATION_IP) 14 | 15 | #and then check the response... 16 | if response == 0: 17 | return True 18 | else: 19 | return False 20 | 21 | 22 | @unittest.skipIf(not is_host_up(PLAYSTATION_IP), 23 | "Playstation host is not up on %s" % (PLAYSTATION_IP)) 24 | class GT7CommunicationTest(unittest.TestCase): 25 | @classmethod 26 | def setUpClass(self) -> None: 27 | self.gt7comm = gt7communication.GT7Communication(PLAYSTATION_IP) 28 | # Do not quit with the main process 29 | self.gt7comm.daemon = False 30 | self.gt7comm.start() 31 | # Sleep until connection is setup 32 | # TODO Add timeout 33 | while not self.gt7comm.is_connected(): 34 | time.sleep(0.1) 35 | 36 | @classmethod 37 | def tearDownClass(self) -> None: 38 | self.gt7comm.stop() 39 | 40 | def test_get_water_temp(self): 41 | car_data = self.gt7comm.get_last_data() 42 | self.assertTrue(self.gt7comm.is_connected()) 43 | # is always 85 44 | self.assertEqual(85, car_data.water_temp) 45 | 46 | # def test_run_add_debug(self): 47 | # while self.gt7comm.is_connected(): 48 | # car_data = self.gt7comm.get_last_data() 49 | # # print(car_data.rpm, car_data.in_race) 50 | 51 | def test_load_laps(self): 52 | self.gt7comm.laps = [Lap()] 53 | self.gt7comm.laps[0].number = 0 54 | 55 | laps = [Lap(), Lap()] 56 | laps[0].number = 1 57 | laps[1].number = 2 58 | 59 | self.gt7comm.load_laps(laps, to_last_position=True) 60 | self.assertEqual(3, len(self.gt7comm.laps)) 61 | self.assertEqual(1, self.gt7comm.laps[1].number) 62 | 63 | self.gt7comm.load_laps(laps, to_first_position=True) 64 | self.assertEqual(5, len(self.gt7comm.laps)) 65 | self.assertEqual(1, self.gt7comm.laps[3].number) 66 | 67 | self.gt7comm.load_laps(laps, replace_other_laps=True) 68 | self.assertEqual(2, len(self.gt7comm.laps)) 69 | self.assertEqual(1, self.gt7comm.laps[0].number) 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | /data/ 131 | 132 | .idea 133 | 134 | session.csv 135 | 136 | test_out 137 | db/cars.csv 138 | gt7dashboard/test/data 139 | -------------------------------------------------------------------------------- /generate_doc.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import gt7dashboard.gt7help as gt7help 4 | 5 | def add_screenshot(filename): 6 | # join path 7 | str_screenshot_path = os.path.join("README.assets", filename) 8 | # check if file exists 9 | if os.path.exists(str_screenshot_path): 10 | return f"" 11 | else: 12 | raise Exception("File does not exist: " + str_screenshot_path) 13 | 14 | if __name__ == '__main__': 15 | 16 | out_markdown = "## Manual\n\n" 17 | 18 | out_markdown += "### Tab 'Get Faster'\n\n" 19 | 20 | out_markdown += "#### Header\n\n" 21 | out_markdown += add_screenshot("screenshot_header.png") + "\n\n" 22 | out_markdown += gt7help.HEADER + "\n\n" 23 | 24 | out_markdown += "#### Lap Controls\n\n" 25 | out_markdown += add_screenshot("screenshot_lapcontrols.png") + "\n\n" 26 | out_markdown += gt7help.LAP_CONTROLS + "\n\n" 27 | 28 | out_markdown += "#### Time / Diff\n\n" 29 | out_markdown += add_screenshot("screenshot_timediff.png") + "\n\n" 30 | out_markdown += gt7help.TIME_DIFF + "\n\n" 31 | 32 | out_markdown += "#### Manual Controls\n\n" 33 | out_markdown += add_screenshot("screenshot_manualcontrols.png") + "\n\n" 34 | out_markdown += gt7help.MANUAL_CONTROLS + "\n\n" 35 | 36 | out_markdown += "#### Speed \n\n" 37 | out_markdown += add_screenshot("screenshot_speed.png") + "\n\n" 38 | out_markdown += gt7help.SPEED_DIAGRAM + "\n\n" 39 | 40 | out_markdown += "#### Race Line\n\n" 41 | out_markdown += add_screenshot("screenshot_raceline.png") + "\n\n" 42 | out_markdown += gt7help.RACE_LINE_MINI + "\n\n" 43 | 44 | out_markdown += "#### Peaks and Valleys\n\n" 45 | out_markdown += add_screenshot("screenshot_peaks_and_valleys.png") + "\n\n" 46 | out_markdown += gt7help.SPEED_PEAKS_AND_VALLEYS + "\n\n" 47 | 48 | out_markdown += "#### Speed Deviation (Spd. Dev.)\n\n" 49 | out_markdown += add_screenshot("screenshot_speeddeviation.png") + "\n\n" 50 | out_markdown += gt7help.SPEED_VARIANCE + "\n\n" 51 | 52 | out_markdown += """I got inspired for this diagram by the [Your Data Driven Podcast](https://www.yourdatadriven.com/). 53 | On two different episodes of this podcast both [Peter Krause](https://www.yourdatadriven.com/ep12-go-faster-now-with-motorsports-data-analytics-guru-peter-krause/) and [Ross Bentley](https://www.yourdatadriven.com/ep3-tips-for-racing-faster-with-ross-bentley/) mentioned this visualization. 54 | If they had one graph it would be the deviation in the (best) laps of the same driver, to improve said drivers performance learning from the differences in already good laps. If they could do it once, they could do it every time.\n\n""" 55 | 56 | out_markdown += "#### Throttle\n\n" 57 | out_markdown += add_screenshot("screenshot_throttle.png") + "\n\n" 58 | out_markdown += gt7help.THROTTLE_DIAGRAM + "\n\n" 59 | 60 | out_markdown += "#### Yaw Rate / Second\n\n" 61 | out_markdown += add_screenshot("screenshot_yaw.png") + "\n\n" 62 | out_markdown += gt7help.YAW_RATE_DIAGRAM + "\n\n" 63 | out_markdown += "[Suellio Almeida](https://suellioalmeida.ca) introduced this concept to me. See [here](https://www.youtube.com/watch?v=B92vFKKjyB0) for more information.\n\n" 64 | 65 | out_markdown += "#### Braking\n\n" 66 | out_markdown += add_screenshot("screenshot_braking.png") + "\n\n" 67 | out_markdown += gt7help.BRAKING_DIAGRAM + "\n\n" 68 | 69 | out_markdown += "#### Coasting\n\n" 70 | out_markdown += add_screenshot("screenshot_coasting.png") + "\n\n" 71 | out_markdown += gt7help.COASTING_DIAGRAM + "\n\n" 72 | 73 | out_markdown += "#### Gear\n\n" 74 | out_markdown += add_screenshot("screenshot_gear.png") + "\n\n" 75 | out_markdown += gt7help.GEAR_DIAGRAM + "\n\n" 76 | 77 | out_markdown += "#### RPM\n\n" 78 | out_markdown += add_screenshot("screenshot_rpm.png") + "\n\n" 79 | out_markdown += gt7help.RPM_DIAGRAM + "\n\n" 80 | 81 | out_markdown += "#### Boost\n\n" 82 | out_markdown += add_screenshot("screenshot_boost.png") + "\n\n" 83 | out_markdown += gt7help.BOOST_DIAGRAM + "\n\n" 84 | 85 | out_markdown += "#### Tire Speed / Car Speed\n\n" 86 | out_markdown += add_screenshot("screenshot_tirespeed.png") + "\n\n" 87 | out_markdown += gt7help.TIRE_DIAGRAM + "\n\n" 88 | 89 | out_markdown += "#### Time Table\n\n" 90 | out_markdown += add_screenshot("screenshot_timetable.png") + "\n\n" 91 | out_markdown += gt7help.TIME_TABLE + "\n\n" 92 | 93 | out_markdown += "#### Fuel Map\n\n" 94 | out_markdown += add_screenshot("screenshot_fuelmap.png") + "\n\n" 95 | out_markdown += gt7help.FUEL_MAP + "\n\n" 96 | 97 | out_markdown += "#### Tuning Info\n\n" 98 | out_markdown += add_screenshot("screenshot_tuninginfo.png") + "\n\n" 99 | out_markdown += gt7help.TUNING_INFO + "\n\n" 100 | 101 | out_markdown += "### Tab 'Race Line'\n\n" 102 | 103 | out_markdown += add_screenshot("screenshot_race_line.png") + "\n\n" 104 | out_markdown += gt7help.RACE_LINE_BIG + "\n\n" 105 | 106 | print(out_markdown) 107 | 108 | with open("README.md", 'r+') as f: 109 | content = f.read() 110 | pos = content.find('## Manual') 111 | if pos != -1: 112 | f.seek(pos) 113 | f.truncate() 114 | f.write(out_markdown) 115 | -------------------------------------------------------------------------------- /gt7dashboard/gt7lap.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from gt7dashboard import gt7helper 3 | 4 | 5 | class Lap: 6 | def __init__(self): 7 | # Nice title for lap 8 | self.title = "" 9 | # Number of all lap ticks 10 | self.lap_ticks = 1 11 | # Lap time after crossing the finish line 12 | self.lap_finish_time = 0 13 | # Live time during a live lap 14 | self.lap_live_time = 0 15 | # Total number of laps 16 | self.total_laps = 0 17 | # Number of current lap 18 | self.number = 0 19 | # Aggregated number of instances where condition is true 20 | self.throttle_and_brake_ticks = 0 21 | self.no_throttle_and_no_brake_ticks = 0 22 | self.full_brake_ticks = 0 23 | self.full_throttle_ticks = 0 24 | self.tires_overheated_ticks = 0 25 | self.tires_spinning_ticks = 0 26 | # Data points with value for every tick 27 | self.data_throttle = [] 28 | self.data_braking = [] 29 | self.data_coasting = [] 30 | self.data_speed = [] 31 | self.data_time = [] 32 | self.data_rpm = [] 33 | self.data_gear = [] 34 | self.data_tires = [] 35 | # Positions on x,y,z 36 | self.data_position_x = [] 37 | self.data_position_y = [] 38 | self.data_position_z = [] 39 | # Fuel 40 | self.fuel_at_start = 0 41 | self.fuel_at_end = -1 42 | self.fuel_consumed = -1 43 | # Boost 44 | self.data_boost = [] 45 | # Yaw Rate 46 | self.data_rotation_yaw = [] 47 | self.data_absolute_yaw_rate_per_second = [] 48 | # Car 49 | self.car_id = 0 50 | 51 | # Always record was set when recording the lap, likely a replay 52 | self.is_replay = False 53 | self.is_manual = False 54 | 55 | self.lap_start_timestamp = datetime.now() 56 | self.lap_end_timestamp = -1 57 | 58 | def __str__(self): 59 | return "\n %s, %2d, %1.f, %4d, %4d, %4d" % ( 60 | self.title, 61 | self.number, 62 | self.fuel_at_end, 63 | self.full_throttle_ticks, 64 | self.full_brake_ticks, 65 | self.no_throttle_and_no_brake_ticks, 66 | ) 67 | 68 | def format(self): 69 | return "Lap %2d, %s (%d Ticks)" % ( 70 | self.number, 71 | self.title, 72 | len(self.data_speed), 73 | ) 74 | 75 | def get_speed_peaks_and_valleys(self): 76 | ( 77 | peak_speed_data_x, 78 | peak_speed_data_y, 79 | valley_speed_data_x, 80 | valley_speed_data_y, 81 | ) = gt7helper.get_speed_peaks_and_valleys(self) 82 | 83 | return ( 84 | peak_speed_data_x, 85 | peak_speed_data_y, 86 | valley_speed_data_x, 87 | valley_speed_data_y, 88 | ) 89 | 90 | def car_name(self) -> str: 91 | # FIXME Breaking change. Not all log files up to this point have this attribute, remove this later 92 | if (not hasattr(self, "car_id")): 93 | return "Car not logged" 94 | return gt7helper.get_car_name_for_car_id(self.car_id) 95 | 96 | def get_data_dict(self, distance_mode=True) -> dict[str, list]: 97 | 98 | raceline_y_throttle, raceline_x_throttle, raceline_z_throttle = gt7helper.get_race_line_coordinates_when_mode_is_active(self, mode=gt7helper.RACE_LINE_THROTTLE_MODE) 99 | raceline_y_braking, raceline_x_braking, raceline_z_braking = gt7helper.get_race_line_coordinates_when_mode_is_active(self, mode=gt7helper.RACE_LINE_BRAKING_MODE) 100 | raceline_y_coasting, raceline_x_coasting, raceline_z_coasting = gt7helper.get_race_line_coordinates_when_mode_is_active(self, mode=gt7helper.RACE_LINE_COASTING_MODE) 101 | 102 | data = { 103 | "throttle": self.data_throttle, 104 | "brake": self.data_braking, 105 | "speed": self.data_speed, 106 | "time": self.data_time, 107 | "tires": self.data_tires, 108 | "rpm": self.data_rpm, 109 | "boost": self.data_boost, 110 | "yaw_rate": self.data_absolute_yaw_rate_per_second, 111 | "gear": self.data_gear, 112 | "ticks": list(range(len(self.data_speed))), 113 | "coast": self.data_coasting, 114 | "raceline_y": self.data_position_y, 115 | "raceline_x": self.data_position_x, 116 | "raceline_z": self.data_position_z, 117 | # For a raceline when throttle is engaged 118 | "raceline_y_throttle": raceline_y_throttle, 119 | "raceline_x_throttle": raceline_x_throttle, 120 | "raceline_z_throttle": raceline_z_throttle, 121 | # For a raceline when braking is engaged 122 | "raceline_y_braking": raceline_y_braking, 123 | "raceline_x_braking": raceline_x_braking, 124 | "raceline_z_braking": raceline_z_braking, 125 | # For a raceline when neither throttle nor brake is engaged 126 | "raceline_y_coasting": raceline_y_coasting, 127 | "raceline_x_coasting": raceline_x_coasting, 128 | "raceline_z_coasting": raceline_z_coasting, 129 | 130 | "distance": gt7helper.get_x_axis_depending_on_mode(self, distance_mode), 131 | } 132 | 133 | return data 134 | -------------------------------------------------------------------------------- /gt7dashboard/gt7help.py: -------------------------------------------------------------------------------- 1 | from bokeh.models import Div 2 | 3 | from gt7dashboard import gt7helper 4 | 5 | SPEED_VARIANCE = f"""Displays the speed deviation of the fastest laps within a {gt7helper.DEFAULT_FASTEST_LAPS_PERCENT_THRESHOLD * 100}% time difference threshold of the fastest lap. 6 | Replay laps are ignored. The speed deviation is calculated as the standard deviation between these fastest laps. 7 | 8 | With a perfect driver in an ideal world, this line would be flat. In a real world situation, you will get an almost flat line, 9 | with bumps at the corners and long straights. This is where even your best laps deviate. 10 | 11 | You may get some insights for improvement on your consistency if you look at the points of the track where this line is bumpy. 12 | 13 | The list on the right hand side shows your best laps that are token into consideration for the speed variance. 14 | """ 15 | 16 | HEADER = """The red or green button reflects the current connection status to Gran Turismo 7. i.e. if there was a packet received successfully in the last second, the button will turn green. 17 | 18 | Next is a brief description of the last and reference lap. The reference lap can be selected on the right side.""" 19 | RACE_LINE_MINI = """This is a race line map with the last lap (blue) and the reference lap (magenta). Zoom in for more details. 20 | 21 | This map is helpful if you are using the index number of a graph to quickly determine where in the lap a measurement was taken. 22 | 23 | See the tab 'Race Line' for a more detailed race line.""" 24 | MANUAL_CONTROLS = """'Log Lap Now' will log a lap now even you have not crossed the finished line. This is helpful for missions or license tests where the end of a test is not necessarily identical with the finish line. 25 | 26 | The checkbox 'Record Replays' will allow you to record replays. Be careful since also background action before and after a time trial is counted as a replay. This is when a car drives on the track in the background of the menu. 27 | 28 | In the 'Best Lap' dropdown list you can select the reference lap. Usually this will point to the best lap of the session. 29 | """ 30 | 31 | TIME_DIFF = """ This is a graph for showing the relative time difference between the last lap and the reference lap. 32 | Everything under the solid bar at 0 is slower than the reference lap. Everything above is slower than the reference lap. 33 | 34 | If you see a bump in this graph to the top or the bottom this means that you were slower or faster at this point respectively. 35 | """ 36 | LAP_CONTROLS = """You can reset all laps with the 'Reset Laps' button. This is helpful if you are switching tracks or cars in a session. Otherwise the different tracks will mix in the dashboard. 37 | 'Save Laps' will save your recorded laps to a file. You can load the laps afterwards with the dropdown list to the right.""" 38 | SPEED_DIAGRAM = """The total speed of the laps selected. This value is in km/h. or mph. depending on your in-game setting""" 39 | THROTTLE_DIAGRAM = """This is the amount of throttle pressure from 0% to 100% of the laps selected.""" 40 | BRAKING_DIAGRAM = """This is the amount of braking pressure from 0% to 100% of the laps selected.""" 41 | COASTING_DIAGRAM = """This is the amount of coasting from 0% to 100% of the laps selected. Coasting is when neither throttle nor brake are engaged.""" 42 | GEAR_DIAGRAM = """This is the current gear of the laps selected.""" 43 | RPM_DIAGRAM = "This is the current RPM of the laps selected." 44 | BOOST_DIAGRAM = "This is the current Boost in x100 kPa of the laps selected." 45 | TIRE_DIAGRAM = """This is the relation between the speed of the tires and the speed of the car. If your tires are faster than your car, your tires might be spinning. If they are slower, your tires might be blocking. Use this judge your car control.""" 46 | 47 | SPEED_PEAKS_AND_VALLEYS = """A list of speed peaks and valleys for the selected laps. We assume peaks are straights (s) and valleys are turns (T). Use this to compare the difference in speed between the last lap and the reference lap on given positions of the race track.""" 48 | TIME_TABLE = """A table with logged information of the session. # is the number of the lap as reported by the game. There might be multiple laps of the same number if you restarted a session. Time and Diff are self-explaining. Info will hold additional meta data, for example if this lap was a replay. 49 | Fuel Consumed is the amount of fuel consumed in the lap. 50 | 51 | What follows know are simple metrics for the characteristics of the lap. This is counted as ticks, which means instances when the game reported a state. For example Full Throttle = 500 means that you were on full throttle during 500 instances when the game sent its telemetry. 52 | The same goes for Full Brake, Coast and Tire Spin. Use this to easily compare your laps. 53 | 54 | You can click on one of these laps to add them to the diagrams above. These laps will be deleted if you reset the view or reload the page. 55 | 56 | Car will hold the car name. You will have to have the `db/cars.csv` file downloaded for this to work. 57 | """ 58 | FUEL_MAP = """This fuel map will help to determine the fuel setting of your car. The game does not report the current fuel setting, so this map is relative. 59 | The current fuel setting will always be at 0. If you want to change the fuel to a leaner setting count downwards with the amount of steps left. For example: If you are at fuel setting 2 in the game and want to go to the games fuel setting 5, have a look at Fuel Lvl. 3 in this map. 60 | It will give you a raw assumption of the laps and time remaining and the assumed time difference in lap time for the new setting.""" 61 | TUNING_INFO = """Here is some useful information you may use for tuning. Such as Max Speed and minimal body height in relation to the track. The later seems to be helpful when determining the possible body height.""" 62 | 63 | RACE_LINE_BIG = """This is a race line map with the last lap (blue) and the reference lap (magenta). This diagram does also feature spead peaks (▴) and valleys (▾) as well as throttle, brake and coasting zones. 64 | 65 | The thinner line of the two is your last lap. The reference line is the thicker translucent line. If you want to make out differences in the race line have a look at the middle of the reference lap line and your line. You may zoom in to spot the differences and read the values on peaks and valleys. 66 | """ 67 | 68 | YAW_RATE_DIAGRAM = """This is the yaw rate per second of your car. Use this to determine the Maximum Rotation Point (MRP). At this point you should normally accelerate.""" 69 | 70 | 71 | def get_help_div(help_text_resource): 72 | return Div(text=get_help_text_resource(help_text_resource), width=7, height=5) 73 | 74 | 75 | def get_help_text_resource(help_text_resource): 76 | return f""" 77 |
🟢
" 45 | else: 46 | div_connection_info.text += "🔴
" 47 | 48 | 49 | def update_reference_lap_select(laps): 50 | reference_lap_select.options = [ 51 | tuple(("-1", "Best Lap")) 52 | ] + gt7helper.bokeh_tuple_for_list_of_laps(laps) 53 | 54 | 55 | @linear() 56 | def update_fuel_map(step): 57 | global g_stored_fuel_map 58 | 59 | if len(app.gt7comm.laps) == 0: 60 | div_fuel_map.text = "" 61 | return 62 | 63 | last_lap = app.gt7comm.laps[0] 64 | 65 | if last_lap == g_stored_fuel_map: 66 | return 67 | else: 68 | g_stored_fuel_map = last_lap 69 | 70 | # TODO Add real live data during a lap 71 | div_fuel_map.text = gt7diagrams.get_fuel_map_html_table(last_lap) 72 | 73 | 74 | def update_race_lines(laps: List[Lap], reference_lap: Lap): 75 | """ 76 | This function updates the race lines on the second tab with the amount of laps 77 | that the race line tab can hold 78 | """ 79 | global race_lines, race_lines_data 80 | 81 | 82 | reference_lap_data = reference_lap.get_data_dict() 83 | 84 | for i, lap in enumerate(laps[:len(race_lines)]): 85 | logger.info(f"Updating Race Line for Lap {len(laps) -i} - {lap.title} and reference lap {reference_lap.title}") 86 | 87 | race_lines[i].title.text = "Lap %d - %s (%s), Reference Lap: %s (%s)" % (len(laps) - i, lap.title, lap.car_name(), reference_lap.title, reference_lap.car_name()) 88 | 89 | lap_data = lap.get_data_dict() 90 | race_lines_data[i][0].data_source.data = lap_data 91 | race_lines_data[i][1].data_source.data = lap_data 92 | race_lines_data[i][2].data_source.data = lap_data 93 | 94 | race_lines_data[i][3].data_source.data = reference_lap_data 95 | race_lines_data[i][4].data_source.data = reference_lap_data 96 | race_lines_data[i][5].data_source.data = reference_lap_data 97 | 98 | race_lines[i].axis.visible = False 99 | 100 | gt7diagrams.add_annotations_to_race_line(race_lines[i], lap, reference_lap) 101 | 102 | # Fixme not working 103 | race_lines[i].x_range = race_lines[0].x_range 104 | 105 | 106 | def update_header_line(div: Div, last_lap: Lap, reference_lap: Lap): 107 | div.text = f"Last Lap: {last_lap.title} ({last_lap.car_name()})
" \ 108 | f"Reference Lap: {reference_lap.title} ({reference_lap.car_name()})
" 109 | 110 | def update_lap_change(): 111 | """ 112 | Is called whenever a lap changes. 113 | It detects if the telemetry date retrieved is the same as the data displayed. 114 | If true, it updates all the visual elements. 115 | """ 116 | global g_laps_stored 117 | global g_session_stored 118 | global g_connection_status_stored 119 | global g_telemetry_update_needed 120 | global g_reference_lap_selected 121 | 122 | update_start_time = time.time() 123 | 124 | laps = app.gt7comm.get_laps() 125 | 126 | if app.gt7comm.session != g_session_stored: 127 | update_tuning_info() 128 | g_session_stored = copy.copy(app.gt7comm.session) 129 | 130 | if app.gt7comm.is_connected() != g_connection_status_stored: 131 | update_connection_info() 132 | g_connection_status_stored = copy.copy(app.gt7comm.is_connected()) 133 | 134 | # This saves on cpu time, 99.9% of the time this is true 135 | if laps == g_laps_stored and not g_telemetry_update_needed: 136 | return 137 | 138 | logger.debug("Rerendering laps") 139 | 140 | reference_lap = Lap() 141 | 142 | if len(laps) > 0: 143 | 144 | last_lap = laps[0] 145 | 146 | if len(laps) > 1: 147 | reference_lap = gt7helper.get_last_reference_median_lap( 148 | laps, reference_lap_selected=g_reference_lap_selected 149 | )[1] 150 | 151 | div_speed_peak_valley_diagram.text = get_speed_peak_and_valley_diagram(last_lap, reference_lap) 152 | 153 | update_header_line(div_header_line, last_lap, reference_lap) 154 | 155 | logger.debug("Updating of %d laps" % len(laps)) 156 | 157 | start_time = time.time() 158 | update_time_table(laps) 159 | logger.debug("Updating time table took %dms" % ((time.time() - start_time) * 1000)) 160 | 161 | start_time = time.time() 162 | update_reference_lap_select(laps) 163 | logger.debug("Updating reference lap select took %dms" % ((time.time() - start_time) * 1000)) 164 | 165 | start_time = time.time() 166 | update_speed_velocity_graph(laps) 167 | logger.debug("Updating speed velocity graph took %dms" % ((time.time() - start_time) * 1000)) 168 | 169 | start_time = time.time() 170 | update_race_lines(laps, reference_lap) 171 | logger.debug("Updating race lines took %dms" % ((time.time() - start_time) * 1000)) 172 | 173 | logger.debug("End of updating laps, whole Update took %dms" % ((time.time() - update_start_time) * 1000)) 174 | 175 | g_laps_stored = laps.copy() 176 | g_telemetry_update_needed = False 177 | 178 | 179 | def update_speed_velocity_graph(laps: List[Lap]): 180 | last_lap, reference_lap, median_lap = gt7helper.get_last_reference_median_lap( 181 | laps, reference_lap_selected=g_reference_lap_selected 182 | ) 183 | 184 | if last_lap: 185 | last_lap_data = last_lap.get_data_dict() 186 | race_diagram.source_last_lap.data = last_lap_data 187 | last_lap_race_line.data_source.data = last_lap_data 188 | 189 | if reference_lap and len(reference_lap.data_speed) > 0: 190 | reference_lap_data = reference_lap.get_data_dict() 191 | race_diagram.source_time_diff.data = calculate_time_diff_by_distance(reference_lap, last_lap) 192 | race_diagram.source_reference_lap.data = reference_lap_data 193 | reference_lap_race_line.data_source.data = reference_lap_data 194 | 195 | if median_lap: 196 | race_diagram.source_median_lap.data = median_lap.get_data_dict() 197 | 198 | 199 | s_race_line.legend.visible = False 200 | s_race_line.axis.visible = False 201 | 202 | fastest_laps = race_diagram.update_fastest_laps_variance(laps) 203 | logger.info("Updating Speed Deviance with %d fastest laps" % len(fastest_laps)) 204 | div_deviance_laps_on_display.text = "" 205 | for fastest_lap in fastest_laps: 206 | div_deviance_laps_on_display.text += f"Lap {fastest_lap.number}: {fastest_lap.title}Max Speed: %d kph
298 |Min Body Height: %d mm
""" % ( 299 | app.gt7comm.session.max_speed, 300 | app.gt7comm.session.min_body_height, 301 | ) 302 | 303 | def get_race_lines_layout(number_of_race_lines): 304 | """ 305 | This function returns the race lines layout. 306 | It returns a grid of 3x3 race lines. Red is braking. 307 | Green is throttling. 308 | """ 309 | i = 0 310 | race_line_diagrams = [] 311 | race_lines_data = [] 312 | 313 | sizing_mode = "scale_height" 314 | 315 | while i < number_of_race_lines: 316 | s_race_line, throttle_line, breaking_line, coasting_line, reference_throttle_line, reference_breaking_line, reference_coasting_line = gt7diagrams.get_throttle_braking_race_line_diagram() 317 | s_race_line.sizing_mode = sizing_mode 318 | race_line_diagrams.append(s_race_line) 319 | race_lines_data.append([throttle_line, breaking_line, coasting_line, reference_throttle_line, reference_breaking_line, reference_coasting_line]) 320 | i+=1 321 | 322 | l = layout(children=race_line_diagrams) 323 | l.sizing_mode = sizing_mode 324 | 325 | return l, race_line_diagrams, race_lines_data 326 | 327 | app = bokeh.application.Application 328 | 329 | # Share the gt7comm connection between sessions by storing them as an application attribute 330 | if not hasattr(app, "gt7comm"): 331 | playstation_ip = os.environ.get("GT7_PLAYSTATION_IP") 332 | load_laps_path = os.environ.get("GT7_LOAD_LAPS_PATH") 333 | 334 | if not playstation_ip: 335 | playstation_ip = "255.255.255.255" 336 | logger.info(f"No IP set in env var GT7_PLAYSTATION_IP using broadcast at {playstation_ip}") 337 | 338 | app.gt7comm = gt7communication.GT7Communication(playstation_ip) 339 | 340 | if load_laps_path: 341 | app.gt7comm.load_laps( 342 | load_laps_from_pickle(load_laps_path), replace_other_laps=True 343 | ) 344 | 345 | app.gt7comm.start() 346 | else: 347 | # Reuse existing thread 348 | if not app.gt7comm.is_connected(): 349 | logger.info("Restarting gt7communcation because of no connection") 350 | app.gt7comm.restart() 351 | else: 352 | # Existing thread has connection, proceed 353 | pass 354 | 355 | 356 | # def init_lap_times_source(): 357 | # global lap_times_source 358 | # lap_times_source.data = gt7helper.pd_data_frame_from_lap([], best_lap_time=app.gt7comm.session.last_lap) 359 | # 360 | # init_lap_times_source() 361 | 362 | g_laps_stored = [] 363 | g_session_stored = None 364 | g_connection_status_stored = None 365 | g_reference_lap_selected = None 366 | g_stored_fuel_map = None 367 | g_telemetry_update_needed = False 368 | 369 | stored_lap_files = gt7helper.bokeh_tuple_for_list_of_lapfiles( 370 | list_lap_files_from_path(os.path.join(os.getcwd(), "data")) 371 | ) 372 | 373 | race_diagram = gt7diagrams.RaceDiagram(width=1000) 374 | race_time_table = gt7diagrams.RaceTimeTable() 375 | colors = itertools.cycle(palette) 376 | 377 | 378 | def table_row_selection_callback(attrname, old, new): 379 | global g_laps_stored 380 | global race_diagram 381 | global race_time_table 382 | global colors 383 | 384 | selectionIndex=race_time_table.lap_times_source.selected.indices 385 | logger.info("you have selected the row nr "+str(selectionIndex)) 386 | 387 | colors = ["blue", "magenta", "green", "orange", "black", "purple"] 388 | # max_additional_laps = len(palette) 389 | colors_index = len(race_diagram.sources_additional_laps) + race_diagram.number_of_default_laps # which are the default colors 390 | 391 | for index in selectionIndex: 392 | if index >= len(colors): 393 | colors_index = 0 394 | 395 | # get element at index of iterator 396 | color = colors[colors_index] 397 | colors_index+=1 398 | lap_to_add = g_laps_stored[index] 399 | new_lap_data_source = race_diagram.add_lap_to_race_diagram(color, legend=g_laps_stored[index].title, visible=True) 400 | new_lap_data_source.data = lap_to_add.get_data_dict() 401 | 402 | 403 | race_time_table.lap_times_source.selected.on_change('indices', table_row_selection_callback) 404 | 405 | # Race line 406 | 407 | race_line_tooltips = [("index", "$index"), ("Breakpoint", "")] 408 | race_line_width = 250 409 | speed_diagram_width = 1200 410 | total_width = race_line_width + speed_diagram_width 411 | s_race_line = figure( 412 | title="Race Line", 413 | x_axis_label="x", 414 | y_axis_label="z", 415 | match_aspect=True, 416 | width=race_line_width, 417 | height=race_line_width, 418 | active_drag="box_zoom", 419 | tooltips=race_line_tooltips, 420 | ) 421 | 422 | # We set this to true, since maps appear flipped in the game 423 | # compared to their actual coordinates 424 | s_race_line.y_range.flipped = True 425 | 426 | s_race_line.toolbar.autohide = True 427 | 428 | last_lap_race_line = s_race_line.line( 429 | x="raceline_x", 430 | y="raceline_z", 431 | legend_label="Last Lap", 432 | line_width=1, 433 | color="blue", 434 | source=ColumnDataSource(data={"raceline_x": [], "raceline_z": []}) 435 | ) 436 | reference_lap_race_line = s_race_line.line( 437 | x="raceline_x", 438 | y="raceline_z", 439 | legend_label="Reference Lap", 440 | line_width=1, 441 | color="magenta", 442 | source=ColumnDataSource(data={"raceline_x": [], "raceline_z": []}) 443 | ) 444 | 445 | select_title = Paragraph(text="Load Laps:", align="center") 446 | select = Select(value="laps", options=stored_lap_files) 447 | select.on_change("value", load_laps_handler) 448 | 449 | reference_lap_select = Select(value="laps") 450 | reference_lap_select.on_change("value", load_reference_lap_handler) 451 | 452 | manual_log_button = Button(label="Log Lap Now") 453 | manual_log_button.on_click(log_lap_button_handler) 454 | 455 | save_button = Button(label="Save Laps") 456 | save_button.on_click(save_button_handler) 457 | 458 | reset_button = Button(label="Reset Laps") 459 | reset_button.on_click(reset_button_handler) 460 | 461 | div_tuning_info = Div(width=200, height=100) 462 | 463 | # div_last_lap = Div(width=200, height=125) 464 | # div_reference_lap = Div(width=200, height=125) 465 | div_speed_peak_valley_diagram = Div(width=200, height=125) 466 | div_gt7_dashboard = Div(width=120, height=30) 467 | div_header_line = Div(width=400, height=30) 468 | div_connection_info = Div(width=30, height=30) 469 | div_deviance_laps_on_display = Div(width=200, height=race_diagram.f_speed_variance.height) 470 | 471 | div_fuel_map = Div(width=200, height=125, css_classes=["fuel_map"]) 472 | 473 | div_gt7_dashboard.text = f"GT7 Dashboard" 474 | 475 | LABELS = ["Record Replays"] 476 | 477 | checkbox_group = CheckboxGroup(labels=LABELS, active=[1]) 478 | checkbox_group.on_change("active", always_record_checkbox_handler) 479 | 480 | race_time_table.t_lap_times.width=900 481 | 482 | l1 = layout( 483 | children=[ 484 | [get_help_div(gt7help.HEADER), div_connection_info, div_gt7_dashboard, div_header_line, reset_button, save_button, select_title, select, get_help_div(gt7help.LAP_CONTROLS)], 485 | [get_help_div(gt7help.TIME_DIFF), race_diagram.f_time_diff, layout(children=[manual_log_button, checkbox_group, reference_lap_select]), get_help_div(gt7help.MANUAL_CONTROLS)], 486 | [get_help_div(gt7help.SPEED_DIAGRAM), race_diagram.f_speed, s_race_line, get_help_div(gt7help.RACE_LINE_MINI)], 487 | [get_help_div(gt7help.SPEED_VARIANCE), race_diagram.f_speed_variance, div_deviance_laps_on_display, get_help_div(gt7help.SPEED_VARIANCE)], 488 | [get_help_div(gt7help.THROTTLE_DIAGRAM), race_diagram.f_throttle, div_speed_peak_valley_diagram, get_help_div(gt7help.SPEED_PEAKS_AND_VALLEYS)], 489 | [get_help_div(gt7help.YAW_RATE_DIAGRAM), race_diagram.f_yaw_rate], 490 | [get_help_div(gt7help.BRAKING_DIAGRAM), race_diagram.f_braking], 491 | [get_help_div(gt7help.COASTING_DIAGRAM), race_diagram.f_coasting], 492 | [get_help_div(gt7help.GEAR_DIAGRAM), race_diagram.f_gear], 493 | [get_help_div(gt7help.RPM_DIAGRAM), race_diagram.f_rpm], 494 | [get_help_div(gt7help.BOOST_DIAGRAM), race_diagram.f_boost], 495 | [get_help_div(gt7help.TIRE_DIAGRAM), race_diagram.f_tires], 496 | [get_help_div(gt7help.TIME_TABLE), race_time_table.t_lap_times, get_help_div(gt7help.FUEL_MAP), div_fuel_map, get_help_div(gt7help.TUNING_INFO), div_tuning_info], 497 | ] 498 | ) 499 | 500 | 501 | 502 | 503 | l2, race_lines, race_lines_data = get_race_lines_layout(number_of_race_lines=1) 504 | 505 | l3 = layout( 506 | [ 507 | [reset_button, save_button], 508 | [div_speed_peak_valley_diagram, div_fuel_map], # TODO Race table does not render twice, one rendering will be empty 509 | ], 510 | sizing_mode="stretch_width", 511 | ) 512 | 513 | # Setup the tabs 514 | tab1 = TabPanel(child=l1, title="Get Faster") 515 | tab2 = TabPanel(child=l2, title="Race Lines") 516 | tab3 = TabPanel(child=l3, title="Race") 517 | tabs = Tabs(tabs=[tab1, tab2, tab3]) 518 | 519 | curdoc().add_root(tabs) 520 | curdoc().title = "GT7 Dashboard" 521 | 522 | # This will only trigger once per lap, but we check every second if anything happened 523 | curdoc().add_periodic_callback(update_lap_change, 1000) 524 | curdoc().add_periodic_callback(update_fuel_map, 5000) 525 | -------------------------------------------------------------------------------- /gt7dashboard/gt7communication.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import logging 4 | import math 5 | import socket 6 | import struct 7 | import time 8 | import copy 9 | import traceback 10 | from datetime import timedelta 11 | from threading import Thread 12 | from typing import List 13 | 14 | from Crypto.Cipher import Salsa20 15 | 16 | from gt7dashboard.gt7helper import seconds_to_lap_time 17 | from gt7dashboard.gt7lap import Lap 18 | 19 | 20 | class GTData: 21 | def __init__(self, ddata): 22 | if not ddata: 23 | return 24 | 25 | self.package_id = struct.unpack('i', ddata[0x70:0x70 + 4])[0] 26 | self.best_lap = struct.unpack('i', ddata[0x78:0x78 + 4])[0] 27 | self.last_lap = struct.unpack('i', ddata[0x7C:0x7C + 4])[0] 28 | self.current_lap = struct.unpack('h', ddata[0x74:0x74 + 2])[0] 29 | self.current_gear = struct.unpack('B', ddata[0x90:0x90 + 1])[0] & 0b00001111 30 | self.suggested_gear = struct.unpack('B', ddata[0x90:0x90 + 1])[0] >> 4 31 | self.fuel_capacity = struct.unpack('f', ddata[0x48:0x48 + 4])[0] 32 | self.current_fuel = struct.unpack('f', ddata[0x44:0x44 + 4])[0] # fuel 33 | self.boost = struct.unpack('f', ddata[0x50:0x50 + 4])[0] - 1 34 | 35 | self.tyre_diameter_FL = struct.unpack('f', ddata[0xB4:0xB4 + 4])[0] 36 | self.tyre_diameter_FR = struct.unpack('f', ddata[0xB8:0xB8 + 4])[0] 37 | self.tyre_diameter_RL = struct.unpack('f', ddata[0xBC:0xBC + 4])[0] 38 | self.tyre_diameter_RR = struct.unpack('f', ddata[0xC0:0xC0 + 4])[0] 39 | 40 | self.type_speed_FL = abs(3.6 * self.tyre_diameter_FL * struct.unpack('f', ddata[0xA4:0xA4 + 4])[0]) 41 | self.type_speed_FR = abs(3.6 * self.tyre_diameter_FR * struct.unpack('f', ddata[0xA8:0xA8 + 4])[0]) 42 | self.type_speed_RL = abs(3.6 * self.tyre_diameter_RL * struct.unpack('f', ddata[0xAC:0xAC + 4])[0]) 43 | self.tyre_speed_RR = abs(3.6 * self.tyre_diameter_RR * struct.unpack('f', ddata[0xB0:0xB0 + 4])[0]) 44 | 45 | self.car_speed = 3.6 * struct.unpack('f', ddata[0x4C:0x4C + 4])[0] 46 | 47 | if self.car_speed > 0: 48 | self.tyre_slip_ratio_FL = '{:6.2f}'.format(self.type_speed_FL / self.car_speed) 49 | self.tyre_slip_ratio_FR = '{:6.2f}'.format(self.type_speed_FR / self.car_speed) 50 | self.tyre_slip_ratio_RL = '{:6.2f}'.format(self.type_speed_RL / self.car_speed) 51 | self.tyre_slip_ratio_RR = '{:6.2f}'.format(self.tyre_speed_RR / self.car_speed) 52 | 53 | self.time_on_track = timedelta( 54 | seconds=round(struct.unpack('i', ddata[0x80:0x80 + 4])[0] / 1000)) # time of day on track 55 | 56 | self.total_laps = struct.unpack('h', ddata[0x76:0x76 + 2])[0] # total laps 57 | 58 | self.current_position = struct.unpack('h', ddata[0x84:0x84 + 2])[0] # current position 59 | self.total_positions = struct.unpack('h', ddata[0x86:0x86 + 2])[0] # total positions 60 | 61 | self.car_id = struct.unpack('i', ddata[0x124:0x124 + 4])[0] # car id 62 | 63 | self.throttle = struct.unpack('B', ddata[0x91:0x91 + 1])[0] / 2.55 # throttle 64 | self.rpm = struct.unpack('f', ddata[0x3C:0x3C + 4])[0] # rpm 65 | self.rpm_rev_warning = struct.unpack('H', ddata[0x88:0x88 + 2])[0] # rpm rev warning 66 | 67 | self.brake = struct.unpack('B', ddata[0x92:0x92 + 1])[0] / 2.55 # brake 68 | 69 | self.boost = struct.unpack('f', ddata[0x50:0x50 + 4])[0] - 1 # boost 70 | 71 | self.rpm_rev_limiter = struct.unpack('H', ddata[0x8A:0x8A + 2])[0] # rpm rev limiter 72 | 73 | self.estimated_top_speed = struct.unpack('h', ddata[0x8C:0x8C + 2])[0] # estimated top speed 74 | 75 | self.clutch = struct.unpack('f', ddata[0xF4:0xF4 + 4])[0] # clutch 76 | self.clutch_engaged = struct.unpack('f', ddata[0xF8:0xF8 + 4])[0] # clutch engaged 77 | self.rpm_after_clutch = struct.unpack('f', ddata[0xFC:0xFC + 4])[0] # rpm after clutch 78 | 79 | self.oil_temp = struct.unpack('f', ddata[0x5C:0x5C + 4])[0] # oil temp 80 | self.water_temp = struct.unpack('f', ddata[0x58:0x58 + 4])[0] # water temp 81 | 82 | self.oil_pressure = struct.unpack('f', ddata[0x54:0x54 + 4])[0] # oil pressure 83 | self.ride_height = 1000 * struct.unpack('f', ddata[0x38:0x38 + 4])[0] # ride height 84 | 85 | self.tyre_temp_FL = struct.unpack('f', ddata[0x60:0x60 + 4])[0] # tyre temp FL 86 | self.tyre_temp_FR = struct.unpack('f', ddata[0x64:0x64 + 4])[0] # tyre temp FR 87 | 88 | self.suspension_fl = struct.unpack('f', ddata[0xC4:0xC4 + 4])[0] # suspension FL 89 | self.suspension_fr = struct.unpack('f', ddata[0xC8:0xC8 + 4])[0] # suspension FR 90 | 91 | self.tyre_temp_rl = struct.unpack('f', ddata[0x68:0x68 + 4])[0] # tyre temp RL 92 | self.tyre_temp_rr = struct.unpack('f', ddata[0x6C:0x6C + 4])[0] # tyre temp RR 93 | 94 | self.suspension_rl = struct.unpack('f', ddata[0xCC:0xCC + 4])[0] # suspension RL 95 | self.suspension_rr = struct.unpack('f', ddata[0xD0:0xD0 + 4])[0] # suspension RR 96 | 97 | self.gear_1 = struct.unpack('f', ddata[0x104:0x104 + 4])[0] # 1st gear 98 | self.gear_2 = struct.unpack('f', ddata[0x108:0x108 + 4])[0] # 2nd gear 99 | self.gear_3 = struct.unpack('f', ddata[0x10C:0x10C + 4])[0] # 3rd gear 100 | self.gear_4 = struct.unpack('f', ddata[0x110:0x110 + 4])[0] # 4th gear 101 | self.gear_5 = struct.unpack('f', ddata[0x114:0x114 + 4])[0] # 5th gear 102 | self.gear_6 = struct.unpack('f', ddata[0x118:0x118 + 4])[0] # 6th gear 103 | self.gear_7 = struct.unpack('f', ddata[0x11C:0x11C + 4])[0] # 7th gear 104 | self.gear_8 = struct.unpack('f', ddata[0x120:0x120 + 4])[0] # 8th gear 105 | 106 | # self.struct.unpack('f', ddata[0x100:0x100+4])[0] # ??? gear 107 | 108 | self.position_x = struct.unpack('f', ddata[0x04:0x04 + 4])[0] # pos X 109 | self.position_y = struct.unpack('f', ddata[0x08:0x08 + 4])[0] # pos Y 110 | self.position_z = struct.unpack('f', ddata[0x0C:0x0C + 4])[0] # pos Z 111 | 112 | self.velocity_x = struct.unpack('f', ddata[0x10:0x10 + 4])[0] # velocity X 113 | self.velocity_y = struct.unpack('f', ddata[0x14:0x14 + 4])[0] # velocity Y 114 | self.velocity_z = struct.unpack('f', ddata[0x18:0x18 + 4])[0] # velocity Z 115 | 116 | self.rotation_pitch = struct.unpack('f', ddata[0x1C:0x1C + 4])[0] # rot Pitch 117 | self.rotation_yaw = struct.unpack('f', ddata[0x20:0x20 + 4])[0] # rot Yaw 118 | self.rotation_roll = struct.unpack('f', ddata[0x24:0x24 + 4])[0] # rot Roll 119 | 120 | self.angular_velocity_x = struct.unpack('f', ddata[0x2C:0x2C + 4])[0] # angular velocity X 121 | self.angular_velocity_y = struct.unpack('f', ddata[0x30:0x30 + 4])[0] # angular velocity Y 122 | self.angular_velocity_z = struct.unpack('f', ddata[0x34:0x34 + 4])[0] # angular velocity Z 123 | 124 | self.is_paused = bin(struct.unpack('B', ddata[0x8E:0x8E + 1])[0])[-2] == '1' 125 | self.in_race = bin(struct.unpack('B', ddata[0x8E:0x8E + 1])[0])[-1] == '1' 126 | 127 | # struct.unpack('f', ddata[0x28:0x28+4])[0] # rot ??? 128 | 129 | # bin(struct.unpack('B', ddata[0x8E:0x8E+1])[0])[2:] # various flags (see https://github.com/Nenkai/PDTools/blob/master/PDTools.SimulatorInterface/SimulatorPacketG7S0.cs) 130 | # bin(struct.unpack('B', ddata[0x8F:0x8F+1])[0])[2:] # various flags (see https://github.com/Nenkai/PDTools/blob/master/PDTools.SimulatorInterface/SimulatorPacketG7S0.cs) 131 | # bin(struct.unpack('B', ddata[0x93:0x93+1])[0])[2:] # 0x93 = ??? 132 | 133 | # struct.unpack('f', ddata[0x94:0x94+4])[0] # 0x94 = ??? 134 | # struct.unpack('f', ddata[0x98:0x98+4])[0] # 0x98 = ??? 135 | # struct.unpack('f', ddata[0x9C:0x9C+4])[0] # 0x9C = ??? 136 | # struct.unpack('f', ddata[0xA0:0xA0+4])[0] # 0xA0 = ??? 137 | 138 | # struct.unpack('f', ddata[0xD4:0xD4+4])[0] # 0xD4 = ??? 139 | # struct.unpack('f', ddata[0xD8:0xD8+4])[0] # 0xD8 = ??? 140 | # struct.unpack('f', ddata[0xDC:0xDC+4])[0] # 0xDC = ??? 141 | # struct.unpack('f', ddata[0xE0:0xE0+4])[0] # 0xE0 = ??? 142 | 143 | # struct.unpack('f', ddata[0xE4:0xE4+4])[0] # 0xE4 = ??? 144 | # struct.unpack('f', ddata[0xE8:0xE8+4])[0] # 0xE8 = ??? 145 | # struct.unpack('f', ddata[0xEC:0xEC+4])[0] # 0xEC = ??? 146 | # struct.unpack('f', ddata[0xF0:0xF0+4])[0] # 0xF0 = ??? 147 | 148 | def to_json(self): 149 | return json.dumps(self, indent=4, sort_keys=True, default=str) 150 | 151 | class Session(): 152 | def __init__(self): 153 | # best lap overall 154 | self.special_packet_time = 0 155 | self.best_lap=-1 156 | self.min_body_height=1000000 # deliberate high number to be counted down 157 | self.max_speed=0 158 | 159 | def __eq__(self, other): 160 | return other is not None and self.best_lap == other.best_lap and self.min_body_height == other.min_body_height and self.max_speed == other.max_speed 161 | 162 | class GT7Communication(Thread): 163 | def __init__(self, playstation_ip): 164 | # Thread control 165 | Thread.__init__(self) 166 | self._shall_run = True 167 | self._shall_restart = False 168 | # True will always quit with the main process 169 | self.daemon = True 170 | 171 | # Set lap callback function as none 172 | self.lap_callback_function = None 173 | 174 | self.playstation_ip = playstation_ip 175 | self.send_port = 33739 176 | self.receive_port = 33740 177 | self._last_time_data_received = 0 178 | 179 | self.current_lap = Lap() 180 | self.session = Session() 181 | self.laps = [] 182 | self.last_data = GTData(None) 183 | 184 | # This is used to record race data in any case. This will override the "in_race" flag. 185 | # When recording data. Useful when recording replays. 186 | self.always_record_data = False 187 | 188 | 189 | def stop(self): 190 | self._shall_run = False 191 | def run(self): 192 | while self._shall_run: 193 | s = None 194 | try: 195 | self._shall_restart = False 196 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 197 | 198 | if self.playstation_ip == "255.255.255.255": 199 | s.setsockopt (socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 200 | 201 | s.bind(('0.0.0.0', self.receive_port)) 202 | self._send_hb(s) 203 | s.settimeout(10) 204 | previous_lap = -1 205 | package_id = 0 206 | package_nr = 0 207 | while not self._shall_restart and self._shall_run: 208 | try: 209 | data, address = s.recvfrom(4096) 210 | package_nr = package_nr + 1 211 | ddata = salsa20_dec(data) 212 | if len(ddata) > 0 and struct.unpack('i', ddata[0x70:0x70 + 4])[0] > package_id: 213 | 214 | self.last_data = GTData(ddata) 215 | self._last_time_data_received = time.time() 216 | 217 | package_id = struct.unpack('i', ddata[0x70:0x70 + 4])[0] 218 | 219 | bstlap = struct.unpack('i', ddata[0x78:0x78 + 4])[0] 220 | lstlap = struct.unpack('i', ddata[0x7C:0x7C + 4])[0] 221 | curlap = struct.unpack('h', ddata[0x74:0x74 + 2])[0] 222 | 223 | if curlap == 0: 224 | self.session.special_packet_time = 0 225 | 226 | if curlap > 0 and (self.last_data.in_race or self.always_record_data): 227 | 228 | if curlap != previous_lap: 229 | # New lap 230 | previous_lap = curlap 231 | 232 | self.session.special_packet_time += lstlap - self.current_lap.lap_ticks * 1000.0 / 60.0 233 | self.session.best_lap = bstlap 234 | 235 | self.finish_lap() 236 | 237 | else: 238 | curLapTime = 0 239 | # Reset lap 240 | self.current_lap = Lap() 241 | 242 | self._log_data(self.last_data) 243 | 244 | if package_nr > 100: 245 | self._send_hb(s) 246 | package_nr = 0 247 | except (OSError, TimeoutError) as e: 248 | # Handler for package exceptions 249 | self._send_hb(s) 250 | package_nr = 0 251 | # Reset package id for new connections 252 | package_id = 0 253 | 254 | except Exception as e: 255 | # Handler for general socket exceptions 256 | # TODO logging not working 257 | print("Error while connecting to %s:%d: %s" % (self.playstation_ip, self.send_port, e)) 258 | s.close() 259 | # Wait before reconnect 260 | time.sleep(5) 261 | 262 | def restart(self): 263 | self._shall_restart = True 264 | 265 | def is_connected(self) -> bool: 266 | return self._last_time_data_received > 0 and (time.time() - self._last_time_data_received) <= 1 267 | 268 | def _send_hb(self, s): 269 | send_data = 'A' 270 | s.sendto(send_data.encode('utf-8'), (self.playstation_ip, self.send_port)) 271 | 272 | def get_last_data(self) -> GTData: 273 | timeout = time.time() + 5 # 5 seconds timeout 274 | while True: 275 | 276 | if self.last_data is not None: 277 | return self.last_data 278 | 279 | if time.time() > timeout: 280 | break 281 | 282 | def get_laps(self) -> List[Lap]: 283 | return self.laps 284 | 285 | def load_laps(self, laps: List[Lap], to_last_position = False, to_first_position = False, replace_other_laps = False): 286 | if to_last_position: 287 | self.laps = self.laps + laps 288 | elif to_first_position: 289 | self.laps = laps + self.laps 290 | elif replace_other_laps: 291 | self.laps = laps 292 | 293 | def _log_data(self, data): 294 | 295 | if not (data.in_race or self.always_record_data): 296 | return 297 | 298 | if data.is_paused: 299 | return 300 | 301 | if data.ride_height < self.session.min_body_height: 302 | self.session.min_body_height = data.ride_height 303 | 304 | if data.car_speed > self.session.max_speed: 305 | self.session.max_speed = data.car_speed 306 | 307 | if data.throttle == 100: 308 | self.current_lap.full_throttle_ticks += 1 309 | 310 | if data.brake == 100: 311 | self.current_lap.full_brake_ticks += 1 312 | 313 | if data.brake == 0 and data.throttle == 0: 314 | self.current_lap.no_throttle_and_no_brake_ticks += 1 315 | self.current_lap.data_coasting.append(1) 316 | else: 317 | self.current_lap.data_coasting.append(0) 318 | 319 | if data.brake > 0 and data.throttle > 0: 320 | self.current_lap.throttle_and_brake_ticks += 1 321 | 322 | self.current_lap.lap_ticks += 1 323 | 324 | if data.tyre_temp_FL > 100 or data.tyre_temp_FR > 100 or data.tyre_temp_rl > 100 or data.tyre_temp_rr > 100: 325 | self.current_lap.tires_overheated_ticks += 1 326 | 327 | self.current_lap.data_braking.append(data.brake) 328 | self.current_lap.data_throttle.append(data.throttle) 329 | self.current_lap.data_speed.append(data.car_speed) 330 | 331 | delta_divisor = data.car_speed 332 | if data.car_speed == 0: 333 | delta_divisor = 1 334 | 335 | delta_fl = data.type_speed_FL / delta_divisor 336 | delta_fr = data.type_speed_FR / delta_divisor 337 | delta_rl = data.type_speed_RL / delta_divisor 338 | delta_rr = data.type_speed_FR / delta_divisor 339 | 340 | if delta_fl > 1.1 or delta_fr > 1.1 or delta_rl > 1.1 or delta_rr > 1.1: 341 | self.current_lap.tires_spinning_ticks += 1 342 | 343 | self.current_lap.data_tires.append(delta_fl + delta_fr + delta_rl + delta_rr) 344 | 345 | ## RPM and shifting 346 | 347 | self.current_lap.data_rpm.append(data.rpm) 348 | self.current_lap.data_gear.append(data.current_gear) 349 | 350 | ## Log Position 351 | 352 | self.current_lap.data_position_x.append(data.position_x) 353 | self.current_lap.data_position_y.append(data.position_y) 354 | self.current_lap.data_position_z.append(data.position_z) 355 | 356 | ## Log Boost 357 | 358 | self.current_lap.data_boost.append(data.boost) 359 | 360 | ## Log Yaw Rate 361 | 362 | # This is the interval to collection yaw rate 363 | interval = 1 * 60 # 1 second has 60 fps and 60 data ticks 364 | self.current_lap.data_rotation_yaw.append(data.rotation_yaw) 365 | 366 | # Collect yaw rate, skip first interval with all zeroes 367 | if len(self.current_lap.data_rotation_yaw) > interval: 368 | yaw_rate_per_second = data.rotation_yaw - self.current_lap.data_rotation_yaw[-interval] 369 | else: 370 | yaw_rate_per_second = 0 371 | 372 | self.current_lap.data_absolute_yaw_rate_per_second.append(abs(yaw_rate_per_second)) 373 | 374 | # Adapted from https://www.gtplanet.net/forum/threads/gt7-is-compatible-with-motion-rig.410728/post-13810797 375 | self.current_lap.lap_live_time = (self.current_lap.lap_ticks * 1. / 60.) - (self.session.special_packet_time / 1000.) 376 | 377 | self.current_lap.data_time.append(self.current_lap.lap_live_time) 378 | 379 | self.current_lap.car_id = data.car_id 380 | 381 | def finish_lap(self, manual=False): 382 | """ 383 | Finishes a lap with info we only know after crossing the line after each lap 384 | """ 385 | 386 | if manual: 387 | # Manual laps have no time assigned, so take current live time as lap finish time. 388 | # Finish time is tracked in seconds while live time is tracked in ms 389 | self.current_lap.lap_finish_time = self.current_lap.lap_live_time * 1000 390 | else: 391 | # Regular finished laps (crossing the finish line in races or time trials) 392 | # have their lap time stored in last_lap 393 | self.current_lap.lap_finish_time = self.last_data.last_lap 394 | 395 | # Track recording meta data 396 | self.current_lap.is_replay = self.always_record_data 397 | self.current_lap.is_manual = manual 398 | 399 | self.current_lap.fuel_at_end = self.last_data.current_fuel 400 | self.current_lap.fuel_consumed = self.current_lap.fuel_at_start - self.current_lap.fuel_at_end 401 | self.current_lap.lap_finish_time = self.current_lap.lap_finish_time 402 | self.current_lap.total_laps = self.last_data.total_laps 403 | self.current_lap.title = seconds_to_lap_time(self.current_lap.lap_finish_time / 1000) 404 | self.current_lap.car_id = self.last_data.car_id 405 | self.current_lap.number = self.last_data.current_lap - 1 # Is not counting the same way as the in-game timetable 406 | # TODO Proper pythonic name 407 | self.current_lap.EstimatedTopSpeed = self.last_data.estimated_top_speed 408 | 409 | self.current_lap.lap_end_timestamp = datetime.datetime.now() 410 | 411 | # Race is not in 0th lap, which is before starting the race. 412 | # We will only persist those laps that have crossed the starting line at least once 413 | # And those laps which have data for speed logged. This will prevent empty laps. 414 | # TODO Correct this comment, this is about Laptime not lap numbers 415 | if self.current_lap.lap_finish_time > 0 and len(self.current_lap.data_speed) > 0: 416 | self.laps.insert(0, self.current_lap) 417 | 418 | # Make a copy of this lap and call the callback function if set 419 | if self.lap_callback_function: 420 | self.lap_callback_function(copy.deepcopy(self.current_lap)) 421 | 422 | # Reset current lap with an empty one 423 | self.current_lap = Lap() 424 | self.current_lap.fuel_at_start = self.last_data.current_fuel 425 | 426 | 427 | def reset(self): 428 | """ 429 | Resets the current lap, all stored laps and the current session. 430 | """ 431 | self.current_lap = Lap() 432 | self.session = Session() 433 | self.last_data = GTData(None) 434 | self.laps = [] 435 | 436 | def set_lap_callback(self, new_lap_callback): 437 | self.lap_callback_function = new_lap_callback 438 | 439 | 440 | # data stream decoding 441 | def salsa20_dec(dat): 442 | key = b'Simulator Interface Packet GT7 ver 0.0' 443 | # Seed IV is always located here 444 | oiv = dat[0x40:0x44] 445 | iv1 = int.from_bytes(oiv, byteorder='little') 446 | # Notice DEADBEAF, not DEADBEEF 447 | iv2 = iv1 ^ 0xDEADBEAF 448 | iv = bytearray() 449 | iv.extend(iv2.to_bytes(4, 'little')) 450 | iv.extend(iv1.to_bytes(4, 'little')) 451 | cipher = Salsa20.new(key[0:32], bytes(iv)) 452 | ddata = cipher.decrypt(dat) 453 | magic = int.from_bytes(ddata[0:4], byteorder='little') 454 | if magic != 0x47375330: 455 | return bytearray(b'') 456 | return ddata 457 | -------------------------------------------------------------------------------- /gt7dashboard/gt7helper.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import itertools 3 | import json 4 | import logging 5 | import os 6 | import pickle 7 | import statistics 8 | import sys 9 | from datetime import datetime, timezone 10 | from pathlib import Path 11 | from statistics import StatisticsError 12 | from typing import Tuple, List 13 | 14 | import pandas as pd 15 | from pandas import DataFrame 16 | from scipy.signal import find_peaks 17 | from tabulate import tabulate 18 | 19 | from gt7dashboard.gt7lap import Lap 20 | from gt7dashboard import gt7helper 21 | 22 | 23 | def calculate_remaining_fuel( 24 | fuel_start_lap: int, fuel_end_lap: int, lap_time: int 25 | ) -> Tuple[int, float, float]: 26 | # no fuel consumed 27 | if fuel_start_lap == fuel_end_lap: 28 | return 0, -1, -1 29 | 30 | # fuel consumed, calculate 31 | fuel_consumed_per_lap = fuel_start_lap - fuel_end_lap 32 | laps_remaining = fuel_end_lap / fuel_consumed_per_lap 33 | time_remaining = laps_remaining * lap_time 34 | 35 | return fuel_consumed_per_lap, laps_remaining, time_remaining 36 | 37 | 38 | def get_x_axis_for_distance(lap: Lap) -> List: 39 | x_axis = [] 40 | tick_time = 16.668 # https://www.gtplanet.net/forum/threads/gt7-is-compatible-with-motion-rig.410728/post-13806131 41 | for i, s in enumerate(lap.data_speed): 42 | # distance traveled + (Speed in km/h / 3.6 / 1000 = mm / ms) * tick_time 43 | if i == 0: 44 | x_axis.append(0) 45 | continue 46 | 47 | x_axis.append(x_axis[i - 1] + (lap.data_speed[i] / 3.6 / 1000) * tick_time) 48 | 49 | return x_axis 50 | 51 | 52 | def get_x_axis_depending_on_mode(lap: Lap, distance_mode: bool): 53 | if distance_mode: 54 | # Calculate distance for x axis 55 | return get_x_axis_for_distance(lap) 56 | else: 57 | # Use ticks as length, which is the length of any given data list 58 | return list(range(len(lap.data_speed))) 59 | pass 60 | 61 | 62 | def get_time_delta_dataframe_for_lap(lap: Lap, name: str) -> DataFrame: 63 | lap_distance = get_x_axis_for_distance(lap) 64 | lap_time = lap.data_time 65 | 66 | # Multiply to match datatype which is nanoseconds? 67 | lap_time_ms = [convert_seconds_to_milliseconds(item) for item in lap_time] 68 | 69 | series = pd.Series( 70 | lap_distance, index=pd.TimedeltaIndex(data=lap_time_ms, unit="ms") 71 | ) 72 | 73 | upsample = series.resample("10ms").asfreq() 74 | interpolated_upsample = upsample.interpolate() 75 | 76 | # Make distance to index and time to value, because we want to join on distance 77 | inverted = pd.Series( 78 | interpolated_upsample.index.values, index=interpolated_upsample 79 | ) 80 | 81 | # Flip around, we have to convert timedelta back to integer to do this 82 | s1 = pd.Series(inverted.values.astype("int64"), name=name, index=inverted.index) 83 | 84 | df1 = DataFrame(data=s1) 85 | # returns a dataframe where index is distance travelled and first data field is time passed 86 | return df1 87 | 88 | 89 | def calculate_time_diff_by_distance( 90 | reference_lap: Lap, comparison_lap: Lap 91 | ) -> DataFrame: 92 | df1 = get_time_delta_dataframe_for_lap(reference_lap, "reference") 93 | df2 = get_time_delta_dataframe_for_lap(comparison_lap, "comparison") 94 | 95 | df = df1.join(df2, how="outer").sort_index().interpolate() 96 | 97 | # After interpolation, we can make the index a normal field and rename it 98 | df.reset_index(inplace=True) 99 | df = df.rename(columns={"index": "distance"}) 100 | 101 | # Convert integer timestamps back to timestamp format 102 | s_reference_timestamped = pd.to_timedelta(getattr(df, "reference")) 103 | s_comparison_timestamped = pd.to_timedelta(getattr(df, "comparison")) 104 | 105 | df["reference"] = s_reference_timestamped 106 | df["comparison"] = s_comparison_timestamped 107 | 108 | df["timedelta"] = df["comparison"] - df["reference"] 109 | return df 110 | 111 | 112 | def mark_if_matches_highest_or_lowest( 113 | value: float, highest: List[int], lowest: List[int], order: int, high_is_best=True 114 | ) -> str: 115 | green = 32 116 | red = 31 117 | reset = 0 118 | 119 | high = green 120 | low = red 121 | 122 | if not high_is_best: 123 | low = green 124 | high = red 125 | 126 | if value == highest[order]: 127 | return "\x1b[1;%dm%0.f\x1b[1;%dm" % (high, value, reset) 128 | 129 | if value == lowest[order]: 130 | return "\x1b[1;%dm%0.f\x1b[1;%dm" % (low, value, reset) 131 | 132 | return "%0.f" % value 133 | 134 | 135 | def format_laps_to_table(laps: List[Lap], best_lap: float) -> str: 136 | highest = [0, 0, 0, 0, 0] 137 | lowest = [999999, 999999, 999999, 999999, 999999] 138 | 139 | # Display lap times 140 | table = [] 141 | for idx, lap in enumerate(laps): 142 | lap_color = 39 # normal color 143 | time_diff = "" 144 | 145 | if best_lap == lap.lap_finish_time: 146 | lap_color = 35 # magenta 147 | elif lap.lap_finish_time < best_lap: 148 | # lap_finish_time cannot be smaller than last_lap, last_lap is always the smallest. 149 | # This can only mean that lap.lap_finish_time is from an earlier race on a different track 150 | time_diff = "-" 151 | elif best_lap > 0: 152 | time_diff = seconds_to_lap_time(-1 * (best_lap / 1000 - lap.lap_finish_time / 1000)) 153 | 154 | ft_ticks = lap.full_throttle_ticks / lap.lap_ticks * 1000 155 | tb_ticks = lap.throttle_and_brake_ticks / lap.lap_ticks * 1000 156 | fb_ticks = lap.full_brake_ticks / lap.lap_ticks * 1000 157 | nt_ticks = lap.no_throttle_and_no_brake_ticks / lap.lap_ticks * 1000 158 | ti_ticks = lap.tires_spinning_ticks / lap.lap_ticks * 1000 159 | 160 | list_of_ticks = [ft_ticks, tb_ticks, fb_ticks, nt_ticks, ti_ticks] 161 | 162 | for i, value in enumerate(list_of_ticks): 163 | if list_of_ticks[i] > highest[i]: 164 | highest[i] = list_of_ticks[i] 165 | 166 | if list_of_ticks[i] <= lowest[i]: 167 | lowest[i] = list_of_ticks[i] 168 | 169 | table.append( 170 | [ 171 | # number 172 | "\x1b[1;%dm%d" % (lap_color, lap.number), 173 | # Timing 174 | seconds_to_lap_time(lap.lap_finish_time / 1000), 175 | time_diff, 176 | lap.fuel_at_end, 177 | lap.fuel_consumed, 178 | # Ticks 179 | ft_ticks, 180 | tb_ticks, 181 | fb_ticks, 182 | nt_ticks, 183 | ti_ticks, 184 | ] 185 | ) 186 | 187 | for i, entry in enumerate(table): 188 | for k, val in enumerate(table[i]): 189 | if k == 5: 190 | table[i][k] = mark_if_matches_highest_or_lowest( 191 | table[i][k], highest, lowest, 0, high_is_best=True 192 | ) 193 | elif k == 6: 194 | table[i][k] = mark_if_matches_highest_or_lowest( 195 | table[i][k], highest, lowest, 1, high_is_best=False 196 | ) 197 | elif k == 7: 198 | table[i][k] = mark_if_matches_highest_or_lowest( 199 | table[i][k], highest, lowest, 2, high_is_best=True 200 | ) 201 | elif k == 8: 202 | table[i][k] = mark_if_matches_highest_or_lowest( 203 | table[i][k], highest, lowest, 3, high_is_best=False 204 | ) 205 | elif k == 9: 206 | table[i][k] = mark_if_matches_highest_or_lowest( 207 | table[i][k], highest, lowest, 4, high_is_best=False 208 | ) 209 | 210 | return tabulate( 211 | table, 212 | headers=["#", "Time", "Diff", "Fuel", "FuCo", "fT", "T+B", "fB", "0T", "Spin"], 213 | floatfmt=".0f", 214 | ) 215 | 216 | 217 | def convert_seconds_to_milliseconds(seconds: int): 218 | minutes = seconds // 60 219 | remaining = seconds % 60 220 | 221 | return minutes * 60000 + remaining * 1000 222 | 223 | 224 | def seconds_to_lap_time(seconds): 225 | prefix = "" 226 | if seconds < 0: 227 | prefix = "-" 228 | seconds *= -1 229 | 230 | minutes = seconds // 60 231 | remaining = seconds % 60 232 | return prefix + "{:01.0f}:{:06.3f}".format(minutes, remaining) 233 | 234 | 235 | def find_speed_peaks_and_valleys( 236 | lap: Lap, width: int = 100 237 | ) -> tuple[list[int], list[int]]: 238 | inv_data_speed = [i * -1 for i in lap.data_speed] 239 | peaks, whatisthis = find_peaks(lap.data_speed, width=width) 240 | valleys, whatisthis = find_peaks(inv_data_speed, width=width) 241 | return list(peaks), list(valleys) 242 | 243 | 244 | def get_speed_peaks_and_valleys(lap: Lap): 245 | peaks, valleys = find_speed_peaks_and_valleys(lap, width=100) 246 | 247 | peak_speed_data_x = [] 248 | peak_speed_data_y = [] 249 | 250 | valley_speed_data_x = [] 251 | valley_speed_data_y = [] 252 | 253 | for p in peaks: 254 | peak_speed_data_x.append(lap.data_speed[p]) 255 | peak_speed_data_y.append(p) 256 | 257 | for v in valleys: 258 | valley_speed_data_x.append(lap.data_speed[v]) 259 | valley_speed_data_y.append(v) 260 | 261 | return ( 262 | peak_speed_data_x, 263 | peak_speed_data_y, 264 | valley_speed_data_x, 265 | valley_speed_data_y, 266 | ) 267 | 268 | 269 | def none_ignoring_median(data): 270 | """Return the median (middle value) of numeric data but ignore None values. 271 | 272 | When the number of data points is odd, return the middle data point. 273 | When the number of data points is even, the median is interpolated by 274 | taking the average of the two middle values: 275 | 276 | >>> none_ignoring_median([1, 3, None, 5]) 277 | 3 278 | >>> none_ignoring_median([1, 3, 5, None, 7]) 279 | 4.0 280 | 281 | """ 282 | # FIXME improve me 283 | filtered_data = [] 284 | for d in data: 285 | if d is not None: 286 | filtered_data.append(d) 287 | filtered_data = sorted(filtered_data) 288 | n = len(filtered_data) 289 | if n == 0: 290 | raise StatisticsError("no median for empty data") 291 | if n % 2 == 1: 292 | return filtered_data[n // 2] 293 | else: 294 | i = n // 2 295 | return (filtered_data[i - 1] + filtered_data[i]) / 2 296 | 297 | 298 | class LapFile: 299 | def __init__(self): 300 | self.name = None 301 | self.path = None 302 | self.size = None 303 | 304 | def __str__(self): 305 | return "%s - %s" % (self.name, human_readable_size(self.size, decimal_places=0)) 306 | 307 | 308 | def list_lap_files_from_path(root: str): 309 | lap_files = [] 310 | for path, sub_dirs, files in os.walk(root): 311 | for name in files: 312 | if name.endswith(".json"): 313 | lf = LapFile() 314 | lf.name = name 315 | lf.path = os.path.join(path, name) 316 | lf.size = os.path.getsize(lf.path) 317 | lap_files.append(lf) 318 | 319 | lap_files.sort(key=lambda x: x.path, reverse=True) 320 | return lap_files 321 | 322 | 323 | def load_laps_from_pickle(path: str) -> List[Lap]: 324 | with open(path, "rb") as f: 325 | return pickle.load(f) 326 | 327 | 328 | def load_laps_from_json(json_file): 329 | with open(json_file, 'r') as file: 330 | data = json.load(file) 331 | 332 | laps = [] 333 | for lap_data in data: 334 | lap = Lap() 335 | lap.__dict__.update(lap_data) 336 | for key, value in lap_data.items(): 337 | if key.endswith('_timestamp') and isinstance(value, str): 338 | value = datetime.fromisoformat(value) 339 | setattr(lap, key, value) 340 | laps.append(lap) 341 | 342 | return laps 343 | 344 | def save_laps_to_pickle(laps: List[Lap]) -> str: 345 | storage_folder = "data" 346 | local_timezone = datetime.now(timezone.utc).astimezone().tzinfo 347 | dt = datetime.now(tz=local_timezone) 348 | str_date_time = dt.strftime("%Y-%m-%d_%H_%M_%S") 349 | storage_filename = "%s_%s.laps" % (str_date_time, get_safe_filename(laps[0].car_name())) 350 | Path(storage_folder).mkdir(parents=True, exist_ok=True) 351 | 352 | path = os.path.join(os.getcwd(), storage_folder, storage_filename) 353 | 354 | with open(path, "wb") as f: 355 | pickle.dump(laps, f) 356 | 357 | return path 358 | 359 | def save_laps_to_json(laps: List[Lap]) -> str: 360 | 361 | storage_folder = "data" 362 | local_timezone = datetime.now(timezone.utc).astimezone().tzinfo 363 | dt = datetime.now(tz=local_timezone) 364 | str_date_time = dt.strftime("%Y-%m-%d_%H_%M_%S") 365 | storage_filename = "%s_%s.json" % (str_date_time, get_safe_filename(laps[0].car_name())) 366 | Path(storage_folder).mkdir(parents=True, exist_ok=True) 367 | 368 | path = os.path.join(os.getcwd(), storage_folder, storage_filename) 369 | 370 | with open(path, "w") as f: 371 | json.dump([ob.__dict__ for ob in laps], f, default=str) 372 | 373 | return path 374 | 375 | 376 | def get_safe_filename(unsafe_filename: str) -> str: 377 | return "".join(x for x in unsafe_filename if x.isalnum() or x in "._- ").replace(" ", "_") 378 | 379 | 380 | def human_readable_size(size, decimal_places=3): 381 | for unit in ["B", "KB", "MB", "GB", "TB"]: 382 | if size < 1024.0: 383 | break 384 | size /= 1024.0 385 | return f"{size:.{decimal_places}f} {unit}" 386 | 387 | 388 | def get_last_reference_median_lap( 389 | laps: List[Lap], reference_lap_selected: Lap 390 | ) -> Tuple[Lap, Lap, Lap]: 391 | last_lap = None 392 | reference_lap = None 393 | median_lap = None 394 | 395 | if len(laps) > 0: # Only show last lap 396 | last_lap = laps[0] 397 | 398 | if len(laps) >= 2 and not reference_lap_selected: 399 | reference_lap = get_best_lap(laps) 400 | 401 | if len(laps) >= 3: 402 | median_lap = get_median_lap(laps) 403 | 404 | if reference_lap_selected: 405 | reference_lap = reference_lap_selected 406 | 407 | return last_lap, reference_lap, median_lap 408 | 409 | 410 | def get_best_lap(laps: List[Lap]): 411 | if len(laps) == 0: 412 | return None 413 | 414 | return sorted(laps, key=lambda x: x.lap_finish_time, reverse=False)[0] 415 | 416 | 417 | def get_median_lap(laps: List[Lap]) -> Lap: 418 | if len(laps) == 0: 419 | raise Exception("Lap list does not contain any laps") 420 | 421 | # Filter out too long laps, like box laps etc. use 10 Seconds of the best lap as a threshold 422 | best_lap = get_best_lap(laps) 423 | ten_seconds = 10000 424 | laps = filter_max_min_laps( 425 | laps, best_lap.lap_finish_time + ten_seconds, best_lap.lap_finish_time - ten_seconds 426 | ) 427 | 428 | median_lap = Lap() 429 | if len(laps) == 0: 430 | return median_lap 431 | 432 | for val in vars(laps[0]): 433 | attributes = [] 434 | for lap in laps: 435 | if val == "options": 436 | continue 437 | attr = getattr(lap, val) 438 | # FIXME why is it sometimes string AND int? 439 | if not isinstance(attr, str) and attr != "" and attr != []: 440 | attributes.append(getattr(lap, val)) 441 | 442 | if len(attributes) == 0: 443 | continue 444 | if isinstance(getattr(laps[0], val), datetime): 445 | continue 446 | 447 | if isinstance(getattr(laps[0], val), list): 448 | median_attribute = [ 449 | none_ignoring_median(k) 450 | for k in itertools.zip_longest(*attributes, fillvalue=None) 451 | ] 452 | else: 453 | median_attribute = statistics.median(attributes) 454 | setattr(median_lap, val, median_attribute) 455 | 456 | median_lap.title = "Median (%d Laps): %s" % ( 457 | len(laps), 458 | seconds_to_lap_time(median_lap.lap_finish_time / 1000), 459 | ) 460 | 461 | return median_lap 462 | 463 | 464 | def get_brake_points(lap): 465 | x = [] 466 | y = [] 467 | for i, b in enumerate(lap.data_braking): 468 | if i > 0: 469 | if lap.data_braking[i - 1] == 0 and lap.data_braking[i] > 0: 470 | x.append(lap.data_position_x[i]) 471 | y.append(lap.data_position_z[i]) 472 | 473 | return x, y 474 | 475 | 476 | def filter_max_min_laps(laps: List[Lap], max_lap_time=-1, min_lap_time=-1) -> List[Lap]: 477 | if max_lap_time > 0: 478 | laps = list(filter(lambda l: l.lap_finish_time <= max_lap_time, laps)) 479 | 480 | if min_lap_time > 0: 481 | laps = list(filter(lambda l: l.lap_finish_time >= min_lap_time, laps)) 482 | 483 | return laps 484 | 485 | 486 | def pd_data_frame_from_lap( 487 | laps: List[Lap], best_lap_time: int 488 | ) -> pd.DataFrame: 489 | df = pd.DataFrame() 490 | for i, lap in enumerate(laps): 491 | time_diff = "" 492 | info = "" 493 | 494 | if lap.is_replay: 495 | info += "Replay" 496 | 497 | if best_lap_time == lap.lap_finish_time: 498 | # lap_color = 35 # magenta 499 | # TODO add some formatting 500 | pass 501 | elif lap.lap_finish_time < best_lap_time: 502 | # lap_finish_time cannot be smaller than last_lap, last_lap is always the smallest. 503 | # This can only mean that lap.lap_finish_time is from an earlier race on a different track 504 | time_diff = "-" 505 | elif best_lap_time > 0: 506 | time_diff = "+" + seconds_to_lap_time( 507 | -1 * (best_lap_time / 1000 - lap.lap_finish_time / 1000) 508 | ) 509 | 510 | df_add = pd.DataFrame( 511 | [ 512 | { 513 | "number": lap.number, 514 | "time": seconds_to_lap_time(lap.lap_finish_time / 1000), 515 | "diff": time_diff, 516 | "timestamp": lap.lap_start_timestamp.strftime('%Y-%m-%d %H:%M:%S'), 517 | "info": info, 518 | "car_name": lap.car_name(), 519 | "fuelconsumed": "%d" % lap.fuel_consumed, 520 | "fullthrottle": "%d" 521 | % (lap.full_throttle_ticks / lap.lap_ticks * 1000), 522 | "throttleandbreak": "%d" 523 | % (lap.throttle_and_brake_ticks / lap.lap_ticks * 1000), 524 | "fullbreak": "%d" % (lap.full_brake_ticks / lap.lap_ticks * 1000), 525 | "nothrottle": "%d" 526 | % (lap.no_throttle_and_no_brake_ticks / lap.lap_ticks * 1000), 527 | "tyrespinning": "%d" 528 | % (lap.tires_spinning_ticks / lap.lap_ticks * 1000), 529 | } 530 | ], 531 | index=[i], 532 | ) 533 | df = pd.concat([df, df_add]) 534 | 535 | return df 536 | 537 | 538 | RACE_LINE_BRAKING_MODE = "RACE_LINE_BRAKING_MODE" 539 | RACE_LINE_THROTTLE_MODE = "RACE_LINE_THROTTLE_MODE" 540 | RACE_LINE_COASTING_MODE = "RACE_LINE_COASTING_MODE" 541 | 542 | 543 | def get_race_line_coordinates_when_mode_is_active(lap: Lap, mode: str): 544 | return_y = [] 545 | return_x = [] 546 | return_z = [] 547 | 548 | for i, _ in enumerate(lap.data_braking): 549 | 550 | if mode == RACE_LINE_BRAKING_MODE: 551 | 552 | if lap.data_braking[i] > lap.data_throttle[i]: 553 | return_y.append(lap.data_position_y[i]) 554 | return_x.append(lap.data_position_x[i]) 555 | return_z.append(lap.data_position_z[i]) 556 | else: 557 | return_y.append("NaN") 558 | return_x.append("NaN") 559 | return_z.append("NaN") 560 | 561 | elif mode == RACE_LINE_THROTTLE_MODE: 562 | 563 | if lap.data_braking[i] < lap.data_throttle[i]: 564 | return_y.append(lap.data_position_y[i]) 565 | return_x.append(lap.data_position_x[i]) 566 | return_z.append(lap.data_position_z[i]) 567 | else: 568 | return_y.append("NaN") 569 | return_x.append("NaN") 570 | return_z.append("NaN") 571 | 572 | if mode == RACE_LINE_COASTING_MODE: 573 | 574 | if lap.data_braking[i] == 0 and lap.data_throttle[i] == 0: 575 | return_y.append(lap.data_position_y[i]) 576 | return_x.append(lap.data_position_x[i]) 577 | return_z.append(lap.data_position_z[i]) 578 | else: 579 | return_y.append("NaN") 580 | return_x.append("NaN") 581 | return_z.append("NaN") 582 | 583 | return return_y, return_x, return_z 584 | 585 | 586 | CARS_CSV_FILENAME = "db/cars.csv" 587 | 588 | 589 | def get_car_name_for_car_id(car_id: int) -> str: 590 | # check if variable is int 591 | if not isinstance(car_id, int): 592 | raise ValueError("car_id must be an integer") 593 | 594 | # check if file exists 595 | if not os.path.isfile(CARS_CSV_FILENAME): 596 | logging.info("Could not find file %s" % CARS_CSV_FILENAME) 597 | return "CAR-ID-%d" % car_id 598 | 599 | # read csv from file 600 | with open(CARS_CSV_FILENAME, 'r') as csv_file: 601 | csv_reader = csv.reader(csv_file, delimiter=',') 602 | for row in csv_reader: 603 | if row[0] == str(car_id): 604 | return row[1] 605 | 606 | return "CAR-ID-%d" % car_id 607 | 608 | 609 | def bokeh_tuple_for_list_of_lapfiles(lapfiles: List[LapFile]): 610 | tuples = [""] # Use empty first option which is default 611 | for lapfile in lapfiles: 612 | tuples.append(tuple((lapfile.path, lapfile.__str__()))) 613 | return tuples 614 | 615 | 616 | def bokeh_tuple_for_list_of_laps(laps: List[Lap]): 617 | tuples = [] 618 | for i, lap in enumerate(laps): 619 | tuples.append(tuple((str(i), lap.format()))) 620 | return tuples 621 | 622 | 623 | class FuelMap: 624 | """A Fuel Map with calculated attributes of the fuel setting 625 | 626 | Attributes: 627 | fuel_consumed_per_lap The amount of fuel consumed per lap with this fuel map 628 | """ 629 | 630 | def __init__(self, mixture_setting, power_percentage, consumption_percentage): 631 | """ 632 | Create a Fuel Map that is relative to the base setting 633 | 634 | :param mixture_setting: Mixture Setting of the Fuel Map 635 | :param power_percentage: Percentage of available power to the car relative to the base setting 636 | :param consumption_percentage: Percentage of fuel consumption relative to the base setting 637 | """ 638 | self.mixture_setting = mixture_setting 639 | self.power_percentage = power_percentage 640 | self.consumption_percentage = consumption_percentage 641 | 642 | self.fuel_consumed_per_lap = 0 643 | self.laps_remaining_on_current_fuel = 0 644 | self.time_remaining_on_current_fuel = 0 645 | self.lap_time_diff = 0 646 | self.lap_time_expected = 0 647 | 648 | def __str__(self): 649 | return "%d\t\t %d%%\t\t\t %d%%\t%d\t%.1f\t%s\t%s" % ( 650 | self.mixture_setting, 651 | self.power_percentage * 100, 652 | self.consumption_percentage * 100, 653 | self.fuel_consumed_per_lap, 654 | self.laps_remaining_on_current_fuel, 655 | seconds_to_lap_time(self.time_remaining_on_current_fuel / 1000), 656 | seconds_to_lap_time(self.lap_time_diff / 1000), 657 | ) 658 | 659 | 660 | def get_fuel_on_consumption_by_relative_fuel_levels(lap: Lap) -> List[FuelMap]: 661 | # Relative Setting, Laps to Go, Time to Go, Assumed Diff in Lap Times 662 | fuel_consumed_per_lap, laps_remaining, time_remaining = calculate_remaining_fuel( 663 | lap.fuel_at_start, lap.fuel_at_end, lap.lap_finish_time 664 | ) 665 | i = -5 666 | 667 | # Source: 668 | # https://www.gtplanet.net/forum/threads/test-results-fuel-mixture-settings-and-other-fuel-saving-techniques.369387/ 669 | fuel_consumption_per_level_change = 8 670 | power_per_level_change = 4 671 | 672 | relative_fuel_maps = [] 673 | 674 | while i <= 5: 675 | relative_fuel_map = FuelMap( 676 | mixture_setting=i, 677 | power_percentage=(100 - i * power_per_level_change) / 100, 678 | consumption_percentage=(100 - i * fuel_consumption_per_level_change) / 100, 679 | ) 680 | 681 | relative_fuel_map.fuel_consumed_per_lap = fuel_consumed_per_lap * relative_fuel_map.consumption_percentage 682 | relative_fuel_map.laps_remaining_on_current_fuel = laps_remaining + laps_remaining * ( 683 | 1 - relative_fuel_map.consumption_percentage 684 | ) 685 | 686 | relative_fuel_map.time_remaining_on_current_fuel = time_remaining + time_remaining * ( 687 | 1 - relative_fuel_map.consumption_percentage 688 | ) 689 | relative_fuel_map.lap_time_diff = lap.lap_finish_time * (1 - relative_fuel_map.power_percentage) 690 | relative_fuel_map.lap_time_expected = lap.lap_finish_time + relative_fuel_map.lap_time_diff 691 | 692 | relative_fuel_maps.append(relative_fuel_map) 693 | i += 1 694 | 695 | return relative_fuel_maps 696 | 697 | 698 | def get_n_fastest_laps_within_percent_threshold_ignoring_replays(laps: List[Lap], number_of_laps: int, 699 | percent_threshold: float): 700 | # FIXME Replace later with this line 701 | # filtered_laps = [lap for lap in laps if not lap.is_replay] 702 | filtered_laps = [lap for lap in laps if not (len(lap.data_speed) == 0 or lap.is_replay)] 703 | 704 | if len(filtered_laps) == 0: 705 | return [] 706 | 707 | # sort laps by finish time 708 | filtered_laps.sort(key=lambda lap: lap.lap_finish_time) 709 | fastest_lap = filtered_laps[0] 710 | threshold_laps = [lap for lap in filtered_laps if 711 | lap.lap_finish_time <= fastest_lap.lap_finish_time * (1 + percent_threshold)] 712 | return threshold_laps[:number_of_laps] 713 | 714 | 715 | DEFAULT_FASTEST_LAPS_PERCENT_THRESHOLD = 0.05 716 | def get_variance_for_fastest_laps(laps: List[Lap], number_of_laps: int = 3, percent_threshold: float = DEFAULT_FASTEST_LAPS_PERCENT_THRESHOLD) -> (DataFrame, list[Lap]): 717 | fastest_laps: list[Lap] = get_n_fastest_laps_within_percent_threshold_ignoring_replays(laps, number_of_laps, percent_threshold) 718 | variance: DataFrame = get_variance_for_laps(fastest_laps) 719 | return variance, fastest_laps 720 | 721 | 722 | def get_variance_for_laps(laps: List[Lap]) -> DataFrame: 723 | 724 | dataframe_distance_columns = [] 725 | merged_df = pd.DataFrame(columns=['distance']) 726 | for lap in laps: 727 | d = {'speed': lap.data_speed, 'distance' : gt7helper.get_x_axis_for_distance(lap)} 728 | df = pd.DataFrame(data=d) 729 | dataframe_distance_columns.append(df) 730 | merged_df = pd.merge(merged_df, df, on='distance', how='outer') 731 | 732 | merged_df = merged_df.sort_values(by='distance') 733 | merged_df = merged_df.set_index('distance') 734 | 735 | # Interpolate missing values 736 | merged_df = merged_df.interpolate() 737 | dbs_df = merged_df.std(axis=1).abs() 738 | dbs_df = dbs_df.reset_index().rename(columns={'index': 'distance'}) 739 | dbs_df.columns = ["distance", "speed_variance"] 740 | 741 | return dbs_df 742 | 743 | PEAK = "PEAK" 744 | VALLEY = "VALLEY" 745 | 746 | def get_peaks_and_valleys_sorted_tuple_list(lap: Lap): 747 | ( 748 | peak_speed_data_x, 749 | peak_speed_data_y, 750 | valley_speed_data_x, 751 | valley_speed_data_y, 752 | ) = lap.get_speed_peaks_and_valleys() 753 | 754 | tuple_list = [] 755 | 756 | tuple_list += zip(peak_speed_data_x, peak_speed_data_y, [PEAK]*len(peak_speed_data_x)) 757 | tuple_list += zip(valley_speed_data_x, valley_speed_data_y, [VALLEY]*len(valley_speed_data_x)) 758 | 759 | tuple_list.sort(key=lambda a: a[1]) 760 | 761 | return tuple_list 762 | 763 | 764 | def calculate_laps_left_on_fuel(current_lap, last_lap) -> float: 765 | # TODO Like F1: A) Benzingemisch -0.72 Bunden 766 | laps_left: float 767 | fuel_consumed_last_lap = last_lap.fuel_at_start - last_lap.fuel_at_end 768 | laps_left = current_lap.fuel - (last_lap.laps_to_go * fuel_consumed_last_lap) 769 | -------------------------------------------------------------------------------- /gt7dashboard/gt7diagrams.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import bokeh 4 | from bokeh.layouts import layout 5 | from bokeh.models import ColumnDataSource, Label, Scatter, Column, Line, TableColumn, DataTable, Range1d 6 | from bokeh.plotting import figure 7 | 8 | from gt7dashboard import gt7helper 9 | from gt7dashboard.gt7lap import Lap 10 | 11 | 12 | def get_throttle_braking_race_line_diagram(): 13 | # TODO Make this work, tooltips just show breakpoint 14 | race_line_tooltips = [("index", "$index")] 15 | s_race_line = figure( 16 | title="Race Line", 17 | match_aspect=True, 18 | active_scroll="wheel_zoom", 19 | tooltips=race_line_tooltips, 20 | ) 21 | 22 | # We set this to true, since maps appear flipped in the game 23 | # compared to their actual coordinates 24 | s_race_line.y_range.flipped = True 25 | 26 | s_race_line.toolbar.autohide = True 27 | 28 | s_race_line.axis.visible = False 29 | s_race_line.xgrid.visible = False 30 | s_race_line.ygrid.visible = False 31 | 32 | throttle_line = s_race_line.line( 33 | x="raceline_x_throttle", 34 | y="raceline_z_throttle", 35 | legend_label="Throttle Last Lap", 36 | line_width=5, 37 | color="green", 38 | source=ColumnDataSource( 39 | data={"raceline_z_throttle": [], "raceline_x_throttle": []} 40 | ), 41 | ) 42 | breaking_line = s_race_line.line( 43 | x="raceline_x_braking", 44 | y="raceline_z_braking", 45 | legend_label="Braking Last Lap", 46 | line_width=5, 47 | color="red", 48 | source=ColumnDataSource( 49 | data={"raceline_z_braking": [], "raceline_x_braking": []} 50 | ), 51 | ) 52 | 53 | coasting_line = s_race_line.line( 54 | x="raceline_x_coasting", 55 | y="raceline_z_coasting", 56 | legend_label="Coasting Last Lap", 57 | line_width=5, 58 | color="blue", 59 | source=ColumnDataSource( 60 | data={"raceline_z_coasting": [], "raceline_x_coasting": []} 61 | ), 62 | ) 63 | 64 | # Reference Lap 65 | 66 | reference_throttle_line = s_race_line.line( 67 | x="raceline_x_throttle", 68 | y="raceline_z_throttle", 69 | legend_label="Throttle Reference", 70 | line_width=15, 71 | alpha=0.3, 72 | color="green", 73 | source=ColumnDataSource( 74 | data={"raceline_z_throttle": [], "raceline_x_throttle": []} 75 | ), 76 | ) 77 | reference_breaking_line = s_race_line.line( 78 | x="raceline_x_braking", 79 | y="raceline_z_braking", 80 | legend_label="Braking Reference", 81 | line_width=15, 82 | alpha=0.3, 83 | color="red", 84 | source=ColumnDataSource( 85 | data={"raceline_z_braking": [], "raceline_x_braking": []} 86 | ), 87 | ) 88 | 89 | reference_coasting_line = s_race_line.line( 90 | x="raceline_x_coasting", 91 | y="raceline_z_coasting", 92 | legend_label="Coasting Reference", 93 | line_width=15, 94 | alpha=0.3, 95 | color="blue", 96 | source=ColumnDataSource( 97 | data={"raceline_z_coasting": [], "raceline_x_coasting": []} 98 | ), 99 | ) 100 | 101 | s_race_line.legend.visible = True 102 | 103 | s_race_line.add_layout(s_race_line.legend[0], "right") 104 | 105 | s_race_line.legend.click_policy = "hide" 106 | 107 | return ( 108 | s_race_line, 109 | throttle_line, 110 | breaking_line, 111 | coasting_line, 112 | reference_throttle_line, 113 | reference_breaking_line, 114 | reference_coasting_line, 115 | ) 116 | 117 | class RaceTimeTable(object): 118 | 119 | def __init__(self): 120 | 121 | self.columns = [ 122 | TableColumn(field="number", title="#"), 123 | TableColumn(field="time", title="Time"), 124 | TableColumn(field="diff", title="Diff"), 125 | TableColumn(field="timestamp", title="Timestamp"), 126 | TableColumn(field="info", title="Info"), 127 | TableColumn(field="fuelconsumed", title="Fuel Cons."), 128 | TableColumn(field="fullthrottle", title="Full Throt."), 129 | TableColumn(field="fullbreak", title="Full Brake"), 130 | TableColumn(field="nothrottle", title="Coast"), 131 | TableColumn(field="tyrespinning", title="Tire Spin"), 132 | TableColumn(field="car_name", title="Car"), 133 | ] 134 | 135 | self.lap_times_source = ColumnDataSource( 136 | gt7helper.pd_data_frame_from_lap([], best_lap_time=0) 137 | ) 138 | self.t_lap_times: DataTable 139 | 140 | self.t_lap_times = DataTable( 141 | source=self.lap_times_source, columns=self.columns, index_position=None, css_classes=["lap_times_table"] 142 | ) 143 | # This will lead to not being rendered 144 | # self.t_lap_times.autosize_mode = "fit_columns" 145 | # Maybe this is related: https://github.com/bokeh/bokeh/issues/10512 ? 146 | 147 | 148 | def show_laps(self, laps: List[Lap]): 149 | best_lap = gt7helper.get_best_lap(laps) 150 | if best_lap == None: 151 | return 152 | 153 | new_df = gt7helper.pd_data_frame_from_lap(laps, best_lap_time=best_lap.lap_finish_time) 154 | self.lap_times_source.data = ColumnDataSource.from_df(new_df) 155 | 156 | class RaceDiagram(object): 157 | def __init__(self, width=400): 158 | """ 159 | Returns figures for time-diff, speed, throttling, braking and coasting. 160 | All with lines for last lap, best lap and median lap. 161 | The last return value is the sources object, that has to be altered 162 | to display data. 163 | """ 164 | 165 | self.speed_lines = [] 166 | self.braking_lines = [] 167 | self.coasting_lines = [] 168 | self.throttle_lines = [] 169 | self.tires_lines = [] 170 | self.rpm_lines = [] 171 | self.gears_lines = [] 172 | self.boost_lines = [] 173 | self.yaw_rate_lines = [] 174 | 175 | # Data Sources 176 | self.source_time_diff = None 177 | self.source_speed_variance = None 178 | self.source_last_lap = None 179 | self.source_reference_lap = None 180 | self.source_median_lap = None 181 | self.sources_additional_laps = [] 182 | 183 | self.additional_laps = List[Lap] 184 | 185 | # This is the number of default laps, 186 | # last lap, best lap and median lap 187 | self.number_of_default_laps = 3 188 | 189 | 190 | tooltips = [ 191 | ("index", "$index"), 192 | ("value", "$y"), 193 | ("Speed", "@speed{0}"), 194 | ("Yaw Rate", "@yaw_rate{0.00}"), 195 | ("Throttle", "@throttle%"), 196 | ("Brake", "@brake%"), 197 | ("Coast", "@coast%"), 198 | ("Gear", "@gear"), 199 | ("Rev", "@rpm{0} RPM"), 200 | ("Distance", "@distance{0} m"), 201 | ("Boost", "@boost{0.00} x 100 kPa"), 202 | ] 203 | 204 | tooltips_timedelta = [ 205 | ("index", "$index"), 206 | ("timedelta", "@timedelta{0} ms"), 207 | ("reference", "@reference{0} ms"), 208 | ("comparison", "@comparison{0} ms"), 209 | ] 210 | 211 | self.tooltips_speed_variance = [ 212 | ("index", "$index"), 213 | ("Distance", "@distance{0} m"), 214 | ("Spd. Deviation", "@speed_variance{0}"), 215 | ] 216 | 217 | self.f_speed = figure( 218 | title="Last, Reference, Median", 219 | y_axis_label="Speed", 220 | width=width, 221 | height=250, 222 | tooltips=tooltips, 223 | active_drag="box_zoom", 224 | ) 225 | 226 | self.f_speed_variance = figure( 227 | y_axis_label="Spd.Dev.", 228 | x_range=self.f_speed.x_range, 229 | y_range=Range1d(0, 50), 230 | width=width, 231 | height=int(self.f_speed.height / 4), 232 | tooltips=self.tooltips_speed_variance, 233 | active_drag="box_zoom", 234 | ) 235 | 236 | self.f_time_diff = figure( 237 | title="Time Diff - Last, Reference", 238 | x_range=self.f_speed.x_range, 239 | y_axis_label="Time / Diff", 240 | width=width, 241 | height=int(self.f_speed.height / 2), 242 | tooltips=tooltips_timedelta, 243 | active_drag="box_zoom", 244 | ) 245 | 246 | self.f_throttle = figure( 247 | x_range=self.f_speed.x_range, 248 | y_axis_label="Throttle", 249 | width=width, 250 | height=int(self.f_speed.height / 2), 251 | tooltips=tooltips, 252 | active_drag="box_zoom", 253 | ) 254 | self.f_braking = figure( 255 | x_range=self.f_speed.x_range, 256 | y_axis_label="Braking", 257 | width=width, 258 | height=int(self.f_speed.height / 2), 259 | tooltips=tooltips, 260 | active_drag="box_zoom", 261 | ) 262 | 263 | self.f_coasting = figure( 264 | x_range=self.f_speed.x_range, 265 | y_axis_label="Coasting", 266 | width=width, 267 | height=int(self.f_speed.height / 2), 268 | tooltips=tooltips, 269 | active_drag="box_zoom", 270 | ) 271 | 272 | self.f_tires = figure( 273 | x_range=self.f_speed.x_range, 274 | y_axis_label="Tire Spd / Car Spd", 275 | width=width, 276 | height=int(self.f_speed.height / 2), 277 | tooltips=tooltips, 278 | active_drag="box_zoom", 279 | ) 280 | 281 | self.f_rpm = figure( 282 | x_range=self.f_speed.x_range, 283 | y_axis_label="RPM", 284 | width=width, 285 | height=int(self.f_speed.height / 2), 286 | tooltips=tooltips, 287 | active_drag="box_zoom", 288 | ) 289 | 290 | self.f_gear = figure( 291 | x_range=self.f_speed.x_range, 292 | y_axis_label="Gear", 293 | width=width, 294 | height=int(self.f_speed.height / 2), 295 | tooltips=tooltips, 296 | active_drag="box_zoom", 297 | ) 298 | 299 | self.f_boost = figure( 300 | x_range=self.f_speed.x_range, 301 | y_axis_label="Boost", 302 | width=width, 303 | height=int(self.f_speed.height / 2), 304 | tooltips=tooltips, 305 | active_drag="box_zoom", 306 | ) 307 | 308 | self.f_yaw_rate = figure( 309 | x_range=self.f_speed.x_range, 310 | y_axis_label="Yaw Rate / Second", 311 | width=width, 312 | height=int(self.f_speed.height / 2), 313 | tooltips=tooltips, 314 | active_drag="box_zoom", 315 | ) 316 | 317 | self.f_speed.toolbar.autohide = True 318 | 319 | span_zero_time_diff = bokeh.models.Span( 320 | location=0, 321 | dimension="width", 322 | line_color="black", 323 | line_dash="dashed", 324 | line_width=1, 325 | ) 326 | self.f_time_diff.add_layout(span_zero_time_diff) 327 | 328 | self.f_time_diff.toolbar.autohide = True 329 | 330 | self.f_speed_variance.xaxis.visible = False 331 | self.f_speed_variance.toolbar.autohide = True 332 | 333 | self.f_throttle.xaxis.visible = False 334 | self.f_throttle.toolbar.autohide = True 335 | 336 | self.f_braking.xaxis.visible = False 337 | self.f_braking.toolbar.autohide = True 338 | 339 | self.f_coasting.xaxis.visible = False 340 | self.f_coasting.toolbar.autohide = True 341 | 342 | self.f_tires.xaxis.visible = False 343 | self.f_tires.toolbar.autohide = True 344 | 345 | self.f_gear.xaxis.visible = False 346 | self.f_gear.toolbar.autohide = True 347 | 348 | self.f_rpm.xaxis.visible = False 349 | self.f_rpm.toolbar.autohide = True 350 | 351 | self.f_boost.xaxis.visible = False 352 | self.f_boost.toolbar.autohide = True 353 | 354 | self.f_yaw_rate.xaxis.visible = False 355 | self.f_yaw_rate.toolbar.autohide = True 356 | 357 | self.source_time_diff = ColumnDataSource(data={"distance": [], "timedelta": []}) 358 | self.f_time_diff.line( 359 | x="distance", 360 | y="timedelta", 361 | source=self.source_time_diff, 362 | line_width=1, 363 | color="blue", 364 | line_alpha=1, 365 | ) 366 | 367 | self.source_last_lap = self.add_lap_to_race_diagram("blue", "Last Lap", True) 368 | 369 | self.source_reference_lap = self.add_lap_to_race_diagram("magenta", "Reference Lap", True) 370 | 371 | self.source_median_lap = self.add_lap_to_race_diagram("green", "Median Lap", False) 372 | 373 | self.f_speed.legend.click_policy = "hide" 374 | self.f_throttle.legend.click_policy = self.f_speed.legend.click_policy 375 | self.f_braking.legend.click_policy = self.f_speed.legend.click_policy 376 | self.f_coasting.legend.click_policy = self.f_speed.legend.click_policy 377 | self.f_tires.legend.click_policy = self.f_speed.legend.click_policy 378 | self.f_gear.legend.click_policy = self.f_speed.legend.click_policy 379 | self.f_rpm.legend.click_policy = self.f_speed.legend.click_policy 380 | self.f_boost.legend.click_policy = self.f_speed.legend.click_policy 381 | self.f_yaw_rate.legend.click_policy = self.f_speed.legend.click_policy 382 | 383 | # Leave padding on the left because rpm is 4 digits and diagrams will not start at the same position otherwise 384 | min_border_left = 60 385 | self.f_time_diff.min_border_left = min_border_left 386 | self.f_speed.min_border_left = min_border_left 387 | self.f_throttle.min_border_left = min_border_left 388 | self.f_braking.min_border_left = min_border_left 389 | self.f_coasting.min_border_left = min_border_left 390 | self.f_tires.min_border_left = min_border_left 391 | self.f_gear.min_border_left = min_border_left 392 | self.f_rpm.min_border_left = min_border_left 393 | self.f_speed_variance.min_border_left = min_border_left 394 | self.f_boost.min_border_left = min_border_left 395 | self.f_yaw_rate.min_border_left = min_border_left 396 | 397 | self.layout = layout(self.f_time_diff, self.f_speed, self.f_speed_variance, self.f_throttle, self.f_yaw_rate, self.f_braking, self.f_coasting, self.f_tires, self.f_gear, self.f_rpm, self.f_boost) 398 | 399 | self.source_speed_variance = ColumnDataSource(data={"distance": [], "speed_variance": []}) 400 | 401 | self.f_speed_variance.line( 402 | x="distance", 403 | y="speed_variance", 404 | source=self.source_speed_variance, 405 | line_width=1, 406 | color="gray", 407 | line_alpha=1, 408 | visible=True 409 | ) 410 | 411 | def add_additional_lap_to_race_diagram(self, color: str, lap: Lap, visible: bool = True): 412 | source = self.add_lap_to_race_diagram(color, lap.title, visible) 413 | source.data = lap.get_data_dict() 414 | self.sources_additional_laps.append(source) 415 | 416 | def update_fastest_laps_variance(self, laps): 417 | # FIXME, many many data points, mayabe reduce by the amount of laps? 418 | variance, fastest_laps = gt7helper.get_variance_for_fastest_laps(laps) 419 | self.source_speed_variance.data = variance 420 | return fastest_laps 421 | 422 | def add_lap_to_race_diagram(self, color: str, legend: str, visible: bool = True): 423 | 424 | # Set empty data for avoiding warnings about missing columns 425 | dummy_data = Lap().get_data_dict() 426 | 427 | source = ColumnDataSource(data=dummy_data) 428 | 429 | self.speed_lines.append(self.f_speed.line( 430 | x="distance", 431 | y="speed", 432 | source=source, 433 | legend_label=legend, 434 | line_width=1, 435 | color=color, 436 | line_alpha=1, 437 | visible=visible 438 | )) 439 | 440 | self.throttle_lines.append(self.f_throttle.line( 441 | x="distance", 442 | y="throttle", 443 | source=source, 444 | legend_label=legend, 445 | line_width=1, 446 | color=color, 447 | line_alpha=1, 448 | visible=visible 449 | )) 450 | 451 | self.braking_lines.append(self.f_braking.line( 452 | x="distance", 453 | y="brake", 454 | source=source, 455 | legend_label=legend, 456 | line_width=1, 457 | color=color, 458 | line_alpha=1, 459 | visible=visible 460 | )) 461 | 462 | self.coasting_lines.append(self.f_coasting.line( 463 | x="distance", 464 | y="coast", 465 | source=source, 466 | legend_label=legend, 467 | line_width=1, 468 | color=color, 469 | line_alpha=1, 470 | visible=visible 471 | )) 472 | 473 | self.tires_lines.append(self.f_tires.line( 474 | x="distance", 475 | y="tires", 476 | source=source, 477 | legend_label=legend, 478 | line_width=1, 479 | color=color, 480 | line_alpha=1, 481 | visible=visible 482 | )) 483 | 484 | self.gears_lines.append(self.f_gear.line( 485 | x="distance", 486 | y="gear", 487 | source=source, 488 | legend_label=legend, 489 | line_width=1, 490 | color=color, 491 | line_alpha=1, 492 | visible=visible 493 | )) 494 | 495 | self.rpm_lines.append(self.f_rpm.line( 496 | x="distance", 497 | y="rpm", 498 | source=source, 499 | legend_label=legend, 500 | line_width=1, 501 | color=color, 502 | line_alpha=1, 503 | visible=visible 504 | )) 505 | 506 | self.boost_lines.append(self.f_boost.line( 507 | x="distance", 508 | y="boost", 509 | source=source, 510 | legend_label=legend, 511 | line_width=1, 512 | color=color, 513 | line_alpha=1, 514 | visible=visible 515 | )) 516 | 517 | self.yaw_rate_lines.append(self.f_yaw_rate.line( 518 | x="distance", 519 | y="yaw_rate", 520 | source=source, 521 | legend_label=legend, 522 | line_width=1, 523 | color=color, 524 | line_alpha=1, 525 | visible=visible 526 | )) 527 | 528 | return source 529 | 530 | def get_layout(self) -> Column: 531 | return self.layout 532 | 533 | def delete_all_additional_laps(self): 534 | # Delete all but first three in list 535 | self.sources_additional_laps = [] 536 | 537 | for i, _ in enumerate(self.f_speed.renderers): 538 | if i >= self.number_of_default_laps: 539 | self.f_speed.renderers.remove(self.f_speed.renderers[i]) # remove the line renderer 540 | self.f_throttle.renderers.remove(self.f_throttle.renderers[i]) # remove the line renderer 541 | self.f_braking.renderers.remove(self.f_braking.renderers[i]) # remove the line renderer 542 | self.f_coasting.renderers.remove(self.f_coasting.renderers[i]) # remove the line renderer 543 | self.f_tires.renderers.remove(self.f_tires.renderers[i]) # remove the line renderer 544 | self.f_boost.renderers.remove(self.f_boost.renderers[i]) # remove the line renderer 545 | self.f_yaw_rate.renderers.remove(self.f_yaw_rate.renderers[i]) # remove the line renderer 546 | # self.f_time_diff.renderers.remove(self.f_time_diff.renderers[i]) # remove the line renderer 547 | 548 | self.f_speed.legend.items.pop(i) 549 | self.f_throttle.legend.items.pop(i) 550 | self.f_braking.legend.items.pop(i) 551 | self.f_coasting.legend.items.pop(i) 552 | self.f_tires.legend.items.pop(i) 553 | self.f_yaw_rate.legend.items.pop(i) 554 | self.f_boost.legend.items.pop(i) 555 | # self.f_time_diff.legend.items.pop(i) 556 | 557 | 558 | 559 | 560 | 561 | 562 | def add_annotations_to_race_line( 563 | race_line: figure, last_lap: Lap, reference_lap: Lap 564 | ): 565 | """ Adds annotations such as speed peaks and valleys and the starting line to the racing line""" 566 | 567 | remove_all_annotation_text_from_figure(race_line) 568 | 569 | decorations = [] 570 | decorations.extend( 571 | _add_peaks_and_valley_decorations_for_lap( 572 | last_lap, race_line, color="blue", offset=0 573 | ) 574 | ) 575 | decorations.extend( 576 | _add_peaks_and_valley_decorations_for_lap( 577 | reference_lap, race_line, color="magenta", offset=0 578 | ) 579 | ) 580 | add_starting_line_to_diagram(race_line, last_lap) 581 | 582 | # This is multiple times faster by adding all texts at once rather than adding them above 583 | # With around 20 positions, this took 27s before. 584 | # Maybe this has something to do with every text being transmitted over network 585 | race_line.center.extend(decorations) 586 | 587 | # Add peaks and valleys of last lap 588 | 589 | 590 | def _add_peaks_and_valley_decorations_for_lap( 591 | lap: Lap, race_line: figure, color, offset 592 | ): 593 | ( 594 | peak_speed_data_x, 595 | peak_speed_data_y, 596 | valley_speed_data_x, 597 | valley_speed_data_y, 598 | ) = lap.get_speed_peaks_and_valleys() 599 | 600 | decorations = [] 601 | 602 | for i in range(len(peak_speed_data_x)): 603 | # shift 10 px to the left 604 | position_x = lap.data_position_x[peak_speed_data_y[i]] 605 | position_y = lap.data_position_z[peak_speed_data_y[i]] 606 | 607 | mytext = Label( 608 | x=position_x, 609 | y=position_y, 610 | text_color=color, 611 | text_font_size="10pt", 612 | text_font_style="bold", 613 | x_offset=offset, 614 | background_fill_color="white", 615 | background_fill_alpha=0.75, 616 | ) 617 | mytext.text = "▴%.0f" % peak_speed_data_x[i] 618 | 619 | decorations.append(mytext) 620 | 621 | for i in range(len(valley_speed_data_x)): 622 | position_x = lap.data_position_x[valley_speed_data_y[i]] 623 | position_y = lap.data_position_z[valley_speed_data_y[i]] 624 | 625 | mytext = Label( 626 | x=position_x, 627 | y=position_y, 628 | text_color=color, 629 | text_font_size="10pt", 630 | x_offset=offset, 631 | text_font_style="bold", 632 | background_fill_color="white", 633 | background_fill_alpha=0.75, 634 | text_align="right", 635 | ) 636 | mytext.text = "%.0f▾" % valley_speed_data_x[i] 637 | 638 | decorations.append(mytext) 639 | 640 | return decorations 641 | 642 | 643 | def remove_all_annotation_text_from_figure(f: figure): 644 | f.center = [r for r in f.center if not isinstance(r, Label)] 645 | 646 | 647 | def get_fuel_map_html_table(last_lap: Lap) -> str: 648 | """ 649 | Returns a html table of relative fuel map. 650 | :param last_lap: 651 | :return: html table 652 | """ 653 | 654 | fuel_maps = gt7helper.get_fuel_on_consumption_by_relative_fuel_levels(last_lap) 655 | table = ( 656 | "| Fuel Lvl. | " 658 | "Fuel Cons. | " 659 | "Laps Rem. | " 660 | "Time Rem. | " 661 | "Time Diff |
|---|---|---|---|---|
| %d | " 671 | "%d | " 672 | "%.1f | " 673 | "%s | " 674 | "%s | " 675 | "
Fuel Remaining: %d
" % last_lap.fuel_at_end 690 | return table 691 | 692 | 693 | def add_starting_line_to_diagram(race_line: figure, last_lap: Lap): 694 | 695 | if len(last_lap.data_position_z) == 0: 696 | return 697 | 698 | x = last_lap.data_position_x[0] 699 | y = last_lap.data_position_z[0] 700 | 701 | # We use a text because scatters are too memory consuming 702 | # and cannot be easily removed from the diagram 703 | mytext = Label( 704 | x=x, 705 | y=y, 706 | text_font_size="10pt", 707 | text_font_style="bold", 708 | background_fill_color="white", 709 | background_fill_alpha=0.25, 710 | text_align="center", 711 | ) 712 | mytext.text = "====" 713 | race_line.center.append(mytext) 714 | 715 | def get_speed_peak_and_valley_diagram(last_lap: Lap, reference_lap: Lap) -> str: 716 | """ 717 | Returns a html div with the speed peaks and valleys of the last lap and the reference lap 718 | as a formatted html table 719 | :param last_lap: Lap 720 | :param reference_lap: Lap 721 | :return: html table with peaks and valleys 722 | """ 723 | table = """| ' 746 | table += ' | %s - %s | ' % ("Last", last_lap.title) 747 | table += '%s - %s | ' % ("Ref.", reference_lap.title) 748 | table += 'Diff | ' 749 | 750 | table += '|||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| # | Pos. | Speed | 754 |# | Pos. | Speed | 755 |Pos. | Speed | 756 ||||||
| █ | ' 783 | 784 | if len(ll_tuple_list) > i: 785 | table += f"""{i+1} | 786 |{"S" if ll_tuple_list[i][2] == gt7helper.PEAK else "T"} | 787 |{ll_tuple_list[i][1]:d} | 788 |{ll_tuple_list[i][0]:.0f} | 789 | """ 790 | 791 | if len(rl_tuple_list) > i: 792 | table += f"""{i+1} | 793 |{"S" if rl_tuple_list[i][2] == gt7helper.PEAK else "T"} | 794 |{rl_tuple_list[i][1]:d} | 795 |{rl_tuple_list[i][0]:.0f} | 796 | """ 797 | 798 | if rl_and_ll_are_same_size: 799 | table += f""" 800 |{diff_pos:d} | 801 |{diff_speed:.0f} | 802 | """ 803 | else: 804 | table += f""" 805 |- | 806 |- | 807 | """ 808 | 809 | 810 | 811 | table += '' 818 | 819 | table += ' | ' 820 | 821 | table = table + """