mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-09-03 01:43:43 +08:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d0ab77cf2e | |||
| 47db84ebfb | |||
| 68be777395 | |||
| ab389498a8 | |||
| 6249f4d5b0 | |||
| 8b88f7dd6e | |||
| 79658800ce | |||
| 36561258fa | |||
| cb85ac1f0e | |||
| c9f1602040 | |||
| 06af2abe67 | |||
| 7d5596d5c3 | |||
| a2e422eee0 | |||
| 51987a62d0 | |||
| e10c0fd960 | |||
| 98ed8111f6 | |||
| da8ce858ec | |||
| 9fa7ef3d17 | |||
| 4adbb85742 | |||
| 70df7f227b | |||
| 0e32059484 | |||
| 7cf55c3b7a | |||
| 682b6a20df | |||
| a67cdf9a51 | |||
| e571e21d14 | |||
| 839d3f5004 | |||
| 5645370f84 | |||
| 633d17cd12 | |||
| 5419f57b3a | |||
| 46f612224c | |||
| 6e0f4f4630 | |||
| 0f9c753e6e | |||
| 131e473f37 | |||
| 30f358eb59 | |||
| 9b9e3ea604 | |||
| 7cc48b5bc9 | |||
| cbf750de20 | |||
| 318257fa3b | |||
| 4cdc16031f | |||
| 31ea1850f7 |
@@ -121,7 +121,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
build_model:
|
||||
runs-on: [self-hosted, chestnut]
|
||||
runs-on: [self-hosted, "${{ inputs.target_hardware == 'chestnut' && 'chestnut' || 'tici' }}"]
|
||||
needs: get_model
|
||||
env:
|
||||
MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }})
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1
|
||||
export QCOM_PRIORITY=12
|
||||
|
||||
if [ -z "$AGNOS_VERSION" ]; then
|
||||
export AGNOS_VERSION="19.6"
|
||||
export AGNOS_VERSION="19.7"
|
||||
fi
|
||||
|
||||
export STAGING_ROOT="/data/safe_staging"
|
||||
|
||||
+1
-1
Submodule opendbc_repo updated: 06743dfb39...f95f996f59
@@ -725,6 +725,7 @@ struct ChestnutState {
|
||||
pcieLtssm @7 :UInt8;
|
||||
supplyVoltage @8 :UInt16; # mV
|
||||
supplyCurrent @9 :Int16; # mA
|
||||
supplyFault @10 :Bool;
|
||||
}
|
||||
|
||||
struct RadarState @0x9a185389d6fdd05f {
|
||||
@@ -1004,6 +1005,7 @@ struct DrivingModelData {
|
||||
frameIdExtra @1 :UInt32;
|
||||
frameDropPerc @6 :Float32;
|
||||
modelExecutionTime @7 :Float32;
|
||||
big @8 :Bool;
|
||||
|
||||
action @2 :ModelDataV2.Action;
|
||||
|
||||
|
||||
@@ -56,29 +56,29 @@
|
||||
},
|
||||
{
|
||||
"name": "boot",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd.img.xz",
|
||||
"hash": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
|
||||
"hash_raw": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d.img.xz",
|
||||
"hash": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d",
|
||||
"hash_raw": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d",
|
||||
"size": 46897152,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "6650e4c46df99ae6dfd6ee895a34b8a2a3cc490a8ce18e16cc3c451c3f822b6e"
|
||||
"ondevice_hash": "d12e1e5b9455b62a1464558716493b33e470d7a7e88da1c4105a3b21d0961808"
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img.xz",
|
||||
"hash": "b134fd04e9da27fa1d359ea0f2742c216fa21a08b5c47e9be22ab3b0563d9b9b",
|
||||
"hash_raw": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img.xz",
|
||||
"hash": "74ffc9c551e1f29cda897ace8a69080fe644f8039977c6885f2b48362e39b744",
|
||||
"hash_raw": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f",
|
||||
"size": 4718592000,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "91242772af771ae96fe2eebc105f2b80a7e1dbaaf6003c2574b62d51b806f468",
|
||||
"ondevice_hash": "6a992680183685eea9db99d915219a37935f45989330d9b619e880450257f448",
|
||||
"alt": {
|
||||
"hash": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img",
|
||||
"hash": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img",
|
||||
"size": 4718592000
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
import os
|
||||
import select
|
||||
import signal
|
||||
import string
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -354,7 +355,7 @@ class Modem:
|
||||
imei = ""
|
||||
|
||||
iccid = (self._atv("AT+QCCID", "+QCCID:") or "").rstrip("F")
|
||||
if not iccid.isdigit():
|
||||
if not all(c in string.hexdigits for c in iccid):
|
||||
iccid = ""
|
||||
|
||||
imsi = first_line("AT+CIMI")
|
||||
|
||||
@@ -4,11 +4,17 @@ from pathlib import Path
|
||||
CHESTNUT_FW_VERSION = "ed4e39b7"
|
||||
CHESTNUT_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001))
|
||||
CHESTNUT_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463))
|
||||
CHESTNUT_USB_PRODUCT = f"custom {CHESTNUT_FW_VERSION}-CLEAN"
|
||||
USB_DEVICES_PATH = Path("/sys/bus/usb/devices")
|
||||
TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation")
|
||||
PRIMARY_USB_CONTROLLER = "a600000.ssusb"
|
||||
|
||||
|
||||
def is_chestnut_usb_id(vendor_id: int, product_id: int, include_bootloader: bool = False) -> bool:
|
||||
ids = CHESTNUT_USB_IDS + CHESTNUT_ROM_USB_IDS if include_bootloader else CHESTNUT_USB_IDS
|
||||
return (vendor_id, product_id) in ids
|
||||
|
||||
|
||||
def get_usb_topology() -> set[str]:
|
||||
try:
|
||||
return set(os.listdir(USB_DEVICES_PATH))
|
||||
@@ -81,7 +87,7 @@ def set_usb_state(device_state, devices: list[dict]) -> None:
|
||||
entry.linkErrorCount = device["linkErrorCount"]
|
||||
entry.usb3Lane = device.get("usb3Lane", "unknown")
|
||||
|
||||
if (entry.vendorId, entry.productId) in CHESTNUT_USB_IDS:
|
||||
if is_chestnut_usb_id(entry.vendorId, entry.productId):
|
||||
chestnut_present = True
|
||||
|
||||
device_state.chestnutPresent = chestnut_present
|
||||
|
||||
@@ -92,6 +92,12 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
|
||||
{"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||
{"Offroad_ChestnutBranch", {CLEAR_ON_MANAGER_START, JSON}},
|
||||
{"Offroad_ChestnutNotDetected", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||
{"Offroad_ChestnutOverheated", {CLEAR_ON_MANAGER_START, JSON}},
|
||||
{"Offroad_ChestnutPcieUnavailable", {CLEAR_ON_MANAGER_START, JSON}},
|
||||
{"Offroad_ChestnutUncompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||
{"Offroad_ChestnutUpdateFailed", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||
{"Offroad_ChestnutUsbSlow", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||
{"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}},
|
||||
{"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}},
|
||||
{"Offroad_ExcessiveActuation", {PERSISTENT, JSON}},
|
||||
@@ -132,6 +138,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
|
||||
{"ChestnutActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
|
||||
{"ChestnutLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
|
||||
{"ChestnutModelError", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
|
||||
{"Version", {PERSISTENT, STRING}},
|
||||
|
||||
// --- sunnypilot params --- //
|
||||
|
||||
@@ -27,14 +27,14 @@ public:
|
||||
auto param_path = Params().getParamPath();
|
||||
if (util::file_exists(param_path)) {
|
||||
std::string real_path = util::readlink(param_path);
|
||||
util::check_system(util::string_format("rm %s -rf", real_path.c_str()));
|
||||
util::check_system(util::string_format("rm -rf %s", real_path.c_str()));
|
||||
unlink(param_path.c_str());
|
||||
}
|
||||
if (getenv("COMMA_CACHE") == nullptr) {
|
||||
util::check_system(util::string_format("rm %s -rf", Path::download_cache_root().c_str()));
|
||||
util::check_system(util::string_format("rm -rf %s", Path::download_cache_root().c_str()));
|
||||
}
|
||||
util::check_system(util::string_format("rm %s -rf", Path::comma_home().c_str()));
|
||||
util::check_system(util::string_format("rm %s -rf", msgq_path.c_str()));
|
||||
util::check_system(util::string_format("rm -rf %s", Path::comma_home().c_str()));
|
||||
util::check_system(util::string_format("rm -rf %s", msgq_path.c_str()));
|
||||
unsetenv("OPENPILOT_PREFIX");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:07bda2fe5d6be0b2854044053c384fe002e96406da119863a443b9344258b500
|
||||
size 1544
|
||||
@@ -10,11 +10,6 @@ from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, chestnut_present, modeld_pkl_path
|
||||
|
||||
|
||||
CAMERA_CONFIGS = [
|
||||
(_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208
|
||||
(_os_fisheye.width, _os_fisheye.height), # mici: 1344x760
|
||||
]
|
||||
|
||||
Import('env', 'arch')
|
||||
chunker_file = File("#openpilot/common/file_chunker.py")
|
||||
lenv = env.Clone()
|
||||
@@ -24,20 +19,22 @@ tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "
|
||||
if 'pycache' not in x and os.path.isfile(os.path.join(tinygrad_root, x))]
|
||||
|
||||
def estimate_pickle_max_size(onnx_size):
|
||||
return 1.2 * onnx_size + 10 * 1024 * 1024 # 20% + 10MB is plenty
|
||||
# QCOM programs for models with spatial recurrent features can approach 2x
|
||||
# the ONNX size. Overestimating only adds an empty trailing chunk.
|
||||
return 2.0 * onnx_size + 10 * 1024 * 1024
|
||||
|
||||
if arch == 'comma_arm64':
|
||||
from openpilot.common.hardware import HARDWARE
|
||||
camera = _os_fisheye if HARDWARE.get_device_type() == "mici" else _ar_ox_fisheye
|
||||
camera_configs = [(camera.width, camera.height)]
|
||||
tg_backend = 'QCOM'
|
||||
tg_flags = f'DEV={tg_backend} IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1'
|
||||
else:
|
||||
camera_configs = [(c.width, c.height) for c in (_ar_ox_fisheye, _os_fisheye)]
|
||||
tg_backend = 'CPU'
|
||||
tg_flags = f'DEV=CPU' if arch == 'Darwin' else 'DEV=CPU:LLVM'
|
||||
|
||||
tg_devices = { # which device to put jit inputs to at runtime
|
||||
'openpilot.selfdrive.modeld.modeld': {
|
||||
'default': {'WARP_DEV': tg_backend, 'QUEUE_DEV': tg_backend},
|
||||
'chestnut': {'WARP_DEV': tg_backend, 'QUEUE_DEV': 'AMD'}
|
||||
},
|
||||
'openpilot.selfdrive.modeld.dmonitoringmodeld': {
|
||||
'default': {'DEV': tg_backend}
|
||||
},
|
||||
@@ -45,7 +42,7 @@ tg_devices = { # which device to put jit inputs to at runtime
|
||||
|
||||
CHESTNUT = chestnut_present()
|
||||
if CHESTNUT:
|
||||
chestnut_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2'
|
||||
chestnut_tg_flags = 'DEBUG=1 DEV=USB+AMD:LLVM FRAME_DEV=CPU FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2 TC_OCCUPANCY_OPT=1'
|
||||
# the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it
|
||||
chestnut_lock = File("models/.chestnut.lock").abspath
|
||||
|
||||
@@ -76,10 +73,9 @@ frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
||||
if not os.getenv('SKIP_TINYGRAD_COMPILE'):
|
||||
for chestnut in [False, True] if CHESTNUT else [False]:
|
||||
target_pkl_path = File(modeld_pkl_path(chestnut)).abspath
|
||||
# BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a chestnut
|
||||
file_prefix, cmd_flags = ('big_', chestnut_tg_flags) if chestnut else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags)
|
||||
file_prefix, cmd_flags = ('big_', chestnut_tg_flags) if chestnut else ('', tg_flags)
|
||||
driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath)
|
||||
camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS)
|
||||
camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in camera_configs)
|
||||
# CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it.
|
||||
taskset = 'taskset -c 7 ' if arch == 'comma_arm64' else ''
|
||||
cmd = (f'{cmd_flags} {mac_brew_string} {taskset}python3 {modeld_dir}/compile_modeld.py '
|
||||
@@ -107,7 +103,7 @@ if not os.getenv('SKIP_TINYGRAD_COMPILE'):
|
||||
actions = Action(do_compile, " [CHESTNUT] $TARGET") if chestnut else [cmd, Action(do_chunk, " [CHUNK] $TARGET")]
|
||||
node = lenv.Command(
|
||||
chunk_targets,
|
||||
tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(chunk_targets), chunker_file],
|
||||
tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(camera_res_args), Value(chunk_targets), chunker_file],
|
||||
actions,
|
||||
)
|
||||
if chestnut:
|
||||
@@ -121,7 +117,7 @@ lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files + script_file
|
||||
|
||||
dm_w, dm_h = DM_INPUT_SIZE
|
||||
compile_dm_warp_script = [File(f"{modeld_dir}/compile_dm_warp.py")]
|
||||
for cam_w, cam_h in CAMERA_CONFIGS:
|
||||
for cam_w, cam_h in camera_configs:
|
||||
dm_pkl_path = File(f"models/dm_warp_{cam_w}x{cam_h}_tinygrad.pkl").abspath
|
||||
cmd = (f'{tg_flags} {mac_brew_string} python3 {modeld_dir}/compile_dm_warp.py '
|
||||
f'--camera-resolution {cam_w}x{cam_h} --warp-to {dm_w}x{dm_h} '
|
||||
@@ -143,5 +139,4 @@ def tg_compile(flags, model_name):
|
||||
Action(do_chunk, " [CHUNK] $TARGET")],
|
||||
)
|
||||
|
||||
if not os.getenv('SKIP_TINYGRAD_COMPILE'):
|
||||
tg_compile(tg_flags, 'dmonitoring_model')
|
||||
tg_compile(tg_flags, 'dmonitoring_model')
|
||||
|
||||
@@ -37,17 +37,12 @@ from tinygrad.engine.jit import TinyJit
|
||||
|
||||
|
||||
NV12Frame = namedtuple("NV12Frame", ['width', 'height', 'stride', 'y_height', 'uv_height', 'size'])
|
||||
WARP_INPUTS = ['tfm', 'big_tfm']
|
||||
POLICY_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs']
|
||||
|
||||
UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32)
|
||||
UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX)
|
||||
|
||||
WARP_DEV = os.getenv('WARP_DEV')
|
||||
MODELD_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs']
|
||||
|
||||
|
||||
def make_random_images(keys, shape, device=None):
|
||||
return {k: Tensor.randint(shape, low=0, high=256, dtype='uint8', device=device).realize() for k in keys}
|
||||
def nv12_copy_size(stride: int, y_height: int, uv_height: int) -> int:
|
||||
# Retain the padded Y and UV plane storage, but skip the trailing kernel/guard allocation.
|
||||
return stride * (y_height + uv_height)
|
||||
|
||||
|
||||
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad, border_fill_val=None):
|
||||
@@ -99,7 +94,7 @@ def make_frame_prepare(nv12: NV12Frame, model_w, model_h):
|
||||
|
||||
def frame_prepare_tinygrad(input_frame, M_inv):
|
||||
# UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling
|
||||
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=WARP_DEV)
|
||||
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=Device.DEFAULT)
|
||||
# deinterleave NV12 UV plane (UVUV... -> separate U, V)
|
||||
uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride)
|
||||
with Context(SPLIT_REDUCEOP=0):
|
||||
@@ -118,49 +113,43 @@ def make_frame_prepare(nv12: NV12Frame, model_w, model_h):
|
||||
return frame_prepare_tinygrad
|
||||
|
||||
|
||||
def make_warp_input_queues(vision_input_shapes, frame_skip, device):
|
||||
img = vision_input_shapes['img'] # (1, 12, 128, 256)
|
||||
n_frames = img[1] // 6
|
||||
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
|
||||
|
||||
npy = {
|
||||
'tfm': np.zeros((3, 3), dtype=np.float32),
|
||||
'big_tfm': np.zeros((3, 3), dtype=np.float32),
|
||||
}
|
||||
input_queues = {
|
||||
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
**{k: Tensor(v, device='NPY').realize() for k, v in npy.items()},
|
||||
}
|
||||
return input_queues, npy
|
||||
|
||||
|
||||
def get_policy_npy_shapes(input_shapes):
|
||||
dp = input_shapes['desire_pulse'] # (1, 25, 8)
|
||||
tc = input_shapes['traffic_convention'] # (1, 2)
|
||||
at = input_shapes['action_t'] # (1, 2)
|
||||
fb = input_shapes['features_buffer'] # (1, 24, 512)
|
||||
fb = input_shapes['features_buffer'] # (1, T-1, ...) e.g. (1, 24, 32, 512) with spatial features
|
||||
feat_dim = math.prod(fb[2:])
|
||||
# TODO prev_feat shouldn't exist and be handled inside the JIT, but corrupt on QCOM for now
|
||||
shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], fb[2])}
|
||||
shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], feat_dim)}
|
||||
return shapes, [math.prod(s) for s in shapes.values()]
|
||||
|
||||
|
||||
def make_input_queues(input_shapes, frame_skip, device):
|
||||
input_queues, npy = make_warp_input_queues(input_shapes, frame_skip, device)
|
||||
|
||||
fb = input_shapes['features_buffer'] # (1, 24, 512), past features only; the model appends the current frame's feature
|
||||
def make_input_queues(input_shapes, frame_skip, device, frame_copy_size):
|
||||
img = input_shapes['img'] # (1, 12, 128, 256)
|
||||
fb = input_shapes['features_buffer'] # (1, T-1, ...), past features only; the model appends the current frame's feature
|
||||
feat_dim = math.prod(fb[2:])
|
||||
dp = input_shapes['desire_pulse'] # (1, 25, 8)
|
||||
n_frames = img[1] // 6
|
||||
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
|
||||
|
||||
shapes, sizes = get_policy_npy_shapes(input_shapes)
|
||||
packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32)
|
||||
policy_shapes, _ = get_policy_npy_shapes(input_shapes)
|
||||
shapes = {'tfm': (3, 3), 'big_tfm': (3, 3)} | policy_shapes
|
||||
sizes = [math.prod(s) for s in shapes.values()]
|
||||
packed_npy_size = sum(sizes) * np.dtype(np.float32).itemsize
|
||||
packed_input = np.zeros(packed_npy_size + 2 * frame_copy_size, dtype=np.uint8)
|
||||
packed_npy_inputs = packed_input[:packed_npy_size].view(np.float32)
|
||||
frames = packed_input[packed_npy_size:]
|
||||
frame_views = {'img': frames[:frame_copy_size], 'big_img': frames[frame_copy_size:]}
|
||||
# views into the packed inputs, to be refilled at runtime
|
||||
npy.update({k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)})
|
||||
input_queues.update({
|
||||
'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], fb[2]), dtype=np.float32), device=device).contiguous().realize(),
|
||||
npy = {k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)}
|
||||
input_queues = {
|
||||
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], feat_dim), dtype=np.float32), device=device).contiguous().realize(),
|
||||
'desire_q': Tensor(np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32), device=device).contiguous().realize(),
|
||||
'packed_npy_inputs': Tensor(packed_npy_inputs, device='NPY').realize(),
|
||||
})
|
||||
return input_queues, npy
|
||||
'packed_npy_inputs': Tensor(packed_input, device='NPY').realize(),
|
||||
}
|
||||
return input_queues, npy, frame_views
|
||||
|
||||
|
||||
def shift_and_sample(buf, new_val, sample_fn):
|
||||
@@ -176,13 +165,15 @@ def sample_desire(buf, frame_skip):
|
||||
return buf.reshape(-1, frame_skip, *buf.shape[1:]).max(1).flatten(0, 1).unsqueeze(0)
|
||||
|
||||
|
||||
def make_warp(nv12, model_w, model_h, frame_skip):
|
||||
def make_warp(nv12, model_w, model_h):
|
||||
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
|
||||
|
||||
def warp(tfm, big_tfm, frame, big_frame):
|
||||
tfm = tfm.to(WARP_DEV)
|
||||
big_tfm = big_tfm.to(WARP_DEV)
|
||||
Tensor.realize(tfm, big_tfm)
|
||||
tfm = tfm.to(Device.DEFAULT)
|
||||
big_tfm = big_tfm.to(Device.DEFAULT)
|
||||
frame = frame.to(Device.DEFAULT)
|
||||
big_frame = big_frame.to(Device.DEFAULT)
|
||||
Tensor.realize(tfm, big_tfm, frame, big_frame)
|
||||
|
||||
warped_frame = frame_prepare(frame, tfm).unsqueeze(0)
|
||||
warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0)
|
||||
@@ -195,10 +186,10 @@ def make_run_policy(model_runner, model_metadata, frame_skip):
|
||||
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
|
||||
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
|
||||
npy_shapes, npy_sizes = get_policy_npy_shapes(model_metadata['input_shapes'])
|
||||
model_input_dtypes = {name: spec.dtype for name, spec in model_runner.graph_inputs.items()}
|
||||
|
||||
def run_policy(warped, img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
|
||||
packed_npy_inputs = packed_npy_inputs.to(Device.DEFAULT)
|
||||
warped = warped.to(Device.DEFAULT)
|
||||
Tensor.realize(packed_npy_inputs, warped)
|
||||
|
||||
img = shift_and_sample(img_q, warped[0:1], sample_skip_fn)
|
||||
@@ -211,33 +202,50 @@ def make_run_policy(model_runner, model_metadata, frame_skip):
|
||||
inputs = {
|
||||
'img': img,
|
||||
'big_img': big_img,
|
||||
'features_buffer': feat_buf,
|
||||
'features_buffer': feat_buf.reshape(model_metadata['input_shapes']['features_buffer']),
|
||||
'desire_pulse': desire_buf,
|
||||
'traffic_convention': traffic_convention,
|
||||
'action_t': action_t,
|
||||
}
|
||||
inputs = {name: value.cast(model_input_dtypes[name]) for name, value in inputs.items()}
|
||||
out = next(iter(model_runner(inputs).values())).cast('float32')
|
||||
return out,
|
||||
return run_policy
|
||||
|
||||
|
||||
def compile_jit(jit, make_random_inputs, input_keys, make_queues):
|
||||
SEED = 42
|
||||
def random_inputs_run(fn, seed, test_val=None, test_buffers=None, expect_match=True):
|
||||
input_queues, npy = make_queues(Device.DEFAULT)
|
||||
rng = np.random.default_rng(seed)
|
||||
Tensor.manual_seed(seed)
|
||||
def make_run_model(warp, run_policy, model_metadata, frame_copy_size):
|
||||
_, policy_sizes = get_policy_npy_shapes(model_metadata['input_shapes'])
|
||||
packed_npy_size = (18 + sum(policy_sizes)) * np.dtype(np.float32).itemsize
|
||||
|
||||
testing = test_val is not None or test_buffers is not None
|
||||
n_runs = 1 if testing else 3
|
||||
def run_model(img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
|
||||
packed_input = packed_npy_inputs.to(Device.DEFAULT)
|
||||
Tensor.realize(packed_input)
|
||||
packed_npy_inputs = packed_input[:packed_npy_size].bitcast('float32')
|
||||
frame = packed_input[packed_npy_size:packed_npy_size + frame_copy_size]
|
||||
big_frame = packed_input[packed_npy_size + frame_copy_size:]
|
||||
tfm, big_tfm, policy_inputs = packed_npy_inputs.split([9, 9, sum(policy_sizes)])
|
||||
warped = warp(tfm.reshape(3, 3), big_tfm.reshape(3, 3), frame, big_frame)
|
||||
return run_policy(warped, img_q, big_img_q, feat_q, desire_q, policy_inputs)
|
||||
return run_model
|
||||
|
||||
|
||||
def compile_jit(jit, input_keys, make_queues, benchmark_runs):
|
||||
if benchmark_runs < 1:
|
||||
raise ValueError("benchmark_runs must be at least 1")
|
||||
|
||||
SEED = 42
|
||||
def random_inputs_run(fn, seed, n_runs, test_val=None, test_buffers=None, expect_match=True):
|
||||
input_queues, npy, frame_views = make_queues(Device.DEFAULT)
|
||||
rng = np.random.default_rng(seed)
|
||||
|
||||
for i in range(n_runs):
|
||||
for v in npy.values():
|
||||
v[:] = rng.standard_normal(v.shape).astype(v.dtype)
|
||||
for v in frame_views.values():
|
||||
v[:] = rng.integers(0, 256, size=v.shape, dtype=np.uint8)
|
||||
Device.default.synchronize()
|
||||
random_inputs = make_random_inputs()
|
||||
st = time.perf_counter()
|
||||
outs = fn(**{k: input_queues[k] for k in input_keys}, **random_inputs)
|
||||
outs = fn(**{k: input_queues[k] for k in input_keys})
|
||||
mt = time.perf_counter()
|
||||
Device.default.synchronize()
|
||||
et = time.perf_counter()
|
||||
@@ -256,14 +264,15 @@ def compile_jit(jit, make_random_inputs, input_keys, make_queues):
|
||||
return val, buffers
|
||||
|
||||
print('capture + replay')
|
||||
test_val, test_buffers = random_inputs_run(jit, SEED)
|
||||
print('pickle round trip')
|
||||
test_val, test_buffers = random_inputs_run(jit, SEED, 3)
|
||||
print(f'pickle round trip ({benchmark_runs} runs per seed)')
|
||||
with tempfile.TemporaryFile(dir=".") as f:
|
||||
dump_oob(jit, f)
|
||||
f.seek(0)
|
||||
jit = load_oob(f)
|
||||
random_inputs_run(jit, SEED, test_val, test_buffers, expect_match=True)
|
||||
random_inputs_run(jit, SEED+1, test_val, test_buffers, expect_match=False)
|
||||
loaded_jit = load_oob(f)
|
||||
random_inputs_run(loaded_jit, SEED, benchmark_runs, test_val, test_buffers, expect_match=True)
|
||||
random_inputs_run(loaded_jit, SEED+1, benchmark_runs, test_val, test_buffers, expect_match=False)
|
||||
# Keep the original so per-resolution JITs share model weight buffers in the final pickle.
|
||||
return jit
|
||||
|
||||
|
||||
@@ -292,27 +301,31 @@ if __name__ == "__main__":
|
||||
p.add_argument('--onnx', required=True)
|
||||
p.add_argument('--output', required=True)
|
||||
p.add_argument('--frame-skip', type=int, required=True)
|
||||
p.add_argument('--benchmark-runs', type=int, default=1,
|
||||
help='timed loaded-JIT runs for each correctness seed')
|
||||
args = p.parse_args()
|
||||
|
||||
model_path = read_file_chunked_to_disk(args.onnx)
|
||||
model_w, model_h = args.model_size
|
||||
|
||||
model_runner = OnnxRunner(model_path)
|
||||
out = {'metadata': make_metadata_dict(model_path)}
|
||||
out = {
|
||||
'metadata': make_metadata_dict(model_path),
|
||||
'input_devices': {'model': Device.DEFAULT},
|
||||
'run_model': {},
|
||||
}
|
||||
|
||||
run_policy_jit = TinyJit(make_run_policy(model_runner, out['metadata'], args.frame_skip), prune=True)
|
||||
|
||||
make_policy_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip)
|
||||
make_random_model_inputs = partial(make_random_images, keys=['warped'], shape=(2, 6, *out['metadata']['input_shapes']['img'][2:]), device=WARP_DEV)
|
||||
out['run_policy'] = compile_jit(run_policy_jit, make_random_model_inputs, POLICY_INPUTS,
|
||||
make_policy_queues)
|
||||
run_policy = make_run_policy(model_runner, out['metadata'], args.frame_skip)
|
||||
|
||||
for cam_w, cam_h in args.camera_resolutions:
|
||||
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
|
||||
make_random_warp_inputs = partial(make_random_images, keys=['frame', 'big_frame'], shape=nv12.size, device=WARP_DEV)
|
||||
warp = TinyJit(make_warp(nv12, model_w, model_h, args.frame_skip), prune=True)
|
||||
make_warp_queues = partial(make_warp_input_queues, out['metadata']['input_shapes'], args.frame_skip)
|
||||
out[(cam_w,cam_h)] = compile_jit(warp, make_random_warp_inputs, WARP_INPUTS, make_warp_queues)
|
||||
frame_copy_size = nv12_copy_size(nv12.stride, nv12.y_height, nv12.uv_height)
|
||||
make_model_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip,
|
||||
frame_copy_size=frame_copy_size)
|
||||
warp = make_warp(nv12, model_w, model_h)
|
||||
run_model_jit = TinyJit(make_run_model(warp, run_policy, out['metadata'], frame_copy_size), prune=True)
|
||||
out['run_model'][(cam_w,cam_h)] = compile_jit(run_model_jit, MODELD_INPUTS, make_model_queues,
|
||||
args.benchmark_runs)
|
||||
|
||||
with open(args.output, "wb") as f:
|
||||
dump_oob(out, f)
|
||||
|
||||
@@ -64,6 +64,7 @@ def fill_driving_model_data(msg: capnp._DynamicStructBuilder, modelv2_send: capn
|
||||
driving_model_data.frameIdExtra = modelV2.frameIdExtra
|
||||
driving_model_data.frameDropPerc = modelV2.frameDropPerc
|
||||
driving_model_data.modelExecutionTime = modelV2.modelExecutionTime
|
||||
driving_model_data.big = modelV2.big
|
||||
driving_model_data.action = modelV2.action
|
||||
driving_model_data.meta.laneChangeState = modelV2.meta.laneChangeState
|
||||
driving_model_data.meta.laneChangeDirection = modelV2.meta.laneChangeDirection
|
||||
|
||||
@@ -7,10 +7,12 @@ import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.common.file_chunker import get_manifest_path
|
||||
from openpilot.common.hardware.usb import CHESTNUT_FW_VERSION, CHESTNUT_USB_IDS, USB_DEVICES_PATH
|
||||
from openpilot.common.hardware.usb import CHESTNUT_USB_PRODUCT, USB_DEVICES_PATH, is_chestnut_usb_id
|
||||
|
||||
MODELS_DIR = Path(__file__).resolve().parent / 'models'
|
||||
TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json'
|
||||
CHESTNUT_POWERED_VOLTAGE = 5000
|
||||
CHESTNUT_PCIE_READY = 0x78
|
||||
|
||||
|
||||
def get_tg_input_devices(process_name: str, chestnut: bool):
|
||||
@@ -50,7 +52,7 @@ def chestnut_present() -> bool:
|
||||
try:
|
||||
usb_id = (int((d / "idVendor").read_text(), 16), int((d / "idProduct").read_text(), 16))
|
||||
product = (d / "product").read_text().strip()
|
||||
if usb_id in CHESTNUT_USB_IDS and product == f"custom {CHESTNUT_FW_VERSION}-CLEAN":
|
||||
if is_chestnut_usb_id(*usb_id) and product == CHESTNUT_USB_PRODUCT:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
@@ -58,3 +60,7 @@ def chestnut_present() -> bool:
|
||||
|
||||
def chestnut_compiled() -> bool:
|
||||
return Path(get_manifest_path(modeld_pkl_path(chestnut=True))).is_file()
|
||||
|
||||
|
||||
def chestnut_ready(state) -> bool:
|
||||
return state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE and not state.supplyFault and state.pcieLtssm == CHESTNUT_PCIE_READY
|
||||
|
||||
@@ -4,8 +4,8 @@ import ctypes
|
||||
from functools import cached_property
|
||||
import os
|
||||
os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom
|
||||
from tinygrad.tensor import Tensor
|
||||
from tinygrad.device import Device
|
||||
import usb1
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
@@ -28,17 +28,17 @@ from openpilot.common.transformations.model import get_warp_matrix
|
||||
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, should_stop, smooth_value, get_curvature_from_plan
|
||||
from openpilot.selfdrive.modeld.parse_model_outputs import Parser
|
||||
from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS
|
||||
from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, nv12_copy_size, MODELD_INPUTS
|
||||
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState
|
||||
from openpilot.common.file_chunker import open_file_chunked
|
||||
from openpilot.common.hardware.usb import CHESTNUT_USB_IDS
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
|
||||
from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, modeld_pkl_path, get_tg_input_devices, load_oob
|
||||
from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, chestnut_ready, modeld_pkl_path, load_oob
|
||||
|
||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
|
||||
|
||||
PROCESS_NAME = "openpilot.selfdrive.modeld.modeld"
|
||||
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
||||
|
||||
LAT_SMOOTH_SECONDS = 0.0
|
||||
@@ -83,6 +83,37 @@ class ChestnutState:
|
||||
self.valid = True
|
||||
self.sends = 0
|
||||
self.metrics = {}
|
||||
self._asm_usb = None
|
||||
|
||||
def _close_asm_usb(self) -> None:
|
||||
if self._asm_usb is not None:
|
||||
self._asm_usb.close()
|
||||
self._asm_usb = None
|
||||
|
||||
def _open_asm_usb(self):
|
||||
context = usb1.USBContext()
|
||||
for vendor_id, product_id in CHESTNUT_USB_IDS:
|
||||
if (handle := context.openByVendorIDAndProductID(vendor_id, product_id, skip_on_error=True)) is not None:
|
||||
return handle
|
||||
context.close()
|
||||
|
||||
def _read_ina(self) -> tuple[int, int, bool]:
|
||||
if "AMD" in Device._opened_devices and self._asm_usb is None:
|
||||
try:
|
||||
raw = Device["AMD"].iface.pci_dev.usb.usb.control_read(0xC0, 5)
|
||||
return struct.unpack('<Hh?', bytes(raw))
|
||||
except Exception:
|
||||
pass
|
||||
if self._asm_usb is None:
|
||||
self._asm_usb = self._open_asm_usb()
|
||||
if self._asm_usb is None:
|
||||
raise usb1.USBErrorNoDevice
|
||||
try:
|
||||
raw = self._asm_usb.controlRead(0xC0, 0xC0, 0, 0, 5, timeout=100)
|
||||
except usb1.USBError:
|
||||
self._close_asm_usb()
|
||||
raise
|
||||
return struct.unpack('<Hh?', bytes(raw))
|
||||
|
||||
@cached_property
|
||||
def power_limit(self) -> int:
|
||||
@@ -118,13 +149,15 @@ class ChestnutState:
|
||||
setattr(state, k, v)
|
||||
|
||||
asm_valid = False
|
||||
try:
|
||||
# ASM runs on USB-C power, these still read without a gpu
|
||||
state.supplyVoltage, state.supplyCurrent, state.supplyFault = self._read_ina()
|
||||
asm_valid = True
|
||||
except Exception:
|
||||
pass
|
||||
if "AMD" in Device._opened_devices:
|
||||
try:
|
||||
# ASM runs on USB-C power, these still read without a gpu
|
||||
asm = Device["AMD"].iface.pci_dev.usb
|
||||
state.pcieLtssm = asm.read(0xB450, 1)[0]
|
||||
state.supplyVoltage, state.supplyCurrent = struct.unpack('<Hh', bytes(asm.usb.control_read(0xC0, 5))[:4])
|
||||
asm_valid = True
|
||||
state.pcieLtssm = Device["AMD"].iface.pci_dev.usb.read(0xB450, 1)[0]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -147,9 +180,9 @@ class ModelState(ModelStateBase):
|
||||
|
||||
def __init__(self, cam_w: int, cam_h: int, chestnut: bool):
|
||||
ModelStateBase.__init__(self)
|
||||
input_devices = get_tg_input_devices(PROCESS_NAME, chestnut)
|
||||
self.WARP_DEV, self.QUEUE_DEV = input_devices['WARP_DEV'], input_devices['QUEUE_DEV']
|
||||
jits = load_oob(open_file_chunked(modeld_pkl_path(chestnut)))
|
||||
input_devices = jits['input_devices']
|
||||
self.model_device = input_devices['model']
|
||||
metadata = jits['metadata']
|
||||
self.input_shapes = metadata['input_shapes']
|
||||
self.vision_input_names = [k for k in self.input_shapes if 'img' in k]
|
||||
@@ -159,13 +192,11 @@ class ModelState(ModelStateBase):
|
||||
self.chestnut = chestnut
|
||||
|
||||
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
||||
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
|
||||
self.full_frames: dict[str, Tensor] = {}
|
||||
self._blob_cache: dict[tuple[str, int], Tensor] = {}
|
||||
self.frame_copy_size = nv12_copy_size(*get_nv12_info(cam_w, cam_h)[:3])
|
||||
self.input_queues, self.npy, self.frame_views = make_input_queues(
|
||||
self.input_shapes, self.frame_skip, device=self.model_device, frame_copy_size=self.frame_copy_size)
|
||||
self.parser = Parser()
|
||||
self.frame_buf_params = {k: get_nv12_info(cam_w, cam_h) for k in ('img', 'big_img')}
|
||||
self.run_policy = jits['run_policy']
|
||||
self.warp = jits[(cam_w,cam_h)]
|
||||
self.run_model = jits['run_model'][(cam_w,cam_h)]
|
||||
|
||||
def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]:
|
||||
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
|
||||
@@ -173,14 +204,8 @@ class ModelState(ModelStateBase):
|
||||
|
||||
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
||||
inputs: dict[str, np.ndarray], after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray]:
|
||||
for key in bufs.keys():
|
||||
ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data
|
||||
yuv_size = self.frame_buf_params[key][3]
|
||||
# There is a ringbuffer of imgs, just cache tensors pointing to all of them
|
||||
cache_key = (key, ptr)
|
||||
if cache_key not in self._blob_cache:
|
||||
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8', device=self.WARP_DEV)
|
||||
self.full_frames[key] = self._blob_cache[cache_key]
|
||||
for key, buf in bufs.items():
|
||||
np.copyto(self.frame_views[key], np.frombuffer(buf.data, dtype=np.uint8, count=self.frame_copy_size))
|
||||
|
||||
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
||||
inputs['desire_pulse'][0] = 0
|
||||
@@ -191,11 +216,7 @@ class ModelState(ModelStateBase):
|
||||
self.npy['tfm'][:,:] = transforms['img'][:,:]
|
||||
self.npy['big_tfm'][:,:] = transforms['big_img'][:,:]
|
||||
|
||||
warped = self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames['img'], big_frame=self.full_frames['big_img'])
|
||||
|
||||
outs, = self.run_policy(
|
||||
**{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped
|
||||
)
|
||||
outs, = self.run_model(**{k: self.input_queues[k] for k in MODELD_INPUTS})
|
||||
if after_enqueue is not None:
|
||||
after_enqueue()
|
||||
model_output = outs.numpy()[0]
|
||||
@@ -209,25 +230,37 @@ class ModelState(ModelStateBase):
|
||||
return outputs_dict
|
||||
|
||||
def warmup(self) -> None:
|
||||
dummy_frames = {k: np.zeros(self.frame_buf_params[k][3], dtype=np.uint8) for k in self.vision_input_names}
|
||||
dummy_frames = {k: np.zeros(self.frame_copy_size, dtype=np.uint8) for k in self.vision_input_names}
|
||||
eye = np.eye(3, dtype=np.float32)
|
||||
dims = {'desire_pulse': ModelConstants.DESIRE_LEN, 'traffic_convention': 2, 'action_t': 2}
|
||||
self.run(dummy_frames, dict.fromkeys(self.vision_input_names, eye), {k: np.zeros(v, dtype=np.float32) for k, v in dims.items()})
|
||||
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
|
||||
self.input_queues, self.npy, self.frame_views = make_input_queues(
|
||||
self.input_shapes, self.frame_skip, device=self.model_device, frame_copy_size=self.frame_copy_size)
|
||||
self.prev_desire[:] = 0
|
||||
self.full_frames.clear()
|
||||
self._blob_cache.clear()
|
||||
|
||||
|
||||
def main(demo=False):
|
||||
cloudlog.warning("modeld init")
|
||||
|
||||
CHESTNUT = chestnut_present() and chestnut_compiled()
|
||||
chestnut_available = chestnut_present() and chestnut_compiled()
|
||||
CHESTNUT = False
|
||||
if chestnut_available:
|
||||
poller = messaging.Poller()
|
||||
sock = messaging.sub_sock("chestnutState", poller=poller, conflate=True)
|
||||
deadline = time.monotonic() + 4. / SERVICE_LIST['deviceState'].frequency
|
||||
while not CHESTNUT and (remaining := deadline - time.monotonic()) > 0.:
|
||||
if not poller.poll(round(remaining * 1000)):
|
||||
break
|
||||
msg = messaging.recv_one_or_none(sock)
|
||||
CHESTNUT = msg is not None and msg.valid and chestnut_ready(msg.chestnutState)
|
||||
if CHESTNUT:
|
||||
os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000'
|
||||
params = Params()
|
||||
params.put_bool("ChestnutLoading", CHESTNUT)
|
||||
params.remove("ChestnutActive")
|
||||
if chestnut_available and not CHESTNUT:
|
||||
params.put_bool("ChestnutActive", False)
|
||||
else:
|
||||
params.remove("ChestnutActive")
|
||||
|
||||
config_realtime_process(7, 54)
|
||||
|
||||
@@ -271,7 +304,11 @@ def main(demo=False):
|
||||
loader.start()
|
||||
loader.join(BIG_MODEL_TIMEOUT)
|
||||
model = big_model
|
||||
if model is None:
|
||||
params.put_bool("ChestnutModelError", True)
|
||||
params.put_bool("ChestnutActive", model is not None)
|
||||
if model is not None:
|
||||
params.remove("ChestnutModelError")
|
||||
|
||||
small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or CHESTNUT else None
|
||||
if model is None:
|
||||
@@ -405,6 +442,7 @@ def main(demo=False):
|
||||
raise
|
||||
# fallback to small model
|
||||
cloudlog.exception("big model failed, fall back to small")
|
||||
params.put_bool("ChestnutModelError", True)
|
||||
params.put_bool("ChestnutActive", False)
|
||||
assert small_model is not None
|
||||
model = small_model
|
||||
@@ -431,12 +469,11 @@ def main(demo=False):
|
||||
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
|
||||
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
|
||||
lane_change_prob = l_lane_change_prob + r_lane_change_prob
|
||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
|
||||
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
||||
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
||||
|
||||
mdv2sp_send = messaging.new_message('modelDataV2SP')
|
||||
left_edge, right_edge = RELC.update_and_fill(modelv2_send.modelV2, mdv2sp_send.modelDataV2SP, v_ego)
|
||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, left_edge, right_edge)
|
||||
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
||||
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
||||
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
|
||||
|
||||
fill_driving_model_data(drivingdata_send, modelv2_send)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a501760a9d1d5fef0eab2b8c5d122d06124fc26dc8e0782e0aa94b82a208f0ff
|
||||
size 1757355221
|
||||
oid sha256:1791d5940b2c048d0639813426dd2cf1d6f2a6727ed51e17c8bcea8bbe754123
|
||||
size 765950064
|
||||
|
||||
@@ -123,22 +123,22 @@ void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::Panda
|
||||
ps.setUptime(health.uptime_pkt);
|
||||
ps.setSafetyTxBlocked(health.safety_tx_blocked_pkt);
|
||||
ps.setSafetyRxInvalid(health.safety_rx_invalid_pkt);
|
||||
ps.setIgnitionLine(health.ignition_line_pkt);
|
||||
ps.setIgnitionCan(health.ignition_can_pkt);
|
||||
ps.setControlsAllowed(health.controls_allowed_pkt);
|
||||
ps.setIgnitionLine((health.flags_pkt & HEALTH_FLAG_IGNITION_LINE) != 0U);
|
||||
ps.setIgnitionCan((health.flags_pkt & HEALTH_FLAG_IGNITION_CAN) != 0U);
|
||||
ps.setControlsAllowed((health.flags_pkt & HEALTH_FLAG_CONTROLS_ALLOWED) != 0U);
|
||||
ps.setTxBufferOverflow(health.tx_buffer_overflow_pkt);
|
||||
ps.setRxBufferOverflow(health.rx_buffer_overflow_pkt);
|
||||
ps.setPandaType(hw_type);
|
||||
ps.setSafetyModel(cereal::CarParams::SafetyModel(health.safety_mode_pkt));
|
||||
ps.setSafetyParam(health.safety_param_pkt);
|
||||
ps.setFaultStatus(cereal::PandaState::FaultStatus(health.fault_status_pkt));
|
||||
ps.setPowerSaveEnabled((bool)(health.power_save_enabled_pkt));
|
||||
ps.setHeartbeatLost((bool)(health.heartbeat_lost_pkt));
|
||||
ps.setPowerSaveEnabled((health.flags_pkt & HEALTH_FLAG_POWER_SAVE_ENABLED) != 0U);
|
||||
ps.setHeartbeatLost((health.flags_pkt & HEALTH_FLAG_HEARTBEAT_LOST) != 0U);
|
||||
ps.setAlternativeExperience(health.alternative_experience_pkt);
|
||||
ps.setHarnessStatus(cereal::PandaState::HarnessStatus(health.car_harness_status_pkt));
|
||||
ps.setInterruptLoad(health.interrupt_load_pkt);
|
||||
ps.setInterruptLoad(health.interrupt_load_pkt / 255.0f);
|
||||
ps.setFanPower(health.fan_power);
|
||||
ps.setSafetyRxChecksInvalid((bool)(health.safety_rx_checks_invalid_pkt));
|
||||
ps.setSafetyRxChecksInvalid((health.flags_pkt & HEALTH_FLAG_SAFETY_RX_CHECKS_INVALID) != 0U);
|
||||
ps.setSpiErrorCount(health.spi_error_count_pkt);
|
||||
ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f);
|
||||
ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f);
|
||||
@@ -198,10 +198,10 @@ std::optional<bool> send_panda_states(PubMaster *pm, Panda *panda, bool is_onroa
|
||||
}
|
||||
|
||||
if (spoofing_started) {
|
||||
health.ignition_line_pkt = 1;
|
||||
health.flags_pkt |= HEALTH_FLAG_IGNITION_LINE;
|
||||
}
|
||||
|
||||
bool ignition_local = ((health.ignition_line_pkt != 0) || (health.ignition_can_pkt != 0)) && !always_offroad;
|
||||
bool ignition_local = ((health.flags_pkt & (HEALTH_FLAG_IGNITION_LINE | HEALTH_FLAG_IGNITION_CAN)) != 0U) && !always_offroad;
|
||||
|
||||
// Make sure CAN buses are live: safety_setter_thread does not work if Panda CAN are silent and there is only one other CAN node
|
||||
if (health.safety_mode_pkt == (uint8_t)(cereal::CarParams::SafetyModel::SILENT)) {
|
||||
@@ -209,7 +209,7 @@ std::optional<bool> send_panda_states(PubMaster *pm, Panda *panda, bool is_onroa
|
||||
}
|
||||
|
||||
bool power_save_desired = !ignition_local;
|
||||
if (health.power_save_enabled_pkt != power_save_desired) {
|
||||
if (((health.flags_pkt & HEALTH_FLAG_POWER_SAVE_ENABLED) != 0U) != power_save_desired) {
|
||||
panda->set_power_saving(power_save_desired);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,30 @@
|
||||
},
|
||||
"Offroad_ChestnutBranch": {
|
||||
"text": "Chestnut detected! Switch to the %1 branch to use chestnut-class models.",
|
||||
"severity": -1
|
||||
},
|
||||
"Offroad_ChestnutNotDetected": {
|
||||
"text": "Chestnut not detected. Check USB and 12V connections.",
|
||||
"severity": 0
|
||||
},
|
||||
"Offroad_ChestnutOverheated": {
|
||||
"text": "Chestnut overheated. Ensure good airflow. Current GPU temperature is %1.",
|
||||
"severity": 0
|
||||
},
|
||||
"Offroad_ChestnutPcieUnavailable": {
|
||||
"text": "%1",
|
||||
"severity": 0
|
||||
},
|
||||
"Offroad_ChestnutUncompiled": {
|
||||
"text": "Chestnut model not compiled. Keep ignition on and reboot the comma.",
|
||||
"severity": 0
|
||||
},
|
||||
"Offroad_ChestnutUpdateFailed": {
|
||||
"text": "Chestnut update failed. Check the USB cable.",
|
||||
"severity": 0
|
||||
},
|
||||
"Offroad_ChestnutUsbSlow": {
|
||||
"text": "Chestnut USB link is slow. Check the USB cable. The current speed is %1.",
|
||||
"severity": 0
|
||||
},
|
||||
"Offroad_UnregisteredHardware": {
|
||||
|
||||
@@ -152,7 +152,7 @@ def migrate_drivingModelData(msgs):
|
||||
add_ops = []
|
||||
for _, msg in msgs:
|
||||
dmd = messaging.new_message('drivingModelData', valid=msg.valid, logMonoTime=msg.logMonoTime)
|
||||
for field in ["frameId", "frameIdExtra", "frameDropPerc", "modelExecutionTime", "action"]:
|
||||
for field in ["frameId", "frameIdExtra", "frameDropPerc", "modelExecutionTime", "big", "action"]:
|
||||
setattr(dmd.drivingModelData, field, getattr(msg.modelV2, field))
|
||||
for meta_field in ["laneChangeState", "laneChangeState"]:
|
||||
setattr(dmd.drivingModelData.meta, meta_field, getattr(msg.modelV2.meta, meta_field))
|
||||
|
||||
@@ -33,9 +33,9 @@ MODEL_REPLAY_BUCKET="model_replay_master"
|
||||
GITHUB = GithubUtils(API_TOKEN, DATA_TOKEN)
|
||||
|
||||
EXEC_TIMINGS = [
|
||||
# model, instant max, average max
|
||||
("modelV2", 0.05, 0.028),
|
||||
("driverStateV2", 0.05, 0.018),
|
||||
# model, instant max, average max, chestnut average max
|
||||
("modelV2", 0.05, 0.03, 0.05),
|
||||
("driverStateV2", 0.05, 0.018, 0.018),
|
||||
]
|
||||
|
||||
def get_log_fn(test_route, ref="master"):
|
||||
@@ -169,11 +169,13 @@ def model_replay(lr, frs):
|
||||
dmonitoringmodeld_msgs = replay_process(dmonitoringmodeld, dmodeld_logs, frs)
|
||||
|
||||
msgs = modeld_msgs + dmonitoringmodeld_msgs
|
||||
chestnut = any(m.modelV2.big for m in modeld_msgs if m.which() == "modelV2")
|
||||
|
||||
header = ['model', 'max instant', 'max instant allowed', 'average', 'max average allowed', 'test result']
|
||||
rows = []
|
||||
timings_ok = True
|
||||
for (s, instant_max, avg_max) in EXEC_TIMINGS:
|
||||
for (s, instant_max, avg_max, chestnut_avg_max) in EXEC_TIMINGS:
|
||||
avg_max = chestnut_avg_max if chestnut else avg_max
|
||||
ts = [getattr(m, s).modelExecutionTime for m in msgs if m.which() == s]
|
||||
# TODO some init can happen in first iteration
|
||||
ts = ts[1:]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import time
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
@@ -26,8 +26,8 @@ class BodyLayout(Widget):
|
||||
self._last_input_time = time.monotonic()
|
||||
self._was_active = False
|
||||
self._offroad_label = UnifiedLabel("turn on ignition to use", 95 if gui_app.big_ui() else 45, FontWeight.DISPLAY,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
alignment=TextAlignment.CENTER,
|
||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
||||
|
||||
def draw_dot_grid(self, rect: rl.Rectangle, dots: list[tuple[int, int]], color: rl.Color):
|
||||
spacing = min(rect.height / GRID_ROWS, rect.width / GRID_COLS)
|
||||
|
||||
@@ -8,7 +8,7 @@ from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButto
|
||||
from openpilot.selfdrive.ui.widgets.prime import PrimeWidget
|
||||
from openpilot.selfdrive.ui.widgets.setup import SetupWidget
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment
|
||||
from openpilot.system.ui.lib.multilang import tr, trn
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
@@ -178,7 +178,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)
|
||||
gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=TextAlignment.RIGHT)
|
||||
|
||||
def _render_home_content(self):
|
||||
self._render_left_column()
|
||||
|
||||
@@ -5,7 +5,7 @@ from enum import IntEnum
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.system.ui.lib.application import FontWeight, TextAlignment, gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||
@@ -115,9 +115,9 @@ 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._title = Label(tr("Welcome to sunnypilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=TextAlignment.LEFT)
|
||||
self._desc = Label(tr("You must accept the Terms of Service to use sunnypilot. Read the latest terms at https://sunnypilot.ai/terms before continuing."),
|
||||
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT)
|
||||
|
||||
self._decline_btn = Button(tr("Decline"), click_callback=on_decline)
|
||||
self._accept_btn = Button(tr("Agree"), button_style=ButtonStyle.PRIMARY, click_callback=on_accept)
|
||||
@@ -150,7 +150,7 @@ class DeclinePage(Widget):
|
||||
def __init__(self, back_callback=None):
|
||||
super().__init__()
|
||||
self._text = Label(tr("You must accept the Terms of Service in order to use sunnypilot."),
|
||||
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT)
|
||||
self._back_btn = Button(tr("Back"), click_callback=back_callback)
|
||||
self._uninstall_btn = Button(tr("Decline, uninstall sunnypilot"), button_style=ButtonStyle.DANGER,
|
||||
click_callback=self._on_uninstall_clicked)
|
||||
|
||||
@@ -199,6 +199,9 @@ class SoftwareLayout(Widget):
|
||||
selection = self._branch_dialog.selection
|
||||
ui_state.params.put("UpdaterTargetBranch", selection, block=True)
|
||||
self._branch_btn.action_item.set_value(selection)
|
||||
self._download_btn.action_item.set_enabled(False)
|
||||
self._waiting_for_updater = True
|
||||
self._waiting_start_ts = time.monotonic()
|
||||
subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
|
||||
self._branch_dialog = None
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import datetime
|
||||
import math
|
||||
import time
|
||||
|
||||
from openpilot.cereal import log
|
||||
@@ -8,7 +9,7 @@ from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.layouts import HBoxLayout
|
||||
from openpilot.system.ui.widgets.icon_widget import IconWidget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment, TextAlignmentVertical
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
|
||||
from openpilot.common.version import RELEASE_BRANCHES
|
||||
|
||||
@@ -69,8 +70,8 @@ class AlertsPill(Widget):
|
||||
|
||||
count_rect = rl.Rectangle(self.rect.x + self.COUNT_OFFSET, self.rect.y, pill_w - self.COUNT_OFFSET, pill_h)
|
||||
gui_label(count_rect, str(alert_count), font_size=36,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
alignment=TextAlignment.CENTER,
|
||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
||||
|
||||
|
||||
class NetworkIcon(Widget):
|
||||
@@ -139,7 +140,9 @@ class MiciHomeLayout(Widget):
|
||||
self._version_text = self._get_version_text()
|
||||
|
||||
self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48))
|
||||
self._usb_icon = IconWidget("icons_mici/usb.png", (62, 40))
|
||||
self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (68, 40))
|
||||
self._chestnut_loading_icon = IconWidget("icons_mici/chestnut.png", (68, 40))
|
||||
self._chestnut_failed_icon = IconWidget("icons_mici/chestnut_orange.png", (68, 40))
|
||||
self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46))
|
||||
self._body_icon = IconWidget("icons_mici/body.png", (54, 37))
|
||||
@@ -150,13 +153,15 @@ class MiciHomeLayout(Widget):
|
||||
IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9),
|
||||
NetworkIcon(),
|
||||
self._experimental_icon,
|
||||
self._usb_icon,
|
||||
self._chestnut_icon,
|
||||
self._chestnut_loading_icon,
|
||||
self._chestnut_failed_icon,
|
||||
self._body_icon,
|
||||
self._mic_icon,
|
||||
], spacing=18)
|
||||
|
||||
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=96, font_weight=FontWeight.DISPLAY, max_width=480, wrap_text=False)
|
||||
self._openpilot_label = UnifiedLabel("openpilot", font_size=96, font_weight=FontWeight.DISPLAY, max_width=480, wrap_text=False)
|
||||
self._version_label = UnifiedLabel("", font_size=36, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
||||
self._large_version_label = UnifiedLabel("", font_size=64, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
||||
self._date_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
||||
@@ -247,12 +252,20 @@ class MiciHomeLayout(Widget):
|
||||
self._version_commit_label.render()
|
||||
|
||||
# ***** Center-aligned bottom section icons *****
|
||||
usb_connected = ui_state.usb_connected
|
||||
usb_unknown = ui_state.usb_unknown
|
||||
chestnut_state = ui_state.chestnut_state
|
||||
self._experimental_icon.set_visible(ui_state.experimental_mode)
|
||||
if gui_app.sunnypilot_ui():
|
||||
self._set_chestnut_visibility()
|
||||
else:
|
||||
self._chestnut_icon.set_visible(ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.LOADING, ChestnutState.ACTIVE))
|
||||
self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
|
||||
self._usb_icon.set_visible(usb_connected and usb_unknown)
|
||||
self._chestnut_icon.set_visible(not usb_unknown and chestnut_state not in
|
||||
(ChestnutState.LOADING, ChestnutState.UNCOMPILED, ChestnutState.FAILED) and
|
||||
(usb_connected or chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE)))
|
||||
self._chestnut_loading_icon.set_visible(not usb_unknown and chestnut_state == ChestnutState.LOADING)
|
||||
self._chestnut_loading_icon.set_opacity(0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)))
|
||||
self._chestnut_failed_icon.set_visible(not usb_unknown and chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
|
||||
self._mic_icon.set_visible(ui_state.recording_audio)
|
||||
self._body_icon.set_visible(bool(ui_state.is_body))
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from openpilot.common.hardware import HARDWARE
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
|
||||
REFRESH_INTERVAL = 5.0 # seconds
|
||||
@@ -62,12 +62,12 @@ class AlertItem(Widget):
|
||||
self._icon_green = gui_app.texture("icons_mici/offroad_alerts/green_wheel.png", self.ICON_SIZE, self.ICON_SIZE)
|
||||
|
||||
self._title_label = UnifiedLabel(text="", font_size=32, font_weight=FontWeight.SEMI_BOLD, text_color=self.TEXT_COLOR,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, line_height=0.95)
|
||||
alignment=TextAlignment.LEFT,
|
||||
alignment_vertical=TextAlignmentVertical.TOP, line_height=0.95)
|
||||
|
||||
self._body_label = UnifiedLabel(text="", font_size=28, font_weight=FontWeight.ROMAN, text_color=self.TEXT_COLOR,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, line_height=0.95)
|
||||
alignment=TextAlignment.LEFT,
|
||||
alignment_vertical=TextAlignmentVertical.BOTTOM, line_height=0.95)
|
||||
|
||||
self._title_text = ""
|
||||
self._body_text = ""
|
||||
@@ -200,8 +200,8 @@ class MiciOffroadAlerts(Scroller):
|
||||
|
||||
# Create empty state label
|
||||
self._empty_label = UnifiedLabel(tr("no alerts"), 65, FontWeight.DISPLAY, rl.WHITE,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
alignment=TextAlignment.CENTER,
|
||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
||||
|
||||
# Build initial alert list
|
||||
self._build_alerts()
|
||||
|
||||
@@ -4,7 +4,7 @@ import pyray as rl
|
||||
from collections.abc import Callable
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.qrcode import make_texture
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app, TextAlignment
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.button import SmallCircleIconButton
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller, Scroller
|
||||
@@ -35,7 +35,7 @@ class DriverCameraSetupDialog(BaseCabinCameraDialog):
|
||||
|
||||
if not self._camera_view.frame:
|
||||
gui_label(rect, tr("camera starting"), font_size=64, font_weight=FontWeight.BOLD,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
alignment=TextAlignment.CENTER)
|
||||
rl.end_scissor_mode()
|
||||
return
|
||||
|
||||
|
||||
@@ -74,6 +74,10 @@ class SoftwareInfoLayoutMici(Widget):
|
||||
|
||||
|
||||
class CheckUpdateButton(BigButton):
|
||||
UPDATER_PROC = "openpilot.system.updated.updated"
|
||||
CHECK_FOR_UPDATE = "SIGUSR1"
|
||||
DOWNLOAD_UPDATE = "SIGHUP"
|
||||
|
||||
def __init__(self):
|
||||
self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75)
|
||||
self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64)
|
||||
@@ -97,15 +101,20 @@ class CheckUpdateButton(BigButton):
|
||||
gui_app.push_widget(dlg)
|
||||
return
|
||||
|
||||
self._signal_updater(self.DOWNLOAD_UPDATE if self.get_value() == "download update" else self.CHECK_FOR_UPDATE)
|
||||
|
||||
def check_for_update(self):
|
||||
self._signal_updater(self.CHECK_FOR_UPDATE)
|
||||
|
||||
def _signal_updater(self, sig: str):
|
||||
self.set_enabled(False)
|
||||
self._state = UpdaterState.WAITING_FOR_UPDATER
|
||||
self._hide_value_t = None
|
||||
self.set_value("")
|
||||
self.set_icon(self._txt_update_icon)
|
||||
|
||||
def run():
|
||||
if self.get_value() == "download update":
|
||||
subprocess.run("pkill -SIGHUP -f openpilot.system.updated.updated", shell=True)
|
||||
else:
|
||||
subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
|
||||
subprocess.run(f"pkill -{sig} -f {self.UPDATER_PROC}", shell=True)
|
||||
|
||||
threading.Thread(target=run, daemon=True).start()
|
||||
|
||||
@@ -184,7 +193,7 @@ class CheckUpdateButton(BigButton):
|
||||
|
||||
class InstallUpdateButton(BigButton):
|
||||
def __init__(self):
|
||||
super().__init__("install update", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70))
|
||||
super().__init__("install now", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70))
|
||||
self.set_visible(lambda: ui_state.is_offroad() and ui_state.params.get_bool("UpdateAvailable"))
|
||||
|
||||
def _update_state(self):
|
||||
@@ -232,8 +241,9 @@ class BranchSelectPage(NavScroller):
|
||||
|
||||
|
||||
class TargetBranchButton(BigButton):
|
||||
def __init__(self):
|
||||
def __init__(self, check_update_btn: CheckUpdateButton):
|
||||
super().__init__("target branch", ui_state.params.get("UpdaterTargetBranch") or "")
|
||||
self._check_update_btn = check_update_btn
|
||||
self.set_click_callback(self._on_click)
|
||||
self.set_visible(not ui_state.params.get_bool("IsTestedBranch"))
|
||||
self.set_enabled(lambda: ui_state.is_offroad())
|
||||
@@ -246,12 +256,15 @@ class TargetBranchButton(BigButton):
|
||||
self.set_value(target)
|
||||
|
||||
def _on_click(self):
|
||||
if not ui_state.params.get("UpdaterAvailableBranches"):
|
||||
gui_app.push_widget(BigDialog("", tr("Failed to get available branches. Ensure you're connected to the internet and try again.")))
|
||||
return
|
||||
gui_app.push_widget(BranchSelectPage(self._on_select))
|
||||
|
||||
def _on_select(self, branch: str):
|
||||
ui_state.params.put("UpdaterTargetBranch", branch, block=True)
|
||||
self.set_value(branch)
|
||||
subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
|
||||
self._check_update_btn.check_for_update()
|
||||
|
||||
|
||||
class SoftwareLayoutMici(NavScroller):
|
||||
@@ -265,10 +278,11 @@ class SoftwareLayoutMici(NavScroller):
|
||||
gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64),
|
||||
uninstall_openpilot_callback, exit_on_confirm=False)
|
||||
|
||||
check_update_btn = CheckUpdateButton()
|
||||
self._scroller.add_widgets([
|
||||
SoftwareInfoLayoutMici(),
|
||||
CheckUpdateButton(),
|
||||
check_update_btn,
|
||||
InstallUpdateButton(),
|
||||
TargetBranchButton(),
|
||||
TargetBranchButton(check_update_btn),
|
||||
uninstall_openpilot_btn,
|
||||
])
|
||||
|
||||
@@ -10,7 +10,7 @@ from opendbc.car.structs import car
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter
|
||||
from openpilot.common.hardware import COMMA_HARDWARE
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
|
||||
@@ -333,7 +333,7 @@ class AlertRenderer(Widget, SpeedLimitAlertRenderer):
|
||||
self._alert_text1_label.set_text(alert_text1)
|
||||
self._alert_text1_label.set_text_color(color)
|
||||
self._alert_text1_label.set_font_size(font_size)
|
||||
self._alert_text1_label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_LEFT if icon_side != 'left' else rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||
self._alert_text1_label.set_alignment(TextAlignment.LEFT if icon_side != 'left' else TextAlignment.RIGHT)
|
||||
self._alert_text1_label.render(text_rect1)
|
||||
|
||||
alert_text2 = alert.text2.lower()
|
||||
@@ -365,5 +365,5 @@ class AlertRenderer(Widget, SpeedLimitAlertRenderer):
|
||||
self._alert_text2_label.set_text(alert_text2)
|
||||
self._alert_text2_label.set_text_color(color)
|
||||
self._alert_text2_label.set_font_size(small_font_size)
|
||||
self._alert_text2_label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_LEFT if icon_side != 'left' else rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||
self._alert_text2_label.set_alignment(TextAlignment.LEFT if icon_side != 'left' else TextAlignment.RIGHT)
|
||||
self._alert_text2_label.render(text_rect2)
|
||||
|
||||
@@ -11,7 +11,7 @@ from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer
|
||||
from openpilot.selfdrive.ui.mici.onroad.model_renderer import ModelRenderer
|
||||
from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall
|
||||
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent, TextAlignment, TextAlignmentVertical
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.common.filter_simple import BounceFilter
|
||||
@@ -158,8 +158,8 @@ class AugmentedRoadView(CameraView):
|
||||
self._confidence_ball = ConfidenceBall()
|
||||
self._offroad_label = UnifiedLabel("start the car to\nuse sunnypilot", 54, FontWeight.DISPLAY,
|
||||
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
alignment=TextAlignment.CENTER,
|
||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
||||
|
||||
self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png")
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from openpilot.cereal.visionipc import VisionStreamType
|
||||
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
|
||||
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
@@ -76,7 +76,7 @@ class BaseCabinCameraDialog(Widget):
|
||||
|
||||
if not self._camera_view.frame:
|
||||
gui_label(rect, tr("camera starting"), font_size=54, font_weight=FontWeight.BOLD,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
alignment=TextAlignment.CENTER)
|
||||
rl.end_scissor_mode()
|
||||
self._publish_alert_sound(None)
|
||||
return
|
||||
@@ -124,12 +124,12 @@ class BaseCabinCameraDialog(Widget):
|
||||
awareness_pct = dm_state.visionPolicyState.awarenessPercent if is_vision else dm_state.wheeltouchPolicyState.awarenessPercent
|
||||
gui_label(rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height),
|
||||
f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||
alignment=TextAlignment.RIGHT,
|
||||
alignment_vertical=TextAlignmentVertical.TOP,
|
||||
color=rl.Color(0, 0, 0, 180))
|
||||
gui_label(rect, f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||
alignment=TextAlignment.RIGHT,
|
||||
alignment_vertical=TextAlignmentVertical.TOP,
|
||||
color=rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||
|
||||
if dm_state.alertLevel == log.DriverMonitoringState.AlertLevel.none:
|
||||
@@ -137,16 +137,16 @@ class BaseCabinCameraDialog(Widget):
|
||||
|
||||
# Show alert level
|
||||
alert_level_str = f"{'Pay Attention' if is_vision else 'Touch Wheel'} - level {dm_state.alertLevel}"
|
||||
alignment = rl.GuiTextAlignment.TEXT_ALIGN_RIGHT if self.driver_state_renderer.is_rhd else rl.GuiTextAlignment.TEXT_ALIGN_LEFT
|
||||
alignment = TextAlignment.RIGHT if self.driver_state_renderer.is_rhd else TextAlignment.LEFT
|
||||
|
||||
shadow_rect = rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height)
|
||||
gui_label(shadow_rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD,
|
||||
alignment=alignment,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM,
|
||||
alignment_vertical=TextAlignmentVertical.BOTTOM,
|
||||
color=rl.Color(0, 0, 0, 180))
|
||||
gui_label(rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD,
|
||||
alignment=alignment,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM,
|
||||
alignment_vertical=TextAlignmentVertical.BOTTOM,
|
||||
color=rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||
|
||||
def _load_eye_textures(self):
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections.abc import Callable
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.scroller import DO_ZOOM
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignmentVertical
|
||||
from openpilot.common.filter_simple import BounceFilter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -125,10 +125,10 @@ class BigButton(Widget):
|
||||
self._rotate_icon_t: float | None = None
|
||||
|
||||
self._label = UnifiedLabel(text, font_size=self._get_label_font_size(), font_weight=FontWeight.BOLD,
|
||||
text_color=LABEL_COLOR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, scroll=scroll,
|
||||
text_color=LABEL_COLOR, alignment_vertical=TextAlignmentVertical.BOTTOM, scroll=scroll,
|
||||
line_height=0.9)
|
||||
self._sub_label = UnifiedLabel(value, font_size=COMPLICATION_SIZE, font_weight=FontWeight.ROMAN,
|
||||
text_color=COMPLICATION_GREY, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||
text_color=COMPLICATION_GREY, alignment_vertical=TextAlignmentVertical.BOTTOM)
|
||||
self._update_label_layout()
|
||||
|
||||
self._load_images()
|
||||
@@ -167,9 +167,9 @@ class BigButton(Widget):
|
||||
def _update_label_layout(self):
|
||||
self._label.set_font_size(self._get_label_font_size())
|
||||
if self.value:
|
||||
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||
self._label.set_alignment_vertical(TextAlignmentVertical.TOP)
|
||||
else:
|
||||
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||
self._label.set_alignment_vertical(TextAlignmentVertical.BOTTOM)
|
||||
|
||||
def set_text(self, text: str):
|
||||
self.text = text
|
||||
@@ -356,8 +356,8 @@ class GreyBigButton(BigButton):
|
||||
self._sub_label.set_font_size(36)
|
||||
self._sub_label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||
self._sub_label.set_font_weight(FontWeight.DISPLAY_REGULAR)
|
||||
self._sub_label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE if not self._label.text else
|
||||
rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||
self._sub_label.set_alignment_vertical(TextAlignmentVertical.MIDDLE if not self._label.text else
|
||||
TextAlignmentVertical.BOTTOM)
|
||||
self._sub_label.set_line_height(0.95)
|
||||
|
||||
@property
|
||||
|
||||
@@ -4,7 +4,7 @@ from dataclasses import dataclass
|
||||
from openpilot.cereal import messaging, log
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.common.hardware import COMMA_HARDWARE
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
@@ -76,10 +76,10 @@ class AlertRenderer(Widget):
|
||||
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||
|
||||
# font size is set dynamically
|
||||
self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||
self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||
self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=TextAlignment.CENTER,
|
||||
text_alignment_vertical=TextAlignmentVertical.TOP)
|
||||
self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=TextAlignment.CENTER,
|
||||
text_alignment_vertical=TextAlignmentVertical.TOP)
|
||||
|
||||
def get_alert(self, sm: messaging.SubMaster) -> Alert | None:
|
||||
"""Generate the current alert based on selfdrive state."""
|
||||
|
||||
@@ -4,7 +4,7 @@ from openpilot.cereal.visionipc import VisionStreamType
|
||||
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
|
||||
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
|
||||
@@ -38,7 +38,7 @@ class CabinCameraDialog(CameraView):
|
||||
tr("camera starting"),
|
||||
font_size=100,
|
||||
font_weight=FontWeight.BOLD,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment=TextAlignment.CENTER,
|
||||
)
|
||||
return -1
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.layouts.home import HomeLayout, HomeLayoutState, HEAD_BUTTON_FONT_SIZE, SPACING
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.multilang import tr, trn
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
@@ -59,7 +59,7 @@ class HomeLayoutSP(HomeLayout):
|
||||
desc_size = measure_text_cached(gui_app.font(FontWeight.NORMAL), description, BRAND_FONT_SIZE)
|
||||
desc_width = desc_size.x
|
||||
desc_rect = rl.Rectangle(version_right - desc_width, self.header_rect.y, desc_width, self.header_rect.height)
|
||||
gui_label(desc_rect, description, BRAND_FONT_SIZE, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||
gui_label(desc_rect, description, BRAND_FONT_SIZE, rl.WHITE, alignment=TextAlignment.RIGHT)
|
||||
|
||||
brand_size = measure_text_cached(gui_app.font(FontWeight.AUDIOWIDE), brand, BRAND_FONT_SIZE)
|
||||
spacing = BRAND_DESC_SPACING if description else 0
|
||||
|
||||
@@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import FontWeight
|
||||
from openpilot.system.ui.lib.application import FontWeight, TextAlignment
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||
@@ -20,7 +20,7 @@ class SunnylinkConsentPage(Widget):
|
||||
self._done_callback = done_callback
|
||||
self._step = 0
|
||||
|
||||
self._title = self._child(Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.AUDIOWIDE, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT))
|
||||
self._title = self._child(Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.AUDIOWIDE, text_alignment=TextAlignment.LEFT))
|
||||
|
||||
self._content = [
|
||||
{
|
||||
@@ -43,7 +43,7 @@ class SunnylinkConsentPage(Widget):
|
||||
self._primary_btn = self._child(Button("", button_style=ButtonStyle.PRIMARY, click_callback=lambda: self._handle_choice("enable")))
|
||||
self._secondary_btn = self._child(Button("", button_style=ButtonStyle.NORMAL, click_callback=lambda: self._handle_choice("secondary")))
|
||||
self._danger_btn = self._child(Button("", button_style=ButtonStyle.DANGER, click_callback=lambda: self._handle_choice("disable")))
|
||||
self._desc = self._child(Label("", font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT))
|
||||
self._desc = self._child(Label("", font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT))
|
||||
|
||||
def _handle_choice(self, choice):
|
||||
if choice == "enable":
|
||||
|
||||
@@ -9,7 +9,7 @@ from openpilot.cereal import custom
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.onboarding import SunnylinkConsentPage
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||
@@ -32,8 +32,8 @@ class SunnylinkHeader(Widget):
|
||||
font_size=90,
|
||||
font_weight=FontWeight.AUDIOWIDE,
|
||||
text_color=rl.WHITE,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||
alignment=TextAlignment.CENTER,
|
||||
alignment_vertical=TextAlignmentVertical.TOP,
|
||||
wrap_text=False,
|
||||
elide=False
|
||||
)
|
||||
@@ -43,8 +43,8 @@ class SunnylinkHeader(Widget):
|
||||
font_size=40,
|
||||
font_weight=FontWeight.NORMAL,
|
||||
text_color=rl.Color(0, 255, 0, 255), # Green
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||
alignment=TextAlignment.CENTER,
|
||||
alignment_vertical=TextAlignmentVertical.TOP,
|
||||
wrap_text=True,
|
||||
elide=False
|
||||
)
|
||||
@@ -55,8 +55,8 @@ class SunnylinkHeader(Widget):
|
||||
font_size=35,
|
||||
font_weight=FontWeight.NORMAL,
|
||||
text_color=rl.Color(255, 165, 0, 255), # Orange
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||
alignment=TextAlignment.CENTER,
|
||||
alignment_vertical=TextAlignmentVertical.TOP,
|
||||
wrap_text=True,
|
||||
elide=False
|
||||
)
|
||||
@@ -109,8 +109,8 @@ class SunnylinkDescriptionItem(Widget):
|
||||
font_size=40,
|
||||
font_weight=FontWeight.NORMAL,
|
||||
text_color=rl.WHITE,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||
alignment=TextAlignment.LEFT,
|
||||
alignment_vertical=TextAlignmentVertical.TOP,
|
||||
wrap_text=True,
|
||||
elide=False,
|
||||
)
|
||||
|
||||
@@ -11,7 +11,6 @@ import pyray as rl
|
||||
from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
|
||||
from openpilot.system.ui.lib.application import FontWeight
|
||||
from openpilot.system.ui.widgets.icon_widget import IconWidget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
|
||||
|
||||
@@ -19,16 +18,16 @@ class MiciHomeLayoutSP(MiciHomeLayout):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=88, font_weight=FontWeight.AUDIOWIDE, max_width=480, wrap_text=False)
|
||||
self._chestnut_loading_icon = IconWidget("icons_mici/chestnut.png", (68, 40))
|
||||
self._chestnut_loading_icon.set_visible(False)
|
||||
failed_idx = self._status_bar_layout.widgets.index(self._chestnut_failed_icon)
|
||||
self._status_bar_layout.widgets.insert(failed_idx + 1, self._chestnut_loading_icon)
|
||||
|
||||
def _set_chestnut_visibility(self):
|
||||
# stock has no loading tier: it shows green from the moment a big model is available. keep the
|
||||
# pulse so the status bar and the onroad HUD agree on what loading looks like.
|
||||
loading = ui_state.chestnut_state == ChestnutState.LOADING
|
||||
self._chestnut_loading_icon._opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))
|
||||
self._chestnut_loading_icon.set_visible(loading)
|
||||
self._chestnut_icon.set_visible(not loading and ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE))
|
||||
self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
|
||||
usb_connected = ui_state.usb_connected
|
||||
usb_unknown = ui_state.usb_unknown
|
||||
chestnut_state = ui_state.chestnut_state
|
||||
loading = chestnut_state == ChestnutState.LOADING
|
||||
|
||||
self._usb_icon.set_visible(usb_connected and usb_unknown)
|
||||
self._chestnut_loading_icon.set_opacity(0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)))
|
||||
self._chestnut_loading_icon.set_visible(not usb_unknown and loading)
|
||||
self._chestnut_icon.set_visible(not usb_unknown and not loading and
|
||||
chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE))
|
||||
self._chestnut_failed_icon.set_visible(not usb_unknown and chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
|
||||
|
||||
@@ -12,6 +12,7 @@ from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.ui.lib.prime_state import PrimeState
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.common.hardware import HARDWARE, PC
|
||||
from openpilot.common.hardware.usb import TYPEC_CC_ORIENTATION_PATH, get_usb_state, is_chestnut_usb_id, read_int
|
||||
from openpilot.selfdrive.modeld.helpers import chestnut_compiled
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP, DeviceSP
|
||||
@@ -95,6 +96,10 @@ class UIState(UIStateSP):
|
||||
self.chestnut_compiled: bool = chestnut_compiled()
|
||||
self.chestnut_active: bool | None = None
|
||||
self.chestnut_loading: bool = False
|
||||
self.usb_connected: bool = False
|
||||
self.usb_connected_ts: float | None = None
|
||||
self.usb_disconnected_ts: float | None = None
|
||||
self.usb_unknown: bool = False
|
||||
self.chestnut_state = ChestnutState.DISCONNECTED
|
||||
self.started: bool = False
|
||||
self.ignition: bool = False
|
||||
@@ -254,6 +259,23 @@ class UIState(UIStateSP):
|
||||
self.chestnut_compiled = chestnut_compiled()
|
||||
self.chestnut_active = self.params.get("ChestnutActive")
|
||||
self.chestnut_loading = self.params.get_bool("ChestnutLoading")
|
||||
now = time.monotonic()
|
||||
if read_int(TYPEC_CC_ORIENTATION_PATH) != 0:
|
||||
self.usb_disconnected_ts = None
|
||||
if not self.usb_connected:
|
||||
self.usb_connected = True
|
||||
self.usb_connected_ts = now
|
||||
self.usb_unknown = False
|
||||
elif self.usb_connected_ts is not None and now - self.usb_connected_ts > 10.:
|
||||
self.usb_unknown = not any(is_chestnut_usb_id(d["vendorId"], d["productId"], True) for d in get_usb_state())
|
||||
self.usb_connected_ts = None
|
||||
elif self.usb_connected:
|
||||
if self.usb_disconnected_ts is None:
|
||||
self.usb_disconnected_ts = now
|
||||
elif now - self.usb_disconnected_ts > PARAM_UPDATE_TIME:
|
||||
self.usb_connected = False
|
||||
self.usb_connected_ts = None
|
||||
self.usb_unknown = False
|
||||
|
||||
UIStateSP.update_params(self)
|
||||
|
||||
|
||||
@@ -8,22 +8,22 @@ See the LICENSE.md file in the root directory for more details.
|
||||
|
||||
import os
|
||||
os.environ['GMMU'] = '0'
|
||||
import numpy as np
|
||||
import threading
|
||||
import time
|
||||
from setproctitle import setproctitle
|
||||
from tinygrad.tensor import Tensor
|
||||
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from openpilot.common.hardware import COMMA_HARDWARE
|
||||
from openpilot.selfdrive.modeld.helpers import chestnut_present, load_oob
|
||||
import time
|
||||
import numpy as np
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from openpilot.cereal import log
|
||||
from opendbc.car.structs import car
|
||||
from openpilot.cereal.services import SERVICE_LIST
|
||||
from setproctitle import setproctitle
|
||||
from openpilot.cereal.messaging import PubMaster, SubMaster
|
||||
from openpilot.cereal.visionipc import VisionStreamType
|
||||
from msgq.visionipc import VisionIpcClient, VisionBuf
|
||||
from opendbc.car.car_helpers import get_demo_car_params
|
||||
|
||||
from tinygrad.tensor import Tensor
|
||||
|
||||
from openpilot.common.file_chunker import open_file_chunked
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.common.params import Params
|
||||
@@ -42,13 +42,13 @@ from openpilot.sunnypilot.modeld_v2.constants import Plan
|
||||
from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants
|
||||
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
|
||||
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues, make_supercombo_input_queues, WARP_INPUTS, POLICY_INPUTS
|
||||
|
||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
||||
from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
|
||||
|
||||
PROCESS_NAME = "openpilot.selfdrive.modeld.modeld_tinygrad"
|
||||
BIG_MODEL_TIMEOUT = 60
|
||||
|
||||
|
||||
def _pkl_exists(path):
|
||||
@@ -68,6 +68,7 @@ def _find_driving_pkl(bundle):
|
||||
pkl_path = os.path.join(model_root, pkl_name)
|
||||
if _pkl_exists(pkl_path):
|
||||
return pkl_path
|
||||
return None
|
||||
|
||||
|
||||
class FrameMeta:
|
||||
@@ -102,7 +103,7 @@ class ModelState(ModelStateBase):
|
||||
self.chestnut = chestnut
|
||||
|
||||
pkl_path = _find_driving_pkl(model_bundle)
|
||||
assert pkl_path is not None, "No driving pkl found — all models must be compiled with compile_modeld.py"
|
||||
assert pkl_path is not None, f"No driving pkl found for {'chestnut' if chestnut else 'small model'} — all models must be compiled with compile_modeld.py"
|
||||
self._init_combined(pkl_path, cam_w, cam_h, model_bundle)
|
||||
|
||||
def _init_combined(self, pkl_path, cam_w, cam_h, bundle):
|
||||
@@ -185,9 +186,6 @@ class ModelState(ModelStateBase):
|
||||
else:
|
||||
self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=frame_tensor, big_frame=big_frame_tensor)
|
||||
|
||||
if self.chestnut:
|
||||
self.warmup()
|
||||
|
||||
def warmup(self) -> None:
|
||||
dummy_frames = {k: np.zeros(self.frame_buf_params[k][3], dtype=np.uint8) for k in self._vision_input_names}
|
||||
transforms = {k: np.eye(3, dtype=np.float32) for k in [self._road_key, self._wide_key] if k}
|
||||
@@ -288,8 +286,7 @@ class ModelState(ModelStateBase):
|
||||
buf[0, -1, :] = outputs['desired_curvature'][0, :] if not self.mlsim else 0
|
||||
|
||||
if self.chestnut and not np.all(np.isfinite(outputs.get('plan', np.array([0.])))):
|
||||
cloudlog.error("model output not finite, dropping frame")
|
||||
return None
|
||||
raise RuntimeError("model output not finite")
|
||||
|
||||
return outputs
|
||||
|
||||
@@ -363,21 +360,26 @@ def main(demo=False):
|
||||
|
||||
model = None
|
||||
if CHESTNUT:
|
||||
import threading
|
||||
def load():
|
||||
nonlocal model
|
||||
model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, chestnut=True)
|
||||
t = threading.Thread(target=load, daemon=True)
|
||||
t.start()
|
||||
t.join(60)
|
||||
if model is None:
|
||||
params.put_bool("ChestnutActive", False)
|
||||
raise RuntimeError("chestnut model load failed or timed out (60s)")
|
||||
params.put_bool("ChestnutActive", True)
|
||||
else:
|
||||
model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, chestnut=False)
|
||||
big_model = None
|
||||
def load_big():
|
||||
nonlocal big_model
|
||||
try:
|
||||
m = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, chestnut=True)
|
||||
m.warmup()
|
||||
big_model = m
|
||||
except Exception:
|
||||
cloudlog.exception("chestnut load failed")
|
||||
loader = threading.Thread(target=load_big, daemon=True)
|
||||
loader.start()
|
||||
loader.join(BIG_MODEL_TIMEOUT)
|
||||
model = big_model
|
||||
params.put_bool("ChestnutActive", model is not None)
|
||||
|
||||
small_model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, chestnut=False) if model is None or CHESTNUT else None
|
||||
if model is None:
|
||||
model = small_model
|
||||
params.put_bool("ChestnutLoading", False)
|
||||
assert model is not None
|
||||
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
|
||||
|
||||
# messaging
|
||||
@@ -386,7 +388,7 @@ def main(demo=False):
|
||||
sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"])
|
||||
|
||||
publish_state = PublishState()
|
||||
chestnut_state = ChestnutState(pm, CHESTNUT) if CHESTNUT else None
|
||||
chestnut_state = ChestnutState(pm, model.chestnut) if CHESTNUT else None
|
||||
|
||||
# setup filter to track dropped frames
|
||||
frame_dropped_filter = FirstOrderFilter(0., 10., 1. / model.constants.MODEL_FREQ)
|
||||
@@ -509,7 +511,19 @@ def main(demo=False):
|
||||
inputs['action_t'] = np.array([lat_action_t, long_action_t], dtype=np.float32)
|
||||
|
||||
mt1 = time.perf_counter()
|
||||
model_output = model.run(bufs, transforms, inputs, prepare_only)
|
||||
try:
|
||||
model_output = model.run(bufs, transforms, inputs, prepare_only)
|
||||
except Exception:
|
||||
if not params.get_bool("ChestnutActive"):
|
||||
raise
|
||||
cloudlog.exception("chestnut failed, falling back to small")
|
||||
params.put_bool("ChestnutActive", False)
|
||||
assert small_model is not None
|
||||
model = small_model
|
||||
if chestnut_state is not None:
|
||||
chestnut_state.big = False
|
||||
run_count = 0
|
||||
model_output = None
|
||||
mt2 = time.perf_counter()
|
||||
model_execution_time = mt2 - mt1
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ class TestStockEquivalence(OpenpilotTestCase):
|
||||
|
||||
frame_skip = derive_frame_skip(SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES)
|
||||
stock_shapes = {**SPLIT_VISION_INPUT_SHAPES, **SPLIT_POLICY_INPUT_SHAPES, 'action_t': (1, 2)}
|
||||
stock_queues, stock_npy = make_input_queues(stock_shapes, frame_skip, device='NPY')
|
||||
stock_queues, stock_npy, _frame_views = make_input_queues(stock_shapes, frame_skip, device='NPY', frame_copy_size=49152)
|
||||
|
||||
assert set(state.input_queues.keys()) == set(stock_queues.keys())
|
||||
# sunnypilot split pipeline has tfm/big_tfm as queues (stock has them in npy only)
|
||||
assert set(stock_queues.keys()) <= set(state.input_queues.keys())
|
||||
assert {'desire', 'traffic_convention'} <= set(state.numpy_inputs.keys())
|
||||
assert set(state.numpy_inputs.keys()) == set(stock_npy.keys()) - {'action_t', 'prev_feat'}
|
||||
|
||||
def test_split_queue_keys_work_with_desire_key(self, model_state_factory):
|
||||
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues
|
||||
|
||||
@@ -7,6 +7,7 @@ See the LICENSE.md file in the root directory for more details.
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
@@ -248,23 +249,27 @@ class TestStockCompileModeldEquivalence(OpenpilotTestCase):
|
||||
input_shapes = {
|
||||
'img': (1, 12, 128, 256),
|
||||
'desire_pulse': (1, 25, 8),
|
||||
'features_buffer': (1, 24, 512), # when https://github.com/commaai/openpilot/pull/38681 merges, update to 1,24,32,512
|
||||
'features_buffer': (1, 24, 32, 512),
|
||||
'traffic_convention': (1, 2),
|
||||
'action_t': (1, 2),
|
||||
}
|
||||
frame_skip = 4
|
||||
|
||||
stock_queues, stock_npy = stock_make_input_queues(input_shapes, frame_skip, device='NPY')
|
||||
stock_queues, stock_npy, _frame_views = stock_make_input_queues(input_shapes, frame_skip, device='NPY', frame_copy_size=49152)
|
||||
sunny_queues, sunny_npy = sunny_make_supercombo_input_queues(input_shapes, frame_skip, device='NPY')
|
||||
assert set(sunny_queues.keys()) == set(stock_queues.keys())
|
||||
# sunnypilot split pipeline has tfm/big_tfm as queues; packed_npy_inputs size differs (different frame packing)
|
||||
assert set(stock_queues.keys()) <= set(sunny_queues.keys())
|
||||
for key in stock_queues:
|
||||
if key == 'packed_npy_inputs':
|
||||
continue
|
||||
assert sunny_queues[key].shape == stock_queues[key].shape, \
|
||||
f"Queue shape mismatch for {key}: sunny {sunny_queues[key].shape} != stock {stock_queues[key].shape}"
|
||||
assert set(sunny_npy.keys()) == set(stock_npy.keys())
|
||||
assert set(stock_npy.keys()) <= set(sunny_npy.keys())
|
||||
for key in stock_npy:
|
||||
assert sunny_npy[key].shape == stock_npy[key].shape, \
|
||||
f"Numpy array shape mismatch for {key}: sunny {sunny_npy[key].shape} != stock {stock_npy[key].shape}"
|
||||
|
||||
@unittest.skip("upstream removed make_warp_input_queues — warp merged into run_model")
|
||||
def test_make_warp_queues_stock_equivalence(self):
|
||||
from openpilot.selfdrive.modeld.compile_modeld import make_warp_input_queues as stock_make_warp_queues
|
||||
from openpilot.sunnypilot.modeld_v2.compile_modeld import make_warp_queues as sunny_make_warp_queues
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
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 io
|
||||
import requests
|
||||
|
||||
from openpilot.common.file_chunker import get_chunk_name
|
||||
from openpilot.common.hardware import hw
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.selfdrive.modeld.helpers import dump_oob
|
||||
import openpilot.sunnypilot.modeld_v2.modeld as modeld_module
|
||||
from openpilot.sunnypilot.modeld_v2.tests import helpers as tests_helpers
|
||||
from openpilot.sunnypilot.modeld_v2.tests.helpers import DummyModel, DummyBundle, CAM_W, CAM_H
|
||||
from openpilot.sunnypilot.models.fetcher import ModelParser, ModelFetcher
|
||||
|
||||
tmp_path = tests_helpers.tmp_path
|
||||
|
||||
|
||||
class TestFallback(OpenpilotTestCase):
|
||||
def test_find_dual_model_in_bundle(self, tmp_path, monkeypatch):
|
||||
lebowski_file = 'driving_lebowski.pkl'
|
||||
tsfdo_file = 'driving_tsfdo.pkl'
|
||||
(tmp_path / lebowski_file).write_bytes(b'fkasdjfkljf')
|
||||
(tmp_path / tsfdo_file).write_bytes(b'dskfajklsdjlsfka')
|
||||
|
||||
monkeypatch.setattr(hw.Paths, 'model_root', staticmethod(lambda: str(tmp_path)))
|
||||
big_bundle = DummyBundle(models=[DummyModel('supercombo', lebowski_file)])
|
||||
small_bundle = DummyBundle(models=[DummyModel('supercombo', tsfdo_file)])
|
||||
big_pkl = modeld_module._find_driving_pkl(big_bundle)
|
||||
small_pkl = modeld_module._find_driving_pkl(small_bundle)
|
||||
|
||||
assert big_pkl is not None and lebowski_file in big_pkl
|
||||
assert small_pkl is not None and tsfdo_file in small_pkl
|
||||
|
||||
def test_download_models_and_init_modelstate_fallback(self, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(hw.Paths, 'model_root', staticmethod(lambda: str(tmp_path)))
|
||||
big_json = requests.get(ModelFetcher.MODEL_URL_CHESTNUT).json()
|
||||
big_bundle = ModelParser.parse_models(big_json)[-1]
|
||||
small_json = requests.get(ModelFetcher.MODEL_URL).json()
|
||||
small_bundle = ModelParser.parse_models(small_json)[-1]
|
||||
|
||||
buf = io.BytesIO()
|
||||
dump_oob(tests_helpers.make_pkl_data(tests_helpers.ARCHETYPES['supercombo_non20hz']), buf)
|
||||
oob_bytes = buf.getvalue()
|
||||
|
||||
for bundle in (big_bundle, small_bundle):
|
||||
artifact = bundle.models[0].artifact
|
||||
for i in range(len(artifact.chunks)):
|
||||
(tmp_path / get_chunk_name(artifact.fileName, i, len(artifact.chunks))).write_bytes(oob_bytes if i == 0 else b"")
|
||||
|
||||
monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, chestnut=None: small_bundle)
|
||||
assert modeld_module.ModelState(CAM_W, CAM_H, chestnut=False).chestnut is False
|
||||
|
||||
monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, chestnut=None: big_bundle)
|
||||
try:
|
||||
assert modeld_module.ModelState(CAM_W, CAM_H, chestnut=True).chestnut is True
|
||||
except Exception as e:
|
||||
assert "AMD" in str(e) or "device" in str(e).lower()
|
||||
@@ -1,11 +1,14 @@
|
||||
import argparse
|
||||
import os
|
||||
import hashlib
|
||||
import requests
|
||||
import re
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.sunnypilot import get_file_hash
|
||||
from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_MODEL
|
||||
from openpilot.sunnypilot.models.fetcher import ModelFetcher
|
||||
|
||||
|
||||
def get_default_model() -> str:
|
||||
@@ -17,27 +20,70 @@ def get_default_model() -> str:
|
||||
|
||||
DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "model_name.py")
|
||||
MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "model_hash")
|
||||
BIG_MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "big_model_hash")
|
||||
SUPERCOMBO_ONNX_PATH = os.path.join(BASEDIR, "openpilot", "selfdrive", "modeld", "models", "driving_supercombo.onnx")
|
||||
BIG_SUPERCOMBO_ONNX_PATH = os.path.join(BASEDIR, "openpilot", "selfdrive", "modeld", "models", "big_driving_supercombo.onnx")
|
||||
|
||||
|
||||
def _read_model_name_fields():
|
||||
with open(DEFAULT_MODEL_NAME_PATH) as f:
|
||||
content = f.read()
|
||||
fields = {}
|
||||
for line in content.splitlines():
|
||||
if "=" in line:
|
||||
key, val = line.split("=", 1)
|
||||
fields[key.strip()] = val.strip().strip('"')
|
||||
return fields
|
||||
|
||||
|
||||
def update_model_hash():
|
||||
fields = _read_model_name_fields()
|
||||
supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH)
|
||||
combined_hash = hashlib.sha256(supercombo_hash.encode()).hexdigest()
|
||||
fingerprint = f"{supercombo_hash}:{fields.get('DEFAULT_MODEL', '')}:{fields.get('DEFAULT_MODEL_REF', '')}"
|
||||
combined_hash = hashlib.sha256(fingerprint.encode()).hexdigest()
|
||||
|
||||
with open(MODEL_HASH_PATH, "w") as f:
|
||||
f.write(combined_hash)
|
||||
|
||||
print(f"Generated and updated new combined model hash to {MODEL_HASH_PATH}")
|
||||
|
||||
if os.path.exists(BIG_SUPERCOMBO_ONNX_PATH):
|
||||
import subprocess
|
||||
rel = os.path.relpath(BIG_SUPERCOMBO_ONNX_PATH, os.getcwd())
|
||||
pointer = subprocess.check_output(["git", "show", f"HEAD:{rel}"], text=True)
|
||||
oid = next(l.split(":", 1)[1] for l in pointer.splitlines() if l.startswith("oid sha256:"))
|
||||
big_fingerprint = f"{oid}:{fields.get('DEFAULT_BIG_MODEL', '')}:{fields.get('DEFAULT_BIG_MODEL_REF', '')}"
|
||||
big_combined_hash = hashlib.sha256(big_fingerprint.encode()).hexdigest()
|
||||
|
||||
with open(BIG_MODEL_HASH_PATH, "w") as f:
|
||||
f.write(big_combined_hash)
|
||||
|
||||
print(f"Generated and updated new big model hash to {BIG_MODEL_HASH_PATH}")
|
||||
|
||||
|
||||
def get_ref_for_name(url: str, name: str) -> str:
|
||||
response = requests.get(url, timeout=10)
|
||||
if response.status_code == 200:
|
||||
bundles = response.json()["bundles"]
|
||||
matching = [b for b in bundles if re.search(name, f"{b['short_name']} {b['display_name']}", re.IGNORECASE)]
|
||||
if matching:
|
||||
return max(matching, key=lambda b: int(b["index"]))["ref"]
|
||||
return ""
|
||||
|
||||
|
||||
def update_default_model_names(default_model_name: str, default_big_model_name: str):
|
||||
print("[CHANGE DEFAULT MODEL NAMES]")
|
||||
small_ref = get_ref_for_name(ModelFetcher.MODEL_URL, default_model_name)
|
||||
big_ref = get_ref_for_name(ModelFetcher.MODEL_URL_CHESTNUT, default_big_model_name)
|
||||
|
||||
with open(DEFAULT_MODEL_NAME_PATH, "w") as f:
|
||||
f.write(f'DEFAULT_MODEL = "{default_model_name}"\n')
|
||||
f.write(f'DEFAULT_MODEL_REF = "{small_ref}"\n')
|
||||
f.write(f'DEFAULT_BIG_MODEL = "{default_big_model_name}"\n')
|
||||
f.write(f'DEFAULT_BIG_MODEL_REF = "{big_ref}"\n')
|
||||
|
||||
print(f'New default small model name: "{default_model_name}"')
|
||||
print(f'New default big model name: "{default_big_model_name}"')
|
||||
print(f'New default small model name: "{default_model_name}" (ref: {small_ref})')
|
||||
print(f'New default big model name: "{default_big_model_name}" (ref: {big_ref})')
|
||||
print("[DONE]")
|
||||
|
||||
|
||||
|
||||
@@ -138,8 +138,8 @@ class ModelCache:
|
||||
|
||||
class ModelFetcher:
|
||||
"""Handles fetching and caching of model data from remote source"""
|
||||
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v21.json"
|
||||
MODEL_URL_CHESTNUT = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_chestnut_v22.json"
|
||||
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v22.json"
|
||||
MODEL_URL_CHESTNUT = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_chestnut_v23.json"
|
||||
|
||||
MODEL_SOURCES = {
|
||||
"qcom": (MODEL_URL, ""),
|
||||
|
||||
@@ -19,7 +19,7 @@ from openpilot.common.hardware.hw import Paths
|
||||
from openpilot.selfdrive.modeld.helpers import chestnut_present
|
||||
|
||||
# SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO
|
||||
REQUIRED_JSON_VERSION = 18
|
||||
REQUIRED_JSON_VERSION = 19
|
||||
|
||||
CUSTOM_MODEL_PATH = Paths.model_root()
|
||||
METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl'
|
||||
|
||||
@@ -328,6 +328,12 @@ class ModelManagerSP:
|
||||
validate_active_bundles(self.params, self.source_models)
|
||||
self.active_bundle = get_active_bundle(self.params, chestnut=self.chestnut_present)
|
||||
|
||||
if get_selected_bundle(self.params, "chestnut") is not None and get_selected_bundle(self.params, "qcom") is None:
|
||||
if self.params.get("ModelManager_DownloadRef") is None:
|
||||
from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL_REF
|
||||
if DEFAULT_MODEL_REF:
|
||||
self.params.put("ModelManager_DownloadRef", DEFAULT_MODEL_REF)
|
||||
|
||||
self._process_download_requests()
|
||||
|
||||
if self.params.get("ModelManager_ClearCache"):
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
DEFAULT_MODEL = "CD210"
|
||||
DEFAULT_BIG_MODEL = "Lebowski"
|
||||
DEFAULT_MODEL_REF = "5b6436a90cf6902b8aaa71c2b6f3d7164d8ae391"
|
||||
DEFAULT_BIG_MODEL = "BMRLNAP Model v4"
|
||||
DEFAULT_BIG_MODEL_REF = "f877d7a0ccc3cce943c76e285214c020cd65c899"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
2c814f08a2c51323b87839fbf8d2c2a9853a2b5536271b3d67f7b7a2de7f9374
|
||||
@@ -1 +1 @@
|
||||
49133798d9cd9cacf47085c7ef8122bfee88cd9c6192a8314c81bfb1b37f5809
|
||||
c5be11d2fb1115be953c541f30c50f7c71a00bc4a0e128e19aa11b60689317fc
|
||||
@@ -5,19 +5,47 @@ 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
|
||||
import subprocess
|
||||
|
||||
from openpilot.sunnypilot import get_file_hash
|
||||
from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, SUPERCOMBO_ONNX_PATH
|
||||
from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, SUPERCOMBO_ONNX_PATH, BIG_MODEL_HASH_PATH, \
|
||||
BIG_SUPERCOMBO_ONNX_PATH, _read_model_name_fields
|
||||
import hashlib
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
|
||||
|
||||
def _get_lfs_oid(path: str) -> str:
|
||||
"""Extract the LFS OID (SHA256 of actual content) from git, works whether the file is smudged or not."""
|
||||
pointer = subprocess.check_output(["git", "show", f"HEAD:{path}"], text=True)
|
||||
for line in pointer.splitlines():
|
||||
if line.startswith("oid sha256:"):
|
||||
return line.split(":", 1)[1]
|
||||
raise ValueError(f"No LFS OID found for {path}")
|
||||
|
||||
|
||||
class TestDefaultModel(OpenpilotTestCase):
|
||||
def test_compare_onnx_hashes(self):
|
||||
fields = _read_model_name_fields()
|
||||
supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH)
|
||||
|
||||
combined_hash = hashlib.sha256(supercombo_hash.encode()).hexdigest()
|
||||
fingerprint = f"{supercombo_hash}:{fields.get('DEFAULT_MODEL', '')}:{fields.get('DEFAULT_MODEL_REF', '')}"
|
||||
combined_hash = hashlib.sha256(fingerprint.encode()).hexdigest()
|
||||
|
||||
with open(MODEL_HASH_PATH) as f:
|
||||
current_hash = f.read().strip()
|
||||
|
||||
assert combined_hash == current_hash, "Run openpilot/sunnypilot/models/default_model.py to update the default model name and hash"
|
||||
|
||||
def test_compare_big_onnx_hashes(self):
|
||||
if not os.path.exists(BIG_SUPERCOMBO_ONNX_PATH):
|
||||
self.skipTest("big_driving_supercombo.onnx not present")
|
||||
|
||||
fields = _read_model_name_fields()
|
||||
oid = _get_lfs_oid(os.path.relpath(BIG_SUPERCOMBO_ONNX_PATH, os.getcwd()))
|
||||
big_fingerprint = f"{oid}:{fields.get('DEFAULT_BIG_MODEL', '')}:{fields.get('DEFAULT_BIG_MODEL_REF', '')}"
|
||||
combined_hash = hashlib.sha256(big_fingerprint.encode()).hexdigest()
|
||||
|
||||
with open(BIG_MODEL_HASH_PATH) as f:
|
||||
current_hash = f.read().strip()
|
||||
|
||||
assert combined_hash == current_hash, "Run openpilot/sunnypilot/models/default_model.py to update the default model name and hash"
|
||||
|
||||
@@ -386,7 +386,7 @@ class TestManagerDownload(ManagerDownloadTestBase):
|
||||
def body():
|
||||
artifact = self.make_artifact(chunked=True)
|
||||
self._bundle.ref = "test-ref"
|
||||
self._bundle.minimumSelectorVersion = 18
|
||||
self._bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION
|
||||
params, store = self._make_params_with_store()
|
||||
self.manager.params = params
|
||||
asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "qcom"))
|
||||
@@ -406,7 +406,7 @@ class TestManagerDownload(ManagerDownloadTestBase):
|
||||
def body():
|
||||
self.make_artifact(chunked=True)
|
||||
self._bundle.ref = "big-ref"
|
||||
self._bundle.minimumSelectorVersion = 18
|
||||
self._bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION
|
||||
params, store = self._make_params_with_store()
|
||||
self.manager.params = params
|
||||
asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "chestnut"))
|
||||
@@ -469,7 +469,7 @@ def manifest_bundle(short_name: str, ref: str, index: int = 0, is_big: bool = Fa
|
||||
"environment": "release",
|
||||
"runner": "tinygrad",
|
||||
"is_big": is_big,
|
||||
"minimum_selector_version": "18",
|
||||
"minimum_selector_version": str(helpers.REQUIRED_JSON_VERSION),
|
||||
"ref": ref,
|
||||
"models": [{
|
||||
"type": "supercombo",
|
||||
@@ -655,7 +655,7 @@ class TestActiveBundleValidation(OpenpilotTestCase):
|
||||
def _raw_bundle(ref: str, runner: int | None = None) -> dict:
|
||||
bundle = custom.ModelManagerSP.ModelBundle.new_message()
|
||||
bundle.ref = ref
|
||||
bundle.minimumSelectorVersion = 18
|
||||
bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION
|
||||
if runner is not None:
|
||||
bundle.runner = runner
|
||||
return bundle.to_dict()
|
||||
@@ -697,7 +697,7 @@ class TestActiveBundleSelection(OpenpilotTestCase):
|
||||
def _raw_bundle(ref: str) -> dict:
|
||||
bundle = custom.ModelManagerSP.ModelBundle.new_message()
|
||||
bundle.ref = ref
|
||||
bundle.minimumSelectorVersion = 18
|
||||
bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION
|
||||
return bundle.to_dict()
|
||||
|
||||
def _params(self, qcom=None, chestnut=None):
|
||||
@@ -744,7 +744,7 @@ class TestEffectiveSource(OpenpilotTestCase):
|
||||
def _raw_bundle(ref: str) -> dict:
|
||||
bundle = custom.ModelManagerSP.ModelBundle.new_message()
|
||||
bundle.ref = ref
|
||||
bundle.minimumSelectorVersion = 18
|
||||
bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION
|
||||
return bundle.to_dict()
|
||||
|
||||
def test_runtime_no_gpu(self):
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import time
|
||||
|
||||
from openpilot.common.hardware.usb import CHESTNUT_USB_PRODUCT, is_chestnut_usb_id
|
||||
from openpilot.common.version import get_build_metadata, CHESTNUT_BRANCHES
|
||||
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, chestnut_compiled
|
||||
|
||||
|
||||
CHESTNUT_RELEASE_BRANCHES = ("release-chestnut", "release-chestnut-staging")
|
||||
CHESTNUT_POWERED_VOLTAGE = 5000
|
||||
GPU_TEMP_LIMIT = 100.
|
||||
MEMORY_TEMP_LIMIT = 95.
|
||||
TEMP_HYSTERESIS = 5.
|
||||
|
||||
|
||||
class ChestnutStatus:
|
||||
def __init__(self):
|
||||
self.started = time.monotonic()
|
||||
self.offroad = True
|
||||
self.pcie_failed = False
|
||||
self.power_seen = False
|
||||
self.power_unavailable = False
|
||||
self.power_lost = False
|
||||
self.power_restored = False
|
||||
self.link_failures = 0
|
||||
self.model_loading_seen = False
|
||||
self.model_attempted = False
|
||||
self.overheated = False
|
||||
self.usb_seen = False
|
||||
self.usb_failed = False
|
||||
|
||||
def update(self, offroad: bool, branch: str, usb_state: list[dict], firmware_failed: bool,
|
||||
model_loading: bool, model_active: bool | None, state, set_alert) -> None:
|
||||
detected = [d for d in usb_state if is_chestnut_usb_id(d["vendorId"], d["productId"], include_bootloader=True)]
|
||||
devices = [d for d in detected if is_chestnut_usb_id(d["vendorId"], d["productId"])]
|
||||
firmware_ok = len(devices) == 1 and devices[0]["product"] == CHESTNUT_USB_PRODUCT
|
||||
|
||||
if self.offroad and not offroad:
|
||||
self.pcie_failed = False
|
||||
self.power_seen = False
|
||||
self.power_unavailable = False
|
||||
self.power_lost = False
|
||||
self.power_restored = False
|
||||
self.link_failures = 0
|
||||
self.model_loading_seen = False
|
||||
self.model_attempted = False
|
||||
self.usb_seen = firmware_ok
|
||||
self.usb_failed = False
|
||||
|
||||
self.model_loading_seen |= model_loading
|
||||
self.model_attempted |= self.model_loading_seen and not model_loading and model_active is not None
|
||||
|
||||
if not offroad and self.usb_seen and not firmware_ok:
|
||||
self.usb_failed = True
|
||||
|
||||
if not offroad and state is not None:
|
||||
powered = state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE
|
||||
power_lost = state.supplyFault or not powered
|
||||
if self.model_attempted and power_lost and not self.power_lost:
|
||||
self.power_unavailable = not self.power_seen
|
||||
self.power_seen |= powered
|
||||
|
||||
if not offroad and self.model_attempted and state is not None:
|
||||
self.link_failures = self.link_failures + 1 if state.pcieLtssm != 0x78 else 0
|
||||
self.pcie_failed |= self.link_failures >= 2 or power_lost
|
||||
self.power_lost |= power_lost
|
||||
|
||||
if self.pcie_failed and self.power_lost and state is not None:
|
||||
self.power_restored |= not state.supplyFault and state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE
|
||||
if self.usb_failed:
|
||||
self.pcie_failed = False
|
||||
self.power_seen = False
|
||||
self.power_unavailable = False
|
||||
self.power_lost = False
|
||||
self.power_restored = False
|
||||
|
||||
if state is not None:
|
||||
gpu_limit = GPU_TEMP_LIMIT - (TEMP_HYSTERESIS if self.overheated else 0.)
|
||||
memory_limit = MEMORY_TEMP_LIMIT - (TEMP_HYSTERESIS if self.overheated else 0.)
|
||||
self.overheated = state.tempC >= gpu_limit or state.memoryTempC >= memory_limit
|
||||
|
||||
release = branch in CHESTNUT_RELEASE_BRANCHES
|
||||
missing = self.usb_failed or (offroad and release and time.monotonic() - self.started > 10. and len(detected) != 1)
|
||||
slow_usb = offroad and len(devices) == 1 and devices[0]["speedMbps"] < 5000
|
||||
big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or chestnut_compiled()
|
||||
current_channel = get_build_metadata().channel
|
||||
chestnut_target = CHESTNUT_BRANCHES.get(current_channel)
|
||||
chestnut_needs_switch = len(devices) == 1 and not big_model_available and chestnut_target is not None
|
||||
set_alert("Offroad_ChestnutBranch", chestnut_needs_switch, chestnut_target if chestnut_needs_switch else None)
|
||||
set_alert("Offroad_ChestnutNotDetected", missing)
|
||||
set_alert("Offroad_ChestnutOverheated", self.overheated, f"{state.tempC:.0f} °C" if state is not None else None)
|
||||
set_alert("Offroad_ChestnutUsbSlow", slow_usb, f"{devices[0]['speedMbps']} Mbps" if slow_usb else None)
|
||||
if self.power_lost:
|
||||
pcie_alert = ("Chestnut power restored. 12V is stable again, cycle ignition." if self.power_restored else
|
||||
"Chestnut power disconnected. Check 12V connection, then cycle ignition." if self.power_unavailable else
|
||||
"Chestnut power lost. Possibly caused by an engine-crank voltage drop. Check 12V connection, then cycle ignition.")
|
||||
else:
|
||||
pcie_alert = "Chestnut GPU unavailable. PCIe link is not up. Check the GPU is securely seated."
|
||||
set_alert("Offroad_ChestnutPcieUnavailable", self.pcie_failed, pcie_alert)
|
||||
set_alert("Offroad_ChestnutUncompiled", offroad and firmware_ok and not chestnut_compiled())
|
||||
set_alert("Offroad_ChestnutUpdateFailed", offroad and firmware_failed)
|
||||
self.offroad = offroad
|
||||
@@ -16,19 +16,19 @@ from openpilot.common.utils import strip_deprecated_keys
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_HW
|
||||
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, chestnut_compiled
|
||||
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
|
||||
from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.hardware.usb import CHESTNUT_FW_VERSION, CHESTNUT_ROM_USB_IDS, CHESTNUT_USB_IDS, get_usb_state, get_usb_topology, set_usb_state
|
||||
from openpilot.common.git import get_short_branch
|
||||
from openpilot.common.hardware.usb import CHESTNUT_FW_VERSION, CHESTNUT_USB_PRODUCT, get_usb_state, get_usb_topology, is_chestnut_usb_id, set_usb_state
|
||||
from openpilot.common.linux import LinuxSystemStats
|
||||
from openpilot.system.loggerd.config import get_available_percent
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.sunnypilot.system.statsd import statlog
|
||||
from openpilot.system.hardware.power_monitoring import PowerMonitoring
|
||||
from openpilot.system.hardware.fan_controller import FanController
|
||||
from openpilot.common.version import terms_version, training_version, get_build_metadata, terms_version_sp, CHESTNUT_BRANCHES
|
||||
|
||||
from openpilot.system.hardware.chestnut.status import ChestnutStatus
|
||||
from openpilot.common.version import terms_version, training_version, get_build_metadata, terms_version_sp
|
||||
|
||||
ThermalStatus = log.DeviceState.ThermalStatus
|
||||
NetworkType = log.DeviceState.NetworkType
|
||||
@@ -49,6 +49,11 @@ class Chestnut:
|
||||
self.attempts = 0
|
||||
self.last_attempt = 0.
|
||||
self.flashed = False
|
||||
self.mismatch = False
|
||||
|
||||
@property
|
||||
def failed(self) -> bool:
|
||||
return self.mismatch and self.attempts >= self.MAX_ATTEMPTS and self.thread is not None and not self.thread.is_alive() and not self.flashed
|
||||
|
||||
def flash(self) -> None:
|
||||
ret = subprocess.run(["sudo", sys.executable, os.path.join(BASEDIR, "openpilot/system/hardware/chestnut/flash.py"), CHESTNUT_FW_VERSION],
|
||||
@@ -57,9 +62,9 @@ class Chestnut:
|
||||
self.flashed = ret.returncode == 0
|
||||
|
||||
def update(self, offroad: bool, usb_state: list[dict]) -> None:
|
||||
mismatch = any((d["vendorId"], d["productId"]) in CHESTNUT_USB_IDS + CHESTNUT_ROM_USB_IDS and
|
||||
d["product"] != f"custom {CHESTNUT_FW_VERSION}-CLEAN" for d in usb_state)
|
||||
if not mismatch:
|
||||
self.mismatch = any(is_chestnut_usb_id(d["vendorId"], d["productId"], include_bootloader=True) and
|
||||
d["product"] != CHESTNUT_USB_PRODUCT for d in usb_state)
|
||||
if not self.mismatch:
|
||||
self.flashed = False
|
||||
return
|
||||
|
||||
@@ -191,7 +196,7 @@ def hw_state_thread(end_event, hw_queue):
|
||||
def hardware_thread(end_event, hw_queue) -> None:
|
||||
system_stats = LinuxSystemStats()
|
||||
pm = messaging.PubMaster(['deviceState'])
|
||||
sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates"], poll="pandaStates")
|
||||
sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates", "chestnutState"], poll="pandaStates")
|
||||
|
||||
count = 0
|
||||
|
||||
@@ -239,7 +244,8 @@ def hardware_thread(end_event, hw_queue) -> None:
|
||||
|
||||
fan_controller = FanController(int(1./DT_HW))
|
||||
chestnut = Chestnut()
|
||||
big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or chestnut_compiled()
|
||||
chestnut_status = ChestnutStatus()
|
||||
branch = get_short_branch()
|
||||
|
||||
while not end_event.is_set():
|
||||
sm.update(PANDA_STATES_TIMEOUT)
|
||||
@@ -301,12 +307,11 @@ def hardware_thread(end_event, hw_queue) -> None:
|
||||
|
||||
set_usb_state(msg.deviceState, last_hw_state.usb_state)
|
||||
chestnut.update(started_ts is None, last_hw_state.usb_state)
|
||||
current_channel = get_build_metadata().channel
|
||||
chestnut_target = CHESTNUT_BRANCHES.get(current_channel)
|
||||
chestnut_needs_switch = msg.deviceState.chestnutPresent and not big_model_available and chestnut_target is not None
|
||||
set_offroad_alert_if_changed("Offroad_ChestnutBranch", chestnut_needs_switch,
|
||||
extra_text=chestnut_target if chestnut_needs_switch else None)
|
||||
|
||||
chestnut_state = sm["chestnutState"]
|
||||
chestnut_valid = sm.alive["chestnutState"] and sm.valid["chestnutState"]
|
||||
chestnut_status.update(started_ts is None, branch, last_hw_state.usb_state, chestnut.failed,
|
||||
params.get_bool("ChestnutLoading"), params.get("ChestnutActive"),
|
||||
chestnut_state if chestnut_valid else None, set_offroad_alert_if_changed)
|
||||
# this subset is only used for offroad
|
||||
temp_sources = [
|
||||
msg.deviceState.memoryTempC,
|
||||
|
||||
@@ -13,7 +13,7 @@ import subprocess
|
||||
from contextlib import contextmanager
|
||||
from collections.abc import Callable
|
||||
from collections import deque
|
||||
from enum import StrEnum
|
||||
from enum import IntEnum, StrEnum
|
||||
from pathlib import Path
|
||||
from typing import NamedTuple
|
||||
from importlib.resources import as_file, files
|
||||
@@ -118,6 +118,18 @@ class FontWeight(StrEnum):
|
||||
DISPLAY = "Inter-Bold.ttf"
|
||||
|
||||
|
||||
class TextAlignment(IntEnum):
|
||||
LEFT = 0
|
||||
CENTER = 1
|
||||
RIGHT = 2
|
||||
|
||||
|
||||
class TextAlignmentVertical(IntEnum):
|
||||
TOP = 0
|
||||
MIDDLE = 1
|
||||
BOTTOM = 2
|
||||
|
||||
|
||||
def font_fallback(font: rl.Font) -> rl.Font:
|
||||
"""Use a Noto fallback for languages not covered by Inter."""
|
||||
if multilang.requires_font_fallback():
|
||||
@@ -335,7 +347,6 @@ class GuiApplication(GuiApplicationExt):
|
||||
rl.set_target_fps(0 if OFFSCREEN or vblank_control else fps)
|
||||
|
||||
self._target_fps = fps
|
||||
self._set_styles()
|
||||
self._load_fonts()
|
||||
self._patch_text_functions()
|
||||
self._patch_scissor_mode()
|
||||
@@ -739,14 +750,6 @@ class GuiApplication(GuiApplicationExt):
|
||||
self._fonts[font_weight_file] = font
|
||||
if multilang.requires_font_fallback():
|
||||
self.fallback_font()
|
||||
rl.gui_set_font(self._fonts[FontWeight.NORMAL])
|
||||
|
||||
def _set_styles(self):
|
||||
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BORDER_WIDTH, 0)
|
||||
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, DEFAULT_TEXT_SIZE)
|
||||
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.BACKGROUND_COLOR, rl.color_to_int(rl.BLACK))
|
||||
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(DEFAULT_TEXT_COLOR))
|
||||
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BASE_COLOR_NORMAL, rl.color_to_int(rl.Color(50, 50, 50, 255)))
|
||||
|
||||
def _patch_text_functions(self):
|
||||
# Wrap pyray text APIs to apply a global text size scale so our px sizes match Qt
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import pyray as rl
|
||||
from collections.abc import Sequence
|
||||
|
||||
|
||||
class GuiStyleContext:
|
||||
def __init__(self, styles: Sequence[tuple[int, int, int]]):
|
||||
"""styles is a list of tuples (control, prop, new_value)"""
|
||||
self.styles = styles
|
||||
self.prev_styles: list[tuple[int, int, int]] = []
|
||||
|
||||
def __enter__(self):
|
||||
for control, prop, new_value in self.styles:
|
||||
prev_value = rl.gui_get_style(control, prop)
|
||||
self.prev_styles.append((control, prop, prev_value))
|
||||
rl.gui_set_style(control, prop, new_value)
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
for control, prop, prev_value in self.prev_styles:
|
||||
rl.gui_set_style(control, prop, prev_value)
|
||||
@@ -18,7 +18,7 @@ from openpilot.common.realtime import config_realtime_process, set_core_affinity
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.common.time_helpers import system_time_valid
|
||||
from openpilot.common.utils import run_cmd
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, ConnectStatus
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
@@ -105,8 +105,8 @@ class StartPage(Widget):
|
||||
super().__init__()
|
||||
|
||||
self._title = UnifiedLabel("start", 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
||||
font_weight=FontWeight.DISPLAY, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
font_weight=FontWeight.DISPLAY, alignment=TextAlignment.CENTER,
|
||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
||||
|
||||
self._start_bg_txt = gui_app.texture("icons_mici/setup/start_button.png", 500, 224, keep_aspect_ratio=False)
|
||||
self._start_bg_pressed_txt = gui_app.texture("icons_mici/setup/start_button_pressed.png", 500, 224, keep_aspect_ratio=False)
|
||||
@@ -197,7 +197,7 @@ class DownloadingPage(NavWidget):
|
||||
self._title_label = UnifiedLabel("downloading...", 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
||||
font_weight=FontWeight.DISPLAY)
|
||||
self._progress_label = UnifiedLabel("", 132, text_color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)),
|
||||
font_weight=FontWeight.ROMAN, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||
font_weight=FontWeight.ROMAN, alignment_vertical=TextAlignmentVertical.BOTTOM)
|
||||
self._progress = 0
|
||||
|
||||
def _back_enabled(self) -> bool:
|
||||
@@ -261,8 +261,8 @@ class BigPillButton(BigButton):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._label.set_font_size(48)
|
||||
self._label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
self._label.set_alignment(TextAlignment.CENTER)
|
||||
self._label.set_alignment_vertical(TextAlignmentVertical.MIDDLE)
|
||||
|
||||
def _load_images(self):
|
||||
if self._green:
|
||||
|
||||
@@ -7,7 +7,7 @@ import pyray as rl
|
||||
from openpilot.common.realtime import config_realtime_process, set_core_affinity
|
||||
from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignmentVertical
|
||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
@@ -30,7 +30,7 @@ class ProgressPage(NavWidget):
|
||||
font_weight=FontWeight.DISPLAY, line_height=0.8)
|
||||
self._progress_percent_label = UnifiedLabel("", 132, text_color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)),
|
||||
font_weight=FontWeight.ROMAN,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||
alignment_vertical=TextAlignmentVertical.BOTTOM)
|
||||
|
||||
def _back_enabled(self) -> bool:
|
||||
return False
|
||||
|
||||
@@ -8,7 +8,7 @@ from collections.abc import Callable
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignmentVertical
|
||||
from openpilot.system.ui.sunnypilot.lib.styles import style
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import ButtonActionSP
|
||||
from openpilot.system.ui.widgets.label import ScrollState, UnifiedLabel
|
||||
@@ -40,7 +40,7 @@ class ScrollingButtonAction(ButtonActionSP):
|
||||
super().__init__(text=text, width=width, enabled=enabled)
|
||||
self._value_label = UnifiedLabelSP("", font_size=style.ITEM_TEXT_FONT_SIZE, font_weight=FontWeight.NORMAL,
|
||||
text_color=self._value_color, scroll=True,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
||||
|
||||
def set_value(self, value: str | Callable[[], str], color: rl.Color = style.ITEM_TEXT_VALUE_COLOR):
|
||||
if self.value != _resolve_value(value, ""):
|
||||
|
||||
@@ -9,6 +9,8 @@ import math
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.lib.application import TextAlignment, TextAlignmentVertical
|
||||
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
|
||||
@@ -61,11 +63,11 @@ class DownloadStatusAction(ItemAction):
|
||||
self._sweep = 0.0
|
||||
|
||||
self._name_label = UnifiedLabel("", font_size=FONT_SIZE, font_weight=FontWeight.NORMAL, text_color=TEXT_COLOR,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
alignment=TextAlignment.LEFT,
|
||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
||||
self._percent_label = UnifiedLabel("", font_size=FONT_SIZE, font_weight=FontWeight.NORMAL, text_color=TEXT_COLOR,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
alignment=TextAlignment.RIGHT,
|
||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
||||
|
||||
def update(self, name, downloading=False, progress=0.0, status_text="", text_color=rl.GRAY, icon=None, icon_color=None, segments=None):
|
||||
self.segments = segments
|
||||
|
||||
@@ -8,7 +8,7 @@ from collections.abc import Callable, Sequence
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight, TextAlignment, TextAlignmentVertical
|
||||
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 import Widget
|
||||
@@ -90,8 +90,8 @@ class ButtonActionSP(ButtonAction):
|
||||
if value_text:
|
||||
value_rect = rl.Rectangle(rect.x, rect.y, rect.width - BUTTON_WIDTH - TEXT_PADDING, rect.height)
|
||||
gui_label(value_rect, value_text, font_size=style.ITEM_TEXT_FONT_SIZE, color=self._value_color,
|
||||
font_weight=FontWeight.NORMAL, alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
font_weight=FontWeight.NORMAL, alignment=TextAlignment.LEFT,
|
||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
||||
|
||||
pressed = self._pressed
|
||||
self._pressed = False
|
||||
@@ -319,7 +319,7 @@ class ListItemSP(ListItem):
|
||||
)
|
||||
if value_rect.width > 0:
|
||||
gui_label(value_rect, value_text, font_size=style.ITEM_TEXT_FONT_SIZE, color=self._right_value_color, font_weight=FontWeight.NORMAL,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
alignment=TextAlignment.RIGHT, alignment_vertical=TextAlignmentVertical.MIDDLE)
|
||||
|
||||
# Render toggle and handle callback
|
||||
if self.action_item.render(left_rect) and self.action_item.enabled:
|
||||
|
||||
@@ -8,6 +8,7 @@ from dataclasses import dataclass, field
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.lib.application import TextAlignment
|
||||
from openpilot.system.ui.lib.application import FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import DialogResult
|
||||
@@ -35,7 +36,7 @@ class TreeFolder:
|
||||
|
||||
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,
|
||||
super().__init__(text, click_callback, button_style=ButtonStyle.NORMAL, text_alignment=TextAlignment.LEFT,
|
||||
text_padding=20 + indent_level * 30, elide_right=True)
|
||||
self.text = text
|
||||
self.ref = ref
|
||||
|
||||
@@ -11,7 +11,7 @@ from openpilot.common.hardware import PC
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||
from openpilot.system.ui.widgets.label import gui_label, gui_text_box
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
|
||||
|
||||
USERDATA = "/dev/disk/by-partlabel/userdata"
|
||||
TIMEOUT = 3*60
|
||||
@@ -38,6 +38,7 @@ class Reset(Widget):
|
||||
self._cancel_button = Button("Cancel", gui_app.request_close)
|
||||
self._confirm_button = Button("Confirm", self._confirm, button_style=ButtonStyle.PRIMARY)
|
||||
self._reboot_button = Button("Reboot", self._reboot)
|
||||
self._body_label = UnifiedLabel(self._get_body_text, 90, line_height=1 / 0.9)
|
||||
|
||||
@staticmethod
|
||||
def _reboot() -> None:
|
||||
@@ -75,7 +76,7 @@ class Reset(Widget):
|
||||
gui_label(label_rect, "System Reset", 100, font_weight=FontWeight.BOLD)
|
||||
|
||||
text_rect = rl.Rectangle(content_rect.x + 140, content_rect.y + 140, content_rect.width - 280, content_rect.height - 90 - 100 * FONT_SCALE)
|
||||
gui_text_box(text_rect, self._get_body_text(), 90)
|
||||
self._body_label.render(text_rect)
|
||||
|
||||
button_height = 160
|
||||
button_spacing = 50
|
||||
|
||||
@@ -13,7 +13,7 @@ import pyray as rl
|
||||
from openpilot.cereal import log
|
||||
from openpilot.common.hardware import HARDWARE
|
||||
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE, TextAlignment, TextAlignmentVertical
|
||||
from openpilot.system.ui.widgets import DialogResult, Widget
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle, ButtonRadio
|
||||
from openpilot.system.ui.widgets.keyboard import Keyboard
|
||||
@@ -67,17 +67,17 @@ class Setup(Widget):
|
||||
self.warning = gui_app.texture("icons/warning.png", 150, 150)
|
||||
self.checkmark = gui_app.texture("icons/circled_check.png", 100, 100)
|
||||
|
||||
self._low_voltage_title_label = Label("WARNING: Low Voltage", TITLE_FONT_SIZE, FontWeight.MEDIUM, rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
self._low_voltage_title_label = Label("WARNING: Low Voltage", TITLE_FONT_SIZE, FontWeight.MEDIUM, TextAlignment.LEFT,
|
||||
text_color=rl.Color(255, 89, 79, 255), text_padding=20)
|
||||
self._low_voltage_body_label = Label("Power your device in a car with a harness or proceed at your own risk.", BODY_FONT_SIZE,
|
||||
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20)
|
||||
text_alignment=TextAlignment.LEFT, text_padding=20)
|
||||
self._low_voltage_continue_button = Button("Continue", self._low_voltage_continue_button_callback)
|
||||
self._low_voltage_poweroff_button = Button("Power Off", HARDWARE.shutdown)
|
||||
|
||||
self._getting_started_button = Button("", self._getting_started_button_callback, button_style=ButtonStyle.PRIMARY, border_radius=0)
|
||||
self._getting_started_title_label = Label("Getting Started", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20)
|
||||
self._getting_started_title_label = Label("Getting Started", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20)
|
||||
self._getting_started_body_label = Label("Before we get on the road, let's finish installation and cover some details.",
|
||||
BODY_FONT_SIZE, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20)
|
||||
BODY_FONT_SIZE, text_alignment=TextAlignment.LEFT, text_padding=20)
|
||||
|
||||
self._software_selection_openpilot_button = ButtonRadio("openpilot", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80)
|
||||
self._software_selection_custom_software_button = ButtonRadio("Custom Software", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80)
|
||||
@@ -85,32 +85,32 @@ class Setup(Widget):
|
||||
button_style=ButtonStyle.PRIMARY)
|
||||
self._software_selection_continue_button.set_enabled(False)
|
||||
self._software_selection_back_button = Button("Back", self._software_selection_back_button_callback)
|
||||
self._software_selection_title_label = Label("Choose Software to Use", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
self._software_selection_title_label = Label("Choose Software to Use", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT,
|
||||
text_padding=20)
|
||||
|
||||
self._download_failed_reboot_button = Button("Reboot device", HARDWARE.reboot)
|
||||
self._download_failed_startover_button = Button("Start over", self._download_failed_startover_button_callback, button_style=ButtonStyle.PRIMARY)
|
||||
self._download_failed_title_label = Label("Download Failed", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20)
|
||||
self._download_failed_url_label = Label("", 52, FontWeight.NORMAL, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20)
|
||||
self._download_failed_body_label = Label("", BODY_FONT_SIZE, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20)
|
||||
self._download_failed_title_label = Label("Download Failed", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20)
|
||||
self._download_failed_url_label = Label("", 52, FontWeight.NORMAL, TextAlignment.LEFT, text_padding=20)
|
||||
self._download_failed_body_label = Label("", BODY_FONT_SIZE, text_alignment=TextAlignment.LEFT, text_padding=20)
|
||||
|
||||
self._network_setup_back_button = Button("Back", self._network_setup_back_button_callback)
|
||||
self._network_setup_continue_button = Button("Waiting for internet", self._network_setup_continue_button_callback,
|
||||
button_style=ButtonStyle.PRIMARY)
|
||||
self._network_setup_continue_button.set_enabled(False)
|
||||
self._network_setup_title_label = Label("Connect to Wi-Fi", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20)
|
||||
self._network_setup_title_label = Label("Connect to Wi-Fi", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20)
|
||||
|
||||
self._custom_software_warning_continue_button = Button("Scroll to continue", self._custom_software_warning_continue_button_callback,
|
||||
button_style=ButtonStyle.PRIMARY)
|
||||
self._custom_software_warning_continue_button.set_enabled(False)
|
||||
self._custom_software_warning_back_button = Button("Back", self._custom_software_warning_back_button_callback)
|
||||
self._custom_software_warning_title_label = Label("WARNING: Custom Software", 81, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
self._custom_software_warning_title_label = Label("WARNING: Custom Software", 81, FontWeight.BOLD, TextAlignment.LEFT,
|
||||
text_color=rl.Color(255, 89, 79, 255),
|
||||
text_padding=60)
|
||||
self._yellow_warning_icon = gui_app.texture("icons/yellow_warning.png", int(68 * FONT_SCALE), int(68 * FONT_SCALE))
|
||||
self._custom_software_warning_body_labels = [
|
||||
Label(text, 68, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||
Label(text, 68, text_alignment=TextAlignment.LEFT,
|
||||
text_alignment_vertical=TextAlignmentVertical.TOP,
|
||||
text_padding=60, icon=self._yellow_warning_icon if has_icon else None)
|
||||
for text, has_icon in [
|
||||
("Use caution when installing third-party software.", False),
|
||||
|
||||
@@ -10,7 +10,7 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||
from openpilot.system.ui.widgets.label import gui_text_box, gui_label
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
|
||||
from openpilot.system.ui.widgets.network import WifiManagerUI
|
||||
|
||||
# Constants
|
||||
@@ -50,6 +50,8 @@ class Updater(Widget):
|
||||
self._install_button = Button("Install", click_callback=self.install_update, button_style=ButtonStyle.PRIMARY)
|
||||
self._back_button = Button("Back", click_callback=lambda: self.set_current_screen(Screen.PROMPT))
|
||||
self._reboot_button = Button("Reboot", click_callback=lambda: HARDWARE.reboot())
|
||||
self._desc_label = UnifiedLabel("An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. " +
|
||||
"The download size is approximately 1GB.", BODY_FONT_SIZE, line_height=1 / 0.9)
|
||||
|
||||
def set_current_screen(self, screen: Screen):
|
||||
self.current_screen = screen
|
||||
@@ -99,11 +101,8 @@ class Updater(Widget):
|
||||
gui_label(title_rect, "Update Required", TITLE_FONT_SIZE, font_weight=FontWeight.BOLD)
|
||||
|
||||
# Description
|
||||
desc_text = ("An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. " +
|
||||
"The download size is approximately 1GB.")
|
||||
|
||||
desc_rect = rl.Rectangle(MARGIN + 50, 250 + TITLE_FONT_SIZE * FONT_SCALE + 75, rect.width - MARGIN * 2 - 100, BODY_FONT_SIZE * FONT_SCALE * 4)
|
||||
gui_text_box(desc_rect, desc_text, BODY_FONT_SIZE)
|
||||
self._desc_label.render(desc_rect)
|
||||
|
||||
# Buttons at the bottom
|
||||
button_y = rect.height - MARGIN - BUTTON_HEIGHT
|
||||
|
||||
@@ -3,7 +3,7 @@ from enum import IntEnum
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import Label
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
@@ -86,7 +86,7 @@ class Button(Widget):
|
||||
font_weight: FontWeight = FontWeight.MEDIUM,
|
||||
button_style: ButtonStyle = ButtonStyle.NORMAL,
|
||||
border_radius: int = 10,
|
||||
text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
text_alignment: int = TextAlignment.CENTER,
|
||||
text_padding: int = 20,
|
||||
icon=None,
|
||||
elide_right: bool = False,
|
||||
@@ -139,7 +139,7 @@ class ButtonRadio(Button):
|
||||
icon,
|
||||
click_callback: Callable[[], None] | None = None,
|
||||
font_size: int = DEFAULT_BUTTON_FONT_SIZE,
|
||||
text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
text_alignment: int = TextAlignment.LEFT,
|
||||
border_radius: int = 10,
|
||||
text_padding: int = 20,
|
||||
):
|
||||
|
||||
@@ -14,3 +14,6 @@ class IconWidget(Widget):
|
||||
def _render(self, _) -> None:
|
||||
color = rl.Color(255, 255, 255, int(self._opacity * 255))
|
||||
rl.draw_texture_ex(self._texture, rl.Vector2(self._rect.x, self._rect.y), 0.0, 1.0, color)
|
||||
|
||||
def set_opacity(self, opacity: float) -> None:
|
||||
self._opacity = opacity
|
||||
|
||||
@@ -5,7 +5,7 @@ from collections.abc import Callable
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import DialogResult, Widget
|
||||
from openpilot.system.ui.widgets.button import ButtonStyle, Button
|
||||
@@ -65,8 +65,8 @@ class Keyboard(Widget):
|
||||
self._layout_name: Literal["lowercase", "uppercase", "numbers", "specials"] = "lowercase"
|
||||
self._caps_lock = False
|
||||
self._last_shift_press_time = 0
|
||||
self._title = Label("", 90, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20)
|
||||
self._sub_title = Label("", 55, FontWeight.NORMAL, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20)
|
||||
self._title = Label("", 90, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20)
|
||||
self._sub_title = Label("", 55, FontWeight.NORMAL, TextAlignment.LEFT, text_padding=20)
|
||||
|
||||
self._max_text_size = max_text_size
|
||||
self._min_text_size = min_text_size
|
||||
|
||||
@@ -4,10 +4,9 @@ from collections.abc import Callable
|
||||
from typing import Union
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR, FONT_SCALE
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR, FONT_SCALE, TextAlignment, TextAlignmentVertical
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.utils import GuiStyleContext
|
||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||
|
||||
ICON_PADDING = 15
|
||||
@@ -32,8 +31,8 @@ def gui_label(
|
||||
font_size: int = DEFAULT_TEXT_SIZE,
|
||||
color: rl.Color = DEFAULT_TEXT_COLOR,
|
||||
font_weight: FontWeight = FontWeight.NORMAL,
|
||||
alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
|
||||
alignment: int = TextAlignment.LEFT,
|
||||
alignment_vertical: int = TextAlignmentVertical.MIDDLE,
|
||||
elide_right: bool = True
|
||||
):
|
||||
font = gui_app.font(font_weight)
|
||||
@@ -57,16 +56,16 @@ def gui_label(
|
||||
|
||||
# Calculate horizontal position based on alignment
|
||||
text_x = rect.x + {
|
||||
rl.GuiTextAlignment.TEXT_ALIGN_LEFT: 0,
|
||||
rl.GuiTextAlignment.TEXT_ALIGN_CENTER: (rect.width - text_size.x) / 2,
|
||||
rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: rect.width - text_size.x,
|
||||
TextAlignment.LEFT: 0,
|
||||
TextAlignment.CENTER: (rect.width - text_size.x) / 2,
|
||||
TextAlignment.RIGHT: rect.width - text_size.x,
|
||||
}.get(alignment, 0)
|
||||
|
||||
# Calculate vertical position based on alignment
|
||||
text_y = rect.y + {
|
||||
rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP: 0,
|
||||
rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE: (rect.height - text_size.y) / 2,
|
||||
rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: rect.height - text_size.y,
|
||||
TextAlignmentVertical.TOP: 0,
|
||||
TextAlignmentVertical.MIDDLE: (rect.height - text_size.y) / 2,
|
||||
TextAlignmentVertical.BOTTOM: rect.height - text_size.y,
|
||||
}.get(alignment_vertical, 0)
|
||||
|
||||
# Draw the text in the specified rectangle
|
||||
@@ -74,42 +73,14 @@ def gui_label(
|
||||
rl.draw_text_ex(font, display_text, rl.Vector2(text_x, text_y), font_size, 0, color)
|
||||
|
||||
|
||||
def gui_text_box(
|
||||
rect: rl.Rectangle,
|
||||
text: str,
|
||||
font_size: int = DEFAULT_TEXT_SIZE,
|
||||
color: rl.Color = DEFAULT_TEXT_COLOR,
|
||||
alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||
font_weight: FontWeight = FontWeight.NORMAL,
|
||||
line_scale: float = 1.0,
|
||||
):
|
||||
styles = [
|
||||
(rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(color)),
|
||||
(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, round(font_size * FONT_SCALE)),
|
||||
(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_LINE_SPACING, round(font_size * FONT_SCALE * line_scale)),
|
||||
(rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_ALIGNMENT, alignment),
|
||||
(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_ALIGNMENT_VERTICAL, alignment_vertical),
|
||||
(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_WRAP_MODE, rl.GuiTextWrapMode.TEXT_WRAP_WORD)
|
||||
]
|
||||
if font_weight != FontWeight.NORMAL:
|
||||
rl.gui_set_font(gui_app.font(font_weight))
|
||||
|
||||
with GuiStyleContext(styles):
|
||||
rl.gui_label(rect, text)
|
||||
|
||||
if font_weight != FontWeight.NORMAL:
|
||||
rl.gui_set_font(gui_app.font(FontWeight.NORMAL))
|
||||
|
||||
|
||||
# Non-interactive text area. Can render an optional specified icon.
|
||||
class Label(Widget):
|
||||
def __init__(self,
|
||||
text: str | Callable[[], str],
|
||||
font_size: int = DEFAULT_TEXT_SIZE,
|
||||
font_weight: FontWeight = FontWeight.NORMAL,
|
||||
text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
text_alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
|
||||
text_alignment: int = TextAlignment.CENTER,
|
||||
text_alignment_vertical: int = TextAlignmentVertical.MIDDLE,
|
||||
text_padding: int = 0,
|
||||
text_color: rl.Color = DEFAULT_TEXT_COLOR,
|
||||
icon: Union[rl.Texture, None] = None,
|
||||
@@ -181,10 +152,10 @@ class Label(Widget):
|
||||
self._update_text(self._text)
|
||||
|
||||
text_size = self._text_size[0] if self._text_size else rl.Vector2(0.0, 0.0)
|
||||
if self._text_alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE:
|
||||
if self._text_alignment_vertical == TextAlignmentVertical.MIDDLE:
|
||||
total_text_height = sum(ts.y for ts in self._text_size) or self._font_size * FONT_SCALE
|
||||
text_pos = rl.Vector2(self._rect.x, (self._rect.y + (self._rect.height - total_text_height) // 2))
|
||||
elif self._text_alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM:
|
||||
elif self._text_alignment_vertical == TextAlignmentVertical.BOTTOM:
|
||||
total_text_height = sum(ts.y for ts in self._text_size) or self._font_size * FONT_SCALE
|
||||
text_pos = rl.Vector2(self._rect.x, self._rect.y + self._rect.height - total_text_height)
|
||||
else:
|
||||
@@ -193,10 +164,10 @@ class Label(Widget):
|
||||
if self._icon:
|
||||
icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2
|
||||
if len(self._text_wrapped) > 0:
|
||||
if self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT:
|
||||
if self._text_alignment == TextAlignment.LEFT:
|
||||
icon_x = self._rect.x + self._text_padding
|
||||
text_pos.x = self._rect.x + self._icon.width + ICON_PADDING
|
||||
elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER:
|
||||
elif self._text_alignment == TextAlignment.CENTER:
|
||||
total_width = self._icon.width + ICON_PADDING + text_size.x
|
||||
icon_x = self._rect.x + (self._rect.width - total_width) / 2
|
||||
text_pos.x = self._rect.x + self._icon.width + ICON_PADDING
|
||||
@@ -208,11 +179,11 @@ class Label(Widget):
|
||||
|
||||
for text, text_size in zip(self._text_wrapped, self._text_size, strict=True):
|
||||
line_pos = rl.Vector2(text_pos.x, text_pos.y)
|
||||
if self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT:
|
||||
if self._text_alignment == TextAlignment.LEFT:
|
||||
line_pos.x += self._text_padding
|
||||
elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER:
|
||||
elif self._text_alignment == TextAlignment.CENTER:
|
||||
line_pos.x += (self._rect.width - text_size.x) // 2
|
||||
elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_RIGHT:
|
||||
elif self._text_alignment == TextAlignment.RIGHT:
|
||||
line_pos.x += self._rect.width - text_size.x - self._text_padding
|
||||
|
||||
rl.draw_text_ex(self._font, text, line_pos, self._font_size, 0, self._text_color)
|
||||
@@ -221,7 +192,7 @@ class Label(Widget):
|
||||
|
||||
class UnifiedLabel(Widget):
|
||||
"""
|
||||
Unified label widget that combines functionality from gui_label, gui_text_box, and Label.
|
||||
Unified label widget that combines functionality from gui_label and Label.
|
||||
|
||||
Supports:
|
||||
- Text wrapping
|
||||
@@ -241,8 +212,8 @@ class UnifiedLabel(Widget):
|
||||
font_size: int = DEFAULT_TEXT_SIZE,
|
||||
font_weight: FontWeight = FontWeight.NORMAL,
|
||||
text_color: rl.Color = DEFAULT_TEXT_COLOR,
|
||||
alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||
alignment: int = TextAlignment.LEFT,
|
||||
alignment_vertical: int = TextAlignmentVertical.TOP,
|
||||
text_padding: int = 0,
|
||||
max_width: int | None = None,
|
||||
elide: bool = True,
|
||||
@@ -561,9 +532,9 @@ class UnifiedLabel(Widget):
|
||||
total_visible_height += size.y * self._line_height
|
||||
|
||||
# Calculate vertical alignment offset
|
||||
if self._alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP:
|
||||
if self._alignment_vertical == TextAlignmentVertical.TOP:
|
||||
start_y = self._rect.y
|
||||
elif self._alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM:
|
||||
elif self._alignment_vertical == TextAlignmentVertical.BOTTOM:
|
||||
start_y = self._rect.y + self._rect.height - total_visible_height
|
||||
else: # TEXT_ALIGN_MIDDLE
|
||||
start_y = self._rect.y + (self._rect.height - total_visible_height) / 2
|
||||
@@ -640,11 +611,11 @@ class UnifiedLabel(Widget):
|
||||
|
||||
def _render_line(self, line, size, current_y, x_offset=0.0):
|
||||
# Calculate horizontal position
|
||||
if self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT:
|
||||
if self._alignment == TextAlignment.LEFT:
|
||||
line_x = self._rect.x + self._text_padding
|
||||
elif self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER:
|
||||
elif self._alignment == TextAlignment.CENTER:
|
||||
line_x = self._rect.x + (self._rect.width - size.x) / 2
|
||||
elif self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_RIGHT:
|
||||
elif self._alignment == TextAlignment.RIGHT:
|
||||
line_x = self._rect.x + self._rect.width - size.x - self._text_padding
|
||||
else:
|
||||
line_x = self._rect.x + self._text_padding
|
||||
@@ -662,9 +633,9 @@ class UnifiedLabel(Widget):
|
||||
def _render_line_shimmer(self, line, line_x, current_y):
|
||||
# Shimmer range based on widest line so sweep is even across all lines
|
||||
max_width = self.text_width
|
||||
if self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_RIGHT:
|
||||
if self._alignment == TextAlignment.RIGHT:
|
||||
shimmer_left = self._rect.x + self._rect.width - self._text_padding - max_width
|
||||
elif self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER:
|
||||
elif self._alignment == TextAlignment.CENTER:
|
||||
shimmer_left = self._rect.x + (self._rect.width - max_width) / 2
|
||||
else:
|
||||
shimmer_left = self._rect.x + self._text_padding
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
import pyray as rl
|
||||
from collections.abc import Callable, Sequence
|
||||
from abc import ABC
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment, TextAlignmentVertical
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
@@ -139,8 +139,8 @@ class ButtonAction(ItemAction):
|
||||
if value_text:
|
||||
value_rect = rl.Rectangle(rect.x, rect.y, rect.width - BUTTON_WIDTH - TEXT_PADDING, rect.height)
|
||||
gui_label(value_rect, value_text, font_size=ITEM_TEXT_FONT_SIZE, color=ITEM_TEXT_VALUE_COLOR,
|
||||
font_weight=FontWeight.NORMAL, alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
font_weight=FontWeight.NORMAL, alignment=TextAlignment.LEFT,
|
||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
||||
|
||||
# TODO: just use the generic Widget click callbacks everywhere, no returning from render
|
||||
pressed = self._pressed
|
||||
@@ -168,8 +168,8 @@ class TextAction(ItemAction):
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> bool:
|
||||
gui_label(self._rect, self.text, font_size=ITEM_TEXT_FONT_SIZE, color=self.color,
|
||||
font_weight=FontWeight.NORMAL, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
font_weight=FontWeight.NORMAL, alignment=TextAlignment.RIGHT,
|
||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
||||
return False
|
||||
|
||||
def set_text(self, text: str | Callable[[], str]):
|
||||
|
||||
@@ -3,7 +3,7 @@ from functools import partial
|
||||
from typing import Any, cast
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.application import gui_app, TextAlignment
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, SecurityType, Network, MeteredType, normalize_ssid
|
||||
@@ -67,7 +67,7 @@ class NavButton(Widget):
|
||||
def _render(self, _):
|
||||
color = rl.Color(74, 74, 74, 255) if self.is_pressed else rl.Color(57, 57, 57, 255)
|
||||
rl.draw_rectangle_rounded(self._rect, 0.6, 10, color)
|
||||
gui_label(self.rect, self.text, font_size=60, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
gui_label(self.rect, self.text, font_size=60, alignment=TextAlignment.CENTER)
|
||||
|
||||
|
||||
class NetworkUI(Widget):
|
||||
@@ -315,7 +315,7 @@ class WifiManagerUI(Widget):
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
if not self._networks:
|
||||
gui_label(rect, tr("Scanning Wi-Fi networks..."), 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
gui_label(rect, tr("Scanning Wi-Fi networks..."), 72, alignment=TextAlignment.CENTER)
|
||||
return
|
||||
|
||||
if self.state == UIState.NEEDS_AUTH and self._state_network:
|
||||
@@ -389,7 +389,7 @@ class WifiManagerUI(Widget):
|
||||
|
||||
if status_text:
|
||||
status_text_rect = rl.Rectangle(security_icon_rect.x - 410, rect.y, 410, ITEM_HEIGHT)
|
||||
gui_label(status_text_rect, status_text, font_size=48, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
gui_label(status_text_rect, status_text, font_size=48, alignment=TextAlignment.CENTER)
|
||||
else:
|
||||
# If the network is saved, show the "Forget" button
|
||||
if self._wifi_manager.is_connection_saved(network.ssid):
|
||||
@@ -455,7 +455,7 @@ class WifiManagerUI(Widget):
|
||||
self._networks = networks
|
||||
for n in self._networks:
|
||||
self._networks_buttons[n.ssid] = Button(normalize_ssid(n.ssid), partial(self._networks_buttons_callback, n), font_size=55,
|
||||
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, button_style=ButtonStyle.TRANSPARENT_WHITE_TEXT)
|
||||
text_alignment=TextAlignment.LEFT, button_style=ButtonStyle.TRANSPARENT_WHITE_TEXT)
|
||||
self._networks_buttons[n.ssid].set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid())
|
||||
self._forget_networks_buttons[n.ssid] = Button(tr("Forget"), partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI,
|
||||
font_size=45)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget, DialogResult
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||
@@ -29,7 +29,7 @@ class MultiOptionDialog(Widget):
|
||||
# Create scroller with option buttons
|
||||
self.option_buttons = [Button(option, click_callback=lambda opt=option: self._on_option_clicked(opt),
|
||||
font_weight=option_font_weight,
|
||||
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, button_style=ButtonStyle.NORMAL,
|
||||
text_alignment=TextAlignment.LEFT, button_style=ButtonStyle.NORMAL,
|
||||
text_padding=50, elide_right=True) for option in options]
|
||||
self.scroller = Scroller(self.option_buttons, spacing=LIST_ITEM_SPACING)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from collections.abc import Callable
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.common.filter_simple import FirstOrderFilter, BounceFilter
|
||||
@@ -41,8 +41,8 @@ class SliderBase(Widget, abc.ABC):
|
||||
self._is_dragging_circle = False
|
||||
|
||||
self._label = self._child(UnifiedLabel(title, font_size=36, font_weight=FontWeight.SEMI_BOLD, text_color=rl.WHITE,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, line_height=0.9, shimmer=True))
|
||||
alignment=TextAlignment.RIGHT,
|
||||
alignment_vertical=TextAlignmentVertical.MIDDLE, line_height=0.9, shimmer=True))
|
||||
|
||||
@abc.abstractmethod
|
||||
def _load_assets(self):
|
||||
|
||||
@@ -99,10 +99,10 @@ cabana_env.Command(assets, "assets/assets.qrc", f"rcc $SOURCES -o $TARGET")
|
||||
cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, "assets/assets.o"]))
|
||||
|
||||
cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc',
|
||||
'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'dbc/dbcqt.cc',
|
||||
'utils/export.cc', 'utils/util.cc', 'utils/elidedlabel.cc',
|
||||
'routesdialog.cc', 'routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc',
|
||||
'utils/export.cc', 'utils/util.cc', 'utils/qtutil.cc', 'utils/strings.cc', 'utils/elidedlabel.cc',
|
||||
'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc',
|
||||
'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'panda.cc',
|
||||
'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'settingsdialog.cc', 'panda.cc',
|
||||
'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc']
|
||||
if arch != "Darwin":
|
||||
cabana_srcs += ['streams/socketcanstream.cc']
|
||||
@@ -119,8 +119,12 @@ if GetOption('extras'):
|
||||
dbc_core_test_env.Object('tests/dbc_core_model', 'dbc/dbc.cc'),
|
||||
dbc_core_test_env.Object('tests/dbc_core_file', 'dbc/dbcfile.cc'),
|
||||
dbc_core_test_env.Object('tests/dbc_core_manager', 'dbc/dbcmanager.cc'),
|
||||
dbc_core_test_env.Object('tests/dbc_core_strings', 'utils/strings.cc'),
|
||||
dbc_core_test_env.Object('tests/dbc_core_util', 'utils/util.cc'),
|
||||
dbc_core_test_env.Object('tests/dbc_core_icons', bootstrap_icons_src),
|
||||
dbc_core_test_env.Object('tests/dbc_core_routes', 'routes.cc'),
|
||||
]
|
||||
dbc_core_test_env.Program('tests/test_dbc_core', dbc_core_test_objects)
|
||||
dbc_core_test_env.Program('tests/test_dbc_core', dbc_core_test_objects, LIBS=[replay_lib, common])
|
||||
|
||||
output_json_file = 'openpilot/tools/cabana/dbc/car_fingerprint_to_dbc.json'
|
||||
generate_dbc = cabana_env.Command('#' + output_json_file,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "tools/cabana/binaryview.h"
|
||||
#include "tools/cabana/dbc/dbcqt.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -14,6 +13,7 @@
|
||||
#include <QToolTip>
|
||||
|
||||
#include "tools/cabana/commands.h"
|
||||
#include "tools/cabana/utils/qtutil.h"
|
||||
|
||||
// BinaryView
|
||||
|
||||
@@ -36,8 +36,8 @@ BinaryView::BinaryView(QWidget *parent) : QTableView(parent) {
|
||||
setMouseTracking(true);
|
||||
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
|
||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &BinaryView::refresh);
|
||||
QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &BinaryView::refresh);
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() { refresh(); }));
|
||||
connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { refresh(); }));
|
||||
|
||||
addShortcuts();
|
||||
setWhatsThis(R"(
|
||||
@@ -386,7 +386,7 @@ QVariant BinaryViewModel::headerData(int section, Qt::Orientation orientation, i
|
||||
|
||||
QVariant BinaryViewModel::data(const QModelIndex &index, int role) const {
|
||||
auto item = (const BinaryViewModel::Item *)index.internalPointer();
|
||||
return role == Qt::ToolTipRole && item && !item->sigs.empty() ? signalToolTip(item->sigs.back()) : QVariant();
|
||||
return role == Qt::ToolTipRole && item && !item->sigs.empty() ? QString::fromStdString(utils::signalToolTip(item->sigs.back())) : QVariant();
|
||||
}
|
||||
|
||||
// BinaryItemDelegate
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include <QStaticText>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QTableView>
|
||||
|
||||
@@ -100,5 +101,6 @@ private:
|
||||
bool is_message_active = false;
|
||||
const cabana::Signal *resize_sig = nullptr;
|
||||
const cabana::Signal *hovered_sig = nullptr;
|
||||
Connections connections_;
|
||||
friend class BinaryItemDelegate;
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#ifdef __linux__
|
||||
#include "tools/cabana/streams/socketcanstream.h"
|
||||
#endif
|
||||
#include "tools/cabana/utils/qtutil.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -134,7 +135,14 @@ int main(int argc, char *argv[]) {
|
||||
app.setApplicationDisplayName("Cabana");
|
||||
//app.setWindowIcon(QIcon(":cabana-icon.png")); // TODO: do this in imgui
|
||||
|
||||
UnixSignalHandler signalHandler;
|
||||
// Marshal exit onto the GUI thread (qApp methods are not thread-safe).
|
||||
UnixSignalHandler signalHandler([]() {
|
||||
QMetaObject::invokeMethod(qApp, []() {
|
||||
printf("\nexiting...\n");
|
||||
qApp->closeAllWindows();
|
||||
qApp->exit();
|
||||
}, Qt::QueuedConnection);
|
||||
});
|
||||
utils::setTheme(settings.theme);
|
||||
|
||||
CabanaArgs args;
|
||||
@@ -146,19 +154,19 @@ int main(int argc, char *argv[]) {
|
||||
AbstractStream *stream = nullptr;
|
||||
|
||||
if (args.msgq) {
|
||||
stream = new DeviceStream(&app);
|
||||
stream = new DeviceStream();
|
||||
} else if (!args.zmq.empty()) {
|
||||
stream = new DeviceStream(&app, QString::fromStdString(args.zmq));
|
||||
stream = new DeviceStream(args.zmq);
|
||||
} else if (args.panda || !args.panda_serial.empty()) {
|
||||
try {
|
||||
stream = new PandaStream(&app, {.serial = args.panda_serial});
|
||||
stream = new PandaStream({.serial = args.panda_serial});
|
||||
} catch (std::exception &e) {
|
||||
fprintf(stderr, "%s\n", e.what());
|
||||
return 0;
|
||||
}
|
||||
#ifdef __linux__
|
||||
} else if (SocketCanStream::available() && !args.socketcan.empty()) {
|
||||
stream = new SocketCanStream(&app, {.device = args.socketcan});
|
||||
stream = new SocketCanStream({.device = args.socketcan});
|
||||
#endif
|
||||
} else {
|
||||
uint32_t replay_flags = REPLAY_FLAG_NONE;
|
||||
@@ -174,7 +182,8 @@ int main(int argc, char *argv[]) {
|
||||
route = DEMO_ROUTE;
|
||||
}
|
||||
if (!route.isEmpty()) {
|
||||
auto replay_stream = std::make_unique<ReplayStream>(&app);
|
||||
auto replay_stream = std::make_unique<ReplayStream>();
|
||||
Connection err = replay_stream->error.connect([](const std::string &msg) { fprintf(stderr, "%s\n", msg.c_str()); });
|
||||
if (!replay_stream->loadRoute(route.toStdString(), args.data_dir, replay_flags, args.auto_source)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -9,13 +9,11 @@
|
||||
#include <QPainter>
|
||||
|
||||
#include "common/yuv.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
CameraWidget::CameraWidget(std::string stream_name, VisionStreamType type, QWidget* parent) :
|
||||
stream_name(stream_name), active_stream_type(type), requested_stream_type(type), QWidget(parent) {
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
qRegisterMetaType<std::set<VisionStreamType>>("availableStreams");
|
||||
QObject::connect(this, &CameraWidget::vipcThreadFrameReceived, this, &CameraWidget::vipcFrameReceived, Qt::QueuedConnection);
|
||||
QObject::connect(this, &CameraWidget::vipcAvailableStreamsUpdated, this, &CameraWidget::availableStreamsUpdated, Qt::QueuedConnection);
|
||||
QObject::connect(QApplication::instance(), &QCoreApplication::aboutToQuit, this, &CameraWidget::stopVipcThread);
|
||||
}
|
||||
|
||||
@@ -38,10 +36,6 @@ void CameraWidget::stopVipcThread() {
|
||||
}
|
||||
}
|
||||
|
||||
void CameraWidget::availableStreamsUpdated(std::set<VisionStreamType> streams) {
|
||||
available_streams = streams;
|
||||
}
|
||||
|
||||
void CameraWidget::paintEvent(QPaintEvent *event) {
|
||||
QPainter p(this);
|
||||
p.fillRect(rect(), bg);
|
||||
@@ -67,10 +61,6 @@ void CameraWidget::paintEvent(QPaintEvent *event) {
|
||||
p.drawImage(video_rect, rgb_frame);
|
||||
}
|
||||
|
||||
void CameraWidget::vipcFrameReceived() {
|
||||
update();
|
||||
}
|
||||
|
||||
void CameraWidget::vipcThread() {
|
||||
VisionStreamType cur_stream = requested_stream_type;
|
||||
std::unique_ptr<VisionIpcClient> vipc_client;
|
||||
@@ -93,7 +83,11 @@ void CameraWidget::vipcThread() {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
continue;
|
||||
}
|
||||
emit vipcAvailableStreamsUpdated(streams);
|
||||
utils::runOnMainThread([this, alive = std::weak_ptr<bool>(alive_), streams]() {
|
||||
if (alive.expired()) return;
|
||||
available_streams = streams;
|
||||
availableStreamsUpdated(streams);
|
||||
});
|
||||
|
||||
if (!vipc_client->connect(false)) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
@@ -112,7 +106,9 @@ void CameraWidget::vipcThread() {
|
||||
std::lock_guard lk(frame_lock);
|
||||
rgb_frame.swap(rgb_back);
|
||||
}
|
||||
emit vipcThreadFrameReceived();
|
||||
utils::runOnMainThread([this, alive = std::weak_ptr<bool>(alive_)]() {
|
||||
if (!alive.expired()) update();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
@@ -11,11 +12,10 @@
|
||||
#include <QWidget>
|
||||
|
||||
#include "openpilot/cereal/visionstream.h"
|
||||
#include "tools/cabana/core/observable.h"
|
||||
#include "msgq/visionipc/visionipc_client.h"
|
||||
|
||||
class CameraWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CameraWidget(std::string stream_name, VisionStreamType stream_type, QWidget* parent = nullptr);
|
||||
~CameraWidget();
|
||||
@@ -23,16 +23,14 @@ public:
|
||||
VisionStreamType getStreamType() { return active_stream_type; }
|
||||
void stopVipcThread();
|
||||
|
||||
signals:
|
||||
void clicked();
|
||||
void vipcThreadFrameReceived();
|
||||
void vipcAvailableStreamsUpdated(std::set<VisionStreamType>);
|
||||
Observable<> clicked;
|
||||
Observable<std::set<VisionStreamType>> availableStreamsUpdated; // invoked on the main thread
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void hideEvent(QHideEvent *event) override { stopVipcThread(); }
|
||||
void mouseReleaseEvent(QMouseEvent *event) override { emit clicked(); }
|
||||
void mouseReleaseEvent(QMouseEvent *event) override { clicked(); }
|
||||
void vipcThread();
|
||||
void clearFrames();
|
||||
|
||||
@@ -47,10 +45,5 @@ protected:
|
||||
std::thread vipc_thread;
|
||||
std::atomic<bool> vipc_exit = false;
|
||||
std::mutex frame_lock;
|
||||
|
||||
protected slots:
|
||||
void vipcFrameReceived();
|
||||
void availableStreamsUpdated(std::set<VisionStreamType> streams);
|
||||
std::shared_ptr<bool> alive_ = std::make_shared<bool>(true);
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(std::set<VisionStreamType>);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "tools/cabana/chart/chart.h"
|
||||
#include "tools/cabana/dbc/dbcqt.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
@@ -37,10 +36,10 @@ ChartView::ChartView(const std::pair<double, double> &x_range, ChartsWidget *par
|
||||
createToolButtons();
|
||||
signal_value_font.setPointSize(9);
|
||||
|
||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::signalRemoved, this, &ChartView::signalRemoved);
|
||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::signalUpdated, this, &ChartView::signalUpdated);
|
||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::msgRemoved, this, &ChartView::msgRemoved);
|
||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::msgUpdated, this, &ChartView::msgUpdated);
|
||||
connections_.push_back(dbc()->signalRemoved.connect([this](const cabana::Signal *sig) { signalRemoved(sig); }));
|
||||
connections_.push_back(dbc()->signalUpdated.connect([this](const cabana::Signal *sig) { signalUpdated(sig); }));
|
||||
connections_.push_back(dbc()->msgRemoved.connect([this](MessageId id) { msgRemoved(id); }));
|
||||
connections_.push_back(dbc()->msgUpdated.connect([this](MessageId id) { msgUpdated(id); }));
|
||||
}
|
||||
|
||||
void ChartView::createToolButtons() {
|
||||
@@ -249,7 +248,7 @@ void ChartView::updateSeries(const cabana::Signal *sig, const MessageEventsMap *
|
||||
}
|
||||
|
||||
if (!can->liveStreaming()) {
|
||||
s.segment_tree.build(s.vals);
|
||||
s.segment_tree.build(s.vals.size(), [&vals = s.vals](int i) { return vals[i].y(); });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "tools/cabana/chart/tiplabel.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
#include "tools/cabana/utils/qtutil.h"
|
||||
|
||||
enum class SeriesType {
|
||||
Line = 0,
|
||||
@@ -126,5 +127,6 @@ private:
|
||||
double tooltip_x = -1;
|
||||
QFont signal_value_font;
|
||||
ChartsWidget *charts_widget;
|
||||
Connections connections_;
|
||||
friend class ChartsWidget;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "tools/cabana/chart/chartswidget.h"
|
||||
#include "tools/cabana/dbc/dbcqt.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <future>
|
||||
@@ -76,10 +75,10 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) {
|
||||
redo_zoom_action = toolbar->addAction(utils::icon("arrow-clockwise"), tr("Redo Zoom"), [this]() { zoom_undo_stack.redo(); });
|
||||
undo_zoom_action->setEnabled(false);
|
||||
redo_zoom_action->setEnabled(false);
|
||||
zoom_undo_stack.setCallbacks({.index_changed = [this]() {
|
||||
connections_.push_back(zoom_undo_stack.indexChanged.connect([this]() {
|
||||
undo_zoom_action->setEnabled(zoom_undo_stack.canUndo());
|
||||
redo_zoom_action->setEnabled(zoom_undo_stack.canRedo());
|
||||
}});
|
||||
}));
|
||||
reset_zoom_action = toolbar->addWidget(reset_zoom_btn = new ToolButton("zoom-out", tr("Reset Zoom")));
|
||||
reset_zoom_btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||||
|
||||
@@ -122,16 +121,16 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) {
|
||||
align_timer->setSingleShot(true);
|
||||
QObject::connect(align_timer, &QTimer::timeout, this, &ChartsWidget::alignCharts);
|
||||
QObject::connect(auto_scroll_timer, &QTimer::timeout, this, &ChartsWidget::doAutoScroll);
|
||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &ChartsWidget::removeAll);
|
||||
QObject::connect(can, &AbstractStream::eventsMerged, this, &ChartsWidget::eventsMerged);
|
||||
QObject::connect(can, &AbstractStream::msgsReceived, this, &ChartsWidget::updateState);
|
||||
QObject::connect(can, &AbstractStream::seeking, this, &ChartsWidget::updateState);
|
||||
QObject::connect(can, &AbstractStream::timeRangeChanged, this, &ChartsWidget::timeRangeChanged);
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() { removeAll(); }));
|
||||
connections_.push_back(can->eventsMerged.connect([this](const MessageEventsMap &events) { eventsMerged(events); }));
|
||||
connections_.push_back(can->msgsReceived.connect([this](const std::set<MessageId> *, bool) { updateState(); }));
|
||||
connections_.push_back(can->seeking.connect([this](double) { updateState(); }));
|
||||
connections_.push_back(can->timeRangeChanged.connect([this](const auto &range) { timeRangeChanged(range); }));
|
||||
QObject::connect(range_slider, &QSlider::valueChanged, this, &ChartsWidget::setMaxChartRange);
|
||||
QObject::connect(new_plot_btn, &QToolButton::clicked, this, &ChartsWidget::newChart);
|
||||
QObject::connect(remove_all_btn, &QToolButton::clicked, this, &ChartsWidget::removeAll);
|
||||
QObject::connect(reset_zoom_btn, &QToolButton::clicked, this, &ChartsWidget::zoomReset);
|
||||
QObject::connect(&settings, &Settings::changed, this, &ChartsWidget::settingChanged);
|
||||
connections_.push_back(settings.changed.connect([this]() { settingChanged(); }));
|
||||
QObject::connect(new_tab_btn, &QToolButton::clicked, this, &ChartsWidget::newTab);
|
||||
QObject::connect(this, &ChartsWidget::seriesChanged, this, &ChartsWidget::updateTabBar);
|
||||
QObject::connect(tabbar, &QTabBar::tabCloseRequested, this, &ChartsWidget::removeTab);
|
||||
@@ -246,7 +245,7 @@ void ChartsWidget::setIsDocked(bool docked) {
|
||||
void ChartsWidget::updateToolBar() {
|
||||
title_label->setText(tr("Charts: %1").arg(charts.size()));
|
||||
columns_action->setText(tr("Columns: %1").arg(column_count));
|
||||
range_lb->setText(utils::formatSeconds(max_chart_range));
|
||||
range_lb->setText(QString::fromStdString(utils::formatSeconds(max_chart_range)));
|
||||
|
||||
bool is_zoomed = can->timeRange().has_value();
|
||||
range_lb_action->setVisible(!is_zoomed);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "tools/cabana/commands.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
#include "tools/cabana/utils/qtutil.h"
|
||||
|
||||
const int CHART_MIN_WIDTH = 300;
|
||||
|
||||
@@ -123,6 +124,7 @@ private:
|
||||
QTimer *align_timer;
|
||||
int current_theme = 0;
|
||||
bool value_tip_visible_ = false;
|
||||
Connections connections_;
|
||||
friend class ChartView;
|
||||
friend class ChartsContainer;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "tools/cabana/chart/signalselector.h"
|
||||
#include "tools/cabana/dbc/dbcqt.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QGridLayout>
|
||||
@@ -9,6 +8,7 @@
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
#include "tools/cabana/utils/qtutil.h"
|
||||
|
||||
SignalSelector::SignalSelector(QString title, QWidget *parent) : QDialog(parent) {
|
||||
setWindowTitle(title);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <QPainter>
|
||||
#include "tools/cabana/utils/qtutil.h"
|
||||
|
||||
void Sparkline::update(const cabana::Signal *sig, CanEventIter first, CanEventIter last, int range, QSize size) {
|
||||
if (first == last || size.isEmpty()) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#include <QToolTip>
|
||||
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
#include "tools/cabana/utils/qtutil.h"
|
||||
|
||||
TipLabel::TipLabel(QWidget *parent) : QLabel(parent, Qt::ToolTip | Qt::FramelessWindowHint) {
|
||||
setAttribute(Qt::WA_ShowWithoutActivating);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "tools/cabana/commands.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
|
||||
// UndoStack
|
||||
@@ -28,22 +29,22 @@ void UndoStack::clear() {
|
||||
bool was_clean = isClean();
|
||||
commands_.clear();
|
||||
index_ = clean_index_ = 0;
|
||||
if (callbacks_.index_changed) callbacks_.index_changed();
|
||||
if (!was_clean && callbacks_.clean_changed) callbacks_.clean_changed(true);
|
||||
indexChanged();
|
||||
if (!was_clean) cleanChanged(true);
|
||||
}
|
||||
|
||||
void UndoStack::setClean() {
|
||||
if (!isClean()) {
|
||||
clean_index_ = index_;
|
||||
if (callbacks_.clean_changed) callbacks_.clean_changed(true);
|
||||
cleanChanged(true);
|
||||
}
|
||||
}
|
||||
|
||||
void UndoStack::setIndex(int index) {
|
||||
bool was_clean = isClean();
|
||||
index_ = index;
|
||||
if (callbacks_.index_changed) callbacks_.index_changed();
|
||||
if (isClean() != was_clean && callbacks_.clean_changed) callbacks_.clean_changed(isClean());
|
||||
indexChanged();
|
||||
if (isClean() != was_clean) cleanChanged(isClean());
|
||||
}
|
||||
|
||||
UndoStack *UndoStack::instance() {
|
||||
@@ -51,18 +52,6 @@ UndoStack *UndoStack::instance() {
|
||||
return &undo_stack;
|
||||
}
|
||||
|
||||
QtUndoNotifier::QtUndoNotifier(QObject *parent) : QObject(parent) {
|
||||
UndoStack::instance()->setCallbacks({
|
||||
.index_changed = [this]() { emit indexChanged(); },
|
||||
.clean_changed = [this](bool clean) { emit cleanChanged(clean); },
|
||||
});
|
||||
}
|
||||
|
||||
QtUndoNotifier *undoNotifier() {
|
||||
static QtUndoNotifier notifier;
|
||||
return ¬ifier;
|
||||
}
|
||||
|
||||
// EditMsgCommand
|
||||
|
||||
EditMsgCommand::EditMsgCommand(const MessageId &id, const std::string &name, int size,
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "tools/cabana/core/observable.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
|
||||
@@ -21,11 +19,6 @@ public:
|
||||
|
||||
class UndoStack {
|
||||
public:
|
||||
struct Callbacks {
|
||||
std::function<void()> index_changed;
|
||||
std::function<void(bool)> clean_changed;
|
||||
};
|
||||
|
||||
void push(UndoCommand *cmd); // takes ownership and calls redo()
|
||||
void undo();
|
||||
void redo();
|
||||
@@ -36,31 +29,18 @@ public:
|
||||
bool canRedo() const { return index_ < (int)commands_.size(); }
|
||||
std::string undoText() const { return canUndo() ? commands_[index_ - 1]->text : ""; }
|
||||
std::string redoText() const { return canRedo() ? commands_[index_]->text : ""; }
|
||||
void setCallbacks(Callbacks callbacks) { callbacks_ = std::move(callbacks); }
|
||||
static UndoStack *instance();
|
||||
|
||||
Observable<> indexChanged;
|
||||
Observable<bool> cleanChanged;
|
||||
|
||||
private:
|
||||
void setIndex(int index);
|
||||
std::vector<std::unique_ptr<UndoCommand>> commands_;
|
||||
int index_ = 0;
|
||||
int clean_index_ = 0;
|
||||
Callbacks callbacks_;
|
||||
};
|
||||
|
||||
// emits Qt signals for the global undo stack
|
||||
class QtUndoNotifier : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit QtUndoNotifier(QObject *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void indexChanged();
|
||||
void cleanChanged(bool clean);
|
||||
};
|
||||
|
||||
QtUndoNotifier *undoNotifier();
|
||||
|
||||
class EditMsgCommand : public UndoCommand {
|
||||
public:
|
||||
EditMsgCommand(const MessageId &id, const std::string &name, int size, const std::string &node,
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace observable_detail {
|
||||
struct HandlerTable {
|
||||
virtual ~HandlerTable() = default;
|
||||
virtual void erase(int id) = 0;
|
||||
};
|
||||
} // namespace observable_detail
|
||||
|
||||
// disconnects on destruction; safe to outlive the Observable
|
||||
class Connection {
|
||||
public:
|
||||
Connection() = default;
|
||||
Connection(std::weak_ptr<observable_detail::HandlerTable> table, int id) : table_(std::move(table)), id_(id) {}
|
||||
Connection(Connection &&other) noexcept { *this = std::move(other); }
|
||||
Connection &operator=(Connection &&other) noexcept {
|
||||
if (this != &other) {
|
||||
disconnect();
|
||||
table_ = std::move(other.table_);
|
||||
id_ = std::exchange(other.id_, -1);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
Connection(const Connection &) = delete;
|
||||
Connection &operator=(const Connection &) = delete;
|
||||
~Connection() { disconnect(); }
|
||||
|
||||
void disconnect() {
|
||||
if (auto table = table_.lock()) table->erase(id_);
|
||||
table_.reset();
|
||||
id_ = -1;
|
||||
}
|
||||
|
||||
private:
|
||||
std::weak_ptr<observable_detail::HandlerTable> table_;
|
||||
int id_ = -1;
|
||||
};
|
||||
|
||||
using Connections = std::vector<Connection>;
|
||||
|
||||
// main thread only. handlers may disconnect (or destroy the Observable) while being invoked.
|
||||
template <typename... Args>
|
||||
class Observable {
|
||||
public:
|
||||
using Handler = std::function<void(Args...)>;
|
||||
|
||||
Observable() = default;
|
||||
Observable(const Observable &) = delete;
|
||||
Observable &operator=(const Observable &) = delete;
|
||||
|
||||
[[nodiscard]] Connection connect(Handler handler) {
|
||||
int id = table_->next_id++;
|
||||
table_->handlers.emplace(id, std::make_shared<Handler>(std::move(handler)));
|
||||
return Connection(table_, id);
|
||||
}
|
||||
|
||||
void operator()(Args... args) const {
|
||||
auto table = table_;
|
||||
std::vector<int> ids;
|
||||
ids.reserve(table->handlers.size());
|
||||
for (const auto &[id, _] : table->handlers) ids.push_back(id);
|
||||
for (int id : ids) {
|
||||
auto it = table->handlers.find(id);
|
||||
if (it == table->handlers.end()) continue;
|
||||
auto handler = it->second;
|
||||
(*handler)(args...);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
struct Table : observable_detail::HandlerTable {
|
||||
std::map<int, std::shared_ptr<Handler>> handlers;
|
||||
int next_id = 0;
|
||||
void erase(int id) override { handlers.erase(id); }
|
||||
};
|
||||
std::shared_ptr<Table> table_ = std::make_shared<Table>();
|
||||
};
|
||||
@@ -17,7 +17,7 @@ bool DBCManager::open(const SourceSet &sources, const std::string &dbc_file_name
|
||||
return false;
|
||||
}
|
||||
|
||||
if (callbacks_.file_changed) callbacks_.file_changed();
|
||||
fileChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ bool DBCManager::open(const SourceSet &sources, const std::string &name, const s
|
||||
return false;
|
||||
}
|
||||
|
||||
if (callbacks_.file_changed) callbacks_.file_changed();
|
||||
fileChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -40,26 +40,26 @@ void DBCManager::close(const SourceSet &sources) {
|
||||
for (auto s : sources) {
|
||||
dbc_files[s] = nullptr;
|
||||
}
|
||||
if (callbacks_.file_changed) callbacks_.file_changed();
|
||||
fileChanged();
|
||||
}
|
||||
|
||||
void DBCManager::close(DBCFile *dbc_file) {
|
||||
for (auto &[_, f] : dbc_files) {
|
||||
if (f.get() == dbc_file) f = nullptr;
|
||||
}
|
||||
if (callbacks_.file_changed) callbacks_.file_changed();
|
||||
fileChanged();
|
||||
}
|
||||
|
||||
void DBCManager::closeAll() {
|
||||
dbc_files.clear();
|
||||
if (callbacks_.file_changed) callbacks_.file_changed();
|
||||
fileChanged();
|
||||
}
|
||||
|
||||
void DBCManager::addSignal(const MessageId &id, const cabana::Signal &sig) {
|
||||
if (auto m = msg(id)) {
|
||||
if (auto s = m->addSignal(sig)) {
|
||||
if (callbacks_.signal_added) callbacks_.signal_added(id, s);
|
||||
if (callbacks_.mask_updated) callbacks_.mask_updated();
|
||||
signalAdded(id, s);
|
||||
maskUpdated();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,8 +67,8 @@ void DBCManager::addSignal(const MessageId &id, const cabana::Signal &sig) {
|
||||
void DBCManager::updateSignal(const MessageId &id, const std::string &sig_name, const cabana::Signal &sig) {
|
||||
if (auto m = msg(id)) {
|
||||
if (auto s = m->updateSignal(sig_name, sig)) {
|
||||
if (callbacks_.signal_updated) callbacks_.signal_updated(s);
|
||||
if (callbacks_.mask_updated) callbacks_.mask_updated();
|
||||
signalUpdated(s);
|
||||
maskUpdated();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,9 +76,9 @@ void DBCManager::updateSignal(const MessageId &id, const std::string &sig_name,
|
||||
void DBCManager::removeSignal(const MessageId &id, const std::string &sig_name) {
|
||||
if (auto m = msg(id)) {
|
||||
if (auto s = m->sig(sig_name)) {
|
||||
if (callbacks_.signal_removed) callbacks_.signal_removed(s);
|
||||
signalRemoved(s);
|
||||
m->removeSignal(sig_name);
|
||||
if (callbacks_.mask_updated) callbacks_.mask_updated();
|
||||
maskUpdated();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,15 +87,15 @@ void DBCManager::updateMsg(const MessageId &id, const std::string &name, uint32_
|
||||
auto dbc_file = findDBCFile(id);
|
||||
assert(dbc_file); // This should be impossible
|
||||
dbc_file->updateMsg(id, name, size, node, comment);
|
||||
if (callbacks_.msg_updated) callbacks_.msg_updated(id);
|
||||
msgUpdated(id);
|
||||
}
|
||||
|
||||
void DBCManager::removeMsg(const MessageId &id) {
|
||||
auto dbc_file = findDBCFile(id);
|
||||
assert(dbc_file); // This should be impossible
|
||||
dbc_file->removeMsg(id);
|
||||
if (callbacks_.msg_removed) callbacks_.msg_removed(id);
|
||||
if (callbacks_.mask_updated) callbacks_.mask_updated();
|
||||
msgRemoved(id);
|
||||
maskUpdated();
|
||||
}
|
||||
|
||||
std::string DBCManager::newMsgName(const MessageId &id) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "tools/cabana/core/observable.h"
|
||||
#include "tools/cabana/dbc/dbcfile.h"
|
||||
|
||||
typedef std::set<int> SourceSet;
|
||||
@@ -15,16 +15,6 @@ inline bool operator<(const std::shared_ptr<DBCFile> &l, const std::shared_ptr<D
|
||||
|
||||
class DBCManager {
|
||||
public:
|
||||
struct Callbacks {
|
||||
std::function<void(MessageId, const cabana::Signal *)> signal_added;
|
||||
std::function<void(const cabana::Signal *)> signal_removed;
|
||||
std::function<void(const cabana::Signal *)> signal_updated;
|
||||
std::function<void(MessageId)> msg_updated;
|
||||
std::function<void(MessageId)> msg_removed;
|
||||
std::function<void()> file_changed;
|
||||
std::function<void()> mask_updated;
|
||||
};
|
||||
|
||||
DBCManager() = default;
|
||||
bool open(const SourceSet &sources, const std::string &dbc_file_name, std::string *error = nullptr);
|
||||
bool open(const SourceSet &sources, const std::string &name, const std::string &content, std::string *error = nullptr);
|
||||
@@ -54,11 +44,17 @@ public:
|
||||
DBCFile *findDBCFile(const uint8_t source);
|
||||
inline DBCFile *findDBCFile(const MessageId &id) { return findDBCFile(id.source); }
|
||||
std::set<DBCFile *> allDBCFiles();
|
||||
void setCallbacks(Callbacks callbacks) { callbacks_ = std::move(callbacks); }
|
||||
|
||||
Observable<MessageId, const cabana::Signal *> signalAdded;
|
||||
Observable<const cabana::Signal *> signalRemoved;
|
||||
Observable<const cabana::Signal *> signalUpdated;
|
||||
Observable<MessageId> msgUpdated;
|
||||
Observable<MessageId> msgRemoved;
|
||||
Observable<> fileChanged;
|
||||
Observable<> maskUpdated;
|
||||
|
||||
private:
|
||||
std::map<int, std::shared_ptr<DBCFile>> dbc_files;
|
||||
Callbacks callbacks_;
|
||||
};
|
||||
|
||||
DBCManager *dbc();
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
#include "tools/cabana/dbc/dbcqt.h"
|
||||
|
||||
QtDBCNotifier::QtDBCNotifier(QObject *parent) : QObject(parent) {
|
||||
dbc()->setCallbacks({
|
||||
.signal_added = [this](MessageId id, const cabana::Signal *sig) { emit signalAdded(id, sig); },
|
||||
.signal_removed = [this](const cabana::Signal *sig) { emit signalRemoved(sig); },
|
||||
.signal_updated = [this](const cabana::Signal *sig) { emit signalUpdated(sig); },
|
||||
.msg_updated = [this](MessageId id) { emit msgUpdated(id); },
|
||||
.msg_removed = [this](MessageId id) { emit msgRemoved(id); },
|
||||
.file_changed = [this]() { emit DBCFileChanged(); },
|
||||
.mask_updated = [this]() { emit maskUpdated(); },
|
||||
});
|
||||
}
|
||||
|
||||
QtDBCNotifier *dbcNotifier() {
|
||||
static QtDBCNotifier notifier;
|
||||
return ¬ifier;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QMetaType>
|
||||
#include <QObject>
|
||||
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
|
||||
Q_DECLARE_METATYPE(MessageId)
|
||||
Q_DECLARE_METATYPE(ValueDescription)
|
||||
|
||||
class QtDBCNotifier : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit QtDBCNotifier(QObject *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void signalAdded(MessageId id, const cabana::Signal *sig);
|
||||
void signalRemoved(const cabana::Signal *sig);
|
||||
void signalUpdated(const cabana::Signal *sig);
|
||||
void msgUpdated(MessageId id);
|
||||
void msgRemoved(MessageId id);
|
||||
void DBCFileChanged();
|
||||
void maskUpdated();
|
||||
};
|
||||
|
||||
QtDBCNotifier *dbcNotifier();
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "tools/cabana/detailwidget.h"
|
||||
#include "tools/cabana/dbc/dbcqt.h"
|
||||
|
||||
#include <QFormLayout>
|
||||
#include <QMenu>
|
||||
@@ -56,9 +55,9 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart
|
||||
QObject::connect(signal_view, &SignalView::showChart, charts, &ChartsWidget::showChart);
|
||||
QObject::connect(signal_view, &SignalView::highlight, binary_view, &BinaryView::highlight);
|
||||
QObject::connect(tab_widget, &QTabWidget::currentChanged, [this]() { updateState(); });
|
||||
QObject::connect(can, &AbstractStream::msgsReceived, this, &DetailWidget::updateState);
|
||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &DetailWidget::refresh);
|
||||
QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &DetailWidget::refresh);
|
||||
connections_.push_back(can->msgsReceived.connect([this](const std::set<MessageId> *msgs, bool) { updateState(msgs); }));
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() { refresh(); }));
|
||||
connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { refresh(); }));
|
||||
QObject::connect(tabbar, &QTabBar::customContextMenuRequested, this, &DetailWidget::showTabBarContextMenu);
|
||||
QObject::connect(tabbar, &QTabBar::currentChanged, [this](int index) {
|
||||
if (index != -1) {
|
||||
@@ -97,11 +96,11 @@ void DetailWidget::createToolBar() {
|
||||
layout()->addWidget(toolbar);
|
||||
|
||||
connect(heatmap_live, &QAbstractButton::toggled, this, [this](bool on) { binary_view->setHeatmapLiveMode(on); });
|
||||
connect(can, &AbstractStream::timeRangeChanged, this, [=](const std::optional<std::pair<double, double>> &range) {
|
||||
connections_.push_back(can->timeRangeChanged.connect([=](const std::optional<std::pair<double, double>> &range) {
|
||||
auto text = range ? QString("%1 - %2").arg(range->first, 0, 'f', 3).arg(range->second, 0, 'f', 3) : "All";
|
||||
heatmap_all->setText(text);
|
||||
(range ? heatmap_all : heatmap_live)->setChecked(true);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
void DetailWidget::showTabBarContextMenu(const QPoint &pt) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QSpinBox>
|
||||
#include <QSplitter>
|
||||
#include <QTabWidget>
|
||||
#include <QTextEdit>
|
||||
@@ -11,6 +12,7 @@
|
||||
#include "tools/cabana/historylog.h"
|
||||
#include "tools/cabana/signalview.h"
|
||||
#include "tools/cabana/utils/elidedlabel.h"
|
||||
#include "tools/cabana/utils/qtutil.h"
|
||||
|
||||
class EditMessageDialog : public QDialog {
|
||||
public:
|
||||
@@ -57,6 +59,7 @@ private:
|
||||
SignalView *signal_view;
|
||||
ChartsWidget *charts;
|
||||
QSplitter *splitter;
|
||||
Connections connections_;
|
||||
};
|
||||
|
||||
class CenterWidget : public QWidget {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "tools/cabana/historylog.h"
|
||||
#include "tools/cabana/dbc/dbcqt.h"
|
||||
|
||||
#include <functional>
|
||||
|
||||
@@ -10,6 +9,12 @@
|
||||
#include "tools/cabana/commands.h"
|
||||
#include "tools/cabana/utils/export.h"
|
||||
|
||||
HistoryLogModel::HistoryLogModel(QObject *parent) : QAbstractTableModel(parent) {
|
||||
connections_.push_back(can->seekedTo.connect([this](double) { reset(); }));
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() { reset(); }));
|
||||
connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { reset(); }));
|
||||
}
|
||||
|
||||
QVariant HistoryLogModel::data(const QModelIndex &index, int role) const {
|
||||
const auto &m = messages[index.row()];
|
||||
const int col = index.column();
|
||||
@@ -207,9 +212,6 @@ LogsWidget::LogsWidget(QWidget *parent) : QFrame(parent) {
|
||||
QObject::connect(comp_box, SIGNAL(activated(int)), this, SLOT(filterChanged()));
|
||||
QObject::connect(value_edit, &QLineEdit::textEdited, this, &LogsWidget::filterChanged);
|
||||
QObject::connect(export_btn, &QToolButton::clicked, this, &LogsWidget::exportToCSV);
|
||||
QObject::connect(can, &AbstractStream::seekedTo, model, &HistoryLogModel::reset);
|
||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, model, &HistoryLogModel::reset);
|
||||
QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, model, &HistoryLogModel::reset);
|
||||
QObject::connect(model, &HistoryLogModel::modelReset, this, &LogsWidget::modelReset);
|
||||
QObject::connect(model, &HistoryLogModel::rowsInserted, [this]() { export_btn->setEnabled(true); });
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
#include "tools/cabana/utils/qtutil.h"
|
||||
|
||||
class HeaderView : public QHeaderView {
|
||||
public:
|
||||
@@ -22,7 +23,7 @@ class HistoryLogModel : public QAbstractTableModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
HistoryLogModel(QObject *parent) : QAbstractTableModel(parent) {}
|
||||
HistoryLogModel(QObject *parent);
|
||||
void setMessage(const MessageId &message_id);
|
||||
void updateState(bool clear = false);
|
||||
void setFilter(int sig_idx, const QString &value, std::function<bool(double, double)> cmp);
|
||||
@@ -54,6 +55,7 @@ public:
|
||||
std::deque<Message> messages;
|
||||
std::vector<cabana::Signal *> sigs;
|
||||
bool hex_mode = false;
|
||||
Connections connections_;
|
||||
};
|
||||
|
||||
class LogsWidget : public QFrame {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "tools/cabana/mainwin.h"
|
||||
#include "tools/cabana/dbc/dbcqt.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
@@ -20,9 +19,11 @@
|
||||
|
||||
#include "json11/json11.hpp"
|
||||
#include "tools/cabana/commands.h"
|
||||
#include "tools/cabana/settingsdialog.h"
|
||||
#include "tools/cabana/streamselector.h"
|
||||
#include "tools/cabana/tools/findsignal.h"
|
||||
#include "tools/cabana/utils/export.h"
|
||||
#include "tools/cabana/utils/qtutil.h"
|
||||
#include "tools/replay/py_downloader.h"
|
||||
#include "tools/replay/util.h"
|
||||
|
||||
@@ -41,15 +42,13 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW
|
||||
restoreGeometry(utils::qbytes(settings.geometry));
|
||||
restoreState(utils::qbytes(settings.window_state));
|
||||
|
||||
// install handlers
|
||||
// download handlers are called from download threads
|
||||
static auto static_main_win = this;
|
||||
qRegisterMetaType<uint64_t>("uint64_t");
|
||||
qRegisterMetaType<SourceSet>("SourceSet");
|
||||
installDownloadProgressHandler([](uint64_t cur, uint64_t total, bool success) {
|
||||
emit static_main_win->updateProgressBar(cur, total, success);
|
||||
utils::runOnMainThread([=]() { static_main_win->updateDownloadProgress(cur, total, success); });
|
||||
});
|
||||
installMessageHandler([](ReplyMsgType type, const std::string msg) {
|
||||
emit static_main_win->showMessage(QString::fromStdString(msg), 2000);
|
||||
utils::runOnMainThread([=]() { static_main_win->statusBar()->showMessage(QString::fromStdString(msg), 2000); });
|
||||
});
|
||||
|
||||
setStyleSheet(QString(R"(QMainWindow::separator {
|
||||
@@ -57,11 +56,14 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW
|
||||
height: %1px; /* when horizontal */
|
||||
})").arg(style()->pixelMetric(QStyle::PM_SplitterWidth)));
|
||||
|
||||
QObject::connect(this, &MainWindow::showMessage, statusBar(), &QStatusBar::showMessage);
|
||||
QObject::connect(this, &MainWindow::updateProgressBar, this, &MainWindow::updateDownloadProgress);
|
||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &MainWindow::DBCFileChanged);
|
||||
QObject::connect(undoNotifier(), &QtUndoNotifier::cleanChanged, this, &MainWindow::undoStackCleanChanged);
|
||||
QObject::connect(&settings, &Settings::changed, this, &MainWindow::updateStatus);
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() { DBCFileChanged(); }));
|
||||
connections_.push_back(UndoStack::instance()->cleanChanged.connect([this](bool clean) { undoStackCleanChanged(clean); }));
|
||||
connections_.push_back(settings.changed.connect([this]() { updateStatus(); }));
|
||||
|
||||
// temporary pump for the non-Qt main thread queue until imgui owns the loop
|
||||
auto *queue_timer = new QTimer(this);
|
||||
QObject::connect(queue_timer, &QTimer::timeout, utils::drainMainThreadQueue);
|
||||
queue_timer->start(10);
|
||||
|
||||
QTimer::singleShot(0, this, [=]() { stream ? openStream(stream, dbc_file) : selectAndOpenStream(); });
|
||||
show();
|
||||
@@ -136,7 +138,7 @@ void MainWindow::createActions() {
|
||||
undo_act->setShortcuts(QKeySequence::Undo);
|
||||
redo_act = edit_menu->addAction(tr("&Redo"), []() { UndoStack::instance()->redo(); });
|
||||
redo_act->setShortcuts(QKeySequence::Redo);
|
||||
QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &MainWindow::updateUndoRedoActions);
|
||||
connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { updateUndoRedoActions(); }));
|
||||
updateUndoRedoActions();
|
||||
|
||||
// View Menu
|
||||
@@ -259,14 +261,14 @@ void MainWindow::selectAndOpenStream() {
|
||||
if (dlg.exec()) {
|
||||
openStream(dlg.stream(), dlg.dbcFile());
|
||||
} else if (!can) {
|
||||
openStream(new DummyStream(this));
|
||||
openStream(new DummyStream());
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::closeStream() {
|
||||
openStream(new DummyStream(this));
|
||||
openStream(new DummyStream());
|
||||
if (dbc()->nonEmptyDBCCount() > 0) {
|
||||
emit dbcNotifier()->DBCFileChanged();
|
||||
dbc()->fileChanged();
|
||||
}
|
||||
statusBar()->showMessage(tr("stream closed"));
|
||||
}
|
||||
@@ -336,13 +338,19 @@ void MainWindow::loadFromClipboard(SourceSet s, bool close_all) {
|
||||
}
|
||||
}
|
||||
|
||||
// stream threads read the global `can` until its destructor joins them
|
||||
MainWindow::~MainWindow() {
|
||||
delete can;
|
||||
can = nullptr;
|
||||
}
|
||||
|
||||
void MainWindow::openStream(AbstractStream *stream, const QString &dbc_file) {
|
||||
if (can) {
|
||||
QObject::connect(can, &QObject::destroyed, this, [=]() { startStream(stream, dbc_file); });
|
||||
can->deleteLater();
|
||||
} else {
|
||||
startStream(stream, dbc_file);
|
||||
}
|
||||
stream_connections_.clear();
|
||||
if (wait_dlg_) wait_dlg_->deleteLater();
|
||||
wait_dlg_ = nullptr;
|
||||
delete can;
|
||||
can = nullptr;
|
||||
startStream(stream, dbc_file);
|
||||
}
|
||||
|
||||
void MainWindow::startStream(AbstractStream *stream, QString dbc_file) {
|
||||
@@ -350,8 +358,10 @@ void MainWindow::startStream(AbstractStream *stream, QString dbc_file) {
|
||||
delete messages_widget;
|
||||
delete video_splitter;
|
||||
|
||||
can = stream;
|
||||
can->setParent(this); // take ownership
|
||||
can = stream; // take ownership
|
||||
stream_connections_.push_back(can->error.connect([this](const std::string &msg) {
|
||||
QMessageBox::warning(this, tr("Error"), QString::fromStdString(msg));
|
||||
}));
|
||||
can->start();
|
||||
|
||||
loadFile(dbc_file);
|
||||
@@ -373,18 +383,19 @@ void MainWindow::startStream(AbstractStream *stream, QString dbc_file) {
|
||||
newFile();
|
||||
}
|
||||
|
||||
QObject::connect(can, &AbstractStream::eventsMerged, this, &MainWindow::eventsMerged);
|
||||
stream_connections_.push_back(can->eventsMerged.connect([this](const MessageEventsMap &) { eventsMerged(); }));
|
||||
|
||||
if (has_stream) {
|
||||
auto wait_dlg = new QProgressDialog(
|
||||
wait_dlg_ = new QProgressDialog(
|
||||
can->liveStreaming() ? tr("Waiting for the live stream to start...") : tr("Loading segment data..."),
|
||||
tr("&Abort"), 0, 100, this);
|
||||
wait_dlg->setWindowModality(Qt::WindowModal);
|
||||
wait_dlg->setFixedSize(400, wait_dlg->sizeHint().height());
|
||||
QObject::connect(wait_dlg, &QProgressDialog::canceled, this, &MainWindow::close);
|
||||
QObject::connect(can, &AbstractStream::eventsMerged, wait_dlg, &QProgressDialog::deleteLater);
|
||||
QObject::connect(this, &MainWindow::updateProgressBar, wait_dlg, [=](uint64_t cur, uint64_t total, bool success) {
|
||||
wait_dlg->setValue((int)((cur / (double)total) * 100));
|
||||
wait_dlg_->setWindowModality(Qt::WindowModal);
|
||||
wait_dlg_->setFixedSize(400, wait_dlg_->sizeHint().height());
|
||||
QObject::connect(wait_dlg_, &QProgressDialog::canceled, this, &MainWindow::close);
|
||||
wait_dlg_connection_ = can->eventsMerged.connect([this](const MessageEventsMap &) {
|
||||
wait_dlg_->deleteLater();
|
||||
wait_dlg_ = nullptr;
|
||||
wait_dlg_connection_.disconnect();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -544,6 +555,7 @@ void MainWindow::remindSaveChanges() {
|
||||
}
|
||||
|
||||
void MainWindow::updateDownloadProgress(uint64_t cur, uint64_t total, bool success) {
|
||||
if (wait_dlg_) wait_dlg_->setValue((int)((cur / (double)total) * 100));
|
||||
if (success && cur < total) {
|
||||
progress_bar->setValue((cur / (double)total) * 100);
|
||||
progress_bar->setFormat(tr("Downloading %p% (%1)").arg(formattedDataSize(total).c_str()));
|
||||
@@ -610,7 +622,7 @@ void MainWindow::closeEvent(QCloseEvent *event) {
|
||||
}
|
||||
|
||||
void MainWindow::setOption() {
|
||||
SettingsDlg dlg(this);
|
||||
SettingsDialog dlg(this);
|
||||
dlg.exec();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,11 +19,14 @@
|
||||
#include "tools/cabana/videowidget.h"
|
||||
#include "tools/cabana/tools/findsimilarbits.h"
|
||||
|
||||
class QProgressDialog;
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MainWindow(AbstractStream *stream, const QString &dbc_file);
|
||||
~MainWindow();
|
||||
void toggleChartsDocking();
|
||||
void showStatusMessage(const QString &msg, int timeout = 0) { statusBar()->showMessage(msg, timeout); }
|
||||
void loadFile(const QString &fn, SourceSet s = SOURCE_ALL);
|
||||
@@ -42,10 +45,6 @@ public slots:
|
||||
void saveAs();
|
||||
void saveToClipboard();
|
||||
|
||||
signals:
|
||||
void showMessage(const QString &msg, int timeout);
|
||||
void updateProgressBar(uint64_t cur, uint64_t total, bool success);
|
||||
|
||||
protected:
|
||||
void startStream(AbstractStream *stream, QString dbc_file);
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
@@ -104,6 +103,10 @@ protected:
|
||||
QAction *redo_act = nullptr;
|
||||
QString car_fingerprint;
|
||||
std::vector<uint8_t> default_state;
|
||||
Connections connections_;
|
||||
Connections stream_connections_;
|
||||
Connection wait_dlg_connection_;
|
||||
QProgressDialog *wait_dlg_ = nullptr;
|
||||
};
|
||||
|
||||
class HelpOverlay : public QWidget {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "tools/cabana/messageswidget.h"
|
||||
#include "tools/cabana/dbc/dbcqt.h"
|
||||
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
@@ -43,9 +42,6 @@ MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget
|
||||
QObject::connect(menu, &QMenu::aboutToShow, this, &MessagesWidget::menuAboutToShow);
|
||||
QObject::connect(header, &MessageViewHeader::customContextMenuRequested, this, &MessagesWidget::headerContextMenuEvent);
|
||||
QObject::connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, header, &MessageViewHeader::updateHeaderPositions);
|
||||
QObject::connect(can, &AbstractStream::msgsReceived, model, &MessageListModel::msgsReceived);
|
||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, model, &MessageListModel::dbcModified);
|
||||
QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, model, &MessageListModel::dbcModified);
|
||||
QObject::connect(model, &MessageListModel::modelReset, [this]() {
|
||||
if (current_msg_id) {
|
||||
selectMessage(*current_msg_id);
|
||||
@@ -96,7 +92,7 @@ QWidget *MessagesWidget::createToolBar() {
|
||||
|
||||
QObject::connect(suppress_add, &QPushButton::clicked, this, &MessagesWidget::suppressHighlighted);
|
||||
QObject::connect(suppress_clear, &QPushButton::clicked, this, &MessagesWidget::suppressHighlighted);
|
||||
QObject::connect(suppress_defined_signals, &QCheckBox::stateChanged, can, &AbstractStream::suppressDefinedSignals);
|
||||
QObject::connect(suppress_defined_signals, &QCheckBox::stateChanged, this, [](int state) { can->suppressDefinedSignals(state); });
|
||||
|
||||
suppressHighlighted();
|
||||
return toolbar;
|
||||
@@ -161,6 +157,12 @@ void MessagesWidget::setMultiLineBytes(bool multi) {
|
||||
|
||||
// MessageListModel
|
||||
|
||||
MessageListModel::MessageListModel(QObject *parent) : QAbstractTableModel(parent) {
|
||||
connections_.push_back(can->msgsReceived.connect([this](const std::set<MessageId> *msgs, bool has_new_ids) { msgsReceived(msgs, has_new_ids); }));
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() { dbcModified(); }));
|
||||
connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { dbcModified(); }));
|
||||
}
|
||||
|
||||
QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, int role) const {
|
||||
if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
|
||||
switch (section) {
|
||||
@@ -193,7 +195,7 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const {
|
||||
switch (index.column()) {
|
||||
case Column::NAME: return item.name;
|
||||
case Column::SOURCE: return item.id.source != INVALID_SOURCE ? QString::number(item.id.source) : NA;
|
||||
case Column::ADDRESS: return toHexString(item.id.address);
|
||||
case Column::ADDRESS: return QString::fromStdString(utils::toHexString(item.id.address));
|
||||
case Column::NODE: return item.node;
|
||||
case Column::FREQ: return item.id.source != INVALID_SOURCE ? getFreq(can->lastMessage(item.id).freq) : NA;
|
||||
case Column::COUNT: return item.id.source != INVALID_SOURCE ? QString::number(can->lastMessage(item.id).count) : NA;
|
||||
@@ -286,7 +288,7 @@ bool MessageListModel::match(const MessageListModel::Item &item) {
|
||||
match = parseRange(txt, item.id.source);
|
||||
break;
|
||||
case Column::ADDRESS:
|
||||
match = toHexString(item.id.address).contains(txt, Qt::CaseInsensitive);
|
||||
match = QString::fromStdString(utils::toHexString(item.id.address)).contains(txt, Qt::CaseInsensitive);
|
||||
match = match || parseRange(txt, item.id.address, 16);
|
||||
break;
|
||||
case Column::NODE:
|
||||
@@ -299,7 +301,7 @@ bool MessageListModel::match(const MessageListModel::Item &item) {
|
||||
match = parseRange(txt, data.count);
|
||||
break;
|
||||
case Column::DATA:
|
||||
match = utils::toHex(data.dat).contains(txt, Qt::CaseInsensitive);
|
||||
match = QString::fromStdString(utils::toHex(data.dat)).contains(txt, Qt::CaseInsensitive);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user