mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-11-03 00:53:44 +08:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ccab8e0003 | |||
| e50154f43d | |||
| 254076de39 | |||
| 2f836d1148 | |||
| 0ac40bfa14 | |||
| 5415413076 | |||
| 58fcda8cb5 | |||
| f1e070281b | |||
| 92b1284e2c | |||
| bd471b3498 | |||
| 928d887820 | |||
| 6ff7af3b5a | |||
| 847778bf36 | |||
| c0a4010bd1 | |||
| 79a0dabc43 | |||
| 4e58a18a4d | |||
| 60b191d2ae | |||
| 4dadc6d42b | |||
| d32dc0446a | |||
| 2b3f163ec0 | |||
| 6fcae63610 | |||
| e0beb9d692 | |||
| 6329edc833 | |||
| 4e5248fdc9 | |||
| bb3b9724b7 | |||
| 5f0cff7ed5 | |||
| 268f6319b4 | |||
| b52fa7525d | |||
| 4fe2d683e1 | |||
| 233076c809 |
@@ -10,13 +10,6 @@ $Cxx.namespace("cereal");
|
||||
# DO rename the structs
|
||||
# DON'T change the identifier (e.g. @0x81c2f05a394cf4af)
|
||||
|
||||
enum LongitudinalPersonalitySP {
|
||||
aggressive @0;
|
||||
standard @1;
|
||||
relaxed @2;
|
||||
overtake @3;
|
||||
}
|
||||
|
||||
struct ModularAssistiveDrivingSystem {
|
||||
state @0 :ModularAssistiveDrivingSystemState;
|
||||
enabled @1 :Bool;
|
||||
@@ -34,7 +27,6 @@ struct ModularAssistiveDrivingSystem {
|
||||
|
||||
struct SelfdriveStateSP @0x81c2f05a394cf4af {
|
||||
mads @0 :ModularAssistiveDrivingSystem;
|
||||
personality @1 :LongitudinalPersonalitySP;
|
||||
}
|
||||
|
||||
struct ModelManagerSP @0xaedffd8f31e7b55d {
|
||||
@@ -97,8 +89,6 @@ struct ModelManagerSP @0xaedffd8f31e7b55d {
|
||||
struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
||||
dec @0 :DynamicExperimentalControl;
|
||||
|
||||
personalityDEPRECATED @1 :LongitudinalPersonalitySP;
|
||||
|
||||
struct DynamicExperimentalControl {
|
||||
state @0 :DynamicExperimentalControlState;
|
||||
enabled @1 :Bool;
|
||||
|
||||
@@ -167,4 +167,7 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"HyundaiRadarTracksToggle", PERSISTENT},
|
||||
|
||||
{"DynamicExperimentalControl", PERSISTENT},
|
||||
|
||||
// Tuning keys
|
||||
{"HkgTuningAngleSmoothingFactor", PERSISTENT | BACKUP}
|
||||
};
|
||||
|
||||
+1
-1
Submodule opendbc_repo updated: c10bc5bd85...6f05e7d5a2
+1
-2
@@ -58,7 +58,7 @@ dependencies = [
|
||||
"future-fstrings",
|
||||
|
||||
# joystickd
|
||||
"inputs",
|
||||
"pygame",
|
||||
|
||||
# these should be removed
|
||||
"psutil",
|
||||
@@ -105,7 +105,6 @@ dev = [
|
||||
"matplotlib",
|
||||
"parameterized >=0.8, <0.9",
|
||||
"pyautogui",
|
||||
"pygame",
|
||||
"pyopencl; platform_machine != 'aarch64'", # broken on arm64
|
||||
"pytools < 2024.1.11; platform_machine != 'aarch64'", # pyopencl use a broken version
|
||||
"pywinctl",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import os
|
||||
import time
|
||||
import numpy as np
|
||||
from cereal import custom
|
||||
from cereal import log
|
||||
from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
@@ -58,23 +58,23 @@ STOP_DISTANCE = 6.0
|
||||
CRUISE_MIN_ACCEL = -1.2
|
||||
CRUISE_MAX_ACCEL = 1.6
|
||||
|
||||
def get_jerk_factor(personality=custom.LongitudinalPersonalitySP.standard):
|
||||
if personality==custom.LongitudinalPersonalitySP.relaxed:
|
||||
def get_jerk_factor(personality=log.LongitudinalPersonality.standard):
|
||||
if personality==log.LongitudinalPersonality.relaxed:
|
||||
return 1.0
|
||||
elif personality==custom.LongitudinalPersonalitySP.standard:
|
||||
elif personality==log.LongitudinalPersonality.standard:
|
||||
return 1.0
|
||||
elif personality==custom.LongitudinalPersonalitySP.aggressive:
|
||||
elif personality==log.LongitudinalPersonality.aggressive:
|
||||
return 0.5
|
||||
else:
|
||||
raise NotImplementedError("Longitudinal personality not supported")
|
||||
|
||||
|
||||
def get_T_FOLLOW(personality=custom.LongitudinalPersonalitySP.standard):
|
||||
if personality==custom.LongitudinalPersonalitySP.relaxed:
|
||||
def get_T_FOLLOW(personality=log.LongitudinalPersonality.standard):
|
||||
if personality==log.LongitudinalPersonality.relaxed:
|
||||
return 1.75
|
||||
elif personality==custom.LongitudinalPersonalitySP.standard:
|
||||
elif personality==log.LongitudinalPersonality.standard:
|
||||
return 1.45
|
||||
elif personality==custom.LongitudinalPersonalitySP.aggressive:
|
||||
elif personality==log.LongitudinalPersonality.aggressive:
|
||||
return 1.25
|
||||
else:
|
||||
raise NotImplementedError("Longitudinal personality not supported")
|
||||
@@ -274,7 +274,7 @@ class LongitudinalMpc:
|
||||
for i in range(N):
|
||||
self.solver.cost_set(i, 'Zl', Zl)
|
||||
|
||||
def set_weights(self, prev_accel_constraint=True, personality=custom.LongitudinalPersonalitySP.standard):
|
||||
def set_weights(self, prev_accel_constraint=True, personality=log.LongitudinalPersonality.standard):
|
||||
jerk_factor = get_jerk_factor(personality)
|
||||
if self.mode == 'acc':
|
||||
a_change_cost = A_CHANGE_COST if prev_accel_constraint else 0
|
||||
@@ -327,7 +327,7 @@ class LongitudinalMpc:
|
||||
lead_xv = self.extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau)
|
||||
return lead_xv
|
||||
|
||||
def update(self, radarstate, v_cruise, x, v, a, j, personality=custom.LongitudinalPersonalitySP.standard):
|
||||
def update(self, radarstate, v_cruise, x, v, a, j, personality=log.LongitudinalPersonality.standard):
|
||||
t_follow = get_T_FOLLOW(personality)
|
||||
v_ego = self.x0[1]
|
||||
self.status = radarstate.leadOne.status or radarstate.leadTwo.status
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import math
|
||||
import numpy as np
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
@@ -162,11 +163,9 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
if force_slow_decel:
|
||||
v_cruise = 0.0
|
||||
|
||||
self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveStateSP'].personality)
|
||||
self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality)
|
||||
self.mpc.set_cur_state(self.v_desired_filter.x, self.a_desired)
|
||||
self.mpc.update(sm['radarState'], v_cruise, x, v, a, j, personality=sm['selfdriveStateSP'].personality)
|
||||
|
||||
|
||||
self.mpc.update(sm['radarState'], v_cruise, x, v, a, j, personality=sm['selfdriveState'].personality)
|
||||
|
||||
self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution)
|
||||
self.a_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.a_solution)
|
||||
@@ -194,7 +193,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
def publish(self, sm, pm):
|
||||
plan_send = messaging.new_message('longitudinalPlan')
|
||||
|
||||
plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState', 'selfdriveState', 'selfdriveStateSP'])
|
||||
plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState', 'selfdriveState'])
|
||||
|
||||
longitudinalPlan = plan_send.longitudinalPlan
|
||||
longitudinalPlan.modelMonoTime = sm.logMonoTime['modelV2']
|
||||
|
||||
@@ -19,7 +19,7 @@ def main():
|
||||
ldw = LaneDepartureWarning()
|
||||
longitudinal_planner = LongitudinalPlanner(CP)
|
||||
pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'longitudinalPlanSP'])
|
||||
sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState', 'selfdriveStateSP'],
|
||||
sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState'],
|
||||
poll='modelV2')
|
||||
|
||||
while True:
|
||||
|
||||
@@ -32,7 +32,7 @@ REPLAY = "REPLAY" in os.environ
|
||||
SIMULATION = "SIMULATION" in os.environ
|
||||
TESTING_CLOSET = "TESTING_CLOSET" in os.environ
|
||||
IGNORE_PROCESSES = {"loggerd", "encoderd", "statsd"}
|
||||
LONGITUDINAL_PERSONALITY_MAP = {v: k for k, v in custom.LongitudinalPersonalitySP.schema.enumerants.items()}
|
||||
LONGITUDINAL_PERSONALITY_MAP = {v: k for k, v in log.LongitudinalPersonality.schema.enumerants.items()}
|
||||
|
||||
ThermalStatus = log.DeviceState.ThermalStatus
|
||||
State = log.SelfdriveState.OpenpilotState
|
||||
@@ -475,6 +475,7 @@ class SelfdriveD(CruiseHelper):
|
||||
ss.state = self.state_machine.state
|
||||
ss.engageable = not self.events.contains(ET.NO_ENTRY)
|
||||
ss.experimentalMode = self.experimental_mode
|
||||
ss.personality = self.personality
|
||||
|
||||
ss.alertText1 = self.AM.current_alert.alert_text_1
|
||||
ss.alertText2 = self.AM.current_alert.alert_text_2
|
||||
@@ -503,7 +504,6 @@ class SelfdriveD(CruiseHelper):
|
||||
mads.enabled = self.mads.enabled
|
||||
mads.active = self.mads.active
|
||||
mads.available = self.mads.enabled_toggle
|
||||
ss_sp.personality = self.personality
|
||||
|
||||
self.pm.send('selfdriveStateSP', ss_sp_msg)
|
||||
|
||||
@@ -532,7 +532,7 @@ class SelfdriveD(CruiseHelper):
|
||||
try:
|
||||
return int(self.params.get('LongitudinalPersonality'))
|
||||
except (ValueError, TypeError):
|
||||
return custom.LongitudinalPersonalitySP.standard
|
||||
return log.LongitudinalPersonality.standard
|
||||
|
||||
def params_thread(self, evt):
|
||||
while not evt.is_set():
|
||||
|
||||
@@ -25,7 +25,6 @@ class Plant:
|
||||
Plant.car_state = messaging.pub_sock('carState')
|
||||
Plant.plan = messaging.sub_sock('longitudinalPlan')
|
||||
Plant.messaging_initialized = True
|
||||
Plant.selfdrive_state_sp = messaging.pub_sock('selfdriveStateSP')
|
||||
|
||||
self.v_lead_prev = 0.0
|
||||
|
||||
@@ -64,7 +63,6 @@ class Plant:
|
||||
radar = messaging.new_message('radarState')
|
||||
control = messaging.new_message('controlsState')
|
||||
ss = messaging.new_message('selfdriveState')
|
||||
ss_sp = messaging.new_message('selfdriveStateSP')
|
||||
car_state = messaging.new_message('carState')
|
||||
lp = messaging.new_message('liveParameters')
|
||||
car_control = messaging.new_message('carControl')
|
||||
@@ -120,7 +118,7 @@ class Plant:
|
||||
|
||||
control.controlsState.longControlState = LongCtrlState.pid if self.enabled else LongCtrlState.off
|
||||
ss.selfdriveState.experimentalMode = self.e2e
|
||||
ss_sp.selfdriveStateSP.personality = self.personality
|
||||
ss.selfdriveState.personality = self.personality
|
||||
control.controlsState.forceDecel = self.force_decel
|
||||
car_state.carState.vEgo = float(self.speed)
|
||||
car_state.carState.standstill = bool(self.speed < 0.01)
|
||||
@@ -133,7 +131,6 @@ class Plant:
|
||||
'carControl': car_control.carControl,
|
||||
'controlsState': control.controlsState,
|
||||
'selfdriveState': ss.selfdriveState,
|
||||
'selfdriveStateSP': ss_sp.selfdriveStateSP,
|
||||
'liveParameters': lp.liveParameters,
|
||||
'modelV2': model.modelV2}
|
||||
self.planner.update(sm)
|
||||
|
||||
@@ -68,6 +68,15 @@ DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) {
|
||||
});
|
||||
addItem(errorLogBtn);
|
||||
|
||||
#ifdef SUNNYPILOT
|
||||
hkgAngleSmoothingFactor = new OptionControlSP("HkgTuningAngleSmoothingFactor", tr("HKG Angle Smoothing Factor"), tr("Applies EMA (Exponential Moving Average) to the desired angle steering.<br/>A value closer to 1 means no smoothing.<br/>A value closer to 0 means very smooth (and thus likely too 'soft' steering)."), "../assets/offroad/icon_blank.png", {0, 10}, 1);
|
||||
connect(hkgAngleSmoothingFactor, &OptionControlSP::updateLabels, hkgAngleSmoothingFactor, [=]() {
|
||||
this->updateToggles(offroad);
|
||||
});
|
||||
addItem(hkgAngleSmoothingFactor);
|
||||
#endif
|
||||
|
||||
|
||||
// Joystick and longitudinal maneuvers should be hidden on release branches
|
||||
is_release = params.getBool("IsReleaseBranch");
|
||||
|
||||
@@ -87,6 +96,9 @@ void DeveloperPanel::updateToggles(bool _offroad) {
|
||||
if (btn != experimentalLongitudinalToggle) {
|
||||
btn->setEnabled(_offroad);
|
||||
}
|
||||
|
||||
auto HkgAngleSmoothingFactorValue = QString::fromStdString(params.get("HkgTuningAngleSmoothingFactor")).toDouble();
|
||||
hkgAngleSmoothingFactor->setLabel(QString::number(HkgAngleSmoothingFactorValue / 10, 'f', 1));
|
||||
}
|
||||
|
||||
// longManeuverToggle and experimentalLongitudinalToggle should not be toggleable if the car does not have longitudinal control
|
||||
|
||||
@@ -21,6 +21,10 @@ private:
|
||||
ParamControl* experimentalLongitudinalToggle;
|
||||
ParamControl* hyundaiRadarTracksToggle;
|
||||
ParamControl* enableGithubRunner;
|
||||
#ifdef SUNNYPILOT
|
||||
OptionControlSP* hkgAngleSmoothingFactor;
|
||||
#endif
|
||||
|
||||
bool is_release;
|
||||
bool offroad = false;
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ def manager_init() -> None:
|
||||
("ModelManager_ModelsCache", ""),
|
||||
("NeuralNetworkLateralControl", "0"),
|
||||
("QuietMode", "0"),
|
||||
("HkgTuningAngleSmoothingFactor", "6")
|
||||
]
|
||||
|
||||
if params.get_bool("RecordFrontLock"):
|
||||
|
||||
@@ -110,7 +110,7 @@ procs = [
|
||||
|
||||
NativeProcess("sensord", "system/sensord", ["./sensord"], only_onroad, enabled=not PC),
|
||||
NativeProcess("ui", "selfdrive/ui", ["./ui"], always_run, watchdog_max_dt=(5 if not PC else None)),
|
||||
PythonProcess("soundd", "selfdrive.ui.soundd", only_onroad),
|
||||
PythonProcess("soundd", "selfdrive.ui.soundd", and_(only_onroad, not_joystick)),
|
||||
PythonProcess("locationd", "selfdrive.locationd.locationd", only_onroad),
|
||||
NativeProcess("_pandad", "selfdrive/pandad", ["./pandad"], always_run, enabled=False),
|
||||
PythonProcess("calibrationd", "selfdrive.locationd.calibrationd", only_onroad),
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
With joystick_control, you can connect your laptop to your comma device over the network and debug controls using a joystick or keyboard.
|
||||
joystick_control uses [inputs](https://pypi.org/project/inputs) which supports many common gamepads and joysticks.
|
||||
|
||||
## Note
|
||||
You might need to install [xow](https://github.com/medusalix/xow) to use the Xbox One controller on comma device.
|
||||
Even though **xone** is recommended on the xow page, **xow** is the one that works with the comma device.
|
||||
|
||||
## Usage
|
||||
|
||||
The car must be off, and openpilot must be offroad before starting `joystick_control`.
|
||||
|
||||
@@ -3,7 +3,10 @@ import os
|
||||
import argparse
|
||||
import threading
|
||||
import numpy as np
|
||||
from inputs import UnpluggedError, get_gamepad
|
||||
try:
|
||||
import pygame as pg
|
||||
except ImportError:
|
||||
print("pygame is not installed. Please install it with 'uv pip install pygame'")
|
||||
|
||||
from cereal import messaging
|
||||
from openpilot.common.params import Params
|
||||
@@ -11,7 +14,10 @@ from openpilot.common.realtime import Ratekeeper
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.tools.lib.kbhit import KBHit
|
||||
|
||||
EXPO = 0.4
|
||||
# Set SDL environment variable to avoid using a video driver for headless operation
|
||||
os.environ["SDL_VIDEODRIVER"] = "dummy"
|
||||
|
||||
EXPO = 0.4 # Exponential factor for joystick response curve
|
||||
|
||||
|
||||
class Keyboard:
|
||||
@@ -40,60 +46,122 @@ class Keyboard:
|
||||
return True
|
||||
|
||||
|
||||
class Joystick:
|
||||
class PyGameJoystick:
|
||||
def __init__(self):
|
||||
# This class supports a PlayStation 5 DualSense controller on the comma 3X
|
||||
# TODO: find a way to get this from API or detect gamepad/PC, perhaps "inputs" doesn't support it
|
||||
self.cancel_button = 'BTN_NORTH' # BTN_NORTH=X/triangle
|
||||
if HARDWARE.get_device_type() == 'pc':
|
||||
accel_axis = 'ABS_Z'
|
||||
steer_axis = 'ABS_RX'
|
||||
# TODO: once the longcontrol API is finalized, we can replace this with outputting gas/brake and steering
|
||||
self.flip_map = {'ABS_RZ': accel_axis}
|
||||
else:
|
||||
accel_axis = 'ABS_RX'
|
||||
steer_axis = 'ABS_Z'
|
||||
self.flip_map = {'ABS_RY': accel_axis}
|
||||
# Initialize pygame and joystick subsystem
|
||||
pg.init()
|
||||
if not pg.joystick.get_init():
|
||||
pg.joystick.init()
|
||||
|
||||
self.min_axis_value = {accel_axis: 0., steer_axis: 0.}
|
||||
self.max_axis_value = {accel_axis: 255., steer_axis: 255.}
|
||||
self.axes_values = {accel_axis: 0., steer_axis: 0.}
|
||||
self.axes_order = [accel_axis, steer_axis]
|
||||
# Find connected joysticks
|
||||
joystick_count = pg.joystick.get_count()
|
||||
if joystick_count == 0:
|
||||
print("No joysticks found. Please connect a controller.")
|
||||
exit(1)
|
||||
|
||||
# Initialize the first joystick
|
||||
self.joystick = pg.joystick.Joystick(0)
|
||||
self.joystick.init()
|
||||
|
||||
print(f"Using joystick: {self.joystick.get_name()}")
|
||||
print(f"Number of axes: {self.joystick.get_numaxes()}")
|
||||
print(f"Number of buttons: {self.joystick.get_numbuttons()}")
|
||||
print(f"Number of hats: {self.joystick.get_numhats()}")
|
||||
|
||||
# This supports PlayStation and Xbox controllers on different platforms
|
||||
if HARDWARE.get_device_type() == 'pc':
|
||||
# Xbox mapping on PC
|
||||
self.accel_axis = 5 # Right trigger
|
||||
self.brake_axis = 4 # Left trigger
|
||||
self.steer_axis = 0 # Left stick horizontal
|
||||
self.cancel_button = 3 # Y/Triangle button
|
||||
else:
|
||||
# PlayStation mapping on comma device
|
||||
self.accel_axis = 5 # R2
|
||||
self.brake_axis = 4 # L2
|
||||
self.steer_axis = 0 # Left stick horizontal
|
||||
self.cancel_button = 3 # Triangle button
|
||||
|
||||
# Configure for adaptive mappings based on detected controller
|
||||
controller_name = self.joystick.get_name().lower()
|
||||
if "xbox" in controller_name:
|
||||
print("Xbox controller detected, using Xbox mappings")
|
||||
self.accel_axis = 5 # Right trigger (RT)
|
||||
self.brake_axis = 4 # Left trigger (LT)
|
||||
self.cancel_button = 3 # Y button
|
||||
elif "playstation" in controller_name or "dual" in controller_name:
|
||||
print("PlayStation controller detected, using PlayStation mappings")
|
||||
self.accel_axis = 5 # R2
|
||||
self.brake_axis = 4 # L2
|
||||
self.cancel_button = 3 # Triangle
|
||||
|
||||
# Initialize values
|
||||
self.axes_values = {'gb': 0., 'steer': 0.} # Maintain same keys as Keyboard class
|
||||
self.axes_order = ['gb', 'steer'] # Match expected format
|
||||
self.cancel = False
|
||||
self.deadzone = 0.03 # 3% deadzone for noisy joysticks
|
||||
|
||||
# Process events once to clear the event queue
|
||||
pg.event.pump()
|
||||
|
||||
def update(self):
|
||||
# Process pygame events
|
||||
try:
|
||||
joystick_event = get_gamepad()[0]
|
||||
except (OSError, UnpluggedError):
|
||||
self.axes_values = {ax: 0. for ax in self.axes_values}
|
||||
for event in pg.event.get():
|
||||
if event.type == pg.JOYDEVICEREMOVED:
|
||||
if event.instance_id == self.joystick.get_instance_id():
|
||||
print("Joystick disconnected!")
|
||||
self.axes_values = {'gb': 0., 'steer': 0.}
|
||||
return False
|
||||
|
||||
elif event.type == pg.JOYBUTTONDOWN:
|
||||
if event.button == self.cancel_button:
|
||||
self.cancel = True
|
||||
|
||||
elif event.type == pg.JOYBUTTONUP:
|
||||
if event.button == self.cancel_button:
|
||||
self.cancel = False
|
||||
except Exception as e:
|
||||
print(f"Error processing events: {e}")
|
||||
return False
|
||||
|
||||
event = (joystick_event.code, joystick_event.state)
|
||||
# Read current joystick state directly
|
||||
try:
|
||||
if not self.joystick.get_init():
|
||||
print("Joystick not initialized")
|
||||
return False
|
||||
|
||||
# flip left trigger to negative accel
|
||||
if event[0] in self.flip_map:
|
||||
event = (self.flip_map[event[0]], -event[1])
|
||||
# Read steering (left stick horizontal)
|
||||
steer_raw = self.joystick.get_axis(self.steer_axis) * -1
|
||||
steer = steer_raw if abs(steer_raw) > self.deadzone else 0.0
|
||||
|
||||
if event[0] == self.cancel_button:
|
||||
if event[1] == 1:
|
||||
self.cancel = True
|
||||
elif event[1] == 0: # state 0 is falling edge
|
||||
self.cancel = False
|
||||
elif event[0] in self.axes_values:
|
||||
self.max_axis_value[event[0]] = max(event[1], self.max_axis_value[event[0]])
|
||||
self.min_axis_value[event[0]] = min(event[1], self.min_axis_value[event[0]])
|
||||
# Read gas (right trigger)
|
||||
accel_raw = self.joystick.get_axis(self.accel_axis)
|
||||
# Convert from [-1, 1] to [0, 1] range (triggers often start at -1 when not pressed)
|
||||
accel = (accel_raw + 1) / 2 if self.accel_axis in [4, 5] else accel_raw
|
||||
accel = accel if accel > self.deadzone else 0.0
|
||||
|
||||
norm = -float(np.interp(event[1], [self.min_axis_value[event[0]], self.max_axis_value[event[0]]], [-1., 1.]))
|
||||
norm = norm if abs(norm) > 0.03 else 0. # center can be noisy, deadzone of 3%
|
||||
self.axes_values[event[0]] = EXPO * norm ** 3 + (1 - EXPO) * norm # less action near center for fine control
|
||||
else:
|
||||
# Read brake (left trigger)
|
||||
brake_raw = self.joystick.get_axis(self.brake_axis)
|
||||
# Convert from [-1, 1] to [0, 1] range (triggers often start at -1 when not pressed)
|
||||
brake = (brake_raw + 1) / 2 if self.brake_axis in [4, 5] else brake_raw
|
||||
brake = brake if brake > self.deadzone else 0.0
|
||||
|
||||
# Apply expo for steering
|
||||
self.axes_values['steer'] = EXPO * steer**3 + (1 - EXPO) * steer # Apply expo for fine control
|
||||
|
||||
# Calculate combined gas/brake value for output [-1, 1] where negative is brake
|
||||
self.axes_values['gb'] = accel - brake
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error reading joystick: {e}")
|
||||
self.axes_values = {'gb': 0., 'steer': 0.}
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def send_thread(joystick):
|
||||
pm = messaging.PubMaster(['testJoystick'])
|
||||
|
||||
rk = Ratekeeper(100, print_delay_threshold=None)
|
||||
|
||||
while True:
|
||||
@@ -105,7 +173,6 @@ def send_thread(joystick):
|
||||
joystick_msg.testJoystick.axes = [joystick.axes_values[ax] for ax in joystick.axes_order]
|
||||
|
||||
pm.send('testJoystick', joystick_msg)
|
||||
|
||||
rk.keep_time()
|
||||
|
||||
|
||||
@@ -117,13 +184,12 @@ def joystick_control_thread(joystick):
|
||||
|
||||
|
||||
def main():
|
||||
joystick_control_thread(Joystick())
|
||||
|
||||
joystick_control_thread(PyGameJoystick())
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='Publishes events from your joystick to control your car.\n' +
|
||||
'openpilot must be offroad before starting joystick_control. This tool supports ' +
|
||||
'a PlayStation 5 DualSense controller on the comma 3X.',
|
||||
'PlayStation and Xbox controllers on various platforms.',
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument('--keyboard', action='store_true', help='Use your keyboard instead of a joystick')
|
||||
args = parser.parse_args()
|
||||
@@ -139,9 +205,14 @@ if __name__ == '__main__':
|
||||
print('Buttons')
|
||||
print('- `R`: Resets axes')
|
||||
print('- `C`: Cancel cruise control')
|
||||
joystick = Keyboard()
|
||||
else:
|
||||
print('Using joystick, make sure to run cereal/messaging/bridge on your device if running over the network!')
|
||||
print('If not running on a comma device, the mapping may need to be adjusted.')
|
||||
print('Using pygame joystick')
|
||||
print('Standard controller mapping:')
|
||||
print('- Left stick: Steering')
|
||||
print('- Right trigger (R2): Gas')
|
||||
print('- Left trigger (L2): Brake')
|
||||
print('- Triangle/Y button: Cancel')
|
||||
joystick = PyGameJoystick()
|
||||
|
||||
joystick = Keyboard() if args.keyboard else Joystick()
|
||||
joystick_control_thread(joystick)
|
||||
|
||||
@@ -19,18 +19,24 @@ def joystickd_thread():
|
||||
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)
|
||||
VM = VehicleModel(CP)
|
||||
|
||||
sm = messaging.SubMaster(['carState', 'onroadEvents', 'liveParameters', 'selfdriveState', 'testJoystick'], frequency=1. / DT_CTRL)
|
||||
sm = messaging.SubMaster(['carState', 'onroadEvents', 'liveParameters', 'selfdriveState', 'testJoystick', 'selfdriveStateSP'], frequency=1. / DT_CTRL)
|
||||
pm = messaging.PubMaster(['carControl', 'controlsState'])
|
||||
|
||||
rk = Ratekeeper(100, print_delay_threshold=None)
|
||||
while 1:
|
||||
sm.update(0)
|
||||
ss_sp = sm['selfdriveStateSP']
|
||||
_lat_active = False
|
||||
if ss_sp.mads.available:
|
||||
_lat_active = ss_sp.mads.active
|
||||
else:
|
||||
_lat_active = sm['selfdriveState'].active
|
||||
|
||||
cc_msg = messaging.new_message('carControl')
|
||||
cc_msg.valid = True
|
||||
CC = cc_msg.carControl
|
||||
CC.enabled = sm['selfdriveState'].enabled
|
||||
CC.latActive = sm['selfdriveState'].active and not sm['carState'].steerFaultTemporary and not sm['carState'].steerFaultPermanent
|
||||
CC.latActive = _lat_active and not sm['carState'].steerFaultTemporary and not sm['carState'].steerFaultPermanent
|
||||
CC.longActive = CC.enabled and not any(e.overrideLongitudinal for e in sm['onroadEvents']) and CP.openpilotLongitudinalControl
|
||||
CC.cruiseControl.cancel = sm['carState'].cruiseState.enabled and (not CC.enabled or not CP.pcmCruise)
|
||||
CC.hudControl.leadDistanceBars = 2
|
||||
|
||||
@@ -690,15 +690,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inputs"
|
||||
version = "0.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/cd/5f434220920f76eb73d19bb7aab8d857445f40aa642718e6e51e850cd663/inputs-0.5.tar.gz", hash = "sha256:a31d5b96a3525f1232f326be9e7ce8ccaf873c6b1fb84d9f3c9bc3d79b23eae4", size = 33393 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/94/040a0d9c81f018c39bd887b7b825013b024deb0a6c795f9524797e2cd41b/inputs-0.5-py2.py3-none-any.whl", hash = "sha256:13f894564e52134cf1e3862b1811da034875eb1f2b62e6021e3776e9669a96ec", size = 33630 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "isodate"
|
||||
version = "0.7.2"
|
||||
@@ -1245,7 +1236,6 @@ dependencies = [
|
||||
{ name = "crcmod" },
|
||||
{ name = "cython" },
|
||||
{ name = "future-fstrings" },
|
||||
{ name = "inputs" },
|
||||
{ name = "json-rpc" },
|
||||
{ name = "libusb1" },
|
||||
{ name = "llvmlite" },
|
||||
@@ -1255,6 +1245,7 @@ dependencies = [
|
||||
{ name = "pyaudio" },
|
||||
{ name = "pycapnp" },
|
||||
{ name = "pycryptodome" },
|
||||
{ name = "pygame" },
|
||||
{ name = "pyjwt" },
|
||||
{ name = "pyopenssl" },
|
||||
{ name = "pyserial" },
|
||||
@@ -1284,7 +1275,6 @@ dev = [
|
||||
{ name = "matplotlib" },
|
||||
{ name = "parameterized" },
|
||||
{ name = "pyautogui" },
|
||||
{ name = "pygame" },
|
||||
{ name = "pyopencl", marker = "platform_machine != 'aarch64'" },
|
||||
{ name = "pyprof2calltree" },
|
||||
{ name = "pytools", marker = "platform_machine != 'aarch64'" },
|
||||
@@ -1339,7 +1329,6 @@ requires-dist = [
|
||||
{ name = "dictdiffer", marker = "extra == 'dev'" },
|
||||
{ name = "future-fstrings" },
|
||||
{ name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" },
|
||||
{ name = "inputs" },
|
||||
{ name = "jinja2", marker = "extra == 'docs'" },
|
||||
{ name = "json-rpc" },
|
||||
{ name = "libusb1" },
|
||||
@@ -1359,7 +1348,7 @@ requires-dist = [
|
||||
{ name = "pyautogui", marker = "extra == 'dev'" },
|
||||
{ name = "pycapnp" },
|
||||
{ name = "pycryptodome" },
|
||||
{ name = "pygame", marker = "extra == 'dev'" },
|
||||
{ name = "pygame" },
|
||||
{ name = "pyjwt" },
|
||||
{ name = "pyopencl", marker = "platform_machine != 'aarch64' and extra == 'dev'" },
|
||||
{ name = "pyopenssl", specifier = "<24.3.0" },
|
||||
|
||||
Reference in New Issue
Block a user