Sync: commaai/openpilot:mastersunnypilot/sunnypilot:master (#1795)

This commit is contained in:
Jason Wen
2026-04-12 16:29:17 -04:00
committed by GitHub
43 changed files with 994 additions and 939 deletions
+3
View File
@@ -52,6 +52,9 @@
"type": "lldb",
"request": "attach",
"pid": "${command:pickMyProcess}",
"sourceMap": {
".": "${workspaceFolder}/opendbc/safety"
},
"initCommands": [
"script import time; time.sleep(3)"
]
+1 -1
View File
@@ -4,7 +4,7 @@ cereal_dir = Dir('.')
gen_dir = Dir('gen')
# Build cereal
schema_files = ['log.capnp', 'car.capnp', 'legacy.capnp', 'custom.capnp']
schema_files = ['log.capnp', 'car.capnp', 'deprecated.capnp', 'custom.capnp']
env.Command([f'gen/cpp/{s}.c++' for s in schema_files] + [f'gen/cpp/{s}.h' for s in schema_files],
schema_files,
f"capnpc --src-prefix={cereal_dir.path} $SOURCES -o c++:{gen_dir.path}/cpp/")
+216 -1
View File
@@ -3,7 +3,7 @@ $Cxx.namespace("cereal");
@0x80ef1ec4889c2a63;
# legacy.capnp: a home for deprecated structs
# deprecated.capnp: a home for deprecated structs
struct LogRotate @0x9811e1f38f62f2d1 {
segmentNum @0 :Int32;
@@ -571,4 +571,219 @@ struct LidarPts @0xe3d6685d4e9d8f7a {
pkt @4 :Data;
}
struct LiveTracksDEPRECATED @0xb16f60103159415a {
trackId @0 :Int32;
dRel @1 :Float32;
yRel @2 :Float32;
vRel @3 :Float32;
aRel @4 :Float32;
timeStamp @5 :Float32;
status @6 :Float32;
currentTime @7 :Float32;
stationary @8 :Bool;
oncoming @9 :Bool;
}
struct LiveMpcData @0x92a5e332a85f32a0 {
x @0 :List(Float32);
y @1 :List(Float32);
psi @2 :List(Float32);
curvature @3 :List(Float32);
qpIterations @4 :UInt32;
calculationTime @5 :UInt64;
cost @6 :Float64;
}
struct LiveLongitudinalMpcData @0xe7e17c434f865ae2 {
xEgo @0 :List(Float32);
vEgo @1 :List(Float32);
aEgo @2 :List(Float32);
xLead @3 :List(Float32);
vLead @4 :List(Float32);
aLead @5 :List(Float32);
aLeadTau @6 :Float32; # lead accel time constant
qpIterations @7 :UInt32;
mpcId @8 :UInt32;
calculationTime @9 :UInt64;
cost @10 :Float64;
}
struct DriverStateDEPRECATED @0xb83c6cc593ed0a00 {
frameId @0 :UInt32;
modelExecutionTime @14 :Float32;
dspExecutionTime @16 :Float32;
rawPredictions @15 :Data;
faceOrientation @3 :List(Float32);
facePosition @4 :List(Float32);
faceProb @5 :Float32;
leftEyeProb @6 :Float32;
rightEyeProb @7 :Float32;
leftBlinkProb @8 :Float32;
rightBlinkProb @9 :Float32;
faceOrientationStd @11 :List(Float32);
facePositionStd @12 :List(Float32);
sunglassesProb @13 :Float32;
poorVision @17 :Float32;
partialFace @18 :Float32;
distractedPose @19 :Float32;
distractedEyes @20 :Float32;
eyesOnRoad @21 :Float32;
phoneUse @22 :Float32;
occludedProb @23 :Float32;
readyProb @24 :List(Float32);
notReadyProb @25 :List(Float32);
irPwrDEPRECATED @10 :Float32;
descriptorDEPRECATED @1 :List(Float32);
stdDEPRECATED @2 :Float32;
}
struct NavModelData @0xac3de5c437be057a {
frameId @0 :UInt32;
locationMonoTime @6 :UInt64;
modelExecutionTime @1 :Float32;
dspExecutionTime @2 :Float32;
features @3 :List(Float32);
# predicted future position
position @4 :XYData;
desirePrediction @5 :List(Float32);
# All SI units and in device frame
struct XYData @0xbe09e615b2507e26 {
x @0 :List(Float32);
y @1 :List(Float32);
xStd @2 :List(Float32);
yStd @3 :List(Float32);
}
}
struct AndroidBuildInfo @0xfe2919d5c21f426c {
board @0 :Text;
bootloader @1 :Text;
brand @2 :Text;
device @3 :Text;
display @4 :Text;
fingerprint @5 :Text;
hardware @6 :Text;
host @7 :Text;
id @8 :Text;
manufacturer @9 :Text;
model @10 :Text;
product @11 :Text;
radioVersion @12 :Text;
serial @13 :Text;
supportedAbis @14 :List(Text);
tags @15 :Text;
time @16 :Int64;
type @17 :Text;
user @18 :Text;
versionCodename @19 :Text;
versionRelease @20 :Text;
versionSdk @21 :Int32;
versionSecurityPatch @22 :Text;
}
struct AndroidSensor @0x9b513b93a887dbcd {
id @0 :Int32;
name @1 :Text;
vendor @2 :Text;
version @3 :Int32;
handle @4 :Int32;
type @5 :Int32;
maxRange @6 :Float32;
resolution @7 :Float32;
power @8 :Float32;
minDelay @9 :Int32;
fifoReservedEventCount @10 :UInt32;
fifoMaxEventCount @11 :UInt32;
stringType @12 :Text;
maxDelay @13 :Int32;
}
struct IosBuildInfo @0xd97e3b28239f5580 {
appVersion @0 :Text;
appBuild @1 :UInt32;
osVersion @2 :Text;
deviceModel @3 :Text;
}
enum FrameTypeDEPRECATED @0xa37f0d8558e193fd {
unknown @0;
neo @1;
chffrAndroid @2;
front @3;
}
struct AndroidCaptureResult @0xbcc3efbac41d2048 {
sensitivity @0 :Int32;
frameDuration @1 :Int64;
exposureTime @2 :Int64;
rollingShutterSkew @3 :UInt64;
colorCorrectionTransform @4 :List(Int32);
colorCorrectionGains @5 :List(Float32);
displayRotation @6 :Int8;
}
enum UsbPowerModeDEPRECATED @0xa8883583b32c9877 {
none @0;
client @1;
cdp @2;
dcp @3;
}
struct LateralINDIState @0x939463348632375e {
active @0 :Bool;
steeringAngleDeg @1 :Float32;
steeringRateDeg @2 :Float32;
steeringAccelDeg @3 :Float32;
rateSetPoint @4 :Float32;
accelSetPoint @5 :Float32;
accelError @6 :Float32;
delayedOutput @7 :Float32;
delta @8 :Float32;
output @9 :Float32;
saturated @10 :Bool;
steeringAngleDesiredDeg @11 :Float32;
steeringRateDesiredDeg @12 :Float32;
}
struct LateralLQRState @0x9024e2d790c82ade {
active @0 :Bool;
steeringAngleDeg @1 :Float32;
i @2 :Float32;
output @3 :Float32;
lqrOutput @4 :Float32;
saturated @5 :Bool;
steeringAngleDesiredDeg @6 :Float32;
}
struct LateralCurvatureState @0xad9d8095c06f7c61 {
active @0 :Bool;
actualCurvature @1 :Float32;
desiredCurvature @2 :Float32;
error @3 :Float32;
p @4 :Float32;
i @5 :Float32;
f @6 :Float32;
output @7 :Float32;
saturated @8 :Bool;
}
struct LateralPlannerSolution @0x84caeca5a6b4acfe {
x @0 :List(Float32);
y @1 :List(Float32);
yaw @2 :List(Float32);
yawRate @3 :List(Float32);
xStd @4 :List(Float32);
yStd @5 :List(Float32);
yawStd @6 :List(Float32);
yawRateStd @7 :List(Float32);
}
struct GpsTrajectory @0x8cfeb072f5301000 {
x @0 :List(Float32);
y @1 :List(Float32);
}
+300 -459
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -39,8 +39,8 @@ _services: dict[str, tuple] = {
"roadEncodeIdx": (False, 20., 1),
"liveTracks": (True, 20.),
"sendcan": (True, 100., 139, QueueSize.MEDIUM),
"logMessage": (True, 0.),
"errorLogMessage": (True, 0., 1),
"logMessage": (True, 0., None, QueueSize.BIG),
"errorLogMessage": (True, 0., 1, QueueSize.BIG),
"liveCalibration": (True, 4., 4),
"liveTorqueParameters": (True, 4., 1),
"liveDelay": (True, 4., 1),
+1 -1
View File
@@ -1 +1 @@
#define DEFAULT_MODEL "OP Model (Default)"
#define DEFAULT_MODEL "OP Model 7 (Default)"
+2 -2
View File
@@ -131,11 +131,11 @@ def get_upload_stream(filepath: str, should_compress: bool) -> tuple[io.Buffered
return compressed_stream, compressed_size
# remove all keys that end in DEPRECATED
# remove all keys that end in DEPRECATED, plus any "deprecated" group
def strip_deprecated_keys(d):
for k in list(d.keys()):
if isinstance(k, str):
if k.endswith('DEPRECATED'):
if k.endswith('DEPRECATED') or k == 'deprecated':
d.pop(k)
elif isinstance(d[k], dict):
strip_deprecated_keys(d[k])
+1 -1
Submodule panda updated: 80846cff66...6cd1972ecf
+3 -5
View File
@@ -39,19 +39,17 @@ def clip_curvature(v_ego, prev_curvature, new_curvature, roll) -> tuple[float, b
return float(new_curvature), limited_accel or limited_max_curv
def get_accel_from_plan(speeds, accels, t_idxs, action_t=DT_MDL, vEgoStopping=0.05):
def get_accel_from_plan(speeds, accels, t_idxs, action_t=DT_MDL, vEgoStopping=0.3):
if len(speeds) == len(t_idxs):
v_now = speeds[0]
a_now = accels[0]
v_target = np.interp(action_t, t_idxs, speeds)
a_target = 2 * (v_target - v_now) / (action_t) - a_now
v_target_1sec = np.interp(action_t + 1.0, t_idxs, speeds)
else:
v_now = 0.0
v_target = 0.0
v_target_1sec = 0.0
a_target = 0.0
should_stop = (v_target < vEgoStopping and
v_target_1sec < vEgoStopping)
should_stop = (v_now < vEgoStopping and a_target < 0.1)
return a_target, should_stop
def curv_from_psis(psi_target, psi_rate, vego, action_t):
+3 -3
View File
@@ -30,9 +30,9 @@ def cycle_alerts(duration=200, is_metric=False):
(EventName.accFaulted, ET.IMMEDIATE_DISABLE),
# DM sequence
(EventName.preDriverDistracted, ET.WARNING),
(EventName.promptDriverDistracted, ET.WARNING),
(EventName.driverDistracted, ET.WARNING),
(EventName.driverDistracted1, ET.WARNING),
(EventName.driverDistracted2, ET.WARNING),
(EventName.driverDistracted3, ET.WARNING),
]
# debug alerts
+2 -2
View File
@@ -17,8 +17,8 @@ def estimate_pickle_max_size(onnx_size):
# THREADS=0 is need to prevent bug: https://github.com/tinygrad/tinygrad/issues/14689
tg_flags = {
'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0',
'Darwin': f'DEV=CPU THREADS=0 HOME={os.path.expanduser("~")} IMAGE=0', # tinygrad calls brew which needs a $HOME in the env
}.get(arch, 'DEV=CPU CPU_LLVM=1 THREADS=0 IMAGE=0')
'Darwin': f'DEV=CPU THREADS=0 HOME={os.path.expanduser("~")}', # tinygrad calls brew which needs a $HOME in the env
}.get(arch, 'DEV=CPU CPU_LLVM=1 THREADS=0')
# Get model metadata
for model_name in ['driving_vision', 'driving_off_policy', 'driving_on_policy', 'dmonitoring_model']:
+3 -1
View File
@@ -414,7 +414,9 @@ def main(demo=False):
posenet_send = messaging.new_message('cameraOdometry')
mdv2sp_send = messaging.new_message('modelDataV2SP')
action = get_action_from_model(model_output, prev_action, lat_delay + DT_MDL, long_delay + DT_MDL, v_ego)
frame_delay = DT_MDL # compensate for time passed since the frame was captured: current_time - timestamp_eof is 50ms on average
action_delay = DT_MDL / 2 # middle of the interval between model output (current state) and next frame (expected state)
action = get_action_from_model(model_output, prev_action, lat_delay + frame_delay + action_delay, long_delay + frame_delay + action_delay, v_ego)
prev_action = action
fill_model_msg(drivingdata_send, modelv2_send, model_output, action,
publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id,
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eb6992bd60bada6162fea298e1a414b6b3d6a326db4eda46b9de62bcd8554754
size 13393859
oid sha256:e53f4e0527766082ba7bde38e275def0fe3c14f6c59ae2854439e239884d3ecc
size 13393365
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:86680a657bbb34f997034d1930bb2cb65c38b9222cea199732f72bd45791cfad
size 13022803
oid sha256:ea89c50da3a16e710da292f97c81b083a982cfdee5c28eca0d37ed2fb99af6c5
size 13022642
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7af05e03fd170653ff5771baf373a2c57b363da12c4c411cd416dee067b4cf58
size 23266366
oid sha256:6263aa3fbb44cde6c68a34cdb7cd8c389789dbc02b15c1911afdac4e018281ae
size 23267151
+10 -12
View File
@@ -345,10 +345,14 @@ class DriverMonitoring:
self._reset_awareness()
return
driver_attentive = self.driver_distraction_filter.x < 0.37
awareness_prev = self.awareness
_reaching_pre = self.awareness - self.step_change <= self.threshold_pre
_reaching_terminal = self.awareness - self.step_change <= 0
standstill_orange_exemption = standstill and _reaching_pre
always_on_red_exemption = always_on_valid and not op_engaged and _reaching_terminal
if (driver_attentive and self.face_detected and self.pose.low_std and self.awareness > 0):
if self.awareness > 0 and \
((self.driver_distraction_filter.x < 0.37 and self.face_detected and self.pose.low_std) or standstill_orange_exemption):
if driver_engaged:
self._reset_awareness()
return
@@ -361,34 +365,28 @@ class DriverMonitoring:
if self.awareness > self.threshold_prompt:
return
_reaching_pre = self.awareness - self.step_change <= self.threshold_pre
_reaching_audible = self.awareness - self.step_change <= self.threshold_prompt
_reaching_terminal = self.awareness - self.step_change <= 0
standstill_exemption = standstill and _reaching_pre
always_on_red_exemption = always_on_valid and not op_engaged and _reaching_terminal
certainly_distracted = self.driver_distraction_filter.x > 0.63 and self.driver_distracted and self.face_detected
maybe_distracted = self.hi_stds > self.settings._HI_STD_FALLBACK_TIME or not self.face_detected
if certainly_distracted or maybe_distracted:
# should always be counting if distracted unless at standstill and reaching green
# also will not be reaching 0 if DM is active when not engaged
if not (standstill_exemption or always_on_red_exemption):
if not (standstill_orange_exemption or always_on_red_exemption):
self.awareness = max(self.awareness - self.step_change, -0.1)
alert = None
if self.awareness <= 0.:
# terminal red alert: disengagement required
alert = EventName.driverDistracted if self.active_monitoring_mode else EventName.driverUnresponsive
alert = EventName.driverDistracted3 if self.active_monitoring_mode else EventName.driverUnresponsive3
self.terminal_time += 1
if awareness_prev > 0.:
self.terminal_alert_cnt += 1
elif self.awareness <= self.threshold_prompt:
# prompt orange alert
alert = EventName.promptDriverDistracted if self.active_monitoring_mode else EventName.promptDriverUnresponsive
alert = EventName.driverDistracted2 if self.active_monitoring_mode else EventName.driverUnresponsive2
elif self.awareness <= self.threshold_pre:
# pre green alert
alert = EventName.preDriverDistracted if self.active_monitoring_mode else EventName.preDriverUnresponsive
alert = EventName.driverDistracted1 if self.active_monitoring_mode else EventName.driverUnresponsive1
if alert is not None:
self.current_events.add(alert)
+36 -26
View File
@@ -76,11 +76,11 @@ class TestMonitoring:
assert len(events[int((d_status.settings._DISTRACTED_TIME-d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL)/2/DT_DMON)]) == 0
assert events[int((d_status.settings._DISTRACTED_TIME-d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL + \
((d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL-d_status.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0] == \
EventName.preDriverDistracted
EventName.driverDistracted1
assert events[int((d_status.settings._DISTRACTED_TIME-d_status.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL + \
((d_status.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0] == EventName.promptDriverDistracted
((d_status.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0] == EventName.driverDistracted2
assert events[int((d_status.settings._DISTRACTED_TIME + \
((TEST_TIMESPAN-10-d_status.settings._DISTRACTED_TIME)/2))/DT_DMON)].names[0] == EventName.driverDistracted
((TEST_TIMESPAN-10-d_status.settings._DISTRACTED_TIME)/2))/DT_DMON)].names[0] == EventName.driverDistracted3
assert isinstance(d_status.awareness, float)
# engaged, no face detected the whole time, no action
@@ -89,11 +89,11 @@ class TestMonitoring:
assert len(events[int((d_status.settings._AWARENESS_TIME-d_status.settings._AWARENESS_PRE_TIME_TILL_TERMINAL)/2/DT_DMON)]) == 0
assert events[int((d_status.settings._AWARENESS_TIME-d_status.settings._AWARENESS_PRE_TIME_TILL_TERMINAL + \
((d_status.settings._AWARENESS_PRE_TIME_TILL_TERMINAL-d_status.settings._AWARENESS_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0] == \
EventName.preDriverUnresponsive
EventName.driverUnresponsive1
assert events[int((d_status.settings._AWARENESS_TIME-d_status.settings._AWARENESS_PROMPT_TIME_TILL_TERMINAL + \
((d_status.settings._AWARENESS_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0] == EventName.promptDriverUnresponsive
((d_status.settings._AWARENESS_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0] == EventName.driverUnresponsive2
assert events[int((d_status.settings._AWARENESS_TIME + \
((TEST_TIMESPAN-10-d_status.settings._AWARENESS_TIME)/2))/DT_DMON)].names[0] == EventName.driverUnresponsive
((TEST_TIMESPAN-10-d_status.settings._AWARENESS_TIME)/2))/DT_DMON)].names[0] == EventName.driverUnresponsive3
# engaged, down to orange, driver pays attention, back to normal; then down to orange, driver touches wheel
# - should have short orange recovery time and no green afterwards; wheel touch only recovers when paying attention
@@ -106,10 +106,10 @@ class TestMonitoring:
[car_interaction_DETECTED] * (int(TEST_TIMESPAN/DT_DMON)-int(DISTRACTED_SECONDS_TO_ORANGE*3/DT_DMON))
events, _ = self._run_seq(ds_vector, interaction_vector, always_true, always_false)
assert len(events[int(DISTRACTED_SECONDS_TO_ORANGE*0.5/DT_DMON)]) == 0
assert events[int((DISTRACTED_SECONDS_TO_ORANGE-0.1)/DT_DMON)].names[0] == EventName.promptDriverDistracted
assert events[int((DISTRACTED_SECONDS_TO_ORANGE-0.1)/DT_DMON)].names[0] == EventName.driverDistracted2
assert len(events[int(DISTRACTED_SECONDS_TO_ORANGE*1.5/DT_DMON)]) == 0
assert events[int((DISTRACTED_SECONDS_TO_ORANGE*3-0.1)/DT_DMON)].names[0] == EventName.promptDriverDistracted
assert events[int((DISTRACTED_SECONDS_TO_ORANGE*3+0.1)/DT_DMON)].names[0] == EventName.promptDriverDistracted
assert events[int((DISTRACTED_SECONDS_TO_ORANGE*3-0.1)/DT_DMON)].names[0] == EventName.driverDistracted2
assert events[int((DISTRACTED_SECONDS_TO_ORANGE*3+0.1)/DT_DMON)].names[0] == EventName.driverDistracted2
assert len(events[int((DISTRACTED_SECONDS_TO_ORANGE*3+2.5)/DT_DMON)]) == 0
# engaged, down to orange, driver dodges camera, then comes back still distracted, down to red, \
@@ -129,9 +129,9 @@ class TestMonitoring:
op_vector[int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+2.5)/DT_DMON):int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+3)/DT_DMON)] \
= [False] * int(0.5/DT_DMON)
events, _ = self._run_seq(ds_vector, interaction_vector, op_vector, always_false)
assert events[int((DISTRACTED_SECONDS_TO_ORANGE+0.5*_invisible_time)/DT_DMON)].names[0] == EventName.promptDriverDistracted
assert events[int((DISTRACTED_SECONDS_TO_RED+1.5*_invisible_time)/DT_DMON)].names[0] == EventName.driverDistracted
assert events[int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+1.5)/DT_DMON)].names[0] == EventName.driverDistracted
assert events[int((DISTRACTED_SECONDS_TO_ORANGE+0.5*_invisible_time)/DT_DMON)].names[0] == EventName.driverDistracted2
assert events[int((DISTRACTED_SECONDS_TO_RED+1.5*_invisible_time)/DT_DMON)].names[0] == EventName.driverDistracted3
assert events[int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+1.5)/DT_DMON)].names[0] == EventName.driverDistracted3
assert len(events[int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+3.5)/DT_DMON)]) == 0
# engaged, invisible driver, down to orange, driver touches wheel; then down to orange again, driver appears
@@ -145,13 +145,13 @@ class TestMonitoring:
interaction_vector[int((INVISIBLE_SECONDS_TO_ORANGE)/DT_DMON):int((INVISIBLE_SECONDS_TO_ORANGE+1)/DT_DMON)] = [True] * int(1/DT_DMON)
events, _ = self._run_seq(ds_vector, interaction_vector, 2*always_true, 2*always_false)
assert len(events[int(INVISIBLE_SECONDS_TO_ORANGE*0.5/DT_DMON)]) == 0
assert events[int((INVISIBLE_SECONDS_TO_ORANGE-0.1)/DT_DMON)].names[0] == EventName.promptDriverUnresponsive
assert events[int((INVISIBLE_SECONDS_TO_ORANGE-0.1)/DT_DMON)].names[0] == EventName.driverUnresponsive2
assert len(events[int((INVISIBLE_SECONDS_TO_ORANGE+0.1)/DT_DMON)]) == 0
if _visible_time == 0.5:
assert events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1-0.1)/DT_DMON)].names[0] == EventName.promptDriverUnresponsive
assert events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1+0.1+_visible_time)/DT_DMON)].names[0] == EventName.preDriverUnresponsive
assert events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1-0.1)/DT_DMON)].names[0] == EventName.driverUnresponsive2
assert events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1+0.1+_visible_time)/DT_DMON)].names[0] == EventName.driverUnresponsive1
elif _visible_time == 10:
assert events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1-0.1)/DT_DMON)].names[0] == EventName.promptDriverUnresponsive
assert events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1-0.1)/DT_DMON)].names[0] == EventName.driverUnresponsive2
assert len(events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1+0.1+_visible_time)/DT_DMON)]) == 0
# engaged, invisible driver, down to red, driver appears and then touches wheel, then disengages/reengages
@@ -166,10 +166,10 @@ class TestMonitoring:
op_vector[int((INVISIBLE_SECONDS_TO_RED+_visible_time+1)/DT_DMON):int((INVISIBLE_SECONDS_TO_RED+_visible_time+0.5)/DT_DMON)] = [False] * int(0.5/DT_DMON)
events, _ = self._run_seq(ds_vector, interaction_vector, op_vector, always_false)
assert len(events[int(INVISIBLE_SECONDS_TO_ORANGE*0.5/DT_DMON)]) == 0
assert events[int((INVISIBLE_SECONDS_TO_ORANGE-0.1)/DT_DMON)].names[0] == EventName.promptDriverUnresponsive
assert events[int((INVISIBLE_SECONDS_TO_RED-0.1)/DT_DMON)].names[0] == EventName.driverUnresponsive
assert events[int((INVISIBLE_SECONDS_TO_RED+0.5*_visible_time)/DT_DMON)].names[0] == EventName.driverUnresponsive
assert events[int((INVISIBLE_SECONDS_TO_RED+_visible_time+0.5)/DT_DMON)].names[0] == EventName.driverUnresponsive
assert events[int((INVISIBLE_SECONDS_TO_ORANGE-0.1)/DT_DMON)].names[0] == EventName.driverUnresponsive2
assert events[int((INVISIBLE_SECONDS_TO_RED-0.1)/DT_DMON)].names[0] == EventName.driverUnresponsive3
assert events[int((INVISIBLE_SECONDS_TO_RED+0.5*_visible_time)/DT_DMON)].names[0] == EventName.driverUnresponsive3
assert events[int((INVISIBLE_SECONDS_TO_RED+_visible_time+0.5)/DT_DMON)].names[0] == EventName.driverUnresponsive3
assert len(events[int((INVISIBLE_SECONDS_TO_RED+_visible_time+1+0.1)/DT_DMON)]) == 0
# disengaged, always distracted driver
@@ -187,8 +187,19 @@ class TestMonitoring:
events, d_status = self._run_seq(always_distracted, always_false, always_true, standstill_vector)
assert len(events[int((_redlight_time-0.1)/DT_DMON)]) == 0
_pre_to_prompt = d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL - d_status.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL
assert events[int((_redlight_time+0.5)/DT_DMON)].names[0] == EventName.preDriverDistracted
assert events[int((_redlight_time+_pre_to_prompt+0.5)/DT_DMON)].names[0] == EventName.promptDriverDistracted
assert events[int((_redlight_time+0.5)/DT_DMON)].names[0] == EventName.driverDistracted1
assert events[int((_redlight_time+_pre_to_prompt+0.5)/DT_DMON)].names[0] == EventName.driverDistracted2
# engaged, distracted while moving, then car stops after reaching orange
# - should reset timer to pre green at standstill
def test_distracted_then_stops(self):
_stop_time = DISTRACTED_SECONDS_TO_ORANGE + 1 # stop 1 second after reaching orange
standstill_vector = always_false[:]
standstill_vector[int(_stop_time/DT_DMON):] = [True] * int((TEST_TIMESPAN-_stop_time)/DT_DMON)
events, _ = self._run_seq(always_distracted, always_false, always_true, standstill_vector)
# just before and briefly after stopping: orange alert; goes away quickly after stopped
assert events[int((_stop_time+0.1)/DT_DMON)].names[0] == EventName.driverDistracted2
assert len(events[int((_stop_time+0.5)/DT_DMON)]) == 0
# engaged, model is somehow uncertain and driver is distracted
# - should fall back to wheel touch after uncertain alert
@@ -196,11 +207,11 @@ class TestMonitoring:
ds_vector = [msg_DISTRACTED_BUT_SOMEHOW_UNCERTAIN] * int(TEST_TIMESPAN/DT_DMON)
interaction_vector = always_false[:]
events, d_status = self._run_seq(ds_vector, interaction_vector, always_true, always_false)
assert EventName.preDriverUnresponsive in \
assert EventName.driverUnresponsive1 in \
events[int((INVISIBLE_SECONDS_TO_ORANGE-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME-0.1)/DT_DMON)].names
assert EventName.promptDriverUnresponsive in \
assert EventName.driverUnresponsive2 in \
events[int((INVISIBLE_SECONDS_TO_ORANGE-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)].names
assert EventName.driverUnresponsive in \
assert EventName.driverUnresponsive3 in \
events[int((INVISIBLE_SECONDS_TO_RED-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)].names
@@ -265,4 +276,3 @@ def test_enabled_states(enabled_state, lat_active_state, expected):
actual_enabled = captured_args[0]
assert actual_enabled == expected, f"Expected op_engaged={expected}, but got {actual_enabled}"
+8 -8
View File
@@ -338,7 +338,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
Priority.LOW, VisualAlert.steerRequired, AudibleAlert.prompt, 1.8),
},
EventName.preDriverDistracted: {
EventName.driverDistracted1: {
ET.PERMANENT: Alert(
"Pay Attention",
"",
@@ -346,7 +346,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
Priority.LOW, VisualAlert.none, AudibleAlert.none, .1),
},
EventName.promptDriverDistracted: {
EventName.driverDistracted2: {
ET.PERMANENT: Alert(
"Pay Attention",
"Driver Distracted",
@@ -354,7 +354,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
Priority.MID, VisualAlert.steerRequired, AudibleAlert.promptDistracted, .1),
},
EventName.driverDistracted: {
EventName.driverDistracted3: {
ET.PERMANENT: Alert(
"DISENGAGE IMMEDIATELY",
"Driver Distracted",
@@ -362,7 +362,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
Priority.HIGH, VisualAlert.steerRequired, AudibleAlert.warningImmediate, .1),
},
EventName.preDriverUnresponsive: {
EventName.driverUnresponsive1: {
ET.PERMANENT: Alert(
"Touch Steering Wheel: No Face Detected",
"",
@@ -370,7 +370,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
Priority.LOW, VisualAlert.steerRequired, AudibleAlert.none, .1),
},
EventName.promptDriverUnresponsive: {
EventName.driverUnresponsive2: {
ET.PERMANENT: Alert(
"Touch Steering Wheel",
"Driver Unresponsive",
@@ -378,7 +378,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
Priority.MID, VisualAlert.steerRequired, AudibleAlert.promptDistracted, .1),
},
EventName.driverUnresponsive: {
EventName.driverUnresponsive3: {
ET.PERMANENT: Alert(
"DISENGAGE IMMEDIATELY",
"Driver Unresponsive",
@@ -858,14 +858,14 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
if HARDWARE.get_device_type() == 'mici':
EVENTS.update({
EventName.preDriverDistracted: {
EventName.driverDistracted1: {
ET.PERMANENT: Alert(
"Pay Attention",
"",
AlertStatus.normal, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.none, 2),
},
EventName.promptDriverDistracted: {
EventName.driverDistracted2: {
ET.PERMANENT: Alert(
"Pay Attention",
"Driver Distracted",
+2 -2
View File
@@ -46,8 +46,8 @@ class FuzzyGenerator:
def generate_struct(self, schema: capnp.lib.capnp._StructSchema, event: str | None = None) -> st.SearchStrategy[dict[str, Any]]:
single_fill: tuple[str, ...] = (event,) if event else (self.draw(st.sampled_from(schema.union_fields)),) if schema.union_fields else ()
fields_to_generate = schema.non_union_fields + single_fill
return st.fixed_dictionaries({field: self.generate_field(schema.fields[field]) for field in fields_to_generate if not field.endswith('DEPRECATED')})
fields_to_generate = [f for f in schema.non_union_fields + single_fill if not f.endswith('DEPRECATED') and f != 'deprecated']
return st.fixed_dictionaries({field: self.generate_field(schema.fields[field]) for field in fields_to_generate})
@staticmethod
@cache
+23 -10
View File
@@ -100,6 +100,17 @@ def migration(inputs: list[str], product: str|None=None):
return decorator
def migrate_onroad_event(event: capnp.lib.capnp._DynamicStructReader):
event_dict = event.to_dict()
try:
return log.OnroadEvent(**event_dict)
except capnp.lib.capnp.KjException as e:
# Ignore legacy events the current schema no longer defines.
if "enum has no such enumerant" in str(e):
return None
raise
@migration(inputs=["longitudinalPlan", "carParams"])
def migrate_longitudinalPlan(msgs):
ops = []
@@ -216,7 +227,7 @@ def migrate_controlsState(msgs):
for field in ("enabled", "active", "state", "engageable", "alertText1", "alertText2",
"alertStatus", "alertSize", "alertType", "experimentalMode",
"personality"):
setattr(ss, field, getattr(msg.controlsState, field+"DEPRECATED"))
setattr(ss, field, getattr(msg.controlsState.deprecated, field))
add_ops.append(m.as_reader())
return [], add_ops, []
@@ -229,10 +240,10 @@ def migrate_carState(msgs):
if msg.which() == 'controlsState':
last_cs = msg
elif msg.which() == 'carState' and last_cs is not None:
if last_cs.controlsState.vCruiseDEPRECATED - msg.carState.vCruise > 0.1:
if last_cs.controlsState.deprecated.vCruise - msg.carState.vCruise > 0.1:
msg = msg.as_builder()
msg.carState.vCruise = last_cs.controlsState.vCruiseDEPRECATED
msg.carState.vCruiseCluster = last_cs.controlsState.vCruiseClusterDEPRECATED
msg.carState.vCruise = last_cs.controlsState.deprecated.vCruise
msg.carState.vCruiseCluster = last_cs.controlsState.deprecated.vCruiseCluster
ops.append((index, msg.as_reader()))
return ops, [], []
@@ -458,12 +469,13 @@ def migrate_onroadEvents(msgs):
for event in msg.onroadEventsDEPRECATED:
try:
if not str(event.name).endswith('DEPRECATED'):
# dict converts name enum into string representation
onroadEvents.append(log.OnroadEvent(**event.to_dict()))
migrated_event = migrate_onroad_event(event)
if migrated_event is not None:
onroadEvents.append(migrated_event)
except RuntimeError: # Member was null
traceback.print_exc()
new_msg = messaging.new_message('onroadEvents', len(msg.onroadEventsDEPRECATED))
new_msg = messaging.new_message('onroadEvents', len(onroadEvents))
new_msg.valid = msg.valid
new_msg.logMonoTime = msg.logMonoTime
new_msg.onroadEvents = onroadEvents
@@ -478,11 +490,12 @@ def migrate_driverMonitoringState(msgs):
for index, msg in msgs:
msg = msg.as_builder()
events = []
for event in msg.driverMonitoringState.eventsDEPRECATED:
for event in msg.driverMonitoringState.deprecated.events:
try:
if not str(event.name).endswith('DEPRECATED'):
# dict converts name enum into string representation
events.append(log.OnroadEvent(**event.to_dict()))
migrated_event = migrate_onroad_event(event)
if migrated_event is not None:
events.append(migrated_event)
except RuntimeError: # Member was null
traceback.print_exc()
+1
View File
@@ -144,6 +144,7 @@ int cachedFetch(const std::string &cache) {
LOGD("Fetching with cache: %s", cache.c_str());
run(util::string_format("cp -rp %s %s", cache.c_str(), TMP_INSTALL_PATH).c_str());
run(util::string_format("cd %s && git remote set-url origin %s", TMP_INSTALL_PATH, GIT_URL.c_str()).c_str());
run(util::string_format("cd %s && git remote set-branches --add origin %s", TMP_INSTALL_PATH, migrated_branch.c_str()).c_str());
renderProgress(10);
+1 -1
View File
@@ -153,7 +153,7 @@ class HudRenderer(Widget):
v_cruise_cluster = car_state.vCruiseCluster
set_speed = (
controls_state.vCruiseDEPRECATED if v_cruise_cluster == 0.0 else v_cruise_cluster
controls_state.deprecated.vCruise if v_cruise_cluster == 0.0 else v_cruise_cluster
)
engaged = sm['selfdriveState'].enabled
if (set_speed != self.set_speed and engaged) or (engaged and not self._engaged):
+1 -1
View File
@@ -86,7 +86,7 @@ class HudRenderer(Widget):
v_cruise_cluster = car_state.vCruiseCluster
self.set_speed = (
controls_state.vCruiseDEPRECATED if v_cruise_cluster == 0.0 else v_cruise_cluster
controls_state.deprecated.vCruise if v_cruise_cluster == 0.0 else v_cruise_cluster
)
self.is_cruise_set = 0 < self.set_speed < SET_SPEED_NA
self.is_cruise_available = self.set_speed != -1
+15 -1
View File
@@ -20,6 +20,7 @@ SAMPLE_RATE = 48000
SAMPLE_BUFFER = 4096 # (approx 100ms)
MAX_VOLUME = 1.0
MIN_VOLUME = 0.1
ALERT_RAMP_TIME = 4 # seconds to ramp to max volume for warningImmediate
SELFDRIVE_STATE_TIMEOUT = 5 # 5 seconds
FILTER_DT = 1. / (micd.SAMPLE_RATE / micd.FFT_SAMPLES)
@@ -82,6 +83,9 @@ class Soundd(QuietMode):
self.current_volume = MIN_VOLUME
self.current_sound_frame = 0
self.ramp_start_volume = MIN_VOLUME
self.ramp_start_time = 0.
self.selfdrive_timeout_alert = False
self.spl_filter_weighted = FirstOrderFilter(0, 2.5, FILTER_DT, initialized=False)
@@ -130,6 +134,9 @@ class Soundd(QuietMode):
def update_alert(self, new_alert):
current_alert_played_once = self.current_alert == AudibleAlert.none or self.current_sound_frame > len(self.loaded_sounds[self.current_alert])
if self.current_alert != new_alert and (new_alert != AudibleAlert.none or current_alert_played_once):
if new_alert == AudibleAlert.warningImmediate:
self.ramp_start_volume = self.current_volume
self.ramp_start_time = time.monotonic()
self.current_alert = new_alert
self.current_sound_frame = 0
@@ -170,12 +177,19 @@ class Soundd(QuietMode):
self.load_param()
if sm.updated['soundPressure'] and self.current_alert == AudibleAlert.none: # only update volume filter when not playing alert
# Always update volume, even when alert is playing
if sm.updated['soundPressure']:
self.spl_filter_weighted.update(sm["soundPressure"].soundPressureWeightedDb)
self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x))
self.get_audible_alert(sm)
# Ramp up immediate warning sound over 4s
if self.current_alert == AudibleAlert.warningImmediate:
elapsed = time.monotonic() - self.ramp_start_time
ramp_vol = float(np.interp(elapsed, [0, ALERT_RAMP_TIME], [self.ramp_start_volume, MAX_VOLUME]))
self.current_volume = max(self.current_volume, ramp_vol)
rk.keep_time()
assert stream.active
+1 -1
View File
@@ -1 +1 @@
adfcb5ccac9cfaf291af6091d12e71be3f543c7694fc29d80caa561dc32194d7
793b5d480edb5a30eed3d0d3bdb43259522978670f6bc3dea7a4d661261d3c48
-9
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env python3
import argparse
import time
from openpilot.system.hardware import HARDWARE
@@ -13,16 +12,13 @@ if __name__ == '__main__':
parser.add_argument('--nickname', nargs=2, metavar=('iccid', 'name'), help='update the nickname for a profile')
args = parser.parse_args()
mutated = False
lpa = HARDWARE.get_sim_lpa()
if args.switch:
lpa.switch_profile(args.switch)
mutated = True
elif args.delete:
confirm = input('are you sure you want to delete this profile? (y/N) ')
if confirm == 'y':
lpa.delete_profile(args.delete)
mutated = True
else:
print('cancelled')
exit(0)
@@ -33,11 +29,6 @@ if __name__ == '__main__':
else:
parser.print_help()
if mutated:
HARDWARE.reboot_modem()
# eUICC needs a small delay post-reboot before querying profiles
time.sleep(.5)
profiles = lpa.list_profiles()
print(f'\n{len(profiles)} profile{"s" if len(profiles) > 1 else ""}:')
for p in profiles:
+233 -67
View File
@@ -2,17 +2,23 @@
import atexit
import base64
import fcntl
import math
import os
import serial
import subprocess
import sys
import termios
import time
from collections.abc import Generator
from collections.abc import Callable, Generator
from contextlib import contextmanager
from typing import Any
from openpilot.system.hardware.base import LPABase, Profile
from openpilot.system.hardware.base import LPABase, LPAError, Profile
DEFAULT_DEVICE = "/dev/ttyUSB2"
DEFAULT_DEVICE = "/dev/modem_at0"
DEFAULT_BAUD = 9600
DEFAULT_TIMEOUT = 5.0
# https://euicc-manual.osmocom.org/docs/lpa/applet-id/
@@ -20,44 +26,82 @@ ISDR_AID = "A0000005591010FFFFFFFF8900000100"
MM = "org.freedesktop.ModemManager1"
MM_MODEM = MM + ".Modem"
ES10X_MSS = 120
OPEN_ISDR_RETRIES = 10
OPEN_ISDR_RETRY_DELAY_S = 0.25
OPEN_ISDR_RESET_ATTEMPT = 5
SEND_APDU_RETRIES = 3
LOCK_FILE = '/dev/shm/modem_lpa.lock'
DEBUG = os.environ.get("DEBUG") == "1"
# TLV Tags
TAG_ICCID = 0x5A
TAG_STATUS = 0x80
TAG_PROFILE_INFO_LIST = 0xBF2D
TAG_SET_NICKNAME = 0xBF29
TAG_ENABLE_PROFILE = 0xBF31
TAG_DELETE_PROFILE = 0xBF33
TAG_OK = 0xA0
PROFILE_OK = 0x00
PROFILE_NOT_IN_DISABLED_STATE = 0x02
PROFILE_CAT_BUSY = 0x05
PROFILE_ERROR_CODES = {
0x01: "iccidOrAidNotFound", PROFILE_NOT_IN_DISABLED_STATE: "profileNotInDisabledState",
0x03: "disallowedByPolicy", 0x04: "wrongProfileReenabling",
PROFILE_CAT_BUSY: "catBusy", 0x06: "undefinedError",
}
STATE_LABELS = {0: "disabled", 1: "enabled", 255: "unknown"}
ICON_LABELS = {0: "jpeg", 1: "png", 255: "unknown"}
CLASS_LABELS = {0: "test", 1: "provisioning", 2: "operational", 255: "unknown"}
# TLV tag -> (field_name, decoder)
FieldMap = dict[int, tuple[str, Callable[[bytes], Any]]]
def b64e(data: bytes) -> str:
return base64.b64encode(data).decode("ascii")
def base64_trim(s: str) -> str:
return "".join(c for c in s if c not in "\n\r \t")
def b64d(s: str) -> bytes:
return base64.b64decode(base64_trim(s))
class AtClient:
def __init__(self, device: str, baud: int, timeout: float, debug: bool) -> None:
self.debug = debug
def __init__(self, device: str, baud: int, timeout: float) -> None:
self.channel: str | None = None
self._device = device
self._baud = baud
self._timeout = timeout
self._serial: serial.Serial | None = None
try:
self._serial = serial.Serial(device, baudrate=baud, timeout=timeout)
self._serial.reset_input_buffer()
except (serial.SerialException, PermissionError, OSError):
pass
self._use_dbus = not os.path.exists(device)
def send_raw(self, data: bytes) -> None:
self._ensure_serial()
self._serial.reset_input_buffer()
self._serial.write(data)
self._serial.flush()
def close(self) -> None:
try:
if self.channel:
self.query(f"AT+CCHC={self.channel}")
try:
self.query(f"AT+CCHC={self.channel}")
except (RuntimeError, TimeoutError):
pass
self.channel = None
finally:
if self._serial:
self._serial.close()
def _send(self, cmd: str) -> None:
if self.debug:
if DEBUG:
print(f"SER >> {cmd}", file=sys.stderr)
self._serial.write((cmd + "\r").encode("ascii"))
@@ -70,7 +114,7 @@ class AtClient:
line = raw.decode(errors="ignore").strip()
if not line:
continue
if self.debug:
if DEBUG:
print(f"SER << {line}", file=sys.stderr)
if line == "OK":
return lines
@@ -78,6 +122,18 @@ class AtClient:
raise RuntimeError(f"AT command failed: {line}")
lines.append(line)
def _ensure_serial(self, reconnect: bool = False) -> None:
if reconnect:
self.channel = None
try:
if self._serial:
self._serial.close()
except Exception:
pass
self._serial = None
if self._serial is None:
self._serial = serial.Serial(self._device, baudrate=self._baud, timeout=self._timeout)
def _get_modem(self):
import dbus
bus = dbus.SystemBus()
@@ -87,48 +143,88 @@ class AtClient:
return bus.get_object(MM, modem_path)
def _dbus_query(self, cmd: str) -> list[str]:
if self.debug:
if DEBUG:
print(f"DBUS >> {cmd}", file=sys.stderr)
try:
result = str(self._get_modem().Command(cmd, math.ceil(self._timeout), dbus_interface=MM_MODEM, timeout=self._timeout))
except Exception as e:
raise RuntimeError(f"AT command failed: {e}") from e
lines = [line.strip() for line in result.splitlines() if line.strip()]
if self.debug:
if DEBUG:
for line in lines:
print(f"DBUS << {line}", file=sys.stderr)
return lines
def query(self, cmd: str) -> list[str]:
if self._serial:
if self._use_dbus:
return self._dbus_query(cmd)
self._ensure_serial()
try:
self._send(cmd)
return self._expect()
except serial.SerialException:
self._ensure_serial(reconnect=True)
self._send(cmd)
return self._expect()
return self._dbus_query(cmd)
def open_isdr(self) -> None:
# close any stale logical channel from a previous crashed session
try:
self.query("AT+CCHC=1")
except RuntimeError:
pass
def _open_isdr_once(self) -> None:
if self.channel:
try:
self.query(f"AT+CCHC={self.channel}")
except RuntimeError:
pass
self.channel = None
# drain any unsolicited responses before opening
if self._serial and not self._use_dbus:
try:
self._serial.reset_input_buffer()
except (OSError, serial.SerialException, termios.error):
self._ensure_serial(reconnect=True)
for line in self.query(f'AT+CCHO="{ISDR_AID}"'):
if line.startswith("+CCHO:") and (ch := line.split(":", 1)[1].strip()):
self.channel = ch
return
raise RuntimeError("Failed to open ISD-R application")
def _reset_modem(self) -> None:
if self._serial:
try:
self._serial.close()
except Exception:
pass
self._serial = None
subprocess.run(['/usr/comma/lte/lte.sh', 'start'], capture_output=True)
def open_isdr(self) -> None:
for attempt in range(OPEN_ISDR_RETRIES):
try:
self._open_isdr_once()
return
except (RuntimeError, TimeoutError, termios.error, serial.SerialException):
time.sleep(OPEN_ISDR_RETRY_DELAY_S)
if attempt == OPEN_ISDR_RESET_ATTEMPT:
self._reset_modem()
raise RuntimeError("Failed to open ISD-R after retries")
def send_apdu(self, apdu: bytes) -> tuple[bytes, int, int]:
if not self.channel:
raise RuntimeError("Logical channel is not open")
hex_payload = apdu.hex().upper()
for line in self.query(f'AT+CGLA={self.channel},{len(hex_payload)},"{hex_payload}"'):
if line.startswith("+CGLA:"):
parts = line.split(":", 1)[1].split(",", 1)
if len(parts) == 2:
data = bytes.fromhex(parts[1].strip().strip('"'))
if len(data) >= 2:
return data[:-2], data[-2], data[-1]
raise RuntimeError("Missing +CGLA response")
for attempt in range(SEND_APDU_RETRIES):
try:
if not self.channel:
self.open_isdr()
hex_payload = apdu.hex().upper()
for line in self.query(f'AT+CGLA={self.channel},{len(hex_payload)},"{hex_payload}"'):
if line.startswith("+CGLA:"):
parts = line.split(":", 1)[1].split(",", 1)
if len(parts) == 2:
data = bytes.fromhex(parts[1].strip().strip('"'))
if len(data) >= 2:
return data[:-2], data[-2], data[-1]
raise RuntimeError("Missing +CGLA response")
except (RuntimeError, ValueError):
self.channel = None
if attempt == SEND_APDU_RETRIES - 1:
raise
raise RuntimeError("send_apdu failed")
# --- TLV utilities ---
@@ -170,12 +266,37 @@ def find_tag(data: bytes, target: int) -> bytes | None:
return next((v for t, v in iter_tlv(data) if t == target), None)
def require_tag(data: bytes, target: int, label: str = "") -> bytes:
v = find_tag(data, target)
if v is None:
raise RuntimeError(f"Missing {label or f'tag 0x{target:X}'}")
return v
def tbcd_to_string(raw: bytes) -> str:
return "".join(str(n) for b in raw for n in (b & 0x0F, b >> 4) if n <= 9)
# Profile field decoders: TLV tag -> (field_name, decoder)
_PROFILE_FIELDS = {
def string_to_tbcd(s: str) -> bytes:
digits = [int(c) for c in s if c.isdigit()]
return bytes(digits[i] | ((digits[i + 1] if i + 1 < len(digits) else 0xF) << 4) for i in range(0, len(digits), 2))
def encode_tlv(tag: int, value: bytes) -> bytes:
tag_bytes = bytes([(tag >> 8) & 0xFF, tag & 0xFF]) if tag > 255 else bytes([tag])
vlen = len(value)
if vlen <= 127:
return tag_bytes + bytes([vlen]) + value
length_bytes = vlen.to_bytes((vlen.bit_length() + 7) // 8, "big")
return tag_bytes + bytes([0x80 | len(length_bytes)]) + length_bytes + value
def int_bytes(n: int) -> bytes:
"""Encode a positive integer as minimal big-endian bytes (at least 1 byte)."""
return n.to_bytes((n.bit_length() + 7) // 8 or 1, "big")
PROFILE: FieldMap = {
TAG_ICCID: ("iccid", tbcd_to_string),
0x4F: ("isdpAid", lambda v: v.hex().upper()),
0x9F70: ("profileState", lambda v: STATE_LABELS.get(v[0], "unknown")),
@@ -188,11 +309,11 @@ _PROFILE_FIELDS = {
}
def _decode_profile_fields(data: bytes) -> dict:
"""Parse known profile metadata TLV fields into a dict."""
result = {}
def decode_struct(data: bytes, field_map: FieldMap) -> dict[str, Any]:
"""Parse TLV data using a {tag: (field_name, decoder)} map into a dict."""
result: dict[str, Any] = {name: None for name, _ in field_map.values()}
for tag, value in iter_tlv(data):
if (field := _PROFILE_FIELDS.get(tag)):
if (field := field_map.get(tag)):
result[field[0]] = field[1](value)
return result
@@ -225,57 +346,102 @@ def es10x_command(client: AtClient, data: bytes) -> bytes:
# --- Profile operations ---
def decode_profiles(blob: bytes) -> list[dict]:
root = find_tag(blob, TAG_PROFILE_INFO_LIST)
if root is None:
raise RuntimeError("Missing ProfileInfoList")
list_ok = find_tag(root, 0xA0)
root = require_tag(blob, TAG_PROFILE_INFO_LIST, "ProfileInfoList")
list_ok = find_tag(root, TAG_OK)
if list_ok is None:
return []
defaults = {name: None for name, _ in _PROFILE_FIELDS.values()}
return [{**defaults, **_decode_profile_fields(value)} for tag, value in iter_tlv(list_ok) if tag == 0xE3]
return [decode_struct(value, PROFILE) for tag, value in iter_tlv(list_ok) if tag == 0xE3]
def list_profiles(client: AtClient) -> list[dict]:
return decode_profiles(es10x_command(client, TAG_PROFILE_INFO_LIST.to_bytes(2, "big") + b"\x00"))
def set_profile_nickname(client: AtClient, iccid: str, nickname: str) -> None:
nickname_bytes = nickname.encode("utf-8")
if len(nickname_bytes) > 64:
raise ValueError("Profile nickname must be 64 bytes or less")
content = encode_tlv(TAG_ICCID, string_to_tbcd(iccid)) + encode_tlv(0x90, nickname_bytes)
response = es10x_command(client, encode_tlv(TAG_SET_NICKNAME, content))
code = require_tag(require_tag(response, TAG_SET_NICKNAME, "SetNicknameResponse"), TAG_STATUS, "SetNickname status")[0]
if code == 0x01:
raise LPAError(f"profile {iccid} not found")
if code != 0x00:
raise RuntimeError(f"SetNickname failed with status 0x{code:02X}")
class TiciLPA(LPABase):
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if hasattr(self, '_client'):
return
self._client = AtClient(DEFAULT_DEVICE, DEFAULT_BAUD, DEFAULT_TIMEOUT, debug=DEBUG)
self._client.open_isdr()
self._client = AtClient(DEFAULT_DEVICE, DEFAULT_BAUD, DEFAULT_TIMEOUT)
atexit.register(self._client.close)
@contextmanager
def _acquire_channel(self):
fd = os.open(LOCK_FILE, os.O_CREAT | os.O_RDWR)
try:
fcntl.flock(fd, fcntl.LOCK_EX)
self._client.open_isdr()
yield
finally:
if self._client.channel:
try:
self._client.query(f"AT+CCHC={self._client.channel}")
except (RuntimeError, TimeoutError):
pass
self._client.channel = None
fcntl.flock(fd, fcntl.LOCK_UN)
os.close(fd)
def list_profiles(self) -> list[Profile]:
return [
Profile(
iccid=p.get("iccid", ""),
nickname=p.get("profileNickname") or "",
enabled=p.get("profileState") == "enabled",
provider=p.get("serviceProviderName") or "",
)
for p in list_profiles(self._client)
]
with self._acquire_channel():
return [
Profile(
iccid=p.get("iccid", ""),
nickname=p.get("profileNickname") or "",
enabled=p.get("profileState") == "enabled",
provider=p.get("serviceProviderName") or "",
)
for p in list_profiles(self._client)
]
def get_active_profile(self) -> Profile | None:
return None
def delete_profile(self, iccid: str) -> None:
return None
if self.is_comma_profile(iccid):
raise LPAError("refusing to delete a comma profile")
with self._acquire_channel():
request = encode_tlv(TAG_DELETE_PROFILE, encode_tlv(TAG_ICCID, string_to_tbcd(iccid)))
response = es10x_command(self._client, request)
code = require_tag(require_tag(response, TAG_DELETE_PROFILE, "DeleteProfileResponse"), TAG_STATUS, "DeleteProfile status")[0]
if code != PROFILE_OK:
raise LPAError(f"DeleteProfile failed: {PROFILE_ERROR_CODES.get(code, 'unknown')} (0x{code:02X})")
def download_profile(self, qr: str, nickname: str | None = None) -> None:
return None
def nickname_profile(self, iccid: str, nickname: str) -> None:
return None
with self._acquire_channel():
set_profile_nickname(self._client, iccid, nickname)
def _enable_profile(self, iccid: str) -> int:
inner = encode_tlv(TAG_OK, encode_tlv(TAG_ICCID, string_to_tbcd(iccid)))
inner += b'\x01\x01\x01' # refreshFlag=1
response = es10x_command(self._client, encode_tlv(TAG_ENABLE_PROFILE, inner))
return require_tag(require_tag(response, TAG_ENABLE_PROFILE, "EnableProfileResponse"), TAG_STATUS, "EnableProfile status")[0]
def switch_profile(self, iccid: str) -> None:
return None
with self._acquire_channel():
code = self._enable_profile(iccid)
if code == PROFILE_CAT_BUSY: # stale eUICC transaction, reset and retry
self._client._reset_modem()
self._client.open_isdr()
code = self._enable_profile(iccid)
if code not in (PROFILE_OK, PROFILE_NOT_IN_DISABLED_STATE):
raise LPAError(f"EnableProfile failed: {PROFILE_ERROR_CODES.get(code, 'unknown')} (0x{code:02X})")
from openpilot.system.hardware import HARDWARE
if HARDWARE.get_device_type() == "mici":
self._client.send_raw(b'AT+CFUN=0\rAT+CFUN=1\r') # mici has no SIM presence pin; raw because CFUN=0 drops serial
self._client._ensure_serial(reconnect=True)
+1 -1
View File
@@ -365,7 +365,7 @@ class UbloxMsgParser:
assert isinstance(s1, Glonass.String1)
eph.p1 = int(s1.p1)
tk = int(s1.t_k)
eph.tkDEPRECATED = tk
eph.deprecated.tk = tk
eph.xVel = float(s1.x_vel) * math.pow(2, -20)
eph.xAccel = float(s1.x_accel) * math.pow(2, -30)
eph.x = float(s1.x) * math.pow(2, -11)
+1 -1
View File
@@ -16,7 +16,7 @@ def generate_type(type_walker, schema_walker) -> str | list[Any] | dict[str, Any
def generate_struct(schema: capnp.lib.capnp._StructSchema) -> dict[str, Any]:
return {field: generate_field(schema.fields[field]) for field in schema.fields if not field.endswith("DEPRECATED")}
return {field: generate_field(schema.fields[field]) for field in schema.fields if not field.endswith("DEPRECATED") and field != "deprecated"}
def generate_field(field: capnp.lib.capnp._StructSchemaField) -> str | list[Any] | dict[str, Any]:
+12 -55
View File
@@ -15,29 +15,23 @@
<body>
<div id="main">
<p class="jumbo">comma body</p>
<audio id="audio" autoplay="true"></audio>
<video id="video" playsinline autoplay muted loop poster="/static/poster.png"></video>
<div id="icon-panel" class="row">
<div class="col-sm-12 col-md-6 details">
<div class="icon-sup-panel col-12">
<div class="icon-sub-panel">
<div class="icon-sub-sub-panel">
<i class="bi bi-speaker-fill pre-blob"></i>
<i class="bi bi-mic-fill pre-blob"></i>
<i class="bi bi-camera-video-fill pre-blob"></i>
</div>
<p class="small">body</p>
<div class="row" style="width: 100%; padding: 10px 10px 0px 10px;">
<div id="wasd" class="col-md-12 row">
<div class="col-md-6 col-sm-12" style="justify-content: center; display: flex; flex-direction: column;">
<div class="wasd-row">
<div class="keys" id="key-w">W</div>
</div>
<div class="icon-sub-panel">
<div class="icon-sub-sub-panel">
<i class="bi bi-speaker-fill pre-blob"></i>
<i class="bi bi-mic-fill pre-blob"></i>
</div>
<p class="small">you</p>
<div class="wasd-row">
<div class="keys" id="key-a">A</div>
<div class="keys" id="key-s">S</div>
<div class="keys" id="key-d">D</div>
</div>
</div>
</div>
<div class="col-sm-12 col-md-6 details">
</div>
<div id="icon-panel" class="row" style="width: 100%; padding: 0px 10px 0px 10px;">
<div class="col-12 details">
<div class="icon-sup-panel col-12">
<div class="icon-sub-panel">
<div class="icon-sub-sub-panel">
@@ -53,43 +47,6 @@
</div>
</div>
</div>
<!-- <div class="icon-sub-panel">
<button type="button" id="start" class="btn btn-light btn-lg">Start</button>
<button type="button" id="stop" class="btn btn-light btn-lg">Stop</button>
</div> -->
</div>
<div class="row" style="width: 100%; padding: 0px 10px 0px 10px;">
<div id="wasd" class="col-md-12 row">
<div class="col-md-6 col-sm-12" style="justify-content: center; display: flex; flex-direction: column;">
<div class="wasd-row">
<div class="keys" id="key-w">W</div>
<div id="key-val"><span id="pos-vals">0,0</span><span>x,y</span></div>
</div>
<div class="wasd-row">
<div class="keys" id="key-a">A</div>
<div class="keys" id="key-s">S</div>
<div class="keys" id="key-d">D</div>
</div>
</div>
<div class="col-md-6 col-sm-12 form-group plan-form">
<label for="plan-text">Plan (w, a, s, d, t)</label>
<label style="font-size: 15px;" for="plan-text">*Extremely Experimental*</label>
<textarea class="form-control" id="plan-text" rows="7" placeholder="1,0,0,0,2"></textarea>
<button type="button" id="plan-button" class="btn btn-light btn-lg">Execute</button>
</div>
</div>
</div>
<div class="row" style="padding: 0px 10px 0px 10px; width: 100%;">
<div class="panel row">
<div class="col-sm-3" style="text-align: center;">
<p>Play Sounds</p>
</div>
<div class="btn-group col-sm-8">
<button type="button" id="sound-engage" class="btn btn-outline-success btn-lg sound">Engage</button>
<button type="button" id="sound-disengage" class="btn btn-outline-warning btn-lg sound">Disengage</button>
<button type="button" id="sound-error" class="btn btn-outline-danger btn-lg sound">Error</button>
</div>
</div>
</div>
<div class="row" style="padding: 0px 10px 0px 10px; width: 100%;">
<div class="panel row">
-34
View File
@@ -18,37 +18,3 @@ export const handleKeyX = (key, setValue) => {
$("#pos-vals").text(x+","+y);
}
};
export async function executePlan() {
let plan = $("#plan-text").val();
const planList = [];
plan.split("\n").forEach(function(e){
let line = e.split(",").map(k=>parseInt(k));
if (line.length != 5 || line.slice(0, 4).map(e=>[1, 0].includes(e)).includes(false) || line[4] < 0 || line[4] > 10){
console.log("invalid plan");
}
else{
planList.push(line)
}
});
async function execute() {
for (var i = 0; i < planList.length; i++) {
let [w, a, s, d, t] = planList[i];
while(t > 0){
console.log(w, a, s, d, t);
if(w==1){$("#key-w").mousedown();}
if(a==1){$("#key-a").mousedown();}
if(s==1){$("#key-s").mousedown();}
if(d==1){$("#key-d").mousedown();}
await sleep(50);
$("#key-w").mouseup();
$("#key-a").mouseup();
$("#key-s").mouseup();
$("#key-d").mouseup();
t = t - 0.05;
}
}
}
execute();
}
+2 -8
View File
@@ -1,5 +1,5 @@
import { handleKeyX, executePlan } from "./controls.js";
import { start, stop, lastChannelMessageTime, playSoundRequest } from "./webrtc.js";
import { handleKeyX } from "./controls.js";
import { start, stop, lastChannelMessageTime } from "./webrtc.js";
export var pc = null;
export var dc = null;
@@ -8,12 +8,6 @@ document.addEventListener('keydown', (e)=>(handleKeyX(e.key.toLowerCase(), 1)));
document.addEventListener('keyup', (e)=>(handleKeyX(e.key.toLowerCase(), 0)));
$(".keys").bind("mousedown touchstart", (e)=>handleKeyX($(e.target).attr('id').replace('key-', ''), 1));
$(".keys").bind("mouseup touchend", (e)=>handleKeyX($(e.target).attr('id').replace('key-', ''), 0));
$("#plan-button").click(executePlan);
$(".sound").click((e)=>{
const sound = $(e.target).attr('id').replace('sound-', '')
return playSoundRequest(sound);
});
setInterval( () => {
const dt = new Date().getTime();
if ((dt - lastChannelMessageTime) > 1000) {
+4 -38
View File
@@ -15,15 +15,6 @@ export function offerRtcRequest(sdp, type) {
}
export function playSoundRequest(sound) {
return fetch('/sound', {
body: JSON.stringify({sound}),
headers: {'Content-Type': 'application/json'},
method: 'POST'
});
}
export function pingHeadRequest() {
return fetch('/', {
method: 'HEAD'
@@ -38,20 +29,18 @@ export function createPeerConnection(pc) {
pc = new RTCPeerConnection(config);
// connect audio / video
// connect video
pc.addEventListener('track', function(evt) {
console.log("Adding Tracks!")
if (evt.track.kind == 'video')
document.getElementById('video').srcObject = evt.streams[0];
else
document.getElementById('audio').srcObject = evt.streams[0];
});
return pc;
}
export function negotiate(pc) {
return pc.createOffer({offerToReceiveAudio:true, offerToReceiveVideo:true}).then(function(offer) {
return pc.createOffer({offerToReceiveVideo:true}).then(function(offer) {
return pc.setLocalDescription(offer);
}).then(function() {
return new Promise(function(resolve) {
@@ -90,14 +79,6 @@ function isMobile() {
export const constraints = {
audio: {
autoGainControl: false,
sampleRate: 48000,
sampleSize: 16,
echoCancellation: true,
noiseSuppression: true,
channelCount: 1
},
video: isMobile()
};
@@ -105,23 +86,8 @@ export const constraints = {
export function start(pc, dc) {
pc = createPeerConnection(pc);
// add audio track
navigator.mediaDevices.enumerateDevices()
.then(function(devices) {
const hasAudioInput = devices.find((device) => device.kind === "audioinput");
var modifiedConstraints = {};
modifiedConstraints.video = constraints.video;
modifiedConstraints.audio = hasAudioInput ? constraints.audio : false;
return Promise.resolve(modifiedConstraints);
})
.then(function(constraints) {
if (constraints.audio || constraints.video) {
return navigator.mediaDevices.getUserMedia(constraints);
} else{
return Promise.resolve(null);
}
})
// add a local video track on mobile
(constraints.video ? navigator.mediaDevices.getUserMedia(constraints) : Promise.resolve(null))
.then(function(stream) {
if (stream) {
stream.getTracks().forEach(function(track) {
-7
View File
@@ -172,13 +172,6 @@ video {
display: none;
}
.plan-form {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
.details {
display: flex;
padding: 0px 10px 0px 10px;
+1 -1
View File
@@ -73,7 +73,7 @@ std::vector<BrowserNode> build_browser_tree(const std::vector<std::string> &path
}
bool is_deprecated_browser_path(const std::string &path) {
return path.find("DEPRECATED") != std::string::npos;
return path.find("DEPRECATED") != std::string::npos || path.find("/deprecated/") != std::string::npos;
}
std::vector<std::string> visible_browser_paths(const RouteData &route_data, bool show_deprecated_fields) {
+3 -79
View File
@@ -6,7 +6,6 @@
#include <cmath>
#include <cstdio>
#include <limits>
#include <unordered_set>
constexpr double PLOT_Y_PAD_FRACTION = 0.4;
@@ -74,10 +73,6 @@ struct StateBlock {
std::string label;
};
struct PaneEnumContext {
std::vector<const EnumInfo *> enums;
};
struct PaneValueFormatContext {
SeriesFormat format;
bool valid = false;
@@ -106,38 +101,6 @@ bool curves_are_bool_like(const std::vector<PreparedCurve> &prepared_curves) {
return true;
}
bool curve_is_state_like(const PreparedCurve &curve) {
if (!curve.display_info.integer_like || curve.xs.size() < 2 || curve.xs.size() != curve.ys.size()) {
return false;
}
if (curve.enum_info != nullptr) {
return true;
}
std::unordered_set<int> distinct_values;
for (double value : curve.ys) {
if (!std::isfinite(value)) {
continue;
}
distinct_values.insert(static_cast<int>(std::llround(value)));
if (distinct_values.size() > 12) {
return false;
}
}
return !distinct_values.empty();
}
bool curves_use_state_blocks(const std::vector<PreparedCurve> &prepared_curves) {
if (prepared_curves.empty()) {
return false;
}
for (const PreparedCurve &curve : prepared_curves) {
if (!curve_is_state_like(curve)) {
return false;
}
}
return true;
}
ImU32 state_block_color(int value, float alpha = 1.0f) {
static constexpr std::array<std::array<uint8_t, 3>, 8> kPalette = {{
{{111, 143, 175}},
@@ -307,36 +270,6 @@ std::optional<double> app_sample_xy_value_at_time(const std::vector<double> &xs,
return y0 + (y1 - y0) * alpha;
}
int format_enum_axis_tick(double value, char *buf, int size, void *user_data) {
const auto *ctx = static_cast<const PaneEnumContext *>(user_data);
const int idx = static_cast<int>(std::llround(value));
if (ctx != nullptr && idx >= 0 && std::abs(value - static_cast<double>(idx)) < 0.01) {
std::vector<std::string_view> names;
names.reserve(ctx->enums.size());
for (const EnumInfo *info : ctx->enums) {
if (info == nullptr || static_cast<size_t>(idx) >= info->names.size()) {
continue;
}
const std::string &name = info->names[static_cast<size_t>(idx)];
if (name.empty()) continue;
if (std::find(names.begin(), names.end(), std::string_view(name)) == names.end()) {
names.emplace_back(name);
}
}
if (!names.empty()) {
std::string joined;
for (size_t i = 0; i < names.size(); ++i) {
if (i != 0) {
joined += ", ";
}
joined += names[i];
}
return std::snprintf(buf, size, "%d (%s)", idx, joined.c_str());
}
}
return std::snprintf(buf, size, "%.6g", value);
}
int format_numeric_axis_tick(double value, char *buf, int size, void *user_data) {
const auto *ctx = static_cast<const PaneValueFormatContext *>(user_data);
if (ctx == nullptr || !ctx->valid) {
@@ -831,23 +764,16 @@ void draw_plot(const AppSession &session, Pane *pane, UiState *state) {
}
const PlotBounds bounds = compute_plot_bounds(*pane, prepared_curves, *state);
PaneEnumContext enum_context;
PaneValueFormatContext pane_value_format;
const bool state_block_mode = curves_use_state_blocks(prepared_curves);
bool all_enum_curves = !prepared_curves.empty();
bool state_block_mode = !prepared_curves.empty();
size_t max_legend_label_width = 0;
for (const PreparedCurve &curve : prepared_curves) {
max_legend_label_width = std::max(max_legend_label_width, curve.label.size());
if (curve.enum_info != nullptr) {
enum_context.enums.push_back(curve.enum_info);
} else {
all_enum_curves = false;
if (curve.enum_info == nullptr) {
state_block_mode = false;
merge_pane_value_format(&pane_value_format, curve.display_info);
}
}
if (prepared_curves.empty()) {
all_enum_curves = false;
}
const int supported_count = static_cast<int>(prepared_curves.size());
const ImVec2 plot_size = ImGui::GetContentRegionAvail();
const bool has_cursor_time = state->has_tracker_time;
@@ -895,8 +821,6 @@ void draw_plot(const AppSession &session, Pane *pane, UiState *state) {
ImPlot::SetupAxisFormat(ImAxis_X1, "%.1f");
if (state_block_mode) {
ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0, 1.0, ImPlotCond_Always);
} else if (all_enum_curves && !enum_context.enums.empty()) {
ImPlot::SetupAxisFormat(ImAxis_Y1, format_enum_axis_tick, &enum_context);
} else if (pane_value_format.valid) {
ImPlot::SetupAxisFormat(ImAxis_Y1, format_numeric_axis_tick, &pane_value_format);
} else {
+2 -2
View File
@@ -1304,7 +1304,7 @@ void append_event_fast(cereal::Event::Which which,
append_can_frame(can_service,
static_cast<uint8_t>(msg.getSrc()),
msg.getAddress(),
msg.getBusTimeDEPRECATED(),
msg.getDeprecated().getBusTime(),
msg.getDat(),
tm,
series);
@@ -1316,7 +1316,7 @@ void append_event_fast(cereal::Event::Which which,
append_can_frame(can_service,
static_cast<uint8_t>(msg.getSrc()),
msg.getAddress(),
msg.getBusTimeDEPRECATED(),
msg.getDeprecated().getBusTime(),
msg.getDat(),
tm,
series);
+3
View File
@@ -383,6 +383,9 @@ function op_switch() {
git submodule update --init --recursive
git submodule foreach git reset --hard
git submodule foreach git clean -df
# remove openpilot update flag if present
rm -f .overlay_init
}
function op_start() {
+12 -2
View File
@@ -5,6 +5,7 @@ import numpy as np
import pyray as rl
from matplotlib.backends.backend_agg import FigureCanvasAgg
from matplotlib.offsetbox import AnchoredOffsetbox, HPacker, TextArea
from openpilot.common.transformations.camera import get_view_frame_from_calib_frame
from openpilot.selfdrive.controls.radard import RADAR_TO_CAMERA
@@ -94,6 +95,7 @@ def draw_path(path, color, img, calibration, top_down, lid_color=None, z_off=0):
def init_plots(arr, name_to_arr_idx, plot_xlims, plot_ylims, plot_names, plot_colors, plot_styles):
color_palette = {"r": (1, 0, 0), "g": (0, 1, 0), "b": (0, 0, 1), "k": (0, 0, 0), "y": (1, 1, 0), "p": (0, 1, 1), "m": (1, 0, 1)}
label_palette = {**color_palette, "b": (43/255, 114/255, 1.0)}
dpi = 90
fig = plt.figure(figsize=(575 / dpi, 600 / dpi), dpi=dpi)
@@ -116,10 +118,18 @@ def init_plots(arr, name_to_arr_idx, plot_xlims, plot_ylims, plot_names, plot_co
plots.append(plot)
idxs.append(name_to_arr_idx[item])
plot_select.append(i)
axs[i].set_title(", ".join(f"{nm} ({cl})" for (nm, cl) in zip(pl_list, plot_colors[i], strict=False)), fontsize=10)
# Build colored title: each label colored to match its plot line
title_texts = []
for j2, (nm, cl) in enumerate(zip(pl_list, plot_colors[i], strict=False)):
if j2 > 0:
title_texts.append(TextArea(", ", textprops=dict(color="white", fontsize=10)))
title_texts.append(TextArea(nm, textprops=dict(color=label_palette[cl], fontsize=10)))
packed = HPacker(children=title_texts, pad=0, sep=0)
ab = AnchoredOffsetbox(loc='lower center', child=packed, bbox_to_anchor=(0.5, 1.0),
bbox_transform=axs[i].transAxes, frameon=False, pad=0)
axs[i].add_artist(ab)
axs[i].tick_params(axis="x", colors="white")
axs[i].tick_params(axis="y", colors="white")
axs[i].title.set_color("white")
if i < len(plot_ylims) - 1:
axs[i].set_xticks([])
+13 -12
View File
@@ -142,18 +142,19 @@ void LogReader::migrateOldEvents() {
new_evt.setLogMonoTime(old_evt.getLogMonoTime());
auto new_state = new_evt.initSelfdriveState();
new_state.setActive(old_state.getActiveDEPRECATED());
new_state.setAlertSize(old_state.getAlertSizeDEPRECATED());
new_state.setAlertSound(old_state.getAlertSound2DEPRECATED());
new_state.setAlertStatus(old_state.getAlertStatusDEPRECATED());
new_state.setAlertText1(old_state.getAlertText1DEPRECATED());
new_state.setAlertText2(old_state.getAlertText2DEPRECATED());
new_state.setAlertType(old_state.getAlertTypeDEPRECATED());
new_state.setEnabled(old_state.getEnabledDEPRECATED());
new_state.setEngageable(old_state.getEngageableDEPRECATED());
new_state.setExperimentalMode(old_state.getExperimentalModeDEPRECATED());
new_state.setPersonality(old_state.getPersonalityDEPRECATED());
new_state.setState(old_state.getStateDEPRECATED());
auto old_dep = old_state.getDeprecated();
new_state.setActive(old_dep.getActive());
new_state.setAlertSize(old_dep.getAlertSize());
new_state.setAlertSound(old_dep.getAlertSound2());
new_state.setAlertStatus(old_dep.getAlertStatus());
new_state.setAlertText1(old_dep.getAlertText1());
new_state.setAlertText2(old_dep.getAlertText2());
new_state.setAlertType(old_dep.getAlertType());
new_state.setEnabled(old_dep.getEnabled());
new_state.setEngageable(old_dep.getEngageable());
new_state.setExperimentalMode(old_dep.getExperimentalMode());
new_state.setPersonality(old_dep.getPersonality());
new_state.setState(old_dep.getState());
// Serialize the new event to the buffer
auto buf_size = msg.getSerializedSize();
+32 -43
View File
@@ -3,7 +3,6 @@ import argparse
import os
import sys
import cv2
import numpy as np
import pyray as rl
@@ -22,7 +21,8 @@ from openpilot.tools.replay.lib.ui_helpers import (
plot_lead,
plot_model,
)
from msgq.visionipc import VisionIpcClient, VisionStreamType
from msgq.visionipc import VisionStreamType
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
os.environ['BASEDIR'] = BASEDIR
@@ -30,8 +30,6 @@ ANGLE_SCALE = 5.0
def ui_thread(addr):
cv2.setNumThreads(1)
# Get monitor info before creating window
rl.set_config_flags(rl.ConfigFlags.FLAG_MSAA_4X_HINT)
rl.init_window(1, 1, "")
@@ -59,14 +57,15 @@ def ui_thread(addr):
font_path = os.path.join(BASEDIR, "selfdrive/assets/fonts/JetBrainsMono-Medium.ttf")
font = rl.load_font_ex(font_path, 32, None, 0)
# Create textures for camera and top-down view
camera_image = rl.gen_image_color(640, 480, rl.BLACK)
camera_texture = rl.load_texture_from_image(camera_image)
rl.unload_image(camera_image)
camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD)
# Overlay texture for model/lane line drawing
overlay_img = np.zeros((480, 640, 4), dtype='uint8')
overlay_image = rl.gen_image_color(640, 480, rl.BLANK)
overlay_texture = rl.load_texture_from_image(overlay_image)
rl.unload_image(overlay_image)
# lid_overlay array is (lidar_x, lidar_y) = (384, 960)
# pygame treats first axis as width, so texture is 384 wide x 960 tall
# For raylib, we need to transpose to get (height, width) = (960, 384) for the RGBA array
top_down_image = rl.gen_image_color(UP.lidar_x, UP.lidar_y, rl.BLACK)
top_down_texture = rl.load_texture_from_image(top_down_image)
rl.unload_image(top_down_image)
@@ -89,7 +88,6 @@ def ui_thread(addr):
)
img = np.zeros((480, 640, 3), dtype='uint8')
imgff = None
num_px = 0
calibration = None
@@ -116,7 +114,7 @@ def ui_thread(addr):
plot_arr = np.zeros((100, len(name_to_arr_idx.values())))
plot_xlims = [(0, plot_arr.shape[0]), (0, plot_arr.shape[0]), (0, plot_arr.shape[0]), (0, plot_arr.shape[0])]
plot_ylims = [(-0.1, 1.1), (-ANGLE_SCALE, ANGLE_SCALE), (0.0, 75.0), (-3.0, 2.0)]
plot_ylims = [(-0.1, 1.1), (-ANGLE_SCALE, ANGLE_SCALE), (0.0, 75.0), (-3.5, 2.0)]
plot_names = [
["gas", "computer_gas", "user_brake", "computer_brake"],
["angle_steers", "angle_steers_des", "angle_steers_k", "steer_torque"],
@@ -138,20 +136,16 @@ def ui_thread(addr):
palette[110] = [110, 110, 110, 255] # car_color (gray)
palette[255] = [255, 255, 255, 255] # WHITE
vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_ROAD, True)
while not rl.window_should_close():
# ***** frame *****
if not vipc_client.is_connected():
vipc_client.connect(False)
rl.begin_drawing()
rl.clear_background(rl.Color(64, 64, 64, 255))
yuv_img_raw = vipc_client.recv()
if yuv_img_raw is None or not yuv_img_raw.data.any():
rl.draw_text_ex(font, "waiting for frames", rl.Vector2(200, 200), 30, 0, rl.WHITE)
rl.end_drawing()
continue
# Render camera (NV12->RGB on GPU via shader)
if camera_view.frame:
cam_h = 640.0 * camera_view.frame.height / camera_view.frame.width
else:
cam_h = 480.0
camera_view.render(rl.Rectangle(0, 0, 640, cam_h))
lid_overlay = lid_overlay_blank.copy()
top_down = top_down_texture, lid_overlay
@@ -159,19 +153,10 @@ def ui_thread(addr):
sm.update(0)
camera = DEVICE_CAMERAS[("tici", str(sm['roadCameraState'].sensor))]
# Use received buffer dimensions (full HEVC can have stride != buffer_len/rows due to VENUS padding)
h, w, stride = yuv_img_raw.height, yuv_img_raw.width, yuv_img_raw.stride
nv12_size = h * 3 // 2 * stride
imgff = np.frombuffer(yuv_img_raw.data, dtype=np.uint8, count=nv12_size).reshape((h * 3 // 2, stride))
num_px = w * h
rgb = cv2.cvtColor(imgff[: h * 3 // 2, : w], cv2.COLOR_YUV2RGB_NV12)
qcam = "QCAM" in os.environ
bb_scale = (528 if qcam else camera.fcam.width) / 640.0
calib_scale = camera.fcam.width / 640.0
zoom_matrix = np.asarray([[bb_scale, 0.0, 0.0], [0.0, bb_scale, 0.0], [0.0, 0.0, 1.0]])
cv2.warpAffine(rgb, zoom_matrix[:2], (img.shape[1], img.shape[0]), dst=img, flags=cv2.WARP_INVERSE_MAP)
if camera_view.frame:
num_px = camera_view.frame.width * camera_view.frame.height
intrinsic_matrix = camera.fcam.intrinsics
@@ -183,7 +168,8 @@ def ui_thread(addr):
else:
angle_steers_k = np.inf
plot_arr[:-1] = plot_arr[1:]
if sm.updated['carState']:
plot_arr[:-1] = plot_arr[1:]
plot_arr[-1, name_to_arr_idx['angle_steers']] = sm['carState'].steeringAngleDeg
plot_arr[-1, name_to_arr_idx['angle_steers_des']] = sm['carControl'].actuators.steeringAngleDeg
plot_arr[-1, name_to_arr_idx['angle_steers_k']] = angle_steers_k
@@ -198,9 +184,10 @@ def ui_thread(addr):
plot_arr[-1, name_to_arr_idx['v_cruise']] = sm['carState'].cruiseState.speed
plot_arr[-1, name_to_arr_idx['a_ego']] = sm['carState'].aEgo
if len(sm['longitudinalPlan'].accels):
plot_arr[-1, name_to_arr_idx['a_target']] = sm['longitudinalPlan'].accels[0]
plot_arr[-1, name_to_arr_idx['a_target']] = sm['longitudinalPlan'].aTarget
# Draw model overlays onto img, then blit as transparent overlay
img[:] = 0
if sm.recv_frame['modelV2']:
plot_model(sm['modelV2'], img, calibration, top_down)
@@ -214,11 +201,12 @@ def ui_thread(addr):
rpyCalib = np.asarray(sm['liveCalibration'].rpyCalib)
calibration = Calibration(num_px, rpyCalib, intrinsic_matrix, calib_scale)
# *** blits ***
# Update camera texture from numpy array
img_rgba = cv2.cvtColor(img, cv2.COLOR_RGB2RGBA)
rl.update_texture(camera_texture, rl.ffi.cast("void *", img_rgba.ctypes.data))
rl.draw_texture(camera_texture, 0, 0, rl.WHITE) # noqa: TID251
# Update overlay texture (RGB img -> RGBA with non-black pixels visible)
mask = np.any(img > 0, axis=2)
overlay_img[:, :, :3] = img
overlay_img[:, :, 3] = mask * 255
rl.update_texture(overlay_texture, rl.ffi.cast("void *", overlay_img.ctypes.data))
rl.draw_texture(overlay_texture, 0, 0, rl.WHITE) # noqa: TID251
# display alerts
rl.draw_text_ex(font, sm['selfdriveState'].alertText1, rl.Vector2(180, 150), 30, 0, rl.RED)
@@ -257,9 +245,10 @@ def ui_thread(addr):
rl.end_drawing()
rl.unload_texture(camera_texture)
rl.unload_texture(overlay_texture)
rl.unload_texture(top_down_texture)
rl.unload_font(font)
camera_view.close()
rl.close_window()
Generated
+33 -33
View File
@@ -291,41 +291,41 @@ wheels = [
[[package]]
name = "cryptography"
version = "46.0.6"
version = "46.0.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" }
sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" },
{ url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" },
{ url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" },
{ url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" },
{ url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" },
{ url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" },
{ url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" },
{ url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" },
{ url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" },
{ url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" },
{ url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" },
{ url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" },
{ url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" },
{ url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" },
{ url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" },
{ url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" },
{ url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" },
{ url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" },
{ url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" },
{ url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" },
{ url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" },
{ url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" },
{ url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" },
{ url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" },
{ url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" },
{ url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" },
{ url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" },
{ url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" },
{ url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" },
{ url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
{ url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
{ url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
{ url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" },
{ url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" },
{ url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" },
{ url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" },
{ url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" },
{ url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
{ url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
{ url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
{ url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" },
{ url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" },
{ url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" },
{ url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
{ url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
{ url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
{ url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" },
{ url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" },
{ url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" },
{ url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" },
{ url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" },
{ url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
{ url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
{ url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
{ url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" },
{ url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" },
]
[[package]]
@@ -1219,7 +1219,7 @@ wheels = [
[[package]]
name = "pytest"
version = "9.0.2"
version = "9.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -1228,9 +1228,9 @@ dependencies = [
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
]
[[package]]