mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-11 02:33:51 +08:00
Merge branch 'upstream/openpilot/master' into sync-20251114
# Conflicts: # .github/workflows/ci_weekly_run.yaml # .github/workflows/raylib_ui_preview.yaml # .github/workflows/tests.yaml # .gitmodules # README.md # SConstruct # common/api.py # common/params_keys.h # docs/CARS.md # msgq_repo # opendbc_repo # panda # selfdrive/car/tests/test_car_interfaces.py # selfdrive/controls/controlsd.py # selfdrive/controls/lib/latcontrol.py # selfdrive/controls/lib/latcontrol_angle.py # selfdrive/controls/lib/latcontrol_pid.py # selfdrive/controls/lib/latcontrol_torque.py # selfdrive/controls/tests/test_latcontrol.py # selfdrive/monitoring/helpers.py # selfdrive/ui/SConscript # selfdrive/ui/main.cc # selfdrive/ui/qt/body.h # selfdrive/ui/qt/home.cc # selfdrive/ui/qt/home.h # selfdrive/ui/qt/network/networking.cc # selfdrive/ui/qt/network/networking.h # selfdrive/ui/qt/network/wifi_manager.cc # selfdrive/ui/qt/offroad/developer_panel.cc # selfdrive/ui/qt/offroad/developer_panel.h # selfdrive/ui/qt/offroad/experimental_mode.cc # selfdrive/ui/qt/offroad/firehose.cc # selfdrive/ui/qt/offroad/firehose.h # selfdrive/ui/qt/offroad/onboarding.cc # selfdrive/ui/qt/offroad/onboarding.h # selfdrive/ui/qt/offroad/settings.cc # selfdrive/ui/qt/offroad/settings.h # selfdrive/ui/qt/offroad/software_settings.cc # selfdrive/ui/qt/onroad/alerts.cc # selfdrive/ui/qt/onroad/annotated_camera.h # selfdrive/ui/qt/onroad/buttons.cc # selfdrive/ui/qt/onroad/buttons.h # selfdrive/ui/qt/onroad/driver_monitoring.cc # selfdrive/ui/qt/onroad/hud.cc # selfdrive/ui/qt/onroad/hud.h # selfdrive/ui/qt/onroad/model.cc # selfdrive/ui/qt/onroad/model.h # selfdrive/ui/qt/onroad/onroad_home.cc # selfdrive/ui/qt/onroad/onroad_home.h # selfdrive/ui/qt/request_repeater.h # selfdrive/ui/qt/sidebar.cc # selfdrive/ui/qt/sidebar.h # selfdrive/ui/qt/util.cc # selfdrive/ui/qt/widgets/cameraview.h # selfdrive/ui/qt/widgets/controls.cc # selfdrive/ui/qt/widgets/controls.h # selfdrive/ui/qt/widgets/input.cc # selfdrive/ui/qt/widgets/input.h # selfdrive/ui/qt/widgets/prime.cc # selfdrive/ui/qt/widgets/prime.h # selfdrive/ui/qt/widgets/ssh_keys.h # selfdrive/ui/qt/widgets/toggle.h # selfdrive/ui/qt/widgets/wifi.cc # selfdrive/ui/qt/widgets/wifi.h # selfdrive/ui/qt/window.cc # selfdrive/ui/qt/window.h # selfdrive/ui/tests/cycle_offroad_alerts.py # selfdrive/ui/tests/test_ui/run.py # selfdrive/ui/translations/main_ar.ts # selfdrive/ui/translations/main_de.ts # selfdrive/ui/translations/main_es.ts # selfdrive/ui/translations/main_fr.ts # selfdrive/ui/translations/main_ja.ts # selfdrive/ui/translations/main_ko.ts # selfdrive/ui/translations/main_nl.ts # selfdrive/ui/translations/main_pl.ts # selfdrive/ui/translations/main_pt-BR.ts # selfdrive/ui/translations/main_th.ts # selfdrive/ui/translations/main_tr.ts # selfdrive/ui/translations/main_zh-CHS.ts # selfdrive/ui/translations/main_zh-CHT.ts # selfdrive/ui/ui.cc # selfdrive/ui/ui.h # system/manager/build.py # system/version.py
This commit is contained in:
@@ -3,4 +3,4 @@ SConscript(['controls/lib/lateral_mpc_lib/SConscript'])
|
||||
SConscript(['controls/lib/longitudinal_mpc_lib/SConscript'])
|
||||
SConscript(['locationd/SConscript'])
|
||||
SConscript(['modeld/SConscript'])
|
||||
SConscript(['ui/SConscript'])
|
||||
SConscript(['ui/SConscript'])
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
*.cc
|
||||
fonts/*.fnt
|
||||
fonts/*.png
|
||||
translations_assets.qrc
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:93cdc4ee9aa40e2afceecc63da0ca05ec7aab4bec991ece51a6b52389f48a477
|
||||
size 10788068
|
||||
Executable
+128
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
import pyray as rl
|
||||
|
||||
FONT_DIR = Path(__file__).resolve().parent
|
||||
SELFDRIVE_DIR = FONT_DIR.parents[1]
|
||||
TRANSLATIONS_DIR = SELFDRIVE_DIR / "ui" / "translations"
|
||||
LANGUAGES_FILE = TRANSLATIONS_DIR / "languages.json"
|
||||
|
||||
GLYPH_PADDING = 6
|
||||
EXTRA_CHARS = "–‑✓×°§•€£¥"
|
||||
UNIFONT_LANGUAGES = {"ar", "th", "zh-CHT", "zh-CHS", "ko", "ja"}
|
||||
|
||||
|
||||
def _languages():
|
||||
if not LANGUAGES_FILE.exists():
|
||||
return {}
|
||||
with LANGUAGES_FILE.open(encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def _char_sets():
|
||||
base = set(map(chr, range(32, 127))) | set(EXTRA_CHARS)
|
||||
unifont = set(base)
|
||||
|
||||
for language, code in _languages().items():
|
||||
unifont.update(language)
|
||||
po_path = TRANSLATIONS_DIR / f"app_{code}.po"
|
||||
try:
|
||||
chars = set(po_path.read_text(encoding="utf-8"))
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
(unifont if code in UNIFONT_LANGUAGES else base).update(chars)
|
||||
|
||||
return tuple(sorted(ord(c) for c in base)), tuple(sorted(ord(c) for c in unifont))
|
||||
|
||||
|
||||
def _glyph_metrics(glyphs, rects, codepoints):
|
||||
entries = []
|
||||
min_offset_y, max_extent = None, 0
|
||||
for idx, codepoint in enumerate(codepoints):
|
||||
glyph = glyphs[idx]
|
||||
rect = rects[idx]
|
||||
width = int(round(rect.width))
|
||||
height = int(round(rect.height))
|
||||
offset_y = int(round(glyph.offsetY))
|
||||
min_offset_y = offset_y if min_offset_y is None else min(min_offset_y, offset_y)
|
||||
max_extent = max(max_extent, offset_y + height)
|
||||
entries.append({
|
||||
"id": codepoint,
|
||||
"x": int(round(rect.x)),
|
||||
"y": int(round(rect.y)),
|
||||
"width": width,
|
||||
"height": height,
|
||||
"xoffset": int(round(glyph.offsetX)),
|
||||
"yoffset": offset_y,
|
||||
"xadvance": int(round(glyph.advanceX)),
|
||||
})
|
||||
|
||||
if min_offset_y is None:
|
||||
raise RuntimeError("No glyphs were generated")
|
||||
|
||||
line_height = int(round(max_extent - min_offset_y))
|
||||
base = int(round(max_extent))
|
||||
return entries, line_height, base
|
||||
|
||||
|
||||
def _write_bmfont(path: Path, font_size: int, face: str, atlas_name: str, line_height: int, base: int, atlas_size, entries):
|
||||
lines = [
|
||||
f"info face=\"{face}\" size=-{font_size} bold=0 italic=0 charset=\"\" unicode=1 stretchH=100 smooth=0 aa=1 padding=0,0,0,0 spacing=0,0 outline=0",
|
||||
f"common lineHeight={line_height} base={base} scaleW={atlas_size[0]} scaleH={atlas_size[1]} pages=1 packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4",
|
||||
f"page id=0 file=\"{atlas_name}\"",
|
||||
f"chars count={len(entries)}",
|
||||
]
|
||||
for entry in entries:
|
||||
lines.append(
|
||||
("char id={id:<4} x={x:<5} y={y:<5} width={width:<5} height={height:<5} " +
|
||||
"xoffset={xoffset:<5} yoffset={yoffset:<5} xadvance={xadvance:<5} page=0 chnl=15").format(**entry)
|
||||
)
|
||||
path.write_text("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
def _process_font(font_path: Path, codepoints: tuple[int, ...]):
|
||||
print(f"Processing {font_path.name}...")
|
||||
|
||||
font_size = {
|
||||
"unifont.otf": 16, # unifont is only 16x8 or 16x16 pixels per glyph
|
||||
}.get(font_path.name, 200)
|
||||
|
||||
data = font_path.read_bytes()
|
||||
file_buf = rl.ffi.new("unsigned char[]", data)
|
||||
cp_buffer = rl.ffi.new("int[]", codepoints)
|
||||
cp_ptr = rl.ffi.cast("int *", cp_buffer)
|
||||
glyphs = rl.load_font_data(rl.ffi.cast("unsigned char *", file_buf), len(data), font_size, cp_ptr, len(codepoints), rl.FontType.FONT_DEFAULT)
|
||||
if glyphs == rl.ffi.NULL:
|
||||
raise RuntimeError("raylib failed to load font data")
|
||||
|
||||
rects_ptr = rl.ffi.new("Rectangle **")
|
||||
image = rl.gen_image_font_atlas(glyphs, rects_ptr, len(codepoints), font_size, GLYPH_PADDING, 0)
|
||||
if image.width == 0 or image.height == 0:
|
||||
raise RuntimeError("raylib returned an empty atlas")
|
||||
|
||||
rects = rects_ptr[0]
|
||||
atlas_name = f"{font_path.stem}.png"
|
||||
atlas_path = FONT_DIR / atlas_name
|
||||
entries, line_height, base = _glyph_metrics(glyphs, rects, codepoints)
|
||||
|
||||
if not rl.export_image(image, atlas_path.as_posix()):
|
||||
raise RuntimeError("Failed to export atlas image")
|
||||
|
||||
_write_bmfont(FONT_DIR / f"{font_path.stem}.fnt", font_size, font_path.stem, atlas_name, line_height, base, (image.width, image.height), entries)
|
||||
|
||||
|
||||
def main():
|
||||
base_cp, unifont_cp = _char_sets()
|
||||
fonts = sorted(FONT_DIR.glob("*.ttf")) + sorted(FONT_DIR.glob("*.otf"))
|
||||
for font in fonts:
|
||||
if "emoji" in font.name.lower():
|
||||
continue
|
||||
glyphs = unifont_cp if font.stem.lower().startswith("unifont") else base_cp
|
||||
_process_font(font, glyphs)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9712a9bc089af7ddc06e0826aa84f2ee23ed2f1a1dddaf2a89c2483e753a8475
|
||||
size 5321484
|
||||
@@ -62,8 +62,8 @@ class TestCarInterfaces:
|
||||
# hypothesis also slows down significantly with just one more message draw
|
||||
LongControl(car_params, car_params_sp)
|
||||
if car_params.steerControlType == CarParams.SteerControlType.angle:
|
||||
LatControlAngle(car_params, car_params_sp, car_interface)
|
||||
LatControlAngle(car_params, car_params_sp, car_interface, DT_CTRL)
|
||||
elif car_params.lateralTuning.which() == 'pid':
|
||||
LatControlPID(car_params, car_params_sp, car_interface)
|
||||
LatControlPID(car_params, car_params_sp, car_interface, DT_CTRL)
|
||||
elif car_params.lateralTuning.which() == 'torque':
|
||||
LatControlTorque(car_params, car_params_sp, car_interface)
|
||||
LatControlTorque(car_params, car_params_sp, car_interface, DT_CTRL)
|
||||
|
||||
@@ -189,7 +189,7 @@ class TestCarModelBase(unittest.TestCase):
|
||||
if tuning == 'pid':
|
||||
self.assertTrue(len(self.CP.lateralTuning.pid.kpV))
|
||||
elif tuning == 'torque':
|
||||
self.assertTrue(self.CP.lateralTuning.torque.kf > 0)
|
||||
self.assertTrue(self.CP.lateralTuning.torque.latAccelFactor > 0)
|
||||
else:
|
||||
raise Exception("unknown tuning")
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from cereal import car, log
|
||||
import cereal.messaging as messaging
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper
|
||||
from openpilot.common.realtime import config_realtime_process, DT_CTRL, Priority, Ratekeeper
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
from opendbc.car.car_helpers import interfaces
|
||||
@@ -19,6 +19,7 @@ from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
|
||||
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD
|
||||
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
|
||||
from openpilot.selfdrive.controls.lib.longcontrol import LongControl
|
||||
from openpilot.selfdrive.modeld.modeld import LAT_SMOOTH_SECONDS
|
||||
from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose
|
||||
|
||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||
@@ -45,7 +46,7 @@ class Controls(ControlsExt, ModelStateBase):
|
||||
|
||||
self.CI = interfaces[self.CP.carFingerprint](self.CP, self.CP_SP)
|
||||
|
||||
self.sm = messaging.SubMaster(['liveParameters', 'liveTorqueParameters', 'modelV2', 'selfdriveState',
|
||||
self.sm = messaging.SubMaster(['liveDelay', 'liveParameters', 'liveTorqueParameters', 'modelV2', 'selfdriveState',
|
||||
'liveCalibration', 'livePose', 'longitudinalPlan', 'carState', 'carOutput',
|
||||
'driverMonitoringState', 'onroadEvents', 'driverAssistance', 'liveDelay'] + self.sm_services_ext,
|
||||
poll='selfdriveState')
|
||||
@@ -62,11 +63,11 @@ class Controls(ControlsExt, ModelStateBase):
|
||||
self.VM = VehicleModel(self.CP)
|
||||
self.LaC: LatControl
|
||||
if self.CP.steerControlType == car.CarParams.SteerControlType.angle:
|
||||
self.LaC = LatControlAngle(self.CP, self.CP_SP, self.CI)
|
||||
self.LaC = LatControlAngle(self.CP, self.CP_SP, self.CI, DT_CTRL)
|
||||
elif self.CP.lateralTuning.which() == 'pid':
|
||||
self.LaC = LatControlPID(self.CP, self.CP_SP, self.CI)
|
||||
self.LaC = LatControlPID(self.CP, self.CP_SP, self.CI, DT_CTRL)
|
||||
elif self.CP.lateralTuning.which() == 'torque':
|
||||
self.LaC = LatControlTorque(self.CP, self.CP_SP, self.CI)
|
||||
self.LaC = LatControlTorque(self.CP, self.CP_SP, self.CI, DT_CTRL)
|
||||
|
||||
def update(self):
|
||||
self.sm.update(15)
|
||||
@@ -139,11 +140,12 @@ class Controls(ControlsExt, ModelStateBase):
|
||||
# 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
|
||||
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
|
||||
|
||||
actuators.curvature = self.desired_curvature
|
||||
steer, steeringAngleDeg, lac_log = self.LaC.update(CC.latActive, CS, self.VM, lp,
|
||||
self.steer_limited_by_safety, self.desired_curvature,
|
||||
self.calibrated_pose, curvature_limited) # TODO what if not available
|
||||
self.calibrated_pose, curvature_limited, lat_delay)
|
||||
actuators.torque = float(steer)
|
||||
actuators.steeringAngleDeg = float(steeringAngleDeg)
|
||||
# Ensure no NaNs/Infs
|
||||
|
||||
@@ -22,7 +22,7 @@ def smooth_value(val, prev_val, tau, dt=DT_MDL):
|
||||
alpha = 1 - np.exp(-dt/tau) if tau > 0 else 1
|
||||
return alpha * val + (1 - alpha) * prev_val
|
||||
|
||||
def clip_curvature(v_ego, prev_curvature, new_curvature, roll):
|
||||
def clip_curvature(v_ego, prev_curvature, new_curvature, roll) -> tuple[float, bool]:
|
||||
# This function respects ISO lateral jerk and acceleration limits + a max curvature
|
||||
v_ego = max(v_ego, MIN_SPEED)
|
||||
max_curvature_rate = MAX_LATERAL_JERK / (v_ego ** 2) # inexact calculation, check https://github.com/commaai/openpilot/pull/24755
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import numpy as np
|
||||
from abc import abstractmethod, ABC
|
||||
|
||||
from openpilot.common.realtime import DT_CTRL
|
||||
from openpilot.selfdrive.locationd.helpers import Pose
|
||||
|
||||
|
||||
class LatControl(ABC):
|
||||
def __init__(self, CP, CP_SP, CI):
|
||||
self.sat_count_rate = 1.0 * DT_CTRL
|
||||
def __init__(self, CP, CP_SP, CI, dt):
|
||||
self.dt = dt
|
||||
self.sat_limit = CP.steerLimitTimer
|
||||
self.sat_count = 0.
|
||||
self.sat_time = 0.
|
||||
self.sat_check_min_speed = 10.
|
||||
|
||||
# we define the steer torque scale as [-1.0...1.0]
|
||||
self.steer_max = 1.0
|
||||
|
||||
@abstractmethod
|
||||
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited):
|
||||
def update(self, active: bool, CS, VM, params, steer_limited_by_safety: bool, desired_curvature: float, calibrated_pose: Pose,
|
||||
curvature_limited: bool, lat_delay: float):
|
||||
pass
|
||||
|
||||
def reset(self):
|
||||
self.sat_count = 0.
|
||||
self.sat_time = 0.
|
||||
|
||||
def _check_saturation(self, saturated, CS, steer_limited_by_safety, curvature_limited):
|
||||
# Saturated only if control output is not being limited by car torque/angle rate limits
|
||||
if (saturated or curvature_limited) and CS.vEgo > self.sat_check_min_speed and not steer_limited_by_safety and not CS.steeringPressed:
|
||||
self.sat_count += self.sat_count_rate
|
||||
self.sat_time += self.dt
|
||||
else:
|
||||
self.sat_count -= self.sat_count_rate
|
||||
self.sat_count = np.clip(self.sat_count, 0.0, self.sat_limit)
|
||||
return self.sat_count > (self.sat_limit - 1e-3)
|
||||
self.sat_time -= self.dt
|
||||
self.sat_time = np.clip(self.sat_time, 0.0, self.sat_limit)
|
||||
return self.sat_time > (self.sat_limit - 1e-3)
|
||||
|
||||
@@ -8,12 +8,12 @@ STEER_ANGLE_SATURATION_THRESHOLD = 2.5 # Degrees
|
||||
|
||||
|
||||
class LatControlAngle(LatControl):
|
||||
def __init__(self, CP, CP_SP, CI):
|
||||
super().__init__(CP, CP_SP, CI)
|
||||
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"
|
||||
|
||||
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited):
|
||||
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()
|
||||
|
||||
if not active:
|
||||
|
||||
@@ -6,14 +6,15 @@ from openpilot.common.pid import PIDController
|
||||
|
||||
|
||||
class LatControlPID(LatControl):
|
||||
def __init__(self, CP, CP_SP, CI):
|
||||
super().__init__(CP, CP_SP, CI)
|
||||
def __init__(self, CP, CP_SP, CI, dt):
|
||||
super().__init__(CP, CP_SP, CI, dt)
|
||||
self.pid = PIDController((CP.lateralTuning.pid.kpBP, CP.lateralTuning.pid.kpV),
|
||||
(CP.lateralTuning.pid.kiBP, CP.lateralTuning.pid.kiV),
|
||||
k_f=CP.lateralTuning.pid.kf, pos_limit=self.steer_max, neg_limit=-self.steer_max)
|
||||
pos_limit=self.steer_max, neg_limit=-self.steer_max)
|
||||
self.ff_factor = CP.lateralTuning.pid.kf
|
||||
self.get_steer_feedforward = CI.get_steer_feedforward_function()
|
||||
|
||||
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited):
|
||||
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited, lat_delay):
|
||||
pid_log = log.ControlsState.LateralPIDState.new_message()
|
||||
pid_log.steeringAngleDeg = float(CS.steeringAngleDeg)
|
||||
pid_log.steeringRateDeg = float(CS.steeringRateDeg)
|
||||
@@ -30,7 +31,7 @@ class LatControlPID(LatControl):
|
||||
|
||||
else:
|
||||
# offset does not contribute to resistive torque
|
||||
ff = self.get_steer_feedforward(angle_steers_des_no_offset, CS.vEgo)
|
||||
ff = self.ff_factor * self.get_steer_feedforward(angle_steers_des_no_offset, CS.vEgo)
|
||||
freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
|
||||
|
||||
output_torque = self.pid.update(error,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import math
|
||||
import numpy as np
|
||||
from collections import deque
|
||||
|
||||
from cereal import log
|
||||
from opendbc.car.lateral import FRICTION_THRESHOLD, get_friction
|
||||
from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
|
||||
from openpilot.common.pid import PIDController
|
||||
|
||||
@@ -15,25 +17,34 @@ from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext import La
|
||||
# wheel slip, or to speed.
|
||||
|
||||
# This controller applies torque to achieve desired lateral
|
||||
# accelerations. To compensate for the low speed effects we
|
||||
# use a LOW_SPEED_FACTOR in the error. Additionally, there is
|
||||
# friction in the steering wheel that needs to be overcome to
|
||||
# move it at all, this is compensated for too.
|
||||
# accelerations. To compensate for the low speed effects the
|
||||
# proportional gain is increased at low speeds by the PID controller.
|
||||
# Additionally, there is friction in the steering wheel that needs
|
||||
# to be overcome to move it at all, this is compensated for too.
|
||||
|
||||
LOW_SPEED_X = [0, 10, 20, 30]
|
||||
LOW_SPEED_Y = [15, 13, 10, 5]
|
||||
KP = 1.0
|
||||
KI = 0.3
|
||||
KD = 0.0
|
||||
INTERP_SPEEDS = [1, 1.5, 2.0, 3.0, 5, 7.5, 10, 15, 30]
|
||||
KP_INTERP = [250, 120, 65, 30, 11.5, 5.5, 3.5, 2.0, KP]
|
||||
|
||||
LP_FILTER_CUTOFF_HZ = 1.2
|
||||
LAT_ACCEL_REQUEST_BUFFER_SECONDS = 1.0
|
||||
VERSION = 0
|
||||
|
||||
class LatControlTorque(LatControl):
|
||||
def __init__(self, CP, CP_SP, CI):
|
||||
super().__init__(CP, CP_SP, CI)
|
||||
def __init__(self, CP, CP_SP, CI, dt):
|
||||
super().__init__(CP, CP_SP, CI, dt)
|
||||
self.torque_params = CP.lateralTuning.torque.as_builder()
|
||||
self.torque_from_lateral_accel = CI.torque_from_lateral_accel()
|
||||
self.lateral_accel_from_torque = CI.lateral_accel_from_torque()
|
||||
self.pid = PIDController(self.torque_params.kp, self.torque_params.ki,
|
||||
k_f=self.torque_params.kf)
|
||||
self.pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI, KD, rate=1/self.dt)
|
||||
self.update_limits()
|
||||
self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg
|
||||
self.lat_accel_request_buffer_len = int(LAT_ACCEL_REQUEST_BUFFER_SECONDS / self.dt)
|
||||
self.lat_accel_request_buffer = deque([0.] * self.lat_accel_request_buffer_len , maxlen=self.lat_accel_request_buffer_len)
|
||||
self.previous_measurement = 0.0
|
||||
self.measurement_rate_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), self.dt)
|
||||
|
||||
self.extension = LatControlTorqueExt(self, CP, CP_SP, CI)
|
||||
|
||||
@@ -47,57 +58,68 @@ class LatControlTorque(LatControl):
|
||||
self.pid.set_limits(self.lateral_accel_from_torque(self.steer_max, self.torque_params),
|
||||
self.lateral_accel_from_torque(-self.steer_max, self.torque_params))
|
||||
|
||||
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited):
|
||||
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited, lat_delay):
|
||||
# Override torque params from extension
|
||||
if self.extension.update_override_torque_params(self.torque_params):
|
||||
self.update_limits()
|
||||
|
||||
pid_log = log.ControlsState.LateralTorqueState.new_message()
|
||||
pid_log.version = VERSION
|
||||
if not active:
|
||||
output_torque = 0.0
|
||||
pid_log.active = False
|
||||
else:
|
||||
actual_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll)
|
||||
measured_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll)
|
||||
roll_compensation = params.roll * ACCELERATION_DUE_TO_GRAVITY
|
||||
curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0))
|
||||
|
||||
desired_lateral_accel = desired_curvature * CS.vEgo ** 2
|
||||
actual_lateral_accel = actual_curvature * CS.vEgo ** 2
|
||||
lateral_accel_deadzone = curvature_deadzone * CS.vEgo ** 2
|
||||
|
||||
low_speed_factor = np.interp(CS.vEgo, LOW_SPEED_X, LOW_SPEED_Y)**2
|
||||
setpoint = desired_lateral_accel + low_speed_factor * desired_curvature
|
||||
measurement = actual_lateral_accel + low_speed_factor * actual_curvature
|
||||
gravity_adjusted_lateral_accel = desired_lateral_accel - roll_compensation
|
||||
delay_frames = int(np.clip(lat_delay / self.dt, 1, self.lat_accel_request_buffer_len))
|
||||
expected_lateral_accel = self.lat_accel_request_buffer[-delay_frames]
|
||||
# TODO factor out lateral jerk from error to later replace it with delay independent alternative
|
||||
future_desired_lateral_accel = desired_curvature * CS.vEgo ** 2
|
||||
self.lat_accel_request_buffer.append(future_desired_lateral_accel)
|
||||
gravity_adjusted_future_lateral_accel = future_desired_lateral_accel - roll_compensation
|
||||
desired_lateral_jerk = (future_desired_lateral_accel - expected_lateral_accel) / lat_delay
|
||||
|
||||
measurement = measured_curvature * CS.vEgo ** 2
|
||||
measurement_rate = self.measurement_rate_filter.update((measurement - self.previous_measurement) / self.dt)
|
||||
self.previous_measurement = measurement
|
||||
|
||||
setpoint = lat_delay * desired_lateral_jerk + expected_lateral_accel
|
||||
error = setpoint - measurement
|
||||
|
||||
# do error correction in lateral acceleration space, convert at end to handle non-linear torque responses correctly
|
||||
pid_log.error = float(setpoint - measurement)
|
||||
ff = gravity_adjusted_lateral_accel
|
||||
pid_log.error = float(error)
|
||||
ff = gravity_adjusted_future_lateral_accel
|
||||
# latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll
|
||||
ff -= self.torque_params.latAccelOffset
|
||||
ff += get_friction(desired_lateral_accel - actual_lateral_accel, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
|
||||
# TODO jerk is weighted by lat_delay for legacy reasons, but should be made independent of it
|
||||
ff += get_friction(error, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
|
||||
|
||||
freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
|
||||
output_lataccel = self.pid.update(pid_log.error,
|
||||
feedforward=ff,
|
||||
speed=CS.vEgo,
|
||||
freeze_integrator=freeze_integrator)
|
||||
-measurement_rate,
|
||||
feedforward=ff,
|
||||
speed=CS.vEgo,
|
||||
freeze_integrator=freeze_integrator)
|
||||
output_torque = self.torque_from_lateral_accel(output_lataccel, self.torque_params)
|
||||
|
||||
# Lateral acceleration torque controller extension updates
|
||||
# Overrides pid_log.error and output_torque
|
||||
pid_log, output_torque = self.extension.update(CS, VM, self.pid, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation,
|
||||
desired_lateral_accel, actual_lateral_accel, lateral_accel_deadzone, gravity_adjusted_lateral_accel,
|
||||
desired_curvature, actual_curvature, steer_limited_by_safety, output_torque)
|
||||
future_desired_lateral_accel, measurement, lateral_accel_deadzone, gravity_adjusted_future_lateral_accel,
|
||||
desired_curvature, measured_curvature, steer_limited_by_safety, output_torque)
|
||||
|
||||
pid_log.active = True
|
||||
pid_log.p = float(self.pid.p)
|
||||
pid_log.i = float(self.pid.i)
|
||||
pid_log.d = float(self.pid.d)
|
||||
pid_log.f = float(self.pid.f)
|
||||
pid_log.output = float(-output_torque) # TODO: log lat accel?
|
||||
pid_log.actualLateralAccel = float(actual_lateral_accel)
|
||||
pid_log.desiredLateralAccel = float(desired_lateral_accel)
|
||||
pid_log.output = float(-output_torque) # TODO: log lat accel?
|
||||
pid_log.actualLateralAccel = float(measurement)
|
||||
pid_log.desiredLateralAccel = float(setpoint)
|
||||
pid_log.desiredLateralJerk = float(desired_lateral_jerk)
|
||||
pid_log.saturated = bool(self._check_saturation(self.steer_max - abs(output_torque) < 1e-3, CS, steer_limited_by_safety, curvature_limited))
|
||||
|
||||
# TODO left is positive in this convention
|
||||
|
||||
@@ -54,7 +54,7 @@ class LongControl:
|
||||
self.long_control_state = LongCtrlState.off
|
||||
self.pid = PIDController((CP.longitudinalTuning.kpBP, CP.longitudinalTuning.kpV),
|
||||
(CP.longitudinalTuning.kiBP, CP.longitudinalTuning.kiV),
|
||||
k_f=CP.longitudinalTuning.kf, rate=1 / DT_CTRL)
|
||||
rate=1 / DT_CTRL)
|
||||
self.last_output_accel = 0.0
|
||||
|
||||
def reset(self):
|
||||
|
||||
@@ -7,6 +7,7 @@ from opendbc.car.toyota.values import CAR as TOYOTA
|
||||
from opendbc.car.nissan.values import CAR as NISSAN
|
||||
from opendbc.car.gm.values import CAR as GM
|
||||
from opendbc.car.vehicle_model import VehicleModel
|
||||
from openpilot.common.realtime import DT_CTRL
|
||||
from openpilot.selfdrive.car.helpers import convert_to_capnp
|
||||
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
|
||||
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
|
||||
@@ -29,7 +30,7 @@ class TestLatControl:
|
||||
CP_SP = convert_to_capnp(CP_SP)
|
||||
VM = VehicleModel(CP)
|
||||
|
||||
controller = controller(CP.as_reader(), CP_SP.as_reader(), CI)
|
||||
controller = controller(CP.as_reader(), CP_SP.as_reader(), CI, DT_CTRL)
|
||||
|
||||
CS = car.CarState.new_message()
|
||||
CS.vEgo = 30
|
||||
@@ -42,13 +43,13 @@ class TestLatControl:
|
||||
|
||||
# Saturate for curvature limited and controller limited
|
||||
for _ in range(1000):
|
||||
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, True)
|
||||
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, True, 0.2)
|
||||
assert lac_log.saturated
|
||||
|
||||
for _ in range(1000):
|
||||
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, False)
|
||||
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, False, 0.2)
|
||||
assert not lac_log.saturated
|
||||
|
||||
for _ in range(1000):
|
||||
_, _, lac_log = controller.update(True, CS, VM, params, False, 1, pose, False)
|
||||
_, _, lac_log = controller.update(True, CS, VM, params, False, 1, pose, False, 0.2)
|
||||
assert lac_log.saturated
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections import defaultdict
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from cereal.services import SERVICE_LIST
|
||||
from openpilot.common.file_helpers import LOG_COMPRESSION_LEVEL
|
||||
from openpilot.common.utils import LOG_COMPRESSION_LEVEL
|
||||
from openpilot.tools.lib.logreader import LogReader
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import numpy as np
|
||||
from collections import deque, defaultdict
|
||||
|
||||
@@ -250,6 +251,8 @@ class TorqueEstimator(ParameterEstimator, TorqueEstimatorExt):
|
||||
def main(demo=False):
|
||||
config_realtime_process([0, 1, 2, 3], 5)
|
||||
|
||||
DEBUG = bool(int(os.getenv("DEBUG", "0")))
|
||||
|
||||
pm = messaging.PubMaster(['liveTorqueParameters'])
|
||||
sm = messaging.SubMaster(['carControl', 'carOutput', 'carState', 'liveCalibration', 'livePose', 'liveDelay'], poll='livePose')
|
||||
|
||||
@@ -268,7 +271,7 @@ def main(demo=False):
|
||||
|
||||
# 4Hz driven by livePose
|
||||
if sm.frame % 5 == 0:
|
||||
pm.send('liveTorqueParameters', estimator.get_msg(valid=sm.all_checks()))
|
||||
pm.send('liveTorqueParameters', estimator.get_msg(valid=sm.all_checks(), with_points=DEBUG))
|
||||
|
||||
# Cache points every 60 seconds while onroad
|
||||
if sm.frame % 240 == 0:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import os
|
||||
import glob
|
||||
|
||||
Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'gpucommon', 'visionipc', 'transformations')
|
||||
Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'visionipc', 'transformations')
|
||||
lenv = env.Clone()
|
||||
lenvCython = envCython.Clone()
|
||||
|
||||
libs = [cereal, messaging, visionipc, gpucommon, common, 'capnp', 'kj', 'pthread']
|
||||
libs = [cereal, messaging, visionipc, common, 'capnp', 'kj', 'pthread']
|
||||
frameworks = []
|
||||
|
||||
common_src = [
|
||||
|
||||
@@ -25,13 +25,13 @@ from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from
|
||||
MODEL_WIDTH, MODEL_HEIGHT = DM_INPUT_SIZE
|
||||
CALIB_LEN = 3
|
||||
FEATURE_LEN = 512
|
||||
OUTPUT_SIZE = 84 + FEATURE_LEN
|
||||
OUTPUT_SIZE = 83 + FEATURE_LEN
|
||||
|
||||
PROCESS_NAME = "selfdrive.modeld.dmonitoringmodeld"
|
||||
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
||||
MODEL_PKL_PATH = Path(__file__).parent / 'models/dmonitoring_model_tinygrad.pkl'
|
||||
|
||||
|
||||
# TODO: slice from meta
|
||||
class DriverStateResult(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("face_orientation", ctypes.c_float*3),
|
||||
@@ -46,8 +46,8 @@ class DriverStateResult(ctypes.Structure):
|
||||
("left_blink_prob", ctypes.c_float),
|
||||
("right_blink_prob", ctypes.c_float),
|
||||
("sunglasses_prob", ctypes.c_float),
|
||||
("occluded_prob", ctypes.c_float),
|
||||
("ready_prob", ctypes.c_float*4),
|
||||
("_unused_c", ctypes.c_float),
|
||||
("_unused_d", ctypes.c_float*4),
|
||||
("not_ready_prob", ctypes.c_float*2)]
|
||||
|
||||
|
||||
@@ -55,7 +55,6 @@ class DMonitoringModelResult(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("driver_state_lhd", DriverStateResult),
|
||||
("driver_state_rhd", DriverStateResult),
|
||||
("poor_vision_prob", ctypes.c_float),
|
||||
("wheel_on_right_prob", ctypes.c_float),
|
||||
("features", ctypes.c_float*FEATURE_LEN)]
|
||||
|
||||
@@ -107,8 +106,6 @@ def fill_driver_state(msg, ds_result: DriverStateResult):
|
||||
msg.leftBlinkProb = float(sigmoid(ds_result.left_blink_prob))
|
||||
msg.rightBlinkProb = float(sigmoid(ds_result.right_blink_prob))
|
||||
msg.sunglassesProb = float(sigmoid(ds_result.sunglasses_prob))
|
||||
msg.occludedProb = float(sigmoid(ds_result.occluded_prob))
|
||||
msg.readyProb = [float(sigmoid(x)) for x in ds_result.ready_prob]
|
||||
msg.notReadyProb = [float(sigmoid(x)) for x in ds_result.not_ready_prob]
|
||||
|
||||
|
||||
@@ -119,7 +116,6 @@ def get_driverstate_packet(model_output: np.ndarray, frame_id: int, location_ts:
|
||||
ds.frameId = frame_id
|
||||
ds.modelExecutionTime = execution_time
|
||||
ds.gpuExecutionTime = gpu_execution_time
|
||||
ds.poorVisionProb = float(sigmoid(model_result.poor_vision_prob))
|
||||
ds.wheelOnRightProb = float(sigmoid(model_result.wheel_on_right_prob))
|
||||
ds.rawPredictions = model_output.tobytes() if SEND_RAW_PRED else b''
|
||||
fill_driver_state(ds.leftDriverData, model_result.driver_state_lhd)
|
||||
|
||||
@@ -62,6 +62,5 @@ Refer to **slice_outputs** and **parse_vision_outputs/parse_policy_outputs** in
|
||||
* (deprecated) distracted probabilities: 2
|
||||
* using phone probability: 1
|
||||
* distracted probability: 1
|
||||
* common outputs 2
|
||||
* poor camera vision probability: 1
|
||||
* common outputs 1
|
||||
* left hand drive probability: 1
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
fa69be01-b430-4504-9d72-7dcb058eb6dd
|
||||
d9fb22d1c4fa3ca3d201dbc8edf1d0f0918e53e6
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:50efe6451a3fb3fa04b6bb0e846544533329bd46ecefe9e657e91214dee2aaeb
|
||||
size 7196502
|
||||
oid sha256:3a53626ab84757813fb16a1441704f2ae7192bef88c331bdc2415be6981d204f
|
||||
size 7191776
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ebb38a934d6472c061cc6010f46d9720ca132d631a47e585a893bdd41ade2419
|
||||
size 12343535
|
||||
oid sha256:c5a1f0655ddf266ed42ad1980389d96f47cc5e756da1fa3ca1477a920bb9b157
|
||||
size 13926324
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:befac016a247b7ad5dc5b55d339d127774ed7bd2b848f1583f72aa4caee37781
|
||||
size 46271991
|
||||
oid sha256:8f16d548ea4eb5d01518a9e90d4527cd97c31a84bcaf6f695dead8f0015fecc4
|
||||
size 46271942
|
||||
|
||||
@@ -4,11 +4,13 @@ import numpy as np
|
||||
from cereal import car, log
|
||||
import cereal.messaging as messaging
|
||||
from openpilot.selfdrive.selfdrived.events import Events
|
||||
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
|
||||
from openpilot.common.realtime import DT_DMON
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.stat_live import RunningStatFilter
|
||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
|
||||
EventName = log.OnroadEvent.EventName
|
||||
|
||||
@@ -34,12 +36,13 @@ class DRIVER_MONITOR_SETTINGS:
|
||||
self._SG_THRESHOLD = 0.9
|
||||
self._BLINK_THRESHOLD = 0.865
|
||||
|
||||
self._EE_THRESH11 = 0.4
|
||||
if HARDWARE.get_device_type() == 'mici':
|
||||
self._EE_THRESH11 = 0.75
|
||||
else:
|
||||
self._EE_THRESH11 = 0.4
|
||||
self._EE_THRESH12 = 15.0
|
||||
self._EE_MAX_OFFSET1 = 0.06
|
||||
self._EE_MIN_OFFSET1 = 0.025
|
||||
self._EE_THRESH21 = 0.01
|
||||
self._EE_THRESH22 = 0.35
|
||||
|
||||
self._POSE_PITCH_THRESHOLD = 0.3133
|
||||
self._POSE_PITCH_THRESHOLD_SLACK = 0.3237
|
||||
@@ -55,6 +58,9 @@ class DRIVER_MONITOR_SETTINGS:
|
||||
self._YAW_MAX_OFFSET = 0.289
|
||||
self._YAW_MIN_OFFSET = -0.0246
|
||||
|
||||
self._DCAM_UNCERTAIN_ALERT_THRESHOLD = 0.1
|
||||
self._DCAM_UNCERTAIN_ALERT_COUNT = int(60 / self._DT_DMON)
|
||||
self._DCAM_UNCERTAIN_RESET_COUNT = int(20 / self._DT_DMON)
|
||||
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
|
||||
@@ -137,11 +143,8 @@ class DriverMonitoring:
|
||||
self.pose = DriverPose(self.settings._POSE_OFFSET_MAX_COUNT)
|
||||
self.blink = DriverBlink()
|
||||
self.eev1 = 0.
|
||||
self.eev2 = 1.
|
||||
self.ee1_offseter = RunningStatFilter(max_trackable=self.settings._POSE_OFFSET_MAX_COUNT)
|
||||
self.ee2_offseter = RunningStatFilter(max_trackable=self.settings._POSE_OFFSET_MAX_COUNT)
|
||||
self.ee1_calibrated = False
|
||||
self.ee2_calibrated = False
|
||||
|
||||
self.always_on = always_on
|
||||
self.distracted_types = []
|
||||
@@ -159,6 +162,9 @@ class DriverMonitoring:
|
||||
self.hi_stds = 0
|
||||
self.threshold_pre = self.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME
|
||||
self.threshold_prompt = self.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME
|
||||
self.dcam_uncertain_cnt = 0
|
||||
self.dcam_uncertain_alerted = False # once per drive
|
||||
self.dcam_reset_cnt = 0
|
||||
|
||||
self.params = Params()
|
||||
self.too_distracted = self.params.get_bool("DriverTooDistracted")
|
||||
@@ -246,7 +252,7 @@ class DriverMonitoring:
|
||||
|
||||
return distracted_types
|
||||
|
||||
def _update_states(self, driver_state, cal_rpy, car_speed, op_engaged):
|
||||
def _update_states(self, driver_state, cal_rpy, car_speed, op_engaged, standstill):
|
||||
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
|
||||
@@ -262,7 +268,7 @@ class DriverMonitoring:
|
||||
driver_data = driver_state.rightDriverData if self.wheel_on_right else driver_state.leftDriverData
|
||||
if not all(len(x) > 0 for x in (driver_data.faceOrientation, driver_data.facePosition,
|
||||
driver_data.faceOrientationStd, driver_data.facePositionStd,
|
||||
driver_data.readyProb, driver_data.notReadyProb)):
|
||||
driver_data.notReadyProb)):
|
||||
return
|
||||
|
||||
self.face_detected = driver_data.faceProb > self.settings._FACE_THRESHOLD
|
||||
@@ -279,7 +285,6 @@ class DriverMonitoring:
|
||||
self.blink.right = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \
|
||||
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
|
||||
self.eev1 = driver_data.notReadyProb[0]
|
||||
self.eev2 = driver_data.readyProb[0]
|
||||
|
||||
self.distracted_types = self._get_distracted_types()
|
||||
self.driver_distracted = (DistractedType.DISTRACTED_E2E in self.distracted_types or DistractedType.DISTRACTED_POSE in self.distracted_types
|
||||
@@ -293,12 +298,20 @@ class DriverMonitoring:
|
||||
self.pose.pitch_offseter.push_and_update(self.pose.pitch)
|
||||
self.pose.yaw_offseter.push_and_update(self.pose.yaw)
|
||||
self.ee1_offseter.push_and_update(self.eev1)
|
||||
self.ee2_offseter.push_and_update(self.eev2)
|
||||
|
||||
self.pose.calibrated = self.pose.pitch_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT and \
|
||||
self.pose.yaw_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
|
||||
self.ee1_calibrated = self.ee1_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
|
||||
self.ee2_calibrated = self.ee2_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
|
||||
|
||||
if self.face_detected and not self.driver_distracted:
|
||||
if model_std_max > self.settings._DCAM_UNCERTAIN_ALERT_THRESHOLD:
|
||||
if not standstill:
|
||||
self.dcam_uncertain_cnt += 1
|
||||
self.dcam_reset_cnt = 0
|
||||
else:
|
||||
self.dcam_reset_cnt += 1
|
||||
if self.dcam_reset_cnt > self.settings._DCAM_UNCERTAIN_RESET_COUNT:
|
||||
self.dcam_uncertain_cnt = 0
|
||||
|
||||
self.is_model_uncertain = self.hi_stds > self.settings._HI_STD_FALLBACK_TIME
|
||||
self._set_timers(self.face_detected and not self.is_model_uncertain)
|
||||
@@ -376,6 +389,10 @@ class DriverMonitoring:
|
||||
if alert is not None:
|
||||
self.current_events.add(alert)
|
||||
|
||||
if self.dcam_uncertain_cnt > self.settings._DCAM_UNCERTAIN_ALERT_COUNT and not self.dcam_uncertain_alerted:
|
||||
set_offroad_alert("Offroad_DriverMonitoringUncertain", True)
|
||||
self.dcam_uncertain_alerted = True
|
||||
|
||||
|
||||
def get_state_packet(self, valid=True):
|
||||
# build driverMonitoringState packet
|
||||
@@ -397,6 +414,7 @@ class DriverMonitoring:
|
||||
"hiStdCount": self.hi_stds,
|
||||
"isActiveMode": self.active_monitoring_mode,
|
||||
"isRHD": self.wheel_on_right,
|
||||
"uncertainCount": self.dcam_uncertain_cnt,
|
||||
}
|
||||
return dat
|
||||
|
||||
@@ -412,7 +430,8 @@ class DriverMonitoring:
|
||||
driver_state=sm['driverStateV2'],
|
||||
cal_rpy=sm['liveCalibration'].rpyCalib,
|
||||
car_speed=sm['carState'].vEgo,
|
||||
op_engaged=sm['selfdriveState'].enabled or sm['carControl'].latActive
|
||||
op_engaged=sm['selfdriveState'].enabled or sm['carControl'].latActive,
|
||||
standstill=sm['carState'].standstill,
|
||||
)
|
||||
|
||||
# Update distraction events
|
||||
|
||||
@@ -25,7 +25,6 @@ def make_msg(face_detected, distracted=False, model_uncertain=False):
|
||||
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
|
||||
ds.leftDriverData.readyProb = [0., 0., 0., 0.]
|
||||
ds.leftDriverData.notReadyProb = [0., 0.]
|
||||
return ds
|
||||
|
||||
@@ -54,7 +53,7 @@ class TestMonitoring:
|
||||
DM = DriverMonitoring()
|
||||
events = []
|
||||
for idx in range(len(msgs)):
|
||||
DM._update_states(msgs[idx], [0, 0, 0], 0, engaged[idx])
|
||||
DM._update_states(msgs[idx], [0, 0, 0], 0, engaged[idx], standstill[idx])
|
||||
# cal_rpy and car_speed don't matter here
|
||||
|
||||
# evaluate events at 10Hz for tests
|
||||
|
||||
@@ -80,7 +80,7 @@ Panda *connect(std::string serial="", uint32_t index=0) {
|
||||
}
|
||||
//panda->enable_deepsleep();
|
||||
|
||||
for (int i = 0; i < PANDA_BUS_CNT; i++) {
|
||||
for (int i = 0; i < PANDA_CAN_CNT; i++) {
|
||||
panda->set_can_fd_auto(i, true);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@ import time
|
||||
import cereal.messaging as messaging
|
||||
from cereal import log
|
||||
from openpilot.common.gpio import gpio_set, gpio_init
|
||||
from panda import Panda, PandaDFU, PandaProtocolMismatch
|
||||
from openpilot.common.retry import retry
|
||||
from panda import Panda, PandaDFU
|
||||
from openpilot.system.manager.process_config import managed_processes
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.hardware.tici.pins import GPIO
|
||||
@@ -50,8 +49,7 @@ class TestPandad:
|
||||
assert not Panda.wait_for_dfu(None, 3)
|
||||
assert not Panda.wait_for_panda(None, 3)
|
||||
|
||||
@retry(attempts=3)
|
||||
def _flash_bootstub_and_test(self, fn, expect_mismatch=False):
|
||||
def _flash_bootstub(self, fn):
|
||||
self._go_to_dfu()
|
||||
pd = PandaDFU(None)
|
||||
if fn is None:
|
||||
@@ -61,16 +59,6 @@ class TestPandad:
|
||||
pd.reset()
|
||||
HARDWARE.reset_internal_panda()
|
||||
|
||||
assert Panda.wait_for_panda(None, 10)
|
||||
if expect_mismatch:
|
||||
with pytest.raises(PandaProtocolMismatch):
|
||||
Panda()
|
||||
else:
|
||||
with Panda() as p:
|
||||
assert p.bootstub
|
||||
|
||||
self._run_test(45)
|
||||
|
||||
def test_in_dfu(self):
|
||||
HARDWARE.recover_internal_panda()
|
||||
self._run_test(60)
|
||||
@@ -106,13 +94,14 @@ class TestPandad:
|
||||
print("startup times", ts, sum(ts) / len(ts))
|
||||
assert 0.1 < (sum(ts)/len(ts)) < 0.7
|
||||
|
||||
def test_protocol_version_check(self):
|
||||
# flash old fw
|
||||
fn = os.path.join(HERE, "bootstub.panda_h7_spiv0.bin")
|
||||
self._flash_bootstub_and_test(fn, expect_mismatch=True)
|
||||
def test_old_spi_protocol(self):
|
||||
# flash firmware with old SPI protocol
|
||||
self._flash_bootstub(os.path.join(HERE, "bootstub.panda_h7_spiv0.bin"))
|
||||
self._run_test(45)
|
||||
|
||||
def test_release_to_devel_bootstub(self):
|
||||
self._flash_bootstub_and_test(None)
|
||||
self._flash_bootstub(None)
|
||||
self._run_test(45)
|
||||
|
||||
def test_recover_from_bad_bootstub(self):
|
||||
self._go_to_dfu()
|
||||
|
||||
@@ -9,7 +9,7 @@ from pprint import pprint
|
||||
import cereal.messaging as messaging
|
||||
from cereal import car, log
|
||||
from opendbc.car.can_definitions import CanData
|
||||
from openpilot.common.retry import retry
|
||||
from openpilot.common.utils import retry
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.timeout import Timeout
|
||||
from openpilot.selfdrive.pandad import can_list_to_can_capnp
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
"text": "OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display.\n\n%1",
|
||||
"severity": 0
|
||||
},
|
||||
"Offroad_DriverMonitoringUncertain": {
|
||||
"text": "openpilot detected poor visibility for driver monitoring. Ensure the device has a clear view of the driver. This can be checked using Settings -> Device -> Driver Camera Preview. Extreme lighting conditions and/or unconventional mounting positions may also trigger this alert.",
|
||||
"severity": 0
|
||||
},
|
||||
"Offroad_ExcessiveActuation": {
|
||||
"text": "openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting.",
|
||||
"severity": 1,
|
||||
|
||||
@@ -80,7 +80,7 @@ def below_engage_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.
|
||||
|
||||
def below_steer_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
|
||||
return Alert(
|
||||
f"Steer Unavailable Below {get_display_speed(CP.minSteerSpeed, metric)}",
|
||||
f"Steer Assist Unavailable Below {get_display_speed(CP.minSteerSpeed, metric)}",
|
||||
"",
|
||||
AlertStatus.userPrompt, AlertSize.small,
|
||||
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.4)
|
||||
@@ -322,7 +322,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
|
||||
EventName.steerTempUnavailableSilent: {
|
||||
ET.WARNING: Alert(
|
||||
"Steering Temporarily Unavailable",
|
||||
"Steering Assist Temporarily Unavailable",
|
||||
"",
|
||||
AlertStatus.userPrompt, AlertSize.small,
|
||||
Priority.LOW, VisualAlert.steerRequired, AudibleAlert.prompt, 1.8),
|
||||
@@ -568,7 +568,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
},
|
||||
|
||||
EventName.steerTempUnavailable: {
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Steering Temporarily Unavailable"),
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Steering Assist Temporarily Unavailable"),
|
||||
ET.NO_ENTRY: NoEntryAlert("Steering Temporarily Unavailable"),
|
||||
},
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
afcab1abb62b9d5678342956cced4712f44e909e
|
||||
b508f43fb0481bce0859c9b6ab4f45ee690b8dab
|
||||
@@ -42,6 +42,7 @@ sudo systemctl restart NetworkManager
|
||||
sudo systemctl disable ssh-param-watcher.path
|
||||
sudo systemctl disable ssh-param-watcher.service
|
||||
sudo mount -o ro,remount /
|
||||
sudo systemctl stop power_monitor
|
||||
|
||||
while true; do
|
||||
if ! sudo systemctl is-active -q ssh; then
|
||||
@@ -54,7 +55,6 @@ while true; do
|
||||
# /data/ciui.py &
|
||||
#fi
|
||||
|
||||
awk '{print \$1}' /proc/uptime > /var/tmp/power_watchdog
|
||||
sleep 5s
|
||||
done
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ CPU usage budget
|
||||
TEST_DURATION = 25
|
||||
LOG_OFFSET = 8
|
||||
|
||||
MAX_TOTAL_CPU = 300. # total for all 8 cores
|
||||
MAX_TOTAL_CPU = 350. # total for all 8 cores
|
||||
PROCS = {
|
||||
# Baseline CPU usage by process
|
||||
"selfdrive.controls.controlsd": 16.0,
|
||||
@@ -42,7 +42,7 @@ PROCS = {
|
||||
"./encoderd": 13.0,
|
||||
"./camerad": 10.0,
|
||||
"selfdrive.controls.plannerd": 8.0,
|
||||
"./ui": 18.0,
|
||||
"selfdrive.ui.ui": 40.0,
|
||||
"system.sensord.sensord": 13.0,
|
||||
"selfdrive.controls.radard": 2.0,
|
||||
"selfdrive.modeld.modeld": 22.0,
|
||||
@@ -206,7 +206,8 @@ class TestOnroad:
|
||||
result += "-------------- UI Draw Timing ------------------\n"
|
||||
result += "------------------------------------------------\n"
|
||||
|
||||
ts = self.ts['uiDebug']['drawTimeMillis']
|
||||
# skip first few frames -- connecting to vipc
|
||||
ts = self.ts['uiDebug']['drawTimeMillis'][15:]
|
||||
result += f"min {min(ts):.2f}ms\n"
|
||||
result += f"max {max(ts):.2f}ms\n"
|
||||
result += f"std {np.std(ts):.2f}ms\n"
|
||||
@@ -215,7 +216,7 @@ class TestOnroad:
|
||||
print(result)
|
||||
|
||||
assert max(ts) < 250.
|
||||
assert np.mean(ts) < 10.
|
||||
assert np.mean(ts) < 20. # TODO: ~6-11ms, increase consistency
|
||||
#self.assertLess(np.std(ts), 5.)
|
||||
|
||||
# some slow frames are expected since camerad/modeld can preempt ui
|
||||
@@ -285,7 +286,7 @@ class TestOnroad:
|
||||
|
||||
# check for big leaks. note that memory usage is
|
||||
# expected to go up while the MSGQ buffers fill up
|
||||
assert np.average(mems) <= 65, "Average memory usage above 65%"
|
||||
assert np.average(mems) <= 85, "Average memory usage above 85%"
|
||||
assert np.max(np.diff(mems)) <= 4, "Max memory increase too high"
|
||||
assert np.average(np.diff(mems)) <= 1, "Average memory increase too high"
|
||||
|
||||
|
||||
@@ -1,14 +1 @@
|
||||
moc_*
|
||||
*.moc
|
||||
|
||||
translations/main_test_en.*
|
||||
|
||||
ui
|
||||
mui
|
||||
watch3
|
||||
installer/installers/*
|
||||
qt/setup/setup
|
||||
qt/setup/reset
|
||||
qt/setup/wifi
|
||||
qt/setup/updater
|
||||
translations/alerts_generated.h
|
||||
|
||||
+27
-65
@@ -1,75 +1,37 @@
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc', 'transformations')
|
||||
from pathlib import Path
|
||||
Import('env', 'arch', 'common')
|
||||
|
||||
base_libs = [common, messaging, visionipc, transformations,
|
||||
'm', 'OpenCL', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"]
|
||||
# build the fonts
|
||||
generator = File("#selfdrive/assets/fonts/process.py")
|
||||
source_files = Glob("#selfdrive/assets/fonts/*.ttf") + Glob("#selfdrive/assets/fonts/*.otf")
|
||||
output_files = [
|
||||
(f.abspath.split('.')[0] + ".fnt", f.abspath.split('.')[0] + ".png")
|
||||
for f in source_files
|
||||
if "NotoColor" not in f.name
|
||||
]
|
||||
env.Command(
|
||||
target=output_files,
|
||||
source=[generator, source_files],
|
||||
action=f"python3 {generator}",
|
||||
)
|
||||
|
||||
if arch == 'larch64':
|
||||
base_libs.append('EGL')
|
||||
|
||||
if arch == "Darwin":
|
||||
del base_libs[base_libs.index('OpenCL')]
|
||||
qt_env['FRAMEWORKS'] += ['OpenCL']
|
||||
|
||||
sp_widgets_src = []
|
||||
sp_qt_src = []
|
||||
sp_qt_util = []
|
||||
if not GetOption('stock_ui'):
|
||||
SConscript(['sunnypilot/SConscript'])
|
||||
Import('sp_widgets_src', 'sp_qt_src', 'sp_qt_util')
|
||||
|
||||
# FIXME: remove this once we're on 5.15 (24.04)
|
||||
qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"]
|
||||
|
||||
qt_util = qt_env.Library("qt_util", ["#selfdrive/ui/qt/api.cc", "#selfdrive/ui/qt/util.cc"] + sp_qt_util, LIBS=base_libs)
|
||||
widgets_src = ["qt/widgets/input.cc", "qt/widgets/wifi.cc", "qt/prime_state.cc",
|
||||
"qt/widgets/ssh_keys.cc", "qt/widgets/toggle.cc", "qt/widgets/controls.cc",
|
||||
"qt/widgets/offroad_alerts.cc", "qt/widgets/prime.cc", "qt/widgets/keyboard.cc",
|
||||
"qt/widgets/scrollview.cc", "qt/widgets/cameraview.cc", "#third_party/qrcode/QrCode.cc",
|
||||
"qt/request_repeater.cc", "qt/qt_window.cc", "qt/network/networking.cc", "qt/network/wifi_manager.cc"] + sp_widgets_src
|
||||
|
||||
widgets = qt_env.Library("qt_widgets", widgets_src, LIBS=base_libs)
|
||||
Export('widgets')
|
||||
qt_libs = [widgets, qt_util] + base_libs
|
||||
|
||||
qt_src = ["main.cc", "ui.cc", "qt/sidebar.cc", "qt/body.cc",
|
||||
"qt/window.cc", "qt/home.cc", "qt/offroad/settings.cc", "qt/offroad/offroad_home.cc",
|
||||
"qt/offroad/software_settings.cc", "qt/offroad/developer_panel.cc", "qt/offroad/onboarding.cc",
|
||||
"qt/offroad/driverview.cc", "qt/offroad/experimental_mode.cc", "qt/offroad/firehose.cc",
|
||||
"qt/onroad/onroad_home.cc", "qt/onroad/annotated_camera.cc", "qt/onroad/model.cc",
|
||||
"qt/onroad/buttons.cc", "qt/onroad/alerts.cc", "qt/onroad/driver_monitoring.cc", "qt/onroad/hud.cc"] + sp_qt_src
|
||||
|
||||
# build translation files
|
||||
# compile gettext .po -> .mo translations
|
||||
with open(File("translations/languages.json").abspath) as f:
|
||||
languages = json.loads(f.read())
|
||||
translation_sources = [f"#selfdrive/ui/translations/{l}.ts" for l in languages.values()]
|
||||
translation_targets = [src.replace(".ts", ".qm") for src in translation_sources]
|
||||
lrelease_bin = 'third_party/qt5/larch64/bin/lrelease' if arch == 'larch64' else 'lrelease'
|
||||
|
||||
lrelease = qt_env.Command(translation_targets, translation_sources, f"{lrelease_bin} $SOURCES")
|
||||
qt_env.NoClean(translation_sources)
|
||||
qt_env.Precious(translation_sources)
|
||||
po_sources = [f"#selfdrive/ui/translations/app_{l}.po" for l in languages.values()]
|
||||
po_sources = [src for src in po_sources if os.path.exists(File(src).abspath)]
|
||||
mo_targets = [src.replace(".po", ".mo") for src in po_sources]
|
||||
mo_build = []
|
||||
for src, tgt in zip(po_sources, mo_targets):
|
||||
mo_build.append(env.Command(tgt, src, "msgfmt -o $TARGET $SOURCE"))
|
||||
mo_alias = env.Alias('mo', mo_build)
|
||||
env.AlwaysBuild(mo_alias)
|
||||
|
||||
# create qrc file for compiled translations to include with assets
|
||||
translations_assets_src = "#selfdrive/assets/translations_assets.qrc"
|
||||
with open(File(translations_assets_src).abspath, 'w') as f:
|
||||
f.write('<!DOCTYPE RCC><RCC version="1.0">\n<qresource>\n')
|
||||
f.write('\n'.join([f'<file alias="{l}">../ui/translations/{l}.qm</file>' for l in languages.values()]))
|
||||
f.write('\n</qresource>\n</RCC>')
|
||||
|
||||
# build assets
|
||||
assets = "#selfdrive/assets/assets.cc"
|
||||
assets_src = "#selfdrive/assets/assets.qrc"
|
||||
qt_env.Command(assets, [assets_src, translations_assets_src], f"rcc $SOURCES -o $TARGET")
|
||||
qt_env.Depends(assets, Glob('#selfdrive/assets/*', exclude=[assets, assets_src, translations_assets_src, "#selfdrive/assets/assets.o"]) + [lrelease])
|
||||
asset_obj = qt_env.Object("assets", assets)
|
||||
|
||||
# build main UI
|
||||
qt_env.Program("ui", qt_src + [asset_obj], LIBS=qt_libs)
|
||||
if GetOption('extras'):
|
||||
qt_src.remove("main.cc") # replaced by test_runner
|
||||
qt_env.Program('tests/test_translations', [asset_obj, 'tests/test_runner.cc', 'tests/test_translations.cc'] + qt_src, LIBS=qt_libs)
|
||||
|
||||
# build installers
|
||||
if arch != "Darwin":
|
||||
raylib_env = env.Clone()
|
||||
@@ -78,7 +40,7 @@ if GetOption('extras'):
|
||||
|
||||
raylib_libs = common + ["raylib"]
|
||||
if arch == "larch64":
|
||||
raylib_libs += ["GLESv2", "wayland-client", "wayland-egl", "EGL"]
|
||||
raylib_libs += ["GLESv2", "EGL", "gbm", "drm"]
|
||||
else:
|
||||
raylib_libs += ["GL"]
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
UI_BORDER_SIZE = 30
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "common/swaglog.h"
|
||||
#include "common/util.h"
|
||||
#include "system/hardware/hw.h"
|
||||
#include "third_party/raylib/include/raylib.h"
|
||||
|
||||
int freshClone();
|
||||
@@ -38,6 +39,27 @@ extern const uint8_t inter_ttf_end[] asm("_binary_selfdrive_ui_installer_inter_a
|
||||
|
||||
Font font;
|
||||
|
||||
std::vector<std::string> tici_prebuilt_branches = {"release3", "release-tizi", "release3-staging", "nightly", "nightly-dev"};
|
||||
std::string migrated_branch;
|
||||
|
||||
void branchMigration() {
|
||||
migrated_branch = BRANCH_STR;
|
||||
cereal::InitData::DeviceType device_type = Hardware::get_device_type();
|
||||
if (device_type == cereal::InitData::DeviceType::TICI) {
|
||||
if (std::find(tici_prebuilt_branches.begin(), tici_prebuilt_branches.end(), BRANCH_STR) != tici_prebuilt_branches.end()) {
|
||||
migrated_branch = "release-tici";
|
||||
} else if (BRANCH_STR == "master") {
|
||||
migrated_branch = "master-tici";
|
||||
}
|
||||
} else if (device_type == cereal::InitData::DeviceType::TIZI) {
|
||||
if (BRANCH_STR == "release3") {
|
||||
migrated_branch = "release-tizi";
|
||||
} else if (BRANCH_STR == "release3-staging") {
|
||||
migrated_branch = "release-tizi-staging";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void run(const char* cmd) {
|
||||
int err = std::system(cmd);
|
||||
assert(err == 0);
|
||||
@@ -87,7 +109,7 @@ int doInstall() {
|
||||
int freshClone() {
|
||||
LOGD("Doing fresh clone");
|
||||
std::string cmd = util::string_format("git clone --progress %s -b %s --depth=1 --recurse-submodules %s 2>&1",
|
||||
GIT_URL.c_str(), BRANCH_STR.c_str(), TMP_INSTALL_PATH);
|
||||
GIT_URL.c_str(), migrated_branch.c_str(), TMP_INSTALL_PATH);
|
||||
return executeGitCommand(cmd);
|
||||
}
|
||||
|
||||
@@ -95,11 +117,11 @@ int cachedFetch(const std::string &cache) {
|
||||
LOGD("Fetching with cache: %s", cache.c_str());
|
||||
|
||||
run(util::string_format("cp -rp %s %s", cache.c_str(), TMP_INSTALL_PATH).c_str());
|
||||
run(util::string_format("cd %s && git remote set-branches --add origin %s", TMP_INSTALL_PATH, BRANCH_STR.c_str()).c_str());
|
||||
run(util::string_format("cd %s && git remote set-branches --add origin %s", TMP_INSTALL_PATH, migrated_branch.c_str()).c_str());
|
||||
|
||||
renderProgress(10);
|
||||
|
||||
return executeGitCommand(util::string_format("cd %s && git fetch --progress origin %s 2>&1", TMP_INSTALL_PATH, BRANCH_STR.c_str()));
|
||||
return executeGitCommand(util::string_format("cd %s && git fetch --progress origin %s 2>&1", TMP_INSTALL_PATH, migrated_branch.c_str()));
|
||||
}
|
||||
|
||||
int executeGitCommand(const std::string &cmd) {
|
||||
@@ -142,8 +164,8 @@ void cloneFinished(int exitCode) {
|
||||
// ensure correct branch is checked out
|
||||
int err = chdir(TMP_INSTALL_PATH);
|
||||
assert(err == 0);
|
||||
run(("git checkout " + BRANCH_STR).c_str());
|
||||
run(("git reset --hard origin/" + BRANCH_STR).c_str());
|
||||
run(("git checkout " + migrated_branch).c_str());
|
||||
run(("git reset --hard origin/" + migrated_branch).c_str());
|
||||
run("git submodule update --init");
|
||||
|
||||
// move into place
|
||||
@@ -193,6 +215,8 @@ int main(int argc, char *argv[]) {
|
||||
font = LoadFontFromMemory(".ttf", inter_ttf, inter_ttf_end - inter_ttf, FONT_SIZE, NULL, 0);
|
||||
SetTextureFilter(font.texture, TEXTURE_FILTER_BILINEAR);
|
||||
|
||||
branchMigration();
|
||||
|
||||
if (util::file_exists(CONTINUE_PATH)) {
|
||||
finishInstall();
|
||||
} else {
|
||||
|
||||
@@ -8,7 +8,9 @@ from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButto
|
||||
from openpilot.selfdrive.ui.widgets.prime import PrimeWidget
|
||||
from openpilot.selfdrive.ui.widgets.setup import SetupWidget
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr, trn
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
HEADER_HEIGHT = 80
|
||||
@@ -35,12 +37,17 @@ class HomeLayout(Widget):
|
||||
self.update_alert = UpdateAlert()
|
||||
self.offroad_alert = OffroadAlert()
|
||||
|
||||
self._layout_widgets = {HomeLayoutState.UPDATE: self.update_alert, HomeLayoutState.ALERTS: self.offroad_alert}
|
||||
|
||||
self.current_state = HomeLayoutState.HOME
|
||||
self.last_refresh = 0
|
||||
self.settings_callback: callable | None = None
|
||||
|
||||
self.update_available = False
|
||||
self.alert_count = 0
|
||||
self._version_text = ""
|
||||
self._prev_update_available = False
|
||||
self._prev_alerts_present = False
|
||||
|
||||
self.header_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self.content_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
@@ -56,14 +63,30 @@ class HomeLayout(Widget):
|
||||
self._exp_mode_button = ExperimentalModeButton()
|
||||
self._setup_callbacks()
|
||||
|
||||
def show_event(self):
|
||||
self._exp_mode_button.show_event()
|
||||
self.last_refresh = time.monotonic()
|
||||
self._refresh()
|
||||
|
||||
def _setup_callbacks(self):
|
||||
self.update_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME))
|
||||
self.offroad_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME))
|
||||
self._exp_mode_button.set_click_callback(lambda: self.settings_callback() if self.settings_callback else None)
|
||||
|
||||
def set_settings_callback(self, callback: Callable):
|
||||
self.settings_callback = callback
|
||||
|
||||
def _set_state(self, state: HomeLayoutState):
|
||||
# propagate show/hide events
|
||||
if state != self.current_state:
|
||||
if state == HomeLayoutState.HOME:
|
||||
self._exp_mode_button.show_event()
|
||||
|
||||
if state in self._layout_widgets:
|
||||
self._layout_widgets[state].show_event()
|
||||
if self.current_state in self._layout_widgets:
|
||||
self._layout_widgets[self.current_state].hide_event()
|
||||
|
||||
self.current_state = state
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
@@ -72,7 +95,6 @@ class HomeLayout(Widget):
|
||||
self._refresh()
|
||||
self.last_refresh = current_time
|
||||
|
||||
self._handle_input()
|
||||
self._render_header()
|
||||
|
||||
# Render content based on current state
|
||||
@@ -83,7 +105,7 @@ class HomeLayout(Widget):
|
||||
elif self.current_state == HomeLayoutState.ALERTS:
|
||||
self._render_alerts_view()
|
||||
|
||||
def _update_layout_rects(self):
|
||||
def _update_state(self):
|
||||
self.header_rect = rl.Rectangle(
|
||||
self._rect.x + CONTENT_MARGIN, self._rect.y + CONTENT_MARGIN, self._rect.width - 2 * CONTENT_MARGIN, HEADER_HEIGHT
|
||||
)
|
||||
@@ -110,59 +132,54 @@ class HomeLayout(Widget):
|
||||
self.alert_notif_rect.x = notif_x
|
||||
self.alert_notif_rect.y = self.header_rect.y + (self.header_rect.height - 60) // 2
|
||||
|
||||
def _handle_input(self):
|
||||
if not rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||
return
|
||||
|
||||
mouse_pos = rl.get_mouse_position()
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
if self.update_available and rl.check_collision_point_rec(mouse_pos, self.update_notif_rect):
|
||||
self._set_state(HomeLayoutState.UPDATE)
|
||||
return
|
||||
|
||||
if self.alert_count > 0 and rl.check_collision_point_rec(mouse_pos, self.alert_notif_rect):
|
||||
elif self.alert_count > 0 and rl.check_collision_point_rec(mouse_pos, self.alert_notif_rect):
|
||||
self._set_state(HomeLayoutState.ALERTS)
|
||||
return
|
||||
|
||||
# Content area input handling
|
||||
if self.current_state == HomeLayoutState.UPDATE:
|
||||
self.update_alert.handle_input(mouse_pos, True)
|
||||
elif self.current_state == HomeLayoutState.ALERTS:
|
||||
self.offroad_alert.handle_input(mouse_pos, True)
|
||||
|
||||
def _render_header(self):
|
||||
font = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
version_text_width = self.header_rect.width
|
||||
|
||||
# Update notification button
|
||||
if self.update_available:
|
||||
version_text_width -= self.update_notif_rect.width
|
||||
|
||||
# Highlight if currently viewing updates
|
||||
highlight_color = rl.Color(255, 140, 40, 255) if self.current_state == HomeLayoutState.UPDATE else rl.Color(255, 102, 0, 255)
|
||||
highlight_color = rl.Color(75, 95, 255, 255) if self.current_state == HomeLayoutState.UPDATE else rl.Color(54, 77, 239, 255)
|
||||
rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color)
|
||||
|
||||
text = "UPDATE"
|
||||
text_width = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE).x
|
||||
text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_width) // 2
|
||||
text_y = self.update_notif_rect.y + (self.update_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2
|
||||
text = tr("UPDATE")
|
||||
text_size = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE)
|
||||
text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_size.x) // 2
|
||||
text_y = self.update_notif_rect.y + (self.update_notif_rect.height - text_size.y) // 2
|
||||
rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
|
||||
|
||||
# Alert notification button
|
||||
if self.alert_count > 0:
|
||||
version_text_width -= self.alert_notif_rect.width
|
||||
|
||||
# Highlight if currently viewing alerts
|
||||
highlight_color = rl.Color(255, 70, 70, 255) if self.current_state == HomeLayoutState.ALERTS else rl.Color(226, 44, 44, 255)
|
||||
rl.draw_rectangle_rounded(self.alert_notif_rect, 0.3, 10, highlight_color)
|
||||
|
||||
alert_text = f"{self.alert_count} ALERT{'S' if self.alert_count > 1 else ''}"
|
||||
text_width = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE).x
|
||||
text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_width) // 2
|
||||
text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2
|
||||
alert_text = trn("{} ALERT", "{} ALERTS", self.alert_count).format(self.alert_count)
|
||||
text_size = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE)
|
||||
text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_size.x) // 2
|
||||
text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - text_size.y) // 2
|
||||
rl.draw_text_ex(font, alert_text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
|
||||
|
||||
# Version text (right aligned)
|
||||
version_text = self._get_version_text()
|
||||
text_width = measure_text_cached(gui_app.font(FontWeight.NORMAL), version_text, 48).x
|
||||
version_x = self.header_rect.x + self.header_rect.width - text_width
|
||||
version_y = self.header_rect.y + (self.header_rect.height - 48) // 2
|
||||
rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), version_text, rl.Vector2(int(version_x), int(version_y)), 48, 0, DEFAULT_TEXT_COLOR)
|
||||
if self.update_available or self.alert_count > 0:
|
||||
version_text_width -= SPACING * 1.5
|
||||
|
||||
version_rect = rl.Rectangle(self.header_rect.x + self.header_rect.width - version_text_width, self.header_rect.y,
|
||||
version_text_width, self.header_rect.height)
|
||||
gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||
|
||||
def _render_home_content(self):
|
||||
self._render_left_column()
|
||||
@@ -193,20 +210,23 @@ class HomeLayout(Widget):
|
||||
self._setup_widget.render(setup_rect)
|
||||
|
||||
def _refresh(self):
|
||||
# TODO: implement _update_state with a timer
|
||||
self.update_available = self.update_alert.refresh()
|
||||
self.alert_count = self.offroad_alert.refresh()
|
||||
self._update_state_priority(self.update_available, self.alert_count > 0)
|
||||
|
||||
def _update_state_priority(self, update_available: bool, alerts_present: bool):
|
||||
current_state = self.current_state
|
||||
self._version_text = self._get_version_text()
|
||||
update_available = self.update_alert.refresh()
|
||||
alert_count = self.offroad_alert.refresh()
|
||||
alerts_present = alert_count > 0
|
||||
|
||||
# Show panels on transition from no alert/update to any alerts/update
|
||||
if not update_available and not alerts_present:
|
||||
self.current_state = HomeLayoutState.HOME
|
||||
elif update_available and (current_state == HomeLayoutState.HOME or (not alerts_present and current_state == HomeLayoutState.ALERTS)):
|
||||
self.current_state = HomeLayoutState.UPDATE
|
||||
elif alerts_present and (current_state == HomeLayoutState.HOME or (not update_available and current_state == HomeLayoutState.UPDATE)):
|
||||
self.current_state = HomeLayoutState.ALERTS
|
||||
self._set_state(HomeLayoutState.HOME)
|
||||
elif update_available and ((not self._prev_update_available) or (not alerts_present and self.current_state == HomeLayoutState.ALERTS)):
|
||||
self._set_state(HomeLayoutState.UPDATE)
|
||||
elif alerts_present and ((not self._prev_alerts_present) or (not update_available and self.current_state == HomeLayoutState.UPDATE)):
|
||||
self._set_state(HomeLayoutState.ALERTS)
|
||||
|
||||
self.update_available = update_available
|
||||
self.alert_count = alert_count
|
||||
self._prev_update_available = update_available
|
||||
self._prev_alerts_present = alerts_present
|
||||
|
||||
def _get_version_text(self) -> str:
|
||||
brand = "openpilot"
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import pyray as rl
|
||||
from enum import IntEnum
|
||||
import cereal.messaging as messaging
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH
|
||||
from openpilot.selfdrive.ui.layouts.home import HomeLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout, PanelType
|
||||
from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView
|
||||
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow
|
||||
|
||||
|
||||
class MainState(IntEnum):
|
||||
@@ -34,16 +36,23 @@ class MainLayout(Widget):
|
||||
# Set callbacks
|
||||
self._setup_callbacks()
|
||||
|
||||
# Start onboarding if terms or training not completed
|
||||
self._onboarding_window = OnboardingWindow()
|
||||
if not self._onboarding_window.completed:
|
||||
gui_app.set_modal_overlay(self._onboarding_window)
|
||||
|
||||
def _render(self, _):
|
||||
self._handle_onroad_transition()
|
||||
self._render_main_content()
|
||||
|
||||
def _setup_callbacks(self):
|
||||
self._sidebar.set_callbacks(on_settings=self._on_settings_clicked,
|
||||
on_flag=self._on_bookmark_clicked)
|
||||
on_flag=self._on_bookmark_clicked,
|
||||
open_settings=lambda: self.open_settings(PanelType.TOGGLES))
|
||||
self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(lambda: self.open_settings(PanelType.FIREHOSE))
|
||||
self._layouts[MainState.HOME].set_settings_callback(lambda: self.open_settings(PanelType.TOGGLES))
|
||||
self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state)
|
||||
self._layouts[MainState.ONROAD].set_callbacks(on_click=self._on_onroad_clicked)
|
||||
self._layouts[MainState.ONROAD].set_click_callback(self._on_onroad_clicked)
|
||||
device.add_interactive_timeout_callback(self._set_mode_for_state)
|
||||
|
||||
def _update_layout_rects(self):
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
from enum import IntEnum
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||
from openpilot.system.ui.widgets.label import Label
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
DEBUG = False
|
||||
|
||||
STEP_RECTS = [rl.Rectangle(104, 800, 633, 175), rl.Rectangle(1835, 0, 2159, 1080), rl.Rectangle(1835, 0, 2156, 1080),
|
||||
rl.Rectangle(1526, 473, 427, 472), rl.Rectangle(1643, 441, 217, 223), rl.Rectangle(1835, 0, 2155, 1080),
|
||||
rl.Rectangle(1786, 591, 267, 236), rl.Rectangle(1353, 0, 804, 1080), rl.Rectangle(1458, 485, 633, 211),
|
||||
rl.Rectangle(95, 794, 1158, 187), rl.Rectangle(1560, 170, 392, 397), rl.Rectangle(1835, 0, 2159, 1080),
|
||||
rl.Rectangle(1351, 0, 807, 1080), rl.Rectangle(1835, 0, 2158, 1080), rl.Rectangle(1531, 82, 441, 920),
|
||||
rl.Rectangle(1336, 438, 490, 393), rl.Rectangle(1835, 0, 2159, 1080), rl.Rectangle(1835, 0, 2159, 1080),
|
||||
rl.Rectangle(87, 795, 1187, 186)]
|
||||
|
||||
DM_RECORD_STEP = 9
|
||||
DM_RECORD_YES_RECT = rl.Rectangle(695, 794, 558, 187)
|
||||
|
||||
RESTART_TRAINING_RECT = rl.Rectangle(87, 795, 472, 186)
|
||||
|
||||
|
||||
class OnboardingState(IntEnum):
|
||||
TERMS = 0
|
||||
ONBOARDING = 1
|
||||
DECLINE = 2
|
||||
|
||||
|
||||
class TrainingGuide(Widget):
|
||||
def __init__(self, completed_callback=None):
|
||||
super().__init__()
|
||||
self._completed_callback = completed_callback
|
||||
|
||||
self._step = 0
|
||||
self._load_image_paths()
|
||||
|
||||
# Load first image now so we show something immediately
|
||||
self._textures = [gui_app.texture(self._image_paths[0])]
|
||||
self._image_objs = []
|
||||
|
||||
threading.Thread(target=self._preload_thread, daemon=True).start()
|
||||
|
||||
def _load_image_paths(self):
|
||||
paths = [fn for fn in os.listdir(os.path.join(BASEDIR, "selfdrive/assets/training")) if re.match(r'^step\d*\.png$', fn)]
|
||||
paths = sorted(paths, key=lambda x: int(re.search(r'\d+', x).group()))
|
||||
self._image_paths = [os.path.join(BASEDIR, "selfdrive/assets/training", fn) for fn in paths]
|
||||
|
||||
def _preload_thread(self):
|
||||
# PNG loading is slow in raylib, so we preload in a thread and upload to GPU in main thread
|
||||
# We've already loaded the first image on init
|
||||
for path in self._image_paths[1:]:
|
||||
self._image_objs.append(gui_app._load_image_from_path(path))
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos):
|
||||
if rl.check_collision_point_rec(mouse_pos, STEP_RECTS[self._step]):
|
||||
# Record DM camera?
|
||||
if self._step == DM_RECORD_STEP:
|
||||
yes = rl.check_collision_point_rec(mouse_pos, DM_RECORD_YES_RECT)
|
||||
print(f"putting RecordFront to {yes}")
|
||||
ui_state.params.put_bool("RecordFront", yes)
|
||||
|
||||
# Restart training?
|
||||
elif self._step == len(self._image_paths) - 1:
|
||||
if rl.check_collision_point_rec(mouse_pos, RESTART_TRAINING_RECT):
|
||||
self._step = -1
|
||||
|
||||
self._step += 1
|
||||
|
||||
# Finished?
|
||||
if self._step >= len(self._image_paths):
|
||||
self._step = 0
|
||||
if self._completed_callback:
|
||||
self._completed_callback()
|
||||
|
||||
def _update_state(self):
|
||||
if len(self._image_objs):
|
||||
self._textures.append(gui_app._load_texture_from_image(self._image_objs.pop(0)))
|
||||
|
||||
def _render(self, _):
|
||||
# Safeguard against fast tapping
|
||||
step = min(self._step, len(self._textures) - 1)
|
||||
rl.draw_texture(self._textures[step], 0, 0, rl.WHITE)
|
||||
|
||||
# progress bar
|
||||
if 0 < step < len(STEP_RECTS) - 1:
|
||||
h = 20
|
||||
w = int((step / (len(STEP_RECTS) - 1)) * self._rect.width)
|
||||
rl.draw_rectangle(int(self._rect.x), int(self._rect.y + self._rect.height - h),
|
||||
w, h, rl.Color(70, 91, 234, 255))
|
||||
|
||||
if DEBUG:
|
||||
rl.draw_rectangle_lines_ex(STEP_RECTS[step], 3, rl.RED)
|
||||
|
||||
return -1
|
||||
|
||||
|
||||
class TermsPage(Widget):
|
||||
def __init__(self, on_accept=None, on_decline=None):
|
||||
super().__init__()
|
||||
self._on_accept = on_accept
|
||||
self._on_decline = on_decline
|
||||
|
||||
self._title = Label(tr("Welcome to openpilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
self._desc = Label(tr("You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing."),
|
||||
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
|
||||
self._decline_btn = Button(tr("Decline"), click_callback=on_decline)
|
||||
self._accept_btn = Button(tr("Agree"), button_style=ButtonStyle.PRIMARY, click_callback=on_accept)
|
||||
|
||||
def _render(self, _):
|
||||
welcome_x = self._rect.x + 165
|
||||
welcome_y = self._rect.y + 165
|
||||
welcome_rect = rl.Rectangle(welcome_x, welcome_y, self._rect.width - welcome_x, 90)
|
||||
self._title.render(welcome_rect)
|
||||
|
||||
desc_x = welcome_x
|
||||
# TODO: Label doesn't top align when wrapping
|
||||
desc_y = welcome_y - 100
|
||||
desc_rect = rl.Rectangle(desc_x, desc_y, self._rect.width - desc_x, self._rect.height - desc_y - 250)
|
||||
self._desc.render(desc_rect)
|
||||
|
||||
btn_y = self._rect.y + self._rect.height - 160 - 45
|
||||
btn_width = (self._rect.width - 45 * 3) / 2
|
||||
self._decline_btn.render(rl.Rectangle(self._rect.x + 45, btn_y, btn_width, 160))
|
||||
self._accept_btn.render(rl.Rectangle(self._rect.x + 45 * 2 + btn_width, btn_y, btn_width, 160))
|
||||
|
||||
if DEBUG:
|
||||
rl.draw_rectangle_lines_ex(welcome_rect, 3, rl.RED)
|
||||
rl.draw_rectangle_lines_ex(desc_rect, 3, rl.RED)
|
||||
|
||||
return -1
|
||||
|
||||
|
||||
class DeclinePage(Widget):
|
||||
def __init__(self, back_callback=None):
|
||||
super().__init__()
|
||||
self._text = Label(tr("You must accept the Terms and Conditions in order to use openpilot."),
|
||||
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
self._back_btn = Button(tr("Back"), click_callback=back_callback)
|
||||
self._uninstall_btn = Button(tr("Decline, uninstall openpilot"), button_style=ButtonStyle.DANGER,
|
||||
click_callback=self._on_uninstall_clicked)
|
||||
|
||||
def _on_uninstall_clicked(self):
|
||||
ui_state.params.put_bool("DoUninstall", True)
|
||||
gui_app.request_close()
|
||||
|
||||
def _render(self, _):
|
||||
btn_y = self._rect.y + self._rect.height - 160 - 45
|
||||
btn_width = (self._rect.width - 45 * 3) / 2
|
||||
self._back_btn.render(rl.Rectangle(self._rect.x + 45, btn_y, btn_width, 160))
|
||||
self._uninstall_btn.render(rl.Rectangle(self._rect.x + 45 * 2 + btn_width, btn_y, btn_width, 160))
|
||||
|
||||
# text rect in middle of top and button
|
||||
text_height = btn_y - (200 + 45)
|
||||
text_rect = rl.Rectangle(self._rect.x + 165, self._rect.y + (btn_y - text_height) / 2 + 10, self._rect.width - (165 * 2), text_height)
|
||||
if DEBUG:
|
||||
rl.draw_rectangle_lines_ex(text_rect, 3, rl.RED)
|
||||
self._text.render(text_rect)
|
||||
|
||||
|
||||
class OnboardingWindow(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._current_terms_version = ui_state.params.get("TermsVersion")
|
||||
self._current_training_version = ui_state.params.get("TrainingVersion")
|
||||
self._accepted_terms: bool = ui_state.params.get("HasAcceptedTerms") == self._current_terms_version
|
||||
self._training_done: bool = ui_state.params.get("CompletedTrainingVersion") == self._current_training_version
|
||||
|
||||
self._state = OnboardingState.TERMS if not self._accepted_terms else OnboardingState.ONBOARDING
|
||||
|
||||
# Windows
|
||||
self._terms = TermsPage(on_accept=self._on_terms_accepted, on_decline=self._on_terms_declined)
|
||||
self._training_guide: TrainingGuide | None = None
|
||||
self._decline_page = DeclinePage(back_callback=self._on_decline_back)
|
||||
|
||||
@property
|
||||
def completed(self) -> bool:
|
||||
return self._accepted_terms and self._training_done
|
||||
|
||||
def _on_terms_declined(self):
|
||||
self._state = OnboardingState.DECLINE
|
||||
|
||||
def _on_decline_back(self):
|
||||
self._state = OnboardingState.TERMS
|
||||
|
||||
def _on_terms_accepted(self):
|
||||
ui_state.params.put("HasAcceptedTerms", self._current_terms_version)
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
if self._training_done:
|
||||
gui_app.set_modal_overlay(None)
|
||||
|
||||
def _on_completed_training(self):
|
||||
ui_state.params.put("CompletedTrainingVersion", self._current_training_version)
|
||||
gui_app.set_modal_overlay(None)
|
||||
|
||||
def _render(self, _):
|
||||
if self._training_guide is None:
|
||||
self._training_guide = TrainingGuide(completed_callback=self._on_completed_training)
|
||||
|
||||
if self._state == OnboardingState.TERMS:
|
||||
self._terms.render(self._rect)
|
||||
if self._state == OnboardingState.ONBOARDING:
|
||||
self._training_guide.render(self._rect)
|
||||
elif self._state == OnboardingState.DECLINE:
|
||||
self._decline_page.render(self._rect)
|
||||
return -1
|
||||
@@ -1,20 +1,30 @@
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.list_view import toggle_item
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.widgets import DialogResult
|
||||
|
||||
# Description constants
|
||||
DESCRIPTIONS = {
|
||||
'enable_adb': (
|
||||
'enable_adb': tr_noop(
|
||||
"ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. " +
|
||||
"See https://docs.comma.ai/how-to/connect-to-comma for more info."
|
||||
),
|
||||
'joystick_debug_mode': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)",
|
||||
'ssh_key': (
|
||||
'ssh_key': tr_noop(
|
||||
"Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username " +
|
||||
"other than your own. A comma employee will NEVER ask you to add their GitHub username."
|
||||
),
|
||||
'alpha_longitudinal': tr_noop(
|
||||
"<b>WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB).</b><br><br>" +
|
||||
"On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. " +
|
||||
"Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. " +
|
||||
"Changing this setting will restart openpilot if the car is powered on."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -22,40 +32,154 @@ class DeveloperLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
items = [
|
||||
toggle_item(
|
||||
"Enable ADB",
|
||||
description=DESCRIPTIONS["enable_adb"],
|
||||
initial_state=self._params.get_bool("AdbEnabled"),
|
||||
callback=self._on_enable_adb,
|
||||
),
|
||||
ssh_key_item("SSH Key", description=DESCRIPTIONS["ssh_key"]),
|
||||
toggle_item(
|
||||
"Joystick Debug Mode",
|
||||
description=DESCRIPTIONS["joystick_debug_mode"],
|
||||
initial_state=self._params.get_bool("JoystickDebugMode"),
|
||||
callback=self._on_joystick_debug_mode,
|
||||
),
|
||||
toggle_item(
|
||||
"Longitudinal Maneuver Mode",
|
||||
description="",
|
||||
initial_state=self._params.get_bool("LongitudinalManeuverMode"),
|
||||
callback=self._on_long_maneuver_mode,
|
||||
),
|
||||
toggle_item(
|
||||
"openpilot Longitudinal Control (Alpha)",
|
||||
description="",
|
||||
initial_state=self._params.get_bool("AlphaLongitudinalEnabled"),
|
||||
callback=self._on_alpha_long_enabled,
|
||||
),
|
||||
]
|
||||
self._is_release = self._params.get_bool("IsReleaseBranch")
|
||||
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
# Build items and keep references for callbacks/state updates
|
||||
self._adb_toggle = toggle_item(
|
||||
lambda: tr("Enable ADB"),
|
||||
description=lambda: tr(DESCRIPTIONS["enable_adb"]),
|
||||
initial_state=self._params.get_bool("AdbEnabled"),
|
||||
callback=self._on_enable_adb,
|
||||
enabled=ui_state.is_offroad,
|
||||
)
|
||||
|
||||
# SSH enable toggle + SSH key management
|
||||
self._ssh_toggle = toggle_item(
|
||||
lambda: tr("Enable SSH"),
|
||||
description="",
|
||||
initial_state=self._params.get_bool("SshEnabled"),
|
||||
callback=self._on_enable_ssh,
|
||||
)
|
||||
self._ssh_keys = ssh_key_item(lambda: tr("SSH Keys"), description=lambda: tr(DESCRIPTIONS["ssh_key"]))
|
||||
|
||||
self._joystick_toggle = toggle_item(
|
||||
lambda: tr("Joystick Debug Mode"),
|
||||
description="",
|
||||
initial_state=self._params.get_bool("JoystickDebugMode"),
|
||||
callback=self._on_joystick_debug_mode,
|
||||
enabled=ui_state.is_offroad,
|
||||
)
|
||||
|
||||
self._long_maneuver_toggle = toggle_item(
|
||||
lambda: tr("Longitudinal Maneuver Mode"),
|
||||
description="",
|
||||
initial_state=self._params.get_bool("LongitudinalManeuverMode"),
|
||||
callback=self._on_long_maneuver_mode,
|
||||
)
|
||||
|
||||
self._alpha_long_toggle = toggle_item(
|
||||
lambda: tr("openpilot Longitudinal Control (Alpha)"),
|
||||
description=lambda: tr(DESCRIPTIONS["alpha_longitudinal"]),
|
||||
initial_state=self._params.get_bool("AlphaLongitudinalEnabled"),
|
||||
callback=self._on_alpha_long_enabled,
|
||||
enabled=lambda: not ui_state.engaged,
|
||||
)
|
||||
|
||||
self._ui_debug_toggle = toggle_item(
|
||||
lambda: tr("UI Debug Mode"),
|
||||
description="",
|
||||
initial_state=self._params.get_bool("ShowDebugInfo"),
|
||||
callback=self._on_enable_ui_debug,
|
||||
)
|
||||
self._on_enable_ui_debug(self._params.get_bool("ShowDebugInfo"))
|
||||
|
||||
self._scroller = Scroller([
|
||||
self._adb_toggle,
|
||||
self._ssh_toggle,
|
||||
self._ssh_keys,
|
||||
self._joystick_toggle,
|
||||
self._long_maneuver_toggle,
|
||||
self._alpha_long_toggle,
|
||||
self._ui_debug_toggle,
|
||||
], line_separator=True, spacing=0)
|
||||
|
||||
# Toggles should be not available to change in onroad state
|
||||
ui_state.add_offroad_transition_callback(self._update_toggles)
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def _on_enable_adb(self): pass
|
||||
def _on_joystick_debug_mode(self): pass
|
||||
def _on_long_maneuver_mode(self): pass
|
||||
def _on_alpha_long_enabled(self): pass
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
self._update_toggles()
|
||||
|
||||
def _update_toggles(self):
|
||||
ui_state.update_params()
|
||||
|
||||
# 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):
|
||||
item.set_visible(not self._is_release)
|
||||
|
||||
# CP gating
|
||||
if ui_state.CP is not None:
|
||||
alpha_avail = ui_state.CP.alphaLongitudinalAvailable
|
||||
if not alpha_avail or self._is_release:
|
||||
self._alpha_long_toggle.set_visible(False)
|
||||
self._params.remove("AlphaLongitudinalEnabled")
|
||||
else:
|
||||
self._alpha_long_toggle.set_visible(True)
|
||||
|
||||
long_man_enabled = ui_state.has_longitudinal_control and ui_state.is_offroad()
|
||||
self._long_maneuver_toggle.action_item.set_enabled(long_man_enabled)
|
||||
if not long_man_enabled:
|
||||
self._long_maneuver_toggle.action_item.set_state(False)
|
||||
self._params.put_bool("LongitudinalManeuverMode", False)
|
||||
else:
|
||||
self._long_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
|
||||
# refresh toggles from params to mirror external changes
|
||||
for key, item in (
|
||||
("AdbEnabled", self._adb_toggle),
|
||||
("SshEnabled", self._ssh_toggle),
|
||||
("JoystickDebugMode", self._joystick_toggle),
|
||||
("LongitudinalManeuverMode", self._long_maneuver_toggle),
|
||||
("AlphaLongitudinalEnabled", self._alpha_long_toggle),
|
||||
("ShowDebugInfo", self._ui_debug_toggle),
|
||||
):
|
||||
item.action_item.set_state(self._params.get_bool(key))
|
||||
|
||||
def _on_enable_ui_debug(self, state: bool):
|
||||
self._params.put_bool("ShowDebugInfo", state)
|
||||
gui_app.set_show_touches(state)
|
||||
gui_app.set_show_fps(state)
|
||||
|
||||
def _on_enable_adb(self, state: bool):
|
||||
self._params.put_bool("AdbEnabled", state)
|
||||
|
||||
def _on_enable_ssh(self, state: bool):
|
||||
self._params.put_bool("SshEnabled", state)
|
||||
|
||||
def _on_joystick_debug_mode(self, state: bool):
|
||||
self._params.put_bool("JoystickDebugMode", state)
|
||||
self._params.put_bool("LongitudinalManeuverMode", False)
|
||||
self._long_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)
|
||||
|
||||
def _on_alpha_long_enabled(self, state: bool):
|
||||
if state:
|
||||
def confirm_callback(result: int):
|
||||
if result == DialogResult.CONFIRM:
|
||||
self._params.put_bool("AlphaLongitudinalEnabled", True)
|
||||
self._params.put_bool("OnroadCycleRequested", True)
|
||||
self._update_toggles()
|
||||
else:
|
||||
self._alpha_long_toggle.action_item.set_state(False)
|
||||
|
||||
# show confirmation dialog
|
||||
content = (f"<h1>{self._alpha_long_toggle.title}</h1><br>" +
|
||||
f"<p>{self._alpha_long_toggle.description}</p>")
|
||||
|
||||
dlg = ConfirmDialog(content, tr("Enable"), rich=True)
|
||||
gui_app.set_modal_overlay(dlg, callback=confirm_callback)
|
||||
|
||||
else:
|
||||
self._params.put_bool("AlphaLongitudinalEnabled", False)
|
||||
self._params.put_bool("OnroadCycleRequested", True)
|
||||
self._update_toggles()
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
import os
|
||||
import json
|
||||
import math
|
||||
|
||||
from cereal import messaging, log
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.layouts.onboarding import TrainingGuide
|
||||
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
|
||||
from openpilot.system.hardware import TICI
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.system.ui.lib.multilang import multilang, tr, tr_noop
|
||||
from openpilot.system.ui.widgets import Widget, DialogResult
|
||||
from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog, alert_dialog
|
||||
from openpilot.system.ui.widgets.html_render import HtmlRenderer
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog
|
||||
from openpilot.system.ui.widgets.html_render import HtmlModal
|
||||
from openpilot.system.ui.widgets.list_view import text_item, button_item, dual_button_item
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
|
||||
# Description constants
|
||||
DESCRIPTIONS = {
|
||||
'pair_device': "Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.",
|
||||
'driver_camera': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)",
|
||||
'reset_calibration': (
|
||||
"openpilot requires the device to be mounted within 4° left or right and within 5° " +
|
||||
"up or 9° down. openpilot is continuously calibrating, resetting is rarely required."
|
||||
),
|
||||
'review_guide': "Review the rules, features, and limitations of openpilot",
|
||||
'pair_device': tr_noop("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer."),
|
||||
'driver_camera': tr_noop("Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)"),
|
||||
'reset_calibration': tr_noop("openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down."),
|
||||
'review_guide': tr_noop("Review the rules, features, and limitations of openpilot"),
|
||||
}
|
||||
|
||||
|
||||
@@ -35,49 +36,61 @@ class DeviceLayout(Widget):
|
||||
self._select_language_dialog: MultiOptionDialog | None = None
|
||||
self._driver_camera: DriverCameraDialog | None = None
|
||||
self._pair_device_dialog: PairingDialog | None = None
|
||||
self._fcc_dialog: HtmlRenderer | None = None
|
||||
self._fcc_dialog: HtmlModal | None = None
|
||||
self._training_guide: TrainingGuide | None = None
|
||||
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
ui_state.add_offroad_transition_callback(self._offroad_transition)
|
||||
|
||||
def _initialize_items(self):
|
||||
dongle_id = self._params.get("DongleId") or "N/A"
|
||||
serial = self._params.get("HardwareSerial") or "N/A"
|
||||
self._pair_device_btn = button_item(lambda: tr("Pair Device"), lambda: tr("PAIR"), lambda: tr(DESCRIPTIONS['pair_device']), callback=self._pair_device)
|
||||
self._pair_device_btn.set_visible(lambda: not ui_state.prime_state.is_paired())
|
||||
|
||||
self._reset_calib_btn = button_item(lambda: tr("Reset Calibration"), lambda: tr("RESET"), lambda: tr(DESCRIPTIONS['reset_calibration']),
|
||||
callback=self._reset_calibration_prompt)
|
||||
self._reset_calib_btn.set_description_opened_callback(self._update_calib_description)
|
||||
|
||||
self._power_off_btn = dual_button_item(lambda: tr("Reboot"), lambda: tr("Power Off"),
|
||||
left_callback=self._reboot_prompt, right_callback=self._power_off_prompt)
|
||||
|
||||
items = [
|
||||
text_item("Dongle ID", dongle_id),
|
||||
text_item("Serial", serial),
|
||||
button_item("Pair Device", "PAIR", DESCRIPTIONS['pair_device'], callback=self._pair_device),
|
||||
button_item("Driver Camera", "PREVIEW", DESCRIPTIONS['driver_camera'], callback=self._show_driver_camera, enabled=ui_state.is_offroad),
|
||||
button_item("Reset Calibration", "RESET", DESCRIPTIONS['reset_calibration'], callback=self._reset_calibration_prompt),
|
||||
regulatory_btn := button_item("Regulatory", "VIEW", callback=self._on_regulatory),
|
||||
button_item("Review Training Guide", "REVIEW", DESCRIPTIONS['review_guide'], self._on_review_training_guide),
|
||||
button_item("Change Language", "CHANGE", callback=self._show_language_selection, enabled=ui_state.is_offroad),
|
||||
dual_button_item("Reboot", "Power Off", left_callback=self._reboot_prompt, right_callback=self._power_off_prompt),
|
||||
text_item(lambda: tr("Dongle ID"), self._params.get("DongleId") or (lambda: tr("N/A"))),
|
||||
text_item(lambda: tr("Serial"), self._params.get("HardwareSerial") or (lambda: tr("N/A"))),
|
||||
self._pair_device_btn,
|
||||
button_item(lambda: tr("Driver Camera"), lambda: tr("PREVIEW"), lambda: tr(DESCRIPTIONS['driver_camera']),
|
||||
callback=self._show_driver_camera, enabled=ui_state.is_offroad),
|
||||
self._reset_calib_btn,
|
||||
button_item(lambda: tr("Review Training Guide"), lambda: tr("REVIEW"), lambda: tr(DESCRIPTIONS['review_guide']),
|
||||
self._on_review_training_guide, enabled=ui_state.is_offroad),
|
||||
regulatory_btn := button_item(lambda: tr("Regulatory"), lambda: tr("VIEW"), callback=self._on_regulatory, enabled=ui_state.is_offroad),
|
||||
button_item(lambda: tr("Change Language"), lambda: tr("CHANGE"), callback=self._show_language_dialog),
|
||||
self._power_off_btn,
|
||||
]
|
||||
regulatory_btn.set_visible(TICI)
|
||||
return items
|
||||
|
||||
def _offroad_transition(self):
|
||||
self._power_off_btn.action_item.right_button.set_visible(ui_state.is_offroad())
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def _show_language_selection(self):
|
||||
try:
|
||||
languages_file = os.path.join(BASEDIR, "selfdrive/ui/translations/languages.json")
|
||||
with open(languages_file, encoding='utf-8') as f:
|
||||
languages = json.load(f)
|
||||
def _show_language_dialog(self):
|
||||
def handle_language_selection(result: int):
|
||||
if result == 1 and self._select_language_dialog:
|
||||
selected_language = multilang.languages[self._select_language_dialog.selection]
|
||||
multilang.change_language(selected_language)
|
||||
self._update_calib_description()
|
||||
self._select_language_dialog = None
|
||||
|
||||
self._select_language_dialog = MultiOptionDialog("Select a language", languages)
|
||||
gui_app.set_modal_overlay(self._select_language_dialog, callback=self._handle_language_selection)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def _handle_language_selection(self, result: int):
|
||||
if result == 1 and self._select_language_dialog:
|
||||
selected_language = self._select_language_dialog.selection
|
||||
self._params.put("LanguageSetting", selected_language)
|
||||
|
||||
self._select_language_dialog = None
|
||||
self._select_language_dialog = MultiOptionDialog(tr("Select a language"), multilang.languages, multilang.codes[multilang.language],
|
||||
option_font_weight=FontWeight.UNIFONT)
|
||||
gui_app.set_modal_overlay(self._select_language_dialog, callback=handle_language_selection)
|
||||
|
||||
def _show_driver_camera(self):
|
||||
if not self._driver_camera:
|
||||
@@ -87,34 +100,80 @@ class DeviceLayout(Widget):
|
||||
|
||||
def _reset_calibration_prompt(self):
|
||||
if ui_state.engaged:
|
||||
gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Reset Calibration"))
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Reset Calibration")))
|
||||
return
|
||||
|
||||
gui_app.set_modal_overlay(
|
||||
lambda: confirm_dialog("Are you sure you want to reset calibration?", "Reset"),
|
||||
callback=self._reset_calibration,
|
||||
)
|
||||
def reset_calibration(result: int):
|
||||
# Check engaged again in case it changed while the dialog was open
|
||||
if ui_state.engaged or result != DialogResult.CONFIRM:
|
||||
return
|
||||
|
||||
def _reset_calibration(self, result: int):
|
||||
if ui_state.engaged or result != DialogResult.CONFIRM:
|
||||
return
|
||||
self._params.remove("CalibrationParams")
|
||||
self._params.remove("LiveTorqueParameters")
|
||||
self._params.remove("LiveParameters")
|
||||
self._params.remove("LiveParametersV2")
|
||||
self._params.remove("LiveDelay")
|
||||
self._params.put_bool("OnroadCycleRequested", True)
|
||||
self._update_calib_description()
|
||||
|
||||
self._params.remove("CalibrationParams")
|
||||
self._params.remove("LiveTorqueParameters")
|
||||
self._params.remove("LiveParameters")
|
||||
self._params.remove("LiveParametersV2")
|
||||
self._params.remove("LiveDelay")
|
||||
self._params.put_bool("OnroadCycleRequested", True)
|
||||
dialog = ConfirmDialog(tr("Are you sure you want to reset calibration?"), tr("Reset"))
|
||||
gui_app.set_modal_overlay(dialog, callback=reset_calibration)
|
||||
|
||||
def _update_calib_description(self):
|
||||
desc = tr(DESCRIPTIONS['reset_calibration'])
|
||||
|
||||
calib_bytes = self._params.get("CalibrationParams")
|
||||
if calib_bytes:
|
||||
try:
|
||||
calib = messaging.log_from_bytes(calib_bytes, log.Event).liveCalibration
|
||||
|
||||
if calib.calStatus != log.LiveCalibrationData.Status.uncalibrated:
|
||||
pitch = math.degrees(calib.rpyCalib[1])
|
||||
yaw = math.degrees(calib.rpyCalib[2])
|
||||
desc += tr(" Your device is pointed {:.1f}° {} and {:.1f}° {}.").format(abs(pitch), tr("down") if pitch > 0 else tr("up"),
|
||||
abs(yaw), tr("left") if yaw > 0 else tr("right"))
|
||||
except Exception:
|
||||
cloudlog.exception("invalid CalibrationParams")
|
||||
|
||||
lag_perc = 0
|
||||
lag_bytes = self._params.get("LiveDelay")
|
||||
if lag_bytes:
|
||||
try:
|
||||
lag_perc = messaging.log_from_bytes(lag_bytes, log.Event).liveDelay.calPerc
|
||||
except Exception:
|
||||
cloudlog.exception("invalid LiveDelay")
|
||||
if lag_perc < 100:
|
||||
desc += tr("<br><br>Steering lag calibration is {}% complete.").format(lag_perc)
|
||||
else:
|
||||
desc += tr("<br><br>Steering lag calibration is complete.")
|
||||
|
||||
torque_bytes = self._params.get("LiveTorqueParameters")
|
||||
if torque_bytes:
|
||||
try:
|
||||
torque = messaging.log_from_bytes(torque_bytes, log.Event).liveTorqueParameters
|
||||
# don't add for non-torque cars
|
||||
if torque.useParams:
|
||||
torque_perc = torque.calPerc
|
||||
if torque_perc < 100:
|
||||
desc += tr(" Steering torque response calibration is {}% complete.").format(torque_perc)
|
||||
else:
|
||||
desc += tr(" Steering torque response calibration is complete.")
|
||||
except Exception:
|
||||
cloudlog.exception("invalid LiveTorqueParameters")
|
||||
|
||||
desc += "<br><br>"
|
||||
desc += tr("openpilot is continuously calibrating, resetting is rarely required. " +
|
||||
"Resetting calibration will restart openpilot if the car is powered on.")
|
||||
|
||||
self._reset_calib_btn.set_description(desc)
|
||||
|
||||
def _reboot_prompt(self):
|
||||
if ui_state.engaged:
|
||||
gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Reboot"))
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Reboot")))
|
||||
return
|
||||
|
||||
gui_app.set_modal_overlay(
|
||||
lambda: confirm_dialog("Are you sure you want to reboot?", "Reboot"),
|
||||
callback=self._perform_reboot,
|
||||
)
|
||||
dialog = ConfirmDialog(tr("Are you sure you want to reboot?"), tr("Reboot"))
|
||||
gui_app.set_modal_overlay(dialog, callback=self._perform_reboot)
|
||||
|
||||
def _perform_reboot(self, result: int):
|
||||
if not ui_state.engaged and result == DialogResult.CONFIRM:
|
||||
@@ -122,13 +181,11 @@ class DeviceLayout(Widget):
|
||||
|
||||
def _power_off_prompt(self):
|
||||
if ui_state.engaged:
|
||||
gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Power Off"))
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Power Off")))
|
||||
return
|
||||
|
||||
gui_app.set_modal_overlay(
|
||||
lambda: confirm_dialog("Are you sure you want to power off?", "Power Off"),
|
||||
callback=self._perform_power_off,
|
||||
)
|
||||
dialog = ConfirmDialog(tr("Are you sure you want to power off?"), tr("Power Off"))
|
||||
gui_app.set_modal_overlay(dialog, callback=self._perform_power_off)
|
||||
|
||||
def _perform_power_off(self, result: int):
|
||||
if not ui_state.engaged and result == DialogResult.CONFIRM:
|
||||
@@ -141,10 +198,13 @@ class DeviceLayout(Widget):
|
||||
|
||||
def _on_regulatory(self):
|
||||
if not self._fcc_dialog:
|
||||
self._fcc_dialog = HtmlRenderer(os.path.join(BASEDIR, "selfdrive/assets/offroad/fcc.html"))
|
||||
self._fcc_dialog = HtmlModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/fcc.html"))
|
||||
gui_app.set_modal_overlay(self._fcc_dialog)
|
||||
|
||||
gui_app.set_modal_overlay(self._fcc_dialog,
|
||||
callback=lambda result: setattr(self, '_fcc_dialog', None),
|
||||
)
|
||||
def _on_review_training_guide(self):
|
||||
if not self._training_guide:
|
||||
def completed_callback():
|
||||
gui_app.set_modal_overlay(None)
|
||||
|
||||
def _on_review_training_guide(self): pass
|
||||
self._training_guide = TrainingGuide(completed_callback=completed_callback)
|
||||
gui_app.set_modal_overlay(self._training_guide)
|
||||
|
||||
@@ -7,21 +7,23 @@ from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
|
||||
from openpilot.system.ui.lib.multilang import tr, trn, tr_noop
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.selfdrive.ui.lib.api_helpers import get_token
|
||||
|
||||
TITLE = "Firehose Mode"
|
||||
DESCRIPTION = (
|
||||
TITLE = tr_noop("Firehose Mode")
|
||||
DESCRIPTION = tr_noop(
|
||||
"openpilot learns to drive by watching humans, like you, drive.\n\n"
|
||||
+ "Firehose Mode allows you to maximize your training data uploads to improve "
|
||||
+ "openpilot's driving models. More data means bigger models, which means better Experimental Mode."
|
||||
)
|
||||
INSTRUCTIONS = (
|
||||
INSTRUCTIONS = tr_noop(
|
||||
"For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.\n\n"
|
||||
+ "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n"
|
||||
+ "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n\n"
|
||||
+ "Frequently Asked Questions\n\n"
|
||||
+ "Does it matter how or where I drive? Nope, just drive as you normally would.\n\n"
|
||||
+ "Do all of my segments get pulled in Firehose Mode? No, we selectively pull a subset of your segments.\n\n"
|
||||
@@ -43,12 +45,16 @@ class FirehoseLayout(Widget):
|
||||
self.params = Params()
|
||||
self.segment_count = self._get_segment_count()
|
||||
self.scroll_panel = GuiScrollPanel()
|
||||
self._content_height = 0
|
||||
|
||||
self.running = True
|
||||
self.update_thread = threading.Thread(target=self._update_loop, daemon=True)
|
||||
self.update_thread.start()
|
||||
self.last_update_time = 0
|
||||
|
||||
def show_event(self):
|
||||
self.scroll_panel.set_offset(0)
|
||||
|
||||
def _get_segment_count(self) -> int:
|
||||
stats = self.params.get(self.PARAM_KEY)
|
||||
if not stats:
|
||||
@@ -66,97 +72,72 @@ class FirehoseLayout(Widget):
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
# Calculate content dimensions
|
||||
content_width = rect.width - 80
|
||||
content_height = self._calculate_content_height(int(content_width))
|
||||
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height)
|
||||
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, self._content_height)
|
||||
|
||||
# Handle scrolling and render with clipping
|
||||
scroll_offset = self.scroll_panel.handle_scroll(rect, content_rect)
|
||||
scroll_offset = self.scroll_panel.update(rect, content_rect)
|
||||
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
|
||||
self._render_content(rect, scroll_offset)
|
||||
self._content_height = self._render_content(rect, scroll_offset)
|
||||
rl.end_scissor_mode()
|
||||
|
||||
def _calculate_content_height(self, content_width: int) -> int:
|
||||
height = 80 # Top margin
|
||||
|
||||
# Title
|
||||
height += 100 + 40
|
||||
|
||||
# Description
|
||||
desc_font = gui_app.font(FontWeight.NORMAL)
|
||||
desc_lines = wrap_text(desc_font, DESCRIPTION, 45, content_width)
|
||||
height += len(desc_lines) * 45 + 40
|
||||
|
||||
# Status section
|
||||
height += 32 # Separator
|
||||
status_text, _ = self._get_status()
|
||||
status_lines = wrap_text(gui_app.font(FontWeight.BOLD), status_text, 60, content_width)
|
||||
height += len(status_lines) * 60 + 20
|
||||
|
||||
# Contribution count (if available)
|
||||
if self.segment_count > 0:
|
||||
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far."
|
||||
contrib_lines = wrap_text(gui_app.font(FontWeight.BOLD), contrib_text, 52, content_width)
|
||||
height += len(contrib_lines) * 52 + 20
|
||||
|
||||
# Instructions section
|
||||
height += 32 # Separator
|
||||
inst_lines = wrap_text(gui_app.font(FontWeight.NORMAL), INSTRUCTIONS, 40, content_width)
|
||||
height += len(inst_lines) * 40 + 40 # Bottom margin
|
||||
|
||||
return height
|
||||
|
||||
def _render_content(self, rect: rl.Rectangle, scroll_offset: rl.Vector2):
|
||||
def _render_content(self, rect: rl.Rectangle, scroll_offset: float) -> int:
|
||||
x = int(rect.x + 40)
|
||||
y = int(rect.y + 40 + scroll_offset.y)
|
||||
y = int(rect.y + 40 + scroll_offset)
|
||||
w = int(rect.width - 80)
|
||||
|
||||
# Title
|
||||
# Title (centered)
|
||||
title_text = tr(TITLE) # live translate
|
||||
title_font = gui_app.font(FontWeight.MEDIUM)
|
||||
rl.draw_text_ex(title_font, TITLE, rl.Vector2(x, y), 100, 0, rl.WHITE)
|
||||
y += 140
|
||||
text_width = measure_text_cached(title_font, title_text, 100).x
|
||||
title_x = rect.x + (rect.width - text_width) / 2
|
||||
rl.draw_text_ex(title_font, title_text, rl.Vector2(title_x, y), 100, 0, rl.WHITE)
|
||||
y += 200
|
||||
|
||||
# Description
|
||||
y = self._draw_wrapped_text(x, y, w, DESCRIPTION, gui_app.font(FontWeight.NORMAL), 45, rl.WHITE)
|
||||
y += 40
|
||||
y = self._draw_wrapped_text(x, y, w, tr(DESCRIPTION), gui_app.font(FontWeight.NORMAL), 45, rl.WHITE)
|
||||
y += 40 + 20
|
||||
|
||||
# Separator
|
||||
rl.draw_rectangle(x, y, w, 2, self.GRAY)
|
||||
y += 30
|
||||
y += 30 + 20
|
||||
|
||||
# Status
|
||||
status_text, status_color = self._get_status()
|
||||
y = self._draw_wrapped_text(x, y, w, status_text, gui_app.font(FontWeight.BOLD), 60, status_color)
|
||||
y += 20
|
||||
y += 20 + 20
|
||||
|
||||
# Contribution count (if available)
|
||||
if self.segment_count > 0:
|
||||
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far."
|
||||
contrib_text = trn("{} segment of your driving is in the training dataset so far.",
|
||||
"{} segments of your driving is in the training dataset so far.", self.segment_count).format(self.segment_count)
|
||||
y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE)
|
||||
y += 20
|
||||
y += 20 + 20
|
||||
|
||||
# Separator
|
||||
rl.draw_rectangle(x, y, w, 2, self.GRAY)
|
||||
y += 30
|
||||
y += 30 + 20
|
||||
|
||||
# Instructions
|
||||
self._draw_wrapped_text(x, y, w, INSTRUCTIONS, gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY)
|
||||
y = self._draw_wrapped_text(x, y, w, tr(INSTRUCTIONS), gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY)
|
||||
|
||||
def _draw_wrapped_text(self, x, y, width, text, font, size, color):
|
||||
wrapped = wrap_text(font, text, size, width)
|
||||
# bottom margin + remove effect of scroll offset
|
||||
return int(round(y - self.scroll_panel.offset + 40))
|
||||
|
||||
def _draw_wrapped_text(self, x, y, width, text, font, font_size, color):
|
||||
wrapped = wrap_text(font, text, font_size, width)
|
||||
for line in wrapped:
|
||||
rl.draw_text_ex(font, line, rl.Vector2(x, y), size, 0, color)
|
||||
y += size
|
||||
return y
|
||||
rl.draw_text_ex(font, line, rl.Vector2(x, y), font_size, 0, color)
|
||||
y += font_size * FONT_SCALE
|
||||
return round(y)
|
||||
|
||||
def _get_status(self) -> tuple[str, rl.Color]:
|
||||
network_type = ui_state.sm["deviceState"].networkType
|
||||
network_metered = ui_state.sm["deviceState"].networkMetered
|
||||
|
||||
if not network_metered and network_type != 0: # Not metered and connected
|
||||
return "ACTIVE", self.GREEN
|
||||
return tr("ACTIVE"), self.GREEN
|
||||
else:
|
||||
return "INACTIVE: connect to an unmetered network", self.RED
|
||||
return tr("INACTIVE: connect to an unmetered network"), self.RED
|
||||
|
||||
def _fetch_firehose_stats(self):
|
||||
try:
|
||||
|
||||
@@ -8,18 +8,16 @@ from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.network import WifiManagerUI
|
||||
|
||||
# Settings close button
|
||||
SETTINGS_CLOSE_TEXT = "×"
|
||||
SETTINGS_CLOSE_TEXT_Y_OFFSET = 8 # The '×' character isn't quite vertically centered in the font so we need to offset it a bit to fully center it
|
||||
from openpilot.system.ui.widgets.network import NetworkUI
|
||||
|
||||
# Constants
|
||||
SIDEBAR_WIDTH = 500
|
||||
CLOSE_BTN_SIZE = 200
|
||||
CLOSE_ICON_SIZE = 70
|
||||
NAV_BTN_HEIGHT = 110
|
||||
PANEL_MARGIN = 50
|
||||
|
||||
@@ -58,15 +56,16 @@ class SettingsLayout(Widget):
|
||||
wifi_manager.set_active(False)
|
||||
|
||||
self._panels = {
|
||||
PanelType.DEVICE: PanelInfo("Device", DeviceLayout()),
|
||||
PanelType.NETWORK: PanelInfo("Network", WifiManagerUI(wifi_manager)),
|
||||
PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout()),
|
||||
PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout()),
|
||||
PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout()),
|
||||
PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayout()),
|
||||
PanelType.DEVICE: PanelInfo(tr_noop("Device"), DeviceLayout()),
|
||||
PanelType.NETWORK: PanelInfo(tr_noop("Network"), NetworkUI(wifi_manager)),
|
||||
PanelType.TOGGLES: PanelInfo(tr_noop("Toggles"), TogglesLayout()),
|
||||
PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayout()),
|
||||
PanelType.FIREHOSE: PanelInfo(tr_noop("Firehose"), FirehoseLayout()),
|
||||
PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayout()),
|
||||
}
|
||||
|
||||
self._font_medium = gui_app.font(FontWeight.MEDIUM)
|
||||
self._close_icon = gui_app.texture("icons/close2.png", CLOSE_ICON_SIZE, CLOSE_ICON_SIZE)
|
||||
|
||||
# Callbacks
|
||||
self._close_callback: Callable | None = None
|
||||
@@ -96,12 +95,21 @@ class SettingsLayout(Widget):
|
||||
close_color = CLOSE_BTN_PRESSED if pressed else CLOSE_BTN_COLOR
|
||||
rl.draw_rectangle_rounded(close_btn_rect, 1.0, 20, close_color)
|
||||
|
||||
close_text_size = measure_text_cached(self._font_medium, SETTINGS_CLOSE_TEXT, 140)
|
||||
close_text_pos = rl.Vector2(
|
||||
close_btn_rect.x + (close_btn_rect.width - close_text_size.x) / 2,
|
||||
close_btn_rect.y + (close_btn_rect.height - close_text_size.y) / 2 - SETTINGS_CLOSE_TEXT_Y_OFFSET,
|
||||
icon_color = rl.Color(255, 255, 255, 255) if not pressed else rl.Color(220, 220, 220, 255)
|
||||
icon_dest = rl.Rectangle(
|
||||
close_btn_rect.x + (close_btn_rect.width - self._close_icon.width) / 2,
|
||||
close_btn_rect.y + (close_btn_rect.height - self._close_icon.height) / 2,
|
||||
self._close_icon.width,
|
||||
self._close_icon.height,
|
||||
)
|
||||
rl.draw_texture_pro(
|
||||
self._close_icon,
|
||||
rl.Rectangle(0, 0, self._close_icon.width, self._close_icon.height),
|
||||
icon_dest,
|
||||
rl.Vector2(0, 0),
|
||||
0,
|
||||
icon_color,
|
||||
)
|
||||
rl.draw_text_ex(self._font_medium, SETTINGS_CLOSE_TEXT, close_text_pos, 140, 0, TEXT_SELECTED)
|
||||
|
||||
# Store close button rect for click detection
|
||||
self._close_btn_rect = close_btn_rect
|
||||
@@ -115,11 +123,12 @@ class SettingsLayout(Widget):
|
||||
is_selected = panel_type == self._current_panel
|
||||
text_color = TEXT_SELECTED if is_selected else TEXT_NORMAL
|
||||
# Draw button text (right-aligned)
|
||||
text_size = measure_text_cached(self._font_medium, panel_info.name, 65)
|
||||
panel_name = tr(panel_info.name)
|
||||
text_size = measure_text_cached(self._font_medium, panel_name, 65)
|
||||
text_pos = rl.Vector2(
|
||||
button_rect.x + button_rect.width - text_size.x, button_rect.y + (button_rect.height - text_size.y) / 2
|
||||
)
|
||||
rl.draw_text_ex(self._font_medium, panel_info.name, text_pos, 65, 0, text_color)
|
||||
rl.draw_text_ex(self._font_medium, panel_name, text_pos, 65, 0, text_color)
|
||||
|
||||
# Store button rect for click detection
|
||||
panel_info.button_rect = button_rect
|
||||
|
||||
@@ -1,42 +1,194 @@
|
||||
from openpilot.common.params import Params
|
||||
import os
|
||||
import time
|
||||
import datetime
|
||||
from openpilot.common.time_helpers import system_time_valid
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr, trn
|
||||
from openpilot.system.ui.widgets import Widget, DialogResult
|
||||
from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog
|
||||
from openpilot.system.ui.widgets.list_view import button_item, text_item
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.widgets.list_view import button_item, text_item, ListItem
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
|
||||
# TODO: remove this. updater fails to respond on startup if time is not correct
|
||||
UPDATED_TIMEOUT = 10 # seconds to wait for updated to respond
|
||||
|
||||
|
||||
def time_ago(date: datetime.datetime | None) -> str:
|
||||
if not date:
|
||||
return tr("never")
|
||||
|
||||
if not system_time_valid():
|
||||
return date.strftime("%a %b %d %Y")
|
||||
|
||||
now = datetime.datetime.now(datetime.UTC)
|
||||
if date.tzinfo is None:
|
||||
date = date.replace(tzinfo=datetime.UTC)
|
||||
|
||||
diff_seconds = int((now - date).total_seconds())
|
||||
if diff_seconds < 60:
|
||||
return tr("now")
|
||||
if diff_seconds < 3600:
|
||||
m = diff_seconds // 60
|
||||
return trn("{} minute ago", "{} minutes ago", m).format(m)
|
||||
if diff_seconds < 86400:
|
||||
h = diff_seconds // 3600
|
||||
return trn("{} hour ago", "{} hours ago", h).format(h)
|
||||
if diff_seconds < 604800:
|
||||
d = diff_seconds // 86400
|
||||
return trn("{} day ago", "{} days ago", d).format(d)
|
||||
return date.strftime("%a %b %d %Y")
|
||||
|
||||
|
||||
class SoftwareLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
items = self._init_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
self._onroad_label = ListItem(lambda: tr("Updates are only downloaded while the car is off."))
|
||||
self._version_item = text_item(lambda: tr("Current Version"), ui_state.params.get("UpdaterCurrentDescription") or "")
|
||||
self._download_btn = button_item(lambda: tr("Download"), lambda: tr("CHECK"), callback=self._on_download_update)
|
||||
|
||||
def _init_items(self):
|
||||
items = [
|
||||
text_item("Current Version", ""),
|
||||
button_item("Download", "CHECK", callback=self._on_download_update),
|
||||
button_item("Install Update", "INSTALL", callback=self._on_install_update),
|
||||
button_item("Target Branch", "SELECT", callback=self._on_select_branch),
|
||||
button_item("Uninstall", "UNINSTALL", callback=self._on_uninstall),
|
||||
]
|
||||
return items
|
||||
# Install button is initially hidden
|
||||
self._install_btn = button_item(lambda: tr("Install Update"), lambda: tr("INSTALL"), callback=self._on_install_update)
|
||||
self._install_btn.set_visible(False)
|
||||
|
||||
# Track waiting-for-updater transition to avoid brief re-enable while still idle
|
||||
self._waiting_for_updater = False
|
||||
self._waiting_start_ts: float = 0.0
|
||||
|
||||
# Branch switcher
|
||||
self._branch_btn = button_item(lambda: tr("Target Branch"), lambda: tr("SELECT"), callback=self._on_select_branch)
|
||||
self._branch_btn.set_visible(not ui_state.params.get_bool("IsTestedBranch"))
|
||||
self._branch_btn.action_item.set_value(ui_state.params.get("UpdaterTargetBranch") or "")
|
||||
self._branch_dialog: MultiOptionDialog | None = None
|
||||
|
||||
self._scroller = Scroller([
|
||||
self._onroad_label,
|
||||
self._version_item,
|
||||
self._download_btn,
|
||||
self._install_btn,
|
||||
self._branch_btn,
|
||||
button_item(lambda: tr("Uninstall"), lambda: tr("UNINSTALL"), callback=self._on_uninstall),
|
||||
], line_separator=True, spacing=0)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def _on_download_update(self): pass
|
||||
def _on_install_update(self): pass
|
||||
def _on_select_branch(self): pass
|
||||
def _update_state(self):
|
||||
# Show/hide onroad warning
|
||||
self._onroad_label.set_visible(ui_state.is_onroad())
|
||||
|
||||
# Update current version and release notes
|
||||
current_desc = ui_state.params.get("UpdaterCurrentDescription") or ""
|
||||
current_release_notes = (ui_state.params.get("UpdaterCurrentReleaseNotes") or b"").decode("utf-8", "replace")
|
||||
self._version_item.action_item.set_text(current_desc)
|
||||
self._version_item.set_description(current_release_notes)
|
||||
|
||||
# Update download button visibility and state
|
||||
self._download_btn.set_visible(ui_state.is_offroad())
|
||||
|
||||
updater_state = ui_state.params.get("UpdaterState") or "idle"
|
||||
failed_count = ui_state.params.get("UpdateFailedCount") or 0
|
||||
fetch_available = ui_state.params.get_bool("UpdaterFetchAvailable")
|
||||
update_available = ui_state.params.get_bool("UpdateAvailable")
|
||||
|
||||
if updater_state != "idle":
|
||||
# Updater responded
|
||||
self._waiting_for_updater = False
|
||||
self._download_btn.action_item.set_enabled(False)
|
||||
self._download_btn.action_item.set_value(updater_state)
|
||||
else:
|
||||
if failed_count > 0:
|
||||
self._download_btn.action_item.set_value(tr("failed to check for update"))
|
||||
self._download_btn.action_item.set_text(tr("CHECK"))
|
||||
elif fetch_available:
|
||||
self._download_btn.action_item.set_value(tr("update available"))
|
||||
self._download_btn.action_item.set_text(tr("DOWNLOAD"))
|
||||
else:
|
||||
last_update = ui_state.params.get("LastUpdateTime")
|
||||
if last_update:
|
||||
formatted = time_ago(last_update)
|
||||
self._download_btn.action_item.set_value(tr("up to date, last checked {}").format(formatted))
|
||||
else:
|
||||
self._download_btn.action_item.set_value(tr("up to date, last checked never"))
|
||||
self._download_btn.action_item.set_text(tr("CHECK"))
|
||||
|
||||
# If we've been waiting too long without a state change, reset state
|
||||
if self._waiting_for_updater and (time.monotonic() - self._waiting_start_ts > UPDATED_TIMEOUT):
|
||||
self._waiting_for_updater = False
|
||||
|
||||
# Only enable if we're not waiting for updater to flip out of idle
|
||||
self._download_btn.action_item.set_enabled(not self._waiting_for_updater)
|
||||
|
||||
# Update target branch button value
|
||||
current_branch = ui_state.params.get("UpdaterTargetBranch") or ""
|
||||
self._branch_btn.action_item.set_value(current_branch)
|
||||
|
||||
# Update install button
|
||||
self._install_btn.set_visible(ui_state.is_offroad() and update_available)
|
||||
if update_available:
|
||||
new_desc = ui_state.params.get("UpdaterNewDescription") or ""
|
||||
new_release_notes = (ui_state.params.get("UpdaterNewReleaseNotes") or b"").decode("utf-8", "replace")
|
||||
self._install_btn.action_item.set_text(tr("INSTALL"))
|
||||
self._install_btn.action_item.set_value(new_desc)
|
||||
self._install_btn.set_description(new_release_notes)
|
||||
# Enable install button for testing (like Qt showEvent)
|
||||
self._install_btn.action_item.set_enabled(True)
|
||||
else:
|
||||
self._install_btn.set_visible(False)
|
||||
|
||||
def _on_download_update(self):
|
||||
# Check if we should start checking or start downloading
|
||||
self._download_btn.action_item.set_enabled(False)
|
||||
if self._download_btn.action_item.text == tr("CHECK"):
|
||||
# Start checking for updates
|
||||
self._waiting_for_updater = True
|
||||
self._waiting_start_ts = time.monotonic()
|
||||
os.system("pkill -SIGUSR1 -f system.updated.updated")
|
||||
else:
|
||||
# Start downloading
|
||||
self._waiting_for_updater = True
|
||||
self._waiting_start_ts = time.monotonic()
|
||||
os.system("pkill -SIGHUP -f system.updated.updated")
|
||||
|
||||
def _on_uninstall(self):
|
||||
def handle_uninstall_confirmation(result):
|
||||
if result == DialogResult.CONFIRM:
|
||||
self._params.put_bool("DoUninstall", True)
|
||||
ui_state.params.put_bool("DoUninstall", True)
|
||||
|
||||
gui_app.set_modal_overlay(
|
||||
lambda: confirm_dialog("Are you sure you want to uninstall?", "Uninstall"),
|
||||
callback=handle_uninstall_confirmation,
|
||||
)
|
||||
dialog = ConfirmDialog(tr("Are you sure you want to uninstall?"), tr("Uninstall"))
|
||||
gui_app.set_modal_overlay(dialog, callback=handle_uninstall_confirmation)
|
||||
|
||||
def _on_install_update(self):
|
||||
# Trigger reboot to install update
|
||||
self._install_btn.action_item.set_enabled(False)
|
||||
ui_state.params.put_bool("DoReboot", True)
|
||||
|
||||
def _on_select_branch(self):
|
||||
# Get available branches and order
|
||||
current_git_branch = ui_state.params.get("GitBranch") or ""
|
||||
branches_str = ui_state.params.get("UpdaterAvailableBranches") or ""
|
||||
branches = [b for b in branches_str.split(",") if b]
|
||||
|
||||
for b in [current_git_branch, "devel-staging", "devel", "nightly", "nightly-dev", "master"]:
|
||||
if b in branches:
|
||||
branches.remove(b)
|
||||
branches.insert(0, b)
|
||||
|
||||
current_target = ui_state.params.get("UpdaterTargetBranch") or ""
|
||||
self._branch_dialog = MultiOptionDialog(tr("Select a branch"), branches, current_target)
|
||||
|
||||
def handle_selection(result):
|
||||
# Confirmed selection
|
||||
if result == DialogResult.CONFIRM and self._branch_dialog is not None and self._branch_dialog.selection:
|
||||
selection = self._branch_dialog.selection
|
||||
ui_state.params.put("UpdaterTargetBranch", selection)
|
||||
self._branch_btn.action_item.set_value(selection)
|
||||
os.system("pkill -SIGUSR1 -f system.updated.updated")
|
||||
self._branch_dialog = None
|
||||
|
||||
gui_app.set_modal_overlay(self._branch_dialog, callback=handle_selection)
|
||||
|
||||
@@ -1,28 +1,36 @@
|
||||
from openpilot.common.params import Params
|
||||
from cereal import log
|
||||
from openpilot.common.params import Params, UnknownKeyName
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.list_view import multiple_button_item, toggle_item
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.widgets import DialogResult
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants
|
||||
|
||||
# Description constants
|
||||
DESCRIPTIONS = {
|
||||
"OpenpilotEnabledToggle": (
|
||||
"OpenpilotEnabledToggle": tr_noop(
|
||||
"Use the openpilot system for adaptive cruise control and lane keep driver assistance. " +
|
||||
"Your attention is required at all times to use this feature."
|
||||
),
|
||||
"DisengageOnAccelerator": "When enabled, pressing the accelerator pedal will disengage openpilot.",
|
||||
"LongitudinalPersonality": (
|
||||
"DisengageOnAccelerator": tr_noop("When enabled, pressing the accelerator pedal will disengage openpilot."),
|
||||
"LongitudinalPersonality": tr_noop(
|
||||
"Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. " +
|
||||
"In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " +
|
||||
"your steering wheel distance button."
|
||||
),
|
||||
"IsLdwEnabled": (
|
||||
"IsLdwEnabled": tr_noop(
|
||||
"Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " +
|
||||
"without a turn signal activated while driving over 31 mph (50 km/h)."
|
||||
),
|
||||
"AlwaysOnDM": "Enable driver monitoring even when openpilot is not engaged.",
|
||||
'RecordFront': "Upload data from the driver facing camera and help improve the driver monitoring algorithm.",
|
||||
"IsMetric": "Display speed in km/h instead of mph.",
|
||||
"RecordAudio": "Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.",
|
||||
"AlwaysOnDM": tr_noop("Enable driver monitoring even when openpilot is not engaged."),
|
||||
'RecordFront': tr_noop("Upload data from the driver facing camera and help improve the driver monitoring algorithm."),
|
||||
"IsMetric": tr_noop("Display speed in km/h instead of mph."),
|
||||
"RecordAudio": tr_noop("Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."),
|
||||
}
|
||||
|
||||
|
||||
@@ -30,66 +38,207 @@ class TogglesLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
items = [
|
||||
toggle_item(
|
||||
"Enable openpilot",
|
||||
DESCRIPTIONS["OpenpilotEnabledToggle"],
|
||||
self._params.get_bool("OpenpilotEnabledToggle"),
|
||||
icon="chffr_wheel.png",
|
||||
),
|
||||
toggle_item(
|
||||
"Experimental Mode",
|
||||
initial_state=self._params.get_bool("ExperimentalMode"),
|
||||
icon="experimental_white.png",
|
||||
),
|
||||
toggle_item(
|
||||
"Disengage on Accelerator Pedal",
|
||||
DESCRIPTIONS["DisengageOnAccelerator"],
|
||||
self._params.get_bool("DisengageOnAccelerator"),
|
||||
icon="disengage_on_accelerator.png",
|
||||
),
|
||||
multiple_button_item(
|
||||
"Driving Personality",
|
||||
DESCRIPTIONS["LongitudinalPersonality"],
|
||||
buttons=["Aggressive", "Standard", "Relaxed"],
|
||||
button_width=255,
|
||||
callback=self._set_longitudinal_personality,
|
||||
selected_index=self._params.get("LongitudinalPersonality", return_default=True),
|
||||
icon="speed_limit.png"
|
||||
),
|
||||
toggle_item(
|
||||
"Enable Lane Departure Warnings",
|
||||
DESCRIPTIONS["IsLdwEnabled"],
|
||||
self._params.get_bool("IsLdwEnabled"),
|
||||
icon="warning.png",
|
||||
),
|
||||
toggle_item(
|
||||
"Always-On Driver Monitoring",
|
||||
DESCRIPTIONS["AlwaysOnDM"],
|
||||
self._params.get_bool("AlwaysOnDM"),
|
||||
icon="monitoring.png",
|
||||
),
|
||||
toggle_item(
|
||||
"Record and Upload Driver Camera",
|
||||
DESCRIPTIONS["RecordFront"],
|
||||
self._params.get_bool("RecordFront"),
|
||||
icon="monitoring.png",
|
||||
),
|
||||
toggle_item(
|
||||
"Record Microphone Audio",
|
||||
DESCRIPTIONS["RecordAudio"],
|
||||
self._params.get_bool("RecordAudio"),
|
||||
icon="microphone.png",
|
||||
),
|
||||
toggle_item(
|
||||
"Use Metric System", DESCRIPTIONS["IsMetric"], self._params.get_bool("IsMetric"), icon="metric.png"
|
||||
),
|
||||
]
|
||||
self._is_release = self._params.get_bool("IsReleaseBranch")
|
||||
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
# param, title, desc, icon, needs_restart
|
||||
self._toggle_defs = {
|
||||
"OpenpilotEnabledToggle": (
|
||||
lambda: tr("Enable openpilot"),
|
||||
DESCRIPTIONS["OpenpilotEnabledToggle"],
|
||||
"chffr_wheel.png",
|
||||
True,
|
||||
),
|
||||
"ExperimentalMode": (
|
||||
lambda: tr("Experimental Mode"),
|
||||
"",
|
||||
"experimental_white.png",
|
||||
False,
|
||||
),
|
||||
"DisengageOnAccelerator": (
|
||||
lambda: tr("Disengage on Accelerator Pedal"),
|
||||
DESCRIPTIONS["DisengageOnAccelerator"],
|
||||
"disengage_on_accelerator.png",
|
||||
False,
|
||||
),
|
||||
"IsLdwEnabled": (
|
||||
lambda: tr("Enable Lane Departure Warnings"),
|
||||
DESCRIPTIONS["IsLdwEnabled"],
|
||||
"warning.png",
|
||||
False,
|
||||
),
|
||||
"AlwaysOnDM": (
|
||||
lambda: tr("Always-On Driver Monitoring"),
|
||||
DESCRIPTIONS["AlwaysOnDM"],
|
||||
"monitoring.png",
|
||||
False,
|
||||
),
|
||||
"RecordFront": (
|
||||
lambda: tr("Record and Upload Driver Camera"),
|
||||
DESCRIPTIONS["RecordFront"],
|
||||
"monitoring.png",
|
||||
True,
|
||||
),
|
||||
"RecordAudio": (
|
||||
lambda: tr("Record and Upload Microphone Audio"),
|
||||
DESCRIPTIONS["RecordAudio"],
|
||||
"microphone.png",
|
||||
True,
|
||||
),
|
||||
"IsMetric": (
|
||||
lambda: tr("Use Metric System"),
|
||||
DESCRIPTIONS["IsMetric"],
|
||||
"metric.png",
|
||||
False,
|
||||
),
|
||||
}
|
||||
|
||||
self._long_personality_setting = multiple_button_item(
|
||||
lambda: tr("Driving Personality"),
|
||||
lambda: tr(DESCRIPTIONS["LongitudinalPersonality"]),
|
||||
buttons=[lambda: tr("Aggressive"), lambda: tr("Standard"), lambda: tr("Relaxed")],
|
||||
button_width=255,
|
||||
callback=self._set_longitudinal_personality,
|
||||
selected_index=self._params.get("LongitudinalPersonality", return_default=True),
|
||||
icon="speed_limit.png"
|
||||
)
|
||||
|
||||
self._toggles = {}
|
||||
self._locked_toggles = set()
|
||||
for param, (title, desc, icon, needs_restart) in self._toggle_defs.items():
|
||||
toggle = toggle_item(
|
||||
title,
|
||||
desc,
|
||||
self._params.get_bool(param),
|
||||
callback=lambda state, p=param: self._toggle_callback(state, p),
|
||||
icon=icon,
|
||||
)
|
||||
|
||||
try:
|
||||
locked = self._params.get_bool(param + "Lock")
|
||||
except UnknownKeyName:
|
||||
locked = False
|
||||
toggle.action_item.set_enabled(not locked)
|
||||
|
||||
# Make description callable for live translation
|
||||
additional_desc = ""
|
||||
if needs_restart and not locked:
|
||||
additional_desc = tr("Changing this setting will restart openpilot if the car is powered on.")
|
||||
toggle.set_description(lambda og_desc=toggle.description, add_desc=additional_desc: tr(og_desc) + (" " + tr(add_desc) if add_desc else ""))
|
||||
|
||||
# track for engaged state updates
|
||||
if locked:
|
||||
self._locked_toggles.add(param)
|
||||
|
||||
self._toggles[param] = toggle
|
||||
|
||||
# insert longitudinal personality after NDOG toggle
|
||||
if param == "DisengageOnAccelerator":
|
||||
self._toggles["LongitudinalPersonality"] = self._long_personality_setting
|
||||
|
||||
self._update_experimental_mode_icon()
|
||||
self._scroller = Scroller(list(self._toggles.values()), line_separator=True, spacing=0)
|
||||
|
||||
ui_state.add_engaged_transition_callback(self._update_toggles)
|
||||
|
||||
def _update_state(self):
|
||||
if ui_state.sm.updated["selfdriveState"]:
|
||||
personality = PERSONALITY_TO_INT[ui_state.sm["selfdriveState"].personality]
|
||||
if personality != ui_state.personality and ui_state.started:
|
||||
self._long_personality_setting.action_item.set_selected_button(personality)
|
||||
ui_state.personality = personality
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
self._update_toggles()
|
||||
|
||||
def _update_toggles(self):
|
||||
ui_state.update_params()
|
||||
|
||||
e2e_description = tr(
|
||||
"openpilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. " +
|
||||
"Experimental features are listed below:<br>" +
|
||||
"<h4>End-to-End Longitudinal Control</h4><br>" +
|
||||
"Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. " +
|
||||
"Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; " +
|
||||
"mistakes should be expected.<br>" +
|
||||
"<h4>New Driving Visualization</h4><br>" +
|
||||
"The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. " +
|
||||
"The Experimental mode logo will also be shown in the top right corner."
|
||||
)
|
||||
|
||||
if ui_state.CP is not None:
|
||||
if ui_state.has_longitudinal_control:
|
||||
self._toggles["ExperimentalMode"].action_item.set_enabled(True)
|
||||
self._toggles["ExperimentalMode"].set_description(e2e_description)
|
||||
self._long_personality_setting.action_item.set_enabled(True)
|
||||
else:
|
||||
# no long for now
|
||||
self._toggles["ExperimentalMode"].action_item.set_enabled(False)
|
||||
self._toggles["ExperimentalMode"].action_item.set_state(False)
|
||||
self._long_personality_setting.action_item.set_enabled(False)
|
||||
self._params.remove("ExperimentalMode")
|
||||
|
||||
unavailable = tr("Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control.")
|
||||
|
||||
long_desc = unavailable + " " + tr("openpilot longitudinal control may come in a future update.")
|
||||
if ui_state.CP.alphaLongitudinalAvailable:
|
||||
if self._is_release:
|
||||
long_desc = unavailable + " " + tr("An alpha version of openpilot longitudinal control can be tested, along with " +
|
||||
"Experimental mode, on non-release branches.")
|
||||
else:
|
||||
long_desc = tr("Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode.")
|
||||
|
||||
self._toggles["ExperimentalMode"].set_description("<b>" + long_desc + "</b><br><br>" + e2e_description)
|
||||
else:
|
||||
self._toggles["ExperimentalMode"].set_description(e2e_description)
|
||||
|
||||
self._update_experimental_mode_icon()
|
||||
|
||||
# TODO: make a param control list item so we don't need to manage internal state as much here
|
||||
# refresh toggles from params to mirror external changes
|
||||
for param in self._toggle_defs:
|
||||
self._toggles[param].action_item.set_state(self._params.get_bool(param))
|
||||
|
||||
# these toggles need restart, block while engaged
|
||||
for toggle_def in self._toggle_defs:
|
||||
if self._toggle_defs[toggle_def][3] and toggle_def not in self._locked_toggles:
|
||||
self._toggles[toggle_def].action_item.set_enabled(not ui_state.engaged)
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def _update_experimental_mode_icon(self):
|
||||
icon = "experimental.png" if self._toggles["ExperimentalMode"].action_item.get_state() else "experimental_white.png"
|
||||
self._toggles["ExperimentalMode"].set_icon(icon)
|
||||
|
||||
def _handle_experimental_mode_toggle(self, state: bool):
|
||||
confirmed = self._params.get_bool("ExperimentalModeConfirmed")
|
||||
if state and not confirmed:
|
||||
def confirm_callback(result: int):
|
||||
if result == DialogResult.CONFIRM:
|
||||
self._params.put_bool("ExperimentalMode", True)
|
||||
self._params.put_bool("ExperimentalModeConfirmed", True)
|
||||
else:
|
||||
self._toggles["ExperimentalMode"].action_item.set_state(False)
|
||||
self._update_experimental_mode_icon()
|
||||
|
||||
# show confirmation dialog
|
||||
content = (f"<h1>{self._toggles['ExperimentalMode'].title}</h1><br>" +
|
||||
f"<p>{self._toggles['ExperimentalMode'].description}</p>")
|
||||
dlg = ConfirmDialog(content, tr("Enable"), rich=True)
|
||||
gui_app.set_modal_overlay(dlg, callback=confirm_callback)
|
||||
else:
|
||||
self._update_experimental_mode_icon()
|
||||
self._params.put_bool("ExperimentalMode", state)
|
||||
|
||||
def _toggle_callback(self, state: bool, param: str):
|
||||
if param == "ExperimentalMode":
|
||||
self._handle_experimental_mode_toggle(state)
|
||||
return
|
||||
|
||||
self._params.put_bool(param, state)
|
||||
if self._toggle_defs[param][3]:
|
||||
self._params.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
def _set_longitudinal_personality(self, button_index: int):
|
||||
self._params.put("LongitudinalPersonality", button_index)
|
||||
|
||||
@@ -4,7 +4,8 @@ from dataclasses import dataclass
|
||||
from collections.abc import Callable
|
||||
from cereal import log
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, FONT_SCALE
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
@@ -23,7 +24,6 @@ NetworkType = log.DeviceState.NetworkType
|
||||
|
||||
# Color scheme
|
||||
class Colors:
|
||||
SIDEBAR_BG = rl.Color(57, 57, 57, 255)
|
||||
WHITE = rl.WHITE
|
||||
WHITE_DIM = rl.Color(255, 255, 255, 85)
|
||||
GRAY = rl.Color(84, 84, 84, 255)
|
||||
@@ -40,13 +40,13 @@ class Colors:
|
||||
|
||||
|
||||
NETWORK_TYPES = {
|
||||
NetworkType.none: "Offline",
|
||||
NetworkType.wifi: "WiFi",
|
||||
NetworkType.cell2G: "2G",
|
||||
NetworkType.cell3G: "3G",
|
||||
NetworkType.cell4G: "LTE",
|
||||
NetworkType.cell5G: "5G",
|
||||
NetworkType.ethernet: "Ethernet",
|
||||
NetworkType.none: tr_noop("--"),
|
||||
NetworkType.wifi: tr_noop("Wi-Fi"),
|
||||
NetworkType.ethernet: tr_noop("ETH"),
|
||||
NetworkType.cell2G: tr_noop("2G"),
|
||||
NetworkType.cell3G: tr_noop("3G"),
|
||||
NetworkType.cell4G: tr_noop("LTE"),
|
||||
NetworkType.cell5G: tr_noop("5G"),
|
||||
}
|
||||
|
||||
|
||||
@@ -68,27 +68,33 @@ class Sidebar(Widget):
|
||||
self._net_type = NETWORK_TYPES.get(NetworkType.none)
|
||||
self._net_strength = 0
|
||||
|
||||
self._temp_status = MetricData("TEMP", "GOOD", Colors.GOOD)
|
||||
self._panda_status = MetricData("VEHICLE", "ONLINE", Colors.GOOD)
|
||||
self._connect_status = MetricData("CONNECT", "OFFLINE", Colors.WARNING)
|
||||
self._temp_status = MetricData(tr_noop("TEMP"), tr_noop("GOOD"), Colors.GOOD)
|
||||
self._panda_status = MetricData(tr_noop("VEHICLE"), tr_noop("ONLINE"), Colors.GOOD)
|
||||
self._connect_status = MetricData(tr_noop("CONNECT"), tr_noop("OFFLINE"), Colors.WARNING)
|
||||
self._recording_audio = False
|
||||
|
||||
self._home_img = gui_app.texture("images/button_home.png", HOME_BTN.width, HOME_BTN.height)
|
||||
self._flag_img = gui_app.texture("images/button_flag.png", HOME_BTN.width, HOME_BTN.height)
|
||||
self._settings_img = gui_app.texture("images/button_settings.png", SETTINGS_BTN.width, SETTINGS_BTN.height)
|
||||
self._mic_img = gui_app.texture("icons/microphone.png", 30, 30)
|
||||
self._mic_indicator_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self._font_regular = gui_app.font(FontWeight.NORMAL)
|
||||
self._font_bold = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
|
||||
# Callbacks
|
||||
self._on_settings_click: Callable | None = None
|
||||
self._on_flag_click: Callable | None = None
|
||||
self._open_settings_callback: Callable | None = None
|
||||
|
||||
def set_callbacks(self, on_settings: Callable | None = None, on_flag: Callable | None = None):
|
||||
def set_callbacks(self, on_settings: Callable | None = None, on_flag: Callable | None = None,
|
||||
open_settings: Callable | None = None):
|
||||
self._on_settings_click = on_settings
|
||||
self._on_flag_click = on_flag
|
||||
self._open_settings_callback = open_settings
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
# Background
|
||||
rl.draw_rectangle_rec(rect, Colors.SIDEBAR_BG)
|
||||
rl.draw_rectangle_rec(rect, rl.BLACK)
|
||||
|
||||
self._draw_buttons(rect)
|
||||
self._draw_network_indicator(rect)
|
||||
@@ -101,13 +107,14 @@ class Sidebar(Widget):
|
||||
|
||||
device_state = sm['deviceState']
|
||||
|
||||
self._recording_audio = ui_state.recording_audio
|
||||
self._update_network_status(device_state)
|
||||
self._update_temperature_status(device_state)
|
||||
self._update_connection_status(device_state)
|
||||
self._update_panda_status()
|
||||
|
||||
def _update_network_status(self, device_state):
|
||||
self._net_type = NETWORK_TYPES.get(device_state.networkType.raw, "Unknown")
|
||||
self._net_type = NETWORK_TYPES.get(device_state.networkType.raw, tr_noop("Unknown"))
|
||||
strength = device_state.networkStrength
|
||||
self._net_strength = max(0, min(5, strength.raw + 1)) if strength > 0 else 0
|
||||
|
||||
@@ -115,26 +122,26 @@ class Sidebar(Widget):
|
||||
thermal_status = device_state.thermalStatus
|
||||
|
||||
if thermal_status == ThermalStatus.green:
|
||||
self._temp_status.update("TEMP", "GOOD", Colors.GOOD)
|
||||
self._temp_status.update(tr_noop("TEMP"), tr_noop("GOOD"), Colors.GOOD)
|
||||
elif thermal_status == ThermalStatus.yellow:
|
||||
self._temp_status.update("TEMP", "OK", Colors.WARNING)
|
||||
self._temp_status.update(tr_noop("TEMP"), tr_noop("OK"), Colors.WARNING)
|
||||
else:
|
||||
self._temp_status.update("TEMP", "HIGH", Colors.DANGER)
|
||||
self._temp_status.update(tr_noop("TEMP"), tr_noop("HIGH"), Colors.DANGER)
|
||||
|
||||
def _update_connection_status(self, device_state):
|
||||
last_ping = device_state.lastAthenaPingTime
|
||||
if last_ping == 0:
|
||||
self._connect_status.update("CONNECT", "OFFLINE", Colors.WARNING)
|
||||
self._connect_status.update(tr_noop("CONNECT"), tr_noop("OFFLINE"), Colors.WARNING)
|
||||
elif time.monotonic_ns() - last_ping < 80_000_000_000: # 80 seconds in nanoseconds
|
||||
self._connect_status.update("CONNECT", "ONLINE", Colors.GOOD)
|
||||
self._connect_status.update(tr_noop("CONNECT"), tr_noop("ONLINE"), Colors.GOOD)
|
||||
else:
|
||||
self._connect_status.update("CONNECT", "ERROR", Colors.DANGER)
|
||||
self._connect_status.update(tr_noop("CONNECT"), tr_noop("ERROR"), Colors.DANGER)
|
||||
|
||||
def _update_panda_status(self):
|
||||
if ui_state.panda_type == log.PandaState.PandaType.unknown:
|
||||
self._panda_status.update("NO", "PANDA", Colors.DANGER)
|
||||
self._panda_status.update(tr_noop("NO"), tr_noop("PANDA"), Colors.DANGER)
|
||||
else:
|
||||
self._panda_status.update("VEHICLE", "ONLINE", Colors.GOOD)
|
||||
self._panda_status.update(tr_noop("VEHICLE"), tr_noop("ONLINE"), Colors.GOOD)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
if rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN):
|
||||
@@ -143,6 +150,9 @@ class Sidebar(Widget):
|
||||
elif rl.check_collision_point_rec(mouse_pos, HOME_BTN) and ui_state.started:
|
||||
if self._on_flag_click:
|
||||
self._on_flag_click()
|
||||
elif self._recording_audio and rl.check_collision_point_rec(mouse_pos, self._mic_indicator_rect):
|
||||
if self._open_settings_callback:
|
||||
self._open_settings_callback()
|
||||
|
||||
def _draw_buttons(self, rect: rl.Rectangle):
|
||||
mouse_pos = rl.get_mouse_position()
|
||||
@@ -160,6 +170,17 @@ class Sidebar(Widget):
|
||||
tint = Colors.BUTTON_PRESSED if (ui_state.started and flag_pressed) else Colors.BUTTON_NORMAL
|
||||
rl.draw_texture(button_img, int(HOME_BTN.x), int(HOME_BTN.y), tint)
|
||||
|
||||
# Microphone button
|
||||
if self._recording_audio:
|
||||
self._mic_indicator_rect = rl.Rectangle(rect.x + rect.width - 130, rect.y + 245, 75, 40)
|
||||
|
||||
mic_pressed = mouse_down and rl.check_collision_point_rec(mouse_pos, self._mic_indicator_rect)
|
||||
bg_color = rl.Color(Colors.DANGER.r, Colors.DANGER.g, Colors.DANGER.b, int(255 * 0.65)) if mic_pressed else Colors.DANGER
|
||||
|
||||
rl.draw_rectangle_rounded(self._mic_indicator_rect, 1, 10, bg_color)
|
||||
rl.draw_texture(self._mic_img, int(self._mic_indicator_rect.x + (self._mic_indicator_rect.width - self._mic_img.width) / 2),
|
||||
int(self._mic_indicator_rect.y + (self._mic_indicator_rect.height - self._mic_img.height) / 2), Colors.WHITE)
|
||||
|
||||
def _draw_network_indicator(self, rect: rl.Rectangle):
|
||||
# Signal strength dots
|
||||
x_start = rect.x + 58
|
||||
@@ -176,7 +197,7 @@ class Sidebar(Widget):
|
||||
# Network type text
|
||||
text_y = rect.y + 247
|
||||
text_pos = rl.Vector2(rect.x + 58, text_y)
|
||||
rl.draw_text_ex(self._font_regular, self._net_type, text_pos, FONT_SIZE, 0, Colors.WHITE)
|
||||
rl.draw_text_ex(self._font_regular, tr(self._net_type), text_pos, FONT_SIZE, 0, Colors.WHITE)
|
||||
|
||||
def _draw_metrics(self, rect: rl.Rectangle):
|
||||
metrics = [(self._temp_status, 338), (self._panda_status, 496), (self._connect_status, 654)]
|
||||
@@ -189,15 +210,15 @@ class Sidebar(Widget):
|
||||
# Draw colored left edge (clipped rounded rectangle)
|
||||
edge_rect = rl.Rectangle(metric_rect.x + 4, metric_rect.y + 4, 100, 118)
|
||||
rl.begin_scissor_mode(int(metric_rect.x + 4), int(metric_rect.y), 18, int(metric_rect.height))
|
||||
rl.draw_rectangle_rounded(edge_rect, 0.18, 10, metric.color)
|
||||
rl.draw_rectangle_rounded(edge_rect, 0.3, 10, metric.color)
|
||||
rl.end_scissor_mode()
|
||||
|
||||
# Draw border
|
||||
rl.draw_rectangle_rounded_lines_ex(metric_rect, 0.15, 10, 2, Colors.METRIC_BORDER)
|
||||
rl.draw_rectangle_rounded_lines_ex(metric_rect, 0.3, 10, 2, Colors.METRIC_BORDER)
|
||||
|
||||
# Draw label and value
|
||||
labels = [metric.label, metric.value]
|
||||
text_y = metric_rect.y + (metric_rect.height / 2 - len(labels) * FONT_SIZE)
|
||||
labels = [tr(metric.label), tr(metric.value)]
|
||||
text_y = metric_rect.y + (metric_rect.height / 2 - len(labels) * FONT_SIZE * FONT_SCALE)
|
||||
for text in labels:
|
||||
text_size = measure_text_cached(self._font_bold, text, FONT_SIZE)
|
||||
text_y += text_size.y
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import time
|
||||
from functools import lru_cache
|
||||
from openpilot.common.api import Api
|
||||
from openpilot.common.time_helpers import system_time_valid
|
||||
|
||||
TOKEN_EXPIRY_HOURS = 2
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _get_token(dongle_id: str, t: int):
|
||||
if not system_time_valid():
|
||||
raise RuntimeError("System time is not valid, cannot generate token")
|
||||
|
||||
return Api(dongle_id).get_token(expiry_hours=TOKEN_EXPIRY_HOURS)
|
||||
|
||||
|
||||
|
||||
@@ -11,14 +11,14 @@ from openpilot.selfdrive.ui.lib.api_helpers import get_token
|
||||
|
||||
|
||||
class PrimeType(IntEnum):
|
||||
UNKNOWN = -2,
|
||||
UNPAIRED = -1,
|
||||
NONE = 0,
|
||||
MAGENTA = 1,
|
||||
LITE = 2,
|
||||
BLUE = 3,
|
||||
MAGENTA_NEW = 4,
|
||||
PURPLE = 5,
|
||||
UNKNOWN = -2
|
||||
UNPAIRED = -1
|
||||
NONE = 0
|
||||
MAGENTA = 1
|
||||
LITE = 2
|
||||
BLUE = 3
|
||||
MAGENTA_NEW = 4
|
||||
PURPLE = 5
|
||||
|
||||
|
||||
class PrimeState:
|
||||
@@ -33,7 +33,6 @@ class PrimeState:
|
||||
|
||||
self._running = False
|
||||
self._thread = None
|
||||
self.start()
|
||||
|
||||
def _load_initial_state(self) -> PrimeType:
|
||||
prime_type_str = os.getenv("PRIME_TYPE") or self._params.get("PrimeType")
|
||||
@@ -96,5 +95,9 @@ class PrimeState:
|
||||
with self._lock:
|
||||
return bool(self.prime_type > PrimeType.NONE)
|
||||
|
||||
def is_paired(self) -> bool:
|
||||
with self._lock:
|
||||
return self.prime_type > PrimeType.UNPAIRED
|
||||
|
||||
def __del__(self):
|
||||
self.stop()
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
#include <sys/resource.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QTranslator>
|
||||
|
||||
#include "system/hardware/hw.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
#include "selfdrive/ui/qt/window.h"
|
||||
|
||||
#ifdef SUNNYPILOT
|
||||
#include "selfdrive/ui/sunnypilot/qt/window.h"
|
||||
#define MainWindow MainWindowSP
|
||||
#else
|
||||
#include "selfdrive/ui/qt/qt_window.h"
|
||||
#endif
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
setpriority(PRIO_PROCESS, 0, -20);
|
||||
|
||||
qInstallMessageHandler(swagLogMessageHandler);
|
||||
initApp(argc, argv);
|
||||
|
||||
QTranslator translator;
|
||||
QString translation_file = QString::fromStdString(Params().get("LanguageSetting"));
|
||||
if (!translator.load(QString(":/%1").arg(translation_file)) && translation_file.length()) {
|
||||
qCritical() << "Failed to load translation file:" << translation_file;
|
||||
}
|
||||
|
||||
QApplication a(argc, argv);
|
||||
a.installTranslator(&translator);
|
||||
|
||||
MainWindow w;
|
||||
setMainWindow(&w);
|
||||
a.installEventFilter(&w);
|
||||
return a.exec();
|
||||
}
|
||||
@@ -4,10 +4,11 @@ from dataclasses import dataclass
|
||||
from cereal import messaging, log
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.hardware import TICI
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_FPS
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import gui_text_box
|
||||
from openpilot.system.ui.widgets.label import Label
|
||||
|
||||
AlertSize = log.SelfdriveState.AlertSize
|
||||
AlertStatus = log.SelfdriveState.AlertStatus
|
||||
@@ -21,14 +22,19 @@ ALERT_FONT_SMALL = 66
|
||||
ALERT_FONT_MEDIUM = 74
|
||||
ALERT_FONT_BIG = 88
|
||||
|
||||
ALERT_HEIGHTS = {
|
||||
AlertSize.small: 271,
|
||||
AlertSize.mid: 420,
|
||||
}
|
||||
|
||||
SELFDRIVE_STATE_TIMEOUT = 5 # Seconds
|
||||
SELFDRIVE_UNRESPONSIVE_TIMEOUT = 10 # Seconds
|
||||
|
||||
# Constants
|
||||
ALERT_COLORS = {
|
||||
AlertStatus.normal: rl.Color(0, 0, 0, 235), # Black
|
||||
AlertStatus.userPrompt: rl.Color(0xFE, 0x8C, 0x34, 235), # Orange
|
||||
AlertStatus.critical: rl.Color(0xC9, 0x22, 0x31, 235), # Red
|
||||
AlertStatus.normal: rl.Color(0x15, 0x15, 0x15, 0xF1), # #151515 with alpha 0xF1
|
||||
AlertStatus.userPrompt: rl.Color(0xDA, 0x6F, 0x25, 0xF1), # #DA6F25 with alpha 0xF1
|
||||
AlertStatus.critical: rl.Color(0xC9, 0x22, 0x31, 0xF1), # #C92231 with alpha 0xF1
|
||||
}
|
||||
|
||||
|
||||
@@ -42,24 +48,24 @@ class Alert:
|
||||
|
||||
# Pre-defined alert instances
|
||||
ALERT_STARTUP_PENDING = Alert(
|
||||
text1="openpilot Unavailable",
|
||||
text2="Waiting to start",
|
||||
text1=tr("openpilot Unavailable"),
|
||||
text2=tr("Waiting to start"),
|
||||
size=AlertSize.mid,
|
||||
status=AlertStatus.normal,
|
||||
)
|
||||
|
||||
ALERT_CRITICAL_TIMEOUT = Alert(
|
||||
text1="TAKE CONTROL IMMEDIATELY",
|
||||
text2="System Unresponsive",
|
||||
text1=tr("TAKE CONTROL IMMEDIATELY"),
|
||||
text2=tr("System Unresponsive"),
|
||||
size=AlertSize.full,
|
||||
status=AlertStatus.critical,
|
||||
)
|
||||
|
||||
ALERT_CRITICAL_REBOOT = Alert(
|
||||
text1="System Unresponsive",
|
||||
text2="Reboot Device",
|
||||
size=AlertSize.full,
|
||||
status=AlertStatus.critical,
|
||||
text1=tr("System Unresponsive"),
|
||||
text2=tr("Reboot Device"),
|
||||
size=AlertSize.mid,
|
||||
status=AlertStatus.normal,
|
||||
)
|
||||
|
||||
|
||||
@@ -69,14 +75,20 @@ class AlertRenderer(Widget):
|
||||
self.font_regular: rl.Font = gui_app.font(FontWeight.NORMAL)
|
||||
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||
|
||||
# font size is set dynamically
|
||||
self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||
self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||
|
||||
def get_alert(self, sm: messaging.SubMaster) -> Alert | None:
|
||||
"""Generate the current alert based on selfdrive state."""
|
||||
ss = sm['selfdriveState']
|
||||
|
||||
# Check if selfdriveState messages have stopped arriving
|
||||
recv_frame = sm.recv_frame['selfdriveState']
|
||||
if not sm.updated['selfdriveState']:
|
||||
recv_frame = sm.recv_frame['selfdriveState']
|
||||
time_since_onroad = (sm.frame - ui_state.started_frame) / DEFAULT_FPS
|
||||
time_since_onroad = time.monotonic() - ui_state.started_time
|
||||
|
||||
# 1. Never received selfdriveState since going onroad
|
||||
waiting_for_startup = recv_frame < ui_state.started_frame
|
||||
@@ -95,13 +107,17 @@ class AlertRenderer(Widget):
|
||||
if ss.alertSize == 0:
|
||||
return None
|
||||
|
||||
# Don't get old alert
|
||||
if recv_frame < ui_state.started_frame:
|
||||
return None
|
||||
|
||||
# Return current alert
|
||||
return Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize.raw, status=ss.alertStatus.raw)
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> bool:
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
alert = self.get_alert(ui_state.sm)
|
||||
if not alert:
|
||||
return False
|
||||
return
|
||||
|
||||
alert_rect = self._get_alert_rect(rect, alert.size)
|
||||
self._draw_background(alert_rect, alert)
|
||||
@@ -113,21 +129,14 @@ class AlertRenderer(Widget):
|
||||
alert_rect.height - 2 * ALERT_PADDING
|
||||
)
|
||||
self._draw_text(text_rect, alert)
|
||||
return True
|
||||
|
||||
def _get_alert_rect(self, rect: rl.Rectangle, size: int) -> rl.Rectangle:
|
||||
if size == AlertSize.full:
|
||||
return rect
|
||||
|
||||
height = (ALERT_FONT_MEDIUM + 2 * ALERT_PADDING if size == AlertSize.small else
|
||||
ALERT_FONT_BIG + ALERT_LINE_SPACING + ALERT_FONT_SMALL + 2 * ALERT_PADDING)
|
||||
|
||||
return rl.Rectangle(
|
||||
rect.x + ALERT_MARGIN,
|
||||
rect.y + rect.height - ALERT_MARGIN - height,
|
||||
rect.width - 2 * ALERT_MARGIN,
|
||||
height
|
||||
)
|
||||
h = ALERT_HEIGHTS.get(size, rect.height)
|
||||
return rl.Rectangle(rect.x + ALERT_MARGIN, rect.y + rect.height - h + ALERT_MARGIN,
|
||||
rect.width - ALERT_MARGIN * 2, h - ALERT_MARGIN * 2)
|
||||
|
||||
def _draw_background(self, rect: rl.Rectangle, alert: Alert) -> None:
|
||||
color = ALERT_COLORS.get(alert.status, ALERT_COLORS[AlertStatus.normal])
|
||||
@@ -150,13 +159,17 @@ class AlertRenderer(Widget):
|
||||
else:
|
||||
is_long = len(alert.text1) > 15
|
||||
font_size1 = 132 if is_long else 177
|
||||
align_ment = rl.GuiTextAlignment.TEXT_ALIGN_CENTER
|
||||
vertical_align = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE
|
||||
text_rect = rl.Rectangle(rect.x, rect.y, rect.width, rect.height // 2)
|
||||
|
||||
gui_text_box(text_rect, alert.text1, font_size1, alignment=align_ment, alignment_vertical=vertical_align, font_weight=FontWeight.BOLD)
|
||||
text_rect.y = rect.y + rect.height // 2
|
||||
gui_text_box(text_rect, alert.text2, ALERT_FONT_BIG, alignment=align_ment)
|
||||
top_offset = 200 if is_long or '\n' in alert.text1 else 270
|
||||
title_rect = rl.Rectangle(rect.x, rect.y + top_offset, rect.width, 600)
|
||||
self._full_text1_label.set_font_size(font_size1)
|
||||
self._full_text1_label.set_text(alert.text1)
|
||||
self._full_text1_label.render(title_rect)
|
||||
|
||||
bottom_offset = 361 if is_long else 420
|
||||
subtitle_rect = rl.Rectangle(rect.x, rect.y + rect.height - bottom_offset, rect.width, 300)
|
||||
self._full_text2_label.set_text(alert.text2)
|
||||
self._full_text2_label.render(subtitle_rect)
|
||||
|
||||
def _draw_centered(self, text, rect, font, font_size, center_y=True, color=rl.WHITE) -> None:
|
||||
text_size = measure_text_cached(font, text, font_size)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import time
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
from cereal import log
|
||||
from cereal import log, messaging
|
||||
from msgq.visionipc import VisionStreamType
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus, UI_BORDER_SIZE
|
||||
from openpilot.selfdrive.ui import UI_BORDER_SIZE
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from openpilot.selfdrive.ui.onroad.alert_renderer import AlertRenderer
|
||||
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
|
||||
from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer
|
||||
@@ -20,13 +21,14 @@ WIDE_CAM = VisionStreamType.VISION_STREAM_WIDE_ROAD
|
||||
DEFAULT_DEVICE_CAMERA = DEVICE_CAMERAS["tici", "ar0231"]
|
||||
|
||||
BORDER_COLORS = {
|
||||
UIStatus.DISENGAGED: rl.Color(0x17, 0x33, 0x49, 0xC8), # Blue for disengaged state
|
||||
UIStatus.OVERRIDE: rl.Color(0x91, 0x9B, 0x95, 0xF1), # Gray for override state
|
||||
UIStatus.ENGAGED: rl.Color(0x17, 0x86, 0x44, 0xF1), # Green for engaged state
|
||||
UIStatus.DISENGAGED: rl.Color(0x12, 0x28, 0x39, 0xFF), # Blue for disengaged state
|
||||
UIStatus.OVERRIDE: rl.Color(0x89, 0x92, 0x8D, 0xFF), # Gray for override state
|
||||
UIStatus.ENGAGED: rl.Color(0x16, 0x7F, 0x40, 0xFF), # Green for engaged state
|
||||
}
|
||||
|
||||
WIDE_CAM_MAX_SPEED = 10.0 # m/s (22 mph)
|
||||
ROAD_CAM_MIN_SPEED = 15.0 # m/s (34 mph)
|
||||
INF_POINT = np.array([1000.0, 0.0, 0.0])
|
||||
|
||||
|
||||
class AugmentedRoadView(CameraView):
|
||||
@@ -38,9 +40,7 @@ class AugmentedRoadView(CameraView):
|
||||
self.view_from_calib = view_frame_from_device_frame.copy()
|
||||
self.view_from_wide_calib = view_frame_from_device_frame.copy()
|
||||
|
||||
self._last_calib_time: float = 0
|
||||
self._last_rect_dims = (0.0, 0.0)
|
||||
self._last_stream_type = stream_type
|
||||
self._matrix_cache_key = (0, 0.0, 0.0, stream_type)
|
||||
self._cached_matrix: np.ndarray | None = None
|
||||
self._content_rect = rl.Rectangle()
|
||||
|
||||
@@ -49,14 +49,12 @@ class AugmentedRoadView(CameraView):
|
||||
self.alert_renderer = AlertRenderer()
|
||||
self.driver_state_renderer = DriverStateRenderer()
|
||||
|
||||
# Callbacks
|
||||
self._click_callback: Callable | None = None
|
||||
|
||||
def set_callbacks(self, on_click: Callable | None = None):
|
||||
self._click_callback = on_click
|
||||
# debug
|
||||
self._pm = messaging.PubMaster(['uiDebug'])
|
||||
|
||||
def _render(self, rect):
|
||||
# Only render when system is started to avoid invalid data access
|
||||
start_draw = time.monotonic()
|
||||
if not ui_state.started:
|
||||
return
|
||||
|
||||
@@ -73,9 +71,6 @@ class AugmentedRoadView(CameraView):
|
||||
rect.height - 2 * UI_BORDER_SIZE,
|
||||
)
|
||||
|
||||
# Draw colored border based on driving state
|
||||
self._draw_border(rect)
|
||||
|
||||
# Enable scissor mode to clip all rendering within content rectangle boundaries
|
||||
# This creates a rendering viewport that prevents graphics from drawing outside the border
|
||||
rl.begin_scissor_mode(
|
||||
@@ -91,8 +86,8 @@ class AugmentedRoadView(CameraView):
|
||||
# Draw all UI overlays
|
||||
self.model_renderer.render(self._content_rect)
|
||||
self._hud_renderer.render(self._content_rect)
|
||||
if not self.alert_renderer.render(self._content_rect):
|
||||
self.driver_state_renderer.render(self._content_rect)
|
||||
self.alert_renderer.render(self._content_rect)
|
||||
self.driver_state_renderer.render(self._content_rect)
|
||||
|
||||
# Custom UI extension point - add custom overlays here
|
||||
# Use self._content_rect for positioning within camera bounds
|
||||
@@ -100,15 +95,29 @@ class AugmentedRoadView(CameraView):
|
||||
# End clipping region
|
||||
rl.end_scissor_mode()
|
||||
|
||||
# Handle click events if no HUD interaction occurred
|
||||
if not self._hud_renderer.handle_mouse_event():
|
||||
if self._click_callback and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||
if rl.check_collision_point_rec(rl.get_mouse_position(), self._content_rect):
|
||||
self._click_callback()
|
||||
# Draw colored border based on driving state
|
||||
self._draw_border(rect)
|
||||
|
||||
# publish uiDebug
|
||||
msg = messaging.new_message('uiDebug')
|
||||
msg.uiDebug.drawTimeMillis = (time.monotonic() - start_draw) * 1000
|
||||
self._pm.send('uiDebug', msg)
|
||||
|
||||
def _handle_mouse_press(self, _):
|
||||
if not self._hud_renderer.user_interacting() and self._click_callback is not None:
|
||||
self._click_callback()
|
||||
|
||||
def _handle_mouse_release(self, _):
|
||||
# We only call click callback on press if not interacting with HUD
|
||||
pass
|
||||
|
||||
def _draw_border(self, rect: rl.Rectangle):
|
||||
rl.draw_rectangle_lines_ex(rect, UI_BORDER_SIZE, rl.BLACK)
|
||||
border_roundness = 0.12
|
||||
border_color = BORDER_COLORS.get(ui_state.status, BORDER_COLORS[UIStatus.DISENGAGED])
|
||||
rl.draw_rectangle_lines_ex(rect, UI_BORDER_SIZE, border_color)
|
||||
border_rect = rl.Rectangle(rect.x + UI_BORDER_SIZE, rect.y + UI_BORDER_SIZE,
|
||||
rect.width - 2 * UI_BORDER_SIZE, rect.height - 2 * UI_BORDER_SIZE)
|
||||
rl.draw_rectangle_rounded_lines_ex(border_rect, border_roundness, 10, UI_BORDER_SIZE, border_color)
|
||||
|
||||
def _switch_stream_if_needed(self, sm):
|
||||
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
|
||||
@@ -151,12 +160,13 @@ class AugmentedRoadView(CameraView):
|
||||
|
||||
def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray:
|
||||
# Check if we can use cached matrix
|
||||
calib_time = ui_state.sm.recv_frame['liveCalibration']
|
||||
current_dims = (self._content_rect.width, self._content_rect.height)
|
||||
if (self._last_calib_time == calib_time and
|
||||
self._last_rect_dims == current_dims and
|
||||
self._last_stream_type == self.stream_type and
|
||||
self._cached_matrix is not None):
|
||||
cache_key = (
|
||||
ui_state.sm.recv_frame['liveCalibration'],
|
||||
self._content_rect.width,
|
||||
self._content_rect.height,
|
||||
self.stream_type
|
||||
)
|
||||
if cache_key == self._matrix_cache_key and self._cached_matrix is not None:
|
||||
return self._cached_matrix
|
||||
|
||||
# Get camera configuration
|
||||
@@ -167,9 +177,8 @@ class AugmentedRoadView(CameraView):
|
||||
zoom = 2.0 if is_wide_camera else 1.1
|
||||
|
||||
# Calculate transforms for vanishing point
|
||||
inf_point = np.array([1000.0, 0.0, 0.0])
|
||||
calib_transform = intrinsic @ calibration
|
||||
kep = calib_transform @ inf_point
|
||||
kep = calib_transform @ INF_POINT
|
||||
|
||||
# Calculate center points and dimensions
|
||||
x, y = self._content_rect.x, self._content_rect.y
|
||||
@@ -192,9 +201,7 @@ class AugmentedRoadView(CameraView):
|
||||
x_offset, y_offset = 0, 0
|
||||
|
||||
# Cache the computed transformation matrix to avoid recalculations
|
||||
self._last_calib_time = calib_time
|
||||
self._last_rect_dims = current_dims
|
||||
self._last_stream_type = self.stream_type
|
||||
self._matrix_cache_key = cache_key
|
||||
self._cached_matrix = np.array([
|
||||
[zoom * 2 * cx / w, 0, -x_offset / w * 2],
|
||||
[0, zoom * 2 * cy / h, -y_offset / h * 2],
|
||||
|
||||
@@ -8,6 +8,7 @@ from openpilot.system.hardware import TICI
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
CONNECTION_RETRY_INTERVAL = 0.2 # seconds between connection attempts
|
||||
|
||||
@@ -67,6 +68,7 @@ else:
|
||||
class CameraView(Widget):
|
||||
def __init__(self, name: str, stream_type: VisionStreamType):
|
||||
super().__init__()
|
||||
# TODO: implement a receiver and connect thread
|
||||
self._name = name
|
||||
# Primary stream
|
||||
self.client = VisionIpcClient(name, stream_type, conflate=True)
|
||||
@@ -103,6 +105,20 @@ class CameraView(Widget):
|
||||
self.egl_texture = rl.load_texture_from_image(temp_image)
|
||||
rl.unload_image(temp_image)
|
||||
|
||||
ui_state.add_offroad_transition_callback(self._offroad_transition)
|
||||
|
||||
def _offroad_transition(self):
|
||||
# Reconnect if not first time going onroad
|
||||
if ui_state.is_onroad() and self.frame is not None:
|
||||
# Prevent old frames from showing when going onroad. Qt has a separate thread
|
||||
# which drains the VisionIpcClient SubSocket for us. Re-connecting is not enough
|
||||
# and only clears internal buffers, not the message queue.
|
||||
self.frame = None
|
||||
self.available_streams.clear()
|
||||
if self.client:
|
||||
del self.client
|
||||
self.client = VisionIpcClient(self._name, self._stream_type, conflate=True)
|
||||
|
||||
def _set_placeholder_color(self, color: rl.Color):
|
||||
"""Set a placeholder color to be drawn when no frame is available."""
|
||||
self._placeholder_color = color
|
||||
@@ -139,6 +155,8 @@ class CameraView(Widget):
|
||||
if self.shader and self.shader.id:
|
||||
rl.unload_shader(self.shader)
|
||||
|
||||
self.frame = None
|
||||
self.available_streams.clear()
|
||||
self.client = None
|
||||
|
||||
def __del__(self):
|
||||
@@ -175,6 +193,9 @@ class CameraView(Widget):
|
||||
if buffer:
|
||||
self._texture_needs_update = True
|
||||
self.frame = buffer
|
||||
elif not self.client.is_connected():
|
||||
# ensure we clear the displayed frame when the connection is lost
|
||||
self.frame = None
|
||||
|
||||
if not self.frame:
|
||||
self._draw_placeholder(rect)
|
||||
|
||||
@@ -3,8 +3,9 @@ import pyray as rl
|
||||
from msgq.visionipc import VisionStreamType
|
||||
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
|
||||
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
|
||||
|
||||
@@ -12,17 +13,25 @@ class DriverCameraDialog(CameraView):
|
||||
def __init__(self):
|
||||
super().__init__("camerad", VisionStreamType.VISION_STREAM_DRIVER)
|
||||
self.driver_state_renderer = DriverStateRenderer()
|
||||
# TODO: this can grow unbounded, should be given some thought
|
||||
device.add_interactive_timeout_callback(self.stop_dmonitoringmodeld)
|
||||
ui_state.params.put_bool("IsDriverViewEnabled", True)
|
||||
|
||||
def stop_dmonitoringmodeld(self):
|
||||
ui_state.params.put_bool("IsDriverViewEnabled", False)
|
||||
gui_app.set_modal_overlay(None)
|
||||
|
||||
def _handle_mouse_release(self, _):
|
||||
super()._handle_mouse_release(_)
|
||||
self.stop_dmonitoringmodeld()
|
||||
|
||||
def _render(self, rect):
|
||||
super()._render(rect)
|
||||
|
||||
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||
return 1
|
||||
|
||||
if not self.frame:
|
||||
gui_label(
|
||||
rect,
|
||||
"camera starting",
|
||||
tr("camera starting"),
|
||||
font_size=100,
|
||||
font_weight=FontWeight.BOLD,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
from cereal import log
|
||||
from dataclasses import dataclass
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UI_BORDER_SIZE
|
||||
from openpilot.selfdrive.ui import UI_BORDER_SIZE
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
AlertSize = log.SelfdriveState.AlertSize
|
||||
|
||||
# Default 3D coordinates for face keypoints as a NumPy array
|
||||
DEFAULT_FACE_KPTS_3D = np.array([
|
||||
[-5.98, -51.20, 8.00], [-17.64, -49.14, 8.00], [-23.81, -46.40, 8.00], [-29.98, -40.91, 8.00],
|
||||
@@ -50,7 +54,6 @@ class DriverStateRenderer(Widget):
|
||||
self.is_active = False
|
||||
self.is_rhd = False
|
||||
self.dm_fade_state = 0.0
|
||||
self.last_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
|
||||
self.driver_pose_vals = np.zeros(3, dtype=np.float32)
|
||||
self.driver_pose_diff = np.zeros(3, dtype=np.float32)
|
||||
self.driver_pose_sins = np.zeros(3, dtype=np.float32)
|
||||
@@ -75,8 +78,8 @@ class DriverStateRenderer(Widget):
|
||||
self.engaged_color = rl.Color(26, 242, 66, 255)
|
||||
self.disengaged_color = rl.Color(139, 139, 139, 255)
|
||||
|
||||
self.set_visible(lambda: (ui_state.sm.recv_frame['driverStateV2'] > ui_state.started_frame and
|
||||
ui_state.sm.seen['driverMonitoringState']))
|
||||
self.set_visible(lambda: (ui_state.sm["selfdriveState"].alertSize == AlertSize.none and
|
||||
ui_state.sm.recv_frame["driverStateV2"] > ui_state.started_frame))
|
||||
|
||||
def _render(self, rect):
|
||||
# Set opacity based on active state
|
||||
@@ -106,11 +109,7 @@ class DriverStateRenderer(Widget):
|
||||
def _update_state(self):
|
||||
"""Update the driver monitoring state based on model data"""
|
||||
sm = ui_state.sm
|
||||
if not sm.updated["driverMonitoringState"]:
|
||||
if (self._rect.x != self.last_rect.x or self._rect.y != self.last_rect.y or
|
||||
self._rect.width != self.last_rect.width or self._rect.height != self.last_rect.height):
|
||||
self._pre_calculate_drawing_elements()
|
||||
self.last_rect = self._rect
|
||||
if not self.is_visible:
|
||||
return
|
||||
|
||||
# Get monitoring state
|
||||
@@ -222,7 +221,7 @@ class DriverStateRenderer(Widget):
|
||||
radius_y = arc_data.height / 2
|
||||
|
||||
x_coords = center_x + np.cos(angles) * radius_x
|
||||
y_coords = center_y + np.sin(angles) * radius_y
|
||||
y_coords = center_y - np.sin(angles) * radius_y
|
||||
|
||||
arc_lines = self.h_arc_lines if is_horizontal else self.v_arc_lines
|
||||
for i, (x_coord, y_coord) in enumerate(zip(x_coords, y_coords, strict=True)):
|
||||
|
||||
@@ -32,26 +32,21 @@ class ExpButton(Widget):
|
||||
self._experimental_mode = selfdrive_state.experimentalMode
|
||||
self._engageable = selfdrive_state.engageable or selfdrive_state.enabled
|
||||
|
||||
def handle_mouse_event(self) -> bool:
|
||||
if rl.check_collision_point_rec(rl.get_mouse_position(), self._rect):
|
||||
if (rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and
|
||||
self._is_toggle_allowed()):
|
||||
new_mode = not self._experimental_mode
|
||||
self._params.put_bool("ExperimentalMode", new_mode)
|
||||
def _handle_mouse_release(self, _):
|
||||
super()._handle_mouse_release(_)
|
||||
if self._is_toggle_allowed():
|
||||
new_mode = not self._experimental_mode
|
||||
self._params.put_bool("ExperimentalMode", new_mode)
|
||||
|
||||
# Hold new state temporarily
|
||||
self._held_mode = new_mode
|
||||
self._hold_end_time = time.monotonic() + self._hold_duration
|
||||
return True
|
||||
return False
|
||||
# Hold new state temporarily
|
||||
self._held_mode = new_mode
|
||||
self._hold_end_time = time.monotonic() + self._hold_duration
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> None:
|
||||
center_x = int(self._rect.x + self._rect.width // 2)
|
||||
center_y = int(self._rect.y + self._rect.height // 2)
|
||||
|
||||
mouse_over = rl.check_collision_point_rec(rl.get_mouse_position(), self._rect)
|
||||
mouse_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self.is_pressed
|
||||
self._white_color.a = 180 if (mouse_down and mouse_over) or not self._engageable else 255
|
||||
self._white_color.a = 180 if self.is_pressed or not self._engageable else 255
|
||||
|
||||
texture = self._txt_exp if self._held_or_actual_mode() else self._txt_wheel
|
||||
rl.draw_circle(center_x, center_y, self._rect.width / 2, self._black_bg)
|
||||
@@ -71,8 +66,5 @@ class ExpButton(Widget):
|
||||
if not self._params.get_bool("ExperimentalModeConfirmed"):
|
||||
return False
|
||||
|
||||
car_params = ui_state.sm["carParams"]
|
||||
if car_params.alphaLongitudinalAvailable:
|
||||
return self._params.get_bool("AlphaLongitudinalEnabled")
|
||||
else:
|
||||
return car_params.openpilotLongitudinalControl
|
||||
# Mirror exp mode toggle using persistent car params
|
||||
return ui_state.has_longitudinal_control
|
||||
|
||||
@@ -4,6 +4,7 @@ from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.ui.onroad.exp_button import ExpButton
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
@@ -60,7 +61,7 @@ class HudRenderer(Widget):
|
||||
super().__init__()
|
||||
"""Initialize the HUD renderer."""
|
||||
self.is_cruise_set: bool = False
|
||||
self.is_cruise_available: bool = False
|
||||
self.is_cruise_available: bool = True
|
||||
self.set_speed: float = SET_SPEED_NA
|
||||
self.speed: float = 0.0
|
||||
self.v_ego_cluster_seen: bool = False
|
||||
@@ -69,7 +70,7 @@ class HudRenderer(Widget):
|
||||
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
self._exp_button = ExpButton(UI_CONFIG.button_size, UI_CONFIG.wheel_icon_size)
|
||||
self._exp_button: ExpButton = ExpButton(UI_CONFIG.button_size, UI_CONFIG.wheel_icon_size)
|
||||
|
||||
def _update_state(self) -> None:
|
||||
"""Update HUD state based on car state and controls state."""
|
||||
@@ -120,8 +121,8 @@ class HudRenderer(Widget):
|
||||
button_y = rect.y + UI_CONFIG.border_size
|
||||
self._exp_button.render(rl.Rectangle(button_x, button_y, UI_CONFIG.button_size, UI_CONFIG.button_size))
|
||||
|
||||
def handle_mouse_event(self) -> bool:
|
||||
return bool(self._exp_button.handle_mouse_event())
|
||||
def user_interacting(self) -> bool:
|
||||
return self._exp_button.is_pressed
|
||||
|
||||
def _draw_set_speed(self, rect: rl.Rectangle) -> None:
|
||||
"""Draw the MAX speed indicator box."""
|
||||
@@ -130,8 +131,8 @@ class HudRenderer(Widget):
|
||||
y = rect.y + 45
|
||||
|
||||
set_speed_rect = rl.Rectangle(x, y, set_speed_width, UI_CONFIG.set_speed_height)
|
||||
rl.draw_rectangle_rounded(set_speed_rect, 0.2, 30, COLORS.black_translucent)
|
||||
rl.draw_rectangle_rounded_lines_ex(set_speed_rect, 0.2, 30, 6, COLORS.border_translucent)
|
||||
rl.draw_rectangle_rounded(set_speed_rect, 0.35, 10, COLORS.black_translucent)
|
||||
rl.draw_rectangle_rounded_lines_ex(set_speed_rect, 0.35, 10, 6, COLORS.border_translucent)
|
||||
|
||||
max_color = COLORS.grey
|
||||
set_speed_color = COLORS.dark_grey
|
||||
@@ -144,7 +145,7 @@ class HudRenderer(Widget):
|
||||
elif ui_state.status == UIStatus.OVERRIDE:
|
||||
max_color = COLORS.override
|
||||
|
||||
max_text = "MAX"
|
||||
max_text = tr("MAX")
|
||||
max_text_width = measure_text_cached(self._font_semi_bold, max_text, FONT_SIZES.max_speed).x
|
||||
rl.draw_text_ex(
|
||||
self._font_semi_bold,
|
||||
@@ -173,7 +174,7 @@ class HudRenderer(Widget):
|
||||
speed_pos = rl.Vector2(rect.x + rect.width / 2 - speed_text_size.x / 2, 180 - speed_text_size.y / 2)
|
||||
rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.white)
|
||||
|
||||
unit_text = "km/h" if ui_state.is_metric else "mph"
|
||||
unit_text = tr("km/h") if ui_state.is_metric else tr("mph")
|
||||
unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit)
|
||||
unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2)
|
||||
rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.white_translucent)
|
||||
|
||||
@@ -3,20 +3,17 @@ import numpy as np
|
||||
import pyray as rl
|
||||
from cereal import messaging, car
|
||||
from dataclasses import dataclass, field
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import DEFAULT_FPS
|
||||
from openpilot.system.ui.lib.shader_polygon import draw_polygon
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
CLIP_MARGIN = 500
|
||||
MIN_DRAW_DISTANCE = 10.0
|
||||
MAX_DRAW_DISTANCE = 100.0
|
||||
PATH_COLOR_TRANSITION_DURATION = 0.5 # Seconds for color transition animation
|
||||
PATH_BLEND_INCREMENT = 1.0 / (PATH_COLOR_TRANSITION_DURATION * DEFAULT_FPS)
|
||||
|
||||
MAX_POINTS = 200
|
||||
|
||||
THROTTLE_COLORS = [
|
||||
rl.Color(13, 248, 122, 102), # HSLF(148/360, 0.94, 0.51, 0.4)
|
||||
@@ -49,7 +46,7 @@ class ModelRenderer(Widget):
|
||||
super().__init__()
|
||||
self._longitudinal_control = False
|
||||
self._experimental_mode = False
|
||||
self._blend_factor = 1.0
|
||||
self._blend_filter = FirstOrderFilter(1.0, 0.25, 1 / gui_app.target_fps)
|
||||
self._prev_allow_throttle = True
|
||||
self._lane_line_probs = np.zeros(4, dtype=np.float32)
|
||||
self._road_edge_stds = np.zeros(2, dtype=np.float32)
|
||||
@@ -67,12 +64,12 @@ class ModelRenderer(Widget):
|
||||
self._transform_dirty = True
|
||||
self._clip_region = None
|
||||
|
||||
self._exp_gradient = {
|
||||
'start': (0.0, 1.0), # Bottom of path
|
||||
'end': (0.0, 0.0), # Top of path
|
||||
'colors': [],
|
||||
'stops': [],
|
||||
}
|
||||
self._exp_gradient = Gradient(
|
||||
start=(0.0, 1.0), # Bottom of path
|
||||
end=(0.0, 0.0), # Top of path
|
||||
colors=[],
|
||||
stops=[],
|
||||
)
|
||||
|
||||
# Get longitudinal control setting from car parameters
|
||||
if car_params := Params().get("CarParams"):
|
||||
@@ -170,12 +167,12 @@ class ModelRenderer(Widget):
|
||||
# Update lane lines using raw points
|
||||
for i, lane_line in enumerate(self._lane_lines):
|
||||
lane_line.projected_points = self._map_line_to_polygon(
|
||||
lane_line.raw_points, 0.025 * self._lane_line_probs[i], 0.0, max_idx
|
||||
lane_line.raw_points, 0.025 * self._lane_line_probs[i], 0.0, max_idx, max_distance
|
||||
)
|
||||
|
||||
# Update road edges using raw points
|
||||
for road_edge in self._road_edges:
|
||||
road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, 0.025, 0.0, max_idx)
|
||||
road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, 0.025, 0.0, max_idx, max_distance)
|
||||
|
||||
# Update path using raw points
|
||||
if lead and lead.status:
|
||||
@@ -184,7 +181,7 @@ class ModelRenderer(Widget):
|
||||
|
||||
max_idx = self._get_path_length_idx(path_x_array, max_distance)
|
||||
self._path.projected_points = self._map_line_to_polygon(
|
||||
self._path.raw_points, 0.9, self._path_offset_z, max_idx, allow_invert=False
|
||||
self._path.raw_points, 0.9, self._path_offset_z, max_idx, max_distance, allow_invert=False
|
||||
)
|
||||
|
||||
self._update_experimental_gradient()
|
||||
@@ -227,8 +224,12 @@ class ModelRenderer(Widget):
|
||||
i += 1 + (1 if (i + 2) < max_len else 0)
|
||||
|
||||
# Store the gradient in the path object
|
||||
self._exp_gradient['colors'] = segment_colors
|
||||
self._exp_gradient['stops'] = gradient_stops
|
||||
self._exp_gradient = Gradient(
|
||||
start=(0.0, 1.0), # Bottom of path
|
||||
end=(0.0, 0.0), # Top of path
|
||||
colors=segment_colors,
|
||||
stops=gradient_stops,
|
||||
)
|
||||
|
||||
def _update_lead_vehicle(self, d_rel, v_rel, point, rect):
|
||||
speed_buff, lead_buff = 10.0, 40.0
|
||||
@@ -277,36 +278,25 @@ class ModelRenderer(Widget):
|
||||
if not self._path.projected_points.size:
|
||||
return
|
||||
|
||||
allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control
|
||||
self._blend_filter.update(int(allow_throttle))
|
||||
|
||||
if self._experimental_mode:
|
||||
# Draw with acceleration coloring
|
||||
if len(self._exp_gradient['colors']) > 1:
|
||||
if len(self._exp_gradient.colors) > 1:
|
||||
draw_polygon(self._rect, self._path.projected_points, gradient=self._exp_gradient)
|
||||
else:
|
||||
draw_polygon(self._rect, self._path.projected_points, rl.Color(255, 255, 255, 30))
|
||||
else:
|
||||
# Draw with throttle/no throttle gradient
|
||||
allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control
|
||||
|
||||
# Start transition if throttle state changes
|
||||
if allow_throttle != self._prev_allow_throttle:
|
||||
self._prev_allow_throttle = allow_throttle
|
||||
self._blend_factor = max(1.0 - self._blend_factor, 0.0)
|
||||
|
||||
# Update blend factor
|
||||
if self._blend_factor < 1.0:
|
||||
self._blend_factor = min(self._blend_factor + PATH_BLEND_INCREMENT, 1.0)
|
||||
|
||||
begin_colors = NO_THROTTLE_COLORS if allow_throttle else THROTTLE_COLORS
|
||||
end_colors = THROTTLE_COLORS if allow_throttle else NO_THROTTLE_COLORS
|
||||
|
||||
# Blend colors based on transition
|
||||
blended_colors = self._blend_colors(begin_colors, end_colors, self._blend_factor)
|
||||
gradient = {
|
||||
'start': (0.0, 1.0), # Bottom of path
|
||||
'end': (0.0, 0.0), # Top of path
|
||||
'colors': blended_colors,
|
||||
'stops': [0.0, 0.5, 1.0],
|
||||
}
|
||||
# Blend throttle/no throttle colors based on transition
|
||||
blend_factor = round(self._blend_filter.x * 100) / 100
|
||||
blended_colors = self._blend_colors(NO_THROTTLE_COLORS, THROTTLE_COLORS, blend_factor)
|
||||
gradient = Gradient(
|
||||
start=(0.0, 1.0), # Bottom of path
|
||||
end=(0.0, 0.0), # Top of path
|
||||
colors=blended_colors,
|
||||
stops=[0.0, 0.5, 1.0],
|
||||
)
|
||||
draw_polygon(self._rect, self._path.projected_points, gradient=gradient)
|
||||
|
||||
def _draw_lead_indicator(self):
|
||||
@@ -319,11 +309,11 @@ class ModelRenderer(Widget):
|
||||
rl.draw_triangle_fan(lead.chevron, len(lead.chevron), rl.Color(201, 34, 49, lead.fill_alpha))
|
||||
|
||||
@staticmethod
|
||||
def _get_path_length_idx(pos_x_array: np.ndarray, path_height: float) -> int:
|
||||
"""Get the index corresponding to the given path height"""
|
||||
def _get_path_length_idx(pos_x_array: np.ndarray, path_distance: float) -> int:
|
||||
"""Get the index corresponding to the given path distance"""
|
||||
if len(pos_x_array) == 0:
|
||||
return 0
|
||||
indices = np.where(pos_x_array <= path_height)[0]
|
||||
indices = np.where(pos_x_array <= path_distance)[0]
|
||||
return indices[-1] if indices.size > 0 else 0
|
||||
|
||||
def _map_to_screen(self, in_x, in_y, in_z):
|
||||
@@ -342,13 +332,24 @@ class ModelRenderer(Widget):
|
||||
|
||||
return (x, y)
|
||||
|
||||
def _map_line_to_polygon(self, line: np.ndarray, y_off: float, z_off: float, max_idx: int, allow_invert: bool = True) -> np.ndarray:
|
||||
def _map_line_to_polygon(self, line: np.ndarray, y_off: float, z_off: float, max_idx: int, max_distance: float, allow_invert: bool = True) -> np.ndarray:
|
||||
"""Convert 3D line to 2D polygon for rendering."""
|
||||
if line.shape[0] == 0:
|
||||
return np.empty((0, 2), dtype=np.float32)
|
||||
|
||||
# Slice points and filter non-negative x-coordinates
|
||||
points = line[:max_idx + 1]
|
||||
|
||||
# Interpolate around max_idx so path end is smooth (max_distance is always >= p0.x)
|
||||
if 0 < max_idx < line.shape[0] - 1:
|
||||
p0 = line[max_idx]
|
||||
p1 = line[max_idx + 1]
|
||||
x0, x1 = p0[0], p1[0]
|
||||
interp_y = np.interp(max_distance, [x0, x1], [p0[1], p1[1]])
|
||||
interp_z = np.interp(max_distance, [x0, x1], [p0[2], p1[2]])
|
||||
interp_point = np.array([max_distance, interp_y, interp_z], dtype=points.dtype)
|
||||
points = np.concatenate((points, interp_point[None, :]), axis=0)
|
||||
|
||||
points = points[points[:, 0] >= 0]
|
||||
if points.shape[0] == 0:
|
||||
return np.empty((0, 2), dtype=np.float32)
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
#include "selfdrive/ui/qt/api.h"
|
||||
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/rsa.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QJsonDocument>
|
||||
#include <QNetworkRequest>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "common/util.h"
|
||||
#include "system/hardware/hw.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
|
||||
namespace CommaApi {
|
||||
|
||||
RSA *get_rsa_private_key() {
|
||||
static std::unique_ptr<RSA, decltype(&RSA_free)> rsa_private(nullptr, RSA_free);
|
||||
if (!rsa_private) {
|
||||
FILE *fp = fopen(Path::rsa_file().c_str(), "rb");
|
||||
if (!fp) {
|
||||
qDebug() << "No RSA private key found, please run manager.py or registration.py";
|
||||
return nullptr;
|
||||
}
|
||||
rsa_private.reset(PEM_read_RSAPrivateKey(fp, NULL, NULL, NULL));
|
||||
fclose(fp);
|
||||
}
|
||||
return rsa_private.get();
|
||||
}
|
||||
|
||||
QByteArray rsa_sign(const QByteArray &data) {
|
||||
RSA *rsa_private = get_rsa_private_key();
|
||||
if (!rsa_private) return {};
|
||||
|
||||
QByteArray sig(RSA_size(rsa_private), Qt::Uninitialized);
|
||||
unsigned int sig_len;
|
||||
int ret = RSA_sign(NID_sha256, (unsigned char*)data.data(), data.size(), (unsigned char*)sig.data(), &sig_len, rsa_private);
|
||||
assert(ret == 1);
|
||||
assert(sig.size() == sig_len);
|
||||
return sig;
|
||||
}
|
||||
|
||||
QString create_jwt(const QJsonObject &payloads, int expiry) {
|
||||
QJsonObject header = {{"alg", "RS256"}};
|
||||
|
||||
auto t = QDateTime::currentSecsSinceEpoch();
|
||||
QJsonObject payload = {{"identity", getDongleId().value_or("")}, {"nbf", t}, {"iat", t}, {"exp", t + expiry}};
|
||||
for (auto it = payloads.begin(); it != payloads.end(); ++it) {
|
||||
payload.insert(it.key(), it.value());
|
||||
}
|
||||
|
||||
auto b64_opts = QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals;
|
||||
QString jwt = QJsonDocument(header).toJson(QJsonDocument::Compact).toBase64(b64_opts) + '.' +
|
||||
QJsonDocument(payload).toJson(QJsonDocument::Compact).toBase64(b64_opts);
|
||||
|
||||
auto hash = QCryptographicHash::hash(jwt.toUtf8(), QCryptographicHash::Sha256);
|
||||
return jwt + "." + rsa_sign(hash).toBase64(b64_opts);
|
||||
}
|
||||
|
||||
} // namespace CommaApi
|
||||
|
||||
HttpRequest::HttpRequest(QObject *parent, bool create_jwt, int timeout) : create_jwt(create_jwt), QObject(parent) {
|
||||
networkTimer = new QTimer(this);
|
||||
networkTimer->setSingleShot(true);
|
||||
networkTimer->setInterval(timeout);
|
||||
connect(networkTimer, &QTimer::timeout, this, &HttpRequest::requestTimeout);
|
||||
}
|
||||
|
||||
bool HttpRequest::active() const {
|
||||
return reply != nullptr;
|
||||
}
|
||||
|
||||
bool HttpRequest::timeout() const {
|
||||
return reply && reply->error() == QNetworkReply::OperationCanceledError;
|
||||
}
|
||||
|
||||
QNetworkRequest HttpRequest::prepareRequest(const QString &requestURL) {
|
||||
QNetworkRequest request;
|
||||
QString token;
|
||||
if (create_jwt) {
|
||||
token = GetJwtToken();
|
||||
} else {
|
||||
QString token_json = QString::fromStdString(util::read_file(util::getenv("HOME") + "/.comma/auth.json"));
|
||||
QJsonDocument json_d = QJsonDocument::fromJson(token_json.toUtf8());
|
||||
token = json_d["access_token"].toString();
|
||||
}
|
||||
|
||||
request.setUrl(QUrl(requestURL));
|
||||
request.setRawHeader("User-Agent", GetUserAgent().toUtf8());
|
||||
|
||||
if (!token.isEmpty()) {
|
||||
request.setRawHeader(QByteArray("Authorization"), ("JWT " + token).toUtf8());
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
void HttpRequest::sendRequest(const QString &requestURL, const Method method) {
|
||||
if (active()) {
|
||||
qDebug() << "HttpRequest is active";
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkRequest request = prepareRequest(requestURL);
|
||||
if (method == Method::GET) {
|
||||
reply = nam()->get(request);
|
||||
} else if (method == Method::DELETE) {
|
||||
reply = nam()->deleteResource(request);
|
||||
}
|
||||
|
||||
networkTimer->start();
|
||||
connect(reply, &QNetworkReply::finished, this, &HttpRequest::requestFinished);
|
||||
}
|
||||
|
||||
void HttpRequest::requestTimeout() {
|
||||
reply->abort();
|
||||
}
|
||||
|
||||
void HttpRequest::requestFinished() {
|
||||
networkTimer->stop();
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
emit requestDone(reply->readAll(), true, reply->error());
|
||||
} else {
|
||||
QString error;
|
||||
if (reply->error() == QNetworkReply::OperationCanceledError) {
|
||||
nam()->clearAccessCache();
|
||||
nam()->clearConnectionCache();
|
||||
error = "Request timed out";
|
||||
} else {
|
||||
error = reply->errorString();
|
||||
}
|
||||
emit requestDone(error, false, reply->error());
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
reply = nullptr;
|
||||
}
|
||||
|
||||
QNetworkAccessManager *HttpRequest::nam() {
|
||||
static QNetworkAccessManager *networkAccessManager = new QNetworkAccessManager(qApp);
|
||||
return networkAccessManager;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
|
||||
#include "util.h"
|
||||
#include "common/util.h"
|
||||
|
||||
namespace CommaApi {
|
||||
|
||||
const QString BASE_URL = util::getenv("API_HOST", "https://api.commadotai.com").c_str();
|
||||
QByteArray rsa_sign(const QByteArray &data);
|
||||
QString create_jwt(const QJsonObject &payloads = {}, int expiry = 3600);
|
||||
|
||||
} // namespace CommaApi
|
||||
|
||||
/**
|
||||
* Makes a request to the request endpoint.
|
||||
*/
|
||||
|
||||
class HttpRequest : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum class Method {GET, DELETE, POST, PUT};
|
||||
|
||||
explicit HttpRequest(QObject* parent, bool create_jwt = true, int timeout = 20000);
|
||||
virtual void sendRequest(const QString &requestURL, Method method);
|
||||
void sendRequest(const QString &requestURL) { sendRequest(requestURL, Method::GET);}
|
||||
bool active() const;
|
||||
bool timeout() const;
|
||||
|
||||
signals:
|
||||
void requestDone(const QString &response, bool success, QNetworkReply::NetworkError error);
|
||||
|
||||
protected:
|
||||
QNetworkReply *reply = nullptr;
|
||||
static QNetworkAccessManager *nam();
|
||||
QTimer *networkTimer = nullptr;
|
||||
bool create_jwt;
|
||||
virtual QNetworkRequest prepareRequest(const QString& requestURL);
|
||||
[[nodiscard]] virtual QString GetJwtToken() const { return CommaApi::create_jwt(); }
|
||||
[[nodiscard]] virtual QString GetUserAgent() const { return getUserAgent(); }
|
||||
|
||||
protected slots:
|
||||
void requestTimeout();
|
||||
void requestFinished();
|
||||
};
|
||||
@@ -1,161 +0,0 @@
|
||||
#include "selfdrive/ui/qt/body.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
#include <QPainter>
|
||||
#include <QStackedLayout>
|
||||
|
||||
#include "common/params.h"
|
||||
#include "common/timing.h"
|
||||
|
||||
RecordButton::RecordButton(QWidget *parent) : QPushButton(parent) {
|
||||
setCheckable(true);
|
||||
setChecked(false);
|
||||
setFixedSize(148, 148);
|
||||
|
||||
QObject::connect(this, &QPushButton::toggled, [=]() {
|
||||
setEnabled(false);
|
||||
});
|
||||
}
|
||||
|
||||
void RecordButton::paintEvent(QPaintEvent *event) {
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
QPoint center(width() / 2, height() / 2);
|
||||
|
||||
QColor bg(isChecked() ? "#FFFFFF" : "#737373");
|
||||
QColor accent(isChecked() ? "#FF0000" : "#FFFFFF");
|
||||
if (!isEnabled()) {
|
||||
bg = QColor("#404040");
|
||||
accent = QColor("#FFFFFF");
|
||||
}
|
||||
|
||||
if (isDown()) {
|
||||
accent.setAlphaF(0.7);
|
||||
}
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(bg);
|
||||
p.drawEllipse(center, 74, 74);
|
||||
|
||||
p.setPen(QPen(accent, 6));
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.drawEllipse(center, 42, 42);
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(accent);
|
||||
p.drawEllipse(center, 22, 22);
|
||||
}
|
||||
|
||||
|
||||
BodyWindow::BodyWindow(QWidget *parent) : fuel_filter(1.0, 5., 1. / UI_FREQ), QWidget(parent) {
|
||||
QStackedLayout *layout = new QStackedLayout(this);
|
||||
layout->setStackingMode(QStackedLayout::StackAll);
|
||||
|
||||
QWidget *w = new QWidget;
|
||||
QVBoxLayout *vlayout = new QVBoxLayout(w);
|
||||
vlayout->setMargin(45);
|
||||
layout->addWidget(w);
|
||||
|
||||
// face
|
||||
face = new QLabel();
|
||||
face->setAlignment(Qt::AlignCenter);
|
||||
layout->addWidget(face);
|
||||
awake = new QMovie("../assets/body/awake.gif", {}, this);
|
||||
awake->setCacheMode(QMovie::CacheAll);
|
||||
sleep = new QMovie("../assets/body/sleep.gif", {}, this);
|
||||
sleep->setCacheMode(QMovie::CacheAll);
|
||||
|
||||
// record button
|
||||
btn = new RecordButton(this);
|
||||
vlayout->addWidget(btn, 0, Qt::AlignBottom | Qt::AlignRight);
|
||||
QObject::connect(btn, &QPushButton::clicked, [=](bool checked) {
|
||||
btn->setEnabled(false);
|
||||
Params().putBool("DisableLogging", !checked);
|
||||
last_button = nanos_since_boot();
|
||||
});
|
||||
w->raise();
|
||||
|
||||
QObject::connect(uiState(), &UIState::uiUpdate, this, &BodyWindow::updateState);
|
||||
}
|
||||
|
||||
void BodyWindow::paintEvent(QPaintEvent *event) {
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
p.fillRect(rect(), QColor(0, 0, 0));
|
||||
|
||||
// battery outline + detail
|
||||
p.translate(width() - 136, 16);
|
||||
const QColor gray = QColor("#737373");
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.setPen(QPen(gray, 4, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
|
||||
p.drawRoundedRect(2, 2, 78, 36, 8, 8);
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(gray);
|
||||
p.drawRoundedRect(84, 12, 6, 16, 4, 4);
|
||||
p.drawRect(84, 12, 3, 16);
|
||||
|
||||
// battery level
|
||||
double fuel = std::clamp(fuel_filter.x(), 0.2f, 1.0f);
|
||||
const int m = 5; // manual margin since we can't do an inner border
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(fuel > 0.25 ? QColor("#32D74B") : QColor("#FF453A"));
|
||||
p.drawRoundedRect(2 + m, 2 + m, (78 - 2*m)*fuel, 36 - 2*m, 4, 4);
|
||||
|
||||
// charging status
|
||||
if (charging) {
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(Qt::white);
|
||||
const QPolygonF charger({
|
||||
QPointF(12.31, 0),
|
||||
QPointF(12.31, 16.92),
|
||||
QPointF(18.46, 16.92),
|
||||
QPointF(6.15, 40),
|
||||
QPointF(6.15, 23.08),
|
||||
QPointF(0, 23.08),
|
||||
});
|
||||
p.drawPolygon(charger.translated(98, 0));
|
||||
}
|
||||
}
|
||||
|
||||
void BodyWindow::offroadTransition(bool offroad) {
|
||||
btn->setChecked(true);
|
||||
btn->setEnabled(true);
|
||||
fuel_filter.reset(1.0);
|
||||
}
|
||||
|
||||
void BodyWindow::updateState(const UIState &s) {
|
||||
if (!isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const SubMaster &sm = *(s.sm);
|
||||
auto cs = sm["carState"].getCarState();
|
||||
|
||||
charging = cs.getCharging();
|
||||
fuel_filter.update(cs.getFuelGauge());
|
||||
|
||||
// TODO: use carState.standstill when that's fixed
|
||||
const bool standstill = std::abs(cs.getVEgo()) < 0.01;
|
||||
QMovie *m = standstill ? sleep : awake;
|
||||
if (m != face->movie()) {
|
||||
face->setMovie(m);
|
||||
face->movie()->start();
|
||||
}
|
||||
|
||||
// update record button state
|
||||
if (sm.updated("managerState") && (sm.rcv_time("managerState") - last_button)*1e-9 > 0.5) {
|
||||
for (auto proc : sm["managerState"].getManagerState().getProcesses()) {
|
||||
if (proc.getName() == "loggerd") {
|
||||
btn->setEnabled(true);
|
||||
btn->setChecked(proc.getRunning());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QMovie>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
|
||||
#include "common/util.h"
|
||||
|
||||
#ifdef SUNNYPILOT
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
#define UIState UIStateSP
|
||||
#else
|
||||
#include "selfdrive/ui/ui.h"
|
||||
#endif
|
||||
|
||||
class RecordButton : public QPushButton {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
RecordButton(QWidget* parent = 0);
|
||||
|
||||
private:
|
||||
void paintEvent(QPaintEvent*) override;
|
||||
};
|
||||
|
||||
class BodyWindow : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
BodyWindow(QWidget* parent = 0);
|
||||
|
||||
private:
|
||||
bool charging = false;
|
||||
uint64_t last_button = 0;
|
||||
FirstOrderFilter fuel_filter;
|
||||
QLabel *face;
|
||||
QMovie *awake, *sleep;
|
||||
RecordButton *btn;
|
||||
void paintEvent(QPaintEvent*) override;
|
||||
|
||||
private slots:
|
||||
void updateState(const UIState &s);
|
||||
void offroadTransition(bool onroad);
|
||||
};
|
||||
@@ -1,95 +0,0 @@
|
||||
#include "selfdrive/ui/qt/home.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QMouseEvent>
|
||||
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
|
||||
// HomeWindow: the container for the offroad and onroad UIs
|
||||
|
||||
HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) {
|
||||
QHBoxLayout *main_layout = new QHBoxLayout(this);
|
||||
main_layout->setMargin(0);
|
||||
main_layout->setSpacing(0);
|
||||
|
||||
sidebar = new Sidebar(this);
|
||||
main_layout->addWidget(sidebar);
|
||||
QObject::connect(sidebar, &Sidebar::openSettings, this, &HomeWindow::openSettings);
|
||||
|
||||
slayout = new QStackedLayout();
|
||||
main_layout->addLayout(slayout);
|
||||
|
||||
home = new OffroadHome(this);
|
||||
QObject::connect(home, &OffroadHome::openSettings, this, &HomeWindow::openSettings);
|
||||
slayout->addWidget(home);
|
||||
|
||||
onroad = new OnroadWindow(this);
|
||||
slayout->addWidget(onroad);
|
||||
|
||||
body = new BodyWindow(this);
|
||||
slayout->addWidget(body);
|
||||
|
||||
driver_view = new DriverViewWindow(this);
|
||||
connect(driver_view, &DriverViewWindow::done, [=] {
|
||||
showDriverView(false);
|
||||
});
|
||||
slayout->addWidget(driver_view);
|
||||
setAttribute(Qt::WA_NoSystemBackground);
|
||||
QObject::connect(uiState(), &UIState::uiUpdate, this, &HomeWindow::updateState);
|
||||
QObject::connect(uiState(), &UIState::offroadTransition, this, &HomeWindow::offroadTransition);
|
||||
QObject::connect(uiState(), &UIState::offroadTransition, sidebar, &Sidebar::offroadTransition);
|
||||
}
|
||||
|
||||
void HomeWindow::showSidebar(bool show) {
|
||||
sidebar->setVisible(show);
|
||||
}
|
||||
|
||||
void HomeWindow::updateState(const UIState &s) {
|
||||
const SubMaster &sm = *(s.sm);
|
||||
|
||||
// switch to the generic robot UI
|
||||
if (onroad->isVisible() && !body->isEnabled() && sm["carParams"].getCarParams().getNotCar()) {
|
||||
body->setEnabled(true);
|
||||
slayout->setCurrentWidget(body);
|
||||
}
|
||||
}
|
||||
|
||||
void HomeWindow::offroadTransition(bool offroad) {
|
||||
body->setEnabled(false);
|
||||
sidebar->setVisible(offroad);
|
||||
if (offroad) {
|
||||
slayout->setCurrentWidget(home);
|
||||
} else {
|
||||
slayout->setCurrentWidget(onroad);
|
||||
}
|
||||
}
|
||||
|
||||
void HomeWindow::showDriverView(bool show) {
|
||||
if (show) {
|
||||
emit closeSettings();
|
||||
slayout->setCurrentWidget(driver_view);
|
||||
} else {
|
||||
slayout->setCurrentWidget(home);
|
||||
}
|
||||
sidebar->setVisible(show == false);
|
||||
}
|
||||
|
||||
void HomeWindow::mousePressEvent(QMouseEvent* e) {
|
||||
// Handle sidebar collapsing
|
||||
if ((onroad->isVisible() || body->isVisible()) && (!sidebar->isVisible() || e->x() > sidebar->width())) {
|
||||
sidebar->setVisible(!sidebar->isVisible());
|
||||
}
|
||||
}
|
||||
|
||||
void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) {
|
||||
HomeWindow::mousePressEvent(e);
|
||||
const SubMaster &sm = *(uiState()->sm);
|
||||
if (sm["carParams"].getCarParams().getNotCar()) {
|
||||
if (onroad->isVisible()) {
|
||||
slayout->setCurrentWidget(body);
|
||||
} else if (body->isVisible()) {
|
||||
slayout->setCurrentWidget(onroad);
|
||||
}
|
||||
showSidebar(false);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QFrame>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QStackedLayout>
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
||||
#include "selfdrive/ui/ui.h"
|
||||
#include "selfdrive/ui/qt/offroad/driverview.h"
|
||||
|
||||
#ifdef SUNNYPILOT
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/onroad/onroad_home.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/sidebar.h"
|
||||
#define OnroadWindow OnroadWindowSP
|
||||
#define OffroadHome OffroadHomeSP
|
||||
#define LayoutWidget LayoutWidgetSP
|
||||
#define Sidebar SidebarSP
|
||||
#define ElidedLabel ElidedLabelSP
|
||||
#define SetupWidget SetupWidgetSP
|
||||
#else
|
||||
#include "selfdrive/ui/qt/widgets/controls.h"
|
||||
#include "selfdrive/ui/qt/onroad/onroad_home.h"
|
||||
#include "selfdrive/ui/qt/sidebar.h"
|
||||
#endif
|
||||
|
||||
#include "selfdrive/ui/qt/offroad/offroad_home.h"
|
||||
|
||||
class HomeWindow : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit HomeWindow(QWidget* parent = 0);
|
||||
|
||||
signals:
|
||||
void openSettings(int index = 0, const QString ¶m = "");
|
||||
void closeSettings();
|
||||
|
||||
public slots:
|
||||
void offroadTransition(bool offroad);
|
||||
void showDriverView(bool show);
|
||||
void showSidebar(bool show);
|
||||
|
||||
protected:
|
||||
void mousePressEvent(QMouseEvent* e) override;
|
||||
void mouseDoubleClickEvent(QMouseEvent* e) override;
|
||||
|
||||
Sidebar *sidebar;
|
||||
OffroadHome *home;
|
||||
OnroadWindow *onroad;
|
||||
BodyWindow *body;
|
||||
DriverViewWindow *driver_view;
|
||||
QStackedLayout *slayout;
|
||||
|
||||
protected slots:
|
||||
virtual void updateState(const UIState &s);
|
||||
};
|
||||
@@ -1,420 +0,0 @@
|
||||
#include "selfdrive/ui/qt/network/networking.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QScrollBar>
|
||||
#include <QStyle>
|
||||
|
||||
#include "selfdrive/ui/qt/qt_window.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
|
||||
#ifdef SUNNYPILOT
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h"
|
||||
#else
|
||||
#include "selfdrive/ui/qt/widgets/controls.h"
|
||||
#include "selfdrive/ui/qt/widgets/scrollview.h"
|
||||
#endif
|
||||
|
||||
static const int ICON_WIDTH = 49;
|
||||
|
||||
// Networking functions
|
||||
|
||||
Networking::Networking(QWidget* parent, bool show_advanced) : QFrame(parent) {
|
||||
main_layout = new QStackedLayout(this);
|
||||
|
||||
wifi = new WifiManager(this);
|
||||
connect(wifi, &WifiManager::refreshSignal, this, &Networking::refresh);
|
||||
connect(wifi, &WifiManager::wrongPassword, this, &Networking::wrongPassword);
|
||||
|
||||
wifiScreen = new QWidget(this);
|
||||
QVBoxLayout* vlayout = new QVBoxLayout(wifiScreen);
|
||||
vlayout->setContentsMargins(20, 20, 20, 20);
|
||||
if (show_advanced) {
|
||||
QPushButton* advancedSettings = new QPushButton(tr("Advanced"));
|
||||
advancedSettings->setObjectName("advanced_btn");
|
||||
advancedSettings->setStyleSheet("margin-right: 30px;");
|
||||
advancedSettings->setFixedSize(400, 100);
|
||||
connect(advancedSettings, &QPushButton::clicked, [=]() { main_layout->setCurrentWidget(an); });
|
||||
vlayout->addSpacing(10);
|
||||
vlayout->addWidget(advancedSettings, 0, Qt::AlignRight);
|
||||
vlayout->addSpacing(10);
|
||||
}
|
||||
|
||||
wifiWidget = new WifiUI(this, wifi);
|
||||
wifiWidget->setObjectName("wifiWidget");
|
||||
connect(wifiWidget, &WifiUI::connectToNetwork, this, &Networking::connectToNetwork);
|
||||
|
||||
ScrollView *wifiScroller = new ScrollView(wifiWidget, this);
|
||||
wifiScroller->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
vlayout->addWidget(wifiScroller, 1);
|
||||
main_layout->addWidget(wifiScreen);
|
||||
|
||||
an = new AdvancedNetworking(this, wifi);
|
||||
connect(an, &AdvancedNetworking::backPress, [=]() { main_layout->setCurrentWidget(wifiScreen); });
|
||||
connect(an, &AdvancedNetworking::requestWifiScreen, [=]() { main_layout->setCurrentWidget(wifiScreen); });
|
||||
main_layout->addWidget(an);
|
||||
|
||||
QPalette pal = palette();
|
||||
pal.setColor(QPalette::Window, QColor(0x29, 0x29, 0x29));
|
||||
setAutoFillBackground(true);
|
||||
setPalette(pal);
|
||||
|
||||
setStyleSheet(R"(
|
||||
#wifiWidget > QPushButton, #back_btn, #advanced_btn {
|
||||
font-size: 50px;
|
||||
margin: 0px;
|
||||
padding: 15px;
|
||||
border-width: 0;
|
||||
border-radius: 30px;
|
||||
color: #dddddd;
|
||||
background-color: #393939;
|
||||
}
|
||||
#back_btn:pressed, #advanced_btn:pressed {
|
||||
background-color: #4a4a4a;
|
||||
}
|
||||
)");
|
||||
main_layout->setCurrentWidget(wifiScreen);
|
||||
}
|
||||
|
||||
void Networking::setPrimeType(PrimeState::Type type) {
|
||||
an->setGsmVisible(type == PrimeState::PRIME_TYPE_NONE || type == PrimeState::PRIME_TYPE_UNKNOWN || \
|
||||
type == PrimeState::PRIME_TYPE_PURPLE || type == PrimeState::PRIME_TYPE_LITE);
|
||||
wifi->ipv4_forward = (type == PrimeState::PRIME_TYPE_NONE || type == PrimeState::PRIME_TYPE_LITE);
|
||||
}
|
||||
|
||||
void Networking::refresh() {
|
||||
wifiWidget->refresh();
|
||||
an->refresh();
|
||||
}
|
||||
|
||||
void Networking::connectToNetwork(const Network n) {
|
||||
if (wifi->isKnownConnection(n.ssid)) {
|
||||
wifi->activateWifiConnection(n.ssid);
|
||||
} else if (n.security_type == SecurityType::OPEN) {
|
||||
wifi->connect(n, false);
|
||||
} else if (n.security_type == SecurityType::WPA) {
|
||||
QString pass = InputDialog::getText(tr("Enter password"), this, tr("for \"%1\"").arg(QString::fromUtf8(n.ssid)), true, 8);
|
||||
if (!pass.isEmpty()) {
|
||||
wifi->connect(n, false, pass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Networking::wrongPassword(const QString &ssid) {
|
||||
if (wifi->seenNetworks.contains(ssid)) {
|
||||
const Network &n = wifi->seenNetworks.value(ssid);
|
||||
QString pass = InputDialog::getText(tr("Wrong password"), this, tr("for \"%1\"").arg(QString::fromUtf8(n.ssid)), true, 8);
|
||||
if (!pass.isEmpty()) {
|
||||
wifi->connect(n, false, pass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Networking::showEvent(QShowEvent *event) {
|
||||
wifi->start();
|
||||
}
|
||||
|
||||
void Networking::hideEvent(QHideEvent *event) {
|
||||
main_layout->setCurrentWidget(wifiScreen);
|
||||
wifi->stop();
|
||||
}
|
||||
|
||||
// AdvancedNetworking functions
|
||||
|
||||
AdvancedNetworking::AdvancedNetworking(QWidget* parent, WifiManager* wifi): QWidget(parent), wifi(wifi) {
|
||||
|
||||
QVBoxLayout* main_layout = new QVBoxLayout(this);
|
||||
main_layout->setMargin(40);
|
||||
main_layout->setSpacing(20);
|
||||
|
||||
// Back button
|
||||
QPushButton* back = new QPushButton(tr("Back"));
|
||||
back->setObjectName("back_btn");
|
||||
back->setFixedSize(400, 100);
|
||||
connect(back, &QPushButton::clicked, [=]() { emit backPress(); });
|
||||
main_layout->addWidget(back, 0, Qt::AlignLeft);
|
||||
|
||||
ListWidget *list = new ListWidget(this);
|
||||
// Enable tethering layout
|
||||
tetheringToggle = new ToggleControl(tr("Enable Tethering"), "", "", wifi->isTetheringEnabled());
|
||||
list->addItem(tetheringToggle);
|
||||
QObject::connect(tetheringToggle, &ToggleControl::toggleFlipped, this, &AdvancedNetworking::toggleTethering);
|
||||
|
||||
// Change tethering password
|
||||
ButtonControl *editPasswordButton = new ButtonControl(tr("Tethering Password"), tr("EDIT"));
|
||||
connect(editPasswordButton, &ButtonControl::clicked, [=]() {
|
||||
QString pass = InputDialog::getText(tr("Enter new tethering password"), this, "", true, 8, wifi->getTetheringPassword());
|
||||
if (!pass.isEmpty()) {
|
||||
wifi->changeTetheringPassword(pass);
|
||||
}
|
||||
});
|
||||
list->addItem(editPasswordButton);
|
||||
|
||||
// IP address
|
||||
ipLabel = new LabelControl(tr("IP Address"), wifi->ipv4_address);
|
||||
list->addItem(ipLabel);
|
||||
|
||||
// Roaming toggle
|
||||
const bool roamingEnabled = params.getBool("GsmRoaming");
|
||||
roamingToggle = new ToggleControl(tr("Enable Roaming"), "", "", roamingEnabled);
|
||||
QObject::connect(roamingToggle, &ToggleControl::toggleFlipped, [=](bool state) {
|
||||
params.putBool("GsmRoaming", state);
|
||||
wifi->updateGsmSettings(state, QString::fromStdString(params.get("GsmApn")), params.getBool("GsmMetered"));
|
||||
});
|
||||
list->addItem(roamingToggle);
|
||||
|
||||
// APN settings
|
||||
editApnButton = new ButtonControl(tr("APN Setting"), tr("EDIT"));
|
||||
connect(editApnButton, &ButtonControl::clicked, [=]() {
|
||||
const QString cur_apn = QString::fromStdString(params.get("GsmApn"));
|
||||
QString apn = InputDialog::getText(tr("Enter APN"), this, tr("leave blank for automatic configuration"), false, -1, cur_apn).trimmed();
|
||||
|
||||
if (apn.isEmpty()) {
|
||||
params.remove("GsmApn");
|
||||
} else {
|
||||
params.put("GsmApn", apn.toStdString());
|
||||
}
|
||||
wifi->updateGsmSettings(params.getBool("GsmRoaming"), apn, params.getBool("GsmMetered"));
|
||||
});
|
||||
list->addItem(editApnButton);
|
||||
|
||||
// Cellular metered toggle (prime lite or none)
|
||||
const bool metered = params.getBool("GsmMetered");
|
||||
cellularMeteredToggle = new ToggleControl(tr("Cellular Metered"), tr("Prevent large data uploads when on a metered cellular connection"), "", metered);
|
||||
QObject::connect(cellularMeteredToggle, &SshToggle::toggleFlipped, [=](bool state) {
|
||||
params.putBool("GsmMetered", state);
|
||||
wifi->updateGsmSettings(params.getBool("GsmRoaming"), QString::fromStdString(params.get("GsmApn")), state);
|
||||
});
|
||||
list->addItem(cellularMeteredToggle);
|
||||
|
||||
// Wi-Fi metered toggle
|
||||
std::vector<QString> metered_button_texts{tr("default"), tr("metered"), tr("unmetered")};
|
||||
wifiMeteredToggle = new MultiButtonControl(tr("Wi-Fi Network Metered"), tr("Prevent large data uploads when on a metered Wi-Fi connection"), "", metered_button_texts);
|
||||
QObject::connect(wifiMeteredToggle, &MultiButtonControl::buttonClicked, [=](int id) {
|
||||
wifiMeteredToggle->setEnabled(false);
|
||||
MeteredType metered = MeteredType::UNKNOWN;
|
||||
if (id == NM_METERED_YES) {
|
||||
metered = MeteredType::YES;
|
||||
} else if (id == NM_METERED_NO) {
|
||||
metered = MeteredType::NO;
|
||||
}
|
||||
auto pending_call = wifi->setCurrentNetworkMetered(metered);
|
||||
if (pending_call) {
|
||||
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(*pending_call);
|
||||
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [=]() {
|
||||
refresh();
|
||||
watcher->deleteLater();
|
||||
});
|
||||
}
|
||||
});
|
||||
list->addItem(wifiMeteredToggle);
|
||||
|
||||
// Hidden Network
|
||||
hiddenNetworkButton = new ButtonControl(tr("Hidden Network"), tr("CONNECT"));
|
||||
connect(hiddenNetworkButton, &ButtonControl::clicked, [=]() {
|
||||
QString ssid = InputDialog::getText(tr("Enter SSID"), this, "", false, 1);
|
||||
if (!ssid.isEmpty()) {
|
||||
QString pass = InputDialog::getText(tr("Enter password"), this, tr("for \"%1\"").arg(ssid), true, -1);
|
||||
Network hidden_network;
|
||||
hidden_network.ssid = ssid.toUtf8();
|
||||
if (!pass.isEmpty()) {
|
||||
hidden_network.security_type = SecurityType::WPA;
|
||||
wifi->connect(hidden_network, true, pass);
|
||||
} else {
|
||||
wifi->connect(hidden_network, true);
|
||||
}
|
||||
emit requestWifiScreen();
|
||||
}
|
||||
});
|
||||
list->addItem(hiddenNetworkButton);
|
||||
|
||||
// Set initial config
|
||||
wifi->updateGsmSettings(roamingEnabled, QString::fromStdString(params.get("GsmApn")), metered);
|
||||
|
||||
main_layout->addWidget(new ScrollView(list, this));
|
||||
main_layout->addStretch(1);
|
||||
}
|
||||
|
||||
void AdvancedNetworking::setGsmVisible(bool visible) {
|
||||
roamingToggle->setVisible(visible);
|
||||
editApnButton->setVisible(visible);
|
||||
cellularMeteredToggle->setVisible(visible);
|
||||
}
|
||||
|
||||
void AdvancedNetworking::refresh() {
|
||||
ipLabel->setText(wifi->ipv4_address);
|
||||
tetheringToggle->setEnabled(true);
|
||||
|
||||
if (wifi->isTetheringEnabled() || wifi->ipv4_address == "") {
|
||||
wifiMeteredToggle->setEnabled(false);
|
||||
wifiMeteredToggle->setCheckedButton(0);
|
||||
} else if (wifi->ipv4_address != "") {
|
||||
MeteredType metered = wifi->currentNetworkMetered();
|
||||
wifiMeteredToggle->setEnabled(true);
|
||||
wifiMeteredToggle->setCheckedButton(static_cast<int>(metered));
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void AdvancedNetworking::toggleTethering(bool enabled) {
|
||||
wifi->setTetheringEnabled(enabled);
|
||||
tetheringToggle->setEnabled(false);
|
||||
if (enabled) {
|
||||
wifiMeteredToggle->setEnabled(false);
|
||||
wifiMeteredToggle->setCheckedButton(0);
|
||||
}
|
||||
}
|
||||
|
||||
// WifiUI functions
|
||||
|
||||
WifiUI::WifiUI(QWidget *parent, WifiManager* wifi) : QWidget(parent), wifi(wifi) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
main_layout->setContentsMargins(0, 0, 0, 0);
|
||||
main_layout->setSpacing(0);
|
||||
|
||||
// load imgs
|
||||
for (const auto &s : {"low", "medium", "high", "full"}) {
|
||||
QPixmap pix(ASSET_PATH + "/icons/wifi_strength_" + s + ".svg");
|
||||
strengths.push_back(pix.scaledToHeight(68, Qt::SmoothTransformation));
|
||||
}
|
||||
lock = QPixmap(ASSET_PATH + "icons/lock_closed.svg").scaledToWidth(ICON_WIDTH, Qt::SmoothTransformation);
|
||||
checkmark = QPixmap(ASSET_PATH + "icons/checkmark.svg").scaledToWidth(ICON_WIDTH, Qt::SmoothTransformation);
|
||||
circled_slash = QPixmap(ASSET_PATH + "icons/circled_slash.svg").scaledToWidth(ICON_WIDTH, Qt::SmoothTransformation);
|
||||
|
||||
scanningLabel = new QLabel(tr("Scanning for networks..."));
|
||||
scanningLabel->setStyleSheet("font-size: 65px;");
|
||||
main_layout->addWidget(scanningLabel, 0, Qt::AlignCenter);
|
||||
|
||||
wifi_list_widget = new ListWidget(this);
|
||||
wifi_list_widget->setVisible(false);
|
||||
main_layout->addWidget(wifi_list_widget);
|
||||
|
||||
setStyleSheet(R"(
|
||||
QScrollBar::handle:vertical {
|
||||
min-height: 0px;
|
||||
border-radius: 4px;
|
||||
background-color: #8A8A8A;
|
||||
}
|
||||
#forgetBtn {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #292929;
|
||||
background-color: #BDBDBD;
|
||||
border-width: 1px solid #828282;
|
||||
border-radius: 5px;
|
||||
padding: 40px;
|
||||
padding-bottom: 16px;
|
||||
padding-top: 16px;
|
||||
}
|
||||
#forgetBtn:pressed {
|
||||
background-color: #828282;
|
||||
}
|
||||
#connecting {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
border-radius: 0;
|
||||
padding: 27px;
|
||||
padding-left: 43px;
|
||||
padding-right: 43px;
|
||||
background-color: black;
|
||||
}
|
||||
#ssidLabel {
|
||||
text-align: left;
|
||||
border: none;
|
||||
padding-top: 50px;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
#ssidLabel:disabled {
|
||||
color: #696969;
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
void WifiUI::refresh() {
|
||||
bool is_empty = wifi->seenNetworks.isEmpty();
|
||||
scanningLabel->setVisible(is_empty);
|
||||
wifi_list_widget->setVisible(!is_empty);
|
||||
if (is_empty) return;
|
||||
|
||||
setUpdatesEnabled(false);
|
||||
|
||||
const bool is_tethering_enabled = wifi->isTetheringEnabled();
|
||||
QList<Network> sortedNetworks = wifi->seenNetworks.values();
|
||||
std::sort(sortedNetworks.begin(), sortedNetworks.end(), compare_by_strength);
|
||||
|
||||
int n = 0;
|
||||
for (Network &network : sortedNetworks) {
|
||||
QPixmap status_icon;
|
||||
if (network.connected == ConnectedType::CONNECTED) {
|
||||
status_icon = checkmark;
|
||||
} else if (network.security_type == SecurityType::UNSUPPORTED) {
|
||||
status_icon = circled_slash;
|
||||
} else if (network.security_type == SecurityType::WPA) {
|
||||
status_icon = lock;
|
||||
}
|
||||
bool show_forget_btn = wifi->isKnownConnection(network.ssid) && !is_tethering_enabled;
|
||||
QPixmap strength = strengths[strengthLevel(network.strength)];
|
||||
|
||||
auto item = getItem(n++);
|
||||
item->setItem(network, status_icon, show_forget_btn, strength);
|
||||
item->setVisible(true);
|
||||
}
|
||||
for (; n < wifi_items.size(); ++n) wifi_items[n]->setVisible(false);
|
||||
|
||||
setUpdatesEnabled(true);
|
||||
}
|
||||
|
||||
WifiItem *WifiUI::getItem(int n) {
|
||||
auto item = n < wifi_items.size() ? wifi_items[n] : wifi_items.emplace_back(new WifiItem(tr("CONNECTING..."), tr("FORGET")));
|
||||
if (!item->parentWidget()) {
|
||||
QObject::connect(item, &WifiItem::connectToNetwork, this, &WifiUI::connectToNetwork);
|
||||
QObject::connect(item, &WifiItem::forgotNetwork, [this](const Network n) {
|
||||
if (ConfirmationDialog::confirm(tr("Forget Wi-Fi Network \"%1\"?").arg(QString::fromUtf8(n.ssid)), tr("Forget"), this))
|
||||
wifi->forgetConnection(n.ssid);
|
||||
});
|
||||
wifi_list_widget->addItem(item);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
// WifiItem
|
||||
|
||||
WifiItem::WifiItem(const QString &connecting_text, const QString &forget_text, QWidget *parent) : QWidget(parent) {
|
||||
QHBoxLayout *hlayout = new QHBoxLayout(this);
|
||||
hlayout->setContentsMargins(44, 0, 73, 0);
|
||||
hlayout->setSpacing(50);
|
||||
|
||||
hlayout->addWidget(ssidLabel = new ElidedLabel());
|
||||
ssidLabel->setObjectName("ssidLabel");
|
||||
ssidLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
||||
hlayout->addWidget(connecting = new QPushButton(connecting_text), 0, Qt::AlignRight);
|
||||
connecting->setObjectName("connecting");
|
||||
hlayout->addWidget(forgetBtn = new QPushButton(forget_text), 0, Qt::AlignRight);
|
||||
forgetBtn->setObjectName("forgetBtn");
|
||||
hlayout->addWidget(iconLabel = new QLabel(), 0, Qt::AlignRight);
|
||||
hlayout->addWidget(strengthLabel = new QLabel(), 0, Qt::AlignRight);
|
||||
|
||||
iconLabel->setFixedWidth(ICON_WIDTH);
|
||||
QObject::connect(forgetBtn, &QPushButton::clicked, [this]() { emit forgotNetwork(network); });
|
||||
QObject::connect(ssidLabel, &ElidedLabel::clicked, [this]() {
|
||||
if (network.connected == ConnectedType::DISCONNECTED) emit connectToNetwork(network);
|
||||
});
|
||||
}
|
||||
|
||||
void WifiItem::setItem(const Network &n, const QPixmap &status_icon, bool show_forget_btn, const QPixmap &strength_icon) {
|
||||
network = n;
|
||||
|
||||
ssidLabel->setText(n.ssid);
|
||||
ssidLabel->setEnabled(n.security_type != SecurityType::UNSUPPORTED);
|
||||
ssidLabel->setFont(InterFont(55, network.connected == ConnectedType::DISCONNECTED ? QFont::Normal : QFont::Bold));
|
||||
|
||||
connecting->setVisible(n.connected == ConnectedType::CONNECTING);
|
||||
forgetBtn->setVisible(show_forget_btn);
|
||||
|
||||
iconLabel->setPixmap(status_icon);
|
||||
strengthLabel->setPixmap(strength_icon);
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "selfdrive/ui/qt/network/wifi_manager.h"
|
||||
#include "selfdrive/ui/qt/prime_state.h"
|
||||
#include "selfdrive/ui/qt/widgets/input.h"
|
||||
#include "selfdrive/ui/qt/widgets/ssh_keys.h"
|
||||
#include "selfdrive/ui/qt/widgets/toggle.h"
|
||||
|
||||
#ifdef SUNNYPILOT
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
#define ButtonControl ButtonControlSP
|
||||
#define MultiButtonControl MultiButtonControlSP
|
||||
#define ElidedLabel ElidedLabelSP
|
||||
#define LabelControl LabelControlSP
|
||||
#define ListWidget ListWidgetSP
|
||||
#define ToggleControl ToggleControlSP
|
||||
#else
|
||||
#include "selfdrive/ui/qt/widgets/controls.h"
|
||||
#endif
|
||||
|
||||
class WifiItem : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit WifiItem(const QString &connecting_text, const QString &forget_text, QWidget* parent = nullptr);
|
||||
void setItem(const Network& n, const QPixmap &icon, bool show_forget_btn, const QPixmap &strength);
|
||||
|
||||
signals:
|
||||
// Cannot pass Network by reference. it may change after the signal is sent.
|
||||
void connectToNetwork(const Network n);
|
||||
void forgotNetwork(const Network n);
|
||||
|
||||
protected:
|
||||
ElidedLabel* ssidLabel;
|
||||
QPushButton* connecting;
|
||||
QPushButton* forgetBtn;
|
||||
QLabel* iconLabel;
|
||||
QLabel* strengthLabel;
|
||||
Network network;
|
||||
};
|
||||
|
||||
class WifiUI : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit WifiUI(QWidget *parent = 0, WifiManager* wifi = 0);
|
||||
|
||||
private:
|
||||
WifiItem *getItem(int n);
|
||||
|
||||
WifiManager *wifi = nullptr;
|
||||
QLabel *scanningLabel = nullptr;
|
||||
QPixmap lock;
|
||||
QPixmap checkmark;
|
||||
QPixmap circled_slash;
|
||||
QVector<QPixmap> strengths;
|
||||
ListWidget *wifi_list_widget = nullptr;
|
||||
std::vector<WifiItem*> wifi_items;
|
||||
|
||||
signals:
|
||||
void connectToNetwork(const Network n);
|
||||
|
||||
public slots:
|
||||
void refresh();
|
||||
};
|
||||
|
||||
class AdvancedNetworking : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AdvancedNetworking(QWidget* parent = 0, WifiManager* wifi = 0);
|
||||
void setGsmVisible(bool visible);
|
||||
|
||||
private:
|
||||
LabelControl* ipLabel;
|
||||
ToggleControl* tetheringToggle;
|
||||
ToggleControl* roamingToggle;
|
||||
ButtonControl* editApnButton;
|
||||
ButtonControl* hiddenNetworkButton;
|
||||
ToggleControl* cellularMeteredToggle;
|
||||
MultiButtonControl* wifiMeteredToggle;
|
||||
WifiManager* wifi = nullptr;
|
||||
Params params;
|
||||
|
||||
signals:
|
||||
void backPress();
|
||||
void requestWifiScreen();
|
||||
|
||||
public slots:
|
||||
void toggleTethering(bool enabled);
|
||||
void refresh();
|
||||
};
|
||||
|
||||
class Networking : public QFrame {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Networking(QWidget* parent = 0, bool show_advanced = true);
|
||||
void setPrimeType(PrimeState::Type type);
|
||||
WifiManager* wifi = nullptr;
|
||||
|
||||
protected:
|
||||
QStackedLayout* main_layout = nullptr;
|
||||
QWidget* wifiScreen = nullptr;
|
||||
AdvancedNetworking* an = nullptr;
|
||||
WifiUI* wifiWidget;
|
||||
|
||||
void showEvent(QShowEvent* event) override;
|
||||
void hideEvent(QHideEvent* event) override;
|
||||
|
||||
public slots:
|
||||
void refresh();
|
||||
|
||||
private slots:
|
||||
void connectToNetwork(const Network n);
|
||||
void wrongPassword(const QString &ssid);
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* We are using a NetworkManager DBUS API : https://developer.gnome.org/NetworkManager/1.26/spec.html
|
||||
* */
|
||||
|
||||
// https://developer.gnome.org/NetworkManager/1.26/nm-dbus-types.html#NM80211ApFlags
|
||||
const int NM_802_11_AP_FLAGS_NONE = 0x00000000;
|
||||
const int NM_802_11_AP_FLAGS_PRIVACY = 0x00000001;
|
||||
const int NM_802_11_AP_FLAGS_WPS = 0x00000002;
|
||||
|
||||
// https://developer.gnome.org/NetworkManager/1.26/nm-dbus-types.html#NM80211ApSecurityFlags
|
||||
const int NM_802_11_AP_SEC_PAIR_WEP40 = 0x00000001;
|
||||
const int NM_802_11_AP_SEC_PAIR_WEP104 = 0x00000002;
|
||||
const int NM_802_11_AP_SEC_GROUP_WEP40 = 0x00000010;
|
||||
const int NM_802_11_AP_SEC_GROUP_WEP104 = 0x00000020;
|
||||
const int NM_802_11_AP_SEC_KEY_MGMT_PSK = 0x00000100;
|
||||
const int NM_802_11_AP_SEC_KEY_MGMT_802_1X = 0x00000200;
|
||||
|
||||
const QString NM_DBUS_PATH = "/org/freedesktop/NetworkManager";
|
||||
const QString NM_DBUS_PATH_SETTINGS = "/org/freedesktop/NetworkManager/Settings";
|
||||
|
||||
const QString NM_DBUS_INTERFACE = "org.freedesktop.NetworkManager";
|
||||
const QString NM_DBUS_INTERFACE_PROPERTIES = "org.freedesktop.DBus.Properties";
|
||||
const QString NM_DBUS_INTERFACE_SETTINGS = "org.freedesktop.NetworkManager.Settings";
|
||||
const QString NM_DBUS_INTERFACE_SETTINGS_CONNECTION = "org.freedesktop.NetworkManager.Settings.Connection";
|
||||
const QString NM_DBUS_INTERFACE_DEVICE = "org.freedesktop.NetworkManager.Device";
|
||||
const QString NM_DBUS_INTERFACE_DEVICE_WIRELESS = "org.freedesktop.NetworkManager.Device.Wireless";
|
||||
const QString NM_DBUS_INTERFACE_ACCESS_POINT = "org.freedesktop.NetworkManager.AccessPoint";
|
||||
const QString NM_DBUS_INTERFACE_ACTIVE_CONNECTION = "org.freedesktop.NetworkManager.Connection.Active";
|
||||
const QString NM_DBUS_INTERFACE_IP4_CONFIG = "org.freedesktop.NetworkManager.IP4Config";
|
||||
|
||||
const QString NM_DBUS_SERVICE = "org.freedesktop.NetworkManager";
|
||||
|
||||
const int NM_DEVICE_STATE_UNKNOWN = 0;
|
||||
const int NM_DEVICE_STATE_ACTIVATED = 100;
|
||||
const int NM_DEVICE_STATE_NEED_AUTH = 60;
|
||||
const int NM_DEVICE_TYPE_WIFI = 2;
|
||||
const int NM_DEVICE_TYPE_MODEM = 8;
|
||||
const int NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8;
|
||||
const int DBUS_TIMEOUT = 100;
|
||||
|
||||
// https://developer-old.gnome.org/NetworkManager/1.26/nm-dbus-types.html#NMMetered
|
||||
const int NM_METERED_UNKNOWN = 0;
|
||||
const int NM_METERED_YES = 1;
|
||||
const int NM_METERED_NO = 2;
|
||||
const int NM_METERED_GUESS_YES = 3;
|
||||
const int NM_METERED_GUESS_NO = 4;
|
||||
@@ -1,539 +0,0 @@
|
||||
#include "selfdrive/ui/qt/network/wifi_manager.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "common/swaglog.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
|
||||
bool compare_by_strength(const Network &a, const Network &b) {
|
||||
return std::tuple(a.connected, strengthLevel(a.strength), b.ssid) >
|
||||
std::tuple(b.connected, strengthLevel(b.strength), a.ssid);
|
||||
}
|
||||
|
||||
template <typename T = QDBusMessage, typename... Args>
|
||||
T call(const QString &path, const QString &interface, const QString &method, Args &&...args) {
|
||||
QDBusInterface nm(NM_DBUS_SERVICE, path, interface, QDBusConnection::systemBus());
|
||||
nm.setTimeout(DBUS_TIMEOUT);
|
||||
|
||||
QDBusMessage response = nm.call(method, std::forward<Args>(args)...);
|
||||
if (response.type() == QDBusMessage::ErrorMessage) {
|
||||
qCritical() << "DBus call error:" << response.errorMessage();
|
||||
return T();
|
||||
}
|
||||
|
||||
if constexpr (std::is_same_v<T, QDBusMessage>) {
|
||||
return response;
|
||||
} else if (response.arguments().count() >= 1) {
|
||||
QVariant vFirst = response.arguments().at(0).value<QDBusVariant>().variant();
|
||||
if (vFirst.canConvert<T>()) {
|
||||
return vFirst.value<T>();
|
||||
}
|
||||
QDebug critical = qCritical();
|
||||
critical << "Variant unpacking failure :" << method << ',';
|
||||
(critical << ... << args);
|
||||
}
|
||||
return T();
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
QDBusPendingCall asyncCall(const QString &path, const QString &interface, const QString &method, Args &&...args) {
|
||||
QDBusInterface nm = QDBusInterface(NM_DBUS_SERVICE, path, interface, QDBusConnection::systemBus());
|
||||
return nm.asyncCall(method, args...);
|
||||
}
|
||||
|
||||
bool emptyPath(const QString &path) {
|
||||
return path == "" || path == "/";
|
||||
}
|
||||
|
||||
WifiManager::WifiManager(QObject *parent) : QObject(parent) {
|
||||
qDBusRegisterMetaType<Connection>();
|
||||
qDBusRegisterMetaType<IpConfig>();
|
||||
|
||||
// Set tethering ssid as "weedle" + first 4 characters of a dongle id
|
||||
tethering_ssid = "weedle";
|
||||
if (auto dongle_id = getDongleId()) {
|
||||
tethering_ssid += "-" + dongle_id->left(4);
|
||||
}
|
||||
|
||||
adapter = getAdapter();
|
||||
if (!adapter.isEmpty()) {
|
||||
setup();
|
||||
} else {
|
||||
QDBusConnection::systemBus().connect(NM_DBUS_SERVICE, NM_DBUS_PATH, NM_DBUS_INTERFACE, "DeviceAdded", this, SLOT(deviceAdded(QDBusObjectPath)));
|
||||
}
|
||||
|
||||
timer.callOnTimeout(this, &WifiManager::requestScan);
|
||||
|
||||
initConnections();
|
||||
}
|
||||
|
||||
void WifiManager::setup() {
|
||||
auto bus = QDBusConnection::systemBus();
|
||||
bus.connect(NM_DBUS_SERVICE, adapter, NM_DBUS_INTERFACE_DEVICE, "StateChanged", this, SLOT(stateChange(unsigned int, unsigned int, unsigned int)));
|
||||
bus.connect(NM_DBUS_SERVICE, adapter, NM_DBUS_INTERFACE_PROPERTIES, "PropertiesChanged", this, SLOT(propertyChange(QString, QVariantMap, QStringList)));
|
||||
|
||||
bus.connect(NM_DBUS_SERVICE, NM_DBUS_PATH_SETTINGS, NM_DBUS_INTERFACE_SETTINGS, "ConnectionRemoved", this, SLOT(connectionRemoved(QDBusObjectPath)));
|
||||
bus.connect(NM_DBUS_SERVICE, NM_DBUS_PATH_SETTINGS, NM_DBUS_INTERFACE_SETTINGS, "NewConnection", this, SLOT(newConnection(QDBusObjectPath)));
|
||||
|
||||
raw_adapter_state = call<uint>(adapter, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_DEVICE, "State");
|
||||
activeAp = call<QDBusObjectPath>(adapter, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_DEVICE_WIRELESS, "ActiveAccessPoint").path();
|
||||
|
||||
requestScan();
|
||||
}
|
||||
|
||||
void WifiManager::start() {
|
||||
timer.start(5000);
|
||||
refreshNetworks();
|
||||
}
|
||||
|
||||
void WifiManager::stop() {
|
||||
timer.stop();
|
||||
}
|
||||
|
||||
void WifiManager::refreshNetworks() {
|
||||
if (adapter.isEmpty() || !timer.isActive()) return;
|
||||
|
||||
QDBusPendingCall pending_call = asyncCall(adapter, NM_DBUS_INTERFACE_DEVICE_WIRELESS, "GetAllAccessPoints");
|
||||
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending_call);
|
||||
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &WifiManager::refreshFinished);
|
||||
}
|
||||
|
||||
void WifiManager::refreshFinished(QDBusPendingCallWatcher *watcher) {
|
||||
ipv4_address = getIp4Address();
|
||||
seenNetworks.clear();
|
||||
|
||||
const QDBusReply<QList<QDBusObjectPath>> watcher_reply = *watcher;
|
||||
if (!watcher_reply.isValid()) {
|
||||
qCritical() << "Failed to refresh";
|
||||
watcher->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const QDBusObjectPath &path : watcher_reply.value()) {
|
||||
QDBusReply<QVariantMap> reply = call(path.path(), NM_DBUS_INTERFACE_PROPERTIES, "GetAll", NM_DBUS_INTERFACE_ACCESS_POINT);
|
||||
if (!reply.isValid()) {
|
||||
qCritical() << "Failed to retrieve properties for path:" << path.path();
|
||||
continue;
|
||||
}
|
||||
|
||||
auto properties = reply.value();
|
||||
const QByteArray ssid = properties["Ssid"].toByteArray();
|
||||
if (ssid.isEmpty()) continue;
|
||||
|
||||
// May be multiple access points for each SSID.
|
||||
// Use first for ssid and security type, then update connected status and strength using all
|
||||
if (!seenNetworks.contains(ssid)) {
|
||||
seenNetworks[ssid] = {ssid, 0U, ConnectedType::DISCONNECTED, getSecurityType(properties)};
|
||||
}
|
||||
|
||||
if (path.path() == activeAp) {
|
||||
seenNetworks[ssid].connected = (ssid == connecting_to_network) ? ConnectedType::CONNECTING : ConnectedType::CONNECTED;
|
||||
}
|
||||
|
||||
uint32_t strength = properties["Strength"].toUInt();
|
||||
if (seenNetworks[ssid].strength < strength) {
|
||||
seenNetworks[ssid].strength = strength;
|
||||
}
|
||||
}
|
||||
|
||||
emit refreshSignal();
|
||||
watcher->deleteLater();
|
||||
}
|
||||
|
||||
QString WifiManager::getIp4Address() {
|
||||
if (raw_adapter_state != NM_DEVICE_STATE_ACTIVATED) return "";
|
||||
|
||||
for (const auto &p : getActiveConnections()) {
|
||||
QString type = call<QString>(p.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Type");
|
||||
if (type == "802-11-wireless") {
|
||||
auto ip4config = call<QDBusObjectPath>(p.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Ip4Config");
|
||||
const auto &arr = call<QDBusArgument>(ip4config.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_IP4_CONFIG, "AddressData");
|
||||
QVariantMap path;
|
||||
arr.beginArray();
|
||||
while (!arr.atEnd()) {
|
||||
arr >> path;
|
||||
arr.endArray();
|
||||
return path.value("address").value<QString>();
|
||||
}
|
||||
arr.endArray();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
SecurityType WifiManager::getSecurityType(const QVariantMap &properties) {
|
||||
int sflag = properties["Flags"].toUInt();
|
||||
int wpaflag = properties["WpaFlags"].toUInt();
|
||||
int rsnflag = properties["RsnFlags"].toUInt();
|
||||
int wpa_props = wpaflag | rsnflag;
|
||||
|
||||
// obtained by looking at flags of networks in the office as reported by an Android phone
|
||||
const int supports_wpa = NM_802_11_AP_SEC_PAIR_WEP40 | NM_802_11_AP_SEC_PAIR_WEP104 | NM_802_11_AP_SEC_GROUP_WEP40 | NM_802_11_AP_SEC_GROUP_WEP104 | NM_802_11_AP_SEC_KEY_MGMT_PSK;
|
||||
|
||||
if ((sflag == NM_802_11_AP_FLAGS_NONE) || ((sflag & NM_802_11_AP_FLAGS_WPS) && !(wpa_props & supports_wpa))) {
|
||||
return SecurityType::OPEN;
|
||||
} else if ((sflag & NM_802_11_AP_FLAGS_PRIVACY) && (wpa_props & supports_wpa) && !(wpa_props & NM_802_11_AP_SEC_KEY_MGMT_802_1X)) {
|
||||
return SecurityType::WPA;
|
||||
} else {
|
||||
LOGW("Unsupported network! sflag: %d, wpaflag: %d, rsnflag: %d", sflag, wpaflag, rsnflag);
|
||||
return SecurityType::UNSUPPORTED;
|
||||
}
|
||||
}
|
||||
|
||||
void WifiManager::connect(const Network &n, const bool is_hidden, const QString &password, const QString &username) {
|
||||
setCurrentConnecting(n.ssid);
|
||||
forgetConnection(n.ssid); // Clear all connections that may already exist to the network we are connecting
|
||||
Connection connection;
|
||||
connection["connection"]["type"] = "802-11-wireless";
|
||||
connection["connection"]["uuid"] = QUuid::createUuid().toString().remove('{').remove('}');
|
||||
connection["connection"]["id"] = "sunnypilot connection " + QString::fromStdString(n.ssid.toStdString());
|
||||
connection["connection"]["autoconnect-retries"] = 0;
|
||||
|
||||
connection["802-11-wireless"]["ssid"] = n.ssid;
|
||||
connection["802-11-wireless"]["hidden"] = is_hidden;
|
||||
connection["802-11-wireless"]["mode"] = "infrastructure";
|
||||
|
||||
if (n.security_type == SecurityType::WPA) {
|
||||
connection["802-11-wireless-security"]["key-mgmt"] = "wpa-psk";
|
||||
connection["802-11-wireless-security"]["auth-alg"] = "open";
|
||||
connection["802-11-wireless-security"]["psk"] = password;
|
||||
}
|
||||
|
||||
connection["ipv4"]["method"] = "auto";
|
||||
connection["ipv4"]["dns-priority"] = 600;
|
||||
connection["ipv6"]["method"] = "ignore";
|
||||
|
||||
asyncCall(NM_DBUS_PATH_SETTINGS, NM_DBUS_INTERFACE_SETTINGS, "AddConnection", QVariant::fromValue(connection));
|
||||
}
|
||||
|
||||
void WifiManager::deactivateConnectionBySsid(const QString &ssid) {
|
||||
for (QDBusObjectPath active_connection : getActiveConnections()) {
|
||||
auto pth = call<QDBusObjectPath>(active_connection.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "SpecificObject");
|
||||
if (!emptyPath(pth.path())) {
|
||||
QString Ssid = get_property(pth.path(), "Ssid");
|
||||
if (Ssid == ssid) {
|
||||
deactivateConnection(active_connection);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WifiManager::deactivateConnection(const QDBusObjectPath &path) {
|
||||
asyncCall(NM_DBUS_PATH, NM_DBUS_INTERFACE, "DeactivateConnection", QVariant::fromValue(path));
|
||||
}
|
||||
|
||||
QVector<QDBusObjectPath> WifiManager::getActiveConnections() {
|
||||
auto result = call<QDBusArgument>(NM_DBUS_PATH, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE, "ActiveConnections");
|
||||
return qdbus_cast<QVector<QDBusObjectPath>>(result);
|
||||
}
|
||||
|
||||
bool WifiManager::isKnownConnection(const QString &ssid) {
|
||||
return !getConnectionPath(ssid).path().isEmpty();
|
||||
}
|
||||
|
||||
void WifiManager::forgetConnection(const QString &ssid) {
|
||||
const QDBusObjectPath &path = getConnectionPath(ssid);
|
||||
if (!path.path().isEmpty()) {
|
||||
call(path.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "Delete");
|
||||
}
|
||||
}
|
||||
|
||||
void WifiManager::setCurrentConnecting(const QString &ssid) {
|
||||
connecting_to_network = ssid;
|
||||
for (auto &network : seenNetworks) {
|
||||
network.connected = (network.ssid == ssid) ? ConnectedType::CONNECTING : ConnectedType::DISCONNECTED;
|
||||
}
|
||||
emit refreshSignal();
|
||||
}
|
||||
|
||||
uint WifiManager::getAdapterType(const QDBusObjectPath &path) {
|
||||
return call<uint>(path.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_DEVICE, "DeviceType");
|
||||
}
|
||||
|
||||
void WifiManager::requestScan() {
|
||||
if (!adapter.isEmpty()) {
|
||||
asyncCall(adapter, NM_DBUS_INTERFACE_DEVICE_WIRELESS, "RequestScan", QVariantMap());
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray WifiManager::get_property(const QString &network_path , const QString &property) {
|
||||
return call<QByteArray>(network_path, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACCESS_POINT, property);
|
||||
}
|
||||
|
||||
QString WifiManager::getAdapter(const uint adapter_type) {
|
||||
QDBusReply<QList<QDBusObjectPath>> response = call(NM_DBUS_PATH, NM_DBUS_INTERFACE, "GetDevices");
|
||||
for (const QDBusObjectPath &path : response.value()) {
|
||||
if (getAdapterType(path) == adapter_type) {
|
||||
return path.path();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void WifiManager::stateChange(unsigned int new_state, unsigned int previous_state, unsigned int change_reason) {
|
||||
raw_adapter_state = new_state;
|
||||
if (new_state == NM_DEVICE_STATE_NEED_AUTH && change_reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT && !connecting_to_network.isEmpty()) {
|
||||
forgetConnection(connecting_to_network);
|
||||
emit wrongPassword(connecting_to_network);
|
||||
} else if (new_state == NM_DEVICE_STATE_ACTIVATED) {
|
||||
connecting_to_network = "";
|
||||
refreshNetworks();
|
||||
}
|
||||
}
|
||||
|
||||
// https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Device.Wireless.html
|
||||
void WifiManager::propertyChange(const QString &interface, const QVariantMap &props, const QStringList &invalidated_props) {
|
||||
if (interface == NM_DBUS_INTERFACE_DEVICE_WIRELESS && props.contains("LastScan")) {
|
||||
refreshNetworks();
|
||||
} else if (interface == NM_DBUS_INTERFACE_DEVICE_WIRELESS && props.contains("ActiveAccessPoint")) {
|
||||
activeAp = props.value("ActiveAccessPoint").value<QDBusObjectPath>().path();
|
||||
}
|
||||
}
|
||||
|
||||
void WifiManager::deviceAdded(const QDBusObjectPath &path) {
|
||||
if (getAdapterType(path) == NM_DEVICE_TYPE_WIFI && emptyPath(adapter)) {
|
||||
adapter = path.path();
|
||||
setup();
|
||||
}
|
||||
}
|
||||
|
||||
void WifiManager::connectionRemoved(const QDBusObjectPath &path) {
|
||||
knownConnections.remove(path);
|
||||
}
|
||||
|
||||
void WifiManager::newConnection(const QDBusObjectPath &path) {
|
||||
Connection settings = getConnectionSettings(path);
|
||||
if (settings.value("connection").value("type") == "802-11-wireless") {
|
||||
knownConnections[path] = settings.value("802-11-wireless").value("ssid").toString();
|
||||
if (knownConnections[path] != tethering_ssid) {
|
||||
activateWifiConnection(knownConnections[path]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QDBusObjectPath WifiManager::getConnectionPath(const QString &ssid) {
|
||||
return knownConnections.key(ssid);
|
||||
}
|
||||
|
||||
Connection WifiManager::getConnectionSettings(const QDBusObjectPath &path) {
|
||||
return QDBusReply<Connection>(call(path.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "GetSettings")).value();
|
||||
}
|
||||
|
||||
void WifiManager::initConnections() {
|
||||
const QDBusReply<QList<QDBusObjectPath>> response = call(NM_DBUS_PATH_SETTINGS, NM_DBUS_INTERFACE_SETTINGS, "ListConnections");
|
||||
for (const QDBusObjectPath &path : response.value()) {
|
||||
const Connection settings = getConnectionSettings(path);
|
||||
if (settings.value("connection").value("type") == "802-11-wireless") {
|
||||
knownConnections[path] = settings.value("802-11-wireless").value("ssid").toString();
|
||||
} else if (settings.value("connection").value("id") == "lte") {
|
||||
lteConnectionPath = path;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isKnownConnection(tethering_ssid)) {
|
||||
addTetheringConnection();
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<QDBusPendingCall> WifiManager::activateWifiConnection(const QString &ssid) {
|
||||
const QDBusObjectPath &path = getConnectionPath(ssid);
|
||||
if (!path.path().isEmpty()) {
|
||||
setCurrentConnecting(ssid);
|
||||
return asyncCall(NM_DBUS_PATH, NM_DBUS_INTERFACE, "ActivateConnection", QVariant::fromValue(path), QVariant::fromValue(QDBusObjectPath(adapter)), QVariant::fromValue(QDBusObjectPath("/")));
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void WifiManager::activateModemConnection(const QDBusObjectPath &path) {
|
||||
QString modem = getAdapter(NM_DEVICE_TYPE_MODEM);
|
||||
if (!path.path().isEmpty() && !modem.isEmpty()) {
|
||||
asyncCall(NM_DBUS_PATH, NM_DBUS_INTERFACE, "ActivateConnection", QVariant::fromValue(path), QVariant::fromValue(QDBusObjectPath(modem)), QVariant::fromValue(QDBusObjectPath("/")));
|
||||
}
|
||||
}
|
||||
|
||||
// function matches tici/hardware.py
|
||||
// FIXME: it can mistakenly show CELL when connected to WIFI
|
||||
NetworkType WifiManager::currentNetworkType() {
|
||||
auto primary_conn = call<QDBusObjectPath>(NM_DBUS_PATH, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE, "PrimaryConnection");
|
||||
auto primary_type = call<QString>(primary_conn.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Type");
|
||||
|
||||
if (primary_type == "802-3-ethernet") {
|
||||
return NetworkType::ETHERNET;
|
||||
} else if (primary_type == "802-11-wireless" && !isTetheringEnabled()) {
|
||||
return NetworkType::WIFI;
|
||||
} else {
|
||||
for (const QDBusObjectPath &conn : getActiveConnections()) {
|
||||
auto type = call<QString>(conn.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Type");
|
||||
if (type == "gsm") {
|
||||
return NetworkType::CELL;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NetworkType::NONE;
|
||||
}
|
||||
|
||||
MeteredType WifiManager::currentNetworkMetered() {
|
||||
MeteredType metered = MeteredType::UNKNOWN;
|
||||
for (const auto &active_conn : getActiveConnections()) {
|
||||
QString type = call<QString>(active_conn.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Type");
|
||||
if (type == "802-11-wireless") {
|
||||
QDBusObjectPath conn = call<QDBusObjectPath>(active_conn.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Connection");
|
||||
if (!conn.path().isEmpty()) {
|
||||
Connection settings = getConnectionSettings(conn);
|
||||
int metered_prop = settings.value("connection").value("metered").toInt();
|
||||
if (metered_prop == NM_METERED_YES) {
|
||||
metered = MeteredType::YES;
|
||||
} else if (metered_prop == NM_METERED_NO) {
|
||||
metered = MeteredType::NO;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return metered;
|
||||
}
|
||||
|
||||
std::optional<QDBusPendingCall> WifiManager::setCurrentNetworkMetered(MeteredType metered) {
|
||||
for (const auto &active_conn : getActiveConnections()) {
|
||||
QString type = call<QString>(active_conn.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Type");
|
||||
if (type == "802-11-wireless") {
|
||||
if (!isTetheringEnabled()) {
|
||||
QDBusObjectPath conn = call<QDBusObjectPath>(active_conn.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Connection");
|
||||
if (!conn.path().isEmpty()) {
|
||||
Connection settings = getConnectionSettings(conn);
|
||||
settings["connection"]["metered"] = static_cast<int>(metered);
|
||||
return asyncCall(conn.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "Update", QVariant::fromValue(settings));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void WifiManager::updateGsmSettings(bool roaming, QString apn, bool metered) {
|
||||
if (!lteConnectionPath.path().isEmpty()) {
|
||||
bool changes = false;
|
||||
bool auto_config = apn.isEmpty();
|
||||
Connection settings = getConnectionSettings(lteConnectionPath);
|
||||
if (settings.value("gsm").value("auto-config").toBool() != auto_config) {
|
||||
qWarning() << "Changing gsm.auto-config to" << auto_config;
|
||||
settings["gsm"]["auto-config"] = auto_config;
|
||||
changes = true;
|
||||
}
|
||||
|
||||
if (settings.value("gsm").value("apn").toString() != apn) {
|
||||
qWarning() << "Changing gsm.apn to" << apn;
|
||||
settings["gsm"]["apn"] = apn;
|
||||
changes = true;
|
||||
}
|
||||
|
||||
if (settings.value("gsm").value("home-only").toBool() == roaming) {
|
||||
qWarning() << "Changing gsm.home-only to" << !roaming;
|
||||
settings["gsm"]["home-only"] = !roaming;
|
||||
changes = true;
|
||||
}
|
||||
|
||||
int meteredInt = metered ? NM_METERED_UNKNOWN : NM_METERED_NO;
|
||||
if (settings.value("connection").value("metered").toInt() != meteredInt) {
|
||||
qWarning() << "Changing connection.metered to" << meteredInt;
|
||||
settings["connection"]["metered"] = meteredInt;
|
||||
changes = true;
|
||||
}
|
||||
|
||||
if (changes) {
|
||||
QDBusPendingCall pending_call = asyncCall(lteConnectionPath.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "UpdateUnsaved", QVariant::fromValue(settings)); // update is temporary
|
||||
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending_call);
|
||||
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher]() {
|
||||
deactivateConnection(lteConnectionPath);
|
||||
activateModemConnection(lteConnectionPath);
|
||||
watcher->deleteLater();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Functions for tethering
|
||||
void WifiManager::addTetheringConnection() {
|
||||
Connection connection;
|
||||
connection["connection"]["id"] = "Hotspot";
|
||||
connection["connection"]["uuid"] = QUuid::createUuid().toString().remove('{').remove('}');
|
||||
connection["connection"]["type"] = "802-11-wireless";
|
||||
connection["connection"]["interface-name"] = "wlan0";
|
||||
connection["connection"]["autoconnect"] = false;
|
||||
|
||||
connection["802-11-wireless"]["band"] = "bg";
|
||||
connection["802-11-wireless"]["mode"] = "ap";
|
||||
connection["802-11-wireless"]["ssid"] = tethering_ssid.toUtf8();
|
||||
|
||||
connection["802-11-wireless-security"]["group"] = QStringList("ccmp");
|
||||
connection["802-11-wireless-security"]["key-mgmt"] = "wpa-psk";
|
||||
connection["802-11-wireless-security"]["pairwise"] = QStringList("ccmp");
|
||||
connection["802-11-wireless-security"]["proto"] = QStringList("rsn");
|
||||
connection["802-11-wireless-security"]["psk"] = defaultTetheringPassword;
|
||||
|
||||
connection["ipv4"]["method"] = "shared";
|
||||
QVariantMap address;
|
||||
address["address"] = "192.168.43.1";
|
||||
address["prefix"] = 24u;
|
||||
connection["ipv4"]["address-data"] = QVariant::fromValue(IpConfig() << address);
|
||||
connection["ipv4"]["gateway"] = "192.168.43.1";
|
||||
connection["ipv4"]["never-default"] = true;
|
||||
connection["ipv6"]["method"] = "ignore";
|
||||
|
||||
asyncCall(NM_DBUS_PATH_SETTINGS, NM_DBUS_INTERFACE_SETTINGS, "AddConnection", QVariant::fromValue(connection));
|
||||
}
|
||||
|
||||
void WifiManager::tetheringActivated(QDBusPendingCallWatcher *call) {
|
||||
if (!ipv4_forward) {
|
||||
QTimer::singleShot(5000, this, [=] {
|
||||
qWarning() << "net.ipv4.ip_forward = 0";
|
||||
std::system("sudo sysctl net.ipv4.ip_forward=0");
|
||||
});
|
||||
}
|
||||
call->deleteLater();
|
||||
tethering_on = true;
|
||||
}
|
||||
|
||||
void WifiManager::setTetheringEnabled(bool enabled) {
|
||||
if (enabled) {
|
||||
auto pending_call = activateWifiConnection(tethering_ssid);
|
||||
|
||||
if (pending_call) {
|
||||
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(*pending_call);
|
||||
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &WifiManager::tetheringActivated);
|
||||
}
|
||||
|
||||
} else {
|
||||
deactivateConnectionBySsid(tethering_ssid);
|
||||
tethering_on = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool WifiManager::isTetheringEnabled() {
|
||||
if (!emptyPath(activeAp)) {
|
||||
return get_property(activeAp, "Ssid") == tethering_ssid;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QString WifiManager::getTetheringPassword() {
|
||||
const QDBusObjectPath &path = getConnectionPath(tethering_ssid);
|
||||
if (!path.path().isEmpty()) {
|
||||
QDBusReply<QMap<QString, QVariantMap>> response = call(path.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "GetSecrets", "802-11-wireless-security");
|
||||
return response.value().value("802-11-wireless-security").value("psk").toString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void WifiManager::changeTetheringPassword(const QString &newPassword) {
|
||||
const QDBusObjectPath &path = getConnectionPath(tethering_ssid);
|
||||
if (!path.path().isEmpty()) {
|
||||
Connection settings = getConnectionSettings(path);
|
||||
settings["802-11-wireless-security"]["psk"] = newPassword;
|
||||
call(path.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "Update", QVariant::fromValue(settings));
|
||||
if (isTetheringEnabled()) {
|
||||
activateWifiConnection(tethering_ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <QtDBus>
|
||||
#include <QTimer>
|
||||
|
||||
#include "selfdrive/ui/qt/network/networkmanager.h"
|
||||
|
||||
enum class SecurityType {
|
||||
OPEN,
|
||||
WPA,
|
||||
UNSUPPORTED
|
||||
};
|
||||
enum class ConnectedType {
|
||||
DISCONNECTED,
|
||||
CONNECTING,
|
||||
CONNECTED
|
||||
};
|
||||
enum class NetworkType {
|
||||
NONE,
|
||||
WIFI,
|
||||
CELL,
|
||||
ETHERNET
|
||||
};
|
||||
enum class MeteredType {
|
||||
UNKNOWN,
|
||||
YES,
|
||||
NO
|
||||
};
|
||||
|
||||
typedef QMap<QString, QVariantMap> Connection;
|
||||
typedef QVector<QVariantMap> IpConfig;
|
||||
|
||||
struct Network {
|
||||
QByteArray ssid;
|
||||
unsigned int strength;
|
||||
ConnectedType connected;
|
||||
SecurityType security_type;
|
||||
};
|
||||
bool compare_by_strength(const Network &a, const Network &b);
|
||||
inline int strengthLevel(unsigned int strength) { return std::clamp((int)round(strength / 33.), 0, 3); }
|
||||
|
||||
class WifiManager : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
QMap<QString, Network> seenNetworks;
|
||||
QMap<QDBusObjectPath, QString> knownConnections;
|
||||
QString ipv4_address;
|
||||
bool tethering_on = false;
|
||||
bool ipv4_forward = false;
|
||||
|
||||
explicit WifiManager(QObject* parent);
|
||||
void start();
|
||||
void stop();
|
||||
void requestScan();
|
||||
void forgetConnection(const QString &ssid);
|
||||
bool isKnownConnection(const QString &ssid);
|
||||
std::optional<QDBusPendingCall> activateWifiConnection(const QString &ssid);
|
||||
NetworkType currentNetworkType();
|
||||
MeteredType currentNetworkMetered();
|
||||
std::optional<QDBusPendingCall> setCurrentNetworkMetered(MeteredType metered);
|
||||
void updateGsmSettings(bool roaming, QString apn, bool metered);
|
||||
void connect(const Network &ssid, const bool is_hidden = false, const QString &password = {}, const QString &username = {});
|
||||
|
||||
// Tethering functions
|
||||
void setTetheringEnabled(bool enabled);
|
||||
bool isTetheringEnabled();
|
||||
void changeTetheringPassword(const QString &newPassword);
|
||||
QString getTetheringPassword();
|
||||
|
||||
private:
|
||||
QString adapter; // Path to network manager wifi-device
|
||||
QTimer timer;
|
||||
unsigned int raw_adapter_state = NM_DEVICE_STATE_UNKNOWN; // Connection status https://developer.gnome.org/NetworkManager/1.26/nm-dbus-types.html#NMDeviceState
|
||||
QString connecting_to_network;
|
||||
QString tethering_ssid;
|
||||
const QString defaultTetheringPassword = "swagswagcomma";
|
||||
QString activeAp;
|
||||
QDBusObjectPath lteConnectionPath;
|
||||
|
||||
QString getAdapter(const uint = NM_DEVICE_TYPE_WIFI);
|
||||
uint getAdapterType(const QDBusObjectPath &path);
|
||||
QString getIp4Address();
|
||||
void deactivateConnectionBySsid(const QString &ssid);
|
||||
void deactivateConnection(const QDBusObjectPath &path);
|
||||
QVector<QDBusObjectPath> getActiveConnections();
|
||||
QByteArray get_property(const QString &network_path, const QString &property);
|
||||
SecurityType getSecurityType(const QVariantMap &properties);
|
||||
QDBusObjectPath getConnectionPath(const QString &ssid);
|
||||
Connection getConnectionSettings(const QDBusObjectPath &path);
|
||||
void initConnections();
|
||||
void setup();
|
||||
void refreshNetworks();
|
||||
void activateModemConnection(const QDBusObjectPath &path);
|
||||
void addTetheringConnection();
|
||||
void setCurrentConnecting(const QString &ssid);
|
||||
|
||||
signals:
|
||||
void wrongPassword(const QString &ssid);
|
||||
void refreshSignal();
|
||||
|
||||
private slots:
|
||||
void stateChange(unsigned int new_state, unsigned int previous_state, unsigned int change_reason);
|
||||
void propertyChange(const QString &interface, const QVariantMap &props, const QStringList &invalidated_props);
|
||||
void deviceAdded(const QDBusObjectPath &path);
|
||||
void connectionRemoved(const QDBusObjectPath &path);
|
||||
void newConnection(const QDBusObjectPath &path);
|
||||
void refreshFinished(QDBusPendingCallWatcher *call);
|
||||
void tetheringActivated(QDBusPendingCallWatcher *call);
|
||||
};
|
||||
@@ -1,103 +0,0 @@
|
||||
#include "selfdrive/ui/qt/offroad/developer_panel.h"
|
||||
#include "selfdrive/ui/qt/widgets/ssh_keys.h"
|
||||
|
||||
#ifdef SUNNYPILOT
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
#else
|
||||
#include "selfdrive/ui/qt/widgets/controls.h"
|
||||
#endif
|
||||
|
||||
DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) {
|
||||
adbToggle = new ParamControl("AdbEnabled", tr("Enable ADB"),
|
||||
tr("ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info."), "");
|
||||
addItem(adbToggle);
|
||||
|
||||
// SSH keys
|
||||
addItem(new SshToggle());
|
||||
addItem(new SshControl());
|
||||
|
||||
joystickToggle = new ParamControl("JoystickDebugMode", tr("Joystick Debug Mode"), "", "");
|
||||
QObject::connect(joystickToggle, &ParamControl::toggleFlipped, [=](bool state) {
|
||||
params.putBool("LongitudinalManeuverMode", false);
|
||||
longManeuverToggle->refresh();
|
||||
});
|
||||
addItem(joystickToggle);
|
||||
|
||||
longManeuverToggle = new ParamControl("LongitudinalManeuverMode", tr("Longitudinal Maneuver Mode"), "", "");
|
||||
QObject::connect(longManeuverToggle, &ParamControl::toggleFlipped, [=](bool state) {
|
||||
params.putBool("JoystickDebugMode", false);
|
||||
joystickToggle->refresh();
|
||||
});
|
||||
addItem(longManeuverToggle);
|
||||
|
||||
experimentalLongitudinalToggle = new ParamControl(
|
||||
"AlphaLongitudinalEnabled",
|
||||
tr("sunnypilot Longitudinal Control (Alpha)"),
|
||||
QString("<b>%1</b><br><br>%2")
|
||||
.arg(tr("WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB)."))
|
||||
.arg(tr("On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. "
|
||||
"Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha.")),
|
||||
""
|
||||
);
|
||||
experimentalLongitudinalToggle->setConfirmation(true, false);
|
||||
QObject::connect(experimentalLongitudinalToggle, &ParamControl::toggleFlipped, [=]() {
|
||||
updateToggles(offroad);
|
||||
});
|
||||
addItem(experimentalLongitudinalToggle);
|
||||
|
||||
// Joystick and longitudinal maneuvers should be hidden on release branches
|
||||
is_release = params.getBool("IsReleaseBranch");
|
||||
|
||||
// Toggles should be not available to change in onroad state
|
||||
QObject::connect(uiState(), &UIState::offroadTransition, this, &DeveloperPanel::updateToggles);
|
||||
}
|
||||
|
||||
void DeveloperPanel::updateToggles(bool _offroad) {
|
||||
for (auto btn : findChildren<ParamControl *>()) {
|
||||
btn->setVisible(!is_release);
|
||||
|
||||
/*
|
||||
* experimentalLongitudinalToggle should be toggelable when:
|
||||
* - visible, and
|
||||
* - during onroad & offroad states
|
||||
*/
|
||||
if (btn != experimentalLongitudinalToggle) {
|
||||
btn->setEnabled(_offroad);
|
||||
}
|
||||
}
|
||||
|
||||
// longManeuverToggle and experimentalLongitudinalToggle should not be toggleable if the car does not have longitudinal control
|
||||
auto cp_bytes = params.get("CarParamsPersistent");
|
||||
if (!cp_bytes.empty()) {
|
||||
AlignedBuffer aligned_buf;
|
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size()));
|
||||
cereal::CarParams::Reader CP = cmsg.getRoot<cereal::CarParams>();
|
||||
|
||||
if (!CP.getAlphaLongitudinalAvailable() || is_release) {
|
||||
params.remove("AlphaLongitudinalEnabled");
|
||||
experimentalLongitudinalToggle->setEnabled(false);
|
||||
}
|
||||
|
||||
/*
|
||||
* experimentalLongitudinalToggle should be visible when:
|
||||
* - is not a release branch, and
|
||||
* - the car supports experimental longitudinal control (alpha)
|
||||
*/
|
||||
experimentalLongitudinalToggle->setVisible(CP.getAlphaLongitudinalAvailable() && !is_release);
|
||||
|
||||
longManeuverToggle->setEnabled(hasLongitudinalControl(CP) && _offroad);
|
||||
} else {
|
||||
longManeuverToggle->setEnabled(false);
|
||||
experimentalLongitudinalToggle->setVisible(false);
|
||||
}
|
||||
experimentalLongitudinalToggle->refresh();
|
||||
|
||||
// Handle specific controls visibility for release branches
|
||||
joystickToggle->setVisible(!is_release);
|
||||
|
||||
offroad = _offroad;
|
||||
}
|
||||
|
||||
void DeveloperPanel::showEvent(QShowEvent *event) {
|
||||
updateToggles(offroad);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef SUNNYPILOT
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||
#else
|
||||
#include "selfdrive/ui/qt/offroad/settings.h"
|
||||
#endif
|
||||
|
||||
class DeveloperPanel : public ListWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DeveloperPanel(SettingsWindow *parent);
|
||||
void showEvent(QShowEvent *event) override;
|
||||
|
||||
protected:
|
||||
Params params;
|
||||
ParamControl* adbToggle;
|
||||
ParamControl* joystickToggle;
|
||||
ParamControl* longManeuverToggle;
|
||||
ParamControl* experimentalLongitudinalToggle;
|
||||
bool is_release;
|
||||
bool offroad = false;
|
||||
|
||||
private slots:
|
||||
void updateToggles(bool _offroad);
|
||||
};
|
||||
@@ -1,82 +0,0 @@
|
||||
#include "selfdrive/ui/qt/offroad/driverview.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <QPainter>
|
||||
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
|
||||
DriverViewWindow::DriverViewWindow(QWidget* parent) : CameraWidget("camerad", VISION_STREAM_DRIVER, parent) {
|
||||
QObject::connect(this, &CameraWidget::clicked, this, &DriverViewWindow::done);
|
||||
QObject::connect(device(), &Device::interactiveTimeout, this, [this]() {
|
||||
if (isVisible()) {
|
||||
emit done();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DriverViewWindow::showEvent(QShowEvent* event) {
|
||||
params.putBool("IsDriverViewEnabled", true);
|
||||
device()->resetInteractiveTimeout(60);
|
||||
CameraWidget::showEvent(event);
|
||||
}
|
||||
|
||||
void DriverViewWindow::hideEvent(QHideEvent* event) {
|
||||
params.putBool("IsDriverViewEnabled", false);
|
||||
stopVipcThread();
|
||||
CameraWidget::hideEvent(event);
|
||||
}
|
||||
|
||||
void DriverViewWindow::paintGL() {
|
||||
CameraWidget::paintGL();
|
||||
|
||||
std::lock_guard lk(frame_lock);
|
||||
QPainter p(this);
|
||||
// startup msg
|
||||
if (frames.empty()) {
|
||||
p.setPen(Qt::white);
|
||||
p.setRenderHint(QPainter::TextAntialiasing);
|
||||
p.setFont(InterFont(100, QFont::Bold));
|
||||
p.drawText(geometry(), Qt::AlignCenter, tr("camera starting"));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &sm = *(uiState()->sm);
|
||||
cereal::DriverStateV2::Reader driver_state = sm["driverStateV2"].getDriverStateV2();
|
||||
bool is_rhd = driver_state.getWheelOnRightProb() > 0.5;
|
||||
auto driver_data = is_rhd ? driver_state.getRightDriverData() : driver_state.getLeftDriverData();
|
||||
|
||||
bool face_detected = driver_data.getFaceProb() > 0.7;
|
||||
if (face_detected) {
|
||||
auto fxy_list = driver_data.getFacePosition();
|
||||
auto std_list = driver_data.getFaceOrientationStd();
|
||||
float face_x = fxy_list[0];
|
||||
float face_y = fxy_list[1];
|
||||
float face_std = std::max(std_list[0], std_list[1]);
|
||||
|
||||
float alpha = 0.7;
|
||||
if (face_std > 0.15) {
|
||||
alpha = std::max(0.7 - (face_std-0.15)*3.5, 0.0);
|
||||
}
|
||||
const int box_size = 220;
|
||||
// use approx instead of distort_points
|
||||
int fbox_x = 1080.0 - 1714.0 * face_x;
|
||||
int fbox_y = -135.0 + (504.0 + std::abs(face_x)*112.0) + (1205.0 - std::abs(face_x)*724.0) * face_y;
|
||||
p.setPen(QPen(QColor(255, 255, 255, alpha * 255), 10));
|
||||
p.drawRoundedRect(fbox_x - box_size / 2, fbox_y - box_size / 2, box_size, box_size, 35.0, 35.0);
|
||||
}
|
||||
|
||||
driver_monitor.updateState(*uiState());
|
||||
driver_monitor.draw(p, rect());
|
||||
}
|
||||
|
||||
mat4 DriverViewWindow::calcFrameMatrix() {
|
||||
const float driver_view_ratio = 2.0;
|
||||
const float yscale = stream_height * driver_view_ratio / stream_width;
|
||||
const float xscale = yscale * glHeight() / glWidth() * stream_width / stream_height;
|
||||
return mat4{{
|
||||
xscale, 0.0, 0.0, 0.0,
|
||||
0.0, yscale, 0.0, 0.0,
|
||||
0.0, 0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0,
|
||||
}};
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "selfdrive/ui/qt/widgets/cameraview.h"
|
||||
#include "selfdrive/ui/qt/onroad/driver_monitoring.h"
|
||||
|
||||
class DriverViewWindow : public CameraWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DriverViewWindow(QWidget *parent);
|
||||
|
||||
signals:
|
||||
void done();
|
||||
|
||||
protected:
|
||||
mat4 calcFrameMatrix() override;
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void hideEvent(QHideEvent *event) override;
|
||||
void paintGL() override;
|
||||
|
||||
Params params;
|
||||
DriverMonitorRenderer driver_monitor;
|
||||
};
|
||||
@@ -1,82 +0,0 @@
|
||||
#include "selfdrive/ui/qt/offroad/experimental_mode.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QStyle>
|
||||
|
||||
#include "selfdrive/ui/ui.h"
|
||||
|
||||
#ifdef SUNNYPILOT
|
||||
constexpr int toggles_settings_index = 3;
|
||||
#else
|
||||
constexpr int toggles_settings_index = 2;
|
||||
#endif
|
||||
|
||||
ExperimentalModeButton::ExperimentalModeButton(QWidget *parent) : QPushButton(parent) {
|
||||
chill_pixmap = QPixmap("../assets/icons/couch.svg").scaledToWidth(img_width, Qt::SmoothTransformation);
|
||||
experimental_pixmap = QPixmap("../assets/icons/experimental_grey.svg").scaledToWidth(img_width, Qt::SmoothTransformation);
|
||||
|
||||
// go to toggles and expand experimental mode description
|
||||
connect(this, &QPushButton::clicked, [=]() { emit openSettings(toggles_settings_index, "ExperimentalMode"); });
|
||||
|
||||
setFixedHeight(125);
|
||||
QHBoxLayout *main_layout = new QHBoxLayout;
|
||||
main_layout->setContentsMargins(horizontal_padding, 0, horizontal_padding, 0);
|
||||
|
||||
mode_label = new QLabel;
|
||||
mode_icon = new QLabel;
|
||||
mode_icon->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
|
||||
|
||||
main_layout->addWidget(mode_label, 1, Qt::AlignLeft);
|
||||
main_layout->addWidget(mode_icon, 0, Qt::AlignRight);
|
||||
|
||||
setLayout(main_layout);
|
||||
|
||||
setStyleSheet(R"(
|
||||
QPushButton {
|
||||
border: none;
|
||||
}
|
||||
|
||||
QLabel {
|
||||
font-size: 45px;
|
||||
font-weight: 300;
|
||||
text-align: left;
|
||||
font-family: JetBrainsMono;
|
||||
color: #000000;
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
void ExperimentalModeButton::paintEvent(QPaintEvent *event) {
|
||||
QPainter p(this);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
QPainterPath path;
|
||||
path.addRoundedRect(rect(), 10, 10);
|
||||
|
||||
// gradient
|
||||
bool pressed = isDown();
|
||||
QLinearGradient gradient(rect().left(), 0, rect().right(), 0);
|
||||
if (experimental_mode) {
|
||||
gradient.setColorAt(0, QColor(255, 155, 63, pressed ? 0xcc : 0xff));
|
||||
gradient.setColorAt(1, QColor(219, 56, 34, pressed ? 0xcc : 0xff));
|
||||
} else {
|
||||
gradient.setColorAt(0, QColor(20, 255, 171, pressed ? 0xcc : 0xff));
|
||||
gradient.setColorAt(1, QColor(35, 149, 255, pressed ? 0xcc : 0xff));
|
||||
}
|
||||
p.fillPath(path, gradient);
|
||||
|
||||
// vertical line
|
||||
p.setPen(QPen(QColor(0, 0, 0, 0x4d), 3, Qt::SolidLine));
|
||||
int line_x = rect().right() - img_width - (2 * horizontal_padding);
|
||||
p.drawLine(line_x, rect().bottom(), line_x, rect().top());
|
||||
}
|
||||
|
||||
void ExperimentalModeButton::showEvent(QShowEvent *event) {
|
||||
experimental_mode = params.getBool("ExperimentalMode");
|
||||
mode_icon->setPixmap(experimental_mode ? experimental_pixmap : chill_pixmap);
|
||||
mode_label->setText(experimental_mode ? tr("EXPERIMENTAL MODE ON") : tr("CHILL MODE ON"));
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
|
||||
#include "common/params.h"
|
||||
|
||||
class ExperimentalModeButton : public QPushButton {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ExperimentalModeButton(QWidget* parent = 0);
|
||||
|
||||
signals:
|
||||
void openSettings(int index = 0, const QString &toggle = "");
|
||||
|
||||
private:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
|
||||
Params params;
|
||||
bool experimental_mode;
|
||||
int img_width = 100;
|
||||
int horizontal_padding = 30;
|
||||
QPixmap experimental_pixmap;
|
||||
QPixmap chill_pixmap;
|
||||
QLabel *mode_label;
|
||||
QLabel *mode_icon;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
};
|
||||
@@ -1,114 +0,0 @@
|
||||
#include "selfdrive/ui/qt/offroad/firehose.h"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QFrame>
|
||||
#include <QScrollArea>
|
||||
#include <QStackedLayout>
|
||||
#include <QProgressBar>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QTimer>
|
||||
|
||||
#ifdef SUNNYPILOT
|
||||
#define UIState UIStateSP
|
||||
#endif
|
||||
|
||||
FirehosePanel::FirehosePanel(SettingsWindow *parent) : QWidget((QWidget*)parent) {
|
||||
layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(40, 40, 40, 40);
|
||||
layout->setSpacing(20);
|
||||
|
||||
// header
|
||||
QLabel *title = new QLabel(tr("Firehose Mode"));
|
||||
title->setStyleSheet("font-size: 100px; font-weight: 500; font-family: 'Noto Color Emoji';");
|
||||
layout->addWidget(title, 0, Qt::AlignCenter);
|
||||
|
||||
// Create a container for the content
|
||||
QFrame *content = new QFrame();
|
||||
content->setStyleSheet("background-color: #292929; border-radius: 15px; padding: 20px;");
|
||||
QVBoxLayout *content_layout = new QVBoxLayout(content);
|
||||
content_layout->setSpacing(20);
|
||||
|
||||
// Top description
|
||||
QLabel *description = new QLabel(tr("sunnypilot learns to drive by watching humans, like you, drive.\n\nFirehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode."));
|
||||
description->setStyleSheet("font-size: 45px; padding-bottom: 20px;");
|
||||
description->setWordWrap(true);
|
||||
content_layout->addWidget(description);
|
||||
|
||||
// Add a separator
|
||||
QFrame *line = new QFrame();
|
||||
line->setFrameShape(QFrame::HLine);
|
||||
line->setFrameShadow(QFrame::Sunken);
|
||||
line->setStyleSheet("background-color: #444444; margin-top: 5px; margin-bottom: 5px;");
|
||||
content_layout->addWidget(line);
|
||||
|
||||
toggle_label = new QLabel(tr("Firehose Mode: ACTIVE"));
|
||||
toggle_label->setStyleSheet("font-size: 60px; font-weight: bold; color: white;");
|
||||
content_layout->addWidget(toggle_label);
|
||||
|
||||
// Add contribution label
|
||||
contribution_label = new QLabel();
|
||||
contribution_label->setStyleSheet("font-size: 52px; margin-top: 10px; margin-bottom: 10px;");
|
||||
contribution_label->setWordWrap(true);
|
||||
contribution_label->hide();
|
||||
content_layout->addWidget(contribution_label);
|
||||
|
||||
// Add a separator before detailed instructions
|
||||
QFrame *line2 = new QFrame();
|
||||
line2->setFrameShape(QFrame::HLine);
|
||||
line2->setFrameShadow(QFrame::Sunken);
|
||||
line2->setStyleSheet("background-color: #444444; margin-top: 10px; margin-bottom: 10px;");
|
||||
content_layout->addWidget(line2);
|
||||
|
||||
// Detailed instructions at the bottom
|
||||
detailed_instructions = new QLabel(tr(
|
||||
"For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br>"
|
||||
"<br>"
|
||||
"Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br>"
|
||||
"<br><br>"
|
||||
"<b>Frequently Asked Questions</b><br><br>"
|
||||
"<i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br>"
|
||||
"<i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br>"
|
||||
"<i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br>"
|
||||
"<i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training."
|
||||
));
|
||||
detailed_instructions->setStyleSheet("font-size: 40px; color: #E4E4E4;");
|
||||
detailed_instructions->setWordWrap(true);
|
||||
content_layout->addWidget(detailed_instructions);
|
||||
|
||||
layout->addWidget(content, 1);
|
||||
|
||||
// Set up the API request for firehose stats
|
||||
const QString dongle_id = QString::fromStdString(Params().get("DongleId"));
|
||||
firehose_stats = new RequestRepeater(this, CommaApi::BASE_URL + "/v1/devices/" + dongle_id + "/firehose_stats",
|
||||
"ApiCache_FirehoseStats", 30, true);
|
||||
QObject::connect(firehose_stats, &RequestRepeater::requestDone, [=](const QString &response, bool success) {
|
||||
if (success) {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8());
|
||||
QJsonObject json = doc.object();
|
||||
int count = json["firehose"].toInt();
|
||||
contribution_label->setText(tr("<b>%n segment(s)</b> of your driving is in the training dataset so far.", "", count));
|
||||
contribution_label->show();
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(uiState(), &UIState::uiUpdate, this, &FirehosePanel::refresh);
|
||||
}
|
||||
|
||||
void FirehosePanel::refresh() {
|
||||
auto deviceState = (*uiState()->sm)["deviceState"].getDeviceState();
|
||||
auto networkType = deviceState.getNetworkType();
|
||||
bool networkMetered = deviceState.getNetworkMetered();
|
||||
|
||||
bool is_active = !networkMetered && (networkType != cereal::DeviceState::NetworkType::NONE);
|
||||
if (is_active) {
|
||||
toggle_label->setText(tr("ACTIVE"));
|
||||
toggle_label->setStyleSheet("font-size: 60px; font-weight: bold; color: #2ecc71;");
|
||||
} else {
|
||||
toggle_label->setText(tr("<span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network"));
|
||||
toggle_label->setStyleSheet("font-size: 60px;");
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QLabel>
|
||||
#include "selfdrive/ui/qt/request_repeater.h"
|
||||
|
||||
#ifdef SUNNYPILOT
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||
#else
|
||||
#include "selfdrive/ui/ui.h"
|
||||
#include "selfdrive/ui/qt/widgets/controls.h"
|
||||
#include "selfdrive/ui/qt/offroad/settings.h"
|
||||
#endif
|
||||
|
||||
// Forward declarations
|
||||
class SettingsWindow;
|
||||
|
||||
class FirehosePanel : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit FirehosePanel(SettingsWindow *parent);
|
||||
|
||||
private:
|
||||
QVBoxLayout *layout;
|
||||
|
||||
QLabel *detailed_instructions;
|
||||
QLabel *contribution_label;
|
||||
QLabel *toggle_label;
|
||||
|
||||
RequestRepeater *firehose_stats;
|
||||
|
||||
private slots:
|
||||
void refresh();
|
||||
};
|
||||
@@ -1,211 +0,0 @@
|
||||
#include "selfdrive/ui/qt/offroad/onboarding.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <QLabel>
|
||||
#include <QPainter>
|
||||
#include <QTransform>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "common/util.h"
|
||||
#include "common/params.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
#include "selfdrive/ui/qt/widgets/input.h"
|
||||
|
||||
TrainingGuide::TrainingGuide(QWidget *parent) : QFrame(parent) {
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
}
|
||||
|
||||
void TrainingGuide::mouseReleaseEvent(QMouseEvent *e) {
|
||||
if (click_timer.elapsed() < 250) {
|
||||
return;
|
||||
}
|
||||
click_timer.restart();
|
||||
|
||||
auto contains = [this](QRect r, const QPoint &pt) {
|
||||
if (image.size() != image_raw_size) {
|
||||
QTransform transform;
|
||||
transform.translate((width()- image.width()) / 2.0, (height()- image.height()) / 2.0);
|
||||
transform.scale(image.width() / (float)image_raw_size.width(), image.height() / (float)image_raw_size.height());
|
||||
r= transform.mapRect(r);
|
||||
}
|
||||
return r.contains(pt);
|
||||
};
|
||||
|
||||
if (contains(boundingRect[currentIndex], e->pos())) {
|
||||
if (currentIndex == 9) {
|
||||
const QRect yes = QRect(707, 804, 531, 164);
|
||||
Params().putBool("RecordFront", contains(yes, e->pos()));
|
||||
}
|
||||
currentIndex += 1;
|
||||
} else if (currentIndex == (boundingRect.size() - 2) && contains(boundingRect.last(), e->pos())) {
|
||||
currentIndex = 0;
|
||||
}
|
||||
|
||||
if (currentIndex >= (boundingRect.size() - 1)) {
|
||||
emit completedTraining();
|
||||
} else {
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void TrainingGuide::showEvent(QShowEvent *event) {
|
||||
currentIndex = 0;
|
||||
click_timer.start();
|
||||
}
|
||||
|
||||
QImage TrainingGuide::loadImage(int id) {
|
||||
QImage img(img_path + QString("step%1.png").arg(id));
|
||||
image_raw_size = img.size();
|
||||
if (image_raw_size != rect().size()) {
|
||||
img = img.scaled(width(), height(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
return img;
|
||||
}
|
||||
|
||||
void TrainingGuide::paintEvent(QPaintEvent *event) {
|
||||
QPainter painter(this);
|
||||
|
||||
QRect bg(0, 0, painter.device()->width(), painter.device()->height());
|
||||
painter.fillRect(bg, QColor("#000000"));
|
||||
|
||||
image = loadImage(currentIndex);
|
||||
QRect rect(image.rect());
|
||||
rect.moveCenter(bg.center());
|
||||
painter.drawImage(rect.topLeft(), image);
|
||||
|
||||
// progress bar
|
||||
if (currentIndex > 0 && currentIndex < (boundingRect.size() - 2)) {
|
||||
const int h = 20;
|
||||
const int w = (currentIndex / (float)(boundingRect.size() - 2)) * width();
|
||||
painter.fillRect(QRect(0, height() - h, w, h), QColor("#465BEA"));
|
||||
}
|
||||
}
|
||||
|
||||
void TermsPage::showEvent(QShowEvent *event) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
main_layout->setContentsMargins(45, 35, 45, 45);
|
||||
main_layout->setSpacing(0);
|
||||
|
||||
QVBoxLayout *vlayout = new QVBoxLayout();
|
||||
vlayout->setContentsMargins(165, 165, 165, 0);
|
||||
main_layout->addLayout(vlayout);
|
||||
|
||||
QLabel *title = new QLabel(tr("Welcome to sunnypilot"));
|
||||
title->setStyleSheet("font-size: 90px; font-weight: 500;");
|
||||
vlayout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft);
|
||||
|
||||
vlayout->addSpacing(90);
|
||||
QLabel *desc = new QLabel(tr("You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing."));
|
||||
desc->setWordWrap(true);
|
||||
desc->setStyleSheet("font-size: 80px; font-weight: 300;");
|
||||
vlayout->addWidget(desc, 0);
|
||||
|
||||
vlayout->addStretch();
|
||||
|
||||
QHBoxLayout* buttons = new QHBoxLayout;
|
||||
buttons->setMargin(0);
|
||||
buttons->setSpacing(45);
|
||||
main_layout->addLayout(buttons);
|
||||
|
||||
QPushButton *decline_btn = new QPushButton(tr("Decline"));
|
||||
buttons->addWidget(decline_btn);
|
||||
QObject::connect(decline_btn, &QPushButton::clicked, this, &TermsPage::declinedTerms);
|
||||
|
||||
accept_btn = new QPushButton(tr("Agree"));
|
||||
accept_btn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #465BEA;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #3049F4;
|
||||
}
|
||||
)");
|
||||
buttons->addWidget(accept_btn);
|
||||
QObject::connect(accept_btn, &QPushButton::clicked, this, &TermsPage::acceptedTerms);
|
||||
}
|
||||
|
||||
void DeclinePage::showEvent(QShowEvent *event) {
|
||||
if (layout()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
main_layout->setMargin(45);
|
||||
main_layout->setSpacing(40);
|
||||
|
||||
QLabel *text = new QLabel(this);
|
||||
text->setText(tr("You must accept the Terms and Conditions in order to use sunnypilot."));
|
||||
text->setStyleSheet(R"(font-size: 80px; font-weight: 300; margin: 200px;)");
|
||||
text->setWordWrap(true);
|
||||
main_layout->addWidget(text, 0, Qt::AlignCenter);
|
||||
|
||||
QHBoxLayout* buttons = new QHBoxLayout;
|
||||
buttons->setSpacing(45);
|
||||
main_layout->addLayout(buttons);
|
||||
|
||||
QPushButton *back_btn = new QPushButton(tr("Back"));
|
||||
buttons->addWidget(back_btn);
|
||||
|
||||
QObject::connect(back_btn, &QPushButton::clicked, this, &DeclinePage::getBack);
|
||||
|
||||
QPushButton *uninstall_btn = new QPushButton(tr("Decline, uninstall %1").arg(getBrand()));
|
||||
uninstall_btn->setStyleSheet("background-color: #B73D3D");
|
||||
buttons->addWidget(uninstall_btn);
|
||||
QObject::connect(uninstall_btn, &QPushButton::clicked, [=]() {
|
||||
Params().putBool("DoUninstall", true);
|
||||
});
|
||||
}
|
||||
|
||||
void OnboardingWindow::updateActiveScreen() {
|
||||
if (!accepted_terms) {
|
||||
setCurrentIndex(0);
|
||||
} else if (!training_done) {
|
||||
setCurrentIndex(1);
|
||||
} else {
|
||||
emit onboardingDone();
|
||||
}
|
||||
}
|
||||
|
||||
OnboardingWindow::OnboardingWindow(QWidget *parent) : QStackedWidget(parent) {
|
||||
std::string current_terms_version = params.get("TermsVersion");
|
||||
std::string current_training_version = params.get("TrainingVersion");
|
||||
accepted_terms = params.get("HasAcceptedTerms") == current_terms_version;
|
||||
training_done = params.get("CompletedTrainingVersion") == current_training_version;
|
||||
|
||||
TermsPage* terms = new TermsPage(this);
|
||||
addWidget(terms);
|
||||
connect(terms, &TermsPage::acceptedTerms, [=]() {
|
||||
params.put("HasAcceptedTerms", current_terms_version);
|
||||
accepted_terms = true;
|
||||
updateActiveScreen();
|
||||
});
|
||||
connect(terms, &TermsPage::declinedTerms, [=]() { setCurrentIndex(2); });
|
||||
|
||||
TrainingGuide* tr = new TrainingGuide(this);
|
||||
addWidget(tr);
|
||||
connect(tr, &TrainingGuide::completedTraining, [=]() {
|
||||
training_done = true;
|
||||
params.put("CompletedTrainingVersion", current_training_version);
|
||||
updateActiveScreen();
|
||||
});
|
||||
|
||||
DeclinePage* declinePage = new DeclinePage(this);
|
||||
addWidget(declinePage);
|
||||
connect(declinePage, &DeclinePage::getBack, [=]() { updateActiveScreen(); });
|
||||
|
||||
setStyleSheet(R"(
|
||||
* {
|
||||
color: white;
|
||||
background-color: black;
|
||||
}
|
||||
QPushButton {
|
||||
height: 160px;
|
||||
font-size: 55px;
|
||||
font-weight: 400;
|
||||
border-radius: 10px;
|
||||
background-color: #4F4F4F;
|
||||
}
|
||||
)");
|
||||
updateActiveScreen();
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QImage>
|
||||
#include <QMouseEvent>
|
||||
#include <QPushButton>
|
||||
#include <QStackedWidget>
|
||||
#include <QWidget>
|
||||
|
||||
#include "common/params.h"
|
||||
#include "selfdrive/ui/qt/qt_window.h"
|
||||
|
||||
class TrainingGuide : public QFrame {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TrainingGuide(QWidget *parent = 0);
|
||||
|
||||
private:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent* e) override;
|
||||
QImage loadImage(int id);
|
||||
|
||||
QImage image;
|
||||
QSize image_raw_size;
|
||||
int currentIndex = 0;
|
||||
|
||||
// Bounding boxes for each training guide step
|
||||
const QRect continueBtn = {1840, 0, 320, 1080};
|
||||
QVector<QRect> boundingRect {
|
||||
QRect(112, 804, 618, 164),
|
||||
continueBtn,
|
||||
continueBtn,
|
||||
QRect(1641, 558, 210, 313),
|
||||
QRect(1662, 528, 184, 108),
|
||||
continueBtn,
|
||||
QRect(1814, 621, 211, 170),
|
||||
QRect(1350, 0, 497, 755),
|
||||
QRect(1540, 386, 468, 238),
|
||||
QRect(112, 804, 1126, 164),
|
||||
QRect(1598, 199, 316, 333),
|
||||
continueBtn,
|
||||
QRect(1364, 90, 796, 990),
|
||||
continueBtn,
|
||||
QRect(1593, 114, 318, 853),
|
||||
QRect(1379, 511, 391, 243),
|
||||
continueBtn,
|
||||
continueBtn,
|
||||
QRect(630, 804, 626, 164),
|
||||
QRect(108, 804, 426, 164),
|
||||
};
|
||||
|
||||
const QString img_path = "../assets/training/";
|
||||
QElapsedTimer click_timer;
|
||||
|
||||
signals:
|
||||
void completedTraining();
|
||||
};
|
||||
|
||||
|
||||
class TermsPage : public QFrame {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TermsPage(QWidget *parent = 0) : QFrame(parent) {}
|
||||
|
||||
private:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
|
||||
protected:
|
||||
QPushButton *accept_btn;
|
||||
|
||||
signals:
|
||||
void acceptedTerms();
|
||||
void declinedTerms();
|
||||
};
|
||||
|
||||
class DeclinePage : public QFrame {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DeclinePage(QWidget *parent = 0) : QFrame(parent) {}
|
||||
|
||||
private:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
|
||||
signals:
|
||||
void getBack();
|
||||
};
|
||||
|
||||
class OnboardingWindow : public QStackedWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit OnboardingWindow(QWidget *parent = 0);
|
||||
inline void showTrainingGuide() { setCurrentIndex(1); }
|
||||
virtual inline bool completed() const { return accepted_terms && training_done; }
|
||||
|
||||
protected:
|
||||
virtual void updateActiveScreen();
|
||||
|
||||
Params params;
|
||||
bool accepted_terms = false, training_done = false;
|
||||
|
||||
signals:
|
||||
void onboardingDone();
|
||||
};
|
||||
@@ -1,551 +0,0 @@
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include "common/watchdog.h"
|
||||
#include "common/util.h"
|
||||
#include "selfdrive/ui/qt/network/networking.h"
|
||||
#include "selfdrive/ui/qt/offroad/settings.h"
|
||||
#include "selfdrive/ui/qt/qt_window.h"
|
||||
#include "selfdrive/ui/qt/widgets/prime.h"
|
||||
#include "selfdrive/ui/qt/widgets/scrollview.h"
|
||||
#include "selfdrive/ui/qt/offroad/developer_panel.h"
|
||||
#include "selfdrive/ui/qt/offroad/firehose.h"
|
||||
|
||||
TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) {
|
||||
// param, title, desc, icon, restart needed
|
||||
std::vector<std::tuple<QString, QString, QString, QString, bool>> toggle_defs{
|
||||
{
|
||||
"OpenpilotEnabledToggle",
|
||||
tr("Enable sunnypilot"),
|
||||
tr("Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature."),
|
||||
"../assets/icons/chffr_wheel.png",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"ExperimentalMode",
|
||||
tr("Experimental Mode"),
|
||||
"",
|
||||
"../assets/icons/experimental_white.svg",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"DisengageOnAccelerator",
|
||||
tr("Disengage on Accelerator Pedal"),
|
||||
tr("When enabled, pressing the accelerator pedal will disengage sunnypilot."),
|
||||
"../assets/icons/disengage_on_accelerator.svg",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"IsLdwEnabled",
|
||||
tr("Enable Lane Departure Warnings"),
|
||||
tr("Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h)."),
|
||||
"../assets/icons/warning.png",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"AlwaysOnDM",
|
||||
tr("Always-On Driver Monitoring"),
|
||||
tr("Enable driver monitoring even when sunnypilot is not engaged."),
|
||||
"../assets/icons/monitoring.png",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"RecordFront",
|
||||
tr("Record and Upload Driver Camera"),
|
||||
tr("Upload data from the driver facing camera and help improve the driver monitoring algorithm."),
|
||||
"../assets/icons/monitoring.png",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"RecordAudio",
|
||||
tr("Record and Upload Microphone Audio"),
|
||||
tr("Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."),
|
||||
"../assets/icons/microphone.png",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"IsMetric",
|
||||
tr("Use Metric System"),
|
||||
tr("Display speed in km/h instead of mph."),
|
||||
"../assets/icons/metric.png",
|
||||
false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
std::vector<QString> longi_button_texts{tr("Aggressive"), tr("Standard"), tr("Relaxed")};
|
||||
long_personality_setting = new ButtonParamControl("LongitudinalPersonality", tr("Driving Personality"),
|
||||
tr("Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. "
|
||||
"In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with "
|
||||
"your steering wheel distance button."),
|
||||
"../assets/icons/speed_limit.png",
|
||||
longi_button_texts);
|
||||
|
||||
// set up uiState update for personality setting
|
||||
QObject::connect(uiState(), &UIState::uiUpdate, this, &TogglesPanel::updateState);
|
||||
|
||||
for (auto &[param, title, desc, icon, needs_restart] : toggle_defs) {
|
||||
auto toggle = new ParamControl(param, title, desc, icon, this);
|
||||
|
||||
bool locked = params.getBool((param + "Lock").toStdString());
|
||||
toggle->setEnabled(!locked);
|
||||
|
||||
if (needs_restart && !locked) {
|
||||
toggle->setDescription(toggle->getDescription() + tr(" Changing this setting will restart openpilot if the car is powered on."));
|
||||
|
||||
QObject::connect(uiState(), &UIState::engagedChanged, [toggle](bool engaged) {
|
||||
toggle->setEnabled(!engaged);
|
||||
});
|
||||
|
||||
QObject::connect(toggle, &ParamControl::toggleFlipped, [=](bool state) {
|
||||
params.putBool("OnroadCycleRequested", true);
|
||||
});
|
||||
}
|
||||
|
||||
addItem(toggle);
|
||||
toggles[param.toStdString()] = toggle;
|
||||
|
||||
// insert longitudinal personality after NDOG toggle
|
||||
if (param == "DisengageOnAccelerator") {
|
||||
addItem(long_personality_setting);
|
||||
}
|
||||
}
|
||||
|
||||
// Toggles with confirmation dialogs
|
||||
#ifndef SUNNYPILOT
|
||||
toggles["ExperimentalMode"]->setActiveIcon("../assets/icons/experimental.svg");
|
||||
#endif
|
||||
toggles["ExperimentalMode"]->setConfirmation(true, true);
|
||||
}
|
||||
|
||||
void TogglesPanel::updateState(const UIState &s) {
|
||||
const SubMaster &sm = *(s.sm);
|
||||
|
||||
if (sm.updated("selfdriveState")) {
|
||||
auto personality = sm["selfdriveState"].getSelfdriveState().getPersonality();
|
||||
if (personality != s.scene.personality && s.scene.started && isVisible()) {
|
||||
long_personality_setting->setCheckedButton(static_cast<int>(personality));
|
||||
}
|
||||
uiState()->scene.personality = personality;
|
||||
}
|
||||
}
|
||||
|
||||
void TogglesPanel::expandToggleDescription(const QString ¶m) {
|
||||
toggles[param.toStdString()]->showDescription();
|
||||
}
|
||||
|
||||
void TogglesPanel::scrollToToggle(const QString ¶m) {
|
||||
if (auto it = toggles.find(param.toStdString()); it != toggles.end()) {
|
||||
auto scroll_area = qobject_cast<QScrollArea*>(parent()->parent());
|
||||
if (scroll_area) {
|
||||
scroll_area->ensureWidgetVisible(it->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TogglesPanel::showEvent(QShowEvent *event) {
|
||||
updateToggles();
|
||||
}
|
||||
|
||||
void TogglesPanel::updateToggles() {
|
||||
auto experimental_mode_toggle = toggles["ExperimentalMode"];
|
||||
const QString e2e_description = QString("%1<br>"
|
||||
"<h4>%2</h4><br>"
|
||||
"%3<br>"
|
||||
"<h4>%4</h4><br>"
|
||||
"%5<br>")
|
||||
.arg(tr("sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below:"))
|
||||
.arg(tr("End-to-End Longitudinal Control"))
|
||||
.arg(tr("Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. "
|
||||
"Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; "
|
||||
"mistakes should be expected."))
|
||||
.arg(tr("New Driving Visualization"))
|
||||
.arg(tr("The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner."));
|
||||
|
||||
const bool is_release = params.getBool("IsReleaseBranch");
|
||||
auto cp_bytes = params.get("CarParamsPersistent");
|
||||
if (!cp_bytes.empty()) {
|
||||
AlignedBuffer aligned_buf;
|
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size()));
|
||||
cereal::CarParams::Reader CP = cmsg.getRoot<cereal::CarParams>();
|
||||
|
||||
if (hasLongitudinalControl(CP)) {
|
||||
// normal description and toggle
|
||||
experimental_mode_toggle->setEnabled(true);
|
||||
experimental_mode_toggle->setDescription(e2e_description);
|
||||
long_personality_setting->setEnabled(true);
|
||||
} else {
|
||||
// no long for now
|
||||
experimental_mode_toggle->setEnabled(false);
|
||||
long_personality_setting->setEnabled(false);
|
||||
params.remove("ExperimentalMode");
|
||||
|
||||
const QString unavailable = tr("Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control.");
|
||||
|
||||
QString long_desc = unavailable + " " + \
|
||||
tr("sunnypilot longitudinal control may come in a future update.");
|
||||
if (CP.getAlphaLongitudinalAvailable()) {
|
||||
if (is_release) {
|
||||
long_desc = unavailable + " " + tr("An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches.");
|
||||
} else {
|
||||
long_desc = tr("Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode.");
|
||||
}
|
||||
}
|
||||
experimental_mode_toggle->setDescription("<b>" + long_desc + "</b><br><br>" + e2e_description);
|
||||
}
|
||||
|
||||
experimental_mode_toggle->refresh();
|
||||
} else {
|
||||
experimental_mode_toggle->setDescription(e2e_description);
|
||||
}
|
||||
}
|
||||
|
||||
DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
|
||||
setSpacing(50);
|
||||
addItem(new LabelControl(tr("Dongle ID"), getDongleId().value_or(tr("N/A"))));
|
||||
addItem(new LabelControl(tr("Serial"), params.get("HardwareSerial").c_str()));
|
||||
|
||||
pair_device = new ButtonControl(tr("Pair Device"), tr("PAIR"),
|
||||
tr("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer."));
|
||||
connect(pair_device, &ButtonControl::clicked, [=]() {
|
||||
PairingPopup popup(this);
|
||||
popup.exec();
|
||||
});
|
||||
addItem(pair_device);
|
||||
|
||||
QObject::connect(uiState()->prime_state, &PrimeState::changed, [this] (PrimeState::Type type) {
|
||||
pair_device->setVisible(type == PrimeState::PRIME_TYPE_UNPAIRED);
|
||||
});
|
||||
|
||||
#ifndef SUNNYPILOT
|
||||
// offroad-only buttons
|
||||
|
||||
auto dcamBtn = new ButtonControl(tr("Driver Camera"), tr("PREVIEW"),
|
||||
tr("Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)"));
|
||||
connect(dcamBtn, &ButtonControl::clicked, [=]() { emit showDriverView(); });
|
||||
addItem(dcamBtn);
|
||||
#endif
|
||||
|
||||
resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), "");
|
||||
connect(resetCalibBtn, &ButtonControl::showDescriptionEvent, this, &DevicePanel::updateCalibDescription);
|
||||
connect(resetCalibBtn, &ButtonControl::clicked, [&]() {
|
||||
if (!uiState()->engaged()) {
|
||||
if (ConfirmationDialog::confirm(tr("Are you sure you want to reset calibration?"), tr("Reset"), this)) {
|
||||
// Check engaged again in case it changed while the dialog was open
|
||||
if (!uiState()->engaged()) {
|
||||
params.remove("CalibrationParams");
|
||||
params.remove("LiveTorqueParameters");
|
||||
params.remove("LiveParameters");
|
||||
params.remove("LiveParametersV2");
|
||||
params.remove("LiveDelay");
|
||||
params.putBool("OnroadCycleRequested", true);
|
||||
updateCalibDescription();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ConfirmationDialog::alert(tr("Disengage to Reset Calibration"), this);
|
||||
}
|
||||
});
|
||||
addItem(resetCalibBtn);
|
||||
|
||||
#ifndef SUNNYPILOT
|
||||
auto retrainingBtn = new ButtonControl(tr("Review Training Guide"), tr("REVIEW"), tr("Review the rules, features, and limitations of sunnypilot"));
|
||||
connect(retrainingBtn, &ButtonControl::clicked, [=]() {
|
||||
if (ConfirmationDialog::confirm(tr("Are you sure you want to review the training guide?"), tr("Review"), this)) {
|
||||
emit reviewTrainingGuide();
|
||||
}
|
||||
});
|
||||
addItem(retrainingBtn);
|
||||
|
||||
if (Hardware::TICI()) {
|
||||
auto regulatoryBtn = new ButtonControl(tr("Regulatory"), tr("VIEW"), "");
|
||||
connect(regulatoryBtn, &ButtonControl::clicked, [=]() {
|
||||
const std::string txt = util::read_file("../assets/offroad/fcc.html");
|
||||
ConfirmationDialog::rich(QString::fromStdString(txt), this);
|
||||
});
|
||||
addItem(regulatoryBtn);
|
||||
}
|
||||
|
||||
auto translateBtn = new ButtonControl(tr("Change Language"), tr("CHANGE"), "");
|
||||
connect(translateBtn, &ButtonControl::clicked, [=]() {
|
||||
QMap<QString, QString> langs = getSupportedLanguages();
|
||||
QString selection = MultiOptionDialog::getSelection(tr("Select a language"), langs.keys(), langs.key(uiState()->language), this);
|
||||
if (!selection.isEmpty()) {
|
||||
// put language setting, exit Qt UI, and trigger fast restart
|
||||
params.put("LanguageSetting", langs[selection].toStdString());
|
||||
qApp->exit(18);
|
||||
watchdog_kick(0);
|
||||
}
|
||||
});
|
||||
addItem(translateBtn);
|
||||
#endif
|
||||
|
||||
QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) {
|
||||
for (auto btn : findChildren<ButtonControl *>()) {
|
||||
if (btn != pair_device && btn != resetCalibBtn) {
|
||||
btn->setEnabled(offroad);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
#ifndef SUNNYPILOT
|
||||
// power buttons
|
||||
QHBoxLayout *power_layout = new QHBoxLayout();
|
||||
power_layout->setSpacing(30);
|
||||
|
||||
QPushButton *reboot_btn = new QPushButton(tr("Reboot"));
|
||||
reboot_btn->setObjectName("reboot_btn");
|
||||
power_layout->addWidget(reboot_btn);
|
||||
QObject::connect(reboot_btn, &QPushButton::clicked, this, &DevicePanel::reboot);
|
||||
|
||||
QPushButton *poweroff_btn = new QPushButton(tr("Power Off"));
|
||||
poweroff_btn->setObjectName("poweroff_btn");
|
||||
power_layout->addWidget(poweroff_btn);
|
||||
QObject::connect(poweroff_btn, &QPushButton::clicked, this, &DevicePanel::poweroff);
|
||||
|
||||
if (!Hardware::PC()) {
|
||||
connect(uiState(), &UIState::offroadTransition, poweroff_btn, &QPushButton::setVisible);
|
||||
}
|
||||
|
||||
setStyleSheet(R"(
|
||||
#reboot_btn { height: 120px; border-radius: 15px; background-color: #393939; }
|
||||
#reboot_btn:pressed { background-color: #4a4a4a; }
|
||||
#poweroff_btn { height: 120px; border-radius: 15px; background-color: #E22C2C; }
|
||||
#poweroff_btn:pressed { background-color: #FF2424; }
|
||||
)");
|
||||
addItem(power_layout);
|
||||
#endif
|
||||
}
|
||||
|
||||
void DevicePanel::updateCalibDescription() {
|
||||
QString desc = tr("sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down.");
|
||||
std::string calib_bytes = params.get("CalibrationParams");
|
||||
if (!calib_bytes.empty()) {
|
||||
try {
|
||||
AlignedBuffer aligned_buf;
|
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(calib_bytes.data(), calib_bytes.size()));
|
||||
auto calib = cmsg.getRoot<cereal::Event>().getLiveCalibration();
|
||||
if (calib.getCalStatus() != cereal::LiveCalibrationData::Status::UNCALIBRATED) {
|
||||
double pitch = calib.getRpyCalib()[1] * (180 / M_PI);
|
||||
double yaw = calib.getRpyCalib()[2] * (180 / M_PI);
|
||||
desc += tr(" Your device is pointed %1° %2 and %3° %4.")
|
||||
.arg(QString::number(std::abs(pitch), 'g', 1), pitch > 0 ? tr("down") : tr("up"),
|
||||
QString::number(std::abs(yaw), 'g', 1), yaw > 0 ? tr("left") : tr("right"));
|
||||
}
|
||||
} catch (kj::Exception) {
|
||||
qInfo() << "invalid CalibrationParams";
|
||||
}
|
||||
}
|
||||
|
||||
int lag_perc = 0;
|
||||
std::string lag_bytes = params.get("LiveDelay");
|
||||
if (!lag_bytes.empty()) {
|
||||
try {
|
||||
AlignedBuffer aligned_buf;
|
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(lag_bytes.data(), lag_bytes.size()));
|
||||
lag_perc = cmsg.getRoot<cereal::Event>().getLiveDelay().getCalPerc();
|
||||
} catch (kj::Exception) {
|
||||
qInfo() << "invalid LiveDelay";
|
||||
}
|
||||
}
|
||||
if (lag_perc < 100) {
|
||||
desc += tr("\n\nSteering lag calibration is %1% complete.").arg(lag_perc);
|
||||
} else {
|
||||
desc += tr("\n\nSteering lag calibration is complete.");
|
||||
}
|
||||
|
||||
std::string torque_bytes = params.get("LiveTorqueParameters");
|
||||
if (!torque_bytes.empty()) {
|
||||
try {
|
||||
AlignedBuffer aligned_buf;
|
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(torque_bytes.data(), torque_bytes.size()));
|
||||
auto torque = cmsg.getRoot<cereal::Event>().getLiveTorqueParameters();
|
||||
// don't add for non-torque cars
|
||||
if (torque.getUseParams()) {
|
||||
int torque_perc = torque.getCalPerc();
|
||||
if (torque_perc < 100) {
|
||||
desc += tr(" Steering torque response calibration is %1% complete.").arg(torque_perc);
|
||||
} else {
|
||||
desc += tr(" Steering torque response calibration is complete.");
|
||||
}
|
||||
}
|
||||
} catch (kj::Exception) {
|
||||
qInfo() << "invalid LiveTorqueParameters";
|
||||
}
|
||||
}
|
||||
|
||||
desc += "\n\n";
|
||||
desc += tr("openpilot is continuously calibrating, resetting is rarely required. "
|
||||
"Resetting calibration will restart openpilot if the car is powered on.");
|
||||
resetCalibBtn->setDescription(desc);
|
||||
}
|
||||
|
||||
void DevicePanel::reboot() {
|
||||
if (!uiState()->engaged()) {
|
||||
if (ConfirmationDialog::confirm(tr("Are you sure you want to reboot?"), tr("Reboot"), this)) {
|
||||
// Check engaged again in case it changed while the dialog was open
|
||||
if (!uiState()->engaged()) {
|
||||
params.putBool("DoReboot", true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ConfirmationDialog::alert(tr("Disengage to Reboot"), this);
|
||||
}
|
||||
}
|
||||
|
||||
void DevicePanel::poweroff() {
|
||||
if (!uiState()->engaged()) {
|
||||
if (ConfirmationDialog::confirm(tr("Are you sure you want to power off?"), tr("Power Off"), this)) {
|
||||
// Check engaged again in case it changed while the dialog was open
|
||||
if (!uiState()->engaged()) {
|
||||
params.putBool("DoShutdown", true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ConfirmationDialog::alert(tr("Disengage to Power Off"), this);
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsWindow::showEvent(QShowEvent *event) {
|
||||
setCurrentPanel(0);
|
||||
}
|
||||
|
||||
void SettingsWindow::setCurrentPanel(int index, const QString ¶m) {
|
||||
if (!param.isEmpty()) {
|
||||
// Check if param ends with "Panel" to determine if it's a panel name
|
||||
if (param.endsWith("Panel")) {
|
||||
QString panelName = param;
|
||||
panelName.chop(5); // Remove "Panel" suffix
|
||||
|
||||
// Find the panel by name
|
||||
for (int i = 0; i < nav_btns->buttons().size(); i++) {
|
||||
bool panel_trimmed = false;
|
||||
#ifdef SUNNYPILOT
|
||||
panel_trimmed = nav_btns->buttons()[i]->text().trimmed() == tr(panelName.toStdString().c_str());
|
||||
#endif
|
||||
if ((nav_btns->buttons()[i]->text() == tr(panelName.toStdString().c_str())) || panel_trimmed) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
emit expandToggleDescription(param);
|
||||
emit scrollToToggle(param);
|
||||
}
|
||||
}
|
||||
|
||||
panel_widget->setCurrentIndex(index);
|
||||
nav_btns->buttons()[index]->setChecked(true);
|
||||
}
|
||||
|
||||
SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) {
|
||||
#ifndef SUNNYPILOT
|
||||
// setup two main layouts
|
||||
sidebar_widget = new QWidget;
|
||||
QVBoxLayout *sidebar_layout = new QVBoxLayout(sidebar_widget);
|
||||
panel_widget = new QStackedWidget();
|
||||
|
||||
// close button
|
||||
QPushButton *close_btn = new QPushButton(tr("×"));
|
||||
close_btn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
font-size: 140px;
|
||||
padding-bottom: 20px;
|
||||
border-radius: 100px;
|
||||
background-color: #292929;
|
||||
font-weight: 400;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #3B3B3B;
|
||||
}
|
||||
)");
|
||||
close_btn->setFixedSize(200, 200);
|
||||
sidebar_layout->addSpacing(45);
|
||||
sidebar_layout->addWidget(close_btn, 0, Qt::AlignCenter);
|
||||
QObject::connect(close_btn, &QPushButton::clicked, this, &SettingsWindow::closeSettings);
|
||||
|
||||
// setup panels
|
||||
DevicePanel *device = new DevicePanel(this);
|
||||
QObject::connect(device, &DevicePanel::reviewTrainingGuide, this, &SettingsWindow::reviewTrainingGuide);
|
||||
QObject::connect(device, &DevicePanel::showDriverView, this, &SettingsWindow::showDriverView);
|
||||
|
||||
TogglesPanel *toggles = new TogglesPanel(this);
|
||||
QObject::connect(this, &SettingsWindow::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription);
|
||||
QObject::connect(this, &SettingsWindow::scrollToToggle, toggles, &TogglesPanel::scrollToToggle);
|
||||
|
||||
auto networking = new Networking(this);
|
||||
QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &Networking::setPrimeType);
|
||||
|
||||
QList<QPair<QString, QWidget *>> panels = {
|
||||
{tr("Device"), device},
|
||||
{tr("Network"), networking},
|
||||
{tr("Toggles"), toggles},
|
||||
{tr("Software"), new SoftwarePanel(this)},
|
||||
{tr("Firehose"), new FirehosePanel(this)},
|
||||
{tr("Developer"), new DeveloperPanel(this)},
|
||||
};
|
||||
|
||||
nav_btns = new QButtonGroup(this);
|
||||
for (auto &[name, panel] : panels) {
|
||||
QPushButton *btn = new QPushButton(name);
|
||||
btn->setCheckable(true);
|
||||
btn->setChecked(nav_btns->buttons().size() == 0);
|
||||
btn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
color: grey;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 65px;
|
||||
font-weight: 500;
|
||||
}
|
||||
QPushButton:checked {
|
||||
color: white;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
color: #ADADAD;
|
||||
}
|
||||
)");
|
||||
btn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
|
||||
nav_btns->addButton(btn);
|
||||
sidebar_layout->addWidget(btn, 0, Qt::AlignRight);
|
||||
|
||||
const int lr_margin = name != tr("Network") ? 50 : 0; // Network panel handles its own margins
|
||||
panel->setContentsMargins(lr_margin, 25, lr_margin, 25);
|
||||
|
||||
ScrollView *panel_frame = new ScrollView(panel, this);
|
||||
panel_widget->addWidget(panel_frame);
|
||||
|
||||
QObject::connect(btn, &QPushButton::clicked, [=, w = panel_frame]() {
|
||||
btn->setChecked(true);
|
||||
panel_widget->setCurrentWidget(w);
|
||||
});
|
||||
}
|
||||
sidebar_layout->setContentsMargins(50, 50, 100, 50);
|
||||
|
||||
// main settings layout, sidebar + main panel
|
||||
QHBoxLayout *main_layout = new QHBoxLayout(this);
|
||||
|
||||
sidebar_widget->setFixedWidth(500);
|
||||
main_layout->addWidget(sidebar_widget);
|
||||
main_layout->addWidget(panel_widget);
|
||||
|
||||
setStyleSheet(R"(
|
||||
* {
|
||||
color: white;
|
||||
font-size: 50px;
|
||||
}
|
||||
SettingsWindow {
|
||||
background-color: black;
|
||||
}
|
||||
QStackedWidget, ScrollView {
|
||||
background-color: #292929;
|
||||
border-radius: 30px;
|
||||
}
|
||||
)");
|
||||
#endif
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include <QButtonGroup>
|
||||
#include <QFrame>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QStackedWidget>
|
||||
#include <QWidget>
|
||||
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
|
||||
#ifdef SUNNYPILOT
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
#define ListWidget ListWidgetSP
|
||||
#define ParamControl ParamControlSP
|
||||
#define ButtonControl ButtonControlSP
|
||||
#define ButtonParamControl ButtonParamControlSP
|
||||
#define ToggleControl ToggleControlSP
|
||||
#define LabelControl LabelControlSP
|
||||
#else
|
||||
#include "selfdrive/ui/ui.h"
|
||||
#include "selfdrive/ui/qt/widgets/controls.h"
|
||||
#endif
|
||||
|
||||
// ********** settings window + top-level panels **********
|
||||
class SettingsWindow : public QFrame {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SettingsWindow(QWidget *parent = 0);
|
||||
void setCurrentPanel(int index, const QString ¶m = "");
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
|
||||
signals:
|
||||
void closeSettings();
|
||||
void reviewTrainingGuide();
|
||||
void showDriverView();
|
||||
void expandToggleDescription(const QString ¶m);
|
||||
void scrollToToggle(const QString ¶m);
|
||||
|
||||
protected:
|
||||
QPushButton *sidebar_alert_widget;
|
||||
QWidget *sidebar_widget;
|
||||
QButtonGroup *nav_btns;
|
||||
QStackedWidget *panel_widget;
|
||||
};
|
||||
|
||||
class DevicePanel : public ListWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DevicePanel(SettingsWindow *parent);
|
||||
|
||||
signals:
|
||||
void reviewTrainingGuide();
|
||||
void showDriverView();
|
||||
|
||||
protected slots:
|
||||
void poweroff();
|
||||
void reboot();
|
||||
void updateCalibDescription();
|
||||
|
||||
protected:
|
||||
Params params;
|
||||
ButtonControl *pair_device;
|
||||
ButtonControl *resetCalibBtn;
|
||||
};
|
||||
|
||||
class TogglesPanel : public ListWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TogglesPanel(SettingsWindow *parent);
|
||||
void showEvent(QShowEvent *event) override;
|
||||
|
||||
public slots:
|
||||
void expandToggleDescription(const QString ¶m);
|
||||
void scrollToToggle(const QString ¶m);
|
||||
|
||||
protected slots:
|
||||
virtual void updateState(const UIState &s);
|
||||
|
||||
protected:
|
||||
Params params;
|
||||
std::map<std::string, ParamControl*> toggles;
|
||||
ButtonParamControl *long_personality_setting;
|
||||
|
||||
virtual void updateToggles();
|
||||
};
|
||||
|
||||
class SoftwarePanel : public ListWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SoftwarePanel(QWidget* parent = nullptr);
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
virtual void updateLabels();
|
||||
void checkForUpdates();
|
||||
|
||||
bool is_onroad = false;
|
||||
|
||||
QLabel *onroadLbl;
|
||||
LabelControl *versionLbl;
|
||||
ButtonControl *installBtn;
|
||||
ButtonControl *downloadBtn;
|
||||
ButtonControl *targetBranchBtn;
|
||||
|
||||
Params params;
|
||||
ParamWatcher *fs_watch;
|
||||
};
|
||||
|
||||
// Forward declaration
|
||||
class FirehosePanel;
|
||||
@@ -1,154 +0,0 @@
|
||||
#include "selfdrive/ui/qt/offroad/settings.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QLabel>
|
||||
|
||||
#include "common/params.h"
|
||||
#include "common/util.h"
|
||||
#include "selfdrive/ui/ui.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
#include "selfdrive/ui/qt/widgets/controls.h"
|
||||
#include "selfdrive/ui/qt/widgets/input.h"
|
||||
#include "system/hardware/hw.h"
|
||||
|
||||
|
||||
void SoftwarePanel::checkForUpdates() {
|
||||
std::system("pkill -SIGUSR1 -f system.updated.updated");
|
||||
}
|
||||
|
||||
SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) {
|
||||
onroadLbl = new QLabel(tr("Updates are only downloaded while the car is off."));
|
||||
onroadLbl->setStyleSheet("font-size: 50px; font-weight: 400; text-align: left; padding-top: 30px; padding-bottom: 30px;");
|
||||
addItem(onroadLbl);
|
||||
|
||||
// current version
|
||||
versionLbl = new LabelControl(tr("Current Version"), "");
|
||||
addItem(versionLbl);
|
||||
|
||||
// download update btn
|
||||
downloadBtn = new ButtonControl(tr("Download"), tr("CHECK"));
|
||||
connect(downloadBtn, &ButtonControl::clicked, [=]() {
|
||||
downloadBtn->setEnabled(false);
|
||||
if (downloadBtn->text() == tr("CHECK")) {
|
||||
checkForUpdates();
|
||||
} else {
|
||||
std::system("pkill -SIGHUP -f system.updated.updated");
|
||||
}
|
||||
});
|
||||
addItem(downloadBtn);
|
||||
|
||||
// install update btn
|
||||
installBtn = new ButtonControl(tr("Install Update"), tr("INSTALL"));
|
||||
connect(installBtn, &ButtonControl::clicked, [=]() {
|
||||
installBtn->setEnabled(false);
|
||||
params.putBool("DoReboot", true);
|
||||
});
|
||||
addItem(installBtn);
|
||||
|
||||
// branch selecting
|
||||
targetBranchBtn = new ButtonControl(tr("Target Branch"), tr("SELECT"));
|
||||
connect(targetBranchBtn, &ButtonControl::clicked, [=]() {
|
||||
auto current = params.get("GitBranch");
|
||||
QStringList branches = QString::fromStdString(params.get("UpdaterAvailableBranches")).split(",");
|
||||
for (QString b : {current.c_str(), "devel-staging", "devel", "nightly", "nightly-dev", "master"}) {
|
||||
auto i = branches.indexOf(b);
|
||||
if (i >= 0) {
|
||||
branches.removeAt(i);
|
||||
branches.insert(0, b);
|
||||
}
|
||||
}
|
||||
|
||||
QString cur = QString::fromStdString(params.get("UpdaterTargetBranch"));
|
||||
QString selection = MultiOptionDialog::getSelection(tr("Select a branch"), branches, cur, this);
|
||||
if (!selection.isEmpty()) {
|
||||
params.put("UpdaterTargetBranch", selection.toStdString());
|
||||
targetBranchBtn->setValue(QString::fromStdString(params.get("UpdaterTargetBranch")));
|
||||
checkForUpdates();
|
||||
}
|
||||
});
|
||||
addItem(targetBranchBtn);
|
||||
|
||||
// uninstall button
|
||||
auto uninstallBtn = new ButtonControl(tr("Uninstall %1").arg(getBrand()), tr("UNINSTALL"));
|
||||
connect(uninstallBtn, &ButtonControl::clicked, [&]() {
|
||||
if (ConfirmationDialog::confirm(tr("Are you sure you want to uninstall?"), tr("Uninstall"), this)) {
|
||||
params.putBool("DoUninstall", true);
|
||||
}
|
||||
});
|
||||
addItem(uninstallBtn);
|
||||
|
||||
fs_watch = new ParamWatcher(this);
|
||||
QObject::connect(fs_watch, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) {
|
||||
updateLabels();
|
||||
});
|
||||
|
||||
connect(uiState(), &UIState::offroadTransition, [=](bool offroad) {
|
||||
is_onroad = !offroad;
|
||||
updateLabels();
|
||||
});
|
||||
|
||||
updateLabels();
|
||||
}
|
||||
|
||||
void SoftwarePanel::showEvent(QShowEvent *event) {
|
||||
// nice for testing on PC
|
||||
installBtn->setEnabled(true);
|
||||
|
||||
updateLabels();
|
||||
}
|
||||
|
||||
void SoftwarePanel::updateLabels() {
|
||||
// add these back in case the files got removed
|
||||
fs_watch->addParam("LastUpdateTime");
|
||||
fs_watch->addParam("UpdateFailedCount");
|
||||
fs_watch->addParam("UpdaterState");
|
||||
fs_watch->addParam("UpdateAvailable");
|
||||
|
||||
if (!isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// updater only runs offroad
|
||||
onroadLbl->setVisible(is_onroad);
|
||||
downloadBtn->setVisible(!is_onroad);
|
||||
|
||||
// download update
|
||||
QString updater_state = QString::fromStdString(params.get("UpdaterState"));
|
||||
bool failed = std::atoi(params.get("UpdateFailedCount").c_str()) > 0;
|
||||
if (updater_state != "idle") {
|
||||
downloadBtn->setEnabled(false);
|
||||
downloadBtn->setValue(updater_state);
|
||||
} else {
|
||||
if (failed) {
|
||||
downloadBtn->setText(tr("CHECK"));
|
||||
downloadBtn->setValue(tr("failed to check for update"));
|
||||
} else if (params.getBool("UpdaterFetchAvailable")) {
|
||||
downloadBtn->setText(tr("DOWNLOAD"));
|
||||
downloadBtn->setValue(tr("update available"));
|
||||
} else {
|
||||
QString lastUpdate = tr("never");
|
||||
auto tm = params.get("LastUpdateTime");
|
||||
if (!tm.empty()) {
|
||||
lastUpdate = timeAgo(QDateTime::fromString(QString::fromStdString(tm + "Z"), Qt::ISODate));
|
||||
}
|
||||
downloadBtn->setText(tr("CHECK"));
|
||||
downloadBtn->setValue(tr("up to date, last checked %1").arg(lastUpdate));
|
||||
}
|
||||
downloadBtn->setEnabled(true);
|
||||
}
|
||||
targetBranchBtn->setValue(QString::fromStdString(params.get("UpdaterTargetBranch")));
|
||||
|
||||
// current + new versions
|
||||
versionLbl->setText(QString::fromStdString(params.get("UpdaterCurrentDescription")));
|
||||
versionLbl->setDescription(QString::fromStdString(params.get("UpdaterCurrentReleaseNotes")));
|
||||
|
||||
installBtn->setVisible(!is_onroad && params.getBool("UpdateAvailable"));
|
||||
installBtn->setValue(QString::fromStdString(params.get("UpdaterNewDescription")));
|
||||
installBtn->setDescription(QString::fromStdString(params.get("UpdaterNewReleaseNotes")));
|
||||
|
||||
update();
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
#include "selfdrive/ui/qt/onroad/alerts.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <map>
|
||||
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
|
||||
void OnroadAlerts::updateState(const UIState &s) {
|
||||
Alert a = getAlert(*(s.sm), s.scene.started_frame);
|
||||
if (!alert.equal(a)) {
|
||||
alert = a;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void OnroadAlerts::clear() {
|
||||
alert = {};
|
||||
update();
|
||||
}
|
||||
|
||||
OnroadAlerts::Alert OnroadAlerts::getAlert(const SubMaster &sm, uint64_t started_frame) {
|
||||
const cereal::SelfdriveState::Reader &ss = sm["selfdriveState"].getSelfdriveState();
|
||||
const uint64_t selfdrive_frame = sm.rcv_frame("selfdriveState");
|
||||
|
||||
Alert a = {};
|
||||
if (selfdrive_frame >= started_frame) { // Don't get old alert.
|
||||
a = {ss.getAlertText1().cStr(), ss.getAlertText2().cStr(),
|
||||
ss.getAlertType().cStr(), ss.getAlertSize(), ss.getAlertStatus()};
|
||||
}
|
||||
|
||||
if (!sm.updated("selfdriveState") && (sm.frame - started_frame) > 5 * UI_FREQ) {
|
||||
const int SELFDRIVE_STATE_TIMEOUT = 5;
|
||||
const int ss_missing = (nanos_since_boot() - sm.rcv_time("selfdriveState")) / 1e9;
|
||||
|
||||
// Handle selfdrive timeout
|
||||
if (selfdrive_frame < started_frame) {
|
||||
// car is started, but selfdriveState hasn't been seen at all
|
||||
a = {tr("sunnypilot Unavailable"), tr("Waiting to start"),
|
||||
"selfdriveWaiting", cereal::SelfdriveState::AlertSize::MID,
|
||||
cereal::SelfdriveState::AlertStatus::NORMAL};
|
||||
} else if (ss_missing > SELFDRIVE_STATE_TIMEOUT && !Hardware::PC()) {
|
||||
// car is started, but selfdrive is lagging or died
|
||||
if (ss.getEnabled() && (ss_missing - SELFDRIVE_STATE_TIMEOUT) < 10) {
|
||||
a = {tr("TAKE CONTROL IMMEDIATELY"), tr("System Unresponsive"),
|
||||
"selfdriveUnresponsive", cereal::SelfdriveState::AlertSize::FULL,
|
||||
cereal::SelfdriveState::AlertStatus::CRITICAL};
|
||||
} else {
|
||||
a = {tr("System Unresponsive"), tr("Reboot Device"),
|
||||
"selfdriveUnresponsivePermanent", cereal::SelfdriveState::AlertSize::MID,
|
||||
cereal::SelfdriveState::AlertStatus::NORMAL};
|
||||
}
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
void OnroadAlerts::paintEvent(QPaintEvent *event) {
|
||||
if (alert.size == cereal::SelfdriveState::AlertSize::NONE) {
|
||||
return;
|
||||
}
|
||||
static std::map<cereal::SelfdriveState::AlertSize, const int> alert_heights = {
|
||||
{cereal::SelfdriveState::AlertSize::SMALL, 271},
|
||||
{cereal::SelfdriveState::AlertSize::MID, 420},
|
||||
{cereal::SelfdriveState::AlertSize::FULL, height()},
|
||||
};
|
||||
int h = alert_heights[alert.size];
|
||||
|
||||
int margin = 40;
|
||||
int radius = 30;
|
||||
if (alert.size == cereal::SelfdriveState::AlertSize::FULL) {
|
||||
margin = 0;
|
||||
radius = 0;
|
||||
}
|
||||
QRect r = QRect(0 + margin, height() - h + margin, width() - margin*2, h - margin*2);
|
||||
|
||||
QPainter p(this);
|
||||
|
||||
// draw background + gradient
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
p.setBrush(QBrush(alert_colors[alert.status]));
|
||||
p.drawRoundedRect(r, radius, radius);
|
||||
|
||||
QLinearGradient g(0, r.y(), 0, r.bottom());
|
||||
g.setColorAt(0, QColor::fromRgbF(0, 0, 0, 0.05));
|
||||
g.setColorAt(1, QColor::fromRgbF(0, 0, 0, 0.35));
|
||||
|
||||
p.setCompositionMode(QPainter::CompositionMode_DestinationOver);
|
||||
p.setBrush(QBrush(g));
|
||||
p.drawRoundedRect(r, radius, radius);
|
||||
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
|
||||
// text
|
||||
const QPoint c = r.center();
|
||||
p.setPen(QColor(0xff, 0xff, 0xff));
|
||||
p.setRenderHint(QPainter::TextAntialiasing);
|
||||
if (alert.size == cereal::SelfdriveState::AlertSize::SMALL) {
|
||||
p.setFont(InterFont(74, QFont::DemiBold));
|
||||
p.drawText(r, Qt::AlignCenter, alert.text1);
|
||||
} else if (alert.size == cereal::SelfdriveState::AlertSize::MID) {
|
||||
p.setFont(InterFont(88, QFont::Bold));
|
||||
p.drawText(QRect(0, c.y() - 125, width(), 150), Qt::AlignHCenter | Qt::AlignTop, alert.text1);
|
||||
p.setFont(InterFont(66));
|
||||
p.drawText(QRect(0, c.y() + 21, width(), 90), Qt::AlignHCenter, alert.text2);
|
||||
} else if (alert.size == cereal::SelfdriveState::AlertSize::FULL) {
|
||||
bool l = alert.text1.length() > 15;
|
||||
p.setFont(InterFont(l ? 132 : 177, QFont::Bold));
|
||||
p.drawText(QRect(0, r.y() + (l ? 240 : 270), width(), 600), Qt::AlignHCenter | Qt::TextWordWrap, alert.text1);
|
||||
p.setFont(InterFont(88));
|
||||
p.drawText(QRect(0, r.height() - (l ? 361 : 420), width(), 300), Qt::AlignHCenter | Qt::TextWordWrap, alert.text2);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "selfdrive/ui/ui.h"
|
||||
|
||||
class OnroadAlerts : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
OnroadAlerts(QWidget *parent = 0) : QWidget(parent) {}
|
||||
void updateState(const UIState &s);
|
||||
void clear();
|
||||
|
||||
protected:
|
||||
struct Alert {
|
||||
QString text1;
|
||||
QString text2;
|
||||
QString type;
|
||||
cereal::SelfdriveState::AlertSize size;
|
||||
cereal::SelfdriveState::AlertStatus status;
|
||||
|
||||
bool equal(const Alert &other) const {
|
||||
return text1 == other.text1 && text2 == other.text2 && type == other.type;
|
||||
}
|
||||
};
|
||||
|
||||
const QMap<cereal::SelfdriveState::AlertStatus, QColor> alert_colors = {
|
||||
{cereal::SelfdriveState::AlertStatus::NORMAL, QColor(0x15, 0x15, 0x15, 0xf1)},
|
||||
{cereal::SelfdriveState::AlertStatus::USER_PROMPT, QColor(0xDA, 0x6F, 0x25, 0xf1)},
|
||||
{cereal::SelfdriveState::AlertStatus::CRITICAL, QColor(0xC9, 0x22, 0x31, 0xf1)},
|
||||
};
|
||||
|
||||
void paintEvent(QPaintEvent*) override;
|
||||
OnroadAlerts::Alert getAlert(const SubMaster &sm, uint64_t started_frame);
|
||||
|
||||
QColor bg;
|
||||
Alert alert = {};
|
||||
};
|
||||
@@ -1,157 +0,0 @@
|
||||
|
||||
#include "selfdrive/ui/qt/onroad/annotated_camera.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "common/swaglog.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
|
||||
// Window that shows camera view and variety of info drawn on top
|
||||
AnnotatedCameraWidget::AnnotatedCameraWidget(VisionStreamType type, QWidget *parent)
|
||||
: fps_filter(UI_FREQ, 3, 1. / UI_FREQ), CameraWidget("camerad", type, parent) {
|
||||
pm = std::make_unique<PubMaster>(std::vector<const char*>{"uiDebug"});
|
||||
|
||||
main_layout = new QVBoxLayout(this);
|
||||
main_layout->setMargin(UI_BORDER_SIZE);
|
||||
main_layout->setSpacing(0);
|
||||
|
||||
experimental_btn = new ExperimentalButton(this);
|
||||
main_layout->addWidget(experimental_btn, 0, Qt::AlignTop | Qt::AlignRight);
|
||||
}
|
||||
|
||||
void AnnotatedCameraWidget::updateState(const UIState &s) {
|
||||
// update engageability/experimental mode button
|
||||
experimental_btn->updateState(s);
|
||||
dmon.updateState(s);
|
||||
}
|
||||
|
||||
void AnnotatedCameraWidget::initializeGL() {
|
||||
CameraWidget::initializeGL();
|
||||
qInfo() << "OpenGL version:" << QString((const char*)glGetString(GL_VERSION));
|
||||
qInfo() << "OpenGL vendor:" << QString((const char*)glGetString(GL_VENDOR));
|
||||
qInfo() << "OpenGL renderer:" << QString((const char*)glGetString(GL_RENDERER));
|
||||
qInfo() << "OpenGL language version:" << QString((const char*)glGetString(GL_SHADING_LANGUAGE_VERSION));
|
||||
|
||||
prev_draw_t = millis_since_boot();
|
||||
setBackgroundColor(bg_colors[STATUS_DISENGAGED]);
|
||||
}
|
||||
|
||||
mat4 AnnotatedCameraWidget::calcFrameMatrix() {
|
||||
// Project point at "infinity" to compute x and y offsets
|
||||
// to ensure this ends up in the middle of the screen
|
||||
// for narrow come and a little lower for wide cam.
|
||||
// TODO: use proper perspective transform?
|
||||
|
||||
// Select intrinsic matrix and calibration based on camera type
|
||||
auto *s = uiState();
|
||||
bool wide_cam = active_stream_type == VISION_STREAM_WIDE_ROAD;
|
||||
const auto &intrinsic_matrix = wide_cam ? ECAM_INTRINSIC_MATRIX : FCAM_INTRINSIC_MATRIX;
|
||||
const auto &calibration = wide_cam ? s->scene.view_from_wide_calib : s->scene.view_from_calib;
|
||||
|
||||
// Compute the calibration transformation matrix
|
||||
const auto calib_transform = intrinsic_matrix * calibration;
|
||||
|
||||
float zoom = wide_cam ? 2.0 : 1.1;
|
||||
Eigen::Vector3f inf(1000., 0., 0.);
|
||||
auto Kep = calib_transform * inf;
|
||||
|
||||
int w = width(), h = height();
|
||||
float center_x = intrinsic_matrix(0, 2);
|
||||
float center_y = intrinsic_matrix(1, 2);
|
||||
|
||||
float max_x_offset = center_x * zoom - w / 2 - 5;
|
||||
float max_y_offset = center_y * zoom - h / 2 - 5;
|
||||
float x_offset = std::clamp<float>((Kep.x() / Kep.z() - center_x) * zoom, -max_x_offset, max_x_offset);
|
||||
float y_offset = std::clamp<float>((Kep.y() / Kep.z() - center_y) * zoom, -max_y_offset, max_y_offset);
|
||||
|
||||
// Apply transformation such that video pixel coordinates match video
|
||||
// 1) Put (0, 0) in the middle of the video
|
||||
// 2) Apply same scaling as video
|
||||
// 3) Put (0, 0) in top left corner of video
|
||||
Eigen::Matrix3f video_transform =(Eigen::Matrix3f() <<
|
||||
zoom, 0.0f, (w / 2 - x_offset) - (center_x * zoom),
|
||||
0.0f, zoom, (h / 2 - y_offset) - (center_y * zoom),
|
||||
0.0f, 0.0f, 1.0f).finished();
|
||||
|
||||
model.setTransform(video_transform * calib_transform);
|
||||
|
||||
float zx = zoom * 2 * center_x / w;
|
||||
float zy = zoom * 2 * center_y / h;
|
||||
return mat4{{
|
||||
zx, 0.0, 0.0, -x_offset / w * 2,
|
||||
0.0, zy, 0.0, y_offset / h * 2,
|
||||
0.0, 0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0,
|
||||
}};
|
||||
}
|
||||
|
||||
void AnnotatedCameraWidget::paintGL() {
|
||||
UIState *s = uiState();
|
||||
SubMaster &sm = *(s->sm);
|
||||
const double start_draw_t = millis_since_boot();
|
||||
|
||||
// draw camera frame
|
||||
{
|
||||
std::lock_guard lk(frame_lock);
|
||||
|
||||
if (frames.empty()) {
|
||||
if (skip_frame_count > 0) {
|
||||
skip_frame_count--;
|
||||
qDebug() << "skipping frame, not ready";
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// skip drawing up to this many frames if we're
|
||||
// missing camera frames. this smooths out the
|
||||
// transitions from the narrow and wide cameras
|
||||
skip_frame_count = 5;
|
||||
}
|
||||
|
||||
// Wide or narrow cam dependent on speed
|
||||
bool has_wide_cam = available_streams.count(VISION_STREAM_WIDE_ROAD);
|
||||
if (has_wide_cam) {
|
||||
float v_ego = sm["carState"].getCarState().getVEgo();
|
||||
if ((v_ego < 10) || available_streams.size() == 1) {
|
||||
wide_cam_requested = true;
|
||||
} else if (v_ego > 15) {
|
||||
wide_cam_requested = false;
|
||||
}
|
||||
wide_cam_requested = wide_cam_requested && sm["selfdriveState"].getSelfdriveState().getExperimentalMode();
|
||||
}
|
||||
CameraWidget::setStreamType(wide_cam_requested ? VISION_STREAM_WIDE_ROAD : VISION_STREAM_ROAD);
|
||||
CameraWidget::setFrameId(sm["modelV2"].getModelV2().getFrameId());
|
||||
CameraWidget::paintGL();
|
||||
}
|
||||
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.setPen(Qt::NoPen);
|
||||
|
||||
model.draw(painter, rect());
|
||||
dmon.draw(painter, rect());
|
||||
hud.updateState(*s);
|
||||
hud.draw(painter, rect());
|
||||
|
||||
double cur_draw_t = millis_since_boot();
|
||||
double dt = cur_draw_t - prev_draw_t;
|
||||
double fps = fps_filter.update(1. / dt * 1000);
|
||||
if (fps < 15) {
|
||||
LOGW("slow frame rate: %.2f fps", fps);
|
||||
}
|
||||
prev_draw_t = cur_draw_t;
|
||||
|
||||
// publish debug msg
|
||||
MessageBuilder msg;
|
||||
auto m = msg.initEvent().initUiDebug();
|
||||
m.setDrawTimeMillis(cur_draw_t - start_draw_t);
|
||||
pm->send("uiDebug", msg);
|
||||
}
|
||||
|
||||
void AnnotatedCameraWidget::showEvent(QShowEvent *event) {
|
||||
CameraWidget::showEvent(event);
|
||||
|
||||
ui_update_params(uiState());
|
||||
prev_draw_t = millis_since_boot();
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <memory>
|
||||
#include "selfdrive/ui/qt/onroad/driver_monitoring.h"
|
||||
#include "selfdrive/ui/qt/onroad/model.h"
|
||||
#include "selfdrive/ui/qt/widgets/cameraview.h"
|
||||
|
||||
#ifdef SUNNYPILOT
|
||||
#include "selfdrive/ui/sunnypilot/qt/onroad/buttons.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/onroad/hud.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/onroad/model.h"
|
||||
#define ExperimentalButton ExperimentalButtonSP
|
||||
#define ModelRenderer ModelRendererSP
|
||||
#define HudRenderer HudRendererSP
|
||||
#else
|
||||
#include "selfdrive/ui/qt/onroad/buttons.h"
|
||||
#include "selfdrive/ui/qt/onroad/hud.h"
|
||||
#endif
|
||||
|
||||
class AnnotatedCameraWidget : public CameraWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AnnotatedCameraWidget(VisionStreamType type, QWidget* parent = 0);
|
||||
virtual ~AnnotatedCameraWidget() = default;
|
||||
virtual void updateState(const UIState &s);
|
||||
|
||||
private:
|
||||
QVBoxLayout *main_layout;
|
||||
ExperimentalButton *experimental_btn;
|
||||
DriverMonitorRenderer dmon;
|
||||
HudRenderer hud;
|
||||
ModelRenderer model;
|
||||
std::unique_ptr<PubMaster> pm;
|
||||
|
||||
int skip_frame_count = 0;
|
||||
bool wide_cam_requested = false;
|
||||
|
||||
protected:
|
||||
void paintGL() override;
|
||||
void initializeGL() override;
|
||||
void showEvent(QShowEvent *event) override;
|
||||
mat4 calcFrameMatrix() override;
|
||||
|
||||
double prev_draw_t = 0;
|
||||
FirstOrderFilter fps_filter;
|
||||
};
|
||||
@@ -1,53 +0,0 @@
|
||||
#include "selfdrive/ui/qt/onroad/buttons.h"
|
||||
|
||||
#include <QPainter>
|
||||
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
|
||||
void drawIcon(QPainter &p, const QPoint ¢er, const QPixmap &img, const QBrush &bg, float opacity) {
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
p.setOpacity(1.0); // bg dictates opacity of ellipse
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(bg);
|
||||
p.drawEllipse(center, btn_size / 2, btn_size / 2);
|
||||
p.setOpacity(opacity);
|
||||
p.drawPixmap(center - QPoint(img.width() / 2, img.height() / 2), img);
|
||||
p.setOpacity(1.0);
|
||||
}
|
||||
|
||||
// ExperimentalButton
|
||||
ExperimentalButton::ExperimentalButton(QWidget *parent) : experimental_mode(false), engageable(false), QPushButton(parent) {
|
||||
setFixedSize(btn_size, btn_size);
|
||||
|
||||
engage_img = loadPixmap("../assets/icons/chffr_wheel.png", {img_size, img_size});
|
||||
experimental_img = loadPixmap("../assets/icons/experimental.svg", {img_size, img_size});
|
||||
QObject::connect(this, &QPushButton::clicked, this, &ExperimentalButton::changeMode);
|
||||
}
|
||||
|
||||
void ExperimentalButton::changeMode() {
|
||||
const auto cp = (*uiState()->sm)["carParams"].getCarParams();
|
||||
bool can_change = hasLongitudinalControl(cp) && params.getBool("ExperimentalModeConfirmed");
|
||||
if (can_change) {
|
||||
params.putBool("ExperimentalMode", !experimental_mode);
|
||||
}
|
||||
}
|
||||
|
||||
void ExperimentalButton::updateState(const UIState &s) {
|
||||
const auto cs = (*s.sm)["selfdriveState"].getSelfdriveState();
|
||||
bool eng = cs.getEngageable() || cs.getEnabled();
|
||||
if ((cs.getExperimentalMode() != experimental_mode) || (eng != engageable)) {
|
||||
engageable = eng;
|
||||
experimental_mode = cs.getExperimentalMode();
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void ExperimentalButton::paintEvent(QPaintEvent *event) {
|
||||
QPainter p(this);
|
||||
drawButton(p);
|
||||
}
|
||||
|
||||
void ExperimentalButton::drawButton(QPainter &p) {
|
||||
QPixmap img = experimental_mode ? experimental_img : engage_img;
|
||||
drawIcon(p, QPoint(btn_size / 2, btn_size / 2), img, QColor(0, 0, 0, 166), (isDown() || !engageable) ? 0.6 : 1.0);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QPushButton>
|
||||
|
||||
#ifdef SUNNYPILOT
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
#else
|
||||
#include "selfdrive/ui/ui.h"
|
||||
#endif
|
||||
|
||||
const int btn_size = 192;
|
||||
const int img_size = (btn_size / 4) * 3;
|
||||
|
||||
class ExperimentalButton : public QPushButton {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ExperimentalButton(QWidget *parent = 0);
|
||||
virtual void updateState(const UIState &s);
|
||||
|
||||
private:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void changeMode();
|
||||
|
||||
Params params;
|
||||
|
||||
protected:
|
||||
virtual void drawButton(QPainter &p);
|
||||
|
||||
QPixmap engage_img;
|
||||
QPixmap experimental_img;
|
||||
bool experimental_mode;
|
||||
bool engageable;
|
||||
};
|
||||
|
||||
void drawIcon(QPainter &p, const QPoint ¢er, const QPixmap &img, const QBrush &bg, float opacity);
|
||||
@@ -1,112 +0,0 @@
|
||||
#include "selfdrive/ui/qt/onroad/driver_monitoring.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "selfdrive/ui/qt/onroad/buttons.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
|
||||
// Default 3D coordinates for face keypoints
|
||||
static constexpr vec3 DEFAULT_FACE_KPTS_3D[] = {
|
||||
{-5.98, -51.20, 8.00}, {-17.64, -49.14, 8.00}, {-23.81, -46.40, 8.00}, {-29.98, -40.91, 8.00}, {-32.04, -37.49, 8.00},
|
||||
{-34.10, -32.00, 8.00}, {-36.16, -21.03, 8.00}, {-36.16, 6.40, 8.00}, {-35.47, 10.51, 8.00}, {-32.73, 19.43, 8.00},
|
||||
{-29.30, 26.29, 8.00}, {-24.50, 33.83, 8.00}, {-19.01, 41.37, 8.00}, {-14.21, 46.17, 8.00}, {-12.16, 47.54, 8.00},
|
||||
{-4.61, 49.60, 8.00}, {4.99, 49.60, 8.00}, {12.53, 47.54, 8.00}, {14.59, 46.17, 8.00}, {19.39, 41.37, 8.00},
|
||||
{24.87, 33.83, 8.00}, {29.67, 26.29, 8.00}, {33.10, 19.43, 8.00}, {35.84, 10.51, 8.00}, {36.53, 6.40, 8.00},
|
||||
{36.53, -21.03, 8.00}, {34.47, -32.00, 8.00}, {32.42, -37.49, 8.00}, {30.36, -40.91, 8.00}, {24.19, -46.40, 8.00},
|
||||
{18.02, -49.14, 8.00}, {6.36, -51.20, 8.00}, {-5.98, -51.20, 8.00},
|
||||
};
|
||||
|
||||
// Colors used for drawing based on monitoring state
|
||||
static const QColor DMON_ENGAGED_COLOR = QColor::fromRgbF(0.1, 0.945, 0.26);
|
||||
static const QColor DMON_DISENGAGED_COLOR = QColor::fromRgbF(0.545, 0.545, 0.545);
|
||||
|
||||
DriverMonitorRenderer::DriverMonitorRenderer() : face_kpts_draw(std::size(DEFAULT_FACE_KPTS_3D)) {
|
||||
dm_img = loadPixmap("../assets/icons/driver_face.png", {img_size + 5, img_size + 5});
|
||||
}
|
||||
|
||||
void DriverMonitorRenderer::updateState(const UIState &s) {
|
||||
auto &sm = *(s.sm);
|
||||
is_visible = sm["selfdriveState"].getSelfdriveState().getAlertSize() == cereal::SelfdriveState::AlertSize::NONE &&
|
||||
sm.rcv_frame("driverStateV2") > s.scene.started_frame;
|
||||
if (!is_visible) return;
|
||||
|
||||
auto dm_state = sm["driverMonitoringState"].getDriverMonitoringState();
|
||||
is_active = dm_state.getIsActiveMode();
|
||||
is_rhd = dm_state.getIsRHD();
|
||||
dm_fade_state = std::clamp(dm_fade_state + 0.2f * (0.5f - is_active), 0.0f, 1.0f);
|
||||
|
||||
const auto &driverstate = sm["driverStateV2"].getDriverStateV2();
|
||||
const auto driver_orient = is_rhd ? driverstate.getRightDriverData().getFaceOrientation() : driverstate.getLeftDriverData().getFaceOrientation();
|
||||
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
float v_this = (i == 0 ? (driver_orient[i] < 0 ? 0.7 : 0.9) : 0.4) * driver_orient[i];
|
||||
driver_pose_diff[i] = std::abs(driver_pose_vals[i] - v_this);
|
||||
driver_pose_vals[i] = 0.8f * v_this + (1 - 0.8) * driver_pose_vals[i];
|
||||
driver_pose_sins[i] = std::sin(driver_pose_vals[i] * (1.0f - dm_fade_state));
|
||||
driver_pose_coss[i] = std::cos(driver_pose_vals[i] * (1.0f - dm_fade_state));
|
||||
}
|
||||
|
||||
auto [sin_y, sin_x, sin_z] = driver_pose_sins;
|
||||
auto [cos_y, cos_x, cos_z] = driver_pose_coss;
|
||||
|
||||
// Rotation matrix for transforming face keypoints based on driver's head orientation
|
||||
const mat3 r_xyz = {{
|
||||
cos_x * cos_z, cos_x * sin_z, -sin_x,
|
||||
-sin_y * sin_x * cos_z - cos_y * sin_z, -sin_y * sin_x * sin_z + cos_y * cos_z, -sin_y * cos_x,
|
||||
cos_y * sin_x * cos_z - sin_y * sin_z, cos_y * sin_x * sin_z + sin_y * cos_z, cos_y * cos_x,
|
||||
}};
|
||||
|
||||
// Transform vertices
|
||||
for (int i = 0; i < face_kpts_draw.size(); ++i) {
|
||||
vec3 kpt = matvecmul3(r_xyz, DEFAULT_FACE_KPTS_3D[i]);
|
||||
face_kpts_draw[i] = {{kpt.v[0], kpt.v[1], kpt.v[2] * (1.0f - dm_fade_state) + 8 * dm_fade_state}};
|
||||
}
|
||||
}
|
||||
|
||||
void DriverMonitorRenderer::draw(QPainter &painter, const QRect &surface_rect) {
|
||||
if (!is_visible) return;
|
||||
|
||||
painter.save();
|
||||
|
||||
int offset = UI_BORDER_SIZE + btn_size / 2;
|
||||
float x = is_rhd ? surface_rect.width() - offset : offset;
|
||||
float y = surface_rect.height() - offset;
|
||||
float opacity = is_active ? 0.65f : 0.2f;
|
||||
|
||||
#ifdef SUNNYPILOT
|
||||
const int dev_ui_info = uiStateSP()->scene.dev_ui_info;
|
||||
y -= dev_ui_info > 1 ? 50 : 0;
|
||||
#endif
|
||||
|
||||
drawIcon(painter, QPoint(x, y), dm_img, QColor(0, 0, 0, 70), opacity);
|
||||
|
||||
QPointF keypoints[std::size(DEFAULT_FACE_KPTS_3D)];
|
||||
for (int i = 0; i < std::size(keypoints); ++i) {
|
||||
const auto &v = face_kpts_draw[i].v;
|
||||
float kp = (v[2] - 8) / 120.0f + 1.0f;
|
||||
keypoints[i] = QPointF(v[0] * kp + x, v[1] * kp + y);
|
||||
}
|
||||
|
||||
painter.setPen(QPen(QColor::fromRgbF(1.0, 1.0, 1.0, opacity), 5.2, Qt::SolidLine, Qt::RoundCap));
|
||||
painter.drawPolyline(keypoints, std::size(keypoints));
|
||||
|
||||
// tracking arcs
|
||||
const int arc_l = 133;
|
||||
const float arc_t_default = 6.7f;
|
||||
const float arc_t_extend = 12.0f;
|
||||
QColor arc_color = uiState()->engaged() ? DMON_ENGAGED_COLOR : DMON_DISENGAGED_COLOR;
|
||||
arc_color.setAlphaF(0.4 * (1.0f - dm_fade_state));
|
||||
|
||||
float delta_x = -driver_pose_sins[1] * arc_l / 2.0f;
|
||||
float delta_y = -driver_pose_sins[0] * arc_l / 2.0f;
|
||||
|
||||
// Draw horizontal tracking arc
|
||||
painter.setPen(QPen(arc_color, arc_t_default + arc_t_extend * std::min(1.0, driver_pose_diff[1] * 5.0), Qt::SolidLine, Qt::RoundCap));
|
||||
painter.drawArc(QRectF(std::min(x + delta_x, x), y - arc_l / 2, std::abs(delta_x), arc_l), (driver_pose_sins[1] > 0 ? 90 : -90) * 16, 180 * 16);
|
||||
|
||||
// Draw vertical tracking arc
|
||||
painter.setPen(QPen(arc_color, arc_t_default + arc_t_extend * std::min(1.0, driver_pose_diff[0] * 5.0), Qt::SolidLine, Qt::RoundCap));
|
||||
painter.drawArc(QRectF(x - arc_l / 2, std::min(y + delta_y, y), arc_l, std::abs(delta_y)), (driver_pose_sins[0] > 0 ? 0 : 180) * 16, 180 * 16);
|
||||
|
||||
painter.restore();
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <QPainter>
|
||||
#include "selfdrive/ui/ui.h"
|
||||
|
||||
class DriverMonitorRenderer {
|
||||
public:
|
||||
DriverMonitorRenderer();
|
||||
void updateState(const UIState &s);
|
||||
void draw(QPainter &painter, const QRect &surface_rect);
|
||||
|
||||
private:
|
||||
float driver_pose_vals[3] = {};
|
||||
float driver_pose_diff[3] = {};
|
||||
float driver_pose_sins[3] = {};
|
||||
float driver_pose_coss[3] = {};
|
||||
bool is_visible = false;
|
||||
bool is_active = false;
|
||||
bool is_rhd = false;
|
||||
float dm_fade_state = 1.0;
|
||||
QPixmap dm_img;
|
||||
std::vector<vec3> face_kpts_draw;
|
||||
};
|
||||
@@ -1,113 +0,0 @@
|
||||
#include "selfdrive/ui/qt/onroad/hud.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
|
||||
constexpr int SET_SPEED_NA = 255;
|
||||
|
||||
HudRenderer::HudRenderer() {}
|
||||
|
||||
void HudRenderer::updateState(const UIState &s) {
|
||||
is_metric = s.scene.is_metric;
|
||||
status = s.status;
|
||||
|
||||
const SubMaster &sm = *(s.sm);
|
||||
if (sm.rcv_frame("carState") < s.scene.started_frame) {
|
||||
is_cruise_set = false;
|
||||
set_speed = SET_SPEED_NA;
|
||||
speed = 0.0;
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &controls_state = sm["controlsState"].getControlsState();
|
||||
const auto &car_state = sm["carState"].getCarState();
|
||||
|
||||
// Handle older routes where vCruiseCluster is not set
|
||||
set_speed = car_state.getVCruiseCluster() == 0.0 ? controls_state.getVCruiseDEPRECATED() : car_state.getVCruiseCluster();
|
||||
is_cruise_set = set_speed > 0 && set_speed != SET_SPEED_NA;
|
||||
is_cruise_available = set_speed != -1;
|
||||
|
||||
if (is_cruise_set && !is_metric) {
|
||||
set_speed *= KM_TO_MILE;
|
||||
}
|
||||
|
||||
// Handle older routes where vEgoCluster is not set
|
||||
v_ego_cluster_seen = v_ego_cluster_seen || car_state.getVEgoCluster() != 0.0;
|
||||
float v_ego = v_ego_cluster_seen ? car_state.getVEgoCluster() : car_state.getVEgo();
|
||||
speed = std::max<float>(0.0f, v_ego * (is_metric ? MS_TO_KPH : MS_TO_MPH));
|
||||
}
|
||||
|
||||
void HudRenderer::draw(QPainter &p, const QRect &surface_rect) {
|
||||
p.save();
|
||||
|
||||
// Draw header gradient
|
||||
QLinearGradient bg(0, UI_HEADER_HEIGHT - (UI_HEADER_HEIGHT / 2.5), 0, UI_HEADER_HEIGHT);
|
||||
bg.setColorAt(0, QColor::fromRgbF(0, 0, 0, 0.45));
|
||||
bg.setColorAt(1, QColor::fromRgbF(0, 0, 0, 0));
|
||||
p.fillRect(0, 0, surface_rect.width(), UI_HEADER_HEIGHT, bg);
|
||||
|
||||
#ifndef SUNNYPILOT
|
||||
if (is_cruise_available) {
|
||||
drawSetSpeed(p, surface_rect);
|
||||
}
|
||||
drawCurrentSpeed(p, surface_rect);
|
||||
#endif
|
||||
|
||||
p.restore();
|
||||
}
|
||||
|
||||
void HudRenderer::drawSetSpeed(QPainter &p, const QRect &surface_rect) {
|
||||
// Draw outer box + border to contain set speed
|
||||
const QSize default_size = {172, 204};
|
||||
QSize set_speed_size = is_metric ? QSize(200, 204) : default_size;
|
||||
QRect set_speed_rect(QPoint(60 + (default_size.width() - set_speed_size.width()) / 2, 45), set_speed_size);
|
||||
|
||||
// Draw set speed box
|
||||
p.setPen(QPen(QColor(255, 255, 255, 75), 6));
|
||||
p.setBrush(QColor(0, 0, 0, 166));
|
||||
p.drawRoundedRect(set_speed_rect, 32, 32);
|
||||
|
||||
// Colors based on status
|
||||
QColor max_color = QColor(0xa6, 0xa6, 0xa6, 0xff);
|
||||
QColor set_speed_color = QColor(0x72, 0x72, 0x72, 0xff);
|
||||
if (is_cruise_set) {
|
||||
set_speed_color = QColor(255, 255, 255);
|
||||
if (status == STATUS_DISENGAGED) {
|
||||
max_color = QColor(255, 255, 255);
|
||||
} else if (status == STATUS_OVERRIDE) {
|
||||
max_color = QColor(0x91, 0x9b, 0x95, 0xff);
|
||||
} else {
|
||||
max_color = QColor(0x80, 0xd8, 0xa6, 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw "MAX" text
|
||||
p.setFont(InterFont(40, QFont::DemiBold));
|
||||
p.setPen(max_color);
|
||||
p.drawText(set_speed_rect.adjusted(0, 27, 0, 0), Qt::AlignTop | Qt::AlignHCenter, tr("MAX"));
|
||||
|
||||
// Draw set speed
|
||||
QString setSpeedStr = is_cruise_set ? QString::number(std::nearbyint(set_speed)) : "–";
|
||||
p.setFont(InterFont(90, QFont::Bold));
|
||||
p.setPen(set_speed_color);
|
||||
p.drawText(set_speed_rect.adjusted(0, 77, 0, 0), Qt::AlignTop | Qt::AlignHCenter, setSpeedStr);
|
||||
}
|
||||
|
||||
void HudRenderer::drawCurrentSpeed(QPainter &p, const QRect &surface_rect) {
|
||||
QString speedStr = QString::number(std::nearbyint(speed));
|
||||
|
||||
p.setFont(InterFont(176, QFont::Bold));
|
||||
drawText(p, surface_rect.center().x(), 210, speedStr);
|
||||
|
||||
p.setFont(InterFont(66));
|
||||
drawText(p, surface_rect.center().x(), 290, is_metric ? tr("km/h") : tr("mph"), 200);
|
||||
}
|
||||
|
||||
void HudRenderer::drawText(QPainter &p, int x, int y, const QString &text, int alpha) {
|
||||
QRect real_rect = p.fontMetrics().boundingRect(text);
|
||||
real_rect.moveCenter({x, y - real_rect.height() / 2});
|
||||
|
||||
p.setPen(QColor(0xff, 0xff, 0xff, alpha));
|
||||
p.drawText(real_rect.x(), real_rect.bottom(), text);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QPainter>
|
||||
|
||||
#ifdef SUNNYPILOT
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
#else
|
||||
#include "selfdrive/ui/ui.h"
|
||||
#endif
|
||||
|
||||
class HudRenderer : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
HudRenderer();
|
||||
virtual ~HudRenderer() = default;
|
||||
virtual void updateState(const UIState &s);
|
||||
virtual void draw(QPainter &p, const QRect &surface_rect);
|
||||
|
||||
protected:
|
||||
void drawSetSpeed(QPainter &p, const QRect &surface_rect);
|
||||
void drawCurrentSpeed(QPainter &p, const QRect &surface_rect);
|
||||
void drawText(QPainter &p, int x, int y, const QString &text, int alpha = 255);
|
||||
|
||||
float speed = 0;
|
||||
float set_speed = 0;
|
||||
bool is_cruise_set = false;
|
||||
bool is_cruise_available = true;
|
||||
bool is_metric = false;
|
||||
bool v_ego_cluster_seen = false;
|
||||
int status = STATUS_DISENGAGED;
|
||||
};
|
||||
@@ -1,237 +0,0 @@
|
||||
#include "selfdrive/ui/qt/onroad/model.h"
|
||||
|
||||
void ModelRenderer::draw(QPainter &painter, const QRect &surface_rect) {
|
||||
auto *s = uiState();
|
||||
auto &sm = *(s->sm);
|
||||
// Check if data is up-to-date
|
||||
if (sm.rcv_frame("liveCalibration") < s->scene.started_frame ||
|
||||
sm.rcv_frame("modelV2") < s->scene.started_frame) {
|
||||
return;
|
||||
}
|
||||
|
||||
clip_region = surface_rect.adjusted(-CLIP_MARGIN, -CLIP_MARGIN, CLIP_MARGIN, CLIP_MARGIN);
|
||||
experimental_mode = sm["selfdriveState"].getSelfdriveState().getExperimentalMode();
|
||||
longitudinal_control = sm["carParams"].getCarParams().getOpenpilotLongitudinalControl();
|
||||
path_offset_z = sm["liveCalibration"].getLiveCalibration().getHeight()[0];
|
||||
|
||||
painter.save();
|
||||
|
||||
const auto &model = sm["modelV2"].getModelV2();
|
||||
const auto &radar_state = sm["radarState"].getRadarState();
|
||||
const auto &lead_one = radar_state.getLeadOne();
|
||||
|
||||
update_model(model, lead_one);
|
||||
drawLaneLines(painter);
|
||||
drawPath(painter, model, surface_rect.height());
|
||||
|
||||
if (longitudinal_control && sm.alive("radarState")) {
|
||||
update_leads(radar_state, model.getPosition());
|
||||
const auto &lead_two = radar_state.getLeadTwo();
|
||||
if (lead_one.getStatus()) {
|
||||
drawLead(painter, lead_one, lead_vertices[0], surface_rect);
|
||||
}
|
||||
if (lead_two.getStatus() && (std::abs(lead_one.getDRel() - lead_two.getDRel()) > 3.0)) {
|
||||
drawLead(painter, lead_two, lead_vertices[1], surface_rect);
|
||||
}
|
||||
}
|
||||
|
||||
painter.restore();
|
||||
}
|
||||
|
||||
void ModelRenderer::update_leads(const cereal::RadarState::Reader &radar_state, const cereal::XYZTData::Reader &line) {
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
const auto &lead_data = (i == 0) ? radar_state.getLeadOne() : radar_state.getLeadTwo();
|
||||
if (lead_data.getStatus()) {
|
||||
float z = line.getZ()[get_path_length_idx(line, lead_data.getDRel())];
|
||||
mapToScreen(lead_data.getDRel(), -lead_data.getYRel(), z + path_offset_z, &lead_vertices[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ModelRenderer::update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead) {
|
||||
const auto &model_position = model.getPosition();
|
||||
float max_distance = std::clamp(*(model_position.getX().end() - 1), MIN_DRAW_DISTANCE, MAX_DRAW_DISTANCE);
|
||||
|
||||
// update lane lines
|
||||
const auto &lane_lines = model.getLaneLines();
|
||||
const auto &line_probs = model.getLaneLineProbs();
|
||||
int max_idx = get_path_length_idx(lane_lines[0], max_distance);
|
||||
for (int i = 0; i < std::size(lane_line_vertices); i++) {
|
||||
lane_line_probs[i] = line_probs[i];
|
||||
mapLineToPolygon(lane_lines[i], 0.025 * lane_line_probs[i], 0, &lane_line_vertices[i], max_idx);
|
||||
}
|
||||
|
||||
// update road edges
|
||||
const auto &road_edges = model.getRoadEdges();
|
||||
const auto &edge_stds = model.getRoadEdgeStds();
|
||||
for (int i = 0; i < std::size(road_edge_vertices); i++) {
|
||||
road_edge_stds[i] = edge_stds[i];
|
||||
mapLineToPolygon(road_edges[i], 0.025, 0, &road_edge_vertices[i], max_idx);
|
||||
}
|
||||
|
||||
// update path
|
||||
if (lead.getStatus()) {
|
||||
const float lead_d = lead.getDRel() * 2.;
|
||||
max_distance = std::clamp((float)(lead_d - fmin(lead_d * 0.35, 10.)), 0.0f, max_distance);
|
||||
}
|
||||
max_idx = get_path_length_idx(model_position, max_distance);
|
||||
mapLineToPolygon(model_position, 0.9, path_offset_z, &track_vertices, max_idx, false);
|
||||
}
|
||||
|
||||
void ModelRenderer::drawLaneLines(QPainter &painter) {
|
||||
// lanelines
|
||||
for (int i = 0; i < std::size(lane_line_vertices); ++i) {
|
||||
painter.setBrush(QColor::fromRgbF(1.0, 1.0, 1.0, std::clamp<float>(lane_line_probs[i], 0.0, 0.7)));
|
||||
painter.drawPolygon(lane_line_vertices[i]);
|
||||
}
|
||||
|
||||
// road edges
|
||||
for (int i = 0; i < std::size(road_edge_vertices); ++i) {
|
||||
painter.setBrush(QColor::fromRgbF(1.0, 0, 0, std::clamp<float>(1.0 - road_edge_stds[i], 0.0, 1.0)));
|
||||
painter.drawPolygon(road_edge_vertices[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void ModelRenderer::drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, int height) {
|
||||
QLinearGradient bg(0, height, 0, 0);
|
||||
if (experimental_mode) {
|
||||
// The first half of track_vertices are the points for the right side of the path
|
||||
const auto &acceleration = model.getAcceleration().getX();
|
||||
const int max_len = std::min<int>(track_vertices.length() / 2, acceleration.size());
|
||||
|
||||
for (int i = 0; i < max_len; ++i) {
|
||||
// Some points are out of frame
|
||||
int track_idx = max_len - i - 1; // flip idx to start from bottom right
|
||||
if (track_vertices[track_idx].y() < 0 || track_vertices[track_idx].y() > height) continue;
|
||||
|
||||
// Flip so 0 is bottom of frame
|
||||
float lin_grad_point = (height - track_vertices[track_idx].y()) / height;
|
||||
|
||||
// speed up: 120, slow down: 0
|
||||
float path_hue = fmax(fmin(60 + acceleration[i] * 35, 120), 0);
|
||||
// FIXME: painter.drawPolygon can be slow if hue is not rounded
|
||||
path_hue = int(path_hue * 100 + 0.5) / 100;
|
||||
|
||||
float saturation = fmin(fabs(acceleration[i] * 1.5), 1);
|
||||
float lightness = util::map_val(saturation, 0.0f, 1.0f, 0.95f, 0.62f); // lighter when grey
|
||||
float alpha = util::map_val(lin_grad_point, 0.75f / 2.f, 0.75f, 0.4f, 0.0f); // matches previous alpha fade
|
||||
bg.setColorAt(lin_grad_point, QColor::fromHslF(path_hue / 360., saturation, lightness, alpha));
|
||||
|
||||
// Skip a point, unless next is last
|
||||
i += (i + 2) < max_len ? 1 : 0;
|
||||
}
|
||||
|
||||
} else {
|
||||
updatePathGradient(bg);
|
||||
}
|
||||
|
||||
painter.setBrush(bg);
|
||||
painter.drawPolygon(track_vertices);
|
||||
}
|
||||
|
||||
void ModelRenderer::updatePathGradient(QLinearGradient &bg) {
|
||||
static const QColor throttle_colors[] = {
|
||||
QColor::fromHslF(148. / 360., 0.94, 0.51, 0.4),
|
||||
QColor::fromHslF(112. / 360., 1.0, 0.68, 0.35),
|
||||
QColor::fromHslF(112. / 360., 1.0, 0.68, 0.0)};
|
||||
|
||||
static const QColor no_throttle_colors[] = {
|
||||
QColor::fromHslF(148. / 360., 0.0, 0.95, 0.4),
|
||||
QColor::fromHslF(112. / 360., 0.0, 0.95, 0.35),
|
||||
QColor::fromHslF(112. / 360., 0.0, 0.95, 0.0),
|
||||
};
|
||||
|
||||
// Transition speed; 0.1 corresponds to 0.5 seconds at UI_FREQ
|
||||
constexpr float transition_speed = 0.1f;
|
||||
|
||||
// Start transition if throttle state changes
|
||||
bool allow_throttle = (*uiState()->sm)["longitudinalPlan"].getLongitudinalPlan().getAllowThrottle() || !longitudinal_control;
|
||||
if (allow_throttle != prev_allow_throttle) {
|
||||
prev_allow_throttle = allow_throttle;
|
||||
// Invert blend factor for a smooth transition when the state changes mid-animation
|
||||
blend_factor = std::max(1.0f - blend_factor, 0.0f);
|
||||
}
|
||||
|
||||
const QColor *begin_colors = allow_throttle ? no_throttle_colors : throttle_colors;
|
||||
const QColor *end_colors = allow_throttle ? throttle_colors : no_throttle_colors;
|
||||
if (blend_factor < 1.0f) {
|
||||
blend_factor = std::min(blend_factor + transition_speed, 1.0f);
|
||||
}
|
||||
|
||||
// Set gradient colors by blending the start and end colors
|
||||
bg.setColorAt(0.0f, blendColors(begin_colors[0], end_colors[0], blend_factor));
|
||||
bg.setColorAt(0.5f, blendColors(begin_colors[1], end_colors[1], blend_factor));
|
||||
bg.setColorAt(1.0f, blendColors(begin_colors[2], end_colors[2], blend_factor));
|
||||
}
|
||||
|
||||
QColor ModelRenderer::blendColors(const QColor &start, const QColor &end, float t) {
|
||||
if (t == 1.0f) return end;
|
||||
return QColor::fromRgbF(
|
||||
(1 - t) * start.redF() + t * end.redF(),
|
||||
(1 - t) * start.greenF() + t * end.greenF(),
|
||||
(1 - t) * start.blueF() + t * end.blueF(),
|
||||
(1 - t) * start.alphaF() + t * end.alphaF());
|
||||
}
|
||||
|
||||
void ModelRenderer::drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data,
|
||||
const QPointF &vd, const QRect &surface_rect) {
|
||||
const float speedBuff = 10.;
|
||||
const float leadBuff = 40.;
|
||||
const float d_rel = lead_data.getDRel();
|
||||
const float v_rel = lead_data.getVRel();
|
||||
|
||||
float fillAlpha = 0;
|
||||
if (d_rel < leadBuff) {
|
||||
fillAlpha = 255 * (1.0 - (d_rel / leadBuff));
|
||||
if (v_rel < 0) {
|
||||
fillAlpha += 255 * (-1 * (v_rel / speedBuff));
|
||||
}
|
||||
fillAlpha = (int)(fmin(fillAlpha, 255));
|
||||
}
|
||||
|
||||
float sz = std::clamp((25 * 30) / (d_rel / 3 + 30), 15.0f, 30.0f) * 2.35;
|
||||
float x = std::clamp<float>(vd.x(), 0.f, surface_rect.width() - sz / 2);
|
||||
float y = std::min<float>(vd.y(), surface_rect.height() - sz * 0.6);
|
||||
|
||||
float g_xo = sz / 5;
|
||||
float g_yo = sz / 10;
|
||||
|
||||
QPointF glow[] = {{x + (sz * 1.35) + g_xo, y + sz + g_yo}, {x, y - g_yo}, {x - (sz * 1.35) - g_xo, y + sz + g_yo}};
|
||||
painter.setBrush(QColor(218, 202, 37, 255));
|
||||
painter.drawPolygon(glow, std::size(glow));
|
||||
|
||||
// chevron
|
||||
QPointF chevron[] = {{x + (sz * 1.25), y + sz}, {x, y}, {x - (sz * 1.25), y + sz}};
|
||||
painter.setBrush(QColor(201, 34, 49, fillAlpha));
|
||||
painter.drawPolygon(chevron, std::size(chevron));
|
||||
}
|
||||
|
||||
// Projects a point in car to space to the corresponding point in full frame image space.
|
||||
bool ModelRenderer::mapToScreen(float in_x, float in_y, float in_z, QPointF *out) {
|
||||
Eigen::Vector3f input(in_x, in_y, in_z);
|
||||
auto pt = car_space_transform * input;
|
||||
*out = QPointF(pt.x() / pt.z(), pt.y() / pt.z());
|
||||
return clip_region.contains(*out);
|
||||
}
|
||||
|
||||
void ModelRenderer::mapLineToPolygon(const cereal::XYZTData::Reader &line, float y_off, float z_off,
|
||||
QPolygonF *pvd, int max_idx, bool allow_invert) {
|
||||
const auto line_x = line.getX(), line_y = line.getY(), line_z = line.getZ();
|
||||
QPointF left, right;
|
||||
pvd->clear();
|
||||
for (int i = 0; i <= max_idx; i++) {
|
||||
// highly negative x positions are drawn above the frame and cause flickering, clip to zy plane of camera
|
||||
if (line_x[i] < 0) continue;
|
||||
|
||||
bool l = mapToScreen(line_x[i], line_y[i] - y_off, line_z[i] + z_off, &left);
|
||||
bool r = mapToScreen(line_x[i], line_y[i] + y_off, line_z[i] + z_off, &right);
|
||||
if (l && r) {
|
||||
// For wider lines the drawn polygon will "invert" when going over a hill and cause artifacts
|
||||
if (!allow_invert && pvd->size() && left.y() > pvd->back().y()) {
|
||||
continue;
|
||||
}
|
||||
pvd->push_back(left);
|
||||
pvd->push_front(right);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QPainter>
|
||||
#include <QPolygonF>
|
||||
|
||||
#ifdef SUNNYPILOT
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
#else
|
||||
#include "selfdrive/ui/ui.h"
|
||||
#endif
|
||||
|
||||
constexpr int CLIP_MARGIN = 500;
|
||||
constexpr float MIN_DRAW_DISTANCE = 10.0;
|
||||
constexpr float MAX_DRAW_DISTANCE = 100.0;
|
||||
|
||||
inline int get_path_length_idx(const cereal::XYZTData::Reader &line, const float path_height) {
|
||||
const auto &line_x = line.getX();
|
||||
int max_idx = 0;
|
||||
for (int i = 1; i < line_x.size() && line_x[i] <= path_height; ++i) {
|
||||
max_idx = i;
|
||||
}
|
||||
return max_idx;
|
||||
}
|
||||
|
||||
class ModelRenderer {
|
||||
public:
|
||||
virtual ~ModelRenderer() = default;
|
||||
|
||||
ModelRenderer() {}
|
||||
void setTransform(const Eigen::Matrix3f &transform) { car_space_transform = transform; }
|
||||
void draw(QPainter &painter, const QRect &surface_rect);
|
||||
|
||||
protected:
|
||||
bool mapToScreen(float in_x, float in_y, float in_z, QPointF *out);
|
||||
void mapLineToPolygon(const cereal::XYZTData::Reader &line, float y_off, float z_off,
|
||||
QPolygonF *pvd, int max_idx, bool allow_invert = true);
|
||||
void drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data, const QPointF &vd, const QRect &surface_rect);
|
||||
void update_leads(const cereal::RadarState::Reader &radar_state, const cereal::XYZTData::Reader &line);
|
||||
virtual void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead);
|
||||
void drawLaneLines(QPainter &painter);
|
||||
void drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, int height);
|
||||
void updatePathGradient(QLinearGradient &bg);
|
||||
QColor blendColors(const QColor &start, const QColor &end, float t);
|
||||
|
||||
bool longitudinal_control = false;
|
||||
bool experimental_mode = false;
|
||||
float blend_factor = 1.0f;
|
||||
bool prev_allow_throttle = true;
|
||||
float lane_line_probs[4] = {};
|
||||
float road_edge_stds[2] = {};
|
||||
float path_offset_z = 1.22f;
|
||||
QPolygonF track_vertices;
|
||||
QPolygonF lane_line_vertices[4] = {};
|
||||
QPolygonF road_edge_vertices[2] = {};
|
||||
QPointF lead_vertices[2] = {};
|
||||
Eigen::Matrix3f car_space_transform = Eigen::Matrix3f::Zero();
|
||||
QRectF clip_region;
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
#include "selfdrive/ui/qt/onroad/onroad_home.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QStackedLayout>
|
||||
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
|
||||
OnroadWindow::OnroadWindow(QWidget *parent) : QWidget(parent) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
main_layout->setMargin(UI_BORDER_SIZE);
|
||||
QStackedLayout *stacked_layout = new QStackedLayout;
|
||||
stacked_layout->setStackingMode(QStackedLayout::StackAll);
|
||||
main_layout->addLayout(stacked_layout);
|
||||
|
||||
nvg = new AnnotatedCameraWidget(VISION_STREAM_ROAD, this);
|
||||
|
||||
QWidget * split_wrapper = new QWidget;
|
||||
split = new QHBoxLayout(split_wrapper);
|
||||
split->setContentsMargins(0, 0, 0, 0);
|
||||
split->setSpacing(0);
|
||||
split->addWidget(nvg);
|
||||
|
||||
if (getenv("DUAL_CAMERA_VIEW")) {
|
||||
CameraWidget *arCam = new CameraWidget("camerad", VISION_STREAM_ROAD, this);
|
||||
split->insertWidget(0, arCam);
|
||||
}
|
||||
|
||||
stacked_layout->addWidget(split_wrapper);
|
||||
|
||||
alerts = new OnroadAlerts(this);
|
||||
alerts->setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
||||
stacked_layout->addWidget(alerts);
|
||||
|
||||
// setup stacking order
|
||||
alerts->raise();
|
||||
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
|
||||
// We handle the connection of the signals on the derived class
|
||||
#ifndef SUNNYPILOT
|
||||
QObject::connect(uiState(), &UIState::uiUpdate, this, &OnroadWindow::updateState);
|
||||
QObject::connect(uiState(), &UIState::offroadTransition, this, &OnroadWindow::offroadTransition);
|
||||
#endif
|
||||
}
|
||||
|
||||
void OnroadWindow::updateState(const UIState &s) {
|
||||
if (!s.scene.started) {
|
||||
return;
|
||||
}
|
||||
|
||||
alerts->updateState(s);
|
||||
nvg->updateState(s);
|
||||
|
||||
QColor bgColor = bg_colors[s.status];
|
||||
if (bg != bgColor) {
|
||||
// repaint border
|
||||
bg = bgColor;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void OnroadWindow::offroadTransition(bool offroad) {
|
||||
alerts->clear();
|
||||
}
|
||||
|
||||
void OnroadWindow::paintEvent(QPaintEvent *event) {
|
||||
QPainter p(this);
|
||||
p.fillRect(rect(), QColor(bg.red(), bg.green(), bg.blue(), 255));
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "selfdrive/ui/qt/onroad/alerts.h"
|
||||
|
||||
#ifdef SUNNYPILOT
|
||||
#include "selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/onroad/alerts.h"
|
||||
#define UIState UIStateSP
|
||||
#define AnnotatedCameraWidget AnnotatedCameraWidgetSP
|
||||
#define OnroadAlerts OnroadAlertsSP
|
||||
#else
|
||||
#include "selfdrive/ui/qt/onroad/annotated_camera.h"
|
||||
#endif
|
||||
|
||||
class OnroadWindow : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
OnroadWindow(QWidget* parent = 0);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event);
|
||||
OnroadAlerts *alerts;
|
||||
AnnotatedCameraWidget *nvg;
|
||||
QColor bg = bg_colors[STATUS_DISENGAGED];
|
||||
QHBoxLayout* split;
|
||||
|
||||
protected slots:
|
||||
virtual void offroadTransition(bool offroad);
|
||||
virtual void updateState(const UIState &s);
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
#include "selfdrive/ui/qt/prime_state.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
|
||||
#include "selfdrive/ui/qt/api.h"
|
||||
#include "selfdrive/ui/qt/request_repeater.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
|
||||
PrimeState::PrimeState(QObject* parent) : QObject(parent) {
|
||||
const char *env_prime_type = std::getenv("PRIME_TYPE");
|
||||
auto type = env_prime_type ? env_prime_type : Params().get("PrimeType");
|
||||
|
||||
if (!type.empty()) {
|
||||
prime_type = static_cast<PrimeState::Type>(std::atoi(type.c_str()));
|
||||
}
|
||||
|
||||
if (auto dongleId = getDongleId()) {
|
||||
QString url = CommaApi::BASE_URL + "/v1.1/devices/" + *dongleId + "/";
|
||||
RequestRepeater* repeater = new RequestRepeater(this, url, "ApiCache_Device", 5);
|
||||
QObject::connect(repeater, &RequestRepeater::requestDone, this, &PrimeState::handleReply);
|
||||
}
|
||||
|
||||
// Emit the initial state change
|
||||
QTimer::singleShot(1, [this]() { emit changed(prime_type); });
|
||||
}
|
||||
|
||||
void PrimeState::handleReply(const QString& response, bool success) {
|
||||
if (!success) return;
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8());
|
||||
if (doc.isNull()) {
|
||||
qDebug() << "JSON Parse failed on getting pairing and PrimeState status";
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json = doc.object();
|
||||
bool is_paired = json["is_paired"].toBool();
|
||||
auto type = static_cast<PrimeState::Type>(json["prime_type"].toInt());
|
||||
setType(is_paired ? type : PrimeState::PRIME_TYPE_UNPAIRED);
|
||||
}
|
||||
|
||||
void PrimeState::setType(PrimeState::Type type) {
|
||||
if (type != prime_type) {
|
||||
prime_type = type;
|
||||
Params().put("PrimeType", std::to_string(prime_type));
|
||||
emit changed(prime_type);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class PrimeState : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
enum Type {
|
||||
PRIME_TYPE_UNKNOWN = -2,
|
||||
PRIME_TYPE_UNPAIRED = -1,
|
||||
PRIME_TYPE_NONE = 0,
|
||||
PRIME_TYPE_MAGENTA = 1,
|
||||
PRIME_TYPE_LITE = 2,
|
||||
PRIME_TYPE_BLUE = 3,
|
||||
PRIME_TYPE_MAGENTA_NEW = 4,
|
||||
PRIME_TYPE_PURPLE = 5,
|
||||
};
|
||||
|
||||
PrimeState(QObject *parent);
|
||||
void setType(PrimeState::Type type);
|
||||
inline PrimeState::Type currentType() const { return prime_type; }
|
||||
inline bool isSubscribed() const { return prime_type > PrimeState::PRIME_TYPE_NONE; }
|
||||
|
||||
signals:
|
||||
void changed(PrimeState::Type prime_type);
|
||||
|
||||
private:
|
||||
void handleReply(const QString &response, bool success);
|
||||
|
||||
PrimeState::Type prime_type = PrimeState::PRIME_TYPE_UNKNOWN;
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
#include "selfdrive/ui/qt/qt_window.h"
|
||||
|
||||
void setMainWindow(QWidget *w) {
|
||||
const float scale = util::getenv("SCALE", 1.0f);
|
||||
const QSize sz = QGuiApplication::primaryScreen()->size();
|
||||
|
||||
if (Hardware::PC() && scale == 1.0 && !(sz - DEVICE_SCREEN_SIZE).isValid()) {
|
||||
w->setMinimumSize(QSize(640, 480)); // allow resize smaller than fullscreen
|
||||
w->setMaximumSize(DEVICE_SCREEN_SIZE);
|
||||
w->resize(sz);
|
||||
} else {
|
||||
w->setFixedSize(DEVICE_SCREEN_SIZE * scale);
|
||||
}
|
||||
w->show();
|
||||
|
||||
#ifdef QCOM2
|
||||
QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface();
|
||||
wl_surface *s = reinterpret_cast<wl_surface*>(native->nativeResourceForWindow("surface", w->windowHandle()));
|
||||
wl_surface_set_buffer_transform(s, WL_OUTPUT_TRANSFORM_270);
|
||||
wl_surface_commit(s);
|
||||
|
||||
w->setWindowState(Qt::WindowFullScreen);
|
||||
w->setVisible(true);
|
||||
|
||||
// ensure we have a valid eglDisplay, otherwise the ui will silently fail
|
||||
void *egl = native->nativeResourceForWindow("egldisplay", w->windowHandle());
|
||||
assert(egl != nullptr);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
extern "C" {
|
||||
void set_main_window(void *w) {
|
||||
setMainWindow((QWidget*)w);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user