mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-12 01:45:07 +08:00
Compare commits
91 Commits
sla-3.0
...
screensave
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acb4bb848b | ||
|
|
0cd913d7ac | ||
|
|
7fd1fea3b0 | ||
|
|
e79c3243b2 | ||
|
|
a936c311d7 | ||
|
|
8e53fe176d | ||
|
|
22a12aec35 | ||
|
|
426c2abd61 | ||
|
|
1a84559cec | ||
|
|
65b61fd965 | ||
|
|
a01449822f | ||
|
|
f52db76444 | ||
|
|
b61f095ef2 | ||
|
|
59e5fd7fe6 | ||
|
|
607811b6ad | ||
|
|
2414fe287d | ||
|
|
f7a4dbc47f | ||
|
|
fd0a5f806c | ||
|
|
de62240e97 | ||
|
|
adba29da1b | ||
|
|
6d6a91c318 | ||
|
|
7259687578 | ||
|
|
83a07e4bfa | ||
|
|
cb65777186 | ||
|
|
282b867fcb | ||
|
|
3fd3c31966 | ||
|
|
f3c3581472 | ||
|
|
465daf323c | ||
|
|
3fe8e155b6 | ||
|
|
c0bffa2a8c | ||
|
|
c9edcaa1a6 | ||
|
|
76101207cb | ||
|
|
62b01d3799 | ||
|
|
a3be1edde7 | ||
|
|
7869542683 | ||
|
|
981aab0500 | ||
|
|
9c82592906 | ||
|
|
5d3f95d420 | ||
|
|
f8d19fe9dd | ||
|
|
9d711350c2 | ||
|
|
45c853c87a | ||
|
|
e8ab9d812d | ||
|
|
ed775185f2 | ||
|
|
7bbbc6588e | ||
|
|
e68c65d15d | ||
|
|
0db8722221 | ||
|
|
a33497ed19 | ||
|
|
91f2bf3459 | ||
|
|
7fad2fc189 | ||
|
|
e74252bdf5 | ||
|
|
6b795ab513 | ||
|
|
3b1b4ef90f | ||
|
|
48d33e98e7 | ||
|
|
2717d97350 | ||
|
|
64232397ed | ||
|
|
0613442ac9 | ||
|
|
93ab6ee06b | ||
|
|
4baa170cfa | ||
|
|
2be95225b1 | ||
|
|
f8e8c59dd5 | ||
|
|
dd9d4bedf4 | ||
|
|
ada449989b | ||
|
|
68c7f69439 | ||
|
|
1db71aff57 | ||
|
|
942ccb90dd | ||
|
|
e6f5aae246 | ||
|
|
7032e4a972 | ||
|
|
ffa78eabaa | ||
|
|
5b03369a8f | ||
|
|
1e0564b484 | ||
|
|
eb94abaa14 | ||
|
|
4da32cc009 | ||
|
|
4820265268 | ||
|
|
01aa6c4204 | ||
|
|
21beea51ec | ||
|
|
98c479830c | ||
|
|
5369b6880f | ||
|
|
a9e57f0a76 | ||
|
|
712a358c94 | ||
|
|
423a7d2ed0 | ||
|
|
e4e10d4b87 | ||
|
|
632e9d13b2 | ||
|
|
362e9ce04b | ||
|
|
51d0666c85 | ||
|
|
deda1329a2 | ||
|
|
4110749cb0 | ||
|
|
3946e643f6 | ||
|
|
0c37a38596 | ||
|
|
9c5acf61c0 | ||
|
|
121b304fe0 | ||
|
|
47d848293b |
@@ -267,9 +267,6 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
||||
active @2 :Bool;
|
||||
vTarget @3 :Float32;
|
||||
aTarget @4 :Float32;
|
||||
capDelta @5 :Float32; # Difference between cluster set-speed and cap (m/s), positive = driver above cap
|
||||
targetCap @6 :Float32; # Speed limit cap being enforced (m/s)
|
||||
disableReason @7 :AssistDisableReason;
|
||||
}
|
||||
|
||||
enum Source {
|
||||
@@ -285,19 +282,6 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
||||
pending @3; # Awaiting new speed limit.
|
||||
adapting @4; # Reducing speed to match new speed limit.
|
||||
active @5; # Cruising at speed limit.
|
||||
capping @6; # Silently capping speed based on limit.
|
||||
tempPaused @7; # Temporarily paused by user.
|
||||
}
|
||||
|
||||
enum AssistDisableReason {
|
||||
none @0;
|
||||
userCancel @1;
|
||||
userTempPause @2;
|
||||
longOverride @3;
|
||||
belowFloor @4;
|
||||
autoResume @5;
|
||||
mapGap @6;
|
||||
gateDisabled @7;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,7 +342,6 @@ struct OnroadEventSP @0xda96579883444c35 {
|
||||
speedLimitChanged @21;
|
||||
speedLimitPending @22;
|
||||
e2eChime @23;
|
||||
speedLimitCapActive @24;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -558,8 +558,8 @@ struct PandaState @0xa7649e2575e4591e {
|
||||
|
||||
# these fields are not used by openpilot, but they're
|
||||
# reserved for forks building alternate experiences.
|
||||
controlsAllowedLateral @38 :Bool;
|
||||
controlsAllowedLongitudinal @39 :Bool;
|
||||
controlsAllowedRESERVED1 @38 :Bool;
|
||||
controlsAllowedRESERVED2 @39 :Bool;
|
||||
|
||||
enum FaultStatus {
|
||||
none @0;
|
||||
|
||||
@@ -1 +1 @@
|
||||
#define DEFAULT_MODEL "POP model (Default)"
|
||||
#define DEFAULT_MODEL "OP Model 7 (Default)"
|
||||
|
||||
@@ -179,6 +179,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"RocketFuel", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ScreenSaverEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ScreenSaverTimeout", {PERSISTENT | BACKUP, INT, "300"}},
|
||||
{"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
@@ -261,9 +263,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"SpeedLimitOffsetType", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"SpeedLimitPolicy", {PERSISTENT | BACKUP, INT, "3"}},
|
||||
{"SpeedLimitValueOffset", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"SpeedLimitUpshiftAccept", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"SpeedLimitMinCapFloor", {PERSISTENT | BACKUP, INT, "25"}},
|
||||
{"SpeedLimitCapAudioCue", {PERSISTENT | BACKUP, INT, "1"}},
|
||||
|
||||
// Smart Cruise Control
|
||||
{"MapTargetVelocities", {CLEAR_ON_ONROAD_TRANSITION, STRING}},
|
||||
|
||||
Submodule opendbc_repo updated: ded068839b...427032a89a
2
panda
2
panda
Submodule panda updated: c0cc96fbad...6cd1972ecf
@@ -33,7 +33,16 @@ if __name__ == "__main__":
|
||||
print("|-| ----- | --------- |")
|
||||
|
||||
for f in glob.glob(BASEDIR + MODEL_PATH + "/*.onnx"):
|
||||
# TODO: add checkpoint to DM
|
||||
if "dmonitoring" in f:
|
||||
continue
|
||||
|
||||
fn = os.path.basename(f)
|
||||
master = get_checkpoint(MASTER_PATH + MODEL_PATH + fn)
|
||||
master_path = MASTER_PATH + MODEL_PATH + fn
|
||||
if os.path.exists(master_path):
|
||||
master = get_checkpoint(master_path)
|
||||
master_col = f"[{master}](https://reporter.comma.life/experiment/{master})"
|
||||
else:
|
||||
master_col = "N/A (new model)"
|
||||
pr = get_checkpoint(BASEDIR + MODEL_PATH + fn)
|
||||
print("|", fn, "|", f"[{master}](https://reporterv2.comma.life/{master})", "|", f"[{pr}](https://reporterv2.comma.life/{pr})", "|")
|
||||
print("|", fn, "|", master_col, "|", f"[{pr}](https://reporter.comma.life/experiment/{pr})", "|")
|
||||
|
||||
@@ -21,7 +21,7 @@ tg_flags = {
|
||||
}.get(arch, 'DEV=CPU CPU_LLVM=1 THREADS=0')
|
||||
|
||||
# Get model metadata
|
||||
for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
|
||||
for model_name in ['driving_vision', 'driving_off_policy', 'driving_on_policy', 'dmonitoring_model']:
|
||||
fn = File(f"models/{model_name}").abspath
|
||||
script_files = [File(Dir("#selfdrive/modeld").File("get_model_metadata.py").abspath)]
|
||||
cmd = f'{tg_flags} python3 {Dir("#selfdrive/modeld").abspath}/get_model_metadata.py {fn}.onnx'
|
||||
@@ -59,6 +59,5 @@ def tg_compile(flags, model_name):
|
||||
)
|
||||
|
||||
# Compile small models
|
||||
for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
|
||||
for model_name in ['driving_vision', 'driving_off_policy', 'driving_on_policy', 'dmonitoring_model']:
|
||||
tg_compile(tg_flags, model_name)
|
||||
|
||||
|
||||
@@ -40,8 +40,10 @@ SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
||||
MODELS_DIR = Path(__file__).parent / 'models'
|
||||
VISION_PKL_PATH = MODELS_DIR / 'driving_vision_tinygrad.pkl'
|
||||
VISION_METADATA_PATH = MODELS_DIR / 'driving_vision_metadata.pkl'
|
||||
POLICY_PKL_PATH = MODELS_DIR / 'driving_policy_tinygrad.pkl'
|
||||
POLICY_METADATA_PATH = MODELS_DIR / 'driving_policy_metadata.pkl'
|
||||
ON_POLICY_PKL_PATH = MODELS_DIR / 'driving_on_policy_tinygrad.pkl'
|
||||
ON_POLICY_METADATA_PATH = MODELS_DIR / 'driving_on_policy_metadata.pkl'
|
||||
OFF_POLICY_PKL_PATH = MODELS_DIR / 'driving_off_policy_tinygrad.pkl'
|
||||
OFF_POLICY_METADATA_PATH = MODELS_DIR / 'driving_off_policy_metadata.pkl'
|
||||
|
||||
LAT_SMOOTH_SECONDS = 0.0
|
||||
LONG_SMOOTH_SECONDS = 0.3
|
||||
@@ -156,7 +158,13 @@ class ModelState(ModelStateBase):
|
||||
self.vision_output_slices = vision_metadata['output_slices']
|
||||
vision_output_size = vision_metadata['output_shapes']['outputs'][1]
|
||||
|
||||
with open(POLICY_METADATA_PATH, 'rb') as f:
|
||||
with open(OFF_POLICY_METADATA_PATH, 'rb') as f:
|
||||
off_policy_metadata = pickle.load(f)
|
||||
self.off_policy_input_shapes = off_policy_metadata['input_shapes']
|
||||
self.off_policy_output_slices = off_policy_metadata['output_slices']
|
||||
off_policy_output_size = off_policy_metadata['output_shapes']['outputs'][1]
|
||||
|
||||
with open(ON_POLICY_METADATA_PATH, 'rb') as f:
|
||||
policy_metadata = pickle.load(f)
|
||||
self.policy_input_shapes = policy_metadata['input_shapes']
|
||||
self.policy_output_slices = policy_metadata['output_slices']
|
||||
@@ -180,11 +188,13 @@ class ModelState(ModelStateBase):
|
||||
self.vision_output = np.zeros(vision_output_size, dtype=np.float32)
|
||||
self.policy_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()}
|
||||
self.policy_output = np.zeros(policy_output_size, dtype=np.float32)
|
||||
self.off_policy_output = np.zeros(off_policy_output_size, dtype=np.float32)
|
||||
self.parser = Parser()
|
||||
self.frame_buf_params : dict[str, tuple[int, int, int, int]] = {}
|
||||
self.update_imgs = None
|
||||
self.vision_run = pickle.loads(read_file_chunked(str(VISION_PKL_PATH)))
|
||||
self.policy_run = pickle.loads(read_file_chunked(str(POLICY_PKL_PATH)))
|
||||
self.policy_run = pickle.loads(read_file_chunked(str(ON_POLICY_PKL_PATH)))
|
||||
self.off_policy_run = pickle.loads(read_file_chunked(str(OFF_POLICY_PKL_PATH)))
|
||||
|
||||
def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]:
|
||||
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
|
||||
@@ -233,9 +243,17 @@ class ModelState(ModelStateBase):
|
||||
|
||||
self.policy_output = self.policy_run(**self.policy_inputs).contiguous().realize().uop.base.buffer.numpy().flatten()
|
||||
policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(self.policy_output, self.policy_output_slices))
|
||||
combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict}
|
||||
|
||||
self.off_policy_output = self.off_policy_run(**self.policy_inputs).contiguous().realize().uop.base.buffer.numpy()
|
||||
off_policy_outputs_dict = self.parser.parse_off_policy_outputs(self.slice_outputs(self.off_policy_output, self.off_policy_output_slices))
|
||||
off_policy_outputs_dict.pop('plan')
|
||||
|
||||
|
||||
combined_outputs_dict = {**vision_outputs_dict, **off_policy_outputs_dict, **policy_outputs_dict}
|
||||
if 'planplus' in combined_outputs_dict and 'plan' in combined_outputs_dict:
|
||||
combined_outputs_dict['plan'] = combined_outputs_dict['plan'] + combined_outputs_dict['planplus']
|
||||
if SEND_RAW_PRED:
|
||||
combined_outputs_dict['raw_pred'] = np.concatenate([self.vision_output.copy(), self.policy_output.copy()])
|
||||
combined_outputs_dict['raw_pred'] = np.concatenate([self.vision_output.copy(), self.policy_output.copy(), self.off_policy_output.copy()])
|
||||
|
||||
return combined_outputs_dict
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
driving_policy.onnx
|
||||
@@ -1 +0,0 @@
|
||||
driving_vision.onnx
|
||||
BIN
selfdrive/modeld/models/driving_off_policy.onnx
LFS
Normal file
BIN
selfdrive/modeld/models/driving_off_policy.onnx
LFS
Normal file
Binary file not shown.
BIN
selfdrive/modeld/models/driving_on_policy.onnx
LFS
Normal file
BIN
selfdrive/modeld/models/driving_on_policy.onnx
LFS
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -96,11 +96,17 @@ class Parser:
|
||||
self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
|
||||
self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,))
|
||||
self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
|
||||
self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH))
|
||||
self.parse_binary_crossentropy('meta', outs)
|
||||
return outs
|
||||
|
||||
def parse_off_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
|
||||
plan_mhp = self.is_mhp(outs, 'plan', ModelConstants.IDX_N * ModelConstants.PLAN_WIDTH)
|
||||
plan_in_N, plan_out_N = (ModelConstants.PLAN_MHP_N, ModelConstants.PLAN_MHP_SELECTION) if plan_mhp else (0, 0)
|
||||
self.parse_mdn('plan', outs, in_N=plan_in_N, out_N=plan_out_N, out_shape=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH))
|
||||
self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
|
||||
self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
|
||||
self.parse_binary_crossentropy('lane_lines_prob', outs)
|
||||
self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH))
|
||||
self.parse_binary_crossentropy('meta', outs)
|
||||
self.parse_binary_crossentropy('lead_prob', outs)
|
||||
lead_mhp = self.is_mhp(outs, 'lead', ModelConstants.LEAD_MHP_SELECTION * ModelConstants.LEAD_TRAJ_LEN * ModelConstants.LEAD_WIDTH)
|
||||
lead_in_N, lead_out_N = (ModelConstants.LEAD_MHP_N, ModelConstants.LEAD_MHP_SELECTION) if lead_mhp else (0, 0)
|
||||
@@ -110,7 +116,7 @@ class Parser:
|
||||
return outs
|
||||
|
||||
def parse_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
|
||||
plan_mhp = self.is_mhp(outs, 'plan', ModelConstants.IDX_N * ModelConstants.PLAN_WIDTH)
|
||||
plan_mhp = self.is_mhp(outs, 'plan', ModelConstants.IDX_N * ModelConstants.PLAN_WIDTH)
|
||||
plan_in_N, plan_out_N = (ModelConstants.PLAN_MHP_N, ModelConstants.PLAN_MHP_SELECTION) if plan_mhp else (0, 0)
|
||||
self.parse_mdn('plan', outs, in_N=plan_in_N, out_N=plan_out_N, out_shape=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH))
|
||||
if 'planplus' in outs:
|
||||
@@ -120,5 +126,6 @@ class Parser:
|
||||
|
||||
def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
|
||||
outs = self.parse_vision_outputs(outs)
|
||||
outs = self.parse_off_policy_outputs(outs)
|
||||
outs = self.parse_policy_outputs(outs)
|
||||
return outs
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#define CUTOFF_IL 400
|
||||
#define SATURATE_IL 1000
|
||||
|
||||
#define ALT_EXP_MADS_DISENGAGE_LATERAL_ON_BRAKE 2048
|
||||
|
||||
ExitHandler do_exit;
|
||||
|
||||
bool check_connected(Panda *panda) {
|
||||
@@ -32,8 +34,15 @@ bool check_connected(Panda *panda) {
|
||||
}
|
||||
|
||||
bool process_mads_heartbeat(SubMaster *sm) {
|
||||
const int &alt_exp = (*sm)["carParams"].getCarParams().getAlternativeExperience();
|
||||
const bool disengage_lateral_on_brake = (alt_exp & ALT_EXP_MADS_DISENGAGE_LATERAL_ON_BRAKE) != 0;
|
||||
|
||||
const auto &mads = (*sm)["selfdriveStateSP"].getSelfdriveStateSP().getMads();
|
||||
return sm->allAliveAndValid({"selfdriveStateSP"}) && mads.getEnabled();
|
||||
const bool heartbeat_type = disengage_lateral_on_brake ? mads.getActive() : mads.getEnabled();
|
||||
|
||||
const bool engaged = sm->allAliveAndValid({"selfdriveStateSP"}) && heartbeat_type;
|
||||
|
||||
return engaged;
|
||||
}
|
||||
|
||||
Panda *connect(std::string serial) {
|
||||
@@ -143,8 +152,6 @@ void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::Panda
|
||||
ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f);
|
||||
ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f);
|
||||
ps.setSoundOutputLevel(health.sound_output_level_pkt);
|
||||
ps.setControlsAllowedLateral(health.controls_allowed_lateral_pkt);
|
||||
ps.setControlsAllowedLongitudinal(health.controls_allowed_longitudinal_pkt);
|
||||
}
|
||||
|
||||
void fill_panda_can_state(cereal::PandaState::PandaCanState::Builder &cs, const can_health_t &can_health) {
|
||||
@@ -373,7 +380,7 @@ void pandad_run(Panda *panda) {
|
||||
|
||||
Params params;
|
||||
RateKeeper rk("pandad", 100);
|
||||
SubMaster sm({"selfdriveState", "selfdriveStateSP"});
|
||||
SubMaster sm({"selfdriveState", "selfdriveStateSP", "carParams"});
|
||||
PubMaster pm({"can", "pandaStates", "peripheralState"});
|
||||
PandaSafety panda_safety(panda);
|
||||
bool engaged = false;
|
||||
|
||||
@@ -13,6 +13,7 @@ from openpilot.system.ui.lib.application import gui_app
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout
|
||||
|
||||
|
||||
ONROAD_DELAY = 2.5 # seconds
|
||||
|
||||
|
||||
|
||||
@@ -21,8 +21,6 @@ from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
SPEED_LIMIT_MODE_BUTTONS = [tr("Off"), tr("Info"), tr("Warning"), tr("Assist")]
|
||||
SPEED_LIMIT_OFFSET_TYPE_BUTTONS = [tr("None"), tr("Fixed"), tr("%")]
|
||||
SPEED_LIMIT_UPSHIFT_ACCEPT_BUTTONS = [tr("Never Raise"), tr("Accel Pedal Confirm")]
|
||||
SPEED_LIMIT_CAP_AUDIO_CUE_BUTTONS = [tr("Off"), tr("On")]
|
||||
|
||||
SPEED_LIMIT_MODE_DESCRIPTIONS = [
|
||||
tr("Off: Disables the Speed Limit functions."),
|
||||
@@ -37,16 +35,6 @@ SPEED_LIMIT_OFFSET_DESCRIPTIONS = [
|
||||
tr("Percent: Adds a percent offset [Speed Limit + (Offset % Speed Limit)]"),
|
||||
]
|
||||
|
||||
SPEED_LIMIT_UPSHIFT_ACCEPT_DESCRIPTIONS = [
|
||||
tr("Never Raise: Keeps the current cap when the speed limit changes."),
|
||||
tr("Accel Pedal Confirm: Accepts new speed limit cap when you release the accelerator pedal."),
|
||||
]
|
||||
|
||||
SPEED_LIMIT_CAP_AUDIO_CUE_DESCRIPTIONS = [
|
||||
tr("Off: No audio cue when entering speed limit capping mode."),
|
||||
tr("On: Plays a low chime when entering speed limit capping mode."),
|
||||
]
|
||||
|
||||
|
||||
class PanelType(IntEnum):
|
||||
SETTINGS = 0
|
||||
@@ -98,42 +86,13 @@ class SpeedLimitSettingsLayout(Widget):
|
||||
label_callback=self._get_offset_label,
|
||||
)
|
||||
|
||||
self._speed_limit_upshift_accept = multiple_button_item_sp(
|
||||
title=lambda: tr("Speed Limit Cap Upshift"),
|
||||
description=self._get_upshift_accept_description,
|
||||
buttons=SPEED_LIMIT_UPSHIFT_ACCEPT_BUTTONS,
|
||||
param="SpeedLimitUpshiftAccept",
|
||||
button_width=500,
|
||||
)
|
||||
|
||||
self._speed_limit_min_cap_floor = option_item_sp(
|
||||
title=lambda: tr("Speed Limit Cap Floor"),
|
||||
param="SpeedLimitMinCapFloor",
|
||||
min_value=0,
|
||||
max_value=40,
|
||||
description=self._get_min_cap_floor_description,
|
||||
label_callback=self._get_min_cap_floor_label,
|
||||
)
|
||||
|
||||
self._speed_limit_cap_audio_cue = multiple_button_item_sp(
|
||||
title=lambda: tr("Speed Limit Cap Audio Cue"),
|
||||
description=self._get_cap_audio_cue_description,
|
||||
buttons=SPEED_LIMIT_CAP_AUDIO_CUE_BUTTONS,
|
||||
param="SpeedLimitCapAudioCue",
|
||||
button_width=450,
|
||||
)
|
||||
|
||||
items = [
|
||||
self._speed_limit_mode,
|
||||
LineSeparatorSP(40),
|
||||
self._source_button,
|
||||
LineSeparatorSP(40),
|
||||
self._speed_limit_offset_type,
|
||||
self._speed_limit_value_offset,
|
||||
LineSeparatorSP(40),
|
||||
self._speed_limit_upshift_accept,
|
||||
self._speed_limit_min_cap_floor,
|
||||
self._speed_limit_cap_audio_cue,
|
||||
self._speed_limit_value_offset
|
||||
]
|
||||
return items
|
||||
|
||||
@@ -161,23 +120,6 @@ class SpeedLimitSettingsLayout(Widget):
|
||||
return f"{value} {unit}"
|
||||
return str(value)
|
||||
|
||||
@staticmethod
|
||||
def _get_upshift_accept_description():
|
||||
return get_highlighted_description(ui_state.params, "SpeedLimitUpshiftAccept", SPEED_LIMIT_UPSHIFT_ACCEPT_DESCRIPTIONS)
|
||||
|
||||
@staticmethod
|
||||
def _get_min_cap_floor_description():
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _get_min_cap_floor_label(value):
|
||||
unit = tr("km/h") if ui_state.is_metric else tr("mph")
|
||||
return f"{value} {unit}"
|
||||
|
||||
@staticmethod
|
||||
def _get_cap_audio_cue_description():
|
||||
return get_highlighted_description(ui_state.params, "SpeedLimitCapAudioCue", SPEED_LIMIT_CAP_AUDIO_CUE_DESCRIPTIONS)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
@@ -186,7 +128,6 @@ class SpeedLimitSettingsLayout(Widget):
|
||||
brand = ui_state.CP.brand
|
||||
has_long = ui_state.has_longitudinal_control
|
||||
has_icbm = ui_state.has_icbm
|
||||
pcm_op_long = has_long and ui_state.CP.pcmCruise
|
||||
|
||||
"""
|
||||
Speed Limit Assist is available when:
|
||||
@@ -203,7 +144,6 @@ class SpeedLimitSettingsLayout(Widget):
|
||||
|
||||
else:
|
||||
sla_available = False
|
||||
pcm_op_long = False
|
||||
|
||||
if not sla_available:
|
||||
self._speed_limit_mode.action_item.set_enabled_buttons({
|
||||
@@ -217,10 +157,6 @@ class SpeedLimitSettingsLayout(Widget):
|
||||
offset_type = ui_state.params.get("SpeedLimitOffsetType", return_default=True)
|
||||
self._speed_limit_value_offset.set_visible(offset_type != int(SpeedLimitOffsetType.off))
|
||||
|
||||
self._speed_limit_upshift_accept.set_visible(pcm_op_long)
|
||||
self._speed_limit_min_cap_floor.set_visible(pcm_op_long)
|
||||
self._speed_limit_cap_audio_cue.set_visible(pcm_op_long)
|
||||
|
||||
def _render(self, rect):
|
||||
if self._current_panel == PanelType.POLICY:
|
||||
self._policy_layout.render(rect)
|
||||
|
||||
@@ -6,10 +6,12 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from enum import IntEnum
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.sunnypilot.widgets.option_control import OptionControlSP
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import option_item_sp
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp, option_item_sp, ToggleActionSP
|
||||
from openpilot.sunnypilot.system.params_migration import ONROAD_BRIGHTNESS_TIMER_VALUES
|
||||
|
||||
|
||||
@@ -23,6 +25,7 @@ class DisplayLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
@@ -61,10 +64,27 @@ class DisplayLayout(Widget):
|
||||
f"{value} s" if value < 60 else f"{int(value/60)} m"),
|
||||
inline=True
|
||||
)
|
||||
self._screensaver_toggle = toggle_item_sp(
|
||||
param="ScreenSaverEnabled",
|
||||
title=lambda: tr("Enable sunnypilot Screen Saver"),
|
||||
description=lambda: tr("Enable screen saver when the device is offroad & idle. " +
|
||||
"The screen saver will kick off after the interactivity timeout expires, and will stay on for the duration configured below."),
|
||||
)
|
||||
self._screensaver_timeout = option_item_sp(
|
||||
param="ScreenSaverTimeout",
|
||||
title=lambda: tr("Screen Saver Timeout"),
|
||||
description=lambda: tr("Configure how long the screen saver should stay on after the interactivity timeout expires."),
|
||||
min_value=60,
|
||||
max_value=600,
|
||||
value_change_step=60,
|
||||
label_callback=lambda value: f"{int(value/60)} m"
|
||||
)
|
||||
items = [
|
||||
self._onroad_brightness,
|
||||
self._onroad_brightness_timer,
|
||||
self._interactivity_timeout,
|
||||
self._screensaver_toggle,
|
||||
self._screensaver_timeout,
|
||||
]
|
||||
return items
|
||||
|
||||
@@ -84,9 +104,21 @@ class DisplayLayout(Widget):
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
brightness_val = self._onroad_brightness.action_item.current_value
|
||||
for _item in self._scroller._items:
|
||||
if isinstance(_item.action_item, ToggleActionSP) and _item.action_item.toggle.param_key is not None:
|
||||
_item.action_item.set_state(self._params.get_bool(_item.action_item.toggle.param_key))
|
||||
elif isinstance(_item.action_item, OptionControlSP) and _item.action_item.param_key is not None:
|
||||
raw_value = self._params.get(_item.action_item.param_key, return_default=True)
|
||||
if _item.action_item.value_map:
|
||||
reverse_map = {v: k for k, v in _item.action_item.value_map.items()}
|
||||
raw_value = reverse_map.get(raw_value, _item.action_item.current_value)
|
||||
_item.action_item.set_value(raw_value)
|
||||
|
||||
brightness_val = self._params.get("OnroadScreenOffBrightness", return_default=True)
|
||||
self._onroad_brightness_timer.action_item.set_enabled(brightness_val not in (OnroadBrightness.AUTO, OnroadBrightness.AUTO_DARK))
|
||||
|
||||
self._screensaver_timeout.set_visible(self._screensaver_toggle.action_item.get_state())
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class ModelsLayout(Widget):
|
||||
|
||||
self._initialize_items()
|
||||
|
||||
self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB")
|
||||
self.clear_cache_item.action_item.set_value(f"{self._calculate_cache_size():.2f} MB")
|
||||
for ctrl, key in [(self.lane_turn_value_control, "LaneTurnValue"), (self.delay_control, "LagdToggleDelay")]:
|
||||
ctrl.action_item.set_value(int(float(ui_state.params.get(key, return_default=True)) * 100))
|
||||
|
||||
@@ -112,7 +112,7 @@ class ModelsLayout(Widget):
|
||||
self.model_manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading)
|
||||
|
||||
@staticmethod
|
||||
def calculate_cache_size():
|
||||
def _calculate_cache_size():
|
||||
cache_size = 0.0
|
||||
if os.path.exists(CUSTOM_MODEL_PATH):
|
||||
cache_size = sum(os.path.getsize(os.path.join(CUSTOM_MODEL_PATH, file)) for file in os.listdir(CUSTOM_MODEL_PATH)) / (1024**2)
|
||||
@@ -122,7 +122,7 @@ class ModelsLayout(Widget):
|
||||
def _callback(response):
|
||||
if response == DialogResult.CONFIRM:
|
||||
ui_state.params.put_bool("ModelManager_ClearCache", True)
|
||||
self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB")
|
||||
self.clear_cache_item.action_item.set_value(f"{self._calculate_cache_size():.2f} MB")
|
||||
|
||||
dialog = ConfirmDialog(tr("This will delete ALL downloaded models from the cache except the currently active model. Are you sure?"),
|
||||
tr("Clear Cache"), callback=_callback)
|
||||
@@ -155,7 +155,7 @@ class ModelsLayout(Widget):
|
||||
|
||||
if (current_time := time.monotonic()) - self.last_cache_calc_time > 0.5:
|
||||
self.last_cache_calc_time = current_time
|
||||
self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB")
|
||||
self.clear_cache_item.action_item.set_value(f"{self._calculate_cache_size():.2f} MB")
|
||||
|
||||
if self.download_status == custom.ModelManagerSP.DownloadStatus.downloading:
|
||||
device._reset_interactive_timeout()
|
||||
|
||||
@@ -5,45 +5,13 @@ 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 collections.abc import Callable
|
||||
import pyray as rl
|
||||
|
||||
from cereal import custom
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
|
||||
class CurrentModelInfo(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.set_rect(rl.Rectangle(0, 0, 360, 180))
|
||||
|
||||
header_color = rl.Color(255, 255, 255, int(255 * 0.9))
|
||||
subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65))
|
||||
max_width = int(self._rect.width - 20)
|
||||
self.current_model_header = UnifiedLabel(tr("active model"), 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY)
|
||||
self.current_model_text = UnifiedLabel(tr("default model"), 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN, scroll=True)
|
||||
|
||||
self.info_header = UnifiedLabel("cache size", 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY)
|
||||
self.info_text = UnifiedLabel("0 mb", 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN)
|
||||
|
||||
def _render(self, _):
|
||||
self.current_model_header.set_position(self._rect.x + 20, self._rect.y - 10)
|
||||
self.current_model_header.render()
|
||||
|
||||
self.current_model_text.set_position(self._rect.x + 20, self._rect.y + 68 - 25)
|
||||
self.current_model_text.render()
|
||||
|
||||
self.info_header.set_position(self._rect.x + 20, self._rect.y + 114 - 30)
|
||||
self.info_header.render()
|
||||
|
||||
self.info_text.set_position(self._rect.x + 20, self._rect.y + 161 - 25)
|
||||
self.info_text.render()
|
||||
|
||||
class ModelsLayoutMici(NavScroller):
|
||||
def __init__(self, back_callback: Callable):
|
||||
@@ -52,35 +20,25 @@ class ModelsLayoutMici(NavScroller):
|
||||
self.original_back_callback = back_callback
|
||||
self.focused_widget = None
|
||||
|
||||
self.current_model_info = CurrentModelInfo()
|
||||
self._download_progress = "."
|
||||
self._download_frame = 0
|
||||
self._was_downloading = False
|
||||
|
||||
self.select_model_btn = BigButton(tr("select model"))
|
||||
self.select_model_btn.set_click_callback(self._show_folders)
|
||||
self.current_model_btn = BigButton(tr("current model"))
|
||||
self.current_model_btn.set_click_callback(self._show_folders)
|
||||
|
||||
self.cancel_download_btn = BigButton(tr("cancel download"))
|
||||
self.cancel_download_btn.set_click_callback(lambda: ui_state.params.remove("ModelManager_DownloadIndex"))
|
||||
|
||||
self.main_items = [self.current_model_info, self.select_model_btn, self.cancel_download_btn]
|
||||
self.main_items = [self.current_model_btn, self.cancel_download_btn]
|
||||
self._scroller.add_widgets(self.main_items)
|
||||
|
||||
@property
|
||||
def model_manager(self):
|
||||
return ui_state.sm["modelManagerSP"]
|
||||
|
||||
def _get_grouped_bundles(self, favorites = None):
|
||||
def _get_grouped_bundles(self):
|
||||
bundles = self.model_manager.availableBundles
|
||||
folders = {}
|
||||
for bundle in bundles:
|
||||
folder = next((override.value for override in bundle.overrides if override.key == "folder"), "")
|
||||
folders.setdefault(folder, []).append(bundle)
|
||||
|
||||
if favorites:
|
||||
for fav_bundle in [bundle for bundle in bundles if bundle.ref in favorites]:
|
||||
folders.setdefault("favorites", []).append(fav_bundle)
|
||||
|
||||
return folders
|
||||
|
||||
def _show_selection_view(self, items, back_callback: Callable):
|
||||
@@ -91,25 +49,18 @@ class ModelsLayoutMici(NavScroller):
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
def _show_folders(self):
|
||||
self.focused_widget = self.select_model_btn
|
||||
|
||||
favs = ui_state.params.get("ModelManager_Favs")
|
||||
favorites = set(favs.split(';')) if favs else set()
|
||||
|
||||
folders = self._get_grouped_bundles(favorites)
|
||||
self.focused_widget = self.current_model_btn
|
||||
folders = self._get_grouped_bundles()
|
||||
folder_buttons = []
|
||||
default_btn = BigButton(tr("default model"))
|
||||
default_btn.set_click_callback(self._select_default)
|
||||
folder_buttons.append(default_btn)
|
||||
|
||||
for folder in sorted(folders.keys(), key=lambda f: max((bundle.index for bundle in folders[f]), default=-1), reverse=True):
|
||||
if folder.lower() in ["release models", "master models", "favorites"]:
|
||||
if folder.lower() in ["release models", "master models"]:
|
||||
btn = BigButton(folder.lower())
|
||||
btn.set_click_callback(lambda f=folder: self._select_folder(f))
|
||||
if folder.lower() == "favorites":
|
||||
folder_buttons.insert(0, btn)
|
||||
else:
|
||||
folder_buttons.append(btn)
|
||||
folder_buttons.append(btn)
|
||||
self._show_selection_view(folder_buttons, self._reset_main_view)
|
||||
|
||||
def _select_model(self, bundle):
|
||||
@@ -121,10 +72,7 @@ class ModelsLayoutMici(NavScroller):
|
||||
self._reset_main_view()
|
||||
|
||||
def _select_folder(self, folder_name):
|
||||
favs = ui_state.params.get("ModelManager_Favs")
|
||||
favorites = set(favs.split(';')) if favs else set()
|
||||
|
||||
folders = self._get_grouped_bundles(favorites)
|
||||
folders = self._get_grouped_bundles()
|
||||
bundles = sorted(folders.get(folder_name, []), key=lambda b: b.index, reverse=True)
|
||||
|
||||
btns = []
|
||||
@@ -138,62 +86,29 @@ class ModelsLayoutMici(NavScroller):
|
||||
def _reset_main_view(self):
|
||||
self._scroller._items = self.main_items
|
||||
self.set_back_callback(self.original_back_callback)
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
self._scroller.scroll_to(0)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
if self._was_downloading:
|
||||
device.set_override_interactive_timeout(None)
|
||||
self._was_downloading = False
|
||||
if self.focused_widget and self.focused_widget in self.main_items:
|
||||
x = self._scroller._pad
|
||||
for item in self.main_items:
|
||||
if not item.is_visible:
|
||||
continue
|
||||
if item == self.focused_widget:
|
||||
break
|
||||
x += item.rect.width + self._scroller._spacing
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
self._scroller.scroll_to(x)
|
||||
self.focused_widget = None
|
||||
else:
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
self.select_model_btn.set_enabled(ui_state.is_offroad())
|
||||
self.cancel_download_btn.set_visible(False)
|
||||
self.current_model_info.current_model_header._shimmer = False
|
||||
self.current_model_info.info_header._shimmer = False
|
||||
|
||||
manager = self.model_manager
|
||||
self._download_frame += 1
|
||||
should_update = self._download_frame % (gui_app.target_fps / 2) == 0
|
||||
if should_update:
|
||||
self._download_progress = self._download_progress + "." if len(self._download_progress) < 3 else ""
|
||||
|
||||
is_downloading = (manager.selectedBundle
|
||||
and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading)
|
||||
if self._was_downloading and not is_downloading:
|
||||
device.set_override_interactive_timeout(None)
|
||||
self._was_downloading = is_downloading
|
||||
|
||||
self.current_model_info.current_model_header.set_text(tr("active model"))
|
||||
self.current_model_info.current_model_text.set_text(manager.activeBundle.displayName.lower() if manager.activeBundle.index > 0 else tr("default model"))
|
||||
self.current_model_info.info_header.set_text(tr("cache size"))
|
||||
self.current_model_info.info_text.set_text(f"{ModelsLayout.calculate_cache_size():.2f} MB")
|
||||
|
||||
if manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.failed:
|
||||
self.current_model_info.info_header.set_text(tr("error") + self._download_progress)
|
||||
self.current_model_info.info_text.set_text(tr("download failed"))
|
||||
|
||||
elif manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading:
|
||||
if manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading:
|
||||
self.current_model_btn.set_value("downloading...")
|
||||
self.cancel_download_btn.set_visible(True)
|
||||
device.set_override_interactive_timeout(5)
|
||||
progress = 0.0
|
||||
count = 0
|
||||
for model in manager.selectedBundle.models:
|
||||
count += 1
|
||||
p = model.artifact.downloadProgress
|
||||
if p.status == custom.ModelManagerSP.DownloadStatus.downloading:
|
||||
progress += p.progress
|
||||
elif p.status in (custom.ModelManagerSP.DownloadStatus.downloaded,
|
||||
custom.ModelManagerSP.DownloadStatus.cached):
|
||||
progress += 100.0
|
||||
|
||||
self.current_model_info.current_model_header.set_text(tr("downloading"))
|
||||
self.current_model_info.current_model_header._shimmer = True
|
||||
self.current_model_info.current_model_text.set_text(f"{manager.selectedBundle.internalName.lower()}")
|
||||
self.current_model_info.info_header.set_text(tr("progress") + self._download_progress)
|
||||
self.current_model_info.info_header._shimmer = True
|
||||
self.current_model_info.info_text.set_text(f"{progress/count:.2f}%")
|
||||
|
||||
else:
|
||||
self.current_model_btn.set_value(manager.activeBundle.internalName.lower() if manager.activeBundle else tr("default model"))
|
||||
self.cancel_download_btn.set_visible(False)
|
||||
self.current_model_btn.set_enabled(ui_state.is_offroad())
|
||||
self.current_model_btn.set_text(tr("current model"))
|
||||
|
||||
@@ -5,32 +5,18 @@ 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 openpilot.selfdrive.ui.mici.layouts.settings import settings as OP
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, BigDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.sunnylink import SunnylinkLayoutMici
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.models import ModelsLayoutMici
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
|
||||
ICON_SIZE = 70
|
||||
BIG_ICON_SIZE = 110
|
||||
|
||||
|
||||
class SettingsLayoutSP(OP.SettingsLayout):
|
||||
def __init__(self):
|
||||
OP.SettingsLayout.__init__(self)
|
||||
|
||||
device_panel = DeviceLayoutMici()
|
||||
self._scroller._items[2].set_click_callback(lambda: gui_app.push_widget(device_panel))
|
||||
|
||||
self.icon_offroad_enable = gui_app.texture("../../sunnypilot/selfdrive/assets/icons_mici/always_offroad.png", BIG_ICON_SIZE,
|
||||
BIG_ICON_SIZE)
|
||||
self.icon_offroad_disable = gui_app.texture("../../sunnypilot/selfdrive/assets/icons_mici/disable_offroad.png", BIG_ICON_SIZE,
|
||||
BIG_ICON_SIZE)
|
||||
self.icon_offroad_slider = gui_app.texture("icons_mici/settings/device/lkas.png", BIG_ICON_SIZE, BIG_ICON_SIZE)
|
||||
|
||||
sunnylink_panel = SunnylinkLayoutMici(back_callback=gui_app.pop_widget)
|
||||
sunnylink_btn = BigButton("sunnylink", "", gui_app.texture("icons_mici/settings/developer/ssh.png", ICON_SIZE, ICON_SIZE))
|
||||
sunnylink_btn.set_click_callback(lambda: gui_app.push_widget(sunnylink_panel))
|
||||
@@ -39,53 +25,10 @@ class SettingsLayoutSP(OP.SettingsLayout):
|
||||
models_btn = BigButton("models", "", gui_app.texture("../../sunnypilot/selfdrive/assets/offroad/icon_models.png", ICON_SIZE, ICON_SIZE))
|
||||
models_btn.set_click_callback(lambda: gui_app.push_widget(models_panel))
|
||||
|
||||
# onroad: enable button sits at the front (left of toggles)
|
||||
self._enable_offroad_btn_onroad = BigCircleButton(self.icon_offroad_enable, red=True)
|
||||
self._enable_offroad_btn_onroad.set_click_callback(lambda: self._handle_always_offroad(True))
|
||||
self._enable_offroad_btn_onroad.set_visible(lambda: ui_state.started and not ui_state.always_offroad)
|
||||
|
||||
# offroad: enable button sits at the end (right of developer)
|
||||
self._enable_offroad_btn_offroad = BigCircleButton(self.icon_offroad_enable, red=True)
|
||||
self._enable_offroad_btn_offroad.set_click_callback(lambda: self._handle_always_offroad(True))
|
||||
self._enable_offroad_btn_offroad.set_visible(lambda: not ui_state.started and not ui_state.always_offroad)
|
||||
|
||||
self._disable_offroad_btn = BigCircleButton(self.icon_offroad_disable, red=False)
|
||||
self._disable_offroad_btn.set_click_callback(lambda: self._handle_always_offroad(False))
|
||||
self._disable_offroad_btn.set_visible(lambda: ui_state.always_offroad)
|
||||
|
||||
items = self._scroller._items.copy()
|
||||
|
||||
items.insert(1, sunnylink_btn)
|
||||
items.insert(2, models_btn)
|
||||
|
||||
# front slots (only one ever visible at a time): exit-always-offroad, then enable-onroad
|
||||
items.insert(0, self._enable_offroad_btn_onroad)
|
||||
items.insert(0, self._disable_offroad_btn)
|
||||
# end slot: enable-offroad (right of developer)
|
||||
items.append(self._enable_offroad_btn_offroad)
|
||||
|
||||
self._scroller._items.clear()
|
||||
for item in items:
|
||||
self._scroller.add_widget(item)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
def _handle_always_offroad(self, enable: bool):
|
||||
|
||||
def _set_offroad_status(status: bool):
|
||||
if not ui_state.engaged:
|
||||
ui_state.params.put_bool("OffroadMode", status)
|
||||
ui_state.always_offroad = status
|
||||
|
||||
if not enable:
|
||||
dlg = BigConfirmationDialog(tr("slide to exit always offroad"), self.icon_offroad_slider, red=False,
|
||||
confirm_callback=lambda: _set_offroad_status(False))
|
||||
else:
|
||||
if ui_state.engaged:
|
||||
gui_app.push_widget(BigDialog(tr("disengage to enable always offroad"), "", ))
|
||||
return
|
||||
|
||||
dlg = BigConfirmationDialog(tr("slide to force offroad"), self.icon_offroad_slider, red=True,
|
||||
confirm_callback=lambda: _set_offroad_status(True))
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
@@ -6,7 +6,6 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import pyray as rl
|
||||
|
||||
from cereal import custom
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiRenderer, DeveloperUiState, get_bottom_dev_ui_offset
|
||||
@@ -24,7 +23,6 @@ from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
|
||||
SLA_ACTIVE_COLOR = rl.Color(0x91, 0x9b, 0x95, 0xff)
|
||||
AssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState
|
||||
|
||||
|
||||
class HudRendererSP(HudRenderer):
|
||||
@@ -91,14 +89,9 @@ class HudRendererSP(HudRenderer):
|
||||
set_speed_color = COLORS.DARK_GREY
|
||||
if self.is_cruise_set:
|
||||
set_speed_color = COLORS.WHITE
|
||||
assist_state = long_plan_sp.speedLimit.assist.state
|
||||
# Green for active/adapting/capping states, grey for tempPaused when override, else normal
|
||||
if assist_state in (AssistState.active, AssistState.adapting, AssistState.capping):
|
||||
if long_plan_sp.speedLimit.assist.active:
|
||||
set_speed_color = SLA_ACTIVE_COLOR if long_override else rl.Color(0, 0xff, 0, 0xff)
|
||||
max_color = SLA_ACTIVE_COLOR if long_override else rl.Color(0x80, 0xd8, 0xa6, 0xff)
|
||||
elif assist_state == AssistState.tempPaused and long_override:
|
||||
set_speed_color = SLA_ACTIVE_COLOR
|
||||
max_color = SLA_ACTIVE_COLOR
|
||||
else:
|
||||
if ui_state.status == UIStatus.ENGAGED:
|
||||
max_color = COLORS.ENGAGED
|
||||
|
||||
@@ -7,7 +7,6 @@ See the LICENSE.md file in the root directory for more details.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
import time
|
||||
import pyray as rl
|
||||
|
||||
from cereal import custom
|
||||
@@ -29,7 +28,6 @@ SET_SPEED_NA = 255
|
||||
KM_TO_MILE = 0.621371
|
||||
|
||||
AssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState
|
||||
AssistDisableReason = custom.LongitudinalPlanSP.SpeedLimit.AssistDisableReason
|
||||
SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimit.Source
|
||||
|
||||
|
||||
@@ -42,7 +40,6 @@ class Colors:
|
||||
DARK_GREY = rl.Color(77, 77, 77, 255)
|
||||
SUB_BG = rl.Color(0, 0, 0, 180)
|
||||
MUTCD_LINES = rl.Color(255, 255, 255, 100)
|
||||
AMBER = rl.Color(255, 176, 0, 255)
|
||||
|
||||
|
||||
class IconSide(StrEnum):
|
||||
@@ -113,11 +110,6 @@ class SpeedLimitRenderer(Widget, SpeedLimitAlertRenderer):
|
||||
self.speed_limit_ahead_valid = False
|
||||
self.speed_limit_ahead_frame = 0
|
||||
|
||||
self.cap_delta = 0.0
|
||||
self.target_cap = 0.0
|
||||
self.disable_reason = AssistDisableReason.none
|
||||
self.disable_reason_timestamp = 0.0
|
||||
|
||||
self.is_cruise_set: bool = False
|
||||
self.is_cruise_available: bool = True
|
||||
self.set_speed: float = SET_SPEED_NA
|
||||
@@ -153,10 +145,6 @@ class SpeedLimitRenderer(Widget, SpeedLimitAlertRenderer):
|
||||
self.speed_limit_final_last = resolver.speedLimitFinalLast * self.speed_conv
|
||||
self.speed_limit_source = resolver.source
|
||||
self.speed_limit_assist_state = assist.state
|
||||
self.cap_delta = assist.capDelta
|
||||
self.target_cap = assist.targetCap * self.speed_conv
|
||||
self.disable_reason = assist.disableReason
|
||||
self.disable_reason_timestamp = time.monotonic()
|
||||
|
||||
if sm.updated["liveMapDataSP"]:
|
||||
lmd = sm["liveMapDataSP"]
|
||||
@@ -206,15 +194,6 @@ class SpeedLimitRenderer(Widget, SpeedLimitAlertRenderer):
|
||||
self._draw_sign_main(sign_rect, alpha)
|
||||
if self.speed_limit_assist_state == AssistState.preActive:
|
||||
self._draw_pre_active_arrow(sign_rect)
|
||||
elif self.speed_limit_assist_state == AssistState.tempPaused:
|
||||
self._draw_temp_paused_icon(sign_rect)
|
||||
elif self.speed_limit_assist_state == AssistState.capping:
|
||||
self._draw_cap_badge(sign_rect)
|
||||
# Also draw ahead info if valid and different from cap (mutual exclusion fix)
|
||||
if self.speed_limit_ahead_valid and round(self.speed_limit_ahead) != round(self.target_cap):
|
||||
ahead_info_y = sign_rect.y + sign_rect.height + 10 + 160 + 10
|
||||
ahead_rect = rl.Rectangle(sign_rect.x, ahead_info_y, sign_rect.width, 100)
|
||||
self._draw_ahead_info(ahead_rect)
|
||||
else:
|
||||
self._draw_ahead_info(sign_rect)
|
||||
|
||||
@@ -250,38 +229,6 @@ class SpeedLimitRenderer(Widget, SpeedLimitAlertRenderer):
|
||||
color = rl.Color(255, 255, 255, int(icon_alpha))
|
||||
rl.draw_texture_ex(txt_icon, rl.Vector2(arrow_x, arrow_y), 0.0, 1.0, color)
|
||||
|
||||
def _draw_temp_paused_icon(self, sign_rect):
|
||||
"""Draw greyed preActive icon when tempPaused."""
|
||||
# Reuse preActive icon with grey alpha
|
||||
icon_alpha = 128 # 50% opacity for paused state
|
||||
txt_icon = self.arrow_blank # Use blank/greyed version
|
||||
sign_margin = 12
|
||||
arrow_spacing = int(sign_margin * 1.4)
|
||||
arrow_x = sign_rect.x + sign_rect.width + arrow_spacing
|
||||
arrow_y = sign_rect.y + (sign_rect.height - txt_icon.height) / 2
|
||||
color = rl.Color(145, 155, 149, icon_alpha) # GREY color with alpha
|
||||
rl.draw_texture_ex(txt_icon, rl.Vector2(arrow_x, arrow_y), 0.0, 1.0, color)
|
||||
|
||||
def _draw_cap_badge(self, sign_rect):
|
||||
"""Draw CAP info panel below speed limit sign during capping."""
|
||||
rect = rl.Rectangle(sign_rect.x + (sign_rect.width - 170) / 2, sign_rect.y + sign_rect.height + 10, 170, 160)
|
||||
rl.draw_rectangle_rounded(rect, 0.35, 10, Colors.SUB_BG)
|
||||
rl.draw_rectangle_rounded_lines_ex(rect, 0.35, 10, 3, Colors.MUTCD_LINES)
|
||||
|
||||
mid_x = rect.x + rect.width / 2
|
||||
|
||||
label_color = Colors.AMBER if self.cap_delta > 0.5 else Colors.GREY
|
||||
self._draw_text_centered(self.font_demi, "CAP", 40, rl.Vector2(mid_x, rect.y + 28), label_color)
|
||||
|
||||
cap_speed = round(self.target_cap)
|
||||
self._draw_text_centered(self.font_bold, str(cap_speed), 70, rl.Vector2(mid_x, rect.y + 82), Colors.WHITE)
|
||||
|
||||
if self.cap_delta > 0.5:
|
||||
delta_display = round(self.cap_delta * self.speed_conv)
|
||||
delta_unit = 'km/h' if ui_state.is_metric else 'mph'
|
||||
delta_text = f'-{delta_display} {delta_unit}'
|
||||
self._draw_text_centered(self.font_norm, delta_text, 36, rl.Vector2(mid_x, rect.y + 134), Colors.GREY)
|
||||
|
||||
def _render_vienna(self, rect, val, sub, color, has_limit, alpha=1.0):
|
||||
center = rl.Vector2(rect.x + rect.width / 2, rect.y + rect.height / 2)
|
||||
radius = (rect.width + 18) / 2
|
||||
|
||||
@@ -11,6 +11,7 @@ from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import OnroadBrightness
|
||||
from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.sunnypilot.widgets.screen_saver import ScreenSaverSP
|
||||
|
||||
OpenpilotState = log.SelfdriveState.OpenpilotState
|
||||
MADSState = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState
|
||||
@@ -34,6 +35,8 @@ class UIStateSP:
|
||||
]
|
||||
|
||||
self.sunnylink_state = SunnylinkState()
|
||||
self.screensaver = ScreenSaverSP()
|
||||
|
||||
self.update_params()
|
||||
|
||||
self.onroad_brightness_timer: int = 0
|
||||
@@ -146,15 +149,27 @@ class UIStateSP:
|
||||
self.true_v_ego_ui = self.params.get_bool("TrueVEgoUI")
|
||||
self.turn_signals = self.params.get_bool("ShowTurnSignals")
|
||||
self.boot_offroad_mode = self.params.get("DeviceBootMode", return_default=True)
|
||||
self.always_offroad = self.params.get_bool("OffroadMode")
|
||||
self.screensaver_enabled = self.params.get_bool("ScreenSaverEnabled")
|
||||
|
||||
|
||||
class DeviceSP:
|
||||
@staticmethod
|
||||
def _set_awake(on: bool, _ui_state):
|
||||
def _set_awake(_device, on: bool, _ui_state):
|
||||
if _ui_state.boot_offroad_mode == 1 and not on:
|
||||
_ui_state.params.put_bool("OffroadMode", True)
|
||||
|
||||
if not on and _ui_state.screensaver_enabled:
|
||||
if _ui_state.screensaver.was_dismissed:
|
||||
_ui_state.screensaver.deinit()
|
||||
gui_app.pop_widget()
|
||||
return True
|
||||
else:
|
||||
_ui_state.screensaver.initialize(dismiss_callback=lambda: _device._set_awake(False))
|
||||
gui_app.push_widget(_ui_state.screensaver)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def set_onroad_brightness(_ui_state, awake: bool, cur_brightness: float) -> float:
|
||||
if not awake or not _ui_state.started:
|
||||
|
||||
@@ -299,11 +299,11 @@ class Device(DeviceSP):
|
||||
|
||||
def _set_awake(self, on: bool):
|
||||
if on != self._awake:
|
||||
DeviceSP._set_awake(on, ui_state)
|
||||
self._awake = on
|
||||
cloudlog.debug(f"setting display power {int(on)}")
|
||||
HARDWARE.set_display_power(on)
|
||||
gui_app.set_should_render(on)
|
||||
if DeviceSP._set_awake(self, on, ui_state):
|
||||
self._awake = on
|
||||
cloudlog.debug(f"setting display power {int(on)}")
|
||||
HARDWARE.set_display_power(on)
|
||||
gui_app.set_should_render(on)
|
||||
|
||||
|
||||
# Global instance
|
||||
|
||||
@@ -33,7 +33,6 @@ class ModularAssistiveDrivingSystem:
|
||||
self.enabled = False
|
||||
self.active = False
|
||||
self.available = False
|
||||
self.lateral_mismatch_counter = 0
|
||||
self.allow_always = False
|
||||
self.no_main_cruise = False
|
||||
self.selfdrive = selfdrive
|
||||
@@ -105,17 +104,6 @@ class ModularAssistiveDrivingSystem:
|
||||
self.events.remove(old_event)
|
||||
self.events_sp.add(new_event)
|
||||
|
||||
def data_sample(self):
|
||||
# When the safety and selfdrived do not agree on controls_allowed_lateral
|
||||
# we want to disengage sunnypilot. However the status from the panda goes through
|
||||
# another socket other than the CAN messages and one can arrive earlier than the other.
|
||||
# Therefore we allow a mismatch for two samples, then we trigger the disengagement.
|
||||
if not self.active or self.selfdrive.enabled:
|
||||
self.lateral_mismatch_counter = 0
|
||||
elif any(not ps.controlsAllowedLateral for ps in self.selfdrive.sm['pandaStates']
|
||||
if ps.safetyModel not in IGNORED_SAFETY_MODES):
|
||||
self.lateral_mismatch_counter += 1
|
||||
|
||||
def update_events(self, CS: structs.CarState):
|
||||
if not self.selfdrive.enabled and self.enabled:
|
||||
if CS.standstill:
|
||||
@@ -198,9 +186,6 @@ class ModularAssistiveDrivingSystem:
|
||||
if self.state_machine.state == State.paused:
|
||||
self.events_sp.add(EventNameSP.silentLkasEnable)
|
||||
|
||||
if self.lateral_mismatch_counter >= 200:
|
||||
self.events_sp.add(EventNameSP.controlsMismatchLateral)
|
||||
|
||||
self.events.remove(EventName.pcmDisable)
|
||||
self.events.remove(EventName.buttonCancel)
|
||||
self.events.remove(EventName.pedalPressed)
|
||||
@@ -210,8 +195,6 @@ class ModularAssistiveDrivingSystem:
|
||||
if not self.enabled_toggle:
|
||||
return
|
||||
|
||||
self.data_sample()
|
||||
|
||||
self.update_events(CS)
|
||||
|
||||
if not self.CP.passive and self.selfdrive.initialized:
|
||||
|
||||
@@ -13,7 +13,7 @@ if PC:
|
||||
model_dir = Dir("models").abspath
|
||||
cmd = f'python3 {Dir("#sunnypilot/modeld_v2").abspath}/install_models_pc.py {model_dir}'
|
||||
|
||||
for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_on_policy', 'driving_policy']:
|
||||
for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_policy']:
|
||||
if File(f"models/{model_name}.onnx").exists():
|
||||
inputs.append(File(f"models/{model_name}.onnx"))
|
||||
inputs.append(File(f"models/{model_name}_tinygrad.pkl"))
|
||||
@@ -42,7 +42,7 @@ def tg_compile(flags, model_name):
|
||||
)
|
||||
|
||||
# Compile models
|
||||
for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_on_policy', 'driving_policy']:
|
||||
for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_policy']:
|
||||
if File(f"models/{model_name}.onnx").exists():
|
||||
tg_compile(tg_flags, model_name)
|
||||
|
||||
|
||||
@@ -8,14 +8,16 @@ from openpilot.sunnypilot import get_file_hash
|
||||
DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "common", "model.h")
|
||||
MODEL_HASH_PATH = os.path.join(BASEDIR, "sunnypilot", "models", "tests", "model_hash")
|
||||
VISION_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_vision.onnx")
|
||||
POLICY_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_policy.onnx")
|
||||
OFF_POLICY_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_off_policy.onnx")
|
||||
ON_POLICY_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_on_policy.onnx")
|
||||
|
||||
|
||||
def update_model_hash():
|
||||
vision_hash = get_file_hash(VISION_ONNX_PATH)
|
||||
policy_hash = get_file_hash(POLICY_ONNX_PATH)
|
||||
off_policy_hash = get_file_hash(OFF_POLICY_ONNX_PATH)
|
||||
on_policy_hash = get_file_hash(ON_POLICY_ONNX_PATH)
|
||||
|
||||
combined_hash = hashlib.sha256((vision_hash + policy_hash).encode()).hexdigest()
|
||||
combined_hash = hashlib.sha256((vision_hash + off_policy_hash + on_policy_hash).encode()).hexdigest()
|
||||
|
||||
with open(MODEL_HASH_PATH, "w") as f:
|
||||
f.write(combined_hash)
|
||||
|
||||
@@ -1 +1 @@
|
||||
5d4d21f1899de21137f69d74a4602c44cc5a6b04cf4e4aa9d0ec9206f8c30350
|
||||
793b5d480edb5a30eed3d0d3bdb43259522978670f6bc3dea7a4d661261d3c48
|
||||
|
||||
@@ -6,16 +6,17 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
from openpilot.sunnypilot import get_file_hash
|
||||
from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, VISION_ONNX_PATH, POLICY_ONNX_PATH
|
||||
from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, VISION_ONNX_PATH, OFF_POLICY_ONNX_PATH, ON_POLICY_ONNX_PATH
|
||||
import hashlib
|
||||
|
||||
|
||||
class TestDefaultModel:
|
||||
def test_compare_onnx_hashes(self):
|
||||
vision_hash = get_file_hash(VISION_ONNX_PATH)
|
||||
policy_hash = get_file_hash(POLICY_ONNX_PATH)
|
||||
off_policy_hash = get_file_hash(OFF_POLICY_ONNX_PATH)
|
||||
on_policy_hash = get_file_hash(ON_POLICY_ONNX_PATH)
|
||||
|
||||
combined_hash = hashlib.sha256((vision_hash + policy_hash).encode()).hexdigest()
|
||||
combined_hash = hashlib.sha256((vision_hash + off_policy_hash + on_policy_hash).encode()).hexdigest()
|
||||
|
||||
with open(MODEL_HASH_PATH) as f:
|
||||
current_hash = f.read().strip()
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -14,8 +14,11 @@ from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
LAT_PLAN_MIN_IDX = 5
|
||||
LATERAL_LAG_MOD = 0.0 # seconds, modifies how far in the future we look ahead for the lateral plan
|
||||
|
||||
KP = 1.0
|
||||
KI = 0.3
|
||||
# from selfdrive/controls/lib/latcontrol_torque.py
|
||||
KP = 0.8
|
||||
KI = 0.15
|
||||
INTERP_SPEEDS = [1, 1.5, 2.0, 3.0, 5, 7.5, 10, 15, 30]
|
||||
KP_INTERP = [250, 120, 65, 30, 11.5, 5.5, 3.5, 2.0, KP]
|
||||
|
||||
|
||||
def get_predicted_lateral_jerk(lat_accels, t_diffs):
|
||||
@@ -58,10 +61,9 @@ class LatControlTorqueExtBase:
|
||||
self.lookahead_lateral_jerk: float = 0.0
|
||||
|
||||
self.torque_from_lateral_accel_in_torque_space = CI.torque_from_lateral_accel_in_torque_space()
|
||||
self.torque_params = lac_torque.torque_params
|
||||
|
||||
self._ff = 0.0
|
||||
self._pid = PIDController(KP, KI)
|
||||
self._pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI)
|
||||
self._pid_log = None
|
||||
self._setpoint = 0.0
|
||||
self._measurement = 0.0
|
||||
|
||||
@@ -60,7 +60,7 @@ class LongitudinalPlannerSP:
|
||||
# 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, CS.gasPressed)
|
||||
self.resolver.speed_limit_final_last, has_speed_limit, self.resolver.distance, self.events_sp)
|
||||
|
||||
targets = {
|
||||
LongitudinalPlanSource.cruise: (v_cruise, a_ego),
|
||||
@@ -132,9 +132,6 @@ class LongitudinalPlannerSP:
|
||||
assist.active = self.sla.is_active
|
||||
assist.vTarget = float(self.sla.output_v_target)
|
||||
assist.aTarget = float(self.sla.output_a_target)
|
||||
assist.capDelta = float(self.sla.cap_delta)
|
||||
assist.targetCap = float(self.sla._target_cap)
|
||||
assist.disableReason = self.sla.disable_reason
|
||||
|
||||
# E2E Alerts
|
||||
e2eAlerts = longitudinalPlanSP.e2eAlerts
|
||||
|
||||
@@ -75,14 +75,14 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase):
|
||||
|
||||
def update_feedforward_torque_space(self, CS):
|
||||
torque_from_setpoint = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._setpoint, self._roll_compensation, CS.vEgo, CS.aEgo),
|
||||
self.torque_params, gravity_adjusted=False)
|
||||
self.lac_torque.torque_params, gravity_adjusted=False)
|
||||
torque_from_measurement = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._measurement, self._roll_compensation, CS.vEgo, CS.aEgo),
|
||||
self.torque_params, gravity_adjusted=False)
|
||||
self.lac_torque.torque_params, gravity_adjusted=False)
|
||||
self._pid_log.error = float(torque_from_setpoint - torque_from_measurement)
|
||||
self._ff = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._gravity_adjusted_lateral_accel, self._roll_compensation,
|
||||
CS.vEgo, CS.aEgo), self.torque_params, gravity_adjusted=True)
|
||||
CS.vEgo, CS.aEgo), self.lac_torque.torque_params, gravity_adjusted=True)
|
||||
self._ff += get_friction_in_torque_space(self._desired_lateral_accel - self._actual_lateral_accel, self._lateral_accel_deadzone,
|
||||
FRICTION_THRESHOLD, self.torque_params)
|
||||
FRICTION_THRESHOLD, self.lac_torque.torque_params)
|
||||
|
||||
def update_output_torque(self, CS):
|
||||
freeze_integrator = self._steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
|
||||
@@ -159,6 +159,6 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase):
|
||||
|
||||
# apply friction override for cars with low NN friction response
|
||||
if self.model.friction_override:
|
||||
self._pid_log.error += get_friction(friction_input, self._lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
|
||||
self._pid_log.error += get_friction(friction_input, self._lateral_accel_deadzone, FRICTION_THRESHOLD, self.lac_torque.torque_params)
|
||||
|
||||
self.update_output_torque(CS)
|
||||
|
||||
@@ -17,8 +17,3 @@ CONFIRM_SPEED_THRESHOLD = {
|
||||
True: 80, # km/h
|
||||
False: 50, # mph
|
||||
}
|
||||
|
||||
MIN_CAP_FLOOR_MAX = {
|
||||
True: 64, # km/h
|
||||
False: 40, # mph
|
||||
}
|
||||
|
||||
@@ -27,8 +27,3 @@ class Mode(IntEnumBase):
|
||||
information = 1
|
||||
warning = 2
|
||||
assist = 3
|
||||
|
||||
|
||||
class UpshiftAccept(IntEnumBase):
|
||||
NEVER_RAISE = 0
|
||||
ACCEL_PEDAL = 1
|
||||
|
||||
@@ -9,7 +9,6 @@ from cereal import custom, car
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode as SpeedLimitMode
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import MIN_CAP_FLOOR_MAX
|
||||
|
||||
|
||||
def compare_cluster_target(v_cruise_cluster: float, target_set_speed: float, is_metric: bool) -> tuple[bool, bool]:
|
||||
@@ -43,9 +42,3 @@ def set_speed_limit_assist_availability(CP: car.CarParams, CP_SP: custom.CarPara
|
||||
params.put("SpeedLimitMode", int(SpeedLimitMode.warning))
|
||||
|
||||
return allowed
|
||||
|
||||
|
||||
def get_min_cap_floor(params: Params, is_metric: bool) -> float:
|
||||
value = params.get("SpeedLimitMinCapFloor", return_default=True)
|
||||
value = max(0, min(value, MIN_CAP_FLOOR_MAX[is_metric]))
|
||||
return value * (CV.KPH_TO_MS if is_metric else CV.MPH_TO_MS)
|
||||
|
||||
@@ -15,20 +15,16 @@ from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
|
||||
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import PCM_LONG_REQUIRED_MAX_SET_SPEED, CONFIRM_SPEED_THRESHOLD
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode, UpshiftAccept
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.helpers import compare_cluster_target, set_speed_limit_assist_availability, \
|
||||
get_min_cap_floor
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.helpers import compare_cluster_target, set_speed_limit_assist_availability
|
||||
|
||||
ButtonType = car.CarState.ButtonEvent.Type
|
||||
EventNameSP = custom.OnroadEventSP.EventName
|
||||
SpeedLimitAssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState
|
||||
AssistDisableReason = custom.LongitudinalPlanSP.SpeedLimit.AssistDisableReason
|
||||
SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimit.Source
|
||||
|
||||
ACTIVE_STATES = (SpeedLimitAssistState.active, SpeedLimitAssistState.adapting)
|
||||
ENABLED_STATES = (SpeedLimitAssistState.preActive, SpeedLimitAssistState.pending, *ACTIVE_STATES)
|
||||
CAP_ACTIVE_STATES = (SpeedLimitAssistState.capping,)
|
||||
CAP_ENABLED_STATES = CAP_ACTIVE_STATES # cap mode has no partially-engaged state
|
||||
|
||||
DISABLED_GUARD_PERIOD = 0.5 # secs.
|
||||
# secs. Time to wait after activation before considering temp deactivation signal.
|
||||
@@ -37,10 +33,6 @@ PRE_ACTIVE_GUARD_PERIOD = {
|
||||
False: 5,
|
||||
}
|
||||
SPEED_LIMIT_CHANGED_HOLD_PERIOD = 1 # secs. Time to wait after speed limit change before switching to preActive.
|
||||
CAP_RAISE_HOLD_PERIOD = 0.2 # secs. Time to confirm limit raise before upshifting.
|
||||
CAP_SUSPEND_GUARD_PERIOD = 1.0 # secs. Time to hold cap disabled after long_override release.
|
||||
USER_PAUSE_TIMEOUT_TICKS = 6000 # 5 min / DT_MDL (0.05 s) = 6000 ticks
|
||||
RESUME_CLUSTER_DELTA_THRESHOLD = 1 # integer display-unit delta (kph or mph)
|
||||
|
||||
LIMIT_MIN_ACC = -1.5 # m/s^2 Maximum deceleration allowed for limit controllers to provide.
|
||||
LIMIT_MAX_ACC = 1.0 # m/s^2 Maximum acceleration allowed for limit controllers to provide while active.
|
||||
@@ -59,7 +51,6 @@ class SpeedLimitAssist:
|
||||
v_ego: float
|
||||
a_ego: float
|
||||
v_offset: float
|
||||
cap_delta: float
|
||||
|
||||
def __init__(self, CP: car.CarParams, CP_SP: custom.CarParamsSP):
|
||||
self.params = Params()
|
||||
@@ -73,12 +64,10 @@ class SpeedLimitAssist:
|
||||
self.enabled = self.params.get("SpeedLimitMode", return_default=True) == Mode.assist
|
||||
self.long_enabled = False
|
||||
self.long_enabled_prev = False
|
||||
self.long_override = False
|
||||
self.is_enabled = False
|
||||
self.is_active = False
|
||||
self.output_v_target = V_CRUISE_UNSET
|
||||
self.output_a_target = 0.
|
||||
self.cap_delta = 0.0
|
||||
self.v_ego = 0.
|
||||
self.a_ego = 0.
|
||||
self.v_offset = 0.
|
||||
@@ -103,28 +92,8 @@ class SpeedLimitAssist:
|
||||
self._minus_hold = 0.
|
||||
self._last_carstate_ts = 0.
|
||||
|
||||
self._cap_change_timer = 0
|
||||
self._cap_suspended_timer = 0
|
||||
self._cap_below_floor = False
|
||||
self._target_cap = 0.0
|
||||
self._cap_upshift_pressed = False
|
||||
self._cap_upshift_release_timer = 0
|
||||
self._cap_audio_cue_fired = False
|
||||
self._cap_raise_accepted = False
|
||||
self._accel_pressed = False
|
||||
self._was_cap_suspended = False
|
||||
self._override_active_last = False
|
||||
self._min_cap_floor = get_min_cap_floor(self.params, self.is_metric)
|
||||
self._cap_upshift_accept = self.params.get("SpeedLimitUpshiftAccept", return_default=True)
|
||||
self._cap_audio_cue_enabled = bool(self.params.get("SpeedLimitCapAudioCue", return_default=True))
|
||||
|
||||
self._user_paused: bool = False
|
||||
self._user_paused_timer: int = 0
|
||||
self._disable_reason = AssistDisableReason.none
|
||||
self._speed_limit_final_last_at_pause = 0.
|
||||
self.tempPaused_count = 0 # diagnostic counter for tests
|
||||
|
||||
# TODO-SP: SLA's own output_a_target for planner
|
||||
# Solution functions mapped to respective states
|
||||
self.acceleration_solutions = {
|
||||
SpeedLimitAssistState.disabled: self.get_current_acceleration_as_target,
|
||||
SpeedLimitAssistState.inactive: self.get_current_acceleration_as_target,
|
||||
@@ -132,26 +101,8 @@ class SpeedLimitAssist:
|
||||
SpeedLimitAssistState.pending: self.get_current_acceleration_as_target,
|
||||
SpeedLimitAssistState.adapting: self.get_adapting_state_target_acceleration,
|
||||
SpeedLimitAssistState.active: self.get_active_state_target_acceleration,
|
||||
SpeedLimitAssistState.capping: self.get_current_acceleration_as_target,
|
||||
SpeedLimitAssistState.tempPaused: self.get_current_acceleration_as_target,
|
||||
}
|
||||
|
||||
@property
|
||||
def disable_reason(self):
|
||||
return self._disable_reason
|
||||
|
||||
@property
|
||||
def _gates_pass(self) -> bool:
|
||||
return self.long_enabled and self.enabled
|
||||
|
||||
@property
|
||||
def _cap_gates_pass(self) -> bool:
|
||||
return self._gates_pass and not self.long_override and self._cap_suspended_timer <= 0
|
||||
|
||||
@property
|
||||
def _cap_entry_ready(self) -> bool:
|
||||
return self._has_speed_limit and not self._cap_below_floor
|
||||
|
||||
@property
|
||||
def speed_limit_changed(self) -> bool:
|
||||
return self._has_speed_limit and bool(self._speed_limit != self.speed_limit_prev)
|
||||
@@ -175,13 +126,13 @@ class SpeedLimitAssist:
|
||||
events_sp.add(EventNameSP.speedLimitActive)
|
||||
|
||||
def get_v_target_from_control(self) -> float:
|
||||
if self.pcm_op_long:
|
||||
if self.state == SpeedLimitAssistState.capping:
|
||||
return min(self.v_cruise_cluster, self._target_cap)
|
||||
else:
|
||||
if self._has_speed_limit and self.is_active:
|
||||
if self._has_speed_limit:
|
||||
if self.pcm_op_long and self.is_enabled:
|
||||
return self._speed_limit_final_last
|
||||
if not self.pcm_op_long and self.is_active:
|
||||
return self._speed_limit_final_last
|
||||
|
||||
# Fallback
|
||||
return V_CRUISE_UNSET
|
||||
|
||||
# TODO-SP: SLA's own output_a_target for planner
|
||||
@@ -193,9 +144,6 @@ class SpeedLimitAssist:
|
||||
self.is_metric = self.params.get_bool("IsMetric")
|
||||
set_speed_limit_assist_availability(self.CP, self.CP_SP, self.params)
|
||||
self.enabled = self.params.get("SpeedLimitMode", return_default=True) == Mode.assist
|
||||
self._min_cap_floor = get_min_cap_floor(self.params, self.is_metric)
|
||||
self._cap_upshift_accept = self.params.get("SpeedLimitUpshiftAccept", return_default=True)
|
||||
self._cap_audio_cue_enabled = bool(self.params.get("SpeedLimitCapAudioCue", return_default=True))
|
||||
|
||||
def update_car_state(self, CS: car.CarState) -> None:
|
||||
now = time.monotonic()
|
||||
@@ -239,12 +187,7 @@ class SpeedLimitAssist:
|
||||
cst_high
|
||||
pcm_long_required_max_set_speed_conv = round(pcm_long_required_max * speed_conv)
|
||||
|
||||
if self.pcm_op_long and self.state not in CAP_ACTIVE_STATES:
|
||||
self.target_set_speed_conv = pcm_long_required_max_set_speed_conv
|
||||
elif not self.pcm_op_long:
|
||||
self.target_set_speed_conv = self.speed_limit_final_last_conv
|
||||
else:
|
||||
self.target_set_speed_conv = self.v_cruise_cluster_conv
|
||||
self.target_set_speed_conv = pcm_long_required_max_set_speed_conv if self.pcm_op_long else self.speed_limit_final_last_conv
|
||||
|
||||
@property
|
||||
def apply_confirm_speed_threshold(self) -> bool:
|
||||
@@ -289,142 +232,74 @@ class SpeedLimitAssist:
|
||||
|
||||
return self._get_button_release(req_plus, req_minus)
|
||||
|
||||
def _cap_limit_change_held(self) -> bool:
|
||||
"""Return True when limit-change hold period has elapsed."""
|
||||
return self._cap_change_timer >= int(SPEED_LIMIT_CHANGED_HOLD_PERIOD / DT_MDL)
|
||||
def update_state_machine_pcm_op_long(self):
|
||||
self.long_engaged_timer = max(0, self.long_engaged_timer - 1)
|
||||
self.pre_active_timer = max(0, self.pre_active_timer - 1)
|
||||
|
||||
def _cap_upshift_release_edge(self) -> bool:
|
||||
"""Return True when limit-raise hold period elapsed after gas release edge."""
|
||||
if self._cap_upshift_pressed and not self._accel_pressed:
|
||||
self._cap_upshift_release_timer = int(CAP_RAISE_HOLD_PERIOD / DT_MDL)
|
||||
|
||||
self._cap_upshift_pressed = self._accel_pressed
|
||||
|
||||
if self._cap_upshift_release_timer > 0:
|
||||
self._cap_upshift_release_timer = max(0, self._cap_upshift_release_timer - 1)
|
||||
if self._cap_upshift_release_timer <= 0:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _go_disabled(self, reason: 'AssistDisableReason') -> None:
|
||||
"""Transition to disabled state with given reason."""
|
||||
self._cap_raise_accepted = False
|
||||
self.state = SpeedLimitAssistState.disabled
|
||||
self._disable_reason = reason
|
||||
|
||||
def _should_exit_temp_pause(self) -> bool:
|
||||
"""Check if conditions warrant exiting temp pause state."""
|
||||
limit_changed = self._speed_limit_final_last != self._speed_limit_final_last_at_pause
|
||||
timer_expired = self._user_paused_timer <= 0
|
||||
cluster_realigned = abs(self.v_cruise_cluster_conv - self.speed_limit_final_last_conv) <= RESUME_CLUSTER_DELTA_THRESHOLD
|
||||
return limit_changed or timer_expired or cluster_realigned
|
||||
|
||||
def update_state_machine_cap(self, events_sp: EventsSP) -> tuple[bool, bool]:
|
||||
"""Cap mode FSM for pcm_op_long cars. Returns (enabled, active)."""
|
||||
# Bookkeeping: timers, override flags (unchanged)
|
||||
self._cap_change_timer = min(self._cap_change_timer + 1,
|
||||
int((SPEED_LIMIT_CHANGED_HOLD_PERIOD + 1) / DT_MDL))
|
||||
self._cap_upshift_release_timer = max(0, self._cap_upshift_release_timer - 1)
|
||||
self._user_paused_timer = max(0, self._user_paused_timer - 1)
|
||||
|
||||
if self._override_active_last and not self.long_override and self._was_cap_suspended:
|
||||
self._cap_suspended_timer = int(CAP_SUSPEND_GUARD_PERIOD / DT_MDL)
|
||||
elif not self.long_override:
|
||||
self._cap_suspended_timer = max(0, self._cap_suspended_timer - 1)
|
||||
|
||||
self._override_active_last = self.long_override
|
||||
|
||||
self._cap_below_floor = self._has_speed_limit and self._speed_limit_final_last < self._min_cap_floor
|
||||
|
||||
# Gate checks FIRST: apply to all non-disabled states (including tempPaused)
|
||||
# ACTIVE, ADAPTING, PENDING, PRE_ACTIVE, INACTIVE
|
||||
if self.state != SpeedLimitAssistState.disabled:
|
||||
if not self._gates_pass:
|
||||
self._go_disabled(AssistDisableReason.gateDisabled)
|
||||
self._was_cap_suspended = False
|
||||
self._cap_suspended_timer = 0
|
||||
elif self.long_override:
|
||||
self._go_disabled(AssistDisableReason.longOverride)
|
||||
self._was_cap_suspended = True
|
||||
if not self.long_enabled or not self.enabled:
|
||||
self.state = SpeedLimitAssistState.disabled
|
||||
|
||||
# Sub-state dispatch (only if gates passed)
|
||||
elif self.state == SpeedLimitAssistState.tempPaused:
|
||||
# Exit conditions: speed limit changed, timer expired, or cluster delta returns
|
||||
if self._should_exit_temp_pause():
|
||||
self._user_paused = False
|
||||
self._user_paused_timer = 0
|
||||
self._go_disabled(AssistDisableReason.autoResume)
|
||||
else:
|
||||
# ACTIVE
|
||||
if self.state == SpeedLimitAssistState.active:
|
||||
if self.v_cruise_cluster_changed:
|
||||
self.state = SpeedLimitAssistState.inactive
|
||||
elif self.speed_limit_changed and self.apply_confirm_speed_threshold:
|
||||
self.state = SpeedLimitAssistState.preActive
|
||||
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
|
||||
elif self._has_speed_limit and self.v_offset < LIMIT_SPEED_OFFSET_TH:
|
||||
self.state = SpeedLimitAssistState.adapting
|
||||
|
||||
elif self.state == SpeedLimitAssistState.capping:
|
||||
# Cluster delta entry: user nudged cruise control away from limit
|
||||
if self.v_cruise_cluster_changed:
|
||||
self._user_paused = True
|
||||
self._user_paused_timer = USER_PAUSE_TIMEOUT_TICKS
|
||||
self._speed_limit_final_last_at_pause = self._speed_limit_final_last
|
||||
self.state = SpeedLimitAssistState.tempPaused
|
||||
self._disable_reason = AssistDisableReason.userTempPause
|
||||
elif not self._has_speed_limit:
|
||||
self._cap_raise_accepted = False
|
||||
self.state = SpeedLimitAssistState.pending
|
||||
self._disable_reason = AssistDisableReason.mapGap
|
||||
elif self._cap_below_floor:
|
||||
self._cap_raise_accepted = False
|
||||
self.state = SpeedLimitAssistState.pending
|
||||
self._disable_reason = AssistDisableReason.belowFloor
|
||||
elif self._speed_limit_final_last != self._target_cap and self._cap_limit_change_held():
|
||||
old_cap = self._target_cap
|
||||
self._target_cap = self._speed_limit_final_last
|
||||
self._cap_change_timer = 0
|
||||
self._cap_raise_accepted = False
|
||||
if self._target_cap > old_cap:
|
||||
if self._cap_upshift_accept == UpshiftAccept.NEVER_RAISE:
|
||||
self._target_cap = old_cap
|
||||
elif self._cap_upshift_accept == UpshiftAccept.ACCEL_PEDAL:
|
||||
if not self._accel_pressed:
|
||||
self._cap_raise_accepted = True
|
||||
else:
|
||||
self._target_cap = old_cap
|
||||
else:
|
||||
self._cap_upshift_release_edge()
|
||||
# ADAPTING
|
||||
elif self.state == SpeedLimitAssistState.adapting:
|
||||
if self.v_cruise_cluster_changed:
|
||||
self.state = SpeedLimitAssistState.inactive
|
||||
elif self.speed_limit_changed and self.apply_confirm_speed_threshold:
|
||||
self.state = SpeedLimitAssistState.preActive
|
||||
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
|
||||
elif self.v_offset >= LIMIT_SPEED_OFFSET_TH:
|
||||
self.state = SpeedLimitAssistState.active
|
||||
|
||||
# PENDING
|
||||
elif self.state == SpeedLimitAssistState.pending:
|
||||
if self.target_set_speed_confirmed:
|
||||
self._update_confirmed_state()
|
||||
elif self.speed_limit_changed:
|
||||
self.state = SpeedLimitAssistState.preActive
|
||||
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
|
||||
|
||||
# PRE_ACTIVE
|
||||
elif self.state == SpeedLimitAssistState.preActive:
|
||||
if self.target_set_speed_confirmed:
|
||||
self._update_confirmed_state()
|
||||
elif self.pre_active_timer <= 0:
|
||||
# Timeout - session ended
|
||||
self.state = SpeedLimitAssistState.inactive
|
||||
|
||||
# INACTIVE
|
||||
elif self.state == SpeedLimitAssistState.inactive:
|
||||
pass
|
||||
|
||||
# DISABLED
|
||||
elif self.state == SpeedLimitAssistState.disabled:
|
||||
if self.long_enabled and self.enabled:
|
||||
# start or reset preActive timer if initially enabled or manual set speed change detected
|
||||
if not self.long_enabled_prev or self.v_cruise_cluster_changed:
|
||||
self.long_engaged_timer = int(DISABLED_GUARD_PERIOD / DT_MDL)
|
||||
|
||||
elif self.long_engaged_timer <= 0:
|
||||
if self.target_set_speed_confirmed:
|
||||
self._update_confirmed_state()
|
||||
elif self._has_speed_limit:
|
||||
self.state = SpeedLimitAssistState.preActive
|
||||
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
|
||||
else:
|
||||
self._cap_upshift_release_edge()
|
||||
else:
|
||||
self._cap_upshift_release_edge()
|
||||
self.state = SpeedLimitAssistState.pending
|
||||
|
||||
elif self.state == SpeedLimitAssistState.pending:
|
||||
if self._cap_entry_ready:
|
||||
if not self._was_cap_suspended:
|
||||
self._target_cap = self._speed_limit_final_last
|
||||
self._cap_change_timer = 0
|
||||
self._cap_audio_cue_fired = False
|
||||
self._cap_raise_accepted = False
|
||||
self._disable_reason = AssistDisableReason.none
|
||||
self.state = SpeedLimitAssistState.capping
|
||||
|
||||
else:
|
||||
# Disabled-entry logic: if gates pass + cap_suspended_timer clear, enter capping/pending
|
||||
if self._cap_gates_pass:
|
||||
if self._cap_entry_ready:
|
||||
if not self._was_cap_suspended:
|
||||
self._target_cap = self._speed_limit_final_last
|
||||
self._cap_change_timer = 0
|
||||
self._cap_audio_cue_fired = False
|
||||
self._disable_reason = AssistDisableReason.none
|
||||
self.state = SpeedLimitAssistState.capping
|
||||
else:
|
||||
self._disable_reason = AssistDisableReason.mapGap if not self._has_speed_limit else AssistDisableReason.belowFloor
|
||||
self.state = SpeedLimitAssistState.pending
|
||||
|
||||
# Audio cue on capping entry
|
||||
if self.state == SpeedLimitAssistState.capping and self._state_prev != SpeedLimitAssistState.capping:
|
||||
# suppress audio cue on override-release re-entry
|
||||
if self._cap_audio_cue_enabled and not self._was_cap_suspended:
|
||||
events_sp.add(EventNameSP.speedLimitCapActive)
|
||||
self._cap_audio_cue_fired = True
|
||||
self._was_cap_suspended = False
|
||||
|
||||
enabled = self.state in CAP_ENABLED_STATES
|
||||
active = self.state in CAP_ACTIVE_STATES
|
||||
enabled = self.state in ENABLED_STATES
|
||||
active = self.state in ACTIVE_STATES
|
||||
|
||||
return enabled, active
|
||||
|
||||
@@ -485,13 +360,13 @@ class SpeedLimitAssist:
|
||||
return enabled, active
|
||||
|
||||
def update_events(self, events_sp: EventsSP) -> None:
|
||||
if not self.pcm_op_long and self.state == SpeedLimitAssistState.preActive:
|
||||
if self.state == SpeedLimitAssistState.preActive:
|
||||
events_sp.add(EventNameSP.speedLimitPreActive)
|
||||
|
||||
if self.state == SpeedLimitAssistState.pending and self._state_prev != SpeedLimitAssistState.pending:
|
||||
events_sp.add(EventNameSP.speedLimitPending)
|
||||
|
||||
if not self.pcm_op_long and self.is_active:
|
||||
if self.is_active:
|
||||
if self._state_prev not in ACTIVE_STATES:
|
||||
self.update_active_event(events_sp)
|
||||
|
||||
@@ -504,9 +379,8 @@ class SpeedLimitAssist:
|
||||
self.update_active_event(events_sp)
|
||||
|
||||
def update(self, long_enabled: bool, long_override: bool, v_ego: float, a_ego: float, v_cruise_cluster: float, speed_limit: float,
|
||||
speed_limit_final_last: float, has_speed_limit: bool, distance: float, events_sp: EventsSP, accel_pressed: bool = False) -> None:
|
||||
speed_limit_final_last: float, has_speed_limit: bool, distance: float, events_sp: EventsSP) -> None:
|
||||
self.long_enabled = long_enabled
|
||||
self.long_override = long_override
|
||||
self.v_ego = v_ego
|
||||
self.a_ego = a_ego
|
||||
|
||||
@@ -514,14 +388,13 @@ class SpeedLimitAssist:
|
||||
self._speed_limit = speed_limit
|
||||
self._speed_limit_final_last = speed_limit_final_last
|
||||
self._distance = distance
|
||||
self._accel_pressed = accel_pressed
|
||||
|
||||
self.update_params()
|
||||
self.update_calculations(v_cruise_cluster)
|
||||
|
||||
self._state_prev = self.state
|
||||
if self.pcm_op_long:
|
||||
self.is_enabled, self.is_active = self.update_state_machine_cap(events_sp)
|
||||
self.is_enabled, self.is_active = self.update_state_machine_pcm_op_long()
|
||||
else:
|
||||
self.is_enabled, self.is_active = self.update_state_machine_non_pcm_long()
|
||||
|
||||
@@ -538,9 +411,4 @@ class SpeedLimitAssist:
|
||||
self.output_v_target = self.get_v_target_from_control()
|
||||
self.output_a_target = self.get_a_target_from_control()
|
||||
|
||||
if self.pcm_op_long and self.state == SpeedLimitAssistState.capping:
|
||||
self.cap_delta = max(0.0, self.v_cruise_cluster - self._target_cap)
|
||||
else:
|
||||
self.cap_delta = 0.0
|
||||
|
||||
self.frame += 1
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from cereal import custom, car
|
||||
from opendbc.car.car_helpers import interfaces
|
||||
from opendbc.car.toyota.values import CAR as TOYOTA
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.helpers import get_min_cap_floor
|
||||
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
||||
|
||||
SpeedLimitAssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState
|
||||
|
||||
|
||||
class CarParamsFactory:
|
||||
@staticmethod
|
||||
def create_car_interface(car_name: str = TOYOTA.TOYOTA_RAV4_TSS2) -> tuple[car.CarParams, custom.CarParamsSP, object]:
|
||||
params = Params()
|
||||
CarInterface = interfaces[car_name]
|
||||
CP = CarInterface.get_non_essential_params(car_name)
|
||||
CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name)
|
||||
CI = CarInterface(CP, CP_SP)
|
||||
CI.CP.openpilotLongitudinalControl = True
|
||||
sunnypilot_interfaces.setup_interfaces(CI, params)
|
||||
return CI.CP, CI.CP_SP, CI
|
||||
|
||||
|
||||
class SpeedLimitAssistScenario:
|
||||
def __init__(self, CP: car.CarParams, CP_SP: custom.CarParamsSP, params: Params = None):
|
||||
if params is None:
|
||||
params = Params()
|
||||
self.params = params
|
||||
self.CP = CP
|
||||
self.CP_SP = CP_SP
|
||||
self.params.put("SpeedLimitMode", int(Mode.assist))
|
||||
self.sla = SpeedLimitAssist(CP, CP_SP)
|
||||
self.sla.update_params()
|
||||
self.events_sp = EventsSP()
|
||||
self.speed_conv = CV.MS_TO_KPH if self.sla.is_metric else CV.MS_TO_MPH
|
||||
|
||||
def set_state(self, state: int) -> "SpeedLimitAssistScenario":
|
||||
self.sla.state = state
|
||||
return self
|
||||
|
||||
def set_speed_limits(self, speed_limit: float, distance: float = 0, speed_limit_final_last: float = 0,
|
||||
speed_limit_prev: float = 0) -> "SpeedLimitAssistScenario":
|
||||
self.sla._speed_limit = speed_limit
|
||||
self.sla._distance = distance
|
||||
self.sla.speed_limit_prev = speed_limit_prev
|
||||
return self
|
||||
|
||||
def set_cruise_speeds(self, v_cruise_cluster: float, v_cruise_cluster_prev: float = None) -> "SpeedLimitAssistScenario":
|
||||
self.sla.v_cruise_cluster = v_cruise_cluster
|
||||
if v_cruise_cluster_prev is None:
|
||||
v_cruise_cluster_prev = v_cruise_cluster
|
||||
self.sla.v_cruise_cluster_prev = v_cruise_cluster_prev
|
||||
self.sla.prev_v_cruise_cluster_conv = round(v_cruise_cluster_prev * self.speed_conv)
|
||||
return self
|
||||
|
||||
def set_engaged(self, op_engaged: bool) -> "SpeedLimitAssistScenario":
|
||||
self.sla.op_engaged = op_engaged
|
||||
return self
|
||||
|
||||
def set_param(self, key: str, value) -> "SpeedLimitAssistScenario":
|
||||
# IntEnum instances carry a .value the Params API does not accept directly
|
||||
if hasattr(value, 'value'):
|
||||
value = value.value
|
||||
|
||||
if isinstance(value, bool):
|
||||
self.params.put_bool(key, value)
|
||||
elif isinstance(value, int):
|
||||
self.params.put(key, value)
|
||||
else:
|
||||
self.params.put(key, str(value) if not isinstance(value, str) else value)
|
||||
|
||||
# Runtime caches these behind PARAMS_UPDATE_PERIOD; force-sync for tests
|
||||
if key == "SpeedLimitMinCapFloor":
|
||||
self.sla._min_cap_floor = get_min_cap_floor(self.sla.params, self.sla.is_metric)
|
||||
elif key == "SpeedLimitUpshiftAccept":
|
||||
self.sla._cap_upshift_accept = self.sla.params.get("SpeedLimitUpshiftAccept", return_default=True)
|
||||
elif key == "SpeedLimitCapAudioCue":
|
||||
self.sla._cap_audio_cue_enabled = bool(self.sla.params.get("SpeedLimitCapAudioCue", return_default=True))
|
||||
elif key == "SpeedLimitMode":
|
||||
self.sla.enabled = self.sla.params.get("SpeedLimitMode", return_default=True) == Mode.assist
|
||||
elif key == "IsMetric":
|
||||
self.sla.is_metric = self.sla.params.get_bool("IsMetric")
|
||||
return self
|
||||
|
||||
def clear_events(self) -> "SpeedLimitAssistScenario":
|
||||
self.events_sp.clear()
|
||||
return self
|
||||
|
||||
def reset_state(self) -> "SpeedLimitAssistScenario":
|
||||
self.sla.state = SpeedLimitAssistState.disabled
|
||||
self.sla.frame = -1
|
||||
self.sla.long_enabled = False
|
||||
self.sla.long_enabled_prev = False
|
||||
self.sla._speed_limit = 0.0
|
||||
self.sla.speed_limit_prev = 0.0
|
||||
self.sla._speed_limit_final_last = 0.0
|
||||
self.sla._distance = 0.0
|
||||
self.sla.long_engaged_timer = 0
|
||||
self.sla.pre_active_timer = 0
|
||||
self.events_sp.clear()
|
||||
return self
|
||||
|
||||
def build(self) -> "SpeedLimitAssistScenario":
|
||||
return self
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def params():
|
||||
p = Params()
|
||||
yield p
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def car_params_factory():
|
||||
return CarParamsFactory()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def scenario_builder(params):
|
||||
def builder(car_name: str = TOYOTA.TOYOTA_RAV4_TSS2) -> SpeedLimitAssistScenario:
|
||||
CP, CP_SP, _ = CarParamsFactory.create_car_interface(car_name)
|
||||
return SpeedLimitAssistScenario(CP, CP_SP, params)
|
||||
return builder
|
||||
@@ -1,362 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from cereal import car, custom
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist, \
|
||||
SpeedLimitAssistState
|
||||
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
||||
|
||||
|
||||
def make_mock_car_params(mocker, pcm_op_long: bool = True) -> car.CarParams:
|
||||
"""Create a minimal CarParams mock with pcm_op_long setting."""
|
||||
cp = mocker.MagicMock(spec=car.CarParams)
|
||||
cp.openpilotLongitudinalControl = pcm_op_long
|
||||
cp.pcmCruise = pcm_op_long
|
||||
cp.brand = "generic"
|
||||
return cp
|
||||
|
||||
|
||||
def make_mock_car_params_sp(mocker) -> custom.CarParamsSP:
|
||||
"""Create a minimal CarParamsSP mock."""
|
||||
cp_sp = mocker.MagicMock(spec=custom.CarParamsSP)
|
||||
return cp_sp
|
||||
|
||||
|
||||
def get_event_name_sp():
|
||||
"""Get EventNameSP enum."""
|
||||
return custom.OnroadEventSP.EventName
|
||||
|
||||
|
||||
def make_sla_factory(mocker, pcm_op_long: bool = True) -> SpeedLimitAssist:
|
||||
"""Factory: create a SpeedLimitAssist instance with mocked params."""
|
||||
cp = make_mock_car_params(mocker, pcm_op_long)
|
||||
cp_sp = make_mock_car_params_sp(mocker)
|
||||
|
||||
mock_params = mocker.MagicMock()
|
||||
mock_params.get_bool.return_value = True
|
||||
mock_params.get.return_value = True
|
||||
|
||||
mocker.patch("openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist.Params",
|
||||
return_value=mock_params)
|
||||
mocker.patch("openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.helpers.set_speed_limit_assist_availability")
|
||||
mocker.patch("openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.helpers.get_min_cap_floor", return_value=5.0)
|
||||
sla = SpeedLimitAssist(cp, cp_sp)
|
||||
return sla
|
||||
|
||||
|
||||
class TestBug1OverrideNoOscillation:
|
||||
"""Override falling edge arms suspend timer, preventing oscillation."""
|
||||
|
||||
def test_sustained_override_stays_disabled(self, mocker):
|
||||
"""5s sustained override: state never enters capping."""
|
||||
sla = make_sla_factory(mocker, pcm_op_long=True)
|
||||
|
||||
sla.long_enabled = True
|
||||
sla.enabled = True
|
||||
sla._has_speed_limit = True
|
||||
sla._speed_limit_final_last = 25.0
|
||||
sla._min_cap_floor = 5.0
|
||||
sla.state = SpeedLimitAssistState.capping
|
||||
sla._state_prev = SpeedLimitAssistState.capping
|
||||
sla._target_cap = 25.0
|
||||
events_sp = EventsSP()
|
||||
|
||||
num_ticks = int(5.0 / DT_MDL)
|
||||
for _ in range(num_ticks):
|
||||
sla.long_override = True
|
||||
sla.update_state_machine_cap(events_sp)
|
||||
|
||||
assert (sla.state != SpeedLimitAssistState.capping), "State entered capping during sustained override"
|
||||
|
||||
assert sla.state == SpeedLimitAssistState.disabled
|
||||
|
||||
def test_override_release_enters_capping_after_guard(self, mocker):
|
||||
"""Release override: state enters capping after guard period."""
|
||||
sla = make_sla_factory(mocker, pcm_op_long=True)
|
||||
|
||||
sla.long_enabled = True
|
||||
sla.enabled = True
|
||||
sla._has_speed_limit = True
|
||||
sla._speed_limit_final_last = 25.0
|
||||
sla._min_cap_floor = 5.0
|
||||
sla.state = SpeedLimitAssistState.disabled
|
||||
sla._state_prev = SpeedLimitAssistState.disabled
|
||||
sla._target_cap = 25.0
|
||||
sla._was_cap_suspended = True
|
||||
sla._override_active_last = True
|
||||
sla.long_override = True
|
||||
events_sp = EventsSP()
|
||||
|
||||
sla.update_state_machine_cap(events_sp)
|
||||
sla.long_override = False
|
||||
|
||||
guard_ticks = int(1.0 / DT_MDL)
|
||||
for i in range(guard_ticks + 1):
|
||||
sla.update_state_machine_cap(events_sp)
|
||||
if i < guard_ticks:
|
||||
assert sla.state == SpeedLimitAssistState.disabled
|
||||
|
||||
assert sla.state == SpeedLimitAssistState.capping
|
||||
|
||||
|
||||
class TestBug2TargetCapPreserved:
|
||||
"""Target cap preserved across override-release cycles."""
|
||||
|
||||
def test_target_cap_preserved_on_override_suspension(self, mocker):
|
||||
"""Override pulse: _target_cap preserved, not reset to new limit."""
|
||||
sla = make_sla_factory(mocker, pcm_op_long=True)
|
||||
|
||||
sla.long_enabled = True
|
||||
sla.enabled = True
|
||||
sla._has_speed_limit = True
|
||||
sla._speed_limit_final_last = 25.0
|
||||
sla._min_cap_floor = 5.0
|
||||
sla.state = SpeedLimitAssistState.capping
|
||||
sla._state_prev = SpeedLimitAssistState.capping
|
||||
sla._target_cap = 25.0
|
||||
sla._cap_change_timer = 0
|
||||
events_sp = EventsSP()
|
||||
|
||||
sla._speed_limit_final_last = 30.0
|
||||
|
||||
sla.long_override = True
|
||||
sla.update_state_machine_cap(events_sp)
|
||||
assert sla.state == SpeedLimitAssistState.disabled
|
||||
assert sla._was_cap_suspended is True
|
||||
|
||||
sla.long_override = False
|
||||
guard_ticks = int(1.0 / DT_MDL)
|
||||
for _ in range(guard_ticks + 1):
|
||||
sla.update_state_machine_cap(events_sp)
|
||||
|
||||
assert sla.state == SpeedLimitAssistState.capping
|
||||
assert sla._target_cap == 25.0, f"Expected 25.0, got {sla._target_cap}"
|
||||
|
||||
|
||||
class TestBug3AccelPressedWired:
|
||||
"""Accel_pressed parameter wired to sla.update()."""
|
||||
|
||||
def test_accel_pressed_parameter_received(self, mocker):
|
||||
"""sla.update() receives accel_pressed=True."""
|
||||
sla = make_sla_factory(mocker, pcm_op_long=True)
|
||||
|
||||
sla.long_enabled = True
|
||||
sla.enabled = True
|
||||
sla.v_ego = 20.0
|
||||
sla.a_ego = 0.0
|
||||
sla.v_cruise_cluster = 30.0
|
||||
events_sp = EventsSP()
|
||||
|
||||
sla.update(
|
||||
long_enabled=True,
|
||||
long_override=False,
|
||||
v_ego=20.0,
|
||||
a_ego=0.0,
|
||||
v_cruise_cluster=30.0,
|
||||
speed_limit=30.0,
|
||||
speed_limit_final_last=30.0,
|
||||
has_speed_limit=True,
|
||||
distance=100.0,
|
||||
events_sp=events_sp,
|
||||
accel_pressed=True,
|
||||
)
|
||||
|
||||
assert sla._accel_pressed is True
|
||||
|
||||
def test_accel_pressed_false_by_default(self, mocker):
|
||||
"""sla.update() with accel_pressed=False (default)."""
|
||||
sla = make_sla_factory(mocker, pcm_op_long=True)
|
||||
|
||||
events_sp = EventsSP()
|
||||
|
||||
sla.update(
|
||||
long_enabled=True,
|
||||
long_override=False,
|
||||
v_ego=20.0,
|
||||
a_ego=0.0,
|
||||
v_cruise_cluster=30.0,
|
||||
speed_limit=30.0,
|
||||
speed_limit_final_last=30.0,
|
||||
has_speed_limit=True,
|
||||
distance=100.0,
|
||||
events_sp=events_sp,
|
||||
)
|
||||
|
||||
assert sla._accel_pressed is False
|
||||
|
||||
|
||||
class TestBug4NoAudioCueOnOverrideReentry:
|
||||
"""Audio cue suppressed on override-release re-entry."""
|
||||
|
||||
def test_cue_fires_on_cold_entry(self, mocker):
|
||||
"""Fresh engagement: audio cue fires exactly once."""
|
||||
sla = make_sla_factory(mocker, pcm_op_long=True)
|
||||
|
||||
sla.long_enabled = True
|
||||
sla.enabled = True
|
||||
sla._has_speed_limit = True
|
||||
sla._speed_limit_final_last = 25.0
|
||||
sla._min_cap_floor = 5.0
|
||||
sla.state = SpeedLimitAssistState.disabled
|
||||
sla._state_prev = SpeedLimitAssistState.disabled
|
||||
sla._was_cap_suspended = False
|
||||
sla._cap_audio_cue_enabled = True
|
||||
events_sp = EventsSP()
|
||||
|
||||
sla.update_state_machine_cap(events_sp)
|
||||
|
||||
assert sla.state == SpeedLimitAssistState.capping
|
||||
assert sla._cap_audio_cue_fired is True
|
||||
event_name_sp = get_event_name_sp()
|
||||
assert event_name_sp.speedLimitCapActive in events_sp.events, "Audio cue event not fired"
|
||||
|
||||
def test_no_cue_on_override_reentry(self, mocker):
|
||||
"""Override-release re-entry: audio cue NOT fired."""
|
||||
sla = make_sla_factory(mocker, pcm_op_long=True)
|
||||
|
||||
sla.long_enabled = True
|
||||
sla.enabled = True
|
||||
sla._has_speed_limit = True
|
||||
sla._speed_limit_final_last = 25.0
|
||||
sla._min_cap_floor = 5.0
|
||||
sla.state = SpeedLimitAssistState.disabled
|
||||
sla._state_prev = SpeedLimitAssistState.capping
|
||||
sla._target_cap = 25.0
|
||||
sla._was_cap_suspended = True
|
||||
sla._override_active_last = False
|
||||
sla._cap_audio_cue_enabled = True
|
||||
sla._cap_audio_cue_fired = True
|
||||
events_sp = EventsSP()
|
||||
|
||||
sla._cap_suspended_timer = int(1.0 / DT_MDL)
|
||||
guard_ticks = int(1.0 / DT_MDL) + 1
|
||||
for _ in range(guard_ticks):
|
||||
sla.update_state_machine_cap(events_sp)
|
||||
sla._state_prev = sla.state
|
||||
|
||||
assert sla.state == SpeedLimitAssistState.capping
|
||||
event_name_sp = get_event_name_sp()
|
||||
assert (event_name_sp.speedLimitCapActive not in events_sp.events), "Audio cue should not fire on override re-entry"
|
||||
|
||||
|
||||
class TestEdgeACases:
|
||||
"""Edge A: No spurious timer on non-capping override."""
|
||||
|
||||
def test_edge_a_no_spurious_timer_on_disabled_override(self, mocker):
|
||||
"""Override during disabled state should not arm timer."""
|
||||
sla = make_sla_factory(mocker, pcm_op_long=True)
|
||||
|
||||
sla.long_enabled = True
|
||||
sla.enabled = True
|
||||
sla._has_speed_limit = False
|
||||
sla._was_cap_suspended = False
|
||||
sla.state = SpeedLimitAssistState.disabled
|
||||
sla._state_prev = SpeedLimitAssistState.disabled
|
||||
events_sp = EventsSP()
|
||||
|
||||
sla.long_override = True
|
||||
sla.update_state_machine_cap(events_sp)
|
||||
|
||||
sla.long_override = False
|
||||
sla.update_state_machine_cap(events_sp)
|
||||
|
||||
assert sla._cap_suspended_timer == 0
|
||||
|
||||
sla._has_speed_limit = True
|
||||
sla._speed_limit_final_last = 25.0
|
||||
sla._min_cap_floor = 5.0
|
||||
sla.update_state_machine_cap(events_sp)
|
||||
|
||||
assert sla.state == SpeedLimitAssistState.capping
|
||||
|
||||
|
||||
class TestEdgeBCases:
|
||||
"""Edge B: _target_cap preserved via pending."""
|
||||
|
||||
def test_edge_b_target_cap_preserved_via_pending(self, mocker):
|
||||
"""Target cap preserved when transiting pending->capping after suspension."""
|
||||
sla = make_sla_factory(mocker, pcm_op_long=True)
|
||||
|
||||
sla.long_enabled = True
|
||||
sla.enabled = True
|
||||
sla._has_speed_limit = True
|
||||
sla._speed_limit_final_last = 25.0
|
||||
sla._min_cap_floor = 5.0
|
||||
sla.state = SpeedLimitAssistState.capping
|
||||
sla._state_prev = SpeedLimitAssistState.capping
|
||||
sla._target_cap = 25.0
|
||||
sla._was_cap_suspended = True
|
||||
events_sp = EventsSP()
|
||||
|
||||
sla._has_speed_limit = False
|
||||
sla.update_state_machine_cap(events_sp)
|
||||
assert sla.state == SpeedLimitAssistState.pending
|
||||
|
||||
sla._has_speed_limit = True
|
||||
sla._speed_limit_final_last = 30.0
|
||||
sla.update_state_machine_cap(events_sp)
|
||||
|
||||
assert sla.state == SpeedLimitAssistState.capping
|
||||
assert sla._target_cap == 25.0
|
||||
|
||||
|
||||
class TestEdgeCCases:
|
||||
"""Edge C: Timer cleared on disengage."""
|
||||
|
||||
def test_edge_c_timer_cleared_on_disengage(self, mocker):
|
||||
"""Timer cleared when long_enabled=False."""
|
||||
sla = make_sla_factory(mocker, pcm_op_long=True)
|
||||
|
||||
sla.long_enabled = True
|
||||
sla.enabled = True
|
||||
sla._has_speed_limit = True
|
||||
sla._speed_limit_final_last = 25.0
|
||||
sla._min_cap_floor = 5.0
|
||||
sla._cap_suspended_timer = 50
|
||||
sla.state = SpeedLimitAssistState.capping
|
||||
sla._state_prev = SpeedLimitAssistState.capping
|
||||
events_sp = EventsSP()
|
||||
|
||||
sla.long_enabled = False
|
||||
sla.update_state_machine_cap(events_sp)
|
||||
|
||||
assert sla._cap_suspended_timer == 0
|
||||
assert sla.state == SpeedLimitAssistState.disabled
|
||||
|
||||
|
||||
class TestTargetCapPublished:
|
||||
"""Target cap published in cereal message."""
|
||||
|
||||
def test_target_cap_written_to_assist(self, mocker):
|
||||
"""_write_assist_fields writes sla._target_cap to assist.targetCap."""
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlannerSP
|
||||
|
||||
sla = make_sla_factory(mocker, pcm_op_long=True)
|
||||
sla._target_cap = 22.35
|
||||
sla.output_v_target = 10.0
|
||||
sla.output_a_target = 0.0
|
||||
sla.cap_delta = 0.0
|
||||
sla.is_enabled = True
|
||||
sla.is_active = False
|
||||
sla.state = SpeedLimitAssistState.disabled
|
||||
|
||||
planner = mocker.MagicMock()
|
||||
planner.sla = sla
|
||||
|
||||
msg = custom.LongitudinalPlanSP.new_message()
|
||||
assist = msg.speedLimit.assist
|
||||
|
||||
LongitudinalPlannerSP._write_assist_fields(planner, assist)
|
||||
|
||||
assert assist.targetCap == pytest.approx(22.35)
|
||||
assert assist.enabled is True
|
||||
assert assist.active is False
|
||||
assert assist.vTarget == pytest.approx(10.0)
|
||||
assert assist.aTarget == pytest.approx(0.0)
|
||||
assert assist.capDelta == pytest.approx(0.0)
|
||||
@@ -7,7 +7,7 @@ See the LICENSE.md file in the root directory for more details.
|
||||
|
||||
import pytest
|
||||
|
||||
from cereal import custom, car
|
||||
from cereal import custom
|
||||
from opendbc.car.car_helpers import interfaces
|
||||
from opendbc.car.rivian.values import CAR as RIVIAN
|
||||
from opendbc.car.tesla.values import CAR as TESLA
|
||||
@@ -25,7 +25,6 @@ from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist
|
||||
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
||||
|
||||
SpeedLimitAssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState
|
||||
ButtonType = car.CarState.ButtonEvent.Type
|
||||
|
||||
ALL_STATES = tuple(SpeedLimitAssistState.schema.enumerants.values())
|
||||
|
||||
@@ -72,7 +71,6 @@ class TestSpeedLimitAssist:
|
||||
CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name)
|
||||
CI = CarInterface(CP, CP_SP)
|
||||
CI.CP.openpilotLongitudinalControl = True # always assume it's openpilot longitudinal
|
||||
CI.CP.pcmCruise = False # test non-PCM FSM path (preActive, pending, adapting, active)
|
||||
sunnypilot_interfaces.setup_interfaces(CI, self.params)
|
||||
return CI
|
||||
|
||||
@@ -85,17 +83,13 @@ class TestSpeedLimitAssist:
|
||||
|
||||
def reset_state(self):
|
||||
self.sla.state = SpeedLimitAssistState.disabled
|
||||
self.sla._state_prev = SpeedLimitAssistState.disabled
|
||||
self.sla.frame = -1
|
||||
self.sla.long_enabled = False
|
||||
self.sla.long_enabled_prev = False
|
||||
self.sla.long_engaged_timer = 0
|
||||
self.sla.pre_active_timer = 0
|
||||
self.sla.last_op_engaged_frame = 0
|
||||
self.sla.op_engaged = False
|
||||
self.sla.op_engaged_prev = False
|
||||
self.sla._speed_limit = 0.
|
||||
self.sla._speed_limit_final_last = 0.
|
||||
self.sla.speed_limit_prev = 0.
|
||||
self.sla.v_cruise_cluster = 0.
|
||||
self.sla.v_cruise_cluster_prev = 0.
|
||||
self.sla.last_valid_speed_limit_offsetted = 0.
|
||||
self.sla._distance = 0.
|
||||
self.events_sp.clear()
|
||||
|
||||
@@ -139,6 +133,20 @@ class TestSpeedLimitAssist:
|
||||
assert self.sla.state == SpeedLimitAssistState.preActive
|
||||
assert self.sla.is_enabled and not self.sla.is_active
|
||||
|
||||
def test_transition_disabled_to_pending_no_speed_limit_not_max_initial_set_speed(self):
|
||||
for _ in range(int(3. / DT_MDL)):
|
||||
self.sla.update(True, False, SPEED_LIMITS['highway'], 0, SPEED_LIMITS['city'], 0, 0, False, 0, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.pending
|
||||
assert self.sla.is_enabled and not self.sla.is_active
|
||||
|
||||
def test_preactive_to_active_with_max_speed_confirmation(self):
|
||||
self.sla.state = SpeedLimitAssistState.preActive
|
||||
self.sla.update(True, False, SPEED_LIMITS['city'], 0, self.pcm_long_max_set_speed, SPEED_LIMITS['highway'],
|
||||
SPEED_LIMITS['highway'], True, 0, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.active
|
||||
assert self.sla.is_enabled and self.sla.is_active
|
||||
assert self.sla.output_v_target == SPEED_LIMITS['highway']
|
||||
|
||||
def test_preactive_timeout_to_inactive(self):
|
||||
self.sla.state = SpeedLimitAssistState.preActive
|
||||
self.sla.update(True, False, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
|
||||
@@ -147,6 +155,47 @@ class TestSpeedLimitAssist:
|
||||
self.sla.update(True, False, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.inactive
|
||||
|
||||
def test_preactive_to_pending_no_speed_limit(self):
|
||||
self.sla.state = SpeedLimitAssistState.preActive
|
||||
self.sla.update(True, False, SPEED_LIMITS['highway'], 0, self.pcm_long_max_set_speed, 0, 0, False, 0, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.pending
|
||||
assert self.sla.is_enabled and not self.sla.is_active
|
||||
|
||||
def test_pending_to_active_when_speed_limit_available(self):
|
||||
self.sla.state = SpeedLimitAssistState.pending
|
||||
self.sla.v_cruise_cluster_prev = self.pcm_long_max_set_speed
|
||||
self.sla.prev_v_cruise_cluster_conv = round(self.pcm_long_max_set_speed * self.speed_conv)
|
||||
|
||||
self.sla.update(True, False, SPEED_LIMITS['highway'], 0, self.pcm_long_max_set_speed,
|
||||
SPEED_LIMITS['highway'], SPEED_LIMITS['highway'], True, 0, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.active
|
||||
|
||||
def test_pending_to_adapting_when_below_speed_limit(self):
|
||||
self.sla.state = SpeedLimitAssistState.pending
|
||||
self.sla.v_cruise_cluster_prev = self.pcm_long_max_set_speed
|
||||
self.sla.prev_v_cruise_cluster_conv = round(self.pcm_long_max_set_speed * self.speed_conv)
|
||||
|
||||
self.sla.update(True, False, SPEED_LIMITS['highway'] + 5, 0, self.pcm_long_max_set_speed,
|
||||
SPEED_LIMITS['highway'], SPEED_LIMITS['highway'], True, 0, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.adapting
|
||||
assert self.sla.is_enabled and self.sla.is_active
|
||||
|
||||
def test_active_to_adapting_transition(self):
|
||||
self.initialize_active_state(self.pcm_long_max_set_speed)
|
||||
|
||||
self.sla.update(True, False, SPEED_LIMITS['highway'] + 2, 0, self.pcm_long_max_set_speed, SPEED_LIMITS['highway'],
|
||||
SPEED_LIMITS['highway'], True, 0, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.adapting
|
||||
|
||||
def test_adapting_to_active_transition(self):
|
||||
self.sla.state = SpeedLimitAssistState.adapting
|
||||
self.sla.v_cruise_cluster_prev = self.pcm_long_max_set_speed
|
||||
self.sla.prev_v_cruise_cluster_conv = round(self.pcm_long_max_set_speed * self.speed_conv)
|
||||
|
||||
self.sla.update(True, False, SPEED_LIMITS['city'], 0, self.pcm_long_max_set_speed, SPEED_LIMITS['highway'],
|
||||
SPEED_LIMITS['highway'], True, 0, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.active
|
||||
|
||||
def test_manual_cruise_change_detection(self):
|
||||
self.sla.state = SpeedLimitAssistState.active
|
||||
expected_cruise = SPEED_LIMITS['highway']
|
||||
@@ -154,7 +203,6 @@ class TestSpeedLimitAssist:
|
||||
|
||||
different_cruise = SPEED_LIMITS['highway'] + 5
|
||||
self.sla.update(True, False, SPEED_LIMITS['city'], 0, different_cruise, SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
|
||||
# In non-pcm mode, manual cruise change transitions to inactive (not tempPaused)
|
||||
assert self.sla.state == SpeedLimitAssistState.inactive
|
||||
|
||||
# TODO-SP: test lower CST cases
|
||||
@@ -228,236 +276,3 @@ class TestSpeedLimitAssist:
|
||||
assert self.sla.state in [SpeedLimitAssistState.preActive, SpeedLimitAssistState.active]
|
||||
elif initial_state in ACTIVE_STATES:
|
||||
assert self.sla.state in ACTIVE_STATES
|
||||
|
||||
def test_non_pcm_regression_method_exists(self):
|
||||
"""Regression: update_state_machine_non_pcm_long() method exists unchanged."""
|
||||
assert hasattr(self.sla, "update_state_machine_non_pcm_long")
|
||||
# Verify it is callable
|
||||
assert callable(self.sla.update_state_machine_non_pcm_long)
|
||||
# Verify method is not affected by cap mode refactor (signature check)
|
||||
import inspect
|
||||
inspect.signature(self.sla.update_state_machine_non_pcm_long)
|
||||
# Non-PCM method should have expected parameters (varies by impl, just verify it's present)
|
||||
|
||||
|
||||
class TestSpeedLimitAssistTempPaused:
|
||||
"""Tests for tempPaused state functionality (cap mode)."""
|
||||
|
||||
def setup_method(self, method):
|
||||
self.params = Params()
|
||||
self.reset_custom_params()
|
||||
self.events_sp = EventsSP()
|
||||
CI = self._setup_platform(DEFAULT_CAR)
|
||||
self.sla = SpeedLimitAssist(CI.CP, CI.CP_SP)
|
||||
self.sla.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.sla.pcm_op_long] / DT_MDL)
|
||||
self.pcm_long_max_set_speed = PCM_LONG_REQUIRED_MAX_SET_SPEED[self.sla.is_metric][1] # use 80 MPH for now
|
||||
self.speed_conv = CV.MS_TO_KPH if self.sla.is_metric else CV.MS_TO_MPH
|
||||
# For temp paused tests, use pcm_op_long = True
|
||||
self.sla.pcm_op_long = True
|
||||
self.sla.enabled = True
|
||||
|
||||
def teardown_method(self, method):
|
||||
self.reset_state()
|
||||
|
||||
def _setup_platform(self, car_name):
|
||||
CarInterface = interfaces[car_name]
|
||||
CP = CarInterface.get_non_essential_params(car_name)
|
||||
CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name)
|
||||
CI = CarInterface(CP, CP_SP)
|
||||
CI.CP.openpilotLongitudinalControl = True # always assume it's openpilot longitudinal
|
||||
CI.CP.pcmCruise = False # test non-PCM FSM path (preActive, pending, adapting, active)
|
||||
sunnypilot_interfaces.setup_interfaces(CI, self.params)
|
||||
return CI
|
||||
|
||||
def reset_custom_params(self):
|
||||
self.params.put("IsReleaseSpBranch", True)
|
||||
self.params.put("SpeedLimitMode", int(Mode.assist))
|
||||
self.params.put_bool("IsMetric", False)
|
||||
self.params.put("SpeedLimitOffsetType", 0)
|
||||
self.params.put("SpeedLimitValueOffset", 0)
|
||||
|
||||
def reset_state(self):
|
||||
self.sla.state = SpeedLimitAssistState.disabled
|
||||
self.sla._state_prev = SpeedLimitAssistState.disabled
|
||||
self.sla.frame = -1
|
||||
self.sla.long_enabled = False
|
||||
self.sla.long_enabled_prev = False
|
||||
self.sla.long_engaged_timer = 0
|
||||
self.sla.pre_active_timer = 0
|
||||
self.sla._speed_limit = 0.
|
||||
self.sla._speed_limit_final_last = 0.
|
||||
self.sla.speed_limit_prev = 0.
|
||||
self.sla.v_cruise_cluster = 0.
|
||||
self.sla.v_cruise_cluster_prev = 0.
|
||||
self.sla._distance = 0.
|
||||
self.events_sp.clear()
|
||||
|
||||
def test_temp_paused_entry_capping_state(self):
|
||||
"""Test that tempPaused entry occurs during state machine when user paused."""
|
||||
self.sla.state = SpeedLimitAssistState.capping
|
||||
self.sla._has_speed_limit = True
|
||||
self.sla._speed_limit_final_last = SPEED_LIMITS['city']
|
||||
self.sla._user_paused = True
|
||||
self.sla._user_paused_timer = 1000
|
||||
|
||||
self.sla.update(True, False, SPEED_LIMITS['city'], 0, self.pcm_long_max_set_speed,
|
||||
SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.tempPaused
|
||||
assert self.sla._disable_reason == custom.LongitudinalPlanSP.SpeedLimit.AssistDisableReason.userTempPause
|
||||
|
||||
def test_temp_paused_exit_on_speed_limit_change(self):
|
||||
"""Test exiting tempPaused when speed limit changes."""
|
||||
self.sla.state = SpeedLimitAssistState.tempPaused
|
||||
self.sla._user_paused = True
|
||||
self.sla._user_paused_timer = 1000
|
||||
self.sla._speed_limit_final_last_at_pause = SPEED_LIMITS['city']
|
||||
self.sla._speed_limit_final_last = SPEED_LIMITS['city']
|
||||
self.sla.long_enabled = True
|
||||
self.sla.enabled = True
|
||||
|
||||
# Change speed limit
|
||||
new_limit = SPEED_LIMITS['highway']
|
||||
self.sla.update(True, False, new_limit, 0, self.pcm_long_max_set_speed,
|
||||
new_limit, new_limit, True, 0, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.disabled
|
||||
assert self.sla._user_paused == False
|
||||
|
||||
def test_temp_paused_exit_on_timer_expiry(self):
|
||||
"""Test exiting tempPaused when 5-minute timer expires."""
|
||||
self.sla.state = SpeedLimitAssistState.tempPaused
|
||||
self.sla._user_paused = True
|
||||
self.sla._user_paused_timer = 1
|
||||
self.sla._speed_limit_final_last_at_pause = SPEED_LIMITS['city']
|
||||
|
||||
# Trigger update with timer expiry
|
||||
self.sla.update(True, False, SPEED_LIMITS['city'], 0, self.pcm_long_max_set_speed,
|
||||
SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.disabled
|
||||
assert self.sla._user_paused_timer <= 0
|
||||
|
||||
def test_disable_reason_user_cancel(self):
|
||||
"""Test disable_reason set to userCancel on pause."""
|
||||
self.sla.state = SpeedLimitAssistState.capping
|
||||
self.sla._user_paused = True
|
||||
self.sla._user_paused_timer = 1000
|
||||
|
||||
self.sla.update(True, False, SPEED_LIMITS['city'], 0, self.pcm_long_max_set_speed,
|
||||
SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.tempPaused
|
||||
assert self.sla._disable_reason == custom.LongitudinalPlanSP.SpeedLimit.AssistDisableReason.userTempPause
|
||||
|
||||
def test_disable_reason_long_override(self):
|
||||
"""Test disable_reason set to longOverride."""
|
||||
self.sla.state = SpeedLimitAssistState.capping
|
||||
self.sla._has_speed_limit = True
|
||||
self.sla._target_cap = SPEED_LIMITS['city']
|
||||
|
||||
self.sla.update(True, True, SPEED_LIMITS['city'], 0, self.pcm_long_max_set_speed,
|
||||
SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.disabled
|
||||
assert self.sla._disable_reason == custom.LongitudinalPlanSP.SpeedLimit.AssistDisableReason.longOverride
|
||||
|
||||
def test_disable_reason_below_floor(self):
|
||||
"""Test disable_reason set to belowFloor."""
|
||||
min_floor = self.sla._min_cap_floor
|
||||
self.sla.state = SpeedLimitAssistState.capping
|
||||
self.sla._has_speed_limit = True
|
||||
self.sla._speed_limit_final_last = min_floor - 1
|
||||
self.sla.long_enabled = True
|
||||
self.sla.enabled = True
|
||||
self.sla.v_cruise_cluster = self.pcm_long_max_set_speed
|
||||
self.sla.v_cruise_cluster_prev = self.pcm_long_max_set_speed
|
||||
self.sla.prev_v_cruise_cluster_conv = round(self.pcm_long_max_set_speed * self.speed_conv)
|
||||
|
||||
self.sla.update(True, False, min_floor - 1, 0, self.pcm_long_max_set_speed,
|
||||
min_floor - 1, min_floor - 1, True, 0, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.pending
|
||||
assert self.sla._disable_reason == custom.LongitudinalPlanSP.SpeedLimit.AssistDisableReason.belowFloor
|
||||
|
||||
def test_temp_paused_entry_cap_cluster_nudge_plus(self):
|
||||
"""Test tempPaused entry in capping state when cluster nudged above limit."""
|
||||
self.sla.state = SpeedLimitAssistState.capping
|
||||
self.sla._has_speed_limit = True
|
||||
self.sla._speed_limit_final_last = SPEED_LIMITS['city']
|
||||
self.sla._target_cap = SPEED_LIMITS['city']
|
||||
self.sla.long_enabled = True
|
||||
self.sla.enabled = True
|
||||
self.sla.v_cruise_cluster = SPEED_LIMITS['city']
|
||||
self.sla.v_cruise_cluster_prev = SPEED_LIMITS['city']
|
||||
self.sla.prev_v_cruise_cluster_conv = round(SPEED_LIMITS['city'] * self.speed_conv)
|
||||
|
||||
# Nudge cluster up (simulate user pressing accel)
|
||||
nudged_cruise = SPEED_LIMITS['city'] + 1.0
|
||||
self.sla.update(True, False, SPEED_LIMITS['city'], 0, nudged_cruise,
|
||||
SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.tempPaused
|
||||
assert self.sla._user_paused == True
|
||||
assert self.sla._user_paused_timer > 0
|
||||
assert self.sla._disable_reason == custom.LongitudinalPlanSP.SpeedLimit.AssistDisableReason.userTempPause
|
||||
|
||||
def test_temp_paused_entry_cap_cluster_nudge_minus(self):
|
||||
"""Test tempPaused entry in capping state when cluster nudged below limit."""
|
||||
self.sla.state = SpeedLimitAssistState.capping
|
||||
self.sla._has_speed_limit = True
|
||||
self.sla._speed_limit_final_last = SPEED_LIMITS['city']
|
||||
self.sla._target_cap = SPEED_LIMITS['city']
|
||||
self.sla.long_enabled = True
|
||||
self.sla.enabled = True
|
||||
self.sla.v_cruise_cluster = SPEED_LIMITS['city']
|
||||
self.sla.v_cruise_cluster_prev = SPEED_LIMITS['city']
|
||||
self.sla.prev_v_cruise_cluster_conv = round(SPEED_LIMITS['city'] * self.speed_conv)
|
||||
|
||||
# Nudge cluster down (simulate user pressing brake/decel)
|
||||
nudged_cruise = SPEED_LIMITS['city'] - 1.0
|
||||
self.sla.update(True, False, SPEED_LIMITS['city'], 0, nudged_cruise,
|
||||
SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.tempPaused
|
||||
assert self.sla._user_paused == True
|
||||
assert self.sla._user_paused_timer > 0
|
||||
|
||||
def test_temp_paused_exit_cluster_returns_to_limit(self):
|
||||
"""Test exiting tempPaused when cluster returns within ±1 of limit."""
|
||||
self.sla.state = SpeedLimitAssistState.tempPaused
|
||||
self.sla._user_paused = True
|
||||
self.sla._user_paused_timer = 1000
|
||||
self.sla._speed_limit_final_last_at_pause = SPEED_LIMITS['city']
|
||||
self.sla._speed_limit_final_last = SPEED_LIMITS['city']
|
||||
self.sla.long_enabled = True
|
||||
self.sla.enabled = True
|
||||
self.sla.v_cruise_cluster_prev = SPEED_LIMITS['city'] + 5
|
||||
self.sla.prev_v_cruise_cluster_conv = round((SPEED_LIMITS['city'] + 5) * self.speed_conv)
|
||||
|
||||
# Return cluster to within ±1 of limit
|
||||
returned_cruise = SPEED_LIMITS['city'] + 0.5
|
||||
self.sla.update(True, False, SPEED_LIMITS['city'], 0, returned_cruise,
|
||||
SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.disabled
|
||||
assert self.sla._user_paused == False
|
||||
|
||||
def test_temp_paused_sticky_double_nudge(self):
|
||||
"""Test that multiple nudges keep state in tempPaused."""
|
||||
self.sla.state = SpeedLimitAssistState.capping
|
||||
self.sla._has_speed_limit = True
|
||||
self.sla._speed_limit_final_last = SPEED_LIMITS['city']
|
||||
self.sla._target_cap = SPEED_LIMITS['city']
|
||||
self.sla.long_enabled = True
|
||||
self.sla.enabled = True
|
||||
self.sla.v_cruise_cluster = SPEED_LIMITS['city']
|
||||
self.sla.v_cruise_cluster_prev = SPEED_LIMITS['city']
|
||||
self.sla.prev_v_cruise_cluster_conv = round(SPEED_LIMITS['city'] * self.speed_conv)
|
||||
|
||||
# First nudge
|
||||
nudged_cruise_1 = SPEED_LIMITS['city'] + 2.0
|
||||
self.sla.update(True, False, SPEED_LIMITS['city'], 0, nudged_cruise_1,
|
||||
SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.tempPaused
|
||||
assert self.sla._user_paused_timer > 0
|
||||
saved_timer_1 = self.sla._user_paused_timer
|
||||
|
||||
# Second nudge while in tempPaused (shouldn't trigger another entry)
|
||||
nudged_cruise_2 = SPEED_LIMITS['city'] + 3.0
|
||||
self.sla.update(True, False, SPEED_LIMITS['city'], 0, nudged_cruise_2,
|
||||
SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.tempPaused
|
||||
# Timer should have been decremented by one update call
|
||||
assert self.sla._user_paused_timer == saved_timer_1 - 1
|
||||
|
||||
@@ -1,585 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
|
||||
Test suite for Speed Limit Assist cap mode (pcm_op_long only).
|
||||
Covers 20 edge cases for FSM, debounce, upshift, pedal release, and audio cue.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from cereal import custom
|
||||
from opendbc.car.toyota.values import CAR as TOYOTA
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import UpshiftAccept
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist, \
|
||||
CAP_ACTIVE_STATES
|
||||
|
||||
SpeedLimitAssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState
|
||||
|
||||
|
||||
class TestSpeedLimitCapMode:
|
||||
|
||||
def test_cap_mode_applies_to_pcm_op_long(self, scenario_builder):
|
||||
"""Cap mode applies to pcm_op_long cars (pcmCruise=True + openpilotLongitudinalControl=True)."""
|
||||
scenario = scenario_builder(TOYOTA.TOYOTA_RAV4_TSS2)
|
||||
scenario.set_state(SpeedLimitAssistState.disabled)
|
||||
assert scenario.sla.pcm_op_long is True
|
||||
assert scenario.sla.state == SpeedLimitAssistState.disabled
|
||||
|
||||
def test_disabled_to_capping_transition(self, scenario_builder):
|
||||
"""FSM: disabled -> capping when speed limit available and engaged."""
|
||||
scenario = scenario_builder()
|
||||
scenario.set_state(SpeedLimitAssistState.disabled)
|
||||
scenario.set_engaged(True)
|
||||
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
|
||||
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
|
||||
|
||||
# Update to enter capping
|
||||
scenario.sla.update(
|
||||
True, False, 25 * CV.MPH_TO_MS, 0,
|
||||
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
|
||||
True, 0, scenario.events_sp
|
||||
)
|
||||
assert scenario.sla.is_active
|
||||
|
||||
def test_capping_to_disabled_on_disengagement(self, scenario_builder):
|
||||
"""FSM: capping -> disabled when user disengages (manual override)."""
|
||||
scenario = scenario_builder()
|
||||
scenario.set_state(SpeedLimitAssistState.capping)
|
||||
scenario.set_engaged(True)
|
||||
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
|
||||
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
|
||||
|
||||
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
|
||||
|
||||
# Simulate manual override (long_override=True)
|
||||
scenario.sla.update(
|
||||
True, True, 25 * CV.MPH_TO_MS, 0,
|
||||
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
|
||||
True, 0, scenario.events_sp
|
||||
)
|
||||
|
||||
assert scenario.sla.state == SpeedLimitAssistState.disabled
|
||||
assert scenario.sla.output_v_target == V_CRUISE_UNSET
|
||||
|
||||
def test_below_floor_pause_transition(self, scenario_builder):
|
||||
"""FSM: capping exits to pending (no cap emitted) when posted limit is below min cap floor."""
|
||||
scenario = scenario_builder()
|
||||
scenario.set_param("SpeedLimitMinCapFloor", 25) # 25 mph floor
|
||||
scenario.set_state(SpeedLimitAssistState.capping)
|
||||
scenario.set_engaged(True)
|
||||
|
||||
# Posted limit 20 mph (below 25 mph floor) -> cap must pause
|
||||
low_limit_ms = 20 * CV.MPH_TO_MS
|
||||
scenario.set_speed_limits(low_limit_ms, 0)
|
||||
scenario.set_cruise_speeds(30 * CV.MPH_TO_MS)
|
||||
|
||||
for _ in range(int(0.5 / DT_MDL)):
|
||||
scenario.sla.update(
|
||||
True, False, 30 * CV.MPH_TO_MS, 0,
|
||||
30 * CV.MPH_TO_MS, low_limit_ms, low_limit_ms,
|
||||
True, 0, scenario.events_sp
|
||||
)
|
||||
|
||||
# Below-floor condition pauses cap: state is not capping, v_target is unset (no cap)
|
||||
assert scenario.sla.state != SpeedLimitAssistState.capping
|
||||
assert scenario.sla.output_v_target == V_CRUISE_UNSET
|
||||
|
||||
def test_resume_from_pause_above_floor(self, scenario_builder):
|
||||
"""FSM: pending (below-floor pause) -> capping when vehicle speed rises above min cap floor."""
|
||||
scenario = scenario_builder()
|
||||
scenario.set_param("SpeedLimitMinCapFloor", 25) # 25 mph
|
||||
scenario.set_state(SpeedLimitAssistState.pending)
|
||||
scenario.set_engaged(True)
|
||||
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
|
||||
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
|
||||
|
||||
# Update with speed above floor
|
||||
scenario.sla.update(
|
||||
True, False, 30 * CV.MPH_TO_MS, 0,
|
||||
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
|
||||
True, 0, scenario.events_sp
|
||||
)
|
||||
# Should resume capping if conditions allow
|
||||
assert scenario.sla.state in CAP_ACTIVE_STATES or scenario.sla.state == SpeedLimitAssistState.pending
|
||||
|
||||
def test_change_debounce_hold_new_limit(self, scenario_builder):
|
||||
"""FSM: New speed limit held for 1s before accepting (debounce)."""
|
||||
scenario = scenario_builder()
|
||||
scenario.set_param("SpeedLimitUpshiftAccept", UpshiftAccept.ACCEL_PEDAL)
|
||||
scenario.set_state(SpeedLimitAssistState.capping)
|
||||
scenario.set_engaged(True)
|
||||
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
|
||||
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
|
||||
|
||||
# Start in capping with initial cap at 25 mph
|
||||
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
|
||||
scenario.sla._has_speed_limit = True
|
||||
|
||||
# First: establish baseline with 25 mph and pressed pedal (to establish state for edge detection)
|
||||
scenario.sla.update(
|
||||
True, False, 25 * CV.MPH_TO_MS, 0,
|
||||
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
|
||||
True, 0, scenario.events_sp, accel_pressed=True
|
||||
)
|
||||
|
||||
# Then: change limit to 35 mph, press pedal briefly, then release and wait for debounce to expire
|
||||
# Press pedal for first 0.5s (speed_limit=25 but speed_limit_final_last=35 simulates detection)
|
||||
for _ in range(int(0.5 / DT_MDL)):
|
||||
scenario.sla.update(
|
||||
True, False, 35 * CV.MPH_TO_MS, 0,
|
||||
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 35 * CV.MPH_TO_MS,
|
||||
True, 0, scenario.events_sp, accel_pressed=True
|
||||
)
|
||||
|
||||
# Release pedal and wait for cap debounce + accel debounce to complete (0.7s more = 1.2s total)
|
||||
for _ in range(int(0.7 / DT_MDL)):
|
||||
scenario.sla.update(
|
||||
True, False, 35 * CV.MPH_TO_MS, 0,
|
||||
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 35 * CV.MPH_TO_MS,
|
||||
True, 0, scenario.events_sp, accel_pressed=False
|
||||
)
|
||||
|
||||
# Cap should now be 35 mph after debounce and pedal release edge
|
||||
assert abs(scenario.sla._target_cap - 35 * CV.MPH_TO_MS) < 0.1
|
||||
assert scenario.sla.state == SpeedLimitAssistState.capping
|
||||
|
||||
def test_upshift_never_raise_keeps_old_cap(self, scenario_builder):
|
||||
"""FSM: NEVER_RAISE mode keeps cap unchanged when limit increases."""
|
||||
scenario = scenario_builder()
|
||||
scenario.set_param("SpeedLimitUpshiftAccept", UpshiftAccept.NEVER_RAISE)
|
||||
scenario.set_state(SpeedLimitAssistState.capping)
|
||||
scenario.set_engaged(True)
|
||||
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
|
||||
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
|
||||
|
||||
# Start with cap at 25 mph
|
||||
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
|
||||
|
||||
# Increase limit to 35 mph, wait for debounce to expire
|
||||
for _ in range(int(1.2 / DT_MDL)):
|
||||
scenario.sla.update(
|
||||
True, False, 35 * CV.MPH_TO_MS, 0,
|
||||
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 35 * CV.MPH_TO_MS,
|
||||
True, 0, scenario.events_sp
|
||||
)
|
||||
|
||||
# In NEVER_RAISE mode, cap stays at 25 mph (old value)
|
||||
assert abs(scenario.sla._target_cap - 25 * CV.MPH_TO_MS) < 0.1
|
||||
assert scenario.sla.state == SpeedLimitAssistState.capping
|
||||
|
||||
def test_upshift_accel_pedal_requires_release(self, scenario_builder):
|
||||
"""FSM: ACCEL_PEDAL mode accepts new cap only on pedal release."""
|
||||
scenario = scenario_builder()
|
||||
scenario.set_param("SpeedLimitUpshiftAccept", UpshiftAccept.ACCEL_PEDAL)
|
||||
scenario.set_state(SpeedLimitAssistState.capping)
|
||||
scenario.set_engaged(True)
|
||||
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
|
||||
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
|
||||
|
||||
# Start with cap at 25 mph
|
||||
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
|
||||
|
||||
# Pedal pressed with new limit 35 mph, wait 1.1s (exceeds cap debounce) but pedal still pressed
|
||||
for _ in range(int(1.1 / DT_MDL)):
|
||||
scenario.sla.update(
|
||||
True, False, 35 * CV.MPH_TO_MS, 0,
|
||||
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 35 * CV.MPH_TO_MS,
|
||||
True, 0, scenario.events_sp, accel_pressed=True
|
||||
)
|
||||
|
||||
# Cap should still be 25 mph (pedal pressed, upshift rejected despite debounce)
|
||||
assert abs(scenario.sla._target_cap - 25 * CV.MPH_TO_MS) < 0.1
|
||||
assert scenario.sla.state == SpeedLimitAssistState.capping
|
||||
|
||||
def test_pedal_release_debounce_200ms(self, scenario_builder):
|
||||
"""FSM: Accel pedal release edge requires 200ms debounce."""
|
||||
scenario = scenario_builder()
|
||||
scenario.set_param("SpeedLimitUpshiftAccept", UpshiftAccept.ACCEL_PEDAL)
|
||||
scenario.set_state(SpeedLimitAssistState.capping)
|
||||
scenario.set_engaged(True)
|
||||
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
|
||||
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
|
||||
|
||||
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
|
||||
|
||||
# First establish the limit change and wait for cap change debounce to start
|
||||
scenario.sla.update(
|
||||
True, False, 35 * CV.MPH_TO_MS, 0,
|
||||
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 35 * CV.MPH_TO_MS,
|
||||
True, 0, scenario.events_sp, accel_pressed=True
|
||||
)
|
||||
|
||||
# Pedal pressed, wait 0.6s, then release and wait 0.5s (total 1.1s > 1.0s cap debounce + 0.2s accel debounce)
|
||||
for _ in range(int(0.6 / DT_MDL)):
|
||||
scenario.sla.update(
|
||||
True, False, 35 * CV.MPH_TO_MS, 0,
|
||||
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 35 * CV.MPH_TO_MS,
|
||||
True, 0, scenario.events_sp, accel_pressed=True
|
||||
)
|
||||
|
||||
# Release and wait for both debounces to complete
|
||||
for _ in range(int(0.5 / DT_MDL)):
|
||||
scenario.sla.update(
|
||||
True, False, 35 * CV.MPH_TO_MS, 0,
|
||||
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 35 * CV.MPH_TO_MS,
|
||||
True, 0, scenario.events_sp, accel_pressed=False
|
||||
)
|
||||
|
||||
# Now cap should be updated to 35 mph (cap debounce complete + accel release edge detected and debounced)
|
||||
assert abs(scenario.sla._target_cap - 35 * CV.MPH_TO_MS) < 0.1
|
||||
assert scenario.sla.state == SpeedLimitAssistState.capping
|
||||
|
||||
def test_suspended_timer_after_long_override(self, scenario_builder):
|
||||
"""FSM: 1s suspension window after manual override release."""
|
||||
scenario = scenario_builder()
|
||||
scenario.set_state(SpeedLimitAssistState.capping)
|
||||
scenario.set_engaged(True)
|
||||
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
|
||||
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
|
||||
|
||||
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
|
||||
|
||||
scenario.sla.update(
|
||||
True, True, 25 * CV.MPH_TO_MS, 0,
|
||||
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
|
||||
True, 0, scenario.events_sp
|
||||
)
|
||||
|
||||
scenario.sla.update(
|
||||
True, False, 25 * CV.MPH_TO_MS, 0,
|
||||
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
|
||||
True, 0, scenario.events_sp
|
||||
)
|
||||
|
||||
assert scenario.sla.state == SpeedLimitAssistState.disabled
|
||||
assert scenario.sla._cap_suspended_timer > 0 # frame counter
|
||||
|
||||
# Attempt to re-engage within suspension window (0.5s)
|
||||
for _ in range(int(0.5 / DT_MDL)):
|
||||
scenario.sla.update(
|
||||
True, False, 25 * CV.MPH_TO_MS, 0,
|
||||
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
|
||||
True, 0, scenario.events_sp
|
||||
)
|
||||
|
||||
# Should still be disabled (suspension active)
|
||||
assert scenario.sla.state == SpeedLimitAssistState.disabled
|
||||
|
||||
# Wait for suspension to expire (1.1s total)
|
||||
for _ in range(int(0.7 / DT_MDL)):
|
||||
scenario.sla.update(
|
||||
True, False, 25 * CV.MPH_TO_MS, 0,
|
||||
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
|
||||
True, 0, scenario.events_sp
|
||||
)
|
||||
|
||||
# Now cap can re-engage and transition to capping
|
||||
assert scenario.sla.state == SpeedLimitAssistState.capping
|
||||
|
||||
def test_audio_cue_fires_once_on_capping_entry(self, scenario_builder):
|
||||
"""Event: speedLimitCapActive fires once on entry to capping state."""
|
||||
scenario = scenario_builder()
|
||||
scenario.set_param("SpeedLimitCapAudioCue", True)
|
||||
scenario.set_state(SpeedLimitAssistState.disabled)
|
||||
scenario.set_engaged(True)
|
||||
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
|
||||
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
|
||||
|
||||
# Enter capping state
|
||||
scenario.sla.update(
|
||||
True, False, 25 * CV.MPH_TO_MS, 0,
|
||||
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
|
||||
True, 0, scenario.events_sp
|
||||
)
|
||||
|
||||
# Verify we transitioned to capping
|
||||
assert scenario.sla.state == SpeedLimitAssistState.capping
|
||||
# Audio cue flag should be set
|
||||
assert scenario.sla._cap_audio_cue_fired is True
|
||||
|
||||
def test_audio_cue_disabled_no_fire(self, scenario_builder):
|
||||
"""Event: speedLimitCapActive suppressed when audio cue disabled."""
|
||||
scenario = scenario_builder()
|
||||
scenario.set_param("SpeedLimitCapAudioCue", False)
|
||||
scenario.set_state(SpeedLimitAssistState.disabled)
|
||||
scenario.set_engaged(True)
|
||||
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
|
||||
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
|
||||
|
||||
# Enter capping state
|
||||
scenario.sla.update(
|
||||
True, False, 25 * CV.MPH_TO_MS, 0,
|
||||
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
|
||||
True, 0, scenario.events_sp
|
||||
)
|
||||
|
||||
# Verify we transitioned to capping
|
||||
assert scenario.sla.state == SpeedLimitAssistState.capping
|
||||
# Audio cue flag should NOT be set (disabled)
|
||||
assert scenario.sla._cap_audio_cue_fired is False
|
||||
|
||||
def test_cap_delta_ui_feedback(self, scenario_builder):
|
||||
"""UI: target_cap tracks posted limit, delta = v_cruise_cluster - target_cap (positive when cap below driver intent)."""
|
||||
scenario = scenario_builder()
|
||||
scenario.set_state(SpeedLimitAssistState.capping)
|
||||
scenario.set_engaged(True)
|
||||
limit_ms = 25 * CV.MPH_TO_MS
|
||||
cruise_ms = 35 * CV.MPH_TO_MS
|
||||
scenario.set_speed_limits(limit_ms, 0)
|
||||
scenario.set_cruise_speeds(cruise_ms)
|
||||
|
||||
scenario.sla._target_cap = limit_ms
|
||||
scenario.sla.update(
|
||||
True, False, 25 * CV.MPH_TO_MS, 0,
|
||||
cruise_ms, limit_ms, limit_ms,
|
||||
True, 0, scenario.events_sp
|
||||
)
|
||||
|
||||
assert abs(scenario.sla.output_v_target - limit_ms) < 0.1
|
||||
assert abs(scenario.sla._target_cap - limit_ms) < 0.1
|
||||
cap_delta = max(0., cruise_ms - scenario.sla._target_cap)
|
||||
assert cap_delta > 0
|
||||
assert abs(cap_delta - 10 * CV.MPH_TO_MS) < 0.1
|
||||
assert abs(scenario.sla.cap_delta - cap_delta) < 0.1
|
||||
assert scenario.sla.state == SpeedLimitAssistState.capping
|
||||
|
||||
def test_min_cap_floor_zero_disables_pause(self, scenario_builder):
|
||||
"""FSM: min cap floor 0 disables pause-on-low-speed behavior."""
|
||||
scenario = scenario_builder()
|
||||
scenario.set_param("SpeedLimitMinCapFloor", 0)
|
||||
scenario.set_state(SpeedLimitAssistState.capping)
|
||||
scenario.set_engaged(True)
|
||||
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
|
||||
scenario.set_cruise_speeds(5 * CV.MPH_TO_MS)
|
||||
|
||||
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
|
||||
|
||||
# Very low speed (5 mph), but floor is 0
|
||||
scenario.sla.update(
|
||||
True, False, 25 * CV.MPH_TO_MS, 0,
|
||||
5 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
|
||||
True, 0, scenario.events_sp
|
||||
)
|
||||
|
||||
# Should continue capping (floor 0 means no pause even at low speed)
|
||||
assert scenario.sla.state == SpeedLimitAssistState.capping
|
||||
assert scenario.sla._cap_below_floor is False
|
||||
|
||||
def test_min_cap_floor_max_value_40_mph(self, scenario_builder):
|
||||
"""FSM: min cap floor clamped to 40 mph (reasonable max)."""
|
||||
scenario = scenario_builder()
|
||||
scenario.set_param("IsMetric", False)
|
||||
scenario.set_param("SpeedLimitMinCapFloor", 40)
|
||||
scenario.sla.is_metric = False
|
||||
scenario.set_state(SpeedLimitAssistState.capping)
|
||||
scenario.set_engaged(True)
|
||||
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
|
||||
scenario.set_cruise_speeds(30 * CV.MPH_TO_MS)
|
||||
|
||||
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
|
||||
|
||||
# Speed 25 mph (limit) is below floor 40 mph, should pause
|
||||
for _ in range(int(0.5 / DT_MDL)):
|
||||
scenario.sla.update(
|
||||
True, False, 25 * CV.MPH_TO_MS, 0,
|
||||
30 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
|
||||
True, 0, scenario.events_sp
|
||||
)
|
||||
|
||||
# Should transition to pending (below floor)
|
||||
assert scenario.sla.state == SpeedLimitAssistState.pending
|
||||
assert scenario.sla._cap_below_floor is True
|
||||
|
||||
def test_v_target_clamped_to_cruise_when_capping(self, scenario_builder):
|
||||
"""Output: v_target = min(v_cruise, cap) when actively capping."""
|
||||
scenario = scenario_builder()
|
||||
scenario.set_state(SpeedLimitAssistState.capping)
|
||||
scenario.set_engaged(True)
|
||||
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
|
||||
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
|
||||
|
||||
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
|
||||
|
||||
scenario.sla.update(
|
||||
True, False, 25 * CV.MPH_TO_MS, 0,
|
||||
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
|
||||
True, 0, scenario.events_sp
|
||||
)
|
||||
|
||||
# v_target should be min(20, 25) = 20 m/s (driver cruise is limiting factor)
|
||||
assert abs(scenario.sla.output_v_target - 20 * CV.MPH_TO_MS) < 0.1
|
||||
assert scenario.sla.state == SpeedLimitAssistState.capping
|
||||
|
||||
# Missing edge cases from matrix (Issues #1, #2, #5, #7, #16, #18, #19)
|
||||
|
||||
def test_school_zone_false_positive_25_mph_floor(self, scenario_builder):
|
||||
"""Edge case #1: OSM school zone 25 mph at inactive hours blocked by floor."""
|
||||
scenario = scenario_builder()
|
||||
scenario.set_param("SpeedLimitMinCapFloor", 25)
|
||||
scenario.set_state(SpeedLimitAssistState.capping)
|
||||
scenario.set_engaged(True)
|
||||
scenario.set_speed_limits(11.18, 0) # 25 mph -> m/s
|
||||
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
|
||||
|
||||
scenario.sla._target_cap = 11.18
|
||||
|
||||
# Speed limit 25 mph = 11.18 m/s is at floor (not strictly above)
|
||||
scenario.sla.update(
|
||||
True, False, 11.18, 0,
|
||||
20 * CV.MPH_TO_MS, 11.18, 11.18,
|
||||
True, 0, scenario.events_sp
|
||||
)
|
||||
|
||||
# Should not pause at exactly floor (must be strictly above)
|
||||
assert scenario.sla.state == SpeedLimitAssistState.capping
|
||||
|
||||
def test_construction_stale_25_mph_pauses(self, scenario_builder):
|
||||
"""Edge case #2: Construction temp speed limit 25 mph pauses on stale data."""
|
||||
scenario = scenario_builder()
|
||||
scenario.set_param("SpeedLimitMinCapFloor", 25)
|
||||
scenario.set_state(SpeedLimitAssistState.capping)
|
||||
scenario.set_engaged(True)
|
||||
# Stale construction limit (25 mph, below floor by resolver check)
|
||||
scenario.set_speed_limits(11.18, 0)
|
||||
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
|
||||
|
||||
scenario.sla._target_cap = 11.18
|
||||
|
||||
# Simulate old data with has_speed_limit=False (resolver cleared it)
|
||||
scenario.sla.update(
|
||||
True, False, 11.18, 0,
|
||||
20 * CV.MPH_TO_MS, 11.18, 11.18,
|
||||
False, 0, scenario.events_sp
|
||||
)
|
||||
|
||||
# Should transition to pending (lost limit)
|
||||
assert scenario.sla.state == SpeedLimitAssistState.pending
|
||||
|
||||
@pytest.mark.parametrize("is_metric", [True, False])
|
||||
def test_kmh_mph_unit_conversion_border(self, scenario_builder, is_metric):
|
||||
"""Edge case #5: km/h ↔ mph border crossing unit conversion."""
|
||||
scenario = scenario_builder()
|
||||
scenario.params.put_bool("IsMetric", is_metric)
|
||||
# Reload SLA to pick up metric setting
|
||||
from opendbc.car.toyota.values import CAR as TOYOTA
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.tests.conftest import CarParamsFactory
|
||||
CP, CP_SP, _ = CarParamsFactory.create_car_interface(TOYOTA.TOYOTA_RAV4_TSS2)
|
||||
scenario.sla = SpeedLimitAssist(CP, CP_SP)
|
||||
scenario.sla.params.put_bool("IsMetric", is_metric)
|
||||
|
||||
scenario.set_state(SpeedLimitAssistState.capping)
|
||||
scenario.set_engaged(True)
|
||||
|
||||
if is_metric:
|
||||
# 65 km/h ≈ 40.4 mph
|
||||
scenario.set_speed_limits(65 * CV.KPH_TO_MS, 0)
|
||||
scenario.set_cruise_speeds(60 * CV.KPH_TO_MS)
|
||||
scenario.sla._target_cap = 65 * CV.KPH_TO_MS
|
||||
else:
|
||||
# 40 mph ≈ 64.4 km/h
|
||||
scenario.set_speed_limits(40 * CV.MPH_TO_MS, 0)
|
||||
scenario.set_cruise_speeds(35 * CV.MPH_TO_MS)
|
||||
scenario.sla._target_cap = 40 * CV.MPH_TO_MS
|
||||
|
||||
scenario.sla.update(
|
||||
True, False, scenario.sla._target_cap, 0,
|
||||
scenario.sla.v_cruise_cluster, scenario.sla._speed_limit,
|
||||
scenario.sla._speed_limit, True, 0, scenario.events_sp
|
||||
)
|
||||
|
||||
# Should remain in capping with correct unit-aware cap
|
||||
assert scenario.sla.state == SpeedLimitAssistState.capping
|
||||
assert scenario.sla._has_speed_limit is True
|
||||
|
||||
def test_gps_tunnel_fade_10s_age_limit(self, scenario_builder):
|
||||
"""Edge case #7: GPS tunnel fade stale limit via LIMIT_MAX_MAP_DATA_AGE."""
|
||||
scenario = scenario_builder()
|
||||
scenario.set_state(SpeedLimitAssistState.capping)
|
||||
scenario.set_engaged(True)
|
||||
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
|
||||
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
|
||||
|
||||
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
|
||||
|
||||
# Simulate has_speed_limit=False (resolver expired data after 10s)
|
||||
scenario.sla.update(
|
||||
True, False, 25 * CV.MPH_TO_MS, 0,
|
||||
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
|
||||
False, 0, scenario.events_sp
|
||||
)
|
||||
|
||||
# Should transition to pending (lost limit)
|
||||
assert scenario.sla.state == SpeedLimitAssistState.pending
|
||||
assert scenario.sla.output_v_target == V_CRUISE_UNSET
|
||||
|
||||
def test_lost_speed_limit_mid_cap_to_pending(self, scenario_builder):
|
||||
"""Edge case #16: Lost speed limit mid-cap transitions to pending."""
|
||||
scenario = scenario_builder()
|
||||
scenario.set_state(SpeedLimitAssistState.capping)
|
||||
scenario.set_engaged(True)
|
||||
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
|
||||
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
|
||||
|
||||
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
|
||||
|
||||
# Simulate limit suddenly unavailable
|
||||
scenario.sla.update(
|
||||
True, False, 25 * CV.MPH_TO_MS, 0,
|
||||
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
|
||||
False, 0, scenario.events_sp
|
||||
)
|
||||
|
||||
# Should transition to pending, emit V_CRUISE_UNSET
|
||||
assert scenario.sla.state == SpeedLimitAssistState.pending
|
||||
assert scenario.sla.output_v_target == V_CRUISE_UNSET
|
||||
|
||||
def test_mode_param_off_disables_sla_on_pcm_op_long(self, scenario_builder):
|
||||
"""Edge case #18: SpeedLimitMode controls engagement on pcm_op_long cars."""
|
||||
scenario = scenario_builder()
|
||||
scenario.set_state(SpeedLimitAssistState.disabled)
|
||||
scenario.set_engaged(True)
|
||||
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
|
||||
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
|
||||
|
||||
scenario.set_param("SpeedLimitMode", 0)
|
||||
|
||||
scenario.sla.update(
|
||||
True, False, 25 * CV.MPH_TO_MS, 0,
|
||||
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
|
||||
True, 0, scenario.events_sp
|
||||
)
|
||||
|
||||
assert scenario.sla.state == SpeedLimitAssistState.disabled
|
||||
|
||||
def test_speed_limit_final_zero_with_has_limit_edge(self, scenario_builder):
|
||||
"""Edge case #19: has_speed_limit=true but speed_limit_final=0 -> pending."""
|
||||
scenario = scenario_builder()
|
||||
scenario.set_state(SpeedLimitAssistState.capping)
|
||||
scenario.set_engaged(True)
|
||||
scenario.set_speed_limits(0, 0) # Zero speed limit (bad data)
|
||||
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
|
||||
|
||||
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
|
||||
|
||||
# has_speed_limit=True but speed_limit_final_last=0
|
||||
scenario.sla.update(
|
||||
True, False, 0, 0,
|
||||
20 * CV.MPH_TO_MS, 0, 0,
|
||||
True, 0, scenario.events_sp
|
||||
)
|
||||
|
||||
# Should transition to pending (bad/zero limit treated as no limit)
|
||||
assert scenario.sla.state == SpeedLimitAssistState.pending
|
||||
|
||||
def test_non_pcm_path_unchanged(self, scenario_builder):
|
||||
"""Regression: Non-PCM update_state_machine_non_pcm_long() unchanged."""
|
||||
scenario = scenario_builder()
|
||||
# Verify method exists and has correct signature
|
||||
assert hasattr(scenario.sla, "update_state_machine_non_pcm_long")
|
||||
# Method signature check (internal implementation detail)
|
||||
@@ -216,14 +216,6 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
Priority.LOW, VisualAlert.none, AudibleAlertSP.promptSingleHigh, 5.),
|
||||
},
|
||||
|
||||
EventNameSP.speedLimitCapActive: {
|
||||
ET.WARNING: Alert(
|
||||
"Speed Limit Capping",
|
||||
"",
|
||||
AlertStatus.normal, AlertSize.small,
|
||||
Priority.LOW, VisualAlert.none, AudibleAlertSP.promptSingleHigh, 5.),
|
||||
},
|
||||
|
||||
EventNameSP.speedLimitChanged: {
|
||||
ET.WARNING: Alert(
|
||||
"Set speed changed",
|
||||
|
||||
@@ -1126,6 +1126,14 @@
|
||||
"title": "Route Count",
|
||||
"description": ""
|
||||
},
|
||||
"ScreenSaverEnabled": {
|
||||
"title": "Enable Screen Saver",
|
||||
"description": ""
|
||||
},
|
||||
"ScreenSaverTimeout": {
|
||||
"title": "Screen Saver Timeout",
|
||||
"description": ""
|
||||
},
|
||||
"SecOCKey": {
|
||||
"title": "Sec Oc Key",
|
||||
"description": ""
|
||||
@@ -1227,41 +1235,6 @@
|
||||
"max": 30,
|
||||
"step": 1
|
||||
},
|
||||
"SpeedLimitUpshiftAccept": {
|
||||
"title": "Speed Limit Cap Upshift",
|
||||
"description": "Mode for accepting new speed limit changes in cap mode.",
|
||||
"options": [
|
||||
{
|
||||
"value": 0,
|
||||
"label": "Never Raise"
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"label": "Accel Pedal Confirm"
|
||||
}
|
||||
]
|
||||
},
|
||||
"SpeedLimitMinCapFloor": {
|
||||
"title": "Speed Limit Cap Floor",
|
||||
"description": "Minimum speed below which speed limit capping is paused.",
|
||||
"min": 0,
|
||||
"max": 40,
|
||||
"step": 1
|
||||
},
|
||||
"SpeedLimitCapAudioCue": {
|
||||
"title": "Speed Limit Cap Audio Cue",
|
||||
"description": "Enable audio cue when entering speed limit capping mode.",
|
||||
"options": [
|
||||
{
|
||||
"value": 0,
|
||||
"label": "Off"
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"label": "On"
|
||||
}
|
||||
]
|
||||
},
|
||||
"SshEnabled": {
|
||||
"title": "Enable SSH",
|
||||
"description": ""
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import matplotlib.pyplot as plt
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import numpy as np
|
||||
import base64
|
||||
import io
|
||||
|
||||
from openpilot.tools.lib.logreader import LogReader, ReadMode
|
||||
|
||||
|
||||
def extract_mem_cpu_data(lr):
|
||||
times, mems, cpus = [], [], []
|
||||
start_time = None
|
||||
|
||||
for msg in lr:
|
||||
if msg.which() == 'procLog':
|
||||
if start_time is None:
|
||||
start_time = msg.logMonoTime
|
||||
mem = msg.procLog.mem
|
||||
mem_usage = (mem.total - mem.available) / mem.total * 100
|
||||
cpu_usages = [(total - cpu.idle) / total * 100 for cpu in msg.procLog.cpuTimes
|
||||
if (total := cpu.idle + cpu.user + cpu.system + cpu.nice + cpu.iowait + cpu.irq + cpu.softirq) > 0]
|
||||
avg_cpu = sum(cpu_usages) / len(cpu_usages) if cpu_usages else 0
|
||||
times.append((msg.logMonoTime - start_time) / 1e9)
|
||||
mems.append(mem_usage)
|
||||
cpus.append(avg_cpu)
|
||||
return times, mems, cpus
|
||||
|
||||
|
||||
def process_segment(lr):
|
||||
return [extract_mem_cpu_data(lr)]
|
||||
|
||||
|
||||
def calculate_r_squared(y_true, y_pred):
|
||||
ss_res = np.sum((y_true - y_pred) ** 2)
|
||||
ss_tot = np.sum((y_true - np.mean(y_true)) ** 2)
|
||||
return 1 - (ss_res / ss_tot) if ss_tot != 0 else 0
|
||||
|
||||
|
||||
def plot_results(segments, segment_data, route_name):
|
||||
valid_data = [d for d in segment_data if d and d[0]]
|
||||
if not valid_data:
|
||||
print("No valid data to plot")
|
||||
return
|
||||
|
||||
avg_mems = [np.mean(d[1]) for d in valid_data]
|
||||
avg_cpus = [np.mean(d[2]) for d in valid_data]
|
||||
valid_segments = [segments[i] for i, d in enumerate(segment_data) if d and d[0]]
|
||||
|
||||
height = max(10, 5 + len(valid_segments) * 0.4)
|
||||
fig1, ax1 = plt.subplots(1, 1, figsize=(12, height), dpi=150)
|
||||
|
||||
y_pos = range(len(valid_segments))
|
||||
ax1.barh([y - 0.2 for y in y_pos], avg_mems, height=0.4, color="dodgerblue", alpha=0.8, label="Avg Mem %")
|
||||
ax1.barh([y + 0.2 for y in y_pos], avg_cpus, height=0.4, color="green", alpha=0.8, label="Avg CPU %")
|
||||
|
||||
for i, (mem, cpu) in enumerate(zip(avg_mems, avg_cpus, strict=True)):
|
||||
ax1.text(mem, i - 0.2, f"{mem:.1f}%", va="center", fontsize=8, color="#005a9e", fontweight="bold")
|
||||
ax1.text(cpu, i + 0.2, f"{cpu:.1f}%", va="center", fontsize=8, color="#005a9e", fontweight="bold")
|
||||
|
||||
ax1.set_yticks(y_pos)
|
||||
ax1.set_yticklabels([f"Seg {s}" for s in valid_segments])
|
||||
ax1.set_xlabel("Usage (%)")
|
||||
ax1.set_title("Average Memory and CPU Usage by Segment")
|
||||
ax1.legend()
|
||||
ax1.grid(axis="x", linestyle="--", alpha=0.5)
|
||||
ax1.invert_yaxis()
|
||||
|
||||
fig2, ax2 = plt.subplots(1, 1, figsize=(12, 8), dpi=150)
|
||||
combined_times, combined_mems, combined_cpus = [], [], []
|
||||
time_offset = 0.0
|
||||
for times, mems, cpus in valid_data:
|
||||
if times:
|
||||
combined_times.extend([t + time_offset for t in times])
|
||||
combined_mems.extend(mems)
|
||||
combined_cpus.extend(cpus)
|
||||
time_offset += max(times)
|
||||
|
||||
ax2.plot(combined_times, combined_mems, color="red", label="Memory Usage", alpha=0.6)
|
||||
ax2.plot(combined_times, combined_cpus, color="blue", label="CPU Usage", alpha=0.6)
|
||||
|
||||
warmup_sec = 60
|
||||
if len(combined_times) > 1 and combined_times[-1] > warmup_sec:
|
||||
mask = np.array(combined_times) > warmup_sec
|
||||
x_reg = np.array(combined_times)[mask]
|
||||
|
||||
y_mem_reg = np.array(combined_mems)[mask]
|
||||
slope_mem, intercept_mem = np.polyfit(x_reg, y_mem_reg, 1)
|
||||
trend_mem = slope_mem * x_reg + intercept_mem
|
||||
r2_mem = calculate_r_squared(y_mem_reg, trend_mem)
|
||||
ax2.plot(x_reg, trend_mem, color="darkred", linestyle="--", linewidth=2.5,
|
||||
label=f"Mem Trend (Slope: {slope_mem:.4f} %/s, R²: {r2_mem:.2f})")
|
||||
|
||||
y_cpu_reg = np.array(combined_cpus)[mask]
|
||||
slope_cpu, intercept_cpu = np.polyfit(x_reg, y_cpu_reg, 1)
|
||||
trend_cpu = slope_cpu * x_reg + intercept_cpu
|
||||
r2_cpu = calculate_r_squared(y_cpu_reg, trend_cpu)
|
||||
ax2.plot(x_reg, trend_cpu, color="navy", linestyle="--", linewidth=2.5,
|
||||
label=f"CPU Trend (Slope: {slope_cpu:.4f} %/s, R²: {r2_cpu:.2f})")
|
||||
|
||||
ax2.set_xlabel("Time (s)")
|
||||
ax2.set_ylabel("Usage (%)")
|
||||
ax2.set_title("Memory and CPU Usage Over Time")
|
||||
ax2.legend(loc='lower left', fontsize='small', framealpha=0.9)
|
||||
ax2.grid(True, linestyle="--", alpha=0.5)
|
||||
|
||||
buffer1 = io.BytesIO()
|
||||
fig1.savefig(buffer1, format='webp', bbox_inches='tight', pad_inches=1.0)
|
||||
buffer1.seek(0)
|
||||
img1 = base64.b64encode(buffer1.getvalue()).decode()
|
||||
|
||||
buffer2 = io.BytesIO()
|
||||
fig2.savefig(buffer2, format='webp', bbox_inches='tight', pad_inches=1.0)
|
||||
buffer2.seek(0)
|
||||
img2 = base64.b64encode(buffer2.getvalue()).decode()
|
||||
|
||||
filename = f"memory_usage_{route_name}.html"
|
||||
save_path = os.path.join(os.path.dirname(__file__), "plots", filename)
|
||||
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||
|
||||
html_template = (
|
||||
"<style>body{font-family:Arial,sans-serif;margin:20px}" +
|
||||
"h1,h2,h3{text-align:center;margin:5px 0}h2{margin-bottom:10px}" +
|
||||
"img{width:100%;max-width:800px;height:auto;display:block;margin:0 auto}</style>" +
|
||||
f"<h1>Memory Profile Report</h1><h3>Route: {route_name.replace('_', '/')}</h3>" +
|
||||
f"<img src='data:image/webp;base64,{img1}'>" +
|
||||
f"<img src='data:image/webp;base64,{img2}'>"
|
||||
)
|
||||
|
||||
plt.close(fig1)
|
||||
plt.close(fig2)
|
||||
|
||||
with open(save_path, "w") as f:
|
||||
f.write(html_template)
|
||||
|
||||
print(f"Report saved to {save_path}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Extract memory usage from route logs.')
|
||||
parser.add_argument('route_or_segment_name', help='Route or segment name from comma connect')
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
print(f"Fetching logs for {args.route_or_segment_name}")
|
||||
lr = LogReader(args.route_or_segment_name, default_mode=ReadMode.QLOG)
|
||||
segment_data = lr.run_across_segments(24, process_segment)
|
||||
segments = list(range(len(segment_data)))
|
||||
route_name = args.route_or_segment_name.replace('/', '_')
|
||||
plot_results(segments, segment_data, route_name)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,136 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import requests
|
||||
from openpilot.tools.lib.route import Route
|
||||
|
||||
|
||||
def get_segments(source, route_id, camera, seg_range):
|
||||
if "@" in source or "comma-" in source or "sunny-" in source: # SSH
|
||||
if not route_id:
|
||||
raise ValueError("route_id required for SSH")
|
||||
cmd = ["ssh", source, f"ls -d /data/media/0/realdata/{route_id.split('--')[0]}--*"]
|
||||
output = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode("utf-8").strip()
|
||||
return [{
|
||||
"type": "ssh",
|
||||
"host": source,
|
||||
"src": os.path.join(path, camera),
|
||||
"num": int(path.split("--")[-1])
|
||||
} for path in sorted(output.split("\n"), key=lambda x: int(x.split("--")[-1])) if path]
|
||||
else: # URL
|
||||
route = Route(route_id)
|
||||
cameras = [camera]
|
||||
if camera == "fcamera.hevc":
|
||||
cameras.extend([c for c in ["ecamera.hevc", "qcamera.ts"] if c != camera])
|
||||
|
||||
for cam in cameras:
|
||||
attr_name = "camera_paths" if cam == "fcamera.hevc" else f"{cam.split('.')[0]}_paths"
|
||||
paths = getattr(route, attr_name)()
|
||||
if any(paths):
|
||||
return [{"type": "url", "src": url, "num": idx, "cam": cam} for idx, url in enumerate(paths) if url]
|
||||
|
||||
raise ValueError(f"No footage found for {route_id}")
|
||||
|
||||
|
||||
def download(job, out_dir):
|
||||
destination = os.path.join(out_dir, f"{job['num']}_{os.path.basename(job.get('cam', job.get('src')))}")
|
||||
if os.path.exists(destination) and os.path.getsize(destination) > 0:
|
||||
return destination
|
||||
|
||||
print(f"Downloading segment {job['num']}")
|
||||
if job["type"] == "ssh":
|
||||
subprocess.check_call(["scp", f"{job['host']}:{job['src']}", destination])
|
||||
else:
|
||||
with requests.get(job["src"], stream=True) as r:
|
||||
r.raise_for_status()
|
||||
with open(destination, 'wb') as f:
|
||||
shutil.copyfileobj(r.raw, f)
|
||||
return destination
|
||||
|
||||
|
||||
def mux(files, output_file, codec):
|
||||
list_filename = f"{output_file}.list.txt"
|
||||
with open(list_filename, 'w') as f:
|
||||
f.write('\n'.join([f"file '{os.path.abspath(name)}'" for name in files]))
|
||||
|
||||
try:
|
||||
cmd = [
|
||||
"ffmpeg", "-y", "-probesize", "100M", "-analyzeduration", "100M", "-f", "concat",
|
||||
"-safe", "0", "-r", "20", "-i", list_filename, "-c", "copy", "-tag:v", codec, output_file
|
||||
]
|
||||
subprocess.check_call(cmd)
|
||||
print(f"Saved: {output_file} ({os.path.getsize(output_file) / 1048576:.2f} MB)")
|
||||
if sys.platform == "darwin":
|
||||
subprocess.run(["open", "-R", output_file])
|
||||
finally:
|
||||
if os.path.exists(list_filename):
|
||||
os.remove(list_filename)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("source")
|
||||
parser.add_argument("route_id", nargs='?')
|
||||
parser.add_argument("--output", "-o", default="output.mp4")
|
||||
parser.add_argument("--camera", "-c", default="fcamera.hevc")
|
||||
parser.add_argument("--keep-segments", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
route_id_str = args.route_id or args.source
|
||||
segment_range = None
|
||||
if "/" in route_id_str:
|
||||
route_id_str, range_str = route_id_str.rsplit("/", 1)
|
||||
if ":" in range_str or range_str.isdigit():
|
||||
segment_range = range_str
|
||||
|
||||
is_ssh = "@" in args.source or "comma-" in args.source or "sunny-" in args.source
|
||||
if not is_ssh and len(route_id_str.split("--")) > 2:
|
||||
route_id_str = "--".join(route_id_str.split("--")[:2])
|
||||
|
||||
segments = get_segments(args.source, route_id_str, args.camera, segment_range)
|
||||
if segment_range:
|
||||
if ":" in segment_range:
|
||||
parts = segment_range.split(":")
|
||||
start_idx = int(parts[0]) if parts[0] else None
|
||||
end_idx = int(parts[1]) if parts[1] else None
|
||||
else:
|
||||
start_idx = int(segment_range)
|
||||
end_idx = start_idx + 1
|
||||
|
||||
segments = [
|
||||
segment for segment in segments
|
||||
if (start_idx is None or segment['num'] >= start_idx) and (end_idx is None or segment['num'] < end_idx)
|
||||
]
|
||||
|
||||
download_dir = f"{route_id_str}_segments"
|
||||
os.makedirs(download_dir, exist_ok=True)
|
||||
|
||||
downloaded_files = sorted(
|
||||
[download(segment, download_dir) for segment in segments],
|
||||
key=lambda x: int(os.path.basename(x).split("_")[0])
|
||||
)
|
||||
|
||||
camera_name = segments[0].get('cam', args.camera)
|
||||
codec = "hvc1" if camera_name.endswith("hevc") else "avc1"
|
||||
mux(downloaded_files, f"{route_id_str}--{args.output}", codec)
|
||||
|
||||
if not args.keep_segments:
|
||||
shutil.rmtree(download_dir)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -338,9 +338,6 @@ def hardware_thread(end_event, hw_queue) -> None:
|
||||
show_alert = (not onroad_conditions["device_temp_good"] or not startup_conditions["device_temp_engageable"]) and onroad_conditions["ignition"]
|
||||
set_offroad_alert_if_changed("Offroad_TemperatureTooHigh", show_alert, extra_text=extra_text)
|
||||
|
||||
if show_alert:
|
||||
msg.deviceState.fanSpeedPercentDesired = 100
|
||||
|
||||
# Handle offroad/onroad transition
|
||||
should_start = all(onroad_conditions.values())
|
||||
if started_ts is None:
|
||||
@@ -438,10 +435,9 @@ def hardware_thread(end_event, hw_queue) -> None:
|
||||
statlog.gauge("fan_speed_percent_desired", msg.deviceState.fanSpeedPercentDesired)
|
||||
statlog.gauge("screen_brightness_percent", msg.deviceState.screenBrightnessPercent)
|
||||
|
||||
# report to server once every 10 minutes, or every 1s when thermally blocked
|
||||
# report to server once every 10 minutes
|
||||
rising_edge_started = should_start and not should_start_prev
|
||||
status_packet_interval = 1. if show_alert else 600.
|
||||
if rising_edge_started or (count % int(status_packet_interval / DT_HW)) == 0:
|
||||
if rising_edge_started or (count % int(600. / DT_HW)) == 0:
|
||||
dat = {
|
||||
'count': count,
|
||||
'pandaStates': [strip_deprecated_keys(p.to_dict()) for p in pandaStates],
|
||||
|
||||
@@ -60,19 +60,17 @@ class OptionControlSP(ItemAction):
|
||||
|
||||
def set_value(self, value: int):
|
||||
"""Set the control to a specific value"""
|
||||
if not (self.min_value <= value <= self.max_value):
|
||||
return
|
||||
if value == self.current_value:
|
||||
return
|
||||
self.current_value = value
|
||||
if self.value_map:
|
||||
self.params.put(self.param_key, self.value_map[value])
|
||||
elif self.use_float_scaling:
|
||||
self.params.put(self.param_key, value / 100.0)
|
||||
else:
|
||||
self.params.put(self.param_key, value)
|
||||
if self.on_value_changed:
|
||||
self.on_value_changed(value)
|
||||
if self.min_value <= value <= self.max_value:
|
||||
self.current_value = value
|
||||
if self.value_map:
|
||||
self.params.put(self.param_key, self.value_map[value])
|
||||
else:
|
||||
if self.use_float_scaling:
|
||||
self.params.put(self.param_key, value / 100.0)
|
||||
else:
|
||||
self.params.put(self.param_key, value)
|
||||
if self.on_value_changed:
|
||||
self.on_value_changed(value)
|
||||
|
||||
def get_displayed_value(self) -> str:
|
||||
"""Get the displayed value, handling value mapping if present"""
|
||||
@@ -159,10 +157,10 @@ class OptionControlSP(ItemAction):
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
if self._minus_enabled and rl.check_collision_point_rec(mouse_pos, self.minus_btn_rect):
|
||||
new_value = self.current_value - self.value_change_step
|
||||
new_value = max(self.min_value, new_value)
|
||||
self.set_value(new_value)
|
||||
self.current_value -= self.value_change_step
|
||||
self.current_value = max(self.min_value, self.current_value)
|
||||
elif self._plus_enabled and rl.check_collision_point_rec(mouse_pos, self.plus_btn_rect):
|
||||
new_value = self.current_value + self.value_change_step
|
||||
new_value = min(self.max_value, new_value)
|
||||
self.set_value(new_value)
|
||||
self.current_value += self.value_change_step
|
||||
self.current_value = min(self.max_value, self.current_value)
|
||||
|
||||
self.set_value(self.current_value)
|
||||
|
||||
108
system/ui/sunnypilot/widgets/screen_saver.py
Normal file
108
system/ui/sunnypilot/widgets/screen_saver.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import os
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
class ScreenSaverSP(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
self.screensaver_timeout = Params().get("ScreenSaverTimeout", return_default=True)
|
||||
self._is_mici = HARDWARE.get_device_type() == 'mici' or (HARDWARE.get_device_type() == "pc" and os.getenv("BIG") != "1")
|
||||
|
||||
self.x = 0.0
|
||||
self.y = 100.0
|
||||
self.vx = 120.0 if self._is_mici else 300.0
|
||||
self.vy = 70.0 if self._is_mici else 200.0
|
||||
self._hue = 150
|
||||
self.color = rl.color_from_hsv(self._hue, 1, 1)
|
||||
|
||||
self.text = "sunnypilot"
|
||||
self.font_size = 50 if self._is_mici else 200
|
||||
self._start_time = None
|
||||
self._dismiss = False
|
||||
self.dismiss_callback = None
|
||||
|
||||
@property
|
||||
def was_dismissed(self) -> bool:
|
||||
return self._dismiss
|
||||
|
||||
def initialize(self, dismiss_callback: Callable):
|
||||
if self._start_time is None:
|
||||
self._start_time = time.monotonic()
|
||||
self.dismiss_callback = dismiss_callback
|
||||
self._dismiss = False
|
||||
|
||||
def deinit(self):
|
||||
self._dismiss = False
|
||||
self._start_time = None
|
||||
|
||||
def _handle_mouse_press(self, mouse_pos):
|
||||
self._reset()
|
||||
return super()._handle_mouse_press(mouse_pos)
|
||||
|
||||
def _reset(self):
|
||||
self._dismiss = True
|
||||
self._start_time = None
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
self.font = gui_app.font(FontWeight.AUDIOWIDE)
|
||||
text_size = measure_text_cached(self.font, self.text, self.font_size, 0)
|
||||
self.logo_width = text_size.x
|
||||
self.logo_height = text_size.y
|
||||
|
||||
if self._start_time and time.monotonic() - self._start_time > self.screensaver_timeout:
|
||||
self._reset()
|
||||
|
||||
dt = rl.get_frame_time()
|
||||
|
||||
self.x += self.vx * dt
|
||||
self.y += self.vy * dt
|
||||
|
||||
hit_x = hit_y = False
|
||||
if self.x + self.logo_width > self.rect.width:
|
||||
self.vx *= -1
|
||||
self.x = self.rect.width - self.logo_width
|
||||
hit_x = True
|
||||
elif self.x < 0:
|
||||
self.vx *= -1
|
||||
self.x = 0
|
||||
hit_x = True
|
||||
|
||||
if self.y + self.logo_height > self.rect.height:
|
||||
self.vy *= -1
|
||||
self.y = self.rect.height - self.logo_height
|
||||
hit_y = True
|
||||
elif self.y < 0:
|
||||
self.vy *= -1
|
||||
self.y = 0
|
||||
hit_y = True
|
||||
|
||||
def hue_dist(a, b):
|
||||
d = abs(a - b)
|
||||
return min(d, 360 - d)
|
||||
|
||||
if hit_x or hit_y:
|
||||
while hue_dist((new_hue := rl.get_random_value(0, 360)), self._hue) < 90:
|
||||
pass
|
||||
self._hue = new_hue
|
||||
self.color = rl.color_from_hsv(self._hue, 1, 1)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
if self._dismiss:
|
||||
self.dismiss_callback()
|
||||
return 0
|
||||
|
||||
self.set_rect(rect)
|
||||
rl.clear_background(rl.BLACK)
|
||||
rl.draw_text_ex(self.font, self.text, rl.Vector2(int(self.x), int(self.y)), self.font_size, 0, self.color)
|
||||
return -1
|
||||
@@ -198,10 +198,7 @@ class TreeOptionDialog(MultiOptionDialog):
|
||||
|
||||
self.option_buttons = self.visible_items
|
||||
self.options = [item.text for item in self.visible_items]
|
||||
# Rebuild scroller items to ensure proper setup of touch callbacks
|
||||
self.scroller._items.clear()
|
||||
for item in self.option_buttons:
|
||||
self.scroller.add_widget(item)
|
||||
self.scroller._items = self.visible_items
|
||||
if reset_scroll:
|
||||
self.scroller.scroll_panel.set_offset(0)
|
||||
|
||||
|
||||
@@ -33,6 +33,6 @@ fi
|
||||
|
||||
# Build _cabana
|
||||
cd "$ROOT"
|
||||
scons -j4 tools/cabana/_cabana cereal/messaging/bridge
|
||||
scons -j4 tools/cabana/_cabana
|
||||
|
||||
exec "$DIR/_cabana" "$@"
|
||||
|
||||
@@ -106,8 +106,8 @@ cereal::PandaState::PandaType Panda::get_hw_type() {
|
||||
|
||||
|
||||
|
||||
void Panda::send_heartbeat(bool engaged, bool engaged_mads) {
|
||||
control_write(0xf3, engaged, engaged_mads);
|
||||
void Panda::send_heartbeat(bool engaged) {
|
||||
control_write(0xf3, engaged, 0);
|
||||
}
|
||||
|
||||
void Panda::set_can_speed_kbps(uint16_t bus, uint16_t speed) {
|
||||
|
||||
@@ -64,7 +64,7 @@ public:
|
||||
// Panda functionality
|
||||
cereal::PandaState::PandaType get_hw_type();
|
||||
void set_safety_model(cereal::CarParams::SafetyModel safety_model, uint16_t safety_param=0U);
|
||||
void send_heartbeat(bool engaged, bool engaged_mads = false);
|
||||
void send_heartbeat(bool engaged);
|
||||
void set_can_speed_kbps(uint16_t bus, uint16_t speed);
|
||||
void set_data_speed_kbps(uint16_t bus, uint16_t speed);
|
||||
bool can_receive(std::vector<can_frame>& out_vec);
|
||||
|
||||
@@ -62,7 +62,7 @@ class GithubUtils:
|
||||
self.api_call(github_path, data=data, method=HTTPMethod.POST, data_call=True)
|
||||
|
||||
def get_bucket_sha(self, bucket):
|
||||
github_path = f"git/ref/heads/{bucket}"
|
||||
github_path = f"git/refs/heads/{bucket}"
|
||||
r = self.api_call(github_path, data_call=True, raise_on_failure=False)
|
||||
return r.json()['object']['sha'] if r.ok else None
|
||||
|
||||
|
||||
@@ -92,8 +92,6 @@ class SimulatedCar:
|
||||
'ignitionLine': simulator_state.ignition,
|
||||
'pandaType': "blackPanda",
|
||||
'controlsAllowed': True,
|
||||
'controlsAllowedLateral': True,
|
||||
'controlsAllowedLongitudinal': True,
|
||||
'safetyModel': 'hondaBosch',
|
||||
'alternativeExperience': self.sm["carParams"].alternativeExperience,
|
||||
'safetyParam': HondaSafetyFlags.RADARLESS.value | HondaSafetyFlags.BOSCH_LONG.value,
|
||||
|
||||
Reference in New Issue
Block a user