diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 34e333bb59..c86c044772 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -1,7 +1,7 @@
name: release
on:
schedule:
- - cron: '0 10 * * *'
+ - cron: '0 9 * * *'
workflow_dispatch:
jobs:
diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml
index 6e17f5a480..e6b5950cbb 100644
--- a/.github/workflows/selfdrive_tests.yaml
+++ b/.github/workflows/selfdrive_tests.yaml
@@ -41,7 +41,12 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: true
- - run: git lfs pull
+ - name: Getting LFS files
+ uses: nick-fields/retry@7152eba30c6575329ac0576536151aca5a72780e
+ with:
+ timeout_minutes: 2
+ max_attempts: 3
+ command: git lfs pull
- name: Build devel
timeout-minutes: 1
run: TARGET_DIR=$STRIPPED_DIR release/build_devel.sh
@@ -97,26 +102,26 @@ jobs:
build_mac:
name: build macOS
- runs-on: macos-latest
+ runs-on: namespace-profile-macos-8x14
steps:
- uses: actions/checkout@v4
with:
submodules: true
- - run: git lfs pull
+ - name: Homebrew cache
+ uses: ./.github/workflows/auto-cache
+ with:
+ path: ~/Library/Caches/Homebrew
- name: Install dependencies
run: ./tools/mac_setup.sh
env:
# package install has DeprecationWarnings
PYTHONWARNINGS: default
+ - run: git lfs pull
- run: echo "CACHE_COMMIT_DATE=$(git log -1 --pretty='format:%cd' --date=format:'%Y-%m-%d-%H:%M')" >> $GITHUB_ENV
- name: Getting scons cache
- uses: 'actions/cache@v4'
+ uses: ./.github/workflows/auto-cache
with:
path: /tmp/scons_cache
- key: scons-${{ runner.arch }}-macos-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }}
- restore-keys: |
- scons-${{ runner.arch }}-macos-${{ env.CACHE_COMMIT_DATE }}
- scons-${{ runner.arch }}-macos
- name: Building openpilot
run: . .venv/bin/activate && scons -j$(nproc)
@@ -169,10 +174,10 @@ jobs:
timeout-minutes: ${{ ((steps.restore-scons-cache.outputs.cache-hit == 'true') && 10 || 30) }} # allow more time when we missed the scons cache
run: ${{ env.RUN }} "scons -j$(nproc)"
- name: Run unit tests
- timeout-minutes: 15
+ timeout-minutes: 1
run: |
${{ env.RUN }} "source selfdrive/test/setup_xvfb.sh && \
- $PYTEST --timeout 60 -m 'not slow' && \
+ MAX_EXAMPLES=1 $PYTEST --timeout 60 -m 'not slow' && \
./selfdrive/ui/tests/create_test_translations.sh && \
QT_QPA_PLATFORM=offscreen ./selfdrive/ui/tests/test_translations && \
pytest ./selfdrive/ui/tests/test_translations.py"
@@ -246,7 +251,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- job: [0, 1]
+ job: [0, 1, 2, 3]
steps:
- uses: actions/checkout@v4
with:
@@ -261,12 +266,12 @@ jobs:
- name: Build openpilot
run: ${{ env.RUN }} "scons -j$(nproc)"
- name: Test car models
- timeout-minutes: 20
+ timeout-minutes: 2
run: |
- ${{ env.RUN }} "$PYTEST selfdrive/car/tests/test_models.py && \
+ ${{ env.RUN }} "FILEREADER_CACHE=1 MAX_EXAMPLES=1 $PYTEST selfdrive/car/tests/test_models.py && \
chmod -R 777 /tmp/comma_download_cache"
env:
- NUM_JOBS: 2
+ NUM_JOBS: 4
JOB_ID: ${{ matrix.job }}
- name: "Upload coverage to Codecov"
uses: codecov/codecov-action@v4
diff --git a/.github/workflows/setup/action.yaml b/.github/workflows/setup/action.yaml
index 701675942f..1315ba989e 100644
--- a/.github/workflows/setup/action.yaml
+++ b/.github/workflows/setup/action.yaml
@@ -20,6 +20,18 @@ runs:
echo "You should not run this action directly. Use setup-with-retry instead"
exit 1
+ - shell: bash
+ name: No retries!
+ run: |
+ if [ "${{ github.run_attempt }}" -gt 1 ]; then
+ echo -e "\033[31m"
+ echo "##################################################"
+ echo " Retries not allowed! Fix the flaky test! "
+ echo "##################################################"
+ echo -e "\033[0m"
+ exit 1
+ fi
+
# do this after checkout to ensure our custom LFS config is used to pull from GitLab
- shell: bash
run: git lfs pull
diff --git a/.gitignore b/.gitignore
index 919e011fe3..4562e47817 100644
--- a/.gitignore
+++ b/.gitignore
@@ -76,6 +76,7 @@ selfdrive/modeld/models/*.thneed
selfdrive/modeld/models/*.pkl
*.bz2
+*.zst
build/
diff --git a/Jenkinsfile b/Jenkinsfile
index b47a3884a0..7a392cdd2b 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -66,7 +66,7 @@ fi
ln -snf ${env.TEST_DIR} /data/pythonpath
cd ${env.TEST_DIR} || true
-${cmd}
+time ${cmd}
END"""
sh script: ssh_cmd, label: step_label
@@ -198,7 +198,7 @@ node {
//["build master-ci", "cd $SOURCE_DIR/release && TARGET_DIR=$TEST_DIR $SOURCE_DIR/scripts/retry.sh ./build_devel.sh"],
step("build openpilot", "cd system/manager && ./build.py"),
step("check dirty", "release/check-dirty.sh"),
- step("onroad tests", "pytest selfdrive/test/test_onroad.py -s"),
+ step("onroad tests", "pytest selfdrive/test/test_onroad.py -s", [timeout: 60]),
//["time to onroad", "pytest selfdrive/test/test_time_to_onroad.py"],
])
},
@@ -207,7 +207,7 @@ node {
step("build", "cd system/manager && ./build.py"),
step("test pandad", "pytest selfdrive/pandad/tests/test_pandad.py", [diffPaths: ["panda/", "selfdrive/pandad/"]]),
step("test power draw", "pytest -s system/hardware/tici/tests/test_power_draw.py"),
- step("test encoder", "LD_LIBRARY_PATH=/usr/local/lib pytest system/loggerd/tests/test_encoder.py"),
+ step("test encoder", "LD_LIBRARY_PATH=/usr/local/lib pytest system/loggerd/tests/test_encoder.py", [timeout: 60]),
step("test pigeond", "pytest system/ubloxd/tests/test_pigeond.py"),
step("test manager", "pytest system/manager/test/test_manager.py"),
])
@@ -221,12 +221,12 @@ node {
'camerad': {
deviceStage("AR0231", "tici-ar0231", ["UNSAFE=1"], [
step("build", "cd system/manager && ./build.py"),
- step("test camerad", "pytest system/camerad/test/test_camerad.py"),
+ step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 60]),
step("test exposure", "pytest system/camerad/test/test_exposure.py"),
])
deviceStage("OX03C10", "tici-ox03c10", ["UNSAFE=1"], [
step("build", "cd system/manager && ./build.py"),
- step("test camerad", "pytest system/camerad/test/test_camerad.py"),
+ step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 60]),
step("test exposure", "pytest system/camerad/test/test_exposure.py"),
])
},
diff --git a/RELEASES.md b/RELEASES.md
index 1435809d84..dbec5206cc 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -4,8 +4,12 @@ Version 0.9.8 (2024-XX-XX)
* Trained in brand new ML simulator
* Model now gates applying positive accel in Chill mode
* New driving monitoring model
- * Reduced false positives related to passengers
+ * Reduced false positives related to passengers
+* Image processing pipeline moved to the ISP
+ * More GPU time for driving models
+ * Power draw reduced 0.5W, which means your device runs cooler
* Added toggle to enable driver monitoring even when openpilot is not engaged
+* Enable openpilot longitudinal control for Ford Q3 vehicles
* New Toyota TSS2 longitudinal tune
Version 0.9.7 (2024-06-13)
diff --git a/cereal/messaging/tests/test_pub_sub_master.py b/cereal/messaging/tests/test_pub_sub_master.py
index 99965319eb..e9bc7a85cb 100644
--- a/cereal/messaging/tests/test_pub_sub_master.py
+++ b/cereal/messaging/tests/test_pub_sub_master.py
@@ -63,14 +63,13 @@ class TestSubMaster:
def test_update_timeout(self):
sock = random_sock()
sm = messaging.SubMaster([sock,])
- for _ in range(5):
- timeout = random.randrange(1000, 5000)
- start_time = time.monotonic()
- sm.update(timeout)
- t = time.monotonic() - start_time
- assert t >= timeout/1000.
- assert t < 5
- assert not any(sm.updated.values())
+ timeout = random.randrange(1000, 3000)
+ start_time = time.monotonic()
+ sm.update(timeout)
+ t = time.monotonic() - start_time
+ assert t >= timeout/1000.
+ assert t < 3
+ assert not any(sm.updated.values())
def test_avg_frequency_checks(self):
for poll in (True, False):
diff --git a/common/pid.py b/common/pid.py
index 29c4d8bd46..36cbf9c4e9 100644
--- a/common/pid.py
+++ b/common/pid.py
@@ -59,15 +59,13 @@ class PIDController:
if override:
self.i -= self.i_unwind_rate * float(np.sign(self.i))
else:
- i = self.i + error * self.k_i * self.i_rate
- control = self.p + i + self.d + self.f
-
- # Update when changing i will move the control away from the limits
- # or when i will move towards the sign of the error
- if ((error >= 0 and (control <= self.pos_limit or i < 0.0)) or
- (error <= 0 and (control >= self.neg_limit or i > 0.0))) and \
- not freeze_integrator:
- self.i = i
+ if not freeze_integrator:
+ self.i = self.i + error * self.k_i * self.i_rate
+
+ # Clip i to prevent exceeding control limits
+ control_no_i = self.p + self.d + self.f
+ control_no_i = clip(control_no_i, self.neg_limit, self.pos_limit)
+ self.i = clip(self.i, self.neg_limit - control_no_i, self.pos_limit - control_no_i)
control = self.p + self.i + self.d + self.f
diff --git a/docs/CARS.md b/docs/CARS.md
index 44c9327378..6861f411d2 100644
--- a/docs/CARS.md
+++ b/docs/CARS.md
@@ -30,21 +30,21 @@ A supported vehicle is one that just works when you install a comma device. All
|comma|body|All|openpilot|0 mph|0 mph|[](##)|[](##)|None||
|CUPRA|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[](##)|[](##)|Parts
- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Ford|Bronco Sport 2021-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Ford|Escape Hybrid 2020-22|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Ford|Escape Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Ford|Explorer 2020-23|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Ford|Explorer Hybrid 2020-23|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Ford|Focus 2018[3](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Ford|Focus Hybrid 2018[3](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Ford|Kuga 2020-22|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Ford|Kuga Hybrid 2020-22|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Ford|Kuga Plug-in Hybrid 2020-22|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Ford|Maverick 2022|LARIAT Luxury|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Ford|Maverick 2023-24|Co-Pilot360 Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Ford|Maverick Hybrid 2023-24|Co-Pilot360 Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Ford|Bronco Sport 2021-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Ford|Escape Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Ford|Escape Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Ford|Explorer 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Ford|Focus 2018[3](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Ford|Focus Hybrid 2018[3](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Ford|Kuga 2020-22|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Ford|Kuga Hybrid 2020-22|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Ford|Kuga Plug-in Hybrid 2020-22|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Ford|Maverick 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Ford|Maverick 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Ford|Maverick Hybrid 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Genesis|G70 2018|All|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai F connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Genesis|G70 2019-21|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai F connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Genesis|G70 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
@@ -184,8 +184,8 @@ A supported vehicle is one that just works when you install a comma device. All
|Lexus|RX Hybrid 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Lexus|RX Hybrid 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Lexus|UX Hybrid 2019-23|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Lincoln|Aviator 2020-24|Co-Pilot360 Plus|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Lincoln|Aviator Plug-in Hybrid 2020-24|Co-Pilot360 Plus|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Lincoln|Aviator 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Lincoln|Aviator Plug-in Hybrid 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|MAN|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|31 mph|[](##)|[](##)|Parts
- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
|MAN|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|31 mph|[](##)|[](##)|Parts
- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
|Mazda|CX-5 2022-24|All|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Mazda connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
diff --git a/docs/how-to/connect-to-comma.md b/docs/how-to/connect-to-comma.md
index 67327fbaaf..5797f8618f 100644
--- a/docs/how-to/connect-to-comma.md
+++ b/docs/how-to/connect-to-comma.md
@@ -11,6 +11,9 @@ On the comma three, the serial console is exposed through a UART-to-USB chip, an
On the comma 3X, the serial console is accessible through the [panda](https://github.com/commaai/panda) using the `panda/tests/som_debug.sh` script.
+ * Username: `comma`
+ * Password: `comma`
+
## SSH
In order to SSH into your device, you'll need a GitHub account with SSH keys. See this [GitHub article](https://docs.github.com/en/github/authenticating-to-github/connecting-to-github-with-ssh) for getting your account setup with SSH keys.
diff --git a/opendbc_repo b/opendbc_repo
index d632cc5bec..9b5f697a1e 160000
--- a/opendbc_repo
+++ b/opendbc_repo
@@ -1 +1 @@
-Subproject commit d632cc5bec14d4e077fdf25e19b24b434c2653fd
+Subproject commit 9b5f697a1ec82cf3a27ac0c93367ff91e83bedb2
diff --git a/panda b/panda
index 0b364ece1e..aab03bc4b6 160000
--- a/panda
+++ b/panda
@@ -1 +1 @@
-Subproject commit 0b364ece1eafa2e66b71be7cade3fdfb56a3014e
+Subproject commit aab03bc4b6ab02be7db3fd60f034a84d79ad93b4
diff --git a/selfdrive/car/tests/test_models.py b/selfdrive/car/tests/test_models.py
index 2a62127749..b50955b2a9 100644
--- a/selfdrive/car/tests/test_models.py
+++ b/selfdrive/car/tests/test_models.py
@@ -4,6 +4,7 @@ import pytest
import random
import unittest # noqa: TID251
from collections import defaultdict, Counter
+from functools import partial
import hypothesis.strategies as st
from hypothesis import Phase, given, settings
from parameterized import parameterized_class
@@ -22,7 +23,8 @@ from openpilot.selfdrive.selfdrived.selfdrived import SelfdriveD
from openpilot.selfdrive.pandad import can_capnp_to_list
from openpilot.selfdrive.test.helpers import read_segment_list
from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT
-from openpilot.tools.lib.logreader import LogReader, LogsUnavailable
+from openpilot.tools.lib.logreader import LogReader, LogsUnavailable, openpilotci_source_zst, openpilotci_source, internal_source, \
+ internal_source_zst, comma_api_source, auto_source
from openpilot.tools.lib.route import SegmentName
from panda.tests.libpanda import libpanda_py
@@ -93,7 +95,7 @@ class TestCarModelBase(unittest.TestCase):
car_fw = msg.carParams.carFw
if msg.carParams.openpilotLongitudinalControl:
experimental_long = True
- if cls.platform is None and not cls.test_route_on_bucket:
+ if cls.platform is None:
live_fingerprint = msg.carParams.carFingerprint
cls.platform = MIGRATION.get(live_fingerprint, live_fingerprint)
@@ -126,7 +128,9 @@ class TestCarModelBase(unittest.TestCase):
segment_range = f"{cls.test_route.route}/{seg}"
try:
- lr = LogReader(segment_range)
+ source = partial(auto_source, sources=[internal_source, internal_source_zst] if len(INTERNAL_SEG_LIST) else \
+ [openpilotci_source_zst, openpilotci_source, comma_api_source])
+ lr = LogReader(segment_range, source=source)
return cls.get_testing_data_from_logreader(lr)
except (LogsUnavailable, AssertionError):
pass
diff --git a/selfdrive/debug/check_can_parser_performance.py b/selfdrive/debug/check_can_parser_performance.py
index 2bddd362ae..7a0db1926b 100755
--- a/selfdrive/debug/check_can_parser_performance.py
+++ b/selfdrive/debug/check_can_parser_performance.py
@@ -14,7 +14,6 @@ N_RUNS = 10
class CarModelTestCase(TestCarModelBase):
test_route = CarTestRoute(DEMO_ROUTE, None)
- ci = False
if __name__ == '__main__':
diff --git a/selfdrive/modeld/dmonitoringmodeld.py b/selfdrive/modeld/dmonitoringmodeld.py
index 3030b31a0c..31440c1295 100755
--- a/selfdrive/modeld/dmonitoringmodeld.py
+++ b/selfdrive/modeld/dmonitoringmodeld.py
@@ -139,7 +139,6 @@ def main():
pm = PubMaster(["driverStateV2"])
calib = np.zeros(CALIB_LEN, dtype=np.float32)
- # last = 0
while True:
buf = vipc_client.recv()
@@ -155,8 +154,6 @@ def main():
t2 = time.perf_counter()
pm.send("driverStateV2", get_driverstate_packet(model_output, vipc_client.frame_id, vipc_client.timestamp_sof, t2 - t1, gpu_execution_time))
- # print("dmonitoring process: %.2fms, from last %.2fms\n" % (t2 - t1, t1 - last))
- # last = t1
if __name__ == "__main__":
diff --git a/selfdrive/modeld/models/supercombo.onnx b/selfdrive/modeld/models/supercombo.onnx
index 5ec978e566..2a0ddef57b 100644
--- a/selfdrive/modeld/models/supercombo.onnx
+++ b/selfdrive/modeld/models/supercombo.onnx
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2a845fd16d6482222c574db833d2badb37ebcdf9c7d2987ab347ef63e728a146
-size 50309976
+oid sha256:c829d824ebc73d15da82516592c07d9784369ccbf710698e919e06a702e70924
+size 50320138
diff --git a/selfdrive/pandad/pandad.cc b/selfdrive/pandad/pandad.cc
index b0b10fe315..a85159f33a 100644
--- a/selfdrive/pandad/pandad.cc
+++ b/selfdrive/pandad/pandad.cc
@@ -306,6 +306,16 @@ void send_peripheral_state(Panda *panda, PubMaster *pm) {
LOGW("reading hwmon took %lfms", read_time);
}
+ // fall back to panda's voltage and current measurement
+ if (ps.getVoltage() == 0 && ps.getCurrent() == 0) {
+ auto health_opt = panda->get_state();
+ if (health_opt) {
+ health_t health = *health_opt;
+ ps.setVoltage(health.voltage_pkt);
+ ps.setCurrent(health.current_pkt);
+ }
+ }
+
uint16_t fan_speed_rpm = panda->get_fan_speed();
ps.setFanSpeedRpm(fan_speed_rpm);
diff --git a/selfdrive/test/process_replay/migration.py b/selfdrive/test/process_replay/migration.py
index cb3db26344..5376f91aee 100644
--- a/selfdrive/test/process_replay/migration.py
+++ b/selfdrive/test/process_replay/migration.py
@@ -6,6 +6,7 @@ import capnp
from cereal import messaging, car, log
from opendbc.car.fingerprints import MIGRATION
from opendbc.car.toyota.values import EPS_SCALE
+from opendbc.car.ford.values import CAR as FORD, FordFlags
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.selfdrive.modeld.fill_model_msg import fill_xyz_poly, fill_lane_line_meta
from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_encode_index
@@ -270,6 +271,8 @@ def migrate_pandaStates(msgs):
"TOYOTA_RAV4": EPS_SCALE["TOYOTA_RAV4"] | Panda.FLAG_TOYOTA_ALT_BRAKE,
"KIA_EV6": Panda.FLAG_HYUNDAI_EV_GAS | Panda.FLAG_HYUNDAI_CANFD_HDA2,
}
+ # TODO: get new Ford route
+ safety_param_migration |= {car: Panda.FLAG_FORD_LONG_CONTROL for car in (set(FORD) - FORD.with_flags(FordFlags.CANFD))}
# Migrate safety param base on carParams
CP = next((m.carParams for _, m in msgs if m.which() == 'carParams'), None)
diff --git a/selfdrive/test/process_replay/model_replay.py b/selfdrive/test/process_replay/model_replay.py
index 7ffaea76b4..c6720cc601 100755
--- a/selfdrive/test/process_replay/model_replay.py
+++ b/selfdrive/test/process_replay/model_replay.py
@@ -58,10 +58,18 @@ def generate_report(proposed, master, tmp, commit):
(lambda x: x.laneLines[1].y[0], "laneLines.y"),
(lambda x: x.meta.disengagePredictions.gasPressProbs[1], "gasPressProbs")
], "modelV2")
+ DriverStateV2_Plots = zl([
+ (lambda x: x.wheelOnRightProb, "wheelOnRightProb"),
+ (lambda x: x.leftDriverData.faceProb, "leftDriverData.faceProb"),
+ (lambda x: x.leftDriverData.faceOrientation[0], "leftDriverData.faceOrientation0"),
+ (lambda x: x.leftDriverData.leftBlinkProb, "leftDriverData.leftBlinkProb"),
+ (lambda x: x.leftDriverData.notReadyProb[0], "leftDriverData.notReadyProb0"),
+ (lambda x: x.rightDriverData.faceProb, "rightDriverData.faceProb"),
+ ], "driverStateV2")
return [plot(map(v[0], get_event(proposed, event)), \
map(v[0], get_event(master, event)), f"{v[1]}_{commit[:7]}", tmp) \
- for v,event in [*ModelV2_Plots]]
+ for v,event in ([*ModelV2_Plots] + [*DriverStateV2_Plots])]
def create_table(title, files, link, open_table=False):
if not files:
diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit
index 2f565836fd..c779902d6b 100644
--- a/selfdrive/test/process_replay/ref_commit
+++ b/selfdrive/test/process_replay/ref_commit
@@ -1 +1 @@
-992ac80ef848afb85562ca24b1c5a3d410aacd05
\ No newline at end of file
+2fc2e865ab77fd8145feab86d454f2111c5d9871
diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py
index ec84f37d1f..4f0705f492 100755
--- a/selfdrive/test/process_replay/test_processes.py
+++ b/selfdrive/test/process_replay/test_processes.py
@@ -56,7 +56,7 @@ segments = [
("NISSAN", "regen58464878D07|2024-08-30--03-15-31--0"),
("VOLKSWAGEN", "regenED976DEB757|2024-08-30--03-18-02--0"),
("MAZDA", "regenACF84CCF482|2024-08-30--03-21-55--0"),
- ("FORD", "regen6ECC59A6307|2024-08-30--03-25-42--0"),
+ ("FORD", "regen756F8230C21|2024-11-07--00-08-24--0"),
]
# dashcamOnly makes don't need to be tested until a full port is done
diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py
index 35338419de..6167e12999 100644
--- a/selfdrive/test/test_onroad.py
+++ b/selfdrive/test/test_onroad.py
@@ -10,8 +10,8 @@ import time
import numpy as np
import zstandard as zstd
from collections import Counter, defaultdict
-from functools import cached_property
from pathlib import Path
+from tabulate import tabulate
from cereal import car, log
import cereal.messaging as messaging
@@ -33,6 +33,9 @@ CPU usage budget
should not exceed MAX_TOTAL_CPU
"""
+TEST_DURATION = 25
+LOG_OFFSET = 8
+
MAX_TOTAL_CPU = 265. # total for all 8 cores
PROCS = {
# Baseline CPU usage by process
@@ -49,28 +52,28 @@ PROCS = {
"selfdrive.controls.radard": 2.0,
"selfdrive.modeld.modeld": 17.0,
"selfdrive.modeld.dmonitoringmodeld": 11.0,
- "system.hardware.hardwared": 3.87,
+ "system.hardware.hardwared": 4.0,
"selfdrive.locationd.calibrationd": 2.0,
"selfdrive.locationd.torqued": 5.0,
"selfdrive.locationd.locationd": 25.0,
"selfdrive.ui.soundd": 3.0,
"selfdrive.monitoring.dmonitoringd": 4.0,
- "./proclogd": 1.54,
- "system.logmessaged": 0.2,
+ "./proclogd": 2.0,
+ "system.logmessaged": 1.0,
"system.tombstoned": 0,
- "./logcatd": 0,
+ "./logcatd": 1.0,
"system.micd": 5.0,
"system.timed": 0,
"selfdrive.pandad.pandad": 0,
- "system.statsd": 0.4,
- "system.loggerd.uploader": (0.5, 15.0),
- "system.loggerd.deleter": 0.1,
+ "system.statsd": 1.0,
+ "system.loggerd.uploader": 15.0,
+ "system.loggerd.deleter": 1.0,
}
PROCS.update({
"tici": {
"./pandad": 4.0,
- "./ubloxd": 0.02,
+ "./ubloxd": 1.0,
"system.ubloxd.pigeond": 6.0,
},
"tizi": {
@@ -98,6 +101,13 @@ TIMINGS = {
"wideRoadCameraState": [1.5, 0.35],
}
+LOGS_SIZE_RATE = {
+ "qlog": 0.0083,
+ "rlog": 0.1528,
+ "qcamera.ts": 0.03828,
+}
+LOGS_SIZE_RATE.update(dict.fromkeys(['ecamera.hevc', 'fcamera.hevc'], 1.2740))
+
def cputime_total(ct):
return ct.cpuUser + ct.cpuSystem + ct.cpuChildrenUser + ct.cpuChildrenSystem
@@ -124,7 +134,7 @@ class TestOnroad:
if os.path.exists(Paths.log_root()):
shutil.rmtree(Paths.log_root())
- # start manager and run openpilot for a minute
+ # start manager and run openpilot for TEST_DURATION
proc = None
try:
manager_path = os.path.join(BASEDIR, "system/manager/manager.py")
@@ -135,26 +145,24 @@ class TestOnroad:
while sm.recv_frame['carState'] < 0:
sm.update(1000)
- # make sure we get at least two full segments
route = None
cls.segments = []
with Timeout(300, "timed out waiting for logs"):
while route is None:
route = params.get("CurrentRoute", encoding="utf-8")
- time.sleep(0.1)
+ time.sleep(0.01)
# test car params caching
params.put("CarParamsCache", car.CarParams().to_bytes())
- while len(cls.segments) < 3:
+ while len(cls.segments) < 1:
segs = set()
if Path(Paths.log_root()).exists():
segs = set(Path(Paths.log_root()).glob(f"{route}--*"))
cls.segments = sorted(segs, key=lambda s: int(str(s).rsplit('--')[-1]))
- time.sleep(2)
+ time.sleep(0.01)
- # chop off last, incomplete segment
- cls.segments = cls.segments[:-1]
+ time.sleep(TEST_DURATION)
finally:
cls.gpu_procs = {psutil.Process(int(f.name)).name() for f in pathlib.Path('/sys/devices/virtual/kgsl/kgsl/proc/').iterdir() if f.is_dir()}
@@ -166,9 +174,8 @@ class TestOnroad:
cls.lrs = [list(LogReader(os.path.join(str(s), "rlog"))) for s in cls.segments]
- # use the second segment by default as it's the first full segment
- cls.lr = list(LogReader(os.path.join(str(cls.segments[1]), "rlog")))
- cls.log_path = cls.segments[1]
+ cls.lr = list(LogReader(os.path.join(str(cls.segments[0]), "rlog")))
+ cls.log_path = cls.segments[0]
cls.log_sizes = {}
for f in cls.log_path.iterdir():
@@ -178,16 +185,13 @@ class TestOnroad:
with open(f, 'rb') as ff:
cls.log_sizes[f] = len(zstd.compress(ff.read(), LOG_COMPRESSION_LEVEL)) / 1e6
+ cls.msgs = defaultdict(list)
+ for m in cls.lr:
+ cls.msgs[m.which()].append(m)
- @cached_property
- def service_msgs(self):
- msgs = defaultdict(list)
- for m in self.lr:
- msgs[m.which()].append(m)
- return msgs
def test_service_frequencies(self, subtests):
- for s, msgs in self.service_msgs.items():
+ for s, msgs in self.msgs.items():
if s in ('initData', 'sentinel'):
continue
@@ -196,10 +200,10 @@ class TestOnroad:
continue
with subtests.test(service=s):
- assert len(msgs) >= math.floor(SERVICE_LIST[s].frequency*55)
+ assert len(msgs) >= math.floor(SERVICE_LIST[s].frequency*int(TEST_DURATION*0.8))
def test_cloudlog_size(self):
- msgs = [m for m in self.lr if m.which() == 'logMessage']
+ msgs = self.msgs['logMessage']
total_size = sum(len(m.as_builder().to_bytes()) for m in msgs)
assert total_size < 3.5e5
@@ -210,16 +214,10 @@ class TestOnroad:
def test_log_sizes(self):
for f, sz in self.log_sizes.items():
- if f.name == "qcamera.ts":
- assert 2.15 < sz < 2.6
- elif f.name == "qlog":
- assert 0.4 < sz < 0.55
- elif f.name == "rlog":
- assert 5 < sz < 50
- elif f.name.endswith('.hevc'):
- assert 70 < sz < 80
- else:
- raise NotImplementedError
+ rate = LOGS_SIZE_RATE[f.name]
+ minn = rate * TEST_DURATION * 0.8
+ maxx = rate * TEST_DURATION * 1.2
+ assert minn < sz < maxx
def test_ui_timings(self):
result = "\n"
@@ -227,7 +225,7 @@ class TestOnroad:
result += "-------------- UI Draw Timing ------------------\n"
result += "------------------------------------------------\n"
- ts = [m.uiDebug.drawTimeMillis for m in self.service_msgs['uiDebug']]
+ ts = [m.uiDebug.drawTimeMillis for m in self.msgs['uiDebug']]
result += f"min {min(ts):.2f}ms\n"
result += f"max {max(ts):.2f}ms\n"
result += f"std {np.std(ts):.2f}ms\n"
@@ -244,53 +242,44 @@ class TestOnroad:
assert len(veryslow) < 5, f"Too many slow frame draw times: {veryslow}"
def test_cpu_usage(self, subtests):
- result = "\n"
- result += "------------------------------------------------\n"
- result += "------------------ CPU Usage -------------------\n"
- result += "------------------------------------------------\n"
+ print("\n------------------------------------------------")
+ print("------------------ CPU Usage -------------------")
+ print("------------------------------------------------")
plogs_by_proc = defaultdict(list)
- for pl in self.service_msgs['procLog']:
+ for pl in self.msgs['procLog']:
for x in pl.procLog.procs:
if len(x.cmdline) > 0:
n = list(x.cmdline)[0]
plogs_by_proc[n].append(x)
- print(plogs_by_proc.keys())
cpu_ok = True
- dt = (self.service_msgs['procLog'][-1].logMonoTime - self.service_msgs['procLog'][0].logMonoTime) / 1e9
- for proc_name, expected_cpu in PROCS.items():
+ dt = (self.msgs['procLog'][-1].logMonoTime - self.msgs['procLog'][0].logMonoTime) / 1e9
+ header = ['process', 'usage', 'expected', 'max allowed', 'test result']
+ rows = []
+ for proc_name, expected in PROCS.items():
- err = ""
- exp = "???"
- cpu_usage = 0.
+ error = ""
+ usage = 0.
x = plogs_by_proc[proc_name]
if len(x) > 2:
cpu_time = cputime_total(x[-1]) - cputime_total(x[0])
- cpu_usage = cpu_time / dt * 100.
-
- if isinstance(expected_cpu, tuple):
- exp = str(expected_cpu)
- minn, maxx = expected_cpu
- else:
- exp = f"{expected_cpu:5.2f}"
- minn = min(expected_cpu * 0.65, max(expected_cpu - 1.0, 0.0))
- maxx = max(expected_cpu * 1.15, expected_cpu + 5.0)
-
- if cpu_usage > maxx:
- err = "using more CPU than expected"
- elif cpu_usage < minn:
- err = "using less CPU than expected"
- else:
- err = "NO METRICS FOUND"
+ usage = cpu_time / dt * 100.
+
+ max_allowed = max(expected * 1.8, expected + 5.0)
+ if usage > max_allowed:
+ error = "❌ USING MORE CPU THAN EXPECTED ❌"
+ cpu_ok = False
- result += f"{proc_name.ljust(35)} {cpu_usage:5.2f}% ({exp}%) {err}\n"
- if len(err) > 0:
+ else:
+ error = "❌ NO METRICS FOUND ❌"
cpu_ok = False
- result += "------------------------------------------------\n"
+
+ rows.append([proc_name, usage, expected, max_allowed, error or "✅"])
+ print(tabulate(rows, header, tablefmt="simple_grid", stralign="center", numalign="center", floatfmt=".2f"))
# Ensure there's no missing procs
- all_procs = {p.name for p in self.service_msgs['managerState'][0].managerState.processes if p.shouldBeRunning}
+ all_procs = {p.name for p in self.msgs['managerState'][0].managerState.processes if p.shouldBeRunning}
for p in all_procs:
with subtests.test(proc=p):
assert any(p in pp for pp in PROCS.keys()), f"Expected CPU usage missing for {p}"
@@ -299,16 +288,15 @@ class TestOnroad:
procs_tot = sum([(max(x) if isinstance(x, tuple) else x) for x in PROCS.values()])
with subtests.test(name="total CPU"):
assert procs_tot < MAX_TOTAL_CPU, "Total CPU budget exceeded"
- result += "------------------------------------------------\n"
- result += f"Total allocated CPU usage is {procs_tot}%, budget is {MAX_TOTAL_CPU}%, {MAX_TOTAL_CPU-procs_tot:.1f}% left\n"
- result += "------------------------------------------------\n"
-
- print(result)
+ print("------------------------------------------------")
+ print(f"Total allocated CPU usage is {procs_tot}%, budget is {MAX_TOTAL_CPU}%, {MAX_TOTAL_CPU-procs_tot:.1f}% left")
+ print("------------------------------------------------")
assert cpu_ok
def test_memory_usage(self):
- mems = [m.deviceState.memoryUsagePercent for m in self.service_msgs['deviceState']]
+ offset = int(SERVICE_LIST['deviceState'].frequency * LOG_OFFSET)
+ mems = [m.deviceState.memoryUsagePercent for m in self.msgs['deviceState'][offset:]]
print("Memory usage: ", mems)
# check for big leaks. note that memory usage is
@@ -324,7 +312,9 @@ class TestOnroad:
result += "-------------- ImgProc Timing ------------------\n"
result += "------------------------------------------------\n"
- ts = [getattr(m, m.which()).processingTime for m in self.lr if 'CameraState' in m.which()]
+ ts = []
+ for s in ['roadCameraState', 'driverCameraState', 'wideCameraState']:
+ ts.extend(getattr(m, s).processingTime for m in self.msgs[s])
assert min(ts) < 0.025, f"high execution time: {min(ts)}"
result += f"execution time: min {min(ts):.5f}s\n"
result += f"execution time: max {max(ts):.5f}s\n"
@@ -357,7 +347,7 @@ class TestOnroad:
cfgs = [("longitudinalPlan", 0.05, 0.05),]
for (s, instant_max, avg_max) in cfgs:
- ts = [getattr(m, s).solverExecutionTime for m in self.service_msgs[s]]
+ ts = [getattr(m, s).solverExecutionTime for m in self.msgs[s]]
assert max(ts) < instant_max, f"high '{s}' execution time: {max(ts)}"
assert np.mean(ts) < avg_max, f"high avg '{s}' execution time: {np.mean(ts)}"
result += f"'{s}' execution time: min {min(ts):.5f}s\n"
@@ -377,7 +367,7 @@ class TestOnroad:
("driverStateV2", 0.050, 0.026),
]
for (s, instant_max, avg_max) in cfgs:
- ts = [getattr(m, s).modelExecutionTime for m in self.service_msgs[s]]
+ ts = [getattr(m, s).modelExecutionTime for m in self.msgs[s]]
assert max(ts) < instant_max, f"high '{s}' execution time: {max(ts)}"
assert np.mean(ts) < avg_max, f"high avg '{s}' execution time: {np.mean(ts)}"
result += f"'{s}' execution time: min {min(ts):.5f}s\n"
@@ -388,33 +378,32 @@ class TestOnroad:
def test_timings(self):
passed = True
- result = "\n"
- result += "------------------------------------------------\n"
- result += "----------------- Service Timings --------------\n"
- result += "------------------------------------------------\n"
+ print("\n------------------------------------------------")
+ print("----------------- Service Timings --------------")
+ print("------------------------------------------------")
+
+ header = ['service', 'max', 'min', 'mean', 'expected mean', 'rsd', 'max allowed rsd', 'test result']
+ rows = []
for s, (maxmin, rsd) in TIMINGS.items():
- msgs = [m.logMonoTime for m in self.service_msgs[s]]
+ offset = int(SERVICE_LIST[s].frequency * LOG_OFFSET)
+ msgs = [m.logMonoTime for m in self.msgs[s][offset:]]
if not len(msgs):
raise Exception(f"missing {s}")
ts = np.diff(msgs) / 1e9
dt = 1 / SERVICE_LIST[s].frequency
- try:
- np.testing.assert_allclose(np.mean(ts), dt, rtol=0.03, err_msg=f"{s} - failed mean timing check")
- np.testing.assert_allclose([np.max(ts), np.min(ts)], dt, rtol=maxmin, err_msg=f"{s} - failed max/min timing check")
- except Exception as e:
- result += str(e) + "\n"
- passed = False
-
- if np.std(ts) / dt > rsd:
- result += f"{s} - failed RSD timing check\n"
- passed = False
-
- result += f"{s.ljust(40)}: {np.array([np.mean(ts), np.max(ts), np.min(ts)])*1e3}\n"
- result += f"{''.ljust(40)} {np.max(np.absolute([np.max(ts)/dt, np.min(ts)/dt]))} {np.std(ts)/dt}\n"
- result += "="*67
- print(result)
+ errors = []
+ if not np.allclose(np.mean(ts), dt, rtol=0.03, atol=0):
+ errors.append("❌ FAILED MEAN TIMING CHECK ❌")
+ if not np.allclose([np.max(ts), np.min(ts)], dt, rtol=maxmin, atol=0):
+ errors.append("❌ FAILED MAX/MIN TIMING CHECK ❌")
+ if (np.std(ts)/dt) > rsd:
+ errors.append("❌ FAILED RSD TIMING CHECK ❌")
+ passed = not errors
+ rows.append([s, *(np.array([np.max(ts), np.min(ts), np.mean(ts), dt])*1e3), np.std(ts)/dt, rsd, "\n".join(errors) or "✅"])
+
+ print(tabulate(rows, header, tablefmt="simple_grid", stralign="center", numalign="center", floatfmt=".2f"))
assert passed
@release_only
@@ -430,11 +419,12 @@ class TestOnroad:
def test_engagable(self):
no_entries = Counter()
- for m in self.service_msgs['onroadEvents']:
+ for m in self.msgs['onroadEvents']:
for evt in m.onroadEvents:
if evt.noEntry:
no_entries[evt.name] += 1
- eng = [m.selfdriveState.engageable for m in self.service_msgs['selfdriveState']]
+ offset = int(SERVICE_LIST['selfdriveState'].frequency * LOG_OFFSET)
+ eng = [m.selfdriveState.engageable for m in self.msgs['selfdriveState'][offset:]]
assert all(eng), \
f"Not engageable for whole segment:\n- selfdriveState.engageable: {Counter(eng)}\n- No entry events: {no_entries}"
diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript
index 19dcc625e2..81c18d03df 100644
--- a/selfdrive/ui/SConscript
+++ b/selfdrive/ui/SConscript
@@ -92,7 +92,7 @@ if GetOption('extras') and arch != "Darwin":
("openpilot", release),
("openpilot_test", f"{release}-staging"),
("openpilot_nightly", "nightly"),
- ("openpilot_internal", "master"),
+ ("openpilot_internal", "nightly-dev"),
]
cont = senv.Command(f"installer/continue_openpilot.o", f"installer/continue_openpilot.sh",
@@ -110,3 +110,5 @@ if GetOption('extras') and arch != "Darwin":
# build watch3
if arch in ['x86_64', 'aarch64', 'Darwin'] or GetOption('extras'):
qt_env.Program("watch3", ["watch3.cc"], LIBS=qt_libs + ['common', 'msgq', 'visionipc'])
+
+SConscript(['raylib/SConscript'])
diff --git a/selfdrive/ui/qt/offroad/software_settings.cc b/selfdrive/ui/qt/offroad/software_settings.cc
index c8245205ce..194b723fc9 100644
--- a/selfdrive/ui/qt/offroad/software_settings.cc
+++ b/selfdrive/ui/qt/offroad/software_settings.cc
@@ -54,7 +54,7 @@ SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) {
connect(targetBranchBtn, &ButtonControl::clicked, [=]() {
auto current = params.get("GitBranch");
QStringList branches = QString::fromStdString(params.get("UpdaterAvailableBranches")).split(",");
- for (QString b : {current.c_str(), "devel-staging", "devel", "nightly", "master-ci", "master"}) {
+ for (QString b : {current.c_str(), "devel-staging", "devel", "nightly", "nightly-dev", "master-ci", "master"}) {
auto i = branches.indexOf(b);
if (i >= 0) {
branches.removeAt(i);
diff --git a/selfdrive/ui/qt/setup/setup.cc b/selfdrive/ui/qt/setup/setup.cc
index aff9b015b3..c716b6b4e9 100644
--- a/selfdrive/ui/qt/setup/setup.cc
+++ b/selfdrive/ui/qt/setup/setup.cc
@@ -408,7 +408,7 @@ Setup::Setup(QWidget *parent) : QStackedWidget(parent) {
std::stringstream buffer;
buffer << std::ifstream("/sys/class/hwmon/hwmon1/in1_input").rdbuf();
float voltage = (float)std::atoi(buffer.str().c_str()) / 1000.;
- if (voltage < 7) {
+ if (voltage > 0 && voltage < 7) {
addWidget(low_voltage());
}
diff --git a/selfdrive/ui/raylib/.gitignore b/selfdrive/ui/raylib/.gitignore
new file mode 100644
index 0000000000..c66ae096aa
--- /dev/null
+++ b/selfdrive/ui/raylib/.gitignore
@@ -0,0 +1 @@
+_spinner
diff --git a/selfdrive/ui/raylib/SConscript b/selfdrive/ui/raylib/SConscript
new file mode 100644
index 0000000000..d603f263e1
--- /dev/null
+++ b/selfdrive/ui/raylib/SConscript
@@ -0,0 +1,17 @@
+Import('env', 'arch', 'common')
+
+raylib_env = env.Clone()
+raylib_util_lib = env.Library("raylib_util_lib", ['util.cc'], LIBS='raylib')
+linked_libs = ['raylib', raylib_util_lib, common]
+raylib_env['LIBPATH'] += [f'#third_party/raylib/{arch}/']
+
+mac_frameworks = []
+if arch == "Darwin":
+ mac_frameworks += ['OpenCL', 'CoreVideo', 'Cocoa', 'GLUT', 'CoreFoundation', 'OpenGL', 'IOKit']
+elif arch == 'larch64':
+ linked_libs += []
+else:
+ linked_libs += ['OpenCL', 'dl', 'pthread']
+
+if arch != 'aarch64':
+ raylib_env.Program("_spinner", ["spinner.cc"], LIBS=linked_libs, FRAMEWORKS=mac_frameworks)
diff --git a/selfdrive/ui/raylib/spinner.cc b/selfdrive/ui/raylib/spinner.cc
new file mode 100644
index 0000000000..99aa5f3269
--- /dev/null
+++ b/selfdrive/ui/raylib/spinner.cc
@@ -0,0 +1,69 @@
+#include
+#include
+#include
+
+#include "selfdrive/ui/raylib/util.h"
+#include "third_party/raylib/include/raylib.h"
+
+constexpr int kProgressBarWidth = 1000;
+constexpr int kProgressBarHeight = 20;
+constexpr float kRotationRate = 12.0f;
+constexpr int kMargin = 200;
+constexpr int kTextureSize = 360;
+constexpr int kFontSize = 80;
+
+int main(int argc, char *argv[]) {
+ initApp("spinner", 30);
+
+ // Turn off input buffering for std::cin
+ std::cin.sync_with_stdio(false);
+ std::cin.tie(nullptr);
+
+ Texture2D commaTexture = LoadTextureResized("../../assets/img_spinner_comma.png", kTextureSize);
+ Texture2D spinnerTexture = LoadTextureResized("../../assets/img_spinner_track.png", kTextureSize);
+
+ float rotation = 0.0f;
+ std::string userInput;
+
+ while (!WindowShouldClose()) {
+ BeginDrawing();
+ ClearBackground(BLACK);
+
+ rotation = fmod(rotation + kRotationRate, 360.0f);
+ Vector2 center = {GetScreenWidth() / 2.0f, GetScreenHeight() / 2.0f};
+ const Vector2 spinnerOrigin{kTextureSize / 2.0f, kTextureSize / 2.0f};
+ const Vector2 commaPosition{center.x - kTextureSize / 2.0f, center.y - kTextureSize / 2.0f};
+
+ // Draw rotating spinner and static comma logo
+ DrawTexturePro(spinnerTexture, {0, 0, (float)kTextureSize, (float)kTextureSize},
+ {center.x, center.y, (float)kTextureSize, (float)kTextureSize},
+ spinnerOrigin, rotation, WHITE);
+ DrawTextureV(commaTexture, commaPosition, WHITE);
+
+ // Check for user input
+ if (std::cin.rdbuf()->in_avail() > 0) {
+ std::getline(std::cin, userInput);
+ }
+
+ // Display either a progress bar or user input text based on input
+ if (!userInput.empty()) {
+ float yPos = GetScreenHeight() - kMargin - kProgressBarHeight;
+ if (std::all_of(userInput.begin(), userInput.end(), ::isdigit)) {
+ Rectangle bar = {center.x - kProgressBarWidth / 2.0f, yPos, kProgressBarWidth, kProgressBarHeight};
+ DrawRectangleRounded(bar, 0.5f, 10, GRAY);
+
+ int progress = std::clamp(std::stoi(userInput), 0, 100);
+ bar.width *= progress / 100.0f;
+ DrawRectangleRounded(bar, 0.5f, 10, RAYWHITE);
+ } else {
+ Vector2 textSize = MeasureTextEx(getFont(), userInput.c_str(), kFontSize, 1.0);
+ DrawTextEx(getFont(), userInput.c_str(), {center.x - textSize.x / 2, yPos}, kFontSize, 1.0, WHITE);
+ }
+ }
+
+ EndDrawing();
+ }
+
+ CloseWindow();
+ return 0;
+}
diff --git a/selfdrive/ui/raylib/util.cc b/selfdrive/ui/raylib/util.cc
new file mode 100644
index 0000000000..73c0e4e0b7
--- /dev/null
+++ b/selfdrive/ui/raylib/util.cc
@@ -0,0 +1,56 @@
+#include "selfdrive/ui/raylib/util.h"
+
+#include
+
+#undef GREEN
+#undef RED
+#undef YELLOW
+#include "common/swaglog.h"
+#include "system/hardware/hw.h"
+
+constexpr std::array(FontWeight::Count)> FONT_FILE_PATHS = {
+ "../../assets/fonts/Inter-Black.ttf",
+ "../../assets/fonts/Inter-Bold.ttf",
+ "../../assets/fonts/Inter-ExtraBold.ttf",
+ "../../assets/fonts/Inter-ExtraLight.ttf",
+ "../../assets/fonts/Inter-Medium.ttf",
+ "../../assets/fonts/Inter-Regular.ttf",
+ "../../assets/fonts/Inter-SemiBold.ttf",
+ "../../assets/fonts/Inter-Thin.ttf",
+};
+
+struct FontManager {
+ FontManager() {
+ for (int i = 0; i < fonts.size(); ++i) {
+ fonts[i] = LoadFontEx(FONT_FILE_PATHS[i], 120, nullptr, 250);
+ SetTextureFilter(fonts[i].texture, TEXTURE_FILTER_TRILINEAR);
+ }
+ }
+
+ ~FontManager() {
+ for (auto &f : fonts) UnloadFont(f);
+ }
+
+ std::array(FontWeight::Count)> fonts;
+};
+
+const Font& getFont(FontWeight weight) {
+ static FontManager font_manager;
+ return font_manager.fonts[(int)weight];
+}
+
+Texture2D LoadTextureResized(const char *fileName, int size) {
+ Image img = LoadImage(fileName);
+ ImageResize(&img, size, size);
+ Texture2D texture = LoadTextureFromImage(img);
+ SetTextureFilter(texture, TEXTURE_FILTER_TRILINEAR);
+ return texture;
+}
+
+void initApp(const char *title, int fps) {
+ Hardware::set_display_power(true);
+ Hardware::set_brightness(65);
+ // SetTraceLogLevel(LOG_NONE);
+ InitWindow(0, 0, title);
+ SetTargetFPS(fps);
+}
diff --git a/selfdrive/ui/raylib/util.h b/selfdrive/ui/raylib/util.h
new file mode 100644
index 0000000000..da2ec7118b
--- /dev/null
+++ b/selfdrive/ui/raylib/util.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include
+
+#include "third_party/raylib/include/raylib.h"
+
+enum class FontWeight {
+ Normal,
+ Bold,
+ ExtraBold,
+ ExtraLight,
+ Medium,
+ Regular,
+ SemiBold,
+ Thin,
+ Count // To represent the total number of fonts
+};
+
+void initApp(const char *title, int fps);
+const Font& getFont(FontWeight weight = FontWeight::Normal);
+Texture2D LoadTextureResized(const char *fileName, int size);
diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts
index 6a551b5afe..bca8cef363 100644
--- a/selfdrive/ui/translations/main_ar.ts
+++ b/selfdrive/ui/translations/main_ar.ts
@@ -117,11 +117,11 @@
DeveloperPanel
Joystick Debug Mode
-
+ وضع تصحيح أخطاء عصا التحكم
Longitudinal Maneuver Mode
-
+ وضع المناورة الطولية
openpilot Longitudinal Control (Alpha)
@@ -455,11 +455,11 @@
Waiting to start
-
+ في انتظار البدء
System Unresponsive
-
+ النظام لا يستجيب
@@ -651,7 +651,7 @@ This may take up to a minute.
Developer
-
+ المطور
diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts
index 56799790fa..c7107b3163 100644
--- a/selfdrive/ui/translations/main_es.ts
+++ b/selfdrive/ui/translations/main_es.ts
@@ -117,11 +117,11 @@
DeveloperPanel
Joystick Debug Mode
-
+ Modo de depuración de joystick
Longitudinal Maneuver Mode
-
+ Modo de maniobra longitudinal
openpilot Longitudinal Control (Alpha)
@@ -635,7 +635,7 @@ Esto puede tardar un minuto.
Developer
-
+ Desarrollador
diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts
index 92c94ab4bc..1ce2b0e16c 100644
--- a/selfdrive/ui/translations/main_ko.ts
+++ b/selfdrive/ui/translations/main_ko.ts
@@ -117,11 +117,11 @@
DeveloperPanel
Joystick Debug Mode
-
+ 조이스틱 디버그 모드
Longitudinal Maneuver Mode
-
+ 롱컨 기동 모드
openpilot Longitudinal Control (Alpha)
@@ -631,7 +631,7 @@ This may take up to a minute.
Developer
-
+ 개발자
diff --git a/system/camerad/cameras/camera_common.cc b/system/camerad/cameras/camera_common.cc
index 97df4ea75f..ff02fe4364 100644
--- a/system/camerad/cameras/camera_common.cc
+++ b/system/camerad/cameras/camera_common.cc
@@ -114,7 +114,7 @@ bool CameraBuf::acquire(int expo_time) {
cur_frame_data.processing_time = (millis_since_boot() - start_time) / 1000.0;
} else {
cur_yuv_buf = vipc_server->get_buffer(stream_type, cur_buf_idx);
- cur_frame_data.processing_time = (double)(cur_frame_data.timestamp_end_of_isp - cur_frame_data.timestamp_eof)*1e-6;
+ cur_frame_data.processing_time = (double)(cur_frame_data.timestamp_end_of_isp - cur_frame_data.timestamp_eof)*1e-9;
}
VisionIpcBufExtra extra = {
diff --git a/system/camerad/cameras/camera_qcom2.cc b/system/camerad/cameras/camera_qcom2.cc
index fb325ac772..0c102051a5 100644
--- a/system/camerad/cameras/camera_qcom2.cc
+++ b/system/camerad/cameras/camera_qcom2.cc
@@ -55,7 +55,7 @@ public:
float fl_pix = 0;
- CameraState(SpectraMaster *master, const CameraConfig &config) : camera(master, config, true /*config.stream_type == VISION_STREAM_ROAD*/) {};
+ CameraState(SpectraMaster *master, const CameraConfig &config) : camera(master, config, config.stream_type == VISION_STREAM_ROAD) {};
~CameraState();
void init(VisionIpcServer *v, cl_device_id device_id, cl_context ctx);
void update_exposure_score(float desired_ev, int exp_t, int exp_g_idx, float exp_gain);
diff --git a/system/camerad/sensors/ar0231.cc b/system/camerad/sensors/ar0231.cc
index e4ef3af404..5234ebd418 100644
--- a/system/camerad/sensors/ar0231.cc
+++ b/system/camerad/sensors/ar0231.cc
@@ -78,6 +78,7 @@ float ar0231_parse_temp_sensor(uint16_t calib1, uint16_t calib2, uint16_t data_r
AR0231::AR0231() {
image_sensor = cereal::FrameData::ImageSensor::AR0231;
+ bayer_pattern = CAM_ISP_PATTERN_BAYER_GRGRGR;
pixel_size_mm = 0.003;
data_word = true;
frame_width = 1928;
@@ -144,16 +145,19 @@ AR0231::AR0231() {
0x020007ff, 0x020007ff, 0x020007ff, 0x020007ff,
0x02000bff, 0x02000bff, 0x02000bff, 0x02000bff,
0x020017ff, 0x020017ff, 0x020017ff, 0x020017ff,
- 0x020006ff, 0x020006ff, 0x020006ff, 0x020006ff,
0x02001bff, 0x02001bff, 0x02001bff, 0x02001bff,
0x020023ff, 0x020023ff, 0x020023ff, 0x020023ff,
0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff,
0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff,
+ 0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff,
};
for (int i = 0; i < 252; i++) {
linearization_lut.push_back(0x0);
}
- linearization_pts = {0x07ff0bff, 0x17ff06ff, 0x1bff23ff, 0x3fff3fff};
+ linearization_pts = {0x07ff0bff, 0x17ff1bff, 0x23ff3fff, 0x3fff3fff};
+ for (int i = 0; i < 884*2; i++) {
+ vignetting_lut.push_back(0xff);
+ }
}
void AR0231::processRegisters(uint8_t *cur_buf, cereal::FrameData::Builder &framed) const {
diff --git a/system/camerad/sensors/os04c10.cc b/system/camerad/sensors/os04c10.cc
index 53ac37f08a..f6ba4504e1 100644
--- a/system/camerad/sensors/os04c10.cc
+++ b/system/camerad/sensors/os04c10.cc
@@ -22,6 +22,7 @@ const uint32_t os04c10_analog_gains_reg[] = {
OS04C10::OS04C10() {
image_sensor = cereal::FrameData::ImageSensor::OS04C10;
+ bayer_pattern = CAM_ISP_PATTERN_BAYER_BGBGBG;
pixel_size_mm = 0.004;
data_word = false;
@@ -70,7 +71,7 @@ OS04C10::OS04C10() {
};
for (int i = 0; i < 65; i++) {
float fx = i / 64.0;
- gamma_lut_rgb.push_back((uint32_t)(pow(fx, 0.7)*1023.0 + 0.5));
+ gamma_lut_rgb.push_back((uint32_t)((10*fx)/(1+9*fx)*1023.0 + 0.5));
}
prepare_gamma_lut();
linearization_lut = {
@@ -78,16 +79,19 @@ OS04C10::OS04C10() {
0x020007ff, 0x020007ff, 0x020007ff, 0x020007ff,
0x02000bff, 0x02000bff, 0x02000bff, 0x02000bff,
0x020017ff, 0x020017ff, 0x020017ff, 0x020017ff,
- 0x020006ff, 0x020006ff, 0x020006ff, 0x020006ff,
0x02001bff, 0x02001bff, 0x02001bff, 0x02001bff,
0x020023ff, 0x020023ff, 0x020023ff, 0x020023ff,
0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff,
0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff,
+ 0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff,
};
for (int i = 0; i < 252; i++) {
linearization_lut.push_back(0x0);
}
- linearization_pts = {0x07ff0bff, 0x17ff06ff, 0x1bff23ff, 0x3fff3fff};
+ linearization_pts = {0x07ff0bff, 0x17ff1bff, 0x23ff3fff, 0x3fff3fff};
+ for (int i = 0; i < 884*2; i++) {
+ vignetting_lut.push_back(0xff);
+ }
}
std::vector OS04C10::getExposureRegisters(int exposure_time, int new_exp_g, bool dc_gain_enabled) const {
diff --git a/system/camerad/sensors/os04c10_cl.h b/system/camerad/sensors/os04c10_cl.h
index 7cfc99655e..e285fb37e0 100644
--- a/system/camerad/sensors/os04c10_cl.h
+++ b/system/camerad/sensors/os04c10_cl.h
@@ -52,11 +52,7 @@ float3 color_correct(float3 rgb) {
}
float3 apply_gamma(float3 rgb, int expo_time) {
- return powr(rgb, 0.7);
-/*float s = log2((float)expo_time);
- if (s < 6) {s = fmin(12.0 - s, 9.0);}
- // log function adaptive to number of bits
- return clamp(log(1 + rgb*(PV_MAX16 - BLACK_LVL)) * (0.48*s*s - 12.92*s + 115.0) - (1.08*s*s - 29.2*s + 260.0), 0.0, 255.0) / 255.0;*/
+ return (10 * rgb) / (1 + 9 * rgb);
}
#endif
diff --git a/system/camerad/sensors/ox03c10.cc b/system/camerad/sensors/ox03c10.cc
index 19742661b1..d8cdc89648 100644
--- a/system/camerad/sensors/ox03c10.cc
+++ b/system/camerad/sensors/ox03c10.cc
@@ -25,6 +25,7 @@ const uint32_t VS_TIME_MAX_OX03C10 = 34; // vs < 35
OX03C10::OX03C10() {
image_sensor = cereal::FrameData::ImageSensor::OX03C10;
+ bayer_pattern = CAM_ISP_PATTERN_BAYER_GRGRGR;
pixel_size_mm = 0.003;
data_word = false;
frame_width = 1928;
@@ -80,9 +81,9 @@ OX03C10::OX03C10() {
0x00200000, 0x00200000, 0x00200000, 0x00200000,
0x00404080, 0x00404080, 0x00404080, 0x00404080,
0x00804100, 0x00804100, 0x00804100, 0x00804100,
- 0x006b8402, 0x006b8402, 0x006b8402, 0x006b8402,
- 0x00b8c070, 0x00b8c070, 0x00b8c070, 0x00b8c070,
- 0x06044804, 0x06044804, 0x06044804, 0x06044804,
+ 0x02014402, 0x02014402, 0x02014402, 0x02014402,
+ 0x0402c804, 0x0402c804, 0x0402c804, 0x0402c804,
+ 0x0805d00a, 0x0805d00a, 0x0805d00a, 0x0805d00a,
0x100ba015, 0x100ba015, 0x100ba015, 0x100ba015,
0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff,
0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff,
@@ -90,7 +91,7 @@ OX03C10::OX03C10() {
for (int i = 0; i < 252; i++) {
linearization_lut.push_back(0x0);
}
- linearization_pts = {0x07ff0bff, 0x17ff06ff, 0x1bff23ff, 0x27ff3fff};
+ linearization_pts = {0x07ff0bff, 0x17ff1bff, 0x1fff23ff, 0x27ff3fff};
for (int i = 0; i < 884*2; i++) {
vignetting_lut.push_back(0xff);
}
diff --git a/system/camerad/sensors/sensor.h b/system/camerad/sensors/sensor.h
index dc2aadfe13..1651fd8061 100644
--- a/system/camerad/sensors/sensor.h
+++ b/system/camerad/sensors/sensor.h
@@ -61,7 +61,7 @@ public:
std::vector init_reg_array;
uint32_t bits_per_pixel;
- uint32_t bayer_pattern = CAM_ISP_PATTERN_BAYER_GRGRGR;
+ uint32_t bayer_pattern;
uint32_t mipi_format;
uint32_t mclk_frequency;
uint32_t frame_data_type;
diff --git a/system/camerad/test/test_camerad.py b/system/camerad/test/test_camerad.py
index ada9594895..07a68e0020 100644
--- a/system/camerad/test/test_camerad.py
+++ b/system/camerad/test/test_camerad.py
@@ -9,7 +9,7 @@ from cereal import log
from cereal.services import SERVICE_LIST
from openpilot.system.manager.process_config import managed_processes
-TEST_TIMESPAN = 30
+TEST_TIMESPAN = 10
LAG_FRAME_TOLERANCE = {log.FrameData.ImageSensor.ar0231: 0.5, # ARs use synced pulses for frame starts
log.FrameData.ImageSensor.ox03c10: 1.1} # OXs react to out-of-sync at next frame
FRAME_DELTA_TOLERANCE = {log.FrameData.ImageSensor.ar0231: 1.0,
@@ -21,39 +21,40 @@ CAMERAS = ('roadCameraState', 'driverCameraState', 'wideRoadCameraState')
@flaky(max_runs=3)
@pytest.mark.tici
class TestCamerad:
- def setup_method(self):
+ @classmethod
+ def setup_class(cls):
# run camerad and record logs
managed_processes['camerad'].start()
time.sleep(3)
socks = {c: messaging.sub_sock(c, conflate=False, timeout=100) for c in CAMERAS}
- self.logs = defaultdict(list)
+ cls.logs = defaultdict(list)
start_time = time.monotonic()
while time.monotonic()- start_time < TEST_TIMESPAN:
for cam, s in socks.items():
- self.logs[cam] += messaging.drain_sock(s)
+ cls.logs[cam] += messaging.drain_sock(s)
time.sleep(0.2)
managed_processes['camerad'].stop()
- self.log_by_frame_id = defaultdict(list)
- self.sensor_type = None
- for cam, msgs in self.logs.items():
- if self.sensor_type is None:
- self.sensor_type = getattr(msgs[0], msgs[0].which()).sensor.raw
+ cls.log_by_frame_id = defaultdict(list)
+ cls.sensor_type = None
+ for cam, msgs in cls.logs.items():
+ if cls.sensor_type is None:
+ cls.sensor_type = getattr(msgs[0], msgs[0].which()).sensor.raw
expected_frames = SERVICE_LIST[cam].frequency * TEST_TIMESPAN
assert expected_frames*0.95 < len(msgs) < expected_frames*1.05, f"unexpected frame count {cam}: {expected_frames=}, got {len(msgs)}"
dts = np.abs(np.diff([getattr(m, m.which()).timestampSof/1e6 for m in msgs]) - 1000/SERVICE_LIST[cam].frequency)
- assert (dts < FRAME_DELTA_TOLERANCE[self.sensor_type]).all(), f"{cam} dts(ms) out of spec: max diff {dts.max()}, 99 percentile {np.percentile(dts, 99)}"
+ assert (dts < FRAME_DELTA_TOLERANCE[cls.sensor_type]).all(), f"{cam} dts(ms) out of spec: max diff {dts.max()}, 99 percentile {np.percentile(dts, 99)}"
for m in msgs:
- self.log_by_frame_id[getattr(m, m.which()).frameId].append(m)
+ cls.log_by_frame_id[getattr(m, m.which()).frameId].append(m)
# strip beginning and end
for _ in range(3):
- mn, mx = min(self.log_by_frame_id.keys()), max(self.log_by_frame_id.keys())
- del self.log_by_frame_id[mn]
- del self.log_by_frame_id[mx]
+ mn, mx = min(cls.log_by_frame_id.keys()), max(cls.log_by_frame_id.keys())
+ del cls.log_by_frame_id[mn]
+ del cls.log_by_frame_id[mx]
def test_frame_skips(self):
skips = {}
diff --git a/system/hardware/hardwared.py b/system/hardware/hardwared.py
index 9ebb9c9d54..da3172e8d1 100755
--- a/system/hardware/hardwared.py
+++ b/system/hardware/hardwared.py
@@ -148,8 +148,7 @@ def hw_state_thread(end_event, hw_queue):
except queue.Full:
pass
- # TODO: remove this once the config is in AGNOS
- if not modem_configured and len(HARDWARE.get_sim_info().get('sim_id', '')) > 0:
+ if not modem_configured and HARDWARE.get_modem_version() is not None:
cloudlog.warning("configuring modem")
HARDWARE.configure_modem()
modem_configured = True
@@ -316,7 +315,7 @@ def hardware_thread(end_event, hw_queue) -> None:
set_offroad_alert_if_changed("Offroad_TemperatureTooHigh", show_alert, extra_text=extra_text)
# TODO: this should move to TICI.initialize_hardware, but we currently can't import params there
- if TICI:
+ if TICI and HARDWARE.get_device_type() == "tici":
if not os.path.isfile("/persist/comma/living-in-the-moment"):
if not Path("/data/media").is_mount():
set_offroad_alert_if_changed("Offroad_StorageMissing", True)
diff --git a/system/hardware/tici/hardware.py b/system/hardware/tici/hardware.py
index 70049d1849..8110a0808e 100644
--- a/system/hardware/tici/hardware.py
+++ b/system/hardware/tici/hardware.py
@@ -467,8 +467,9 @@ class Tici(HardwareBase):
cmds = []
if manufacturer == 'Cavli Inc.':
cmds += [
- # use sim slot
- 'AT^SIMSWAP=1',
+ 'AT^SIMSWAP=1', # use SIM slot, instead of internal eSIM
+ 'AT$QCSIMSLEEP=0', # disable SIM sleep
+ 'AT$QCSIMCFG=SimPowerSave,0', # more sleep disable
# ethernet config
'AT$QCPCFG=usbNet,0',
diff --git a/system/hardware/tici/tests/test_power_draw.py b/system/hardware/tici/tests/test_power_draw.py
index 0ef34549b5..8598b2faa2 100644
--- a/system/hardware/tici/tests/test_power_draw.py
+++ b/system/hardware/tici/tests/test_power_draw.py
@@ -31,7 +31,7 @@ class Proc:
PROCS = [
- Proc(['camerad'], 2.1, msgs=['roadCameraState', 'wideRoadCameraState', 'driverCameraState']),
+ Proc(['camerad'], 1.75, msgs=['roadCameraState', 'wideRoadCameraState', 'driverCameraState']),
Proc(['modeld'], 1.12, atol=0.2, msgs=['modelV2']),
Proc(['dmonitoringmodeld'], 0.5, msgs=['driverStateV2']),
Proc(['encoderd'], 0.23, msgs=[]),
diff --git a/system/loggerd/tests/test_encoder.py b/system/loggerd/tests/test_encoder.py
index 75862a9d45..cf38c8bc31 100644
--- a/system/loggerd/tests/test_encoder.py
+++ b/system/loggerd/tests/test_encoder.py
@@ -61,7 +61,7 @@ class TestEncoder:
time.sleep(1.0)
managed_processes['camerad'].start()
- num_segments = int(os.getenv("SEGMENTS", random.randint(10, 15)))
+ num_segments = int(os.getenv("SEGMENTS", random.randint(2, 8)))
# wait for loggerd to make the dir for first segment
route_prefix_path = None
diff --git a/system/loggerd/tests/test_uploader.py b/system/loggerd/tests/test_uploader.py
index fa71600263..a05a28ee4e 100644
--- a/system/loggerd/tests/test_uploader.py
+++ b/system/loggerd/tests/test_uploader.py
@@ -73,7 +73,7 @@ class TestUploader(UploaderTestCase):
self.start_thread()
# allow enough time that files could upload twice if there is a bug in the logic
- time.sleep(5)
+ time.sleep(2)
self.join_thread()
exp_order = self.gen_order([self.seg_num], [])
@@ -91,7 +91,7 @@ class TestUploader(UploaderTestCase):
self.start_thread()
# allow enough time that files could upload twice if there is a bug in the logic
- time.sleep(5)
+ time.sleep(2)
self.join_thread()
exp_order = self.gen_order([self.seg_num], [])
@@ -110,7 +110,7 @@ class TestUploader(UploaderTestCase):
self.start_thread()
# allow enough time that files could upload twice if there is a bug in the logic
- time.sleep(5)
+ time.sleep(2)
self.join_thread()
exp_order = self.gen_order([self.seg_num], [])
@@ -137,7 +137,7 @@ class TestUploader(UploaderTestCase):
self.start_thread()
# allow enough time that files could upload twice if there is a bug in the logic
- time.sleep(5)
+ time.sleep(2)
self.join_thread()
assert len(log_handler.upload_ignored) == 0, "Some files were ignored"
@@ -155,7 +155,7 @@ class TestUploader(UploaderTestCase):
f_paths = self.gen_files(lock=True, boot=False)
# allow enough time that files should have been uploaded if they would be uploaded
- time.sleep(5)
+ time.sleep(2)
self.join_thread()
for f_path in f_paths:
@@ -168,7 +168,7 @@ class TestUploader(UploaderTestCase):
self.start_thread()
# allow enough time that files could upload twice if there is a bug in the logic
- time.sleep(5)
+ time.sleep(2)
self.join_thread()
assert len(log_handler.upload_order) == 0, "File uploaded again"
diff --git a/system/sentry.py b/system/sentry.py
index 6b0332169b..63bf789b6f 100644
--- a/system/sentry.py
+++ b/system/sentry.py
@@ -63,8 +63,6 @@ def init(project: SentryProject) -> bool:
max_value_length=8192,
environment=env)
- build_metadata = get_build_metadata()
-
sentry_sdk.set_user({"id": dongle_id})
sentry_sdk.set_tag("dirty", build_metadata.openpilot.is_dirty)
sentry_sdk.set_tag("origin", build_metadata.openpilot.git_origin)
diff --git a/system/tests/test_logmessaged.py b/system/tests/test_logmessaged.py
index 3baf5300c0..03c13437bc 100644
--- a/system/tests/test_logmessaged.py
+++ b/system/tests/test_logmessaged.py
@@ -35,7 +35,7 @@ class TestLogmessaged:
msgs = [f"abc {i}" for i in range(10)]
for m in msgs:
cloudlog.error(m)
- time.sleep(3)
+ time.sleep(1)
m = messaging.drain_sock(self.sock)
assert len(m) == len(msgs)
assert len(self._get_log_files()) >= 1
@@ -45,7 +45,7 @@ class TestLogmessaged:
msg = "a"*3*1024*1024
for _ in range(n):
cloudlog.info(msg)
- time.sleep(3)
+ time.sleep(1)
msgs = messaging.drain_sock(self.sock)
assert len(msgs) == 0
diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc
index ee8f42ea72..189f8ac18f 100644
--- a/tools/cabana/mainwin.cc
+++ b/tools/cabana/mainwin.cc
@@ -51,7 +51,7 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW
emit static_main_win->updateProgressBar(cur, total, success);
});
qInstallMessageHandler([](QtMsgType type, const QMessageLogContext &context, const QString &msg) {
- if (type == QtDebugMsg) std::cout << msg.toStdString() << std::endl;
+ if (type == QtDebugMsg) return;
emit static_main_win->showMessage(msg, 2000);
});
installMessageHandler([](ReplyMsgType type, const std::string msg) { qInfo() << msg.c_str(); });
diff --git a/tools/cabana/videowidget.cc b/tools/cabana/videowidget.cc
index 7f9e3d6960..3dddc0fb64 100644
--- a/tools/cabana/videowidget.cc
+++ b/tools/cabana/videowidget.cc
@@ -1,14 +1,12 @@
#include "tools/cabana/videowidget.h"
#include
-#include
#include
#include
#include
#include
#include
-#include
#include
#include
#include
@@ -27,9 +25,7 @@ static const QColor timeline_colors[] = {
static Replay *getReplay() {
auto stream = qobject_cast(can);
- if (!stream) return nullptr;
-
- return stream->getReplay();
+ return stream ? stream->getReplay() : nullptr;
}
VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) {
@@ -144,13 +140,9 @@ QWidget *VideoWidget::createCameraWidget() {
camera_tab->setAutoHide(true);
camera_tab->setExpanding(false);
- QStackedLayout *stacked = new QStackedLayout();
- stacked->setStackingMode(QStackedLayout::StackAll);
- stacked->addWidget(cam_widget = new StreamCameraView("camerad", VISION_STREAM_ROAD));
+ l->addWidget(cam_widget = new StreamCameraView("camerad", VISION_STREAM_ROAD));
cam_widget->setMinimumHeight(MIN_VIDEO_HEIGHT);
cam_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding);
- stacked->addWidget(alert_label = new InfoLabel(this));
- l->addLayout(stacked);
l->addWidget(slider = new Slider(w));
slider->setSingleStep(0);
@@ -165,16 +157,13 @@ QWidget *VideoWidget::createCameraWidget() {
QObject::connect(camera_tab, &QTabBar::currentChanged, [this](int index) {
if (index != -1) cam_widget->setStreamType((VisionStreamType)camera_tab->tabData(index).toInt());
});
- QObject::connect(static_cast(can), &ReplayStream::qLogLoaded, slider, &Slider::parseQLog, Qt::QueuedConnection);
+ QObject::connect(static_cast(can), &ReplayStream::qLogLoaded, cam_widget, &StreamCameraView::parseQLog, Qt::QueuedConnection);
+ slider->installEventFilter(cam_widget);
return w;
}
void VideoWidget::vipcAvailableStreamsUpdated(std::set streams) {
- static const QString stream_names[] = {
- [VISION_STREAM_ROAD] = "Road camera",
- [VISION_STREAM_WIDE_ROAD] = "Wide road camera",
- [VISION_STREAM_DRIVER] = "Driver camera"};
-
+ static const QString stream_names[] = {"Road camera", "Driver camera", "Wide road camera"};
for (int i = 0; i < streams.size(); ++i) {
if (camera_tab->count() <= i) {
camera_tab->addTab(QString());
@@ -189,16 +178,9 @@ void VideoWidget::vipcAvailableStreamsUpdated(std::set streams
}
void VideoWidget::loopPlaybackClicked() {
- auto replay = getReplay();
- if (!replay) return;
-
- if (replay->hasFlag(REPLAY_FLAG_NO_LOOP)) {
- replay->removeFlag(REPLAY_FLAG_NO_LOOP);
- loop_btn->setIcon("repeat");
- } else {
- replay->addFlag(REPLAY_FLAG_NO_LOOP);
- loop_btn->setIcon("repeat-1");
- }
+ bool is_looping = getReplay()->loop();
+ getReplay()->setLoop(!is_looping);
+ loop_btn->setIcon(!is_looping ? "repeat" : "repeat-1");
}
void VideoWidget::timeRangeChanged() {
@@ -223,7 +205,9 @@ void VideoWidget::updateState() {
if (!slider->isSliderDown()) {
slider->setCurrentSecond(can->currentSec());
}
- alert_label->showAlert(slider->alertInfo(can->currentSec()));
+ if (camera_tab->count() == 0) { // No streams available
+ cam_widget->update(); // Manually refresh to show alert events
+ }
time_btn->setText(QString("%1 / %2").arg(formatTime(can->currentSec(), true),
formatTime(slider->maximum() / slider->factor)));
} else {
@@ -239,41 +223,9 @@ void VideoWidget::updatePlayBtnState() {
// Slider
Slider::Slider(QWidget *parent) : QSlider(Qt::Horizontal, parent) {
- thumbnail_label = new InfoLabel(parent);
setMouseTracking(true);
}
-std::optional Slider::alertInfo(double seconds) {
- return getReplay()->findAlertAtTime(seconds);
-}
-
-QPixmap Slider::thumbnail(double seconds) {
- auto it = thumbnails.lowerBound(can->toMonoTime(seconds));
- return it != thumbnails.end() ? it.value() : QPixmap();
-}
-
-void Slider::setTimeRange(double min, double max) {
- assert(min < max);
- setRange(min * factor, max * factor);
-}
-
-void Slider::parseQLog(std::shared_ptr qlog) {
- std::mutex mutex;
- QtConcurrent::blockingMap(qlog->events.cbegin(), qlog->events.cend(), [&mutex, this](const Event &e) {
- if (e.which == cereal::Event::Which::THUMBNAIL) {
- capnp::FlatArrayMessageReader reader(e.data);
- auto thumb = reader.getRoot().getThumbnail();
- auto data = thumb.getThumbnail();
- if (QPixmap pm; pm.loadFromData(data.begin(), data.size(), "jpeg")) {
- QPixmap scaled = pm.scaledToHeight(MIN_VIDEO_HEIGHT - THUMBNAIL_MARGIN * 2, Qt::SmoothTransformation);
- std::lock_guard lk(mutex);
- thumbnails[thumb.getTimestampEof()] = scaled;
- }
- }
- });
- update();
-}
-
void Slider::paintEvent(QPaintEvent *ev) {
QPainter p(this);
QRect r = rect().adjusted(0, 4, 0, -4);
@@ -288,9 +240,8 @@ void Slider::paintEvent(QPaintEvent *ev) {
p.fillRect(r, color);
};
- auto replay = getReplay();
- if (replay) {
- for (const auto &entry: *replay->getTimeline()) {
+ if (auto replay = getReplay()) {
+ for (const auto &entry : *replay->getTimeline()) {
fillRange(entry.start_time, entry.end_time, timeline_colors[(int)entry.type]);
}
@@ -319,84 +270,7 @@ void Slider::mousePressEvent(QMouseEvent *e) {
}
}
-void Slider::mouseMoveEvent(QMouseEvent *e) {
- int pos = std::clamp(e->pos().x(), 0, width());
- double seconds = (minimum() + pos * ((maximum() - minimum()) / (double)width())) / factor;
- QPixmap thumb = thumbnail(seconds);
- if (!thumb.isNull()) {
- int x = std::clamp(pos - thumb.width() / 2, THUMBNAIL_MARGIN, width() - thumb.width() - THUMBNAIL_MARGIN + 1);
- int y = -thumb.height() - THUMBNAIL_MARGIN;
- thumbnail_label->showPixmap(mapToParent(QPoint(x, y)), utils::formatSeconds(seconds), thumb, alertInfo(seconds));
- } else {
- thumbnail_label->hide();
- }
- QSlider::mouseMoveEvent(e);
-}
-
-bool Slider::event(QEvent *event) {
- switch (event->type()) {
- case QEvent::WindowActivate:
- case QEvent::WindowDeactivate:
- case QEvent::FocusIn:
- case QEvent::FocusOut:
- case QEvent::Leave:
- thumbnail_label->hide();
- break;
- default:
- break;
- }
- return QSlider::event(event);
-}
-
-// InfoLabel
-
-InfoLabel::InfoLabel(QWidget *parent) : QWidget(parent, Qt::WindowStaysOnTopHint) {
- setAttribute(Qt::WA_ShowWithoutActivating);
- setAttribute(Qt::WA_TransparentForMouseEvents);
- setVisible(false);
-}
-
-void InfoLabel::showPixmap(const QPoint &pt, const QString &sec, const QPixmap &pm, const std::optional &alert) {
- second = sec;
- pixmap = pm;
- alert_info = alert;
- setGeometry(QRect(pt, pm.size()));
- setVisible(true);
- update();
-}
-
-void InfoLabel::showAlert(const std::optional &alert) {
- alert_info = alert;
- pixmap = {};
- setVisible(alert_info.has_value());
- update();
-}
-
-void InfoLabel::paintEvent(QPaintEvent *event) {
- QPainter p(this);
- p.setPen(QPen(palette().color(QPalette::BrightText), 2));
- if (!pixmap.isNull()) {
- p.drawPixmap(0, 0, pixmap);
- p.drawRect(rect());
- p.drawText(rect().adjusted(0, 0, 0, -THUMBNAIL_MARGIN), second, Qt::AlignHCenter | Qt::AlignBottom);
- }
- if (alert_info) {
- QColor color = timeline_colors[int(alert_info->type)];
- color.setAlphaF(0.5);
- QString text = QString::fromStdString(alert_info->text1);
- if (!alert_info->text2.empty()) text += "\n" + QString::fromStdString(alert_info->text2);
-
- if (!pixmap.isNull()) {
- QFont font;
- font.setPixelSize(11);
- p.setFont(font);
- }
- QRect text_rect = rect().adjusted(1, 1, -1, -1);
- QRect r = p.fontMetrics().boundingRect(text_rect, Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap, text);
- p.fillRect(text_rect.left(), r.top(), text_rect.width(), r.height(), color);
- p.drawText(text_rect, Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap, text);
- }
-}
+// StreamCameraView
StreamCameraView::StreamCameraView(std::string stream_name, VisionStreamType stream_type, QWidget *parent)
: CameraWidget(stream_name, stream_type, parent) {
@@ -404,15 +278,93 @@ StreamCameraView::StreamCameraView(std::string stream_name, VisionStreamType str
fade_animation->setDuration(500);
fade_animation->setStartValue(0.2f);
fade_animation->setEndValue(0.7f);
+ fade_animation->setEasingCurve(QEasingCurve::InOutQuad);
+ connect(fade_animation, &QPropertyAnimation::valueChanged, this, QOverload<>::of(&StreamCameraView::update));
+}
+
+void StreamCameraView::parseQLog(std::shared_ptr qlog) {
+ std::mutex mutex;
+ QtConcurrent::blockingMap(qlog->events.cbegin(), qlog->events.cend(), [this, &mutex](const Event &e) {
+ if (e.which == cereal::Event::Which::THUMBNAIL) {
+ capnp::FlatArrayMessageReader reader(e.data);
+ auto thumb_data = reader.getRoot().getThumbnail();
+ auto image_data = thumb_data.getThumbnail();
+ if (QPixmap thumb; thumb.loadFromData(image_data.begin(), image_data.size(), "jpeg")) {
+ QPixmap generated_thumb = generateThumbnail(thumb, can->toSeconds(thumb_data.getTimestampEof()));
+ std::lock_guard lock(mutex);
+ thumbnails[thumb_data.getTimestampEof()] = generated_thumb;
+ }
+ }
+ });
+ update();
}
void StreamCameraView::paintGL() {
CameraWidget::paintGL();
+ QPainter p(this);
+ if (auto alert = getReplay()->findAlertAtTime(can->currentSec())) {
+ drawAlert(p, rect(), *alert);
+ }
+ if (thumbnail_pt_) {
+ drawThumbnail(p);
+ }
if (can->isPaused()) {
- QPainter p(this);
- p.setPen(QColor(200, 200, 200, static_cast(255 * overlay_opacity)));
+ p.setPen(QColor(200, 200, 200, static_cast(255 * fade_animation->currentValue().toFloat())));
p.setFont(QFont(font().family(), 16, QFont::Bold));
p.drawText(rect(), Qt::AlignCenter, tr("PAUSED"));
}
}
+
+QPixmap StreamCameraView::generateThumbnail(QPixmap thumb, double seconds) {
+ QPixmap scaled = thumb.scaledToHeight(MIN_VIDEO_HEIGHT - THUMBNAIL_MARGIN * 2, Qt::SmoothTransformation);
+ QPainter p(&scaled);
+ p.setPen(QPen(palette().color(QPalette::BrightText), 2));
+ p.drawRect(scaled.rect());
+ if (auto alert = getReplay()->findAlertAtTime(seconds)) {
+ p.setFont(QFont(font().family(), 10));
+ drawAlert(p, scaled.rect(), *alert);
+ }
+ return scaled;
+}
+
+void StreamCameraView::drawThumbnail(QPainter &p) {
+ int pos = std::clamp(thumbnail_pt_->x(), 0, width());
+ auto [min_sec, max_sec] = can->timeRange().value_or(std::make_pair(can->minSeconds(), can->maxSeconds()));
+ double seconds = min_sec + pos * (max_sec - min_sec) / width();
+
+ auto it = thumbnails.lowerBound(can->toMonoTime(seconds));
+ if (it != thumbnails.end()) {
+ const QPixmap &thumb = it.value();
+ int x = std::clamp(pos - thumb.width() / 2, THUMBNAIL_MARGIN, width() - thumb.width() - THUMBNAIL_MARGIN + 1);
+ int y = height() - thumb.height() - THUMBNAIL_MARGIN;
+
+ p.drawPixmap(x, y, thumb);
+ p.setPen(QPen(palette().color(QPalette::BrightText), 2));
+ p.drawText(x, y, thumb.width(), thumb.height() - THUMBNAIL_MARGIN, Qt::AlignHCenter | Qt::AlignBottom, QString::number(seconds));
+ }
+}
+
+void StreamCameraView::drawAlert(QPainter &p, const QRect &rect, const Timeline::Entry &alert) {
+ p.setPen(QPen(palette().color(QPalette::BrightText), 2));
+ QColor color = timeline_colors[int(alert.type)];
+ color.setAlphaF(0.5);
+ QString text = QString::fromStdString(alert.text1);
+ if (!alert.text2.empty()) text += "\n" + QString::fromStdString(alert.text2);
+
+ QRect text_rect = rect.adjusted(1, 1, -1, -1);
+ QRect r = p.fontMetrics().boundingRect(text_rect, Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap, text);
+ p.fillRect(text_rect.left(), r.top(), text_rect.width(), r.height(), color);
+ p.drawText(text_rect, Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap, text);
+}
+
+bool StreamCameraView::eventFilter(QObject *, QEvent *event) {
+ if (event->type() == QEvent::MouseMove) {
+ thumbnail_pt_ = static_cast(event)->pos();
+ update();
+ } else if (event->type() == QEvent::Leave) {
+ thumbnail_pt_ = std::nullopt;
+ update();
+ }
+ return false;
+}
diff --git a/tools/cabana/videowidget.h b/tools/cabana/videowidget.h
index d3342c34d7..78503365e5 100644
--- a/tools/cabana/videowidget.h
+++ b/tools/cabana/videowidget.h
@@ -1,6 +1,5 @@
#pragma once
-#include