date: 2025-06-21T09:02:55
master commit: 5e3fc13751
parent
100f89a161
commit
c49af46c1e
481 changed files with 19488 additions and 17260 deletions
@ -0,0 +1,52 @@ |
||||
import os |
||||
import subprocess |
||||
from openpilot.common.basedir import BASEDIR |
||||
|
||||
|
||||
class Spinner: |
||||
def __init__(self): |
||||
try: |
||||
self.spinner_proc = subprocess.Popen(["./spinner.py"], |
||||
stdin=subprocess.PIPE, |
||||
cwd=os.path.join(BASEDIR, "system", "ui"), |
||||
close_fds=True) |
||||
except OSError: |
||||
self.spinner_proc = None |
||||
|
||||
def __enter__(self): |
||||
return self |
||||
|
||||
def update(self, spinner_text: str): |
||||
if self.spinner_proc is not None: |
||||
self.spinner_proc.stdin.write(spinner_text.encode('utf8') + b"\n") |
||||
try: |
||||
self.spinner_proc.stdin.flush() |
||||
except BrokenPipeError: |
||||
pass |
||||
|
||||
def update_progress(self, cur: float, total: float): |
||||
self.update(str(round(100 * cur / total))) |
||||
|
||||
def close(self): |
||||
if self.spinner_proc is not None: |
||||
self.spinner_proc.kill() |
||||
try: |
||||
self.spinner_proc.communicate(timeout=2.) |
||||
except subprocess.TimeoutExpired: |
||||
print("WARNING: failed to kill spinner") |
||||
self.spinner_proc = None |
||||
|
||||
def __del__(self): |
||||
self.close() |
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback): |
||||
self.close() |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
import time |
||||
with Spinner() as s: |
||||
s.update("Spinner text") |
||||
time.sleep(5.0) |
||||
print("gone") |
||||
time.sleep(5.0) |
@ -0,0 +1,63 @@ |
||||
#!/usr/bin/env python3 |
||||
import os |
||||
import time |
||||
import subprocess |
||||
from openpilot.common.basedir import BASEDIR |
||||
|
||||
|
||||
class TextWindow: |
||||
def __init__(self, text): |
||||
try: |
||||
self.text_proc = subprocess.Popen(["./text.py", text], |
||||
stdin=subprocess.PIPE, |
||||
cwd=os.path.join(BASEDIR, "system", "ui"), |
||||
close_fds=True) |
||||
except OSError: |
||||
self.text_proc = None |
||||
|
||||
def get_status(self): |
||||
if self.text_proc is not None: |
||||
self.text_proc.poll() |
||||
return self.text_proc.returncode |
||||
return None |
||||
|
||||
def __enter__(self): |
||||
return self |
||||
|
||||
def close(self): |
||||
if self.text_proc is not None: |
||||
self.text_proc.terminate() |
||||
self.text_proc = None |
||||
|
||||
def wait_for_exit(self): |
||||
if self.text_proc is not None: |
||||
while True: |
||||
if self.get_status() == 1: |
||||
return |
||||
time.sleep(0.1) |
||||
|
||||
def __del__(self): |
||||
self.close() |
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback): |
||||
self.close() |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
text = """Traceback (most recent call last): |
||||
File "./controlsd.py", line 608, in <module> |
||||
main() |
||||
File "./controlsd.py", line 604, in main |
||||
controlsd_thread(sm, pm, logcan) |
||||
File "./controlsd.py", line 455, in controlsd_thread |
||||
1/0 |
||||
ZeroDivisionError: division by zero""" |
||||
print(text) |
||||
|
||||
with TextWindow(text) as s: |
||||
for _ in range(100): |
||||
if s.get_status() == 1: |
||||
print("Got exit button") |
||||
break |
||||
time.sleep(0.1) |
||||
print("gone") |
@ -1 +1 @@ |
||||
#define COMMA_VERSION "0.9.9" |
||||
#define COMMA_VERSION "0.9.10" |
||||
|
@ -0,0 +1,22 @@ |
||||
import os |
||||
import time |
||||
import struct |
||||
from openpilot.system.hardware.hw import Paths |
||||
|
||||
WATCHDOG_FN = f"{Paths.shm_path()}/wd_" |
||||
_LAST_KICK = 0.0 |
||||
|
||||
def kick_watchdog(): |
||||
global _LAST_KICK |
||||
current_time = time.monotonic() |
||||
|
||||
if current_time - _LAST_KICK < 1.0: |
||||
return |
||||
|
||||
try: |
||||
with open(f"{WATCHDOG_FN}{os.getpid()}", 'wb') as f: |
||||
f.write(struct.pack('<Q', int(current_time * 1e9))) |
||||
f.flush() |
||||
_LAST_KICK = current_time |
||||
except OSError: |
||||
pass |
@ -1,33 +0,0 @@ |
||||
# openpilot development workflow |
||||
|
||||
Aside from the ML models, most tools used for openpilot development are in this repo. |
||||
|
||||
Most development happens on normal Ubuntu workstations, and not in cars or directly on comma devices. See the [setup guide](../tools) for getting your PC setup for openpilot development. |
||||
|
||||
## Quick start |
||||
|
||||
```bash |
||||
# get the latest stuff |
||||
git pull |
||||
git lfs pull |
||||
git submodule update --init --recursive |
||||
|
||||
# update dependencies |
||||
tools/ubuntu_setup.sh |
||||
|
||||
# build everything |
||||
scons -j$(nproc) |
||||
|
||||
# build just the ui with either of these |
||||
scons -j8 selfdrive/ui/ |
||||
cd selfdrive/ui/ && scons -u -j8 |
||||
|
||||
# test everything |
||||
pytest |
||||
|
||||
# test just logging services |
||||
cd system/loggerd && pytest . |
||||
|
||||
# run the linter |
||||
op lint |
||||
``` |
@ -1 +1 @@ |
||||
8aadf02b2fd91f4e1285e18c2c7feb32d93b66f5 |
||||
5e3fc13751dc9b9c5d5e0991a17c672eda8bd122 |
@ -1 +1 @@ |
||||
1749153081 2025-06-05 12:51:21 -0700 |
||||
1750452370 2025-06-20 13:46:10 -0700 |
@ -1,35 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include "board_declarations.h" |
||||
|
||||
// //////////////////// //
|
||||
// Grey Panda (STM32F4) //
|
||||
// //////////////////// //
|
||||
|
||||
// Most hardware functionality is similar to white panda
|
||||
|
||||
board board_grey = { |
||||
.set_bootkick = unused_set_bootkick, |
||||
.harness_config = &white_harness_config, |
||||
.has_spi = false, |
||||
.has_canfd = false, |
||||
.fan_max_rpm = 0U, |
||||
.fan_max_pwm = 100U, |
||||
.avdd_mV = 3300U, |
||||
.fan_stall_recovery = false, |
||||
.fan_enable_cooldown_time = 0U, |
||||
.init = white_grey_init, |
||||
.init_bootloader = white_grey_init_bootloader, |
||||
.enable_can_transceiver = white_enable_can_transceiver, |
||||
.led_GPIO = {GPIOC, GPIOC, GPIOC}, |
||||
.led_pin = {9, 7, 6}, |
||||
.set_can_mode = white_set_can_mode, |
||||
.check_ignition = white_check_ignition, |
||||
.read_voltage_mV = white_read_voltage_mV, |
||||
.read_current_mA = white_read_current_mA, |
||||
.set_fan_enabled = unused_set_fan_enabled, |
||||
.set_ir_power = unused_set_ir_power, |
||||
.set_siren = unused_set_siren, |
||||
.read_som_gpio = unused_read_som_gpio, |
||||
.set_amp_enabled = unused_set_amp_enabled |
||||
}; |
@ -1,166 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include "board_declarations.h" |
||||
|
||||
// /////////////////////// //
|
||||
// Uno (STM32F4) + Harness //
|
||||
// /////////////////////// //
|
||||
|
||||
static void uno_enable_can_transceiver(uint8_t transceiver, bool enabled) { |
||||
switch (transceiver){ |
||||
case 1U: |
||||
set_gpio_output(GPIOC, 1, !enabled); |
||||
break; |
||||
case 2U: |
||||
set_gpio_output(GPIOC, 13, !enabled); |
||||
break; |
||||
case 3U: |
||||
set_gpio_output(GPIOA, 0, !enabled); |
||||
break; |
||||
case 4U: |
||||
set_gpio_output(GPIOB, 10, !enabled); |
||||
break; |
||||
default: |
||||
print("Invalid CAN transceiver ("); puth(transceiver); print("): enabling failed\n"); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
static void uno_set_bootkick(BootState state) { |
||||
if (state == BOOT_BOOTKICK) { |
||||
set_gpio_output(GPIOB, 14, false); |
||||
} else { |
||||
// We want the pin to be floating, not forced high!
|
||||
set_gpio_mode(GPIOB, 14, MODE_INPUT); |
||||
} |
||||
} |
||||
|
||||
static void uno_set_can_mode(uint8_t mode) { |
||||
uno_enable_can_transceiver(2U, false); |
||||
uno_enable_can_transceiver(4U, false); |
||||
switch (mode) { |
||||
case CAN_MODE_NORMAL: |
||||
case CAN_MODE_OBD_CAN2: |
||||
if ((bool)(mode == CAN_MODE_NORMAL) != (bool)(harness.status == HARNESS_STATUS_FLIPPED)) { |
||||
// B12,B13: disable OBD mode
|
||||
set_gpio_mode(GPIOB, 12, MODE_INPUT); |
||||
set_gpio_mode(GPIOB, 13, MODE_INPUT); |
||||
|
||||
// B5,B6: normal CAN2 mode
|
||||
set_gpio_alternate(GPIOB, 5, GPIO_AF9_CAN2); |
||||
set_gpio_alternate(GPIOB, 6, GPIO_AF9_CAN2); |
||||
uno_enable_can_transceiver(2U, true); |
||||
} else { |
||||
// B5,B6: disable normal CAN2 mode
|
||||
set_gpio_mode(GPIOB, 5, MODE_INPUT); |
||||
set_gpio_mode(GPIOB, 6, MODE_INPUT); |
||||
|
||||
// B12,B13: OBD mode
|
||||
set_gpio_alternate(GPIOB, 12, GPIO_AF9_CAN2); |
||||
set_gpio_alternate(GPIOB, 13, GPIO_AF9_CAN2); |
||||
uno_enable_can_transceiver(4U, true); |
||||
} |
||||
break; |
||||
default: |
||||
print("Tried to set unsupported CAN mode: "); puth(mode); print("\n"); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
static bool uno_check_ignition(void){ |
||||
// ignition is checked through harness
|
||||
return harness_check_ignition(); |
||||
} |
||||
|
||||
static void uno_set_usb_switch(bool phone){ |
||||
set_gpio_output(GPIOB, 3, phone); |
||||
} |
||||
|
||||
static void uno_set_ir_power(uint8_t percentage){ |
||||
pwm_set(TIM4, 2, percentage); |
||||
} |
||||
|
||||
static void uno_set_fan_enabled(bool enabled){ |
||||
set_gpio_output(GPIOA, 1, enabled); |
||||
} |
||||
|
||||
static void uno_init(void) { |
||||
common_init_gpio(); |
||||
|
||||
// A8,A15: normal CAN3 mode
|
||||
set_gpio_alternate(GPIOA, 8, GPIO_AF11_CAN3); |
||||
set_gpio_alternate(GPIOA, 15, GPIO_AF11_CAN3); |
||||
|
||||
// GPS off
|
||||
set_gpio_output(GPIOB, 1, 0); |
||||
set_gpio_output(GPIOC, 5, 0); |
||||
set_gpio_output(GPIOC, 12, 0); |
||||
|
||||
// C8: FAN PWM aka TIM3_CH3
|
||||
set_gpio_alternate(GPIOC, 8, GPIO_AF2_TIM3); |
||||
|
||||
// Turn on phone regulator
|
||||
set_gpio_output(GPIOB, 4, true); |
||||
|
||||
// Initialize IR PWM and set to 0%
|
||||
set_gpio_alternate(GPIOB, 7, GPIO_AF2_TIM4); |
||||
pwm_init(TIM4, 2); |
||||
uno_set_ir_power(0U); |
||||
|
||||
// Switch to phone usb mode if harness connection is powered by less than 7V
|
||||
if(white_read_voltage_mV() < 7000U){ |
||||
uno_set_usb_switch(true); |
||||
} else { |
||||
uno_set_usb_switch(false); |
||||
} |
||||
|
||||
// Bootkick phone
|
||||
uno_set_bootkick(BOOT_BOOTKICK); |
||||
} |
||||
|
||||
static void uno_init_bootloader(void) { |
||||
// GPS off
|
||||
set_gpio_output(GPIOB, 1, 0); |
||||
set_gpio_output(GPIOC, 5, 0); |
||||
set_gpio_output(GPIOC, 12, 0); |
||||
} |
||||
|
||||
static harness_configuration uno_harness_config = { |
||||
.has_harness = true, |
||||
.GPIO_SBU1 = GPIOC, |
||||
.GPIO_SBU2 = GPIOC, |
||||
.GPIO_relay_SBU1 = GPIOC, |
||||
.GPIO_relay_SBU2 = GPIOC, |
||||
.pin_SBU1 = 0, |
||||
.pin_SBU2 = 3, |
||||
.pin_relay_SBU1 = 10, |
||||
.pin_relay_SBU2 = 11, |
||||
.adc_channel_SBU1 = 10, |
||||
.adc_channel_SBU2 = 13 |
||||
}; |
||||
|
||||
board board_uno = { |
||||
.harness_config = &uno_harness_config, |
||||
.has_spi = false, |
||||
.has_canfd = false, |
||||
.fan_max_rpm = 5100U, |
||||
.fan_max_pwm = 100U, |
||||
.avdd_mV = 3300U, |
||||
.fan_stall_recovery = false, |
||||
.fan_enable_cooldown_time = 0U, |
||||
.init = uno_init, |
||||
.init_bootloader = uno_init_bootloader, |
||||
.enable_can_transceiver = uno_enable_can_transceiver, |
||||
.led_GPIO = {GPIOC, GPIOC, GPIOC}, |
||||
.led_pin = {9, 7, 6}, |
||||
.set_can_mode = uno_set_can_mode, |
||||
.check_ignition = uno_check_ignition, |
||||
.read_voltage_mV = white_read_voltage_mV, |
||||
.read_current_mA = unused_read_current, |
||||
.set_fan_enabled = uno_set_fan_enabled, |
||||
.set_ir_power = uno_set_ir_power, |
||||
.set_siren = unused_set_siren, |
||||
.set_bootkick = uno_set_bootkick, |
||||
.read_som_gpio = unused_read_som_gpio, |
||||
.set_amp_enabled = unused_set_amp_enabled |
||||
}; |
@ -1,45 +0,0 @@ |
||||
#!/usr/bin/env python3 |
||||
import os |
||||
import time |
||||
import random |
||||
import contextlib |
||||
|
||||
from panda import PandaJungle |
||||
from panda import Panda |
||||
|
||||
PANDA_UNDER_TEST = Panda.HW_TYPE_UNO |
||||
|
||||
panda_jungle = PandaJungle() |
||||
|
||||
def silent_panda_connect(): |
||||
with open(os.devnull, "w") as devnull: |
||||
with contextlib.redirect_stdout(devnull): |
||||
panda = Panda() |
||||
return panda |
||||
|
||||
def reboot_panda(harness_orientation=PandaJungle.HARNESS_ORIENTATION_NONE, ignition=False): |
||||
print(f"Restarting panda with harness orientation: {harness_orientation} and ignition: {ignition}") |
||||
panda_jungle.set_panda_power(False) |
||||
panda_jungle.set_harness_orientation(harness_orientation) |
||||
panda_jungle.set_ignition(ignition) |
||||
time.sleep(2) |
||||
panda_jungle.set_panda_power(True) |
||||
time.sleep(2) |
||||
|
||||
count = 0 |
||||
if __name__ == "__main__": |
||||
while True: |
||||
ignition = random.randint(0, 1) |
||||
harness_orientation = random.randint(0, 2) |
||||
reboot_panda(harness_orientation, ignition) |
||||
|
||||
p = silent_panda_connect() |
||||
assert p.get_type() == PANDA_UNDER_TEST |
||||
assert p.health()['car_harness_status'] == harness_orientation |
||||
if harness_orientation != PandaJungle.HARNESS_ORIENTATION_NONE: |
||||
assert p.health()['ignition_line'] == ignition |
||||
|
||||
count += 1 |
||||
print(f"Passed {count} loops") |
||||
|
||||
|
@ -1,36 +1,31 @@ |
||||
# openpilot releases |
||||
|
||||
``` |
||||
## release checklist |
||||
|
||||
**Go to `devel-staging`** |
||||
- [ ] update RELEASES.md |
||||
- [ ] update `devel-staging`: `git reset --hard origin/master-ci` |
||||
- [ ] open a pull request from `devel-staging` to `devel` |
||||
- [ ] post on Discord |
||||
|
||||
**Go to `devel`** |
||||
- [ ] update RELEASES.md |
||||
- [ ] close out milestone |
||||
- [ ] post on Discord dev channel |
||||
- [ ] bump version on master: `common/version.h` and `RELEASES.md` |
||||
- [ ] merge the pull request |
||||
|
||||
tests: |
||||
- [ ] before merging the pull request |
||||
- [ ] update from previous release -> new release |
||||
- [ ] update from new release -> previous release |
||||
- [ ] fresh install with `openpilot-test.comma.ai` |
||||
- [ ] drive on fresh install |
||||
- [ ] comma body test |
||||
- [ ] no submodules or LFS |
||||
- [ ] check sentry, MTBF, etc. |
||||
|
||||
**Go to `release3`** |
||||
- [ ] publish the blog post |
||||
- [ ] `git reset --hard origin/release3-staging` |
||||
- [ ] tag the release |
||||
``` |
||||
git tag v0.X.X <commit-hash> |
||||
git push origin v0.X.X |
||||
``` |
||||
- [ ] tag the release: `git tag v0.X.X <commit-hash> && git push origin v0.X.X` |
||||
- [ ] create GitHub release |
||||
- [ ] final test install on `openpilot.comma.ai` |
||||
- [ ] update production |
||||
- [ ] Post on Discord, X, etc. |
||||
- [ ] update factory provisioning |
||||
- [ ] close out milestone |
||||
- [ ] post on Discord, X, etc. |
||||
``` |
||||
|
@ -0,0 +1,25 @@ |
||||
from cereal import car |
||||
from openpilot.selfdrive.locationd.torqued import TorqueEstimator |
||||
|
||||
|
||||
def test_cal_percent(): |
||||
est = TorqueEstimator(car.CarParams()) |
||||
msg = est.get_msg() |
||||
assert msg.liveTorqueParameters.calPerc == 0 |
||||
|
||||
for (low, high), min_pts in zip(est.filtered_points.buckets.keys(), |
||||
est.filtered_points.buckets_min_points.values(), strict=True): |
||||
for _ in range(int(min_pts)): |
||||
est.filtered_points.add_point((low + high) / 2.0, 0.0) |
||||
|
||||
# enough bucket points, but not enough total points |
||||
msg = est.get_msg() |
||||
assert msg.liveTorqueParameters.calPerc == (len(est.filtered_points) / est.min_points_total * 100 + 100) / 2 |
||||
|
||||
# add enough points to bucket with most capacity |
||||
key = list(est.filtered_points.buckets)[0] |
||||
for _ in range(est.min_points_total - len(est.filtered_points)): |
||||
est.filtered_points.add_point((key[0] + key[1]) / 2.0, 0.0) |
||||
|
||||
msg = est.get_msg() |
||||
assert msg.liveTorqueParameters.calPerc == 100 |
Binary file not shown.
Binary file not shown.
@ -1,20 +0,0 @@ |
||||
#!/usr/bin/env bash |
||||
set -e |
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" |
||||
OP_ROOT="$DIR/../../" |
||||
|
||||
if [ -z "$BUILD" ]; then |
||||
docker pull ghcr.io/commaai/openpilot-base:latest |
||||
else |
||||
docker build --cache-from ghcr.io/commaai/openpilot-base:latest -t ghcr.io/commaai/openpilot-base:latest -f $OP_ROOT/Dockerfile.openpilot_base . |
||||
fi |
||||
|
||||
docker run \ |
||||
-it \ |
||||
--rm \ |
||||
--volume $OP_ROOT:$OP_ROOT \ |
||||
--workdir $PWD \ |
||||
--env PYTHONPATH=$OP_ROOT \ |
||||
ghcr.io/commaai/openpilot-base:latest \ |
||||
/bin/bash |
@ -1 +1 @@ |
||||
9e2fe2942fbf77f24bccdbef15893831f9c0b390 |
||||
f440c9e0469d32d350aa99ddaa8f44591a2ce690 |
@ -1,17 +1,214 @@ |
||||
import time |
||||
import pyray as rl |
||||
from openpilot.system.ui.lib.label import gui_text_box |
||||
from collections.abc import Callable |
||||
from enum import IntEnum |
||||
from openpilot.common.params import Params |
||||
from openpilot.selfdrive.ui.widgets.offroad_alerts import UpdateAlert, OffroadAlert |
||||
from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButton |
||||
from openpilot.selfdrive.ui.widgets.prime import PrimeWidget |
||||
from openpilot.selfdrive.ui.widgets.setup import SetupWidget |
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached |
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR |
||||
from openpilot.system.ui.lib.widget import Widget |
||||
|
||||
HEADER_HEIGHT = 80 |
||||
HEAD_BUTTON_FONT_SIZE = 40 |
||||
CONTENT_MARGIN = 40 |
||||
SPACING = 25 |
||||
RIGHT_COLUMN_WIDTH = 750 |
||||
REFRESH_INTERVAL = 10.0 |
||||
|
||||
class HomeLayout: |
||||
PRIME_BG_COLOR = rl.Color(51, 51, 51, 255) |
||||
|
||||
|
||||
class HomeLayoutState(IntEnum): |
||||
HOME = 0 |
||||
UPDATE = 1 |
||||
ALERTS = 2 |
||||
|
||||
|
||||
class HomeLayout(Widget): |
||||
def __init__(self): |
||||
pass |
||||
|
||||
def render(self, rect: rl.Rectangle): |
||||
gui_text_box( |
||||
rect, |
||||
"Demo Home Layout", |
||||
font_size=170, |
||||
color=rl.WHITE, |
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, |
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, |
||||
super().__init__() |
||||
self.params = Params() |
||||
|
||||
self.update_alert = UpdateAlert() |
||||
self.offroad_alert = OffroadAlert() |
||||
|
||||
self.current_state = HomeLayoutState.HOME |
||||
self.last_refresh = 0 |
||||
self.settings_callback: callable | None = None |
||||
|
||||
self.update_available = False |
||||
self.alert_count = 0 |
||||
|
||||
self.header_rect = rl.Rectangle(0, 0, 0, 0) |
||||
self.content_rect = rl.Rectangle(0, 0, 0, 0) |
||||
self.left_column_rect = rl.Rectangle(0, 0, 0, 0) |
||||
self.right_column_rect = rl.Rectangle(0, 0, 0, 0) |
||||
|
||||
self.update_notif_rect = rl.Rectangle(0, 0, 200, HEADER_HEIGHT - 10) |
||||
self.alert_notif_rect = rl.Rectangle(0, 0, 220, HEADER_HEIGHT - 10) |
||||
|
||||
self._prime_widget = PrimeWidget() |
||||
self._setup_widget = SetupWidget() |
||||
|
||||
self._exp_mode_button = ExperimentalModeButton() |
||||
self._setup_callbacks() |
||||
|
||||
def _setup_callbacks(self): |
||||
self.update_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME)) |
||||
self.offroad_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME)) |
||||
|
||||
def set_settings_callback(self, callback: Callable): |
||||
self.settings_callback = callback |
||||
|
||||
def _set_state(self, state: HomeLayoutState): |
||||
self.current_state = state |
||||
|
||||
def _render(self, rect: rl.Rectangle): |
||||
current_time = time.time() |
||||
if current_time - self.last_refresh >= REFRESH_INTERVAL: |
||||
self._refresh() |
||||
self.last_refresh = current_time |
||||
|
||||
self._handle_input() |
||||
self._render_header() |
||||
|
||||
# Render content based on current state |
||||
if self.current_state == HomeLayoutState.HOME: |
||||
self._render_home_content() |
||||
elif self.current_state == HomeLayoutState.UPDATE: |
||||
self._render_update_view() |
||||
elif self.current_state == HomeLayoutState.ALERTS: |
||||
self._render_alerts_view() |
||||
|
||||
def _update_layout_rects(self): |
||||
self.header_rect = rl.Rectangle( |
||||
self._rect.x + CONTENT_MARGIN, self._rect.y + CONTENT_MARGIN, self._rect.width - 2 * CONTENT_MARGIN, HEADER_HEIGHT |
||||
) |
||||
|
||||
content_y = self._rect.y + CONTENT_MARGIN + HEADER_HEIGHT + SPACING |
||||
content_height = self._rect.height - CONTENT_MARGIN - HEADER_HEIGHT - SPACING - CONTENT_MARGIN |
||||
|
||||
self.content_rect = rl.Rectangle( |
||||
self._rect.x + CONTENT_MARGIN, content_y, self._rect.width - 2 * CONTENT_MARGIN, content_height |
||||
) |
||||
|
||||
left_width = self.content_rect.width - RIGHT_COLUMN_WIDTH - SPACING |
||||
|
||||
self.left_column_rect = rl.Rectangle(self.content_rect.x, self.content_rect.y, left_width, self.content_rect.height) |
||||
|
||||
self.right_column_rect = rl.Rectangle( |
||||
self.content_rect.x + left_width + SPACING, self.content_rect.y, RIGHT_COLUMN_WIDTH, self.content_rect.height |
||||
) |
||||
|
||||
self.update_notif_rect.x = self.header_rect.x |
||||
self.update_notif_rect.y = self.header_rect.y + (self.header_rect.height - 60) // 2 |
||||
|
||||
notif_x = self.header_rect.x + (220 if self.update_available else 0) |
||||
self.alert_notif_rect.x = notif_x |
||||
self.alert_notif_rect.y = self.header_rect.y + (self.header_rect.height - 60) // 2 |
||||
|
||||
def _handle_input(self): |
||||
if not rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): |
||||
return |
||||
|
||||
mouse_pos = rl.get_mouse_position() |
||||
|
||||
if self.update_available and rl.check_collision_point_rec(mouse_pos, self.update_notif_rect): |
||||
self._set_state(HomeLayoutState.UPDATE) |
||||
return |
||||
|
||||
if self.alert_count > 0 and rl.check_collision_point_rec(mouse_pos, self.alert_notif_rect): |
||||
self._set_state(HomeLayoutState.ALERTS) |
||||
return |
||||
|
||||
# Content area input handling |
||||
if self.current_state == HomeLayoutState.UPDATE: |
||||
self.update_alert.handle_input(mouse_pos, True) |
||||
elif self.current_state == HomeLayoutState.ALERTS: |
||||
self.offroad_alert.handle_input(mouse_pos, True) |
||||
|
||||
def _render_header(self): |
||||
font = gui_app.font(FontWeight.MEDIUM) |
||||
|
||||
# Update notification button |
||||
if self.update_available: |
||||
# Highlight if currently viewing updates |
||||
highlight_color = rl.Color(255, 140, 40, 255) if self.current_state == HomeLayoutState.UPDATE else rl.Color(255, 102, 0, 255) |
||||
rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color) |
||||
|
||||
text = "UPDATE" |
||||
text_width = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE).x |
||||
text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_width) // 2 |
||||
text_y = self.update_notif_rect.y + (self.update_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2 |
||||
rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE) |
||||
|
||||
# Alert notification button |
||||
if self.alert_count > 0: |
||||
# Highlight if currently viewing alerts |
||||
highlight_color = rl.Color(255, 70, 70, 255) if self.current_state == HomeLayoutState.ALERTS else rl.Color(226, 44, 44, 255) |
||||
rl.draw_rectangle_rounded(self.alert_notif_rect, 0.3, 10, highlight_color) |
||||
|
||||
alert_text = f"{self.alert_count} ALERT{'S' if self.alert_count > 1 else ''}" |
||||
text_width = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE).x |
||||
text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_width) // 2 |
||||
text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2 |
||||
rl.draw_text_ex(font, alert_text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE) |
||||
|
||||
# Version text (right aligned) |
||||
version_text = self._get_version_text() |
||||
text_width = measure_text_cached(gui_app.font(FontWeight.NORMAL), version_text, 48).x |
||||
version_x = self.header_rect.x + self.header_rect.width - text_width |
||||
version_y = self.header_rect.y + (self.header_rect.height - 48) // 2 |
||||
rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), version_text, rl.Vector2(int(version_x), int(version_y)), 48, 0, DEFAULT_TEXT_COLOR) |
||||
|
||||
def _render_home_content(self): |
||||
self._render_left_column() |
||||
self._render_right_column() |
||||
|
||||
def _render_update_view(self): |
||||
self.update_alert.render(self.content_rect) |
||||
|
||||
def _render_alerts_view(self): |
||||
self.offroad_alert.render(self.content_rect) |
||||
|
||||
def _render_left_column(self): |
||||
self._prime_widget.render(self.left_column_rect) |
||||
|
||||
def _render_right_column(self): |
||||
exp_height = 125 |
||||
exp_rect = rl.Rectangle( |
||||
self.right_column_rect.x, self.right_column_rect.y, self.right_column_rect.width, exp_height |
||||
) |
||||
self._exp_mode_button.render(exp_rect) |
||||
|
||||
setup_rect = rl.Rectangle( |
||||
self.right_column_rect.x, |
||||
self.right_column_rect.y + exp_height + SPACING, |
||||
self.right_column_rect.width, |
||||
self.right_column_rect.height - exp_height - SPACING, |
||||
) |
||||
self._setup_widget.render(setup_rect) |
||||
|
||||
def _refresh(self): |
||||
# TODO: implement _update_state with a timer |
||||
self.update_available = self.update_alert.refresh() |
||||
self.alert_count = self.offroad_alert.refresh() |
||||
self._update_state_priority(self.update_available, self.alert_count > 0) |
||||
|
||||
def _update_state_priority(self, update_available: bool, alerts_present: bool): |
||||
current_state = self.current_state |
||||
|
||||
if not update_available and not alerts_present: |
||||
self.current_state = HomeLayoutState.HOME |
||||
elif update_available and (current_state == HomeLayoutState.HOME or (not alerts_present and current_state == HomeLayoutState.ALERTS)): |
||||
self.current_state = HomeLayoutState.UPDATE |
||||
elif alerts_present and (current_state == HomeLayoutState.HOME or (not update_available and current_state == HomeLayoutState.UPDATE)): |
||||
self.current_state = HomeLayoutState.ALERTS |
||||
|
||||
def _get_version_text(self) -> str: |
||||
brand = "openpilot" |
||||
description = self.params.get("UpdaterCurrentDescription", encoding='utf-8') |
||||
return f"{brand} {description}" if description else brand |
||||
|
@ -0,0 +1,17 @@ |
||||
import pyray as rl |
||||
from openpilot.system.ui.lib.widget import Widget |
||||
from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper |
||||
from openpilot.system.ui.widgets.network import WifiManagerUI |
||||
|
||||
|
||||
class NetworkLayout(Widget): |
||||
def __init__(self): |
||||
super().__init__() |
||||
self.wifi_manager = WifiManagerWrapper() |
||||
self.wifi_ui = WifiManagerUI(self.wifi_manager) |
||||
|
||||
def _render(self, rect: rl.Rectangle): |
||||
self.wifi_ui.render(rect) |
||||
|
||||
def shutdown(self): |
||||
self.wifi_manager.shutdown() |
@ -0,0 +1,175 @@ |
||||
import pyray as rl |
||||
import json |
||||
import time |
||||
import threading |
||||
|
||||
from openpilot.common.api import Api, api_get |
||||
from openpilot.common.params import Params |
||||
from openpilot.common.swaglog import cloudlog |
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight |
||||
from openpilot.system.ui.lib.wrap_text import wrap_text |
||||
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel |
||||
from openpilot.system.ui.lib.widget import Widget |
||||
from openpilot.selfdrive.ui.ui_state import ui_state |
||||
|
||||
|
||||
TITLE = "Firehose Mode" |
||||
DESCRIPTION = ( |
||||
"openpilot learns to drive by watching humans, like you, drive.\n\n" |
||||
+ "Firehose Mode allows you to maximize your training data uploads to improve " |
||||
+ "openpilot's driving models. More data means bigger models, which means better Experimental Mode." |
||||
) |
||||
INSTRUCTIONS = ( |
||||
"For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.\n\n" |
||||
+ "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n" |
||||
+ "Frequently Asked Questions\n\n" |
||||
+ "Does it matter how or where I drive? Nope, just drive as you normally would.\n\n" |
||||
+ "Do all of my segments get pulled in Firehose Mode? No, we selectively pull a subset of your segments.\n\n" |
||||
+ "What's a good USB-C adapter? Any fast phone or laptop charger should be fine.\n\n" |
||||
+ "Does it matter which software I run? Yes, only upstream openpilot (and particular forks) are able to be used for training." |
||||
) |
||||
|
||||
|
||||
class FirehoseLayout(Widget): |
||||
PARAM_KEY = "ApiCache_FirehoseStats" |
||||
GREEN = rl.Color(46, 204, 113, 255) |
||||
RED = rl.Color(231, 76, 60, 255) |
||||
GRAY = rl.Color(68, 68, 68, 255) |
||||
LIGHT_GRAY = rl.Color(228, 228, 228, 255) |
||||
UPDATE_INTERVAL = 30 # seconds |
||||
|
||||
def __init__(self): |
||||
super().__init__() |
||||
self.params = Params() |
||||
self.segment_count = self._get_segment_count() |
||||
self.scroll_panel = GuiScrollPanel() |
||||
|
||||
self.running = True |
||||
self.update_thread = threading.Thread(target=self._update_loop, daemon=True) |
||||
self.update_thread.start() |
||||
self.last_update_time = 0 |
||||
|
||||
def _get_segment_count(self) -> int: |
||||
stats = self.params.get(self.PARAM_KEY, encoding='utf8') |
||||
try: |
||||
return int(json.loads(stats).get("firehose", 0)) |
||||
except Exception: |
||||
cloudlog.exception(f"Failed to decode firehose stats: {stats}") |
||||
return 0 |
||||
|
||||
def __del__(self): |
||||
self.running = False |
||||
if self.update_thread and self.update_thread.is_alive(): |
||||
self.update_thread.join(timeout=1.0) |
||||
|
||||
def _render(self, rect: rl.Rectangle): |
||||
# Calculate content dimensions |
||||
content_width = rect.width - 80 |
||||
content_height = self._calculate_content_height(int(content_width)) |
||||
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height) |
||||
|
||||
# Handle scrolling and render with clipping |
||||
scroll_offset = self.scroll_panel.handle_scroll(rect, content_rect) |
||||
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) |
||||
self._render_content(rect, scroll_offset) |
||||
rl.end_scissor_mode() |
||||
|
||||
def _calculate_content_height(self, content_width: int) -> int: |
||||
height = 80 # Top margin |
||||
|
||||
# Title |
||||
height += 100 + 40 |
||||
|
||||
# Description |
||||
desc_font = gui_app.font(FontWeight.NORMAL) |
||||
desc_lines = wrap_text(desc_font, DESCRIPTION, 45, content_width) |
||||
height += len(desc_lines) * 45 + 40 |
||||
|
||||
# Status section |
||||
height += 32 # Separator |
||||
status_text, _ = self._get_status() |
||||
status_lines = wrap_text(gui_app.font(FontWeight.BOLD), status_text, 60, content_width) |
||||
height += len(status_lines) * 60 + 20 |
||||
|
||||
# Contribution count (if available) |
||||
if self.segment_count > 0: |
||||
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far." |
||||
contrib_lines = wrap_text(gui_app.font(FontWeight.BOLD), contrib_text, 52, content_width) |
||||
height += len(contrib_lines) * 52 + 20 |
||||
|
||||
# Instructions section |
||||
height += 32 # Separator |
||||
inst_lines = wrap_text(gui_app.font(FontWeight.NORMAL), INSTRUCTIONS, 40, content_width) |
||||
height += len(inst_lines) * 40 + 40 # Bottom margin |
||||
|
||||
return height |
||||
|
||||
def _render_content(self, rect: rl.Rectangle, scroll_offset: rl.Vector2): |
||||
x = int(rect.x + 40) |
||||
y = int(rect.y + 40 + scroll_offset.y) |
||||
w = int(rect.width - 80) |
||||
|
||||
# Title |
||||
title_font = gui_app.font(FontWeight.MEDIUM) |
||||
rl.draw_text_ex(title_font, TITLE, rl.Vector2(x, y), 100, 0, rl.WHITE) |
||||
y += 140 |
||||
|
||||
# Description |
||||
y = self._draw_wrapped_text(x, y, w, DESCRIPTION, gui_app.font(FontWeight.NORMAL), 45, rl.WHITE) |
||||
y += 40 |
||||
|
||||
# Separator |
||||
rl.draw_rectangle(x, y, w, 2, self.GRAY) |
||||
y += 30 |
||||
|
||||
# Status |
||||
status_text, status_color = self._get_status() |
||||
y = self._draw_wrapped_text(x, y, w, status_text, gui_app.font(FontWeight.BOLD), 60, status_color) |
||||
y += 20 |
||||
|
||||
# Contribution count (if available) |
||||
if self.segment_count > 0: |
||||
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far." |
||||
y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE) |
||||
y += 20 |
||||
|
||||
# Separator |
||||
rl.draw_rectangle(x, y, w, 2, self.GRAY) |
||||
y += 30 |
||||
|
||||
# Instructions |
||||
self._draw_wrapped_text(x, y, w, INSTRUCTIONS, gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY) |
||||
|
||||
def _draw_wrapped_text(self, x, y, width, text, font, size, color): |
||||
wrapped = wrap_text(font, text, size, width) |
||||
for line in wrapped: |
||||
rl.draw_text_ex(font, line, rl.Vector2(x, y), size, 0, color) |
||||
y += size |
||||
return y |
||||
|
||||
def _get_status(self) -> tuple[str, rl.Color]: |
||||
network_type = ui_state.sm["deviceState"].networkType |
||||
network_metered = ui_state.sm["deviceState"].networkMetered |
||||
|
||||
if not network_metered and network_type != 0: # Not metered and connected |
||||
return "ACTIVE", self.GREEN |
||||
else: |
||||
return "INACTIVE: connect to an unmetered network", self.RED |
||||
|
||||
def _fetch_firehose_stats(self): |
||||
try: |
||||
dongle_id = self.params.get("DongleId", encoding='utf8') or "" |
||||
identity_token = Api(dongle_id).get_token() |
||||
response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token) |
||||
if response.status_code == 200: |
||||
data = response.json() |
||||
self.segment_count = data.get("firehose", 0) |
||||
self.params.put(self.PARAM_KEY, json.dumps(data)) |
||||
except Exception as e: |
||||
cloudlog.error(f"Failed to fetch firehose stats: {e}") |
||||
|
||||
def _update_loop(self): |
||||
while self.running: |
||||
if not ui_state.started: |
||||
self._fetch_firehose_stats() |
||||
time.sleep(self.UPDATE_INTERVAL) |
@ -0,0 +1,98 @@ |
||||
from enum import IntEnum |
||||
import os |
||||
import threading |
||||
import time |
||||
|
||||
from openpilot.common.api import Api, api_get |
||||
from openpilot.common.params import Params |
||||
from openpilot.common.swaglog import cloudlog |
||||
|
||||
|
||||
class PrimeType(IntEnum): |
||||
UNKNOWN = -2, |
||||
UNPAIRED = -1, |
||||
NONE = 0, |
||||
MAGENTA = 1, |
||||
LITE = 2, |
||||
BLUE = 3, |
||||
MAGENTA_NEW = 4, |
||||
PURPLE = 5, |
||||
|
||||
|
||||
class PrimeState: |
||||
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 |
||||
|
||||
def __init__(self): |
||||
self._params = Params() |
||||
self._lock = threading.Lock() |
||||
self.prime_type: PrimeType = self._load_initial_state() |
||||
|
||||
self._running = False |
||||
self._thread = None |
||||
self.start() |
||||
|
||||
def _load_initial_state(self) -> PrimeType: |
||||
prime_type_str = os.getenv("PRIME_TYPE") or self._params.get("PrimeType", encoding='utf8') |
||||
try: |
||||
if prime_type_str is not None: |
||||
return PrimeType(int(prime_type_str)) |
||||
except (ValueError, TypeError): |
||||
pass |
||||
return PrimeType.UNKNOWN |
||||
|
||||
def _fetch_prime_status(self) -> None: |
||||
dongle_id = self._params.get("DongleId", encoding='utf8') |
||||
if not dongle_id: |
||||
return |
||||
|
||||
try: |
||||
identity_token = Api(dongle_id).get_token() |
||||
response = api_get(f"v1.1/devices/{dongle_id}", timeout=self.API_TIMEOUT, access_token=identity_token) |
||||
if response.status_code == 200: |
||||
data = response.json() |
||||
is_paired = data.get("is_paired", False) |
||||
prime_type = data.get("prime_type", 0) |
||||
self.set_type(PrimeType(prime_type) if is_paired else PrimeType.UNPAIRED) |
||||
except Exception as e: |
||||
cloudlog.error(f"Failed to fetch prime status: {e}") |
||||
|
||||
def set_type(self, prime_type: PrimeType) -> None: |
||||
with self._lock: |
||||
if prime_type != self.prime_type: |
||||
self.prime_type = prime_type |
||||
self._params.put("PrimeType", str(int(prime_type))) |
||||
cloudlog.info(f"Prime type updated to {prime_type}") |
||||
|
||||
def _worker_thread(self) -> None: |
||||
while self._running: |
||||
self._fetch_prime_status() |
||||
|
||||
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_type(self) -> PrimeType: |
||||
with self._lock: |
||||
return self.prime_type |
||||
|
||||
def is_prime(self) -> bool: |
||||
with self._lock: |
||||
return bool(self.prime_type > PrimeType.NONE) |
||||
|
||||
def __del__(self): |
||||
self.stop() |
@ -0,0 +1,78 @@ |
||||
import time |
||||
import pyray as rl |
||||
from openpilot.selfdrive.ui.ui_state import ui_state |
||||
from openpilot.system.ui.lib.application import gui_app |
||||
from openpilot.system.ui.lib.widget import Widget |
||||
from openpilot.common.params import Params |
||||
|
||||
|
||||
class ExpButton(Widget): |
||||
def __init__(self, button_size: int, icon_size: int): |
||||
super().__init__() |
||||
self._params = Params() |
||||
self._experimental_mode: bool = False |
||||
self._engageable: bool = False |
||||
|
||||
# State hold mechanism |
||||
self._hold_duration = 2.0 # seconds |
||||
self._held_mode: bool | None = None |
||||
self._hold_end_time: float | None = None |
||||
|
||||
self._white_color: rl.Color = rl.Color(255, 255, 255, 255) |
||||
self._black_bg: rl.Color = rl.Color(0, 0, 0, 166) |
||||
self._txt_wheel: rl.Texture = gui_app.texture('icons/chffr_wheel.png', icon_size, icon_size) |
||||
self._txt_exp: rl.Texture = gui_app.texture('icons/experimental.png', icon_size, icon_size) |
||||
self._rect = rl.Rectangle(0, 0, button_size, button_size) |
||||
|
||||
def set_rect(self, rect: rl.Rectangle) -> None: |
||||
self._rect.x, self._rect.y = rect.x, rect.y |
||||
|
||||
def _update_state(self) -> None: |
||||
selfdrive_state = ui_state.sm["selfdriveState"] |
||||
self._experimental_mode = selfdrive_state.experimentalMode |
||||
self._engageable = selfdrive_state.engageable or selfdrive_state.enabled |
||||
|
||||
def handle_mouse_event(self) -> bool: |
||||
if rl.check_collision_point_rec(rl.get_mouse_position(), self._rect): |
||||
if (rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and |
||||
self._is_toggle_allowed()): |
||||
new_mode = not self._experimental_mode |
||||
self._params.put_bool("ExperimentalMode", new_mode) |
||||
|
||||
# Hold new state temporarily |
||||
self._held_mode = new_mode |
||||
self._hold_end_time = time.time() + self._hold_duration |
||||
return True |
||||
return False |
||||
|
||||
def _render(self, rect: rl.Rectangle) -> None: |
||||
center_x = int(self._rect.x + self._rect.width // 2) |
||||
center_y = int(self._rect.y + self._rect.height // 2) |
||||
|
||||
mouse_over = rl.check_collision_point_rec(rl.get_mouse_position(), self._rect) |
||||
mouse_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._is_pressed |
||||
self._white_color.a = 180 if (mouse_down and mouse_over) or not self._engageable else 255 |
||||
|
||||
texture = self._txt_exp if self._held_or_actual_mode() else self._txt_wheel |
||||
rl.draw_circle(center_x, center_y, self._rect.width / 2, self._black_bg) |
||||
rl.draw_texture(texture, center_x - texture.width // 2, center_y - texture.height // 2, self._white_color) |
||||
|
||||
def _held_or_actual_mode(self): |
||||
now = time.time() |
||||
if self._hold_end_time and now < self._hold_end_time: |
||||
return self._held_mode |
||||
|
||||
if self._hold_end_time and now >= self._hold_end_time: |
||||
self._hold_end_time = self._held_mode = None |
||||
|
||||
return self._experimental_mode |
||||
|
||||
def _is_toggle_allowed(self): |
||||
if not self._params.get_bool("ExperimentalModeConfirmed"): |
||||
return False |
||||
|
||||
car_params = ui_state.sm["carParams"] |
||||
if car_params.alphaLongitudinalAvailable: |
||||
return self._params.get_bool("AlphaLongitudinalEnabled") |
||||
else: |
||||
return car_params.openpilotLongitudinalControl |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue