mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-08 23:04:19 +08:00
Compare commits
147 Commits
accel-cont
...
only-chubb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5563618c73 | ||
|
|
881d06867d | ||
|
|
1067145641 | ||
|
|
a966e2fcfe | ||
|
|
7c9a628308 | ||
|
|
e830c1edab | ||
|
|
eb91efe2c2 | ||
|
|
028a4d10e7 | ||
|
|
edf697392c | ||
|
|
d991bc9bc4 | ||
|
|
f6a0a830ca | ||
|
|
a97aa56d3c | ||
|
|
45b9663780 | ||
|
|
0b384119ec | ||
|
|
140809a564 | ||
|
|
37172a0cbc | ||
|
|
78248cdbba | ||
|
|
f5d3fd3927 | ||
|
|
95762333d5 | ||
|
|
435284427e | ||
|
|
02078a8d0f | ||
|
|
59e4cf4188 | ||
|
|
b8205522f0 | ||
|
|
4dda8f52a4 | ||
|
|
5d4ae3c26e | ||
|
|
d62c036018 | ||
|
|
a81044868d | ||
|
|
029e4974c3 | ||
|
|
70d722c0d1 | ||
|
|
afb8000bbc | ||
|
|
f0f7ab0f35 | ||
|
|
92767345f2 | ||
|
|
f6799f686b | ||
|
|
c801918c89 | ||
|
|
6919407d2c | ||
|
|
898f782f86 | ||
|
|
68dc50546e | ||
|
|
4c57ffeca2 | ||
|
|
639c1fdf7d | ||
|
|
90ebbc6232 | ||
|
|
a6e8048ed7 | ||
|
|
bfae9de4b2 | ||
|
|
e9c8d1f8ff | ||
|
|
35e26e5a4e | ||
|
|
26abc81892 | ||
|
|
d58805156c | ||
|
|
a3af62629d | ||
|
|
b737989e64 | ||
|
|
5fbc358fd5 | ||
|
|
ce30d815f7 | ||
|
|
fdde1aa6a1 | ||
|
|
961b2a2d30 | ||
|
|
f3d39d481a | ||
|
|
6e037d80ff | ||
|
|
907bc5cf06 | ||
|
|
b3ff268f89 | ||
|
|
42e08515e6 | ||
|
|
d0ec46dc5d | ||
|
|
48a8802298 | ||
|
|
79971b9eb2 | ||
|
|
7ba21f9f1b | ||
|
|
6b4118ab27 | ||
|
|
0844424ad1 | ||
|
|
5901c9b41f | ||
|
|
d52ce19c15 | ||
|
|
05cc9a14e2 | ||
|
|
18f8956e0e | ||
|
|
0aa6f22c26 | ||
|
|
c90f262ce7 | ||
|
|
e8ee5a23f0 | ||
|
|
4a189f828a | ||
|
|
072e18faef | ||
|
|
3b1fddfde9 | ||
|
|
bddec6971e | ||
|
|
34e02b6ae5 | ||
|
|
c98cc5d40a | ||
|
|
4a0d8063e5 | ||
|
|
e2e52bcccb | ||
|
|
ccf86b7b72 | ||
|
|
483894cfc8 | ||
|
|
a678554122 | ||
|
|
bfd3eab260 | ||
|
|
f5aedbce6e | ||
|
|
4f860dd397 | ||
|
|
f308d9ab17 | ||
|
|
9226222ad4 | ||
|
|
3e317a8b4d | ||
|
|
be9f007a2e | ||
|
|
22b7849771 | ||
|
|
40f2030048 | ||
|
|
93b8395c7a | ||
|
|
0eae4e0b3b | ||
|
|
37ffa5ed21 | ||
|
|
05e3eaf2fc | ||
|
|
c8fc344d68 | ||
|
|
264948e5ff | ||
|
|
9d87beac8e | ||
|
|
2e0bc80f94 | ||
|
|
4b8781886a | ||
|
|
97edff5e5c | ||
|
|
a81570a6c2 | ||
|
|
5620e60aa1 | ||
|
|
db16bc6615 | ||
|
|
f1eafe56d7 | ||
|
|
7d4993cc42 | ||
|
|
0f6ad56fb9 | ||
|
|
f5b4f3b206 | ||
|
|
4e8060c4f8 | ||
|
|
5212203cc2 | ||
|
|
f1e359294f | ||
|
|
a9d2b9be30 | ||
|
|
718cd3f685 | ||
|
|
2d33d368f3 | ||
|
|
fb1b0655c4 | ||
|
|
01842dbdca | ||
|
|
5957db94f6 | ||
|
|
ef1810913e | ||
|
|
ed775185f2 | ||
|
|
7bbbc6588e | ||
|
|
e68c65d15d | ||
|
|
0db8722221 | ||
|
|
a33497ed19 | ||
|
|
91f2bf3459 | ||
|
|
7fad2fc189 | ||
|
|
9f303e9ea9 | ||
|
|
0613442ac9 | ||
|
|
e6f5aae246 | ||
|
|
7032e4a972 | ||
|
|
5b03369a8f | ||
|
|
1e0564b484 | ||
|
|
eb94abaa14 | ||
|
|
c270268d3a | ||
|
|
4820265268 | ||
|
|
01aa6c4204 | ||
|
|
6d6c975bfb | ||
|
|
accf09c34e | ||
|
|
3a3f7a3843 | ||
|
|
21beea51ec | ||
|
|
06c1557785 | ||
|
|
423a7d2ed0 | ||
|
|
e4e10d4b87 | ||
|
|
362e9ce04b | ||
|
|
3946e643f6 | ||
|
|
0c37a38596 | ||
|
|
9c5acf61c0 | ||
|
|
121b304fe0 | ||
|
|
47d848293b |
@@ -33,7 +33,8 @@ void zmq_to_msgq(const std::vector<std::string> &endpoints, const std::string &i
|
||||
for (auto endpoint : endpoints) {
|
||||
auto pub_sock = new MSGQPubSocket();
|
||||
auto sub_sock = new ZMQSubSocket();
|
||||
pub_sock->connect(pub_context.get(), endpoint);
|
||||
size_t queue_size = services.at(endpoint).queue_size;
|
||||
pub_sock->connect(pub_context.get(), endpoint, true, queue_size);
|
||||
sub_sock->connect(sub_context.get(), endpoint, ip, false);
|
||||
|
||||
poller->registerSocket(sub_sock);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include "cereal/services.h"
|
||||
#include "common/util.h"
|
||||
|
||||
extern ExitHandler do_exit;
|
||||
@@ -108,7 +109,8 @@ void MsgqToZmq::zmqMonitorThread() {
|
||||
if (++pair.connected_clients == 1) {
|
||||
// Create new MSGQ subscriber socket and map to ZMQ publisher
|
||||
pair.sub_sock = std::make_unique<MSGQSubSocket>();
|
||||
pair.sub_sock->connect(msgq_context.get(), pair.endpoint, "127.0.0.1");
|
||||
size_t queue_size = services.at(pair.endpoint).queue_size;
|
||||
pair.sub_sock->connect(msgq_context.get(), pair.endpoint, "127.0.0.1", false, true, queue_size);
|
||||
sub2pub[pair.sub_sock.get()] = pair.pub_sock.get();
|
||||
registerSockets();
|
||||
}
|
||||
|
||||
Submodule opendbc_repo updated: bbc6869d70...9c01b0b55e
@@ -3,7 +3,7 @@ import numpy as np
|
||||
from collections import deque
|
||||
|
||||
from cereal import log
|
||||
from opendbc.car.lateral import FRICTION_THRESHOLD, get_friction
|
||||
from opendbc.car.lateral import get_friction, get_friction_threshold
|
||||
from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
|
||||
@@ -95,7 +95,7 @@ class LatControlTorque(LatControl):
|
||||
# latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll
|
||||
ff -= self.torque_params.latAccelOffset
|
||||
# TODO jerk is weighted by lat_delay for legacy reasons, but should be made independent of it
|
||||
ff += get_friction(error, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
|
||||
ff += get_friction(error, lateral_accel_deadzone, get_friction_threshold(CS.vEgo), self.torque_params)
|
||||
|
||||
freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
|
||||
output_lataccel = self.pid.update(pid_log.error,
|
||||
|
||||
@@ -35,15 +35,14 @@ X_EGO_OBSTACLE_COST = 3.
|
||||
X_EGO_COST = 0.
|
||||
V_EGO_COST = 0.
|
||||
A_EGO_COST = 0.
|
||||
J_EGO_COST = 5.0
|
||||
A_CHANGE_COST = 200.
|
||||
J_EGO_COST = 10.0
|
||||
A_CHANGE_COST = 150.
|
||||
DANGER_ZONE_COST = 100.
|
||||
CRASH_DISTANCE = .25
|
||||
LEAD_DANGER_FACTOR = 0.75
|
||||
LIMIT_COST = 1e6
|
||||
ACADOS_SOLVER_TYPE = 'SQP_RTI'
|
||||
|
||||
|
||||
# Fewer timestamps don't hurt performance and lead to
|
||||
# much better convergence of the MPC with low iterations
|
||||
N = 12
|
||||
@@ -53,7 +52,7 @@ T_IDXS_LST = [index_function(idx, max_val=MAX_T, max_idx=N) for idx in range(N+1
|
||||
T_IDXS = np.array(T_IDXS_LST)
|
||||
FCW_IDXS = T_IDXS < 5.0
|
||||
T_DIFFS = np.diff(T_IDXS, prepend=[0.])
|
||||
COMFORT_BRAKE = 2.5
|
||||
COMFORT_BRAKE = 2.0
|
||||
STOP_DISTANCE = 6.0
|
||||
CRUISE_MIN_ACCEL = -1.2
|
||||
CRUISE_MAX_ACCEL = 1.6
|
||||
@@ -85,20 +84,12 @@ def get_stopped_equivalence_factor(v_lead):
|
||||
def get_safe_obstacle_distance(v_ego, t_follow):
|
||||
return (v_ego**2) / (2 * COMFORT_BRAKE) + t_follow * v_ego + STOP_DISTANCE
|
||||
|
||||
def desired_follow_distance(v_ego, v_lead, t_follow=None):
|
||||
if t_follow is None:
|
||||
t_follow = get_T_FOLLOW()
|
||||
return get_safe_obstacle_distance(v_ego, t_follow) - get_stopped_equivalence_factor(v_lead)
|
||||
|
||||
|
||||
def gen_long_model():
|
||||
model = AcadosModel()
|
||||
model.name = MODEL_NAME
|
||||
|
||||
# set up states & controls
|
||||
x_ego = SX.sym('x_ego')
|
||||
v_ego = SX.sym('v_ego')
|
||||
a_ego = SX.sym('a_ego')
|
||||
# states
|
||||
x_ego, v_ego, a_ego = SX.sym('x_ego'), SX.sym('v_ego'), SX.sym('a_ego')
|
||||
model.x = vertcat(x_ego, v_ego, a_ego)
|
||||
|
||||
# controls
|
||||
@@ -126,7 +117,6 @@ def gen_long_model():
|
||||
model.f_expl_expr = f_expl
|
||||
return model
|
||||
|
||||
|
||||
def gen_long_ocp():
|
||||
ocp = AcadosOcp()
|
||||
ocp.model = gen_long_model()
|
||||
@@ -222,30 +212,31 @@ def gen_long_ocp():
|
||||
|
||||
|
||||
class LongitudinalMpc:
|
||||
def __init__(self, mode='acc', dt=DT_MDL):
|
||||
self.mode = mode
|
||||
def __init__(self, dt=DT_MDL):
|
||||
self.dt = dt
|
||||
self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
|
||||
self.reset()
|
||||
self.source = SOURCES[2]
|
||||
|
||||
def reset(self):
|
||||
# self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
|
||||
self.solver.reset()
|
||||
# self.solver.options_set('print_level', 2)
|
||||
|
||||
self.x_sol = np.zeros((N+1, X_DIM))
|
||||
self.u_sol = np.zeros((N, 1))
|
||||
self.v_solution = np.zeros(N+1)
|
||||
self.a_solution = np.zeros(N+1)
|
||||
self.prev_a = np.array(self.a_solution)
|
||||
self.j_solution = np.zeros(N)
|
||||
self.prev_a = np.array(self.a_solution)
|
||||
self.yref = np.zeros((N+1, COST_DIM))
|
||||
|
||||
for i in range(N):
|
||||
self.solver.cost_set(i, "yref", self.yref[i])
|
||||
self.solver.cost_set(N, "yref", self.yref[N][:COST_E_DIM])
|
||||
self.x_sol = np.zeros((N+1, X_DIM))
|
||||
self.u_sol = np.zeros((N,1))
|
||||
|
||||
self.params = np.zeros((N+1, PARAM_DIM))
|
||||
for i in range(N+1):
|
||||
self.solver.set(i, 'x', np.zeros(X_DIM))
|
||||
|
||||
self.last_cloudlog_t = 0
|
||||
self.status = False
|
||||
self.crash_cnt = 0.0
|
||||
@@ -276,16 +267,9 @@ class LongitudinalMpc:
|
||||
|
||||
def set_weights(self, prev_accel_constraint=True, personality=log.LongitudinalPersonality.standard):
|
||||
jerk_factor = get_jerk_factor(personality)
|
||||
if self.mode == 'acc':
|
||||
a_change_cost = A_CHANGE_COST if prev_accel_constraint else 0
|
||||
cost_weights = [X_EGO_OBSTACLE_COST, X_EGO_COST, V_EGO_COST, A_EGO_COST, jerk_factor * a_change_cost, jerk_factor * J_EGO_COST]
|
||||
constraint_cost_weights = [LIMIT_COST, LIMIT_COST, LIMIT_COST, DANGER_ZONE_COST]
|
||||
elif self.mode == 'blended':
|
||||
a_change_cost = 40.0 if prev_accel_constraint else 0
|
||||
cost_weights = [0., 0.1, 0.2, 5.0, a_change_cost, 1.0]
|
||||
constraint_cost_weights = [LIMIT_COST, LIMIT_COST, LIMIT_COST, DANGER_ZONE_COST]
|
||||
else:
|
||||
raise NotImplementedError(f'Planner mode {self.mode} not recognized in planner cost set')
|
||||
a_change_cost = A_CHANGE_COST if prev_accel_constraint else 0
|
||||
cost_weights = [X_EGO_OBSTACLE_COST, X_EGO_COST, V_EGO_COST, A_EGO_COST, jerk_factor * a_change_cost, jerk_factor * J_EGO_COST]
|
||||
constraint_cost_weights = [LIMIT_COST, LIMIT_COST, LIMIT_COST, DANGER_ZONE_COST]
|
||||
self.set_cost_weights(cost_weights, constraint_cost_weights)
|
||||
|
||||
def set_cur_state(self, v, a):
|
||||
@@ -320,14 +304,14 @@ class LongitudinalMpc:
|
||||
|
||||
# MPC will not converge if immediate crash is expected
|
||||
# Clip lead distance to what is still possible to brake for
|
||||
min_x_lead = ((v_ego + v_lead)/2) * (v_ego - v_lead) / (-ACCEL_MIN * 2)
|
||||
min_x_lead = (v_ego + v_lead) * (v_ego - v_lead) / (-ACCEL_MIN * 2)
|
||||
x_lead = np.clip(x_lead, min_x_lead, 1e8)
|
||||
v_lead = np.clip(v_lead, 0.0, 1e8)
|
||||
a_lead = np.clip(a_lead, -10., 5.)
|
||||
lead_xv = self.extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau)
|
||||
return lead_xv
|
||||
|
||||
def update(self, radarstate, v_cruise, x, v, a, j, personality=log.LongitudinalPersonality.standard):
|
||||
def update(self, radarstate, v_cruise, personality=log.LongitudinalPersonality.standard):
|
||||
t_follow = get_T_FOLLOW(personality)
|
||||
v_ego = self.x0[1]
|
||||
self.status = radarstate.leadOne.status or radarstate.leadTwo.status
|
||||
@@ -341,56 +325,28 @@ class LongitudinalMpc:
|
||||
lead_0_obstacle = lead_xv_0[:,0] + get_stopped_equivalence_factor(lead_xv_0[:,1])
|
||||
lead_1_obstacle = lead_xv_1[:,0] + get_stopped_equivalence_factor(lead_xv_1[:,1])
|
||||
|
||||
self.params[:,0] = ACCEL_MIN
|
||||
self.params[:,1] = ACCEL_MAX
|
||||
# Fake an obstacle for cruise, this ensures smooth acceleration to set speed
|
||||
# when the leads are no factor.
|
||||
v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05)
|
||||
# TODO does this make sense when max_a is negative?
|
||||
v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05)
|
||||
v_cruise_clipped = np.clip(v_cruise * np.ones(N+1), v_lower, v_upper)
|
||||
cruise_obstacle = np.cumsum(T_DIFFS * v_cruise_clipped) + get_safe_obstacle_distance(v_cruise_clipped, t_follow)
|
||||
|
||||
# Update in ACC mode or ACC/e2e blend
|
||||
if self.mode == 'acc':
|
||||
self.params[:,5] = LEAD_DANGER_FACTOR
|
||||
x_obstacles = np.column_stack([lead_0_obstacle, lead_1_obstacle, cruise_obstacle])
|
||||
self.source = SOURCES[np.argmin(x_obstacles[0])]
|
||||
|
||||
# Fake an obstacle for cruise, this ensures smooth acceleration to set speed
|
||||
# when the leads are no factor.
|
||||
v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05)
|
||||
# TODO does this make sense when max_a is negative?
|
||||
v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05)
|
||||
v_cruise_clipped = np.clip(v_cruise * np.ones(N+1),
|
||||
v_lower,
|
||||
v_upper)
|
||||
cruise_obstacle = np.cumsum(T_DIFFS * v_cruise_clipped) + get_safe_obstacle_distance(v_cruise_clipped, t_follow)
|
||||
x_obstacles = np.column_stack([lead_0_obstacle, lead_1_obstacle, cruise_obstacle])
|
||||
self.source = SOURCES[np.argmin(x_obstacles[0])]
|
||||
|
||||
# These are not used in ACC mode
|
||||
x[:], v[:], a[:], j[:] = 0.0, 0.0, 0.0, 0.0
|
||||
|
||||
elif self.mode == 'blended':
|
||||
self.params[:,5] = 1.0
|
||||
|
||||
x_obstacles = np.column_stack([lead_0_obstacle,
|
||||
lead_1_obstacle])
|
||||
cruise_target = T_IDXS * np.clip(v_cruise, v_ego - 2.0, 1e3) + x[0]
|
||||
xforward = ((v[1:] + v[:-1]) / 2) * (T_IDXS[1:] - T_IDXS[:-1])
|
||||
x = np.cumsum(np.insert(xforward, 0, x[0]))
|
||||
|
||||
x_and_cruise = np.column_stack([x, cruise_target])
|
||||
x = np.min(x_and_cruise, axis=1)
|
||||
|
||||
self.source = 'e2e' if x_and_cruise[1,0] < x_and_cruise[1,1] else 'cruise'
|
||||
|
||||
else:
|
||||
raise NotImplementedError(f'Planner mode {self.mode} not recognized in planner update')
|
||||
|
||||
self.yref[:,1] = x
|
||||
self.yref[:,2] = v
|
||||
self.yref[:,3] = a
|
||||
self.yref[:,5] = j
|
||||
self.yref[:,:] = 0.0
|
||||
for i in range(N):
|
||||
self.solver.set(i, "yref", self.yref[i])
|
||||
self.solver.set(N, "yref", self.yref[N][:COST_E_DIM])
|
||||
|
||||
self.params[:,0] = ACCEL_MIN
|
||||
self.params[:,1] = ACCEL_MAX
|
||||
self.params[:,2] = np.min(x_obstacles, axis=1)
|
||||
self.params[:,3] = np.copy(self.prev_a)
|
||||
self.params[:,4] = t_follow
|
||||
self.params[:,5] = LEAD_DANGER_FACTOR
|
||||
|
||||
self.run()
|
||||
if (np.any(lead_xv_0[FCW_IDXS,0] - self.x_sol[FCW_IDXS,0] < CRASH_DISTANCE) and
|
||||
@@ -399,18 +355,7 @@ class LongitudinalMpc:
|
||||
else:
|
||||
self.crash_cnt = 0
|
||||
|
||||
# Check if it got within lead comfort range
|
||||
# TODO This should be done cleaner
|
||||
if self.mode == 'blended':
|
||||
if any((lead_0_obstacle - get_safe_obstacle_distance(self.x_sol[:,1], t_follow))- self.x_sol[:,0] < 0.0):
|
||||
self.source = 'lead0'
|
||||
if any((lead_1_obstacle - get_safe_obstacle_distance(self.x_sol[:,1], t_follow))- self.x_sol[:,0] < 0.0) and \
|
||||
(lead_1_obstacle[0] - lead_0_obstacle[0]):
|
||||
self.source = 'lead1'
|
||||
|
||||
def run(self):
|
||||
# t0 = time.monotonic()
|
||||
# reset = 0
|
||||
for i in range(N+1):
|
||||
self.solver.set(i, 'p', self.params[i])
|
||||
self.solver.constraints_set(0, "lbx", self.x0)
|
||||
@@ -422,13 +367,6 @@ class LongitudinalMpc:
|
||||
self.time_linearization = float(self.solver.get_stats('time_lin')[0])
|
||||
self.time_integrator = float(self.solver.get_stats('time_sim')[0])
|
||||
|
||||
# qp_iter = self.solver.get_stats('statistics')[-1][-1] # SQP_RTI specific
|
||||
# print(f"long_mpc timings: tot {self.solve_time:.2e}, qp {self.time_qp_solution:.2e}, lin {self.time_linearization:.2e}, \
|
||||
# integrator {self.time_integrator:.2e}, qp_iter {qp_iter}")
|
||||
# res = self.solver.get_residuals()
|
||||
# print(f"long_mpc residuals: {res[0]:.2e}, {res[1]:.2e}, {res[2]:.2e}, {res[3]:.2e}")
|
||||
# self.solver.print_statistics()
|
||||
|
||||
for i in range(N+1):
|
||||
self.x_sol[i] = self.solver.get(i, 'x')
|
||||
for i in range(N):
|
||||
@@ -446,12 +384,8 @@ class LongitudinalMpc:
|
||||
self.last_cloudlog_t = t
|
||||
cloudlog.warning(f"Long mpc reset, solution_status: {self.solution_status}")
|
||||
self.reset()
|
||||
# reset = 1
|
||||
# print(f"long_mpc timings: total internal {self.solve_time:.2e}, external: {(time.monotonic() - t0):.2e} qp {self.time_qp_solution:.2e}, \
|
||||
# lin {self.time_linearization:.2e} qp_iter {qp_iter}, reset {reset}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ocp = gen_long_ocp()
|
||||
AcadosOcpSolver.generate(ocp, json_file=JSON_FILE)
|
||||
# AcadosOcpSolver.build(ocp.code_export_directory, with_cython=True)
|
||||
|
||||
@@ -9,7 +9,7 @@ from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc, SOURCES
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_accel_from_plan
|
||||
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET
|
||||
@@ -28,14 +28,12 @@ MIN_ALLOW_THROTTLE_SPEED = 2.5
|
||||
_A_TOTAL_MAX_V = [1.7, 3.2]
|
||||
_A_TOTAL_MAX_BP = [20., 40.]
|
||||
|
||||
|
||||
def get_max_accel(v_ego):
|
||||
return np.interp(v_ego, A_CRUISE_MAX_BP, A_CRUISE_MAX_VALS)
|
||||
|
||||
def get_coast_accel(pitch):
|
||||
return np.sin(pitch) * -5.65 - 0.3 # fitted from data using xx/projects/allow_throttle/compute_coast_accel.py
|
||||
|
||||
|
||||
def limit_accel_in_turns(v_ego, angle_steers, a_target, CP):
|
||||
"""
|
||||
This function returns a limited long acceleration allowed, depending on the existing lateral acceleration
|
||||
@@ -70,7 +68,6 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
self.v_desired_trajectory = np.zeros(CONTROL_N)
|
||||
self.a_desired_trajectory = np.zeros(CONTROL_N)
|
||||
self.j_desired_trajectory = np.zeros(CONTROL_N)
|
||||
self.solverExecutionTime = 0.0
|
||||
|
||||
@staticmethod
|
||||
def parse_model(model_msg):
|
||||
@@ -123,12 +120,9 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
# No change cost when user is controlling the speed, or when standstill
|
||||
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
|
||||
|
||||
if mode == 'acc':
|
||||
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
|
||||
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
|
||||
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
|
||||
else:
|
||||
accel_clip = [ACCEL_MIN, ACCEL_MAX]
|
||||
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
|
||||
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
|
||||
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
|
||||
|
||||
if reset_state:
|
||||
self.v_desired_filter.x = v_ego
|
||||
@@ -137,7 +131,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
|
||||
# Prevent divergence, smooth in current v_ego
|
||||
self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego))
|
||||
x, v, a, j, throttle_prob = self.parse_model(sm['modelV2'])
|
||||
_, _, _, _, throttle_prob = self.parse_model(sm['modelV2'])
|
||||
# Don't clip at low speeds since throttle_prob doesn't account for creep
|
||||
self.allow_throttle = throttle_prob > ALLOW_THROTTLE_THRESHOLD or v_ego <= MIN_ALLOW_THROTTLE_SPEED
|
||||
|
||||
@@ -154,7 +148,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
|
||||
self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality)
|
||||
self.mpc.set_cur_state(self.v_desired_filter.x, self.a_desired)
|
||||
self.mpc.update(sm['radarState'], v_cruise, x, v, a, j, personality=sm['selfdriveState'].personality)
|
||||
self.mpc.update(sm['radarState'], v_cruise, personality=sm['selfdriveState'].personality)
|
||||
|
||||
self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution)
|
||||
self.a_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.a_solution)
|
||||
@@ -176,12 +170,11 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
|
||||
output_should_stop_e2e = sm['modelV2'].action.shouldStop
|
||||
|
||||
if mode == 'acc' or not self.mlsim:
|
||||
output_a_target = output_a_target_mpc
|
||||
self.output_should_stop = output_should_stop_mpc
|
||||
else:
|
||||
output_a_target = min(output_a_target_mpc, output_a_target_e2e)
|
||||
self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc
|
||||
output_a_target = min(output_a_target_mpc, output_a_target_e2e)
|
||||
self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc
|
||||
if output_a_target_e2e < output_a_target_mpc:
|
||||
self.mpc.source = SOURCES[3]
|
||||
|
||||
|
||||
for idx in range(2):
|
||||
accel_clip[idx] = np.clip(accel_clip[idx], self.prev_accel_clip[idx] - 0.05, self.prev_accel_clip[idx] + 0.05)
|
||||
|
||||
@@ -4,10 +4,15 @@ from parameterized import parameterized_class
|
||||
|
||||
from cereal import log
|
||||
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import desired_follow_distance, get_T_FOLLOW
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import get_safe_obstacle_distance, get_stopped_equivalence_factor, get_T_FOLLOW
|
||||
from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver
|
||||
|
||||
|
||||
def desired_follow_distance(v_ego, v_lead, t_follow=None):
|
||||
if t_follow is None:
|
||||
t_follow = get_T_FOLLOW()
|
||||
return get_safe_obstacle_distance(v_ego, t_follow) - get_stopped_equivalence_factor(v_lead)
|
||||
|
||||
def run_following_distance_simulation(v_lead, t_end=100.0, e2e=False, personality=0):
|
||||
man = Maneuver(
|
||||
'',
|
||||
|
||||
@@ -31,6 +31,7 @@ from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
|
||||
from openpilot.selfdrive.modeld.models.commonmodel_pyx import DrivingModelFrame, CLContext
|
||||
from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address
|
||||
|
||||
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
|
||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||
from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase
|
||||
|
||||
@@ -284,6 +285,7 @@ def main(demo=False):
|
||||
buf_main, buf_extra = None, None
|
||||
meta_main = FrameMeta()
|
||||
meta_extra = FrameMeta()
|
||||
camera_offset_helper = CameraOffsetHelper()
|
||||
|
||||
|
||||
if demo:
|
||||
@@ -339,12 +341,14 @@ def main(demo=False):
|
||||
v_ego = max(sm["carState"].vEgo, 0.)
|
||||
if sm.frame % 60 == 0:
|
||||
model.lat_delay = get_lat_delay(params, sm["liveDelay"].lateralDelay)
|
||||
camera_offset_helper.set_offset(params.get("CameraOffset", return_default=True))
|
||||
lat_delay = model.lat_delay + LAT_SMOOTH_SECONDS
|
||||
if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']:
|
||||
device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32)
|
||||
dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))]
|
||||
model_transform_main = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics, False).astype(np.float32)
|
||||
model_transform_extra = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics, True).astype(np.float32)
|
||||
model_transform_main, model_transform_extra = camera_offset_helper.update(model_transform_main, model_transform_extra, sm, main_wide_camera)
|
||||
live_calib_seen = True
|
||||
|
||||
traffic_convention = np.zeros(2)
|
||||
|
||||
@@ -68,4 +68,4 @@ if GetOption('extras'):
|
||||
obj = raylib_env.Object(f"installer/installers/installer_{name}.o", ["installer/installer.cc"], CPPDEFINES=d)
|
||||
f = raylib_env.Program(f"installer/installers/installer_{name}", [obj, cont, inter, inter_bold, inter_light], LIBS=raylib_libs)
|
||||
# keep installers small
|
||||
assert f[0].get_size() < 1900*1e3, f[0].get_size()
|
||||
assert f[0].get_size() < 19000*1e3, f[0].get_size()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
@@ -35,7 +34,7 @@ DESCRIPTIONS = {
|
||||
class DeveloperLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._params = ui_state.params
|
||||
self._is_release = self._params.get_bool("IsReleaseBranch")
|
||||
|
||||
# Build items and keep references for callbacks/state updates
|
||||
|
||||
@@ -3,7 +3,6 @@ import math
|
||||
|
||||
from cereal import messaging, log
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
@@ -35,7 +34,7 @@ class DeviceLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
self._params = ui_state.params
|
||||
self._select_language_dialog: MultiOptionDialog | None = None
|
||||
self._driver_camera: DriverCameraDialog | None = None
|
||||
self._pair_device_dialog: PairingDialog | None = None
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from cereal import log
|
||||
from openpilot.common.params import Params, UnknownKeyName
|
||||
from openpilot.common.params import UnknownKeyName
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.list_view import multiple_button_item, toggle_item
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
@@ -41,7 +41,7 @@ DESCRIPTIONS = {
|
||||
class TogglesLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._params = ui_state.params
|
||||
self._is_release = self._params.get_bool("IsReleaseBranch")
|
||||
|
||||
# param, title, desc, icon, needs_restart
|
||||
@@ -198,11 +198,6 @@ class TogglesLayout(Widget):
|
||||
|
||||
self._update_experimental_mode_icon()
|
||||
|
||||
# TODO: make a param control list item so we don't need to manage internal state as much here
|
||||
# refresh toggles from params to mirror external changes
|
||||
for param in self._toggle_defs:
|
||||
self._toggles[param].action_item.set_state(self._params.get_bool(param))
|
||||
|
||||
# these toggles need restart, block while engaged
|
||||
for toggle_def in self._toggle_defs:
|
||||
if self._toggle_defs[toggle_def][3] and toggle_def not in self._locked_toggles:
|
||||
|
||||
214
selfdrive/ui/mici/layouts/settings/models.py
Normal file
214
selfdrive/ui/mici/layouts/settings/models.py
Normal file
@@ -0,0 +1,214 @@
|
||||
"""
|
||||
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 time
|
||||
from collections.abc import Callable
|
||||
|
||||
from cereal import custom
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialogV2
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import NavWidget, Widget
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
|
||||
|
||||
class ModelsLayoutMici(NavWidget):
|
||||
def __init__(self, back_callback: Callable):
|
||||
super().__init__()
|
||||
self.set_back_callback(back_callback)
|
||||
self.original_back_callback = back_callback
|
||||
self.refresh_start_time = 0
|
||||
self.focused_widget = None
|
||||
|
||||
self.current_model_btn = BigButton(tr("current model"), "", "")
|
||||
self.current_model_btn.set_click_callback(self._show_folders)
|
||||
|
||||
self.refresh_btn = BigButton(tr("refresh model list"), "", "")
|
||||
self.refresh_btn.set_click_callback(self._handle_refresh)
|
||||
|
||||
self.clear_cache_btn = BigButton(tr("clear model cache"), "", "")
|
||||
self.clear_cache_btn.set_click_callback(self._handle_clear_cache)
|
||||
|
||||
self.cancel_download_btn = BigButton(tr("cancel download"), "", "")
|
||||
self.cancel_download_btn.set_click_callback(lambda: ui_state.params.remove("ModelManager_DownloadIndex"))
|
||||
|
||||
self.lane_turn_toggle = BigToggle(text=tr("use lane turn desires"), initial_state=ui_state.params.get_bool("LaneTurnDesire"),
|
||||
toggle_callback=lambda state: ui_state.params.put_bool("LaneTurnDesire", state))
|
||||
|
||||
self.lagd_toggle = BigToggle(text=tr("live learning steer delay"), initial_state=ui_state.params.get_bool("LagdToggle"),
|
||||
toggle_callback=lambda state: ui_state.params.put_bool("LagdToggle", state))
|
||||
|
||||
self.lane_turn_value_btn = BigButton(tr("adjust lane turn speed"), "", "")
|
||||
self.lane_turn_value_btn.set_click_callback(self._adjust_lane_turn)
|
||||
self.delay_btn = BigButton(tr("adjust software delay"), "", "")
|
||||
self.delay_btn.set_click_callback(self._adjust_delay)
|
||||
|
||||
self.main_items: list[Widget] = [self.current_model_btn, self.cancel_download_btn, self.refresh_btn, self.clear_cache_btn, self.lane_turn_toggle,
|
||||
self.lane_turn_value_btn, self.lagd_toggle, self.delay_btn]
|
||||
self._scroller = Scroller(self.main_items, snap_items=False)
|
||||
|
||||
@property
|
||||
def model_manager(self):
|
||||
return ui_state.sm["modelManagerSP"]
|
||||
|
||||
def _get_grouped_bundles(self):
|
||||
bundles = self.model_manager.availableBundles
|
||||
folders = {}
|
||||
for bundle in bundles:
|
||||
folder = next((override.value for override in bundle.overrides if override.key == "folder"), "")
|
||||
folders.setdefault(folder, []).append(bundle)
|
||||
return folders
|
||||
|
||||
def _show_selection_view(self, items: list[Widget], back_callback: Callable):
|
||||
self._scroller._items = items
|
||||
for item in items:
|
||||
item.set_touch_valid_callback(lambda: self._scroller.scroll_panel.is_touch_valid() and self._scroller.enabled)
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
def _show_folders(self):
|
||||
self.focused_widget = self.current_model_btn
|
||||
folders = self._get_grouped_bundles()
|
||||
folder_buttons = []
|
||||
default_btn = BigButton(tr("default model"), "", "")
|
||||
default_btn.set_click_callback(self._select_default)
|
||||
folder_buttons.append(default_btn)
|
||||
|
||||
for folder in sorted(folders.keys(), key=lambda f: max((bundle.index for bundle in folders[f]), default=-1), reverse=True):
|
||||
if folder:
|
||||
btn = BigButton(folder.lower(), "", "")
|
||||
btn.set_click_callback(lambda f=folder: self._select_folder(f))
|
||||
folder_buttons.append(btn)
|
||||
self._show_selection_view(folder_buttons, self._reset_main_view)
|
||||
|
||||
def _handle_refresh(self):
|
||||
self.refresh_btn.set_text(tr("refreshing..."))
|
||||
self.refresh_start_time = time.monotonic()
|
||||
ui_state.params.put("ModelManager_LastSyncTime", 0)
|
||||
|
||||
def _handle_clear_cache(self):
|
||||
gui_app.set_modal_overlay(BigConfirmationDialogV2(tr("clear model cache?"), "icons_mici/settings/device/update.png",
|
||||
confirm_callback=lambda: ui_state.params.put_bool("ModelManager_ClearCache", True)))
|
||||
|
||||
def _select_model(self, bundle):
|
||||
ui_state.params.put("ModelManager_DownloadIndex", bundle.index)
|
||||
self._reset_main_view()
|
||||
|
||||
def _select_default(self):
|
||||
ui_state.params.remove("ModelManager_ActiveBundle")
|
||||
self._reset_main_view()
|
||||
|
||||
def _select_folder(self, folder_name):
|
||||
folders = self._get_grouped_bundles()
|
||||
bundles = sorted(folders.get(folder_name, []), key=lambda b: b.index, reverse=True)
|
||||
|
||||
btns = []
|
||||
for bundle in bundles:
|
||||
txt = bundle.displayName.lower()
|
||||
if self.model_manager.activeBundle and self.model_manager.activeBundle.index == bundle.index:
|
||||
txt += " (active)"
|
||||
elif bundle.status in (custom.ModelManagerSP.DownloadStatus.downloaded, custom.ModelManagerSP.DownloadStatus.cached):
|
||||
txt += " (cached)"
|
||||
|
||||
btn = BigButton(txt, "", "")
|
||||
btn.set_click_callback(lambda b=bundle: self._select_model(b))
|
||||
btns.append(btn)
|
||||
self._show_selection_view(btns, self._show_folders)
|
||||
|
||||
def _reset_main_view(self):
|
||||
self._scroller._items = self.main_items
|
||||
self.set_back_callback(self.original_back_callback)
|
||||
if self.focused_widget and self.focused_widget in self.main_items:
|
||||
x = self._scroller._pad_start
|
||||
for item in self.main_items:
|
||||
if not item.is_visible:
|
||||
continue
|
||||
if item == self.focused_widget:
|
||||
break
|
||||
x += item.rect.width + self._scroller._spacing
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
self._scroller.scroll_to(x)
|
||||
self.focused_widget = None
|
||||
else:
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
|
||||
def _create_buttons(self, values, current_val, label, callback):
|
||||
buttons = []
|
||||
for value in values:
|
||||
suffix = " (current)" if value == current_val else ""
|
||||
btn = BigButton(f"{label(value)}{suffix}", "", "")
|
||||
btn.set_click_callback(lambda v=value: callback(v))
|
||||
buttons.append(btn)
|
||||
return buttons
|
||||
|
||||
def _adjust_lane_turn(self):
|
||||
self.focused_widget = self.lane_turn_value_btn
|
||||
lane_turn_value = float(ui_state.params.get("LaneTurnValue", return_default=True))
|
||||
is_metric = ui_state.is_metric
|
||||
cur = int(round(lane_turn_value * CV.MPH_TO_KPH)) if is_metric else int(round(lane_turn_value))
|
||||
values = [8, 16, 24, 32] if is_metric else [5, 10, 15, 20]
|
||||
|
||||
btns = self._create_buttons(values, cur, lambda v: f"{v} {'km/h' if is_metric else 'mph'}", self._set_lane_turn)
|
||||
self._show_selection_view(btns, self._reset_main_view)
|
||||
|
||||
def _set_lane_turn(self, value):
|
||||
val = value / CV.MPH_TO_KPH if ui_state.is_metric else float(value)
|
||||
ui_state.params.put("LaneTurnValue", val)
|
||||
self._reset_main_view()
|
||||
|
||||
def _adjust_delay(self):
|
||||
self.focused_widget = self.delay_btn
|
||||
current_delay = float(ui_state.params.get("LagdToggleDelay", return_default=True))
|
||||
values = [round(i * 0.01, 2) for i in range(10, 31)]
|
||||
btns = self._create_buttons(values, current_delay, lambda v: f"{v:.2f}s", self._set_delay)
|
||||
self._show_selection_view(btns, self._reset_main_view)
|
||||
|
||||
def _set_delay(self, value):
|
||||
ui_state.params.put("LagdToggleDelay", value)
|
||||
self._reset_main_view()
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
if self.refresh_start_time > 0 and time.monotonic() - self.refresh_start_time > 1:
|
||||
self.refresh_btn.set_text(tr("refresh model list"))
|
||||
self.refresh_start_time = 0
|
||||
|
||||
manager = self.model_manager
|
||||
if manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading:
|
||||
self.current_model_btn.set_value(f"downloading {manager.selectedBundle.displayName.lower()}")
|
||||
self.cancel_download_btn.set_visible(True)
|
||||
else:
|
||||
self.current_model_btn.set_value(manager.activeBundle.internalName.lower() if manager.activeBundle else tr("default model"))
|
||||
self.cancel_download_btn.set_visible(False)
|
||||
self.current_model_btn.set_enabled(ui_state.is_offroad())
|
||||
self.current_model_btn.set_text(tr("current model"))
|
||||
|
||||
advanced_controls = ui_state.params.get_bool("ShowAdvancedControls")
|
||||
turn_desires = ui_state.params.get_bool("LaneTurnDesire")
|
||||
lagd_delay = ui_state.params.get_bool("LagdToggle")
|
||||
|
||||
self.lane_turn_value_btn.set_visible(turn_desires and advanced_controls)
|
||||
if turn_desires and advanced_controls:
|
||||
lane_turn_value = float(ui_state.params.get("LaneTurnValue", return_default=True))
|
||||
val = int(round(lane_turn_value * CV.MPH_TO_KPH)) if ui_state.is_metric else int(round(lane_turn_value))
|
||||
self.lane_turn_value_btn.set_text(tr("adjust lane turn speed"))
|
||||
self.lane_turn_value_btn.set_value(f"{val} {'km/h' if ui_state.is_metric else 'mph'}")
|
||||
|
||||
self.delay_btn.set_visible(not lagd_delay and advanced_controls)
|
||||
if not lagd_delay and advanced_controls:
|
||||
toggle_delay = float(ui_state.params.get("LagdToggleDelay", return_default=True))
|
||||
self.delay_btn.set_text(tr("adjust software delay"))
|
||||
self.delay_btn.set_value(f"{toggle_delay:.2f}s")
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._scroller.show_event()
|
||||
@@ -226,9 +226,7 @@ class ModelsLayout(Widget):
|
||||
turn_desire: bool = ui_state.params.get_bool("LaneTurnDesire")
|
||||
live_delay: bool = ui_state.params.get_bool("LagdToggle")
|
||||
|
||||
self.lane_turn_desire_toggle.action_item.set_state(turn_desire)
|
||||
self.lane_turn_value_control.set_visible(turn_desire and advanced_controls)
|
||||
self.lagd_toggle.action_item.set_state(live_delay)
|
||||
self.delay_control.set_visible(not live_delay and advanced_controls)
|
||||
new_step = int(round(100 / CV.MPH_TO_KPH)) if ui_state.is_metric else 100
|
||||
if self.lane_turn_value_control.action_item.value_change_step != new_step:
|
||||
|
||||
@@ -336,7 +336,6 @@ class SunnylinkLayout(Widget):
|
||||
self._sunnylink_enabled = ui_state.params.get_bool("SunnylinkEnabled")
|
||||
self._sunnylink_toggle.set_right_value(tr("Dongle ID") + ": " + self._get_sunnylink_dongle_id())
|
||||
self._sunnylink_toggle.action_item.set_enabled(not ui_state.is_onroad())
|
||||
self._sunnylink_toggle.action_item.set_state(self._sunnylink_enabled)
|
||||
self._sunnylink_uploader_toggle.action_item.set_enabled(self._sunnylink_enabled)
|
||||
self.handle_backup_restore_progress()
|
||||
|
||||
|
||||
@@ -55,5 +55,4 @@ class HyundaiSettings(BrandSettings):
|
||||
self.longitudinal_tuning_item.action_item.set_enabled(not longitudinal_tuning_disabled)
|
||||
self.longitudinal_tuning_item.set_description(long_tuning_desc)
|
||||
self.longitudinal_tuning_item.show_description(True)
|
||||
self.longitudinal_tuning_item.action_item.set_selected_button(tuning_param)
|
||||
self.longitudinal_tuning_item.set_visible(self.alpha_long_available)
|
||||
|
||||
@@ -9,6 +9,7 @@ from enum import IntEnum
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings import settings as OP
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.sunnylink import SunnylinkLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.models import ModelsLayoutMici
|
||||
|
||||
ICON_SIZE = 70
|
||||
|
||||
@@ -16,6 +17,7 @@ OP.PanelType = IntEnum(
|
||||
"PanelType",
|
||||
[es.name for es in OP.PanelType] + [
|
||||
"SUNNYLINK",
|
||||
"MODELS",
|
||||
],
|
||||
start=0,
|
||||
)
|
||||
@@ -27,13 +29,17 @@ class SettingsLayoutSP(OP.SettingsLayout):
|
||||
|
||||
sunnylink_btn = BigButton("sunnylink", "", "icons_mici/settings/developer/ssh.png")
|
||||
sunnylink_btn.set_click_callback(lambda: self._set_current_panel(OP.PanelType.SUNNYLINK))
|
||||
models_btn = BigButton("models", "", "../../sunnypilot/selfdrive/assets/offroad/icon_models.png")
|
||||
models_btn.set_click_callback(lambda: self._set_current_panel(OP.PanelType.MODELS))
|
||||
self._panels.update({
|
||||
OP.PanelType.SUNNYLINK: OP.PanelInfo("sunnylink", SunnylinkLayoutMici(back_callback=lambda: self._set_current_panel(None))),
|
||||
OP.PanelType.MODELS: OP.PanelInfo("models", ModelsLayoutMici(back_callback=lambda: self._set_current_panel(None))),
|
||||
})
|
||||
|
||||
items = self._scroller._items.copy()
|
||||
|
||||
items.insert(1, sunnylink_btn)
|
||||
items.insert(2, models_btn)
|
||||
self._scroller._items.clear()
|
||||
for item in items:
|
||||
self._scroller.add_widget(item)
|
||||
|
||||
41
selfdrive/ui/sunnypilot/ui_helpers.py
Normal file
41
selfdrive/ui/sunnypilot/ui_helpers.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
def update_item_from_param(item, key, params):
|
||||
if not (action := getattr(item, 'action_item', None)):
|
||||
return
|
||||
|
||||
if hasattr(action, 'set_state'):
|
||||
action.set_state(params.get_bool(key))
|
||||
elif hasattr(action, 'set_value'):
|
||||
action.set_value(params.get(key, return_default=True))
|
||||
else:
|
||||
try:
|
||||
val = int(params.get(key, return_default=True))
|
||||
if hasattr(action, 'selected_button'):
|
||||
action.selected_button = val
|
||||
if hasattr(action, 'current_value'):
|
||||
action.current_value = val
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
|
||||
def sync_layout_params(layout, param_name, params):
|
||||
targets = []
|
||||
if toggles := getattr(layout, '_toggles', None):
|
||||
targets.extend([(item, k) for k, item in toggles.items()])
|
||||
|
||||
items = getattr(layout, 'items', []) or getattr(getattr(layout, '_scroller', None), '_items', [])
|
||||
for item in items:
|
||||
action = getattr(item, 'action_item', None)
|
||||
if key := getattr(action, 'param_key', None) or getattr(getattr(action, 'toggle', None), 'param_key', None):
|
||||
targets.append((item, key))
|
||||
|
||||
for item, key in targets:
|
||||
if param_name is None or key == param_name:
|
||||
update_item_from_param(item, key, params)
|
||||
@@ -7,10 +7,13 @@ See the LICENSE.md file in the root directory for more details.
|
||||
from enum import Enum
|
||||
|
||||
from cereal import messaging, log, custom
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
from openpilot.sunnypilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import OnroadBrightness
|
||||
from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.selfdrive.ui.sunnypilot.ui_helpers import sync_layout_params
|
||||
|
||||
OpenpilotState = log.SelfdriveState.OpenpilotState
|
||||
MADSState = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState
|
||||
@@ -27,6 +30,8 @@ class OnroadTimerStatus(Enum):
|
||||
class UIStateSP:
|
||||
def __init__(self):
|
||||
self.params = Params()
|
||||
self.params.add_watcher(self.on_param_change)
|
||||
self.params.start()
|
||||
self.sm_services_ext = [
|
||||
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
|
||||
"gpsLocation", "liveTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "liveDelay"
|
||||
@@ -34,6 +39,16 @@ class UIStateSP:
|
||||
|
||||
self.sunnylink_state = SunnylinkState()
|
||||
self.update_params()
|
||||
self.active_layout = None
|
||||
self.changed_params = set()
|
||||
|
||||
def set_active_layout(self, layout):
|
||||
self.active_layout = layout
|
||||
if layout:
|
||||
sync_layout_params(layout, None, self.params)
|
||||
|
||||
def on_param_change(self, param_name):
|
||||
self.changed_params.add(param_name)
|
||||
|
||||
self.onroad_brightness_timer: int = 0
|
||||
self.custom_interactive_timeout: int = self.params.get("InteractivityTimeout", return_default=True)
|
||||
@@ -79,6 +94,18 @@ class UIStateSP:
|
||||
def auto_onroad_brightness(self) -> bool:
|
||||
return self.onroad_brightness in (OnroadBrightness.AUTO, OnroadBrightness.AUTO_DARK)
|
||||
|
||||
if not self.params.is_watching():
|
||||
cloudlog.warning("ParamWatcher thread died, restarting...")
|
||||
self.params.start()
|
||||
|
||||
if self.changed_params:
|
||||
while self.changed_params:
|
||||
self.changed_params.pop()
|
||||
|
||||
if self.active_layout:
|
||||
sync_layout_params(self.active_layout, None, self.params)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def update_status(ss, ss_sp, onroad_evt) -> str:
|
||||
state = ss.state
|
||||
|
||||
@@ -6,7 +6,6 @@ from collections.abc import Callable
|
||||
from enum import Enum
|
||||
from cereal import messaging, car, log
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.ui.lib.prime_state import PrimeState
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
@@ -36,7 +35,6 @@ class UIState(UIStateSP):
|
||||
|
||||
def _initialize(self):
|
||||
UIStateSP.__init__(self)
|
||||
self.params = Params()
|
||||
self.sm = messaging.SubMaster(
|
||||
[
|
||||
"modelV2",
|
||||
|
||||
146
sunnypilot/common/README.md
Normal file
146
sunnypilot/common/README.md
Normal file
@@ -0,0 +1,146 @@
|
||||
<center>
|
||||
|
||||
# Comparative Analysis of Parameter Getter Methods: Params vs. ParamWatcher
|
||||
|
||||
James (sunnypilot Developer) <br>
|
||||
December 13, 2025
|
||||
|
||||
</center>
|
||||
|
||||
|
||||
## <br><br> Abstract
|
||||
|
||||
This research report examines the inefficiencies in standard parameter access methods within sunnypilot and proposes an optimized alternative, ParamWatcher. The standard `Params::get()` method incurs significant CPU and memory overhead due to repeated file I/O operations (sunnypilot, 2025). ParamWatcher utilizes OS-level file system events (inotify on Linux, FSEvents on macOS) to maintain an in-memory cache, reducing I/O to near zero (Linux man-pages, 2025-c; Apple Inc., n.d.-a). Empirical benchmarks with ~10 million parameter accesses demonstrate a 14.5x CPU speedup and flat memory usage (514.7 KB vs. 497.7 KB for base Params, with only 17 KB overhead). The implementation employs a process-local singleton pattern for efficiency in multi-process architectures (Gamma et al., 1994). Results indicate ParamWatcher eliminates UI stutters and GC pauses, enhancing system responsiveness without compromising data freshness.
|
||||
|
||||
**Keywords:** parameter access, file I/O optimization, event-driven caching, autonomous driving systems, performance benchmarking
|
||||
|
||||
## Introduction
|
||||
|
||||
In sunnypilot, efficient parameter management is important for real-time system access. The standard `Params::get()` method, implemented in C++ and wrapped in Python, performs full file I/O cycles for each access, leading to high CPU overhead and memory churn (sunnypilot, 2025). This is particularly problematic in UI loops where parameters are queried frequently (e.g., 50 toggles at 20 FPS equates to ~1,000 reads/second).
|
||||
|
||||
This inefficiency stems from architectural mismatches: C++ streams are designed for throughput, not latency (cppreference.com, n.d.-a). Each call triggers kernel mode switches, heap allocations, and garbage collection in Python, causing UI stutters (Linux man-pages, 2025-a; Linux man-pages, 2025-b).
|
||||
|
||||
### Inefficiencies in Standard Parameter Access
|
||||
The standard `Params::get()` method executes a full file I/O lifecycle—opening, allocating, reading, and closing—for every function call. This results in significant CPU overhead and memory churn due to the frequency of these operations in the user interface loop.
|
||||
|
||||
#### System Overhead Analysis
|
||||
- **System Call Overhead**: Every read operation requires context switches into kernel mode. The `Params::get` function calls `util::read_file` (sunnypilot, 2025), which subsequently invokes `std::ifstream` (sunnypilot, 2025).
|
||||
- **Impact**: Frequent context switching degrades performance (Linux man-pages, 2025-a; Linux man-pages, 2025-b).
|
||||
- **C++ Stream Overhead**: The use of `std::ifstream` introduces additional overhead for maintaining stream state and buffering compared to raw file descriptors (cppreference.com, n.d.-a; Codezup, 2025).
|
||||
- **Memory Churn**: The instantiation of `std::string result(size, '\0');` forces heap allocation and deallocation during every call (sunnypilot, 2025). This stresses the memory allocator and can lead to fragmentation (cppreference.com, n.d.-b).
|
||||
|
||||
This report introduces ParamWatcher, an event-driven caching solution using OS file system events. It shifts from polling to notifications, caching converted values in static RAM. I propose that ParamWatcher achieves minimum 10x+ CPU gains with bounded non increasing memory, improving sunnypilot's performance both on latency and responsiveness.
|
||||
|
||||
## Method
|
||||
|
||||
### Materials
|
||||
- **System:** sunnypilot, running on macOS, Ubuntu/Linux, comma 3x, and comma four.
|
||||
- **Parameters:** 231 defined keys in `param_keys.h`.
|
||||
- **Tools:** Python 3, tracemalloc for memory profiling, time.perf_counter for CPU timing, ctypes for OS integration (Python Software Foundation, 2025).
|
||||
|
||||
### Implementation Details
|
||||
ParamWatcher provides cross-platform file system monitoring using ctypes for direct OS integration (Python Software Foundation, 2025).
|
||||
|
||||
#### Linux Implementation
|
||||
On Linux, ParamWatcher uses the inotify subsystem for efficient file change detection (Linux man-pages, 2025-c). It loads `libc.so.6` to access system calls, initializes an inotify instance, and watches the parameters directory for events like `IN_MODIFY` and `IN_CLOSE_WRITE` (Linux Kernel Organization, 2005). Events are polled with `select.epoll()` and parsed using `struct.unpack_from()` to avoid ctypes overhead. Filenames are extracted and passed to cache invalidation, ensuring real-time updates without polling (Codezup, 2025).
|
||||
|
||||
- **Library Loading**: `libc = ctypes.CDLL('libc.so.6')` loads the standard C library to access system calls.
|
||||
- **Initialization**: `inotify_init()` is called to create a new inotify instance, returning a file descriptor.
|
||||
- **Watch Setup**: `inotify_add_watch(fd, path, mask)` registers the parameters directory. The mask includes `IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVED_TO | IN_CLOSE_WRITE` (Linux Kernel Organization, 2005) to capture all relevant file changes.
|
||||
- **Event Loop**:
|
||||
- **Polling**: `select.epoll()` is used to efficiently wait for activity on the file descriptor without busy-waiting.
|
||||
- **Reading**: When events occur, `os.read(fd, 1024)` retrieves the raw binary event data.
|
||||
- **Parsing**: The code uses Python's `struct` module (`struct.unpack_from("iIII", ...)`) to parse the C-style `inotify_event` structures directly from the buffer, avoiding the overhead of defining `ctypes` structures.
|
||||
- **Handling**: Extracted filenames are passed to `_trigger_callbacks`, which invalidates the specific cache entry (`self._cache.pop(path, None)`), forcing a fresh read on the next access.
|
||||
|
||||
#### macOS Implementation
|
||||
On macOS, ParamWatcher leverages FSEvents from CoreServices for directory monitoring (Apple Inc., n.d.-a). It defines a C-compatible callback using `CFUNCTYPE`, creates an `FSEventStream` with `kFSEventStreamCreateFlagFileEvents`, and schedules it on the run loop (Apple Inc., n.d.-b). Events are filtered for modifications, creations, and renames (Apple Inc., n.d.-c), triggering cache invalidation for affected parameters.
|
||||
|
||||
- **Framework Loading**: `ctypes.cdll.LoadLibrary` loads `CoreServices` and `CoreFoundation`.
|
||||
- **Callback Definition**: `CFUNCTYPE` is used to define a C-compatible callback function. This function is invoked by the OS whenever a change occurs in the watched directory.
|
||||
- **Stream Creation**: `FSEventStreamCreate` creates a stream for the target directory. The `kFSEventStreamCreateFlagFileEvents` flag is used to request file-level granularity where available.
|
||||
- **Event Filtering**: The callback filters events using flags such as `kFSEventStreamEventFlagItemCreated` and `kFSEventStreamEventFlagItemModified` to ensure only relevant file changes trigger updates (Apple Inc., n.d.-c).
|
||||
- **Scheduling**: `FSEventStreamScheduleWithRunLoop` attaches the stream to the current thread's run loop (Apple Inc., n.d.-b).
|
||||
- **Execution**: `CFRunLoopRun()` starts the event loop. This passes control to the OS, which wakes the thread only when necessary.
|
||||
- **Handling**: Inside the callback, the code iterates through the changed paths provided by the OS. It extracts the filename and calls `_trigger_callbacks` to invalidate the cache for that specific parameter.
|
||||
|
||||
### Procedure
|
||||
Benchmarks simulated heavy load:
|
||||
- **Memory Test:** ~10 million gets (43,290 loops over 231 keys), measured with tracemalloc.
|
||||
- **CPU Test:** Same load, timed with perf_counter.
|
||||
- Comparisons: Base Params vs. ParamWatcher.
|
||||
|
||||
## Results
|
||||
|
||||
### Memory Usage
|
||||
Using tracemalloc for peak memory measurement during ~10 million parameter accesses (43,290 loops over 231 keys), base Params peaked at 497.7 KB, while ParamWatcher peaked at 514.7 KB (17 KB overhead). ParamWatcher's memory remained flat post-initialization, preventing churn.
|
||||
|
||||
| Condition | Memory (KB) | Overhead |
|
||||
|-----------|-------------|----------|
|
||||
| Base Params | 497.7 | - |
|
||||
| ParamWatcher | 514.7 | 17 KB |
|
||||
### CPU Performance
|
||||
ParamWatcher was 14.5x faster: 4.52s vs. 65.43s to complete ~10 million param gets.
|
||||
|
||||
| Condition | Time (s) | Speedup |
|
||||
|-----------|----------|---------|
|
||||
| Base Params | 65.43 | 1x |
|
||||
| ParamWatcher | 4.52 | 14.5x |
|
||||
|
||||
### Scalability
|
||||
No degradation at scale; cache invalidation maintained freshness.
|
||||
|
||||
See Appendix A for visual graphs of memory usage over a 30-minute time span, captured on comma four. These two routes are of equal conditions: each route started completely unplugged to two minutes offroad, followed by onroad state, with ambient temperature at 75 degrees fahrenheit. These routes are direct comparisons of pre-ParamWatcher and ParamWatcher implementations. Appendix A also includes I/O capture graphs for those 30-minute routes, demonstrating reductions in file system activity post-ParamWatcher.
|
||||
|
||||
## Discussion
|
||||
|
||||
ParamWatcher successfully optimizes parameter access, delivering substantial CPU gains with minimal memory overhead. The event-driven approach eliminates I/O bottlenecks, reducing GC pressure and UI stutters (cppreference.com, n.d.-b). The 17 KB memory overhead is negligible compared to the megabytes of churn from base Params, ensuring bounded usage in multi-process environments via the singleton pattern (Gamma et al., 1994).
|
||||
|
||||
Results demonstrate scalability without degradation, with cache invalidation maintaining data freshness. This optimization enhances system responsiveness.
|
||||
Limitations include potential event latency in high-load scenarios (<10 ms, imperceptible for UI) and increased complexity from background threads.
|
||||
Trade-offs: Static RAM (~17 KB) vs. dynamic churn; benefits outweigh costs for param-heavy workloads.
|
||||
|
||||
## <br> Appendix A: Memory Usage Graphs
|
||||
|
||||
### Base Params Memory Usage
|
||||

|
||||
|
||||
### ParamWatcher Memory Usage
|
||||

|
||||
|
||||
### Base Params IO Usage
|
||||

|
||||
|
||||
### ParamWatcher IO Usage
|
||||

|
||||
|
||||
|
||||
## <br>References
|
||||
|
||||
Apple Inc. (n.d.-a). *File System Events*. Retrieved from https://developer.apple.com/documentation/coreservices/file_system_events
|
||||
|
||||
Apple Inc. (n.d.-b). *CFRunLoop*. Retrieved from https://developer.apple.com/documentation/corefoundation/cfrunloop
|
||||
|
||||
Apple Inc. (n.d.-c). *FSEventStreamEventFlags*. Retrieved from https://developer.apple.com/documentation/coreservices/1455361-fseventstreameventflags
|
||||
|
||||
Codezup. (2025). *Efficient File I/O in C++*. Retrieved from https://codezup.com/efficient-file-io-cpp-best-practices/
|
||||
|
||||
cppreference.com. (n.d.-a). *std::basic_ifstream*. Retrieved from https://en.cppreference.com/w/cpp/io/basic_ifstream
|
||||
|
||||
cppreference.com. (n.d.-b). *std::basic_string*. Retrieved from https://en.cppreference.com/w/cpp/string/basic_string/basic_string
|
||||
|
||||
Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). *Design Patterns: Elements of Reusable Object-Oriented Software*. Addison-Wesley.
|
||||
|
||||
Linux Kernel Organization. (2005). *include/uapi/linux/inotify.h*. Retrieved from https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/inotify.h
|
||||
|
||||
Linux man-pages. (2025-a). *open(2)*. Retrieved from https://man7.org/linux/man-pages/man2/open.2.html
|
||||
|
||||
Linux man-pages. (2025-b). *read(2)*. Retrieved from https://man7.org/linux/man-pages/man2/read.2.html
|
||||
|
||||
Linux man-pages. (2025-c). *inotify(7)*. Retrieved from https://man7.org/linux/man-pages/man7/inotify.7.html
|
||||
|
||||
Python Software Foundation. (2025). *ctypes — A foreign function library for Python*. Retrieved from https://docs.python.org/3/library/ctypes.html
|
||||
|
||||
sunnypilot. (2025). *common/params.cc* [Source code]. GitHub. https://github.com/sunnypilot/sunnypilot/blob/master/common/params.cc#L180C1-L206C2
|
||||
|
||||
sunnypilot. (2025). *common/util.cc* [Source code]. GitHub. https://github.com/sunnypilot/sunnypilot/blob/master/common/util.cc#L79C1-L117C2
|
||||
0
sunnypilot/common/__init__.py
Normal file
0
sunnypilot/common/__init__.py
Normal file
3
sunnypilot/common/assets/io_usage_param_watcher.png
Normal file
3
sunnypilot/common/assets/io_usage_param_watcher.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b94f93515223b41fba5250ffb7c795ebb1640c524de5296393b02bd36a202a18
|
||||
size 566837
|
||||
3
sunnypilot/common/assets/io_usage_pre_paramwatcher.png
Normal file
3
sunnypilot/common/assets/io_usage_pre_paramwatcher.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:696dc900066c78de38e64f0636716a588f68f2fa4a3d4fbfdb5ca3f8ab49e922
|
||||
size 291424
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:abb4a5ec3108d337cb4aab0d009c5dd7c03f601f725188fcb933f7dc4cd1b1fa
|
||||
size 248658
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a27f8ba0633e806083ce41bb51fbabcd027f8ad713534dc04298cc2f350b3389
|
||||
size 311370
|
||||
193
sunnypilot/common/param_watcher.py
Normal file
193
sunnypilot/common/param_watcher.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
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 os
|
||||
import platform
|
||||
import struct
|
||||
import select
|
||||
import threading
|
||||
import time
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import traceback
|
||||
from ctypes import c_void_p, c_size_t, POINTER, c_uint32, c_uint64
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
|
||||
IN_MODIFY = 0x00000002
|
||||
IN_CREATE = 0x00000100
|
||||
IN_DELETE = 0x00000200
|
||||
IN_MOVED_TO = 0x00000080
|
||||
IN_CLOSE_WRITE = 0x00000008
|
||||
|
||||
|
||||
class ParamWatcher(Params):
|
||||
_instance = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance._initialized = False
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if self._initialized:
|
||||
return
|
||||
super().__init__()
|
||||
cloudlog.warning("ParamWatcher initialized")
|
||||
self._cache = {}
|
||||
self._last_trigger = {}
|
||||
self._version = {}
|
||||
self._lock = threading.Lock()
|
||||
self._callbacks = []
|
||||
self.last_accessed_param = None
|
||||
self._initialized = True
|
||||
self.start()
|
||||
|
||||
def start(self):
|
||||
if getattr(self, '_thread', None) and self._thread.is_alive():
|
||||
return
|
||||
self._thread = threading.Thread(target=self._run_watcher, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def is_watching(self):
|
||||
return getattr(self, '_thread', None) and self._thread.is_alive()
|
||||
|
||||
def add_watcher(self, callback):
|
||||
if callback not in self._callbacks:
|
||||
self._callbacks.append(callback)
|
||||
|
||||
def _trigger_callbacks(self, path):
|
||||
with self._lock:
|
||||
if (now := time.monotonic()) - self._last_trigger.get(path, 0) < 0.1:
|
||||
return
|
||||
self._last_trigger[path] = now
|
||||
self._version[path] = self._version.get(path, 0) + 1
|
||||
self._cache.pop(path, None)
|
||||
|
||||
for callback in self._callbacks:
|
||||
try:
|
||||
callback(path)
|
||||
except Exception:
|
||||
cloudlog.exception("Param watcher callback failed")
|
||||
|
||||
def _get_cached(self, key, getter, sig):
|
||||
k = str(key)
|
||||
with self._lock:
|
||||
bucket = self._cache.get(k)
|
||||
if bucket and sig in bucket:
|
||||
if bucket[sig][0] == self._version.get(k, 0):
|
||||
return bucket[sig][1]
|
||||
|
||||
start_ver = self._version.get(k, 0)
|
||||
val = getter()
|
||||
with self._lock:
|
||||
if self._version.get(k, 0) != start_ver:
|
||||
val = getter()
|
||||
self._cache.setdefault(k, {})[sig] = (self._version.get(k, 0), val)
|
||||
return val
|
||||
|
||||
def get(self, key, block=False, return_default=False):
|
||||
self.last_accessed_param = key
|
||||
if block:
|
||||
return super().get(key, block, return_default)
|
||||
fetcher = super().get
|
||||
return self._get_cached(key, lambda: fetcher(key, block, return_default), (block, return_default))
|
||||
|
||||
def get_bool(self, key, block=False):
|
||||
self.last_accessed_param = key
|
||||
if block:
|
||||
return super().get_bool(key, block)
|
||||
fetcher = super().get_bool
|
||||
return self._get_cached(key, lambda: fetcher(key, block), ("bool", block))
|
||||
|
||||
def _run_watcher(self):
|
||||
system = platform.system()
|
||||
while True:
|
||||
try:
|
||||
if system == "Linux":
|
||||
self._run_linux()
|
||||
elif system == "Darwin":
|
||||
self._run_darwin()
|
||||
except Exception:
|
||||
cloudlog.exception("Param watcher crashed")
|
||||
time.sleep(2)
|
||||
|
||||
def _run_linux(self):
|
||||
path = Paths.params_root()
|
||||
libc = ctypes.CDLL('libc.so.6')
|
||||
fd = libc.inotify_init()
|
||||
libc.inotify_add_watch(fd, path.encode(), IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVED_TO | IN_CLOSE_WRITE)
|
||||
|
||||
try:
|
||||
poll = select.epoll()
|
||||
poll.register(fd, select.EPOLLIN)
|
||||
while True:
|
||||
for fileno, _ in poll.poll():
|
||||
if fileno == fd:
|
||||
buffer = os.read(fd, 2048)
|
||||
i = 0
|
||||
while i + 16 <= len(buffer):
|
||||
_, mask, _, name_len = struct.unpack_from("iIII", buffer, i)
|
||||
if mask & (IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVED_TO | IN_CLOSE_WRITE):
|
||||
name = buffer[i+16:i+16+name_len].rstrip(b"\0").decode()
|
||||
if not name.startswith("."):
|
||||
self._trigger_callbacks(name)
|
||||
i += 16 + name_len
|
||||
finally:
|
||||
if 'poll' in locals():
|
||||
poll.unregister(fd)
|
||||
poll.close()
|
||||
os.close(fd)
|
||||
|
||||
def _run_darwin(self):
|
||||
CS = ctypes.cdll.LoadLibrary(ctypes.util.find_library("CoreServices"))
|
||||
CF = ctypes.cdll.LoadLibrary(ctypes.util.find_library("CoreFoundation"))
|
||||
|
||||
kCFAllocatorDefault = c_void_p(0)
|
||||
kCFStringEncodingUTF8 = 0x08000100
|
||||
kFSEventStreamCreateFlagFileEvents = 0x00000010
|
||||
kFSEventStreamEventFlagItemCreated = 0x00000100
|
||||
kFSEventStreamEventFlagItemRemoved = 0x00000200
|
||||
kFSEventStreamEventFlagItemRenamed = 0x00000800
|
||||
kFSEventStreamEventFlagItemModified = 0x00001000
|
||||
|
||||
CF.CFStringCreateWithCString.restype = c_void_p
|
||||
CF.CFStringCreateWithCString.argtypes = [c_void_p, ctypes.c_char_p, c_uint32]
|
||||
CF.CFArrayCreate.restype = c_void_p
|
||||
CF.CFArrayCreate.argtypes = [c_void_p, POINTER(c_void_p), c_size_t, c_void_p]
|
||||
CS.FSEventStreamCreate.restype = c_void_p
|
||||
CS.FSEventStreamCreate.argtypes = [c_void_p, c_void_p, c_void_p, c_void_p, c_uint64, ctypes.c_double, c_uint32]
|
||||
CS.FSEventStreamScheduleWithRunLoop.argtypes = [c_void_p, c_void_p, c_void_p]
|
||||
CS.FSEventStreamStart.argtypes = [c_void_p]
|
||||
CF.CFRunLoopGetCurrent.restype = c_void_p
|
||||
|
||||
def _cb(stream, ctx, num, paths, flags, ids):
|
||||
try:
|
||||
paths_arr = ctypes.cast(paths, POINTER(c_void_p))
|
||||
flags_arr = ctypes.cast(flags, POINTER(c_uint32))
|
||||
for i in range(num):
|
||||
path = ctypes.cast(paths_arr[i], ctypes.c_char_p).value
|
||||
if path and (flags_arr[i] & (kFSEventStreamEventFlagItemCreated | kFSEventStreamEventFlagItemRemoved |
|
||||
kFSEventStreamEventFlagItemRenamed | kFSEventStreamEventFlagItemModified)):
|
||||
self._trigger_callbacks(os.path.basename(path.decode('utf-8').rstrip('/')))
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
self._darwin_cb = ctypes.CFUNCTYPE(None, c_void_p, c_void_p, c_size_t, c_void_p, POINTER(c_uint32), POINTER(c_uint64))(_cb)
|
||||
|
||||
path_str = Paths.params_root().encode('utf-8')
|
||||
cf_path = CF.CFStringCreateWithCString(kCFAllocatorDefault, path_str, kCFStringEncodingUTF8)
|
||||
cf_paths = CF.CFArrayCreate(kCFAllocatorDefault, (c_void_p * 1)(cf_path), 1, None)
|
||||
stream = CS.FSEventStreamCreate(kCFAllocatorDefault, self._darwin_cb, None, cf_paths, -1, 0.05, kFSEventStreamCreateFlagFileEvents)
|
||||
|
||||
run_loop = CF.CFRunLoopGetCurrent()
|
||||
kDefaultMode = CF.CFStringCreateWithCString(kCFAllocatorDefault, b"kCFRunLoopDefaultMode", kCFStringEncodingUTF8)
|
||||
CS.FSEventStreamScheduleWithRunLoop(stream, run_loop, kDefaultMode)
|
||||
CS.FSEventStreamStart(stream)
|
||||
CF.CFRunLoopRun()
|
||||
4
sunnypilot/common/params.py
Normal file
4
sunnypilot/common/params.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from openpilot.common.params_pyx import ParamKeyFlag, ParamKeyType, UnknownKeyName
|
||||
from openpilot.sunnypilot.common.param_watcher import ParamWatcher as Params
|
||||
|
||||
__all__ = ["Params", "ParamKeyFlag", "ParamKeyType", "UnknownKeyName"]
|
||||
0
sunnypilot/common/tests/__init__.py
Normal file
0
sunnypilot/common/tests/__init__.py
Normal file
94
sunnypilot/common/tests/test_param_watcher.py
Normal file
94
sunnypilot/common/tests/test_param_watcher.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import time
|
||||
import pytest
|
||||
import threading
|
||||
import tracemalloc
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.params_pyx import UnknownKeyName
|
||||
|
||||
from openpilot.sunnypilot.common.param_watcher import ParamWatcher
|
||||
|
||||
|
||||
class TestParamWatcher:
|
||||
BYTES_KEYS = ["LocationFilterInitialState", "UpdaterCurrentReleaseNotes", "UpdaterNewReleaseNotes"]
|
||||
BOOL_KEYS = [
|
||||
"IsMetric", "AdbEnabled", "AlwaysOnDM", "ExperimentalMode",
|
||||
"ExperimentalModeConfirmed", "DisengageOnAccelerator",
|
||||
"OpenpilotEnabledToggle", "RecordAudio", "RecordFront"
|
||||
]
|
||||
_key_counter = 0
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_method(self):
|
||||
self.params = Params()
|
||||
self.key_index = TestParamWatcher._key_counter
|
||||
TestParamWatcher._key_counter += 1
|
||||
self.bytes_key = self.BYTES_KEYS[self.key_index % len(self.BYTES_KEYS)]
|
||||
self.bool_key = self.BOOL_KEYS[self.key_index % len(self.BOOL_KEYS)]
|
||||
|
||||
@pytest.fixture
|
||||
def param_watcher(self):
|
||||
ParamWatcher._instance = None
|
||||
param_watch = ParamWatcher()
|
||||
param_watch.start()
|
||||
assert param_watch.is_watching(), "ParamWatcher thread died"
|
||||
return param_watch
|
||||
|
||||
def teardown_method(self):
|
||||
for key in (self.bytes_key, self.bool_key):
|
||||
try:
|
||||
self.params.remove(key)
|
||||
except UnknownKeyName:
|
||||
pass
|
||||
|
||||
def test_watcher_detects_change(self, param_watcher):
|
||||
val = b"123"
|
||||
self.params.put(self.bytes_key, val)
|
||||
assert param_watcher.get(self.bytes_key) == val
|
||||
|
||||
def test_watcher_get_bool(self, param_watcher):
|
||||
self.params.put_bool(self.bool_key, True)
|
||||
assert param_watcher.get_bool(self.bool_key) is True # First read should populate internal cache
|
||||
|
||||
def test_performance_comparison(self, param_watcher):
|
||||
plain_params = self.params
|
||||
|
||||
for key in self.BYTES_KEYS:
|
||||
plain_params.put(key, b"x" * 10000)
|
||||
param_watcher.get(key)
|
||||
for key in self.BOOL_KEYS:
|
||||
plain_params.put_bool(key, True)
|
||||
param_watcher.get_bool(key)
|
||||
|
||||
def bench(get_bytes, get_bool):
|
||||
tracemalloc.start()
|
||||
start_time = time.process_time()
|
||||
for _ in range(1000):
|
||||
for key in self.BYTES_KEYS:
|
||||
get_bytes(key)
|
||||
for key in self.BOOL_KEYS:
|
||||
get_bool(key)
|
||||
duration = time.process_time() - start_time
|
||||
_, memory = tracemalloc.get_traced_memory()
|
||||
tracemalloc.stop()
|
||||
return duration, memory
|
||||
|
||||
plain_cpu, plain_memory = bench(plain_params.get, plain_params.get_bool)
|
||||
watcher_cpu, watcher_memory = bench(param_watcher.get, param_watcher.get_bool)
|
||||
|
||||
# ParamWatcher *should* be significantly faster and use less memory than Params()
|
||||
assert watcher_cpu < plain_cpu * 0.6, f"PW CPU ({watcher_cpu:.4f}s) should be < 60% of Param call ({plain_cpu:.4f}s)"
|
||||
assert watcher_memory < plain_memory * 0.5, f"PW Memory ({watcher_memory}B) should be < 50% of Param call ({plain_memory}B)"
|
||||
|
||||
def test_cache_invalidation_simulation(self, param_watcher):
|
||||
self.params.put(self.bytes_key, b"old")
|
||||
assert param_watcher.get(self.bytes_key) == b"old"
|
||||
time.sleep(0.2)
|
||||
|
||||
event = threading.Event()
|
||||
param_watcher.add_watcher(lambda key: event.set())
|
||||
param_watcher._trigger_callbacks(self.bytes_key)
|
||||
assert event.wait(timeout=2), "Callback not triggered"
|
||||
|
||||
self.params.put(self.bytes_key, b"new")
|
||||
assert param_watcher.get(self.bytes_key) == b"new"
|
||||
@@ -286,6 +286,7 @@ def main(demo=False):
|
||||
v_ego = max(sm["carState"].vEgo, 0.)
|
||||
if sm.frame % 60 == 0:
|
||||
model.lat_delay = get_lat_delay(params, sm["liveDelay"].lateralDelay)
|
||||
camera_offset_helper.set_offset(params.get("CameraOffset", return_default=True))
|
||||
model.PLANPLUS_CONTROL = params.get("PlanplusControl", return_default=True)
|
||||
camera_offset_helper.set_offset(params.get("CameraOffset", return_default=True))
|
||||
lat_delay = model.lat_delay + model.LAT_SMOOTH_SECONDS
|
||||
|
||||
@@ -8,7 +8,7 @@ from collections import deque
|
||||
import math
|
||||
import numpy as np
|
||||
|
||||
from opendbc.car.lateral import FRICTION_THRESHOLD, get_friction
|
||||
from opendbc.car.lateral import get_friction, get_friction_threshold
|
||||
from opendbc.sunnypilot.car.interfaces import LatControlInputs
|
||||
from opendbc.sunnypilot.car.lateral_ext import get_friction as get_friction_in_torque_space
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
@@ -82,7 +82,7 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase):
|
||||
self._ff = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._gravity_adjusted_lateral_accel, self._roll_compensation,
|
||||
CS.vEgo, CS.aEgo), self.lac_torque.torque_params, gravity_adjusted=True)
|
||||
self._ff += get_friction_in_torque_space(self._desired_lateral_accel - self._actual_lateral_accel, self._lateral_accel_deadzone,
|
||||
FRICTION_THRESHOLD, self.lac_torque.torque_params)
|
||||
get_friction_threshold(CS.vEgo), self.lac_torque.torque_params)
|
||||
|
||||
def update_output_torque(self, CS):
|
||||
freeze_integrator = self._steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
|
||||
@@ -159,6 +159,6 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase):
|
||||
|
||||
# apply friction override for cars with low NN friction response
|
||||
if self.model.friction_override:
|
||||
self._pid_log.error += get_friction(friction_input, self._lateral_accel_deadzone, FRICTION_THRESHOLD, self.lac_torque.torque_params)
|
||||
self._pid_log.error += get_friction(friction_input, self._lateral_accel_deadzone, get_friction_threshold(CS.vEgo), self.lac_torque.torque_params)
|
||||
|
||||
self.update_output_torque(CS)
|
||||
|
||||
@@ -19,29 +19,30 @@ VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.VisionState
|
||||
ACTIVE_STATES = (VisionState.entering, VisionState.turning, VisionState.leaving)
|
||||
ENABLED_STATES = (VisionState.enabled, VisionState.overriding, *ACTIVE_STATES)
|
||||
|
||||
_ENTERING_PRED_LAT_ACC_TH = 1.3 # Predicted Lat Acc threshold to trigger entering turn state.
|
||||
_ABORT_ENTERING_PRED_LAT_ACC_TH = 1.1 # Predicted Lat Acc threshold to abort entering state if speed drops.
|
||||
_ENTERING_PRED_LAT_ACC_TH = 0.8 # Predicted Lat Acc threshold to trigger entering turn state.
|
||||
_ABORT_ENTERING_PRED_LAT_ACC_TH = 0.6 # Predicted Lat Acc threshold to abort entering state if speed drops.
|
||||
|
||||
_TURNING_LAT_ACC_TH = 1.6 # Lat Acc threshold to trigger turning state.
|
||||
_TURNING_LAT_ACC_TH = 1.4 # Lat Acc threshold to trigger turning state.
|
||||
|
||||
_LEAVING_LAT_ACC_TH = 1.3 # Lat Acc threshold to trigger leaving turn state.
|
||||
_FINISH_LAT_ACC_TH = 1.1 # Lat Acc threshold to trigger the end of the turn cycle.
|
||||
_LEAVING_LAT_ACC_TH = 1.0 # Lat Acc threshold to trigger leaving turn state.
|
||||
_FINISH_LAT_ACC_TH = 0.6 # Lat Acc threshold to trigger the end of the turn cycle.
|
||||
|
||||
_A_LAT_REG_MAX = 2. # Maximum lateral acceleration
|
||||
_A_LAT_REG_MAX = 2.0 # Maximum lateral acceleration
|
||||
|
||||
_NO_OVERSHOOT_TIME_HORIZON = 4. # s. Time to use for velocity desired based on a_target when not overshooting.
|
||||
|
||||
# Lookup table for the minimum smooth deceleration during the ENTERING state
|
||||
# depending on the actual maximum absolute lateral acceleration predicted on the turn ahead.
|
||||
_ENTERING_SMOOTH_DECEL_V = [-0.2, -1.] # min decel value allowed on ENTERING state
|
||||
_ENTERING_SMOOTH_DECEL_BP = [1.3, 3.] # absolute value of lat acc ahead
|
||||
_ENTERING_SMOOTH_DECEL_V = [-0.1, -0.4] # min decel value allowed on ENTERING state
|
||||
_ENTERING_SMOOTH_DECEL_BP = [0.8, 2.5] # absolute value of lat acc ahead
|
||||
|
||||
# Lookup table for the acceleration for the TURNING state
|
||||
# depending on the current lateral acceleration of the vehicle.
|
||||
_TURNING_ACC_V = [0.5, 0., -0.4] # acc value
|
||||
_TURNING_ACC_BP = [1.5, 2.3, 3.] # absolute value of current lat acc
|
||||
_TURNING_ACC_BP = [1.5, 2.0, 2.25] # absolute value of current lat acc
|
||||
|
||||
_LEAVING_ACC = 0.5 # Conformable acceleration to regain speed while leaving a turn.
|
||||
_LEAVING_ACC_V = [0.5, 0.1] # Conformable acceleration to regain speed while leaving a turn.
|
||||
_LEAVING_ACC_BP = [0.6, 1.2]
|
||||
|
||||
|
||||
class SmartCruiseControlVision:
|
||||
@@ -177,7 +178,7 @@ class SmartCruiseControlVision:
|
||||
# LEAVING
|
||||
elif self.state == VisionState.leaving:
|
||||
# When leaving, we provide a comfortable acceleration to regain speed.
|
||||
a_target = _LEAVING_ACC
|
||||
a_target = np.interp(self.current_lat_acc, _LEAVING_ACC_BP, _LEAVING_ACC_V)
|
||||
else:
|
||||
raise NotImplementedError(f"SCC-V state not supported: {self.state}")
|
||||
|
||||
|
||||
0
sunnypilot/tools/__init__.py
Normal file
0
sunnypilot/tools/__init__.py
Normal file
0
sunnypilot/tools/memory_profiler/__init__.py
Normal file
0
sunnypilot/tools/memory_profiler/__init__.py
Normal file
160
sunnypilot/tools/memory_profiler/mem_usage.py
Normal file
160
sunnypilot/tools/memory_profiler/mem_usage.py
Normal file
@@ -0,0 +1,160 @@
|
||||
import matplotlib.pyplot as plt
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import numpy as np
|
||||
import base64
|
||||
import io
|
||||
|
||||
from openpilot.tools.lib.logreader import LogReader, ReadMode
|
||||
|
||||
def extract_mem_cpu_data(lr):
|
||||
times, mems, cpus = [], [], []
|
||||
start_time = None
|
||||
|
||||
for msg in lr:
|
||||
if msg.which() == 'procLog':
|
||||
if start_time is None:
|
||||
start_time = msg.logMonoTime
|
||||
mem = msg.procLog.mem
|
||||
mem_usage = (mem.total - mem.available) / mem.total * 100
|
||||
cpu_usages = [(total - cpu.idle) / total * 100 for cpu in msg.procLog.cpuTimes
|
||||
if (total := cpu.idle + cpu.user + cpu.system + cpu.nice + cpu.iowait + cpu.irq + cpu.softirq) > 0]
|
||||
avg_cpu = sum(cpu_usages) / len(cpu_usages) if cpu_usages else 0
|
||||
times.append((msg.logMonoTime - start_time) / 1e9)
|
||||
mems.append(mem_usage)
|
||||
cpus.append(avg_cpu)
|
||||
return times, mems, cpus
|
||||
|
||||
def process_segment(lr):
|
||||
return [extract_mem_cpu_data(lr)]
|
||||
|
||||
def calculate_r_squared(y_true, y_pred):
|
||||
ss_res = np.sum((y_true - y_pred) ** 2)
|
||||
ss_tot = np.sum((y_true - np.mean(y_true)) ** 2)
|
||||
return 1 - (ss_res / ss_tot) if ss_tot != 0 else 0
|
||||
|
||||
def plot_results(segments, segment_data, route_name):
|
||||
valid_data = [d for d in segment_data if d and d[0]]
|
||||
if not valid_data:
|
||||
print("No valid data to plot")
|
||||
return
|
||||
|
||||
avg_mems = [np.mean(d[1]) for d in valid_data]
|
||||
avg_cpus = [np.mean(d[2]) for d in valid_data]
|
||||
valid_segments = [segments[i] for i, d in enumerate(segment_data) if d and d[0]]
|
||||
|
||||
height = max(10, 5 + len(valid_segments) * 0.4)
|
||||
fig1, ax1 = plt.subplots(1, 1, figsize=(12, height), dpi=150)
|
||||
|
||||
y_pos = range(len(valid_segments))
|
||||
ax1.barh([y - 0.2 for y in y_pos], avg_mems, height=0.4, color="dodgerblue", alpha=0.8, label="Avg Mem %")
|
||||
ax1.barh([y + 0.2 for y in y_pos], avg_cpus, height=0.4, color="green", alpha=0.8, label="Avg CPU %")
|
||||
|
||||
for i, (mem, cpu) in enumerate(zip(avg_mems, avg_cpus, strict=True)):
|
||||
ax1.text(mem, i - 0.2, f"{mem:.1f}%", va="center", fontsize=8, color="#005a9e", fontweight="bold")
|
||||
ax1.text(cpu, i + 0.2, f"{cpu:.1f}%", va="center", fontsize=8, color="#005a9e", fontweight="bold")
|
||||
|
||||
ax1.set_yticks(y_pos)
|
||||
ax1.set_yticklabels([f"Seg {s}" for s in valid_segments])
|
||||
ax1.set_xlabel("Usage (%)")
|
||||
ax1.set_title("Average Memory and CPU Usage by Segment")
|
||||
ax1.legend()
|
||||
ax1.grid(axis="x", linestyle="--", alpha=0.5)
|
||||
ax1.invert_yaxis()
|
||||
|
||||
fig2, ax2 = plt.subplots(1, 1, figsize=(12, 8), dpi=150)
|
||||
combined_times, combined_mems, combined_cpus = [], [], []
|
||||
time_offset = 0.0
|
||||
for times, mems, cpus in valid_data:
|
||||
if times:
|
||||
combined_times.extend([t + time_offset for t in times])
|
||||
combined_mems.extend(mems)
|
||||
combined_cpus.extend(cpus)
|
||||
time_offset += max(times)
|
||||
|
||||
ax2.plot(combined_times, combined_mems, color="red", label="Memory Usage", alpha=0.6)
|
||||
ax2.plot(combined_times, combined_cpus, color="blue", label="CPU Usage", alpha=0.6)
|
||||
|
||||
warmup_sec = 60
|
||||
if len(combined_times) > 1 and combined_times[-1] > warmup_sec:
|
||||
mask = np.array(combined_times) > warmup_sec
|
||||
x_reg = np.array(combined_times)[mask]
|
||||
|
||||
y_mem_reg = np.array(combined_mems)[mask]
|
||||
slope_mem, intercept_mem = np.polyfit(x_reg, y_mem_reg, 1)
|
||||
trend_mem = slope_mem * x_reg + intercept_mem
|
||||
r2_mem = calculate_r_squared(y_mem_reg, trend_mem)
|
||||
ax2.plot(x_reg, trend_mem, color="darkred", linestyle="--", linewidth=2.5,
|
||||
label=f"Mem Trend (Slope: {slope_mem:.4f} %/s, R²: {r2_mem:.2f})")
|
||||
|
||||
y_cpu_reg = np.array(combined_cpus)[mask]
|
||||
slope_cpu, intercept_cpu = np.polyfit(x_reg, y_cpu_reg, 1)
|
||||
trend_cpu = slope_cpu * x_reg + intercept_cpu
|
||||
r2_cpu = calculate_r_squared(y_cpu_reg, trend_cpu)
|
||||
ax2.plot(x_reg, trend_cpu, color="navy", linestyle="--", linewidth=2.5,
|
||||
label=f"CPU Trend (Slope: {slope_cpu:.4f} %/s, R²: {r2_cpu:.2f})")
|
||||
|
||||
ax2.set_xlabel("Time (s)")
|
||||
ax2.set_ylabel("Usage (%)")
|
||||
ax2.set_title("Memory and CPU Usage Over Time")
|
||||
ax2.legend(loc='lower left', fontsize='small', framealpha=0.9)
|
||||
ax2.grid(True, linestyle="--", alpha=0.5)
|
||||
|
||||
buffer1 = io.BytesIO()
|
||||
fig1.savefig(buffer1, format='webp', bbox_inches='tight', pad_inches=1.0)
|
||||
buffer1.seek(0)
|
||||
img1 = base64.b64encode(buffer1.getvalue()).decode()
|
||||
|
||||
buffer2 = io.BytesIO()
|
||||
fig2.savefig(buffer2, format='webp', bbox_inches='tight', pad_inches=1.0)
|
||||
buffer2.seek(0)
|
||||
img2 = base64.b64encode(buffer2.getvalue()).decode()
|
||||
|
||||
filename = f"memory_usage_{route_name}.html"
|
||||
save_path = os.path.join(os.path.dirname(__file__), "plots", filename)
|
||||
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||
|
||||
html_template = (
|
||||
"<style>body{font-family:Arial,sans-serif;margin:20px}" +
|
||||
"h1,h2,h3{text-align:center;margin:5px 0}h2{margin-bottom:10px}" +
|
||||
"img{width:100%;max-width:800px;height:auto;display:block;margin:0 auto}</style>" +
|
||||
f"<h1>Memory Profile Report</h1><h3>Route: {route_name.replace('_', '/')}</h3>" +
|
||||
f"<img src='data:image/webp;base64,{img1}'>" +
|
||||
f"<img src='data:image/webp;base64,{img2}'>"
|
||||
)
|
||||
|
||||
plt.close(fig1)
|
||||
plt.close(fig2)
|
||||
|
||||
with open(save_path, "w") as f:
|
||||
f.write(html_template)
|
||||
|
||||
print(f"Report saved to {save_path}")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Extract memory usage from route logs.')
|
||||
parser.add_argument('route_or_segment_name', help='Route or segment name from comma connect')
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
print(f"Fetching logs for {args.route_or_segment_name}")
|
||||
lr = LogReader(args.route_or_segment_name, default_mode=ReadMode.QLOG)
|
||||
segment_data = lr.run_across_segments(24, process_segment)
|
||||
segments = list(range(len(segment_data)))
|
||||
route_name = args.route_or_segment_name.replace('/', '_')
|
||||
plot_results(segments, segment_data, route_name)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""
|
||||
To run this script:
|
||||
|
||||
source .venv/bin/activate &&
|
||||
python sunnypilot/tools/memory_profiler/mem_usage.py {route_or_segment_name}
|
||||
|
||||
replace {route_or_segment_name} with full comma connect route. e.g., e1c2f3718946cc1/00000015--5888108fd9/7
|
||||
"""
|
||||
130
sunnypilot/tools/pull_footage.py
Executable file
130
sunnypilot/tools/pull_footage.py
Executable file
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import requests
|
||||
from openpilot.tools.lib.route import Route
|
||||
|
||||
|
||||
def get_segments(source, route_id, camera, seg_range):
|
||||
if "@" in source or "comma-" or "sunny-" in source: # SSH
|
||||
if not route_id:
|
||||
raise ValueError("route_id required for SSH")
|
||||
cmd = ["ssh", source, f"ls -d /data/media/0/realdata/{route_id.split('--')[0]}--*"]
|
||||
output = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode("utf-8").strip()
|
||||
return [{
|
||||
"type": "ssh",
|
||||
"host": source,
|
||||
"src": os.path.join(path, camera),
|
||||
"num": int(path.split("--")[-1])
|
||||
} for path in sorted(output.split("\n"), key=lambda x: int(x.split("--")[-1])) if path]
|
||||
else: # URL
|
||||
route = Route(route_id)
|
||||
cameras = [camera]
|
||||
if camera == "fcamera.hevc":
|
||||
cameras.extend([c for c in ["ecamera.hevc", "qcamera.ts"] if c != camera])
|
||||
|
||||
for cam in cameras:
|
||||
attr_name = "camera_paths" if cam == "fcamera.hevc" else f"{cam.split('.')[0]}_paths"
|
||||
paths = getattr(route, attr_name)()
|
||||
if any(paths):
|
||||
return [{"type": "url", "src": url, "num": idx, "cam": cam} for idx, url in enumerate(paths) if url]
|
||||
|
||||
raise ValueError(f"No footage found for {route_id}")
|
||||
|
||||
|
||||
def download(job, out_dir):
|
||||
destination = os.path.join(out_dir, f"{job['num']}_{os.path.basename(job.get('cam', job.get('src')))}")
|
||||
if os.path.exists(destination) and os.path.getsize(destination) > 0:
|
||||
return destination
|
||||
|
||||
print(f"Downloading segment {job['num']}")
|
||||
if job["type"] == "ssh":
|
||||
subprocess.check_call(["scp", f"{job['host']}:{job['src']}", destination])
|
||||
else:
|
||||
with requests.get(job["src"], stream=True) as r:
|
||||
r.raise_for_status()
|
||||
with open(destination, 'wb') as f:
|
||||
shutil.copyfileobj(r.raw, f)
|
||||
return destination
|
||||
|
||||
|
||||
def mux(files, output_file, codec):
|
||||
list_filename = f"{output_file}.list.txt"
|
||||
with open(list_filename, 'w') as f:
|
||||
f.write('\n'.join([f"file '{os.path.abspath(name)}'" for name in files]))
|
||||
|
||||
try:
|
||||
cmd = [
|
||||
"ffmpeg", "-y", "-probesize", "100M", "-analyzeduration", "100M", "-f", "concat",
|
||||
"-safe", "0", "-r", "20", "-i", list_filename, "-c", "copy", "-tag:v", codec, output_file
|
||||
]
|
||||
subprocess.check_call(cmd)
|
||||
print(f"Saved: {output_file} ({os.path.getsize(output_file) / 1048576:.2f} MB)")
|
||||
if sys.platform == "darwin":
|
||||
subprocess.run(["open", "-R", output_file])
|
||||
finally:
|
||||
if os.path.exists(list_filename):
|
||||
os.remove(list_filename)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("source")
|
||||
parser.add_argument("route_id", nargs='?')
|
||||
parser.add_argument("--output", "-o", default="output.mp4")
|
||||
parser.add_argument("--camera", "-c", default="fcamera.hevc")
|
||||
parser.add_argument("--keep-segments", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
route_id_str = args.route_id or args.source
|
||||
segment_range = None
|
||||
if "/" in route_id_str:
|
||||
route_id_str, range_str = route_id_str.rsplit("/", 1)
|
||||
if ":" in range_str or range_str.isdigit():
|
||||
segment_range = range_str
|
||||
|
||||
is_ssh = "@" in args.source or "comma-" in args.source
|
||||
if not is_ssh and len(route_id_str.split("--")) > 2:
|
||||
route_id_str = "--".join(route_id_str.split("--")[:2])
|
||||
|
||||
segments = get_segments(args.source, route_id_str, args.camera, segment_range)
|
||||
if segment_range:
|
||||
if ":" in segment_range:
|
||||
parts = segment_range.split(":")
|
||||
start_idx = int(parts[0]) if parts[0] else None
|
||||
end_idx = int(parts[1]) if parts[1] else None
|
||||
else:
|
||||
start_idx = int(segment_range)
|
||||
end_idx = start_idx + 1
|
||||
|
||||
segments = [
|
||||
segment for segment in segments
|
||||
if (start_idx is None or segment['num'] >= start_idx) and (end_idx is None or segment['num'] < end_idx)
|
||||
]
|
||||
|
||||
download_dir = f"{route_id_str}_segments"
|
||||
os.makedirs(download_dir, exist_ok=True)
|
||||
|
||||
downloaded_files = sorted(
|
||||
[download(segment, download_dir) for segment in segments],
|
||||
key=lambda x: int(os.path.basename(x).split("_")[0])
|
||||
)
|
||||
|
||||
camera_name = segments[0].get('cam', args.camera)
|
||||
codec = "hvc1" if camera_name.endswith("hevc") else "avc1"
|
||||
mux(downloaded_files, f"{route_id_str}--{args.output}", codec)
|
||||
|
||||
if not args.keep_segments:
|
||||
shutil.rmtree(download_dir)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -95,3 +95,10 @@ class Paths:
|
||||
return str(Path(Paths.comma_home()) / "media" / "0" / "osm")
|
||||
else:
|
||||
return "/data/media/0/osm"
|
||||
|
||||
@staticmethod
|
||||
def params_root() -> str:
|
||||
if PC:
|
||||
return str(Path(Paths.comma_home()) / "params" / "d")
|
||||
else:
|
||||
return "/data/params/d"
|
||||
|
||||
@@ -215,7 +215,8 @@ def main() -> None:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unblock_stdout()
|
||||
if sys.platform != 'darwin':
|
||||
unblock_stdout()
|
||||
|
||||
try:
|
||||
main()
|
||||
|
||||
@@ -10,7 +10,7 @@ from collections import deque
|
||||
|
||||
MIN_VELOCITY = 10 # px/s, changes from auto scroll to steady state
|
||||
MIN_VELOCITY_FOR_CLICKING = 2 * 60 # px/s, accepts clicks while auto scrolling below this velocity
|
||||
MIN_DRAG_PIXELS = 12
|
||||
MIN_DRAG_PIXELS = 5
|
||||
AUTO_SCROLL_TC_SNAP = 0.025
|
||||
AUTO_SCROLL_TC = 0.18
|
||||
BOUNCE_RETURN_RATE = 10.0
|
||||
@@ -221,5 +221,4 @@ class GuiScrollPanel2:
|
||||
return self._state
|
||||
|
||||
def is_touch_valid(self) -> bool:
|
||||
# MIN_VELOCITY_FOR_CLICKING is checked in auto-scroll state
|
||||
return bool(self._state != ScrollState.MANUAL_SCROLL)
|
||||
return bool(self._state in (ScrollState.STEADY, ScrollState.PRESSED))
|
||||
|
||||
@@ -321,6 +321,10 @@ def simple_button_item_sp(button_text: str | Callable[[], str], callback: Callab
|
||||
|
||||
def toggle_item_sp(title: str | Callable[[], str], description: str | Callable[[], str] | None = None, initial_state: bool = False,
|
||||
callback: Callable | None = None, icon: str = "", enabled: bool | Callable[[], bool] = True, param: str | None = None) -> ListItemSP:
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
if param is None and hasattr(ui_state.params, 'last_accessed_param') and ui_state.params.last_accessed_param:
|
||||
param = ui_state.params.last_accessed_param
|
||||
ui_state.params.last_accessed_param = None
|
||||
action = ToggleActionSP(initial_state=initial_state, enabled=enabled, callback=callback, param=param)
|
||||
return ListItemSP(title=title, description=description, action_item=action, icon=icon, callback=callback)
|
||||
|
||||
@@ -328,6 +332,10 @@ def toggle_item_sp(title: str | Callable[[], str], description: str | Callable[[
|
||||
def multiple_button_item_sp(title: str | Callable[[], str], description: str | Callable[[], str], buttons: list[str | Callable[[], str]],
|
||||
selected_index: int = 0, button_width: int = style.BUTTON_ACTION_WIDTH, callback: Callable | None = None,
|
||||
icon: str = "", param: str | None = None, inline: bool = False) -> ListItemSP:
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
if param is None and hasattr(ui_state.params, 'last_accessed_param') and ui_state.params.last_accessed_param:
|
||||
param = ui_state.params.last_accessed_param
|
||||
ui_state.params.last_accessed_param = None
|
||||
action = MultipleButtonActionSP(buttons, button_width, selected_index, callback=callback, param=param)
|
||||
return ListItemSP(title=title, description=description, icon=icon, action_item=action, inline=inline)
|
||||
|
||||
|
||||
172
tools/profile_params.py
Executable file
172
tools/profile_params.py
Executable file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
import struct
|
||||
import random
|
||||
import select
|
||||
import time
|
||||
import ctypes
|
||||
import argparse
|
||||
import csv
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.ticker as ticker
|
||||
from collections import defaultdict
|
||||
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
|
||||
from openpilot.sunnypilot.common.param_watcher import ParamWatcher, IN_CLOSE_WRITE, IN_MOVED_TO
|
||||
|
||||
IN_ACCESS = 0x00000001
|
||||
|
||||
|
||||
def get_linux_monitor(params_path, reads, writes):
|
||||
libc = ctypes.CDLL('libc.so.6')
|
||||
fd = libc.inotify_init()
|
||||
if fd < 0:
|
||||
return None
|
||||
|
||||
mask = IN_ACCESS | IN_MOVED_TO | IN_CLOSE_WRITE
|
||||
if libc.inotify_add_watch(fd, params_path.encode(), mask) < 0:
|
||||
return None
|
||||
|
||||
poll_obj = select.epoll()
|
||||
poll_obj.register(fd, select.EPOLLIN)
|
||||
|
||||
def monitor():
|
||||
for fileno, _ in poll_obj.poll(0.1):
|
||||
if fileno == fd:
|
||||
buffer = os.read(fd, 4096)
|
||||
i = 0
|
||||
while i + 16 <= len(buffer):
|
||||
wd, mask, cookie, name_len = struct.unpack_from("iIII", buffer, i)
|
||||
name = buffer[i+16:i+16+name_len].rstrip(b"\0").decode('utf-8', 'ignore')
|
||||
if name and not name.startswith("."):
|
||||
if mask & IN_ACCESS:
|
||||
reads[name] += 1
|
||||
elif mask & (IN_MOVED_TO | IN_CLOSE_WRITE):
|
||||
writes[name] += 1
|
||||
i += 16 + name_len
|
||||
|
||||
def cleanup():
|
||||
os.close(fd)
|
||||
return monitor, cleanup
|
||||
|
||||
def get_darwin_monitor(params_path, reads, writes):
|
||||
print("WARNING: macOS only reports WRITES.")
|
||||
|
||||
def callback(name):
|
||||
writes[name] += 1
|
||||
|
||||
watcher = ParamWatcher()
|
||||
watcher.add_watcher(callback)
|
||||
|
||||
def monitor():
|
||||
time.sleep(0.1)
|
||||
|
||||
def cleanup():
|
||||
if callback in watcher._callbacks:
|
||||
watcher._callbacks.remove(callback)
|
||||
return monitor, cleanup
|
||||
|
||||
def profile_params():
|
||||
parser = argparse.ArgumentParser(description="Profile Params I/O")
|
||||
parser.add_argument("--timeout", type=int, default=30, help="Timeout in minutes (default: 30 mins)")
|
||||
default_out = os.path.join(os.path.dirname(os.path.abspath(__file__)), f"params_profile_{random.randrange(99999)}.csv")
|
||||
parser.add_argument("--out", type=str, default=default_out, help="Output CSV file")
|
||||
args = parser.parse_args()
|
||||
|
||||
path = Paths.params_root()
|
||||
if not os.path.exists(path):
|
||||
return print(f"Error: {path} not found")
|
||||
|
||||
print(f"Profiling Params I/O at: {path}\nPress CTRL+C to stop.")
|
||||
reads, writes = defaultdict(int), defaultdict(int)
|
||||
|
||||
setup = get_linux_monitor if platform.system() == "Linux" else \
|
||||
get_darwin_monitor if platform.system() == "Darwin" else None
|
||||
|
||||
if not setup:
|
||||
return print("Unsupported platform")
|
||||
monitor, cleanup = setup(path, reads, writes) or (None, None)
|
||||
|
||||
if not monitor:
|
||||
return print("Failed to initialize monitor")
|
||||
|
||||
start_time = time.monotonic()
|
||||
timeout_seconds = args.timeout * 60
|
||||
last_print = start_time
|
||||
|
||||
try:
|
||||
while True:
|
||||
monitor()
|
||||
if time.monotonic() - last_print > 1.0:
|
||||
sys.stdout.write(".")
|
||||
sys.stdout.flush()
|
||||
last_print = time.monotonic()
|
||||
|
||||
if args.timeout > 0 and (time.monotonic() - start_time) > timeout_seconds:
|
||||
print("\nTimeout reached.")
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nStopping...")
|
||||
finally:
|
||||
cleanup()
|
||||
|
||||
duration = time.monotonic() - start_time
|
||||
|
||||
if args.out:
|
||||
with open(args.out, 'w', newline='') as csvfile:
|
||||
writer = csv.writer(csvfile)
|
||||
writer.writerow(['Param Name', 'Reads/sec', 'Writes/sec', 'Total Reads', 'Total Writes'])
|
||||
for k in sorted(set(reads) | set(writes), key=lambda k: reads[k] + writes[k], reverse=True):
|
||||
writer.writerow([k, f"{reads[k]/duration:.1f}", f"{writes[k]/duration:.1f}", reads[k], writes[k]])
|
||||
print(f"\nCSV report saved to {args.out}")
|
||||
|
||||
if plt:
|
||||
data = []
|
||||
for k in sorted(set(reads) | set(writes), key=lambda k: reads[k] + writes[k], reverse=True):
|
||||
data.append((k, reads[k]/duration, writes[k]/duration))
|
||||
|
||||
if data:
|
||||
data = data[:10]
|
||||
names = [x[0] for x in data]
|
||||
read_rates = [x[1] for x in data]
|
||||
write_rates = [x[2] for x in data]
|
||||
|
||||
bar_height = 0.35
|
||||
plt.figure(figsize=(12, len(names) * 0.5 + 2), dpi=150)
|
||||
y_pos = range(len(names))
|
||||
|
||||
y_pos_reads = [y - bar_height/2 for y in y_pos]
|
||||
y_pos_writes = [y + bar_height/2 for y in y_pos]
|
||||
|
||||
plt.barh(y_pos_reads, read_rates, height=bar_height, align='center', color='dodgerblue', alpha=0.8, label='Reads/sec')
|
||||
plt.barh(y_pos_writes, write_rates, height=bar_height, align='center', color='red', alpha=0.8, label='Writes/sec')
|
||||
|
||||
for i, (r_rate, w_rate) in enumerate(zip(read_rates, write_rates, strict=False)):
|
||||
if r_rate > 0:
|
||||
plt.text(r_rate, y_pos_reads[i], f"{r_rate:.2f}", va='center', fontsize=8, color='#005a9e', fontweight='bold')
|
||||
if w_rate > 0:
|
||||
plt.text(w_rate, y_pos_writes[i], f"{w_rate:.2f}", va='center', fontsize=8, color='#a30000', fontweight='bold')
|
||||
|
||||
max_val = max(max(read_rates), max(write_rates)) if read_rates else 0
|
||||
plt.xlim(0, max_val * 1.15)
|
||||
|
||||
plt.yticks(y_pos, names)
|
||||
plt.xlabel('Rate (Hz)')
|
||||
plt.title('Top 10 Params I/O Profile')
|
||||
plt.legend()
|
||||
plt.grid(axis='x', linestyle='--', alpha=0.5)
|
||||
plt.gca().xaxis.set_major_locator(ticker.MaxNLocator(integer=True, nbins='auto'))
|
||||
plt.tight_layout()
|
||||
plt.gca().invert_yaxis()
|
||||
|
||||
plot_filename = os.path.splitext(args.out)[0] + ".png"
|
||||
plt.savefig(plot_filename)
|
||||
print(f"Plot saved to {plot_filename}")
|
||||
else:
|
||||
print("matplotlib not found, skipping plot generation")
|
||||
|
||||
if __name__ == "__main__":
|
||||
profile_params()
|
||||
Reference in New Issue
Block a user