Compare commits

...

18 Commits

Author SHA1 Message Date
James Vecellio-Grant
1f8cf99c2a Merge branch 'master' into optional-turn-desire-nudge 2025-09-18 06:11:15 -07:00
James Vecellio-Grant
7aa9d43187 Merge branch 'master' into optional-turn-desire-nudge 2025-09-15 06:14:24 -07:00
James Vecellio-Grant
920fd823ca Merge branch 'master' into optional-turn-desire-nudge 2025-09-12 06:46:53 -07:00
discountchubbs
d450a4479f not used 2025-09-08 05:40:02 -07:00
discountchubbs
70b4de2c6b Merge remote-tracking branch 'origin/optional-turn-desire-nudge' into optional-turn-desire-nudge 2025-09-07 20:54:53 -07:00
discountchubbs
7611857719 remove 2025-09-07 20:54:45 -07:00
James Vecellio-Grant
82467e9436 Merge branch 'master' into optional-turn-desire-nudge 2025-09-07 20:46:57 -07:00
discountchubbs
bff22dd699 comments 2025-09-07 20:46:11 -07:00
James Vecellio-Grant
71e1149ee3 Update selfdrive/controls/lib/desire_helper.py
Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-09-07 20:43:33 -07:00
discountchubbs
b63d0a1d18 modeld: Add lane turn direction handling to desire logic 2025-09-07 16:22:40 -07:00
discountchubbs
f6b30ef364 what 2025-09-07 15:56:46 -07:00
discountchubbs
c85acd8d7f state handling 2025-09-07 15:52:14 -07:00
discountchubbs
fd3df55a0c Refactor LaneTurnController to remove dependency on DesireHelper 2025-09-07 13:39:00 -07:00
discountchubbs
566210d678 Refactor lane turn desire handling in DesireHelper and LaneTurnController 2025-09-07 12:48:29 -07:00
discountchubbs
d5e3410c65 remove 2025-09-07 11:23:11 -07:00
discountchubbs
7028301532 steer press 2025-09-07 11:22:37 -07:00
discountchubbs
15a3d5fa7e keep -> turn 2025-09-07 08:51:07 -07:00
discountchubbs
255ca767cd Implement nudge mode for lane turn desires in LaneTurnController and DesireHelper 2025-09-07 08:47:42 -07:00
14 changed files with 283 additions and 151 deletions

View File

@@ -203,7 +203,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"LagdToggle", {PERSISTENT | BACKUP, BOOL, "1"}},
{"LagdToggleDelay", {PERSISTENT | BACKUP, FLOAT, "0.2"}},
{"LagdValueCache", {PERSISTENT, FLOAT, "0.2"}},
{"LaneTurnDesire", {PERSISTENT | BACKUP, BOOL, "0"}},
{"LaneTurnDesire", {PERSISTENT | BACKUP, INT, "0"}},
{"LaneTurnValue", {PERSISTENT | BACKUP, FLOAT, "19.0"}},
// mapd

View File

@@ -1,4 +1,4 @@
from cereal import log, custom
from cereal import log
from openpilot.common.constants import CV
from openpilot.common.realtime import DT_MDL
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode
@@ -31,12 +31,6 @@ DESIRES = {
},
}
TURN_DESIRES = {
custom.TurnDirection.none: log.Desire.none,
custom.TurnDirection.turnLeft: log.Desire.turnLeft,
custom.TurnDirection.turnRight: log.Desire.turnRight,
}
class DesireHelper:
def __init__(self):
@@ -48,8 +42,7 @@ class DesireHelper:
self.prev_one_blinker = False
self.desire = log.Desire.none
self.alc = AutoLaneChangeController(self)
self.lane_turn_controller = LaneTurnController(self)
self.lane_turn_direction = custom.TurnDirection.none
self.lane_turn_controller = LaneTurnController()
@staticmethod
def get_lane_change_direction(CS):
@@ -57,16 +50,10 @@ class DesireHelper:
def update(self, carstate, lateral_active, lane_change_prob):
self.alc.update_params()
self.lane_turn_controller.update_params()
v_ego = carstate.vEgo
one_blinker = carstate.leftBlinker != carstate.rightBlinker
below_lane_change_speed = v_ego < LANE_CHANGE_SPEED_MIN
# Lane turn controller update
self.lane_turn_controller.update_lane_turn(blindspot_left=carstate.leftBlindspot, blindspot_right=carstate.rightBlindspot,
left_blinker=carstate.leftBlinker, right_blinker=carstate.rightBlinker, v_ego=v_ego)
self.lane_turn_direction = self.lane_turn_controller.get_turn_direction()
if not lateral_active or self.lane_change_timer > LANE_CHANGE_TIME_MAX or self.alc.lane_change_set_timer == AutoLaneChangeMode.OFF:
self.lane_change_state = LaneChangeState.off
self.lane_change_direction = LaneChangeDirection.none
@@ -126,10 +113,13 @@ class DesireHelper:
self.prev_one_blinker = one_blinker
if self.lane_turn_direction != custom.TurnDirection.none:
self.desire = TURN_DESIRES[self.lane_turn_direction]
else:
self.desire = DESIRES[self.lane_change_direction][self.lane_change_state]
self.desire = DESIRES[self.lane_change_direction][self.lane_change_state]
# Lane turn controller update
turn_desire = self.lane_turn_controller.update(carstate.leftBlindspot, carstate.rightBlindspot, carstate.leftBlinker, carstate.rightBlinker,
carstate.vEgo, carstate.steeringPressed, carstate.steeringTorque)
if turn_desire != log.Desire.none:
self.desire = turn_desire
# Send keep pulse once per second during LaneChangeStart.preLaneChange
if self.lane_change_state in (LaneChangeState.off, LaneChangeState.laneChangeStarting):

View File

@@ -398,7 +398,7 @@ def main(demo=False):
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_controller.turn_direction
drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state
drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction

View File

@@ -48,6 +48,7 @@ qt_src = [
lateral_panel_qt_src = [
"sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.cc",
"sunnypilot/qt/offroad/settings/lateral/lane_change_settings.cc",
"sunnypilot/qt/offroad/settings/lateral/lane_turn_settings.cc",
"sunnypilot/qt/offroad/settings/lateral/mads_settings.cc",
"sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc",
]

View File

@@ -0,0 +1,86 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_turn_settings.h"
LaneTurnSettings::LaneTurnSettings(QWidget* parent) : QWidget(parent) {
QVBoxLayout* main_layout = new QVBoxLayout(this);
main_layout->setContentsMargins(50, 20, 50, 20);
main_layout->setSpacing(20);
// Back button
PanelBackButton* back = new PanelBackButton(tr("Back"));
connect(back, &QPushButton::clicked, [=]() { emit backPress(); });
main_layout->addWidget(back, 0, Qt::AlignLeft);
ListWidgetSP *list = new ListWidgetSP(this, false);
// Lane Turn Desire Control
laneTurnDesireControl = new LaneTurnDesireControl();
laneTurnDesireControl->setUpdateOtherToggles(true);
laneTurnDesireControl->showDescription();
connect(laneTurnDesireControl, &OptionControlSP::updateLabels, laneTurnDesireControl, &LaneTurnDesireControl::refresh);
list->addItem(laneTurnDesireControl);
// Lane Turn Value control
bool is_metric = params.getBool("IsMetric");
int per_value_change = is_metric ? 62 : 100; // ~1 km/h or 1 mph
laneTurnValueControl = new OptionControlSP("LaneTurnValue", tr("Adjust Lane Turn Speed"),
tr("Set the maximum speed for lane turn desires. Default is 19 %1.").arg(is_metric ? "km/h" : "mph"),
"", {500, 2000}, per_value_change, false, nullptr, true, true);
laneTurnValueControl->showDescription();
list->addItem(laneTurnValueControl);
// show/hide value control based on desire
connect(laneTurnDesireControl, &OptionControlSP::updateLabels, this, &LaneTurnSettings::updateValueControlVisibility);
connect(laneTurnValueControl, &OptionControlSP::updateLabels, this, &LaneTurnSettings::refreshLaneTurnValueControl);
main_layout->addWidget(new ScrollViewSP(list, this));
}
void LaneTurnSettings::showEvent(QShowEvent *event) {
updateValueControlVisibility();
}
void LaneTurnSettings::updateValueControlVisibility() {
QString option = QString::fromStdString(params.get("LaneTurnDesire"));
bool visible = (option != "0");
laneTurnValueControl->setVisible(visible);
if (visible) {
refreshLaneTurnValueControl();
}
}
void LaneTurnSettings::refreshLaneTurnValueControl() {
if (!laneTurnValueControl) return;
float stored_mph = QString::fromStdString(params.get("LaneTurnValue")).toFloat();
bool is_metric = params.getBool("IsMetric");
QString unit = is_metric ? "km/h" : "mph";
float display_value = is_metric ? stored_mph * 1.609344f : stored_mph;
laneTurnValueControl->setLabel(QString::number(qRound(display_value)) + " " + unit);
}
// Lane Turn Desire Control
LaneTurnDesireControl::LaneTurnDesireControl() : OptionControlSP(
"LaneTurnDesire",
tr("Lane Turn Desires"),
tr("If you're driving at 20 mph (32 km/h) or below and have your blinker on, "
"the car will plan a turn in that direction at the nearest drivable path. "
"This prevents situations (like at red lights) where the car might plan the wrong turn direction."),
"../assets/offroad/icon_shell.png",
{0, 2}) {
refresh();
}
void LaneTurnDesireControl::refresh() {
QString option = QString::fromStdString(params.get("LaneTurnDesire"));
static const QMap<QString, QString> options = {
{"0", tr("Off")},
{"1", tr("Nudge")},
{"2", tr("Nudgeless")},
};
setLabel(options.value(option, tr("Off")));
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#pragma once
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h"
class LaneTurnDesireControl : public OptionControlSP {
Q_OBJECT
public:
LaneTurnDesireControl();
void refresh();
private:
Params params;
};
class LaneTurnSettings : public QWidget {
Q_OBJECT
public:
explicit LaneTurnSettings(QWidget* parent = nullptr);
void showEvent(QShowEvent *event) override;
signals:
void backPress();
private:
void refreshLaneTurnValueControl();
void updateValueControlVisibility();
private:
Params params;
std::map<std::string, ParamControlSP*> toggles;
LaneTurnDesireControl *laneTurnDesireControl;
OptionControlSP *laneTurnValueControl;
};

View File

@@ -59,7 +59,27 @@ LateralPanel::LateralPanel(SettingsWindowSP *parent) : QFrame(parent) {
sunnypilotScroller->restoreScrollPosition();
main_layout->setCurrentWidget(sunnypilotScreen);
});
list->addItem(laneChangeSettingsButton);
// Lane Turn Settings
laneTurnSettingsButton = new PushButtonSP(tr("Customize Lane Turn"));
laneTurnSettingsButton->setObjectName("lane_turn_btn");
connect(laneTurnSettingsButton, &QPushButton::clicked, [=]() {
sunnypilotScroller->setLastScrollPosition();
main_layout->setCurrentWidget(laneTurnWidget);
});
laneTurnWidget = new LaneTurnSettings(this);
connect(laneTurnWidget, &LaneTurnSettings::backPress, [=]() {
sunnypilotScroller->restoreScrollPosition();
main_layout->setCurrentWidget(sunnypilotScreen);
});
QWidget *laneButtonsWidget = new QWidget();
QHBoxLayout *laneButtonsLayout = new QHBoxLayout(laneButtonsWidget);
laneButtonsLayout->setContentsMargins(0, 0, 0, 0);
laneChangeSettingsButton->setFixedWidth(750); laneTurnSettingsButton->setFixedWidth(750);
laneButtonsLayout->addWidget(laneChangeSettingsButton); laneButtonsLayout->addWidget(laneTurnSettingsButton);
list->addItem(laneButtonsWidget);
list->addItem(vertical_space(0));
list->addItem(horizontal_line());
@@ -100,6 +120,7 @@ LateralPanel::LateralPanel(SettingsWindowSP *parent) : QFrame(parent) {
main_layout->addWidget(sunnypilotScreen);
main_layout->addWidget(madsWidget);
main_layout->addWidget(laneChangeWidget);
main_layout->addWidget(laneTurnWidget);
setStyleSheet(R"(
#back_btn {

View File

@@ -15,6 +15,7 @@
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_turn_settings.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h"
@@ -43,6 +44,8 @@ private:
MadsSettings *madsWidget = nullptr;
PushButtonSP *laneChangeSettingsButton;
LaneChangeSettings *laneChangeWidget = nullptr;
PushButtonSP *laneTurnSettingsButton;
LaneTurnSettings *laneTurnWidget = nullptr;
NeuralNetworkLateralControl *nnlcToggle = nullptr;
BlinkerPauseLateralSettings *blinkerPauseLateralSettings = nullptr;

View File

@@ -103,30 +103,6 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) {
list->addItem(policyFrame);
list->addItem(horizontal_line());
// Lane Turn Desire toggle
lane_turn_desire_toggle = new ParamControlSP("LaneTurnDesire", tr("Use Lane Turn Desires"),
"If youre driving at 20 mph (32 km/h) or below and have your blinker on, "
"the car will plan a turn in that direction at the nearest drivable path. "
"This prevents situations (like at red lights) where the car might plan the wrong turn direction.",
"../assets/offroad/icon_shell.png");
list->addItem(lane_turn_desire_toggle);
// Lane Turn Value control
int max_value_mph = 20;
bool is_metric_initial = params.getBool("IsMetric");
const float K = 1.609344f;
int per_value_change_scaled = is_metric_initial ? static_cast<int>(std::round((1.0f / K) * 100.0f)) : 100; // 100 -> 1 mph
lane_turn_value_control = new OptionControlSP("LaneTurnValue", tr("Adjust Lane Turn Speed"),
tr("Set the maximum speed for lane turn desires. Default is 19 %1.").arg(is_metric_initial ? "km/h" : "mph"),
"", {5 * 100, max_value_mph * 100}, per_value_change_scaled, false, nullptr, true, true);
lane_turn_value_control->showDescription();
list->addItem(lane_turn_value_control);
// Show based on toggle
refreshLaneTurnValueControl();
connect(lane_turn_desire_toggle, &ParamControlSP::toggleFlipped, this, &ModelsPanel::refreshLaneTurnValueControl);
connect(lane_turn_value_control, &OptionControlSP::updateLabels, this, &ModelsPanel::refreshLaneTurnValueControl);
// LiveDelay toggle
lagd_toggle_control = new ParamControlSP("LagdToggle", tr("Live Learning Steer Delay"), "", "../assets/offroad/icon_shell.png");
lagd_toggle_control->showDescription();
@@ -169,23 +145,6 @@ QFrame* ModelsPanel::createModelDetailFrame(QWidget *parent, QString &typeName,
return frame;
}
void ModelsPanel::refreshLaneTurnValueControl() {
if (!lane_turn_value_control) return;
float stored_mph = QString::fromStdString(params.get("LaneTurnValue")).toFloat();
bool is_metric = params.getBool("IsMetric");
QString unit = is_metric ? "km/h" : "mph";
float display_value = stored_mph;
if (is_metric) {
display_value = stored_mph * 1.609344f;
}
lane_turn_value_control->setLabel(QString::number(static_cast<int>(std::round(display_value))) + " " + unit);
lane_turn_value_control->setVisible(params.getBool("LaneTurnDesire"));
}
/**
* @brief Updates the UI with bundle download progress information
* Reads status from modelManagerSP cereal message and displays status for all models
*/
void ModelsPanel::handleBundleDownloadProgress() {
supercomboFrame->setVisible(false);
visionFrame->setVisible(false);
@@ -456,9 +415,6 @@ void ModelsPanel::updateLabels() {
delay_control->setLabel(QString::number(value, 'f', 2) + "s");
}
// Update lane turn desire label and visibility
refreshLaneTurnValueControl();
clearModelCacheBtn->setValue(QString::number(calculateCacheSize(), 'f', 2) + " MB");
}

View File

@@ -38,7 +38,6 @@ private:
void updateLabels();
void handleCurrentModelLblBtnClicked();
void handleBundleDownloadProgress();
void refreshLaneTurnValueControl();
void showResetParamsDialog();
QProgressBar* createProgressBar(QWidget *parent);
QFrame* createModelDetailFrame(QWidget *parent, QString &typeName, QProgressBar *progressBar);
@@ -83,6 +82,4 @@ private:
Params params;
ButtonControlSP *clearModelCacheBtn;
ButtonControlSP *refreshAvailableModelsBtn;
ParamControlSP *lane_turn_desire_toggle;
OptionControlSP *lane_turn_value_control;
};

View File

@@ -317,7 +317,7 @@ def main(demo=False):
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_controller.turn_direction
drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state
drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction

View File

@@ -343,7 +343,7 @@ def main(demo=False):
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_controller.turn_direction
drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state
drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction

View File

@@ -4,25 +4,36 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from cereal import custom
from cereal import custom, log
from openpilot.common.constants import CV
from openpilot.common.params import Params
LANE_CHANGE_SPEED_MIN = 20 * CV.MPH_TO_MS
TURN_STATE = {
'OFF': 0,
'NUDGE': 1,
'NUDGELESS': 2
}
TURN_DESIRES = {
custom.TurnDirection.none: log.Desire.none,
custom.TurnDirection.turnLeft: log.Desire.turnLeft,
custom.TurnDirection.turnRight: log.Desire.turnRight,
}
class LaneTurnController:
def __init__(self, desire_helper):
self.DH = desire_helper
def __init__(self):
self.turn_direction = custom.TurnDirection.none
self.params = Params()
self.lane_turn_value = float(self.params.get("LaneTurnValue", return_default=True)) * CV.MPH_TO_MS
self.param_read_counter = 0
self.enabled = self.params.get_bool("LaneTurnDesire")
self.lane_turn_type = self.params.get("LaneTurnDesire", return_default=True)
def read_params(self):
self.enabled = self.params.get_bool("LaneTurnDesire")
self.lane_turn_type = self.params.get("LaneTurnDesire", return_default=True)
value = float(self.params.get("LaneTurnValue", return_default=True)) * CV.MPH_TO_MS
self.lane_turn_value = min(float(LANE_CHANGE_SPEED_MIN), value)
@@ -32,14 +43,30 @@ class LaneTurnController:
self.param_read_counter += 1
def update_lane_turn(self, blindspot_left: bool, blindspot_right: bool, left_blinker: bool, right_blinker: bool, v_ego: float) -> None:
if left_blinker and not right_blinker and v_ego < self.lane_turn_value and not blindspot_left:
if self.lane_turn_type != TURN_STATE['OFF'] and left_blinker and not right_blinker and v_ego < self.lane_turn_value and not blindspot_left:
self.turn_direction = custom.TurnDirection.turnLeft
elif right_blinker and not left_blinker and v_ego < self.lane_turn_value and not blindspot_right:
elif self.lane_turn_type != TURN_STATE['OFF'] and right_blinker and not left_blinker and v_ego < self.lane_turn_value and not blindspot_right:
self.turn_direction = custom.TurnDirection.turnRight
else:
self.turn_direction = custom.TurnDirection.none
def get_turn_direction(self):
if not self.enabled:
return custom.TurnDirection.none
return self.turn_direction
def return_desire(self, steering_pressed: bool, steering_torque: float) -> log.Desire:
# disabled or no turn -> return none
if self.lane_turn_type == TURN_STATE['OFF'] or self.turn_direction == custom.TurnDirection.none:
return log.Desire.none
if self.lane_turn_type == TURN_STATE['NUDGE']:
# In nudge mode, require steering press for turn desire
turn_torque_applied = steering_pressed and ((steering_torque > 0 and self.turn_direction == custom.TurnDirection.turnLeft) or
(steering_torque < 0 and self.turn_direction == custom.TurnDirection.turnRight))
if not turn_torque_applied:
return log.Desire.none
return TURN_DESIRES[self.turn_direction]
def update(self, blindspot_left: bool, blindspot_right: bool, left_blinker: bool, right_blinker: bool, v_ego: float,
steering_pressed: bool, steering_torque: float) -> log.Desire:
self.update_params()
self.update_lane_turn(blindspot_left, blindspot_right, left_blinker, right_blinker, v_ego)
turn_desire = self.return_desire(steering_pressed, steering_torque)
return turn_desire

View File

@@ -1,77 +1,68 @@
import pytest
from cereal import log
import numpy as np
from cereal import custom, log
from openpilot.common.params import Params
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController, LANE_CHANGE_SPEED_MIN
from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController, LANE_CHANGE_SPEED_MIN, TURN_STATE
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeMode
class TurnDirection:
none = 0
turnLeft = 1
turnRight = 2
@pytest.mark.parametrize("left_blinker,right_blinker,v_ego,blindspot_left,blindspot_right,expected", [
(True, False, 5, False, False, TurnDirection.turnLeft),
(False, True, 6, False, False, TurnDirection.turnRight),
(True, False, 9, False, False, TurnDirection.none),
(True, False, 7, True, False, TurnDirection.none),
(False, True, 6, False, True, TurnDirection.none),
(False, False, 5, False, False, TurnDirection.none),
(True, True, 5, False, False, TurnDirection.none),
])
(True, False, 5, False, False, custom.TurnDirection.turnLeft),
(False, True, 6, False, False, custom.TurnDirection.turnRight),
(True, False, 9, False, False, custom.TurnDirection.none),
(True, False, 7, True, False, custom.TurnDirection.none),
(False, True, 6, False, True, custom.TurnDirection.none),
(False, False, 5, False, False, custom.TurnDirection.none),
(True, True, 5, False, False, custom.TurnDirection.none),
], ids=["left blinker", "right blinker", "no blinkers", "left blindspot w/ left blinker", "right blindspot w/ left blinker",
"no blinkers", "both blinkers"])
def test_lane_turn_desire_conditions(left_blinker, right_blinker, v_ego, blindspot_left, blindspot_right, expected):
dh = DesireHelper()
controller = LaneTurnController(dh)
controller.enabled = True
controller = LaneTurnController()
controller.lane_turn_type = TURN_STATE['NUDGELESS']
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
controller.turn_direction = TurnDirection.none
controller.turn_direction = custom.TurnDirection.none
controller.update_lane_turn(blindspot_left, blindspot_right, left_blinker, right_blinker, v_ego)
assert controller.get_turn_direction() == expected
assert np.equal(controller.turn_direction, expected), f"Expected {expected}, speed {v_ego}, got {controller.turn_direction}"
def test_lane_turn_desire_disabled():
dh = DesireHelper()
controller = LaneTurnController(dh)
controller.enabled = False
controller = LaneTurnController()
controller.lane_turn_type = TURN_STATE['OFF']
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
controller.turn_direction = TurnDirection.none
controller.update_lane_turn(False, False, True, False, 7)
assert controller.get_turn_direction() == TurnDirection.none
controller.turn_direction = custom.TurnDirection.none
controller.update_lane_turn(False, False, True, False, 6)
assert np.equal(controller.turn_direction , custom.TurnDirection.none), f"Expected no turn desire, got {controller.turn_direction}"
def test_lane_turn_overrides_lane_change():
dh = DesireHelper()
controller = LaneTurnController(dh)
controller.enabled = True
controller = LaneTurnController()
controller.lane_turn_type = TURN_STATE['NUDGELESS']
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
controller.turn_direction = TurnDirection.none
# left turn desire
controller.turn_direction = custom.TurnDirection.none
controller.update_lane_turn(False, False, True, False, 5)
assert controller.get_turn_direction() == TurnDirection.turnLeft
# right turn desire
assert np.equal(controller.turn_direction, custom.TurnDirection.turnLeft), f"Expected left turn desire, got {controller.turn_direction}"
controller.update_lane_turn(False, False, False, True, 6)
assert controller.get_turn_direction() == TurnDirection.turnRight
# no turn
assert np.equal(controller.turn_direction, custom.TurnDirection.turnRight), f"Expected right turn desire, got {controller.turn_direction}"
controller.update_lane_turn(False, False, False, False, 7)
assert controller.get_turn_direction() == TurnDirection.none
assert np.equal(controller.turn_direction, custom.TurnDirection.none), f"Expected no turn desire, got {controller.turn_direction}"
@pytest.mark.parametrize("v_ego,expected", [
(8.93, TurnDirection.turnLeft), # just below threshold
(8.96, TurnDirection.none), # above threshold
(8.95, TurnDirection.none), # just above threshold
])
(8.93, custom.TurnDirection.turnLeft), # just below threshold
(8.96, custom.TurnDirection.none), # above threshold
(8.95, custom.TurnDirection.none), # just above threshold
], ids=["below threshold", "above threshold", "just above threshold"])
def test_lane_turn_desire_speed_boundary(v_ego, expected):
dh = DesireHelper()
controller = LaneTurnController(dh)
controller.enabled = True
controller = LaneTurnController()
controller.lane_turn_type = TURN_STATE['NUDGELESS']
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
controller.turn_direction = TurnDirection.none
controller.turn_direction = custom.TurnDirection.none
controller.update_lane_turn(False, True, True, False, v_ego)
assert controller.get_turn_direction() == expected
assert np.equal(controller.turn_direction, expected), f"Expected {expected} at speed {v_ego}, got {controller.turn_direction}"
class DummyCarState:
@@ -86,28 +77,43 @@ class DummyCarState:
self.steeringTorque = steeringTorque
self.brakePressed = brakePressed
@pytest.fixture
def set_lane_turn_params():
params = Params()
params.put("LaneTurnDesire", True)
params.put("LaneTurnValue", 20.0)
@pytest.mark.parametrize("carstate, lateral_active, lane_change_prob, expected_desire", [
# Lane turn desire overrides lane change desire
(DummyCarState(vEgo=5, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False), True, 1.0, log.Desire.turnLeft),
(DummyCarState(vEgo=7, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False), True, 1.0, log.Desire.turnRight),
# Lane change desire only (no turn desires)
(DummyCarState(vEgo=9, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False,
steeringPressed=True, steeringTorque=1), True, 1.0, log.Desire.laneChangeLeft),
(DummyCarState(vEgo=9, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False,
steeringPressed=True, steeringTorque=-1), True, 1.0, log.Desire.laneChangeRight),
# No desire (inactive)
(DummyCarState(vEgo=9, leftBlinker=False, rightBlinker=False), False, 1.0, log.Desire.none),
(DummyCarState(vEgo=4, leftBlinker=False, rightBlinker=False), True, 1.0, log.Desire.none), # No blinkers? no desire!
])
def test_desire_helper_integration(carstate, lateral_active, lane_change_prob, expected_desire, set_lane_turn_params):
def nudgeless_params():
params = Params()
params.put("LaneTurnDesire", TURN_STATE['NUDGELESS'])
params.put("LaneTurnValue", 20.0)
params.put("AutoLaneChangeTimer", AutoLaneChangeMode.NUDGELESS)
def nudge_params():
params = Params()
params.put("LaneTurnDesire", TURN_STATE['NUDGE'])
params.put("LaneTurnValue", 20.0)
params.put("AutoLaneChangeTimer", AutoLaneChangeMode.NUDGE)
@pytest.mark.parametrize("carstate, expected_desire", [
(DummyCarState(vEgo=5, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False), log.Desire.turnLeft),
(DummyCarState(vEgo=7, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False), log.Desire.turnRight),
(DummyCarState(vEgo=5, leftBlinker=False, rightBlinker=False, leftBlindspot=False, rightBlindspot=False), log.Desire.none),
(DummyCarState(vEgo=5, leftBlinker=True, rightBlinker=True, leftBlindspot=False, rightBlindspot=False), log.Desire.none),
], ids=["nudgeless left turn", "nudgeless right turn", "no blinkers", "both blinkers"])
def test_lane_turn_nudgeless(carstate, expected_desire):
nudgeless_params()
dh = DesireHelper()
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
for _ in range(10):
dh.update(carstate, lateral_active, lane_change_prob)
assert dh.desire == expected_desire # The first four tests were unit tests to test the controller, where this tests the integration in desire helpers
dh.update(carstate, True, 1.0)
assert np.equal(dh.desire, expected_desire), f"Expected {expected_desire}, got {dh.desire}"
@pytest.mark.parametrize("carstate, expected_desire", [
(DummyCarState(vEgo=5, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False,
steeringPressed=True, steeringTorque=1), log.Desire.turnLeft),
(DummyCarState(vEgo=7, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False,
steeringPressed=True, steeringTorque=-1), log.Desire.turnRight),
(DummyCarState(vEgo=5, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False), log.Desire.none),
(DummyCarState(vEgo=7, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False), log.Desire.none),
], ids=["nudge left turn with input", "nudge right turn with input", "nudge left turn no input", "nudge right turn no input"])
def test_lane_turn_nudge(carstate, expected_desire):
nudge_params()
dh = DesireHelper()
dh.update(carstate, True, 1.0)
assert np.equal(dh.desire, expected_desire), f"Expected {expected_desire}, got {dh.desire}"