diff --git a/.gitignore b/.gitignore
index 71a6c46..b33b2f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,5 @@
sf.json
phue-rf-save.json
__pycache__/*
-external/__pycache__/*
\ No newline at end of file
+external/__pycache__/*
+external/modified/__pycache__/*
\ No newline at end of file
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..ad239f4
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Othneil Drew
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index f8e69df..11c7356 100644
--- a/README.md
+++ b/README.md
@@ -1,35 +1,164 @@
-# phue-racing-flags
+[![Forks][forks-shield]][forks-url]
+[![Stargazers][stars-shield]][stars-url]
+[![Issues][issues-shield]][issues-url]
+[![MIT License][license-shield]][license-url]
-## What is phue-racing-flags?
+
+
+
+
phue-racing-flags
-phue-racing-flags is a small tool written in Python, that allows you to use your Philips Hue lights to display the current racing flag of your Assetto Corsa Competizione Race.
+
+ Use your Philips Hue lights as Racing Flags.
+
+
+ Explore the docs »
+
+
+ Report Bug
+ ·
+ Request Feature
+
+
-## What does it look like?
+
+
+ Table of Contents
+
+ -
+ About The Project
+
+
+ - Supported Systems and Racing Simulators
+ -
+ Local Development
+
+
+ - Usage
+ - Roadmap
+ - Contributing
+ - License
+ - Contact
+ - Acknowledgements
+ - Disclaimer
+
+
-The design of the app tries to resemble the design of motorsport dashboards like Bosch's DDU systems.
+
+## About The Project
-![grafik](https://user-images.githubusercontent.com/12392728/120864997-75105f80-c58d-11eb-9492-2bbec0c6bcf8.png)
+![grafik](https://user-images.githubusercontent.com/12392728/120937234-b11dfe80-c70c-11eb-87bf-3c58046e0905.png)
-## How do I use it?
+One Friday evening I thought to myself that it would be pretty sweet to use my Philips Hue lights as indicators for the racing flags inside of Assetto Corsa Competizione. As no app was available to achieve this, I decided to take matters into my own hand and create one.
-1. Download the .exe file from here: https://github.com/TUnbehaun/phue-racing-flags/releases/latest/download/phue-racing-flags.exe
+### Built With
+
+* [Python](https://www.python.org/)
+
+## Supported Systems and Racing Simulators
+
+The app is designed for Windows and currently supports the following racing simulators:
+* [Assetto Corsa Competizione](https://www.assettocorsa.it/competizione/)
+* [iRacing](https://www.iracing.com/)
+
+
+## Local Development
+
+To get a local development copy up and running follow these simple steps.
+
+### Prerequisites
+
+* Python
+ https://www.python.org/downloads/
+
+### Installation
+
+* Clone the repo
+ ```sh
+ git clone https://github.com/TUnbehaun/phue-racing-flags.git
+ ```
+### Run the app
+
+* Start the GUI
+ ```sh
+ python gui.py
+ ```
+
+
+## Usage
+
+To just use the app itself without setting up a local development copy, you can follow these simple steps:
+
+1. Download the latest .exe file from here: https://github.com/TUnbehaun/phue-racing-flags/releases/latest/download/phue-racing-flags.exe
2. Run the .exe file
-3. Enter the IP Address of your Philips Hue Bridge in the "bridge ip" input field.
+
+Once the app is started, you can use it the following way:
+
+1. Enter the IP Address of your Philips Hue Bridge in the "bridge ip" input field.
(You can find the IP Address of your Bridge in the interface of your Router)
-5. Press the (hardware) link button on your Philips Hue Bridge and then within 30 seconds hit the "Connect" button in the app.
+2. Press the (hardware) link button on your Philips Hue Bridge and then within 30 seconds hit the "Connect" button in the app.
(Pressing the (hardware) link button on your Philips Hue Bridge is only necessary for the very first time you connect the app to a new Bridge)
-6. You should be able to choose one of your lights under "flag light" to use as the Racing Flag Light
-7. Test the Racing Flag Light by using the buttons under "color test"
-8. Adjust brightness if needed
-9. To start syncing the Racing Flag Light with the Assetto Corsa Competizione Race Flag click "Start" under "acc sync"
-10. To stop syncing the Racing Flag Light with the ACC Race Flag click "Stop" under "acc sync"
+3. You should be able to choose one or multiple of your lights under "flag light" to use as the Racing Flag Light(s)
+4. Use the radio buttons under "sim" to switch between Assetto Corsa Competizione or iRacing.
+5. Test the Racing Flag Light(s) by using the buttons under "color test".
+6. Adjust brightness if needed.
+7. To start syncing the Racing Flag Light(s) with your simulator's race flag click "Start" under "live sync".
+8. To stop syncing the Racing Flag Light(s) click "Stop" under "live sync".
+
+
+## Roadmap
+
+See the [open issues](https://github.com/TUnbehaun/phue-racing-flags/issues) for a list of proposed features (and known issues).
+
+
+## Contributing
+
+Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**.
+
+1. Fork the Project
+2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
+3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
+4. Push to the Branch (`git push origin feature/AmazingFeature`)
+5. Open a Pull Request
+
+
+## License
+
+Distributed under the MIT License. See `LICENSE` for more information.
+
+
+## Contact
+
+Tim Unbehaun - tim@defacto.cool
+
+Project Link: [https://github.com/TUnbehaun/phue-racing-flags](https://github.com/TUnbehaun/phue-racing-flags)
+
+
+## Acknowledgements
+
+* [phue](https://github.com/studioimaginaire/phue)
+* [PySimpleGui](https://github.com/PySimpleGUI/PySimpleGUI)
+* [PyInstaller](http://www.pyinstaller.org/)
+* [pyirsdk](https://github.com/kutu/pyirsdk)
-## What is the sf.json file used for?
+## Disclaimer
-The sf.json file is used for storing your last entries for "bridge ip", "flag light" and "brightness". This brings the convencience, that upon restart of the app, everything is just as you left it. The sf.json file has to be located in the same folder as the .exe file for this to work.
+I am in no way affiliated with Philips, Kunos Simulazioni or iRacing.
-## What is planned for the future?
+
+
+[forks-shield]: https://img.shields.io/github/forks/TUnbehaun/phue-racing-flags.svg?style=for-the-badge
+[forks-url]: https://github.com/TUnbehaun/phue-racing-flags/network/members
+[stars-shield]: https://img.shields.io/github/stars/TUnbehaun/phue-racing-flags.svg?style=for-the-badge
+[stars-url]: https://github.com/TUnbehaun/phue-racing-flags/stargazers
+[issues-shield]: https://img.shields.io/github/issues/TUnbehaun/phue-racing-flags.svg?style=for-the-badge
+[issues-url]: https://github.com/TUnbehaun/phue-racing-flags/issues
+[license-shield]: https://img.shields.io/github/license/TUnbehaun/phue-racing-flags.svg?style=for-the-badge
+[license-url]: https://github.com/TUnbehaun/phue-racing-flags/blob/master/LICENSE.txt
-* Enabling a custom color mapping per flag
-* Supporting other sims like iRacing, rFactor, RaceRoom, etc.
-* Enabling the mapping of other racing metrics to Philips Hue lights (motor revs, time delta, etc.)
diff --git a/external/modified/irsdk.py b/external/modified/irsdk.py
new file mode 100644
index 0000000..6038cba
--- /dev/null
+++ b/external/modified/irsdk.py
@@ -0,0 +1,677 @@
+#
+# BEWARE: THIS IS NOT THE ORIGINAL irsdk.py FILE FOUND ON https://github.com/kutu/pyirsdk
+#
+# The following changes were made:
+# Removal of all pyyaml dependencies
+# Added the ability for the API to run without iRacing running in the background
+#
+#
+
+import re
+import mmap
+import struct
+import ctypes
+from urllib import request, error
+
+VERSION = '1.2.6'
+
+SIM_STATUS_URL = 'http://127.0.0.1:32034/get_sim_status?object=simStatus'
+
+MEMMAPFILE = 'Local\\IRSDKMemMapFileName'
+MEMMAPFILESIZE = 1164 * 1024
+BROADCASTMSGNAME = 'IRSDK_BROADCASTMSG'
+
+VAR_TYPE_MAP = ['c', '?', 'i', 'I', 'f', 'd']
+
+YAML_TRANSLATER = bytes.maketrans(b'\x81\x8D\x8F\x90\x9D', b' ')
+YAML_CODE_PAGE = 'cp1252'
+
+class StatusField:
+ status_connected = 1
+
+class EngineWarnings:
+ water_temp_warning = 0x01
+ fuel_pressure_warning = 0x02
+ oil_pressure_warning = 0x04
+ engine_stalled = 0x08
+ pit_speed_limiter = 0x10
+ rev_limiter_active = 0x20
+
+class Flags:
+ # global flags
+ checkered = 0x0001
+ white = 0x0002
+ green = 0x0004
+ yellow = 0x0008
+ red = 0x0010
+ blue = 0x0020
+ debris = 0x0040
+ crossed = 0x0080
+ yellow_waving = 0x0100
+ one_lap_to_green = 0x0200
+ green_held = 0x0400
+ ten_to_go = 0x0800
+ five_to_go = 0x1000
+ random_waving = 0x2000
+ caution = 0x4000
+ caution_waving = 0x8000
+
+ # drivers black flags
+ black = 0x010000
+ disqualify = 0x020000
+ servicible = 0x040000 # car is allowed service (not a flag)
+ furled = 0x080000
+ repair = 0x100000
+
+ # start lights
+ start_hidden = 0x10000000
+ start_ready = 0x20000000
+ start_set = 0x40000000
+ start_go = 0x80000000
+
+class TrkLoc:
+ not_in_world = -1
+ off_track = 0
+ in_pit_stall = 1
+ aproaching_pits = 2
+ on_track = 3
+
+class TrkSurf:
+ not_in_world = -1
+ undefined = 0
+ asphalt_1 = 1
+ asphalt_2 = 2
+ asphalt_3 = 3
+ asphalt_4 = 4
+ concrete_1 = 5
+ concrete_2 = 6
+ racing_dirt_1 = 7
+ racing_dirt_2 = 8
+ paint_1 = 9
+ paint_2 = 10
+ rumble_1 = 11
+ rumble_2 = 12
+ rumble_3 = 13
+ rumble_4 = 14
+ grass_1 = 15
+ grass_2 = 16
+ grass_3 = 17
+ grass_4 = 18
+ dirt_1 = 19
+ dirt_2 = 20
+ dirt_3 = 21
+ dirt_4 = 22
+ sand = 23
+ gravel_1 = 24
+ gravel_2 = 25
+ grasscrete = 26
+ astroturf = 27
+
+class SessionState:
+ invalid = 0
+ get_in_car = 1
+ warmup = 2
+ parade_laps = 3
+ racing = 4
+ checkered = 5
+ cool_down = 6
+
+class CameraState:
+ is_session_screen = 0x0001 # the camera tool can only be activated if viewing the session screen (out of car)
+ is_scenic_active = 0x0002 # the scenic camera is active (no focus car)
+
+ # these can be changed with a broadcast message
+ cam_tool_active = 0x0004
+ ui_hidden = 0x0008
+ use_auto_shot_selection = 0x0010
+ use_temporary_edits = 0x0020
+ use_key_acceleration = 0x0040
+ use_key10x_acceleration = 0x0080
+ use_mouse_aim_mode = 0x0100
+
+class BroadcastMsg:
+ cam_switch_pos = 0 # car position, group, camera
+ cam_switch_num = 1 # driver #, group, camera
+ cam_set_state = 2 # CameraState, unused, unused
+ replay_set_play_speed = 3 # speed, slowMotion, unused
+ replay_set_play_position = 4 # RpyPosMode, Frame Number (high, low)
+ replay_search = 5 # RpySrchMode, unused, unused
+ replay_set_state = 6 # RpyStateMode, unused, unused
+ reload_textures = 7 # ReloadTexturesMode, carIdx, unused
+ chat_command = 8 # ChatCommandMode, subCommand, unused
+ pit_command = 9 # PitCommandMode, parameter
+ telem_command = 10 # irsdk_TelemCommandMode, unused, unused
+ ffb_command = 11 # irsdk_FFBCommandMode, value (float, high, low)
+ replay_search_session_time = 12 # sessionNum, sessionTimeMS (high, low)
+
+class ChatCommandMode:
+ macro = 0 # pass in a number from 1-15 representing the chat macro to launch
+ begin_chat = 1 # Open up a new chat window
+ reply = 2 # Reply to last private chat
+ cancel = 3 # Close chat window
+
+class PitCommandMode: # this only works when the driver is in the car
+ clear = 0 # Clear all pit checkboxes
+ ws = 1 # Clean the winshield, using one tear off
+ fuel = 2 # Add fuel, optionally specify the amount to add in liters or pass '0' to use existing amount
+ lf = 3 # Change the left front tire, optionally specifying the pressure in KPa or pass '0' to use existing pressure
+ rf = 4 # right front
+ lr = 5 # left rear
+ rr = 6 # right rear
+ clear_tires = 7 # Clear tire pit checkboxes
+ fr = 8 # Request a fast repair
+ clear_ws = 9 # Uncheck Clean the winshield checkbox
+ clear_fr = 10 # Uncheck request a fast repair
+ clear_fuel = 11 # Uncheck add fuel
+
+class TelemCommandMode: # You can call this any time, but telemtry only records when driver is in there car
+ stop = 0 # Turn telemetry recording off
+ start = 1 # Turn telemetry recording on
+ restart = 2 # Write current file to disk and start a new one
+
+class RpyStateMode:
+ erase_tape = 0 # clear any data in the replay tape
+
+class ReloadTexturesMode:
+ all = 0 # reload all textuers
+ car_idx = 1 # reload only textures for the specific carIdx
+
+class RpySrchMode:
+ to_start = 0
+ to_end = 1
+ prev_session = 2
+ next_session = 3
+ prev_lap = 4
+ next_lap = 5
+ prev_frame = 6
+ next_frame = 7
+ prev_incident = 8
+ next_incident = 9
+
+class RpyPosMode:
+ begin = 0
+ current = 1
+ end = 2
+
+class csMode:
+ at_incident = -3
+ at_leader = -2
+ at_exciting = -1
+
+class PitSvFlags:
+ lf_tire_change = 0x01
+ rf_tire_change = 0x02
+ lr_tire_change = 0x04
+ rr_tire_change = 0x08
+ fuel_fill = 0x10
+ windshield_tearoff = 0x20
+ fast_repair = 0x40
+
+class PitSvStatus:
+ # status
+ none = 0
+ in_progress = 1
+ complete = 2
+ # errors
+ too_far_left = 100
+ too_far_right = 101
+ too_far_forward = 102
+ too_far_back = 103
+ bad_angle = 104
+ cant_fix_that = 105
+
+class PaceMode:
+ single_file_start = 0
+ double_file_start = 1
+ single_file_restart = 2
+ double_file_restart = 3
+ not_pacing = 4
+
+class PaceFlags:
+ end_of_line = 0x01
+ free_pass = 0x02
+ waved_around = 0x04
+
+class CarLeftRight:
+ clear = 1 # no cars around us.
+ car_left = 2 # there is a car to our left.
+ car_right = 3 # there is a car to our right.
+ car_left_right = 4 # there are cars on each side.
+ two_cars_left = 5 # there are two cars to our left.
+ two_cars_right = 6 # there are two cars to our right.
+
+class FFBCommandMode: # You can call this any time
+ ffb_command_max_force = 0 # Set the maximum force when mapping steering torque force to direct input units (float in Nm)
+
+
+
+class IRSDKStruct:
+ @classmethod
+ def property_value(cls, offset, var_type):
+ struct_type = struct.Struct(var_type)
+ return property(lambda self: self.get(offset, struct_type))
+
+ @classmethod
+ def property_value_str(cls, offset, var_type):
+ struct_type = struct.Struct(var_type)
+ return property(lambda self: self.get(offset, struct_type).strip(b'\x00').decode('latin-1'))
+
+ def __init__(self, shared_mem, offset=0):
+ self._shared_mem = shared_mem
+ self._offset = offset
+
+ def get(self, offset, struct_type):
+ return struct_type.unpack_from(self._shared_mem, self._offset + offset)[0]
+
+class Header(IRSDKStruct):
+ version = IRSDKStruct.property_value(0, 'i')
+ status = IRSDKStruct.property_value(4, 'i')
+ tick_rate = IRSDKStruct.property_value(8, 'i')
+
+ session_info_update = IRSDKStruct.property_value(12, 'i')
+ session_info_len = IRSDKStruct.property_value(16, 'i')
+ session_info_offset = IRSDKStruct.property_value(20, 'i')
+
+ num_vars = IRSDKStruct.property_value(24, 'i')
+ var_header_offset = IRSDKStruct.property_value(28, 'i')
+
+ num_buf = IRSDKStruct.property_value(32, 'i')
+ buf_len = IRSDKStruct.property_value(36, 'i')
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.var_buf = [
+ VarBuffer(self._shared_mem, 48 + i * 16)
+ for i in range(self.num_buf)
+ ]
+
+class VarBuffer(IRSDKStruct):
+ tick_count = IRSDKStruct.property_value(0, 'i')
+ buf_offset = IRSDKStruct.property_value(4, 'i')
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._freezed_memory = None
+
+ def freeze(self):
+ self._freezed_memory = self._shared_mem[:]
+
+ def unfreeze(self):
+ self._freezed_memory = None
+
+ def get_memory(self):
+ return self._freezed_memory or self._shared_mem
+
+class VarHeader(IRSDKStruct):
+ type = IRSDKStruct.property_value(0, 'i')
+ offset = IRSDKStruct.property_value(4, 'i')
+ count = IRSDKStruct.property_value(8, 'i')
+ count_as_time = IRSDKStruct.property_value(12, '?')
+ name = IRSDKStruct.property_value_str(16, '32s')
+ desc = IRSDKStruct.property_value_str(48, '64s')
+ unit = IRSDKStruct.property_value_str(112, '32s')
+
+class DiskSubHeader(IRSDKStruct):
+ session_start_date = IRSDKStruct.property_value(0, 'Q')
+ session_start_time = IRSDKStruct.property_value(8, 'd')
+ session_end_time = IRSDKStruct.property_value(16, 'd')
+ session_lap_count = IRSDKStruct.property_value(24, 'i')
+ session_record_count = IRSDKStruct.property_value(28, 'i')
+
+class IRSDK:
+ def __init__(self, parse_yaml_async=False):
+ self.parse_yaml_async = parse_yaml_async
+ self.is_initialized = False
+ self.last_session_info_update = 0
+
+ self._shared_mem = None
+ self._header = None
+
+ self.__var_headers = None
+ self.__var_headers_dict = None
+ self.__var_headers_names = None
+ self.__var_buffer_latest = None
+ self.__session_info_dict = {}
+ self.__broadcast_msg_id = None
+ self.__is_using_test_file = False
+ self.__workaround_connected_state = 0
+
+ def __getitem__(self, key):
+ if key in self._var_headers_dict:
+ var_header = self._var_headers_dict[key]
+ var_buf_latest = self._var_buffer_latest
+ res = struct.unpack_from(
+ VAR_TYPE_MAP[var_header.type] * var_header.count,
+ var_buf_latest.get_memory(),
+ var_buf_latest.buf_offset + var_header.offset)
+ return res[0] if var_header.count == 1 else list(res)
+
+ return self._get_session_info(key)
+
+ @property
+ def is_connected(self):
+ if self._header:
+ if self._header.status == StatusField.status_connected:
+ self.__workaround_connected_state = 0
+ if self.__workaround_connected_state == 0 and self._header.status != StatusField.status_connected:
+ self.__workaround_connected_state = 1
+ if self.__workaround_connected_state == 1 and (self['SessionNum'] is None or self.__is_using_test_file):
+ self.__workaround_connected_state = 2
+ if self.__workaround_connected_state == 2 and self['SessionNum'] is not None:
+ self.__workaround_connected_state = 3
+ return self._header is not None and \
+ (self._header.status == StatusField.status_connected or self.__workaround_connected_state == 3)
+
+ @property
+ def session_info_update(self):
+ return self._header.session_info_update
+
+ @property
+ def var_headers_names(self):
+ if self.__var_headers_names is None:
+ self.__var_headers_names = [var_header.name for var_header in self._var_headers]
+ return self.__var_headers_names
+
+ def startup(self, test_file=None, dump_to=None):
+ if test_file is None and not self._check_sim_status():
+ return False
+
+ if self._shared_mem is None:
+ if test_file:
+ f = open(test_file, 'rb')
+ self._shared_mem = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
+ self.__is_using_test_file = True
+ else:
+ self._shared_mem = mmap.mmap(0, MEMMAPFILESIZE, MEMMAPFILE, access=mmap.ACCESS_READ)
+
+ if self._shared_mem:
+ if dump_to:
+ with open(dump_to, 'wb') as f:
+ f.write(self._shared_mem)
+ self._header = Header(self._shared_mem)
+ self.is_initialized = self._header.version >= 1 and len(self._header.var_buf) > 0
+
+ return self.is_initialized
+
+ def shutdown(self):
+ self.is_initialized = False
+ self.last_session_info_update = 0
+ if self._shared_mem:
+ self._shared_mem.close()
+ self._shared_mem = None
+ self._header = None
+ self.__var_headers = None
+ self.__var_headers_dict = None
+ self.__var_headers_names = None
+ self.__var_buffer_latest = None
+ self.__session_info_dict = {}
+ self.__broadcast_msg_id = None
+
+ def parse_to(self, to_file):
+ if not self.is_initialized:
+ return
+ f = open(to_file, 'w', encoding='utf-8')
+ f.write(self._shared_mem[self._header.session_info_offset:self._header.session_info_len].rstrip(b'\x00').decode(YAML_CODE_PAGE))
+ f.write('\n'.join([
+ '{:32}{}'.format(i, self[i])
+ for i in sorted(self._var_headers_dict.keys(), key=str.lower)
+ ]))
+ f.close()
+
+ def cam_switch_pos(self, position=0, group=1, camera=0):
+ return self._broadcast_msg(BroadcastMsg.cam_switch_pos, position, group, camera)
+
+ def cam_switch_num(self, car_number='1', group=1, camera=0):
+ return self._broadcast_msg(BroadcastMsg.cam_switch_num, self._pad_car_num(car_number), group, camera)
+
+ def cam_set_state(self, camera_state=CameraState.cam_tool_active):
+ return self._broadcast_msg(BroadcastMsg.cam_set_state, camera_state)
+
+ def replay_set_play_speed(self, speed=0, slow_motion=False):
+ return self._broadcast_msg(BroadcastMsg.replay_set_play_speed, speed, 1 if slow_motion else 0)
+
+ def replay_set_play_position(self, pos_mode=RpyPosMode.begin, frame_num=0):
+ return self._broadcast_msg(BroadcastMsg.replay_set_play_position, pos_mode, frame_num)
+
+ def replay_search(self, search_mode=RpySrchMode.to_start):
+ return self._broadcast_msg(BroadcastMsg.replay_search, search_mode)
+
+ def replay_set_state(self, state_mode=RpyStateMode.erase_tape):
+ return self._broadcast_msg(BroadcastMsg.replay_set_state, state_mode)
+
+ def reload_all_textures(self):
+ return self._broadcast_msg(BroadcastMsg.reload_textures, ReloadTexturesMode.all)
+
+ def reload_texture(self, car_idx=0):
+ return self._broadcast_msg(BroadcastMsg.reload_textures, ReloadTexturesMode.car_idx, car_idx)
+
+ def chat_command(self, chat_command_mode=ChatCommandMode.begin_chat):
+ return self._broadcast_msg(BroadcastMsg.chat_command, chat_command_mode)
+
+ def chat_command_macro(self, macro_num=0):
+ return self._broadcast_msg(BroadcastMsg.chat_command, ChatCommandMode.macro, macro_num)
+
+ def pit_command(self, pit_command_mode=PitCommandMode.clear, var=0):
+ return self._broadcast_msg(BroadcastMsg.pit_command, pit_command_mode, var)
+
+ def telem_command(self, telem_command_mode=TelemCommandMode.stop):
+ return self._broadcast_msg(BroadcastMsg.telem_command, telem_command_mode)
+
+ def ffb_command(self, ffb_command_mode=FFBCommandMode.ffb_command_max_force, value=0.0):
+ return self._broadcast_msg(BroadcastMsg.ffb_command, ffb_command_mode, float(value))
+
+ def replay_search_session_time(self, session_num=0, session_time_ms=0):
+ return self._broadcast_msg(BroadcastMsg.replay_search_session_time, session_num, session_time_ms)
+
+ def _check_sim_status(self):
+ try:
+ return 'running:1' in request.urlopen(SIM_STATUS_URL).read().decode('utf-8')
+ except error.URLError as e:
+ print("Failed to connect to sim: {}".format(e.reason))
+ return False
+
+ @property
+ def _var_buffer_latest(self):
+ if not self.is_initialized and not self.startup():
+ return None
+ if self.__var_buffer_latest:
+ return self.__var_buffer_latest
+ return sorted(self._header.var_buf, key=lambda v: v.tick_count)[-1]
+
+ @property
+ def _var_headers(self):
+ if self.__var_headers is None:
+ self.__var_headers = []
+ if self._header:
+ for i in range(self._header.num_vars):
+ var_header = VarHeader(self._shared_mem, self._header.var_header_offset + i * 144)
+ self._var_headers.append(var_header)
+ return self.__var_headers
+
+ @property
+ def _var_headers_dict(self):
+ if self.__var_headers_dict is None:
+ self.__var_headers_dict = {}
+ for var_header in self._var_headers:
+ self.__var_headers_dict[var_header.name] = var_header
+ return self.__var_headers_dict
+
+ def freeze_var_buffer_latest(self):
+ self.unfreeze_var_buffer_latest()
+ self.__var_buffer_latest = self._var_buffer_latest
+ self.__var_buffer_latest.freeze()
+
+ def unfreeze_var_buffer_latest(self):
+ if self.__var_buffer_latest:
+ self.__var_buffer_latest.unfreeze()
+ self.__var_buffer_latest = None
+
+ def get_session_info_update_by_key(self, key):
+ if key in self.__session_info_dict:
+ return self.__session_info_dict[key]['update']
+ return None
+
+ def _get_session_info(self, key):
+ if self._header and self.last_session_info_update < self._header.session_info_update:
+ self.last_session_info_update = self._header.session_info_update
+ for session_data in self.__session_info_dict.values():
+ # keep previous parsed data, in case binary data not changed
+ if session_data['data']:
+ session_data['data_last'] = session_data['data']
+ session_data['data'] = None
+
+ if key not in self.__session_info_dict:
+ self.__session_info_dict[key] = dict(data=None)
+
+ session_data = self.__session_info_dict[key]
+
+ # already have and parsed
+ if session_data['data']:
+ return session_data['data']
+
+ if self.parse_yaml_async:
+ if 'async_session_info_update' not in session_data or session_data['async_session_info_update'] < self.last_session_info_update:
+ session_data['async_session_info_update'] = self.last_session_info_update
+
+ return session_data['data']
+
+ def _get_session_info_binary(self, key):
+ start = self._header.session_info_offset
+ end = start + self._header.session_info_len
+ # search section by key
+ match_start = re.compile(('\n%s:\n' % key).encode(YAML_CODE_PAGE)).search(self._shared_mem, start, end)
+ if not match_start:
+ return None
+ match_end = re.compile(b'\n\n').search(self._shared_mem, match_start.start() + 1, end)
+ if not match_end:
+ return None
+ return self._shared_mem[match_start.start() + 1 : match_end.start()]
+
+ @property
+ def _broadcast_msg_id(self):
+ if self.__broadcast_msg_id is None:
+ self.__broadcast_msg_id = ctypes.windll.user32.RegisterWindowMessageW(BROADCASTMSGNAME)
+ return self.__broadcast_msg_id
+
+ def _broadcast_msg(self, broadcast_type=0, var1=0, var2=0, var3=0):
+ if isinstance(var2, float):
+ var2 = int(var2 * 65536.0)
+ return ctypes.windll.user32.SendNotifyMessageW(0xFFFF, self._broadcast_msg_id,
+ broadcast_type | var1 << 16, var2 | var3 << 16)
+
+ def _pad_car_num(self, num):
+ num = str(num)
+ num_len = len(num)
+ zero = num_len - len(num.lstrip("0"))
+ if zero > 0 and num_len == zero:
+ zero -= 1
+ num = int(num)
+ if zero:
+ num_place = 3 if num > 99 else 2 if num > 9 else 1
+ return num + 1000 * (num_place + zero)
+ return num
+
+class IBT:
+ def __init__(self):
+ self._ibt_file = None
+ self._shared_mem = None
+ self._header = None
+ self._disk_header = None
+
+ self.__var_headers = None
+ self.__var_headers_dict = None
+ self.__var_headers_names = None
+ self.__session_info_dict = None
+
+ def __getitem__(self, key):
+ return self.get(self._disk_header.session_record_count - 1, key)
+
+ @property
+ def file_name(self):
+ return self._ibt_file and self._ibt_file.name
+
+ @property
+ def var_header_buffer_tick(self):
+ return self._header and self._header.var_buf[0].tick_count
+
+ @property
+ def var_headers_names(self):
+ if not self._header:
+ return None
+ if self.__var_headers_names is None:
+ self.__var_headers_names = [var_header.name for var_header in self._var_headers]
+ return self.__var_headers_names
+
+ def open(self, ibt_file):
+ self._ibt_file = open(ibt_file, 'rb')
+ self._shared_mem = mmap.mmap(self._ibt_file.fileno(), 0, access=mmap.ACCESS_READ)
+ self._header = Header(self._shared_mem)
+ self._disk_header = DiskSubHeader(self._shared_mem, 112)
+
+ def close(self):
+ if self._shared_mem:
+ self._shared_mem.close()
+
+ if self._ibt_file:
+ self._ibt_file.close()
+
+ self._ibt_file = None
+ self._shared_mem = None
+ self._header = None
+ self._disk_header = None
+
+ self.__var_headers = None
+ self.__var_headers_dict = None
+ self.__var_headers_names = None
+ self.__session_info_dict = None
+
+ def get(self, index, key):
+ if not self._header:
+ return None
+ if 0 > index >= self._disk_header.session_record_count:
+ return None
+ if key in self._var_headers_dict:
+ var_header = self._var_headers_dict[key]
+ fmt = VAR_TYPE_MAP[var_header.type] * var_header.count
+ var_offset = var_header.offset + self._header.var_buf[0].buf_offset + index * self._header.buf_len
+ res = struct.unpack_from(fmt, self._shared_mem, var_offset)
+ return list(res) if var_header.count > 1 else res[0]
+ return None
+
+ def get_all(self, key):
+ if not self._header:
+ return None
+ if key in self._var_headers_dict:
+ var_header = self._var_headers_dict[key]
+ fmt = VAR_TYPE_MAP[var_header.type] * var_header.count
+ var_offset = var_header.offset + self._header.var_buf[0].buf_offset
+ buf_len = self._header.buf_len
+ is_array = var_header.count > 1
+ results = []
+ for i in range(self._disk_header.session_record_count):
+ res = struct.unpack_from(fmt, self._shared_mem, var_offset + i * buf_len)
+ results.append(list(res) if is_array else res[0])
+ return results
+ return None
+
+ @property
+ def _var_headers(self):
+ if not self._header:
+ return None
+ if self.__var_headers is None:
+ self.__var_headers = []
+ for i in range(self._header.num_vars):
+ var_header = VarHeader(self._shared_mem, self._header.var_header_offset + i * 144)
+ self._var_headers.append(var_header)
+ return self.__var_headers
+
+ @property
+ def _var_headers_dict(self):
+ if not self._header:
+ return None
+ if self.__var_headers_dict is None:
+ self.__var_headers_dict = {}
+ for var_header in self._var_headers:
+ self.__var_headers_dict[var_header.name] = var_header
+ return self.__var_headers_dict
diff --git a/gui.py b/gui.py
index 3da87dd..1ac164e 100644
--- a/gui.py
+++ b/gui.py
@@ -1,19 +1,21 @@
import external.PySimpleGUI as sg
from external.phue import Bridge
+import acc
+import iracing
import threading
import json
-import acc
import time
-
# Global Variables
SAVE_FILE_PATH = './phue-rf-save.json'
HUE_CONNECTION = {
'ip': '',
'lights': [],
- 'brightness': 255
+ 'brightness': 255,
+ 'sim': 'ACC'
}
-STOP_SYNC: bool
+
+STOP_SYNC = True
# Hue Colors
HUE_COLOR_NO_FLAG = None
@@ -65,11 +67,13 @@ def open_window():
# GUI Frames
flag_frame_layout = [
- [sg.Graph(canvas_size=(875, 100), graph_bottom_left=(0, 0), graph_top_right=(875, 100), background_color=GUI_COLOR_NO_FLAG, key='CANVAS_FLAG')]
+ [sg.Graph(canvas_size=(875, 100), graph_bottom_left=(0, 0), graph_top_right=(875, 100),
+ background_color=GUI_COLOR_NO_FLAG, key='CANVAS_FLAG')]
]
bridge_ip_frame_layout = [
- [sg.Input(key='INPUT_IP', default_text=HUE_CONNECTION['ip'], font=('Helvetica', 24), size=(15, 1)), sg.Button('Connect', key='BTN_BRIDGE', font=('Helvetica', 24))]
+ [sg.Input(key='INPUT_IP', default_text=HUE_CONNECTION['ip'], font=('Helvetica', 24), size=(15, 1)),
+ sg.Button('Connect', key='BTN_BRIDGE', font=('Helvetica', 24))]
]
bridge_status_frame_layout = [
@@ -77,36 +81,93 @@ def open_window():
]
light_menu_frame_layout = [
- [sg.Listbox(values=light_options, key='MENU_LIGHT', disabled=disable_lights_menu, default_values=HUE_CONNECTION['lights'], enable_events=True, font=('Helvetica', 24), size=(23, 4), select_mode='multiple')]
+ [sg.Listbox(values=light_options, key='MENU_LIGHT', disabled=disable_lights_menu,
+ default_values=HUE_CONNECTION['lights'], enable_events=True, font=('Helvetica', 24), size=(23, 4),
+ select_mode='multiple')]
]
brightness_menu_frame_layout = [
- [sg.Slider(range=(1, 255), default_value=int(HUE_CONNECTION['brightness']), size=(20, 20), orientation='horizontal', font=('Helvetica', 24), enable_events=True, key='SLIDER_BRIGHTNESS')]
+ [sg.Slider(range=(1, 255), default_value=int(HUE_CONNECTION['brightness']), size=(20, 20),
+ orientation='horizontal', font=('Helvetica', 24), enable_events=True, key='SLIDER_BRIGHTNESS')]
+ ]
+
+ sim_select_frame_layout = [
+ [sg.Radio('Assetto Corsa Competizione', 'SIM_SELECT', font=('Helvetica', 24), disabled=disable_lights_menu,
+ key='SIM_SELECT_ACC', enable_events=True, default=HUE_CONNECTION['sim'] == 'ACC'),
+ sg.Radio('iRacing', 'SIM_SELECT', font=('Helvetica', 24), size=(21, 1), disabled=disable_lights_menu,
+ key='SIM_SELECT_IRACING', enable_events=True, default=HUE_CONNECTION['sim'] == 'iRacing')]
+ ]
+
+ acc_color_test_frame_layout = [
+ [sg.Button('No Flag', key='BTN_ACC_NO_FLAG', button_color=('#ffffff', GUI_COLOR_NO_FLAG),
+ disabled=disable_lights_menu, font=('Helvetica', 24)),
+ sg.Button('Blue Flag', key='BTN_ACC_BLUE_FLAG', button_color=('#ffffff', GUI_COLOR_BLUE_FLAG),
+ disabled=disable_lights_menu, font=('Helvetica', 24)),
+ sg.Button('Yellow Flag', key='BTN_ACC_YELLOW_FLAG', button_color=('#000000', GUI_COLOR_YELLOW_FLAG),
+ disabled=disable_lights_menu, font=('Helvetica', 24)),
+ sg.Button('Black Flag', key='BTN_ACC_BLACK_FLAG', button_color=('#ffffff', GUI_COLOR_BLACK_FLAG),
+ disabled=disable_lights_menu, font=('Helvetica', 24)),
+ sg.Button('White Flag', key='BTN_ACC_WHITE_FLAG', button_color=('#000000', GUI_COLOR_WHITE_FLAG),
+ disabled=disable_lights_menu, font=('Helvetica', 24))],
+ [sg.Button('Checkered Flag', key='BTN_ACC_CHECKERED_FLAG', button_color=('#ffffff', GUI_COLOR_CHECKERED_FLAG),
+ disabled=disable_lights_menu, font=('Helvetica', 24)),
+ sg.Button('Penalty Flag', key='BTN_ACC_PENALTY_FLAG', button_color=('#ffffff', GUI_COLOR_PENALTY_FLAG),
+ disabled=disable_lights_menu, font=('Helvetica', 24)),
+ sg.Button('Green Flag', key='BTN_ACC_GREEN_FLAG', button_color=('#ffffff', GUI_COLOR_GREEN_FLAG),
+ disabled=disable_lights_menu, font=('Helvetica', 24)),
+ sg.Button('Orange Flag', key='BTN_ACC_ORANGE_FLAG', button_color=('#ffffff', GUI_COLOR_ORANGE_FLAG),
+ disabled=disable_lights_menu, font=('Helvetica', 24))]
]
- color_test_frame_layout = [
- [sg.Button('No Flag', key='BTN_NO_FLAG', button_color=('#ffffff', GUI_COLOR_NO_FLAG), disabled=disable_lights_menu, font=('Helvetica', 24)), sg.Button('Blue Flag', key='BTN_BLUE_FLAG', button_color=('#ffffff', GUI_COLOR_BLUE_FLAG), disabled=disable_lights_menu, font=('Helvetica', 24)), sg.Button('Yellow Flag', key='BTN_YELLOW_FLAG', button_color=('#000000', GUI_COLOR_YELLOW_FLAG), disabled=disable_lights_menu, font=('Helvetica', 24)), sg.Button('Black Flag', key='BTN_BLACK_FLAG', button_color=('#ffffff', GUI_COLOR_BLACK_FLAG), disabled=disable_lights_menu, font=('Helvetica', 24)), sg.Button('White Flag', key='BTN_WHITE_FLAG', button_color=('#000000', GUI_COLOR_WHITE_FLAG), disabled=disable_lights_menu, font=('Helvetica', 24))],
- [sg.Button('Checkered Flag', key='BTN_CHECKERED_FLAG', button_color=('#ffffff', GUI_COLOR_CHECKERED_FLAG), disabled=disable_lights_menu, font=('Helvetica', 24)), sg.Button('Penalty Flag', key='BTN_PENALTY_FLAG', button_color=('#ffffff', GUI_COLOR_PENALTY_FLAG), disabled=disable_lights_menu, font=('Helvetica', 24)), sg.Button('Green Flag', key='BTN_GREEN_FLAG', button_color=('#ffffff', GUI_COLOR_GREEN_FLAG), disabled=disable_lights_menu, font=('Helvetica', 24)), sg.Button('Orange Flag', key='BTN_ORANGE_FLAG', button_color=('#ffffff', GUI_COLOR_ORANGE_FLAG), disabled=disable_lights_menu, font=('Helvetica', 24))]
+ iracing_color_test_frame_layout = [
+ [sg.Button('No Flag', key='BTN_IRACING_NO_FLAG', button_color=('#ffffff', GUI_COLOR_NO_FLAG),
+ disabled=disable_lights_menu, font=('Helvetica', 24)),
+ sg.Button('Blue Flag', key='BTN_IRACING_BLUE_FLAG', button_color=('#ffffff', GUI_COLOR_BLUE_FLAG),
+ disabled=disable_lights_menu, font=('Helvetica', 24)),
+ sg.Button('Yellow Flag', key='BTN_IRACING_YELLOW_FLAG', button_color=('#000000', GUI_COLOR_YELLOW_FLAG),
+ disabled=disable_lights_menu, font=('Helvetica', 24)),
+ sg.Button('Black Flag', key='BTN_IRACING_BLACK_FLAG', button_color=('#ffffff', GUI_COLOR_BLACK_FLAG),
+ disabled=disable_lights_menu, font=('Helvetica', 24)),
+ sg.Button('White Flag', key='BTN_IRACING_WHITE_FLAG', button_color=('#000000', GUI_COLOR_WHITE_FLAG),
+ disabled=disable_lights_menu, font=('Helvetica', 24))],
+ [sg.Button('Checkered Flag', key='BTN_IRACING_CHEQUERED_FLAG',
+ button_color=('#ffffff', GUI_COLOR_CHECKERED_FLAG),
+ disabled=disable_lights_menu, font=('Helvetica', 24)),
+ sg.Button('Red Flag', key='BTN_IRACING_RED_FLAG', button_color=('#ffffff', GUI_COLOR_PENALTY_FLAG),
+ disabled=disable_lights_menu, font=('Helvetica', 24)),
+ sg.Button('Green Flag', key='BTN_IRACING_GREEN_FLAG', button_color=('#ffffff', GUI_COLOR_GREEN_FLAG),
+ disabled=disable_lights_menu, font=('Helvetica', 24)),
+ sg.Button('Meatball Flag', key='BTN_IRACING_MEATBALL_FLAG', button_color=('#ffffff', GUI_COLOR_ORANGE_FLAG),
+ disabled=disable_lights_menu, font=('Helvetica', 24))]
]
- acc_controls_frame_layout = [
- [sg.Button('Start', key='BTN_ACC_START', disabled=disable_lights_menu, font=('Helvetica', 24)),
- sg.Button('Stop', key='BTN_ACC_STOP', disabled=disable_lights_menu, font=('Helvetica', 24))]
+ sync_controls_frame_layout = [
+ [sg.Button('Start', key='BTN_SYNC_START', disabled=disable_lights_menu, font=('Helvetica', 24)),
+ sg.Button('Stop', key='BTN_SYNC_STOP', disabled=disable_lights_menu, font=('Helvetica', 24))]
]
- acc_status_frame_layout = [
- [sg.Text(size=(34, 1), key='MSG_ACC_SYNC_STATUS', text='Stopped.', font=('Helvetica', 24))]
+ sync_status_frame_layout = [
+ [sg.Text(size=(34, 1), key='MSG_SYNC_STATUS', text='Stopped.', font=('Helvetica', 24))]
]
# Window Layout
layout = [
[sg.Frame('flag', flag_frame_layout, font=('Helvetica', 10), title_color='#ffffff')],
- [sg.Frame('bridge ip', bridge_ip_frame_layout, font=('Helvetica', 10), title_color='#ffffff'), sg.Frame('bridge status', bridge_status_frame_layout, font=('Helvetica', 10), title_color='#ffffff')],
- [sg.Frame('lights', light_menu_frame_layout, font=('Helvetica', 10), title_color='#ffffff'), sg.Frame('brightness', brightness_menu_frame_layout, font=('Helvetica', 10), title_color='#ffffff')],
- [sg.Frame('flag test', color_test_frame_layout, font=('Helvetica', 10), title_color='#ffffff')],
- [sg.Frame('acc sync', acc_controls_frame_layout, font=('Helvetica', 10), title_color='#ffffff'), sg.Frame('sync status', acc_status_frame_layout, font=('Helvetica', 10), title_color='#ffffff')],
- [sg.Text('If you are connecting this app to your Bridge for the first time, you have to press the Pairing Button on your Bridge and then connect within 30 seconds.', size=(80, 2), text_color='#b71c1c', key='MSG_30_SECONDS', visible=show_30_seconds_info)],
+ [sg.Frame('bridge ip', bridge_ip_frame_layout, font=('Helvetica', 10), title_color='#ffffff'),
+ sg.Frame('bridge status', bridge_status_frame_layout, font=('Helvetica', 10), title_color='#ffffff')],
+ [sg.Frame('lights', light_menu_frame_layout, font=('Helvetica', 10), title_color='#ffffff'),
+ sg.Frame('brightness', brightness_menu_frame_layout, font=('Helvetica', 10), title_color='#ffffff')],
+ [sg.Frame('sim', sim_select_frame_layout, font=('Helvetica', 10), title_color='#ffffff')],
+ [sg.pin(sg.Frame('flag test', acc_color_test_frame_layout, font=('Helvetica', 10), title_color='#ffffff',
+ visible=HUE_CONNECTION['sim'] == 'ACC', key='FRAME_ACC_FLAGS'))],
+ [sg.pin(sg.Frame('flag test', iracing_color_test_frame_layout, font=('Helvetica', 10), title_color='#ffffff',
+ visible=HUE_CONNECTION['sim'] == 'iRacing', key='FRAME_IRACING_FLAGS'))],
+ [sg.Frame('live sync', sync_controls_frame_layout, font=('Helvetica', 10), title_color='#ffffff'),
+ sg.Frame('sync status', sync_status_frame_layout, font=('Helvetica', 10), title_color='#ffffff')],
+ [sg.Text(
+ 'If you are connecting this app to your Bridge for the first time, you have to press the Pairing Button on your Bridge and then connect within 30 seconds.',
+ size=(80, 2), text_color='#b71c1c', key='MSG_30_SECONDS', visible=show_30_seconds_info)],
]
window = sg.Window('phue-racing-flags', layout, font='Helvetica', finalize=True)
@@ -132,44 +193,88 @@ def open_window():
HUE_CONNECTION['brightness'] = values['SLIDER_BRIGHTNESS']
save_hue_connection_to_file()
- if event == 'BTN_NO_FLAG':
- raise_flag(acc.ACCFlagType.ACC_NO_FLAG, bridge, window)
+ if event == 'SIM_SELECT_ACC':
+ stop_sync()
+ HUE_CONNECTION['sim'] = 'ACC'
+ window['FRAME_ACC_FLAGS'].update(visible=True)
+ window['FRAME_IRACING_FLAGS'].update(visible=False)
+ save_hue_connection_to_file()
+
+ if event == 'SIM_SELECT_IRACING':
+ stop_sync()
+ HUE_CONNECTION['sim'] = 'iRacing'
+ window['FRAME_ACC_FLAGS'].update(visible=False)
+ window['FRAME_IRACING_FLAGS'].update(visible=True)
+ save_hue_connection_to_file()
+
+ # ACC Flag Buttons
+
+ if event == 'BTN_ACC_NO_FLAG':
+ raise_acc_flag(acc.ACCFlagType.ACC_NO_FLAG, bridge, window)
+
+ if event == 'BTN_ACC_BLUE_FLAG':
+ raise_acc_flag(acc.ACCFlagType.ACC_BLUE_FLAG, bridge, window)
- if event == 'BTN_BLUE_FLAG':
- raise_flag(acc.ACCFlagType.ACC_BLUE_FLAG, bridge, window)
+ if event == 'BTN_ACC_YELLOW_FLAG':
+ raise_acc_flag(acc.ACCFlagType.ACC_YELLOW_FLAG, bridge, window)
- if event == 'BTN_YELLOW_FLAG':
- raise_flag(acc.ACCFlagType.ACC_YELLOW_FLAG, bridge, window)
+ if event == 'BTN_ACC_BLACK_FLAG':
+ raise_acc_flag(acc.ACCFlagType.ACC_BLACK_FLAG, bridge, window)
- if event == 'BTN_BLACK_FLAG':
- raise_flag(acc.ACCFlagType.ACC_BLACK_FLAG, bridge, window)
+ if event == 'BTN_ACC_WHITE_FLAG':
+ raise_acc_flag(acc.ACCFlagType.ACC_WHITE_FLAG, bridge, window)
- if event == 'BTN_WHITE_FLAG':
- raise_flag(acc.ACCFlagType.ACC_WHITE_FLAG, bridge, window)
+ if event == 'BTN_ACC_CHECKERED_FLAG':
+ raise_acc_flag(acc.ACCFlagType.ACC_CHECKERED_FLAG, bridge, window)
- if event == 'BTN_CHECKERED_FLAG':
- raise_flag(acc.ACCFlagType.ACC_CHECKERED_FLAG, bridge, window)
+ if event == 'BTN_ACC_PENALTY_FLAG':
+ raise_acc_flag(acc.ACCFlagType.ACC_PENALTY_FLAG, bridge, window)
- if event == 'BTN_PENALTY_FLAG':
- raise_flag(acc.ACCFlagType.ACC_PENALTY_FLAG, bridge, window)
+ if event == 'BTN_ACC_GREEN_FLAG':
+ raise_acc_flag(acc.ACCFlagType.ACC_GREEN_FLAG, bridge, window)
- if event == 'BTN_GREEN_FLAG':
- raise_flag(acc.ACCFlagType.ACC_GREEN_FLAG, bridge, window)
+ if event == 'BTN_ACC_ORANGE_FLAG':
+ raise_acc_flag(acc.ACCFlagType.ACC_ORANGE_FLAG, bridge, window)
- if event == 'BTN_ORANGE_FLAG':
- raise_flag(acc.ACCFlagType.ACC_ORANGE_FLAG, bridge, window)
+ # iRacing Flag Buttons
- if event == 'BTN_ACC_START':
- window['MSG_ACC_SYNC_STATUS'].update('Running.')
- thread = threading.Thread(target=start_acc_sync, args=(bridge, window,))
+ if event == 'BTN_IRACING_NO_FLAG':
+ raise_iracing_flag(iracing.IRacingGUIFlagType.IRACING_NO_FLAG, bridge, window)
+
+ if event == 'BTN_IRACING_BLUE_FLAG':
+ raise_iracing_flag(iracing.IRacingGUIFlagType.IRACING_BLUE_FLAG, bridge, window)
+
+ if event == 'BTN_IRACING_YELLOW_FLAG':
+ raise_iracing_flag(iracing.IRacingGUIFlagType.IRACING_YELLOW_FLAG, bridge, window)
+
+ if event == 'BTN_IRACING_BLACK_FLAG':
+ raise_iracing_flag(iracing.IRacingGUIFlagType.IRACING_BLACK_FLAG, bridge, window)
+
+ if event == 'BTN_IRACING_WHITE_FLAG':
+ raise_iracing_flag(iracing.IRacingGUIFlagType.IRACING_WHITE_FLAG, bridge, window)
+
+ if event == 'BTN_IRACING_CHEQUERED_FLAG':
+ raise_iracing_flag(iracing.IRacingGUIFlagType.IRACING_CHEQUERED_FLAG, bridge, window)
+
+ if event == 'BTN_IRACING_RED_FLAG':
+ raise_iracing_flag(iracing.IRacingGUIFlagType.IRACING_RED_FLAG, bridge, window)
+
+ if event == 'BTN_IRACING_GREEN_FLAG':
+ raise_iracing_flag(iracing.IRacingGUIFlagType.IRACING_GREEN_FLAG, bridge, window)
+
+ if event == 'BTN_IRACING_MEATBALL_FLAG':
+ raise_iracing_flag(iracing.IRacingGUIFlagType.IRACING_MEATBALL_FLAG, bridge, window)
+
+ if event == 'BTN_SYNC_START':
+ window['MSG_SYNC_STATUS'].update('Running.')
+ thread = threading.Thread(target=start_sync, args=(bridge, window,))
thread.start()
- if event == 'BTN_ACC_STOP':
- window['MSG_ACC_SYNC_STATUS'].update('Stopped.')
- stop_acc_sync()
+ if event == 'BTN_SYNC_STOP':
+ stop_sync()
if event == sg.WINDOW_CLOSED:
- stop_acc_sync()
+ stop_sync()
window.close()
break
@@ -177,18 +282,33 @@ def open_window():
def enable_interface(bridge: Bridge, window: sg.Window):
window['MSG_BRIDGE'].update('Connection established.')
window['MENU_LIGHT'].update(disabled=False)
- window['BTN_NO_FLAG'].update(disabled=False)
- window['BTN_BLUE_FLAG'].update(disabled=False)
- window['BTN_YELLOW_FLAG'].update(disabled=False)
- window['BTN_BLACK_FLAG'].update(disabled=False)
- window['BTN_WHITE_FLAG'].update(disabled=False)
- window['BTN_CHECKERED_FLAG'].update(disabled=False)
- window['BTN_CHECKERED_FLAG'].update(disabled=False)
- window['BTN_PENALTY_FLAG'].update(disabled=False)
- window['BTN_GREEN_FLAG'].update(disabled=False)
- window['BTN_ORANGE_FLAG'].update(disabled=False)
- window['BTN_ACC_START'].update(disabled=False)
- window['BTN_ACC_STOP'].update(disabled=False)
+
+ # ACC Flag Buttons
+ window['BTN_ACC_NO_FLAG'].update(disabled=False)
+ window['BTN_ACC_BLUE_FLAG'].update(disabled=False)
+ window['BTN_ACC_YELLOW_FLAG'].update(disabled=False)
+ window['BTN_ACC_BLACK_FLAG'].update(disabled=False)
+ window['BTN_ACC_WHITE_FLAG'].update(disabled=False)
+ window['BTN_ACC_CHECKERED_FLAG'].update(disabled=False)
+ window['BTN_ACC_PENALTY_FLAG'].update(disabled=False)
+ window['BTN_ACC_GREEN_FLAG'].update(disabled=False)
+ window['BTN_ACC_ORANGE_FLAG'].update(disabled=False)
+
+ # iRacing Flag Buttons
+ window['BTN_IRACING_NO_FLAG'].update(disabled=False)
+ window['BTN_IRACING_BLUE_FLAG'].update(disabled=False)
+ window['BTN_IRACING_YELLOW_FLAG'].update(disabled=False)
+ window['BTN_IRACING_BLACK_FLAG'].update(disabled=False)
+ window['BTN_IRACING_WHITE_FLAG'].update(disabled=False)
+ window['BTN_IRACING_CHEQUERED_FLAG'].update(disabled=False)
+ window['BTN_IRACING_RED_FLAG'].update(disabled=False)
+ window['BTN_IRACING_GREEN_FLAG'].update(disabled=False)
+ window['BTN_IRACING_MEATBALL_FLAG'].update(disabled=False)
+
+ window['BTN_SYNC_START'].update(disabled=False)
+ window['BTN_SYNC_STOP'].update(disabled=False)
+ window['SIM_SELECT_ACC'].update(disabled=False)
+ window['SIM_SELECT_IRACING'].update(disabled=False)
window['MENU_LIGHT'].update(values=get_lights_from_bridge(bridge))
window['MSG_30_SECONDS'].update(visible=False)
@@ -196,18 +316,34 @@ def enable_interface(bridge: Bridge, window: sg.Window):
def disable_interface(window: sg.Window):
window['MSG_BRIDGE'].update('Connection failed.')
window['MENU_LIGHT'].update(disabled=True)
- window['BTN_NO_FLAG'].update(disabled=True)
- window['BTN_BLUE_FLAG'].update(disabled=True)
- window['BTN_YELLOW_FLAG'].update(disabled=True)
- window['BTN_BLACK_FLAG'].update(disabled=True)
- window['BTN_WHITE_FLAG'].update(disabled=True)
- window['BTN_CHECKERED_FLAG'].update(disabled=True)
- window['BTN_CHECKERED_FLAG'].update(disabled=True)
- window['BTN_PENALTY_FLAG'].update(disabled=True)
- window['BTN_GREEN_FLAG'].update(disabled=True)
- window['BTN_ORANGE_FLAG'].update(disabled=True)
- window['BTN_ACC_START'].update(disabled=True)
- window['BTN_ACC_STOP'].update(disabled=True)
+
+ # ACC Flag Buttons
+ window['BTN_ACC_NO_FLAG'].update(disabled=True)
+ window['BTN_ACC_BLUE_FLAG'].update(disabled=True)
+ window['BTN_ACC_YELLOW_FLAG'].update(disabled=True)
+ window['BTN_ACC_BLACK_FLAG'].update(disabled=True)
+ window['BTN_ACC_WHITE_FLAG'].update(disabled=True)
+ window['BTN_ACC_CHECKERED_FLAG'].update(disabled=True)
+ window['BTN_ACC_CHECKERED_FLAG'].update(disabled=True)
+ window['BTN_ACC_PENALTY_FLAG'].update(disabled=True)
+ window['BTN_ACC_GREEN_FLAG'].update(disabled=True)
+ window['BTN_ACC_ORANGE_FLAG'].update(disabled=True)
+
+ # iRacing Flag Buttons
+ window['BTN_IRACING_NO_FLAG'].update(disabled=True)
+ window['BTN_IRACING_BLUE_FLAG'].update(disabled=True)
+ window['BTN_IRACING_YELLOW_FLAG'].update(disabled=True)
+ window['BTN_IRACING_BLACK_FLAG'].update(disabled=True)
+ window['BTN_IRACING_WHITE_FLAG'].update(disabled=True)
+ window['BTN_IRACING_CHEQUERED_FLAG'].update(disabled=True)
+ window['BTN_IRACING_RED_FLAG'].update(disabled=True)
+ window['BTN_IRACING_GREEN_FLAG'].update(disabled=True)
+ window['BTN_IRACING_MEATBALL_FLAG'].update(disabled=True)
+
+ window['BTN_SYNC_START'].update(disabled=True)
+ window['BTN_SYNC_STOP'].update(disabled=True)
+ window['SIM_SELECT_ACC'].update(disabled=True)
+ window['SIM_SELECT_IRACING'].update(disabled=True)
window['MENU_LIGHT'].update(values=[])
window['MSG_30_SECONDS'].update(visible=True)
@@ -219,11 +355,13 @@ def load_hue_connection_from_file():
HUE_CONNECTION['ip'] = data['ip']
HUE_CONNECTION['lights'] = data['lights']
HUE_CONNECTION['brightness'] = data['brightness']
- except FileNotFoundError as error:
+ HUE_CONNECTION['sim'] = data['sim'] or 'ACC'
+ except (FileNotFoundError, KeyError) as error:
print(error)
HUE_CONNECTION['ip'] = ''
HUE_CONNECTION['lights'] = ''
HUE_CONNECTION['brightness'] = 255
+ HUE_CONNECTION['sim'] = 'ACC'
def save_hue_connection_to_file():
@@ -249,24 +387,32 @@ def get_lights_from_bridge(bridge: Bridge) -> []:
return light_options
-def sync_color(bridge: Bridge, window: sg.Window):
+def sync_acc_color(bridge: Bridge, window: sg.Window):
global HUE_CONNECTION
flag = acc.get_flag()
- raise_flag(flag, bridge, window)
+ raise_acc_flag(flag, bridge, window)
+
+def sync_iracing_color(bridge: Bridge, window: sg.Window):
+ global HUE_CONNECTION
+ flag = iracing.get_flag()
+ raise_iracing_flag(flag, bridge, window)
-def raise_flag(flag: acc.ACCFlagType, bridge: Bridge, window: sg.Window):
+
+def raise_acc_flag(flag: acc.ACCFlagType, bridge: Bridge, window: sg.Window):
if flag == acc.ACCFlagType.ACC_NO_FLAG:
for light in HUE_CONNECTION['lights']:
bridge.set_light(light, {'transitiontime': 0, 'on': False})
window['CANVAS_FLAG'].update(background_color=GUI_COLOR_NO_FLAG)
if flag == acc.ACCFlagType.ACC_BLUE_FLAG:
for light in HUE_CONNECTION['lights']:
- bridge.set_light(light, {'transitiontime': 0, 'on': True, 'bri': int(HUE_CONNECTION['brightness']), 'xy': HUE_COLOR_BLUE_FLAG})
+ bridge.set_light(light, {'transitiontime': 0, 'on': True, 'bri': int(HUE_CONNECTION['brightness']),
+ 'xy': HUE_COLOR_BLUE_FLAG})
window['CANVAS_FLAG'].update(background_color=GUI_COLOR_BLUE_FLAG)
if flag == acc.ACCFlagType.ACC_YELLOW_FLAG:
for light in HUE_CONNECTION['lights']:
- bridge.set_light(light, {'transitiontime': 0, 'on': True, 'bri': int(HUE_CONNECTION['brightness']), 'xy': HUE_COLOR_YELLOW_FLAG})
+ bridge.set_light(light, {'transitiontime': 0, 'on': True, 'bri': int(HUE_CONNECTION['brightness']),
+ 'xy': HUE_COLOR_YELLOW_FLAG})
window['CANVAS_FLAG'].update(background_color=GUI_COLOR_YELLOW_FLAG)
if flag == acc.ACCFlagType.ACC_BLACK_FLAG:
for light in HUE_CONNECTION['lights']:
@@ -274,7 +420,8 @@ def raise_flag(flag: acc.ACCFlagType, bridge: Bridge, window: sg.Window):
window['CANVAS_FLAG'].update(background_color=GUI_COLOR_BLACK_FLAG)
if flag == acc.ACCFlagType.ACC_WHITE_FLAG:
for light in HUE_CONNECTION['lights']:
- bridge.set_light(light, {'transitiontime': 0, 'on': True, 'bri': int(HUE_CONNECTION['brightness']), 'xy': HUE_COLOR_WHITE_FLAG})
+ bridge.set_light(light, {'transitiontime': 0, 'on': True, 'bri': int(HUE_CONNECTION['brightness']),
+ 'xy': HUE_COLOR_WHITE_FLAG})
window['CANVAS_FLAG'].update(background_color=GUI_COLOR_WHITE_FLAG)
if flag == acc.ACCFlagType.ACC_CHECKERED_FLAG:
for light in HUE_CONNECTION['lights']:
@@ -282,31 +429,85 @@ def raise_flag(flag: acc.ACCFlagType, bridge: Bridge, window: sg.Window):
window['CANVAS_FLAG'].update(background_color=GUI_COLOR_CHECKERED_FLAG)
if flag == acc.ACCFlagType.ACC_PENALTY_FLAG:
for light in HUE_CONNECTION['lights']:
- bridge.set_light(light, {'transitiontime': 0, 'on': True, 'bri': int(HUE_CONNECTION['brightness']), 'xy': HUE_COLOR_PENALTY_FLAG})
+ bridge.set_light(light, {'transitiontime': 0, 'on': True, 'bri': int(HUE_CONNECTION['brightness']),
+ 'xy': HUE_COLOR_PENALTY_FLAG})
window['CANVAS_FLAG'].update(background_color=GUI_COLOR_PENALTY_FLAG)
if flag == acc.ACCFlagType.ACC_GREEN_FLAG:
for light in HUE_CONNECTION['lights']:
- bridge.set_light(light, {'transitiontime': 0, 'on': True, 'bri': int(HUE_CONNECTION['brightness']), 'xy': HUE_COLOR_GREEN_FLAG})
+ bridge.set_light(light, {'transitiontime': 0, 'on': True, 'bri': int(HUE_CONNECTION['brightness']),
+ 'xy': HUE_COLOR_GREEN_FLAG})
window['CANVAS_FLAG'].update(background_color=GUI_COLOR_GREEN_FLAG)
if flag == acc.ACCFlagType.ACC_ORANGE_FLAG:
for light in HUE_CONNECTION['lights']:
- bridge.set_light(light, {'transitiontime': 0, 'on': True, 'bri': int(HUE_CONNECTION['brightness']), 'xy': HUE_COLOR_ORANGE_FLAG})
+ bridge.set_light(light, {'transitiontime': 0, 'on': True, 'bri': int(HUE_CONNECTION['brightness']),
+ 'xy': HUE_COLOR_ORANGE_FLAG})
window['CANVAS_FLAG'].update(background_color=GUI_COLOR_ORANGE_FLAG)
+def raise_iracing_flag(flag: iracing.IRacingGUIFlagType, bridge: Bridge, window: sg.Window):
+ if flag == iracing.IRacingGUIFlagType.IRACING_NO_FLAG:
+ for light in HUE_CONNECTION['lights']:
+ bridge.set_light(light, {'transitiontime': 0, 'on': False})
+ window['CANVAS_FLAG'].update(background_color=GUI_COLOR_NO_FLAG)
+ if flag == iracing.IRacingGUIFlagType.IRACING_BLUE_FLAG:
+ for light in HUE_CONNECTION['lights']:
+ bridge.set_light(light, {'transitiontime': 0, 'on': True, 'bri': int(HUE_CONNECTION['brightness']),
+ 'xy': HUE_COLOR_BLUE_FLAG})
+ window['CANVAS_FLAG'].update(background_color=GUI_COLOR_BLUE_FLAG)
+ if flag == iracing.IRacingGUIFlagType.IRACING_YELLOW_FLAG:
+ for light in HUE_CONNECTION['lights']:
+ bridge.set_light(light, {'transitiontime': 0, 'on': True, 'bri': int(HUE_CONNECTION['brightness']),
+ 'xy': HUE_COLOR_YELLOW_FLAG})
+ window['CANVAS_FLAG'].update(background_color=GUI_COLOR_YELLOW_FLAG)
+ if flag == iracing.IRacingGUIFlagType.IRACING_BLACK_FLAG:
+ for light in HUE_CONNECTION['lights']:
+ bridge.set_light(light, {'transitiontime': 0, 'on': False})
+ window['CANVAS_FLAG'].update(background_color=GUI_COLOR_BLACK_FLAG)
+ if flag == iracing.IRacingGUIFlagType.IRACING_WHITE_FLAG:
+ for light in HUE_CONNECTION['lights']:
+ bridge.set_light(light, {'transitiontime': 0, 'on': True, 'bri': int(HUE_CONNECTION['brightness']),
+ 'xy': HUE_COLOR_WHITE_FLAG})
+ window['CANVAS_FLAG'].update(background_color=GUI_COLOR_WHITE_FLAG)
+ if flag == iracing.IRacingGUIFlagType.IRACING_CHEQUERED_FLAG:
+ for light in HUE_CONNECTION['lights']:
+ bridge.set_light(light, {'transitiontime': 0, 'on': False})
+ window['CANVAS_FLAG'].update(background_color=GUI_COLOR_CHECKERED_FLAG)
+ if flag == iracing.IRacingGUIFlagType.IRACING_RED_FLAG:
+ for light in HUE_CONNECTION['lights']:
+ bridge.set_light(light, {'transitiontime': 0, 'on': True, 'bri': int(HUE_CONNECTION['brightness']),
+ 'xy': HUE_COLOR_PENALTY_FLAG})
+ window['CANVAS_FLAG'].update(background_color=GUI_COLOR_PENALTY_FLAG)
+ if flag == iracing.IRacingGUIFlagType.IRACING_GREEN_FLAG:
+ for light in HUE_CONNECTION['lights']:
+ bridge.set_light(light, {'transitiontime': 0, 'on': True, 'bri': int(HUE_CONNECTION['brightness']),
+ 'xy': HUE_COLOR_GREEN_FLAG})
+ window['CANVAS_FLAG'].update(background_color=GUI_COLOR_GREEN_FLAG)
+ if flag == iracing.IRacingGUIFlagType.IRACING_MEATBALL_FLAG:
+ for light in HUE_CONNECTION['lights']:
+ bridge.set_light(light, {'transitiontime': 0, 'on': True, 'bri': int(HUE_CONNECTION['brightness']),
+ 'xy': HUE_COLOR_ORANGE_FLAG})
+ window['CANVAS_FLAG'].update(background_color=GUI_COLOR_ORANGE_FLAG)
+
-def start_acc_sync(bridge: Bridge, window: sg.Window):
+def start_sync(bridge: Bridge, window: sg.Window):
global STOP_SYNC
- STOP_SYNC = False
- while True:
- sync_color(bridge, window)
- time.sleep(0.1)
+ if STOP_SYNC:
+ STOP_SYNC = False
+ while True:
+ if HUE_CONNECTION['sim'] == 'ACC':
+ sync_acc_color(bridge, window)
+ time.sleep(0.1)
- if STOP_SYNC:
- break
+ if HUE_CONNECTION['sim'] == 'iRacing':
+ sync_iracing_color(bridge, window)
+ time.sleep(0.1)
+
+ if STOP_SYNC:
+ window['MSG_SYNC_STATUS'].update('Stopped.')
+ break
-def stop_acc_sync():
+def stop_sync():
global STOP_SYNC
STOP_SYNC = True
diff --git a/iracing.py b/iracing.py
new file mode 100644
index 0000000..30c1192
--- /dev/null
+++ b/iracing.py
@@ -0,0 +1,85 @@
+import external.modified.irsdk as irsdk
+from enum import Enum
+
+
+class IRacingMemoryFlagType(Enum):
+ # global flags
+ checkered = 0x0001
+ white = 0x0002
+ green = 0x0004
+ yellow = 0x0008
+ red = 0x0010
+ blue = 0x0020
+ debris = 0x0040
+ crossed = 0x0080
+ yellow_waving = 0x0100
+ one_lap_to_green = 0x0200
+ green_held = 0x0400
+ ten_to_go = 0x0800
+ five_to_go = 0x1000
+ random_waving = 0x2000
+ caution = 0x4000
+ caution_waving = 0x8000
+
+ # drivers black flags
+ black = 0x010000
+ disqualify = 0x020000
+ servicible = 0x040000 # car is allowed service (not a flag)
+ furled = 0x080000
+ repair = 0x100000
+
+ # start lights
+ start_hidden = 0x10000000
+ start_ready = 0x20000000
+ start_set = 0x40000000
+ start_go = 0x80000000
+
+
+class IRacingGUIFlagType(Enum):
+ IRACING_NO_FLAG = 0
+ IRACING_BLUE_FLAG = 1
+ IRACING_MEATBALL_FLAG = 2
+ IRACING_BLACK_FLAG = 3
+ IRACING_YELLOW_FLAG = 4
+ IRACING_GREEN_FLAG = 5
+ IRACING_WHITE_FLAG = 6
+ IRACING_CHEQUERED_FLAG = 7
+ IRACING_RED_FLAG = 8
+
+
+def get_flag() -> IRacingGUIFlagType:
+ memory_flags = []
+ gui_flags = []
+ ir = irsdk.IRSDK()
+ ir.startup()
+ session_flag = ir['SessionFlags']
+
+ if session_flag:
+ for flag in IRacingMemoryFlagType:
+ if IRacingMemoryFlagType(flag).value & session_flag == IRacingMemoryFlagType(flag).value:
+ memory_flags.append(flag)
+
+ if IRacingMemoryFlagType.blue in memory_flags:
+ gui_flags.append(IRacingGUIFlagType.IRACING_BLUE_FLAG)
+ if IRacingMemoryFlagType.repair in memory_flags:
+ gui_flags.append(IRacingGUIFlagType.IRACING_MEATBALL_FLAG)
+ if IRacingMemoryFlagType.black in memory_flags:
+ gui_flags.append(IRacingGUIFlagType.IRACING_BLACK_FLAG)
+ if IRacingMemoryFlagType.yellow in memory_flags:
+ gui_flags.append(IRacingGUIFlagType.IRACING_YELLOW_FLAG)
+ if IRacingMemoryFlagType.green in memory_flags:
+ gui_flags.append(IRacingGUIFlagType.IRACING_GREEN_FLAG)
+ if IRacingMemoryFlagType.white in memory_flags:
+ gui_flags.append(IRacingGUIFlagType.IRACING_WHITE_FLAG)
+ if IRacingMemoryFlagType.checkered in memory_flags:
+ gui_flags.append(IRacingGUIFlagType.IRACING_CHEQUERED_FLAG)
+ if IRacingMemoryFlagType.red in memory_flags:
+ gui_flags.append(IRacingGUIFlagType.IRACING_RED_FLAG)
+
+ if len(gui_flags) == 0 or len(gui_flags) > 1:
+ return IRacingGUIFlagType.IRACING_NO_FLAG
+ else:
+ return gui_flags[0]
+
+ else:
+ return IRacingGUIFlagType.IRACING_NO_FLAG