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 config --global --add safe.directory '*'
git lfs pull git lfs pull
- name: Push master-ci - 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 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: jobs:
build_release: build_release:
@ -52,7 +52,7 @@ jobs:
command: git lfs pull command: git lfs pull
- name: Build devel - name: Build devel
timeout-minutes: 1 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 - uses: ./.github/workflows/setup-with-retry
- name: Build openpilot and run checks - 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 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 - name: Run unit tests
timeout-minutes: ${{ contains(runner.name, 'nsc') && ((steps.setup-step.outputs.duration < 18) && 1 || 2) || 20 }} timeout-minutes: ${{ contains(runner.name, 'nsc') && ((steps.setup-step.outputs.duration < 18) && 1 || 2) || 20 }}
run: | 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' && \ MAX_EXAMPLES=1 $PYTEST -m 'not slow' && \
./selfdrive/ui/tests/create_test_translations.sh && \ ./selfdrive/ui/tests/create_test_translations.sh && \
QT_QPA_PLATFORM=offscreen ./selfdrive/ui/tests/test_translations && \ QT_QPA_PLATFORM=offscreen ./selfdrive/ui/tests/test_translations && \

2
.gitignore vendored

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

2
Jenkinsfile vendored

@ -167,7 +167,7 @@ node {
env.GIT_COMMIT = checkout(scm).GIT_COMMIT env.GIT_COMMIT = checkout(scm).GIT_COMMIT
def excludeBranches = ['__nightly', 'devel', 'devel-staging', 'release3', 'release3-staging', def excludeBranches = ['__nightly', 'devel', 'devel-staging', 'release3', 'release3-staging',
'testing-closet*', 'hotfix-*'] 'release-tici', 'testing-closet*', 'hotfix-*']
def excludeRegex = excludeBranches.join('|').replaceAll('\\*', '.*') def excludeRegex = excludeBranches.join('|').replaceAll('\\*', '.*')
if (env.BRANCH_NAME != 'master' && !env.BRANCH_NAME.contains('__jenkins_loop_')) { 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. | | `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` | 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. | | `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 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 * New driving model
* Lead car ground-truth fixes * New training architecture
* Ported over VAE from the MLSIM stack * Described in our CVPR paper: "Learning to Drive from a World Model"
* New training architecture described in CVPR paper * 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 * Enable live-learned steering actuation delay
* Opt-in audio recording for dashcam video * 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) Version 0.9.9 (2025-05-23)
======================== ========================

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

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

@ -72,9 +72,11 @@ _services: dict[str, tuple] = {
"navRoute": (True, 0.), "navRoute": (True, 0.),
"navThumbnail": (True, 0.), "navThumbnail": (True, 0.),
"qRoadEncodeIdx": (False, 20.), "qRoadEncodeIdx": (False, 20.),
"userFlag": (True, 0., 1), "userBookmark": (True, 0., 1),
"soundPressure": (True, 10., 10), "soundPressure": (True, 10., 10),
"rawAudioData": (False, 20.), "rawAudioData": (False, 20.),
"bookmarkButton": (True, 0., 1),
"audioFeedback": (True, 0., 1),
# debug # debug
"uiDebug": (True, 0., 1), "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}}, {"LastOffroadStatusPacket", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, JSON}},
{"LastPowerDropDetected", {CLEAR_ON_MANAGER_START, STRING}}, {"LastPowerDropDetected", {CLEAR_ON_MANAGER_START, STRING}},
{"LastUpdateException", {CLEAR_ON_MANAGER_START, STRING}}, {"LastUpdateException", {CLEAR_ON_MANAGER_START, STRING}},
{"LastUpdateRouteCount", {PERSISTENT, INT}}, {"LastUpdateRouteCount", {PERSISTENT, INT, "0"}},
{"LastUpdateTime", {PERSISTENT, TIME}}, {"LastUpdateTime", {PERSISTENT, TIME}},
{"LastUpdateUptimeOnroad", {PERSISTENT, FLOAT}}, {"LastUpdateUptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
{"LiveDelay", {PERSISTENT, BYTES}}, {"LiveDelay", {PERSISTENT, BYTES}},
{"LiveParameters", {PERSISTENT, JSON}}, {"LiveParameters", {PERSISTENT, JSON}},
{"LiveParametersV2", {PERSISTENT, BYTES}}, {"LiveParametersV2", {PERSISTENT, BYTES}},
@ -105,6 +105,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"PandaSignatures", {CLEAR_ON_MANAGER_START, BYTES}}, {"PandaSignatures", {CLEAR_ON_MANAGER_START, BYTES}},
{"PrimeType", {PERSISTENT, INT}}, {"PrimeType", {PERSISTENT, INT}},
{"RecordAudio", {PERSISTENT, BOOL}}, {"RecordAudio", {PERSISTENT, BOOL}},
{"RecordAudioFeedback", {PERSISTENT, BOOL, "0"}},
{"RecordFront", {PERSISTENT, BOOL}}, {"RecordFront", {PERSISTENT, BOOL}},
{"RecordFrontLock", {PERSISTENT, BOOL}}, // for the internal fleet {"RecordFrontLock", {PERSISTENT, BOOL}}, // for the internal fleet
{"SecOCKey", {PERSISTENT | DONT_LOG, STRING}}, {"SecOCKey", {PERSISTENT | DONT_LOG, STRING}},

@ -102,14 +102,14 @@ cdef class Params:
return cast(value) return cast(value)
raise TypeError(f"Type mismatch while writing param {key}: {proposed_type=} {expected_type=} {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: if value is None:
return None return None
try: try:
return CPP_2_PYTHON[t](value) return CPP_2_PYTHON[t](value)
except (KeyError, TypeError, ValueError): except (KeyError, TypeError, ValueError):
cloudlog.warning(f"Failed to cast param {key} with {value=} from type {t=}") 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): def get(self, key, bool block=False, bool return_default=False):
cdef string k = self.check_key(key) cdef string k = self.check_key(key)
@ -126,8 +126,8 @@ cdef class Params:
# it means we got an interrupt while waiting # it means we got an interrupt while waiting
raise KeyboardInterrupt raise KeyboardInterrupt
else: else:
return self.cpp2python(t, default_val, None, key) return self._cpp2python(t, default_val, None, key)
return self.cpp2python(t, val, default_val, key) return self._cpp2python(t, val, default_val, key)
def get_bool(self, key, bool block=False): def get_bool(self, key, bool block=False):
cdef string k = self.check_key(key) cdef string k = self.check_key(key)
@ -188,4 +188,9 @@ cdef class Params:
cdef string k = self.check_key(key) cdef string k = self.check_key(key)
cdef ParamKeyType t = self.p.getKeyType(k) cdef ParamKeyType t = self.p.getKeyType(k)
cdef optional[string] default = self.p.getKeyDefaultValue(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): if isinstance(self._k_d, Number):
self._k_d = [[0], [self._k_d]] self._k_d = [[0], [self._k_d]]
self.pos_limit = pos_limit self.set_limits(pos_limit, neg_limit)
self.neg_limit = neg_limit
self.i_unwind_rate = 0.3 / rate
self.i_rate = 1.0 / rate self.i_rate = 1.0 / rate
self.speed = 0.0 self.speed = 0.0
@ -35,10 +33,6 @@ class PIDController:
def k_d(self): def k_d(self):
return np.interp(self.speed, self._k_d[0], self._k_d[1]) 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): def reset(self):
self.p = 0.0 self.p = 0.0
self.i = 0.0 self.i = 0.0
@ -46,25 +40,25 @@ class PIDController:
self.f = 0.0 self.f = 0.0
self.control = 0 self.control = 0
def update(self, error, error_rate=0.0, speed=0.0, override=False, feedforward=0., freeze_integrator=False): def set_limits(self, pos_limit, neg_limit):
self.speed = speed 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.p = float(error) * self.k_p
self.f = feedforward * self.k_f self.f = feedforward * self.k_f
self.d = error_rate * self.k_d 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: 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 # Don't allow windup if already clipping
control_no_i = self.p + self.d + self.f test_control = self.p + i + self.d + self.f
control_no_i = np.clip(control_no_i, self.neg_limit, self.pos_limit) i_upperbound = self.i if test_control > self.pos_limit else self.pos_limit
self.i = np.clip(self.i, self.neg_limit - control_no_i, self.pos_limit - control_no_i) 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 control = self.p + self.i + self.d + self.f
self.control = np.clip(control, self.neg_limit, self.pos_limit) self.control = np.clip(control, self.neg_limit, self.pos_limit)
return self.control return self.control

@ -1,4 +1,6 @@
import subprocess import subprocess
from contextlib import contextmanager
from subprocess import Popen, PIPE, TimeoutExpired
def run_cmd(cmd: list[str], cwd=None, env=None) -> str: 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: except subprocess.CalledProcessError:
return default 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. 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| |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 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|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 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>||| |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>||| |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>||| |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>|| |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 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 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 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 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 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 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 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 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|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 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 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 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|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|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>||| |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|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|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 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>||| |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 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>||| |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 ### Getting Started
* Setup your [development environment](../tools/) * Set up your [development environment](/tools/)
* Join our [Discord](https://discord.comma.ai) * Join our [Discord](https://discord.comma.ai)
* Docs are at https://docs.comma.ai and https://blog.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 export VECLIB_MAXIMUM_THREADS=1
if [ -z "$AGNOS_VERSION" ]; then if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="12.4" export AGNOS_VERSION="12.8"
fi fi
export STAGING_ROOT="/data/safe_staging" 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 = [ 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')", "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] [project.urls]

@ -3,29 +3,34 @@
``` ```
## release checklist ## release checklist
**Go to `devel-staging`** ### Go to staging
- [ ] make a GitHub issue to track release
- [ ] create release master branch
- [ ] update RELEASES.md - [ ] 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` - [ ] 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 previous release -> new release
- [ ] update from new release -> previous release - [ ] update from new release -> previous release
- [ ] fresh install with `openpilot-test.comma.ai` - [ ] fresh install with `openpilot-test.comma.ai`
- [ ] drive on fresh install - [ ] drive on fresh install
- [ ] no submodules or LFS - [ ] no submodules or LFS
- [ ] check sentry, MTBF, etc. - [ ] check sentry, MTBF, etc.
- [ ] stress test passes in production
**Go to `release3`**
- [ ] publish the blog post - [ ] publish the blog post
- [ ] `git reset --hard origin/release3-staging` - [ ] `git reset --hard origin/release3-staging`
- [ ] tag the release: `git tag v0.X.X <commit-hash> && git push origin v0.X.X` - [ ] tag the release: `git tag v0.X.X <commit-hash> && git push origin v0.X.X`
- [ ] create GitHub release - [ ] create GitHub release
- [ ] final test install on `openpilot.comma.ai` - [ ] final test install on `openpilot.comma.ai`
- [ ] update factory provisioning - [ ] update factory provisioning
- [ ] close out milestone - [ ] close out milestone and issue
- [ ] post on Discord, X, etc. - [ ] post on Discord, X, etc.
``` ```

@ -17,28 +17,23 @@ rm -rf $TARGET_DIR
mkdir -p $TARGET_DIR mkdir -p $TARGET_DIR
cd $TARGET_DIR cd $TARGET_DIR
cp -r $SOURCE_DIR/.git $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 cd $TARGET_DIR
git fetch --depth 1 origin __nightly # tmp branch
git fetch --depth 1 origin devel git checkout --orphan tmp
git checkout -f --track origin/__nightly
git reset --hard __nightly
git checkout __nightly
git reset --hard origin/devel
git clean -xdff
git lfs uninstall
# remove everything except .git # remove everything except .git
echo "[-] erasing old openpilot T=$SECONDS" 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 '{}' \; find . -maxdepth 1 -not -path './.git' -not -name '.' -not -name '..' -exec rm -rf '{}' \;
# reset source tree # cleanup before the copy
cd $SOURCE_DIR cd $SOURCE_DIR
git clean -xdff git clean -xdff
git submodule foreach --recursive git clean -xdff
# do the files copy # do the files copy
echo "[-] copying files T=$SECONDS" echo "[-] copying files T=$SECONDS"
@ -47,6 +42,7 @@ cp -pR --parents $(./release/release_files.py) $TARGET_DIR/
# in the directory # in the directory
cd $TARGET_DIR cd $TARGET_DIR
rm -rf .git/modules/
rm -f panda/board/obj/panda.bin.signed rm -f panda/board/obj/panda.bin.signed
# include source commit hash and build date in commit # include source commit hash and build date in commit
@ -85,7 +81,7 @@ fi
if [ ! -z "$BRANCH" ]; then if [ ! -z "$BRANCH" ]; then
echo "[-] Pushing to $BRANCH T=$SECONDS" echo "[-] Pushing to $BRANCH T=$SECONDS"
git push -f origin __nightly:$BRANCH git push -f origin tmp:$BRANCH
fi fi
echo "[-] done T=$SECONDS, ready at $TARGET_DIR" 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') 'driverMonitoringState', 'onroadEvents', 'driverAssistance'], poll='selfdriveState')
self.pm = messaging.PubMaster(['carControl', 'controlsState']) self.pm = messaging.PubMaster(['carControl', 'controlsState'])
self.steer_limited_by_controls = False self.steer_limited_by_safety = False
self.curvature = 0.0 self.curvature = 0.0
self.desired_curvature = 0.0 self.desired_curvature = 0.0
@ -120,7 +120,7 @@ class Controls:
actuators.curvature = self.desired_curvature actuators.curvature = self.desired_curvature
steer, steeringAngleDeg, lac_log = self.LaC.update(CC.latActive, CS, self.VM, lp, 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 curvature_limited) # TODO what if not available
actuators.torque = float(steer) actuators.torque = float(steer)
actuators.steeringAngleDeg = float(steeringAngleDeg) actuators.steeringAngleDeg = float(steeringAngleDeg)
@ -167,10 +167,10 @@ class Controls:
if self.sm['selfdriveState'].active: if self.sm['selfdriveState'].active:
CO = self.sm['carOutput'] CO = self.sm['carOutput']
if self.CP.steerControlType == car.CarParams.SteerControlType.angle: 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 STEER_ANGLE_SATURATION_THRESHOLD
else: 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 # TODO: both controlsState and carControl valids should be set by
# sm.all_checks(), but this creates a circular dependency # sm.all_checks(), but this creates a circular dependency

@ -15,15 +15,15 @@ class LatControl(ABC):
self.steer_max = 1.0 self.steer_max = 1.0
@abstractmethod @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 pass
def reset(self): def reset(self):
self.sat_count = 0. 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 # 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 self.sat_count += self.sat_count_rate
else: else:
self.sat_count -= self.sat_count_rate self.sat_count -= self.sat_count_rate

@ -3,6 +3,7 @@ import math
from cereal import log from cereal import log
from openpilot.selfdrive.controls.lib.latcontrol import LatControl from openpilot.selfdrive.controls.lib.latcontrol import LatControl
# TODO This is speed dependent
STEER_ANGLE_SATURATION_THRESHOLD = 2.5 # Degrees STEER_ANGLE_SATURATION_THRESHOLD = 2.5 # Degrees
@ -10,9 +11,9 @@ class LatControlAngle(LatControl):
def __init__(self, CP, CI): def __init__(self, CP, CI):
super().__init__(CP, CI) super().__init__(CP, CI)
self.sat_check_min_speed = 5. 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() angle_log = log.ControlsState.LateralAngleState.new_message()
if not active: 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 = math.degrees(VM.get_steer_from_curvature(-desired_curvature, CS.vEgo, params.roll))
angle_steers_des += params.angleOffsetDeg 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 # 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: else:
# for cars which use a method of limiting torque such as a torque signal (Nissan and Toyota) # 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 # 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) 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() self.get_steer_feedforward = CI.get_steer_feedforward_function()
def reset(self): def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, curvature_limited):
super().reset()
self.pid.reset()
def update(self, active, CS, VM, params, steer_limited_by_controls, desired_curvature, curvature_limited):
pid_log = log.ControlsState.LateralPIDState.new_message() pid_log = log.ControlsState.LateralPIDState.new_message()
pid_log.steeringAngleDeg = float(CS.steeringAngleDeg) pid_log.steeringAngleDeg = float(CS.steeringAngleDeg)
pid_log.steeringRateDeg = float(CS.steeringRateDeg) pid_log.steeringRateDeg = float(CS.steeringRateDeg)
@ -29,20 +25,24 @@ class LatControlPID(LatControl):
pid_log.steeringAngleDesiredDeg = angle_steers_des pid_log.steeringAngleDesiredDeg = angle_steers_des
pid_log.angleError = error pid_log.angleError = error
if not active: if not active:
output_steer = 0.0 output_torque = 0.0
pid_log.active = False pid_log.active = False
self.pid.reset()
else: else:
# offset does not contribute to resistive torque # 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.active = True
pid_log.p = float(self.pid.p) pid_log.p = float(self.pid.p)
pid_log.i = float(self.pid.i) pid_log.i = float(self.pid.i)
pid_log.f = float(self.pid.f) pid_log.f = float(self.pid.f)
pid_log.output = float(output_steer) pid_log.output = float(output_torque)
pid_log.saturated = bool(self._check_saturation(self.steer_max - abs(output_steer) < 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))
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 cereal import log
from opendbc.car.lateral import FRICTION_THRESHOLD, get_friction 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.common.constants import ACCELERATION_DUE_TO_GRAVITY
from openpilot.selfdrive.controls.lib.latcontrol import LatControl from openpilot.selfdrive.controls.lib.latcontrol import LatControl
from openpilot.common.pid import PIDController from openpilot.common.pid import PIDController
@ -27,17 +26,24 @@ class LatControlTorque(LatControl):
def __init__(self, CP, CI): def __init__(self, CP, CI):
super().__init__(CP, CI) super().__init__(CP, CI)
self.torque_params = CP.lateralTuning.torque.as_builder() 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.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 self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg
def update_live_torque_params(self, latAccelFactor, latAccelOffset, friction): def update_live_torque_params(self, latAccelFactor, latAccelOffset, friction):
self.torque_params.latAccelFactor = latAccelFactor self.torque_params.latAccelFactor = latAccelFactor
self.torque_params.latAccelOffset = latAccelOffset self.torque_params.latAccelOffset = latAccelOffset
self.torque_params.friction = friction 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() pid_log = log.ControlsState.LateralTorqueState.new_message()
if not active: if not active:
output_torque = 0.0 output_torque = 0.0
@ -57,30 +63,28 @@ class LatControlTorque(LatControl):
setpoint = desired_lateral_accel + low_speed_factor * desired_curvature setpoint = desired_lateral_accel + low_speed_factor * desired_curvature
measurement = actual_lateral_accel + low_speed_factor * actual_curvature measurement = actual_lateral_accel + low_speed_factor * actual_curvature
gravity_adjusted_lateral_accel = desired_lateral_accel - roll_compensation 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) # do error correction in lateral acceleration space, convert at end to handle non-linear torque responses correctly
torque_from_measurement = self.torque_from_lateral_accel(LatControlInputs(measurement, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params, pid_log.error = float(setpoint - measurement)
gravity_adjusted=False) ff = gravity_adjusted_lateral_accel
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)
ff += get_friction(desired_lateral_accel - actual_lateral_accel, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) 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 freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
output_torque = self.pid.update(pid_log.error, output_lataccel = self.pid.update(pid_log.error,
feedforward=ff, feedforward=ff,
speed=CS.vEgo, speed=CS.vEgo,
freeze_integrator=freeze_integrator) freeze_integrator=freeze_integrator)
output_torque = self.torque_from_lateral_accel(output_lataccel, self.torque_params)
pid_log.active = True pid_log.active = True
pid_log.p = float(self.pid.p) pid_log.p = float(self.pid.p)
pid_log.i = float(self.pid.i) pid_log.i = float(self.pid.i)
pid_log.d = float(self.pid.d) pid_log.d = float(self.pid.d)
pid_log.f = float(self.pid.f) 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.actualLateralAccel = float(actual_lateral_accel)
pid_log.desiredLateralAccel = float(desired_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 # TODO left is positive in this convention
return -output_torque, 0.0, pid_log 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_VALS = [1.6, 1.2, 0.8, 0.6]
A_CRUISE_MAX_BP = [0., 10.0, 25., 40.] A_CRUISE_MAX_BP = [0., 10.0, 25., 40.]
CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N] 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 MIN_ALLOW_THROTTLE_SPEED = 2.5
# Lookup table for turns # Lookup table for turns
@ -90,7 +90,7 @@ class LongitudinalPlanner:
return x, v, a, j, throttle_prob return x, v, a, j, throttle_prob
def update(self, sm): 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: if len(sm['carControl'].orientationNED) == 3:
accel_coast = get_coast_accel(sm['carControl'].orientationNED[1]) 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 # No change cost when user is controlling the speed, or when standstill
prev_accel_constraint = not (reset_state or sm['carState'].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)] accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg 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) 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_a_target_e2e = sm['modelV2'].action.desiredAcceleration
output_should_stop_e2e = sm['modelV2'].action.shouldStop output_should_stop_e2e = sm['modelV2'].action.shouldStop
if self.mode == 'acc': if mode == 'acc':
output_a_target = output_a_target_mpc output_a_target = output_a_target_mpc
self.output_should_stop = output_should_stop_mpc self.output_should_stop = output_should_stop_mpc
else: else:

@ -5,6 +5,7 @@ from opendbc.car.car_helpers import interfaces
from opendbc.car.honda.values import CAR as HONDA from opendbc.car.honda.values import CAR as HONDA
from opendbc.car.toyota.values import CAR as TOYOTA from opendbc.car.toyota.values import CAR as TOYOTA
from opendbc.car.nissan.values import CAR as NISSAN 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 opendbc.car.vehicle_model import VehicleModel
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
@ -13,7 +14,8 @@ from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle
class TestLatControl: 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): def test_saturation(self, car_name, controller):
CarInterface = interfaces[car_name] CarInterface = interfaces[car_name]
CP = CarInterface.get_non_essential_params(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 MIN_ENGAGE_BUFFER = 2 # secs
VERSION = 1 # bump this to invalidate old parameter caches VERSION = 1 # bump this to invalidate old parameter caches
ALLOWED_CARS = ['toyota', 'hyundai', 'rivian'] ALLOWED_CARS = ['toyota', 'hyundai', 'rivian', 'honda']
def slope2rot(slope): 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_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_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) self.temporal_idxs = slice(-1-(ModelConstants.TEMPORAL_SKIP*(ModelConstants.INPUT_HISTORY_BUFFER_LEN-1)), None, ModelConstants.TEMPORAL_SKIP)
# policy inputs # policy inputs
self.numpy_inputs = { self.numpy_inputs = {
'desire': np.zeros((1, ModelConstants.INPUT_HISTORY_BUFFER_LEN, ModelConstants.DESIRE_LEN), dtype=np.float32), '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), '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), '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['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['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} imgs_cl = {name: self.frames[name].prepare(bufs[name], transforms[name].flatten()) for name in self.vision_input_names}
if TICI and not USBGPU: 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() 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)) 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} combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict}
if SEND_RAW_PRED: if SEND_RAW_PRED:
combined_outputs_dict['raw_pred'] = np.concatenate([self.vision_output.copy(), self.policy_output.copy()]) 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 frame_id = sm["roadCameraState"].frameId
v_ego = max(sm["carState"].vEgo, 0.) v_ego = max(sm["carState"].vEgo, 0.)
lat_delay = sm["liveDelay"].lateralDelay + LAT_SMOOTH_SECONDS 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']: if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']:
device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32)
dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))]
@ -325,7 +315,6 @@ def main(demo=False):
inputs:dict[str, np.ndarray] = { inputs:dict[str, np.ndarray] = {
'desire': vec_desire, 'desire': vec_desire,
'traffic_convention': traffic_convention, 'traffic_convention': traffic_convention,
'lateral_control_params': lateral_control_params,
} }
mt1 = time.perf_counter() mt1 = time.perf_counter()

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

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

@ -22,9 +22,10 @@ class Parser:
self.ignore_missing = ignore_missing self.ignore_missing = ignore_missing
def check_missing(self, outs, name): 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}") raise ValueError(f"Missing output {name}")
return name not in outs return missing
def parse_categorical_crossentropy(self, name, outs, out_shape=None): def parse_categorical_crossentropy(self, name, outs, out_shape=None):
if self.check_missing(outs, name): if self.check_missing(outs, name):
@ -84,6 +85,13 @@ class Parser:
outs[name] = pred_mu_final.reshape(final_shape) outs[name] = pred_mu_final.reshape(final_shape)
outs[name + '_stds'] = pred_std_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]: 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('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,)) self.parse_mdn('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_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('meta', outs)
self.parse_binary_crossentropy('lead_prob', 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, lead_mhp = self.is_mhp(outs, 'lead', ModelConstants.LEAD_MHP_SELECTION * ModelConstants.LEAD_TRAJ_LEN * ModelConstants.LEAD_WIDTH)
out_shape=(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 return outs
def parse_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: 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, plan_mhp = self.is_mhp(outs, 'plan', ModelConstants.IDX_N * ModelConstants.PLAN_WIDTH)
out_shape=(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)
if 'lat_planner_solution' in outs: self.parse_mdn('plan', outs, in_N=plan_in_N, out_N=plan_out_N, out_shape=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH))
self.parse_mdn('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,))
self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,)) self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,))
return outs return outs

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

@ -42,7 +42,7 @@
"severity": 0 "severity": 0
}, },
"Offroad_ExcessiveActuation": { "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, "severity": 1,
"_comment": "Set extra field to lateral or longitudinal." "_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.git import get_short_branch
from openpilot.common.realtime import DT_CTRL from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.locationd.calibrationd import MIN_SPEED_FILTER 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 AlertSize = log.SelfdriveState.AlertSize
AlertStatus = log.SelfdriveState.AlertStatus AlertStatus = log.SelfdriveState.AlertStatus
@ -198,6 +200,7 @@ class StartupAlert(Alert):
Priority.LOWER, VisualAlert.none, AudibleAlert.none, 5.), Priority.LOWER, VisualAlert.none, AudibleAlert.none, 5.),
# ********** helper functions ********** # ********** helper functions **********
def get_display_speed(speed_ms: float, metric: bool) -> str: 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))) 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) 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 *** # *** debug alerts ***
def out_of_space_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: 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) return NormalPermanentAlert("Invalid LKAS setting", text)
EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
# ********** events with no alerts ********** # ********** events with no alerts **********
@ -991,9 +1003,13 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
ET.WARNING: personality_changed_alert, ET.WARNING: personality_changed_alert,
}, },
EventName.userFlag: { EventName.userBookmark: {
ET.PERMANENT: NormalPermanentAlert("Bookmark Saved", duration=1.5), 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 cereal import car, log
from msgq.visionipc import VisionIpcClient, VisionStreamType from msgq.visionipc import VisionIpcClient, VisionStreamType
from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX
from openpilot.common.params import Params 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.car.car_specific import CarSpecificEvents
from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose
from openpilot.selfdrive.selfdrived.events import Events, ET 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.state import StateMachine
from openpilot.selfdrive.selfdrived.alertmanager import AlertManager, set_offroad_alert 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 TESTING_CLOSET = "TESTING_CLOSET" in os.environ
LONGITUDINAL_PERSONALITY_MAP = {v: k for k, v in log.LongitudinalPersonality.schema.enumerants.items()} 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 ThermalStatus = log.DeviceState.ThermalStatus
State = log.SelfdriveState.OpenpilotState State = log.SelfdriveState.OpenpilotState
@ -43,19 +42,6 @@ SafetyModel = car.CarParams.SafetyModel
IGNORED_SAFETY_MODES = (SafetyModel.silent, SafetyModel.noOutput) 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: class SelfdriveD:
def __init__(self, CP=None): def __init__(self, CP=None):
self.params = Params() self.params = Params()
@ -74,6 +60,8 @@ class SelfdriveD:
self.pose_calibrator = PoseCalibrator() self.pose_calibrator = PoseCalibrator()
self.calibrated_pose: Pose | None = None self.calibrated_pose: Pose | None = None
self.excessive_actuation_check = ExcessiveActuationCheck()
self.excessive_actuation = self.params.get("Offroad_ExcessiveActuation") is not None
# Setup sockets # Setup sockets
self.pm = messaging.PubMaster(['selfdriveState', 'onroadEvents']) self.pm = messaging.PubMaster(['selfdriveState', 'onroadEvents'])
@ -95,7 +83,7 @@ class SelfdriveD:
self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'liveCalibration', self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'liveCalibration',
'carOutput', 'driverMonitoringState', 'longitudinalPlan', 'livePose', 'liveDelay', 'carOutput', 'driverMonitoringState', 'longitudinalPlan', 'livePose', 'liveDelay',
'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters', 'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters',
'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userFlag'] + \ 'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userBookmark', 'audioFeedback'] + \
self.camera_packets + self.sensor_packets + self.gps_packets, self.camera_packets + self.sensor_packets + self.gps_packets,
ignore_alive=ignore, ignore_avg_freq=ignore, ignore_alive=ignore, ignore_avg_freq=ignore,
ignore_valid=ignore, frequency=int(1/DT_CTRL)) ignore_valid=ignore, frequency=int(1/DT_CTRL))
@ -131,8 +119,6 @@ class SelfdriveD:
self.experimental_mode = False self.experimental_mode = False
self.personality = self.params.get("LongitudinalPersonality", return_default=True) self.personality = self.params.get("LongitudinalPersonality", return_default=True)
self.recalibrating_seen = False 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.state_machine = StateMachine()
self.rk = Ratekeeper(100, print_delay_threshold=None) self.rk = Ratekeeper(100, print_delay_threshold=None)
@ -181,9 +167,12 @@ class SelfdriveD:
self.events.add(EventName.selfdriveInitializing) self.events.add(EventName.selfdriveInitializing)
return return
# Check for user flag (bookmark) press # Check for user bookmark press (bookmark button or end of LKAS button feedback)
if self.sm.updated['userFlag']: if self.sm.updated['userBookmark']:
self.events.add(EventName.userFlag) 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 # Don't add any more events while in dashcam mode
if self.CP.passive: if self.CP.passive:
@ -249,7 +238,10 @@ class SelfdriveD:
if self.sm['driverAssistance'].leftLaneDeparture or self.sm['driverAssistance'].rightLaneDeparture: if self.sm['driverAssistance'].leftLaneDeparture or self.sm['driverAssistance'].rightLaneDeparture:
self.events.add(EventName.ldw) 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']: if self.sm.updated['liveCalibration']:
self.pose_calibrator.feed_live_calib(self.sm['liveCalibration']) self.pose_calibrator.feed_live_calib(self.sm['liveCalibration'])
if self.sm.updated['livePose']: if self.sm.updated['livePose']:
@ -257,13 +249,14 @@ class SelfdriveD:
self.calibrated_pose = self.pose_calibrator.build_calibrated_pose(device_pose) self.calibrated_pose = self.pose_calibrator.build_calibrated_pose(device_pose)
if self.calibrated_pose is not None: 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) excessive_actuation = self.excessive_actuation_check.update(self.sm, CS, self.calibrated_pose)
if not self.excessive_actuation and excessive_actuation: if not self.excessive_actuation and excessive_actuation is not None:
set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text="longitudinal") set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text=str(excessive_actuation))
self.excessive_actuation = True self.excessive_actuation = True
if self.excessive_actuation: if self.excessive_actuation:
self.events.add(EventName.excessiveActuation) self.events.add(EventName.excessiveActuation)
# ******************************************************************************************
# Handle lane change # Handle lane change
if self.sm['modelV2'].meta.laneChangeState == LaneChangeState.preLaneChange: if self.sm['modelV2'].meta.laneChangeState == LaneChangeState.preLaneChange:

@ -418,10 +418,10 @@ CONFIGS = [
proc_name="selfdrived", proc_name="selfdrived",
pubs=[ pubs=[
"carState", "deviceState", "pandaStates", "peripheralState", "liveCalibration", "driverMonitoringState", "carState", "deviceState", "pandaStates", "peripheralState", "liveCalibration", "driverMonitoringState",
"longitudinalPlan", "livePose", "liveDelay", "liveParameters", "radarState", "longitudinalPlan", "livePose", "liveDelay", "liveParameters", "radarState", "modelV2",
"modelV2", "driverCameraState", "roadCameraState", "wideRoadCameraState", "managerState", "driverCameraState", "roadCameraState", "wideRoadCameraState", "managerState", "liveTorqueParameters",
"liveTorqueParameters", "accelerometer", "gyroscope", "carOutput", "accelerometer", "gyroscope", "carOutput", "gpsLocationExternal", "gpsLocation", "controlsState",
"gpsLocationExternal", "gpsLocation", "controlsState", "carControl", "driverAssistance", "alertDebug", "carControl", "driverAssistance", "alertDebug", "audioFeedback",
], ],
subs=["selfdriveState", "onroadEvents"], subs=["selfdriveState", "onroadEvents"],
ignore=["logMonoTime"], 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 # 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/" BASE_URL = "https://commadataci.blob.core.windows.net/openpilotci/"
REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit") REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit")

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

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

@ -63,14 +63,8 @@ if GetOption('extras'):
qt_src.remove("main.cc") # replaced by test_runner 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.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 # build installers
if arch != "Darwin":
raylib_env = env.Clone() raylib_env = env.Clone()
raylib_env['LIBPATH'] += [f'#third_party/raylib/{arch}/'] raylib_env['LIBPATH'] += [f'#third_party/raylib/{arch}/']
raylib_env['LINKFLAGS'].append('-Wl,-strip-debug') 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) f = raylib_env.Program(f"installer/installers/installer_{name}", [obj, cont, inter], LIBS=raylib_libs)
# keep installers small # keep installers small
assert f[0].get_size() < 1900*1e3, f[0].get_size() 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 GIT_SSH_URL "git@github.com:commaai/openpilot.git"
#define CONTINUE_PATH "/data/continue.sh" #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" #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[] 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 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"); 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); 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) { void renderProgress(int progress) {
BeginDrawing(); BeginDrawing();
ClearBackground(BLACK); ClearBackground(BLACK);
@ -62,11 +74,11 @@ int doInstall() {
} }
// cleanup previous install attempts // cleanup previous install attempts
run("rm -rf " TMP_INSTALL_PATH " " INSTALL_PATH); run("rm -rf " TMP_INSTALL_PATH);
// do the install // do the install
if (util::file_exists(CACHE_PATH)) { if (util::file_exists(INSTALL_PATH) && util::file_exists(VALID_CACHE_PATH)) {
return cachedFetch(CACHE_PATH); return cachedFetch(INSTALL_PATH);
} else { } else {
return freshClone(); return freshClone();
} }
@ -135,7 +147,9 @@ void cloneFinished(int exitCode) {
run("git submodule update --init"); run("git submodule update --init");
// move into place // 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 #ifdef INTERNAL
run("mkdir -p /data/params/d/"); run("mkdir -p /data/params/d/");
@ -153,9 +167,9 @@ void cloneFinished(int exitCode) {
param << value; param << value;
param.close(); param.close();
} }
run("cd " INSTALL_PATH " && " run(("cd " + INSTALL_PATH + " && "
"git remote set-url origin --push " GIT_SSH_URL " && " "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 #endif
// write continue.sh // write continue.sh
@ -171,16 +185,22 @@ void cloneFinished(int exitCode) {
run("mv /data/continue.sh.new " CONTINUE_PATH); run("mv /data/continue.sh.new " CONTINUE_PATH);
// wait for the installed software's UI to take over // wait for the installed software's UI to take over
util::sleep_for(60 * 1000); finishInstall();
} }
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
InitWindow(2160, 1080, "Installer"); 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); SetTextureFilter(font.texture, TEXTURE_FILTER_BILINEAR);
if (util::file_exists(CONTINUE_PATH)) {
finishInstall();
} else {
renderProgress(0); renderProgress(0);
int result = doInstall(); int result = doInstall();
cloneFinished(result); cloneFinished(result);
}
CloseWindow(); CloseWindow();
UnloadFont(font); UnloadFont(font);
return 0; return 0;

@ -19,7 +19,7 @@ class MainLayout(Widget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._pm = messaging.PubMaster(['userFlag']) self._pm = messaging.PubMaster(['bookmarkButton'])
self._sidebar = Sidebar() self._sidebar = Sidebar()
self._current_mode = MainState.HOME self._current_mode = MainState.HOME
@ -40,7 +40,7 @@ class MainLayout(Widget):
def _setup_callbacks(self): def _setup_callbacks(self):
self._sidebar.set_callbacks(on_settings=self._on_settings_clicked, 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.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.SETTINGS].set_callbacks(on_close=self._set_mode_for_state)
self._layouts[MainState.ONROAD].set_callbacks(on_click=self._on_onroad_clicked) self._layouts[MainState.ONROAD].set_callbacks(on_click=self._on_onroad_clicked)
@ -76,10 +76,10 @@ class MainLayout(Widget):
def _on_settings_clicked(self): def _on_settings_clicked(self):
self.open_settings(PanelType.DEVICE) self.open_settings(PanelType.DEVICE)
def _on_flag_clicked(self): def _on_bookmark_clicked(self):
user_flag = messaging.new_message('userFlag') user_bookmark = messaging.new_message('bookmarkButton')
user_flag.valid = True user_bookmark.valid = True
self._pm.send('userFlag', user_flag) self._pm.send('bookmarkButton', user_bookmark)
def _on_onroad_clicked(self): def _on_onroad_clicked(self):
self._sidebar.set_visible(not self._sidebar.is_visible) 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_COLOR = rl.Color(41, 41, 41, 255)
CLOSE_BTN_PRESSED = rl.Color(59, 59, 59, 255) CLOSE_BTN_PRESSED = rl.Color(59, 59, 59, 255)
TEXT_NORMAL = rl.Color(128, 128, 128, 255) TEXT_NORMAL = rl.Color(128, 128, 128, 255)
TEXT_SELECTED = rl.Color(255, 255, 255, 255) TEXT_SELECTED = rl.WHITE
class PanelType(IntEnum): class PanelType(IntEnum):

@ -24,18 +24,18 @@ NetworkType = log.DeviceState.NetworkType
# Color scheme # Color scheme
class Colors: class Colors:
SIDEBAR_BG = rl.Color(57, 57, 57, 255) 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) WHITE_DIM = rl.Color(255, 255, 255, 85)
GRAY = rl.Color(84, 84, 84, 255) GRAY = rl.Color(84, 84, 84, 255)
# Status colors # Status colors
GOOD = rl.Color(255, 255, 255, 255) GOOD = rl.WHITE
WARNING = rl.Color(218, 202, 37, 255) WARNING = rl.Color(218, 202, 37, 255)
DANGER = rl.Color(201, 34, 49, 255) DANGER = rl.Color(201, 34, 49, 255)
# UI elements # UI elements
METRIC_BORDER = rl.Color(255, 255, 255, 85) 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) BUTTON_PRESSED = rl.Color(255, 255, 255, 166)
@ -146,7 +146,7 @@ class Sidebar(Widget):
def _draw_buttons(self, rect: rl.Rectangle): def _draw_buttons(self, rect: rl.Rectangle):
mouse_pos = rl.get_mouse_position() 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 button
settings_down = mouse_down and rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN) 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 os
import threading import threading
import time import time
from functools import lru_cache
from openpilot.common.api import Api, api_get from openpilot.common.api import Api, api_get
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
TOKEN_EXPIRY_HOURS = 2
class PrimeType(IntEnum): class PrimeType(IntEnum):
UNKNOWN = -2, UNKNOWN = -2,
@ -20,6 +23,12 @@ class PrimeType(IntEnum):
PURPLE = 5, 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: class PrimeState:
FETCH_INTERVAL = 5.0 # seconds between API calls FETCH_INTERVAL = 5.0 # seconds between API calls
API_TIMEOUT = 10.0 # seconds for API requests API_TIMEOUT = 10.0 # seconds for API requests
@ -49,13 +58,15 @@ class PrimeState:
return return
try: 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) response = api_get(f"v1.1/devices/{dongle_id}", timeout=self.API_TIMEOUT, access_token=identity_token)
if response.status_code == 200: if response.status_code == 200:
data = response.json() data = response.json()
is_paired = data.get("is_paired", False) is_paired = data.get("is_paired", False)
prime_type = data.get("prime_type", 0) prime_type = data.get("prime_type", 0)
self.set_type(PrimeType(prime_type) if is_paired else PrimeType.UNPAIRED) 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: except Exception as e:
cloudlog.error(f"Failed to fetch prime status: {e}") cloudlog.error(f"Failed to fetch prime status: {e}")

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

@ -50,7 +50,7 @@ class ExpButton(Widget):
center_y = int(self._rect.y + self._rect.height // 2) center_y = int(self._rect.y + self._rect.height // 2)
mouse_over = rl.check_collision_point_rec(rl.get_mouse_position(), self._rect) 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 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 texture = self._txt_exp if self._held_or_actual_mode() else self._txt_wheel

@ -34,7 +34,7 @@ class FontSizes:
@dataclass(frozen=True) @dataclass(frozen=True)
class Colors: 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) disengaged: rl.Color = rl.Color(145, 155, 149, 255)
override: rl.Color = rl.Color(145, 155, 149, 255) # Added override: rl.Color = rl.Color(145, 155, 149, 255) # Added
engaged: rl.Color = rl.Color(128, 216, 166, 255) 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) white_translucent: rl.Color = rl.Color(255, 255, 255, 200)
border_translucent: rl.Color = rl.Color(255, 255, 255, 75) border_translucent: rl.Color = rl.Color(255, 255, 255, 75)
header_gradient_start: rl.Color = rl.Color(0, 0, 0, 114) 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() 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._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""" """Pre-calculate experimental mode gradient colors"""
if not self._experimental_mode: if not self._experimental_mode:
return return
@ -201,22 +201,21 @@ class ModelRenderer(Widget):
i = 0 i = 0
while i < max_len: while i < max_len:
track_idx = max_len - i - 1 # flip idx to start from bottom right # Some points (screen space) are out of frame (rect space)
track_y = self._path.projected_points[track_idx][1] track_y = self._path.projected_points[i][1]
if track_y < 0 or track_y > height: if track_y < self._rect.y or track_y > (self._rect.y + self._rect.height):
i += 1 i += 1
continue continue
# Calculate color based on acceleration # Calculate color based on acceleration (0 is bottom, 1 is top)
lin_grad_point = (height - track_y) / height lin_grad_point = 1 - (track_y - self._rect.y) / self._rect.height
# speed up: 120, slow down: 0 # speed up: 120, slow down: 0
path_hue = max(min(60 + self._acceleration_x[i] * 35, 120), 0) path_hue = np.clip(60 + self._acceleration_x[i] * 35, 0, 120)
path_hue = int(path_hue * 100 + 0.5) / 100
saturation = min(abs(self._acceleration_x[i] * 1.5), 1) saturation = min(abs(self._acceleration_x[i] * 1.5), 1)
lightness = self._map_val(saturation, 0.0, 1.0, 0.95, 0.62) lightness = np.interp(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) alpha = np.interp(lin_grad_point, [0.75 / 2.0, 0.75], [0.4, 0.0])
# Use HSL to RGB conversion # Use HSL to RGB conversion
color = self._hsla_to_color(path_hue / 360.0, saturation, lightness, alpha) color = self._hsla_to_color(path_hue / 360.0, saturation, lightness, alpha)
@ -280,7 +279,7 @@ class ModelRenderer(Widget):
if self._experimental_mode: if self._experimental_mode:
# Draw with acceleration coloring # 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) draw_polygon(self._rect, self._path.projected_points, gradient=self._exp_gradient)
else: else:
draw_polygon(self._rect, self._path.projected_points, rl.Color(255, 255, 255, 30)) 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) 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 @staticmethod
def _hsla_to_color(h, s, l, a): def _hsla_to_color(h, s, l, a):
rgb = colorsys.hls_to_rgb(h, l, s) 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()); flag_img = loadPixmap("../assets/images/button_flag.png", home_btn.size());
settings_img = loadPixmap("../assets/images/button_settings.png", settings_btn.size(), Qt::IgnoreAspectRatio); settings_img = loadPixmap("../assets/images/button_settings.png", settings_btn.size(), Qt::IgnoreAspectRatio);
mic_img = loadPixmap("../assets/icons/microphone.png", QSize(30, 30)); 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(); }); 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); 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) { void Sidebar::mousePressEvent(QMouseEvent *event) {
@ -61,8 +62,8 @@ void Sidebar::mouseReleaseEvent(QMouseEvent *event) {
} }
if (onroad && home_btn.contains(event->pos())) { if (onroad && home_btn.contains(event->pos())) {
MessageBuilder msg; MessageBuilder msg;
msg.initEvent().initUserFlag(); msg.initEvent().initBookmarkButton();
pm->send("userFlag", msg); pm->send("bookmarkButton", msg);
} else if (settings_btn.contains(event->pos())) { } else if (settings_btn.contains(event->pos())) {
emit openSettings(); emit openSettings();
} else if (recording_audio && mic_indicator_btn.contains(event->pos())) { } else if (recording_audio && mic_indicator_btn.contains(event->pos())) {
@ -150,7 +151,12 @@ void Sidebar::paintEvent(QPaintEvent *event) {
p.setFont(InterFont(35)); p.setFont(InterFont(35));
p.setPen(QColor(0xff, 0xff, 0xff)); p.setPen(QColor(0xff, 0xff, 0xff));
const QRect r = QRect(58, 247, width() - 100, 50); 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); p.drawText(r, Qt::AlignLeft | Qt::AlignVCenter, net_type);
}
// metrics // metrics
drawMetric(p, temp_status.first, temp_status.second, 338); drawMetric(p, temp_status.first, temp_status.second, 338);

@ -37,7 +37,7 @@ protected:
void mouseReleaseEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override;
void drawMetric(QPainter &p, const QPair<QString, QString> &label, QColor c, int y); 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; bool onroad, recording_audio, flag_pressed, settings_pressed, mic_indicator_pressed;
const QMap<cereal::DeviceState::NetworkType, QString> network_type = { const QMap<cereal::DeviceState::NetworkType, QString> network_type = {
{cereal::DeviceState::NetworkType::NONE, tr("--")}, {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> <translation type="unfinished">تأخير التحديث</translation>
</message> </message>
<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> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
@ -700,111 +700,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<translation>خرطوم الحريق</translation> <translation>خرطوم الحريق</translation>
</message> </message>
</context> </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> <context>
<name>SetupWidget</name> <name>SetupWidget</name>
<message> <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> <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> <translation type="unfinished"></translation>
</message> </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>
<context> <context>
<name>WiFiPromptWidget</name> <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> <translation type="unfinished">Update pausieren</translation>
</message> </message>
<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> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
@ -680,111 +680,6 @@ Der Firehose-Modus ermöglicht es dir, deine Trainingsdaten-Uploads zu maximiere
<translation>Firehose</translation> <translation>Firehose</translation>
</message> </message>
</context> </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> <context>
<name>SetupWidget</name> <name>SetupWidget</name>
<message> <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> <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> <translation type="unfinished"></translation>
</message> </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>
<context> <context>
<name>WiFiPromptWidget</name> <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> <translation type="unfinished">Posponer Actualización</translation>
</message> </message>
<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> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
@ -684,113 +684,6 @@ El Modo Firehose te permite maximizar las subidas de datos de entrenamiento para
<translation>Firehose</translation> <translation>Firehose</translation>
</message> </message>
</context> </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> <context>
<name>SetupWidget</name> <name>SetupWidget</name>
<message> <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> <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> <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>
<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>
<context> <context>
<name>WiFiPromptWidget</name> <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> <translation type="unfinished">Reporter la mise à jour</translation>
</message> </message>
<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> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
@ -678,111 +678,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </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> <context>
<name>SetupWidget</name> <name>SetupWidget</name>
<message> <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> <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> <translation type="unfinished"></translation>
</message> </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>
<context> <context>
<name>WiFiPromptWidget</name> <name>WiFiPromptWidget</name>

@ -501,8 +501,8 @@ Firehoseモードを有効にすると学習データを最大限アップロー
<translation></translation> <translation></translation>
</message> </message>
<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>openpilotが過剰な%1https://comma.ai/support からサポートへご連絡下さい。</translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
@ -679,113 +679,6 @@ Firehoseモードを有効にすると学習データを最大限アップロー
<translation></translation> <translation></translation>
</message> </message>
</context> </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> <context>
<name>SetupWidget</name> <name>SetupWidget</name>
<message> <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> <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> <translation> comma connect </translation>
</message> </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>
<context> <context>
<name>WiFiPromptWidget</name> <name>WiFiPromptWidget</name>

@ -501,8 +501,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<translation> </translation> <translation> </translation>
</message> </message>
<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> %1 . . https://comma.ai/support 에 문의하여 지원받으세요.</translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
@ -679,113 +679,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<translation></translation> <translation></translation>
</message> </message>
</context> </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> <context>
<name>SetupWidget</name> <name>SetupWidget</name>
<message> <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> <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> <translation> . comma connect의 .</translation>
</message> </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>
<context> <context>
<name>WiFiPromptWidget</name> <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> <translation>Adiar Atualização</translation>
</message> </message>
<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>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> <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> </message>
</context> </context>
<context> <context>
@ -684,113 +684,6 @@ O Modo Firehose permite maximizar o envio de dados de treinamento para melhorar
<translation>Firehose</translation> <translation>Firehose</translation>
</message> </message>
</context> </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> <context>
<name>SetupWidget</name> <name>SetupWidget</name>
<message> <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> <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> <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>
<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>
<context> <context>
<name>WiFiPromptWidget</name> <name>WiFiPromptWidget</name>

@ -497,7 +497,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<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> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
@ -675,111 +675,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<translation></translation> <translation></translation>
</message> </message>
</context> </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> <context>
<name>SetupWidget</name> <name>SetupWidget</name>
<message> <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> <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> <translation type="unfinished"></translation>
</message> </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>
<context> <context>
<name>WiFiPromptWidget</name> <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> <translation type="unfinished">Güncellemeyi sessize al</translation>
</message> </message>
<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> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
@ -672,111 +672,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </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> <context>
<name>SetupWidget</name> <name>SetupWidget</name>
<message> <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> <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> <translation type="unfinished"></translation>
</message> </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>
<context> <context>
<name>WiFiPromptWidget</name> <name>WiFiPromptWidget</name>

@ -494,15 +494,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
</message> </message>
<message> <message>
<source>Acknowledge Excessive Actuation</source> <source>Acknowledge Excessive Actuation</source>
<translation type="unfinished"></translation> <translation></translation>
</message> </message>
<message> <message>
<source>Snooze Update</source> <source>Snooze Update</source>
<translation type="unfinished"></translation> <translation></translation>
</message> </message>
<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> <translation>openpilot %1 访 https://comma.ai/support 联系客服,并提供您设备的 Dongle ID 以便进行故障排查。</translation>
</message> </message>
</context> </context>
<context> <context>
@ -679,113 +679,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<translation>Firehose</translation> <translation>Firehose</translation>
</message> </message>
</context> </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> <context>
<name>SetupWidget</name> <name>SetupWidget</name>
<message> <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> <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> <translation> comma connect </translation>
</message> </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>
<context> <context>
<name>WiFiPromptWidget</name> <name>WiFiPromptWidget</name>

@ -494,15 +494,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
</message> </message>
<message> <message>
<source>Acknowledge Excessive Actuation</source> <source>Acknowledge Excessive Actuation</source>
<translation type="unfinished"></translation> <translation></translation>
</message> </message>
<message> <message>
<source>Snooze Update</source> <source>Snooze Update</source>
<translation type="unfinished"></translation> <translation></translation>
</message> </message>
<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> <translation>openpilot %1 https://comma.ai/support 聯絡客服,並提供您裝置的 Dongle ID 以進行故障排除。</translation>
</message> </message>
</context> </context>
<context> <context>
@ -679,113 +679,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<translation>Firehose</translation> <translation>Firehose</translation>
</message> </message>
</context> </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> <context>
<name>SetupWidget</name> <name>SetupWidget</name>
<message> <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> <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> <translation> comma connect </translation>
</message> </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>
<context> <context>
<name>WiFiPromptWidget</name> <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.params = Params()
self.experimental_mode = self.params.get_bool("ExperimentalMode") 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.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) 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), rl.draw_rectangle_gradient_h(int(rect.x), int(rect.y), int(rect.width), int(rect.height),
start_color, end_color) start_color, end_color)
def _handle_interaction(self, rect): def _handle_mouse_release(self, mouse_pos):
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):
self.experimental_mode = not self.experimental_mode self.experimental_mode = not self.experimental_mode
# TODO: Opening settings for ExperimentalMode # TODO: Opening settings for ExperimentalMode
self.params.put_bool("ExperimentalMode", self.experimental_mode) 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)) rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
self._draw_gradient_background(rect) self._draw_gradient_background(rect)
@ -61,7 +53,7 @@ class ExperimentalModeButton(Widget):
text_x = rect.x + self.horizontal_padding text_x = rect.x + self.horizontal_padding
text_y = rect.y + rect.height / 2 - 45 // 2 # Center vertically 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) # Draw icon (right aligned)
icon_x = rect.x + rect.width - self.horizontal_padding - self.img_width icon_x = rect.x + rect.width - self.horizontal_padding - self.img_width
@ -71,4 +63,4 @@ class ExperimentalModeButton(Widget):
# Draw current mode icon # Draw current mode icon
current_icon = self.experimental_pixmap if self.experimental_mode else self.chill_pixmap 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) 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) 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))) 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) 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 section
features_y = desc_y + text_size.y + 50 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)) 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: def update(self, cur_temp: float, ignition: bool) -> int:
self.controller.neg_limit = -(100 if ignition else 30) self.controller.pos_limit = 100 if ignition else 30
self.controller.pos_limit = -(30 if ignition else 0) self.controller.neg_limit = 30 if ignition else 0
if ignition != self.last_ignition: if ignition != self.last_ignition:
self.controller.reset() self.controller.reset()
error = 75 - cur_temp error = cur_temp - 75
fan_pwr_out = -int(self.controller.update( fan_pwr_out = int(self.controller.update(
error=error, 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 self.last_ignition = ignition

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

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

@ -46,7 +46,7 @@ FfmpegEncoder::~FfmpegEncoder() {
} }
void FfmpegEncoder::encoder_open() { 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_H264
: AV_CODEC_ID_FFVHUFF; : AV_CODEC_ID_FFVHUFF;
const AVCodec *codec = avcodec_find_encoder(codec_id); 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.driver, "msm_vidc_driver") == 0);
assert(strcmp((const char *)cap.card, "msm_vidc_venc") == 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 = { struct v4l2_format fmt_out = {
.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, .type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
.fmt = { .fmt = {
@ -162,7 +165,7 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei
// downscales are free with v4l // downscales are free with v4l
.width = (unsigned int)(out_width), .width = (unsigned int)(out_width),
.height = (unsigned int)(out_height), .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, .field = V4L2_FIELD_ANY,
.colorspace = V4L2_COLORSPACE_DEFAULT, .colorspace = V4L2_COLORSPACE_DEFAULT,
} }
@ -205,8 +208,10 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei
// shared ctrls // shared ctrls
{ {
struct v4l2_control 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_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_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_PRIORITY, .value = V4L2_MPEG_VIDC_VIDEO_PRIORITY_REALTIME_DISABLE},
{ .id = V4L2_CID_MPEG_VIDC_VIDEO_IDR_PERIOD, .value = 1}, { .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[] = { 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_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_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_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) { for (auto ctrl : ctrls) {
util::safe_ioctl(fd, VIDIOC_S_CTRL, &ctrl, "VIDIOC_S_CTRL failed"); 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[] = { struct v4l2_control ctrls[] = {
{ .id = V4L2_CID_MPEG_VIDEO_H264_PROFILE, .value = V4L2_MPEG_VIDEO_H264_PROFILE_HIGH}, { .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_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_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_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}, { .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; return bytes_count;
} }
void handle_user_flag(LoggerdState *s) { void handle_preserve_segment(LoggerdState *s) {
static int prev_segment = -1; static int prev_segment = -1;
if (s->logger.segment() == prev_segment) return; if (s->logger.segment() == prev_segment) return;
@ -222,7 +222,7 @@ void loggerd_thread() {
typedef struct ServiceState { typedef struct ServiceState {
std::string name; std::string name;
int counter, freq; int counter, freq;
bool encoder, user_flag, record_audio; bool encoder, preserve_segment, record_audio;
} ServiceState; } ServiceState;
std::unordered_map<SubSocket*, ServiceState> service_state; std::unordered_map<SubSocket*, ServiceState> service_state;
std::unordered_map<SubSocket*, struct RemoteEncoder> remote_encoders; std::unordered_map<SubSocket*, struct RemoteEncoder> remote_encoders;
@ -246,7 +246,7 @@ void loggerd_thread() {
.counter = 0, .counter = 0,
.freq = it.decimation, .freq = it.decimation,
.encoder = encoder, .encoder = encoder,
.user_flag = it.name == "userFlag", .preserve_segment = (it.name == "userBookmark") || (it.name == "audioFeedback"),
.record_audio = record_audio, .record_audio = record_audio,
}; };
} }
@ -281,8 +281,8 @@ void loggerd_thread() {
if (do_exit) break; if (do_exit) break;
ServiceState &service = service_state[sock]; ServiceState &service = service_state[sock];
if (service.user_flag) { if (service.preserve_segment) {
handle_user_flag(&s); handle_preserve_segment(&s);
} }
// drain socket // drain socket

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

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

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

@ -40,7 +40,7 @@ def manager_init() -> None:
# set unset params to their default value # set unset params to their default value
for k in params.all_keys(): for k in params.all_keys():
default_value = params.get_default_value(k) 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) params.put(k, default_value)
# Create folders needed for msgq # Create folders needed for msgq

@ -81,6 +81,7 @@ procs = [
PythonProcess("sensord", "system.sensord.sensord", only_onroad, enabled=not PC), 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)), 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("soundd", "selfdrive.ui.soundd", only_onroad),
PythonProcess("locationd", "selfdrive.locationd.locationd", only_onroad), PythonProcess("locationd", "selfdrive.locationd.locationd", only_onroad),
NativeProcess("_pandad", "selfdrive/pandad", ["./pandad"], always_run, enabled=False), 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("updated", "system.updated.updated", only_offroad, enabled=not PC),
PythonProcess("uploader", "system.loggerd.uploader", always_run), PythonProcess("uploader", "system.loggerd.uploader", always_run),
PythonProcess("statsd", "system.statsd", always_run), PythonProcess("statsd", "system.statsd", always_run),
PythonProcess("feedbackd", "selfdrive.ui.feedback.feedbackd", only_onroad),
# debug procs # debug procs
NativeProcess("bridge", "cereal/messaging", ["./bridge"], notcar), NativeProcess("bridge", "cereal/messaging", ["./bridge"], notcar),

@ -46,9 +46,10 @@ class TestManager:
manager.main() manager.main()
for k in params.all_keys(): for k in params.all_keys():
default_value = params.get_default_value(k) 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(k) == default_value
assert params.get("OpenpilotEnabledToggle") 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") @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): 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_DROP_THRESHOLD = 0.9 # FPS drop threshold for triggering a warning
FPS_CRITICAL_THRESHOLD = 0.5 # Critical threshold for triggering strict actions FPS_CRITICAL_THRESHOLD = 0.5 # Critical threshold for triggering strict actions
MOUSE_THREAD_RATE = 140 # touch controller runs at 140Hz 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" ENABLE_VSYNC = os.getenv("ENABLE_VSYNC", "0") == "1"
SHOW_FPS = os.getenv("SHOW_FPS") == "1" SHOW_FPS = os.getenv("SHOW_FPS") == "1"
@ -67,9 +67,10 @@ class MouseEvent(NamedTuple):
class MouseState: 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._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._rk = Ratekeeper(MOUSE_THREAD_RATE)
self._lock = threading.Lock() self._lock = threading.Lock()
@ -100,10 +101,12 @@ class MouseState:
self._rk.keep_time() self._rk.keep_time()
def _handle_mouse_event(self): 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) 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( ev = MouseEvent(
MousePos(mouse_pos.x, mouse_pos.y), MousePos(x, y),
slot, slot,
rl.is_mouse_button_pressed(slot), rl.is_mouse_button_pressed(slot),
rl.is_mouse_button_released(slot), rl.is_mouse_button_released(slot),
@ -133,7 +136,7 @@ class GuiApplication:
self._trace_log_callback = None self._trace_log_callback = None
self._modal_overlay = ModalOverlay() self._modal_overlay = ModalOverlay()
self._mouse = MouseState() self._mouse = MouseState(self._scale)
self._mouse_events: list[MouseEvent] = [] self._mouse_events: list[MouseEvent] = []
# Debug variables # 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); float t = clamp(dot(pos - gradientStart, normalizedDir) / gradientLength, 0.0, 1.0);
if (gradientColorCount <= 1) return gradientColors[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++) { for (int i = 0; i < gradientColorCount - 1; i++) {
if (t >= gradientStops[i] && t <= gradientStops[i+1]) { if (t >= gradientStops[i] && t <= gradientStops[i+1]) {
float segmentT = (t - gradientStops[i]) / (gradientStops[i+1] - gradientStops[i]); 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 NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8
TETHERING_IP_ADDRESS = "192.168.43.1" TETHERING_IP_ADDRESS = "192.168.43.1"
DEFAULT_TETHERING_PASSWORD = "12345678" DEFAULT_TETHERING_PASSWORD = "swagswagcomma"
# NetworkManager device states # NetworkManager device states
@ -101,8 +101,8 @@ class WifiManager:
"""Connect to the DBus system bus.""" """Connect to the DBus system bus."""
try: try:
self.bus = await MessageBus(bus_type=BusType.SYSTEM).connect() self.bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
if not await self._find_wifi_device(): while not await self._find_wifi_device():
raise ValueError("No Wi-Fi device found") await asyncio.sleep(1)
await self._setup_signals(self.device_path) await self._setup_signals(self.device_path)
self.active_ap_path = await self.get_active_access_point() 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) settings_iface.on_connection_removed(self._on_connection_removed)
def _on_properties_changed(self, interface: str, changed: dict, invalidated: list): 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()) asyncio.create_task(self._refresh_networks())
elif interface == NM_WIRELESS_IFACE and "ActiveAccessPoint" in changed: elif interface == NM_WIRELESS_IFACE and "ActiveAccessPoint" in changed:
new_ap_path = changed["ActiveAccessPoint"].value new_ap_path = changed["ActiveAccessPoint"].value
if self.active_ap_path != new_ap_path: if self.active_ap_path != new_ap_path:
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): def _on_state_changed(self, new_state: int, old_state: int, reason: int):
if new_state == NMDeviceState.ACTIVATED: if new_state == NMDeviceState.ACTIVATED:
if self.callbacks.activated: if self.callbacks.activated:
self.callbacks.activated() self.callbacks.activated()
asyncio.create_task(self._refresh_networks())
self._current_connection_ssid = None self._current_connection_ssid = None
asyncio.create_task(self._refresh_networks())
elif new_state in (NMDeviceState.DISCONNECTED, NMDeviceState.NEED_AUTH): elif new_state in (NMDeviceState.DISCONNECTED, NMDeviceState.NEED_AUTH):
for network in self.networks: for network in self.networks:
network.is_connected = False network.is_connected = False
@ -487,9 +486,6 @@ class WifiManager:
if self.callbacks.forgotten: if self.callbacks.forgotten:
self.callbacks.forgotten(ssid) self.callbacks.forgotten(ssid)
# Update network list to reflect the removed saved connection
asyncio.create_task(self._refresh_networks())
break break
async def _add_saved_connection(self, path: str) -> None: async def _add_saved_connection(self, path: str) -> None:
@ -498,14 +494,13 @@ class WifiManager:
settings = await self._get_connection_settings(path) settings = await self._get_connection_settings(path)
if ssid := self._extract_ssid(settings): if ssid := self._extract_ssid(settings):
self.saved_connections[ssid] = path self.saved_connections[ssid] = path
await self._refresh_networks()
except DBusError as e: except DBusError as e:
cloudlog.error(f"Failed to add connection {path}: {e}") cloudlog.error(f"Failed to add connection {path}: {e}")
def _extract_ssid(self, settings: dict) -> str | None: def _extract_ssid(self, settings: dict) -> str | None:
"""Extract SSID from connection settings.""" """Extract SSID from connection settings."""
ssid_variant = settings.get('802-11-wireless', {}).get('ssid', Variant('ay', b'')).value 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): async def _add_match_rule(self, rule):
"""Add a match rule on the bus.""" """Add a match rule on the bus."""

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

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

Loading…
Cancel
Save