diff --git a/SConstruct b/SConstruct
index 178b0cc872..e015218f2a 100644
--- a/SConstruct
+++ b/SConstruct
@@ -433,6 +433,10 @@ SConscript(['selfdrive/navd/SConscript'])
SConscript(['tools/replay/SConscript'])
+opendbc = abspath([File('opendbc/can/libdbc.so')])
+Export('opendbc')
+SConscript(['tools/cabana/SConscript'])
+
if GetOption('test'):
SConscript('panda/tests/safety/SConscript')
diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript
index 92f6578dfc..84e055752a 100644
--- a/selfdrive/ui/SConscript
+++ b/selfdrive/ui/SConscript
@@ -32,6 +32,7 @@ if maps:
qt_env['CPPDEFINES'] += ["ENABLE_MAPS"]
widgets = qt_env.Library("qt_widgets", widgets_src, LIBS=base_libs)
+Export('widgets')
qt_libs = [widgets, qt_util] + base_libs
# build assets
diff --git a/tools/cabana/.gitignore b/tools/cabana/.gitignore
new file mode 100644
index 0000000000..0c21d5530d
--- /dev/null
+++ b/tools/cabana/.gitignore
@@ -0,0 +1,4 @@
+moc_*
+*.moc
+
+_cabana
diff --git a/tools/cabana/README b/tools/cabana/README
new file mode 100644
index 0000000000..f64e6b2d2d
--- /dev/null
+++ b/tools/cabana/README
@@ -0,0 +1,9 @@
+# Cabana
+
+
+
+Cabana is a tool developed to view raw CAN data. One use for this is creating and editing [CAN Dictionaries](http://socialledge.com/sjsu/index.php/DBC_Format) (DBC files), and the tool provides direct integration with [commaai/opendbc](https://github.com/commaai/opendbc) (a collection of DBC files), allowing you to load the DBC files direct from source, and save to your fork. In addition, you can load routes from [comma connect](https://connect.comma.ai).
+
+## Usage Instructions
+
+See [openpilot wiki](https://github.com/commaai/openpilot/wiki/Cabana)
diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript
new file mode 100644
index 0000000000..f32ee166b6
--- /dev/null
+++ b/tools/cabana/SConscript
@@ -0,0 +1,20 @@
+import os
+Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc',
+ 'cereal', 'transformations', 'widgets', 'replay_lib', 'opendbc')
+
+base_frameworks = qt_env['FRAMEWORKS']
+base_libs = [common, messaging, cereal, visionipc, transformations, 'zmq',
+ 'capnp', 'kj', 'm', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"]
+
+if arch == "Darwin":
+ base_frameworks.append('OpenCL')
+else:
+ base_libs.append('OpenCL')
+
+qt_libs = ['qt_util', 'Qt5Charts'] + base_libs
+if arch in ['x86_64', 'Darwin'] and GetOption('extras'):
+ qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"]
+
+ # qt_env["LD_LIBRARY_PATH"] = [Dir(f"#opendbc/can").abspath]
+ cabana_libs = [widgets, cereal, messaging, visionipc, replay_lib, opendbc,'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv'] + qt_libs
+ qt_env.Program('_cabana', ['cabana.cc', 'mainwin.cc', 'chartswidget.cc', 'videowidget.cc', 'parser.cc', 'messageswidget.cc', 'detailwidget.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks)
diff --git a/tools/cabana/cabana b/tools/cabana/cabana
new file mode 100755
index 0000000000..b29dd66e3d
--- /dev/null
+++ b/tools/cabana/cabana
@@ -0,0 +1,4 @@
+#!/bin/sh
+cd "$(dirname "$0")"
+export LD_LIBRARY_PATH="../../opendbc/can:$LD_LIBRARY_PATH"
+exec ./_cabana "$1"
diff --git a/tools/cabana/cabana.cc b/tools/cabana/cabana.cc
new file mode 100644
index 0000000000..0adc744b49
--- /dev/null
+++ b/tools/cabana/cabana.cc
@@ -0,0 +1,34 @@
+#include
+#include
+
+#include "selfdrive/ui/qt/util.h"
+#include "tools/cabana/mainwin.h"
+
+const QString DEMO_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36";
+Parser *parser = nullptr;
+
+int main(int argc, char *argv[]) {
+ initApp(argc, argv);
+ QApplication app(argc, argv);
+
+ QCommandLineParser cmd_parser;
+ cmd_parser.addHelpOption();
+ cmd_parser.addPositionalArgument("route", "the drive to replay. find your drives at connect.comma.ai");
+ cmd_parser.addOption({"demo", "use a demo route instead of providing your own"});
+ cmd_parser.addOption({"data_dir", "local directory with routes", "data_dir"});
+ cmd_parser.addOption({"qcam", "use qcamera"});
+ cmd_parser.process(app);
+ const QStringList args = cmd_parser.positionalArguments();
+ if (args.empty() && !cmd_parser.isSet("demo")) {
+ cmd_parser.showHelp();
+ }
+
+ const QString route = args.empty() ? DEMO_ROUTE : args.first();
+ parser = new Parser(&app);
+ if (!parser->loadRoute(route, cmd_parser.value("data_dir"), cmd_parser.isSet("qcam"))) {
+ return 0;
+ }
+ MainWindow w;
+ w.showMaximized();
+ return app.exec();
+}
diff --git a/tools/cabana/chartswidget.cc b/tools/cabana/chartswidget.cc
new file mode 100644
index 0000000000..a8fe39968e
--- /dev/null
+++ b/tools/cabana/chartswidget.cc
@@ -0,0 +1,102 @@
+#include "tools/cabana/chartswidget.h"
+
+#include
+
+using namespace QtCharts;
+
+int64_t get_raw_value(const QByteArray &msg, const Signal &sig) {
+ int64_t ret = 0;
+
+ int i = sig.msb / 8;
+ int bits = sig.size;
+ while (i >= 0 && i < msg.size() && bits > 0) {
+ int lsb = (int)(sig.lsb / 8) == i ? sig.lsb : i * 8;
+ int msb = (int)(sig.msb / 8) == i ? sig.msb : (i + 1) * 8 - 1;
+ int size = msb - lsb + 1;
+
+ uint64_t d = (msg[i] >> (lsb - (i * 8))) & ((1ULL << size) - 1);
+ ret |= d << (bits - size);
+
+ bits -= size;
+ i = sig.is_little_endian ? i - 1 : i + 1;
+ }
+ return ret;
+}
+
+ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) {
+ main_layout = new QVBoxLayout(this);
+ main_layout->setContentsMargins(0, 0, 0, 0);
+ connect(parser, &Parser::updated, this, &ChartsWidget::updateState);
+ connect(parser, &Parser::showPlot, this, &ChartsWidget::addChart);
+ connect(parser, &Parser::hidePlot, this, &ChartsWidget::removeChart);
+}
+
+void ChartsWidget::addChart(const QString &id, const QString &sig_name) {
+ const QString char_name = id + sig_name;
+ if (charts.find(char_name) == charts.end()) {
+ QLineSeries *series = new QLineSeries();
+ series->setUseOpenGL(true);
+ auto chart = new QChart();
+ chart->setTitle(id + ": " + sig_name);
+ chart->addSeries(series);
+ chart->createDefaultAxes();
+ chart->legend()->hide();
+ auto chart_view = new QChartView(chart);
+ chart_view->setMinimumSize({width(), 300});
+ chart_view->setMaximumSize({width(), 300});
+ chart_view->setRenderHint(QPainter::Antialiasing);
+ main_layout->addWidget(chart_view);
+ charts[char_name] = {.id = id, .sig_name = sig_name, .chart_view = chart_view};
+ }
+}
+
+void ChartsWidget::removeChart(const QString &id, const QString &sig_name) {
+ auto it = charts.find(id + sig_name);
+ if (it == charts.end()) return;
+
+ delete it->second.chart_view;
+ charts.erase(it);
+}
+
+void ChartsWidget::updateState() {
+ static double last_update = millis_since_boot();
+ double current_ts = millis_since_boot();
+ bool update = (current_ts - last_update) > 500;
+ if (update) {
+ last_update = current_ts;
+ }
+
+ auto getSig = [=](const QString &id, const QString &name) -> const Signal * {
+ for (auto &sig : parser->getMsg(id)->sigs) {
+ if (name == sig.name.c_str()) return &sig;
+ }
+ return nullptr;
+ };
+
+ for (auto &[_, c] : charts) {
+ if (auto sig = getSig(c.id, c.sig_name)) {
+ const auto &can_data = parser->can_msgs[c.id].back();
+ int64_t val = get_raw_value(can_data.dat, *sig);
+ if (sig->is_signed) {
+ val -= ((val >> (sig->size - 1)) & 0x1) ? (1ULL << sig->size) : 0;
+ }
+ double value = val * sig->factor + sig->offset;
+
+ if (value > c.max_y) c.max_y = value;
+ if (value < c.min_y) c.min_y = value;
+
+ while (c.data.size() > DATA_LIST_SIZE) {
+ c.data.pop_front();
+ }
+ c.data.push_back({can_data.ts / 1000., value});
+
+ if (update) {
+ QChart *chart = c.chart_view->chart();
+ QLineSeries *series = (QLineSeries *)chart->series()[0];
+ series->replace(c.data);
+ chart->axisX()->setRange(c.data.front().x(), c.data.back().x());
+ chart->axisY()->setRange(c.min_y, c.max_y);
+ }
+ }
+ }
+}
diff --git a/tools/cabana/chartswidget.h b/tools/cabana/chartswidget.h
new file mode 100644
index 0000000000..7bc8335a32
--- /dev/null
+++ b/tools/cabana/chartswidget.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include