mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-08 19:36:14 +08:00
Compare commits
26 Commits
test-texts
...
navigation
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82540646d7 | ||
|
|
f3d8b24bf4 | ||
|
|
880ed98ffc | ||
|
|
dcaf84d04c | ||
|
|
3a82a0797a | ||
|
|
2d1f3833e4 | ||
|
|
e28dd1e1aa | ||
|
|
1a62ae821e | ||
|
|
1063114408 | ||
|
|
cefb344183 | ||
|
|
81b37712f1 | ||
|
|
1a4c48249b | ||
|
|
3d8763b3ce | ||
|
|
b2427a5f20 | ||
|
|
cf2b033c79 | ||
|
|
589e33f665 | ||
|
|
399ed08926 | ||
|
|
6aac50ab56 | ||
|
|
211c8adcce | ||
|
|
07b8e7783d | ||
|
|
53bf5b0d41 | ||
|
|
8c33592628 | ||
|
|
3bbb33f6bd | ||
|
|
5bd9549bd1 | ||
|
|
3481702715 | ||
|
|
c9781ee31d |
33
.github/workflows/sunnypilot-build-prebuilt.yaml
vendored
33
.github/workflows/sunnypilot-build-prebuilt.yaml
vendored
@@ -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,15 +180,6 @@ 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
|
||||
@@ -250,8 +241,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.
|
||||
@@ -302,7 +293,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: |
|
||||
@@ -311,7 +302,7 @@ jobs:
|
||||
git push -f origin ${TAG}
|
||||
|
||||
notify:
|
||||
needs:
|
||||
needs:
|
||||
- prepare_strategy
|
||||
- build
|
||||
- publish
|
||||
@@ -340,7 +331,7 @@ jobs:
|
||||
${{ vars.DISCOURSE_GENERAL_UPDATE_NOTICE }}
|
||||
EOF
|
||||
)
|
||||
|
||||
|
||||
{
|
||||
echo 'content<<EOFMARKER'
|
||||
echo "$MESSAGE"
|
||||
|
||||
3
.github/workflows/tests.yaml
vendored
3
.github/workflows/tests.yaml
vendored
@@ -21,11 +21,12 @@ env:
|
||||
PYTHONWARNINGS: error
|
||||
BASE_IMAGE: sunnypilot-base
|
||||
AZURE_TOKEN: ${{ secrets.AZURE_COMMADATACI_OPENPILOTCI_TOKEN }}
|
||||
MAPBOX_TOKEN_CI: ${{ secrets.MAPBOX_TOKEN_CI }}
|
||||
|
||||
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD: release/ci/docker_build_sp.sh base
|
||||
|
||||
RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c
|
||||
RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -e MAPBOX_TOKEN_CI=$MAPBOX_TOKEN_CI -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c
|
||||
|
||||
PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical
|
||||
|
||||
|
||||
10
SConstruct
10
SConstruct
@@ -75,6 +75,7 @@ env = Environment(
|
||||
"#third_party/acados/include/hpipm/include",
|
||||
"#third_party/catch2/include",
|
||||
"#third_party/libyuv/include",
|
||||
"#third_party/snpe/include",
|
||||
],
|
||||
LIBPATH=[
|
||||
"#common",
|
||||
@@ -100,6 +101,7 @@ 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)
|
||||
@@ -123,6 +125,14 @@ 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"])
|
||||
|
||||
@@ -189,6 +189,11 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
|
||||
{"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}},
|
||||
|
||||
// Navigation params
|
||||
{"MapboxToken", {PERSISTENT | BACKUP, STRING}},
|
||||
{"MapboxSettings", {CLEAR_ON_MANAGER_START, JSON}},
|
||||
{"MapboxRoute", {CLEAR_ON_MANAGER_START, STRING}},
|
||||
|
||||
// Neural Network Lateral Control
|
||||
{"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import math
|
||||
import threading
|
||||
import time
|
||||
from numbers import Number
|
||||
|
||||
from cereal import car, log
|
||||
@@ -20,6 +22,8 @@ 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
|
||||
@@ -29,7 +33,7 @@ LaneChangeDirection = log.LaneChangeDirection
|
||||
ACTUATOR_FIELDS = tuple(car.CarControl.Actuators.schema.fields.keys())
|
||||
|
||||
|
||||
class Controls(ControlsExt):
|
||||
class Controls(ControlsExt, ModelStateBase):
|
||||
def __init__(self) -> None:
|
||||
self.params = Params()
|
||||
cloudlog.info("controlsd is waiting for CarParams")
|
||||
@@ -38,6 +42,7 @@ class Controls(ControlsExt):
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -226,15 +231,30 @@ class Controls(ControlsExt):
|
||||
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)
|
||||
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()
|
||||
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()
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -177,7 +177,7 @@ class HomeLayout(Widget):
|
||||
|
||||
version_rect = rl.Rectangle(self.header_rect.x + self.header_rect.width - version_text_width, self.header_rect.y,
|
||||
version_text_width, self.header_rect.height)
|
||||
gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, font_weight=FontWeight.AUDIOWIDE)
|
||||
gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||
|
||||
def _render_home_content(self):
|
||||
self._render_left_column()
|
||||
@@ -227,6 +227,6 @@ class HomeLayout(Widget):
|
||||
self._prev_alerts_present = alerts_present
|
||||
|
||||
def _get_version_text(self) -> str:
|
||||
brand = "sunnypilot"
|
||||
brand = "openpilot"
|
||||
description = self.params.get("UpdaterCurrentDescription")
|
||||
return f"{brand} {description}" if description else brand
|
||||
|
||||
@@ -10,9 +10,6 @@ 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 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."),
|
||||
self._title = Label(tr("Welcome to openpilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
self._desc = Label(tr("You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing."),
|
||||
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
|
||||
self._decline_btn = Button(tr("Decline"), click_callback=on_decline)
|
||||
@@ -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 sunnypilot."),
|
||||
self._text = Label(tr("You must accept the Terms and Conditions in order to use openpilot."),
|
||||
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
self._back_btn = Button(tr("Back"), click_callback=back_callback)
|
||||
self._uninstall_btn = Button(tr("Decline, uninstall sunnypilot"), button_style=ButtonStyle.DANGER,
|
||||
self._uninstall_btn = Button(tr("Decline, uninstall openpilot"), button_style=ButtonStyle.DANGER,
|
||||
click_callback=self._on_uninstall_clicked)
|
||||
|
||||
def _on_uninstall_clicked(self):
|
||||
|
||||
@@ -23,11 +23,10 @@ DESCRIPTIONS = {
|
||||
"other than your own. A comma employee will NEVER ask you to add their GitHub username."
|
||||
),
|
||||
'alpha_longitudinal': tr_noop(
|
||||
"<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."
|
||||
"<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."
|
||||
),
|
||||
}
|
||||
|
||||
@@ -72,7 +71,7 @@ class DeveloperLayout(Widget):
|
||||
)
|
||||
|
||||
self._alpha_long_toggle = toggle_item(
|
||||
lambda: tr("sunnypilot Longitudinal Control (Alpha)"),
|
||||
lambda: tr("openpilot Longitudinal Control (Alpha)"),
|
||||
description=lambda: tr(DESCRIPTIONS["alpha_longitudinal"]),
|
||||
initial_state=self._params.get_bool("AlphaLongitudinalEnabled"),
|
||||
callback=self._on_alpha_long_enabled,
|
||||
@@ -149,7 +148,6 @@ 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("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"),
|
||||
'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"),
|
||||
}
|
||||
|
||||
|
||||
@@ -162,8 +162,8 @@ class DeviceLayout(Widget):
|
||||
cloudlog.exception("invalid LiveTorqueParameters")
|
||||
|
||||
desc += "<br><br>"
|
||||
desc += tr("sunnypilot is continuously calibrating, resetting is rarely required. " +
|
||||
"Resetting calibration will restart sunnypilot if the car is powered on.")
|
||||
desc += tr("openpilot is continuously calibrating, resetting is rarely required. " +
|
||||
"Resetting calibration will restart openpilot if the car is powered on.")
|
||||
|
||||
self._reset_calib_btn.set_description(desc)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from openpilot.selfdrive.ui.lib.api_helpers import get_token
|
||||
|
||||
TITLE = tr_noop("Firehose Mode")
|
||||
DESCRIPTION = tr_noop(
|
||||
"sunnypilot learns to drive by watching humans, like you, drive.\n\n"
|
||||
"openpilot learns to drive by watching humans, like you, drive.\n\n"
|
||||
+ "Firehose Mode allows you to maximize your training data uploads to improve "
|
||||
+ "openpilot's driving models. More data means bigger models, which means better Experimental Mode."
|
||||
)
|
||||
|
||||
@@ -11,27 +11,26 @@ 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 sunnypilot system for adaptive cruise control and lane keep driver assistance. " +
|
||||
"Use the openpilot system for adaptive cruise control and lane keep driver assistance. " +
|
||||
"Your attention is required at all times to use this feature."
|
||||
),
|
||||
"DisengageOnAccelerator": tr_noop("When enabled, pressing the accelerator pedal will disengage sunnypilot."),
|
||||
"DisengageOnAccelerator": tr_noop("When enabled, pressing the accelerator pedal will disengage openpilot."),
|
||||
"LongitudinalPersonality": tr_noop(
|
||||
"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 " +
|
||||
"Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. " +
|
||||
"In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " +
|
||||
"your steering wheel distance button."
|
||||
),
|
||||
"IsLdwEnabled": 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 sunnypilot is not engaged."),
|
||||
"AlwaysOnDM": tr_noop("Enable driver monitoring even when openpilot is not engaged."),
|
||||
'RecordFront': tr_noop("Upload data from the driver facing camera and help improve the driver monitoring algorithm."),
|
||||
"IsMetric": tr_noop("Display speed in km/h instead of mph."),
|
||||
"RecordAudio": tr_noop("Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."),
|
||||
@@ -47,7 +46,7 @@ class TogglesLayout(Widget):
|
||||
# param, title, desc, icon, needs_restart
|
||||
self._toggle_defs = {
|
||||
"OpenpilotEnabledToggle": (
|
||||
lambda: tr("Enable sunnypilot"),
|
||||
lambda: tr("Enable openpilot"),
|
||||
DESCRIPTIONS["OpenpilotEnabledToggle"],
|
||||
"chffr_wheel.png",
|
||||
True,
|
||||
@@ -100,7 +99,7 @@ class TogglesLayout(Widget):
|
||||
lambda: tr("Driving Personality"),
|
||||
lambda: tr(DESCRIPTIONS["LongitudinalPersonality"]),
|
||||
buttons=[lambda: tr("Aggressive"), lambda: tr("Standard"), lambda: tr("Relaxed")],
|
||||
button_width=300,
|
||||
button_width=255,
|
||||
callback=self._set_longitudinal_personality,
|
||||
selected_index=self._params.get("LongitudinalPersonality", return_default=True),
|
||||
icon="speed_limit.png"
|
||||
@@ -126,7 +125,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 sunnypilot if the car is powered on.")
|
||||
additional_desc = tr("Changing this setting will restart openpilot if the car is powered on.")
|
||||
toggle.set_description(lambda og_desc=toggle.description, add_desc=additional_desc: tr(og_desc) + (" " + tr(add_desc) if add_desc else ""))
|
||||
|
||||
# track for engaged state updates
|
||||
@@ -159,10 +158,10 @@ class TogglesLayout(Widget):
|
||||
ui_state.update_params()
|
||||
|
||||
e2e_description = tr(
|
||||
"sunnypilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. " +
|
||||
"openpilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. " +
|
||||
"Experimental features are listed below:<br>" +
|
||||
"<h4>End-to-End Longitudinal Control</h4><br>" +
|
||||
"Let the driving model control the gas and brakes. sunnypilot 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. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. " +
|
||||
"Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; " +
|
||||
"mistakes should be expected.<br>" +
|
||||
"<h4>New Driving Visualization</h4><br>" +
|
||||
@@ -184,13 +183,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("sunnypilot longitudinal control may come in a future update.")
|
||||
long_desc = unavailable + " " + tr("openpilot longitudinal control may come in a future update.")
|
||||
if ui_state.CP.alphaLongitudinalAvailable:
|
||||
if self._is_release:
|
||||
long_desc = unavailable + " " + tr("An alpha version of sunnypilot longitudinal control can be tested, along with " +
|
||||
long_desc = unavailable + " " + tr("An alpha version of openpilot longitudinal control can be tested, along with " +
|
||||
"Experimental mode, on non-release branches.")
|
||||
else:
|
||||
long_desc = tr("Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode.")
|
||||
long_desc = tr("Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode.")
|
||||
|
||||
self._toggles["ExperimentalMode"].set_description("<b>" + long_desc + "</b><br><br>" + e2e_description)
|
||||
else:
|
||||
|
||||
@@ -45,7 +45,7 @@ class DeviceStatus(Widget):
|
||||
self._version_text = self._get_version_text()
|
||||
|
||||
def _get_version_text(self) -> str:
|
||||
brand = "sunnypilot"
|
||||
brand = "openpilot"
|
||||
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("sunnypilot", font_size=96, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.DISPLAY)
|
||||
self._openpilot_label = MiciLabel("openpilot", 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 sunnypilot {version}, {date}. go to comma.ai/blog to read the release notes."
|
||||
update_alert_data.text = f"update available\n openpilot {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. sunnypilot is a driver assistance system.\n\n" +
|
||||
self._warning_label = UnifiedLabel("1. openpilot 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 sunnypilot", self._on_uninstall)
|
||||
self._uninstall_slider = SmallSlider("uninstall openpilot", 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 sunnypilot",
|
||||
self._warning_header = TermsHeader("you must accept the\nterms to use openpilot",
|
||||
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 sunnypilot. " +
|
||||
self._terms_label = UnifiedLabel("You must accept the Terms and Conditions to use openpilot. " +
|
||||
"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 sunnypilot", "", self._txt_update_icon)
|
||||
super().__init__("update openpilot", "", 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 sunnypilot")
|
||||
self.set_text("update openpilot")
|
||||
|
||||
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 sunnypilot", "", "icons_mici/settings/device/uninstall.png")
|
||||
uninstall_openpilot_btn = BigButton("uninstall openpilot", "", "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(
|
||||
"sunnypilot learns to drive by watching humans, like you, drive.\n\n"
|
||||
"openpilot learns to drive by watching humans, like you, drive.\n\n"
|
||||
+ "Firehose Mode allows you to maximize your training data uploads to improve "
|
||||
+ "openpilot's driving models. More data means bigger models, which means better Experimental Mode."
|
||||
)
|
||||
|
||||
@@ -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 sunnypilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback)
|
||||
enable_openpilot = BigParamControl("enable openpilot", "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="sunnypilot Unavailable",
|
||||
text1="openpilot 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 sunnypilot", 54, FontWeight.DISPLAY,
|
||||
self._offroad_label = UnifiedLabel("start the car to\nuse openpilot", 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 sunnypilot")
|
||||
self._offroad_label.set_text("start the car to\nuse openpilot")
|
||||
|
||||
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("sunnypilot Unavailable"),
|
||||
text1=tr("openpilot Unavailable"),
|
||||
text2=tr("Waiting to start"),
|
||||
size=AlertSize.mid,
|
||||
status=AlertStatus.normal,
|
||||
|
||||
158
selfdrive/ui/qt/offroad/offroad_home.cc
Normal file
158
selfdrive/ui/qt/offroad/offroad_home.cc
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 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")));
|
||||
}
|
||||
}
|
||||
59
selfdrive/ui/qt/offroad/offroad_home.h
Normal file
59
selfdrive/ui/qt/offroad/offroad_home.h
Normal file
@@ -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.
|
||||
*/
|
||||
|
||||
#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;
|
||||
};
|
||||
@@ -1,30 +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.
|
||||
"""
|
||||
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()
|
||||
@@ -1,12 +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.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout
|
||||
|
||||
|
||||
class DeviceLayoutSP(DeviceLayout):
|
||||
def __init__(self):
|
||||
DeviceLayout.__init__(self)
|
||||
@@ -1,30 +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.
|
||||
"""
|
||||
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()
|
||||
@@ -1,30 +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.
|
||||
"""
|
||||
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()
|
||||
@@ -1,30 +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.
|
||||
"""
|
||||
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()
|
||||
@@ -1,46 +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.
|
||||
"""
|
||||
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()
|
||||
@@ -1,30 +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.
|
||||
"""
|
||||
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()
|
||||
@@ -1,202 +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.
|
||||
"""
|
||||
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()
|
||||
@@ -1,30 +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.
|
||||
"""
|
||||
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()
|
||||
@@ -1,30 +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.
|
||||
"""
|
||||
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()
|
||||
@@ -1,30 +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.
|
||||
"""
|
||||
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()
|
||||
@@ -1,67 +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.
|
||||
"""
|
||||
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()
|
||||
@@ -1,16 +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.
|
||||
"""
|
||||
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."""
|
||||
@@ -1,15 +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.
|
||||
"""
|
||||
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
|
||||
@@ -1,15 +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.
|
||||
"""
|
||||
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
|
||||
@@ -1,45 +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.
|
||||
"""
|
||||
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
|
||||
@@ -1,15 +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.
|
||||
"""
|
||||
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
|
||||
@@ -1,15 +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.
|
||||
"""
|
||||
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
|
||||
@@ -1,15 +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.
|
||||
"""
|
||||
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
|
||||
@@ -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.
|
||||
"""
|
||||
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)
|
||||
@@ -1,15 +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.
|
||||
"""
|
||||
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
|
||||
@@ -1,15 +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.
|
||||
"""
|
||||
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
|
||||
@@ -1,15 +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.
|
||||
"""
|
||||
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
|
||||
@@ -1,15 +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.
|
||||
"""
|
||||
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
|
||||
@@ -1,54 +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.
|
||||
"""
|
||||
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)
|
||||
@@ -1,43 +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.
|
||||
"""
|
||||
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())
|
||||
@@ -1,15 +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.
|
||||
"""
|
||||
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
|
||||
@@ -1,15 +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.
|
||||
"""
|
||||
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
|
||||
@@ -1,138 +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.
|
||||
"""
|
||||
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)
|
||||
@@ -1,30 +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.
|
||||
"""
|
||||
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()
|
||||
@@ -1,29 +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.
|
||||
"""
|
||||
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, scroll=None):
|
||||
def setup_homescreen(click, pm: PubMaster):
|
||||
pass
|
||||
|
||||
|
||||
def setup_homescreen_update_available(click, pm: PubMaster, scroll=None):
|
||||
def setup_homescreen_update_available(click, pm: PubMaster):
|
||||
params = Params()
|
||||
params.put_bool("UpdateAvailable", True)
|
||||
put_update_params(params)
|
||||
setup_offroad_alert(click, pm)
|
||||
|
||||
|
||||
def setup_settings(click, pm: PubMaster, scroll=None):
|
||||
def setup_settings(click, pm: PubMaster):
|
||||
click(100, 100)
|
||||
|
||||
|
||||
def close_settings(click, pm: PubMaster, scroll=None):
|
||||
click(140, 120)
|
||||
def close_settings(click, pm: PubMaster):
|
||||
click(240, 216)
|
||||
|
||||
|
||||
def setup_settings_network(click, pm: PubMaster, scroll=None):
|
||||
def setup_settings_network(click, pm: PubMaster):
|
||||
setup_settings(click, pm)
|
||||
click(278, 450)
|
||||
|
||||
|
||||
def setup_settings_network_advanced(click, pm: PubMaster, scroll=None):
|
||||
setup_settings_network(click, pm, scroll=scroll)
|
||||
def setup_settings_network_advanced(click, pm: PubMaster):
|
||||
setup_settings_network(click, pm)
|
||||
click(1880, 100)
|
||||
|
||||
|
||||
def setup_settings_toggles(click, pm: PubMaster, scroll=None):
|
||||
def setup_settings_toggles(click, pm: PubMaster):
|
||||
setup_settings(click, pm)
|
||||
click(278, 620)
|
||||
click(278, 600)
|
||||
|
||||
|
||||
def setup_settings_software(click, pm: PubMaster, scroll=None):
|
||||
def setup_settings_software(click, pm: PubMaster):
|
||||
put_update_params(Params())
|
||||
setup_settings(click, pm)
|
||||
click(278, 730)
|
||||
click(278, 720)
|
||||
|
||||
|
||||
def setup_settings_software_download(click, pm: PubMaster, scroll=None):
|
||||
def setup_settings_software_download(click, pm: PubMaster):
|
||||
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, scroll=None):
|
||||
setup_settings_software(click, pm)
|
||||
|
||||
|
||||
def setup_settings_software_release_notes(click, pm: PubMaster, scroll=None):
|
||||
setup_settings_software(click, pm, scroll=scroll)
|
||||
def setup_settings_software_release_notes(click, pm: PubMaster):
|
||||
setup_settings_software(click, pm)
|
||||
click(588, 110) # expand description for current version
|
||||
|
||||
|
||||
def setup_settings_software_branch_switcher(click, pm: PubMaster, scroll=None):
|
||||
setup_settings_software(click, pm, scroll=scroll)
|
||||
def setup_settings_software_branch_switcher(click, pm: PubMaster):
|
||||
setup_settings_software(click, pm)
|
||||
params = Params()
|
||||
params.put("UpdaterAvailableBranches", f"master,nightly,release,{BRANCH_NAME}")
|
||||
params.put("GitBranch", BRANCH_NAME) # should be on top
|
||||
@@ -103,32 +103,30 @@ def setup_settings_software_branch_switcher(click, pm: PubMaster, scroll=None):
|
||||
click(1984, 449)
|
||||
|
||||
|
||||
def setup_settings_firehose(click, pm: PubMaster, scroll=None):
|
||||
def setup_settings_firehose(click, pm: PubMaster):
|
||||
setup_settings(click, pm)
|
||||
scroll(-20, 278, 950)
|
||||
click(278, 850)
|
||||
click(278, 845)
|
||||
|
||||
|
||||
def setup_settings_developer(click, pm: PubMaster, scroll=None):
|
||||
def setup_settings_developer(click, pm: PubMaster):
|
||||
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, scroll=None):
|
||||
setup_settings_developer(click, pm, scroll=scroll)
|
||||
def setup_keyboard(click, pm: PubMaster):
|
||||
setup_settings_developer(click, pm)
|
||||
click(1930, 470)
|
||||
|
||||
|
||||
def setup_pair_device(click, pm: PubMaster, scroll=None):
|
||||
def setup_pair_device(click, pm: PubMaster):
|
||||
click(1950, 800)
|
||||
|
||||
|
||||
def setup_offroad_alert(click, pm: PubMaster, scroll=None):
|
||||
def setup_offroad_alert(click, pm: PubMaster):
|
||||
put_update_params(Params())
|
||||
set_offroad_alert("Offroad_TemperatureTooHigh", True, extra_text='99C')
|
||||
set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text='longitudinal')
|
||||
@@ -139,73 +137,22 @@ def setup_offroad_alert(click, pm: PubMaster, scroll=None):
|
||||
close_settings(click, pm)
|
||||
|
||||
|
||||
def setup_confirmation_dialog(click, pm: PubMaster, scroll=None):
|
||||
def setup_confirmation_dialog(click, pm: PubMaster):
|
||||
setup_settings(click, pm)
|
||||
click(1985, 791) # reset calibration
|
||||
|
||||
|
||||
def setup_experimental_mode_description(click, pm: PubMaster, scroll=None):
|
||||
def setup_experimental_mode_description(click, pm: PubMaster):
|
||||
setup_settings_toggles(click, pm)
|
||||
click(1200, 280) # expand description for experimental mode
|
||||
|
||||
|
||||
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_openpilot_long_confirmation_dialog(click, pm: PubMaster):
|
||||
setup_settings_developer(click, pm)
|
||||
click(650, 960) # toggle openpilot longitudinal control
|
||||
|
||||
|
||||
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):
|
||||
def setup_onroad(click, pm: PubMaster):
|
||||
ds = messaging.new_message('deviceState')
|
||||
ds.deviceState.started = True
|
||||
|
||||
@@ -226,7 +173,7 @@ def setup_onroad(click, pm: PubMaster, scroll=None):
|
||||
time.sleep(0.05)
|
||||
|
||||
|
||||
def setup_onroad_sidebar(click, pm: PubMaster, scroll=None):
|
||||
def setup_onroad_sidebar(click, pm: PubMaster):
|
||||
setup_onroad(click, pm)
|
||||
click(100, 100) # open sidebar
|
||||
|
||||
@@ -245,23 +192,23 @@ def setup_onroad_alert(click, pm: PubMaster, size: log.SelfdriveState.AlertSize,
|
||||
time.sleep(0.05)
|
||||
|
||||
|
||||
def setup_onroad_small_alert(click, pm: PubMaster, scroll=None):
|
||||
def setup_onroad_small_alert(click, pm: PubMaster):
|
||||
setup_onroad_alert(click, pm, AlertSize.small, "Small Alert", "This is a small alert", AlertStatus.normal)
|
||||
|
||||
|
||||
def setup_onroad_medium_alert(click, pm: PubMaster, scroll=None):
|
||||
def setup_onroad_medium_alert(click, pm: PubMaster):
|
||||
setup_onroad_alert(click, pm, AlertSize.mid, "Medium Alert", "This is a medium alert", AlertStatus.userPrompt)
|
||||
|
||||
|
||||
def setup_onroad_full_alert(click, pm: PubMaster, scroll=None):
|
||||
def setup_onroad_full_alert(click, pm: PubMaster):
|
||||
setup_onroad_alert(click, pm, AlertSize.full, "DISENGAGE IMMEDIATELY", "Driver Distracted", AlertStatus.critical)
|
||||
|
||||
|
||||
def setup_onroad_full_alert_multiline(click, pm: PubMaster, scroll=None):
|
||||
def setup_onroad_full_alert_multiline(click, pm: PubMaster):
|
||||
setup_onroad_alert(click, pm, AlertSize.full, "Reverse\nGear", "", AlertStatus.normal)
|
||||
|
||||
|
||||
def setup_onroad_full_alert_long_text(click, pm: PubMaster, scroll=None):
|
||||
def setup_onroad_full_alert_long_text(click, pm: PubMaster):
|
||||
setup_onroad_alert(click, pm, AlertSize.full, "TAKE CONTROL IMMEDIATELY", "Calibration Invalid: Remount Device & Recalibrate", AlertStatus.userPrompt)
|
||||
|
||||
|
||||
@@ -296,19 +243,6 @@ 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):
|
||||
@@ -342,20 +276,11 @@ 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, self.scroll)
|
||||
setup_case(self.click, self.pm)
|
||||
self.screenshot(name)
|
||||
|
||||
|
||||
|
||||
@@ -12,8 +12,6 @@ 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
|
||||
|
||||
|
||||
@@ -23,7 +21,7 @@ class UIStatus(Enum):
|
||||
OVERRIDE = "override"
|
||||
|
||||
|
||||
class UIState(UIStateSP):
|
||||
class UIState:
|
||||
_instance: 'UIState | None' = None
|
||||
|
||||
def __new__(cls):
|
||||
@@ -33,7 +31,6 @@ class UIState(UIStateSP):
|
||||
return cls._instance
|
||||
|
||||
def _initialize(self):
|
||||
UIStateSP.__init__(self)
|
||||
self.params = Params()
|
||||
self.sm = messaging.SubMaster(
|
||||
[
|
||||
@@ -58,7 +55,7 @@ class UIState(UIStateSP):
|
||||
"carControl",
|
||||
"liveParameters",
|
||||
"rawAudioData",
|
||||
] + self.sm_services_ext
|
||||
]
|
||||
)
|
||||
|
||||
self.prime_state = PrimeState()
|
||||
@@ -114,7 +111,6 @@ class UIState(UIStateSP):
|
||||
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
|
||||
@@ -184,7 +180,6 @@ class UIState(UIStateSP):
|
||||
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,3 +1,5 @@
|
||||
import glob
|
||||
|
||||
Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'visionipc', 'transformations')
|
||||
lenv = env.Clone()
|
||||
lenvCython = envCython.Clone()
|
||||
@@ -21,6 +23,12 @@ 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']
|
||||
@@ -32,13 +40,20 @@ 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,15 +4,20 @@ 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,3 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
#include "sunnypilot/modeld/runners/runmodel.h"
|
||||
#include "sunnypilot/modeld/runners/snpemodel.h"
|
||||
|
||||
116
sunnypilot/modeld/runners/snpemodel.cc
Normal file
116
sunnypilot/modeld/runners/snpemodel.cc
Normal file
@@ -0,0 +1,116 @@
|
||||
#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();
|
||||
}
|
||||
}
|
||||
52
sunnypilot/modeld/runners/snpemodel.h
Normal file
52
sunnypilot/modeld/runners/snpemodel.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#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;
|
||||
};
|
||||
9
sunnypilot/modeld/runners/snpemodel.pxd
Normal file
9
sunnypilot/modeld/runners/snpemodel.pxd
Normal file
@@ -0,0 +1,9 @@
|
||||
# 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)
|
||||
17
sunnypilot/modeld/runners/snpemodel_pyx.pyx
Normal file
17
sunnypilot/modeld/runners/snpemodel_pyx.pyx
Normal file
@@ -0,0 +1,17 @@
|
||||
# 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_v10.json"
|
||||
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-docs/refs/heads/gh-pages/docs/driving_models_v9.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 = 12
|
||||
REQUIRED_MIN_SELECTOR_VERSION = 12
|
||||
CURRENT_SELECTOR_VERSION = 11
|
||||
REQUIRED_MIN_SELECTOR_VERSION = 11
|
||||
|
||||
USE_ONNX = os.getenv('USE_ONNX', PC)
|
||||
|
||||
|
||||
5
sunnypilot/navd/README.md
Normal file
5
sunnypilot/navd/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Navigation
|
||||
|
||||
Navigation daemon with Mapbox integration for semi-offline navigation. This module handles route planning, geocoding, and turn-by-turn instructions to support autonomous driving features.
|
||||
|
||||
- `navigation_helpers/`: Mapbox API integration and navigation instructions processing.
|
||||
@@ -72,6 +72,15 @@ class Coordinate:
|
||||
return x * EARTH_MEAN_RADIUS
|
||||
|
||||
|
||||
def bearing_between_two_points(point_one: Coordinate, point_two: Coordinate) -> float:
|
||||
dlon = math.radians(point_two.longitude - point_one.longitude)
|
||||
bearing_radians = math.atan2(math.sin(dlon)* math.cos(point_two.latitude), math.cos(point_one.latitude) * math.sin(point_two.latitude) -
|
||||
math.sin(point_one.latitude) * math.cos(point_two.latitude) * math.cos(dlon))
|
||||
bearing_degrees = math.degrees(bearing_radians)
|
||||
bearing_normalized = (bearing_degrees + 360) % 360
|
||||
return bearing_normalized
|
||||
|
||||
|
||||
def minimum_distance(a: Coordinate, b: Coordinate, p: Coordinate):
|
||||
if a.distance_to(b) < 0.01:
|
||||
return a.distance_to(p)
|
||||
@@ -126,6 +135,8 @@ def string_to_direction(direction: str) -> str:
|
||||
if d in direction:
|
||||
if 'slight' in direction and d in MODIFIABLE_DIRECTIONS:
|
||||
return 'slight' + d.capitalize()
|
||||
elif 'sharp' in direction and d in MODIFIABLE_DIRECTIONS:
|
||||
return 'sharp' + d.capitalize()
|
||||
return d
|
||||
return 'none'
|
||||
|
||||
|
||||
113
sunnypilot/navd/navigation_helpers/mapbox_integration.py
Normal file
113
sunnypilot/navd/navigation_helpers/mapbox_integration.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
Copyright (c) 2021-, James Vecellio, 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 requests
|
||||
from urllib.parse import quote
|
||||
|
||||
from openpilot.common.params import Params
|
||||
|
||||
|
||||
class MapboxIntegration:
|
||||
def __init__(self):
|
||||
self.params = Params()
|
||||
|
||||
def get_public_token(self) -> str:
|
||||
token: str = self.params.get('MapboxToken', return_default=True)
|
||||
return token
|
||||
|
||||
def set_destination(self, postvars, current_lon, current_lat, bearing=None) -> tuple[dict, bool]:
|
||||
if 'latitude' in postvars and 'longitude' in postvars:
|
||||
self.nav_confirmed(postvars, current_lon, current_lat, bearing)
|
||||
return postvars, True
|
||||
|
||||
addr = postvars['place_name']
|
||||
if not addr:
|
||||
return postvars, False
|
||||
|
||||
token = self.get_public_token()
|
||||
url = f'https://api.mapbox.com/geocoding/v5/mapbox.places/{quote(addr)}.json?access_token={token}&limit=1&proximity={current_lon},{current_lat}'
|
||||
try:
|
||||
response = requests.get(url, timeout=5)
|
||||
if response.status_code == 200:
|
||||
features = response.json()['features']
|
||||
if features:
|
||||
longitude, latitude = features[0]['geometry']['coordinates']
|
||||
postvars.update({'latitude': latitude, 'longitude': longitude, 'name': addr})
|
||||
self.nav_confirmed(postvars, current_lon, current_lat, bearing)
|
||||
return postvars, True
|
||||
except requests.RequestException:
|
||||
pass # Broad exception to handle network errors like no internet without crashing navd process.
|
||||
return postvars, False
|
||||
|
||||
def nav_confirmed(self, postvars, start_lon, start_lat, bearing=None) -> None:
|
||||
if not postvars:
|
||||
return
|
||||
|
||||
latitude = float(postvars['latitude'])
|
||||
longitude = float(postvars['longitude'])
|
||||
|
||||
data: dict = {'navData': {'current': {'latitude': latitude, 'longitude': longitude}, 'route': {}}}
|
||||
|
||||
token = self.get_public_token()
|
||||
route_data = self.generate_route(start_lon, start_lat, longitude, latitude, token, bearing)
|
||||
if route_data:
|
||||
data['navData']['route'] = route_data
|
||||
self.params.put('MapboxSettings', data)
|
||||
|
||||
@staticmethod
|
||||
def generate_route(start_lon, start_lat, end_lon, end_lat, token, bearing=None) -> dict | None:
|
||||
if not token:
|
||||
return None
|
||||
|
||||
params = {
|
||||
'access_token': token,
|
||||
'geometries': 'geojson',
|
||||
'steps': 'true',
|
||||
'overview': 'full',
|
||||
'annotations': 'maxspeed',
|
||||
'alternatives': 'false',
|
||||
'banner_instructions': 'true',
|
||||
}
|
||||
if bearing is not None:
|
||||
params['bearings'] = f'{int((bearing + 360) % 360):.0f},90;'
|
||||
|
||||
try:
|
||||
response = requests.get(f'https://api.mapbox.com/directions/v5/mapbox/driving/{start_lon},{start_lat};{end_lon},{end_lat}', params=params, timeout=5)
|
||||
data = response.json() if response.status_code == 200 else {}
|
||||
except requests.RequestException:
|
||||
return None
|
||||
|
||||
routes = data['routes'] if data else None
|
||||
legs = routes[0]['legs'] if routes else None
|
||||
|
||||
if data.get('code') != 'Ok' or not routes or not legs:
|
||||
return None
|
||||
|
||||
route = routes[0]
|
||||
leg = legs[0]
|
||||
|
||||
steps = [
|
||||
{
|
||||
'maneuver': step['maneuver']['type'],
|
||||
'instruction': step['maneuver']['instruction'],
|
||||
'distance': step['distance'],
|
||||
'duration': step['duration'],
|
||||
'location': {'longitude': step['maneuver']['location'][0], 'latitude': step['maneuver']['location'][1]},
|
||||
'modifier': step['maneuver'].get('modifier', 'none'),
|
||||
'bannerInstructions': step['bannerInstructions'],
|
||||
}
|
||||
for step in leg['steps']
|
||||
]
|
||||
|
||||
maxspeed = [{'speed': item['speed'], 'unit': item['unit']} for item in leg['annotation']['maxspeed'] if 'speed' in item]
|
||||
|
||||
return {
|
||||
'steps': steps,
|
||||
'totalDistance': route['distance'],
|
||||
'totalDuration': route['duration'],
|
||||
'geometry': [{'longitude': coord[0], 'latitude': coord[1]} for coord in route['geometry']['coordinates']],
|
||||
'maxspeed': maxspeed,
|
||||
}
|
||||
152
sunnypilot/navd/navigation_helpers/nav_instructions.py
Normal file
152
sunnypilot/navd/navigation_helpers/nav_instructions.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
Copyright (c) 2021-, James Vecellio, 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 numpy import interp
|
||||
|
||||
from openpilot.common.params import Params
|
||||
|
||||
from openpilot.sunnypilot.navd.helpers import Coordinate, bearing_between_two_points, distance_along_geometry, string_to_direction
|
||||
|
||||
|
||||
class NavigationInstructions:
|
||||
def __init__(self):
|
||||
self.coord = Coordinate(0, 0)
|
||||
self.params = Params()
|
||||
|
||||
self._cached_route = None
|
||||
self._route_loaded = False
|
||||
self._no_route = False
|
||||
|
||||
self.closest_idx: float = 0
|
||||
|
||||
def get_route_progress(self, current_lat, current_lon) -> dict | None:
|
||||
route = self.get_current_route()
|
||||
if not route or not route['geometry'] or not route['steps']:
|
||||
return None
|
||||
|
||||
self.coord.latitude = current_lat
|
||||
self.coord.longitude = current_lon
|
||||
|
||||
# Find the closest point on the route relative to self
|
||||
self.closest_idx, min_distance = min(((idx, self.coord.distance_to(coord)) for idx, coord in enumerate(route['geometry'])), key=lambda x: x[1])
|
||||
closest_cumulative = distance_along_geometry(route['geometry'], self.coord)
|
||||
|
||||
# Find the current step index, which is the HIGHEST idx where the step location cumulative less/equal closest cumulative
|
||||
current_step_idx = max((idx for idx, step in enumerate(route['steps']) if step['cumulative_distance'] <= closest_cumulative), default=-1)
|
||||
current_step = route['steps'][current_step_idx if current_step_idx >= 0 else 0]
|
||||
|
||||
# The next turn is the next step relative to our cumulative index
|
||||
next_turn_idx = current_step_idx + 1
|
||||
next_turn = route['steps'][next_turn_idx] if 0 <= next_turn_idx < len(route['steps']) else None
|
||||
|
||||
current_maxspeed = current_step['maxspeed']
|
||||
|
||||
distance_to_end_of_step = max(0, current_step['distance'] - (closest_cumulative - current_step['cumulative_distance']))
|
||||
|
||||
all_maneuvers: list = []
|
||||
max_maneuvers = 3
|
||||
for idx in range(current_step_idx, min(current_step_idx + max_maneuvers, len(route['steps']))):
|
||||
step = route['steps'][idx]
|
||||
if idx == current_step_idx:
|
||||
distance = distance_to_end_of_step
|
||||
else:
|
||||
distance = step['cumulative_distance'] - closest_cumulative
|
||||
all_maneuvers.append({'distance': distance, 'type': step['maneuver'], 'modifier': step['modifier'], 'instruction': step['instruction']})
|
||||
|
||||
return {
|
||||
'distance_from_route': min_distance,
|
||||
'current_step': current_step,
|
||||
'next_turn': next_turn,
|
||||
'current_maxspeed': current_maxspeed,
|
||||
'all_maneuvers': all_maneuvers,
|
||||
'current_step_idx': current_step_idx,
|
||||
'distance_to_end_of_step': distance_to_end_of_step,
|
||||
}
|
||||
|
||||
def get_current_route(self):
|
||||
if self._route_loaded and self._cached_route is not None:
|
||||
return self._cached_route
|
||||
if self._no_route:
|
||||
return None
|
||||
|
||||
param_value = self.params.get('MapboxSettings')
|
||||
route = param_value['navData']['route'] if param_value else None
|
||||
if not route:
|
||||
self._no_route = True
|
||||
return None
|
||||
|
||||
geometry = [Coordinate(coord['latitude'], coord['longitude']) for coord in route['geometry']]
|
||||
cumulative_distances = [0.0]
|
||||
cumulative_distances.extend(cumulative_distances[-1] + geometry[step - 1].distance_to(geometry[step]) for step in range(1, len(geometry)))
|
||||
maxspeed = [(speed['speed'], speed['unit']) for speed in route['maxspeed']]
|
||||
steps = []
|
||||
for step in route['steps']:
|
||||
location = Coordinate(step['location']['latitude'], step['location']['longitude'])
|
||||
closest_idx = min(range(len(geometry)), key=lambda i: location.distance_to(geometry[i]))
|
||||
steps.append({
|
||||
'bannerInstructions': step['bannerInstructions'],
|
||||
'distance': step['distance'],
|
||||
'duration': step['duration'],
|
||||
'maneuver': step['maneuver'],
|
||||
'location': location,
|
||||
'cumulative_distance': cumulative_distances[closest_idx],
|
||||
'maxspeed': maxspeed[min(closest_idx, len(maxspeed) - 1)] if len(maxspeed) > 0 else (0, 'kmh'),
|
||||
'modifier': string_to_direction(step['modifier']),
|
||||
'instruction': step['instruction'],
|
||||
})
|
||||
self._cached_route = {
|
||||
'bearings': [bearing_between_two_points(geometry[i], geometry[i+2]) for i in range(len(geometry)-2)],
|
||||
'steps': steps,
|
||||
'total_distance': route['totalDistance'],
|
||||
'total_duration': route['totalDuration'],
|
||||
'geometry': geometry,
|
||||
'cumulative_distances': cumulative_distances,
|
||||
'maxspeed': maxspeed,
|
||||
}
|
||||
self._route_loaded = True
|
||||
return self._cached_route
|
||||
|
||||
def clear_route_cache(self):
|
||||
self._cached_route = None
|
||||
self._route_loaded = False
|
||||
self._no_route = False
|
||||
|
||||
def route_bearing_misalign(self, route, bearing, v_ego) -> bool:
|
||||
route_bearing_misalign:bool = False
|
||||
|
||||
if v_ego < 5.0:
|
||||
route_bearing_misalign = False
|
||||
elif 0 < self.closest_idx < len(route['geometry']) -1:
|
||||
route_bearing = route['bearings'][self.closest_idx -1]
|
||||
current_bearing_normalized = (bearing + 360) % 360
|
||||
bearing_difference = abs(current_bearing_normalized - route_bearing)
|
||||
|
||||
if min(bearing_difference, 360 - bearing_difference) > 95:
|
||||
route_bearing_misalign = True # flag for recompute/cancellation
|
||||
return route_bearing_misalign
|
||||
|
||||
def get_upcoming_turn_from_progress(self, progress, current_lat, current_lon, v_ego: float) -> str:
|
||||
if progress and progress['next_turn']:
|
||||
speed_breakpoints: list = [0, 5, 10, 15, 20, 25, 30, 35, 40]
|
||||
distance_breakpoints: list = [20, 25, 30, 45, 60, 75, 90, 105, 120]
|
||||
distance_interp = interp(v_ego, speed_breakpoints, distance_breakpoints)
|
||||
|
||||
self.coord.latitude = current_lat
|
||||
self.coord.longitude = current_lon
|
||||
distance = self.coord.distance_to(progress['next_turn']['location'])
|
||||
|
||||
if distance <= distance_interp:
|
||||
modifier = progress['next_turn']['modifier']
|
||||
return str(modifier)
|
||||
return 'none'
|
||||
|
||||
@staticmethod
|
||||
def arrived_at_destination(progress, v_ego) -> bool:
|
||||
if v_ego < 1.0:
|
||||
maneuvers = progress['all_maneuvers'][0]
|
||||
if maneuvers['type'] == 'arrive' or maneuvers['instruction'].startswith('Your destination'):
|
||||
return True
|
||||
return False
|
||||
98
sunnypilot/navd/navigation_helpers/tests/test_mapbox.py
Normal file
98
sunnypilot/navd/navigation_helpers/tests/test_mapbox.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
Copyright (c) 2021-, James Vecellio, 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 os
|
||||
|
||||
from openpilot.common.constants import CV
|
||||
|
||||
from openpilot.sunnypilot.navd.navigation_helpers.mapbox_integration import MapboxIntegration
|
||||
from openpilot.sunnypilot.navd.navigation_helpers.nav_instructions import NavigationInstructions
|
||||
|
||||
|
||||
class TestMapbox:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.mapbox = MapboxIntegration()
|
||||
cls.nav = NavigationInstructions()
|
||||
|
||||
token = os.environ.get('MAPBOX_TOKEN_CI')
|
||||
if token:
|
||||
cls.mapbox.params.put('MapboxToken', token)
|
||||
|
||||
# route setup
|
||||
cls.current_lon, cls.current_lat = -119.17557, 34.23305
|
||||
cls.mapbox.params.put('MapboxRoute', '740 E Ventura Blvd. Camarillo, CA')
|
||||
cls.postvars = {"place_name": cls.mapbox.params.get('MapboxRoute')}
|
||||
cls.postvars, cls.valid_addr = cls.mapbox.set_destination(cls.postvars, cls.current_lon, cls.current_lat)
|
||||
cls.route = cls.nav.get_current_route()
|
||||
cls.progress = cls.nav.get_route_progress(cls.current_lat, cls.current_lon)
|
||||
|
||||
def test_set_destination(self):
|
||||
assert self.valid_addr
|
||||
settings = self.mapbox.params.get('MapboxSettings')
|
||||
assert settings is not None
|
||||
dest_lat = settings['navData']['current']['latitude']
|
||||
dest_lon = settings['navData']['current']['longitude']
|
||||
assert dest_lat == self.postvars["latitude"] and dest_lon == self.postvars["longitude"]
|
||||
|
||||
def test_get_route(self):
|
||||
assert self.route is not None
|
||||
assert 'steps' in self.route
|
||||
assert 'geometry' in self.route
|
||||
assert 'maxspeed' in self.route
|
||||
assert 'total_distance' in self.route
|
||||
assert 'total_duration' in self.route
|
||||
assert len(self.route['steps']) > 0
|
||||
assert len(self.route['geometry']) > 0
|
||||
assert len(self.route['maxspeed']) > 0
|
||||
|
||||
if self.route and 'steps' in self.route:
|
||||
for step in self.route['steps']:
|
||||
assert 'modifier' in step
|
||||
|
||||
def test_upcoming_turn_detection(self):
|
||||
upcoming = self.nav.get_upcoming_turn_from_progress(self.progress, self.current_lat, self.current_lon, v_ego=40.0)
|
||||
assert isinstance(upcoming, str)
|
||||
assert upcoming == 'none'
|
||||
|
||||
if self.route['steps']:
|
||||
turn_lat = self.route['steps'][1]['location'].latitude
|
||||
turn_lon = self.route['steps'][1]['location'].longitude
|
||||
close_lat = turn_lat - 0.000175 # slightly before the turn
|
||||
if self.progress and self.progress.get('next_turn'):
|
||||
expected_turn = self.progress['next_turn']['modifier']
|
||||
upcoming_close = self.nav.get_upcoming_turn_from_progress(self.progress, close_lat, turn_lon, v_ego=0.0)
|
||||
if expected_turn:
|
||||
assert upcoming_close == expected_turn == 'right', "Should be a right turn upcoming"
|
||||
|
||||
def test_route_progress_tracking(self):
|
||||
assert self.progress is not None
|
||||
assert 'distance_from_route' in self.progress
|
||||
assert 'next_turn' in self.progress
|
||||
assert 'current_maxspeed' in self.progress
|
||||
assert 'all_maneuvers' in self.progress
|
||||
assert 'distance_to_end_of_step' in self.progress
|
||||
assert self.progress['distance_from_route'] >= 0
|
||||
assert isinstance(self.progress['all_maneuvers'], list)
|
||||
|
||||
def test_speed_limit_handling(self):
|
||||
speed_limit_metric = self.progress['current_maxspeed'][0]
|
||||
speed_limit_imperial = (round(speed_limit_metric * CV.KPH_TO_MPH))
|
||||
assert isinstance(speed_limit_metric, int)
|
||||
assert isinstance(speed_limit_imperial, int)
|
||||
|
||||
def test_arrival_detection(self):
|
||||
is_arrived = self.nav.arrived_at_destination(self.progress, 2.0)
|
||||
assert isinstance(is_arrived, bool)
|
||||
assert not is_arrived
|
||||
|
||||
def test_bearing_misalign(self):
|
||||
lat = self.route['steps'][1]['location'].latitude
|
||||
lon = self.route['steps'][1]['location'].longitude
|
||||
self.nav.get_route_progress(lat, lon)
|
||||
route_bearing_misaligned = self.nav.route_bearing_misalign(self.route, 45, 5.0)
|
||||
# based on math: closest index: 7, normalized bearing: 45 route bearing: 180.5486953778888, expected differential: 135.54869538
|
||||
assert route_bearing_misaligned
|
||||
Binary file not shown.
Binary file not shown.
@@ -4,27 +4,21 @@ 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(ModelStateBase):
|
||||
class ControlsExt:
|
||||
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)
|
||||
@@ -33,14 +27,8 @@ class ControlsExt(ModelStateBase):
|
||||
self.sm_services_ext = ['radarState', 'selfdriveStateSP']
|
||||
self.pm_services_ext = ['carControlSP']
|
||||
|
||||
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_params_sp(self) -> None:
|
||||
self.blinker_pause_lateral.get_params()
|
||||
|
||||
def get_lat_active(self, sm: messaging.SubMaster) -> bool:
|
||||
if self.blinker_pause_lateral.update(sm['carState']):
|
||||
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
import random
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import jwt
|
||||
from openpilot.common.api.base import BaseApi
|
||||
@@ -80,19 +81,23 @@ class SunnylinkApi(BaseApi):
|
||||
if sunnylink_dongle_id not in (None, UNREGISTERED_SUNNYLINK_DONGLE_ID):
|
||||
return sunnylink_dongle_id
|
||||
|
||||
jwt_algo, private_key, public_key = BaseApi.get_key_pair()
|
||||
privkey_path = Path(f"{Paths.persist_root()}/comma/id_rsa")
|
||||
pubkey_path = Path(f"{Paths.persist_root()}/comma/id_rsa.pub")
|
||||
|
||||
start_time = time.monotonic()
|
||||
successful_registration = False
|
||||
if not public_key:
|
||||
if not pubkey_path.is_file():
|
||||
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=jwt_algo)
|
||||
register_token = jwt.encode({'register': True, 'exp': datetime.utcnow() + timedelta(hours=1)}, private_key, algorithm='RS256')
|
||||
try:
|
||||
if verbose or time.monotonic() - start_time < timeout / 2:
|
||||
self._status_update("Registering device to sunnylink...")
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
#!/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
|
||||
@@ -37,11 +32,9 @@ 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'])
|
||||
@@ -187,30 +180,16 @@ 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 | object | dict | None]]] = {"params": []}
|
||||
params_dict: dict[str, list[dict[str, str | bool | int | None]]] = {"params": []}
|
||||
for key in available_keys:
|
||||
value = get_param_as_byte(key, get_default=True)
|
||||
|
||||
param_entry = {
|
||||
params_dict["params"].append({
|
||||
"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", []))}
|
||||
|
||||
@@ -259,7 +238,10 @@ 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
@@ -1,205 +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.
|
||||
"""
|
||||
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()
|
||||
@@ -1,86 +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.
|
||||
"""
|
||||
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"]
|
||||
@@ -1,202 +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.
|
||||
"""
|
||||
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
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/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()
|
||||
@@ -94,7 +94,6 @@ class FontWeight(StrEnum):
|
||||
BOLD = "Inter-Bold.fnt"
|
||||
SEMI_BOLD = "Inter-SemiBold.fnt"
|
||||
UNIFONT = "unifont.fnt"
|
||||
AUDIOWIDE = "Audiowide-Regular.ttf"
|
||||
|
||||
# Small UI fonts
|
||||
DISPLAY_REGULAR = "Inter-Regular.fnt"
|
||||
@@ -228,8 +227,6 @@ class GuiApplication(GuiApplicationExt):
|
||||
self._render_profiler = None
|
||||
self._render_profile_start_time = None
|
||||
|
||||
GuiApplicationExt.__init__(self)
|
||||
|
||||
@property
|
||||
def frame(self):
|
||||
return self._frame
|
||||
@@ -470,9 +467,6 @@ 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,12 +41,8 @@ class GuiScrollPanel:
|
||||
if DEBUG:
|
||||
rl.draw_rectangle_lines(0, 0, abs(int(self._velocity_filter_y.x)), 10, rl.RED)
|
||||
|
||||
# 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
|
||||
# Handle mouse wheel
|
||||
self._offset_filter_y.x += rl.get_mouse_wheel_move() * MOUSE_WHEEL_SCROLL_SPEED
|
||||
|
||||
max_scroll_distance = max(0, content.height - bounds.height)
|
||||
if self._scroll_state == ScrollState.IDLE:
|
||||
|
||||
@@ -383,7 +383,7 @@ class WifiManager:
|
||||
'connection': {
|
||||
'type': ('s', '802-11-wireless'),
|
||||
'uuid': ('s', str(uuid.uuid4())),
|
||||
'id': ('s', f'sunnypilot connection {ssid}'),
|
||||
'id': ('s', f'openpilot 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 sunnypilot version"
|
||||
error_msg = "Incompatible openpilot version"
|
||||
self.download_failed(self.download_url, error_msg)
|
||||
except Exception:
|
||||
error_msg = "Invalid URL"
|
||||
|
||||
@@ -6,35 +6,10 @@ 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,24 +17,19 @@ 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 = BASE_BG_COLOR
|
||||
OFF_BG_COLOR = rl.Color(70, 70, 70, 255) # Lighter Grey
|
||||
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
|
||||
@@ -51,29 +46,5 @@ 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
|
||||
|
||||
# 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
|
||||
|
||||
@@ -1,40 +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.
|
||||
"""
|
||||
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
|
||||
@@ -1,26 +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.
|
||||
"""
|
||||
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)
|
||||
@@ -1,42 +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.
|
||||
"""
|
||||
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,13 +7,10 @@ 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, MultipleButtonAction, _resolve_value
|
||||
from openpilot.system.ui.widgets.list_view import ListItem, ToggleAction, ItemAction
|
||||
from openpilot.system.ui.sunnypilot.lib.styles import style
|
||||
from openpilot.system.ui.sunnypilot.widgets.option_control import OptionControlSP, LABEL_WIDTH
|
||||
|
||||
|
||||
class ToggleActionSP(ToggleAction):
|
||||
@@ -23,80 +20,11 @@ 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, inline: bool = True, title_color: rl.Color = style.ITEM_TEXT_COLOR):
|
||||
action_item: ItemAction | None = None):
|
||||
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)
|
||||
@@ -105,19 +33,10 @@ 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:
|
||||
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: # 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)
|
||||
|
||||
action_width = self.action_item.rect.width
|
||||
if isinstance(self.action_item, ToggleAction):
|
||||
@@ -128,13 +47,6 @@ 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)
|
||||
@@ -150,9 +62,9 @@ class ListItemSP(ListItem):
|
||||
|
||||
# Draw title
|
||||
if self.title:
|
||||
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)
|
||||
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)
|
||||
|
||||
# Render toggle and handle callback
|
||||
if self.action_item.render(left_rect) and self.action_item.enabled:
|
||||
@@ -162,13 +74,14 @@ class ListItemSP(ListItem):
|
||||
else:
|
||||
if self.title:
|
||||
# Draw main text
|
||||
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)
|
||||
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)
|
||||
|
||||
# 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:
|
||||
@@ -178,12 +91,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)
|
||||
|
||||
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)
|
||||
description_rect = rl.Rectangle(
|
||||
self._rect.x + style.ITEM_PADDING,
|
||||
self._rect.y + style.ITEM_DESC_V_OFFSET,
|
||||
content_width,
|
||||
description_height
|
||||
)
|
||||
self._html_renderer.render(description_rect)
|
||||
|
||||
|
||||
@@ -191,23 +104,3 @@ 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)
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
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)
|
||||
@@ -1,57 +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.
|
||||
"""
|
||||
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)
|
||||
@@ -1,240 +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.
|
||||
"""
|
||||
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._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 + border
|
||||
rl.draw_rectangle_rounded(input_rect, roundness, 10, rl.Color(0, 0, 0, 0))
|
||||
rl.draw_rectangle_rounded_lines_ex(input_rect, roundness, 10, 3, rl.Color(150, 150, 150, 200))
|
||||
|
||||
# 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_release(self, mouse_pos):
|
||||
if self._search_rect and rl.check_collision_point_rec(mouse_pos, self._search_rect):
|
||||
self._on_search_clicked()
|
||||
return True
|
||||
return super()._handle_mouse_release(mouse_pos)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user