mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-07-24 22:52:05 +08:00
Merge branch 'master' into hkg-angle-steering-2025
# Conflicts: # opendbc_repo
This commit is contained in:
@@ -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 }}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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')
|
||||
|
||||
@@ -154,6 +154,7 @@ struct ModelManagerSP @0xaedffd8f31e7b55d {
|
||||
vision @2;
|
||||
policy @3;
|
||||
offPolicy @4;
|
||||
onPolicy @5;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-5
@@ -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;
|
||||
|
||||
@@ -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
@@ -1 +1 @@
|
||||
#define DEFAULT_MODEL "CD210 (Default)"
|
||||
#define DEFAULT_MODEL "OP Model (Default)"
|
||||
|
||||
@@ -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"}},
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 msgq_repo updated: ed2777747d...b7688b9bd7
+1
-1
Submodule opendbc_repo updated: dc9db542eb...66ff670a24
+1
-1
Submodule panda updated: 6ddc631bdd...01a2c250f9
@@ -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
Submodule rednose_repo updated: 6ccb8d0556...7ffefa3d88
@@ -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
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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])}
|
||||
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ee29ee5bce84d1ce23e9ff381280de9b4e4d96d2934cd751740354884e112c66
|
||||
size 46877473
|
||||
oid sha256:7af05e03fd170653ff5771baf373a2c57b363da12c4c411cd416dee067b4cf58
|
||||
size 23266366
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)))}"
|
||||
)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Multilanguage
|
||||
|
||||
[](#)
|
||||
+499
-663
File diff suppressed because it is too large
Load Diff
+312
-473
File diff suppressed because it is too large
Load Diff
+298
-459
File diff suppressed because it is too large
Load Diff
+332
-493
File diff suppressed because it is too large
Load Diff
+319
-2223
File diff suppressed because it is too large
Load Diff
+295
-456
File diff suppressed because it is too large
Load Diff
+294
-455
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+459
-625
File diff suppressed because it is too large
Load Diff
+330
-491
File diff suppressed because it is too large
Load Diff
+295
-457
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
@@ -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()
|
||||
Executable
+26
@@ -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
|
||||
)"
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 @@
|
||||
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."
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:57e798d421bb56bb058ed9b0c83dd97fe1e411cde3a2bd6eb4a8705234f69027
|
||||
size 453096
|
||||
Vendored
+10
@@ -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,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
Reference in New Issue
Block a user