mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-07-25 14:02:06 +08:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ccef2dd04 | |||
| 44d7a81806 | |||
| 7f69eca029 | |||
| ff4d1923f0 | |||
| 3a91ae08a9 | |||
| b161764b1e | |||
| 7057c57419 | |||
| 7d4df73ea5 | |||
| 29fe152bd3 | |||
| 31918c067a | |||
| daf5ea2783 | |||
| fd7295c980 | |||
| 220cfff04d | |||
| ee0fb6bf8e | |||
| 0cd2bbf6c0 | |||
| 0871abcf55 | |||
| 8ccb777192 | |||
| 0593667601 | |||
| 288a5e14da | |||
| 9447aa0e3d |
@@ -43,6 +43,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: uv lock
|
- name: uv lock
|
||||||
|
if: github.repository == 'commaai/openpilot'
|
||||||
run: |
|
run: |
|
||||||
python3 -m ensurepip --upgrade
|
python3 -m ensurepip --upgrade
|
||||||
pip3 install uv
|
pip3 install uv
|
||||||
|
|||||||
+10
-1
@@ -172,6 +172,8 @@ struct OnroadEventSP @0xda96579883444c35 {
|
|||||||
experimentalModeSwitched @14;
|
experimentalModeSwitched @14;
|
||||||
wrongCarModeAlertOnly @15;
|
wrongCarModeAlertOnly @15;
|
||||||
pedalPressedAlertOnly @16;
|
pedalPressedAlertOnly @16;
|
||||||
|
laneTurnLeft @17;
|
||||||
|
laneTurnRight @18;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,9 +260,16 @@ struct LiveMapDataSP @0xf416ec09499d9d19 {
|
|||||||
roadName @5 :Text;
|
roadName @5 :Text;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CustomReserved9 @0xa1680744031fdb2d {
|
struct ModelDataV2SP @0xa1680744031fdb2d {
|
||||||
|
laneTurnDirection @0 :TurnDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum TurnDirection {
|
||||||
|
none @0;
|
||||||
|
turnLeft @1;
|
||||||
|
turnRight @2;
|
||||||
|
}
|
||||||
|
|
||||||
struct CustomReserved10 @0xcb9fd56c7057593a {
|
struct CustomReserved10 @0xcb9fd56c7057593a {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2631,7 +2631,7 @@ struct Event {
|
|||||||
backupManagerSP @113 :Custom.BackupManagerSP;
|
backupManagerSP @113 :Custom.BackupManagerSP;
|
||||||
carStateSP @114 :Custom.CarStateSP;
|
carStateSP @114 :Custom.CarStateSP;
|
||||||
liveMapDataSP @115 :Custom.LiveMapDataSP;
|
liveMapDataSP @115 :Custom.LiveMapDataSP;
|
||||||
customReserved9 @116 :Custom.CustomReserved9;
|
modelDataV2SP @116 :Custom.ModelDataV2SP;
|
||||||
customReserved10 @136 :Custom.CustomReserved10;
|
customReserved10 @136 :Custom.CustomReserved10;
|
||||||
customReserved11 @137 :Custom.CustomReserved11;
|
customReserved11 @137 :Custom.CustomReserved11;
|
||||||
customReserved12 @138 :Custom.CustomReserved12;
|
customReserved12 @138 :Custom.CustomReserved12;
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ _services: dict[str, tuple] = {
|
|||||||
"carControlSP": (True, 100., 10),
|
"carControlSP": (True, 100., 10),
|
||||||
"carStateSP": (True, 100., 10),
|
"carStateSP": (True, 100., 10),
|
||||||
"liveMapDataSP": (True, 1., 1),
|
"liveMapDataSP": (True, 1., 1),
|
||||||
|
"modelDataV2SP": (True, 20.),
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
"uiDebug": (True, 0., 1),
|
"uiDebug": (True, 0., 1),
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"MaxTimeOffroad", {PERSISTENT | BACKUP, INT, "1800"}},
|
{"MaxTimeOffroad", {PERSISTENT | BACKUP, INT, "1800"}},
|
||||||
{"ModelRunnerTypeCache", {CLEAR_ON_ONROAD_TRANSITION, INT}},
|
{"ModelRunnerTypeCache", {CLEAR_ON_ONROAD_TRANSITION, INT}},
|
||||||
{"OffroadMode", {CLEAR_ON_MANAGER_START, BOOL}},
|
{"OffroadMode", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||||
|
{"Offroad_TiciSupport", {CLEAR_ON_MANAGER_START, JSON}},
|
||||||
{"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
@@ -195,10 +196,12 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
|
||||||
// model panel params
|
// sunnypilot model params
|
||||||
{"LagdToggle", {PERSISTENT | BACKUP, BOOL, "1"}},
|
{"LagdToggle", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||||
{"LagdToggleDelay", {PERSISTENT | BACKUP, FLOAT, "0.2"}},
|
{"LagdToggleDelay", {PERSISTENT | BACKUP, FLOAT, "0.2"}},
|
||||||
{"LagdValueCache", {PERSISTENT, FLOAT, "0.2"}},
|
{"LagdValueCache", {PERSISTENT, FLOAT, "0.2"}},
|
||||||
|
{"LaneTurnDesire", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
{"LaneTurnValue", {PERSISTENT | BACKUP, FLOAT, "19.0"}},
|
||||||
|
|
||||||
// mapd
|
// mapd
|
||||||
{"MapAdvisorySpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT}},
|
{"MapAdvisorySpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT}},
|
||||||
|
|||||||
+1
-1
Submodule opendbc_repo updated: 004fa8df07...42bbd450b9
+1
-1
Submodule panda updated: f10ddc6a89...7eab6fd61b
@@ -1,7 +1,8 @@
|
|||||||
from cereal import log
|
from cereal import log, custom
|
||||||
from openpilot.common.constants import CV
|
from openpilot.common.constants import CV
|
||||||
from openpilot.common.realtime import DT_MDL
|
from openpilot.common.realtime import DT_MDL
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode
|
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController
|
||||||
|
|
||||||
LaneChangeState = log.LaneChangeState
|
LaneChangeState = log.LaneChangeState
|
||||||
LaneChangeDirection = log.LaneChangeDirection
|
LaneChangeDirection = log.LaneChangeDirection
|
||||||
@@ -30,6 +31,12 @@ DESIRES = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TURN_DESIRES = {
|
||||||
|
custom.TurnDirection.none: log.Desire.none,
|
||||||
|
custom.TurnDirection.turnLeft: log.Desire.turnLeft,
|
||||||
|
custom.TurnDirection.turnRight: log.Desire.turnRight,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class DesireHelper:
|
class DesireHelper:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -41,13 +48,21 @@ class DesireHelper:
|
|||||||
self.prev_one_blinker = False
|
self.prev_one_blinker = False
|
||||||
self.desire = log.Desire.none
|
self.desire = log.Desire.none
|
||||||
self.alc = AutoLaneChangeController(self)
|
self.alc = AutoLaneChangeController(self)
|
||||||
|
self.lane_turn_controller = LaneTurnController(self)
|
||||||
|
self.lane_turn_direction = custom.TurnDirection.none
|
||||||
|
|
||||||
def update(self, carstate, lateral_active, lane_change_prob):
|
def update(self, carstate, lateral_active, lane_change_prob):
|
||||||
self.alc.update_params()
|
self.alc.update_params()
|
||||||
|
self.lane_turn_controller.update_params()
|
||||||
v_ego = carstate.vEgo
|
v_ego = carstate.vEgo
|
||||||
one_blinker = carstate.leftBlinker != carstate.rightBlinker
|
one_blinker = carstate.leftBlinker != carstate.rightBlinker
|
||||||
below_lane_change_speed = v_ego < LANE_CHANGE_SPEED_MIN
|
below_lane_change_speed = v_ego < LANE_CHANGE_SPEED_MIN
|
||||||
|
|
||||||
|
# Lane turn controller update
|
||||||
|
self.lane_turn_controller.update_lane_turn(blindspot_left=carstate.leftBlindspot, blindspot_right=carstate.rightBlindspot,
|
||||||
|
left_blinker=carstate.leftBlinker, right_blinker=carstate.rightBlinker, v_ego=v_ego)
|
||||||
|
self.lane_turn_direction = self.lane_turn_controller.get_turn_direction()
|
||||||
|
|
||||||
if not lateral_active or self.lane_change_timer > LANE_CHANGE_TIME_MAX or self.alc.lane_change_set_timer == AutoLaneChangeMode.OFF:
|
if not lateral_active or self.lane_change_timer > LANE_CHANGE_TIME_MAX or self.alc.lane_change_set_timer == AutoLaneChangeMode.OFF:
|
||||||
self.lane_change_state = LaneChangeState.off
|
self.lane_change_state = LaneChangeState.off
|
||||||
self.lane_change_direction = LaneChangeDirection.none
|
self.lane_change_direction = LaneChangeDirection.none
|
||||||
@@ -106,7 +121,10 @@ class DesireHelper:
|
|||||||
|
|
||||||
self.prev_one_blinker = one_blinker
|
self.prev_one_blinker = one_blinker
|
||||||
|
|
||||||
self.desire = DESIRES[self.lane_change_direction][self.lane_change_state]
|
if self.lane_turn_direction != custom.TurnDirection.none:
|
||||||
|
self.desire = TURN_DESIRES[self.lane_turn_direction]
|
||||||
|
else:
|
||||||
|
self.desire = DESIRES[self.lane_change_direction][self.lane_change_state]
|
||||||
|
|
||||||
# Send keep pulse once per second during LaneChangeStart.preLaneChange
|
# Send keep pulse once per second during LaneChangeStart.preLaneChange
|
||||||
if self.lane_change_state in (LaneChangeState.off, LaneChangeState.laneChangeStarting):
|
if self.lane_change_state in (LaneChangeState.off, LaneChangeState.laneChangeStarting):
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ def main(demo=False):
|
|||||||
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
|
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
|
||||||
|
|
||||||
# messaging
|
# messaging
|
||||||
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry"])
|
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"])
|
||||||
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"])
|
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"])
|
||||||
|
|
||||||
publish_state = PublishState()
|
publish_state = PublishState()
|
||||||
@@ -333,6 +333,7 @@ def main(demo=False):
|
|||||||
modelv2_send = messaging.new_message('modelV2')
|
modelv2_send = messaging.new_message('modelV2')
|
||||||
drivingdata_send = messaging.new_message('drivingModelData')
|
drivingdata_send = messaging.new_message('drivingModelData')
|
||||||
posenet_send = messaging.new_message('cameraOdometry')
|
posenet_send = messaging.new_message('cameraOdometry')
|
||||||
|
mdv2sp_send = messaging.new_message('modelDataV2SP')
|
||||||
|
|
||||||
action = get_action_from_model(model_output, prev_action, lat_delay + DT_MDL, long_delay + DT_MDL, v_ego)
|
action = get_action_from_model(model_output, prev_action, lat_delay + DT_MDL, long_delay + DT_MDL, v_ego)
|
||||||
prev_action = action
|
prev_action = action
|
||||||
@@ -347,6 +348,7 @@ def main(demo=False):
|
|||||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
|
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
|
||||||
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
||||||
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
||||||
|
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
|
||||||
drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state
|
drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state
|
||||||
drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction
|
drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction
|
||||||
|
|
||||||
@@ -354,6 +356,7 @@ def main(demo=False):
|
|||||||
pm.send('modelV2', modelv2_send)
|
pm.send('modelV2', modelv2_send)
|
||||||
pm.send('drivingModelData', drivingdata_send)
|
pm.send('drivingModelData', drivingdata_send)
|
||||||
pm.send('cameraOdometry', posenet_send)
|
pm.send('cameraOdometry', posenet_send)
|
||||||
|
pm.send('modelDataV2SP', mdv2sp_send)
|
||||||
last_vipc_frame_id = meta_main.frame_id
|
last_vipc_frame_id = meta_main.frame_id
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -49,5 +49,10 @@
|
|||||||
"text": "openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting.",
|
"text": "openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting.",
|
||||||
"severity": 1,
|
"severity": 1,
|
||||||
"_comment": "Set extra field to lateral or longitudinal."
|
"_comment": "Set extra field to lateral or longitudinal."
|
||||||
|
},
|
||||||
|
"Offroad_TiciSupport": {
|
||||||
|
"text": "<b>Unsupported branch detected</b> - The current version of <b><u>%1</u></b> branch is no longer supported on the comma three. Please go to <b>[Device > Software]</b> and install a supported branch with <b><u>-tici</u></b> in the branch name for the comma three.",
|
||||||
|
"severity": 1,
|
||||||
|
"_comment": "Set extra field to the current branch name."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ class SelfdriveD(CruiseHelper):
|
|||||||
# TODO: de-couple selfdrived with card/conflate on carState without introducing controls mismatches
|
# TODO: de-couple selfdrived with card/conflate on carState without introducing controls mismatches
|
||||||
self.car_state_sock = messaging.sub_sock('carState', timeout=20)
|
self.car_state_sock = messaging.sub_sock('carState', timeout=20)
|
||||||
|
|
||||||
ignore = self.sensor_packets + self.gps_packets + ['alertDebug']
|
ignore = self.sensor_packets + self.gps_packets + ['alertDebug'] + ['modelDataV2SP']
|
||||||
if SIMULATION:
|
if SIMULATION:
|
||||||
ignore += ['driverCameraState', 'managerState']
|
ignore += ['driverCameraState', 'managerState']
|
||||||
if REPLAY:
|
if REPLAY:
|
||||||
@@ -95,7 +95,8 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'liveCalibration',
|
self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'liveCalibration',
|
||||||
'carOutput', 'driverMonitoringState', 'longitudinalPlan', 'livePose', 'liveDelay',
|
'carOutput', 'driverMonitoringState', 'longitudinalPlan', 'livePose', 'liveDelay',
|
||||||
'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters',
|
'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters',
|
||||||
'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userBookmark', 'audioFeedback'] + \
|
'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userBookmark', 'audioFeedback',
|
||||||
|
'modelDataV2SP'] + \
|
||||||
self.camera_packets + self.sensor_packets + self.gps_packets,
|
self.camera_packets + self.sensor_packets + self.gps_packets,
|
||||||
ignore_alive=ignore, ignore_avg_freq=ignore,
|
ignore_alive=ignore, ignore_avg_freq=ignore,
|
||||||
ignore_valid=ignore, frequency=int(1/DT_CTRL))
|
ignore_valid=ignore, frequency=int(1/DT_CTRL))
|
||||||
@@ -300,6 +301,13 @@ class SelfdriveD(CruiseHelper):
|
|||||||
LaneChangeState.laneChangeFinishing):
|
LaneChangeState.laneChangeFinishing):
|
||||||
self.events.add(EventName.laneChange)
|
self.events.add(EventName.laneChange)
|
||||||
|
|
||||||
|
# Handle lane turn
|
||||||
|
lane_turn_direction = self.sm['modelDataV2SP'].laneTurnDirection
|
||||||
|
if lane_turn_direction == custom.TurnDirection.turnLeft:
|
||||||
|
self.events_sp.add(custom.OnroadEventSP.EventName.laneTurnLeft)
|
||||||
|
elif lane_turn_direction == custom.TurnDirection.turnRight:
|
||||||
|
self.events_sp.add(custom.OnroadEventSP.EventName.laneTurnRight)
|
||||||
|
|
||||||
for i, pandaState in enumerate(self.sm['pandaStates']):
|
for i, pandaState in enumerate(self.sm['pandaStates']):
|
||||||
# All pandas must match the list of safetyConfigs, and if outside this list, must be silent or noOutput
|
# All pandas must match the list of safetyConfigs, and if outside this list, must be silent or noOutput
|
||||||
if i < len(self.CP.safetyConfigs):
|
if i < len(self.CP.safetyConfigs):
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ void ModelRenderer::draw(QPainter &painter, const QRect &surface_rect) {
|
|||||||
drawLead(painter, lead_two, lead_vertices[1], surface_rect);
|
drawLead(painter, lead_two, lead_vertices[1], surface_rect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
drawLeadStatus(painter, surface_rect.height(), surface_rect.width());
|
|
||||||
|
|
||||||
painter.restore();
|
painter.restore();
|
||||||
}
|
}
|
||||||
@@ -175,172 +174,6 @@ QColor ModelRenderer::blendColors(const QColor &start, const QColor &end, float
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void ModelRenderer::drawLeadStatus(QPainter &painter, int height, int width) {
|
|
||||||
auto *s = uiState();
|
|
||||||
auto &sm = *(s->sm);
|
|
||||||
|
|
||||||
if (!sm.alive("radarState")) return;
|
|
||||||
|
|
||||||
const auto &radar_state = sm["radarState"].getRadarState();
|
|
||||||
const auto &lead_one = radar_state.getLeadOne();
|
|
||||||
const auto &lead_two = radar_state.getLeadTwo();
|
|
||||||
|
|
||||||
// Check if we have any active leads
|
|
||||||
bool has_lead_one = lead_one.getStatus();
|
|
||||||
bool has_lead_two = lead_two.getStatus();
|
|
||||||
|
|
||||||
if (!has_lead_one && !has_lead_two) {
|
|
||||||
// Fade out status display
|
|
||||||
lead_status_alpha = std::max(0.0f, lead_status_alpha - 0.05f);
|
|
||||||
if (lead_status_alpha <= 0.0f) return;
|
|
||||||
} else {
|
|
||||||
// Fade in status display
|
|
||||||
lead_status_alpha = std::min(1.0f, lead_status_alpha + 0.1f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw status for each lead vehicle under its chevron
|
|
||||||
if (true) {
|
|
||||||
drawLeadStatusAtPosition(painter, lead_one, lead_vertices[0], height, width, "L1");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (has_lead_two && std::abs(lead_one.getDRel() - lead_two.getDRel()) > 3.0) {
|
|
||||||
drawLeadStatusAtPosition(painter, lead_two, lead_vertices[1], height, width, "L2");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModelRenderer::drawLeadStatusAtPosition(QPainter &painter,
|
|
||||||
const cereal::RadarState::LeadData::Reader &lead_data,
|
|
||||||
const QPointF &chevron_pos,
|
|
||||||
int height, int width,
|
|
||||||
const QString &label) {
|
|
||||||
|
|
||||||
float d_rel = lead_data.getDRel();
|
|
||||||
float v_rel = lead_data.getVRel();
|
|
||||||
auto *s = uiState();
|
|
||||||
auto &sm = *(s->sm);
|
|
||||||
float v_ego = sm["carState"].getCarState().getVEgo();
|
|
||||||
|
|
||||||
int chevron_data = std::atoi(Params().get("ChevronInfo").c_str());
|
|
||||||
|
|
||||||
// Calculate chevron size (same logic as drawLead)
|
|
||||||
float sz = std::clamp((25 * 30) / (d_rel / 3 + 30), 15.0f, 30.0f) * 2.35;
|
|
||||||
|
|
||||||
QFont content_font = painter.font();
|
|
||||||
content_font.setPixelSize(35);
|
|
||||||
content_font.setBold(true);
|
|
||||||
painter.setFont(content_font);
|
|
||||||
|
|
||||||
QFontMetrics fm(content_font);
|
|
||||||
bool is_metric = s->scene.is_metric;
|
|
||||||
|
|
||||||
QStringList text_lines;
|
|
||||||
|
|
||||||
const int chevron_types = 3;
|
|
||||||
const int chevron_all = chevron_types + 1; // All metrics (value 4)
|
|
||||||
QStringList chevron_text[chevron_types];
|
|
||||||
int position;
|
|
||||||
float val;
|
|
||||||
|
|
||||||
// Distance display (chevron_data == 1 or all)
|
|
||||||
if (chevron_data == 1 || chevron_data == chevron_all) {
|
|
||||||
position = 0;
|
|
||||||
val = std::max(0.0f, d_rel);
|
|
||||||
QString distance_unit = is_metric ? "m" : "ft";
|
|
||||||
if (!is_metric) {
|
|
||||||
val *= 3.28084f; // Convert meters to feet
|
|
||||||
}
|
|
||||||
chevron_text[position].append(QString::number(val, 'f', 0) + " " + distance_unit);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Absolute velocity display (chevron_data == 2 or all)
|
|
||||||
if (chevron_data == 2 || chevron_data == chevron_all) {
|
|
||||||
position = (chevron_data == 2) ? 0 : 1;
|
|
||||||
val = std::max(0.0f, (v_rel + v_ego) * (is_metric ? static_cast<float>(MS_TO_KPH) : static_cast<float>(MS_TO_MPH)));
|
|
||||||
chevron_text[position].append(QString::number(val, 'f', 0) + " " + (is_metric ? "km/h" : "mph"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Time-to-contact display (chevron_data == 3 or all)
|
|
||||||
if (chevron_data == 3 || chevron_data == chevron_all) {
|
|
||||||
position = (chevron_data == 3) ? 0 : 2;
|
|
||||||
val = (d_rel > 0 && v_ego > 0) ? std::max(0.0f, d_rel / v_ego) : 0.0f;
|
|
||||||
QString ttc_str = (val > 0 && val < 200) ? QString::number(val, 'f', 1) + "s" : "---";
|
|
||||||
chevron_text[position].append(ttc_str);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect all non-empty text lines
|
|
||||||
for (int i = 0; i < chevron_types; ++i) {
|
|
||||||
if (!chevron_text[i].isEmpty()) {
|
|
||||||
text_lines.append(chevron_text[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no text to display, return early
|
|
||||||
if (text_lines.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Text box dimensions
|
|
||||||
float str_w = 150; // Width of text area
|
|
||||||
float str_h = 45; // Height per line
|
|
||||||
|
|
||||||
// Position text below chevron, centered horizontally
|
|
||||||
float text_x = chevron_pos.x() - str_w / 2;
|
|
||||||
float text_y = chevron_pos.y() + sz + 15;
|
|
||||||
|
|
||||||
// Clamp to screen bounds
|
|
||||||
text_x = std::clamp(text_x, 10.0f, (float)width - str_w - 10);
|
|
||||||
|
|
||||||
// Shadow offset
|
|
||||||
QPoint shadow_offset(2, 2);
|
|
||||||
|
|
||||||
// Draw each line of text with shadow
|
|
||||||
for (int i = 0; i < text_lines.size(); ++i) {
|
|
||||||
if (!text_lines[i].isEmpty()) {
|
|
||||||
QRect textRect(text_x, text_y + (i * str_h), str_w, str_h);
|
|
||||||
|
|
||||||
// Draw shadow
|
|
||||||
painter.setPen(QColor(0x0, 0x0, 0x0, (int)(200 * lead_status_alpha)));
|
|
||||||
painter.drawText(textRect.translated(shadow_offset.x(), shadow_offset.y()),
|
|
||||||
Qt::AlignBottom | Qt::AlignHCenter, text_lines[i]);
|
|
||||||
|
|
||||||
// Determine text color based on content and danger level
|
|
||||||
QColor text_color;
|
|
||||||
|
|
||||||
// Check if this is a distance line (contains 'm' or 'ft')
|
|
||||||
if (text_lines[i].contains("m") || text_lines[i].contains("ft")) {
|
|
||||||
if (d_rel < 20.0f) {
|
|
||||||
text_color = QColor(255, 80, 80, (int)(255 * lead_status_alpha)); // Red - danger
|
|
||||||
} else if (d_rel < 40.0f) {
|
|
||||||
text_color = QColor(255, 200, 80, (int)(255 * lead_status_alpha)); // Yellow - caution
|
|
||||||
} else {
|
|
||||||
text_color = QColor(80, 255, 120, (int)(255 * lead_status_alpha)); // Green - safe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Enhanced color coding for time-to-contact
|
|
||||||
else if (text_lines[i].contains("s") && !text_lines[i].contains("---")) {
|
|
||||||
float ttc_val = text_lines[i].left(text_lines[i].length() - 1).toFloat();
|
|
||||||
if (ttc_val < 3.0f) {
|
|
||||||
text_color = QColor(255, 80, 80, (int)(255 * lead_status_alpha)); // Red - urgent
|
|
||||||
} else if (ttc_val < 6.0f) {
|
|
||||||
text_color = QColor(255, 200, 80, (int)(255 * lead_status_alpha)); // Yellow - caution
|
|
||||||
} else {
|
|
||||||
text_color = QColor(0xff, 0xff, 0xff, (int)(255 * lead_status_alpha)); // White - safe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
text_color = QColor(0xff, 0xff, 0xff, (int)(255 * lead_status_alpha)); // White for other lines
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw main text
|
|
||||||
painter.setPen(text_color);
|
|
||||||
painter.drawText(textRect, Qt::AlignBottom | Qt::AlignHCenter, text_lines[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset pen
|
|
||||||
painter.setPen(Qt::NoPen);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModelRenderer::drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data,
|
void ModelRenderer::drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data,
|
||||||
const QPointF &vd, const QRect &surface_rect) {
|
const QPointF &vd, const QRect &surface_rect) {
|
||||||
const float speedBuff = 10.;
|
const float speedBuff = 10.;
|
||||||
|
|||||||
@@ -34,12 +34,6 @@ protected:
|
|||||||
bool mapToScreen(float in_x, float in_y, float in_z, QPointF *out);
|
bool mapToScreen(float in_x, float in_y, float in_z, QPointF *out);
|
||||||
void mapLineToPolygon(const cereal::XYZTData::Reader &line, float y_off, float z_off,
|
void mapLineToPolygon(const cereal::XYZTData::Reader &line, float y_off, float z_off,
|
||||||
QPolygonF *pvd, int max_idx, bool allow_invert = true);
|
QPolygonF *pvd, int max_idx, bool allow_invert = true);
|
||||||
void drawLeadStatus(QPainter &painter, int height, int width);
|
|
||||||
void drawLeadStatusAtPosition(QPainter &painter,
|
|
||||||
const cereal::RadarState::LeadData::Reader &lead_data,
|
|
||||||
const QPointF &chevron_pos,
|
|
||||||
int height, int width,
|
|
||||||
const QString &label);
|
|
||||||
void drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data, const QPointF &vd, const QRect &surface_rect);
|
void drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data, const QPointF &vd, const QRect &surface_rect);
|
||||||
void update_leads(const cereal::RadarState::Reader &radar_state, const cereal::XYZTData::Reader &line);
|
void update_leads(const cereal::RadarState::Reader &radar_state, const cereal::XYZTData::Reader &line);
|
||||||
virtual void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead);
|
virtual void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead);
|
||||||
@@ -65,8 +59,4 @@ protected:
|
|||||||
Eigen::Matrix3f car_space_transform = Eigen::Matrix3f::Zero();
|
Eigen::Matrix3f car_space_transform = Eigen::Matrix3f::Zero();
|
||||||
QRectF clip_region;
|
QRectF clip_region;
|
||||||
|
|
||||||
float lead_status_alpha = 0.0f;
|
|
||||||
QPointF lead_status_pos;
|
|
||||||
QString lead_status_text;
|
|
||||||
QColor lead_status_color;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -101,32 +101,42 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) {
|
|||||||
QString policyType = tr("Policy Model");
|
QString policyType = tr("Policy Model");
|
||||||
policyFrame = createModelDetailFrame(this, policyType, policyProgressBar);
|
policyFrame = createModelDetailFrame(this, policyType, policyProgressBar);
|
||||||
list->addItem(policyFrame);
|
list->addItem(policyFrame);
|
||||||
|
|
||||||
list->addItem(horizontal_line());
|
list->addItem(horizontal_line());
|
||||||
|
|
||||||
|
// Lane Turn Desire toggle
|
||||||
|
lane_turn_desire_toggle = new ParamControlSP("LaneTurnDesire", tr("Use Lane Turn Desires"),
|
||||||
|
"If you’re driving at 20 mph (32 km/h) or below and have your blinker on, "
|
||||||
|
"the car will plan a turn in that direction at the nearest drivable path. "
|
||||||
|
"This prevents situations (like at red lights) where the car might plan the wrong turn direction.",
|
||||||
|
"../assets/offroad/icon_shell.png");
|
||||||
|
list->addItem(lane_turn_desire_toggle);
|
||||||
|
|
||||||
|
// Lane Turn Value control
|
||||||
|
int max_value_mph = 20;
|
||||||
|
bool is_metric_initial = params.getBool("IsMetric");
|
||||||
|
const float K = 1.609344f;
|
||||||
|
int per_value_change_scaled = is_metric_initial ? static_cast<int>(std::round((1.0f / K) * 100.0f)) : 100; // 100 -> 1 mph
|
||||||
|
lane_turn_value_control = new OptionControlSP("LaneTurnValue", tr("Adjust Lane Turn Speed"),
|
||||||
|
tr("Set the maximum speed for lane turn desires. Default is 19 %1.").arg(is_metric_initial ? "km/h" : "mph"),
|
||||||
|
"", {5 * 100, max_value_mph * 100}, per_value_change_scaled, false, nullptr, true, true);
|
||||||
|
lane_turn_value_control->showDescription();
|
||||||
|
list->addItem(lane_turn_value_control);
|
||||||
|
|
||||||
|
// Show based on toggle
|
||||||
|
refreshLaneTurnValueControl();
|
||||||
|
connect(lane_turn_desire_toggle, &ParamControlSP::toggleFlipped, this, &ModelsPanel::refreshLaneTurnValueControl);
|
||||||
|
connect(lane_turn_value_control, &OptionControlSP::updateLabels, this, &ModelsPanel::refreshLaneTurnValueControl);
|
||||||
|
|
||||||
// LiveDelay toggle
|
// LiveDelay toggle
|
||||||
lagd_toggle_control = new ParamControlSP("LagdToggle", tr("Live Learning Steer Delay"), "", "../assets/offroad/icon_shell.png");
|
lagd_toggle_control = new ParamControlSP("LagdToggle", tr("Live Learning Steer Delay"), "", "../assets/offroad/icon_shell.png");
|
||||||
lagd_toggle_control->showDescription();
|
lagd_toggle_control->showDescription();
|
||||||
list->addItem(lagd_toggle_control);
|
list->addItem(lagd_toggle_control);
|
||||||
|
|
||||||
// Software delay control
|
// Software delay control
|
||||||
int liveDelayMaxInt = 30;
|
|
||||||
std::string liveDelayBytes = params.get("LiveDelay");
|
|
||||||
if (!liveDelayBytes.empty()) {
|
|
||||||
capnp::FlatArrayMessageReader msg(kj::ArrayPtr<const capnp::word>(
|
|
||||||
reinterpret_cast<const capnp::word*>(liveDelayBytes.data()),
|
|
||||||
liveDelayBytes.size() / sizeof(capnp::word)));
|
|
||||||
auto event = msg.getRoot<cereal::Event>();
|
|
||||||
if (event.hasLiveDelay()) {
|
|
||||||
auto liveDelay = event.getLiveDelay();
|
|
||||||
float lateralDelay = liveDelay.getLateralDelay();
|
|
||||||
liveDelayMaxInt = static_cast<int>(lateralDelay * 100.0f) + 20;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delay_control = new OptionControlSP("LagdToggleDelay", tr("Adjust Software Delay"),
|
delay_control = new OptionControlSP("LagdToggleDelay", tr("Adjust Software Delay"),
|
||||||
tr("Adjust the software delay when Live Learning Steer Delay is toggled off."
|
tr("Adjust the software delay when Live Learning Steer Delay is toggled off."
|
||||||
"\nThe default software delay value is 0.2"),
|
"\nThe default software delay value is 0.2"),
|
||||||
"", {5, liveDelayMaxInt}, 1, false, nullptr, true, true);
|
"", {5, 50}, 1, false, nullptr, true, true);
|
||||||
|
|
||||||
connect(delay_control, &OptionControlSP::updateLabels, [=]() {
|
connect(delay_control, &OptionControlSP::updateLabels, [=]() {
|
||||||
float value = QString::fromStdString(params.get("LagdToggleDelay")).toFloat();
|
float value = QString::fromStdString(params.get("LagdToggleDelay")).toFloat();
|
||||||
@@ -159,6 +169,19 @@ QFrame* ModelsPanel::createModelDetailFrame(QWidget *parent, QString &typeName,
|
|||||||
return frame;
|
return frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ModelsPanel::refreshLaneTurnValueControl() {
|
||||||
|
if (!lane_turn_value_control) return;
|
||||||
|
float stored_mph = QString::fromStdString(params.get("LaneTurnValue")).toFloat();
|
||||||
|
bool is_metric = params.getBool("IsMetric");
|
||||||
|
QString unit = is_metric ? "km/h" : "mph";
|
||||||
|
float display_value = stored_mph;
|
||||||
|
if (is_metric) {
|
||||||
|
display_value = stored_mph * 1.609344f;
|
||||||
|
}
|
||||||
|
lane_turn_value_control->setLabel(QString::number(static_cast<int>(std::round(display_value))) + " " + unit);
|
||||||
|
lane_turn_value_control->setVisible(params.getBool("LaneTurnDesire"));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Updates the UI with bundle download progress information
|
* @brief Updates the UI with bundle download progress information
|
||||||
* Reads status from modelManagerSP cereal message and displays status for all models
|
* Reads status from modelManagerSP cereal message and displays status for all models
|
||||||
@@ -401,34 +424,28 @@ void ModelsPanel::updateLabels() {
|
|||||||
"Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience.");
|
"Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience.");
|
||||||
bool lagdEnabled = params.getBool("LagdToggle");
|
bool lagdEnabled = params.getBool("LagdToggle");
|
||||||
if (lagdEnabled) {
|
if (lagdEnabled) {
|
||||||
std::string liveDelayBytes = params.get("LiveDelay");
|
auto liveDelayBytes = params.get("LiveDelay");
|
||||||
if (!liveDelayBytes.empty()) {
|
if (!liveDelayBytes.empty()) {
|
||||||
capnp::FlatArrayMessageReader msg(kj::ArrayPtr<const capnp::word>(
|
auto LD = loadCerealEvent(params, "LiveDelay");
|
||||||
reinterpret_cast<const capnp::word*>(liveDelayBytes.data()),
|
float lateralDelay = LD->getLiveDelay().getLateralDelay();
|
||||||
liveDelayBytes.size() / sizeof(capnp::word)));
|
desc += QString("<br><br><b><span style=\"color:#e0e0e0\">%1</span></b> <span style=\"color:#e0e0e0\">%2 s</span>")
|
||||||
auto event = msg.getRoot<cereal::Event>();
|
.arg(tr("Live Steer Delay:")).arg(QString::number(lateralDelay, 'f', 3));
|
||||||
if (event.hasLiveDelay()) {
|
|
||||||
auto liveDelay = event.getLiveDelay();
|
|
||||||
float lateralDelay = liveDelay.getLateralDelay();
|
|
||||||
desc += QString("<br><br><b><span style=\"color:#e0e0e0\">%1</span></b> <span style=\"color:#e0e0e0\">%2 s</span>")
|
|
||||||
.arg(tr("Live Steer Delay:")).arg(QString::number(lateralDelay, 'f', 3));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
std::string carParamsBytes = params.get("CarParamsPersistent");
|
auto carParamsBytes = params.get("CarParamsPersistent");
|
||||||
if (!carParamsBytes.empty()) {
|
if (!carParamsBytes.empty()) {
|
||||||
capnp::FlatArrayMessageReader msg(kj::ArrayPtr<const capnp::word>(
|
AlignedBuffer aligned_buf_cp;
|
||||||
reinterpret_cast<const capnp::word*>(carParamsBytes.data()),
|
capnp::FlatArrayMessageReader cmsg(aligned_buf_cp.align(carParamsBytes.data(), carParamsBytes.size()));
|
||||||
carParamsBytes.size() / sizeof(capnp::word)));
|
cereal::CarParams::Reader CP = cmsg.getRoot<cereal::CarParams>();
|
||||||
auto carParams = msg.getRoot<cereal::CarParams>();
|
|
||||||
float steerDelay = carParams.getSteerActuatorDelay();
|
float steerDelay = CP.getSteerActuatorDelay();
|
||||||
float softwareDelay = QString::fromStdString(params.get("LagdToggleDelay")).toFloat();
|
float softwareDelay = QString::fromStdString(params.get("LagdToggleDelay")).toFloat();
|
||||||
float totalLag = steerDelay + softwareDelay;
|
float totalLag = steerDelay + softwareDelay;
|
||||||
desc += QString("<br><br><span style=\"color:#e0e0e0\">"
|
desc += QString("<br><br><span style=\"color:#e0e0e0\">"
|
||||||
"<b>%1</b> %2 s + <b>%3</b> %4 s = <b>%5</b> %6 s</span>")
|
"<b>%1</b> %2 s + <b>%3</b> %4 s = <b>%5</b> %6 s</span>")
|
||||||
.arg(tr("Actuator Delay:"), QString::number(steerDelay, 'f', 2),
|
.arg(tr("Actuator Delay:"), QString::number(steerDelay, 'f', 2),
|
||||||
tr("Software Delay:"), QString::number(softwareDelay, 'f', 2),
|
tr("Software Delay:"), QString::number(softwareDelay, 'f', 2),
|
||||||
tr("Total Delay:"), QString::number(totalLag, 'f', 2));
|
tr("Total Delay:"), QString::number(totalLag, 'f', 2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lagd_toggle_control->setDescription(desc);
|
lagd_toggle_control->setDescription(desc);
|
||||||
@@ -439,6 +456,9 @@ void ModelsPanel::updateLabels() {
|
|||||||
delay_control->setLabel(QString::number(value, 'f', 2) + "s");
|
delay_control->setLabel(QString::number(value, 'f', 2) + "s");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update lane turn desire label and visibility
|
||||||
|
refreshLaneTurnValueControl();
|
||||||
|
|
||||||
clearModelCacheBtn->setValue(QString::number(calculateCacheSize(), 'f', 2) + " MB");
|
clearModelCacheBtn->setValue(QString::number(calculateCacheSize(), 'f', 2) + " MB");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
#include <QProgressBar>
|
#include <QProgressBar>
|
||||||
|
|
||||||
|
#include "selfdrive/ui/sunnypilot/qt/util.h"
|
||||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||||
|
|
||||||
class ModelsPanel : public QWidget {
|
class ModelsPanel : public QWidget {
|
||||||
@@ -37,6 +38,7 @@ private:
|
|||||||
void updateLabels();
|
void updateLabels();
|
||||||
void handleCurrentModelLblBtnClicked();
|
void handleCurrentModelLblBtnClicked();
|
||||||
void handleBundleDownloadProgress();
|
void handleBundleDownloadProgress();
|
||||||
|
void refreshLaneTurnValueControl();
|
||||||
void showResetParamsDialog();
|
void showResetParamsDialog();
|
||||||
QProgressBar* createProgressBar(QWidget *parent);
|
QProgressBar* createProgressBar(QWidget *parent);
|
||||||
QFrame* createModelDetailFrame(QWidget *parent, QString &typeName, QProgressBar *progressBar);
|
QFrame* createModelDetailFrame(QWidget *parent, QString &typeName, QProgressBar *progressBar);
|
||||||
@@ -81,5 +83,6 @@ private:
|
|||||||
Params params;
|
Params params;
|
||||||
ButtonControlSP *clearModelCacheBtn;
|
ButtonControlSP *clearModelCacheBtn;
|
||||||
ButtonControlSP *refreshAvailableModelsBtn;
|
ButtonControlSP *refreshAvailableModelsBtn;
|
||||||
|
ParamControlSP *lane_turn_desire_toggle;
|
||||||
|
OptionControlSP *lane_turn_value_control;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,12 +11,39 @@ SoftwarePanelSP::SoftwarePanelSP(QWidget *parent) : SoftwarePanel(parent) {
|
|||||||
// branch selector
|
// branch selector
|
||||||
QObject::disconnect(targetBranchBtn, nullptr, nullptr, nullptr);
|
QObject::disconnect(targetBranchBtn, nullptr, nullptr, nullptr);
|
||||||
connect(targetBranchBtn, &ButtonControlSP::clicked, [=]() {
|
connect(targetBranchBtn, &ButtonControlSP::clicked, [=]() {
|
||||||
InputDialog d(tr("Search Branch"), this, tr("Enter search keywords, or leave blank to list all branches."), false);
|
if (Hardware::get_device_type() == cereal::InitData::DeviceType::TICI) {
|
||||||
|
auto current = params.get("GitBranch");
|
||||||
|
QStringList allBranches = QString::fromStdString(params.get("UpdaterAvailableBranches")).split(",");
|
||||||
|
QStringList branches;
|
||||||
|
for (const QString &b : allBranches) {
|
||||||
|
if (b.endsWith("-tici")) {
|
||||||
|
branches.append(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (QString b : {current.c_str(), "master-tici", "staging-tici", "release-tici"}) {
|
||||||
|
auto i = branches.indexOf(b);
|
||||||
|
if (i >= 0) {
|
||||||
|
branches.removeAt(i);
|
||||||
|
branches.insert(0, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString cur = QString::fromStdString(params.get("UpdaterTargetBranch"));
|
||||||
|
QString selection = MultiOptionDialog::getSelection(tr("Select a branch"), branches, cur, this);
|
||||||
|
if (!selection.isEmpty()) {
|
||||||
|
params.put("UpdaterTargetBranch", selection.toStdString());
|
||||||
|
targetBranchBtn->setValue(QString::fromStdString(params.get("UpdaterTargetBranch")));
|
||||||
|
checkForUpdates();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
InputDialog d(tr("Search Branch"), this, tr("Enter search keywords, or leave blank to list all branches."), false);
|
||||||
d.setMinLength(0);
|
d.setMinLength(0);
|
||||||
const int ret = d.exec();
|
const int ret = d.exec();
|
||||||
if (ret) {
|
if (ret) {
|
||||||
searchBranches(d.text());
|
searchBranches(d.text());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Disable Updates toggle
|
// Disable Updates toggle
|
||||||
|
|||||||
@@ -86,5 +86,154 @@ void ModelRendererSP::drawPath(QPainter &painter, const cereal::ModelDataV2::Rea
|
|||||||
} else {
|
} else {
|
||||||
// Normal path rendering
|
// Normal path rendering
|
||||||
ModelRenderer::drawPath(painter, model, surface_rect.height());
|
ModelRenderer::drawPath(painter, model, surface_rect.height());
|
||||||
|
|
||||||
|
drawLeadStatus(painter, surface_rect.height(), surface_rect.width());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ModelRendererSP::drawLeadStatus(QPainter &painter, int height, int width) {
|
||||||
|
auto *s = uiState();
|
||||||
|
auto &sm = *(s->sm);
|
||||||
|
|
||||||
|
bool longitudinal_control = sm["carParams"].getCarParams().getOpenpilotLongitudinalControl();
|
||||||
|
if (!longitudinal_control) {
|
||||||
|
lead_status_alpha = std::max(0.0f, lead_status_alpha - 0.05f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sm.alive("radarState")) {
|
||||||
|
lead_status_alpha = std::max(0.0f, lead_status_alpha - 0.05f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &radar_state = sm["radarState"].getRadarState();
|
||||||
|
const auto &lead_one = radar_state.getLeadOne();
|
||||||
|
const auto &lead_two = radar_state.getLeadTwo();
|
||||||
|
|
||||||
|
bool has_lead_one = lead_one.getStatus();
|
||||||
|
bool has_lead_two = lead_two.getStatus();
|
||||||
|
|
||||||
|
if (!has_lead_one && !has_lead_two) {
|
||||||
|
lead_status_alpha = std::max(0.0f, lead_status_alpha - 0.05f);
|
||||||
|
if (lead_status_alpha <= 0.0f) return;
|
||||||
|
} else {
|
||||||
|
lead_status_alpha = std::min(1.0f, lead_status_alpha + 0.1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has_lead_one) {
|
||||||
|
drawLeadStatusAtPosition(painter, lead_one, lead_vertices[0], height, width, "L1");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has_lead_two && std::abs(lead_one.getDRel() - lead_two.getDRel()) > 3.0) {
|
||||||
|
drawLeadStatusAtPosition(painter, lead_two, lead_vertices[1], height, width, "L2");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ModelRendererSP::drawLeadStatusAtPosition(QPainter &painter,
|
||||||
|
const cereal::RadarState::LeadData::Reader &lead_data,
|
||||||
|
const QPointF &chevron_pos,
|
||||||
|
int height, int width,
|
||||||
|
const QString &label) {
|
||||||
|
float d_rel = lead_data.getDRel();
|
||||||
|
float v_rel = lead_data.getVRel();
|
||||||
|
auto *s = uiState();
|
||||||
|
auto &sm = *(s->sm);
|
||||||
|
float v_ego = sm["carState"].getCarState().getVEgo();
|
||||||
|
|
||||||
|
int chevron_data = std::atoi(Params().get("ChevronInfo").c_str());
|
||||||
|
float sz = std::clamp((25 * 30) / (d_rel / 3 + 30), 15.0f, 30.0f) * 2.35;
|
||||||
|
|
||||||
|
QFont content_font = painter.font();
|
||||||
|
content_font.setPixelSize(42);
|
||||||
|
content_font.setBold(true);
|
||||||
|
painter.setFont(content_font);
|
||||||
|
|
||||||
|
bool is_metric = s->scene.is_metric;
|
||||||
|
QStringList text_lines;
|
||||||
|
|
||||||
|
const int chevron_all = 4;
|
||||||
|
QStringList chevron_text[3];
|
||||||
|
|
||||||
|
// Distance display
|
||||||
|
if (chevron_data == 1 || chevron_data == chevron_all) {
|
||||||
|
int pos = 0;
|
||||||
|
float val = std::max(0.0f, d_rel);
|
||||||
|
QString unit = is_metric ? "m" : "ft";
|
||||||
|
if (!is_metric) val *= 3.28084f;
|
||||||
|
chevron_text[pos].append(QString::number(val, 'f', 0) + " " + unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Speed display
|
||||||
|
if (chevron_data == 2 || chevron_data == chevron_all) {
|
||||||
|
int pos = (chevron_data == 2) ? 0 : 1;
|
||||||
|
float multiplier = is_metric ? static_cast<float>(MS_TO_KPH) : static_cast<float>(MS_TO_MPH);
|
||||||
|
float val = std::max(0.0f, (v_rel + v_ego) * multiplier);
|
||||||
|
QString unit = is_metric ? "km/h" : "mph";
|
||||||
|
chevron_text[pos].append(QString::number(val, 'f', 0) + " " + unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time to contact
|
||||||
|
if (chevron_data == 3 || chevron_data == chevron_all) {
|
||||||
|
int pos = (chevron_data == 3) ? 0 : 2;
|
||||||
|
float val = (d_rel > 0 && v_ego > 0) ? std::max(0.0f, d_rel / v_ego) : 0.0f;
|
||||||
|
QString ttc = (val > 0 && val < 200) ? QString::number(val, 'f', 1) + "s" : "---";
|
||||||
|
chevron_text[pos].append(ttc);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
if (!chevron_text[i].isEmpty()) {
|
||||||
|
text_lines.append(chevron_text[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text_lines.isEmpty()) return;
|
||||||
|
|
||||||
|
QFontMetrics fm(content_font);
|
||||||
|
float text_width = 120.0f;
|
||||||
|
for (const QString &line : text_lines) {
|
||||||
|
text_width = std::max(text_width, fm.horizontalAdvance(line) + 20.0f);
|
||||||
|
}
|
||||||
|
text_width = std::min(text_width, 250.0f);
|
||||||
|
|
||||||
|
float line_height = 45.0f;
|
||||||
|
float total_height = text_lines.size() * line_height;
|
||||||
|
float margin = 20.0f;
|
||||||
|
|
||||||
|
float text_y = chevron_pos.y() + sz + 15;
|
||||||
|
if (text_y + total_height > height - margin) {
|
||||||
|
text_y = chevron_pos.y() - sz - 15 - total_height;
|
||||||
|
text_y = std::max(margin, text_y);
|
||||||
|
}
|
||||||
|
|
||||||
|
float text_x = chevron_pos.x() - text_width / 2;
|
||||||
|
text_x = std::clamp(text_x, margin, (float)width - text_width - margin);
|
||||||
|
|
||||||
|
QPoint shadow_offset(2, 2);
|
||||||
|
|
||||||
|
for (int i = 0; i < text_lines.size(); ++i) {
|
||||||
|
float y = text_y + (i * line_height);
|
||||||
|
if (y + line_height > height - margin) break;
|
||||||
|
|
||||||
|
QRect rect(text_x, y, text_width, line_height);
|
||||||
|
|
||||||
|
// Draw shadow
|
||||||
|
painter.setPen(QColor(0, 0, 0, (int)(200 * lead_status_alpha)));
|
||||||
|
painter.drawText(rect.translated(shadow_offset), Qt::AlignCenter, text_lines[i]);
|
||||||
|
|
||||||
|
QColor text_color = QColor(255, 255, 255, (int)(255 * lead_status_alpha));
|
||||||
|
if (text_lines[i].contains("m") || text_lines[i].contains("ft")) {
|
||||||
|
if (d_rel < 20.0f) {
|
||||||
|
text_color = QColor(255, 80, 80, (int)(255 * lead_status_alpha));
|
||||||
|
} else if (d_rel < 40.0f) {
|
||||||
|
text_color = QColor(255, 200, 80, (int)(255 * lead_status_alpha));
|
||||||
|
} else {
|
||||||
|
text_color = QColor(80, 255, 120, (int)(255 * lead_status_alpha));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
painter.setPen(text_color);
|
||||||
|
painter.drawText(rect, Qt::AlignCenter, text_lines[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
painter.setPen(Qt::NoPen);
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,17 @@ private:
|
|||||||
void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead) override;
|
void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead) override;
|
||||||
void drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, const QRect &rect) override;
|
void drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, const QRect &rect) override;
|
||||||
|
|
||||||
|
// Lead status display methods
|
||||||
|
void drawLeadStatus(QPainter &painter, int height, int width);
|
||||||
|
void drawLeadStatusAtPosition(QPainter &painter,
|
||||||
|
const cereal::RadarState::LeadData::Reader &lead_data,
|
||||||
|
const QPointF &chevron_pos,
|
||||||
|
int height, int width,
|
||||||
|
const QString &label);
|
||||||
|
|
||||||
QPolygonF left_blindspot_vertices;
|
QPolygonF left_blindspot_vertices;
|
||||||
QPolygonF right_blindspot_vertices;
|
QPolygonF right_blindspot_vertices;
|
||||||
|
|
||||||
|
// Lead status animation
|
||||||
|
float lead_status_alpha = 0.0f;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -110,3 +110,16 @@ QStringList searchFromList(const QString &query, const QStringList &list) {
|
|||||||
}
|
}
|
||||||
return search_results;
|
return search_results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<cereal::Event::Reader> loadCerealEvent(Params& params, const std::string& _param) {
|
||||||
|
std::string bytes = params.get(_param);
|
||||||
|
|
||||||
|
try {
|
||||||
|
AlignedBuffer aligned_buf;
|
||||||
|
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(bytes.data(), bytes.size()));
|
||||||
|
return cmsg.getRoot<cereal::Event>();
|
||||||
|
} catch (kj::Exception& e) {
|
||||||
|
qInfo() << "invalid " << QString::fromStdString(_param) << ":" << e.getDescription().cStr();
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,8 +15,11 @@
|
|||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||||
|
|
||||||
QString getUserAgent(bool sunnylink = false);
|
QString getUserAgent(bool sunnylink = false);
|
||||||
std::optional<QString> getSunnylinkDongleId();
|
std::optional<QString> getSunnylinkDongleId();
|
||||||
std::optional<QString> getParamIgnoringDefault(const std::string ¶m_name, const std::string &default_value);
|
std::optional<QString> getParamIgnoringDefault(const std::string ¶m_name, const std::string &default_value);
|
||||||
QMap<QString, QVariantMap> loadPlatformList();
|
QMap<QString, QVariantMap> loadPlatformList();
|
||||||
QStringList searchFromList(const QString &query, const QStringList &list);
|
QStringList searchFromList(const QString &query, const QStringList &list);
|
||||||
|
std::optional<cereal::Event::Reader> loadCerealEvent(Params& params, const std::string& _param);
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ def main(demo=False):
|
|||||||
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
|
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
|
||||||
|
|
||||||
# messaging
|
# messaging
|
||||||
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry"])
|
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"])
|
||||||
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"])
|
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"])
|
||||||
|
|
||||||
publish_state = PublishState()
|
publish_state = PublishState()
|
||||||
@@ -304,6 +304,7 @@ def main(demo=False):
|
|||||||
modelv2_send = messaging.new_message('modelV2')
|
modelv2_send = messaging.new_message('modelV2')
|
||||||
drivingdata_send = messaging.new_message('drivingModelData')
|
drivingdata_send = messaging.new_message('drivingModelData')
|
||||||
posenet_send = messaging.new_message('cameraOdometry')
|
posenet_send = messaging.new_message('cameraOdometry')
|
||||||
|
mdv2sp_send = messaging.new_message('modelDataV2SP')
|
||||||
action = model.get_action_from_model(model_output, prev_action, long_delay + DT_MDL)
|
action = model.get_action_from_model(model_output, prev_action, long_delay + DT_MDL)
|
||||||
fill_model_msg(drivingdata_send, modelv2_send, model_output, action, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id,
|
fill_model_msg(drivingdata_send, modelv2_send, model_output, action, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id,
|
||||||
frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen,
|
frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen,
|
||||||
@@ -316,6 +317,7 @@ def main(demo=False):
|
|||||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
|
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
|
||||||
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
||||||
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
||||||
|
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
|
||||||
drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state
|
drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state
|
||||||
drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction
|
drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction
|
||||||
|
|
||||||
@@ -323,6 +325,7 @@ def main(demo=False):
|
|||||||
pm.send('modelV2', modelv2_send)
|
pm.send('modelV2', modelv2_send)
|
||||||
pm.send('drivingModelData', drivingdata_send)
|
pm.send('drivingModelData', drivingdata_send)
|
||||||
pm.send('cameraOdometry', posenet_send)
|
pm.send('cameraOdometry', posenet_send)
|
||||||
|
pm.send('modelDataV2SP', mdv2sp_send)
|
||||||
|
|
||||||
last_vipc_frame_id = meta_main.frame_id
|
last_vipc_frame_id = meta_main.frame_id
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase
|
|||||||
from openpilot.sunnypilot.models.helpers import get_active_bundle
|
from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||||
from openpilot.sunnypilot.models.runners.helpers import get_model_runner
|
from openpilot.sunnypilot.models.runners.helpers import get_model_runner
|
||||||
|
|
||||||
PROCESS_NAME = "selfdrive.modeld.modeld"
|
PROCESS_NAME = "selfdrive.modeld.modeld_tinygrad"
|
||||||
|
|
||||||
|
|
||||||
class FrameMeta:
|
class FrameMeta:
|
||||||
@@ -77,42 +77,47 @@ class ModelState(ModelStateBase):
|
|||||||
self.numpy_inputs[key] = np.zeros(shape, dtype=np.float32)
|
self.numpy_inputs[key] = np.zeros(shape, dtype=np.float32)
|
||||||
# Temporal input: shape is [batch, history, features]
|
# Temporal input: shape is [batch, history, features]
|
||||||
if len(shape) == 3 and shape[1] > 1:
|
if len(shape) == 3 and shape[1] > 1:
|
||||||
buffer_history_len = max(100, (shape[1] * 4 if shape[1] < 100 else shape[1])) # Allow for higher history buffers in the future
|
buffer_history_len = shape[1] * 4 if shape[1] < 99 else shape[1] # Allow for higher history buffers in the future
|
||||||
feature_len = shape[2]
|
feature_len = shape[2]
|
||||||
self.temporal_buffers[key] = np.zeros((1, buffer_history_len, feature_len), dtype=np.float32)
|
|
||||||
features_buffer_shape = self.model_runner.input_shapes.get('features_buffer')
|
features_buffer_shape = self.model_runner.input_shapes.get('features_buffer')
|
||||||
if shape[1] in (24, 25) and features_buffer_shape is not None and features_buffer_shape[1] == 24: # 20Hz
|
if shape[1] in (24, 25) and features_buffer_shape is not None and features_buffer_shape[1] == 24: # 20Hz
|
||||||
|
buffer_history_len = (features_buffer_shape[1] + 1) * 4
|
||||||
step = int(-buffer_history_len / shape[1])
|
step = int(-buffer_history_len / shape[1])
|
||||||
self.temporal_idxs_map[key] = np.arange(step, step * (shape[1] + 1), step)[::-1]
|
self.temporal_idxs_map[key] = np.arange(step, step * (shape[1] + 1), step)[::-1]
|
||||||
elif shape[1] == 25: # Split
|
elif shape[1] == 25: # Split
|
||||||
skip = buffer_history_len // shape[1]
|
skip = buffer_history_len // shape[1]
|
||||||
self.temporal_idxs_map[key] = np.arange(buffer_history_len)[-1 - (skip * (shape[1] - 1))::skip]
|
self.temporal_idxs_map[key] = np.arange(buffer_history_len)[-1 - (skip * (shape[1] - 1))::skip]
|
||||||
elif shape[1] == buffer_history_len: # non20hz
|
elif shape[1] >= 99: # non20hz
|
||||||
self.temporal_idxs_map[key] = np.arange(buffer_history_len)
|
self.temporal_idxs_map[key] = np.arange(shape[1])
|
||||||
|
self.temporal_buffers[key] = np.zeros((1, buffer_history_len, feature_len), dtype=np.float32)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mlsim(self) -> bool:
|
def mlsim(self) -> bool:
|
||||||
return bool(self.generation is not None and self.generation >= 11)
|
return bool(self.generation is not None and self.generation >= 11)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def desire_key(self) -> str:
|
||||||
|
return next(key for key in self.numpy_inputs if key.startswith('desire'))
|
||||||
|
|
||||||
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
||||||
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
|
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
|
||||||
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
||||||
inputs['desire'][0] = 0
|
inputs[self.desire_key][0] = 0
|
||||||
new_desire = np.where(inputs['desire'] - self.prev_desire > .99, inputs['desire'], 0)
|
new_desire = np.where(inputs[self.desire_key] - self.prev_desire > .99, inputs[self.desire_key], 0)
|
||||||
self.prev_desire[:] = inputs['desire']
|
self.prev_desire[:] = inputs[self.desire_key]
|
||||||
self.temporal_buffers['desire'][0,:-1] = self.temporal_buffers['desire'][0,1:]
|
self.temporal_buffers[self.desire_key][0,:-1] = self.temporal_buffers[self.desire_key][0,1:]
|
||||||
self.temporal_buffers['desire'][0,-1] = new_desire
|
self.temporal_buffers[self.desire_key][0,-1] = new_desire
|
||||||
|
|
||||||
# Roll buffer and assign based on desire.shape[1] value
|
# Roll buffer and assign based on desire.shape[1] value
|
||||||
if self.temporal_buffers['desire'].shape[1] > self.numpy_inputs['desire'].shape[1]:
|
if self.temporal_buffers[self.desire_key].shape[1] > self.numpy_inputs[self.desire_key].shape[1]:
|
||||||
skip = self.temporal_buffers['desire'].shape[1] // self.numpy_inputs['desire'].shape[1]
|
skip = self.temporal_buffers[self.desire_key].shape[1] // self.numpy_inputs[self.desire_key].shape[1]
|
||||||
self.numpy_inputs['desire'][:] = (
|
self.numpy_inputs[self.desire_key][:] = (self.temporal_buffers[self.desire_key][0].reshape(
|
||||||
self.temporal_buffers['desire'][0].reshape(self.numpy_inputs['desire'].shape[0], self.numpy_inputs['desire'].shape[1], skip, -1).max(axis=2))
|
self.numpy_inputs[self.desire_key].shape[0], self.numpy_inputs[self.desire_key].shape[1], skip, -1).max(axis=2))
|
||||||
else:
|
else:
|
||||||
self.numpy_inputs['desire'][:] = self.temporal_buffers['desire'][0, self.temporal_idxs_map['desire']]
|
self.numpy_inputs[self.desire_key][:] = self.temporal_buffers[self.desire_key][0, self.temporal_idxs_map[self.desire_key]]
|
||||||
|
|
||||||
for key in self.numpy_inputs:
|
for key in self.numpy_inputs:
|
||||||
if key in inputs and key not in ['desire']:
|
if key in inputs and key not in [self.desire_key]:
|
||||||
self.numpy_inputs[key][:] = inputs[key]
|
self.numpy_inputs[key][:] = inputs[key]
|
||||||
|
|
||||||
imgs_cl = {name: self.frames[name].prepare(bufs[name], transforms[name].flatten()) for name in self.model_runner.vision_input_names}
|
imgs_cl = {name: self.frames[name].prepare(bufs[name], transforms[name].flatten()) for name in self.model_runner.vision_input_names}
|
||||||
@@ -156,10 +161,11 @@ class ModelState(ModelStateBase):
|
|||||||
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS)
|
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS)
|
||||||
|
|
||||||
desired_curvature = get_curvature_from_output(model_output, v_ego, lat_action_t, self.mlsim)
|
desired_curvature = get_curvature_from_output(model_output, v_ego, lat_action_t, self.mlsim)
|
||||||
if v_ego > self.MIN_LAT_CONTROL_SPEED:
|
if self.generation is not None and self.generation >= 10: # smooth curvature for post FOF models
|
||||||
desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, self.LAT_SMOOTH_SECONDS)
|
if v_ego > self.MIN_LAT_CONTROL_SPEED:
|
||||||
else:
|
desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, self.LAT_SMOOTH_SECONDS)
|
||||||
desired_curvature = prev_action.desiredCurvature
|
else:
|
||||||
|
desired_curvature = prev_action.desiredCurvature
|
||||||
|
|
||||||
return log.ModelDataV2.Action(desiredCurvature=float(desired_curvature),desiredAcceleration=float(desired_accel), shouldStop=bool(should_stop))
|
return log.ModelDataV2.Action(desiredCurvature=float(desired_curvature),desiredAcceleration=float(desired_accel), shouldStop=bool(should_stop))
|
||||||
|
|
||||||
@@ -202,7 +208,7 @@ def main(demo=False):
|
|||||||
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
|
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
|
||||||
|
|
||||||
# messaging
|
# messaging
|
||||||
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry"])
|
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"])
|
||||||
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"])
|
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"])
|
||||||
|
|
||||||
publish_state = PublishState()
|
publish_state = PublishState()
|
||||||
@@ -306,7 +312,7 @@ def main(demo=False):
|
|||||||
bufs = {name: buf_extra if 'big' in name else buf_main for name in model.model_runner.vision_input_names}
|
bufs = {name: buf_extra if 'big' in name else buf_main for name in model.model_runner.vision_input_names}
|
||||||
transforms = {name: model_transform_extra if 'big' in name else model_transform_main for name in model.model_runner.vision_input_names}
|
transforms = {name: model_transform_extra if 'big' in name else model_transform_main for name in model.model_runner.vision_input_names}
|
||||||
inputs:dict[str, np.ndarray] = {
|
inputs:dict[str, np.ndarray] = {
|
||||||
'desire': vec_desire,
|
model.desire_key: vec_desire,
|
||||||
'traffic_convention': traffic_convention,
|
'traffic_convention': traffic_convention,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,6 +328,7 @@ def main(demo=False):
|
|||||||
modelv2_send = messaging.new_message('modelV2')
|
modelv2_send = messaging.new_message('modelV2')
|
||||||
drivingdata_send = messaging.new_message('drivingModelData')
|
drivingdata_send = messaging.new_message('drivingModelData')
|
||||||
posenet_send = messaging.new_message('cameraOdometry')
|
posenet_send = messaging.new_message('cameraOdometry')
|
||||||
|
mdv2sp_send = messaging.new_message('modelDataV2SP')
|
||||||
|
|
||||||
action = model.get_action_from_model(model_output, prev_action, lat_delay + DT_MDL, long_delay + DT_MDL, v_ego)
|
action = model.get_action_from_model(model_output, prev_action, lat_delay + DT_MDL, long_delay + DT_MDL, v_ego)
|
||||||
prev_action = action
|
prev_action = action
|
||||||
@@ -336,6 +343,7 @@ def main(demo=False):
|
|||||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
|
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
|
||||||
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
||||||
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
||||||
|
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
|
||||||
drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state
|
drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state
|
||||||
drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction
|
drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction
|
||||||
|
|
||||||
@@ -343,6 +351,7 @@ def main(demo=False):
|
|||||||
pm.send('modelV2', modelv2_send)
|
pm.send('modelV2', modelv2_send)
|
||||||
pm.send('drivingModelData', drivingdata_send)
|
pm.send('drivingModelData', drivingdata_send)
|
||||||
pm.send('cameraOdometry', posenet_send)
|
pm.send('cameraOdometry', posenet_send)
|
||||||
|
pm.send('modelDataV2SP', mdv2sp_send)
|
||||||
last_vipc_frame_id = meta_main.frame_id
|
last_vipc_frame_id = meta_main.frame_id
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,16 @@ import openpilot.sunnypilot.modeld_v2.modeld as modeld_module
|
|||||||
|
|
||||||
ModelState = modeld_module.ModelState
|
ModelState = modeld_module.ModelState
|
||||||
|
|
||||||
|
|
||||||
# These are the shapes extracted/loaded from the model onnx
|
# These are the shapes extracted/loaded from the model onnx
|
||||||
SHAPE_MODE_PARAMS = [
|
SHAPE_MODE_PARAMS = [
|
||||||
({'desire': (1, 25, 8), 'features_buffer': (1, 25, 512), 'prev_desired_curv': (1, 25, 1)}, 'split'),
|
({'desire': (1, 100, 8), 'features_buffer': (1, 99, 512), "nav_features": (1, 256), "nav_instructions": (1, 150)}, 'non20hz'), # Optimus Prime
|
||||||
({'desire': (1, 25, 8), 'features_buffer': (1, 24, 512), 'prev_desired_curv': (1, 25, 1)}, '20hz'),
|
({'desire': (1, 100, 8), 'features_buffer': (1, 99, 512), "lat_planner_state": (1, 4),}, 'non20hz'), # farmville
|
||||||
({'desire': (1, 100, 8), 'features_buffer': (1, 99, 512), 'prev_desired_curv': (1, 100, 1)}, 'non20hz'),
|
({'desire': (1, 100, 8), 'features_buffer': (1, 99, 512), "lateral_control_params": (1, 2), "prev_desired_curv": (1, 100, 1)}, 'non20hz'), # wd40
|
||||||
|
({'desire': (1, 100, 8), 'features_buffer': (1, 99, 512), 'prev_desired_curv': (1, 100, 1), "lateral_control_params": (1, 2),}, 'non20hz'), # NTS
|
||||||
|
({'desire': (1, 25, 8), 'features_buffer': (1, 24, 512)}, '20hz'), # NPR
|
||||||
|
({'desire': (1, 100, 8), 'features_buffer': (1, 99, 512), 'prev_desired_curv': (1, 100, 1), "lateral_control_params": (1, 2),}, 'non20hz'), # NTS
|
||||||
|
({'desire': (1, 25, 8), 'features_buffer': (1, 25, 512)}, 'split'), # Steam Powered v2
|
||||||
|
({'desire_pulse': (1, 25, 8), 'features_buffer': (1, 25, 512)}, 'split'), # desire rename
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -95,9 +99,7 @@ def get_expected_indices(shape, constants, mode, key=None):
|
|||||||
idxs = np.arange(step_size, step_size * (num_elements + 1), step_size)[::-1]
|
idxs = np.arange(step_size, step_size * (num_elements + 1), step_size)[::-1]
|
||||||
return idxs
|
return idxs
|
||||||
elif mode == 'non20hz':
|
elif mode == 'non20hz':
|
||||||
if key and shape[1] == constants.FULL_HISTORY_BUFFER_LEN:
|
return np.arange(shape[1])
|
||||||
return np.arange(constants.FULL_HISTORY_BUFFER_LEN)
|
|
||||||
return None
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -108,6 +110,8 @@ def test_buffer_shapes_and_indices(shapes, mode, apply_patches):
|
|||||||
for key in shapes:
|
for key in shapes:
|
||||||
buf = state.temporal_buffers.get(key, None)
|
buf = state.temporal_buffers.get(key, None)
|
||||||
idxs = state.temporal_idxs_map.get(key, None)
|
idxs = state.temporal_idxs_map.get(key, None)
|
||||||
|
if buf is None:
|
||||||
|
continue # not all shapes are 3D, and the non-3D ones are not buffered
|
||||||
# Buffer shape logic
|
# Buffer shape logic
|
||||||
if mode == 'split':
|
if mode == 'split':
|
||||||
expected_shape = (1, constants.FULL_HISTORY_BUFFER_LEN, shapes[key][2])
|
expected_shape = (1, constants.FULL_HISTORY_BUFFER_LEN, shapes[key][2])
|
||||||
@@ -116,10 +120,7 @@ def test_buffer_shapes_and_indices(shapes, mode, apply_patches):
|
|||||||
expected_shape = (1, constants.FULL_HISTORY_BUFFER_LEN, shapes[key][2])
|
expected_shape = (1, constants.FULL_HISTORY_BUFFER_LEN, shapes[key][2])
|
||||||
expected_idxs = get_expected_indices(shapes[key], constants, '20hz', key)
|
expected_idxs = get_expected_indices(shapes[key], constants, '20hz', key)
|
||||||
elif mode == 'non20hz':
|
elif mode == 'non20hz':
|
||||||
if key == 'features_buffer':
|
expected_shape = (1, shapes[key][1], shapes[key][2])
|
||||||
expected_shape = (1, shapes[key][1]*4, shapes[key][2])
|
|
||||||
else:
|
|
||||||
expected_shape = (1, shapes[key][1], shapes[key][2])
|
|
||||||
expected_idxs = get_expected_indices(shapes[key], constants, 'non20hz', key)
|
expected_idxs = get_expected_indices(shapes[key], constants, 'non20hz', key)
|
||||||
|
|
||||||
assert buf is not None, f"{key}: buffer not found"
|
assert buf is not None, f"{key}: buffer not found"
|
||||||
@@ -130,10 +131,10 @@ def test_buffer_shapes_and_indices(shapes, mode, apply_patches):
|
|||||||
assert idxs is None or idxs.size == 0, f"{key}: buffer idxs should be None or empty"
|
assert idxs is None or idxs.size == 0, f"{key}: buffer idxs should be None or empty"
|
||||||
|
|
||||||
|
|
||||||
def legacy_buffer_update(buf, new_val, mode, key, constants, idxs):
|
def legacy_buffer_update(buf, new_val, mode, key, constants, idxs, input_shape, prev_desire=None):
|
||||||
# This is what we compare the new dynamic logic to, to ensure it does the same thing
|
# This is what we compare the new dynamic logic to, to ensure it does the same thing
|
||||||
if mode == 'split':
|
if mode == 'split':
|
||||||
if key == 'desire':
|
if key == 'desire' or key.startswith('desire'):
|
||||||
buf[0,:-1] = buf[0,1:]
|
buf[0,:-1] = buf[0,1:]
|
||||||
buf[0,-1] = new_val
|
buf[0,-1] = new_val
|
||||||
return buf.reshape((1, constants.INPUT_HISTORY_BUFFER_LEN, constants.TEMPORAL_SKIP, -1)).max(axis=2)
|
return buf.reshape((1, constants.INPUT_HISTORY_BUFFER_LEN, constants.TEMPORAL_SKIP, -1)).max(axis=2)
|
||||||
@@ -173,15 +174,22 @@ def legacy_buffer_update(buf, new_val, mode, key, constants, idxs):
|
|||||||
return legacy_buf[idxs]
|
return legacy_buf[idxs]
|
||||||
elif mode == 'non20hz':
|
elif mode == 'non20hz':
|
||||||
if key == 'desire':
|
if key == 'desire':
|
||||||
length = new_val.shape[0]
|
desire_len = constants.DESIRE_LEN
|
||||||
buf[0,:-1,:length] = buf[0,1:,:length]
|
if prev_desire is None:
|
||||||
buf[0,-1,:length] = new_val[:length]
|
prev_desire = np.zeros(desire_len, dtype=np.float32)
|
||||||
|
# Set first element to zero
|
||||||
|
new_val = new_val.copy()
|
||||||
|
new_val[0] = 0
|
||||||
|
# Shift buffer by desire len
|
||||||
|
buf[0][:-desire_len] = buf[0][desire_len:]
|
||||||
|
# Only insert new desire if rising edge
|
||||||
|
buf[0][-desire_len:] = np.where(new_val - prev_desire > 0.99, new_val, 0)
|
||||||
|
prev_desire[:] = new_val
|
||||||
return buf[0]
|
return buf[0]
|
||||||
elif key == 'features_buffer':
|
elif key == 'features_buffer':
|
||||||
feature_len = new_val.shape[0]
|
buf[0, :-1] = buf[0, 1:]
|
||||||
buf[0,:-1,:feature_len] = buf[0,1:,:feature_len]
|
buf[0, -1] = new_val
|
||||||
buf[0,-1,:feature_len] = new_val[:feature_len]
|
return buf[0, -input_shape[1]:] # (99, 512)
|
||||||
return buf[0]
|
|
||||||
elif key == 'prev_desired_curv':
|
elif key == 'prev_desired_curv':
|
||||||
length = new_val.shape[0]
|
length = new_val.shape[0]
|
||||||
buf[0,:-length,0] = buf[0,length:,0]
|
buf[0,:-length,0] = buf[0,length:,0]
|
||||||
@@ -191,32 +199,18 @@ def legacy_buffer_update(buf, new_val, mode, key, constants, idxs):
|
|||||||
|
|
||||||
|
|
||||||
def dynamic_buffer_update(state, key, new_val, mode):
|
def dynamic_buffer_update(state, key, new_val, mode):
|
||||||
if key == 'desire':
|
if key == 'desire' or key.startswith('desire'):
|
||||||
state.temporal_buffers['desire'][0,:-1] = state.temporal_buffers['desire'][0,1:]
|
inputs = {k: np.zeros(v[2], dtype=np.float32) if len(v) == 3 else np.zeros(v[1], dtype=np.float32)
|
||||||
state.temporal_buffers['desire'][0,-1] = new_val
|
for k, v in state.model_runner.input_shapes.items() if k != key}
|
||||||
if state.temporal_buffers['desire'].shape[1] > state.numpy_inputs['desire'].shape[1]:
|
inputs[key] = new_val.copy()
|
||||||
skip = state.temporal_buffers['desire'].shape[1] // state.numpy_inputs['desire'].shape[1]
|
# ModelState.run expects desire as a pulse, so we zero the first element.
|
||||||
return state.temporal_buffers['desire'][0].reshape(
|
inputs[key][0] = 0
|
||||||
state.numpy_inputs['desire'].shape[0], state.numpy_inputs['desire'].shape[1], skip, -1
|
state.run({}, {}, inputs, prepare_only=False)
|
||||||
).max(axis=2)
|
return state.numpy_inputs[key]
|
||||||
else:
|
|
||||||
return state.temporal_buffers['desire'][0, state.temporal_idxs_map['desire']]
|
|
||||||
|
|
||||||
inputs = {'desire': np.zeros((1, state.constants.DESIRE_LEN), dtype=np.float32)}
|
|
||||||
for k, tb in state.temporal_buffers.items():
|
|
||||||
if k in state.temporal_idxs_map:
|
|
||||||
continue
|
|
||||||
buf_len = tb.shape[1]
|
|
||||||
if k in state.numpy_inputs:
|
|
||||||
out_len = state.numpy_inputs[k].shape[1]
|
|
||||||
if out_len <= buf_len:
|
|
||||||
state.temporal_idxs_map[k] = np.arange(buf_len)[-out_len:]
|
|
||||||
else:
|
|
||||||
state.temporal_idxs_map[k] = np.arange(buf_len)
|
|
||||||
else:
|
|
||||||
state.temporal_idxs_map[k] = np.arange(buf_len)
|
|
||||||
|
|
||||||
if key == 'features_buffer':
|
if key == 'features_buffer':
|
||||||
|
inputs = {k: np.zeros(v[2], dtype=np.float32) if len(v) == 3 else np.zeros(v[1], dtype=np.float32)
|
||||||
|
for k, v in state.model_runner.input_shapes.items() if k != 'features_buffer'}
|
||||||
def run_model_stub():
|
def run_model_stub():
|
||||||
return {
|
return {
|
||||||
'hidden_state': np.asarray(new_val, dtype=np.float32).reshape(1, -1),
|
'hidden_state': np.asarray(new_val, dtype=np.float32).reshape(1, -1),
|
||||||
@@ -226,6 +220,8 @@ def dynamic_buffer_update(state, key, new_val, mode):
|
|||||||
return state.numpy_inputs['features_buffer'][0]
|
return state.numpy_inputs['features_buffer'][0]
|
||||||
|
|
||||||
if key == 'prev_desired_curv':
|
if key == 'prev_desired_curv':
|
||||||
|
inputs = {k: np.zeros(v[2], dtype=np.float32) if len(v) == 3 else np.zeros(v[1], dtype=np.float32)
|
||||||
|
for k, v in state.model_runner.input_shapes.items() if k != 'prev_desired_curv'}
|
||||||
def run_model_stub():
|
def run_model_stub():
|
||||||
return {
|
return {
|
||||||
'hidden_state': np.zeros((1, state.constants.FEATURE_LEN), dtype=np.float32),
|
'hidden_state': np.zeros((1, state.constants.FEATURE_LEN), dtype=np.float32),
|
||||||
@@ -241,16 +237,27 @@ def dynamic_buffer_update(state, key, new_val, mode):
|
|||||||
@pytest.mark.parametrize("key", ["desire", "features_buffer", "prev_desired_curv"])
|
@pytest.mark.parametrize("key", ["desire", "features_buffer", "prev_desired_curv"])
|
||||||
def test_buffer_update_equivalence(shapes, mode, key, apply_patches):
|
def test_buffer_update_equivalence(shapes, mode, key, apply_patches):
|
||||||
state = ModelState(None)
|
state = ModelState(None)
|
||||||
|
if key == "desire":
|
||||||
|
desire_keys = [k for k in shapes.keys() if k.startswith('desire')]
|
||||||
|
if desire_keys:
|
||||||
|
actual_key = desire_keys[0] # Use the first (and likely only) desire key
|
||||||
|
else:
|
||||||
|
actual_key = key
|
||||||
|
|
||||||
|
if actual_key not in state.numpy_inputs:
|
||||||
|
pytest.skip()
|
||||||
|
|
||||||
constants = DummyModelRunner(shapes).constants
|
constants = DummyModelRunner(shapes).constants
|
||||||
buf = state.temporal_buffers.get(key, None)
|
buf = state.temporal_buffers.get(actual_key, None)
|
||||||
idxs = state.temporal_idxs_map.get(key, None)
|
idxs = state.temporal_idxs_map.get(actual_key, None)
|
||||||
input_shape = shapes[key]
|
input_shape = shapes[actual_key]
|
||||||
|
prev_desire = np.zeros(constants.DESIRE_LEN, dtype=np.float32) if key == 'desire' else None
|
||||||
|
|
||||||
for step in range(20): # multiple steps to ensure history is built up
|
for step in range(20): # multiple steps to ensure history is built up
|
||||||
new_val = np.full((input_shape[2],), step, dtype=np.float32)
|
new_val = np.full((input_shape[2],), step, dtype=np.float32)
|
||||||
expected = legacy_buffer_update(buf, new_val, mode, key, constants, idxs)
|
expected = legacy_buffer_update(buf, new_val, mode, actual_key, constants, idxs, input_shape, prev_desire)
|
||||||
actual = dynamic_buffer_update(state, key, new_val, mode)
|
actual = dynamic_buffer_update(state, actual_key, new_val, mode)
|
||||||
# Model returns the reduced numpy_inputs history, compare the last n entries so the test is checking the same slices.
|
|
||||||
if expected is not None and actual is not None and expected.shape != actual.shape:
|
if expected is not None and actual is not None and expected.shape != actual.shape:
|
||||||
if expected.ndim == 2 and actual.ndim == 2 and expected.shape[1] == actual.shape[1]:
|
if expected.ndim == 2 and actual.ndim == 2 and expected.shape[1] == actual.shape[1]:
|
||||||
expected = expected[-actual.shape[0]:]
|
expected = expected[-actual.shape[0]:]
|
||||||
assert np.allclose(actual, expected), f"{mode} {key}: dynamic buffer update does not match legacy logic"
|
assert np.allclose(actual, expected), f"{mode} {actual_key}: dynamic buffer update does not match legacy logic"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ See the LICENSE.md file in the root directory for more details.
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
from requests.exceptions import (SSLError, RequestException, HTTPError)
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.common.swaglog import cloudlog
|
from openpilot.common.swaglog import cloudlog
|
||||||
from sunnypilot.models.helpers import is_bundle_version_compatible
|
from sunnypilot.models.helpers import is_bundle_version_compatible
|
||||||
@@ -122,19 +123,36 @@ class ModelFetcher:
|
|||||||
self.model_cache = ModelCache(params)
|
self.model_cache = ModelCache(params)
|
||||||
self.model_parser = ModelParser()
|
self.model_parser = ModelParser()
|
||||||
|
|
||||||
def _fetch_and_cache_models(self) -> list[custom.ModelManagerSP.ModelBundle]:
|
def _fetch_and_cache_models(self) -> list[custom.ModelManagerSP.ModelBundle] | None:
|
||||||
"""Fetches fresh model data from remote and updates cache"""
|
"""Fetches fresh model data from remote and updates cache.
|
||||||
|
Returns None on transport errors. Raises on 404 and other fatal HTTP errors.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
response = requests.get(self.MODEL_URL, timeout=10)
|
response = requests.get(self.MODEL_URL, timeout=10)
|
||||||
response.raise_for_status()
|
|
||||||
json_data = response.json()
|
|
||||||
|
|
||||||
|
# Explicitly handle 404 differently
|
||||||
|
if response.status_code == 404:
|
||||||
|
cloudlog.error(f"Models URL returned 404 Not Found: {self.MODEL_URL}")
|
||||||
|
raise HTTPError(f"404 Not Found: {self.MODEL_URL}", response=response)
|
||||||
|
|
||||||
|
# Raise for any other 4xx/5xx
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
json_data = response.json()
|
||||||
self.model_cache.set(json_data)
|
self.model_cache.set(json_data)
|
||||||
cloudlog.debug("Successfully updated models cache")
|
cloudlog.debug("Successfully updated models cache")
|
||||||
return self.model_parser.parse_models(json_data)
|
return self.model_parser.parse_models(json_data)
|
||||||
except Exception:
|
|
||||||
cloudlog.exception("Error fetching models")
|
except ConnectionError as e:
|
||||||
raise
|
cloudlog.warning(f"DNS/connection error while fetching models: {e}")
|
||||||
|
except SSLError as e:
|
||||||
|
cloudlog.warning(f"SSL error while fetching models: {e}")
|
||||||
|
except RequestException as e:
|
||||||
|
cloudlog.warning(f"Request transport error while fetching models: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
cloudlog.exception(f"Unexpected error fetching models: {e}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def get_available_bundles(self) -> list[custom.ModelManagerSP.ModelBundle]:
|
def get_available_bundles(self) -> list[custom.ModelManagerSP.ModelBundle]:
|
||||||
"""Gets the list of available models, with smart cache handling"""
|
"""Gets the list of available models, with smart cache handling"""
|
||||||
@@ -144,12 +162,12 @@ class ModelFetcher:
|
|||||||
cloudlog.debug("Using valid cached models data")
|
cloudlog.debug("Using valid cached models data")
|
||||||
return self.model_parser.parse_models(cached_data)
|
return self.model_parser.parse_models(cached_data)
|
||||||
|
|
||||||
try:
|
fetched_bundles = self._fetch_and_cache_models()
|
||||||
return self._fetch_and_cache_models()
|
if fetched_bundles is not None:
|
||||||
except Exception:
|
return fetched_bundles
|
||||||
if not cached_data:
|
|
||||||
cloudlog.exception("Failed to fetch fresh data and no cache available")
|
if not cached_data:
|
||||||
raise
|
cloudlog.warning("Failed to fetch fresh data and no cache available")
|
||||||
|
|
||||||
cloudlog.warning("Failed to fetch fresh data. Using expired cache as fallback")
|
cloudlog.warning("Failed to fetch fresh data. Using expired cache as fallback")
|
||||||
return self.model_parser.parse_models(cached_data)
|
return self.model_parser.parse_models(cached_data)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from openpilot.system.hardware.hw import Paths
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# see the README.md for more details on the model selector versioning
|
# see the README.md for more details on the model selector versioning
|
||||||
CURRENT_SELECTOR_VERSION = 9
|
CURRENT_SELECTOR_VERSION = 10
|
||||||
REQUIRED_MIN_SELECTOR_VERSION = 9
|
REQUIRED_MIN_SELECTOR_VERSION = 9
|
||||||
|
|
||||||
USE_ONNX = os.getenv('USE_ONNX', PC)
|
USE_ONNX = os.getenv('USE_ONNX', PC)
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
from cereal import custom
|
||||||
|
|
||||||
|
from openpilot.common.constants import CV
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
|
||||||
|
LANE_CHANGE_SPEED_MIN = 20 * CV.MPH_TO_MS
|
||||||
|
|
||||||
|
|
||||||
|
class LaneTurnController:
|
||||||
|
def __init__(self, desire_helper):
|
||||||
|
self.DH = desire_helper
|
||||||
|
self.turn_direction = custom.TurnDirection.none
|
||||||
|
self.params = Params()
|
||||||
|
self.lane_turn_value = float(self.params.get("LaneTurnValue", return_default=True)) * CV.MPH_TO_MS
|
||||||
|
self.param_read_counter = 0
|
||||||
|
self.enabled = self.params.get_bool("LaneTurnDesire")
|
||||||
|
|
||||||
|
def read_params(self):
|
||||||
|
self.enabled = self.params.get_bool("LaneTurnDesire")
|
||||||
|
value = float(self.params.get("LaneTurnValue", return_default=True)) * CV.MPH_TO_MS
|
||||||
|
self.lane_turn_value = min(float(LANE_CHANGE_SPEED_MIN), value)
|
||||||
|
|
||||||
|
def update_params(self) -> None:
|
||||||
|
if self.param_read_counter % 50 == 0:
|
||||||
|
self.read_params()
|
||||||
|
self.param_read_counter += 1
|
||||||
|
|
||||||
|
def update_lane_turn(self, blindspot_left: bool, blindspot_right: bool, left_blinker: bool, right_blinker: bool, v_ego: float) -> None:
|
||||||
|
if left_blinker and not right_blinker and v_ego < self.lane_turn_value and not blindspot_left:
|
||||||
|
self.turn_direction = custom.TurnDirection.turnLeft
|
||||||
|
elif right_blinker and not left_blinker and v_ego < self.lane_turn_value and not blindspot_right:
|
||||||
|
self.turn_direction = custom.TurnDirection.turnRight
|
||||||
|
else:
|
||||||
|
self.turn_direction = custom.TurnDirection.none
|
||||||
|
|
||||||
|
def get_turn_direction(self):
|
||||||
|
if not self.enabled:
|
||||||
|
return custom.TurnDirection.none
|
||||||
|
return self.turn_direction
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import pytest
|
||||||
|
from cereal import log
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
|
||||||
|
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController, LANE_CHANGE_SPEED_MIN
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeMode
|
||||||
|
|
||||||
|
|
||||||
|
class TurnDirection:
|
||||||
|
none = 0
|
||||||
|
turnLeft = 1
|
||||||
|
turnRight = 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("left_blinker,right_blinker,v_ego,blindspot_left,blindspot_right,expected", [
|
||||||
|
(True, False, 5, False, False, TurnDirection.turnLeft),
|
||||||
|
(False, True, 6, False, False, TurnDirection.turnRight),
|
||||||
|
(True, False, 9, False, False, TurnDirection.none),
|
||||||
|
(True, False, 7, True, False, TurnDirection.none),
|
||||||
|
(False, True, 6, False, True, TurnDirection.none),
|
||||||
|
(False, False, 5, False, False, TurnDirection.none),
|
||||||
|
(True, True, 5, False, False, TurnDirection.none),
|
||||||
|
])
|
||||||
|
def test_lane_turn_desire_conditions(left_blinker, right_blinker, v_ego, blindspot_left, blindspot_right, expected):
|
||||||
|
dh = DesireHelper()
|
||||||
|
controller = LaneTurnController(dh)
|
||||||
|
controller.enabled = True
|
||||||
|
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
|
||||||
|
controller.turn_direction = TurnDirection.none
|
||||||
|
controller.update_lane_turn(blindspot_left, blindspot_right, left_blinker, right_blinker, v_ego)
|
||||||
|
assert controller.get_turn_direction() == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_lane_turn_desire_disabled():
|
||||||
|
dh = DesireHelper()
|
||||||
|
controller = LaneTurnController(dh)
|
||||||
|
controller.enabled = False
|
||||||
|
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
|
||||||
|
controller.turn_direction = TurnDirection.none
|
||||||
|
controller.update_lane_turn(False, False, True, False, 7)
|
||||||
|
assert controller.get_turn_direction() == TurnDirection.none
|
||||||
|
|
||||||
|
|
||||||
|
def test_lane_turn_overrides_lane_change():
|
||||||
|
dh = DesireHelper()
|
||||||
|
controller = LaneTurnController(dh)
|
||||||
|
controller.enabled = True
|
||||||
|
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
|
||||||
|
controller.turn_direction = TurnDirection.none
|
||||||
|
# left turn desire
|
||||||
|
controller.update_lane_turn(False, False, True, False, 5)
|
||||||
|
assert controller.get_turn_direction() == TurnDirection.turnLeft
|
||||||
|
# right turn desire
|
||||||
|
controller.update_lane_turn(False, False, False, True, 6)
|
||||||
|
assert controller.get_turn_direction() == TurnDirection.turnRight
|
||||||
|
# no turn
|
||||||
|
controller.update_lane_turn(False, False, False, False, 7)
|
||||||
|
assert controller.get_turn_direction() == TurnDirection.none
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("v_ego,expected", [
|
||||||
|
(8.93, TurnDirection.turnLeft), # just below threshold
|
||||||
|
(8.96, TurnDirection.none), # above threshold
|
||||||
|
(8.95, TurnDirection.none), # just above threshold
|
||||||
|
])
|
||||||
|
def test_lane_turn_desire_speed_boundary(v_ego, expected):
|
||||||
|
dh = DesireHelper()
|
||||||
|
controller = LaneTurnController(dh)
|
||||||
|
controller.enabled = True
|
||||||
|
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
|
||||||
|
controller.turn_direction = TurnDirection.none
|
||||||
|
controller.update_lane_turn(False, True, True, False, v_ego)
|
||||||
|
assert controller.get_turn_direction() == expected
|
||||||
|
|
||||||
|
|
||||||
|
class DummyCarState:
|
||||||
|
def __init__(self, vEgo=0, leftBlinker=False, rightBlinker=False, leftBlindspot=False, rightBlindspot=False,
|
||||||
|
steeringPressed=False, steeringTorque=0, brakePressed=False):
|
||||||
|
self.vEgo = vEgo
|
||||||
|
self.leftBlinker = leftBlinker
|
||||||
|
self.rightBlinker = rightBlinker
|
||||||
|
self.leftBlindspot = leftBlindspot
|
||||||
|
self.rightBlindspot = rightBlindspot
|
||||||
|
self.steeringPressed = steeringPressed
|
||||||
|
self.steeringTorque = steeringTorque
|
||||||
|
self.brakePressed = brakePressed
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def set_lane_turn_params():
|
||||||
|
params = Params()
|
||||||
|
params.put("LaneTurnDesire", True)
|
||||||
|
params.put("LaneTurnValue", 20.0)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("carstate, lateral_active, lane_change_prob, expected_desire", [
|
||||||
|
# Lane turn desire overrides lane change desire
|
||||||
|
(DummyCarState(vEgo=5, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False), True, 1.0, log.Desire.turnLeft),
|
||||||
|
(DummyCarState(vEgo=7, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False), True, 1.0, log.Desire.turnRight),
|
||||||
|
# Lane change desire only (no turn desires)
|
||||||
|
(DummyCarState(vEgo=9, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False,
|
||||||
|
steeringPressed=True, steeringTorque=1), True, 1.0, log.Desire.laneChangeLeft),
|
||||||
|
(DummyCarState(vEgo=9, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False,
|
||||||
|
steeringPressed=True, steeringTorque=-1), True, 1.0, log.Desire.laneChangeRight),
|
||||||
|
# No desire (inactive)
|
||||||
|
(DummyCarState(vEgo=9, leftBlinker=False, rightBlinker=False), False, 1.0, log.Desire.none),
|
||||||
|
(DummyCarState(vEgo=4, leftBlinker=False, rightBlinker=False), True, 1.0, log.Desire.none), # No blinkers? no desire!
|
||||||
|
])
|
||||||
|
def test_desire_helper_integration(carstate, lateral_active, lane_change_prob, expected_desire, set_lane_turn_params):
|
||||||
|
dh = DesireHelper()
|
||||||
|
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
|
||||||
|
for _ in range(10):
|
||||||
|
dh.update(carstate, lateral_active, lane_change_prob)
|
||||||
|
assert dh.desire == expected_desire # The first four tests were unit tests to test the controller, where this tests the integration in desire helpers
|
||||||
@@ -132,6 +132,21 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
|||||||
|
|
||||||
EventNameSP.pedalPressedAlertOnly: {
|
EventNameSP.pedalPressedAlertOnly: {
|
||||||
ET.WARNING: NoEntryAlert("Pedal Pressed")
|
ET.WARNING: NoEntryAlert("Pedal Pressed")
|
||||||
}
|
},
|
||||||
|
|
||||||
|
EventNameSP.laneTurnLeft: {
|
||||||
|
ET.WARNING: Alert(
|
||||||
|
"Turning Left",
|
||||||
|
"",
|
||||||
|
AlertStatus.normal, AlertSize.small,
|
||||||
|
Priority.LOW, VisualAlert.none, AudibleAlert.none, 1.),
|
||||||
|
},
|
||||||
|
|
||||||
|
EventNameSP.laneTurnRight: {
|
||||||
|
ET.WARNING: Alert(
|
||||||
|
"Turning Right",
|
||||||
|
"",
|
||||||
|
AlertStatus.normal, AlertSize.small,
|
||||||
|
Priority.LOW, VisualAlert.none, AudibleAlert.none, 1.),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
import errno
|
||||||
import gzip
|
import gzip
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import ssl
|
import ssl
|
||||||
import threading
|
import threading
|
||||||
@@ -17,11 +19,11 @@ from openpilot.common.swaglog import cloudlog
|
|||||||
from openpilot.system.athena.athenad import ws_send, jsonrpc_handler, \
|
from openpilot.system.athena.athenad import ws_send, jsonrpc_handler, \
|
||||||
recv_queue, UploadQueueCache, upload_queue, cur_upload_items, backoff, ws_manage, log_handler, start_local_proxy_shim, upload_handler
|
recv_queue, UploadQueueCache, upload_queue, cur_upload_items, backoff, ws_manage, log_handler, start_local_proxy_shim, upload_handler
|
||||||
from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutException,
|
from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutException,
|
||||||
create_connection)
|
create_connection, WebSocketConnectionClosedException)
|
||||||
|
|
||||||
import cereal.messaging as messaging
|
import cereal.messaging as messaging
|
||||||
from sunnypilot.sunnylink.api import SunnylinkApi
|
from sunnypilot.sunnylink.api import SunnylinkApi
|
||||||
from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready
|
from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte
|
||||||
|
|
||||||
SUNNYLINK_ATHENA_HOST = os.getenv('SUNNYLINK_ATHENA_HOST', 'wss://ws.stg.api.sunnypilot.ai')
|
SUNNYLINK_ATHENA_HOST = os.getenv('SUNNYLINK_ATHENA_HOST', 'wss://ws.stg.api.sunnypilot.ai')
|
||||||
HANDLER_THREADS = int(os.getenv('HANDLER_THREADS', "4"))
|
HANDLER_THREADS = int(os.getenv('HANDLER_THREADS', "4"))
|
||||||
@@ -48,7 +50,7 @@ def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None:
|
|||||||
threading.Thread(target=ws_ping, args=(ws, end_event), name='ws_ping'),
|
threading.Thread(target=ws_ping, args=(ws, end_event), name='ws_ping'),
|
||||||
threading.Thread(target=ws_queue, args=(end_event,), name='ws_queue'),
|
threading.Thread(target=ws_queue, args=(end_event,), name='ws_queue'),
|
||||||
threading.Thread(target=upload_handler, args=(end_event,), name='upload_handler'),
|
threading.Thread(target=upload_handler, args=(end_event,), name='upload_handler'),
|
||||||
# threading.Thread(target=sunny_log_handler, args=(end_event, comma_prime_cellular_end_event), name='log_handler'),
|
threading.Thread(target=sunny_log_handler, args=(end_event, comma_prime_cellular_end_event), name='log_handler'),
|
||||||
# threading.Thread(target=stat_handler, args=(end_event,), name='stat_handler'),
|
# threading.Thread(target=stat_handler, args=(end_event,), name='stat_handler'),
|
||||||
] + [
|
] + [
|
||||||
threading.Thread(target=jsonrpc_handler, args=(end_event, partial(startLocalProxy, end_event),), name=f'worker_{x}')
|
threading.Thread(target=jsonrpc_handler, args=(end_event, partial(startLocalProxy, end_event),), name=f'worker_{x}')
|
||||||
@@ -107,10 +109,13 @@ def ws_recv(ws: WebSocket, end_event: threading.Event) -> None:
|
|||||||
except WebSocketTimeoutException:
|
except WebSocketTimeoutException:
|
||||||
ns_since_last_ping = int(time.monotonic() * 1e9) - last_ping
|
ns_since_last_ping = int(time.monotonic() * 1e9) - last_ping
|
||||||
if ns_since_last_ping > SUNNYLINK_RECONNECT_TIMEOUT_S * 1e9:
|
if ns_since_last_ping > SUNNYLINK_RECONNECT_TIMEOUT_S * 1e9:
|
||||||
cloudlog.exception("sunnylinkd.ws_recv.timeout")
|
cloudlog.warning("sunnylinkd.ws_recv.timeout")
|
||||||
end_event.set()
|
end_event.set()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
cloudlog.exception("sunnylinkd.ws_recv.exception")
|
if isinstance(e, WebSocketConnectionClosedException):
|
||||||
|
cloudlog.warning(f"sunnylinkd.ws_recv.{type(e).__name__}")
|
||||||
|
else:
|
||||||
|
cloudlog.exception("sunnylinkd.ws_recv.exception")
|
||||||
end_event.set()
|
end_event.set()
|
||||||
|
|
||||||
|
|
||||||
@@ -137,11 +142,15 @@ def ws_queue(end_event: threading.Event) -> None:
|
|||||||
sunnylink_api.resume_queued(timeout=29)
|
sunnylink_api.resume_queued(timeout=29)
|
||||||
resume_requested = True
|
resume_requested = True
|
||||||
tries = 0
|
tries = 0
|
||||||
except Exception:
|
except Exception as e:
|
||||||
cloudlog.exception("sunnylinkd.ws_queue.resume_queued.exception")
|
if isinstance(e, (ConnectionError, TimeoutError)):
|
||||||
|
cloudlog.warning(f"sunnylinkd.ws_queue.resume_queued.{type(e).__name__}")
|
||||||
|
else:
|
||||||
|
cloudlog.exception("sunnylinkd.ws_queue.resume_queued.exception")
|
||||||
|
|
||||||
resume_requested = False
|
resume_requested = False
|
||||||
tries += 1
|
tries += 1
|
||||||
time.sleep(backoff(tries)) # Wait for the backoff time before the next attempt
|
time.sleep(backoff(tries))
|
||||||
|
|
||||||
if end_event.is_set():
|
if end_event.is_set():
|
||||||
cloudlog.debug("end_event is set, exiting ws_queue thread")
|
cloudlog.debug("end_event is set, exiting ws_queue thread")
|
||||||
@@ -171,16 +180,22 @@ def getParamsAllKeys() -> list[str]:
|
|||||||
|
|
||||||
@dispatcher.add_method
|
@dispatcher.add_method
|
||||||
def getParams(params_keys: list[str], compression: bool = False) -> str | dict[str, str]:
|
def getParams(params_keys: list[str], compression: bool = False) -> str | dict[str, str]:
|
||||||
|
params = Params()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
params = Params()
|
param_keys_validated = [key for key in params_keys if key in getParamsAllKeys()]
|
||||||
params_dict: dict[str, bytes] = {key: params.get(key) or b'' for key in params_keys}
|
params_dict: dict[str, list[dict[str, str | bool | int ]]] = {"params": [
|
||||||
|
{
|
||||||
|
"key": key,
|
||||||
|
"value": base64.b64encode(gzip.compress(get_param_as_byte(key)) if compression else get_param_as_byte(key)).decode('utf-8'),
|
||||||
|
"type": int(params.get_type(key).value),
|
||||||
|
"is_compressed": compression
|
||||||
|
} for key in param_keys_validated
|
||||||
|
]}
|
||||||
|
|
||||||
# Compress the values before encoding to base64 as output from params.get is bytes and same for compression
|
response = {str(param.get('key')): str(param.get('value')) for param in params_dict.get("params", [])}
|
||||||
if compression:
|
response |= {"params": json.dumps(params_dict.get("params", []))} # Upcoming for settings v1
|
||||||
params_dict = {key: gzip.compress(value) for key, value in params_dict.items()}
|
return response
|
||||||
|
|
||||||
# Last step is to encode the values to base64 and decode to utf-8 for JSON serialization
|
|
||||||
return {key: base64.b64encode(value).decode('utf-8') for key, value in params_dict.items()}
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
cloudlog.exception("sunnylinkd.getParams.exception", e)
|
cloudlog.exception("sunnylinkd.getParams.exception", e)
|
||||||
@@ -252,14 +267,19 @@ def main(exit_event: threading.Event = None):
|
|||||||
handle_long_poll(ws, exit_event)
|
handle_long_poll(ws, exit_event)
|
||||||
except (KeyboardInterrupt, SystemExit):
|
except (KeyboardInterrupt, SystemExit):
|
||||||
break
|
break
|
||||||
except (ConnectionError, TimeoutError, WebSocketException):
|
except Exception as e:
|
||||||
conn_retries += 1
|
conn_retries += 1
|
||||||
params.remove("LastSunnylinkPingTime")
|
params.remove("LastSunnylinkPingTime")
|
||||||
except Exception:
|
|
||||||
cloudlog.exception("sunnylinkd.main.exception")
|
|
||||||
|
|
||||||
conn_retries += 1
|
if isinstance(e, (ConnectionError, TimeoutError, WebSocketException)):
|
||||||
params.remove("LastSunnylinkPingTime")
|
cloudlog.warning(f"sunnylinkd.main.{type(e).__name__}")
|
||||||
|
elif isinstance(e, OSError):
|
||||||
|
name = errno.errorcode.get(e.errno or -1, "UNKNOWN")
|
||||||
|
msg = f"sunnylinkd.main.OSError.{name} ({e.errno})"
|
||||||
|
is_expected_error = e.errno in (errno.ENETDOWN, errno.ENETRESET, errno.ENETUNREACH)
|
||||||
|
cloudlog.warning(msg) if is_expected_error else cloudlog.exception(msg)
|
||||||
|
else:
|
||||||
|
cloudlog.exception("sunnylinkd.main.exception")
|
||||||
|
|
||||||
time.sleep(backoff(conn_retries))
|
time.sleep(backoff(conn_retries))
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from openpilot.system.version import get_version
|
|||||||
from cereal import messaging, custom
|
from cereal import messaging, custom
|
||||||
from sunnypilot.sunnylink.api import SunnylinkApi
|
from sunnypilot.sunnylink.api import SunnylinkApi
|
||||||
from sunnypilot.sunnylink.backups.utils import decrypt_compressed_data, encrypt_compress_data, SnakeCaseEncoder
|
from sunnypilot.sunnylink.backups.utils import decrypt_compressed_data, encrypt_compress_data, SnakeCaseEncoder
|
||||||
|
from sunnypilot.sunnylink.utils import get_param_as_byte
|
||||||
|
|
||||||
|
|
||||||
class OperationType(Enum):
|
class OperationType(Enum):
|
||||||
@@ -74,7 +75,7 @@ class BackupManagerSP:
|
|||||||
config_data = {}
|
config_data = {}
|
||||||
params_to_backup = [k.decode('utf-8') for k in self.params.all_keys(ParamKeyFlag.BACKUP)]
|
params_to_backup = [k.decode('utf-8') for k in self.params.all_keys(ParamKeyFlag.BACKUP)]
|
||||||
for param in params_to_backup:
|
for param in params_to_backup:
|
||||||
value = str(self.params.get(param)).encode('utf-8')
|
value = get_param_as_byte(param)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
config_data[param] = base64.b64encode(value).decode('utf-8')
|
config_data[param] = base64.b64encode(value).decode('utf-8')
|
||||||
return config_data
|
return config_data
|
||||||
@@ -113,6 +114,7 @@ class BackupManagerSP:
|
|||||||
payload = json.loads(json.dumps(backup_info.to_dict(), cls=SnakeCaseEncoder))
|
payload = json.loads(json.dumps(backup_info.to_dict(), cls=SnakeCaseEncoder))
|
||||||
self._update_progress(75.0, OperationType.BACKUP)
|
self._update_progress(75.0, OperationType.BACKUP)
|
||||||
|
|
||||||
|
cloudlog.debug(f"Uploading backup with payload: {json.dumps(payload)}")
|
||||||
# Upload to sunnylink
|
# Upload to sunnylink
|
||||||
result = self.api.api_get(
|
result = self.api.api_get(
|
||||||
f"backup/{self.device_id}",
|
f"backup/{self.device_id}",
|
||||||
@@ -124,9 +126,11 @@ class BackupManagerSP:
|
|||||||
if result:
|
if result:
|
||||||
self.backup_status = custom.BackupManagerSP.Status.completed
|
self.backup_status = custom.BackupManagerSP.Status.completed
|
||||||
self._update_progress(100.0, OperationType.BACKUP)
|
self._update_progress(100.0, OperationType.BACKUP)
|
||||||
|
cloudlog.info("Backup successfully created and uploaded")
|
||||||
else:
|
else:
|
||||||
self.backup_status = custom.BackupManagerSP.Status.failed
|
self.backup_status = custom.BackupManagerSP.Status.failed
|
||||||
self.last_error = "Failed to upload backup"
|
self.last_error = "Failed to upload backup"
|
||||||
|
cloudlog.error(result)
|
||||||
self._report_status()
|
self._report_status()
|
||||||
|
|
||||||
return bool(self.backup_status == custom.BackupManagerSP.Status.completed)
|
return bool(self.backup_status == custom.BackupManagerSP.Status.completed)
|
||||||
@@ -264,8 +268,8 @@ class BackupManagerSP:
|
|||||||
# Check for backup command
|
# Check for backup command
|
||||||
if self.params.get_bool("BackupManager_CreateBackup"):
|
if self.params.get_bool("BackupManager_CreateBackup"):
|
||||||
try:
|
try:
|
||||||
await self.create_backup()
|
if await self.create_backup():
|
||||||
reset_progress = True
|
reset_progress = True
|
||||||
finally:
|
finally:
|
||||||
self.params.remove("BackupManager_CreateBackup")
|
self.params.remove("BackupManager_CreateBackup")
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import json
|
||||||
from sunnypilot.sunnylink.api import SunnylinkApi, UNREGISTERED_SUNNYLINK_DONGLE_ID
|
from sunnypilot.sunnylink.api import SunnylinkApi, UNREGISTERED_SUNNYLINK_DONGLE_ID
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params, ParamKeyType
|
||||||
from openpilot.system.version import is_prebuilt
|
from openpilot.system.version import is_prebuilt
|
||||||
|
|
||||||
|
|
||||||
@@ -55,3 +56,15 @@ def get_api_token():
|
|||||||
sunnylink_api = SunnylinkApi(sunnylink_dongle_id)
|
sunnylink_api = SunnylinkApi(sunnylink_dongle_id)
|
||||||
token = sunnylink_api.get_token()
|
token = sunnylink_api.get_token()
|
||||||
print(f"API Token: {token}")
|
print(f"API Token: {token}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_param_as_byte(param_name: str) -> bytes:
|
||||||
|
params = Params()
|
||||||
|
param = params.get(param_name)
|
||||||
|
param_type = params.get_type(param_name)
|
||||||
|
|
||||||
|
if param_type == ParamKeyType.BYTES:
|
||||||
|
return bytes(param)
|
||||||
|
elif param_type == ParamKeyType.JSON:
|
||||||
|
return json.dumps(param).encode('utf-8')
|
||||||
|
return str(param).encode('utf-8')
|
||||||
|
|||||||
@@ -9,3 +9,5 @@ export VECLIB_MAXIMUM_THREADS=1
|
|||||||
if [ -z "$AGNOS_VERSION" ]; then
|
if [ -z "$AGNOS_VERSION" ]; then
|
||||||
export AGNOS_VERSION="12.8"
|
export AGNOS_VERSION="12.8"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
export STAGING_ROOT="/data/safe_staging"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from openpilot.system.statsd import statlog
|
|||||||
from openpilot.common.swaglog import cloudlog
|
from openpilot.common.swaglog import cloudlog
|
||||||
from openpilot.system.hardware.power_monitoring import PowerMonitoring
|
from openpilot.system.hardware.power_monitoring import PowerMonitoring
|
||||||
from openpilot.system.hardware.fan_controller import TiciFanController
|
from openpilot.system.hardware.fan_controller import TiciFanController
|
||||||
from openpilot.system.version import terms_version, training_version
|
from openpilot.system.version import terms_version, training_version, get_build_metadata
|
||||||
|
|
||||||
ThermalStatus = log.DeviceState.ThermalStatus
|
ThermalStatus = log.DeviceState.ThermalStatus
|
||||||
NetworkType = log.DeviceState.NetworkType
|
NetworkType = log.DeviceState.NetworkType
|
||||||
@@ -326,6 +326,16 @@ def hardware_thread(end_event, hw_queue) -> None:
|
|||||||
startup_conditions["not_always_offroad"] = not offroad_mode
|
startup_conditions["not_always_offroad"] = not offroad_mode
|
||||||
onroad_conditions["not_always_offroad"] = not offroad_mode
|
onroad_conditions["not_always_offroad"] = not offroad_mode
|
||||||
|
|
||||||
|
# if an unsupported device and branch is detected, going onroad is blocked
|
||||||
|
# only allow going onroad when:
|
||||||
|
# - TIZI, or
|
||||||
|
# - TICI and channel_type is "tici"
|
||||||
|
build_metadata = get_build_metadata()
|
||||||
|
is_unsupported_combo = TICI and HARDWARE.get_device_type() == "tici" and build_metadata.channel_type != "tici"
|
||||||
|
startup_conditions["not_tici"] = not is_unsupported_combo
|
||||||
|
onroad_conditions["not_tici"] = not is_unsupported_combo
|
||||||
|
set_offroad_alert("Offroad_TiciSupport", is_unsupported_combo, extra_text=build_metadata.channel)
|
||||||
|
|
||||||
# if the temperature enters the danger zone, go offroad to cool down
|
# if the temperature enters the danger zone, go offroad to cool down
|
||||||
onroad_conditions["device_temp_good"] = thermal_status < ThermalStatus.danger
|
onroad_conditions["device_temp_good"] = thermal_status < ThermalStatus.danger
|
||||||
extra_text = f"{offroad_comp_temp:.1f}C"
|
extra_text = f"{offroad_comp_temp:.1f}C"
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import psutil
|
|||||||
import shutil
|
import shutil
|
||||||
import signal
|
import signal
|
||||||
import fcntl
|
import fcntl
|
||||||
import time
|
|
||||||
import threading
|
import threading
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -19,7 +18,7 @@ from openpilot.common.markdown import parse_markdown
|
|||||||
from openpilot.common.swaglog import cloudlog
|
from openpilot.common.swaglog import cloudlog
|
||||||
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
|
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
|
||||||
from openpilot.system.hardware import AGNOS, HARDWARE
|
from openpilot.system.hardware import AGNOS, HARDWARE
|
||||||
from openpilot.system.version import get_build_metadata
|
from openpilot.system.version import get_build_metadata, SP_BRANCH_MIGRATIONS
|
||||||
|
|
||||||
LOCK_FILE = os.getenv("UPDATER_LOCK_FILE", "/tmp/safe_staging_overlay.lock")
|
LOCK_FILE = os.getenv("UPDATER_LOCK_FILE", "/tmp/safe_staging_overlay.lock")
|
||||||
STAGING_ROOT = os.getenv("UPDATER_STAGING_ROOT", "/data/safe_staging")
|
STAGING_ROOT = os.getenv("UPDATER_STAGING_ROOT", "/data/safe_staging")
|
||||||
@@ -190,15 +189,6 @@ def finalize_update() -> None:
|
|||||||
run(["git", "reset", "--hard"], FINALIZED)
|
run(["git", "reset", "--hard"], FINALIZED)
|
||||||
run(["git", "submodule", "foreach", "--recursive", "git", "reset", "--hard"], FINALIZED)
|
run(["git", "submodule", "foreach", "--recursive", "git", "reset", "--hard"], FINALIZED)
|
||||||
|
|
||||||
cloudlog.info("Starting git cleanup in finalized update")
|
|
||||||
t = time.monotonic()
|
|
||||||
try:
|
|
||||||
run(["git", "gc"], FINALIZED)
|
|
||||||
run(["git", "lfs", "prune"], FINALIZED)
|
|
||||||
cloudlog.event("Done git cleanup", duration=time.monotonic() - t)
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
cloudlog.exception(f"Failed git cleanup, took {time.monotonic() - t:.3f} s")
|
|
||||||
|
|
||||||
set_consistent_flag(True)
|
set_consistent_flag(True)
|
||||||
cloudlog.info("done finalizing overlay")
|
cloudlog.info("done finalizing overlay")
|
||||||
|
|
||||||
@@ -242,9 +232,7 @@ class Updater:
|
|||||||
b: str | None = self.params.get("UpdaterTargetBranch")
|
b: str | None = self.params.get("UpdaterTargetBranch")
|
||||||
if b is None:
|
if b is None:
|
||||||
b = self.get_branch(BASEDIR)
|
b = self.get_branch(BASEDIR)
|
||||||
b = {
|
b = SP_BRANCH_MIGRATIONS.get((HARDWARE.get_device_type(), b), b)
|
||||||
("tici", "release3"): "release-tici"
|
|
||||||
}.get((HARDWARE.get_device_type(), b), b)
|
|
||||||
return b
|
return b
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
+12
-2
@@ -16,6 +16,13 @@ MASTER_SP_BRANCHES = ['master']
|
|||||||
RELEASE_BRANCHES = ['release3-staging', 'release3', 'release-tici', 'nightly'] + RELEASE_SP_BRANCHES
|
RELEASE_BRANCHES = ['release3-staging', 'release3', 'release-tici', 'nightly'] + RELEASE_SP_BRANCHES
|
||||||
TESTED_BRANCHES = RELEASE_BRANCHES + ['devel', 'devel-staging', 'nightly-dev'] + TESTED_SP_BRANCHES
|
TESTED_BRANCHES = RELEASE_BRANCHES + ['devel', 'devel-staging', 'nightly-dev'] + TESTED_SP_BRANCHES
|
||||||
|
|
||||||
|
SP_BRANCH_MIGRATIONS = {
|
||||||
|
("tici", "staging-c3-new"): "staging-tici",
|
||||||
|
("tici", "dev-c3-new"): "staging-tici",
|
||||||
|
("tici", "master"): "master-tici",
|
||||||
|
("tici", "master-dev-c3-new"): "master-tici",
|
||||||
|
}
|
||||||
|
|
||||||
BUILD_METADATA_FILENAME = "build.json"
|
BUILD_METADATA_FILENAME = "build.json"
|
||||||
|
|
||||||
training_version: str = "0.2.0"
|
training_version: str = "0.2.0"
|
||||||
@@ -85,7 +92,8 @@ class OpenpilotMetadata:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def sunnypilot_remote(self) -> bool:
|
def sunnypilot_remote(self) -> bool:
|
||||||
return self.git_normalized_origin == "github.com/sunnypilot/sunnypilot"
|
return self.git_normalized_origin in ("github.com/sunnypilot/sunnypilot",
|
||||||
|
"github.com/sunnypilot/openpilot")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def git_normalized_origin(self) -> str:
|
def git_normalized_origin(self) -> str:
|
||||||
@@ -127,7 +135,9 @@ class BuildMetadata:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def channel_type(self) -> str:
|
def channel_type(self) -> str:
|
||||||
if self.development_channel:
|
if self.channel.endswith("-tici"):
|
||||||
|
return "tici"
|
||||||
|
elif self.development_channel:
|
||||||
return "development"
|
return "development"
|
||||||
elif self.channel.startswith("staging-"):
|
elif self.channel.startswith("staging-"):
|
||||||
return "staging"
|
return "staging"
|
||||||
|
|||||||
Reference in New Issue
Block a user