feat:data slove and update heatmap
This commit is contained in:
@@ -1,131 +1,131 @@
|
||||
//
|
||||
// Created by Lenn on 2025/10/17.
|
||||
//
|
||||
|
||||
#ifndef TOUCHSENSOR_HEATMAP_H
|
||||
#define TOUCHSENSOR_HEATMAP_H
|
||||
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
||||
#include "modern-qt/utility/wrapper/property.hh"
|
||||
#include "qcustomplot/qcustomplot.h"
|
||||
#include "modern-qt/utility/wrapper/widget.hh"
|
||||
#include <concepts>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qvector.h>
|
||||
|
||||
struct point_data {
|
||||
double x;
|
||||
double y;
|
||||
double z;
|
||||
explicit point_data(double x, double y, double z) : x{x}, y{y}, z{z} {}
|
||||
};
|
||||
using PointData = struct point_data;
|
||||
namespace creeper {
|
||||
class HeatMapPlot;
|
||||
|
||||
namespace plot_widget::internal {
|
||||
class BasicPlot : public QCustomPlot {
|
||||
CREEPER_PIMPL_DEFINITION(BasicPlot)
|
||||
friend class HeatMapPlot;
|
||||
public:
|
||||
// BasicPlot();
|
||||
// ~BasicPlot();
|
||||
// BasicPlot(const BasicPlot&) = delete;
|
||||
// BasicPlot& operator=(const BasicPlot&) = delete;
|
||||
|
||||
void init_plot()const;
|
||||
void load_theme_manager(ThemeManager&)const;
|
||||
void set_xlabel_text(const QString&)const;
|
||||
void set_ylabel_text(const QString&)const;
|
||||
void set_matrix_size(const QSize&)const;
|
||||
void set_matrix_size(const int& w, const int& h)const;
|
||||
void set_data(const QVector<PointData>& data)const;
|
||||
void set_color_gradient_range(const double& min, const double& max)const;
|
||||
QSize get_matrix_size() const;
|
||||
bool is_initialized() const;
|
||||
|
||||
public slots:
|
||||
void update_dynamic_heatmap(const QVector<PointData>& map)const;
|
||||
void dataChanged(const QVector<PointData>& map)const;
|
||||
void dataRangeChanged(const double& min, const double& max)const;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent*) override;
|
||||
|
||||
private:
|
||||
friend struct Impl;
|
||||
|
||||
};
|
||||
} // namespace plot_widget::internal
|
||||
|
||||
namespace plot_widget::pro {
|
||||
using Token = common::Token<plot_widget::internal::BasicPlot>;
|
||||
|
||||
using XLabelText = common::pro::String<Token,
|
||||
[](auto& self, const auto& string) {
|
||||
self.set_xlabel_text(string);
|
||||
}>;
|
||||
|
||||
using YLabelText = common::pro::String<Token,
|
||||
[](auto& self, const auto& string) {
|
||||
self.set_ylabel_text(string);
|
||||
}>;
|
||||
|
||||
struct MatrixSize : Token {
|
||||
QSize size;
|
||||
explicit MatrixSize(const int& w, const int& h) : size{w, h} {}
|
||||
explicit MatrixSize(const QSize& s) : size{s} {}
|
||||
void apply(auto& self) const {
|
||||
self.set_matrix_size(size);
|
||||
}
|
||||
};
|
||||
|
||||
using Data = common::pro::Vector<Token, PointData,
|
||||
[](auto& self, const auto& data) {
|
||||
self.set_data(data);
|
||||
}>;
|
||||
|
||||
struct ColorRange : Token {
|
||||
double min;
|
||||
double max;
|
||||
explicit ColorRange(double min, double max) : min{min}, max{max} {}
|
||||
void apply(auto& self) const {
|
||||
self.set_color_gradient_range(min, max);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename F>
|
||||
using OnDataChanged =
|
||||
common::pro::SignalInjection<F, Token, &internal::BasicPlot::dataChanged>;
|
||||
|
||||
template <typename F>
|
||||
using OnDataRangeChanged =
|
||||
common::pro::SignalInjection<F, Token, &internal::BasicPlot::dataRangeChanged>;
|
||||
|
||||
template<class PlotWidget>
|
||||
concept trait = std::derived_from<PlotWidget, Token>;
|
||||
|
||||
using PlotData = common::pro::Vector<Token, PointData, [](auto& self, const auto& vec) {
|
||||
self.set_data(vec);
|
||||
}>;
|
||||
|
||||
using DataRange = common::pro::Array<Token, int, 2, [](auto& self, const auto& arr) {
|
||||
self.set_color_gradient_range(arr[0], arr[1]);
|
||||
}>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
using namespace widget::pro;
|
||||
using namespace theme::pro;
|
||||
}
|
||||
|
||||
struct HeatMapPlot
|
||||
: public Declarative<plot_widget::internal::BasicPlot,
|
||||
CheckerOr<plot_widget::pro::checker, widget::pro::checker, theme::pro::checker>> {
|
||||
using Declarative::Declarative;
|
||||
void paintEvent(QPaintEvent*) override;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // TOUCHSENSOR_HEATMAP_H
|
||||
//
|
||||
// Created by Lenn on 2025/10/17.
|
||||
//
|
||||
|
||||
#ifndef TOUCHSENSOR_HEATMAP_H
|
||||
#define TOUCHSENSOR_HEATMAP_H
|
||||
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
||||
#include "modern-qt/utility/wrapper/property.hh"
|
||||
#include "qcustomplot/qcustomplot.h"
|
||||
#include "modern-qt/utility/wrapper/widget.hh"
|
||||
#include <concepts>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qvector.h>
|
||||
|
||||
struct point_data {
|
||||
double x;
|
||||
double y;
|
||||
double z;
|
||||
explicit point_data(double x, double y, double z) : x{x}, y{y}, z{z} {}
|
||||
};
|
||||
using PointData = struct point_data;
|
||||
namespace creeper {
|
||||
class HeatMapPlot;
|
||||
|
||||
namespace plot_widget::internal {
|
||||
class BasicPlot : public QCustomPlot {
|
||||
CREEPER_PIMPL_DEFINITION(BasicPlot)
|
||||
friend class HeatMapPlot;
|
||||
public:
|
||||
// BasicPlot();
|
||||
// ~BasicPlot();
|
||||
// BasicPlot(const BasicPlot&) = delete;
|
||||
// BasicPlot& operator=(const BasicPlot&) = delete;
|
||||
|
||||
void init_plot()const;
|
||||
void load_theme_manager(ThemeManager&)const;
|
||||
void set_xlabel_text(const QString&)const;
|
||||
void set_ylabel_text(const QString&)const;
|
||||
void set_matrix_size(const QSize&)const;
|
||||
void set_matrix_size(const int& w, const int& h)const;
|
||||
void set_data(const QVector<PointData>& data)const;
|
||||
void set_color_gradient_range(const double& min, const double& max)const;
|
||||
QSize get_matrix_size() const;
|
||||
bool is_initialized() const;
|
||||
|
||||
public slots:
|
||||
void update_dynamic_heatmap(const QVector<PointData>& map)const;
|
||||
void dataChanged(const QVector<PointData>& map)const;
|
||||
void dataRangeChanged(const double& min, const double& max)const;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent*) override;
|
||||
|
||||
private:
|
||||
friend struct Impl;
|
||||
|
||||
};
|
||||
} // namespace plot_widget::internal
|
||||
|
||||
namespace plot_widget::pro {
|
||||
using Token = common::Token<plot_widget::internal::BasicPlot>;
|
||||
|
||||
using XLabelText = common::pro::String<Token,
|
||||
[](auto& self, const auto& string) {
|
||||
self.set_xlabel_text(string);
|
||||
}>;
|
||||
|
||||
using YLabelText = common::pro::String<Token,
|
||||
[](auto& self, const auto& string) {
|
||||
self.set_ylabel_text(string);
|
||||
}>;
|
||||
|
||||
struct MatrixSize : Token {
|
||||
QSize size;
|
||||
explicit MatrixSize(const int& w, const int& h) : size{w, h} {}
|
||||
explicit MatrixSize(const QSize& s) : size{s} {}
|
||||
void apply(auto& self) const {
|
||||
self.set_matrix_size(size);
|
||||
}
|
||||
};
|
||||
|
||||
using Data = common::pro::Vector<Token, PointData,
|
||||
[](auto& self, const auto& data) {
|
||||
self.set_data(data);
|
||||
}>;
|
||||
|
||||
struct ColorRange : Token {
|
||||
double min;
|
||||
double max;
|
||||
explicit ColorRange(double min, double max) : min{min}, max{max} {}
|
||||
void apply(auto& self) const {
|
||||
self.set_color_gradient_range(min, max);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename F>
|
||||
using OnDataChanged =
|
||||
common::pro::SignalInjection<F, Token, &internal::BasicPlot::dataChanged>;
|
||||
|
||||
template <typename F>
|
||||
using OnDataRangeChanged =
|
||||
common::pro::SignalInjection<F, Token, &internal::BasicPlot::dataRangeChanged>;
|
||||
|
||||
template<class PlotWidget>
|
||||
concept trait = std::derived_from<PlotWidget, Token>;
|
||||
|
||||
using PlotData = common::pro::Vector<Token, PointData, [](auto& self, const auto& vec) {
|
||||
self.set_data(vec);
|
||||
}>;
|
||||
|
||||
using DataRange = common::pro::Array<Token, int, 2, [](auto& self, const auto& arr) {
|
||||
self.set_color_gradient_range(arr[0], arr[1]);
|
||||
}>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
using namespace widget::pro;
|
||||
using namespace theme::pro;
|
||||
}
|
||||
|
||||
struct HeatMapPlot
|
||||
: public Declarative<plot_widget::internal::BasicPlot,
|
||||
CheckerOr<plot_widget::pro::checker, widget::pro::checker, theme::pro::checker>> {
|
||||
using Declarative::Declarative;
|
||||
void paintEvent(QPaintEvent*) override;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // TOUCHSENSOR_HEATMAP_H
|
||||
|
||||
@@ -1,197 +1,177 @@
|
||||
//
|
||||
// Created by Lenn on 2025/10/17.
|
||||
//
|
||||
|
||||
#ifndef TOUCHSENSOR_HEATMAP_IMPL_HH
|
||||
#define TOUCHSENSOR_HEATMAP_IMPL_HH
|
||||
|
||||
#include "heatmap.hh"
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/widget/sliders.hh"
|
||||
#include <memory>
|
||||
#include <qcolor.h>
|
||||
#include <qdebug.h>
|
||||
using namespace creeper::plot_widget::internal;
|
||||
|
||||
struct BasicPlot::Impl {
|
||||
explicit Impl(BasicPlot& self) noexcept : self{self}, initialized(false), matrix_size(QSize{3, 4}) {}
|
||||
|
||||
public:
|
||||
auto set_xlabel_text(const QString& text) -> void {
|
||||
xlabel = text;
|
||||
if (initialized) {
|
||||
self.xAxis->setLabel(text);
|
||||
self.replot();
|
||||
}
|
||||
}
|
||||
|
||||
auto set_ylabel_text(const QString& text) -> void {
|
||||
ylabel = text;
|
||||
if (initialized) {
|
||||
self.yAxis->setLabel(text);
|
||||
self.replot();
|
||||
}
|
||||
}
|
||||
|
||||
auto set_matrix_size(const QSize& size) -> void {
|
||||
matrix_size = size;
|
||||
if (initialized) {
|
||||
// 重新初始化热力图以适应新的矩阵大小
|
||||
reset_plot();
|
||||
if (!data_points.isEmpty()) {
|
||||
set_data(data_points);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto load_theme_manager(ThemeManager& mgr) -> void {
|
||||
mgr.append_handler(&self, [this](const ThemeManager& mgr) {
|
||||
// 可以根据主题更新颜色渐变等
|
||||
if (initialized) {
|
||||
self.replot();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
auto set_color_gradient_range(const double& min, const double& max) -> void {
|
||||
if (initialized && self.plottableCount() > 0) {
|
||||
auto* cpmp = static_cast<QCPColorMap*>(self.plottable(0));
|
||||
cpmp->setDataRange(QCPRange(min, max));
|
||||
self.replot();
|
||||
}
|
||||
color_min = min;
|
||||
color_max = max;
|
||||
}
|
||||
|
||||
auto set_data(const QVector<PointData>& data) -> void {
|
||||
data_points = data;
|
||||
if (initialized) {
|
||||
update_plot_data();
|
||||
}
|
||||
}
|
||||
|
||||
auto initialize_plot() -> void {
|
||||
if (initialized) return;
|
||||
|
||||
QCPColorMap* cpmp = new QCPColorMap(self.xAxis, self.yAxis);
|
||||
cpmp->data()->setSize(matrix_size.width(), matrix_size.height());
|
||||
cpmp->data()->setRange(QCPRange(0.5, matrix_size.width() - 0.5),
|
||||
QCPRange(0.5, matrix_size.height() - 0.5));
|
||||
|
||||
// 配置坐标轴
|
||||
QSharedPointer<QCPAxisTickerText> xticker(new QCPAxisTickerText);
|
||||
QSharedPointer<QCPAxisTickerText> yticker(new QCPAxisTickerText);
|
||||
xticker->setSubTickCount(1);
|
||||
yticker->setSubTickCount(1);
|
||||
self.xAxis->setVisible(false);
|
||||
self.yAxis->setVisible(false);
|
||||
self.xAxis->setTicker(xticker);
|
||||
self.yAxis->setTicker(yticker);
|
||||
|
||||
// 设置网格
|
||||
self.xAxis->grid()->setPen(Qt::NoPen);
|
||||
self.yAxis->grid()->setPen(Qt::NoPen);
|
||||
self.xAxis->grid()->setSubGridVisible(true);
|
||||
self.yAxis->grid()->setSubGridVisible(true);
|
||||
self.xAxis->setSubTicks(true);
|
||||
self.yAxis->setSubTicks(true);
|
||||
self.xAxis->setTickLength(0);
|
||||
self.yAxis->setTickLength(0);
|
||||
self.xAxis->setSubTickLength(6);
|
||||
self.yAxis->setSubTickLength(6);
|
||||
|
||||
// 设置范围
|
||||
self.xAxis->setRange(0, matrix_size.width());
|
||||
self.yAxis->setRange(0, matrix_size.height());
|
||||
|
||||
// 设置标签
|
||||
if (!xlabel.isEmpty()) self.xAxis->setLabel(xlabel);
|
||||
if (!ylabel.isEmpty()) self.yAxis->setLabel(ylabel);
|
||||
|
||||
// 添加/复用颜色刻度(避免重复 addElement 到相同单元格导致告警)
|
||||
QCPLayoutElement* occupied = self.plotLayout()->element(1, 0);
|
||||
QCPColorScale* color_scale = occupied ? qobject_cast<QCPColorScale*>(occupied) : nullptr;
|
||||
if (!color_scale) {
|
||||
if (occupied) {
|
||||
// 单元格被其他元素占用,移除并删除后再放入 ColorScale
|
||||
self.plotLayout()->remove(occupied);
|
||||
delete occupied;
|
||||
occupied = nullptr;
|
||||
}
|
||||
color_scale = new QCPColorScale(&self);
|
||||
color_scale->setType(QCPAxis::atBottom);
|
||||
self.plotLayout()->addElement(1, 0, color_scale);
|
||||
}
|
||||
cpmp->setColorScale(color_scale);
|
||||
|
||||
// 设置颜色渐变
|
||||
QCPColorGradient gradient;
|
||||
gradient.setColorStopAt(0.0, QColor(246, 239, 166)); // F6EFA6
|
||||
gradient.setColorStopAt(1.0, QColor(191, 68, 76)); // BF444C
|
||||
cpmp->setGradient(gradient);
|
||||
|
||||
// 设置数据范围
|
||||
cpmp->setDataRange(QCPRange(color_min, color_max));
|
||||
cpmp->setInterpolate(false);
|
||||
|
||||
// 配置边距
|
||||
QCPMarginGroup *margin_group = new QCPMarginGroup(&self);
|
||||
self.axisRect()->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
|
||||
color_scale->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
|
||||
|
||||
initialized = true;
|
||||
|
||||
// 如果已有数据,更新图表
|
||||
if (!data_points.isEmpty()) {
|
||||
update_plot_data();
|
||||
}
|
||||
}
|
||||
|
||||
auto reset_plot() -> void {
|
||||
// 清除所有绘图元素
|
||||
self.clearPlottables();
|
||||
self.clearGraphs();
|
||||
self.clearItems();
|
||||
self.clearFocus();
|
||||
|
||||
// 重新初始化
|
||||
initialized = false;
|
||||
initialize_plot();
|
||||
}
|
||||
auto update_plot_data() -> void {
|
||||
if (!initialized || self.plottableCount() == 0) return;
|
||||
|
||||
auto* cpmp = static_cast<QCPColorMap*>(self.plottable(0));
|
||||
|
||||
// 设置新数据
|
||||
for (const auto& item : data_points) {
|
||||
if (item.x >= 0 && item.x < matrix_size.width() &&
|
||||
item.y >= 0 && item.y < matrix_size.height()) {
|
||||
cpmp->data()->setCell(item.x, item.y, item.z);
|
||||
}
|
||||
}
|
||||
|
||||
// 重绘
|
||||
self.replot();
|
||||
}
|
||||
|
||||
auto is_plot_initialized() const -> bool {
|
||||
return initialized;
|
||||
}
|
||||
|
||||
auto get_matrix_size() const -> QSize {
|
||||
return matrix_size;
|
||||
}
|
||||
|
||||
private:
|
||||
QString xlabel;
|
||||
QString ylabel;
|
||||
QSize matrix_size;
|
||||
QVector<PointData> data_points;
|
||||
double color_min = 0.0;
|
||||
double color_max = 800.0;
|
||||
bool initialized;
|
||||
BasicPlot& self;
|
||||
};
|
||||
|
||||
#endif // TOUCHSENSOR_HEATMAP_IMPL_HH
|
||||
//
|
||||
// Created by Lenn on 2025/10/17.
|
||||
//
|
||||
|
||||
#ifndef TOUCHSENSOR_HEATMAP_IMPL_HH
|
||||
#define TOUCHSENSOR_HEATMAP_IMPL_HH
|
||||
|
||||
#include "heatmap.hh"
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/widget/sliders.hh"
|
||||
#include <memory>
|
||||
#include <qcolor.h>
|
||||
#include <qdebug.h>
|
||||
using namespace creeper::plot_widget::internal;
|
||||
|
||||
struct BasicPlot::Impl {
|
||||
explicit Impl(BasicPlot& self) noexcept : self{self}, initialized(false), matrix_size(QSize{3, 4}) {}
|
||||
|
||||
public:
|
||||
auto set_xlabel_text(const QString& text) -> void {
|
||||
xlabel = text;
|
||||
if (initialized) {
|
||||
self.xAxis->setLabel(text);
|
||||
self.replot();
|
||||
}
|
||||
}
|
||||
|
||||
auto set_ylabel_text(const QString& text) -> void {
|
||||
ylabel = text;
|
||||
if (initialized) {
|
||||
self.yAxis->setLabel(text);
|
||||
self.replot();
|
||||
}
|
||||
}
|
||||
|
||||
auto set_matrix_size(const QSize& size) -> void {
|
||||
matrix_size = size;
|
||||
if (initialized) {
|
||||
reset_plot();
|
||||
if (!data_points.isEmpty()) {
|
||||
set_data(data_points);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto load_theme_manager(ThemeManager& mgr) -> void {
|
||||
mgr.append_handler(&self, [this](const ThemeManager& mgr) {
|
||||
if (initialized) {
|
||||
self.replot();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
auto set_color_gradient_range(const double& min, const double& max) -> void {
|
||||
if (initialized && self.plottableCount() > 0) {
|
||||
auto* cpmp = static_cast<QCPColorMap*>(self.plottable(0));
|
||||
cpmp->setDataRange(QCPRange(min, max));
|
||||
self.replot();
|
||||
}
|
||||
color_min = min;
|
||||
color_max = max;
|
||||
}
|
||||
|
||||
auto set_data(const QVector<PointData>& data) -> void {
|
||||
data_points = data;
|
||||
if (initialized) {
|
||||
update_plot_data();
|
||||
}
|
||||
}
|
||||
|
||||
auto initialize_plot() -> void {
|
||||
if (initialized) return;
|
||||
|
||||
QCPColorMap* cpmp = new QCPColorMap(self.xAxis, self.yAxis);
|
||||
cpmp->data()->setSize(matrix_size.width(), matrix_size.height());
|
||||
cpmp->data()->setRange(QCPRange(0.5, matrix_size.width() - 0.5),
|
||||
QCPRange(0.5, matrix_size.height() - 0.5));
|
||||
|
||||
QSharedPointer<QCPAxisTickerText> xticker(new QCPAxisTickerText);
|
||||
QSharedPointer<QCPAxisTickerText> yticker(new QCPAxisTickerText);
|
||||
xticker->setSubTickCount(1);
|
||||
yticker->setSubTickCount(1);
|
||||
self.xAxis->setVisible(false);
|
||||
self.yAxis->setVisible(false);
|
||||
self.xAxis->setTicker(xticker);
|
||||
self.yAxis->setTicker(yticker);
|
||||
|
||||
self.xAxis->grid()->setPen(Qt::NoPen);
|
||||
self.yAxis->grid()->setPen(Qt::NoPen);
|
||||
self.xAxis->grid()->setSubGridVisible(true);
|
||||
self.yAxis->grid()->setSubGridVisible(true);
|
||||
self.xAxis->setSubTicks(true);
|
||||
self.yAxis->setSubTicks(true);
|
||||
self.xAxis->setTickLength(0);
|
||||
self.yAxis->setTickLength(0);
|
||||
self.xAxis->setSubTickLength(6);
|
||||
self.yAxis->setSubTickLength(6);
|
||||
|
||||
|
||||
self.xAxis->setRange(0, matrix_size.width());
|
||||
self.yAxis->setRange(0, matrix_size.height());
|
||||
|
||||
if (!xlabel.isEmpty()) self.xAxis->setLabel(xlabel);
|
||||
if (!ylabel.isEmpty()) self.yAxis->setLabel(ylabel);
|
||||
|
||||
QCPColorScale* color_scale = new QCPColorScale(&self);
|
||||
color_scale->setType(QCPAxis::atBottom);
|
||||
self.plotLayout()->addElement(1, 0, color_scale);
|
||||
cpmp->setColorScale(color_scale);
|
||||
|
||||
QCPColorGradient gradient;
|
||||
gradient.setColorStopAt(0.0, QColor(246, 239, 166)); // F6EFA6
|
||||
gradient.setColorStopAt(1.0, QColor(191, 68, 76)); // BF444C
|
||||
cpmp->setGradient(gradient);
|
||||
|
||||
cpmp->setDataRange(QCPRange(color_min, color_max));
|
||||
cpmp->setInterpolate(false);
|
||||
|
||||
QCPMarginGroup *margin_group = new QCPMarginGroup(&self);
|
||||
self.axisRect()->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
|
||||
color_scale->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
|
||||
|
||||
initialized = true;
|
||||
|
||||
if (!data_points.isEmpty()) {
|
||||
update_plot_data();
|
||||
}
|
||||
}
|
||||
|
||||
auto reset_plot() -> void {
|
||||
// 清除所有绘图元素
|
||||
self.clearPlottables();
|
||||
self.clearGraphs();
|
||||
self.clearItems();
|
||||
self.clearFocus();
|
||||
|
||||
// 重新初始化
|
||||
initialized = false;
|
||||
initialize_plot();
|
||||
}
|
||||
auto update_plot_data() -> void {
|
||||
if (!initialized || self.plottableCount() == 0) return;
|
||||
|
||||
auto* cpmp = static_cast<QCPColorMap*>(self.plottable(0));
|
||||
|
||||
// 设置新数据
|
||||
for (const auto& item : data_points) {
|
||||
if (item.x >= 0 && item.x < matrix_size.width() &&
|
||||
item.y >= 0 && item.y < matrix_size.height()) {
|
||||
cpmp->data()->setCell(item.x, item.y, item.z);
|
||||
}
|
||||
}
|
||||
|
||||
// 重绘
|
||||
self.replot();
|
||||
}
|
||||
|
||||
auto is_plot_initialized() const -> bool {
|
||||
return initialized;
|
||||
}
|
||||
|
||||
auto get_matrix_size() const -> QSize {
|
||||
return matrix_size;
|
||||
}
|
||||
|
||||
private:
|
||||
QString xlabel;
|
||||
QString ylabel;
|
||||
QSize matrix_size;
|
||||
QVector<PointData> data_points;
|
||||
double color_min = 0.0;
|
||||
double color_max = 800.0;
|
||||
bool initialized;
|
||||
BasicPlot& self;
|
||||
};
|
||||
|
||||
#endif // TOUCHSENSOR_HEATMAP_IMPL_HH
|
||||
|
||||
@@ -1,125 +1,119 @@
|
||||
#pragma once
|
||||
|
||||
#include "components/ffmsep/cpdecoder.hh"
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include <initializer_list>
|
||||
|
||||
namespace ffmsep {
|
||||
|
||||
inline constexpr int CP_SUCCESS = 0;
|
||||
inline constexpr int CP_ERROR_EOF = -1;
|
||||
inline constexpr int CP_ERROR_EAGAIN = -2;
|
||||
inline constexpr int CP_ERROR_NOT_OPEN = -3;
|
||||
inline constexpr int CP_ERROR_INVALID_STATE = -4;
|
||||
inline constexpr int CP_ERROR_INVALID_ARGUMENT = -5;
|
||||
|
||||
enum class CPMediaType : std::uint8_t {
|
||||
Unknow = 0,
|
||||
Data,
|
||||
};
|
||||
|
||||
enum class CPCodecID : std::uint32_t {
|
||||
Unknow = 0,
|
||||
Tactile = 0x54514354u // 'T','Q','C','T':触觉传感器协议标识 Tactile Quick Codec Type
|
||||
};
|
||||
|
||||
struct CPPacket {
|
||||
std::vector<std::uint8_t> payload;
|
||||
std::int64_t pts = 0;
|
||||
std::int64_t dts = 0;
|
||||
bool end_of_stream = false;
|
||||
bool flush = false;
|
||||
|
||||
CPPacket() = default;
|
||||
CPPacket(std::vector<std::uint8_t> data, std::int64_t pts_value = 0, std::int64_t dts_value = 0) noexcept
|
||||
: payload(std::move(data)), pts(pts_value), dts(dts_value) {}
|
||||
|
||||
[[nodiscard]] bool empty() const noexcept {return payload.empty();}
|
||||
};
|
||||
|
||||
struct CPFrame {
|
||||
std::vector<std::uint8_t> data;
|
||||
std::int64_t pts = 0;
|
||||
bool key_frame = false;
|
||||
bool valid = false;
|
||||
|
||||
void reset() noexcept {
|
||||
data.clear();
|
||||
key_frame = false;
|
||||
valid = false;
|
||||
pts = 0;
|
||||
}
|
||||
};
|
||||
|
||||
struct CPCodecContext;
|
||||
|
||||
struct CPCodec {
|
||||
using InitFn = int(*)(CPCodecContext*);
|
||||
using CloseFn = void(*)(CPCodecContext*);
|
||||
using SendPacketFn = int(*)(CPCodecContext*, const CPPacket&);
|
||||
using ReceiveFrameFn = int(*)(CPCodecContext*, CPFrame&);
|
||||
|
||||
const char* name = nullptr;
|
||||
const char* long_name = nullptr;
|
||||
CPMediaType type = CPMediaType::Unknow;
|
||||
CPCodecID id = CPCodecID::Unknow;
|
||||
std::size_t priv_data_size = 0;
|
||||
InitFn init = nullptr;
|
||||
CloseFn close = nullptr;
|
||||
SendPacketFn send_packet = nullptr;
|
||||
ReceiveFrameFn receive_frame = nullptr;
|
||||
};
|
||||
|
||||
struct CPCodecContext {
|
||||
const CPCodec* codec = nullptr;
|
||||
void* priv_data = nullptr;
|
||||
CPMediaType codec_type = CPMediaType::Unknow;
|
||||
bool is_open = false;
|
||||
|
||||
void clear() noexcept {
|
||||
codec = nullptr;
|
||||
priv_data = nullptr;
|
||||
codec_type = CPMediaType::Unknow;
|
||||
is_open = false;
|
||||
priv_storage.clear();
|
||||
}
|
||||
|
||||
void* ensure_priv_storage(std::size_t size);
|
||||
void release_priv_storage() noexcept;
|
||||
|
||||
template<typename T>
|
||||
[[nodiscard]] T* priv_as() noexcept {
|
||||
return static_cast<T*>(priv_data);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
[[nodiscard]] const T* priv_as() const noexcept {
|
||||
return static_cast<const T*>(priv_data);
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::uint8_t> priv_storage;
|
||||
friend CPCodecContext* cpcodec_alloc_context(const CPCodec*);
|
||||
friend int cpcodec_open(CPCodecContext*, const CPCodec*);
|
||||
friend int cpcodec_close(CPCodecContext*);
|
||||
};
|
||||
|
||||
void cpcodec_register(const CPCodec* codec);
|
||||
void cpcodec_register_many(std::initializer_list<const CPCodec*> codecs);
|
||||
const CPCodec* cpcodec_find_decoder(CPCodecID id);
|
||||
const CPCodec* cpcodec_find_decoder_by_name(std::string_view name);
|
||||
std::vector<const CPCodec*> cpcodec_list_codecs();
|
||||
|
||||
CPCodecContext* cpcodec_alloc_context(const CPCodec* codec);
|
||||
int cpcodec_open(CPCodecContext*, const CPCodec*);
|
||||
int cpcodec_close(CPCodecContext*);
|
||||
void cpcodec_free_context(CPCodecContext **ctx);
|
||||
int cpcodec_send_packet(CPCodecContext*, const CPPacket*);
|
||||
int cpcodec_receive_frame(CPCodecContext*, CPFrame*);
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include <initializer_list>
|
||||
|
||||
namespace ffmsep {
|
||||
|
||||
inline constexpr int CP_SUCCESS = 0;
|
||||
inline constexpr int CP_ERROR_EOF = -1;
|
||||
inline constexpr int CP_ERROR_EAGAIN = -2;
|
||||
inline constexpr int CP_ERROR_NOT_OPEN = -3;
|
||||
inline constexpr int CP_ERROR_INVALID_STATE = -4;
|
||||
inline constexpr int CP_ERROR_INVALID_ARGUMENT = -5;
|
||||
|
||||
enum class CPMediaType : std::uint8_t {
|
||||
Unknow = 0,
|
||||
Data,
|
||||
};
|
||||
|
||||
enum class CPCodecID : std::uint32_t {
|
||||
Unknow = 0,
|
||||
Tactile = 0x54514354u // 'T','Q','C','T':触觉传感器协议标识 Tactile Quick Codec Type
|
||||
};
|
||||
|
||||
struct CPPacket {
|
||||
std::vector<std::uint8_t> payload;
|
||||
std::int64_t pts = 0;
|
||||
std::int64_t dts = 0;
|
||||
bool end_of_stream = false;
|
||||
bool flush = false;
|
||||
|
||||
CPPacket() = default;
|
||||
CPPacket(std::vector<std::uint8_t> data, std::int64_t pts_value = 0, std::int64_t dts_value = 0) noexcept
|
||||
: payload(std::move(data)), pts(pts_value), dts(dts_value) {}
|
||||
|
||||
[[nodiscard]] bool empty() const noexcept {return payload.empty();}
|
||||
};
|
||||
|
||||
struct CPFrame {
|
||||
std::vector<std::uint8_t> data;
|
||||
std::int64_t pts = 0;
|
||||
bool key_frame = false;
|
||||
bool valid = false;
|
||||
|
||||
void reset() noexcept {
|
||||
data.clear();
|
||||
key_frame = false;
|
||||
valid = false;
|
||||
pts = 0;
|
||||
}
|
||||
};
|
||||
|
||||
struct CPCodecContext;
|
||||
|
||||
struct CPCodec {
|
||||
using InitFn = int(*)(CPCodecContext*);
|
||||
using CloseFn = void(*)(CPCodecContext*);
|
||||
using SendPacketFn = int(*)(CPCodecContext*, const CPPacket&);
|
||||
using ReceiveFrameFn = int(*)(CPCodecContext*, CPFrame&);
|
||||
|
||||
const char* name = nullptr;
|
||||
const char* long_name = nullptr;
|
||||
CPMediaType type = CPMediaType::Unknow;
|
||||
CPCodecID id = CPCodecID::Unknow;
|
||||
std::size_t priv_data_size = 0;
|
||||
InitFn init = nullptr;
|
||||
CloseFn close = nullptr;
|
||||
SendPacketFn send_packet = nullptr;
|
||||
ReceiveFrameFn receive_frame = nullptr;
|
||||
};
|
||||
|
||||
struct CPCodecContext {
|
||||
const CPCodec* codec = nullptr;
|
||||
void* priv_data = nullptr;
|
||||
CPMediaType codec_type = CPMediaType::Unknow;
|
||||
bool is_open = false;
|
||||
|
||||
void clear() noexcept {
|
||||
codec = nullptr;
|
||||
priv_data = nullptr;
|
||||
codec_type = CPMediaType::Unknow;
|
||||
is_open = false;
|
||||
priv_storage.clear();
|
||||
}
|
||||
|
||||
void* ensure_priv_storage(std::size_t size);
|
||||
void release_priv_storage() noexcept;
|
||||
|
||||
template<typename T>
|
||||
[[nodiscard]] T* priv_as() noexcept {
|
||||
return static_cast<T*>(priv_data);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
[[nodiscard]] const T* priv_as() const noexcept {
|
||||
return static_cast<const T*>(priv_data);
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::uint8_t> priv_storage;
|
||||
friend CPCodecContext* cpcodec_alloc_context(const CPCodec*);
|
||||
friend int cpcodec_open(CPCodecContext*, const CPCodec*);
|
||||
friend int cpcodec_close(CPCodecContext*);
|
||||
};
|
||||
void cpcodec_register(const CPCodec* codec);
|
||||
void cpcodec_register_many(std::initializer_list<const CPCodec*> codecs);
|
||||
const CPCodec* cpcodec_find_decoder(CPCodecID id);
|
||||
const CPCodec* cpcodec_find_decoder_by_name(std::string_view name);
|
||||
std::vector<const CPCodec*> cpcodec_list_codecs();
|
||||
|
||||
CPCodecContext* cpcodec_alloc_context(const CPCodec* codec);
|
||||
int cpcodec_open(CPCodecContext*, const CPCodec*);
|
||||
int cpcodec_close(CPCodecContext*);
|
||||
void cpcodec_free_context(CPCodecContext **ctx);
|
||||
int cpcodec_send_packet(CPCodecContext*, const CPPacket*);
|
||||
int cpcodec_receive_frame(CPCodecContext*, CPFrame*);
|
||||
}
|
||||
@@ -62,6 +62,9 @@ struct CPStreamCore::Impl {
|
||||
if (config_.frame_queue_capacity == 0U) {
|
||||
config_.frame_queue_capacity = 1U;
|
||||
}
|
||||
if (config_.slave_request_interval.count() < 0) {
|
||||
config_.slave_request_interval = std::chrono::milliseconds{0};
|
||||
}
|
||||
frame_queue_capacity_ = config_.frame_queue_capacity;
|
||||
}
|
||||
|
||||
@@ -106,7 +109,9 @@ struct CPStreamCore::Impl {
|
||||
config_.parity,
|
||||
config_.stopbits,
|
||||
config_.flowcontrol);
|
||||
serial->open();
|
||||
if (!serial->isOpen()) {
|
||||
serial->open();
|
||||
}
|
||||
serial->flush();
|
||||
|
||||
{
|
||||
@@ -213,6 +218,9 @@ struct CPStreamCore::Impl {
|
||||
|
||||
reader_thread_ = std::thread(&Impl::reader_loop, this);
|
||||
decoder_thread_ = std::thread(&Impl::decoder_loop, this);
|
||||
if (!config_.slave_request_command.empty()) {
|
||||
slave_thread_ = std::thread(&Impl::slave_loop, this);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -227,6 +235,9 @@ struct CPStreamCore::Impl {
|
||||
if (reader_thread_.joinable()) {
|
||||
reader_thread_.join();
|
||||
}
|
||||
if (slave_thread_.joinable()) {
|
||||
slave_thread_.join();
|
||||
}
|
||||
|
||||
signal_decoder_flush(true);
|
||||
packet_cv_.notify_all();
|
||||
@@ -399,6 +410,33 @@ struct CPStreamCore::Impl {
|
||||
}
|
||||
}
|
||||
|
||||
void slave_loop() {
|
||||
const auto command = config_.slave_request_command;
|
||||
auto interval = config_.slave_request_interval;
|
||||
if (interval.count() < 0) {
|
||||
interval = std::chrono::milliseconds{0};
|
||||
}
|
||||
const bool repeat = interval.count() > 0;
|
||||
|
||||
while (!stop_requested_.load(std::memory_order_acquire)) {
|
||||
const bool success = send(command);
|
||||
if (!success) {
|
||||
std::this_thread::sleep_for(kReaderIdleSleep);
|
||||
continue;
|
||||
}
|
||||
if (!repeat) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto remaining = interval;
|
||||
while (remaining.count() > 0 && !stop_requested_.load(std::memory_order_acquire)) {
|
||||
const auto step = std::min(remaining, kReaderIdleSleep);
|
||||
std::this_thread::sleep_for(step);
|
||||
remaining -= step;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void decoder_loop() {
|
||||
while (true) {
|
||||
Packet packet;
|
||||
@@ -450,6 +488,15 @@ struct CPStreamCore::Impl {
|
||||
decoded.pts = frame.pts;
|
||||
decoded.received_at = std::chrono::steady_clock::now();
|
||||
decoded.frame = std::move(frame);
|
||||
if (codec_descriptor_ && codec_descriptor_->id == CPCodecID::Tactile) {
|
||||
if (auto parsed = tactile::parse_frame(decoded.frame)) {
|
||||
decoded.tactile = parsed;
|
||||
decoded.tactile_pressures = tactile::parse_pressure_values(*parsed);
|
||||
if (auto matrix = tactile::parse_matrix_size_payload(*parsed)) {
|
||||
decoded.tactile_matrix_size = matrix;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FrameCallback callback_copy;
|
||||
{
|
||||
@@ -520,6 +567,7 @@ struct CPStreamCore::Impl {
|
||||
CPCodecContext* codec_ctx_ = nullptr;
|
||||
|
||||
std::thread reader_thread_;
|
||||
std::thread slave_thread_;
|
||||
std::thread decoder_thread_;
|
||||
|
||||
std::mutex packet_mutex_;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "components/ffmsep/cpdecoder.hh"
|
||||
#include "components/ffmsep/tactile/tacdec.hh"
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
@@ -16,6 +17,9 @@ struct DecodedFrame {
|
||||
CPFrame frame;
|
||||
std::chrono::steady_clock::time_point received_at{};
|
||||
std::int64_t pts = 0;
|
||||
std::optional<tactile::TactileFrame> tactile;
|
||||
std::vector<std::uint16_t> tactile_pressures;
|
||||
std::optional<tactile::MatrixSize> tactile_matrix_size;
|
||||
};
|
||||
|
||||
struct CPStreamConfig {
|
||||
@@ -31,6 +35,8 @@ struct CPStreamConfig {
|
||||
std::size_t frame_queue_capacity = 16;
|
||||
CPCodecID codec_id = CPCodecID::Unknow;
|
||||
std::string codec_name;
|
||||
std::vector<std::uint8_t> slave_request_command{};
|
||||
std::chrono::milliseconds slave_request_interval{200};
|
||||
};
|
||||
|
||||
class CPStreamCore {
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
#include "tacdec.hh"
|
||||
#include "components/ffmsep/cpdecoder.hh"
|
||||
#include "tacdec.hh"
|
||||
#include "components/ffmsep/cpdecoder.hh"
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <new>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <new>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace ffmsep::tactile {
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t kMinimumFrameSize = 1
|
||||
+ 1
|
||||
+ 1
|
||||
+ 1
|
||||
+ 0
|
||||
+ 2
|
||||
+ 2;
|
||||
|
||||
constexpr std::uint16_t kCrcInitial = 0xFFFF;
|
||||
constexpr std::uint16_t kCrcPolynomial = 0xA001;
|
||||
constexpr std::size_t kHeaderSize = 4U; // start bytes + length field
|
||||
constexpr std::size_t kFixedSectionSize = 1U + 1U + 1U + 4U + 2U + 1U; // address..status
|
||||
constexpr std::size_t kMinimumFrameSize = kHeaderSize + kFixedSectionSize + 1U; // + CRC byte
|
||||
constexpr std::uint8_t kCrcPolynomial = 0x07U;
|
||||
constexpr std::uint8_t kCrcInitial = 0x00U;
|
||||
constexpr std::uint8_t kCrcXorOut = 0xA9U;
|
||||
constexpr std::array<std::uint8_t, 2> kStartSequence{
|
||||
kStartByteFirst,
|
||||
kStartByteSecond
|
||||
};
|
||||
|
||||
struct TactileDecoderContext {
|
||||
std::vector<std::uint8_t> fifo;
|
||||
@@ -27,77 +28,72 @@ struct TactileDecoderContext {
|
||||
std::int64_t next_pts = 0;
|
||||
};
|
||||
|
||||
std::size_t frame_length_from_payload(std::uint8_t payload_length) {
|
||||
return 1U + 1U + 1U + 1U + payload_length + 2U + 2U;
|
||||
}
|
||||
|
||||
const std::uint8_t* buffer_data(const std::vector<std::uint8_t>& buf) {
|
||||
return buf.empty() ? nullptr : buf.data();
|
||||
}
|
||||
|
||||
std::uint16_t crc16_modbus(const std::uint8_t* data, std::size_t length) {
|
||||
std::uint16_t crc = kCrcInitial;
|
||||
std::uint8_t crc8_with_xorout(const std::uint8_t* data, std::size_t length) {
|
||||
std::uint8_t reg = kCrcInitial;
|
||||
for (std::size_t i = 0; i < length; ++i) {
|
||||
crc ^= static_cast<std::uint16_t>(data[i]);
|
||||
reg ^= data[i];
|
||||
for (int bit = 0; bit < 8; ++bit) {
|
||||
if ((crc & 0x0001U) != 0U) {
|
||||
crc = static_cast<std::uint16_t>((crc >> 1U) ^ kCrcPolynomial);
|
||||
}
|
||||
else {
|
||||
crc = static_cast<std::uint16_t>(crc >> 1U);
|
||||
if ((reg & 0x80U) != 0U) {
|
||||
reg = static_cast<std::uint8_t>((reg << 1U) ^ kCrcPolynomial);
|
||||
} else {
|
||||
reg = static_cast<std::uint8_t>(reg << 1U);
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
return static_cast<std::uint8_t>(reg ^ kCrcXorOut);
|
||||
}
|
||||
|
||||
TactileDecoderContext* get_priv(CPCodecContext* ctx) {
|
||||
return ctx ? ctx->priv_as<TactileDecoderContext>() : nullptr;
|
||||
}
|
||||
|
||||
int tactile_init(CPCodecContext* ctx) {
|
||||
if (!ctx) {
|
||||
return CP_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (!ctx->priv_data) {
|
||||
ctx->ensure_priv_storage(sizeof(TactileDecoderContext));
|
||||
}
|
||||
auto* storage = static_cast<TactileDecoderContext*>(ctx->priv_data);
|
||||
new (storage) TactileDecoderContext();
|
||||
return CP_SUCCESS;
|
||||
}
|
||||
|
||||
void tactile_close(CPCodecContext* ctx) {
|
||||
if (!ctx || !ctx->priv_data) {
|
||||
return;
|
||||
}
|
||||
if (auto* priv = get_priv(ctx); priv != nullptr) {
|
||||
priv->~TactileDecoderContext();
|
||||
}
|
||||
}
|
||||
|
||||
int tactile_send_packet(CPCodecContext* ctx, const CPPacket& packet) {
|
||||
auto priv = get_priv(ctx);
|
||||
if (!priv) {
|
||||
return CP_ERROR_INVALID_STATE;
|
||||
}
|
||||
if (packet.flush) {
|
||||
priv->fifo.clear();
|
||||
priv->end_of_stream = false;
|
||||
priv->next_pts = 0;
|
||||
}
|
||||
|
||||
if (!packet.payload.empty()) {
|
||||
priv->fifo.insert(priv->fifo.end(), packet.payload.begin(), packet.payload.end());
|
||||
}
|
||||
|
||||
if (packet.end_of_stream) {
|
||||
priv->end_of_stream = true;
|
||||
}
|
||||
|
||||
return CP_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
int tactile_init(CPCodecContext* ctx) {
|
||||
if (!ctx) {
|
||||
return CP_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (!ctx->priv_data) {
|
||||
ctx->ensure_priv_storage(sizeof(TactileDecoderContext));
|
||||
}
|
||||
auto* storage = static_cast<TactileDecoderContext*>(ctx->priv_data);
|
||||
new (storage) TactileDecoderContext();
|
||||
return CP_SUCCESS;
|
||||
}
|
||||
|
||||
void tactile_close(CPCodecContext* ctx) {
|
||||
if (!ctx || !ctx->priv_data) {
|
||||
return;
|
||||
}
|
||||
if (auto* priv = get_priv(ctx); priv != nullptr) {
|
||||
priv->~TactileDecoderContext();
|
||||
}
|
||||
}
|
||||
|
||||
int tactile_send_packet(CPCodecContext* ctx, const CPPacket& packet) {
|
||||
auto priv = get_priv(ctx);
|
||||
if (!priv) {
|
||||
return CP_ERROR_INVALID_STATE;
|
||||
}
|
||||
if (packet.flush) {
|
||||
priv->fifo.clear();
|
||||
priv->end_of_stream = false;
|
||||
priv->next_pts = 0;
|
||||
}
|
||||
|
||||
if (!packet.payload.empty()) {
|
||||
priv->fifo.insert(priv->fifo.end(), packet.payload.begin(), packet.payload.end());
|
||||
}
|
||||
|
||||
if (packet.end_of_stream) {
|
||||
priv->end_of_stream = true;
|
||||
}
|
||||
|
||||
return CP_SUCCESS;
|
||||
}
|
||||
|
||||
int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
|
||||
auto* priv = get_priv(ctx);
|
||||
if (!priv) {
|
||||
@@ -115,7 +111,8 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
|
||||
return CP_ERROR_EAGAIN;
|
||||
}
|
||||
|
||||
auto start_it = std::find(buf.begin(), buf.end(), kStartByte);
|
||||
const auto start_it = std::search(buf.begin(), buf.end(),
|
||||
kStartSequence.begin(), kStartSequence.end());
|
||||
if (start_it == buf.end()) {
|
||||
buf.clear();
|
||||
if (priv->end_of_stream) {
|
||||
@@ -129,7 +126,7 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
|
||||
buf.erase(buf.begin(), start_it);
|
||||
}
|
||||
|
||||
if (buf.size() < kMinimumFrameSize) {
|
||||
if (buf.size() < kHeaderSize) {
|
||||
if (priv->end_of_stream) {
|
||||
buf.clear();
|
||||
priv->end_of_stream = false;
|
||||
@@ -139,11 +136,21 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
|
||||
}
|
||||
|
||||
const std::uint8_t* data = buffer_data(buf);
|
||||
const std::uint8_t address = data[1U];
|
||||
const FunctionCode function = static_cast<FunctionCode>(data[2U]);
|
||||
const std::uint8_t payload_length = data[3U];
|
||||
const std::size_t total_frame_length = frame_length_from_payload(payload_length);
|
||||
if (!data) {
|
||||
buf.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::uint16_t data_length =
|
||||
static_cast<std::uint16_t>(data[2]) |
|
||||
static_cast<std::uint16_t>(static_cast<std::uint16_t>(data[3]) << 8U);
|
||||
|
||||
if (data_length < kFixedSectionSize) {
|
||||
buf.erase(buf.begin());
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::size_t total_frame_length = kHeaderSize + static_cast<std::size_t>(data_length) + 1U;
|
||||
if (buf.size() < total_frame_length) {
|
||||
if (priv->end_of_stream) {
|
||||
buf.clear();
|
||||
@@ -153,33 +160,13 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
|
||||
return CP_ERROR_EAGAIN;
|
||||
}
|
||||
|
||||
const std::size_t payload_offset = 4U;
|
||||
const std::size_t crc_offset = payload_offset + payload_length;
|
||||
const std::size_t end_offset = crc_offset + 2U;
|
||||
|
||||
const std::uint8_t crc_lo = data[crc_offset];
|
||||
const std::uint8_t crc_hi = data[crc_offset + 1U];
|
||||
const std::uint16_t crc_value = static_cast<std::uint16_t>(crc_lo) |
|
||||
static_cast<std::uint16_t>(crc_hi << 8U);
|
||||
|
||||
const std::uint8_t end_first = data[end_offset];
|
||||
const std::uint8_t end_second = data[end_offset + 1U];
|
||||
|
||||
if (end_first != kEndByteFirst || end_second != kEndByteSecond) {
|
||||
const std::uint8_t computed_crc = crc8_with_xorout(data + kHeaderSize, data_length);
|
||||
const std::uint8_t frame_crc = data[kHeaderSize + static_cast<std::size_t>(data_length)];
|
||||
if (computed_crc != frame_crc) {
|
||||
buf.erase(buf.begin());
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::size_t crc_region_length = 3U + payload_length;
|
||||
const std::uint16_t computed_crc = crc16_modbus(data + 1U, crc_region_length);
|
||||
if (computed_crc != crc_value) {
|
||||
buf.erase(buf.begin());
|
||||
continue;
|
||||
}
|
||||
|
||||
(void)address;
|
||||
(void)function;
|
||||
|
||||
frame.data.assign(buf.begin(), buf.begin() + static_cast<std::ptrdiff_t>(total_frame_length));
|
||||
frame.pts = priv->next_pts++;
|
||||
frame.key_frame = true;
|
||||
@@ -189,20 +176,20 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
|
||||
return CP_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
const CPCodec kTactileCodec {
|
||||
.name = "tactile_serial",
|
||||
.long_name = "Framed tactile sensor serial protocol decoder",
|
||||
.type = CPMediaType::Data,
|
||||
.id = CPCodecID::Tactile,
|
||||
.priv_data_size = sizeof(TactileDecoderContext),
|
||||
.init = &tactile_init,
|
||||
.close = &tactile_close,
|
||||
.send_packet = &tactile_send_packet,
|
||||
.receive_frame = &tactile_receive_frame
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const CPCodec kTactileCodec {
|
||||
.name = "tactile_serial",
|
||||
.long_name = "Framed tactile sensor serial protocol decoder",
|
||||
.type = CPMediaType::Data,
|
||||
.id = CPCodecID::Tactile,
|
||||
.priv_data_size = sizeof(TactileDecoderContext),
|
||||
.init = &tactile_init,
|
||||
.close = &tactile_close,
|
||||
.send_packet = &tactile_send_packet,
|
||||
.receive_frame = &tactile_receive_frame
|
||||
};
|
||||
}
|
||||
|
||||
std::optional<TactileFrame> parse_frame(const CPFrame& frame) {
|
||||
if (!frame.valid || frame.data.size() < kMinimumFrameSize) {
|
||||
return std::nullopt;
|
||||
@@ -210,64 +197,92 @@ std::optional<TactileFrame> parse_frame(const CPFrame& frame) {
|
||||
const auto* bytes = frame.data.data();
|
||||
const std::size_t size = frame.data.size();
|
||||
|
||||
if (bytes[0] != kStartByte) {
|
||||
if (bytes[0] != kStartByteFirst || bytes[1] != kStartByteSecond) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (bytes[size - 2] != kEndByteFirst || bytes[size - 1] != kEndByteSecond) {
|
||||
const std::uint16_t data_length =
|
||||
static_cast<std::uint16_t>(bytes[2]) |
|
||||
static_cast<std::uint16_t>(static_cast<std::uint16_t>(bytes[3]) << 8U);
|
||||
if (data_length < kFixedSectionSize) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (size < 4U) {
|
||||
const std::size_t expected_size = kHeaderSize + static_cast<std::size_t>(data_length) + 1U;
|
||||
if (size != expected_size) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const std::uint8_t length = bytes[3];
|
||||
if (frame_length_from_payload(length) != size) {
|
||||
const std::uint8_t crc_byte = bytes[size - 1U];
|
||||
const std::uint8_t computed_crc = crc8_with_xorout(bytes + kHeaderSize, data_length);
|
||||
if (computed_crc != crc_byte) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const std::uint8_t device_address = bytes[4];
|
||||
const std::uint8_t reserved = bytes[5];
|
||||
const std::uint8_t response_function = bytes[6];
|
||||
const std::uint32_t start_address =
|
||||
static_cast<std::uint32_t>(bytes[7]) |
|
||||
(static_cast<std::uint32_t>(bytes[8]) << 8U) |
|
||||
(static_cast<std::uint32_t>(bytes[9]) << 16U) |
|
||||
(static_cast<std::uint32_t>(bytes[10]) << 24U);
|
||||
const std::uint16_t return_byte_count =
|
||||
static_cast<std::uint16_t>(bytes[11]) |
|
||||
(static_cast<std::uint16_t>(bytes[12]) << 8U);
|
||||
const std::uint8_t status = bytes[13];
|
||||
|
||||
const std::size_t payload_offset = kHeaderSize + kFixedSectionSize;
|
||||
const std::size_t payload_length = static_cast<std::size_t>(data_length) - kFixedSectionSize;
|
||||
if (payload_length != return_byte_count) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const std::uint8_t address = bytes[1];
|
||||
const FunctionCode function = static_cast<FunctionCode>(bytes[2]);
|
||||
const std::size_t payload_offset = 4U;
|
||||
|
||||
TactileFrame parsed{};
|
||||
parsed.device_address = address;
|
||||
parsed.function = function;
|
||||
parsed.data_length = length;
|
||||
parsed.payload.assign(bytes + payload_offset, bytes + payload_offset + length);
|
||||
parsed.device_address = device_address;
|
||||
parsed.reserved = reserved;
|
||||
parsed.response_function = response_function;
|
||||
parsed.function = static_cast<FunctionCode>(response_function & 0x7FU);
|
||||
parsed.start_address = start_address;
|
||||
parsed.return_byte_count = return_byte_count;
|
||||
parsed.status = status;
|
||||
parsed.payload.assign(bytes + payload_offset, bytes + payload_offset + payload_length);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
|
||||
std::vector<std::uint16_t> parse_pressure_values(const TactileFrame& frame) {
|
||||
if (frame.payload.size() != frame.return_byte_count) {
|
||||
return {};
|
||||
}
|
||||
if (frame.payload.empty() || (frame.payload.size() % 2U != 0U)) {
|
||||
return {};
|
||||
}
|
||||
std::vector<std::uint16_t> values;
|
||||
values.reserve(frame.payload.size() / 2U);
|
||||
for (std::size_t idx = 0; idx + 1U < frame.payload.size(); idx += 2U) {
|
||||
const std::uint16_t value = static_cast<std::uint16_t>(
|
||||
static_cast<std::uint16_t>(frame.payload[idx]) |
|
||||
static_cast<std::uint16_t>(frame.payload[idx + 1U] << 8U));
|
||||
values.push_back(value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
std::optional<MatrixSize> parse_matrix_size_payload(const TactileFrame& frame) {
|
||||
if (frame.payload.size() != 2U) {
|
||||
return std::nullopt;
|
||||
}
|
||||
MatrixSize size{};
|
||||
size.long_edge = frame.payload[0];
|
||||
size.short_edge = frame.payload[1];
|
||||
return size;
|
||||
}
|
||||
|
||||
const CPCodec* tactile_codec() {
|
||||
return &kTactileCodec;
|
||||
}
|
||||
|
||||
void register_tactile_codec() {
|
||||
cpcodec_register(&kTactileCodec);
|
||||
}
|
||||
}
|
||||
std::vector<std::uint16_t> values;
|
||||
values.reserve(frame.payload.size() / 2U);
|
||||
for (std::size_t idx = 0; idx + 1U < frame.payload.size(); idx += 2U) {
|
||||
const std::uint16_t value = static_cast<std::uint16_t>(
|
||||
static_cast<std::uint16_t>(frame.payload[idx]) |
|
||||
static_cast<std::uint16_t>(frame.payload[idx + 1U] << 8U));
|
||||
values.push_back(value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
std::optional<MatrixSize> parse_matrix_size_payload(const TactileFrame& frame) {
|
||||
if (frame.payload.size() != 2U) {
|
||||
return std::nullopt;
|
||||
}
|
||||
MatrixSize size{};
|
||||
size.long_edge = frame.payload[0];
|
||||
size.short_edge = frame.payload[1];
|
||||
return size;
|
||||
}
|
||||
|
||||
const CPCodec* tactile_codec() {
|
||||
return &kTactileCodec;
|
||||
}
|
||||
|
||||
void register_tactile_codec() {
|
||||
cpcodec_register(&kTactileCodec);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include "cpdecoder.hh"
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "cpdecoder.hh"
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace ffmsep::tactile {
|
||||
inline constexpr std::uint8_t kStartByte = 0x3A;
|
||||
inline constexpr std::uint8_t kEndByteFirst = 0x0D;
|
||||
inline constexpr std::uint8_t kEndByteSecond = 0x0A;
|
||||
inline constexpr std::uint8_t kStartByteFirst = 0xAA;
|
||||
inline constexpr std::uint8_t kStartByteSecond = 0x55;
|
||||
|
||||
enum class FunctionCode : std::uint8_t {
|
||||
Unknown = 0x00,
|
||||
ReadMatrix = 0x01,
|
||||
ReadSingle = 0x02,
|
||||
ReadTemperature = 0x03,
|
||||
SetDeviceId = 0x51,
|
||||
SetMatrixSize = 0x52,
|
||||
CalibrationMode = 0x53,
|
||||
};
|
||||
|
||||
struct MatrixSize {
|
||||
std::uint8_t long_edge = 0;
|
||||
std::uint8_t short_edge = 0;
|
||||
};
|
||||
ReadTemperature = 0x03,
|
||||
SetDeviceId = 0x51,
|
||||
SetMatrixSize = 0x52,
|
||||
CalibrationMode = 0x53,
|
||||
};
|
||||
|
||||
struct MatrixSize {
|
||||
std::uint8_t long_edge = 0;
|
||||
std::uint8_t short_edge = 0;
|
||||
};
|
||||
|
||||
struct TactileFrame {
|
||||
std::uint8_t device_address = 0;
|
||||
std::uint8_t reserved = 0;
|
||||
std::uint8_t response_function = 0;
|
||||
FunctionCode function = FunctionCode::Unknown;
|
||||
std::uint8_t data_length = 0;
|
||||
std::uint32_t start_address = 0;
|
||||
std::uint16_t return_byte_count = 0;
|
||||
std::uint8_t status = 0;
|
||||
std::vector<std::uint8_t> payload;
|
||||
};
|
||||
|
||||
std::optional<TactileFrame> parse_frame(const CPFrame& frame);
|
||||
std::vector<std::uint16_t> parse_pressure_values(const TactileFrame& frame);
|
||||
std::optional<MatrixSize> parse_matrix_size_payload(const TactileFrame& frame);
|
||||
std::optional<MatrixSize> parse_patrix_coordinate_payload(const TactileFrame& frame);
|
||||
|
||||
const CPCodec* tactile_codec();
|
||||
void register_tactile_codec();
|
||||
}
|
||||
std::optional<MatrixSize> parse_matrix_size_payload(const TactileFrame& frame);
|
||||
std::optional<MatrixSize> parse_patrix_coordinate_payload(const TactileFrame& frame);
|
||||
|
||||
const CPCodec* tactile_codec();
|
||||
void register_tactile_codec();
|
||||
}
|
||||
|
||||
@@ -1,103 +1,103 @@
|
||||
#include "component.hh"
|
||||
|
||||
#include "modern-qt/core/application.hh"
|
||||
#include "modern-qt/layout/group.hh"
|
||||
#include "modern-qt/layout/linear.hh"
|
||||
#include "modern-qt/layout/mutual-exclusion-group.hh"
|
||||
#include "modern-qt/utility/material-icon.hh"
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/widget/buttons/icon-button.hh"
|
||||
#include "modern-qt/widget/cards/filled-card.hh"
|
||||
#include "modern-qt/widget/image.hh"
|
||||
|
||||
using namespace creeper;
|
||||
namespace fc = filled_card::pro;
|
||||
namespace sg = select_group::pro;
|
||||
namespace ln = linear::pro;
|
||||
namespace im = image::pro;
|
||||
namespace ic = icon_button::pro;
|
||||
|
||||
auto NavComponent(NavComponentState& state) noexcept -> raw_pointer<QWidget> {
|
||||
|
||||
const auto AvatarComponent = new Image {
|
||||
im::FixedSize {60, 60},
|
||||
im::Radius {-1},
|
||||
im::ContentScale {ContentScale::CROP},
|
||||
im::BorderWidth {3},
|
||||
im::PainterResource {
|
||||
":/images/images/logo.png",
|
||||
// "./images/logo.png",
|
||||
},
|
||||
};
|
||||
state.manager.append_handler(AvatarComponent, [AvatarComponent](const ThemeManager& manager) {
|
||||
const auto colorscheme = manager.color_scheme();
|
||||
const auto colorborder = colorscheme.secondary_container;
|
||||
AvatarComponent->set_border_color(colorborder);
|
||||
});
|
||||
|
||||
const auto navigation_icons_config = std::tuple {
|
||||
ic::ThemeManager {state.manager},
|
||||
ic::ColorStandard,
|
||||
ic::ShapeRound,
|
||||
ic::TypesToggleUnselected,
|
||||
ic::WidthDefault,
|
||||
ic::Font {material::regular::font_1},
|
||||
ic::FixedSize {IconButton::kSmallContainerSize},
|
||||
};
|
||||
|
||||
return new FilledCard {
|
||||
fc::ThemeManager {state.manager},
|
||||
fc::Radius {0},
|
||||
fc::Level {CardLevel::HIGHEST},
|
||||
fc::Layout<Col> {
|
||||
ln::Spacing {10},
|
||||
ln::Margin {15},
|
||||
ln::Item {
|
||||
{0, Qt::AlignHCenter},
|
||||
AvatarComponent,
|
||||
},
|
||||
ln::SpacingItem {20},
|
||||
ln::Item<SelectGroup<Col, IconButton>> {
|
||||
{0, Qt::AlignHCenter},
|
||||
ln::Margin {0},
|
||||
ln::SpacingItem {10},
|
||||
sg::Compose {
|
||||
state.buttons_context | std::views::enumerate,
|
||||
[&](int index, const auto& context) {
|
||||
const auto& [name, icon] = context;
|
||||
|
||||
const auto status = (index == 0)
|
||||
? ic::TypesToggleSelected
|
||||
: ic::TypesToggleUnselected;
|
||||
|
||||
return new IconButton {
|
||||
navigation_icons_config,
|
||||
status,
|
||||
ic::FontIcon(icon.data()),
|
||||
ic::Clickable {[=]{state.switch_callback(index, name);}},
|
||||
};
|
||||
},
|
||||
Qt::AlignHCenter,
|
||||
},
|
||||
sg::SignalInjection{&IconButton::clicked},
|
||||
},
|
||||
ln::SpacingItem {40},
|
||||
ln::Stretch {255},
|
||||
ln::Item<IconButton> {
|
||||
{0, Qt::AlignHCenter},
|
||||
navigation_icons_config,
|
||||
ic::TypesDefault,
|
||||
ic::FontIcon {material::icon::kLogout},
|
||||
ic::Clickable {&app::quit},
|
||||
},
|
||||
ln::Item<IconButton> {
|
||||
{0, Qt::AlignHCenter},
|
||||
navigation_icons_config,
|
||||
ic::ColorFilled,
|
||||
ic::FontIcon {material::icon::kDarkMode},
|
||||
ic::Clickable{[&]{state.manager.toggle_color_mode();state.manager.apply_theme();}},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#include "component.hh"
|
||||
|
||||
#include "modern-qt/core/application.hh"
|
||||
#include "modern-qt/layout/group.hh"
|
||||
#include "modern-qt/layout/linear.hh"
|
||||
#include "modern-qt/layout/mutual-exclusion-group.hh"
|
||||
#include "modern-qt/utility/material-icon.hh"
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/widget/buttons/icon-button.hh"
|
||||
#include "modern-qt/widget/cards/filled-card.hh"
|
||||
#include "modern-qt/widget/image.hh"
|
||||
|
||||
using namespace creeper;
|
||||
namespace fc = filled_card::pro;
|
||||
namespace sg = select_group::pro;
|
||||
namespace ln = linear::pro;
|
||||
namespace im = image::pro;
|
||||
namespace ic = icon_button::pro;
|
||||
|
||||
auto NavComponent(NavComponentState& state) noexcept -> raw_pointer<QWidget> {
|
||||
|
||||
const auto AvatarComponent = new Image {
|
||||
im::FixedSize {60, 60},
|
||||
im::Radius {-1},
|
||||
im::ContentScale {ContentScale::CROP},
|
||||
im::BorderWidth {3},
|
||||
im::PainterResource {
|
||||
":/images/images/logo.png",
|
||||
// "./images/logo.png",
|
||||
},
|
||||
};
|
||||
state.manager.append_handler(AvatarComponent, [AvatarComponent](const ThemeManager& manager) {
|
||||
const auto colorscheme = manager.color_scheme();
|
||||
const auto colorborder = colorscheme.secondary_container;
|
||||
AvatarComponent->set_border_color(colorborder);
|
||||
});
|
||||
|
||||
const auto navigation_icons_config = std::tuple {
|
||||
ic::ThemeManager {state.manager},
|
||||
ic::ColorStandard,
|
||||
ic::ShapeRound,
|
||||
ic::TypesToggleUnselected,
|
||||
ic::WidthDefault,
|
||||
ic::Font {material::regular::font_1},
|
||||
ic::FixedSize {IconButton::kSmallContainerSize},
|
||||
};
|
||||
|
||||
return new FilledCard {
|
||||
fc::ThemeManager {state.manager},
|
||||
fc::Radius {0},
|
||||
fc::Level {CardLevel::HIGHEST},
|
||||
fc::Layout<Col> {
|
||||
ln::Spacing {10},
|
||||
ln::Margin {15},
|
||||
ln::Item {
|
||||
{0, Qt::AlignHCenter},
|
||||
AvatarComponent,
|
||||
},
|
||||
ln::SpacingItem {20},
|
||||
ln::Item<SelectGroup<Col, IconButton>> {
|
||||
{0, Qt::AlignHCenter},
|
||||
ln::Margin {0},
|
||||
ln::SpacingItem {10},
|
||||
sg::Compose {
|
||||
state.buttons_context | std::views::enumerate,
|
||||
[&](int index, const auto& context) {
|
||||
const auto& [name, icon] = context;
|
||||
|
||||
const auto status = (index == 0)
|
||||
? ic::TypesToggleSelected
|
||||
: ic::TypesToggleUnselected;
|
||||
|
||||
return new IconButton {
|
||||
navigation_icons_config,
|
||||
status,
|
||||
ic::FontIcon(icon.data()),
|
||||
ic::Clickable {[=]{state.switch_callback(index, name);}},
|
||||
};
|
||||
},
|
||||
Qt::AlignHCenter,
|
||||
},
|
||||
sg::SignalInjection{&IconButton::clicked},
|
||||
},
|
||||
ln::SpacingItem {40},
|
||||
ln::Stretch {255},
|
||||
ln::Item<IconButton> {
|
||||
{0, Qt::AlignHCenter},
|
||||
navigation_icons_config,
|
||||
ic::TypesDefault,
|
||||
ic::FontIcon {material::icon::kLogout},
|
||||
ic::Clickable {&app::quit},
|
||||
},
|
||||
ln::Item<IconButton> {
|
||||
{0, Qt::AlignHCenter},
|
||||
navigation_icons_config,
|
||||
ic::ColorFilled,
|
||||
ic::FontIcon {material::icon::kDarkMode},
|
||||
ic::Clickable{[&]{state.manager.toggle_color_mode();state.manager.apply_theme();}},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,213 +1,589 @@
|
||||
//
|
||||
// Created by Lenn on 2025/10/14.
|
||||
//
|
||||
|
||||
//
|
||||
// Created by Lenn on 2025/10/14.
|
||||
//
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <QString>
|
||||
#include <QObject>
|
||||
#include <QMetaObject>
|
||||
#include <QStringList>
|
||||
#include <QtCore/Qt>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <qsize.h>
|
||||
#include <qsizepolicy.h>
|
||||
#include <random>
|
||||
#include <chrono>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include "component.hh"
|
||||
#include "cpstream_core.hh"
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/utility/wrapper/layout.hh"
|
||||
#include "modern-qt/utility/wrapper/widget.hh"
|
||||
#include "components/charts/heatmap.hh"
|
||||
#include <modern-qt/layout/flow.hh>
|
||||
#include <modern-qt/layout/linear.hh>
|
||||
#include <modern-qt/utility/material-icon.hh>
|
||||
#include <modern-qt/utility/wrapper/mutable-value.hh>
|
||||
#include <modern-qt/widget/buttons/icon-button.hh>
|
||||
#include <modern-qt/widget/cards/filled-card.hh>
|
||||
#include <modern-qt/widget/cards/outlined-card.hh>
|
||||
#include <modern-qt/widget/image.hh>
|
||||
#include <modern-qt/widget/shape/wave-circle.hh>
|
||||
#include <modern-qt/widget/sliders.hh>
|
||||
#include <modern-qt/widget/switch.hh>
|
||||
#include <modern-qt/widget/text-fields.hh>
|
||||
#include <modern-qt/widget/text.hh>
|
||||
#include <modern-qt/widget/select.hh>
|
||||
#include "components/ffmsep/tactile/tacdec.hh"
|
||||
|
||||
|
||||
using namespace creeper;
|
||||
namespace capro = card::pro;
|
||||
namespace lnpro = linear::pro;
|
||||
namespace impro = image::pro;
|
||||
namespace ibpro = icon_button::pro;
|
||||
namespace slpro = select_widget::pro;
|
||||
namespace pwpro = plot_widget::pro;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::array<std::uint8_t, 14> kSlaveRequestCommand{
|
||||
0x55, 0xAA, 0x09, 0x00, 0x34, 0x00, 0xFB,
|
||||
0x00, 0x1C, 0x00, 0x00, 0x18, 0x00, 0x7A
|
||||
};
|
||||
|
||||
QVector<PointData> make_flat_points(const QSize& size, double value = 0.0) {
|
||||
const int width = std::max(size.width(), 1);
|
||||
const int height = std::max(size.height(), 1);
|
||||
QVector<PointData> points;
|
||||
points.reserve(static_cast<int>(width * height));
|
||||
for (int y = 0; y < height; ++y) {
|
||||
for (int x = 0; x < width; ++x) {
|
||||
points.append(PointData{
|
||||
static_cast<double>(x),
|
||||
static_cast<double>(y),
|
||||
value
|
||||
});
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
std::once_flag& codec_registration_flag() {
|
||||
static std::once_flag flag;
|
||||
return flag;
|
||||
}
|
||||
|
||||
class SensorStreamController : public QObject {
|
||||
public:
|
||||
SensorStreamController(std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data,
|
||||
std::shared_ptr<MutableValue<QSize>> matrix_context,
|
||||
QObject* parent = nullptr)
|
||||
: QObject(parent)
|
||||
, heatmap_data_(std::move(heatmap_data))
|
||||
, matrix_context_(std::move(matrix_context)) {
|
||||
std::call_once(codec_registration_flag(), [] {
|
||||
ffmsep::tactile::register_tactile_codec();
|
||||
});
|
||||
}
|
||||
|
||||
~SensorStreamController() override {
|
||||
reset_core();
|
||||
}
|
||||
|
||||
bool start(const QString& requested_port, std::uint32_t baudrate) {
|
||||
if (is_connected()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto ports = ffmsep::CPStreamCore::list_available_ports();
|
||||
std::string port_utf8;
|
||||
if (!requested_port.isEmpty()) {
|
||||
port_utf8 = requested_port.toStdString();
|
||||
const auto it = std::find_if(
|
||||
ports.begin(), ports.end(),
|
||||
[&](const serial::PortInfo& info) { return info.port == port_utf8; });
|
||||
if (it == ports.end()) {
|
||||
if (ports.empty()) {
|
||||
std::cerr << "SensorStreamController: requested port '" << port_utf8 << "' not available and no other ports detected.\n";
|
||||
last_error_ = QString::fromUtf8("未检测到串口");
|
||||
return false;
|
||||
}
|
||||
std::cerr << "SensorStreamController: requested port '" << port_utf8 << "' not available, falling back to first detected port.\n";
|
||||
port_utf8 = ports.front().port;
|
||||
}
|
||||
} else if (!ports.empty()) {
|
||||
port_utf8 = ports.front().port;
|
||||
} else {
|
||||
std::cerr << "SensorStreamController: no serial ports available\n";
|
||||
last_error_ = QString::fromUtf8("未检测到串口");
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::uint32_t baud = baudrate == 0U ? 115200U : baudrate;
|
||||
|
||||
ffmsep::CPStreamConfig cfg;
|
||||
cfg.port = port_utf8;
|
||||
cfg.baudrate = baud;
|
||||
cfg.codec_id = ffmsep::CPCodecID::Tactile;
|
||||
cfg.read_chunk_size = 256;
|
||||
cfg.packet_queue_capacity = 128;
|
||||
cfg.frame_queue_capacity = 32;
|
||||
cfg.slave_request_command.assign(kSlaveRequestCommand.begin(), kSlaveRequestCommand.end());
|
||||
cfg.slave_request_interval = std::chrono::milliseconds{200};
|
||||
|
||||
reset_core();
|
||||
core_ = std::make_unique<ffmsep::CPStreamCore>();
|
||||
|
||||
if (!core_->open(cfg)) {
|
||||
last_error_ = QString::fromStdString(core_->last_error());
|
||||
std::cerr << "SensorStreamController: open failed - " << core_->last_error() << "\n";
|
||||
reset_core();
|
||||
return false;
|
||||
}
|
||||
|
||||
core_->set_frame_callback([this](const ffmsep::DecodedFrame& frame) {
|
||||
handle_frame(frame);
|
||||
});
|
||||
|
||||
if (!core_->start()) {
|
||||
last_error_ = QString::fromStdString(core_->last_error());
|
||||
std::cerr << "SensorStreamController: start failed - " << core_->last_error() << "\n";
|
||||
reset_core();
|
||||
return false;
|
||||
}
|
||||
|
||||
active_port_ = QString::fromStdString(cfg.port);
|
||||
last_error_.clear();
|
||||
connected_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
#include "component.hh"
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/utility/wrapper/layout.hh"
|
||||
#include "modern-qt/utility/wrapper/widget.hh"
|
||||
#include "components/charts/heatmap.hh"
|
||||
#include <modern-qt/layout/flow.hh>
|
||||
#include <modern-qt/layout/linear.hh>
|
||||
#include <modern-qt/utility/material-icon.hh>
|
||||
#include <modern-qt/utility/wrapper/mutable-value.hh>
|
||||
#include <modern-qt/widget/buttons/icon-button.hh>
|
||||
#include <modern-qt/widget/cards/filled-card.hh>
|
||||
#include <modern-qt/widget/cards/outlined-card.hh>
|
||||
#include <modern-qt/widget/image.hh>
|
||||
#include <modern-qt/widget/shape/wave-circle.hh>
|
||||
#include <modern-qt/widget/sliders.hh>
|
||||
#include <modern-qt/widget/switch.hh>
|
||||
#include <modern-qt/widget/text-fields.hh>
|
||||
#include <modern-qt/widget/text.hh>
|
||||
#include <modern-qt/widget/select.hh>
|
||||
void stop() {
|
||||
reset_core();
|
||||
active_port_.clear();
|
||||
if (heatmap_data_ && matrix_context_) {
|
||||
heatmap_data_->set(make_flat_points(matrix_context_->get()));
|
||||
}
|
||||
connected_ = false;
|
||||
}
|
||||
|
||||
using namespace creeper;
|
||||
namespace capro = card::pro;
|
||||
namespace lnpro = linear::pro;
|
||||
namespace impro = image::pro;
|
||||
namespace ibpro = icon_button::pro;
|
||||
namespace slpro = select_widget::pro;
|
||||
namespace pwpro = plot_widget::pro;
|
||||
[[nodiscard]] bool is_running() const noexcept {
|
||||
return core_ && core_->is_running();
|
||||
}
|
||||
|
||||
static auto ComConfigComponent(ThemeManager& manager, auto&& callback) {
|
||||
auto slogen_context = std::make_shared<MutableValue<QString>>();
|
||||
slogen_context->set_silent("BanG Bream! It's MyGo!!!");
|
||||
|
||||
auto select_com_context = std::make_shared<MutableValue<QStringList>>();
|
||||
select_com_context->set_silent(QStringList {"COM1", "COM2", "COM3", "COM4", "COM5"});
|
||||
[[nodiscard]] bool is_connected() const noexcept {
|
||||
return connected_;
|
||||
}
|
||||
|
||||
auto select_baud_context = std::make_shared<MutableValue<QStringList>>();
|
||||
select_baud_context->set_silent(QStringList {"9600", "115200"});
|
||||
[[nodiscard]] QString active_port() const {
|
||||
return active_port_;
|
||||
}
|
||||
|
||||
[[nodiscard]] QString last_error() const {
|
||||
return last_error_;
|
||||
}
|
||||
|
||||
private:
|
||||
void reset_core() {
|
||||
connected_ = false;
|
||||
if (!core_) {
|
||||
return;
|
||||
}
|
||||
core_->set_frame_callback({});
|
||||
if (core_->is_running()) {
|
||||
core_->stop();
|
||||
}
|
||||
if (core_->is_open()) {
|
||||
core_->close();
|
||||
}
|
||||
core_.reset();
|
||||
}
|
||||
|
||||
static QSize to_qsize(const ffmsep::tactile::MatrixSize& m) {
|
||||
return QSize{
|
||||
static_cast<int>(m.long_edge),
|
||||
static_cast<int>(m.short_edge)
|
||||
};
|
||||
}
|
||||
|
||||
void handle_frame(const ffmsep::DecodedFrame& frame) {
|
||||
if (!frame.tactile || frame.tactile_pressures.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto pressures = frame.tactile_pressures;
|
||||
auto size_hint = frame.tactile_matrix_size;
|
||||
auto frame_bytes = frame.frame.data;
|
||||
std::vector<std::uint8_t> raw_payload;
|
||||
if (frame.tactile) {
|
||||
raw_payload = frame.tactile->payload;
|
||||
}
|
||||
|
||||
QMetaObject::invokeMethod(
|
||||
this,
|
||||
[this,
|
||||
pressures = std::move(pressures),
|
||||
size_hint,
|
||||
frame_bytes = std::move(frame_bytes),
|
||||
raw_payload = std::move(raw_payload)]() {
|
||||
const auto format_raw = [](const std::vector<std::uint8_t>& data) -> std::string {
|
||||
if (data.empty()) {
|
||||
return "[]";
|
||||
}
|
||||
std::ostringstream oss;
|
||||
oss << '[';
|
||||
oss << std::uppercase << std::setfill('0');
|
||||
for (std::size_t idx = 0; idx < data.size(); ++idx) {
|
||||
if (idx != 0U) {
|
||||
oss << ' ';
|
||||
}
|
||||
oss << std::setw(2) << std::hex << static_cast<unsigned int>(data[idx]);
|
||||
}
|
||||
oss << ']';
|
||||
return oss.str();
|
||||
};
|
||||
|
||||
std::cout << "[Sensor] frame=" << format_raw(frame_bytes);
|
||||
std::cout << " payload=" << format_raw(raw_payload);
|
||||
std::cout << " received " << pressures.size() << " pressure values";
|
||||
if (size_hint) {
|
||||
std::cout << " matrix=" << int(size_hint->long_edge)
|
||||
<< "x" << int(size_hint->short_edge);
|
||||
}
|
||||
const std::size_t preview = std::min<std::size_t>(pressures.size(), 12);
|
||||
if (preview > 0) {
|
||||
std::cout << " values=[";
|
||||
for (std::size_t idx = 0; idx < preview; ++idx) {
|
||||
if (idx != 0U) {
|
||||
std::cout << ", ";
|
||||
}
|
||||
std::cout << pressures[idx];
|
||||
}
|
||||
if (preview < pressures.size()) {
|
||||
std::cout << ", ...";
|
||||
}
|
||||
std::cout << "]";
|
||||
}
|
||||
std::cout << std::endl;
|
||||
|
||||
auto matrix = matrix_context_->get();
|
||||
if (size_hint) {
|
||||
matrix = to_qsize(*size_hint);
|
||||
}
|
||||
matrix = normalize_matrix(matrix, pressures.size());
|
||||
if (matrix.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QVector<PointData> points;
|
||||
points.reserve(matrix.width() * matrix.height());
|
||||
for (int y = 0; y < matrix.height(); ++y) {
|
||||
for (int x = 0; x < matrix.width(); ++x) {
|
||||
const int idx = y * matrix.width() + x;
|
||||
if (idx >= static_cast<int>(pressures.size())) {
|
||||
break;
|
||||
}
|
||||
const auto value = static_cast<double>(pressures[static_cast<std::size_t>(idx)]);
|
||||
points.append(PointData{
|
||||
static_cast<double>(x),
|
||||
static_cast<double>(y),
|
||||
value
|
||||
});
|
||||
}
|
||||
}
|
||||
matrix_context_->set(matrix);
|
||||
heatmap_data_->set(std::move(points));
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
[[nodiscard]] QSize normalize_matrix(QSize candidate, std::size_t value_count) const {
|
||||
if (value_count == 0U) {
|
||||
return QSize{};
|
||||
}
|
||||
|
||||
const auto row = new Row {
|
||||
// lnpro::Item<FilledTextField> {
|
||||
// text_field::pro::ThemeManager {manager},
|
||||
// text_field::pro::LeadingIcon {material::icon::kSearch, material::regular::font},
|
||||
// MutableForward {
|
||||
// text_field::pro::LabelText {},
|
||||
// slogen_context,
|
||||
// },
|
||||
// },
|
||||
const auto adapt_from = [value_count](const QSize& hint) -> std::optional<QSize> {
|
||||
if (hint.width() <= 0 && hint.height() <= 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (hint.width() > 0 && hint.height() > 0) {
|
||||
const auto cells = static_cast<std::size_t>(hint.width()) *
|
||||
static_cast<std::size_t>(hint.height());
|
||||
if (cells == value_count) {
|
||||
return hint;
|
||||
}
|
||||
}
|
||||
|
||||
if (hint.width() > 0) {
|
||||
const auto width = static_cast<std::size_t>(hint.width());
|
||||
if (width != 0U && (value_count % width) == 0U) {
|
||||
const auto height = static_cast<int>(value_count / width);
|
||||
return QSize{hint.width(), height};
|
||||
}
|
||||
}
|
||||
|
||||
if (hint.height() > 0) {
|
||||
const auto height = static_cast<std::size_t>(hint.height());
|
||||
if (height != 0U && (value_count % height) == 0U) {
|
||||
const auto width = static_cast<int>(value_count / height);
|
||||
return QSize{width, hint.height()};
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
if (auto adjusted = adapt_from(candidate)) {
|
||||
return *adjusted;
|
||||
}
|
||||
|
||||
if (auto adjusted = adapt_from(matrix_context_->get())) {
|
||||
return *adjusted;
|
||||
}
|
||||
|
||||
const auto root = static_cast<int>(std::sqrt(static_cast<double>(value_count)));
|
||||
for (int width = root; width >= 1; --width) {
|
||||
const auto divisor = static_cast<std::size_t>(width);
|
||||
if (divisor == 0U) {
|
||||
continue;
|
||||
}
|
||||
if ((value_count % divisor) == 0U) {
|
||||
const auto height = static_cast<int>(value_count / divisor);
|
||||
return QSize{width, height};
|
||||
}
|
||||
}
|
||||
|
||||
return QSize{static_cast<int>(value_count), 1};
|
||||
}
|
||||
|
||||
std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data_;
|
||||
std::shared_ptr<MutableValue<QSize>> matrix_context_;
|
||||
std::unique_ptr<ffmsep::CPStreamCore> core_;
|
||||
QString active_port_;
|
||||
QString last_error_;
|
||||
bool connected_ = false;
|
||||
};
|
||||
|
||||
struct SensorUiState {
|
||||
std::shared_ptr<MutableValue<QString>> link_icon =
|
||||
std::make_shared<MutableValue<QString>>(QString::fromLatin1(material::icon::kAddLink));
|
||||
std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data =
|
||||
std::make_shared<MutableValue<QVector<PointData>>>();
|
||||
std::shared_ptr<MutableValue<QSize>> heatmap_matrix =
|
||||
std::make_shared<MutableValue<QSize>>();
|
||||
std::shared_ptr<MutableValue<QStringList>> port_items =
|
||||
std::make_shared<MutableValue<QStringList>>();
|
||||
QString selected_port;
|
||||
std::uint32_t selected_baud = 115200;
|
||||
std::unique_ptr<SensorStreamController> controller;
|
||||
|
||||
SensorUiState() {
|
||||
const QSize size{3, 4};
|
||||
heatmap_matrix->set_silent(size);
|
||||
heatmap_data->set_silent(make_flat_points(size));
|
||||
|
||||
// 初始化串口列表
|
||||
QStringList ports_list;
|
||||
const auto ports = ffmsep::CPStreamCore::list_available_ports();
|
||||
ports_list.reserve(static_cast<qsizetype>(ports.size()));
|
||||
for (const auto& info : ports) {
|
||||
ports_list.emplace_back(QString::fromStdString(info.port));
|
||||
}
|
||||
port_items->set_silent(ports_list);
|
||||
if (selected_port.isEmpty() && !ports_list.isEmpty()) {
|
||||
selected_port = ports_list.front();
|
||||
}
|
||||
|
||||
controller = std::make_unique<SensorStreamController>(heatmap_data, heatmap_matrix);
|
||||
}
|
||||
};
|
||||
|
||||
SensorUiState& sensor_state() {
|
||||
static SensorUiState state;
|
||||
return state;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static auto ComConfigComponent(ThemeManager& manager) {
|
||||
auto& sensor = sensor_state();
|
||||
auto link_icon_context = sensor.link_icon;
|
||||
|
||||
// 串口下拉:改为绑定可变数据源,初始值由 SensorUiState 构造时填充
|
||||
if (sensor.selected_port.isEmpty() && !sensor.port_items->get().isEmpty()) {
|
||||
sensor.selected_port = sensor.port_items->get().front();
|
||||
}
|
||||
|
||||
const QStringList baud_items{
|
||||
QString::fromLatin1("9600"),
|
||||
QString::fromLatin1("115200")
|
||||
};
|
||||
if (sensor.selected_baud == 0U) {
|
||||
sensor.selected_baud = 115200U;
|
||||
}
|
||||
|
||||
const auto row = new Row {
|
||||
// lnpro::Item<FilledTextField> {
|
||||
// text_field::pro::ThemeManager {manager},
|
||||
// text_field::pro::LeadingIcon {material::icon::kSearch, material::regular::font},
|
||||
// MutableForward {
|
||||
// text_field::pro::LabelText {},
|
||||
// slogen_context,
|
||||
// },
|
||||
// },
|
||||
lnpro::Item<MatSelect> {
|
||||
slpro::ThemeManager {manager},
|
||||
slpro::LeadingIcon {material::icon::kArrowDropDown, material::regular::font},
|
||||
slpro::IndexChanged {[&](auto& self){ qDebug() << self.currentIndex();}},
|
||||
slpro::IndexChanged {[sensor_ptr = &sensor](auto& self){
|
||||
const auto text = self.currentText();
|
||||
if (!text.isEmpty()) {
|
||||
sensor_ptr->selected_port = text;
|
||||
}
|
||||
}},
|
||||
slpro::LeadingText {"COM"},
|
||||
MutableForward {
|
||||
slpro::SelectItems {},
|
||||
select_com_context,
|
||||
}
|
||||
},
|
||||
lnpro::Item<MatSelect> {
|
||||
slpro::ThemeManager {manager },
|
||||
slpro::LeadingIcon { material::icon::kArrowDropDown, material::regular::font},
|
||||
slpro::IndexChanged {[&](auto& self){ qDebug() << self.currentIndex();}},
|
||||
slpro::LeadingText {"Baud"},
|
||||
MutableForward {
|
||||
slpro::SelectItems {},
|
||||
select_baud_context,
|
||||
}
|
||||
},
|
||||
lnpro::SpacingItem {20},
|
||||
lnpro::Item<IconButton> {
|
||||
ibpro::ThemeManager {manager},
|
||||
ibpro::FixedSize {40, 40},
|
||||
ibpro::Color { IconButton::Color::TONAL },
|
||||
ibpro::Font { material::kRoundSmallFont },
|
||||
ibpro::FontIcon { material::icon::kAddLink },
|
||||
ibpro::Clickable {[slogen_context] {
|
||||
constexpr auto random_slogen = [] {
|
||||
constexpr auto slogens = std::array {
|
||||
"为什么要演奏《春日影》!",
|
||||
"我从来不觉得玩乐队开心过。",
|
||||
"我好想…成为人啊!",
|
||||
"那你愿意……跟我组一辈子的乐队吗?",
|
||||
"过去软弱的我…已经死了。",
|
||||
};
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution<> dist(0, slogens.size() - 1);
|
||||
return QString::fromUtf8(slogens[dist(gen)]);
|
||||
};
|
||||
*slogen_context = random_slogen();
|
||||
}},
|
||||
sensor.port_items,
|
||||
},
|
||||
},
|
||||
lnpro::Item<MatSelect> {
|
||||
slpro::ThemeManager {manager },
|
||||
slpro::LeadingIcon { material::icon::kArrowDropDown, material::regular::font},
|
||||
slpro::IndexChanged {[sensor_ptr = &sensor](auto& self){
|
||||
bool ok = false;
|
||||
const auto text = self.currentText();
|
||||
const auto value = text.toUInt(&ok);
|
||||
if (ok && value > 0U) {
|
||||
sensor_ptr->selected_baud = static_cast<std::uint32_t>(value);
|
||||
}
|
||||
}},
|
||||
slpro::LeadingText {"Baud"},
|
||||
slpro::SelectItems {baud_items},
|
||||
},
|
||||
lnpro::SpacingItem {20},
|
||||
lnpro::Item<IconButton> {
|
||||
ibpro::ThemeManager {manager},
|
||||
ibpro::FixedSize {40, 40},
|
||||
ibpro::Color { IconButton::Color::TONAL },
|
||||
ibpro::Font { material::kRoundSmallFont },
|
||||
MutableForward {
|
||||
icon_button::pro::FontIcon {},
|
||||
link_icon_context,
|
||||
},
|
||||
ibpro::Clickable { [sensor_ptr = &sensor, link_icon_context]{
|
||||
auto& sensor = *sensor_ptr;
|
||||
if (!sensor.controller) {
|
||||
return;
|
||||
}
|
||||
if (sensor.controller->is_connected()) {
|
||||
sensor.controller->stop();
|
||||
link_icon_context->set(QString::fromLatin1(material::icon::kAddLink));
|
||||
} else {
|
||||
const auto port = sensor.selected_port;
|
||||
const auto baud = sensor.selected_baud == 0U ? 115200U : sensor.selected_baud;
|
||||
if (sensor.controller->start(port, baud)) {
|
||||
sensor.selected_port = sensor.controller->active_port();
|
||||
link_icon_context->set(QString::fromLatin1(material::icon::kLinkOff));
|
||||
} else {
|
||||
std::cerr << "Failed to start sensor stream: "
|
||||
<< sensor.controller->last_error().toStdString()
|
||||
<< "\n";
|
||||
}
|
||||
}
|
||||
} }
|
||||
},
|
||||
lnpro::Item<IconButton> {
|
||||
ibpro::ThemeManager { manager },
|
||||
ibpro::FixedSize { 40, 40 },
|
||||
ibpro::Color { IconButton::Color::TONAL },
|
||||
ibpro::Font { material::kRoundSmallFont },
|
||||
ibpro::FontIcon { material::icon::kRefresh },
|
||||
ibpro::Clickable {[select_baud_context] {
|
||||
|
||||
static constexpr auto options_group1 = std::array {
|
||||
"第一组选项1", "第一组选项2", "第一组选项3"
|
||||
};
|
||||
static constexpr auto options_group2 = std::array {
|
||||
"第二组选项A", "第二组选项B", "第二组选项C", "第二组选项D"
|
||||
};
|
||||
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution<> dist(0, 1);
|
||||
|
||||
QStringList new_options;
|
||||
if (dist(gen) == 0) {
|
||||
for (const auto& option : options_group1) {
|
||||
new_options << QString::fromUtf8(option);
|
||||
}
|
||||
} else {
|
||||
for (const auto& option : options_group2) {
|
||||
new_options << QString::fromUtf8(option);
|
||||
}
|
||||
ibpro::Clickable {[&sensor] {
|
||||
// 刷新串口列表
|
||||
QStringList ports_list;
|
||||
const auto ports = ffmsep::CPStreamCore::list_available_ports();
|
||||
ports_list.reserve(static_cast<qsizetype>(ports.size()));
|
||||
for (const auto& info : ports) {
|
||||
ports_list.emplace_back(QString::fromStdString(info.port));
|
||||
}
|
||||
*select_baud_context = new_options;
|
||||
|
||||
// 保持原选择(若仍然存在)
|
||||
if (!sensor.selected_port.isEmpty()) {
|
||||
const bool exists = ports_list.contains(sensor.selected_port);
|
||||
if (!exists) {
|
||||
sensor.selected_port = ports_list.isEmpty() ? QString{} : ports_list.front();
|
||||
}
|
||||
} else if (!ports_list.isEmpty()) {
|
||||
sensor.selected_port = ports_list.front();
|
||||
}
|
||||
|
||||
sensor.port_items->set(std::move(ports_list));
|
||||
}},
|
||||
},
|
||||
|
||||
};
|
||||
return new Widget {
|
||||
widget::pro::Layout {row},
|
||||
};
|
||||
}
|
||||
|
||||
static auto DisplayComponent(ThemeManager& manager, int index = 0) noexcept {
|
||||
auto heatmap_context = std::make_shared<MutableValue<QVector<PointData>>>();
|
||||
heatmap_context->set_silent(QVector<PointData>{
|
||||
PointData{0, 0, 1}, PointData{1, 0, 2}, PointData{2, 0, 3},
|
||||
PointData{0, 1, 3}, PointData{1, 1, 4}, PointData{2, 1, 5},
|
||||
PointData{0, 2, 6}, PointData{1, 2, 7}, PointData{2, 2, 8},
|
||||
PointData{0, 3, 9}, PointData{1, 3, 10}, PointData{2, 3, 11},
|
||||
});
|
||||
const auto row = new Row{
|
||||
lnpro::Item<HeatMapPlot> {
|
||||
plot_widget::pro::SizePolicy {
|
||||
QSizePolicy::Expanding,
|
||||
},
|
||||
MutableForward {
|
||||
plot_widget::pro::PlotData {},
|
||||
heatmap_context,
|
||||
},
|
||||
pwpro::MatrixSize {
|
||||
QSize{3, 4}
|
||||
},
|
||||
},
|
||||
};
|
||||
return new Widget {
|
||||
widget::pro::Layout{row},
|
||||
};
|
||||
}
|
||||
auto ViewComponent(ViewComponentState& state) noexcept -> raw_pointer<QWidget> {
|
||||
const auto texts = std::array {
|
||||
std::make_shared<MutableValue<QString>>("0.500"),
|
||||
std::make_shared<MutableValue<QString>>("0.500"),
|
||||
std::make_shared<MutableValue<QString>>("0.500"),
|
||||
};
|
||||
const auto progresses = std::array {
|
||||
std::make_shared<MutableValue<double>>(0.5),
|
||||
std::make_shared<MutableValue<double>>(0.5),
|
||||
std::make_shared<MutableValue<double>>(0.5),
|
||||
};
|
||||
return new FilledCard {
|
||||
capro::ThemeManager { state.manager },
|
||||
capro::SizePolicy {QSizePolicy::Expanding},
|
||||
capro::Layout<Col> {
|
||||
lnpro::Alignment {Qt::AlignTop},
|
||||
lnpro::Margin {10},
|
||||
lnpro::Spacing {10},
|
||||
|
||||
lnpro::Item {
|
||||
ComConfigComponent(state.manager,
|
||||
[texts, progresses] {
|
||||
constexpr auto random_unit = []() {
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
static std::uniform_real_distribution<double> dist(0.0, 1.0);
|
||||
return dist(gen);
|
||||
};
|
||||
for (auto&& [string, number] : std::views::zip(texts, progresses)) {
|
||||
auto v = random_unit();
|
||||
*number = v;
|
||||
*string = QString::number(v, 'f', 3);
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
lnpro::Item<Row> {
|
||||
lnpro::Item {
|
||||
DisplayComponent(state.manager),
|
||||
},
|
||||
lnpro::Item {
|
||||
DisplayComponent(state.manager),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
};
|
||||
return new Widget {
|
||||
widget::pro::Layout {row},
|
||||
};
|
||||
}
|
||||
|
||||
static auto DisplayComponent(ThemeManager& /*manager*/, int /*index*/ = 0) noexcept {
|
||||
auto& sensor = sensor_state();
|
||||
const auto row = new Row{
|
||||
lnpro::Item<HeatMapPlot> {
|
||||
plot_widget::pro::SizePolicy {
|
||||
QSizePolicy::Expanding,
|
||||
},
|
||||
MutableForward {
|
||||
plot_widget::pro::PlotData {},
|
||||
sensor.heatmap_data,
|
||||
},
|
||||
pwpro::MatrixSize {
|
||||
sensor.heatmap_matrix->get()
|
||||
},
|
||||
MutableTransform {
|
||||
[](auto& widget, const QSize& size) {
|
||||
pwpro::MatrixSize{size}.apply(widget);
|
||||
},
|
||||
sensor.heatmap_matrix
|
||||
},
|
||||
},
|
||||
};
|
||||
return new Widget {
|
||||
widget::pro::Layout{row},
|
||||
};
|
||||
}
|
||||
|
||||
auto ViewComponent(ViewComponentState& state) noexcept -> raw_pointer<QWidget> {
|
||||
return new FilledCard {
|
||||
capro::ThemeManager { state.manager },
|
||||
capro::SizePolicy {QSizePolicy::Expanding},
|
||||
capro::Layout<Col> {
|
||||
lnpro::Alignment {Qt::AlignTop},
|
||||
lnpro::Margin {10},
|
||||
lnpro::Spacing {10},
|
||||
|
||||
lnpro::Item {
|
||||
ComConfigComponent(state.manager),
|
||||
},
|
||||
|
||||
lnpro::Item<Row> {
|
||||
lnpro::Item {
|
||||
DisplayComponent(state.manager),
|
||||
},
|
||||
lnpro::Item {
|
||||
DisplayComponent(state.manager),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user