mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-12 05:16:06 +08:00
Compare commits
99 Commits
what-is-go
...
visual-sty
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
669f3b7945 | ||
|
|
63122e1a33 | ||
|
|
c1d3ae427b | ||
|
|
2ab45b552d | ||
|
|
8c1d59fecd | ||
|
|
cde88fd8ed | ||
|
|
4b5de0eddb | ||
|
|
071147baaf | ||
|
|
18af4d6ad6 | ||
|
|
b81d5bca3c | ||
|
|
682d738ffa | ||
|
|
c9b1afb154 | ||
|
|
6e3bd3fbed | ||
|
|
42592dd550 | ||
|
|
b2f7d72a33 | ||
|
|
2e0ce18c84 | ||
|
|
2a9a4a9263 | ||
|
|
e63fb10fdb | ||
|
|
b54941928d | ||
|
|
b85b8ffacf | ||
|
|
3ff2e9b26a | ||
|
|
4b3ffc722a | ||
|
|
5f49066829 | ||
|
|
ab2eb218d5 | ||
|
|
6212b174e9 | ||
|
|
a8a6e5708a | ||
|
|
54b060f178 | ||
|
|
8266386cd0 | ||
|
|
8bbe87ee22 | ||
|
|
d35ac0c145 | ||
|
|
a134ae1e29 | ||
|
|
6a69759b9e | ||
|
|
b9063e2966 | ||
|
|
4397a4387a | ||
|
|
32dc384524 | ||
|
|
68fa239b97 | ||
|
|
c8367fbc25 | ||
|
|
76fc4514e1 | ||
|
|
073ce2b4df | ||
|
|
5d516ba89f | ||
|
|
e819a0dcb1 | ||
|
|
ba4b583e6e | ||
|
|
5954354356 | ||
|
|
23ff232333 | ||
|
|
0bb47fcfa9 | ||
|
|
99a5682371 | ||
|
|
22ca343050 | ||
|
|
0049d20151 | ||
|
|
b9f8f4e8ac | ||
|
|
26564dd42f | ||
|
|
ec440e4568 | ||
|
|
69817f887b | ||
|
|
df21208b7c | ||
|
|
660c994c5e | ||
|
|
904cc796b0 | ||
|
|
1a9a1e1b8a | ||
|
|
2ae7078c0f | ||
|
|
0fde830a30 | ||
|
|
7c744a42e5 | ||
|
|
90cc169dec | ||
|
|
36c1e11a9e | ||
|
|
5e26a99337 | ||
|
|
53683cf90b | ||
|
|
95dcc23887 | ||
|
|
cef3163c7c | ||
|
|
cbc34dd1f6 | ||
|
|
fbd1f6bad1 | ||
|
|
f2ddf9abba | ||
|
|
8a5eaf5ba6 | ||
|
|
ddb377ac5b | ||
|
|
512a01d28c | ||
|
|
b30e275169 | ||
|
|
ee9fe5c9ed | ||
|
|
e672a352d3 | ||
|
|
361d107040 | ||
|
|
36eb047cd3 | ||
|
|
218c6172e6 | ||
|
|
8f35e4fc3c | ||
|
|
c965df39d6 | ||
|
|
53ef69f3c3 | ||
|
|
ea9ca18c8b | ||
|
|
225858261e | ||
|
|
43de43b34a | ||
|
|
96ddbe35a1 | ||
|
|
7d547ad533 | ||
|
|
13de58b845 | ||
|
|
3d8c563a4b | ||
|
|
bc75199d5a | ||
|
|
ba6e18ed91 | ||
|
|
74dbcd699b | ||
|
|
35c6af0190 | ||
|
|
827de88c8b | ||
|
|
54ca4b537d | ||
|
|
c61f327076 | ||
|
|
d569913e5a | ||
|
|
418c93be06 | ||
|
|
5acc040a89 | ||
|
|
e45b17c230 | ||
|
|
b14b6246d7 |
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,6 +1,30 @@
|
||||
sunnypilot Version 2025.002.000 (2025-xx-xx)
|
||||
sunnypilot Version 2025.003.000 (20xx-xx-xx)
|
||||
========================
|
||||
|
||||
sunnypilot Version 2025.002.000 (2025-11-06)
|
||||
========================
|
||||
* What's Changed (sunnypilot/sunnypilot)
|
||||
* models: bump model json to v8 by @Discountchubbs
|
||||
* Bug: Model UI Crash Fix by @nayan8teen
|
||||
* controlsd: add `CP_SP` to `get_pid_accel_limits` by @THERoenPR
|
||||
* sunnylink: update uploader button logic to support novice tier and above by @devtekve
|
||||
* Tesla: Coop Steering by @AmyJeanes
|
||||
* ui: update discord references and add forum widget by @devtekve
|
||||
* ui: Fix spacing in sunnylink panel by @devtekve
|
||||
* docs: Update README installation branches and discord links by @mpurnell1 in
|
||||
* stats: sunnylink integration by @devtekve
|
||||
* bug: Fix initial registration for sunnylink by @devtekve
|
||||
* What's Changed (sunnypilot/opendbc)
|
||||
* Honda: add brake hold messages for Clarity by @mvl-boston
|
||||
* interface: add `CP_SP` to `get_pid_accel_limits` method signature by @roenthomas
|
||||
* Honda: use fixed accel min/max constants for Gas Interceptor by @roenthomas
|
||||
* Tesla: Coop Steering by @AmyJeanes
|
||||
* New Contributors (sunnypilot/sunnypilot)
|
||||
* @THERoenPR made their first contribution in "controlsd: add `CP_SP` to `get_pid_accel_limits`"
|
||||
* @AmyJeanes made their first contribution in "Tesla: Coop Steering"
|
||||
* @mpurnell1 made their first contribution in "docs: Update README installation branches and discord links"
|
||||
* Full Changelog: https://github.com/sunnypilot/sunnypilot/compare/v2025.001.000...v2025.002.000
|
||||
|
||||
sunnypilot Version 2025.001.000 (2025-10-25)
|
||||
========================
|
||||
* 🛠️ Major rewrite
|
||||
|
||||
36
README.md
36
README.md
@@ -3,11 +3,9 @@
|
||||
## 🌞 What is sunnypilot?
|
||||
[sunnypilot](https://github.com/sunnyhaibin/sunnypilot) is a fork of comma.ai's openpilot, an open source driver assistance system. sunnypilot offers the user a unique driving experience for over 300+ supported car makes and models with modified behaviors of driving assist engagements. sunnypilot complies with comma.ai's safety rules as accurately as possible.
|
||||
|
||||
## 💭 Join our Discord
|
||||
Join the official sunnypilot Discord server to stay up to date with all the latest features and be a part of shaping the future of sunnypilot!
|
||||
* https://discord.gg/sunnypilot
|
||||
|
||||
 
|
||||
## 💭 Join our Community Forum
|
||||
Join the official sunnypilot community forum to stay up to date with all the latest features and be a part of shaping the future of sunnypilot!
|
||||
* https://community.sunnypilot.ai/
|
||||
|
||||
## Documentation
|
||||
https://docs.sunnypilot.ai/ is your one stop shop for everything from features to installation to FAQ about the sunnypilot
|
||||
@@ -16,13 +14,13 @@ https://docs.sunnypilot.ai/ is your one stop shop for everything from features t
|
||||
* A supported device to run this software
|
||||
* a [comma three](https://comma.ai/shop/products/three) or a [C3X](https://comma.ai/shop/comma-3x)
|
||||
* This software
|
||||
* One of [the 300+ supported cars](https://github.com/commaai/openpilot/blob/master/docs/CARS.md). We support Honda, Toyota, Hyundai, Nissan, Kia, Chrysler, Lexus, Acura, Audi, VW, Ford and more. If your car is not supported but has adaptive cruise control and lane-keeping assist, it's likely able to run sunnypilot.
|
||||
* One of [the 325+ supported cars](https://github.com/sunnypilot/sunnypilot/blob/master/docs/CARS.md). We support Honda, Toyota, Hyundai, Nissan, Kia, Chrysler, Lexus, Acura, Audi, VW, Ford, and more. If your car is not supported but has adaptive cruise control and lane-keeping assist, it's likely able to run sunnypilot.
|
||||
* A [car harness](https://comma.ai/shop/products/car-harness) to connect to your car
|
||||
|
||||
Detailed instructions for [how to mount the device in a car](https://comma.ai/setup).
|
||||
|
||||
## Installation
|
||||
Please refer to [Recommended Branches](#-recommended-branches) to find your preferred/supported branch. This guide will assume you want to install the latest `staging-c3-new` branch.
|
||||
Please refer to [Recommended Branches](#recommended-branches) to find your preferred/supported branch. This guide will assume you want to install the latest `staging` branch.
|
||||
|
||||
### If you want to use our newest branches (our rewrite)
|
||||
> [!TIP]
|
||||
@@ -31,28 +29,28 @@ Please refer to [Recommended Branches](#-recommended-branches) to find your pref
|
||||
* sunnypilot not installed or you installed a version before 0.8.17?
|
||||
1. [Factory reset/uninstall](https://github.com/commaai/openpilot/wiki/FAQ#how-can-i-reset-the-device) the previous software if you have another software/fork installed.
|
||||
2. After factory reset/uninstall and upon reboot, select `Custom Software` when given the option.
|
||||
3. Input the installation URL per [Recommended Branches](#-recommended-branches). Example: ```https://staging-c3-new.sunnypilot.ai```.
|
||||
3. Input the installation URL per [Recommended Branches](#recommended-branches). Example: ```https://staging.sunnypilot.ai```.
|
||||
4. Complete the rest of the installation following the onscreen instructions.
|
||||
|
||||
* sunnypilot already installed and you installed a version after 0.8.17?
|
||||
1. On the comma three, go to `Settings` ▶️ `Software`.
|
||||
1. On the comma three/3X, go to `Settings` ▶️ `Software`.
|
||||
2. At the `Download` option, press `CHECK`. This will fetch the list of latest branches from sunnypilot.
|
||||
3. At the `Target Branch` option, press `SELECT` to open the Target Branch selector.
|
||||
4. Scroll to select the desired branch per Recommended Branches (see below). Example: `staging-c3-new`
|
||||
4. Scroll to select the desired branch per Recommended Branches (see below). Example: `staging`
|
||||
|
||||
|
||||
| Branch | Installation URL |
|
||||
|:----------------:|:---------------------------------------------:|
|
||||
| `staging-c3-new` | `https://staging-c3-new.sunnypilot.ai` |
|
||||
| `dev-c3-new` | `https://dev-c3-new.sunnypilot.ai` |
|
||||
| `custom-branch` | `https://install.sunnypilot.ai/{branch_name}` |
|
||||
| `release-c3-new` | **Not yet available**. |
|
||||
### Recommended Branches
|
||||
| Branch | Installation URL |
|
||||
|:---------------:|:---------------------------------------------:|
|
||||
| `release` | `https://release.sunnypilot.ai` |
|
||||
| `staging` | `https://staging.sunnypilot.ai` |
|
||||
| `dev` | `https://dev.sunnypilot.ai` |
|
||||
| `custom-branch` | `https://install.sunnypilot.ai/{branch_name}` |
|
||||
|
||||
> [!TIP]
|
||||
> You can use sunnypilot/targetbranch as an install URL. Example: 'sunnypilot/staging-c3-new'.
|
||||
> You can use sunnypilot/targetbranch as an install URL. Example: 'sunnypilot/staging'.
|
||||
|
||||
> [!NOTE]
|
||||
> Do you require further assistance with software installation? Join the [sunnypilot Discord server](https://discord.sunnypilot.com) and message us in the `#installation-help` channel.
|
||||
> Do you require further assistance with software installation? Join the [sunnypilot community forum](https://community.sunnypilot.ai/new-topic?category=general/qa) and create a topic in the General/Q&A Category channel.
|
||||
|
||||
|
||||
<details>
|
||||
|
||||
@@ -172,6 +172,14 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"VisualRadarTracks", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"VisualRadarTracksDelay", {PERSISTENT | BACKUP, FLOAT, "0.0"}},
|
||||
{"VisualWideCam", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"VisualStyle", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"VisualStyleZoom", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"VisualStyleOverhead", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"VisualStyleOverheadZoom", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"VisualStyleOverheadThreshold", {PERSISTENT | BACKUP, INT, "20"}},
|
||||
|
||||
// MADS params
|
||||
{"Mads", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||
@@ -208,6 +216,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"HyundaiLongitudinalTuning", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"TeslaCoopSteering", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
{"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
Submodule opendbc_repo updated: e0e1626820...c32e79f3c6
@@ -25,6 +25,11 @@ void AnnotatedCameraWidget::updateState(const UIState &s) {
|
||||
// update engageability/experimental mode button
|
||||
experimental_btn->updateState(s);
|
||||
dmon.updateState(s);
|
||||
if (s.scene.visual_style == 0) {
|
||||
setBackgroundColor(bg_colors[STATUS_DISENGAGED]);
|
||||
} else {
|
||||
setBackgroundColor(QColor(0, 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
void AnnotatedCameraWidget::initializeGL() {
|
||||
@@ -35,7 +40,12 @@ void AnnotatedCameraWidget::initializeGL() {
|
||||
qInfo() << "OpenGL language version:" << QString((const char*)glGetString(GL_SHADING_LANGUAGE_VERSION));
|
||||
|
||||
prev_draw_t = millis_since_boot();
|
||||
setBackgroundColor(bg_colors[STATUS_DISENGAGED]);
|
||||
auto *s = uiState();
|
||||
if (s->scene.visual_style == 0) {
|
||||
setBackgroundColor(bg_colors[STATUS_DISENGAGED]);
|
||||
} else {
|
||||
setBackgroundColor(QColor(0, 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
mat4 AnnotatedCameraWidget::calcFrameMatrix() {
|
||||
@@ -118,7 +128,13 @@ void AnnotatedCameraWidget::paintGL() {
|
||||
} else if (v_ego > 15) {
|
||||
wide_cam_requested = false;
|
||||
}
|
||||
wide_cam_requested = wide_cam_requested && sm["selfdriveState"].getSelfdriveState().getExperimentalMode();
|
||||
if (s->scene.visual_wide_cam == 1) {
|
||||
wide_cam_requested = true;
|
||||
} else if (s->scene.visual_wide_cam == 2) {
|
||||
wide_cam_requested = false;
|
||||
} else {
|
||||
wide_cam_requested = wide_cam_requested && sm["selfdriveState"].getSelfdriveState().getExperimentalMode();
|
||||
}
|
||||
}
|
||||
CameraWidget::setStreamType(wide_cam_requested ? VISION_STREAM_WIDE_ROAD : VISION_STREAM_ROAD);
|
||||
CameraWidget::setFrameId(sm["modelV2"].getModelV2().getFrameId());
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "selfdrive/ui/qt/onroad/model.h"
|
||||
#include <algorithm>
|
||||
|
||||
void ModelRenderer::draw(QPainter &painter, const QRect &surface_rect) {
|
||||
auto *s = uiState();
|
||||
@@ -49,8 +50,14 @@ void ModelRenderer::update_leads(const cereal::RadarState::Reader &radar_state,
|
||||
}
|
||||
|
||||
void ModelRenderer::update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead) {
|
||||
auto *s = uiState();
|
||||
const auto &model_position = model.getPosition();
|
||||
float max_distance = std::clamp(*(model_position.getX().end() - 1), MIN_DRAW_DISTANCE, MAX_DRAW_DISTANCE);
|
||||
float max_distance;
|
||||
if (s->scene.visual_style == 0) {
|
||||
max_distance = std::clamp(*(model_position.getX().end() - 1), MIN_DRAW_DISTANCE, MAX_DRAW_DISTANCE);
|
||||
} else {
|
||||
max_distance = std::clamp(*(model_position.getX().end() - 1), MIN_DRAW_DISTANCE, MAX_DRAW_DISTANCE);
|
||||
}
|
||||
|
||||
// update lane lines
|
||||
const auto &lane_lines = model.getLaneLines();
|
||||
@@ -58,7 +65,11 @@ void ModelRenderer::update_model(const cereal::ModelDataV2::Reader &model, const
|
||||
int max_idx = get_path_length_idx(lane_lines[0], max_distance);
|
||||
for (int i = 0; i < std::size(lane_line_vertices); i++) {
|
||||
lane_line_probs[i] = line_probs[i];
|
||||
mapLineToPolygon(lane_lines[i], 0.025 * lane_line_probs[i], 0, &lane_line_vertices[i], max_idx);
|
||||
if (s->scene.visual_style == 2) {
|
||||
mapLineToPolygon(lane_lines[i], 0.075 * lane_line_probs[i], 0, &lane_line_vertices[i], max_idx);
|
||||
} else {
|
||||
mapLineToPolygon(lane_lines[i], 0.025 * lane_line_probs[i], 0, &lane_line_vertices[i], max_idx);
|
||||
}
|
||||
}
|
||||
|
||||
// update road edges
|
||||
@@ -66,7 +77,11 @@ void ModelRenderer::update_model(const cereal::ModelDataV2::Reader &model, const
|
||||
const auto &edge_stds = model.getRoadEdgeStds();
|
||||
for (int i = 0; i < std::size(road_edge_vertices); i++) {
|
||||
road_edge_stds[i] = edge_stds[i];
|
||||
mapLineToPolygon(road_edges[i], 0.025, 0, &road_edge_vertices[i], max_idx);
|
||||
if (s->scene.visual_style == 2) {
|
||||
mapLineToPolygon(road_edges[i], 0.1, 0, &road_edge_vertices[i], max_idx);
|
||||
} else {
|
||||
mapLineToPolygon(road_edges[i], 0.025, 0, &road_edge_vertices[i], max_idx);
|
||||
}
|
||||
}
|
||||
|
||||
// update path
|
||||
@@ -79,16 +94,112 @@ void ModelRenderer::update_model(const cereal::ModelDataV2::Reader &model, const
|
||||
}
|
||||
|
||||
void ModelRenderer::drawLaneLines(QPainter &painter) {
|
||||
// lanelines
|
||||
for (int i = 0; i < std::size(lane_line_vertices); ++i) {
|
||||
painter.setBrush(QColor::fromRgbF(1.0, 1.0, 1.0, std::clamp<float>(lane_line_probs[i], 0.0, 0.7)));
|
||||
painter.drawPolygon(lane_line_vertices[i]);
|
||||
}
|
||||
auto *s = uiState();
|
||||
if (s->scene.visual_style == 2) {
|
||||
QRectF r = clip_region;
|
||||
|
||||
// road edges
|
||||
for (int i = 0; i < std::size(road_edge_vertices); ++i) {
|
||||
painter.setBrush(QColor::fromRgbF(1.0, 0, 0, std::clamp<float>(1.0 - road_edge_stds[i], 0.0, 1.0)));
|
||||
painter.drawPolygon(road_edge_vertices[i]);
|
||||
qreal horizonY = r.bottom();
|
||||
if (!road_edge_vertices[0].isEmpty() || !road_edge_vertices[1].isEmpty()) {
|
||||
qreal leftH = r.top();
|
||||
qreal rightH = r.top();
|
||||
|
||||
if (!road_edge_vertices[0].isEmpty()) {
|
||||
leftH = std::numeric_limits<qreal>::max();
|
||||
for (const QPointF &pt : road_edge_vertices[0]) {
|
||||
if (pt.y() < leftH) leftH = pt.y();
|
||||
}
|
||||
}
|
||||
|
||||
if (!road_edge_vertices[1].isEmpty()) {
|
||||
rightH = std::numeric_limits<qreal>::max();
|
||||
for (const QPointF &pt : road_edge_vertices[1]) {
|
||||
if (pt.y() < rightH) rightH = pt.y();
|
||||
}
|
||||
}
|
||||
|
||||
horizonY = std::max(leftH, rightH);
|
||||
}
|
||||
|
||||
painter.fillRect(QRectF(r.left(), horizonY + 0, r.width(), r.bottom() - (horizonY + 0)), QColor("#111111"));
|
||||
|
||||
auto buildFill = [&](const QPolygonF &edgeRibbon, bool isLeftSide) -> QPolygonF {
|
||||
if (edgeRibbon.isEmpty()) return {};
|
||||
|
||||
QMap<int, QPointF> byY;
|
||||
for (const QPointF &pt : edgeRibbon) {
|
||||
int yi = int(std::round(pt.y()));
|
||||
if (!byY.contains(yi)) {
|
||||
byY[yi] = pt;
|
||||
} else {
|
||||
if (isLeftSide) {
|
||||
if (pt.x() > byY[yi].x()) byY[yi] = pt;
|
||||
} else {
|
||||
if (pt.x() < byY[yi].x()) byY[yi] = pt;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (byY.isEmpty()) return {};
|
||||
|
||||
QPolygonF curve;
|
||||
for (auto it = byY.cbegin(); it != byY.cend(); ++it) {
|
||||
curve << it.value();
|
||||
}
|
||||
if (curve.size() < 2) return {};
|
||||
|
||||
const qreal topY = curve.first().y();
|
||||
QPolygonF fill;
|
||||
if (isLeftSide) {
|
||||
fill << QPointF(r.left(), topY);
|
||||
for (const QPointF &pt : curve) fill << pt;
|
||||
fill << QPointF(r.left(), r.bottom());
|
||||
} else {
|
||||
fill << QPointF(r.right(), topY);
|
||||
for (const QPointF &pt : curve) fill << pt;
|
||||
fill << QPointF(r.right(), r.bottom());
|
||||
}
|
||||
return fill;
|
||||
};
|
||||
|
||||
QPolygonF leftFill = buildFill(road_edge_vertices[0], true);
|
||||
QPolygonF rightFill = buildFill(road_edge_vertices[1], false);
|
||||
|
||||
if (!leftFill.isEmpty()) {
|
||||
painter.setBrush(QColor("#222222"));
|
||||
painter.drawPolygon(leftFill);
|
||||
}
|
||||
if (!rightFill.isEmpty()) {
|
||||
painter.setBrush(QColor("#222222"));
|
||||
painter.drawPolygon(rightFill);
|
||||
}
|
||||
|
||||
for (int i = 0; i < std::size(lane_line_vertices); ++i) {
|
||||
painter.setBrush(QColor::fromRgbF(0.902, 0.902, 0.902, std::clamp<float>(lane_line_probs[i], 0.0, 0.7)));
|
||||
painter.drawPolygon(lane_line_vertices[i]);
|
||||
}
|
||||
|
||||
for (int i = 0; i < std::size(road_edge_vertices); ++i) {
|
||||
painter.setBrush(QColor(0x55, 0x55, 0x55, 255));
|
||||
painter.drawPolygon(road_edge_vertices[i]);
|
||||
}
|
||||
|
||||
QLinearGradient bgGrad(r.left(), horizonY - 100, r.left(), horizonY + 100);
|
||||
bgGrad.setColorAt(0.0, QColor("#000000"));
|
||||
bgGrad.setColorAt(0.5, QColor("#111111"));
|
||||
bgGrad.setColorAt(1.0, QColor("#111111"));
|
||||
painter.fillRect(QRectF(r.left(), horizonY - 200, r.width(), 200), bgGrad);
|
||||
|
||||
} else {
|
||||
// lanelines
|
||||
for (int i = 0; i < std::size(lane_line_vertices); ++i) {
|
||||
painter.setBrush(QColor::fromRgbF(1.0, 1.0, 1.0, std::clamp<float>(lane_line_probs[i], 0.0, 0.7)));
|
||||
painter.drawPolygon(lane_line_vertices[i]);
|
||||
}
|
||||
|
||||
// road edges
|
||||
for (int i = 0; i < std::size(road_edge_vertices); ++i) {
|
||||
painter.setBrush(QColor::fromRgbF(1.0, 0, 0, std::clamp<float>(1.0 - road_edge_stds[i], 0.0, 1.0)));
|
||||
painter.drawPolygon(road_edge_vertices[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +286,7 @@ QColor ModelRenderer::blendColors(const QColor &start, const QColor &end, float
|
||||
|
||||
void ModelRenderer::drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data,
|
||||
const QPointF &vd, const QRect &surface_rect) {
|
||||
auto *s = uiState();
|
||||
const float speedBuff = 10.;
|
||||
const float leadBuff = 40.;
|
||||
const float d_rel = lead_data.getDRel();
|
||||
@@ -197,20 +309,133 @@ void ModelRenderer::drawLead(QPainter &painter, const cereal::RadarState::LeadDa
|
||||
float g_yo = sz / 10;
|
||||
|
||||
QPointF glow[] = {{x + (sz * 1.35) + g_xo, y + sz + g_yo}, {x, y - g_yo}, {x - (sz * 1.35) - g_xo, y + sz + g_yo}};
|
||||
painter.setBrush(QColor(218, 202, 37, 255));
|
||||
if (s->scene.visual_style == 2) {
|
||||
painter.setBrush(QColor(0xE6, 0xE6, 0xE6, 255));
|
||||
} else {
|
||||
painter.setBrush(QColor(218, 202, 37, 255));
|
||||
}
|
||||
painter.drawPolygon(glow, std::size(glow));
|
||||
|
||||
// chevron
|
||||
QPointF chevron[] = {{x + (sz * 1.25), y + sz}, {x, y}, {x - (sz * 1.25), y + sz}};
|
||||
painter.setBrush(QColor(201, 34, 49, fillAlpha));
|
||||
if (s->scene.visual_style == 2) {
|
||||
painter.setBrush(QColor(0, 0, 0, fillAlpha));
|
||||
} else {
|
||||
painter.setBrush(QColor(201, 34, 49, fillAlpha));
|
||||
}
|
||||
painter.drawPolygon(chevron, std::size(chevron));
|
||||
}
|
||||
|
||||
// Projects a point in car to space to the corresponding point in full frame image space.
|
||||
float mapRange(float x, float in_min, float in_max, float out_min, float out_max) {
|
||||
if (in_min < in_max) {
|
||||
x = std::clamp(x, in_min, in_max);
|
||||
} else {
|
||||
x = std::clamp(x, in_max, in_min);
|
||||
}
|
||||
return out_min + (x - in_min) * (out_max - out_min) / (in_max - in_min);
|
||||
}
|
||||
|
||||
// Projects a point in car space to the corresponding point in full frame image space.
|
||||
bool ModelRenderer::mapToScreen(float in_x, float in_y, float in_z, QPointF *out) {
|
||||
auto *s = uiState();
|
||||
auto &sm = *(s->sm);
|
||||
float blend_speed_mph = fabsf(sm["carState"].getCarState().getVEgo() * 2.23694f);
|
||||
|
||||
Eigen::Vector3f input(in_x, in_y, in_z);
|
||||
|
||||
if ((s->scene.visual_style_zoom == 1 || s->scene.visual_style_zoom == 2) && s->scene.visual_style != 0) {
|
||||
float zoom_start = 20.0f;
|
||||
float zoom_end = 50.0f;
|
||||
|
||||
if (s->scene.visual_style_zoom == 2) {
|
||||
std::swap(zoom_start, zoom_end);
|
||||
}
|
||||
|
||||
float IN_X_OFFSET = mapRange(blend_speed_mph, zoom_start, zoom_end, 0.0f, 24.0f);
|
||||
float IN_Y_OFFSET = mapRange(blend_speed_mph, zoom_start, zoom_end, 1.0f, 2.0f);
|
||||
float IN_Z_OFFSET = mapRange(blend_speed_mph, zoom_start, zoom_end, 0.0f, 5.0f);
|
||||
float PITCH_DEG = mapRange(blend_speed_mph, zoom_start, zoom_end, 0.0f, 5.0f);
|
||||
|
||||
input = Eigen::Vector3f(in_x + IN_X_OFFSET, in_y / IN_Y_OFFSET, in_z + IN_Z_OFFSET);
|
||||
Eigen::AngleAxisf pitch_rot(PITCH_DEG * M_PI / 180.0f, Eigen::Vector3f::UnitY());
|
||||
input = pitch_rot * input;
|
||||
}
|
||||
|
||||
auto pt = car_space_transform * input;
|
||||
*out = QPointF(pt.x() / pt.z(), pt.y() / pt.z());
|
||||
bool normal_valid = (pt.z() > 1e-3f &&
|
||||
std::isfinite(pt.x()) && std::isfinite(pt.y()));
|
||||
QPointF normal_view;
|
||||
if (normal_valid) {
|
||||
normal_view = QPointF(pt.x() / pt.z(), pt.y() / pt.z());
|
||||
}
|
||||
|
||||
const float base_scale_x = 20.0f;
|
||||
const float base_scale_y = 15.0f;
|
||||
const float y_offset = 450.0f;
|
||||
|
||||
float factor_scale_x = 0.0f;
|
||||
if (blend_speed_mph > 0.0f) {
|
||||
if (s->scene.visual_style_overhead_zoom == 1) {
|
||||
factor_scale_x = mapRange(blend_speed_mph, 0.0f, 50.0f, 30.0f, 0.0f);
|
||||
} else if (s->scene.visual_style_overhead_zoom == 2) {
|
||||
factor_scale_x = mapRange(blend_speed_mph, 50.0f, 0.0f, 30.0f, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
float scale_x = base_scale_x + factor_scale_x;
|
||||
float scale_y = base_scale_y;
|
||||
|
||||
QPointF topdown_view(
|
||||
clip_region.center().x() + in_y * scale_x,
|
||||
(clip_region.bottom() - y_offset) - in_x * scale_y
|
||||
);
|
||||
|
||||
if ((s->scene.visual_style_overhead == 1 || s->scene.visual_style_overhead == 2) && s->scene.visual_style != 0) {
|
||||
static float blend = 0.0f;
|
||||
static float target_blend = 0.0f;
|
||||
static double last_t = millis_since_boot();
|
||||
|
||||
const bool inverted = (s->scene.visual_style_overhead == 2);
|
||||
const float threshold = s->scene.visual_style_overhead_threshold;
|
||||
const float hysteresis = 5.0f;
|
||||
|
||||
if (!inverted) {
|
||||
if (target_blend < 0.5f && blend_speed_mph > threshold) {
|
||||
target_blend = 1.0f;
|
||||
} else if (target_blend > 0.5f && blend_speed_mph < threshold - hysteresis) {
|
||||
target_blend = 0.0f;
|
||||
}
|
||||
} else {
|
||||
if (target_blend < 0.5f && blend_speed_mph < threshold) {
|
||||
target_blend = 1.0f;
|
||||
} else if (target_blend > 0.5f && blend_speed_mph > threshold + hysteresis) {
|
||||
target_blend = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
double now = millis_since_boot();
|
||||
double dt = (now - last_t) / 1000.0;
|
||||
last_t = now;
|
||||
|
||||
const float transition_time = 1.50f;
|
||||
float step = dt / transition_time;
|
||||
|
||||
if (blend < target_blend) {
|
||||
blend = std::min(blend + step, target_blend);
|
||||
} else if (blend > target_blend) {
|
||||
blend = std::max(blend - step, target_blend);
|
||||
}
|
||||
|
||||
if (!normal_valid) return false;
|
||||
*out = QPointF(
|
||||
(1 - blend) * normal_view.x() + blend * topdown_view.x(),
|
||||
(1 - blend) * normal_view.y() + blend * topdown_view.y()
|
||||
);
|
||||
} else {
|
||||
if (!normal_valid) return false;
|
||||
*out = normal_view;
|
||||
}
|
||||
|
||||
return clip_region.contains(*out);
|
||||
}
|
||||
|
||||
|
||||
@@ -196,6 +196,7 @@ mat4 CameraWidget::calcFrameMatrix() {
|
||||
}
|
||||
|
||||
void CameraWidget::paintGL() {
|
||||
auto *s = uiState();
|
||||
glClearColor(bg.redF(), bg.greenF(), bg.blueF(), bg.alphaF());
|
||||
glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
|
||||
|
||||
@@ -248,7 +249,9 @@ void CameraWidget::paintGL() {
|
||||
|
||||
glUniformMatrix4fv(program->uniformLocation("uTransform"), 1, GL_TRUE, frame_mat.v);
|
||||
glEnableVertexAttribArray(0);
|
||||
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, (const void *)0);
|
||||
if (s->scene.visual_style == 0) {
|
||||
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, (const void *)0);
|
||||
}
|
||||
glDisableVertexAttribArray(0);
|
||||
glBindVertexArray(0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
@@ -11,17 +11,18 @@ WiFiPromptWidget::WiFiPromptWidget(QWidget *parent) : QFrame(parent) {
|
||||
main_layout->setContentsMargins(56, 40, 56, 40);
|
||||
main_layout->setSpacing(42);
|
||||
|
||||
QLabel *title = new QLabel(tr("<span style='font-family: \"Noto Color Emoji\";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span>"));
|
||||
title->setStyleSheet("font-size: 64px; font-weight: 500;");
|
||||
community_popup = new SunnylinkCommunityPopup(this);
|
||||
QLabel *title = new QLabel(tr("sunnypilot Community"));
|
||||
title->setStyleSheet("font-size: 56px; font-weight: 500;");
|
||||
main_layout->addWidget(title);
|
||||
|
||||
QLabel *desc = new QLabel(tr("Maximize your training data uploads to improve openpilot's driving models."));
|
||||
QLabel *desc = new QLabel(tr("Need help or have ideas?<br><b>Join</b> our community now!"));
|
||||
desc->setStyleSheet("font-size: 40px; font-weight: 400;");
|
||||
desc->setWordWrap(true);
|
||||
main_layout->addWidget(desc);
|
||||
|
||||
QPushButton *settings_btn = new QPushButton(tr("Open"));
|
||||
connect(settings_btn, &QPushButton::clicked, [=]() { emit openSettings(1, "FirehosePanel"); });
|
||||
QPushButton *settings_btn = new QPushButton(tr("Learn More"));
|
||||
connect(settings_btn, &QPushButton::clicked, [=]() { community_popup->exec(); });
|
||||
settings_btn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
font-size: 48px;
|
||||
|
||||
@@ -3,12 +3,17 @@
|
||||
#include <QFrame>
|
||||
#include <QWidget>
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/community_widget.h"
|
||||
|
||||
class WiFiPromptWidget : public QFrame {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit WiFiPromptWidget(QWidget* parent = 0);
|
||||
|
||||
private:
|
||||
SunnylinkCommunityPopup *community_popup;
|
||||
|
||||
signals:
|
||||
void openSettings(int index = 0, const QString ¶m = "");
|
||||
};
|
||||
|
||||
@@ -35,6 +35,7 @@ qt_src = [
|
||||
"sunnypilot/qt/offroad/settings/software_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/sunnylink_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc",
|
||||
"sunnypilot/qt/offroad/settings/sunnylink/community_widget.cc",
|
||||
"sunnypilot/qt/offroad/settings/trips_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/vehicle_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/visuals_panel.cc",
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Copyright (c) 2025-, sunnypilot 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.
|
||||
*/
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/community_widget.h"
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/util.h"
|
||||
|
||||
using qrcodegen::QrCode;
|
||||
|
||||
// --- SunnylinkCommunityQRWidget ---
|
||||
|
||||
SunnylinkCommunityQRWidget::SunnylinkCommunityQRWidget(QWidget* parent)
|
||||
: QWidget(parent) {}
|
||||
|
||||
void SunnylinkCommunityQRWidget::showEvent(QShowEvent *event) {
|
||||
updateQrCode(SUNNYLINK_COMMUNITY_URL);
|
||||
update();
|
||||
}
|
||||
|
||||
void SunnylinkCommunityQRWidget::updateQrCode(const QString &text) {
|
||||
QrCode qr = QrCode::encodeText(text.toUtf8().data(), QrCode::Ecc::LOW);
|
||||
qint32 sz = qr.getSize();
|
||||
QImage im(sz, sz, QImage::Format_RGB32);
|
||||
|
||||
QRgb black = qRgb(0, 0, 0);
|
||||
QRgb white = qRgb(255, 255, 255);
|
||||
for (int y = 0; y < sz; y++) {
|
||||
for (int x = 0; x < sz; x++) {
|
||||
im.setPixel(x, y, qr.getModule(x, y) ? black : white);
|
||||
}
|
||||
}
|
||||
|
||||
int final_sz = ((width() / sz) - 1) * sz;
|
||||
img = QPixmap::fromImage(im.scaled(final_sz, final_sz, Qt::KeepAspectRatio), Qt::MonoOnly);
|
||||
}
|
||||
|
||||
void SunnylinkCommunityQRWidget::paintEvent(QPaintEvent *e) {
|
||||
QPainter p(this);
|
||||
p.fillRect(rect(), Qt::white);
|
||||
|
||||
if (!img.isNull()) {
|
||||
QSize s = (size() - img.size()) / 2;
|
||||
p.drawPixmap(s.width(), s.height(), img);
|
||||
}
|
||||
}
|
||||
|
||||
// --- SunnylinkCommunityPopup ---
|
||||
|
||||
QStringList SunnylinkCommunityPopup::getInstructions() {
|
||||
QStringList instructions;
|
||||
instructions << tr("Scan the QR code and join us!");
|
||||
return instructions;
|
||||
}
|
||||
|
||||
SunnylinkCommunityPopup::SunnylinkCommunityPopup(QWidget* parent)
|
||||
: DialogBase(parent) {
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mainLayout->setSpacing(0);
|
||||
|
||||
// Solarized Light base3 background
|
||||
setStyleSheet("SunnylinkCommunityPopup { background-color: #FDF6E3; }");
|
||||
|
||||
// Header spanning full width
|
||||
auto headerWidget = new QWidget(this);
|
||||
auto headerLayout = new QHBoxLayout(headerWidget);
|
||||
headerLayout->setContentsMargins(85, 50, 85, 30);
|
||||
headerLayout->setSpacing(30);
|
||||
|
||||
auto close = new QPushButton(QIcon(":/icons/close.svg"), "", this);
|
||||
close->setIconSize(QSize(80, 80));
|
||||
close->setStyleSheet("border: none;");
|
||||
connect(close, &QPushButton::clicked, this, &QDialog::reject);
|
||||
headerLayout->addWidget(close, 0, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
|
||||
const auto title = new QLabel(tr("Join the sunnypilot Community Forum"), this);
|
||||
// Solarized base02 for text
|
||||
title->setStyleSheet("font-size: 65px; color: #073642;");
|
||||
title->setWordWrap(false);
|
||||
title->setAlignment(Qt::AlignCenter);
|
||||
headerLayout->addWidget(title, 1);
|
||||
|
||||
// Spacer to balance the close button on the right
|
||||
auto spacer = new QWidget(this);
|
||||
spacer->setFixedSize(80, 80);
|
||||
headerLayout->addWidget(spacer, 0);
|
||||
|
||||
mainLayout->addWidget(headerWidget);
|
||||
|
||||
// Two-column content layout
|
||||
auto contentLayout = new QHBoxLayout();
|
||||
contentLayout->setContentsMargins(0, 0, 0, 0);
|
||||
contentLayout->setSpacing(0);
|
||||
mainLayout->addLayout(contentLayout, 66);
|
||||
|
||||
// Left side: description
|
||||
auto leftLayout = new QVBoxLayout();
|
||||
leftLayout->setContentsMargins(85, 40, 50, 70);
|
||||
leftLayout->setSpacing(35);
|
||||
contentLayout->addLayout(leftLayout, 40);
|
||||
|
||||
// Hype / intro paragraph
|
||||
const auto desc = new QLabel(tr(
|
||||
"We're excited to announce our <b>sunnypilot Community Forum</b><br><br>"
|
||||
"Over the years, Discord just hasn't scaled well for our growing community.<br>"
|
||||
"It's noisy, unsearchable, and great discussions disappear too easily.<br>"
|
||||
"Our new community forum aims to fix that by making it easier to <b>find answers, share ideas, track feedback, report bugs, help newcomers</b> and more!<br><br>"
|
||||
"<b>Here's what's waiting for you:</b><br>"
|
||||
"• Fully <b>indexable</b> and discoverable through search engines 🔎<br>"
|
||||
"• <b>AI-powered</b>🤖 topic and chat summaries, spam detection, and more<br>"
|
||||
"• A <b>trust-level system</b>✅ that rewards meaningful contributions<br>"
|
||||
"• Designed to work <b>on your own time</b>.🧘<br><br>"
|
||||
"Scan the QR code on the right and join the discussion!"
|
||||
), this);
|
||||
// Solarized base01 for body text
|
||||
desc->setStyleSheet("font-size: 40px; color: #586E75;");
|
||||
desc->setWordWrap(true);
|
||||
leftLayout->addWidget(desc);
|
||||
|
||||
leftLayout->addStretch();
|
||||
|
||||
// Right side: QR code and instructions
|
||||
auto rightLayout = new QVBoxLayout();
|
||||
rightLayout->setContentsMargins(50, 40, 85, 70);
|
||||
rightLayout->setSpacing(40);
|
||||
contentLayout->addLayout(rightLayout, 1);
|
||||
|
||||
// QR code (smaller, fixed size)
|
||||
auto *qr = new SunnylinkCommunityQRWidget(this);
|
||||
qr->setFixedSize(500, 500);
|
||||
rightLayout->addStretch();
|
||||
rightLayout->addWidget(qr, 0, Qt::AlignCenter);
|
||||
rightLayout->addStretch();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2025-, sunnypilot 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QrCode.hpp>
|
||||
#include <QtCore/qjsonobject.h>
|
||||
|
||||
#include "common/util.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
|
||||
const QString SUNNYLINK_COMMUNITY_URL = "https://community.sunnypilot.ai/sp-qr";
|
||||
|
||||
class SunnylinkCommunityQRWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SunnylinkCommunityQRWidget(QWidget* parent = nullptr);
|
||||
void paintEvent(QPaintEvent*) override;
|
||||
|
||||
private:
|
||||
QPixmap img;
|
||||
void updateQrCode(const QString &text);
|
||||
void showEvent(QShowEvent *event) override;
|
||||
};
|
||||
|
||||
// Popup widget
|
||||
class SunnylinkCommunityPopup : public DialogBase {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SunnylinkCommunityPopup(QWidget* parent = nullptr);
|
||||
|
||||
private:
|
||||
static QStringList getInstructions();
|
||||
};
|
||||
@@ -79,11 +79,11 @@ QStringList SunnylinkSponsorPopup::getInstructions(bool sponsor_pair) {
|
||||
instructions << tr("Scan the QR code to login to your GitHub account")
|
||||
<< tr("Follow the prompts to complete the pairing process")
|
||||
<< tr("Re-enter the \"sunnylink\" panel to verify sponsorship status")
|
||||
<< tr("If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot");
|
||||
<< tr("If sponsorship status was not updated, please contact a moderator on our forum at https://community.sunnypilot.ai");
|
||||
} else {
|
||||
instructions << tr("Scan the QR code to visit sunnyhaibin's GitHub Sponsors page")
|
||||
<< tr("Choose your sponsorship tier and confirm your support")
|
||||
<< tr("Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status");
|
||||
<< tr("Join our Community Forum at https://community.sunnypilot.ai and reach out to a moderator if you have issues");
|
||||
}
|
||||
return instructions;
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) {
|
||||
QString sunnylinkUploaderDesc = tr("Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.)");
|
||||
sunnylinkUploaderEnabledBtn = new ParamControlSP(
|
||||
"EnableSunnylinkUploader",
|
||||
tr("Enable sunnylink uploader (just for testing infrastructure)"),
|
||||
tr("Enable sunnylink uploader (infrastructure test)"),
|
||||
sunnylinkUploaderDesc,
|
||||
"", nullptr, true);
|
||||
list->addItem(sunnylinkUploaderEnabledBtn);
|
||||
|
||||
@@ -8,7 +8,41 @@
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.h"
|
||||
|
||||
TeslaSettings::TeslaSettings(QWidget *parent) : BrandSettingsInterface(parent) {
|
||||
constexpr int coopSteeringMinKmh = 23; // minimum speed for cooperative steering (enforced by Tesla firmware)
|
||||
constexpr int oemSteeringMinKmh = 48; // minimum speed for OEM lane departure avoidance (enforced by Tesla firmware)
|
||||
bool is_metric = params.getBool("IsMetric");
|
||||
QString unit = is_metric ? "km/h" : "mph";
|
||||
int display_value_coop;
|
||||
int display_value_oem;
|
||||
if (is_metric) {
|
||||
display_value_coop = coopSteeringMinKmh;
|
||||
display_value_oem = oemSteeringMinKmh;
|
||||
} else {
|
||||
display_value_coop = static_cast<int>(std::round(coopSteeringMinKmh * KM_TO_MILE));
|
||||
display_value_oem = static_cast<int>(std::round(oemSteeringMinKmh * KM_TO_MILE));
|
||||
}
|
||||
const QString coop_desc = QString("<b>%1</b><br><br>"
|
||||
"%2<br>"
|
||||
"%3<br>")
|
||||
.arg(tr("Warning: May experience steering oscillations below %5 %6 during turns, recommend disabling this feature if you experience these."))
|
||||
.arg(tr("Allows the driver to provide limited steering input while openpilot is engaged."))
|
||||
.arg(tr("Only works above %4 %6."))
|
||||
.arg(display_value_coop)
|
||||
.arg(display_value_oem)
|
||||
.arg(unit);
|
||||
|
||||
coopSteeringToggle = new ParamControlSP(
|
||||
"TeslaCoopSteering",
|
||||
tr("Cooperative Steering (Beta)"),
|
||||
coop_desc,
|
||||
"",
|
||||
this
|
||||
);
|
||||
list->addItem(coopSteeringToggle);
|
||||
coopSteeringToggle->showDescription();
|
||||
coopSteeringToggle->setConfirmation(true, false);
|
||||
}
|
||||
|
||||
void TeslaSettings::updateSettings() {
|
||||
coopSteeringToggle->setEnabled(offroad);
|
||||
}
|
||||
|
||||
@@ -22,5 +22,5 @@ public:
|
||||
void updateSettings() override;
|
||||
|
||||
private:
|
||||
bool offroad = false;
|
||||
ParamControlSP *coopSteeringToggle = nullptr;
|
||||
};
|
||||
|
||||
@@ -11,6 +11,18 @@ VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) {
|
||||
param_watcher = new ParamWatcher(this);
|
||||
connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) {
|
||||
paramsRefresh();
|
||||
if (param_name == "VisualStyle") {
|
||||
visual_style_value = param_value.toInt();
|
||||
} else if (param_name == "VisualStyleOverhead") {
|
||||
visual_style_overhead_value = param_value.toInt();
|
||||
} else if (param_name == "VisualRadarTracks") {
|
||||
bool radar_tracks_enabled = param_value.toInt() != 0;
|
||||
visual_radar_tracks_delay_settings->setVisible(radar_tracks_enabled);
|
||||
}
|
||||
visual_style_zoom_settings->setVisible(visual_style_value != 0);
|
||||
visual_style_overhead_settings->setVisible(visual_style_value != 0);
|
||||
visual_style_overhead_zoom_settings->setVisible(visual_style_value != 0 && visual_style_overhead_value != 0);
|
||||
visual_style_overhead_threshold_settings->setVisible(visual_style_value != 0 && visual_style_overhead_value != 0);
|
||||
});
|
||||
|
||||
main_layout = new QStackedLayout(this);
|
||||
@@ -90,6 +102,13 @@ VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) {
|
||||
"",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"VisualRadarTracks",
|
||||
tr("Show Radar Tracks"),
|
||||
tr("Shows what the cars radar sees."),
|
||||
"",
|
||||
false,
|
||||
},
|
||||
};
|
||||
|
||||
// Add regular toggles first
|
||||
@@ -116,6 +135,111 @@ VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) {
|
||||
param_watcher->addParam(param);
|
||||
}
|
||||
|
||||
// Visuals: Radar Tracks Delay
|
||||
visual_radar_tracks_delay_settings = new OptionControlSP("VisualRadarTracksDelay", tr("Adjust Visual Radar Tracks Delay"),
|
||||
tr("Delays radar tracks to better match what you see through the camera."),
|
||||
"", {0, 100}, 10, false, nullptr, true);
|
||||
|
||||
connect(visual_radar_tracks_delay_settings, &OptionControlSP::updateLabels, [=]() {
|
||||
float radar_tracks_delay_value = QString::fromStdString(params.get("VisualRadarTracksDelay")).toFloat();
|
||||
visual_radar_tracks_delay_settings->setLabel(QString::number(radar_tracks_delay_value, 'f', 1) + " s");
|
||||
});
|
||||
|
||||
float radar_tracks_delay_value = QString::fromStdString(params.get("VisualRadarTracksDelay")).toFloat();
|
||||
visual_radar_tracks_delay_settings->setLabel(QString::number(radar_tracks_delay_value, 'f', 1) + " s");
|
||||
|
||||
list->addItem(visual_radar_tracks_delay_settings);
|
||||
|
||||
// Wide Cam
|
||||
std::vector<QString> visual_wide_cam_settings_texts{tr("Auto"), tr("On"), tr("Off")};
|
||||
visual_wide_cam_settings = new ButtonParamControlSP(
|
||||
"VisualWideCam", tr("Wide Cam"), tr("Override the wide cam view regardless of experimental mode status."),
|
||||
"",
|
||||
visual_wide_cam_settings_texts,
|
||||
250);
|
||||
list->addItem(visual_wide_cam_settings);
|
||||
|
||||
// Visual Style
|
||||
std::vector<QString> visual_style_settings_texts{tr("Default"), tr("Minimal"), tr("Vision")};
|
||||
visual_style_settings = new ButtonParamControlSP(
|
||||
"VisualStyle", tr("Visual Style"),
|
||||
tr(
|
||||
"Switch between different on-road visualization layouts."
|
||||
"<ul style='margin-left: 10px; margin-top: 4px;'>"
|
||||
"<li><b>Default:</b> Standard OpenPilot layout with camera and path view.</li>"
|
||||
"<li><b>Minimal:</b> Clean interface without camera feed or extra elements.</li>"
|
||||
"<li><b>Vision:</b> Experimental layout that focuses on model perception and environment.</li>"
|
||||
"</ul>"
|
||||
),
|
||||
"",
|
||||
visual_style_settings_texts,
|
||||
380);
|
||||
list->addItem(visual_style_settings);
|
||||
|
||||
// Visual Style Zoom
|
||||
std::vector<QString> visual_style_zoom_settings_texts{tr("Disabled"), tr("Enabled"), tr("Inverted")};
|
||||
visual_style_zoom_settings = new ButtonParamControlSP(
|
||||
"VisualStyleZoom", tr("Visual Style Zoom"),
|
||||
tr(
|
||||
"Enables dynamic zooming based on driving speed in the selected visual style."
|
||||
"<ul style='margin-left: 10px; margin-top: 4px;'>"
|
||||
"<li><b>Disabled:</b> Keeps the zoom fixed.</li>"
|
||||
"<li><b>Enabled:</b> Zooms in at low speed and out at high speed.</li>"
|
||||
"<li><b>Inverted:</b> Reverses the zoom behavior.</li>"
|
||||
"</ul>"
|
||||
),
|
||||
"",
|
||||
visual_style_zoom_settings_texts,
|
||||
380);
|
||||
list->addItem(visual_style_zoom_settings);
|
||||
|
||||
// Visual Style Overhead
|
||||
std::vector<QString> visual_style_overhead_settings_texts{tr("Disabled"), tr("Enabled"), tr("Inverted")};
|
||||
visual_style_overhead_settings = new ButtonParamControlSP(
|
||||
"VisualStyleOverhead", tr("Visual Style Overhead"),
|
||||
tr(
|
||||
"Toggles an overhead (top-down) camera view for a 2D-style perspective."
|
||||
"<ul style='margin-left: 10px; margin-top: 4px;'>"
|
||||
"<li><b>Disabled:</b> Keeps the standard forward 3D view.</li>"
|
||||
"<li><b>Enabled:</b> Switches to overhead view when active.</li>"
|
||||
"<li><b>Inverted:</b> Reverses when the transition happens.</li>"
|
||||
"</ul>"
|
||||
),
|
||||
"",
|
||||
visual_style_overhead_settings_texts,
|
||||
380);
|
||||
list->addItem(visual_style_overhead_settings);
|
||||
|
||||
// Visual Style Overhead Zoom
|
||||
std::vector<QString> visual_style_overhead_zoom_settings_texts{tr("Disabled"), tr("Enabled"), tr("Inverted")};
|
||||
visual_style_overhead_zoom_settings = new ButtonParamControlSP(
|
||||
"VisualStyleOverheadZoom", tr("Visual Style Overhead Zoom"),
|
||||
tr(
|
||||
"Controls zooming behavior while in overhead mode."
|
||||
"<ul style='margin-left: 10px; margin-top: 4px;'>"
|
||||
"<li><b>Disabled:</b> Keeps a fixed zoom level in overhead mode.</li>"
|
||||
"<li><b>Enabled:</b> Zooms dynamically based on speed while overhead.</li>"
|
||||
"<li><b>Inverted:</b> Opposite zoom direction.</li>"
|
||||
"</ul>"
|
||||
),
|
||||
"",
|
||||
visual_style_overhead_zoom_settings_texts,
|
||||
380);
|
||||
list->addItem(visual_style_overhead_zoom_settings);
|
||||
|
||||
// Visual Style Overhead Threshold
|
||||
visual_style_overhead_threshold_settings = new OptionControlSP(
|
||||
"VisualStyleOverheadThreshold", tr("Visual Style Overhead Threshold"),
|
||||
tr("Sets the speed (in mph) where the display transitions between normal and overhead view."),
|
||||
"", {10, 80}, 5, false, nullptr, false);
|
||||
auto updateThresholdLabel = [=]() {
|
||||
int mph = QString::fromStdString(params.get("VisualStyleOverheadThreshold")).toInt();
|
||||
visual_style_overhead_threshold_settings->setLabel(QString("%1 mph").arg(mph));
|
||||
};
|
||||
connect(visual_style_overhead_threshold_settings, &OptionControlSP::updateLabels, updateThresholdLabel);
|
||||
updateThresholdLabel();
|
||||
list->addItem(visual_style_overhead_threshold_settings);
|
||||
|
||||
// Visuals: Display Metrics below Chevron
|
||||
std::vector<QString> chevron_info_settings_texts{tr("Off"), tr("Distance"), tr("Speed"), tr("Time"), tr("All")};
|
||||
chevron_info_settings = new ButtonParamControlSP(
|
||||
@@ -136,6 +260,19 @@ VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) {
|
||||
380);
|
||||
list->addItem(dev_ui_settings);
|
||||
|
||||
bool radar_tracks_enabled = QString::fromStdString(params.get("VisualRadarTracks")).toInt() != 0;
|
||||
visual_radar_tracks_delay_settings->setVisible(radar_tracks_enabled);
|
||||
param_watcher->addParam("VisualRadarTracks");
|
||||
|
||||
visual_style_value = QString::fromStdString(params.get("VisualStyle")).toInt();
|
||||
visual_style_overhead_value = QString::fromStdString(params.get("VisualStyleOverhead")).toInt();
|
||||
visual_style_zoom_settings->setVisible(visual_style_value != 0);
|
||||
visual_style_overhead_settings->setVisible(visual_style_value != 0);
|
||||
visual_style_overhead_zoom_settings->setVisible(visual_style_value != 0 && visual_style_overhead_value != 0);
|
||||
visual_style_overhead_threshold_settings->setVisible(visual_style_value != 0 && visual_style_overhead_value != 0);
|
||||
param_watcher->addParam("VisualStyle");
|
||||
param_watcher->addParam("VisualStyleOverhead");
|
||||
|
||||
sunnypilotScroller = new ScrollViewSP(list, this);
|
||||
vlayout->addWidget(sunnypilotScroller);
|
||||
|
||||
@@ -191,4 +328,19 @@ void VisualsPanel::paramsRefresh() {
|
||||
if (dev_ui_settings) {
|
||||
dev_ui_settings->refresh();
|
||||
}
|
||||
if (visual_wide_cam_settings) {
|
||||
visual_wide_cam_settings->refresh();
|
||||
}
|
||||
if (visual_style_settings) {
|
||||
visual_style_settings->refresh();
|
||||
}
|
||||
if (visual_style_zoom_settings) {
|
||||
visual_style_zoom_settings->refresh();
|
||||
}
|
||||
if (visual_style_overhead_settings) {
|
||||
visual_style_overhead_settings->refresh();
|
||||
}
|
||||
if (visual_style_overhead_zoom_settings) {
|
||||
visual_style_overhead_zoom_settings->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,4 +32,14 @@ protected:
|
||||
ButtonParamControlSP *dev_ui_settings;
|
||||
|
||||
bool has_longitudinal_control = false;
|
||||
|
||||
OptionControlSP *visual_radar_tracks_delay_settings;
|
||||
ButtonParamControlSP *visual_wide_cam_settings;
|
||||
int visual_style_value = 0;
|
||||
int visual_style_overhead_value = 0;
|
||||
ButtonParamControlSP *visual_style_settings;
|
||||
ButtonParamControlSP *visual_style_zoom_settings;
|
||||
ButtonParamControlSP *visual_style_overhead_settings;
|
||||
ButtonParamControlSP *visual_style_overhead_zoom_settings;
|
||||
OptionControlSP *visual_style_overhead_threshold_settings;
|
||||
};
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
#include "selfdrive/ui/sunnypilot/qt/onroad/model.h"
|
||||
|
||||
|
||||
void ModelRendererSP::drawRadarPoint(QPainter &painter, const QPointF &pos, float v_rel, float radius) {
|
||||
painter.setBrush(QColor(255, 255, 255, 200));
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.drawEllipse(pos, radius, radius);
|
||||
}
|
||||
|
||||
void ModelRendererSP::update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead) {
|
||||
ModelRenderer::update_model(model, lead);
|
||||
const auto &model_position = model.getPosition();
|
||||
@@ -67,6 +73,26 @@ void ModelRendererSP::draw(QPainter &painter, const QRect &surface_rect) {
|
||||
const bool right_blindspot = car_state.getRightBlindspot();
|
||||
drawBlindspot(painter, surface_rect, left_blindspot, right_blindspot);
|
||||
}
|
||||
|
||||
if (s->scene.visual_radar_tracks) {
|
||||
if (sm.alive("liveTracks") && sm.rcv_frame("liveTracks") >= s->scene.started_frame) {
|
||||
const auto &tracks = sm["liveTracks"].getLiveTracks().getPoints();
|
||||
for (const auto &track : tracks) {
|
||||
if (!std::isfinite(track.getDRel()) || !std::isfinite(track.getYRel())) continue;
|
||||
float t_lag = s->scene.visual_radar_tracks_delay;
|
||||
float d_pred = track.getDRel();
|
||||
float y_pred = track.getYRel();
|
||||
if (t_lag > 0.0f) {
|
||||
d_pred += track.getVRel() * t_lag + 0.5f * track.getARel() * t_lag * t_lag;
|
||||
}
|
||||
QPointF screen_pt;
|
||||
if (mapToScreen(d_pred, -y_pred, path_offset_z, &screen_pt)) {
|
||||
drawRadarPoint(painter, screen_pt, track.getVRel(), 10.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawLeadStatus(painter, surface_rect.height(), surface_rect.width());
|
||||
|
||||
painter.restore();
|
||||
|
||||
@@ -28,4 +28,6 @@ private:
|
||||
|
||||
// Lead status animation
|
||||
float lead_status_alpha = 0.0f;
|
||||
|
||||
void drawRadarPoint(QPainter &painter, const QPointF &pos, float v_rel, float radius = 10.0f);
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ UIStateSP::UIStateSP(QObject *parent) : UIState(parent) {
|
||||
"wideRoadCameraState", "managerState", "selfdriveState", "longitudinalPlan",
|
||||
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
|
||||
"carControl", "gpsLocationExternal", "gpsLocation", "liveTorqueParameters",
|
||||
"carStateSP", "liveParameters", "liveMapDataSP", "carParamsSP"
|
||||
"carStateSP", "liveParameters", "liveMapDataSP", "carParamsSP", "liveTracks"
|
||||
});
|
||||
|
||||
// update timer
|
||||
@@ -44,6 +44,14 @@ UIStateSP::UIStateSP(QObject *parent) : UIState(parent) {
|
||||
});
|
||||
param_watcher->addParam("DevUIInfo");
|
||||
param_watcher->addParam("StandstillTimer");
|
||||
param_watcher->addParam("VisualRadarTracks");
|
||||
param_watcher->addParam("VisualRadarTracksDelay");
|
||||
param_watcher->addParam("VisualWideCam");
|
||||
param_watcher->addParam("VisualStyle");
|
||||
param_watcher->addParam("VisualStyleZoom");
|
||||
param_watcher->addParam("VisualStyleOverhead");
|
||||
param_watcher->addParam("VisualStyleOverheadZoom");
|
||||
param_watcher->addParam("VisualStyleOverheadThreshold");
|
||||
}
|
||||
|
||||
// This method overrides completely the update method from the parent class intentionally.
|
||||
@@ -76,6 +84,17 @@ void ui_update_params_sp(UIStateSP *s) {
|
||||
s->scene.chevron_info = std::atoi(params.get("ChevronInfo").c_str());
|
||||
s->scene.blindspot_ui = params.getBool("BlindSpot");
|
||||
s->scene.rainbow_mode = params.getBool("RainbowMode");
|
||||
|
||||
s->scene.visual_radar_tracks = QString::fromStdString(params.get("VisualRadarTracks")).toInt();
|
||||
s->scene.visual_radar_tracks_delay = QString::fromStdString(params.get("VisualRadarTracksDelay")).toFloat();
|
||||
|
||||
s->scene.visual_wide_cam = QString::fromStdString(params.get("VisualWideCam")).toInt();
|
||||
|
||||
s->scene.visual_style = QString::fromStdString(params.get("VisualStyle")).toInt();
|
||||
s->scene.visual_style_zoom = QString::fromStdString(params.get("VisualStyleZoom")).toInt();
|
||||
s->scene.visual_style_overhead = QString::fromStdString(params.get("VisualStyleOverhead")).toInt();
|
||||
s->scene.visual_style_overhead_zoom = QString::fromStdString(params.get("VisualStyleOverheadZoom")).toInt();
|
||||
s->scene.visual_style_overhead_threshold = QString::fromStdString(params.get("VisualStyleOverheadThreshold")).toInt();
|
||||
}
|
||||
|
||||
void UIStateSP::reset_onroad_sleep_timer(OnroadTimerStatusToggle toggleTimerStatus) {
|
||||
|
||||
@@ -21,4 +21,12 @@ typedef struct UISceneSP : UIScene {
|
||||
int chevron_info;
|
||||
bool blindspot_ui;
|
||||
bool rainbow_mode;
|
||||
int visual_radar_tracks = 0;
|
||||
float visual_radar_tracks_delay = 0;
|
||||
int visual_wide_cam = 0;
|
||||
int visual_style = 0;
|
||||
int visual_style_zoom = 0;
|
||||
int visual_style_overhead = 0;
|
||||
int visual_style_overhead_zoom = 0;
|
||||
int visual_style_overhead_threshold = 20.0;
|
||||
} UISceneSP;
|
||||
|
||||
@@ -1 +1 @@
|
||||
#define SUNNYPILOT_VERSION "2025.002.000"
|
||||
#define SUNNYPILOT_VERSION "2025.003.000"
|
||||
|
||||
@@ -15,6 +15,8 @@ from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.helpers import set_
|
||||
|
||||
import openpilot.system.sentry as sentry
|
||||
|
||||
from sunnypilot.sunnylink.statsd import STATSLOGSP
|
||||
|
||||
|
||||
def log_fingerprint(CP: structs.CarParams) -> None:
|
||||
if CP.carFingerprint == "MOCK":
|
||||
@@ -100,6 +102,9 @@ def setup_interfaces(CI: CarInterfaceBase, params: Params = None) -> None:
|
||||
_initialize_torque_lateral_control(CI, CP, enforce_torque, nnlc_enabled)
|
||||
_cleanup_unsupported_params(CP, CP_SP)
|
||||
|
||||
STATSLOGSP.raw('sunnypilot.car_params', CP.to_dict())
|
||||
# STATSLOGSP.raw('sunnypilot_params.car_params_sp', CP_SP.to_dict()) # https://github.com/sunnypilot/opendbc/pull/361
|
||||
|
||||
|
||||
def initialize_params(params) -> list[dict[str, Any]]:
|
||||
keys: list = []
|
||||
@@ -115,4 +120,9 @@ def initialize_params(params) -> list[dict[str, Any]]:
|
||||
"SubaruStopAndGoManualParkingBrake",
|
||||
])
|
||||
|
||||
# tesla
|
||||
keys.extend([
|
||||
"TeslaCoopSteering",
|
||||
])
|
||||
|
||||
return [{k: params.get(k, return_default=True)} for k in keys]
|
||||
|
||||
@@ -16,8 +16,9 @@ from functools import partial
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import set_core_affinity
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
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, stat_handler
|
||||
from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutException,
|
||||
create_connection, WebSocketConnectionClosedException)
|
||||
|
||||
@@ -33,9 +34,6 @@ SUNNYLINK_RECONNECT_TIMEOUT_S = 70 # FYI changing this will also would require
|
||||
DISALLOW_LOG_UPLOAD = threading.Event()
|
||||
|
||||
params = Params()
|
||||
sunnylink_dongle_id = params.get("SunnylinkDongleId")
|
||||
sunnylink_api = SunnylinkApi(sunnylink_dongle_id)
|
||||
|
||||
|
||||
def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None:
|
||||
cloudlog.info("sunnylinkd.handle_long_poll started")
|
||||
@@ -51,7 +49,7 @@ def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None:
|
||||
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=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, Paths.stats_sp_root(), True), name='stat_handler'),
|
||||
] + [
|
||||
threading.Thread(target=jsonrpc_handler, args=(end_event, partial(startLocalProxy, end_event),), name=f'worker_{x}')
|
||||
for x in range(HANDLER_THREADS)
|
||||
@@ -132,6 +130,8 @@ def ws_ping(ws: WebSocket, end_event: threading.Event) -> None:
|
||||
|
||||
|
||||
def ws_queue(end_event: threading.Event) -> None:
|
||||
sunnylink_dongle_id = params.get("SunnylinkDongleId")
|
||||
sunnylink_api = SunnylinkApi(sunnylink_dongle_id)
|
||||
resume_requested = False
|
||||
tries = 0
|
||||
|
||||
@@ -233,6 +233,9 @@ def saveParams(params_to_update: dict[str, str], compression: bool = False) -> N
|
||||
|
||||
|
||||
def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local_port: int) -> dict[str, int]:
|
||||
sunnylink_dongle_id = params.get("SunnylinkDongleId")
|
||||
sunnylink_api = SunnylinkApi(sunnylink_dongle_id)
|
||||
|
||||
cloudlog.debug("athena.startLocalProxy.starting")
|
||||
ws = create_connection(
|
||||
remote_ws_uri,
|
||||
@@ -254,6 +257,8 @@ def main(exit_event: threading.Event = None):
|
||||
cloudlog.info("Waiting for sunnylink registration to complete")
|
||||
time.sleep(10)
|
||||
|
||||
sunnylink_dongle_id = params.get("SunnylinkDongleId")
|
||||
sunnylink_api = SunnylinkApi(sunnylink_dongle_id)
|
||||
UploadQueueCache.initialize(upload_queue)
|
||||
|
||||
ws_uri = f"{SUNNYLINK_ATHENA_HOST}"
|
||||
|
||||
278
sunnypilot/sunnylink/statsd.py
Executable file
278
sunnypilot/sunnylink/statsd.py
Executable file
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env python3
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
import zmq
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, UTC
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from cereal.messaging import SubMaster
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.common.file_helpers import atomic_write_in_dir
|
||||
from openpilot.system.version import get_build_metadata
|
||||
from openpilot.system.loggerd.config import STATS_DIR_FILE_LIMIT, STATS_SOCKET, STATS_FLUSH_TIME_S
|
||||
from openpilot.system.statsd import METRIC_TYPE, StatLogSP
|
||||
from openpilot.common.realtime import Ratekeeper
|
||||
|
||||
STATSLOGSP = StatLogSP(intercept=False)
|
||||
|
||||
def sp_stats(end_event):
|
||||
"""Collect sunnypilot-specific statistics and send as raw metrics."""
|
||||
rk = Ratekeeper(.1, print_delay_threshold=None)
|
||||
statlogsp = STATSLOGSP
|
||||
params = Params()
|
||||
|
||||
def flatten_dict(d, parent_key='', sep='.'):
|
||||
items = {}
|
||||
if isinstance(d, dict):
|
||||
for k, v in d.items():
|
||||
new_key = f"{parent_key}{sep}{k}" if parent_key else k
|
||||
items.update(flatten_dict(v, new_key, sep=sep))
|
||||
elif isinstance(d, (list, tuple)):
|
||||
for i, v in enumerate(d):
|
||||
new_key = f"{parent_key}[{i}]"
|
||||
items.update(flatten_dict(v, new_key, sep=sep))
|
||||
else:
|
||||
items[parent_key] = d
|
||||
return items
|
||||
|
||||
# Collect sunnypilot parameters
|
||||
stats_dict = {}
|
||||
|
||||
param_keys = [
|
||||
'SunnylinkEnabled',
|
||||
'AutoLaneChangeBsmDelay',
|
||||
'AutoLaneChangeTimer',
|
||||
'CarPlatformBundle',
|
||||
'CurrentRoute',
|
||||
'DevUIInfo',
|
||||
'EnableCopyparty',
|
||||
'IntelligentCruiseButtonManagement',
|
||||
'QuietMode',
|
||||
'RainbowMode',
|
||||
'ShowAdvancedControls',
|
||||
'Mads',
|
||||
'MadsMainCruiseAllowed',
|
||||
'MadsSteeringMode',
|
||||
'MadsUnifiedEngagementMode',
|
||||
'ModelManager_ActiveBundle',
|
||||
'ModelManager_Favs',
|
||||
'EnableSunnylinkUploader',
|
||||
'SunnylinkEnabled',
|
||||
'InstallDate',
|
||||
'UptimeOffroad',
|
||||
'UptimeOnroad',
|
||||
]
|
||||
|
||||
while not end_event.is_set():
|
||||
try:
|
||||
for key in param_keys:
|
||||
|
||||
try:
|
||||
value = params.get(key)
|
||||
except Exception as e:
|
||||
stats_dict[key] = e
|
||||
continue
|
||||
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
if isinstance(value, (dict, list, tuple)):
|
||||
stats_dict.update(flatten_dict(value, key))
|
||||
else:
|
||||
stats_dict[key] = value
|
||||
|
||||
if stats_dict:
|
||||
statlogsp.raw('sunnypilot.device_params', stats_dict)
|
||||
except Exception as e:
|
||||
cloudlog.error(f"Exception {e}")
|
||||
finally:
|
||||
rk.keep_time()
|
||||
|
||||
|
||||
def stats_main(end_event):
|
||||
comma_dongle_id = Params().get("DongleId")
|
||||
sunnylink_dongle_id = Params().get("SunnylinkDongleId")
|
||||
|
||||
def get_influxdb_line(measurement: str, value: float | dict[str, float], timestamp: datetime, tags: dict) -> str:
|
||||
res = f"{measurement}"
|
||||
for k, v in tags.items():
|
||||
res += f",{k}={str(v)}"
|
||||
res += " "
|
||||
|
||||
if isinstance(value, float):
|
||||
value = {'value': value}
|
||||
|
||||
for k, v in value.items():
|
||||
res += f"{k}={str(v)},"
|
||||
|
||||
res += f"sunnylink_dongle_id=\"{sunnylink_dongle_id}\",comma_dongle_id=\"{comma_dongle_id}\" {int(timestamp.timestamp() * 1e9)}\n"
|
||||
return res
|
||||
|
||||
def get_influxdb_line_raw(measurement: str, value: dict, timestamp: datetime, tags: dict) -> str:
|
||||
res = f"{measurement}"
|
||||
try:
|
||||
custom_tags = ""
|
||||
for k, v in tags.items():
|
||||
custom_tags += f",{k}={str(v)}"
|
||||
res += custom_tags
|
||||
|
||||
fields = ""
|
||||
for k, v in value.items():
|
||||
# Skip complex types - only keep simple scalar values
|
||||
if isinstance(v, (dict, list, bytes, bytearray)):
|
||||
continue
|
||||
|
||||
fields += f"{k}={json.dumps(v)},"
|
||||
|
||||
res += f" {fields}"
|
||||
except Exception as e:
|
||||
cloudlog.error(f"Unable to get influxdb line for: {value}")
|
||||
res += f",invalid=1 reason={e},"
|
||||
|
||||
res += f"sunnylink_dongle_id=\"{sunnylink_dongle_id}\",comma_dongle_id=\"{comma_dongle_id}\" {int(timestamp.timestamp() * 1e9)}\n"
|
||||
return res
|
||||
|
||||
# open statistics socket
|
||||
ctx = zmq.Context.instance()
|
||||
sock = ctx.socket(zmq.PULL)
|
||||
sock.bind(f"{STATS_SOCKET}_sp")
|
||||
|
||||
STATS_DIR = Paths.stats_sp_root()
|
||||
|
||||
# initialize stats directory
|
||||
Path(STATS_DIR).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
build_metadata = get_build_metadata()
|
||||
|
||||
# initialize tags
|
||||
tags = {
|
||||
'started': False,
|
||||
'version': build_metadata.openpilot.version,
|
||||
'branch': build_metadata.channel,
|
||||
'dirty': build_metadata.openpilot.is_dirty,
|
||||
'origin': build_metadata.openpilot.git_normalized_origin,
|
||||
'deviceType': HARDWARE.get_device_type(),
|
||||
}
|
||||
|
||||
# subscribe to deviceState for started state
|
||||
sm = SubMaster(['deviceState'])
|
||||
|
||||
idx = 0
|
||||
boot_uid = str(uuid.uuid4())[:8]
|
||||
last_flush_time = time.monotonic()
|
||||
gauges = {}
|
||||
samples: dict[str, list[float]] = defaultdict(list)
|
||||
raws: dict = defaultdict()
|
||||
try:
|
||||
while not end_event.is_set():
|
||||
started_prev = sm['deviceState'].started
|
||||
sm.update()
|
||||
|
||||
# Update metrics
|
||||
while True:
|
||||
try:
|
||||
metric = sock.recv_string(zmq.NOBLOCK)
|
||||
try:
|
||||
metric_type = metric.split('|')[1]
|
||||
metric_name = metric.split(':')[0]
|
||||
metric_value_raw = metric.split('|')[0].split(':')[1]
|
||||
|
||||
if metric_type == METRIC_TYPE.GAUGE:
|
||||
metric_value = float(metric_value_raw)
|
||||
gauges[metric_name] = metric_value
|
||||
elif metric_type == METRIC_TYPE.SAMPLE:
|
||||
metric_value = float(metric_value_raw)
|
||||
samples[metric_name].append(metric_value)
|
||||
elif metric_type == METRIC_TYPE.RAW:
|
||||
raws[metric_name] = metric_value_raw
|
||||
else:
|
||||
cloudlog.event("unknown metric type", metric_type=metric_type)
|
||||
except Exception:
|
||||
print(traceback.format_exc())
|
||||
cloudlog.event("malformed metric", metric=metric)
|
||||
except zmq.error.Again:
|
||||
break
|
||||
|
||||
# flush when started state changes or after FLUSH_TIME_S
|
||||
if (time.monotonic() > last_flush_time + STATS_FLUSH_TIME_S) or (sm['deviceState'].started != started_prev):
|
||||
result = ""
|
||||
current_time = datetime.now(UTC)
|
||||
tags['started'] = sm['deviceState'].started
|
||||
|
||||
for key, value in raws.items():
|
||||
decoded_value = json.loads(base64.b64decode(value).decode('utf-8'))
|
||||
result += get_influxdb_line_raw(key, decoded_value, current_time, tags)
|
||||
|
||||
for key, value in gauges.items():
|
||||
result += get_influxdb_line(f"gauge.{key}", value, current_time, tags)
|
||||
|
||||
for key, values in samples.items():
|
||||
values.sort()
|
||||
sample_count = len(values)
|
||||
sample_sum = sum(values)
|
||||
|
||||
stats = {
|
||||
'count': sample_count,
|
||||
'min': values[0],
|
||||
'max': values[-1],
|
||||
'mean': sample_sum / sample_count,
|
||||
}
|
||||
for percentile in [0.05, 0.5, 0.95]:
|
||||
value = values[int(round(percentile * (sample_count - 1)))]
|
||||
stats[f"p{int(percentile * 100)}"] = value
|
||||
|
||||
result += get_influxdb_line(f"sample.{key}", stats, current_time, tags)
|
||||
|
||||
# clear intermediate data
|
||||
gauges.clear()
|
||||
samples.clear()
|
||||
last_flush_time = time.monotonic()
|
||||
|
||||
# check that we aren't filling up the drive
|
||||
if len(os.listdir(STATS_DIR)) < STATS_DIR_FILE_LIMIT:
|
||||
if len(result) > 0:
|
||||
stats_path = os.path.join(STATS_DIR, f"{boot_uid}_{idx}")
|
||||
with atomic_write_in_dir(stats_path) as f:
|
||||
f.write(result)
|
||||
idx += 1
|
||||
else:
|
||||
cloudlog.error("stats dir full")
|
||||
finally:
|
||||
sock.close()
|
||||
ctx.term()
|
||||
|
||||
|
||||
def main():
|
||||
rk = Ratekeeper(1, print_delay_threshold=None)
|
||||
end_event = threading.Event()
|
||||
|
||||
threads = [
|
||||
threading.Thread(target=stats_main, args=(end_event,)),
|
||||
threading.Thread(target=sp_stats, args=(end_event,)),
|
||||
]
|
||||
|
||||
for t in threads:
|
||||
t.start()
|
||||
|
||||
try:
|
||||
while all(t.is_alive() for t in threads):
|
||||
rk.keep_time()
|
||||
finally:
|
||||
end_event.set()
|
||||
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -744,26 +744,40 @@ def log_handler(end_event: threading.Event, log_attr_name=LOG_ATTR_NAME) -> None
|
||||
cloudlog.exception("athena.log_handler.exception")
|
||||
|
||||
|
||||
def stat_handler(end_event: threading.Event) -> None:
|
||||
STATS_DIR = Paths.stats_root()
|
||||
def stat_handler(end_event: threading.Event, stats_dir=None, is_sunnylink=False) -> None:
|
||||
stats_dir = stats_dir or Paths.stats_root()
|
||||
last_scan = 0.0
|
||||
|
||||
while not end_event.is_set():
|
||||
curr_scan = time.monotonic()
|
||||
try:
|
||||
if curr_scan - last_scan > 10:
|
||||
stat_filenames = list(filter(lambda name: not name.startswith(tempfile.gettempprefix()), os.listdir(STATS_DIR)))
|
||||
stat_filenames = list(filter(lambda name: not name.startswith(tempfile.gettempprefix()), os.listdir(stats_dir)))
|
||||
if len(stat_filenames) > 0:
|
||||
stat_path = os.path.join(STATS_DIR, stat_filenames[0])
|
||||
stat_path = os.path.join(stats_dir, stat_filenames[0])
|
||||
with open(stat_path) as f:
|
||||
payload = f.read()
|
||||
is_compressed = False
|
||||
|
||||
# Log the current size of the file
|
||||
if is_sunnylink:
|
||||
# Compress and encode the data if it exceeds the maximum size
|
||||
compressed_data = gzip.compress(payload.encode())
|
||||
payload = base64.b64encode(compressed_data).decode()
|
||||
is_compressed = True
|
||||
|
||||
jsonrpc = {
|
||||
"method": "storeStats",
|
||||
"params": {
|
||||
"stats": f.read()
|
||||
"stats": payload
|
||||
},
|
||||
"jsonrpc": "2.0",
|
||||
"id": stat_filenames[0]
|
||||
}
|
||||
|
||||
if is_sunnylink and is_compressed:
|
||||
jsonrpc["params"]["compressed"] = is_compressed
|
||||
|
||||
low_priority_send_queue.put_nowait(json.dumps(jsonrpc))
|
||||
os.remove(stat_path)
|
||||
last_scan = curr_scan
|
||||
|
||||
@@ -55,6 +55,13 @@ class Paths:
|
||||
else:
|
||||
return "/data/stats/"
|
||||
|
||||
@staticmethod
|
||||
def stats_sp_root() -> str:
|
||||
if PC:
|
||||
return str(Path(Paths.comma_home()) / "stats")
|
||||
else:
|
||||
return "/data/stats_sp/"
|
||||
|
||||
@staticmethod
|
||||
def config_root() -> str:
|
||||
if PC:
|
||||
|
||||
@@ -164,6 +164,7 @@ procs = [
|
||||
# sunnylink <3
|
||||
DaemonProcess("manage_sunnylinkd", "sunnypilot.sunnylink.athena.manage_sunnylinkd", "SunnylinkdPid"),
|
||||
PythonProcess("sunnylink_registration_manager", "sunnypilot.sunnylink.registration_manager", sunnylink_need_register_shim),
|
||||
PythonProcess("statsd_sp", "sunnypilot.sunnylink.statsd", and_(always_run, sunnylink_ready_shim)),
|
||||
]
|
||||
|
||||
# sunnypilot
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
from decimal import Decimal
|
||||
|
||||
import zmq
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, UTC
|
||||
from datetime import datetime, UTC, date
|
||||
from typing import NoReturn
|
||||
|
||||
from openpilot.common.params import Params
|
||||
@@ -21,18 +25,21 @@ from openpilot.system.loggerd.config import STATS_DIR_FILE_LIMIT, STATS_SOCKET,
|
||||
class METRIC_TYPE:
|
||||
GAUGE = 'g'
|
||||
SAMPLE = 'sa'
|
||||
RAW = 'r'
|
||||
|
||||
|
||||
class StatLog:
|
||||
def __init__(self):
|
||||
self.pid = None
|
||||
self.zctx = None
|
||||
self.sock = None
|
||||
self.stats_socket = STATS_SOCKET
|
||||
|
||||
def connect(self) -> None:
|
||||
self.zctx = zmq.Context()
|
||||
self.zctx = zmq.Context.instance() or zmq.Context()
|
||||
self.sock = self.zctx.socket(zmq.PUSH)
|
||||
self.sock.setsockopt(zmq.LINGER, 10)
|
||||
self.sock.connect(STATS_SOCKET)
|
||||
self.sock.connect(self.stats_socket)
|
||||
self.pid = os.getpid()
|
||||
|
||||
def __del__(self):
|
||||
@@ -60,6 +67,50 @@ class StatLog:
|
||||
self._send(f"{name}:{value}|{METRIC_TYPE.SAMPLE}")
|
||||
|
||||
|
||||
class StatLogSP(StatLog):
|
||||
def __init__(self, intercept=True):
|
||||
"""
|
||||
Initializes the class instance with an optional parameter to determine
|
||||
if statistical logging should be configured or not.
|
||||
|
||||
:param intercept: A boolean flag that indicates whether to initialize
|
||||
the `comma_statlog`. If True, the `comma_statlog` attribute is
|
||||
instantiated as a `StatLog` object. Defaults to True.
|
||||
"""
|
||||
super().__init__()
|
||||
self.comma_statlog = StatLog() if intercept else None
|
||||
self.stats_socket = f"{STATS_SOCKET}_sp"
|
||||
|
||||
def connect(self) -> None:
|
||||
super().connect()
|
||||
if self.comma_statlog:
|
||||
self.comma_statlog.connect()
|
||||
|
||||
def __del__(self):
|
||||
super().__del__()
|
||||
if self.comma_statlog:
|
||||
self.comma_statlog.__del__()
|
||||
|
||||
def _send(self, metric: str) -> None:
|
||||
super()._send(metric)
|
||||
if self.comma_statlog:
|
||||
self.comma_statlog._send(metric)
|
||||
|
||||
@staticmethod
|
||||
def default_converter(obj):
|
||||
if isinstance(obj, (datetime, date)):
|
||||
return obj.isoformat()
|
||||
if isinstance(obj, set):
|
||||
return list(obj)
|
||||
if isinstance(obj, Decimal):
|
||||
return float(obj)
|
||||
return str(obj) # fallback for unknown types
|
||||
|
||||
def raw(self, name: str, value: dict) -> None:
|
||||
encoded_dict = base64.b64encode(json.dumps(value, default=self.default_converter).encode("utf-8")).decode("utf-8")
|
||||
self._send(f"{name}:{encoded_dict}|{METRIC_TYPE.RAW}")
|
||||
|
||||
|
||||
def main() -> NoReturn:
|
||||
dongle_id = Params().get("DongleId")
|
||||
def get_influxdb_line(measurement: str, value: float | dict[str, float], timestamp: datetime, tags: dict) -> str:
|
||||
@@ -180,4 +231,4 @@ def main() -> NoReturn:
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
else:
|
||||
statlog = StatLog()
|
||||
statlog = StatLogSP(intercept=True)
|
||||
|
||||
Reference in New Issue
Block a user