diff --git a/.github/workflows/sunnypilot-master-dev-prep.yaml b/.github/workflows/sunnypilot-master-dev-prep.yaml index 10794bf0f..8e8e3c3d9 100644 --- a/.github/workflows/sunnypilot-master-dev-prep.yaml +++ b/.github/workflows/sunnypilot-master-dev-prep.yaml @@ -174,6 +174,24 @@ jobs: echo ' pushurl = ${{ env.LFS_PUSH_URL }}' >> .lfsconfig echo ' locksverify = false' >> .lfsconfig + - name: Restore workflows from source + run: | + TARGET_BRANCH="${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }}" + SOURCE_BRANCH="${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }}" + + # Ensure we are on the target branch + git checkout $TARGET_BRANCH + + echo "Restoring .github/workflows from $SOURCE_BRANCH" + git checkout origin/$SOURCE_BRANCH -- .github/workflows + + if ! git diff --cached --quiet; then + echo "Workflows differ. Committing restoration." + git commit -m "chore: restore .github/workflows from $SOURCE_BRANCH" + else + echo "Workflows match $SOURCE_BRANCH." + fi + - uses: actions/create-github-app-token@v2 id: ci-token with: diff --git a/selfdrive/monitoring/helpers.py b/selfdrive/monitoring/helpers.py index 002e01ae3..4f068c4f5 100644 --- a/selfdrive/monitoring/helpers.py +++ b/selfdrive/monitoring/helpers.py @@ -449,7 +449,6 @@ class DriverMonitoring: rpyCalib = [0., 0., 0.] else: highway_speed = sm['carState'].vEgo - # TODO-SP: unit test to assert both control checks are always present enabled = sm['selfdriveState'].enabled or sm['carControl'].latActive wrong_gear = sm['carState'].gearShifter not in (car.CarState.GearShifter.drive, car.CarState.GearShifter.low) standstill = sm['carState'].standstill diff --git a/selfdrive/monitoring/test_monitoring.py b/selfdrive/monitoring/test_monitoring.py index 6ea9b8028..75adb6a2c 100644 --- a/selfdrive/monitoring/test_monitoring.py +++ b/selfdrive/monitoring/test_monitoring.py @@ -1,6 +1,7 @@ import numpy as np +import pytest -from cereal import log +from cereal import log, car from openpilot.common.realtime import DT_DMON from openpilot.selfdrive.monitoring.helpers import DriverMonitoring, DRIVER_MONITOR_SETTINGS from openpilot.system.hardware import HARDWARE @@ -204,3 +205,66 @@ class TestMonitoring: assert EventName.driverUnresponsive in \ events[int((INVISIBLE_SECONDS_TO_RED-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)].names + +@pytest.mark.parametrize("enabled_state, lat_active_state, expected", [ + (False, False, False), # Both Disabled + (True, False, True), # OP Enabled, Lat Inactive + (False, True, True), # OP Disabled, Lat Active (e.g. MADS) + (True, True, True) # Both Active +]) +def test_enabled_states(enabled_state, lat_active_state, expected): + """ + Test DriverMonitoring.run_step with all 4 combinations of: + - selfdriveState.enabled (True/False) + - carControl.latActive (True/False) + """ + cs = car.CarState.new_message() + cs.vEgo = 30.0 + cs.gearShifter = car.CarState.GearShifter.drive + cs.standstill = False + cs.steeringPressed = False + cs.gasPressed = False + + ss = log.SelfdriveState.new_message() + ss.enabled = enabled_state + + cc = car.CarControl.new_message() + cc.latActive = lat_active_state + + mv2 = log.ModelDataV2.new_message() + mv2.meta.disengagePredictions.brakeDisengageProbs = [0.0] + + lc = log.LiveCalibrationData.new_message() + lc.rpyCalib = [0.0, 0.0, 0.0] + + ds = make_msg(False) + + sm = { + 'carState': cs, + 'selfdriveState': ss, + 'carControl': cc, + 'modelV2': mv2, + 'liveCalibration': lc, + 'driverStateV2': ds + } + + driver_monitoring = DriverMonitoring() + + # run_test doesn't assign enabled to a variable, so we need to spy on _update_events to see its value + captured_args = [] + original_update_events = driver_monitoring._update_events + + def spy_update_events(driver_engaged, op_engaged, standstill, wrong_gear, car_speed): + captured_args.append(op_engaged) + return original_update_events(driver_engaged, op_engaged, standstill, wrong_gear, car_speed) + + driver_monitoring._update_events = spy_update_events + + driver_monitoring.run_step(sm, demo=False) + + # Assertion + assert len(captured_args) == 1, "Expected _update_events to be called exactly once" + actual_enabled = captured_args[0] + + assert actual_enabled == expected, f"Expected op_engaged={expected}, but got {actual_enabled}" + diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py index fe90e3bdf..ac08538df 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -23,7 +23,10 @@ class UIStateSP: self.sunnylink_state = SunnylinkState() def update(self) -> None: - self.sunnylink_state.start() + if self.sunnylink_enabled: + self.sunnylink_state.start() + else: + self.sunnylink_state.stop() @staticmethod def update_status(ss, ss_sp, onroad_evt) -> str: diff --git a/sunnypilot/sunnylink/sunnylink_state.py b/sunnypilot/sunnylink/sunnylink_state.py index 13b5ad81e..efdfa7071 100644 --- a/sunnypilot/sunnylink/sunnylink_state.py +++ b/sunnypilot/sunnylink/sunnylink_state.py @@ -136,10 +136,11 @@ class SunnylinkState: token = self._api.get_token() response = self._api.api_get(f"device/{self.sunnylink_dongle_id}/roles", method='GET', access_token=token) if response.status_code == 200: - self._roles = _parse_roles(response.text) - self._params.put("SunnylinkCache_Roles", response.text) - sponsor_tier = self._get_highest_tier() + roles = response.text + self._params.put("SunnylinkCache_Roles", roles) with self._lock: + self._roles = _parse_roles(roles) + sponsor_tier = self._get_highest_tier() if sponsor_tier != self.sponsor_tier: self.sponsor_tier = sponsor_tier cloudlog.info(f"Sunnylink sponsor tier updated to {sponsor_tier.name}") @@ -157,7 +158,7 @@ class SunnylinkState: users = response.text self._params.put("SunnylinkCache_Users", users) with self._lock: - _parse_users(users) + self._users = _parse_users(users) except Exception as e: cloudlog.exception(f"Failed to fetch sunnylink users: {e} for dongle id {self.sunnylink_dongle_id}") diff --git a/system/ui/sunnypilot/widgets/list_view.py b/system/ui/sunnypilot/widgets/list_view.py index 6ba41f143..4a9c5fead 100644 --- a/system/ui/sunnypilot/widgets/list_view.py +++ b/system/ui/sunnypilot/widgets/list_view.py @@ -296,12 +296,12 @@ def option_item_sp(title: str | Callable[[], str], param: str, value_change_step: int = 1, on_value_changed: Callable[[int], None] | None = None, enabled: bool | Callable[[], bool] = True, icon: str = "", label_width: int = LABEL_WIDTH, value_map: dict[int, int] | None = None, - use_float_scaling: bool = False, label_callback: Callable[[int], str] | None = None) -> ListItemSP: + use_float_scaling: bool = False, label_callback: Callable[[int], str] | None = None, inline: bool = False) -> ListItemSP: action = OptionControlSP( param, min_value, max_value, value_change_step, enabled, on_value_changed, value_map, label_width, use_float_scaling, label_callback ) - return ListItemSP(title=title, description=description, action_item=action, icon=icon) + return ListItemSP(title=title, description=description, action_item=action, icon=icon, inline=inline) def button_item_sp(title: str | Callable[[], str], button_text: str | Callable[[], str], description: str | Callable[[], str] | None = None,