parent
100f89a161
commit
a93a862616
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 |
# openpilot releases |
||||||
|
|
||||||
|
``` |
||||||
## release checklist |
## release checklist |
||||||
|
|
||||||
**Go to `devel-staging`** |
**Go to `devel-staging`** |
||||||
|
- [ ] update RELEASES.md |
||||||
- [ ] update `devel-staging`: `git reset --hard origin/master-ci` |
- [ ] update `devel-staging`: `git reset --hard origin/master-ci` |
||||||
- [ ] open a pull request from `devel-staging` to `devel` |
- [ ] open a pull request from `devel-staging` to `devel` |
||||||
|
- [ ] post on Discord |
||||||
|
|
||||||
**Go to `devel`** |
**Go to `devel`** |
||||||
- [ ] update RELEASES.md |
|
||||||
- [ ] close out milestone |
|
||||||
- [ ] post on Discord dev channel |
|
||||||
- [ ] bump version on master: `common/version.h` and `RELEASES.md` |
- [ ] bump version on master: `common/version.h` and `RELEASES.md` |
||||||
- [ ] merge the pull request |
- [ ] before merging the pull request |
||||||
|
- [ ] update from previous release -> new release |
||||||
tests: |
- [ ] update from new release -> previous release |
||||||
- [ ] update from previous release -> new release |
- [ ] fresh install with `openpilot-test.comma.ai` |
||||||
- [ ] update from new release -> previous release |
- [ ] drive on fresh install |
||||||
- [ ] fresh install with `openpilot-test.comma.ai` |
- [ ] no submodules or LFS |
||||||
- [ ] drive on fresh install |
- [ ] check sentry, MTBF, etc. |
||||||
- [ ] comma body test |
|
||||||
- [ ] no submodules or LFS |
|
||||||
- [ ] check sentry, MTBF, etc. |
|
||||||
|
|
||||||
**Go to `release3`** |
**Go to `release3`** |
||||||
- [ ] publish the blog post |
- [ ] publish the blog post |
||||||
- [ ] `git reset --hard origin/release3-staging` |
- [ ] `git reset --hard origin/release3-staging` |
||||||
- [ ] tag the release |
- [ ] tag the release: `git tag v0.X.X <commit-hash> && git push origin v0.X.X` |
||||||
``` |
|
||||||
git tag v0.X.X <commit-hash> |
|
||||||
git push origin v0.X.X |
|
||||||
``` |
|
||||||
- [ ] create GitHub release |
- [ ] create GitHub release |
||||||
- [ ] final test install on `openpilot.comma.ai` |
- [ ] final test install on `openpilot.comma.ai` |
||||||
- [ ] update production |
- [ ] update factory provisioning |
||||||
- [ ] Post on Discord, X, etc. |
- [ ] 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 |
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): |
def __init__(self): |
||||||
pass |
super().__init__() |
||||||
|
self.params = Params() |
||||||
def render(self, rect: rl.Rectangle): |
|
||||||
gui_text_box( |
self.update_alert = UpdateAlert() |
||||||
rect, |
self.offroad_alert = OffroadAlert() |
||||||
"Demo Home Layout", |
|
||||||
font_size=170, |
self.current_state = HomeLayoutState.HOME |
||||||
color=rl.WHITE, |
self.last_refresh = 0 |
||||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, |
self.settings_callback: callable | None = None |
||||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, |
|
||||||
|
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