Merge branch 'master' into hkg-angle-steering-2025

# Conflicts:
#	opendbc_repo
This commit is contained in:
DevTekVE
2026-04-03 14:00:54 +02:00
162 changed files with 19755 additions and 9005 deletions
+45
View File
@@ -0,0 +1,45 @@
name: diff report
on:
pull_request_target:
types: [opened, synchronize, reopened]
jobs:
comment:
name: comment
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
pull-requests: write
actions: read
steps:
- name: Wait for process replay
id: wait
continue-on-error: true
uses: lewagon/wait-on-check-action@v1.3.4
with:
ref: ${{ github.event.pull_request.head.sha }}
check-name: process replay
repo-token: ${{ secrets.GITHUB_TOKEN }}
allowed-conclusions: success,failure
wait-interval: 20
- name: Download diff
if: steps.wait.outcome == 'success'
uses: dawidd6/action-download-artifact@v6
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: tests.yaml
workflow_conclusion: ''
pr: ${{ github.event.number }}
name: diff_report_${{ github.event.number }}
path: .
allow_forks: true
- name: Comment on PR
if: steps.wait.outcome == 'success'
uses: thollander/actions-comment-pull-request@v2
with:
filePath: diff_report.txt
comment_tag: diff_report
pr_number: ${{ github.event.number }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-22
View File
@@ -9,28 +9,6 @@ env:
PYTHONPATH: ${{ github.workspace }}
jobs:
update_translations:
runs-on: ubuntu-latest
if: github.repository == 'sunnypilot/sunnypilot'
steps:
- uses: actions/checkout@v6
with:
submodules: true
- run: ./tools/op.sh setup
- name: Update translations
run: python3 selfdrive/ui/update_translations.py --vanish
- name: Create Pull Request
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0
with:
author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
commit-message: "Update translations"
title: "[bot] Update translations"
body: "Automatic PR from repo-maintenance -> update_translations"
branch: "update-translations"
base: "master"
delete-branch: true
labels: bot
package_updates:
name: package_updates
runs-on: ubuntu-latest
+10
View File
@@ -156,12 +156,22 @@ jobs:
id: print-diff
if: always()
run: cat selfdrive/test/process_replay/diff.txt
- name: Print diff report
if: always()
run: cat selfdrive/test/process_replay/diff_report.txt
- uses: actions/upload-artifact@v6
if: always()
continue-on-error: true
with:
name: process_replay_diff.txt
path: selfdrive/test/process_replay/diff.txt
- name: Upload diff report
uses: actions/upload-artifact@v6
if: always() && github.event_name == 'pull_request'
continue-on-error: true
with:
name: diff_report_${{ github.event.number }}
path: selfdrive/test/process_replay/diff_report.txt
- name: Checkout ci-artifacts
if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master'
uses: actions/checkout@v4
+2 -1
View File
@@ -1,7 +1,8 @@
Version 0.11.1 (2026-04-08)
Version 0.11.1 (2026-04-22)
========================
* New driver monitoring model
* Improved image processing pipeline for driver camera
* Rivian R1S and R1T 2025 support thanks to lukasloetkolben!
Version 0.11.0 (2026-03-17)
========================
+9 -3
View File
@@ -47,7 +47,8 @@ pkgs = [importlib.import_module(name) for name in pkg_names]
# be distributed with all Linux distros and macOS, or
# vendored in commaai/dependencies.
allowed_system_libs = {
"EGL", "GLESv2", "GL", "Qt5Charts", "Qt5Core", "Qt5Gui", "Qt5Widgets",
"EGL", "GLESv2", "GL",
"Qt5Charts", "Qt5Core", "Qt5Gui", "Qt5Widgets",
"dl", "drm", "gbm", "m", "pthread",
}
@@ -256,8 +257,13 @@ SConscript([
SConscript(['sunnypilot/SConscript'])
if Dir('#tools/cabana/').exists() and arch != "larch64":
SConscript(['tools/cabana/SConscript'])
# Build tools
if arch != "larch64":
SConscript([
'tools/replay/SConscript',
'tools/cabana/SConscript',
'tools/jotpluggler/SConscript',
])
env.CompilationDatabase('compile_commands.json')
+1
View File
@@ -154,6 +154,7 @@ struct ModelManagerSP @0xaedffd8f31e7b55d {
vision @2;
policy @3;
offPolicy @4;
onPolicy @5;
}
}
+16 -5
View File
@@ -88,6 +88,7 @@ struct OnroadEvent @0xc4fa6047f024e718 {
lowMemory @51;
stockAeb @52;
stockLkas @98;
lateralManeuver @99;
ldw @53;
carUnrecognized @54;
invalidLkasSetting @55;
@@ -1241,6 +1242,10 @@ struct DriverAssistance {
# FCW, AEB, etc. will go here
}
struct LateralManeuverPlan {
desiredCurvature @0 :Float32; # 1/m
}
struct LongitudinalPlan @0xe00b5b3eba12876c {
modelMonoTime @9 :UInt64;
hasLead @7 :Bool;
@@ -1426,6 +1431,8 @@ struct LivePose {
posenetOK @5 :Bool = false;
sensorsOK @6 :Bool = false;
timestamp @8 :UInt64;
debugFilterState @7 :FilterState;
struct XYZMeasurement {
@@ -2169,12 +2176,14 @@ struct DriverStateV2 {
facePosition @2 :List(Float32);
facePositionStd @3 :List(Float32);
faceProb @4 :Float32;
leftEyeProb @5 :Float32;
rightEyeProb @6 :Float32;
leftBlinkProb @7 :Float32;
rightBlinkProb @8 :Float32;
sunglassesProb @9 :Float32;
eyesVisibleProb @14 :Float32;
eyesClosedProb @15 :Float32;
phoneProb @13 :Float32;
leftEyeProbDEPRECATED @5 :Float32;
rightEyeProbDEPRECATED @6 :Float32;
leftBlinkProbDEPRECATED @7 :Float32;
rightBlinkProbDEPRECATED @8 :Float32;
sunglassesProbDEPRECATED @9 :Float32;
notReadyProbDEPRECATED @12 :List(Float32);
occludedProbDEPRECATED @10 :Float32;
readyProbDEPRECATED @11 :List(Float32);
@@ -2610,6 +2619,8 @@ struct Event {
bookmarkButton @148 :UserBookmark;
audioFeedback @149 :AudioFeedback;
lateralManeuverPlan @150 :LateralManeuverPlan;
# *********** debug ***********
testJoystick @52 :Joystick;
roadEncodeData @86 :EncodeData;
+1
View File
@@ -49,6 +49,7 @@ _services: dict[str, tuple] = {
"carControl": (True, 100., 10),
"carOutput": (True, 100., 10),
"longitudinalPlan": (True, 20., 10),
"lateralManeuverPlan": (True, 20.),
"driverAssistance": (True, 20., 20),
"procLog": (True, 0.5, 15, QueueSize.BIG),
"gpsLocationExternal": (True, 10., 10),
+1 -1
View File
@@ -1 +1 @@
#define DEFAULT_MODEL "CD210 (Default)"
#define DEFAULT_MODEL "OP Model (Default)"
+2 -1
View File
@@ -82,6 +82,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"LiveParametersV2", {PERSISTENT, BYTES}},
{"LiveTorqueParameters", {PERSISTENT | DONT_LOG, BYTES}},
{"LocationFilterInitialState", {PERSISTENT, BYTES}},
{"LateralManeuverMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"LongitudinalManeuverMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"LongitudinalPersonality", {PERSISTENT | BACKUP, INT, std::to_string(static_cast<int>(cereal::LongitudinalPersonality::STANDARD))}},
{"NetworkMetered", {PERSISTENT | BACKUP, BOOL}},
@@ -271,7 +272,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"EnforceTorqueControl", {PERSISTENT | BACKUP, BOOL}},
{"LiveTorqueParamsToggle", {PERSISTENT | BACKUP , BOOL}},
{"LiveTorqueParamsRelaxedToggle", {PERSISTENT | BACKUP , BOOL}},
{"TorqueControlTune", {PERSISTENT | BACKUP, FLOAT}},
{"TorqueControlTune", {PERSISTENT | BACKUP, FLOAT, "0.0"}},
{"TorqueParamsOverrideEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
{"TorqueParamsOverrideFriction", {PERSISTENT | BACKUP, FLOAT, "0.1"}},
{"TorqueParamsOverrideLatAccelFactor", {PERSISTENT | BACKUP, FLOAT, "2.5"}},
+1 -1
View File
@@ -39,7 +39,7 @@ All of these are examples of good PRs:
### First contribution
[Projects / openpilot bounties](https://github.com/orgs/commaai/projects/26/views/1?pane=info) is the best place to get started and goes in-depth on what's expected when working on a bounty.
There are a lot of bounties that don't require a comma 3X or a car.
There are a lot of bounties that don't require a comma four or a car.
## Pull Requests
+1 -1
View File
@@ -6,4 +6,4 @@
* **segment**: routes are split into one minute chunks called segments.
* **comma connect**: the web viewer for all your routes; check it out at [connect.comma.ai](https://connect.comma.ai).
* **panda**: this is the secondary processor on the device that implements the functional safety and directly talks to the car over CAN. See the [panda repo](https://github.com/commaai/panda).
* **comma 3X**: the latest hardware by comma.ai for running openpilot. more info at [comma.ai/shop](https://comma.ai/shop).
* **comma four**: the latest hardware by comma.ai for running openpilot. more info at [comma.ai/shop/comma-four](https://www.comma.ai/shop/comma-four).
+1 -1
View File
@@ -5,7 +5,7 @@
## How do I use it?
openpilot is designed to be used on the comma 3X.
openpilot is designed to be used on the comma four.
## How does it work?
+6 -6
View File
@@ -1,15 +1,15 @@
# connect to a comma 3X
# connect to a comma four
A comma 3X is a normal [Linux](https://github.com/commaai/agnos-builder) computer that exposes [SSH](https://wiki.archlinux.org/title/Secure_Shell) and a [serial console](https://wiki.archlinux.org/title/Working_with_the_serial_console).
A comma four is a normal [Linux](https://github.com/commaai/agnos-builder) computer that exposes [SSH](https://wiki.archlinux.org/title/Secure_Shell) and a [serial console](https://wiki.archlinux.org/title/Working_with_the_serial_console).
## Serial Console
On both the comma three and 3X, the serial console is accessible from the main OBD-C port.
Connect the comma 3X to your computer with a normal USB C cable, or use a [comma serial](https://comma.ai/shop/comma-serial) for steady 12V power.
On both the comma three and comma four, the serial console is accessible from the main OBD-C port.
Connect the comma four to your computer with a normal USB C cable, or use a [comma serial](https://comma.ai/shop/comma-serial) for steady 12V power.
On the comma three, the serial console is exposed through a UART-to-USB chip, and `tools/scripts/serial.sh` can be used to connect.
On the comma 3X, the serial console is accessible through the [panda](https://github.com/commaai/panda) using the `panda/tests/som_debug.sh` script.
On the comma four, the serial console is accessible through the [panda](https://github.com/commaai/panda) using the `panda/tests/som_debug.sh` script.
* Username: `comma`
* Password: `comma`
@@ -45,7 +45,7 @@ In order to use ADB on your device, you'll need to perform the following steps u
* Here's an example command for connecting to your device using its tethered connection: `adb connect 192.168.43.1:5555`
> [!NOTE]
> The default port for ADB is 5555 on the comma 3X.
> The default port for ADB is 5555 on the comma four.
For more info on ADB, see the [Android Debug Bridge (ADB) documentation](https://developer.android.com/tools/adb).
+1 -1
View File
@@ -8,7 +8,7 @@ Replaying is a critical tool for openpilot development and debugging.
Just run `tools/replay/replay --demo`.
## Replaying CAN data
*Hardware required: jungle and comma 3X*
*Hardware required: jungle and comma four*
1. Connect your PC to a jungle.
2.
+1 -1
View File
@@ -3,7 +3,7 @@
In 30 minutes, we'll get an openpilot development environment set up on your computer and make some changes to openpilot's UI.
And if you have a comma 3X, we'll deploy the change to your device for testing.
And if you have a comma four, we'll deploy the change to your device for testing.
## 1. Set up your development environment
+1
View File
@@ -7,6 +7,7 @@ source "$DIR/launch_env.sh"
function agnos_init {
# TODO: move this to agnos
sudo rm -f /data/etc/NetworkManager/system-connections/*.nmmeta
rm -f /data/scons_cache/config.lock
# set success flag for current boot slot
sudo abctl --set_success
+1 -1
Submodule panda updated: 6ddc631bdd...01a2c250f9
+1
View File
@@ -107,6 +107,7 @@ dev = [
]
tools = [
"imgui @ git+https://github.com/commaai/dependencies.git@release-imgui#subdirectory=imgui",
"metadrive-simulator @ git+https://github.com/commaai/metadrive.git@minimal ; (platform_machine != 'aarch64')",
]
+1 -1
View File
@@ -104,7 +104,7 @@ def generate_metadata(model_path: Path, output_dir: Path, short_name: str):
metadata_file = metadata_file.rename(output_path / f"{base}_{short_name.lower()}_metadata.pkl")
# Build the metadata structure
model_type = "offPolicy" if "off_policy" in base else base.split("_")[-1]
model_type = "offPolicy" if "off_policy" in base else "onPolicy" if "on_policy" in base else base.split("_")[-1]
model_metadata = {
"type": model_type,
+7 -2
View File
@@ -38,6 +38,11 @@ if __name__ == "__main__":
continue
fn = os.path.basename(f)
master = get_checkpoint(MASTER_PATH + MODEL_PATH + fn)
master_path = MASTER_PATH + MODEL_PATH + fn
if os.path.exists(master_path):
master = get_checkpoint(master_path)
master_col = f"[{master}](https://reporter.comma.life/experiment/{master})"
else:
master_col = "N/A (new model)"
pr = get_checkpoint(BASEDIR + MODEL_PATH + fn)
print("|", fn, "|", f"[{master}](https://reporter.comma.life/experiment/{master})", "|", f"[{pr}](https://reporter.comma.life/experiment/{pr})", "|")
print("|", fn, "|", master_col, "|", f"[{pr}](https://reporter.comma.life/experiment/{pr})", "|")
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:56de402482b5987ed9a0ff3f793a1c89f857304b34fbb8a3deb5b5d4a332be1c
size 3688
+1 -2
View File
@@ -98,7 +98,6 @@ class Car:
break
alpha_long_allowed = self.params.get_bool("AlphaLongitudinalEnabled")
num_pandas = len(messaging.recv_one_retry(self.sm.sock['pandaStates']).pandaStates)
cached_params = None
cached_params_raw = self.params.get("CarParamsCache")
@@ -109,7 +108,7 @@ class Car:
fixed_fingerprint = (self.params.get("CarPlatformBundle") or {}).get("platform", None)
init_params_list_sp = sunnypilot_interfaces.initialize_params(self.params)
self.CI = get_car(*self.can_callbacks, obd_callback(self.params), alpha_long_allowed, is_release, num_pandas, cached_params,
self.CI = get_car(*self.can_callbacks, obd_callback(self.params), alpha_long_allowed, is_release, cached_params,
fixed_fingerprint, init_params_list_sp, is_release_sp)
sunnypilot_interfaces.setup_interfaces(self.CI, self.params)
self.RI = interfaces[self.CI.CP.carFingerprint].RadarInterface(self.CI.CP, self.CI.CP_SP)
+5 -2
View File
@@ -42,7 +42,7 @@ class Controls(ControlsExt):
self.CI = interfaces[self.CP.carFingerprint](self.CP, self.CP_SP)
self.sm = messaging.SubMaster(['liveDelay', 'liveParameters', 'liveTorqueParameters', 'modelV2', 'selfdriveState',
'liveCalibration', 'livePose', 'longitudinalPlan', 'carState', 'carOutput',
'liveCalibration', 'livePose', 'longitudinalPlan', 'lateralManeuverPlan', 'carState', 'carOutput',
'driverMonitoringState', 'onroadEvents', 'driverAssistance', 'liveDelay'] + self.sm_services_ext,
poll='selfdriveState')
self.pm = messaging.PubMaster(['carControl', 'controlsState'] + self.pm_services_ext)
@@ -135,7 +135,10 @@ class Controls(ControlsExt):
# Steering PID loop and lateral MPC
# Reset desired curvature to current to avoid violating the limits on engage
new_desired_curvature = model_v2.action.desiredCurvature if CC.latActive else self.curvature
if self.sm.valid['lateralManeuverPlan']:
new_desired_curvature = self.sm['lateralManeuverPlan'].desiredCurvature if CC.latActive else self.curvature
else:
new_desired_curvature = model_v2.action.desiredCurvature if CC.latActive else self.curvature
self.desired_curvature, curvature_limited = clip_curvature(CS.vEgo, self.desired_curvature, new_desired_curvature, lp.roll)
lat_delay = self.sm["liveDelay"].lateralDelay + LAT_SMOOTH_SECONDS
+1 -1
View File
@@ -11,7 +11,7 @@ class LatControlAngle(LatControl):
def __init__(self, CP, CP_SP, CI, dt):
super().__init__(CP, CP_SP, CI, dt)
self.sat_check_min_speed = 5.
self.use_steer_limited_by_safety = CP.brand == "tesla"
self.use_steer_limited_by_safety = CP.brand in ("tesla", "hyundai")
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited, lat_delay):
angle_log = log.ControlsState.LateralAngleState.new_message()
@@ -50,8 +50,10 @@ def simulate_straight_road_msgs(est):
lat_accels = TORQUE_TUNE.latAccelFactor * steer_torques
for t, steer_torque, lat_accel in zip(ts, steer_torques, lat_accels, strict=True):
carOutput.actuatorsOutput.torque = float(-steer_torque)
livePose.orientationNED.x = float(np.deg2rad(ROLL_BIAS_DEG))
livePose.angularVelocityDevice.z = float(lat_accel / V_EGO)
livePose.orientationNED = {'x': float(np.deg2rad(ROLL_BIAS_DEG)), 'valid': True}
livePose.angularVelocityDevice = {'z': float(lat_accel / V_EGO), 'valid': True}
livePose.inputsOK, livePose.sensorsOK, livePose.posenetOK = True, True, True
livePose.timestamp = int(t * 1e9)
for which, msg in (('carControl', carControl), ('carOutput', carOutput), ('carState', carState), ('livePose', livePose)):
est.handle_log(t, which, msg)
+1 -3
View File
@@ -45,8 +45,6 @@ if __name__ == "__main__":
extra[(Ecu.unknown, 0x750, i)] = []
extra = {"any": {"debug": extra}}
num_pandas = len(messaging.recv_one_retry(pandaStates_sock).pandaStates)
t = time.monotonic()
print("Getting vin...")
set_obd_multiplexing(True)
@@ -56,7 +54,7 @@ if __name__ == "__main__":
print()
t = time.monotonic()
fw_vers = get_fw_versions(*can_callbacks, set_obd_multiplexing, query_brand=args.brand, extra=extra, num_pandas=num_pandas, progress=True)
fw_vers = get_fw_versions(*can_callbacks, set_obd_multiplexing, query_brand=args.brand, extra=extra, progress=True)
_, candidates = match_fw_to_car(fw_vers, vin)
print()
@@ -44,7 +44,7 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser(description='View back and forth ISO-TP communication between various ECUs given an address')
parser.add_argument('route', nargs='?', help='Route name, live if not specified')
parser.add_argument('--addrs', nargs='*', default=[], help='List of tx address to view (0x7e0 for engine)')
parser.add_argument('--rxoffset', default='')
parser.add_argument('--rxoffset', default='0x8')
args = parser.parse_args()
addrs = [int(addr, base=16) if addr.startswith('0x') else int(addr) for addr in args.addrs]
+8 -2
View File
@@ -28,6 +28,9 @@ INPUT_INVALID_LIMIT = 2.0 # 1 (camodo) / 9 (sensor) bad input[s] ignored
INPUT_INVALID_RECOVERY = 10.0 # ~10 secs to resume after exceeding allowed bad inputs by one
POSENET_STD_INITIAL_VALUE = 10.0
POSENET_STD_HIST_HALF = 20
CAM_ODO_POSE_DELAY = 0.1 # dependent on the vision model context frames and temporal frequency (current model is 5 fps with 2 context frames)
CAM_ODO_ROT_STD_MULT = 10
CAM_ODO_TRANS_STD_MULT = 4
def calculate_invalid_input_decay(invalid_limit, recovery_time, frequency):
@@ -155,6 +158,8 @@ class LocationEstimator:
self.device_from_calib = rot_from_euler(calib)
elif which == "cameraOdometry":
# camera odometry is delayed depending on the model context frames and temporal frequency
t = msg.timestampEof * 1e-9 - CAM_ODO_POSE_DELAY
if not self._validate_timestamp(t):
return HandleLogResult.TIMING_INVALID
@@ -177,8 +182,8 @@ class LocationEstimator:
self.posenet_stds[-1] = trans_calib_std[0]
# Multiply by N to avoid to high certainty in kalman filter because of temporally correlated noise
rot_calib_std *= 10
trans_calib_std *= 2
rot_calib_std *= CAM_ODO_ROT_STD_MULT
trans_calib_std *= CAM_ODO_TRANS_STD_MULT
rot_device_std = rotate_std(self.device_from_calib, rot_calib_std)
trans_device_std = rotate_std(self.device_from_calib, trans_calib_std)
@@ -234,6 +239,7 @@ class LocationEstimator:
livePose.inputsOK = inputs_valid
livePose.posenetOK = not std_spike or self.car_speed <= 5.0
livePose.sensorsOK = sensors_valid
livePose.timestamp = int(np.nan_to_num(self.kf.t) * 1e9)
return msg
+2 -2
View File
@@ -47,13 +47,13 @@ class PoseKalman(KalmanFilter):
# process noise
Q = np.diag([0.001**2, 0.001**2, 0.001**2,
0.01**2, 0.01**2, 0.01**2,
0.1**2, 0.1**2, 0.1**2,
0.085**2, 0.085**2, 0.085**2,
(0.005 / 100)**2, (0.005 / 100)**2, (0.005 / 100)**2,
3**2, 3**2, 3**2,
0.005**2, 0.005**2, 0.005**2])
obs_noise = {ObservationKind.PHONE_GYRO: np.diag([0.025**2, 0.025**2, 0.025**2]),
ObservationKind.PHONE_ACCEL: np.diag([.5**2, .5**2, .5**2]),
ObservationKind.PHONE_ACCEL: np.diag([0.75**2, 0.75**2, 0.75**2]),
ObservationKind.CAMERA_ODO_TRANSLATION: np.diag([0.5**2, 0.5**2, 0.5**2]),
ObservationKind.CAMERA_ODO_ROTATION: np.diag([0.05**2, 0.05**2, 0.05**2])}
+1
View File
@@ -65,6 +65,7 @@ class VehicleParamsLearner:
def handle_log(self, t: float, which: str, msg: capnp._DynamicStructReader):
if which == 'livePose':
t = msg.timestamp * 1e-9
device_pose = Pose.from_live_pose(msg)
calibrated_pose = self.calibrator.build_calibrated_pose(device_pose)
@@ -3,6 +3,7 @@ from collections import defaultdict
from enum import Enum
from openpilot.tools.lib.logreader import LogReader
from openpilot.selfdrive.locationd.lagd import masked_symmetric_moving_average
from openpilot.selfdrive.test.process_replay.migration import migrate_all
from openpilot.selfdrive.test.process_replay.process_replay import replay_process_with_name
@@ -15,6 +16,7 @@ SELECT_COMPARE_FIELDS = {
'inputs_flag': ['inputsOK'],
'sensors_flag': ['sensorsOK'],
}
SMOOTH_FIELDS = ['yaw_rate', 'roll']
JUNK_IDX = 100
CONSISTENT_SPIKES_COUNT = 10
@@ -32,6 +34,8 @@ class Scenario(Enum):
def get_select_fields_data(logs):
def sig_smooth(signal):
return masked_symmetric_moving_average(signal, np.ones_like(signal), 5, 1.0)
def get_nested_keys(msg, keys):
val = None
for key in keys:
@@ -44,6 +48,8 @@ def get_select_fields_data(logs):
data[key].append(get_nested_keys(msg, fields))
for key in data:
data[key] = np.array(data[key][JUNK_IDX:], dtype=float)
if key in SMOOTH_FIELDS:
data[key] = sig_smooth(data[key])
return data
@@ -110,7 +116,7 @@ class TestLocationdScenarios:
"""
orig_data, replayed_data = run_scenarios(Scenario.BASE, self.logs)
assert np.allclose(orig_data['yaw_rate'], replayed_data['yaw_rate'], atol=np.radians(0.35))
assert np.allclose(orig_data['roll'], replayed_data['roll'], atol=np.radians(0.55))
assert np.allclose(orig_data['roll'], replayed_data['roll'], atol=np.radians(0.35))
def test_gyro_off(self):
"""
@@ -135,7 +141,7 @@ class TestLocationdScenarios:
"""
orig_data, replayed_data = run_scenarios(Scenario.GYRO_SPIKE_MIDWAY, self.logs)
assert np.allclose(orig_data['yaw_rate'], replayed_data['yaw_rate'], atol=np.radians(0.35))
assert np.allclose(orig_data['roll'], replayed_data['roll'], atol=np.radians(0.55))
assert np.allclose(orig_data['roll'], replayed_data['roll'], atol=np.radians(0.35))
assert np.all(replayed_data['inputs_flag'] == orig_data['inputs_flag'])
assert np.all(replayed_data['sensors_flag'] == orig_data['sensors_flag'])
@@ -169,7 +175,7 @@ class TestLocationdScenarios:
"""
orig_data, replayed_data = run_scenarios(Scenario.ACCEL_SPIKE_MIDWAY, self.logs)
assert np.allclose(orig_data['yaw_rate'], replayed_data['yaw_rate'], atol=np.radians(0.35))
assert np.allclose(orig_data['roll'], replayed_data['roll'], atol=np.radians(0.55))
assert np.allclose(orig_data['roll'], replayed_data['roll'], atol=np.radians(0.35))
def test_single_timing_spike(self):
"""
+3 -1
View File
@@ -188,7 +188,9 @@ class TorqueEstimator(ParameterEstimator, TorqueEstimatorExt):
self.lag = get_lat_delay(self.params, msg.lateralDelay)
# calculate lateral accel from past steering torque
elif which == "livePose":
if len(self.raw_points['steer_torque']) == self.hist_len:
is_valid = msg.angularVelocityDevice.valid and msg.orientationNED.valid and msg.inputsOK and msg.sensorsOK and msg.posenetOK
if len(self.raw_points['steer_torque']) == self.hist_len and is_valid:
t = msg.timestamp * 1e-9
device_pose = Pose.from_live_pose(msg)
calibrated_pose = self.calibrator.build_calibrated_pose(device_pose)
angular_velocity_calibrated = calibrated_pose.angular_velocity
+2 -16
View File
@@ -21,7 +21,7 @@ tg_flags = {
}.get(arch, 'DEV=CPU CPU_LLVM=1 THREADS=0 IMAGE=0')
# Get model metadata
for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
for model_name in ['driving_vision', 'driving_off_policy', 'driving_on_policy', 'dmonitoring_model']:
fn = File(f"models/{model_name}").abspath
script_files = [File(Dir("#selfdrive/modeld").File("get_model_metadata.py").abspath)]
cmd = f'{tg_flags} python3 {Dir("#selfdrive/modeld").abspath}/get_model_metadata.py {fn}.onnx'
@@ -59,19 +59,5 @@ def tg_compile(flags, model_name):
)
# Compile small models
for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
for model_name in ['driving_vision', 'driving_off_policy', 'driving_on_policy', 'dmonitoring_model']:
tg_compile(tg_flags, model_name)
# Compile BIG model if USB GPU is available
if "USBGPU" in os.environ:
import subprocess
# because tg doesn't support multi-process
devs = subprocess.check_output('python3 -c "from tinygrad import Device; print(list(Device.get_available_devices()))"', shell=True, cwd=env.Dir('#').abspath)
if b"AMD" in devs:
print("USB GPU detected... building")
flags = "DEV=AMD AMD_IFACE=USB AMD_LLVM=1 NOLOCALS=0 IMAGE=0"
bp = tg_compile(flags, "big_driving_policy")
bv = tg_compile(flags, "big_driving_vision")
lenv.SideEffect('lock', [bp, bv]) # tg doesn't support multi-process so build serially
else:
print("USB GPU not detected... skipping")
+3 -6
View File
@@ -80,7 +80,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', 'left_eye_prob', 'right_eye_prob','left_blink_prob', 'right_blink_prob', 'sunglasses_prob', 'using_phone_prob']:
for key in ['face_prob', 'eyes_visible_prob', 'eyes_closed_prob', 'using_phone_prob']:
parsed[f'{key}_{ds_suffix}'] = sigmoid(model_output[f'{key}_{ds_suffix}'])
return parsed
@@ -90,11 +90,8 @@ 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.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.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.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):
+26 -8
View File
@@ -37,11 +37,13 @@ from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
PROCESS_NAME = "selfdrive.modeld.modeld"
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
VISION_PKL_PATH = Path(__file__).parent / 'models/driving_vision_tinygrad.pkl'
POLICY_PKL_PATH = Path(__file__).parent / 'models/driving_policy_tinygrad.pkl'
VISION_METADATA_PATH = Path(__file__).parent / 'models/driving_vision_metadata.pkl'
POLICY_METADATA_PATH = Path(__file__).parent / 'models/driving_policy_metadata.pkl'
MODELS_DIR = Path(__file__).parent / 'models'
VISION_PKL_PATH = MODELS_DIR / 'driving_vision_tinygrad.pkl'
VISION_METADATA_PATH = MODELS_DIR / 'driving_vision_metadata.pkl'
ON_POLICY_PKL_PATH = MODELS_DIR / 'driving_on_policy_tinygrad.pkl'
ON_POLICY_METADATA_PATH = MODELS_DIR / 'driving_on_policy_metadata.pkl'
OFF_POLICY_PKL_PATH = MODELS_DIR / 'driving_off_policy_tinygrad.pkl'
OFF_POLICY_METADATA_PATH = MODELS_DIR / 'driving_off_policy_metadata.pkl'
LAT_SMOOTH_SECONDS = 0.0
LONG_SMOOTH_SECONDS = 0.3
@@ -156,7 +158,13 @@ class ModelState(ModelStateBase):
self.vision_output_slices = vision_metadata['output_slices']
vision_output_size = vision_metadata['output_shapes']['outputs'][1]
with open(POLICY_METADATA_PATH, 'rb') as f:
with open(OFF_POLICY_METADATA_PATH, 'rb') as f:
off_policy_metadata = pickle.load(f)
self.off_policy_input_shapes = off_policy_metadata['input_shapes']
self.off_policy_output_slices = off_policy_metadata['output_slices']
off_policy_output_size = off_policy_metadata['output_shapes']['outputs'][1]
with open(ON_POLICY_METADATA_PATH, 'rb') as f:
policy_metadata = pickle.load(f)
self.policy_input_shapes = policy_metadata['input_shapes']
self.policy_output_slices = policy_metadata['output_slices']
@@ -180,11 +188,13 @@ class ModelState(ModelStateBase):
self.vision_output = np.zeros(vision_output_size, dtype=np.float32)
self.policy_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()}
self.policy_output = np.zeros(policy_output_size, dtype=np.float32)
self.off_policy_output = np.zeros(off_policy_output_size, dtype=np.float32)
self.parser = Parser()
self.frame_buf_params : dict[str, tuple[int, int, int, int]] = {}
self.update_imgs = None
self.vision_run = pickle.loads(read_file_chunked(str(VISION_PKL_PATH)))
self.policy_run = pickle.loads(read_file_chunked(str(POLICY_PKL_PATH)))
self.policy_run = pickle.loads(read_file_chunked(str(ON_POLICY_PKL_PATH)))
self.off_policy_run = pickle.loads(read_file_chunked(str(OFF_POLICY_PKL_PATH)))
def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]:
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
@@ -233,9 +243,17 @@ class ModelState(ModelStateBase):
self.policy_output = self.policy_run(**self.policy_inputs).contiguous().realize().uop.base.buffer.numpy().flatten()
policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(self.policy_output, self.policy_output_slices))
combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict}
self.off_policy_output = self.off_policy_run(**self.policy_inputs).contiguous().realize().uop.base.buffer.numpy()
off_policy_outputs_dict = self.parser.parse_off_policy_outputs(self.slice_outputs(self.off_policy_output, self.off_policy_output_slices))
off_policy_outputs_dict.pop('plan')
combined_outputs_dict = {**vision_outputs_dict, **off_policy_outputs_dict, **policy_outputs_dict}
if 'planplus' in combined_outputs_dict and 'plan' in combined_outputs_dict:
combined_outputs_dict['plan'] = combined_outputs_dict['plan'] + combined_outputs_dict['planplus']
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(), self.off_policy_output.copy()])
return combined_outputs_dict
@@ -1 +0,0 @@
driving_policy.onnx
@@ -1 +0,0 @@
driving_vision.onnx
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7aff7ff1dc08bbaf562a8f77380ab5e5914f8557dba2f760d87e4d953f5604b0
size 7307246
oid sha256:2fd471febb6e973313ac0d0c6755f6410c1937ba92230b58a433761e8c883072
size 7364290
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eb6992bd60bada6162fea298e1a414b6b3d6a326db4eda46b9de62bcd8554754
size 13393859
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:86680a657bbb34f997034d1930bb2cb65c38b9222cea199732f72bd45791cfad
size 13022803
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:78477124cbf3ffe30fa951ebada8410b43c4242c6054584d656f1d329b067e15
size 14060847
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ee29ee5bce84d1ce23e9ff381280de9b4e4d96d2934cd751740354884e112c66
size 46877473
oid sha256:7af05e03fd170653ff5771baf373a2c57b363da12c4c411cd416dee067b4cf58
size 23266366
+9 -2
View File
@@ -96,11 +96,17 @@ class Parser:
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('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH))
self.parse_binary_crossentropy('meta', outs)
return outs
def parse_off_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
plan_mhp = self.is_mhp(outs, 'plan', ModelConstants.IDX_N * ModelConstants.PLAN_WIDTH)
plan_in_N, plan_out_N = (ModelConstants.PLAN_MHP_N, ModelConstants.PLAN_MHP_SELECTION) if plan_mhp else (0, 0)
self.parse_mdn('plan', outs, in_N=plan_in_N, out_N=plan_out_N, out_shape=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH))
self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
self.parse_binary_crossentropy('lane_lines_prob', outs)
self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH))
self.parse_binary_crossentropy('meta', outs)
self.parse_binary_crossentropy('lead_prob', outs)
lead_mhp = self.is_mhp(outs, 'lead', ModelConstants.LEAD_MHP_SELECTION * ModelConstants.LEAD_TRAJ_LEN * ModelConstants.LEAD_WIDTH)
lead_in_N, lead_out_N = (ModelConstants.LEAD_MHP_N, ModelConstants.LEAD_MHP_SELECTION) if lead_mhp else (0, 0)
@@ -120,5 +126,6 @@ class Parser:
def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
outs = self.parse_vision_outputs(outs)
outs = self.parse_off_policy_outputs(outs)
outs = self.parse_policy_outputs(outs)
return outs
+25 -23
View File
@@ -1,4 +1,4 @@
from math import atan2
from math import atan2, radians
import numpy as np
from cereal import car, log
@@ -32,9 +32,8 @@ class DRIVER_MONITOR_SETTINGS:
self._DISTRACTED_PROMPT_TIME_TILL_TERMINAL = 6.
self._FACE_THRESHOLD = 0.7
self._EYE_THRESHOLD = 0.65
self._SG_THRESHOLD = 0.9
self._BLINK_THRESHOLD = 0.865
self._EYE_THRESHOLD = 0.5
self._BLINK_THRESHOLD = 0.5
self._PHONE_THRESH = 0.5
self._POSE_PITCH_THRESHOLD = 0.3133
@@ -43,6 +42,9 @@ class DRIVER_MONITOR_SETTINGS:
self._POSE_YAW_THRESHOLD = 0.4020
self._POSE_YAW_THRESHOLD_SLACK = 0.5042
self._POSE_YAW_THRESHOLD_STRICT = self._POSE_YAW_THRESHOLD
self._POSE_YAW_MIN_STEER_DEG = 30
self._POSE_YAW_STEER_FACTOR = 0.15
self._POSE_YAW_STEER_MAX_OFFSET = 0.3927
self._PITCH_NATURAL_OFFSET = 0.011 # initial value before offset is learned
self._PITCH_NATURAL_THRESHOLD = 0.449
self._YAW_NATURAL_OFFSET = 0.075 # initial value before offset is learned
@@ -59,7 +61,6 @@ class DRIVER_MONITOR_SETTINGS:
self._POSESTD_THRESHOLD = 0.3
self._HI_STD_FALLBACK_TIME = int(10 / self._DT_DMON) # fall back to wheel touch if model is uncertain for 10s
self._DISTRACTED_FILTER_TS = 0.25 # 0.6Hz
self._ALWAYS_ON_ALERT_MIN_SPEED = 11
self._POSE_CALIB_MIN_SPEED = 13 # 30 mph
self._POSE_OFFSET_MIN_COUNT = int(60 / self._DT_DMON) # valid data counts before calibration completes, 1min cumulative
@@ -101,6 +102,7 @@ class DriverPose:
self.low_std = True
self.cfactor_pitch = 1.
self.cfactor_yaw = 1.
self.steer_yaw_offset = 0.
class DriverProb:
def __init__(self, raw_priors, max_trackable):
@@ -108,11 +110,6 @@ 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
@@ -147,7 +144,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 = DriverBlink()
self.blink_prob = 0.
self.phone_prob = 0.
self.always_on = always_on
@@ -238,7 +235,11 @@ class DriverMonitoring:
yaw_error = self.pose.yaw - min(max(self.pose.yaw_offseter.filtered_stat.mean(),
self.settings._YAW_MIN_OFFSET), self.settings._YAW_MAX_OFFSET)
pitch_error = 0 if pitch_error > 0 else abs(pitch_error) # no positive pitch limit
yaw_error = abs(yaw_error)
if yaw_error * self.pose.steer_yaw_offset > 0: # unidirectional
yaw_error = max(abs(yaw_error) - min(abs(self.pose.steer_yaw_offset), self.settings._POSE_YAW_STEER_MAX_OFFSET), 0.)
else:
yaw_error = abs(yaw_error)
pitch_threshold = self.settings._POSE_PITCH_THRESHOLD * self.pose.cfactor_pitch if self.pose.calibrated else self.settings._PITCH_NATURAL_THRESHOLD
yaw_threshold = self.settings._POSE_YAW_THRESHOLD * self.pose.cfactor_yaw
@@ -246,7 +247,7 @@ class DriverMonitoring:
if pitch_error > pitch_threshold or yaw_error > yaw_threshold:
distracted_types.append(DistractedType.DISTRACTED_POSE)
if (self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD:
if self.blink_prob > self.settings._BLINK_THRESHOLD:
distracted_types.append(DistractedType.DISTRACTED_BLINK)
if self.phone_prob > self.settings._PHONE_THRESH:
@@ -254,7 +255,7 @@ class DriverMonitoring:
return distracted_types
def _update_states(self, driver_state, cal_rpy, car_speed, op_engaged, standstill, demo_mode=False):
def _update_states(self, driver_state, cal_rpy, car_speed, op_engaged, standstill, demo_mode=False, steering_angle_deg=0.):
rhd_pred = driver_state.wheelOnRightProb
# calibrates only when there's movement and either face detected
if car_speed > self.settings._WHEELPOS_CALIB_MIN_SPEED and (driver_state.leftDriverData.faceProb > self.settings._FACE_THRESHOLD or
@@ -277,17 +278,17 @@ class DriverMonitoring:
self.face_detected = driver_data.faceProb > self.settings._FACE_THRESHOLD
self.pose.roll, self.pose.pitch, self.pose.yaw = face_orientation_from_net(driver_data.faceOrientation, driver_data.facePosition, cal_rpy)
steer_d = max(abs(steering_angle_deg) - self.settings._POSE_YAW_MIN_STEER_DEG, 0.)
self.pose.steer_yaw_offset = radians(steer_d) * -np.sign(steering_angle_deg) * self.settings._POSE_YAW_STEER_FACTOR
if self.wheel_on_right:
self.pose.yaw *= -1
self.pose.steer_yaw_offset *= -1
self.wheel_on_right_last = self.wheel_on_right
self.pose.pitch_std = driver_data.faceOrientationStd[0]
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.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.blink_prob = driver_data.eyesClosedProb * (driver_data.eyesVisibleProb > self.settings._EYE_THRESHOLD)
self.phone_prob = driver_data.phoneProb
self.distracted_types = self._get_distracted_types()
@@ -360,19 +361,19 @@ class DriverMonitoring:
if self.awareness > self.threshold_prompt:
return
_reaching_pre = self.awareness - self.step_change <= self.threshold_pre
_reaching_audible = self.awareness - self.step_change <= self.threshold_prompt
_reaching_terminal = self.awareness - self.step_change <= 0
standstill_orange_exemption = standstill and _reaching_audible
standstill_exemption = standstill and _reaching_pre
always_on_red_exemption = always_on_valid and not op_engaged and _reaching_terminal
always_on_lowspeed_exemption = always_on_valid and not op_engaged and car_speed < self.settings._ALWAYS_ON_ALERT_MIN_SPEED
certainly_distracted = self.driver_distraction_filter.x > 0.63 and self.driver_distracted and self.face_detected
maybe_distracted = self.hi_stds > self.settings._HI_STD_FALLBACK_TIME or not self.face_detected
if certainly_distracted or maybe_distracted:
# should always be counting if distracted unless at standstill (lowspeed for always-on) and reaching orange
# should always be counting if distracted unless at standstill and reaching green
# also will not be reaching 0 if DM is active when not engaged
if not (standstill_orange_exemption or always_on_red_exemption or (always_on_lowspeed_exemption and _reaching_audible)):
if not (standstill_exemption or always_on_red_exemption):
self.awareness = max(self.awareness - self.step_change, -0.1)
alert = None
@@ -385,7 +386,7 @@ class DriverMonitoring:
elif self.awareness <= self.threshold_prompt:
# prompt orange alert
alert = EventName.promptDriverDistracted if self.active_monitoring_mode else EventName.promptDriverUnresponsive
elif self.awareness <= self.threshold_pre and not always_on_lowspeed_exemption:
elif self.awareness <= self.threshold_pre:
# pre green alert
alert = EventName.preDriverDistracted if self.active_monitoring_mode else EventName.preDriverUnresponsive
@@ -451,6 +452,7 @@ class DriverMonitoring:
op_engaged=enabled,
standstill=standstill,
demo_mode=demo,
steering_angle_deg=sm['carState'].steeringAngleDeg,
)
# Update distraction events
+6 -8
View File
@@ -20,10 +20,8 @@ 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.leftEyeProb = 1.
ds.leftDriverData.rightEyeProb = 1.
ds.leftDriverData.leftBlinkProb = 1. * distracted
ds.leftDriverData.rightBlinkProb = 1. * distracted
ds.leftDriverData.eyesVisibleProb = 1.
ds.leftDriverData.eyesClosedProb = 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
@@ -187,10 +185,10 @@ class TestMonitoring:
standstill_vector = always_true[:]
standstill_vector[int(_redlight_time/DT_DMON):] = [False] * int((TEST_TIMESPAN-_redlight_time)/DT_DMON)
events, d_status = self._run_seq(always_distracted, always_false, always_true, standstill_vector)
assert events[int((d_status.settings._DISTRACTED_TIME-d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL+1)/DT_DMON)].names[0] == \
EventName.preDriverDistracted
assert events[int((_redlight_time-0.1)/DT_DMON)].names[0] == EventName.preDriverDistracted
assert events[int((_redlight_time+0.5)/DT_DMON)].names[0] == EventName.promptDriverDistracted
assert len(events[int((_redlight_time-0.1)/DT_DMON)]) == 0
_pre_to_prompt = d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL - d_status.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL
assert events[int((_redlight_time+0.5)/DT_DMON)].names[0] == EventName.preDriverDistracted
assert events[int((_redlight_time+_pre_to_prompt+0.5)/DT_DMON)].names[0] == EventName.promptDriverDistracted
# engaged, model is somehow uncertain and driver is distracted
# - should fall back to wheel touch after uncertain alert
-16
View File
@@ -78,22 +78,6 @@ class TestPandad:
assert any(Panda(s).is_internal() for s in Panda.list())
def test_best_case_startup_time(self):
# run once so we're up to date
self._run_test(60)
ts = []
for _ in range(10):
# should be nearly instant this time
dt = self._run_test(5)
ts.append(dt)
# 5s for USB (due to enumeration)
# - 0.2s pandad -> pandad
# - plus some buffer
print("startup times", ts, sum(ts) / len(ts))
assert 0.1 < (sum(ts)/len(ts)) < 0.7
def test_old_spi_protocol(self):
# flash firmware with old SPI protocol
self._flash_bootstub(os.path.join(HERE, "bootstub.panda_h7_spiv0.bin"))
+5
View File
@@ -235,6 +235,11 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
"Ensure road ahead is clear"),
},
EventName.lateralManeuver: {
ET.WARNING: longitudinal_maneuver_alert,
ET.PERMANENT: NormalPermanentAlert("Lateral Maneuver Mode"),
},
EventName.selfdriveInitializing: {
ET.NO_ENTRY: NoEntryAlert("System Initializing"),
},
+6 -3
View File
@@ -89,7 +89,7 @@ class SelfdriveD(CruiseHelper):
# TODO: de-couple selfdrived with card/conflate on carState without introducing controls mismatches
self.car_state_sock = messaging.sub_sock('carState', timeout=20)
ignore = self.sensor_packets + self.gps_packets + ['alertDebug'] + ['modelDataV2SP']
ignore = self.sensor_packets + self.gps_packets + ['alertDebug', 'lateralManeuverPlan'] + ['modelDataV2SP']
if SIMULATION:
ignore += ['driverCameraState', 'managerState']
if REPLAY:
@@ -99,7 +99,7 @@ class SelfdriveD(CruiseHelper):
'carOutput', 'driverMonitoringState', 'longitudinalPlan', 'livePose', 'liveDelay',
'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters',
'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userBookmark', 'audioFeedback',
'modelDataV2SP', 'longitudinalPlanSP'] + \
'lateralManeuverPlan', 'modelDataV2SP', 'longitudinalPlanSP'] + \
self.camera_packets + self.sensor_packets + self.gps_packets,
ignore_alive=ignore, ignore_avg_freq=ignore,
ignore_valid=ignore, frequency=int(1/DT_CTRL))
@@ -183,7 +183,10 @@ class SelfdriveD(CruiseHelper):
self.events.add(EventName.joystickDebug)
self.startup_event = None
if self.sm.recv_frame['alertDebug'] > 0:
if self.sm.recv_frame['lateralManeuverPlan'] > 0:
self.events.add(EventName.lateralManeuver)
self.startup_event = None
elif self.sm.recv_frame['alertDebug'] > 0:
self.events.add(EventName.longitudinalManeuver)
self.startup_event = None
@@ -0,0 +1,92 @@
import os
from collections import defaultdict
from opendbc.car.tests.car_diff import format_diff, format_numeric_diffs
from openpilot.selfdrive.test.process_replay.compare_logs import compare_logs
from openpilot.selfdrive.test.process_replay.process_replay import PROC_REPLAY_DIR
class MsgWrap:
"""Adapter so to_dict() includes defaults"""
def __init__(self, msg):
self._msg = msg
def to_dict(self) -> dict:
return self._msg.to_dict(verbose=True)
def diff_process(cfg, ref_msgs, new_msgs) -> tuple | None:
ref = defaultdict(list)
new = defaultdict(list)
for m in ref_msgs:
if m.which() in cfg.subs:
ref[m.which()].append(m)
for m in new_msgs:
if m.which() in cfg.subs:
new[m.which()].append(m)
diffs = []
for sub in cfg.subs:
if len(ref[sub]) != len(new[sub]):
diffs.append((f"{sub} (message count)", 0, (len(ref[sub]), len(new[sub])), 0))
for i, (r, n) in enumerate(zip(ref[sub], new[sub], strict=False)):
for d in compare_logs([r], [n], cfg.ignore, tolerance=cfg.tolerance):
if d[0] == "change":
a, b = d[2]
if a != a and b != b:
continue
diffs.append((d[1], i, d[2], r.logMonoTime))
elif d[0] in ("add", "remove"):
for item in d[2]:
if item[1] != item[1]:
continue
diffs.append((f"{d[1]}.{item[0]}", i, (d[0], item[1]), r.logMonoTime))
return (diffs, ref, new) if diffs else None
def diff_format(diffs, ref, new, field) -> list[str]:
if any(part.isdigit() for part in field.split(".")):
return format_numeric_diffs(diffs)
msg_type = field.split(".")[0]
ref_ts = [(m.logMonoTime, MsgWrap(m)) for m in ref.get(msg_type, [])]
new_wrapped = [MsgWrap(m) for m in new.get(msg_type, [])]
return format_diff(diffs, ref_ts, new_wrapped, field)
def diff_report(replay_diffs, segments) -> None:
seg_to_plat = {seg: plat for plat, seg in segments}
with_diffs, errors, n_passed = [], [], 0
for seg, proc, data in replay_diffs:
plat = seg_to_plat.get(seg, "UNKNOWN")
if data is None:
n_passed += 1
elif isinstance(data, str):
errors.append((plat, seg, proc, data))
else:
with_diffs.append((plat, seg, proc, data))
icon = "⚠️" if with_diffs else ""
lines = [
"## Process replay diff report",
"Replays driving segments through this PR and compares the behavior to master.",
"Please review any changes carefully to ensure they are expected.\n",
f"{icon} {len(with_diffs)} changed, {n_passed} passed, {len(errors)} errors",
]
for plat, seg, proc, err in errors:
lines.append(f"\nERROR {plat} - {seg} [{proc}]: {err}")
if with_diffs:
lines.append("<details><summary><b>Show changes</b></summary>\n\n```")
for plat, seg, proc, (diffs, ref, new) in with_diffs:
lines.append(f"\n{plat} - {seg} [{proc}]")
by_field = defaultdict(list)
for d in diffs:
by_field[d[0]].append(d)
for field, fd in sorted(by_field.items()):
lines.append(f"\n {field} ({len(fd)} diffs)")
lines.extend(diff_format(fd, ref, new, field))
lines.append("```\n</details>")
with open(os.path.join(PROC_REPLAY_DIR, "diff_report.txt"), "w") as f:
f.write("\n".join(lines))
@@ -39,6 +39,7 @@ def migrate_all(lr: LogIterable, manager_states: bool = False, panda_states: boo
migrate_carOutput,
migrate_controlsState,
migrate_carState,
migrate_livePose,
migrate_liveTracks,
migrate_driverAssistance,
migrate_drivingModelData,
@@ -177,6 +178,7 @@ def migrate_liveLocationKalman(msgs):
m = messaging.new_message('livePose')
m.valid = msg.valid
m.logMonoTime = msg.logMonoTime
m.livePose.timestamp = msg.logMonoTime
for field in ["orientationNED", "velocityDevice", "accelerationDevice", "angularVelocityDevice"]:
lp_field, llk_field = getattr(m.livePose, field), getattr(msg.liveLocationKalmanDEPRECATED, field)
lp_field.x, lp_field.y, lp_field.z = llk_field.value or nans
@@ -188,6 +190,21 @@ def migrate_liveLocationKalman(msgs):
return ops, [], []
@migration(inputs=["livePose"])
def migrate_livePose(msgs):
ops = []
needs_migration = all(msg.livePose.timestamp == 0 for _, msg in msgs if msg.which() == 'livePose')
if not needs_migration:
return [], [], []
for index, msg in msgs:
if msg.which() == "livePose":
new_msg = msg.as_builder()
new_msg.livePose.timestamp = msg.logMonoTime
ops.append((index, new_msg.as_reader()))
return ops, [], []
@migration(inputs=["controlsState"], product="selfdriveState")
def migrate_controlsState(msgs):
add_ops = []
@@ -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.leftBlinkProb), "leftDriverData.leftBlinkProb"),
(lambda x: get_idx_if_non_empty(x.leftDriverData.eyesClosedProb), "leftDriverData.eyesClosedProb"),
(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")
@@ -146,6 +146,7 @@ class ProcessContainer:
self.cfg = copy.deepcopy(cfg)
self.process = copy.deepcopy(managed_processes[cfg.proc_name])
self.msg_queue: list[capnp._DynamicStructReader] = []
self.last_input_log_mono_time: int = -1
self.cnt = 0
self.pm: messaging.PubMaster | None = None
self.sockets: list[messaging.SubSocket] | None = None
@@ -268,6 +269,7 @@ class ProcessContainer:
ms = messaging.drain_sock(socket)
for m in ms:
m = m.as_builder()
assert start_time > 0, "start_time must be positive"
m.logMonoTime = start_time + int(self.cfg.processing_time * 1e9)
output_msgs.append(m.as_reader())
return output_msgs
@@ -294,10 +296,11 @@ class ProcessContainer:
trigger_empty_recv = any(m.which() == self.cfg.main_pub for m in self.msg_queue)
# get output msgs from previous inputs
output_msgs = self.get_output_msgs(msg.logMonoTime)
output_msgs = self.get_output_msgs(self.last_input_log_mono_time)
for m in self.msg_queue:
self.pm.send(m.which(), m.as_builder())
self.last_input_log_mono_time = max(self.last_input_log_mono_time, m.logMonoTime)
# send frames if needed
if self.vipc_server is not None and m.which() in self.cfg.vision_pubs:
camera_state = getattr(m, m.which())
@@ -513,6 +516,7 @@ CONFIGS = [
ignore=["logMonoTime"],
should_recv_callback=MessageBasedRcvCallback("cameraOdometry"),
tolerance=NUMPY_TOLERANCE,
processing_time=0.01,
),
ProcessConfig(
proc_name="paramsd",
@@ -716,7 +720,7 @@ def _replay_multi_process(
# flush last set of messages from each process
for container in containers:
last_time = log_msgs[-1].logMonoTime if len(log_msgs) > 0 else int(time.monotonic() * 1e9)
last_time = container.last_input_log_mono_time if container.last_input_log_mono_time > 0 else int(time.monotonic() * 1e9)
log_msgs.extend(container.get_output_msgs(last_time))
finally:
for container in containers:
@@ -3,6 +3,7 @@ import argparse
import concurrent.futures
import os
import sys
import traceback
from collections import defaultdict
from tqdm import tqdm
from typing import Any
@@ -11,6 +12,7 @@ from opendbc.car.car_helpers import interface_names
from openpilot.common.git import get_commit
from openpilot.tools.lib.openpilotci import get_url
from openpilot.selfdrive.test.process_replay.compare_logs import compare_logs, format_diff
from openpilot.selfdrive.test.process_replay.diff_report import diff_process, diff_report
from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, PROC_REPLAY_DIR, FAKEDATA, replay_process, \
check_most_messages_valid
from openpilot.tools.lib.filereader import FileReader
@@ -72,11 +74,16 @@ EXCLUDED_PROCS = {"modeld", "dmonitoringmodeld"}
def run_test_process(data):
segment, cfg, args, cur_log_fn, ref_log_path, lr_dat = data
ref_log_msgs = list(LogReader(ref_log_path))
lr = LogReader.from_bytes(lr_dat)
res, log_msgs = test_process(cfg, lr, segment, ref_log_path, cur_log_fn, args.ignore_fields, args.ignore_msgs)
res, log_msgs = test_process(cfg, lr, segment, ref_log_msgs, cur_log_fn, args.ignore_fields, args.ignore_msgs)
# save logs so we can update refs
save_log(cur_log_fn, log_msgs)
return (segment, cfg.proc_name, res)
try:
diff_data = diff_process(cfg, ref_log_msgs, log_msgs)
except Exception:
diff_data = traceback.format_exc()
return (segment, cfg.proc_name, res, diff_data)
def get_log_data(segment):
@@ -85,14 +92,12 @@ def get_log_data(segment):
return (segment, f.read())
def test_process(cfg, lr, segment, ref_log_path, new_log_path, ignore_fields=None, ignore_msgs=None):
def test_process(cfg, lr, segment, ref_log_msgs, new_log_path, ignore_fields=None, ignore_msgs=None):
if ignore_fields is None:
ignore_fields = []
if ignore_msgs is None:
ignore_msgs = []
ref_log_msgs = list(LogReader(ref_log_path))
try:
log_msgs = replay_process(cfg, lr, disable_progress=True)
except Exception as e:
@@ -201,9 +206,11 @@ if __name__ == "__main__":
log_paths[segment][cfg.proc_name]['new'] = cur_log_fn
results: Any = defaultdict(dict)
diffs: list = []
p2 = pool.map(run_test_process, pool_args)
for (segment, proc, result) in tqdm(p2, desc="Running Tests", total=len(pool_args)):
for (segment, proc, result, diff_data) in tqdm(p2, desc="Running Tests", total=len(pool_args)):
results[segment][proc] = result
diffs.append((segment, proc, diff_data))
diff_short, diff_long, failed = format_diff(results, log_paths, ref_commit)
if not args.update_refs:
@@ -211,6 +218,11 @@ if __name__ == "__main__":
f.write(diff_long)
print(diff_short)
try:
diff_report(diffs, segments)
except Exception:
print(f"failed to generate diff report:\n{traceback.format_exc()}")
if failed:
print("TEST FAILED")
else:
+1 -1
View File
@@ -48,7 +48,7 @@ Font font_display;
const bool tici_device = Hardware::get_device_type() == cereal::InitData::DeviceType::TICI ||
Hardware::get_device_type() == cereal::InitData::DeviceType::TIZI;
std::vector<std::string> tici_prebuilt_branches = {"release3", "release-tizi", "release3-staging", "nightly", "nightly-dev"};
std::vector<std::string> tici_prebuilt_branches = {"release3", "release-tici", "release3-staging", "nightly", "nightly-dev"};
std::string migrated_branch;
void branchMigration() {
+26 -1
View File
@@ -71,6 +71,13 @@ class DeveloperLayout(Widget):
callback=self._on_long_maneuver_mode,
)
self._lat_maneuver_toggle = toggle_item(
lambda: tr("Lateral Maneuver Mode"),
description="",
initial_state=self._params.get_bool("LateralManeuverMode"),
callback=self._on_lat_maneuver_mode,
)
self._alpha_long_toggle = toggle_item(
lambda: tr("sunnypilot Longitudinal Control (Alpha)"),
description=lambda: tr(DESCRIPTIONS["alpha_longitudinal"]),
@@ -93,6 +100,7 @@ class DeveloperLayout(Widget):
self._ssh_keys,
self._joystick_toggle,
self._long_maneuver_toggle,
self._lat_maneuver_toggle,
self._alpha_long_toggle,
self._ui_debug_toggle,
], line_separator=True, spacing=0)
@@ -113,7 +121,7 @@ class DeveloperLayout(Widget):
# Hide non-release toggles on release builds
# TODO: we can do an onroad cycle, but alpha long toggle requires a deinit function to re-enable radar and not fault
for item in (self._joystick_toggle, self._long_maneuver_toggle, self._alpha_long_toggle):
for item in (self._joystick_toggle, self._long_maneuver_toggle, self._lat_maneuver_toggle, self._alpha_long_toggle):
item.set_visible(not self._is_release)
# CP gating
@@ -130,8 +138,12 @@ class DeveloperLayout(Widget):
if not long_man_enabled:
self._long_maneuver_toggle.action_item.set_state(False)
self._params.put_bool("LongitudinalManeuverMode", False)
lat_man_enabled = ui_state.is_offroad()
self._lat_maneuver_toggle.action_item.set_enabled(lat_man_enabled)
else:
self._long_maneuver_toggle.action_item.set_enabled(False)
self._lat_maneuver_toggle.action_item.set_enabled(False)
self._alpha_long_toggle.set_visible(False)
# TODO: make a param control list item so we don't need to manage internal state as much here
@@ -141,6 +153,7 @@ class DeveloperLayout(Widget):
("SshEnabled", self._ssh_toggle),
("JoystickDebugMode", self._joystick_toggle),
("LongitudinalManeuverMode", self._long_maneuver_toggle),
("LateralManeuverMode", self._lat_maneuver_toggle),
("AlphaLongitudinalEnabled", self._alpha_long_toggle),
("ShowDebugInfo", self._ui_debug_toggle),
):
@@ -162,11 +175,23 @@ class DeveloperLayout(Widget):
self._params.put_bool("JoystickDebugMode", state)
self._params.put_bool("LongitudinalManeuverMode", False)
self._long_maneuver_toggle.action_item.set_state(False)
self._params.put_bool("LateralManeuverMode", False)
self._lat_maneuver_toggle.action_item.set_state(False)
def _on_long_maneuver_mode(self, state: bool):
self._params.put_bool("LongitudinalManeuverMode", state)
self._params.put_bool("JoystickDebugMode", False)
self._joystick_toggle.action_item.set_state(False)
self._params.put_bool("LateralManeuverMode", False)
self._lat_maneuver_toggle.action_item.set_state(False)
def _on_lat_maneuver_mode(self, state: bool):
self._params.put_bool("LateralManeuverMode", state)
self._params.put_bool("ExperimentalMode", False)
self._params.put_bool("JoystickDebugMode", False)
self._joystick_toggle.action_item.set_state(False)
self._params.put_bool("LongitudinalManeuverMode", False)
self._long_maneuver_toggle.action_item.set_state(False)
def _on_alpha_long_enabled(self, state: bool):
if state:
@@ -55,6 +55,9 @@ class DeveloperLayoutMici(NavScroller):
self._long_maneuver_toggle = BigToggle("longitudinal maneuver mode",
initial_state=ui_state.params.get_bool("LongitudinalManeuverMode"),
toggle_callback=self._on_long_maneuver_mode)
self._lat_maneuver_toggle = BigToggle("lateral maneuver mode",
initial_state=ui_state.params.get_bool("LateralManeuverMode"),
toggle_callback=self._on_lat_maneuver_mode)
self._alpha_long_toggle = BigToggle("alpha longitudinal",
initial_state=ui_state.params.get_bool("AlphaLongitudinalEnabled"),
toggle_callback=self._on_alpha_long_enabled)
@@ -68,6 +71,7 @@ class DeveloperLayoutMici(NavScroller):
self._ssh_keys_btn,
self._joystick_toggle,
self._long_maneuver_toggle,
self._lat_maneuver_toggle,
self._alpha_long_toggle,
self._debug_mode_toggle,
])
@@ -78,12 +82,13 @@ class DeveloperLayoutMici(NavScroller):
("SshEnabled", self._ssh_toggle),
("JoystickDebugMode", self._joystick_toggle),
("LongitudinalManeuverMode", self._long_maneuver_toggle),
("LateralManeuverMode", self._lat_maneuver_toggle),
("AlphaLongitudinalEnabled", self._alpha_long_toggle),
("ShowDebugInfo", self._debug_mode_toggle),
)
onroad_blocked_toggles = (self._adb_toggle, self._joystick_toggle)
release_blocked_toggles = (self._joystick_toggle, self._long_maneuver_toggle, self._alpha_long_toggle)
engaged_blocked_toggles = (self._long_maneuver_toggle, self._alpha_long_toggle)
release_blocked_toggles = (self._joystick_toggle, self._long_maneuver_toggle, self._lat_maneuver_toggle, self._alpha_long_toggle)
engaged_blocked_toggles = (self._long_maneuver_toggle, self._lat_maneuver_toggle, self._alpha_long_toggle)
# Hide non-release toggles on release builds
for item in release_blocked_toggles:
@@ -129,8 +134,12 @@ class DeveloperLayoutMici(NavScroller):
if not long_man_enabled:
self._long_maneuver_toggle.set_checked(False)
ui_state.params.put_bool("LongitudinalManeuverMode", False)
lat_man_enabled = ui_state.is_offroad()
self._lat_maneuver_toggle.set_enabled(lat_man_enabled)
else:
self._long_maneuver_toggle.set_enabled(False)
self._lat_maneuver_toggle.set_enabled(False)
self._alpha_long_toggle.set_visible(False)
# Refresh toggles from params to mirror external changes
@@ -141,11 +150,24 @@ class DeveloperLayoutMici(NavScroller):
ui_state.params.put_bool("JoystickDebugMode", state)
ui_state.params.put_bool("LongitudinalManeuverMode", False)
self._long_maneuver_toggle.set_checked(False)
ui_state.params.put_bool("LateralManeuverMode", False)
self._lat_maneuver_toggle.set_checked(False)
def _on_long_maneuver_mode(self, state: bool):
ui_state.params.put_bool("LongitudinalManeuverMode", state)
ui_state.params.put_bool("JoystickDebugMode", False)
self._joystick_toggle.set_checked(False)
ui_state.params.put_bool("LateralManeuverMode", False)
self._lat_maneuver_toggle.set_checked(False)
restart_needed_callback(state)
def _on_lat_maneuver_mode(self, state: bool):
ui_state.params.put_bool("LateralManeuverMode", state)
ui_state.params.put_bool("ExperimentalMode", False)
ui_state.params.put_bool("JoystickDebugMode", False)
self._joystick_toggle.set_checked(False)
ui_state.params.put_bool("LongitudinalManeuverMode", False)
self._long_maneuver_toggle.set_checked(False)
restart_needed_callback(state)
def _on_alpha_long_enabled(self, state: bool):
@@ -39,8 +39,6 @@ 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()
@@ -154,8 +152,6 @@ 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"]
@@ -202,31 +198,21 @@ 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
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
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)]:
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):
+1 -1
View File
@@ -120,7 +120,7 @@ class HudRenderer(Widget):
self._txt_wheel: rl.Texture = gui_app.texture('icons_mici/wheel.png', 50, 50)
self._txt_wheel_critical: rl.Texture = gui_app.texture('icons_mici/wheel_critical.png', 50, 50)
self._txt_exclamation_point: rl.Texture = gui_app.texture('icons_mici/exclamation_point.png', 44, 44)
self._txt_exclamation_point: rl.Texture = gui_app.texture('icons_mici/exclamation_point.png', 9, 44)
self._wheel_alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
self._wheel_y_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps)
@@ -58,6 +58,8 @@ class ModelsLayout(Widget):
self.supercombo_label = progress_item(tr("Driving Model"))
self.vision_label = progress_item(tr("Vision Model"))
self.policy_label = progress_item(tr("Policy Model"))
self.off_policy_label = progress_item(tr("Off-Policy Model"))
self.on_policy_label = progress_item(tr("On-Policy Model"))
self.refresh_item = button_item(tr("Refresh Model List"), tr("REFRESH"), "",
lambda: (ui_state.params.put("ModelManager_LastSyncTime", 0),
@@ -91,7 +93,7 @@ class ModelsLayout(Widget):
self.lagd_toggle = toggle_item_sp(tr("Live Learning Steer Delay"), "", param="LagdToggle")
self.items = [self.current_model_item, self.cancel_download_item, self.supercombo_label, self.vision_label,
self.policy_label, self.refresh_item, self.clear_cache_item, self.lane_turn_desire_toggle,
self.policy_label, self.off_policy_label, self.on_policy_label, self.refresh_item, self.clear_cache_item, self.lane_turn_desire_toggle,
self.lane_turn_value_control, self.lagd_toggle, self.delay_control]
def _update_lagd_description(self, lagd_toggle: bool):
@@ -129,7 +131,9 @@ class ModelsLayout(Widget):
def _handle_bundle_download_progress(self):
labels = {custom.ModelManagerSP.Model.Type.supercombo: self.supercombo_label,
custom.ModelManagerSP.Model.Type.vision: self.vision_label,
custom.ModelManagerSP.Model.Type.policy: self.policy_label}
custom.ModelManagerSP.Model.Type.policy: self.policy_label,
custom.ModelManagerSP.Model.Type.offPolicy: self.off_policy_label,
custom.ModelManagerSP.Model.Type.onPolicy: self.on_policy_label}
for label in labels.values():
label.set_visible(False)
self.cancel_download_item.set_visible(False)
@@ -8,7 +8,7 @@ from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base impo
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp
from opendbc.car.hyundai.values import CAR, CANFD_UNSUPPORTED_LONGITUDINAL_CAR, UNSUPPORTED_LONGITUDINAL_CAR
from opendbc.car.hyundai.values import CAR, UNSUPPORTED_LONGITUDINAL_CAR
class HyundaiSettings(BrandSettings):
@@ -31,7 +31,7 @@ class HyundaiSettings(BrandSettings):
bundle = ui_state.params.get("CarPlatformBundle")
if bundle:
platform = bundle.get("platform")
self.alpha_long_available = CAR[platform] not in (UNSUPPORTED_LONGITUDINAL_CAR | CANFD_UNSUPPORTED_LONGITUDINAL_CAR)
self.alpha_long_available = CAR[platform] not in set().union(*UNSUPPORTED_LONGITUDINAL_CAR.values())
elif ui_state.CP is not None:
self.alpha_long_available = ui_state.CP.alphaLongitudinalAvailable
+81 -99
View File
@@ -1,124 +1,106 @@
import pytest
import json
import os
import re
import xml.etree.ElementTree as ET
import string
import requests
from openpilot.common.parameterized import parameterized_class
from openpilot.system.ui.lib.multilang import TRANSLATIONS_DIR, LANGUAGES_FILE
from pathlib import Path
with open(str(LANGUAGES_FILE)) as f:
translation_files = json.load(f)
import pytest
UNFINISHED_TRANSLATION_TAG = "<translation type=\"unfinished\"" # non-empty translations can be marked unfinished
LOCATION_TAG = "<location "
FORMAT_ARG = re.compile("%[0-9]+")
from openpilot.selfdrive.ui.translations.potools import parse_po
from openpilot.system.ui.lib.multilang import LANGUAGES_FILE, TRANSLATIONS_DIR
PERCENT_PLACEHOLDER_RE = re.compile(r"%(?:n|\d+)")
BAD_ENTITY_RE = re.compile(r'@(\w+);')
LINE_NUMBER_REF_RE = re.compile(r'^#:\s+.+:\d+(?:\s|$)')
FORMATTER = string.Formatter()
PO_DIR = Path(str(TRANSLATIONS_DIR))
with LANGUAGES_FILE.open(encoding='utf-8') as f:
TRANSLATION_LANGUAGES = json.load(f)
@pytest.mark.skip("TODO: update for raylib")
@parameterized_class(("name", "file"), translation_files.items())
class TestTranslations:
name: str
file: str
def extract_placeholders(text: str) -> list[str]:
placeholders = PERCENT_PLACEHOLDER_RE.findall(text)
@staticmethod
def _read_translation_file(path, file):
tr_file = os.path.join(path, f"{file}.ts")
with open(tr_file) as f:
return f.read()
try:
parsed = list(FORMATTER.parse(text))
except ValueError as e:
raise AssertionError(f"invalid brace formatting in {text!r}: {e}") from e
def test_missing_translation_files(self):
assert os.path.exists(os.path.join(str(TRANSLATIONS_DIR), f"{self.file}.ts")), \
f"{self.name} has no XML translation file, run selfdrive/ui/update_translations.py"
for _, field_name, format_spec, conversion in parsed:
if field_name is None:
continue
@pytest.mark.skip("Only test unfinished translations before going to release")
def test_unfinished_translations(self):
cur_translations = self._read_translation_file(TRANSLATIONS_DIR, self.file)
assert UNFINISHED_TRANSLATION_TAG not in cur_translations, \
f"{self.file} ({self.name}) translation file has unfinished translations. Finish translations or mark them as completed in Qt Linguist"
token = "{"
token += field_name
if conversion:
token += f"!{conversion}"
if format_spec:
token += f":{format_spec}"
token += "}"
placeholders.append(token)
def test_vanished_translations(self):
cur_translations = self._read_translation_file(TRANSLATIONS_DIR, self.file)
assert "<translation type=\"vanished\">" not in cur_translations, \
f"{self.file} ({self.name}) translation file has obsolete translations. Run selfdrive/ui/update_translations.py --vanish to remove them"
return sorted(placeholders)
def test_finished_translations(self):
"""
Tests ran on each translation marked "finished"
Plural:
- that any numerus (plural) translations have all plural forms non-empty
- that the correct format specifier is used (%n)
Non-plural:
- that translation is not empty
- that translation format arguments are consistent
"""
tr_xml = ET.parse(os.path.join(TRANSLATIONS_DIR, f"{self.file}.ts"))
for context in tr_xml.getroot():
for message in context.iterfind("message"):
translation = message.find("translation")
source_text = message.find("source").text
def load_po_text(po_path: Path) -> str:
return po_path.read_text(encoding='utf-8')
# Do not test unfinished translations
if translation.get("type") == "unfinished":
continue
if message.get("numerus") == "yes":
numerusform = [t.text for t in translation.findall("numerusform")]
@pytest.mark.parametrize("language_code", sorted(TRANSLATION_LANGUAGES.values()))
def test_translation_file_exists(language_code: str):
po_path = PO_DIR / f"app_{language_code}.po"
assert po_path.exists(), f"missing translation file: {po_path}"
for nf in numerusform:
assert nf is not None, f"Ensure all plural translation forms are completed: {source_text}"
assert "%n" in nf, "Ensure numerus argument (%n) exists in translation."
assert FORMAT_ARG.search(nf) is None, f"Plural translations must use %n, not %1, %2, etc.: {numerusform}"
else:
assert translation.text is not None, f"Ensure translation is completed: {source_text}"
@pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name)
def test_translation_placeholders_are_preserved(po_path: Path):
_, entries = parse_po(po_path)
language = po_path.stem.removeprefix("app_")
source_args = FORMAT_ARG.findall(source_text)
translation_args = FORMAT_ARG.findall(translation.text)
assert sorted(source_args) == sorted(translation_args), \
f"Ensure format arguments are consistent: `{source_text}` vs. `{translation.text}`"
for entry in entries:
source_placeholders = extract_placeholders(entry.msgid)
def test_no_locations(self):
for line in self._read_translation_file(TRANSLATIONS_DIR, self.file).splitlines():
assert not line.strip().startswith(LOCATION_TAG), \
f"Line contains location tag: {line.strip()}, remove all line numbers."
def test_entities_error(self):
cur_translations = self._read_translation_file(TRANSLATIONS_DIR, self.file)
matches = re.findall(r'@(\w+);', cur_translations)
assert len(matches) == 0, f"The string(s) {matches} were found with '@' instead of '&'"
def test_bad_language(self):
IGNORED_WORDS = {'pédale'}
match = re.search(r'([a-zA-Z]{2,3})', self.file)
assert match, f"{self.name} - could not parse language"
try:
response = requests.get(
f"https://raw.githubusercontent.com/LDNOOBW/List-of-Dirty-Naughty-Obscene-and-Otherwise-Bad-Words/master/{match.group(1)}"
if entry.is_plural:
plural_placeholders = extract_placeholders(entry.msgid_plural)
message = (
f"{language}: source plural placeholders do not match singular for "
+ f"{entry.msgid!r}: {source_placeholders} vs {plural_placeholders}"
)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
if e.response is not None and e.response.status_code == 429:
pytest.skip("word list rate limited")
raise
assert plural_placeholders == source_placeholders, message
banned_words = {line.strip() for line in response.text.splitlines()}
for context in ET.parse(os.path.join(TRANSLATIONS_DIR, f"{self.file}.ts")).getroot():
for message in context.iterfind("message"):
translation = message.find("translation")
if translation.get("type") == "unfinished":
for idx, msgstr in sorted(entry.msgstr_plural.items()):
if not msgstr:
continue
translation_text = " ".join([t.text for t in translation.findall("numerusform")]) if message.get("numerus") == "yes" else translation.text
translated_placeholders = extract_placeholders(msgstr)
message = (
f"{language}: plural form {idx} changes placeholders for {entry.msgid!r}: "
+ f"expected {source_placeholders}, got {translated_placeholders}"
)
assert translated_placeholders == source_placeholders, message
else:
if not entry.msgstr:
continue
if not translation_text:
continue
translated_placeholders = extract_placeholders(entry.msgstr)
message = (
f"{language}: translation changes placeholders for {entry.msgid!r}: "
+ f"expected {source_placeholders}, got {translated_placeholders}"
)
assert translated_placeholders == source_placeholders, message
words = set(translation_text.translate(str.maketrans('', '', string.punctuation + '%n')).lower().split())
bad_words_found = words & (banned_words - IGNORED_WORDS)
assert not bad_words_found, f"Bad language found in {self.name}: '{translation_text}'. Bad word(s): {', '.join(bad_words_found)}"
@pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name)
def test_translation_refs_do_not_include_line_numbers(po_path: Path):
for line in load_po_text(po_path).splitlines():
assert not LINE_NUMBER_REF_RE.match(line), (
f"{po_path.name}: line-number source reference found: {line}"
)
@pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name)
def test_translation_entities_are_valid(po_path: Path):
matches = BAD_ENTITY_RE.findall(load_po_text(po_path))
assert not matches, (
f"{po_path.name}: found '@...;' entity typo(s): {', '.join(sorted(set(matches)))}"
)
-3
View File
@@ -1,3 +0,0 @@
# Multilanguage
[![languages](https://raw.githubusercontent.com/commaai/openpilot/badges/translation_badge.svg)](#)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-138
View File
@@ -1,138 +0,0 @@
#!/usr/bin/env python3
import argparse
import json
import os
import pathlib
import xml.etree.ElementTree as ET
from typing import cast
import requests
TRANSLATIONS_DIR = pathlib.Path(__file__).resolve().parent
TRANSLATIONS_LANGUAGES = TRANSLATIONS_DIR / "languages.json"
OPENAI_MODEL = "gpt-4"
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
OPENAI_PROMPT = "You are a professional translator from English to {language} (ISO 639 language code). " + \
"The following sentence or word is in the GUI of a software called openpilot, translate it accordingly."
def get_language_files(languages: list[str] | None = None) -> dict[str, pathlib.Path]:
files = {}
with open(TRANSLATIONS_LANGUAGES) as fp:
language_dict = json.load(fp)
for filename in language_dict.values():
path = TRANSLATIONS_DIR / f"{filename}.ts"
language = path.stem
if languages is None or language in languages:
files[language] = path
return files
def translate_phrase(text: str, language: str) -> str:
response = requests.post(
"https://api.openai.com/v1/chat/completions",
json={
"model": OPENAI_MODEL,
"messages": [
{
"role": "system",
"content": OPENAI_PROMPT.format(language=language),
},
{
"role": "user",
"content": text,
},
],
"temperature": 0.8,
"max_tokens": 1024,
"top_p": 1,
},
headers={
"Authorization": f"Bearer {OPENAI_API_KEY}",
"Content-Type": "application/json",
},
)
if 400 <= response.status_code < 600:
raise requests.HTTPError(f'Error {response.status_code}: {response.json()}', response=response)
data = response.json()
return cast(str, data["choices"][0]["message"]["content"])
def translate_file(path: pathlib.Path, language: str, all_: bool) -> None:
tree = ET.parse(path)
root = tree.getroot()
for context in root.findall("./context"):
name = context.find("name")
if name is None:
raise ValueError("name not found")
print(f"Context: {name.text}")
for message in context.findall("./message"):
source = message.find("source")
translation = message.find("translation")
if source is None or translation is None:
raise ValueError("source or translation not found")
if not all_ and translation.attrib.get("type") != "unfinished":
continue
llm_translation = translate_phrase(cast(str, source.text), language)
print(f"Source: {source.text}\n" +
f"Current translation: {translation.text}\n" +
f"LLM translation: {llm_translation}")
translation.text = llm_translation
with path.open("w", encoding="utf-8") as fp:
fp.write('<?xml version="1.0" encoding="utf-8"?>\n' +
'<!DOCTYPE TS>\n' +
ET.tostring(root, encoding="utf-8").decode())
def main():
arg_parser = argparse.ArgumentParser("Auto translate")
group = arg_parser.add_mutually_exclusive_group(required=True)
group.add_argument("-a", "--all-files", action="store_true", help="Translate all files")
group.add_argument("-f", "--file", nargs="+", help="Translate the selected files. (Example: -f fr de)")
arg_parser.add_argument("-t", "--all-translations", action="store_true", default=False, help="Translate all sections. (Default: only unfinished)")
args = arg_parser.parse_args()
if OPENAI_API_KEY is None:
print("OpenAI API key is missing. (Hint: use `export OPENAI_API_KEY=YOUR-KEY` before you run the script).\n" +
"If you don't have one go to: https://beta.openai.com/account/api-keys.")
exit(1)
files = get_language_files(None if args.all_files else args.file)
if args.file:
missing_files = set(args.file) - set(files)
if len(missing_files):
print(f"No language files found: {missing_files}")
exit(1)
print(f"Translation mode: {'all' if args.all_translations else 'only unfinished'}. Files: {list(files)}")
for lang, path in files.items():
print(f"Translate {lang} ({path})")
translate_file(path, lang, args.all_translations)
if __name__ == "__main__":
main()
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd)"
ROOT="$DIR/../../../"
cd $DIR
./update_translations.py
command -v codex >/dev/null || {
echo "Install codex CLI to continue:"
echo "-> https://developers.openai.com/codex/cli"
echo
exit 1
}
codex exec --cd "$ROOT" -c 'model_reasoning_effort="low"' --dangerously-bypass-approvals-and-sandbox "$(cat <<EOF
Update openpilot UI translations in selfdrive/ui/translations.
- Translate English UI text naturally.
- Preserve placeholders (%n, %1, {}, {:.1f}), HTML/tags, and plural forms.
- Edit .po files in place.
- Print a short summary of changes.
- All strings should be translated. Don't stop until it's 100%.
- Be mindful of the layout/style of the UI and length of the original English string.
EOF
)"
-108
View File
@@ -1,108 +0,0 @@
#!/usr/bin/env python3
import json
import os
import requests
import xml.etree.ElementTree as ET
from openpilot.common.basedir import BASEDIR
from openpilot.selfdrive.ui.update_translations import LANGUAGES_FILE, TRANSLATIONS_DIR
BADGE_HEIGHT = 20 + 8
SHIELDS_URL = "https://img.shields.io/badge"
def parse_po_file(file_path):
"""
Parse a .po file and count total and unfinished translations.
Returns: (total_translations, unfinished_translations)
"""
with open(file_path) as f:
content = f.read()
total_translations = 0
unfinished_translations = 0
# Split into entries (separated by blank lines)
entries = content.split('\n\n')
for entry in entries:
# Skip header entry (contains Project-Id-Version)
if 'Project-Id-Version' in entry:
continue
# Check if this entry has a msgid (translation entry)
# After skipping header, any entry with msgid " is a translation
# (both msgid "content" and msgid "" for multiline contain msgid ")
if 'msgid "' not in entry:
continue
total_translations += 1
# Check if msgstr is empty (unfinished translation)
if 'msgstr ""' in entry:
# Check if there are continuation lines with content after msgstr ""
lines = entry.split('\n')
msgstr_idx = None
for i, line in enumerate(lines):
if line.strip().startswith('msgstr ""'):
msgstr_idx = i
break
if msgstr_idx is not None:
# Check if any continuation lines have content
has_content = False
for line in lines[msgstr_idx + 1:]:
stripped = line.strip()
# Continuation line with content
if stripped.startswith('"') and len(stripped) > 2:
has_content = True
break
# End of entry
if stripped.startswith(('msgid', '#')) or not stripped:
break
if not has_content:
unfinished_translations += 1
return (total_translations, unfinished_translations)
if __name__ == "__main__":
with open(LANGUAGES_FILE) as f:
translation_files = json.load(f)
badge_svg = []
max_badge_width = 0 # keep track of max width to set parent element
for idx, (name, file) in enumerate(translation_files.items()):
po_file_path = os.path.join(str(TRANSLATIONS_DIR), f"app_{file}.po")
total_translations, unfinished_translations = parse_po_file(po_file_path)
percent_finished = int(100 - (unfinished_translations / total_translations * 100.)) if total_translations > 0 else 0
color = f"rgb{(94, 188, 0) if percent_finished == 100 else (248, 255, 50) if percent_finished > 90 else (204, 55, 27)}"
# Download badge
badge_label = f"LANGUAGE {name}"
badge_message = f"{percent_finished}% complete"
if unfinished_translations != 0:
badge_message += f" ({unfinished_translations} unfinished)"
r = requests.get(f"{SHIELDS_URL}/{badge_label}-{badge_message}-{color}", timeout=10)
assert r.status_code == 200, "Error downloading badge"
content_svg = r.content.decode("utf-8")
xml = ET.fromstring(content_svg)
assert "width" in xml.attrib
max_badge_width = max(max_badge_width, int(xml.attrib["width"]))
# Make tag ids in each badge unique to combine them into one svg
for tag in ("r", "s"):
content_svg = content_svg.replace(f'id="{tag}"', f'id="{tag}{idx}"')
content_svg = content_svg.replace(f'"url(#{tag})"', f'"url(#{tag}{idx})"')
badge_svg.extend([f'<g transform="translate(0, {idx * BADGE_HEIGHT})">', content_svg, "</g>"])
badge_svg.insert(0, '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ' +
f'height="{len(translation_files) * BADGE_HEIGHT}" width="{max_badge_width}">')
badge_svg.append("</svg>")
with open(os.path.join(BASEDIR, "translation_badge.svg"), "w") as badge_f:
badge_f.write("\n".join(badge_svg))
+25 -54
View File
@@ -8,7 +8,6 @@ import ast
import os
import re
from dataclasses import dataclass, field
from datetime import UTC, datetime
from pathlib import Path
@@ -165,18 +164,18 @@ def write_po(path: str | Path, header: POEntry | None, entries: list[POEntry]) -
if header:
for c in header.comments:
f.write(c + '\n')
if header.flags:
f.write('#, ' + ', '.join(header.flags) + '\n')
f.write(f'msgid {_quote("")}\n')
f.write(f'msgstr {_quote(header.msgstr)}\n\n')
for entry in entries:
for c in entry.comments:
f.write(c + '\n')
for ref in entry.source_refs:
# Keep file-level context for translators, but drop line numbers to
# avoid churning PO diffs on unrelated code edits.
source_files = sorted({ref.rsplit(':', 1)[0] for ref in entry.source_refs})
for ref in source_files:
f.write(f'#: {ref}\n')
if entry.flags:
f.write('#, ' + ', '.join(entry.flags) + '\n')
# Runtime loading ignores gettext flags; omit them to reduce noise.
f.write(f'msgid {_quote(entry.msgid)}\n')
if entry.is_plural:
f.write(f'msgid_plural {_quote(entry.msgid_plural)}\n')
@@ -256,31 +255,24 @@ def extract_strings(files: list[str], basedir: str) -> list[POEntry]:
# ──── POT generation ────
def _build_pot_header() -> POEntry:
return POEntry(
msgstr='Content-Type: text/plain; charset=UTF-8\n',
)
def _build_po_header(language: str) -> POEntry:
plural_forms = PLURAL_FORMS.get(language, 'nplurals=2; plural=(n != 1);')
return POEntry(
msgstr='Content-Type: text/plain; charset=UTF-8\n' +
f'Language: {language}\n' +
f'Plural-Forms: {plural_forms}\n',
)
def generate_pot(entries: list[POEntry], pot_path: str | Path) -> None:
"""Generate a .pot template file from extracted entries."""
now = datetime.now(UTC).strftime('%Y-%m-%d %H:%M%z')
header = POEntry(
comments=[
'# SOME DESCRIPTIVE TITLE.',
"# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER",
'# This file is distributed under the same license as the PACKAGE package.',
'# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.',
'#',
],
flags=['fuzzy'],
msgstr='Project-Id-Version: PACKAGE VERSION\n' +
'Report-Msgid-Bugs-To: \n' +
f'POT-Creation-Date: {now}\n' +
'PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n' +
'Last-Translator: FULL NAME <EMAIL@ADDRESS>\n' +
'Language-Team: LANGUAGE <LL@li.org>\n' +
'Language: \n' +
'MIME-Version: 1.0\n' +
'Content-Type: text/plain; charset=UTF-8\n' +
'Content-Transfer-Encoding: 8bit\n' +
'Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n',
)
write_po(pot_path, header, entries)
write_po(pot_path, _build_pot_header(), entries)
# ──── PO init (replaces msginit) ────
@@ -305,43 +297,22 @@ def init_po(pot_path: str | Path, po_path: str | Path, language: str) -> None:
"""Create a new .po file from a .pot template (replaces msginit)."""
_, entries = parse_po(pot_path)
plural_forms = PLURAL_FORMS.get(language, 'nplurals=2; plural=(n != 1);')
now = datetime.now(UTC).strftime('%Y-%m-%d %H:%M%z')
header = POEntry(
comments=[
f'# {language} translations for PACKAGE package.',
"# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER",
'# This file is distributed under the same license as the PACKAGE package.',
'# Automatically generated.',
'#',
],
msgstr='Project-Id-Version: PACKAGE VERSION\n' +
'Report-Msgid-Bugs-To: \n' +
f'POT-Creation-Date: {now}\n' +
f'PO-Revision-Date: {now}\n' +
'Last-Translator: Automatically generated\n' +
'Language-Team: none\n' +
f'Language: {language}\n' +
'MIME-Version: 1.0\n' +
'Content-Type: text/plain; charset=UTF-8\n' +
'Content-Transfer-Encoding: 8bit\n' +
f'Plural-Forms: {plural_forms}\n',
)
nplurals = int(re.search(r'nplurals=(\d+)', plural_forms).group(1))
for e in entries:
if e.is_plural:
e.msgstr_plural = dict.fromkeys(range(nplurals), '')
write_po(po_path, header, entries)
write_po(po_path, _build_po_header(language), entries)
# ──── PO merge (replaces msgmerge) ────
def merge_po(po_path: str | Path, pot_path: str | Path) -> None:
"""Update a .po file with entries from a .pot template (replaces msgmerge --update)."""
po_header, po_entries = parse_po(po_path)
_, po_entries = parse_po(po_path)
_, pot_entries = parse_po(pot_path)
language = Path(po_path).stem.removeprefix("app_")
existing = {e.msgid: e for e in po_entries}
merged = []
@@ -359,4 +330,4 @@ def merge_po(po_path: str | Path, pot_path: str | Path) -> None:
merged.append(pot_e)
merged.sort(key=lambda e: e.msgid)
write_po(po_path, po_header, merged)
write_po(po_path, _build_po_header(language), merged)
+1 -1
View File
@@ -30,7 +30,7 @@ def generate_metadata_pkl(model_path, output_path):
def install_models(model_dir):
model_dir = Path(model_dir)
models = ["driving_off_policy", "driving_policy", "driving_vision"]
models = ["driving_off_policy", "driving_on_policy", "driving_vision"]
found_models = []
for model in models:
+5 -3
View File
@@ -8,14 +8,16 @@ from openpilot.sunnypilot import get_file_hash
DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "common", "model.h")
MODEL_HASH_PATH = os.path.join(BASEDIR, "sunnypilot", "models", "tests", "model_hash")
VISION_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_vision.onnx")
POLICY_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_policy.onnx")
OFF_POLICY_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_off_policy.onnx")
ON_POLICY_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_on_policy.onnx")
def update_model_hash():
vision_hash = get_file_hash(VISION_ONNX_PATH)
policy_hash = get_file_hash(POLICY_ONNX_PATH)
off_policy_hash = get_file_hash(OFF_POLICY_ONNX_PATH)
on_policy_hash = get_file_hash(ON_POLICY_ONNX_PATH)
combined_hash = hashlib.sha256((vision_hash + policy_hash).encode()).hexdigest()
combined_hash = hashlib.sha256((vision_hash + off_policy_hash + on_policy_hash).encode()).hexdigest()
with open(MODEL_HASH_PATH, "w") as f:
f.write(combined_hash)
+1 -1
View File
@@ -116,7 +116,7 @@ class ModelCache:
class ModelFetcher:
"""Handles fetching and caching of model data from remote source"""
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v15.json"
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v16.json"
def __init__(self, params: Params):
self.params = params
+3 -2
View File
@@ -16,8 +16,9 @@ def get_model_runner() -> ModelRunner:
bundle = get_active_bundle()
if bundle and bundle.models:
model_types = {m.type.raw for m in bundle.models}
# Check if the bundle uses separate vision and policy models
if ModelType.vision in model_types or ModelType.policy in model_types:
# Check if the bundle uses separate vision and policy models (legacy or new split format)
split_types = {ModelType.vision, ModelType.policy, ModelType.offPolicy, ModelType.onPolicy}
if model_types & split_types:
return TinygradSplitRunner()
# Otherwise, assume a single model (likely supercombo)
if bundle.models:
@@ -29,6 +29,22 @@ class OffPolicyTinygrad(ModularRunner, ABC):
return result
class OnPolicyTinygrad(ModularRunner, ABC):
"""
A TinygradRunner specialized for on-policy models.
Uses a SplitParser to handle outputs specific to the on-policy part of a split model setup.
"""
def __init__(self):
self._on_policy_parser = SplitParser()
self.parser_method_dict[ModelType.onPolicy] = self._parse_on_policy_outputs
def _parse_on_policy_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
"""Parses on-policy model outputs using SplitParser."""
result: NumpyDict = self._on_policy_parser.parse_policy_outputs(self._slice_outputs(model_outputs))
return result
class PolicyTinygrad(ModularRunner, ABC):
"""
A TinygradRunner specialized for policy-only models.
@@ -3,14 +3,14 @@ import pickle
import numpy as np
from openpilot.sunnypilot.models.runners.constants import NumpyDict, ModelType, ShapeDict, CUSTOM_MODEL_PATH, SliceDict
from openpilot.sunnypilot.models.runners.model_runner import ModelRunner
from openpilot.sunnypilot.models.runners.tinygrad.model_types import PolicyTinygrad, VisionTinygrad, SupercomboTinygrad, OffPolicyTinygrad
from openpilot.sunnypilot.models.runners.tinygrad.model_types import PolicyTinygrad, VisionTinygrad, SupercomboTinygrad, OffPolicyTinygrad, OnPolicyTinygrad
from openpilot.sunnypilot.models.split_model_constants import SplitModelConstants
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants
from tinygrad.tensor import Tensor
class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTinygrad, OffPolicyTinygrad):
class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTinygrad, OffPolicyTinygrad, OnPolicyTinygrad):
"""
A ModelRunner implementation for executing Tinygrad models.
@@ -26,6 +26,7 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny
PolicyTinygrad.__init__(self)
VisionTinygrad.__init__(self)
OffPolicyTinygrad.__init__(self)
OnPolicyTinygrad.__init__(self)
self._constants = ModelConstants
self._model_data = self.models.get(model_type)
if not self._model_data or not self._model_data.model:
@@ -98,20 +99,30 @@ class TinygradSplitRunner(ModelRunner):
super().__init__()
self.is_20hz_3d = True
self.vision_runner = TinygradRunner(ModelType.vision)
self.policy_runner = TinygradRunner(ModelType.policy)
self.policy_runner = TinygradRunner(ModelType.policy) if self.models.get(ModelType.policy) else None
self.off_policy_runner = TinygradRunner(ModelType.offPolicy) if self.models.get(ModelType.offPolicy) else None
self.on_policy_runner = TinygradRunner(ModelType.onPolicy) if self.models.get(ModelType.onPolicy) else None
self._constants = SplitModelConstants
def _run_model(self) -> NumpyDict:
"""Runs both vision and policy models and merges their parsed outputs."""
policy_output = self.policy_runner.run_model()
vision_output = self.vision_runner.run_model()
outputs = {**policy_output, **vision_output}
outputs = {**vision_output}
if self.policy_runner:
policy_output = self.policy_runner.run_model()
outputs.update(policy_output)
if self.off_policy_runner:
off_policy_output = self.off_policy_runner.run_model()
if self.on_policy_runner:
off_policy_output.pop('plan', None)
outputs.update(off_policy_output)
if self.on_policy_runner:
on_policy_output = self.on_policy_runner.run_model()
outputs.update(on_policy_output)
if 'planplus' in outputs and 'plan' in outputs:
outputs['plan'] = outputs['plan'] + outputs['planplus']
@@ -125,31 +136,44 @@ class TinygradSplitRunner(ModelRunner):
@property
def input_shapes(self) -> ShapeDict:
"""Returns the combined input shapes from both vision and policy models."""
shapes = {**self.policy_runner.input_shapes, **self.vision_runner.input_shapes}
shapes = {**self.vision_runner.input_shapes}
if self.policy_runner:
shapes.update(self.policy_runner.input_shapes)
if self.off_policy_runner:
shapes.update(self.off_policy_runner.input_shapes)
if self.on_policy_runner:
shapes.update(self.on_policy_runner.input_shapes)
return shapes
@property
def output_slices(self) -> SliceDict:
"""Returns the combined output slices from both vision and policy models."""
slices = {**self.policy_runner.output_slices, **self.vision_runner.output_slices}
slices = {**self.vision_runner.output_slices}
if self.policy_runner:
slices.update(self.policy_runner.output_slices)
if self.off_policy_runner:
slices.update(self.off_policy_runner.output_slices)
if self.on_policy_runner:
slices.update(self.on_policy_runner.output_slices)
return slices
def prepare_inputs(self, numpy_inputs: NumpyDict) -> dict:
"""Prepares inputs for both vision and policy models."""
# Policy inputs only depend on numpy_inputs
self.policy_runner.prepare_policy_inputs(numpy_inputs)
if self.policy_runner:
self.policy_runner.prepare_policy_inputs(numpy_inputs)
for key in self.vision_input_names:
if key in self.inputs:
self.vision_runner.inputs[key] = self.inputs[key].cast(self.vision_runner.input_to_dtype[key])
inputs = {**self.policy_runner.inputs, **self.vision_runner.inputs}
inputs = {**self.vision_runner.inputs}
if self.policy_runner:
inputs.update(self.policy_runner.inputs)
if self.off_policy_runner:
self.off_policy_runner.prepare_policy_inputs(numpy_inputs)
inputs.update(self.off_policy_runner.inputs)
if self.on_policy_runner:
self.on_policy_runner.prepare_policy_inputs(numpy_inputs)
inputs.update(self.on_policy_runner.inputs)
return inputs
+1 -1
View File
@@ -1 +1 @@
32f57bdc91f910df1f48ddae7c59aaf6e751f9df6756da481a210577dbce8bcf
adfcb5ccac9cfaf291af6091d12e71be3f543c7694fc29d80caa561dc32194d7
@@ -6,16 +6,17 @@ See the LICENSE.md file in the root directory for more details.
"""
from openpilot.sunnypilot import get_file_hash
from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, VISION_ONNX_PATH, POLICY_ONNX_PATH
from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, VISION_ONNX_PATH, OFF_POLICY_ONNX_PATH, ON_POLICY_ONNX_PATH
import hashlib
class TestDefaultModel:
def test_compare_onnx_hashes(self):
vision_hash = get_file_hash(VISION_ONNX_PATH)
policy_hash = get_file_hash(POLICY_ONNX_PATH)
off_policy_hash = get_file_hash(OFF_POLICY_ONNX_PATH)
on_policy_hash = get_file_hash(ON_POLICY_ONNX_PATH)
combined_hash = hashlib.sha256((vision_hash + policy_hash).encode()).hexdigest()
combined_hash = hashlib.sha256((vision_hash + off_policy_hash + on_policy_hash).encode()).hexdigest()
with open(MODEL_HASH_PATH) as f:
current_hash = f.read().strip()
@@ -105,7 +105,7 @@ class TestSpeedLimitAssist:
assert not self.sla.is_active
assert V_CRUISE_UNSET == self.sla.get_v_target_from_control()
@pytest.mark.parametrize("car_name", [RIVIAN.RIVIAN_R1_GEN1, TESLA.TESLA_MODEL_Y], indirect=True)
@pytest.mark.parametrize("car_name", [RIVIAN.RIVIAN_R1, TESLA.TESLA_MODEL_Y], indirect=True)
def test_disallowed_brands(self, car_name):
"""
Speed Limit Assist is disabled for the following brands and conditions:
@@ -588,6 +588,10 @@
"title": "Last Update Uptime Onroad",
"description": ""
},
"LateralManeuverMode": {
"title": "Lateral Maneuver Mode",
"description": ""
},
"LeadDepartAlert": {
"title": "Lead Departure Alert (Beta)",
"description": "A chime and on-screen alert (TIZI/TICI only) will play when you are stopped, and the vehicle in front of you start moving. <br>Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly."
+8 -3
View File
@@ -3,11 +3,16 @@ Import('env', 'arch', 'messaging', 'common', 'visionipc')
libs = [common, messaging, visionipc,
'avformat', 'avcodec', 'swresample', 'avutil', 'x264',
'pthread', 'z', 'm', 'zstd']
frameworks = []
src = ['logger.cc', 'zstd_writer.cc', 'video_writer.cc', 'encoder/encoder.cc', 'encoder/v4l_encoder.cc', 'encoder/jpeg_encoder.cc']
if arch != "larch64":
src += ['encoder/ffmpeg_encoder.cc']
libs += ['yuv']
if arch == "Darwin":
frameworks += ['VideoToolbox', 'CoreMedia', 'CoreFoundation', 'CoreVideo']
else:
libs += ['va', 'va-drm', 'drm']
if arch == "Darwin":
# exclude v4l
@@ -16,9 +21,9 @@ if arch == "Darwin":
logger_lib = env.Library('logger', src)
libs.insert(0, logger_lib)
env.Program('loggerd', ['loggerd.cc'], LIBS=libs)
env.Program('encoderd', ['encoderd.cc'], LIBS=libs + ["jpeg"])
env.Program('bootlog.cc', LIBS=libs)
env.Program('loggerd', ['loggerd.cc'], LIBS=libs, FRAMEWORKS=frameworks)
env.Program('encoderd', ['encoderd.cc'], LIBS=libs + ["jpeg"], FRAMEWORKS=frameworks)
env.Program('bootlog.cc', LIBS=libs, FRAMEWORKS=frameworks)
if GetOption('extras'):
env.Program('tests/test_logger', ['tests/test_runner.cc', 'tests/test_logger.cc', 'tests/test_zstd_writer.cc'], LIBS=libs)
+4
View File
@@ -46,6 +46,9 @@ def not_joystick(started: bool, params: Params, CP: car.CarParams) -> bool:
def long_maneuver(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and params.get_bool("LongitudinalManeuverMode")
def lat_maneuver(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and params.get_bool("LateralManeuverMode")
def not_long_maneuver(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and not params.get_bool("LongitudinalManeuverMode")
@@ -142,6 +145,7 @@ procs = [
PythonProcess("pigeond", "system.ubloxd.pigeond", ublox, enabled=TICI),
PythonProcess("plannerd", "selfdrive.controls.plannerd", not_long_maneuver),
PythonProcess("maneuversd", "tools.longitudinal_maneuvers.maneuversd", long_maneuver),
PythonProcess("lateral_maneuversd", "tools.lateral_maneuvers.lateral_maneuversd", lat_maneuver),
PythonProcess("radard", "selfdrive.controls.radard", only_onroad),
PythonProcess("hardwared", "system.hardware.hardwared", always_run),
PythonProcess("tombstoned", "system.tombstoned", always_run, enabled=not PC),
+5
View File
@@ -457,6 +457,11 @@ class GuiApplication(GuiApplicationExt):
def texture(self, asset_path: str, width: int | None = None, height: int | None = None,
alpha_premultiply=False, keep_aspect_ratio=True, flip_x: bool = False) -> rl.Texture:
if width is not None:
width = round(width)
if height is not None:
height = round(height)
cache_key = f"{asset_path}_{width}_{height}_{alpha_premultiply}_{keep_aspect_ratio}_{flip_x}"
if cache_key in self._textures:
return self._textures[cache_key]
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:57e798d421bb56bb058ed9b0c83dd97fe1e411cde3a2bd6eb4a8705234f69027
size 453096
+10
View File
@@ -12,3 +12,13 @@ cd icons
git fetch --all
git checkout d5aa187483a1b0b186f87adcfa8576350d970d98
cp bootstrap-icons.svg ../
# Convert WOFF → TTF for imgui (imgui only reads TTF/OTF)
python3 -c "
from fontTools.ttLib import TTFont
import io
f = TTFont('font/fonts/bootstrap-icons.woff')
f.flavor = None
f.save('../bootstrap-icons.ttf')
print('bootstrap-icons.ttf written')
"
+4 -5
View File
@@ -4,7 +4,7 @@ import shutil
import libusb
Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal')
Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal', 'replay_lib')
# Detect Qt - skip build if not available
if arch == "Darwin":
@@ -18,9 +18,6 @@ else:
if not has_qt:
Return()
SConscript(['#tools/replay/SConscript'])
Import('replay_lib')
qt_env = env.Clone()
qt_modules = ["Widgets", "Gui", "Core"]
@@ -69,7 +66,7 @@ base_frameworks = qt_env['FRAMEWORKS']
base_libs = [common, messaging, cereal, visionipc, 'm', 'pthread'] + qt_env["LIBS"]
if arch == "Darwin":
base_frameworks.append('QtCharts')
base_frameworks += ['QtCharts', 'CoreFoundation', 'CoreVideo', 'CoreMedia', 'IOKit', 'Security', 'VideoToolbox']
else:
base_libs.append('Qt5Charts')
@@ -78,6 +75,8 @@ cabana_env['CPPPATH'] += [libusb.INCLUDE_DIR]
cabana_env['LIBPATH'] += [libusb.LIB_DIR]
cabana_libs = [cereal, messaging, visionipc, replay_lib, 'avformat', 'avcodec', 'swresample', 'avutil', 'x264', 'z', 'bz2', 'zstd', 'yuv', 'usb-1.0'] + base_libs
if arch != "Darwin":
cabana_libs += ['va', 'va-drm', 'drm']
opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (cabana_env.Dir("../../opendbc/dbc").abspath)
cabana_env['CXXFLAGS'] += [opendbc_path]

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