mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-08-23 02:23:47 +08:00
Merge remote-tracking branch 'refs/remotes/openpilot/master' into small-sync
# Conflicts: # cereal # opendbc # panda # selfdrive/car/__init__.py # selfdrive/monitoring/dmonitoringd.py
This commit is contained in:
@@ -26,7 +26,7 @@ cabana_env.Command(assets, assets_src, f"rcc $SOURCES -o $TARGET")
|
||||
cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, assets_src, "assets/assets.o"]))
|
||||
|
||||
cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/socketcanstream.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc',
|
||||
'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc',
|
||||
'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc',
|
||||
'utils/export.cc', 'utils/util.cc',
|
||||
'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc',
|
||||
'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks)
|
||||
|
||||
+10
-22
@@ -3,7 +3,6 @@
|
||||
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
#include "tools/cabana/mainwin.h"
|
||||
#include "tools/cabana/streamselector.h"
|
||||
#include "tools/cabana/streams/devicestream.h"
|
||||
#include "tools/cabana/streams/pandastream.h"
|
||||
#include "tools/cabana/streams/replaystream.h"
|
||||
@@ -82,28 +81,17 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
int ret = 0;
|
||||
{
|
||||
MainWindow w;
|
||||
QTimer::singleShot(0, [&]() {
|
||||
if (!stream) {
|
||||
StreamSelector dlg(&stream);
|
||||
dlg.exec();
|
||||
dbc_file = dlg.dbcFile();
|
||||
}
|
||||
if (!stream) {
|
||||
stream = new DummyStream(&app);
|
||||
}
|
||||
stream->start();
|
||||
if (!dbc_file.isEmpty()) {
|
||||
w.loadFile(dbc_file);
|
||||
}
|
||||
w.show();
|
||||
});
|
||||
|
||||
ret = app.exec();
|
||||
MainWindow w;
|
||||
if (stream) {
|
||||
stream->start();
|
||||
if (!dbc_file.isEmpty()) {
|
||||
w.loadFile(dbc_file);
|
||||
}
|
||||
} else {
|
||||
w.openStream();
|
||||
}
|
||||
|
||||
w.show();
|
||||
int ret = app.exec();
|
||||
delete can;
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
// ChartAxisElement's padding is 4 (https://codebrowser.dev/qt5/qtcharts/src/charts/axis/chartaxiselement_p.h.html)
|
||||
const int AXIS_X_TOP_MARGIN = 4;
|
||||
const double MIN_ZOOM_SECONDS = 0.01; // 10ms
|
||||
// Define a small value of epsilon to compare double values
|
||||
const float EPSILON = 0.000001;
|
||||
static inline bool xLessThan(const QPointF &p, float x) { return p.x() < (x - EPSILON); }
|
||||
@@ -511,7 +512,7 @@ void ChartView::mouseReleaseEvent(QMouseEvent *event) {
|
||||
if (rubber->width() <= 0) {
|
||||
// no rubber dragged, seek to mouse position
|
||||
can->seekTo(min);
|
||||
} else if (rubber->width() > 10 && (max - min) > 0.01) { // Minimum range is 10 milliseconds.
|
||||
} else if (rubber->width() > 10 && (max - min) > MIN_ZOOM_SECONDS) {
|
||||
charts_widget->zoom_undo_stack->push(new ZoomCommand(charts_widget, {min, max}));
|
||||
} else {
|
||||
viewport()->update();
|
||||
|
||||
@@ -197,7 +197,8 @@ void ChartsWidget::updateState() {
|
||||
display_range.second = display_range.first + max_chart_range;
|
||||
} else if (cur_sec < (zoomed_range.first - 0.1) || cur_sec >= zoomed_range.second) {
|
||||
// loop in zoomed range
|
||||
can->seekTo(zoomed_range.first);
|
||||
QTimer::singleShot(0, [ts = zoomed_range.first]() { can->seekTo(ts);});
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &range = is_zoomed ? zoomed_range : display_range;
|
||||
|
||||
@@ -262,6 +262,9 @@ void MainWindow::openStream() {
|
||||
}
|
||||
stream->start();
|
||||
statusBar()->showMessage(tr("Route %1 loaded").arg(can->routeName()), 2000);
|
||||
} else if (!can) {
|
||||
stream = new DummyStream(this);
|
||||
stream->start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -138,13 +138,16 @@ void AbstractStream::updateLastMsgsTo(double sec) {
|
||||
auto prev = std::prev(it);
|
||||
double ts = (*prev)->mono_time / 1e9 - routeStartTime();
|
||||
auto &m = msgs[id];
|
||||
double freq = 0;
|
||||
// Keep suppressed bits.
|
||||
if (auto old_m = messages_.find(id); old_m != messages_.end()) {
|
||||
freq = old_m->second.freq;
|
||||
m.last_changes.reserve(old_m->second.last_changes.size());
|
||||
std::transform(old_m->second.last_changes.cbegin(), old_m->second.last_changes.cend(),
|
||||
std::back_inserter(m.last_changes),
|
||||
[](const auto &change) { return CanData::ByteLastChange{.suppressed = change.suppressed}; });
|
||||
}
|
||||
m.compute(id, (*prev)->dat, (*prev)->size, ts, getSpeed(), {});
|
||||
m.compute(id, (*prev)->dat, (*prev)->size, ts, getSpeed(), {}, freq);
|
||||
m.count = std::distance(ev.begin(), prev) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ public:
|
||||
signals:
|
||||
void paused();
|
||||
void resume();
|
||||
void seekingTo(double sec);
|
||||
void seekedTo(double sec);
|
||||
void streamStarted();
|
||||
void eventsMerged(const MessageEventsMap &events_map);
|
||||
@@ -107,6 +108,7 @@ protected:
|
||||
uint64_t lastEventMonoTime() const { return lastest_event_ts; }
|
||||
|
||||
std::vector<const CanEvent *> all_events_;
|
||||
double current_sec_ = 0;
|
||||
uint64_t lastest_event_ts = 0;
|
||||
|
||||
private:
|
||||
@@ -114,7 +116,6 @@ private:
|
||||
void updateLastMsgsTo(double sec);
|
||||
void updateMasks();
|
||||
|
||||
double current_sec_ = 0;
|
||||
MessageEventsMap events_;
|
||||
std::unordered_map<MessageId, CanData> last_msgs;
|
||||
std::unique_ptr<MonotonicBuffer> event_buffer_;
|
||||
|
||||
@@ -6,39 +6,12 @@
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QThread>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
// TODO: remove clearLayout
|
||||
static void clearLayout(QLayout* layout) {
|
||||
while (layout->count() > 0) {
|
||||
QLayoutItem* item = layout->takeAt(0);
|
||||
if (QWidget* widget = item->widget()) {
|
||||
widget->deleteLater();
|
||||
}
|
||||
if (QLayout* childLayout = item->layout()) {
|
||||
clearLayout(childLayout);
|
||||
}
|
||||
delete item;
|
||||
}
|
||||
}
|
||||
|
||||
PandaStream::PandaStream(QObject *parent, PandaStreamConfig config_) : config(config_), LiveStream(parent) {
|
||||
if (config.serial.isEmpty()) {
|
||||
auto serials = Panda::list();
|
||||
if (serials.size() == 0) {
|
||||
throw std::runtime_error("No panda found");
|
||||
}
|
||||
config.serial = QString::fromStdString(serials[0]);
|
||||
}
|
||||
|
||||
qDebug() << "Connecting to panda with serial" << config.serial;
|
||||
if (!connect()) {
|
||||
throw std::runtime_error("Failed to connect to panda");
|
||||
}
|
||||
}
|
||||
PandaStream::PandaStream(QObject *parent, PandaStreamConfig config_) : config(config_), LiveStream(parent) {}
|
||||
|
||||
bool PandaStream::connect() {
|
||||
try {
|
||||
qDebug() << "Connecting to panda with serial" << config.serial;
|
||||
panda.reset(new Panda(config.serial.toStdString()));
|
||||
config.bus_config.resize(3);
|
||||
qDebug() << "Connected";
|
||||
@@ -47,7 +20,6 @@ bool PandaStream::connect() {
|
||||
}
|
||||
|
||||
panda->set_safety_model(cereal::CarParams::SafetyModel::SILENT);
|
||||
|
||||
for (int bus = 0; bus < config.bus_config.size(); bus++) {
|
||||
panda->set_can_speed_kbps(bus, config.bus_config[bus].can_speed_kbps);
|
||||
|
||||
@@ -60,7 +32,6 @@ bool PandaStream::connect() {
|
||||
panda->set_data_speed_kbps(bus, 10);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -108,26 +79,14 @@ AbstractOpenStreamWidget *PandaStream::widget(AbstractStream **stream) {
|
||||
// OpenPandaWidget
|
||||
|
||||
OpenPandaWidget::OpenPandaWidget(AbstractStream **stream) : AbstractOpenStreamWidget(stream) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
main_layout->addStretch(1);
|
||||
|
||||
QFormLayout *form_layout = new QFormLayout();
|
||||
|
||||
form_layout = new QFormLayout(this);
|
||||
QHBoxLayout *serial_layout = new QHBoxLayout();
|
||||
serial_edit = new QComboBox();
|
||||
serial_edit->setFixedWidth(300);
|
||||
serial_layout->addWidget(serial_edit);
|
||||
serial_layout->addWidget(serial_edit = new QComboBox());
|
||||
|
||||
QPushButton *refresh = new QPushButton(tr("Refresh"));
|
||||
refresh->setFixedWidth(100);
|
||||
refresh->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
|
||||
serial_layout->addWidget(refresh);
|
||||
form_layout->addRow(tr("Serial"), serial_layout);
|
||||
main_layout->addLayout(form_layout);
|
||||
|
||||
config_layout = new QFormLayout();
|
||||
main_layout->addLayout(config_layout);
|
||||
|
||||
main_layout->addStretch(1);
|
||||
|
||||
QObject::connect(refresh, &QPushButton::clicked, this, &OpenPandaWidget::refreshSerials);
|
||||
QObject::connect(serial_edit, &QComboBox::currentTextChanged, this, &OpenPandaWidget::buildConfigForm);
|
||||
@@ -145,15 +104,16 @@ void OpenPandaWidget::refreshSerials() {
|
||||
}
|
||||
|
||||
void OpenPandaWidget::buildConfigForm() {
|
||||
clearLayout(config_layout);
|
||||
QString serial = serial_edit->currentText();
|
||||
for (int i = form_layout->rowCount() - 1; i > 0; --i) {
|
||||
form_layout->removeRow(i);
|
||||
}
|
||||
|
||||
QString serial = serial_edit->currentText();
|
||||
bool has_fd = false;
|
||||
bool has_panda = !serial.isEmpty();
|
||||
|
||||
if (has_panda) {
|
||||
try {
|
||||
Panda panda = Panda(serial.toStdString());
|
||||
Panda panda(serial.toStdString());
|
||||
has_fd = (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA) || (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA_V2);
|
||||
} catch (const std::exception& e) {
|
||||
has_panda = false;
|
||||
@@ -201,20 +161,22 @@ void OpenPandaWidget::buildConfigForm() {
|
||||
QObject::connect(enable_fd, &QCheckBox::stateChanged, [=](int state) {config.bus_config[i].can_fd = (bool)state;});
|
||||
}
|
||||
|
||||
config_layout->addRow(tr("Bus %1:").arg(i), bus_layout);
|
||||
form_layout->addRow(tr("Bus %1:").arg(i), bus_layout);
|
||||
}
|
||||
} else {
|
||||
config.serial = "";
|
||||
config_layout->addWidget(new QLabel(tr("No panda found")));
|
||||
form_layout->addWidget(new QLabel(tr("No panda found")));
|
||||
}
|
||||
}
|
||||
|
||||
bool OpenPandaWidget::open() {
|
||||
try {
|
||||
*stream = new PandaStream(qApp, config);
|
||||
} catch (std::exception &e) {
|
||||
QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to panda: '%1'").arg(e.what()));
|
||||
return false;
|
||||
if (!config.serial.isEmpty()) {
|
||||
auto panda_stream = std::make_unique<PandaStream>(qApp, config);
|
||||
if (panda_stream->connect()) {
|
||||
*stream = panda_stream.release();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to panda"));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ class PandaStream : public LiveStream {
|
||||
Q_OBJECT
|
||||
public:
|
||||
PandaStream(QObject *parent, PandaStreamConfig config_ = {});
|
||||
bool connect();
|
||||
static AbstractOpenStreamWidget *widget(AbstractStream **stream);
|
||||
inline QString routeName() const override {
|
||||
return QString("Live Streaming From Panda %1").arg(config.serial);
|
||||
@@ -28,7 +29,6 @@ public:
|
||||
|
||||
protected:
|
||||
void streamThread() override;
|
||||
bool connect();
|
||||
|
||||
std::unique_ptr<Panda> panda;
|
||||
PandaStreamConfig config = {};
|
||||
@@ -47,6 +47,6 @@ private:
|
||||
void buildConfigForm();
|
||||
|
||||
QComboBox *serial_edit;
|
||||
QFormLayout *config_layout;
|
||||
QFormLayout *form_layout;
|
||||
PandaStreamConfig config = {};
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QPushButton>
|
||||
|
||||
#include "common/timing.h"
|
||||
#include "tools/cabana/streams/routes.h"
|
||||
|
||||
ReplayStream::ReplayStream(QObject *parent) : AbstractStream(parent) {
|
||||
unsetenv("ZMQ");
|
||||
@@ -84,6 +85,16 @@ bool ReplayStream::eventFilter(const Event *event) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void ReplayStream::seekTo(double ts) {
|
||||
// Update timestamp and notify receivers of the time change.
|
||||
current_sec_ = ts;
|
||||
std::set<MessageId> new_msgs;
|
||||
msgsReceived(&new_msgs, false);
|
||||
|
||||
// Seek to the specified timestamp
|
||||
replay->seekTo(std::max(double(0), ts), false);
|
||||
}
|
||||
|
||||
void ReplayStream::pause(bool pause) {
|
||||
replay->pause(pause);
|
||||
emit(pause ? paused() : resume());
|
||||
@@ -97,29 +108,36 @@ AbstractOpenStreamWidget *ReplayStream::widget(AbstractStream **stream) {
|
||||
// OpenReplayWidget
|
||||
|
||||
OpenReplayWidget::OpenReplayWidget(AbstractStream **stream) : AbstractOpenStreamWidget(stream) {
|
||||
// TODO: get route list from api.comma.ai
|
||||
QGridLayout *grid_layout = new QGridLayout(this);
|
||||
grid_layout->addWidget(new QLabel(tr("Route")), 0, 0);
|
||||
grid_layout->addWidget(route_edit = new QLineEdit(this), 0, 1);
|
||||
route_edit->setPlaceholderText(tr("Enter remote route name or click browse to select a local route"));
|
||||
auto file_btn = new QPushButton(tr("Browse..."), this);
|
||||
grid_layout->addWidget(file_btn, 0, 2);
|
||||
route_edit->setPlaceholderText(tr("Enter route name or browse for local/remote route"));
|
||||
auto browse_remote_btn = new QPushButton(tr("Remote route..."), this);
|
||||
grid_layout->addWidget(browse_remote_btn, 0, 2);
|
||||
auto browse_local_btn = new QPushButton(tr("Local route..."), this);
|
||||
grid_layout->addWidget(browse_local_btn, 0, 3);
|
||||
|
||||
grid_layout->addWidget(new QLabel(tr("Camera")), 1, 0);
|
||||
QHBoxLayout *camera_layout = new QHBoxLayout();
|
||||
for (auto c : {tr("Road camera"), tr("Driver camera"), tr("Wide road camera")})
|
||||
camera_layout->addWidget(cameras.emplace_back(new QCheckBox(c, this)));
|
||||
cameras[0]->setChecked(true);
|
||||
camera_layout->addStretch(1);
|
||||
grid_layout->addItem(camera_layout, 1, 1);
|
||||
|
||||
setMinimumWidth(550);
|
||||
QObject::connect(file_btn, &QPushButton::clicked, [=]() {
|
||||
QObject::connect(browse_local_btn, &QPushButton::clicked, [=]() {
|
||||
QString dir = QFileDialog::getExistingDirectory(this, tr("Open Local Route"), settings.last_route_dir);
|
||||
if (!dir.isEmpty()) {
|
||||
route_edit->setText(dir);
|
||||
settings.last_route_dir = QFileInfo(dir).absolutePath();
|
||||
}
|
||||
});
|
||||
QObject::connect(browse_remote_btn, &QPushButton::clicked, [this]() {
|
||||
RoutesDialog route_dlg(this);
|
||||
if (route_dlg.exec()) {
|
||||
route_edit->setText(route_dlg.route());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool OpenReplayWidget::open() {
|
||||
|
||||
@@ -18,7 +18,7 @@ public:
|
||||
void start() override;
|
||||
bool loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags = REPLAY_FLAG_NONE);
|
||||
bool eventFilter(const Event *event);
|
||||
void seekTo(double ts) override { replay->seekTo(std::max(double(0), ts), false); }
|
||||
void seekTo(double ts) override;
|
||||
bool liveStreaming() const override { return false; }
|
||||
inline QString routeName() const override { return replay->route()->name(); }
|
||||
inline QString carFingerprint() const override { return replay->carFingerprint().c_str(); }
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "tools/cabana/streams/routes.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QListWidget>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
|
||||
#include "system/hardware/hw.h"
|
||||
|
||||
// The RouteListWidget class extends QListWidget to display a custom message when empty
|
||||
class RouteListWidget : public QListWidget {
|
||||
public:
|
||||
RouteListWidget(QWidget *parent = nullptr) : QListWidget(parent) {}
|
||||
void setEmptyText(const QString &text) {
|
||||
empty_text_ = text;
|
||||
viewport()->update();
|
||||
}
|
||||
void paintEvent(QPaintEvent *event) override {
|
||||
QListWidget::paintEvent(event);
|
||||
if (count() == 0) {
|
||||
QPainter painter(viewport());
|
||||
painter.drawText(viewport()->rect(), Qt::AlignCenter, empty_text_);
|
||||
}
|
||||
}
|
||||
QString empty_text_ = tr("No items");
|
||||
};
|
||||
|
||||
RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent) {
|
||||
setWindowTitle(tr("Remote routes"));
|
||||
|
||||
QFormLayout *layout = new QFormLayout(this);
|
||||
layout->addRow(tr("Device"), device_list_ = new QComboBox(this));
|
||||
layout->addRow(tr("Duration"), period_selector_ = new QComboBox(this));
|
||||
layout->addRow(route_list_ = new RouteListWidget(this));
|
||||
auto button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
layout->addRow(button_box);
|
||||
|
||||
device_list_->addItem(tr("Loading..."));
|
||||
// Populate period selector with predefined durations
|
||||
period_selector_->addItem(tr("Last week"), 7);
|
||||
period_selector_->addItem(tr("Last 2 weeks"), 14);
|
||||
period_selector_->addItem(tr("Last month"), 30);
|
||||
period_selector_->addItem(tr("Last 6 months"), 180);
|
||||
|
||||
// Connect signals and slots
|
||||
connect(device_list_, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RoutesDialog::fetchRoutes);
|
||||
connect(period_selector_, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RoutesDialog::fetchRoutes);
|
||||
connect(route_list_, &QListWidget::itemDoubleClicked, this, &QDialog::accept);
|
||||
QObject::connect(button_box, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
QObject::connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
// Send request to fetch devices
|
||||
HttpRequest *http = new HttpRequest(this, !Hardware::PC());
|
||||
QObject::connect(http, &HttpRequest::requestDone, this, &RoutesDialog::parseDeviceList);
|
||||
http->sendRequest(CommaApi::BASE_URL + "/v1/me/devices/");
|
||||
}
|
||||
|
||||
void RoutesDialog::parseDeviceList(const QString &json, bool success, QNetworkReply::NetworkError err) {
|
||||
if (success) {
|
||||
device_list_->clear();
|
||||
auto devices = QJsonDocument::fromJson(json.toUtf8()).array();
|
||||
for (const QJsonValue &device : devices) {
|
||||
QString dongle_id = device["dongle_id"].toString();
|
||||
device_list_->addItem(dongle_id, dongle_id);
|
||||
}
|
||||
} else {
|
||||
bool unauthorized = (err == QNetworkReply::ContentAccessDenied || err == QNetworkReply::AuthenticationRequiredError);
|
||||
QMessageBox::warning(this, tr("Error"), unauthorized ? tr("Unauthorized, Authenticate with tools/lib/auth.py") : tr("Network error"));
|
||||
reject();
|
||||
}
|
||||
sender()->deleteLater();
|
||||
}
|
||||
|
||||
void RoutesDialog::fetchRoutes() {
|
||||
if (device_list_->currentIndex() == -1 || device_list_->currentData().isNull())
|
||||
return;
|
||||
|
||||
route_list_->clear();
|
||||
route_list_->setEmptyText(tr("Loading..."));
|
||||
|
||||
HttpRequest *http = new HttpRequest(this, !Hardware::PC());
|
||||
QObject::connect(http, &HttpRequest::requestDone, this, &RoutesDialog::parseRouteList);
|
||||
|
||||
// Construct URL with selected device and date range
|
||||
auto dongle_id = device_list_->currentData().toString();
|
||||
QDateTime current = QDateTime::currentDateTime();
|
||||
QString url = QString("%1/v1/devices/%2/routes_segments?start=%3&end=%4")
|
||||
.arg(CommaApi::BASE_URL).arg(dongle_id)
|
||||
.arg(current.addDays(-(period_selector_->currentData().toInt())).toMSecsSinceEpoch())
|
||||
.arg(current.toMSecsSinceEpoch());
|
||||
http->sendRequest(url);
|
||||
}
|
||||
|
||||
void RoutesDialog::parseRouteList(const QString &json, bool success, QNetworkReply::NetworkError err) {
|
||||
if (success) {
|
||||
for (const QJsonValue &route : QJsonDocument::fromJson(json.toUtf8()).array()) {
|
||||
uint64_t start_time = route["start_time_utc_millis"].toDouble();
|
||||
uint64_t end_time = route["end_time_utc_millis"].toDouble();
|
||||
auto datetime = QDateTime::fromMSecsSinceEpoch(start_time);
|
||||
auto item = new QListWidgetItem(QString("%1 %2min").arg(datetime.toString()).arg((end_time - start_time) / (1000 * 60)));
|
||||
item->setData(Qt::UserRole, route["fullname"].toString());
|
||||
route_list_->addItem(item);
|
||||
}
|
||||
// Select first route if available
|
||||
if (route_list_->count() > 0) route_list_->setCurrentRow(0);
|
||||
} else {
|
||||
QMessageBox::warning(this, tr("Error"), tr("Failed to fetch routes. Check your network connection."));
|
||||
reject();
|
||||
}
|
||||
route_list_->setEmptyText(tr("No items"));
|
||||
sender()->deleteLater();
|
||||
}
|
||||
|
||||
void RoutesDialog::accept() {
|
||||
if (auto current_item = route_list_->currentItem()) {
|
||||
route_ = current_item->data(Qt::UserRole).toString();
|
||||
}
|
||||
QDialog::accept();
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
|
||||
#include "selfdrive/ui/qt/api.h"
|
||||
|
||||
class RouteListWidget;
|
||||
|
||||
class RoutesDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
RoutesDialog(QWidget *parent);
|
||||
QString route() const { return route_; }
|
||||
|
||||
protected:
|
||||
void accept() override;
|
||||
void parseDeviceList(const QString &json, bool success, QNetworkReply::NetworkError err);
|
||||
void parseRouteList(const QString &json, bool success, QNetworkReply::NetworkError err);
|
||||
void fetchRoutes();
|
||||
|
||||
QComboBox *device_list_;
|
||||
QComboBox *period_selector_;
|
||||
RouteListWidget *route_list_;
|
||||
QString route_;
|
||||
};
|
||||
@@ -13,12 +13,8 @@
|
||||
|
||||
StreamSelector::StreamSelector(AbstractStream **stream, QWidget *parent) : QDialog(parent) {
|
||||
setWindowTitle(tr("Open stream"));
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
|
||||
QWidget *w = new QWidget(this);
|
||||
QVBoxLayout *layout = new QVBoxLayout(w);
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
tab = new QTabWidget(this);
|
||||
tab->setTabBarAutoHide(true);
|
||||
layout->addWidget(tab);
|
||||
|
||||
QHBoxLayout *dbc_layout = new QHBoxLayout();
|
||||
@@ -35,9 +31,8 @@ StreamSelector::StreamSelector(AbstractStream **stream, QWidget *parent) : QDial
|
||||
line->setFrameStyle(QFrame::HLine | QFrame::Sunken);
|
||||
layout->addWidget(line);
|
||||
|
||||
main_layout->addWidget(w);
|
||||
auto btn_box = new QDialogButtonBox(QDialogButtonBox::Open | QDialogButtonBox::Cancel);
|
||||
main_layout->addWidget(btn_box);
|
||||
layout->addWidget(btn_box);
|
||||
|
||||
addStreamWidget(ReplayStream::widget(stream));
|
||||
addStreamWidget(PandaStream::widget(stream));
|
||||
@@ -48,14 +43,11 @@ StreamSelector::StreamSelector(AbstractStream **stream, QWidget *parent) : QDial
|
||||
|
||||
QObject::connect(btn_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
QObject::connect(btn_box, &QDialogButtonBox::accepted, [=]() {
|
||||
btn_box->button(QDialogButtonBox::Open)->setEnabled(false);
|
||||
w->setEnabled(false);
|
||||
setEnabled(false);
|
||||
if (((AbstractOpenStreamWidget *)tab->currentWidget())->open()) {
|
||||
accept();
|
||||
} else {
|
||||
btn_box->button(QDialogButtonBox::Open)->setEnabled(true);
|
||||
w->setEnabled(true);
|
||||
}
|
||||
setEnabled(true);
|
||||
});
|
||||
QObject::connect(file_btn, &QPushButton::clicked, [this]() {
|
||||
QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), settings.last_dir, "DBC (*.dbc)");
|
||||
|
||||
@@ -4,67 +4,37 @@ import argparse
|
||||
from collections import defaultdict
|
||||
from openpilot.selfdrive.debug.format_fingerprints import format_brand_fw_versions
|
||||
|
||||
from openpilot.selfdrive.car.fw_versions import match_fw_to_car
|
||||
from openpilot.selfdrive.car.interfaces import get_interface_attr
|
||||
from openpilot.selfdrive.car.fingerprints import MIGRATION
|
||||
from openpilot.selfdrive.car.fw_versions import MODEL_TO_BRAND, match_fw_to_car
|
||||
from openpilot.tools.lib.logreader import LogReader, ReadMode
|
||||
|
||||
|
||||
ALL_FW_VERSIONS = get_interface_attr("FW_VERSIONS")
|
||||
ALL_CARS = get_interface_attr("CAR")
|
||||
|
||||
PLATFORM_TO_PYTHON_CAR_NAME = {brand: {car.value: car.name for car in ALL_CARS[brand]} for brand in ALL_CARS}
|
||||
BRAND_TO_PLATFORMS = {brand: [car.value for car in ALL_CARS[brand]] for brand in ALL_CARS}
|
||||
PLATFORM_TO_BRAND = dict(sum([[(platform, brand) for platform in BRAND_TO_PLATFORMS[brand]] for brand in BRAND_TO_PLATFORMS], []))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Auto fingerprint from a route")
|
||||
parser.add_argument("route", help="The route name to use")
|
||||
parser.add_argument("platform", help="The platform, or leave empty to auto-determine using fuzzy", default=None, nargs='?')
|
||||
parser.add_argument("platform", help="The platform, or leave empty to auto-determine using fuzzy", default=None, nargs="?")
|
||||
args = parser.parse_args()
|
||||
|
||||
lr = LogReader(args.route, ReadMode.QLOG)
|
||||
|
||||
carFw = None
|
||||
carVin = None
|
||||
carPlatform = None
|
||||
|
||||
platform: str | None = None
|
||||
|
||||
CP = lr.first("carParams")
|
||||
assert CP is not None, "No carParams in route"
|
||||
|
||||
if CP is None:
|
||||
raise Exception("No fw versions in the provided route...")
|
||||
carPlatform = MIGRATION.get(CP.carFingerprint, CP.carFingerprint)
|
||||
|
||||
carFw = CP.carFw
|
||||
carVin = CP.carVin
|
||||
carPlatform = CP.carFingerprint
|
||||
|
||||
if args.platform is None: # attempt to auto-determine platform with other fuzzy fingerprints
|
||||
_, possible_platforms = match_fw_to_car(carFw, carVin, log=False)
|
||||
|
||||
if len(possible_platforms) != 1:
|
||||
print(f"Unable to auto-determine platform, possible platforms: {possible_platforms}")
|
||||
|
||||
if carPlatform != "MOCK":
|
||||
print("Using platform from route")
|
||||
platform = carPlatform
|
||||
else:
|
||||
platform = None
|
||||
else:
|
||||
platform = list(possible_platforms)[0]
|
||||
else:
|
||||
if args.platform is not None:
|
||||
platform = args.platform
|
||||
elif carPlatform != "MOCK":
|
||||
platform = carPlatform
|
||||
else:
|
||||
_, matches = match_fw_to_car(CP.carFw, CP.carVin, log=False)
|
||||
assert len(matches) == 1, f"Unable to auto-determine platform, matches: {matches}"
|
||||
platform = list(matches)[0]
|
||||
|
||||
if platform is None:
|
||||
raise Exception("unable to determine platform, try manually specifying the fingerprint.")
|
||||
|
||||
print("Attempting to add fw version for: ", platform)
|
||||
print("Attempting to add fw version for:", platform)
|
||||
|
||||
fw_versions: dict[str, dict[tuple, list[bytes]]] = defaultdict(lambda: defaultdict(list))
|
||||
brand = PLATFORM_TO_BRAND[platform]
|
||||
brand = MODEL_TO_BRAND[platform]
|
||||
|
||||
for fw in carFw:
|
||||
for fw in CP.carFw:
|
||||
if fw.brand == brand and not fw.logging:
|
||||
addr = fw.address
|
||||
subAddr = None if fw.subAddress == 0 else fw.subAddress
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"from openpilot.selfdrive.car.subaru.values import CAR, SubaruFlags\n",
|
||||
"from openpilot.selfdrive.car.subaru.fingerprints import FW_VERSIONS\n",
|
||||
"\n",
|
||||
"TEST_PLATFORMS = CAR.without_flags(SubaruFlags.PREGLOBAL)\n",
|
||||
"TEST_PLATFORMS = set(CAR) - CAR.with_flags(SubaruFlags.PREGLOBAL)\n",
|
||||
"\n",
|
||||
"Ecu = car.CarParams.Ecu\n",
|
||||
"\n",
|
||||
|
||||
@@ -12,7 +12,7 @@ def create_test_models_suite(routes: list[CarTestRoute], ci=False) -> unittest.T
|
||||
test_suite = unittest.TestSuite()
|
||||
for test_route in routes:
|
||||
# create new test case and discover tests
|
||||
test_case_args = {"car_model": test_route.car_model, "test_route": test_route, "ci": ci}
|
||||
test_case_args = {"platform": test_route.car_model, "test_route": test_route, "test_route_on_bucket": ci}
|
||||
CarModelTestCase = type("CarModelTestCase", (TestCarModel,), test_case_args)
|
||||
test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(CarModelTestCase))
|
||||
return test_suite
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import json
|
||||
import base64
|
||||
import os
|
||||
import subprocess
|
||||
from multiprocessing import Pool
|
||||
from openpilot.tools.lib.route import Route
|
||||
from openpilot.tools.lib.logreader import LogReader
|
||||
|
||||
try:
|
||||
from mcap.writer import Writer, CompressionType
|
||||
except ImportError:
|
||||
print("mcap module not found. Attempting to install...")
|
||||
subprocess.run([sys.executable, "-m", "pip", "install", "mcap"])
|
||||
# Attempt to import again after installation
|
||||
try:
|
||||
from mcap.writer import Writer, CompressionType
|
||||
except ImportError:
|
||||
print("Failed to install mcap module. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
FOXGLOVE_IMAGE_SCHEME_TITLE = "foxglove.CompressedImage"
|
||||
FOXGLOVE_GEOJSON_TITLE = "foxglove.GeoJSON"
|
||||
FOXGLOVE_IMAGE_ENCODING = "base64"
|
||||
OUT_MCAP_FILE_NAME = "json_log.mcap"
|
||||
RLOG_FOLDER = "rlogs"
|
||||
SCHEMAS_FOLDER = "schemas"
|
||||
SCHEMA_EXTENSION = ".json"
|
||||
|
||||
schemas: dict[str, int] = {}
|
||||
channels: dict[str, int] = {}
|
||||
writer: Writer
|
||||
|
||||
|
||||
def convertBytesToString(data):
|
||||
if isinstance(data, bytes):
|
||||
return data.decode('latin-1') # Assuming UTF-8 encoding, adjust if needed
|
||||
elif isinstance(data, list):
|
||||
return [convertBytesToString(item) for item in data]
|
||||
elif isinstance(data, dict):
|
||||
return {key: convertBytesToString(value) for key, value in data.items()}
|
||||
else:
|
||||
return data
|
||||
|
||||
|
||||
# Load jsonscheme for every Event
|
||||
def loadSchema(schemaName):
|
||||
with open(os.path.join(SCHEMAS_FOLDER, schemaName + SCHEMA_EXTENSION), "r") as file:
|
||||
return json.loads(file.read())
|
||||
|
||||
|
||||
# Foxglove creates one graph of an array, and not one for each item of an array
|
||||
# This can be avoided by transforming array to separate objects
|
||||
def transformListsToJsonDict(json_data):
|
||||
def convert_array_to_dict(array):
|
||||
new_dict = {}
|
||||
for index, item in enumerate(array):
|
||||
if isinstance(item, dict):
|
||||
new_dict[index] = transformListsToJsonDict(item)
|
||||
else:
|
||||
new_dict[index] = item
|
||||
return new_dict
|
||||
|
||||
new_data = {}
|
||||
for key, value in json_data.items():
|
||||
if isinstance(value, list):
|
||||
new_data[key] = convert_array_to_dict(value)
|
||||
elif isinstance(value, dict):
|
||||
new_data[key] = transformListsToJsonDict(value)
|
||||
else:
|
||||
new_data[key] = value
|
||||
return new_data
|
||||
|
||||
|
||||
# Transform openpilot thumbnail to foxglove compressedImage
|
||||
def transformToFoxgloveSchema(jsonMsg):
|
||||
bytesImgData = jsonMsg.get("thumbnail").get("thumbnail").encode('latin1')
|
||||
base64ImgData = base64.b64encode(bytesImgData)
|
||||
base64_string = base64ImgData.decode('utf-8')
|
||||
foxMsg = {
|
||||
"timestamp": {"sec": "0", "nsec": jsonMsg.get("logMonoTime")},
|
||||
"frame_id": str(jsonMsg.get("thumbnail").get("frameId")),
|
||||
"data": base64_string,
|
||||
"format": "jpeg",
|
||||
}
|
||||
return foxMsg
|
||||
|
||||
|
||||
# TODO: Check if there is a tool to build GEOJson
|
||||
def transformMapCoordinates(jsonMsg):
|
||||
coordinates = []
|
||||
for jsonCoords in jsonMsg.get("navRoute").get("coordinates"):
|
||||
coordinates.append([jsonCoords.get("longitude"), jsonCoords.get("latitude")])
|
||||
|
||||
# Define the GeoJSON
|
||||
geojson_data = {
|
||||
"type": "FeatureCollection",
|
||||
"features": [{"type": "Feature", "geometry": {"type": "LineString", "coordinates": coordinates}, "logMonoTime": jsonMsg.get("logMonoTime")}],
|
||||
}
|
||||
|
||||
# Create the final JSON with the GeoJSON data encoded as a string
|
||||
geoJson = {"geojson": json.dumps(geojson_data)}
|
||||
|
||||
return geoJson
|
||||
|
||||
|
||||
def jsonToScheme(jsonData):
|
||||
zeroArray = False
|
||||
schema = {"type": "object", "properties": {}, "required": []}
|
||||
for key, value in jsonData.items():
|
||||
if isinstance(value, dict):
|
||||
tempScheme, zeroArray = jsonToScheme(value)
|
||||
if tempScheme == 0:
|
||||
return 0
|
||||
schema["properties"][key] = tempScheme
|
||||
schema["required"].append(key)
|
||||
elif isinstance(value, list):
|
||||
if all(isinstance(item, dict) for item in value) and len(value) > 0: # Handle zero value arrays
|
||||
# Handle array of objects
|
||||
tempScheme, zeroArray = jsonToScheme(value[0])
|
||||
schema["properties"][key] = {"type": "array", "items": tempScheme if value else {}}
|
||||
schema["required"].append(key)
|
||||
else:
|
||||
if len(value) == 0:
|
||||
zeroArray = True
|
||||
# Handle array of primitive types
|
||||
schema["properties"][key] = {"type": "array", "items": {"type": "string"}}
|
||||
schema["required"].append(key)
|
||||
else:
|
||||
typeName = type(value).__name__
|
||||
if typeName == "str":
|
||||
typeName = "string"
|
||||
elif typeName == "bool":
|
||||
typeName = "boolean"
|
||||
elif typeName == "float":
|
||||
typeName = "number"
|
||||
elif typeName == "int":
|
||||
typeName = "integer"
|
||||
schema["properties"][key] = {"type": typeName}
|
||||
schema["required"].append(key)
|
||||
|
||||
return schema, zeroArray
|
||||
|
||||
|
||||
def saveScheme(scheme, schemaFileName):
|
||||
schemaFileName = schemaFileName + SCHEMA_EXTENSION
|
||||
# Create the new schemas folder
|
||||
os.makedirs(SCHEMAS_FOLDER, exist_ok=True)
|
||||
with open(os.path.join(SCHEMAS_FOLDER, schemaFileName), 'w') as json_file:
|
||||
json.dump(convertBytesToString(scheme), json_file)
|
||||
|
||||
|
||||
def convertToFoxGloveFormat(jsonData, rlogTopic):
|
||||
jsonData["title"] = rlogTopic
|
||||
if rlogTopic == "thumbnail":
|
||||
jsonData = transformToFoxgloveSchema(jsonData)
|
||||
jsonData["title"] = FOXGLOVE_IMAGE_SCHEME_TITLE
|
||||
elif rlogTopic == "navRoute":
|
||||
jsonData = transformMapCoordinates(jsonData)
|
||||
jsonData["title"] = FOXGLOVE_GEOJSON_TITLE
|
||||
else:
|
||||
jsonData = transformListsToJsonDict(jsonData)
|
||||
return jsonData
|
||||
|
||||
|
||||
def generateSchemas():
|
||||
listOfDirs = os.listdir(RLOG_FOLDER)
|
||||
# Open every dir in rlogs
|
||||
for directory in listOfDirs:
|
||||
# List every file in every rlog dir
|
||||
dirPath = os.path.join(RLOG_FOLDER, directory)
|
||||
listOfFiles = os.listdir(dirPath)
|
||||
lastIteration = len(listOfFiles)
|
||||
for iteration, file in enumerate(listOfFiles):
|
||||
# Load json data from every file until found one without empty arrays
|
||||
filePath = os.path.join(dirPath, file)
|
||||
with open(filePath, 'r') as jsonFile:
|
||||
jsonData = json.load(jsonFile)
|
||||
scheme, zerroArray = jsonToScheme(jsonData)
|
||||
# If array of len 0 has been found, type of its data can not be parsed, skip to the next log
|
||||
# in search for a non empty array. If there is not an non empty array in logs, put a dummy string type
|
||||
if zerroArray and not iteration == lastIteration - 1:
|
||||
continue
|
||||
title = jsonData.get("title")
|
||||
scheme["title"] = title
|
||||
# Add contentEncoding type, hardcoded in foxglove format
|
||||
if title == FOXGLOVE_IMAGE_SCHEME_TITLE:
|
||||
scheme["properties"]["data"]["contentEncoding"] = FOXGLOVE_IMAGE_ENCODING
|
||||
saveScheme(scheme, directory)
|
||||
break
|
||||
|
||||
|
||||
def downloadLogs(logPaths):
|
||||
segment_counter = 0
|
||||
for logPath in logPaths:
|
||||
segment_counter += 1
|
||||
msg_counter = 1
|
||||
print(segment_counter)
|
||||
rlog = LogReader(logPath)
|
||||
for msg in rlog:
|
||||
jsonMsg = json.loads(json.dumps(convertBytesToString(msg.to_dict())))
|
||||
jsonMsg = convertToFoxGloveFormat(jsonMsg, msg.which())
|
||||
rlog_dir_path = os.path.join(RLOG_FOLDER, msg.which())
|
||||
if not os.path.exists(rlog_dir_path):
|
||||
os.makedirs(rlog_dir_path)
|
||||
file_path = os.path.join(rlog_dir_path, str(segment_counter) + "," + str(msg_counter))
|
||||
with open(file_path, 'w') as json_file:
|
||||
json.dump(jsonMsg, json_file)
|
||||
msg_counter += 1
|
||||
|
||||
|
||||
def getLogMonoTime(jsonMsg):
|
||||
if jsonMsg.get("title") == FOXGLOVE_IMAGE_SCHEME_TITLE:
|
||||
logMonoTime = jsonMsg.get("timestamp").get("nsec")
|
||||
elif jsonMsg.get("title") == FOXGLOVE_GEOJSON_TITLE:
|
||||
logMonoTime = json.loads(jsonMsg.get("geojson")).get("features")[0].get("logMonoTime")
|
||||
else:
|
||||
logMonoTime = jsonMsg.get("logMonoTime")
|
||||
return logMonoTime
|
||||
|
||||
|
||||
def processMsgs(args):
|
||||
msgFile, rlogTopicPath, rlogTopic = args
|
||||
msgFilePath = os.path.join(rlogTopicPath, msgFile)
|
||||
with open(msgFilePath, "r") as file:
|
||||
jsonMsg = json.load(file)
|
||||
logMonoTime = getLogMonoTime(jsonMsg)
|
||||
return {'channel_id': channels[rlogTopic], 'log_time': logMonoTime, 'data': json.dumps(jsonMsg).encode("utf-8"), 'publish_time': logMonoTime}
|
||||
|
||||
|
||||
# Get logs from a path, and convert them into mcap
|
||||
def createMcap(logPaths):
|
||||
print(f"Downloading logs [{len(logPaths)}]")
|
||||
downloadLogs(logPaths)
|
||||
print("Creating schemas")
|
||||
generateSchemas()
|
||||
print("Creating mcap file")
|
||||
|
||||
listOfRlogTopics = os.listdir(RLOG_FOLDER)
|
||||
print(f"Registering schemas and channels [{len(listOfRlogTopics)}]")
|
||||
for counter, rlogTopic in enumerate(listOfRlogTopics):
|
||||
print(counter)
|
||||
schema = loadSchema(rlogTopic)
|
||||
schema_id = writer.register_schema(name=schema.get("title"), encoding="jsonschema", data=json.dumps(schema).encode())
|
||||
schemas[rlogTopic] = schema_id
|
||||
channel_id = writer.register_channel(schema_id=schemas[rlogTopic], topic=rlogTopic, message_encoding="json")
|
||||
channels[rlogTopic] = channel_id
|
||||
rlogTopicPath = os.path.join(RLOG_FOLDER, rlogTopic)
|
||||
msgFiles = os.listdir(rlogTopicPath)
|
||||
pool = Pool()
|
||||
results = pool.map(processMsgs, [(msgFile, rlogTopicPath, rlogTopic) for msgFile in msgFiles])
|
||||
pool.close()
|
||||
pool.join()
|
||||
for result in results:
|
||||
writer.add_message(channel_id=result['channel_id'], log_time=result['log_time'], data=result['data'], publish_time=result['publish_time'])
|
||||
|
||||
|
||||
def is_program_installed(program_name):
|
||||
try:
|
||||
# Check if the program is installed using dpkg (for traditional Debian packages)
|
||||
subprocess.run(["dpkg", "-l", program_name], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
# Check if the program is installed using snap
|
||||
try:
|
||||
subprocess.run(["snap", "list", program_name], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Example usage:
|
||||
program_name = "foxglove-studio" # Change this to the program you want to check
|
||||
if is_program_installed(program_name):
|
||||
print(f"{program_name} detected.")
|
||||
else:
|
||||
print(f"{program_name} could not be detected.")
|
||||
installFoxglove = input("Would you like to install it? YES/NO? - ")
|
||||
if installFoxglove.lower() == "yes":
|
||||
try:
|
||||
subprocess.run(['./install_foxglove.sh'], check=True)
|
||||
print("Installation completed successfully.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Installation failed with return code {e.returncode}.")
|
||||
# Get a route
|
||||
if len(sys.argv) == 1:
|
||||
route_name = "a2a0ccea32023010|2023-07-27--13-01-19"
|
||||
print("No route was provided, using demo route")
|
||||
else:
|
||||
route_name = sys.argv[1]
|
||||
# Get logs for a route
|
||||
print("Getting route log paths")
|
||||
route = Route(route_name)
|
||||
logPaths = route.log_paths()
|
||||
# Start mcap writer
|
||||
with open(OUT_MCAP_FILE_NAME, "wb") as stream:
|
||||
writer = Writer(stream, compression=CompressionType.NONE)
|
||||
writer.start()
|
||||
createMcap(logPaths)
|
||||
writer.finish()
|
||||
print(f"File {OUT_MCAP_FILE_NAME} has been successfully created. Please import it into foxglove studio to continue.")
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
echo "Installing foxglvoe studio..."
|
||||
sudo snap install foxglove-studio
|
||||
+27
-19
@@ -1,12 +1,13 @@
|
||||
#include "tools/replay/camera.h"
|
||||
|
||||
#include <capnp/dynamic.h>
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include "third_party/linux/include/msm_media_info.h"
|
||||
#include "tools/replay/util.h"
|
||||
|
||||
const int BUFFER_COUNT = 40;
|
||||
|
||||
std::tuple<size_t, size_t, size_t> get_nv12_info(int width, int height) {
|
||||
int nv12_width = VENUS_Y_STRIDE(COLOR_FMT_NV12, width);
|
||||
int nv12_height = VENUS_Y_SCANLINES(COLOR_FMT_NV12, height);
|
||||
@@ -36,10 +37,12 @@ CameraServer::~CameraServer() {
|
||||
void CameraServer::startVipcServer() {
|
||||
vipc_server_.reset(new VisionIpcServer("camerad"));
|
||||
for (auto &cam : cameras_) {
|
||||
cam.cached_buf.clear();
|
||||
|
||||
if (cam.width > 0 && cam.height > 0) {
|
||||
rInfo("camera[%d] frame size %dx%d", cam.type, cam.width, cam.height);
|
||||
auto [nv12_width, nv12_height, nv12_buffer_size] = get_nv12_info(cam.width, cam.height);
|
||||
vipc_server_->create_buffers_with_sizes(cam.stream_type, YUV_BUFFER_COUNT, false, cam.width, cam.height,
|
||||
vipc_server_->create_buffers_with_sizes(cam.stream_type, BUFFER_COUNT, false, cam.width, cam.height,
|
||||
nv12_buffer_size, nv12_width, nv12_width * nv12_height);
|
||||
if (!cam.thread.joinable()) {
|
||||
cam.thread = std::thread(&CameraServer::cameraThread, this, std::ref(cam));
|
||||
@@ -50,13 +53,6 @@ void CameraServer::startVipcServer() {
|
||||
}
|
||||
|
||||
void CameraServer::cameraThread(Camera &cam) {
|
||||
auto read_frame = [&](FrameReader *fr, int frame_id) {
|
||||
VisionBuf *yuv_buf = vipc_server_->get_buffer(cam.stream_type);
|
||||
assert(yuv_buf);
|
||||
bool ret = fr->get(frame_id, yuv_buf);
|
||||
return ret ? yuv_buf : nullptr;
|
||||
};
|
||||
|
||||
while (true) {
|
||||
const auto [fr, event] = cam.queue.pop();
|
||||
if (!fr) break;
|
||||
@@ -66,29 +62,41 @@ void CameraServer::cameraThread(Camera &cam) {
|
||||
auto eidx = capnp::AnyStruct::Reader(evt).getPointerSection()[0].getAs<cereal::EncodeIndex>();
|
||||
if (eidx.getType() != cereal::EncodeIndex::Type::FULL_H_E_V_C) continue;
|
||||
|
||||
const int id = eidx.getSegmentId();
|
||||
bool prefetched = (id == cam.cached_id && eidx.getSegmentNum() == cam.cached_seg);
|
||||
auto yuv = prefetched ? cam.cached_buf : read_frame(fr, id);
|
||||
if (yuv) {
|
||||
int segment_id = eidx.getSegmentId();
|
||||
uint32_t frame_id = eidx.getFrameId();
|
||||
if (auto yuv = getFrame(cam, fr, segment_id, frame_id)) {
|
||||
VisionIpcBufExtra extra = {
|
||||
.frame_id = eidx.getFrameId(),
|
||||
.frame_id = frame_id,
|
||||
.timestamp_sof = eidx.getTimestampSof(),
|
||||
.timestamp_eof = eidx.getTimestampEof(),
|
||||
};
|
||||
yuv->set_frame_id(eidx.getFrameId());
|
||||
vipc_server_->send(yuv, &extra);
|
||||
} else {
|
||||
rError("camera[%d] failed to get frame: %lu", cam.type, eidx.getSegmentId());
|
||||
rError("camera[%d] failed to get frame: %lu", cam.type, segment_id);
|
||||
}
|
||||
|
||||
cam.cached_id = id + 1;
|
||||
cam.cached_seg = eidx.getSegmentNum();
|
||||
cam.cached_buf = read_frame(fr, cam.cached_id);
|
||||
// Prefetch the next frame
|
||||
getFrame(cam, fr, segment_id + 1, frame_id + 1);
|
||||
|
||||
--publishing_;
|
||||
}
|
||||
}
|
||||
|
||||
VisionBuf *CameraServer::getFrame(Camera &cam, FrameReader *fr, int32_t segment_id, uint32_t frame_id) {
|
||||
// Check if the frame is cached
|
||||
auto buf_it = std::find_if(cam.cached_buf.begin(), cam.cached_buf.end(),
|
||||
[frame_id](VisionBuf *buf) { return buf->get_frame_id() == frame_id; });
|
||||
if (buf_it != cam.cached_buf.end()) return *buf_it;
|
||||
|
||||
VisionBuf *yuv_buf = vipc_server_->get_buffer(cam.stream_type);
|
||||
if (fr->get(segment_id, yuv_buf)) {
|
||||
yuv_buf->set_frame_id(frame_id);
|
||||
cam.cached_buf.insert(yuv_buf);
|
||||
return yuv_buf;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void CameraServer::pushFrame(CameraType type, FrameReader *fr, const Event *event) {
|
||||
auto &cam = cameras_[type];
|
||||
if (cam.width != fr->width || cam.height != fr->height) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
@@ -26,12 +27,11 @@ protected:
|
||||
int height;
|
||||
std::thread thread;
|
||||
SafeQueue<std::pair<FrameReader*, const Event *>> queue;
|
||||
int cached_id = -1;
|
||||
int cached_seg = -1;
|
||||
VisionBuf * cached_buf;
|
||||
std::set<VisionBuf *> cached_buf;
|
||||
};
|
||||
void startVipcServer();
|
||||
void cameraThread(Camera &cam);
|
||||
VisionBuf *getFrame(Camera &cam, FrameReader *fr, int32_t segment_id, uint32_t frame_id);
|
||||
|
||||
Camera cameras_[MAX_CAMERAS] = {
|
||||
{.type = RoadCam, .stream_type = VISION_STREAM_ROAD},
|
||||
|
||||
@@ -468,12 +468,15 @@ std::vector<Event>::const_iterator Replay::publishEvents(std::vector<Event>::con
|
||||
// Skip events if socket is not present
|
||||
if (!sockets_[evt.which]) continue;
|
||||
|
||||
int64_t time_diff = (evt.mono_time - evt_start_ts) / speed_ - (nanos_since_boot() - loop_start_ts);
|
||||
// if time_diff is greater than 1 second, it means that an invalid segment is skipped
|
||||
if (time_diff >= 1e9 || speed_ != prev_replay_speed) {
|
||||
// reset event start times
|
||||
const uint64_t current_nanos = nanos_since_boot();
|
||||
const int64_t time_diff = (evt.mono_time - evt_start_ts) / speed_ - (current_nanos - loop_start_ts);
|
||||
|
||||
// Reset timestamps for potential synchronization issues:
|
||||
// - A negative time_diff may indicate slow execution or system wake-up,
|
||||
// - A time_diff exceeding 1 second suggests a skipped segment.
|
||||
if ((time_diff < -1e9 || time_diff >= 1e9) || speed_ != prev_replay_speed) {
|
||||
evt_start_ts = evt.mono_time;
|
||||
loop_start_ts = nanos_since_boot();
|
||||
loop_start_ts = current_nanos;
|
||||
prev_replay_speed = speed_;
|
||||
} else if (time_diff > 0) {
|
||||
precise_nano_sleep(time_diff);
|
||||
|
||||
@@ -46,6 +46,7 @@ class SimulatorBridge(ABC):
|
||||
self.world: World | None = None
|
||||
|
||||
self.past_startup_engaged = False
|
||||
self.startup_button_prev = True
|
||||
|
||||
def _on_shutdown(self, signal, frame):
|
||||
self.shutdown()
|
||||
@@ -161,7 +162,8 @@ Ignition: {self.simulator_state.ignition} Engaged: {self.simulator_state.is_enga
|
||||
|
||||
self.past_startup_engaged = True
|
||||
elif not self.past_startup_engaged and controlsState.engageable:
|
||||
self.simulator_state.cruise_button = CruiseButtons.DECEL_SET # force engagement on startup
|
||||
self.simulator_state.cruise_button = CruiseButtons.DECEL_SET if self.startup_button_prev else CruiseButtons.MAIN # force engagement on startup
|
||||
self.startup_button_prev = not self.startup_button_prev
|
||||
|
||||
throttle_out = throttle_op if self.simulator_state.is_engaged else throttle_manual
|
||||
brake_out = brake_op if self.simulator_state.is_engaged else brake_manual
|
||||
|
||||
@@ -62,8 +62,6 @@ class TestSimBridgeBase(unittest.TestCase):
|
||||
while time.monotonic() < start_time + max_time_per_step:
|
||||
sm.update()
|
||||
|
||||
q.put("cruise_down") # Try engaging
|
||||
|
||||
if sm.all_alive() and sm['controlsState'].active:
|
||||
control_active += 1
|
||||
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import ft4222
|
||||
import ft4222.I2CMaster
|
||||
|
||||
DEBUG = False
|
||||
|
||||
INA231_ADDR = 0x40
|
||||
INA231_REG_CONFIG = 0x00
|
||||
INA231_REG_SHUNT_VOLTAGE = 0x01
|
||||
INA231_REG_BUS_VOLTAGE = 0x02
|
||||
INA231_REG_POWER = 0x03
|
||||
INA231_REG_CURRENT = 0x04
|
||||
INA231_REG_CALIBRATION = 0x05
|
||||
|
||||
INA231_BUS_LSB = 1.25e-3
|
||||
INA231_SHUNT_LSB = 2.5e-6
|
||||
SHUNT_RESISTOR = 30e-3
|
||||
CURRENT_LSB = 1e-5
|
||||
|
||||
class Zookeeper:
|
||||
def __init__(self):
|
||||
if ft4222.createDeviceInfoList() < 2:
|
||||
raise Exception("No connected zookeeper found!")
|
||||
self.dev_a = ft4222.openByDescription("FT4222 A")
|
||||
self.dev_b = ft4222.openByDescription("FT4222 B")
|
||||
|
||||
if DEBUG:
|
||||
for i in range(ft4222.createDeviceInfoList()):
|
||||
print(f"Device {i}: {ft4222.getDeviceInfoDetail(i, False)}")
|
||||
|
||||
# Setup GPIO
|
||||
self.dev_b.gpio_Init(gpio2=ft4222.Dir.OUTPUT, gpio3=ft4222.Dir.OUTPUT)
|
||||
self.dev_b.setSuspendOut(False)
|
||||
self.dev_b.setWakeUpInterrut(False)
|
||||
|
||||
# Setup I2C
|
||||
self.dev_a.i2cMaster_Init(kbps=400)
|
||||
self._initialize_ina()
|
||||
|
||||
# Helper functions
|
||||
def _read_ina_register(self, register, length):
|
||||
self.dev_a.i2cMaster_WriteEx(INA231_ADDR, data=register, flag=ft4222.I2CMaster.Flag.REPEATED_START)
|
||||
return self.dev_a.i2cMaster_Read(INA231_ADDR, bytesToRead=length)
|
||||
|
||||
def _write_ina_register(self, register, data):
|
||||
msg = register.to_bytes(1, byteorder="big") + data.to_bytes(2, byteorder="big")
|
||||
self.dev_a.i2cMaster_Write(INA231_ADDR, data=msg)
|
||||
|
||||
def _initialize_ina(self):
|
||||
# Config
|
||||
self._write_ina_register(INA231_REG_CONFIG, 0x4127)
|
||||
|
||||
# Calibration
|
||||
CAL_VALUE = int(0.00512 / (CURRENT_LSB * SHUNT_RESISTOR))
|
||||
if DEBUG:
|
||||
print(f"Calibration value: {hex(CAL_VALUE)}")
|
||||
self._write_ina_register(INA231_REG_CALIBRATION, CAL_VALUE)
|
||||
|
||||
def _set_gpio(self, number, enabled):
|
||||
self.dev_b.gpio_Write(portNum=number, value=enabled)
|
||||
|
||||
# Public API functions
|
||||
def set_device_power(self, enabled):
|
||||
self._set_gpio(2, enabled)
|
||||
|
||||
def set_device_ignition(self, enabled):
|
||||
self._set_gpio(3, enabled)
|
||||
|
||||
def read_current(self):
|
||||
# Returns in A
|
||||
return int.from_bytes(self._read_ina_register(INA231_REG_CURRENT, 2), byteorder="big") * CURRENT_LSB
|
||||
|
||||
def read_power(self):
|
||||
# Returns in W
|
||||
return int.from_bytes(self._read_ina_register(INA231_REG_POWER, 2), byteorder="big") * CURRENT_LSB * 25
|
||||
|
||||
def read_voltage(self):
|
||||
# Returns in V
|
||||
return int.from_bytes(self._read_ina_register(INA231_REG_BUS_VOLTAGE, 2), byteorder="big") * INA231_BUS_LSB
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import time
|
||||
from openpilot.tools.zookeeper import Zookeeper
|
||||
|
||||
# Usage: check_consumption.py <averaging_time_sec> <max_average_power_W>
|
||||
# Exit code: 0 -> passed
|
||||
# 1 -> failed
|
||||
|
||||
if __name__ == "__main__":
|
||||
z = Zookeeper()
|
||||
|
||||
averaging_time_s = int(sys.argv[1])
|
||||
max_average_power = float(sys.argv[2])
|
||||
|
||||
start_time = time.time()
|
||||
measurements = []
|
||||
while time.time() - start_time < averaging_time_s:
|
||||
measurements.append(z.read_power())
|
||||
time.sleep(0.1)
|
||||
|
||||
average_power = sum(measurements)/len(measurements)
|
||||
print(f"Average power: {round(average_power, 4)}W")
|
||||
|
||||
if average_power > max_average_power:
|
||||
exit(1)
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from openpilot.tools.zookeeper import Zookeeper
|
||||
|
||||
if __name__ == "__main__":
|
||||
z = Zookeeper()
|
||||
z.set_device_power(False)
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from socket import gethostbyname, gaierror
|
||||
from openpilot.tools.zookeeper import Zookeeper
|
||||
|
||||
def is_online(ip):
|
||||
try:
|
||||
addr = gethostbyname(ip)
|
||||
return (os.system(f"ping -c 1 {addr} > /dev/null") == 0)
|
||||
except gaierror:
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
z = Zookeeper()
|
||||
z.set_device_power(True)
|
||||
|
||||
|
||||
ip = str(sys.argv[1])
|
||||
timeout = int(sys.argv[2])
|
||||
start_time = time.time()
|
||||
while not is_online(ip):
|
||||
print(f"{ip} not online yet!")
|
||||
|
||||
if time.time() - start_time > timeout:
|
||||
print("Timed out!")
|
||||
raise TimeoutError()
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
from openpilot.tools.zookeeper import Zookeeper
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
z = Zookeeper()
|
||||
z.set_device_ignition(1 if int(sys.argv[1]) > 0 else 0)
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import time
|
||||
import datetime
|
||||
|
||||
from openpilot.common.realtime import Ratekeeper
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.tools.zookeeper import Zookeeper
|
||||
|
||||
if __name__ == "__main__":
|
||||
z = Zookeeper()
|
||||
z.set_device_power(True)
|
||||
z.set_device_ignition(False)
|
||||
|
||||
duration = None
|
||||
if len(sys.argv) > 1:
|
||||
duration = int(sys.argv[1])
|
||||
|
||||
rate = 123
|
||||
rk = Ratekeeper(rate, print_delay_threshold=None)
|
||||
fltr = FirstOrderFilter(0, 5, 1. / rate, initialized=False)
|
||||
|
||||
measurements = []
|
||||
start_time = time.monotonic()
|
||||
|
||||
try:
|
||||
while duration is None or time.monotonic() - start_time < duration:
|
||||
fltr.update(z.read_power())
|
||||
if rk.frame % rate == 0:
|
||||
measurements.append(fltr.x)
|
||||
t = datetime.timedelta(seconds=time.monotonic() - start_time)
|
||||
avg = sum(measurements) / len(measurements)
|
||||
print(f"Now: {fltr.x:.2f} W, Avg: {avg:.2f} W over {t}")
|
||||
rk.keep_time()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
t = datetime.timedelta(seconds=time.monotonic() - start_time)
|
||||
avg = sum(measurements) / len(measurements)
|
||||
print(f"\nAverage power: {avg:.2f}W over {t}")
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import time
|
||||
from openpilot.tools.zookeeper import Zookeeper
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
z = Zookeeper()
|
||||
z.set_device_power(True)
|
||||
|
||||
i = 0
|
||||
ign = False
|
||||
while 1:
|
||||
voltage = round(z.read_voltage(), 2)
|
||||
current = round(z.read_current(), 3)
|
||||
power = round(z.read_power(), 2)
|
||||
z.set_device_ignition(ign)
|
||||
print(f"Voltage: {voltage}V, Current: {current}A, Power: {power}W, Ignition: {ign}")
|
||||
|
||||
if i > 200:
|
||||
ign = not ign
|
||||
i = 0
|
||||
|
||||
i += 1
|
||||
time.sleep(0.1)
|
||||
Reference in New Issue
Block a user