mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-23 21:02:08 +08:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f275604cb2 | |||
| 495bacc576 | |||
| 45a9e4c14c | |||
| 67e23a408c | |||
| a31c5cf227 | |||
| 5164951ffa | |||
| db845cdaab | |||
| 006052c2f3 | |||
| 32c48b5ffc | |||
| 85503e6054 | |||
| 2a4bb20f9d | |||
| 295dac06f1 | |||
| eb11de561b | |||
| 1dd06cc174 | |||
| 21601660c5 | |||
| 923318b6bb | |||
| 6e5fb2b9e9 | |||
| d413310cbb | |||
| 86751559cd | |||
| 5093b0d1b3 | |||
| e0fd11c184 | |||
| 204eaebf55 | |||
| 9fcb4c9ca8 | |||
| da561e8285 | |||
| 691eb4ee9f | |||
| c69a340599 | |||
| 5ef2f5d1fb | |||
| 3ced0d6115 | |||
| cdcc2392a8 | |||
| 22aff8d2eb | |||
| 2c8cb003f9 | |||
| 3ae81d10a3 | |||
| c90289c949 | |||
| b32c64130c | |||
| 9cafeefaf1 | |||
| 2484851edd | |||
| 870d5c3a74 | |||
| 8e841cac89 | |||
| 51081e8464 | |||
| 5f8875b16e | |||
| c47223ec59 | |||
| 3e84bbc9aa | |||
| 8587e08908 | |||
| 4dadfbad1f | |||
| 4a0ba000d3 | |||
| 1d7165786a | |||
| c750a631ef | |||
| 0f6006c87b | |||
| 19eca5e035 | |||
| 5010d752f6 | |||
| 07efcb4abb | |||
| c1d3ae427b | |||
| 2ab45b552d | |||
| 8c1d59fecd | |||
| cde88fd8ed | |||
| 4b5de0eddb | |||
| 071147baaf | |||
| 18af4d6ad6 | |||
| b81d5bca3c | |||
| 682d738ffa |
@@ -4,6 +4,7 @@
|
||||
[submodule "opendbc"]
|
||||
path = opendbc_repo
|
||||
url = https://github.com/sunnypilot/opendbc.git
|
||||
branch = tn
|
||||
[submodule "msgq"]
|
||||
path = msgq_repo
|
||||
url = https://github.com/sunnypilot/msgq.git
|
||||
|
||||
+85
-4
@@ -192,6 +192,7 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
||||
aTarget @5 :Float32;
|
||||
events @6 :List(OnroadEventSP.Event);
|
||||
e2eAlerts @7 :E2eAlerts;
|
||||
accelPersonality @8 :AccelerationPersonality;
|
||||
|
||||
struct DynamicExperimentalControl {
|
||||
state @0 :DynamicExperimentalControlState;
|
||||
@@ -203,7 +204,11 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
||||
blended @1;
|
||||
}
|
||||
}
|
||||
|
||||
enum AccelerationPersonality {
|
||||
sport @0;
|
||||
normal @1;
|
||||
eco @2;
|
||||
}
|
||||
struct SmartCruiseControl {
|
||||
vision @0 :Vision;
|
||||
map @1 :Map;
|
||||
@@ -340,6 +345,7 @@ struct OnroadEventSP @0xda96579883444c35 {
|
||||
speedLimitChanged @21;
|
||||
speedLimitPending @22;
|
||||
e2eChime @23;
|
||||
laneChangeRoadEdge @24;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,6 +452,8 @@ struct LiveMapDataSP @0xf416ec09499d9d19 {
|
||||
|
||||
struct ModelDataV2SP @0xa1680744031fdb2d {
|
||||
laneTurnDirection @0 :TurnDirection;
|
||||
leftLaneChangeEdgeBlock @1 :Bool;
|
||||
rightLaneChangeEdgeBlock @2 :Bool;
|
||||
|
||||
enum TurnDirection {
|
||||
none @0;
|
||||
@@ -488,11 +496,84 @@ struct CustomReserved15 @0xbd443b539493bc68 {
|
||||
struct CustomReserved16 @0xfc6241ed8877b611 {
|
||||
}
|
||||
|
||||
struct CustomReserved17 @0xa30662f84033036c {
|
||||
enum MapdExtendedOutType {
|
||||
paths @0;
|
||||
settings @1;
|
||||
}
|
||||
|
||||
struct CustomReserved18 @0xc86a3d38d13eb3ef {
|
||||
struct MapdExtendedOut @0xa30662f84033036c {
|
||||
type @0 :MapdExtendedOutType;
|
||||
json @1 :Text;
|
||||
}
|
||||
|
||||
struct CustomReserved19 @0xa4f1eb3323f5f582 {
|
||||
enum MapdInputType {
|
||||
download @0;
|
||||
setTargetLateralAccel @1;
|
||||
setSpeedLimitOffset @2;
|
||||
setSpeedLimitControl @3;
|
||||
setCurveSpeedControl @4;
|
||||
setVisionCurveSpeedControl @5;
|
||||
setLogLevel @6;
|
||||
setVisionCurveTargetLatA @7;
|
||||
setVisionCurveMinTargetV @8;
|
||||
reloadSettings @9;
|
||||
saveSettings @10;
|
||||
setEnableSpeed @11;
|
||||
setVisionCurveUseEnableSpeed @12;
|
||||
setCurveUseEnableSpeed @13;
|
||||
setSpeedLimitUseEnableSpeed @14;
|
||||
setHoldLastSeenSpeedLimit @15;
|
||||
setCurveTargetJerk @16;
|
||||
setCurveTargetAccel @17;
|
||||
setCurveTargetOffset @18;
|
||||
setDefaultLaneWidth @19;
|
||||
setCurveTargetLatA @20;
|
||||
loadDefaultSettings @21;
|
||||
loadRecommendedSettings @22;
|
||||
setSlowDownForNextSpeedLimit @23;
|
||||
setSpeedUpForNextSpeedLimit @24;
|
||||
setHoldSpeedLimitWhileChangingSetSpeed @25;
|
||||
}
|
||||
|
||||
enum SpeedLimitOffsetType {
|
||||
static @0;
|
||||
percent @1;
|
||||
}
|
||||
|
||||
struct MapdIn @0xc86a3d38d13eb3ef {
|
||||
type @0 :MapdInputType;
|
||||
float @1 :Float32;
|
||||
str @2 :Text;
|
||||
bool @3 :Bool;
|
||||
}
|
||||
|
||||
enum RoadContext {
|
||||
freeway @0;
|
||||
city @1;
|
||||
unknown @2;
|
||||
}
|
||||
|
||||
struct MapdOut @0xa4f1eb3323f5f582 {
|
||||
wayName @0 :Text;
|
||||
wayRef @1 :Text;
|
||||
roadName @2 :Text;
|
||||
speedLimit @3 :Float32;
|
||||
nextSpeedLimit @4 :Float32;
|
||||
nextSpeedLimitDistance @5 :Float32;
|
||||
hazard @6 :Text;
|
||||
nextHazard @7 :Text;
|
||||
nextHazardDistance @8 :Float32;
|
||||
advisorySpeed @9 :Float32;
|
||||
nextAdvisorySpeed @10 :Float32;
|
||||
nextAdvisorySpeedDistance @11 :Float32;
|
||||
oneWay @12 :Bool;
|
||||
lanes @13 :UInt8;
|
||||
tileLoaded @14 :Bool;
|
||||
speedLimitOffset @15 :Float32;
|
||||
suggestedSpeed @16 :Float32;
|
||||
estimatedRoadWidth @17 :Float32;
|
||||
roadContext @18 :RoadContext;
|
||||
distanceFromWayCenter @19 :Float32;
|
||||
visionCurveSpeed @20 :Float32;
|
||||
curveSpeed @21 :Float32;
|
||||
}
|
||||
|
||||
+3
-3
@@ -2639,9 +2639,9 @@ struct Event {
|
||||
customReserved14 @140 :Custom.CustomReserved14;
|
||||
customReserved15 @141 :Custom.CustomReserved15;
|
||||
customReserved16 @142 :Custom.CustomReserved16;
|
||||
customReserved17 @143 :Custom.CustomReserved17;
|
||||
customReserved18 @144 :Custom.CustomReserved18;
|
||||
customReserved19 @145 :Custom.CustomReserved19;
|
||||
mapdExtendedOut @143 :Custom.MapdExtendedOut;
|
||||
mapdIn @144 :Custom.MapdIn;
|
||||
mapdOut @145 :Custom.MapdOut;
|
||||
|
||||
# *********** legacy + deprecated ***********
|
||||
model @9 :Legacy.ModelData; # TODO: rename modelV2 and mark this as deprecated
|
||||
|
||||
@@ -91,6 +91,7 @@ _services: dict[str, tuple] = {
|
||||
"modelDataV2SP": (True, 20.),
|
||||
"navigationd": (True, 3.),
|
||||
"liveLocationKalman": (True, 20.),
|
||||
"mapdOut": (True, 20., 20),
|
||||
|
||||
# debug
|
||||
"uiDebug": (True, 0., 1),
|
||||
|
||||
@@ -130,6 +130,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"Version", {PERSISTENT, STRING}},
|
||||
|
||||
// --- sunnypilot params --- //
|
||||
{"AccelPersonality", {PERSISTENT | BACKUP, INT, std::to_string(static_cast<int>(cereal::LongitudinalPlanSP::AccelerationPersonality::NORMAL))}},
|
||||
{"AccelPersonalityEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
|
||||
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
@@ -146,6 +148,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"CustomAccShortPressIncrement", {PERSISTENT | BACKUP, INT, "1"}},
|
||||
{"DeviceBootMode", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"DevUIInfo", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"DynamicFollow", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"EnableCopyparty", {PERSISTENT | BACKUP, BOOL}},
|
||||
{"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}},
|
||||
{"GreenLightAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
@@ -168,11 +171,19 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"RoadEdgeLaneChangeEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
// toyota specific params
|
||||
{"ToyotaAutoHold", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaEnhancedBsm", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaTSS2Long", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaDriveMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
// MADS params
|
||||
{"Mads", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||
{"MadsMainCruiseAllowed", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||
@@ -228,6 +239,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"LaneTurnDesire", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"LaneTurnValue", {PERSISTENT | BACKUP, FLOAT, "19.0"}},
|
||||
|
||||
|
||||
// mapd v020
|
||||
{"MapdSettings", {PERSISTENT | BACKUP, JSON}},
|
||||
// mapd
|
||||
{"MapAdvisorySpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT}},
|
||||
{"MapdVersion", {PERSISTENT, STRING}},
|
||||
|
||||
+1
-1
Submodule opendbc_repo updated: c32e79f3c6...2bcc263692
@@ -10,7 +10,7 @@ from cereal import car, log, custom
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper
|
||||
from openpilot.common.swaglog import cloudlog, ForwardingHandler
|
||||
|
||||
from opendbc.safety import ALTERNATIVE_EXPERIENCE
|
||||
from opendbc.car import DT_CTRL, structs
|
||||
from opendbc.car.can_definitions import CanData, CanRecvCallable, CanSendCallable
|
||||
from opendbc.car.carlog import carlog
|
||||
@@ -123,7 +123,13 @@ class Car:
|
||||
self.CI, self.CP, self.CP_SP = CI, CI.CP, CI.CP_SP
|
||||
self.RI = RI
|
||||
|
||||
# set alternative experiences from parameters
|
||||
sp_toyota_auto_brake_hold = self.params.get_bool("ToyotaAutoHold")
|
||||
self.CP.alternativeExperience = 0
|
||||
if sp_toyota_auto_brake_hold:
|
||||
self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.ALLOW_AEB
|
||||
|
||||
|
||||
# mads
|
||||
set_alternative_experience(self.CP, self.CP_SP, self.params)
|
||||
set_car_specific_params(self.CP, self.CP_SP, self.params)
|
||||
|
||||
@@ -58,7 +58,7 @@ class DesireHelper:
|
||||
def get_lane_change_direction(CS):
|
||||
return LaneChangeDirection.left if CS.leftBlinker else LaneChangeDirection.right
|
||||
|
||||
def update(self, carstate, lateral_active, lane_change_prob):
|
||||
def update(self, carstate, lateral_active, lane_change_prob, left_edge_detected, right_edge_detected):
|
||||
self.alc.update_params()
|
||||
self.lane_turn_controller.update_params()
|
||||
v_ego = carstate.vEgo
|
||||
@@ -90,8 +90,8 @@ class DesireHelper:
|
||||
((carstate.steeringTorque > 0 and self.lane_change_direction == LaneChangeDirection.left) or
|
||||
(carstate.steeringTorque < 0 and self.lane_change_direction == LaneChangeDirection.right))
|
||||
|
||||
blindspot_detected = ((carstate.leftBlindspot and self.lane_change_direction == LaneChangeDirection.left) or
|
||||
(carstate.rightBlindspot and self.lane_change_direction == LaneChangeDirection.right))
|
||||
blindspot_detected = (((carstate.leftBlindspot or left_edge_detected) and self.lane_change_direction == LaneChangeDirection.left) or
|
||||
((carstate.rightBlindspot or right_edge_detected) and self.lane_change_direction == LaneChangeDirection.right))
|
||||
|
||||
self.alc.update_lane_change(blindspot_detected, carstate.brakePressed)
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.modeld.constants import index_function
|
||||
from openpilot.selfdrive.controls.radard import _LEAD_ACCEL_TAU
|
||||
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller import AccelPersonalityController
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.dynamic_personality.dynamic_follow import FollowDistanceController
|
||||
if __name__ == '__main__': # generating code
|
||||
from openpilot.third_party.acados.acados_template import AcadosModel, AcadosOcp, AcadosOcpSolver
|
||||
else:
|
||||
@@ -228,6 +230,8 @@ class LongitudinalMpc:
|
||||
self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
|
||||
self.reset()
|
||||
self.source = SOURCES[2]
|
||||
self.accel_controller = AccelPersonalityController()
|
||||
self.dynamic_follow = FollowDistanceController()
|
||||
|
||||
def reset(self):
|
||||
# self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
|
||||
@@ -328,10 +332,27 @@ class LongitudinalMpc:
|
||||
return lead_xv
|
||||
|
||||
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]
|
||||
|
||||
if self.dynamic_follow.is_enabled():
|
||||
t_follow = self.dynamic_follow.get_follow_distance_multiplier(v_ego)
|
||||
#print(f"DEBUG: dynamic_follow enabled, t_follow={t_follow:.3f}, v_ego={v_ego:.2f}, v_cruise={v_cruise:.2f}")
|
||||
else:
|
||||
t_follow = get_T_FOLLOW(personality)
|
||||
#print(f"DEBUG: dynamic_follow disabled, using personality t_follow={t_follow:.3f}, personality={personality}")
|
||||
|
||||
self.status = radarstate.leadOne.status or radarstate.leadTwo.status
|
||||
|
||||
# Get acceleration limits
|
||||
if self.accel_controller.is_enabled():
|
||||
min_accel = self.accel_controller.get_min_accel(v_ego)
|
||||
#print(f"DEBUG: accel_enabled=True, min_accel={min_accel:.3f}")
|
||||
else:
|
||||
min_accel = CRUISE_MIN_ACCEL
|
||||
#print(f"DEBUG: accel_enabled=False, using stock min_accel={min_accel}")
|
||||
|
||||
a_cruise_min = min_accel
|
||||
|
||||
lead_xv_0 = self.process_lead(radarstate.leadOne)
|
||||
lead_xv_1 = self.process_lead(radarstate.leadTwo)
|
||||
|
||||
@@ -350,7 +371,7 @@ class LongitudinalMpc:
|
||||
|
||||
# Fake an obstacle for cruise, this ensures smooth acceleration to set speed
|
||||
# when the leads are no factor.
|
||||
v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05)
|
||||
v_lower = v_ego + (T_IDXS * a_cruise_min * 1.05)
|
||||
# TODO does this make sense when max_a is negative?
|
||||
v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05)
|
||||
v_cruise_clipped = np.clip(v_cruise * np.ones(N+1),
|
||||
|
||||
@@ -124,7 +124,13 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
|
||||
|
||||
if mode == 'acc':
|
||||
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
|
||||
if self.accel_controller.is_enabled():
|
||||
max_accel = self.accel_controller.get_max_accel(v_ego)
|
||||
#print(f"Vibe personality active - max accel: {max_accel:.3f}")
|
||||
accel_clip = [ACCEL_MIN, max_accel]
|
||||
else:
|
||||
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
|
||||
|
||||
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
|
||||
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
|
||||
else:
|
||||
@@ -149,6 +155,10 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
# Get new v_cruise and a_desired from Smart Cruise Control and Speed Limit Assist
|
||||
v_cruise, self.a_desired = LongitudinalPlannerSP.update_targets(self, sm, self.v_desired_filter.x, self.a_desired, v_cruise)
|
||||
|
||||
if sm.valid['mapdOut']:
|
||||
if sm['mapdOut'].suggestedSpeed > 0 and v_cruise > sm['mapdOut'].suggestedSpeed:
|
||||
v_cruise = sm['mapdOut'].suggestedSpeed
|
||||
|
||||
if force_slow_decel:
|
||||
v_cruise = 0.0
|
||||
|
||||
|
||||
@@ -27,12 +27,12 @@ def main():
|
||||
longitudinal_planner = LongitudinalPlanner(CP, CP_SP)
|
||||
pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'longitudinalPlanSP'])
|
||||
sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState',
|
||||
'liveMapDataSP', 'carStateSP', gps_location_service],
|
||||
'liveMapDataSP', 'carStateSP', 'mapdOut', gps_location_service],
|
||||
poll='carState')
|
||||
|
||||
while True:
|
||||
sm.update()
|
||||
longitudinal_planner.sla.update_car_state(sm['carState'])
|
||||
#longitudinal_planner.sla.update_car_state(sm['carState'])
|
||||
if sm.updated['modelV2']:
|
||||
longitudinal_planner.update(sm)
|
||||
longitudinal_planner.publish(sm, pm)
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -51,8 +51,8 @@ def tg_compile(flags, model_name):
|
||||
for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
|
||||
flags = {
|
||||
'larch64': 'DEV=QCOM',
|
||||
'Darwin': 'DEV=CPU IMAGE=0',
|
||||
}.get(arch, 'DEV=LLVM IMAGE=0')
|
||||
'Darwin': f'DEV=CPU HOME={os.path.expanduser("~")} IMAGE=0', # tinygrad calls brew which needs a $HOME in the env
|
||||
}.get(arch, 'DEV=CPU CPU_LLVM=1 IMAGE=0')
|
||||
tg_compile(flags, model_name)
|
||||
|
||||
# Compile BIG model if USB GPU is available
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
from openpilot.system.hardware import TICI
|
||||
os.environ['DEV'] = 'QCOM' if TICI else 'LLVM'
|
||||
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
|
||||
from tinygrad.tensor import Tensor
|
||||
from tinygrad.dtype import dtypes
|
||||
import math
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
from openpilot.system.hardware import TICI
|
||||
os.environ['DEV'] = 'QCOM' if TICI else 'LLVM'
|
||||
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
|
||||
USBGPU = "USBGPU" in os.environ
|
||||
if USBGPU:
|
||||
os.environ['DEV'] = 'AMD'
|
||||
@@ -33,7 +33,7 @@ from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from
|
||||
|
||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||
from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase
|
||||
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
|
||||
|
||||
PROCESS_NAME = "selfdrive.modeld.modeld"
|
||||
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
||||
@@ -298,6 +298,7 @@ def main(demo=False):
|
||||
prev_action = log.ModelDataV2.Action()
|
||||
|
||||
DH = DesireHelper()
|
||||
RELC = RoadEdgeLaneChangeController(params.get_bool("RoadEdgeLaneChangeEnabled"))
|
||||
|
||||
while True:
|
||||
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
|
||||
@@ -395,7 +396,10 @@ def main(demo=False):
|
||||
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
|
||||
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
|
||||
lane_change_prob = l_lane_change_prob + r_lane_change_prob
|
||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
|
||||
RELC.update(modelv2_send.modelV2.roadEdgeStds, modelv2_send.modelV2.laneLineProbs)
|
||||
mdv2sp_send.modelDataV2SP.leftLaneChangeEdgeBlock = RELC.left_edge_detected
|
||||
mdv2sp_send.modelDataV2SP.rightLaneChangeEdgeBlock = RELC.right_edge_detected
|
||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, RELC.left_edge_detected, RELC.right_edge_detected)
|
||||
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
|
||||
|
||||
@@ -88,7 +88,7 @@ class SelfdriveD(CruiseHelper):
|
||||
# TODO: de-couple selfdrived with card/conflate on carState without introducing controls mismatches
|
||||
self.car_state_sock = messaging.sub_sock('carState', timeout=20)
|
||||
|
||||
ignore = self.sensor_packets + self.gps_packets + ['alertDebug'] + ['modelDataV2SP']
|
||||
ignore = self.sensor_packets + self.gps_packets + ['alertDebug'] + ['modelDataV2SP'] + ['navigationd']
|
||||
if SIMULATION:
|
||||
ignore += ['driverCameraState', 'managerState']
|
||||
if REPLAY:
|
||||
@@ -98,7 +98,7 @@ class SelfdriveD(CruiseHelper):
|
||||
'carOutput', 'driverMonitoringState', 'longitudinalPlan', 'livePose', 'liveDelay',
|
||||
'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters',
|
||||
'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userBookmark', 'audioFeedback',
|
||||
'modelDataV2SP', 'longitudinalPlanSP'] + \
|
||||
'modelDataV2SP', 'longitudinalPlanSP', 'navigationd'] + \
|
||||
self.camera_packets + self.sensor_packets + self.gps_packets,
|
||||
ignore_alive=ignore, ignore_avg_freq=ignore,
|
||||
ignore_valid=ignore, frequency=int(1/DT_CTRL))
|
||||
@@ -230,8 +230,8 @@ class SelfdriveD(CruiseHelper):
|
||||
|
||||
# Disable on rising edge of accelerator or brake. Also disable on brake when speed > 0
|
||||
if (CS.gasPressed and not self.CS_prev.gasPressed and self.disengage_on_accelerator) or \
|
||||
(CS.brakePressed and (not self.CS_prev.brakePressed or not CS.standstill)) or \
|
||||
(CS.regenBraking and (not self.CS_prev.regenBraking or not CS.standstill)):
|
||||
(CS.brakePressed and (not self.CS_prev.brakePressed or not CS.standstill)) or \
|
||||
(CS.regenBraking and (not self.CS_prev.regenBraking or not CS.standstill)):
|
||||
self.events.add(EventName.pedalPressed)
|
||||
|
||||
# Create events for temperature, disk space, and memory
|
||||
@@ -292,9 +292,15 @@ class SelfdriveD(CruiseHelper):
|
||||
# Handle lane change
|
||||
if self.sm['modelV2'].meta.laneChangeState == LaneChangeState.preLaneChange:
|
||||
direction = self.sm['modelV2'].meta.laneChangeDirection
|
||||
mdv2sp = self.sm['modelDataV2SP']
|
||||
|
||||
if (CS.leftBlindspot and direction == LaneChangeDirection.left) or \
|
||||
(CS.rightBlindspot and direction == LaneChangeDirection.right):
|
||||
self.events.add(EventName.laneChangeBlocked)
|
||||
|
||||
elif mdv2sp.leftLaneChangeEdgeBlock or mdv2sp.rightLaneChangeEdgeBlock:
|
||||
self.events_sp.add(custom.OnroadEventSP.EventName.laneChangeRoadEdge)
|
||||
|
||||
else:
|
||||
if direction == LaneChangeDirection.left:
|
||||
self.events.add(EventName.preLaneChangeLeft)
|
||||
|
||||
@@ -33,6 +33,42 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) {
|
||||
"../assets/icons/experimental_white.svg",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"ToyotaDriveMode",
|
||||
tr("Enable drive mode btn link"),
|
||||
tr("Links cars drive mode btn with accel personalities based on personality (i.e., relaxed, standard, sport)"),
|
||||
"../assets/offroad/icon_blank.png",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"ToyotaAutoHold",
|
||||
tr("Toyota: Auto Brake Hold FOR TSS2 HYBRID CARS"),
|
||||
tr("As you may auto brake hold currently supported by openpilot, this feature will allow sunnypilot to automatically hold the vehicle at a stop when the lead car is stopped. (TSS2 Hybird only)"),
|
||||
"../assets/offroad/icon_blank.png",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"ToyotaEnhancedBsm",
|
||||
tr("Toyota: Prius TSS2 BSM and some tssp"),
|
||||
tr("Add support for BSM."),
|
||||
"../assets/offroad/icon_blank.png",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"ToyotaTSS2Long",
|
||||
tr("Toyota: custom tune"),
|
||||
tr("idk something gas and brake"),
|
||||
"../assets/offroad/icon_blank.png",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"ToyotaStockLongitudinal",
|
||||
tr("Toyota: Stock Toyota Longitudinal"),
|
||||
tr("This feature will allow sunnypilot to use the stock Toyota longitudinal control instead of the sunnypilot longitudinal control. "
|
||||
""),
|
||||
"../assets/offroad/icon_blank.png",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"DisengageOnAccelerator",
|
||||
tr("Disengage on Accelerator Pedal"),
|
||||
@@ -85,7 +121,15 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) {
|
||||
"your steering wheel distance button."),
|
||||
"../assets/icons/speed_limit.png",
|
||||
longi_button_texts);
|
||||
|
||||
// accel controller
|
||||
std::vector<QString> accel_personality_texts{tr("Sport"), tr("Normal"), tr("Eco")};
|
||||
accel_personality_setting = new ButtonParamControlSP("AccelPersonality", tr("Acceleration Personality"),
|
||||
tr("Normal is recommended. In sport mode, sunnypilot will provide aggressive acceleration for a dynamic driving experience. "
|
||||
"In eco mode, sunnypilot will apply smoother and more relaxed acceleration. On supported cars, you can cycle through these "
|
||||
"acceleration personality within Onroad Settings on the driving screen."),
|
||||
"",
|
||||
accel_personality_texts);
|
||||
accel_personality_setting->showDescription();
|
||||
// set up uiState update for personality setting
|
||||
QObject::connect(uiState(), &UIState::uiUpdate, this, &TogglesPanel::updateState);
|
||||
|
||||
@@ -113,6 +157,7 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) {
|
||||
// insert longitudinal personality after NDOG toggle
|
||||
if (param == "DisengageOnAccelerator") {
|
||||
addItem(long_personality_setting);
|
||||
addItem(accel_personality_setting);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +178,13 @@ void TogglesPanel::updateState(const UIState &s) {
|
||||
}
|
||||
uiState()->scene.personality = personality;
|
||||
}
|
||||
if (sm.updated("longitudinalPlanSP")) {
|
||||
auto accel_personality = sm["longitudinalPlanSP"].getLongitudinalPlanSP().getAccelPersonality();
|
||||
if (accel_personality != s.scene.accel_personality && s.scene.started && isVisible()) {
|
||||
accel_personality_setting->setCheckedButton(static_cast<int>(accel_personality));
|
||||
}
|
||||
uiState()->scene.accel_personality = accel_personality;
|
||||
}
|
||||
}
|
||||
|
||||
void TogglesPanel::expandToggleDescription(const QString ¶m) {
|
||||
@@ -179,10 +231,12 @@ void TogglesPanel::updateToggles() {
|
||||
experimental_mode_toggle->setEnabled(true);
|
||||
experimental_mode_toggle->setDescription(e2e_description);
|
||||
long_personality_setting->setEnabled(true);
|
||||
accel_personality_setting->setEnabled(true);
|
||||
} else {
|
||||
// no long for now
|
||||
experimental_mode_toggle->setEnabled(false);
|
||||
long_personality_setting->setEnabled(false);
|
||||
accel_personality_setting->setEnabled(true);
|
||||
params.remove("ExperimentalMode");
|
||||
|
||||
const QString unavailable = tr("Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control.");
|
||||
|
||||
@@ -88,6 +88,7 @@ protected:
|
||||
Params params;
|
||||
std::map<std::string, ParamControl*> toggles;
|
||||
ButtonParamControl *long_personality_setting;
|
||||
ButtonParamControl *accel_personality_setting;
|
||||
|
||||
virtual void updateToggles();
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ void ModelRenderer::draw(QPainter &painter, const QRect &surface_rect) {
|
||||
|
||||
update_model(model, lead_one);
|
||||
drawLaneLines(painter);
|
||||
drawPath(painter, model, surface_rect.height());
|
||||
drawPath(painter, model, surface_rect.height(), surface_rect.width());
|
||||
|
||||
if (longitudinal_control && sm.alive("radarState")) {
|
||||
update_leads(radar_state, model.getPosition());
|
||||
@@ -92,7 +92,7 @@ void ModelRenderer::drawLaneLines(QPainter &painter) {
|
||||
}
|
||||
}
|
||||
|
||||
void ModelRenderer::drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, int height) {
|
||||
void ModelRenderer::drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, int height, int width) {
|
||||
QLinearGradient bg(0, height, 0, 0);
|
||||
if (experimental_mode) {
|
||||
// The first half of track_vertices are the points for the right side of the path
|
||||
@@ -127,6 +127,9 @@ void ModelRenderer::drawPath(QPainter &painter, const cereal::ModelDataV2::Reade
|
||||
|
||||
painter.setBrush(bg);
|
||||
painter.drawPolygon(track_vertices);
|
||||
|
||||
//LongFuel(painter,height, width);
|
||||
//LateralFuel(painter, height, width);
|
||||
}
|
||||
|
||||
void ModelRenderer::updatePathGradient(QLinearGradient &bg) {
|
||||
@@ -173,6 +176,195 @@ QColor ModelRenderer::blendColors(const QColor &start, const QColor &end, float
|
||||
(1 - t) * start.alphaF() + t * end.alphaF());
|
||||
}
|
||||
|
||||
void ModelRenderer::drawGaugeBackground(QPainter &painter, qreal centerX, qreal centerY) {
|
||||
const qreal backgroundSize = GAUGE_SIZE * BACKGROUND_SIZE_MULTIPLIER;
|
||||
|
||||
// Draw circular background
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(BACKGROUND_COLOR);
|
||||
painter.drawEllipse(QPointF(centerX, centerY), backgroundSize / 2, backgroundSize / 2);
|
||||
|
||||
// Draw border
|
||||
QPen borderPen(BORDER_COLOR);
|
||||
borderPen.setWidth(BORDER_PEN_WIDTH);
|
||||
painter.setPen(borderPen);
|
||||
painter.drawEllipse(QPointF(centerX, centerY), backgroundSize / 2 + 1, backgroundSize / 2 + 1);
|
||||
|
||||
// Draw background semicircle
|
||||
QPen semicirclePen(GAUGE_BACKGROUND_COLOR);
|
||||
semicirclePen.setWidth(GAUGE_PEN_WIDTH);
|
||||
semicirclePen.setCapStyle(Qt::RoundCap);
|
||||
painter.setPen(semicirclePen);
|
||||
painter.drawArc(QRectF(centerX - GAUGE_SIZE / 2, centerY - GAUGE_SIZE / 2,
|
||||
GAUGE_SIZE, GAUGE_SIZE), 0, SEMICIRCLE_SPAN);
|
||||
}
|
||||
|
||||
QColor ModelRenderer::getIndicatorColor(float absoluteValue, float lowThreshold, float highThreshold) {
|
||||
if (absoluteValue < lowThreshold) {
|
||||
return LOW_INDICATOR_COLOR;
|
||||
} else if (absoluteValue < highThreshold) {
|
||||
return MODERATE_INDICATOR_COLOR;
|
||||
} else {
|
||||
return HIGH_INDICATOR_COLOR;
|
||||
}
|
||||
}
|
||||
|
||||
int ModelRenderer::calculateSpanAngle(float absoluteValue, float maxValue) {
|
||||
const int spanAngle = static_cast<int>(QUARTER_CIRCLE_SPAN * (absoluteValue / maxValue));
|
||||
return std::clamp(spanAngle, 0, QUARTER_CIRCLE_SPAN);
|
||||
}
|
||||
|
||||
void ModelRenderer::drawGaugeArc(QPainter &painter, qreal centerX, qreal centerY,
|
||||
float value, bool isPositive, const QString &label) {
|
||||
const float absoluteValue = std::abs(value);
|
||||
|
||||
if (absoluteValue <= MIN_THRESHOLD) {
|
||||
return; // Skip drawing if value is too small
|
||||
}
|
||||
|
||||
// Set up the arc rectangle
|
||||
const QRectF arcRect(centerX - GAUGE_SIZE / 2, centerY - GAUGE_SIZE / 2,
|
||||
GAUGE_SIZE, GAUGE_SIZE);
|
||||
|
||||
// Configure pen for the indicator arc
|
||||
QPen indicatorPen;
|
||||
indicatorPen.setWidth(GAUGE_PEN_WIDTH);
|
||||
indicatorPen.setCapStyle(Qt::RoundCap);
|
||||
painter.setPen(indicatorPen);
|
||||
|
||||
// Draw the arc based on direction
|
||||
const int spanAngle = calculateSpanAngle(absoluteValue, 1.0f); // Adjust max value as needed
|
||||
if (isPositive) {
|
||||
painter.drawArc(arcRect, STARTING_ANGLE, spanAngle);
|
||||
} else {
|
||||
painter.drawArc(arcRect, STARTING_ANGLE, -spanAngle);
|
||||
}
|
||||
|
||||
// Draw center label
|
||||
painter.setPen(Qt::white);
|
||||
QFont font = painter.font();
|
||||
font.setPixelSize(20);
|
||||
font.setBold(true);
|
||||
painter.setFont(font);
|
||||
painter.drawText(QRectF(centerX - 50, centerY + 10, 100, 20), Qt::AlignCenter, label);
|
||||
}
|
||||
|
||||
void ModelRenderer::LongFuel(QPainter &painter, int height, int width) {
|
||||
const qreal rectWidth = static_cast<qreal>(width);
|
||||
const qreal rectHeight = static_cast<qreal>(height);
|
||||
|
||||
UIState *s = uiState();
|
||||
if (!s || !s->sm) {
|
||||
return; // Safety check
|
||||
}
|
||||
|
||||
// Get current acceleration
|
||||
const float currentAcceleration = (*s->sm)["carControl"].getCarControl().getActuators().getAccel();
|
||||
const float absoluteAcceleration = std::abs(currentAcceleration);
|
||||
|
||||
// Calculate gauge position
|
||||
const qreal centerX = rectWidth / 17;
|
||||
const qreal centerY = rectHeight / 2 + 120;
|
||||
|
||||
// Draw gauge background
|
||||
drawGaugeBackground(painter, centerX, centerY);
|
||||
|
||||
// Skip drawing arc if acceleration is too small
|
||||
if (absoluteAcceleration <= MIN_THRESHOLD) {
|
||||
drawGaugeArc(painter, centerX, centerY, 0.0f, true, "LONG");
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine indicator color based on acceleration magnitude
|
||||
const QColor indicatorColor = getIndicatorColor(absoluteAcceleration, 0.3f, 0.6f);
|
||||
|
||||
// Calculate span angle (scale for better visibility)
|
||||
const int spanAngle = static_cast<int>(QUARTER_CIRCLE_SPAN * absoluteAcceleration);
|
||||
const int clampedSpanAngle = std::clamp(spanAngle, 0, QUARTER_CIRCLE_SPAN);
|
||||
|
||||
// Draw the acceleration arc
|
||||
QPen indicatorPen(indicatorColor);
|
||||
indicatorPen.setWidth(GAUGE_PEN_WIDTH);
|
||||
indicatorPen.setCapStyle(Qt::RoundCap);
|
||||
painter.setPen(indicatorPen);
|
||||
|
||||
const QRectF arcRect(centerX - GAUGE_SIZE / 2, centerY - GAUGE_SIZE / 2,
|
||||
GAUGE_SIZE, GAUGE_SIZE);
|
||||
|
||||
// Draw arc based on acceleration direction
|
||||
if (currentAcceleration > 0) {
|
||||
painter.drawArc(arcRect, STARTING_ANGLE, -clampedSpanAngle); // Left side for positive
|
||||
} else {
|
||||
painter.drawArc(arcRect, STARTING_ANGLE, clampedSpanAngle); // Right side for negative
|
||||
}
|
||||
|
||||
// Draw center label
|
||||
painter.setPen(Qt::white);
|
||||
QFont font = painter.font();
|
||||
font.setPixelSize(20);
|
||||
font.setBold(true);
|
||||
painter.setFont(font);
|
||||
painter.drawText(QRectF(centerX - 50, centerY + 10, 100, 20), Qt::AlignCenter, "LONG");
|
||||
}
|
||||
|
||||
void ModelRenderer::LateralFuel(QPainter &painter, int height, int width) {
|
||||
const qreal rectWidth = static_cast<qreal>(width);
|
||||
const qreal rectHeight = static_cast<qreal>(height);
|
||||
|
||||
UIState *s = uiState();
|
||||
if (!s || !s->sm) {
|
||||
return; // Safety check
|
||||
}
|
||||
|
||||
// Get current steering angle
|
||||
const float currentLateral = (*s->sm)["carState"].getCarState().getSteeringAngleDeg();
|
||||
const float absoluteLateral = std::abs(currentLateral);
|
||||
|
||||
// Calculate gauge position
|
||||
const qreal centerX = rectWidth / 17;
|
||||
const qreal centerY = rectHeight / 2 - 120;
|
||||
|
||||
// Draw gauge background
|
||||
drawGaugeBackground(painter, centerX, centerY);
|
||||
|
||||
// Skip drawing arc if lateral force is too small
|
||||
if (absoluteLateral <= 0.1f) {
|
||||
drawGaugeArc(painter, centerX, centerY, 0.0f, true, "LAT");
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine indicator color based on lateral force magnitude
|
||||
const QColor indicatorColor = getIndicatorColor(absoluteLateral, 5.0f, 15.0f);
|
||||
|
||||
// Calculate span angle (normalized to max expected steering angle)
|
||||
const float maxSteeringAngle = 15.0f; // Adjust based on your vehicle's characteristics
|
||||
const int spanAngle = static_cast<int>(QUARTER_CIRCLE_SPAN * (absoluteLateral / maxSteeringAngle));
|
||||
const int clampedSpanAngle = std::clamp(spanAngle, 0, QUARTER_CIRCLE_SPAN);
|
||||
|
||||
// Draw the lateral arc
|
||||
QPen indicatorPen(indicatorColor);
|
||||
indicatorPen.setWidth(GAUGE_PEN_WIDTH);
|
||||
indicatorPen.setCapStyle(Qt::RoundCap);
|
||||
painter.setPen(indicatorPen);
|
||||
|
||||
const QRectF arcRect(centerX - GAUGE_SIZE / 2, centerY - GAUGE_SIZE / 2,
|
||||
GAUGE_SIZE, GAUGE_SIZE);
|
||||
|
||||
// Draw arc based on steering direction
|
||||
if (currentLateral < 0) {
|
||||
painter.drawArc(arcRect, STARTING_ANGLE, -clampedSpanAngle); // Left turn
|
||||
} else {
|
||||
painter.drawArc(arcRect, STARTING_ANGLE, clampedSpanAngle); // Right turn
|
||||
}
|
||||
|
||||
// Draw center label
|
||||
painter.setPen(Qt::white);
|
||||
QFont font = painter.font();
|
||||
font.setPixelSize(20);
|
||||
font.setBold(true);
|
||||
painter.setFont(font);
|
||||
painter.drawText(QRectF(centerX - 50, centerY + 10, 100, 20), Qt::AlignCenter, "LAT");
|
||||
}
|
||||
void ModelRenderer::drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data,
|
||||
const QPointF &vd, const QRect &surface_rect) {
|
||||
const float speedBuff = 10.;
|
||||
|
||||
@@ -29,6 +29,8 @@ public:
|
||||
ModelRenderer() {}
|
||||
void setTransform(const Eigen::Matrix3f &transform) { car_space_transform = transform; }
|
||||
void draw(QPainter &painter, const QRect &surface_rect);
|
||||
void LongFuel(QPainter &p, int height, int width);
|
||||
void LateralFuel(QPainter &p, int height, int width);
|
||||
|
||||
protected:
|
||||
bool mapToScreen(float in_x, float in_y, float in_z, QPointF *out);
|
||||
@@ -38,7 +40,16 @@ protected:
|
||||
void update_leads(const cereal::RadarState::Reader &radar_state, const cereal::XYZTData::Reader &line);
|
||||
virtual void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead);
|
||||
void drawLaneLines(QPainter &painter);
|
||||
void drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, int height);
|
||||
void drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, int height, int width);
|
||||
|
||||
// Gauge helper methods
|
||||
void drawGaugeBackground(QPainter &painter, qreal centerX, qreal centerY);
|
||||
void drawGaugeArc(QPainter &painter, qreal centerX, qreal centerY,
|
||||
float value, bool isPositive, const QString &label);
|
||||
QColor getIndicatorColor(float absoluteValue, float lowThreshold, float highThreshold);
|
||||
int calculateSpanAngle(float absoluteValue, float maxValue);
|
||||
|
||||
|
||||
void updatePathGradient(QLinearGradient &bg);
|
||||
QColor blendColors(const QColor &start, const QColor &end, float t);
|
||||
|
||||
@@ -55,4 +66,22 @@ protected:
|
||||
QPointF lead_vertices[2] = {};
|
||||
Eigen::Matrix3f car_space_transform = Eigen::Matrix3f::Zero();
|
||||
QRectF clip_region;
|
||||
|
||||
// Gauge configuration constants
|
||||
static constexpr qreal GAUGE_SIZE = 140.0;
|
||||
static constexpr qreal BACKGROUND_SIZE_MULTIPLIER = 1.4;
|
||||
static constexpr qreal GAUGE_PEN_WIDTH = 30.0;
|
||||
static constexpr qreal BORDER_PEN_WIDTH = 2.0;
|
||||
static constexpr int SEMICIRCLE_SPAN = 180 * 16;
|
||||
static constexpr int QUARTER_CIRCLE_SPAN = 90 * 16;
|
||||
static constexpr int STARTING_ANGLE = 90 * 16;
|
||||
static constexpr qreal MIN_THRESHOLD = 0.01;
|
||||
|
||||
// Color constants - Note: QColor cannot be constexpr, use inline static const instead
|
||||
inline static const QColor BACKGROUND_COLOR = QColor(0, 0, 0, 80);
|
||||
inline static const QColor BORDER_COLOR = QColor(0, 0, 0, 100);
|
||||
inline static const QColor GAUGE_BACKGROUND_COLOR = QColor(50, 50, 50);
|
||||
inline static const QColor LOW_INDICATOR_COLOR = QColor(23, 241, 66, 200);
|
||||
inline static const QColor MODERATE_INDICATOR_COLOR = QColor(255, 166, 0, 200);
|
||||
inline static const QColor HIGH_INDICATOR_COLOR = QColor(245, 0, 0, 200);
|
||||
};
|
||||
|
||||
@@ -33,6 +33,12 @@ LaneChangeSettings::LaneChangeSettings(QWidget* parent) : QWidget(parent) {
|
||||
tr("Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering."),
|
||||
"../assets/offroad/icon_blank.png",
|
||||
},
|
||||
{
|
||||
"RoadEdgeLaneChangeEnabled",
|
||||
tr("Block Lane Change: Road Edge Detection"),
|
||||
tr("Enable this toggle to block lane change when road edge is detected on the stalk actuated side."),
|
||||
"../assets/offroad/icon_blank.png",
|
||||
}
|
||||
};
|
||||
|
||||
// Controls: Auto Lane Change Timer
|
||||
|
||||
@@ -75,6 +75,22 @@ LongitudinalPanel::LongitudinalPanel(QWidget *parent) : QWidget(parent) {
|
||||
|
||||
QObject::connect(uiState(), &UIState::offroadTransition, this, &LongitudinalPanel::refresh);
|
||||
|
||||
// Acceleration Personality
|
||||
AccelPersonalityControl = new ParamControlSP("AccelPersonalityEnabled",
|
||||
tr("Acceleration Personality"),
|
||||
tr("Controls acceleration behavior: Eco (efficient), Normal (balanced), Sport (responsive). "
|
||||
"Adjust how aggressively the vehicle accelerates while maintaining smooth operation."),
|
||||
"../assets/offroad/icon_shell.png");
|
||||
list->addItem(AccelPersonalityControl);
|
||||
|
||||
// Dynamic Personality
|
||||
DynamicPersonalityControl = new ParamControlSP("DynamicFollow",
|
||||
tr("Following Distance Personality"),
|
||||
tr("Controls following distance and braking behavior: Relaxed (longer distance, gentler braking), Standard (balanced), Aggressive (shorter distance, firmer braking). "
|
||||
"Fine-tune your comfort level in traffic situations."),
|
||||
"../assets/offroad/icon_shell.png");
|
||||
list->addItem(DynamicPersonalityControl);
|
||||
|
||||
speedLimitSettings = new PushButtonSP(tr("Speed Limit"), 750, this);
|
||||
connect(speedLimitSettings, &QPushButton::clicked, [&]() {
|
||||
cruisePanelScroller->setLastScrollPosition();
|
||||
@@ -164,6 +180,10 @@ void LongitudinalPanel::refresh(bool _offroad) {
|
||||
dynamicExperimentalControl->refresh();
|
||||
SmartCruiseControlVision->refresh();
|
||||
SmartCruiseControlMap->refresh();
|
||||
AccelPersonalityControl->setEnabled(true);
|
||||
DynamicPersonalityControl->setEnabled(true);
|
||||
AccelPersonalityControl->refresh();
|
||||
DynamicPersonalityControl->refresh();
|
||||
} else {
|
||||
has_longitudinal_control = false;
|
||||
is_pcm_cruise = false;
|
||||
|
||||
@@ -36,6 +36,9 @@ private:
|
||||
ParamControl *SmartCruiseControlMap;
|
||||
ParamControl *intelligentCruiseButtonManagement = nullptr;
|
||||
ParamControl *dynamicExperimentalControl = nullptr;
|
||||
|
||||
ParamControlSP *AccelPersonalityControl;
|
||||
ParamControlSP *DynamicPersonalityControl;
|
||||
SpeedLimitSettings *speedLimitScreen;
|
||||
PushButtonSP *speedLimitSettings;
|
||||
};
|
||||
|
||||
@@ -44,8 +44,6 @@ void HudRendererSP::updateState(const UIState &s) {
|
||||
const auto car_params = sm["carParams"].getCarParams();
|
||||
const auto car_params_sp = sm["carParamsSP"].getCarParamsSP();
|
||||
const auto lp_sp = sm["longitudinalPlanSP"].getLongitudinalPlanSP();
|
||||
const auto lmd = sm["liveMapDataSP"].getLiveMapDataSP();
|
||||
|
||||
if (sm.updated("carParams")) {
|
||||
steerControlType = car_params.getSteerControlType();
|
||||
}
|
||||
@@ -54,37 +52,80 @@ void HudRendererSP::updateState(const UIState &s) {
|
||||
pcmCruiseSpeed = car_params_sp.getPcmCruiseSpeed();
|
||||
}
|
||||
|
||||
if (sm.updated("longitudinalPlanSP")) {
|
||||
speedLimit = lp_sp.getSpeedLimit().getResolver().getSpeedLimit() * speedConv;
|
||||
speedLimitLast = lp_sp.getSpeedLimit().getResolver().getSpeedLimitLast() * speedConv;
|
||||
speedLimitOffset = lp_sp.getSpeedLimit().getResolver().getSpeedLimitOffset() * speedConv;
|
||||
speedLimitValid = lp_sp.getSpeedLimit().getResolver().getSpeedLimitValid();
|
||||
speedLimitLastValid = lp_sp.getSpeedLimit().getResolver().getSpeedLimitLastValid();
|
||||
speedLimitFinalLast = lp_sp.getSpeedLimit().getResolver().getSpeedLimitFinalLast() * speedConv;
|
||||
speedLimitSource = lp_sp.getSpeedLimit().getResolver().getSource();
|
||||
speedLimitAssistState = lp_sp.getSpeedLimit().getAssist().getState();
|
||||
speedLimitAssistActive = lp_sp.getSpeedLimit().getAssist().getActive();
|
||||
smartCruiseControlVisionEnabled = lp_sp.getSmartCruiseControl().getVision().getEnabled();
|
||||
smartCruiseControlVisionActive = lp_sp.getSmartCruiseControl().getVision().getActive();
|
||||
smartCruiseControlMapEnabled = lp_sp.getSmartCruiseControl().getMap().getEnabled();
|
||||
smartCruiseControlMapActive = lp_sp.getSmartCruiseControl().getMap().getActive();
|
||||
}
|
||||
if (sm.alive("mapdOut") && sm.rcv_frame("mapdOut") > 0) {
|
||||
const auto mapd = sm["mapdOut"].getMapdOut();
|
||||
|
||||
// Road name can come from wayName, wayRef, or roadName
|
||||
wayName = QString::fromStdString(mapd.getWayName());
|
||||
wayRef = QString::fromStdString(mapd.getWayRef());
|
||||
QString mapdRoadName = QString::fromStdString(mapd.getRoadName());
|
||||
|
||||
if (!mapdRoadName.isEmpty()) {
|
||||
roadNameStr = mapdRoadName;
|
||||
} else if (!wayRef.isEmpty() && !wayName.isEmpty()) {
|
||||
roadNameStr = wayRef + " - " + wayName;
|
||||
} else if (!wayName.isEmpty()) {
|
||||
roadNameStr = wayName;
|
||||
} else if (!wayRef.isEmpty()) {
|
||||
roadNameStr = wayRef;
|
||||
} else {
|
||||
roadNameStr = "";
|
||||
}
|
||||
|
||||
greenLightAlert = lp_sp.getE2eAlerts().getGreenLightAlert();
|
||||
leadDepartAlert = lp_sp.getE2eAlerts().getLeadDepartAlert();
|
||||
|
||||
if (sm.updated("liveMapDataSP")) {
|
||||
roadNameStr = QString::fromStdString(lmd.getRoadName());
|
||||
speedLimitAheadValid = lmd.getSpeedLimitAheadValid();
|
||||
speedLimitAhead = lmd.getSpeedLimitAhead() * speedConv;
|
||||
speedLimitAheadDistance = lmd.getSpeedLimitAheadDistance();
|
||||
tileLoaded = mapd.getTileLoaded();
|
||||
|
||||
float mapdSpeedLimitRaw = mapd.getSpeedLimit();
|
||||
float mapdOffsetRaw = mapd.getSpeedLimitOffset();
|
||||
|
||||
|
||||
mapdSpeedLimit = mapdSpeedLimitRaw * speedConv;
|
||||
speedLimit = mapdSpeedLimit;
|
||||
speedLimitLast = mapdSpeedLimit;
|
||||
speedLimitOffset = mapdOffsetRaw * speedConv;
|
||||
speedLimitValid = tileLoaded && mapdSpeedLimitRaw > 0;
|
||||
speedLimitLastValid = speedLimitValid;
|
||||
speedLimitFinalLast = mapdSpeedLimit + speedLimitOffset;
|
||||
|
||||
if (tileLoaded) {
|
||||
speedLimitSource = 1; // MAP
|
||||
} else {
|
||||
speedLimitSource = 0; // NONE
|
||||
}
|
||||
|
||||
float nextSpeedLimitRaw = mapd.getNextSpeedLimit();
|
||||
speedLimitAheadValid = nextSpeedLimitRaw > 0 && tileLoaded;
|
||||
speedLimitAhead = nextSpeedLimitRaw * speedConv;
|
||||
speedLimitAheadDistance = mapd.getNextSpeedLimitDistance();
|
||||
|
||||
if (speedLimitAheadDistance < speedLimitAheadDistancePrev && speedLimitAheadValidFrame < SPEED_LIMIT_AHEAD_VALID_FRAME_THRESHOLD) {
|
||||
speedLimitAheadValidFrame++;
|
||||
} else if (speedLimitAheadDistance > speedLimitAheadDistancePrev && speedLimitAheadValidFrame > 0) {
|
||||
speedLimitAheadValidFrame--;
|
||||
}
|
||||
|
||||
// SCC data from mapd
|
||||
suggestedSpeed = mapd.getSuggestedSpeed() * speedConv;
|
||||
visionCurveSpeed = mapd.getVisionCurveSpeed() * speedConv;
|
||||
curveSpeed = mapd.getCurveSpeed() * speedConv;
|
||||
|
||||
smartCruiseControlVisionEnabled = visionCurveSpeed > 0;
|
||||
smartCruiseControlVisionActive = visionCurveSpeed > 0 && visionCurveSpeed < speedLimit;
|
||||
|
||||
smartCruiseControlMapEnabled = curveSpeed > 0;
|
||||
smartCruiseControlMapActive = curveSpeed > 0 && curveSpeed < speedLimit;
|
||||
|
||||
advisorySpeed = mapd.getAdvisorySpeed() * speedConv;
|
||||
nextAdvisorySpeed = mapd.getNextAdvisorySpeed() * speedConv;
|
||||
nextAdvisorySpeedDistance = mapd.getNextAdvisorySpeedDistance();
|
||||
}
|
||||
speedLimitAheadDistancePrev = speedLimitAheadDistance;
|
||||
|
||||
speedLimitAssistState = 0;
|
||||
speedLimitAssistActive = false;
|
||||
|
||||
static int reverse_delay = 0;
|
||||
bool reverse_allowed = false;
|
||||
if (car_state.getGearShifter() != cereal::CarState::GearShifter::REVERSE) {
|
||||
@@ -108,7 +149,7 @@ void HudRendererSP::updateState(const UIState &s) {
|
||||
}
|
||||
|
||||
if (sm.updated(gps_source)) {
|
||||
gpsAccuracy = is_gps_location_external ? gpsLocation.getHorizontalAccuracy() : 1.0; // External reports accuracy, internal does not.
|
||||
gpsAccuracy = is_gps_location_external ? gpsLocation.getHorizontalAccuracy() : 1.0;
|
||||
altitude = gpsLocation.getAltitude();
|
||||
bearingAccuracyDeg = gpsLocation.getBearingAccuracyDeg();
|
||||
bearingDeg = gpsLocation.getBearingDeg();
|
||||
@@ -176,12 +217,7 @@ void HudRendererSP::updateState(const UIState &s) {
|
||||
navigationDistance = QString::number(std::round(dist / 50.0) * 50) + " m";
|
||||
}
|
||||
} else {
|
||||
float dist_km = dist / 1000;
|
||||
if (dist_km >= 10.0) {
|
||||
navigationDistance = QString::number(std::floor(dist_km)) + " km";
|
||||
} else {
|
||||
navigationDistance = QString::number(dist_km, 'f', 1) + " km";
|
||||
}
|
||||
navigationDistance = QString::number(dist / 1000, 'f', 1) + " km";
|
||||
}
|
||||
} else {
|
||||
float dist_ft = dist * 3.28084f;
|
||||
@@ -193,12 +229,7 @@ void HudRendererSP::updateState(const UIState &s) {
|
||||
navigationDistance = QString::number((std::round(dist_ft / 50.0) * 50)) + " ft";
|
||||
}
|
||||
} else {
|
||||
float dist_mi = dist_ft / 5280;
|
||||
if (dist_mi >= 10.0) {
|
||||
navigationDistance = QString::number(std::floor(dist_mi)) + " mi";
|
||||
} else {
|
||||
navigationDistance = QString::number(dist_mi, 'f', 1) + " mi";
|
||||
}
|
||||
navigationDistance = QString::number(dist_ft / 5280, 'f', 1) + " mi";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,7 +323,7 @@ void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) {
|
||||
const int sign_height = 204;
|
||||
QRect sign_rect(sign_x, sign_y, sign_width, sign_height);
|
||||
|
||||
if (speedLimitAssistState == cereal::LongitudinalPlanSP::SpeedLimit::AssistState::PRE_ACTIVE) {
|
||||
if (speedLimitAssistState == 1) {
|
||||
speedLimitAssistFrame++;
|
||||
showSpeedLimit = speed_limit_assist_pre_active_pulse;
|
||||
drawSpeedLimitPreActiveArrow(p, sign_rect);
|
||||
@@ -305,7 +336,7 @@ void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) {
|
||||
drawSpeedLimitSigns(p, sign_rect);
|
||||
|
||||
// do not show during SLA's preActive state
|
||||
if (speedLimitAssistState != cereal::LongitudinalPlanSP::SpeedLimit::AssistState::PRE_ACTIVE) {
|
||||
if (speedLimitAssistState != 1) {
|
||||
drawUpcomingSpeedLimit(p);
|
||||
}
|
||||
}
|
||||
@@ -645,7 +676,7 @@ void HudRendererSP::drawSpeedLimitSigns(QPainter &p, QRect &sign_rect) {
|
||||
|
||||
void HudRendererSP::drawUpcomingSpeedLimit(QPainter &p) {
|
||||
bool speed_limit_ahead = speedLimitAheadValid && speedLimitAhead > 0 && speedLimitAhead != speedLimit && speedLimitAheadValidFrame > 0 &&
|
||||
speedLimitSource == cereal::LongitudinalPlanSP::SpeedLimit::Source::MAP;
|
||||
tileLoaded;
|
||||
if (!speed_limit_ahead) {
|
||||
return;
|
||||
}
|
||||
@@ -1096,15 +1127,10 @@ void HudRendererSP::drawNavigationHUD(QPainter &p, const QRect &surface_rect) {
|
||||
|
||||
const int container_width = 1080;
|
||||
const int container_height = 225;
|
||||
int container_x = (surface_rect.width() - container_width) / 2;
|
||||
const int container_x = (surface_rect.width() - container_width) / 2;
|
||||
const int container_y = 62;
|
||||
const int border_radius = 42;
|
||||
|
||||
|
||||
if (speedLimitAssistState == cereal::LongitudinalPlanSP::SpeedLimit::AssistState::PRE_ACTIVE) {
|
||||
container_x += 190;
|
||||
}
|
||||
|
||||
QRect container_rect(container_x, container_y, container_width, container_height);
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
@@ -1127,7 +1153,7 @@ void HudRendererSP::drawNavigationHUD(QPainter &p, const QRect &surface_rect) {
|
||||
// Distance
|
||||
p.setFont(InterFont(48, QFont::Bold));
|
||||
p.setPen(Qt::white);
|
||||
QRect distance_rect(icon_x - 25, icon_y + icon_size, icon_size + 50, 38);
|
||||
QRect distance_rect(icon_x, icon_y + icon_size, icon_size, 38);
|
||||
p.drawText(distance_rect, Qt::AlignCenter, navigationDistance);
|
||||
|
||||
const int then_section_width = 180;
|
||||
|
||||
@@ -85,7 +85,7 @@ private:
|
||||
bool speedLimitValid;
|
||||
bool speedLimitLastValid;
|
||||
float speedLimitFinalLast;
|
||||
cereal::LongitudinalPlanSP::SpeedLimit::Source speedLimitSource;
|
||||
int speedLimitSource; // 0=NONE, 1=MAP
|
||||
bool speedLimitAheadValid;
|
||||
float speedLimitAhead;
|
||||
float speedLimitAheadDistance;
|
||||
@@ -94,7 +94,7 @@ private:
|
||||
SpeedLimitMode speedLimitMode = SpeedLimitMode::OFF;
|
||||
bool roadName;
|
||||
QString roadNameStr;
|
||||
cereal::LongitudinalPlanSP::SpeedLimit::AssistState speedLimitAssistState;
|
||||
int speedLimitAssistState; // 0=NONE, 1=PRE_ACTIVE, etc.
|
||||
bool speedLimitAssistActive;
|
||||
int speedLimitAssistFrame;
|
||||
QPixmap plus_arrow_up_img;
|
||||
@@ -131,4 +131,15 @@ private:
|
||||
QString navigationNextModifier;
|
||||
QString navigationNextManeuverType;
|
||||
bool navigationHasNext;
|
||||
|
||||
QString wayName;
|
||||
QString wayRef;
|
||||
float mapdSpeedLimit;
|
||||
float advisorySpeed;
|
||||
float nextAdvisorySpeed;
|
||||
float nextAdvisorySpeedDistance;
|
||||
float suggestedSpeed;
|
||||
float visionCurveSpeed;
|
||||
float curveSpeed;
|
||||
bool tileLoaded;
|
||||
};
|
||||
|
||||
@@ -48,7 +48,7 @@ void ModelRendererSP::draw(QPainter &painter, const QRect &surface_rect) {
|
||||
if (s->scene.rainbow_mode) {
|
||||
drawRainbowPath(painter, surface_rect);
|
||||
} else {
|
||||
ModelRenderer::drawPath(painter, model, surface_rect.height());
|
||||
ModelRenderer::drawPath(painter, model, surface_rect.height(), surface_rect.width());
|
||||
}
|
||||
|
||||
if (longitudinal_control && sm.alive("radarState")) {
|
||||
|
||||
@@ -29,7 +29,8 @@ UIStateSP::UIStateSP(QObject *parent) : UIState(parent) {
|
||||
"wideRoadCameraState", "managerState", "selfdriveState", "longitudinalPlan",
|
||||
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
|
||||
"carControl", "gpsLocationExternal", "gpsLocation", "liveTorqueParameters",
|
||||
"carStateSP", "liveParameters", "liveMapDataSP", "carParamsSP", "navigationd"
|
||||
"carStateSP", "liveParameters", "liveMapDataSP", "carParamsSP", "navigationd",
|
||||
"mapdOut"
|
||||
});
|
||||
|
||||
// update timer
|
||||
|
||||
@@ -60,6 +60,7 @@ typedef struct UIScene {
|
||||
cereal::PandaState::PandaType pandaType;
|
||||
|
||||
cereal::LongitudinalPersonality personality;
|
||||
cereal::LongitudinalPlanSP::AccelerationPersonality accel_personality;
|
||||
|
||||
float light_sensor = -1;
|
||||
bool started, ignition, is_metric, recording_audio;
|
||||
|
||||
@@ -28,6 +28,7 @@ from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan
|
||||
from openpilot.sunnypilot.models.helpers import get_active_bundle, get_model_path, load_metadata, prepare_inputs, load_meta_constants
|
||||
from openpilot.sunnypilot.modeld.models.commonmodel_pyx import ModelFrame, CLContext
|
||||
from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
|
||||
|
||||
|
||||
PROCESS_NAME = "selfdrive.modeld.modeld_snpe"
|
||||
@@ -209,6 +210,7 @@ def main(demo=False):
|
||||
prev_action = log.ModelDataV2.Action()
|
||||
|
||||
DH = DesireHelper()
|
||||
RELC = RoadEdgeLaneChangeController(params.get_bool("RoadEdgeLaneChangeEnabled"))
|
||||
|
||||
while True:
|
||||
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
|
||||
@@ -314,7 +316,10 @@ def main(demo=False):
|
||||
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
|
||||
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
|
||||
lane_change_prob = l_lane_change_prob + r_lane_change_prob
|
||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
|
||||
RELC.update(modelv2_send.modelV2.roadEdgeStds, modelv2_send.modelV2.laneLineProbs)
|
||||
mdv2sp_send.modelDataV2SP.leftLaneChangeEdgeBlock = RELC.left_edge_detected
|
||||
mdv2sp_send.modelDataV2SP.rightLaneChangeEdgeBlock = RELC.right_edge_detected
|
||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, RELC.left_edge_detected, RELC.right_edge_detected)
|
||||
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
|
||||
|
||||
@@ -26,6 +26,7 @@ from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||
from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase
|
||||
from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||
from openpilot.sunnypilot.models.runners.helpers import get_model_runner
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
|
||||
|
||||
PROCESS_NAME = "selfdrive.modeld.modeld_tinygrad"
|
||||
|
||||
@@ -239,6 +240,9 @@ def main(demo=False):
|
||||
prev_action = log.ModelDataV2.Action()
|
||||
|
||||
DH = DesireHelper()
|
||||
RELC = RoadEdgeLaneChangeController(params.get_bool("RoadEdgeLaneChangeEnabled"))
|
||||
|
||||
|
||||
|
||||
while True:
|
||||
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
|
||||
@@ -340,7 +344,10 @@ def main(demo=False):
|
||||
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
|
||||
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
|
||||
lane_change_prob = l_lane_change_prob + r_lane_change_prob
|
||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
|
||||
RELC.update(modelv2_send.modelV2.roadEdgeStds, modelv2_send.modelV2.laneLineProbs)
|
||||
mdv2sp_send.modelDataV2SP.leftLaneChangeEdgeBlock = RELC.left_edge_detected
|
||||
mdv2sp_send.modelDataV2SP.rightLaneChangeEdgeBlock = RELC.right_edge_detected
|
||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, RELC.left_edge_detected, RELC.right_edge_detected)
|
||||
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
|
||||
|
||||
@@ -116,7 +116,7 @@ class ModelCache:
|
||||
|
||||
class ModelFetcher:
|
||||
"""Handles fetching and caching of model data from remote source"""
|
||||
MODEL_URL = "https://docs.sunnypilot.ai/driving_models_v8.json"
|
||||
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-docs/refs/heads/gh-pages/docs/driving_models_v9.json"
|
||||
|
||||
def __init__(self, params: Params):
|
||||
self.params = params
|
||||
|
||||
@@ -19,8 +19,8 @@ from openpilot.system.hardware.hw import Paths
|
||||
from pathlib import Path
|
||||
|
||||
# see the README.md for more details on the model selector versioning
|
||||
CURRENT_SELECTOR_VERSION = 10
|
||||
REQUIRED_MIN_SELECTOR_VERSION = 9
|
||||
CURRENT_SELECTOR_VERSION = 11
|
||||
REQUIRED_MIN_SELECTOR_VERSION = 11
|
||||
|
||||
USE_ONNX = os.getenv('USE_ONNX', PC)
|
||||
|
||||
|
||||
@@ -14,13 +14,10 @@ CUSTOM_MODEL_PATH = Paths.model_root()
|
||||
# Set QCOM environment variable for TICI devices, potentially enabling hardware acceleration
|
||||
USBGPU = "USBGPU" in os.environ
|
||||
if USBGPU:
|
||||
os.environ['AMD'] = '1'
|
||||
os.environ['DEV'] = 'AMD'
|
||||
os.environ['AMD_IFACE'] = 'USB'
|
||||
elif TICI:
|
||||
os.environ['QCOM'] = '1'
|
||||
else:
|
||||
os.environ['LLVM'] = '1'
|
||||
os.environ['JIT'] = '2' # TODO: This may cause issues
|
||||
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
|
||||
|
||||
|
||||
class ModelData:
|
||||
|
||||
@@ -31,14 +31,14 @@ class NavigationDesires:
|
||||
self.desire = log.Desire.none
|
||||
if self.nav_allowed and nav_msg.valid and lateral_active:
|
||||
upcoming = nav_msg.upcomingTurn
|
||||
# if upcoming == 'slightLeft' and (not CS.leftBlindspot or CS.vEgo < self._turn_speed_limit):
|
||||
# self.desire = log.Desire.keepLeft
|
||||
# elif upcoming == 'slightRight' and (not CS.rightBlindspot or CS.vEgo < self._turn_speed_limit):
|
||||
# self.desire = log.Desire.keepRight
|
||||
if (upcoming == 'left' and not CS.rightBlinker
|
||||
if upcoming == 'slightLeft' and (not CS.leftBlindspot or CS.vEgo < self._turn_speed_limit):
|
||||
self.desire = log.Desire.keepLeft
|
||||
elif upcoming == 'slightRight' and (not CS.rightBlindspot or CS.vEgo < self._turn_speed_limit):
|
||||
self.desire = log.Desire.keepRight
|
||||
elif (upcoming == 'left' and CS.steeringPressed and CS.steeringTorque > 0 and not CS.rightBlinker
|
||||
and not CS.leftBlindspot and CS.vEgo < self._turn_speed_limit):
|
||||
self.desire = log.Desire.turnLeft
|
||||
elif (upcoming == 'right' and not CS.leftBlinker
|
||||
elif (upcoming == 'right' and CS.steeringPressed and CS.steeringTorque < 0 and not CS.leftBlinker
|
||||
and not CS.rightBlindspot and CS.vEgo < self._turn_speed_limit):
|
||||
self.desire = log.Desire.turnRight
|
||||
return self.desire
|
||||
|
||||
@@ -22,20 +22,20 @@ def make_car(vEgo=0, leftBlinker=False, rightBlinker=False, leftBlindspot=False,
|
||||
)
|
||||
|
||||
NAVIGATION_PARAMS: list[tuple] = [
|
||||
('slightLeft', make_car(steeringPressed=True, steeringTorque=1), log.Desire.keepLeft),
|
||||
('slightRight', make_car(steeringPressed=True, steeringTorque=-1), log.Desire.keepRight),
|
||||
('slightLeft', make_car(), log.Desire.keepLeft),
|
||||
('slightRight', make_car(), log.Desire.keepRight),
|
||||
('slightLeft', make_car(vEgo=9, leftBlindspot=True), log.Desire.none),
|
||||
('slightRight', make_car(vEgo=9, rightBlindspot=True), log.Desire.none),
|
||||
('left', make_car(vEgo=5, leftBlinker=True, rightBlinker=False, leftBlindspot=False), log.Desire.turnLeft),
|
||||
('left', make_car(vEgo=5, leftBlinker=True, rightBlinker=False, leftBlindspot=False, steeringPressed=True, steeringTorque=1), log.Desire.turnLeft),
|
||||
('left', make_car(vEgo=5, leftBlinker=False, rightBlinker=True), log.Desire.none),
|
||||
('right', make_car(vEgo=6, rightBlinker=True, leftBlindspot=False), log.Desire.turnRight),
|
||||
('right', make_car(vEgo=6, rightBlinker=True, leftBlindspot=False, steeringPressed=True, steeringTorque=-1), log.Desire.turnRight),
|
||||
('right', make_car(vEgo=6, rightBlinker=True, rightBlindspot=True), log.Desire.none),
|
||||
('left', make_car(vEgo=9, leftBlinker=True), log.Desire.none),
|
||||
]
|
||||
|
||||
INTEGRATION_PARAMS: list[tuple] = [(carstate, upcoming, log.Desire.none, expected) for upcoming, carstate, expected in NAVIGATION_PARAMS] + [
|
||||
(make_car(vEgo=6, leftBlinker=True, steeringPressed=True, steeringTorque=1), 'slightLeft', log.Desire.turnLeft, log.Desire.keepLeft),
|
||||
(make_car(vEgo=5, rightBlinker=True, steeringPressed=True, steeringTorque=-1), 'slightRight', log.Desire.turnRight, log.Desire.keepRight),
|
||||
(make_car(vEgo=9, leftBlinker=True, steeringPressed=True, steeringTorque=1), 'slightLeft', log.Desire.turnLeft, log.Desire.keepLeft),
|
||||
(make_car(vEgo=9, rightBlinker=True, steeringPressed=True, steeringTorque=-1), 'slightRight', log.Desire.turnRight, log.Desire.keepRight),
|
||||
(make_car(vEgo=9, leftBlinker=True), 'slightLeft', log.Desire.laneChangeLeft, log.Desire.laneChangeLeft),
|
||||
(make_car(vEgo=9, rightBlinker=True), 'slightRight', log.Desire.laneChangeRight, log.Desire.laneChangeRight),
|
||||
(make_car(vEgo=9), 'none', log.Desire.none, log.Desire.none),
|
||||
|
||||
@@ -4,8 +4,6 @@ Copyright (c) 2021-, James Vecellio, Haibin Wen, sunnypilot, and a number of oth
|
||||
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.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.params import Params
|
||||
|
||||
@@ -111,17 +109,12 @@ class NavigationInstructions:
|
||||
self._route_loaded = False
|
||||
self._no_route = False
|
||||
|
||||
def get_upcoming_turn_from_progress(self, progress, current_lat, current_lon, v_ego: float) -> str:
|
||||
def get_upcoming_turn_from_progress(self, progress, current_lat, current_lon) -> str:
|
||||
if progress and progress['next_turn']:
|
||||
speed_breakpoints: list = [0, 5, 10, 15, 20, 25, 30, 35, 40]
|
||||
distance_breakpoints: list = [20, 25, 30, 45, 60, 75, 90, 105, 120]
|
||||
distance_interp = np.interp(v_ego, speed_breakpoints, distance_breakpoints)
|
||||
|
||||
self.coord.latitude = current_lat
|
||||
self.coord.longitude = current_lon
|
||||
distance = self.coord.distance_to(progress['next_turn']['location'])
|
||||
|
||||
if distance <= distance_interp:
|
||||
if distance <= 30.0:
|
||||
modifier = progress['next_turn']['modifier']
|
||||
return str(modifier)
|
||||
return 'none'
|
||||
|
||||
@@ -54,17 +54,17 @@ class TestMapbox:
|
||||
assert 'modifier' in step
|
||||
|
||||
def test_upcoming_turn_detection(self):
|
||||
upcoming = self.nav.get_upcoming_turn_from_progress(self.progress, self.current_lat, self.current_lon, v_ego=40.0)
|
||||
upcoming = self.nav.get_upcoming_turn_from_progress(self.progress, self.current_lat, self.current_lon)
|
||||
assert isinstance(upcoming, str)
|
||||
assert upcoming == 'none'
|
||||
|
||||
if self.route['steps']:
|
||||
turn_lat = self.route['steps'][1]['location'].latitude
|
||||
turn_lon = self.route['steps'][1]['location'].longitude
|
||||
close_lat = turn_lat - 0.000175 # slightly before the turn
|
||||
close_lat = turn_lat - 0.00025 # slightly before the turn
|
||||
if self.progress and self.progress.get('next_turn'):
|
||||
expected_turn = self.progress['next_turn']['modifier']
|
||||
upcoming_close = self.nav.get_upcoming_turn_from_progress(self.progress, close_lat, turn_lon, v_ego=0.0)
|
||||
upcoming_close = self.nav.get_upcoming_turn_from_progress(self.progress, close_lat, turn_lon)
|
||||
if expected_turn:
|
||||
assert upcoming_close == expected_turn == 'right', "Should be a right turn upcoming"
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ 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.
|
||||
"""
|
||||
import math
|
||||
import numpy as np
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from cereal import custom
|
||||
@@ -25,7 +24,7 @@ class Navigationd:
|
||||
self.mapbox = MapboxIntegration()
|
||||
self.nav_instructions = NavigationInstructions()
|
||||
|
||||
self.sm = messaging.SubMaster(['carState', 'liveLocationKalman'])
|
||||
self.sm = messaging.SubMaster(['liveLocationKalman'])
|
||||
self.pm = messaging.PubMaster(['navigationd'])
|
||||
self.rk = Ratekeeper(3) # 3 Hz
|
||||
|
||||
@@ -77,13 +76,12 @@ class Navigationd:
|
||||
|
||||
self.valid = self.route is not None
|
||||
|
||||
def _update_navigation(self, v_ego) -> tuple[str, dict | None, dict]:
|
||||
def _update_navigation(self) -> tuple[str, dict | None, dict]:
|
||||
banner_instructions: str = ''
|
||||
nav_data: dict = {}
|
||||
if self.allow_navigation and self.last_position is not None:
|
||||
if progress := self.nav_instructions.get_route_progress(self.last_position.latitude, self.last_position.longitude):
|
||||
nav_data['upcoming_turn'] = self.nav_instructions.get_upcoming_turn_from_progress(progress, self.last_position.latitude,
|
||||
self.last_position.longitude, v_ego)
|
||||
nav_data['upcoming_turn'] = self.nav_instructions.get_upcoming_turn_from_progress(progress, self.last_position.latitude, self.last_position.longitude)
|
||||
nav_data['current_speed_limit'] = self.nav_instructions.get_current_speed_limit_from_progress(progress, self.is_metric)
|
||||
arrived = self.nav_instructions.arrived_at_destination(progress)
|
||||
|
||||
@@ -93,9 +91,7 @@ class Navigationd:
|
||||
banner_instructions = parsed['maneuverPrimaryText']
|
||||
|
||||
nav_data['distance_from_route'] = progress['distance_from_route']
|
||||
speed_breakpoints: list = [0.0, 5.0, 10.0, 20.0, 40.0]
|
||||
distance_list: list = [100.0, 125.0, 150.0, 200.0, 250.0]
|
||||
large_distance: bool = progress['distance_from_route'] > float(np.interp(v_ego, speed_breakpoints, distance_list))
|
||||
large_distance = progress['distance_from_route'] > 100
|
||||
|
||||
if large_distance:
|
||||
self.cancel_route_counter = self.cancel_route_counter + 1 if progress['distance_from_route'] > NAV_CV.QUARTER_MILE else 0
|
||||
@@ -110,7 +106,7 @@ class Navigationd:
|
||||
# Don't recompute in last segment to prevent reroute loops
|
||||
if self.route:
|
||||
if progress['current_step_idx'] == len(self.route['steps']) - 1:
|
||||
self.reroute_counter = 0
|
||||
self.allow_recompute = False
|
||||
else:
|
||||
banner_instructions = ''
|
||||
progress = None
|
||||
@@ -142,8 +138,7 @@ class Navigationd:
|
||||
cloudlog.warning('navigationd init')
|
||||
|
||||
while True:
|
||||
self.sm.update(0)
|
||||
v_ego = self.sm['carState'].vEgo
|
||||
self.sm.update()
|
||||
location = self.sm['liveLocationKalman']
|
||||
localizer_valid = location.positionGeodetic.valid if location else False
|
||||
|
||||
@@ -152,7 +147,7 @@ class Navigationd:
|
||||
self.last_position = Coordinate(location.positionGeodetic.value[0], location.positionGeodetic.value[1])
|
||||
|
||||
self._update_params()
|
||||
banner_instructions, progress, nav_data = self._update_navigation(v_ego)
|
||||
banner_instructions, progress, nav_data = self._update_navigation()
|
||||
|
||||
msg = self._build_navigation_message(banner_instructions, progress, nav_data, valid=localizer_valid)
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
Copyright (c) 2021-, rav4kumar, 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
|
||||
import numpy as np
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.common.params import Params
|
||||
|
||||
AccelPersonality = custom.LongitudinalPlanSP.AccelerationPersonality
|
||||
|
||||
# Acceleration Profiles
|
||||
MAX_ACCEL_PROFILES = {
|
||||
AccelPersonality.eco: [1.8, 1.80, 1.40, .700, .410, .30, .22, .009],
|
||||
AccelPersonality.normal: [1.9, 1.90, 1.50, .800, .530, .40, .26, .120],
|
||||
AccelPersonality.sport: [2.0, 2.00, 1.60, .900, .680, .50, .35, .200],
|
||||
}
|
||||
MAX_ACCEL_BREAKPOINTS = [0., 4., 6., 9., 16., 25., 30., 55.]
|
||||
|
||||
# Braking Profiles
|
||||
MIN_ACCEL_PROFILES = {
|
||||
AccelPersonality.eco: [-1.20, -1.20, -1.20],
|
||||
AccelPersonality.normal: [-1.30, -1.30, -1.30],
|
||||
AccelPersonality.sport: [-1.30, -1.40, -1.40],
|
||||
}
|
||||
MIN_ACCEL_BREAKPOINTS = [5., 10., 36.]
|
||||
|
||||
|
||||
DECEL_SMOOTH_ALPHA = 0.02 # Very aggressive smoothing for decel (lower = smoother)
|
||||
ACCEL_SMOOTH_ALPHA = 0.01 # Less aggressive for accel (higher = more responsive)
|
||||
|
||||
# Asymmetric rate limiting
|
||||
MAX_DECEL_INCREASE_RATE = 0.1 # When braking harder (m/s² per second)
|
||||
MAX_DECEL_DECREASE_RATE = 0.30 # When releasing brake (m/s² per second)
|
||||
|
||||
|
||||
|
||||
class AccelPersonalityController:
|
||||
|
||||
def __init__(self):
|
||||
self.params = Params()
|
||||
self.frame = 0
|
||||
self.accel_personality = AccelPersonality.normal
|
||||
self.last_max_accel = 2.0
|
||||
self.last_min_accel = -0.01
|
||||
self.first_run = True
|
||||
self.param_keys = {
|
||||
'personality': 'AccelPersonality',
|
||||
'enabled': 'AccelPersonalityEnabled'
|
||||
}
|
||||
self._load_personality_from_params()
|
||||
|
||||
def _load_personality_from_params(self):
|
||||
try:
|
||||
saved = self.params.get(self.param_keys['personality'])
|
||||
if saved is not None:
|
||||
personality_value = int(saved)
|
||||
if personality_value in [AccelPersonality.eco, AccelPersonality.normal, AccelPersonality.sport]:
|
||||
self.accel_personality = personality_value
|
||||
else:
|
||||
self.accel_personality = AccelPersonality.normal
|
||||
except (ValueError, TypeError):
|
||||
self.accel_personality = AccelPersonality.normal
|
||||
|
||||
def _update_from_params(self):
|
||||
if self.frame % int(1. / DT_MDL) != 0:
|
||||
return
|
||||
self._load_personality_from_params()
|
||||
|
||||
def get_accel_personality(self) -> int:
|
||||
self._update_from_params()
|
||||
return int(self.accel_personality)
|
||||
|
||||
def set_accel_personality(self, personality: int):
|
||||
if personality not in [AccelPersonality.eco, AccelPersonality.normal, AccelPersonality.sport]:
|
||||
return
|
||||
|
||||
self.accel_personality = personality
|
||||
self.params.put(self.param_keys['personality'], str(personality))
|
||||
|
||||
def cycle_accel_personality(self) -> int:
|
||||
personalities = [AccelPersonality.eco, AccelPersonality.normal, AccelPersonality.sport]
|
||||
current_idx = personalities.index(self.accel_personality)
|
||||
next_personality = personalities[(current_idx + 1) % len(personalities)]
|
||||
self.set_accel_personality(next_personality)
|
||||
return int(next_personality)
|
||||
|
||||
def get_accel_limits(self, v_ego: float) -> tuple[float, float]:
|
||||
v_ego = max(0.0, v_ego)
|
||||
target_max_accel = np.interp(v_ego, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[self.accel_personality])
|
||||
target_min_accel = np.interp(v_ego, MIN_ACCEL_BREAKPOINTS, MIN_ACCEL_PROFILES[self.accel_personality])
|
||||
|
||||
if self.first_run:
|
||||
self.last_max_accel = target_max_accel
|
||||
self.last_min_accel = target_min_accel
|
||||
self.first_run = False
|
||||
return float(target_min_accel), float(target_max_accel)
|
||||
|
||||
# exponential smoothing to max accel
|
||||
self.last_max_accel = (ACCEL_SMOOTH_ALPHA * target_max_accel + (1 - ACCEL_SMOOTH_ALPHA) * self.last_max_accel)
|
||||
|
||||
# VERY aggressive smoothing to min accel for ultra-smooth braking
|
||||
smoothed_decel = (DECEL_SMOOTH_ALPHA * target_min_accel + (1 - DECEL_SMOOTH_ALPHA) * self.last_min_accel)
|
||||
|
||||
# asymmetric rate limiting
|
||||
decel_change = smoothed_decel - self.last_min_accel
|
||||
if decel_change < 0:
|
||||
max_change_per_step = MAX_DECEL_INCREASE_RATE * DT_MDL
|
||||
else:
|
||||
max_change_per_step = MAX_DECEL_DECREASE_RATE * DT_MDL
|
||||
|
||||
decel_change = np.clip(decel_change, -max_change_per_step, max_change_per_step)
|
||||
self.last_min_accel = self.last_min_accel + decel_change
|
||||
|
||||
if self.last_min_accel > self.last_max_accel:
|
||||
self.last_min_accel = self.last_max_accel - 0.1
|
||||
|
||||
return float(self.last_min_accel), float(self.last_max_accel)
|
||||
|
||||
def get_min_accel(self, v_ego: float) -> float:
|
||||
return self.get_accel_limits(v_ego)[0]
|
||||
|
||||
def get_max_accel(self, v_ego: float) -> float:
|
||||
return self.get_accel_limits(v_ego)[1]
|
||||
|
||||
def is_enabled(self) -> bool:
|
||||
return self.params.get_bool(self.param_keys['enabled'])
|
||||
|
||||
def set_enabled(self, enabled: bool):
|
||||
self.params.put_bool(self.param_keys['enabled'], enabled)
|
||||
|
||||
def toggle_enabled(self) -> bool:
|
||||
current = self.is_enabled()
|
||||
self.set_enabled(not current)
|
||||
return not current
|
||||
|
||||
def reset(self):
|
||||
self.accel_personality = AccelPersonality.normal
|
||||
self.frame = 0
|
||||
self.last_max_accel = 2.0
|
||||
self.last_min_accel = -0.01
|
||||
self.first_run = True
|
||||
|
||||
def update(self):
|
||||
self.frame += 1
|
||||
self._update_from_params()
|
||||
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
Copyright (c) 2021-, rav4kumar, 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 log
|
||||
import numpy as np
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.common.params import Params
|
||||
|
||||
LongPersonality = log.LongitudinalPersonality
|
||||
|
||||
# Follow distance profiles mapped to LongPersonality
|
||||
FOLLOW_PROFILES = {
|
||||
LongPersonality.relaxed: [1.50, 1.50, 1.66, 1.66, 1.64, 1.89],
|
||||
LongPersonality.standard: [1.30, 1.30, 1.45, 1.45, 1.44, 1.50],
|
||||
LongPersonality.aggressive: [0.97, 0.97, 1.25, 1.25, 1.24, 1.28],
|
||||
}
|
||||
|
||||
FOLLOW_BREAKPOINTS = [0., 3., 4, 11, 25., 36]
|
||||
|
||||
SMOOTHING_BASE = 0.99 # Base smoothing factor (higher = smoother)
|
||||
SMOOTHING_RANGE = 0.50 # Additional smoothing at high speeds
|
||||
SMOOTHING_SPEED_THRESHOLD = 36.0 # m/s (~80 mph) for max smoothing
|
||||
PERSONALITY_CHANGE_COOLDOWN_S = 0.1
|
||||
|
||||
|
||||
class FollowDistanceController:
|
||||
def __init__(self):
|
||||
self.params = Params()
|
||||
self.frame = 0
|
||||
self.personality = LongPersonality.standard
|
||||
self.current_multiplier = None
|
||||
self.first_run = True
|
||||
self.personality_change_cooldown = 0
|
||||
self.personality_cooldown_frames = int(PERSONALITY_CHANGE_COOLDOWN_S / DT_MDL)
|
||||
self._load_personality()
|
||||
|
||||
def _load_personality(self):
|
||||
try:
|
||||
saved = self.params.get('LongitudinalPersonality')
|
||||
if saved is not None:
|
||||
val = int(saved)
|
||||
if val in [LongPersonality.relaxed, LongPersonality.standard, LongPersonality.aggressive]:
|
||||
self.personality = val
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
def _update_from_params(self):
|
||||
if self.frame % int(1. / DT_MDL) != 0:
|
||||
return
|
||||
|
||||
if self.personality_change_cooldown > 0:
|
||||
self.personality_change_cooldown -= 1
|
||||
return
|
||||
|
||||
try:
|
||||
param = self.params.get('LongitudinalPersonality')
|
||||
if param is not None:
|
||||
val = int(param)
|
||||
if val in [LongPersonality.relaxed, LongPersonality.standard, LongPersonality.aggressive]:
|
||||
if val != self.personality:
|
||||
self.personality = val
|
||||
self.personality_change_cooldown = self.personality_cooldown_frames
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
def _get_smoothing_factor(self, v_ego: float) -> float:
|
||||
speed_factor = np.clip(v_ego / SMOOTHING_SPEED_THRESHOLD, 0.3, 1.0)
|
||||
return SMOOTHING_BASE + (SMOOTHING_RANGE * speed_factor)
|
||||
|
||||
def is_enabled(self) -> bool:
|
||||
return self.params.get_bool('DynamicFollow')
|
||||
|
||||
def set_enabled(self, enabled: bool):
|
||||
self.params.put_bool('DynamicFollow', enabled)
|
||||
|
||||
def toggle(self) -> bool:
|
||||
enabled = self.is_enabled()
|
||||
self.set_enabled(not enabled)
|
||||
return not enabled
|
||||
|
||||
def get_personality(self) -> int:
|
||||
self._update_from_params()
|
||||
return int(self.personality)
|
||||
|
||||
def set_personality(self, personality: int):
|
||||
if personality not in [LongPersonality.relaxed, LongPersonality.standard, LongPersonality.aggressive]:
|
||||
return
|
||||
|
||||
self.personality = personality
|
||||
self.params.put('LongitudinalPersonality', str(personality))
|
||||
self.personality_change_cooldown = self.personality_cooldown_frames
|
||||
|
||||
def cycle_personality(self) -> int:
|
||||
personalities = [LongPersonality.relaxed, LongPersonality.standard, LongPersonality.aggressive]
|
||||
current_idx = personalities.index(self.personality)
|
||||
next_personality = personalities[(current_idx + 1) % len(personalities)]
|
||||
self.set_personality(next_personality)
|
||||
return int(next_personality)
|
||||
|
||||
def get_follow_distance_multiplier(self, v_ego: float) -> float:
|
||||
self._update_from_params()
|
||||
v_ego = max(0.0, v_ego)
|
||||
target = float(np.interp(v_ego, FOLLOW_BREAKPOINTS, FOLLOW_PROFILES[self.personality]))
|
||||
|
||||
if self.first_run:
|
||||
self.current_multiplier = target
|
||||
self.first_run = False
|
||||
return self.current_multiplier
|
||||
|
||||
#exponential smoothing with speedadaptive factor
|
||||
alpha = self._get_smoothing_factor(v_ego)
|
||||
self.current_multiplier = alpha * self.current_multiplier + (1.0 - alpha) * target
|
||||
return self.current_multiplier
|
||||
|
||||
def reset(self):
|
||||
self.personality = LongPersonality.standard
|
||||
self.frame = 0
|
||||
self.current_multiplier = None
|
||||
self.first_run = True
|
||||
self.personality_change_cooldown = 0
|
||||
|
||||
def update(self):
|
||||
self.frame += 1
|
||||
self._update_from_params()
|
||||
@@ -17,6 +17,7 @@ from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_resolve
|
||||
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
||||
from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller import AccelPersonalityController
|
||||
DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState
|
||||
LongitudinalPlanSource = custom.LongitudinalPlanSP.LongitudinalPlanSource
|
||||
|
||||
@@ -24,27 +25,27 @@ LongitudinalPlanSource = custom.LongitudinalPlanSP.LongitudinalPlanSource
|
||||
class LongitudinalPlannerSP:
|
||||
def __init__(self, CP: structs.CarParams, CP_SP: structs.CarParamsSP, mpc):
|
||||
self.events_sp = EventsSP()
|
||||
self.resolver = SpeedLimitResolver()
|
||||
self.dec = DynamicExperimentalController(CP, mpc)
|
||||
self.scc = SmartCruiseControl()
|
||||
self.resolver = SpeedLimitResolver()
|
||||
self.sla = SpeedLimitAssist(CP, CP_SP)
|
||||
self.accel_controller = AccelPersonalityController()
|
||||
self.generation = int(model_bundle.generation) if (model_bundle := get_active_bundle()) else None
|
||||
self.source = LongitudinalPlanSource.cruise
|
||||
self.e2e_alerts_helper = E2EAlertsHelper()
|
||||
|
||||
# Disabled controllers
|
||||
self.scc = None
|
||||
self.sla = None
|
||||
self.resolver = None
|
||||
|
||||
self.output_v_target = 0.
|
||||
self.output_a_target = 0.
|
||||
|
||||
@property
|
||||
def mlsim(self) -> bool:
|
||||
# If we don't have a generation set, we assume it's default model. Which as of today are mlsim.
|
||||
return bool(self.generation is None or self.generation >= 11)
|
||||
|
||||
def get_mpc_mode(self) -> str | None:
|
||||
if not self.dec.active():
|
||||
return None
|
||||
|
||||
return self.dec.mode()
|
||||
|
||||
def update_targets(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> tuple[float, float]:
|
||||
@@ -52,39 +53,20 @@ class LongitudinalPlannerSP:
|
||||
v_cruise_cluster_kph = min(CS.vCruiseCluster, V_CRUISE_MAX)
|
||||
v_cruise_cluster = v_cruise_cluster_kph * CV.KPH_TO_MS
|
||||
|
||||
long_enabled = sm['carControl'].enabled
|
||||
long_override = sm['carControl'].cruiseControl.override
|
||||
|
||||
# Smart Cruise Control
|
||||
self.scc.update(sm, long_enabled, long_override, v_ego, a_ego, v_cruise)
|
||||
|
||||
# Speed Limit Resolver
|
||||
self.resolver.update(v_ego, sm)
|
||||
|
||||
# Speed Limit Assist
|
||||
has_speed_limit = self.resolver.speed_limit_valid or self.resolver.speed_limit_last_valid
|
||||
self.sla.update(long_enabled, long_override, v_ego, a_ego, v_cruise_cluster, self.resolver.speed_limit,
|
||||
self.resolver.speed_limit_final_last, has_speed_limit, self.resolver.distance, self.events_sp)
|
||||
|
||||
targets = {
|
||||
LongitudinalPlanSource.cruise: (v_cruise, a_ego),
|
||||
LongitudinalPlanSource.sccVision: (self.scc.vision.output_v_target, self.scc.vision.output_a_target),
|
||||
LongitudinalPlanSource.sccMap: (self.scc.map.output_v_target, self.scc.map.output_a_target),
|
||||
LongitudinalPlanSource.speedLimitAssist: (self.sla.output_v_target, self.sla.output_a_target),
|
||||
}
|
||||
|
||||
self.source = min(targets, key=lambda k: targets[k][0])
|
||||
self.output_v_target, self.output_a_target = targets[self.source]
|
||||
# Skip SCC and Speed Limit logic
|
||||
self.source = LongitudinalPlanSource.cruise
|
||||
self.output_v_target = v_cruise_cluster
|
||||
self.output_a_target = a_ego
|
||||
return self.output_v_target, self.output_a_target
|
||||
|
||||
def update(self, sm: messaging.SubMaster) -> None:
|
||||
self.events_sp.clear()
|
||||
self.dec.update(sm)
|
||||
self.e2e_alerts_helper.update(sm, self.events_sp)
|
||||
self.accel_controller.update()
|
||||
|
||||
def publish_longitudinal_plan_sp(self, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None:
|
||||
plan_sp_send = messaging.new_message('longitudinalPlanSP')
|
||||
|
||||
plan_sp_send.valid = sm.all_checks(service_list=['carState', 'controlsState'])
|
||||
|
||||
longitudinalPlanSP = plan_sp_send.longitudinalPlanSP
|
||||
@@ -99,43 +81,10 @@ class LongitudinalPlannerSP:
|
||||
dec.enabled = self.dec.enabled()
|
||||
dec.active = self.dec.active()
|
||||
|
||||
# Smart Cruise Control
|
||||
smartCruiseControl = longitudinalPlanSP.smartCruiseControl
|
||||
# Vision Control
|
||||
sccVision = smartCruiseControl.vision
|
||||
sccVision.state = self.scc.vision.state
|
||||
sccVision.vTarget = float(self.scc.vision.output_v_target)
|
||||
sccVision.aTarget = float(self.scc.vision.output_a_target)
|
||||
sccVision.currentLateralAccel = float(self.scc.vision.current_lat_acc)
|
||||
sccVision.maxPredictedLateralAccel = float(self.scc.vision.max_pred_lat_acc)
|
||||
sccVision.enabled = self.scc.vision.is_enabled
|
||||
sccVision.active = self.scc.vision.is_active
|
||||
# Map Control
|
||||
sccMap = smartCruiseControl.map
|
||||
sccMap.state = self.scc.map.state
|
||||
sccMap.vTarget = float(self.scc.map.output_v_target)
|
||||
sccMap.aTarget = float(self.scc.map.output_a_target)
|
||||
sccMap.enabled = self.scc.map.is_enabled
|
||||
sccMap.active = self.scc.map.is_active
|
||||
|
||||
# Speed Limit
|
||||
speedLimit = longitudinalPlanSP.speedLimit
|
||||
resolver = speedLimit.resolver
|
||||
resolver.speedLimit = float(self.resolver.speed_limit)
|
||||
resolver.speedLimitLast = float(self.resolver.speed_limit_last)
|
||||
resolver.speedLimitFinal = float(self.resolver.speed_limit_final)
|
||||
resolver.speedLimitFinalLast = float(self.resolver.speed_limit_final_last)
|
||||
resolver.speedLimitValid = self.resolver.speed_limit_valid
|
||||
resolver.speedLimitLastValid = self.resolver.speed_limit_last_valid
|
||||
resolver.speedLimitOffset = float(self.resolver.speed_limit_offset)
|
||||
resolver.distToSpeedLimit = float(self.resolver.distance)
|
||||
resolver.source = self.resolver.source
|
||||
assist = speedLimit.assist
|
||||
assist.state = self.sla.state
|
||||
assist.enabled = self.sla.is_enabled
|
||||
assist.active = self.sla.is_active
|
||||
assist.vTarget = float(self.sla.output_v_target)
|
||||
assist.aTarget = float(self.sla.output_a_target)
|
||||
# Skip SCC and Speed Limit fields (leave zeroed)
|
||||
longitudinalPlanSP.smartCruiseControl.vision.enabled = False
|
||||
longitudinalPlanSP.smartCruiseControl.map.enabled = False
|
||||
longitudinalPlanSP.speedLimit.assist.enabled = False
|
||||
|
||||
# E2E Alerts
|
||||
e2eAlerts = longitudinalPlanSP.e2eAlerts
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
Copyright (c) 2021-, rav4kumar, 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.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from cereal import log
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.common.params import Params
|
||||
|
||||
NEARSIDE_PROB = 0.2
|
||||
EDGE_PROB = 0.35
|
||||
EDGE_REACTION_TIME = 1.0
|
||||
|
||||
class RoadEdgeLaneChangeController:
|
||||
def __init__(self, desire_helper):
|
||||
self.desire_helper = desire_helper
|
||||
self.params = Params()
|
||||
self.enabled = self.params.get_bool("RoadEdgeLaneChangeEnabled")
|
||||
self.left_edge_detected = False
|
||||
self.right_edge_detected = False
|
||||
self.left_edge_timer = 0.0
|
||||
self.right_edge_timer = 0.0
|
||||
self._frame = 0
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
self.enabled = enabled
|
||||
if not enabled:
|
||||
self._reset_state()
|
||||
|
||||
def _read_params(self) -> None:
|
||||
if self._frame % int(1. / DT_MDL) == 0:
|
||||
self.enabled = self.params.get_bool("RoadEdgeLaneChangeEnabled")
|
||||
|
||||
def _reset_state(self):
|
||||
self.left_edge_detected = False
|
||||
self.right_edge_detected = False
|
||||
self.left_edge_timer = 0.0
|
||||
self.right_edge_timer = 0.0
|
||||
|
||||
def _update_edge_detection(self, road_edge_stds, lane_line_probs):
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
left_road_edge_prob = np.clip(1.0 - road_edge_stds[0], 0.0, 1.0)
|
||||
right_road_edge_prob = np.clip(1.0 - road_edge_stds[1], 0.0, 1.0)
|
||||
|
||||
# Lane line probabilities: [left_outer, left_inner, right_inner, right_outer]
|
||||
left_lane_nearside_prob = lane_line_probs[0] if len(lane_line_probs) > 0 else 0.0
|
||||
right_lane_nearside_prob = lane_line_probs[3] if len(lane_line_probs) > 3 else 0.0
|
||||
|
||||
left_edge_conditions = (
|
||||
left_road_edge_prob > EDGE_PROB and
|
||||
left_lane_nearside_prob < NEARSIDE_PROB and
|
||||
(len(lane_line_probs) <= 3 or right_lane_nearside_prob >= left_lane_nearside_prob)
|
||||
)
|
||||
right_edge_conditions = (
|
||||
right_road_edge_prob > EDGE_PROB and
|
||||
right_lane_nearside_prob < NEARSIDE_PROB and
|
||||
(len(lane_line_probs) <= 0 or left_lane_nearside_prob >= right_lane_nearside_prob)
|
||||
)
|
||||
|
||||
if left_edge_conditions:
|
||||
self.left_edge_timer += DT_MDL
|
||||
self.left_edge_detected = self.left_edge_timer > EDGE_REACTION_TIME
|
||||
else:
|
||||
self.left_edge_timer = 0.0
|
||||
self.left_edge_detected = False
|
||||
|
||||
if right_edge_conditions:
|
||||
self.right_edge_timer += DT_MDL
|
||||
self.right_edge_detected = self.right_edge_timer > EDGE_REACTION_TIME
|
||||
else:
|
||||
self.right_edge_timer = 0.0
|
||||
self.right_edge_detected = False
|
||||
|
||||
def update(self, road_edge_stds, lane_line_probs):
|
||||
self._read_params()
|
||||
|
||||
if not self.enabled:
|
||||
self._frame += 1
|
||||
return
|
||||
|
||||
self._update_edge_detection(road_edge_stds, lane_line_probs)
|
||||
self._frame += 1
|
||||
|
||||
def should_trigger_lane_change(self, carstate, lateral_active):
|
||||
if not self.enabled:
|
||||
return False, log.LaneChangeDirection.none
|
||||
return False, log.LaneChangeDirection.none
|
||||
|
||||
def is_lane_change_blocked(self, direction):
|
||||
if not self.enabled:
|
||||
return False
|
||||
|
||||
if direction == log.LaneChangeDirection.left:
|
||||
return self.left_edge_detected
|
||||
elif direction == log.LaneChangeDirection.right:
|
||||
return self.right_edge_detected
|
||||
|
||||
return False
|
||||
|
||||
def can_change_lane_left(self):
|
||||
return not self.left_edge_detected if self.enabled else True
|
||||
|
||||
def can_change_lane_right(self):
|
||||
return not self.right_edge_detected if self.enabled else True
|
||||
|
||||
@property
|
||||
def edge_detected(self):
|
||||
return self.left_edge_detected or self.right_edge_detected
|
||||
@@ -6,6 +6,9 @@ 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.auto_lane_change import AutoLaneChangeMode
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
|
||||
|
||||
|
||||
TurnDirection = custom.ModelDataV2SP.TurnDirection
|
||||
|
||||
@@ -121,7 +124,10 @@ def set_lane_turn_params():
|
||||
(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):
|
||||
dh = DesireHelper()
|
||||
for _ in range(10):
|
||||
dh.update(carstate, lateral_active, lane_change_prob)
|
||||
assert dh.desire == expected_desire
|
||||
dh = DesireHelper()
|
||||
relc = RoadEdgeLaneChangeController(dh)
|
||||
relc.set_enabled(True)
|
||||
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
|
||||
for _ in range(10):
|
||||
dh.update(carstate, lateral_active, lane_change_prob, left_edge_detected=relc.left_edge_detected, right_edge_detected=relc.right_edge_detected)
|
||||
assert dh.desire == expected_desire # The first four tests were unit tests to test the controller, where this tests the integration in desire helpers
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
"""
|
||||
Copyright (c) 2021-, rav4kumar, 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.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from cereal import log
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController, EDGE_REACTION_TIME
|
||||
|
||||
@pytest.fixture
|
||||
def relc_controller(mocker):
|
||||
mock_params = mocker.patch("openpilot.sunnypilot.selfdrive.controls.lib.relc.Params")
|
||||
mock_params.return_value.get_bool.return_value = True
|
||||
|
||||
DH = DesireHelper()
|
||||
relc = RoadEdgeLaneChangeController(DH)
|
||||
relc.set_enabled(True)
|
||||
return relc
|
||||
|
||||
|
||||
def test_disable_resets_state(relc_controller):
|
||||
relc = relc_controller
|
||||
relc.left_edge_detected = True
|
||||
relc.right_edge_detected = True
|
||||
relc.left_edge_timer = 5.0
|
||||
relc.right_edge_timer = 5.0
|
||||
|
||||
relc.set_enabled(False)
|
||||
|
||||
assert not relc.left_edge_detected
|
||||
assert not relc.right_edge_detected
|
||||
assert relc.left_edge_timer == 0.0
|
||||
assert relc.right_edge_timer == 0.0
|
||||
|
||||
|
||||
def test_lane_change_blocked_left(relc_controller):
|
||||
relc = relc_controller
|
||||
relc.left_edge_detected = True
|
||||
assert relc.is_lane_change_blocked(log.LaneChangeDirection.left)
|
||||
|
||||
|
||||
def test_lane_change_blocked_right(relc_controller):
|
||||
relc = relc_controller
|
||||
relc.right_edge_detected = True
|
||||
assert relc.is_lane_change_blocked(log.LaneChangeDirection.right)
|
||||
|
||||
|
||||
def test_lane_change_not_blocked_opposite_side(relc_controller):
|
||||
relc = relc_controller
|
||||
relc.left_edge_detected = True
|
||||
assert not relc.is_lane_change_blocked(log.LaneChangeDirection.right)
|
||||
|
||||
relc.left_edge_detected = False
|
||||
relc.right_edge_detected = True
|
||||
assert not relc.is_lane_change_blocked(log.LaneChangeDirection.left)
|
||||
|
||||
|
||||
def test_lane_change_not_blocked_when_disabled(relc_controller):
|
||||
relc = relc_controller
|
||||
relc.set_enabled(False)
|
||||
relc.left_edge_detected = True
|
||||
relc.right_edge_detected = True
|
||||
|
||||
assert not relc.is_lane_change_blocked(log.LaneChangeDirection.left)
|
||||
assert not relc.is_lane_change_blocked(log.LaneChangeDirection.right)
|
||||
|
||||
|
||||
def test_can_change_lane_left(relc_controller):
|
||||
relc = relc_controller
|
||||
assert relc.can_change_lane_left()
|
||||
|
||||
relc.left_edge_detected = True
|
||||
assert not relc.can_change_lane_left()
|
||||
|
||||
|
||||
def test_can_change_lane_right(relc_controller):
|
||||
relc = relc_controller
|
||||
assert relc.can_change_lane_right()
|
||||
|
||||
relc.right_edge_detected = True
|
||||
assert not relc.can_change_lane_right()
|
||||
|
||||
|
||||
def test_can_change_lane_when_disabled(relc_controller):
|
||||
relc = relc_controller
|
||||
relc.set_enabled(False)
|
||||
relc.left_edge_detected = True
|
||||
relc.right_edge_detected = True
|
||||
|
||||
assert relc.can_change_lane_left()
|
||||
assert relc.can_change_lane_right()
|
||||
|
||||
|
||||
def test_edge_detected_property(relc_controller):
|
||||
relc = relc_controller
|
||||
assert not relc.edge_detected
|
||||
|
||||
relc.left_edge_detected = True
|
||||
assert relc.edge_detected
|
||||
|
||||
relc.left_edge_detected = False
|
||||
relc.right_edge_detected = True
|
||||
assert relc.edge_detected
|
||||
|
||||
relc.left_edge_detected = True
|
||||
assert relc.edge_detected
|
||||
|
||||
|
||||
def test_should_trigger_lane_change(relc_controller):
|
||||
relc = relc_controller
|
||||
should_trigger, direction = relc.should_trigger_lane_change(None, True)
|
||||
assert not should_trigger
|
||||
assert direction == log.LaneChangeDirection.none
|
||||
|
||||
|
||||
def test_update_increments_frame(relc_controller):
|
||||
relc = relc_controller
|
||||
initial = relc._frame
|
||||
relc.update([0.5, 0.5], [0.5, 0.5, 0.5, 0.5])
|
||||
assert relc._frame == initial + 1
|
||||
|
||||
|
||||
def test_left_edge_detection(relc_controller):
|
||||
relc = relc_controller
|
||||
road_edge_stds = [0.0, 0.9]
|
||||
lane_line_probs = [0.0, 0.8, 0.8, 0.8]
|
||||
|
||||
num_updates = int(EDGE_REACTION_TIME / DT_MDL) + 5
|
||||
for _ in range(num_updates):
|
||||
relc.update(road_edge_stds, lane_line_probs)
|
||||
|
||||
assert relc.left_edge_detected
|
||||
|
||||
|
||||
def test_right_edge_detection(relc_controller):
|
||||
relc = relc_controller
|
||||
road_edge_stds = [0.9, 0.0]
|
||||
lane_line_probs = [0.8, 0.8, 0.8, 0.0]
|
||||
|
||||
num_updates = int(EDGE_REACTION_TIME / DT_MDL) + 5
|
||||
for _ in range(num_updates):
|
||||
relc.update(road_edge_stds, lane_line_probs)
|
||||
|
||||
assert relc.right_edge_detected
|
||||
|
||||
|
||||
def test_edge_detection_requires_time(relc_controller):
|
||||
relc = relc_controller
|
||||
road_edge_stds = [0.0, 0.9]
|
||||
lane_line_probs = [0.0, 0.8, 0.8, 0.8]
|
||||
|
||||
num_updates = int(EDGE_REACTION_TIME / DT_MDL) - 1
|
||||
for _ in range(num_updates):
|
||||
relc.update(road_edge_stds, lane_line_probs)
|
||||
|
||||
assert not relc.left_edge_detected
|
||||
|
||||
|
||||
def test_edge_detection_clears(relc_controller):
|
||||
relc = relc_controller
|
||||
road_edge_stds = [0.0, 0.9]
|
||||
lane_line_probs = [0.0, 0.8, 0.8, 0.8]
|
||||
|
||||
num_updates = int(EDGE_REACTION_TIME / DT_MDL) + 5
|
||||
for _ in range(num_updates):
|
||||
relc.update(road_edge_stds, lane_line_probs)
|
||||
assert relc.left_edge_detected
|
||||
|
||||
road_edge_stds = [0.9, 0.9]
|
||||
relc.update(road_edge_stds, lane_line_probs)
|
||||
|
||||
assert not relc.left_edge_detected
|
||||
assert relc.left_edge_timer == 0.0
|
||||
|
||||
|
||||
def test_both_edges_detected(relc_controller):
|
||||
relc = relc_controller
|
||||
road_edge_stds = [0.0, 0.0]
|
||||
lane_line_probs = [0.0, 0.8, 0.8, 0.0]
|
||||
|
||||
num_updates = int(EDGE_REACTION_TIME / DT_MDL) + 5
|
||||
for _ in range(num_updates):
|
||||
relc.update(road_edge_stds, lane_line_probs)
|
||||
|
||||
assert relc.left_edge_detected
|
||||
assert relc.right_edge_detected
|
||||
@@ -0,0 +1,314 @@
|
||||
import pytest
|
||||
|
||||
# Import the actual modules
|
||||
from cereal import log, custom
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
|
||||
# Import the enums we need for testing
|
||||
LongPersonality = log.LongitudinalPersonality
|
||||
AccelPersonality = custom.LongitudinalPlanSP.AccelerationPersonality
|
||||
|
||||
|
||||
class MockParams:
|
||||
"""Simple mock for Params class"""
|
||||
def __init__(self):
|
||||
self.data = {}
|
||||
self.bool_data = {
|
||||
'VibePersonalityEnabled': True,
|
||||
'VibeAccelPersonalityEnabled': True,
|
||||
'VibeFollowPersonalityEnabled': True
|
||||
}
|
||||
|
||||
def get(self, key, encoding=None):
|
||||
return self.data.get(key)
|
||||
|
||||
def get_bool(self, key):
|
||||
return self.bool_data.get(key, True)
|
||||
|
||||
def put(self, key, value):
|
||||
self.data[key] = value
|
||||
|
||||
def put_bool(self, key, value):
|
||||
self.bool_data[key] = value
|
||||
|
||||
def reset_mock(self):
|
||||
self.call_count = 0
|
||||
|
||||
@property
|
||||
def call_count(self):
|
||||
return getattr(self, '_call_count', 0)
|
||||
|
||||
@call_count.setter
|
||||
def call_count(self, value):
|
||||
self._call_count = value
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_params():
|
||||
"""Create mock params instance"""
|
||||
return MockParams()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def controller(mock_params, monkeypatch):
|
||||
"""Create controller instance with mocked Params"""
|
||||
# Patch the Params import in the controller module
|
||||
monkeypatch.setattr('openpilot.sunnypilot.selfdrive.controls.lib.vibe_personality.vibe_personality.Params',
|
||||
lambda: mock_params)
|
||||
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.vibe_personality.vibe_personality import VibePersonalityController
|
||||
return VibePersonalityController()
|
||||
|
||||
|
||||
class TestVibePersonalityController:
|
||||
|
||||
def test_initialization(self, controller):
|
||||
"""Test controller initializes with correct defaults"""
|
||||
assert controller.frame == 0
|
||||
assert controller.accel_personality == AccelPersonality.normal
|
||||
assert controller.long_personality == LongPersonality.standard
|
||||
assert 'accel_personality' in controller.param_keys
|
||||
assert 'long_personality' in controller.param_keys
|
||||
|
||||
def test_frame_increment(self, controller):
|
||||
"""Test frame counter increments correctly"""
|
||||
initial_frame = controller.frame
|
||||
controller.update()
|
||||
assert controller.frame == initial_frame + 1
|
||||
|
||||
controller.update()
|
||||
assert controller.frame == initial_frame + 2
|
||||
|
||||
def test_parameter_reading_throttled(self, controller, mock_params):
|
||||
"""Test parameters are only read every DT_MDL frames"""
|
||||
# Track calls manually
|
||||
original_get = mock_params.get
|
||||
call_count = 0
|
||||
|
||||
def counting_get(*args, **kwargs):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
return original_get(*args, **kwargs)
|
||||
|
||||
mock_params.get = counting_get
|
||||
|
||||
# First call should read params (frame 0)
|
||||
controller._update_from_params()
|
||||
|
||||
# Reset counter
|
||||
call_count = 0
|
||||
|
||||
# Advance frame but not to threshold
|
||||
controller.frame = 5 # Less than int(1/DT_MDL)
|
||||
controller._update_from_params()
|
||||
assert call_count == 0 # Should not read params
|
||||
|
||||
# Advance to threshold
|
||||
controller.frame = int(1. / DT_MDL) # Equal to threshold
|
||||
controller._update_from_params()
|
||||
assert call_count >= 2 # Should read both personality params
|
||||
|
||||
def test_accel_personality_management(self, controller, mock_params):
|
||||
"""Test acceleration personality setting and cycling"""
|
||||
# Test setting valid personality
|
||||
assert controller.set_accel_personality(AccelPersonality.eco)
|
||||
assert controller.accel_personality == AccelPersonality.eco
|
||||
|
||||
assert controller.set_accel_personality(AccelPersonality.sport)
|
||||
assert controller.accel_personality == AccelPersonality.sport
|
||||
|
||||
# Test setting invalid personality
|
||||
assert not controller.set_accel_personality(999)
|
||||
assert controller.accel_personality == AccelPersonality.sport # Should remain unchanged
|
||||
|
||||
# Test cycling
|
||||
controller.accel_personality = AccelPersonality.eco
|
||||
next_personality = controller.cycle_accel_personality()
|
||||
assert next_personality == AccelPersonality.normal # should cycle to normal
|
||||
assert controller.accel_personality == AccelPersonality.normal
|
||||
|
||||
next_personality = controller.cycle_accel_personality()
|
||||
assert next_personality == AccelPersonality.sport # should cycle to sport
|
||||
|
||||
next_personality = controller.cycle_accel_personality()
|
||||
assert next_personality == AccelPersonality.eco # should cycle back to eco
|
||||
|
||||
def test_long_personality_management(self, controller, mock_params):
|
||||
"""Test longitudinal personality setting and cycling"""
|
||||
# Test setting valid personality
|
||||
assert controller.set_long_personality(LongPersonality.relaxed)
|
||||
assert controller.long_personality == LongPersonality.relaxed
|
||||
|
||||
assert controller.set_long_personality(LongPersonality.aggressive)
|
||||
assert controller.long_personality == LongPersonality.aggressive
|
||||
|
||||
# Test setting invalid personality
|
||||
assert not controller.set_long_personality(999)
|
||||
assert controller.long_personality == LongPersonality.aggressive # Should remain unchanged
|
||||
|
||||
# Test cycling
|
||||
controller.long_personality = LongPersonality.standard
|
||||
next_personality = controller.cycle_long_personality()
|
||||
assert next_personality == LongPersonality.aggressive # should cycle to aggressive
|
||||
assert controller.long_personality == LongPersonality.aggressive
|
||||
|
||||
next_personality = controller.cycle_long_personality()
|
||||
assert next_personality == LongPersonality.relaxed # should cycle to relaxed
|
||||
|
||||
next_personality = controller.cycle_long_personality()
|
||||
assert next_personality == LongPersonality.standard # should cycle back to standard
|
||||
|
||||
def test_toggle_functions(self, controller, mock_params):
|
||||
"""Test toggle functionality"""
|
||||
# Set initial state to False
|
||||
mock_params.bool_data['VibePersonalityEnabled'] = False
|
||||
|
||||
result = controller.toggle_personality()
|
||||
assert result # Should toggle to True
|
||||
assert mock_params.bool_data['VibePersonalityEnabled']
|
||||
|
||||
# Set initial state to True
|
||||
mock_params.bool_data['VibeAccelPersonalityEnabled'] = True
|
||||
|
||||
result = controller.toggle_accel_personality()
|
||||
assert not result # Should toggle to False
|
||||
assert not mock_params.bool_data['VibeAccelPersonalityEnabled']
|
||||
|
||||
def test_enable_checks(self, controller, mock_params):
|
||||
"""Test various enable state checks"""
|
||||
# All enabled
|
||||
mock_params.bool_data = {
|
||||
'VibePersonalityEnabled': True,
|
||||
'VibeAccelPersonalityEnabled': True,
|
||||
'VibeFollowPersonalityEnabled': True
|
||||
}
|
||||
|
||||
assert controller.is_enabled()
|
||||
assert controller.is_accel_enabled()
|
||||
assert controller.is_follow_enabled()
|
||||
|
||||
# Main toggle disabled
|
||||
mock_params.bool_data['VibePersonalityEnabled'] = False
|
||||
|
||||
assert not controller.is_enabled()
|
||||
assert not controller.is_accel_enabled()
|
||||
assert not controller.is_follow_enabled()
|
||||
|
||||
def test_accel_limits_calculation(self, controller, mock_params):
|
||||
"""Test acceleration limits calculation"""
|
||||
# Enable all features through mock_params bool_data
|
||||
mock_params.bool_data = {
|
||||
'VibePersonalityEnabled': True,
|
||||
'VibeAccelPersonalityEnabled': True,
|
||||
'VibeFollowPersonalityEnabled': True
|
||||
}
|
||||
|
||||
# Test with different speeds and personalities
|
||||
controller.accel_personality = 1 # normal
|
||||
controller.long_personality = 1 # standard
|
||||
|
||||
limits = controller.get_accel_limits(10.0) # 10 m/s
|
||||
assert limits is not None
|
||||
min_a, max_a = limits
|
||||
assert isinstance(min_a, float)
|
||||
assert isinstance(max_a, float)
|
||||
assert min_a < 0 # Should be negative (braking)
|
||||
assert max_a > 0 # Should be positive (acceleration)
|
||||
|
||||
# Test with disabled controller
|
||||
mock_params.bool_data['VibePersonalityEnabled'] = False
|
||||
limits = controller.get_accel_limits(10.0)
|
||||
assert limits is None
|
||||
|
||||
def test_follow_distance_multiplier(self, controller, mock_params):
|
||||
"""Test following distance multiplier calculation"""
|
||||
# Enable controller
|
||||
mock_params.bool_data['VibePersonalityEnabled'] = True
|
||||
mock_params.bool_data['VibeFollowPersonalityEnabled'] = True
|
||||
|
||||
# Test with different speeds and personalities
|
||||
controller.long_personality = LongPersonality.relaxed
|
||||
|
||||
multiplier = controller.get_follow_distance_multiplier(15.0) # 15 m/s
|
||||
assert multiplier is not None
|
||||
assert isinstance(multiplier, float)
|
||||
assert multiplier > 0
|
||||
|
||||
# Test with different personality - aggressive should have shorter distance
|
||||
controller.long_personality = LongPersonality.aggressive
|
||||
aggressive_multiplier = controller.get_follow_distance_multiplier(15.0)
|
||||
assert aggressive_multiplier is not None
|
||||
assert aggressive_multiplier < multiplier # Aggressive should have shorter distance
|
||||
|
||||
# Test with disabled controller
|
||||
mock_params.bool_data['VibeFollowPersonalityEnabled'] = False
|
||||
multiplier = controller.get_follow_distance_multiplier(15.0)
|
||||
assert multiplier is None
|
||||
|
||||
def test_personality_differences(self, controller, mock_params):
|
||||
"""Test that different personalities actually produce different values"""
|
||||
# Enable controller
|
||||
mock_params.bool_data['VibePersonalityEnabled'] = True
|
||||
mock_params.bool_data['VibeAccelPersonalityEnabled'] = True
|
||||
mock_params.bool_data['VibeFollowPersonalityEnabled'] = True
|
||||
|
||||
# Test acceleration differences - sport should have higher max acceleration than eco
|
||||
controller.accel_personality = AccelPersonality.eco
|
||||
eco_limits = controller.get_accel_limits(20.0)
|
||||
|
||||
controller.accel_personality = AccelPersonality.sport
|
||||
sport_limits = controller.get_accel_limits(20.0)
|
||||
|
||||
assert sport_limits[1] > eco_limits[1] # Sport should have higher max acceleration
|
||||
|
||||
# Test following distance differences - relaxed should have longer distance than aggressive
|
||||
controller.long_personality = LongPersonality.relaxed
|
||||
relaxed_dist = controller.get_follow_distance_multiplier(20.0)
|
||||
|
||||
controller.long_personality = LongPersonality.aggressive
|
||||
aggressive_dist = controller.get_follow_distance_multiplier(20.0)
|
||||
|
||||
assert relaxed_dist > aggressive_dist # Relaxed should have longer following distance
|
||||
|
||||
def test_reset(self, controller):
|
||||
"""Test reset functionality"""
|
||||
# Change some values
|
||||
controller.accel_personality = AccelPersonality.sport
|
||||
controller.long_personality = LongPersonality.relaxed
|
||||
controller.frame = 100
|
||||
|
||||
# Reset
|
||||
controller.reset()
|
||||
|
||||
# Check defaults are restored
|
||||
assert controller.accel_personality == AccelPersonality.normal
|
||||
assert controller.long_personality == LongPersonality.standard
|
||||
assert controller.frame == 0
|
||||
|
||||
def test_edge_cases(self, controller, mock_params):
|
||||
"""Test edge cases and error handling"""
|
||||
# Enable all features
|
||||
mock_params.bool_data = {
|
||||
'VibePersonalityEnabled': True,
|
||||
'VibeAccelPersonalityEnabled': True,
|
||||
'VibeFollowPersonalityEnabled': True
|
||||
}
|
||||
|
||||
# Test with zero speed
|
||||
limits = controller.get_accel_limits(0.0)
|
||||
assert limits is not None
|
||||
|
||||
multiplier = controller.get_follow_distance_multiplier(0.0)
|
||||
assert multiplier is not None
|
||||
|
||||
# Test with very high speed
|
||||
limits = controller.get_accel_limits(100.0)
|
||||
assert limits is not None
|
||||
|
||||
multiplier = controller.get_follow_distance_multiplier(100.0)
|
||||
assert multiplier is not None
|
||||
|
||||
# Test interpolation works correctly
|
||||
low_speed_limits = controller.get_accel_limits(5.0)
|
||||
high_speed_limits = controller.get_accel_limits(50.0)
|
||||
assert low_speed_limits[1] > high_speed_limits[1] # Max accel should decrease with speed
|
||||
@@ -0,0 +1,144 @@
|
||||
"""
|
||||
Copyright (c) 2021-, rav4kumar, 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 log, custom
|
||||
import numpy as np
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.common.params import Params
|
||||
|
||||
LongPersonality = log.LongitudinalPersonality
|
||||
AccelPersonality = custom.LongitudinalPlanSP.AccelerationPersonality
|
||||
|
||||
# Acceleration Profiles mapped to AccelPersonality (eco/normal/sport)
|
||||
MAX_ACCEL_PROFILES = {
|
||||
AccelPersonality.eco: [2.0, 1.99, 1.88, 1.10, .500, .292, .15, .10], # eco
|
||||
AccelPersonality.normal: [2.0, 2.00, 1.94, 1.22, .635, .33, .22, .16], # normal
|
||||
AccelPersonality.sport: [2.0, 2.00, 2.00, 1.85, .800, .54, .32, .22], # sport
|
||||
}
|
||||
MAX_ACCEL_BREAKPOINTS = [0., 4., 6., 9., 16., 25., 30., 55.]
|
||||
|
||||
# Braking profiles mapped to LongPersonality (relaxed/standard/aggressive)
|
||||
MIN_ACCEL_PROFILES = {
|
||||
LongPersonality.relaxed: [-.0006, -.0006, -.010, -.30, -1.20], # gentler braking
|
||||
LongPersonality.standard: [-.0007, -.0007, -.012, -.35, -1.20], # normal braking
|
||||
LongPersonality.aggressive: [-.0020, -.0008, -.014, -.40, -1.20], # more aggressive braking
|
||||
}
|
||||
MIN_ACCEL_BREAKPOINTS = [0., 3.0, 11., 14, 50.]
|
||||
|
||||
# Follow distance profiles mapped to LongPersonality (relaxed/standard/aggressive)
|
||||
FOLLOW_PROFILES = {
|
||||
LongPersonality.relaxed: [1.55, 1.65, 1.65, 1.80], # more spread out
|
||||
LongPersonality.standard: [1.45, 1.45, 1.45, 1.55], # balanced
|
||||
LongPersonality.aggressive: [1.20, 1.25, 1.28, 1.35], # tighter
|
||||
}
|
||||
FOLLOW_BREAKPOINTS = [0., 6., 18., 36.]
|
||||
|
||||
|
||||
class VibePersonalityController:
|
||||
"""Controller for acceleration and distance personalities"""
|
||||
|
||||
def __init__(self):
|
||||
self.params = Params()
|
||||
self.frame = 0
|
||||
self.accel_personality = AccelPersonality.normal
|
||||
self.long_personality = LongPersonality.standard
|
||||
self.param_keys = {
|
||||
'accel_personality': 'AccelPersonality',
|
||||
'long_personality': 'LongitudinalPersonality',
|
||||
'enabled': 'VibePersonalityEnabled',
|
||||
'accel_enabled': 'VibeAccelPersonalityEnabled',
|
||||
'follow_enabled': 'VibeFollowPersonalityEnabled'
|
||||
}
|
||||
|
||||
def _update_from_params(self):
|
||||
"""Update personalities from params"""
|
||||
if self.frame % int(1. / DT_MDL) != 0:
|
||||
return
|
||||
|
||||
accel_personality_int = int(self.params.get(self.param_keys['accel_personality']))
|
||||
self.accel_personality = accel_personality_int
|
||||
|
||||
long_personality_int = int(self.params.get(self.param_keys['long_personality']))
|
||||
self.long_personality = long_personality_int
|
||||
|
||||
def _get_toggle_state(self, key: str) -> bool:
|
||||
return self.params.get_bool(self.param_keys[key])
|
||||
|
||||
def _set_toggle_state(self, key: str, value: bool):
|
||||
self.params.put_bool(self.param_keys[key], value)
|
||||
|
||||
def set_accel_personality(self, personality: int) -> bool:
|
||||
self.accel_personality = personality
|
||||
self.params.put(self.param_keys['accel_personality'], str(personality))
|
||||
return True
|
||||
|
||||
def cycle_accel_personality(self) -> int:
|
||||
personalities = [AccelPersonality.eco, AccelPersonality.normal, AccelPersonality.sport]
|
||||
current_idx = personalities.index(self.accel_personality)
|
||||
next_personality = personalities[(current_idx + 1) % len(personalities)]
|
||||
self.set_accel_personality(next_personality)
|
||||
return int(next_personality)
|
||||
|
||||
def get_accel_personality(self) -> int:
|
||||
self._update_from_params()
|
||||
return int(self.accel_personality)
|
||||
|
||||
def set_long_personality(self, personality: int) -> bool:
|
||||
self.long_personality = personality
|
||||
self.params.put(self.param_keys['long_personality'], str(personality))
|
||||
return True
|
||||
|
||||
def cycle_long_personality(self) -> int:
|
||||
personalities = [LongPersonality.relaxed, LongPersonality.standard, LongPersonality.aggressive]
|
||||
current_idx = personalities.index(self.long_personality)
|
||||
next_personality = personalities[(current_idx + 1) % len(personalities)]
|
||||
self.set_long_personality(next_personality)
|
||||
return int(next_personality)
|
||||
|
||||
def get_long_personality(self) -> int:
|
||||
self._update_from_params()
|
||||
return int(self.long_personality)
|
||||
|
||||
def toggle_personality(self): return self._toggle_flag('enabled')
|
||||
def toggle_accel_personality(self): return self._toggle_flag('accel_enabled')
|
||||
def toggle_follow_distance_personality(self): return self._toggle_flag('follow_enabled')
|
||||
|
||||
def _toggle_flag(self, key):
|
||||
current = self._get_toggle_state(key)
|
||||
self._set_toggle_state(key, not current)
|
||||
return not current
|
||||
|
||||
def set_personality_enabled(self, enabled: bool): self._set_toggle_state('enabled', enabled)
|
||||
def is_accel_enabled(self) -> bool: return self._get_toggle_state('enabled') and self._get_toggle_state('accel_enabled')
|
||||
def is_follow_enabled(self) -> bool: return self._get_toggle_state('enabled') and self._get_toggle_state('follow_enabled')
|
||||
def is_enabled(self) -> bool: return self._get_toggle_state('enabled') and (self._get_toggle_state('accel_enabled') or self._get_toggle_state('follow_enabled'))
|
||||
|
||||
def get_accel_limits(self, v_ego: float) -> tuple[float, float]:
|
||||
"""Get acceleration limits based on current personalities."""
|
||||
self._update_from_params()
|
||||
max_a = np.interp(v_ego, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[self.accel_personality])
|
||||
min_a = np.interp(v_ego, MIN_ACCEL_BREAKPOINTS, MIN_ACCEL_PROFILES[self.long_personality])
|
||||
return float(min_a), float(max_a)
|
||||
|
||||
def get_follow_distance_multiplier(self, v_ego: float) -> float:
|
||||
"""Get dynamic following distance based on speed and personality"""
|
||||
self._update_from_params()
|
||||
return float(np.interp(v_ego, FOLLOW_BREAKPOINTS, FOLLOW_PROFILES[self.long_personality]))
|
||||
|
||||
def get_min_accel(self, v_ego: float) -> float:
|
||||
return self.get_accel_limits(v_ego)[0]
|
||||
|
||||
def get_max_accel(self, v_ego: float) -> float:
|
||||
return self.get_accel_limits(v_ego)[1]
|
||||
|
||||
def reset(self):
|
||||
self.accel_personality = AccelPersonality.normal
|
||||
self.long_personality = LongPersonality.standard
|
||||
self.frame = 0
|
||||
|
||||
def update(self):
|
||||
self.frame += 1
|
||||
@@ -226,4 +226,12 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
AlertStatus.normal, AlertSize.none,
|
||||
Priority.MID, VisualAlert.none, AudibleAlert.prompt, 3.),
|
||||
},
|
||||
|
||||
EventNameSP.laneChangeRoadEdge: {
|
||||
ET.WARNING: Alert(
|
||||
"Lane Change Unavailable: Road Edge",
|
||||
"",
|
||||
AlertStatus.userPrompt, AlertSize.small,
|
||||
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.1),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -178,14 +178,15 @@ procs += [
|
||||
PythonProcess("backup_manager", "sunnypilot.sunnylink.backups.manager", and_(only_offroad, sunnylink_ready_shim)),
|
||||
|
||||
# mapd
|
||||
NativeProcess("mapd", Paths.mapd_root(), ["bash", "-c", f"{MAPD_PATH} > /dev/null 2>&1"], mapd_ready),
|
||||
PythonProcess("mapd_manager", "sunnypilot.mapd.mapd_manager", always_run),
|
||||
#NativeProcess("mapd", Paths.mapd_root(), ["bash", "-c", f"{MAPD_PATH} > /dev/null 2>&1"], mapd_ready),
|
||||
#PythonProcess("mapd_manager", "sunnypilot.mapd.mapd_manager", always_run),
|
||||
NativeProcess("mapd", "selfdrive", ["./mapd"], always_run),
|
||||
|
||||
# navigationd
|
||||
PythonProcess("navigationd", "sunnypilot.navd.navigationd", only_onroad),
|
||||
|
||||
# locationd
|
||||
NativeProcess("locationd_llk", "sunnypilot/selfdrive/locationd", ["./locationd"], only_onroad),
|
||||
#NativeProcess("locationd_llk", "sunnypilot/selfdrive/locationd", ["./locationd"], only_onroad),
|
||||
]
|
||||
|
||||
if os.path.exists("./github_runner.sh"):
|
||||
|
||||
+1
-1
Submodule tinygrad_repo updated: d2bb1bcb97...7296c74cbd
Reference in New Issue
Block a user