Merge branch 'master' of ssh://github.com/commaai/openpilot into qcom-replay

sync
Adeeb Shihadeh 1 month ago
commit 7455dde790
  1. 2
      .github/workflows/release.yaml
  2. 8
      .github/workflows/selfdrive_tests.yaml
  3. 2
      .gitignore
  4. 2
      Jenkinsfile
  5. 1
      README.md
  6. 19
      RELEASES.md
  7. 16
      cereal/log.capnp
  8. 2
      cereal/messaging/tests/test_pub_sub_master.py
  9. 4
      cereal/services.py
  10. 5
      common/params_keys.h
  11. 15
      common/params_pyx.pyx
  12. 30
      common/pid.py
  13. 15
      common/run.py
  14. 2
      common/version.h
  15. 12
      docs/CARS.md
  16. 2
      docs/CONTRIBUTING.md
  17. 2
      launch_env.sh
  18. 2
      opendbc_repo
  19. 2
      panda
  20. 1
      pyproject.toml
  21. 25
      release/README.md
  22. 22
      release/build_stripped.sh
  23. 3
      selfdrive/assets/icons/link.png
  24. 8
      selfdrive/controls/controlsd.py
  25. 6
      selfdrive/controls/lib/latcontrol.py
  26. 9
      selfdrive/controls/lib/latcontrol_angle.py
  27. 26
      selfdrive/controls/lib/latcontrol_pid.py
  28. 34
      selfdrive/controls/lib/latcontrol_torque.py
  29. 8
      selfdrive/controls/lib/longitudinal_planner.py
  30. 0
      selfdrive/controls/lib/tests/__init__.py
  31. 4
      selfdrive/controls/tests/test_latcontrol.py
  32. 2
      selfdrive/locationd/torqued.py
  33. 11
      selfdrive/modeld/modeld.py
  34. 4
      selfdrive/modeld/models/driving_policy.onnx
  35. 4
      selfdrive/modeld/models/driving_vision.onnx
  36. 28
      selfdrive/modeld/parse_model_outputs.py
  37. 2
      selfdrive/pandad/pandad.py
  38. 2
      selfdrive/selfdrived/alerts_offroad.json
  39. 18
      selfdrive/selfdrived/events.py
  40. 54
      selfdrive/selfdrived/helpers.py
  41. 43
      selfdrive/selfdrived/selfdrived.py
  42. 8
      selfdrive/test/process_replay/process_replay.py
  43. 2
      selfdrive/test/process_replay/ref_commit
  44. 2
      selfdrive/test/process_replay/test_processes.py
  45. 1
      selfdrive/test/setup_device_ci.sh
  46. 15
      selfdrive/test/test_onroad.py
  47. 12
      selfdrive/ui/SConscript
  48. 71
      selfdrive/ui/feedback/feedbackd.py
  49. 40
      selfdrive/ui/installer/installer.cc
  50. 12
      selfdrive/ui/layouts/main.py
  51. 2
      selfdrive/ui/layouts/settings/settings.py
  52. 8
      selfdrive/ui/layouts/sidebar.py
  53. 13
      selfdrive/ui/lib/prime_state.py
  54. 18
      selfdrive/ui/onroad/cameraview.py
  55. 2
      selfdrive/ui/onroad/exp_button.py
  56. 4
      selfdrive/ui/onroad/hud_renderer.py
  57. 30
      selfdrive/ui/onroad/model_renderer.py
  58. 23
      selfdrive/ui/qt/python_helpers.py
  59. 541
      selfdrive/ui/qt/setup/setup.cc
  60. 38
      selfdrive/ui/qt/setup/setup.h
  61. 12
      selfdrive/ui/qt/sidebar.cc
  62. 2
      selfdrive/ui/qt/sidebar.h
  63. 53
      selfdrive/ui/tests/test_feedbackd.py
  64. 8
      selfdrive/ui/tests/test_raylib_ui.py
  65. 117
      selfdrive/ui/translations/main_ar.ts
  66. 117
      selfdrive/ui/translations/main_de.ts
  67. 119
      selfdrive/ui/translations/main_es.ts
  68. 117
      selfdrive/ui/translations/main_fr.ts
  69. 121
      selfdrive/ui/translations/main_ja.ts
  70. 121
      selfdrive/ui/translations/main_ko.ts
  71. 123
      selfdrive/ui/translations/main_pt-BR.ts
  72. 117
      selfdrive/ui/translations/main_th.ts
  73. 117
      selfdrive/ui/translations/main_tr.ts
  74. 127
      selfdrive/ui/translations/main_zh-CHS.ts
  75. 127
      selfdrive/ui/translations/main_zh-CHT.ts
  76. 33
      selfdrive/ui/watch3.cc
  77. 17
      selfdrive/ui/watch3.py
  78. 18
      selfdrive/ui/widgets/exp_mode_button.py
  79. 2
      selfdrive/ui/widgets/prime.py
  80. 34
      system/camerad/sensors/ar0231_cl.h
  81. 58
      system/camerad/sensors/os04c10_cl.h
  82. 47
      system/camerad/sensors/ox03c10_cl.h
  83. 10
      system/hardware/fan_controller.py
  84. 38
      system/hardware/tici/agnos.json
  85. 72
      system/hardware/tici/all-partitions.json
  86. 2
      system/loggerd/encoder/encoder.cc
  87. 2
      system/loggerd/encoder/ffmpeg_encoder.cc
  88. 15
      system/loggerd/encoder/v4l_encoder.cc
  89. 10
      system/loggerd/loggerd.cc
  90. 49
      system/loggerd/loggerd.h
  91. 24
      system/loggerd/tests/test_encoder.py
  92. 8
      system/loggerd/tests/test_loggerd.py
  93. 2
      system/manager/manager.py
  94. 2
      system/manager/process_config.py
  95. 3
      system/manager/test/test_manager.py
  96. 15
      system/ui/lib/application.py
  97. 47
      system/ui/lib/emoji.py
  98. 4
      system/ui/lib/shader_polygon.py
  99. 17
      system/ui/lib/wifi_manager.py
  100. 2
      system/ui/reset.py
  101. Some files were not shown because too many files have changed in this diff Show More

@ -39,4 +39,4 @@ jobs:
git config --global --add safe.directory '*'
git lfs pull
- name: Push master-ci
run: BRANCH=__nightly release/build_devel.sh
run: BRANCH=__nightly release/build_stripped.sh

@ -27,7 +27,7 @@ env:
RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c
PYTEST: pytest --continue-on-collection-errors --durations=0 --durations-min=5 -n logical
PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical
jobs:
build_release:
@ -52,7 +52,7 @@ jobs:
command: git lfs pull
- name: Build devel
timeout-minutes: 1
run: TARGET_DIR=$STRIPPED_DIR release/build_devel.sh
run: TARGET_DIR=$STRIPPED_DIR release/build_stripped.sh
- uses: ./.github/workflows/setup-with-retry
- name: Build openpilot and run checks
timeout-minutes: ${{ ((steps.restore-scons-cache.outputs.cache-hit == 'true') && 10 || 30) }} # allow more time when we missed the scons cache
@ -161,7 +161,9 @@ jobs:
- name: Run unit tests
timeout-minutes: ${{ contains(runner.name, 'nsc') && ((steps.setup-step.outputs.duration < 18) && 1 || 2) || 20 }}
run: |
${{ env.RUN }} "$PYTEST --collect-only -m 'not slow' &> /dev/null && \
${{ env.RUN }} "source selfdrive/test/setup_xvfb.sh && \
# Pre-compile Python bytecode so each pytest worker doesn't need to
$PYTEST --collect-only -m 'not slow' -qq && \
MAX_EXAMPLES=1 $PYTEST -m 'not slow' && \
./selfdrive/ui/tests/create_test_translations.sh && \
QT_QPA_PLATFORM=offscreen ./selfdrive/ui/tests/test_translations && \

2
.gitignore vendored

@ -13,9 +13,11 @@ venv/
model2.png
a.out
.hypothesis
.cache/
/docs_site/
*.mp4
*.dylib
*.DSYM
*.d

2
Jenkinsfile vendored

@ -167,7 +167,7 @@ node {
env.GIT_COMMIT = checkout(scm).GIT_COMMIT
def excludeBranches = ['__nightly', 'devel', 'devel-staging', 'release3', 'release3-staging',
'testing-closet*', 'hotfix-*']
'release-tici', 'testing-closet*', 'hotfix-*']
def excludeRegex = excludeBranches.join('|').replaceAll('\\*', '.*')
if (env.BRANCH_NAME != 'master' && !env.BRANCH_NAME.contains('__jenkins_loop_')) {

@ -56,7 +56,6 @@ We have detailed instructions for [how to install the harness and device in a ca
| `release3-staging` | openpilot-test.comma.ai | This is the staging branch for releases. Use it to get new releases slightly early. |
| `nightly` | openpilot-nightly.comma.ai | This is the bleeding edge development branch. Do not expect this to be stable. |
| `nightly-dev` | installer.comma.ai/commaai/nightly-dev | Same as nightly, but includes experimental development features for some cars. |
| `secretgoodopenpilot` | installer.comma.ai/commaai/secretgoodopenpilot | This is a preview branch from the autonomy team where new driving models get merged earlier than master. |
To start developing openpilot
------

@ -1,11 +1,22 @@
Version 0.10.0 (2025-07-07)
Version 0.10.1 (2025-09-08)
========================
* Record driving feedback using LKAS button
* Honda City 2023 support thanks to drFritz!
Version 0.10.0 (2025-08-05)
========================
* New driving model
* Lead car ground-truth fixes
* Ported over VAE from the MLSIM stack
* New training architecture described in CVPR paper
* New training architecture
* Described in our CVPR paper: "Learning to Drive from a World Model"
* Longitudinal MPC replaced by E2E planning from World Model in Experimental Mode
* Action from lateral MPC as training objective replaced by E2E planning from World Model
* Low-speed lead car ground-truth fixes
* Enable live-learned steering actuation delay
* Opt-in audio recording for dashcam video
* Acura MDX 2025 support thanks to vanillagorillaa and MVL!
* Honda Accord 2023-25 support thanks to vanillagorillaa and MVL!
* Honda CR-V 2023-25 support thanks to vanillagorillaa and MVL!
* Honda Pilot 2023-25 support thanks to vanillagorillaa and MVL!
Version 0.9.9 (2025-05-23)
========================

@ -127,8 +127,9 @@ struct OnroadEvent @0xc4fa6047f024e718 {
espActive @90;
personalityChanged @91;
aeb @92;
userFlag @95;
userBookmark @95;
excessiveActuation @96;
audioFeedback @97;
soundsUnavailableDEPRECATED @47;
}
@ -2468,7 +2469,7 @@ struct DebugAlert {
alertText2 @1 :Text;
}
struct UserFlag {
struct UserBookmark @0xfe346a9de48d9b50 {
}
struct SoundPressure @0xdc24138990726023 {
@ -2486,6 +2487,11 @@ struct AudioData {
sampleRate @1 :UInt32;
}
struct AudioFeedback {
audio @0 :AudioData;
blockNum @1 :UInt16;
}
struct Touch {
sec @0 :Int64;
usec @1 :Int64;
@ -2586,9 +2592,13 @@ struct Event {
mapRenderState @105: MapRenderState;
# UI services
userFlag @93 :UserFlag;
uiDebug @102 :UIDebug;
# driving feedback
userBookmark @93 :UserBookmark;
bookmarkButton @148 :UserBookmark;
audioFeedback @149 :AudioFeedback;
# *********** debug ***********
testJoystick @52 :Joystick;
roadEncodeData @86 :EncodeData;

@ -86,7 +86,7 @@ class TestSubMaster:
"cameraOdometry": (20, 10),
"liveCalibration": (4, 4),
"carParams": (None, None),
"userFlag": (None, None),
"userBookmark": (None, None),
}
for service, (max_freq, min_freq) in checks.items():

@ -72,9 +72,11 @@ _services: dict[str, tuple] = {
"navRoute": (True, 0.),
"navThumbnail": (True, 0.),
"qRoadEncodeIdx": (False, 20.),
"userFlag": (True, 0., 1),
"userBookmark": (True, 0., 1),
"soundPressure": (True, 10., 10),
"rawAudioData": (False, 20.),
"bookmarkButton": (True, 0., 1),
"audioFeedback": (True, 0., 1),
# debug
"uiDebug": (True, 0., 1),

@ -73,9 +73,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"LastOffroadStatusPacket", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, JSON}},
{"LastPowerDropDetected", {CLEAR_ON_MANAGER_START, STRING}},
{"LastUpdateException", {CLEAR_ON_MANAGER_START, STRING}},
{"LastUpdateRouteCount", {PERSISTENT, INT}},
{"LastUpdateRouteCount", {PERSISTENT, INT, "0"}},
{"LastUpdateTime", {PERSISTENT, TIME}},
{"LastUpdateUptimeOnroad", {PERSISTENT, FLOAT}},
{"LastUpdateUptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
{"LiveDelay", {PERSISTENT, BYTES}},
{"LiveParameters", {PERSISTENT, JSON}},
{"LiveParametersV2", {PERSISTENT, BYTES}},
@ -105,6 +105,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"PandaSignatures", {CLEAR_ON_MANAGER_START, BYTES}},
{"PrimeType", {PERSISTENT, INT}},
{"RecordAudio", {PERSISTENT, BOOL}},
{"RecordAudioFeedback", {PERSISTENT, BOOL, "0"}},
{"RecordFront", {PERSISTENT, BOOL}},
{"RecordFrontLock", {PERSISTENT, BOOL}}, // for the internal fleet
{"SecOCKey", {PERSISTENT | DONT_LOG, STRING}},

@ -102,14 +102,14 @@ cdef class Params:
return cast(value)
raise TypeError(f"Type mismatch while writing param {key}: {proposed_type=} {expected_type=} {value=}")
def cpp2python(self, t, value, default, key):
def _cpp2python(self, t, value, default, key):
if value is None:
return None
try:
return CPP_2_PYTHON[t](value)
except (KeyError, TypeError, ValueError):
cloudlog.warning(f"Failed to cast param {key} with {value=} from type {t=}")
return self.cpp2python(t, default, None, key)
return self._cpp2python(t, default, None, key)
def get(self, key, bool block=False, bool return_default=False):
cdef string k = self.check_key(key)
@ -126,8 +126,8 @@ cdef class Params:
# it means we got an interrupt while waiting
raise KeyboardInterrupt
else:
return self.cpp2python(t, default_val, None, key)
return self.cpp2python(t, val, default_val, key)
return self._cpp2python(t, default_val, None, key)
return self._cpp2python(t, val, default_val, key)
def get_bool(self, key, bool block=False):
cdef string k = self.check_key(key)
@ -188,4 +188,9 @@ cdef class Params:
cdef string k = self.check_key(key)
cdef ParamKeyType t = self.p.getKeyType(k)
cdef optional[string] default = self.p.getKeyDefaultValue(k)
return self.cpp2python(t, default.value(), None, key) if default.has_value() else None
return self._cpp2python(t, default.value(), None, key) if default.has_value() else None
def cpp2python(self, key, value):
cdef string k = self.check_key(key)
cdef ParamKeyType t = self.p.getKeyType(k)
return self._cpp2python(t, value, None, key)

@ -14,10 +14,8 @@ class PIDController:
if isinstance(self._k_d, Number):
self._k_d = [[0], [self._k_d]]
self.pos_limit = pos_limit
self.neg_limit = neg_limit
self.set_limits(pos_limit, neg_limit)
self.i_unwind_rate = 0.3 / rate
self.i_rate = 1.0 / rate
self.speed = 0.0
@ -35,10 +33,6 @@ class PIDController:
def k_d(self):
return np.interp(self.speed, self._k_d[0], self._k_d[1])
@property
def error_integral(self):
return self.i/self.k_i
def reset(self):
self.p = 0.0
self.i = 0.0
@ -46,25 +40,25 @@ class PIDController:
self.f = 0.0
self.control = 0
def update(self, error, error_rate=0.0, speed=0.0, override=False, feedforward=0., freeze_integrator=False):
self.speed = speed
def set_limits(self, pos_limit, neg_limit):
self.pos_limit = pos_limit
self.neg_limit = neg_limit
def update(self, error, error_rate=0.0, speed=0.0, feedforward=0., freeze_integrator=False):
self.speed = speed
self.p = float(error) * self.k_p
self.f = feedforward * self.k_f
self.d = error_rate * self.k_d
if override:
self.i -= self.i_unwind_rate * float(np.sign(self.i))
else:
if not freeze_integrator:
self.i = self.i + error * self.k_i * self.i_rate
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 = np.clip(control_no_i, self.neg_limit, self.pos_limit)
self.i = np.clip(self.i, self.neg_limit - control_no_i, self.pos_limit - control_no_i)
# Don't allow windup if already clipping
test_control = self.p + i + self.d + self.f
i_upperbound = self.i if test_control > self.pos_limit else self.pos_limit
i_lowerbound = self.i if test_control < self.neg_limit else self.neg_limit
self.i = np.clip(i, i_lowerbound, i_upperbound)
control = self.p + self.i + self.d + self.f
self.control = np.clip(control, self.neg_limit, self.pos_limit)
return self.control

@ -1,4 +1,6 @@
import subprocess
from contextlib import contextmanager
from subprocess import Popen, PIPE, TimeoutExpired
def run_cmd(cmd: list[str], cwd=None, env=None) -> str:
@ -11,3 +13,16 @@ def run_cmd_default(cmd: list[str], default: str = "", cwd=None, env=None) -> st
except subprocess.CalledProcessError:
return default
@contextmanager
def managed_proc(cmd: list[str], env: dict[str, str]):
proc = Popen(cmd, env=env, stdout=PIPE, stderr=PIPE)
try:
yield proc
finally:
if proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=5)
except TimeoutExpired:
proc.kill()

@ -1 +1 @@
#define COMMA_VERSION "0.10.0"
#define COMMA_VERSION "0.10.1"

@ -4,12 +4,13 @@
A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified.
# 313 Supported Cars
# 321 Supported Cars
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br>&nbsp;|Video|Setup Video|
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|Acura|ILX 2016-18|Technology Plus Package or AcuraWatch Plus|openpilot|26 mph|25 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Acura ILX 2016-18">Buy Here</a></sub></details>|||
|Acura|ILX 2019|All|openpilot|26 mph|25 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Acura ILX 2019">Buy Here</a></sub></details>|||
|Acura|MDX 2025|All except Type S|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Acura MDX 2025">Buy Here</a></sub></details>|||
|Acura|RDX 2016-18|AcuraWatch Plus or Advance Package|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Acura RDX 2016-18">Buy Here</a></sub></details>|||
|Acura|RDX 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Acura RDX 2019-21">Buy Here</a></sub></details>|||
|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Audi A3 2014-19">Buy Here</a></sub></details>|||
@ -72,18 +73,24 @@ A supported vehicle is one that just works when you install a comma device. All
|Genesis|GV80 2023[<sup>6</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai M connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Genesis GV80 2023">Buy Here</a></sub></details>|||
|GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=GMC Sierra 1500 2020-21">Buy Here</a></sub></details>|<a href="https://youtu.be/5HbNoBLzRwE" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Honda|Accord 2018-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord 2018-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=mrUwlj3Mi58" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Honda|Accord 2023-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord 2023-25">Buy Here</a></sub></details>|||
|Honda|Accord Hybrid 2018-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord Hybrid 2018-22">Buy Here</a></sub></details>|||
|Honda|Accord Hybrid 2023-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord Hybrid 2023-25">Buy Here</a></sub></details>|||
|Honda|City (Brazil only) 2023|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|14 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda City (Brazil only) 2023">Buy Here</a></sub></details>|||
|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2016-18">Buy Here</a></sub></details>|<a href="https://youtu.be/-IkImTe1NYE" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Honda|Civic 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|2 mph[<sup>5</sup>](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=4Iz1Mz5LGF8" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Honda|Civic 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Honda|Civic Hatchback 2017-21|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2017-21">Buy Here</a></sub></details>|||
|Honda|Civic Hatchback 2017-18|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2017-18">Buy Here</a></sub></details>|||
|Honda|Civic Hatchback 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2019-21">Buy Here</a></sub></details>|||
|Honda|Civic Hatchback 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Honda|Civic Hatchback Hybrid 2025|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback Hybrid 2025">Buy Here</a></sub></details>|||
|Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback Hybrid (Europe only) 2023">Buy Here</a></sub></details>|||
|Honda|Civic Hybrid 2025|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hybrid 2025">Buy Here</a></sub></details>|||
|Honda|CR-V 2015-16|Touring Trim|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda CR-V 2015-16">Buy Here</a></sub></details>|||
|Honda|CR-V 2017-22|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|15 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda CR-V 2017-22">Buy Here</a></sub></details>|||
|Honda|CR-V 2023-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda CR-V 2023-25">Buy Here</a></sub></details>|||
|Honda|CR-V Hybrid 2017-22|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda CR-V Hybrid 2017-22">Buy Here</a></sub></details>|||
|Honda|CR-V Hybrid 2023-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda CR-V Hybrid 2023-25">Buy Here</a></sub></details>|||
|Honda|e 2020|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda e 2020">Buy Here</a></sub></details>|||
|Honda|Fit 2018-20|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Fit 2018-20">Buy Here</a></sub></details>|||
|Honda|Freed 2020|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Freed 2020">Buy Here</a></sub></details>|||
@ -94,6 +101,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Honda|Odyssey 2018-20|Honda Sensing|openpilot|26 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Odyssey 2018-20">Buy Here</a></sub></details>|||
|Honda|Passport 2019-25|All|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Passport 2019-25">Buy Here</a></sub></details>|||
|Honda|Pilot 2016-22|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Pilot 2016-22">Buy Here</a></sub></details>|||
|Honda|Pilot 2023-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Pilot 2023-25">Buy Here</a></sub></details>|||
|Honda|Ridgeline 2017-25|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Ridgeline 2017-25">Buy Here</a></sub></details>|||
|Hyundai|Azera 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Azera 2022">Buy Here</a></sub></details>|||
|Hyundai|Azera Hybrid 2019|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Azera Hybrid 2019">Buy Here</a></sub></details>|||

@ -6,7 +6,7 @@ Development is coordinated through [Discord](https://discord.comma.ai) and GitHu
### Getting Started
* Setup your [development environment](../tools/)
* Set up your [development environment](/tools/)
* Join our [Discord](https://discord.comma.ai)
* Docs are at https://docs.comma.ai and https://blog.comma.ai

@ -7,7 +7,7 @@ export OPENBLAS_NUM_THREADS=1
export VECLIB_MAXIMUM_THREADS=1
if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="12.4"
export AGNOS_VERSION="12.8"
fi
export STAGING_ROOT="/data/safe_staging"

@ -1 +1 @@
Subproject commit 22b8df68fb6d8aa197fb9b432a428da6dd96c98c
Subproject commit 43006b9a41e233325cb7cbcb6ff40de0234217a0

@ -1 +1 @@
Subproject commit c2723b2f6bcce7e2d97f685db1ec2472a8dd28f5
Subproject commit 3dc21386239e3073a623156b75901aa302340d6c

@ -120,7 +120,6 @@ dev = [
tools = [
"metadrive-simulator @ https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl ; (platform_machine != 'aarch64')",
#"rerun-sdk >= 0.18", # this is pretty big, so only enable once we use it
]
[project.urls]

@ -3,29 +3,34 @@
```
## release checklist
**Go to `devel-staging`**
### Go to staging
- [ ] make a GitHub issue to track release
- [ ] create release master branch
- [ ] update RELEASES.md
- [ ] update `devel-staging`: `git reset --hard origin/master-ci`
- [ ] open a pull request from `devel-staging` to `devel`
- [ ] post on Discord
**Go to `devel`**
- [ ] bump version on master: `common/version.h` and `RELEASES.md`
- [ ] before merging the pull request
- [ ] build new userdata partition from `release3-staging`
- [ ] post on Discord, tag `@release crew`
Updating staging:
1. either rebase on master or cherry-pick changes
2. run this to update: `BRANCH=devel-staging release/build_devel.sh`
3. build new userdata partition from `release3-staging`
### Go to release
- [ ] before going to release, test the following:
- [ ] update from previous release -> new release
- [ ] update from new release -> previous release
- [ ] fresh install with `openpilot-test.comma.ai`
- [ ] drive on fresh install
- [ ] no submodules or LFS
- [ ] check sentry, MTBF, etc.
**Go to `release3`**
- [ ] stress test passes in production
- [ ] publish the blog post
- [ ] `git reset --hard origin/release3-staging`
- [ ] tag the release: `git tag v0.X.X <commit-hash> && git push origin v0.X.X`
- [ ] create GitHub release
- [ ] final test install on `openpilot.comma.ai`
- [ ] update factory provisioning
- [ ] close out milestone
- [ ] close out milestone and issue
- [ ] post on Discord, X, etc.
```

@ -17,28 +17,23 @@ rm -rf $TARGET_DIR
mkdir -p $TARGET_DIR
cd $TARGET_DIR
cp -r $SOURCE_DIR/.git $TARGET_DIR
pre-commit uninstall || true
echo "[-] bringing __nightly and devel in sync T=$SECONDS"
echo "[-] setting up stripped branch sync T=$SECONDS"
cd $TARGET_DIR
git fetch --depth 1 origin __nightly
git fetch --depth 1 origin devel
git checkout -f --track origin/__nightly
git reset --hard __nightly
git checkout __nightly
git reset --hard origin/devel
git clean -xdff
git lfs uninstall
# tmp branch
git checkout --orphan tmp
# remove everything except .git
echo "[-] erasing old openpilot T=$SECONDS"
git submodule deinit -f --all
git rm -rf --cached .
find . -maxdepth 1 -not -path './.git' -not -name '.' -not -name '..' -exec rm -rf '{}' \;
# reset source tree
# cleanup before the copy
cd $SOURCE_DIR
git clean -xdff
git submodule foreach --recursive git clean -xdff
# do the files copy
echo "[-] copying files T=$SECONDS"
@ -47,6 +42,7 @@ cp -pR --parents $(./release/release_files.py) $TARGET_DIR/
# in the directory
cd $TARGET_DIR
rm -rf .git/modules/
rm -f panda/board/obj/panda.bin.signed
# include source commit hash and build date in commit
@ -85,7 +81,7 @@ fi
if [ ! -z "$BRANCH" ]; then
echo "[-] Pushing to $BRANCH T=$SECONDS"
git push -f origin __nightly:$BRANCH
git push -f origin tmp:$BRANCH
fi
echo "[-] done T=$SECONDS, ready at $TARGET_DIR"

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a69514fe68dd1b4c73f28771640dcba10fc40989d1c7e771cb48bfd830fef206
size 5713

@ -40,7 +40,7 @@ class Controls:
'driverMonitoringState', 'onroadEvents', 'driverAssistance'], poll='selfdriveState')
self.pm = messaging.PubMaster(['carControl', 'controlsState'])
self.steer_limited_by_controls = False
self.steer_limited_by_safety = False
self.curvature = 0.0
self.desired_curvature = 0.0
@ -120,7 +120,7 @@ class Controls:
actuators.curvature = self.desired_curvature
steer, steeringAngleDeg, lac_log = self.LaC.update(CC.latActive, CS, self.VM, lp,
self.steer_limited_by_controls, self.desired_curvature,
self.steer_limited_by_safety, self.desired_curvature,
curvature_limited) # TODO what if not available
actuators.torque = float(steer)
actuators.steeringAngleDeg = float(steeringAngleDeg)
@ -167,10 +167,10 @@ class Controls:
if self.sm['selfdriveState'].active:
CO = self.sm['carOutput']
if self.CP.steerControlType == car.CarParams.SteerControlType.angle:
self.steer_limited_by_controls = abs(CC.actuators.steeringAngleDeg - CO.actuatorsOutput.steeringAngleDeg) > \
self.steer_limited_by_safety = abs(CC.actuators.steeringAngleDeg - CO.actuatorsOutput.steeringAngleDeg) > \
STEER_ANGLE_SATURATION_THRESHOLD
else:
self.steer_limited_by_controls = abs(CC.actuators.torque - CO.actuatorsOutput.torque) > 1e-2
self.steer_limited_by_safety = abs(CC.actuators.torque - CO.actuatorsOutput.torque) > 1e-2
# TODO: both controlsState and carControl valids should be set by
# sm.all_checks(), but this creates a circular dependency

@ -15,15 +15,15 @@ class LatControl(ABC):
self.steer_max = 1.0
@abstractmethod
def update(self, active, CS, VM, params, steer_limited_by_controls, desired_curvature, calibrated_pose, curvature_limited):
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited):
pass
def reset(self):
self.sat_count = 0.
def _check_saturation(self, saturated, CS, steer_limited_by_controls, curvature_limited):
def _check_saturation(self, saturated, CS, steer_limited_by_safety, curvature_limited):
# Saturated only if control output is not being limited by car torque/angle rate limits
if (saturated or curvature_limited) and CS.vEgo > self.sat_check_min_speed and not steer_limited_by_controls and not CS.steeringPressed:
if (saturated or curvature_limited) and CS.vEgo > self.sat_check_min_speed and not steer_limited_by_safety and not CS.steeringPressed:
self.sat_count += self.sat_count_rate
else:
self.sat_count -= self.sat_count_rate

@ -3,6 +3,7 @@ import math
from cereal import log
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
# TODO This is speed dependent
STEER_ANGLE_SATURATION_THRESHOLD = 2.5 # Degrees
@ -10,9 +11,9 @@ class LatControlAngle(LatControl):
def __init__(self, CP, CI):
super().__init__(CP, CI)
self.sat_check_min_speed = 5.
self.use_steer_limited_by_controls = CP.brand == "tesla"
self.use_steer_limited_by_safety = CP.brand == "tesla"
def update(self, active, CS, VM, params, steer_limited_by_controls, desired_curvature, curvature_limited):
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, curvature_limited):
angle_log = log.ControlsState.LateralAngleState.new_message()
if not active:
@ -23,9 +24,9 @@ class LatControlAngle(LatControl):
angle_steers_des = math.degrees(VM.get_steer_from_curvature(-desired_curvature, CS.vEgo, params.roll))
angle_steers_des += params.angleOffsetDeg
if self.use_steer_limited_by_controls:
if self.use_steer_limited_by_safety:
# these cars' carcontrollers calculate max lateral accel and jerk, so we can rely on carOutput for saturation
angle_control_saturated = steer_limited_by_controls
angle_control_saturated = steer_limited_by_safety
else:
# for cars which use a method of limiting torque such as a torque signal (Nissan and Toyota)
# or relying on EPS (Ford Q3), carOutput does not capture maxing out torque # TODO: this can be improved

@ -13,11 +13,7 @@ class LatControlPID(LatControl):
k_f=CP.lateralTuning.pid.kf, pos_limit=self.steer_max, neg_limit=-self.steer_max)
self.get_steer_feedforward = CI.get_steer_feedforward_function()
def reset(self):
super().reset()
self.pid.reset()
def update(self, active, CS, VM, params, steer_limited_by_controls, desired_curvature, curvature_limited):
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, curvature_limited):
pid_log = log.ControlsState.LateralPIDState.new_message()
pid_log.steeringAngleDeg = float(CS.steeringAngleDeg)
pid_log.steeringRateDeg = float(CS.steeringRateDeg)
@ -29,20 +25,24 @@ class LatControlPID(LatControl):
pid_log.steeringAngleDesiredDeg = angle_steers_des
pid_log.angleError = error
if not active:
output_steer = 0.0
output_torque = 0.0
pid_log.active = False
self.pid.reset()
else:
# offset does not contribute to resistive torque
steer_feedforward = self.get_steer_feedforward(angle_steers_des_no_offset, CS.vEgo)
ff = self.get_steer_feedforward(angle_steers_des_no_offset, CS.vEgo)
freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
output_torque = self.pid.update(error,
feedforward=ff,
speed=CS.vEgo,
freeze_integrator=freeze_integrator)
output_steer = self.pid.update(error, override=CS.steeringPressed,
feedforward=steer_feedforward, speed=CS.vEgo)
pid_log.active = True
pid_log.p = float(self.pid.p)
pid_log.i = float(self.pid.i)
pid_log.f = float(self.pid.f)
pid_log.output = float(output_steer)
pid_log.saturated = bool(self._check_saturation(self.steer_max - abs(output_steer) < 1e-3, CS, steer_limited_by_controls, curvature_limited))
pid_log.output = float(output_torque)
pid_log.saturated = bool(self._check_saturation(self.steer_max - abs(output_torque) < 1e-3, CS, steer_limited_by_safety, curvature_limited))
return output_steer, angle_steers_des, pid_log
return output_torque, angle_steers_des, pid_log

@ -3,7 +3,6 @@ import numpy as np
from cereal import log
from opendbc.car.lateral import FRICTION_THRESHOLD, get_friction
from opendbc.car.interfaces import LatControlInputs
from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
from openpilot.common.pid import PIDController
@ -27,17 +26,24 @@ class LatControlTorque(LatControl):
def __init__(self, CP, CI):
super().__init__(CP, CI)
self.torque_params = CP.lateralTuning.torque.as_builder()
self.pid = PIDController(self.torque_params.kp, self.torque_params.ki,
k_f=self.torque_params.kf, pos_limit=self.steer_max, neg_limit=-self.steer_max)
self.torque_from_lateral_accel = CI.torque_from_lateral_accel()
self.lateral_accel_from_torque = CI.lateral_accel_from_torque()
self.pid = PIDController(self.torque_params.kp, self.torque_params.ki,
k_f=self.torque_params.kf)
self.update_limits()
self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg
def update_live_torque_params(self, latAccelFactor, latAccelOffset, friction):
self.torque_params.latAccelFactor = latAccelFactor
self.torque_params.latAccelOffset = latAccelOffset
self.torque_params.friction = friction
self.update_limits()
def update(self, active, CS, VM, params, steer_limited_by_controls, desired_curvature, curvature_limited):
def update_limits(self):
self.pid.set_limits(self.lateral_accel_from_torque(self.steer_max, self.torque_params),
self.lateral_accel_from_torque(-self.steer_max, self.torque_params))
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, curvature_limited):
pid_log = log.ControlsState.LateralTorqueState.new_message()
if not active:
output_torque = 0.0
@ -57,30 +63,28 @@ class LatControlTorque(LatControl):
setpoint = desired_lateral_accel + low_speed_factor * desired_curvature
measurement = actual_lateral_accel + low_speed_factor * actual_curvature
gravity_adjusted_lateral_accel = desired_lateral_accel - roll_compensation
torque_from_setpoint = self.torque_from_lateral_accel(LatControlInputs(setpoint, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params,
gravity_adjusted=False)
torque_from_measurement = self.torque_from_lateral_accel(LatControlInputs(measurement, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params,
gravity_adjusted=False)
pid_log.error = float(torque_from_setpoint - torque_from_measurement)
ff = self.torque_from_lateral_accel(LatControlInputs(gravity_adjusted_lateral_accel, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params,
gravity_adjusted=True)
# do error correction in lateral acceleration space, convert at end to handle non-linear torque responses correctly
pid_log.error = float(setpoint - measurement)
ff = gravity_adjusted_lateral_accel
ff += get_friction(desired_lateral_accel - actual_lateral_accel, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
freeze_integrator = steer_limited_by_controls or CS.steeringPressed or CS.vEgo < 5
output_torque = self.pid.update(pid_log.error,
freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
output_lataccel = self.pid.update(pid_log.error,
feedforward=ff,
speed=CS.vEgo,
freeze_integrator=freeze_integrator)
output_torque = self.torque_from_lateral_accel(output_lataccel, self.torque_params)
pid_log.active = True
pid_log.p = float(self.pid.p)
pid_log.i = float(self.pid.i)
pid_log.d = float(self.pid.d)
pid_log.f = float(self.pid.f)
pid_log.output = float(-output_torque)
pid_log.output = float(-output_torque) # TODO: log lat accel?
pid_log.actualLateralAccel = float(actual_lateral_accel)
pid_log.desiredLateralAccel = float(desired_lateral_accel)
pid_log.saturated = bool(self._check_saturation(self.steer_max - abs(output_torque) < 1e-3, CS, steer_limited_by_controls, curvature_limited))
pid_log.saturated = bool(self._check_saturation(self.steer_max - abs(output_torque) < 1e-3, CS, steer_limited_by_safety, curvature_limited))
# TODO left is positive in this convention
return -output_torque, 0.0, pid_log

@ -19,7 +19,7 @@ LON_MPC_STEP = 0.2 # first step is 0.2s
A_CRUISE_MAX_VALS = [1.6, 1.2, 0.8, 0.6]
A_CRUISE_MAX_BP = [0., 10.0, 25., 40.]
CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N]
ALLOW_THROTTLE_THRESHOLD = 0.5
ALLOW_THROTTLE_THRESHOLD = 0.4
MIN_ALLOW_THROTTLE_SPEED = 2.5
# Lookup table for turns
@ -90,7 +90,7 @@ class LongitudinalPlanner:
return x, v, a, j, throttle_prob
def update(self, sm):
self.mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc'
mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc'
if len(sm['carControl'].orientationNED) == 3:
accel_coast = get_coast_accel(sm['carControl'].orientationNED[1])
@ -113,7 +113,7 @@ class LongitudinalPlanner:
# No change cost when user is controlling the speed, or when standstill
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
if self.mode == 'acc':
if mode == 'acc':
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
@ -163,7 +163,7 @@ class LongitudinalPlanner:
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
output_should_stop_e2e = sm['modelV2'].action.shouldStop
if self.mode == 'acc':
if mode == 'acc':
output_a_target = output_a_target_mpc
self.output_should_stop = output_should_stop_mpc
else:

@ -5,6 +5,7 @@ from opendbc.car.car_helpers import interfaces
from opendbc.car.honda.values import CAR as HONDA
from opendbc.car.toyota.values import CAR as TOYOTA
from opendbc.car.nissan.values import CAR as NISSAN
from opendbc.car.gm.values import CAR as GM
from opendbc.car.vehicle_model import VehicleModel
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
@ -13,7 +14,8 @@ from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle
class TestLatControl:
@parameterized.expand([(HONDA.HONDA_CIVIC, LatControlPID), (TOYOTA.TOYOTA_RAV4, LatControlTorque), (NISSAN.NISSAN_LEAF, LatControlAngle)])
@parameterized.expand([(HONDA.HONDA_CIVIC, LatControlPID), (TOYOTA.TOYOTA_RAV4, LatControlTorque),
(NISSAN.NISSAN_LEAF, LatControlAngle), (GM.CHEVROLET_BOLT_EUV, LatControlTorque)])
def test_saturation(self, car_name, controller):
CarInterface = interfaces[car_name]
CP = CarInterface.get_non_essential_params(car_name)

@ -32,7 +32,7 @@ MIN_BUCKET_POINTS = np.array([100, 300, 500, 500, 500, 500, 300, 100])
MIN_ENGAGE_BUFFER = 2 # secs
VERSION = 1 # bump this to invalidate old parameter caches
ALLOWED_CARS = ['toyota', 'hyundai', 'rivian']
ALLOWED_CARS = ['toyota', 'hyundai', 'rivian', 'honda']
def slope2rot(slope):

@ -102,15 +102,12 @@ class ModelState:
self.full_features_buffer = np.zeros((1, ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32)
self.full_desire = np.zeros((1, ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.DESIRE_LEN), dtype=np.float32)
self.full_prev_desired_curv = np.zeros((1, ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.PREV_DESIRED_CURV_LEN), dtype=np.float32)
self.temporal_idxs = slice(-1-(ModelConstants.TEMPORAL_SKIP*(ModelConstants.INPUT_HISTORY_BUFFER_LEN-1)), None, ModelConstants.TEMPORAL_SKIP)
# policy inputs
self.numpy_inputs = {
'desire': np.zeros((1, ModelConstants.INPUT_HISTORY_BUFFER_LEN, ModelConstants.DESIRE_LEN), dtype=np.float32),
'traffic_convention': np.zeros((1, ModelConstants.TRAFFIC_CONVENTION_LEN), dtype=np.float32),
'lateral_control_params': np.zeros((1, ModelConstants.LATERAL_CONTROL_PARAMS_LEN), dtype=np.float32),
'prev_desired_curv': np.zeros((1, ModelConstants.INPUT_HISTORY_BUFFER_LEN, ModelConstants.PREV_DESIRED_CURV_LEN), dtype=np.float32),
'features_buffer': np.zeros((1, ModelConstants.INPUT_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32),
}
@ -143,7 +140,6 @@ class ModelState:
self.numpy_inputs['desire'][:] = self.full_desire.reshape((1,ModelConstants.INPUT_HISTORY_BUFFER_LEN,ModelConstants.TEMPORAL_SKIP,-1)).max(axis=2)
self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention']
self.numpy_inputs['lateral_control_params'][:] = inputs['lateral_control_params']
imgs_cl = {name: self.frames[name].prepare(bufs[name], transforms[name].flatten()) for name in self.vision_input_names}
if TICI and not USBGPU:
@ -169,11 +165,6 @@ class ModelState:
self.policy_output = self.policy_run(**self.policy_inputs).contiguous().realize().uop.base.buffer.numpy()
policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(self.policy_output, self.policy_output_slices))
# TODO model only uses last value now
self.full_prev_desired_curv[0,:-1] = self.full_prev_desired_curv[0,1:]
self.full_prev_desired_curv[0,-1,:] = policy_outputs_dict['desired_curvature'][0, :]
self.numpy_inputs['prev_desired_curv'][:] = 0*self.full_prev_desired_curv[0, self.temporal_idxs]
combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict}
if SEND_RAW_PRED:
combined_outputs_dict['raw_pred'] = np.concatenate([self.vision_output.copy(), self.policy_output.copy()])
@ -292,7 +283,6 @@ def main(demo=False):
frame_id = sm["roadCameraState"].frameId
v_ego = max(sm["carState"].vEgo, 0.)
lat_delay = sm["liveDelay"].lateralDelay + LAT_SMOOTH_SECONDS
lateral_control_params = np.array([v_ego, lat_delay], dtype=np.float32)
if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']:
device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32)
dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))]
@ -325,7 +315,6 @@ def main(demo=False):
inputs:dict[str, np.ndarray] = {
'desire': vec_desire,
'traffic_convention': traffic_convention,
'lateral_control_params': lateral_control_params,
}
mt1 = time.perf_counter()

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:22aec22a10ce09384d4a4af2a0bbff08d54af7e0c888503508f356fae4ff0e29
size 15583374
oid sha256:04b763fb71efe57a8a4c4168a8043ecd58939015026ded0dc755ded6905ac251
size 12343523

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c824f68646a3b94f117f01c70dc8316fb466e05fbd42ccdba440b8a8dc86914b
size 46265993
oid sha256:e66bb8d53eced3786ed71a59b55ffc6810944cb217f0518621cc76303260a1ef
size 46271991

@ -22,9 +22,10 @@ class Parser:
self.ignore_missing = ignore_missing
def check_missing(self, outs, name):
if name not in outs and not self.ignore_missing:
missing = name not in outs
if missing and not self.ignore_missing:
raise ValueError(f"Missing output {name}")
return name not in outs
return missing
def parse_categorical_crossentropy(self, name, outs, out_shape=None):
if self.check_missing(outs, name):
@ -84,6 +85,13 @@ class Parser:
outs[name] = pred_mu_final.reshape(final_shape)
outs[name + '_stds'] = pred_std_final.reshape(final_shape)
def is_mhp(self, outs, name, shape):
if self.check_missing(outs, name):
return False
if outs[name].shape[1] == 2 * shape:
return False
return True
def parse_vision_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,))
@ -94,17 +102,17 @@ class Parser:
self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH))
self.parse_binary_crossentropy('meta', outs)
self.parse_binary_crossentropy('lead_prob', outs)
self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION,
out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH))
lead_mhp = self.is_mhp(outs, 'lead', ModelConstants.LEAD_MHP_SELECTION * ModelConstants.LEAD_TRAJ_LEN * ModelConstants.LEAD_WIDTH)
lead_in_N, lead_out_N = (ModelConstants.LEAD_MHP_N, ModelConstants.LEAD_MHP_SELECTION) if lead_mhp else (0, 0)
lead_out_shape = (ModelConstants.LEAD_TRAJ_LEN, ModelConstants.LEAD_WIDTH) if lead_mhp else \
(ModelConstants.LEAD_MHP_SELECTION, ModelConstants.LEAD_TRAJ_LEN, ModelConstants.LEAD_WIDTH)
self.parse_mdn('lead', outs, in_N=lead_in_N, out_N=lead_out_N, out_shape=lead_out_shape)
return outs
def parse_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION,
out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH))
if 'lat_planner_solution' in outs:
self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH))
if 'desired_curvature' in outs:
self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,))
plan_mhp = self.is_mhp(outs, 'plan', ModelConstants.IDX_N * ModelConstants.PLAN_WIDTH)
plan_in_N, plan_out_N = (ModelConstants.PLAN_MHP_N, ModelConstants.PLAN_MHP_SELECTION) if plan_mhp else (0, 0)
self.parse_mdn('plan', outs, in_N=plan_in_N, out_N=plan_out_N, out_shape=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH))
self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,))
return outs

@ -87,7 +87,7 @@ def main() -> None:
# TODO: remove this in the next AGNOS
# wait until USB is up before counting
if time.monotonic() < 35.:
if time.monotonic() < 60.:
no_internal_panda_count = 0
# Handle missing internal panda

@ -42,7 +42,7 @@
"severity": 0
},
"Offroad_ExcessiveActuation": {
"text": "openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support.",
"text": "openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting.",
"severity": 1,
"_comment": "Set extra field to lateral or longitudinal."
}

@ -11,6 +11,8 @@ from openpilot.common.constants import CV
from openpilot.common.git import get_short_branch
from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.locationd.calibrationd import MIN_SPEED_FILTER
from openpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER
from openpilot.selfdrive.ui.feedback.feedbackd import FEEDBACK_MAX_DURATION
AlertSize = log.SelfdriveState.AlertSize
AlertStatus = log.SelfdriveState.AlertStatus
@ -198,6 +200,7 @@ class StartupAlert(Alert):
Priority.LOWER, VisualAlert.none, AudibleAlert.none, 5.),
# ********** helper functions **********
def get_display_speed(speed_ms: float, metric: bool) -> str:
speed = int(round(speed_ms * (CV.MS_TO_KPH if metric else CV.MS_TO_MPH)))
@ -252,6 +255,14 @@ def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messag
Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2)
def audio_feedback_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
duration = FEEDBACK_MAX_DURATION - ((sm['audioFeedback'].blockNum + 1) * SAMPLE_BUFFER / SAMPLE_RATE)
return NormalPermanentAlert(
"Recording Audio Feedback",
f"{round(duration)} second{'s' if round(duration) != 1 else ''} remaining. Press again to save early.",
priority=Priority.LOW)
# *** debug alerts ***
def out_of_space_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
@ -370,6 +381,7 @@ def invalid_lkas_setting_alert(CP: car.CarParams, CS: car.CarState, sm: messagin
return NormalPermanentAlert("Invalid LKAS setting", text)
EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
# ********** events with no alerts **********
@ -991,9 +1003,13 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
ET.WARNING: personality_changed_alert,
},
EventName.userFlag: {
EventName.userBookmark: {
ET.PERMANENT: NormalPermanentAlert("Bookmark Saved", duration=1.5),
},
EventName.audioFeedback: {
ET.PERMANENT: audio_feedback_alert,
},
}

@ -0,0 +1,54 @@
import math
from enum import StrEnum, auto
from cereal import car, messaging
from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.locationd.helpers import Pose
from opendbc.car import ACCELERATION_DUE_TO_GRAVITY
from opendbc.car.lateral import ISO_LATERAL_ACCEL
from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX
MIN_EXCESSIVE_ACTUATION_COUNT = int(0.25 / DT_CTRL)
MIN_LATERAL_ENGAGE_BUFFER = int(1 / DT_CTRL)
class ExcessiveActuationType(StrEnum):
LONGITUDINAL = auto()
LATERAL = auto()
class ExcessiveActuationCheck:
def __init__(self):
self._excessive_counter = 0
self._engaged_counter = 0
def update(self, sm: messaging.SubMaster, CS: car.CarState, calibrated_pose: Pose) -> ExcessiveActuationType | None:
# CS.aEgo can be noisy to bumps in the road, transitioning from standstill, losing traction, etc.
# longitudinal
accel_calibrated = calibrated_pose.acceleration.x
excessive_long_actuation = sm['carControl'].longActive and (accel_calibrated > ACCEL_MAX * 2 or accel_calibrated < ACCEL_MIN * 2)
# lateral
yaw_rate = calibrated_pose.angular_velocity.yaw
roll = sm['liveParameters'].roll
roll_compensated_lateral_accel = (CS.vEgo * yaw_rate) - (math.sin(roll) * ACCELERATION_DUE_TO_GRAVITY)
# Prevent false positives after overriding
excessive_lat_actuation = False
self._engaged_counter = self._engaged_counter + 1 if sm['carControl'].latActive and not CS.steeringPressed else 0
if self._engaged_counter > MIN_LATERAL_ENGAGE_BUFFER:
if abs(roll_compensated_lateral_accel) > ISO_LATERAL_ACCEL * 2:
excessive_lat_actuation = True
# livePose acceleration can be noisy due to bad mounting or aliased livePose measurements
livepose_valid = abs(CS.aEgo - accel_calibrated) < 2
self._excessive_counter = self._excessive_counter + 1 if livepose_valid and (excessive_long_actuation or excessive_lat_actuation) else 0
excessive_type = None
if self._excessive_counter > MIN_EXCESSIVE_ACTUATION_COUNT:
if excessive_long_actuation:
excessive_type = ExcessiveActuationType.LONGITUDINAL
else:
excessive_type = ExcessiveActuationType.LATERAL
return excessive_type

@ -7,7 +7,6 @@ import cereal.messaging as messaging
from cereal import car, log
from msgq.visionipc import VisionIpcClient, VisionStreamType
from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX
from openpilot.common.params import Params
@ -18,6 +17,7 @@ from openpilot.common.gps import get_gps_location_service
from openpilot.selfdrive.car.car_specific import CarSpecificEvents
from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose
from openpilot.selfdrive.selfdrived.events import Events, ET
from openpilot.selfdrive.selfdrived.helpers import ExcessiveActuationCheck
from openpilot.selfdrive.selfdrived.state import StateMachine
from openpilot.selfdrive.selfdrived.alertmanager import AlertManager, set_offroad_alert
@ -29,7 +29,6 @@ SIMULATION = "SIMULATION" in os.environ
TESTING_CLOSET = "TESTING_CLOSET" in os.environ
LONGITUDINAL_PERSONALITY_MAP = {v: k for k, v in log.LongitudinalPersonality.schema.enumerants.items()}
MIN_EXCESSIVE_ACTUATION_COUNT = int(0.25 / DT_CTRL)
ThermalStatus = log.DeviceState.ThermalStatus
State = log.SelfdriveState.OpenpilotState
@ -43,19 +42,6 @@ SafetyModel = car.CarParams.SafetyModel
IGNORED_SAFETY_MODES = (SafetyModel.silent, SafetyModel.noOutput)
def check_excessive_actuation(sm: messaging.SubMaster, CS: car.CarState, calibrated_pose: Pose, counter: int) -> tuple[int, bool]:
# CS.aEgo can be noisy to bumps in the road, transitioning from standstill, losing traction, etc.
accel_calibrated = calibrated_pose.acceleration.x
# livePose acceleration can be noisy due to bad mounting or aliased livePose measurements
accel_valid = abs(CS.aEgo - accel_calibrated) < 2
excessive_actuation = accel_calibrated > ACCEL_MAX * 2 or accel_calibrated < ACCEL_MIN * 2
counter = counter + 1 if sm['carControl'].longActive and excessive_actuation and accel_valid else 0
return counter, counter > MIN_EXCESSIVE_ACTUATION_COUNT
class SelfdriveD:
def __init__(self, CP=None):
self.params = Params()
@ -74,6 +60,8 @@ class SelfdriveD:
self.pose_calibrator = PoseCalibrator()
self.calibrated_pose: Pose | None = None
self.excessive_actuation_check = ExcessiveActuationCheck()
self.excessive_actuation = self.params.get("Offroad_ExcessiveActuation") is not None
# Setup sockets
self.pm = messaging.PubMaster(['selfdriveState', 'onroadEvents'])
@ -95,7 +83,7 @@ class SelfdriveD:
self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'liveCalibration',
'carOutput', 'driverMonitoringState', 'longitudinalPlan', 'livePose', 'liveDelay',
'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters',
'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userFlag'] + \
'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userBookmark', 'audioFeedback'] + \
self.camera_packets + self.sensor_packets + self.gps_packets,
ignore_alive=ignore, ignore_avg_freq=ignore,
ignore_valid=ignore, frequency=int(1/DT_CTRL))
@ -131,8 +119,6 @@ class SelfdriveD:
self.experimental_mode = False
self.personality = self.params.get("LongitudinalPersonality", return_default=True)
self.recalibrating_seen = False
self.excessive_actuation = self.params.get("Offroad_ExcessiveActuation") is not None
self.excessive_actuation_counter = 0
self.state_machine = StateMachine()
self.rk = Ratekeeper(100, print_delay_threshold=None)
@ -181,9 +167,12 @@ class SelfdriveD:
self.events.add(EventName.selfdriveInitializing)
return
# Check for user flag (bookmark) press
if self.sm.updated['userFlag']:
self.events.add(EventName.userFlag)
# Check for user bookmark press (bookmark button or end of LKAS button feedback)
if self.sm.updated['userBookmark']:
self.events.add(EventName.userBookmark)
if self.sm.updated['audioFeedback']:
self.events.add(EventName.audioFeedback)
# Don't add any more events while in dashcam mode
if self.CP.passive:
@ -249,7 +238,10 @@ class SelfdriveD:
if self.sm['driverAssistance'].leftLaneDeparture or self.sm['driverAssistance'].rightLaneDeparture:
self.events.add(EventName.ldw)
# Check for excessive (longitudinal) actuation
# ******************************************************************************************
# NOTE: To fork maintainers.
# Disabling or nerfing safety features will get you and your users banned from our servers.
# We recommend that you do not change these numbers from the defaults.
if self.sm.updated['liveCalibration']:
self.pose_calibrator.feed_live_calib(self.sm['liveCalibration'])
if self.sm.updated['livePose']:
@ -257,13 +249,14 @@ class SelfdriveD:
self.calibrated_pose = self.pose_calibrator.build_calibrated_pose(device_pose)
if self.calibrated_pose is not None:
self.excessive_actuation_counter, excessive_actuation = check_excessive_actuation(self.sm, CS, self.calibrated_pose, self.excessive_actuation_counter)
if not self.excessive_actuation and excessive_actuation:
set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text="longitudinal")
excessive_actuation = self.excessive_actuation_check.update(self.sm, CS, self.calibrated_pose)
if not self.excessive_actuation and excessive_actuation is not None:
set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text=str(excessive_actuation))
self.excessive_actuation = True
if self.excessive_actuation:
self.events.add(EventName.excessiveActuation)
# ******************************************************************************************
# Handle lane change
if self.sm['modelV2'].meta.laneChangeState == LaneChangeState.preLaneChange:

@ -418,10 +418,10 @@ CONFIGS = [
proc_name="selfdrived",
pubs=[
"carState", "deviceState", "pandaStates", "peripheralState", "liveCalibration", "driverMonitoringState",
"longitudinalPlan", "livePose", "liveDelay", "liveParameters", "radarState",
"modelV2", "driverCameraState", "roadCameraState", "wideRoadCameraState", "managerState",
"liveTorqueParameters", "accelerometer", "gyroscope", "carOutput",
"gpsLocationExternal", "gpsLocation", "controlsState", "carControl", "driverAssistance", "alertDebug",
"longitudinalPlan", "livePose", "liveDelay", "liveParameters", "radarState", "modelV2",
"driverCameraState", "roadCameraState", "wideRoadCameraState", "managerState", "liveTorqueParameters",
"accelerometer", "gyroscope", "carOutput", "gpsLocationExternal", "gpsLocation", "controlsState",
"carControl", "driverAssistance", "alertDebug", "audioFeedback",
],
subs=["selfdriveState", "onroadEvents"],
ignore=["logMonoTime"],

@ -1 +1 @@
8ff1b4c9c7a34589142a07579b0051acddfe7699
6d3219bca9f66a229b38a5382d301a92b0147edb

@ -61,7 +61,7 @@ segments = [
]
# dashcamOnly makes don't need to be tested until a full port is done
excluded_interfaces = ["mock", "body"]
excluded_interfaces = ["mock", "body", "psa"]
BASE_URL = "https://commadataci.blob.core.windows.net/openpilotci/"
REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit")

@ -54,6 +54,7 @@ while true; do
# /data/ciui.py &
#fi
awk '{print \$1}' /proc/uptime > /var/tmp/power_watchdog
sleep 5s
done

@ -55,6 +55,7 @@ PROCS = {
"selfdrive.locationd.paramsd": 9.0,
"selfdrive.locationd.lagd": 11.0,
"selfdrive.ui.soundd": 3.0,
"selfdrive.ui.feedback.feedbackd": 1.0,
"selfdrive.monitoring.dmonitoringd": 4.0,
"./proclogd": 2.0,
"system.logmessaged": 1.0,
@ -332,20 +333,18 @@ class TestOnroad:
assert np.all(eof_sof_diff > 0)
assert np.all(eof_sof_diff < 50*1e6)
first_fid = {c: min(self.ts[c]['frameId']) for c in cams}
first_fid = {min(self.ts[c]['frameId']) for c in cams}
assert len(first_fid) == 1, "Cameras don't start on same frame ID"
if cam.endswith('CameraState'):
# camerad guarantees that all cams start on frame ID 0
# (note loggerd also needs to start up fast enough to catch it)
assert set(first_fid.values()) == {0, }, "Cameras don't start on frame ID 0"
else:
# encoder guarantees all cams start on the same frame ID
assert len(set(first_fid.values())) == 1, "Cameras don't start on same frame ID"
assert next(iter(first_fid)) < 100, "Cameras start on frame ID too high"
# we don't do a full segment rotation, so these might not match exactly
last_fid = {c: max(self.ts[c]['frameId']) for c in cams}
assert max(last_fid.values()) - min(last_fid.values()) < 10
last_fid = {max(self.ts[c]['frameId']) for c in cams}
assert max(last_fid) - min(last_fid) < 10
start, end = min(first_fid.values()), min(last_fid.values())
start, end = min(first_fid), min(last_fid)
for i in range(end-start):
ts = {c: round(self.ts[c]['timestampSof'][i]/1e6, 1) for c in cams}
diff = (max(ts.values()) - min(ts.values()))

@ -63,14 +63,8 @@ if GetOption('extras'):
qt_src.remove("main.cc") # replaced by test_runner
qt_env.Program('tests/test_translations', [asset_obj, 'tests/test_runner.cc', 'tests/test_translations.cc'] + qt_src, LIBS=qt_libs)
qt_env.SharedLibrary("qt/python_helpers", ["qt/qt_window.cc"], LIBS=qt_libs)
# setup
qt_env.Program("qt/setup/setup", ["qt/setup/setup.cc", asset_obj],
LIBS=qt_libs + ['curl', 'common'])
if arch != "Darwin":
# build installers
if arch != "Darwin":
raylib_env = env.Clone()
raylib_env['LIBPATH'] += [f'#third_party/raylib/{arch}/']
raylib_env['LINKFLAGS'].append('-Wl,-strip-debug')
@ -102,7 +96,3 @@ if GetOption('extras'):
f = raylib_env.Program(f"installer/installers/installer_{name}", [obj, cont, inter], LIBS=raylib_libs)
# keep installers small
assert f[0].get_size() < 1900*1e3, f[0].get_size()
# build watch3
if arch in ['x86_64', 'aarch64', 'Darwin'] or GetOption('extras'):
qt_env.Program("watch3", ["watch3.cc"], LIBS=qt_libs + ['common', 'msgq', 'visionipc'])

@ -0,0 +1,71 @@
#!/usr/bin/env python3
import cereal.messaging as messaging
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from cereal import car
from openpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER
FEEDBACK_MAX_DURATION = 10.0
ButtonType = car.CarState.ButtonEvent.Type
def main():
params = Params()
pm = messaging.PubMaster(['userBookmark', 'audioFeedback'])
sm = messaging.SubMaster(['rawAudioData', 'bookmarkButton', 'carState'])
should_record_audio = False
block_num = 0
waiting_for_release = False
early_stop_triggered = False
while True:
sm.update()
should_send_bookmark = False
# TODO: https://github.com/commaai/openpilot/issues/36015
if False and sm.updated['carState'] and sm['carState'].canValid:
for be in sm['carState'].buttonEvents:
if be.type == ButtonType.lkas:
if be.pressed:
if not should_record_audio:
if params.get_bool("RecordAudioFeedback"): # Start recording on first press if toggle set
should_record_audio = True
block_num = 0
waiting_for_release = False
early_stop_triggered = False
cloudlog.info("LKAS button pressed - starting 10-second audio feedback")
else:
should_send_bookmark = True # immediately send bookmark if toggle false
cloudlog.info("LKAS button pressed - bookmarking")
elif should_record_audio and not waiting_for_release: # Wait for release of second press to stop recording early
waiting_for_release = True
elif waiting_for_release: # Second press released
waiting_for_release = False
early_stop_triggered = True
cloudlog.info("LKAS button released - ending recording early")
if should_record_audio and sm.updated['rawAudioData']:
raw_audio = sm['rawAudioData']
msg = messaging.new_message('audioFeedback', valid=True)
msg.audioFeedback.audio.data = raw_audio.data
msg.audioFeedback.audio.sampleRate = raw_audio.sampleRate
msg.audioFeedback.blockNum = block_num
block_num += 1
if (block_num * SAMPLE_BUFFER / SAMPLE_RATE) >= FEEDBACK_MAX_DURATION or early_stop_triggered: # Check for timeout or early stop
should_send_bookmark = True # send bookmark at end of audio segment
should_record_audio = False
early_stop_triggered = False
cloudlog.info("10-second recording completed or second button press - stopping audio feedback")
pm.send('audioFeedback', msg)
if sm.updated['bookmarkButton']:
cloudlog.info("Bookmark button pressed!")
should_send_bookmark = True
if should_send_bookmark:
msg = messaging.new_message('userBookmark', valid=True)
pm.send('userBookmark', msg)
if __name__ == '__main__':
main()

@ -24,11 +24,13 @@ const std::string BRANCH_STR = get_str(BRANCH "?
#define GIT_SSH_URL "git@github.com:commaai/openpilot.git"
#define CONTINUE_PATH "/data/continue.sh"
const std::string CACHE_PATH = "/data/openpilot.cache";
const std::string INSTALL_PATH = "/data/openpilot";
const std::string VALID_CACHE_PATH = "/data/.openpilot_cache";
#define INSTALL_PATH "/data/openpilot"
#define TMP_INSTALL_PATH "/data/tmppilot"
const int FONT_SIZE = 120;
extern const uint8_t str_continue[] asm("_binary_selfdrive_ui_installer_continue_openpilot_sh_start");
extern const uint8_t str_continue_end[] asm("_binary_selfdrive_ui_installer_continue_openpilot_sh_end");
extern const uint8_t inter_ttf[] asm("_binary_selfdrive_ui_installer_inter_ascii_ttf_start");
@ -41,6 +43,16 @@ void run(const char* cmd) {
assert(err == 0);
}
void finishInstall() {
BeginDrawing();
ClearBackground(BLACK);
const char *m = "Finishing install...";
int text_width = MeasureText(m, FONT_SIZE);
DrawTextEx(font, m, (Vector2){(float)(GetScreenWidth() - text_width)/2 + FONT_SIZE, (float)(GetScreenHeight() - FONT_SIZE)/2}, FONT_SIZE, 0, WHITE);
EndDrawing();
util::sleep_for(60 * 1000);
}
void renderProgress(int progress) {
BeginDrawing();
ClearBackground(BLACK);
@ -62,11 +74,11 @@ int doInstall() {
}
// cleanup previous install attempts
run("rm -rf " TMP_INSTALL_PATH " " INSTALL_PATH);
run("rm -rf " TMP_INSTALL_PATH);
// do the install
if (util::file_exists(CACHE_PATH)) {
return cachedFetch(CACHE_PATH);
if (util::file_exists(INSTALL_PATH) && util::file_exists(VALID_CACHE_PATH)) {
return cachedFetch(INSTALL_PATH);
} else {
return freshClone();
}
@ -135,7 +147,9 @@ void cloneFinished(int exitCode) {
run("git submodule update --init");
// move into place
run("mv " TMP_INSTALL_PATH " " INSTALL_PATH);
run(("rm -f " + VALID_CACHE_PATH).c_str());
run(("rm -rf " + INSTALL_PATH).c_str());
run(util::string_format("mv %s %s", TMP_INSTALL_PATH, INSTALL_PATH.c_str()).c_str());
#ifdef INTERNAL
run("mkdir -p /data/params/d/");
@ -153,9 +167,9 @@ void cloneFinished(int exitCode) {
param << value;
param.close();
}
run("cd " INSTALL_PATH " && "
run(("cd " + INSTALL_PATH + " && "
"git remote set-url origin --push " GIT_SSH_URL " && "
"git config --replace-all remote.origin.fetch \"+refs/heads/*:refs/remotes/origin/*\"");
"git config --replace-all remote.origin.fetch \"+refs/heads/*:refs/remotes/origin/*\"").c_str());
#endif
// write continue.sh
@ -171,16 +185,22 @@ void cloneFinished(int exitCode) {
run("mv /data/continue.sh.new " CONTINUE_PATH);
// wait for the installed software's UI to take over
util::sleep_for(60 * 1000);
finishInstall();
}
int main(int argc, char *argv[]) {
InitWindow(2160, 1080, "Installer");
font = LoadFontFromMemory(".ttf", inter_ttf, inter_ttf_end - inter_ttf, 120, NULL, 0);
font = LoadFontFromMemory(".ttf", inter_ttf, inter_ttf_end - inter_ttf, FONT_SIZE, NULL, 0);
SetTextureFilter(font.texture, TEXTURE_FILTER_BILINEAR);
if (util::file_exists(CONTINUE_PATH)) {
finishInstall();
} else {
renderProgress(0);
int result = doInstall();
cloneFinished(result);
}
CloseWindow();
UnloadFont(font);
return 0;

@ -19,7 +19,7 @@ class MainLayout(Widget):
def __init__(self):
super().__init__()
self._pm = messaging.PubMaster(['userFlag'])
self._pm = messaging.PubMaster(['bookmarkButton'])
self._sidebar = Sidebar()
self._current_mode = MainState.HOME
@ -40,7 +40,7 @@ class MainLayout(Widget):
def _setup_callbacks(self):
self._sidebar.set_callbacks(on_settings=self._on_settings_clicked,
on_flag=self._on_flag_clicked)
on_flag=self._on_bookmark_clicked)
self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(lambda: self.open_settings(PanelType.FIREHOSE))
self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state)
self._layouts[MainState.ONROAD].set_callbacks(on_click=self._on_onroad_clicked)
@ -76,10 +76,10 @@ class MainLayout(Widget):
def _on_settings_clicked(self):
self.open_settings(PanelType.DEVICE)
def _on_flag_clicked(self):
user_flag = messaging.new_message('userFlag')
user_flag.valid = True
self._pm.send('userFlag', user_flag)
def _on_bookmark_clicked(self):
user_bookmark = messaging.new_message('bookmarkButton')
user_bookmark.valid = True
self._pm.send('bookmarkButton', user_bookmark)
def _on_onroad_clicked(self):
self._sidebar.set_visible(not self._sidebar.is_visible)

@ -28,7 +28,7 @@ PANEL_COLOR = rl.Color(41, 41, 41, 255)
CLOSE_BTN_COLOR = rl.Color(41, 41, 41, 255)
CLOSE_BTN_PRESSED = rl.Color(59, 59, 59, 255)
TEXT_NORMAL = rl.Color(128, 128, 128, 255)
TEXT_SELECTED = rl.Color(255, 255, 255, 255)
TEXT_SELECTED = rl.WHITE
class PanelType(IntEnum):

@ -24,18 +24,18 @@ NetworkType = log.DeviceState.NetworkType
# Color scheme
class Colors:
SIDEBAR_BG = rl.Color(57, 57, 57, 255)
WHITE = rl.Color(255, 255, 255, 255)
WHITE = rl.WHITE
WHITE_DIM = rl.Color(255, 255, 255, 85)
GRAY = rl.Color(84, 84, 84, 255)
# Status colors
GOOD = rl.Color(255, 255, 255, 255)
GOOD = rl.WHITE
WARNING = rl.Color(218, 202, 37, 255)
DANGER = rl.Color(201, 34, 49, 255)
# UI elements
METRIC_BORDER = rl.Color(255, 255, 255, 85)
BUTTON_NORMAL = rl.Color(255, 255, 255, 255)
BUTTON_NORMAL = rl.WHITE
BUTTON_PRESSED = rl.Color(255, 255, 255, 166)
@ -146,7 +146,7 @@ class Sidebar(Widget):
def _draw_buttons(self, rect: rl.Rectangle):
mouse_pos = rl.get_mouse_position()
mouse_down = self._is_pressed and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT)
mouse_down = self.is_pressed and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT)
# Settings button
settings_down = mouse_down and rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN)

@ -2,12 +2,15 @@ from enum import IntEnum
import os
import threading
import time
from functools import lru_cache
from openpilot.common.api import Api, api_get
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
TOKEN_EXPIRY_HOURS = 2
class PrimeType(IntEnum):
UNKNOWN = -2,
@ -20,6 +23,12 @@ class PrimeType(IntEnum):
PURPLE = 5,
@lru_cache(maxsize=1)
def get_token(dongle_id: str, t: int):
print('getting token')
return Api(dongle_id).get_token(expiry_hours=TOKEN_EXPIRY_HOURS)
class PrimeState:
FETCH_INTERVAL = 5.0 # seconds between API calls
API_TIMEOUT = 10.0 # seconds for API requests
@ -49,13 +58,15 @@ class PrimeState:
return
try:
identity_token = Api(dongle_id).get_token()
identity_token = get_token(dongle_id, int(time.monotonic() / (TOKEN_EXPIRY_HOURS / 2 * 60 * 60)))
response = api_get(f"v1.1/devices/{dongle_id}", timeout=self.API_TIMEOUT, access_token=identity_token)
if response.status_code == 200:
data = response.json()
is_paired = data.get("is_paired", False)
prime_type = data.get("prime_type", 0)
self.set_type(PrimeType(prime_type) if is_paired else PrimeType.UNPAIRED)
elif response.status_code == 401:
get_token.cache_clear()
except Exception as e:
cloudlog.error(f"Failed to fetch prime status: {e}")

@ -141,6 +141,9 @@ class CameraView(Widget):
self.client = None
def __del__(self):
self.close()
def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray:
if not self.frame:
return np.eye(3)
@ -337,16 +340,7 @@ class CameraView(Widget):
if __name__ == "__main__":
gui_app.init_window("watch3")
road_camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD)
driver_camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER)
wide_road_camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD)
try:
gui_app.init_window("camera view")
road = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD)
for _ in gui_app.render():
road_camera_view.render(rl.Rectangle(gui_app.width // 4, 0, gui_app.width // 2, gui_app.height // 2))
driver_camera_view.render(rl.Rectangle(0, gui_app.height // 2, gui_app.width // 2, gui_app.height // 2))
wide_road_camera_view.render(rl.Rectangle(gui_app.width // 2, gui_app.height // 2, gui_app.width // 2, gui_app.height // 2))
finally:
road_camera_view.close()
driver_camera_view.close()
wide_road_camera_view.close()
road.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))

@ -50,7 +50,7 @@ class ExpButton(Widget):
center_y = int(self._rect.y + self._rect.height // 2)
mouse_over = rl.check_collision_point_rec(rl.get_mouse_position(), self._rect)
mouse_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._is_pressed
mouse_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self.is_pressed
self._white_color.a = 180 if (mouse_down and mouse_over) or not self._engageable else 255
texture = self._txt_exp if self._held_or_actual_mode() else self._txt_wheel

@ -34,7 +34,7 @@ class FontSizes:
@dataclass(frozen=True)
class Colors:
white: rl.Color = rl.Color(255, 255, 255, 255)
white: rl.Color = rl.WHITE
disengaged: rl.Color = rl.Color(145, 155, 149, 255)
override: rl.Color = rl.Color(145, 155, 149, 255) # Added
engaged: rl.Color = rl.Color(128, 216, 166, 255)
@ -47,7 +47,7 @@ class Colors:
white_translucent: rl.Color = rl.Color(255, 255, 255, 200)
border_translucent: rl.Color = rl.Color(255, 255, 255, 75)
header_gradient_start: rl.Color = rl.Color(0, 0, 0, 114)
header_gradient_end: rl.Color = rl.Color(0, 0, 0, 0)
header_gradient_end: rl.Color = rl.BLANK
UI_CONFIG = UIConfig()

@ -187,9 +187,9 @@ class ModelRenderer(Widget):
self._path.raw_points, 0.9, self._path_offset_z, max_idx, allow_invert=False
)
self._update_experimental_gradient(self._rect.height)
self._update_experimental_gradient()
def _update_experimental_gradient(self, height):
def _update_experimental_gradient(self):
"""Pre-calculate experimental mode gradient colors"""
if not self._experimental_mode:
return
@ -201,22 +201,21 @@ class ModelRenderer(Widget):
i = 0
while i < max_len:
track_idx = max_len - i - 1 # flip idx to start from bottom right
track_y = self._path.projected_points[track_idx][1]
if track_y < 0 or track_y > height:
# Some points (screen space) are out of frame (rect space)
track_y = self._path.projected_points[i][1]
if track_y < self._rect.y or track_y > (self._rect.y + self._rect.height):
i += 1
continue
# Calculate color based on acceleration
lin_grad_point = (height - track_y) / height
# Calculate color based on acceleration (0 is bottom, 1 is top)
lin_grad_point = 1 - (track_y - self._rect.y) / self._rect.height
# speed up: 120, slow down: 0
path_hue = max(min(60 + self._acceleration_x[i] * 35, 120), 0)
path_hue = int(path_hue * 100 + 0.5) / 100
path_hue = np.clip(60 + self._acceleration_x[i] * 35, 0, 120)
saturation = min(abs(self._acceleration_x[i] * 1.5), 1)
lightness = self._map_val(saturation, 0.0, 1.0, 0.95, 0.62)
alpha = self._map_val(lin_grad_point, 0.75 / 2.0, 0.75, 0.4, 0.0)
lightness = np.interp(saturation, [0.0, 1.0], [0.95, 0.62])
alpha = np.interp(lin_grad_point, [0.75 / 2.0, 0.75], [0.4, 0.0])
# Use HSL to RGB conversion
color = self._hsla_to_color(path_hue / 360.0, saturation, lightness, alpha)
@ -280,7 +279,7 @@ class ModelRenderer(Widget):
if self._experimental_mode:
# Draw with acceleration coloring
if len(self._exp_gradient['colors']) > 2:
if len(self._exp_gradient['colors']) > 1:
draw_polygon(self._rect, self._path.projected_points, gradient=self._exp_gradient)
else:
draw_polygon(self._rect, self._path.projected_points, rl.Color(255, 255, 255, 30))
@ -409,13 +408,6 @@ class ModelRenderer(Widget):
return np.vstack((left_screen.T, right_screen[:, ::-1].T)).astype(np.float32)
@staticmethod
def _map_val(x, x0, x1, y0, y1):
x = np.clip(x, x0, x1)
ra = x1 - x0
rb = y1 - y0
return (x - x0) * rb / ra + y0 if ra != 0 else y0
@staticmethod
def _hsla_to_color(h, s, l, a):
rgb = colorsys.hls_to_rgb(h, l, s)

@ -1,23 +0,0 @@
import os
import platform
from cffi import FFI
import sip
from openpilot.common.basedir import BASEDIR
def suffix():
return ".dylib" if platform.system() == "Darwin" else ".so"
def get_ffi():
lib = os.path.join(BASEDIR, "selfdrive", "ui", "qt", "libpython_helpers" + suffix())
ffi = FFI()
ffi.cdef("void set_main_window(void *w);")
return ffi, ffi.dlopen(lib)
def set_main_window(widget):
ffi, lib = get_ffi()
lib.set_main_window(ffi.cast('void*', sip.unwrapinstance(widget)))

@ -1,541 +0,0 @@
#include "selfdrive/ui/qt/setup/setup.h"
#include <cstdio>
#include <cstdlib>
#include <sstream>
#include <string>
#include <QApplication>
#include <QLabel>
#include <QVBoxLayout>
#include <curl/curl.h>
#include "common/util.h"
#include "system/hardware/hw.h"
#include "selfdrive/ui/qt/api.h"
#include "selfdrive/ui/qt/qt_window.h"
#include "selfdrive/ui/qt/network/networking.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/qt/widgets/input.h"
const std::string USER_AGENT = "AGNOSSetup-";
const QString OPENPILOT_URL = "https://openpilot.comma.ai";
bool is_elf(char *fname) {
FILE *fp = fopen(fname, "rb");
if (fp == NULL) {
return false;
}
char buf[4];
size_t n = fread(buf, 1, 4, fp);
fclose(fp);
return n == 4 && buf[0] == 0x7f && buf[1] == 'E' && buf[2] == 'L' && buf[3] == 'F';
}
void Setup::download(QString url) {
// autocomplete incomplete urls
if (QRegularExpression("^([^/.]+)/([^/]+)$").match(url).hasMatch()) {
url.prepend("https://installer.comma.ai/");
}
CURL *curl = curl_easy_init();
if (!curl) {
emit finished(url, tr("Something went wrong. Reboot the device."));
return;
}
auto version = util::read_file("/VERSION");
struct curl_slist *list = NULL;
std::string header = "X-openpilot-serial: " + Hardware::get_serial();
list = curl_slist_append(list, header.c_str());
char tmpfile[] = "/tmp/installer_XXXXXX";
FILE *fp = fdopen(mkstemp(tmpfile), "wb");
curl_easy_setopt(curl, CURLOPT_URL, url.toStdString().c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NULL);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, (USER_AGENT + version).c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
int ret = curl_easy_perform(curl);
long res_status = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &res_status);
if (ret != CURLE_OK || res_status != 200) {
emit finished(url, tr("Ensure the entered URL is valid, and the device’s internet connection is good."));
} else if (!is_elf(tmpfile)) {
emit finished(url, tr("No custom software found at this URL."));
} else {
rename(tmpfile, "/tmp/installer");
FILE *fp_url = fopen("/tmp/installer_url", "w");
fprintf(fp_url, "%s", url.toStdString().c_str());
fclose(fp_url);
emit finished(url);
}
curl_slist_free_all(list);
curl_easy_cleanup(curl);
fclose(fp);
}
QWidget * Setup::low_voltage() {
QWidget *widget = new QWidget();
QVBoxLayout *main_layout = new QVBoxLayout(widget);
main_layout->setContentsMargins(55, 0, 55, 55);
main_layout->setSpacing(0);
// inner text layout: warning icon, title, and body
QVBoxLayout *inner_layout = new QVBoxLayout();
inner_layout->setContentsMargins(110, 144, 365, 0);
main_layout->addLayout(inner_layout);
QLabel *triangle = new QLabel();
triangle->setPixmap(QPixmap(ASSET_PATH + "icons/warning.png"));
inner_layout->addWidget(triangle, 0, Qt::AlignTop | Qt::AlignLeft);
inner_layout->addSpacing(80);
QLabel *title = new QLabel(tr("WARNING: Low Voltage"));
title->setStyleSheet("font-size: 90px; font-weight: 500; color: #FF594F;");
inner_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft);
inner_layout->addSpacing(25);
QLabel *body = new QLabel(tr("Power your device in a car with a harness or proceed at your own risk."));
body->setWordWrap(true);
body->setAlignment(Qt::AlignTop | Qt::AlignLeft);
body->setStyleSheet("font-size: 80px; font-weight: 300;");
inner_layout->addWidget(body);
inner_layout->addStretch();
// power off + continue buttons
QHBoxLayout *blayout = new QHBoxLayout();
blayout->setSpacing(50);
main_layout->addLayout(blayout, 0);
QPushButton *poweroff = new QPushButton(tr("Power off"));
poweroff->setObjectName("navBtn");
blayout->addWidget(poweroff);
QObject::connect(poweroff, &QPushButton::clicked, this, [=]() {
Hardware::poweroff();
});
QPushButton *cont = new QPushButton(tr("Continue"));
cont->setObjectName("navBtn");
blayout->addWidget(cont);
QObject::connect(cont, &QPushButton::clicked, this, &Setup::nextPage);
return widget;
}
QWidget * Setup::custom_software_warning() {
QWidget *widget = new QWidget();
QVBoxLayout *main_layout = new QVBoxLayout(widget);
main_layout->setContentsMargins(55, 0, 55, 55);
main_layout->setSpacing(0);
QVBoxLayout *inner_layout = new QVBoxLayout();
inner_layout->setContentsMargins(110, 110, 300, 0);
main_layout->addLayout(inner_layout);
QLabel *title = new QLabel(tr("WARNING: Custom Software"));
title->setStyleSheet("font-size: 90px; font-weight: 500; color: #FF594F;");
inner_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft);
inner_layout->addSpacing(25);
QLabel *body = new QLabel(tr("Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle.\n\nIf you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later."));
body->setWordWrap(true);
body->setAlignment(Qt::AlignTop | Qt::AlignLeft);
body->setStyleSheet("font-size: 65px; font-weight: 300;");
inner_layout->addWidget(body);
inner_layout->addStretch();
QHBoxLayout *blayout = new QHBoxLayout();
blayout->setSpacing(50);
main_layout->addLayout(blayout, 0);
QPushButton *back = new QPushButton(tr("Back"));
back->setObjectName("navBtn");
blayout->addWidget(back);
QObject::connect(back, &QPushButton::clicked, this, &Setup::prevPage);
QPushButton *cont = new QPushButton(tr("Continue"));
cont->setObjectName("navBtn");
blayout->addWidget(cont);
QObject::connect(cont, &QPushButton::clicked, this, [=]() {
QTimer::singleShot(0, [=]() {
setCurrentWidget(downloading_widget);
});
QString url = InputDialog::getText(tr("Enter URL"), this, tr("for Custom Software"));
if (!url.isEmpty()) {
QTimer::singleShot(1000, this, [=]() {
download(url);
});
} else {
setCurrentWidget(software_selection_widget);
}
});
return widget;
}
QWidget * Setup::getting_started() {
QWidget *widget = new QWidget();
QHBoxLayout *main_layout = new QHBoxLayout(widget);
main_layout->setMargin(0);
QVBoxLayout *vlayout = new QVBoxLayout();
vlayout->setContentsMargins(165, 280, 100, 0);
main_layout->addLayout(vlayout);
QLabel *title = new QLabel(tr("Getting Started"));
title->setStyleSheet("font-size: 90px; font-weight: 500;");
vlayout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft);
vlayout->addSpacing(90);
QLabel *desc = new QLabel(tr("Before we get on the road, let’s finish installation and cover some details."));
desc->setWordWrap(true);
desc->setStyleSheet("font-size: 80px; font-weight: 300;");
vlayout->addWidget(desc, 0, Qt::AlignTop | Qt::AlignLeft);
vlayout->addStretch();
QPushButton *btn = new QPushButton();
btn->setIcon(QIcon(":/images/button_continue_triangle.svg"));
btn->setIconSize(QSize(54, 106));
btn->setFixedSize(310, 1080);
btn->setProperty("primary", true);
btn->setStyleSheet("border: none;");
main_layout->addWidget(btn, 0, Qt::AlignRight);
QObject::connect(btn, &QPushButton::clicked, this, &Setup::nextPage);
return widget;
}
QWidget * Setup::network_setup() {
QWidget *widget = new QWidget();
QVBoxLayout *main_layout = new QVBoxLayout(widget);
main_layout->setContentsMargins(55, 50, 55, 50);
// title
QLabel *title = new QLabel(tr("Connect to Wi-Fi"));
title->setStyleSheet("font-size: 90px; font-weight: 500;");
main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop);
main_layout->addSpacing(25);
// wifi widget
Networking *networking = new Networking(this, false);
networking->setStyleSheet("Networking {background-color: #292929; border-radius: 13px;}");
main_layout->addWidget(networking, 1);
main_layout->addSpacing(35);
// back + continue buttons
QHBoxLayout *blayout = new QHBoxLayout;
main_layout->addLayout(blayout);
blayout->setSpacing(50);
QPushButton *back = new QPushButton(tr("Back"));
back->setObjectName("navBtn");
QObject::connect(back, &QPushButton::clicked, this, &Setup::prevPage);
blayout->addWidget(back);
QPushButton *cont = new QPushButton();
cont->setObjectName("navBtn");
cont->setProperty("primary", true);
cont->setEnabled(false);
QObject::connect(cont, &QPushButton::clicked, this, &Setup::nextPage);
blayout->addWidget(cont);
// setup timer for testing internet connection
HttpRequest *request = new HttpRequest(this, false, 2500);
QObject::connect(request, &HttpRequest::requestDone, [=](const QString &, bool success) {
cont->setEnabled(success);
if (success) {
const bool wifi = networking->wifi->currentNetworkType() == NetworkType::WIFI;
cont->setText(wifi ? tr("Continue") : tr("Continue without Wi-Fi"));
} else {
cont->setText(tr("Waiting for internet"));
}
repaint();
});
request->sendRequest(OPENPILOT_URL);
QTimer *timer = new QTimer(this);
QObject::connect(timer, &QTimer::timeout, [=]() {
if (!request->active() && cont->isVisible()) {
request->sendRequest(OPENPILOT_URL);
}
});
timer->start(1000);
return widget;
}
QWidget * radio_button(QString title, QButtonGroup *group) {
QPushButton *btn = new QPushButton(title);
btn->setCheckable(true);
group->addButton(btn);
btn->setStyleSheet(R"(
QPushButton {
height: 230;
padding-left: 100px;
padding-right: 100px;
text-align: left;
font-size: 80px;
font-weight: 400;
border-radius: 10px;
background-color: #4F4F4F;
}
QPushButton:checked {
background-color: #465BEA;
}
)");
// checkmark icon
QPixmap pix(":/icons/circled_check.svg");
btn->setIcon(pix);
btn->setIconSize(QSize(0, 0));
btn->setLayoutDirection(Qt::RightToLeft);
QObject::connect(btn, &QPushButton::toggled, [=](bool checked) {
btn->setIconSize(checked ? QSize(104, 104) : QSize(0, 0));
});
return btn;
}
QWidget * Setup::software_selection() {
QWidget *widget = new QWidget();
QVBoxLayout *main_layout = new QVBoxLayout(widget);
main_layout->setContentsMargins(55, 50, 55, 50);
main_layout->setSpacing(0);
// title
QLabel *title = new QLabel(tr("Choose Software to Install"));
title->setStyleSheet("font-size: 90px; font-weight: 500;");
main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop);
main_layout->addSpacing(50);
// openpilot + custom radio buttons
QButtonGroup *group = new QButtonGroup(widget);
group->setExclusive(true);
QWidget *openpilot = radio_button(tr("openpilot"), group);
main_layout->addWidget(openpilot);
main_layout->addSpacing(30);
QWidget *custom = radio_button(tr("Custom Software"), group);
main_layout->addWidget(custom);
main_layout->addStretch();
// back + continue buttons
QHBoxLayout *blayout = new QHBoxLayout;
main_layout->addLayout(blayout);
blayout->setSpacing(50);
QPushButton *back = new QPushButton(tr("Back"));
back->setObjectName("navBtn");
QObject::connect(back, &QPushButton::clicked, this, &Setup::prevPage);
blayout->addWidget(back);
QPushButton *cont = new QPushButton(tr("Continue"));
cont->setObjectName("navBtn");
cont->setEnabled(false);
cont->setProperty("primary", true);
blayout->addWidget(cont);
QObject::connect(cont, &QPushButton::clicked, [=]() {
if (group->checkedButton() != openpilot) {
QTimer::singleShot(0, [=]() {
setCurrentWidget(custom_software_warning_widget);
});
} else {
QTimer::singleShot(0, [=]() {
setCurrentWidget(downloading_widget);
});
QTimer::singleShot(1000, this, [=]() {
download(OPENPILOT_URL);
});
}
});
connect(group, QOverload<QAbstractButton *>::of(&QButtonGroup::buttonClicked), [=](QAbstractButton *btn) {
btn->setChecked(true);
cont->setEnabled(true);
});
return widget;
}
QWidget * Setup::downloading() {
QWidget *widget = new QWidget();
QVBoxLayout *main_layout = new QVBoxLayout(widget);
QLabel *txt = new QLabel(tr("Downloading..."));
txt->setStyleSheet("font-size: 90px; font-weight: 500;");
main_layout->addWidget(txt, 0, Qt::AlignCenter);
return widget;
}
QWidget * Setup::download_failed(QLabel *url, QLabel *body) {
QWidget *widget = new QWidget();
QVBoxLayout *main_layout = new QVBoxLayout(widget);
main_layout->setContentsMargins(55, 185, 55, 55);
main_layout->setSpacing(0);
QLabel *title = new QLabel(tr("Download Failed"));
title->setStyleSheet("font-size: 90px; font-weight: 500;");
main_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft);
main_layout->addSpacing(67);
url->setWordWrap(true);
url->setAlignment(Qt::AlignTop | Qt::AlignLeft);
url->setStyleSheet("font-family: \"JetBrains Mono\"; font-size: 64px; font-weight: 400; margin-right: 100px;");
main_layout->addWidget(url);
main_layout->addSpacing(48);
body->setWordWrap(true);
body->setAlignment(Qt::AlignTop | Qt::AlignLeft);
body->setStyleSheet("font-size: 80px; font-weight: 300; margin-right: 100px;");
main_layout->addWidget(body);
main_layout->addStretch();
// reboot + start over buttons
QHBoxLayout *blayout = new QHBoxLayout();
blayout->setSpacing(50);
main_layout->addLayout(blayout, 0);
QPushButton *reboot = new QPushButton(tr("Reboot device"));
reboot->setObjectName("navBtn");
blayout->addWidget(reboot);
QObject::connect(reboot, &QPushButton::clicked, this, [=]() {
Hardware::reboot();
});
QPushButton *restart = new QPushButton(tr("Start over"));
restart->setObjectName("navBtn");
restart->setProperty("primary", true);
blayout->addWidget(restart);
QObject::connect(restart, &QPushButton::clicked, this, [=]() {
setCurrentIndex(1);
});
widget->setStyleSheet(R"(
QLabel {
margin-left: 117;
}
)");
return widget;
}
void Setup::prevPage() {
setCurrentIndex(currentIndex() - 1);
}
void Setup::nextPage() {
setCurrentIndex(currentIndex() + 1);
}
Setup::Setup(QWidget *parent) : QStackedWidget(parent) {
if (std::getenv("MULTILANG")) {
selectLanguage();
}
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) {
addWidget(low_voltage());
}
addWidget(getting_started());
addWidget(network_setup());
software_selection_widget = software_selection();
addWidget(software_selection_widget);
custom_software_warning_widget = custom_software_warning();
addWidget(custom_software_warning_widget);
downloading_widget = downloading();
addWidget(downloading_widget);
QLabel *url_label = new QLabel();
QLabel *body_label = new QLabel();
failed_widget = download_failed(url_label, body_label);
addWidget(failed_widget);
QObject::connect(this, &Setup::finished, [=](const QString &url, const QString &error) {
qDebug() << "finished" << url << error;
if (error.isEmpty()) {
// hide setup on success
QTimer::singleShot(3000, this, &QWidget::hide);
} else {
url_label->setText(url);
body_label->setText(error);
setCurrentWidget(failed_widget);
}
});
// TODO: revisit pressed bg color
setStyleSheet(R"(
* {
color: white;
font-family: Inter;
}
Setup {
background-color: black;
}
QPushButton#navBtn {
height: 160;
font-size: 55px;
font-weight: 400;
border-radius: 10px;
background-color: #333333;
}
QPushButton#navBtn:disabled, QPushButton[primary='true']:disabled {
color: #808080;
background-color: #333333;
}
QPushButton#navBtn:pressed {
background-color: #444444;
}
QPushButton[primary='true'], #navBtn[primary='true'] {
background-color: #465BEA;
}
QPushButton[primary='true']:pressed, #navBtn:pressed[primary='true'] {
background-color: #3049F4;
}
)");
}
void Setup::selectLanguage() {
QMap<QString, QString> langs = getSupportedLanguages();
QString selection = MultiOptionDialog::getSelection(tr("Select a language"), langs.keys(), "", this);
if (!selection.isEmpty()) {
QString selectedLang = langs[selection];
Params().put("LanguageSetting", selectedLang.toStdString());
if (translator.load(":/" + selectedLang)) {
qApp->installTranslator(&translator);
}
}
}
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
Setup setup;
setMainWindow(&setup);
return a.exec();
}

@ -1,38 +0,0 @@
#pragma once
#include <QLabel>
#include <QStackedWidget>
#include <QString>
#include <QTranslator>
#include <QWidget>
class Setup : public QStackedWidget {
Q_OBJECT
public:
explicit Setup(QWidget *parent = 0);
private:
void selectLanguage();
QWidget *low_voltage();
QWidget *custom_software_warning();
QWidget *getting_started();
QWidget *network_setup();
QWidget *software_selection();
QWidget *downloading();
QWidget *download_failed(QLabel *url, QLabel *body);
QWidget *failed_widget;
QWidget *downloading_widget;
QWidget *custom_software_warning_widget;
QWidget *software_selection_widget;
QTranslator translator;
signals:
void finished(const QString &url, const QString &error = "");
public slots:
void nextPage();
void prevPage();
void download(QString url);
};

@ -29,6 +29,7 @@ Sidebar::Sidebar(QWidget *parent) : QFrame(parent), onroad(false), flag_pressed(
flag_img = loadPixmap("../assets/images/button_flag.png", home_btn.size());
settings_img = loadPixmap("../assets/images/button_settings.png", settings_btn.size(), Qt::IgnoreAspectRatio);
mic_img = loadPixmap("../assets/icons/microphone.png", QSize(30, 30));
link_img = loadPixmap("../assets/icons/link.png", QSize(60, 60));
connect(this, &Sidebar::valueChanged, [=] { update(); });
@ -38,7 +39,7 @@ Sidebar::Sidebar(QWidget *parent) : QFrame(parent), onroad(false), flag_pressed(
QObject::connect(uiState(), &UIState::uiUpdate, this, &Sidebar::updateState);
pm = std::make_unique<PubMaster>(std::vector<const char*>{"userFlag"});
pm = std::make_unique<PubMaster>(std::vector<const char*>{"bookmarkButton"});
}
void Sidebar::mousePressEvent(QMouseEvent *event) {
@ -61,8 +62,8 @@ void Sidebar::mouseReleaseEvent(QMouseEvent *event) {
}
if (onroad && home_btn.contains(event->pos())) {
MessageBuilder msg;
msg.initEvent().initUserFlag();
pm->send("userFlag", msg);
msg.initEvent().initBookmarkButton();
pm->send("bookmarkButton", msg);
} else if (settings_btn.contains(event->pos())) {
emit openSettings();
} else if (recording_audio && mic_indicator_btn.contains(event->pos())) {
@ -150,7 +151,12 @@ void Sidebar::paintEvent(QPaintEvent *event) {
p.setFont(InterFont(35));
p.setPen(QColor(0xff, 0xff, 0xff));
const QRect r = QRect(58, 247, width() - 100, 50);
if (net_type == "Hotspot") {
p.drawPixmap(r.x(), r.y() + (r.height() - link_img.height()) / 2, link_img);
} else {
p.drawText(r, Qt::AlignLeft | Qt::AlignVCenter, net_type);
}
// metrics
drawMetric(p, temp_status.first, temp_status.second, 338);

@ -37,7 +37,7 @@ protected:
void mouseReleaseEvent(QMouseEvent *event) override;
void drawMetric(QPainter &p, const QPair<QString, QString> &label, QColor c, int y);
QPixmap home_img, flag_img, settings_img, mic_img;
QPixmap home_img, flag_img, settings_img, mic_img, link_img;
bool onroad, recording_audio, flag_pressed, settings_pressed, mic_indicator_pressed;
const QMap<cereal::DeviceState::NetworkType, QString> network_type = {
{cereal::DeviceState::NetworkType::NONE, tr("--")},

@ -0,0 +1,53 @@
import pytest
import cereal.messaging as messaging
from cereal import car
from openpilot.common.params import Params
from openpilot.system.manager.process_config import managed_processes
@pytest.mark.skip("tmp disabled")
class TestFeedbackd:
def setup_method(self):
self.pm = messaging.PubMaster(['carState', 'rawAudioData'])
self.sm = messaging.SubMaster(['audioFeedback'])
def _send_lkas_button(self, pressed: bool):
msg = messaging.new_message('carState')
msg.carState.canValid = True
msg.carState.buttonEvents = [{'type': car.CarState.ButtonEvent.Type.lkas, 'pressed': pressed}]
self.pm.send('carState', msg)
def _send_audio_data(self, count: int = 5):
for _ in range(count):
audio_msg = messaging.new_message('rawAudioData')
audio_msg.rawAudioData.data = bytes(1600) # 800 samples of int16
audio_msg.rawAudioData.sampleRate = 16000
self.pm.send('rawAudioData', audio_msg)
self.sm.update(timeout=100)
@pytest.mark.parametrize("record_feedback", [False, True])
def test_audio_feedback(self, record_feedback):
Params().put_bool("RecordAudioFeedback", record_feedback)
managed_processes["feedbackd"].start()
assert self.pm.wait_for_readers_to_update('carState', timeout=5)
assert self.pm.wait_for_readers_to_update('rawAudioData', timeout=5)
self._send_lkas_button(pressed=True)
self._send_audio_data()
self._send_lkas_button(pressed=False)
self._send_audio_data()
if record_feedback:
assert self.sm.updated['audioFeedback'], "audioFeedback should be published when enabled"
else:
assert not self.sm.updated['audioFeedback'], "audioFeedback should not be published when disabled"
self._send_lkas_button(pressed=True)
self._send_audio_data()
self._send_lkas_button(pressed=False)
self._send_audio_data()
assert not self.sm.updated['audioFeedback'], "audioFeedback should not be published after second press"
managed_processes["feedbackd"].stop()

@ -0,0 +1,8 @@
import time
from openpilot.selfdrive.test.helpers import with_processes
@with_processes(["raylib_ui"])
def test_raylib_ui():
"""Test initialization of the UI widgets is successful."""
time.sleep(1)

@ -507,7 +507,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<translation type="unfinished">تأخير التحديث</translation>
</message>
<message>
<source>openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support.</source>
<source>openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device&apos;s Dongle ID for troubleshooting.</source>
<translation type="unfinished"></translation>
</message>
</context>
@ -700,111 +700,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<translation>خرطوم الحريق</translation>
</message>
</context>
<context>
<name>Setup</name>
<message>
<source>WARNING: Low Voltage</source>
<translation>تحذير: الجهد منخفض</translation>
</message>
<message>
<source>Power your device in a car with a harness or proceed at your own risk.</source>
<translation>شغل جهازك في السيارة عن طريق شرطان التوصيل، أو تابع على مسؤوليتك.</translation>
</message>
<message>
<source>Power off</source>
<translation>إيقاف التشغيل</translation>
</message>
<message>
<source>Continue</source>
<translation>متابعة</translation>
</message>
<message>
<source>Getting Started</source>
<translation>البدء</translation>
</message>
<message>
<source>Before we get on the road, lets finish installation and cover some details.</source>
<translation>قبل أن ننطلق في الطريق، دعنا ننتهي من التثبيت ونغطي بعض التفاصيل.</translation>
</message>
<message>
<source>Connect to Wi-Fi</source>
<translation>الاتصال بشبكة الواي فاي</translation>
</message>
<message>
<source>Back</source>
<translation>السابق</translation>
</message>
<message>
<source>Continue without Wi-Fi</source>
<translation>المتابعة بدون شبكة الواي فاي</translation>
</message>
<message>
<source>Waiting for internet</source>
<translation>بانتظار الاتصال بالإنترنت</translation>
</message>
<message>
<source>Enter URL</source>
<translation>أدخل رابط URL</translation>
</message>
<message>
<source>for Custom Software</source>
<translation>للبرامج المخصصة</translation>
</message>
<message>
<source>Downloading...</source>
<translation>يتم الآن التنزيل...</translation>
</message>
<message>
<source>Download Failed</source>
<translation>فشل التنزيل</translation>
</message>
<message>
<source>Ensure the entered URL is valid, and the devices internet connection is good.</source>
<translation>تأكد من أن رابط URL الذي أدخلته صالح، وأن اتصال الجهاز بالإنترنت جيد.</translation>
</message>
<message>
<source>Reboot device</source>
<translation>إعادة التشغيل</translation>
</message>
<message>
<source>Start over</source>
<translation>البدء من جديد</translation>
</message>
<message>
<source>Something went wrong. Reboot the device.</source>
<translation>حدث خطأ ما. أعد التشغيل الجهاز.</translation>
</message>
<message>
<source>No custom software found at this URL.</source>
<translation>لم يتم العثور على برنامج خاص لعنوان URL ها.</translation>
</message>
<message>
<source>Select a language</source>
<translation>اختر لغة</translation>
</message>
<message>
<source>Choose Software to Install</source>
<translation>اختر البرنامج للتثبيت</translation>
</message>
<message>
<source>openpilot</source>
<translation>openpilot</translation>
</message>
<message>
<source>Custom Software</source>
<translation>البرمجيات المخصصة</translation>
</message>
<message>
<source>WARNING: Custom Software</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle.
If you&apos;d like to proceed, use https://flash.comma.ai to restore your device to a factory state later.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SetupWidget</name>
<message>
@ -1157,6 +1052,16 @@ If you&apos;d like to proceed, use https://flash.comma.ai to restore your device
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Record Audio Feedback with LKAS button</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device&apos;s storage.
Note that this feature is only compatible with select cars.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>WiFiPromptWidget</name>

@ -499,7 +499,7 @@ Der Firehose-Modus ermöglicht es dir, deine Trainingsdaten-Uploads zu maximiere
<translation type="unfinished">Update pausieren</translation>
</message>
<message>
<source>openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support.</source>
<source>openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device&apos;s Dongle ID for troubleshooting.</source>
<translation type="unfinished"></translation>
</message>
</context>
@ -680,111 +680,6 @@ Der Firehose-Modus ermöglicht es dir, deine Trainingsdaten-Uploads zu maximiere
<translation>Firehose</translation>
</message>
</context>
<context>
<name>Setup</name>
<message>
<source>WARNING: Low Voltage</source>
<translation>Warnung: Batteriespannung niedrig</translation>
</message>
<message>
<source>Power your device in a car with a harness or proceed at your own risk.</source>
<translation>Versorge dein Gerät über einen Kabelbaum im Auto mit Strom, oder fahre auf eigene Gefahr fort.</translation>
</message>
<message>
<source>Power off</source>
<translation>Ausschalten</translation>
</message>
<message>
<source>Continue</source>
<translation>Fortsetzen</translation>
</message>
<message>
<source>Getting Started</source>
<translation>Loslegen</translation>
</message>
<message>
<source>Before we get on the road, lets finish installation and cover some details.</source>
<translation>Bevor wir uns auf die Straße begeben, lass uns die Installation fertigstellen und einige Details prüfen.</translation>
</message>
<message>
<source>Connect to Wi-Fi</source>
<translation>Mit WLAN verbinden</translation>
</message>
<message>
<source>Back</source>
<translation>Zurück</translation>
</message>
<message>
<source>Continue without Wi-Fi</source>
<translation>Ohne WLAN fortsetzen</translation>
</message>
<message>
<source>Waiting for internet</source>
<translation>Auf Internet warten</translation>
</message>
<message>
<source>Enter URL</source>
<translation>URL eingeben</translation>
</message>
<message>
<source>for Custom Software</source>
<translation>für spezifische Software</translation>
</message>
<message>
<source>Downloading...</source>
<translation>Herunterladen...</translation>
</message>
<message>
<source>Download Failed</source>
<translation>Herunterladen fehlgeschlagen</translation>
</message>
<message>
<source>Ensure the entered URL is valid, and the devices internet connection is good.</source>
<translation>Stelle sicher, dass die eingegebene URL korrekt ist und dein Gerät eine stabile Internetverbindung hat.</translation>
</message>
<message>
<source>Reboot device</source>
<translation>Gerät neustarten</translation>
</message>
<message>
<source>Start over</source>
<translation>Von neuem beginnen</translation>
</message>
<message>
<source>No custom software found at this URL.</source>
<translation>Keine benutzerdefinierte Software unter dieser URL gefunden.</translation>
</message>
<message>
<source>Something went wrong. Reboot the device.</source>
<translation>Etwas ist schiefgelaufen. Starte das Gerät neu.</translation>
</message>
<message>
<source>Select a language</source>
<translation>Sprache wählen</translation>
</message>
<message>
<source>Choose Software to Install</source>
<translation>Wähle die zu installierende Software</translation>
</message>
<message>
<source>openpilot</source>
<translation>openpilot</translation>
</message>
<message>
<source>Custom Software</source>
<translation>Benutzerdefinierte Software</translation>
</message>
<message>
<source>WARNING: Custom Software</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle.
If you&apos;d like to proceed, use https://flash.comma.ai to restore your device to a factory state later.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SetupWidget</name>
<message>
@ -1139,6 +1034,16 @@ If you&apos;d like to proceed, use https://flash.comma.ai to restore your device
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Record Audio Feedback with LKAS button</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device&apos;s storage.
Note that this feature is only compatible with select cars.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>WiFiPromptWidget</name>

@ -503,7 +503,7 @@ El Modo Firehose te permite maximizar las subidas de datos de entrenamiento para
<translation type="unfinished">Posponer Actualización</translation>
</message>
<message>
<source>openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support.</source>
<source>openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device&apos;s Dongle ID for troubleshooting.</source>
<translation type="unfinished"></translation>
</message>
</context>
@ -684,113 +684,6 @@ El Modo Firehose te permite maximizar las subidas de datos de entrenamiento para
<translation>Firehose</translation>
</message>
</context>
<context>
<name>Setup</name>
<message>
<source>Something went wrong. Reboot the device.</source>
<translation>Algo ha ido mal. Reinicie el dispositivo.</translation>
</message>
<message>
<source>Ensure the entered URL is valid, and the devices internet connection is good.</source>
<translation>Asegúrese de que la URL insertada es válida y que el dispositivo tiene buena conexión.</translation>
</message>
<message>
<source>No custom software found at this URL.</source>
<translation>No encontramos software personalizado en esta URL.</translation>
</message>
<message>
<source>WARNING: Low Voltage</source>
<translation>ALERTA: Voltaje bajo</translation>
</message>
<message>
<source>Power your device in a car with a harness or proceed at your own risk.</source>
<translation>Encienda su dispositivo en un auto con el arnés o proceda bajo su propio riesgo.</translation>
</message>
<message>
<source>Power off</source>
<translation>Apagar</translation>
</message>
<message>
<source>Continue</source>
<translation>Continuar</translation>
</message>
<message>
<source>Getting Started</source>
<translation>Comenzando</translation>
</message>
<message>
<source>Before we get on the road, lets finish installation and cover some details.</source>
<translation>Antes de ponernos en marcha, terminemos la instalación y cubramos algunos detalles.</translation>
</message>
<message>
<source>Connect to Wi-Fi</source>
<translation>Conectarse al Wi-Fi</translation>
</message>
<message>
<source>Back</source>
<translation>Volver</translation>
</message>
<message>
<source>Continue without Wi-Fi</source>
<translation>Continuar sin Wi-Fi</translation>
</message>
<message>
<source>Waiting for internet</source>
<translation>Esperando conexión a internet</translation>
</message>
<message>
<source>Choose Software to Install</source>
<translation>Elija el software a instalar</translation>
</message>
<message>
<source>openpilot</source>
<translation>openpilot</translation>
</message>
<message>
<source>Custom Software</source>
<translation>Software personalizado</translation>
</message>
<message>
<source>Enter URL</source>
<translation>Insertar URL</translation>
</message>
<message>
<source>for Custom Software</source>
<translation>para Software personalizado</translation>
</message>
<message>
<source>Downloading...</source>
<translation>Descargando...</translation>
</message>
<message>
<source>Download Failed</source>
<translation>Descarga fallida</translation>
</message>
<message>
<source>Reboot device</source>
<translation>Reiniciar Dispositivo</translation>
</message>
<message>
<source>Start over</source>
<translation>Comenzar de nuevo</translation>
</message>
<message>
<source>Select a language</source>
<translation>Seleccione un idioma</translation>
</message>
<message>
<source>WARNING: Custom Software</source>
<translation>ADVERTENCIA: Software personalizado</translation>
</message>
<message>
<source>Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle.
If you&apos;d like to proceed, use https://flash.comma.ai to restore your device to a factory state later.</source>
<translation>Tenga cuidado al instalar software de terceros. El software de terceros no ha sido probado por comma y puede causar daños a su dispositivo y/o vehículo.
Si desea continuar, utilice https://flash.comma.ai para restaurar su dispositivo a un estado de fábrica más tarde.</translation>
</message>
</context>
<context>
<name>SetupWidget</name>
<message>
@ -1143,6 +1036,16 @@ Si desea continuar, utilice https://flash.comma.ai para restaurar su dispositivo
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
<translation>Graba y almacena el audio del micrófono mientras conduces. El audio se incluirá en el video de la cámara del tablero en comma connect.</translation>
</message>
<message>
<source>Record Audio Feedback with LKAS button</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device&apos;s storage.
Note that this feature is only compatible with select cars.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>WiFiPromptWidget</name>

@ -497,7 +497,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<translation type="unfinished">Reporter la mise à jour</translation>
</message>
<message>
<source>openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support.</source>
<source>openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device&apos;s Dongle ID for troubleshooting.</source>
<translation type="unfinished"></translation>
</message>
</context>
@ -678,111 +678,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Setup</name>
<message>
<source>Something went wrong. Reboot the device.</source>
<translation>Un problème est survenu. Redémarrez l&apos;appareil.</translation>
</message>
<message>
<source>Ensure the entered URL is valid, and the devices internet connection is good.</source>
<translation>Assurez-vous que l&apos;URL saisie est valide et que la connexion internet de l&apos;appareil est bonne.</translation>
</message>
<message>
<source>No custom software found at this URL.</source>
<translation>Aucun logiciel personnalisé trouvé à cette URL.</translation>
</message>
<message>
<source>WARNING: Low Voltage</source>
<translation>ATTENTION : Tension faible</translation>
</message>
<message>
<source>Power your device in a car with a harness or proceed at your own risk.</source>
<translation>Alimentez votre appareil dans une voiture avec un harness ou continuez à vos risques et périls.</translation>
</message>
<message>
<source>Power off</source>
<translation>Éteindre</translation>
</message>
<message>
<source>Continue</source>
<translation>Continuer</translation>
</message>
<message>
<source>Getting Started</source>
<translation>Commencer</translation>
</message>
<message>
<source>Before we get on the road, lets finish installation and cover some details.</source>
<translation>Avant de prendre la route, terminons l&apos;installation et passons en revue quelques détails.</translation>
</message>
<message>
<source>Connect to Wi-Fi</source>
<translation>Se connecter au Wi-Fi</translation>
</message>
<message>
<source>Back</source>
<translation>Retour</translation>
</message>
<message>
<source>Enter URL</source>
<translation>Entrer l&apos;URL</translation>
</message>
<message>
<source>for Custom Software</source>
<translation>pour logiciel personnalisé</translation>
</message>
<message>
<source>Continue without Wi-Fi</source>
<translation>Continuer sans Wi-Fi</translation>
</message>
<message>
<source>Waiting for internet</source>
<translation>En attente d&apos;internet</translation>
</message>
<message>
<source>Downloading...</source>
<translation>Téléchargement...</translation>
</message>
<message>
<source>Download Failed</source>
<translation>Échec du téléchargement</translation>
</message>
<message>
<source>Reboot device</source>
<translation>Redémarrer l&apos;appareil</translation>
</message>
<message>
<source>Start over</source>
<translation>Recommencer</translation>
</message>
<message>
<source>Select a language</source>
<translation>Choisir une langue</translation>
</message>
<message>
<source>Choose Software to Install</source>
<translation>Choisir le logiciel à installer</translation>
</message>
<message>
<source>openpilot</source>
<translation>openpilot</translation>
</message>
<message>
<source>Custom Software</source>
<translation>Logiciel personnalisé</translation>
</message>
<message>
<source>WARNING: Custom Software</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle.
If you&apos;d like to proceed, use https://flash.comma.ai to restore your device to a factory state later.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SetupWidget</name>
<message>
@ -1135,6 +1030,16 @@ If you&apos;d like to proceed, use https://flash.comma.ai to restore your device
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Record Audio Feedback with LKAS button</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device&apos;s storage.
Note that this feature is only compatible with select cars.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>WiFiPromptWidget</name>

@ -501,8 +501,8 @@ Firehoseモードを有効にすると学習データを最大限アップロー
<translation></translation>
</message>
<message>
<source>openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support.</source>
<translation>openpilotが過剰な%1https://comma.ai/support からサポートへご連絡下さい。</translation>
<source>openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device&apos;s Dongle ID for troubleshooting.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
@ -679,113 +679,6 @@ Firehoseモードを有効にすると学習データを最大限アップロー
<translation></translation>
</message>
</context>
<context>
<name>Setup</name>
<message>
<source>WARNING: Low Voltage</source>
<translation></translation>
</message>
<message>
<source>Power your device in a car with a harness or proceed at your own risk.</source>
<translation>使</translation>
</message>
<message>
<source>Power off</source>
<translation></translation>
</message>
<message>
<source>Continue</source>
<translation></translation>
</message>
<message>
<source>Getting Started</source>
<translation></translation>
</message>
<message>
<source>Before we get on the road, lets finish installation and cover some details.</source>
<translation></translation>
</message>
<message>
<source>Connect to Wi-Fi</source>
<translation>Wi-Fiに接続</translation>
</message>
<message>
<source>Back</source>
<translation></translation>
</message>
<message>
<source>Continue without Wi-Fi</source>
<translation>Wi-Fiに接続せずに続行</translation>
</message>
<message>
<source>Waiting for internet</source>
<translation></translation>
</message>
<message>
<source>Enter URL</source>
<translation>URLの入力</translation>
</message>
<message>
<source>for Custom Software</source>
<translation></translation>
</message>
<message>
<source>Downloading...</source>
<translation>...</translation>
</message>
<message>
<source>Download Failed</source>
<translation></translation>
</message>
<message>
<source>Ensure the entered URL is valid, and the devices internet connection is good.</source>
<translation>URLが正しいかどうか</translation>
</message>
<message>
<source>Reboot device</source>
<translation></translation>
</message>
<message>
<source>Start over</source>
<translation></translation>
</message>
<message>
<source>No custom software found at this URL.</source>
<translation>URLはカスタムソフトウェアではありません</translation>
</message>
<message>
<source>Something went wrong. Reboot the device.</source>
<translation></translation>
</message>
<message>
<source>Select a language</source>
<translation></translation>
</message>
<message>
<source>Choose Software to Install</source>
<translation></translation>
</message>
<message>
<source>openpilot</source>
<translation>openpilot</translation>
</message>
<message>
<source>Custom Software</source>
<translation></translation>
</message>
<message>
<source>WARNING: Custom Software</source>
<translation>警告: カスタムソフトウェア</translation>
</message>
<message>
<source>Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle.
If you&apos;d like to proceed, use https://flash.comma.ai to restore your device to a factory state later.</source>
<translation>commaによってテストされておらず
https://flash.comma.ai を使用してください。</translation>
</message>
</context>
<context>
<name>SetupWidget</name>
<message>
@ -1138,6 +1031,16 @@ If you&apos;d like to proceed, use https://flash.comma.ai to restore your device
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
<translation> comma connect </translation>
</message>
<message>
<source>Record Audio Feedback with LKAS button</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device&apos;s storage.
Note that this feature is only compatible with select cars.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>WiFiPromptWidget</name>

@ -501,8 +501,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<translation> </translation>
</message>
<message>
<source>openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support.</source>
<translation> %1 . . https://comma.ai/support 에 문의하여 지원받으세요.</translation>
<source>openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device&apos;s Dongle ID for troubleshooting.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
@ -679,113 +679,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<translation></translation>
</message>
</context>
<context>
<name>Setup</name>
<message>
<source>WARNING: Low Voltage</source>
<translation>경고: 전압이 </translation>
</message>
<message>
<source>Power your device in a car with a harness or proceed at your own risk.</source>
<translation> . USB .</translation>
</message>
<message>
<source>Power off</source>
<translation> </translation>
</message>
<message>
<source>Continue</source>
<translation></translation>
</message>
<message>
<source>Getting Started</source>
<translation></translation>
</message>
<message>
<source>Before we get on the road, lets finish installation and cover some details.</source>
<translation> .</translation>
</message>
<message>
<source>Connect to Wi-Fi</source>
<translation>Wi-Fi </translation>
</message>
<message>
<source>Back</source>
<translation></translation>
</message>
<message>
<source>Continue without Wi-Fi</source>
<translation>Wi-Fi </translation>
</message>
<message>
<source>Waiting for internet</source>
<translation> </translation>
</message>
<message>
<source>Enter URL</source>
<translation>URL </translation>
</message>
<message>
<source>for Custom Software</source>
<translation> </translation>
</message>
<message>
<source>Downloading...</source>
<translation> ...</translation>
</message>
<message>
<source>Download Failed</source>
<translation> </translation>
</message>
<message>
<source>Ensure the entered URL is valid, and the devices internet connection is good.</source>
<translation> URL이 .</translation>
</message>
<message>
<source>Reboot device</source>
<translation> </translation>
</message>
<message>
<source>Start over</source>
<translation> </translation>
</message>
<message>
<source>Something went wrong. Reboot the device.</source>
<translation> . .</translation>
</message>
<message>
<source>No custom software found at this URL.</source>
<translation> URL에서 .</translation>
</message>
<message>
<source>Select a language</source>
<translation> </translation>
</message>
<message>
<source>Choose Software to Install</source>
<translation> </translation>
</message>
<message>
<source>openpilot</source>
<translation></translation>
</message>
<message>
<source>Custom Software</source>
<translation> </translation>
</message>
<message>
<source>WARNING: Custom Software</source>
<translation>경고: 커스텀 </translation>
</message>
<message>
<source>Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle.
If you&apos;d like to proceed, use https://flash.comma.ai to restore your device to a factory state later.</source>
<translation> . comma에 .
https://flash.comma.ai를 사용하여 나중에 장치를 공장 초기화하세요.</translation>
</message>
</context>
<context>
<name>SetupWidget</name>
<message>
@ -1138,6 +1031,16 @@ If you&apos;d like to proceed, use https://flash.comma.ai to restore your device
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
<translation> . comma connect의 .</translation>
</message>
<message>
<source>Record Audio Feedback with LKAS button</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device&apos;s storage.
Note that this feature is only compatible with select cars.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>WiFiPromptWidget</name>

@ -503,8 +503,8 @@ O Modo Firehose permite maximizar o envio de dados de treinamento para melhorar
<translation>Adiar Atualização</translation>
</message>
<message>
<source>openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support.</source>
<translation>O openpilot detectou uma atuação excessiva de %1. Isso pode ser causado por uma falha no software. Por favor, entre em contato com o suporte em https://comma.ai/support.</translation>
<source>openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device&apos;s Dongle ID for troubleshooting.</source>
<translation>openpilot detectou atuação excessiva de %1 na sua última condução. Entre em contato com o suporte em https://comma.ai/support e compartilhe o Dongle ID do seu dispositivo para solução de problemas.</translation>
</message>
</context>
<context>
@ -684,113 +684,6 @@ O Modo Firehose permite maximizar o envio de dados de treinamento para melhorar
<translation>Firehose</translation>
</message>
</context>
<context>
<name>Setup</name>
<message>
<source>WARNING: Low Voltage</source>
<translation>ALERTA: Baixa Voltagem</translation>
</message>
<message>
<source>Power your device in a car with a harness or proceed at your own risk.</source>
<translation>Ligue seu dispositivo em um carro com um chicote ou prossiga por sua conta e risco.</translation>
</message>
<message>
<source>Power off</source>
<translation>Desligar</translation>
</message>
<message>
<source>Continue</source>
<translation>Continuar</translation>
</message>
<message>
<source>Getting Started</source>
<translation>Começando</translation>
</message>
<message>
<source>Before we get on the road, lets finish installation and cover some details.</source>
<translation>Antes de pegarmos a estrada, vamos terminar a instalação e cobrir alguns detalhes.</translation>
</message>
<message>
<source>Connect to Wi-Fi</source>
<translation>Conectar ao Wi-Fi</translation>
</message>
<message>
<source>Back</source>
<translation>Voltar</translation>
</message>
<message>
<source>Continue without Wi-Fi</source>
<translation>Continuar sem Wi-Fi</translation>
</message>
<message>
<source>Waiting for internet</source>
<translation>Esperando pela internet</translation>
</message>
<message>
<source>Enter URL</source>
<translation>Preencher URL</translation>
</message>
<message>
<source>for Custom Software</source>
<translation>para o Software Customizado</translation>
</message>
<message>
<source>Downloading...</source>
<translation>Baixando...</translation>
</message>
<message>
<source>Download Failed</source>
<translation>Download Falhou</translation>
</message>
<message>
<source>Ensure the entered URL is valid, and the devices internet connection is good.</source>
<translation>Garanta que a URL inserida é valida, e uma boa conexão à internet.</translation>
</message>
<message>
<source>Reboot device</source>
<translation>Reiniciar Dispositivo</translation>
</message>
<message>
<source>Start over</source>
<translation>Inicializar</translation>
</message>
<message>
<source>No custom software found at this URL.</source>
<translation>Não software personalizado nesta URL.</translation>
</message>
<message>
<source>Something went wrong. Reboot the device.</source>
<translation>Algo deu errado. Reinicie o dispositivo.</translation>
</message>
<message>
<source>Select a language</source>
<translation>Selecione o Idioma</translation>
</message>
<message>
<source>Choose Software to Install</source>
<translation>Escolha o Software a ser Instalado</translation>
</message>
<message>
<source>openpilot</source>
<translation>openpilot</translation>
</message>
<message>
<source>Custom Software</source>
<translation>Software Customizado</translation>
</message>
<message>
<source>WARNING: Custom Software</source>
<translation>AVISO: Software Personalizado</translation>
</message>
<message>
<source>Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle.
If you&apos;d like to proceed, use https://flash.comma.ai to restore your device to a factory state later.</source>
<translation>Tenha cuidado ao instalar software de terceiros. Softwares de terceiros não foram testados pela comma e podem causar danos ao seu dispositivo e/ou veículo.
Se quiser continuar, use https://flash.comma.ai para restaurar seu dispositivo ao estado de fábrica mais tarde.</translation>
</message>
</context>
<context>
<name>SetupWidget</name>
<message>
@ -1143,6 +1036,18 @@ Se quiser continuar, use https://flash.comma.ai para restaurar seu dispositivo a
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
<translation>Grave e armazene o áudio do microfone enquanto estiver dirigindo. O áudio será incluído ao vídeo dashcam no comma connect.</translation>
</message>
<message>
<source>Record Audio Feedback with LKAS button</source>
<translation>Gravar feedback de áudio com o botão LKAS</translation>
</message>
<message>
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device&apos;s storage.
Note that this feature is only compatible with select cars.</source>
<translation>Pressione o botão LKAS para gravar e compartilhar feedback de direção com a equipe do openpilot. Quando esta opção estiver desativada, o botão funcionará como um botão de marcador. O evento será destacado no comma connect e o segmento será preservado no armazenamento do seu dispositivo.
Observe que este recurso é compatível apenas com alguns modelos de carros.</translation>
</message>
</context>
<context>
<name>WiFiPromptWidget</name>

@ -497,7 +497,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<translation type="unfinished"></translation>
</message>
<message>
<source>openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support.</source>
<source>openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device&apos;s Dongle ID for troubleshooting.</source>
<translation type="unfinished"></translation>
</message>
</context>
@ -675,111 +675,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<translation></translation>
</message>
</context>
<context>
<name>Setup</name>
<message>
<source>WARNING: Low Voltage</source>
<translation>คำเตอน: แรงดนแบตเตอร</translation>
</message>
<message>
<source>Power your device in a car with a harness or proceed at your own risk.</source>
<translation> </translation>
</message>
<message>
<source>Power off</source>
<translation></translation>
</message>
<message>
<source>Continue</source>
<translation></translation>
</message>
<message>
<source>Getting Started</source>
<translation></translation>
</message>
<message>
<source>Before we get on the road, lets finish installation and cover some details.</source>
<translation> </translation>
</message>
<message>
<source>Connect to Wi-Fi</source>
<translation> Wi-Fi</translation>
</message>
<message>
<source>Back</source>
<translation></translation>
</message>
<message>
<source>Continue without Wi-Fi</source>
<translation> Wi-Fi</translation>
</message>
<message>
<source>Waiting for internet</source>
<translation></translation>
</message>
<message>
<source>Enter URL</source>
<translation> URL</translation>
</message>
<message>
<source>for Custom Software</source>
<translation></translation>
</message>
<message>
<source>Downloading...</source>
<translation>...</translation>
</message>
<message>
<source>Download Failed</source>
<translation></translation>
</message>
<message>
<source>Ensure the entered URL is valid, and the devices internet connection is good.</source>
<translation> URL </translation>
</message>
<message>
<source>Reboot device</source>
<translation></translation>
</message>
<message>
<source>Start over</source>
<translation></translation>
</message>
<message>
<source>Something went wrong. Reboot the device.</source>
<translation> </translation>
</message>
<message>
<source>No custom software found at this URL.</source>
<translation> URL </translation>
</message>
<message>
<source>Select a language</source>
<translation></translation>
</message>
<message>
<source>Choose Software to Install</source>
<translation></translation>
</message>
<message>
<source>openpilot</source>
<translation>openpilot</translation>
</message>
<message>
<source>Custom Software</source>
<translation></translation>
</message>
<message>
<source>WARNING: Custom Software</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle.
If you&apos;d like to proceed, use https://flash.comma.ai to restore your device to a factory state later.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SetupWidget</name>
<message>
@ -1132,6 +1027,16 @@ If you&apos;d like to proceed, use https://flash.comma.ai to restore your device
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Record Audio Feedback with LKAS button</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device&apos;s storage.
Note that this feature is only compatible with select cars.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>WiFiPromptWidget</name>

@ -494,7 +494,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<translation type="unfinished">Güncellemeyi sessize al</translation>
</message>
<message>
<source>openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support.</source>
<source>openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device&apos;s Dongle ID for troubleshooting.</source>
<translation type="unfinished"></translation>
</message>
</context>
@ -672,111 +672,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Setup</name>
<message>
<source>WARNING: Low Voltage</source>
<translation>UYARI: Düşük voltaj</translation>
</message>
<message>
<source>Power your device in a car with a harness or proceed at your own risk.</source>
<translation>Cihazınızı emniyet kemeri olan bir arabada çalıştırın veya riski kabul ederek devam edin.</translation>
</message>
<message>
<source>Power off</source>
<translation>Sistemi kapat</translation>
</message>
<message>
<source>Continue</source>
<translation>Devam et</translation>
</message>
<message>
<source>Getting Started</source>
<translation>Başlarken</translation>
</message>
<message>
<source>Before we get on the road, lets finish installation and cover some details.</source>
<translation>Yola çıkmadan önce kurulumu bitirin ve bazı detayları gözden geçirin..</translation>
</message>
<message>
<source>Connect to Wi-Fi</source>
<translation>Wi-Fi ile bağlan</translation>
</message>
<message>
<source>Back</source>
<translation>Geri</translation>
</message>
<message>
<source>Continue without Wi-Fi</source>
<translation>Wi-Fi bağlantısı olmadan devam edin</translation>
</message>
<message>
<source>Waiting for internet</source>
<translation>İnternet bağlantısı bekleniyor.</translation>
</message>
<message>
<source>Enter URL</source>
<translation>URL girin</translation>
</message>
<message>
<source>for Custom Software</source>
<translation>özel yazılım için</translation>
</message>
<message>
<source>Downloading...</source>
<translation>İndiriliyor...</translation>
</message>
<message>
<source>Download Failed</source>
<translation>İndirme başarısız.</translation>
</message>
<message>
<source>Ensure the entered URL is valid, and the devices internet connection is good.</source>
<translation>Girilen URL nin geçerli olduğundan ve cihazın internet bağlantısının olduğunu kontrol edin</translation>
</message>
<message>
<source>Reboot device</source>
<translation>Cihazı yeniden başlat</translation>
</message>
<message>
<source>Start over</source>
<translation>Zacznij od początku</translation>
</message>
<message>
<source>Something went wrong. Reboot the device.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No custom software found at this URL.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Select a language</source>
<translation>Dil seçin</translation>
</message>
<message>
<source>Choose Software to Install</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>openpilot</source>
<translation type="unfinished">openpilot</translation>
</message>
<message>
<source>Custom Software</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>WARNING: Custom Software</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle.
If you&apos;d like to proceed, use https://flash.comma.ai to restore your device to a factory state later.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SetupWidget</name>
<message>
@ -1129,6 +1024,16 @@ If you&apos;d like to proceed, use https://flash.comma.ai to restore your device
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Record Audio Feedback with LKAS button</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device&apos;s storage.
Note that this feature is only compatible with select cars.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>WiFiPromptWidget</name>

@ -494,15 +494,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
</message>
<message>
<source>Acknowledge Excessive Actuation</source>
<translation type="unfinished"></translation>
<translation></translation>
</message>
<message>
<source>Snooze Update</source>
<translation type="unfinished"></translation>
<translation></translation>
</message>
<message>
<source>openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support.</source>
<translation type="unfinished"></translation>
<source>openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device&apos;s Dongle ID for troubleshooting.</source>
<translation>openpilot %1 访 https://comma.ai/support 联系客服,并提供您设备的 Dongle ID 以便进行故障排查。</translation>
</message>
</context>
<context>
@ -679,113 +679,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<translation>Firehose</translation>
</message>
</context>
<context>
<name>Setup</name>
<message>
<source>WARNING: Low Voltage</source>
<translation></translation>
</message>
<message>
<source>Power your device in a car with a harness or proceed at your own risk.</source>
<translation>使car harness线束为您的设备供电</translation>
</message>
<message>
<source>Power off</source>
<translation></translation>
</message>
<message>
<source>Continue</source>
<translation></translation>
</message>
<message>
<source>Getting Started</source>
<translation></translation>
</message>
<message>
<source>Before we get on the road, lets finish installation and cover some details.</source>
<translation></translation>
</message>
<message>
<source>Connect to Wi-Fi</source>
<translation>WiFi</translation>
</message>
<message>
<source>Back</source>
<translation></translation>
</message>
<message>
<source>Continue without Wi-Fi</source>
<translation>WiFi并继续</translation>
</message>
<message>
<source>Waiting for internet</source>
<translation></translation>
</message>
<message>
<source>Enter URL</source>
<translation></translation>
</message>
<message>
<source>for Custom Software</source>
<translation></translation>
</message>
<message>
<source>Downloading...</source>
<translation></translation>
</message>
<message>
<source>Download Failed</source>
<translation></translation>
</message>
<message>
<source>Ensure the entered URL is valid, and the devices internet connection is good.</source>
<translation>URL有效</translation>
</message>
<message>
<source>Reboot device</source>
<translation></translation>
</message>
<message>
<source>Start over</source>
<translation></translation>
</message>
<message>
<source>No custom software found at this URL.</source>
<translation></translation>
</message>
<message>
<source>Something went wrong. Reboot the device.</source>
<translation></translation>
</message>
<message>
<source>Select a language</source>
<translation></translation>
</message>
<message>
<source>Choose Software to Install</source>
<translation></translation>
</message>
<message>
<source>openpilot</source>
<translation>openpilot</translation>
</message>
<message>
<source>Custom Software</source>
<translation></translation>
</message>
<message>
<source>WARNING: Custom Software</source>
<translation></translation>
</message>
<message>
<source>Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle.
If you&apos;d like to proceed, use https://flash.comma.ai to restore your device to a factory state later.</source>
<translation> comma
https://flash.comma.ai 将设备恢复到出厂状态。</translation>
</message>
</context>
<context>
<name>SetupWidget</name>
<message>
@ -1138,6 +1031,18 @@ If you&apos;d like to proceed, use https://flash.comma.ai to restore your device
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
<translation> comma connect </translation>
</message>
<message>
<source>Record Audio Feedback with LKAS button</source>
<translation>使</translation>
</message>
<message>
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device&apos;s storage.
Note that this feature is only compatible with select cars.</source>
<translation> openpilot comma connect
</translation>
</message>
</context>
<context>
<name>WiFiPromptWidget</name>

@ -494,15 +494,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
</message>
<message>
<source>Acknowledge Excessive Actuation</source>
<translation type="unfinished"></translation>
<translation></translation>
</message>
<message>
<source>Snooze Update</source>
<translation type="unfinished"></translation>
<translation></translation>
</message>
<message>
<source>openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support.</source>
<translation type="unfinished"></translation>
<source>openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device&apos;s Dongle ID for troubleshooting.</source>
<translation>openpilot %1 https://comma.ai/support 聯絡客服,並提供您裝置的 Dongle ID 以進行故障排除。</translation>
</message>
</context>
<context>
@ -679,113 +679,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<translation>Firehose</translation>
</message>
</context>
<context>
<name>Setup</name>
<message>
<source>WARNING: Low Voltage</source>
<translation></translation>
</message>
<message>
<source>Power your device in a car with a harness or proceed at your own risk.</source>
<translation>使 harness </translation>
</message>
<message>
<source>Power off</source>
<translation></translation>
</message>
<message>
<source>Continue</source>
<translation></translation>
</message>
<message>
<source>Getting Started</source>
<translation></translation>
</message>
<message>
<source>Before we get on the road, lets finish installation and cover some details.</source>
<translation></translation>
</message>
<message>
<source>Connect to Wi-Fi</source>
<translation></translation>
</message>
<message>
<source>Back</source>
<translation></translation>
</message>
<message>
<source>Continue without Wi-Fi</source>
<translation> Wi-Fi </translation>
</message>
<message>
<source>Waiting for internet</source>
<translation></translation>
</message>
<message>
<source>Enter URL</source>
<translation></translation>
</message>
<message>
<source>for Custom Software</source>
<translation></translation>
</message>
<message>
<source>Downloading...</source>
<translation></translation>
</message>
<message>
<source>Download Failed</source>
<translation></translation>
</message>
<message>
<source>Ensure the entered URL is valid, and the devices internet connection is good.</source>
<translation></translation>
</message>
<message>
<source>Reboot device</source>
<translation></translation>
</message>
<message>
<source>Start over</source>
<translation></translation>
</message>
<message>
<source>No custom software found at this URL.</source>
<translation></translation>
</message>
<message>
<source>Something went wrong. Reboot the device.</source>
<translation></translation>
</message>
<message>
<source>Select a language</source>
<translation></translation>
</message>
<message>
<source>Choose Software to Install</source>
<translation></translation>
</message>
<message>
<source>openpilot</source>
<translation>openpilot</translation>
</message>
<message>
<source>Custom Software</source>
<translation></translation>
</message>
<message>
<source>WARNING: Custom Software</source>
<translation></translation>
</message>
<message>
<source>Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle.
If you&apos;d like to proceed, use https://flash.comma.ai to restore your device to a factory state later.</source>
<translation> comma
使 https://flash.comma.ai 將您的裝置恢復至出廠狀態。</translation>
</message>
</context>
<context>
<name>SetupWidget</name>
<message>
@ -1138,6 +1031,18 @@ If you&apos;d like to proceed, use https://flash.comma.ai to restore your device
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
<translation> comma connect </translation>
</message>
<message>
<source>Record Audio Feedback with LKAS button</source>
<translation>使</translation>
</message>
<message>
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device&apos;s storage.
Note that this feature is only compatible with select cars.</source>
<translation> openpilot comma connect
</translation>
</message>
</context>
<context>
<name>WiFiPromptWidget</name>

@ -1,33 +0,0 @@
#include <QApplication>
#include <QtWidgets>
#include "selfdrive/ui/qt/qt_window.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/qt/widgets/cameraview.h"
int main(int argc, char *argv[]) {
initApp(argc, argv);
QApplication a(argc, argv);
QWidget w;
setMainWindow(&w);
QVBoxLayout *layout = new QVBoxLayout(&w);
layout->setMargin(0);
layout->setSpacing(0);
{
QHBoxLayout *hlayout = new QHBoxLayout();
layout->addLayout(hlayout);
hlayout->addWidget(new CameraWidget("camerad", VISION_STREAM_ROAD));
}
{
QHBoxLayout *hlayout = new QHBoxLayout();
layout->addLayout(hlayout);
hlayout->addWidget(new CameraWidget("camerad", VISION_STREAM_DRIVER));
hlayout->addWidget(new CameraWidget("camerad", VISION_STREAM_WIDE_ROAD));
}
return a.exec();
}

@ -0,0 +1,17 @@
#!/usr/bin/env python3
import pyray as rl
from msgq.visionipc import VisionStreamType
from openpilot.system.ui.lib.application import gui_app
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
if __name__ == "__main__":
gui_app.init_window("watch3")
road = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD)
driver = CameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER)
wide = CameraView("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD)
for _ in gui_app.render():
road.render(rl.Rectangle(gui_app.width // 4, 0, gui_app.width // 2, gui_app.height // 2))
driver.render(rl.Rectangle(0, gui_app.height // 2, gui_app.width // 2, gui_app.height // 2))
wide.render(rl.Rectangle(gui_app.width // 2, gui_app.height // 2, gui_app.width // 2, gui_app.height // 2))

@ -14,7 +14,6 @@ class ExperimentalModeButton(Widget):
self.params = Params()
self.experimental_mode = self.params.get_bool("ExperimentalMode")
self.is_pressed = False
self.chill_pixmap = gui_app.texture("icons/couch.png", self.img_width, self.img_width)
self.experimental_pixmap = gui_app.texture("icons/experimental_grey.png", self.img_width, self.img_width)
@ -32,20 +31,13 @@ class ExperimentalModeButton(Widget):
rl.draw_rectangle_gradient_h(int(rect.x), int(rect.y), int(rect.width), int(rect.height),
start_color, end_color)
def _handle_interaction(self, rect):
mouse_pos = rl.get_mouse_position()
mouse_in_rect = rl.check_collision_point_rec(mouse_pos, rect)
self.is_pressed = mouse_in_rect and rl.is_mouse_button_down(rl.MOUSE_BUTTON_LEFT)
return mouse_in_rect and rl.is_mouse_button_released(rl.MOUSE_BUTTON_LEFT)
def _render(self, rect):
if self._handle_interaction(rect):
def _handle_mouse_release(self, mouse_pos):
self.experimental_mode = not self.experimental_mode
# TODO: Opening settings for ExperimentalMode
self.params.put_bool("ExperimentalMode", self.experimental_mode)
rl.draw_rectangle_rounded(rect, 0.08, 20, rl.Color(255, 255, 255, 255))
def _render(self, rect):
rl.draw_rectangle_rounded(rect, 0.08, 20, rl.WHITE)
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
self._draw_gradient_background(rect)
@ -61,7 +53,7 @@ class ExperimentalModeButton(Widget):
text_x = rect.x + self.horizontal_padding
text_y = rect.y + rect.height / 2 - 45 // 2 # Center vertically
rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), text, rl.Vector2(int(text_x), int(text_y)), 45, 0, rl.Color(0, 0, 0, 255))
rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), text, rl.Vector2(int(text_x), int(text_y)), 45, 0, rl.BLACK)
# Draw icon (right aligned)
icon_x = rect.x + rect.width - self.horizontal_padding - self.img_width
@ -71,4 +63,4 @@ class ExperimentalModeButton(Widget):
# Draw current mode icon
current_icon = self.experimental_pixmap if self.experimental_mode else self.chill_pixmap
source_rect = rl.Rectangle(0, 0, current_icon.width, current_icon.height)
rl.draw_texture_pro(current_icon, source_rect, icon_rect, rl.Vector2(0, 0), 0, rl.Color(255, 255, 255, 255))
rl.draw_texture_pro(current_icon, source_rect, icon_rect, rl.Vector2(0, 0), 0, rl.WHITE)

@ -36,7 +36,7 @@ class PrimeWidget(Widget):
font = gui_app.font(FontWeight.LIGHT)
wrapped_text = "\n".join(wrap_text(font, "Become a comma prime member at connect.comma.ai", 56, int(w)))
text_size = measure_text_cached(font, wrapped_text, 56)
rl.draw_text_ex(font, wrapped_text, rl.Vector2(x, desc_y), 56, 0, rl.Color(255, 255, 255, 255))
rl.draw_text_ex(font, wrapped_text, rl.Vector2(x, desc_y), 56, 0, rl.WHITE)
# Features section
features_y = desc_y + text_size.y + 50

@ -1,34 +0,0 @@
#if SENSOR_ID == 1
#define VIGNETTE_PROFILE_8DT0MM
#define BIT_DEPTH 12
#define PV_MAX 4096
#define BLACK_LVL 168
float4 normalize_pv(int4 parsed, float vignette_factor) {
float4 pv = (convert_float4(parsed) - BLACK_LVL) / (PV_MAX - BLACK_LVL);
return clamp(pv*vignette_factor, 0.0, 1.0);
}
float3 color_correct(float3 rgb) {
float3 corrected = rgb.x * (float3)(1.82717181, -0.31231438, 0.07307673);
corrected += rgb.y * (float3)(-0.5743977, 1.36858544, -0.53183455);
corrected += rgb.z * (float3)(-0.25277411, -0.05627105, 1.45875782);
return corrected;
}
float3 apply_gamma(float3 rgb, int expo_time) {
// tone mapping params
const float gamma_k = 0.75;
const float gamma_b = 0.125;
const float mp = 0.01; // ideally midpoint should be adaptive
const float rk = 9 - 100*mp;
// poly approximation for s curve
return (rgb > mp) ?
((rk * (rgb-mp) * (1-(gamma_k*mp+gamma_b)) * (1+1/(rk*(1-mp))) / (1+rk*(rgb-mp))) + gamma_k*mp + gamma_b) :
((rk * (rgb-mp) * (gamma_k*mp+gamma_b) * (1+1/(rk*mp)) / (1-rk*(rgb-mp))) + gamma_k*mp + gamma_b);
}
#endif

@ -1,58 +0,0 @@
#if SENSOR_ID == 3
#define BGGR
#define VIGNETTE_PROFILE_4DT6MM
#define BIT_DEPTH 12
#define PV_MAX10 1023
#define PV_MAX12 4095
#define PV_MAX16 65536 // gamma curve is calibrated to 16bit
#define BLACK_LVL 48
float combine_dual_pvs(float lv, float sv, int expo_time) {
float svc = fmax(sv * expo_time, (float)(64 * (PV_MAX10 - BLACK_LVL)));
float svd = sv * fmin(expo_time, 8.0) / 8;
if (expo_time > 64) {
if (lv < PV_MAX10 - BLACK_LVL) {
return lv / (PV_MAX16 - BLACK_LVL);
} else {
return (svc / 64) / (PV_MAX16 - BLACK_LVL);
}
} else {
if (lv > 32) {
return (lv * 64 / fmax(expo_time, 8.0)) / (PV_MAX16 - BLACK_LVL);
} else {
return svd / (PV_MAX16 - BLACK_LVL);
}
}
}
float4 normalize_pv_hdr(int4 parsed, int4 short_parsed, float vignette_factor, int expo_time) {
float4 pl = convert_float4(parsed - BLACK_LVL);
float4 ps = convert_float4(short_parsed - BLACK_LVL);
float4 pv;
pv.s0 = combine_dual_pvs(pl.s0, ps.s0, expo_time);
pv.s1 = combine_dual_pvs(pl.s1, ps.s1, expo_time);
pv.s2 = combine_dual_pvs(pl.s2, ps.s2, expo_time);
pv.s3 = combine_dual_pvs(pl.s3, ps.s3, expo_time);
return clamp(pv*vignette_factor, 0.0, 1.0);
}
float4 normalize_pv(int4 parsed, float vignette_factor) {
float4 pv = (convert_float4(parsed) - BLACK_LVL) / (PV_MAX12 - BLACK_LVL);
return clamp(pv*vignette_factor, 0.0, 1.0);
}
float3 color_correct(float3 rgb) {
float3 corrected = rgb.x * (float3)(1.55361989, -0.268894615, -0.000593219);
corrected += rgb.y * (float3)(-0.421217301, 1.51883144, -0.69760146);
corrected += rgb.z * (float3)(-0.132402589, -0.249936825, 1.69819468);
return corrected;
}
float3 apply_gamma(float3 rgb, int expo_time) {
return (10 * rgb) / (1 + 9 * rgb);
}
#endif

@ -1,47 +0,0 @@
#if SENSOR_ID == 2
#define VIGNETTE_PROFILE_8DT0MM
#define BIT_DEPTH 12
#define BLACK_LVL 64
float ox_lut_func(int x) {
if (x < 512) {
return x * 5.94873e-8;
} else if (512 <= x && x < 768) {
return 3.0458e-05 + (x-512) * 1.19913e-7;
} else if (768 <= x && x < 1536) {
return 6.1154e-05 + (x-768) * 2.38493e-7;
} else if (1536 <= x && x < 1792) {
return 0.0002448 + (x-1536) * 9.56930e-7;
} else if (1792 <= x && x < 2048) {
return 0.00048977 + (x-1792) * 1.91441e-6;
} else if (2048 <= x && x < 2304) {
return 0.00097984 + (x-2048) * 3.82937e-6;
} else if (2304 <= x && x < 2560) {
return 0.0019601 + (x-2304) * 7.659055e-6;
} else if (2560 <= x && x < 2816) {
return 0.0039207 + (x-2560) * 1.525e-5;
} else {
return 0.0078421 + (exp((x-2816)/273.0) - 1) * 0.0092421;
}
}
float4 normalize_pv(int4 parsed, float vignette_factor) {
// PWL
float4 pv = {ox_lut_func(parsed.s0), ox_lut_func(parsed.s1), ox_lut_func(parsed.s2), ox_lut_func(parsed.s3)};
return clamp(pv*vignette_factor*256.0, 0.0, 1.0);
}
float3 color_correct(float3 rgb) {
float3 corrected = rgb.x * (float3)(1.5664815, -0.29808738, -0.03973474);
corrected += rgb.y * (float3)(-0.48672447, 1.41914433, -0.40295248);
corrected += rgb.z * (float3)(-0.07975703, -0.12105695, 1.44268722);
return corrected;
}
float3 apply_gamma(float3 rgb, int expo_time) {
return -0.507089*exp(-12.54124638*rgb) + 0.9655*powr(rgb, 0.5) - 0.472597*rgb + 0.507089;
}
#endif

@ -21,16 +21,16 @@ class TiciFanController(BaseFanController):
self.controller = PIDController(k_p=0, k_i=4e-3, k_f=1, rate=(1 / DT_HW))
def update(self, cur_temp: float, ignition: bool) -> int:
self.controller.neg_limit = -(100 if ignition else 30)
self.controller.pos_limit = -(30 if ignition else 0)
self.controller.pos_limit = 100 if ignition else 30
self.controller.neg_limit = 30 if ignition else 0
if ignition != self.last_ignition:
self.controller.reset()
error = 75 - cur_temp
fan_pwr_out = -int(self.controller.update(
error = cur_temp - 75
fan_pwr_out = int(self.controller.update(
error=error,
feedforward=np.interp(cur_temp, [60.0, 100.0], [0, -100])
feedforward=np.interp(cur_temp, [60.0, 100.0], [0, 100])
))
self.last_ignition = ignition

@ -1,25 +1,25 @@
[
{
"name": "xbl",
"url": "https://commadist.azureedge.net/agnosupdate/xbl-6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1.img.xz",
"hash": "6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1",
"hash_raw": "6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1",
"url": "https://commadist.azureedge.net/agnosupdate/xbl-effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b.img.xz",
"hash": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b",
"hash_raw": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b",
"size": 3282256,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "003a17ab1be68a696f7efe4c9938e8be511d4aacfc2f3211fc896bdc1681d089"
"ondevice_hash": "ed61a650bea0c56652dd0fc68465d8fc722a4e6489dc8f257630c42c6adcdc89"
},
{
"name": "xbl_config",
"url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535.img.xz",
"hash": "63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535",
"hash_raw": "63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535",
"url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c.img.xz",
"hash": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c",
"hash_raw": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c",
"size": 98124,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "2a855dd636cc94718b64bea83a44d0a31741ecaa8f72a63613ff348ec7404091"
"ondevice_hash": "b12801ffaa81e58e3cef914488d3b447e35483ba549b28c6cd9deb4814c3265f"
},
{
"name": "abl",
@ -56,28 +56,28 @@
},
{
"name": "boot",
"url": "https://commadist.azureedge.net/agnosupdate/boot-4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef.img.xz",
"hash": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef",
"hash_raw": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef",
"size": 18479104,
"url": "https://commadist.azureedge.net/agnosupdate/boot-0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4.img.xz",
"hash": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4",
"hash_raw": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4",
"size": 18515968,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "8d7094d774faa4e801e36b403a31b53b913b31d086f4dc682d2f64710c557e8a"
"ondevice_hash": "492ae27f569e8db457c79d0e358a7a6297d1a1c685c2b1ae6deba7315d3a6cb0"
},
{
"name": "system",
"url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img.xz",
"hash": "cccd7073d067027396f2afd49874729757db0bbbc79853a0bf2938bd356fe164",
"hash_raw": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39",
"url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img.xz",
"hash": "1468d50b7ad0fda0f04074755d21e786e3b1b6ca5dd5b17eb2608202025e6126",
"hash_raw": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087",
"size": 5368709120,
"sparse": true,
"full_check": false,
"has_ab": true,
"ondevice_hash": "c7707f16ce7d977748677cc354e250943b4ff6c21b9a19a492053d32397cf9ec",
"ondevice_hash": "242aa5adad1c04e1398e00e2440d1babf962022eb12b89adf2e60ee3068946e7",
"alt": {
"hash": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39",
"url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img",
"hash": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087",
"url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img",
"size": 5368709120
}
}

@ -97,14 +97,14 @@
},
{
"name": "persist",
"url": "https://commadist.azureedge.net/agnosupdate/persist-9814b07851292f510f3794b767489f38ab379a99f0ea75dc620ad2d3a496d54d.img.xz",
"hash": "9814b07851292f510f3794b767489f38ab379a99f0ea75dc620ad2d3a496d54d",
"hash_raw": "9814b07851292f510f3794b767489f38ab379a99f0ea75dc620ad2d3a496d54d",
"size": 33554432,
"url": "https://commadist.azureedge.net/agnosupdate/persist-d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786.img.xz",
"hash": "d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786",
"hash_raw": "d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786",
"size": 4096,
"sparse": false,
"full_check": true,
"has_ab": false,
"ondevice_hash": "9814b07851292f510f3794b767489f38ab379a99f0ea75dc620ad2d3a496d54d"
"ondevice_hash": "d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786"
},
{
"name": "systemrw",
@ -130,25 +130,25 @@
},
{
"name": "xbl",
"url": "https://commadist.azureedge.net/agnosupdate/xbl-6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1.img.xz",
"hash": "6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1",
"hash_raw": "6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1",
"url": "https://commadist.azureedge.net/agnosupdate/xbl-effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b.img.xz",
"hash": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b",
"hash_raw": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b",
"size": 3282256,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "003a17ab1be68a696f7efe4c9938e8be511d4aacfc2f3211fc896bdc1681d089"
"ondevice_hash": "ed61a650bea0c56652dd0fc68465d8fc722a4e6489dc8f257630c42c6adcdc89"
},
{
"name": "xbl_config",
"url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535.img.xz",
"hash": "63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535",
"hash_raw": "63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535",
"url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c.img.xz",
"hash": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c",
"hash_raw": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c",
"size": 98124,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "2a855dd636cc94718b64bea83a44d0a31741ecaa8f72a63613ff348ec7404091"
"ondevice_hash": "b12801ffaa81e58e3cef914488d3b447e35483ba549b28c6cd9deb4814c3265f"
},
{
"name": "abl",
@ -339,62 +339,62 @@
},
{
"name": "boot",
"url": "https://commadist.azureedge.net/agnosupdate/boot-4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef.img.xz",
"hash": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef",
"hash_raw": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef",
"size": 18479104,
"url": "https://commadist.azureedge.net/agnosupdate/boot-0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4.img.xz",
"hash": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4",
"hash_raw": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4",
"size": 18515968,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "8d7094d774faa4e801e36b403a31b53b913b31d086f4dc682d2f64710c557e8a"
"ondevice_hash": "492ae27f569e8db457c79d0e358a7a6297d1a1c685c2b1ae6deba7315d3a6cb0"
},
{
"name": "system",
"url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img.xz",
"hash": "cccd7073d067027396f2afd49874729757db0bbbc79853a0bf2938bd356fe164",
"hash_raw": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39",
"url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img.xz",
"hash": "1468d50b7ad0fda0f04074755d21e786e3b1b6ca5dd5b17eb2608202025e6126",
"hash_raw": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087",
"size": 5368709120,
"sparse": true,
"full_check": false,
"has_ab": true,
"ondevice_hash": "c7707f16ce7d977748677cc354e250943b4ff6c21b9a19a492053d32397cf9ec",
"ondevice_hash": "242aa5adad1c04e1398e00e2440d1babf962022eb12b89adf2e60ee3068946e7",
"alt": {
"hash": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39",
"url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img",
"hash": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087",
"url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img",
"size": 5368709120
}
},
{
"name": "userdata_90",
"url": "https://commadist.azureedge.net/agnosupdate/userdata_90-f0c675e0fae420870c9ba8979fa246b170f4f1a7a04b49609b55b6bdfa8c1b21.img.xz",
"hash": "3d8a007bae088c5959eb9b82454013f91868946d78380fecea2b1afdfb575c02",
"hash_raw": "f0c675e0fae420870c9ba8979fa246b170f4f1a7a04b49609b55b6bdfa8c1b21",
"url": "https://commadist.azureedge.net/agnosupdate/userdata_90-602d5103cba97e1b07f76508d5febb47cfc4463a7e31bd20e461b55c801feb0a.img.xz",
"hash": "6a11d448bac50467791809339051eed2894aae971c37bf6284b3b972a99ba3ac",
"hash_raw": "602d5103cba97e1b07f76508d5febb47cfc4463a7e31bd20e461b55c801feb0a",
"size": 96636764160,
"sparse": true,
"full_check": true,
"has_ab": false,
"ondevice_hash": "5bfbabb8ff96b149056aa75d5b7e66a7cdd9cb4bcefe23b922c292f7f3a43462"
"ondevice_hash": "e014d92940a696bf8582807259820ab73948b950656ed83a45da738f26083705"
},
{
"name": "userdata_89",
"url": "https://commadist.azureedge.net/agnosupdate/userdata_89-06fc52be37b42690ed7b4f8c66c4611309a2dea9fca37dd9d27d1eff302eb1bf.img.xz",
"hash": "443f136484294b210318842d09fb618d5411c8bdbab9f7421d8c89eb291a8d3f",
"hash_raw": "06fc52be37b42690ed7b4f8c66c4611309a2dea9fca37dd9d27d1eff302eb1bf",
"url": "https://commadist.azureedge.net/agnosupdate/userdata_89-4d7f6d12a5557eb6e3cbff9a4cd595677456fdfddcc879eddcea96a43a9d8b48.img.xz",
"hash": "748e31a5fc01fc256c012e359c3382d1f98cce98feafe8ecc0fca3e47caef116",
"hash_raw": "4d7f6d12a5557eb6e3cbff9a4cd595677456fdfddcc879eddcea96a43a9d8b48",
"size": 95563022336,
"sparse": true,
"full_check": true,
"has_ab": false,
"ondevice_hash": "67db02b29a7e4435951c64cc962a474d048ed444aa912f3494391417cd51a074"
"ondevice_hash": "c181b93050787adcfef730c086bcb780f28508d84e6376d9b80d37e5dc02b55e"
},
{
"name": "userdata_30",
"url": "https://commadist.azureedge.net/agnosupdate/userdata_30-06679488f0c5c3fcfd5f351133050751cd189f705e478a979c45fc4a166d18a6.img.xz",
"hash": "875b580cb786f290a842e9187fd945657561886123eb3075a26f7995a18068f6",
"hash_raw": "06679488f0c5c3fcfd5f351133050751cd189f705e478a979c45fc4a166d18a6",
"url": "https://commadist.azureedge.net/agnosupdate/userdata_30-80a76c8e56bbd7536fd5e87e8daa12984e2960db4edeb1f83229b2baeecc4668.img.xz",
"hash": "09ff390e639e4373d772e1688d05a5ac77a573463ed1deeff86390686fa686f9",
"hash_raw": "80a76c8e56bbd7536fd5e87e8daa12984e2960db4edeb1f83229b2baeecc4668",
"size": 32212254720,
"sparse": true,
"full_check": true,
"has_ab": false,
"ondevice_hash": "16e27ba3c5cf9f0394ce6235ba6021b8a2de293fdb08399f8ca832fa5e4d0b9d"
"ondevice_hash": "2c01ab470c02121c721ff6afc25582437e821686207f3afef659387afb69c507"
}
]

@ -21,7 +21,7 @@ void VideoEncoder::publisher_publish(int segment_num, uint32_t idx, VisionIpcBuf
edata.setFrameId(extra.frame_id);
edata.setTimestampSof(extra.timestamp_sof);
edata.setTimestampEof(extra.timestamp_eof);
edata.setType(encoder_info.encode_type);
edata.setType(encoder_info.get_settings(in_width).encode_type);
edata.setEncodeId(cnt++);
edata.setSegmentNum(segment_num);
edata.setSegmentId(idx);

@ -46,7 +46,7 @@ FfmpegEncoder::~FfmpegEncoder() {
}
void FfmpegEncoder::encoder_open() {
auto codec_id = encoder_info.encode_type == cereal::EncodeIndex::Type::QCAMERA_H264
auto codec_id = encoder_info.get_settings(in_width).encode_type == cereal::EncodeIndex::Type::QCAMERA_H264
? AV_CODEC_ID_H264
: AV_CODEC_ID_FFVHUFF;
const AVCodec *codec = avcodec_find_encoder(codec_id);

@ -155,6 +155,9 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei
assert(strcmp((const char *)cap.driver, "msm_vidc_driver") == 0);
assert(strcmp((const char *)cap.card, "msm_vidc_venc") == 0);
EncoderSettings encoder_settings = encoder_info.get_settings(in_width);
bool is_h265 = encoder_settings.encode_type == cereal::EncodeIndex::Type::FULL_H_E_V_C;
struct v4l2_format fmt_out = {
.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
.fmt = {
@ -162,7 +165,7 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei
// downscales are free with v4l
.width = (unsigned int)(out_width),
.height = (unsigned int)(out_height),
.pixelformat = (encoder_info.encode_type == cereal::EncodeIndex::Type::FULL_H_E_V_C) ? V4L2_PIX_FMT_HEVC : V4L2_PIX_FMT_H264,
.pixelformat = is_h265 ? V4L2_PIX_FMT_HEVC : V4L2_PIX_FMT_H264,
.field = V4L2_FIELD_ANY,
.colorspace = V4L2_COLORSPACE_DEFAULT,
}
@ -205,8 +208,10 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei
// shared ctrls
{
struct v4l2_control ctrls[] = {
{ .id = V4L2_CID_MPEG_VIDEO_BITRATE, .value = encoder_settings.bitrate},
{ .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_P_FRAMES, .value = encoder_settings.gop_size - encoder_settings.b_frames - 1},
{ .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_B_FRAMES, .value = encoder_settings.b_frames},
{ .id = V4L2_CID_MPEG_VIDEO_HEADER_MODE, .value = V4L2_MPEG_VIDEO_HEADER_MODE_SEPARATE},
{ .id = V4L2_CID_MPEG_VIDEO_BITRATE, .value = encoder_info.bitrate},
{ .id = V4L2_CID_MPEG_VIDC_VIDEO_RATE_CONTROL, .value = V4L2_CID_MPEG_VIDC_VIDEO_RATE_CONTROL_VBR_CFR},
{ .id = V4L2_CID_MPEG_VIDC_VIDEO_PRIORITY, .value = V4L2_MPEG_VIDC_VIDEO_PRIORITY_REALTIME_DISABLE},
{ .id = V4L2_CID_MPEG_VIDC_VIDEO_IDR_PERIOD, .value = 1},
@ -216,13 +221,11 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei
}
}
if (encoder_info.encode_type == cereal::EncodeIndex::Type::FULL_H_E_V_C) {
if (is_h265) {
struct v4l2_control ctrls[] = {
{ .id = V4L2_CID_MPEG_VIDC_VIDEO_HEVC_PROFILE, .value = V4L2_MPEG_VIDC_VIDEO_HEVC_PROFILE_MAIN},
{ .id = V4L2_CID_MPEG_VIDC_VIDEO_HEVC_TIER_LEVEL, .value = V4L2_MPEG_VIDC_VIDEO_HEVC_LEVEL_HIGH_TIER_LEVEL_5},
{ .id = V4L2_CID_MPEG_VIDC_VIDEO_VUI_TIMING_INFO, .value = V4L2_MPEG_VIDC_VIDEO_VUI_TIMING_INFO_ENABLED},
{ .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_P_FRAMES, .value = 29},
{ .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_B_FRAMES, .value = 0},
};
for (auto ctrl : ctrls) {
util::safe_ioctl(fd, VIDIOC_S_CTRL, &ctrl, "VIDIOC_S_CTRL failed");
@ -231,8 +234,6 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei
struct v4l2_control ctrls[] = {
{ .id = V4L2_CID_MPEG_VIDEO_H264_PROFILE, .value = V4L2_MPEG_VIDEO_H264_PROFILE_HIGH},
{ .id = V4L2_CID_MPEG_VIDEO_H264_LEVEL, .value = V4L2_MPEG_VIDEO_H264_LEVEL_UNKNOWN},
{ .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_P_FRAMES, .value = 14},
{ .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_B_FRAMES, .value = 0},
{ .id = V4L2_CID_MPEG_VIDEO_H264_ENTROPY_MODE, .value = V4L2_MPEG_VIDEO_H264_ENTROPY_MODE_CABAC},
{ .id = V4L2_CID_MPEG_VIDC_VIDEO_H264_CABAC_MODEL, .value = V4L2_CID_MPEG_VIDC_VIDEO_H264_CABAC_MODEL_0},
{ .id = V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_MODE, .value = 0},

@ -194,7 +194,7 @@ int handle_encoder_msg(LoggerdState *s, Message *msg, std::string &name, struct
return bytes_count;
}
void handle_user_flag(LoggerdState *s) {
void handle_preserve_segment(LoggerdState *s) {
static int prev_segment = -1;
if (s->logger.segment() == prev_segment) return;
@ -222,7 +222,7 @@ void loggerd_thread() {
typedef struct ServiceState {
std::string name;
int counter, freq;
bool encoder, user_flag, record_audio;
bool encoder, preserve_segment, record_audio;
} ServiceState;
std::unordered_map<SubSocket*, ServiceState> service_state;
std::unordered_map<SubSocket*, struct RemoteEncoder> remote_encoders;
@ -246,7 +246,7 @@ void loggerd_thread() {
.counter = 0,
.freq = it.decimation,
.encoder = encoder,
.user_flag = it.name == "userFlag",
.preserve_segment = (it.name == "userBookmark") || (it.name == "audioFeedback"),
.record_audio = record_audio,
};
}
@ -281,8 +281,8 @@ void loggerd_thread() {
if (do_exit) break;
ServiceState &service = service_state[sock];
if (service.user_flag) {
handle_user_flag(&s);
if (service.preserve_segment) {
handle_preserve_segment(&s);
}
// drain socket

@ -13,10 +13,7 @@
#include "system/loggerd/logger.h"
constexpr int MAIN_FPS = 20;
const int MAIN_BITRATE = 1e7;
const int LIVESTREAM_BITRATE = 1e6;
const int QCAM_BITRATE = 256000;
const auto MAIN_ENCODE_TYPE = Hardware::PC() ? cereal::EncodeIndex::Type::BIG_BOX_LOSSLESS : cereal::EncodeIndex::Type::FULL_H_E_V_C;
#define NO_CAMERA_PATIENCE 500 // fall back to time-based rotation if all cameras are dead
#define INIT_ENCODE_FUNCTIONS(encode_type) \
@ -29,6 +26,30 @@ const int SEGMENT_LENGTH = LOGGERD_TEST ? atoi(getenv("LOGGERD_SEGMENT_LENGTH"))
constexpr char PRESERVE_ATTR_NAME[] = "user.preserve";
constexpr char PRESERVE_ATTR_VALUE = '1';
struct EncoderSettings {
cereal::EncodeIndex::Type encode_type;
int bitrate;
int gop_size;
int b_frames = 0; // we don't use b frames
static EncoderSettings MainEncoderSettings(int in_width) {
if (in_width <= 1344) {
return EncoderSettings{.encode_type = MAIN_ENCODE_TYPE, .bitrate = 5'000'000, .gop_size = 20};
} else {
return EncoderSettings{.encode_type = MAIN_ENCODE_TYPE, .bitrate = 10'000'000, .gop_size = 30};
}
}
static EncoderSettings QcamEncoderSettings() {
return EncoderSettings{.encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .bitrate = 256'000, .gop_size = 15};
}
static EncoderSettings StreamEncoderSettings() {
return EncoderSettings{.encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .bitrate = 1'000'000, .gop_size = 15};
}
};
class EncoderInfo {
public:
const char *publish_name;
@ -39,9 +60,8 @@ public:
int frame_width = -1;
int frame_height = -1;
int fps = MAIN_FPS;
int bitrate = MAIN_BITRATE;
cereal::EncodeIndex::Type encode_type = Hardware::PC() ? cereal::EncodeIndex::Type::BIG_BOX_LOSSLESS
: cereal::EncodeIndex::Type::FULL_H_E_V_C;
std::function<EncoderSettings(int)> get_settings;
::cereal::EncodeData::Reader (cereal::Event::Reader::*get_encode_data_func)() const;
void (cereal::Event::Builder::*set_encode_idx_func)(::cereal::EncodeIndex::Reader);
cereal::EncodeData::Builder (cereal::Event::Builder::*init_encode_data_func)();
@ -59,12 +79,14 @@ const EncoderInfo main_road_encoder_info = {
.publish_name = "roadEncodeData",
.thumbnail_name = "thumbnail",
.filename = "fcamera.hevc",
.get_settings = [](int in_width){return EncoderSettings::MainEncoderSettings(in_width);},
INIT_ENCODE_FUNCTIONS(RoadEncode),
};
const EncoderInfo main_wide_road_encoder_info = {
.publish_name = "wideRoadEncodeData",
.filename = "ecamera.hevc",
.get_settings = [](int in_width){return EncoderSettings::MainEncoderSettings(in_width);},
INIT_ENCODE_FUNCTIONS(WideRoadEncode),
};
@ -72,39 +94,36 @@ const EncoderInfo main_driver_encoder_info = {
.publish_name = "driverEncodeData",
.filename = "dcamera.hevc",
.record = Params().getBool("RecordFront"),
.get_settings = [](int in_width){return EncoderSettings::MainEncoderSettings(in_width);},
INIT_ENCODE_FUNCTIONS(DriverEncode),
};
const EncoderInfo stream_road_encoder_info = {
.publish_name = "livestreamRoadEncodeData",
//.thumbnail_name = "thumbnail",
.encode_type = cereal::EncodeIndex::Type::QCAMERA_H264,
.record = false,
.bitrate = LIVESTREAM_BITRATE,
.get_settings = [](int){return EncoderSettings::StreamEncoderSettings();},
INIT_ENCODE_FUNCTIONS(LivestreamRoadEncode),
};
const EncoderInfo stream_wide_road_encoder_info = {
.publish_name = "livestreamWideRoadEncodeData",
.encode_type = cereal::EncodeIndex::Type::QCAMERA_H264,
.record = false,
.bitrate = LIVESTREAM_BITRATE,
.get_settings = [](int){return EncoderSettings::StreamEncoderSettings();},
INIT_ENCODE_FUNCTIONS(LivestreamWideRoadEncode),
};
const EncoderInfo stream_driver_encoder_info = {
.publish_name = "livestreamDriverEncodeData",
.encode_type = cereal::EncodeIndex::Type::QCAMERA_H264,
.record = false,
.bitrate = LIVESTREAM_BITRATE,
.get_settings = [](int){return EncoderSettings::StreamEncoderSettings();},
INIT_ENCODE_FUNCTIONS(LivestreamDriverEncode),
};
const EncoderInfo qcam_encoder_info = {
.publish_name = "qRoadEncodeData",
.filename = "qcamera.ts",
.bitrate = QCAM_BITRATE,
.encode_type = cereal::EncodeIndex::Type::QCAMERA_H264,
.get_settings = [](int){return EncoderSettings::QcamEncoderSettings();},
.frame_width = 526,
.frame_height = 330,
.include_audio = Params().getBool("RecordAudio"),

@ -19,11 +19,12 @@ from openpilot.system.hardware.hw import Paths
SEGMENT_LENGTH = 2
FULL_SIZE = 2507572
def hevc_size(w): return FULL_SIZE // 2 if w <= 1344 else FULL_SIZE
CAMERAS = [
("fcamera.hevc", 20, FULL_SIZE, "roadEncodeIdx"),
("dcamera.hevc", 20, FULL_SIZE, "driverEncodeIdx"),
("ecamera.hevc", 20, FULL_SIZE, "wideRoadEncodeIdx"),
("qcamera.ts", 20, 130000, None),
("fcamera.hevc", 20, hevc_size, "roadEncodeIdx"),
("dcamera.hevc", 20, hevc_size, "driverEncodeIdx"),
("ecamera.hevc", 20, hevc_size, "wideRoadEncodeIdx"),
("qcamera.ts", 20, lambda x: 130000, None),
]
# we check frame count, so we don't have to be too strict on size
@ -76,7 +77,7 @@ class TestEncoder:
# check each camera file size
counts = []
first_frames = []
for camera, fps, size, encode_idx_name in CAMERAS:
for camera, fps, size_lambda, encode_idx_name in CAMERAS:
if not record_front and "dcamera" in camera:
continue
@ -86,14 +87,14 @@ class TestEncoder:
assert os.path.exists(file_path), f"segment #{i}: '{file_path}' missing"
# TODO: this ffprobe call is really slow
# check frame count
cmd = f"ffprobe -v error -select_streams v:0 -count_packets -show_entries stream=nb_read_packets -of csv=p=0 {file_path}"
# get width and check frame count
cmd = f"ffprobe -v error -select_streams v:0 -count_packets -show_entries stream=nb_read_packets,width -of csv=p=0 {file_path}"
if TICI:
cmd = "LD_LIBRARY_PATH=/usr/local/lib " + cmd
expected_frames = fps * SEGMENT_LENGTH
probe = subprocess.check_output(cmd, shell=True, encoding='utf8')
frame_count = int(probe.split('\n')[0].strip())
probe = subprocess.check_output(cmd, shell=True, encoding='utf8').split('\n')[0].strip().split(',')
frame_width, frame_count = int(probe[0]), int(probe[1])
counts.append(frame_count)
assert frame_count == expected_frames, \
@ -101,8 +102,9 @@ class TestEncoder:
# sanity check file size
file_size = os.path.getsize(file_path)
assert math.isclose(file_size, size, rel_tol=FILE_SIZE_TOLERANCE), \
f"{file_path} size {file_size} isn't close to target size {size}"
target_size = size_lambda(frame_width)
assert math.isclose(file_size, target_size, rel_tol=FILE_SIZE_TOLERANCE), \
f"{file_path} size {file_size} isn't close to target size {target_size}"
# Check encodeIdx
if encode_idx_name is not None:

@ -282,15 +282,15 @@ class TestLoggerd:
sent.clear_write_flag()
assert sent.to_bytes() == m.as_builder().to_bytes()
def test_preserving_flagged_segments(self):
services = set(random.sample(CEREAL_SERVICES, random.randint(5, 10))) | {"userFlag"}
def test_preserving_bookmarked_segments(self):
services = set(random.sample(CEREAL_SERVICES, random.randint(5, 10))) | {"userBookmark"}
self._publish_random_messages(services)
segment_dir = self._get_latest_log_dir()
assert getxattr(segment_dir, PRESERVE_ATTR_NAME) == PRESERVE_ATTR_VALUE
def test_not_preserving_unflagged_segments(self):
services = set(random.sample(CEREAL_SERVICES, random.randint(5, 10))) - {"userFlag"}
def test_not_preserving_nonbookmarked_segments(self):
services = set(random.sample(CEREAL_SERVICES, random.randint(5, 10))) - {"userBookmark", "audioFeedback"}
self._publish_random_messages(services)
segment_dir = self._get_latest_log_dir()

@ -40,7 +40,7 @@ def manager_init() -> None:
# set unset params to their default value
for k in params.all_keys():
default_value = params.get_default_value(k)
if default_value and params.get(k) is None:
if default_value is not None and params.get(k) is None:
params.put(k, default_value)
# Create folders needed for msgq

@ -81,6 +81,7 @@ procs = [
PythonProcess("sensord", "system.sensord.sensord", only_onroad, enabled=not PC),
NativeProcess("ui", "selfdrive/ui", ["./ui"], always_run, watchdog_max_dt=(5 if not PC else None)),
PythonProcess("raylib_ui", "selfdrive.ui.ui", always_run, enabled=False, watchdog_max_dt=(5 if not PC else None)),
PythonProcess("soundd", "selfdrive.ui.soundd", only_onroad),
PythonProcess("locationd", "selfdrive.locationd.locationd", only_onroad),
NativeProcess("_pandad", "selfdrive/pandad", ["./pandad"], always_run, enabled=False),
@ -106,6 +107,7 @@ procs = [
PythonProcess("updated", "system.updated.updated", only_offroad, enabled=not PC),
PythonProcess("uploader", "system.loggerd.uploader", always_run),
PythonProcess("statsd", "system.statsd", always_run),
PythonProcess("feedbackd", "selfdrive.ui.feedback.feedbackd", only_onroad),
# debug procs
NativeProcess("bridge", "cereal/messaging", ["./bridge"], notcar),

@ -46,9 +46,10 @@ class TestManager:
manager.main()
for k in params.all_keys():
default_value = params.get_default_value(k)
if default_value:
if default_value is not None:
assert params.get(k) == default_value
assert params.get("OpenpilotEnabledToggle")
assert params.get("RouteCount") == 0
@pytest.mark.skip("this test is flaky the way it's currently written, should be moved to test_onroad")
def test_clean_exit(self, subtests):

@ -19,7 +19,7 @@ FPS_LOG_INTERVAL = 5 # Seconds between logging FPS drops
FPS_DROP_THRESHOLD = 0.9 # FPS drop threshold for triggering a warning
FPS_CRITICAL_THRESHOLD = 0.5 # Critical threshold for triggering strict actions
MOUSE_THREAD_RATE = 140 # touch controller runs at 140Hz
MAX_TOUCH_SLOT = 2
MAX_TOUCH_SLOTS = 2
ENABLE_VSYNC = os.getenv("ENABLE_VSYNC", "0") == "1"
SHOW_FPS = os.getenv("SHOW_FPS") == "1"
@ -67,9 +67,10 @@ class MouseEvent(NamedTuple):
class MouseState:
def __init__(self):
def __init__(self, scale: float = 1.0):
self._scale = scale
self._events: deque[MouseEvent] = deque(maxlen=MOUSE_THREAD_RATE) # bound event list
self._prev_mouse_event: list[MouseEvent | None] = [None] * MAX_TOUCH_SLOT
self._prev_mouse_event: list[MouseEvent | None] = [None] * MAX_TOUCH_SLOTS
self._rk = Ratekeeper(MOUSE_THREAD_RATE)
self._lock = threading.Lock()
@ -100,10 +101,12 @@ class MouseState:
self._rk.keep_time()
def _handle_mouse_event(self):
for slot in range(MAX_TOUCH_SLOT):
for slot in range(MAX_TOUCH_SLOTS):
mouse_pos = rl.get_touch_position(slot)
x = mouse_pos.x / self._scale if self._scale != 1.0 else mouse_pos.x
y = mouse_pos.y / self._scale if self._scale != 1.0 else mouse_pos.y
ev = MouseEvent(
MousePos(mouse_pos.x, mouse_pos.y),
MousePos(x, y),
slot,
rl.is_mouse_button_pressed(slot),
rl.is_mouse_button_released(slot),
@ -133,7 +136,7 @@ class GuiApplication:
self._trace_log_callback = None
self._modal_overlay = ModalOverlay()
self._mouse = MouseState()
self._mouse = MouseState(self._scale)
self._mouse_events: list[MouseEvent] = []
# Debug variables

@ -0,0 +1,47 @@
import io
import re
from PIL import Image, ImageDraw, ImageFont
import pyray as rl
_cache: dict[str, rl.Texture] = {}
EMOJI_REGEX = re.compile(
"""[\U0001F600-\U0001F64F
\U0001F300-\U0001F5FF
\U0001F680-\U0001F6FF
\U0001F1E0-\U0001F1FF
\U00002700-\U000027BF
\U0001F900-\U0001F9FF
\U00002600-\U000026FF
\U00002300-\U000023FF
\U00002B00-\U00002BFF
\U0001FA70-\U0001FAFF
\U0001F700-\U0001F77F
\u2640-\u2642
\u2600-\u2B55
\u200d
\u23cf
\u23e9
\u231a
\ufe0f
\u3030
]+""",
flags=re.UNICODE
)
def find_emoji(text):
return [(m.start(), m.end(), m.group()) for m in EMOJI_REGEX.finditer(text)]
def emoji_tex(emoji):
if emoji not in _cache:
img = Image.new("RGBA", (128, 128), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("NotoColorEmoji", 109)
draw.text((0, 0), emoji, font=font, embedded_color=True)
buffer = io.BytesIO()
img.save(buffer, format="PNG")
l = buffer.tell()
buffer.seek(0)
_cache[emoji] = rl.load_texture_from_image(rl.load_image_from_memory(".png", buffer.getvalue(), l))
return _cache[emoji]

@ -39,6 +39,10 @@ vec4 getGradientColor(vec2 pos) {
float t = clamp(dot(pos - gradientStart, normalizedDir) / gradientLength, 0.0, 1.0);
if (gradientColorCount <= 1) return gradientColors[0];
// handle t before first / after last stop
if (t <= gradientStops[0]) return gradientColors[0];
if (t >= gradientStops[gradientColorCount-1]) return gradientColors[gradientColorCount-1];
for (int i = 0; i < gradientColorCount - 1; i++) {
if (t >= gradientStops[i] && t <= gradientStops[i+1]) {
float segmentT = (t - gradientStops[i]) / (gradientStops[i+1] - gradientStops[i]);

@ -37,7 +37,7 @@ NM_DEVICE_IFACE = "org.freedesktop.NetworkManager.Device"
NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8
TETHERING_IP_ADDRESS = "192.168.43.1"
DEFAULT_TETHERING_PASSWORD = "12345678"
DEFAULT_TETHERING_PASSWORD = "swagswagcomma"
# NetworkManager device states
@ -101,8 +101,8 @@ class WifiManager:
"""Connect to the DBus system bus."""
try:
self.bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
if not await self._find_wifi_device():
raise ValueError("No Wi-Fi device found")
while not await self._find_wifi_device():
await asyncio.sleep(1)
await self._setup_signals(self.device_path)
self.active_ap_path = await self.get_active_access_point()
@ -441,20 +441,19 @@ class WifiManager:
settings_iface.on_connection_removed(self._on_connection_removed)
def _on_properties_changed(self, interface: str, changed: dict, invalidated: list):
if 'LastScan' in changed:
if interface == NM_WIRELESS_IFACE and 'LastScan' in changed:
asyncio.create_task(self._refresh_networks())
elif interface == NM_WIRELESS_IFACE and "ActiveAccessPoint" in changed:
new_ap_path = changed["ActiveAccessPoint"].value
if self.active_ap_path != new_ap_path:
self.active_ap_path = new_ap_path
asyncio.create_task(self._refresh_networks())
def _on_state_changed(self, new_state: int, old_state: int, reason: int):
if new_state == NMDeviceState.ACTIVATED:
if self.callbacks.activated:
self.callbacks.activated()
asyncio.create_task(self._refresh_networks())
self._current_connection_ssid = None
asyncio.create_task(self._refresh_networks())
elif new_state in (NMDeviceState.DISCONNECTED, NMDeviceState.NEED_AUTH):
for network in self.networks:
network.is_connected = False
@ -487,9 +486,6 @@ class WifiManager:
if self.callbacks.forgotten:
self.callbacks.forgotten(ssid)
# Update network list to reflect the removed saved connection
asyncio.create_task(self._refresh_networks())
break
async def _add_saved_connection(self, path: str) -> None:
@ -498,14 +494,13 @@ class WifiManager:
settings = await self._get_connection_settings(path)
if ssid := self._extract_ssid(settings):
self.saved_connections[ssid] = path
await self._refresh_networks()
except DBusError as e:
cloudlog.error(f"Failed to add connection {path}: {e}")
def _extract_ssid(self, settings: dict) -> str | None:
"""Extract SSID from connection settings."""
ssid_variant = settings.get('802-11-wireless', {}).get('ssid', Variant('ay', b'')).value
return ''.join(chr(b) for b in ssid_variant) if ssid_variant else None
return bytes(ssid_variant).decode('utf-8') if ssid_variant else None
async def _add_match_rule(self, rule):
"""Add a match rule on the bus."""

@ -125,7 +125,7 @@ def main():
elif sys.argv[1] == "--format":
mode = ResetMode.FORMAT
gui_app.init_window("System Reset")
gui_app.init_window("System Reset", 20)
reset = Reset(mode)
if mode == ResetMode.FORMAT:

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save