mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-11-15 23:54:07 +08:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 431ea4ed6d | |||
| bea05d4624 | |||
| b9c54e07fb | |||
| c560ac43aa | |||
| a83c64ffbd | |||
| f08dfa1c14 | |||
| 3c5841ff02 | |||
| 138d637bbd | |||
| eda189c564 | |||
| 04504d47f3 | |||
| 7ba9876fa4 | |||
| f312c011e8 | |||
| 9ee965d2e0 | |||
| 859745ea86 | |||
| 79fa8803b6 | |||
| c68c914444 | |||
| 0f7498e214 | |||
| ebca4fc901 | |||
| 023b842e3c | |||
| a4454721ea | |||
| 3e29a0ccfe | |||
| bee820f8ed | |||
| 99ed90d459 | |||
| aa5a7ecb31 | |||
| 9595a6f246 | |||
| 1d6d0fb85c | |||
| 4914445415 | |||
| c229eb4f38 | |||
| da35e60101 | |||
| 1e95e1cc01 | |||
| ad88c306e9 | |||
| 42b2e1534b | |||
| c53e2134e2 | |||
| 844f4cbc74 | |||
| 9edc36ca66 | |||
| 3cd55260d9 |
@@ -6,10 +6,10 @@ env:
|
||||
CI_DIR: ${{ github.workspace }}/release/ci
|
||||
SCONS_CACHE_DIR: ${{ github.workspace }}/release/ci/scons_cache
|
||||
PUBLIC_REPO_URL: "https://github.com/sunnypilot/sunnypilot"
|
||||
|
||||
|
||||
# Branch configurations
|
||||
STAGING_SOURCE_BRANCH: 'master'
|
||||
|
||||
|
||||
# Runtime configuration
|
||||
SOURCE_BRANCH: "${{ github.head_ref || github.ref_name }}"
|
||||
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
cancel="$(echo "$CONFIG" | jq -r '.cancel_publish_in_progress')";
|
||||
echo "cancel_publish_in_progress=$( [ "$cancel" = "null" ] && echo "true" || echo $cancel)" >> $GITHUB_OUTPUT
|
||||
echo "publish_concurrency_group=publish-${BRANCH}$( [ "$cancel" = "null" ] || [ "$cancel" = "true" ] || echo "${{ github.sha }}" )" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
is_stable_branch="$(echo "$CONFIG" | jq -r '.stable_branch // false')";
|
||||
echo "is_stable_branch=$is_stable_branch" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
fi
|
||||
echo "build=$BUILD" >> $GITHUB_OUTPUT
|
||||
cat $GITHUB_OUTPUT
|
||||
|
||||
|
||||
validate_tests:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [ prepare_strategy ]
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
needs.prepare_strategy.result == 'success' &&
|
||||
(needs.validate_tests.result == 'success' || needs.validate_tests.result == 'skipped') &&
|
||||
(!contains(github.event_name, 'pull_request') ||
|
||||
(github.event.action == 'labeled' && github.event.label.name == 'prebuilt'))
|
||||
(github.event.action == 'labeled' && github.event.label.name == 'prebuilt'))
|
||||
}}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
with:
|
||||
path: ${{env.SCONS_CACHE_DIR}}
|
||||
key: scons-${{ runner.os }}-${{ runner.arch }}-${{ env.SOURCE_BRANCH }}-${{ github.sha }}
|
||||
# Note: GitHub Actions enforces cache isolation between different build sources (PR builds, workflow dispatches, etc.)
|
||||
# Note: GitHub Actions enforces cache isolation between different build sources (PR builds, workflow dispatches, etc.)
|
||||
# for security. Only caches from the default branch are shared across all builds. This is by design and cannot be overridden.
|
||||
restore-keys: |
|
||||
scons-${{ runner.os }}-${{ runner.arch }}-${{ env.SOURCE_BRANCH }}
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
echo "version=${{ needs.prepare_strategy.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
echo "extra_version_identifier=${{ needs.prepare_strategy.outputs.extra_version_identifier }}" >> $GITHUB_OUTPUT
|
||||
echo "commit_sha=${{ github.sha }}" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
# Set up common environment
|
||||
source /etc/profile;
|
||||
export UV_PROJECT_ENVIRONMENT=${HOME}/venv
|
||||
@@ -180,6 +180,15 @@ jobs:
|
||||
./release/release_files.py | sort | uniq | rsync -rRl${RUNNER_DEBUG:+v} --files-from=- . $BUILD_DIR/
|
||||
cd $BUILD_DIR
|
||||
sed -i '/from .board.jungle import PandaJungle, PandaJungleDFU/s/^/#/' panda/__init__.py
|
||||
echo "Building sunnypilot's modeld..."
|
||||
scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal sunnypilot/modeld
|
||||
echo "Building sunnypilot's modeld_v2..."
|
||||
scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal sunnypilot/modeld_v2
|
||||
echo "Building sunnypilot's locationd..."
|
||||
scons -j2 cache_dir=${{env.SCONS_CACHE_DIR}} --minimal sunnypilot/selfdrive/locationd
|
||||
echo "Building openpilot's locationd..."
|
||||
scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal selfdrive/locationd
|
||||
echo "Building rest of sunnypilot"
|
||||
scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal
|
||||
touch ${BUILD_DIR}/prebuilt
|
||||
if [[ "${{ runner.debug }}" == "1" ]]; then
|
||||
@@ -241,8 +250,8 @@ jobs:
|
||||
if: always()
|
||||
run: |
|
||||
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable
|
||||
|
||||
|
||||
|
||||
|
||||
publish:
|
||||
concurrency:
|
||||
# We do a bit of a hack here to avoid canceling the publishing job if a new commit comes in while we're publishing by adding the sha to the group name.
|
||||
@@ -293,7 +302,7 @@ jobs:
|
||||
echo "1. Go to: ${{ github.server_url }}/${{ github.repository }}/settings/variables/actions/AUTO_DEPLOY_PREBUILT_BRANCHES"
|
||||
echo "2. Current value: ${{ vars.AUTO_DEPLOY_PREBUILT_BRANCHES }}"
|
||||
echo "3. Update as needed (JSON array with no spaces)"
|
||||
|
||||
|
||||
- name: Tag ${{ needs.prepare_strategy.outputs.environment }}
|
||||
if: ${{ needs.prepare_strategy.outputs.is_stable_branch == 'true' && (github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/')) }}
|
||||
run: |
|
||||
@@ -302,7 +311,7 @@ jobs:
|
||||
git push -f origin ${TAG}
|
||||
|
||||
notify:
|
||||
needs:
|
||||
needs:
|
||||
- prepare_strategy
|
||||
- build
|
||||
- publish
|
||||
@@ -331,7 +340,7 @@ jobs:
|
||||
${{ vars.DISCOURSE_GENERAL_UPDATE_NOTICE }}
|
||||
EOF
|
||||
)
|
||||
|
||||
|
||||
{
|
||||
echo 'content<<EOFMARKER'
|
||||
echo "$MESSAGE"
|
||||
|
||||
-10
@@ -75,7 +75,6 @@ env = Environment(
|
||||
"#third_party/acados/include/hpipm/include",
|
||||
"#third_party/catch2/include",
|
||||
"#third_party/libyuv/include",
|
||||
"#third_party/snpe/include",
|
||||
],
|
||||
LIBPATH=[
|
||||
"#common",
|
||||
@@ -101,7 +100,6 @@ if arch == "larch64":
|
||||
"/usr/local/lib",
|
||||
"/system/vendor/lib64",
|
||||
"/usr/lib/aarch64-linux-gnu",
|
||||
"#third_party/snpe/larch64",
|
||||
])
|
||||
arch_flags = ["-D__TICI__", "-mcpu=cortex-a57", "-DQCOM2"]
|
||||
env.Append(CCFLAGS=arch_flags)
|
||||
@@ -125,14 +123,6 @@ else:
|
||||
"/usr/local/lib",
|
||||
])
|
||||
|
||||
if arch == "x86_64":
|
||||
env.Append(LIBPATH=[
|
||||
f"#third_party/snpe/{arch}"
|
||||
])
|
||||
env.Append(RPATH=[
|
||||
Dir(f"#third_party/snpe/{arch}").abspath,
|
||||
])
|
||||
|
||||
# Sanitizers and extra CCFLAGS from CLI
|
||||
if GetOption('asan'):
|
||||
env.Append(CCFLAGS=["-fsanitize=address", "-fno-omit-frame-pointer"])
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import math
|
||||
import threading
|
||||
import time
|
||||
from numbers import Number
|
||||
|
||||
from cereal import car, log
|
||||
@@ -22,8 +20,6 @@ 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
|
||||
from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase
|
||||
from openpilot.sunnypilot.selfdrive.controls.controlsd_ext import ControlsExt
|
||||
|
||||
State = log.SelfdriveState.OpenpilotState
|
||||
@@ -33,7 +29,7 @@ LaneChangeDirection = log.LaneChangeDirection
|
||||
ACTUATOR_FIELDS = tuple(car.CarControl.Actuators.schema.fields.keys())
|
||||
|
||||
|
||||
class Controls(ControlsExt, ModelStateBase):
|
||||
class Controls(ControlsExt):
|
||||
def __init__(self) -> None:
|
||||
self.params = Params()
|
||||
cloudlog.info("controlsd is waiting for CarParams")
|
||||
@@ -42,7 +38,6 @@ class Controls(ControlsExt, ModelStateBase):
|
||||
|
||||
# Initialize sunnypilot controlsd extension and base model state
|
||||
ControlsExt.__init__(self, self.CP, self.params)
|
||||
ModelStateBase.__init__(self)
|
||||
|
||||
self.CI = interfaces[self.CP.carFingerprint](self.CP, self.CP_SP)
|
||||
|
||||
@@ -231,30 +226,15 @@ class Controls(ControlsExt, ModelStateBase):
|
||||
cc_send.carControl = CC
|
||||
self.pm.send('carControl', cc_send)
|
||||
|
||||
def params_thread(self, evt):
|
||||
while not evt.is_set():
|
||||
self.get_params_sp()
|
||||
|
||||
if self.CP.lateralTuning.which() == 'torque':
|
||||
self.lat_delay = get_lat_delay(self.params, self.sm["liveDelay"].lateralDelay)
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
def run(self):
|
||||
rk = Ratekeeper(100, print_delay_threshold=None)
|
||||
e = threading.Event()
|
||||
t = threading.Thread(target=self.params_thread, args=(e,))
|
||||
try:
|
||||
t.start()
|
||||
while True:
|
||||
self.update()
|
||||
CC, lac_log = self.state_control()
|
||||
self.publish(CC, lac_log)
|
||||
self.run_ext(self.sm, self.pm)
|
||||
rk.monitor_time()
|
||||
finally:
|
||||
e.set()
|
||||
t.join()
|
||||
while True:
|
||||
self.update()
|
||||
CC, lac_log = self.state_control()
|
||||
self.publish(CC, lac_log)
|
||||
self.get_params_sp(self.sm)
|
||||
self.run_ext(self.sm, self.pm)
|
||||
rk.monitor_time()
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -227,6 +227,6 @@ class HomeLayout(Widget):
|
||||
self._prev_alerts_present = alerts_present
|
||||
|
||||
def _get_version_text(self) -> str:
|
||||
brand = "openpilot"
|
||||
brand = "sunnypilot"
|
||||
description = self.params.get("UpdaterCurrentDescription")
|
||||
return f"{brand} {description}" if description else brand
|
||||
|
||||
@@ -10,6 +10,9 @@ 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
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.settings import SettingsLayoutSP as SettingsLayout
|
||||
|
||||
|
||||
class MainState(IntEnum):
|
||||
HOME = 0
|
||||
|
||||
@@ -109,8 +109,8 @@ class TermsPage(Widget):
|
||||
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."),
|
||||
self._title = Label(tr("Welcome to sunnypilot"), 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 sunnypilot. 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)
|
||||
@@ -143,10 +143,10 @@ class TermsPage(Widget):
|
||||
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."),
|
||||
self._text = Label(tr("You must accept the Terms and Conditions in order to use sunnypilot."),
|
||||
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,
|
||||
self._uninstall_btn = Button(tr("Decline, uninstall sunnypilot"), button_style=ButtonStyle.DANGER,
|
||||
click_callback=self._on_uninstall_clicked)
|
||||
|
||||
def _on_uninstall_clicked(self):
|
||||
|
||||
@@ -23,10 +23,11 @@ DESCRIPTIONS = {
|
||||
"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."
|
||||
"<b>WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB).</b><br><br>" +
|
||||
"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. " +
|
||||
"Changing this setting will restart sunnypilot if the car is powered on."
|
||||
),
|
||||
}
|
||||
|
||||
@@ -71,7 +72,7 @@ class DeveloperLayout(Widget):
|
||||
)
|
||||
|
||||
self._alpha_long_toggle = toggle_item(
|
||||
lambda: tr("openpilot Longitudinal Control (Alpha)"),
|
||||
lambda: tr("sunnypilot Longitudinal Control (Alpha)"),
|
||||
description=lambda: tr(DESCRIPTIONS["alpha_longitudinal"]),
|
||||
initial_state=self._params.get_bool("AlphaLongitudinalEnabled"),
|
||||
callback=self._on_alpha_long_enabled,
|
||||
@@ -148,6 +149,7 @@ class DeveloperLayout(Widget):
|
||||
self._params.put_bool("ShowDebugInfo", state)
|
||||
gui_app.set_show_touches(state)
|
||||
gui_app.set_show_fps(state)
|
||||
gui_app.set_show_mouse_coords(state)
|
||||
|
||||
def _on_enable_adb(self, state: bool):
|
||||
self._params.put_bool("AdbEnabled", state)
|
||||
|
||||
@@ -23,8 +23,8 @@ from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
DESCRIPTIONS = {
|
||||
'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"),
|
||||
'reset_calibration': tr_noop("sunnypilot 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 sunnypilot"),
|
||||
}
|
||||
|
||||
|
||||
@@ -162,8 +162,8 @@ class DeviceLayout(Widget):
|
||||
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.")
|
||||
desc += tr("sunnypilot is continuously calibrating, resetting is rarely required. " +
|
||||
"Resetting calibration will restart sunnypilot if the car is powered on.")
|
||||
|
||||
self._reset_calib_btn.set_description(desc)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from openpilot.selfdrive.ui.lib.api_helpers import get_token
|
||||
|
||||
TITLE = tr_noop("Firehose Mode")
|
||||
DESCRIPTION = tr_noop(
|
||||
"openpilot learns to drive by watching humans, like you, drive.\n\n"
|
||||
"sunnypilot 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."
|
||||
)
|
||||
|
||||
@@ -11,26 +11,27 @@ from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp as toggle_item
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp as multiple_button_item
|
||||
|
||||
PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants
|
||||
|
||||
# Description constants
|
||||
DESCRIPTIONS = {
|
||||
"OpenpilotEnabledToggle": tr_noop(
|
||||
"Use the openpilot system for adaptive cruise control and lane keep driver assistance. " +
|
||||
"Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. " +
|
||||
"Your attention is required at all times to use this feature."
|
||||
),
|
||||
"DisengageOnAccelerator": tr_noop("When enabled, pressing the accelerator pedal will disengage openpilot."),
|
||||
"DisengageOnAccelerator": tr_noop("When enabled, pressing the accelerator pedal will disengage sunnypilot."),
|
||||
"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 " +
|
||||
"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."
|
||||
),
|
||||
"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": tr_noop("Enable driver monitoring even when openpilot is not engaged."),
|
||||
"AlwaysOnDM": tr_noop("Enable driver monitoring even when sunnypilot 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."),
|
||||
@@ -46,7 +47,7 @@ class TogglesLayout(Widget):
|
||||
# param, title, desc, icon, needs_restart
|
||||
self._toggle_defs = {
|
||||
"OpenpilotEnabledToggle": (
|
||||
lambda: tr("Enable openpilot"),
|
||||
lambda: tr("Enable sunnypilot"),
|
||||
DESCRIPTIONS["OpenpilotEnabledToggle"],
|
||||
"chffr_wheel.png",
|
||||
True,
|
||||
@@ -99,7 +100,7 @@ class TogglesLayout(Widget):
|
||||
lambda: tr("Driving Personality"),
|
||||
lambda: tr(DESCRIPTIONS["LongitudinalPersonality"]),
|
||||
buttons=[lambda: tr("Aggressive"), lambda: tr("Standard"), lambda: tr("Relaxed")],
|
||||
button_width=255,
|
||||
button_width=300,
|
||||
callback=self._set_longitudinal_personality,
|
||||
selected_index=self._params.get("LongitudinalPersonality", return_default=True),
|
||||
icon="speed_limit.png"
|
||||
@@ -125,7 +126,7 @@ class TogglesLayout(Widget):
|
||||
# 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.")
|
||||
additional_desc = tr("Changing this setting will restart sunnypilot 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
|
||||
@@ -158,10 +159,10 @@ class TogglesLayout(Widget):
|
||||
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. " +
|
||||
"sunnypilot 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. " +
|
||||
"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.<br>" +
|
||||
"<h4>New Driving Visualization</h4><br>" +
|
||||
@@ -183,13 +184,13 @@ class TogglesLayout(Widget):
|
||||
|
||||
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.")
|
||||
long_desc = unavailable + " " + tr("sunnypilot 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 " +
|
||||
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 openpilot longitudinal control (alpha) toggle to allow Experimental mode.")
|
||||
long_desc = tr("Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode.")
|
||||
|
||||
self._toggles["ExperimentalMode"].set_description("<b>" + long_desc + "</b><br><br>" + e2e_description)
|
||||
else:
|
||||
|
||||
@@ -45,7 +45,7 @@ class DeviceStatus(Widget):
|
||||
self._version_text = self._get_version_text()
|
||||
|
||||
def _get_version_text(self) -> str:
|
||||
brand = "openpilot"
|
||||
brand = "sunnypilot"
|
||||
description = ui_state.params.get("UpdaterCurrentDescription")
|
||||
return f"{brand} {description}" if description else brand
|
||||
|
||||
@@ -111,7 +111,7 @@ class MiciHomeLayout(Widget):
|
||||
self._cell_high_txt = gui_app.texture("icons_mici/settings/network/cell_strength_high.png", 55, 35)
|
||||
self._cell_full_txt = gui_app.texture("icons_mici/settings/network/cell_strength_full.png", 55, 35)
|
||||
|
||||
self._openpilot_label = MiciLabel("openpilot", font_size=96, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.DISPLAY)
|
||||
self._openpilot_label = MiciLabel("sunnypilot", font_size=96, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.DISPLAY)
|
||||
self._version_label = MiciLabel("", font_size=36, font_weight=FontWeight.ROMAN)
|
||||
self._large_version_label = MiciLabel("", font_size=64, color=rl.GRAY, font_weight=FontWeight.ROMAN)
|
||||
self._date_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN)
|
||||
|
||||
@@ -254,7 +254,7 @@ class MiciOffroadAlerts(Widget):
|
||||
parts = new_desc.split(" / ")
|
||||
if len(parts) > 3:
|
||||
version, date = parts[0], parts[3]
|
||||
update_alert_data.text = f"update available\n openpilot {version}, {date}. go to comma.ai/blog to read the release notes."
|
||||
update_alert_data.text = f"update available\n sunnypilot {version}, {date}. go to comma.ai/blog to read the release notes."
|
||||
|
||||
update_alert_data.visible = True
|
||||
active_count += 1
|
||||
|
||||
@@ -182,7 +182,7 @@ class TrainingGuideAttentionNotice(SetupTermsPage):
|
||||
def __init__(self, continue_callback):
|
||||
super().__init__(continue_callback, continue_text="continue")
|
||||
self._title_header = TermsHeader("driver assistance", gui_app.texture("icons_mici/setup/warning.png", 60, 60))
|
||||
self._warning_label = UnifiedLabel("1. openpilot is a driver assistance system.\n\n" +
|
||||
self._warning_label = UnifiedLabel("1. sunnypilot is a driver assistance system.\n\n" +
|
||||
"2. You must pay attention at all times.\n\n" +
|
||||
"3. You must be ready to take over at any time.\n\n" +
|
||||
"4. You are fully responsible for driving the car.", 42,
|
||||
@@ -239,12 +239,12 @@ class TrainingGuide(Widget):
|
||||
class DeclinePage(Widget):
|
||||
def __init__(self, back_callback=None):
|
||||
super().__init__()
|
||||
self._uninstall_slider = SmallSlider("uninstall openpilot", self._on_uninstall)
|
||||
self._uninstall_slider = SmallSlider("uninstall sunnypilot", self._on_uninstall)
|
||||
|
||||
self._back_button = SmallButton("back")
|
||||
self._back_button.set_click_callback(back_callback)
|
||||
|
||||
self._warning_header = TermsHeader("you must accept the\nterms to use openpilot",
|
||||
self._warning_header = TermsHeader("you must accept the\nterms to use sunnypilot",
|
||||
gui_app.texture("icons_mici/setup/red_warning.png", 66, 60))
|
||||
|
||||
def _on_uninstall(self):
|
||||
@@ -282,7 +282,7 @@ class TermsPage(SetupTermsPage):
|
||||
info_txt = gui_app.texture("icons_mici/setup/green_info.png", 60, 60)
|
||||
self._title_header = TermsHeader("terms & conditions", info_txt)
|
||||
|
||||
self._terms_label = UnifiedLabel("You must accept the Terms and Conditions to use openpilot. " +
|
||||
self._terms_label = UnifiedLabel("You must accept the Terms and Conditions to use sunnypilot. " +
|
||||
"Read the latest terms at https://comma.ai/terms before continuing.", 36,
|
||||
FontWeight.ROMAN)
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ class UpdateOpenpilotBigButton(BigButton):
|
||||
self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 64)
|
||||
self._txt_reboot_icon = gui_app.texture("icons_mici/settings/device/reboot.png", 64, 64)
|
||||
self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64)
|
||||
super().__init__("update openpilot", "", self._txt_update_icon)
|
||||
super().__init__("update sunnypilot", "", self._txt_update_icon)
|
||||
|
||||
self._waiting_for_updater_t: float | None = None
|
||||
self._hide_value_t: float | None = None
|
||||
@@ -193,7 +193,7 @@ class UpdateOpenpilotBigButton(BigButton):
|
||||
if value:
|
||||
self.set_text("")
|
||||
else:
|
||||
self.set_text("update openpilot")
|
||||
self.set_text("update sunnypilot")
|
||||
|
||||
def _update_state(self):
|
||||
if ui_state.started:
|
||||
@@ -294,7 +294,7 @@ class DeviceLayoutMici(NavWidget):
|
||||
reset_calibration_btn = BigButton("reset calibration", "", "icons_mici/settings/device/lkas.png")
|
||||
reset_calibration_btn.set_click_callback(lambda: _engaged_confirmation_callback(reset_calibration_callback, "reset"))
|
||||
|
||||
uninstall_openpilot_btn = BigButton("uninstall openpilot", "", "icons_mici/settings/device/uninstall.png")
|
||||
uninstall_openpilot_btn = BigButton("uninstall sunnypilot", "", "icons_mici/settings/device/uninstall.png")
|
||||
uninstall_openpilot_btn.set_click_callback(lambda: _engaged_confirmation_callback(uninstall_openpilot_callback, "uninstall"))
|
||||
|
||||
reboot_btn = BigCircleButton("icons_mici/settings/device/reboot.png", red=False)
|
||||
|
||||
@@ -17,7 +17,7 @@ from openpilot.system.ui.widgets import NavWidget
|
||||
|
||||
TITLE = tr_noop("Firehose Mode")
|
||||
DESCRIPTION = tr_noop(
|
||||
"openpilot learns to drive by watching humans, like you, drive.\n\n"
|
||||
"sunnypilot 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."
|
||||
)
|
||||
|
||||
@@ -24,7 +24,7 @@ class TogglesLayoutMici(NavWidget):
|
||||
always_on_dm_toggle = BigParamControl("always-on driver monitor", "AlwaysOnDM")
|
||||
record_front = BigParamControl("record & upload driver camera", "RecordFront", toggle_callback=restart_needed_callback)
|
||||
record_mic = BigParamControl("record & upload mic audio", "RecordAudio", toggle_callback=restart_needed_callback)
|
||||
enable_openpilot = BigParamControl("enable openpilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback)
|
||||
enable_openpilot = BigParamControl("enable sunnypilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback)
|
||||
|
||||
self._scroller = Scroller([
|
||||
self._personality_toggle,
|
||||
|
||||
@@ -65,7 +65,7 @@ class Alert:
|
||||
|
||||
# Pre-defined alert instances
|
||||
ALERT_STARTUP_PENDING = Alert(
|
||||
text1="openpilot Unavailable",
|
||||
text1="sunnypilot Unavailable",
|
||||
text2="Waiting to start",
|
||||
size=AlertSize.mid,
|
||||
status=AlertStatus.normal,
|
||||
|
||||
@@ -153,7 +153,7 @@ class AugmentedRoadView(CameraView):
|
||||
self._alert_renderer = AlertRenderer()
|
||||
self._driver_state_renderer = DriverStateRenderer()
|
||||
self._confidence_ball = ConfidenceBall()
|
||||
self._offroad_label = UnifiedLabel("start the car to\nuse openpilot", 54, FontWeight.DISPLAY,
|
||||
self._offroad_label = UnifiedLabel("start the car to\nuse sunnypilot", 54, FontWeight.DISPLAY,
|
||||
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
@@ -171,7 +171,7 @@ class AugmentedRoadView(CameraView):
|
||||
if ui_state.panda_type == log.PandaState.PandaType.unknown:
|
||||
self._offroad_label.set_text("system booting")
|
||||
else:
|
||||
self._offroad_label.set_text("start the car to\nuse openpilot")
|
||||
self._offroad_label.set_text("start the car to\nuse sunnypilot")
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
# Don't trigger click callback if bookmark was triggered
|
||||
|
||||
@@ -48,7 +48,7 @@ class Alert:
|
||||
|
||||
# Pre-defined alert instances
|
||||
ALERT_STARTUP_PENDING = Alert(
|
||||
text1=tr("openpilot Unavailable"),
|
||||
text1=tr("sunnypilot Unavailable"),
|
||||
text2=tr("Waiting to start"),
|
||||
size=AlertSize.mid,
|
||||
status=AlertStatus.normal,
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#include "selfdrive/ui/qt/offroad/offroad_home.h"
|
||||
|
||||
#include "selfdrive/ui/qt/offroad/experimental_mode.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
#include "selfdrive/ui/qt/widgets/prime.h"
|
||||
|
||||
// OffroadHome: the offroad home page
|
||||
|
||||
OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) {
|
||||
QVBoxLayout* main_layout = new QVBoxLayout(this);
|
||||
main_layout->setContentsMargins(40, 40, 40, 40);
|
||||
|
||||
// top header
|
||||
header_layout = new QHBoxLayout();
|
||||
header_layout->setContentsMargins(0, 0, 0, 0);
|
||||
header_layout->setSpacing(16);
|
||||
|
||||
update_notif = new QPushButton(tr("UPDATE"));
|
||||
update_notif->setVisible(false);
|
||||
update_notif->setStyleSheet("background-color: #364DEF;");
|
||||
QObject::connect(update_notif, &QPushButton::clicked, [=]() { center_layout->setCurrentIndex(1); });
|
||||
header_layout->addWidget(update_notif, 0, Qt::AlignHCenter | Qt::AlignLeft);
|
||||
|
||||
alert_notif = new QPushButton();
|
||||
alert_notif->setVisible(false);
|
||||
alert_notif->setStyleSheet("background-color: #E22C2C;");
|
||||
QObject::connect(alert_notif, &QPushButton::clicked, [=] { center_layout->setCurrentIndex(2); });
|
||||
header_layout->addWidget(alert_notif, 0, Qt::AlignHCenter | Qt::AlignLeft);
|
||||
|
||||
version = new ElidedLabel();
|
||||
header_layout->addWidget(version, 0, Qt::AlignHCenter | Qt::AlignRight);
|
||||
|
||||
main_layout->addLayout(header_layout);
|
||||
|
||||
// main content
|
||||
main_layout->addSpacing(25);
|
||||
center_layout = new QStackedLayout();
|
||||
|
||||
QWidget *home_widget = new QWidget(this);
|
||||
{
|
||||
home_layout = new QHBoxLayout(home_widget);
|
||||
home_layout->setContentsMargins(0, 0, 0, 0);
|
||||
home_layout->setSpacing(30);
|
||||
|
||||
#ifndef SUNNYPILOT
|
||||
// left: PrimeAdWidget
|
||||
QStackedWidget *left_widget = new QStackedWidget(this);
|
||||
QVBoxLayout *left_prime_layout = new QVBoxLayout();
|
||||
left_prime_layout->setContentsMargins(0, 0, 0, 0);
|
||||
QWidget *prime_user = new PrimeUserWidget();
|
||||
prime_user->setStyleSheet(R"(
|
||||
border-radius: 10px;
|
||||
background-color: #333333;
|
||||
)");
|
||||
left_prime_layout->addWidget(prime_user);
|
||||
left_prime_layout->addStretch();
|
||||
left_widget->addWidget(new LayoutWidget(left_prime_layout));
|
||||
left_widget->addWidget(new PrimeAdWidget);
|
||||
left_widget->setStyleSheet("border-radius: 10px;");
|
||||
|
||||
connect(uiState()->prime_state, &PrimeState::changed, [left_widget]() {
|
||||
left_widget->setCurrentIndex(uiState()->prime_state->isSubscribed() ? 0 : 1);
|
||||
});
|
||||
|
||||
home_layout->addWidget(left_widget, 1);
|
||||
#endif
|
||||
|
||||
// right: ExperimentalModeButton, SetupWidget
|
||||
QWidget* right_widget = new QWidget(this);
|
||||
QVBoxLayout* right_column = new QVBoxLayout(right_widget);
|
||||
right_column->setContentsMargins(0, 0, 0, 0);
|
||||
right_widget->setFixedWidth(750);
|
||||
right_column->setSpacing(30);
|
||||
|
||||
ExperimentalModeButton *experimental_mode = new ExperimentalModeButton(this);
|
||||
QObject::connect(experimental_mode, &ExperimentalModeButton::openSettings, this, &OffroadHome::openSettings);
|
||||
right_column->addWidget(experimental_mode, 1);
|
||||
|
||||
SetupWidget *setup_widget = new SetupWidget;
|
||||
QObject::connect(setup_widget, &SetupWidget::openSettings, this, &OffroadHome::openSettings);
|
||||
right_column->addWidget(setup_widget, 1);
|
||||
|
||||
home_layout->addWidget(right_widget, 1);
|
||||
}
|
||||
center_layout->addWidget(home_widget);
|
||||
|
||||
// add update & alerts widgets
|
||||
update_widget = new UpdateAlert();
|
||||
QObject::connect(update_widget, &UpdateAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); });
|
||||
center_layout->addWidget(update_widget);
|
||||
alerts_widget = new OffroadAlert();
|
||||
QObject::connect(alerts_widget, &OffroadAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); });
|
||||
center_layout->addWidget(alerts_widget);
|
||||
|
||||
main_layout->addLayout(center_layout, 1);
|
||||
|
||||
// set up refresh timer
|
||||
timer = new QTimer(this);
|
||||
timer->callOnTimeout(this, &OffroadHome::refresh);
|
||||
|
||||
setStyleSheet(R"(
|
||||
* {
|
||||
color: white;
|
||||
}
|
||||
OffroadHome {
|
||||
background-color: black;
|
||||
}
|
||||
OffroadHome > QPushButton {
|
||||
padding: 15px 30px;
|
||||
border-radius: 5px;
|
||||
font-size: 40px;
|
||||
font-weight: 500;
|
||||
}
|
||||
OffroadHome > QLabel {
|
||||
font-size: 55px;
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
void OffroadHome::showEvent(QShowEvent *event) {
|
||||
refresh();
|
||||
timer->start(10 * 1000);
|
||||
}
|
||||
|
||||
void OffroadHome::hideEvent(QHideEvent *event) {
|
||||
timer->stop();
|
||||
}
|
||||
|
||||
void OffroadHome::refresh() {
|
||||
version->setText(getBrand() + " " + QString::fromStdString(params.get("UpdaterCurrentDescription")));
|
||||
|
||||
bool updateAvailable = update_widget->refresh();
|
||||
int alerts = alerts_widget->refresh();
|
||||
|
||||
// pop-up new notification
|
||||
int idx = center_layout->currentIndex();
|
||||
if (!updateAvailable && !alerts) {
|
||||
idx = 0;
|
||||
} else if (updateAvailable && (!update_notif->isVisible() || (!alerts && idx == 2))) {
|
||||
idx = 1;
|
||||
} else if (alerts && (!alert_notif->isVisible() || (!updateAvailable && idx == 1))) {
|
||||
idx = 2;
|
||||
}
|
||||
center_layout->setCurrentIndex(idx);
|
||||
|
||||
update_notif->setVisible(updateAvailable);
|
||||
alert_notif->setVisible(alerts);
|
||||
if (alerts) {
|
||||
alert_notif->setText(QString::number(alerts) + (alerts > 1 ? tr(" ALERTS") : tr(" ALERT")));
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/params.h"
|
||||
#include "selfdrive/ui/qt/body.h"
|
||||
#include "selfdrive/ui/qt/widgets/offroad_alerts.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/sidebar.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/prime.h"
|
||||
#define OnroadWindow OnroadWindowSP
|
||||
#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"
|
||||
#include "selfdrive/ui/qt/widgets/prime.h"
|
||||
#endif
|
||||
|
||||
class OffroadHome : public QFrame {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit OffroadHome(QWidget* parent = 0);
|
||||
|
||||
signals:
|
||||
void openSettings(int index = 0, const QString ¶m = "");
|
||||
|
||||
protected:
|
||||
QHBoxLayout *home_layout;
|
||||
QHBoxLayout *header_layout;
|
||||
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void refresh();
|
||||
|
||||
private:
|
||||
void hideEvent(QHideEvent *event) override;
|
||||
|
||||
Params params;
|
||||
|
||||
QTimer* timer;
|
||||
ElidedLabel* version;
|
||||
QStackedLayout* center_layout;
|
||||
UpdateAlert *update_widget;
|
||||
OffroadAlert* alerts_widget;
|
||||
QPushButton* alert_notif;
|
||||
QPushButton* update_notif;
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
|
||||
class CruiseLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
|
||||
items = [
|
||||
]
|
||||
return items
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout
|
||||
|
||||
|
||||
class DeviceLayoutSP(DeviceLayout):
|
||||
def __init__(self):
|
||||
DeviceLayout.__init__(self)
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
|
||||
class DisplayLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
items = [
|
||||
|
||||
]
|
||||
return items
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
|
||||
class ModelsLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
items = [
|
||||
|
||||
]
|
||||
return items
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
|
||||
class NavigationLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
items = [
|
||||
|
||||
]
|
||||
return items
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import threading
|
||||
import time
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||
from openpilot.system.ui.widgets.network import NetworkUI, PanelType
|
||||
|
||||
|
||||
class NetworkUISP(NetworkUI):
|
||||
def __init__(self, wifi_manager):
|
||||
super().__init__(wifi_manager)
|
||||
|
||||
self.scan_button = Button(tr("Scan"), self._scan_clicked, button_style=ButtonStyle.NORMAL, font_size=60, border_radius=30)
|
||||
self.scan_button.set_rect(rl.Rectangle(0, 0, 400, 100))
|
||||
|
||||
self._scanning = False
|
||||
self._wifi_manager.add_callbacks(networks_updated=self._on_networks_updated)
|
||||
|
||||
def _scan_clicked(self):
|
||||
self._scanning = True
|
||||
self.scan_button.set_text(tr("Scanning..."))
|
||||
self.scan_button.set_enabled(False)
|
||||
|
||||
threading.Thread(target=self._wifi_manager._update_networks, daemon=True).start()
|
||||
self._wifi_manager._request_scan()
|
||||
self._wifi_manager._last_network_update = time.monotonic()
|
||||
|
||||
def _on_networks_updated(self, networks):
|
||||
if self._scanning:
|
||||
self._scanning = False
|
||||
self.scan_button.set_text(tr("Scan"))
|
||||
self.scan_button.set_enabled(True)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
super()._render(rect)
|
||||
|
||||
if self._current_panel == PanelType.WIFI:
|
||||
self.scan_button.set_position(self._rect.x, self._rect.y + 20)
|
||||
self.scan_button.render()
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
|
||||
class OSMLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
items = [
|
||||
|
||||
]
|
||||
return items
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
@@ -0,0 +1,202 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.layouts.settings import settings as OP
|
||||
from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.device import DeviceLayoutSP
|
||||
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, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr_noop
|
||||
from openpilot.system.ui.sunnypilot.lib.styles import style
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
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.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.network import NetworkUISP
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.sunnylink import SunnylinkLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.osm import OSMLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.trips import TripsLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle import VehicleLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.steering import SteeringLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise import CruiseLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.visuals import VisualsLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import DisplayLayout
|
||||
|
||||
# from openpilot.selfdrive.ui.sunnypilot.layouts.settings.navigation import NavigationLayout
|
||||
|
||||
OP.PANEL_COLOR = rl.Color(10, 10, 10, 255)
|
||||
ICON_SIZE = 70
|
||||
|
||||
OP.PanelType = IntEnum( # type: ignore
|
||||
"PanelType",
|
||||
[es.name for es in OP.PanelType] + [
|
||||
"SUNNYLINK",
|
||||
"MODELS",
|
||||
"STEERING",
|
||||
"CRUISE",
|
||||
"VISUALS",
|
||||
"DISPLAY",
|
||||
"OSM",
|
||||
"NAVIGATION",
|
||||
"TRIPS",
|
||||
"VEHICLE",
|
||||
],
|
||||
start=0,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PanelInfo(OP.PanelInfo):
|
||||
icon: str = ""
|
||||
|
||||
|
||||
class NavButton(Widget):
|
||||
def __init__(self, parent, p_type, p_info):
|
||||
super().__init__()
|
||||
self.parent = parent
|
||||
self.panel_type = p_type
|
||||
self.panel_info = p_info
|
||||
|
||||
def _render(self, rect):
|
||||
is_selected = self.panel_type == self.parent._current_panel
|
||||
text_color = OP.TEXT_SELECTED if is_selected else OP.TEXT_NORMAL
|
||||
content_x = rect.x + 90
|
||||
text_size = measure_text_cached(self.parent._font_medium, self.panel_info.name, 65)
|
||||
|
||||
# Draw background if selected
|
||||
if is_selected:
|
||||
self.container_rect = rl.Rectangle(
|
||||
content_x - 50, rect.y, OP.SIDEBAR_WIDTH - 50, OP.NAV_BTN_HEIGHT
|
||||
)
|
||||
rl.draw_rectangle_rounded(self.container_rect, 0.2, 5, OP.CLOSE_BTN_COLOR)
|
||||
|
||||
if self.panel_info.icon:
|
||||
icon_texture = gui_app.texture(self.panel_info.icon, ICON_SIZE, ICON_SIZE, keep_aspect_ratio=True)
|
||||
rl.draw_texture(icon_texture, int(content_x), int(rect.y + (OP.NAV_BTN_HEIGHT - icon_texture.height) / 2),
|
||||
rl.WHITE)
|
||||
content_x += ICON_SIZE + 20
|
||||
|
||||
# Draw button text (right-aligned)
|
||||
text_pos = rl.Vector2(
|
||||
content_x,
|
||||
rect.y + (OP.NAV_BTN_HEIGHT - text_size.y) / 2
|
||||
)
|
||||
rl.draw_text_ex(self.parent._font_medium, self.panel_info.name, text_pos, 55, 0, text_color)
|
||||
|
||||
# Store button rect for click detection
|
||||
self.panel_info.button_rect = rect
|
||||
|
||||
|
||||
class SettingsLayoutSP(OP.SettingsLayout):
|
||||
def __init__(self):
|
||||
OP.SettingsLayout.__init__(self)
|
||||
self._nav_items: list[Widget] = []
|
||||
|
||||
# Create sidebar scroller
|
||||
self._sidebar_scroller = Scroller([], spacing=0, line_separator=False, pad_end=False)
|
||||
|
||||
# Panel configuration
|
||||
wifi_manager = WifiManager()
|
||||
wifi_manager.set_active(False)
|
||||
|
||||
self._panels = {
|
||||
OP.PanelType.DEVICE: PanelInfo(tr_noop("Device"), DeviceLayoutSP(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_home.png"),
|
||||
OP.PanelType.NETWORK: PanelInfo(tr_noop("Network"), NetworkUISP(wifi_manager), icon="icons/network.png"),
|
||||
OP.PanelType.SUNNYLINK: PanelInfo(tr_noop("sunnylink"), SunnylinkLayout(), icon="icons/shell.png"),
|
||||
OP.PanelType.TOGGLES: PanelInfo(tr_noop("Toggles"), TogglesLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"),
|
||||
OP.PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_software.png"),
|
||||
OP.PanelType.MODELS: PanelInfo(tr_noop("Models"), ModelsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_models.png"),
|
||||
OP.PanelType.STEERING: PanelInfo(tr_noop("Steering"), SteeringLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_lateral.png"),
|
||||
OP.PanelType.CRUISE: PanelInfo(tr_noop("Cruise"), CruiseLayout(), icon="icons/speed_limit.png"),
|
||||
OP.PanelType.VISUALS: PanelInfo(tr_noop("Visuals"), VisualsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_visuals.png"),
|
||||
OP.PanelType.DISPLAY: PanelInfo(tr_noop("Display"), DisplayLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_display.png"),
|
||||
OP.PanelType.OSM: PanelInfo(tr_noop("OSM"), OSMLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_map.png"),
|
||||
# OP.PanelType.NAVIGATION: PanelInfo(tr_noop("Navigation"), NavigationLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_map.png"),
|
||||
OP.PanelType.TRIPS: PanelInfo(tr_noop("Trips"), TripsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"),
|
||||
OP.PanelType.VEHICLE: PanelInfo(tr_noop("Vehicle"), VehicleLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"),
|
||||
OP.PanelType.FIREHOSE: PanelInfo(tr_noop("Firehose"), FirehoseLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_firehose.png"),
|
||||
OP.PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayout(), icon="icons/shell.png"),
|
||||
}
|
||||
|
||||
def _draw_sidebar(self, rect: rl.Rectangle):
|
||||
rl.draw_rectangle_rec(rect, OP.SIDEBAR_COLOR)
|
||||
|
||||
# Close button
|
||||
close_btn_rect = rl.Rectangle(
|
||||
rect.x + style.ITEM_PADDING * 3, rect.y + style.ITEM_PADDING * 2, style.CLOSE_BTN_SIZE, style.CLOSE_BTN_SIZE
|
||||
)
|
||||
|
||||
pressed = (rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and
|
||||
rl.check_collision_point_rec(rl.get_mouse_position(), close_btn_rect))
|
||||
close_color = OP.CLOSE_BTN_PRESSED if pressed else OP.CLOSE_BTN_COLOR
|
||||
rl.draw_rectangle_rounded(close_btn_rect, 1.0, 20, close_color)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
# Store close button rect for click detection
|
||||
self._close_btn_rect = close_btn_rect
|
||||
|
||||
# Navigation buttons with scroller
|
||||
if not self._nav_items:
|
||||
for panel_type, panel_info in self._panels.items():
|
||||
nav_button = NavButton(self, panel_type, panel_info)
|
||||
nav_button.rect.width = rect.width - 100 # Full width minus padding
|
||||
nav_button.rect.height = OP.NAV_BTN_HEIGHT
|
||||
self._nav_items.append(nav_button)
|
||||
self._sidebar_scroller.add_widget(nav_button)
|
||||
|
||||
# Draw navigation section with scroller
|
||||
nav_rect = rl.Rectangle(
|
||||
rect.x,
|
||||
self._close_btn_rect.height + style.ITEM_PADDING * 4, # Starting Y position for nav items
|
||||
rect.width,
|
||||
rect.height - 300 # Remaining height after close button
|
||||
)
|
||||
|
||||
if self._nav_items:
|
||||
self._sidebar_scroller.render(nav_rect)
|
||||
return
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos) -> bool:
|
||||
# Check close button
|
||||
if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect):
|
||||
if self._close_callback:
|
||||
self._close_callback()
|
||||
return True
|
||||
|
||||
# Check navigation buttons
|
||||
for panel_type, panel_info in self._panels.items():
|
||||
if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect) and self._sidebar_scroller.scroll_panel.is_touch_valid():
|
||||
self.set_current_panel(panel_type)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._panels[self._current_panel].instance.show_event()
|
||||
self._sidebar_scroller.show_event()
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
|
||||
class SteeringLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
items = [
|
||||
|
||||
]
|
||||
return items
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
|
||||
class SunnylinkLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
items = [
|
||||
|
||||
]
|
||||
return items
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
|
||||
class TripsLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
items = [
|
||||
|
||||
]
|
||||
return items
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.list_view import ButtonAction
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.factory import BrandSettingsFactory
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.platform_selector import PlatformSelector, LegendWidget
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP
|
||||
|
||||
|
||||
class VehicleLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._brand_settings = None
|
||||
self._brand_items = []
|
||||
self._current_brand = None
|
||||
self._platform_selector = PlatformSelector(self._update_brand_settings)
|
||||
|
||||
self._vehicle_item = ListItemSP(title=self._platform_selector.text, action_item=ButtonAction(text=tr("Select")),
|
||||
callback=self._platform_selector._on_clicked)
|
||||
self._vehicle_item.title_color = self._platform_selector.color
|
||||
self._legend_widget = LegendWidget(self._platform_selector)
|
||||
|
||||
self.items = [self._vehicle_item, self._legend_widget]
|
||||
self._scroller = Scroller(self.items, line_separator=True, spacing=0)
|
||||
|
||||
@staticmethod
|
||||
def get_brand():
|
||||
if bundle := ui_state.params.get("CarPlatformBundle"):
|
||||
return bundle.get("brand", "")
|
||||
elif ui_state.CP and ui_state.CP.carFingerprint != "MOCK":
|
||||
return ui_state.CP.brand
|
||||
return ""
|
||||
|
||||
def _update_brand_settings(self):
|
||||
self._vehicle_item._title = self._platform_selector.text
|
||||
self._vehicle_item.title_color = self._platform_selector.color
|
||||
vehicle_text = tr("Remove") if ui_state.params.get("CarPlatformBundle") else tr("Select")
|
||||
self._vehicle_item.action_item.set_text(vehicle_text)
|
||||
|
||||
brand = self.get_brand()
|
||||
if brand != self._current_brand:
|
||||
self._current_brand = brand
|
||||
self._brand_settings = BrandSettingsFactory.create_brand_settings(brand)
|
||||
self._brand_items = self._brand_settings.items if self._brand_settings else []
|
||||
|
||||
self.items = [self._vehicle_item, self._legend_widget] + self._brand_items
|
||||
self._scroller = Scroller(self.items, line_separator=True, spacing=0)
|
||||
|
||||
def _update_state(self):
|
||||
self._update_brand_settings()
|
||||
if self._brand_settings:
|
||||
self._brand_settings.update_settings()
|
||||
self._platform_selector.refresh()
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import abc
|
||||
|
||||
|
||||
class BrandSettings(abc.ABC):
|
||||
def __init__(self):
|
||||
self.items = []
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_settings(self) -> None:
|
||||
"""Update the settings based on the current vehicle brand."""
|
||||
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class BodySettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class ChryslerSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.body import BodySettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.chrysler import ChryslerSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.ford import FordSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.gm import GMSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.honda import HondaSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.hyundai import HyundaiSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.mazda import MazdaSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.nissan import NissanSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.psa import PSASettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.rivian import RivianSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.subaru import SubaruSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.tesla import TeslaSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.toyota import ToyotaSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.volkswagen import VolkswagenSettings
|
||||
|
||||
|
||||
class BrandSettingsFactory:
|
||||
_BRAND_MAP: dict[str, type[BrandSettings]] = {
|
||||
"body": BodySettings,
|
||||
"chrysler": ChryslerSettings,
|
||||
"ford": FordSettings,
|
||||
"gm": GMSettings,
|
||||
"honda": HondaSettings,
|
||||
"hyundai": HyundaiSettings,
|
||||
"mazda": MazdaSettings,
|
||||
"nissan": NissanSettings,
|
||||
"psa": PSASettings,
|
||||
"rivian": RivianSettings,
|
||||
"subaru": SubaruSettings,
|
||||
"tesla": TeslaSettings,
|
||||
"toyota": ToyotaSettings,
|
||||
"volkswagen": VolkswagenSettings,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def create_brand_settings(brand: str) -> BrandSettings | None:
|
||||
cls = BrandSettingsFactory._BRAND_MAP.get(brand)
|
||||
return cls() if cls is not None else None
|
||||
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class FordSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class GMSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class HondaSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp
|
||||
from opendbc.car.hyundai.values import CAR, CANFD_UNSUPPORTED_LONGITUDINAL_CAR, UNSUPPORTED_LONGITUDINAL_CAR
|
||||
|
||||
|
||||
class HyundaiSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.alpha_long_available = False
|
||||
|
||||
tuning_texts = [tr("Off"), tr("Dynamic"), tr("Predictive")]
|
||||
self.longitudinal_tuning_item = multiple_button_item_sp(tr("Custom Longitudinal Tuning"), "", tuning_texts,
|
||||
button_width=300, callback=self._on_tuning_selected,
|
||||
param="HyundaiLongitudinalTuning", inline=False)
|
||||
self.items = [self.longitudinal_tuning_item]
|
||||
|
||||
@staticmethod
|
||||
def _on_tuning_selected(index):
|
||||
ui_state.params.put("HyundaiLongitudinalTuning", index)
|
||||
|
||||
def update_settings(self):
|
||||
self.alpha_long_available = False
|
||||
bundle = ui_state.params.get("CarPlatformBundle")
|
||||
if bundle:
|
||||
platform = bundle.get("platform")
|
||||
self.alpha_long_available = CAR[platform] not in (UNSUPPORTED_LONGITUDINAL_CAR | CANFD_UNSUPPORTED_LONGITUDINAL_CAR)
|
||||
elif ui_state.CP:
|
||||
self.alpha_long_available = ui_state.CP.alphaLongitudinalAvailable
|
||||
|
||||
tuning_param = int(ui_state.params.get("HyundaiLongitudinalTuning") or "0")
|
||||
long_enabled = ui_state.has_longitudinal_control
|
||||
|
||||
long_tuning_descs = [
|
||||
tr("Your vehicle will use the Default longitudinal tuning."),
|
||||
tr("Your vehicle will use the Dynamic longitudinal tuning."),
|
||||
tr("Your vehicle will use the Predictive longitudinal tuning."),
|
||||
]
|
||||
long_tuning_desc = long_tuning_descs[tuning_param] if tuning_param < len(long_tuning_descs) else long_tuning_descs[0]
|
||||
|
||||
longitudinal_tuning_disabled = not ui_state.is_offroad() or not long_enabled
|
||||
if longitudinal_tuning_disabled:
|
||||
if not ui_state.is_offroad():
|
||||
long_tuning_desc = tr("This feature is unavailable while the car is onroad.")
|
||||
elif not long_enabled:
|
||||
long_tuning_desc = tr("This feature is unavailable because sunnypilot Longitudinal Control (Alpha) is not enabled.")
|
||||
|
||||
self.longitudinal_tuning_item.action_item.set_enabled(not longitudinal_tuning_disabled)
|
||||
self.longitudinal_tuning_item.set_description(long_tuning_desc)
|
||||
self.longitudinal_tuning_item.show_description(True)
|
||||
self.longitudinal_tuning_item.action_item.set_selected_button(tuning_param)
|
||||
self.longitudinal_tuning_item.set_visible(self.alpha_long_available)
|
||||
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class MazdaSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class NissanSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class PSASettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class RivianSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||
from opendbc.car.subaru.values import CAR, SubaruFlags
|
||||
|
||||
|
||||
class SubaruSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.has_stop_and_go = False
|
||||
|
||||
self.stop_and_go_toggle = toggle_item_sp(tr("Stop and Go (Beta)"), "", param="SubaruStopAndGo", callback=self._on_toggle_changed)
|
||||
|
||||
self.stop_and_go_manual_parking_brake_toggle = toggle_item_sp(tr("Stop and Go for Manual Parking Brake (Beta)"), "",
|
||||
param="SubaruStopAndGoManualParkingBrake", callback=self._on_toggle_changed)
|
||||
|
||||
self.items = [self.stop_and_go_toggle, self.stop_and_go_manual_parking_brake_toggle]
|
||||
|
||||
def _on_toggle_changed(self, _):
|
||||
self.update_settings()
|
||||
|
||||
def stop_and_go_disabled_msg(self):
|
||||
if not self.has_stop_and_go:
|
||||
return tr("This feature is currently not available on this platform.")
|
||||
elif not ui_state.is_offroad():
|
||||
return tr("Enable \"Always Offroad\" in Device panel, or turn vehicle off to toggle.")
|
||||
return ""
|
||||
|
||||
def update_settings(self):
|
||||
bundle = ui_state.params.get("CarPlatformBundle")
|
||||
if bundle:
|
||||
platform = bundle.get("platform")
|
||||
config = CAR[platform].config
|
||||
self.has_stop_and_go = not (config.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
|
||||
elif ui_state.CP:
|
||||
self.has_stop_and_go = not (ui_state.CP.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
|
||||
|
||||
disabled_msg = self.stop_and_go_disabled_msg()
|
||||
descriptions = [
|
||||
tr("Experimental feature to enable auto-resume during stop-and-go for certain supported Subaru platforms."),
|
||||
tr("Experimental feature to enable stop and go for Subaru Global models with manual handbrake. " +
|
||||
"Models with electric parking brake should keep this disabled. Thanks to martinl for this implementation!")
|
||||
]
|
||||
|
||||
for toggle, desc in zip([self.stop_and_go_toggle, self.stop_and_go_manual_parking_brake_toggle], descriptions, strict=True):
|
||||
toggle.action_item.set_enabled(self.has_stop_and_go and ui_state.is_offroad())
|
||||
toggle.set_description(f"<b>{disabled_msg}</b><br><br>{desc}" if disabled_msg else desc)
|
||||
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||
|
||||
COOP_STEERING_MIN_KMH = 23
|
||||
OEM_STEERING_MIN_KMH = 48
|
||||
KM_TO_MILE = 0.621371
|
||||
|
||||
|
||||
class TeslaSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.coop_steering_toggle = toggle_item_sp(tr("Cooperative Steering (Beta)"), "", param="TeslaCoopSteering")
|
||||
self.items = [self.coop_steering_toggle]
|
||||
|
||||
def update_settings(self):
|
||||
is_metric = ui_state.is_metric
|
||||
unit = "km/h" if is_metric else "mph"
|
||||
|
||||
display_value_coop = COOP_STEERING_MIN_KMH if is_metric else round(COOP_STEERING_MIN_KMH * KM_TO_MILE)
|
||||
display_value_oem = OEM_STEERING_MIN_KMH if is_metric else round(OEM_STEERING_MIN_KMH * KM_TO_MILE)
|
||||
|
||||
coop_steering_disabled_msg = tr("Enable \"Always Offroad\" in Device panel, or turn vehicle off to toggle.")
|
||||
coop_steering_warning = tr(f"Warning: May experience steering oscillations below {display_value_oem} {unit} during turns, " +
|
||||
"recommend disabling this feature if you experience these.")
|
||||
coop_steering_desc = (
|
||||
f"<b>{coop_steering_warning}</b><br><br>" +
|
||||
f"{tr('Allows the driver to provide limited steering input while openpilot is engaged.')}<br>" +
|
||||
f"{tr(f'Only works above {display_value_coop} {unit}.')}"
|
||||
)
|
||||
|
||||
if not ui_state.is_offroad():
|
||||
coop_steering_desc = f"<b>{coop_steering_disabled_msg}</b><br><br>{coop_steering_desc}"
|
||||
|
||||
self.coop_steering_toggle.set_description(coop_steering_desc)
|
||||
self.coop_steering_toggle.action_item.set_enabled(ui_state.is_offroad())
|
||||
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class ToyotaSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class VolkswagenSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import DialogResult, Widget
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
|
||||
from openpilot.system.ui.sunnypilot.lib.styles import style
|
||||
from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeOptionDialog, TreeNode, TreeFolder
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
CAR_LIST_JSON_OUT = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "car", "car_list.json")
|
||||
|
||||
|
||||
class LegendWidget(Widget):
|
||||
def __init__(self, platform_selector):
|
||||
super().__init__()
|
||||
self.set_rect(rl.Rectangle(0, 0, 0, 350))
|
||||
self._platform_selector = platform_selector
|
||||
self._font = gui_app.font(FontWeight.NORMAL)
|
||||
self._bold_font = gui_app.font(FontWeight.BOLD)
|
||||
|
||||
def _render(self, rect):
|
||||
x = rect.x + 20
|
||||
y = rect.y + 20
|
||||
rl.draw_text_ex(self._font, tr("Select vehicle to force fingerprint manually."), rl.Vector2(x, y), 40, 0, style.ITEM_DESC_TEXT_COLOR)
|
||||
y += 80
|
||||
rl.draw_text_ex(self._font, tr("Colors represent vehicle fingerprint status:"), rl.Vector2(x, y), 40, 0, style.ITEM_DESC_TEXT_COLOR)
|
||||
y += 80
|
||||
|
||||
items = [
|
||||
(style.GREEN, tr("Fingerprinted automatically")),
|
||||
(style.BLUE, tr("Manually selected fingerprint")),
|
||||
(style.YELLOW, tr("Not fingerprinted or manually selected")),
|
||||
]
|
||||
for color, text in items:
|
||||
p_color = self._platform_selector.color
|
||||
is_active = p_color.r == color.r and p_color.g == color.g and p_color.b == color.b and p_color.a == color.a
|
||||
rl.draw_rectangle(int(x), int(y + 5), 30, 30, color)
|
||||
font = self._bold_font if is_active else self._font
|
||||
text_color = rl.WHITE if is_active else style.ITEM_DESC_TEXT_COLOR
|
||||
rl.draw_text_ex(font, f"- {text}", rl.Vector2(x + 50, y - 7), 40, 0, text_color)
|
||||
y += 50
|
||||
|
||||
|
||||
class PlatformSelector(Button):
|
||||
def __init__(self, on_platform_change: Callable[[], None] | None = None):
|
||||
super().__init__(tr("Vehicle"), self._on_clicked, button_style=ButtonStyle.NORMAL)
|
||||
self.set_rect(rl.Rectangle(0, 0, 0, 120))
|
||||
|
||||
with open(CAR_LIST_JSON_OUT) as car_list_json:
|
||||
self._platforms = json.load(car_list_json)
|
||||
|
||||
self._on_platform_change = on_platform_change
|
||||
self.refresh()
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return self._label._text
|
||||
|
||||
def set_parent_rect(self, parent_rect):
|
||||
super().set_parent_rect(parent_rect)
|
||||
self._rect.width = parent_rect.width
|
||||
|
||||
def _on_clicked(self):
|
||||
if ui_state.params.get("CarPlatformBundle"):
|
||||
ui_state.params.remove("CarPlatformBundle")
|
||||
self.refresh()
|
||||
if self._on_platform_change:
|
||||
self._on_platform_change()
|
||||
else:
|
||||
self._show_platform_dialog()
|
||||
|
||||
def _set_platform(self, platform_name):
|
||||
if data := self._platforms.get(platform_name):
|
||||
ui_state.params.put("CarPlatformBundle", {**data, "name": platform_name})
|
||||
self.refresh()
|
||||
if self._on_platform_change:
|
||||
self._on_platform_change()
|
||||
|
||||
def _on_platform_selected(self, dialog, res):
|
||||
if res == DialogResult.CONFIRM and dialog.selection_ref:
|
||||
offroad_msg = tr("This setting will take effect immediately.") if ui_state.is_offroad else \
|
||||
tr("This setting will take effect once the device enters offroad state.")
|
||||
|
||||
confirm_dialog = ConfirmDialog(offroad_msg, tr("Confirm"))
|
||||
|
||||
callback = partial(self._confirm_platform, dialog.selection_ref)
|
||||
gui_app.set_modal_overlay(confirm_dialog, callback=callback)
|
||||
|
||||
def _confirm_platform(self, platform_name, res):
|
||||
if res == DialogResult.CONFIRM:
|
||||
self._set_platform(platform_name)
|
||||
|
||||
def _show_platform_dialog(self):
|
||||
platforms = sorted(self._platforms.keys())
|
||||
makes = sorted({self._platforms[p].get('make') for p in platforms})
|
||||
folders = [TreeFolder(make, [TreeNode(p, {
|
||||
'display_name': p,
|
||||
'search_tags': f"{p} {self._platforms[p].get('make')} {' '.join(map(str, self._platforms[p].get('year', [])))} {self._platforms[p].get('model', p)}"
|
||||
}) for p in platforms if self._platforms[p].get('make') == make]) for make in makes]
|
||||
dialog = TreeOptionDialog(
|
||||
tr("Select a vehicle"),
|
||||
folders,
|
||||
search_title=tr("Search your vehicle"),
|
||||
search_subtitle=tr("Enter model year (e.g., 2021) and model (Toyota Corolla):"),
|
||||
search_funcs=[lambda node: node.data.get('display_name', ''), lambda node: node.data.get('search_tags', '')]
|
||||
)
|
||||
callback = partial(self._on_platform_selected, dialog)
|
||||
dialog.on_exit = callback
|
||||
gui_app.set_modal_overlay(dialog, callback=callback)
|
||||
|
||||
def refresh(self):
|
||||
self.color = style.YELLOW
|
||||
self._platform = tr("Unrecognized Vehicle")
|
||||
self.set_text(tr("No vehicle selected"))
|
||||
|
||||
if bundle := ui_state.params.get("CarPlatformBundle"):
|
||||
self._platform = bundle.get("name", "")
|
||||
self.set_text(self._platform)
|
||||
self.color = style.BLUE
|
||||
elif ui_state.CP and ui_state.CP.carFingerprint != "MOCK":
|
||||
self._platform = ui_state.CP.carFingerprint
|
||||
self.set_text(self._platform)
|
||||
self.color = style.GREEN
|
||||
self.set_enabled(True)
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
|
||||
class VisualsLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
items = [
|
||||
|
||||
]
|
||||
return items
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from cereal import messaging, custom
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState
|
||||
|
||||
|
||||
class UIStateSP:
|
||||
def __init__(self):
|
||||
self.params = Params()
|
||||
self.sm_services_ext = [
|
||||
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
|
||||
"gpsLocation", "liveTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP"
|
||||
]
|
||||
|
||||
self.sunnylink_state = SunnylinkState()
|
||||
|
||||
def update(self) -> None:
|
||||
self.sunnylink_state.start()
|
||||
|
||||
def update_params(self) -> None:
|
||||
CP_SP_bytes = self.params.get("CarParamsSPPersistent")
|
||||
if CP_SP_bytes is not None:
|
||||
self.CP_SP = messaging.log_from_bytes(CP_SP_bytes, custom.CarParamsSP)
|
||||
self.sunnylink_enabled = self.params.get_bool("SunnylinkEnabled")
|
||||
@@ -41,47 +41,47 @@ def put_update_params(params: Params):
|
||||
params.put("UpdaterTargetBranch", BRANCH_NAME)
|
||||
|
||||
|
||||
def setup_homescreen(click, pm: PubMaster):
|
||||
def setup_homescreen(click, pm: PubMaster, scroll=None):
|
||||
pass
|
||||
|
||||
|
||||
def setup_homescreen_update_available(click, pm: PubMaster):
|
||||
def setup_homescreen_update_available(click, pm: PubMaster, scroll=None):
|
||||
params = Params()
|
||||
params.put_bool("UpdateAvailable", True)
|
||||
put_update_params(params)
|
||||
setup_offroad_alert(click, pm)
|
||||
|
||||
|
||||
def setup_settings(click, pm: PubMaster):
|
||||
def setup_settings(click, pm: PubMaster, scroll=None):
|
||||
click(100, 100)
|
||||
|
||||
|
||||
def close_settings(click, pm: PubMaster):
|
||||
click(240, 216)
|
||||
def close_settings(click, pm: PubMaster, scroll=None):
|
||||
click(140, 120)
|
||||
|
||||
|
||||
def setup_settings_network(click, pm: PubMaster):
|
||||
def setup_settings_network(click, pm: PubMaster, scroll=None):
|
||||
setup_settings(click, pm)
|
||||
click(278, 450)
|
||||
|
||||
|
||||
def setup_settings_network_advanced(click, pm: PubMaster):
|
||||
setup_settings_network(click, pm)
|
||||
def setup_settings_network_advanced(click, pm: PubMaster, scroll=None):
|
||||
setup_settings_network(click, pm, scroll=scroll)
|
||||
click(1880, 100)
|
||||
|
||||
|
||||
def setup_settings_toggles(click, pm: PubMaster):
|
||||
def setup_settings_toggles(click, pm: PubMaster, scroll=None):
|
||||
setup_settings(click, pm)
|
||||
click(278, 600)
|
||||
click(278, 620)
|
||||
|
||||
|
||||
def setup_settings_software(click, pm: PubMaster):
|
||||
def setup_settings_software(click, pm: PubMaster, scroll=None):
|
||||
put_update_params(Params())
|
||||
setup_settings(click, pm)
|
||||
click(278, 720)
|
||||
click(278, 730)
|
||||
|
||||
|
||||
def setup_settings_software_download(click, pm: PubMaster):
|
||||
def setup_settings_software_download(click, pm: PubMaster, scroll=None):
|
||||
params = Params()
|
||||
# setup_settings_software but with "DOWNLOAD" button to test long text
|
||||
params.put("UpdaterState", "idle")
|
||||
@@ -89,13 +89,13 @@ def setup_settings_software_download(click, pm: PubMaster):
|
||||
setup_settings_software(click, pm)
|
||||
|
||||
|
||||
def setup_settings_software_release_notes(click, pm: PubMaster):
|
||||
setup_settings_software(click, pm)
|
||||
def setup_settings_software_release_notes(click, pm: PubMaster, scroll=None):
|
||||
setup_settings_software(click, pm, scroll=scroll)
|
||||
click(588, 110) # expand description for current version
|
||||
|
||||
|
||||
def setup_settings_software_branch_switcher(click, pm: PubMaster):
|
||||
setup_settings_software(click, pm)
|
||||
def setup_settings_software_branch_switcher(click, pm: PubMaster, scroll=None):
|
||||
setup_settings_software(click, pm, scroll=scroll)
|
||||
params = Params()
|
||||
params.put("UpdaterAvailableBranches", f"master,nightly,release,{BRANCH_NAME}")
|
||||
params.put("GitBranch", BRANCH_NAME) # should be on top
|
||||
@@ -103,30 +103,32 @@ def setup_settings_software_branch_switcher(click, pm: PubMaster):
|
||||
click(1984, 449)
|
||||
|
||||
|
||||
def setup_settings_firehose(click, pm: PubMaster):
|
||||
def setup_settings_firehose(click, pm: PubMaster, scroll=None):
|
||||
setup_settings(click, pm)
|
||||
click(278, 845)
|
||||
scroll(-20, 278, 950)
|
||||
click(278, 850)
|
||||
|
||||
|
||||
def setup_settings_developer(click, pm: PubMaster):
|
||||
def setup_settings_developer(click, pm: PubMaster, scroll=None):
|
||||
CP = car.CarParams()
|
||||
CP.alphaLongitudinalAvailable = True # show alpha long control toggle
|
||||
Params().put("CarParamsPersistent", CP.to_bytes())
|
||||
|
||||
setup_settings(click, pm)
|
||||
scroll(-20, 278, 950)
|
||||
click(278, 950)
|
||||
|
||||
|
||||
def setup_keyboard(click, pm: PubMaster):
|
||||
setup_settings_developer(click, pm)
|
||||
def setup_keyboard(click, pm: PubMaster, scroll=None):
|
||||
setup_settings_developer(click, pm, scroll=scroll)
|
||||
click(1930, 470)
|
||||
|
||||
|
||||
def setup_pair_device(click, pm: PubMaster):
|
||||
def setup_pair_device(click, pm: PubMaster, scroll=None):
|
||||
click(1950, 800)
|
||||
|
||||
|
||||
def setup_offroad_alert(click, pm: PubMaster):
|
||||
def setup_offroad_alert(click, pm: PubMaster, scroll=None):
|
||||
put_update_params(Params())
|
||||
set_offroad_alert("Offroad_TemperatureTooHigh", True, extra_text='99C')
|
||||
set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text='longitudinal')
|
||||
@@ -137,22 +139,73 @@ def setup_offroad_alert(click, pm: PubMaster):
|
||||
close_settings(click, pm)
|
||||
|
||||
|
||||
def setup_confirmation_dialog(click, pm: PubMaster):
|
||||
def setup_confirmation_dialog(click, pm: PubMaster, scroll=None):
|
||||
setup_settings(click, pm)
|
||||
click(1985, 791) # reset calibration
|
||||
|
||||
|
||||
def setup_experimental_mode_description(click, pm: PubMaster):
|
||||
def setup_experimental_mode_description(click, pm: PubMaster, scroll=None):
|
||||
setup_settings_toggles(click, pm)
|
||||
click(1200, 280) # expand description for experimental mode
|
||||
|
||||
|
||||
def setup_openpilot_long_confirmation_dialog(click, pm: PubMaster):
|
||||
setup_settings_developer(click, pm)
|
||||
click(650, 960) # toggle openpilot longitudinal control
|
||||
def setup_openpilot_long_confirmation_dialog(click, pm: PubMaster, scroll=None):
|
||||
setup_settings_developer(click, pm, scroll=scroll)
|
||||
click(650, 960) # toggle sunnypilot longitudinal control
|
||||
|
||||
|
||||
def setup_onroad(click, pm: PubMaster):
|
||||
def setup_settings_sunnylink(click, pm: PubMaster, scroll=None):
|
||||
setup_settings(click, pm)
|
||||
click(278, 510)
|
||||
|
||||
|
||||
def setup_settings_models(click, pm: PubMaster, scroll=None):
|
||||
setup_settings(click, pm)
|
||||
click(278, 840)
|
||||
|
||||
|
||||
def setup_settings_steering(click, pm: PubMaster, scroll=None):
|
||||
setup_settings(click, pm)
|
||||
click(278, 950)
|
||||
|
||||
|
||||
def setup_settings_cruise(click, pm: PubMaster, scroll=None):
|
||||
setup_settings(click, pm)
|
||||
scroll(-4, 278, 950)
|
||||
click(278, 860)
|
||||
|
||||
|
||||
def setup_settings_visuals(click, pm: PubMaster, scroll=None):
|
||||
setup_settings(click, pm)
|
||||
scroll(-20, 278, 950)
|
||||
click(278, 330)
|
||||
|
||||
|
||||
def setup_settings_display(click, pm: PubMaster, scroll=None):
|
||||
setup_settings(click, pm)
|
||||
scroll(-20, 278, 950)
|
||||
click(278, 420)
|
||||
|
||||
|
||||
def setup_settings_osm(click, pm: PubMaster, scroll=None):
|
||||
setup_settings(click, pm)
|
||||
scroll(-20, 278, 950)
|
||||
click(278, 520)
|
||||
|
||||
|
||||
def setup_settings_trips(click, pm: PubMaster, scroll=None):
|
||||
setup_settings(click, pm)
|
||||
scroll(-20, 278, 950)
|
||||
click(278, 630)
|
||||
|
||||
|
||||
def setup_settings_vehicle(click, pm: PubMaster, scroll=None):
|
||||
setup_settings(click, pm)
|
||||
scroll(-20, 278, 950)
|
||||
click(278, 750)
|
||||
|
||||
|
||||
def setup_onroad(click, pm: PubMaster, scroll=None):
|
||||
ds = messaging.new_message('deviceState')
|
||||
ds.deviceState.started = True
|
||||
|
||||
@@ -173,7 +226,7 @@ def setup_onroad(click, pm: PubMaster):
|
||||
time.sleep(0.05)
|
||||
|
||||
|
||||
def setup_onroad_sidebar(click, pm: PubMaster):
|
||||
def setup_onroad_sidebar(click, pm: PubMaster, scroll=None):
|
||||
setup_onroad(click, pm)
|
||||
click(100, 100) # open sidebar
|
||||
|
||||
@@ -192,23 +245,23 @@ def setup_onroad_alert(click, pm: PubMaster, size: log.SelfdriveState.AlertSize,
|
||||
time.sleep(0.05)
|
||||
|
||||
|
||||
def setup_onroad_small_alert(click, pm: PubMaster):
|
||||
def setup_onroad_small_alert(click, pm: PubMaster, scroll=None):
|
||||
setup_onroad_alert(click, pm, AlertSize.small, "Small Alert", "This is a small alert", AlertStatus.normal)
|
||||
|
||||
|
||||
def setup_onroad_medium_alert(click, pm: PubMaster):
|
||||
def setup_onroad_medium_alert(click, pm: PubMaster, scroll=None):
|
||||
setup_onroad_alert(click, pm, AlertSize.mid, "Medium Alert", "This is a medium alert", AlertStatus.userPrompt)
|
||||
|
||||
|
||||
def setup_onroad_full_alert(click, pm: PubMaster):
|
||||
def setup_onroad_full_alert(click, pm: PubMaster, scroll=None):
|
||||
setup_onroad_alert(click, pm, AlertSize.full, "DISENGAGE IMMEDIATELY", "Driver Distracted", AlertStatus.critical)
|
||||
|
||||
|
||||
def setup_onroad_full_alert_multiline(click, pm: PubMaster):
|
||||
def setup_onroad_full_alert_multiline(click, pm: PubMaster, scroll=None):
|
||||
setup_onroad_alert(click, pm, AlertSize.full, "Reverse\nGear", "", AlertStatus.normal)
|
||||
|
||||
|
||||
def setup_onroad_full_alert_long_text(click, pm: PubMaster):
|
||||
def setup_onroad_full_alert_long_text(click, pm: PubMaster, scroll=None):
|
||||
setup_onroad_alert(click, pm, AlertSize.full, "TAKE CONTROL IMMEDIATELY", "Calibration Invalid: Remount Device & Recalibrate", AlertStatus.userPrompt)
|
||||
|
||||
|
||||
@@ -243,6 +296,19 @@ CASES = {
|
||||
"onroad_full_alert_long_text": setup_onroad_full_alert_long_text,
|
||||
}
|
||||
|
||||
# sunnypilot cases
|
||||
CASES.update({
|
||||
"settings_sunnylink": setup_settings_sunnylink,
|
||||
"settings_models": setup_settings_models,
|
||||
"settings_steering": setup_settings_steering,
|
||||
"settings_cruise": setup_settings_cruise,
|
||||
"settings_visuals": setup_settings_visuals,
|
||||
"settings_display": setup_settings_display,
|
||||
"settings_osm": setup_settings_osm,
|
||||
"settings_trips": setup_settings_trips,
|
||||
"settings_vehicle": setup_settings_vehicle,
|
||||
})
|
||||
|
||||
|
||||
class TestUI:
|
||||
def __init__(self):
|
||||
@@ -276,11 +342,20 @@ class TestUI:
|
||||
time.sleep(0.01)
|
||||
pyautogui.mouseUp(self.ui.left + x, self.ui.top + y, *args, **kwargs)
|
||||
|
||||
def scroll(self, clicks: int, x, y, *args, **kwargs):
|
||||
if clicks == 0:
|
||||
return
|
||||
click = -1 if clicks < 0 else 1 # -1 = down, 1 = up
|
||||
for _ in range(abs(clicks)):
|
||||
pyautogui.scroll(click, self.ui.left + x, self.ui.top + y, *args, **kwargs) # scroll for individual clicks since we need to delay between clicks
|
||||
time.sleep(0.01) # small delay between scroll clicks to work properly
|
||||
time.sleep(2) # wait for scroll to fully settle
|
||||
|
||||
@with_processes(["ui"])
|
||||
def test_ui(self, name, setup_case):
|
||||
self.setup()
|
||||
time.sleep(UI_DELAY) # wait for UI to start
|
||||
setup_case(self.click, self.pm)
|
||||
setup_case(self.click, self.pm, self.scroll)
|
||||
self.screenshot(name)
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ from openpilot.selfdrive.ui.lib.prime_state import PrimeState
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.hardware import HARDWARE, PC
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP
|
||||
|
||||
BACKLIGHT_OFFROAD = 65 if HARDWARE.get_device_type() == "mici" else 50
|
||||
|
||||
|
||||
@@ -21,7 +23,7 @@ class UIStatus(Enum):
|
||||
OVERRIDE = "override"
|
||||
|
||||
|
||||
class UIState:
|
||||
class UIState(UIStateSP):
|
||||
_instance: 'UIState | None' = None
|
||||
|
||||
def __new__(cls):
|
||||
@@ -31,6 +33,7 @@ class UIState:
|
||||
return cls._instance
|
||||
|
||||
def _initialize(self):
|
||||
UIStateSP.__init__(self)
|
||||
self.params = Params()
|
||||
self.sm = messaging.SubMaster(
|
||||
[
|
||||
@@ -55,7 +58,7 @@ class UIState:
|
||||
"carControl",
|
||||
"liveParameters",
|
||||
"rawAudioData",
|
||||
]
|
||||
] + self.sm_services_ext
|
||||
)
|
||||
|
||||
self.prime_state = PrimeState()
|
||||
@@ -111,6 +114,7 @@ class UIState:
|
||||
if time.monotonic() - self._param_update_time > 5.0:
|
||||
self.update_params()
|
||||
device.update()
|
||||
UIStateSP.update(self)
|
||||
|
||||
def _update_state(self) -> None:
|
||||
# Handle panda states updates
|
||||
@@ -180,6 +184,7 @@ class UIState:
|
||||
self.has_longitudinal_control = self.params.get_bool("AlphaLongitudinalEnabled")
|
||||
else:
|
||||
self.has_longitudinal_control = self.CP.openpilotLongitudinalControl
|
||||
UIStateSP.update_params(self)
|
||||
self._param_update_time = time.monotonic()
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import glob
|
||||
|
||||
Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'visionipc', 'transformations')
|
||||
lenv = env.Clone()
|
||||
lenvCython = envCython.Clone()
|
||||
@@ -23,12 +21,6 @@ thneed_src_qcom = thneed_src_common + ["thneed/thneed_qcom2.cc"]
|
||||
thneed_src_pc = thneed_src_common + ["thneed/thneed_pc.cc"]
|
||||
thneed_src = thneed_src_qcom if arch == "larch64" else thneed_src_pc
|
||||
|
||||
# SNPE except on Mac and ARM Linux
|
||||
snpe_lib = []
|
||||
if arch != "Darwin" and arch != "aarch64":
|
||||
common_src += ['runners/snpemodel.cc']
|
||||
snpe_lib += ['SNPE']
|
||||
|
||||
# OpenCL is a framework on Mac
|
||||
if arch == "Darwin":
|
||||
frameworks += ['OpenCL']
|
||||
@@ -40,20 +32,13 @@ for pathdef, fn in {'TRANSFORM': 'transforms/transform.cl', 'LOADYUV': 'transfor
|
||||
for xenv in (lenv, lenvCython):
|
||||
xenv['CXXFLAGS'].append(f'-D{pathdef}_PATH=\\"{File(fn).abspath}\\"')
|
||||
|
||||
# Compile cython
|
||||
snpe_rpath_qcom = "/data/pythonpath/third_party/snpe/larch64"
|
||||
snpe_rpath_pc = f"{Dir('#').abspath}/third_party/snpe/x86_64-linux-clang"
|
||||
snpe_rpath = lenvCython['RPATH'] + [snpe_rpath_qcom if arch == "larch64" else snpe_rpath_pc]
|
||||
|
||||
cython_libs = envCython["LIBS"] + libs
|
||||
snpemodel_lib = lenv.Library('snpemodel', ['runners/snpemodel.cc'])
|
||||
commonmodel_lib = lenv.Library('commonmodel', common_src)
|
||||
|
||||
lenvCython.Program('runners/runmodel_pyx.so', 'runners/runmodel_pyx.pyx', LIBS=cython_libs, FRAMEWORKS=frameworks)
|
||||
lenvCython.Program('runners/snpemodel_pyx.so', 'runners/snpemodel_pyx.pyx', LIBS=[snpemodel_lib, snpe_lib, *cython_libs], FRAMEWORKS=frameworks, RPATH=snpe_rpath)
|
||||
lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks)
|
||||
|
||||
if arch == "larch64":
|
||||
thneed_lib = env.SharedLibrary('thneed', thneed_src, LIBS=[common, 'OpenCL', 'dl'])
|
||||
thneedmodel_lib = env.Library('thneedmodel', ['runners/thneedmodel.cc'])
|
||||
lenvCython.Program('runners/thneedmodel_pyx.so', 'runners/thneedmodel_pyx.pyx', LIBS=envCython["LIBS"]+[thneedmodel_lib, thneed_lib, common, 'dl', 'OpenCL'])
|
||||
lenvCython.Program('runners/thneedmodel_pyx.so', 'runners/thneedmodel_pyx.pyx', LIBS=envCython["LIBS"]+[thneedmodel_lib, thneed_lib, common, 'dl', 'OpenCL'])
|
||||
|
||||
@@ -4,20 +4,15 @@ from openpilot.sunnypilot.modeld.runners.runmodel_pyx import RunModel, Runtime
|
||||
assert Runtime
|
||||
|
||||
USE_THNEED = int(os.getenv('USE_THNEED', str(int(TICI))))
|
||||
USE_SNPE = int(os.getenv('USE_SNPE', str(int(TICI))))
|
||||
|
||||
class ModelRunner(RunModel):
|
||||
THNEED = 'THNEED'
|
||||
SNPE = 'SNPE'
|
||||
ONNX = 'ONNX'
|
||||
|
||||
def __new__(cls, paths, *args, **kwargs):
|
||||
if ModelRunner.THNEED in paths and USE_THNEED:
|
||||
from openpilot.sunnypilot.modeld.runners.thneedmodel_pyx import ThneedModel as Runner
|
||||
runner_type = ModelRunner.THNEED
|
||||
elif ModelRunner.SNPE in paths and USE_SNPE:
|
||||
from openpilot.sunnypilot.modeld.runners.snpemodel_pyx import SNPEModel as Runner
|
||||
runner_type = ModelRunner.SNPE
|
||||
elif ModelRunner.ONNX in paths:
|
||||
from openpilot.sunnypilot.modeld.runners.onnxmodel import ONNXModel as Runner
|
||||
runner_type = ModelRunner.ONNX
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include "sunnypilot/modeld/runners/runmodel.h"
|
||||
#include "sunnypilot/modeld/runners/snpemodel.h"
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
#pragma clang diagnostic ignored "-Wexceptions"
|
||||
|
||||
#include "sunnypilot/modeld/runners/snpemodel.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "common/util.h"
|
||||
#include "common/timing.h"
|
||||
|
||||
void PrintErrorStringAndExit() {
|
||||
std::cerr << zdl::DlSystem::getLastErrorString() << std::endl;
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
SNPEModel::SNPEModel(const std::string path, float *_output, size_t _output_size, int runtime, bool _use_tf8, cl_context context) {
|
||||
output = _output;
|
||||
output_size = _output_size;
|
||||
use_tf8 = _use_tf8;
|
||||
|
||||
#ifdef QCOM2
|
||||
if (runtime == USE_GPU_RUNTIME) {
|
||||
snpe_runtime = zdl::DlSystem::Runtime_t::GPU;
|
||||
} else if (runtime == USE_DSP_RUNTIME) {
|
||||
snpe_runtime = zdl::DlSystem::Runtime_t::DSP;
|
||||
} else {
|
||||
snpe_runtime = zdl::DlSystem::Runtime_t::CPU;
|
||||
}
|
||||
assert(zdl::SNPE::SNPEFactory::isRuntimeAvailable(snpe_runtime));
|
||||
#endif
|
||||
model_data = util::read_file(path);
|
||||
assert(model_data.size() > 0);
|
||||
|
||||
// load model
|
||||
std::unique_ptr<zdl::DlContainer::IDlContainer> container = zdl::DlContainer::IDlContainer::open((uint8_t*)model_data.data(), model_data.size());
|
||||
if (!container) { PrintErrorStringAndExit(); }
|
||||
LOGW("loaded model with size: %lu", model_data.size());
|
||||
|
||||
// create model runner
|
||||
zdl::SNPE::SNPEBuilder snpe_builder(container.get());
|
||||
while (!snpe) {
|
||||
#ifdef QCOM2
|
||||
snpe = snpe_builder.setOutputLayers({})
|
||||
.setRuntimeProcessor(snpe_runtime)
|
||||
.setUseUserSuppliedBuffers(true)
|
||||
.setPerformanceProfile(zdl::DlSystem::PerformanceProfile_t::HIGH_PERFORMANCE)
|
||||
.build();
|
||||
#else
|
||||
snpe = snpe_builder.setOutputLayers({})
|
||||
.setUseUserSuppliedBuffers(true)
|
||||
.setPerformanceProfile(zdl::DlSystem::PerformanceProfile_t::HIGH_PERFORMANCE)
|
||||
.build();
|
||||
#endif
|
||||
if (!snpe) std::cerr << zdl::DlSystem::getLastErrorString() << std::endl;
|
||||
}
|
||||
|
||||
// create output buffer
|
||||
zdl::DlSystem::UserBufferEncodingFloat ub_encoding_float;
|
||||
zdl::DlSystem::IUserBufferFactory &ub_factory = zdl::SNPE::SNPEFactory::getUserBufferFactory();
|
||||
|
||||
const auto &output_tensor_names_opt = snpe->getOutputTensorNames();
|
||||
if (!output_tensor_names_opt) throw std::runtime_error("Error obtaining output tensor names");
|
||||
const auto &output_tensor_names = *output_tensor_names_opt;
|
||||
assert(output_tensor_names.size() == 1);
|
||||
const char *output_tensor_name = output_tensor_names.at(0);
|
||||
const zdl::DlSystem::TensorShape &buffer_shape = snpe->getInputOutputBufferAttributes(output_tensor_name)->getDims();
|
||||
if (output_size != 0) {
|
||||
assert(output_size == buffer_shape[1]);
|
||||
} else {
|
||||
output_size = buffer_shape[1];
|
||||
}
|
||||
std::vector<size_t> output_strides = {output_size * sizeof(float), sizeof(float)};
|
||||
output_buffer = ub_factory.createUserBuffer(output, output_size * sizeof(float), output_strides, &ub_encoding_float);
|
||||
output_map.add(output_tensor_name, output_buffer.get());
|
||||
}
|
||||
|
||||
void SNPEModel::addInput(const std::string name, float *buffer, int size) {
|
||||
const int idx = inputs.size();
|
||||
const auto &input_tensor_names_opt = snpe->getInputTensorNames();
|
||||
if (!input_tensor_names_opt) throw std::runtime_error("Error obtaining input tensor names");
|
||||
const auto &input_tensor_names = *input_tensor_names_opt;
|
||||
const char *input_tensor_name = input_tensor_names.at(idx);
|
||||
const bool input_tf8 = use_tf8 && strcmp(input_tensor_name, "input_img") == 0; // TODO: This is a terrible hack, get rid of this name check both here and in onnx_runner.py
|
||||
LOGW("adding index %d: %s", idx, input_tensor_name);
|
||||
|
||||
zdl::DlSystem::UserBufferEncodingFloat ub_encoding_float;
|
||||
zdl::DlSystem::UserBufferEncodingTf8 ub_encoding_tf8(0, 1./255); // network takes 0-1
|
||||
zdl::DlSystem::IUserBufferFactory &ub_factory = zdl::SNPE::SNPEFactory::getUserBufferFactory();
|
||||
zdl::DlSystem::UserBufferEncoding *input_encoding = input_tf8 ? (zdl::DlSystem::UserBufferEncoding*)&ub_encoding_tf8 : (zdl::DlSystem::UserBufferEncoding*)&ub_encoding_float;
|
||||
|
||||
const auto &buffer_shape_opt = snpe->getInputDimensions(input_tensor_name);
|
||||
const zdl::DlSystem::TensorShape &buffer_shape = *buffer_shape_opt;
|
||||
size_t size_of_input = input_tf8 ? sizeof(uint8_t) : sizeof(float);
|
||||
std::vector<size_t> strides(buffer_shape.rank());
|
||||
strides[strides.size() - 1] = size_of_input;
|
||||
size_t product = 1;
|
||||
for (size_t i = 0; i < buffer_shape.rank(); i++) product *= buffer_shape[i];
|
||||
size_t stride = strides[strides.size() - 1];
|
||||
for (size_t i = buffer_shape.rank() - 1; i > 0; i--) {
|
||||
stride *= buffer_shape[i];
|
||||
strides[i-1] = stride;
|
||||
}
|
||||
|
||||
auto input_buffer = ub_factory.createUserBuffer(buffer, product*size_of_input, strides, input_encoding);
|
||||
input_map.add(input_tensor_name, input_buffer.get());
|
||||
inputs.push_back(std::unique_ptr<SNPEModelInput>(new SNPEModelInput(name, buffer, size, std::move(input_buffer))));
|
||||
}
|
||||
|
||||
void SNPEModel::execute() {
|
||||
if (!snpe->execute(input_map, output_map)) {
|
||||
PrintErrorStringAndExit();
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
#pragma once
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <DlContainer/IDlContainer.hpp>
|
||||
#include <DlSystem/DlError.hpp>
|
||||
#include <DlSystem/ITensor.hpp>
|
||||
#include <DlSystem/ITensorFactory.hpp>
|
||||
#include <DlSystem/IUserBuffer.hpp>
|
||||
#include <DlSystem/IUserBufferFactory.hpp>
|
||||
#include <SNPE/SNPE.hpp>
|
||||
#include <SNPE/SNPEBuilder.hpp>
|
||||
#include <SNPE/SNPEFactory.hpp>
|
||||
|
||||
#include "sunnypilot/modeld/runners/runmodel.h"
|
||||
|
||||
struct SNPEModelInput : public ModelInput {
|
||||
std::unique_ptr<zdl::DlSystem::IUserBuffer> snpe_buffer;
|
||||
|
||||
SNPEModelInput(const std::string _name, float *_buffer, int _size, std::unique_ptr<zdl::DlSystem::IUserBuffer> _snpe_buffer) : ModelInput(_name, _buffer, _size), snpe_buffer(std::move(_snpe_buffer)) {}
|
||||
void setBuffer(float *_buffer, int _size) {
|
||||
ModelInput::setBuffer(_buffer, _size);
|
||||
assert(snpe_buffer->setBufferAddress(_buffer) == true);
|
||||
}
|
||||
};
|
||||
|
||||
class SNPEModel : public RunModel {
|
||||
public:
|
||||
SNPEModel(const std::string path, float *_output, size_t _output_size, int runtime, bool use_tf8 = false, cl_context context = NULL);
|
||||
void addInput(const std::string name, float *buffer, int size);
|
||||
void execute();
|
||||
|
||||
private:
|
||||
std::string model_data;
|
||||
|
||||
#ifdef QCOM2
|
||||
zdl::DlSystem::Runtime_t snpe_runtime;
|
||||
#endif
|
||||
|
||||
// snpe model stuff
|
||||
std::unique_ptr<zdl::SNPE::SNPE> snpe;
|
||||
zdl::DlSystem::UserBufferMap input_map;
|
||||
zdl::DlSystem::UserBufferMap output_map;
|
||||
std::unique_ptr<zdl::DlSystem::IUserBuffer> output_buffer;
|
||||
|
||||
bool use_tf8;
|
||||
float *output;
|
||||
size_t output_size;
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
# distutils: language = c++
|
||||
|
||||
from libcpp.string cimport string
|
||||
|
||||
from msgq.visionipc.visionipc cimport cl_context
|
||||
|
||||
cdef extern from "sunnypilot/modeld/runners/snpemodel.h":
|
||||
cdef cppclass SNPEModel:
|
||||
SNPEModel(string, float*, size_t, int, bool, cl_context)
|
||||
@@ -1,17 +0,0 @@
|
||||
# distutils: language = c++
|
||||
# cython: c_string_encoding=ascii, language_level=3
|
||||
|
||||
import os
|
||||
from libcpp cimport bool
|
||||
from libcpp.string cimport string
|
||||
|
||||
from .snpemodel cimport SNPEModel as cppSNPEModel
|
||||
from openpilot.sunnypilot.modeld.models.commonmodel_pyx cimport CLContext
|
||||
from openpilot.sunnypilot.modeld.runners.runmodel_pyx cimport RunModel
|
||||
from openpilot.sunnypilot.modeld.runners.runmodel cimport RunModel as cppRunModel
|
||||
|
||||
os.environ['ADSP_LIBRARY_PATH'] = "/data/pythonpath/third_party/snpe/dsp/"
|
||||
|
||||
cdef class SNPEModel(RunModel):
|
||||
def __cinit__(self, string path, float[:] output, int runtime, bool use_tf8, CLContext context):
|
||||
self.model = <cppRunModel *> new cppSNPEModel(path, &output[0], len(output), runtime, use_tf8, context.context)
|
||||
@@ -116,7 +116,7 @@ class ModelCache:
|
||||
|
||||
class ModelFetcher:
|
||||
"""Handles fetching and caching of model data from remote source"""
|
||||
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-docs/refs/heads/gh-pages/docs/driving_models_v9.json"
|
||||
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-docs/refs/heads/gh-pages/docs/driving_models_v10.json"
|
||||
|
||||
def __init__(self, params: Params):
|
||||
self.params = params
|
||||
|
||||
@@ -19,8 +19,8 @@ from openpilot.system.hardware.hw import Paths
|
||||
from pathlib import Path
|
||||
|
||||
# see the README.md for more details on the model selector versioning
|
||||
CURRENT_SELECTOR_VERSION = 11
|
||||
REQUIRED_MIN_SELECTOR_VERSION = 11
|
||||
CURRENT_SELECTOR_VERSION = 12
|
||||
REQUIRED_MIN_SELECTOR_VERSION = 12
|
||||
|
||||
USE_ONNX = os.getenv('USE_ONNX', PC)
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a258a022f0ab90368327d899ed4fb85b47dd0e35c93508e35851d9c3184528c0
|
||||
size 36382
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8623dbc5c7dd043a91d98777cb423cfd116014ed6390af6d7d00c1f8dea3c6e8
|
||||
size 1181
|
||||
@@ -4,21 +4,27 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import time
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from cereal import log, custom
|
||||
|
||||
from opendbc.car import structs
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
|
||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||
from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.blinker_pause_lateral import BlinkerPauseLateral
|
||||
|
||||
|
||||
class ControlsExt:
|
||||
class ControlsExt(ModelStateBase):
|
||||
def __init__(self, CP: structs.CarParams, params: Params):
|
||||
ModelStateBase.__init__(self)
|
||||
self.CP = CP
|
||||
self.params = params
|
||||
self._param_update_time: float = 0.0
|
||||
self.blinker_pause_lateral = BlinkerPauseLateral()
|
||||
self.get_params_sp()
|
||||
|
||||
cloudlog.info("controlsd_ext is waiting for CarParamsSP")
|
||||
self.CP_SP = messaging.log_from_bytes(params.get("CarParamsSP", block=True), custom.CarParamsSP)
|
||||
@@ -27,8 +33,14 @@ class ControlsExt:
|
||||
self.sm_services_ext = ['radarState', 'selfdriveStateSP']
|
||||
self.pm_services_ext = ['carControlSP']
|
||||
|
||||
def get_params_sp(self) -> None:
|
||||
self.blinker_pause_lateral.get_params()
|
||||
def get_params_sp(self, sm: messaging.SubMaster) -> None:
|
||||
if time.monotonic() - self._param_update_time > PARAMS_UPDATE_PERIOD:
|
||||
self.blinker_pause_lateral.get_params()
|
||||
|
||||
if self.CP.lateralTuning.which() == 'torque':
|
||||
self.lat_delay = get_lat_delay(self.params, sm["liveDelay"].lateralDelay)
|
||||
|
||||
self._param_update_time = time.monotonic()
|
||||
|
||||
def get_lat_active(self, sm: messaging.SubMaster) -> bool:
|
||||
if self.blinker_pause_lateral.update(sm['carState']):
|
||||
|
||||
@@ -3,7 +3,6 @@ import os
|
||||
import random
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import jwt
|
||||
from openpilot.common.api.base import BaseApi
|
||||
@@ -81,23 +80,19 @@ class SunnylinkApi(BaseApi):
|
||||
if sunnylink_dongle_id not in (None, UNREGISTERED_SUNNYLINK_DONGLE_ID):
|
||||
return sunnylink_dongle_id
|
||||
|
||||
privkey_path = Path(f"{Paths.persist_root()}/comma/id_rsa")
|
||||
pubkey_path = Path(f"{Paths.persist_root()}/comma/id_rsa.pub")
|
||||
jwt_algo, private_key, public_key = BaseApi.get_key_pair()
|
||||
|
||||
start_time = time.monotonic()
|
||||
successful_registration = False
|
||||
if not pubkey_path.is_file():
|
||||
if not public_key:
|
||||
sunnylink_dongle_id = UNREGISTERED_SUNNYLINK_DONGLE_ID
|
||||
self._status_update("Public key not found, setting dongle ID to unregistered.")
|
||||
else:
|
||||
Params().put("LastSunnylinkPingTime", 0) # Reset the last ping time to 0 if we are trying to register
|
||||
with pubkey_path.open() as f1, privkey_path.open() as f2:
|
||||
public_key = f1.read()
|
||||
private_key = f2.read()
|
||||
|
||||
backoff = 1
|
||||
while True:
|
||||
register_token = jwt.encode({'register': True, 'exp': datetime.utcnow() + timedelta(hours=1)}, private_key, algorithm='RS256')
|
||||
register_token = jwt.encode({'register': True, 'exp': datetime.utcnow() + timedelta(hours=1)}, private_key, algorithm=jwt_algo)
|
||||
try:
|
||||
if verbose or time.monotonic() - start_time < timeout / 2:
|
||||
self._status_update("Registering device to sunnylink...")
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
@@ -32,9 +37,11 @@ LOCAL_PORT_WHITELIST = {8022}
|
||||
SUNNYLINK_LOG_ATTR_NAME = "user.sunny.upload"
|
||||
SUNNYLINK_RECONNECT_TIMEOUT_S = 70 # FYI changing this will also would require a change on sidebar.cc
|
||||
DISALLOW_LOG_UPLOAD = threading.Event()
|
||||
METADATA_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "params_metadata.json")
|
||||
|
||||
params = Params()
|
||||
|
||||
|
||||
def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None:
|
||||
cloudlog.info("sunnylinkd.handle_long_poll started")
|
||||
sm = messaging.SubMaster(['deviceState'])
|
||||
@@ -180,16 +187,30 @@ def getParamsAllKeys() -> list[str]:
|
||||
|
||||
@dispatcher.add_method
|
||||
def getParamsAllKeysV1() -> dict[str, str]:
|
||||
try:
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
except Exception:
|
||||
cloudlog.exception("sunnylinkd.getParamsAllKeysV1.exception")
|
||||
metadata = {}
|
||||
|
||||
available_keys: list[str] = [k.decode('utf-8') for k in Params().all_keys()]
|
||||
|
||||
params_dict: dict[str, list[dict[str, str | bool | int | None]]] = {"params": []}
|
||||
params_dict: dict[str, list[dict[str, str | bool | int | object | dict | None]]] = {"params": []}
|
||||
for key in available_keys:
|
||||
value = get_param_as_byte(key, get_default=True)
|
||||
params_dict["params"].append({
|
||||
|
||||
param_entry = {
|
||||
"key": key,
|
||||
"type": int(params.get_type(key).value),
|
||||
"default_value": base64.b64encode(value).decode('utf-8') if value else None,
|
||||
})
|
||||
}
|
||||
|
||||
if key in metadata:
|
||||
meta_copy = metadata[key].copy()
|
||||
param_entry["_extra"] = meta_copy
|
||||
|
||||
params_dict["params"].append(param_entry)
|
||||
|
||||
return {"keys": json.dumps(params_dict.get("params", []))}
|
||||
|
||||
@@ -238,10 +259,7 @@ def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local
|
||||
|
||||
cloudlog.debug("athena.startLocalProxy.starting")
|
||||
ws = create_connection(
|
||||
remote_ws_uri,
|
||||
header={"Authorization": f"Bearer {sunnylink_api.get_token()}"},
|
||||
enable_multithread=True,
|
||||
sslopt={"cert_reqs": ssl.CERT_NONE}
|
||||
remote_ws_uri, header={"Authorization": f"Bearer {sunnylink_api.get_token()}"}, enable_multithread=True, sslopt={"cert_reqs": ssl.CERT_NONE}
|
||||
)
|
||||
|
||||
return start_local_proxy_shim(global_end_event, local_port, ws)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,205 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from enum import IntEnum
|
||||
import threading
|
||||
import time
|
||||
import json
|
||||
|
||||
from cereal import messaging
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID, SunnylinkApi
|
||||
|
||||
|
||||
class RoleType(IntEnum):
|
||||
READONLY = 0
|
||||
SPONSOR = 1
|
||||
ADMIN = 2
|
||||
|
||||
|
||||
class SponsorTier(IntEnum):
|
||||
FREE = 0
|
||||
NOVICE = 1
|
||||
SUPPORTER = 2
|
||||
CONTRIBUTOR = 3
|
||||
BENEFACTOR = 4
|
||||
GUARDIAN = 5
|
||||
|
||||
|
||||
class User:
|
||||
device_id: str
|
||||
user_id: str
|
||||
created_at: int
|
||||
updated_at: int
|
||||
token_hash: str
|
||||
|
||||
def __init__(self, json_data):
|
||||
self.device_id = json_data.get("device_id")
|
||||
self.user_id = json_data.get("user_id")
|
||||
self.created_at = json_data.get("created_at")
|
||||
self.updated_at = json_data.get("updated_at")
|
||||
self.token_hash = json_data.get("token_hash")
|
||||
|
||||
|
||||
class Role:
|
||||
role_type: str
|
||||
role_tier: str
|
||||
|
||||
def __init__(self, json_data):
|
||||
self.role_type = json_data.get("role_type")
|
||||
self.role_tier = json_data.get("role_tier")
|
||||
|
||||
|
||||
def _parse_roles(roles: str) -> list[Role]:
|
||||
lst_roles = []
|
||||
try:
|
||||
roles_list = json.loads(roles)
|
||||
for r in roles_list:
|
||||
try:
|
||||
role = Role(r)
|
||||
lst_roles.append(role)
|
||||
except Exception as e:
|
||||
cloudlog.exception(f"Failed to parse role {r}: {e}")
|
||||
return lst_roles
|
||||
except Exception as e:
|
||||
cloudlog.exception(f"Error parsing roles: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def _parse_users(users: str) -> list[User]:
|
||||
lst_users = []
|
||||
try:
|
||||
users_list = json.loads(users)
|
||||
for u in users_list:
|
||||
try:
|
||||
user = User(u)
|
||||
lst_users.append(user)
|
||||
except Exception as e:
|
||||
cloudlog.exception(f"Failed to parse user {u}: {e}")
|
||||
return lst_users
|
||||
except Exception as e:
|
||||
cloudlog.exception(f"Error parsing users: {e}")
|
||||
return []
|
||||
|
||||
|
||||
class SunnylinkState:
|
||||
FETCH_INTERVAL = 5.0 # seconds between API calls
|
||||
API_TIMEOUT = 10.0 # seconds for API requests
|
||||
SLEEP_INTERVAL = 0.5 # seconds to sleep between checks in the worker thread
|
||||
NOT_PAIRED_USERNAMES = ["unregisteredsponsor", "temporarysponsor"]
|
||||
|
||||
def __init__(self):
|
||||
self._params = Params()
|
||||
self._lock = threading.Lock()
|
||||
self._running = False
|
||||
self._thread = None
|
||||
self._sm = messaging.SubMaster(['deviceState'])
|
||||
|
||||
self._roles: list[Role] = []
|
||||
self._users: list[User] = []
|
||||
self.sponsor_tier: SponsorTier = SponsorTier.FREE
|
||||
self.sunnylink_dongle_id = self._params.get("SunnylinkDongleId")
|
||||
self._api = SunnylinkApi(self.sunnylink_dongle_id)
|
||||
|
||||
self._load_initial_state()
|
||||
|
||||
def _load_initial_state(self) -> None:
|
||||
roles_cache = self._params.get("SunnylinkCache_Roles")
|
||||
users_cache = self._params.get("SunnylinkCache_Users")
|
||||
if roles_cache is not None:
|
||||
self._roles = _parse_roles(roles_cache)
|
||||
self.sponsor_tier = self._get_highest_tier()
|
||||
if users_cache is not None:
|
||||
self._users = _parse_users(users_cache)
|
||||
|
||||
def _get_highest_tier(self) -> SponsorTier:
|
||||
role_tier = SponsorTier.FREE
|
||||
for role in self._roles:
|
||||
try:
|
||||
if RoleType[role.role_type.upper()] == RoleType.SPONSOR:
|
||||
role_tier = max(role_tier, SponsorTier[role.role_tier.upper()])
|
||||
except Exception as e:
|
||||
cloudlog.exception(f"Error parsing role {role}: {e} for dongle id {self.sunnylink_dongle_id}")
|
||||
return role_tier
|
||||
|
||||
def _fetch_roles(self) -> None:
|
||||
if not self.sunnylink_dongle_id or self.sunnylink_dongle_id == UNREGISTERED_SUNNYLINK_DONGLE_ID:
|
||||
return
|
||||
|
||||
try:
|
||||
token = self._api.get_token()
|
||||
response = self._api.api_get(f"device/{self.sunnylink_dongle_id}/roles", method='GET', access_token=token)
|
||||
if response.status_code == 200:
|
||||
self._roles = _parse_roles(response.text)
|
||||
self._params.put("SunnylinkCache_Roles", response.text)
|
||||
sponsor_tier = self._get_highest_tier()
|
||||
with self._lock:
|
||||
if sponsor_tier != self.sponsor_tier:
|
||||
self.sponsor_tier = sponsor_tier
|
||||
cloudlog.info(f"Sunnylink sponsor tier updated to {sponsor_tier.name}")
|
||||
except Exception as e:
|
||||
cloudlog.exception(f"Failed to fetch sunnylink roles: {e} for dongle id {self.sunnylink_dongle_id}")
|
||||
|
||||
def _fetch_users(self) -> None:
|
||||
if not self.sunnylink_dongle_id or self.sunnylink_dongle_id == UNREGISTERED_SUNNYLINK_DONGLE_ID:
|
||||
return
|
||||
|
||||
try:
|
||||
token = self._api.get_token()
|
||||
response = self._api.api_get(f"device/{self.sunnylink_dongle_id}/users", method='GET', access_token=token)
|
||||
if response.status_code == 200:
|
||||
users = response.text
|
||||
self._params.put("SunnylinkCache_Users", users)
|
||||
with self._lock:
|
||||
_parse_users(users)
|
||||
except Exception as e:
|
||||
cloudlog.exception(f"Failed to fetch sunnylink users: {e} for dongle id {self.sunnylink_dongle_id}")
|
||||
|
||||
def _worker_thread(self) -> None:
|
||||
while self._running:
|
||||
if self.is_connected():
|
||||
self._fetch_roles()
|
||||
self._fetch_users()
|
||||
|
||||
for _ in range(int(self.FETCH_INTERVAL / self.SLEEP_INTERVAL)):
|
||||
if not self._running:
|
||||
break
|
||||
time.sleep(self.SLEEP_INTERVAL)
|
||||
|
||||
def start(self) -> None:
|
||||
if self._thread and self._thread.is_alive():
|
||||
return
|
||||
self._running = True
|
||||
self._thread = threading.Thread(target=self._worker_thread, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def stop(self) -> None:
|
||||
self._running = False
|
||||
if self._thread and self._thread.is_alive():
|
||||
self._thread.join(timeout=1.0)
|
||||
|
||||
def get_sponsor_tier(self) -> SponsorTier:
|
||||
with self._lock:
|
||||
return self.sponsor_tier
|
||||
|
||||
def is_sponsor(self) -> bool:
|
||||
with self._lock:
|
||||
is_sponsor = any(role.role_type.upper() == RoleType.SPONSOR.name and role.role_tier.upper() != SponsorTier.FREE.name
|
||||
for role in self._roles)
|
||||
return is_sponsor
|
||||
|
||||
def is_paired(self) -> bool:
|
||||
with self._lock:
|
||||
is_paired = any(user.user_id not in self.NOT_PAIRED_USERNAMES for user in self._users)
|
||||
return is_paired
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
network_type = self._sm["deviceState"].networkType
|
||||
return bool(network_type != 0)
|
||||
|
||||
def __del__(self):
|
||||
self.stop()
|
||||
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import json
|
||||
|
||||
from openpilot.sunnypilot.sunnylink.athena.sunnylinkd import getParamsAllKeysV1, METADATA_PATH
|
||||
|
||||
|
||||
def test_get_params_all_keys_v1():
|
||||
"""
|
||||
Test the getParamsAllKeysV1 API endpoint.
|
||||
|
||||
Why:
|
||||
This endpoint is used by the UI (and potentially external tools) to fetch the list of
|
||||
available parameters along with their metadata (titles, descriptions, options, constraints).
|
||||
We need to ensure it returns the correct structure and that the metadata from
|
||||
params_metadata.json is correctly merged into the response.
|
||||
|
||||
Expected:
|
||||
- The response should contain a "keys" field which is a JSON string of a list of parameters.
|
||||
- Each parameter object should have "key", "type", "default_value", and optionally "_extra".
|
||||
- The "_extra" field should contain the rich metadata (title, options, min/max, etc.) matching
|
||||
the source of truth (params_metadata.json).
|
||||
"""
|
||||
response = getParamsAllKeysV1()
|
||||
assert "keys" in response
|
||||
|
||||
keys_json = response["keys"]
|
||||
params_list = json.loads(keys_json)
|
||||
|
||||
assert isinstance(params_list, list)
|
||||
assert len(params_list) > 0
|
||||
|
||||
# Check structure of first item
|
||||
first_param = params_list[0]
|
||||
assert "key" in first_param
|
||||
assert "type" in first_param
|
||||
assert "default_value" in first_param
|
||||
|
||||
if "_extra" in first_param:
|
||||
assert isinstance(first_param["_extra"], dict)
|
||||
assert "default" not in first_param["_extra"]
|
||||
assert "type" not in first_param["_extra"]
|
||||
|
||||
# Load the source of truth
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
# Verify that the API response matches the metadata file for a few sample keys
|
||||
# This ensures the plumbing is working without being brittle to content changes
|
||||
|
||||
# 1. Check a key that should have metadata
|
||||
keys_with_metadata = [k for k in params_list if k["key"] in metadata]
|
||||
assert len(keys_with_metadata) > 0, "No parameters found that match metadata keys"
|
||||
|
||||
for param in keys_with_metadata[:5]: # Check first 5 matches
|
||||
key = param["key"]
|
||||
expected_meta = metadata[key]
|
||||
|
||||
assert "_extra" in param, f"Parameter {key} should have _extra field"
|
||||
actual_meta = param["_extra"]
|
||||
|
||||
# Verify all fields in JSON are present in the API response
|
||||
for meta_key, meta_val in expected_meta.items():
|
||||
assert meta_key in actual_meta, f"Missing {meta_key} in API response for {key}"
|
||||
assert actual_meta[meta_key] == meta_val, f"Mismatch for {key}.{meta_key}: expected {meta_val}, got {actual_meta[meta_key]}"
|
||||
|
||||
# 2. Check that we are correctly serving options if they exist
|
||||
params_with_options = [k for k in keys_with_metadata if "options" in k.get("_extra", {})]
|
||||
if params_with_options:
|
||||
param = params_with_options[0]
|
||||
key = param["key"]
|
||||
assert isinstance(param["_extra"]["options"], list), f"Options for {key} should be a list"
|
||||
assert param["_extra"]["options"] == metadata[key]["options"]
|
||||
|
||||
# 3. Check that we are correctly serving numeric constraints if they exist
|
||||
params_with_constraints = [k for k in keys_with_metadata if "min" in k.get("_extra", {})]
|
||||
if params_with_constraints:
|
||||
param = params_with_constraints[0]
|
||||
key = param["key"]
|
||||
assert param["_extra"]["min"] == metadata[key]["min"]
|
||||
assert param["_extra"]["max"] == metadata[key]["max"]
|
||||
assert param["_extra"]["step"] == metadata[key]["step"]
|
||||
@@ -0,0 +1,202 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.sunnypilot.sunnylink.athena.sunnylinkd import METADATA_PATH
|
||||
|
||||
|
||||
def test_metadata_json_exists():
|
||||
"""
|
||||
Test that the params_metadata.json file exists at the expected path.
|
||||
|
||||
Why:
|
||||
The metadata file is the source of truth for parameter descriptions, options, and constraints.
|
||||
If it's missing, the UI will not be able to display rich information for parameters.
|
||||
|
||||
Expected:
|
||||
The file should exist at sunnypilot/sunnylink/params_metadata.json.
|
||||
"""
|
||||
assert os.path.exists(METADATA_PATH), f"Metadata file not found at {METADATA_PATH}"
|
||||
|
||||
|
||||
def test_metadata_json_valid():
|
||||
"""
|
||||
Test that the params_metadata.json file contains valid JSON.
|
||||
|
||||
Why:
|
||||
Invalid JSON will cause the metadata loading to fail, potentially crashing the UI or
|
||||
resulting in missing metadata.
|
||||
|
||||
Expected:
|
||||
The file content should be parseable as a JSON object (dictionary).
|
||||
"""
|
||||
with open(METADATA_PATH) as f:
|
||||
try:
|
||||
data = json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
pytest.fail("Metadata file is not valid JSON")
|
||||
|
||||
assert isinstance(data, dict), "Metadata root must be a dictionary"
|
||||
|
||||
|
||||
def test_all_params_have_metadata():
|
||||
"""
|
||||
Test that every parameter in the codebase has a corresponding entry in params_metadata.json.
|
||||
|
||||
Why:
|
||||
We want to ensure 100% coverage of parameter metadata. Any parameter added to the codebase
|
||||
should also be documented in the metadata file.
|
||||
|
||||
Expected:
|
||||
There should be no parameters in Params() that are missing from the metadata file.
|
||||
If this fails, run 'python3 sunnypilot/sunnylink/tools/update_params_metadata.py'.
|
||||
"""
|
||||
params = Params()
|
||||
all_keys = [k.decode('utf-8') for k in params.all_keys()]
|
||||
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
missing_keys = [key for key in all_keys if key not in metadata]
|
||||
|
||||
if missing_keys:
|
||||
pytest.fail(
|
||||
f"The following parameters are missing from metadata: {missing_keys}. "
|
||||
+ "Please run 'python3 sunnypilot/sunnylink/tools/update_params_metadata.py' to update."
|
||||
)
|
||||
|
||||
|
||||
def test_metadata_keys_exist_in_params():
|
||||
"""
|
||||
Test that all keys in params_metadata.json actually exist in the codebase.
|
||||
|
||||
Why:
|
||||
We want to avoid stale metadata for parameters that have been removed or renamed.
|
||||
This keeps the metadata file clean and relevant.
|
||||
|
||||
Expected:
|
||||
There should be no keys in the metadata file that are not present in Params().
|
||||
This prints a warning rather than failing, as it's less critical than missing metadata.
|
||||
"""
|
||||
params = Params()
|
||||
all_keys = {k.decode('utf-8') for k in params.all_keys()}
|
||||
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
extra_keys = [key for key in metadata.keys() if key not in all_keys]
|
||||
|
||||
if extra_keys:
|
||||
print(f"Warning: The following keys in metadata do not exist in Params: {extra_keys}")
|
||||
|
||||
|
||||
def test_no_default_titles():
|
||||
"""
|
||||
Test that no parameter has a title that is identical to its key.
|
||||
|
||||
Why:
|
||||
The default behavior of the update script is to set the title equal to the key.
|
||||
We want to force developers to provide human-readable, descriptive titles for all parameters.
|
||||
|
||||
Expected:
|
||||
No parameter metadata should have 'title' == 'key'.
|
||||
"""
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
default_title_keys = [key for key, meta in metadata.items() if meta.get("title") == key]
|
||||
|
||||
if default_title_keys:
|
||||
pytest.fail(
|
||||
f"The following parameters have default titles (title == key): {default_title_keys}. "
|
||||
+ "Please update 'params_metadata.json' with descriptive titles."
|
||||
)
|
||||
|
||||
|
||||
def test_options_structure():
|
||||
"""
|
||||
Test that the 'options' field in metadata follows the correct structure.
|
||||
|
||||
Why:
|
||||
The UI expects 'options' to be a list of objects with 'value' and 'label' keys.
|
||||
Incorrect structure will break the UI rendering for dropdowns/toggles.
|
||||
|
||||
Expected:
|
||||
If 'options' is present, it must be a list of dicts, and each dict must have 'value' and 'label'.
|
||||
"""
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
for key, meta in metadata.items():
|
||||
if "options" in meta:
|
||||
options = meta["options"]
|
||||
assert isinstance(options, list), f"Options for {key} must be a list"
|
||||
for option in options:
|
||||
assert isinstance(option, dict), f"Option in {key} must be a dictionary"
|
||||
assert "value" in option, f"Option in {key} must have a 'value' key"
|
||||
assert "label" in option, f"Option in {key} must have a 'label' key"
|
||||
|
||||
|
||||
def test_numeric_constraints():
|
||||
"""
|
||||
Test that numeric parameters have valid 'min', 'max', and 'step' constraints.
|
||||
|
||||
Why:
|
||||
The UI uses these constraints to validate user input and render sliders/steppers.
|
||||
Missing or invalid constraints can lead to UI bugs or invalid parameter values.
|
||||
|
||||
Expected:
|
||||
If any of min/max/step is present, ALL of them must be present.
|
||||
They must be numbers (int/float), and min must be less than max.
|
||||
"""
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
for key, meta in metadata.items():
|
||||
if "min" in meta or "max" in meta or "step" in meta:
|
||||
assert "min" in meta, f"Numeric param {key} must have 'min'"
|
||||
assert "max" in meta, f"Numeric param {key} must have 'max'"
|
||||
assert "step" in meta, f"Numeric param {key} must have 'step'"
|
||||
|
||||
assert isinstance(meta["min"], (int, float)), f"Min for {key} must be number"
|
||||
assert isinstance(meta["max"], (int, float)), f"Max for {key} must be number"
|
||||
assert isinstance(meta["step"], (int, float)), f"Step for {key} must be number"
|
||||
assert meta["min"] < meta["max"], f"Min must be less than max for {key}"
|
||||
|
||||
|
||||
def test_known_params_metadata():
|
||||
"""
|
||||
Test specific known parameters to ensure they have the expected rich metadata.
|
||||
|
||||
Why:
|
||||
This acts as a spot check to ensure that our rich metadata population logic is working correctly
|
||||
and that critical parameters (like LongitudinalPersonality) have their options and constraints preserved.
|
||||
|
||||
Expected:
|
||||
'LongitudinalPersonality' should have 3 options (Aggressive, Standard, Relaxed).
|
||||
'CustomAccLongPressIncrement' should have min=1, max=10, step=1.
|
||||
"""
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
# Check an enum-like param
|
||||
lp = metadata.get("LongitudinalPersonality")
|
||||
assert lp is not None
|
||||
assert "options" in lp
|
||||
assert len(lp["options"]) == 3
|
||||
assert lp["options"][0]["label"] == "Aggressive"
|
||||
assert lp["options"][0]["value"] == 0
|
||||
|
||||
# Check a numeric param
|
||||
acc_long = metadata.get("CustomAccLongPressIncrement")
|
||||
assert acc_long is not None
|
||||
assert acc_long["min"] == 1
|
||||
assert acc_long["max"] == 10
|
||||
assert acc_long["step"] == 1
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
|
||||
from openpilot.common.params import Params
|
||||
|
||||
METADATA_PATH = os.path.join(os.path.dirname(__file__), "../params_metadata.json")
|
||||
|
||||
|
||||
def main():
|
||||
params = Params()
|
||||
all_keys = params.all_keys()
|
||||
|
||||
if os.path.exists(METADATA_PATH):
|
||||
with open(METADATA_PATH) as f:
|
||||
try:
|
||||
data = json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
data = {}
|
||||
else:
|
||||
data = {}
|
||||
|
||||
# Add new keys
|
||||
for key in all_keys:
|
||||
key_str = key.decode("utf-8")
|
||||
if key_str not in data:
|
||||
print(f"Adding new key: {key_str}")
|
||||
data[key_str] = {
|
||||
"title": key_str,
|
||||
"description": "",
|
||||
}
|
||||
|
||||
# Remove deleted keys
|
||||
# keys_to_remove = [k for k in data.keys() if k.encode("utf-8") not in all_keys]
|
||||
# for k in keys_to_remove:
|
||||
# print(f"Removing deleted key: {k}")
|
||||
# del data[k]
|
||||
|
||||
# Sort keys
|
||||
sorted_data = dict(sorted(data.items()))
|
||||
|
||||
with open(METADATA_PATH, "w") as f:
|
||||
json.dump(sorted_data, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
print(f"Updated {METADATA_PATH}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -227,6 +227,8 @@ class GuiApplication(GuiApplicationExt):
|
||||
self._render_profiler = None
|
||||
self._render_profile_start_time = None
|
||||
|
||||
GuiApplicationExt.__init__(self)
|
||||
|
||||
@property
|
||||
def frame(self):
|
||||
return self._frame
|
||||
@@ -467,6 +469,9 @@ class GuiApplication(GuiApplicationExt):
|
||||
if self._show_touches:
|
||||
self._draw_touch_points()
|
||||
|
||||
if self._show_mouse_coords:
|
||||
self._draw_mouse_coordinates(gui_app.font(FontWeight.SEMI_BOLD))
|
||||
|
||||
if self._grid_size > 0:
|
||||
self._draw_grid()
|
||||
|
||||
|
||||
@@ -41,8 +41,12 @@ class GuiScrollPanel:
|
||||
if DEBUG:
|
||||
rl.draw_rectangle_lines(0, 0, abs(int(self._velocity_filter_y.x)), 10, rl.RED)
|
||||
|
||||
# Handle mouse wheel
|
||||
self._offset_filter_y.x += rl.get_mouse_wheel_move() * MOUSE_WHEEL_SCROLL_SPEED
|
||||
# Handle mouse wheel only when the mouse cursor is over this panel
|
||||
mouse_wheel = rl.get_mouse_wheel_move()
|
||||
if mouse_wheel != 0:
|
||||
mouse_pos = rl.get_mouse_position()
|
||||
if rl.check_collision_point_rec(mouse_pos, bounds):
|
||||
self._offset_filter_y.x += mouse_wheel * MOUSE_WHEEL_SCROLL_SPEED
|
||||
|
||||
max_scroll_distance = max(0, content.height - bounds.height)
|
||||
if self._scroll_state == ScrollState.IDLE:
|
||||
|
||||
@@ -16,6 +16,7 @@ AUTO_SCROLL_TC = 0.18
|
||||
BOUNCE_RETURN_RATE = 10.0
|
||||
REJECT_DECELERATION_FACTOR = 3
|
||||
MAX_SPEED = 10000.0 # px/s
|
||||
MOUSE_WHEEL_SCROLL_SPEED = 5
|
||||
|
||||
DEBUG = os.getenv("DEBUG_SCROLL", "0") == "1"
|
||||
|
||||
@@ -58,7 +59,7 @@ class GuiScrollPanel2:
|
||||
self._handle_mouse_event(mouse_event, bounds, bounds_size, content_size)
|
||||
self._previous_mouse_event = mouse_event
|
||||
|
||||
self._update_state(bounds_size, content_size)
|
||||
self._update_state(bounds, bounds_size, content_size)
|
||||
|
||||
if DEBUG:
|
||||
print('Velocity:', self._velocity)
|
||||
@@ -67,8 +68,17 @@ class GuiScrollPanel2:
|
||||
print()
|
||||
return self.get_offset()
|
||||
|
||||
def _update_state(self, bounds_size: float, content_size: float) -> None:
|
||||
def _update_state(self, bounds: rl.Rectangle, bounds_size: float, content_size: float) -> None:
|
||||
"""Runs per render frame, independent of mouse events. Updates auto-scrolling state and velocity."""
|
||||
mouse_wheel = rl.get_mouse_wheel_move()
|
||||
if mouse_wheel != 0:
|
||||
mouse_pos = rl.get_mouse_position()
|
||||
if rl.check_collision_point_rec(mouse_pos, bounds):
|
||||
scroll_delta = mouse_wheel * MOUSE_WHEEL_SCROLL_SPEED
|
||||
current_offset = self._offset.x if self._horizontal else self._offset.y
|
||||
self.set_offset(current_offset + scroll_delta)
|
||||
self._velocity = 0.0
|
||||
|
||||
if self._state == ScrollState.AUTO_SCROLL:
|
||||
# simple exponential return if out of bounds
|
||||
out_of_bounds = self.get_offset() > 0 or self.get_offset() < (bounds_size - content_size)
|
||||
|
||||
@@ -383,7 +383,7 @@ class WifiManager:
|
||||
'connection': {
|
||||
'type': ('s', '802-11-wireless'),
|
||||
'uuid': ('s', str(uuid.uuid4())),
|
||||
'id': ('s', f'openpilot connection {ssid}'),
|
||||
'id': ('s', f'sunnypilot connection {ssid}'),
|
||||
'autoconnect-retries': ('i', 0),
|
||||
},
|
||||
'802-11-wireless': {
|
||||
|
||||
@@ -701,7 +701,7 @@ class Setup(Widget):
|
||||
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 409:
|
||||
error_msg = "Incompatible openpilot version"
|
||||
error_msg = "Incompatible sunnypilot version"
|
||||
self.download_failed(self.download_url, error_msg)
|
||||
except Exception:
|
||||
error_msg = "Invalid URL"
|
||||
|
||||
@@ -6,10 +6,35 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import os
|
||||
|
||||
import pyray as rl
|
||||
|
||||
SHOW_MOUSE_COORDS = os.getenv("SHOW_MOUSE_COORDS") == "1"
|
||||
SUNNYPILOT_UI = os.getenv("SUNNYPILOT_UI", "1") == "1"
|
||||
|
||||
|
||||
class GuiApplicationExt:
|
||||
def __init__(self):
|
||||
self._show_mouse_coords = SHOW_MOUSE_COORDS
|
||||
|
||||
@staticmethod
|
||||
def sunnypilot_ui() -> bool:
|
||||
return SUNNYPILOT_UI
|
||||
|
||||
def _draw_mouse_coordinates(self, font):
|
||||
coords_text = f"X:{int(rl.get_mouse_x())}, Y:{int(rl.get_mouse_y())}"
|
||||
|
||||
green_color = rl.Color(0, 159, 47, 255) # Match the green color of FPS counter
|
||||
|
||||
# Calculate text width to position it at the right edge; estimate width based on text length
|
||||
# Each character is approximately 10-12 pixels wide at font size 20
|
||||
estimated_text_width = len(coords_text) * 11
|
||||
|
||||
# Position text at the top right corner, 10px from the top
|
||||
screen_width = self._scaled_width if self._scale != 1.0 else self._width
|
||||
text_pos = rl.Vector2(screen_width - estimated_text_width - 10, 6)
|
||||
|
||||
# Draw the text
|
||||
rl.draw_text_ex(font, coords_text, text_pos, 20, 0, green_color)
|
||||
|
||||
def set_show_mouse_coords(self, show: bool):
|
||||
self._show_mouse_coords = show
|
||||
|
||||
@@ -17,19 +17,24 @@ class Base:
|
||||
ITEM_TEXT_FONT_SIZE = 50
|
||||
ITEM_DESC_FONT_SIZE = 40
|
||||
ITEM_DESC_V_OFFSET = 150
|
||||
CLOSE_BTN_SIZE = 160
|
||||
|
||||
# Toggle Control
|
||||
TOGGLE_HEIGHT = 120
|
||||
TOGGLE_WIDTH = int(TOGGLE_HEIGHT * 1.75)
|
||||
TOGGLE_BG_HEIGHT = TOGGLE_HEIGHT - 20
|
||||
|
||||
# Button Control
|
||||
BUTTON_WIDTH = 300
|
||||
BUTTON_HEIGHT = 120
|
||||
|
||||
|
||||
@dataclass
|
||||
class DefaultStyleSP(Base):
|
||||
# Base Colors
|
||||
BASE_BG_COLOR = rl.Color(57, 57, 57, 255) # Grey
|
||||
ON_BG_COLOR = rl.Color(28, 101, 186, 255) # Blue
|
||||
OFF_BG_COLOR = rl.Color(70, 70, 70, 255) # Lighter Grey
|
||||
OFF_BG_COLOR = BASE_BG_COLOR
|
||||
ON_HOVER_BG_COLOR = rl.Color(17, 78, 150, 255) # Dark Blue
|
||||
OFF_HOVER_BG_COLOR = rl.Color(21, 21, 21, 255) # Dark gray
|
||||
DISABLED_ON_BG_COLOR = rl.Color(37, 70, 107, 255) # Dull Blue
|
||||
@@ -46,5 +51,32 @@ class DefaultStyleSP(Base):
|
||||
TOGGLE_DISABLED_OFF_COLOR = DISABLED_OFF_BG_COLOR
|
||||
TOGGLE_DISABLED_KNOB_COLOR = rl.Color(88, 88, 88, 255) # Lighter Grey
|
||||
|
||||
# Multi Button Control
|
||||
MBC_TRANSPARENT = rl.Color(255, 255, 255, 0)
|
||||
MBC_BG_CHECKED_ENABLED = rl.Color(0x69, 0x68, 0x68, 0xFF)
|
||||
MBC_DISABLED = rl.Color(0xFF, 0xFF, 0xFF, 0x33)
|
||||
|
||||
# Option Control
|
||||
OPTION_CONTROL_CONTAINER_BG = OFF_BG_COLOR
|
||||
OPTION_CONTROL_BTN_ENABLED = rl.Color(88, 88, 88, 255)
|
||||
OPTION_CONTROL_BTN_PRESSED = rl.Color(0x69, 0x68, 0x68, 0xFF)
|
||||
OPTION_CONTROL_BTN_DISABLED = DISABLED_OFF_BG_COLOR
|
||||
OPTION_CONTROL_TEXT_ENABLED = rl.WHITE
|
||||
OPTION_CONTROL_TEXT_PRESSED = rl.WHITE
|
||||
OPTION_CONTROL_TEXT_DISABLED = ITEM_DISABLED_TEXT_COLOR
|
||||
|
||||
# Tree Button Colors
|
||||
BUTTON_PRIMARY_COLOR = rl.Color(70, 91, 234, 255) # Royal Blue
|
||||
BUTTON_NEUTRAL_GRAY = rl.Color(51, 51, 51, 255)
|
||||
BUTTON_DISABLED_BG_COLOR = rl.Color(30, 30, 30, 255) # Very Dark Grey
|
||||
TREE_DIALOG_TRANSPARENT = rl.Color(0, 0, 0, 0)
|
||||
TREE_DIALOG_SEARCH_BUTTON_PRESSED = rl.Color(0x69, 0x68, 0x68, 0xFF)
|
||||
TREE_DIALOG_SEARCH_BUTTON_BORDER = rl.Color(150, 150, 150, 200)
|
||||
|
||||
# Vehicle Description Colors
|
||||
GREEN = rl.Color(0, 241, 0, 255)
|
||||
BLUE = rl.Color(0, 134, 233, 255)
|
||||
YELLOW = rl.Color(255, 213, 0, 255)
|
||||
|
||||
|
||||
style = DefaultStyleSP
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
|
||||
def normalize(text: str) -> str:
|
||||
return unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode('utf-8').lower()
|
||||
|
||||
|
||||
def search_from_list(query: str, items: list[str]) -> list[str]:
|
||||
if not query:
|
||||
return items
|
||||
|
||||
normalized_query = normalize(query)
|
||||
search_terms = [re.sub(r'[^a-z0-9]', '', term) for term in normalized_query.split() if term.strip()]
|
||||
|
||||
results = []
|
||||
for item in items:
|
||||
normalized_item = normalize(item)
|
||||
item_with_spaces = re.sub(r'[^a-z0-9\s]', ' ', normalized_item)
|
||||
item_stripped = re.sub(r'[^a-z0-9]', '', normalized_item)
|
||||
|
||||
all_terms_match = True
|
||||
for term in search_terms:
|
||||
if not term:
|
||||
continue
|
||||
|
||||
if term not in item_with_spaces and term not in item_stripped:
|
||||
all_terms_match = False
|
||||
break
|
||||
|
||||
if all_terms_match:
|
||||
results.append(item)
|
||||
|
||||
return results
|
||||
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import math
|
||||
|
||||
import pyray as rl
|
||||
|
||||
|
||||
def draw_star(center_x, center_y, radius, is_filled, color):
|
||||
center = rl.Vector2(center_x, center_y)
|
||||
points = []
|
||||
|
||||
for i in range(10):
|
||||
angle = -(i * 36 + 18) * math.pi / 180
|
||||
r = radius if i % 2 == 0 else radius / 2
|
||||
x = center_x + r * math.cos(angle)
|
||||
y = center_y + r * math.sin(angle)
|
||||
points.append(rl.Vector2(x, y))
|
||||
|
||||
for i in range(10):
|
||||
if is_filled:
|
||||
rl.draw_triangle(center, points[i], points[(i + 1) % 10], color)
|
||||
rl.draw_line_ex(points[i], points[(i + 1) % 10], 2, color)
|
||||
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from collections.abc import Callable
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import DialogResult
|
||||
from openpilot.system.ui.widgets.keyboard import Keyboard
|
||||
|
||||
|
||||
class InputDialogSP:
|
||||
def __init__(self, title: str, sub_title: str | None = None, current_text: str = "", param: str | None = None,
|
||||
callback: Callable[[DialogResult, str], None] | None = None,
|
||||
min_text_size: int = 0, password_mode: bool = False):
|
||||
self.callback = callback
|
||||
self.current_text = current_text
|
||||
self.keyboard = Keyboard(max_text_size=255, min_text_size=min_text_size, password_mode=password_mode)
|
||||
self.param = param
|
||||
self._params = Params()
|
||||
self.sub_title = sub_title
|
||||
self.title = title
|
||||
|
||||
def show(self):
|
||||
self.keyboard.reset(min_text_size=self.keyboard._min_text_size)
|
||||
if self.sub_title:
|
||||
self.keyboard.set_title(self.title, self.sub_title)
|
||||
else:
|
||||
self.keyboard.set_title(self.title)
|
||||
self.keyboard.set_text(self.current_text)
|
||||
|
||||
def internal_callback(result: DialogResult):
|
||||
text = self.keyboard.text if result == DialogResult.CONFIRM else ""
|
||||
if result == DialogResult.CONFIRM and self.param:
|
||||
self._params.put(self.param, text)
|
||||
if self.callback:
|
||||
self.callback(result, text)
|
||||
|
||||
gui_app.set_modal_overlay(self.keyboard, internal_callback)
|
||||
@@ -7,10 +7,13 @@ See the LICENSE.md file in the root directory for more details.
|
||||
from collections.abc import Callable
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.lib.application import MousePos
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.sunnypilot.widgets.toggle import ToggleSP
|
||||
from openpilot.system.ui.widgets.list_view import ListItem, ToggleAction, ItemAction
|
||||
from openpilot.system.ui.widgets.list_view import ListItem, ToggleAction, ItemAction, MultipleButtonAction, _resolve_value
|
||||
from openpilot.system.ui.sunnypilot.lib.styles import style
|
||||
from openpilot.system.ui.sunnypilot.widgets.option_control import OptionControlSP, LABEL_WIDTH
|
||||
|
||||
|
||||
class ToggleActionSP(ToggleAction):
|
||||
@@ -20,11 +23,80 @@ class ToggleActionSP(ToggleAction):
|
||||
self.toggle = ToggleSP(initial_state=initial_state, callback=callback, param=param)
|
||||
|
||||
|
||||
class MultipleButtonActionSP(MultipleButtonAction):
|
||||
def __init__(self, buttons: list[str | Callable[[], str]], button_width: int, selected_index: int = 0, callback: Callable = None,
|
||||
param: str | None = None):
|
||||
MultipleButtonAction.__init__(self, buttons, button_width, selected_index, callback)
|
||||
self.param_key = param
|
||||
self.params = Params()
|
||||
if self.param_key:
|
||||
self.selected_button = int(self.params.get(self.param_key, return_default=True))
|
||||
self._anim_x: float | None = None
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
|
||||
button_y = rect.y + (rect.height - style.BUTTON_HEIGHT) / 2
|
||||
|
||||
total_width = len(self.buttons) * self.button_width
|
||||
track_rect = rl.Rectangle(rect.x, button_y, total_width, style.BUTTON_HEIGHT)
|
||||
|
||||
bg_color = style.MBC_TRANSPARENT
|
||||
text_color = style.ITEM_TEXT_COLOR if self.enabled else style.MBC_DISABLED
|
||||
highlight_color = style.MBC_BG_CHECKED_ENABLED if self.enabled else style.MBC_DISABLED
|
||||
|
||||
# background
|
||||
rl.draw_rectangle_rounded(track_rect, 0.2, 20, bg_color)
|
||||
|
||||
# border
|
||||
border_color = style.MBC_BG_CHECKED_ENABLED if self.enabled else style.MBC_DISABLED
|
||||
rl.draw_rectangle_rounded_lines_ex(track_rect, 0.2, 20, 2, border_color)
|
||||
|
||||
# highlight with animation
|
||||
target_x = rect.x + self.selected_button * self.button_width
|
||||
if not self._anim_x:
|
||||
self._anim_x = target_x
|
||||
self._anim_x += (target_x - self._anim_x) * 0.2
|
||||
|
||||
highlight_rect = rl.Rectangle(self._anim_x, button_y, self.button_width, style.BUTTON_HEIGHT)
|
||||
rl.draw_rectangle_rounded(highlight_rect, 0.2, 20, highlight_color)
|
||||
|
||||
# text
|
||||
for i, _text in enumerate(self.buttons):
|
||||
button_x = rect.x + i * self.button_width
|
||||
|
||||
text = _resolve_value(_text, "")
|
||||
text_size = measure_text_cached(self._font, text, 40)
|
||||
text_x = button_x + (self.button_width - text_size.x) / 2
|
||||
text_y = button_y + (style.BUTTON_HEIGHT - text_size.y) / 2
|
||||
|
||||
rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, text_color)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
MultipleButtonAction._handle_mouse_release(self, mouse_pos)
|
||||
if self.param_key:
|
||||
self.params.put(self.param_key, self.selected_button)
|
||||
|
||||
|
||||
class ListItemSP(ListItem):
|
||||
def __init__(self, title: str | Callable[[], str] = "", icon: str | None = None, description: str | Callable[[], str] | None = None,
|
||||
description_visible: bool = False, callback: Callable | None = None,
|
||||
action_item: ItemAction | None = None):
|
||||
action_item: ItemAction | None = None, inline: bool = True, title_color: rl.Color = style.ITEM_TEXT_COLOR):
|
||||
ListItem.__init__(self, title, icon, description, description_visible, callback, action_item)
|
||||
self.title_color = title_color
|
||||
self.inline = inline
|
||||
if not self.inline:
|
||||
self._rect.height += style.ITEM_BASE_HEIGHT/1.75
|
||||
|
||||
def get_item_height(self, font: rl.Font, max_width: int) -> float:
|
||||
height = super().get_item_height(font, max_width)
|
||||
|
||||
if self.description_visible:
|
||||
height += style.ITEM_PADDING * 1.5
|
||||
|
||||
if not self.inline:
|
||||
height += style.ITEM_BASE_HEIGHT / 1.75
|
||||
|
||||
return height
|
||||
|
||||
def show_description(self, show: bool):
|
||||
self._set_description_visible(show)
|
||||
@@ -33,10 +105,19 @@ class ListItemSP(ListItem):
|
||||
if not self.action_item:
|
||||
return rl.Rectangle(0, 0, 0, 0)
|
||||
|
||||
if not self.inline:
|
||||
has_description = bool(self.description) and self.description_visible
|
||||
|
||||
if has_description:
|
||||
action_y = item_rect.y + self._text_size.y + style.ITEM_PADDING * 3
|
||||
else:
|
||||
action_y = item_rect.y + item_rect.height - style.BUTTON_HEIGHT - style.ITEM_PADDING * 1.5
|
||||
|
||||
return rl.Rectangle(item_rect.x + style.ITEM_PADDING, action_y, item_rect.width - (style.ITEM_PADDING * 2), style.BUTTON_HEIGHT)
|
||||
|
||||
right_width = self.action_item.rect.width
|
||||
if right_width == 0: # Full width action (like DualButtonAction)
|
||||
return rl.Rectangle(item_rect.x + style.ITEM_PADDING, item_rect.y,
|
||||
item_rect.width - (style.ITEM_PADDING * 2), style.ITEM_BASE_HEIGHT)
|
||||
if right_width == 0:
|
||||
return rl.Rectangle(item_rect.x + style.ITEM_PADDING, item_rect.y, item_rect.width - (style.ITEM_PADDING * 2), style.ITEM_BASE_HEIGHT)
|
||||
|
||||
action_width = self.action_item.rect.width
|
||||
if isinstance(self.action_item, ToggleAction):
|
||||
@@ -47,6 +128,13 @@ class ListItemSP(ListItem):
|
||||
return rl.Rectangle(action_x, action_y, action_width, style.ITEM_BASE_HEIGHT)
|
||||
|
||||
def _render(self, _):
|
||||
if not self.is_visible:
|
||||
return
|
||||
|
||||
# Don't draw items that are not in parent's viewport
|
||||
if (self._rect.y + self.rect.height) <= self._parent_rect.y or self._rect.y >= (self._parent_rect.y + self._parent_rect.height):
|
||||
return
|
||||
|
||||
content_x = self._rect.x + style.ITEM_PADDING
|
||||
text_x = content_x
|
||||
left_action_item = isinstance(self.action_item, ToggleAction)
|
||||
@@ -62,9 +150,9 @@ class ListItemSP(ListItem):
|
||||
|
||||
# Draw title
|
||||
if self.title:
|
||||
text_size = measure_text_cached(self._font, self.title, style.ITEM_TEXT_FONT_SIZE)
|
||||
item_y = self._rect.y + (style.ITEM_BASE_HEIGHT - text_size.y) // 2
|
||||
rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), style.ITEM_TEXT_FONT_SIZE, 0, style.ITEM_TEXT_COLOR)
|
||||
self._text_size = measure_text_cached(self._font, self.title, style.ITEM_TEXT_FONT_SIZE)
|
||||
item_y = self._rect.y + (style.ITEM_BASE_HEIGHT - self._text_size.y) // 2
|
||||
rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), style.ITEM_TEXT_FONT_SIZE, 0, self.title_color)
|
||||
|
||||
# Render toggle and handle callback
|
||||
if self.action_item.render(left_rect) and self.action_item.enabled:
|
||||
@@ -74,14 +162,13 @@ class ListItemSP(ListItem):
|
||||
else:
|
||||
if self.title:
|
||||
# Draw main text
|
||||
text_size = measure_text_cached(self._font, self.title, style.ITEM_TEXT_FONT_SIZE)
|
||||
item_y = self._rect.y + (style.ITEM_BASE_HEIGHT - text_size.y) // 2
|
||||
rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), style.ITEM_TEXT_FONT_SIZE, 0, style.ITEM_TEXT_COLOR)
|
||||
self._text_size = measure_text_cached(self._font, self.title, style.ITEM_TEXT_FONT_SIZE)
|
||||
item_y = self._rect.y + (style.ITEM_BASE_HEIGHT - self._text_size.y) // 2 if self.inline else self._rect.y + style.ITEM_PADDING * 1.5
|
||||
rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), style.ITEM_TEXT_FONT_SIZE, 0, self.title_color)
|
||||
|
||||
# Draw right item if present
|
||||
if self.action_item:
|
||||
right_rect = self.get_right_item_rect(self._rect)
|
||||
right_rect.y = self._rect.y
|
||||
if self.action_item.render(right_rect) and self.action_item.enabled:
|
||||
# Right item was clicked/activated
|
||||
if self.callback:
|
||||
@@ -91,12 +178,12 @@ class ListItemSP(ListItem):
|
||||
if self.description_visible:
|
||||
content_width = int(self._rect.width - style.ITEM_PADDING * 2)
|
||||
description_height = self._html_renderer.get_total_height(content_width)
|
||||
description_rect = rl.Rectangle(
|
||||
self._rect.x + style.ITEM_PADDING,
|
||||
self._rect.y + style.ITEM_DESC_V_OFFSET,
|
||||
content_width,
|
||||
description_height
|
||||
)
|
||||
|
||||
desc_y = self._rect.y + style.ITEM_DESC_V_OFFSET
|
||||
if not self.inline and self.action_item:
|
||||
desc_y = self.action_item.rect.y + style.ITEM_DESC_V_OFFSET - style.ITEM_PADDING * 0.5
|
||||
|
||||
description_rect = rl.Rectangle(self._rect.x + style.ITEM_PADDING, desc_y, content_width, description_height)
|
||||
self._html_renderer.render(description_rect)
|
||||
|
||||
|
||||
@@ -104,3 +191,23 @@ def toggle_item_sp(title: str | Callable[[], str], description: str | Callable[[
|
||||
callback: Callable | None = None, icon: str = "", enabled: bool | Callable[[], bool] = True, param: str | None = None) -> ListItemSP:
|
||||
action = ToggleActionSP(initial_state=initial_state, enabled=enabled, callback=callback, param=param)
|
||||
return ListItemSP(title=title, description=description, action_item=action, icon=icon, callback=callback)
|
||||
|
||||
|
||||
def multiple_button_item_sp(title: str | Callable[[], str], description: str | Callable[[], str], buttons: list[str | Callable[[], str]],
|
||||
selected_index: int = 0, button_width: int = style.BUTTON_WIDTH, callback: Callable = None,
|
||||
icon: str = "", param: str | None = None, inline: bool = False) -> ListItemSP:
|
||||
action = MultipleButtonActionSP(buttons, button_width, selected_index, callback=callback, param=param)
|
||||
return ListItemSP(title=title, description=description, icon=icon, action_item=action, inline=inline)
|
||||
|
||||
|
||||
def option_item_sp(title: str | Callable[[], str], param: str,
|
||||
min_value: int, max_value: int, description: str | Callable[[], str] | None = None,
|
||||
value_change_step: int = 1, on_value_changed: Callable[[int], None] | None = None,
|
||||
enabled: bool | Callable[[], bool] = True,
|
||||
icon: str = "", label_width: int = LABEL_WIDTH, value_map: dict[int, int] | None = None,
|
||||
use_float_scaling: bool = False, label_callback: Callable[[int], str] | None = None) -> ListItemSP:
|
||||
action = OptionControlSP(
|
||||
param, min_value, max_value, value_change_step,
|
||||
enabled, on_value_changed, value_map, label_width, use_float_scaling, label_callback
|
||||
)
|
||||
return ListItemSP(title=title, description=description, action_item=action, icon=icon)
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.sunnypilot.lib.styles import style
|
||||
from openpilot.system.ui.widgets.list_view import ItemAction
|
||||
|
||||
# Dimensions and styling constants
|
||||
BUTTON_WIDTH = 150
|
||||
BUTTON_HEIGHT = 150
|
||||
LABEL_WIDTH = 350
|
||||
BUTTON_SPACING = 25
|
||||
VALUE_FONT_SIZE = 50
|
||||
BUTTON_FONT_SIZE = 60
|
||||
CONTAINER_PADDING = 20
|
||||
|
||||
|
||||
class OptionControlSP(ItemAction):
|
||||
def __init__(self, param: str, min_value: int, max_value: int,
|
||||
value_change_step: int = 1, enabled: bool | Callable[[], bool] = True,
|
||||
on_value_changed: Callable[[int], None] | None = None,
|
||||
value_map: dict[int, int] | None = None,
|
||||
label_width: int = LABEL_WIDTH,
|
||||
use_float_scaling: bool = False, label_callback: Callable[[int], str] | None = None):
|
||||
|
||||
super().__init__(enabled=enabled)
|
||||
self.params = Params()
|
||||
self.param_key = param
|
||||
self.min_value = min_value
|
||||
self.max_value = max_value
|
||||
self.value_change_step = value_change_step
|
||||
self._minus_enabled = enabled
|
||||
self._plus_enabled = enabled
|
||||
self.on_value_changed = on_value_changed
|
||||
self.value_map = value_map
|
||||
self.label_width = label_width
|
||||
self.use_float_scaling = use_float_scaling
|
||||
self.current_value = min_value
|
||||
self.label_callback = label_callback
|
||||
if self.value_map:
|
||||
for key in self.value_map:
|
||||
if self.value_map[key] == self.params.get(self.param_key, return_default=True):
|
||||
self.current_value = int(key)
|
||||
break
|
||||
else:
|
||||
self.current_value = int(self.params.get(self.param_key, return_default=True))
|
||||
|
||||
# Initialize font and button styles
|
||||
self._font = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
# Layout rectangles for components
|
||||
self.minus_btn_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self.plus_btn_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
|
||||
def get_value(self) -> int:
|
||||
"""Get the current value of the control"""
|
||||
return self.current_value
|
||||
|
||||
def set_value(self, value: int):
|
||||
"""Set the control to a specific value"""
|
||||
if self.min_value <= value <= self.max_value:
|
||||
self.current_value = value
|
||||
if self.value_map:
|
||||
self.params.put(self.param_key, self.value_map[value])
|
||||
else:
|
||||
if self.use_float_scaling:
|
||||
self.params.put(self.param_key, value / 100.0)
|
||||
else:
|
||||
self.params.put(self.param_key, value)
|
||||
if self.on_value_changed:
|
||||
self.on_value_changed(value)
|
||||
|
||||
def get_displayed_value(self) -> str:
|
||||
"""Get the displayed value, handling value mapping if present"""
|
||||
value = self.current_value
|
||||
|
||||
if callable(self.label_callback):
|
||||
if self.value_map:
|
||||
return self.label_callback(self.value_map[value])
|
||||
else:
|
||||
return self.label_callback(value)
|
||||
|
||||
if self.value_map:
|
||||
# Use the value map to get the display string
|
||||
if value in self.value_map:
|
||||
return str(self.value_map[value]) # Return the display string
|
||||
|
||||
# If using float scaling, format as float
|
||||
if self.use_float_scaling:
|
||||
return f"{value / 100.0:.2f}"
|
||||
|
||||
return str(value)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
if self._rect.width == 0 or self._rect.height == 0 or not self.is_visible:
|
||||
return
|
||||
|
||||
control_width = (BUTTON_WIDTH * 2) + self.label_width + (BUTTON_SPACING * 2)
|
||||
total_width = control_width + (CONTAINER_PADDING * 2)
|
||||
self._rect.width = total_width
|
||||
|
||||
start_x = self._rect.x + self._rect.width - control_width - (CONTAINER_PADDING * 2)
|
||||
component_y = rect.y + (rect.height - BUTTON_HEIGHT) / 2
|
||||
self.container_rect = rl.Rectangle(start_x, component_y, total_width, BUTTON_HEIGHT)
|
||||
|
||||
# background
|
||||
rl.draw_rectangle_rounded(self.container_rect, 0.2, 20, style.OPTION_CONTROL_CONTAINER_BG)
|
||||
|
||||
# minus button
|
||||
self.minus_btn_rect = rl.Rectangle(self.container_rect.x, component_y, BUTTON_WIDTH + CONTAINER_PADDING,
|
||||
BUTTON_HEIGHT)
|
||||
|
||||
# label
|
||||
label_x = self.container_rect.x + CONTAINER_PADDING + BUTTON_WIDTH + BUTTON_SPACING
|
||||
self.label_rect = rl.Rectangle(label_x, component_y, self.label_width, BUTTON_HEIGHT)
|
||||
|
||||
# plus button
|
||||
plus_x = label_x + self.label_width + BUTTON_SPACING
|
||||
self.plus_btn_rect = rl.Rectangle(plus_x, component_y, BUTTON_WIDTH + CONTAINER_PADDING, BUTTON_HEIGHT)
|
||||
|
||||
self._minus_enabled = self.enabled and self.current_value > self.min_value
|
||||
self._plus_enabled = self.enabled and self.current_value < self.max_value
|
||||
|
||||
self._render_button(self.minus_btn_rect, "-", self._minus_enabled)
|
||||
self._render_value_label()
|
||||
self._render_button(self.plus_btn_rect, "+", self._plus_enabled)
|
||||
|
||||
def _render_button(self, rect: rl.Rectangle, text: str, enabled: bool):
|
||||
mouse_pos = rl.get_mouse_position()
|
||||
is_pressed = (rl.check_collision_point_rec(mouse_pos, rect) and
|
||||
self._touch_valid() and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT))
|
||||
|
||||
text_color = style.ITEM_TEXT_COLOR if enabled else style.ITEM_DISABLED_TEXT_COLOR
|
||||
|
||||
# highlight
|
||||
if enabled and is_pressed:
|
||||
rl.draw_rectangle_rounded(rect, 0.2, 20, style.OPTION_CONTROL_BTN_PRESSED)
|
||||
|
||||
# button text
|
||||
text_size = measure_text_cached(self._font, text, BUTTON_FONT_SIZE)
|
||||
text_x = rect.x + (rect.width - text_size.x) / 2
|
||||
text_y = rect.y + (rect.height - text_size.y) / 2
|
||||
rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), BUTTON_FONT_SIZE, 0, text_color)
|
||||
|
||||
def _render_value_label(self):
|
||||
"""Render the current value label"""
|
||||
text = self.get_displayed_value()
|
||||
text_color = style.ITEM_TEXT_COLOR if self.enabled else style.ITEM_DISABLED_TEXT_COLOR
|
||||
|
||||
text_size = measure_text_cached(self._font, text, VALUE_FONT_SIZE)
|
||||
text_x = self.label_rect.x + (self.label_rect.width - text_size.x) / 2
|
||||
text_y = self.label_rect.y + (self.label_rect.height - text_size.y) / 2
|
||||
|
||||
rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), VALUE_FONT_SIZE, 0, text_color)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
if self._minus_enabled and rl.check_collision_point_rec(mouse_pos, self.minus_btn_rect):
|
||||
self.current_value -= self.value_change_step
|
||||
self.current_value = max(self.min_value, self.current_value)
|
||||
elif self._plus_enabled and rl.check_collision_point_rec(mouse_pos, self.plus_btn_rect):
|
||||
self.current_value += self.value_change_step
|
||||
self.current_value = min(self.max_value, self.current_value)
|
||||
|
||||
self.set_value(self.current_value)
|
||||
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets.list_view import ListItem, ItemAction
|
||||
|
||||
|
||||
class ProgressBarAction(ItemAction):
|
||||
def __init__(self, width=600):
|
||||
super().__init__(width=width)
|
||||
self.progress = 0.0
|
||||
self.text = ""
|
||||
self.show_progress = False
|
||||
self.text_color = rl.GRAY
|
||||
self._font = gui_app.font(FontWeight.NORMAL)
|
||||
|
||||
def update(self, progress, text, show_progress=False, text_color=rl.GRAY):
|
||||
self.progress = progress
|
||||
self.text = text
|
||||
self.show_progress = show_progress
|
||||
self.text_color = text_color
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
font_size = 40
|
||||
text_size = measure_text_cached(self._font, self.text, font_size)
|
||||
padding = 30
|
||||
bar_width = text_size.x + 2 * padding
|
||||
text_x = (bar_width - text_size.x) / 2
|
||||
|
||||
if self.show_progress and len(parts := self.text.split(' - ', 1)) == 2:
|
||||
prefix = parts[0]
|
||||
max_prefix_w = measure_text_cached(self._font, "100%", font_size).x
|
||||
current_prefix_w = measure_text_cached(self._font, prefix, font_size).x
|
||||
|
||||
bar_width = (text_size.x - current_prefix_w + max_prefix_w) + 2 * padding
|
||||
text_x = padding + (max_prefix_w - current_prefix_w)
|
||||
|
||||
bar_height = 60
|
||||
bar_rect = rl.Rectangle(rect.x + rect.width - bar_width, rect.y + (rect.height - bar_height) / 2, bar_width, bar_height)
|
||||
|
||||
if self.show_progress:
|
||||
inner_rect = rl.Rectangle(bar_rect.x + 4, bar_rect.y + 4, bar_rect.width - 8, bar_rect.height - 8)
|
||||
if inner_rect.width > 0:
|
||||
fill_width = max(0, min(inner_rect.width, inner_rect.width * (self.progress / 100.0)))
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(inner_rect.x, inner_rect.y, fill_width, inner_rect.height), 0.2, 10, rl.Color(30, 121, 232, 255))
|
||||
|
||||
rl.draw_text_ex(self._font, self.text, rl.Vector2(bar_rect.x + text_x, bar_rect.y + (bar_height - text_size.y) / 2), font_size, 0, self.text_color)
|
||||
|
||||
|
||||
def progress_item(title):
|
||||
action = ProgressBarAction()
|
||||
return ListItem(title=title, action_item=action)
|
||||
@@ -0,0 +1,255 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import DialogResult
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle, BUTTON_PRESSED_BACKGROUND_COLORS
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
|
||||
from openpilot.system.ui.sunnypilot.lib.styles import style
|
||||
from openpilot.system.ui.sunnypilot.widgets.helpers.fuzzy_search import search_from_list
|
||||
from openpilot.system.ui.sunnypilot.widgets.helpers.star_icon import draw_star
|
||||
from openpilot.system.ui.sunnypilot.widgets.input_dialog import InputDialogSP
|
||||
|
||||
|
||||
@dataclass
|
||||
class TreeNode:
|
||||
ref: str
|
||||
data: dict = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TreeFolder:
|
||||
folder: str
|
||||
nodes: list
|
||||
|
||||
|
||||
class TreeItemWidget(Button):
|
||||
def __init__(self, text, ref, is_folder=False, indent_level=0, click_callback=None, favorite_callback=None, is_favorite=False, is_expanded=False):
|
||||
super().__init__(text, click_callback, button_style=ButtonStyle.NORMAL, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
text_padding=20 + indent_level * 30, elide_right=True)
|
||||
self.text = text
|
||||
self.ref = ref
|
||||
self.is_folder = is_folder
|
||||
self.indent_level = indent_level
|
||||
self.is_favorite = is_favorite
|
||||
self.selected = False
|
||||
self._favorite_callback = favorite_callback
|
||||
self.text_padding = 20 + indent_level * 30
|
||||
self.border_radius = 10
|
||||
self.is_expanded = is_expanded
|
||||
|
||||
def _render(self, rect):
|
||||
indent = 60 * self.indent_level
|
||||
self._rect = rl.Rectangle(rect.x + indent, rect.y, rect.width - indent, rect.height)
|
||||
if self.is_pressed:
|
||||
color = BUTTON_PRESSED_BACKGROUND_COLORS[self._button_style]
|
||||
elif self.selected and self.ref != "search_bar":
|
||||
color = style.BUTTON_PRIMARY_COLOR
|
||||
else:
|
||||
color = style.BUTTON_DISABLED_BG_COLOR
|
||||
roundness = self.border_radius / (min(self._rect.width, self._rect.height) / 2)
|
||||
rl.draw_rectangle_rounded(self._rect, roundness, 10, color)
|
||||
text_offset = self.text_padding + 20 - 15 if self.is_expanded and not self.is_folder and self.indent_level > 0 else self.text_padding + 20
|
||||
text_rect = rl.Rectangle(self._rect.x + text_offset, self._rect.y, self._rect.width - self.text_padding - 20 - 90, self._rect.height)
|
||||
self._label.render(text_rect)
|
||||
|
||||
if not self.is_folder and self._favorite_callback:
|
||||
draw_star(self._rect.x + self._rect.width - 90, self._rect.y + self._rect.height / 2, 40, self.is_favorite,
|
||||
style.ON_BG_COLOR if self.is_favorite else rl.GRAY)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos):
|
||||
star_rect = rl.Rectangle(self._rect.x + self._rect.width - 90 - 40, self._rect.y + self._rect.height / 2 - 40, 80, 80)
|
||||
if not self.is_folder and self._favorite_callback and rl.check_collision_point_rec(mouse_pos, star_rect):
|
||||
self._favorite_callback()
|
||||
return True
|
||||
return super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
|
||||
class TreeOptionDialog(MultiOptionDialog):
|
||||
def __init__(self, title, folders, current_ref="", fav_param="", option_font_weight=FontWeight.MEDIUM, search_prompt=None,
|
||||
get_folders_fn=None, on_exit=None, display_func=None, search_funcs=None, search_title=None, search_subtitle=None):
|
||||
super().__init__(title, [], "", option_font_weight)
|
||||
self.folders = folders
|
||||
self.selection_ref = current_ref
|
||||
self.fav_param = fav_param
|
||||
self.expanded = set()
|
||||
self.params = Params()
|
||||
val = self.params.get(fav_param) if fav_param else None
|
||||
self.favorites = set(val.split(';')) if val else set()
|
||||
self.query = ""
|
||||
self.search_prompt = search_prompt or tr("Search")
|
||||
self.get_folders_fn = get_folders_fn
|
||||
self.on_exit = on_exit
|
||||
self.display_func = display_func or (lambda node: node.data.get('display_name', node.ref))
|
||||
self.search_funcs = search_funcs or [lambda node: node.data.get('display_name', ''), lambda node: node.data.get('short_name', '')]
|
||||
self._search_rect = None
|
||||
self._search_width = 0.475
|
||||
|
||||
# Default title & overridable subtitle for InputDialogSP
|
||||
self.search_title = search_title or tr("Enter search query")
|
||||
self.search_subtitle = search_subtitle
|
||||
self.search_dialog = None
|
||||
self._search_pressed = False
|
||||
|
||||
self._build_visible_items()
|
||||
|
||||
def _on_search_confirm(self, result, text):
|
||||
if result == DialogResult.CONFIRM:
|
||||
self.query = text
|
||||
self._build_visible_items()
|
||||
gui_app.set_modal_overlay(self, callback=self.on_exit)
|
||||
|
||||
def _on_search_clicked(self):
|
||||
self.search_dialog = InputDialogSP(
|
||||
self.search_title,
|
||||
self.search_subtitle,
|
||||
current_text=self.query,
|
||||
callback=self._on_search_confirm,
|
||||
)
|
||||
self.search_dialog.show()
|
||||
|
||||
def _toggle_folder(self, folder):
|
||||
if folder.folder:
|
||||
if folder.folder in self.expanded:
|
||||
self.expanded.remove(folder.folder)
|
||||
else:
|
||||
self.expanded.add(folder.folder)
|
||||
if folder == self.folders[-1] and folder.folder in self.expanded:
|
||||
self.scroller.scroll_panel.set_offset(self.scroller.scroll_panel.offset - 200)
|
||||
self._build_visible_items(reset_scroll=False)
|
||||
|
||||
def _select_node(self, node):
|
||||
self.selection = self.display_func(node)
|
||||
self.selection_ref = node.ref
|
||||
|
||||
def _toggle_favorite(self, node):
|
||||
self.favorites.remove(node.ref) if node.ref in self.favorites else self.favorites.add(node.ref)
|
||||
if self.fav_param:
|
||||
self.params.put(self.fav_param, ';'.join(self.favorites))
|
||||
if self.get_folders_fn:
|
||||
self.folders = self.get_folders_fn(self.favorites)
|
||||
self._build_visible_items(reset_scroll=False)
|
||||
|
||||
def _build_visible_items(self, reset_scroll=True):
|
||||
self.visible_items = []
|
||||
for folder in self.folders:
|
||||
nodes = [node for node in folder.nodes if not self.query or search_from_list(self.query, [search_func(node) for search_func in self.search_funcs])]
|
||||
if not nodes and self.query:
|
||||
continue
|
||||
expanded = folder.folder in self.expanded or not folder.folder or bool(self.query)
|
||||
if folder.folder:
|
||||
self.visible_items.append(TreeItemWidget(f"{'-' if expanded else '+'} {folder.folder}", "", True, 0,
|
||||
lambda folder_ref=folder: self._toggle_folder(folder_ref)))
|
||||
if expanded:
|
||||
for node in nodes:
|
||||
favorite_cb = (lambda node_ref=node: self._toggle_favorite(node_ref)) if self.fav_param and node.ref != "Default" else None
|
||||
self.visible_items.append(TreeItemWidget(self.display_func(node), node.ref, False, 1 if folder.folder else 0,
|
||||
lambda node_ref=node: self._select_node(node_ref),
|
||||
favorite_cb, node.ref in self.favorites, is_expanded=expanded))
|
||||
self.option_buttons = self.visible_items
|
||||
self.options = [item.text for item in self.visible_items]
|
||||
self.scroller._items = self.visible_items
|
||||
if reset_scroll:
|
||||
self.scroller.scroll_panel.set_offset(0)
|
||||
|
||||
def _render(self, rect):
|
||||
dialog_content_rect = rl.Rectangle(rect.x + 50, rect.y + 50, rect.width - 100, rect.height - 100)
|
||||
rl.draw_rectangle_rounded(dialog_content_rect, 0.02, 20, rl.BLACK)
|
||||
|
||||
# Title on the left
|
||||
title_rect = rl.Rectangle(dialog_content_rect.x + 50, dialog_content_rect.y + 50, dialog_content_rect.width * 0.5, 70)
|
||||
gui_label(title_rect, self.title, 70, font_weight=FontWeight.BOLD)
|
||||
|
||||
# Search bar on the top right
|
||||
search_width = dialog_content_rect.width * self._search_width
|
||||
search_height = 110
|
||||
search_x = dialog_content_rect.x + dialog_content_rect.width - 50 - search_width
|
||||
search_y = dialog_content_rect.y + 40 # align roughly with title
|
||||
|
||||
self._search_rect = rl.Rectangle(search_x, search_y, search_width, search_height)
|
||||
|
||||
# Draw search field
|
||||
inset = 4
|
||||
roundness = 0.3
|
||||
input_rect = rl.Rectangle(self._search_rect.x + inset, self._search_rect.y + inset,
|
||||
self._search_rect.width - inset * 2, self._search_rect.height - inset * 2)
|
||||
|
||||
# Transparent fill (unpressed), white fill (pressed), border
|
||||
fill_color = style.TREE_DIALOG_SEARCH_BUTTON_PRESSED if self._search_pressed else style.TREE_DIALOG_TRANSPARENT
|
||||
rl.draw_rectangle_rounded(input_rect, roundness, 10, fill_color)
|
||||
rl.draw_rectangle_rounded_lines_ex(input_rect, roundness, 10, 3, style.TREE_DIALOG_SEARCH_BUTTON_BORDER)
|
||||
|
||||
# Magnifying glass icon
|
||||
icon_color = rl.Color(180, 180, 180, 240)
|
||||
cx = input_rect.x + 60
|
||||
cy = input_rect.y + input_rect.height / 2 - 5
|
||||
radius = min(input_rect.height * 0.28, 26)
|
||||
|
||||
circle_thickness = 4
|
||||
for i in range(circle_thickness):
|
||||
rl.draw_circle_lines(int(cx), int(cy), radius - i, icon_color)
|
||||
|
||||
handle_thickness = 5
|
||||
inner_x = cx + radius * 0.65
|
||||
inner_y = cy + radius * 0.65
|
||||
outer_x = cx + radius * 1.45
|
||||
outer_y = cy + radius * 1.45
|
||||
|
||||
rl.draw_line_ex(rl.Vector2(inner_x, inner_y), rl.Vector2(outer_x, outer_y), handle_thickness, icon_color)
|
||||
|
||||
# User text (query), placed after the icon if present
|
||||
if self.query:
|
||||
text_start_x = outer_x + 45
|
||||
text_rect = rl.Rectangle(text_start_x, input_rect.y, input_rect.x + input_rect.width - text_start_x - 10, input_rect.height)
|
||||
gui_label(text_rect, self.query, 70, font_weight=FontWeight.MEDIUM)
|
||||
|
||||
options_top = self._search_rect.y + self._search_rect.height + 40
|
||||
options_area_rect = rl.Rectangle(dialog_content_rect.x + 50, options_top, dialog_content_rect.width - 100,
|
||||
dialog_content_rect.height - (options_top - dialog_content_rect.y) - 210)
|
||||
|
||||
for index, option_text in enumerate(self.options):
|
||||
self.option_buttons[index].selected = (option_text == self.selection)
|
||||
self.option_buttons[index].set_button_style(ButtonStyle.PRIMARY if option_text == self.selection else ButtonStyle.NORMAL)
|
||||
self.option_buttons[index].set_rect(rl.Rectangle(0, 0, options_area_rect.width, 135))
|
||||
self.scroller.render(options_area_rect)
|
||||
|
||||
button_width = (dialog_content_rect.width - 150) / 2
|
||||
button_y_position = dialog_content_rect.y + dialog_content_rect.height - 160
|
||||
|
||||
cancel_rect = rl.Rectangle(dialog_content_rect.x + 50, button_y_position, button_width, 160)
|
||||
self.cancel_button.render(cancel_rect)
|
||||
|
||||
select_rect = rl.Rectangle(dialog_content_rect.x + 100 + button_width, button_y_position, button_width, 160)
|
||||
self.select_button.set_enabled(self.selection != self.current)
|
||||
self.select_button.render(select_rect)
|
||||
|
||||
return self._result
|
||||
|
||||
def _handle_mouse_press(self, mouse_pos):
|
||||
if self._search_rect and rl.check_collision_point_rec(mouse_pos, self._search_rect):
|
||||
self._search_pressed = True
|
||||
return True
|
||||
return super()._handle_mouse_press(mouse_pos)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos):
|
||||
clicked_search = False
|
||||
if self._search_rect and rl.check_collision_point_rec(mouse_pos, self._search_rect):
|
||||
clicked_search = self._search_pressed
|
||||
|
||||
self._search_pressed = False
|
||||
|
||||
if clicked_search:
|
||||
self._on_search_clicked()
|
||||
return True
|
||||
|
||||
return super()._handle_mouse_release(mouse_pos)
|
||||
@@ -16,7 +16,9 @@ from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.widgets.list_view import ButtonAction, ListItem, MultipleButtonAction, ToggleAction, button_item, text_item
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP as ListItem
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import ToggleActionSP as ToggleAction
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import MultipleButtonActionSP as MultipleButtonAction
|
||||
|
||||
# These are only used for AdvancedNetworkSettings, standalone apps just need WifiManagerUI
|
||||
try:
|
||||
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user