diff --git a/.github/workflows/raylib_ui_preview.yaml b/.github/workflows/raylib_ui_preview.yaml index ff9655d155f827..40f01072791280 100644 --- a/.github/workflows/raylib_ui_preview.yaml +++ b/.github/workflows/raylib_ui_preview.yaml @@ -2,9 +2,16 @@ name: "raylib ui preview" on: push: branches: - - master - pull_request_target: - types: [assigned, opened, synchronize, reopened, edited] + - 'master' + # pull_request_target: + # types: [assigned, opened, synchronize, reopened, edited] + # branches: + # - '**' + # paths: + # - 'selfdrive/ui/**' + # - 'system/ui/**' + pull_request: + types: [opened, synchronize, reopened, edited] branches: - 'master' paths: @@ -17,10 +24,11 @@ env: REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }} BRANCH_NAME: "openpilot/pr-${{ github.event.number }}-raylib-ui" + CI_ARTIFACTS_OWNER: ${{ github.repository_owner }} + CI_ARTIFACTS_REPO: openpilot-ci-artifacts jobs: preview: - if: github.repository == 'commaai/openpilot' name: preview runs-on: ubuntu-latest timeout-minutes: 20 @@ -59,13 +67,13 @@ jobs: - name: Getting master ui uses: actions/checkout@v4 with: - repository: commaai/ci-artifacts + repository: ${{ env.CI_ARTIFACTS_OWNER }}/${{ env.CI_ARTIFACTS_REPO }} ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} path: ${{ github.workspace }}/master_ui_raylib ref: openpilot_master_ui_raylib - name: Saving new master ui - if: github.ref == 'refs/heads/master' && github.event_name == 'push' + if: github.event_name == 'push' working-directory: ${{ github.workspace }}/master_ui_raylib run: | git checkout --orphan=new_master_ui_raylib @@ -80,12 +88,12 @@ jobs: git push origin openpilot_master_ui_raylib --force - name: Finding diff - if: github.event_name == 'pull_request_target' + if: github.event_name == 'pull_request' || github.event_name == 'pull_request_target' id: find_diff run: >- sudo apt-get update && sudo apt-get install -y imagemagick - scenes=$(find ${{ github.workspace }}/pr_ui/*.png -type f -printf "%f\n" | cut -d '.' -f 1 | grep -v 'pair_device') + scenes=$(find ${{ github.workspace }}/pr_ui/*.png -type f -printf "%f\n" | cut -d '.' -f 1) A=($scenes) DIFF="" @@ -102,7 +110,7 @@ jobs: DIFF="${DIFF}" DIFF="${DIFF}" - DIFF="${DIFF} " + DIFF="${DIFF} " DIFF="${DIFF}" DIFF="${DIFF}
" @@ -119,13 +127,13 @@ jobs: DIFF="${DIFF}" DIFF="${DIFF}" - DIFF="${DIFF} " - DIFF="${DIFF} " + DIFF="${DIFF} " + DIFF="${DIFF} " DIFF="${DIFF}" DIFF="${DIFF}" - DIFF="${DIFF} " - DIFF="${DIFF} " + DIFF="${DIFF} " + DIFF="${DIFF} " DIFF="${DIFF}" DIFF="${DIFF}
master proposed master proposed
diff composite diff diff composite diff
" @@ -138,7 +146,7 @@ jobs: if [[ $INDEX -eq 0 ]]; then TABLE="${TABLE}" fi - TABLE="${TABLE} " + TABLE="${TABLE} " if [[ $INDEX -eq 1 || $(($i + 1)) -eq ${#A[*]} ]]; then TABLE="${TABLE}" fi @@ -149,7 +157,7 @@ jobs: echo "DIFF=$DIFF$TABLE" >> "$GITHUB_OUTPUT" - name: Saving proposed ui - if: github.event_name == 'pull_request_target' + if: github.event_name == 'pull_request' || github.event_name == 'pull_request_target' working-directory: ${{ github.workspace }}/master_ui_raylib run: | git config user.name "GitHub Actions Bot" @@ -162,7 +170,7 @@ jobs: git push origin ${{ env.BRANCH_NAME }} --force - name: Comment Screenshots on PR - if: github.event_name == 'pull_request_target' + if: github.event_name == 'pull_request' || github.event_name == 'pull_request_target' uses: thollander/actions-comment-pull-request@v2 with: message: | @@ -171,4 +179,4 @@ jobs: ${{ steps.find_diff.outputs.DIFF }} comment_tag: run_id_screenshots_raylib pr_number: ${{ github.event.number }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index 35ced1e38b4e7c..35c03f4957a0e6 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -32,12 +32,7 @@ env: jobs: build_release: name: build release - runs-on: ${{ - (github.repository == 'commaai/openpilot') && - ((github.event_name != 'pull_request') || - (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) - && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') - || fromJSON('["ubuntu-24.04"]') }} + runs-on: ubuntu-24.04 env: STRIPPED_DIR: /tmp/releasepilot steps: @@ -65,23 +60,17 @@ jobs: cd $STRIPPED_DIR ${{ env.RUN }} "release/check-dirty.sh" - name: Check submodules - if: github.repository == 'commaai/openpilot' timeout-minutes: 3 run: release/check-submodules.sh build: - runs-on: ${{ - (github.repository == 'commaai/openpilot') && - ((github.event_name != 'pull_request') || - (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) - && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') - || fromJSON('["ubuntu-24.04"]') }} + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 with: submodules: true - name: Setup docker push - if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'commaai/openpilot' + if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' run: | echo "PUSH_IMAGE=true" >> "$GITHUB_ENV" $DOCKER_LOGIN @@ -91,7 +80,7 @@ jobs: build_mac: name: build macOS - runs-on: ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-macos-8x14' || 'macos-latest' }} + runs-on: macos-latest steps: - uses: actions/checkout@v4 with: @@ -105,6 +94,10 @@ jobs: restore-keys: | brew-macos-${{ env.CACHE_COMMIT_DATE }} brew-macos + # - name: Force relink openssl@3 + # run: | + # brew unlink openssl@3 || true + # brew link --overwrite openssl@3 - name: Install dependencies run: ./tools/mac_setup.sh env: @@ -124,12 +117,7 @@ jobs: static_analysis: name: static analysis - runs-on: ${{ - (github.repository == 'commaai/openpilot') && - ((github.event_name != 'pull_request') || - (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) - && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') - || fromJSON('["ubuntu-24.04"]') }} + runs-on: ubuntu-24.04 env: PYTHONWARNINGS: default steps: @@ -143,12 +131,7 @@ jobs: unit_tests: name: unit tests - runs-on: ${{ - (github.repository == 'commaai/openpilot') && - ((github.event_name != 'pull_request') || - (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) - && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') - || fromJSON('["ubuntu-24.04"]') }} + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 with: @@ -170,12 +153,7 @@ jobs: process_replay: name: process replay - runs-on: ${{ - (github.repository == 'commaai/openpilot') && - ((github.event_name != 'pull_request') || - (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) - && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') - || fromJSON('["ubuntu-24.04"]') }} + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 with: @@ -219,12 +197,7 @@ jobs: simulator_driving: name: simulator driving - runs-on: ${{ - (github.repository == 'commaai/openpilot') && - ((github.event_name != 'pull_request') || - (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) - && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') - || fromJSON('["ubuntu-24.04"]') }} + runs-on: ubuntu-24.04 if: false # FIXME: Started to timeout recently steps: - uses: actions/checkout@v4 @@ -245,12 +218,7 @@ jobs: create_ui_report: # This job name needs to be the same as UI_JOB_NAME in ui_preview.yaml name: Create UI Report - runs-on: ${{ - (github.repository == 'commaai/openpilot') && - ((github.event_name != 'pull_request') || - (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) - && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') - || fromJSON('["ubuntu-24.04"]') }} + runs-on: ubuntu-24.04 if: false # FIXME: FrameReader is broken on CI runners steps: - uses: actions/checkout@v4 @@ -280,12 +248,7 @@ jobs: create_raylib_ui_report: name: Create raylib UI Report - runs-on: ${{ - (github.repository == 'commaai/openpilot') && - ((github.event_name != 'pull_request') || - (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) - && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') - || fromJSON('["ubuntu-24.04"]') }} + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 with: @@ -294,7 +257,7 @@ jobs: - name: Build openpilot run: ${{ env.RUN }} "scons -j$(nproc)" - name: Create raylib UI Report - run: > + run: | ${{ env.RUN }} "PYTHONWARNINGS=ignore && source selfdrive/test/setup_xvfb.sh && python3 selfdrive/ui/tests/test_ui/raylib_screenshots.py" diff --git a/selfdrive/ui/layouts/settings/device.py b/selfdrive/ui/layouts/settings/device.py index 66341db37b1dae..140debe20743b5 100644 --- a/selfdrive/ui/layouts/settings/device.py +++ b/selfdrive/ui/layouts/settings/device.py @@ -167,8 +167,7 @@ def _update_calib_description(self): cloudlog.exception("invalid LiveTorqueParameters") desc += "

" - desc += ("openpilot is continuously calibrating, resetting is rarely required. " + - "Resetting calibration will restart openpilot if the car is powered on.") + desc += "openpilot is continuously calibrating, resetting is rarely required. " + "Resetting calibration will restart openpilot if the car is powered on." self._reset_calib_btn.set_description(desc) @@ -208,7 +207,9 @@ def _on_regulatory(self): def _on_review_training_guide(self): if not self._training_guide: + def completed_callback(): gui_app.set_modal_overlay(None) + self._training_guide = TrainingGuide(completed_callback=completed_callback) gui_app.set_modal_overlay(self._training_guide) diff --git a/selfdrive/ui/layouts/settings/software.py b/selfdrive/ui/layouts/settings/software.py index 0c17a54fbe4ae0..856463611218d6 100644 --- a/selfdrive/ui/layouts/settings/software.py +++ b/selfdrive/ui/layouts/settings/software.py @@ -45,7 +45,7 @@ def __init__(self): self._onroad_label = ListItem(title="Updates are only downloaded while the car is off.") self._version_item = text_item("Current Version", ui_state.params.get("UpdaterCurrentDescription") or "") - self._download_btn = button_item("Download", "CHECK", callback=self._on_download_update) + self._download_btn = button_item("DOWNLOAD", "CHECK", callback=self._on_download_update) # Install button is initially hidden self._install_btn = button_item("Install Update", "INSTALL", callback=self._on_install_update) diff --git a/selfdrive/ui/tests/test_ui/raylib_screenshots.py b/selfdrive/ui/tests/test_ui/raylib_screenshots.py index bd40b89624831a..137b90991d86e9 100755 --- a/selfdrive/ui/tests/test_ui/raylib_screenshots.py +++ b/selfdrive/ui/tests/test_ui/raylib_screenshots.py @@ -5,11 +5,13 @@ import time import pathlib from collections import namedtuple +from collections.abc import Callable +from typing import NotRequired, TypedDict import pyautogui -import pywinctl +from PIL import ImageChops -from cereal import log +from cereal import car, log from cereal import messaging from cereal.messaging import PubMaster from openpilot.common.basedir import BASEDIR @@ -23,6 +25,10 @@ TEST_OUTPUT_DIR = TEST_DIR / "raylib_report" SCREENSHOTS_DIR = TEST_OUTPUT_DIR / "screenshots" UI_DELAY = 0.2 +SCROLL_DELAY = 1.5 # Delay screenshot by this many seconds after scrolling (to allow scroll to settle) +DEFAULT_SCROLL_AMOUNT = -20 # Good for most full screen scrollers +MAX_SCREENSHOTS_PER_CASE = 8 # Maximum screenshots to generate while scrolling + # Offroad alerts to test OFFROAD_ALERTS = ['Offroad_IsTakingSnapshot'] @@ -36,83 +42,109 @@ def put_update_params(params: Params): params.put("UpdaterNewDescription", description) -def setup_homescreen(click, pm: PubMaster): +def setup_homescreen(click, scroll, pm: PubMaster): pass -def setup_settings(click, pm: PubMaster): +def setup_settings(click, scroll, pm: PubMaster): click(100, 100) -def close_settings(click, pm: PubMaster): +def close_settings(click, scroll, pm: PubMaster): click(240, 216) -def setup_settings_network(click, pm: PubMaster): - setup_settings(click, pm) +def setup_settings_network(click, scroll, pm: PubMaster): + setup_settings(click, scroll, pm) click(278, 450) -def setup_settings_toggles(click, pm: PubMaster): - setup_settings(click, pm) +def setup_settings_toggles(click, scroll, pm: PubMaster): + setup_settings(click, scroll, pm) click(278, 600) -def setup_settings_software(click, pm: PubMaster): +def setup_settings_software(click, scroll, pm: PubMaster): put_update_params(Params()) - setup_settings(click, pm) + setup_settings(click, scroll, pm) click(278, 720) -def setup_settings_firehose(click, pm: PubMaster): - setup_settings(click, pm) +def setup_settings_firehose(click, scroll, pm: PubMaster): + setup_settings(click, scroll, pm) click(278, 845) -def setup_settings_developer(click, pm: PubMaster): - setup_settings(click, pm) +def setup_settings_developer(click, scroll, pm: PubMaster): + CP = car.CarParams() + CP.alphaLongitudinalAvailable = True # show alpha long control toggle + Params().put("CarParamsPersistent", CP.to_bytes()) + + setup_settings(click, scroll, pm) click(278, 950) -def setup_keyboard(click, pm: PubMaster): - setup_settings_developer(click, pm) +def setup_keyboard(click, scroll, pm: PubMaster): + setup_settings_developer(click, scroll, pm) click(1930, 470) -def setup_pair_device(click, pm: PubMaster): +def setup_openpilot_long_control_confirmation_dialog(click, scroll, pm: PubMaster): + setup_settings_developer(click, scroll, pm) + click(2000, 960) # toggle openpilot longitudinal control + + +def setup_pair_device(click, scroll, pm: PubMaster): click(1950, 800) -def setup_offroad_alert(click, pm: PubMaster): +def setup_offroad_alert(click, scroll, pm: PubMaster): set_offroad_alert("Offroad_TemperatureTooHigh", True, extra_text='99C') set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text='longitudinal') for alert in OFFROAD_ALERTS: set_offroad_alert(alert, True) - setup_settings(click, pm) - close_settings(click, pm) + setup_settings(click, scroll, pm) + close_settings(click, scroll, pm) -def setup_confirmation_dialog(click, pm: PubMaster): - setup_settings(click, pm) +def setup_confirmation_dialog(click, scroll, pm: PubMaster): + setup_settings(click, scroll, pm) click(1985, 791) # reset calibration -def setup_homescreen_update_available(click, pm: PubMaster): +def setup_homescreen_update_available(click, scroll, pm: PubMaster): params = Params() params.put_bool("UpdateAvailable", True) put_update_params(params) - setup_settings(click, pm) - close_settings(click, pm) + setup_settings(click, scroll, pm) + close_settings(click, scroll, pm) -def setup_software_release_notes(click, pm: PubMaster): - setup_settings(click, pm) - setup_settings_software(click, pm) +def setup_software_release_notes(click, scroll, pm: PubMaster): + setup_settings(click, scroll, pm) + setup_settings_software(click, scroll, pm) click(588, 110) # expand description for current version -CASES = { +def setup_experimental_mode_description(click, scroll, pm: PubMaster): + setup_settings(click, scroll, pm) + setup_settings_toggles(click, scroll, pm) + click(1200, 280) # expand description for experimental mode + scroll(-4) # scroll down to show more of the description + time.sleep(1) + + +class CaseConfig(TypedDict): + scroll_amount: NotRequired[int] + scroll_enabled: NotRequired[bool] + + +SetupFunction = Callable[[Callable[..., None], Callable[..., None], PubMaster], None] +CaseValue = SetupFunction | tuple[SetupFunction, CaseConfig | None] + +# Value can be the setup function, or tuple of (setup func, config) +CASES: dict[str, CaseValue] = { "homescreen": setup_homescreen, "settings_device": setup_settings, "settings_network": setup_settings_network, @@ -120,12 +152,17 @@ def setup_software_release_notes(click, pm: PubMaster): "settings_software": setup_settings_software, "settings_firehose": setup_settings_firehose, "settings_developer": setup_settings_developer, - "keyboard": setup_keyboard, + "keyboard": (setup_keyboard, {"scroll_enabled": False}), # The blinking cursor makes it think there was a change when scrolling "pair_device": setup_pair_device, - "offroad_alert": setup_offroad_alert, - "homescreen_update_available": setup_homescreen_update_available, + "offroad_alert": (setup_offroad_alert, {"scroll_amount": -12}), # smaller scrollable area + "homescreen_update_available": (setup_homescreen_update_available, {"scroll_amount": -12}), # smaller scrollable area "confirmation_dialog": setup_confirmation_dialog, "software_release_notes": setup_software_release_notes, + "experimental_mode_description": ( + setup_experimental_mode_description, + {"scroll_enabled": False}, + ), + "openpilot_long_control_confirmation_dialog": setup_openpilot_long_control_confirmation_dialog, } @@ -144,28 +181,76 @@ def setup(self): ds.clear_write_flag() time.sleep(0.05) time.sleep(0.5) - try: - self.ui = pywinctl.getWindowsWithTitle("UI")[0] - except Exception as e: - print(f"failed to find ui window, assuming that it's in the top left (for Xvfb) {e}") - self.ui = namedtuple("bb", ["left", "top", "width", "height"])(0, 0, 2160, 1080) + self.ui = namedtuple("bb", ["left", "top", "width", "height"])(0, 0, 2160, 1080) - def screenshot(self, name: str): + def screenshot(self): full_screenshot = pyautogui.screenshot() cropped = full_screenshot.crop((self.ui.left, self.ui.top, self.ui.left + self.ui.width, self.ui.top + self.ui.height)) - cropped.save(SCREENSHOTS_DIR / f"{name}.png") + return cropped + + def screenshot_and_save(self, name: str): + screenshot = self.screenshot() + screenshot.save(SCREENSHOTS_DIR / f"{name}.png") + return screenshot + + def capture_scrollable(self, name: str, scroll_clicks: int, max_screenshots=MAX_SCREENSHOTS_PER_CASE): + # Take first screenshot + prev = self.screenshot_and_save(name) + + # Scroll until there are no more changes or we reach the limit + for i in range(1, max_screenshots): + self.scroll(scroll_clicks) + time.sleep(SCROLL_DELAY) + curr = self.screenshot() + + # Check for difference + try: + # TODO: This might need to be more robust to allow for small pixel diffs in case scrolling isn't consistent, but so far it seems to work + diff = ImageChops.difference(prev.convert('RGB'), curr.convert('RGB')) + if diff.getbbox() is None: + # no changes -> reached end + break + except Exception as e: + print(f"error comparing screenshots: {e}") + break + + # Save the current page + curr.save(SCREENSHOTS_DIR / f"{name}_{i}.png") + + prev = curr def click(self, x: int, y: int, *args, **kwargs): pyautogui.mouseDown(self.ui.left + x, self.ui.top + y, *args, **kwargs) time.sleep(0.01) pyautogui.mouseUp(self.ui.left + x, self.ui.top + y, *args, **kwargs) + def scroll(self, clicks: int, *args, **kwargs): + if clicks == 0: + return + click = -1 if clicks < 0 else 1 # -1 = down, 1 = up + for _ in range(abs(clicks)): + pyautogui.scroll(click, *args, **kwargs) # scroll for individual clicks since we need to delay between clicks + time.sleep(0.01) # small delay between scroll clicks to work properly in xvfb + @with_processes(["ui"]) - def test_ui(self, name, setup_case): + def test_ui(self, name: str, setup_case: SetupFunction, config: CaseConfig | None = None): self.setup() - time.sleep(UI_DELAY) # wait for UI to start - setup_case(self.click, self.pm) - self.screenshot(name) + time.sleep(UI_DELAY) # Wait for UI to start + setup_case(self.click, self.scroll, self.pm) + config = config or {} + + # Just take a screenshot if scrolling is disabled + scroll_enabled = config.get("scroll_enabled", True) + if not scroll_enabled: + self.screenshot_and_save(name) + return + + try: + scroll_clicks = config.get("scroll_amount", DEFAULT_SCROLL_AMOUNT) + self.capture_scrollable(name, scroll_clicks=scroll_clicks) + except Exception as e: + print(f"failed capturing scrollable page, falling back to single screenshot: {e}") + self.screenshot_and_save(name) def create_screenshots(): @@ -178,7 +263,8 @@ def create_screenshots(): params = Params() params.put("DongleId", "123456789012345") for name, setup in CASES.items(): - t.test_ui(name, setup) + setup_fn, cfg = setup if isinstance(setup, tuple) else (setup, None) + t.test_ui(name, setup_fn, cfg) if __name__ == "__main__": diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index 0ae0b35359e6e8..f6bc2be3e5b851 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -32,6 +32,10 @@ else brew up fi +# Relink openssl +brew unlink openssl@3 || true +brew link overwrite openssl@3 + brew bundle --file=- <<-EOS brew "git-lfs" brew "capnp"