├── .gitignore ├── can └── readme.txt ├── ports.py ├── requirements.txt ├── decode_can_log.py ├── games ├── ats.py ├── fh5.py └── supporting_files │ ├── data_format.txt │ └── forza.py ├── s550_data.py ├── README.md ├── replay.py ├── database.md ├── interactive_brute_force.py └── s550_cluster.py /.gitignore: -------------------------------------------------------------------------------- 1 | \venv 2 | \__pycache__ -------------------------------------------------------------------------------- /can/readme.txt: -------------------------------------------------------------------------------- 1 | Captured from python-can can.logger using slcan, baudrate 500000 on the GT350 -------------------------------------------------------------------------------- /ports.py: -------------------------------------------------------------------------------- 1 | import serial.tools.list_ports 2 | ports = serial.tools.list_ports.comports() 3 | 4 | # run this to determine which port the USB-TO-CAN sensor is attached to 5 | for port, desc, hwid in sorted(ports): 6 | print("{}: {} [{}]".format(port, desc, hwid)) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2023.5.7 2 | charset-normalizer==3.1.0 3 | idna==3.4 4 | packaging==23.1 5 | pynput==1.7.6 6 | pyserial==3.5 7 | python-can==4.2.0 8 | pywin32==306 9 | requests==2.30.0 10 | six==1.16.0 11 | typing_extensions==4.5.0 12 | urllib3==2.0.2 13 | wrapt==1.15.0 14 | -------------------------------------------------------------------------------- /decode_can_log.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import csv 3 | import sys 4 | 5 | # 4 Feb 2024 6 | # script re-worked so output will work with my fork of v-ivanyshyn's parse_can_logs 7 | # https://github.com/EricTurner3/parse_can_logs 8 | 9 | data = [] 10 | 11 | with open(sys.argv[1], newline='') as f: 12 | reader = csv.reader(f) 13 | row_num = 1 14 | for row in reader: 15 | 16 | if row_num == 1: 17 | row[2:10] = "" 18 | pass # skip the header row on decoding 19 | else: 20 | row[0] = int(float(row[0])) #convert str timestamp to float 21 | bytes = row[6] 22 | #convert the b64 to hex 23 | row[2::] = base64.b64decode(bytes).hex(' ').upper().split(" ") 24 | 25 | data.append(row) 26 | row_num = row_num +1 27 | 28 | with open('log.csv', 'w', newline='') as f: 29 | writer = csv.writer(f) 30 | writer.writerows(data) -------------------------------------------------------------------------------- /games/ats.py: -------------------------------------------------------------------------------- 1 | ''' 2 | American Truck Simulator / Euro Truck Simulator 2 - Game Interface Script 3 | 10 May 2023 4 | 5 | This file properly formats data from ATS/ETS2 to be mapped into the cluster 6 | 7 | REQUIRES: https://github.com/Funbit/ets2-telemetry-server 8 | ''' 9 | import sys 10 | sys.path.append('../') 11 | import s550_data as d 12 | import requests as r 13 | import json 14 | 15 | def data(): 16 | formatted_data = d.s550_ipc('ats') 17 | data_out = json.loads(r.get("http://127.0.0.1:25555/api/ets2/telemetry").text)['truck'] 18 | 19 | # map the forza values to expected cluster 20 | formatted_data.icon_parking_brake = int(data_out['parkBrakeOn']) 21 | formatted_data.engine_on = int(data_out['engineOn']) 22 | formatted_data.icon_high_beams = int(data_out['lightsBeamHighOn']) 23 | formatted_data.icon_left_signal = int(data_out['blinkerLeftActive']) 24 | formatted_data.icon_right_signal = int(data_out['blinkerRightActive']) 25 | 26 | formatted_data.value_speed = data_out['speed'] / 1.75 27 | formatted_data.value_rpm = data_out['engineRpm'] 28 | formatted_data.value_rpm_max = data_out['engineRpmMax'] 29 | formatted_data.oil_temp = data_out['oilTemperature'] 30 | formatted_data.engine_temp = data_out['waterTemperature'] 31 | return formatted_data -------------------------------------------------------------------------------- /games/fh5.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Forza Horizon 5 - Game Interface Script 3 | 10 May 2023 4 | 5 | This file properly formats data from FH5 to be mapped into the cluster 6 | ''' 7 | import sys 8 | sys.path.append('../') 9 | import s550_data as d 10 | import games.supporting_files.forza as forza 11 | 12 | def data(): 13 | formatted_data = d.s550_ipc('fh5') 14 | fh5_data_out = forza.fetch_forza_data() 15 | 16 | # map the forza values to expected cluster 17 | if (fh5_data_out['HandBrake'] > 0): 18 | formatted_data.icon_parking_brake = 1 19 | else: 20 | formatted_data.icon_parking_brake = 0 21 | 22 | tire_slip_threshold = 10 23 | if (fh5_data_out['TireSlipRatioFrontLeft'] >= tire_slip_threshold or \ 24 | fh5_data_out['TireSlipRatioFrontRight'] >= tire_slip_threshold or \ 25 | fh5_data_out['TireSlipRatioRearLeft'] >= tire_slip_threshold or \ 26 | fh5_data_out['TireSlipRatioRearRight'] >= tire_slip_threshold): 27 | formatted_data.icon_traction_control = 2 28 | else: 29 | formatted_data.icon_traction_control = 0 30 | 31 | formatted_data.value_speed = fh5_data_out['Speed'] * 2 32 | formatted_data.value_rpm = fh5_data_out['CurrentEngineRpm'] 33 | formatted_data.value_rpm_max = fh5_data_out['EngineMaxRpm'] 34 | formatted_data.value_boost = fh5_data_out['Boost'] 35 | formatted_data.value_odometer = fh5_data_out['DistanceTraveled'] 36 | return formatted_data -------------------------------------------------------------------------------- /s550_data.py: -------------------------------------------------------------------------------- 1 | ''' 2 | S550 Data Format 3 | 10 May 2023 4 | 5 | This script provides the default schema in order to send to the s550.py file 6 | This can be inherited to 7 | ''' 8 | 9 | class s550_ipc: 10 | 11 | def __init__(self, game): 12 | self.game = game 13 | 14 | self.engine_on = 1 # 1 on; 0 off. Disables backlight when off 15 | # icons have 3 states 16 | # 0 - off 17 | # 1 - on 18 | # 2 - on, flashing (some) 19 | self.icon_launch_control = 0 20 | self.icon_abs = 0 21 | self.icon_traction_control = 0 22 | self.icon_airbag = 0 23 | self.icon_seatbelt = 0 # no flashing 24 | self.icon_parking_brake = 0 # no flashing 25 | self.icon_left_signal = 0 # either 0 or 2 26 | self.icon_right_signal = 0 # either 0 or 2 27 | self.icon_high_beams = 0 # no flashing 28 | 29 | # raw values, typically numbers used in gauges 30 | self.value_speed = 0.0 # miles per hour 31 | self.value_rpm = 0.0 32 | self.value_rpm_max = 8000 # modify if game car has higher/lower RPM than cluster 33 | self.value_odometer = 0 # kilometers 34 | self.value_boost = 0.0 # currently unmapped 35 | self.value_fuel = 0.0 # currently unmapped 36 | self.oil_temp = 0.0 # celsius 37 | self.engine_temp = 0.0 # celsius 38 | -------------------------------------------------------------------------------- /games/supporting_files/data_format.txt: -------------------------------------------------------------------------------- 1 | s32 IsRaceOn 2 | u32 TimestampMS 3 | f32 EngineMaxRpm 4 | f32 EngineIdleRpm 5 | f32 CurrentEngineRpm 6 | f32 AccelerationX 7 | f32 AccelerationY 8 | f32 AccelerationZ 9 | f32 VelocityX 10 | f32 VelocityY 11 | f32 VelocityZ 12 | f32 AngularVelocityX 13 | f32 AngularVelocityY 14 | f32 AngularVelocityZ 15 | f32 Yaw 16 | f32 Pitch 17 | f32 Roll 18 | f32 NormalizedSuspensionTravelFrontLeft 19 | f32 NormalizedSuspensionTravelFrontRight 20 | f32 NormalizedSuspensionTravelRearLeft 21 | f32 NormalizedSuspensionTravelRearRight 22 | f32 TireSlipRatioFrontLeft 23 | f32 TireSlipRatioFrontRight 24 | f32 TireSlipRatioRearLeft 25 | f32 TireSlipRatioRearRight 26 | f32 WheelRotationSpeedFrontLeft 27 | f32 WheelRotationSpeedFrontRight 28 | f32 WheelRotationSpeedRearLeft 29 | f32 WheelRotationSpeedRearRight 30 | s32 WheelOnRumbleStripFrontLeft 31 | s32 WheelOnRumbleStripFrontRight 32 | s32 WheelOnRumbleStripRearLeft 33 | s32 WheelOnRumbleStripRearRight 34 | f32 WheelInPuddleDepthFrontLeft 35 | f32 WheelInPuddleDepthFrontRight 36 | f32 WheelInPuddleDepthRearLeft 37 | f32 WheelInPuddleDepthRearRight 38 | f32 SurfaceRumbleFrontLeft 39 | f32 SurfaceRumbleFrontRight 40 | f32 SurfaceRumbleRearLeft 41 | f32 SurfaceRumbleRearRight 42 | f32 TireSlipAngleFrontLeft 43 | f32 TireSlipAngleFrontRight 44 | f32 TireSlipAngleRearLeft 45 | f32 TireSlipAngleRearRight 46 | f32 TireCombinedSlipFrontLeft 47 | f32 TireCombinedSlipFrontRight 48 | f32 TireCombinedSlipRearLeft 49 | f32 TireCombinedSlipRearRight 50 | f32 SuspensionTravelMetersFrontLeft 51 | f32 SuspensionTravelMetersFrontRight 52 | f32 SuspensionTravelMetersRearLeft 53 | f32 SuspensionTravelMetersRearRight 54 | s32 CarOrdinal 55 | s32 CarClass 56 | s32 CarPerformanceIndex 57 | s32 DrivetrainType 58 | s32 NumCylinders 59 | hzn HorizonPlaceholder 60 | f32 PositionX 61 | f32 PositionY 62 | f32 PositionZ 63 | f32 Speed 64 | f32 Power 65 | f32 Torque 66 | f32 TireTempFrontLeft 67 | f32 TireTempFrontRight 68 | f32 TireTempRearLeft 69 | f32 TireTempRearRight 70 | f32 Boost 71 | f32 Fuel 72 | f32 DistanceTraveled 73 | f32 BestLap 74 | f32 LastLap 75 | f32 CurrentLap 76 | f32 CurrentRaceTime 77 | u16 LapNumber 78 | u8 RacePosition 79 | u8 Accel 80 | u8 Brake 81 | u8 Clutch 82 | u8 HandBrake 83 | u8 Gear 84 | s8 Steer 85 | s8 NormalizedDrivingLine 86 | s8 NormalizedAIBrakeDifference -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ford Mustang (S550) CAN Bus Research & Scripts 2 | === 3 | 4 | This repo serves as a compendium of scripts and hours of trial/error research to try and map the S550 canbus, specifically towards the IPC. 5 | 6 | Main Scripts 7 | * s550_cluster.py - Currently supports Forza Horizon 5, American Truck Simulator & Euro Truck Simulator 2 in an alpha state. Allows that game to interface with the cluster. See s550_data.py for the model in translating other game data 8 | * [Video Demonstration](https://youtu.be/KNyn1v3_cwc) 9 | * interactive_brute_force.py - This script allows you to use the up,down,left,right,enter keys to control the menu on the cluster 10 | * arrows.py and brute_force.py were merged into a single interactive script 11 | * [Video Demonstration](https://youtu.be/OzUs28GIq0A) 12 | 13 | Supporting Scripts 14 | * ports.py - Script that was pulled from StackOverflow to show which port the USB-to-CAN interface is running on. Required to communicate with the IPC 15 | * decode_can_log.py - Takes a CANdump from the can folder and converts the b64 data string to a more easily readable byte array 16 | * replay.py - Attempt to replay data from the CANdump at a slower pace to determine functionality (doesn't quite work) 17 | 18 | Requirements: 19 | * USB-to-CAN adapter. I am using a [CANable](https://openlightlabs.com/collections/frontpage/products/canable-0-4) 20 | * Wires / [Breadboard Wires](https://www.amazon.com/dp/B01EV70C78) 21 | * 12v power supply (I had one laying around with a 5.5mm x 2.1mm plug barrel on the end) 22 | * You can wire one of the female connectors from [here](https://www.amazon.com/dp/B079RBL339) to allow for easy plug and play. 23 | * S550 Cluster (you can find them for under $100 on eBay.) 24 | * I am unsure of the digital clusters functionality, some data should be shared so it should work in an alpha state. 25 | * Python, PIP, & Packages in the requirements.txt file 26 | 27 | Booting Your Device: 28 | * APIM / SYNC2 / SYNC3 29 | * Pin 1 - 12V 30 | * Pin 37 - Ground 31 | * Pin 19 - HSCAN High (goes to USB-to-CAN adapter) 32 | * Pin 20 - HSCAN Low (goes to USB-to-CAN adapter) 33 | * Instrument Cluster 34 | * Pin 8 - 12V 35 | * Pin 3 - Ground 36 | * Pin 12 - HSCAN High (goes to USB-to-CAN adapter) 37 | * Pin 13 - HSCAN Low (goes to USB-to-CAN adapter) 38 | 39 | References 40 | * [v-ivanyshyn's Ford CAN IDs summary](https://github.com/v-ivanyshyn/parse_can_logs/blob/master/Ford%20CAN%20IDs%20Summary.md) 41 | * this helped me find the tire pressure -------------------------------------------------------------------------------- /games/supporting_files/forza.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import struct 3 | from os import system, name, path 4 | 5 | # https://github.com/nikidziuba/Forza_horizon_data_out_python 6 | 7 | UDP_IP = "0.0.0.0" #This sets server ip to the RPi ip 8 | UDP_PORT = 30500 #You can freely edit this 9 | 10 | 11 | 12 | #reading data and assigning names to data types in data_types dict 13 | data_types = {} 14 | with open(path.join(path.dirname(__file__),'data_format.txt'), 'r') as f: 15 | lines = f.read().split('\n') 16 | for line in lines: 17 | data_types[line.split()[1]] = line.split()[0] 18 | 19 | 20 | #assigning sizes in bytes to each variable type 21 | jumps={ 22 | 's32': 4, #Signed 32bit int, 4 bytes of size 23 | 'u32': 4, #Unsigned 32bit int 24 | 'f32': 4, #Floating point 32bit 25 | 'u16': 2, #Unsigned 16bit int 26 | 'u8': 1, #Unsigned 8bit int 27 | 's8': 1, #Signed 8bit int 28 | 'hzn': 12 #Unknown, 12 bytes of.. something 29 | } 30 | 31 | 32 | 33 | 34 | def get_data(data): 35 | return_dict={} 36 | 37 | #additional var 38 | passed_data = data 39 | 40 | for i in data_types: 41 | d_type = data_types[i]#checks data type (s32, u32 etc.) 42 | jump=jumps[d_type]#gets size of data 43 | current = passed_data[:jump]#gets data 44 | 45 | decoded = 0 46 | #complicated decoding for each type of data 47 | if d_type == 's32': 48 | decoded = int.from_bytes(current, byteorder='little', signed = True) 49 | elif d_type == 'u32': 50 | decoded = int.from_bytes(current, byteorder='little', signed=False) 51 | elif d_type == 'f32': 52 | decoded = struct.unpack('f', current)[0] 53 | elif d_type == 'u16': 54 | decoded = struct.unpack('H', current)[0] 55 | elif d_type == 'u8': 56 | decoded = struct.unpack('B', current)[0] 57 | elif d_type == 's8': 58 | decoded = struct.unpack('b', current)[0] 59 | 60 | #adds decoded data to the dict 61 | return_dict[i] = decoded 62 | 63 | 64 | #removes already read bytes from the variable 65 | passed_data = passed_data[jump:] 66 | 67 | 68 | 69 | #returns the dict 70 | return return_dict 71 | 72 | 73 | #setting up an udp server 74 | sock = socket.socket(socket.AF_INET, # Internet 75 | socket.SOCK_DGRAM) # UDP 76 | 77 | sock.bind((UDP_IP, UDP_PORT)) 78 | 79 | 80 | 81 | def fetch_forza_data(): 82 | data, addr = sock.recvfrom(1500) # buffer size is 1500 bytes, this line reads data from the socket 83 | ##received data is now in the retuturned_data dict, key names are in data_format.txt 84 | returned_data = get_data(data) 85 | return returned_data 86 | 87 | 88 | def clear(): 89 | # for windows 90 | if name == 'nt': 91 | _ = system('cls') 92 | 93 | # for mac and linux(here, os.name is 'posix') 94 | else: 95 | _ = system('clear') 96 | 97 | if __name__ == "__main__": 98 | while True: 99 | d = fetch_forza_data() 100 | print(d['Boost']) 101 | 102 | 103 | -------------------------------------------------------------------------------- /replay.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import base64 3 | import sys 4 | import can 5 | import time 6 | from pynput import keyboard 7 | 8 | from multiprocessing.pool import ThreadPool as Pool 9 | 10 | gap = 0.01 11 | 12 | start = time.time() 13 | 14 | # VARIABLES 15 | # change the channel and bitrate as needed 16 | bus = can.interface.Bus(bustype='slcan', channel='COM3', bitrate=500000) 17 | 18 | data = [] 19 | 20 | def send_msg(id, ts, data, verbose=True): 21 | try: 22 | msg = can.Message(timestamp = ts, arbitration_id=id, 23 | data=data, 24 | is_extended_id=False) 25 | 26 | 27 | 28 | bus.send(msg) 29 | if (verbose): 30 | print(msg) 31 | except can.CanError: 32 | print(" {} - Message NOT sent".format(str(hex(id)))) 33 | 34 | with open(sys.argv[1], newline='') as f: 35 | reader = csv.reader(f) 36 | for row in reader: 37 | data.append(row) 38 | 39 | def send_on(): 40 | while(True): 41 | send_msg(0x3B3, start, [0x40, 0x8B, 0x02, 0x0b, 0x18, 0x05, 0xC0, 0xE2], verbose=False) 42 | time.sleep(0.16) 43 | 44 | 45 | def replay(data): 46 | for row in data: 47 | id = int(row[1],16) 48 | ts = float(row[0]) 49 | #convert the b64 to hex 50 | c = base64.b64decode(row[6]).hex() 51 | # convert the hex string into an int range 52 | data = [int(c[i:i+2],16) for i in range(0,len(c),2)] 53 | # replace that in the function 54 | send_msg(id, ts, data) 55 | 56 | # 4 Feb 2024 - add ability to interact with cluster during a replay 57 | def arrows_test(direction): 58 | UP = 0x08 59 | DOWN = 0x01 60 | LEFT = 0x02 61 | RIGHT = 0x04 62 | ENTER = 0x10 63 | data = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] 64 | 65 | if(direction == 'up'): 66 | data[0] = UP 67 | elif(direction == 'down'): 68 | data[0] = DOWN 69 | elif(direction == 'left'): 70 | data[0] = LEFT 71 | elif(direction == 'right'): 72 | data[0] = RIGHT 73 | elif(direction == 'enter'): 74 | data[0] = ENTER 75 | 76 | send_msg(0x81, start, data, verbose=False) 77 | print('') 78 | 79 | def on_press(key): 80 | try: 81 | k = key.char # single-char keys 82 | except: 83 | k = key.name # other keys 84 | if k in ['up', 'down', 'left', 'right', 'enter']: # keys of interest 85 | # self.keys.append(k) # store it in global-like variable 86 | print('Key pressed: ' + k) 87 | arrows_test(k) 88 | #return k # stop listener; remove this if want more keys 89 | 90 | def keys(): 91 | listener = keyboard.Listener(on_press=on_press) 92 | listener.start() # start to listen on a separate thread 93 | listener.join() # remove if main thread is polling self.keys 94 | 95 | data.remove(data[0]) # remove header 96 | 97 | pool = Pool(3) 98 | #pool.apply_async(send_on) # send the 0x3B3 on command at 0.16 interval to keep cluster alive 99 | pool.apply_async(replay, (data,)) # replay the data from log at a slower pace to rev eng 100 | pool.apply_async(keys) # send whatever key is pressed to send to the cluster. 101 | pool.close() 102 | pool.join() -------------------------------------------------------------------------------- /database.md: -------------------------------------------------------------------------------- 1 | # Ford CAN Database 2 | Vehicles Tested: 2015 Mustang GT Premium / 2016 Ford Mustang GT350 3 | 4 | 5 | **Notes**: 6 | * All byte references start at 0, so the range is 0-7 7 | 8 | # 0x4C - Airbag / Seatbelt Indicators 9 | Sample Message: `10 AF FF 00 00 00 00 00` 10 | ``` 11 | Byte 0 - 12 | 0x0_ - Airbag Indicator Off 13 | 0x4_ - Airbag Indicator On 14 | 0x8_ - Airbag Indicator Flashing 15 | Byte 1 - Seatbelt: 16 | 0x00 (no icon); 17 | 0xAF both seltbeats undone 18 | 0x5F pass / driver seatbelts on 19 | 0x6F driver only seatbelt on 20 | 0x9F pass only seatbelt on 21 | ``` 22 | 23 | # 0x81 - Steering Wheel Buttons 24 | Sample Message: `00 00 00 00 00 00 00 00` 25 | ``` 26 | Byte 1 27 | 0x02 - left arrow 28 | 0x04 - right arrow 29 | 0x08 - up arrow 30 | 0x01 - down arrow 31 | 0x10 - OK button 32 | ``` 33 | 34 | # 0x82 - Misc 35 | Sample Message: `82 00 14 40 7E 00 64 FF` 36 | ``` 37 | Byte 2 & 3 - Fuel Consumption ?? 38 | Byte 6 - Steering Mode 39 | 0x40 - Normal 40 | 0x44 - Sport 41 | 0x48 - Comfort 42 | ``` 43 | 44 | # 0x83 - Clockspring 45 | Sample Message: `00 E0 80 00 00 00 00 00` 46 | ``` 47 | Byte 0 & 4 change based on the high beams status 48 | 00 & 00 - off 49 | 40 & 08 - flash high beams 50 | 00 & 10 - high beam switch activated 51 | 80 & 00 - high beams on 52 | 53 | Byte 0 also changes if the turn signal lever is depressed, though this does not appear to trigger the actual turn signal indicators on the cluster 54 | 00 - off 55 | 10 - left signal 56 | 50 - left signal & flash high beams (add first digits) 57 | 20 - right signal 58 | ``` 59 | 60 | # 0x156 - Coolant & Oil Temp Gauge 61 | Sample Message: `9E 99 00 00 03 00 00 00` 62 | ``` 63 | Byte 0 is for the Engine/Coolant Temp Gauge (seen on analog cluster under RPM) 64 | Temp is in Celsius, int(byte0) - 60 = Temp, thus A0 would be 106c or 320f 65 | Gauge doesn't start to move until around 0x80 66 | Gauge seems to have a safe zone and 'freezes' from around 0xA6 (106c) to 0xC0 (132c) where it then rapidly ramps up and can trigger overheat warning 67 | Byte 1 is for the Oil Temp Gauge (seen under center screen, Gauge Mode > Oil Temp) 68 | Temp is in Celsius, int(byte0) - 60 = Temp 69 | Gauge starts to move at 0x60 (36c) and caps out around 0xDA (158c) 70 | Byte 4 toggles the engine overheat message and maxes engine temp guage (doesn't seem to be necessary as a high byte 0 will also trigger the warning) 71 | 0x03 - normal 72 | 0x08 - overheat 73 | ``` 74 | 75 | # 0x178 - Launch Control / ABS Indicators 76 | Sample Message: `00 00 02 00 0E 88 C6 86` 77 | ``` 78 | Byte 0 79 | 0x2_ - LC Icon 80 | 0x8_ - RPM Icon, LC Fault 81 | 0xA_ - LC Flashing Icon 82 | ``` 83 | 84 | # 0x179 - Warning Suppression 85 | Sample Message: `00 00 00 00 96 00 02 C8` 86 | ``` 87 | Several Warning messages such as fuel service inlet, change oil soon, oil change required. 88 | 89 | I have not dove into what each exact bit does, but this line shuts the above warnings off. 90 | ``` 91 | 92 | # 0x202 - Speed Guage 93 | Sample Message: `00 FA 10 00 60 00 00 00` 94 | ``` 95 | Byte 4 must have a first digit of 6 or the gauge does not work 96 | Bytes 6 & 7 are the speed gauge 97 | Unsure the reasons why but speed in MPH * 175 nets the proper gauge value 98 | ``` 99 | 100 | # 0x204 - RPM Guage 101 | Sample Message: `C0 00 7D 01 4B 00 00 00` 102 | ``` 103 | Bytes 3 & 4 - RPM 104 | Gauge goes from 0 to 4000. Seems to be 1/2 of the actual RPM 105 | If RPM is 5000, the int that is passed should be 2500 (09 C4) 106 | ``` 107 | 108 | # 0x3B3 - IPC On, Door Status, Parking Brake Indicator 109 | Sample Message: `40 88 00 12 10 00 80 02` 110 | 111 | This one message deals with a large amount of data, and the bytes can be added to maintain multiple functions. I have some understanding of what it does but not entirely. 112 | 113 | If this message is not sent, the cluster will not even boot up when power is supplied. 114 | ``` 115 | Byte 0 - Headlamp On/Off 116 | 40 - Off 117 | 44 - On 118 | Byte 1 - Light Mode 119 | 48 - DRL 120 | 88 - Night 121 | 4A - Hazard 122 | Byte 0 & 1 also have to do with if the IPC is on, other notes: 123 | 40 48 is running, doors closed, lights off 124 | 41 48 is running, trunk ajar 125 | Byte 3 has to do with backlight 126 | 0x00 is backlight off 127 | 0x0a is backlight on (mycolor) 128 | 0x10 is backlight on (white) 129 | Byte 6 is Parking Brake On (0x80/0xC0) or Off (0x00) 130 | Byte 7 deals with the doors 131 | First digit 0, 1, 2, 3 is Closed, Passenger Ajar, Driver Ajar, Both Ajar respectively. 132 | Second Digit is 2 for closed or A for Hood Ajar 133 | 02 - All Closed, 32 - Driver/Pass Door Open, 2A - Driver + Hood Doors Open 134 | ``` 135 | # 0x3B5 - Tire Pressure 136 | Sample Message: `00 CE 00 CE 00 CE 00 CE` 137 | 138 | Credit [here](https://github.com/v-ivanyshyn/parse_can_logs/blob/master/Ford%20CAN%20IDs%20Summary.md) 139 | ``` 140 | Byte 1 - Front Left Tire Pressure 141 | Byte 3 - Front Right Tire Pressure 142 | Byte 5 - Rear Right Tire Pressure 143 | Byte 7 - Rear Left Tire Pressure 144 | ``` 145 | 146 | 147 | # 0x3C3 - Misc 1 148 | Sample Message: `40 00 10 00 00 00 80 00` 149 | ``` 150 | Byte 0 151 | 0x4_ -> ? 152 | 0x5_ -> ? 153 | 0x_C -> High Beams Off 154 | 0x_E -> High Beams On 155 | Byte 1 - Dimming? 156 | First Bit: 0, 4, 8, C 157 | Second Bit: C, D, 8 158 | Byte 2 159 | 0x0_ -> Parking Brake Off 160 | 0x1_ -> Parking Brake On 161 | Byte 3 - Parking Brake On Warning / Brake Fluid Level Low warning 162 | Byte 6 163 | 0x00 -> ? 164 | 0x80 -> ? 165 | ``` 166 | 167 | # 0x416 - Misc 2 168 | Sample Message: `50 00 FE 00 01 00 00 00` 169 | ``` 170 | ABS, Traction Control Off, Traction Control Loss Icons, Airbag 171 | 172 | Byte 1 - 173 | 0x2_ - Check Brake System warning 174 | 0x4_ - AdvanceTrac System Warning 175 | Byte 5 has to do with a solid traction control or a flashing icon 176 | 0x00 - Indicator Off 177 | 0x02 - Indicator Solid 178 | 0x0F - Indicator Flashing 179 | 0x80 - TC Off Indicator 180 | 0x18 - ATC Off Indicator 181 | Byte 6 - 182 | 0x0_ - ABS light off 183 | 0x4_ - ABS Solid 184 | 0x8_ - ABS Flash Slow 185 | 0xD_ - ABS Flash Fast 186 | ``` 187 | 188 | # 0x430 - Odometer 189 | Sample Message: `37 01 B9 AA C0 5E 37 5C` 190 | 191 | Note this does not appear to allow arbitrary updates, if the mileage is not close to what is programmed in the cluster then it just shows ------- 192 | ``` 193 | Byte 1, 2, 3 show odometer in kilometers 194 | ``` 195 | 196 | # 0x431 - Transmission Warnings 197 | Sample Message: `05 1D 40 EB B7 83 46 00` 198 | 199 | 200 | ``` 201 | Controls warning messages for 202 | - Transmission Adaptive Mode 203 | - Transmission IndicatMode 204 | - Transmission Warming Up 205 | - Transmission Adjusted 206 | ``` -------------------------------------------------------------------------------- /interactive_brute_force.py: -------------------------------------------------------------------------------- 1 | ''' 2 | CanBus - Interactive Brute Force 3 | Eric Turner 4 | 6 May 2023 5 | 6 | 7 | 8 | 9 | This script allows you use your arrow keys / enter to interface with the cluster. 10 | Also allows you to test brute force options 11 | ''' 12 | import can 13 | import time 14 | from pynput import keyboard 15 | from multiprocessing.pool import ThreadPool as Pool 16 | from random import randint 17 | 18 | 19 | key = None 20 | 21 | start = time.time() 22 | 23 | # VARIABLES 24 | # change the channel and bitrate as needed 25 | bus = can.interface.Bus(bustype='slcan', channel='COM3', bitrate=500000) 26 | speed = 0.1 # speed in ms to send messages 27 | can_ids = (0x37,) # tuple of CAN Ids to brute force. They will all be set to the same 28 | seq_bytes = (4,) # if using sequential, the bytes to increment through 29 | sample_data = [0x37, 0x00, 0x00, 0x00, 0xC0, 0x7A, 0x37, 0x1C] # sample set of data to start with 30 | 31 | # SAMPLE DATA - 2015 Ford Mustang GT - Manual - MyColor 32 | # 200 => [0x00, 0x00, 0x7F, 0xF0, 0x81, 0x57, 0x00, 0x00] 33 | # 204 => [0xC0, 0x00, 0x7D, 0x00, 0x00, 0x00, 0x00, 0x00] 34 | # 3B3 => [0x40, 0x8B, 0x0a, 0x12, 0x18, 0x05, 0xC0, 0xE2] 35 | 36 | def send_msg(id, ts, data, verbose=True): 37 | try: 38 | msg = can.Message(timestamp = time.time() - ts, arbitration_id=id, 39 | data=data, 40 | is_extended_id=False) 41 | bus.send(msg) 42 | if (verbose): 43 | print(msg, end="") 44 | except can.CanError: 45 | print(" {} - Message NOT sent".format(str(hex(id)))) 46 | 47 | def arrows_test(direction): 48 | UP = 0x08 49 | DOWN = 0x01 50 | LEFT = 0x02 51 | RIGHT = 0x04 52 | ENTER = 0x10 53 | data = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] 54 | 55 | if(direction == 'up'): 56 | data[0] = UP 57 | elif(direction == 'down'): 58 | data[0] = DOWN 59 | elif(direction == 'left'): 60 | data[0] = LEFT 61 | elif(direction == 'right'): 62 | data[0] = RIGHT 63 | elif(direction == 'enter'): 64 | data[0] = ENTER 65 | 66 | send_msg(0x81, start, data, verbose=False) 67 | print('') 68 | 69 | def on_press(key): 70 | try: 71 | k = key.char # single-char keys 72 | except: 73 | k = key.name # other keys 74 | if k in ['up', 'down', 'left', 'right', 'enter']: # keys of interest 75 | # self.keys.append(k) # store it in global-like variable 76 | print('Key pressed: ' + k) 77 | arrows_test(k) 78 | #return k # stop listener; remove this if want more keys 79 | 80 | ''' 81 | Generates a random byte from 0x00 to 0xFF in order 82 | Pass a tuple index to modify the sample data to the random byte for a brute force method of determining what the code does 83 | ''' 84 | 85 | 86 | def send_on(): 87 | while(True): 88 | send_msg(0x3B3, start, [0x40, 0x8B, 0x02, 0x0a, 0x18, 0x05, 0xC0, 0xE2], verbose=False) 89 | time.sleep(speed) 90 | 91 | def send_null(): 92 | while(True): 93 | send_msg(0x81, start, [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], verbose=False) 94 | print('') 95 | 96 | time.sleep(speed*2) 97 | 98 | 99 | def send_random_pairs(id, index): 100 | rand = randint(0, 65535) # 0x00 - 0xFF 101 | rand_hex = bytearray(rand.to_bytes(2, 'big')) 102 | data = [0x40, 0x8B, 0x02, 0x12, 0x18, 0x05, 0xC0, 0xE2] 103 | data[index] = rand_hex[0] 104 | send_msg(id, start, data) 105 | print(' - {}'.format(rand_hex[0])) 106 | 107 | ''' 108 | Generates a random byte from 0x00 to 0xFF in order 109 | Pass a tuple index to modify the sample data to the random byte for a brute force method of determining what the code does 110 | ''' 111 | def send_sequential_byte(index, can_ids, speed, data=[00,00,00,00,00,00,00,00]): 112 | for x in range(0x00, 0xFF): 113 | for id in can_ids: 114 | data = data 115 | for i in index: 116 | data[i] = x 117 | send_msg(id, start, data) 118 | print(' - {}'.format(str(x))) 119 | time.sleep(speed) 120 | 121 | # m#erged bruteforce.py into here 122 | ''' 123 | Similar to the above function, except this one will increment over multiple byte ranges. 124 | So it will treat multiple bytes as one large number and increment it all together. 125 | ''' 126 | def send_incremental_bytes(starting_index, starting_byte, num_bytes, can_ids, speed, data=[00,00,00,00,00,00,00,00]): 127 | min = starting_byte 128 | max = int('0x' + ('FF' * num_bytes), 16) 129 | for x in range(min, max): 130 | new_bytes = x.to_bytes(num_bytes, 'big') 131 | for id in can_ids: 132 | for i in range(len(new_bytes)): 133 | data[i + (starting_index)] = new_bytes[i] # replace bytes 134 | send_msg(id, start, data) 135 | print(' - {}'.format(str(x))) 136 | time.sleep(speed) 137 | # this method allows us to also run the brute force commands but still use arrows 138 | # this way I can start experimenting with digital gauges such as oil pressure or air/fuel ratio 139 | def brute_force(): 140 | while(True): 141 | #send_sequential_byte(seq_bytes, can_ids, 00.16, sample_data) 142 | 143 | send_incremental_bytes( 144 | starting_index=1 , #0 tart 145 | starting_byte = 0x006572, 146 | num_bytes= 3, # 1 start 147 | can_ids=can_ids, 148 | speed=0.16, 149 | data=sample_data 150 | ) 151 | 152 | 153 | def send_0x5__series(): 154 | x5xx_SERIES_1 = 0x581 155 | x5xx_SERIES_2 = 0x596 156 | x5xx_SERIES_3 = 0x59E 157 | x5xx_SERIES_4 = 0x5B3 158 | x5xx_SERIES_5 = 0x5B5 159 | # not sure what these do but since they seem to be hard coded, gonna send them 160 | send_msg(x5xx_SERIES_1, start, [0x81, 00, 0xFF,0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 161 | send_msg(x5xx_SERIES_2, start, [0x96, 00, 0xFF,0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 162 | send_msg(x5xx_SERIES_3, start, [0x9E, 00, 0xFF,0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 163 | send_msg(x5xx_SERIES_4, start, [0xB3, 00, 0xFF,0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 164 | send_msg(x5xx_SERIES_5, start, [0xB5, 00, 0xFF,0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 165 | 166 | def keys(): 167 | listener = keyboard.Listener(on_press=on_press) 168 | listener.start() # start to listen on a separate thread 169 | listener.join() # remove if main thread is polling self.keys 170 | 171 | 172 | 173 | pool = Pool(5) 174 | pool.apply_async(send_on) # send the 0x3B3 on command 175 | pool.apply_async(send_null) # send a blank version of 0x81 176 | pool.apply_async(keys) # send whatever key is pressed to send to the cluster. 177 | pool.apply_async(brute_force) # test out our brute force data, comment this out if not needed 178 | #pool.apply_async(send_0x5__series) 179 | pool.close() 180 | pool.join() -------------------------------------------------------------------------------- /s550_cluster.py: -------------------------------------------------------------------------------- 1 | import can 2 | import time 3 | from multiprocessing.pool import ThreadPool as Pool 4 | from pynput import keyboard 5 | import sys 6 | import games.fh5 7 | import games.ats 8 | 9 | can_adapter_channel = 'COM3' 10 | retry_period = 3 # seconds 11 | 12 | pool_size = 8 13 | # start time for timestamps 14 | start = time.time() 15 | # different messages have different polling intervals 16 | # this can be determined by taking one of the converted logs and doing a pivot on id, count(id). Sort by count(id). Notice the groupings of messages 17 | # the higher the speed, the more common it is / the more times that ID is visible in CAN traffic 18 | 19 | # this will easily speed up or slow down the traffic while still keeping the speeds at the same interval 20 | # < 1 speeds up; > 1 slows down 21 | modifier = .1 22 | SPEED_ONE = 0.16 * modifier 23 | SPEED_TWO = 0.32 * modifier 24 | SPEED_THREE = 0.48 * modifier 25 | SPEED_FOUR = 0.64 * modifier 26 | SPEED_FIVE = 0.80 * modifier 27 | SPEED_SIX = 1.6 * modifier 28 | SPEED_SEVEN = 3.2 * modifier 29 | SPEED_EIGHT = 6.4 * modifier 30 | 31 | #CAN IDs 32 | RPM = 0x204 33 | SPEED = 0x202 34 | DOOR_STATUS = 0x3B3 35 | SEATBELT = 0x4C 36 | ODOMETER = 0x430 37 | BUTTONS = 0x81 38 | TIRE_PRESSURE = 0x3B5 # thanks to v-ivanyshyn's work 39 | CLIMATE_CONTROL = 0x326 40 | CLIMATE_FAN = 0x35E 41 | STEERING = 0x76 42 | MISC_1 = 0x3C3 43 | MISC_2 = 0x416 44 | MISC_3 = 0x217 45 | MISC_4 = 0x85 46 | MISC_5 = 0x91 47 | MISC_6 = 0x92 48 | MISC_7 = 0x200 49 | MISC_8 = 0x167 50 | MISC_9 = 0x415 51 | MISC_10 = 0x130 52 | MISC_11 = 0x77 53 | MISC_12 = 0x82 # thanks to v-ivanyshyn's work 54 | MISC_13 = 0x230 55 | APIM_1 = 0x044 # https://github.com/EtoTen/stm32_can_sim/blob/main/src/main.cpp 56 | APIM_2 = 0x048 57 | APIM_3 = 0x109 58 | # these 5 are odd ones. The first byte is always the digits that come after the 0x5-- 59 | # the rest of the bytes are 00 FF FF FF FF FF FF. 60 | x5xx_SERIES_1 = 0x581 61 | x5xx_SERIES_2 = 0x596 62 | x5xx_SERIES_3 = 0x59E 63 | x5xx_SERIES_4 = 0x5B3 64 | x5xx_SERIES_5 = 0x5B5 65 | LC = 0x178 66 | WARNINGS_1 = 0x179 67 | ENGINE_TEMP = 0x156 68 | 69 | 70 | 71 | # Stock slcan firmware on Windows 72 | bus = can.ThreadSafeBus(bustype='slcan', channel=can_adapter_channel, bitrate=500000) 73 | 74 | 75 | def send_msg(id, ts, data): 76 | try: 77 | msg = can.Message(timestamp = time.time() - ts, arbitration_id=id, 78 | data=data, 79 | is_extended_id=False) 80 | bus.send(msg) 81 | print(msg, end="") 82 | except can.CanError: 83 | print(" {} - Message NOT sent".format(str(hex(id)))) 84 | 85 | 86 | def send_seatbelt_icon(clusterdata): 87 | ''' 88 | Byte 0 - 89 | 0x0_ - Airbag Indicator Off 90 | 0x4_ - Airbag Indicator On 91 | 0x8_ - Airbag Indicator Flashing 92 | Byte 1 - Seatbelt: 93 | 0xAF both seltbeats undone 94 | 0x5F pass / driver seatbelts on 95 | 0x6F driver only seatbelt on 96 | 0x9F pass only seatbelt on 97 | ''' 98 | if(clusterdata.icon_seatbelt == 0): 99 | seatbelt = 0x5F 100 | else: 101 | seatbelt = 0xAF 102 | data = [0x10, seatbelt, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00] 103 | return send_msg(SEATBELT, start, data) 104 | 105 | def send_misc_1(clusterdata): 106 | ''' 107 | Bunch of unknown functions. Can set off vehcile alarm, high beam icon, parking brake message 108 | Byte 0 109 | 0x4_ -> ? 110 | 0x5_ -> ? 111 | 0x_C -> High Beams Off 112 | 0x_E -> High Beams On 113 | Byte 1 - Dimming? 114 | First Bit: 0, 4, 8, C 115 | Second Bit: C, D, 8 116 | Byte 2 117 | 0x0_ -> Parking Brake Off 118 | 0x1_ -> Parking Brake On 119 | Byte 3 - Parking Brake On Warning / Brake Fluid Level Low warning 120 | Byte 6 121 | 0x00 -> ? 122 | 0x80 -> ? 123 | ''' 124 | if(clusterdata.icon_high_beams == 0): 125 | high_beams = 0x4C # off 126 | else: 127 | high_beams = 0x4E # on 128 | if(clusterdata.icon_parking_brake == 0): 129 | parking_brake_light = 0x00 # off 130 | else: 131 | parking_brake_light = 0x10 # on 132 | 133 | data = [high_beams, 0x48, parking_brake_light, 0x01, 0x00, 0x00, 0x00, 0x00] 134 | return send_msg(MISC_1, start, data) 135 | 136 | def send_misc_2(clusterdata): 137 | ''' 138 | ABS, Traction Control Off, Traction Control Loss Icons, Airbag 139 | 140 | Byte 1 - 141 | 0x2_ - Check Brake System warning 142 | 0x4_ - AdvanceTrac System Warning 143 | Byte 5 has to do with a solid traction control or a flashing icon 144 | 0x00 - Off 145 | 0x02 - Solid 146 | 0x0F - Flashing 147 | 0x80 - TC Off Indicator 148 | 0x18 - ATC Off Indicator 149 | Byte 6 - 150 | 0x0_ - ABS light off 151 | 0x4_ - ABS Solid 152 | 0x8_ - ABS Flash Slow 153 | 0xD_ - ABS Flash Fast 154 | ''' 155 | if(clusterdata.icon_traction_control == 2): 156 | traction_control = 0x0F # flashing 157 | elif(clusterdata.icon_traction_control == 1): 158 | traction_control = 0x02 # solid on 159 | else: 160 | traction_control = 0x00 # off 161 | 162 | if(clusterdata.icon_abs == 2): 163 | abs_icon = 0xD0 # flashing 164 | elif(clusterdata.icon_abs == 1): 165 | abs_icon = 0x40 # solid on 166 | else: 167 | abs_icon = 0x00 # off 168 | 169 | 170 | data = [00, 00, 00, 00, 00, traction_control, abs_icon, 00] 171 | return send_msg(MISC_2, start, data) 172 | 173 | def send_misc_3(clusterdata): 174 | data = [0x1A, 0x58, 0x1A, 0x58, 0x1A, 0x84, 0x1A, 0x74] 175 | return send_msg(MISC_3, start, data) 176 | 177 | def send_misc_4(clusterdata): 178 | data = [0x7C, 0xFF, 0x80, 0x00, 0x7A, 0x40, 0x7C, 0xEA] 179 | return send_msg(MISC_4, start, data) 180 | 181 | def send_misc_5(clusterdata): 182 | data = [0x7E, 0xE3, 0x7F, 0x39, 0x3D, 0x93, 0xF0, 0x00] 183 | return send_msg(MISC_5, start, data) 184 | 185 | def send_misc_6(clusterdata): 186 | data = [0x6F, 0xBA, 0x6F, 0x92, 0x73, 0x62, 0x94, 0x93] 187 | return send_msg(MISC_6, start, data) 188 | 189 | def send_misc_7(clusterdata): 190 | data = [0x00, 0x00, 0x80, 0x47, 0x80, 0x47, 0x00, 0x00] 191 | return send_msg(MISC_7, start, data) 192 | 193 | def send_misc_8(clusterdata): 194 | ''' 195 | CAN ID: 0x167 196 | Byte 0 & 1 197 | Frist Byte is 0x7F, can increase to 0x80 198 | First Bit: 3, 4, F 199 | 200 | Byte 5 & 6: 5 Feb 24, Possibly the MAF sensor which can be used for boost/vacuum 201 | ??? Gauge related? Seems to have increased and decreased when driving 202 | First Byte is 0x19 - 0x1A 203 | First Bit: 1 204 | Second Bit : 9, A 205 | Third Bit: 0 - F 206 | ''' 207 | 208 | 209 | data = [0x72, 0x7F, 0x4B, 0x00, 0x00, 0x1A, 0xED, 0x00] 210 | return send_msg(MISC_8, start, data) 211 | 212 | def send_misc_9(clusterdata): 213 | ''' 214 | 215 | ''' 216 | data = [00, 0x00, 0xD0, 0xFA, 0x0F, 0xFE, 0x0F, 0xFE] 217 | return send_msg(MISC_9, start, data) 218 | 219 | def send_misc_10(clusterdata): 220 | ''' 221 | 222 | ''' 223 | data = [0x82, 0x00, 0x14, 0x40, 0x7B, 0x00, 0x64, 0xFF] 224 | return send_msg(MISC_10, start, data) 225 | 226 | def send_misc_11(clusterdata): 227 | ''' 228 | Byte 6 - Hill Start Assist Not Available Warning 229 | 0x_c - displays warning 230 | 0x_6 - shuts warning up 231 | ''' 232 | data = [0x00, 0x00, 0x07, 0xFF, 0x7F, 0xF7, 0xE6, 0x02] 233 | return send_msg(MISC_11, start, data) 234 | 235 | # 4 Feb 2024 236 | def send_misc_12(clusterdata): 237 | ''' 238 | Byte 2 & 3 - Fuel Consumption ?? 239 | Byte 6 - Steering Mode 240 | 0x40 - Normal 241 | 0x44 - Sport 242 | 0x48 - Comfort 243 | ''' 244 | data = [0x5C, 0x00, 0x14, 0x98, 0xAF, 0x00, 0x44, 0xFF] 245 | return send_msg(MISC_12, start, data) 246 | 247 | def send_misc_13(clusterdata): 248 | ''' 249 | Byte 0 & 1 ?? 250 | ''' 251 | data = [0xF0, 0x1C, 0x00, 0x00, 0x4B, 0x00, 0x00, 0x00] 252 | return send_msg(MISC_13, start, data) 253 | 254 | # 10 Feb 2024 - trying to get the APIM to boot, might require this 255 | def send_climate(clusterdata): 256 | data = [0x81, 0x95, 0x02, 0x4A, 0x01, 0xAA, 0x00, 0x00] 257 | return send_msg(CLIMATE_CONTROL, start, data) 258 | 259 | def send_climate_fan(clusterdata): 260 | data = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00] 261 | return send_msg(CLIMATE_FAN, start, data) 262 | 263 | def send_apim_1(clusterdata): 264 | data = [0x88, 0xC0, 0x0C, 0x10, 0x04, 0x00, 0x02, 0x00] 265 | return send_msg(APIM_1, start, data) 266 | 267 | def send_apim_2(clusterdata): 268 | data = [0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0xE0, 0x00] 269 | return send_msg(APIM_2, start, data) 270 | 271 | def send_apim_3(clusterdata): 272 | data = [0x00, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x28] 273 | return send_msg(APIM_3, start, data) 274 | 275 | def send_0x5__series(clusterdata): 276 | # not sure what these do but since they seem to be hard coded, gonna send them 277 | send_msg(x5xx_SERIES_1, start, [0x81, 00, 0xFF,0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 278 | print('') 279 | send_msg(x5xx_SERIES_2, start, [0x96, 00, 0xFF,0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 280 | print('') 281 | send_msg(x5xx_SERIES_3, start, [0x9E, 00, 0xFF,0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 282 | print('') 283 | send_msg(x5xx_SERIES_4, start, [0xB3, 00, 0xFF,0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 284 | print('') 285 | return send_msg(x5xx_SERIES_5, start, [0xB5, 00, 0xFF,0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 286 | 287 | def send_warnings_1(clusterdata): 288 | ''' 289 | Several Warning messages such as fuel service inlet, change oil soon, oil change required 290 | ''' 291 | data = [0x00, 0x00, 0x00, 0x00, 0x96, 0x00, 0x02, 0xC8] 292 | return send_msg(WARNINGS_1, start, data) 293 | 294 | def send_launch_control(clusterdata): 295 | ''' 296 | Byte 0 297 | 0x2_ - LC Icon 298 | 0x8_ - RPM Icon, LC Fault 299 | 0xa_ - LC Flashing Icon 300 | ''' 301 | if(clusterdata.icon_launch_control == 2): 302 | launch_control = 0xA0 # flashing 303 | elif(clusterdata.icon_launch_control == 1): 304 | launch_control = 0x20 # solid on 305 | else: 306 | launch_control = 0x00 # off 307 | data = [launch_control, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] 308 | return send_msg(LC, start, data) 309 | 310 | def send_door_status(clusterdata): 311 | ''' 312 | Byte 0 & 1 have to do with if the IPC is on 313 | 40 48 is running, doors closed, lights off 314 | 41 48 is running, trunk ajar 315 | Byte 3 has to do with backlight 316 | 0x00 is backlight off 317 | 0x0a is backlight on (mycolor) 318 | 0x10 is backlight on (white) 319 | Byte 7 is Parking Brake On (0x80/0xC0) or Off (0x00) 320 | Byte 8 deals with the doors 321 | First digit 0, 1, 2, 3 is Closed, Passenger Ajar, Driver Ajar, Both Ajar respectively. 322 | Second Digit is 2 for closed or A for Hood Ajar 323 | 02 - All Closed, 32 - Driver/Pass Door Open, 2A - Driver + Hood Doors Open 324 | ''' 325 | if(clusterdata.icon_parking_brake == 0): 326 | parking_brake_light = 0x00 # off 327 | else: 328 | parking_brake_light = 0xC0 # on 329 | 330 | if(clusterdata.engine_on == 1): 331 | lights = 0x0A 332 | else: 333 | lights = 0x00 334 | data = [0x40, 0x48, 0x02, lights, 0x18, 0x05, parking_brake_light, 0x02] 335 | return send_msg(DOOR_STATUS, start, data) 336 | 337 | def send_rpm(clusterdata): 338 | ''' 339 | Gauge goes from 0 to 4000. Seems to be 1/2 of the actual RPM 340 | If RPM is 5000, the int that is passed should be 2500 341 | ''' 342 | gauge_max = 4000 343 | 344 | try: 345 | rpm = (gauge_max * int(clusterdata.value_rpm))/clusterdata.value_rpm_max 346 | except: 347 | rpm = 0 # if the game is paused, these values will be 0 and cause a ZeroDivisionError 348 | 349 | rpm_hex = bytearray(int(rpm).to_bytes(2, 'big')) 350 | 351 | #data = [0xC1, 0x5F, 0x7D, rpm_hex[0], rpm_hex[1], 0x00, 0x00, 0x00] 352 | data = [0xC0, 0x69, 0x7D, rpm_hex[0], rpm_hex[1], 0x00, 0x00, 0x00] 353 | return send_msg(RPM, start, data) 354 | 355 | def send_speed(clusterdata): 356 | ''' 357 | Byte 4 must have a first digit of C or the gauge does not work 358 | Bytes 6 & 7 are the speed gauge 359 | ''' 360 | speed = clusterdata.value_speed 361 | gauge_pos = int(speed) * 175 362 | speed_hex = bytearray((gauge_pos).to_bytes(2, 'big')) 363 | #print(" speed: {} HEX: {}; 1: {} 2:{}".format(speed, speed_hex, speed_hex[0], speed_hex[1])) 364 | 365 | data = [0x00, 0x00, 0x00, 0x00, 0x60, 0x00, speed_hex[0], speed_hex[1]] 366 | return send_msg(SPEED, start, data) 367 | 368 | 369 | def send_odometer(clusterdata): 370 | odometer = bytearray((clusterdata.value_odometer).to_bytes(3, 'big')) 371 | data = [0x37, odometer[0], odometer[1], odometer[2], 0xC0, 0x7A, 0x37, 0x1C] 372 | return send_msg(ODOMETER, start, data) 373 | 374 | # 16 Jan 2025 375 | def send_engine_temp(clusterdata): 376 | ''' 377 | Byte 0 is for the Engine/Coolant Temp Gauge (seen on analog cluster under RPM) 378 | Temp is in Celsius, int(byte0) - 60 = Temp, thus A0 would be 106c or 320f 379 | Gauge doesn't start to move until around 0x80 380 | Gauge seems to have a safe zone and 'freezes' from around 0xA6 (106c) to 0xC0 (132c) where it then rapidly ramps up and can trigger overheat warning 381 | Byte 1 is for the Oil Temp Gauge (seen under center screen, Gauge Mode > Oil Temp) 382 | Temp is in Celsius, int(byte0) - 60 = Temp 383 | Gauge starts to move at 0x60 (36c) and caps out around 0xDA (158c) 384 | Byte 4 toggles the engine overheat message and maxes engine temp guage (doesn't seem to be necessary as a high byte 0 will also trigger the warning) 385 | 0x03 - normal 386 | 0x08 - overheat 387 | ''' 388 | oil_temp = int(clusterdata.oil_temp) + 60 389 | engine_temp = int(clusterdata.engine_temp) + 60 390 | data = [engine_temp, oil_temp, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00] 391 | return send_msg(ENGINE_TEMP, start, data) 392 | 393 | # 4 Feb 2024 394 | def send_tire_pressure(clusterdata): 395 | ''' 396 | In kPa 397 | Byte 1 - Front Left Tire Pressure 398 | Byte 3 - Front Right Tire Pressure 399 | Byte 5 - Rear Right Tire Pressure 400 | Byte 7 - Rear Left Tire Pressure 401 | ''' 402 | 403 | tire_pressure_dummy = 0xCE # 30 PSI / 206 kPa 404 | 405 | data = [0x00, tire_pressure_dummy, 0x00, tire_pressure_dummy, 0x00, tire_pressure_dummy, 0x00, tire_pressure_dummy] 406 | return send_msg(TIRE_PRESSURE, start, data) 407 | 408 | # 4 Feb 2024 409 | def send_steering(clusterdata): 410 | ''' 411 | Byte 0 & 1 control steering angle, -0.1 - 1.0 + 1600 412 | ''' 413 | 414 | data = [0x3E, 0x83, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00] 415 | return send_msg(STEERING, start, data) 416 | 417 | def MENU_NAV(direction): 418 | UP = 0x08 419 | DOWN = 0x01 420 | LEFT = 0x02 421 | RIGHT = 0x04 422 | ENTER = 0x10 423 | data = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] 424 | 425 | if(direction == 'up'): 426 | data[0] = UP 427 | elif(direction == 'down'): 428 | data[0] = DOWN 429 | elif(direction == 'left'): 430 | data[0] = LEFT 431 | elif(direction == 'right'): 432 | data[0] = RIGHT 433 | elif(direction == 'enter'): 434 | data[0] = ENTER 435 | else: 436 | data[0] = 0x00 437 | 438 | return send_msg(BUTTONS, start, data) 439 | 440 | def on_press(key): 441 | try: 442 | k = key.char # single-char keys 443 | except: 444 | k = key.name # other keys 445 | if k in ['up', 'down', 'left', 'right', 'enter']: # keys of interest 446 | # self.keys.append(k) # store it in global-like variable 447 | #print('Key pressed: ' + k) 448 | MENU_NAV(k) 449 | print(" - " + 'KEY PRESSED: {} '.format(k) + ' - MENU_NAV') 450 | #return k # stop listener; remove this if want more keys 451 | 452 | def keys(): 453 | listener = keyboard.Listener(on_press=on_press) 454 | listener.start() # start to listen on a separate thread 455 | #listener.join() 456 | 457 | THREADS = [ 458 | (SPEED_ONE, [send_rpm, send_door_status, send_speed, send_apim_1, send_apim_2, send_apim_3]), 459 | (SPEED_TWO, [send_warnings_1, send_seatbelt_icon, send_misc_2, send_misc_8, send_climate, send_climate_fan, send_engine_temp]), 460 | (SPEED_THREE, [send_misc_1, send_launch_control, send_tire_pressure]), 461 | (SPEED_SEVEN, [MENU_NAV, send_0x5__series]) 462 | ] 463 | 464 | def load_game(game): 465 | supported_games = ['fh5', 'ats'] 466 | if(game in supported_games): 467 | #print('Loading {}'.format(game)) 468 | if game == 'fh5': 469 | return games.fh5.data() 470 | elif game == 'ats': 471 | time.sleep(0.1) # trying to poll the API endpoint too much causes it to fail 472 | return games.ats.data() 473 | else: 474 | pass 475 | 476 | # setup the thread loop 477 | def _thread(t, game): 478 | while True: 479 | for m in t[1]: 480 | m(load_game(game)) 481 | print(" - " + '{0:.2f}'.format(t[0]) + ' - ' + m.__name__) 482 | #time.sleep(t[0]) 483 | 484 | 485 | def activate(game=None): 486 | if(game==None): 487 | try: 488 | game = sys.argv[1] 489 | except: 490 | game = 'fh5' # default to fh5 if no param is passed 491 | 492 | 493 | print('* Loading game: {}'.format(game)) 494 | pool = Pool(6) 495 | print('* Starting thread for keyboard navigation') 496 | pool.apply_async(keys) # send whatever key is pressed to send to the cluster 497 | # create a thread for each speed 498 | for speed in THREADS: 499 | print('* Starting thread for speed: {}, functions: {}'.format(speed[0], [m.__name__ for m in speed[1]])) 500 | pool.apply_async(_thread, args=(speed,game), error_callback=print) 501 | pool.close() 502 | pool.join() 503 | 504 | 505 | 506 | if __name__ == "__main__": 507 | activate() --------------------------------------------------------------------------------