Compare commits

..

91 Commits

Author SHA1 Message Date
nayan
acb4bb848b solve for lint 2026-04-12 23:06:56 -04:00
nayan
0cd913d7ac better 2026-04-12 22:57:41 -04:00
nayan
7fd1fea3b0 lint 2026-04-12 22:39:29 -04:00
nayan
e79c3243b2 fix 2026-04-12 19:24:27 -04:00
nayan
a936c311d7 Merge remote-tracking branch 'origin/master' into screensaver
# Conflicts:
#	selfdrive/ui/sunnypilot/layouts/settings/display.py
2026-04-12 19:21:05 -04:00
nayan
8e53fe176d better 2026-02-21 23:18:47 -05:00
nayan
22a12aec35 Merge remote-tracking branch 'origin/master' into screensaver
# Conflicts:
#	common/params_keys.h
#	selfdrive/ui/sunnypilot/layouts/settings/display.py
#	selfdrive/ui/sunnypilot/ui_state.py
#	selfdrive/ui/ui_state.py
2026-02-21 22:59:38 -05:00
nayan
426c2abd61 use ui_state param 2026-01-04 14:37:07 -05:00
Nayan
1a84559cec Merge branch 'master' into screensaver 2026-01-04 14:16:28 -05:00
nayan
65b61fd965 handle default 2026-01-03 14:01:28 -05:00
nayan
a01449822f Merge branch 'rl-display-panel' into screensaver 2026-01-03 13:34:08 -05:00
nayan
f52db76444 inline everything again 2026-01-03 13:31:56 -05:00
Nayan
b61f095ef2 Merge branch 'master' into rl-display-panel 2026-01-03 13:29:18 -05:00
DevTekVE
59e5fd7fe6 Merge branch 'master' into screensaver 2026-01-03 16:35:10 +01:00
Nayan
607811b6ad Merge branch 'master' into screensaver 2025-12-08 15:52:52 -05:00
nayan
2414fe287d fix 2025-12-08 15:13:26 -05:00
nayan
f7a4dbc47f changes 2025-12-07 12:53:18 -05:00
nayan
fd0a5f806c ugh 2025-12-05 14:23:25 -05:00
nayan
de62240e97 refresh controls 2025-12-05 13:41:35 -05:00
nayan
adba29da1b Merge remote-tracking branch 'origin/master' into rl-display-panel 2025-12-05 13:03:16 -05:00
nayan
6d6a91c318 really @devtekve? REALLY?? 2025-12-04 17:38:16 -05:00
nayan
7259687578 it wasn't me 2025-12-04 17:31:34 -05:00
nayan
83a07e4bfa we doing this now @devtekve? FINE!! 2025-12-04 15:44:04 -05:00
nayan
cb65777186 not doing this 2025-12-04 15:36:12 -05:00
Nayan
282b867fcb Merge branch 'master' into screensaver 2025-12-04 15:34:36 -05:00
nayan
3fd3c31966 add toggle 2025-12-04 15:33:45 -05:00
nayan
f3c3581472 Merge branch 'rl-display-panel' into screensaver 2025-12-04 15:08:28 -05:00
nayan
465daf323c add param, timeout 2025-12-04 15:03:31 -05:00
nayan
3fe8e155b6 better 2025-12-03 23:15:11 -05:00
nayan
c0bffa2a8c oops 2025-12-03 22:57:14 -05:00
nayan
c9edcaa1a6 why? because! 2025-12-03 22:48:17 -05:00
DevTekVE
76101207cb Removed hide for now 2025-11-30 12:51:16 +01:00
DevTekVE
62b01d3799 Merge branch 'master' into rl-display-panel
# Conflicts:
#	selfdrive/ui/layouts/main.py
#	selfdrive/ui/layouts/settings/toggles.py
#	selfdrive/ui/sunnypilot/layouts/settings/cruise.py
#	selfdrive/ui/sunnypilot/layouts/settings/device.py
#	selfdrive/ui/sunnypilot/layouts/settings/display.py
#	selfdrive/ui/sunnypilot/layouts/settings/models.py
#	selfdrive/ui/sunnypilot/layouts/settings/navigation.py
#	selfdrive/ui/sunnypilot/layouts/settings/osm.py
#	selfdrive/ui/sunnypilot/layouts/settings/settings.py
#	selfdrive/ui/sunnypilot/layouts/settings/steering.py
#	selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py
#	selfdrive/ui/sunnypilot/layouts/settings/trips.py
#	selfdrive/ui/sunnypilot/layouts/settings/vehicle.py
#	selfdrive/ui/sunnypilot/layouts/settings/visuals.py
#	selfdrive/ui/sunnypilot/ui_state.py
#	selfdrive/ui/tests/test_ui/raylib_screenshots.py
#	system/ui/sunnypilot/lib/application.py
#	system/ui/sunnypilot/lib/styles.py
#	system/ui/sunnypilot/widgets/list_view.py
#	system/ui/sunnypilot/widgets/option_control.py
#	system/ui/sunnypilot/widgets/toggle.py
2025-11-30 12:50:39 +01:00
nayan
a3be1edde7 optimizations 2025-11-22 23:48:06 -05:00
nayan
7869542683 Merge branch 'py-ui-state-sp' into rl-display-panel 2025-11-21 18:16:47 -05:00
nayan
981aab0500 Merge branch 'rl-sp-panels' into rl-display-panel 2025-11-21 18:16:43 -05:00
nayan
9c82592906 Merge branch 'rl-sp-toggles' into rl-display-panel 2025-11-21 18:16:40 -05:00
nayan
5d3f95d420 use gui_app.sunnypilot_ui() 2025-11-21 18:08:19 -05:00
nayan
f8d19fe9dd Merge branch 'rl-sp-toggles' into rl-sp-panels 2025-11-21 18:07:19 -05:00
nayan
9d711350c2 Merge remote-tracking branch 'origin/ui-gui-app-ext' into rl-sp-panels 2025-11-21 18:07:03 -05:00
nayan
45c853c87a Merge remote-tracking branch 'origin/ui-gui-app-ext' into py-ui-state-sp 2025-11-21 17:57:35 -05:00
nayan
e8ab9d812d use gui_app.sunnypilot_ui() 2025-11-21 17:57:06 -05:00
nayan
ed775185f2 use gui_app.sunnypilot_ui() 2025-11-21 17:49:27 -05:00
nayan
7bbbc6588e Merge remote-tracking branch 'origin/ui-gui-app-ext' into rl-sp-toggles 2025-11-21 17:42:23 -05:00
nayan
e68c65d15d Merge remote-tracking branch 'origin/master' into rl-sp-toggles 2025-11-21 17:40:10 -05:00
Jason Wen
0db8722221 Merge branch 'master' into ui-gui-app-ext 2025-11-21 17:24:14 -05:00
Jason Wen
a33497ed19 add to readme 2025-11-21 16:42:59 -05:00
Jason Wen
91f2bf3459 ui: GuiApplicationExt 2025-11-21 16:23:01 -05:00
Jason Wen
7fad2fc189 Merge branch 'master' into rl-sp-toggles 2025-11-21 15:55:34 -05:00
nayan
e74252bdf5 Merge remote-tracking branch 'origin/master' into py-ui-state-sp 2025-11-21 15:37:33 -05:00
nayan
6b795ab513 Merge branch 'rl-sp-panels' into rl-display-panel
# Conflicts:
#	selfdrive/ui/sunnypilot/layouts/settings/display.py
2025-11-21 15:30:40 -05:00
nayan
3b1b4ef90f Merge remote-tracking branch 'origin/master' into rl-display-panel 2025-11-21 15:30:13 -05:00
nayan
48d33e98e7 scroller -> scroller_tici 2025-11-21 15:27:51 -05:00
nayan
2717d97350 scroller -> scroller_tici 2025-11-21 15:20:31 -05:00
nayan
64232397ed Merge remote-tracking branch 'origin/master' into rl-sp-panels 2025-11-21 15:16:50 -05:00
Jason Wen
0613442ac9 Merge branch 'master' into rl-sp-toggles 2025-11-21 15:00:14 -05:00
nayan
93ab6ee06b lint 2025-11-20 23:49:57 -05:00
nayan
4baa170cfa hide all controls 2025-11-20 23:46:16 -05:00
nayan
2be95225b1 add all controls 2025-11-20 23:41:15 -05:00
nayan
f8e8c59dd5 option control value fix 2025-11-20 23:34:24 -05:00
nayan
dd9d4bedf4 init 2025-11-20 22:58:55 -05:00
nayan
ada449989b Merge branch 'rl-sp-optioncontrol' into rl-display-panel 2025-11-20 22:43:51 -05:00
nayan
68c7f69439 AAARGGGGGG..... 2025-11-20 22:18:04 -05:00
nayan
1db71aff57 I. SAID. SIMPLIFY. 2025-11-20 22:15:43 -05:00
nayan
942ccb90dd simplify 2025-11-20 22:08:06 -05:00
nayan
e6f5aae246 remove padding from line separator.
like, WHY? 😩😩
2025-11-20 18:05:12 -05:00
nayan
7032e4a972 add show_description method 2025-11-20 18:00:44 -05:00
nayan
ffa78eabaa Revert "add ui_update callback"
This reverts commit 4da32cc009.
2025-11-20 17:58:19 -05:00
nayan
5b03369a8f listitem -> listitemsp 2025-11-20 17:56:26 -05:00
nayan
1e0564b484 this 2025-11-20 08:05:20 -05:00
nayan
eb94abaa14 better padding 2025-11-19 23:44:05 -05:00
nayan
4da32cc009 add ui_update callback 2025-11-19 23:03:56 -05:00
nayan
4820265268 better 2025-11-18 19:09:46 -05:00
nayan
01aa6c4204 param to control stock vs sp ui 2025-11-18 18:51:52 -05:00
nayan
21beea51ec introducing ui_state_sp for py 2025-11-18 16:26:48 -05:00
nayan
98c479830c Need this 2025-11-16 14:00:37 -05:00
nayan
5369b6880f Option Control 2025-11-16 13:58:14 -05:00
nayan
a9e57f0a76 add ui previews 2025-11-16 12:47:37 -05:00
nayan
712a358c94 Merge branch 'rl-sp-toggles' into rl-sp-panels 2025-11-16 11:23:31 -05:00
nayan
423a7d2ed0 fix ui preview 2025-11-16 11:15:28 -05:00
nayan
e4e10d4b87 fix callback 2025-11-16 11:15:22 -05:00
nayan
632e9d13b2 Merge branch 'rl-sp-toggles' into rl-sp-panels 2025-11-16 09:55:25 -05:00
nayan
362e9ce04b sp raylib preview 2025-11-16 09:53:28 -05:00
nayan
51d0666c85 more patience, grasshopper 2025-11-16 09:33:35 -05:00
nayan
deda1329a2 patience, grasshopper 2025-11-16 09:33:35 -05:00
nayan
4110749cb0 Panels. With Icons. And Scroller. 2025-11-16 09:33:35 -05:00
nayan
3946e643f6 optimizations 2025-11-16 09:29:58 -05:00
nayan
0c37a38596 Lint 2025-11-16 09:29:58 -05:00
nayan
9c5acf61c0 SP Toggles 2025-11-16 09:29:58 -05:00
nayan
121b304fe0 init styles 2025-11-16 09:29:58 -05:00
nayan
47d848293b param to control stock vs sp ui 2025-11-16 09:29:58 -05:00
60 changed files with 467 additions and 2338 deletions

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -1 +1 @@
#define DEFAULT_MODEL "POP model (Default)"
#define DEFAULT_MODEL "OP Model 7 (Default)"

View File

@@ -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}},

2
panda

Submodule panda updated: c0cc96fbad...6cd1972ecf

View File

@@ -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})", "|")

View File

@@ -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)

View File

@@ -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

View File

@@ -1 +0,0 @@
driving_policy.onnx

View File

@@ -1 +0,0 @@
driving_vision.onnx

Binary file not shown.

Binary file not shown.

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()

View File

@@ -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"))

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -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)

View File

@@ -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)

View File

@@ -1 +1 @@
5d4d21f1899de21137f69d74a4602c44cc5a6b04cf4e4aa9d0ec9206f8c30350
793b5d480edb5a30eed3d0d3bdb43259522978670f6bc3dea7a4d661261d3c48

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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
}

View File

@@ -27,8 +27,3 @@ class Mode(IntEnumBase):
information = 1
warning = 2
assist = 3
class UpshiftAccept(IntEnumBase):
NEVER_RAISE = 0
ACCEL_PEDAL = 1

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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",

View File

@@ -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": ""

View File

@@ -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()

View File

@@ -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()

View File

@@ -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],

View File

@@ -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)

View 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

View File

@@ -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)

View File

@@ -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" "$@"

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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

View File

@@ -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,