diff --git a/cereal/log.capnp b/cereal/log.capnp index 50777e4fb..9d4ca5e80 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -560,8 +560,8 @@ struct PandaState @0xa7649e2575e4591e { # these fields are not used by openpilot, but they're # reserved for forks building alternate experiences. - controlsAllowedRESERVED1 @38 :Bool; - controlsAllowedRESERVED2 @39 :Bool; + controlsAllowedLateral @38 :Bool; + controlsAllowedLongitudinal @39 :Bool; enum FaultStatus { none @0; diff --git a/common/model.h b/common/model.h index fc1110431..d134ebd15 100644 --- a/common/model.h +++ b/common/model.h @@ -1 +1 @@ -#define DEFAULT_MODEL "OP Model 7 (Default)" +#define DEFAULT_MODEL "POP model (Default)" diff --git a/opendbc_repo b/opendbc_repo index 8d529c156..2b538cde7 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 8d529c156600ff68308d2f5adf4fbec934ebd483 +Subproject commit 2b538cde7cc39c5c3be7d34dbf44ecc06bc64611 diff --git a/panda b/panda index 1681ede8d..821baec41 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 1681ede8dba76eec13cc303b957d134b6a269750 +Subproject commit 821baec41b98bf5b65bbd43276405f708ff8aba1 diff --git a/scripts/reporter.py b/scripts/reporter.py index 64f6cb99b..93b71761a 100755 --- a/scripts/reporter.py +++ b/scripts/reporter.py @@ -33,16 +33,7 @@ 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_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)" + master = get_checkpoint(MASTER_PATH + MODEL_PATH + fn) pr = get_checkpoint(BASEDIR + MODEL_PATH + fn) - print("|", fn, "|", master_col, "|", f"[{pr}](https://reporter.comma.life/experiment/{pr})", "|") + print("|", fn, "|", f"[{master}](https://reporterv2.comma.life/{master})", "|", f"[{pr}](https://reporterv2.comma.life/{pr})", "|") diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript index 7a82ff88b..f02e66722 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -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_off_policy', 'driving_on_policy', 'dmonitoring_model']: +for model_name in ['driving_vision', 'driving_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,5 +59,6 @@ def tg_compile(flags, model_name): ) # Compile small models -for model_name in ['driving_vision', 'driving_off_policy', 'driving_on_policy', 'dmonitoring_model']: +for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: tg_compile(tg_flags, model_name) + diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index df77c9c0e..d774e475d 100755 --- a/selfdrive/modeld/modeld.py +++ b/selfdrive/modeld/modeld.py @@ -40,10 +40,8 @@ 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' -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' +POLICY_PKL_PATH = MODELS_DIR / 'driving_policy_tinygrad.pkl' +POLICY_METADATA_PATH = MODELS_DIR / 'driving_policy_metadata.pkl' LAT_SMOOTH_SECONDS = 0.0 LONG_SMOOTH_SECONDS = 0.3 @@ -158,13 +156,7 @@ class ModelState(ModelStateBase): self.vision_output_slices = vision_metadata['output_slices'] vision_output_size = vision_metadata['output_shapes']['outputs'][1] - 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: + with open(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'] @@ -188,13 +180,11 @@ 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(ON_POLICY_PKL_PATH))) - self.off_policy_run = pickle.loads(read_file_chunked(str(OFF_POLICY_PKL_PATH))) + self.policy_run = pickle.loads(read_file_chunked(str(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()} @@ -243,17 +233,9 @@ 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)) - - 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'] + combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict} if SEND_RAW_PRED: - combined_outputs_dict['raw_pred'] = np.concatenate([self.vision_output.copy(), self.policy_output.copy(), self.off_policy_output.copy()]) + combined_outputs_dict['raw_pred'] = np.concatenate([self.vision_output.copy(), self.policy_output.copy()]) return combined_outputs_dict diff --git a/selfdrive/modeld/models/big_driving_policy.onnx b/selfdrive/modeld/models/big_driving_policy.onnx new file mode 120000 index 000000000..e1b653a14 --- /dev/null +++ b/selfdrive/modeld/models/big_driving_policy.onnx @@ -0,0 +1 @@ +driving_policy.onnx \ No newline at end of file diff --git a/selfdrive/modeld/models/big_driving_vision.onnx b/selfdrive/modeld/models/big_driving_vision.onnx new file mode 120000 index 000000000..28ee71dd7 --- /dev/null +++ b/selfdrive/modeld/models/big_driving_vision.onnx @@ -0,0 +1 @@ +driving_vision.onnx \ No newline at end of file diff --git a/selfdrive/modeld/models/driving_off_policy.onnx b/selfdrive/modeld/models/driving_off_policy.onnx deleted file mode 100644 index 5b0effc10..000000000 --- a/selfdrive/modeld/models/driving_off_policy.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e53f4e0527766082ba7bde38e275def0fe3c14f6c59ae2854439e239884d3ecc -size 13393365 diff --git a/selfdrive/modeld/models/driving_on_policy.onnx b/selfdrive/modeld/models/driving_on_policy.onnx deleted file mode 100644 index bfe10ca18..000000000 --- a/selfdrive/modeld/models/driving_on_policy.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ea89c50da3a16e710da292f97c81b083a982cfdee5c28eca0d37ed2fb99af6c5 -size 13022642 diff --git a/selfdrive/modeld/models/driving_policy.onnx b/selfdrive/modeld/models/driving_policy.onnx new file mode 100644 index 000000000..7c71bc947 --- /dev/null +++ b/selfdrive/modeld/models/driving_policy.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:853c6634746ff439a848349d00e4d5581cd941f13f7c1862c31b72a31cc24858 +size 14061595 diff --git a/selfdrive/modeld/models/driving_vision.onnx b/selfdrive/modeld/models/driving_vision.onnx index 335e9dbcf..afd617667 100644 --- a/selfdrive/modeld/models/driving_vision.onnx +++ b/selfdrive/modeld/models/driving_vision.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6263aa3fbb44cde6c68a34cdb7cd8c389789dbc02b15c1911afdac4e018281ae -size 23267151 +oid sha256:940e9006a25f27f0b6e85da798e6a8fd1f6dd492dd7d0b9ff1a9436460f46129 +size 46887794 diff --git a/selfdrive/modeld/parse_model_outputs.py b/selfdrive/modeld/parse_model_outputs.py index 802f0ad85..a0b45d2a9 100644 --- a/selfdrive/modeld/parse_model_outputs.py +++ b/selfdrive/modeld/parse_model_outputs.py @@ -96,17 +96,11 @@ 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) @@ -116,7 +110,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: @@ -126,6 +120,5 @@ 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 diff --git a/selfdrive/pandad/pandad.cc b/selfdrive/pandad/pandad.cc index 3d0a551d8..cafc3e122 100644 --- a/selfdrive/pandad/pandad.cc +++ b/selfdrive/pandad/pandad.cc @@ -21,8 +21,6 @@ #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) { @@ -34,15 +32,8 @@ 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(); - const bool heartbeat_type = disengage_lateral_on_brake ? mads.getActive() : mads.getEnabled(); - - const bool engaged = sm->allAliveAndValid({"selfdriveStateSP"}) && heartbeat_type; - - return engaged; + return sm->allAliveAndValid({"selfdriveStateSP"}) && mads.getEnabled(); } Panda *connect(std::string serial) { @@ -152,6 +143,8 @@ 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) { @@ -380,7 +373,7 @@ void pandad_run(Panda *panda) { Params params; RateKeeper rk("pandad", 100); - SubMaster sm({"selfdriveState", "selfdriveStateSP", "carParams"}); + SubMaster sm({"selfdriveState", "selfdriveStateSP"}); PubMaster pm({"can", "pandaStates", "peripheralState"}); PandaSafety panda_safety(panda); bool engaged = false; diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index 2f41e1f17..e7dfd3410 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -13,7 +13,6 @@ 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 diff --git a/selfdrive/ui/sunnypilot/layouts/settings/display.py b/selfdrive/ui/sunnypilot/layouts/settings/display.py index acd7b52dc..8ba566366 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/display.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/display.py @@ -6,12 +6,10 @@ 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, ToggleActionSP +from openpilot.system.ui.sunnypilot.widgets.list_view import option_item_sp from openpilot.sunnypilot.system.params_migration import ONROAD_BRIGHTNESS_TIMER_VALUES @@ -25,7 +23,6 @@ class DisplayLayout(Widget): def __init__(self): super().__init__() - self._params = Params() items = self._initialize_items() self._scroller = Scroller(items, line_separator=True, spacing=0) @@ -87,17 +84,7 @@ class DisplayLayout(Widget): def _update_state(self): super()._update_state() - 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) + brightness_val = self._onroad_brightness.action_item.current_value self._onroad_brightness_timer.action_item.set_enabled(brightness_val not in (OnroadBrightness.AUTO, OnroadBrightness.AUTO_DARK)) def _render(self, rect): diff --git a/selfdrive/ui/sunnypilot/layouts/settings/models.py b/selfdrive/ui/sunnypilot/layouts/settings/models.py index b34604af0..adbd99f1f 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/models.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/models.py @@ -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() diff --git a/selfdrive/ui/sunnypilot/mici/layouts/models.py b/selfdrive/ui/sunnypilot/mici/layouts/models.py index d8da750fe..331743416 100644 --- a/selfdrive/ui/sunnypilot/mici/layouts/models.py +++ b/selfdrive/ui/sunnypilot/mici/layouts/models.py @@ -5,13 +5,45 @@ 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.ui_state import ui_state +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.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): @@ -20,25 +52,35 @@ class ModelsLayoutMici(NavScroller): self.original_back_callback = back_callback self.focused_widget = None - self.current_model_btn = BigButton(tr("current model")) - self.current_model_btn.set_click_callback(self._show_folders) + 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.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_btn, self.cancel_download_btn] + self.main_items = [self.current_model_info, self.select_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): + def _get_grouped_bundles(self, favorites = None): 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): @@ -49,18 +91,25 @@ class ModelsLayoutMici(NavScroller): self.set_back_callback(back_callback) def _show_folders(self): - self.focused_widget = self.current_model_btn - folders = self._get_grouped_bundles() + 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) 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"]: + if folder.lower() in ["release models", "master models", "favorites"]: btn = BigButton(folder.lower()) btn.set_click_callback(lambda f=folder: self._select_folder(f)) - folder_buttons.append(btn) + if folder.lower() == "favorites": + folder_buttons.insert(0, btn) + else: + folder_buttons.append(btn) self._show_selection_view(folder_buttons, self._reset_main_view) def _select_model(self, bundle): @@ -72,7 +121,10 @@ class ModelsLayoutMici(NavScroller): self._reset_main_view() def _select_folder(self, folder_name): - folders = self._get_grouped_bundles() + favs = ui_state.params.get("ModelManager_Favs") + favorites = set(favs.split(';')) if favs else set() + + folders = self._get_grouped_bundles(favorites) bundles = sorted(folders.get(folder_name, []), key=lambda b: b.index, reverse=True) btns = [] @@ -86,29 +138,62 @@ class ModelsLayoutMici(NavScroller): def _reset_main_view(self): self._scroller._items = self.main_items self.set_back_callback(self.original_back_callback) - 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) + 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 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 - if manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading: - self.current_model_btn.set_value("downloading...") + 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: self.cancel_download_btn.set_visible(True) - 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")) + 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}%") + diff --git a/selfdrive/ui/sunnypilot/mici/layouts/settings.py b/selfdrive/ui/sunnypilot/mici/layouts/settings.py index 9e160521c..85a782ecb 100644 --- a/selfdrive/ui/sunnypilot/mici/layouts/settings.py +++ b/selfdrive/ui/sunnypilot/mici/layouts/settings.py @@ -5,18 +5,32 @@ 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.widgets.button import BigButton +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.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)) @@ -25,10 +39,53 @@ 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) diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py index 7766c353a..2a48e9eee 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -146,6 +146,7 @@ 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") class DeviceSP: diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 7eab55e6e..0c87da4a2 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -33,6 +33,7 @@ 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 @@ -104,6 +105,17 @@ 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: @@ -186,6 +198,9 @@ 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) @@ -195,6 +210,8 @@ class ModularAssistiveDrivingSystem: if not self.enabled_toggle: return + self.data_sample() + self.update_events(CS) if not self.CP.passive and self.selfdrive.initialized: diff --git a/sunnypilot/modeld_v2/SConscript b/sunnypilot/modeld_v2/SConscript index ddf889c0c..8526fd272 100644 --- a/sunnypilot/modeld_v2/SConscript +++ b/sunnypilot/modeld_v2/SConscript @@ -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_policy']: + for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_on_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_policy']: +for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_on_policy', 'driving_policy']: if File(f"models/{model_name}.onnx").exists(): tg_compile(tg_flags, model_name) diff --git a/sunnypilot/models/default_model.py b/sunnypilot/models/default_model.py index d540efbff..0260a3c3b 100755 --- a/sunnypilot/models/default_model.py +++ b/sunnypilot/models/default_model.py @@ -8,16 +8,14 @@ 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") -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") +POLICY_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_policy.onnx") def update_model_hash(): vision_hash = get_file_hash(VISION_ONNX_PATH) - off_policy_hash = get_file_hash(OFF_POLICY_ONNX_PATH) - on_policy_hash = get_file_hash(ON_POLICY_ONNX_PATH) + policy_hash = get_file_hash(POLICY_ONNX_PATH) - combined_hash = hashlib.sha256((vision_hash + off_policy_hash + on_policy_hash).encode()).hexdigest() + combined_hash = hashlib.sha256((vision_hash + policy_hash).encode()).hexdigest() with open(MODEL_HASH_PATH, "w") as f: f.write(combined_hash) diff --git a/sunnypilot/models/tests/model_hash b/sunnypilot/models/tests/model_hash index 3fdf97dd1..eaf923358 100644 --- a/sunnypilot/models/tests/model_hash +++ b/sunnypilot/models/tests/model_hash @@ -1 +1 @@ -793b5d480edb5a30eed3d0d3bdb43259522978670f6bc3dea7a4d661261d3c48 +5d4d21f1899de21137f69d74a4602c44cc5a6b04cf4e4aa9d0ec9206f8c30350 \ No newline at end of file diff --git a/sunnypilot/models/tests/test_default_model.py b/sunnypilot/models/tests/test_default_model.py index abe685c36..7c2fde70a 100644 --- a/sunnypilot/models/tests/test_default_model.py +++ b/sunnypilot/models/tests/test_default_model.py @@ -6,17 +6,16 @@ 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, OFF_POLICY_ONNX_PATH, ON_POLICY_ONNX_PATH +from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, VISION_ONNX_PATH, POLICY_ONNX_PATH import hashlib class TestDefaultModel: def test_compare_onnx_hashes(self): vision_hash = get_file_hash(VISION_ONNX_PATH) - off_policy_hash = get_file_hash(OFF_POLICY_ONNX_PATH) - on_policy_hash = get_file_hash(ON_POLICY_ONNX_PATH) + policy_hash = get_file_hash(POLICY_ONNX_PATH) - combined_hash = hashlib.sha256((vision_hash + off_policy_hash + on_policy_hash).encode()).hexdigest() + combined_hash = hashlib.sha256((vision_hash + policy_hash).encode()).hexdigest() with open(MODEL_HASH_PATH) as f: current_hash = f.read().strip() diff --git a/sunnypilot/selfdrive/assets/icons_mici/always_offroad.png b/sunnypilot/selfdrive/assets/icons_mici/always_offroad.png new file mode 100644 index 000000000..56f35669c --- /dev/null +++ b/sunnypilot/selfdrive/assets/icons_mici/always_offroad.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e459241d896824f5e8207d568847acab3dedd41caae7af59d4c17e043663b0c9 +size 4035 diff --git a/sunnypilot/selfdrive/assets/icons_mici/disable_offroad.png b/sunnypilot/selfdrive/assets/icons_mici/disable_offroad.png new file mode 100644 index 000000000..146734aaf --- /dev/null +++ b/sunnypilot/selfdrive/assets/icons_mici/disable_offroad.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27a0fca872d4586f578d246890b83674cdb7ecb03f58b2b0379b4b64a5816053 +size 3908 diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py index c6658bdc7..df773889a 100644 --- a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py +++ b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py @@ -14,11 +14,8 @@ 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 -# 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] +KP = 1.0 +KI = 0.3 def get_predicted_lateral_jerk(lat_accels, t_diffs): @@ -61,9 +58,10 @@ 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([INTERP_SPEEDS, KP_INTERP], KI) + self._pid = PIDController(KP, KI) self._pid_log = None self._setpoint = 0.0 self._measurement = 0.0 diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py index 2db88299c..1738a11e4 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py @@ -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.lac_torque.torque_params, gravity_adjusted=False) + self.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.lac_torque.torque_params, gravity_adjusted=False) + self.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.lac_torque.torque_params, gravity_adjusted=True) + CS.vEgo, CS.aEgo), self.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.lac_torque.torque_params) + FRICTION_THRESHOLD, self.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.lac_torque.torque_params) + self._pid_log.error += get_friction(friction_input, self._lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) self.update_output_torque(CS) diff --git a/sunnypilot/tools/__init__.py b/sunnypilot/tools/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sunnypilot/tools/memory_profiler/__init__.py b/sunnypilot/tools/memory_profiler/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sunnypilot/tools/memory_profiler/mem_usage.py b/sunnypilot/tools/memory_profiler/mem_usage.py new file mode 100644 index 000000000..20b4bb2d0 --- /dev/null +++ b/sunnypilot/tools/memory_profiler/mem_usage.py @@ -0,0 +1,164 @@ +""" +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 = ( + "" + + f"