forked from IQ.Lvbs/IQ.Pilot
IQ.Pilot Release Commit @ 0b96bd5
This commit is contained in:
@@ -1,26 +1,51 @@
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
#include "tools/cabana/dbc/dbcqt.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
#include <QApplication>
|
||||
#include "common/timing.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
|
||||
static const int EVENT_NEXT_BUFFER_SIZE = 6 * 1024 * 1024; // 6MB
|
||||
static const int EVENT_NEXT_BUFFER_SIZE = 6 * 1024 * 1024;
|
||||
|
||||
AbstractStream *can = nullptr;
|
||||
|
||||
AbstractStream::AbstractStream(QObject *parent) : QObject(parent) {
|
||||
assert(parent != nullptr);
|
||||
AbstractStream::AbstractStream() {
|
||||
event_buffer_ = std::make_unique<MonotonicBuffer>(EVENT_NEXT_BUFFER_SIZE);
|
||||
|
||||
QObject::connect(this, &AbstractStream::privateUpdateLastMsgsSignal, this, &AbstractStream::updateLastMessages, Qt::QueuedConnection);
|
||||
QObject::connect(this, &AbstractStream::seekedTo, this, &AbstractStream::updateLastMsgsTo);
|
||||
QObject::connect(this, &AbstractStream::seeking, this, [this](double sec) { current_sec_ = sec; });
|
||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &AbstractStream::updateMasks);
|
||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::maskUpdated, this, &AbstractStream::updateMasks);
|
||||
|
||||
connections_.push_back(seekedTo.connect([this](double sec) { updateLastMsgsTo(sec); }));
|
||||
connections_.push_back(seeking.connect([this](double sec) { current_sec_ = sec; }));
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() { updateMasks(); }));
|
||||
connections_.push_back(dbc()->maskUpdated.connect([this]() { updateMasks(); }));
|
||||
}
|
||||
|
||||
void AbstractStream::postToMainThread(std::function<void()> fn) {
|
||||
utils::runOnMainThread([alive = std::weak_ptr<bool>(alive_), fn = std::move(fn)]() {
|
||||
if (!alive.expired()) fn();
|
||||
});
|
||||
}
|
||||
|
||||
void AbstractStream::postToMainThreadAndWait(std::function<void()> fn) {
|
||||
assert(!utils::isMainThread());
|
||||
std::unique_lock lock(mutex_);
|
||||
if (exiting_) return;
|
||||
auto done = std::make_shared<bool>(false);
|
||||
postToMainThread([this, alive = std::weak_ptr<bool>(alive_), done, fn = std::move(fn)]() {
|
||||
fn();
|
||||
if (alive.expired()) return;
|
||||
std::lock_guard lk(mutex_);
|
||||
*done = true;
|
||||
wait_cv_.notify_all();
|
||||
});
|
||||
wait_cv_.wait(lock, [&]() { return *done || exiting_; });
|
||||
}
|
||||
|
||||
void AbstractStream::cancelWaits() {
|
||||
std::lock_guard lk(mutex_);
|
||||
exiting_ = true;
|
||||
wait_cv_.notify_all();
|
||||
}
|
||||
|
||||
void AbstractStream::updateMasks() {
|
||||
@@ -34,7 +59,7 @@ void AbstractStream::updateMasks() {
|
||||
masks_[{.source = (uint8_t)s, .address = address}] = m.mask;
|
||||
}
|
||||
}
|
||||
// clear bit change counts
|
||||
|
||||
for (auto &[id, m] : messages_) {
|
||||
auto &mask = masks_[id];
|
||||
const int size = std::min(mask.size(), m.last_changes.size());
|
||||
@@ -97,9 +122,8 @@ void AbstractStream::updateLastMessages() {
|
||||
|
||||
if (sources.size() != prev_src_size) {
|
||||
updateMasks();
|
||||
emit sourcesUpdated(sources);
|
||||
}
|
||||
emit msgsReceived(&msgs, prev_msg_size != last_msgs.size());
|
||||
msgsReceived(&msgs, prev_msg_size != last_msgs.size());
|
||||
}
|
||||
|
||||
void AbstractStream::setTimeRange(const std::optional<std::pair<double, double>> &range) {
|
||||
@@ -107,7 +131,7 @@ void AbstractStream::setTimeRange(const std::optional<std::pair<double, double>>
|
||||
if (time_range_ && (current_sec_ < time_range_->first || current_sec_ >= time_range_->second)) {
|
||||
seekTo(time_range_->first);
|
||||
}
|
||||
emit timeRangeChanged(time_range_);
|
||||
timeRangeChanged(time_range_);
|
||||
}
|
||||
|
||||
void AbstractStream::updateEvent(const MessageId &id, double sec, const uint8_t *data, uint8_t size) {
|
||||
@@ -132,7 +156,7 @@ bool AbstractStream::isMessageActive(const MessageId &id) const {
|
||||
if (id.source == INVALID_SOURCE) {
|
||||
return false;
|
||||
}
|
||||
// Check if the message is active based on time difference and frequency
|
||||
|
||||
const auto &m = lastMessage(id);
|
||||
float delta = currentSec() - m.ts;
|
||||
|
||||
@@ -140,7 +164,7 @@ bool AbstractStream::isMessageActive(const MessageId &id) const {
|
||||
return delta < 1.5;
|
||||
}
|
||||
|
||||
return delta < (5.0 / m.freq) + (1.0 / settings.fps);
|
||||
return delta < (5.0 / m.freq) + (1.0 / STREAM_UPDATE_FPS);
|
||||
}
|
||||
|
||||
void AbstractStream::updateLastMsgsTo(double sec) {
|
||||
@@ -154,7 +178,7 @@ void AbstractStream::updateLastMsgsTo(double sec) {
|
||||
if (it != ev.begin()) {
|
||||
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());
|
||||
@@ -175,16 +199,16 @@ void AbstractStream::updateLastMsgsTo(double sec) {
|
||||
std::any_of(messages_.cbegin(), messages_.cend(),
|
||||
[this](const auto &m) { return !last_msgs.count(m.first); });
|
||||
last_msgs = messages_;
|
||||
emit msgsReceived(nullptr, id_changed);
|
||||
msgsReceived(nullptr, id_changed);
|
||||
|
||||
std::lock_guard lk(mutex_);
|
||||
seek_finished_ = true;
|
||||
seek_finished_cv_.notify_one();
|
||||
wait_cv_.notify_all();
|
||||
}
|
||||
|
||||
void AbstractStream::waitForSeekFinshed() {
|
||||
std::unique_lock lock(mutex_);
|
||||
seek_finished_cv_.wait(lock, [this]() { return seek_finished_; });
|
||||
wait_cv_.wait(lock, [this]() { return seek_finished_ || exiting_; });
|
||||
seek_finished_ = false;
|
||||
}
|
||||
|
||||
@@ -203,11 +227,15 @@ void AbstractStream::mergeEvents(const std::vector<const CanEvent *> &events) {
|
||||
static MessageEventsMap msg_events;
|
||||
std::for_each(msg_events.begin(), msg_events.end(), [](auto &e) { e.second.clear(); });
|
||||
|
||||
// Group events by message ID
|
||||
|
||||
for (auto e : events) {
|
||||
msg_events[{.source = e->src, .address = e->address}].push_back(e);
|
||||
}
|
||||
|
||||
insertEvents(events, msg_events);
|
||||
}
|
||||
|
||||
void AbstractStream::insertEvents(const std::vector<const CanEvent *> &events, const MessageEventsMap &msg_events) {
|
||||
if (!events.empty()) {
|
||||
for (const auto &[id, new_e] : msg_events) {
|
||||
if (!new_e.empty()) {
|
||||
@@ -218,16 +246,16 @@ void AbstractStream::mergeEvents(const std::vector<const CanEvent *> &events) {
|
||||
}
|
||||
auto pos = std::upper_bound(all_events_.cbegin(), all_events_.cend(), events.front()->mono_time, CompareCanEvent());
|
||||
all_events_.insert(pos, events.cbegin(), events.cend());
|
||||
emit eventsMerged(msg_events);
|
||||
eventsMerged(msg_events);
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<CanEventIter, CanEventIter> AbstractStream::eventsInRange(const MessageId &id, std::optional<std::pair<double, double>> time_range) const {
|
||||
const auto &events = can->events(id);
|
||||
const auto &events = this->events(id);
|
||||
if (!time_range) return {events.begin(), events.end()};
|
||||
|
||||
auto first = std::lower_bound(events.begin(), events.end(), can->toMonoTime(time_range->first), CompareCanEvent());
|
||||
auto last = std::upper_bound(first, events.end(), can->toMonoTime(time_range->second), CompareCanEvent());
|
||||
auto first = std::lower_bound(events.begin(), events.end(), toMonoTime(time_range->first), CompareCanEvent());
|
||||
auto last = std::upper_bound(first, events.end(), toMonoTime(time_range->second), CompareCanEvent());
|
||||
return {first, last};
|
||||
}
|
||||
|
||||
@@ -248,7 +276,7 @@ inline CabanaColor blend(const CabanaColor &a, const CabanaColor &b) {
|
||||
return CabanaColor((a.red() + b.red()) / 2, (a.green() + b.green()) / 2, (a.blue() + b.blue()) / 2, (a.alpha() + b.alpha()) / 2);
|
||||
}
|
||||
|
||||
// Calculate the frequency from the past one minute data
|
||||
|
||||
double calc_freq(const MessageId &msg_id, double current_sec) {
|
||||
auto [first, last] = can->eventsInRange(msg_id, std::make_pair(current_sec - 59, current_sec));
|
||||
int count = std::distance(first, last);
|
||||
@@ -258,7 +286,7 @@ double calc_freq(const MessageId &msg_id, double current_sec) {
|
||||
return duration > std::numeric_limits<double>::epsilon() ? (count - 1) / duration : 0.0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
}
|
||||
|
||||
void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const int size, double current_sec,
|
||||
double playback_speed, const std::vector<uint8_t> &mask, double in_freq) {
|
||||
@@ -291,21 +319,21 @@ void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const in
|
||||
const uint8_t cur = can_data[i] & mask_byte;
|
||||
if (last != cur) {
|
||||
const int delta = cur - last;
|
||||
// Keep track if signal is changing randomly, or mostly moving in the same direction
|
||||
|
||||
last_change.same_delta_counter += std::signbit(delta) == std::signbit(last_change.delta) ? 1 : -4;
|
||||
last_change.same_delta_counter = std::clamp(last_change.same_delta_counter, 0, 16);
|
||||
|
||||
const double delta_t = ts - last_change.ts;
|
||||
// Mostly moves in the same direction, color based on delta up/down
|
||||
|
||||
if (delta_t * freq > periodic_threshold || last_change.same_delta_counter > 8) {
|
||||
// Last change was while ago, choose color based on delta up or down
|
||||
|
||||
colors[i] = getColor(cur > last ? CYAN : RED);
|
||||
} else {
|
||||
// Periodic changes
|
||||
|
||||
colors[i] = blend(colors[i], getColor(GREYISH_BLUE));
|
||||
}
|
||||
|
||||
// Track bit level changes
|
||||
|
||||
auto &row_bit_flips = bit_flip_counts[i];
|
||||
const uint8_t diff = (cur ^ last);
|
||||
for (int bit = 0; bit < 8; bit++) {
|
||||
@@ -317,7 +345,7 @@ void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const in
|
||||
last_change.ts = ts;
|
||||
last_change.delta = delta;
|
||||
} else {
|
||||
// Fade out
|
||||
|
||||
colors[i].setAlphaF(std::max(0.0f, colors[i].alphaF() - alpha_delta));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <array>
|
||||
#include <condition_variable>
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
@@ -14,16 +15,15 @@
|
||||
|
||||
#include "cereal/messaging/messaging.h"
|
||||
#include "tools/cabana/core/can_data.h"
|
||||
#include "tools/cabana/core/observable.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
#include "tools/replay/util.h"
|
||||
|
||||
class AbstractStream : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
class AbstractStream {
|
||||
public:
|
||||
AbstractStream(QObject *parent);
|
||||
virtual ~AbstractStream() {}
|
||||
AbstractStream();
|
||||
virtual ~AbstractStream() = default;
|
||||
virtual void start() = 0;
|
||||
virtual bool liveStreaming() const { return true; }
|
||||
virtual void seekTo(double ts) {}
|
||||
@@ -56,22 +56,25 @@ public:
|
||||
void clearSuppressed();
|
||||
void suppressDefinedSignals(bool suppress);
|
||||
|
||||
signals:
|
||||
void paused();
|
||||
void resume();
|
||||
void seeking(double sec);
|
||||
void seekedTo(double sec);
|
||||
void timeRangeChanged(const std::optional<std::pair<double, double>> &range);
|
||||
void eventsMerged(const MessageEventsMap &events_map);
|
||||
void msgsReceived(const std::set<MessageId> *new_msgs, bool has_new_ids);
|
||||
void sourcesUpdated(const SourceSet &s);
|
||||
void privateUpdateLastMsgsSignal();
|
||||
|
||||
public:
|
||||
Observable<> paused;
|
||||
Observable<> resume;
|
||||
Observable<double> seeking;
|
||||
Observable<double> seekedTo;
|
||||
Observable<const std::optional<std::pair<double, double>> &> timeRangeChanged;
|
||||
Observable<const MessageEventsMap &> eventsMerged;
|
||||
Observable<const std::set<MessageId> *, bool> msgsReceived;
|
||||
Observable<const std::string &> error;
|
||||
|
||||
SourceSet sources;
|
||||
|
||||
protected:
|
||||
void postToMainThread(std::function<void()> fn);
|
||||
void postToMainThreadAndWait(std::function<void()> fn);
|
||||
void cancelWaits();
|
||||
void requestUpdateLastMessages() { postToMainThread([this]() { updateLastMessages(); }); }
|
||||
void mergeEvents(const std::vector<const CanEvent *> &events);
|
||||
void insertEvents(const std::vector<const CanEvent *> &events, const MessageEventsMap &msg_events);
|
||||
const CanEvent *newEvent(uint64_t mono_time, const cereal::CanData::Reader &c);
|
||||
void updateEvent(const MessageId &id, double sec, const uint8_t *data, uint8_t size);
|
||||
void waitForSeekFinshed();
|
||||
@@ -87,33 +90,24 @@ private:
|
||||
MessageEventsMap events_;
|
||||
std::unordered_map<MessageId, CanData> last_msgs;
|
||||
std::unique_ptr<MonotonicBuffer> event_buffer_;
|
||||
std::shared_ptr<bool> alive_ = std::make_shared<bool>(true);
|
||||
Connections connections_;
|
||||
|
||||
|
||||
// Members accessed in multiple threads. (mutex protected)
|
||||
std::mutex mutex_;
|
||||
std::condition_variable seek_finished_cv_;
|
||||
std::condition_variable wait_cv_;
|
||||
bool seek_finished_ = false;
|
||||
bool exiting_ = false;
|
||||
std::set<MessageId> new_msgs_;
|
||||
std::unordered_map<MessageId, CanData> messages_;
|
||||
std::unordered_map<MessageId, std::vector<uint8_t>> masks_;
|
||||
};
|
||||
|
||||
class AbstractOpenStreamWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
AbstractOpenStreamWidget(QWidget *parent = nullptr) : QWidget(parent) {}
|
||||
virtual AbstractStream *open() = 0;
|
||||
|
||||
signals:
|
||||
void enableOpenButton(bool);
|
||||
};
|
||||
|
||||
class DummyStream : public AbstractStream {
|
||||
Q_OBJECT
|
||||
public:
|
||||
DummyStream(QObject *parent) : AbstractStream(parent) {}
|
||||
std::string routeName() const override { return "No Stream"; }
|
||||
void start() override {}
|
||||
};
|
||||
|
||||
// A global pointer referring to the unique AbstractStream object
|
||||
|
||||
extern AbstractStream *can;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "tools/cabana/streams/devicestream.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <csignal>
|
||||
@@ -9,22 +10,17 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
#include "cereal/services.h"
|
||||
|
||||
#include <QButtonGroup>
|
||||
#include <QFormLayout>
|
||||
#include <QMessageBox>
|
||||
#include <QRadioButton>
|
||||
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
// DeviceStream
|
||||
|
||||
DeviceStream::DeviceStream(QObject *parent, Mode mode, QString address)
|
||||
: mode_(mode), address_(address.isEmpty() ? "127.0.0.1" : address), LiveStream(parent) {
|
||||
|
||||
DeviceStream::DeviceStream(Mode mode, std::string address)
|
||||
: mode_(mode), address_(address.empty() ? "127.0.0.1" : std::move(address)) {
|
||||
}
|
||||
|
||||
DeviceStream::~DeviceStream() {
|
||||
@@ -43,7 +39,7 @@ void DeviceStream::stopBridge() {
|
||||
bridge_pid = -1;
|
||||
return;
|
||||
}
|
||||
usleep(100000); // 100ms, up to ~3s
|
||||
usleep(100000);
|
||||
}
|
||||
::kill(bridge_pid, SIGKILL);
|
||||
::waitpid(bridge_pid, nullptr, 0);
|
||||
@@ -53,17 +49,14 @@ void DeviceStream::stopBridge() {
|
||||
void DeviceStream::start() {
|
||||
if (mode_ == Mode::Bridge) {
|
||||
stopBridge();
|
||||
const std::string path = (std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()) /
|
||||
"../../cereal/messaging/bridge").lexically_normal().string();
|
||||
const std::string addr = address_.toStdString();
|
||||
const std::string path = (executableDir() / "../../cereal/messaging/bridge").lexically_normal().string();
|
||||
const char *can_filter = "/\"can/\"";
|
||||
|
||||
// Self-pipe: write end is CLOEXEC so it closes on successful exec. If exec
|
||||
// fails, the child writes errno and the parent aborts stream start.
|
||||
|
||||
|
||||
int err_pipe[2] = {-1, -1};
|
||||
if (::pipe(err_pipe) != 0) {
|
||||
QMessageBox::warning(nullptr, tr("Error"),
|
||||
tr("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno))));
|
||||
error(std::string("Failed to start bridge: ") + strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -71,7 +64,7 @@ void DeviceStream::start() {
|
||||
if (pid == 0) {
|
||||
::close(err_pipe[0]);
|
||||
::fcntl(err_pipe[1], F_SETFD, FD_CLOEXEC);
|
||||
execl(path.c_str(), path.c_str(), addr.c_str(), can_filter, static_cast<char *>(nullptr));
|
||||
execl(path.c_str(), path.c_str(), address_.c_str(), can_filter, static_cast<char *>(nullptr));
|
||||
const int err = errno;
|
||||
(void)!::write(err_pipe[1], &err, sizeof(err));
|
||||
_exit(127);
|
||||
@@ -80,8 +73,7 @@ void DeviceStream::start() {
|
||||
::close(err_pipe[1]);
|
||||
if (pid < 0) {
|
||||
::close(err_pipe[0]);
|
||||
QMessageBox::warning(nullptr, tr("Error"),
|
||||
tr("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno))));
|
||||
error(std::string("Failed to start bridge: ") + strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -89,11 +81,10 @@ void DeviceStream::start() {
|
||||
const ssize_t n = ::read(err_pipe[0], &exec_errno, sizeof(exec_errno));
|
||||
::close(err_pipe[0]);
|
||||
if (n == static_cast<ssize_t>(sizeof(exec_errno))) {
|
||||
// Child failed to exec; reap and surface the error.
|
||||
|
||||
int status = 0;
|
||||
::waitpid(pid, &status, 0);
|
||||
QMessageBox::warning(nullptr, tr("Error"),
|
||||
tr("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(exec_errno))));
|
||||
error(std::string("Failed to start bridge: ") + strerror(exec_errno));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -104,16 +95,13 @@ void DeviceStream::start() {
|
||||
}
|
||||
|
||||
void DeviceStream::streamThread() {
|
||||
// Bridge mode republishes into local msgq, so only the direct Zmq mode talks ZMQ.
|
||||
// (Upstream sets ZMQ=1 for its bridge path too, which reads nothing — the bridge
|
||||
// publishes to msgq.)
|
||||
mode_ == Mode::Zmq ? setenv("ZMQ", "1", 1) : unsetenv("ZMQ");
|
||||
const std::string address = mode_ == Mode::Zmq ? address_.toStdString() : "127.0.0.1";
|
||||
const std::string socket_address = mode_ == Mode::Zmq ? address_ : "127.0.0.1";
|
||||
|
||||
std::unique_ptr<Context> context(Context::create());
|
||||
std::unique_ptr<SubSocket> sock(SubSocket::create(context.get(), "can", address, false, true, services.at("can").queue_size));
|
||||
std::unique_ptr<SubSocket> sock(SubSocket::create(context.get(), "can", socket_address, false, true, services.at("can").queue_size));
|
||||
assert(sock != NULL);
|
||||
// run as fast as messages come in
|
||||
|
||||
while (!exit_) {
|
||||
std::unique_ptr<Message> msg(sock->receive(true));
|
||||
if (!msg) {
|
||||
@@ -123,36 +111,3 @@ void DeviceStream::streamThread() {
|
||||
handleEvent(kj::ArrayPtr<capnp::word>((capnp::word*)msg->getData(), msg->getSize() / sizeof(capnp::word)));
|
||||
}
|
||||
}
|
||||
|
||||
// OpenDeviceWidget
|
||||
|
||||
OpenDeviceWidget::OpenDeviceWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) {
|
||||
QRadioButton *msgq = new QRadioButton(tr("MSGQ"));
|
||||
QRadioButton *zmq = new QRadioButton(tr("ZMQ"));
|
||||
QRadioButton *bridge = new QRadioButton(tr("Bridge"));
|
||||
zmq->setToolTip(tr("Subscribe directly to a ZMQ 'can' publisher: a device running "
|
||||
"cereal/messaging/bridge, or konn3kt_canproxy.py on 127.0.0.1."));
|
||||
bridge->setToolTip(tr("Run cereal/messaging/bridge locally against the device and read msgq."));
|
||||
ip_address = new QLineEdit(this);
|
||||
ip_address->setPlaceholderText(tr("Enter device Ip Address"));
|
||||
ip_address->setValidator(new IpAddressValidator(this));
|
||||
|
||||
group = new QButtonGroup(this);
|
||||
group->addButton(msgq, static_cast<int>(DeviceStream::Mode::Msgq));
|
||||
group->addButton(zmq, static_cast<int>(DeviceStream::Mode::Zmq));
|
||||
group->addButton(bridge, static_cast<int>(DeviceStream::Mode::Bridge));
|
||||
|
||||
QFormLayout *form_layout = new QFormLayout(this);
|
||||
form_layout->addRow(msgq);
|
||||
form_layout->addRow(zmq, ip_address);
|
||||
form_layout->addRow(bridge);
|
||||
QObject::connect(group, qOverload<QAbstractButton *, bool>(&QButtonGroup::buttonToggled), [=](QAbstractButton *button, bool checked) {
|
||||
if (checked) ip_address->setEnabled(button != msgq);
|
||||
});
|
||||
zmq->setChecked(true);
|
||||
}
|
||||
|
||||
AbstractStream *OpenDeviceWidget::open() {
|
||||
auto mode = static_cast<DeviceStream::Mode>(group->checkedId());
|
||||
return new DeviceStream(qApp, mode, mode == DeviceStream::Mode::Msgq ? "" : ip_address->text());
|
||||
}
|
||||
|
||||
@@ -2,28 +2,17 @@
|
||||
|
||||
#include "tools/cabana/streams/livestream.h"
|
||||
|
||||
#include <string>
|
||||
#include <sys/types.h>
|
||||
|
||||
// IQ.Pilot patch: upstream (#38484) folded the ZMQ path into "fork a local bridge and
|
||||
// read msgq". iqpilot needs the direct ZMQ attach kept as a first-class mode, because
|
||||
// tools/cabana/konn3kt_canproxy.py publishes a remote device's CAN onto a LOCAL ZMQ
|
||||
// "can" socket and Cabana attaches to it — see that script's header for the topology.
|
||||
// So the mode is explicit rather than inferred from whether an address was entered:
|
||||
//
|
||||
// Msgq - local msgq, no address (cabana running on the device)
|
||||
// Zmq - ZMQ subscribe straight to <address> (konn3kt_canproxy, or `bridge` on the device)
|
||||
// Bridge - fork cereal/messaging/bridge <address>, (upstream's convenience path)
|
||||
// which ZMQ-subscribes there and republishes
|
||||
// to local msgq, then read msgq
|
||||
class DeviceStream : public LiveStream {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum class Mode { Msgq, Zmq, Bridge };
|
||||
|
||||
DeviceStream(QObject *parent, Mode mode = Mode::Msgq, QString address = {});
|
||||
DeviceStream(Mode mode = Mode::Msgq, std::string address = {});
|
||||
~DeviceStream();
|
||||
inline std::string routeName() const override {
|
||||
return "Live Streaming From " + address_.toStdString();
|
||||
return "Live Streaming From " + address_;
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -32,17 +21,5 @@ protected:
|
||||
void stopBridge();
|
||||
pid_t bridge_pid = -1;
|
||||
const Mode mode_;
|
||||
const QString address_;
|
||||
};
|
||||
|
||||
class OpenDeviceWidget : public AbstractOpenStreamWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
OpenDeviceWidget(QWidget *parent = nullptr);
|
||||
AbstractStream *open() override;
|
||||
|
||||
private:
|
||||
QLineEdit *ip_address;
|
||||
QButtonGroup *group;
|
||||
const std::string address_;
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "common/timing.h"
|
||||
#include "common/util.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
|
||||
struct LiveStream::Logger {
|
||||
Logger() : start_ts(seconds_since_epoch()), segment_num(-1) {}
|
||||
@@ -21,12 +22,9 @@ struct LiveStream::Logger {
|
||||
localtime_r(&start_time, &local_time);
|
||||
std::ostringstream date;
|
||||
date << std::put_time(&local_time, "%Y-%m-%d--%H-%M-%S");
|
||||
QString dir = QString("%1/%2--%3")
|
||||
.arg(QString::fromStdString(settings.log_path))
|
||||
.arg(QString::fromStdString(date.str()))
|
||||
.arg(n);
|
||||
util::create_directories(dir.toStdString(), 0755);
|
||||
fs.reset(new std::ofstream((dir + "/rlog").toStdString(), std::ios::binary | std::ios::out));
|
||||
std::string dir = settings.log_path + "/" + date.str() + "--" + std::to_string(n);
|
||||
util::create_directories(dir, 0755);
|
||||
fs.reset(new std::ofstream(dir + "/rlog", std::ios::binary | std::ios::out));
|
||||
}
|
||||
|
||||
auto bytes = data.asBytes();
|
||||
@@ -38,7 +36,7 @@ struct LiveStream::Logger {
|
||||
uint64_t start_ts;
|
||||
};
|
||||
|
||||
LiveStream::LiveStream(QObject *parent) : AbstractStream(parent) {
|
||||
LiveStream::LiveStream() {
|
||||
if (settings.log_livestream) {
|
||||
logger = std::make_unique<Logger>();
|
||||
}
|
||||
@@ -50,7 +48,6 @@ LiveStream::~LiveStream() {
|
||||
|
||||
void LiveStream::start() {
|
||||
begin_date_time = std::chrono::system_clock::now();
|
||||
fps_ = settings.fps;
|
||||
exit_ = false;
|
||||
stream_thread = std::thread(&LiveStream::streamThread, this);
|
||||
update_thread = std::thread(&LiveStream::updateThread, this);
|
||||
@@ -64,15 +61,15 @@ void LiveStream::stop() {
|
||||
|
||||
void LiveStream::updateThread() {
|
||||
while (!exit_) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000 / fps_));
|
||||
// coalesce: skip the emit if the main thread hasn't processed the previous one yet.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000 / STREAM_UPDATE_FPS));
|
||||
|
||||
if (!update_pending_.exchange(true)) {
|
||||
emit privateUpdateLastMsgsSignal();
|
||||
requestUpdateLastMessages();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// called in streamThread
|
||||
|
||||
void LiveStream::handleEvent(kj::ArrayPtr<capnp::word> data) {
|
||||
if (logger) {
|
||||
logger->write(data);
|
||||
@@ -89,12 +86,11 @@ void LiveStream::handleEvent(kj::ArrayPtr<capnp::word> data) {
|
||||
}
|
||||
}
|
||||
|
||||
// called on the main thread by the queued privateUpdateLastMsgsSignal connection
|
||||
|
||||
void LiveStream::updateLastMessages() {
|
||||
update_pending_ = false;
|
||||
fps_ = settings.fps;
|
||||
{
|
||||
// merge events received from live stream thread.
|
||||
|
||||
std::lock_guard lk(lock);
|
||||
mergeEvents(received_events_);
|
||||
uint64_t last_received_ts = !received_events_.empty() ? received_events_.back()->mono_time : 0;
|
||||
@@ -142,10 +138,10 @@ void LiveStream::seekTo(double sec) {
|
||||
first_update_ts = nanos_since_boot();
|
||||
current_event_ts = first_event_ts = std::min<uint64_t>(sec * 1e9 + begin_event_ts, lastest_event_ts);
|
||||
post_last_event = (first_event_ts == lastest_event_ts);
|
||||
emit seekedTo((current_event_ts - begin_event_ts) / 1e9);
|
||||
seekedTo((current_event_ts - begin_event_ts) / 1e9);
|
||||
}
|
||||
|
||||
void LiveStream::pause(bool pause) {
|
||||
paused_ = pause;
|
||||
emit(pause ? paused() : resume());
|
||||
pause ? paused() : resume();
|
||||
}
|
||||
|
||||
@@ -9,10 +9,8 @@
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
|
||||
class LiveStream : public AbstractStream {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
LiveStream(QObject *parent);
|
||||
LiveStream();
|
||||
virtual ~LiveStream();
|
||||
void start() override;
|
||||
void stop();
|
||||
@@ -39,7 +37,6 @@ private:
|
||||
std::mutex lock;
|
||||
std::thread stream_thread, update_thread;
|
||||
std::atomic<bool> update_pending_ = false;
|
||||
std::atomic<int> fps_ = 10;
|
||||
std::vector<const CanEvent *> received_events_;
|
||||
|
||||
std::chrono::system_clock::time_point begin_date_time;
|
||||
|
||||
@@ -4,13 +4,7 @@
|
||||
#include <cstdio>
|
||||
#include <thread>
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QTimer>
|
||||
|
||||
PandaStream::PandaStream(QObject *parent, PandaStreamConfig config_) : config(config_), LiveStream(parent) {
|
||||
PandaStream::PandaStream(PandaStreamConfig config_) : config(config_) {
|
||||
if (!connect()) {
|
||||
throw std::runtime_error("Failed to connect to panda");
|
||||
}
|
||||
@@ -30,12 +24,12 @@ bool PandaStream::connect() {
|
||||
for (int bus = 0; bus < config.bus_config.size(); bus++) {
|
||||
panda->set_can_speed_kbps(bus, config.bus_config[bus].can_speed_kbps);
|
||||
|
||||
// CAN-FD
|
||||
|
||||
if (panda->hw_type == cereal::PandaState::PandaType::RED_PANDA || panda->hw_type == cereal::PandaState::PandaType::RED_PANDA_V2) {
|
||||
if (config.bus_config[bus].can_fd) {
|
||||
panda->set_data_speed_kbps(bus, config.bus_config[bus].data_speed_kbps);
|
||||
} else {
|
||||
// Hack to disable can-fd by setting data speed to a low value
|
||||
|
||||
panda->set_data_speed_kbps(bus, 10);
|
||||
}
|
||||
}
|
||||
@@ -77,113 +71,3 @@ void PandaStream::streamThread() {
|
||||
panda->send_heartbeat(false);
|
||||
}
|
||||
}
|
||||
|
||||
// OpenPandaWidget
|
||||
|
||||
OpenPandaWidget::OpenPandaWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) {
|
||||
form_layout = new QFormLayout(this);
|
||||
if (can && dynamic_cast<PandaStream *>(can) != nullptr) {
|
||||
form_layout->addWidget(new QLabel(tr("Already connected to %1.").arg(QString::fromStdString(can->routeName()))));
|
||||
form_layout->addWidget(new QLabel("Close the current connection via [File menu -> Close Stream] before connecting to another Panda."));
|
||||
QTimer::singleShot(0, [this]() { emit enableOpenButton(false); });
|
||||
return;
|
||||
}
|
||||
|
||||
QHBoxLayout *serial_layout = new QHBoxLayout();
|
||||
serial_layout->addWidget(serial_edit = new QComboBox());
|
||||
|
||||
QPushButton *refresh = new QPushButton(tr("Refresh"));
|
||||
refresh->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
|
||||
serial_layout->addWidget(refresh);
|
||||
form_layout->addRow(tr("Serial"), serial_layout);
|
||||
|
||||
QObject::connect(refresh, &QPushButton::clicked, this, &OpenPandaWidget::refreshSerials);
|
||||
QObject::connect(serial_edit, &QComboBox::currentTextChanged, this, &OpenPandaWidget::buildConfigForm);
|
||||
|
||||
// Populate serials
|
||||
refreshSerials();
|
||||
buildConfigForm();
|
||||
}
|
||||
|
||||
void OpenPandaWidget::refreshSerials() {
|
||||
serial_edit->clear();
|
||||
for (auto serial : Panda::list()) {
|
||||
serial_edit->addItem(QString::fromStdString(serial));
|
||||
}
|
||||
}
|
||||
|
||||
void OpenPandaWidget::buildConfigForm() {
|
||||
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(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) {
|
||||
fprintf(stderr, "failed to open panda %s\n", serial.toUtf8().constData());
|
||||
has_panda = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (has_panda) {
|
||||
config.serial = serial.toStdString();
|
||||
config.bus_config.resize(3);
|
||||
for (int i = 0; i < config.bus_config.size(); i++) {
|
||||
QHBoxLayout *bus_layout = new QHBoxLayout;
|
||||
|
||||
// CAN Speed
|
||||
bus_layout->addWidget(new QLabel(tr("CAN Speed (kbps):")));
|
||||
QComboBox *can_speed = new QComboBox;
|
||||
for (int j = 0; j < std::size(speeds); j++) {
|
||||
can_speed->addItem(QString::number(speeds[j]));
|
||||
|
||||
if (data_speeds[j] == config.bus_config[i].can_speed_kbps) {
|
||||
can_speed->setCurrentIndex(j);
|
||||
}
|
||||
}
|
||||
QObject::connect(can_speed, qOverload<int>(&QComboBox::currentIndexChanged), [=](int index) {config.bus_config[i].can_speed_kbps = speeds[index];});
|
||||
bus_layout->addWidget(can_speed);
|
||||
|
||||
// CAN-FD Speed
|
||||
if (has_fd) {
|
||||
QCheckBox *enable_fd = new QCheckBox("CAN-FD");
|
||||
bus_layout->addWidget(enable_fd);
|
||||
bus_layout->addWidget(new QLabel(tr("Data Speed (kbps):")));
|
||||
QComboBox *data_speed = new QComboBox;
|
||||
for (int j = 0; j < std::size(data_speeds); j++) {
|
||||
data_speed->addItem(QString::number(data_speeds[j]));
|
||||
|
||||
if (data_speeds[j] == config.bus_config[i].data_speed_kbps) {
|
||||
data_speed->setCurrentIndex(j);
|
||||
}
|
||||
}
|
||||
|
||||
data_speed->setEnabled(false);
|
||||
bus_layout->addWidget(data_speed);
|
||||
|
||||
QObject::connect(data_speed, qOverload<int>(&QComboBox::currentIndexChanged), [=](int index) {config.bus_config[i].data_speed_kbps = data_speeds[index];});
|
||||
QObject::connect(enable_fd, &QCheckBox::stateChanged, data_speed, &QComboBox::setEnabled);
|
||||
QObject::connect(enable_fd, &QCheckBox::stateChanged, [=](int state) {config.bus_config[i].can_fd = (bool)state;});
|
||||
}
|
||||
|
||||
form_layout->addRow(tr("Bus %1:").arg(i), bus_layout);
|
||||
}
|
||||
} else {
|
||||
config.serial = "";
|
||||
form_layout->addWidget(new QLabel(tr("No panda found")));
|
||||
}
|
||||
}
|
||||
|
||||
AbstractStream *OpenPandaWidget::open() {
|
||||
try {
|
||||
return new PandaStream(qApp, config);
|
||||
} catch (std::exception &e) {
|
||||
QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to panda: '%1'").arg(e.what()));
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,9 @@
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QFormLayout>
|
||||
|
||||
#include "tools/cabana/streams/livestream.h"
|
||||
#include "tools/cabana/panda.h"
|
||||
|
||||
const uint32_t speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U};
|
||||
const uint32_t data_speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U, 2000U, 5000U};
|
||||
|
||||
struct BusConfig {
|
||||
int can_speed_kbps = 500;
|
||||
int data_speed_kbps = 2000;
|
||||
@@ -24,9 +18,8 @@ struct PandaStreamConfig {
|
||||
};
|
||||
|
||||
class PandaStream : public LiveStream {
|
||||
Q_OBJECT
|
||||
public:
|
||||
PandaStream(QObject *parent, PandaStreamConfig config_ = {});
|
||||
PandaStream(PandaStreamConfig config_ = {});
|
||||
~PandaStream() { stop(); }
|
||||
inline std::string routeName() const override {
|
||||
return "Panda: " + config.serial;
|
||||
@@ -39,19 +32,3 @@ protected:
|
||||
std::unique_ptr<Panda> panda;
|
||||
PandaStreamConfig config = {};
|
||||
};
|
||||
|
||||
class OpenPandaWidget : public AbstractOpenStreamWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
OpenPandaWidget(QWidget *parent = nullptr);
|
||||
AbstractStream *open() override;
|
||||
|
||||
private:
|
||||
void refreshSerials();
|
||||
void buildConfigForm();
|
||||
|
||||
QComboBox *serial_edit;
|
||||
QFormLayout *form_layout;
|
||||
PandaStreamConfig config = {};
|
||||
};
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
#include "tools/cabana/streams/replaystream.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include <QLabel>
|
||||
#include <QFileDialog>
|
||||
#include <QGridLayout>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <string>
|
||||
|
||||
#include "common/timing.h"
|
||||
#include "common/util.h"
|
||||
#include "tools/cabana/streams/routes.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
|
||||
ReplayStream::ReplayStream(QObject *parent) : AbstractStream(parent) {
|
||||
ReplayStream::ReplayStream() {
|
||||
unsetenv("ZMQ");
|
||||
setenv("COMMA_CACHE", "/tmp/comma_download_cache", 1);
|
||||
|
||||
op_prefix = std::make_unique<OpenpilotPrefix>();
|
||||
|
||||
QObject::connect(&settings, &Settings::changed, this, [this]() {
|
||||
settings_connection_ = settings.changed.connect([this]() {
|
||||
if (replay) replay->setSegmentCacheLimit(settings.max_cached_minutes);
|
||||
});
|
||||
}
|
||||
|
||||
ReplayStream::~ReplayStream() {
|
||||
cancelWaits();
|
||||
}
|
||||
|
||||
|
||||
|
||||
void ReplayStream::mergeSegments() {
|
||||
auto event_data = replay->getEventData();
|
||||
for (const auto &[n, seg] : event_data->segments) {
|
||||
@@ -31,58 +31,59 @@ void ReplayStream::mergeSegments() {
|
||||
|
||||
std::vector<const CanEvent *> new_events;
|
||||
new_events.reserve(seg->log->events.size());
|
||||
MessageEventsMap msg_events;
|
||||
for (const Event &e : seg->log->events) {
|
||||
if (e.which == cereal::Event::Which::CAN) {
|
||||
capnp::FlatArrayMessageReader reader(e.data);
|
||||
auto event = reader.getRoot<cereal::Event>();
|
||||
for (const auto &c : event.getCan()) {
|
||||
new_events.push_back(newEvent(e.mono_time, c));
|
||||
const CanEvent *ce = newEvent(e.mono_time, c);
|
||||
new_events.push_back(ce);
|
||||
msg_events[{.source = ce->src, .address = ce->address}].push_back(ce);
|
||||
}
|
||||
}
|
||||
}
|
||||
mergeEvents(new_events);
|
||||
postToMainThreadAndWait([&]() { insertEvents(new_events, msg_events); });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ReplayStream::loadRoute(const std::string &route, const std::string &data_dir, uint32_t replay_flags, bool auto_source) {
|
||||
replay.reset(new Replay(route, {"can", "roadEncodeIdx", "driverEncodeIdx", "wideRoadEncodeIdx", "carParams"},
|
||||
replay.reset(new Replay(route, {"can", "narrowRoadEncodeIdx", "cabinEncodeIdx", "wideRoadEncodeIdx", "carParams"},
|
||||
{}, nullptr, replay_flags, data_dir, auto_source));
|
||||
replay->setSegmentCacheLimit(settings.max_cached_minutes);
|
||||
replay->installEventFilter([this](const Event *event) { return eventFilter(event); });
|
||||
|
||||
// Forward replay callbacks to corresponding Qt signals.
|
||||
replay->onSeeking = [this](double sec) { emit seeking(sec); };
|
||||
|
||||
replay->onSeeking = [this](double sec) { postToMainThread([this, sec]() { seeking(sec); }); };
|
||||
replay->onSeekedTo = [this](double sec) {
|
||||
emit seekedTo(sec);
|
||||
postToMainThread([this, sec]() { seekedTo(sec); });
|
||||
waitForSeekFinshed();
|
||||
};
|
||||
replay->onQLogLoaded = [this](std::shared_ptr<LogReader> qlog) { emit qLogLoaded(qlog); };
|
||||
replay->onSegmentsMerged = [this]() { QMetaObject::invokeMethod(this, &ReplayStream::mergeSegments, Qt::BlockingQueuedConnection); };
|
||||
replay->onQLogLoaded = [this](std::shared_ptr<LogReader> qlog) { postToMainThread([this, qlog]() { qLogLoaded(qlog); }); };
|
||||
replay->onSegmentsMerged = [this]() { mergeSegments(); };
|
||||
|
||||
bool success = replay->load();
|
||||
if (!success) {
|
||||
std::string message;
|
||||
if (replay->lastRouteError() == RouteLoadError::Unauthorized) {
|
||||
auto auth_content = util::read_file(util::getenv("HOME") + "/.comma/auth.json");
|
||||
QString message;
|
||||
if (auth_content.empty()) {
|
||||
message = "Authentication Required. Please run the following command to authenticate:\n\n"
|
||||
"python3 openpilot/tools/lib/auth.py\n\n"
|
||||
"python3 iqpilot/tools/lib/auth.py\n\n"
|
||||
"This will grant access to routes from your comma account.";
|
||||
} else {
|
||||
message = tr("Access Denied. You do not have permission to access route:\n\n%1\n\n"
|
||||
"This is likely a private route.").arg(QString::fromStdString(route));
|
||||
message = "Access Denied. You do not have permission to access route:\n\n" + route + "\n\n"
|
||||
"This is likely a private route.";
|
||||
}
|
||||
QMessageBox::warning(nullptr, tr("Access Denied"), message);
|
||||
} else if (replay->lastRouteError() == RouteLoadError::NetworkError) {
|
||||
QMessageBox::warning(nullptr, tr("Network Error"),
|
||||
tr("Unable to load the route:\n\n %1.\n\nPlease check your network connection and try again.").arg(QString::fromStdString(route)));
|
||||
message = "Unable to load the route:\n\n " + route + ".\n\nPlease check your network connection and try again.";
|
||||
} else if (replay->lastRouteError() == RouteLoadError::FileNotFound) {
|
||||
QMessageBox::warning(nullptr, tr("Route Not Found"),
|
||||
tr("The specified route could not be found:\n\n %1.\n\nPlease check the route name and try again.").arg(QString::fromStdString(route)));
|
||||
message = "The specified route could not be found:\n\n " + route + ".\n\nPlease check the route name and try again.";
|
||||
} else {
|
||||
QMessageBox::warning(nullptr, tr("Route Load Failed"), tr("Failed to load route: '%1'").arg(QString::fromStdString(route)));
|
||||
message = "Failed to load route: '" + route + "'";
|
||||
}
|
||||
error(message);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
@@ -101,8 +102,8 @@ bool ReplayStream::eventFilter(const Event *event) {
|
||||
}
|
||||
|
||||
double ts = millis_since_boot();
|
||||
if ((ts - prev_update_ts) > (1000.0 / settings.fps)) {
|
||||
emit privateUpdateLastMsgsSignal();
|
||||
if ((ts - prev_update_ts) > (1000.0 / STREAM_UPDATE_FPS)) {
|
||||
requestUpdateLastMessages();
|
||||
prev_update_ts = ts;
|
||||
}
|
||||
return true;
|
||||
@@ -110,66 +111,5 @@ bool ReplayStream::eventFilter(const Event *event) {
|
||||
|
||||
void ReplayStream::pause(bool pause) {
|
||||
replay->pause(pause);
|
||||
emit(pause ? paused() : resume());
|
||||
}
|
||||
|
||||
|
||||
// OpenReplayWidget
|
||||
|
||||
OpenReplayWidget::OpenReplayWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) {
|
||||
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 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);
|
||||
|
||||
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(browse_local_btn, &QPushButton::clicked, [=]() {
|
||||
QString dir = QFileDialog::getExistingDirectory(this, tr("Open Local Route"), QString::fromStdString(settings.last_route_dir));
|
||||
if (!dir.isEmpty()) {
|
||||
route_edit->setText(dir);
|
||||
settings.last_route_dir = std::filesystem::absolute(dir.toStdString()).parent_path().string();
|
||||
}
|
||||
});
|
||||
QObject::connect(browse_remote_btn, &QPushButton::clicked, [this]() {
|
||||
RoutesDialog route_dlg(this);
|
||||
if (route_dlg.exec()) {
|
||||
route_edit->setText(route_dlg.route());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
AbstractStream *OpenReplayWidget::open() {
|
||||
QString route = route_edit->text();
|
||||
QString data_dir;
|
||||
if (int idx = route.lastIndexOf('/'); idx != -1 && util::file_exists(route.toStdString())) {
|
||||
data_dir = route.mid(0, idx + 1);
|
||||
route = route.mid(idx + 1);
|
||||
}
|
||||
|
||||
bool is_valid_format = Route::parseRoute(route.toStdString()).str.size() > 0;
|
||||
if (!is_valid_format) {
|
||||
QMessageBox::warning(nullptr, tr("Warning"), tr("Invalid route format: '%1'").arg(route));
|
||||
} else {
|
||||
auto replay_stream = std::make_unique<ReplayStream>(qApp);
|
||||
uint32_t flags = REPLAY_FLAG_NONE;
|
||||
if (cameras[1]->isChecked()) flags |= REPLAY_FLAG_DCAM;
|
||||
if (cameras[2]->isChecked()) flags |= REPLAY_FLAG_ECAM;
|
||||
if (flags == REPLAY_FLAG_NONE && !cameras[0]->isChecked()) flags = REPLAY_FLAG_NO_VIPC;
|
||||
|
||||
if (replay_stream->loadRoute(route.toStdString(), data_dir.toStdString(), flags)) {
|
||||
return replay_stream.release();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
pause ? paused() : resume();
|
||||
}
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
#include "common/prefix.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
#include "tools/replay/replay.h"
|
||||
|
||||
Q_DECLARE_METATYPE(std::shared_ptr<LogReader>);
|
||||
|
||||
class ReplayStream : public AbstractStream {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ReplayStream(QObject *parent);
|
||||
ReplayStream();
|
||||
~ReplayStream();
|
||||
void start() override { replay->start(); }
|
||||
bool loadRoute(const std::string &route, const std::string &data_dir, uint32_t replay_flags = REPLAY_FLAG_NONE, bool auto_source = false);
|
||||
bool eventFilter(const Event *event);
|
||||
@@ -36,24 +31,13 @@ public:
|
||||
inline bool isPaused() const override { return replay->isPaused(); }
|
||||
void pause(bool pause) override;
|
||||
|
||||
signals:
|
||||
void qLogLoaded(std::shared_ptr<LogReader> qlog);
|
||||
|
||||
Observable<std::shared_ptr<LogReader>> qLogLoaded;
|
||||
|
||||
private:
|
||||
void mergeSegments();
|
||||
std::unique_ptr<Replay> replay = nullptr;
|
||||
Connection settings_connection_;
|
||||
std::set<int> processed_segments;
|
||||
std::unique_ptr<OpenpilotPrefix> op_prefix;
|
||||
};
|
||||
|
||||
class OpenReplayWidget : public AbstractOpenStreamWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
OpenReplayWidget(QWidget *parent = nullptr);
|
||||
AbstractStream *open() override;
|
||||
|
||||
private:
|
||||
QLineEdit *route_edit;
|
||||
std::vector<QCheckBox *> cameras;
|
||||
};
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
#include "tools/cabana/streams/routes.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QListWidget>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
|
||||
#include "third_party/json11/json11.hpp"
|
||||
// IQ.Pilot patch: iqpilot's tools/replay has no py_downloader — device and route
|
||||
// listing come from the konn3kt API over libcurl. CommaApi2 returns the same JSON
|
||||
// shapes and the same {"error": ...} envelope upstream's PyDownloader produces, so
|
||||
// only the call sites change.
|
||||
#include "tools/replay/api.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Parse a konn3kt API JSON response into (success, error_code).
|
||||
std::pair<bool, int> checkApiResponse(const std::string &result) {
|
||||
if (result.empty()) return {false, 500};
|
||||
std::string err;
|
||||
auto doc = json11::Json::parse(result, err);
|
||||
if (!err.empty()) return {false, 500};
|
||||
if (doc.is_object() && doc["error"].is_string()) {
|
||||
return {false, doc["error"].string_value() == "unauthorized" ? 401 : 500};
|
||||
}
|
||||
return {true, 0};
|
||||
}
|
||||
|
||||
int64_t nowUnixMs() {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
// Parse ISO-8601 (with optional fractional seconds / Z) to unix ms. Returns 0 on failure.
|
||||
int64_t parseIsoToUnixMs(const std::string &s) {
|
||||
std::string bytes = s;
|
||||
if (!bytes.empty() && (bytes.back() == 'Z' || bytes.back() == 'z')) bytes.pop_back();
|
||||
int millis = 0;
|
||||
auto dot = bytes.find('.');
|
||||
if (dot != std::string::npos) {
|
||||
std::string frac = bytes.substr(dot + 1);
|
||||
bytes = bytes.substr(0, dot);
|
||||
while (frac.size() < 3) frac.push_back('0');
|
||||
millis = std::atoi(frac.substr(0, 3).c_str());
|
||||
}
|
||||
std::tm tm{};
|
||||
const char *ret = strptime(bytes.c_str(), "%Y-%m-%dT%H:%M:%S", &tm);
|
||||
if (!ret) ret = strptime(bytes.c_str(), "%Y-%m-%d %H:%M:%S", &tm);
|
||||
if (!ret) return 0;
|
||||
tm.tm_isdst = -1;
|
||||
time_t secs = timegm(&tm);
|
||||
if (secs == static_cast<time_t>(-1)) return 0;
|
||||
return static_cast<int64_t>(secs) * 1000 + millis;
|
||||
}
|
||||
|
||||
QString formatUnixMs(int64_t ms) {
|
||||
time_t secs = static_cast<time_t>(ms / 1000);
|
||||
std::tm tm{};
|
||||
localtime_r(&secs, &tm);
|
||||
char buf[64];
|
||||
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
|
||||
return QString::fromUtf8(buf);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// 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(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..."));
|
||||
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);
|
||||
period_selector_->addItem(tr("Preserved"), -1);
|
||||
|
||||
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);
|
||||
connect(button_box, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
// Fetch devices
|
||||
std::thread([this, alive = std::weak_ptr<bool>(alive_)]() {
|
||||
std::string result = CommaApi2::getDevices();
|
||||
QMetaObject::invokeMethod(qApp, [this, alive, r = QString::fromStdString(result), response = checkApiResponse(result)]() {
|
||||
if (!alive.expired()) parseDeviceList(r, response.first, response.second);
|
||||
}, Qt::QueuedConnection);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void RoutesDialog::parseDeviceList(const QString &json, bool success, int error_code) {
|
||||
if (success) {
|
||||
device_list_->clear();
|
||||
std::string err;
|
||||
auto doc = json11::Json::parse(json.toStdString(), err);
|
||||
if (err.empty() && doc.is_array()) {
|
||||
for (const auto &device : doc.array_items()) {
|
||||
QString dongle_id = QString::fromStdString(device["dongle_id"].string_value());
|
||||
device_list_->addItem(dongle_id, dongle_id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
QMessageBox::warning(this, tr("Error"), error_code == 401 ? tr("Unauthorized. Authenticate with tools/lib/auth.py") : tr("Network error"));
|
||||
reject();
|
||||
}
|
||||
}
|
||||
|
||||
void RoutesDialog::fetchRoutes() {
|
||||
if (device_list_->currentIndex() == -1 || device_list_->currentData().isNull())
|
||||
return;
|
||||
|
||||
route_list_->clear();
|
||||
route_list_->setEmptyText(tr("Loading..."));
|
||||
|
||||
std::string did = device_list_->currentText().toStdString();
|
||||
int period = period_selector_->currentData().toInt();
|
||||
|
||||
bool preserved = (period == -1);
|
||||
int64_t start_ms = 0, end_ms = 0;
|
||||
if (!preserved) {
|
||||
end_ms = nowUnixMs();
|
||||
start_ms = end_ms - static_cast<int64_t>(period) * 24LL * 60LL * 60LL * 1000LL;
|
||||
}
|
||||
|
||||
int request_id = ++fetch_id_;
|
||||
std::thread([this, alive = std::weak_ptr<bool>(alive_), did, start_ms, end_ms, preserved, request_id]() {
|
||||
std::string result = CommaApi2::getDeviceRoutes(did, start_ms, end_ms, preserved);
|
||||
QMetaObject::invokeMethod(qApp, [this, alive, r = QString::fromStdString(result), response = checkApiResponse(result), request_id]() {
|
||||
if (!alive.expired() && fetch_id_ == request_id) parseRouteList(r, response.first, response.second);
|
||||
}, Qt::QueuedConnection);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void RoutesDialog::parseRouteList(const QString &json, bool success, int error_code) {
|
||||
if (success) {
|
||||
std::string err;
|
||||
auto doc = json11::Json::parse(json.toStdString(), err);
|
||||
if (err.empty() && doc.is_array()) {
|
||||
for (const auto &route : doc.array_items()) {
|
||||
int64_t from_ms = 0, to_ms = 0;
|
||||
if (period_selector_->currentData().toInt() == -1) {
|
||||
from_ms = parseIsoToUnixMs(route["start_time"].string_value());
|
||||
to_ms = parseIsoToUnixMs(route["end_time"].string_value());
|
||||
} else {
|
||||
from_ms = static_cast<int64_t>(route["start_time_utc_millis"].number_value());
|
||||
to_ms = static_cast<int64_t>(route["end_time_utc_millis"].number_value());
|
||||
}
|
||||
const int mins = static_cast<int>((to_ms - from_ms) / 60000);
|
||||
auto item = new QListWidgetItem(QString("%1 %2min").arg(formatUnixMs(from_ms)).arg(mins));
|
||||
item->setData(Qt::UserRole, QString::fromStdString(route["fullname"].string_value()));
|
||||
route_list_->addItem(item);
|
||||
}
|
||||
}
|
||||
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"));
|
||||
}
|
||||
|
||||
QString RoutesDialog::route() {
|
||||
auto current_item = route_list_->currentItem();
|
||||
return current_item ? current_item->data(Qt::UserRole).toString() : "";
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
|
||||
class RouteListWidget;
|
||||
|
||||
class RoutesDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
RoutesDialog(QWidget *parent);
|
||||
QString route();
|
||||
|
||||
protected:
|
||||
void parseDeviceList(const QString &json, bool success, int error_code);
|
||||
void parseRouteList(const QString &json, bool success, int error_code);
|
||||
void fetchRoutes();
|
||||
|
||||
QComboBox *device_list_;
|
||||
QComboBox *period_selector_;
|
||||
RouteListWidget *route_list_;
|
||||
std::atomic<int> fetch_id_{0};
|
||||
// expires on destruction; guards main-thread callbacks from detached worker threads
|
||||
std::shared_ptr<bool> alive_ = std::make_shared<bool>(true);
|
||||
};
|
||||
@@ -8,15 +8,8 @@
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#include <QFormLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
|
||||
SocketCanStream::SocketCanStream(QObject *parent, SocketCanStreamConfig config_) : config(config_), LiveStream(parent) {
|
||||
SocketCanStream::SocketCanStream(SocketCanStreamConfig config_) : config(config_) {
|
||||
if (!available()) {
|
||||
throw std::runtime_error("SocketCAN not available");
|
||||
}
|
||||
@@ -49,7 +42,7 @@ bool SocketCanStream::connect() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enable CAN-FD
|
||||
|
||||
int fd_enable = 1;
|
||||
setsockopt(sock_fd, SOL_CAN_RAW, CAN_RAW_FD_FRAMES, &fd_enable, sizeof(fd_enable));
|
||||
|
||||
@@ -72,8 +65,8 @@ bool SocketCanStream::connect() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set read timeout so the thread can check for interruption
|
||||
struct timeval tv = {.tv_sec = 0, .tv_usec = 100000}; // 100ms
|
||||
|
||||
struct timeval tv = {.tv_sec = 0, .tv_usec = 100000};
|
||||
setsockopt(sock_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
|
||||
return true;
|
||||
@@ -86,7 +79,7 @@ void SocketCanStream::streamThread() {
|
||||
ssize_t nbytes = read(sock_fd, &frame, sizeof(frame));
|
||||
if (nbytes <= 0) continue;
|
||||
|
||||
uint8_t len = (nbytes == CAN_MTU) ? frame.len : frame.len; // works for both CAN and CAN-FD
|
||||
uint8_t len = (nbytes == CAN_MTU) ? frame.len : frame.len;
|
||||
|
||||
MessageBuilder msg;
|
||||
auto evt = msg.initEvent();
|
||||
@@ -98,51 +91,3 @@ void SocketCanStream::streamThread() {
|
||||
handleEvent(capnp::messageToFlatArray(msg));
|
||||
}
|
||||
}
|
||||
|
||||
OpenSocketCanWidget::OpenSocketCanWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
main_layout->addStretch(1);
|
||||
|
||||
QFormLayout *form_layout = new QFormLayout();
|
||||
|
||||
QHBoxLayout *device_layout = new QHBoxLayout();
|
||||
device_edit = new QComboBox();
|
||||
device_edit->setFixedWidth(300);
|
||||
device_layout->addWidget(device_edit);
|
||||
|
||||
QPushButton *refresh = new QPushButton(tr("Refresh"));
|
||||
refresh->setFixedWidth(100);
|
||||
device_layout->addWidget(refresh);
|
||||
form_layout->addRow(tr("Device"), device_layout);
|
||||
main_layout->addLayout(form_layout);
|
||||
|
||||
main_layout->addStretch(1);
|
||||
|
||||
QObject::connect(refresh, &QPushButton::clicked, this, &OpenSocketCanWidget::refreshDevices);
|
||||
QObject::connect(device_edit, &QComboBox::currentTextChanged, this, [=]{ config.device = device_edit->currentText().toStdString(); });
|
||||
|
||||
// Populate devices
|
||||
refreshDevices();
|
||||
}
|
||||
|
||||
void OpenSocketCanWidget::refreshDevices() {
|
||||
device_edit->clear();
|
||||
// Scan /sys/class/net/ for CAN interfaces (type 280 = ARPHRD_CAN)
|
||||
std::error_code ec;
|
||||
for (const auto &entry : std::filesystem::directory_iterator("/sys/class/net", ec)) {
|
||||
std::ifstream type_file(entry.path() / "type");
|
||||
int type = 0;
|
||||
if (type_file >> type && type == 280) {
|
||||
device_edit->addItem(QString::fromStdString(entry.path().filename().string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AbstractStream *OpenSocketCanWidget::open() {
|
||||
try {
|
||||
return new SocketCanStream(qApp, config);
|
||||
} catch (std::exception &e) {
|
||||
QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to SocketCAN device: '%1'").arg(e.what()));
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <QComboBox>
|
||||
|
||||
#include "tools/cabana/streams/livestream.h"
|
||||
|
||||
struct SocketCanStreamConfig {
|
||||
std::string device = ""; // TODO: support multiple devices/buses at once
|
||||
std::string device = "";
|
||||
};
|
||||
|
||||
class SocketCanStream : public LiveStream {
|
||||
Q_OBJECT
|
||||
public:
|
||||
SocketCanStream(QObject *parent, SocketCanStreamConfig config_ = {});
|
||||
SocketCanStream(SocketCanStreamConfig config_ = {});
|
||||
~SocketCanStream();
|
||||
static bool available();
|
||||
|
||||
@@ -26,17 +23,3 @@ protected:
|
||||
SocketCanStreamConfig config = {};
|
||||
int sock_fd = -1;
|
||||
};
|
||||
|
||||
class OpenSocketCanWidget : public AbstractOpenStreamWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
OpenSocketCanWidget(QWidget *parent = nullptr);
|
||||
AbstractStream *open() override;
|
||||
|
||||
private:
|
||||
void refreshDevices();
|
||||
|
||||
QComboBox *device_edit;
|
||||
SocketCanStreamConfig config = {};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user