Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Linux Version #8

Open
alfredgu001324 opened this issue Dec 15, 2024 · 9 comments
Open

Linux Version #8

alfredgu001324 opened this issue Dec 15, 2024 · 9 comments

Comments

@alfredgu001324
Copy link

Thanks for the amazing work! I am wondering is there a way to run this game and this repo in linux OS? It seems that the current setup is on windows.

Looking forward to your reply!

@dasGringuen
Copy link
Owner

Currently, it is Windows-only. I did try once to install it on Linux using Proton and it was working fine, but we still need to test the plugin interface and adapt the command issuing, which currently uses vJoy (Windows-only).

We are planning to implement this at some point, but if you would like to contribute, you are more than welcome!

@alfredgu001324
Copy link
Author

I see, thanks for the quick reply! Yes I also just downloaded the game and it was working fine, currently trying to make the plugin work using uinput but still in progress. Will let you know if it works!

@alfredgu001324
Copy link
Author

I wonder what is the range of the input when setting: env.set_actions(np.array([steer, throttle, brake])). Is it all -1 to 1 or something else? I noticed that there are several conversions being done when mapping the raw input to self.steer, self.acc and self.brake in car_control.py

@dasGringuen
Copy link
Owner

For the Steering:

Each car has a fixed maximum steering wheel angle that, as far as I know, cannot be changed. For example, the GT3 car has a maximum steering angle of +-320 degrees, while the Dallara has a maximum of +-240 degrees. In ac_client.py, these maximum angles correspond to 1 and -1, which vJoy internally maps to 0 and 2.

In our experiments, we initially scaled the steering input to constrain it slightly. In ac_client.py:

self.scale_steer = 0.6  # An input of 1 was scaled to this value

However, this scaling didn’t have much effect, so you can safely ignore and remove it.

Therefore, in uinput, ensure that the -1 to 1 range from ac_client maps directly to the car's maximum possible steering wheel angle.

When working with human demonstations, we needed a mapping information to correctly load the demonstrations (i.e., to map the steering wheel angle read from telemetry to actions). These values are specified in the configuration file.
For example, see: [steer_map.csv for the Dallara car]

For the Throttle and Brake:

The throttle and brake inputs map -1 to a fully released pedal and 1 to full brake or full throttle.

To Debug:

To debug, check the following variables to get feedback directly from AC, not from vJoy or the input interface:

  • Steering Angle:
    env.state["steerAngle"] – current steering wheel angle

  • Throttle Status:
    env.state["accStatus"] – throttle

  • Brake Status:
    env.state["brakeStatus"] – brake

@alfredgu001324
Copy link
Author

I see, thank you so much! I will take a closer look at them during winter break.

The current setup on linux works for me right now:

  1. Instead of choosing Wheels in Control options, choose xbox.

  2. Install virtual xbox: sudo apt-get install xboxdrv

  3. Turn on virtual xbox: sudo xboxdrv --daemon --silent --mimic-xpad --type xbox360 --dbus disabled

  4. ls /dev/input to see if js0 and event17 are present. If yes, do sudo chmod 666 /dev/input/event17 OR sudo chmod a+rw /dev/input/event17 to give access to modify them. This is used for controlling the virtual xbox.

  5. Replace vjoy.py with the following vjoy_linux.py:

import struct
import time

class vJoyLinux:
    def __init__(self, reference=1):
        self.reference = reference
        self.device = None
        self.acquired = False
        self.js_path = "/dev/input/js0"
        self.event_path = "/dev/input/event17"
        
    def open(self):
        """Open the virtual joystick device"""
        try:
            self.device = open(self.event_path, 'wb')
            self.acquired = True
            return True
        except Exception as e:
            print(f"Failed to open vJoy device: {e}")
            return False

    def close(self):
        """Close the virtual joystick device"""
        try:
            if self.device:
                self.device.close()
            self.acquired = False
            return True
        except Exception as e:
            print(f"Failed to close vJoy device: {e}")
            return False

    def generateJoystickPosition(self, 
                               wThrottle=0, wRudder=0, wAileron=0,
                               wAxisX=0, wAxisY=0, wAxisZ=0,
                               wAxisXRot=0, wAxisYRot=0, wAxisZRot=0,
                               wSlider=0, wDial=0, wWheel=0,
                               wAxisVX=0, wAxisVY=0, wAxisVZ=0,
                               wAxisVBRX=0, wAxisVBRY=0, wAxisVBRZ=0,
                               lButtons=0, bHats=0, bHatsEx1=0, bHatsEx2=0, bHatsEx3=0):
        """Generate a joystick position structure compatible with the original vJoy"""
        joyPosFormat = "BlllllllllllllllllllIIII"
        pos = struct.pack(joyPosFormat, self.reference, wThrottle, wRudder,
                         wAileron, wAxisX, wAxisY, wAxisZ, wAxisXRot, wAxisYRot,
                         wAxisZRot, wSlider, wDial, wWheel, wAxisVX, wAxisVY, wAxisVZ,
                         wAxisVBRX, wAxisVBRY, wAxisVBRZ, lButtons, bHats, bHatsEx1, bHatsEx2, bHatsEx3)
        return pos

    def _send_event(self, event_type, code, value):
        """Send an input event to the device"""
        # Use signed integer format for steering events
        if event_type == 0x02:  # EV_REL
            EVENT_FORMAT = 'llHHi'
        else:
            EVENT_FORMAT = 'llHHi'
        event = struct.pack(EVENT_FORMAT, 0, 0, event_type, code, value)
        self.device.write(event)
        self.device.flush()

    def update(self, joystickPosition):
        """Update the joystick state based on the provided position structure"""
        if not self.device or not self.acquired:
            return False

        try:
            # Unpack the joystick position structure
            values = struct.unpack("BlllllllllllllllllllIIII", joystickPosition)
            
            # EV_ABS for absolute axes events
            EV_REL = 0x02  # For steering
            EV_ABS = 0x03
            
            # Axis codes
            ABS_X = 0x00  # Left stick X (Steering)
            REL_X = 0x00   # Steering (now using relative movement)
            ABS_RZ = 0x05 # Right trigger (Throttle)
            ABS_Z = 0x02  # Left trigger (Brake)
            
            # Map steering (wAxisX)
            steer_value = values[4]
            # Map 0-32768 to -65535 to 65535
            scaled_steer = int(((steer_value / 32768) * 2 - 1) * 65535)
            # Ensure the value stays within -65535 to 65535
            scaled_steer = max(-65535, min(65535, scaled_steer))
            self._send_event(EV_ABS, ABS_X, scaled_steer)
            
            # Map throttle (wAxisY) from 0-32768 to 0-255
            throttle_value = int((values[5] / 32768.0) * 255)
            throttle_value = max(0, min(255, throttle_value))
            self._send_event(EV_ABS, ABS_RZ, throttle_value)
            
            # Map brake (wAxisZ) from 0-32768 to 0-255
            brake_value = int((values[6] / 32768.0) * 255)
            brake_value = max(0, min(255, brake_value))
            self._send_event(EV_ABS, ABS_Z, brake_value)
            
            # Send a synchronization event
            self._send_event(0, 0, 0)
            
            return True
        except Exception as e:
            # breakpoint()
            print(f"Failed to update joystick state: {e}")
            return False

def setJoy(valueX, valueY, valueZ, onButtons, scale):
    """
    Set joystick position with correct input ranges:
    valueX: 0.4 (full left) to 1.6 (full right), 1.0 is center
    valueY: 0 (no throttle) to 1 (full throttle)
    valueZ: 0 (no brake) to 1 (full brake)
    scale: typically 16384
    """
    # Map steering from 0.4-1.6 range to 0-32768 range
    # At 0.4 -> 0
    # At 1.0 -> 16384
    # At 1.6 -> 32768
    normalized_x = (valueX - 0.4) / 1.2  # Convert to 0-1 range
    xPos = int(normalized_x * 32768)
    xPos = max(0, min(32768, xPos))
    
    # Map throttle and brake from 0-1 to 0-32768
    yPos = int(valueY * 32768)
    zPos = int(valueZ * 32768)
    
    vjoy = vJoyLinux()
    if vjoy.open():
        try:
            if onButtons != 0:
                joystickPosition = vjoy.generateJoystickPosition(wAxisX=xPos, wAxisY=yPos, wAxisZ=zPos, lButtons=onButtons)
                vjoy.update(joystickPosition)
                time.sleep(0.01)

            joystickPosition = vjoy.generateJoystickPosition(wAxisX=xPos, wAxisY=yPos, wAxisZ=zPos)
            vjoy.update(joystickPosition)
        finally:
            vjoy.close()
  1. Change the import in car_control.py from vjoy to vJoyLinux. And it should be working.

  2. I have tested this with two checkpoints: python train.py --test --load_path /home/guxunjia/masters_project/assetto_corsa_gym/checkpoints/barcelone/ AssettoCorsa.track=ks_barcelona-layout_gp AssettoCorsa.car=bmw_z4_gt3 and python train.py --test --load_path /home/guxunjia/masters_project/assetto_corsa_gym/checkpoints AssettoCorsa.track=monza AssettoCorsa.car=bmw_z4_gt3, and they are all working fine. So this should be good.

@dasGringuen
Copy link
Owner

Wow, this is great news!

  • Context Manager: Could you install it and it works fine? This is needed to restart the car.
  • Are you getting the same lap time using the checkpoints we provided? Or do you have your own?
  • Maybe better to keep vjoy open? By creating the vJoyLinux object only once?

@alfredgu001324
Copy link
Author

  1. Yes, I did install the Context Manager according to this video, but to run the repo I actually did not use it, because the launch page in Context Manager is different from the default launch page. And there are a few missing items in the Context Manager that I cannot change (for example the automatic gearbox selection).

  2. Using the ckpts on hugging face, I have the following laptime:

  • BMW_z4_gt3 + Monza: 1:52.952
  • BMW_z4_gt3 + Barcelona-GP: 1:51.053
  1. Uhmm what does this mean? I simply change the import and define the object only once in init.

@dasGringuen
Copy link
Owner

Cool! It's doing a slightly slower lap time for some reason. I’ll give it a try. Maybe training from scratch.

  1. Sorry, my bad. I thought we were calling the setJoy function from vjoy.py directly, not the one inside the class. Actually, that function outside the class should be removed since we’re not using it.

I’ll merge this and add you as a contributor. Or, if you prefer, go ahead and make a pull request.

Thanks!

@alfredgu001324
Copy link
Author

  1. Sounds good! Yes please give it a try, I did not really "triple" check it so I might be wrong somewhere too.
  2. Lol yes, I just left it there for completeness. Feel free to remove it from my script.
  3. Thank you so much! If you can test it and make sure it works, I am more than happy to be named as a contributor haha.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants