diff --git a/include/app/pages/vehicle.hpp b/include/app/pages/vehicle.hpp index 0401dcc3..2e88655e 100644 --- a/include/app/pages/vehicle.hpp +++ b/include/app/pages/vehicle.hpp @@ -81,11 +81,13 @@ class DataTab : public QWidget { private: Arbiter &arbiter; - QWidget *speedo_tach_widget(); + QWidget *speed_widget(); + QWidget *rpm_widget(); // QWidget *mileage_data_widget(); - QWidget *engine_data_widget(); QWidget *coolant_temp_widget(); QWidget *engine_load_widget(); + QWidget *intake_temp_widget(); + QWidget *maf_widget(); std::vector gauges; }; diff --git a/src/app/pages/vehicle.cpp b/src/app/pages/vehicle.cpp index 1193568e..648e3594 100644 --- a/src/app/pages/vehicle.cpp +++ b/src/app/pages/vehicle.cpp @@ -1,6 +1,11 @@ #include +#include +#include #include +#include +#include + #include "app/config.hpp" #include "app/pages/vehicle.hpp" #include "app/window.hpp" @@ -8,6 +13,170 @@ #include "canbus/elm327.hpp" #include "plugins/vehicle_plugin.hpp" +namespace { + +class RadialGaugeLabel : public QLabel +{ +public: + explicit RadialGaugeLabel(QWidget *parent = nullptr) + : QLabel(parent) + { + // The previous RPM gauge requested a 190x190 size. A fixed 200x200 + // canvas makes every gauge identical and is approximately 5% larger. + constexpr int gauge_size = 200; + + this->setAlignment(Qt::AlignCenter); + this->setAutoFillBackground(false); + this->setAttribute(Qt::WA_TranslucentBackground); + this->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + this->setFixedSize(gauge_size, gauge_size); + } + + QSize sizeHint() const override + { + return QSize(200, 200); + } + + QSize minimumSizeHint() const override + { + return QSize(200, 200); + } + + void set_gauge_fonts(const QFont &value_font, const QFont &unit_font) + { + this->value_font = value_font; + this->unit_font = unit_font; + this->update(); + } + + void set_gauge_unit(const QString &unit) + { + this->unit = unit; + this->update(); + } + + void set_gauge_value(const QString &text, double value, bool valid) + { + this->value_text = text; + this->value = value; + this->valid = valid; + this->update(); + } + +protected: + void paintEvent(QPaintEvent *) override + { + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, true); + + const qreal side = std::max(20.0, std::min(this->width(), this->height()) - 8.0); + const QPointF center(this->width() / 2.0, this->height() / 2.0); + const qreal ring_width = std::max(5.0, side * 0.045); + const QRectF ring_rect(center.x() - side / 2.0 + ring_width, + center.y() - side / 2.0 + ring_width, + side - ring_width * 2.0, + side - ring_width * 2.0); + const QRectF face_rect(center.x() - side / 2.0 + ring_width / 2.0, + center.y() - side / 2.0 + ring_width / 2.0, + side - ring_width, + side - ring_width); + + const QColor face(16, 16, 16); + const QColor outer_ring(95, 95, 95); + const QColor green(0, 210, 32); + const QColor red(255, 35, 35); + + painter.setPen(Qt::NoPen); + painter.setBrush(face); + painter.drawEllipse(face_rect); + + constexpr qreal start_angle = 225.0; + constexpr qreal span_angle = -270.0; + + QPen outer_pen(outer_ring, ring_width, Qt::SolidLine, Qt::RoundCap); + painter.setPen(outer_pen); + painter.setBrush(Qt::NoBrush); + painter.drawArc(ring_rect, static_cast(start_angle * 16.0), static_cast(span_angle * 16.0)); + + // Draw the warning zone at the high end of the gauge. + QPen warning_pen(red, ring_width * 0.82, Qt::SolidLine, Qt::RoundCap); + painter.setPen(warning_pen); + painter.drawArc(ring_rect, + static_cast((start_angle + span_angle * 0.84) * 16.0), + static_cast((span_angle * 0.16) * 16.0)); + + const double max_value = this->max_for_unit(this->unit); + double fraction = this->valid ? (this->value / max_value) : 0.0; + fraction = std::max(0.0, std::min(1.0, fraction)); + constexpr int segments = 28; + const int lit_segments = static_cast(std::round(fraction * segments)); + const qreal segment_span = span_angle / segments; + + QPen progress_pen(green, ring_width * 0.82, Qt::SolidLine, Qt::FlatCap); + painter.setPen(progress_pen); + for (int i = 0; i < lit_segments; ++i) { + const qreal segment_start = start_angle + segment_span * i; + painter.drawArc(ring_rect, + static_cast(segment_start * 16.0), + static_cast((segment_span * 0.62) * 16.0)); + } + + QFont display_value_font = this->value_font; + if (display_value_font.family().isEmpty()) + display_value_font = this->font(); + display_value_font.setBold(true); + + QFont display_unit_font = this->unit_font; + if (display_unit_font.family().isEmpty()) + display_unit_font = this->font(); + display_unit_font.setBold(true); + + painter.setPen(green); + painter.setFont(display_value_font); + QRectF value_rect(center.x() - side * 0.38, + center.y() - side * 0.20, + side * 0.76, + side * 0.30); + painter.drawText(value_rect, Qt::AlignCenter, this->value_text); + + painter.setFont(display_unit_font); + QRectF unit_rect(center.x() - side * 0.38, + center.y() + side * 0.08, + side * 0.76, + side * 0.20); + painter.drawText(unit_rect, Qt::AlignCenter, this->unit); + } + +private: + double max_for_unit(const QString &unit) const + { + if (unit == QStringLiteral("km/h")) + return 220.0; + if (unit == QStringLiteral("mph")) + return 140.0; + if (unit.contains(QStringLiteral("rpm"), Qt::CaseInsensitive)) + return 8.0; + if (unit == QStringLiteral("%")) + return 100.0; + if (unit == QStringLiteral("°C")) + return 120.0; + if (unit == QStringLiteral("°F")) + return 250.0; + if (unit == QStringLiteral("g/s")) + return 200.0; + return 100.0; + } + + bool valid = false; + double value = 0.0; + QString value_text = QStringLiteral("--"); + QString unit; + QFont value_font; + QFont unit_font; +}; + +} // namespace + Gauge::Gauge(units_t units, QFont value_font, QFont unit_font, Gauge::Orientation orientation, int rate, std::vector cmds, int precision, obd_decoder_t decoder, QWidget *parent) : QWidget(parent) @@ -49,14 +218,14 @@ Gauge::Gauge(units_t units, QFont value_font, QFont unit_font, Gauge::Orientatio layout = new QVBoxLayout(this); else layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); - value_label = new QLabel(this->null_value(), this); - value_label->setFont(value_font); - value_label->setAlignment(Qt::AlignCenter); - - QLabel *unit_label = new QLabel(this->si ? units.second : units.first, this); - unit_label->setFont(unit_font); - unit_label->setAlignment(Qt::AlignCenter); + value_label = new RadialGaugeLabel(this); + auto radial_gauge = static_cast(value_label); + radial_gauge->set_gauge_fonts(value_font, unit_font); + radial_gauge->set_gauge_unit(this->si ? units.second : units.first); + radial_gauge->set_gauge_value(this->null_value(), 0.0, false); this->timer = new QTimer(this); connect(this->timer, &QTimer::timeout, [this, bus, cmds]() { @@ -65,24 +234,24 @@ Gauge::Gauge(units_t units, QFont value_font, QFont unit_font, Gauge::Orientatio } }); - connect(config, &Config::si_units_changed, [this, units, unit_label](bool si) { + connect(config, &Config::si_units_changed, [this, units](bool si) { this->si = si; - unit_label->setText(this->si ? units.second : units.first); - value_label->setText(this->null_value()); + auto radial_gauge = static_cast(value_label); + radial_gauge->set_gauge_unit(this->si ? units.second : units.first); + radial_gauge->set_gauge_value(this->null_value(), 0.0, false); }); - layout->addStretch(6); - layout->addWidget(value_label); layout->addStretch(1); - layout->addWidget(unit_label); - layout->addStretch(4); + layout->addWidget(value_label, 0, Qt::AlignCenter); + layout->addStretch(1); } void Gauge::can_callback(QByteArray payload){ Response resp = Response(payload); for(auto cmd : cmds){ if(cmd.frame.payload().at(2) == resp.PID){ - value_label->setText(this->format_value(this->decoder(cmd.decoder(resp), this->si))); + double value = this->decoder(cmd.decoder(resp), this->si); + static_cast(value_label)->set_gauge_value(this->format_value(value), value, true); } } } @@ -331,66 +500,94 @@ DataTab::DataTab(Arbiter &arbiter, QWidget *parent) : QWidget(parent) , arbiter(arbiter) { - QHBoxLayout *layout = new QHBoxLayout(this); + // Use one grid for the entire Vehicle page so every gauge shares the + // same row and column geometry. This also removes the old vertical + // divider between the driving and engine-data sections. + QGridLayout *layout = new QGridLayout(this); + layout->setContentsMargins(16, 8, 16, 8); + layout->setHorizontalSpacing(20); + layout->setVerticalSpacing(12); + + layout->addWidget(this->speed_widget(), 0, 0, Qt::AlignCenter); + layout->addWidget(this->coolant_temp_widget(), 0, 1, Qt::AlignCenter); + layout->addWidget(this->intake_temp_widget(), 0, 2, Qt::AlignCenter); + layout->addWidget(this->rpm_widget(), 1, 0, Qt::AlignCenter); + layout->addWidget(this->engine_load_widget(), 1, 1, Qt::AlignCenter); + layout->addWidget(this->maf_widget(), 1, 2, Qt::AlignCenter); + + for (int column = 0; column < 3; ++column) + layout->setColumnStretch(column, 1); + for (int row = 0; row < 2; ++row) + layout->setRowStretch(row, 1); - QWidget *driving_data = this->speedo_tach_widget(); - layout->addWidget(driving_data); - layout->addWidget(Session::Forge::br(true)); - - QWidget *engine_data = this->engine_data_widget(); - layout->addWidget(engine_data); - - QSizePolicy sp_left(QSizePolicy::Preferred, QSizePolicy::Preferred); - sp_left.setHorizontalStretch(5); - driving_data->setSizePolicy(sp_left); - QSizePolicy sp_right(QSizePolicy::Preferred, QSizePolicy::Preferred); - sp_right.setHorizontalStretch(2); - engine_data->setSizePolicy(sp_right); for (auto &gauge : this->gauges) gauge->start(); } -QWidget *DataTab::speedo_tach_widget() +QWidget *DataTab::speed_widget() { QWidget *widget = new QWidget(this); QVBoxLayout *layout = new QVBoxLayout(widget); layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(4); - layout->addStretch(3); - - QFont speed_value_font(this->arbiter.forge().font(36, true)); + QFont value_font(this->arbiter.forge().font(38, true)); + value_font.setBold(true); - QFont speed_unit_font(this->arbiter.forge().font(16)); - speed_unit_font.setWeight(QFont::Light); - speed_unit_font.setItalic(true); + QFont unit_font(this->arbiter.forge().font(16)); + unit_font.setBold(true); + unit_font.setItalic(false); - Gauge *speed = new Gauge({"mph", "km/h"}, speed_value_font, speed_unit_font, + Gauge *speed = new Gauge({"mph", "km/h"}, value_font, unit_font, Gauge::BOTTOM, 100, {cmds.SPEED}, 0, [](double x, bool si) { return si ? x : kph_to_mph(x); }, widget); - layout->addWidget(speed); + layout->addWidget(speed, 0, Qt::AlignHCenter); this->gauges.push_back(speed); - layout->addStretch(2); + QFont label_font(this->arbiter.forge().font(14)); + label_font.setBold(true); - QFont tach_value_font(this->arbiter.forge().font(24, true)); + QLabel *speed_label = new QLabel("Speed", widget); + speed_label->setFont(label_font); + speed_label->setAlignment(Qt::AlignHCenter); + layout->addWidget(speed_label); - QFont tach_unit_font(this->arbiter.forge().font(12)); - tach_unit_font.setWeight(QFont::Light); - tach_unit_font.setItalic(true); + return widget; +} - Gauge *rpm = new Gauge({"x1000rpm", "x1000rpm"}, tach_value_font, - tach_unit_font, Gauge::BOTTOM, 100, {cmds.RPM}, 1, +QWidget *DataTab::rpm_widget() +{ + QWidget *widget = new QWidget(this); + QVBoxLayout *layout = new QVBoxLayout(widget); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(4); + + QFont value_font(this->arbiter.forge().font(38, true)); + value_font.setBold(true); + + QFont unit_font(this->arbiter.forge().font(16)); + unit_font.setBold(true); + unit_font.setItalic(false); + + Gauge *rpm = new Gauge({"x1000rpm", "x1000rpm"}, value_font, unit_font, + Gauge::BOTTOM, 100, {cmds.RPM}, 1, [](double x, bool _) { return x / 1000.0; }, widget); - layout->addWidget(rpm); + layout->addWidget(rpm, 0, Qt::AlignHCenter); this->gauges.push_back(rpm); - layout->addStretch(1); + QFont label_font(this->arbiter.forge().font(14)); + label_font.setBold(true); + + QLabel *rpm_label = new QLabel("RPM", widget); + rpm_label->setFont(label_font); + rpm_label->setAlignment(Qt::AlignHCenter); + layout->addWidget(rpm_label); return widget; } /* socketcan/elm327 rewrite right now only has support for one PID per gauge, so we can't calculate milage at this point. - This is because gauges act more as event handlers now for each PID. + This is because gauges act more as event handlers now for each PID. Multi-PID gauges could feasibly be reimplemented if there was a helper method that stored received values, and only calls the gauge update once all values have been updated since last gauge update. @@ -398,15 +595,15 @@ QWidget *DataTab::speedo_tach_widget() // QWidget *DataTab::mileage_data_widget() // { -// QWidget *widget = new QWidget(this); -// QHBoxLayout *layout = new QHBoxLayout(widget); -// -// QFont value_font(Theme::font_36); -// value_font.setFamily("Titillium Web"); -// -// QFont unit_font(Theme::font_14); -// unit_font.setWeight(QFont::Light); -// unit_font.setItalic(true); +// QWidget *widget = new QWidget(this); +// QHBoxLayout *layout = new QHBoxLayout(widget); +// +// QFont value_font(Theme::font_36); +// value_font.setFamily("Titillium Web"); +// +// QFont unit_font(Theme::font_14); +// unit_font.setWeight(QFont::Light); +// unit_font.setItalic(true); // // Gauge *mileage = new Gauge({"mpg", "km/L"}, value_font, unit_font, // Gauge::BOTTOM, 100, {cmds.SPEED, cmds.MAF}, 1, @@ -416,51 +613,34 @@ QWidget *DataTab::speedo_tach_widget() // widget); // layout->addWidget(mileage); // this->gauges.push_back(mileage); - +// // return widget; // } -QWidget *DataTab::engine_data_widget() -{ - QWidget *widget = new QWidget(this); - QVBoxLayout *layout = new QVBoxLayout(widget); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(0); - - layout->addStretch(); - layout->addWidget(this->coolant_temp_widget()); - layout->addStretch(); - layout->addWidget(Session::Forge::br()); - layout->addStretch(); - layout->addWidget(this->engine_load_widget()); - layout->addStretch(); - - return widget; -} - QWidget *DataTab::coolant_temp_widget() { QWidget *widget = new QWidget(this); QVBoxLayout *layout = new QVBoxLayout(widget); layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(0); + layout->setSpacing(4); - QFont value_font(this->arbiter.forge().font(16, true)); + QFont value_font(this->arbiter.forge().font(38, true)); + value_font.setBold(true); - QFont unit_font(this->arbiter.forge().font(12)); - unit_font.setWeight(QFont::Light); - unit_font.setItalic(true); + QFont unit_font(this->arbiter.forge().font(16)); + unit_font.setBold(true); + unit_font.setItalic(false); Gauge *coolant_temp = new Gauge( - {"°F", "°C"}, value_font, unit_font, Gauge::RIGHT, 5000, + {"°F", "°C"}, value_font, unit_font, Gauge::BOTTOM, 5000, {cmds.COOLANT_TEMP}, 1, [](double x, bool si) { return si ? x : c_to_f(x); }, widget); - layout->addWidget(coolant_temp); + layout->addWidget(coolant_temp, 0, Qt::AlignHCenter); this->gauges.push_back(coolant_temp); - QFont label_font(this->arbiter.forge().font(10)); - label_font.setWeight(QFont::Light); + QFont label_font(this->arbiter.forge().font(14)); + label_font.setBold(true); - QLabel *coolant_temp_label = new QLabel("coolant", widget); + QLabel *coolant_temp_label = new QLabel("Coolant", widget); coolant_temp_label->setFont(label_font); coolant_temp_label->setAlignment(Qt::AlignHCenter); layout->addWidget(coolant_temp_label); @@ -473,26 +653,92 @@ QWidget *DataTab::engine_load_widget() QWidget *widget = new QWidget(this); QVBoxLayout *layout = new QVBoxLayout(widget); layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(0); + layout->setSpacing(4); - QFont value_font(this->arbiter.forge().font(16, true)); + QFont value_font(this->arbiter.forge().font(38, true)); + value_font.setBold(true); - QFont unit_font(this->arbiter.forge().font(12)); - unit_font.setWeight(QFont::Light); - unit_font.setItalic(true); + QFont unit_font(this->arbiter.forge().font(16)); + unit_font.setBold(true); + unit_font.setItalic(false); Gauge *engine_load = - new Gauge({"%", "%"}, value_font, unit_font, Gauge::RIGHT, + new Gauge({"%", "%"}, value_font, unit_font, Gauge::BOTTOM, 500, {cmds.LOAD}, 1, [](double x, bool _) { return x; }, widget); - layout->addWidget(engine_load); + layout->addWidget(engine_load, 0, Qt::AlignHCenter); this->gauges.push_back(engine_load); - QFont label_font(this->arbiter.forge().font(10)); - label_font.setWeight(QFont::Light); + QFont label_font(this->arbiter.forge().font(14)); + label_font.setBold(true); - QLabel *engine_load_label = new QLabel("load", widget); + QLabel *engine_load_label = new QLabel("Load", widget); engine_load_label->setFont(label_font); engine_load_label->setAlignment(Qt::AlignHCenter); layout->addWidget(engine_load_label); + + return widget; +} + +QWidget *DataTab::intake_temp_widget() +{ + QWidget *widget = new QWidget(this); + QVBoxLayout *layout = new QVBoxLayout(widget); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(4); + + QFont value_font(this->arbiter.forge().font(38, true)); + value_font.setBold(true); + + QFont unit_font(this->arbiter.forge().font(16)); + unit_font.setBold(true); + unit_font.setItalic(false); + + Gauge *intake_temp = new Gauge( + {"°F", "°C"}, value_font, unit_font, Gauge::BOTTOM, 5000, + {cmds.INTAKE_TEMP}, 1, [](double x, bool si) { return si ? x : c_to_f(x); }, widget); + layout->addWidget(intake_temp, 0, Qt::AlignHCenter); + this->gauges.push_back(intake_temp); + + QFont label_font(this->arbiter.forge().font(14)); + label_font.setBold(true); + + QLabel *intake_temp_label = new QLabel("Intake Air", widget); + intake_temp_label->setFont(label_font); + intake_temp_label->setAlignment(Qt::AlignHCenter); + layout->addWidget(intake_temp_label); + + return widget; +} + +QWidget *DataTab::maf_widget() +{ + QWidget *widget = new QWidget(this); + QVBoxLayout *layout = new QVBoxLayout(widget); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(4); + + QFont value_font(this->arbiter.forge().font(38, true)); + value_font.setBold(true); + + QFont unit_font(this->arbiter.forge().font(16)); + unit_font.setBold(true); + unit_font.setItalic(false); + + // OBD-II PID 0x10 is conventionally reported in grams per second, + // including on many non-SI scan tools, so both unit modes use g/s. + Gauge *maf = new Gauge( + {"g/s", "g/s"}, value_font, unit_font, Gauge::BOTTOM, 500, + {cmds.MAF}, 1, [](double x, bool _) { return x; }, widget); + layout->addWidget(maf, 0, Qt::AlignHCenter); + this->gauges.push_back(maf); + + QFont label_font(this->arbiter.forge().font(14)); + label_font.setBold(true); + + QLabel *maf_label = new QLabel("MAF", widget); + maf_label->setFont(label_font); + maf_label->setAlignment(Qt::AlignHCenter); + layout->addWidget(maf_label); + return widget; }