mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-28 00:42:06 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da6313dbe9 | |||
| 01a843e0ac | |||
| 097dd9b5f2 | |||
| 122ac986de | |||
| bc27262a92 | |||
| 066ba92e77 |
+30
-1
@@ -1,5 +1,34 @@
|
||||
sunnypilot Version 2026.002.000 (2026-xx-xx)
|
||||
sunnypilot Version 2026.002.000 (2026-06-28)
|
||||
========================
|
||||
* What's Changed (sunnypilot/sunnypilot)
|
||||
* ui: update gates for certain toggles by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1830
|
||||
* release: ignore upstream IsReleaseBranch by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1831
|
||||
* manager: disable DEVELOPMENT_ONLY reset by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1833
|
||||
* sunnylink: fix max time offroad values by @nayan8teen in https://github.com/sunnypilot/sunnypilot/pull/1835
|
||||
* ui: show default model name by @nayan8teen in https://github.com/sunnypilot/sunnypilot/pull/1837
|
||||
* sunnylink: add CarParams fallback for brand-specific capabilities by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1839
|
||||
* sunnylink SDUI: tweak DisableUpdate param for clarity by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1842
|
||||
* Revert "DM: Lancia Delta HF Integrale model" by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1849
|
||||
* modeld_v2: safe model validation by @Discountchubbs in https://github.com/sunnypilot/sunnypilot/pull/1855
|
||||
* Revert "deprecate `carState.brake`" for Honda Gas Interceptor by @mvl-boston in https://github.com/sunnypilot/sunnypilot/pull/1860
|
||||
* sunnylink: deprecate legacy params metadata by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1862
|
||||
* ui: reset Enforce Torque Control and NNLC if both are enabled by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1863
|
||||
* What's Changed (sunnypilot/opendbc)
|
||||
* Rivian: suppress ACM hold-the-wheel warning during MADS-only lateral by @lukasloetkolben in https://github.com/sunnypilot/opendbc/pull/465
|
||||
* Sync: `commaai/opendbc:master` → `sunnypilot/opendbc:master` by @sunnyhaibin in https://github.com/sunnypilot/opendbc/pull/479
|
||||
* safety: add option to ignore frequency check for RX checks by @sunnyhaibin in https://github.com/sunnypilot/opendbc/pull/480
|
||||
* Revert "deprecate carState.brake" for Honda Gas Interceptor by @mvl-boston in https://github.com/sunnypilot/opendbc/pull/481
|
||||
* New Contributors (sunnypilot/sunnypilot)
|
||||
* @mvl-boston made their first contribution in https://github.com/sunnypilot/sunnypilot/pull/1860
|
||||
* Full Changelog: https://github.com/sunnypilot/sunnypilot/compare/v2026.001.007...v2026.002.000
|
||||
************************
|
||||
* Synced with commaai's openpilot (v0.11.1)
|
||||
* master commit 69e2c321e49760e52f7983eaa0a5f77cb95de637 (June 02, 2026)
|
||||
* New driver monitoring model
|
||||
* Improved image processing pipeline for driver camera
|
||||
* Improved thermal policy for comma four
|
||||
* Acura MDX 2022-24 support thanks to mvl-boston!
|
||||
* Rivian R1S and R1T 2025 support thanks to lukasloetkolben!
|
||||
|
||||
sunnypilot Version 2026.001.000 (2026-05-06)
|
||||
========================
|
||||
|
||||
+1
-1
Submodule opendbc_repo updated: 4ec5a35e29...b9712d20ef
@@ -13,7 +13,7 @@ from opendbc.car import DT_CTRL, gen_empty_fingerprint, structs
|
||||
from opendbc.car.can_definitions import CanData
|
||||
from opendbc.car.car_helpers import FRAME_FINGERPRINT, interfaces
|
||||
from opendbc.car.fingerprints import MIGRATION
|
||||
from opendbc.car.honda.values import HondaFlags
|
||||
from opendbc.car.honda.values import CAR as HONDA, HondaFlags
|
||||
from opendbc.car.structs import car
|
||||
from opendbc.car.tests.routes import non_tested_cars, routes, CarTestRoute
|
||||
from opendbc.car.values import Platform, PLATFORMS
|
||||
@@ -358,7 +358,13 @@ class TestCarModelBase(unittest.TestCase):
|
||||
self.assertEqual(CS.gasPressed, self.safety.get_gas_pressed_prev())
|
||||
|
||||
if self.safety.get_brake_pressed_prev() != prev_panda_brake:
|
||||
self.assertEqual(CS.brakePressed, self.safety.get_brake_pressed_prev())
|
||||
# TODO: remove this exception once this mismatch is resolved
|
||||
brake_pressed = CS.brakePressed
|
||||
if CS.brakePressed and not self.safety.get_brake_pressed_prev():
|
||||
if self.CP.carFingerprint in (HONDA.HONDA_PILOT, HONDA.HONDA_RIDGELINE) and CS.brake > 0.05:
|
||||
brake_pressed = False
|
||||
|
||||
self.assertEqual(brake_pressed, self.safety.get_brake_pressed_prev())
|
||||
|
||||
if self.safety.get_regen_braking_prev() != prev_panda_regen_braking:
|
||||
self.assertEqual(CS.regenBraking, self.safety.get_regen_braking_prev())
|
||||
@@ -442,7 +448,12 @@ class TestCarModelBase(unittest.TestCase):
|
||||
checks['steeringAngleDeg'] += (angle_can > (self.safety.get_angle_meas_max() + 1) or
|
||||
angle_can < (self.safety.get_angle_meas_min() - 1))
|
||||
|
||||
checks['brakePressed'] += CS.brakePressed != self.safety.get_brake_pressed_prev()
|
||||
# TODO: remove this exception once this mismatch is resolved
|
||||
brake_pressed = CS.brakePressed
|
||||
if CS.brakePressed and not self.safety.get_brake_pressed_prev():
|
||||
if self.CP.carFingerprint in (HONDA.HONDA_PILOT, HONDA.HONDA_RIDGELINE) and CS.brakeDEPRECATED > 0.05:
|
||||
brake_pressed = False
|
||||
checks['brakePressed'] += brake_pressed != self.safety.get_brake_pressed_prev()
|
||||
checks['regenBraking'] += CS.regenBraking != self.safety.get_regen_braking_prev()
|
||||
checks['steeringDisengage'] += CS.steeringDisengage != self.safety.get_steering_disengage_prev()
|
||||
|
||||
|
||||
@@ -134,6 +134,11 @@ class SteeringLayout(Widget):
|
||||
|
||||
enforce_torque_enabled = self._torque_control_toggle.action_item.get_state()
|
||||
nnlc_enabled = self._nnlc_toggle.action_item.get_state()
|
||||
if enforce_torque_enabled and nnlc_enabled:
|
||||
self._torque_control_toggle.action_item.set_state(False)
|
||||
self._nnlc_toggle.action_item.set_state(False)
|
||||
enforce_torque_enabled = False
|
||||
nnlc_enabled = False
|
||||
self._nnlc_toggle.action_item.set_enabled(ui_state.is_offroad() and torque_allowed and not enforce_torque_enabled)
|
||||
self._torque_control_toggle.action_item.set_enabled(ui_state.is_offroad() and torque_allowed and not nnlc_enabled)
|
||||
self._torque_customization_button.action_item.set_enabled(self._torque_control_toggle.action_item.get_state())
|
||||
|
||||
@@ -179,6 +179,10 @@ class UIStateSP:
|
||||
CP = self.CP
|
||||
|
||||
if CP is not None:
|
||||
if self.params.get_bool("EnforceTorqueControl") and self.params.get_bool("NeuralNetworkLateralControl"):
|
||||
self.params.put_bool("EnforceTorqueControl", False, block=True)
|
||||
self.params.put_bool("NeuralNetworkLateralControl", False, block=True)
|
||||
|
||||
# Angle steering: no torque-based lateral controls
|
||||
if CP.steerControlType == car.CarParams.SteerControlType.angle:
|
||||
self.params.remove("EnforceTorqueControl")
|
||||
|
||||
@@ -188,7 +188,7 @@ def make_supercombo_input_queues(input_shapes, frame_skip, device):
|
||||
n_frames = img_shape[1] // 6
|
||||
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img_shape[2], img_shape[3])
|
||||
|
||||
npy_keys = {}
|
||||
numpy_keys = {}
|
||||
queue_keys = {}
|
||||
|
||||
for key, shape in input_shapes.items():
|
||||
@@ -196,7 +196,7 @@ def make_supercombo_input_queues(input_shapes, frame_skip, device):
|
||||
continue
|
||||
if len(shape) == 3 and shape[1] > 1:
|
||||
if key.startswith('desire'):
|
||||
npy_keys[key] = np.zeros(shape[2], dtype=np.float32)
|
||||
numpy_keys[key] = np.zeros(shape[2], dtype=np.float32)
|
||||
queue_keys[f'{key}_q'] = Tensor(
|
||||
np.zeros((frame_skip * shape[1], shape[0], shape[2]), dtype=np.float32),
|
||||
device=device).contiguous().realize()
|
||||
@@ -205,24 +205,24 @@ def make_supercombo_input_queues(input_shapes, frame_skip, device):
|
||||
np.zeros((frame_skip * (shape[1] - 1) + 1, shape[0], shape[2]), dtype=np.float32),
|
||||
device=device).contiguous().realize()
|
||||
else:
|
||||
npy_keys[key] = np.zeros(shape, dtype=np.float32)
|
||||
numpy_keys[key] = np.zeros(shape, dtype=np.float32)
|
||||
elif len(shape) == 2:
|
||||
npy_keys[key] = np.zeros(shape, dtype=np.float32)
|
||||
numpy_keys[key] = np.zeros(shape, dtype=np.float32)
|
||||
|
||||
if 'traffic_convention' not in npy_keys:
|
||||
if 'traffic_convention' not in numpy_keys:
|
||||
tc_shape = input_shapes.get('traffic_convention', (1, 2))
|
||||
npy_keys['traffic_convention'] = np.zeros(tc_shape, dtype=np.float32)
|
||||
numpy_keys['traffic_convention'] = np.zeros(tc_shape, dtype=np.float32)
|
||||
|
||||
npy_keys['tfm'] = np.zeros((3, 3), dtype=np.float32)
|
||||
npy_keys['big_tfm'] = np.zeros((3, 3), dtype=np.float32)
|
||||
numpy_keys['tfm'] = np.zeros((3, 3), dtype=np.float32)
|
||||
numpy_keys['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(),
|
||||
**queue_keys,
|
||||
**{k: Tensor(v, device='NPY').realize() for k, v in npy_keys.items()},
|
||||
**{k: Tensor(v, device='NPY').realize() for k, v in numpy_keys.items()},
|
||||
}
|
||||
return input_queues, npy_keys
|
||||
return input_queues, numpy_keys
|
||||
|
||||
|
||||
def make_run_supercombo(model_runner, nv12: NV12Frame, model_w, model_h,
|
||||
|
||||
@@ -62,11 +62,6 @@ def _find_driving_pkl(bundle):
|
||||
if _pkl_exists(pkl_path):
|
||||
return pkl_path
|
||||
|
||||
fallback = os.path.join(model_root, 'driving_tinygrad.pkl')
|
||||
if _pkl_exists(fallback):
|
||||
return fallback
|
||||
return None
|
||||
|
||||
|
||||
class FrameMeta:
|
||||
frame_id: int = 0
|
||||
@@ -125,7 +120,7 @@ class ModelState(ModelStateBase):
|
||||
self._vision_input_names = [k for k in model_metadata['input_shapes'] if 'img' in k]
|
||||
from openpilot.sunnypilot.modeld_v2.compile_modeld import make_supercombo_input_queues
|
||||
frame_skip = derive_frame_skip({}, model_metadata['input_shapes'])
|
||||
self.input_queues, self.npy = make_supercombo_input_queues(model_metadata['input_shapes'], frame_skip, device=self.DEV)
|
||||
self.input_queues, self.numpy_inputs = make_supercombo_input_queues(model_metadata['input_shapes'], frame_skip, device=self.DEV)
|
||||
else:
|
||||
vision_metadata = metadata['vision']
|
||||
policy_keys = [k for k in metadata if k != 'vision']
|
||||
@@ -143,7 +138,7 @@ class ModelState(ModelStateBase):
|
||||
policy_input_shapes = first_policy_metadata['input_shapes']
|
||||
self._vision_input_names = [k for k in vision_input_shapes if 'img' in k]
|
||||
frame_skip = derive_frame_skip(vision_input_shapes, policy_input_shapes)
|
||||
self.input_queues, self.npy = make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, device=self.DEV)
|
||||
self.input_queues, self.numpy_inputs = make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, device=self.DEV)
|
||||
|
||||
from openpilot.sunnypilot.modeld_v2.parse_model_outputs_split import Parser as SplitParser
|
||||
from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser as CombinedParser
|
||||
@@ -183,7 +178,7 @@ class ModelState(ModelStateBase):
|
||||
|
||||
@property
|
||||
def desire_key(self) -> str:
|
||||
return next(k for k in self.npy if k.startswith('desire'))
|
||||
return next(k for k in self.numpy_inputs if k.startswith('desire'))
|
||||
|
||||
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
||||
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
|
||||
@@ -199,16 +194,16 @@ class ModelState(ModelStateBase):
|
||||
|
||||
desire_key = self.desire_key
|
||||
inputs[desire_key][0] = 0
|
||||
self.npy[desire_key][:] = np.where(inputs[desire_key] - self.prev_desire > .99, inputs[desire_key], 0)
|
||||
self.numpy_inputs[desire_key][:] = np.where(inputs[desire_key] - self.prev_desire > .99, inputs[desire_key], 0)
|
||||
self.prev_desire[:] = inputs[desire_key]
|
||||
for key in ('traffic_convention', 'lateral_control_params'):
|
||||
if key in self.npy and key in inputs:
|
||||
self.npy[key][:] = inputs[key]
|
||||
if key in self.numpy_inputs and key in inputs:
|
||||
self.numpy_inputs[key][:] = inputs[key]
|
||||
|
||||
road_key = next(n for n in bufs if 'big' not in n)
|
||||
wide_key = next(n for n in bufs if 'big' in n)
|
||||
self.npy['tfm'][:, :] = transforms[road_key].reshape(3, 3)
|
||||
self.npy['big_tfm'][:, :] = transforms[wide_key].reshape(3, 3)
|
||||
self.numpy_inputs['tfm'][:, :] = transforms[road_key].reshape(3, 3)
|
||||
self.numpy_inputs['big_tfm'][:, :] = transforms[wide_key].reshape(3, 3)
|
||||
|
||||
if prepare_only:
|
||||
self._warp_enqueue(**self.input_queues, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key])
|
||||
@@ -236,8 +231,8 @@ class ModelState(ModelStateBase):
|
||||
if 'planplus' in outputs and 'plan' in outputs:
|
||||
outputs['plan'] = outputs['plan'] + outputs['planplus']
|
||||
|
||||
if 'desired_curvature' in outputs and 'prev_desired_curv' in self.npy:
|
||||
buf = self.npy['prev_desired_curv']
|
||||
if 'desired_curvature' in outputs and 'prev_desired_curv' in self.numpy_inputs:
|
||||
buf = self.numpy_inputs['prev_desired_curv']
|
||||
buf[0, :-1] = buf[0, 1:]
|
||||
buf[0, -1, :] = outputs['desired_curvature'][0, :] if not self.mlsim else 0
|
||||
|
||||
@@ -409,7 +404,7 @@ def main(demo=False):
|
||||
'traffic_convention': traffic_convention,
|
||||
}
|
||||
|
||||
if 'lateral_control_params' in model.npy:
|
||||
if 'lateral_control_params' in model.numpy_inputs:
|
||||
inputs['lateral_control_params'] = np.array([v_ego, lat_delay], dtype=np.float32)
|
||||
|
||||
mt1 = time.perf_counter()
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
## Neural networks in openpilot
|
||||
To view the architecture of the ONNX networks, you can use [netron](https://netron.app/)
|
||||
|
||||
## Supercombo
|
||||
### Supercombo input format (Full size: 799906 x float32)
|
||||
* **image stream**
|
||||
* Two consecutive images (256 * 512 * 3 in RGB) recorded at 20 Hz : 393216 = 2 * 6 * 128 * 256
|
||||
* Each 256 * 512 image is represented in YUV420 with 6 channels : 6 * 128 * 256
|
||||
* Channels 0,1,2,3 represent the full-res Y channel and are represented in numpy as Y[::2, ::2], Y[::2, 1::2], Y[1::2, ::2], and Y[1::2, 1::2]
|
||||
* Channel 4 represents the half-res U channel
|
||||
* Channel 5 represents the half-res V channel
|
||||
* **wide image stream**
|
||||
* Two consecutive images (256 * 512 * 3 in RGB) recorded at 20 Hz : 393216 = 2 * 6 * 128 * 256
|
||||
* Each 256 * 512 image is represented in YUV420 with 6 channels : 6 * 128 * 256
|
||||
* Channels 0,1,2,3 represent the full-res Y channel and are represented in numpy as Y[::2, ::2], Y[::2, 1::2], Y[1::2, ::2], and Y[1::2, 1::2]
|
||||
* Channel 4 represents the half-res U channel
|
||||
* Channel 5 represents the half-res V channel
|
||||
* **desire**
|
||||
* one-hot encoded buffer to command model to execute certain actions, bit needs to be sent for the past 5 seconds (at 20FPS) : 100 * 8
|
||||
* **traffic convention**
|
||||
* one-hot encoded vector to tell model whether traffic is right-hand or left-hand traffic : 2
|
||||
* **feature buffer**
|
||||
* A buffer of intermediate features that gets appended to the current feature to form a 5 seconds temporal context (at 20FPS) : 99 * 512
|
||||
|
||||
|
||||
### Supercombo output format (Full size: XXX x float32)
|
||||
Read [here](https://github.com/commaai/openpilot/blob/90af436a121164a51da9fa48d093c29f738adf6a/selfdrive/modeld/models/driving.h#L236) for more.
|
||||
|
||||
|
||||
## Driver Monitoring Model
|
||||
* .onnx model can be run with onnx runtimes
|
||||
* .dlc file is a pre-quantized model and only runs on qualcomm DSPs
|
||||
|
||||
### input format
|
||||
* single image W = 1440 H = 960 luminance channel (Y) from the planar YUV420 format:
|
||||
* full input size is 1440 * 960 = 1382400
|
||||
* normalized ranging from 0.0 to 1.0 in float32 (onnx runner) or ranging from 0 to 255 in uint8 (snpe runner)
|
||||
* camera calibration angles (roll, pitch, yaw) from liveCalibration: 3 x float32 inputs
|
||||
|
||||
### output format
|
||||
* 84 x float32 outputs = 2 + 41 * 2 ([parsing example](https://github.com/commaai/openpilot/blob/22ce4e17ba0d3bfcf37f8255a4dd1dc683fe0c38/selfdrive/modeld/models/dmonitoring.cc#L33))
|
||||
* for each person in the front seats (2 * 41)
|
||||
* face pose: 12 = 6 + 6
|
||||
* face orientation [pitch, yaw, roll] in camera frame: 3
|
||||
* face position [dx, dy] relative to image center: 2
|
||||
* normalized face size: 1
|
||||
* standard deviations for above outputs: 6
|
||||
* face visible probability: 1
|
||||
* eyes: 20 = (8 + 1) + (8 + 1) + 1 + 1
|
||||
* eye position and size, and their standard deviations: 8
|
||||
* eye visible probability: 1
|
||||
* eye closed probability: 1
|
||||
* wearing sunglasses probability: 1
|
||||
* face occluded probability: 1
|
||||
* touching wheel probability: 1
|
||||
* paying attention probability: 1
|
||||
* (deprecated) distracted probabilities: 2
|
||||
* using phone probability: 1
|
||||
* distracted probability: 1
|
||||
* common outputs 2
|
||||
* poor camera vision probability: 1
|
||||
* left hand drive probability: 1
|
||||
@@ -1,101 +0,0 @@
|
||||
// clang++ -O2 repro.cc && ./a.out
|
||||
|
||||
#include <sched.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
|
||||
static inline double millis_since_boot() {
|
||||
struct timespec t;
|
||||
clock_gettime(CLOCK_BOOTTIME, &t);
|
||||
return t.tv_sec * 1000.0 + t.tv_nsec * 1e-6;
|
||||
}
|
||||
|
||||
#define MODEL_WIDTH 320
|
||||
#define MODEL_HEIGHT 640
|
||||
|
||||
// null function still breaks it
|
||||
#define input_lambda(x) x
|
||||
|
||||
// this is copied from models/dmonitoring.cc, and is the code that triggers the issue
|
||||
void inner(uint8_t *resized_buf, float *net_input_buf) {
|
||||
int resized_width = MODEL_WIDTH;
|
||||
int resized_height = MODEL_HEIGHT;
|
||||
|
||||
// one shot conversion, O(n) anyway
|
||||
// yuvframe2tensor, normalize
|
||||
for (int r = 0; r < MODEL_HEIGHT/2; r++) {
|
||||
for (int c = 0; c < MODEL_WIDTH/2; c++) {
|
||||
// Y_ul
|
||||
net_input_buf[(c*MODEL_HEIGHT/2) + r] = input_lambda(resized_buf[(2*r*resized_width) + (2*c)]);
|
||||
// Y_ur
|
||||
net_input_buf[(c*MODEL_HEIGHT/2) + r + (2*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = input_lambda(resized_buf[(2*r*resized_width) + (2*c+1)]);
|
||||
// Y_dl
|
||||
net_input_buf[(c*MODEL_HEIGHT/2) + r + ((MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = input_lambda(resized_buf[(2*r*resized_width+1) + (2*c)]);
|
||||
// Y_dr
|
||||
net_input_buf[(c*MODEL_HEIGHT/2) + r + (3*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = input_lambda(resized_buf[(2*r*resized_width+1) + (2*c+1)]);
|
||||
// U
|
||||
net_input_buf[(c*MODEL_HEIGHT/2) + r + (4*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = input_lambda(resized_buf[(resized_width*resized_height) + (r*resized_width/2) + c]);
|
||||
// V
|
||||
net_input_buf[(c*MODEL_HEIGHT/2) + r + (5*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = input_lambda(resized_buf[(resized_width*resized_height) + ((resized_width/2)*(resized_height/2)) + (r*resized_width/2) + c]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float trial() {
|
||||
int resized_width = MODEL_WIDTH;
|
||||
int resized_height = MODEL_HEIGHT;
|
||||
|
||||
int yuv_buf_len = (MODEL_WIDTH/2) * (MODEL_HEIGHT/2) * 6; // Y|u|v -> y|y|y|y|u|v
|
||||
|
||||
// allocate the buffers
|
||||
uint8_t *resized_buf = (uint8_t*)malloc(resized_width*resized_height*3/2);
|
||||
float *net_input_buf = (float*)malloc(yuv_buf_len*sizeof(float));
|
||||
printf("allocate -- %p 0x%x -- %p 0x%lx\n", resized_buf, resized_width*resized_height*3/2, net_input_buf, yuv_buf_len*sizeof(float));
|
||||
|
||||
// test for bad buffers
|
||||
static int CNT = 20;
|
||||
float avg = 0.0;
|
||||
for (int i = 0; i < CNT; i++) {
|
||||
double s4 = millis_since_boot();
|
||||
inner(resized_buf, net_input_buf);
|
||||
double s5 = millis_since_boot();
|
||||
avg += s5-s4;
|
||||
}
|
||||
avg /= CNT;
|
||||
|
||||
// once it's bad, it's reliably bad
|
||||
if (avg > 10) {
|
||||
printf("HIT %f\n", avg);
|
||||
printf("BAD\n");
|
||||
|
||||
for (int i = 0; i < 200; i++) {
|
||||
double s4 = millis_since_boot();
|
||||
inner(resized_buf, net_input_buf);
|
||||
double s5 = millis_since_boot();
|
||||
printf("%.2f ", s5-s4);
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// don't free so we get a different buffer each time
|
||||
//free(resized_buf);
|
||||
//free(net_input_buf);
|
||||
|
||||
return avg;
|
||||
}
|
||||
|
||||
int main() {
|
||||
while (true) {
|
||||
float ret = trial();
|
||||
printf("got %f\n", ret);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,16 +46,6 @@ class TestFindDrivingPkl:
|
||||
assert result is not None
|
||||
assert 'driving_fof_tinygrad.pkl' in result
|
||||
|
||||
def test_finds_fallback_driving_tinygrad(self, tmp_path, monkeypatch):
|
||||
(tmp_path / 'driving_tinygrad.pkl').write_bytes(b'fake')
|
||||
from openpilot.system.hardware import hw
|
||||
monkeypatch.setattr(hw.Paths, 'model_root', staticmethod(lambda: str(tmp_path)))
|
||||
|
||||
bundle = DummyBundle(models=[DummyModel('vision', 'nonexistent.pkl')])
|
||||
result = _find_driving_pkl(bundle)
|
||||
assert result is not None
|
||||
assert 'driving_tinygrad.pkl' in result
|
||||
|
||||
|
||||
# Init — assertion guard
|
||||
|
||||
@@ -84,8 +74,8 @@ class TestStockEquivalence:
|
||||
skip_keys = {'action_t'}
|
||||
assert set(state.input_queues.keys()) == set(stock_queues.keys()) - skip_keys, \
|
||||
f"Queue keys differ: v2={set(state.input_queues.keys())}, stock={set(stock_queues.keys())}"
|
||||
assert set(state.npy.keys()) == set(stock_npy.keys()) - skip_keys, \
|
||||
f"Npy keys differ: v2={set(state.npy.keys())}, stock={set(stock_npy.keys())}"
|
||||
assert set(state.numpy_inputs.keys()) == set(stock_npy.keys()) - skip_keys, \
|
||||
f"Npy keys differ: v2={set(state.numpy_inputs.keys())}, stock={set(stock_npy.keys())}"
|
||||
|
||||
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
|
||||
@@ -188,16 +178,16 @@ class TestInputQueueCreation:
|
||||
def test_npy_contains_transforms(self, archetype_name, model_state_factory):
|
||||
arch = ARCHETYPES[archetype_name]
|
||||
state = model_state_factory(arch)
|
||||
assert 'tfm' in state.npy, f"{arch.name}: 'tfm' missing from npy"
|
||||
assert 'big_tfm' in state.npy, f"{arch.name}: 'big_tfm' missing from npy"
|
||||
assert state.npy['tfm'].shape == (3, 3)
|
||||
assert state.npy['big_tfm'].shape == (3, 3)
|
||||
assert 'tfm' in state.numpy_inputs, f"{arch.name}: 'tfm' missing from npy"
|
||||
assert 'big_tfm' in state.numpy_inputs, f"{arch.name}: 'big_tfm' missing from npy"
|
||||
assert state.numpy_inputs['tfm'].shape == (3, 3)
|
||||
assert state.numpy_inputs['big_tfm'].shape == (3, 3)
|
||||
|
||||
@pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES)
|
||||
def test_npy_contains_desire(self, archetype_name, model_state_factory):
|
||||
arch = ARCHETYPES[archetype_name]
|
||||
state = model_state_factory(arch)
|
||||
assert arch.expected_desire_key in state.npy, \
|
||||
assert arch.expected_desire_key in state.numpy_inputs, \
|
||||
f"{arch.name}: '{arch.expected_desire_key}' missing from npy"
|
||||
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
clang++ -I /home/batman/one/external/tensorflow/include/ -L /home/batman/one/external/tensorflow/lib -Wl,-rpath=/home/batman/one/external/tensorflow/lib main.cc -ltensorflow
|
||||
@@ -1,69 +0,0 @@
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include "tensorflow/c/c_api.h"
|
||||
|
||||
void* read_file(const char* path, size_t* out_len) {
|
||||
FILE* f = fopen(path, "r");
|
||||
if (!f) {
|
||||
return NULL;
|
||||
}
|
||||
fseek(f, 0, SEEK_END);
|
||||
long f_len = ftell(f);
|
||||
rewind(f);
|
||||
|
||||
char* buf = (char*)calloc(f_len, 1);
|
||||
assert(buf);
|
||||
|
||||
size_t num_read = fread(buf, f_len, 1, f);
|
||||
fclose(f);
|
||||
|
||||
if (num_read != 1) {
|
||||
free(buf);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (out_len) {
|
||||
*out_len = f_len;
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
static void DeallocateBuffer(void* data, size_t) {
|
||||
free(data);
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
TF_Buffer* buf;
|
||||
TF_Graph* graph;
|
||||
TF_Status* status;
|
||||
char *path = argv[1];
|
||||
|
||||
// load model
|
||||
{
|
||||
size_t model_size;
|
||||
char tmp[1024];
|
||||
snprintf(tmp, sizeof(tmp), "%s.pb", path);
|
||||
printf("loading model %s\n", tmp);
|
||||
uint8_t *model_data = (uint8_t *)read_file(tmp, &model_size);
|
||||
buf = TF_NewBuffer();
|
||||
buf->data = model_data;
|
||||
buf->length = model_size;
|
||||
buf->data_deallocator = DeallocateBuffer;
|
||||
printf("loaded model of size %d\n", model_size);
|
||||
}
|
||||
|
||||
// import graph
|
||||
status = TF_NewStatus();
|
||||
graph = TF_NewGraph();
|
||||
TF_ImportGraphDefOptions *opts = TF_NewImportGraphDefOptions();
|
||||
TF_GraphImportGraphDef(graph, buf, opts, status);
|
||||
TF_DeleteImportGraphDefOptions(opts);
|
||||
TF_DeleteBuffer(buf);
|
||||
if (TF_GetCode(status) != TF_OK) {
|
||||
printf("FAIL: %s\n", TF_Message(status));
|
||||
} else {
|
||||
printf("SUCCESS\n");
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import tensorflow as tf
|
||||
|
||||
with open(sys.argv[1], "rb") as f:
|
||||
graph_def = tf.compat.v1.GraphDef()
|
||||
graph_def.ParseFromString(f.read())
|
||||
#tf.io.write_graph(graph_def, '', sys.argv[1]+".try")
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import time
|
||||
import numpy as np
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from openpilot.system.manager.process_config import managed_processes
|
||||
|
||||
|
||||
N = int(os.getenv("N", "5"))
|
||||
TIME = int(os.getenv("TIME", "30"))
|
||||
|
||||
if __name__ == "__main__":
|
||||
sock = messaging.sub_sock('modelV2', conflate=False, timeout=1000)
|
||||
|
||||
execution_times = []
|
||||
|
||||
for _ in range(N):
|
||||
os.environ['LOGPRINT'] = 'debug'
|
||||
managed_processes['modeld'].start()
|
||||
time.sleep(5)
|
||||
|
||||
t = []
|
||||
start = time.monotonic()
|
||||
while time.monotonic() - start < TIME:
|
||||
msgs = messaging.drain_sock(sock, wait_for_one=True)
|
||||
for m in msgs:
|
||||
t.append(m.modelV2.modelExecutionTime)
|
||||
|
||||
execution_times.append(np.array(t[10:]) * 1000)
|
||||
managed_processes['modeld'].stop()
|
||||
|
||||
print("\n\n")
|
||||
print(f"ran modeld {N} times for {TIME}s each")
|
||||
for _, t in enumerate(execution_times):
|
||||
print(f"\tavg: {sum(t)/len(t):0.2f}ms, min: {min(t):0.2f}ms, max: {max(t):0.2f}ms")
|
||||
print("\n\n")
|
||||
+117
-85
@@ -6,80 +6,138 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import pickle
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from cereal import custom
|
||||
from openpilot.sunnypilot.models.constants import Meta, MetaTombRaider, MetaSimPose
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.sunnypilot.models.constants import Meta, MetaSimPose, MetaTombRaider
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from pathlib import Path
|
||||
|
||||
# see the README.md for more details on the model selector versioning
|
||||
CURRENT_SELECTOR_VERSION = 15
|
||||
REQUIRED_MIN_SELECTOR_VERSION = 14
|
||||
|
||||
# SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO
|
||||
REQUIRED_JSON_VERSION = 15
|
||||
|
||||
CUSTOM_MODEL_PATH = Paths.model_root()
|
||||
METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl'
|
||||
|
||||
ModelManager = custom.ModelManagerSP
|
||||
_LAST_VALIDATED_RAW = None
|
||||
|
||||
|
||||
def _compute_hash(file_path: str) -> str | None:
|
||||
from openpilot.common.file_chunker import read_file_chunked
|
||||
try:
|
||||
return hashlib.sha256(read_file_chunked(file_path)).hexdigest().lower()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
async def verify_file(file_path: str, expected_hash: str) -> bool:
|
||||
from openpilot.common.file_chunker import read_file_chunked
|
||||
try:
|
||||
data = read_file_chunked(file_path)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
return hashlib.sha256(data).hexdigest().lower() == expected_hash.lower()
|
||||
file_hash = _compute_hash(file_path)
|
||||
return file_hash == expected_hash.lower() if file_hash else False
|
||||
|
||||
|
||||
def _verify_file(file_path: str, expected_hash: str) -> bool:
|
||||
file_hash = _compute_hash(file_path)
|
||||
return file_hash == expected_hash.lower() if file_hash else False
|
||||
|
||||
|
||||
def is_bundle_version_compatible(bundle: dict) -> bool:
|
||||
"""
|
||||
Checks whether the model bundle is compatible with the current selector version constraints.
|
||||
|
||||
The bundle specifies a `minimum_selector_version`, which defines the minimum selector version
|
||||
The bundle parsed from the json specifies a `minimum_selector_version`, which defines the minimum selector version
|
||||
required to load the model. This function ensures that:
|
||||
|
||||
1. The model is not too old: the bundle must require at least `REQUIRED_MIN_SELECTOR_VERSION`.
|
||||
2. The model is not too new: it must support the current selector version (`CURRENT_SELECTOR_VERSION`).
|
||||
|
||||
This allows the selector to enforce both a minimum and maximum range of supported models,
|
||||
even if a model would otherwise be compatible.
|
||||
|
||||
:param bundle: Dictionary containing `minimum_selector_version`, as defined by the model bundle.
|
||||
:type bundle: Dict
|
||||
:return: True if the selector version is within the accepted range for the bundle; otherwise False.
|
||||
:rtype: Bool
|
||||
the bundle MUST match the `REQUIRED_JSON_VERSION` set here in helpers.
|
||||
"""
|
||||
return bool(REQUIRED_MIN_SELECTOR_VERSION <= bundle.get("minimumSelectorVersion", 0) <= CURRENT_SELECTOR_VERSION)
|
||||
return bundle.get("minimumSelectorVersion", 0) == REQUIRED_JSON_VERSION
|
||||
|
||||
|
||||
def get_active_bundle(params: Params = None) -> custom.ModelManagerSP.ModelBundle:
|
||||
"""Gets the active model bundle from cache"""
|
||||
if params is None:
|
||||
params = Params()
|
||||
def _bundle_artifacts(bundle: custom.ModelManagerSP.ModelBundle) -> list[tuple[str, str]]:
|
||||
artifacts = []
|
||||
for model in getattr(bundle, 'models', []) or []:
|
||||
for artifact in (getattr(model, 'artifact', None), getattr(model, 'metadata', None)):
|
||||
if artifact and getattr(artifact, 'fileName', None) and getattr(artifact, 'downloadUri', None):
|
||||
sha256 = getattr(artifact.downloadUri, 'sha256', None)
|
||||
if sha256:
|
||||
artifacts.append((artifact.fileName, sha256))
|
||||
return artifacts
|
||||
|
||||
|
||||
def _bundle_is_valid_locally(bundle: custom.ModelManagerSP.ModelBundle) -> bool:
|
||||
model_root = Paths.model_root()
|
||||
return all(_verify_file(os.path.join(model_root, file_name), expected_hash)
|
||||
for file_name, expected_hash in _bundle_artifacts(bundle))
|
||||
|
||||
|
||||
def _bundle_needs_reset(active_bundle: custom.ModelManagerSP.ModelBundle, available_bundles: list[custom.ModelManagerSP.ModelBundle] | None) -> bool:
|
||||
if active_bundle is None:
|
||||
return False
|
||||
|
||||
if available_bundles is not None:
|
||||
matching_bundle = None
|
||||
for bundle in available_bundles:
|
||||
if getattr(active_bundle, 'ref', None) and getattr(bundle, 'ref', None):
|
||||
if active_bundle.ref == bundle.ref:
|
||||
matching_bundle = bundle
|
||||
break
|
||||
elif getattr(active_bundle, 'internalName', None) == getattr(bundle, 'internalName', None):
|
||||
matching_bundle = bundle
|
||||
break
|
||||
|
||||
if matching_bundle is None:
|
||||
return True
|
||||
if active_bundle.minimumSelectorVersion != matching_bundle.minimumSelectorVersion:
|
||||
return True
|
||||
|
||||
active_runner = getattr(active_bundle, 'runner', None)
|
||||
matching_runner = getattr(matching_bundle, 'runner', None)
|
||||
if active_runner is not None and matching_runner is not None:
|
||||
if getattr(active_runner, 'raw', active_runner) != getattr(matching_runner, 'raw', matching_runner):
|
||||
return True
|
||||
if set(_bundle_artifacts(active_bundle)) != set(_bundle_artifacts(matching_bundle)):
|
||||
return True
|
||||
|
||||
return not _bundle_is_valid_locally(active_bundle)
|
||||
|
||||
|
||||
def validate_active_bundle(params: Params, available_bundles: list[custom.ModelManagerSP.ModelBundle] | None = None) -> None:
|
||||
global _LAST_VALIDATED_RAW
|
||||
|
||||
raw_bundle = params.get("ModelManager_ActiveBundle")
|
||||
if not raw_bundle:
|
||||
return
|
||||
|
||||
if raw_bundle == _LAST_VALIDATED_RAW:
|
||||
return
|
||||
|
||||
active_bundle = get_active_bundle(params, raw_bundle_dict=raw_bundle)
|
||||
if active_bundle is None or _bundle_needs_reset(active_bundle, available_bundles):
|
||||
cloudlog.warning("Active model bundle invalid; resetting to default")
|
||||
params.remove("ModelManager_ActiveBundle")
|
||||
params.put("ModelRunnerTypeCache", int(custom.ModelManagerSP.Runner.stock), block=True)
|
||||
_LAST_VALIDATED_RAW = None
|
||||
else:
|
||||
_LAST_VALIDATED_RAW = raw_bundle
|
||||
|
||||
|
||||
def get_active_bundle(params: Params | None = None, raw_bundle_dict: dict | bytes | None = None) -> "custom.ModelManagerSP.ModelBundle | None":
|
||||
params = params or Params()
|
||||
try:
|
||||
if (active_bundle := params.get("ModelManager_ActiveBundle") or {}) and is_bundle_version_compatible(active_bundle):
|
||||
return custom.ModelManagerSP.ModelBundle(**active_bundle)
|
||||
active_bundle_dict = raw_bundle_dict if raw_bundle_dict is not None else (params.get("ModelManager_ActiveBundle") or {})
|
||||
if active_bundle_dict and is_bundle_version_compatible(active_bundle_dict):
|
||||
return custom.ModelManagerSP.ModelBundle(**active_bundle_dict)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_active_model_runner(params: Params = None, force_check=False) -> int:
|
||||
if params is None:
|
||||
params = Params()
|
||||
|
||||
def get_active_model_runner(params: Params | None = None, force_check: bool = False) -> int:
|
||||
params = params or Params()
|
||||
cached_runner_type = params.get("ModelRunnerTypeCache")
|
||||
if cached_runner_type is not None and not force_check:
|
||||
return cached_runner_type
|
||||
|
||||
runner_type = custom.ModelManagerSP.Runner.stock
|
||||
|
||||
if active_bundle := get_active_bundle(params):
|
||||
runner_type = active_bundle.runner.raw
|
||||
|
||||
@@ -88,66 +146,40 @@ def get_active_model_runner(params: Params = None, force_check=False) -> int:
|
||||
|
||||
return runner_type
|
||||
|
||||
|
||||
def _get_model():
|
||||
if bundle := get_active_bundle():
|
||||
drive_model = next(model for model in bundle.models if model.type == ModelManager.Model.Type.supercombo)
|
||||
return drive_model
|
||||
|
||||
return None
|
||||
|
||||
def load_metadata():
|
||||
metadata_path = METADATA_PATH
|
||||
|
||||
if model := _get_model():
|
||||
metadata_path = f"{CUSTOM_MODEL_PATH}/{model.metadata.fileName}"
|
||||
def load_metadata():
|
||||
model = _get_model()
|
||||
metadata_path = f"{CUSTOM_MODEL_PATH}/{model.metadata.fileName}" if model else METADATA_PATH
|
||||
|
||||
with open(metadata_path, 'rb') as f:
|
||||
return pickle.load(f)
|
||||
|
||||
|
||||
def prepare_inputs(model_metadata) -> dict[str, np.ndarray]:
|
||||
# img buffers are managed in openCL transform code so we don't pass them as inputs
|
||||
inputs = {
|
||||
k: np.zeros(v, dtype=np.float32).flatten()
|
||||
for k, v in model_metadata['input_shapes'].items()
|
||||
if 'img' not in k
|
||||
def prepare_inputs(model_metadata: dict) -> dict[str, np.ndarray]:
|
||||
return {
|
||||
key: np.zeros(shape, dtype=np.float32).flatten()
|
||||
for key, shape in model_metadata['input_shapes'].items()
|
||||
if 'img' not in key
|
||||
}
|
||||
|
||||
return inputs
|
||||
|
||||
def load_meta_constants(model_metadata: dict):
|
||||
""" Loads the appropriate meta model class based on key shapes"""
|
||||
if 'sim_pose' in model_metadata['input_shapes']:
|
||||
return MetaSimPose
|
||||
|
||||
def load_meta_constants(model_metadata):
|
||||
"""
|
||||
Determines and loads the appropriate meta model class based on the metadata provided. The function checks
|
||||
specific keys and conditions within the provided metadata dictionary to identify the corresponding meta
|
||||
model class to return.
|
||||
meta_slice = model_metadata['output_slices']['meta']
|
||||
if (meta_slice.start, meta_slice.stop, meta_slice.step) == (5868, 5921, None):
|
||||
return MetaTombRaider
|
||||
|
||||
:param model_metadata: Dictionary containing metadata about the model. It includes
|
||||
details such as input shapes, output slices, and other configurations for identifying
|
||||
metadata-dependent meta model classes.
|
||||
:type model_metadata: dict
|
||||
:return: The appropriate meta model class (Meta, MetaSimPose, or MetaTombRaider)
|
||||
based on the conditions and metadata provided.
|
||||
:rtype: type
|
||||
"""
|
||||
meta = Meta # Default Meta
|
||||
|
||||
if 'sim_pose' in model_metadata['input_shapes'].keys():
|
||||
# Meta for models with sim_pose input
|
||||
meta = MetaSimPose
|
||||
else:
|
||||
# Meta for Tomb Raider, it does not include sim_pose input but has the same meta slice as previous models
|
||||
meta_slice = model_metadata['output_slices']['meta']
|
||||
meta_tf_slice = slice(5868, 5921, None)
|
||||
|
||||
if (
|
||||
meta_slice.start == meta_tf_slice.start and
|
||||
meta_slice.stop == meta_tf_slice.stop and
|
||||
meta_slice.step == meta_tf_slice.step
|
||||
):
|
||||
meta = MetaTombRaider
|
||||
|
||||
return meta
|
||||
return Meta
|
||||
|
||||
|
||||
# The following method(s) are modeld helper methods
|
||||
|
||||
@@ -17,7 +17,7 @@ from openpilot.system.hardware.hw import Paths
|
||||
|
||||
from cereal import messaging, custom
|
||||
from openpilot.sunnypilot.models.fetcher import ModelFetcher
|
||||
from openpilot.sunnypilot.models.helpers import verify_file, get_active_bundle
|
||||
from openpilot.sunnypilot.models.helpers import get_active_bundle, validate_active_bundle, verify_file
|
||||
|
||||
|
||||
class ModelManagerSP:
|
||||
@@ -239,6 +239,7 @@ class ModelManagerSP:
|
||||
while True:
|
||||
try:
|
||||
self.available_models = self.model_fetcher.get_available_bundles()
|
||||
validate_active_bundle(self.params, self.available_models)
|
||||
self.active_bundle = get_active_bundle(self.params)
|
||||
|
||||
if (index_to_download := self.params.get("ModelManager_DownloadIndex")) is not None:
|
||||
@@ -252,8 +253,8 @@ class ModelManagerSP:
|
||||
self.selected_bundle = None
|
||||
|
||||
if self.params.get("ModelManager_ClearCache"):
|
||||
self.clear_model_cache()
|
||||
self.params.remove("ModelManager_ClearCache")
|
||||
self.clear_model_cache()
|
||||
self.params.remove("ModelManager_ClearCache")
|
||||
|
||||
self._report_status()
|
||||
rk.keep_time()
|
||||
|
||||
@@ -41,7 +41,6 @@ LOCAL_PORT_WHITELIST = {8022}
|
||||
SUNNYLINK_LOG_ATTR_NAME = "user.sunny.upload"
|
||||
SUNNYLINK_RECONNECT_TIMEOUT_S = 70 # FYI changing this will also would require a change on sidebar.cc
|
||||
DISALLOW_LOG_UPLOAD = threading.Event()
|
||||
METADATA_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "params_metadata.json")
|
||||
|
||||
params = Params()
|
||||
|
||||
@@ -170,39 +169,6 @@ def getParamsAllKeys() -> list[str]:
|
||||
return keys
|
||||
|
||||
|
||||
@dispatcher.add_method
|
||||
def getParamsAllKeysV1() -> dict[str, str]:
|
||||
try:
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
except Exception:
|
||||
cloudlog.exception("sunnylinkd.getParamsAllKeysV1.metadata.exception")
|
||||
metadata = {}
|
||||
|
||||
try:
|
||||
available_keys: list[str] = [k.decode('utf-8') for k in Params().all_keys()]
|
||||
|
||||
params_dict: dict[str, list[dict[str, str | bool | int | object | dict | None]]] = {"params": []}
|
||||
for key in available_keys:
|
||||
value = get_param_as_byte(key, get_default=True)
|
||||
|
||||
param_entry = {
|
||||
"key": key,
|
||||
"type": int(params.get_type(key).value),
|
||||
"default_value": base64.b64encode(value).decode('utf-8') if value else None,
|
||||
}
|
||||
|
||||
if key in metadata:
|
||||
meta_copy = metadata[key].copy()
|
||||
param_entry["_extra"] = meta_copy
|
||||
|
||||
params_dict["params"].append(param_entry)
|
||||
return {"keys": json.dumps(params_dict.get("params", []))}
|
||||
except Exception:
|
||||
cloudlog.exception("sunnylinkd.getParamsAllKeysV1.exception")
|
||||
raise
|
||||
|
||||
|
||||
@dispatcher.add_method
|
||||
def getParamsMetadata() -> str:
|
||||
"""Return settings_ui.json + live capabilities as gzip-compressed, base64-encoded string.
|
||||
|
||||
@@ -97,12 +97,11 @@ The compiler splices a list-context `$ref` into its parent list. Macros may refe
|
||||
|
||||
```
|
||||
1. common/params_keys.h — add/remove the C++ param key
|
||||
2. params_metadata.json — automated via update_params_metadata.py
|
||||
3. settings_ui_src/pages/<page>.yaml — add/edit/remove the item in the right section
|
||||
4. python sunnypilot/sunnylink/tools/compile_settings_ui.py
|
||||
5. python sunnypilot/sunnylink/tools/validate_settings_ui.py (or: --check on the compiler)
|
||||
6. uv run python -m pytest sunnypilot/sunnylink/tests/ # run regression + compiler tests
|
||||
7. commit
|
||||
2. settings_ui_src/pages/<page>.yaml — add/edit/remove the item in the right section
|
||||
3. python sunnypilot/sunnylink/tools/compile_settings_ui.py
|
||||
4. python sunnypilot/sunnylink/tools/validate_settings_ui.py (or: --check on the compiler)
|
||||
5. uv run python -m pytest sunnypilot/sunnylink/tests/ # run regression + compiler tests
|
||||
6. commit
|
||||
```
|
||||
|
||||
CI runs `compile_settings_ui.py --check` to fail on hand-edited `settings_ui.json`.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,86 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import json
|
||||
|
||||
from openpilot.sunnypilot.sunnylink.athena.sunnylinkd import getParamsAllKeysV1, METADATA_PATH
|
||||
|
||||
|
||||
def test_get_params_all_keys_v1():
|
||||
"""
|
||||
Test the getParamsAllKeysV1 API endpoint.
|
||||
|
||||
Why:
|
||||
This endpoint is used by the UI (and potentially external tools) to fetch the list of
|
||||
available parameters along with their metadata (titles, descriptions, options, constraints).
|
||||
We need to ensure it returns the correct structure and that the metadata from
|
||||
params_metadata.json is correctly merged into the response.
|
||||
|
||||
Expected:
|
||||
- The response should contain a "keys" field which is a JSON string of a list of parameters.
|
||||
- Each parameter object should have "key", "type", "default_value", and optionally "_extra".
|
||||
- The "_extra" field should contain the rich metadata (title, options, min/max, etc.) matching
|
||||
the source of truth (params_metadata.json).
|
||||
"""
|
||||
response = getParamsAllKeysV1()
|
||||
assert "keys" in response
|
||||
|
||||
keys_json = response["keys"]
|
||||
params_list = json.loads(keys_json)
|
||||
|
||||
assert isinstance(params_list, list)
|
||||
assert len(params_list) > 0
|
||||
|
||||
# Check structure of first item
|
||||
first_param = params_list[0]
|
||||
assert "key" in first_param
|
||||
assert "type" in first_param
|
||||
assert "default_value" in first_param
|
||||
|
||||
if "_extra" in first_param:
|
||||
assert isinstance(first_param["_extra"], dict)
|
||||
assert "default" not in first_param["_extra"]
|
||||
assert "type" not in first_param["_extra"]
|
||||
|
||||
# Load the source of truth
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
# Verify that the API response matches the metadata file for a few sample keys
|
||||
# This ensures the plumbing is working without being brittle to content changes
|
||||
|
||||
# 1. Check a key that should have metadata
|
||||
keys_with_metadata = [k for k in params_list if k["key"] in metadata]
|
||||
assert len(keys_with_metadata) > 0, "No parameters found that match metadata keys"
|
||||
|
||||
for param in keys_with_metadata[:5]: # Check first 5 matches
|
||||
key = param["key"]
|
||||
expected_meta = metadata[key]
|
||||
|
||||
assert "_extra" in param, f"Parameter {key} should have _extra field"
|
||||
actual_meta = param["_extra"]
|
||||
|
||||
# Verify all fields in JSON are present in the API response
|
||||
for meta_key, meta_val in expected_meta.items():
|
||||
assert meta_key in actual_meta, f"Missing {meta_key} in API response for {key}"
|
||||
assert actual_meta[meta_key] == meta_val, f"Mismatch for {key}.{meta_key}: expected {meta_val}, got {actual_meta[meta_key]}"
|
||||
|
||||
# 2. Check that we are correctly serving options if they exist
|
||||
params_with_options = [k for k in keys_with_metadata if "options" in k.get("_extra", {})]
|
||||
if params_with_options:
|
||||
param = params_with_options[0]
|
||||
key = param["key"]
|
||||
assert isinstance(param["_extra"]["options"], list), f"Options for {key} should be a list"
|
||||
assert param["_extra"]["options"] == metadata[key]["options"]
|
||||
|
||||
# 3. Check that we are correctly serving numeric constraints if they exist
|
||||
params_with_constraints = [k for k in keys_with_metadata if "min" in k.get("_extra", {})]
|
||||
if params_with_constraints:
|
||||
param = params_with_constraints[0]
|
||||
key = param["key"]
|
||||
assert param["_extra"]["min"] == metadata[key]["min"]
|
||||
assert param["_extra"]["max"] == metadata[key]["max"]
|
||||
assert param["_extra"]["step"] == metadata[key]["step"]
|
||||
@@ -1,284 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.sunnypilot.sunnylink.athena.sunnylinkd import METADATA_PATH
|
||||
|
||||
|
||||
def test_metadata_json_exists():
|
||||
"""
|
||||
Test that the params_metadata.json file exists at the expected path.
|
||||
|
||||
Why:
|
||||
The metadata file is the source of truth for parameter descriptions, options, and constraints.
|
||||
If it's missing, the UI will not be able to display rich information for parameters.
|
||||
|
||||
Expected:
|
||||
The file should exist at sunnypilot/sunnylink/params_metadata.json.
|
||||
"""
|
||||
assert os.path.exists(METADATA_PATH), f"Metadata file not found at {METADATA_PATH}"
|
||||
|
||||
|
||||
def test_metadata_json_valid():
|
||||
"""
|
||||
Test that the params_metadata.json file contains valid JSON.
|
||||
|
||||
Why:
|
||||
Invalid JSON will cause the metadata loading to fail, potentially crashing the UI or
|
||||
resulting in missing metadata.
|
||||
|
||||
Expected:
|
||||
The file content should be parseable as a JSON object (dictionary).
|
||||
"""
|
||||
with open(METADATA_PATH) as f:
|
||||
try:
|
||||
data = json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
pytest.fail("Metadata file is not valid JSON")
|
||||
|
||||
assert isinstance(data, dict), "Metadata root must be a dictionary"
|
||||
|
||||
|
||||
def test_all_params_have_metadata():
|
||||
"""
|
||||
Test that every parameter in the codebase has a corresponding entry in params_metadata.json.
|
||||
|
||||
Why:
|
||||
We want to ensure 100% coverage of parameter metadata. Any parameter added to the codebase
|
||||
should also be documented in the metadata file.
|
||||
|
||||
Expected:
|
||||
There should be no parameters in Params() that are missing from the metadata file.
|
||||
If this fails, run 'python3 sunnypilot/sunnylink/tools/update_params_metadata.py'.
|
||||
"""
|
||||
params = Params()
|
||||
all_keys = [k.decode('utf-8') for k in params.all_keys()]
|
||||
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
missing_keys = [key for key in all_keys if key not in metadata]
|
||||
|
||||
if missing_keys:
|
||||
pytest.fail(
|
||||
f"The following parameters are missing from metadata: {missing_keys}. "
|
||||
+ "Please run 'python3 sunnypilot/sunnylink/tools/update_params_metadata.py' to update."
|
||||
)
|
||||
|
||||
|
||||
def test_metadata_keys_exist_in_params():
|
||||
"""
|
||||
Test that all keys in params_metadata.json actually exist in the codebase.
|
||||
|
||||
Why:
|
||||
We want to avoid stale metadata for parameters that have been removed or renamed.
|
||||
This keeps the metadata file clean and relevant.
|
||||
|
||||
Expected:
|
||||
There should be no keys in the metadata file that are not present in Params().
|
||||
This prints a warning rather than failing, as it's less critical than missing metadata.
|
||||
"""
|
||||
params = Params()
|
||||
all_keys = {k.decode('utf-8') for k in params.all_keys()}
|
||||
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
extra_keys = [key for key in metadata.keys() if key not in all_keys]
|
||||
|
||||
if extra_keys:
|
||||
print(f"Warning: The following keys in metadata do not exist in Params: {extra_keys}")
|
||||
|
||||
|
||||
def test_no_default_titles():
|
||||
"""
|
||||
Test that no parameter has a title that is identical to its key.
|
||||
|
||||
Why:
|
||||
The default behavior of the update script is to set the title equal to the key.
|
||||
We want to force developers to provide human-readable, descriptive titles for all parameters.
|
||||
|
||||
Expected:
|
||||
No parameter metadata should have 'title' == 'key'.
|
||||
"""
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
default_title_keys = [key for key, meta in metadata.items() if meta.get("title") == key]
|
||||
|
||||
if default_title_keys:
|
||||
pytest.fail(
|
||||
f"The following parameters have default titles (title == key): {default_title_keys}. "
|
||||
+ "Please update 'params_metadata.json' with descriptive titles."
|
||||
)
|
||||
|
||||
|
||||
def test_options_structure():
|
||||
"""
|
||||
Test that the 'options' field in metadata follows the correct structure.
|
||||
|
||||
Why:
|
||||
The UI expects 'options' to be a list of objects with 'value' and 'label' keys.
|
||||
Incorrect structure will break the UI rendering for dropdowns/toggles.
|
||||
|
||||
Expected:
|
||||
If 'options' is present, it must be a list of dicts, and each dict must have 'value' and 'label'.
|
||||
"""
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
for key, meta in metadata.items():
|
||||
if "options" in meta:
|
||||
options = meta["options"]
|
||||
assert isinstance(options, list), f"Options for {key} must be a list"
|
||||
for option in options:
|
||||
assert isinstance(option, dict), f"Option in {key} must be a dictionary"
|
||||
assert "value" in option, f"Option in {key} must have a 'value' key"
|
||||
assert "label" in option, f"Option in {key} must have a 'label' key"
|
||||
|
||||
|
||||
def test_numeric_constraints():
|
||||
"""
|
||||
Test that numeric parameters have valid 'min', 'max', and 'step' constraints.
|
||||
|
||||
Why:
|
||||
The UI uses these constraints to validate user input and render sliders/steppers.
|
||||
Missing or invalid constraints can lead to UI bugs or invalid parameter values.
|
||||
|
||||
Expected:
|
||||
If any of min/max/step is present, ALL of them must be present.
|
||||
They must be numbers (int/float), and min must be less than max.
|
||||
"""
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
for key, meta in metadata.items():
|
||||
if "min" in meta or "max" in meta or "step" in meta:
|
||||
assert "min" in meta, f"Numeric param {key} must have 'min'"
|
||||
assert "max" in meta, f"Numeric param {key} must have 'max'"
|
||||
assert "step" in meta, f"Numeric param {key} must have 'step'"
|
||||
|
||||
assert isinstance(meta["min"], (int, float)), f"Min for {key} must be number"
|
||||
assert isinstance(meta["max"], (int, float)), f"Max for {key} must be number"
|
||||
assert isinstance(meta["step"], (int, float)), f"Step for {key} must be number"
|
||||
assert meta["min"] < meta["max"], f"Min must be less than max for {key}"
|
||||
|
||||
|
||||
def test_known_params_metadata():
|
||||
"""
|
||||
Test specific known parameters to ensure they have the expected rich metadata.
|
||||
|
||||
Why:
|
||||
This acts as a spot check to ensure that our rich metadata population logic is working correctly
|
||||
and that critical parameters (like LongitudinalPersonality) have their options and constraints preserved.
|
||||
|
||||
Expected:
|
||||
'LongitudinalPersonality' should have 3 options (Aggressive, Standard, Relaxed).
|
||||
'CustomAccLongPressIncrement' should have min=1, max=10, step=1.
|
||||
"""
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
# Check an enum-like param
|
||||
lp = metadata.get("LongitudinalPersonality")
|
||||
assert lp is not None
|
||||
assert "options" in lp
|
||||
assert len(lp["options"]) == 3
|
||||
assert lp["options"][0]["label"] == "Aggressive"
|
||||
assert lp["options"][0]["value"] == 0
|
||||
|
||||
# Check a numeric param
|
||||
acc_long = metadata.get("CustomAccLongPressIncrement")
|
||||
assert acc_long is not None
|
||||
assert acc_long["min"] == 1
|
||||
assert acc_long["max"] == 10
|
||||
assert acc_long["step"] == 1
|
||||
|
||||
|
||||
def test_torque_control_tune_versions_in_sync():
|
||||
"""
|
||||
Test that TorqueControlTune options in params_metadata.json match versions in latcontrol_torque_versions.json.
|
||||
|
||||
Why:
|
||||
The TorqueControlTune dropdown in the UI should always reflect the available torque tune versions.
|
||||
If versions are added/removed from latcontrol_torque_versions.json, the metadata must be updated accordingly.
|
||||
|
||||
Expected:
|
||||
- TorqueControlTune should have a 'Default' option with empty string value
|
||||
- All versions from latcontrol_torque_versions.json should be present in the options
|
||||
- The version values and labels should match between both files
|
||||
"""
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
|
||||
versions_json_path = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "controls", "lib", "latcontrol_torque_versions.json")
|
||||
sync_script_path = "python3 sunnypilot/sunnylink/tools/sync_torque_versions.py"
|
||||
|
||||
# Load both files
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
with open(versions_json_path) as f:
|
||||
versions = json.load(f)
|
||||
|
||||
# Get TorqueControlTune metadata
|
||||
torque_tune = metadata.get("TorqueControlTune")
|
||||
if torque_tune is None:
|
||||
pytest.fail(f"TorqueControlTune not found in params_metadata.json. Please run '{sync_script_path}' to sync.")
|
||||
|
||||
if "options" not in torque_tune:
|
||||
pytest.fail(f"TorqueControlTune must have options. Please run '{sync_script_path}' to sync.")
|
||||
|
||||
options = torque_tune["options"]
|
||||
if not isinstance(options, list):
|
||||
pytest.fail(f"TorqueControlTune options must be a list. Please run '{sync_script_path}' to sync.")
|
||||
|
||||
if len(options) == 0:
|
||||
pytest.fail(f"TorqueControlTune must have at least one option. Please run '{sync_script_path}' to sync.")
|
||||
|
||||
# Check that Default option exists
|
||||
default_option = next((opt for opt in options if opt.get("value") == ""), None)
|
||||
if default_option is None:
|
||||
pytest.fail(f"TorqueControlTune must have a 'Default' option with empty string value. Please run '{sync_script_path}' to sync.")
|
||||
|
||||
if default_option.get("label") != "Default":
|
||||
pytest.fail(f"Default option must have label 'Default'. Please run '{sync_script_path}' to sync.")
|
||||
|
||||
# Build expected options from versions.json
|
||||
expected_version_keys = set(versions.keys())
|
||||
actual_version_keys = set()
|
||||
|
||||
for option in options:
|
||||
if option.get("value") == "":
|
||||
continue # Skip the default option
|
||||
|
||||
label = option.get("label")
|
||||
value = option.get("value")
|
||||
|
||||
# Check that this option corresponds to a version
|
||||
if label not in versions:
|
||||
pytest.fail(f"Option label '{label}' not found in latcontrol_torque_versions.json. Please run '{sync_script_path}' to sync.")
|
||||
|
||||
# Check that the value matches the version number
|
||||
expected_value = float(versions[label]["version"])
|
||||
if value != expected_value:
|
||||
pytest.fail(f"Option '{label}' has value {value}, expected {expected_value}. Please run '{sync_script_path}' to sync.")
|
||||
|
||||
actual_version_keys.add(label)
|
||||
|
||||
# Check that all versions are represented
|
||||
missing_versions = expected_version_keys - actual_version_keys
|
||||
if missing_versions:
|
||||
pytest.fail(f"The following versions are missing from TorqueControlTune options: {missing_versions}. " +
|
||||
f"Please run '{sync_script_path}' to sync.")
|
||||
|
||||
extra_versions = actual_version_keys - expected_version_keys
|
||||
if extra_versions:
|
||||
pytest.fail("The following versions in TorqueControlTune options are not in latcontrol_torque_versions.json: " +
|
||||
f"{extra_versions}. Please run '{sync_script_path}' to sync.")
|
||||
@@ -1,133 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.sunnypilot.system.params_migration import ONROAD_BRIGHTNESS_TIMER_VALUES
|
||||
|
||||
METADATA_PATH = os.path.join(os.path.dirname(__file__), "../params_metadata.json")
|
||||
TORQUE_VERSIONS_JSON = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "controls", "lib", "latcontrol_torque_versions.json")
|
||||
|
||||
|
||||
def main():
|
||||
params = Params()
|
||||
all_keys = params.all_keys()
|
||||
|
||||
if os.path.exists(METADATA_PATH):
|
||||
with open(METADATA_PATH) as f:
|
||||
try:
|
||||
data = json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
data = {}
|
||||
else:
|
||||
data = {}
|
||||
|
||||
# Add new keys
|
||||
for key in all_keys:
|
||||
key_str = key.decode("utf-8")
|
||||
if key_str not in data:
|
||||
print(f"Adding new key: {key_str}")
|
||||
data[key_str] = {
|
||||
"title": key_str,
|
||||
"description": "",
|
||||
}
|
||||
|
||||
# Remove deleted keys
|
||||
# keys_to_remove = [k for k in data.keys() if k.encode("utf-8") not in all_keys]
|
||||
# for k in keys_to_remove:
|
||||
# print(f"Removing deleted key: {k}")
|
||||
# del data[k]
|
||||
|
||||
# Sort keys
|
||||
sorted_data = dict(sorted(data.items()))
|
||||
|
||||
with open(METADATA_PATH, "w") as f:
|
||||
json.dump(sorted_data, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
print(f"Updated {METADATA_PATH}")
|
||||
|
||||
# update onroad screen brightness params
|
||||
update_onroad_brightness_param()
|
||||
|
||||
# update onroad screen brightness timer params
|
||||
update_onroad_brightness_timer_param()
|
||||
|
||||
# update torque versions param
|
||||
update_torque_versions_param()
|
||||
|
||||
|
||||
def update_onroad_brightness_param():
|
||||
try:
|
||||
with open(METADATA_PATH) as f:
|
||||
params_metadata = json.load(f)
|
||||
if "OnroadScreenOffBrightness" in params_metadata:
|
||||
options = [
|
||||
{"value": 0, "label": "Auto (Default)"},
|
||||
{"value": 1, "label": "Auto (Dark)"},
|
||||
{"value": 2, "label": "Screen Off"},
|
||||
]
|
||||
for i in range(3, 23):
|
||||
options.append({"value": i, "label": f"{(i - 2) * 5} %"})
|
||||
params_metadata["OnroadScreenOffBrightness"]["options"] = options
|
||||
with open(METADATA_PATH, 'w') as f:
|
||||
json.dump(params_metadata, f, indent=2)
|
||||
f.write('\n')
|
||||
print(f"Updated OnroadScreenOffBrightness options in params_metadata.json with {len(options)} options.")
|
||||
except Exception as e:
|
||||
print(f"Failed to update OnroadScreenOffBrightness versions in params_metadata.json: {e}")
|
||||
|
||||
|
||||
def update_onroad_brightness_timer_param():
|
||||
try:
|
||||
with open(METADATA_PATH) as f:
|
||||
params_metadata = json.load(f)
|
||||
if "OnroadScreenOffTimer" in params_metadata:
|
||||
options = []
|
||||
for _index, seconds in sorted(ONROAD_BRIGHTNESS_TIMER_VALUES.items()):
|
||||
label = f"{seconds}s" if seconds < 60 else f"{seconds // 60}m"
|
||||
options.append({"value": seconds, "label": label})
|
||||
params_metadata["OnroadScreenOffTimer"]["options"] = options
|
||||
with open(METADATA_PATH, 'w') as f:
|
||||
json.dump(params_metadata, f, indent=2)
|
||||
f.write('\n')
|
||||
print(f"Updated OnroadScreenOffTimer options in params_metadata.json with {len(options)} options.")
|
||||
except Exception as e:
|
||||
print(f"Failed to update OnroadScreenOffTimer options in params_metadata.json: {e}")
|
||||
|
||||
|
||||
def update_torque_versions_param():
|
||||
with open(TORQUE_VERSIONS_JSON) as f:
|
||||
current_versions = json.load(f)
|
||||
|
||||
try:
|
||||
with open(METADATA_PATH) as f:
|
||||
params_metadata = json.load(f)
|
||||
|
||||
options = [{"value": "", "label": "Default"}]
|
||||
for version_key, version_data in current_versions.items():
|
||||
version_value = float(version_data["version"])
|
||||
options.append({"value": version_value, "label": str(version_key)})
|
||||
|
||||
if "TorqueControlTune" in params_metadata:
|
||||
params_metadata["TorqueControlTune"]["options"] = options
|
||||
|
||||
with open(METADATA_PATH, 'w') as f:
|
||||
json.dump(params_metadata, f, indent=2)
|
||||
f.write('\n')
|
||||
|
||||
print(f"Updated TorqueControlTune options in params_metadata.json with {len(options)} options: \n{options}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to update TorqueControlTune versions in params_metadata.json: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1369,15 +1369,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "2.61.1"
|
||||
version = "2.62.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/3b/4bc6b348bbd331daa14d4babe9f2b99bc854f4da41560eefb9488d78481d/sentry_sdk-2.61.1.tar.gz", hash = "sha256:9c6adccb3feefa9ba032c8d295ca477575c2f11896046a2b0ad686c47c4af555", size = 459429, upload-time = "2026-06-01T07:24:18.875Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/5d/a343201726150e05f2036eeb6e493e2e2f8bf8a66f5aa70f2f4ac96f9ca3/sentry_sdk-2.62.0.tar.gz", hash = "sha256:3c870b9f50d9fd15b58c817dbde1c7cfaa9fe3f05df0a4c6edd5571cb82f5491", size = 463986, upload-time = "2026-06-08T13:23:49.223Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/54/c9218db183846e08efaf68534889ef42e499dde432778881104a42f7071b/sentry_sdk-2.61.1-py3-none-any.whl", hash = "sha256:fa36eaf4b8ad708f718500d4bdcc1532637526a22beb874d88cbc0a46458b5ae", size = 483735, upload-time = "2026-06-01T07:24:17.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/07/05440381627877aae223fd68f330df9b9fc6641d08bf65328b55235617a2/sentry_sdk-2.62.0-py3-none-any.whl", hash = "sha256:27f61d13a86c3c1648dec666dd5a64f79772dd6a84b446f11866601ecab24f6f", size = 490586, upload-time = "2026-06-08T13:23:47.486Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1491,27 +1491,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ty"
|
||||
version = "0.0.44"
|
||||
version = "0.0.46"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/f4/fbb120226e4f239652525a664bad976a23fea58c646d1323f2296fee8a61/ty-0.0.44.tar.gz", hash = "sha256:5886229830ab77022842a1c55d2ef57405621a91fc465969fa6d538661898173", size = 5803665, upload-time = "2026-06-05T03:33:48.612Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/7d/d95b5a9dea83472006be3ce5e480028c44b34138d84d0172e910f287fb69/ty-0.0.46.tar.gz", hash = "sha256:c6c2d7105b5633b49950b4c3a90d1ed2613eb9d794ad582bbbf6c4ffcb93accf", size = 5832380, upload-time = "2026-06-09T03:28:05.056Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/c6/b5b8c4762efb4d85401652658786506867553ecfc2beac3bcf361a15937f/ty-0.0.44-py3-none-linux_armv6l.whl", hash = "sha256:272d31e7ad49b1dc5e8465a9fe700354e14c755b40d9c75f08f031d786903df3", size = 11607267, upload-time = "2026-06-05T03:33:27.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/5c/f4b405570737f44ab0fc4214117fe43353f8f0825a1823d9e99e9c8e57be/ty-0.0.44-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b92c4ddd7a3daf2049715edec9dc70cf6fd31a5a318ee647258f90dd75495eed", size = 11382826, upload-time = "2026-06-05T03:33:54.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/aa/fb9835aa492b148d7754cb4c3db07f31a7e2e09f0d8e0e8e297f01125dd2/ty-0.0.44-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4d42cfd84a690f6654b2a4f0515027c21b692cf2512d32e6433f754893a95609", size = 10809741, upload-time = "2026-06-05T03:33:33.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/f5/0b20ba6b66837a5a37bab7f74ac0732c66e766b5f0b2d55b30816b15f348/ty-0.0.44-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc47ae87e4cb7db2a9166bb23b78a905c3626e523296ec5bccf36b5e89bda6b", size = 11318153, upload-time = "2026-06-05T03:34:09.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/bb/b82ea730774a4f950f06d355fbc120d51eac7da23b57fc79ef6ff7c79cbb/ty-0.0.44-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46d867e80f16f421ac72c9a85240dbf050d62d9b3fbd10a8b5b082fb21679e0b", size = 11403108, upload-time = "2026-06-05T03:33:57.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/41/e2c83856165291049c702eda4e2ef3d3ebd875e8a0a77b8cc4ef3156aa1c/ty-0.0.44-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:411f5de0f96a4e4e5cccc3e0d55954c41f6a99ee6ca1fe5a7226cbc68406e053", size = 11944815, upload-time = "2026-06-05T03:34:15.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/95/1fa6a101eb9d5bec042b87e5ca9c8fc349b75961beca6306f95af5cd5539/ty-0.0.44-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b15f01ecb4e2b46c05a1769293f9d32c3d4a1e4e7dfccf37c604d705dc3e3f4", size = 12476121, upload-time = "2026-06-05T03:33:51.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/6a/da4b45b1229d39207c6140681c2aaf4f5691bcb1dc830b84450ca25c8f57/ty-0.0.44-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:edd32b7467af509c99c0244c2226a4e4c03400699003ec33373282ab931654d9", size = 12091340, upload-time = "2026-06-05T03:33:36.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/c7/e1c9260ea5188195962ff1214ace418b5d69187e8fa7c0a1ec4994b8071b/ty-0.0.44-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:503a585f4007387c3afc58bae23a7ca1b9f236cbdb1a881dc36110655ceb1937", size = 11986201, upload-time = "2026-06-05T03:34:00.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/f9/312bb112da9b1a7da295bb0426be85e72ad48da4e4266c36d77256b4058d/ty-0.0.44-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d28bcfa83243d77c2316944e8cf197f73597bf17d1ddc047d0b10a762531252", size = 12168475, upload-time = "2026-06-05T03:33:30.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/de/64978d603f6c3e5dd7cb97eca2214567d8ad0c85fa4a7435b7852ae4b779/ty-0.0.44-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:56fd2dd0192def189715b25f5338f6222fb827884dc34111e50aa1c4e061cee5", size = 11292937, upload-time = "2026-06-05T03:34:06.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/63/a625d8a3c71dcaa01988d330f849c465fe72ead4b0bbab44fe4bd6e672b5/ty-0.0.44-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7f8d990489032de1984e73c159f3e760d754cf83a602b874827d943821f63595", size = 11421560, upload-time = "2026-06-05T03:33:23.995Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/96/61aeba0e629b0c91bd316ff94d00e38817ec493ae4316f39508988daa287/ty-0.0.44-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f61ffe72996a755432922fe90b28db593f572eb5cbf48e3ef4e67b282533d1b0", size = 11580282, upload-time = "2026-06-05T03:34:03.308Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/f7/256e1538ce21cab67b381201444c42454de69d310059c4929d92a0ee9c48/ty-0.0.44-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2b237a143bac4f30cec9257d45f01e72da97030a80a09a2b69cfef065f09c37f", size = 12085723, upload-time = "2026-06-05T03:33:45.953Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/76/ec3957c10872643a98db7a7895101ad89c5b7cba4bc6c4aebbbfc91756cc/ty-0.0.44-py3-none-win32.whl", hash = "sha256:6a24586c65419223ac5bab4822d49ab493a5d19ea2a897514284c232b9d6166a", size = 10892978, upload-time = "2026-06-05T03:34:12.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/7d/ba24050432196e7d7f03945e5c379951593c48e04e5c5d5275cfc4624791/ty-0.0.44-py3-none-win_amd64.whl", hash = "sha256:8cccb27e348c89a9733fbad1b2efadfbad79b107c7e52adb52dfd8a70156a38d", size = 11987058, upload-time = "2026-06-05T03:33:42.692Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/34/16ec3f1fec75292d9c56a8b5fef037ceaba85a5c30562206c1a245a00a67/ty-0.0.44-py3-none-win_arm64.whl", hash = "sha256:58049504e7a12bf1957f24a5384a332c94d5590127083a80db5e5a1bed34190b", size = 11329961, upload-time = "2026-06-05T03:33:39.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/24/f9f7533c391610521f4164e6b8e37ef72d0c1ee8651bc0d9ce9e658b953b/ty-0.0.46-py3-none-linux_armv6l.whl", hash = "sha256:5e716337994699cbc1a1a7b7a3e6622306f2574c710330f9d9691c2c3d8391b0", size = 11756264, upload-time = "2026-06-09T03:28:20.112Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/49/ff3d13655b9b5cc8176f4c3446bf7ec2df43c8ad9e5272d4adc5d952fa45/ty-0.0.46-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:51d618dec5403635690d0e3e298cd0ad3d84ebc6a576652939ef30ce96fce4b2", size = 11492723, upload-time = "2026-06-09T03:28:13.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/4a/e7e3209e353c5835c7756339bbcdfda10852407b80fbb9ed46c17241873a/ty-0.0.46-py3-none-macosx_11_0_arm64.whl", hash = "sha256:acbafd6a2351b07a6cf4c945b0b1d47f6d2826faac2526a351dfa74d3a3cc664", size = 10892822, upload-time = "2026-06-09T03:27:51.179Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/20/4390c90434a9ddefcecb65e8df00e4c2700e9739dc0baf58bed36d25f713/ty-0.0.46-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de5df602ffd760612ae36602bbad69b0123ff6cffd92e62aa92b7709317d69e3", size = 11408745, upload-time = "2026-06-09T03:27:58.049Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/0c/f13a1bf9c6798530c773667095a6cf8f73ec9721db359423e7249bff7fbc/ty-0.0.46-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7abf5a10b30d8641faad90f6a19989daec941bb90261159e05cfeb04d2012046", size = 11544432, upload-time = "2026-06-09T03:27:53.519Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/69/eb3710c13dff846a0362df04fadd8a39b64ccc244c0d02ce5285ede8eae5/ty-0.0.46-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8770404139c6ccee2ce2fc226478cfa4100915133c876c257e52197b8b92051d", size = 12031228, upload-time = "2026-06-09T03:28:29.816Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/68/5f5db9c84c1d44acdc67281089b372d9d818ee68123a60c59c66187095e2/ty-0.0.46-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f960d5a6e4860076924d2b86891d9872c4a3daa4663fb416e640b22cf3dbf68e", size = 12596073, upload-time = "2026-06-09T03:28:25.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/be/cfd0bb272e6a1491f6de30c60da1f39c2b3c3524ec64a5c92b71365c9185/ty-0.0.46-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d9000a4a3ed08fc37e8a2ff0b801cde06e1c2af3bc053677744bb5a1b751030", size = 12284885, upload-time = "2026-06-09T03:28:10.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/3a/2cd541f6320f5d6f70a45725c4e1016efedd5545348bb23b47ffb3e4c724/ty-0.0.46-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1160e6dc86536109ab755f7142f36f4dda5333c8330cf230d61819494d27125", size = 12079480, upload-time = "2026-06-09T03:27:55.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/91/8e0075bc6568fb477e7ef4d805c67fa6902b692cb4419e0bf5ce3c04c5bc/ty-0.0.46-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b619c0efe007731f8221fa787701bfa4402da7a83eb26c61ae25e77b6ace6384", size = 12316547, upload-time = "2026-06-09T03:28:08.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/28/b96cbfeda019a4044c6a8cd06ff84d08b631d4ba7d9a1e6dc0311df3563a/ty-0.0.46-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ad98fccb6a8a94c4121b993761a0deee602f5826c4162e0a91f4f8118ddadd42", size = 11392846, upload-time = "2026-06-09T03:28:00.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/d0/4d77f699a95ac7a13b94ca1a58682667cfe974f91557d9e2a9fc0b808a7f/ty-0.0.46-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:74536b13c3cc3f5944408669c202d4c57c3d19ff154732df8e6145718aef9191", size = 11559017, upload-time = "2026-06-09T03:28:17.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/62/1d6f6b51c2b132da8011c6a41ead0c1fd2a0b17ea72304bcf6ce084d581a/ty-0.0.46-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5e50b1e96ced41b609e24ed27d9e4f508584ed7f4d0bb717ca8c8d75d2fd1b7c", size = 11666509, upload-time = "2026-06-09T03:28:22.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/9a/6643894bc12cb30c281f4c8bf37f6d30c1fbd9484ef39a12b0ea6dae3c1c/ty-0.0.46-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0a7d9f58d26d938e5d2f607481b7a412d8c00d675a1ec72004fa9d6b3b9def99", size = 12180448, upload-time = "2026-06-09T03:28:32.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/68/0f3b7bb03a7da676ef51b1c0af0bde1e500d69d5f0c807ed63b6f30b66dd/ty-0.0.46-py3-none-win32.whl", hash = "sha256:26db0ce89c573e60132d14e9688c9329a1633b1a8c26fe457025c7c406f7d5e6", size = 10960002, upload-time = "2026-06-09T03:28:02.832Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/f4/91ff618b2dee39d0633d23e1adac0174aa1de80df17e270acac534034dbc/ty-0.0.46-py3-none-win_amd64.whl", hash = "sha256:90e8e6d446b9cb7cb4bede9fca7b3c99fd1e2355605ecf431c131a51db2a5e93", size = 12097413, upload-time = "2026-06-09T03:28:27.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/2e/300174fca375a27a7c28dd80e990d857d7b3e3b25980c65063f980aa2f17/ty-0.0.46-py3-none-win_arm64.whl", hash = "sha256:ebd320d82605079b901a095dc4711037a0c488b4ace79a602fef4df0d3f4cf74", size = 11439595, upload-time = "2026-06-09T03:28:15.355Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user