Remove Qt (#36427)
* rm qt from ui scons * rm qt translation litter * rm ccs * more * fix cabana * more * more * morepull/36438/head
parent
378212e5ab
commit
1e73025f86
106 changed files with 79 additions and 23213 deletions
@ -1,174 +0,0 @@ |
||||
name: "ui preview" |
||||
on: |
||||
push: |
||||
branches: |
||||
- master |
||||
pull_request_target: |
||||
types: [assigned, opened, synchronize, reopened, edited] |
||||
branches: |
||||
- 'master' |
||||
paths: |
||||
- 'selfdrive/ui/**' |
||||
workflow_dispatch: |
||||
|
||||
env: |
||||
UI_JOB_NAME: "Create UI Report" |
||||
REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} |
||||
SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }} |
||||
BRANCH_NAME: "openpilot/pr-${{ github.event.number }}" |
||||
|
||||
jobs: |
||||
preview: |
||||
#if: github.repository == 'commaai/openpilot' |
||||
if: false # FIXME: FrameReader is broken on CI runners |
||||
name: preview |
||||
runs-on: ubuntu-latest |
||||
timeout-minutes: 20 |
||||
permissions: |
||||
contents: read |
||||
pull-requests: write |
||||
actions: read |
||||
steps: |
||||
- name: Waiting for ui generation to start |
||||
run: sleep 30 |
||||
|
||||
- name: Waiting for ui generation to end |
||||
uses: lewagon/wait-on-check-action@v1.3.4 |
||||
with: |
||||
ref: ${{ env.SHA }} |
||||
check-name: ${{ env.UI_JOB_NAME }} |
||||
repo-token: ${{ secrets.GITHUB_TOKEN }} |
||||
allowed-conclusions: success |
||||
wait-interval: 20 |
||||
|
||||
- name: Getting workflow run ID |
||||
id: get_run_id |
||||
run: | |
||||
echo "run_id=$(curl https://api.github.com/repos/${{ github.repository }}/commits/${{ env.SHA }}/check-runs | jq -r '.check_runs[] | select(.name == "${{ env.UI_JOB_NAME }}") | .html_url | capture("(?<number>[0-9]+)") | .number')" >> $GITHUB_OUTPUT |
||||
|
||||
- name: Getting proposed ui |
||||
id: download-artifact |
||||
uses: dawidd6/action-download-artifact@v6 |
||||
with: |
||||
github_token: ${{ secrets.GITHUB_TOKEN }} |
||||
run_id: ${{ steps.get_run_id.outputs.run_id }} |
||||
search_artifacts: true |
||||
name: report-1-${{ env.REPORT_NAME }} |
||||
path: ${{ github.workspace }}/pr_ui |
||||
|
||||
- name: Getting master ui |
||||
uses: actions/checkout@v4 |
||||
with: |
||||
repository: commaai/ci-artifacts |
||||
ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} |
||||
path: ${{ github.workspace }}/master_ui |
||||
ref: openpilot_master_ui |
||||
|
||||
- name: Saving new master ui |
||||
if: github.ref == 'refs/heads/master' && github.event_name == 'push' |
||||
working-directory: ${{ github.workspace }}/master_ui |
||||
run: | |
||||
git checkout --orphan=new_master_ui |
||||
git rm -rf * |
||||
git branch -D openpilot_master_ui |
||||
git branch -m openpilot_master_ui |
||||
git config user.name "GitHub Actions Bot" |
||||
git config user.email "<>" |
||||
mv ${{ github.workspace }}/pr_ui/*.png . |
||||
git add . |
||||
git commit -m "screenshots for commit ${{ env.SHA }}" |
||||
git push origin openpilot_master_ui --force |
||||
|
||||
- name: Finding diff |
||||
if: github.event_name == 'pull_request_target' |
||||
id: find_diff |
||||
run: >- |
||||
sudo apt-get update && sudo apt-get install -y imagemagick |
||||
|
||||
scenes=$(find ${{ github.workspace }}/pr_ui/*.png -type f -printf "%f\n" | cut -d '.' -f 1 | grep -v 'pair_device') |
||||
A=($scenes) |
||||
|
||||
DIFF="" |
||||
TABLE="<details><summary>All Screenshots</summary>" |
||||
TABLE="${TABLE}<table>" |
||||
|
||||
for ((i=0; i<${#A[*]}; i=i+1)); |
||||
do |
||||
# Check if the master file exists |
||||
if [ ! -f "${{ github.workspace }}/master_ui/${A[$i]}.png" ]; then |
||||
# This is a new file in PR UI that doesn't exist in master |
||||
DIFF="${DIFF}<details open>" |
||||
DIFF="${DIFF}<summary>${A[$i]} : \$\${\\color{cyan}\\text{NEW}}\$\$</summary>" |
||||
DIFF="${DIFF}<table>" |
||||
|
||||
DIFF="${DIFF}<tr>" |
||||
DIFF="${DIFF} <td> <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}.png\"> </td>" |
||||
DIFF="${DIFF}</tr>" |
||||
|
||||
DIFF="${DIFF}</table>" |
||||
DIFF="${DIFF}</details>" |
||||
elif ! compare -fuzz 2% -highlight-color DeepSkyBlue1 -lowlight-color Black -compose Src ${{ github.workspace }}/master_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png; then |
||||
convert ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png -transparent black mask.png |
||||
composite mask.png ${{ github.workspace }}/master_ui/${A[$i]}.png composite_diff.png |
||||
convert -delay 100 ${{ github.workspace }}/master_ui/${A[$i]}.png composite_diff.png -loop 0 ${{ github.workspace }}/pr_ui/${A[$i]}_diff.gif |
||||
|
||||
mv ${{ github.workspace }}/master_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_master_ref.png |
||||
|
||||
DIFF="${DIFF}<details open>" |
||||
DIFF="${DIFF}<summary>${A[$i]} : \$\${\\color{red}\\text{DIFFERENT}}\$\$</summary>" |
||||
DIFF="${DIFF}<table>" |
||||
|
||||
DIFF="${DIFF}<tr>" |
||||
DIFF="${DIFF} <td> master <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}_master_ref.png\"> </td>" |
||||
DIFF="${DIFF} <td> proposed <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}.png\"> </td>" |
||||
DIFF="${DIFF}</tr>" |
||||
|
||||
DIFF="${DIFF}<tr>" |
||||
DIFF="${DIFF} <td> diff <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}_diff.png\"> </td>" |
||||
DIFF="${DIFF} <td> composite diff <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}_diff.gif\"> </td>" |
||||
DIFF="${DIFF}</tr>" |
||||
|
||||
DIFF="${DIFF}</table>" |
||||
DIFF="${DIFF}</details>" |
||||
else |
||||
rm -f ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png |
||||
fi |
||||
|
||||
INDEX=$(($i % 2)) |
||||
if [[ $INDEX -eq 0 ]]; then |
||||
TABLE="${TABLE}<tr>" |
||||
fi |
||||
TABLE="${TABLE} <td> <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}.png\"> </td>" |
||||
if [[ $INDEX -eq 1 || $(($i + 1)) -eq ${#A[*]} ]]; then |
||||
TABLE="${TABLE}</tr>" |
||||
fi |
||||
done |
||||
|
||||
TABLE="${TABLE}</table></details>" |
||||
|
||||
echo "DIFF=$DIFF$TABLE" >> "$GITHUB_OUTPUT" |
||||
|
||||
- name: Saving proposed ui |
||||
if: github.event_name == 'pull_request_target' |
||||
working-directory: ${{ github.workspace }}/master_ui |
||||
run: | |
||||
git config user.name "GitHub Actions Bot" |
||||
git config user.email "<>" |
||||
git checkout --orphan=${{ env.BRANCH_NAME }} |
||||
git rm -rf * |
||||
mv ${{ github.workspace }}/pr_ui/* . |
||||
git add . |
||||
git commit -m "screenshots for PR #${{ github.event.number }}" |
||||
git push origin ${{ env.BRANCH_NAME }} --force |
||||
|
||||
- name: Comment Screenshots on PR |
||||
if: github.event_name == 'pull_request_target' |
||||
uses: thollander/actions-comment-pull-request@v2 |
||||
with: |
||||
message: | |
||||
<!-- _(run_id_screenshots **${{ github.run_id }}**)_ --> |
||||
## UI Preview |
||||
${{ steps.find_diff.outputs.DIFF }} |
||||
comment_tag: run_id_screenshots |
||||
pr_number: ${{ github.event.number }} |
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
||||
@ -1,30 +0,0 @@ |
||||
#include <sys/resource.h> |
||||
|
||||
#include <QApplication> |
||||
#include <QTranslator> |
||||
|
||||
#include "system/hardware/hw.h" |
||||
#include "selfdrive/ui/qt/qt_window.h" |
||||
#include "selfdrive/ui/qt/util.h" |
||||
#include "selfdrive/ui/qt/window.h" |
||||
|
||||
int main(int argc, char *argv[]) { |
||||
setpriority(PRIO_PROCESS, 0, -20); |
||||
|
||||
qInstallMessageHandler(swagLogMessageHandler); |
||||
initApp(argc, argv); |
||||
|
||||
QTranslator translator; |
||||
QString translation_file = QString::fromStdString(Params().get("LanguageSetting")); |
||||
if (!translator.load(QString(":/%1").arg(translation_file)) && translation_file.length()) { |
||||
qCritical() << "Failed to load translation file:" << translation_file; |
||||
} |
||||
|
||||
QApplication a(argc, argv); |
||||
a.installTranslator(&translator); |
||||
|
||||
MainWindow w; |
||||
setMainWindow(&w); |
||||
a.installEventFilter(&w); |
||||
return a.exec(); |
||||
} |
||||
@ -1,142 +0,0 @@ |
||||
#include "selfdrive/ui/qt/api.h" |
||||
|
||||
#include <openssl/pem.h> |
||||
#include <openssl/rsa.h> |
||||
|
||||
#include <QApplication> |
||||
#include <QCryptographicHash> |
||||
#include <QDateTime> |
||||
#include <QDebug> |
||||
#include <QJsonDocument> |
||||
#include <QNetworkRequest> |
||||
|
||||
#include <memory> |
||||
#include <string> |
||||
|
||||
#include "common/util.h" |
||||
#include "system/hardware/hw.h" |
||||
#include "selfdrive/ui/qt/util.h" |
||||
|
||||
namespace CommaApi { |
||||
|
||||
RSA *get_rsa_private_key() { |
||||
static std::unique_ptr<RSA, decltype(&RSA_free)> rsa_private(nullptr, RSA_free); |
||||
if (!rsa_private) { |
||||
FILE *fp = fopen(Path::rsa_file().c_str(), "rb"); |
||||
if (!fp) { |
||||
qDebug() << "No RSA private key found, please run manager.py or registration.py"; |
||||
return nullptr; |
||||
} |
||||
rsa_private.reset(PEM_read_RSAPrivateKey(fp, NULL, NULL, NULL)); |
||||
fclose(fp); |
||||
} |
||||
return rsa_private.get(); |
||||
} |
||||
|
||||
QByteArray rsa_sign(const QByteArray &data) { |
||||
RSA *rsa_private = get_rsa_private_key(); |
||||
if (!rsa_private) return {}; |
||||
|
||||
QByteArray sig(RSA_size(rsa_private), Qt::Uninitialized); |
||||
unsigned int sig_len; |
||||
int ret = RSA_sign(NID_sha256, (unsigned char*)data.data(), data.size(), (unsigned char*)sig.data(), &sig_len, rsa_private); |
||||
assert(ret == 1); |
||||
assert(sig.size() == sig_len); |
||||
return sig; |
||||
} |
||||
|
||||
QString create_jwt(const QJsonObject &payloads, int expiry) { |
||||
QJsonObject header = {{"alg", "RS256"}}; |
||||
|
||||
auto t = QDateTime::currentSecsSinceEpoch(); |
||||
QJsonObject payload = {{"identity", getDongleId().value_or("")}, {"nbf", t}, {"iat", t}, {"exp", t + expiry}}; |
||||
for (auto it = payloads.begin(); it != payloads.end(); ++it) { |
||||
payload.insert(it.key(), it.value()); |
||||
} |
||||
|
||||
auto b64_opts = QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals; |
||||
QString jwt = QJsonDocument(header).toJson(QJsonDocument::Compact).toBase64(b64_opts) + '.' + |
||||
QJsonDocument(payload).toJson(QJsonDocument::Compact).toBase64(b64_opts); |
||||
|
||||
auto hash = QCryptographicHash::hash(jwt.toUtf8(), QCryptographicHash::Sha256); |
||||
return jwt + "." + rsa_sign(hash).toBase64(b64_opts); |
||||
} |
||||
|
||||
} // namespace CommaApi
|
||||
|
||||
HttpRequest::HttpRequest(QObject *parent, bool create_jwt, int timeout) : create_jwt(create_jwt), QObject(parent) { |
||||
networkTimer = new QTimer(this); |
||||
networkTimer->setSingleShot(true); |
||||
networkTimer->setInterval(timeout); |
||||
connect(networkTimer, &QTimer::timeout, this, &HttpRequest::requestTimeout); |
||||
} |
||||
|
||||
bool HttpRequest::active() const { |
||||
return reply != nullptr; |
||||
} |
||||
|
||||
bool HttpRequest::timeout() const { |
||||
return reply && reply->error() == QNetworkReply::OperationCanceledError; |
||||
} |
||||
|
||||
void HttpRequest::sendRequest(const QString &requestURL, const HttpRequest::Method method) { |
||||
if (active()) { |
||||
qDebug() << "HttpRequest is active"; |
||||
return; |
||||
} |
||||
QString token; |
||||
if (create_jwt) { |
||||
token = CommaApi::create_jwt(); |
||||
} else { |
||||
QString token_json = QString::fromStdString(util::read_file(util::getenv("HOME") + "/.comma/auth.json")); |
||||
QJsonDocument json_d = QJsonDocument::fromJson(token_json.toUtf8()); |
||||
token = json_d["access_token"].toString(); |
||||
} |
||||
|
||||
QNetworkRequest request; |
||||
request.setUrl(QUrl(requestURL)); |
||||
request.setRawHeader("User-Agent", getUserAgent().toUtf8()); |
||||
|
||||
if (!token.isEmpty()) { |
||||
request.setRawHeader(QByteArray("Authorization"), ("JWT " + token).toUtf8()); |
||||
} |
||||
|
||||
if (method == HttpRequest::Method::GET) { |
||||
reply = nam()->get(request); |
||||
} else if (method == HttpRequest::Method::DELETE) { |
||||
reply = nam()->deleteResource(request); |
||||
} |
||||
|
||||
networkTimer->start(); |
||||
connect(reply, &QNetworkReply::finished, this, &HttpRequest::requestFinished); |
||||
} |
||||
|
||||
void HttpRequest::requestTimeout() { |
||||
reply->abort(); |
||||
} |
||||
|
||||
void HttpRequest::requestFinished() { |
||||
networkTimer->stop(); |
||||
|
||||
if (reply->error() == QNetworkReply::NoError) { |
||||
emit requestDone(reply->readAll(), true, reply->error()); |
||||
} else { |
||||
QString error; |
||||
if (reply->error() == QNetworkReply::OperationCanceledError) { |
||||
nam()->clearAccessCache(); |
||||
nam()->clearConnectionCache(); |
||||
error = "Request timed out"; |
||||
} else { |
||||
error = reply->errorString(); |
||||
} |
||||
emit requestDone(error, false, reply->error()); |
||||
} |
||||
|
||||
reply->deleteLater(); |
||||
reply = nullptr; |
||||
} |
||||
|
||||
QNetworkAccessManager *HttpRequest::nam() { |
||||
static QNetworkAccessManager *networkAccessManager = new QNetworkAccessManager(qApp); |
||||
return networkAccessManager; |
||||
} |
||||
@ -1,47 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <QJsonObject> |
||||
#include <QNetworkReply> |
||||
#include <QString> |
||||
#include <QTimer> |
||||
|
||||
#include "common/util.h" |
||||
|
||||
namespace CommaApi { |
||||
|
||||
const QString BASE_URL = util::getenv("API_HOST", "https://api.commadotai.com").c_str(); |
||||
QByteArray rsa_sign(const QByteArray &data); |
||||
QString create_jwt(const QJsonObject &payloads = {}, int expiry = 3600); |
||||
|
||||
} // namespace CommaApi
|
||||
|
||||
/**
|
||||
* Makes a request to the request endpoint. |
||||
*/ |
||||
|
||||
class HttpRequest : public QObject { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
enum class Method {GET, DELETE}; |
||||
|
||||
explicit HttpRequest(QObject* parent, bool create_jwt = true, int timeout = 20000); |
||||
void sendRequest(const QString &requestURL, const Method method = Method::GET); |
||||
bool active() const; |
||||
bool timeout() const; |
||||
|
||||
signals: |
||||
void requestDone(const QString &response, bool success, QNetworkReply::NetworkError error); |
||||
|
||||
protected: |
||||
QNetworkReply *reply = nullptr; |
||||
|
||||
private: |
||||
static QNetworkAccessManager *nam(); |
||||
QTimer *networkTimer = nullptr; |
||||
bool create_jwt; |
||||
|
||||
private slots: |
||||
void requestTimeout(); |
||||
void requestFinished(); |
||||
}; |
||||
@ -1,161 +0,0 @@ |
||||
#include "selfdrive/ui/qt/body.h" |
||||
|
||||
#include <cmath> |
||||
#include <algorithm> |
||||
|
||||
#include <QPainter> |
||||
#include <QStackedLayout> |
||||
|
||||
#include "common/params.h" |
||||
#include "common/timing.h" |
||||
|
||||
RecordButton::RecordButton(QWidget *parent) : QPushButton(parent) { |
||||
setCheckable(true); |
||||
setChecked(false); |
||||
setFixedSize(148, 148); |
||||
|
||||
QObject::connect(this, &QPushButton::toggled, [=]() { |
||||
setEnabled(false); |
||||
}); |
||||
} |
||||
|
||||
void RecordButton::paintEvent(QPaintEvent *event) { |
||||
QPainter p(this); |
||||
p.setRenderHint(QPainter::Antialiasing); |
||||
|
||||
QPoint center(width() / 2, height() / 2); |
||||
|
||||
QColor bg(isChecked() ? "#FFFFFF" : "#737373"); |
||||
QColor accent(isChecked() ? "#FF0000" : "#FFFFFF"); |
||||
if (!isEnabled()) { |
||||
bg = QColor("#404040"); |
||||
accent = QColor("#FFFFFF"); |
||||
} |
||||
|
||||
if (isDown()) { |
||||
accent.setAlphaF(0.7); |
||||
} |
||||
|
||||
p.setPen(Qt::NoPen); |
||||
p.setBrush(bg); |
||||
p.drawEllipse(center, 74, 74); |
||||
|
||||
p.setPen(QPen(accent, 6)); |
||||
p.setBrush(Qt::NoBrush); |
||||
p.drawEllipse(center, 42, 42); |
||||
|
||||
p.setPen(Qt::NoPen); |
||||
p.setBrush(accent); |
||||
p.drawEllipse(center, 22, 22); |
||||
} |
||||
|
||||
|
||||
BodyWindow::BodyWindow(QWidget *parent) : fuel_filter(1.0, 5., 1. / UI_FREQ), QWidget(parent) { |
||||
QStackedLayout *layout = new QStackedLayout(this); |
||||
layout->setStackingMode(QStackedLayout::StackAll); |
||||
|
||||
QWidget *w = new QWidget; |
||||
QVBoxLayout *vlayout = new QVBoxLayout(w); |
||||
vlayout->setMargin(45); |
||||
layout->addWidget(w); |
||||
|
||||
// face
|
||||
face = new QLabel(); |
||||
face->setAlignment(Qt::AlignCenter); |
||||
layout->addWidget(face); |
||||
awake = new QMovie("../assets/body/awake.gif", {}, this); |
||||
awake->setCacheMode(QMovie::CacheAll); |
||||
sleep = new QMovie("../assets/body/sleep.gif", {}, this); |
||||
sleep->setCacheMode(QMovie::CacheAll); |
||||
|
||||
// record button
|
||||
btn = new RecordButton(this); |
||||
vlayout->addWidget(btn, 0, Qt::AlignBottom | Qt::AlignRight); |
||||
QObject::connect(btn, &QPushButton::clicked, [=](bool checked) { |
||||
btn->setEnabled(false); |
||||
Params().putBool("DisableLogging", !checked); |
||||
last_button = nanos_since_boot(); |
||||
}); |
||||
w->raise(); |
||||
|
||||
QObject::connect(uiState(), &UIState::uiUpdate, this, &BodyWindow::updateState); |
||||
} |
||||
|
||||
void BodyWindow::paintEvent(QPaintEvent *event) { |
||||
QPainter p(this); |
||||
p.setRenderHint(QPainter::Antialiasing); |
||||
|
||||
p.fillRect(rect(), QColor(0, 0, 0)); |
||||
|
||||
// battery outline + detail
|
||||
p.translate(width() - 136, 16); |
||||
const QColor gray = QColor("#737373"); |
||||
p.setBrush(Qt::NoBrush); |
||||
p.setPen(QPen(gray, 4, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); |
||||
p.drawRoundedRect(2, 2, 78, 36, 8, 8); |
||||
|
||||
p.setPen(Qt::NoPen); |
||||
p.setBrush(gray); |
||||
p.drawRoundedRect(84, 12, 6, 16, 4, 4); |
||||
p.drawRect(84, 12, 3, 16); |
||||
|
||||
// battery level
|
||||
double fuel = std::clamp(fuel_filter.x(), 0.2f, 1.0f); |
||||
const int m = 5; // manual margin since we can't do an inner border
|
||||
p.setPen(Qt::NoPen); |
||||
p.setBrush(fuel > 0.25 ? QColor("#32D74B") : QColor("#FF453A")); |
||||
p.drawRoundedRect(2 + m, 2 + m, (78 - 2*m)*fuel, 36 - 2*m, 4, 4); |
||||
|
||||
// charging status
|
||||
if (charging) { |
||||
p.setPen(Qt::NoPen); |
||||
p.setBrush(Qt::white); |
||||
const QPolygonF charger({ |
||||
QPointF(12.31, 0), |
||||
QPointF(12.31, 16.92), |
||||
QPointF(18.46, 16.92), |
||||
QPointF(6.15, 40), |
||||
QPointF(6.15, 23.08), |
||||
QPointF(0, 23.08), |
||||
}); |
||||
p.drawPolygon(charger.translated(98, 0)); |
||||
} |
||||
} |
||||
|
||||
void BodyWindow::offroadTransition(bool offroad) { |
||||
btn->setChecked(true); |
||||
btn->setEnabled(true); |
||||
fuel_filter.reset(1.0); |
||||
} |
||||
|
||||
void BodyWindow::updateState(const UIState &s) { |
||||
if (!isVisible()) { |
||||
return; |
||||
} |
||||
|
||||
const SubMaster &sm = *(s.sm); |
||||
auto cs = sm["carState"].getCarState(); |
||||
|
||||
charging = cs.getCharging(); |
||||
fuel_filter.update(cs.getFuelGauge()); |
||||
|
||||
// TODO: use carState.standstill when that's fixed
|
||||
const bool standstill = std::abs(cs.getVEgo()) < 0.01; |
||||
QMovie *m = standstill ? sleep : awake; |
||||
if (m != face->movie()) { |
||||
face->setMovie(m); |
||||
face->movie()->start(); |
||||
} |
||||
|
||||
// update record button state
|
||||
if (sm.updated("managerState") && (sm.rcv_time("managerState") - last_button)*1e-9 > 0.5) { |
||||
for (auto proc : sm["managerState"].getManagerState().getProcesses()) { |
||||
if (proc.getName() == "loggerd") { |
||||
btn->setEnabled(true); |
||||
btn->setChecked(proc.getRunning()); |
||||
} |
||||
} |
||||
} |
||||
|
||||
update(); |
||||
} |
||||
@ -1,38 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <QMovie> |
||||
#include <QLabel> |
||||
#include <QPushButton> |
||||
|
||||
#include "common/util.h" |
||||
#include "selfdrive/ui/ui.h" |
||||
|
||||
class RecordButton : public QPushButton { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
RecordButton(QWidget* parent = 0); |
||||
|
||||
private: |
||||
void paintEvent(QPaintEvent*) override; |
||||
}; |
||||
|
||||
class BodyWindow : public QWidget { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
BodyWindow(QWidget* parent = 0); |
||||
|
||||
private: |
||||
bool charging = false; |
||||
uint64_t last_button = 0; |
||||
FirstOrderFilter fuel_filter; |
||||
QLabel *face; |
||||
QMovie *awake, *sleep; |
||||
RecordButton *btn; |
||||
void paintEvent(QPaintEvent*) override; |
||||
|
||||
private slots: |
||||
void updateState(const UIState &s); |
||||
void offroadTransition(bool onroad); |
||||
}; |
||||
@ -1,243 +0,0 @@ |
||||
#include "selfdrive/ui/qt/home.h" |
||||
|
||||
#include <QHBoxLayout> |
||||
#include <QMouseEvent> |
||||
#include <QStackedWidget> |
||||
#include <QVBoxLayout> |
||||
|
||||
#include "selfdrive/ui/qt/offroad/experimental_mode.h" |
||||
#include "selfdrive/ui/qt/util.h" |
||||
#include "selfdrive/ui/qt/widgets/prime.h" |
||||
|
||||
// HomeWindow: the container for the offroad and onroad UIs
|
||||
|
||||
HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) { |
||||
QHBoxLayout *main_layout = new QHBoxLayout(this); |
||||
main_layout->setMargin(0); |
||||
main_layout->setSpacing(0); |
||||
|
||||
sidebar = new Sidebar(this); |
||||
main_layout->addWidget(sidebar); |
||||
QObject::connect(sidebar, &Sidebar::openSettings, this, &HomeWindow::openSettings); |
||||
|
||||
slayout = new QStackedLayout(); |
||||
main_layout->addLayout(slayout); |
||||
|
||||
home = new OffroadHome(this); |
||||
QObject::connect(home, &OffroadHome::openSettings, this, &HomeWindow::openSettings); |
||||
slayout->addWidget(home); |
||||
|
||||
onroad = new OnroadWindow(this); |
||||
slayout->addWidget(onroad); |
||||
|
||||
body = new BodyWindow(this); |
||||
slayout->addWidget(body); |
||||
|
||||
driver_view = new DriverViewWindow(this); |
||||
connect(driver_view, &DriverViewWindow::done, [=] { |
||||
showDriverView(false); |
||||
}); |
||||
slayout->addWidget(driver_view); |
||||
setAttribute(Qt::WA_NoSystemBackground); |
||||
QObject::connect(uiState(), &UIState::uiUpdate, this, &HomeWindow::updateState); |
||||
QObject::connect(uiState(), &UIState::offroadTransition, this, &HomeWindow::offroadTransition); |
||||
QObject::connect(uiState(), &UIState::offroadTransition, sidebar, &Sidebar::offroadTransition); |
||||
} |
||||
|
||||
void HomeWindow::showSidebar(bool show) { |
||||
sidebar->setVisible(show); |
||||
} |
||||
|
||||
void HomeWindow::updateState(const UIState &s) { |
||||
const SubMaster &sm = *(s.sm); |
||||
|
||||
// switch to the generic robot UI
|
||||
if (onroad->isVisible() && !body->isEnabled() && sm["carParams"].getCarParams().getNotCar()) { |
||||
body->setEnabled(true); |
||||
slayout->setCurrentWidget(body); |
||||
} |
||||
} |
||||
|
||||
void HomeWindow::offroadTransition(bool offroad) { |
||||
body->setEnabled(false); |
||||
sidebar->setVisible(offroad); |
||||
if (offroad) { |
||||
slayout->setCurrentWidget(home); |
||||
} else { |
||||
slayout->setCurrentWidget(onroad); |
||||
} |
||||
} |
||||
|
||||
void HomeWindow::showDriverView(bool show) { |
||||
if (show) { |
||||
emit closeSettings(); |
||||
slayout->setCurrentWidget(driver_view); |
||||
} else { |
||||
slayout->setCurrentWidget(home); |
||||
} |
||||
sidebar->setVisible(show == false); |
||||
} |
||||
|
||||
void HomeWindow::mousePressEvent(QMouseEvent* e) { |
||||
// Handle sidebar collapsing
|
||||
if ((onroad->isVisible() || body->isVisible()) && (!sidebar->isVisible() || e->x() > sidebar->width())) { |
||||
sidebar->setVisible(!sidebar->isVisible()); |
||||
} |
||||
} |
||||
|
||||
void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) { |
||||
HomeWindow::mousePressEvent(e); |
||||
const SubMaster &sm = *(uiState()->sm); |
||||
if (sm["carParams"].getCarParams().getNotCar()) { |
||||
if (onroad->isVisible()) { |
||||
slayout->setCurrentWidget(body); |
||||
} else if (body->isVisible()) { |
||||
slayout->setCurrentWidget(onroad); |
||||
} |
||||
showSidebar(false); |
||||
} |
||||
} |
||||
|
||||
// OffroadHome: the offroad home page
|
||||
|
||||
OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) { |
||||
QVBoxLayout* main_layout = new QVBoxLayout(this); |
||||
main_layout->setContentsMargins(40, 40, 40, 40); |
||||
|
||||
// top header
|
||||
QHBoxLayout* header_layout = new QHBoxLayout(); |
||||
header_layout->setContentsMargins(0, 0, 0, 0); |
||||
header_layout->setSpacing(16); |
||||
|
||||
update_notif = new QPushButton(tr("UPDATE")); |
||||
update_notif->setVisible(false); |
||||
update_notif->setStyleSheet("background-color: #364DEF;"); |
||||
QObject::connect(update_notif, &QPushButton::clicked, [=]() { center_layout->setCurrentIndex(1); }); |
||||
header_layout->addWidget(update_notif, 0, Qt::AlignHCenter | Qt::AlignLeft); |
||||
|
||||
alert_notif = new QPushButton(); |
||||
alert_notif->setVisible(false); |
||||
alert_notif->setStyleSheet("background-color: #E22C2C;"); |
||||
QObject::connect(alert_notif, &QPushButton::clicked, [=] { center_layout->setCurrentIndex(2); }); |
||||
header_layout->addWidget(alert_notif, 0, Qt::AlignHCenter | Qt::AlignLeft); |
||||
|
||||
version = new ElidedLabel(); |
||||
header_layout->addWidget(version, 0, Qt::AlignHCenter | Qt::AlignRight); |
||||
|
||||
main_layout->addLayout(header_layout); |
||||
|
||||
// main content
|
||||
main_layout->addSpacing(25); |
||||
center_layout = new QStackedLayout(); |
||||
|
||||
QWidget *home_widget = new QWidget(this); |
||||
{ |
||||
QHBoxLayout *home_layout = new QHBoxLayout(home_widget); |
||||
home_layout->setContentsMargins(0, 0, 0, 0); |
||||
home_layout->setSpacing(30); |
||||
|
||||
// left: PrimeAdWidget
|
||||
QStackedWidget *left_widget = new QStackedWidget(this); |
||||
QVBoxLayout *left_prime_layout = new QVBoxLayout(); |
||||
left_prime_layout->setContentsMargins(0, 0, 0, 0); |
||||
QWidget *prime_user = new PrimeUserWidget(); |
||||
prime_user->setStyleSheet(R"( |
||||
border-radius: 10px; |
||||
background-color: #333333; |
||||
)"); |
||||
left_prime_layout->addWidget(prime_user); |
||||
left_prime_layout->addStretch(); |
||||
left_widget->addWidget(new LayoutWidget(left_prime_layout)); |
||||
left_widget->addWidget(new PrimeAdWidget); |
||||
left_widget->setStyleSheet("border-radius: 10px;"); |
||||
|
||||
connect(uiState()->prime_state, &PrimeState::changed, [left_widget]() { |
||||
left_widget->setCurrentIndex(uiState()->prime_state->isSubscribed() ? 0 : 1); |
||||
}); |
||||
|
||||
home_layout->addWidget(left_widget, 1); |
||||
|
||||
// right: ExperimentalModeButton, SetupWidget
|
||||
QWidget* right_widget = new QWidget(this); |
||||
QVBoxLayout* right_column = new QVBoxLayout(right_widget); |
||||
right_column->setContentsMargins(0, 0, 0, 0); |
||||
right_widget->setFixedWidth(750); |
||||
right_column->setSpacing(30); |
||||
|
||||
ExperimentalModeButton *experimental_mode = new ExperimentalModeButton(this); |
||||
QObject::connect(experimental_mode, &ExperimentalModeButton::openSettings, this, &OffroadHome::openSettings); |
||||
right_column->addWidget(experimental_mode, 1); |
||||
|
||||
SetupWidget *setup_widget = new SetupWidget; |
||||
QObject::connect(setup_widget, &SetupWidget::openSettings, this, &OffroadHome::openSettings); |
||||
right_column->addWidget(setup_widget, 1); |
||||
|
||||
home_layout->addWidget(right_widget, 1); |
||||
} |
||||
center_layout->addWidget(home_widget); |
||||
|
||||
// add update & alerts widgets
|
||||
update_widget = new UpdateAlert(); |
||||
QObject::connect(update_widget, &UpdateAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); }); |
||||
center_layout->addWidget(update_widget); |
||||
alerts_widget = new OffroadAlert(); |
||||
QObject::connect(alerts_widget, &OffroadAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); }); |
||||
center_layout->addWidget(alerts_widget); |
||||
|
||||
main_layout->addLayout(center_layout, 1); |
||||
|
||||
// set up refresh timer
|
||||
timer = new QTimer(this); |
||||
timer->callOnTimeout(this, &OffroadHome::refresh); |
||||
|
||||
setStyleSheet(R"( |
||||
* { |
||||
color: white; |
||||
} |
||||
OffroadHome { |
||||
background-color: black; |
||||
} |
||||
OffroadHome > QPushButton { |
||||
padding: 15px 30px; |
||||
border-radius: 5px; |
||||
font-size: 40px; |
||||
font-weight: 500; |
||||
} |
||||
OffroadHome > QLabel { |
||||
font-size: 55px; |
||||
} |
||||
)"); |
||||
} |
||||
|
||||
void OffroadHome::showEvent(QShowEvent *event) { |
||||
refresh(); |
||||
timer->start(10 * 1000); |
||||
} |
||||
|
||||
void OffroadHome::hideEvent(QHideEvent *event) { |
||||
timer->stop(); |
||||
} |
||||
|
||||
void OffroadHome::refresh() { |
||||
version->setText(getBrand() + " " + QString::fromStdString(params.get("UpdaterCurrentDescription"))); |
||||
|
||||
bool updateAvailable = update_widget->refresh(); |
||||
int alerts = alerts_widget->refresh(); |
||||
|
||||
// pop-up new notification
|
||||
int idx = center_layout->currentIndex(); |
||||
if (!updateAvailable && !alerts) { |
||||
idx = 0; |
||||
} else if (updateAvailable && (!update_notif->isVisible() || (!alerts && idx == 2))) { |
||||
idx = 1; |
||||
} else if (alerts && (!alert_notif->isVisible() || (!updateAvailable && idx == 1))) { |
||||
idx = 2; |
||||
} |
||||
center_layout->setCurrentIndex(idx); |
||||
|
||||
update_notif->setVisible(updateAvailable); |
||||
alert_notif->setVisible(alerts); |
||||
if (alerts) { |
||||
alert_notif->setText(QString::number(alerts) + (alerts > 1 ? tr(" ALERTS") : tr(" ALERT"))); |
||||
} |
||||
} |
||||
@ -1,73 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <QFrame> |
||||
#include <QLabel> |
||||
#include <QPushButton> |
||||
#include <QStackedLayout> |
||||
#include <QTimer> |
||||
#include <QWidget> |
||||
|
||||
#include "common/params.h" |
||||
#include "selfdrive/ui/qt/offroad/driverview.h" |
||||
#include "selfdrive/ui/qt/body.h" |
||||
#include "selfdrive/ui/qt/onroad/onroad_home.h" |
||||
#include "selfdrive/ui/qt/sidebar.h" |
||||
#include "selfdrive/ui/qt/widgets/controls.h" |
||||
#include "selfdrive/ui/qt/widgets/offroad_alerts.h" |
||||
#include "selfdrive/ui/ui.h" |
||||
|
||||
class OffroadHome : public QFrame { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit OffroadHome(QWidget* parent = 0); |
||||
|
||||
signals: |
||||
void openSettings(int index = 0, const QString ¶m = ""); |
||||
|
||||
private: |
||||
void showEvent(QShowEvent *event) override; |
||||
void hideEvent(QHideEvent *event) override; |
||||
void refresh(); |
||||
|
||||
Params params; |
||||
|
||||
QTimer* timer; |
||||
ElidedLabel* version; |
||||
QStackedLayout* center_layout; |
||||
UpdateAlert *update_widget; |
||||
OffroadAlert* alerts_widget; |
||||
QPushButton* alert_notif; |
||||
QPushButton* update_notif; |
||||
}; |
||||
|
||||
class HomeWindow : public QWidget { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit HomeWindow(QWidget* parent = 0); |
||||
|
||||
signals: |
||||
void openSettings(int index = 0, const QString ¶m = ""); |
||||
void closeSettings(); |
||||
|
||||
public slots: |
||||
void offroadTransition(bool offroad); |
||||
void showDriverView(bool show); |
||||
void showSidebar(bool show); |
||||
|
||||
protected: |
||||
void mousePressEvent(QMouseEvent* e) override; |
||||
void mouseDoubleClickEvent(QMouseEvent* e) override; |
||||
|
||||
private: |
||||
Sidebar *sidebar; |
||||
OffroadHome *home; |
||||
OnroadWindow *onroad; |
||||
BodyWindow *body; |
||||
DriverViewWindow *driver_view; |
||||
QStackedLayout *slayout; |
||||
|
||||
private slots: |
||||
void updateState(const UIState &s); |
||||
}; |
||||
@ -1,413 +0,0 @@ |
||||
#include "selfdrive/ui/qt/network/networking.h" |
||||
|
||||
#include <algorithm> |
||||
|
||||
#include <QHBoxLayout> |
||||
#include <QScrollBar> |
||||
#include <QStyle> |
||||
|
||||
#include "selfdrive/ui/qt/qt_window.h" |
||||
#include "selfdrive/ui/qt/util.h" |
||||
#include "selfdrive/ui/qt/widgets/controls.h" |
||||
#include "selfdrive/ui/qt/widgets/scrollview.h" |
||||
|
||||
static const int ICON_WIDTH = 49; |
||||
|
||||
// Networking functions
|
||||
|
||||
Networking::Networking(QWidget* parent, bool show_advanced) : QFrame(parent) { |
||||
main_layout = new QStackedLayout(this); |
||||
|
||||
wifi = new WifiManager(this); |
||||
connect(wifi, &WifiManager::refreshSignal, this, &Networking::refresh); |
||||
connect(wifi, &WifiManager::wrongPassword, this, &Networking::wrongPassword); |
||||
|
||||
wifiScreen = new QWidget(this); |
||||
QVBoxLayout* vlayout = new QVBoxLayout(wifiScreen); |
||||
vlayout->setContentsMargins(20, 20, 20, 20); |
||||
if (show_advanced) { |
||||
QPushButton* advancedSettings = new QPushButton(tr("Advanced")); |
||||
advancedSettings->setObjectName("advanced_btn"); |
||||
advancedSettings->setStyleSheet("margin-right: 30px;"); |
||||
advancedSettings->setFixedSize(400, 100); |
||||
connect(advancedSettings, &QPushButton::clicked, [=]() { main_layout->setCurrentWidget(an); }); |
||||
vlayout->addSpacing(10); |
||||
vlayout->addWidget(advancedSettings, 0, Qt::AlignRight); |
||||
vlayout->addSpacing(10); |
||||
} |
||||
|
||||
wifiWidget = new WifiUI(this, wifi); |
||||
wifiWidget->setObjectName("wifiWidget"); |
||||
connect(wifiWidget, &WifiUI::connectToNetwork, this, &Networking::connectToNetwork); |
||||
|
||||
ScrollView *wifiScroller = new ScrollView(wifiWidget, this); |
||||
wifiScroller->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); |
||||
vlayout->addWidget(wifiScroller, 1); |
||||
main_layout->addWidget(wifiScreen); |
||||
|
||||
an = new AdvancedNetworking(this, wifi); |
||||
connect(an, &AdvancedNetworking::backPress, [=]() { main_layout->setCurrentWidget(wifiScreen); }); |
||||
connect(an, &AdvancedNetworking::requestWifiScreen, [=]() { main_layout->setCurrentWidget(wifiScreen); }); |
||||
main_layout->addWidget(an); |
||||
|
||||
QPalette pal = palette(); |
||||
pal.setColor(QPalette::Window, QColor(0x29, 0x29, 0x29)); |
||||
setAutoFillBackground(true); |
||||
setPalette(pal); |
||||
|
||||
setStyleSheet(R"( |
||||
#wifiWidget > QPushButton, #back_btn, #advanced_btn { |
||||
font-size: 50px; |
||||
margin: 0px; |
||||
padding: 15px; |
||||
border-width: 0; |
||||
border-radius: 30px; |
||||
color: #dddddd; |
||||
background-color: #393939; |
||||
} |
||||
#back_btn:pressed, #advanced_btn:pressed { |
||||
background-color: #4a4a4a; |
||||
} |
||||
)"); |
||||
main_layout->setCurrentWidget(wifiScreen); |
||||
} |
||||
|
||||
void Networking::setPrimeType(PrimeState::Type type) { |
||||
an->setGsmVisible(type == PrimeState::PRIME_TYPE_NONE || type == PrimeState::PRIME_TYPE_LITE); |
||||
wifi->ipv4_forward = (type == PrimeState::PRIME_TYPE_NONE || type == PrimeState::PRIME_TYPE_LITE); |
||||
} |
||||
|
||||
void Networking::refresh() { |
||||
wifiWidget->refresh(); |
||||
an->refresh(); |
||||
} |
||||
|
||||
void Networking::connectToNetwork(const Network n) { |
||||
if (wifi->isKnownConnection(n.ssid)) { |
||||
wifi->activateWifiConnection(n.ssid); |
||||
} else if (n.security_type == SecurityType::OPEN) { |
||||
wifi->connect(n, false); |
||||
} else if (n.security_type == SecurityType::WPA) { |
||||
QString pass = InputDialog::getText(tr("Enter password"), this, tr("for \"%1\"").arg(QString::fromUtf8(n.ssid)), true, 8); |
||||
if (!pass.isEmpty()) { |
||||
wifi->connect(n, false, pass); |
||||
} |
||||
} |
||||
} |
||||
|
||||
void Networking::wrongPassword(const QString &ssid) { |
||||
if (wifi->seenNetworks.contains(ssid)) { |
||||
const Network &n = wifi->seenNetworks.value(ssid); |
||||
QString pass = InputDialog::getText(tr("Wrong password"), this, tr("for \"%1\"").arg(QString::fromUtf8(n.ssid)), true, 8); |
||||
if (!pass.isEmpty()) { |
||||
wifi->connect(n, false, pass); |
||||
} |
||||
} |
||||
} |
||||
|
||||
void Networking::showEvent(QShowEvent *event) { |
||||
wifi->start(); |
||||
} |
||||
|
||||
void Networking::hideEvent(QHideEvent *event) { |
||||
main_layout->setCurrentWidget(wifiScreen); |
||||
wifi->stop(); |
||||
} |
||||
|
||||
// AdvancedNetworking functions
|
||||
|
||||
AdvancedNetworking::AdvancedNetworking(QWidget* parent, WifiManager* wifi): QWidget(parent), wifi(wifi) { |
||||
|
||||
QVBoxLayout* main_layout = new QVBoxLayout(this); |
||||
main_layout->setMargin(40); |
||||
main_layout->setSpacing(20); |
||||
|
||||
// Back button
|
||||
QPushButton* back = new QPushButton(tr("Back")); |
||||
back->setObjectName("back_btn"); |
||||
back->setFixedSize(400, 100); |
||||
connect(back, &QPushButton::clicked, [=]() { emit backPress(); }); |
||||
main_layout->addWidget(back, 0, Qt::AlignLeft); |
||||
|
||||
ListWidget *list = new ListWidget(this); |
||||
// Enable tethering layout
|
||||
tetheringToggle = new ToggleControl(tr("Enable Tethering"), "", "", wifi->isTetheringEnabled()); |
||||
list->addItem(tetheringToggle); |
||||
QObject::connect(tetheringToggle, &ToggleControl::toggleFlipped, this, &AdvancedNetworking::toggleTethering); |
||||
|
||||
// Change tethering password
|
||||
ButtonControl *editPasswordButton = new ButtonControl(tr("Tethering Password"), tr("EDIT")); |
||||
connect(editPasswordButton, &ButtonControl::clicked, [=]() { |
||||
QString pass = InputDialog::getText(tr("Enter new tethering password"), this, "", true, 8, wifi->getTetheringPassword()); |
||||
if (!pass.isEmpty()) { |
||||
wifi->changeTetheringPassword(pass); |
||||
} |
||||
}); |
||||
list->addItem(editPasswordButton); |
||||
|
||||
// IP address
|
||||
ipLabel = new LabelControl(tr("IP Address"), wifi->ipv4_address); |
||||
list->addItem(ipLabel); |
||||
|
||||
// Roaming toggle
|
||||
const bool roamingEnabled = params.getBool("GsmRoaming"); |
||||
roamingToggle = new ToggleControl(tr("Enable Roaming"), "", "", roamingEnabled); |
||||
QObject::connect(roamingToggle, &ToggleControl::toggleFlipped, [=](bool state) { |
||||
params.putBool("GsmRoaming", state); |
||||
wifi->updateGsmSettings(state, QString::fromStdString(params.get("GsmApn")), params.getBool("GsmMetered")); |
||||
}); |
||||
list->addItem(roamingToggle); |
||||
|
||||
// APN settings
|
||||
editApnButton = new ButtonControl(tr("APN Setting"), tr("EDIT")); |
||||
connect(editApnButton, &ButtonControl::clicked, [=]() { |
||||
const QString cur_apn = QString::fromStdString(params.get("GsmApn")); |
||||
QString apn = InputDialog::getText(tr("Enter APN"), this, tr("leave blank for automatic configuration"), false, -1, cur_apn).trimmed(); |
||||
|
||||
if (apn.isEmpty()) { |
||||
params.remove("GsmApn"); |
||||
} else { |
||||
params.put("GsmApn", apn.toStdString()); |
||||
} |
||||
wifi->updateGsmSettings(params.getBool("GsmRoaming"), apn, params.getBool("GsmMetered")); |
||||
}); |
||||
list->addItem(editApnButton); |
||||
|
||||
// Cellular metered toggle (prime lite or none)
|
||||
const bool metered = params.getBool("GsmMetered"); |
||||
cellularMeteredToggle = new ToggleControl(tr("Cellular Metered"), tr("Prevent large data uploads when on a metered cellular connection"), "", metered); |
||||
QObject::connect(cellularMeteredToggle, &SshToggle::toggleFlipped, [=](bool state) { |
||||
params.putBool("GsmMetered", state); |
||||
wifi->updateGsmSettings(params.getBool("GsmRoaming"), QString::fromStdString(params.get("GsmApn")), state); |
||||
}); |
||||
list->addItem(cellularMeteredToggle); |
||||
|
||||
// Wi-Fi metered toggle
|
||||
std::vector<QString> metered_button_texts{tr("default"), tr("metered"), tr("unmetered")}; |
||||
wifiMeteredToggle = new MultiButtonControl(tr("Wi-Fi Network Metered"), tr("Prevent large data uploads when on a metered Wi-Fi connection"), "", metered_button_texts); |
||||
QObject::connect(wifiMeteredToggle, &MultiButtonControl::buttonClicked, [=](int id) { |
||||
wifiMeteredToggle->setEnabled(false); |
||||
MeteredType metered = MeteredType::UNKNOWN; |
||||
if (id == NM_METERED_YES) { |
||||
metered = MeteredType::YES; |
||||
} else if (id == NM_METERED_NO) { |
||||
metered = MeteredType::NO; |
||||
} |
||||
auto pending_call = wifi->setCurrentNetworkMetered(metered); |
||||
if (pending_call) { |
||||
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(*pending_call); |
||||
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [=]() { |
||||
refresh(); |
||||
watcher->deleteLater(); |
||||
}); |
||||
} |
||||
}); |
||||
list->addItem(wifiMeteredToggle); |
||||
|
||||
// Hidden Network
|
||||
hiddenNetworkButton = new ButtonControl(tr("Hidden Network"), tr("CONNECT")); |
||||
connect(hiddenNetworkButton, &ButtonControl::clicked, [=]() { |
||||
QString ssid = InputDialog::getText(tr("Enter SSID"), this, "", false, 1); |
||||
if (!ssid.isEmpty()) { |
||||
QString pass = InputDialog::getText(tr("Enter password"), this, tr("for \"%1\"").arg(ssid), true, -1); |
||||
Network hidden_network; |
||||
hidden_network.ssid = ssid.toUtf8(); |
||||
if (!pass.isEmpty()) { |
||||
hidden_network.security_type = SecurityType::WPA; |
||||
wifi->connect(hidden_network, true, pass); |
||||
} else { |
||||
wifi->connect(hidden_network, true); |
||||
} |
||||
emit requestWifiScreen(); |
||||
} |
||||
}); |
||||
list->addItem(hiddenNetworkButton); |
||||
|
||||
// Set initial config
|
||||
wifi->updateGsmSettings(roamingEnabled, QString::fromStdString(params.get("GsmApn")), metered); |
||||
|
||||
main_layout->addWidget(new ScrollView(list, this)); |
||||
main_layout->addStretch(1); |
||||
} |
||||
|
||||
void AdvancedNetworking::setGsmVisible(bool visible) { |
||||
roamingToggle->setVisible(visible); |
||||
editApnButton->setVisible(visible); |
||||
cellularMeteredToggle->setVisible(visible); |
||||
} |
||||
|
||||
void AdvancedNetworking::refresh() { |
||||
ipLabel->setText(wifi->ipv4_address); |
||||
tetheringToggle->setEnabled(true); |
||||
|
||||
if (wifi->isTetheringEnabled() || wifi->ipv4_address == "") { |
||||
wifiMeteredToggle->setEnabled(false); |
||||
wifiMeteredToggle->setCheckedButton(0); |
||||
} else if (wifi->ipv4_address != "") { |
||||
MeteredType metered = wifi->currentNetworkMetered(); |
||||
wifiMeteredToggle->setEnabled(true); |
||||
wifiMeteredToggle->setCheckedButton(static_cast<int>(metered)); |
||||
} |
||||
|
||||
update(); |
||||
} |
||||
|
||||
void AdvancedNetworking::toggleTethering(bool enabled) { |
||||
wifi->setTetheringEnabled(enabled); |
||||
tetheringToggle->setEnabled(false); |
||||
if (enabled) { |
||||
wifiMeteredToggle->setEnabled(false); |
||||
wifiMeteredToggle->setCheckedButton(0); |
||||
} |
||||
} |
||||
|
||||
// WifiUI functions
|
||||
|
||||
WifiUI::WifiUI(QWidget *parent, WifiManager* wifi) : QWidget(parent), wifi(wifi) { |
||||
QVBoxLayout *main_layout = new QVBoxLayout(this); |
||||
main_layout->setContentsMargins(0, 0, 0, 0); |
||||
main_layout->setSpacing(0); |
||||
|
||||
// load imgs
|
||||
for (const auto &s : {"low", "medium", "high", "full"}) { |
||||
QPixmap pix(ASSET_PATH + "/icons/wifi_strength_" + s + ".svg"); |
||||
strengths.push_back(pix.scaledToHeight(68, Qt::SmoothTransformation)); |
||||
} |
||||
lock = QPixmap(ASSET_PATH + "icons/lock_closed.svg").scaledToWidth(ICON_WIDTH, Qt::SmoothTransformation); |
||||
checkmark = QPixmap(ASSET_PATH + "icons/checkmark.svg").scaledToWidth(ICON_WIDTH, Qt::SmoothTransformation); |
||||
circled_slash = QPixmap(ASSET_PATH + "icons/circled_slash.svg").scaledToWidth(ICON_WIDTH, Qt::SmoothTransformation); |
||||
|
||||
scanningLabel = new QLabel(tr("Scanning for networks...")); |
||||
scanningLabel->setStyleSheet("font-size: 65px;"); |
||||
main_layout->addWidget(scanningLabel, 0, Qt::AlignCenter); |
||||
|
||||
wifi_list_widget = new ListWidget(this); |
||||
wifi_list_widget->setVisible(false); |
||||
main_layout->addWidget(wifi_list_widget); |
||||
|
||||
setStyleSheet(R"( |
||||
QScrollBar::handle:vertical { |
||||
min-height: 0px; |
||||
border-radius: 4px; |
||||
background-color: #8A8A8A; |
||||
} |
||||
#forgetBtn { |
||||
font-size: 32px; |
||||
font-weight: 600; |
||||
color: #292929; |
||||
background-color: #BDBDBD; |
||||
border-width: 1px solid #828282; |
||||
border-radius: 5px; |
||||
padding: 40px; |
||||
padding-bottom: 16px; |
||||
padding-top: 16px; |
||||
} |
||||
#forgetBtn:pressed { |
||||
background-color: #828282; |
||||
} |
||||
#connecting { |
||||
font-size: 32px; |
||||
font-weight: 600; |
||||
color: white; |
||||
border-radius: 0; |
||||
padding: 27px; |
||||
padding-left: 43px; |
||||
padding-right: 43px; |
||||
background-color: black; |
||||
} |
||||
#ssidLabel { |
||||
text-align: left; |
||||
border: none; |
||||
padding-top: 50px; |
||||
padding-bottom: 50px; |
||||
} |
||||
#ssidLabel:disabled { |
||||
color: #696969; |
||||
} |
||||
)"); |
||||
} |
||||
|
||||
void WifiUI::refresh() { |
||||
bool is_empty = wifi->seenNetworks.isEmpty(); |
||||
scanningLabel->setVisible(is_empty); |
||||
wifi_list_widget->setVisible(!is_empty); |
||||
if (is_empty) return; |
||||
|
||||
setUpdatesEnabled(false); |
||||
|
||||
const bool is_tethering_enabled = wifi->isTetheringEnabled(); |
||||
QList<Network> sortedNetworks = wifi->seenNetworks.values(); |
||||
std::sort(sortedNetworks.begin(), sortedNetworks.end(), compare_by_strength); |
||||
|
||||
int n = 0; |
||||
for (Network &network : sortedNetworks) { |
||||
QPixmap status_icon; |
||||
if (network.connected == ConnectedType::CONNECTED) { |
||||
status_icon = checkmark; |
||||
} else if (network.security_type == SecurityType::UNSUPPORTED) { |
||||
status_icon = circled_slash; |
||||
} else if (network.security_type == SecurityType::WPA) { |
||||
status_icon = lock; |
||||
} |
||||
bool show_forget_btn = wifi->isKnownConnection(network.ssid) && !is_tethering_enabled; |
||||
QPixmap strength = strengths[strengthLevel(network.strength)]; |
||||
|
||||
auto item = getItem(n++); |
||||
item->setItem(network, status_icon, show_forget_btn, strength); |
||||
item->setVisible(true); |
||||
} |
||||
for (; n < wifi_items.size(); ++n) wifi_items[n]->setVisible(false); |
||||
|
||||
setUpdatesEnabled(true); |
||||
} |
||||
|
||||
WifiItem *WifiUI::getItem(int n) { |
||||
auto item = n < wifi_items.size() ? wifi_items[n] : wifi_items.emplace_back(new WifiItem(tr("CONNECTING..."), tr("FORGET"))); |
||||
if (!item->parentWidget()) { |
||||
QObject::connect(item, &WifiItem::connectToNetwork, this, &WifiUI::connectToNetwork); |
||||
QObject::connect(item, &WifiItem::forgotNetwork, [this](const Network n) { |
||||
if (ConfirmationDialog::confirm(tr("Forget Wi-Fi Network \"%1\"?").arg(QString::fromUtf8(n.ssid)), tr("Forget"), this)) |
||||
wifi->forgetConnection(n.ssid); |
||||
}); |
||||
wifi_list_widget->addItem(item); |
||||
} |
||||
return item; |
||||
} |
||||
|
||||
// WifiItem
|
||||
|
||||
WifiItem::WifiItem(const QString &connecting_text, const QString &forget_text, QWidget *parent) : QWidget(parent) { |
||||
QHBoxLayout *hlayout = new QHBoxLayout(this); |
||||
hlayout->setContentsMargins(44, 0, 73, 0); |
||||
hlayout->setSpacing(50); |
||||
|
||||
hlayout->addWidget(ssidLabel = new ElidedLabel()); |
||||
ssidLabel->setObjectName("ssidLabel"); |
||||
ssidLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); |
||||
hlayout->addWidget(connecting = new QPushButton(connecting_text), 0, Qt::AlignRight); |
||||
connecting->setObjectName("connecting"); |
||||
hlayout->addWidget(forgetBtn = new QPushButton(forget_text), 0, Qt::AlignRight); |
||||
forgetBtn->setObjectName("forgetBtn"); |
||||
hlayout->addWidget(iconLabel = new QLabel(), 0, Qt::AlignRight); |
||||
hlayout->addWidget(strengthLabel = new QLabel(), 0, Qt::AlignRight); |
||||
|
||||
iconLabel->setFixedWidth(ICON_WIDTH); |
||||
QObject::connect(forgetBtn, &QPushButton::clicked, [this]() { emit forgotNetwork(network); }); |
||||
QObject::connect(ssidLabel, &ElidedLabel::clicked, [this]() { |
||||
if (network.connected == ConnectedType::DISCONNECTED) emit connectToNetwork(network); |
||||
}); |
||||
} |
||||
|
||||
void WifiItem::setItem(const Network &n, const QPixmap &status_icon, bool show_forget_btn, const QPixmap &strength_icon) { |
||||
network = n; |
||||
|
||||
ssidLabel->setText(n.ssid); |
||||
ssidLabel->setEnabled(n.security_type != SecurityType::UNSUPPORTED); |
||||
ssidLabel->setFont(InterFont(55, network.connected == ConnectedType::DISCONNECTED ? QFont::Normal : QFont::Bold)); |
||||
|
||||
connecting->setVisible(n.connected == ConnectedType::CONNECTING); |
||||
forgetBtn->setVisible(show_forget_btn); |
||||
|
||||
iconLabel->setPixmap(status_icon); |
||||
strengthLabel->setPixmap(strength_icon); |
||||
} |
||||
@ -1,105 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <vector> |
||||
|
||||
#include "selfdrive/ui/qt/network/wifi_manager.h" |
||||
#include "selfdrive/ui/qt/prime_state.h" |
||||
#include "selfdrive/ui/qt/widgets/input.h" |
||||
#include "selfdrive/ui/qt/widgets/ssh_keys.h" |
||||
#include "selfdrive/ui/qt/widgets/toggle.h" |
||||
|
||||
class WifiItem : public QWidget { |
||||
Q_OBJECT |
||||
public: |
||||
explicit WifiItem(const QString &connecting_text, const QString &forget_text, QWidget* parent = nullptr); |
||||
void setItem(const Network& n, const QPixmap &icon, bool show_forget_btn, const QPixmap &strength); |
||||
|
||||
signals: |
||||
// Cannot pass Network by reference. it may change after the signal is sent.
|
||||
void connectToNetwork(const Network n); |
||||
void forgotNetwork(const Network n); |
||||
|
||||
protected: |
||||
ElidedLabel* ssidLabel; |
||||
QPushButton* connecting; |
||||
QPushButton* forgetBtn; |
||||
QLabel* iconLabel; |
||||
QLabel* strengthLabel; |
||||
Network network; |
||||
}; |
||||
|
||||
class WifiUI : public QWidget { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit WifiUI(QWidget *parent = 0, WifiManager* wifi = 0); |
||||
|
||||
private: |
||||
WifiItem *getItem(int n); |
||||
|
||||
WifiManager *wifi = nullptr; |
||||
QLabel *scanningLabel = nullptr; |
||||
QPixmap lock; |
||||
QPixmap checkmark; |
||||
QPixmap circled_slash; |
||||
QVector<QPixmap> strengths; |
||||
ListWidget *wifi_list_widget = nullptr; |
||||
std::vector<WifiItem*> wifi_items; |
||||
|
||||
signals: |
||||
void connectToNetwork(const Network n); |
||||
|
||||
public slots: |
||||
void refresh(); |
||||
}; |
||||
|
||||
class AdvancedNetworking : public QWidget { |
||||
Q_OBJECT |
||||
public: |
||||
explicit AdvancedNetworking(QWidget* parent = 0, WifiManager* wifi = 0); |
||||
void setGsmVisible(bool visible); |
||||
|
||||
private: |
||||
LabelControl* ipLabel; |
||||
ToggleControl* tetheringToggle; |
||||
ToggleControl* roamingToggle; |
||||
ButtonControl* editApnButton; |
||||
ButtonControl* hiddenNetworkButton; |
||||
ToggleControl* cellularMeteredToggle; |
||||
MultiButtonControl* wifiMeteredToggle; |
||||
WifiManager* wifi = nullptr; |
||||
Params params; |
||||
|
||||
signals: |
||||
void backPress(); |
||||
void requestWifiScreen(); |
||||
|
||||
public slots: |
||||
void toggleTethering(bool enabled); |
||||
void refresh(); |
||||
}; |
||||
|
||||
class Networking : public QFrame { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit Networking(QWidget* parent = 0, bool show_advanced = true); |
||||
void setPrimeType(PrimeState::Type type); |
||||
WifiManager* wifi = nullptr; |
||||
|
||||
private: |
||||
QStackedLayout* main_layout = nullptr; |
||||
QWidget* wifiScreen = nullptr; |
||||
AdvancedNetworking* an = nullptr; |
||||
WifiUI* wifiWidget; |
||||
|
||||
void showEvent(QShowEvent* event) override; |
||||
void hideEvent(QHideEvent* event) override; |
||||
|
||||
public slots: |
||||
void refresh(); |
||||
|
||||
private slots: |
||||
void connectToNetwork(const Network n); |
||||
void wrongPassword(const QString &ssid); |
||||
}; |
||||
@ -1,48 +0,0 @@ |
||||
#pragma once |
||||
|
||||
/**
|
||||
* We are using a NetworkManager DBUS API : https://developer.gnome.org/NetworkManager/1.26/spec.html
|
||||
* */ |
||||
|
||||
// https://developer.gnome.org/NetworkManager/1.26/nm-dbus-types.html#NM80211ApFlags
|
||||
const int NM_802_11_AP_FLAGS_NONE = 0x00000000; |
||||
const int NM_802_11_AP_FLAGS_PRIVACY = 0x00000001; |
||||
const int NM_802_11_AP_FLAGS_WPS = 0x00000002; |
||||
|
||||
// https://developer.gnome.org/NetworkManager/1.26/nm-dbus-types.html#NM80211ApSecurityFlags
|
||||
const int NM_802_11_AP_SEC_PAIR_WEP40 = 0x00000001; |
||||
const int NM_802_11_AP_SEC_PAIR_WEP104 = 0x00000002; |
||||
const int NM_802_11_AP_SEC_GROUP_WEP40 = 0x00000010; |
||||
const int NM_802_11_AP_SEC_GROUP_WEP104 = 0x00000020; |
||||
const int NM_802_11_AP_SEC_KEY_MGMT_PSK = 0x00000100; |
||||
const int NM_802_11_AP_SEC_KEY_MGMT_802_1X = 0x00000200; |
||||
|
||||
const QString NM_DBUS_PATH = "/org/freedesktop/NetworkManager"; |
||||
const QString NM_DBUS_PATH_SETTINGS = "/org/freedesktop/NetworkManager/Settings"; |
||||
|
||||
const QString NM_DBUS_INTERFACE = "org.freedesktop.NetworkManager"; |
||||
const QString NM_DBUS_INTERFACE_PROPERTIES = "org.freedesktop.DBus.Properties"; |
||||
const QString NM_DBUS_INTERFACE_SETTINGS = "org.freedesktop.NetworkManager.Settings"; |
||||
const QString NM_DBUS_INTERFACE_SETTINGS_CONNECTION = "org.freedesktop.NetworkManager.Settings.Connection"; |
||||
const QString NM_DBUS_INTERFACE_DEVICE = "org.freedesktop.NetworkManager.Device"; |
||||
const QString NM_DBUS_INTERFACE_DEVICE_WIRELESS = "org.freedesktop.NetworkManager.Device.Wireless"; |
||||
const QString NM_DBUS_INTERFACE_ACCESS_POINT = "org.freedesktop.NetworkManager.AccessPoint"; |
||||
const QString NM_DBUS_INTERFACE_ACTIVE_CONNECTION = "org.freedesktop.NetworkManager.Connection.Active"; |
||||
const QString NM_DBUS_INTERFACE_IP4_CONFIG = "org.freedesktop.NetworkManager.IP4Config"; |
||||
|
||||
const QString NM_DBUS_SERVICE = "org.freedesktop.NetworkManager"; |
||||
|
||||
const int NM_DEVICE_STATE_UNKNOWN = 0; |
||||
const int NM_DEVICE_STATE_ACTIVATED = 100; |
||||
const int NM_DEVICE_STATE_NEED_AUTH = 60; |
||||
const int NM_DEVICE_TYPE_WIFI = 2; |
||||
const int NM_DEVICE_TYPE_MODEM = 8; |
||||
const int NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8; |
||||
const int DBUS_TIMEOUT = 100; |
||||
|
||||
// https://developer-old.gnome.org/NetworkManager/1.26/nm-dbus-types.html#NMMetered
|
||||
const int NM_METERED_UNKNOWN = 0; |
||||
const int NM_METERED_YES = 1; |
||||
const int NM_METERED_NO = 2; |
||||
const int NM_METERED_GUESS_YES = 3; |
||||
const int NM_METERED_GUESS_NO = 4; |
||||
@ -1,539 +0,0 @@ |
||||
#include "selfdrive/ui/qt/network/wifi_manager.h" |
||||
|
||||
#include <utility> |
||||
|
||||
#include "common/swaglog.h" |
||||
#include "selfdrive/ui/qt/util.h" |
||||
|
||||
bool compare_by_strength(const Network &a, const Network &b) { |
||||
return std::tuple(a.connected, strengthLevel(a.strength), b.ssid) > |
||||
std::tuple(b.connected, strengthLevel(b.strength), a.ssid); |
||||
} |
||||
|
||||
template <typename T = QDBusMessage, typename... Args> |
||||
T call(const QString &path, const QString &interface, const QString &method, Args &&...args) { |
||||
QDBusInterface nm(NM_DBUS_SERVICE, path, interface, QDBusConnection::systemBus()); |
||||
nm.setTimeout(DBUS_TIMEOUT); |
||||
|
||||
QDBusMessage response = nm.call(method, std::forward<Args>(args)...); |
||||
if (response.type() == QDBusMessage::ErrorMessage) { |
||||
qCritical() << "DBus call error:" << response.errorMessage(); |
||||
return T(); |
||||
} |
||||
|
||||
if constexpr (std::is_same_v<T, QDBusMessage>) { |
||||
return response; |
||||
} else if (response.arguments().count() >= 1) { |
||||
QVariant vFirst = response.arguments().at(0).value<QDBusVariant>().variant(); |
||||
if (vFirst.canConvert<T>()) { |
||||
return vFirst.value<T>(); |
||||
} |
||||
QDebug critical = qCritical(); |
||||
critical << "Variant unpacking failure :" << method << ','; |
||||
(critical << ... << args); |
||||
} |
||||
return T(); |
||||
} |
||||
|
||||
template <typename... Args> |
||||
QDBusPendingCall asyncCall(const QString &path, const QString &interface, const QString &method, Args &&...args) { |
||||
QDBusInterface nm = QDBusInterface(NM_DBUS_SERVICE, path, interface, QDBusConnection::systemBus()); |
||||
return nm.asyncCall(method, args...); |
||||
} |
||||
|
||||
bool emptyPath(const QString &path) { |
||||
return path == "" || path == "/"; |
||||
} |
||||
|
||||
WifiManager::WifiManager(QObject *parent) : QObject(parent) { |
||||
qDBusRegisterMetaType<Connection>(); |
||||
qDBusRegisterMetaType<IpConfig>(); |
||||
|
||||
// Set tethering ssid as "weedle" + first 4 characters of a dongle id
|
||||
tethering_ssid = "weedle"; |
||||
if (auto dongle_id = getDongleId()) { |
||||
tethering_ssid += "-" + dongle_id->left(4); |
||||
} |
||||
|
||||
adapter = getAdapter(); |
||||
if (!adapter.isEmpty()) { |
||||
setup(); |
||||
} else { |
||||
QDBusConnection::systemBus().connect(NM_DBUS_SERVICE, NM_DBUS_PATH, NM_DBUS_INTERFACE, "DeviceAdded", this, SLOT(deviceAdded(QDBusObjectPath))); |
||||
} |
||||
|
||||
timer.callOnTimeout(this, &WifiManager::requestScan); |
||||
|
||||
initConnections(); |
||||
} |
||||
|
||||
void WifiManager::setup() { |
||||
auto bus = QDBusConnection::systemBus(); |
||||
bus.connect(NM_DBUS_SERVICE, adapter, NM_DBUS_INTERFACE_DEVICE, "StateChanged", this, SLOT(stateChange(unsigned int, unsigned int, unsigned int))); |
||||
bus.connect(NM_DBUS_SERVICE, adapter, NM_DBUS_INTERFACE_PROPERTIES, "PropertiesChanged", this, SLOT(propertyChange(QString, QVariantMap, QStringList))); |
||||
|
||||
bus.connect(NM_DBUS_SERVICE, NM_DBUS_PATH_SETTINGS, NM_DBUS_INTERFACE_SETTINGS, "ConnectionRemoved", this, SLOT(connectionRemoved(QDBusObjectPath))); |
||||
bus.connect(NM_DBUS_SERVICE, NM_DBUS_PATH_SETTINGS, NM_DBUS_INTERFACE_SETTINGS, "NewConnection", this, SLOT(newConnection(QDBusObjectPath))); |
||||
|
||||
raw_adapter_state = call<uint>(adapter, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_DEVICE, "State"); |
||||
activeAp = call<QDBusObjectPath>(adapter, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_DEVICE_WIRELESS, "ActiveAccessPoint").path(); |
||||
|
||||
requestScan(); |
||||
} |
||||
|
||||
void WifiManager::start() { |
||||
timer.start(5000); |
||||
refreshNetworks(); |
||||
} |
||||
|
||||
void WifiManager::stop() { |
||||
timer.stop(); |
||||
} |
||||
|
||||
void WifiManager::refreshNetworks() { |
||||
if (adapter.isEmpty() || !timer.isActive()) return; |
||||
|
||||
QDBusPendingCall pending_call = asyncCall(adapter, NM_DBUS_INTERFACE_DEVICE_WIRELESS, "GetAllAccessPoints"); |
||||
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending_call); |
||||
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &WifiManager::refreshFinished); |
||||
} |
||||
|
||||
void WifiManager::refreshFinished(QDBusPendingCallWatcher *watcher) { |
||||
ipv4_address = getIp4Address(); |
||||
seenNetworks.clear(); |
||||
|
||||
const QDBusReply<QList<QDBusObjectPath>> watcher_reply = *watcher; |
||||
if (!watcher_reply.isValid()) { |
||||
qCritical() << "Failed to refresh"; |
||||
watcher->deleteLater(); |
||||
return; |
||||
} |
||||
|
||||
for (const QDBusObjectPath &path : watcher_reply.value()) { |
||||
QDBusReply<QVariantMap> reply = call(path.path(), NM_DBUS_INTERFACE_PROPERTIES, "GetAll", NM_DBUS_INTERFACE_ACCESS_POINT); |
||||
if (!reply.isValid()) { |
||||
qCritical() << "Failed to retrieve properties for path:" << path.path(); |
||||
continue; |
||||
} |
||||
|
||||
auto properties = reply.value(); |
||||
const QByteArray ssid = properties["Ssid"].toByteArray(); |
||||
if (ssid.isEmpty()) continue; |
||||
|
||||
// May be multiple access points for each SSID.
|
||||
// Use first for ssid and security type, then update connected status and strength using all
|
||||
if (!seenNetworks.contains(ssid)) { |
||||
seenNetworks[ssid] = {ssid, 0U, ConnectedType::DISCONNECTED, getSecurityType(properties)}; |
||||
} |
||||
|
||||
if (path.path() == activeAp) { |
||||
seenNetworks[ssid].connected = (ssid == connecting_to_network) ? ConnectedType::CONNECTING : ConnectedType::CONNECTED; |
||||
} |
||||
|
||||
uint32_t strength = properties["Strength"].toUInt(); |
||||
if (seenNetworks[ssid].strength < strength) { |
||||
seenNetworks[ssid].strength = strength; |
||||
} |
||||
} |
||||
|
||||
emit refreshSignal(); |
||||
watcher->deleteLater(); |
||||
} |
||||
|
||||
QString WifiManager::getIp4Address() { |
||||
if (raw_adapter_state != NM_DEVICE_STATE_ACTIVATED) return ""; |
||||
|
||||
for (const auto &p : getActiveConnections()) { |
||||
QString type = call<QString>(p.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Type"); |
||||
if (type == "802-11-wireless") { |
||||
auto ip4config = call<QDBusObjectPath>(p.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Ip4Config"); |
||||
const auto &arr = call<QDBusArgument>(ip4config.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_IP4_CONFIG, "AddressData"); |
||||
QVariantMap path; |
||||
arr.beginArray(); |
||||
while (!arr.atEnd()) { |
||||
arr >> path; |
||||
arr.endArray(); |
||||
return path.value("address").value<QString>(); |
||||
} |
||||
arr.endArray(); |
||||
} |
||||
} |
||||
return ""; |
||||
} |
||||
|
||||
SecurityType WifiManager::getSecurityType(const QVariantMap &properties) { |
||||
int sflag = properties["Flags"].toUInt(); |
||||
int wpaflag = properties["WpaFlags"].toUInt(); |
||||
int rsnflag = properties["RsnFlags"].toUInt(); |
||||
int wpa_props = wpaflag | rsnflag; |
||||
|
||||
// obtained by looking at flags of networks in the office as reported by an Android phone
|
||||
const int supports_wpa = NM_802_11_AP_SEC_PAIR_WEP40 | NM_802_11_AP_SEC_PAIR_WEP104 | NM_802_11_AP_SEC_GROUP_WEP40 | NM_802_11_AP_SEC_GROUP_WEP104 | NM_802_11_AP_SEC_KEY_MGMT_PSK; |
||||
|
||||
if ((sflag == NM_802_11_AP_FLAGS_NONE) || ((sflag & NM_802_11_AP_FLAGS_WPS) && !(wpa_props & supports_wpa))) { |
||||
return SecurityType::OPEN; |
||||
} else if ((sflag & NM_802_11_AP_FLAGS_PRIVACY) && (wpa_props & supports_wpa) && !(wpa_props & NM_802_11_AP_SEC_KEY_MGMT_802_1X)) { |
||||
return SecurityType::WPA; |
||||
} else { |
||||
LOGW("Unsupported network! sflag: %d, wpaflag: %d, rsnflag: %d", sflag, wpaflag, rsnflag); |
||||
return SecurityType::UNSUPPORTED; |
||||
} |
||||
} |
||||
|
||||
void WifiManager::connect(const Network &n, const bool is_hidden, const QString &password, const QString &username) { |
||||
setCurrentConnecting(n.ssid); |
||||
forgetConnection(n.ssid); // Clear all connections that may already exist to the network we are connecting
|
||||
Connection connection; |
||||
connection["connection"]["type"] = "802-11-wireless"; |
||||
connection["connection"]["uuid"] = QUuid::createUuid().toString().remove('{').remove('}'); |
||||
connection["connection"]["id"] = "openpilot connection " + QString::fromStdString(n.ssid.toStdString()); |
||||
connection["connection"]["autoconnect-retries"] = 0; |
||||
|
||||
connection["802-11-wireless"]["ssid"] = n.ssid; |
||||
connection["802-11-wireless"]["hidden"] = is_hidden; |
||||
connection["802-11-wireless"]["mode"] = "infrastructure"; |
||||
|
||||
if (n.security_type == SecurityType::WPA) { |
||||
connection["802-11-wireless-security"]["key-mgmt"] = "wpa-psk"; |
||||
connection["802-11-wireless-security"]["auth-alg"] = "open"; |
||||
connection["802-11-wireless-security"]["psk"] = password; |
||||
} |
||||
|
||||
connection["ipv4"]["method"] = "auto"; |
||||
connection["ipv4"]["dns-priority"] = 600; |
||||
connection["ipv6"]["method"] = "ignore"; |
||||
|
||||
asyncCall(NM_DBUS_PATH_SETTINGS, NM_DBUS_INTERFACE_SETTINGS, "AddConnection", QVariant::fromValue(connection)); |
||||
} |
||||
|
||||
void WifiManager::deactivateConnectionBySsid(const QString &ssid) { |
||||
for (QDBusObjectPath active_connection : getActiveConnections()) { |
||||
auto pth = call<QDBusObjectPath>(active_connection.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "SpecificObject"); |
||||
if (!emptyPath(pth.path())) { |
||||
QString Ssid = get_property(pth.path(), "Ssid"); |
||||
if (Ssid == ssid) { |
||||
deactivateConnection(active_connection); |
||||
return; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
void WifiManager::deactivateConnection(const QDBusObjectPath &path) { |
||||
asyncCall(NM_DBUS_PATH, NM_DBUS_INTERFACE, "DeactivateConnection", QVariant::fromValue(path)); |
||||
} |
||||
|
||||
QVector<QDBusObjectPath> WifiManager::getActiveConnections() { |
||||
auto result = call<QDBusArgument>(NM_DBUS_PATH, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE, "ActiveConnections"); |
||||
return qdbus_cast<QVector<QDBusObjectPath>>(result); |
||||
} |
||||
|
||||
bool WifiManager::isKnownConnection(const QString &ssid) { |
||||
return !getConnectionPath(ssid).path().isEmpty(); |
||||
} |
||||
|
||||
void WifiManager::forgetConnection(const QString &ssid) { |
||||
const QDBusObjectPath &path = getConnectionPath(ssid); |
||||
if (!path.path().isEmpty()) { |
||||
call(path.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "Delete"); |
||||
} |
||||
} |
||||
|
||||
void WifiManager::setCurrentConnecting(const QString &ssid) { |
||||
connecting_to_network = ssid; |
||||
for (auto &network : seenNetworks) { |
||||
network.connected = (network.ssid == ssid) ? ConnectedType::CONNECTING : ConnectedType::DISCONNECTED; |
||||
} |
||||
emit refreshSignal(); |
||||
} |
||||
|
||||
uint WifiManager::getAdapterType(const QDBusObjectPath &path) { |
||||
return call<uint>(path.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_DEVICE, "DeviceType"); |
||||
} |
||||
|
||||
void WifiManager::requestScan() { |
||||
if (!adapter.isEmpty()) { |
||||
asyncCall(adapter, NM_DBUS_INTERFACE_DEVICE_WIRELESS, "RequestScan", QVariantMap()); |
||||
} |
||||
} |
||||
|
||||
QByteArray WifiManager::get_property(const QString &network_path , const QString &property) { |
||||
return call<QByteArray>(network_path, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACCESS_POINT, property); |
||||
} |
||||
|
||||
QString WifiManager::getAdapter(const uint adapter_type) { |
||||
QDBusReply<QList<QDBusObjectPath>> response = call(NM_DBUS_PATH, NM_DBUS_INTERFACE, "GetDevices"); |
||||
for (const QDBusObjectPath &path : response.value()) { |
||||
if (getAdapterType(path) == adapter_type) { |
||||
return path.path(); |
||||
} |
||||
} |
||||
return ""; |
||||
} |
||||
|
||||
void WifiManager::stateChange(unsigned int new_state, unsigned int previous_state, unsigned int change_reason) { |
||||
raw_adapter_state = new_state; |
||||
if (new_state == NM_DEVICE_STATE_NEED_AUTH && change_reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT && !connecting_to_network.isEmpty()) { |
||||
forgetConnection(connecting_to_network); |
||||
emit wrongPassword(connecting_to_network); |
||||
} else if (new_state == NM_DEVICE_STATE_ACTIVATED) { |
||||
connecting_to_network = ""; |
||||
refreshNetworks(); |
||||
} |
||||
} |
||||
|
||||
// https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Device.Wireless.html
|
||||
void WifiManager::propertyChange(const QString &interface, const QVariantMap &props, const QStringList &invalidated_props) { |
||||
if (interface == NM_DBUS_INTERFACE_DEVICE_WIRELESS && props.contains("LastScan")) { |
||||
refreshNetworks(); |
||||
} else if (interface == NM_DBUS_INTERFACE_DEVICE_WIRELESS && props.contains("ActiveAccessPoint")) { |
||||
activeAp = props.value("ActiveAccessPoint").value<QDBusObjectPath>().path(); |
||||
} |
||||
} |
||||
|
||||
void WifiManager::deviceAdded(const QDBusObjectPath &path) { |
||||
if (getAdapterType(path) == NM_DEVICE_TYPE_WIFI && emptyPath(adapter)) { |
||||
adapter = path.path(); |
||||
setup(); |
||||
} |
||||
} |
||||
|
||||
void WifiManager::connectionRemoved(const QDBusObjectPath &path) { |
||||
knownConnections.remove(path); |
||||
} |
||||
|
||||
void WifiManager::newConnection(const QDBusObjectPath &path) { |
||||
Connection settings = getConnectionSettings(path); |
||||
if (settings.value("connection").value("type") == "802-11-wireless") { |
||||
knownConnections[path] = settings.value("802-11-wireless").value("ssid").toString(); |
||||
if (knownConnections[path] != tethering_ssid) { |
||||
activateWifiConnection(knownConnections[path]); |
||||
} |
||||
} |
||||
} |
||||
|
||||
QDBusObjectPath WifiManager::getConnectionPath(const QString &ssid) { |
||||
return knownConnections.key(ssid); |
||||
} |
||||
|
||||
Connection WifiManager::getConnectionSettings(const QDBusObjectPath &path) { |
||||
return QDBusReply<Connection>(call(path.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "GetSettings")).value(); |
||||
} |
||||
|
||||
void WifiManager::initConnections() { |
||||
const QDBusReply<QList<QDBusObjectPath>> response = call(NM_DBUS_PATH_SETTINGS, NM_DBUS_INTERFACE_SETTINGS, "ListConnections"); |
||||
for (const QDBusObjectPath &path : response.value()) { |
||||
const Connection settings = getConnectionSettings(path); |
||||
if (settings.value("connection").value("type") == "802-11-wireless") { |
||||
knownConnections[path] = settings.value("802-11-wireless").value("ssid").toString(); |
||||
} else if (settings.value("connection").value("id") == "lte") { |
||||
lteConnectionPath = path; |
||||
} |
||||
} |
||||
|
||||
if (!isKnownConnection(tethering_ssid)) { |
||||
addTetheringConnection(); |
||||
} |
||||
} |
||||
|
||||
std::optional<QDBusPendingCall> WifiManager::activateWifiConnection(const QString &ssid) { |
||||
const QDBusObjectPath &path = getConnectionPath(ssid); |
||||
if (!path.path().isEmpty()) { |
||||
setCurrentConnecting(ssid); |
||||
return asyncCall(NM_DBUS_PATH, NM_DBUS_INTERFACE, "ActivateConnection", QVariant::fromValue(path), QVariant::fromValue(QDBusObjectPath(adapter)), QVariant::fromValue(QDBusObjectPath("/"))); |
||||
} |
||||
return std::nullopt; |
||||
} |
||||
|
||||
void WifiManager::activateModemConnection(const QDBusObjectPath &path) { |
||||
QString modem = getAdapter(NM_DEVICE_TYPE_MODEM); |
||||
if (!path.path().isEmpty() && !modem.isEmpty()) { |
||||
asyncCall(NM_DBUS_PATH, NM_DBUS_INTERFACE, "ActivateConnection", QVariant::fromValue(path), QVariant::fromValue(QDBusObjectPath(modem)), QVariant::fromValue(QDBusObjectPath("/"))); |
||||
} |
||||
} |
||||
|
||||
// function matches tici/hardware.py
|
||||
// FIXME: it can mistakenly show CELL when connected to WIFI
|
||||
NetworkType WifiManager::currentNetworkType() { |
||||
auto primary_conn = call<QDBusObjectPath>(NM_DBUS_PATH, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE, "PrimaryConnection"); |
||||
auto primary_type = call<QString>(primary_conn.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Type"); |
||||
|
||||
if (primary_type == "802-3-ethernet") { |
||||
return NetworkType::ETHERNET; |
||||
} else if (primary_type == "802-11-wireless" && !isTetheringEnabled()) { |
||||
return NetworkType::WIFI; |
||||
} else { |
||||
for (const QDBusObjectPath &conn : getActiveConnections()) { |
||||
auto type = call<QString>(conn.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Type"); |
||||
if (type == "gsm") { |
||||
return NetworkType::CELL; |
||||
} |
||||
} |
||||
} |
||||
return NetworkType::NONE; |
||||
} |
||||
|
||||
MeteredType WifiManager::currentNetworkMetered() { |
||||
MeteredType metered = MeteredType::UNKNOWN; |
||||
for (const auto &active_conn : getActiveConnections()) { |
||||
QString type = call<QString>(active_conn.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Type"); |
||||
if (type == "802-11-wireless") { |
||||
QDBusObjectPath conn = call<QDBusObjectPath>(active_conn.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Connection"); |
||||
if (!conn.path().isEmpty()) { |
||||
Connection settings = getConnectionSettings(conn); |
||||
int metered_prop = settings.value("connection").value("metered").toInt(); |
||||
if (metered_prop == NM_METERED_YES) { |
||||
metered = MeteredType::YES; |
||||
} else if (metered_prop == NM_METERED_NO) { |
||||
metered = MeteredType::NO; |
||||
} |
||||
} |
||||
break; |
||||
} |
||||
} |
||||
return metered; |
||||
} |
||||
|
||||
std::optional<QDBusPendingCall> WifiManager::setCurrentNetworkMetered(MeteredType metered) { |
||||
for (const auto &active_conn : getActiveConnections()) { |
||||
QString type = call<QString>(active_conn.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Type"); |
||||
if (type == "802-11-wireless") { |
||||
if (!isTetheringEnabled()) { |
||||
QDBusObjectPath conn = call<QDBusObjectPath>(active_conn.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Connection"); |
||||
if (!conn.path().isEmpty()) { |
||||
Connection settings = getConnectionSettings(conn); |
||||
settings["connection"]["metered"] = static_cast<int>(metered); |
||||
return asyncCall(conn.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "Update", QVariant::fromValue(settings)); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return std::nullopt; |
||||
} |
||||
|
||||
void WifiManager::updateGsmSettings(bool roaming, QString apn, bool metered) { |
||||
if (!lteConnectionPath.path().isEmpty()) { |
||||
bool changes = false; |
||||
bool auto_config = apn.isEmpty(); |
||||
Connection settings = getConnectionSettings(lteConnectionPath); |
||||
if (settings.value("gsm").value("auto-config").toBool() != auto_config) { |
||||
qWarning() << "Changing gsm.auto-config to" << auto_config; |
||||
settings["gsm"]["auto-config"] = auto_config; |
||||
changes = true; |
||||
} |
||||
|
||||
if (settings.value("gsm").value("apn").toString() != apn) { |
||||
qWarning() << "Changing gsm.apn to" << apn; |
||||
settings["gsm"]["apn"] = apn; |
||||
changes = true; |
||||
} |
||||
|
||||
if (settings.value("gsm").value("home-only").toBool() == roaming) { |
||||
qWarning() << "Changing gsm.home-only to" << !roaming; |
||||
settings["gsm"]["home-only"] = !roaming; |
||||
changes = true; |
||||
} |
||||
|
||||
int meteredInt = metered ? NM_METERED_UNKNOWN : NM_METERED_NO; |
||||
if (settings.value("connection").value("metered").toInt() != meteredInt) { |
||||
qWarning() << "Changing connection.metered to" << meteredInt; |
||||
settings["connection"]["metered"] = meteredInt; |
||||
changes = true; |
||||
} |
||||
|
||||
if (changes) { |
||||
QDBusPendingCall pending_call = asyncCall(lteConnectionPath.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "UpdateUnsaved", QVariant::fromValue(settings)); // update is temporary
|
||||
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending_call); |
||||
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher]() { |
||||
deactivateConnection(lteConnectionPath); |
||||
activateModemConnection(lteConnectionPath); |
||||
watcher->deleteLater(); |
||||
}); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Functions for tethering
|
||||
void WifiManager::addTetheringConnection() { |
||||
Connection connection; |
||||
connection["connection"]["id"] = "Hotspot"; |
||||
connection["connection"]["uuid"] = QUuid::createUuid().toString().remove('{').remove('}'); |
||||
connection["connection"]["type"] = "802-11-wireless"; |
||||
connection["connection"]["interface-name"] = "wlan0"; |
||||
connection["connection"]["autoconnect"] = false; |
||||
|
||||
connection["802-11-wireless"]["band"] = "bg"; |
||||
connection["802-11-wireless"]["mode"] = "ap"; |
||||
connection["802-11-wireless"]["ssid"] = tethering_ssid.toUtf8(); |
||||
|
||||
connection["802-11-wireless-security"]["group"] = QStringList("ccmp"); |
||||
connection["802-11-wireless-security"]["key-mgmt"] = "wpa-psk"; |
||||
connection["802-11-wireless-security"]["pairwise"] = QStringList("ccmp"); |
||||
connection["802-11-wireless-security"]["proto"] = QStringList("rsn"); |
||||
connection["802-11-wireless-security"]["psk"] = defaultTetheringPassword; |
||||
|
||||
connection["ipv4"]["method"] = "shared"; |
||||
QVariantMap address; |
||||
address["address"] = "192.168.43.1"; |
||||
address["prefix"] = 24u; |
||||
connection["ipv4"]["address-data"] = QVariant::fromValue(IpConfig() << address); |
||||
connection["ipv4"]["gateway"] = "192.168.43.1"; |
||||
connection["ipv4"]["never-default"] = true; |
||||
connection["ipv6"]["method"] = "ignore"; |
||||
|
||||
asyncCall(NM_DBUS_PATH_SETTINGS, NM_DBUS_INTERFACE_SETTINGS, "AddConnection", QVariant::fromValue(connection)); |
||||
} |
||||
|
||||
void WifiManager::tetheringActivated(QDBusPendingCallWatcher *call) { |
||||
if (!ipv4_forward) { |
||||
QTimer::singleShot(5000, this, [=] { |
||||
qWarning() << "net.ipv4.ip_forward = 0"; |
||||
std::system("sudo sysctl net.ipv4.ip_forward=0"); |
||||
}); |
||||
} |
||||
call->deleteLater(); |
||||
tethering_on = true; |
||||
} |
||||
|
||||
void WifiManager::setTetheringEnabled(bool enabled) { |
||||
if (enabled) { |
||||
auto pending_call = activateWifiConnection(tethering_ssid); |
||||
|
||||
if (pending_call) { |
||||
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(*pending_call); |
||||
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &WifiManager::tetheringActivated); |
||||
} |
||||
|
||||
} else { |
||||
deactivateConnectionBySsid(tethering_ssid); |
||||
tethering_on = false; |
||||
} |
||||
} |
||||
|
||||
bool WifiManager::isTetheringEnabled() { |
||||
if (!emptyPath(activeAp)) { |
||||
return get_property(activeAp, "Ssid") == tethering_ssid; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
QString WifiManager::getTetheringPassword() { |
||||
const QDBusObjectPath &path = getConnectionPath(tethering_ssid); |
||||
if (!path.path().isEmpty()) { |
||||
QDBusReply<QMap<QString, QVariantMap>> response = call(path.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "GetSecrets", "802-11-wireless-security"); |
||||
return response.value().value("802-11-wireless-security").value("psk").toString(); |
||||
} |
||||
return ""; |
||||
} |
||||
|
||||
void WifiManager::changeTetheringPassword(const QString &newPassword) { |
||||
const QDBusObjectPath &path = getConnectionPath(tethering_ssid); |
||||
if (!path.path().isEmpty()) { |
||||
Connection settings = getConnectionSettings(path); |
||||
settings["802-11-wireless-security"]["psk"] = newPassword; |
||||
call(path.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "Update", QVariant::fromValue(settings)); |
||||
if (isTetheringEnabled()) { |
||||
activateWifiConnection(tethering_ssid); |
||||
} |
||||
} |
||||
} |
||||
@ -1,111 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <optional> |
||||
#include <QtDBus> |
||||
#include <QTimer> |
||||
|
||||
#include "selfdrive/ui/qt/network/networkmanager.h" |
||||
|
||||
enum class SecurityType { |
||||
OPEN, |
||||
WPA, |
||||
UNSUPPORTED |
||||
}; |
||||
enum class ConnectedType { |
||||
DISCONNECTED, |
||||
CONNECTING, |
||||
CONNECTED |
||||
}; |
||||
enum class NetworkType { |
||||
NONE, |
||||
WIFI, |
||||
CELL, |
||||
ETHERNET |
||||
}; |
||||
enum class MeteredType { |
||||
UNKNOWN, |
||||
YES, |
||||
NO |
||||
}; |
||||
|
||||
typedef QMap<QString, QVariantMap> Connection; |
||||
typedef QVector<QVariantMap> IpConfig; |
||||
|
||||
struct Network { |
||||
QByteArray ssid; |
||||
unsigned int strength; |
||||
ConnectedType connected; |
||||
SecurityType security_type; |
||||
}; |
||||
bool compare_by_strength(const Network &a, const Network &b); |
||||
inline int strengthLevel(unsigned int strength) { return std::clamp((int)round(strength / 33.), 0, 3); } |
||||
|
||||
class WifiManager : public QObject { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
QMap<QString, Network> seenNetworks; |
||||
QMap<QDBusObjectPath, QString> knownConnections; |
||||
QString ipv4_address; |
||||
bool tethering_on = false; |
||||
bool ipv4_forward = false; |
||||
|
||||
explicit WifiManager(QObject* parent); |
||||
void start(); |
||||
void stop(); |
||||
void requestScan(); |
||||
void forgetConnection(const QString &ssid); |
||||
bool isKnownConnection(const QString &ssid); |
||||
std::optional<QDBusPendingCall> activateWifiConnection(const QString &ssid); |
||||
NetworkType currentNetworkType(); |
||||
MeteredType currentNetworkMetered(); |
||||
std::optional<QDBusPendingCall> setCurrentNetworkMetered(MeteredType metered); |
||||
void updateGsmSettings(bool roaming, QString apn, bool metered); |
||||
void connect(const Network &ssid, const bool is_hidden = false, const QString &password = {}, const QString &username = {}); |
||||
|
||||
// Tethering functions
|
||||
void setTetheringEnabled(bool enabled); |
||||
bool isTetheringEnabled(); |
||||
void changeTetheringPassword(const QString &newPassword); |
||||
QString getTetheringPassword(); |
||||
|
||||
private: |
||||
QString adapter; // Path to network manager wifi-device
|
||||
QTimer timer; |
||||
unsigned int raw_adapter_state = NM_DEVICE_STATE_UNKNOWN; // Connection status https://developer.gnome.org/NetworkManager/1.26/nm-dbus-types.html#NMDeviceState
|
||||
QString connecting_to_network; |
||||
QString tethering_ssid; |
||||
const QString defaultTetheringPassword = "swagswagcomma"; |
||||
QString activeAp; |
||||
QDBusObjectPath lteConnectionPath; |
||||
|
||||
QString getAdapter(const uint = NM_DEVICE_TYPE_WIFI); |
||||
uint getAdapterType(const QDBusObjectPath &path); |
||||
QString getIp4Address(); |
||||
void deactivateConnectionBySsid(const QString &ssid); |
||||
void deactivateConnection(const QDBusObjectPath &path); |
||||
QVector<QDBusObjectPath> getActiveConnections(); |
||||
QByteArray get_property(const QString &network_path, const QString &property); |
||||
SecurityType getSecurityType(const QVariantMap &properties); |
||||
QDBusObjectPath getConnectionPath(const QString &ssid); |
||||
Connection getConnectionSettings(const QDBusObjectPath &path); |
||||
void initConnections(); |
||||
void setup(); |
||||
void refreshNetworks(); |
||||
void activateModemConnection(const QDBusObjectPath &path); |
||||
void addTetheringConnection(); |
||||
void setCurrentConnecting(const QString &ssid); |
||||
|
||||
signals: |
||||
void wrongPassword(const QString &ssid); |
||||
void refreshSignal(); |
||||
|
||||
private slots: |
||||
void stateChange(unsigned int new_state, unsigned int previous_state, unsigned int change_reason); |
||||
void propertyChange(const QString &interface, const QVariantMap &props, const QStringList &invalidated_props); |
||||
void deviceAdded(const QDBusObjectPath &path); |
||||
void connectionRemoved(const QDBusObjectPath &path); |
||||
void newConnection(const QDBusObjectPath &path); |
||||
void refreshFinished(QDBusPendingCallWatcher *call); |
||||
void tetheringActivated(QDBusPendingCallWatcher *call); |
||||
}; |
||||
@ -1,95 +0,0 @@ |
||||
#include "selfdrive/ui/qt/offroad/developer_panel.h" |
||||
#include "selfdrive/ui/qt/widgets/ssh_keys.h" |
||||
#include "selfdrive/ui/qt/widgets/controls.h" |
||||
|
||||
DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) { |
||||
adbToggle = new ParamControl("AdbEnabled", tr("Enable ADB"), |
||||
tr("ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info."), ""); |
||||
addItem(adbToggle); |
||||
|
||||
// SSH keys
|
||||
addItem(new SshToggle()); |
||||
addItem(new SshControl()); |
||||
|
||||
joystickToggle = new ParamControl("JoystickDebugMode", tr("Joystick Debug Mode"), "", ""); |
||||
QObject::connect(joystickToggle, &ParamControl::toggleFlipped, [=](bool state) { |
||||
params.putBool("LongitudinalManeuverMode", false); |
||||
longManeuverToggle->refresh(); |
||||
}); |
||||
addItem(joystickToggle); |
||||
|
||||
longManeuverToggle = new ParamControl("LongitudinalManeuverMode", tr("Longitudinal Maneuver Mode"), "", ""); |
||||
QObject::connect(longManeuverToggle, &ParamControl::toggleFlipped, [=](bool state) { |
||||
params.putBool("JoystickDebugMode", false); |
||||
joystickToggle->refresh(); |
||||
}); |
||||
addItem(longManeuverToggle); |
||||
|
||||
experimentalLongitudinalToggle = new ParamControl( |
||||
"AlphaLongitudinalEnabled", |
||||
tr("openpilot Longitudinal Control (Alpha)"), |
||||
QString("<b>%1</b><br><br>%2") |
||||
.arg(tr("WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB).")) |
||||
.arg(tr("On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. " |
||||
"Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha.")), |
||||
"" |
||||
); |
||||
experimentalLongitudinalToggle->setConfirmation(true, false); |
||||
QObject::connect(experimentalLongitudinalToggle, &ParamControl::toggleFlipped, [=]() { |
||||
updateToggles(offroad); |
||||
}); |
||||
addItem(experimentalLongitudinalToggle); |
||||
|
||||
// Joystick and longitudinal maneuvers should be hidden on release branches
|
||||
is_release = params.getBool("IsReleaseBranch"); |
||||
|
||||
// Toggles should be not available to change in onroad state
|
||||
QObject::connect(uiState(), &UIState::offroadTransition, this, &DeveloperPanel::updateToggles); |
||||
} |
||||
|
||||
void DeveloperPanel::updateToggles(bool _offroad) { |
||||
for (auto btn : findChildren<ParamControl *>()) { |
||||
btn->setVisible(!is_release); |
||||
|
||||
/*
|
||||
* experimentalLongitudinalToggle should be toggelable when: |
||||
* - visible, and |
||||
* - during onroad & offroad states |
||||
*/ |
||||
if (btn != experimentalLongitudinalToggle) { |
||||
btn->setEnabled(_offroad); |
||||
} |
||||
} |
||||
|
||||
// longManeuverToggle and experimentalLongitudinalToggle should not be toggleable if the car does not have longitudinal control
|
||||
auto cp_bytes = params.get("CarParamsPersistent"); |
||||
if (!cp_bytes.empty()) { |
||||
AlignedBuffer aligned_buf; |
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size())); |
||||
cereal::CarParams::Reader CP = cmsg.getRoot<cereal::CarParams>(); |
||||
|
||||
if (!CP.getAlphaLongitudinalAvailable() || is_release) { |
||||
params.remove("AlphaLongitudinalEnabled"); |
||||
experimentalLongitudinalToggle->setEnabled(false); |
||||
} |
||||
|
||||
/*
|
||||
* experimentalLongitudinalToggle should be visible when: |
||||
* - is not a release branch, and |
||||
* - the car supports experimental longitudinal control (alpha) |
||||
*/ |
||||
experimentalLongitudinalToggle->setVisible(CP.getAlphaLongitudinalAvailable() && !is_release); |
||||
|
||||
longManeuverToggle->setEnabled(hasLongitudinalControl(CP) && _offroad); |
||||
} else { |
||||
longManeuverToggle->setEnabled(false); |
||||
experimentalLongitudinalToggle->setVisible(false); |
||||
} |
||||
experimentalLongitudinalToggle->refresh(); |
||||
|
||||
offroad = _offroad; |
||||
} |
||||
|
||||
void DeveloperPanel::showEvent(QShowEvent *event) { |
||||
updateToggles(offroad); |
||||
} |
||||
@ -1,22 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include "selfdrive/ui/qt/offroad/settings.h" |
||||
|
||||
class DeveloperPanel : public ListWidget { |
||||
Q_OBJECT |
||||
public: |
||||
explicit DeveloperPanel(SettingsWindow *parent); |
||||
void showEvent(QShowEvent *event) override; |
||||
|
||||
private: |
||||
Params params; |
||||
ParamControl* adbToggle; |
||||
ParamControl* joystickToggle; |
||||
ParamControl* longManeuverToggle; |
||||
ParamControl* experimentalLongitudinalToggle; |
||||
bool is_release; |
||||
bool offroad = false; |
||||
|
||||
private slots: |
||||
void updateToggles(bool _offroad); |
||||
}; |
||||
@ -1,82 +0,0 @@ |
||||
#include "selfdrive/ui/qt/offroad/driverview.h" |
||||
|
||||
#include <algorithm> |
||||
#include <QPainter> |
||||
|
||||
#include "selfdrive/ui/qt/util.h" |
||||
|
||||
DriverViewWindow::DriverViewWindow(QWidget* parent) : CameraWidget("camerad", VISION_STREAM_DRIVER, parent) { |
||||
QObject::connect(this, &CameraWidget::clicked, this, &DriverViewWindow::done); |
||||
QObject::connect(device(), &Device::interactiveTimeout, this, [this]() { |
||||
if (isVisible()) { |
||||
emit done(); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
void DriverViewWindow::showEvent(QShowEvent* event) { |
||||
params.putBool("IsDriverViewEnabled", true); |
||||
device()->resetInteractiveTimeout(60); |
||||
CameraWidget::showEvent(event); |
||||
} |
||||
|
||||
void DriverViewWindow::hideEvent(QHideEvent* event) { |
||||
params.putBool("IsDriverViewEnabled", false); |
||||
stopVipcThread(); |
||||
CameraWidget::hideEvent(event); |
||||
} |
||||
|
||||
void DriverViewWindow::paintGL() { |
||||
CameraWidget::paintGL(); |
||||
|
||||
std::lock_guard lk(frame_lock); |
||||
QPainter p(this); |
||||
// startup msg
|
||||
if (frames.empty()) { |
||||
p.setPen(Qt::white); |
||||
p.setRenderHint(QPainter::TextAntialiasing); |
||||
p.setFont(InterFont(100, QFont::Bold)); |
||||
p.drawText(geometry(), Qt::AlignCenter, tr("camera starting")); |
||||
return; |
||||
} |
||||
|
||||
const auto &sm = *(uiState()->sm); |
||||
cereal::DriverStateV2::Reader driver_state = sm["driverStateV2"].getDriverStateV2(); |
||||
bool is_rhd = driver_state.getWheelOnRightProb() > 0.5; |
||||
auto driver_data = is_rhd ? driver_state.getRightDriverData() : driver_state.getLeftDriverData(); |
||||
|
||||
bool face_detected = driver_data.getFaceProb() > 0.7; |
||||
if (face_detected) { |
||||
auto fxy_list = driver_data.getFacePosition(); |
||||
auto std_list = driver_data.getFaceOrientationStd(); |
||||
float face_x = fxy_list[0]; |
||||
float face_y = fxy_list[1]; |
||||
float face_std = std::max(std_list[0], std_list[1]); |
||||
|
||||
float alpha = 0.7; |
||||
if (face_std > 0.15) { |
||||
alpha = std::max(0.7 - (face_std-0.15)*3.5, 0.0); |
||||
} |
||||
const int box_size = 220; |
||||
// use approx instead of distort_points
|
||||
int fbox_x = 1080.0 - 1714.0 * face_x; |
||||
int fbox_y = -135.0 + (504.0 + std::abs(face_x)*112.0) + (1205.0 - std::abs(face_x)*724.0) * face_y; |
||||
p.setPen(QPen(QColor(255, 255, 255, alpha * 255), 10)); |
||||
p.drawRoundedRect(fbox_x - box_size / 2, fbox_y - box_size / 2, box_size, box_size, 35.0, 35.0); |
||||
} |
||||
|
||||
driver_monitor.updateState(*uiState()); |
||||
driver_monitor.draw(p, rect()); |
||||
} |
||||
|
||||
mat4 DriverViewWindow::calcFrameMatrix() { |
||||
const float driver_view_ratio = 2.0; |
||||
const float yscale = stream_height * driver_view_ratio / stream_width; |
||||
const float xscale = yscale * glHeight() / glWidth() * stream_width / stream_height; |
||||
return mat4{{ |
||||
xscale, 0.0, 0.0, 0.0, |
||||
0.0, yscale, 0.0, 0.0, |
||||
0.0, 0.0, 1.0, 0.0, |
||||
0.0, 0.0, 0.0, 1.0, |
||||
}}; |
||||
} |
||||
@ -1,23 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include "selfdrive/ui/qt/widgets/cameraview.h" |
||||
#include "selfdrive/ui/qt/onroad/driver_monitoring.h" |
||||
|
||||
class DriverViewWindow : public CameraWidget { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit DriverViewWindow(QWidget *parent); |
||||
|
||||
signals: |
||||
void done(); |
||||
|
||||
protected: |
||||
mat4 calcFrameMatrix() override; |
||||
void showEvent(QShowEvent *event) override; |
||||
void hideEvent(QHideEvent *event) override; |
||||
void paintGL() override; |
||||
|
||||
Params params; |
||||
DriverMonitorRenderer driver_monitor; |
||||
}; |
||||
@ -1,76 +0,0 @@ |
||||
#include "selfdrive/ui/qt/offroad/experimental_mode.h" |
||||
|
||||
#include <QDebug> |
||||
#include <QHBoxLayout> |
||||
#include <QPainter> |
||||
#include <QPainterPath> |
||||
#include <QStyle> |
||||
|
||||
#include "selfdrive/ui/ui.h" |
||||
|
||||
ExperimentalModeButton::ExperimentalModeButton(QWidget *parent) : QPushButton(parent) { |
||||
chill_pixmap = QPixmap("../assets/icons/couch.svg").scaledToWidth(img_width, Qt::SmoothTransformation); |
||||
experimental_pixmap = QPixmap("../assets/icons/experimental_grey.svg").scaledToWidth(img_width, Qt::SmoothTransformation); |
||||
|
||||
// go to toggles and expand experimental mode description
|
||||
connect(this, &QPushButton::clicked, [=]() { emit openSettings(2, "ExperimentalMode"); }); |
||||
|
||||
setFixedHeight(125); |
||||
QHBoxLayout *main_layout = new QHBoxLayout; |
||||
main_layout->setContentsMargins(horizontal_padding, 0, horizontal_padding, 0); |
||||
|
||||
mode_label = new QLabel; |
||||
mode_icon = new QLabel; |
||||
mode_icon->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); |
||||
|
||||
main_layout->addWidget(mode_label, 1, Qt::AlignLeft); |
||||
main_layout->addWidget(mode_icon, 0, Qt::AlignRight); |
||||
|
||||
setLayout(main_layout); |
||||
|
||||
setStyleSheet(R"( |
||||
QPushButton { |
||||
border: none; |
||||
} |
||||
|
||||
QLabel { |
||||
font-size: 45px; |
||||
font-weight: 300; |
||||
text-align: left; |
||||
font-family: JetBrainsMono; |
||||
color: #000000; |
||||
} |
||||
)"); |
||||
} |
||||
|
||||
void ExperimentalModeButton::paintEvent(QPaintEvent *event) { |
||||
QPainter p(this); |
||||
p.setPen(Qt::NoPen); |
||||
p.setRenderHint(QPainter::Antialiasing); |
||||
|
||||
QPainterPath path; |
||||
path.addRoundedRect(rect(), 10, 10); |
||||
|
||||
// gradient
|
||||
bool pressed = isDown(); |
||||
QLinearGradient gradient(rect().left(), 0, rect().right(), 0); |
||||
if (experimental_mode) { |
||||
gradient.setColorAt(0, QColor(255, 155, 63, pressed ? 0xcc : 0xff)); |
||||
gradient.setColorAt(1, QColor(219, 56, 34, pressed ? 0xcc : 0xff)); |
||||
} else { |
||||
gradient.setColorAt(0, QColor(20, 255, 171, pressed ? 0xcc : 0xff)); |
||||
gradient.setColorAt(1, QColor(35, 149, 255, pressed ? 0xcc : 0xff)); |
||||
} |
||||
p.fillPath(path, gradient); |
||||
|
||||
// vertical line
|
||||
p.setPen(QPen(QColor(0, 0, 0, 0x4d), 3, Qt::SolidLine)); |
||||
int line_x = rect().right() - img_width - (2 * horizontal_padding); |
||||
p.drawLine(line_x, rect().bottom(), line_x, rect().top()); |
||||
} |
||||
|
||||
void ExperimentalModeButton::showEvent(QShowEvent *event) { |
||||
experimental_mode = params.getBool("ExperimentalMode"); |
||||
mode_icon->setPixmap(experimental_mode ? experimental_pixmap : chill_pixmap); |
||||
mode_label->setText(experimental_mode ? tr("EXPERIMENTAL MODE ON") : tr("CHILL MODE ON")); |
||||
} |
||||
@ -1,31 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <QLabel> |
||||
#include <QPushButton> |
||||
|
||||
#include "common/params.h" |
||||
|
||||
class ExperimentalModeButton : public QPushButton { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit ExperimentalModeButton(QWidget* parent = 0); |
||||
|
||||
signals: |
||||
void openSettings(int index = 0, const QString &toggle = ""); |
||||
|
||||
private: |
||||
void showEvent(QShowEvent *event) override; |
||||
|
||||
Params params; |
||||
bool experimental_mode; |
||||
int img_width = 100; |
||||
int horizontal_padding = 30; |
||||
QPixmap experimental_pixmap; |
||||
QPixmap chill_pixmap; |
||||
QLabel *mode_label; |
||||
QLabel *mode_icon; |
||||
|
||||
protected: |
||||
void paintEvent(QPaintEvent *event) override; |
||||
}; |
||||
@ -1,112 +0,0 @@ |
||||
#include "selfdrive/ui/qt/offroad/firehose.h" |
||||
#include "selfdrive/ui/ui.h" |
||||
#include "selfdrive/ui/qt/offroad/settings.h" |
||||
|
||||
#include <QLabel> |
||||
#include <QPushButton> |
||||
#include <QVBoxLayout> |
||||
#include <QHBoxLayout> |
||||
#include <QFrame> |
||||
#include <QScrollArea> |
||||
#include <QStackedLayout> |
||||
#include <QProgressBar> |
||||
#include <QJsonDocument> |
||||
#include <QJsonObject> |
||||
#include <QTimer> |
||||
|
||||
FirehosePanel::FirehosePanel(SettingsWindow *parent) : QWidget((QWidget*)parent) { |
||||
layout = new QVBoxLayout(this); |
||||
layout->setContentsMargins(40, 40, 40, 40); |
||||
layout->setSpacing(20); |
||||
|
||||
// header
|
||||
QLabel *title = new QLabel(tr("Firehose Mode")); |
||||
title->setStyleSheet("font-size: 100px; font-weight: 500; font-family: 'Noto Color Emoji';"); |
||||
layout->addWidget(title, 0, Qt::AlignCenter); |
||||
|
||||
// Create a container for the content
|
||||
QFrame *content = new QFrame(); |
||||
content->setStyleSheet("background-color: #292929; border-radius: 15px; padding: 20px;"); |
||||
QVBoxLayout *content_layout = new QVBoxLayout(content); |
||||
content_layout->setSpacing(20); |
||||
|
||||
// Top description
|
||||
QLabel *description = new QLabel(tr("openpilot learns to drive by watching humans, like you, drive.\n\nFirehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode.")); |
||||
description->setStyleSheet("font-size: 45px; padding-bottom: 20px;"); |
||||
description->setWordWrap(true); |
||||
content_layout->addWidget(description); |
||||
|
||||
// Add a separator
|
||||
QFrame *line = new QFrame(); |
||||
line->setFrameShape(QFrame::HLine); |
||||
line->setFrameShadow(QFrame::Sunken); |
||||
line->setStyleSheet("background-color: #444444; margin-top: 5px; margin-bottom: 5px;"); |
||||
content_layout->addWidget(line); |
||||
|
||||
toggle_label = new QLabel(tr("Firehose Mode: ACTIVE")); |
||||
toggle_label->setStyleSheet("font-size: 60px; font-weight: bold; color: white;"); |
||||
content_layout->addWidget(toggle_label); |
||||
|
||||
// Add contribution label
|
||||
contribution_label = new QLabel(); |
||||
contribution_label->setStyleSheet("font-size: 52px; margin-top: 10px; margin-bottom: 10px;"); |
||||
contribution_label->setWordWrap(true); |
||||
contribution_label->hide(); |
||||
content_layout->addWidget(contribution_label); |
||||
|
||||
// Add a separator before detailed instructions
|
||||
QFrame *line2 = new QFrame(); |
||||
line2->setFrameShape(QFrame::HLine); |
||||
line2->setFrameShadow(QFrame::Sunken); |
||||
line2->setStyleSheet("background-color: #444444; margin-top: 10px; margin-bottom: 10px;"); |
||||
content_layout->addWidget(line2); |
||||
|
||||
// Detailed instructions at the bottom
|
||||
detailed_instructions = new QLabel(tr( |
||||
"For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br>" |
||||
"<br>" |
||||
"Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br>" |
||||
"<br><br>" |
||||
"<b>Frequently Asked Questions</b><br><br>" |
||||
"<i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br>" |
||||
"<i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br>" |
||||
"<i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br>" |
||||
"<i>Does it matter which software I run?</i> Yes, only upstream openpilot (and particular forks) are able to be used for training." |
||||
)); |
||||
detailed_instructions->setStyleSheet("font-size: 40px; color: #E4E4E4;"); |
||||
detailed_instructions->setWordWrap(true); |
||||
content_layout->addWidget(detailed_instructions); |
||||
|
||||
layout->addWidget(content, 1); |
||||
|
||||
// Set up the API request for firehose stats
|
||||
const QString dongle_id = QString::fromStdString(Params().get("DongleId")); |
||||
firehose_stats = new RequestRepeater(this, CommaApi::BASE_URL + "/v1/devices/" + dongle_id + "/firehose_stats", |
||||
"ApiCache_FirehoseStats", 30, true); |
||||
QObject::connect(firehose_stats, &RequestRepeater::requestDone, [=](const QString &response, bool success) { |
||||
if (success) { |
||||
QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8()); |
||||
QJsonObject json = doc.object(); |
||||
int count = json["firehose"].toInt(); |
||||
contribution_label->setText(tr("<b>%n segment(s)</b> of your driving is in the training dataset so far.", "", count)); |
||||
contribution_label->show(); |
||||
} |
||||
}); |
||||
|
||||
QObject::connect(uiState(), &UIState::uiUpdate, this, &FirehosePanel::refresh); |
||||
} |
||||
|
||||
void FirehosePanel::refresh() { |
||||
auto deviceState = (*uiState()->sm)["deviceState"].getDeviceState(); |
||||
auto networkType = deviceState.getNetworkType(); |
||||
bool networkMetered = deviceState.getNetworkMetered(); |
||||
|
||||
bool is_active = !networkMetered && (networkType != cereal::DeviceState::NetworkType::NONE); |
||||
if (is_active) { |
||||
toggle_label->setText(tr("ACTIVE")); |
||||
toggle_label->setStyleSheet("font-size: 60px; font-weight: bold; color: #2ecc71;"); |
||||
} else { |
||||
toggle_label->setText(tr("<span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network")); |
||||
toggle_label->setStyleSheet("font-size: 60px;"); |
||||
} |
||||
} |
||||
@ -1,27 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <QWidget> |
||||
#include <QVBoxLayout> |
||||
#include <QLabel> |
||||
#include "selfdrive/ui/qt/request_repeater.h" |
||||
|
||||
// Forward declarations
|
||||
class SettingsWindow; |
||||
|
||||
class FirehosePanel : public QWidget { |
||||
Q_OBJECT |
||||
public: |
||||
explicit FirehosePanel(SettingsWindow *parent); |
||||
|
||||
private: |
||||
QVBoxLayout *layout; |
||||
|
||||
QLabel *detailed_instructions; |
||||
QLabel *contribution_label; |
||||
QLabel *toggle_label; |
||||
|
||||
RequestRepeater *firehose_stats; |
||||
|
||||
private slots: |
||||
void refresh(); |
||||
}; |
||||
@ -1,211 +0,0 @@ |
||||
#include "selfdrive/ui/qt/offroad/onboarding.h" |
||||
|
||||
#include <string> |
||||
|
||||
#include <QLabel> |
||||
#include <QPainter> |
||||
#include <QTransform> |
||||
#include <QVBoxLayout> |
||||
|
||||
#include "common/util.h" |
||||
#include "common/params.h" |
||||
#include "selfdrive/ui/qt/util.h" |
||||
#include "selfdrive/ui/qt/widgets/input.h" |
||||
|
||||
TrainingGuide::TrainingGuide(QWidget *parent) : QFrame(parent) { |
||||
setAttribute(Qt::WA_OpaquePaintEvent); |
||||
} |
||||
|
||||
void TrainingGuide::mouseReleaseEvent(QMouseEvent *e) { |
||||
if (click_timer.elapsed() < 250) { |
||||
return; |
||||
} |
||||
click_timer.restart(); |
||||
|
||||
auto contains = [this](QRect r, const QPoint &pt) { |
||||
if (image.size() != image_raw_size) { |
||||
QTransform transform; |
||||
transform.translate((width()- image.width()) / 2.0, (height()- image.height()) / 2.0); |
||||
transform.scale(image.width() / (float)image_raw_size.width(), image.height() / (float)image_raw_size.height()); |
||||
r= transform.mapRect(r); |
||||
} |
||||
return r.contains(pt); |
||||
}; |
||||
|
||||
if (contains(boundingRect[currentIndex], e->pos())) { |
||||
if (currentIndex == 9) { |
||||
const QRect yes = QRect(707, 804, 531, 164); |
||||
Params().putBool("RecordFront", contains(yes, e->pos())); |
||||
} |
||||
currentIndex += 1; |
||||
} else if (currentIndex == (boundingRect.size() - 2) && contains(boundingRect.last(), e->pos())) { |
||||
currentIndex = 0; |
||||
} |
||||
|
||||
if (currentIndex >= (boundingRect.size() - 1)) { |
||||
emit completedTraining(); |
||||
} else { |
||||
update(); |
||||
} |
||||
} |
||||
|
||||
void TrainingGuide::showEvent(QShowEvent *event) { |
||||
currentIndex = 0; |
||||
click_timer.start(); |
||||
} |
||||
|
||||
QImage TrainingGuide::loadImage(int id) { |
||||
QImage img(img_path + QString("step%1.png").arg(id)); |
||||
image_raw_size = img.size(); |
||||
if (image_raw_size != rect().size()) { |
||||
img = img.scaled(width(), height(), Qt::KeepAspectRatio, Qt::SmoothTransformation); |
||||
} |
||||
return img; |
||||
} |
||||
|
||||
void TrainingGuide::paintEvent(QPaintEvent *event) { |
||||
QPainter painter(this); |
||||
|
||||
QRect bg(0, 0, painter.device()->width(), painter.device()->height()); |
||||
painter.fillRect(bg, QColor("#000000")); |
||||
|
||||
image = loadImage(currentIndex); |
||||
QRect rect(image.rect()); |
||||
rect.moveCenter(bg.center()); |
||||
painter.drawImage(rect.topLeft(), image); |
||||
|
||||
// progress bar
|
||||
if (currentIndex > 0 && currentIndex < (boundingRect.size() - 2)) { |
||||
const int h = 20; |
||||
const int w = (currentIndex / (float)(boundingRect.size() - 2)) * width(); |
||||
painter.fillRect(QRect(0, height() - h, w, h), QColor("#465BEA")); |
||||
} |
||||
} |
||||
|
||||
void TermsPage::showEvent(QShowEvent *event) { |
||||
QVBoxLayout *main_layout = new QVBoxLayout(this); |
||||
main_layout->setContentsMargins(45, 35, 45, 45); |
||||
main_layout->setSpacing(0); |
||||
|
||||
QVBoxLayout *vlayout = new QVBoxLayout(); |
||||
vlayout->setContentsMargins(165, 165, 165, 0); |
||||
main_layout->addLayout(vlayout); |
||||
|
||||
QLabel *title = new QLabel(tr("Welcome to openpilot")); |
||||
title->setStyleSheet("font-size: 90px; font-weight: 500;"); |
||||
vlayout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); |
||||
|
||||
vlayout->addSpacing(90); |
||||
QLabel *desc = new QLabel(tr("You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing.")); |
||||
desc->setWordWrap(true); |
||||
desc->setStyleSheet("font-size: 80px; font-weight: 300;"); |
||||
vlayout->addWidget(desc, 0); |
||||
|
||||
vlayout->addStretch(); |
||||
|
||||
QHBoxLayout* buttons = new QHBoxLayout; |
||||
buttons->setMargin(0); |
||||
buttons->setSpacing(45); |
||||
main_layout->addLayout(buttons); |
||||
|
||||
QPushButton *decline_btn = new QPushButton(tr("Decline")); |
||||
buttons->addWidget(decline_btn); |
||||
QObject::connect(decline_btn, &QPushButton::clicked, this, &TermsPage::declinedTerms); |
||||
|
||||
accept_btn = new QPushButton(tr("Agree")); |
||||
accept_btn->setStyleSheet(R"( |
||||
QPushButton { |
||||
background-color: #465BEA; |
||||
} |
||||
QPushButton:pressed { |
||||
background-color: #3049F4; |
||||
} |
||||
)"); |
||||
buttons->addWidget(accept_btn); |
||||
QObject::connect(accept_btn, &QPushButton::clicked, this, &TermsPage::acceptedTerms); |
||||
} |
||||
|
||||
void DeclinePage::showEvent(QShowEvent *event) { |
||||
if (layout()) { |
||||
return; |
||||
} |
||||
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this); |
||||
main_layout->setMargin(45); |
||||
main_layout->setSpacing(40); |
||||
|
||||
QLabel *text = new QLabel(this); |
||||
text->setText(tr("You must accept the Terms and Conditions in order to use openpilot.")); |
||||
text->setStyleSheet(R"(font-size: 80px; font-weight: 300; margin: 200px;)"); |
||||
text->setWordWrap(true); |
||||
main_layout->addWidget(text, 0, Qt::AlignCenter); |
||||
|
||||
QHBoxLayout* buttons = new QHBoxLayout; |
||||
buttons->setSpacing(45); |
||||
main_layout->addLayout(buttons); |
||||
|
||||
QPushButton *back_btn = new QPushButton(tr("Back")); |
||||
buttons->addWidget(back_btn); |
||||
|
||||
QObject::connect(back_btn, &QPushButton::clicked, this, &DeclinePage::getBack); |
||||
|
||||
QPushButton *uninstall_btn = new QPushButton(tr("Decline, uninstall %1").arg(getBrand())); |
||||
uninstall_btn->setStyleSheet("background-color: #B73D3D"); |
||||
buttons->addWidget(uninstall_btn); |
||||
QObject::connect(uninstall_btn, &QPushButton::clicked, [=]() { |
||||
Params().putBool("DoUninstall", true); |
||||
}); |
||||
} |
||||
|
||||
void OnboardingWindow::updateActiveScreen() { |
||||
if (!accepted_terms) { |
||||
setCurrentIndex(0); |
||||
} else if (!training_done) { |
||||
setCurrentIndex(1); |
||||
} else { |
||||
emit onboardingDone(); |
||||
} |
||||
} |
||||
|
||||
OnboardingWindow::OnboardingWindow(QWidget *parent) : QStackedWidget(parent) { |
||||
std::string current_terms_version = params.get("TermsVersion"); |
||||
std::string current_training_version = params.get("TrainingVersion"); |
||||
accepted_terms = params.get("HasAcceptedTerms") == current_terms_version; |
||||
training_done = params.get("CompletedTrainingVersion") == current_training_version; |
||||
|
||||
TermsPage* terms = new TermsPage(this); |
||||
addWidget(terms); |
||||
connect(terms, &TermsPage::acceptedTerms, [=]() { |
||||
params.put("HasAcceptedTerms", current_terms_version); |
||||
accepted_terms = true; |
||||
updateActiveScreen(); |
||||
}); |
||||
connect(terms, &TermsPage::declinedTerms, [=]() { setCurrentIndex(2); }); |
||||
|
||||
TrainingGuide* tr = new TrainingGuide(this); |
||||
addWidget(tr); |
||||
connect(tr, &TrainingGuide::completedTraining, [=]() { |
||||
training_done = true; |
||||
params.put("CompletedTrainingVersion", current_training_version); |
||||
updateActiveScreen(); |
||||
}); |
||||
|
||||
DeclinePage* declinePage = new DeclinePage(this); |
||||
addWidget(declinePage); |
||||
connect(declinePage, &DeclinePage::getBack, [=]() { updateActiveScreen(); }); |
||||
|
||||
setStyleSheet(R"( |
||||
* { |
||||
color: white; |
||||
background-color: black; |
||||
} |
||||
QPushButton { |
||||
height: 160px; |
||||
font-size: 55px; |
||||
font-weight: 400; |
||||
border-radius: 10px; |
||||
background-color: #4F4F4F; |
||||
} |
||||
)"); |
||||
updateActiveScreen(); |
||||
} |
||||
@ -1,107 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <QElapsedTimer> |
||||
#include <QImage> |
||||
#include <QMouseEvent> |
||||
#include <QPushButton> |
||||
#include <QStackedWidget> |
||||
#include <QWidget> |
||||
|
||||
#include "common/params.h" |
||||
#include "selfdrive/ui/qt/qt_window.h" |
||||
|
||||
class TrainingGuide : public QFrame { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit TrainingGuide(QWidget *parent = 0); |
||||
|
||||
private: |
||||
void showEvent(QShowEvent *event) override; |
||||
void paintEvent(QPaintEvent *event) override; |
||||
void mouseReleaseEvent(QMouseEvent* e) override; |
||||
QImage loadImage(int id); |
||||
|
||||
QImage image; |
||||
QSize image_raw_size; |
||||
int currentIndex = 0; |
||||
|
||||
// Bounding boxes for each training guide step
|
||||
const QRect continueBtn = {1840, 0, 320, 1080}; |
||||
QVector<QRect> boundingRect { |
||||
QRect(112, 804, 618, 164), |
||||
continueBtn, |
||||
continueBtn, |
||||
QRect(1641, 558, 210, 313), |
||||
QRect(1662, 528, 184, 108), |
||||
continueBtn, |
||||
QRect(1814, 621, 211, 170), |
||||
QRect(1350, 0, 497, 755), |
||||
QRect(1540, 386, 468, 238), |
||||
QRect(112, 804, 1126, 164), |
||||
QRect(1598, 199, 316, 333), |
||||
continueBtn, |
||||
QRect(1364, 90, 796, 990), |
||||
continueBtn, |
||||
QRect(1593, 114, 318, 853), |
||||
QRect(1379, 511, 391, 243), |
||||
continueBtn, |
||||
continueBtn, |
||||
QRect(630, 804, 626, 164), |
||||
QRect(108, 804, 426, 164), |
||||
}; |
||||
|
||||
const QString img_path = "../assets/training/"; |
||||
QElapsedTimer click_timer; |
||||
|
||||
signals: |
||||
void completedTraining(); |
||||
}; |
||||
|
||||
|
||||
class TermsPage : public QFrame { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit TermsPage(QWidget *parent = 0) : QFrame(parent) {} |
||||
|
||||
private: |
||||
void showEvent(QShowEvent *event) override; |
||||
|
||||
QPushButton *accept_btn; |
||||
|
||||
signals: |
||||
void acceptedTerms(); |
||||
void declinedTerms(); |
||||
}; |
||||
|
||||
class DeclinePage : public QFrame { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit DeclinePage(QWidget *parent = 0) : QFrame(parent) {} |
||||
|
||||
private: |
||||
void showEvent(QShowEvent *event) override; |
||||
|
||||
signals: |
||||
void getBack(); |
||||
}; |
||||
|
||||
class OnboardingWindow : public QStackedWidget { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit OnboardingWindow(QWidget *parent = 0); |
||||
inline void showTrainingGuide() { setCurrentIndex(1); } |
||||
inline bool completed() const { return accepted_terms && training_done; } |
||||
|
||||
private: |
||||
void updateActiveScreen(); |
||||
|
||||
Params params; |
||||
bool accepted_terms = false, training_done = false; |
||||
|
||||
signals: |
||||
void onboardingDone(); |
||||
}; |
||||
@ -1,535 +0,0 @@ |
||||
#include <cassert> |
||||
#include <cmath> |
||||
#include <string> |
||||
#include <tuple> |
||||
#include <vector> |
||||
|
||||
#include <QDebug> |
||||
|
||||
#include "common/util.h" |
||||
#include "selfdrive/ui/qt/network/networking.h" |
||||
#include "selfdrive/ui/qt/offroad/settings.h" |
||||
#include "selfdrive/ui/qt/qt_window.h" |
||||
#include "selfdrive/ui/qt/widgets/prime.h" |
||||
#include "selfdrive/ui/qt/widgets/scrollview.h" |
||||
#include "selfdrive/ui/qt/offroad/developer_panel.h" |
||||
#include "selfdrive/ui/qt/offroad/firehose.h" |
||||
|
||||
TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { |
||||
// param, title, desc, icon, restart needed
|
||||
std::vector<std::tuple<QString, QString, QString, QString, bool>> toggle_defs{ |
||||
{ |
||||
"OpenpilotEnabledToggle", |
||||
tr("Enable openpilot"), |
||||
tr("Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature."), |
||||
"../assets/icons/chffr_wheel.png", |
||||
true, |
||||
}, |
||||
{ |
||||
"ExperimentalMode", |
||||
tr("Experimental Mode"), |
||||
"", |
||||
"../assets/icons/experimental_white.svg", |
||||
false, |
||||
}, |
||||
{ |
||||
"DisengageOnAccelerator", |
||||
tr("Disengage on Accelerator Pedal"), |
||||
tr("When enabled, pressing the accelerator pedal will disengage openpilot."), |
||||
"../assets/icons/disengage_on_accelerator.svg", |
||||
false, |
||||
}, |
||||
{ |
||||
"IsLdwEnabled", |
||||
tr("Enable Lane Departure Warnings"), |
||||
tr("Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h)."), |
||||
"../assets/icons/warning.png", |
||||
false, |
||||
}, |
||||
{ |
||||
"AlwaysOnDM", |
||||
tr("Always-On Driver Monitoring"), |
||||
tr("Enable driver monitoring even when openpilot is not engaged."), |
||||
"../assets/icons/monitoring.png", |
||||
false, |
||||
}, |
||||
{ |
||||
"RecordFront", |
||||
tr("Record and Upload Driver Camera"), |
||||
tr("Upload data from the driver facing camera and help improve the driver monitoring algorithm."), |
||||
"../assets/icons/monitoring.png", |
||||
true, |
||||
}, |
||||
{ |
||||
"RecordAudio", |
||||
tr("Record and Upload Microphone Audio"), |
||||
tr("Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."), |
||||
"../assets/icons/microphone.png", |
||||
true, |
||||
}, |
||||
{ |
||||
"IsMetric", |
||||
tr("Use Metric System"), |
||||
tr("Display speed in km/h instead of mph."), |
||||
"../assets/icons/metric.png", |
||||
false, |
||||
}, |
||||
}; |
||||
|
||||
|
||||
std::vector<QString> longi_button_texts{tr("Aggressive"), tr("Standard"), tr("Relaxed")}; |
||||
long_personality_setting = new ButtonParamControl("LongitudinalPersonality", tr("Driving Personality"), |
||||
tr("Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. " |
||||
"In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " |
||||
"your steering wheel distance button."), |
||||
"../assets/icons/speed_limit.png", |
||||
longi_button_texts); |
||||
|
||||
// set up uiState update for personality setting
|
||||
QObject::connect(uiState(), &UIState::uiUpdate, this, &TogglesPanel::updateState); |
||||
|
||||
for (auto &[param, title, desc, icon, needs_restart] : toggle_defs) { |
||||
auto toggle = new ParamControl(param, title, desc, icon, this); |
||||
|
||||
bool locked = params.getBool((param + "Lock").toStdString()); |
||||
toggle->setEnabled(!locked); |
||||
|
||||
if (needs_restart && !locked) { |
||||
toggle->setDescription(toggle->getDescription() + tr(" Changing this setting will restart openpilot if the car is powered on.")); |
||||
|
||||
QObject::connect(uiState(), &UIState::engagedChanged, [toggle](bool engaged) { |
||||
toggle->setEnabled(!engaged); |
||||
}); |
||||
|
||||
QObject::connect(toggle, &ParamControl::toggleFlipped, [=](bool state) { |
||||
params.putBool("OnroadCycleRequested", true); |
||||
}); |
||||
} |
||||
|
||||
addItem(toggle); |
||||
toggles[param.toStdString()] = toggle; |
||||
|
||||
// insert longitudinal personality after NDOG toggle
|
||||
if (param == "DisengageOnAccelerator") { |
||||
addItem(long_personality_setting); |
||||
} |
||||
} |
||||
|
||||
// Toggles with confirmation dialogs
|
||||
toggles["ExperimentalMode"]->setActiveIcon("../assets/icons/experimental.svg"); |
||||
toggles["ExperimentalMode"]->setConfirmation(true, true); |
||||
} |
||||
|
||||
void TogglesPanel::updateState(const UIState &s) { |
||||
const SubMaster &sm = *(s.sm); |
||||
|
||||
if (sm.updated("selfdriveState")) { |
||||
auto personality = sm["selfdriveState"].getSelfdriveState().getPersonality(); |
||||
if (personality != s.scene.personality && s.scene.started && isVisible()) { |
||||
long_personality_setting->setCheckedButton(static_cast<int>(personality)); |
||||
} |
||||
uiState()->scene.personality = personality; |
||||
} |
||||
} |
||||
|
||||
void TogglesPanel::expandToggleDescription(const QString ¶m) { |
||||
toggles[param.toStdString()]->showDescription(); |
||||
} |
||||
|
||||
void TogglesPanel::scrollToToggle(const QString ¶m) { |
||||
if (auto it = toggles.find(param.toStdString()); it != toggles.end()) { |
||||
auto scroll_area = qobject_cast<QScrollArea*>(parent()->parent()); |
||||
if (scroll_area) { |
||||
scroll_area->ensureWidgetVisible(it->second); |
||||
} |
||||
} |
||||
} |
||||
|
||||
void TogglesPanel::showEvent(QShowEvent *event) { |
||||
updateToggles(); |
||||
} |
||||
|
||||
void TogglesPanel::updateToggles() { |
||||
auto experimental_mode_toggle = toggles["ExperimentalMode"]; |
||||
const QString e2e_description = QString("%1<br>" |
||||
"<h4>%2</h4><br>" |
||||
"%3<br>" |
||||
"<h4>%4</h4><br>" |
||||
"%5<br>") |
||||
.arg(tr("openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below:")) |
||||
.arg(tr("End-to-End Longitudinal Control")) |
||||
.arg(tr("Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. " |
||||
"Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; " |
||||
"mistakes should be expected.")) |
||||
.arg(tr("New Driving Visualization")) |
||||
.arg(tr("The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner.")); |
||||
|
||||
const bool is_release = params.getBool("IsReleaseBranch"); |
||||
auto cp_bytes = params.get("CarParamsPersistent"); |
||||
if (!cp_bytes.empty()) { |
||||
AlignedBuffer aligned_buf; |
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size())); |
||||
cereal::CarParams::Reader CP = cmsg.getRoot<cereal::CarParams>(); |
||||
|
||||
if (hasLongitudinalControl(CP)) { |
||||
// normal description and toggle
|
||||
experimental_mode_toggle->setEnabled(true); |
||||
experimental_mode_toggle->setDescription(e2e_description); |
||||
long_personality_setting->setEnabled(true); |
||||
} else { |
||||
// no long for now
|
||||
experimental_mode_toggle->setEnabled(false); |
||||
long_personality_setting->setEnabled(false); |
||||
params.remove("ExperimentalMode"); |
||||
|
||||
const QString unavailable = tr("Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control."); |
||||
|
||||
QString long_desc = unavailable + " " + \
|
||||
tr("openpilot longitudinal control may come in a future update."); |
||||
if (CP.getAlphaLongitudinalAvailable()) { |
||||
if (is_release) { |
||||
long_desc = unavailable + " " + tr("An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches."); |
||||
} else { |
||||
long_desc = tr("Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode."); |
||||
} |
||||
} |
||||
experimental_mode_toggle->setDescription("<b>" + long_desc + "</b><br><br>" + e2e_description); |
||||
} |
||||
|
||||
experimental_mode_toggle->refresh(); |
||||
} else { |
||||
experimental_mode_toggle->setDescription(e2e_description); |
||||
} |
||||
} |
||||
|
||||
DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { |
||||
setSpacing(50); |
||||
addItem(new LabelControl(tr("Dongle ID"), getDongleId().value_or(tr("N/A")))); |
||||
addItem(new LabelControl(tr("Serial"), params.get("HardwareSerial").c_str())); |
||||
|
||||
pair_device = new ButtonControl(tr("Pair Device"), tr("PAIR"), |
||||
tr("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.")); |
||||
connect(pair_device, &ButtonControl::clicked, [=]() { |
||||
PairingPopup popup(this); |
||||
popup.exec(); |
||||
}); |
||||
addItem(pair_device); |
||||
|
||||
// offroad-only buttons
|
||||
|
||||
auto dcamBtn = new ButtonControl(tr("Driver Camera"), tr("PREVIEW"), |
||||
tr("Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)")); |
||||
connect(dcamBtn, &ButtonControl::clicked, [=]() { emit showDriverView(); }); |
||||
addItem(dcamBtn); |
||||
|
||||
resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), ""); |
||||
connect(resetCalibBtn, &ButtonControl::showDescriptionEvent, this, &DevicePanel::updateCalibDescription); |
||||
connect(resetCalibBtn, &ButtonControl::clicked, [&]() { |
||||
if (!uiState()->engaged()) { |
||||
if (ConfirmationDialog::confirm(tr("Are you sure you want to reset calibration?"), tr("Reset"), this)) { |
||||
// Check engaged again in case it changed while the dialog was open
|
||||
if (!uiState()->engaged()) { |
||||
params.remove("CalibrationParams"); |
||||
params.remove("LiveTorqueParameters"); |
||||
params.remove("LiveParameters"); |
||||
params.remove("LiveParametersV2"); |
||||
params.remove("LiveDelay"); |
||||
params.putBool("OnroadCycleRequested", true); |
||||
updateCalibDescription(); |
||||
} |
||||
} |
||||
} else { |
||||
ConfirmationDialog::alert(tr("Disengage to Reset Calibration"), this); |
||||
} |
||||
}); |
||||
addItem(resetCalibBtn); |
||||
|
||||
auto retrainingBtn = new ButtonControl(tr("Review Training Guide"), tr("REVIEW"), tr("Review the rules, features, and limitations of openpilot")); |
||||
connect(retrainingBtn, &ButtonControl::clicked, [=]() { |
||||
if (ConfirmationDialog::confirm(tr("Are you sure you want to review the training guide?"), tr("Review"), this)) { |
||||
emit reviewTrainingGuide(); |
||||
} |
||||
}); |
||||
addItem(retrainingBtn); |
||||
|
||||
if (Hardware::TICI()) { |
||||
auto regulatoryBtn = new ButtonControl(tr("Regulatory"), tr("VIEW"), ""); |
||||
connect(regulatoryBtn, &ButtonControl::clicked, [=]() { |
||||
const std::string txt = util::read_file("../assets/offroad/fcc.html"); |
||||
ConfirmationDialog::rich(QString::fromStdString(txt), this); |
||||
}); |
||||
addItem(regulatoryBtn); |
||||
} |
||||
|
||||
auto translateBtn = new ButtonControl(tr("Change Language"), tr("CHANGE"), ""); |
||||
connect(translateBtn, &ButtonControl::clicked, [=]() { |
||||
QMap<QString, QString> langs = getSupportedLanguages(); |
||||
QString selection = MultiOptionDialog::getSelection(tr("Select a language"), langs.keys(), langs.key(uiState()->language), this); |
||||
if (!selection.isEmpty()) { |
||||
// put language setting, exit Qt UI, and trigger fast restart
|
||||
params.put("LanguageSetting", langs[selection].toStdString()); |
||||
qApp->exit(18); |
||||
} |
||||
}); |
||||
addItem(translateBtn); |
||||
|
||||
QObject::connect(uiState()->prime_state, &PrimeState::changed, [this] (PrimeState::Type type) { |
||||
pair_device->setVisible(type == PrimeState::PRIME_TYPE_UNPAIRED); |
||||
}); |
||||
QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { |
||||
for (auto btn : findChildren<ButtonControl *>()) { |
||||
if (btn != pair_device && btn != resetCalibBtn) { |
||||
btn->setEnabled(offroad); |
||||
} |
||||
} |
||||
}); |
||||
|
||||
// power buttons
|
||||
QHBoxLayout *power_layout = new QHBoxLayout(); |
||||
power_layout->setSpacing(30); |
||||
|
||||
QPushButton *reboot_btn = new QPushButton(tr("Reboot")); |
||||
reboot_btn->setObjectName("reboot_btn"); |
||||
power_layout->addWidget(reboot_btn); |
||||
QObject::connect(reboot_btn, &QPushButton::clicked, this, &DevicePanel::reboot); |
||||
|
||||
QPushButton *poweroff_btn = new QPushButton(tr("Power Off")); |
||||
poweroff_btn->setObjectName("poweroff_btn"); |
||||
power_layout->addWidget(poweroff_btn); |
||||
QObject::connect(poweroff_btn, &QPushButton::clicked, this, &DevicePanel::poweroff); |
||||
|
||||
if (!Hardware::PC()) { |
||||
connect(uiState(), &UIState::offroadTransition, poweroff_btn, &QPushButton::setVisible); |
||||
} |
||||
|
||||
setStyleSheet(R"( |
||||
#reboot_btn { height: 120px; border-radius: 15px; background-color: #393939; } |
||||
#reboot_btn:pressed { background-color: #4a4a4a; } |
||||
#poweroff_btn { height: 120px; border-radius: 15px; background-color: #E22C2C; } |
||||
#poweroff_btn:pressed { background-color: #FF2424; } |
||||
)"); |
||||
addItem(power_layout); |
||||
} |
||||
|
||||
void DevicePanel::updateCalibDescription() { |
||||
QString desc = tr("openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down."); |
||||
std::string calib_bytes = params.get("CalibrationParams"); |
||||
if (!calib_bytes.empty()) { |
||||
try { |
||||
AlignedBuffer aligned_buf; |
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(calib_bytes.data(), calib_bytes.size())); |
||||
auto calib = cmsg.getRoot<cereal::Event>().getLiveCalibration(); |
||||
if (calib.getCalStatus() != cereal::LiveCalibrationData::Status::UNCALIBRATED) { |
||||
double pitch = calib.getRpyCalib()[1] * (180 / M_PI); |
||||
double yaw = calib.getRpyCalib()[2] * (180 / M_PI); |
||||
desc += tr(" Your device is pointed %1° %2 and %3° %4.") |
||||
.arg(QString::number(std::abs(pitch), 'g', 1), pitch > 0 ? tr("down") : tr("up"), |
||||
QString::number(std::abs(yaw), 'g', 1), yaw > 0 ? tr("left") : tr("right")); |
||||
} |
||||
} catch (kj::Exception) { |
||||
qInfo() << "invalid CalibrationParams"; |
||||
} |
||||
} |
||||
|
||||
int lag_perc = 0; |
||||
std::string lag_bytes = params.get("LiveDelay"); |
||||
if (!lag_bytes.empty()) { |
||||
try { |
||||
AlignedBuffer aligned_buf; |
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(lag_bytes.data(), lag_bytes.size())); |
||||
lag_perc = cmsg.getRoot<cereal::Event>().getLiveDelay().getCalPerc(); |
||||
} catch (kj::Exception) { |
||||
qInfo() << "invalid LiveDelay"; |
||||
} |
||||
} |
||||
if (lag_perc < 100) { |
||||
desc += tr("\n\nSteering lag calibration is %1% complete.").arg(lag_perc); |
||||
} else { |
||||
desc += tr("\n\nSteering lag calibration is complete."); |
||||
} |
||||
|
||||
std::string torque_bytes = params.get("LiveTorqueParameters"); |
||||
if (!torque_bytes.empty()) { |
||||
try { |
||||
AlignedBuffer aligned_buf; |
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(torque_bytes.data(), torque_bytes.size())); |
||||
auto torque = cmsg.getRoot<cereal::Event>().getLiveTorqueParameters(); |
||||
// don't add for non-torque cars
|
||||
if (torque.getUseParams()) { |
||||
int torque_perc = torque.getCalPerc(); |
||||
if (torque_perc < 100) { |
||||
desc += tr(" Steering torque response calibration is %1% complete.").arg(torque_perc); |
||||
} else { |
||||
desc += tr(" Steering torque response calibration is complete."); |
||||
} |
||||
} |
||||
} catch (kj::Exception) { |
||||
qInfo() << "invalid LiveTorqueParameters"; |
||||
} |
||||
} |
||||
|
||||
desc += "\n\n"; |
||||
desc += tr("openpilot is continuously calibrating, resetting is rarely required. " |
||||
"Resetting calibration will restart openpilot if the car is powered on."); |
||||
resetCalibBtn->setDescription(desc); |
||||
} |
||||
|
||||
void DevicePanel::reboot() { |
||||
if (!uiState()->engaged()) { |
||||
if (ConfirmationDialog::confirm(tr("Are you sure you want to reboot?"), tr("Reboot"), this)) { |
||||
// Check engaged again in case it changed while the dialog was open
|
||||
if (!uiState()->engaged()) { |
||||
params.putBool("DoReboot", true); |
||||
} |
||||
} |
||||
} else { |
||||
ConfirmationDialog::alert(tr("Disengage to Reboot"), this); |
||||
} |
||||
} |
||||
|
||||
void DevicePanel::poweroff() { |
||||
if (!uiState()->engaged()) { |
||||
if (ConfirmationDialog::confirm(tr("Are you sure you want to power off?"), tr("Power Off"), this)) { |
||||
// Check engaged again in case it changed while the dialog was open
|
||||
if (!uiState()->engaged()) { |
||||
params.putBool("DoShutdown", true); |
||||
} |
||||
} |
||||
} else { |
||||
ConfirmationDialog::alert(tr("Disengage to Power Off"), this); |
||||
} |
||||
} |
||||
|
||||
void SettingsWindow::showEvent(QShowEvent *event) { |
||||
setCurrentPanel(0); |
||||
} |
||||
|
||||
void SettingsWindow::setCurrentPanel(int index, const QString ¶m) { |
||||
if (!param.isEmpty()) { |
||||
// Check if param ends with "Panel" to determine if it's a panel name
|
||||
if (param.endsWith("Panel")) { |
||||
QString panelName = param; |
||||
panelName.chop(5); // Remove "Panel" suffix
|
||||
|
||||
// Find the panel by name
|
||||
for (int i = 0; i < nav_btns->buttons().size(); i++) { |
||||
if (nav_btns->buttons()[i]->text() == tr(panelName.toStdString().c_str())) { |
||||
index = i; |
||||
break; |
||||
} |
||||
} |
||||
} else { |
||||
emit expandToggleDescription(param); |
||||
emit scrollToToggle(param); |
||||
} |
||||
} |
||||
|
||||
panel_widget->setCurrentIndex(index); |
||||
nav_btns->buttons()[index]->setChecked(true); |
||||
} |
||||
|
||||
SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { |
||||
|
||||
// setup two main layouts
|
||||
sidebar_widget = new QWidget; |
||||
QVBoxLayout *sidebar_layout = new QVBoxLayout(sidebar_widget); |
||||
panel_widget = new QStackedWidget(); |
||||
|
||||
// close button
|
||||
QPushButton *close_btn = new QPushButton(tr("×")); |
||||
close_btn->setStyleSheet(R"( |
||||
QPushButton { |
||||
font-size: 140px; |
||||
padding-bottom: 20px; |
||||
border-radius: 100px; |
||||
background-color: #292929; |
||||
font-weight: 400; |
||||
} |
||||
QPushButton:pressed { |
||||
background-color: #3B3B3B; |
||||
} |
||||
)"); |
||||
close_btn->setFixedSize(200, 200); |
||||
sidebar_layout->addSpacing(45); |
||||
sidebar_layout->addWidget(close_btn, 0, Qt::AlignCenter); |
||||
QObject::connect(close_btn, &QPushButton::clicked, this, &SettingsWindow::closeSettings); |
||||
|
||||
// setup panels
|
||||
DevicePanel *device = new DevicePanel(this); |
||||
QObject::connect(device, &DevicePanel::reviewTrainingGuide, this, &SettingsWindow::reviewTrainingGuide); |
||||
QObject::connect(device, &DevicePanel::showDriverView, this, &SettingsWindow::showDriverView); |
||||
|
||||
TogglesPanel *toggles = new TogglesPanel(this); |
||||
QObject::connect(this, &SettingsWindow::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription); |
||||
QObject::connect(this, &SettingsWindow::scrollToToggle, toggles, &TogglesPanel::scrollToToggle); |
||||
|
||||
auto networking = new Networking(this); |
||||
QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &Networking::setPrimeType); |
||||
|
||||
QList<QPair<QString, QWidget *>> panels = { |
||||
{tr("Device"), device}, |
||||
{tr("Network"), networking}, |
||||
{tr("Toggles"), toggles}, |
||||
{tr("Software"), new SoftwarePanel(this)}, |
||||
{tr("Firehose"), new FirehosePanel(this)}, |
||||
{tr("Developer"), new DeveloperPanel(this)}, |
||||
}; |
||||
|
||||
nav_btns = new QButtonGroup(this); |
||||
for (auto &[name, panel] : panels) { |
||||
QPushButton *btn = new QPushButton(name); |
||||
btn->setCheckable(true); |
||||
btn->setChecked(nav_btns->buttons().size() == 0); |
||||
btn->setStyleSheet(R"( |
||||
QPushButton { |
||||
color: grey; |
||||
border: none; |
||||
background: none; |
||||
font-size: 65px; |
||||
font-weight: 500; |
||||
} |
||||
QPushButton:checked { |
||||
color: white; |
||||
} |
||||
QPushButton:pressed { |
||||
color: #ADADAD; |
||||
} |
||||
)"); |
||||
btn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); |
||||
nav_btns->addButton(btn); |
||||
sidebar_layout->addWidget(btn, 0, Qt::AlignRight); |
||||
|
||||
const int lr_margin = name != tr("Network") ? 50 : 0; // Network panel handles its own margins
|
||||
panel->setContentsMargins(lr_margin, 25, lr_margin, 25); |
||||
|
||||
ScrollView *panel_frame = new ScrollView(panel, this); |
||||
panel_widget->addWidget(panel_frame); |
||||
|
||||
QObject::connect(btn, &QPushButton::clicked, [=, w = panel_frame]() { |
||||
btn->setChecked(true); |
||||
panel_widget->setCurrentWidget(w); |
||||
}); |
||||
} |
||||
sidebar_layout->setContentsMargins(50, 50, 100, 50); |
||||
|
||||
// main settings layout, sidebar + main panel
|
||||
QHBoxLayout *main_layout = new QHBoxLayout(this); |
||||
|
||||
sidebar_widget->setFixedWidth(500); |
||||
main_layout->addWidget(sidebar_widget); |
||||
main_layout->addWidget(panel_widget); |
||||
|
||||
setStyleSheet(R"( |
||||
* { |
||||
color: white; |
||||
font-size: 50px; |
||||
} |
||||
SettingsWindow { |
||||
background-color: black; |
||||
} |
||||
QStackedWidget, ScrollView { |
||||
background-color: #292929; |
||||
border-radius: 30px; |
||||
} |
||||
)"); |
||||
} |
||||
@ -1,106 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <map> |
||||
#include <string> |
||||
|
||||
#include <QButtonGroup> |
||||
#include <QFrame> |
||||
#include <QLabel> |
||||
#include <QPushButton> |
||||
#include <QStackedWidget> |
||||
#include <QWidget> |
||||
|
||||
#include "selfdrive/ui/ui.h" |
||||
#include "selfdrive/ui/qt/util.h" |
||||
#include "selfdrive/ui/qt/widgets/controls.h" |
||||
|
||||
// ********** settings window + top-level panels **********
|
||||
class SettingsWindow : public QFrame { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit SettingsWindow(QWidget *parent = 0); |
||||
void setCurrentPanel(int index, const QString ¶m = ""); |
||||
|
||||
protected: |
||||
void showEvent(QShowEvent *event) override; |
||||
|
||||
signals: |
||||
void closeSettings(); |
||||
void reviewTrainingGuide(); |
||||
void showDriverView(); |
||||
void expandToggleDescription(const QString ¶m); |
||||
void scrollToToggle(const QString ¶m); |
||||
|
||||
private: |
||||
QPushButton *sidebar_alert_widget; |
||||
QWidget *sidebar_widget; |
||||
QButtonGroup *nav_btns; |
||||
QStackedWidget *panel_widget; |
||||
}; |
||||
|
||||
class DevicePanel : public ListWidget { |
||||
Q_OBJECT |
||||
public: |
||||
explicit DevicePanel(SettingsWindow *parent); |
||||
|
||||
signals: |
||||
void reviewTrainingGuide(); |
||||
void showDriverView(); |
||||
|
||||
private slots: |
||||
void poweroff(); |
||||
void reboot(); |
||||
void updateCalibDescription(); |
||||
|
||||
private: |
||||
Params params; |
||||
ButtonControl *pair_device; |
||||
ButtonControl *resetCalibBtn; |
||||
}; |
||||
|
||||
class TogglesPanel : public ListWidget { |
||||
Q_OBJECT |
||||
public: |
||||
explicit TogglesPanel(SettingsWindow *parent); |
||||
void showEvent(QShowEvent *event) override; |
||||
|
||||
public slots: |
||||
void expandToggleDescription(const QString ¶m); |
||||
void scrollToToggle(const QString ¶m); |
||||
|
||||
private slots: |
||||
void updateState(const UIState &s); |
||||
|
||||
private: |
||||
Params params; |
||||
std::map<std::string, ParamControl*> toggles; |
||||
ButtonParamControl *long_personality_setting; |
||||
|
||||
void updateToggles(); |
||||
}; |
||||
|
||||
class SoftwarePanel : public ListWidget { |
||||
Q_OBJECT |
||||
public: |
||||
explicit SoftwarePanel(QWidget* parent = nullptr); |
||||
|
||||
private: |
||||
void showEvent(QShowEvent *event) override; |
||||
void updateLabels(); |
||||
void checkForUpdates(); |
||||
|
||||
bool is_onroad = false; |
||||
|
||||
QLabel *onroadLbl; |
||||
LabelControl *versionLbl; |
||||
ButtonControl *installBtn; |
||||
ButtonControl *downloadBtn; |
||||
ButtonControl *targetBranchBtn; |
||||
|
||||
Params params; |
||||
ParamWatcher *fs_watch; |
||||
}; |
||||
|
||||
// Forward declaration
|
||||
class FirehosePanel; |
||||
@ -1,156 +0,0 @@ |
||||
#include "selfdrive/ui/qt/offroad/settings.h" |
||||
|
||||
#include <cassert> |
||||
#include <cmath> |
||||
#include <string> |
||||
|
||||
#include <QDebug> |
||||
#include <QLabel> |
||||
|
||||
#include "common/params.h" |
||||
#include "common/util.h" |
||||
#include "selfdrive/ui/ui.h" |
||||
#include "selfdrive/ui/qt/util.h" |
||||
#include "selfdrive/ui/qt/widgets/controls.h" |
||||
#include "selfdrive/ui/qt/widgets/input.h" |
||||
#include "system/hardware/hw.h" |
||||
|
||||
|
||||
void SoftwarePanel::checkForUpdates() { |
||||
std::system("pkill -SIGUSR1 -f system.updated.updated"); |
||||
} |
||||
|
||||
SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) { |
||||
onroadLbl = new QLabel(tr("Updates are only downloaded while the car is off.")); |
||||
onroadLbl->setStyleSheet("font-size: 50px; font-weight: 400; text-align: left; padding-top: 30px; padding-bottom: 30px;"); |
||||
addItem(onroadLbl); |
||||
|
||||
// current version
|
||||
versionLbl = new LabelControl(tr("Current Version"), ""); |
||||
addItem(versionLbl); |
||||
|
||||
// download update btn
|
||||
downloadBtn = new ButtonControl(tr("Download"), tr("CHECK")); |
||||
connect(downloadBtn, &ButtonControl::clicked, [=]() { |
||||
downloadBtn->setEnabled(false); |
||||
if (downloadBtn->text() == tr("CHECK")) { |
||||
checkForUpdates(); |
||||
} else { |
||||
std::system("pkill -SIGHUP -f system.updated.updated"); |
||||
} |
||||
}); |
||||
addItem(downloadBtn); |
||||
|
||||
// install update btn
|
||||
installBtn = new ButtonControl(tr("Install Update"), tr("INSTALL")); |
||||
connect(installBtn, &ButtonControl::clicked, [=]() { |
||||
installBtn->setEnabled(false); |
||||
params.putBool("DoReboot", true); |
||||
}); |
||||
addItem(installBtn); |
||||
|
||||
// branch selecting
|
||||
targetBranchBtn = new ButtonControl(tr("Target Branch"), tr("SELECT")); |
||||
connect(targetBranchBtn, &ButtonControl::clicked, [=]() { |
||||
auto current = params.get("GitBranch"); |
||||
QStringList branches = QString::fromStdString(params.get("UpdaterAvailableBranches")).split(","); |
||||
for (QString b : {current.c_str(), "devel-staging", "devel", "nightly", "nightly-dev", "master"}) { |
||||
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(); |
||||
} |
||||
}); |
||||
if (!params.getBool("IsTestedBranch")) { |
||||
addItem(targetBranchBtn); |
||||
} |
||||
|
||||
// uninstall button
|
||||
auto uninstallBtn = new ButtonControl(tr("Uninstall %1").arg(getBrand()), tr("UNINSTALL")); |
||||
connect(uninstallBtn, &ButtonControl::clicked, [&]() { |
||||
if (ConfirmationDialog::confirm(tr("Are you sure you want to uninstall?"), tr("Uninstall"), this)) { |
||||
params.putBool("DoUninstall", true); |
||||
} |
||||
}); |
||||
addItem(uninstallBtn); |
||||
|
||||
fs_watch = new ParamWatcher(this); |
||||
QObject::connect(fs_watch, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) { |
||||
updateLabels(); |
||||
}); |
||||
|
||||
connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { |
||||
is_onroad = !offroad; |
||||
updateLabels(); |
||||
}); |
||||
|
||||
updateLabels(); |
||||
} |
||||
|
||||
void SoftwarePanel::showEvent(QShowEvent *event) { |
||||
// nice for testing on PC
|
||||
installBtn->setEnabled(true); |
||||
|
||||
updateLabels(); |
||||
} |
||||
|
||||
void SoftwarePanel::updateLabels() { |
||||
// add these back in case the files got removed
|
||||
fs_watch->addParam("LastUpdateTime"); |
||||
fs_watch->addParam("UpdateFailedCount"); |
||||
fs_watch->addParam("UpdaterState"); |
||||
fs_watch->addParam("UpdateAvailable"); |
||||
|
||||
if (!isVisible()) { |
||||
return; |
||||
} |
||||
|
||||
// updater only runs offroad
|
||||
onroadLbl->setVisible(is_onroad); |
||||
downloadBtn->setVisible(!is_onroad); |
||||
|
||||
// download update
|
||||
QString updater_state = QString::fromStdString(params.get("UpdaterState")); |
||||
bool failed = std::atoi(params.get("UpdateFailedCount").c_str()) > 0; |
||||
if (updater_state != "idle") { |
||||
downloadBtn->setEnabled(false); |
||||
downloadBtn->setValue(updater_state); |
||||
} else { |
||||
if (failed) { |
||||
downloadBtn->setText(tr("CHECK")); |
||||
downloadBtn->setValue(tr("failed to check for update")); |
||||
} else if (params.getBool("UpdaterFetchAvailable")) { |
||||
downloadBtn->setText(tr("DOWNLOAD")); |
||||
downloadBtn->setValue(tr("update available")); |
||||
} else { |
||||
QString lastUpdate = tr("never"); |
||||
auto tm = params.get("LastUpdateTime"); |
||||
if (!tm.empty()) { |
||||
lastUpdate = timeAgo(QDateTime::fromString(QString::fromStdString(tm + "Z"), Qt::ISODate)); |
||||
} |
||||
downloadBtn->setText(tr("CHECK")); |
||||
downloadBtn->setValue(tr("up to date, last checked %1").arg(lastUpdate)); |
||||
} |
||||
downloadBtn->setEnabled(true); |
||||
} |
||||
targetBranchBtn->setValue(QString::fromStdString(params.get("UpdaterTargetBranch"))); |
||||
|
||||
// current + new versions
|
||||
versionLbl->setText(QString::fromStdString(params.get("UpdaterCurrentDescription"))); |
||||
versionLbl->setDescription(QString::fromStdString(params.get("UpdaterCurrentReleaseNotes"))); |
||||
|
||||
installBtn->setVisible(!is_onroad && params.getBool("UpdateAvailable")); |
||||
installBtn->setValue(QString::fromStdString(params.get("UpdaterNewDescription"))); |
||||
installBtn->setDescription(QString::fromStdString(params.get("UpdaterNewReleaseNotes"))); |
||||
|
||||
update(); |
||||
} |
||||
@ -1,112 +0,0 @@ |
||||
#include "selfdrive/ui/qt/onroad/alerts.h" |
||||
|
||||
#include <QPainter> |
||||
#include <map> |
||||
|
||||
#include "selfdrive/ui/qt/util.h" |
||||
|
||||
void OnroadAlerts::updateState(const UIState &s) { |
||||
Alert a = getAlert(*(s.sm), s.scene.started_frame); |
||||
if (!alert.equal(a)) { |
||||
alert = a; |
||||
update(); |
||||
} |
||||
} |
||||
|
||||
void OnroadAlerts::clear() { |
||||
alert = {}; |
||||
update(); |
||||
} |
||||
|
||||
OnroadAlerts::Alert OnroadAlerts::getAlert(const SubMaster &sm, uint64_t started_frame) { |
||||
const cereal::SelfdriveState::Reader &ss = sm["selfdriveState"].getSelfdriveState(); |
||||
const uint64_t selfdrive_frame = sm.rcv_frame("selfdriveState"); |
||||
|
||||
Alert a = {}; |
||||
if (selfdrive_frame >= started_frame) { // Don't get old alert.
|
||||
a = {ss.getAlertText1().cStr(), ss.getAlertText2().cStr(), |
||||
ss.getAlertType().cStr(), ss.getAlertSize(), ss.getAlertStatus()}; |
||||
} |
||||
|
||||
if (!sm.updated("selfdriveState") && (sm.frame - started_frame) > 5 * UI_FREQ) { |
||||
const int SELFDRIVE_STATE_TIMEOUT = 5; |
||||
const int ss_missing = (nanos_since_boot() - sm.rcv_time("selfdriveState")) / 1e9; |
||||
|
||||
// Handle selfdrive timeout
|
||||
if (selfdrive_frame < started_frame) { |
||||
// car is started, but selfdriveState hasn't been seen at all
|
||||
a = {tr("openpilot Unavailable"), tr("Waiting to start"), |
||||
"selfdriveWaiting", cereal::SelfdriveState::AlertSize::MID, |
||||
cereal::SelfdriveState::AlertStatus::NORMAL}; |
||||
} else if (ss_missing > SELFDRIVE_STATE_TIMEOUT && !Hardware::PC()) { |
||||
// car is started, but selfdrive is lagging or died
|
||||
if (ss.getEnabled() && (ss_missing - SELFDRIVE_STATE_TIMEOUT) < 10) { |
||||
a = {tr("TAKE CONTROL IMMEDIATELY"), tr("System Unresponsive"), |
||||
"selfdriveUnresponsive", cereal::SelfdriveState::AlertSize::FULL, |
||||
cereal::SelfdriveState::AlertStatus::CRITICAL}; |
||||
} else { |
||||
a = {tr("System Unresponsive"), tr("Reboot Device"), |
||||
"selfdriveUnresponsivePermanent", cereal::SelfdriveState::AlertSize::MID, |
||||
cereal::SelfdriveState::AlertStatus::NORMAL}; |
||||
} |
||||
} |
||||
} |
||||
return a; |
||||
} |
||||
|
||||
void OnroadAlerts::paintEvent(QPaintEvent *event) { |
||||
if (alert.size == cereal::SelfdriveState::AlertSize::NONE) { |
||||
return; |
||||
} |
||||
static std::map<cereal::SelfdriveState::AlertSize, const int> alert_heights = { |
||||
{cereal::SelfdriveState::AlertSize::SMALL, 271}, |
||||
{cereal::SelfdriveState::AlertSize::MID, 420}, |
||||
{cereal::SelfdriveState::AlertSize::FULL, height()}, |
||||
}; |
||||
int h = alert_heights[alert.size]; |
||||
|
||||
int margin = 40; |
||||
int radius = 30; |
||||
if (alert.size == cereal::SelfdriveState::AlertSize::FULL) { |
||||
margin = 0; |
||||
radius = 0; |
||||
} |
||||
QRect r = QRect(0 + margin, height() - h + margin, width() - margin*2, h - margin*2); |
||||
|
||||
QPainter p(this); |
||||
|
||||
// draw background + gradient
|
||||
p.setPen(Qt::NoPen); |
||||
p.setCompositionMode(QPainter::CompositionMode_SourceOver); |
||||
p.setBrush(QBrush(alert_colors[alert.status])); |
||||
p.drawRoundedRect(r, radius, radius); |
||||
|
||||
QLinearGradient g(0, r.y(), 0, r.bottom()); |
||||
g.setColorAt(0, QColor::fromRgbF(0, 0, 0, 0.05)); |
||||
g.setColorAt(1, QColor::fromRgbF(0, 0, 0, 0.35)); |
||||
|
||||
p.setCompositionMode(QPainter::CompositionMode_DestinationOver); |
||||
p.setBrush(QBrush(g)); |
||||
p.drawRoundedRect(r, radius, radius); |
||||
p.setCompositionMode(QPainter::CompositionMode_SourceOver); |
||||
|
||||
// text
|
||||
const QPoint c = r.center(); |
||||
p.setPen(QColor(0xff, 0xff, 0xff)); |
||||
p.setRenderHint(QPainter::TextAntialiasing); |
||||
if (alert.size == cereal::SelfdriveState::AlertSize::SMALL) { |
||||
p.setFont(InterFont(74, QFont::DemiBold)); |
||||
p.drawText(r, Qt::AlignCenter, alert.text1); |
||||
} else if (alert.size == cereal::SelfdriveState::AlertSize::MID) { |
||||
p.setFont(InterFont(88, QFont::Bold)); |
||||
p.drawText(QRect(0, c.y() - 125, width(), 150), Qt::AlignHCenter | Qt::AlignTop, alert.text1); |
||||
p.setFont(InterFont(66)); |
||||
p.drawText(QRect(0, c.y() + 21, width(), 90), Qt::AlignHCenter, alert.text2); |
||||
} else if (alert.size == cereal::SelfdriveState::AlertSize::FULL) { |
||||
bool l = alert.text1.length() > 15; |
||||
p.setFont(InterFont(l ? 132 : 177, QFont::Bold)); |
||||
p.drawText(QRect(0, r.y() + (l ? 240 : 270), width(), 600), Qt::AlignHCenter | Qt::TextWordWrap, alert.text1); |
||||
p.setFont(InterFont(88)); |
||||
p.drawText(QRect(0, r.height() - (l ? 361 : 420), width(), 300), Qt::AlignHCenter | Qt::TextWordWrap, alert.text2); |
||||
} |
||||
} |
||||
@ -1,39 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <QWidget> |
||||
|
||||
#include "selfdrive/ui/ui.h" |
||||
|
||||
class OnroadAlerts : public QWidget { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
OnroadAlerts(QWidget *parent = 0) : QWidget(parent) {} |
||||
void updateState(const UIState &s); |
||||
void clear(); |
||||
|
||||
protected: |
||||
struct Alert { |
||||
QString text1; |
||||
QString text2; |
||||
QString type; |
||||
cereal::SelfdriveState::AlertSize size; |
||||
cereal::SelfdriveState::AlertStatus status; |
||||
|
||||
bool equal(const Alert &other) const { |
||||
return text1 == other.text1 && text2 == other.text2 && type == other.type; |
||||
} |
||||
}; |
||||
|
||||
const QMap<cereal::SelfdriveState::AlertStatus, QColor> alert_colors = { |
||||
{cereal::SelfdriveState::AlertStatus::NORMAL, QColor(0x15, 0x15, 0x15, 0xf1)}, |
||||
{cereal::SelfdriveState::AlertStatus::USER_PROMPT, QColor(0xDA, 0x6F, 0x25, 0xf1)}, |
||||
{cereal::SelfdriveState::AlertStatus::CRITICAL, QColor(0xC9, 0x22, 0x31, 0xf1)}, |
||||
}; |
||||
|
||||
void paintEvent(QPaintEvent*) override; |
||||
OnroadAlerts::Alert getAlert(const SubMaster &sm, uint64_t started_frame); |
||||
|
||||
QColor bg; |
||||
Alert alert = {}; |
||||
}; |
||||
@ -1,157 +0,0 @@ |
||||
|
||||
#include "selfdrive/ui/qt/onroad/annotated_camera.h" |
||||
|
||||
#include <QPainter> |
||||
#include <algorithm> |
||||
#include <cmath> |
||||
|
||||
#include "common/swaglog.h" |
||||
#include "selfdrive/ui/qt/util.h" |
||||
|
||||
// Window that shows camera view and variety of info drawn on top
|
||||
AnnotatedCameraWidget::AnnotatedCameraWidget(VisionStreamType type, QWidget *parent) |
||||
: fps_filter(UI_FREQ, 3, 1. / UI_FREQ), CameraWidget("camerad", type, parent) { |
||||
pm = std::make_unique<PubMaster>(std::vector<const char*>{"uiDebug"}); |
||||
|
||||
main_layout = new QVBoxLayout(this); |
||||
main_layout->setMargin(UI_BORDER_SIZE); |
||||
main_layout->setSpacing(0); |
||||
|
||||
experimental_btn = new ExperimentalButton(this); |
||||
main_layout->addWidget(experimental_btn, 0, Qt::AlignTop | Qt::AlignRight); |
||||
} |
||||
|
||||
void AnnotatedCameraWidget::updateState(const UIState &s) { |
||||
// update engageability/experimental mode button
|
||||
experimental_btn->updateState(s); |
||||
dmon.updateState(s); |
||||
} |
||||
|
||||
void AnnotatedCameraWidget::initializeGL() { |
||||
CameraWidget::initializeGL(); |
||||
qInfo() << "OpenGL version:" << QString((const char*)glGetString(GL_VERSION)); |
||||
qInfo() << "OpenGL vendor:" << QString((const char*)glGetString(GL_VENDOR)); |
||||
qInfo() << "OpenGL renderer:" << QString((const char*)glGetString(GL_RENDERER)); |
||||
qInfo() << "OpenGL language version:" << QString((const char*)glGetString(GL_SHADING_LANGUAGE_VERSION)); |
||||
|
||||
prev_draw_t = millis_since_boot(); |
||||
setBackgroundColor(bg_colors[STATUS_DISENGAGED]); |
||||
} |
||||
|
||||
mat4 AnnotatedCameraWidget::calcFrameMatrix() { |
||||
// Project point at "infinity" to compute x and y offsets
|
||||
// to ensure this ends up in the middle of the screen
|
||||
// for narrow come and a little lower for wide cam.
|
||||
// TODO: use proper perspective transform?
|
||||
|
||||
// Select intrinsic matrix and calibration based on camera type
|
||||
auto *s = uiState(); |
||||
bool wide_cam = active_stream_type == VISION_STREAM_WIDE_ROAD; |
||||
const auto &intrinsic_matrix = wide_cam ? ECAM_INTRINSIC_MATRIX : FCAM_INTRINSIC_MATRIX; |
||||
const auto &calibration = wide_cam ? s->scene.view_from_wide_calib : s->scene.view_from_calib; |
||||
|
||||
// Compute the calibration transformation matrix
|
||||
const auto calib_transform = intrinsic_matrix * calibration; |
||||
|
||||
float zoom = wide_cam ? 2.0 : 1.1; |
||||
Eigen::Vector3f inf(1000., 0., 0.); |
||||
auto Kep = calib_transform * inf; |
||||
|
||||
int w = width(), h = height(); |
||||
float center_x = intrinsic_matrix(0, 2); |
||||
float center_y = intrinsic_matrix(1, 2); |
||||
|
||||
float max_x_offset = center_x * zoom - w / 2 - 5; |
||||
float max_y_offset = center_y * zoom - h / 2 - 5; |
||||
float x_offset = std::clamp<float>((Kep.x() / Kep.z() - center_x) * zoom, -max_x_offset, max_x_offset); |
||||
float y_offset = std::clamp<float>((Kep.y() / Kep.z() - center_y) * zoom, -max_y_offset, max_y_offset); |
||||
|
||||
// Apply transformation such that video pixel coordinates match video
|
||||
// 1) Put (0, 0) in the middle of the video
|
||||
// 2) Apply same scaling as video
|
||||
// 3) Put (0, 0) in top left corner of video
|
||||
Eigen::Matrix3f video_transform =(Eigen::Matrix3f() << |
||||
zoom, 0.0f, (w / 2 - x_offset) - (center_x * zoom), |
||||
0.0f, zoom, (h / 2 - y_offset) - (center_y * zoom), |
||||
0.0f, 0.0f, 1.0f).finished(); |
||||
|
||||
model.setTransform(video_transform * calib_transform); |
||||
|
||||
float zx = zoom * 2 * center_x / w; |
||||
float zy = zoom * 2 * center_y / h; |
||||
return mat4{{ |
||||
zx, 0.0, 0.0, -x_offset / w * 2, |
||||
0.0, zy, 0.0, y_offset / h * 2, |
||||
0.0, 0.0, 1.0, 0.0, |
||||
0.0, 0.0, 0.0, 1.0, |
||||
}}; |
||||
} |
||||
|
||||
void AnnotatedCameraWidget::paintGL() { |
||||
UIState *s = uiState(); |
||||
SubMaster &sm = *(s->sm); |
||||
const double start_draw_t = millis_since_boot(); |
||||
|
||||
// draw camera frame
|
||||
{ |
||||
std::lock_guard lk(frame_lock); |
||||
|
||||
if (frames.empty()) { |
||||
if (skip_frame_count > 0) { |
||||
skip_frame_count--; |
||||
qDebug() << "skipping frame, not ready"; |
||||
return; |
||||
} |
||||
} else { |
||||
// skip drawing up to this many frames if we're
|
||||
// missing camera frames. this smooths out the
|
||||
// transitions from the narrow and wide cameras
|
||||
skip_frame_count = 5; |
||||
} |
||||
|
||||
// Wide or narrow cam dependent on speed
|
||||
bool has_wide_cam = available_streams.count(VISION_STREAM_WIDE_ROAD); |
||||
if (has_wide_cam) { |
||||
float v_ego = sm["carState"].getCarState().getVEgo(); |
||||
if ((v_ego < 10) || available_streams.size() == 1) { |
||||
wide_cam_requested = true; |
||||
} else if (v_ego > 15) { |
||||
wide_cam_requested = false; |
||||
} |
||||
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()); |
||||
CameraWidget::paintGL(); |
||||
} |
||||
|
||||
QPainter painter(this); |
||||
painter.setRenderHint(QPainter::Antialiasing); |
||||
painter.setPen(Qt::NoPen); |
||||
|
||||
model.draw(painter, rect()); |
||||
dmon.draw(painter, rect()); |
||||
hud.updateState(*s); |
||||
hud.draw(painter, rect()); |
||||
|
||||
double cur_draw_t = millis_since_boot(); |
||||
double dt = cur_draw_t - prev_draw_t; |
||||
double fps = fps_filter.update(1. / dt * 1000); |
||||
if (fps < 15) { |
||||
LOGW("slow frame rate: %.2f fps", fps); |
||||
} |
||||
prev_draw_t = cur_draw_t; |
||||
|
||||
// publish debug msg
|
||||
MessageBuilder msg; |
||||
auto m = msg.initEvent().initUiDebug(); |
||||
m.setDrawTimeMillis(cur_draw_t - start_draw_t); |
||||
pm->send("uiDebug", msg); |
||||
} |
||||
|
||||
void AnnotatedCameraWidget::showEvent(QShowEvent *event) { |
||||
CameraWidget::showEvent(event); |
||||
|
||||
ui_update_params(uiState()); |
||||
prev_draw_t = millis_since_boot(); |
||||
} |
||||
@ -1,37 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <QVBoxLayout> |
||||
#include <memory> |
||||
#include "selfdrive/ui/qt/onroad/hud.h" |
||||
#include "selfdrive/ui/qt/onroad/buttons.h" |
||||
#include "selfdrive/ui/qt/onroad/driver_monitoring.h" |
||||
#include "selfdrive/ui/qt/onroad/model.h" |
||||
#include "selfdrive/ui/qt/widgets/cameraview.h" |
||||
|
||||
class AnnotatedCameraWidget : public CameraWidget { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit AnnotatedCameraWidget(VisionStreamType type, QWidget* parent = 0); |
||||
void updateState(const UIState &s); |
||||
|
||||
private: |
||||
QVBoxLayout *main_layout; |
||||
ExperimentalButton *experimental_btn; |
||||
DriverMonitorRenderer dmon; |
||||
HudRenderer hud; |
||||
ModelRenderer model; |
||||
std::unique_ptr<PubMaster> pm; |
||||
|
||||
int skip_frame_count = 0; |
||||
bool wide_cam_requested = false; |
||||
|
||||
protected: |
||||
void paintGL() override; |
||||
void initializeGL() override; |
||||
void showEvent(QShowEvent *event) override; |
||||
mat4 calcFrameMatrix() override; |
||||
|
||||
double prev_draw_t = 0; |
||||
FirstOrderFilter fps_filter; |
||||
}; |
||||
@ -1,49 +0,0 @@ |
||||
#include "selfdrive/ui/qt/onroad/buttons.h" |
||||
|
||||
#include <QPainter> |
||||
|
||||
#include "selfdrive/ui/qt/util.h" |
||||
|
||||
void drawIcon(QPainter &p, const QPoint ¢er, const QPixmap &img, const QBrush &bg, float opacity) { |
||||
p.setRenderHint(QPainter::Antialiasing); |
||||
p.setOpacity(1.0); // bg dictates opacity of ellipse
|
||||
p.setPen(Qt::NoPen); |
||||
p.setBrush(bg); |
||||
p.drawEllipse(center, btn_size / 2, btn_size / 2); |
||||
p.setOpacity(opacity); |
||||
p.drawPixmap(center - QPoint(img.width() / 2, img.height() / 2), img); |
||||
p.setOpacity(1.0); |
||||
} |
||||
|
||||
// ExperimentalButton
|
||||
ExperimentalButton::ExperimentalButton(QWidget *parent) : experimental_mode(false), engageable(false), QPushButton(parent) { |
||||
setFixedSize(btn_size, btn_size); |
||||
|
||||
engage_img = loadPixmap("../assets/icons/chffr_wheel.png", {img_size, img_size}); |
||||
experimental_img = loadPixmap("../assets/icons/experimental.svg", {img_size, img_size}); |
||||
QObject::connect(this, &QPushButton::clicked, this, &ExperimentalButton::changeMode); |
||||
} |
||||
|
||||
void ExperimentalButton::changeMode() { |
||||
const auto cp = (*uiState()->sm)["carParams"].getCarParams(); |
||||
bool can_change = hasLongitudinalControl(cp) && params.getBool("ExperimentalModeConfirmed"); |
||||
if (can_change) { |
||||
params.putBool("ExperimentalMode", !experimental_mode); |
||||
} |
||||
} |
||||
|
||||
void ExperimentalButton::updateState(const UIState &s) { |
||||
const auto cs = (*s.sm)["selfdriveState"].getSelfdriveState(); |
||||
bool eng = cs.getEngageable() || cs.getEnabled(); |
||||
if ((cs.getExperimentalMode() != experimental_mode) || (eng != engageable)) { |
||||
engageable = eng; |
||||
experimental_mode = cs.getExperimentalMode(); |
||||
update(); |
||||
} |
||||
} |
||||
|
||||
void ExperimentalButton::paintEvent(QPaintEvent *event) { |
||||
QPainter p(this); |
||||
QPixmap img = experimental_mode ? experimental_img : engage_img; |
||||
drawIcon(p, QPoint(btn_size / 2, btn_size / 2), img, QColor(0, 0, 0, 166), (isDown() || !engageable) ? 0.6 : 1.0); |
||||
} |
||||
@ -1,28 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <QPushButton> |
||||
|
||||
#include "selfdrive/ui/ui.h" |
||||
|
||||
const int btn_size = 192; |
||||
const int img_size = (btn_size / 4) * 3; |
||||
|
||||
class ExperimentalButton : public QPushButton { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit ExperimentalButton(QWidget *parent = 0); |
||||
void updateState(const UIState &s); |
||||
|
||||
private: |
||||
void paintEvent(QPaintEvent *event) override; |
||||
void changeMode(); |
||||
|
||||
Params params; |
||||
QPixmap engage_img; |
||||
QPixmap experimental_img; |
||||
bool experimental_mode; |
||||
bool engageable; |
||||
}; |
||||
|
||||
void drawIcon(QPainter &p, const QPoint ¢er, const QPixmap &img, const QBrush &bg, float opacity); |
||||
@ -1,107 +0,0 @@ |
||||
#include "selfdrive/ui/qt/onroad/driver_monitoring.h" |
||||
#include <algorithm> |
||||
#include <cmath> |
||||
|
||||
#include "selfdrive/ui/qt/onroad/buttons.h" |
||||
#include "selfdrive/ui/qt/util.h" |
||||
|
||||
// Default 3D coordinates for face keypoints
|
||||
static constexpr vec3 DEFAULT_FACE_KPTS_3D[] = { |
||||
{-5.98, -51.20, 8.00}, {-17.64, -49.14, 8.00}, {-23.81, -46.40, 8.00}, {-29.98, -40.91, 8.00}, {-32.04, -37.49, 8.00}, |
||||
{-34.10, -32.00, 8.00}, {-36.16, -21.03, 8.00}, {-36.16, 6.40, 8.00}, {-35.47, 10.51, 8.00}, {-32.73, 19.43, 8.00}, |
||||
{-29.30, 26.29, 8.00}, {-24.50, 33.83, 8.00}, {-19.01, 41.37, 8.00}, {-14.21, 46.17, 8.00}, {-12.16, 47.54, 8.00}, |
||||
{-4.61, 49.60, 8.00}, {4.99, 49.60, 8.00}, {12.53, 47.54, 8.00}, {14.59, 46.17, 8.00}, {19.39, 41.37, 8.00}, |
||||
{24.87, 33.83, 8.00}, {29.67, 26.29, 8.00}, {33.10, 19.43, 8.00}, {35.84, 10.51, 8.00}, {36.53, 6.40, 8.00}, |
||||
{36.53, -21.03, 8.00}, {34.47, -32.00, 8.00}, {32.42, -37.49, 8.00}, {30.36, -40.91, 8.00}, {24.19, -46.40, 8.00}, |
||||
{18.02, -49.14, 8.00}, {6.36, -51.20, 8.00}, {-5.98, -51.20, 8.00}, |
||||
}; |
||||
|
||||
// Colors used for drawing based on monitoring state
|
||||
static const QColor DMON_ENGAGED_COLOR = QColor::fromRgbF(0.1, 0.945, 0.26); |
||||
static const QColor DMON_DISENGAGED_COLOR = QColor::fromRgbF(0.545, 0.545, 0.545); |
||||
|
||||
DriverMonitorRenderer::DriverMonitorRenderer() : face_kpts_draw(std::size(DEFAULT_FACE_KPTS_3D)) { |
||||
dm_img = loadPixmap("../assets/icons/driver_face.png", {img_size + 5, img_size + 5}); |
||||
} |
||||
|
||||
void DriverMonitorRenderer::updateState(const UIState &s) { |
||||
auto &sm = *(s.sm); |
||||
is_visible = sm["selfdriveState"].getSelfdriveState().getAlertSize() == cereal::SelfdriveState::AlertSize::NONE && |
||||
sm.rcv_frame("driverStateV2") > s.scene.started_frame; |
||||
if (!is_visible) return; |
||||
|
||||
auto dm_state = sm["driverMonitoringState"].getDriverMonitoringState(); |
||||
is_active = dm_state.getIsActiveMode(); |
||||
is_rhd = dm_state.getIsRHD(); |
||||
dm_fade_state = std::clamp(dm_fade_state + 0.2f * (0.5f - is_active), 0.0f, 1.0f); |
||||
|
||||
const auto &driverstate = sm["driverStateV2"].getDriverStateV2(); |
||||
const auto driver_orient = is_rhd ? driverstate.getRightDriverData().getFaceOrientation() : driverstate.getLeftDriverData().getFaceOrientation(); |
||||
|
||||
for (int i = 0; i < 3; ++i) { |
||||
float v_this = (i == 0 ? (driver_orient[i] < 0 ? 0.7 : 0.9) : 0.4) * driver_orient[i]; |
||||
driver_pose_diff[i] = std::abs(driver_pose_vals[i] - v_this); |
||||
driver_pose_vals[i] = 0.8f * v_this + (1 - 0.8) * driver_pose_vals[i]; |
||||
driver_pose_sins[i] = std::sin(driver_pose_vals[i] * (1.0f - dm_fade_state)); |
||||
driver_pose_coss[i] = std::cos(driver_pose_vals[i] * (1.0f - dm_fade_state)); |
||||
} |
||||
|
||||
auto [sin_y, sin_x, sin_z] = driver_pose_sins; |
||||
auto [cos_y, cos_x, cos_z] = driver_pose_coss; |
||||
|
||||
// Rotation matrix for transforming face keypoints based on driver's head orientation
|
||||
const mat3 r_xyz = {{ |
||||
cos_x * cos_z, cos_x * sin_z, -sin_x, |
||||
-sin_y * sin_x * cos_z - cos_y * sin_z, -sin_y * sin_x * sin_z + cos_y * cos_z, -sin_y * cos_x, |
||||
cos_y * sin_x * cos_z - sin_y * sin_z, cos_y * sin_x * sin_z + sin_y * cos_z, cos_y * cos_x, |
||||
}}; |
||||
|
||||
// Transform vertices
|
||||
for (int i = 0; i < face_kpts_draw.size(); ++i) { |
||||
vec3 kpt = matvecmul3(r_xyz, DEFAULT_FACE_KPTS_3D[i]); |
||||
face_kpts_draw[i] = {{kpt.v[0], kpt.v[1], kpt.v[2] * (1.0f - dm_fade_state) + 8 * dm_fade_state}}; |
||||
} |
||||
} |
||||
|
||||
void DriverMonitorRenderer::draw(QPainter &painter, const QRect &surface_rect) { |
||||
if (!is_visible) return; |
||||
|
||||
painter.save(); |
||||
|
||||
int offset = UI_BORDER_SIZE + btn_size / 2; |
||||
float x = is_rhd ? surface_rect.width() - offset : offset; |
||||
float y = surface_rect.height() - offset; |
||||
float opacity = is_active ? 0.65f : 0.2f; |
||||
|
||||
drawIcon(painter, QPoint(x, y), dm_img, QColor(0, 0, 0, 70), opacity); |
||||
|
||||
QPointF keypoints[std::size(DEFAULT_FACE_KPTS_3D)]; |
||||
for (int i = 0; i < std::size(keypoints); ++i) { |
||||
const auto &v = face_kpts_draw[i].v; |
||||
float kp = (v[2] - 8) / 120.0f + 1.0f; |
||||
keypoints[i] = QPointF(v[0] * kp + x, v[1] * kp + y); |
||||
} |
||||
|
||||
painter.setPen(QPen(QColor::fromRgbF(1.0, 1.0, 1.0, opacity), 5.2, Qt::SolidLine, Qt::RoundCap)); |
||||
painter.drawPolyline(keypoints, std::size(keypoints)); |
||||
|
||||
// tracking arcs
|
||||
const int arc_l = 133; |
||||
const float arc_t_default = 6.7f; |
||||
const float arc_t_extend = 12.0f; |
||||
QColor arc_color = uiState()->engaged() ? DMON_ENGAGED_COLOR : DMON_DISENGAGED_COLOR; |
||||
arc_color.setAlphaF(0.4 * (1.0f - dm_fade_state)); |
||||
|
||||
float delta_x = -driver_pose_sins[1] * arc_l / 2.0f; |
||||
float delta_y = -driver_pose_sins[0] * arc_l / 2.0f; |
||||
|
||||
// Draw horizontal tracking arc
|
||||
painter.setPen(QPen(arc_color, arc_t_default + arc_t_extend * std::min(1.0, driver_pose_diff[1] * 5.0), Qt::SolidLine, Qt::RoundCap)); |
||||
painter.drawArc(QRectF(std::min(x + delta_x, x), y - arc_l / 2, std::abs(delta_x), arc_l), (driver_pose_sins[1] > 0 ? 90 : -90) * 16, 180 * 16); |
||||
|
||||
// Draw vertical tracking arc
|
||||
painter.setPen(QPen(arc_color, arc_t_default + arc_t_extend * std::min(1.0, driver_pose_diff[0] * 5.0), Qt::SolidLine, Qt::RoundCap)); |
||||
painter.drawArc(QRectF(x - arc_l / 2, std::min(y + delta_y, y), arc_l, std::abs(delta_y)), (driver_pose_sins[0] > 0 ? 0 : 180) * 16, 180 * 16); |
||||
|
||||
painter.restore(); |
||||
} |
||||
@ -1,24 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <vector> |
||||
#include <QPainter> |
||||
#include "selfdrive/ui/ui.h" |
||||
|
||||
class DriverMonitorRenderer { |
||||
public: |
||||
DriverMonitorRenderer(); |
||||
void updateState(const UIState &s); |
||||
void draw(QPainter &painter, const QRect &surface_rect); |
||||
|
||||
private: |
||||
float driver_pose_vals[3] = {}; |
||||
float driver_pose_diff[3] = {}; |
||||
float driver_pose_sins[3] = {}; |
||||
float driver_pose_coss[3] = {}; |
||||
bool is_visible = false; |
||||
bool is_active = false; |
||||
bool is_rhd = false; |
||||
float dm_fade_state = 1.0; |
||||
QPixmap dm_img; |
||||
std::vector<vec3> face_kpts_draw; |
||||
}; |
||||
@ -1,112 +0,0 @@ |
||||
#include "selfdrive/ui/qt/onroad/hud.h" |
||||
|
||||
#include <cmath> |
||||
|
||||
#include "selfdrive/ui/qt/util.h" |
||||
|
||||
constexpr int SET_SPEED_NA = 255; |
||||
|
||||
HudRenderer::HudRenderer() {} |
||||
|
||||
void HudRenderer::updateState(const UIState &s) { |
||||
is_metric = s.scene.is_metric; |
||||
status = s.status; |
||||
|
||||
const SubMaster &sm = *(s.sm); |
||||
if (sm.rcv_frame("carState") < s.scene.started_frame) { |
||||
is_cruise_set = false; |
||||
set_speed = SET_SPEED_NA; |
||||
speed = 0.0; |
||||
return; |
||||
} |
||||
|
||||
const auto &controls_state = sm["controlsState"].getControlsState(); |
||||
const auto &car_state = sm["carState"].getCarState(); |
||||
|
||||
// Handle older routes where vCruiseCluster is not set
|
||||
set_speed = car_state.getVCruiseCluster() == 0.0 ? controls_state.getVCruiseDEPRECATED() : car_state.getVCruiseCluster(); |
||||
is_cruise_set = set_speed > 0 && set_speed != SET_SPEED_NA; |
||||
is_cruise_available = set_speed != -1; |
||||
|
||||
if (is_cruise_set && !is_metric) { |
||||
set_speed *= KM_TO_MILE; |
||||
} |
||||
|
||||
// Handle older routes where vEgoCluster is not set
|
||||
v_ego_cluster_seen = v_ego_cluster_seen || car_state.getVEgoCluster() != 0.0; |
||||
float v_ego = v_ego_cluster_seen ? car_state.getVEgoCluster() : car_state.getVEgo(); |
||||
speed = std::max<float>(0.0f, v_ego * (is_metric ? MS_TO_KPH : MS_TO_MPH)); |
||||
} |
||||
|
||||
void HudRenderer::draw(QPainter &p, const QRect &surface_rect) { |
||||
p.save(); |
||||
|
||||
// Draw header gradient
|
||||
QLinearGradient bg(0, UI_HEADER_HEIGHT - (UI_HEADER_HEIGHT / 2.5), 0, UI_HEADER_HEIGHT); |
||||
bg.setColorAt(0, QColor::fromRgbF(0, 0, 0, 0.45)); |
||||
bg.setColorAt(1, QColor::fromRgbF(0, 0, 0, 0)); |
||||
p.fillRect(0, 0, surface_rect.width(), UI_HEADER_HEIGHT, bg); |
||||
|
||||
|
||||
if (is_cruise_available) { |
||||
drawSetSpeed(p, surface_rect); |
||||
} |
||||
drawCurrentSpeed(p, surface_rect); |
||||
|
||||
p.restore(); |
||||
} |
||||
|
||||
void HudRenderer::drawSetSpeed(QPainter &p, const QRect &surface_rect) { |
||||
// Draw outer box + border to contain set speed
|
||||
const QSize default_size = {172, 204}; |
||||
QSize set_speed_size = is_metric ? QSize(200, 204) : default_size; |
||||
QRect set_speed_rect(QPoint(60 + (default_size.width() - set_speed_size.width()) / 2, 45), set_speed_size); |
||||
|
||||
// Draw set speed box
|
||||
p.setPen(QPen(QColor(255, 255, 255, 75), 6)); |
||||
p.setBrush(QColor(0, 0, 0, 166)); |
||||
p.drawRoundedRect(set_speed_rect, 32, 32); |
||||
|
||||
// Colors based on status
|
||||
QColor max_color = QColor(0xa6, 0xa6, 0xa6, 0xff); |
||||
QColor set_speed_color = QColor(0x72, 0x72, 0x72, 0xff); |
||||
if (is_cruise_set) { |
||||
set_speed_color = QColor(255, 255, 255); |
||||
if (status == STATUS_DISENGAGED) { |
||||
max_color = QColor(255, 255, 255); |
||||
} else if (status == STATUS_OVERRIDE) { |
||||
max_color = QColor(0x91, 0x9b, 0x95, 0xff); |
||||
} else { |
||||
max_color = QColor(0x80, 0xd8, 0xa6, 0xff); |
||||
} |
||||
} |
||||
|
||||
// Draw "MAX" text
|
||||
p.setFont(InterFont(40, QFont::DemiBold)); |
||||
p.setPen(max_color); |
||||
p.drawText(set_speed_rect.adjusted(0, 27, 0, 0), Qt::AlignTop | Qt::AlignHCenter, tr("MAX")); |
||||
|
||||
// Draw set speed
|
||||
QString setSpeedStr = is_cruise_set ? QString::number(std::nearbyint(set_speed)) : "–"; |
||||
p.setFont(InterFont(90, QFont::Bold)); |
||||
p.setPen(set_speed_color); |
||||
p.drawText(set_speed_rect.adjusted(0, 77, 0, 0), Qt::AlignTop | Qt::AlignHCenter, setSpeedStr); |
||||
} |
||||
|
||||
void HudRenderer::drawCurrentSpeed(QPainter &p, const QRect &surface_rect) { |
||||
QString speedStr = QString::number(std::nearbyint(speed)); |
||||
|
||||
p.setFont(InterFont(176, QFont::Bold)); |
||||
drawText(p, surface_rect.center().x(), 210, speedStr); |
||||
|
||||
p.setFont(InterFont(66)); |
||||
drawText(p, surface_rect.center().x(), 290, is_metric ? tr("km/h") : tr("mph"), 200); |
||||
} |
||||
|
||||
void HudRenderer::drawText(QPainter &p, int x, int y, const QString &text, int alpha) { |
||||
QRect real_rect = p.fontMetrics().boundingRect(text); |
||||
real_rect.moveCenter({x, y - real_rect.height() / 2}); |
||||
|
||||
p.setPen(QColor(0xff, 0xff, 0xff, alpha)); |
||||
p.drawText(real_rect.x(), real_rect.bottom(), text); |
||||
} |
||||
@ -1,26 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <QPainter> |
||||
#include "selfdrive/ui/ui.h" |
||||
|
||||
class HudRenderer : public QObject { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
HudRenderer(); |
||||
void updateState(const UIState &s); |
||||
void draw(QPainter &p, const QRect &surface_rect); |
||||
|
||||
private: |
||||
void drawSetSpeed(QPainter &p, const QRect &surface_rect); |
||||
void drawCurrentSpeed(QPainter &p, const QRect &surface_rect); |
||||
void drawText(QPainter &p, int x, int y, const QString &text, int alpha = 255); |
||||
|
||||
float speed = 0; |
||||
float set_speed = 0; |
||||
bool is_cruise_set = false; |
||||
bool is_cruise_available = true; |
||||
bool is_metric = false; |
||||
bool v_ego_cluster_seen = false; |
||||
int status = STATUS_DISENGAGED; |
||||
}; |
||||
@ -1,250 +0,0 @@ |
||||
#include "selfdrive/ui/qt/onroad/model.h" |
||||
|
||||
constexpr int CLIP_MARGIN = 500; |
||||
constexpr float MIN_DRAW_DISTANCE = 10.0; |
||||
constexpr float MAX_DRAW_DISTANCE = 100.0; |
||||
|
||||
static int get_path_length_idx(const cereal::XYZTData::Reader &line, const float path_height) { |
||||
const auto &line_x = line.getX(); |
||||
int max_idx = 0; |
||||
for (int i = 1; i < line_x.size() && line_x[i] <= path_height; ++i) { |
||||
max_idx = i; |
||||
} |
||||
return max_idx; |
||||
} |
||||
|
||||
void ModelRenderer::draw(QPainter &painter, const QRect &surface_rect) { |
||||
auto *s = uiState(); |
||||
auto &sm = *(s->sm); |
||||
// Check if data is up-to-date
|
||||
if (sm.rcv_frame("liveCalibration") < s->scene.started_frame || |
||||
sm.rcv_frame("modelV2") < s->scene.started_frame) { |
||||
return; |
||||
} |
||||
|
||||
clip_region = surface_rect.adjusted(-CLIP_MARGIN, -CLIP_MARGIN, CLIP_MARGIN, CLIP_MARGIN); |
||||
experimental_mode = sm["selfdriveState"].getSelfdriveState().getExperimentalMode(); |
||||
longitudinal_control = sm["carParams"].getCarParams().getOpenpilotLongitudinalControl(); |
||||
path_offset_z = sm["liveCalibration"].getLiveCalibration().getHeight()[0]; |
||||
|
||||
painter.save(); |
||||
|
||||
const auto &model = sm["modelV2"].getModelV2(); |
||||
const auto &radar_state = sm["radarState"].getRadarState(); |
||||
const auto &lead_one = radar_state.getLeadOne(); |
||||
|
||||
update_model(model, lead_one); |
||||
drawLaneLines(painter); |
||||
drawPath(painter, model, surface_rect.height()); |
||||
|
||||
if (longitudinal_control && sm.alive("radarState")) { |
||||
update_leads(radar_state, model.getPosition()); |
||||
const auto &lead_two = radar_state.getLeadTwo(); |
||||
if (lead_one.getStatus()) { |
||||
drawLead(painter, lead_one, lead_vertices[0], surface_rect); |
||||
} |
||||
if (lead_two.getStatus() && (std::abs(lead_one.getDRel() - lead_two.getDRel()) > 3.0)) { |
||||
drawLead(painter, lead_two, lead_vertices[1], surface_rect); |
||||
} |
||||
} |
||||
|
||||
painter.restore(); |
||||
} |
||||
|
||||
void ModelRenderer::update_leads(const cereal::RadarState::Reader &radar_state, const cereal::XYZTData::Reader &line) { |
||||
for (int i = 0; i < 2; ++i) { |
||||
const auto &lead_data = (i == 0) ? radar_state.getLeadOne() : radar_state.getLeadTwo(); |
||||
if (lead_data.getStatus()) { |
||||
float z = line.getZ()[get_path_length_idx(line, lead_data.getDRel())]; |
||||
mapToScreen(lead_data.getDRel(), -lead_data.getYRel(), z + path_offset_z, &lead_vertices[i]); |
||||
} |
||||
} |
||||
} |
||||
|
||||
void ModelRenderer::update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead) { |
||||
const auto &model_position = model.getPosition(); |
||||
float max_distance = std::clamp(*(model_position.getX().end() - 1), MIN_DRAW_DISTANCE, MAX_DRAW_DISTANCE); |
||||
|
||||
// update lane lines
|
||||
const auto &lane_lines = model.getLaneLines(); |
||||
const auto &line_probs = model.getLaneLineProbs(); |
||||
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); |
||||
} |
||||
|
||||
// update road edges
|
||||
const auto &road_edges = model.getRoadEdges(); |
||||
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); |
||||
} |
||||
|
||||
// update path
|
||||
if (lead.getStatus()) { |
||||
const float lead_d = lead.getDRel() * 2.; |
||||
max_distance = std::clamp((float)(lead_d - fmin(lead_d * 0.35, 10.)), 0.0f, max_distance); |
||||
} |
||||
max_idx = get_path_length_idx(model_position, max_distance); |
||||
mapLineToPolygon(model_position, 0.9, path_offset_z, &track_vertices, max_idx, false); |
||||
} |
||||
|
||||
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]); |
||||
} |
||||
|
||||
// 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]); |
||||
} |
||||
} |
||||
|
||||
void ModelRenderer::drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, int height) { |
||||
QLinearGradient bg(0, height, 0, 0); |
||||
if (experimental_mode) { |
||||
// The first half of track_vertices are the points for the right side of the path
|
||||
const auto &acceleration = model.getAcceleration().getX(); |
||||
const int max_len = std::min<int>(track_vertices.length() / 2, acceleration.size()); |
||||
|
||||
for (int i = 0; i < max_len; ++i) { |
||||
// Some points are out of frame
|
||||
int track_idx = max_len - i - 1; // flip idx to start from bottom right
|
||||
if (track_vertices[track_idx].y() < 0 || track_vertices[track_idx].y() > height) continue; |
||||
|
||||
// Flip so 0 is bottom of frame
|
||||
float lin_grad_point = (height - track_vertices[track_idx].y()) / height; |
||||
|
||||
// speed up: 120, slow down: 0
|
||||
float path_hue = fmax(fmin(60 + acceleration[i] * 35, 120), 0); |
||||
// FIXME: painter.drawPolygon can be slow if hue is not rounded
|
||||
path_hue = int(path_hue * 100 + 0.5) / 100; |
||||
|
||||
float saturation = fmin(fabs(acceleration[i] * 1.5), 1); |
||||
float lightness = util::map_val(saturation, 0.0f, 1.0f, 0.95f, 0.62f); // lighter when grey
|
||||
float alpha = util::map_val(lin_grad_point, 0.75f / 2.f, 0.75f, 0.4f, 0.0f); // matches previous alpha fade
|
||||
bg.setColorAt(lin_grad_point, QColor::fromHslF(path_hue / 360., saturation, lightness, alpha)); |
||||
|
||||
// Skip a point, unless next is last
|
||||
i += (i + 2) < max_len ? 1 : 0; |
||||
} |
||||
|
||||
} else { |
||||
updatePathGradient(bg); |
||||
} |
||||
|
||||
painter.setBrush(bg); |
||||
painter.drawPolygon(track_vertices); |
||||
} |
||||
|
||||
void ModelRenderer::updatePathGradient(QLinearGradient &bg) { |
||||
static const QColor throttle_colors[] = { |
||||
QColor::fromHslF(148. / 360., 0.94, 0.51, 0.4), |
||||
QColor::fromHslF(112. / 360., 1.0, 0.68, 0.35), |
||||
QColor::fromHslF(112. / 360., 1.0, 0.68, 0.0)}; |
||||
|
||||
static const QColor no_throttle_colors[] = { |
||||
QColor::fromHslF(148. / 360., 0.0, 0.95, 0.4), |
||||
QColor::fromHslF(112. / 360., 0.0, 0.95, 0.35), |
||||
QColor::fromHslF(112. / 360., 0.0, 0.95, 0.0), |
||||
}; |
||||
|
||||
// Transition speed; 0.1 corresponds to 0.5 seconds at UI_FREQ
|
||||
constexpr float transition_speed = 0.1f; |
||||
|
||||
// Start transition if throttle state changes
|
||||
bool allow_throttle = (*uiState()->sm)["longitudinalPlan"].getLongitudinalPlan().getAllowThrottle() || !longitudinal_control; |
||||
if (allow_throttle != prev_allow_throttle) { |
||||
prev_allow_throttle = allow_throttle; |
||||
// Invert blend factor for a smooth transition when the state changes mid-animation
|
||||
blend_factor = std::max(1.0f - blend_factor, 0.0f); |
||||
} |
||||
|
||||
const QColor *begin_colors = allow_throttle ? no_throttle_colors : throttle_colors; |
||||
const QColor *end_colors = allow_throttle ? throttle_colors : no_throttle_colors; |
||||
if (blend_factor < 1.0f) { |
||||
blend_factor = std::min(blend_factor + transition_speed, 1.0f); |
||||
} |
||||
|
||||
// Set gradient colors by blending the start and end colors
|
||||
bg.setColorAt(0.0f, blendColors(begin_colors[0], end_colors[0], blend_factor)); |
||||
bg.setColorAt(0.5f, blendColors(begin_colors[1], end_colors[1], blend_factor)); |
||||
bg.setColorAt(1.0f, blendColors(begin_colors[2], end_colors[2], blend_factor)); |
||||
} |
||||
|
||||
QColor ModelRenderer::blendColors(const QColor &start, const QColor &end, float t) { |
||||
if (t == 1.0f) return end; |
||||
return QColor::fromRgbF( |
||||
(1 - t) * start.redF() + t * end.redF(), |
||||
(1 - t) * start.greenF() + t * end.greenF(), |
||||
(1 - t) * start.blueF() + t * end.blueF(), |
||||
(1 - t) * start.alphaF() + t * end.alphaF()); |
||||
} |
||||
|
||||
void ModelRenderer::drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data, |
||||
const QPointF &vd, const QRect &surface_rect) { |
||||
const float speedBuff = 10.; |
||||
const float leadBuff = 40.; |
||||
const float d_rel = lead_data.getDRel(); |
||||
const float v_rel = lead_data.getVRel(); |
||||
|
||||
float fillAlpha = 0; |
||||
if (d_rel < leadBuff) { |
||||
fillAlpha = 255 * (1.0 - (d_rel / leadBuff)); |
||||
if (v_rel < 0) { |
||||
fillAlpha += 255 * (-1 * (v_rel / speedBuff)); |
||||
} |
||||
fillAlpha = (int)(fmin(fillAlpha, 255)); |
||||
} |
||||
|
||||
float sz = std::clamp((25 * 30) / (d_rel / 3 + 30), 15.0f, 30.0f) * 2.35; |
||||
float x = std::clamp<float>(vd.x(), 0.f, surface_rect.width() - sz / 2); |
||||
float y = std::min<float>(vd.y(), surface_rect.height() - sz * 0.6); |
||||
|
||||
float g_xo = sz / 5; |
||||
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)); |
||||
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)); |
||||
painter.drawPolygon(chevron, std::size(chevron)); |
||||
} |
||||
|
||||
// Projects a point in car to space to the corresponding point in full frame image space.
|
||||
bool ModelRenderer::mapToScreen(float in_x, float in_y, float in_z, QPointF *out) { |
||||
Eigen::Vector3f input(in_x, in_y, in_z); |
||||
auto pt = car_space_transform * input; |
||||
*out = QPointF(pt.x() / pt.z(), pt.y() / pt.z()); |
||||
return clip_region.contains(*out); |
||||
} |
||||
|
||||
void ModelRenderer::mapLineToPolygon(const cereal::XYZTData::Reader &line, float y_off, float z_off, |
||||
QPolygonF *pvd, int max_idx, bool allow_invert) { |
||||
const auto line_x = line.getX(), line_y = line.getY(), line_z = line.getZ(); |
||||
QPointF left, right; |
||||
pvd->clear(); |
||||
for (int i = 0; i <= max_idx; i++) { |
||||
// highly negative x positions are drawn above the frame and cause flickering, clip to zy plane of camera
|
||||
if (line_x[i] < 0) continue; |
||||
|
||||
bool l = mapToScreen(line_x[i], line_y[i] - y_off, line_z[i] + z_off, &left); |
||||
bool r = mapToScreen(line_x[i], line_y[i] + y_off, line_z[i] + z_off, &right); |
||||
if (l && r) { |
||||
// For wider lines the drawn polygon will "invert" when going over a hill and cause artifacts
|
||||
if (!allow_invert && pvd->size() && left.y() > pvd->back().y()) { |
||||
continue; |
||||
} |
||||
pvd->push_back(left); |
||||
pvd->push_front(right); |
||||
} |
||||
} |
||||
} |
||||
@ -1,39 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <QPainter> |
||||
#include <QPolygonF> |
||||
|
||||
#include "selfdrive/ui/ui.h" |
||||
|
||||
class ModelRenderer { |
||||
public: |
||||
ModelRenderer() {} |
||||
void setTransform(const Eigen::Matrix3f &transform) { car_space_transform = transform; } |
||||
void draw(QPainter &painter, const QRect &surface_rect); |
||||
|
||||
private: |
||||
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, |
||||
QPolygonF *pvd, int max_idx, bool allow_invert = true); |
||||
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_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead); |
||||
void drawLaneLines(QPainter &painter); |
||||
void drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, int height); |
||||
void updatePathGradient(QLinearGradient &bg); |
||||
QColor blendColors(const QColor &start, const QColor &end, float t); |
||||
|
||||
bool longitudinal_control = false; |
||||
bool experimental_mode = false; |
||||
float blend_factor = 1.0f; |
||||
bool prev_allow_throttle = true; |
||||
float lane_line_probs[4] = {}; |
||||
float road_edge_stds[2] = {}; |
||||
float path_offset_z = 1.22f; |
||||
QPolygonF track_vertices; |
||||
QPolygonF lane_line_vertices[4] = {}; |
||||
QPolygonF road_edge_vertices[2] = {}; |
||||
QPointF lead_vertices[2] = {}; |
||||
Eigen::Matrix3f car_space_transform = Eigen::Matrix3f::Zero(); |
||||
QRectF clip_region; |
||||
}; |
||||
@ -1,65 +0,0 @@ |
||||
#include "selfdrive/ui/qt/onroad/onroad_home.h" |
||||
|
||||
#include <QPainter> |
||||
#include <QStackedLayout> |
||||
|
||||
#include "selfdrive/ui/qt/util.h" |
||||
|
||||
OnroadWindow::OnroadWindow(QWidget *parent) : QWidget(parent) { |
||||
QVBoxLayout *main_layout = new QVBoxLayout(this); |
||||
main_layout->setMargin(UI_BORDER_SIZE); |
||||
QStackedLayout *stacked_layout = new QStackedLayout; |
||||
stacked_layout->setStackingMode(QStackedLayout::StackAll); |
||||
main_layout->addLayout(stacked_layout); |
||||
|
||||
nvg = new AnnotatedCameraWidget(VISION_STREAM_ROAD, this); |
||||
|
||||
QWidget * split_wrapper = new QWidget; |
||||
split = new QHBoxLayout(split_wrapper); |
||||
split->setContentsMargins(0, 0, 0, 0); |
||||
split->setSpacing(0); |
||||
split->addWidget(nvg); |
||||
|
||||
if (getenv("DUAL_CAMERA_VIEW")) { |
||||
CameraWidget *arCam = new CameraWidget("camerad", VISION_STREAM_ROAD, this); |
||||
split->insertWidget(0, arCam); |
||||
} |
||||
|
||||
stacked_layout->addWidget(split_wrapper); |
||||
|
||||
alerts = new OnroadAlerts(this); |
||||
alerts->setAttribute(Qt::WA_TransparentForMouseEvents, true); |
||||
stacked_layout->addWidget(alerts); |
||||
|
||||
// setup stacking order
|
||||
alerts->raise(); |
||||
|
||||
setAttribute(Qt::WA_OpaquePaintEvent); |
||||
QObject::connect(uiState(), &UIState::uiUpdate, this, &OnroadWindow::updateState); |
||||
QObject::connect(uiState(), &UIState::offroadTransition, this, &OnroadWindow::offroadTransition); |
||||
} |
||||
|
||||
void OnroadWindow::updateState(const UIState &s) { |
||||
if (!s.scene.started) { |
||||
return; |
||||
} |
||||
|
||||
alerts->updateState(s); |
||||
nvg->updateState(s); |
||||
|
||||
QColor bgColor = bg_colors[s.status]; |
||||
if (bg != bgColor) { |
||||
// repaint border
|
||||
bg = bgColor; |
||||
update(); |
||||
} |
||||
} |
||||
|
||||
void OnroadWindow::offroadTransition(bool offroad) { |
||||
alerts->clear(); |
||||
} |
||||
|
||||
void OnroadWindow::paintEvent(QPaintEvent *event) { |
||||
QPainter p(this); |
||||
p.fillRect(rect(), QColor(bg.red(), bg.green(), bg.blue(), 255)); |
||||
} |
||||
@ -1,22 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include "selfdrive/ui/qt/onroad/alerts.h" |
||||
#include "selfdrive/ui/qt/onroad/annotated_camera.h" |
||||
|
||||
class OnroadWindow : public QWidget { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
OnroadWindow(QWidget* parent = 0); |
||||
|
||||
private: |
||||
void paintEvent(QPaintEvent *event); |
||||
OnroadAlerts *alerts; |
||||
AnnotatedCameraWidget *nvg; |
||||
QColor bg = bg_colors[STATUS_DISENGAGED]; |
||||
QHBoxLayout* split; |
||||
|
||||
private slots: |
||||
void offroadTransition(bool offroad); |
||||
void updateState(const UIState &s); |
||||
}; |
||||
@ -1,48 +0,0 @@ |
||||
#include "selfdrive/ui/qt/prime_state.h" |
||||
|
||||
#include <QJsonDocument> |
||||
|
||||
#include "selfdrive/ui/qt/api.h" |
||||
#include "selfdrive/ui/qt/request_repeater.h" |
||||
#include "selfdrive/ui/qt/util.h" |
||||
|
||||
PrimeState::PrimeState(QObject* parent) : QObject(parent) { |
||||
const char *env_prime_type = std::getenv("PRIME_TYPE"); |
||||
auto type = env_prime_type ? env_prime_type : Params().get("PrimeType"); |
||||
|
||||
if (!type.empty()) { |
||||
prime_type = static_cast<PrimeState::Type>(std::atoi(type.c_str())); |
||||
} |
||||
|
||||
if (auto dongleId = getDongleId()) { |
||||
QString url = CommaApi::BASE_URL + "/v1.1/devices/" + *dongleId + "/"; |
||||
RequestRepeater* repeater = new RequestRepeater(this, url, "ApiCache_Device", 5); |
||||
QObject::connect(repeater, &RequestRepeater::requestDone, this, &PrimeState::handleReply); |
||||
} |
||||
|
||||
// Emit the initial state change
|
||||
QTimer::singleShot(1, [this]() { emit changed(prime_type); }); |
||||
} |
||||
|
||||
void PrimeState::handleReply(const QString& response, bool success) { |
||||
if (!success) return; |
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8()); |
||||
if (doc.isNull()) { |
||||
qDebug() << "JSON Parse failed on getting pairing and PrimeState status"; |
||||
return; |
||||
} |
||||
|
||||
QJsonObject json = doc.object(); |
||||
bool is_paired = json["is_paired"].toBool(); |
||||
auto type = static_cast<PrimeState::Type>(json["prime_type"].toInt()); |
||||
setType(is_paired ? type : PrimeState::PRIME_TYPE_UNPAIRED); |
||||
} |
||||
|
||||
void PrimeState::setType(PrimeState::Type type) { |
||||
if (type != prime_type) { |
||||
prime_type = type; |
||||
Params().put("PrimeType", std::to_string(prime_type)); |
||||
emit changed(prime_type); |
||||
} |
||||
} |
||||
@ -1,33 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <QObject> |
||||
|
||||
class PrimeState : public QObject { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
|
||||
enum Type { |
||||
PRIME_TYPE_UNKNOWN = -2, |
||||
PRIME_TYPE_UNPAIRED = -1, |
||||
PRIME_TYPE_NONE = 0, |
||||
PRIME_TYPE_MAGENTA = 1, |
||||
PRIME_TYPE_LITE = 2, |
||||
PRIME_TYPE_BLUE = 3, |
||||
PRIME_TYPE_MAGENTA_NEW = 4, |
||||
PRIME_TYPE_PURPLE = 5, |
||||
}; |
||||
|
||||
PrimeState(QObject *parent); |
||||
void setType(PrimeState::Type type); |
||||
inline PrimeState::Type currentType() const { return prime_type; } |
||||
inline bool isSubscribed() const { return prime_type > PrimeState::PRIME_TYPE_NONE; } |
||||
|
||||
signals: |
||||
void changed(PrimeState::Type prime_type); |
||||
|
||||
private: |
||||
void handleReply(const QString &response, bool success); |
||||
|
||||
PrimeState::Type prime_type = PrimeState::PRIME_TYPE_UNKNOWN; |
||||
}; |
||||
@ -1,36 +0,0 @@ |
||||
#include "selfdrive/ui/qt/qt_window.h" |
||||
|
||||
void setMainWindow(QWidget *w) { |
||||
const float scale = util::getenv("SCALE", 1.0f); |
||||
const QSize sz = QGuiApplication::primaryScreen()->size(); |
||||
|
||||
if (Hardware::PC() && scale == 1.0 && !(sz - DEVICE_SCREEN_SIZE).isValid()) { |
||||
w->setMinimumSize(QSize(640, 480)); // allow resize smaller than fullscreen
|
||||
w->setMaximumSize(DEVICE_SCREEN_SIZE); |
||||
w->resize(sz); |
||||
} else { |
||||
w->setFixedSize(DEVICE_SCREEN_SIZE * scale); |
||||
} |
||||
w->show(); |
||||
|
||||
#ifdef __TICI__ |
||||
QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface(); |
||||
wl_surface *s = reinterpret_cast<wl_surface*>(native->nativeResourceForWindow("surface", w->windowHandle())); |
||||
wl_surface_set_buffer_transform(s, WL_OUTPUT_TRANSFORM_270); |
||||
wl_surface_commit(s); |
||||
|
||||
w->setWindowState(Qt::WindowFullScreen); |
||||
w->setVisible(true); |
||||
|
||||
// ensure we have a valid eglDisplay, otherwise the ui will silently fail
|
||||
void *egl = native->nativeResourceForWindow("egldisplay", w->windowHandle()); |
||||
assert(egl != nullptr); |
||||
#endif |
||||
} |
||||
|
||||
|
||||
extern "C" { |
||||
void set_main_window(void *w) { |
||||
setMainWindow((QWidget*)w); |
||||
} |
||||
} |
||||
@ -1,20 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <string> |
||||
|
||||
#include <QApplication> |
||||
#include <QScreen> |
||||
#include <QWidget> |
||||
|
||||
#ifdef __TICI__ |
||||
#include <qpa/qplatformnativeinterface.h> |
||||
#include <wayland-client-protocol.h> |
||||
#include <QPlatformSurfaceEvent> |
||||
#endif |
||||
|
||||
#include "system/hardware/hw.h" |
||||
|
||||
const QString ASSET_PATH = ":/"; |
||||
const QSize DEVICE_SCREEN_SIZE = {2160, 1080}; |
||||
|
||||
void setMainWindow(QWidget *w); |
||||
@ -1,27 +0,0 @@ |
||||
#include "selfdrive/ui/qt/request_repeater.h" |
||||
|
||||
RequestRepeater::RequestRepeater(QObject *parent, const QString &requestURL, const QString &cacheKey, |
||||
int period, bool while_onroad) : HttpRequest(parent) { |
||||
timer = new QTimer(this); |
||||
timer->setTimerType(Qt::VeryCoarseTimer); |
||||
QObject::connect(timer, &QTimer::timeout, [=]() { |
||||
if ((!uiState()->scene.started || while_onroad) && device()->isAwake() && !active()) { |
||||
sendRequest(requestURL); |
||||
} |
||||
}); |
||||
|
||||
timer->start(period * 1000); |
||||
|
||||
if (!cacheKey.isEmpty()) { |
||||
prevResp = QString::fromStdString(params.get(cacheKey.toStdString())); |
||||
if (!prevResp.isEmpty()) { |
||||
QTimer::singleShot(500, [=]() { emit requestDone(prevResp, true, QNetworkReply::NoError); }); |
||||
} |
||||
QObject::connect(this, &HttpRequest::requestDone, [=](const QString &resp, bool success) { |
||||
if (success && resp != prevResp) { |
||||
params.put(cacheKey.toStdString(), resp.toStdString()); |
||||
prevResp = resp; |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
@ -1,15 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include "common/util.h" |
||||
#include "selfdrive/ui/qt/api.h" |
||||
#include "selfdrive/ui/ui.h" |
||||
|
||||
class RequestRepeater : public HttpRequest { |
||||
public: |
||||
RequestRepeater(QObject *parent, const QString &requestURL, const QString &cacheKey = "", int period = 0, bool while_onroad=false); |
||||
|
||||
private: |
||||
Params params; |
||||
QTimer *timer; |
||||
QString prevResp; |
||||
}; |
||||
@ -1,165 +0,0 @@ |
||||
#include "selfdrive/ui/qt/sidebar.h" |
||||
|
||||
#include <QMouseEvent> |
||||
|
||||
#include "selfdrive/ui/qt/util.h" |
||||
|
||||
void Sidebar::drawMetric(QPainter &p, const QPair<QString, QString> &label, QColor c, int y) { |
||||
const QRect rect = {30, y, 240, 126}; |
||||
|
||||
p.setPen(Qt::NoPen); |
||||
p.setBrush(QBrush(c)); |
||||
p.setClipRect(rect.x() + 4, rect.y(), 18, rect.height(), Qt::ClipOperation::ReplaceClip); |
||||
p.drawRoundedRect(QRect(rect.x() + 4, rect.y() + 4, 100, 118), 18, 18); |
||||
p.setClipping(false); |
||||
|
||||
QPen pen = QPen(QColor(0xff, 0xff, 0xff, 0x55)); |
||||
pen.setWidth(2); |
||||
p.setPen(pen); |
||||
p.setBrush(Qt::NoBrush); |
||||
p.drawRoundedRect(rect, 20, 20); |
||||
|
||||
p.setPen(QColor(0xff, 0xff, 0xff)); |
||||
p.setFont(InterFont(35, QFont::DemiBold)); |
||||
p.drawText(rect.adjusted(22, 0, 0, 0), Qt::AlignCenter, label.first + "\n" + label.second); |
||||
} |
||||
|
||||
Sidebar::Sidebar(QWidget *parent) : QFrame(parent), onroad(false), flag_pressed(false), settings_pressed(false), mic_indicator_pressed(false) { |
||||
home_img = loadPixmap("../assets/images/button_home.png", home_btn.size()); |
||||
flag_img = loadPixmap("../assets/images/button_flag.png", home_btn.size()); |
||||
settings_img = loadPixmap("../assets/images/button_settings.png", settings_btn.size(), Qt::IgnoreAspectRatio); |
||||
mic_img = loadPixmap("../assets/icons/microphone.png", QSize(30, 30)); |
||||
link_img = loadPixmap("../assets/icons/link.png", QSize(60, 60)); |
||||
|
||||
connect(this, &Sidebar::valueChanged, [=] { update(); }); |
||||
|
||||
setAttribute(Qt::WA_OpaquePaintEvent); |
||||
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); |
||||
setFixedWidth(300); |
||||
|
||||
QObject::connect(uiState(), &UIState::uiUpdate, this, &Sidebar::updateState); |
||||
|
||||
pm = std::make_unique<PubMaster>(std::vector<const char*>{"bookmarkButton"}); |
||||
} |
||||
|
||||
void Sidebar::mousePressEvent(QMouseEvent *event) { |
||||
if (onroad && home_btn.contains(event->pos())) { |
||||
flag_pressed = true; |
||||
update(); |
||||
} else if (settings_btn.contains(event->pos())) { |
||||
settings_pressed = true; |
||||
update(); |
||||
} else if (recording_audio && mic_indicator_btn.contains(event->pos())) { |
||||
mic_indicator_pressed = true; |
||||
update(); |
||||
} |
||||
} |
||||
|
||||
void Sidebar::mouseReleaseEvent(QMouseEvent *event) { |
||||
if (flag_pressed || settings_pressed || mic_indicator_pressed) { |
||||
flag_pressed = settings_pressed = mic_indicator_pressed = false; |
||||
update(); |
||||
} |
||||
if (onroad && home_btn.contains(event->pos())) { |
||||
MessageBuilder msg; |
||||
msg.initEvent().initBookmarkButton(); |
||||
pm->send("bookmarkButton", msg); |
||||
} else if (settings_btn.contains(event->pos())) { |
||||
emit openSettings(); |
||||
} else if (recording_audio && mic_indicator_btn.contains(event->pos())) { |
||||
emit openSettings(2, "RecordAudio"); |
||||
} |
||||
} |
||||
|
||||
void Sidebar::offroadTransition(bool offroad) { |
||||
onroad = !offroad; |
||||
update(); |
||||
} |
||||
|
||||
void Sidebar::updateState(const UIState &s) { |
||||
if (!isVisible()) return; |
||||
|
||||
auto &sm = *(s.sm); |
||||
|
||||
networking = networking ? networking : window()->findChild<Networking *>(""); |
||||
bool tethering_on = networking && networking->wifi->tethering_on; |
||||
auto deviceState = sm["deviceState"].getDeviceState(); |
||||
setProperty("netType", tethering_on ? "Hotspot": network_type[deviceState.getNetworkType()]); |
||||
int strength = tethering_on ? 4 : (int)deviceState.getNetworkStrength(); |
||||
setProperty("netStrength", strength > 0 ? strength + 1 : 0); |
||||
|
||||
ItemStatus connectStatus; |
||||
auto last_ping = deviceState.getLastAthenaPingTime(); |
||||
if (last_ping == 0) { |
||||
connectStatus = ItemStatus{{tr("CONNECT"), tr("OFFLINE")}, warning_color}; |
||||
} else { |
||||
connectStatus = nanos_since_boot() - last_ping < 80e9 |
||||
? ItemStatus{{tr("CONNECT"), tr("ONLINE")}, good_color} |
||||
: ItemStatus{{tr("CONNECT"), tr("ERROR")}, danger_color}; |
||||
} |
||||
setProperty("connectStatus", QVariant::fromValue(connectStatus)); |
||||
|
||||
ItemStatus tempStatus = {{tr("TEMP"), tr("HIGH")}, danger_color}; |
||||
auto ts = deviceState.getThermalStatus(); |
||||
if (ts == cereal::DeviceState::ThermalStatus::GREEN) { |
||||
tempStatus = {{tr("TEMP"), tr("GOOD")}, good_color}; |
||||
} else if (ts == cereal::DeviceState::ThermalStatus::YELLOW) { |
||||
tempStatus = {{tr("TEMP"), tr("OK")}, warning_color}; |
||||
} |
||||
setProperty("tempStatus", QVariant::fromValue(tempStatus)); |
||||
|
||||
ItemStatus pandaStatus = {{tr("VEHICLE"), tr("ONLINE")}, good_color}; |
||||
if (s.scene.pandaType == cereal::PandaState::PandaType::UNKNOWN) { |
||||
pandaStatus = {{tr("NO"), tr("PANDA")}, danger_color}; |
||||
} |
||||
setProperty("pandaStatus", QVariant::fromValue(pandaStatus)); |
||||
|
||||
setProperty("recordingAudio", s.scene.recording_audio); |
||||
} |
||||
|
||||
void Sidebar::paintEvent(QPaintEvent *event) { |
||||
QPainter p(this); |
||||
p.setPen(Qt::NoPen); |
||||
p.setRenderHint(QPainter::Antialiasing); |
||||
|
||||
p.fillRect(rect(), QColor(57, 57, 57)); |
||||
|
||||
// buttons
|
||||
p.setOpacity(settings_pressed ? 0.65 : 1.0); |
||||
p.drawPixmap(settings_btn.x(), settings_btn.y(), settings_img); |
||||
p.setOpacity(onroad && flag_pressed ? 0.65 : 1.0); |
||||
p.drawPixmap(home_btn.x(), home_btn.y(), onroad ? flag_img : home_img); |
||||
if (recording_audio) { |
||||
p.setBrush(danger_color); |
||||
p.setOpacity(mic_indicator_pressed ? 0.65 : 1.0); |
||||
p.drawRoundedRect(mic_indicator_btn, mic_indicator_btn.height() / 2, mic_indicator_btn.height() / 2); |
||||
int icon_x = mic_indicator_btn.x() + (mic_indicator_btn.width() - mic_img.width()) / 2; |
||||
int icon_y = mic_indicator_btn.y() + (mic_indicator_btn.height() - mic_img.height()) / 2; |
||||
p.drawPixmap(icon_x, icon_y, mic_img); |
||||
} |
||||
p.setOpacity(1.0); |
||||
|
||||
// network
|
||||
int x = 58; |
||||
const QColor gray(0x54, 0x54, 0x54); |
||||
for (int i = 0; i < 5; ++i) { |
||||
p.setBrush(i < net_strength ? Qt::white : gray); |
||||
p.drawEllipse(x, 196, 27, 27); |
||||
x += 37; |
||||
} |
||||
|
||||
p.setFont(InterFont(35)); |
||||
p.setPen(QColor(0xff, 0xff, 0xff)); |
||||
const QRect r = QRect(58, 247, width() - 100, 50); |
||||
|
||||
if (net_type == "Hotspot") { |
||||
p.drawPixmap(r.x(), r.y() + (r.height() - link_img.height()) / 2, link_img); |
||||
} else { |
||||
p.drawText(r, Qt::AlignLeft | Qt::AlignVCenter, net_type); |
||||
} |
||||
|
||||
// metrics
|
||||
drawMetric(p, temp_status.first, temp_status.second, 338); |
||||
drawMetric(p, panda_status.first, panda_status.second, 496); |
||||
drawMetric(p, connect_status.first, connect_status.second, 654); |
||||
} |
||||
@ -1,66 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <memory> |
||||
|
||||
#include <QFrame> |
||||
#include <QMap> |
||||
|
||||
#include "selfdrive/ui/ui.h" |
||||
#include "selfdrive/ui/qt/network/networking.h" |
||||
|
||||
typedef QPair<QPair<QString, QString>, QColor> ItemStatus; |
||||
Q_DECLARE_METATYPE(ItemStatus); |
||||
|
||||
class Sidebar : public QFrame { |
||||
Q_OBJECT |
||||
Q_PROPERTY(ItemStatus connectStatus MEMBER connect_status NOTIFY valueChanged); |
||||
Q_PROPERTY(ItemStatus pandaStatus MEMBER panda_status NOTIFY valueChanged); |
||||
Q_PROPERTY(ItemStatus tempStatus MEMBER temp_status NOTIFY valueChanged); |
||||
Q_PROPERTY(QString netType MEMBER net_type NOTIFY valueChanged); |
||||
Q_PROPERTY(int netStrength MEMBER net_strength NOTIFY valueChanged); |
||||
Q_PROPERTY(bool recordingAudio MEMBER recording_audio NOTIFY valueChanged); |
||||
|
||||
public: |
||||
explicit Sidebar(QWidget* parent = 0); |
||||
|
||||
signals: |
||||
void openSettings(int index = 0, const QString ¶m = ""); |
||||
void valueChanged(); |
||||
|
||||
public slots: |
||||
void offroadTransition(bool offroad); |
||||
void updateState(const UIState &s); |
||||
|
||||
protected: |
||||
void paintEvent(QPaintEvent *event) override; |
||||
void mousePressEvent(QMouseEvent *event) override; |
||||
void mouseReleaseEvent(QMouseEvent *event) override; |
||||
void drawMetric(QPainter &p, const QPair<QString, QString> &label, QColor c, int y); |
||||
|
||||
QPixmap home_img, flag_img, settings_img, mic_img, link_img; |
||||
bool onroad, recording_audio, flag_pressed, settings_pressed, mic_indicator_pressed; |
||||
const QMap<cereal::DeviceState::NetworkType, QString> network_type = { |
||||
{cereal::DeviceState::NetworkType::NONE, tr("--")}, |
||||
{cereal::DeviceState::NetworkType::WIFI, tr("Wi-Fi")}, |
||||
{cereal::DeviceState::NetworkType::ETHERNET, tr("ETH")}, |
||||
{cereal::DeviceState::NetworkType::CELL2_G, tr("2G")}, |
||||
{cereal::DeviceState::NetworkType::CELL3_G, tr("3G")}, |
||||
{cereal::DeviceState::NetworkType::CELL4_G, tr("LTE")}, |
||||
{cereal::DeviceState::NetworkType::CELL5_G, tr("5G")} |
||||
}; |
||||
|
||||
const QRect home_btn = QRect(60, 860, 180, 180); |
||||
const QRect settings_btn = QRect(50, 35, 200, 117); |
||||
const QRect mic_indicator_btn = QRect(158, 252, 75, 40); |
||||
const QColor good_color = QColor(255, 255, 255); |
||||
const QColor warning_color = QColor(218, 202, 37); |
||||
const QColor danger_color = QColor(201, 34, 49); |
||||
|
||||
ItemStatus connect_status, panda_status, temp_status; |
||||
QString net_type; |
||||
int net_strength = 0; |
||||
|
||||
private: |
||||
std::unique_ptr<PubMaster> pm; |
||||
Networking *networking = nullptr; |
||||
}; |
||||
@ -1,228 +0,0 @@ |
||||
#include "selfdrive/ui/qt/util.h" |
||||
|
||||
#include <map> |
||||
#include <string> |
||||
#include <vector> |
||||
|
||||
#include <QApplication> |
||||
#include <QDir> |
||||
#include <QFile> |
||||
#include <QFileInfo> |
||||
#include <QHash> |
||||
#include <QJsonDocument> |
||||
#include <QJsonObject> |
||||
#include <QLayoutItem> |
||||
#include <QStyleOption> |
||||
#include <QPainterPath> |
||||
#include <QTextStream> |
||||
#include <QtXml/QDomDocument> |
||||
|
||||
#include "common/swaglog.h" |
||||
#include "common/util.h" |
||||
#include "system/hardware/hw.h" |
||||
|
||||
QString getVersion() { |
||||
static QString version = QString::fromStdString(Params().get("Version")); |
||||
return version; |
||||
} |
||||
|
||||
QString getBrand() { |
||||
return QObject::tr("openpilot"); |
||||
} |
||||
|
||||
QString getUserAgent() { |
||||
return "openpilot-" + getVersion(); |
||||
} |
||||
|
||||
std::optional<QString> getDongleId() { |
||||
std::string id = Params().get("DongleId"); |
||||
|
||||
if (!id.empty() && (id != "UnregisteredDevice")) { |
||||
return QString::fromStdString(id); |
||||
} else { |
||||
return {}; |
||||
} |
||||
} |
||||
|
||||
QMap<QString, QString> getSupportedLanguages() { |
||||
QFile f(":/languages.json"); |
||||
f.open(QIODevice::ReadOnly | QIODevice::Text); |
||||
QString val = f.readAll(); |
||||
|
||||
QJsonObject obj = QJsonDocument::fromJson(val.toUtf8()).object(); |
||||
QMap<QString, QString> map; |
||||
for (auto key : obj.keys()) { |
||||
map[key] = obj[key].toString(); |
||||
} |
||||
return map; |
||||
} |
||||
|
||||
QString timeAgo(const QDateTime &date) { |
||||
if (!util::system_time_valid()) { |
||||
return date.date().toString(); |
||||
} |
||||
|
||||
int diff = date.secsTo(QDateTime::currentDateTimeUtc()); |
||||
|
||||
QString s; |
||||
if (diff < 60) { |
||||
s = QObject::tr("now"); |
||||
} else if (diff < 60 * 60) { |
||||
int minutes = diff / 60; |
||||
s = QObject::tr("%n minute(s) ago", "", minutes); |
||||
} else if (diff < 60 * 60 * 24) { |
||||
int hours = diff / (60 * 60); |
||||
s = QObject::tr("%n hour(s) ago", "", hours); |
||||
} else if (diff < 3600 * 24 * 7) { |
||||
int days = diff / (60 * 60 * 24); |
||||
s = QObject::tr("%n day(s) ago", "", days); |
||||
} else { |
||||
s = date.date().toString(); |
||||
} |
||||
|
||||
return s; |
||||
} |
||||
|
||||
void setQtSurfaceFormat() { |
||||
QSurfaceFormat fmt; |
||||
#ifdef __APPLE__ |
||||
fmt.setVersion(3, 2); |
||||
fmt.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); |
||||
fmt.setRenderableType(QSurfaceFormat::OpenGL); |
||||
#else |
||||
fmt.setRenderableType(QSurfaceFormat::OpenGLES); |
||||
#endif |
||||
fmt.setSamples(16); |
||||
fmt.setStencilBufferSize(1); |
||||
QSurfaceFormat::setDefaultFormat(fmt); |
||||
} |
||||
|
||||
void sigTermHandler(int s) { |
||||
std::signal(s, SIG_DFL); |
||||
qApp->quit(); |
||||
} |
||||
|
||||
void initApp(int argc, char *argv[], bool disable_hidpi) { |
||||
Hardware::set_display_power(true); |
||||
Hardware::set_brightness(65); |
||||
|
||||
// setup signal handlers to exit gracefully
|
||||
std::signal(SIGINT, sigTermHandler); |
||||
std::signal(SIGTERM, sigTermHandler); |
||||
|
||||
QString app_dir; |
||||
#ifdef __APPLE__ |
||||
// Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering
|
||||
QApplication tmp(argc, argv); |
||||
app_dir = QCoreApplication::applicationDirPath(); |
||||
if (disable_hidpi) { |
||||
qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio()).toLocal8Bit()); |
||||
} |
||||
#else |
||||
app_dir = QFileInfo(util::readlink("/proc/self/exe").c_str()).path(); |
||||
#endif |
||||
|
||||
qputenv("QT_DBL_CLICK_DIST", QByteArray::number(150)); |
||||
// ensure the current dir matches the exectuable's directory
|
||||
QDir::setCurrent(app_dir); |
||||
|
||||
setQtSurfaceFormat(); |
||||
} |
||||
|
||||
void swagLogMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { |
||||
static std::map<QtMsgType, int> levels = { |
||||
{QtMsgType::QtDebugMsg, CLOUDLOG_DEBUG}, |
||||
{QtMsgType::QtInfoMsg, CLOUDLOG_INFO}, |
||||
{QtMsgType::QtWarningMsg, CLOUDLOG_WARNING}, |
||||
{QtMsgType::QtCriticalMsg, CLOUDLOG_ERROR}, |
||||
{QtMsgType::QtSystemMsg, CLOUDLOG_ERROR}, |
||||
{QtMsgType::QtFatalMsg, CLOUDLOG_CRITICAL}, |
||||
}; |
||||
|
||||
std::string file, function; |
||||
if (context.file != nullptr) file = context.file; |
||||
if (context.function != nullptr) function = context.function; |
||||
|
||||
auto bts = msg.toUtf8(); |
||||
cloudlog_e(levels[type], file.c_str(), context.line, function.c_str(), "%s", bts.constData()); |
||||
} |
||||
|
||||
|
||||
QWidget* topWidget(QWidget* widget) { |
||||
while (widget->parentWidget() != nullptr) widget=widget->parentWidget(); |
||||
return widget; |
||||
} |
||||
|
||||
QPixmap loadPixmap(const QString &fileName, const QSize &size, Qt::AspectRatioMode aspectRatioMode) { |
||||
if (size.isEmpty()) { |
||||
return QPixmap(fileName); |
||||
} else { |
||||
return QPixmap(fileName).scaled(size, aspectRatioMode, Qt::SmoothTransformation); |
||||
} |
||||
} |
||||
|
||||
static QHash<QString, QByteArray> load_bootstrap_icons() { |
||||
QHash<QString, QByteArray> icons; |
||||
|
||||
QFile f(":/bootstrap-icons.svg"); |
||||
if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { |
||||
QDomDocument xml; |
||||
xml.setContent(&f); |
||||
QDomNode n = xml.documentElement().firstChild(); |
||||
while (!n.isNull()) { |
||||
QDomElement e = n.toElement(); |
||||
if (!e.isNull() && e.hasAttribute("id")) { |
||||
QString svg_str; |
||||
QTextStream stream(&svg_str); |
||||
n.save(stream, 0); |
||||
svg_str.replace("<symbol", "<svg"); |
||||
svg_str.replace("</symbol>", "</svg>"); |
||||
icons[e.attribute("id")] = svg_str.toUtf8(); |
||||
} |
||||
n = n.nextSibling(); |
||||
} |
||||
} |
||||
return icons; |
||||
} |
||||
|
||||
QPixmap bootstrapPixmap(const QString &id) { |
||||
static QHash<QString, QByteArray> icons = load_bootstrap_icons(); |
||||
|
||||
QPixmap pixmap; |
||||
if (auto it = icons.find(id); it != icons.end()) { |
||||
pixmap.loadFromData(it.value(), "svg"); |
||||
} |
||||
return pixmap; |
||||
} |
||||
|
||||
bool hasLongitudinalControl(const cereal::CarParams::Reader &car_params) { |
||||
// Using the experimental longitudinal toggle, returns whether longitudinal control
|
||||
// will be active without needing a restart of openpilot
|
||||
return car_params.getAlphaLongitudinalAvailable() |
||||
? Params().getBool("AlphaLongitudinalEnabled") |
||||
: car_params.getOpenpilotLongitudinalControl(); |
||||
} |
||||
|
||||
// ParamWatcher
|
||||
|
||||
ParamWatcher::ParamWatcher(QObject *parent) : QObject(parent) { |
||||
watcher = new QFileSystemWatcher(this); |
||||
QObject::connect(watcher, &QFileSystemWatcher::fileChanged, this, &ParamWatcher::fileChanged); |
||||
} |
||||
|
||||
void ParamWatcher::fileChanged(const QString &path) { |
||||
auto param_name = QFileInfo(path).fileName(); |
||||
auto param_value = QString::fromStdString(params.get(param_name.toStdString())); |
||||
|
||||
auto it = params_hash.find(param_name); |
||||
bool content_changed = (it == params_hash.end()) || (it.value() != param_value); |
||||
params_hash[param_name] = param_value; |
||||
// emit signal when the content changes.
|
||||
if (content_changed) { |
||||
emit paramChanged(param_name, param_value); |
||||
} |
||||
} |
||||
|
||||
void ParamWatcher::addParam(const QString ¶m_name) { |
||||
watcher->addPath(QString::fromStdString(params.getParamPath(param_name.toStdString()))); |
||||
} |
||||
@ -1,54 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <optional> |
||||
#include <vector> |
||||
|
||||
#include <QDateTime> |
||||
#include <QFileSystemWatcher> |
||||
#include <QPainter> |
||||
#include <QPixmap> |
||||
#include <QSurfaceFormat> |
||||
#include <QWidget> |
||||
|
||||
#include "cereal/gen/cpp/car.capnp.h" |
||||
#include "common/params.h" |
||||
|
||||
QString getVersion(); |
||||
QString getBrand(); |
||||
QString getUserAgent(); |
||||
std::optional<QString> getDongleId(); |
||||
QMap<QString, QString> getSupportedLanguages(); |
||||
void setQtSurfaceFormat(); |
||||
void sigTermHandler(int s); |
||||
QString timeAgo(const QDateTime &date); |
||||
void swagLogMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg); |
||||
void initApp(int argc, char *argv[], bool disable_hidpi = true); |
||||
QWidget* topWidget(QWidget* widget); |
||||
QPixmap loadPixmap(const QString &fileName, const QSize &size = {}, Qt::AspectRatioMode aspectRatioMode = Qt::KeepAspectRatio); |
||||
QPixmap bootstrapPixmap(const QString &id); |
||||
bool hasLongitudinalControl(const cereal::CarParams::Reader &car_params); |
||||
|
||||
struct InterFont : public QFont { |
||||
InterFont(int pixel_size, QFont::Weight weight = QFont::Normal) : QFont("Inter") { |
||||
setPixelSize(pixel_size); |
||||
setWeight(weight); |
||||
} |
||||
}; |
||||
|
||||
class ParamWatcher : public QObject { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
ParamWatcher(QObject *parent); |
||||
void addParam(const QString ¶m_name); |
||||
|
||||
signals: |
||||
void paramChanged(const QString ¶m_name, const QString ¶m_value); |
||||
|
||||
private: |
||||
void fileChanged(const QString &path); |
||||
|
||||
QFileSystemWatcher *watcher; |
||||
QHash<QString, QString> params_hash; |
||||
Params params; |
||||
}; |
||||
@ -1,365 +0,0 @@ |
||||
#include "selfdrive/ui/qt/widgets/cameraview.h" |
||||
|
||||
#ifdef __APPLE__ |
||||
#include <OpenGL/gl3.h> |
||||
#else |
||||
#include <GLES3/gl3.h> |
||||
#endif |
||||
|
||||
#include <cmath> |
||||
#include <QApplication> |
||||
|
||||
namespace { |
||||
|
||||
const char frame_vertex_shader[] = |
||||
#ifdef __APPLE__ |
||||
"#version 330 core\n" |
||||
#else |
||||
"#version 300 es\n" |
||||
#endif |
||||
"layout(location = 0) in vec4 aPosition;\n" |
||||
"layout(location = 1) in vec2 aTexCoord;\n" |
||||
"uniform mat4 uTransform;\n" |
||||
"out vec2 vTexCoord;\n" |
||||
"void main() {\n" |
||||
" gl_Position = uTransform * aPosition;\n" |
||||
" vTexCoord = aTexCoord;\n" |
||||
"}\n"; |
||||
|
||||
const char frame_fragment_shader[] = |
||||
#ifdef __TICI__ |
||||
"#version 300 es\n" |
||||
"#extension GL_OES_EGL_image_external_essl3 : enable\n" |
||||
"precision mediump float;\n" |
||||
"uniform samplerExternalOES uTexture;\n" |
||||
"in vec2 vTexCoord;\n" |
||||
"out vec4 colorOut;\n" |
||||
"void main() {\n" |
||||
" colorOut = texture(uTexture, vTexCoord);\n" |
||||
// gamma to improve worst case visibility when dark
|
||||
" colorOut.rgb = pow(colorOut.rgb, vec3(1.0/1.28));\n" |
||||
"}\n"; |
||||
#else |
||||
#ifdef __APPLE__ |
||||
"#version 330 core\n" |
||||
#else |
||||
"#version 300 es\n" |
||||
"precision mediump float;\n" |
||||
#endif |
||||
"uniform sampler2D uTextureY;\n" |
||||
"uniform sampler2D uTextureUV;\n" |
||||
"in vec2 vTexCoord;\n" |
||||
"out vec4 colorOut;\n" |
||||
"void main() {\n" |
||||
" float y = texture(uTextureY, vTexCoord).r;\n" |
||||
" vec2 uv = texture(uTextureUV, vTexCoord).rg - 0.5;\n" |
||||
" float r = y + 1.402 * uv.y;\n" |
||||
" float g = y - 0.344 * uv.x - 0.714 * uv.y;\n" |
||||
" float b = y + 1.772 * uv.x;\n" |
||||
" colorOut = vec4(r, g, b, 1.0);\n" |
||||
"}\n"; |
||||
#endif |
||||
|
||||
} // namespace
|
||||
|
||||
CameraWidget::CameraWidget(std::string stream_name, VisionStreamType type, QWidget* parent) : |
||||
stream_name(stream_name), active_stream_type(type), requested_stream_type(type), QOpenGLWidget(parent) { |
||||
setAttribute(Qt::WA_OpaquePaintEvent); |
||||
qRegisterMetaType<std::set<VisionStreamType>>("availableStreams"); |
||||
QObject::connect(this, &CameraWidget::vipcThreadConnected, this, &CameraWidget::vipcConnected, Qt::BlockingQueuedConnection); |
||||
QObject::connect(this, &CameraWidget::vipcThreadFrameReceived, this, &CameraWidget::vipcFrameReceived, Qt::QueuedConnection); |
||||
QObject::connect(this, &CameraWidget::vipcAvailableStreamsUpdated, this, &CameraWidget::availableStreamsUpdated, Qt::QueuedConnection); |
||||
QObject::connect(QApplication::instance(), &QCoreApplication::aboutToQuit, this, &CameraWidget::stopVipcThread); |
||||
} |
||||
|
||||
CameraWidget::~CameraWidget() { |
||||
makeCurrent(); |
||||
stopVipcThread(); |
||||
if (isValid()) { |
||||
glDeleteVertexArrays(1, &frame_vao); |
||||
glDeleteBuffers(1, &frame_vbo); |
||||
glDeleteBuffers(1, &frame_ibo); |
||||
#ifndef __TICI__ |
||||
glDeleteTextures(2, textures); |
||||
#endif |
||||
} |
||||
doneCurrent(); |
||||
} |
||||
|
||||
// Qt uses device-independent pixels, depending on platform this may be
|
||||
// different to what OpenGL uses
|
||||
int CameraWidget::glWidth() { |
||||
return width() * devicePixelRatio(); |
||||
} |
||||
|
||||
int CameraWidget::glHeight() { |
||||
return height() * devicePixelRatio(); |
||||
} |
||||
|
||||
void CameraWidget::initializeGL() { |
||||
initializeOpenGLFunctions(); |
||||
|
||||
program = std::make_unique<QOpenGLShaderProgram>(context()); |
||||
bool ret = program->addShaderFromSourceCode(QOpenGLShader::Vertex, frame_vertex_shader); |
||||
assert(ret); |
||||
ret = program->addShaderFromSourceCode(QOpenGLShader::Fragment, frame_fragment_shader); |
||||
assert(ret); |
||||
|
||||
program->link(); |
||||
GLint frame_pos_loc = program->attributeLocation("aPosition"); |
||||
GLint frame_texcoord_loc = program->attributeLocation("aTexCoord"); |
||||
|
||||
auto [x1, x2, y1, y2] = requested_stream_type == VISION_STREAM_DRIVER ? std::tuple(0.f, 1.f, 1.f, 0.f) : std::tuple(1.f, 0.f, 1.f, 0.f); |
||||
const uint8_t frame_indicies[] = {0, 1, 2, 0, 2, 3}; |
||||
const float frame_coords[4][4] = { |
||||
{-1.0, -1.0, x2, y1}, // bl
|
||||
{-1.0, 1.0, x2, y2}, // tl
|
||||
{ 1.0, 1.0, x1, y2}, // tr
|
||||
{ 1.0, -1.0, x1, y1}, // br
|
||||
}; |
||||
|
||||
glGenVertexArrays(1, &frame_vao); |
||||
glBindVertexArray(frame_vao); |
||||
glGenBuffers(1, &frame_vbo); |
||||
glBindBuffer(GL_ARRAY_BUFFER, frame_vbo); |
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(frame_coords), frame_coords, GL_STATIC_DRAW); |
||||
glEnableVertexAttribArray(frame_pos_loc); |
||||
glVertexAttribPointer(frame_pos_loc, 2, GL_FLOAT, GL_FALSE, |
||||
sizeof(frame_coords[0]), (const void *)0); |
||||
glEnableVertexAttribArray(frame_texcoord_loc); |
||||
glVertexAttribPointer(frame_texcoord_loc, 2, GL_FLOAT, GL_FALSE, |
||||
sizeof(frame_coords[0]), (const void *)(sizeof(float) * 2)); |
||||
glGenBuffers(1, &frame_ibo); |
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, frame_ibo); |
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(frame_indicies), frame_indicies, GL_STATIC_DRAW); |
||||
glBindBuffer(GL_ARRAY_BUFFER, 0); |
||||
glBindVertexArray(0); |
||||
|
||||
glUseProgram(program->programId()); |
||||
|
||||
#ifdef __TICI__ |
||||
glUniform1i(program->uniformLocation("uTexture"), 0); |
||||
#else |
||||
glGenTextures(2, textures); |
||||
glUniform1i(program->uniformLocation("uTextureY"), 0); |
||||
glUniform1i(program->uniformLocation("uTextureUV"), 1); |
||||
#endif |
||||
} |
||||
|
||||
void CameraWidget::showEvent(QShowEvent *event) { |
||||
if (!vipc_thread) { |
||||
clearFrames(); |
||||
vipc_thread = new QThread(); |
||||
connect(vipc_thread, &QThread::started, [=]() { vipcThread(); }); |
||||
connect(vipc_thread, &QThread::finished, vipc_thread, &QObject::deleteLater); |
||||
vipc_thread->start(); |
||||
} |
||||
} |
||||
|
||||
void CameraWidget::stopVipcThread() { |
||||
makeCurrent(); |
||||
if (vipc_thread) { |
||||
vipc_thread->requestInterruption(); |
||||
vipc_thread->quit(); |
||||
vipc_thread->wait(); |
||||
vipc_thread = nullptr; |
||||
} |
||||
|
||||
#ifdef __TICI__ |
||||
EGLDisplay egl_display = eglGetCurrentDisplay(); |
||||
assert(egl_display != EGL_NO_DISPLAY); |
||||
for (auto &pair : egl_images) { |
||||
eglDestroyImageKHR(egl_display, pair.second); |
||||
assert(eglGetError() == EGL_SUCCESS); |
||||
} |
||||
egl_images.clear(); |
||||
#endif |
||||
} |
||||
|
||||
void CameraWidget::availableStreamsUpdated(std::set<VisionStreamType> streams) { |
||||
available_streams = streams; |
||||
} |
||||
|
||||
mat4 CameraWidget::calcFrameMatrix() { |
||||
// Scale the frame to fit the widget while maintaining the aspect ratio.
|
||||
float widget_aspect_ratio = (float)width() / height(); |
||||
float frame_aspect_ratio = (float)stream_width / stream_height; |
||||
float zx = std::min(frame_aspect_ratio / widget_aspect_ratio, 1.0f); |
||||
float zy = std::min(widget_aspect_ratio / frame_aspect_ratio, 1.0f); |
||||
|
||||
return mat4{{ |
||||
zx, 0.0, 0.0, 0.0, |
||||
0.0, zy, 0.0, 0.0, |
||||
0.0, 0.0, 1.0, 0.0, |
||||
0.0, 0.0, 0.0, 1.0, |
||||
}}; |
||||
} |
||||
|
||||
void CameraWidget::paintGL() { |
||||
glClearColor(bg.redF(), bg.greenF(), bg.blueF(), bg.alphaF()); |
||||
glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT); |
||||
|
||||
std::lock_guard lk(frame_lock); |
||||
if (frames.empty()) return; |
||||
|
||||
int frame_idx = frames.size() - 1; |
||||
|
||||
// Always draw latest frame until sync logic is more stable
|
||||
// for (frame_idx = 0; frame_idx < frames.size() - 1; frame_idx++) {
|
||||
// if (frames[frame_idx].first == draw_frame_id) break;
|
||||
// }
|
||||
|
||||
// Log duplicate/dropped frames
|
||||
if (frames[frame_idx].first == prev_frame_id) { |
||||
qDebug() << "Drawing same frame twice" << frames[frame_idx].first; |
||||
} else if (frames[frame_idx].first != prev_frame_id + 1) { |
||||
qDebug() << "Skipped frame" << frames[frame_idx].first; |
||||
} |
||||
prev_frame_id = frames[frame_idx].first; |
||||
VisionBuf *frame = frames[frame_idx].second; |
||||
assert(frame != nullptr); |
||||
|
||||
auto frame_mat = calcFrameMatrix(); |
||||
|
||||
glViewport(0, 0, glWidth(), glHeight()); |
||||
glBindVertexArray(frame_vao); |
||||
glUseProgram(program->programId()); |
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); |
||||
|
||||
#ifdef __TICI__ |
||||
// no frame copy
|
||||
glActiveTexture(GL_TEXTURE0); |
||||
glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, egl_images[frame->idx]); |
||||
assert(glGetError() == GL_NO_ERROR); |
||||
#else |
||||
// fallback to copy
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride); |
||||
glActiveTexture(GL_TEXTURE0); |
||||
glBindTexture(GL_TEXTURE_2D, textures[0]); |
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, stream_width, stream_height, GL_RED, GL_UNSIGNED_BYTE, frame->y); |
||||
assert(glGetError() == GL_NO_ERROR); |
||||
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride/2); |
||||
glActiveTexture(GL_TEXTURE0 + 1); |
||||
glBindTexture(GL_TEXTURE_2D, textures[1]); |
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, stream_width/2, stream_height/2, GL_RG, GL_UNSIGNED_BYTE, frame->uv); |
||||
assert(glGetError() == GL_NO_ERROR); |
||||
#endif |
||||
|
||||
glUniformMatrix4fv(program->uniformLocation("uTransform"), 1, GL_TRUE, frame_mat.v); |
||||
glEnableVertexAttribArray(0); |
||||
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, (const void *)0); |
||||
glDisableVertexAttribArray(0); |
||||
glBindVertexArray(0); |
||||
glBindTexture(GL_TEXTURE_2D, 0); |
||||
glActiveTexture(GL_TEXTURE0); |
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4); |
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); |
||||
} |
||||
|
||||
void CameraWidget::vipcConnected(VisionIpcClient *vipc_client) { |
||||
makeCurrent(); |
||||
stream_width = vipc_client->buffers[0].width; |
||||
stream_height = vipc_client->buffers[0].height; |
||||
stream_stride = vipc_client->buffers[0].stride; |
||||
|
||||
#ifdef __TICI__ |
||||
EGLDisplay egl_display = eglGetCurrentDisplay(); |
||||
assert(egl_display != EGL_NO_DISPLAY); |
||||
for (auto &pair : egl_images) { |
||||
eglDestroyImageKHR(egl_display, pair.second); |
||||
} |
||||
egl_images.clear(); |
||||
|
||||
for (int i = 0; i < vipc_client->num_buffers; i++) { // import buffers into OpenGL
|
||||
int fd = dup(vipc_client->buffers[i].fd); // eglDestroyImageKHR will close, so duplicate
|
||||
EGLint img_attrs[] = { |
||||
EGL_WIDTH, (int)vipc_client->buffers[i].width, |
||||
EGL_HEIGHT, (int)vipc_client->buffers[i].height, |
||||
EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_NV12, |
||||
EGL_DMA_BUF_PLANE0_FD_EXT, fd, |
||||
EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0, |
||||
EGL_DMA_BUF_PLANE0_PITCH_EXT, (int)vipc_client->buffers[i].stride, |
||||
EGL_DMA_BUF_PLANE1_FD_EXT, fd, |
||||
EGL_DMA_BUF_PLANE1_OFFSET_EXT, (int)vipc_client->buffers[i].uv_offset, |
||||
EGL_DMA_BUF_PLANE1_PITCH_EXT, (int)vipc_client->buffers[i].stride, |
||||
EGL_NONE |
||||
}; |
||||
egl_images[i] = eglCreateImageKHR(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, 0, img_attrs); |
||||
assert(eglGetError() == EGL_SUCCESS); |
||||
} |
||||
#else |
||||
glBindTexture(GL_TEXTURE_2D, textures[0]); |
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); |
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); |
||||
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); |
||||
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); |
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, stream_width, stream_height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); |
||||
assert(glGetError() == GL_NO_ERROR); |
||||
|
||||
glBindTexture(GL_TEXTURE_2D, textures[1]); |
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); |
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); |
||||
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); |
||||
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); |
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RG8, stream_width/2, stream_height/2, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr); |
||||
assert(glGetError() == GL_NO_ERROR); |
||||
#endif |
||||
} |
||||
|
||||
void CameraWidget::vipcFrameReceived() { |
||||
update(); |
||||
} |
||||
|
||||
void CameraWidget::vipcThread() { |
||||
VisionStreamType cur_stream = requested_stream_type; |
||||
std::unique_ptr<VisionIpcClient> vipc_client; |
||||
VisionIpcBufExtra meta_main = {0}; |
||||
|
||||
while (!QThread::currentThread()->isInterruptionRequested()) { |
||||
if (!vipc_client || cur_stream != requested_stream_type) { |
||||
clearFrames(); |
||||
qDebug().nospace() << "connecting to stream " << requested_stream_type << ", was connected to " << cur_stream; |
||||
cur_stream = requested_stream_type; |
||||
vipc_client.reset(new VisionIpcClient(stream_name, cur_stream, false)); |
||||
} |
||||
active_stream_type = cur_stream; |
||||
|
||||
if (!vipc_client->connected) { |
||||
clearFrames(); |
||||
auto streams = VisionIpcClient::getAvailableStreams(stream_name, false); |
||||
if (streams.empty()) { |
||||
QThread::msleep(100); |
||||
continue; |
||||
} |
||||
emit vipcAvailableStreamsUpdated(streams); |
||||
|
||||
if (!vipc_client->connect(false)) { |
||||
QThread::msleep(100); |
||||
continue; |
||||
} |
||||
emit vipcThreadConnected(vipc_client.get()); |
||||
} |
||||
|
||||
if (VisionBuf *buf = vipc_client->recv(&meta_main, 1000)) { |
||||
{ |
||||
std::lock_guard lk(frame_lock); |
||||
frames.push_back(std::make_pair(meta_main.frame_id, buf)); |
||||
while (frames.size() > FRAME_BUFFER_SIZE) { |
||||
frames.pop_front(); |
||||
} |
||||
} |
||||
emit vipcThreadFrameReceived(); |
||||
} else { |
||||
if (!isVisible()) { |
||||
vipc_client->connected = false; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
void CameraWidget::clearFrames() { |
||||
std::lock_guard lk(frame_lock); |
||||
frames.clear(); |
||||
available_streams.clear(); |
||||
} |
||||
@ -1,89 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <deque> |
||||
#include <map> |
||||
#include <memory> |
||||
#include <mutex> |
||||
#include <set> |
||||
#include <string> |
||||
#include <utility> |
||||
|
||||
#include <QOpenGLFunctions> |
||||
#include <QOpenGLShaderProgram> |
||||
#include <QOpenGLWidget> |
||||
#include <QThread> |
||||
|
||||
#ifdef __TICI__ |
||||
#define EGL_EGLEXT_PROTOTYPES |
||||
#define EGL_NO_X11 |
||||
#define GL_TEXTURE_EXTERNAL_OES 0x8D65 |
||||
#include <EGL/egl.h> |
||||
#include <EGL/eglext.h> |
||||
#include <drm/drm_fourcc.h> |
||||
#endif |
||||
|
||||
#include "msgq/visionipc/visionipc_client.h" |
||||
#include "selfdrive/ui/ui.h" |
||||
|
||||
const int FRAME_BUFFER_SIZE = 5; |
||||
|
||||
class CameraWidget : public QOpenGLWidget, protected QOpenGLFunctions { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
using QOpenGLWidget::QOpenGLWidget; |
||||
explicit CameraWidget(std::string stream_name, VisionStreamType stream_type, QWidget* parent = nullptr); |
||||
~CameraWidget(); |
||||
void setBackgroundColor(const QColor &color) { bg = color; } |
||||
void setFrameId(int frame_id) { draw_frame_id = frame_id; } |
||||
void setStreamType(VisionStreamType type) { requested_stream_type = type; } |
||||
VisionStreamType getStreamType() { return active_stream_type; } |
||||
void stopVipcThread(); |
||||
|
||||
signals: |
||||
void clicked(); |
||||
void vipcThreadConnected(VisionIpcClient *); |
||||
void vipcThreadFrameReceived(); |
||||
void vipcAvailableStreamsUpdated(std::set<VisionStreamType>); |
||||
|
||||
protected: |
||||
void paintGL() override; |
||||
void initializeGL() override; |
||||
void showEvent(QShowEvent *event) override; |
||||
void mouseReleaseEvent(QMouseEvent *event) override { emit clicked(); } |
||||
virtual mat4 calcFrameMatrix(); |
||||
void vipcThread(); |
||||
void clearFrames(); |
||||
|
||||
int glWidth(); |
||||
int glHeight(); |
||||
|
||||
GLuint frame_vao, frame_vbo, frame_ibo; |
||||
GLuint textures[2]; |
||||
std::unique_ptr<QOpenGLShaderProgram> program; |
||||
QColor bg = QColor("#000000"); |
||||
|
||||
#ifdef __TICI__ |
||||
std::map<int, EGLImageKHR> egl_images; |
||||
#endif |
||||
|
||||
std::string stream_name; |
||||
int stream_width = 0; |
||||
int stream_height = 0; |
||||
int stream_stride = 0; |
||||
std::atomic<VisionStreamType> active_stream_type; |
||||
std::atomic<VisionStreamType> requested_stream_type; |
||||
std::set<VisionStreamType> available_streams; |
||||
QThread *vipc_thread = nullptr; |
||||
std::recursive_mutex frame_lock; |
||||
std::deque<std::pair<uint32_t, VisionBuf*>> frames; |
||||
uint32_t draw_frame_id = 0; |
||||
uint32_t prev_frame_id = 0; |
||||
|
||||
protected slots: |
||||
void vipcConnected(VisionIpcClient *vipc_client); |
||||
void vipcFrameReceived(); |
||||
void availableStreamsUpdated(std::set<VisionStreamType> streams); |
||||
}; |
||||
|
||||
Q_DECLARE_METATYPE(std::set<VisionStreamType>); |
||||
@ -1,141 +0,0 @@ |
||||
#include "selfdrive/ui/qt/widgets/controls.h" |
||||
|
||||
#include <QPainter> |
||||
#include <QStyleOption> |
||||
|
||||
AbstractControl::AbstractControl(const QString &title, const QString &desc, const QString &icon, QWidget *parent) : QFrame(parent) { |
||||
QVBoxLayout *main_layout = new QVBoxLayout(this); |
||||
main_layout->setMargin(0); |
||||
|
||||
hlayout = new QHBoxLayout; |
||||
hlayout->setMargin(0); |
||||
hlayout->setSpacing(20); |
||||
|
||||
// left icon
|
||||
icon_label = new QLabel(this); |
||||
hlayout->addWidget(icon_label); |
||||
if (!icon.isEmpty()) { |
||||
icon_pixmap = QPixmap(icon).scaledToWidth(80, Qt::SmoothTransformation); |
||||
icon_label->setPixmap(icon_pixmap); |
||||
icon_label->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); |
||||
} |
||||
icon_label->setVisible(!icon.isEmpty()); |
||||
|
||||
// title
|
||||
title_label = new QPushButton(title); |
||||
title_label->setFixedHeight(120); |
||||
title_label->setStyleSheet("font-size: 50px; font-weight: 400; text-align: left; border: none;"); |
||||
hlayout->addWidget(title_label, 1); |
||||
|
||||
// value next to control button
|
||||
value = new ElidedLabel(); |
||||
value->setAlignment(Qt::AlignRight | Qt::AlignVCenter); |
||||
value->setStyleSheet("color: #aaaaaa"); |
||||
hlayout->addWidget(value); |
||||
|
||||
main_layout->addLayout(hlayout); |
||||
|
||||
// description
|
||||
description = new QLabel(desc); |
||||
description->setContentsMargins(40, 20, 40, 20); |
||||
description->setStyleSheet("font-size: 40px; color: grey"); |
||||
description->setWordWrap(true); |
||||
description->setVisible(false); |
||||
main_layout->addWidget(description); |
||||
|
||||
connect(title_label, &QPushButton::clicked, [=]() { |
||||
if (!description->isVisible()) { |
||||
emit showDescriptionEvent(); |
||||
} |
||||
|
||||
if (!description->text().isEmpty()) { |
||||
description->setVisible(!description->isVisible()); |
||||
} |
||||
}); |
||||
|
||||
main_layout->addStretch(); |
||||
} |
||||
|
||||
void AbstractControl::hideEvent(QHideEvent *e) { |
||||
if (description != nullptr) { |
||||
description->hide(); |
||||
} |
||||
} |
||||
|
||||
// controls
|
||||
|
||||
ButtonControl::ButtonControl(const QString &title, const QString &text, const QString &desc, QWidget *parent) : AbstractControl(title, desc, "", parent) { |
||||
btn.setText(text); |
||||
btn.setStyleSheet(R"( |
||||
QPushButton { |
||||
padding: 0; |
||||
border-radius: 50px; |
||||
font-size: 35px; |
||||
font-weight: 500; |
||||
color: #E4E4E4; |
||||
background-color: #393939; |
||||
} |
||||
QPushButton:pressed { |
||||
background-color: #4a4a4a; |
||||
} |
||||
QPushButton:disabled { |
||||
color: #33E4E4E4; |
||||
} |
||||
)"); |
||||
btn.setFixedSize(250, 100); |
||||
QObject::connect(&btn, &QPushButton::clicked, this, &ButtonControl::clicked); |
||||
hlayout->addWidget(&btn); |
||||
} |
||||
|
||||
// ElidedLabel
|
||||
|
||||
ElidedLabel::ElidedLabel(QWidget *parent) : ElidedLabel({}, parent) {} |
||||
|
||||
ElidedLabel::ElidedLabel(const QString &text, QWidget *parent) : QLabel(text.trimmed(), parent) { |
||||
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); |
||||
setMinimumWidth(1); |
||||
} |
||||
|
||||
void ElidedLabel::resizeEvent(QResizeEvent* event) { |
||||
QLabel::resizeEvent(event); |
||||
lastText_ = elidedText_ = ""; |
||||
} |
||||
|
||||
void ElidedLabel::paintEvent(QPaintEvent *event) { |
||||
const QString curText = text(); |
||||
if (curText != lastText_) { |
||||
elidedText_ = fontMetrics().elidedText(curText, Qt::ElideRight, contentsRect().width()); |
||||
lastText_ = curText; |
||||
} |
||||
|
||||
QPainter painter(this); |
||||
drawFrame(&painter); |
||||
QStyleOption opt; |
||||
opt.initFrom(this); |
||||
style()->drawItemText(&painter, contentsRect(), alignment(), opt.palette, isEnabled(), elidedText_, foregroundRole()); |
||||
} |
||||
|
||||
// ParamControl
|
||||
|
||||
ParamControl::ParamControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent) |
||||
: ToggleControl(title, desc, icon, false, parent) { |
||||
key = param.toStdString(); |
||||
QObject::connect(this, &ParamControl::toggleFlipped, this, &ParamControl::toggleClicked); |
||||
} |
||||
|
||||
void ParamControl::toggleClicked(bool state) { |
||||
auto do_confirm = [this]() { |
||||
QString content("<body><h2 style=\"text-align: center;\">" + title_label->text() + "</h2><br>" |
||||
"<p style=\"text-align: center; margin: 0 128px; font-size: 50px;\">" + getDescription() + "</p></body>"); |
||||
return ConfirmationDialog(content, tr("Enable"), tr("Cancel"), true, this).exec(); |
||||
}; |
||||
|
||||
bool confirmed = store_confirm && params.getBool(key + "Confirmed"); |
||||
if (!confirm || confirmed || !state || do_confirm()) { |
||||
if (store_confirm && state) params.putBool(key + "Confirmed", true); |
||||
params.putBool(key, state); |
||||
setIcon(state); |
||||
} else { |
||||
toggle.togglePosition(); |
||||
} |
||||
} |
||||
@ -1,319 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <string> |
||||
#include <vector> |
||||
|
||||
#include <QButtonGroup> |
||||
#include <QFrame> |
||||
#include <QHBoxLayout> |
||||
#include <QLabel> |
||||
#include <QPainter> |
||||
#include <QPushButton> |
||||
|
||||
#include "common/params.h" |
||||
#include "selfdrive/ui/qt/widgets/input.h" |
||||
#include "selfdrive/ui/qt/widgets/toggle.h" |
||||
|
||||
class ElidedLabel : public QLabel { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit ElidedLabel(QWidget *parent = 0); |
||||
explicit ElidedLabel(const QString &text, QWidget *parent = 0); |
||||
|
||||
signals: |
||||
void clicked(); |
||||
|
||||
protected: |
||||
void paintEvent(QPaintEvent *event) override; |
||||
void resizeEvent(QResizeEvent* event) override; |
||||
void mouseReleaseEvent(QMouseEvent *event) override { |
||||
if (rect().contains(event->pos())) { |
||||
emit clicked(); |
||||
} |
||||
} |
||||
QString lastText_, elidedText_; |
||||
}; |
||||
|
||||
|
||||
class AbstractControl : public QFrame { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
void setDescription(const QString &desc) { |
||||
if (description) description->setText(desc); |
||||
} |
||||
|
||||
void setTitle(const QString &title) { |
||||
title_label->setText(title); |
||||
} |
||||
|
||||
void setValue(const QString &val) { |
||||
value->setText(val); |
||||
} |
||||
|
||||
const QString getDescription() { |
||||
return description->text(); |
||||
} |
||||
|
||||
QLabel *icon_label; |
||||
QPixmap icon_pixmap; |
||||
|
||||
public slots: |
||||
void showDescription() { |
||||
description->setVisible(true); |
||||
} |
||||
|
||||
signals: |
||||
void showDescriptionEvent(); |
||||
|
||||
protected: |
||||
AbstractControl(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr); |
||||
void hideEvent(QHideEvent *e) override; |
||||
|
||||
QHBoxLayout *hlayout; |
||||
QPushButton *title_label; |
||||
|
||||
private: |
||||
ElidedLabel *value; |
||||
QLabel *description = nullptr; |
||||
}; |
||||
|
||||
// widget to display a value
|
||||
class LabelControl : public AbstractControl { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
LabelControl(const QString &title, const QString &text = "", const QString &desc = "", QWidget *parent = nullptr) : AbstractControl(title, desc, "", parent) { |
||||
label.setText(text); |
||||
label.setAlignment(Qt::AlignRight | Qt::AlignVCenter); |
||||
hlayout->addWidget(&label); |
||||
} |
||||
void setText(const QString &text) { label.setText(text); } |
||||
|
||||
private: |
||||
ElidedLabel label; |
||||
}; |
||||
|
||||
// widget for a button with a label
|
||||
class ButtonControl : public AbstractControl { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
ButtonControl(const QString &title, const QString &text, const QString &desc = "", QWidget *parent = nullptr); |
||||
inline void setText(const QString &text) { btn.setText(text); } |
||||
inline QString text() const { return btn.text(); } |
||||
|
||||
signals: |
||||
void clicked(); |
||||
|
||||
public slots: |
||||
void setEnabled(bool enabled) { btn.setEnabled(enabled); } |
||||
|
||||
private: |
||||
QPushButton btn; |
||||
}; |
||||
|
||||
class ToggleControl : public AbstractControl { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
ToggleControl(const QString &title, const QString &desc = "", const QString &icon = "", const bool state = false, QWidget *parent = nullptr) : AbstractControl(title, desc, icon, parent) { |
||||
toggle.setFixedSize(150, 100); |
||||
if (state) { |
||||
toggle.togglePosition(); |
||||
} |
||||
hlayout->addWidget(&toggle); |
||||
QObject::connect(&toggle, &Toggle::stateChanged, this, &ToggleControl::toggleFlipped); |
||||
} |
||||
|
||||
void setEnabled(bool enabled) { |
||||
toggle.setEnabled(enabled); |
||||
toggle.update(); |
||||
} |
||||
|
||||
signals: |
||||
void toggleFlipped(bool state); |
||||
|
||||
protected: |
||||
Toggle toggle; |
||||
}; |
||||
|
||||
// widget to toggle params
|
||||
class ParamControl : public ToggleControl { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
ParamControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent = nullptr); |
||||
void setConfirmation(bool _confirm, bool _store_confirm) { |
||||
confirm = _confirm; |
||||
store_confirm = _store_confirm; |
||||
} |
||||
|
||||
void setActiveIcon(const QString &icon) { |
||||
active_icon_pixmap = QPixmap(icon).scaledToWidth(80, Qt::SmoothTransformation); |
||||
} |
||||
|
||||
void refresh() { |
||||
bool state = params.getBool(key); |
||||
if (state != toggle.on) { |
||||
toggle.togglePosition(); |
||||
setIcon(state); |
||||
} |
||||
} |
||||
|
||||
void showEvent(QShowEvent *event) override { |
||||
refresh(); |
||||
} |
||||
|
||||
private: |
||||
void toggleClicked(bool state); |
||||
void setIcon(bool state) { |
||||
if (state && !active_icon_pixmap.isNull()) { |
||||
icon_label->setPixmap(active_icon_pixmap); |
||||
} else if (!icon_pixmap.isNull()) { |
||||
icon_label->setPixmap(icon_pixmap); |
||||
} |
||||
} |
||||
|
||||
std::string key; |
||||
Params params; |
||||
QPixmap active_icon_pixmap; |
||||
bool confirm = false; |
||||
bool store_confirm = false; |
||||
}; |
||||
|
||||
class MultiButtonControl : public AbstractControl { |
||||
Q_OBJECT |
||||
public: |
||||
MultiButtonControl(const QString &title, const QString &desc, const QString &icon, |
||||
const std::vector<QString> &button_texts, const int minimum_button_width = 225) : AbstractControl(title, desc, icon) { |
||||
const QString style = R"( |
||||
QPushButton { |
||||
border-radius: 50px; |
||||
font-size: 40px; |
||||
font-weight: 500; |
||||
height:100px; |
||||
padding: 0 25 0 25; |
||||
color: #E4E4E4; |
||||
background-color: #393939; |
||||
} |
||||
QPushButton:pressed { |
||||
background-color: #4a4a4a; |
||||
} |
||||
QPushButton:checked:enabled { |
||||
background-color: #33Ab4C; |
||||
} |
||||
QPushButton:checked:disabled { |
||||
background-color: #9933Ab4C; |
||||
} |
||||
QPushButton:disabled { |
||||
color: #33E4E4E4; |
||||
} |
||||
)"; |
||||
|
||||
button_group = new QButtonGroup(this); |
||||
button_group->setExclusive(true); |
||||
for (int i = 0; i < button_texts.size(); i++) { |
||||
QPushButton *button = new QPushButton(button_texts[i], this); |
||||
button->setCheckable(true); |
||||
button->setChecked(i == 0); |
||||
button->setStyleSheet(style); |
||||
button->setMinimumWidth(minimum_button_width); |
||||
hlayout->addWidget(button); |
||||
button_group->addButton(button, i); |
||||
} |
||||
|
||||
QObject::connect(button_group, QOverload<int>::of(&QButtonGroup::buttonClicked), this, &MultiButtonControl::buttonClicked); |
||||
} |
||||
|
||||
void setEnabled(bool enable) { |
||||
for (auto btn : button_group->buttons()) { |
||||
btn->setEnabled(enable); |
||||
} |
||||
} |
||||
|
||||
void setCheckedButton(int id) { |
||||
button_group->button(id)->setChecked(true); |
||||
} |
||||
|
||||
signals: |
||||
void buttonClicked(int id); |
||||
|
||||
protected: |
||||
QButtonGroup *button_group; |
||||
}; |
||||
|
||||
class ButtonParamControl : public MultiButtonControl { |
||||
Q_OBJECT |
||||
public: |
||||
ButtonParamControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, |
||||
const std::vector<QString> &button_texts, const int minimum_button_width = 225) : MultiButtonControl(title, desc, icon, |
||||
button_texts, minimum_button_width) { |
||||
key = param.toStdString(); |
||||
int value = atoi(params.get(key).c_str()); |
||||
|
||||
if (value > 0 && value < button_group->buttons().size()) { |
||||
button_group->button(value)->setChecked(true); |
||||
} |
||||
|
||||
QObject::connect(this, QOverload<int>::of(&MultiButtonControl::buttonClicked), [=](int id) { |
||||
params.put(key, std::to_string(id)); |
||||
}); |
||||
} |
||||
|
||||
void refresh() { |
||||
int value = atoi(params.get(key).c_str()); |
||||
button_group->button(value)->setChecked(true); |
||||
} |
||||
|
||||
void showEvent(QShowEvent *event) override { |
||||
refresh(); |
||||
} |
||||
|
||||
private: |
||||
std::string key; |
||||
Params params; |
||||
}; |
||||
|
||||
class ListWidget : public QWidget { |
||||
Q_OBJECT |
||||
public: |
||||
explicit ListWidget(QWidget *parent = 0) : QWidget(parent), outer_layout(this) { |
||||
outer_layout.setMargin(0); |
||||
outer_layout.setSpacing(0); |
||||
outer_layout.addLayout(&inner_layout); |
||||
inner_layout.setMargin(0); |
||||
inner_layout.setSpacing(25); // default spacing is 25
|
||||
outer_layout.addStretch(1); |
||||
} |
||||
inline void addItem(QWidget *w) { inner_layout.addWidget(w); } |
||||
inline void addItem(QLayout *layout) { inner_layout.addLayout(layout); } |
||||
inline void setSpacing(int spacing) { inner_layout.setSpacing(spacing); } |
||||
|
||||
private: |
||||
void paintEvent(QPaintEvent *) override { |
||||
QPainter p(this); |
||||
p.setPen(Qt::gray); |
||||
for (int i = 0; i < inner_layout.count() - 1; ++i) { |
||||
QWidget *widget = inner_layout.itemAt(i)->widget(); |
||||
if (widget == nullptr || widget->isVisible()) { |
||||
QRect r = inner_layout.itemAt(i)->geometry(); |
||||
int bottom = r.bottom() + inner_layout.spacing() / 2; |
||||
p.drawLine(r.left() + 40, bottom, r.right() - 40, bottom); |
||||
} |
||||
} |
||||
} |
||||
QVBoxLayout outer_layout; |
||||
QVBoxLayout inner_layout; |
||||
}; |
||||
|
||||
// convenience class for wrapping layouts
|
||||
class LayoutWidget : public QWidget { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
LayoutWidget(QLayout *l, QWidget *parent = nullptr) : QWidget(parent) { |
||||
setLayout(l); |
||||
} |
||||
}; |
||||
@ -1,336 +0,0 @@ |
||||
#include "selfdrive/ui/qt/widgets/input.h" |
||||
|
||||
#include <QPushButton> |
||||
#include <QButtonGroup> |
||||
|
||||
#include "system/hardware/hw.h" |
||||
#include "selfdrive/ui/qt/util.h" |
||||
#include "selfdrive/ui/qt/qt_window.h" |
||||
#include "selfdrive/ui/qt/widgets/scrollview.h" |
||||
|
||||
|
||||
DialogBase::DialogBase(QWidget *parent) : QDialog(parent) { |
||||
Q_ASSERT(parent != nullptr); |
||||
parent->installEventFilter(this); |
||||
|
||||
setStyleSheet(R"( |
||||
* { |
||||
outline: none; |
||||
color: white; |
||||
font-family: Inter; |
||||
} |
||||
DialogBase { |
||||
background-color: black; |
||||
} |
||||
QPushButton { |
||||
height: 160; |
||||
font-size: 55px; |
||||
font-weight: 400; |
||||
border-radius: 10px; |
||||
color: white; |
||||
background-color: #333333; |
||||
} |
||||
QPushButton:pressed { |
||||
background-color: #444444; |
||||
} |
||||
)"); |
||||
} |
||||
|
||||
bool DialogBase::eventFilter(QObject *o, QEvent *e) { |
||||
if (o == parent() && e->type() == QEvent::Hide) { |
||||
reject(); |
||||
} |
||||
return QDialog::eventFilter(o, e); |
||||
} |
||||
|
||||
int DialogBase::exec() { |
||||
setMainWindow(this); |
||||
return QDialog::exec(); |
||||
} |
||||
|
||||
InputDialog::InputDialog(const QString &title, QWidget *parent, const QString &subtitle, bool secret) : DialogBase(parent) { |
||||
main_layout = new QVBoxLayout(this); |
||||
main_layout->setContentsMargins(50, 55, 50, 50); |
||||
main_layout->setSpacing(0); |
||||
|
||||
// build header
|
||||
QHBoxLayout *header_layout = new QHBoxLayout(); |
||||
|
||||
QVBoxLayout *vlayout = new QVBoxLayout; |
||||
header_layout->addLayout(vlayout); |
||||
label = new QLabel(title, this); |
||||
label->setStyleSheet("font-size: 90px; font-weight: bold;"); |
||||
vlayout->addWidget(label, 1, Qt::AlignTop | Qt::AlignLeft); |
||||
|
||||
if (!subtitle.isEmpty()) { |
||||
sublabel = new QLabel(subtitle, this); |
||||
sublabel->setStyleSheet("font-size: 55px; font-weight: light; color: #BDBDBD;"); |
||||
vlayout->addWidget(sublabel, 1, Qt::AlignTop | Qt::AlignLeft); |
||||
} |
||||
|
||||
QPushButton* cancel_btn = new QPushButton(tr("Cancel")); |
||||
cancel_btn->setFixedSize(386, 125); |
||||
cancel_btn->setStyleSheet(R"( |
||||
QPushButton { |
||||
font-size: 48px; |
||||
border-radius: 10px; |
||||
color: #E4E4E4; |
||||
background-color: #333333; |
||||
} |
||||
QPushButton:pressed { |
||||
background-color: #444444; |
||||
} |
||||
)"); |
||||
header_layout->addWidget(cancel_btn, 0, Qt::AlignRight); |
||||
QObject::connect(cancel_btn, &QPushButton::clicked, this, &InputDialog::reject); |
||||
QObject::connect(cancel_btn, &QPushButton::clicked, this, &InputDialog::cancel); |
||||
|
||||
main_layout->addLayout(header_layout); |
||||
|
||||
// text box
|
||||
main_layout->addStretch(2); |
||||
|
||||
QWidget *textbox_widget = new QWidget; |
||||
textbox_widget->setObjectName("textbox"); |
||||
QHBoxLayout *textbox_layout = new QHBoxLayout(textbox_widget); |
||||
textbox_layout->setContentsMargins(50, 0, 50, 0); |
||||
|
||||
textbox_widget->setStyleSheet(R"( |
||||
#textbox { |
||||
margin-left: 50px; |
||||
margin-right: 50px; |
||||
border-radius: 0; |
||||
border-bottom: 3px solid #BDBDBD; |
||||
} |
||||
* { |
||||
border: none; |
||||
font-size: 80px; |
||||
font-weight: light; |
||||
background-color: transparent; |
||||
} |
||||
)"); |
||||
|
||||
line = new QLineEdit(); |
||||
line->setStyleSheet("lineedit-password-character: 8226; lineedit-password-mask-delay: 1500;"); |
||||
textbox_layout->addWidget(line, 1); |
||||
|
||||
if (secret) { |
||||
eye_btn = new QPushButton(); |
||||
eye_btn->setCheckable(true); |
||||
eye_btn->setFixedSize(150, 120); |
||||
QObject::connect(eye_btn, &QPushButton::toggled, [=](bool checked) { |
||||
if (checked) { |
||||
eye_btn->setIcon(QIcon(ASSET_PATH + "icons/eye_closed.svg")); |
||||
eye_btn->setIconSize(QSize(81, 54)); |
||||
line->setEchoMode(QLineEdit::Password); |
||||
} else { |
||||
eye_btn->setIcon(QIcon(ASSET_PATH + "icons/eye_open.svg")); |
||||
eye_btn->setIconSize(QSize(81, 44)); |
||||
line->setEchoMode(QLineEdit::Normal); |
||||
} |
||||
}); |
||||
eye_btn->toggle(); |
||||
eye_btn->setChecked(false); |
||||
textbox_layout->addWidget(eye_btn); |
||||
} |
||||
|
||||
main_layout->addWidget(textbox_widget, 0, Qt::AlignBottom); |
||||
main_layout->addSpacing(25); |
||||
|
||||
k = new Keyboard(this); |
||||
QObject::connect(k, &Keyboard::emitEnter, this, &InputDialog::handleEnter); |
||||
QObject::connect(k, &Keyboard::emitBackspace, this, [=]() { |
||||
line->backspace(); |
||||
}); |
||||
QObject::connect(k, &Keyboard::emitKey, this, [=](const QString &key) { |
||||
line->insert(key.left(1)); |
||||
}); |
||||
|
||||
main_layout->addWidget(k, 2, Qt::AlignBottom); |
||||
} |
||||
|
||||
QString InputDialog::getText(const QString &prompt, QWidget *parent, const QString &subtitle, |
||||
bool secret, int minLength, const QString &defaultText) { |
||||
InputDialog d(prompt, parent, subtitle, secret); |
||||
d.line->setText(defaultText); |
||||
d.setMinLength(minLength); |
||||
const int ret = d.exec(); |
||||
return ret ? d.text() : QString(); |
||||
} |
||||
|
||||
QString InputDialog::text() { |
||||
return line->text(); |
||||
} |
||||
|
||||
void InputDialog::show() { |
||||
setMainWindow(this); |
||||
} |
||||
|
||||
void InputDialog::handleEnter() { |
||||
if (line->text().length() >= minLength) { |
||||
done(QDialog::Accepted); |
||||
emitText(line->text()); |
||||
} else { |
||||
setMessage(tr("Need at least %n character(s)!", "", minLength), false); |
||||
} |
||||
} |
||||
|
||||
void InputDialog::setMessage(const QString &message, bool clearInputField) { |
||||
label->setText(message); |
||||
if (clearInputField) { |
||||
line->setText(""); |
||||
} |
||||
} |
||||
|
||||
void InputDialog::setMinLength(int length) { |
||||
minLength = length; |
||||
} |
||||
|
||||
// ConfirmationDialog
|
||||
|
||||
ConfirmationDialog::ConfirmationDialog(const QString &prompt_text, const QString &confirm_text, const QString &cancel_text, |
||||
const bool rich, QWidget *parent) : DialogBase(parent) { |
||||
QFrame *container = new QFrame(this); |
||||
container->setStyleSheet(R"( |
||||
QFrame { background-color: #1B1B1B; color: #C9C9C9; } |
||||
#confirm_btn { background-color: #465BEA; } |
||||
#confirm_btn:pressed { background-color: #3049F4; } |
||||
)"); |
||||
QVBoxLayout *main_layout = new QVBoxLayout(container); |
||||
main_layout->setContentsMargins(32, rich ? 32 : 120, 32, 32); |
||||
|
||||
QLabel *prompt = new QLabel(prompt_text, this); |
||||
prompt->setWordWrap(true); |
||||
prompt->setAlignment(rich ? Qt::AlignLeft : Qt::AlignHCenter); |
||||
prompt->setStyleSheet((rich ? "font-size: 42px; font-weight: light;" : "font-size: 70px; font-weight: bold;") + QString(" margin: 45px;")); |
||||
main_layout->addWidget(rich ? (QWidget*)new ScrollView(prompt, this) : (QWidget*)prompt, 1, Qt::AlignTop); |
||||
|
||||
// cancel + confirm buttons
|
||||
QHBoxLayout *btn_layout = new QHBoxLayout(); |
||||
btn_layout->setSpacing(30); |
||||
main_layout->addLayout(btn_layout); |
||||
|
||||
if (cancel_text.length()) { |
||||
QPushButton* cancel_btn = new QPushButton(cancel_text); |
||||
btn_layout->addWidget(cancel_btn); |
||||
QObject::connect(cancel_btn, &QPushButton::clicked, this, &ConfirmationDialog::reject); |
||||
} |
||||
|
||||
if (confirm_text.length()) { |
||||
QPushButton* confirm_btn = new QPushButton(confirm_text); |
||||
confirm_btn->setObjectName("confirm_btn"); |
||||
btn_layout->addWidget(confirm_btn); |
||||
QObject::connect(confirm_btn, &QPushButton::clicked, this, &ConfirmationDialog::accept); |
||||
} |
||||
|
||||
QVBoxLayout *outer_layout = new QVBoxLayout(this); |
||||
int margin = rich ? 100 : 200; |
||||
outer_layout->setContentsMargins(margin, margin, margin, margin); |
||||
outer_layout->addWidget(container); |
||||
} |
||||
|
||||
bool ConfirmationDialog::alert(const QString &prompt_text, QWidget *parent) { |
||||
ConfirmationDialog d(prompt_text, tr("Ok"), "", false, parent); |
||||
return d.exec(); |
||||
} |
||||
|
||||
bool ConfirmationDialog::confirm(const QString &prompt_text, const QString &confirm_text, QWidget *parent) { |
||||
ConfirmationDialog d(prompt_text, confirm_text, tr("Cancel"), false, parent); |
||||
return d.exec(); |
||||
} |
||||
|
||||
bool ConfirmationDialog::rich(const QString &prompt_text, QWidget *parent) { |
||||
ConfirmationDialog d(prompt_text, tr("Ok"), "", true, parent); |
||||
return d.exec(); |
||||
} |
||||
|
||||
// MultiOptionDialog
|
||||
|
||||
MultiOptionDialog::MultiOptionDialog(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent) : DialogBase(parent) { |
||||
QFrame *container = new QFrame(this); |
||||
container->setStyleSheet(R"( |
||||
QFrame { background-color: #1B1B1B; } |
||||
#confirm_btn[enabled="false"] { background-color: #2B2B2B; } |
||||
#confirm_btn:enabled { background-color: #465BEA; } |
||||
#confirm_btn:enabled:pressed { background-color: #3049F4; } |
||||
)"); |
||||
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(container); |
||||
main_layout->setContentsMargins(55, 50, 55, 50); |
||||
|
||||
QLabel *title = new QLabel(prompt_text, this); |
||||
title->setStyleSheet("font-size: 70px; font-weight: 500;"); |
||||
main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop); |
||||
main_layout->addSpacing(25); |
||||
|
||||
QWidget *listWidget = new QWidget(this); |
||||
QVBoxLayout *listLayout = new QVBoxLayout(listWidget); |
||||
listLayout->setSpacing(20); |
||||
listWidget->setStyleSheet(R"( |
||||
QPushButton { |
||||
height: 135; |
||||
padding: 0px 50px; |
||||
text-align: left; |
||||
font-size: 55px; |
||||
font-weight: 300; |
||||
border-radius: 10px; |
||||
background-color: #4F4F4F; |
||||
} |
||||
QPushButton:checked { background-color: #465BEA; } |
||||
)"); |
||||
|
||||
QButtonGroup *group = new QButtonGroup(listWidget); |
||||
group->setExclusive(true); |
||||
|
||||
QPushButton *confirm_btn = new QPushButton(tr("Select")); |
||||
confirm_btn->setObjectName("confirm_btn"); |
||||
confirm_btn->setEnabled(false); |
||||
|
||||
for (const QString &s : l) { |
||||
QPushButton *selectionLabel = new QPushButton(s); |
||||
selectionLabel->setCheckable(true); |
||||
selectionLabel->setChecked(s == current); |
||||
QObject::connect(selectionLabel, &QPushButton::toggled, [=](bool checked) { |
||||
if (checked) selection = s; |
||||
if (selection != current) { |
||||
confirm_btn->setEnabled(true); |
||||
} else { |
||||
confirm_btn->setEnabled(false); |
||||
} |
||||
}); |
||||
|
||||
group->addButton(selectionLabel); |
||||
listLayout->addWidget(selectionLabel); |
||||
} |
||||
// add stretch to keep buttons spaced correctly
|
||||
listLayout->addStretch(1); |
||||
|
||||
ScrollView *scroll_view = new ScrollView(listWidget, this); |
||||
scroll_view->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); |
||||
|
||||
main_layout->addWidget(scroll_view); |
||||
main_layout->addSpacing(35); |
||||
|
||||
// cancel + confirm buttons
|
||||
QHBoxLayout *blayout = new QHBoxLayout; |
||||
main_layout->addLayout(blayout); |
||||
blayout->setSpacing(50); |
||||
|
||||
QPushButton *cancel_btn = new QPushButton(tr("Cancel")); |
||||
QObject::connect(cancel_btn, &QPushButton::clicked, this, &ConfirmationDialog::reject); |
||||
QObject::connect(confirm_btn, &QPushButton::clicked, this, &ConfirmationDialog::accept); |
||||
blayout->addWidget(cancel_btn); |
||||
blayout->addWidget(confirm_btn); |
||||
|
||||
QVBoxLayout *outer_layout = new QVBoxLayout(this); |
||||
outer_layout->setContentsMargins(50, 50, 50, 50); |
||||
outer_layout->addWidget(container); |
||||
} |
||||
|
||||
QString MultiOptionDialog::getSelection(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent) { |
||||
MultiOptionDialog d(prompt_text, l, current, parent); |
||||
if (d.exec()) { |
||||
return d.selection; |
||||
} |
||||
return ""; |
||||
} |
||||
@ -1,71 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <QDialog> |
||||
#include <QLabel> |
||||
#include <QLineEdit> |
||||
#include <QString> |
||||
#include <QVBoxLayout> |
||||
#include <QWidget> |
||||
|
||||
#include "selfdrive/ui/qt/widgets/keyboard.h" |
||||
|
||||
|
||||
class DialogBase : public QDialog { |
||||
Q_OBJECT |
||||
|
||||
protected: |
||||
DialogBase(QWidget *parent); |
||||
bool eventFilter(QObject *o, QEvent *e) override; |
||||
|
||||
public slots: |
||||
int exec() override; |
||||
}; |
||||
|
||||
class InputDialog : public DialogBase { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit InputDialog(const QString &title, QWidget *parent, const QString &subtitle = "", bool secret = false); |
||||
static QString getText(const QString &title, QWidget *parent, const QString &subtitle = "", |
||||
bool secret = false, int minLength = -1, const QString &defaultText = ""); |
||||
QString text(); |
||||
void setMessage(const QString &message, bool clearInputField = true); |
||||
void setMinLength(int length); |
||||
void show(); |
||||
|
||||
private: |
||||
int minLength; |
||||
QLineEdit *line; |
||||
Keyboard *k; |
||||
QLabel *label; |
||||
QLabel *sublabel; |
||||
QVBoxLayout *main_layout; |
||||
QPushButton *eye_btn; |
||||
|
||||
private slots: |
||||
void handleEnter(); |
||||
|
||||
signals: |
||||
void cancel(); |
||||
void emitText(const QString &text); |
||||
}; |
||||
|
||||
class ConfirmationDialog : public DialogBase { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit ConfirmationDialog(const QString &prompt_text, const QString &confirm_text, |
||||
const QString &cancel_text, const bool rich, QWidget* parent); |
||||
static bool alert(const QString &prompt_text, QWidget *parent); |
||||
static bool confirm(const QString &prompt_text, const QString &confirm_text, QWidget *parent); |
||||
static bool rich(const QString &prompt_text, QWidget *parent); |
||||
}; |
||||
|
||||
class MultiOptionDialog : public DialogBase { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit MultiOptionDialog(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent); |
||||
static QString getSelection(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent); |
||||
QString selection; |
||||
}; |
||||
@ -1,182 +0,0 @@ |
||||
#include "selfdrive/ui/qt/widgets/keyboard.h" |
||||
|
||||
#include <vector> |
||||
|
||||
#include <QButtonGroup> |
||||
#include <QHBoxLayout> |
||||
#include <QMap> |
||||
#include <QTouchEvent> |
||||
#include <QVBoxLayout> |
||||
|
||||
const QString BACKSPACE_KEY = "⌫"; |
||||
const QString ENTER_KEY = "→"; |
||||
const QString SHIFT_KEY = "⇧"; |
||||
const QString CAPS_LOCK_KEY = "⇪"; |
||||
|
||||
const QMap<QString, int> KEY_STRETCH = {{" ", 3}, {ENTER_KEY, 2}}; |
||||
|
||||
const QStringList CONTROL_BUTTONS = {SHIFT_KEY, CAPS_LOCK_KEY, "ABC", "#+=", "123", BACKSPACE_KEY, ENTER_KEY}; |
||||
|
||||
const float key_spacing_vertical = 20; |
||||
const float key_spacing_horizontal = 15; |
||||
|
||||
KeyButton::KeyButton(const QString &text, QWidget *parent) : QPushButton(text, parent) { |
||||
setAttribute(Qt::WA_AcceptTouchEvents); |
||||
setFocusPolicy(Qt::NoFocus); |
||||
} |
||||
|
||||
bool KeyButton::event(QEvent *event) { |
||||
if (event->type() == QEvent::TouchBegin || event->type() == QEvent::TouchEnd) { |
||||
QTouchEvent *touchEvent = static_cast<QTouchEvent *>(event); |
||||
if (!touchEvent->touchPoints().empty()) { |
||||
const QEvent::Type mouseType = event->type() == QEvent::TouchBegin ? QEvent::MouseButtonPress : QEvent::MouseButtonRelease; |
||||
QMouseEvent mouseEvent(mouseType, touchEvent->touchPoints().front().pos(), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); |
||||
QPushButton::event(&mouseEvent); |
||||
event->accept(); |
||||
parentWidget()->update(); |
||||
return true; |
||||
} |
||||
} |
||||
return QPushButton::event(event); |
||||
} |
||||
|
||||
KeyboardLayout::KeyboardLayout(QWidget* parent, const std::vector<QVector<QString>>& layout) : QWidget(parent) { |
||||
QVBoxLayout* main_layout = new QVBoxLayout(this); |
||||
main_layout->setMargin(0); |
||||
main_layout->setSpacing(0); |
||||
|
||||
QButtonGroup* btn_group = new QButtonGroup(this); |
||||
QObject::connect(btn_group, SIGNAL(buttonClicked(QAbstractButton*)), parent, SLOT(handleButton(QAbstractButton*))); |
||||
|
||||
for (const auto &s : layout) { |
||||
QHBoxLayout *hlayout = new QHBoxLayout; |
||||
hlayout->setSpacing(0); |
||||
|
||||
if (main_layout->count() == 1) { |
||||
hlayout->addSpacing(90); |
||||
} |
||||
|
||||
for (const QString &p : s) { |
||||
KeyButton* btn = new KeyButton(p); |
||||
if (p == BACKSPACE_KEY) { |
||||
btn->setAutoRepeat(true); |
||||
} else if (p == ENTER_KEY) { |
||||
btn->setStyleSheet(R"( |
||||
QPushButton { |
||||
background-color: #465BEA; |
||||
} |
||||
QPushButton:pressed { |
||||
background-color: #444444; |
||||
} |
||||
)"); |
||||
} |
||||
btn->setFixedHeight(135 + key_spacing_vertical); |
||||
btn_group->addButton(btn); |
||||
hlayout->addWidget(btn, KEY_STRETCH.value(p, 1)); |
||||
} |
||||
|
||||
if (main_layout->count() == 1) { |
||||
hlayout->addSpacing(90); |
||||
} |
||||
|
||||
main_layout->addLayout(hlayout); |
||||
} |
||||
|
||||
setStyleSheet(QString(R"( |
||||
QPushButton { |
||||
font-size: 75px; |
||||
margin-left: %1px; |
||||
margin-right: %1px; |
||||
margin-top: %2px; |
||||
margin-bottom: %2px; |
||||
padding: 0px; |
||||
border-radius: 10px; |
||||
color: #dddddd; |
||||
background-color: #444444; |
||||
} |
||||
QPushButton:pressed { |
||||
background-color: #333333; |
||||
} |
||||
)").arg(key_spacing_vertical / 2).arg(key_spacing_horizontal / 2)); |
||||
} |
||||
|
||||
Keyboard::Keyboard(QWidget *parent) : QFrame(parent) { |
||||
main_layout = new QStackedLayout(this); |
||||
main_layout->setMargin(0); |
||||
|
||||
// lowercase
|
||||
std::vector<QVector<QString>> lowercase = { |
||||
{"q", "w", "e", "r", "t", "y", "u", "i", "o", "p"}, |
||||
{"a", "s", "d", "f", "g", "h", "j", "k", "l"}, |
||||
{SHIFT_KEY, "z", "x", "c", "v", "b", "n", "m", BACKSPACE_KEY}, |
||||
{"123", "/", "-", " ", ".", ENTER_KEY}, |
||||
}; |
||||
main_layout->addWidget(new KeyboardLayout(this, lowercase)); |
||||
|
||||
// uppercase
|
||||
std::vector<QVector<QString>> uppercase = { |
||||
{"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"}, |
||||
{"A", "S", "D", "F", "G", "H", "J", "K", "L"}, |
||||
{SHIFT_KEY, "Z", "X", "C", "V", "B", "N", "M", BACKSPACE_KEY}, |
||||
{"123", "/", "-", " ", ".", ENTER_KEY}, |
||||
}; |
||||
main_layout->addWidget(new KeyboardLayout(this, uppercase)); |
||||
|
||||
// numbers + specials
|
||||
std::vector<QVector<QString>> numbers = { |
||||
{"1", "2", "3", "4", "5", "6", "7", "8", "9", "0"}, |
||||
{"-", "/", ":", ";", "(", ")", "$", "&&", "@", "\""}, |
||||
{"#+=", ".", ",", "?", "!", "`", BACKSPACE_KEY}, |
||||
{"ABC", " ", ".", ENTER_KEY}, |
||||
}; |
||||
main_layout->addWidget(new KeyboardLayout(this, numbers)); |
||||
|
||||
// extra specials
|
||||
std::vector<QVector<QString>> specials = { |
||||
{"[", "]", "{", "}", "#", "%", "^", "*", "+", "="}, |
||||
{"_", "\\", "|", "~", "<", ">", "€", "£", "¥", "•"}, |
||||
{"123", ".", ",", "?", "!", "'", BACKSPACE_KEY}, |
||||
{"ABC", " ", ".", ENTER_KEY}, |
||||
}; |
||||
main_layout->addWidget(new KeyboardLayout(this, specials)); |
||||
|
||||
main_layout->setCurrentIndex(0); |
||||
} |
||||
|
||||
void Keyboard::handleCapsPress() { |
||||
shift_state = (shift_state + 1) % 3; |
||||
bool is_uppercase = shift_state > 0; |
||||
main_layout->setCurrentIndex(is_uppercase); |
||||
|
||||
for (KeyButton* btn : main_layout->currentWidget()->findChildren<KeyButton*>()) { |
||||
if (btn->text() == SHIFT_KEY || btn->text() == CAPS_LOCK_KEY) { |
||||
btn->setText(shift_state == 2 ? CAPS_LOCK_KEY : SHIFT_KEY); |
||||
btn->setStyleSheet(is_uppercase ? "background-color: #465BEA;" : ""); |
||||
} |
||||
} |
||||
} |
||||
|
||||
void Keyboard::handleButton(QAbstractButton* btn) { |
||||
const QString &key = btn->text(); |
||||
if (CONTROL_BUTTONS.contains(key)) { |
||||
if (key == "ABC" || key == "123" || key == "#+=") { |
||||
int index = (key == "ABC") ? 0 : (key == "123" ? 2 : 3); |
||||
main_layout->setCurrentIndex(index); |
||||
shift_state = 0; |
||||
} else if (key == SHIFT_KEY || key == CAPS_LOCK_KEY) { |
||||
handleCapsPress(); |
||||
} else if (key == ENTER_KEY) { |
||||
main_layout->setCurrentIndex(0); |
||||
shift_state = 0; |
||||
emit emitEnter(); |
||||
} else if (key == BACKSPACE_KEY) { |
||||
emit emitBackspace(); |
||||
} |
||||
} else { |
||||
if (shift_state == 1 && "A" <= key && key <= "Z") { |
||||
main_layout->setCurrentIndex(0); |
||||
shift_state = 0; |
||||
} |
||||
emit emitKey(key); |
||||
} |
||||
} |
||||
@ -1,42 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <vector> |
||||
|
||||
#include <QFrame> |
||||
#include <QPushButton> |
||||
#include <QStackedLayout> |
||||
|
||||
class KeyButton : public QPushButton { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
KeyButton(const QString &text, QWidget *parent = 0); |
||||
bool event(QEvent *event) override; |
||||
}; |
||||
|
||||
class KeyboardLayout : public QWidget { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit KeyboardLayout(QWidget* parent, const std::vector<QVector<QString>>& layout); |
||||
}; |
||||
|
||||
class Keyboard : public QFrame { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit Keyboard(QWidget *parent = 0); |
||||
|
||||
private: |
||||
QStackedLayout* main_layout; |
||||
int shift_state = 0; |
||||
|
||||
private slots: |
||||
void handleButton(QAbstractButton* m_button); |
||||
void handleCapsPress(); |
||||
|
||||
signals: |
||||
void emitKey(const QString &s); |
||||
void emitBackspace(); |
||||
void emitEnter(); |
||||
}; |
||||
@ -1,138 +0,0 @@ |
||||
#include "selfdrive/ui/qt/widgets/offroad_alerts.h" |
||||
|
||||
#include <algorithm> |
||||
#include <string> |
||||
#include <vector> |
||||
#include <utility> |
||||
|
||||
#include <QHBoxLayout> |
||||
#include <QJsonDocument> |
||||
#include <QJsonObject> |
||||
|
||||
#include "common/util.h" |
||||
#include "system/hardware/hw.h" |
||||
#include "selfdrive/ui/qt/widgets/scrollview.h" |
||||
|
||||
AbstractAlert::AbstractAlert(bool hasRebootBtn, QWidget *parent) : QFrame(parent) { |
||||
QVBoxLayout *main_layout = new QVBoxLayout(this); |
||||
main_layout->setMargin(50); |
||||
main_layout->setSpacing(30); |
||||
|
||||
QWidget *widget = new QWidget; |
||||
scrollable_layout = new QVBoxLayout(widget); |
||||
widget->setStyleSheet("background-color: transparent;"); |
||||
main_layout->addWidget(new ScrollView(widget)); |
||||
|
||||
// bottom footer, dismiss + reboot buttons
|
||||
QHBoxLayout *footer_layout = new QHBoxLayout(); |
||||
main_layout->addLayout(footer_layout); |
||||
|
||||
QPushButton *dismiss_btn = new QPushButton(tr("Close")); |
||||
dismiss_btn->setFixedSize(400, 125); |
||||
footer_layout->addWidget(dismiss_btn, 0, Qt::AlignBottom | Qt::AlignLeft); |
||||
QObject::connect(dismiss_btn, &QPushButton::clicked, this, &AbstractAlert::dismiss); |
||||
|
||||
action_btn = new QPushButton(); |
||||
action_btn->setVisible(false); |
||||
action_btn->setFixedHeight(125); |
||||
footer_layout->addWidget(action_btn, 0, Qt::AlignBottom | Qt::AlignRight); |
||||
QObject::connect(action_btn, &QPushButton::clicked, [=]() { |
||||
if (!alerts["Offroad_ExcessiveActuation"]->text().isEmpty()) { |
||||
params.remove("Offroad_ExcessiveActuation"); |
||||
} else { |
||||
params.putBool("SnoozeUpdate", true); |
||||
} |
||||
}); |
||||
QObject::connect(action_btn, &QPushButton::clicked, this, &AbstractAlert::dismiss); |
||||
action_btn->setStyleSheet("color: white; background-color: #4F4F4F; padding-left: 60px; padding-right: 60px;"); |
||||
|
||||
if (hasRebootBtn) { |
||||
QPushButton *rebootBtn = new QPushButton(tr("Reboot and Update")); |
||||
rebootBtn->setFixedSize(600, 125); |
||||
footer_layout->addWidget(rebootBtn, 0, Qt::AlignBottom | Qt::AlignRight); |
||||
QObject::connect(rebootBtn, &QPushButton::clicked, [=]() { Hardware::reboot(); }); |
||||
} |
||||
|
||||
setStyleSheet(R"( |
||||
* { |
||||
font-size: 48px; |
||||
color: white; |
||||
} |
||||
QFrame { |
||||
border-radius: 30px; |
||||
background-color: #393939; |
||||
} |
||||
QPushButton { |
||||
color: black; |
||||
font-weight: 500; |
||||
border-radius: 30px; |
||||
background-color: white; |
||||
} |
||||
)"); |
||||
} |
||||
|
||||
int OffroadAlert::refresh() { |
||||
// build widgets for each offroad alert on first refresh
|
||||
if (alerts.empty()) { |
||||
QString json = util::read_file("../selfdrived/alerts_offroad.json").c_str(); |
||||
QJsonObject obj = QJsonDocument::fromJson(json.toUtf8()).object(); |
||||
|
||||
// descending sort labels by severity
|
||||
std::vector<std::pair<std::string, int>> sorted; |
||||
for (auto it = obj.constBegin(); it != obj.constEnd(); ++it) { |
||||
sorted.push_back({it.key().toStdString(), it.value()["severity"].toInt()}); |
||||
} |
||||
std::sort(sorted.begin(), sorted.end(), [=](auto &l, auto &r) { return l.second > r.second; }); |
||||
|
||||
for (auto &[key, severity] : sorted) { |
||||
QLabel *l = new QLabel(this); |
||||
alerts[key] = l; |
||||
l->setMargin(60); |
||||
l->setWordWrap(true); |
||||
l->setStyleSheet(QString("background-color: %1").arg(severity ? "#E22C2C" : "#292929")); |
||||
scrollable_layout->addWidget(l); |
||||
} |
||||
scrollable_layout->addStretch(1); |
||||
} |
||||
|
||||
int alertCount = 0; |
||||
for (const auto &[key, label] : alerts) { |
||||
QString text; |
||||
std::string bytes = params.get(key); |
||||
if (bytes.size()) { |
||||
auto doc_par = QJsonDocument::fromJson(bytes.c_str()); |
||||
text = tr(doc_par["text"].toString().toUtf8().data()); |
||||
auto extra = doc_par["extra"].toString(); |
||||
if (!extra.isEmpty()) { |
||||
text = text.arg(extra); |
||||
} |
||||
} |
||||
label->setText(text); |
||||
label->setVisible(!text.isEmpty()); |
||||
alertCount += !text.isEmpty(); |
||||
} |
||||
|
||||
action_btn->setVisible(!alerts["Offroad_ExcessiveActuation"]->text().isEmpty() || !alerts["Offroad_ConnectivityNeeded"]->text().isEmpty()); |
||||
if (!alerts["Offroad_ExcessiveActuation"]->text().isEmpty()) { |
||||
action_btn->setText(tr("Acknowledge Excessive Actuation")); |
||||
} else { |
||||
action_btn->setText(tr("Snooze Update")); |
||||
} |
||||
|
||||
return alertCount; |
||||
} |
||||
|
||||
UpdateAlert::UpdateAlert(QWidget *parent) : AbstractAlert(true, parent) { |
||||
releaseNotes = new QLabel(this); |
||||
releaseNotes->setWordWrap(true); |
||||
releaseNotes->setAlignment(Qt::AlignTop); |
||||
scrollable_layout->addWidget(releaseNotes); |
||||
} |
||||
|
||||
bool UpdateAlert::refresh() { |
||||
bool updateAvailable = params.getBool("UpdateAvailable"); |
||||
if (updateAvailable) { |
||||
releaseNotes->setText(params.get("UpdaterNewReleaseNotes").c_str()); |
||||
} |
||||
return updateAvailable; |
||||
} |
||||
@ -1,44 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <map> |
||||
#include <string> |
||||
|
||||
#include <QLabel> |
||||
#include <QPushButton> |
||||
#include <QVBoxLayout> |
||||
|
||||
#include "common/params.h" |
||||
|
||||
class AbstractAlert : public QFrame { |
||||
Q_OBJECT |
||||
|
||||
protected: |
||||
AbstractAlert(bool hasRebootBtn, QWidget *parent = nullptr); |
||||
|
||||
QPushButton *action_btn; |
||||
QVBoxLayout *scrollable_layout; |
||||
Params params; |
||||
std::map<std::string, QLabel*> alerts; |
||||
|
||||
signals: |
||||
void dismiss(); |
||||
}; |
||||
|
||||
class UpdateAlert : public AbstractAlert { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
UpdateAlert(QWidget *parent = 0); |
||||
bool refresh(); |
||||
|
||||
private: |
||||
QLabel *releaseNotes = nullptr; |
||||
}; |
||||
|
||||
class OffroadAlert : public AbstractAlert { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit OffroadAlert(QWidget *parent = 0) : AbstractAlert(false, parent) {} |
||||
int refresh(); |
||||
}; |
||||
@ -1,265 +0,0 @@ |
||||
#include "selfdrive/ui/qt/widgets/prime.h" |
||||
|
||||
#include <QDebug> |
||||
#include <QJsonDocument> |
||||
#include <QJsonObject> |
||||
#include <QLabel> |
||||
#include <QPushButton> |
||||
#include <QStackedWidget> |
||||
#include <QTimer> |
||||
#include <QVBoxLayout> |
||||
|
||||
#include <QrCode.hpp> |
||||
|
||||
#include "selfdrive/ui/qt/request_repeater.h" |
||||
#include "selfdrive/ui/qt/util.h" |
||||
#include "selfdrive/ui/qt/qt_window.h" |
||||
#include "selfdrive/ui/qt/widgets/wifi.h" |
||||
|
||||
using qrcodegen::QrCode; |
||||
|
||||
PairingQRWidget::PairingQRWidget(QWidget* parent) : QWidget(parent) { |
||||
timer = new QTimer(this); |
||||
connect(timer, &QTimer::timeout, this, &PairingQRWidget::refresh); |
||||
} |
||||
|
||||
void PairingQRWidget::showEvent(QShowEvent *event) { |
||||
refresh(); |
||||
timer->start(5 * 60 * 1000); |
||||
device()->setOffroadBrightness(100); |
||||
} |
||||
|
||||
void PairingQRWidget::hideEvent(QHideEvent *event) { |
||||
timer->stop(); |
||||
device()->setOffroadBrightness(BACKLIGHT_OFFROAD); |
||||
} |
||||
|
||||
void PairingQRWidget::refresh() { |
||||
QString pairToken = CommaApi::create_jwt({{"pair", true}}); |
||||
QString qrString = "https://connect.comma.ai/?pair=" + pairToken; |
||||
this->updateQrCode(qrString); |
||||
update(); |
||||
} |
||||
|
||||
void PairingQRWidget::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); |
||||
} |
||||
} |
||||
|
||||
// Integer division to prevent anti-aliasing
|
||||
int final_sz = ((width() / sz) - 1) * sz; |
||||
img = QPixmap::fromImage(im.scaled(final_sz, final_sz, Qt::KeepAspectRatio), Qt::MonoOnly); |
||||
} |
||||
|
||||
void PairingQRWidget::paintEvent(QPaintEvent *e) { |
||||
QPainter p(this); |
||||
p.fillRect(rect(), Qt::white); |
||||
|
||||
QSize s = (size() - img.size()) / 2; |
||||
p.drawPixmap(s.width(), s.height(), img); |
||||
} |
||||
|
||||
|
||||
PairingPopup::PairingPopup(QWidget *parent) : DialogBase(parent) { |
||||
QHBoxLayout *hlayout = new QHBoxLayout(this); |
||||
hlayout->setContentsMargins(0, 0, 0, 0); |
||||
hlayout->setSpacing(0); |
||||
|
||||
setStyleSheet("PairingPopup { background-color: #E0E0E0; }"); |
||||
|
||||
// text
|
||||
QVBoxLayout *vlayout = new QVBoxLayout(); |
||||
vlayout->setContentsMargins(85, 70, 50, 70); |
||||
vlayout->setSpacing(50); |
||||
hlayout->addLayout(vlayout, 1); |
||||
{ |
||||
QPushButton *close = new QPushButton(QIcon(":/icons/close.svg"), "", this); |
||||
close->setIconSize(QSize(80, 80)); |
||||
close->setStyleSheet("border: none;"); |
||||
vlayout->addWidget(close, 0, Qt::AlignLeft); |
||||
QObject::connect(close, &QPushButton::clicked, this, &QDialog::reject); |
||||
|
||||
vlayout->addSpacing(30); |
||||
|
||||
QLabel *title = new QLabel(tr("Pair your device to your comma account"), this); |
||||
title->setStyleSheet("font-size: 75px; color: black;"); |
||||
title->setWordWrap(true); |
||||
vlayout->addWidget(title); |
||||
|
||||
QLabel *instructions = new QLabel(QString(R"( |
||||
<ol type='1' style='margin-left: 15px;'> |
||||
<li style='margin-bottom: 50px;'>%1</li> |
||||
<li style='margin-bottom: 50px;'>%2</li> |
||||
<li style='margin-bottom: 50px;'>%3</li> |
||||
</ol> |
||||
)").arg(tr("Go to https://connect.comma.ai on your phone"))
|
||||
.arg(tr("Click \"add new device\" and scan the QR code on the right")) |
||||
.arg(tr("Bookmark connect.comma.ai to your home screen to use it like an app")), this); |
||||
|
||||
instructions->setStyleSheet("font-size: 47px; font-weight: bold; color: black;"); |
||||
instructions->setWordWrap(true); |
||||
vlayout->addWidget(instructions); |
||||
|
||||
vlayout->addStretch(); |
||||
} |
||||
|
||||
// QR code
|
||||
PairingQRWidget *qr = new PairingQRWidget(this); |
||||
hlayout->addWidget(qr, 1); |
||||
} |
||||
|
||||
int PairingPopup::exec() { |
||||
if (!util::system_time_valid()) { |
||||
ConfirmationDialog::alert(tr("Please connect to Wi-Fi to complete initial pairing"), parentWidget()); |
||||
return QDialog::Rejected; |
||||
} |
||||
return DialogBase::exec(); |
||||
} |
||||
|
||||
|
||||
PrimeUserWidget::PrimeUserWidget(QWidget *parent) : QFrame(parent) { |
||||
setObjectName("primeWidget"); |
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this); |
||||
mainLayout->setContentsMargins(56, 40, 56, 40); |
||||
mainLayout->setSpacing(20); |
||||
|
||||
QLabel *subscribed = new QLabel(tr("✓ SUBSCRIBED")); |
||||
subscribed->setStyleSheet("font-size: 41px; font-weight: bold; color: #86FF4E;"); |
||||
mainLayout->addWidget(subscribed); |
||||
|
||||
QLabel *commaPrime = new QLabel(tr("comma prime")); |
||||
commaPrime->setStyleSheet("font-size: 75px; font-weight: bold;"); |
||||
mainLayout->addWidget(commaPrime); |
||||
} |
||||
|
||||
|
||||
PrimeAdWidget::PrimeAdWidget(QWidget* parent) : QFrame(parent) { |
||||
QVBoxLayout *main_layout = new QVBoxLayout(this); |
||||
main_layout->setContentsMargins(80, 90, 80, 60); |
||||
main_layout->setSpacing(0); |
||||
|
||||
QLabel *upgrade = new QLabel(tr("Upgrade Now")); |
||||
upgrade->setStyleSheet("font-size: 75px; font-weight: bold;"); |
||||
main_layout->addWidget(upgrade, 0, Qt::AlignTop); |
||||
main_layout->addSpacing(50); |
||||
|
||||
QLabel *description = new QLabel(tr("Become a comma prime member at connect.comma.ai")); |
||||
description->setStyleSheet("font-size: 56px; font-weight: light; color: white;"); |
||||
description->setWordWrap(true); |
||||
main_layout->addWidget(description, 0, Qt::AlignTop); |
||||
|
||||
main_layout->addStretch(); |
||||
|
||||
QLabel *features = new QLabel(tr("PRIME FEATURES:")); |
||||
features->setStyleSheet("font-size: 41px; font-weight: bold; color: #E5E5E5;"); |
||||
main_layout->addWidget(features, 0, Qt::AlignBottom); |
||||
main_layout->addSpacing(30); |
||||
|
||||
QVector<QString> bullets = {tr("Remote access"), tr("24/7 LTE connectivity"), tr("1 year of drive storage"), tr("Remote snapshots")}; |
||||
for (auto &b : bullets) { |
||||
const QString check = "<b><font color='#465BEA'>✓</font></b> "; |
||||
QLabel *l = new QLabel(check + b); |
||||
l->setAlignment(Qt::AlignLeft); |
||||
l->setStyleSheet("font-size: 50px; margin-bottom: 15px;"); |
||||
main_layout->addWidget(l, 0, Qt::AlignBottom); |
||||
} |
||||
|
||||
setStyleSheet(R"( |
||||
PrimeAdWidget { |
||||
border-radius: 10px; |
||||
background-color: #333333; |
||||
} |
||||
)"); |
||||
} |
||||
|
||||
|
||||
SetupWidget::SetupWidget(QWidget* parent) : QFrame(parent) { |
||||
mainLayout = new QStackedWidget; |
||||
|
||||
// Unpaired, registration prompt layout
|
||||
|
||||
QFrame* finishRegistration = new QFrame; |
||||
finishRegistration->setObjectName("primeWidget"); |
||||
QVBoxLayout* finishRegistrationLayout = new QVBoxLayout(finishRegistration); |
||||
finishRegistrationLayout->setSpacing(38); |
||||
finishRegistrationLayout->setContentsMargins(64, 48, 64, 48); |
||||
|
||||
QLabel* registrationTitle = new QLabel(tr("Finish Setup")); |
||||
registrationTitle->setStyleSheet("font-size: 75px; font-weight: bold;"); |
||||
finishRegistrationLayout->addWidget(registrationTitle); |
||||
|
||||
QLabel* registrationDescription = new QLabel(tr("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.")); |
||||
registrationDescription->setWordWrap(true); |
||||
registrationDescription->setStyleSheet("font-size: 50px; font-weight: light;"); |
||||
finishRegistrationLayout->addWidget(registrationDescription); |
||||
|
||||
finishRegistrationLayout->addStretch(); |
||||
|
||||
QPushButton* pair = new QPushButton(tr("Pair device")); |
||||
pair->setStyleSheet(R"( |
||||
QPushButton { |
||||
font-size: 55px; |
||||
font-weight: 500; |
||||
border-radius: 10px; |
||||
background-color: #465BEA; |
||||
padding: 64px; |
||||
} |
||||
QPushButton:pressed { |
||||
background-color: #3049F4; |
||||
} |
||||
)"); |
||||
finishRegistrationLayout->addWidget(pair); |
||||
|
||||
popup = new PairingPopup(this); |
||||
QObject::connect(pair, &QPushButton::clicked, popup, &PairingPopup::exec); |
||||
|
||||
mainLayout->addWidget(finishRegistration); |
||||
|
||||
// build stacked layout
|
||||
QVBoxLayout *outer_layout = new QVBoxLayout(this); |
||||
outer_layout->setContentsMargins(0, 0, 0, 0); |
||||
outer_layout->addWidget(mainLayout); |
||||
|
||||
QWidget *content = new QWidget; |
||||
QVBoxLayout *content_layout = new QVBoxLayout(content); |
||||
content_layout->setContentsMargins(0, 0, 0, 0); |
||||
content_layout->setSpacing(30); |
||||
|
||||
WiFiPromptWidget *wifi_prompt = new WiFiPromptWidget; |
||||
QObject::connect(wifi_prompt, &WiFiPromptWidget::openSettings, this, &SetupWidget::openSettings); |
||||
content_layout->addWidget(wifi_prompt); |
||||
content_layout->addStretch(); |
||||
|
||||
mainLayout->addWidget(content); |
||||
|
||||
mainLayout->setCurrentIndex(1); |
||||
|
||||
setStyleSheet(R"( |
||||
#primeWidget { |
||||
border-radius: 10px; |
||||
background-color: #333333; |
||||
} |
||||
)"); |
||||
|
||||
// Retain size while hidden
|
||||
QSizePolicy sp_retain = sizePolicy(); |
||||
sp_retain.setRetainSizeWhenHidden(true); |
||||
setSizePolicy(sp_retain); |
||||
|
||||
QObject::connect(uiState()->prime_state, &PrimeState::changed, [this](PrimeState::Type type) { |
||||
if (type == PrimeState::PRIME_TYPE_UNPAIRED) { |
||||
mainLayout->setCurrentIndex(0); // Display "Pair your device" widget
|
||||
} else { |
||||
popup->reject(); |
||||
mainLayout->setCurrentIndex(1); // Display Wi-Fi prompt widget
|
||||
} |
||||
}); |
||||
} |
||||
@ -1,70 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <QLabel> |
||||
#include <QStackedWidget> |
||||
#include <QVBoxLayout> |
||||
#include <QWidget> |
||||
|
||||
#include "selfdrive/ui/qt/widgets/input.h" |
||||
|
||||
// pairing QR code
|
||||
class PairingQRWidget : public QWidget { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit PairingQRWidget(QWidget* parent = 0); |
||||
void paintEvent(QPaintEvent*) override; |
||||
|
||||
private: |
||||
QPixmap img; |
||||
QTimer *timer; |
||||
void updateQrCode(const QString &text); |
||||
void showEvent(QShowEvent *event) override; |
||||
void hideEvent(QHideEvent *event) override; |
||||
|
||||
private slots: |
||||
void refresh(); |
||||
}; |
||||
|
||||
|
||||
// pairing popup widget
|
||||
class PairingPopup : public DialogBase { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit PairingPopup(QWidget* parent); |
||||
int exec() override; |
||||
}; |
||||
|
||||
|
||||
// widget for paired users with prime
|
||||
class PrimeUserWidget : public QFrame { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit PrimeUserWidget(QWidget* parent = 0); |
||||
}; |
||||
|
||||
|
||||
// widget for paired users without prime
|
||||
class PrimeAdWidget : public QFrame { |
||||
Q_OBJECT |
||||
public: |
||||
explicit PrimeAdWidget(QWidget* parent = 0); |
||||
}; |
||||
|
||||
|
||||
// container widget
|
||||
class SetupWidget : public QFrame { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit SetupWidget(QWidget* parent = 0); |
||||
|
||||
signals: |
||||
void openSettings(int index = 0, const QString ¶m = ""); |
||||
|
||||
private: |
||||
PairingPopup *popup; |
||||
QStackedWidget *mainLayout; |
||||
}; |
||||
@ -1,49 +0,0 @@ |
||||
#include "selfdrive/ui/qt/widgets/scrollview.h" |
||||
|
||||
#include <QScrollBar> |
||||
#include <QScroller> |
||||
|
||||
// TODO: disable horizontal scrolling and resize
|
||||
|
||||
ScrollView::ScrollView(QWidget *w, QWidget *parent) : QScrollArea(parent) { |
||||
setWidget(w); |
||||
setWidgetResizable(true); |
||||
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); |
||||
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); |
||||
setStyleSheet("background-color: transparent; border:none"); |
||||
|
||||
QString style = R"( |
||||
QScrollBar:vertical { |
||||
border: none; |
||||
background: transparent; |
||||
width: 10px; |
||||
margin: 0; |
||||
} |
||||
QScrollBar::handle:vertical { |
||||
min-height: 0px; |
||||
border-radius: 5px; |
||||
background-color: white; |
||||
} |
||||
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { |
||||
height: 0px; |
||||
} |
||||
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { |
||||
background: none; |
||||
} |
||||
)"; |
||||
verticalScrollBar()->setStyleSheet(style); |
||||
horizontalScrollBar()->setStyleSheet(style); |
||||
|
||||
QScroller *scroller = QScroller::scroller(this->viewport()); |
||||
QScrollerProperties sp = scroller->scrollerProperties(); |
||||
|
||||
sp.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QVariant::fromValue<QScrollerProperties::OvershootPolicy>(QScrollerProperties::OvershootAlwaysOff)); |
||||
sp.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QVariant::fromValue<QScrollerProperties::OvershootPolicy>(QScrollerProperties::OvershootAlwaysOff)); |
||||
sp.setScrollMetric(QScrollerProperties::MousePressEventDelay, 0.01); |
||||
scroller->grabGesture(this->viewport(), QScroller::LeftMouseButtonGesture); |
||||
scroller->setScrollerProperties(sp); |
||||
} |
||||
|
||||
void ScrollView::hideEvent(QHideEvent *e) { |
||||
verticalScrollBar()->setValue(0); |
||||
} |
||||
@ -1,12 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <QScrollArea> |
||||
|
||||
class ScrollView : public QScrollArea { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit ScrollView(QWidget *w = nullptr, QWidget *parent = nullptr); |
||||
protected: |
||||
void hideEvent(QHideEvent *e) override; |
||||
}; |
||||
@ -1,64 +0,0 @@ |
||||
#include "selfdrive/ui/qt/widgets/ssh_keys.h" |
||||
|
||||
#include "common/params.h" |
||||
#include "selfdrive/ui/qt/api.h" |
||||
#include "selfdrive/ui/qt/widgets/input.h" |
||||
|
||||
SshControl::SshControl() : |
||||
ButtonControl(tr("SSH Keys"), "", tr("Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username " |
||||
"other than your own. A comma employee will NEVER ask you to add their GitHub username.")) { |
||||
|
||||
QObject::connect(this, &ButtonControl::clicked, [=]() { |
||||
if (text() == tr("ADD")) { |
||||
QString username = InputDialog::getText(tr("Enter your GitHub username"), this); |
||||
if (username.length() > 0) { |
||||
setText(tr("LOADING")); |
||||
setEnabled(false); |
||||
getUserKeys(username); |
||||
} |
||||
} else { |
||||
params.remove("GithubUsername"); |
||||
params.remove("GithubSshKeys"); |
||||
refresh(); |
||||
} |
||||
}); |
||||
|
||||
refresh(); |
||||
} |
||||
|
||||
void SshControl::refresh() { |
||||
QString param = QString::fromStdString(params.get("GithubSshKeys")); |
||||
if (param.length()) { |
||||
setValue(QString::fromStdString(params.get("GithubUsername"))); |
||||
setText(tr("REMOVE")); |
||||
} else { |
||||
setValue(""); |
||||
setText(tr("ADD")); |
||||
} |
||||
setEnabled(true); |
||||
} |
||||
|
||||
void SshControl::getUserKeys(const QString &username) { |
||||
HttpRequest *request = new HttpRequest(this, false); |
||||
QObject::connect(request, &HttpRequest::requestDone, [=](const QString &resp, bool success) { |
||||
if (success) { |
||||
if (!resp.isEmpty()) { |
||||
params.put("GithubUsername", username.toStdString()); |
||||
params.put("GithubSshKeys", resp.toStdString()); |
||||
} else { |
||||
ConfirmationDialog::alert(tr("Username '%1' has no keys on GitHub").arg(username), this); |
||||
} |
||||
} else { |
||||
if (request->timeout()) { |
||||
ConfirmationDialog::alert(tr("Request timed out"), this); |
||||
} else { |
||||
ConfirmationDialog::alert(tr("Username '%1' doesn't exist on GitHub").arg(username), this); |
||||
} |
||||
} |
||||
|
||||
refresh(); |
||||
request->deleteLater(); |
||||
}); |
||||
|
||||
request->sendRequest("https://github.com/" + username + ".keys"); |
||||
} |
||||
@ -1,32 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <QPushButton> |
||||
|
||||
#include "system/hardware/hw.h" |
||||
#include "selfdrive/ui/qt/widgets/controls.h" |
||||
|
||||
// SSH enable toggle
|
||||
class SshToggle : public ToggleControl { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
SshToggle() : ToggleControl(tr("Enable SSH"), "", "", Hardware::get_ssh_enabled()) { |
||||
QObject::connect(this, &SshToggle::toggleFlipped, [=](bool state) { |
||||
Hardware::set_ssh_enabled(state); |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
// SSH key management widget
|
||||
class SshControl : public ButtonControl { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
SshControl(); |
||||
|
||||
private: |
||||
Params params; |
||||
|
||||
void refresh(); |
||||
void getUserKeys(const QString &username); |
||||
}; |
||||
@ -1,83 +0,0 @@ |
||||
#include "selfdrive/ui/qt/widgets/toggle.h" |
||||
|
||||
#include <QPainter> |
||||
|
||||
Toggle::Toggle(QWidget *parent) : QAbstractButton(parent), |
||||
_height(80), |
||||
_height_rect(60), |
||||
on(false), |
||||
_anim(new QPropertyAnimation(this, "offset_circle", this)) |
||||
{ |
||||
_radius = _height / 2; |
||||
_x_circle = _radius; |
||||
_y_circle = _radius; |
||||
_y_rect = (_height - _height_rect)/2; |
||||
circleColor = QColor(0xffffff); // placeholder
|
||||
green = QColor(0xffffff); // placeholder
|
||||
setEnabled(true); |
||||
} |
||||
|
||||
void Toggle::paintEvent(QPaintEvent *e) { |
||||
this->setFixedHeight(_height); |
||||
QPainter p(this); |
||||
p.setPen(Qt::NoPen); |
||||
p.setRenderHint(QPainter::Antialiasing, true); |
||||
|
||||
// Draw toggle background left
|
||||
p.setBrush(green); |
||||
p.drawRoundedRect(QRect(0, _y_rect, _x_circle + _radius, _height_rect), _height_rect/2, _height_rect/2); |
||||
|
||||
// Draw toggle background right
|
||||
p.setBrush(QColor(0x393939)); |
||||
p.drawRoundedRect(QRect(_x_circle - _radius, _y_rect, width() - (_x_circle - _radius), _height_rect), _height_rect/2, _height_rect/2); |
||||
|
||||
// Draw toggle circle
|
||||
p.setBrush(circleColor); |
||||
p.drawEllipse(QRectF(_x_circle - _radius, _y_circle - _radius, 2 * _radius, 2 * _radius)); |
||||
} |
||||
|
||||
void Toggle::mouseReleaseEvent(QMouseEvent *e) { |
||||
if (!enabled) { |
||||
return; |
||||
} |
||||
const int left = _radius; |
||||
const int right = width() - _radius; |
||||
if ((_x_circle != left && _x_circle != right) || !this->rect().contains(e->localPos().toPoint())) { |
||||
// If mouse release isn't in rect or animation is running, don't parse touch events
|
||||
return; |
||||
} |
||||
if (e->button() & Qt::LeftButton) { |
||||
togglePosition(); |
||||
emit stateChanged(on); |
||||
} |
||||
} |
||||
|
||||
void Toggle::togglePosition() { |
||||
on = !on; |
||||
const int left = _radius; |
||||
const int right = width() - _radius; |
||||
_anim->setStartValue(on ? left + immediateOffset : right - immediateOffset); |
||||
_anim->setEndValue(on ? right : left); |
||||
_anim->setDuration(animation_duration); |
||||
_anim->start(); |
||||
repaint(); |
||||
} |
||||
|
||||
void Toggle::enterEvent(QEvent *e) { |
||||
QAbstractButton::enterEvent(e); |
||||
} |
||||
|
||||
bool Toggle::getEnabled() { |
||||
return enabled; |
||||
} |
||||
|
||||
void Toggle::setEnabled(bool value) { |
||||
enabled = value; |
||||
if (value) { |
||||
circleColor.setRgb(0xfafafa); |
||||
green.setRgb(0x33ab4c); |
||||
} else { |
||||
circleColor.setRgb(0x888888); |
||||
green.setRgb(0x227722); |
||||
} |
||||
} |
||||
@ -1,44 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <QAbstractButton> |
||||
#include <QMouseEvent> |
||||
#include <QPropertyAnimation> |
||||
|
||||
class Toggle : public QAbstractButton { |
||||
Q_OBJECT |
||||
Q_PROPERTY(int offset_circle READ offset_circle WRITE set_offset_circle CONSTANT) |
||||
|
||||
public: |
||||
Toggle(QWidget* parent = nullptr); |
||||
void togglePosition(); |
||||
bool on; |
||||
int animation_duration = 150; |
||||
int immediateOffset = 0; |
||||
int offset_circle() const { |
||||
return _x_circle; |
||||
} |
||||
|
||||
void set_offset_circle(int o) { |
||||
_x_circle = o; |
||||
update(); |
||||
} |
||||
bool getEnabled(); |
||||
void setEnabled(bool value); |
||||
|
||||
protected: |
||||
void paintEvent(QPaintEvent*) override; |
||||
void mouseReleaseEvent(QMouseEvent*) override; |
||||
void enterEvent(QEvent*) override; |
||||
|
||||
private: |
||||
QColor circleColor; |
||||
QColor green; |
||||
bool enabled = true; |
||||
int _x_circle, _y_circle; |
||||
int _height, _radius; |
||||
int _height_rect, _y_rect; |
||||
QPropertyAnimation *_anim = nullptr; |
||||
|
||||
signals: |
||||
void stateChanged(bool new_state); |
||||
}; |
||||
@ -1,45 +0,0 @@ |
||||
#include "selfdrive/ui/qt/widgets/wifi.h" |
||||
|
||||
#include <QHBoxLayout> |
||||
#include <QLabel> |
||||
#include <QPixmap> |
||||
#include <QPushButton> |
||||
|
||||
WiFiPromptWidget::WiFiPromptWidget(QWidget *parent) : QFrame(parent) { |
||||
// Setup Firehose Mode
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this); |
||||
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;"); |
||||
main_layout->addWidget(title); |
||||
|
||||
QLabel *desc = new QLabel(tr("Maximize your training data uploads to improve openpilot's driving models.")); |
||||
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"); }); |
||||
settings_btn->setStyleSheet(R"( |
||||
QPushButton { |
||||
font-size: 48px; |
||||
font-weight: 500; |
||||
border-radius: 10px; |
||||
background-color: #465BEA; |
||||
padding: 32px; |
||||
} |
||||
QPushButton:pressed { |
||||
background-color: #3049F4; |
||||
} |
||||
)"); |
||||
main_layout->addWidget(settings_btn); |
||||
|
||||
setStyleSheet(R"( |
||||
WiFiPromptWidget { |
||||
background-color: #333333; |
||||
border-radius: 10px; |
||||
} |
||||
)"); |
||||
} |
||||
@ -1,14 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <QFrame> |
||||
#include <QWidget> |
||||
|
||||
class WiFiPromptWidget : public QFrame { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit WiFiPromptWidget(QWidget* parent = 0); |
||||
|
||||
signals: |
||||
void openSettings(int index = 0, const QString ¶m = ""); |
||||
}; |
||||
@ -1,98 +0,0 @@ |
||||
#include "selfdrive/ui/qt/window.h" |
||||
|
||||
#include <QFontDatabase> |
||||
|
||||
#include "system/hardware/hw.h" |
||||
|
||||
MainWindow::MainWindow(QWidget *parent) : QWidget(parent) { |
||||
main_layout = new QStackedLayout(this); |
||||
main_layout->setMargin(0); |
||||
|
||||
homeWindow = new HomeWindow(this); |
||||
main_layout->addWidget(homeWindow); |
||||
QObject::connect(homeWindow, &HomeWindow::openSettings, this, &MainWindow::openSettings); |
||||
QObject::connect(homeWindow, &HomeWindow::closeSettings, this, &MainWindow::closeSettings); |
||||
|
||||
settingsWindow = new SettingsWindow(this); |
||||
main_layout->addWidget(settingsWindow); |
||||
QObject::connect(settingsWindow, &SettingsWindow::closeSettings, this, &MainWindow::closeSettings); |
||||
QObject::connect(settingsWindow, &SettingsWindow::reviewTrainingGuide, [=]() { |
||||
onboardingWindow->showTrainingGuide(); |
||||
main_layout->setCurrentWidget(onboardingWindow); |
||||
}); |
||||
QObject::connect(settingsWindow, &SettingsWindow::showDriverView, [=] { |
||||
homeWindow->showDriverView(true); |
||||
}); |
||||
|
||||
onboardingWindow = new OnboardingWindow(this); |
||||
main_layout->addWidget(onboardingWindow); |
||||
QObject::connect(onboardingWindow, &OnboardingWindow::onboardingDone, [=]() { |
||||
main_layout->setCurrentWidget(homeWindow); |
||||
}); |
||||
if (!onboardingWindow->completed()) { |
||||
main_layout->setCurrentWidget(onboardingWindow); |
||||
} |
||||
|
||||
QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { |
||||
if (!offroad) { |
||||
closeSettings(); |
||||
} |
||||
}); |
||||
QObject::connect(device(), &Device::interactiveTimeout, [=]() { |
||||
if (main_layout->currentWidget() == settingsWindow) { |
||||
closeSettings(); |
||||
} |
||||
}); |
||||
|
||||
// load fonts
|
||||
QFontDatabase::addApplicationFont("../assets/fonts/Inter-Black.ttf"); |
||||
QFontDatabase::addApplicationFont("../assets/fonts/Inter-Bold.ttf"); |
||||
QFontDatabase::addApplicationFont("../assets/fonts/Inter-ExtraBold.ttf"); |
||||
QFontDatabase::addApplicationFont("../assets/fonts/Inter-ExtraLight.ttf"); |
||||
QFontDatabase::addApplicationFont("../assets/fonts/Inter-Medium.ttf"); |
||||
QFontDatabase::addApplicationFont("../assets/fonts/Inter-Regular.ttf"); |
||||
QFontDatabase::addApplicationFont("../assets/fonts/Inter-SemiBold.ttf"); |
||||
QFontDatabase::addApplicationFont("../assets/fonts/Inter-Thin.ttf"); |
||||
QFontDatabase::addApplicationFont("../assets/fonts/JetBrainsMono-Medium.ttf"); |
||||
|
||||
// no outline to prevent the focus rectangle
|
||||
setStyleSheet(R"( |
||||
* { |
||||
font-family: Inter; |
||||
outline: none; |
||||
} |
||||
)"); |
||||
setAttribute(Qt::WA_NoSystemBackground); |
||||
} |
||||
|
||||
void MainWindow::openSettings(int index, const QString ¶m) { |
||||
main_layout->setCurrentWidget(settingsWindow); |
||||
settingsWindow->setCurrentPanel(index, param); |
||||
} |
||||
|
||||
void MainWindow::closeSettings() { |
||||
main_layout->setCurrentWidget(homeWindow); |
||||
|
||||
if (uiState()->scene.started) { |
||||
homeWindow->showSidebar(false); |
||||
} |
||||
} |
||||
|
||||
bool MainWindow::eventFilter(QObject *obj, QEvent *event) { |
||||
bool ignore = false; |
||||
switch (event->type()) { |
||||
case QEvent::TouchBegin: |
||||
case QEvent::TouchUpdate: |
||||
case QEvent::TouchEnd: |
||||
case QEvent::MouseButtonPress: |
||||
case QEvent::MouseMove: { |
||||
// ignore events when device is awakened by resetInteractiveTimeout
|
||||
ignore = !device()->isAwake(); |
||||
device()->resetInteractiveTimeout(); |
||||
break; |
||||
} |
||||
default: |
||||
break; |
||||
} |
||||
return ignore; |
||||
} |
||||
@ -1,25 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <QStackedLayout> |
||||
#include <QWidget> |
||||
|
||||
#include "selfdrive/ui/qt/home.h" |
||||
#include "selfdrive/ui/qt/offroad/onboarding.h" |
||||
#include "selfdrive/ui/qt/offroad/settings.h" |
||||
|
||||
class MainWindow : public QWidget { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit MainWindow(QWidget *parent = 0); |
||||
|
||||
private: |
||||
bool eventFilter(QObject *obj, QEvent *event) override; |
||||
void openSettings(int index = 0, const QString ¶m = ""); |
||||
void closeSettings(); |
||||
|
||||
QStackedLayout *main_layout; |
||||
HomeWindow *homeWindow; |
||||
SettingsWindow *settingsWindow; |
||||
OnboardingWindow *onboardingWindow; |
||||
}; |
||||
@ -1,18 +0,0 @@ |
||||
#!/usr/bin/env bash |
||||
|
||||
set -e |
||||
|
||||
UI_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"/.. |
||||
TEST_TEXT="(WRAPPED_SOURCE_TEXT)" |
||||
TEST_TS_FILE=$UI_DIR/translations/test_en.ts |
||||
TEST_QM_FILE=$UI_DIR/translations/test_en.qm |
||||
|
||||
# translation strings |
||||
UNFINISHED="<translation type=\"unfinished\"><\/translation>" |
||||
TRANSLATED="<translation>$TEST_TEXT<\/translation>" |
||||
|
||||
mkdir -p $UI_DIR/translations |
||||
rm -f $TEST_TS_FILE $TEST_QM_FILE |
||||
lupdate -recursive "$UI_DIR" -ts $TEST_TS_FILE |
||||
sed -i "s/$UNFINISHED/$TRANSLATED/" $TEST_TS_FILE |
||||
lrelease $TEST_TS_FILE |
||||
@ -1,26 +0,0 @@ |
||||
#define CATCH_CONFIG_RUNNER |
||||
#include "catch2/catch.hpp" |
||||
|
||||
#include <QApplication> |
||||
#include <QDebug> |
||||
#include <QDir> |
||||
#include <QTranslator> |
||||
|
||||
int main(int argc, char **argv) { |
||||
// unit tests for Qt
|
||||
QApplication app(argc, argv); |
||||
|
||||
QString language_file = "test_en"; |
||||
// FIXME: pytest-cpp considers this print as a test case
|
||||
qDebug() << "Loading language:" << language_file; |
||||
|
||||
QTranslator translator; |
||||
QString translationsPath = QDir::cleanPath(qApp->applicationDirPath() + "/../translations"); |
||||
if (!translator.load(language_file, translationsPath)) { |
||||
qDebug() << "Failed to load translation file!"; |
||||
} |
||||
app.installTranslator(&translator); |
||||
|
||||
const int res = Catch::Session().run(argc, argv); |
||||
return (res < 0xff ? res : 0xff); |
||||
} |
||||
@ -1,48 +0,0 @@ |
||||
#include "catch2/catch.hpp" |
||||
|
||||
#include "common/params.h" |
||||
#include "selfdrive/ui/qt/window.h" |
||||
|
||||
const QString TEST_TEXT = "(WRAPPED_SOURCE_TEXT)"; // what each string should be translated to
|
||||
QRegExp RE_NUM("\\d*"); |
||||
|
||||
QStringList getParentWidgets(QWidget* widget){ |
||||
QStringList parentWidgets; |
||||
while (widget->parentWidget() != Q_NULLPTR) { |
||||
widget = widget->parentWidget(); |
||||
parentWidgets.append(widget->metaObject()->className()); |
||||
} |
||||
return parentWidgets; |
||||
} |
||||
|
||||
template <typename T> |
||||
void checkWidgetTrWrap(MainWindow &w) { |
||||
for (auto widget : w.findChildren<T>()) { |
||||
const QString text = widget->text(); |
||||
bool isNumber = RE_NUM.exactMatch(text); |
||||
bool wrapped = text.contains(TEST_TEXT); |
||||
QString parentWidgets = getParentWidgets(widget).join("->"); |
||||
|
||||
if (!text.isEmpty() && !isNumber && !wrapped) { |
||||
FAIL(("\"" + text + "\" must be wrapped. Parent widgets: " + parentWidgets).toStdString()); |
||||
} |
||||
|
||||
// warn if source string wrapped, but UI adds text
|
||||
// TODO: add way to ignore this
|
||||
if (wrapped && text != TEST_TEXT) { |
||||
WARN(("\"" + text + "\" is dynamic and needs a custom retranslate function. Parent widgets: " + parentWidgets).toStdString()); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Tests all strings in the UI are wrapped with tr()
|
||||
TEST_CASE("UI: test all strings wrapped") { |
||||
Params().remove("LanguageSetting"); |
||||
Params().remove("HardwareSerial"); |
||||
Params().remove("DongleId"); |
||||
qputenv("TICI", "1"); |
||||
|
||||
MainWindow w; |
||||
checkWidgetTrWrap<QPushButton*>(w); |
||||
checkWidgetTrWrap<QLabel*>(w); |
||||
} |
||||
@ -1,310 +0,0 @@ |
||||
#!/usr/bin/env python3 |
||||
import capnp |
||||
import pathlib |
||||
import shutil |
||||
import sys |
||||
import os |
||||
import pywinctl |
||||
import pyautogui |
||||
import pickle |
||||
import time |
||||
from collections import namedtuple |
||||
|
||||
from cereal import car, log |
||||
from msgq.visionipc import VisionIpcServer, VisionStreamType |
||||
from cereal.messaging import PubMaster, log_from_bytes, sub_sock |
||||
from openpilot.common.basedir import BASEDIR |
||||
from openpilot.common.params import Params |
||||
from openpilot.common.prefix import OpenpilotPrefix |
||||
from openpilot.common.transformations.camera import CameraConfig, DEVICE_CAMERAS |
||||
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert |
||||
from openpilot.selfdrive.test.helpers import with_processes |
||||
from openpilot.selfdrive.test.process_replay.migration import migrate, migrate_controlsState, migrate_carState |
||||
from openpilot.tools.lib.logreader import LogReader |
||||
from openpilot.tools.lib.framereader import FrameReader |
||||
from openpilot.tools.lib.route import Route |
||||
from openpilot.tools.lib.cache import DEFAULT_CACHE_DIR |
||||
|
||||
UI_DELAY = 0.1 # may be slower on CI? |
||||
TEST_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" |
||||
|
||||
STREAMS: list[tuple[VisionStreamType, CameraConfig, bytes]] = [] |
||||
OFFROAD_ALERTS = ['Offroad_IsTakingSnapshot', ] |
||||
DATA: dict[str, capnp.lib.capnp._DynamicStructBuilder] = dict.fromkeys( |
||||
["carParams", "deviceState", "pandaStates", "controlsState", "selfdriveState", |
||||
"liveCalibration", "modelV2", "radarState", "driverMonitoringState", "carState", |
||||
"driverStateV2", "roadCameraState", "wideRoadCameraState", "driverCameraState"], None) |
||||
|
||||
def setup_homescreen(click, pm: PubMaster): |
||||
pass |
||||
|
||||
def setup_settings_device(click, pm: PubMaster): |
||||
click(100, 100) |
||||
|
||||
def setup_settings_toggles(click, pm: PubMaster): |
||||
setup_settings_device(click, pm) |
||||
click(278, 600) |
||||
time.sleep(UI_DELAY) |
||||
|
||||
def setup_settings_software(click, pm: PubMaster): |
||||
setup_settings_device(click, pm) |
||||
click(278, 720) |
||||
time.sleep(UI_DELAY) |
||||
|
||||
def setup_settings_firehose(click, pm: PubMaster): |
||||
click(1780, 730) |
||||
|
||||
def setup_settings_developer(click, pm: PubMaster): |
||||
CP = car.CarParams() |
||||
CP.alphaLongitudinalAvailable = True |
||||
Params().put("CarParamsPersistent", CP.to_bytes()) |
||||
|
||||
setup_settings_device(click, pm) |
||||
click(278, 970) |
||||
time.sleep(UI_DELAY) |
||||
|
||||
def setup_onroad(click, pm: PubMaster): |
||||
vipc_server = VisionIpcServer("camerad") |
||||
for stream_type, cam, _ in STREAMS: |
||||
vipc_server.create_buffers(stream_type, 5, cam.width, cam.height) |
||||
vipc_server.start_listener() |
||||
|
||||
uidebug_received_cnt = 0 |
||||
packet_id = 0 |
||||
uidebug_sock = sub_sock('uiDebug') |
||||
|
||||
# Condition check for uiDebug processing |
||||
check_uidebug = DATA['deviceState'].deviceState.started and not DATA['carParams'].carParams.notCar |
||||
|
||||
# Loop until 20 'uiDebug' messages are received |
||||
while uidebug_received_cnt <= 20: |
||||
for service, data in DATA.items(): |
||||
if data: |
||||
data.clear_write_flag() |
||||
pm.send(service, data) |
||||
|
||||
for stream_type, _, image in STREAMS: |
||||
vipc_server.send(stream_type, image, packet_id, packet_id, packet_id) |
||||
|
||||
if check_uidebug: |
||||
while uidebug_sock.receive(non_blocking=True): |
||||
uidebug_received_cnt += 1 |
||||
else: |
||||
uidebug_received_cnt += 1 |
||||
|
||||
packet_id += 1 |
||||
time.sleep(0.05) |
||||
|
||||
def setup_onroad_disengaged(click, pm: PubMaster): |
||||
DATA['selfdriveState'].selfdriveState.enabled = False |
||||
setup_onroad(click, pm) |
||||
DATA['selfdriveState'].selfdriveState.enabled = True |
||||
|
||||
def setup_onroad_override(click, pm: PubMaster): |
||||
DATA['selfdriveState'].selfdriveState.state = log.SelfdriveState.OpenpilotState.overriding |
||||
setup_onroad(click, pm) |
||||
DATA['selfdriveState'].selfdriveState.state = log.SelfdriveState.OpenpilotState.enabled |
||||
|
||||
|
||||
def setup_onroad_wide(click, pm: PubMaster): |
||||
DATA['selfdriveState'].selfdriveState.experimentalMode = True |
||||
DATA["carState"].carState.vEgo = 1 |
||||
setup_onroad(click, pm) |
||||
|
||||
def setup_onroad_sidebar(click, pm: PubMaster): |
||||
setup_onroad(click, pm) |
||||
click(500, 500) |
||||
setup_onroad(click, pm) |
||||
|
||||
def setup_onroad_wide_sidebar(click, pm: PubMaster): |
||||
setup_onroad_wide(click, pm) |
||||
click(500, 500) |
||||
setup_onroad_wide(click, pm) |
||||
|
||||
def setup_body(click, pm: PubMaster): |
||||
DATA['carParams'].carParams.brand = "body" |
||||
DATA['carParams'].carParams.notCar = True |
||||
DATA['carState'].carState.charging = True |
||||
DATA['carState'].carState.fuelGauge = 50.0 |
||||
setup_onroad(click, pm) |
||||
|
||||
def setup_keyboard(click, pm: PubMaster): |
||||
setup_settings_device(click, pm) |
||||
click(250, 965) |
||||
click(1930, 420) |
||||
|
||||
def setup_keyboard_uppercase(click, pm: PubMaster): |
||||
setup_keyboard(click, pm) |
||||
click(200, 800) |
||||
|
||||
def setup_driver_camera(click, pm: PubMaster): |
||||
setup_settings_device(click, pm) |
||||
click(1950, 435) |
||||
DATA['deviceState'].deviceState.started = False |
||||
setup_onroad(click, pm) |
||||
DATA['deviceState'].deviceState.started = True |
||||
|
||||
def setup_onroad_alert(click, pm: PubMaster, text1, text2, size, status=log.SelfdriveState.AlertStatus.normal): |
||||
print(f'setup onroad alert, size: {size}') |
||||
state = DATA['selfdriveState'] |
||||
origin_state_bytes = state.to_bytes() |
||||
cs = state.selfdriveState |
||||
cs.alertText1 = text1 |
||||
cs.alertText2 = text2 |
||||
cs.alertSize = size |
||||
cs.alertStatus = status |
||||
cs.alertType = "test_onroad_alert" |
||||
setup_onroad(click, pm) |
||||
DATA['selfdriveState'] = log_from_bytes(origin_state_bytes).as_builder() |
||||
|
||||
def setup_onroad_alert_small(click, pm: PubMaster): |
||||
setup_onroad_alert(click, pm, 'This is a small alert message', '', log.SelfdriveState.AlertSize.small) |
||||
|
||||
def setup_onroad_alert_mid(click, pm: PubMaster): |
||||
setup_onroad_alert(click, pm, 'Medium Alert', 'This is a medium alert message', log.SelfdriveState.AlertSize.mid) |
||||
|
||||
def setup_onroad_alert_full(click, pm: PubMaster): |
||||
setup_onroad_alert(click, pm, 'Full Alert', 'This is a full alert message', log.SelfdriveState.AlertSize.full) |
||||
|
||||
def setup_offroad_alert(click, pm: PubMaster): |
||||
for alert in OFFROAD_ALERTS: |
||||
set_offroad_alert(alert, True) |
||||
|
||||
# Toggle between settings and home to refresh the offroad alert widget |
||||
setup_settings_device(click, pm) |
||||
click(240, 216) |
||||
|
||||
def setup_update_available(click, pm: PubMaster): |
||||
Params().put_bool("UpdateAvailable", True) |
||||
release_notes_path = os.path.join(BASEDIR, "RELEASES.md") |
||||
with open(release_notes_path) as file: |
||||
release_notes = file.read().split('\n\n', 1)[0] |
||||
Params().put("UpdaterNewReleaseNotes", release_notes + "\n") |
||||
|
||||
setup_settings_device(click, pm) |
||||
click(240, 216) |
||||
|
||||
def setup_pair_device(click, pm: PubMaster): |
||||
click(1950, 435) |
||||
click(1800, 900) |
||||
|
||||
CASES = { |
||||
"homescreen": setup_homescreen, |
||||
"prime": setup_homescreen, |
||||
"pair_device": setup_pair_device, |
||||
"settings_device": setup_settings_device, |
||||
"settings_toggles": setup_settings_toggles, |
||||
"settings_software": setup_settings_software, |
||||
"settings_firehose": setup_settings_firehose, |
||||
"settings_developer": setup_settings_developer, |
||||
"onroad": setup_onroad, |
||||
"onroad_disengaged": setup_onroad_disengaged, |
||||
"onroad_override": setup_onroad_override, |
||||
"onroad_sidebar": setup_onroad_sidebar, |
||||
"onroad_alert_small": setup_onroad_alert_small, |
||||
"onroad_alert_mid": setup_onroad_alert_mid, |
||||
"onroad_alert_full": setup_onroad_alert_full, |
||||
"onroad_wide": setup_onroad_wide, |
||||
"onroad_wide_sidebar": setup_onroad_wide_sidebar, |
||||
"driver_camera": setup_driver_camera, |
||||
"body": setup_body, |
||||
"offroad_alert": setup_offroad_alert, |
||||
"update_available": setup_update_available, |
||||
"keyboard": setup_keyboard, |
||||
"keyboard_uppercase": setup_keyboard_uppercase |
||||
} |
||||
|
||||
TEST_DIR = pathlib.Path(__file__).parent |
||||
|
||||
TEST_OUTPUT_DIR = TEST_DIR / "report_1" |
||||
SCREENSHOTS_DIR = TEST_OUTPUT_DIR / "screenshots" |
||||
|
||||
|
||||
class TestUI: |
||||
def __init__(self): |
||||
os.environ["SCALE"] = "1" |
||||
sys.modules["mouseinfo"] = False |
||||
|
||||
def setup(self): |
||||
self.pm = PubMaster(list(DATA.keys())) |
||||
DATA['deviceState'].deviceState.networkType = log.DeviceState.NetworkType.wifi |
||||
DATA['deviceState'].deviceState.lastAthenaPingTime = 0 |
||||
for _ in range(10): |
||||
self.pm.send('deviceState', DATA['deviceState']) |
||||
DATA['deviceState'].clear_write_flag() |
||||
time.sleep(0.05) |
||||
try: |
||||
self.ui = pywinctl.getWindowsWithTitle("ui")[0] |
||||
except Exception as e: |
||||
print(f"failed to find ui window, assuming that it's in the top left (for Xvfb) {e}") |
||||
self.ui = namedtuple("bb", ["left", "top", "width", "height"])(0,0,2160,1080) |
||||
|
||||
def screenshot(self, name): |
||||
im = pyautogui.screenshot(SCREENSHOTS_DIR / f"{name}.png", region=(self.ui.left, self.ui.top, self.ui.width, self.ui.height)) |
||||
assert im.width == 2160 |
||||
assert im.height == 1080 |
||||
|
||||
def click(self, x, y, *args, **kwargs): |
||||
pyautogui.click(self.ui.left + x, self.ui.top + y, *args, **kwargs) |
||||
time.sleep(UI_DELAY) # give enough time for the UI to react |
||||
|
||||
@with_processes(["ui"]) |
||||
def test_ui(self, name, setup_case): |
||||
self.setup() |
||||
setup_case(self.click, self.pm) |
||||
self.screenshot(name) |
||||
|
||||
def create_screenshots(): |
||||
if TEST_OUTPUT_DIR.exists(): |
||||
shutil.rmtree(TEST_OUTPUT_DIR) |
||||
|
||||
SCREENSHOTS_DIR.mkdir(parents=True) |
||||
|
||||
route = Route(TEST_ROUTE) |
||||
|
||||
segnum = 2 |
||||
lr = LogReader(route.qlog_paths()[segnum]) |
||||
DATA['carParams'] = next((event.as_builder() for event in lr if event.which() == 'carParams'), None) |
||||
for event in migrate(lr, [migrate_controlsState, migrate_carState]): |
||||
if event.which() in DATA: |
||||
DATA[event.which()] = event.as_builder() |
||||
|
||||
if all(DATA.values()): |
||||
break |
||||
|
||||
cam = DEVICE_CAMERAS[("tici", "ar0231")] |
||||
|
||||
frames_cache = f'{DEFAULT_CACHE_DIR}/ui_frames' |
||||
if os.path.isfile(frames_cache): |
||||
with open(frames_cache, 'rb') as f: |
||||
frames = pickle.load(f) |
||||
road_img = frames[0] |
||||
wide_road_img = frames[1] |
||||
driver_img = frames[2] |
||||
else: |
||||
with open(frames_cache, 'wb') as f: |
||||
road_img = FrameReader(route.camera_paths()[segnum], pix_fmt="nv12").get(0) |
||||
wide_road_img = FrameReader(route.ecamera_paths()[segnum], pix_fmt="nv12").get(0) |
||||
driver_img = FrameReader(route.dcamera_paths()[segnum], pix_fmt="nv12").get(0) |
||||
pickle.dump([road_img, wide_road_img, driver_img], f) |
||||
|
||||
STREAMS.append((VisionStreamType.VISION_STREAM_ROAD, cam.fcam, road_img.flatten().tobytes())) |
||||
STREAMS.append((VisionStreamType.VISION_STREAM_WIDE_ROAD, cam.ecam, wide_road_img.flatten().tobytes())) |
||||
STREAMS.append((VisionStreamType.VISION_STREAM_DRIVER, cam.dcam, driver_img.flatten().tobytes())) |
||||
|
||||
t = TestUI() |
||||
|
||||
for name, setup in CASES.items(): |
||||
with OpenpilotPrefix(): |
||||
params = Params() |
||||
params.put("DongleId", "123456789012345") |
||||
if name == 'prime': |
||||
params.put('PrimeType', 1) |
||||
elif name == 'pair_device': |
||||
params.put('ApiCache_Device', {"is_paired":0, "prime_type":-1}) |
||||
|
||||
t.test_ui(name, setup) |
||||
|
||||
if __name__ == "__main__": |
||||
print("creating test screenshots") |
||||
create_screenshots() |
||||
@ -1,71 +0,0 @@ |
||||
# Multilanguage |
||||
|
||||
[](#) |
||||
|
||||
## Contributing |
||||
|
||||
Before getting started, make sure you have set up the openpilot Ubuntu development environment by reading the [tools README.md](/tools/README.md). |
||||
|
||||
### Policy |
||||
|
||||
Most of the languages supported by openpilot come from and are maintained by the community via pull requests. A pull request likely to be merged is one that [fixes a translation or adds missing translations.](https://github.com/commaai/openpilot/blob/master/selfdrive/ui/translations/README.md#improving-an-existing-language) |
||||
|
||||
We also generally merge pull requests adding support for a new language if there are community members willing to maintain it. Maintaining a language is ensuring quality and completion of translations before each openpilot release. |
||||
|
||||
comma may remove or hide language support from releases depending on translation quality and completeness. |
||||
|
||||
### Adding a New Language |
||||
|
||||
openpilot provides a few tools to help contributors manage their translations and to ensure quality. To get started: |
||||
|
||||
1. Add your new language to [languages.json](/selfdrive/ui/translations/languages.json) with the appropriate [language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) and the localized language name (Traditional Chinese is `中文(繁體)`). |
||||
2. Generate the XML translation file (`*.ts`): |
||||
```shell |
||||
selfdrive/ui/update_translations.py |
||||
``` |
||||
3. Edit the translation file, marking each translation as completed: |
||||
```shell |
||||
linguist selfdrive/ui/translations/your_language_file.ts |
||||
``` |
||||
4. View your finished translations by compiling and starting the UI, then find it in the language selector: |
||||
```shell |
||||
scons -j$(nproc) selfdrive/ui && selfdrive/ui/ui |
||||
``` |
||||
5. Read [Checking the UI](#checking-the-ui) to double-check your translations fit in the UI. |
||||
|
||||
### Improving an Existing Language |
||||
|
||||
Follow step 3. above, you can review existing translations and add missing ones. Once you're done, just open a pull request to openpilot. |
||||
|
||||
### Checking the UI |
||||
Different languages use varying space to convey the same message, so it's a good idea to double-check that your translations do not overlap and fit into each widget. Start the UI (step 4. above) and view each page, making adjustments to translations as needed. |
||||
|
||||
#### To view offroad alerts: |
||||
|
||||
With the UI started, you can view the offroad alerts with: |
||||
```shell |
||||
selfdrive/ui/tests/cycle_offroad_alerts.py |
||||
``` |
||||
|
||||
### Updating the UI |
||||
|
||||
Any time you edit source code in the UI, you need to update the translations to ensure the line numbers and contexts are up to date (first step above). |
||||
|
||||
### Testing |
||||
|
||||
openpilot has a few unit tests to make sure all translations are up-to-date and that all strings are wrapped in a translation marker. They are run in CI, but you can also run them locally. |
||||
|
||||
Tests translation files up to date: |
||||
|
||||
```shell |
||||
selfdrive/ui/tests/test_translations.py |
||||
``` |
||||
|
||||
Tests all static source strings are wrapped: |
||||
|
||||
```shell |
||||
selfdrive/ui/tests/create_test_translations.sh && selfdrive/ui/tests/test_translations |
||||
``` |
||||
|
||||
--- |
||||
 |
||||
File diff suppressed because it is too large
Load Diff
@ -1,62 +0,0 @@ |
||||
#!/usr/bin/env python3 |
||||
import json |
||||
import os |
||||
import requests |
||||
import xml.etree.ElementTree as ET |
||||
|
||||
from openpilot.common.basedir import BASEDIR |
||||
from openpilot.selfdrive.ui.tests.test_translations import UNFINISHED_TRANSLATION_TAG |
||||
from openpilot.selfdrive.ui.update_translations import LANGUAGES_FILE, TRANSLATIONS_DIR |
||||
|
||||
TRANSLATION_TAG = "<translation" |
||||
BADGE_HEIGHT = 20 + 8 |
||||
SHIELDS_URL = "https://img.shields.io/badge" |
||||
|
||||
if __name__ == "__main__": |
||||
with open(LANGUAGES_FILE) as f: |
||||
translation_files = json.load(f) |
||||
|
||||
badge_svg = [] |
||||
max_badge_width = 0 # keep track of max width to set parent element |
||||
for idx, (name, file) in enumerate(translation_files.items()): |
||||
with open(os.path.join(TRANSLATIONS_DIR, f"{file}.ts")) as tr_f: |
||||
tr_file = tr_f.read() |
||||
|
||||
total_translations = 0 |
||||
unfinished_translations = 0 |
||||
for line in tr_file.splitlines(): |
||||
if TRANSLATION_TAG in line: |
||||
total_translations += 1 |
||||
if UNFINISHED_TRANSLATION_TAG in line: |
||||
unfinished_translations += 1 |
||||
|
||||
percent_finished = int(100 - (unfinished_translations / total_translations * 100.)) |
||||
color = f"rgb{(94, 188, 0) if percent_finished == 100 else (248, 255, 50) if percent_finished > 90 else (204, 55, 27)}" |
||||
|
||||
# Download badge |
||||
badge_label = f"LANGUAGE {name}" |
||||
badge_message = f"{percent_finished}% complete" |
||||
if unfinished_translations != 0: |
||||
badge_message += f" ({unfinished_translations} unfinished)" |
||||
|
||||
r = requests.get(f"{SHIELDS_URL}/{badge_label}-{badge_message}-{color}", timeout=10) |
||||
assert r.status_code == 200, "Error downloading badge" |
||||
content_svg = r.content.decode("utf-8") |
||||
|
||||
xml = ET.fromstring(content_svg) |
||||
assert "width" in xml.attrib |
||||
max_badge_width = max(max_badge_width, int(xml.attrib["width"])) |
||||
|
||||
# Make tag ids in each badge unique to combine them into one svg |
||||
for tag in ("r", "s"): |
||||
content_svg = content_svg.replace(f'id="{tag}"', f'id="{tag}{idx}"') |
||||
content_svg = content_svg.replace(f'"url(#{tag})"', f'"url(#{tag}{idx})"') |
||||
|
||||
badge_svg.extend([f'<g transform="translate(0, {idx * BADGE_HEIGHT})">', content_svg, "</g>"]) |
||||
|
||||
badge_svg.insert(0, '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ' + |
||||
f'height="{len(translation_files) * BADGE_HEIGHT}" width="{max_badge_width}">') |
||||
badge_svg.append("</svg>") |
||||
|
||||
with open(os.path.join(BASEDIR, "translation_badge.svg"), "w") as badge_f: |
||||
badge_f.write("\n".join(badge_svg)) |
||||
File diff suppressed because it is too large
Load Diff
@ -1,48 +0,0 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<!DOCTYPE TS> |
||||
<TS version="2.1" language="en_US"> |
||||
<context> |
||||
<name>FirehosePanel</name> |
||||
<message numerus="yes"> |
||||
<source><b>%n segment(s)</b> of your driving is in the training dataset so far.</source> |
||||
<translation> |
||||
<numerusform><b>%n segment</b> of your driving is in the training dataset so far.</numerusform> |
||||
<numerusform><b>%n segments</b> of your driving are in the training dataset so far.</numerusform> |
||||
</translation> |
||||
</message> |
||||
</context> |
||||
<context> |
||||
<name>InputDialog</name> |
||||
<message numerus="yes"> |
||||
<source>Need at least %n character(s)!</source> |
||||
<translation> |
||||
<numerusform>Need at least %n character!</numerusform> |
||||
<numerusform>Need at least %n characters!</numerusform> |
||||
</translation> |
||||
</message> |
||||
</context> |
||||
<context> |
||||
<name>QObject</name> |
||||
<message numerus="yes"> |
||||
<source>%n minute(s) ago</source> |
||||
<translation> |
||||
<numerusform>%n minute ago</numerusform> |
||||
<numerusform>%n minutes ago</numerusform> |
||||
</translation> |
||||
</message> |
||||
<message numerus="yes"> |
||||
<source>%n hour(s) ago</source> |
||||
<translation> |
||||
<numerusform>%n hour ago</numerusform> |
||||
<numerusform>%n hours ago</numerusform> |
||||
</translation> |
||||
</message> |
||||
<message numerus="yes"> |
||||
<source>%n day(s) ago</source> |
||||
<translation> |
||||
<numerusform>%n day ago</numerusform> |
||||
<numerusform>%n days ago</numerusform> |
||||
</translation> |
||||
</message> |
||||
</context> |
||||
</TS> |
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,199 +0,0 @@ |
||||
#include "selfdrive/ui/ui.h" |
||||
|
||||
#include <algorithm> |
||||
#include <cmath> |
||||
|
||||
#include <QtConcurrent> |
||||
|
||||
#include "common/transformations/orientation.hpp" |
||||
#include "common/swaglog.h" |
||||
#include "common/util.h" |
||||
#include "system/hardware/hw.h" |
||||
|
||||
#define BACKLIGHT_DT 0.05 |
||||
#define BACKLIGHT_TS 10.00 |
||||
|
||||
static void update_sockets(UIState *s) { |
||||
s->sm->update(0); |
||||
} |
||||
|
||||
static void update_state(UIState *s) { |
||||
SubMaster &sm = *(s->sm); |
||||
UIScene &scene = s->scene; |
||||
|
||||
if (sm.updated("liveCalibration")) { |
||||
auto list2rot = [](const capnp::List<float>::Reader &rpy_list) ->Eigen::Matrix3f { |
||||
return euler2rot({rpy_list[0], rpy_list[1], rpy_list[2]}).cast<float>(); |
||||
}; |
||||
|
||||
auto live_calib = sm["liveCalibration"].getLiveCalibration(); |
||||
if (live_calib.getCalStatus() == cereal::LiveCalibrationData::Status::CALIBRATED) { |
||||
auto device_from_calib = list2rot(live_calib.getRpyCalib()); |
||||
auto wide_from_device = list2rot(live_calib.getWideFromDeviceEuler()); |
||||
s->scene.view_from_calib = VIEW_FROM_DEVICE * device_from_calib; |
||||
s->scene.view_from_wide_calib = VIEW_FROM_DEVICE * wide_from_device * device_from_calib; |
||||
} else { |
||||
s->scene.view_from_calib = s->scene.view_from_wide_calib = VIEW_FROM_DEVICE; |
||||
} |
||||
} |
||||
if (sm.updated("pandaStates")) { |
||||
auto pandaStates = sm["pandaStates"].getPandaStates(); |
||||
if (pandaStates.size() > 0) { |
||||
scene.pandaType = pandaStates[0].getPandaType(); |
||||
|
||||
if (scene.pandaType != cereal::PandaState::PandaType::UNKNOWN) { |
||||
scene.ignition = false; |
||||
for (const auto& pandaState : pandaStates) { |
||||
scene.ignition |= pandaState.getIgnitionLine() || pandaState.getIgnitionCan(); |
||||
} |
||||
} |
||||
} |
||||
} else if ((s->sm->frame - s->sm->rcv_frame("pandaStates")) > 5*UI_FREQ) { |
||||
scene.pandaType = cereal::PandaState::PandaType::UNKNOWN; |
||||
} |
||||
if (sm.updated("wideRoadCameraState")) { |
||||
auto cam_state = sm["wideRoadCameraState"].getWideRoadCameraState(); |
||||
scene.light_sensor = std::max(100.0f - cam_state.getExposureValPercent(), 0.0f); |
||||
} else if (!sm.allAliveAndValid({"wideRoadCameraState"})) { |
||||
scene.light_sensor = -1; |
||||
} |
||||
scene.started = sm["deviceState"].getDeviceState().getStarted() && scene.ignition; |
||||
|
||||
auto params = Params(); |
||||
scene.recording_audio = params.getBool("RecordAudio") && scene.started; |
||||
} |
||||
|
||||
void ui_update_params(UIState *s) { |
||||
auto params = Params(); |
||||
s->scene.is_metric = params.getBool("IsMetric"); |
||||
} |
||||
|
||||
void UIState::updateStatus() { |
||||
if (scene.started && sm->updated("selfdriveState")) { |
||||
auto ss = (*sm)["selfdriveState"].getSelfdriveState(); |
||||
auto state = ss.getState(); |
||||
if (state == cereal::SelfdriveState::OpenpilotState::PRE_ENABLED || state == cereal::SelfdriveState::OpenpilotState::OVERRIDING) { |
||||
status = STATUS_OVERRIDE; |
||||
} else { |
||||
status = ss.getEnabled() ? STATUS_ENGAGED : STATUS_DISENGAGED; |
||||
} |
||||
} |
||||
|
||||
if (engaged() != engaged_prev) { |
||||
engaged_prev = engaged(); |
||||
emit engagedChanged(engaged()); |
||||
} |
||||
|
||||
// Handle onroad/offroad transition
|
||||
if (scene.started != started_prev || sm->frame == 1) { |
||||
if (scene.started) { |
||||
status = STATUS_DISENGAGED; |
||||
scene.started_frame = sm->frame; |
||||
} |
||||
started_prev = scene.started; |
||||
emit offroadTransition(!scene.started); |
||||
} |
||||
} |
||||
|
||||
UIState::UIState(QObject *parent) : QObject(parent) { |
||||
sm = std::make_unique<SubMaster>(std::vector<const char*>{ |
||||
"modelV2", "controlsState", "liveCalibration", "radarState", "deviceState", |
||||
"pandaStates", "carParams", "driverMonitoringState", "carState", "driverStateV2", |
||||
"wideRoadCameraState", "managerState", "selfdriveState", "longitudinalPlan", |
||||
}); |
||||
prime_state = new PrimeState(this); |
||||
language = QString::fromStdString(Params().get("LanguageSetting")); |
||||
|
||||
// update timer
|
||||
timer = new QTimer(this); |
||||
QObject::connect(timer, &QTimer::timeout, this, &UIState::update); |
||||
timer->start(1000 / UI_FREQ); |
||||
} |
||||
|
||||
void UIState::update() { |
||||
update_sockets(this); |
||||
update_state(this); |
||||
updateStatus(); |
||||
|
||||
emit uiUpdate(*this); |
||||
} |
||||
|
||||
Device::Device(QObject *parent) : brightness_filter(BACKLIGHT_OFFROAD, BACKLIGHT_TS, BACKLIGHT_DT), QObject(parent) { |
||||
setAwake(true); |
||||
resetInteractiveTimeout(); |
||||
|
||||
QObject::connect(uiState(), &UIState::uiUpdate, this, &Device::update); |
||||
} |
||||
|
||||
void Device::update(const UIState &s) { |
||||
updateBrightness(s); |
||||
updateWakefulness(s); |
||||
} |
||||
|
||||
void Device::setAwake(bool on) { |
||||
if (on != awake) { |
||||
awake = on; |
||||
Hardware::set_display_power(awake); |
||||
LOGD("setting display power %d", awake); |
||||
emit displayPowerChanged(awake); |
||||
} |
||||
} |
||||
|
||||
void Device::resetInteractiveTimeout(int timeout) { |
||||
if (timeout == -1) { |
||||
timeout = (ignition_on ? 10 : 30); |
||||
} |
||||
interactive_timeout = timeout * UI_FREQ; |
||||
} |
||||
|
||||
void Device::updateBrightness(const UIState &s) { |
||||
float clipped_brightness = offroad_brightness; |
||||
if (s.scene.started && s.scene.light_sensor >= 0) { |
||||
clipped_brightness = s.scene.light_sensor; |
||||
|
||||
// CIE 1931 - https://www.photonstophotos.net/GeneralTopics/Exposure/Psychometric_Lightness_and_Gamma.htm
|
||||
if (clipped_brightness <= 8) { |
||||
clipped_brightness = (clipped_brightness / 903.3); |
||||
} else { |
||||
clipped_brightness = std::pow((clipped_brightness + 16.0) / 116.0, 3.0); |
||||
} |
||||
|
||||
// Scale back to 10% to 100%
|
||||
clipped_brightness = std::clamp(100.0f * clipped_brightness, 10.0f, 100.0f); |
||||
} |
||||
|
||||
int brightness = brightness_filter.update(clipped_brightness); |
||||
if (!awake) { |
||||
brightness = 0; |
||||
} |
||||
|
||||
if (brightness != last_brightness) { |
||||
if (!brightness_future.isRunning()) { |
||||
brightness_future = QtConcurrent::run(Hardware::set_brightness, brightness); |
||||
last_brightness = brightness; |
||||
} |
||||
} |
||||
} |
||||
|
||||
void Device::updateWakefulness(const UIState &s) { |
||||
bool ignition_just_turned_off = !s.scene.ignition && ignition_on; |
||||
ignition_on = s.scene.ignition; |
||||
|
||||
if (ignition_just_turned_off) { |
||||
resetInteractiveTimeout(); |
||||
} else if (interactive_timeout > 0 && --interactive_timeout == 0) { |
||||
emit interactiveTimeout(); |
||||
} |
||||
|
||||
setAwake(s.scene.ignition || interactive_timeout > 0); |
||||
} |
||||
|
||||
UIState *uiState() { |
||||
static UIState ui_state; |
||||
return &ui_state; |
||||
} |
||||
|
||||
Device *device() { |
||||
static Device _device; |
||||
return &_device; |
||||
} |
||||
@ -1,132 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <eigen3/Eigen/Dense> |
||||
#include <memory> |
||||
#include <string> |
||||
|
||||
#include <QTimer> |
||||
#include <QColor> |
||||
#include <QFuture> |
||||
|
||||
#include "cereal/messaging/messaging.h" |
||||
#include "common/mat.h" |
||||
#include "common/params.h" |
||||
#include "common/util.h" |
||||
#include "system/hardware/hw.h" |
||||
#include "selfdrive/ui/qt/prime_state.h" |
||||
|
||||
const int UI_BORDER_SIZE = 30; |
||||
const int UI_HEADER_HEIGHT = 420; |
||||
|
||||
const int UI_FREQ = 20; // Hz
|
||||
const int BACKLIGHT_OFFROAD = 50; |
||||
|
||||
const Eigen::Matrix3f VIEW_FROM_DEVICE = (Eigen::Matrix3f() << |
||||
0.0, 1.0, 0.0, |
||||
0.0, 0.0, 1.0, |
||||
1.0, 0.0, 0.0).finished(); |
||||
|
||||
const Eigen::Matrix3f FCAM_INTRINSIC_MATRIX = (Eigen::Matrix3f() << |
||||
2648.0, 0.0, 1928.0 / 2, |
||||
0.0, 2648.0, 1208.0 / 2, |
||||
0.0, 0.0, 1.0).finished(); |
||||
|
||||
// tici ecam focal probably wrong? magnification is not consistent across frame
|
||||
// Need to retrain model before this can be changed
|
||||
const Eigen::Matrix3f ECAM_INTRINSIC_MATRIX = (Eigen::Matrix3f() << |
||||
567.0, 0.0, 1928.0 / 2, |
||||
0.0, 567.0, 1208.0 / 2, |
||||
0.0, 0.0, 1.0).finished(); |
||||
|
||||
typedef enum UIStatus { |
||||
STATUS_DISENGAGED, |
||||
STATUS_OVERRIDE, |
||||
STATUS_ENGAGED, |
||||
} UIStatus; |
||||
|
||||
const QColor bg_colors [] = { |
||||
[STATUS_DISENGAGED] = QColor(0x17, 0x33, 0x49, 0xc8), |
||||
[STATUS_OVERRIDE] = QColor(0x91, 0x9b, 0x95, 0xf1), |
||||
[STATUS_ENGAGED] = QColor(0x17, 0x86, 0x44, 0xf1), |
||||
}; |
||||
|
||||
typedef struct UIScene { |
||||
Eigen::Matrix3f view_from_calib = VIEW_FROM_DEVICE; |
||||
Eigen::Matrix3f view_from_wide_calib = VIEW_FROM_DEVICE; |
||||
cereal::PandaState::PandaType pandaType; |
||||
|
||||
cereal::LongitudinalPersonality personality; |
||||
|
||||
float light_sensor = -1; |
||||
bool started, ignition, is_metric, recording_audio; |
||||
uint64_t started_frame; |
||||
} UIScene; |
||||
|
||||
class UIState : public QObject { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
UIState(QObject* parent = 0); |
||||
void updateStatus(); |
||||
inline bool engaged() const { |
||||
return scene.started && (*sm)["selfdriveState"].getSelfdriveState().getEnabled(); |
||||
} |
||||
|
||||
std::unique_ptr<SubMaster> sm; |
||||
UIStatus status; |
||||
UIScene scene = {}; |
||||
QString language; |
||||
PrimeState *prime_state; |
||||
|
||||
signals: |
||||
void uiUpdate(const UIState &s); |
||||
void offroadTransition(bool offroad); |
||||
void engagedChanged(bool engaged); |
||||
|
||||
private slots: |
||||
void update(); |
||||
|
||||
private: |
||||
QTimer *timer; |
||||
bool started_prev = false; |
||||
bool engaged_prev = false; |
||||
}; |
||||
|
||||
UIState *uiState(); |
||||
|
||||
// device management class
|
||||
class Device : public QObject { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
Device(QObject *parent = 0); |
||||
bool isAwake() { return awake; } |
||||
void setOffroadBrightness(int brightness) { |
||||
offroad_brightness = std::clamp(brightness, 0, 100); |
||||
} |
||||
|
||||
private: |
||||
bool awake = false; |
||||
int interactive_timeout = 0; |
||||
bool ignition_on = false; |
||||
|
||||
int offroad_brightness = BACKLIGHT_OFFROAD; |
||||
int last_brightness = 0; |
||||
FirstOrderFilter brightness_filter; |
||||
QFuture<void> brightness_future; |
||||
|
||||
void updateBrightness(const UIState &s); |
||||
void updateWakefulness(const UIState &s); |
||||
void setAwake(bool on); |
||||
|
||||
signals: |
||||
void displayPowerChanged(bool on); |
||||
void interactiveTimeout(); |
||||
|
||||
public slots: |
||||
void resetInteractiveTimeout(int timeout = -1); |
||||
void update(const UIState &s); |
||||
}; |
||||
|
||||
Device *device(); |
||||
void ui_update_params(UIState *s); |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue