mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-08 11:25:51 +08:00
Compare commits
27 Commits
67e5bd3c1e
...
v2026.001.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7985bd8e13 | ||
|
|
9b5f339759 | ||
|
|
5360b879a0 | ||
|
|
ac05b4efb6 | ||
|
|
9d6fbb6324 | ||
|
|
edc4950ea7 | ||
|
|
c7e57a1bc1 | ||
|
|
353ed2a9e1 | ||
|
|
1db8b82f16 | ||
|
|
e8964ce7ae | ||
|
|
ad799442a8 | ||
|
|
592f062326 | ||
|
|
70fd56f69c | ||
|
|
29cd05d6ed | ||
|
|
b1232629c3 | ||
|
|
8539ad0373 | ||
|
|
f468467606 | ||
|
|
4cf822a6cc | ||
|
|
ac8af9aa94 | ||
|
|
1ac64f7360 | ||
|
|
505881cbc5 | ||
|
|
a68ed2fd01 | ||
|
|
2aa179bcac | ||
|
|
090b404fee | ||
|
|
4c36db0091 | ||
|
|
c25b581ae5 | ||
|
|
2316b1142c |
73
.github/workflows/cereal_validation.yaml
vendored
73
.github/workflows/cereal_validation.yaml
vendored
@@ -23,56 +23,43 @@ env:
|
||||
CI: 1
|
||||
|
||||
jobs:
|
||||
generate_cereal_artifact:
|
||||
name: Generate cereal validation artifacts
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: true
|
||||
- run: ./tools/op.sh setup
|
||||
- name: Build openpilot
|
||||
run: scons -j$(nproc) cereal
|
||||
- name: Dump sunnypilot schema
|
||||
run: |
|
||||
export PYTHONPATH=${{ github.workspace }}
|
||||
python3 cereal/messaging/tests/validate_sp_cereal_upstream.py -g -f schema.json
|
||||
- name: 'Prepare artifact'
|
||||
run: |
|
||||
mkdir -p "cereal/messaging/tests/cereal_validations"
|
||||
cp cereal/messaging/tests/validate_sp_cereal_upstream.py "cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py"
|
||||
cp schema.json "cereal/messaging/tests/cereal_validations/schema.json"
|
||||
- name: 'Upload Artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cereal_validations
|
||||
path: cereal/messaging/tests/cereal_validations
|
||||
|
||||
validate_cereal_with_upstream:
|
||||
name: Validate cereal with Upstream
|
||||
runs-on: ubuntu-24.04
|
||||
needs: generate_cereal_artifact
|
||||
steps:
|
||||
- name: Checkout sunnypilot
|
||||
- name: Checkout sunnypilot cereal
|
||||
uses: actions/checkout@v6
|
||||
- name: Checkout upstream openpilot
|
||||
with:
|
||||
sparse-checkout: cereal
|
||||
|
||||
- name: Init sunnypilot opendbc submodule
|
||||
run: git submodule update --init --depth 1 opendbc_repo
|
||||
|
||||
- name: Checkout upstream openpilot cereal
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: 'commaai/openpilot'
|
||||
path: openpilot
|
||||
submodules: true
|
||||
path: upstream_openpilot
|
||||
sparse-checkout: cereal
|
||||
ref: "refs/heads/master"
|
||||
- run: ./tools/op.sh setup
|
||||
- name: Build openpilot
|
||||
working-directory: openpilot
|
||||
run: scons -j$(nproc) cereal
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cereal_validations
|
||||
path: openpilot/cereal/messaging/tests/cereal_validations
|
||||
- name: 'Validate sunnypilot schema against upstream'
|
||||
|
||||
- name: Init upstream opendbc submodule
|
||||
working-directory: upstream_openpilot
|
||||
run: git submodule update --init --depth 1 opendbc_repo
|
||||
|
||||
- name: Install uv
|
||||
run: pip install uv
|
||||
|
||||
- name: Generate sunnypilot schema
|
||||
run: |
|
||||
export PYTHONPATH=${{ github.workspace }}/openpilot
|
||||
chmod +x openpilot/cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py
|
||||
python3 openpilot/cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py -r -f openpilot/cereal/messaging/tests/cereal_validations/schema.json
|
||||
PYCAPNP_VER=$(python3 -c "import re; m=re.search(r'name = \"pycapnp\"\nversion = \"([^\"]+)\"', open('uv.lock').read()); print(m.group(1))")
|
||||
uv run --isolated --with "pycapnp==${PYCAPNP_VER}" \
|
||||
python3 cereal/messaging/tests/validate_sp_cereal_upstream.py \
|
||||
-g -f /tmp/sp_schema.json --cereal-dir cereal
|
||||
|
||||
- name: Validate against upstream
|
||||
run: |
|
||||
PYCAPNP_VER=$(python3 -c "import re; m=re.search(r'name = \"pycapnp\"\nversion = \"([^\"]+)\"', open('uv.lock').read()); print(m.group(1))")
|
||||
uv run --isolated --with "pycapnp==${PYCAPNP_VER}" \
|
||||
python3 cereal/messaging/tests/validate_sp_cereal_upstream.py \
|
||||
-r -f /tmp/sp_schema.json --cereal-dir upstream_openpilot/cereal
|
||||
|
||||
50
CHANGELOG.md
50
CHANGELOG.md
@@ -1,3 +1,39 @@
|
||||
sunnypilot Version 2026.001.007 (2026-05-27)
|
||||
========================
|
||||
* What's Changed (sunnypilot/sunnypilot)
|
||||
* Revert "DM: Lancia Delta HF Integrale model" by @sunnyhaibin
|
||||
|
||||
sunnypilot Version 2026.001.006 (2026-05-17)
|
||||
========================
|
||||
* What's Changed (sunnypilot/sunnypilot)
|
||||
* sunnylink SDUI: tweak DisableUpdate param for clarity by @sunnyhaibin
|
||||
|
||||
sunnypilot Version 2026.001.005 (2026-05-13)
|
||||
========================
|
||||
* What's Changed (sunnypilot/sunnypilot)
|
||||
* sunnylink: add CarParams fallback for brand-specific capabilities by @sunnyhaibin
|
||||
|
||||
sunnypilot Version 2026.001.004 (2026-05-10)
|
||||
========================
|
||||
* What's Changed (sunnypilot/sunnypilot)
|
||||
* sunnylink: switch athena domain by @DevTekVE
|
||||
* sunnylink: fix max time offroad values by @nayan8teen
|
||||
|
||||
sunnypilot Version 2026.001.003 (2026-05-08)
|
||||
========================
|
||||
* What's Changed (sunnypilot/sunnypilot)
|
||||
* manager: disable DEVELOPMENT_ONLY reset by @sunnyhaibin
|
||||
|
||||
sunnypilot Version 2026.001.002 (2026-05-07)
|
||||
========================
|
||||
* What's Changed (sunnypilot/sunnypilot)
|
||||
* release: ignore upstream IsReleaseBranch by @sunnyhaibin
|
||||
|
||||
sunnypilot Version 2026.001.001 (2026-05-06)
|
||||
========================
|
||||
* What's Changed (sunnypilot/sunnypilot)
|
||||
* ui: update gates for certain toggles by @sunnyhaibin
|
||||
|
||||
sunnypilot Version 2026.001.000 (2026-05-06)
|
||||
========================
|
||||
* What's Changed (sunnypilot/sunnypilot)
|
||||
@@ -170,6 +206,20 @@ sunnypilot Version 2026.001.000 (2026-05-06)
|
||||
* @royjr made their first contribution in "HKG: add KIA_FORTE_2019_NON_SCC fingerprint"
|
||||
* @ssysm made their first contribution in "Tesla: remove `TESLA_MODEL_X` from `dashcamOnly`"
|
||||
* Full Changelog: https://github.com/sunnypilot/sunnypilot/compare/v2025.002.000...v2026.001.000
|
||||
************************
|
||||
* Synced with commaai's openpilot (v0.11.1)
|
||||
* master commit c001f3c9b490a80e69539f0af6022f6e07ceb721 (April 16, 2026)
|
||||
* New driver monitoring model
|
||||
* Improved image processing pipeline for driver camera
|
||||
* Rivian R1S and R1T 2025 support thanks to lukasloetkolben!
|
||||
* New driving model #36798
|
||||
* Fully trained using a learned simulator
|
||||
* Improved longitudinal performance in Experimental mode
|
||||
* Reduce comma four standby power usage by 77% to 52 mW
|
||||
* Kia K7 2017 support thanks to royjr!
|
||||
* Lexus LS 2018 support thanks to Hacheoy!
|
||||
* Improved inter-process communication memory efficiency
|
||||
* comma four support
|
||||
|
||||
sunnypilot Version 2025.002.000 (2025-11-06)
|
||||
========================
|
||||
|
||||
@@ -2054,16 +2054,14 @@ struct DriverStateV2 {
|
||||
facePosition @2 :List(Float32);
|
||||
facePositionStd @3 :List(Float32);
|
||||
faceProb @4 :Float32;
|
||||
eyesVisibleProb @14 :Float32;
|
||||
eyesClosedProb @15 :Float32;
|
||||
leftEyeProb @5 :Float32;
|
||||
rightEyeProb @6 :Float32;
|
||||
leftBlinkProb @7 :Float32;
|
||||
rightBlinkProb @8 :Float32;
|
||||
sunglassesProb @9 :Float32;
|
||||
phoneProb @13 :Float32;
|
||||
|
||||
deprecated :group {
|
||||
leftEyeProb @5 :Float32;
|
||||
rightEyeProb @6 :Float32;
|
||||
leftBlinkProb @7 :Float32;
|
||||
rightBlinkProb @8 :Float32;
|
||||
sunglassesProb @9 :Float32;
|
||||
notReadyProb @12 :List(Float32);
|
||||
occludedProb @10 :Float32;
|
||||
readyProb @11 :List(Float32);
|
||||
|
||||
@@ -13,6 +13,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
@@ -104,8 +105,15 @@ def collect_schema(root: Any) -> dict[str, dict]:
|
||||
return structs
|
||||
|
||||
|
||||
def dump_schema(path: str) -> None:
|
||||
from cereal import log
|
||||
def load_log(cereal_dir: str) -> Any:
|
||||
import capnp
|
||||
cereal_dir = os.path.abspath(cereal_dir)
|
||||
capnp.remove_import_hook()
|
||||
return capnp.load(os.path.join(cereal_dir, "log.capnp"), imports=[cereal_dir])
|
||||
|
||||
|
||||
def dump_schema(cereal_dir: str, path: str) -> None:
|
||||
log = load_log(cereal_dir)
|
||||
payload = {
|
||||
"root": hex_id(log.Event.schema.node.id),
|
||||
"structs": collect_schema(log.Event.schema),
|
||||
@@ -206,8 +214,8 @@ def load_peer(path: str) -> dict:
|
||||
return json.load(handle)
|
||||
|
||||
|
||||
def run_read(peer_path: str) -> int:
|
||||
from cereal import log
|
||||
def run_read(cereal_dir: str, peer_path: str) -> int:
|
||||
log = load_log(cereal_dir)
|
||||
peer_dump = load_peer(peer_path)
|
||||
local_dump = {
|
||||
"root": hex_id(log.Event.schema.node.id),
|
||||
@@ -235,16 +243,13 @@ def main() -> int:
|
||||
mode.add_argument("-g", "--generate", action="store_true", help="dump local schema to JSON")
|
||||
mode.add_argument("-r", "--read", action="store_true", help="load peer JSON and diff against local")
|
||||
parser.add_argument("-f", "--file", default="schema.json", help="JSON file path (default: schema.json)")
|
||||
parser.add_argument("--cereal-dir", required=True, help="path to cereal directory containing log.capnp")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
if args.generate:
|
||||
dump_schema(args.file)
|
||||
return 0
|
||||
return run_read(args.file)
|
||||
except ImportError as exc:
|
||||
print(f"error: cannot import cereal ({exc}). did scons build cereal?")
|
||||
return 2
|
||||
if args.generate:
|
||||
dump_schema(args.cereal_dir, args.file)
|
||||
return 0
|
||||
return run_read(args.cereal_dir, args.file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1 +1 @@
|
||||
#define DEFAULT_MODEL "POP model (Default)"
|
||||
#define DEFAULT_MODEL "CD210 (Default)"
|
||||
|
||||
BIN
selfdrive/assets/icons_mici/onroad/glasses.png
LFS
Normal file
BIN
selfdrive/assets/icons_mici/onroad/glasses.png
LFS
Normal file
Binary file not shown.
@@ -86,7 +86,7 @@ class Car:
|
||||
|
||||
self.can_callbacks = can_comm_callbacks(self.can_sock, self.pm.sock['sendcan'])
|
||||
|
||||
is_release = self.params.get_bool("IsReleaseBranch")
|
||||
is_release = False # self.params.get_bool("IsReleaseBranch")
|
||||
is_release_sp = self.params.get_bool("IsReleaseSpBranch")
|
||||
|
||||
if CI is None:
|
||||
|
||||
@@ -83,7 +83,7 @@ def parse_model_output(model_output):
|
||||
face_descs = model_output[f'face_descs_{ds_suffix}']
|
||||
parsed[f'face_descs_{ds_suffix}'] = face_descs[:, :-6]
|
||||
parsed[f'face_descs_{ds_suffix}_std'] = safe_exp(face_descs[:, -6:])
|
||||
for key in ['face_prob', 'eyes_visible_prob', 'eyes_closed_prob', 'using_phone_prob']:
|
||||
for key in ['face_prob', 'left_eye_prob', 'right_eye_prob','left_blink_prob', 'right_blink_prob', 'sunglasses_prob', 'using_phone_prob']:
|
||||
parsed[f'{key}_{ds_suffix}'] = sigmoid(model_output[f'{key}_{ds_suffix}'])
|
||||
return parsed
|
||||
|
||||
@@ -93,8 +93,11 @@ def fill_driver_data(msg, model_output, ds_suffix):
|
||||
msg.facePosition = model_output[f'face_descs_{ds_suffix}'][0, 3:5].tolist()
|
||||
msg.facePositionStd = model_output[f'face_descs_{ds_suffix}_std'][0, 3:5].tolist()
|
||||
msg.faceProb = model_output[f'face_prob_{ds_suffix}'][0, 0].item()
|
||||
msg.eyesVisibleProb = model_output[f'eyes_visible_prob_{ds_suffix}'][0, 0].item()
|
||||
msg.eyesClosedProb = model_output[f'eyes_closed_prob_{ds_suffix}'][0, 0].item()
|
||||
msg.leftEyeProb = model_output[f'left_eye_prob_{ds_suffix}'][0, 0].item()
|
||||
msg.rightEyeProb = model_output[f'right_eye_prob_{ds_suffix}'][0, 0].item()
|
||||
msg.leftBlinkProb = model_output[f'left_blink_prob_{ds_suffix}'][0, 0].item()
|
||||
msg.rightBlinkProb = model_output[f'right_blink_prob_{ds_suffix}'][0, 0].item()
|
||||
msg.sunglassesProb = model_output[f'sunglasses_prob_{ds_suffix}'][0, 0].item()
|
||||
msg.phoneProb = model_output[f'using_phone_prob_{ds_suffix}'][0, 0].item()
|
||||
|
||||
def get_driverstate_packet(model_output, frame_id: int, location_ts: int, exec_time: float, gpu_exec_time: float):
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -32,8 +32,9 @@ class DRIVER_MONITOR_SETTINGS:
|
||||
self._DISTRACTED_PROMPT_TIME_TILL_TERMINAL = 6.
|
||||
|
||||
self._FACE_THRESHOLD = 0.7
|
||||
self._EYE_THRESHOLD = 0.5
|
||||
self._BLINK_THRESHOLD = 0.5
|
||||
self._EYE_THRESHOLD = 0.65
|
||||
self._SG_THRESHOLD = 0.9
|
||||
self._BLINK_THRESHOLD = 0.865
|
||||
self._PHONE_THRESH = 0.5
|
||||
|
||||
self._POSE_PITCH_THRESHOLD = 0.3133
|
||||
@@ -110,6 +111,11 @@ class DriverProb:
|
||||
self.prob_offseter = RunningStatFilter(raw_priors=raw_priors, max_trackable=max_trackable)
|
||||
self.prob_calibrated = False
|
||||
|
||||
class DriverBlink:
|
||||
def __init__(self):
|
||||
self.left = 0.
|
||||
self.right = 0.
|
||||
|
||||
|
||||
# model output refers to center of undistorted+leveled image
|
||||
EFL = 598.0 # focal length in K
|
||||
@@ -144,7 +150,7 @@ class DriverMonitoring:
|
||||
wheelpos_filter_raw_priors = (self.settings._WHEELPOS_DATA_AVG, self.settings._WHEELPOS_DATA_VAR, 2)
|
||||
self.wheelpos = DriverProb(raw_priors=wheelpos_filter_raw_priors, max_trackable=self.settings._WHEELPOS_MAX_COUNT)
|
||||
self.pose = DriverPose(settings=self.settings)
|
||||
self.blink_prob = 0.
|
||||
self.blink = DriverBlink()
|
||||
self.phone_prob = 0.
|
||||
|
||||
self.always_on = always_on
|
||||
@@ -247,7 +253,7 @@ class DriverMonitoring:
|
||||
if pitch_error > pitch_threshold or yaw_error > yaw_threshold:
|
||||
distracted_types.append(DistractedType.DISTRACTED_POSE)
|
||||
|
||||
if self.blink_prob > self.settings._BLINK_THRESHOLD:
|
||||
if (self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD:
|
||||
distracted_types.append(DistractedType.DISTRACTED_BLINK)
|
||||
|
||||
if self.phone_prob > self.settings._PHONE_THRESH:
|
||||
@@ -288,7 +294,10 @@ class DriverMonitoring:
|
||||
self.pose.yaw_std = driver_data.faceOrientationStd[1]
|
||||
model_std_max = max(self.pose.pitch_std, self.pose.yaw_std)
|
||||
self.pose.low_std = model_std_max < self.settings._POSESTD_THRESHOLD
|
||||
self.blink_prob = driver_data.eyesClosedProb * (driver_data.eyesVisibleProb > self.settings._EYE_THRESHOLD)
|
||||
self.blink.left = driver_data.leftBlinkProb * (driver_data.leftEyeProb > self.settings._EYE_THRESHOLD) \
|
||||
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
|
||||
self.blink.right = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \
|
||||
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
|
||||
self.phone_prob = driver_data.phoneProb
|
||||
|
||||
self.distracted_types = self._get_distracted_types()
|
||||
|
||||
@@ -20,8 +20,10 @@ def make_msg(face_detected, distracted=False, model_uncertain=False):
|
||||
ds.leftDriverData.faceOrientation = [0., 0., 0.]
|
||||
ds.leftDriverData.facePosition = [0., 0.]
|
||||
ds.leftDriverData.faceProb = 1. * face_detected
|
||||
ds.leftDriverData.eyesVisibleProb = 1.
|
||||
ds.leftDriverData.eyesClosedProb = 1. * distracted
|
||||
ds.leftDriverData.leftEyeProb = 1.
|
||||
ds.leftDriverData.rightEyeProb = 1.
|
||||
ds.leftDriverData.leftBlinkProb = 1. * distracted
|
||||
ds.leftDriverData.rightBlinkProb = 1. * distracted
|
||||
ds.leftDriverData.faceOrientationStd = [1.*model_uncertain, 1.*model_uncertain, 1.*model_uncertain]
|
||||
ds.leftDriverData.facePositionStd = [1.*model_uncertain, 1.*model_uncertain]
|
||||
# TODO: test both separately when e2e is used
|
||||
|
||||
@@ -76,7 +76,7 @@ def generate_report(proposed, master, tmp, commit):
|
||||
(lambda x: get_idx_if_non_empty(x.wheelOnRightProb), "wheelOnRightProb"),
|
||||
(lambda x: get_idx_if_non_empty(x.leftDriverData.faceProb), "leftDriverData.faceProb"),
|
||||
(lambda x: get_idx_if_non_empty(x.leftDriverData.faceOrientation, 0), "leftDriverData.faceOrientation0"),
|
||||
(lambda x: get_idx_if_non_empty(x.leftDriverData.eyesClosedProb), "leftDriverData.eyesClosedProb"),
|
||||
(lambda x: get_idx_if_non_empty(x.leftDriverData.leftBlinkProb), "leftDriverData.leftBlinkProb"),
|
||||
(lambda x: get_idx_if_non_empty(x.leftDriverData.phoneProb), "leftDriverData.phoneProb"),
|
||||
(lambda x: get_idx_if_non_empty(x.rightDriverData.faceProb), "rightDriverData.faceProb"),
|
||||
], "driverStateV2")
|
||||
|
||||
@@ -36,7 +36,7 @@ class DeveloperLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._is_release = self._params.get_bool("IsReleaseBranch")
|
||||
self._is_release = False # self._params.get_bool("IsReleaseBranch")
|
||||
|
||||
# Build items and keep references for callbacks/state updates
|
||||
self._adb_toggle = toggle_item(
|
||||
|
||||
@@ -42,7 +42,7 @@ class TogglesLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._is_release = self._params.get_bool("IsReleaseBranch")
|
||||
self._is_release = False # self._params.get_bool("IsReleaseBranch")
|
||||
|
||||
# param, title, desc, icon, needs_restart
|
||||
self._toggle_defs = {
|
||||
|
||||
@@ -39,6 +39,8 @@ class BaseDriverCameraDialog(Widget):
|
||||
self._eye_fill_texture = None
|
||||
self._eye_orange_texture = None
|
||||
self._eye_size = 74
|
||||
self._glasses_texture = None
|
||||
self._glasses_size = 171
|
||||
|
||||
self._load_eye_textures()
|
||||
|
||||
@@ -152,6 +154,8 @@ class BaseDriverCameraDialog(Widget):
|
||||
self._eye_fill_texture = gui_app.texture("icons_mici/onroad/eye_fill.png", self._eye_size, self._eye_size)
|
||||
if self._eye_orange_texture is None:
|
||||
self._eye_orange_texture = gui_app.texture("icons_mici/onroad/eye_orange.png", self._eye_size, self._eye_size)
|
||||
if self._glasses_texture is None:
|
||||
self._glasses_texture = gui_app.texture("icons_mici/onroad/glasses.png", self._glasses_size, self._glasses_size)
|
||||
|
||||
def _draw_face_detection(self, rect: rl.Rectangle):
|
||||
dm_state = ui_state.sm["driverMonitoringState"]
|
||||
@@ -198,21 +202,31 @@ class BaseDriverCameraDialog(Widget):
|
||||
eye_offset_x = 10
|
||||
eye_offset_y = 10
|
||||
eye_spacing = self._eye_size + 15
|
||||
eyes_prob = driver_data.eyesVisibleProb
|
||||
|
||||
left_eye_x = rect.x + eye_offset_x
|
||||
left_eye_y = rect.y + eye_offset_y
|
||||
left_eye_prob = driver_data.leftEyeProb
|
||||
|
||||
right_eye_x = rect.x + eye_offset_x + eye_spacing
|
||||
right_eye_y = rect.y + eye_offset_y
|
||||
right_eye_prob = driver_data.rightEyeProb
|
||||
|
||||
# Draw eyes with opacity based on probability
|
||||
fill_opacity = eyes_prob
|
||||
orange_opacity = 1.0 - eyes_prob
|
||||
for eye_x, eye_y in [(left_eye_x, left_eye_y), (right_eye_x, right_eye_y)]:
|
||||
for eye_x, eye_y, eye_prob in [(left_eye_x, left_eye_y, left_eye_prob), (right_eye_x, right_eye_y, right_eye_prob)]:
|
||||
fill_opacity = eye_prob
|
||||
orange_opacity = 1.0 - eye_prob
|
||||
|
||||
rl.draw_texture_v(self._eye_orange_texture, (eye_x, eye_y), rl.Color(255, 255, 255, int(255 * orange_opacity)))
|
||||
rl.draw_texture_v(self._eye_fill_texture, (eye_x, eye_y), rl.Color(255, 255, 255, int(255 * fill_opacity)))
|
||||
|
||||
# Draw sunglasses indicator based on sunglasses probability
|
||||
# Position glasses centered between the two eyes at top left
|
||||
glasses_x = rect.x + eye_offset_x - 4
|
||||
glasses_y = rect.y
|
||||
glasses_pos = rl.Vector2(glasses_x, glasses_y)
|
||||
glasses_prob = driver_data.sunglassesProb
|
||||
rl.draw_texture_v(self._glasses_texture, glasses_pos, rl.Color(70, 80, 161, int(255 * glasses_prob)))
|
||||
|
||||
|
||||
class DriverCameraDialog(NavWidget, BaseDriverCameraDialog):
|
||||
def __init__(self):
|
||||
|
||||
@@ -159,7 +159,6 @@ class UIStateSP:
|
||||
|
||||
def _enforce_constraints(self) -> None:
|
||||
has_long = self.has_longitudinal_control
|
||||
has_icbm = self.has_icbm
|
||||
CP = self.CP
|
||||
|
||||
if CP is not None:
|
||||
@@ -168,8 +167,8 @@ class UIStateSP:
|
||||
self.params.remove("EnforceTorqueControl")
|
||||
self.params.remove("NeuralNetworkLateralControl")
|
||||
|
||||
# Alpha longitudinal: clear if not available or on release branch
|
||||
if not CP.alphaLongitudinalAvailable or self.params.get_bool("IsReleaseBranch"):
|
||||
# Alpha longitudinal: clear if not available
|
||||
if not CP.alphaLongitudinalAvailable:
|
||||
self.params.remove("AlphaLongitudinalEnabled")
|
||||
|
||||
# BSM not available: clear BSM-dependent settings
|
||||
@@ -181,21 +180,23 @@ class UIStateSP:
|
||||
self.params.remove("NeuralNetworkLateralControl")
|
||||
self.params.remove("AlphaLongitudinalEnabled")
|
||||
|
||||
# No longitudinal control: no experimental mode
|
||||
# No longitudinal control: no experimental mode or DEC
|
||||
if not has_long:
|
||||
self.params.remove("ExperimentalMode")
|
||||
self.params.remove("DynamicExperimentalControl")
|
||||
|
||||
# ICBM: clear if not available or if full longitudinal control is active
|
||||
if self.CP_SP is not None:
|
||||
if not self.CP_SP.intelligentCruiseButtonManagementAvailable or has_long:
|
||||
self.params.remove("IntelligentCruiseButtonManagement")
|
||||
self.has_icbm = False
|
||||
else:
|
||||
self.params.remove("IntelligentCruiseButtonManagement")
|
||||
self.has_icbm = False
|
||||
|
||||
# Cruise features requiring longitudinal or ICBM
|
||||
if not (has_long or has_icbm):
|
||||
if not (has_long or self.has_icbm):
|
||||
self.params.remove("CustomAccIncrementsEnabled")
|
||||
self.params.remove("DynamicExperimentalControl")
|
||||
self.params.remove("SmartCruiseControlVision")
|
||||
self.params.remove("SmartCruiseControlMap")
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ class UIState(UIStateSP):
|
||||
|
||||
# Core state variables
|
||||
self.is_metric: bool = self.params.get_bool("IsMetric")
|
||||
self.is_release = self.params.get_bool("IsReleaseBranch")
|
||||
self.is_release = False # self.params.get_bool("IsReleaseBranch")
|
||||
self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM")
|
||||
self.started: bool = False
|
||||
self.ignition: bool = False
|
||||
|
||||
@@ -1 +1 @@
|
||||
#define SUNNYPILOT_VERSION "2026.001.000"
|
||||
#define SUNNYPILOT_VERSION "2026.001.007"
|
||||
|
||||
@@ -1 +1 @@
|
||||
5d4d21f1899de21137f69d74a4602c44cc5a6b04cf4e4aa9d0ec9206f8c30350
|
||||
32f57bdc91f910df1f48ddae7c59aaf6e751f9df6756da481a210577dbce8bcf
|
||||
@@ -78,6 +78,38 @@ def _bundle_field(bundle: dict | None, key: str) -> str:
|
||||
return bundle.get(key, "") if isinstance(bundle, dict) else ""
|
||||
|
||||
|
||||
def _resolve_brand_capabilities(caps: dict, bundle_platform: str, CP) -> None:
|
||||
"""Set brand-specific capabilities from bundle platform or CarParams fallback.
|
||||
|
||||
Bundle (manual car selection) is a pre-fingerprint approximation.
|
||||
CarParams (auto-fingerprint) is the authoritative post-fingerprint source.
|
||||
Mirrors the per-brand update_settings() logic in device UI layouts.
|
||||
"""
|
||||
brand = caps["brand"]
|
||||
|
||||
if brand == "hyundai":
|
||||
if bundle_platform:
|
||||
try:
|
||||
unsupported = set().union(*UNSUPPORTED_LONGITUDINAL_CAR.values())
|
||||
caps["hyundai_alpha_long_available"] = HYUNDAI_CAR[bundle_platform] not in unsupported
|
||||
except KeyError:
|
||||
cloudlog.exception(f"capabilities: unknown hyundai platform {bundle_platform!r}")
|
||||
elif CP is not None:
|
||||
caps["hyundai_alpha_long_available"] = bool(CP.alphaLongitudinalAvailable)
|
||||
|
||||
elif brand == "subaru":
|
||||
if bundle_platform:
|
||||
try:
|
||||
flags = SUBARU_CAR[bundle_platform].config.flags
|
||||
caps["subaru_has_sng"] = not bool(flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
|
||||
caps["has_stop_and_go"] = caps["subaru_has_sng"]
|
||||
except KeyError:
|
||||
cloudlog.exception(f"capabilities: unknown subaru platform {bundle_platform!r}")
|
||||
elif CP is not None:
|
||||
caps["subaru_has_sng"] = not bool(CP.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
|
||||
caps["has_stop_and_go"] = caps["subaru_has_sng"]
|
||||
|
||||
|
||||
def generate_capabilities(params: Params | None = None) -> dict:
|
||||
"""Generate a SettingsCapabilities dict from CarParams + boolean params.
|
||||
|
||||
@@ -94,7 +126,7 @@ def generate_capabilities(params: Params | None = None) -> dict:
|
||||
|
||||
# Hardware + boolean params (no CarParams dependency)
|
||||
caps["device_type"] = HARDWARE.get_device_type()
|
||||
caps["is_release"] = params.get_bool("IsReleaseBranch")
|
||||
caps["is_release"] = False # params.get_bool("IsReleaseBranch")
|
||||
caps["is_sp_release"] = params.get_bool("IsReleaseSpBranch")
|
||||
caps["is_development"] = params.get_bool("IsDevelopmentBranch")
|
||||
caps["stock_longitudinal"] = params.get_bool("ToyotaEnforceStockLongitudinal")
|
||||
@@ -108,6 +140,7 @@ def generate_capabilities(params: Params | None = None) -> dict:
|
||||
caps["brand"] = bundle_brand
|
||||
|
||||
# CarParams-derived capabilities
|
||||
CP = None
|
||||
CP_bytes = params.get("CarParamsPersistent")
|
||||
if CP_bytes is not None:
|
||||
try:
|
||||
@@ -129,6 +162,7 @@ def generate_capabilities(params: Params | None = None) -> dict:
|
||||
# Generic SnG fallback. Brand-specific opaque flags below override.
|
||||
caps["has_stop_and_go"] = bool(CP.openpilotLongitudinalControl)
|
||||
except Exception:
|
||||
CP = None
|
||||
cloudlog.exception("capabilities: failed to deserialize CarParamsPersistent")
|
||||
|
||||
# CarParamsSP-derived capabilities
|
||||
@@ -142,23 +176,7 @@ def generate_capabilities(params: Params | None = None) -> dict:
|
||||
except Exception:
|
||||
cloudlog.exception("capabilities: failed to deserialize CarParamsSPPersistent")
|
||||
|
||||
# Brand-specific opaque flags. Mirror Raylib brand-settings logic so the
|
||||
# device and the dashboard agree on per-platform availability without
|
||||
# leaking the platform identifier over the wire.
|
||||
if caps["brand"] == "subaru" and bundle_platform:
|
||||
try:
|
||||
flags = SUBARU_CAR[bundle_platform].config.flags
|
||||
caps["subaru_has_sng"] = not bool(flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
|
||||
caps["has_stop_and_go"] = caps["subaru_has_sng"]
|
||||
except KeyError:
|
||||
cloudlog.exception(f"capabilities: unknown subaru platform {bundle_platform!r}")
|
||||
|
||||
if caps["brand"] == "hyundai" and bundle_platform:
|
||||
try:
|
||||
unsupported = set().union(*UNSUPPORTED_LONGITUDINAL_CAR.values())
|
||||
caps["hyundai_alpha_long_available"] = HYUNDAI_CAR[bundle_platform] not in unsupported
|
||||
except KeyError:
|
||||
cloudlog.exception(f"capabilities: unknown hyundai platform {bundle_platform!r}")
|
||||
_resolve_brand_capabilities(caps, bundle_platform, CP)
|
||||
|
||||
return caps
|
||||
|
||||
|
||||
@@ -574,19 +574,9 @@
|
||||
"description": "Let the model decide when to use sunnypilot ACC or sunnypilot End to End Longitudinal.",
|
||||
"visibility": [
|
||||
{
|
||||
"type": "any",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "has_longitudinal_control",
|
||||
"equals": true
|
||||
},
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "has_icbm",
|
||||
"equals": true
|
||||
}
|
||||
]
|
||||
"type": "capability",
|
||||
"field": "has_longitudinal_control",
|
||||
"equals": true
|
||||
}
|
||||
],
|
||||
"enablement": [
|
||||
@@ -1603,47 +1593,47 @@
|
||||
"label": "Always On"
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"value": 5,
|
||||
"label": "5m"
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"value": 10,
|
||||
"label": "10m"
|
||||
},
|
||||
{
|
||||
"value": 3,
|
||||
"value": 15,
|
||||
"label": "15m"
|
||||
},
|
||||
{
|
||||
"value": 4,
|
||||
"value": 30,
|
||||
"label": "30m"
|
||||
},
|
||||
{
|
||||
"value": 5,
|
||||
"value": 60,
|
||||
"label": "1h"
|
||||
},
|
||||
{
|
||||
"value": 6,
|
||||
"value": 120,
|
||||
"label": "2h"
|
||||
},
|
||||
{
|
||||
"value": 7,
|
||||
"value": 180,
|
||||
"label": "3h"
|
||||
},
|
||||
{
|
||||
"value": 8,
|
||||
"value": 300,
|
||||
"label": "5h"
|
||||
},
|
||||
{
|
||||
"value": 9,
|
||||
"value": 600,
|
||||
"label": "10h"
|
||||
},
|
||||
{
|
||||
"value": 10,
|
||||
"value": 1440,
|
||||
"label": "24h"
|
||||
},
|
||||
{
|
||||
"value": 11,
|
||||
"value": 1800,
|
||||
"label": "30h (Default)"
|
||||
}
|
||||
]
|
||||
@@ -1674,13 +1664,13 @@
|
||||
{
|
||||
"id": "updates",
|
||||
"title": "Updates",
|
||||
"description": "Control automatic software updates",
|
||||
"description": "Control software updates",
|
||||
"items": [
|
||||
{
|
||||
"key": "DisableUpdates",
|
||||
"widget": "toggle",
|
||||
"title": "Disable Updates",
|
||||
"description": "When enabled, automatic software updates will be off. This requires a reboot to take effect.",
|
||||
"description": "When enabled, software updates will be off. This requires a reboot to take effect.",
|
||||
"enablement": [
|
||||
{
|
||||
"type": "offroad_only"
|
||||
@@ -1731,26 +1721,6 @@
|
||||
"key": "JoystickDebugMode",
|
||||
"widget": "toggle",
|
||||
"title": "Joystick Debug Mode",
|
||||
"visibility": [
|
||||
{
|
||||
"type": "not",
|
||||
"condition": {
|
||||
"type": "any",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "is_release",
|
||||
"equals": true
|
||||
},
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "is_sp_release",
|
||||
"equals": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"enablement": [
|
||||
{
|
||||
"type": "offroad_only"
|
||||
@@ -1775,19 +1745,9 @@
|
||||
{
|
||||
"type": "not",
|
||||
"condition": {
|
||||
"type": "any",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "is_release",
|
||||
"equals": true
|
||||
},
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "is_sp_release",
|
||||
"equals": true
|
||||
}
|
||||
]
|
||||
"type": "capability",
|
||||
"field": "has_icbm",
|
||||
"equals": true
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1900,19 +1860,9 @@
|
||||
{
|
||||
"type": "not",
|
||||
"condition": {
|
||||
"type": "any",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "is_release",
|
||||
"equals": true
|
||||
},
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "is_sp_release",
|
||||
"equals": true
|
||||
}
|
||||
]
|
||||
"type": "capability",
|
||||
"field": "is_sp_release",
|
||||
"equals": true
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -1947,11 +1897,6 @@
|
||||
"condition": {
|
||||
"type": "any",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "is_release",
|
||||
"equals": true
|
||||
},
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "is_sp_release",
|
||||
|
||||
@@ -59,12 +59,7 @@ macros:
|
||||
- type: not
|
||||
condition: {type: capability, field: tesla_has_vehicle_bus, equals: true}
|
||||
|
||||
# Hide everything but a clearly-marked release branch (matches Raylib
|
||||
# _is_release_branch = is_release OR is_sp_release).
|
||||
# Hide on sunnypilot release branches (is_release is hardcoded False everywhere; is_sp_release is the active gate).
|
||||
release_branches_hide:
|
||||
- type: not
|
||||
condition:
|
||||
type: any
|
||||
conditions:
|
||||
- {type: capability, field: is_release, equals: true}
|
||||
- {type: capability, field: is_sp_release, equals: true}
|
||||
condition: {type: capability, field: is_sp_release, equals: true}
|
||||
|
||||
@@ -21,14 +21,7 @@ sections:
|
||||
title: Dynamic Experimental Control
|
||||
description: Let the model decide when to use sunnypilot ACC or sunnypilot End to End Longitudinal.
|
||||
visibility:
|
||||
- type: any
|
||||
conditions:
|
||||
- type: capability
|
||||
field: has_longitudinal_control
|
||||
equals: true
|
||||
- type: capability
|
||||
field: has_icbm
|
||||
equals: true
|
||||
- $ref: '#/macros/longitudinal'
|
||||
enablement:
|
||||
- $ref: '#/macros/longitudinal'
|
||||
- key: DisengageOnAccelerator
|
||||
|
||||
@@ -26,8 +26,6 @@ sections:
|
||||
- key: JoystickDebugMode
|
||||
widget: toggle
|
||||
title: Joystick Debug Mode
|
||||
visibility:
|
||||
- $ref: '#/macros/release_branches_hide'
|
||||
enablement:
|
||||
- $ref: '#/macros/offroad'
|
||||
- key: AlphaLongitudinalEnabled
|
||||
@@ -46,14 +44,9 @@ sections:
|
||||
equals: true
|
||||
- type: not
|
||||
condition:
|
||||
type: any
|
||||
conditions:
|
||||
- type: capability
|
||||
field: is_release
|
||||
equals: true
|
||||
- type: capability
|
||||
field: is_sp_release
|
||||
equals: true
|
||||
type: capability
|
||||
field: has_icbm
|
||||
equals: true
|
||||
enablement:
|
||||
- $ref: '#/macros/not_engaged'
|
||||
- key: ShowDebugInfo
|
||||
@@ -131,9 +124,6 @@ sections:
|
||||
condition:
|
||||
type: any
|
||||
conditions:
|
||||
- type: capability
|
||||
field: is_release
|
||||
equals: true
|
||||
- type: capability
|
||||
field: is_sp_release
|
||||
equals: true
|
||||
|
||||
@@ -37,27 +37,27 @@ sections:
|
||||
options:
|
||||
- value: 0
|
||||
label: Always On
|
||||
- value: 1
|
||||
label: 5m
|
||||
- value: 2
|
||||
label: 10m
|
||||
- value: 3
|
||||
label: 15m
|
||||
- value: 4
|
||||
label: 30m
|
||||
- value: 5
|
||||
label: 1h
|
||||
- value: 6
|
||||
label: 2h
|
||||
- value: 7
|
||||
label: 3h
|
||||
- value: 8
|
||||
label: 5h
|
||||
- value: 9
|
||||
label: 10h
|
||||
label: 5m
|
||||
- value: 10
|
||||
label: 10m
|
||||
- value: 15
|
||||
label: 15m
|
||||
- value: 30
|
||||
label: 30m
|
||||
- value: 60
|
||||
label: 1h
|
||||
- value: 120
|
||||
label: 2h
|
||||
- value: 180
|
||||
label: 3h
|
||||
- value: 300
|
||||
label: 5h
|
||||
- value: 600
|
||||
label: 10h
|
||||
- value: 1440
|
||||
label: 24h
|
||||
- value: 11
|
||||
- value: 1800
|
||||
label: 30h (Default)
|
||||
- id: language
|
||||
title: Language
|
||||
|
||||
@@ -9,12 +9,12 @@ description: Software update preferences
|
||||
sections:
|
||||
- id: updates
|
||||
title: Updates
|
||||
description: Control automatic software updates
|
||||
description: Control software updates
|
||||
items:
|
||||
- key: DisableUpdates
|
||||
widget: toggle
|
||||
title: Disable Updates
|
||||
description: When enabled, automatic software updates will be off. This requires a reboot to take effect.
|
||||
description: When enabled, software updates will be off. This requires a reboot to take effect.
|
||||
enablement:
|
||||
- $ref: '#/macros/offroad'
|
||||
- $ref: '#/macros/advanced_only'
|
||||
|
||||
@@ -15,6 +15,7 @@ compiled output once the compiler has produced it.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import difflib
|
||||
import json
|
||||
import os
|
||||
|
||||
@@ -44,7 +45,16 @@ def committed() -> dict:
|
||||
class TestRoundtrip:
|
||||
def test_compiled_matches_committed(self, compiled, committed):
|
||||
"""Compiled output must match the checked-in JSON."""
|
||||
assert compiled == committed
|
||||
if compiled == committed:
|
||||
return
|
||||
diff = "\n".join(difflib.unified_diff(
|
||||
json.dumps(committed, indent=2).splitlines(),
|
||||
json.dumps(compiled, indent=2).splitlines(),
|
||||
fromfile="settings_ui.json (committed)",
|
||||
tofile="settings_ui.json (freshly compiled)",
|
||||
lineterm="",
|
||||
))
|
||||
pytest.fail(f"settings_ui.json schema mismatch — run compile_settings_ui.py\n\n{diff}")
|
||||
|
||||
def test_committed_file_is_canonical(self):
|
||||
"""Compiled output must byte-match the checked-in file (including trailing newline).
|
||||
@@ -53,7 +63,16 @@ class TestRoundtrip:
|
||||
rendered = json.dumps(schema, indent=2) + "\n"
|
||||
with open(DEFAULT_OUT) as f:
|
||||
current = f.read()
|
||||
assert current == rendered, "settings_ui.json out of sync — run compile_settings_ui.py"
|
||||
if current == rendered:
|
||||
return
|
||||
diff = "\n".join(difflib.unified_diff(
|
||||
current.splitlines(),
|
||||
rendered.splitlines(),
|
||||
fromfile="settings_ui.json (on disk)",
|
||||
tofile="settings_ui.json (freshly compiled)",
|
||||
lineterm="",
|
||||
))
|
||||
pytest.fail(f"settings_ui.json out of sync — run compile_settings_ui.py\n\n{diff}")
|
||||
|
||||
|
||||
class TestRefResolution:
|
||||
|
||||
@@ -181,17 +181,14 @@ class TestTorqueOptionGeneration:
|
||||
|
||||
class TestReleaseBranchGates:
|
||||
@pytest.mark.parametrize("key", [
|
||||
"JoystickDebugMode",
|
||||
"AlphaLongitudinalEnabled",
|
||||
"EnableGithubRunner",
|
||||
"QuickBootToggle",
|
||||
])
|
||||
def test_sp_dev_items_gate_on_is_sp_release(self, schema, key):
|
||||
"""SP dev items must hide on either release branch (is_release OR is_sp_release)."""
|
||||
"""sunnypilot dev items must hide on sunnypilot release branches (is_sp_release gate)."""
|
||||
item = _find_item(schema, key)
|
||||
assert item is not None, f"{key} not found in schema"
|
||||
rules = (item.get("visibility") or []) + (item.get("enablement") or [])
|
||||
assert _references_capability_field(rules, "is_release"), f"{key} missing is_release gate"
|
||||
assert _references_capability_field(rules, "is_sp_release"), f"{key} missing is_sp_release gate"
|
||||
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ def manager_init() -> None:
|
||||
params.clear_all(ParamKeyFlag.CLEAR_ON_ONROAD_TRANSITION)
|
||||
params.clear_all(ParamKeyFlag.CLEAR_ON_OFFROAD_TRANSITION)
|
||||
params.clear_all(ParamKeyFlag.CLEAR_ON_IGNITION_ON)
|
||||
if build_metadata.release_channel:
|
||||
params.clear_all(ParamKeyFlag.DEVELOPMENT_ONLY)
|
||||
# if build_metadata.release_channel:
|
||||
# params.clear_all(ParamKeyFlag.DEVELOPMENT_ONLY)
|
||||
|
||||
# device boot mode
|
||||
if params.get("DeviceBootMode") == 1: # start in Always Offroad mode
|
||||
|
||||
Reference in New Issue
Block a user