颜色修改红绿,修复帧错误卡顿bug
This commit is contained in:
@@ -91,6 +91,8 @@ set(TOUCHSENSOR_HEADERS
|
|||||||
component.hh
|
component.hh
|
||||||
components/charts/heatmap.hh
|
components/charts/heatmap.hh
|
||||||
components/charts/heatmap.impl.hh
|
components/charts/heatmap.impl.hh
|
||||||
|
components/charts/vector_field.hh
|
||||||
|
components/charts/line_chart.hh
|
||||||
dlog/dlog.hh
|
dlog/dlog.hh
|
||||||
${FFMSEP_HEADERS}
|
${FFMSEP_HEADERS}
|
||||||
components/setting.cc
|
components/setting.cc
|
||||||
@@ -98,7 +100,15 @@ set(TOUCHSENSOR_HEADERS
|
|||||||
|
|
||||||
qt6_add_resources(APP_RESOURCES resources.qrc)
|
qt6_add_resources(APP_RESOURCES resources.qrc)
|
||||||
|
|
||||||
add_executable(${PROJECT_NAME} WIN32
|
# add_executable(${PROJECT_NAME} WIN32
|
||||||
|
# ${COMPONENT_SOURCES}
|
||||||
|
# ${UTILITY_SOURCES}
|
||||||
|
# ${TOUCHSENSOR_HEADERS}
|
||||||
|
# ${BASE_SOURCES}
|
||||||
|
# main.cc
|
||||||
|
# )
|
||||||
|
|
||||||
|
add_executable(${PROJECT_NAME}
|
||||||
${COMPONENT_SOURCES}
|
${COMPONENT_SOURCES}
|
||||||
${UTILITY_SOURCES}
|
${UTILITY_SOURCES}
|
||||||
${TOUCHSENSOR_HEADERS}
|
${TOUCHSENSOR_HEADERS}
|
||||||
|
|||||||
@@ -127,10 +127,9 @@ public:
|
|||||||
cpmp->setColorScale(color_scale);
|
cpmp->setColorScale(color_scale);
|
||||||
|
|
||||||
QCPColorGradient gradient;
|
QCPColorGradient gradient;
|
||||||
gradient.setColorStopAt(0.0, QColor(240, 246, 255)); // 低值淡色
|
gradient.setColorStopAt(0.0, QColor(0, 176, 80));
|
||||||
gradient.setColorStopAt(0.35, QColor(142, 197, 252));
|
gradient.setColorStopAt(0.5, QColor(255, 214, 10));
|
||||||
gradient.setColorStopAt(0.7, QColor(56, 128, 199));
|
gradient.setColorStopAt(1.0, QColor(204, 0, 0));
|
||||||
gradient.setColorStopAt(1.0, QColor(8, 36, 95)); // 高值深色
|
|
||||||
cpmp->setGradient(gradient);
|
cpmp->setGradient(gradient);
|
||||||
|
|
||||||
cpmp->setDataRange(QCPRange(color_min, color_max));
|
cpmp->setDataRange(QCPRange(color_min, color_max));
|
||||||
|
|||||||
221
components/charts/line_chart.cc
Normal file
221
components/charts/line_chart.cc
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
//
|
||||||
|
// Created by Codex on 2025/12/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "line_chart.hh"
|
||||||
|
#include <QLinearGradient>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
using creeper::line_widget::internal::LinePlot;
|
||||||
|
|
||||||
|
LinePlot::LinePlot() {
|
||||||
|
setBackground(Qt::transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
LinePlot::~LinePlot() = default;
|
||||||
|
|
||||||
|
void LinePlot::load_theme_manager(creeper::ThemeManager& mgr) {
|
||||||
|
mgr.append_handler(this, [this](const creeper::ThemeManager& manager) {
|
||||||
|
scheme_ = manager.color_scheme();
|
||||||
|
apply_theme();
|
||||||
|
if (initialized_) {
|
||||||
|
replot();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinePlot::set_data(const QVector<QPointF>& points) {
|
||||||
|
points_ = points;
|
||||||
|
trim_points();
|
||||||
|
if (initialized_) {
|
||||||
|
update_graph();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinePlot::set_max_points(int count) {
|
||||||
|
const int clamped = std::max(1, count);
|
||||||
|
if (clamped == max_points_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
max_points_ = clamped;
|
||||||
|
trim_points();
|
||||||
|
if (initialized_) {
|
||||||
|
update_graph();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinePlot::paintEvent(QPaintEvent* event) {
|
||||||
|
if (!initialized_) {
|
||||||
|
initialize_plot();
|
||||||
|
}
|
||||||
|
QCustomPlot::paintEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinePlot::initialize_plot() {
|
||||||
|
if (initialized_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
axisRect()->setAutoMargins(QCP::msNone);
|
||||||
|
axisRect()->setMargins(QMargins(40, 12, 12, 18));
|
||||||
|
axisRect()->setBackground(QBrush(QColor(246, 249, 255)));
|
||||||
|
|
||||||
|
legend->setVisible(false);
|
||||||
|
xAxis->setVisible(true);
|
||||||
|
yAxis->setVisible(true);
|
||||||
|
xAxis->setRange(0.0, std::max(10, max_points_));
|
||||||
|
yAxis->setRange(0.0, default_y_range_);
|
||||||
|
|
||||||
|
xAxis->grid()->setVisible(false);
|
||||||
|
yAxis->grid()->setVisible(false);
|
||||||
|
xAxis->setTicks(false);
|
||||||
|
yAxis->setTicks(true);
|
||||||
|
xAxis->setSubTicks(false);
|
||||||
|
yAxis->setSubTicks(false);
|
||||||
|
xAxis->setTickLength(0);
|
||||||
|
yAxis->setTickLength(0);
|
||||||
|
yAxis->setTickLabelPadding(6);
|
||||||
|
yAxis->setLabelPadding(8);
|
||||||
|
|
||||||
|
graph_ = addGraph();
|
||||||
|
graph_->setLineStyle(QCPGraph::lsLine);
|
||||||
|
graph_->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, QPen(Qt::NoPen), QBrush(Qt::white), 6));
|
||||||
|
graph_->setBrush(QColor(16, 54, 128, 42));
|
||||||
|
graph_->setAntialiased(true);
|
||||||
|
graph_->setAdaptiveSampling(true);
|
||||||
|
|
||||||
|
initialized_ = true;
|
||||||
|
apply_theme();
|
||||||
|
update_graph();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinePlot::reset_graph_range() {
|
||||||
|
xAxis->setRange(0.0, std::max(10, max_points_));
|
||||||
|
yAxis->setRange(0.0, default_y_range_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinePlot::apply_theme() {
|
||||||
|
QColor line_color{ 16, 54, 128 };
|
||||||
|
QColor text_color{ 30, 30, 30 };
|
||||||
|
QColor bg_color{ 232, 238, 248 };
|
||||||
|
if (scheme_.has_value()) {
|
||||||
|
if (scheme_->primary.isValid()) {
|
||||||
|
line_color = scheme_->primary;
|
||||||
|
}
|
||||||
|
if (scheme_->on_surface.isValid()) {
|
||||||
|
text_color = scheme_->on_surface;
|
||||||
|
}
|
||||||
|
if (scheme_->surface_container_high.isValid()) {
|
||||||
|
bg_color = scheme_->surface_container_high;
|
||||||
|
}
|
||||||
|
else if (scheme_->surface_container.isValid()) {
|
||||||
|
bg_color = scheme_->surface_container;
|
||||||
|
}
|
||||||
|
else if (scheme_->surface.isValid()) {
|
||||||
|
bg_color = scheme_->surface;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QColor grid_color = bg_color;
|
||||||
|
|
||||||
|
QPen axis_pen(bg_color);
|
||||||
|
axis_pen.setWidthF(1.0);
|
||||||
|
xAxis->setBasePen(axis_pen);
|
||||||
|
yAxis->setBasePen(axis_pen);
|
||||||
|
xAxis->setTickPen(axis_pen);
|
||||||
|
yAxis->setTickPen(axis_pen);
|
||||||
|
xAxis->setTickLabelColor(Qt::transparent);
|
||||||
|
yAxis->setTickLabelColor(text_color);
|
||||||
|
xAxis->setLabelColor(Qt::transparent);
|
||||||
|
yAxis->setLabelColor(text_color);
|
||||||
|
axisRect()->setBackground(bg_color);
|
||||||
|
|
||||||
|
if (graph_) {
|
||||||
|
QPen pen(line_color);
|
||||||
|
pen.setWidthF(3.0);
|
||||||
|
pen.setCapStyle(Qt::RoundCap);
|
||||||
|
graph_->setPen(pen);
|
||||||
|
|
||||||
|
QLinearGradient fill_grad(0, 0, 0, 1);
|
||||||
|
fill_grad.setCoordinateMode(QGradient::CoordinateMode::ObjectBoundingMode);
|
||||||
|
fill_grad.setColorAt(0.0, QColor(line_color.red(), line_color.green(), line_color.blue(), 70));
|
||||||
|
fill_grad.setColorAt(1.0, QColor(line_color.red(), line_color.green(), line_color.blue(), 18));
|
||||||
|
graph_->setBrush(QBrush(fill_grad));
|
||||||
|
|
||||||
|
auto scatter = graph_->scatterStyle();
|
||||||
|
scatter.setPen(QPen(line_color, 1.5));
|
||||||
|
scatter.setBrush(QBrush(QColor(bg_color).lighter(104)));
|
||||||
|
scatter.setSize(7);
|
||||||
|
graph_->setScatterStyle(scatter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinePlot::update_graph() {
|
||||||
|
if (!initialized_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!graph_) {
|
||||||
|
graph_ = addGraph();
|
||||||
|
apply_theme();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (points_.isEmpty()) {
|
||||||
|
graph_->data()->clear();
|
||||||
|
reset_graph_range();
|
||||||
|
replot();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<double> keys(points_.size());
|
||||||
|
QVector<double> values(points_.size());
|
||||||
|
double min_key = std::numeric_limits<double>::max();
|
||||||
|
double max_key = std::numeric_limits<double>::lowest();
|
||||||
|
double min_val = std::numeric_limits<double>::max();
|
||||||
|
double max_val = std::numeric_limits<double>::lowest();
|
||||||
|
|
||||||
|
for (int i = 0; i < points_.size(); ++i) {
|
||||||
|
const auto& pt = points_[i];
|
||||||
|
keys[i] = pt.x();
|
||||||
|
values[i] = pt.y();
|
||||||
|
min_key = std::min(min_key, pt.x());
|
||||||
|
max_key = std::max(max_key, pt.x());
|
||||||
|
min_val = std::min(min_val, pt.y());
|
||||||
|
max_val = std::max(max_val, pt.y());
|
||||||
|
}
|
||||||
|
|
||||||
|
graph_->setData(keys, values, true);
|
||||||
|
|
||||||
|
if (min_key == std::numeric_limits<double>::max()) {
|
||||||
|
reset_graph_range();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const double key_span = std::max(1e-3, max_key - min_key);
|
||||||
|
xAxis->setRange(min_key, max_key + key_span * 0.02);
|
||||||
|
|
||||||
|
double value_span = max_val - min_val;
|
||||||
|
if (value_span < 1e-3) {
|
||||||
|
value_span = std::max(std::abs(max_val), 1.0);
|
||||||
|
min_val = max_val - value_span * 0.5;
|
||||||
|
}
|
||||||
|
const double padding = std::max(value_span * 0.25, 1.0);
|
||||||
|
yAxis->setRange(min_val - padding, max_val + padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
replot();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinePlot::trim_points() {
|
||||||
|
if (max_points_ <= 0 || points_.size() <= max_points_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int start = points_.size() - max_points_;
|
||||||
|
points_ = points_.mid(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
using namespace creeper;
|
||||||
|
|
||||||
|
void SumLinePlot::paintEvent(QPaintEvent* event) {
|
||||||
|
line_widget::internal::LinePlot::paintEvent(event);
|
||||||
|
}
|
||||||
79
components/charts/line_chart.hh
Normal file
79
components/charts/line_chart.hh
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
//
|
||||||
|
// Created by Codex on 2025/12/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "creeper-qt/utility/theme/theme.hh"
|
||||||
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
|
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||||
|
#include "qcustomplot/qcustomplot.h"
|
||||||
|
#include <QPaintEvent>
|
||||||
|
#include <QPointF>
|
||||||
|
#include <concepts>
|
||||||
|
#include <optional>
|
||||||
|
#include <qsize.h>
|
||||||
|
#include <qvector.h>
|
||||||
|
|
||||||
|
namespace creeper {
|
||||||
|
class SumLinePlot;
|
||||||
|
|
||||||
|
namespace line_widget::internal {
|
||||||
|
class LinePlot: public QCustomPlot {
|
||||||
|
public:
|
||||||
|
LinePlot();
|
||||||
|
~LinePlot() override;
|
||||||
|
|
||||||
|
void load_theme_manager(ThemeManager& mgr);
|
||||||
|
void set_data(const QVector<QPointF>& points);
|
||||||
|
void set_max_points(int count);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void initialize_plot();
|
||||||
|
void update_graph();
|
||||||
|
void apply_theme();
|
||||||
|
void reset_graph_range();
|
||||||
|
void trim_points();
|
||||||
|
|
||||||
|
QVector<QPointF> points_;
|
||||||
|
bool initialized_ = false;
|
||||||
|
std::optional<ColorScheme> scheme_;
|
||||||
|
QCPGraph* graph_ = nullptr;
|
||||||
|
int max_points_ = 240;
|
||||||
|
double default_y_range_ = 100.0;
|
||||||
|
};
|
||||||
|
} // namespace line_widget::internal
|
||||||
|
|
||||||
|
namespace line_widget::pro {
|
||||||
|
using Token = common::Token<internal::LinePlot>;
|
||||||
|
|
||||||
|
struct MaxPoints: Token {
|
||||||
|
int count;
|
||||||
|
explicit MaxPoints(int c): count{ c } { }
|
||||||
|
void apply(auto& self) const { self.set_max_points(count); }
|
||||||
|
};
|
||||||
|
|
||||||
|
using PlotData = DerivedProp<Token, QVector<QPointF>, [](auto& self, const auto& vec) {
|
||||||
|
self.set_data(vec);
|
||||||
|
}>;
|
||||||
|
|
||||||
|
template<class PlotWidget>
|
||||||
|
concept trait = std::derived_from<PlotWidget, Token>;
|
||||||
|
|
||||||
|
CREEPER_DEFINE_CHECKER(trait);
|
||||||
|
|
||||||
|
using namespace widget::pro;
|
||||||
|
using namespace theme::pro;
|
||||||
|
} // namespace line_widget::pro
|
||||||
|
|
||||||
|
struct SumLinePlot
|
||||||
|
: public Declarative<line_widget::internal::LinePlot,
|
||||||
|
CheckerOr<line_widget::pro::checker, widget::pro::checker, theme::pro::checker>> {
|
||||||
|
using Declarative::Declarative;
|
||||||
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
};
|
||||||
|
} // namespace creeper
|
||||||
202
components/charts/vector_field.cc
Normal file
202
components/charts/vector_field.cc
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
//
|
||||||
|
// Created by Codex on 2025/12/05.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "vector_field.hh"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using creeper::vector_widget::internal::VectorPlot;
|
||||||
|
|
||||||
|
VectorPlot::VectorPlot() {
|
||||||
|
setBackground(Qt::transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
VectorPlot::~VectorPlot() = default;
|
||||||
|
|
||||||
|
void VectorPlot::load_theme_manager(creeper::ThemeManager& mgr) {
|
||||||
|
mgr.append_handler(this, [this](const creeper::ThemeManager& manager) {
|
||||||
|
scheme_ = manager.color_scheme();
|
||||||
|
apply_color_scheme();
|
||||||
|
if (initialized_) {
|
||||||
|
replot();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void VectorPlot::set_matrix_size(const QSize& size) {
|
||||||
|
if (size == matrix_size_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
matrix_size_ = size;
|
||||||
|
reset_plot();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VectorPlot::set_data(const QVector<PointData>& data) {
|
||||||
|
data_points_ = data;
|
||||||
|
if (initialized_) {
|
||||||
|
update_vectors();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VectorPlot::paintEvent(QPaintEvent* event) {
|
||||||
|
if (!initialized_) {
|
||||||
|
initialize_plot();
|
||||||
|
}
|
||||||
|
QCustomPlot::paintEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VectorPlot::initialize_plot() {
|
||||||
|
if (initialized_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
xAxis->setVisible(false);
|
||||||
|
yAxis->setVisible(false);
|
||||||
|
xAxis->grid()->setPen(Qt::NoPen);
|
||||||
|
yAxis->grid()->setPen(Qt::NoPen);
|
||||||
|
xAxis->setRange(0.0, std::max(1, matrix_size_.width()));
|
||||||
|
yAxis->setRange(0.0, std::max(1, matrix_size_.height()));
|
||||||
|
|
||||||
|
xAxis->setSubTicks(true);
|
||||||
|
yAxis->setSubTicks(true);
|
||||||
|
xAxis->setTickLength(0);
|
||||||
|
yAxis->setTickLength(0);
|
||||||
|
xAxis->setSubTickLength(4);
|
||||||
|
yAxis->setSubTickLength(4);
|
||||||
|
|
||||||
|
initialized_ = true;
|
||||||
|
apply_color_scheme();
|
||||||
|
ensure_arrows();
|
||||||
|
update_vectors();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VectorPlot::reset_plot() {
|
||||||
|
clearItems();
|
||||||
|
primary_arrow_ = nullptr;
|
||||||
|
initialized_ = false;
|
||||||
|
initialize_plot();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VectorPlot::ensure_arrows() {
|
||||||
|
// no-op: legacy multi-arrow support removed
|
||||||
|
}
|
||||||
|
|
||||||
|
void VectorPlot::ensure_primary_arrow() {
|
||||||
|
if (primary_arrow_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
primary_arrow_ = new QCPItemLine(this);
|
||||||
|
primary_arrow_->start->setType(QCPItemPosition::ptPlotCoords);
|
||||||
|
primary_arrow_->end->setType(QCPItemPosition::ptPlotCoords);
|
||||||
|
primary_arrow_->setClipToAxisRect(true);
|
||||||
|
primary_arrow_->setClipAxisRect(axisRect());
|
||||||
|
primary_arrow_->setHead(QCPLineEnding(QCPLineEnding::esSpikeArrow, 14, 8));
|
||||||
|
primary_arrow_->setTail(QCPLineEnding(QCPLineEnding::esNone));
|
||||||
|
apply_color_scheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VectorPlot::apply_color_scheme() {
|
||||||
|
QColor pen_color = arrow_color_;
|
||||||
|
if (scheme_.has_value() && scheme_->primary.isValid()) {
|
||||||
|
pen_color = scheme_->primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPen strong_pen(pen_color);
|
||||||
|
strong_pen.setWidthF(8.0); // 稍细一点但仍然饱满
|
||||||
|
strong_pen.setCapStyle(Qt::FlatCap);
|
||||||
|
strong_pen.setJoinStyle(Qt::MiterJoin);
|
||||||
|
if (primary_arrow_) {
|
||||||
|
primary_arrow_->setPen(strong_pen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VectorPlot::update_vectors() {
|
||||||
|
if (!initialized_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ensure_primary_arrow();
|
||||||
|
|
||||||
|
const int width = std::max(1, matrix_size_.width());
|
||||||
|
const int height = std::max(1, matrix_size_.height());
|
||||||
|
const int expected = width * height;
|
||||||
|
if (expected <= 0 || !primary_arrow_) {
|
||||||
|
replot();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<double> values(static_cast<std::size_t>(expected), 0.0);
|
||||||
|
for (const auto& item: data_points_) {
|
||||||
|
const int x = static_cast<int>(item.x);
|
||||||
|
const int y = static_cast<int>(item.y);
|
||||||
|
if (x >= 0 && x < width && y >= 0 && y < height) {
|
||||||
|
const int idx = y * width + x;
|
||||||
|
values[static_cast<std::size_t>(idx)] = item.z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto value_at = [&](int x, int y) -> double {
|
||||||
|
if (x < 0 || x >= width || y < 0 || y >= height) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
return values[static_cast<std::size_t>(y * width + x)];
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::pair<double, double>> grads(static_cast<std::size_t>(expected), { 0.0, 0.0 });
|
||||||
|
double max_mag = 0.0;
|
||||||
|
double sum_gx = 0.0;
|
||||||
|
double sum_gy = 0.0;
|
||||||
|
for (int y = 0; y < height; ++y) {
|
||||||
|
for (int x = 0; x < width; ++x) {
|
||||||
|
const double gx = 0.5 * (value_at(x + 1, y) - value_at(x - 1, y));
|
||||||
|
const double gy = 0.5 * (value_at(x, y + 1) - value_at(x, y - 1));
|
||||||
|
const double mag = std::sqrt(gx * gx + gy * gy);
|
||||||
|
grads[static_cast<std::size_t>(y * width + x)] = { gx, gy };
|
||||||
|
max_mag = std::max(max_mag, mag);
|
||||||
|
sum_gx += gx;
|
||||||
|
sum_gy += gy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const double scale = (max_mag > 1e-6) ? (0.35 / max_mag) : 0.0;
|
||||||
|
const bool fallback = max_mag <= 1e-6;
|
||||||
|
const double mid_x = static_cast<double>(width) * 0.5;
|
||||||
|
const double mid_y = static_cast<double>(height) * 0.5;
|
||||||
|
|
||||||
|
double dir_x = sum_gx;
|
||||||
|
double dir_y = sum_gy;
|
||||||
|
if (fallback || std::abs(dir_x) + std::abs(dir_y) < 1e-6) {
|
||||||
|
dir_x = 0.0;
|
||||||
|
dir_y = -1.0; // 默认向上指,保证可见
|
||||||
|
}
|
||||||
|
double dir_len = std::sqrt(dir_x * dir_x + dir_y * dir_y);
|
||||||
|
if (dir_len < 1e-6) {
|
||||||
|
dir_x = 0.0;
|
||||||
|
dir_y = -1.0;
|
||||||
|
dir_len = 1.0;
|
||||||
|
}
|
||||||
|
dir_x /= dir_len;
|
||||||
|
dir_y /= dir_len;
|
||||||
|
const double arrow_len = 0.48 * std::min(width, height); // 稍长的指针
|
||||||
|
const double tail_ratio = 0.25; // 穿过中心的尾巴略长
|
||||||
|
const double tail_len = arrow_len * tail_ratio;
|
||||||
|
const double head_len = arrow_len - tail_len;
|
||||||
|
const double backoff = std::max(0.5, head_len * 0.12); // 轻微回缩,避免方头顶出
|
||||||
|
const double head_base = std::max(0.0, head_len - backoff);
|
||||||
|
const double cx = static_cast<double>(width) * 0.5;
|
||||||
|
const double cy = static_cast<double>(height) * 0.5;
|
||||||
|
if (primary_arrow_) {
|
||||||
|
primary_arrow_->start->setCoords(cx - dir_x * tail_len, cy - dir_y * tail_len);
|
||||||
|
primary_arrow_->end->setCoords(cx + dir_x * head_base, cy + dir_y * head_base);
|
||||||
|
primary_arrow_->setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
replot();
|
||||||
|
}
|
||||||
|
|
||||||
|
using namespace creeper;
|
||||||
|
|
||||||
|
void VectorFieldPlot::paintEvent(QPaintEvent* event) {
|
||||||
|
vector_widget::internal::VectorPlot::paintEvent(event);
|
||||||
|
}
|
||||||
82
components/charts/vector_field.hh
Normal file
82
components/charts/vector_field.hh
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
//
|
||||||
|
// Created by Codex on 2025/12/05.
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "components/charts/heatmap.hh" // for PointData definition
|
||||||
|
#include "creeper-qt/utility/theme/theme.hh"
|
||||||
|
#include "creeper-qt/utility/wrapper/common.hh"
|
||||||
|
#include "creeper-qt/utility/wrapper/property.hh"
|
||||||
|
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||||
|
#include "qcustomplot/qcustomplot.h"
|
||||||
|
#include <optional>
|
||||||
|
#include <QPaintEvent>
|
||||||
|
#include <concepts>
|
||||||
|
#include <qsize.h>
|
||||||
|
#include <qvector.h>
|
||||||
|
|
||||||
|
namespace creeper {
|
||||||
|
class VectorFieldPlot;
|
||||||
|
|
||||||
|
namespace vector_widget::internal {
|
||||||
|
class VectorPlot : public QCustomPlot {
|
||||||
|
public:
|
||||||
|
VectorPlot();
|
||||||
|
~VectorPlot() override;
|
||||||
|
|
||||||
|
void load_theme_manager(ThemeManager& mgr);
|
||||||
|
void set_matrix_size(const QSize& size);
|
||||||
|
void set_data(const QVector<PointData>& data);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void initialize_plot();
|
||||||
|
void reset_plot();
|
||||||
|
void update_vectors();
|
||||||
|
void ensure_arrows();
|
||||||
|
void apply_color_scheme();
|
||||||
|
|
||||||
|
QSize matrix_size_{ 3, 4 };
|
||||||
|
QVector<PointData> data_points_;
|
||||||
|
bool initialized_ = false;
|
||||||
|
std::optional<ColorScheme> scheme_;
|
||||||
|
QColor arrow_color_{ 16, 54, 128 }; // 深蓝色
|
||||||
|
QCPItemLine* primary_arrow_ = nullptr;
|
||||||
|
|
||||||
|
void ensure_primary_arrow();
|
||||||
|
};
|
||||||
|
} // namespace vector_widget::internal
|
||||||
|
|
||||||
|
namespace vector_widget::pro {
|
||||||
|
using Token = common::Token<internal::VectorPlot>;
|
||||||
|
|
||||||
|
struct MatrixSize : Token {
|
||||||
|
QSize size;
|
||||||
|
explicit MatrixSize(const QSize& s) : size{ s } { }
|
||||||
|
explicit MatrixSize(int w, int h) : size{ w, h } { }
|
||||||
|
void apply(auto& self) const { self.set_matrix_size(size); }
|
||||||
|
};
|
||||||
|
|
||||||
|
using PlotData = DerivedProp<Token, QVector<PointData>, [](auto& self, const auto& vec) {
|
||||||
|
self.set_data(vec);
|
||||||
|
}>;
|
||||||
|
|
||||||
|
template<class PlotWidget>
|
||||||
|
concept trait = std::derived_from<PlotWidget, Token>;
|
||||||
|
|
||||||
|
CREEPER_DEFINE_CHECKER(trait);
|
||||||
|
|
||||||
|
using namespace widget::pro;
|
||||||
|
using namespace theme::pro;
|
||||||
|
} // namespace vector_widget::pro
|
||||||
|
|
||||||
|
struct VectorFieldPlot
|
||||||
|
: public Declarative<vector_widget::internal::VectorPlot,
|
||||||
|
CheckerOr<vector_widget::pro::checker, widget::pro::checker, theme::pro::checker>> {
|
||||||
|
using Declarative::Declarative;
|
||||||
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
};
|
||||||
|
} // namespace creeper
|
||||||
@@ -23,7 +23,8 @@ enum class CPMediaType : std::uint8_t {
|
|||||||
|
|
||||||
enum class CPCodecID : std::uint32_t {
|
enum class CPCodecID : std::uint32_t {
|
||||||
Unknow = 0,
|
Unknow = 0,
|
||||||
Tactile = 0x54514354u // 'T','Q','C','T':触觉传感器协议标识 Tactile Quick Codec Type
|
Tactile = 0x54514354u, // 'T','Q','C','T':触觉传感器协议标识 Tactile Quick Codec Type
|
||||||
|
PiezoresistiveB = 0x54514342u // 'T','Q','C','B':压阻B测试协议
|
||||||
};
|
};
|
||||||
|
|
||||||
struct CPPacket {
|
struct CPPacket {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include "components/ffmsep/presist/presist.hh"
|
#include "components/ffmsep/presist/presist.hh"
|
||||||
#include "dlog/dlog.hh"
|
#include "dlog/dlog.hh"
|
||||||
|
#include <iostream>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
@@ -11,12 +11,16 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#include <future>
|
#include <future>
|
||||||
|
#include <ios>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <qlogging.h>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <qdebug.h>
|
||||||
|
#include <iostream>
|
||||||
using namespace std::chrono_literals;
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
|
||||||
@@ -51,8 +55,7 @@ struct CPStreamCore::Impl {
|
|||||||
bool flush = false;
|
bool flush = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit Impl(CPStreamConfig config)
|
explicit Impl(CPStreamConfig config): config_(std::move(config)) {
|
||||||
: config_(std::move(config)) {
|
|
||||||
normalize_config();
|
normalize_config();
|
||||||
frame_writer_ = std::make_unique<persist::JsonWritter>();
|
frame_writer_ = std::make_unique<persist::JsonWritter>();
|
||||||
}
|
}
|
||||||
@@ -125,19 +128,22 @@ struct CPStreamCore::Impl {
|
|||||||
std::lock_guard<std::mutex> lock(serial_mutex_);
|
std::lock_guard<std::mutex> lock(serial_mutex_);
|
||||||
serial_ = std::move(serial);
|
serial_ = std::move(serial);
|
||||||
}
|
}
|
||||||
} catch (const serial::IOException& ex) {
|
}
|
||||||
|
catch (const serial::IOException& ex) {
|
||||||
set_last_error(ex.what() ? ex.what() : "serial IO exception");
|
set_last_error(ex.what() ? ex.what() : "serial IO exception");
|
||||||
cpcodec_close(codec_ctx_);
|
cpcodec_close(codec_ctx_);
|
||||||
cpcodec_free_context(&codec_ctx_);
|
cpcodec_free_context(&codec_ctx_);
|
||||||
codec_ctx_ = nullptr;
|
codec_ctx_ = nullptr;
|
||||||
return false;
|
return false;
|
||||||
} catch (const serial::SerialException& ex) {
|
}
|
||||||
|
catch (const serial::SerialException& ex) {
|
||||||
set_last_error(ex.what() ? ex.what() : "serial exception");
|
set_last_error(ex.what() ? ex.what() : "serial exception");
|
||||||
cpcodec_close(codec_ctx_);
|
cpcodec_close(codec_ctx_);
|
||||||
cpcodec_free_context(&codec_ctx_);
|
cpcodec_free_context(&codec_ctx_);
|
||||||
codec_ctx_ = nullptr;
|
codec_ctx_ = nullptr;
|
||||||
return false;
|
return false;
|
||||||
} catch (const std::exception& ex) {
|
}
|
||||||
|
catch (const std::exception& ex) {
|
||||||
set_last_error(ex.what());
|
set_last_error(ex.what());
|
||||||
cpcodec_close(codec_ctx_);
|
cpcodec_close(codec_ctx_);
|
||||||
cpcodec_free_context(&codec_ctx_);
|
cpcodec_free_context(&codec_ctx_);
|
||||||
@@ -177,7 +183,8 @@ struct CPStreamCore::Impl {
|
|||||||
if (serial_->isOpen()) {
|
if (serial_->isOpen()) {
|
||||||
serial_->close();
|
serial_->close();
|
||||||
}
|
}
|
||||||
} catch (...) {
|
}
|
||||||
|
catch (...) {
|
||||||
// Ignore close errors.
|
// Ignore close errors.
|
||||||
}
|
}
|
||||||
serial_.reset();
|
serial_.reset();
|
||||||
@@ -298,11 +305,14 @@ struct CPStreamCore::Impl {
|
|||||||
try {
|
try {
|
||||||
const auto written = serial_copy->write(data, size);
|
const auto written = serial_copy->write(data, size);
|
||||||
return written == size;
|
return written == size;
|
||||||
} catch (const serial::IOException& ex) {
|
}
|
||||||
|
catch (const serial::IOException& ex) {
|
||||||
set_last_error(ex.what() ? ex.what() : "serial IO exception");
|
set_last_error(ex.what() ? ex.what() : "serial IO exception");
|
||||||
} catch (const serial::SerialException& ex) {
|
}
|
||||||
|
catch (const serial::SerialException& ex) {
|
||||||
set_last_error(ex.what() ? ex.what() : "serial exception");
|
set_last_error(ex.what() ? ex.what() : "serial exception");
|
||||||
} catch (const std::exception& ex) {
|
}
|
||||||
|
catch (const std::exception& ex) {
|
||||||
set_last_error(ex.what());
|
set_last_error(ex.what());
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -379,8 +389,7 @@ struct CPStreamCore::Impl {
|
|||||||
promise.set_value(persist::WriteResult{
|
promise.set_value(persist::WriteResult{
|
||||||
false,
|
false,
|
||||||
"no recorded frames available",
|
"no recorded frames available",
|
||||||
path
|
path });
|
||||||
});
|
|
||||||
return future;
|
return future;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,6 +418,7 @@ struct CPStreamCore::Impl {
|
|||||||
std::vector<std::uint8_t> buffer(config_.read_chunk_size);
|
std::vector<std::uint8_t> buffer(config_.read_chunk_size);
|
||||||
|
|
||||||
while (!stop_requested_.load(std::memory_order_acquire)) {
|
while (!stop_requested_.load(std::memory_order_acquire)) {
|
||||||
|
|
||||||
std::shared_ptr<serial::Serial> serial_copy;
|
std::shared_ptr<serial::Serial> serial_copy;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(serial_mutex_);
|
std::lock_guard<std::mutex> lock(serial_mutex_);
|
||||||
@@ -422,15 +432,19 @@ struct CPStreamCore::Impl {
|
|||||||
std::size_t bytes_read = 0;
|
std::size_t bytes_read = 0;
|
||||||
try {
|
try {
|
||||||
bytes_read = serial_copy->read(buffer.data(), buffer.size());
|
bytes_read = serial_copy->read(buffer.data(), buffer.size());
|
||||||
} catch (const serial::IOException& ex) {
|
qDebug() << "bytes_read: " << bytes_read;
|
||||||
|
}
|
||||||
|
catch (const serial::IOException& ex) {
|
||||||
set_last_error(ex.what() ? ex.what() : "serial IO exception");
|
set_last_error(ex.what() ? ex.what() : "serial IO exception");
|
||||||
std::this_thread::sleep_for(kReaderIdleSleep);
|
std::this_thread::sleep_for(kReaderIdleSleep);
|
||||||
continue;
|
continue;
|
||||||
} catch (const serial::SerialException& ex) {
|
}
|
||||||
|
catch (const serial::SerialException& ex) {
|
||||||
set_last_error(ex.what() ? ex.what() : "serial exception");
|
set_last_error(ex.what() ? ex.what() : "serial exception");
|
||||||
std::this_thread::sleep_for(kReaderIdleSleep);
|
std::this_thread::sleep_for(kReaderIdleSleep);
|
||||||
continue;
|
continue;
|
||||||
} catch (const std::exception& ex) {
|
}
|
||||||
|
catch (const std::exception& ex) {
|
||||||
set_last_error(ex.what());
|
set_last_error(ex.what());
|
||||||
std::this_thread::sleep_for(kReaderIdleSleep);
|
std::this_thread::sleep_for(kReaderIdleSleep);
|
||||||
continue;
|
continue;
|
||||||
@@ -440,9 +454,26 @@ struct CPStreamCore::Impl {
|
|||||||
std::this_thread::sleep_for(kReaderIdleSleep);
|
std::this_thread::sleep_for(kReaderIdleSleep);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const auto format_command =
|
||||||
|
[](const std::vector<std::uint8_t>& data) -> std::string {
|
||||||
|
if (data.empty()) {
|
||||||
|
return "[]";
|
||||||
|
}
|
||||||
|
std::ostringstream 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();
|
||||||
|
};
|
||||||
Packet packet;
|
Packet packet;
|
||||||
packet.payload.assign(buffer.begin(), buffer.begin() + static_cast<std::ptrdiff_t>(bytes_read));
|
packet.payload.assign(buffer.begin(), buffer.begin() + static_cast<std::ptrdiff_t>(bytes_read));
|
||||||
|
// std::cout << "======payload======" << std::endl;
|
||||||
|
// std::cout << format_command(packet.payload) << std::endl;
|
||||||
packet.pts = pts_counter_.fetch_add(1, std::memory_order_relaxed);
|
packet.pts = pts_counter_.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -544,6 +575,10 @@ struct CPStreamCore::Impl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (decoded->id == CPCodecID::PiezoresistiveB) {
|
||||||
|
decoded->tactile_pressures =
|
||||||
|
tactile::parse_piezoresistive_b_pressures(decoded->frame);
|
||||||
|
}
|
||||||
|
|
||||||
FrameCallback callback_copy;
|
FrameCallback callback_copy;
|
||||||
{
|
{
|
||||||
@@ -560,14 +595,16 @@ struct CPStreamCore::Impl {
|
|||||||
frame_queue_.pop_front();
|
frame_queue_.pop_front();
|
||||||
}
|
}
|
||||||
frame_queue_.push_back(decoded);
|
frame_queue_.push_back(decoded);
|
||||||
if (decoded->id == CPCodecID::Tactile) {
|
if (decoded->id == CPCodecID::Tactile || decoded->id == CPCodecID::PiezoresistiveB) {
|
||||||
frame_record_queue_.push_back(decoded);
|
frame_record_queue_.push_back(decoded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
frame_cv_.notify_one();
|
frame_cv_.notify_one();
|
||||||
} else if (rc == CP_ERROR_EAGAIN) {
|
}
|
||||||
|
else if (rc == CP_ERROR_EAGAIN) {
|
||||||
break;
|
break;
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
if (rc == CP_ERROR_EOF && packet.end_of_stream) {
|
if (rc == CP_ERROR_EOF && packet.end_of_stream) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -645,8 +682,7 @@ struct CPStreamCore::Impl {
|
|||||||
std::unique_ptr<persist::JsonWritter> frame_writer_;
|
std::unique_ptr<persist::JsonWritter> frame_writer_;
|
||||||
};
|
};
|
||||||
|
|
||||||
CPStreamCore::CPStreamCore(CPStreamConfig config)
|
CPStreamCore::CPStreamCore(CPStreamConfig config): impl_(std::make_unique<Impl>(std::move(config))) {}
|
||||||
: impl_(std::make_unique<Impl>(std::move(config))) {}
|
|
||||||
|
|
||||||
CPStreamCore::~CPStreamCore() {
|
CPStreamCore::~CPStreamCore() {
|
||||||
if (impl_) {
|
if (impl_) {
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
|
#include <nlohmann/json_fwd.hpp>
|
||||||
|
#include <sstream>
|
||||||
#include <system_error>
|
#include <system_error>
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
@@ -20,6 +22,33 @@ namespace {
|
|||||||
|
|
||||||
using nlohmann::json;
|
using nlohmann::json;
|
||||||
|
|
||||||
|
// 旧的 JSON 导出实现保留在此,避免直接删除,便于回退。
|
||||||
|
// bool is_simple_array(const json& value) { ... }
|
||||||
|
// void dump_compact_json(...)
|
||||||
|
// json serialize_tactile_frame(const DecodedFrame& frame) { ... }
|
||||||
|
|
||||||
|
std::string payload_to_csv_row(const std::vector<std::uint8_t>& payload) {
|
||||||
|
// Combine every 2 bytes (little-endian) into one 16-bit value, output in decimal.
|
||||||
|
std::ostringstream oss;
|
||||||
|
bool first = true;
|
||||||
|
for (std::size_t idx = 0; idx + 1U < payload.size(); idx += 2U) {
|
||||||
|
const auto value =
|
||||||
|
static_cast<std::uint16_t>(payload[idx]) | static_cast<std::uint16_t>(payload[idx + 1U] << 8U);
|
||||||
|
if (!first) {
|
||||||
|
oss << ',';
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
oss << value;
|
||||||
|
}
|
||||||
|
return oss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
using nlohmann::json;
|
||||||
|
|
||||||
bool is_simple_array(const json& value) {
|
bool is_simple_array(const json& value) {
|
||||||
if (!value.is_array()) {
|
if (!value.is_array()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -48,7 +77,8 @@ void dump_compact_json(std::ostream& out,
|
|||||||
out << child_indent_str << json(it.key()).dump() << ": ";
|
out << child_indent_str << json(it.key()).dump() << ": ";
|
||||||
dump_compact_json(out, it.value(), child_indent, indent_step);
|
dump_compact_json(out, it.value(), child_indent, indent_step);
|
||||||
}
|
}
|
||||||
out << '\n' << indent_str << '}';
|
out << '\n'
|
||||||
|
<< indent_str << '}';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +110,8 @@ void dump_compact_json(std::ostream& out,
|
|||||||
out << child_indent_str;
|
out << child_indent_str;
|
||||||
dump_compact_json(out, item, child_indent, indent_step);
|
dump_compact_json(out, item, child_indent, indent_step);
|
||||||
}
|
}
|
||||||
out << '\n' << indent_str << ']';
|
out << '\n'
|
||||||
|
<< indent_str << ']';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +151,6 @@ json serialize_tactile_frame(const DecodedFrame& frame) {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool WriteQueue::push(WriteRequest&& req) {
|
bool WriteQueue::push(WriteRequest&& req) {
|
||||||
@@ -158,8 +188,7 @@ void WriteQueue::stop() {
|
|||||||
cond_.notify_all();
|
cond_.notify_all();
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonWritter::JsonWritter()
|
JsonWritter::JsonWritter(): write_thread_([this] { run(); }) {}
|
||||||
: write_thread_([this] { run(); }) {}
|
|
||||||
|
|
||||||
JsonWritter::~JsonWritter() {
|
JsonWritter::~JsonWritter() {
|
||||||
stop();
|
stop();
|
||||||
@@ -185,9 +214,11 @@ void JsonWritter::run() {
|
|||||||
try {
|
try {
|
||||||
auto result = write_once(request.path, std::move(request.frames));
|
auto result = write_once(request.path, std::move(request.frames));
|
||||||
request.promise.set_value(std::move(result));
|
request.promise.set_value(std::move(result));
|
||||||
} catch (const std::exception& ex) {
|
}
|
||||||
|
catch (const std::exception& ex) {
|
||||||
request.promise.set_value(WriteResult{ false, ex.what(), request.path });
|
request.promise.set_value(WriteResult{ false, ex.what(), request.path });
|
||||||
} catch (...) {
|
}
|
||||||
|
catch (...) {
|
||||||
request.promise.set_value(WriteResult{ false, "unknown error", request.path });
|
request.promise.set_value(WriteResult{ false, "unknown error", request.path });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -199,26 +230,6 @@ WriteResult JsonWritter::write_once(const std::string& path,
|
|||||||
return { false, "export path is empty", path };
|
return { false, "export path is empty", path };
|
||||||
}
|
}
|
||||||
|
|
||||||
json tactile_frames = json::array();
|
|
||||||
|
|
||||||
for (const auto& frame : frames) {
|
|
||||||
if (!frame) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (frame->id != CPCodecID::Tactile || !frame->tactile) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
tactile_frames.push_back(serialize_tactile_frame(*frame));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tactile_frames.empty()) {
|
|
||||||
return {false, "no tactile frames available for export", path};
|
|
||||||
}
|
|
||||||
|
|
||||||
json root;
|
|
||||||
root["codec"] = "tactile";
|
|
||||||
root["frames"] = std::move(tactile_frames);
|
|
||||||
|
|
||||||
std::filesystem::path fs_path(path);
|
std::filesystem::path fs_path(path);
|
||||||
if (fs_path.has_parent_path()) {
|
if (fs_path.has_parent_path()) {
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
@@ -233,12 +244,35 @@ WriteResult JsonWritter::write_once(const std::string& path,
|
|||||||
return { false, "failed to open export file", path };
|
return { false, "failed to open export file", path };
|
||||||
}
|
}
|
||||||
|
|
||||||
dump_compact_json(stream, root);
|
bool wrote_any = false;
|
||||||
stream << '\n';
|
for (const auto& frame: frames) {
|
||||||
|
if (!frame) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
if (frame->id == CPCodecID::Tactile && frame->tactile) {
|
||||||
|
payload = frame->tactile->payload;
|
||||||
|
}
|
||||||
|
else if (frame->id == CPCodecID::PiezoresistiveB) {
|
||||||
|
payload = tactile::extract_piezoresistive_b_payload(frame->frame);
|
||||||
|
}
|
||||||
|
if (payload.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto row = payload_to_csv_row(payload);
|
||||||
|
stream << row << '\n';
|
||||||
|
wrote_any = true;
|
||||||
|
}
|
||||||
|
|
||||||
stream.flush();
|
stream.flush();
|
||||||
if (!stream.good()) {
|
if (!stream.good()) {
|
||||||
return { false, "failed to write export file", path };
|
return { false, "failed to write export file", path };
|
||||||
}
|
}
|
||||||
|
if (!wrote_any) {
|
||||||
|
return { false, "no tactile frames available for export", path };
|
||||||
|
}
|
||||||
|
|
||||||
return { true, {}, path };
|
return { true, {}, path };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,15 @@
|
|||||||
#include "components/ffmsep/cpdecoder.hh"
|
#include "components/ffmsep/cpdecoder.hh"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <new>
|
#include <new>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <qlogging.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <qdebug.h>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
namespace ffmsep::tactile {
|
namespace ffmsep::tactile {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -21,11 +25,37 @@ constexpr std::array<std::uint8_t, 2> kStartSequence{
|
|||||||
kStartByteFirst,
|
kStartByteFirst,
|
||||||
kStartByteSecond
|
kStartByteSecond
|
||||||
};
|
};
|
||||||
|
constexpr std::size_t kAbsoluteMaxPayloadBytes = 4096U; // 硬上限,防止异常配置撑爆内存
|
||||||
|
constexpr std::array<std::uint8_t, 2> kPiezoresistiveBStartSequence{
|
||||||
|
kPiezoresistiveBStartByteFirst,
|
||||||
|
kPiezoresistiveBStartByteSecond
|
||||||
|
};
|
||||||
|
constexpr std::array<std::uint8_t, 2> kPiezoresistiveBEndSequence{
|
||||||
|
kPiezoresistiveBEndByteFirst,
|
||||||
|
kPiezoresistiveBEndByteSecond
|
||||||
|
};
|
||||||
|
constexpr std::size_t kPiezoresistiveBPayloadSize =
|
||||||
|
kPiezoresistiveBValueCount * 2U;
|
||||||
|
constexpr std::size_t kPiezoresistiveBFrameSize =
|
||||||
|
kPiezoresistiveBStartSequence.size() + kPiezoresistiveBPayloadSize + kPiezoresistiveBEndSequence.size();
|
||||||
|
|
||||||
struct TactileDecoderContext {
|
struct TactileDecoderContext {
|
||||||
std::vector<std::uint8_t> fifo;
|
std::vector<std::uint8_t> fifo;
|
||||||
bool end_of_stream = false;
|
bool end_of_stream = false;
|
||||||
std::int64_t next_pts = 0;
|
std::int64_t next_pts = 0;
|
||||||
|
CPCodecID codec_id = CPCodecID::Unknow;
|
||||||
|
std::size_t max_payload_bytes = kPiezoresistiveBPayloadSize;
|
||||||
|
std::size_t max_frame_bytes = kHeaderSize + kFixedSectionSize + kPiezoresistiveBPayloadSize + 1U;
|
||||||
|
std::size_t max_fifo_bytes = (kHeaderSize + kFixedSectionSize + kPiezoresistiveBPayloadSize + 1U) * 2U;
|
||||||
|
|
||||||
|
void update_limits(std::size_t payload_bytes) {
|
||||||
|
const auto clamped_payload = std::min<std::size_t>(
|
||||||
|
std::max<std::size_t>(payload_bytes, 2U),
|
||||||
|
kAbsoluteMaxPayloadBytes);
|
||||||
|
max_payload_bytes = clamped_payload;
|
||||||
|
max_frame_bytes = kHeaderSize + kFixedSectionSize + max_payload_bytes + 1U;
|
||||||
|
max_fifo_bytes = max_frame_bytes * 2U;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const std::uint8_t* buffer_data(const std::vector<std::uint8_t>& buf) {
|
const std::uint8_t* buffer_data(const std::vector<std::uint8_t>& buf) {
|
||||||
@@ -33,24 +63,78 @@ const std::uint8_t* buffer_data(const std::vector<std::uint8_t>& buf) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::uint8_t crc8_with_xorout(const std::uint8_t* data, std::size_t length) {
|
std::uint8_t crc8_with_xorout(const std::uint8_t* data, std::size_t length) {
|
||||||
|
#if 0
|
||||||
std::uint8_t reg = kCrcInitial;
|
std::uint8_t reg = kCrcInitial;
|
||||||
for (std::size_t i = 0; i < length; ++i) {
|
for (std::size_t i = 0; i < length; ++i) {
|
||||||
reg ^= data[i];
|
reg ^= data[i];
|
||||||
for (int bit = 0; bit < 8; ++bit) {
|
for (int bit = 0; bit < 8; ++bit) {
|
||||||
if ((reg & 0x80U) != 0U) {
|
if ((reg & 0x80U) != 0U) {
|
||||||
reg = static_cast<std::uint8_t>((reg << 1U) ^ kCrcPolynomial);
|
reg = static_cast<std::uint8_t>((reg << 1U) ^ kCrcPolynomial);
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
reg = static_cast<std::uint8_t>(reg << 1U);
|
reg = static_cast<std::uint8_t>(reg << 1U);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return static_cast<std::uint8_t>(reg ^ kCrcXorOut);
|
return static_cast<std::uint8_t>(reg ^ kCrcXorOut);
|
||||||
|
#endif
|
||||||
|
constexpr std::uint8_t kPolynomial = 0x07;
|
||||||
|
constexpr std::uint8_t kInitial = 0x00;
|
||||||
|
constexpr std::uint8_t kXorOut =
|
||||||
|
0x55; // CRC-8/ITU params match device expectation
|
||||||
|
|
||||||
|
std::uint8_t reg = kInitial;
|
||||||
|
for (std::size_t idx = 0; idx < length; ++idx) {
|
||||||
|
reg = static_cast<std::uint8_t>(reg ^ data[idx]);
|
||||||
|
for (int bit = 0; bit < 8; ++bit) {
|
||||||
|
if ((reg & 0x80U) != 0U) {
|
||||||
|
reg = static_cast<std::uint8_t>((reg << 1U) ^ kPolynomial);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
reg = static_cast<std::uint8_t>(reg << 1U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return static_cast<std::uint8_t>(reg ^ kXorOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
TactileDecoderContext* get_priv(CPCodecContext* ctx) {
|
TactileDecoderContext* get_priv(CPCodecContext* ctx) {
|
||||||
return ctx ? ctx->priv_as<TactileDecoderContext>() : nullptr;
|
return ctx ? ctx->priv_as<TactileDecoderContext>() : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<std::size_t N>
|
||||||
|
void keep_partial_start_prefix(std::vector<std::uint8_t>& buf, const std::array<std::uint8_t, N>& start_sequence) {
|
||||||
|
if (buf.empty() || N == 0U) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::size_t max_prefix = std::min<std::size_t>(N - 1U, buf.size());
|
||||||
|
for (std::size_t len = max_prefix; len > 0; --len) {
|
||||||
|
const auto seq_begin = start_sequence.begin();
|
||||||
|
const auto seq_end = seq_begin + static_cast<std::ptrdiff_t>(len);
|
||||||
|
const auto buf_begin =
|
||||||
|
buf.end() - static_cast<std::ptrdiff_t>(len);
|
||||||
|
if (std::equal(seq_begin, seq_end, buf_begin)) {
|
||||||
|
std::vector<std::uint8_t> tail(buf_begin, buf.end());
|
||||||
|
buf.swap(tail);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void trim_fifo_if_needed(std::vector<std::uint8_t>& buf, std::size_t max_fifo_bytes) {
|
||||||
|
if (buf.size() <= max_fifo_bytes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto excess = buf.size() - max_fifo_bytes;
|
||||||
|
buf.erase(buf.begin(), buf.begin() + static_cast<std::ptrdiff_t>(excess));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::atomic<std::size_t>& expected_payload_bytes_for_tactile() {
|
||||||
|
static std::atomic<std::size_t> expected{kPiezoresistiveBPayloadSize};
|
||||||
|
return expected;
|
||||||
|
}
|
||||||
|
|
||||||
int tactile_init(CPCodecContext* ctx) {
|
int tactile_init(CPCodecContext* ctx) {
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
return CP_ERROR_INVALID_ARGUMENT;
|
return CP_ERROR_INVALID_ARGUMENT;
|
||||||
@@ -60,6 +144,14 @@ int tactile_init(CPCodecContext* ctx) {
|
|||||||
}
|
}
|
||||||
auto* storage = static_cast<TactileDecoderContext*>(ctx->priv_data);
|
auto* storage = static_cast<TactileDecoderContext*>(ctx->priv_data);
|
||||||
new (storage) TactileDecoderContext();
|
new (storage) TactileDecoderContext();
|
||||||
|
storage->codec_id = ctx->codec ? ctx->codec->id : CPCodecID::Unknow;
|
||||||
|
if (storage->codec_id == CPCodecID::Tactile) {
|
||||||
|
const auto expected = expected_payload_bytes_for_tactile().load(std::memory_order_relaxed);
|
||||||
|
storage->update_limits(expected);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
storage->update_limits(kPiezoresistiveBPayloadSize);
|
||||||
|
}
|
||||||
return CP_SUCCESS;
|
return CP_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +177,7 @@ int tactile_send_packet(CPCodecContext* ctx, const CPPacket& packet) {
|
|||||||
|
|
||||||
if (!packet.payload.empty()) {
|
if (!packet.payload.empty()) {
|
||||||
priv->fifo.insert(priv->fifo.end(), packet.payload.begin(), packet.payload.end());
|
priv->fifo.insert(priv->fifo.end(), packet.payload.begin(), packet.payload.end());
|
||||||
|
trim_fifo_if_needed(priv->fifo, priv->max_fifo_bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (packet.end_of_stream) {
|
if (packet.end_of_stream) {
|
||||||
@@ -111,10 +204,9 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
|
|||||||
return CP_ERROR_EAGAIN;
|
return CP_ERROR_EAGAIN;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto start_it = std::search(buf.begin(), buf.end(),
|
const auto start_it = std::search(buf.begin(), buf.end(), kStartSequence.begin(), kStartSequence.end());
|
||||||
kStartSequence.begin(), kStartSequence.end());
|
|
||||||
if (start_it == buf.end()) {
|
if (start_it == buf.end()) {
|
||||||
buf.clear();
|
keep_partial_start_prefix(buf, kStartSequence);
|
||||||
if (priv->end_of_stream) {
|
if (priv->end_of_stream) {
|
||||||
priv->end_of_stream = false;
|
priv->end_of_stream = false;
|
||||||
return CP_ERROR_EOF;
|
return CP_ERROR_EOF;
|
||||||
@@ -142,8 +234,7 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const std::uint16_t data_length =
|
const std::uint16_t data_length =
|
||||||
static_cast<std::uint16_t>(data[2]) |
|
static_cast<std::uint16_t>(data[2]) | static_cast<std::uint16_t>(static_cast<std::uint16_t>(data[3]) << 8U);
|
||||||
static_cast<std::uint16_t>(static_cast<std::uint16_t>(data[3]) << 8U);
|
|
||||||
|
|
||||||
if (data_length < kFixedSectionSize) {
|
if (data_length < kFixedSectionSize) {
|
||||||
buf.erase(buf.begin());
|
buf.erase(buf.begin());
|
||||||
@@ -151,6 +242,10 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const std::size_t total_frame_length = kHeaderSize + static_cast<std::size_t>(data_length) + 1U;
|
const std::size_t total_frame_length = kHeaderSize + static_cast<std::size_t>(data_length) + 1U;
|
||||||
|
if (total_frame_length > priv->max_frame_bytes) {
|
||||||
|
buf.erase(buf.begin());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (buf.size() < total_frame_length) {
|
if (buf.size() < total_frame_length) {
|
||||||
if (priv->end_of_stream) {
|
if (priv->end_of_stream) {
|
||||||
buf.clear();
|
buf.clear();
|
||||||
@@ -160,8 +255,10 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
|
|||||||
return CP_ERROR_EAGAIN;
|
return CP_ERROR_EAGAIN;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::uint8_t computed_crc = crc8_with_xorout(data + kHeaderSize, data_length);
|
const auto crc_offset = total_frame_length - 1U;
|
||||||
const std::uint8_t frame_crc = data[kHeaderSize + static_cast<std::size_t>(data_length)];
|
const std::uint8_t computed_crc =
|
||||||
|
crc8_with_xorout(data, crc_offset); // header..last payload byte (excludes CRC)
|
||||||
|
const std::uint8_t frame_crc = data[crc_offset];
|
||||||
if (computed_crc != frame_crc) {
|
if (computed_crc != frame_crc) {
|
||||||
buf.erase(buf.begin());
|
buf.erase(buf.begin());
|
||||||
continue;
|
continue;
|
||||||
@@ -177,6 +274,70 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int tactile_b_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
|
||||||
|
auto* priv = get_priv(ctx);
|
||||||
|
if (!priv) {
|
||||||
|
return CP_ERROR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& buf = priv->fifo;
|
||||||
|
while (true) {
|
||||||
|
if (buf.size() < kPiezoresistiveBStartSequence.size()) {
|
||||||
|
if (priv->end_of_stream) {
|
||||||
|
buf.clear();
|
||||||
|
priv->end_of_stream = false;
|
||||||
|
return CP_ERROR_EOF;
|
||||||
|
}
|
||||||
|
return CP_ERROR_EAGAIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto start_it = std::search(buf.begin(),
|
||||||
|
buf.end(),
|
||||||
|
kPiezoresistiveBStartSequence.begin(),
|
||||||
|
kPiezoresistiveBStartSequence.end());
|
||||||
|
if (start_it == buf.end()) {
|
||||||
|
keep_partial_start_prefix(buf, kPiezoresistiveBStartSequence);
|
||||||
|
if (priv->end_of_stream) {
|
||||||
|
priv->end_of_stream = false;
|
||||||
|
return CP_ERROR_EOF;
|
||||||
|
}
|
||||||
|
return CP_ERROR_EAGAIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start_it != buf.begin()) {
|
||||||
|
buf.erase(buf.begin(), start_it);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf.size() < kPiezoresistiveBFrameSize) {
|
||||||
|
if (priv->end_of_stream) {
|
||||||
|
buf.clear();
|
||||||
|
priv->end_of_stream = false;
|
||||||
|
return CP_ERROR_EOF;
|
||||||
|
}
|
||||||
|
return CP_ERROR_EAGAIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto end_offset = kPiezoresistiveBFrameSize - kPiezoresistiveBEndSequence.size();
|
||||||
|
const auto end_it = buf.begin() + static_cast<std::ptrdiff_t>(end_offset);
|
||||||
|
if (!std::equal(end_it,
|
||||||
|
end_it + static_cast<std::ptrdiff_t>(kPiezoresistiveBEndSequence.size()),
|
||||||
|
kPiezoresistiveBEndSequence.begin())) {
|
||||||
|
buf.erase(buf.begin());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.data.assign(buf.begin(),
|
||||||
|
buf.begin() + static_cast<std::ptrdiff_t>(kPiezoresistiveBFrameSize));
|
||||||
|
frame.pts = priv->next_pts++;
|
||||||
|
frame.key_frame = true;
|
||||||
|
frame.valid = true;
|
||||||
|
|
||||||
|
buf.erase(buf.begin(),
|
||||||
|
buf.begin() + static_cast<std::ptrdiff_t>(kPiezoresistiveBFrameSize));
|
||||||
|
return CP_SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const CPCodec kTactileCodec{
|
const CPCodec kTactileCodec{
|
||||||
.name = "tactile_serial",
|
.name = "tactile_serial",
|
||||||
.long_name = "Framed tactile sensor serial protocol decoder",
|
.long_name = "Framed tactile sensor serial protocol decoder",
|
||||||
@@ -188,56 +349,69 @@ const CPCodec kTactileCodec {
|
|||||||
.send_packet = &tactile_send_packet,
|
.send_packet = &tactile_send_packet,
|
||||||
.receive_frame = &tactile_receive_frame
|
.receive_frame = &tactile_receive_frame
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
const CPCodec kTactileBCodec{
|
||||||
|
.name = "tactile_serial_b",
|
||||||
|
.long_name = "Piezoresistive B tactile serial protocol decoder",
|
||||||
|
.type = CPMediaType::Data,
|
||||||
|
.id = CPCodecID::PiezoresistiveB,
|
||||||
|
.priv_data_size = sizeof(TactileDecoderContext),
|
||||||
|
.init = &tactile_init,
|
||||||
|
.close = &tactile_close,
|
||||||
|
.send_packet = &tactile_send_packet,
|
||||||
|
.receive_frame = &tactile_b_receive_frame
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
std::optional<TactileFrame> parse_frame(const CPFrame& frame) {
|
std::optional<TactileFrame> parse_frame(const CPFrame& frame) {
|
||||||
if (!frame.valid || frame.data.size() < kMinimumFrameSize) {
|
// if (!frame.valid || frame.data.size() < kMinimumFrameSize) {
|
||||||
return std::nullopt;
|
// return std::nullopt;
|
||||||
}
|
// }
|
||||||
|
std::cout << "frame valid:" << frame.valid << ", frame.data.size:" << frame.data.size() << std::endl;
|
||||||
|
|
||||||
const auto* bytes = frame.data.data();
|
const auto* bytes = frame.data.data();
|
||||||
const std::size_t size = frame.data.size();
|
const std::size_t size = frame.data.size();
|
||||||
|
|
||||||
if (bytes[0] != kStartByteFirst || bytes[1] != kStartByteSecond) {
|
if (bytes[0] != kStartByteFirst || bytes[1] != kStartByteSecond) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
std::cout << "frame valid1:" << frame.valid << ", frame.data.size1:" << frame.data.size() << std::endl;
|
||||||
const std::uint16_t data_length =
|
const std::uint16_t data_length =
|
||||||
static_cast<std::uint16_t>(bytes[2]) |
|
static_cast<std::uint16_t>(bytes[2]) | static_cast<std::uint16_t>(static_cast<std::uint16_t>(bytes[3]) << 8U);
|
||||||
static_cast<std::uint16_t>(static_cast<std::uint16_t>(bytes[3]) << 8U);
|
qDebug() << "data_length: " << data_length;
|
||||||
if (data_length < kFixedSectionSize) {
|
if (data_length < kFixedSectionSize) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
std::cout << "frame valid2:" << frame.valid << ", frame.data.size1:" << frame.data.size() << std::endl;
|
||||||
const std::size_t expected_size = kHeaderSize + static_cast<std::size_t>(data_length) + 1U;
|
const std::size_t expected_size = kHeaderSize + static_cast<std::size_t>(data_length) + 1U;
|
||||||
if (size != expected_size) {
|
if (size != expected_size) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
std::cout << "frame valid3:" << frame.valid << ", frame.data.size1:" << frame.data.size() << std::endl;
|
||||||
const std::uint8_t crc_byte = bytes[size - 1U];
|
const std::uint8_t crc_byte = bytes[expected_size - 1U];
|
||||||
const std::uint8_t computed_crc = crc8_with_xorout(bytes + kHeaderSize, data_length);
|
const std::uint8_t computed_crc =
|
||||||
|
crc8_with_xorout(bytes, expected_size - 1U); // header..last payload byte
|
||||||
if (computed_crc != crc_byte) {
|
if (computed_crc != crc_byte) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
std::cout << "frame valid4:" << frame.valid << ", frame.data.size1:" << frame.data.size() << std::endl;
|
||||||
const std::uint8_t device_address = bytes[4];
|
const std::uint8_t device_address = bytes[4];
|
||||||
const std::uint8_t reserved = bytes[5];
|
const std::uint8_t reserved = bytes[5];
|
||||||
const std::uint8_t response_function = bytes[6];
|
const std::uint8_t response_function = bytes[6];
|
||||||
const std::uint32_t start_address =
|
const std::uint32_t start_address =
|
||||||
static_cast<std::uint32_t>(bytes[7]) |
|
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);
|
||||||
(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 =
|
const std::uint16_t return_byte_count =
|
||||||
static_cast<std::uint16_t>(bytes[11]) |
|
static_cast<std::uint16_t>(bytes[11]) | (static_cast<std::uint16_t>(bytes[12]) << 8U);
|
||||||
(static_cast<std::uint16_t>(bytes[12]) << 8U);
|
|
||||||
const std::uint8_t status = bytes[13];
|
const std::uint8_t status = bytes[13];
|
||||||
|
|
||||||
const std::size_t payload_offset = kHeaderSize + kFixedSectionSize;
|
const std::size_t payload_offset = kHeaderSize + kFixedSectionSize;
|
||||||
const std::size_t payload_length = static_cast<std::size_t>(data_length) - kFixedSectionSize;
|
const std::size_t payload_available =
|
||||||
if (payload_length != return_byte_count) {
|
data_length > kFixedSectionSize ? static_cast<std::size_t>(data_length) - kFixedSectionSize : 0U;
|
||||||
|
const std::size_t requested_payload = static_cast<std::size_t>(return_byte_count);
|
||||||
|
if (payload_available < requested_payload) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
std::cout << "frame valid5:" << frame.valid << ", frame.data.size1:" << frame.data.size() << std::endl;
|
||||||
TactileFrame parsed{};
|
TactileFrame parsed{};
|
||||||
parsed.device_address = device_address;
|
parsed.device_address = device_address;
|
||||||
parsed.reserved = reserved;
|
parsed.reserved = reserved;
|
||||||
@@ -246,23 +420,22 @@ std::optional<TactileFrame> parse_frame(const CPFrame& frame) {
|
|||||||
parsed.start_address = start_address;
|
parsed.start_address = start_address;
|
||||||
parsed.return_byte_count = return_byte_count;
|
parsed.return_byte_count = return_byte_count;
|
||||||
parsed.status = status;
|
parsed.status = status;
|
||||||
parsed.payload.assign(bytes + payload_offset, bytes + payload_offset + payload_length);
|
parsed.payload.assign(bytes + payload_offset, bytes + payload_offset + requested_payload);
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::uint16_t> parse_pressure_values(const TactileFrame& frame) {
|
std::vector<std::uint16_t> parse_pressure_values(const TactileFrame& frame) {
|
||||||
if (frame.payload.size() != frame.return_byte_count) {
|
std::cout << "parse_pressure_values" << std::endl;
|
||||||
return {};
|
const auto requested_bytes = static_cast<std::size_t>(frame.return_byte_count);
|
||||||
}
|
const auto usable_bytes = std::min(requested_bytes, frame.payload.size());
|
||||||
if (frame.payload.empty() || (frame.payload.size() % 2U != 0U)) {
|
if (usable_bytes == 0U || (usable_bytes % 2U != 0U)) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
std::vector<std::uint16_t> values;
|
std::vector<std::uint16_t> values;
|
||||||
values.reserve(frame.payload.size() / 2U);
|
values.reserve(usable_bytes / 2U);
|
||||||
for (std::size_t idx = 0; idx + 1U < frame.payload.size(); idx += 2U) {
|
for (std::size_t idx = 0; idx + 1U < usable_bytes; idx += 2U) {
|
||||||
const std::uint16_t value = static_cast<std::uint16_t>(
|
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]) | static_cast<std::uint16_t>(frame.payload[idx + 1U] << 8U));
|
||||||
static_cast<std::uint16_t>(frame.payload[idx + 1U] << 8U));
|
|
||||||
values.push_back(value);
|
values.push_back(value);
|
||||||
}
|
}
|
||||||
return values;
|
return values;
|
||||||
@@ -278,6 +451,71 @@ std::optional<MatrixSize> parse_matrix_size_payload(const TactileFrame& frame) {
|
|||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint16_t> parse_piezoresistive_b_pressures(const CPFrame& frame) {
|
||||||
|
// if (!frame.valid) {
|
||||||
|
// return {};
|
||||||
|
// }
|
||||||
|
// if (frame.data.size() != kPiezoresistiveBFrameSize) {
|
||||||
|
// return {};
|
||||||
|
// }
|
||||||
|
// if (frame.data.size() < kPiezoresistiveBFrameSize) {
|
||||||
|
// return {};
|
||||||
|
// }
|
||||||
|
// if (frame.data[0] != kPiezoresistiveBStartByteFirst || frame.data[1] != kPiezoresistiveBStartByteSecond) {
|
||||||
|
// return {};
|
||||||
|
// }
|
||||||
|
const auto end_offset = kPiezoresistiveBFrameSize - kPiezoresistiveBEndSequence.size();
|
||||||
|
// if (frame.data[end_offset] != kPiezoresistiveBEndByteFirst || frame.data[end_offset + 1U] != kPiezoresistiveBEndByteSecond) {
|
||||||
|
// return {};
|
||||||
|
// }
|
||||||
|
|
||||||
|
std::vector<std::uint16_t> values;
|
||||||
|
values.reserve(kPiezoresistiveBValueCount);
|
||||||
|
std::cout << "valuessize:" << values.size() << std::endl;
|
||||||
|
const auto payload_offset = kPiezoresistiveBStartSequence.size();
|
||||||
|
for (std::size_t idx = 0; idx < kPiezoresistiveBValueCount; ++idx) {
|
||||||
|
const auto base = payload_offset + idx * 2U;
|
||||||
|
if (base + 1U >= frame.data.size()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const auto hi = static_cast<std::uint16_t>(frame.data[base]);
|
||||||
|
const auto lo = static_cast<std::uint16_t>(frame.data[base + 1U]);
|
||||||
|
values.push_back(static_cast<std::uint16_t>((hi << 8U) | lo));
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> extract_piezoresistive_b_payload(const CPFrame& frame) {
|
||||||
|
if (!frame.valid) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (frame.data.size() != kPiezoresistiveBFrameSize) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (frame.data[0] != kPiezoresistiveBStartByteFirst || frame.data[1] != kPiezoresistiveBStartByteSecond) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const auto payload_offset = kPiezoresistiveBStartSequence.size();
|
||||||
|
const auto payload_end = payload_offset + kPiezoresistiveBPayloadSize;
|
||||||
|
if (frame.data.size() < payload_end + kPiezoresistiveBEndSequence.size()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (frame.data[payload_end] != kPiezoresistiveBEndByteFirst || frame.data[payload_end + 1U] != kPiezoresistiveBEndByteSecond) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::vector<std::uint8_t>(
|
||||||
|
frame.data.begin() + static_cast<std::ptrdiff_t>(payload_offset),
|
||||||
|
frame.data.begin() + static_cast<std::ptrdiff_t>(payload_end));
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_tactile_expected_payload_bytes(std::size_t bytes) {
|
||||||
|
const auto clamped = std::min<std::size_t>(
|
||||||
|
std::max<std::size_t>(bytes, 2U),
|
||||||
|
kAbsoluteMaxPayloadBytes);
|
||||||
|
expected_payload_bytes_for_tactile().store(clamped, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
const CPCodec* tactile_codec() {
|
const CPCodec* tactile_codec() {
|
||||||
return &kTactileCodec;
|
return &kTactileCodec;
|
||||||
}
|
}
|
||||||
@@ -285,4 +523,12 @@ const CPCodec* tactile_codec() {
|
|||||||
void register_tactile_codec() {
|
void register_tactile_codec() {
|
||||||
cpcodec_register(&kTactileCodec);
|
cpcodec_register(&kTactileCodec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CPCodec* tactile_b_codec() {
|
||||||
|
return &kTactileBCodec;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void register_tactile_b_codec() {
|
||||||
|
cpcodec_register(&kTactileBCodec);
|
||||||
|
}
|
||||||
|
} // namespace ffmsep::tactile
|
||||||
|
|||||||
@@ -8,6 +8,11 @@
|
|||||||
namespace ffmsep::tactile {
|
namespace ffmsep::tactile {
|
||||||
inline constexpr std::uint8_t kStartByteFirst = 0xAA;
|
inline constexpr std::uint8_t kStartByteFirst = 0xAA;
|
||||||
inline constexpr std::uint8_t kStartByteSecond = 0x55;
|
inline constexpr std::uint8_t kStartByteSecond = 0x55;
|
||||||
|
inline constexpr std::uint8_t kPiezoresistiveBStartByteFirst = 0xF0;
|
||||||
|
inline constexpr std::uint8_t kPiezoresistiveBStartByteSecond = 0xF1;
|
||||||
|
inline constexpr std::uint8_t kPiezoresistiveBEndByteFirst = 0xF1;
|
||||||
|
inline constexpr std::uint8_t kPiezoresistiveBEndByteSecond = 0xF0;
|
||||||
|
inline constexpr std::size_t kPiezoresistiveBValueCount = 200;
|
||||||
|
|
||||||
enum class FunctionCode : std::uint8_t {
|
enum class FunctionCode : std::uint8_t {
|
||||||
Unknown = 0x00,
|
Unknown = 0x00,
|
||||||
@@ -40,7 +45,13 @@ 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_matrix_size_payload(const TactileFrame &frame);
|
||||||
std::optional<MatrixSize>
|
std::optional<MatrixSize>
|
||||||
parse_patrix_coordinate_payload(const TactileFrame &frame);
|
parse_patrix_coordinate_payload(const TactileFrame &frame);
|
||||||
|
std::vector<std::uint16_t> parse_piezoresistive_b_pressures(const CPFrame &frame);
|
||||||
|
std::vector<std::uint8_t> extract_piezoresistive_b_payload(const CPFrame &frame);
|
||||||
|
// 配置触觉 A 类型预期的 payload 字节数(点数 * 2),用于限制解码 FIFO。
|
||||||
|
void set_tactile_expected_payload_bytes(std::size_t bytes);
|
||||||
|
|
||||||
const CPCodec *tactile_codec();
|
const CPCodec *tactile_codec();
|
||||||
void register_tactile_codec();
|
void register_tactile_codec();
|
||||||
|
const CPCodec *tactile_b_codec();
|
||||||
|
void register_tactile_b_codec();
|
||||||
} // namespace ffmsep::tactile
|
} // namespace ffmsep::tactile
|
||||||
|
|||||||
@@ -42,6 +42,12 @@
|
|||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QSpinBox>
|
#include <QSpinBox>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
|
||||||
namespace repest_literals {
|
namespace repest_literals {
|
||||||
@@ -66,6 +72,61 @@ namespace fbpro = filled_button::pro;
|
|||||||
static std::weak_ptr<MutableValue<std::vector<ConfigProfile>>> g_profiles_store;
|
static std::weak_ptr<MutableValue<std::vector<ConfigProfile>>> g_profiles_store;
|
||||||
static std::function<void()> g_profiles_refresh;
|
static std::function<void()> g_profiles_refresh;
|
||||||
|
|
||||||
|
static QString TactileTypeToJsonString(Tactile_TYPE type)
|
||||||
|
{
|
||||||
|
switch (type) {
|
||||||
|
case Tactile_TYPE::PiezoresistiveA:
|
||||||
|
return QStringLiteral("PiezoresistiveA");
|
||||||
|
case Tactile_TYPE::PiezoresistiveB:
|
||||||
|
return QStringLiteral("PiezoresistiveB");
|
||||||
|
case Tactile_TYPE::Hall:
|
||||||
|
default:
|
||||||
|
return QStringLiteral("Hall");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Tactile_TYPE TactileTypeFromJsonString(const QString& str)
|
||||||
|
{
|
||||||
|
if (str == QStringLiteral("PiezoresistiveA")) {
|
||||||
|
return Tactile_TYPE::PiezoresistiveA;
|
||||||
|
}
|
||||||
|
if (str == QStringLiteral("PiezoresistiveB")) {
|
||||||
|
return Tactile_TYPE::PiezoresistiveB;
|
||||||
|
}
|
||||||
|
return Tactile_TYPE::Hall;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QJsonObject ConfigProfileToJson(const ConfigProfile& profile)
|
||||||
|
{
|
||||||
|
QJsonObject obj;
|
||||||
|
obj.insert(QStringLiteral("name"), profile.name);
|
||||||
|
obj.insert(QStringLiteral("type"), TactileTypeToJsonString(profile.type));
|
||||||
|
obj.insert(QStringLiteral("matrix_width"), profile.matrix_width);
|
||||||
|
obj.insert(QStringLiteral("matrix_height"), profile.matrix_height);
|
||||||
|
obj.insert(QStringLiteral("range_left"), profile.range_left);
|
||||||
|
obj.insert(QStringLiteral("range_right"), profile.range_right);
|
||||||
|
obj.insert(QStringLiteral("baud_rate"), profile.baud_rate);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ConfigProfileFromJson(const QJsonObject& obj, ConfigProfile& out_profile)
|
||||||
|
{
|
||||||
|
const auto name = obj.value(QStringLiteral("name")).toString();
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out_profile.name = name;
|
||||||
|
out_profile.type = TactileTypeFromJsonString(obj.value(QStringLiteral("type")).toString());
|
||||||
|
out_profile.matrix_width = obj.value(QStringLiteral("matrix_width")).toInt(0);
|
||||||
|
out_profile.matrix_height = obj.value(QStringLiteral("matrix_height")).toInt(0);
|
||||||
|
out_profile.range_left = obj.value(QStringLiteral("range_left")).toInt(0);
|
||||||
|
out_profile.range_right = obj.value(QStringLiteral("range_right")).toInt(0);
|
||||||
|
out_profile.baud_rate = obj.value(QStringLiteral("baud_rate")).toInt(0);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static void ShowEditProfileDialog(
|
static void ShowEditProfileDialog(
|
||||||
const ConfigProfile& current,
|
const ConfigProfile& current,
|
||||||
const std::shared_ptr<MutableValue<std::vector<ConfigProfile>>>& profiles_store) {
|
const std::shared_ptr<MutableValue<std::vector<ConfigProfile>>>& profiles_store) {
|
||||||
@@ -252,7 +313,75 @@ static auto ImportProfileLongItem(creeper::ThemeManager& manager) {
|
|||||||
widget::pro::MinimumHeight {40},
|
widget::pro::MinimumHeight {40},
|
||||||
widget::pro::MinimumWidth {320},
|
widget::pro::MinimumWidth {320},
|
||||||
fbpro::Radius {12},
|
fbpro::Radius {12},
|
||||||
fbpro::Clickable {[]{ qDebug() << "ImportProfileLongItem"; }},
|
fbpro::Clickable {[]{
|
||||||
|
const QString file_name = QFileDialog::getOpenFileName(
|
||||||
|
nullptr,
|
||||||
|
QStringLiteral("导入配置"),
|
||||||
|
QString(),
|
||||||
|
QStringLiteral("配置文件 (*.conf);;所有文件 (*.*)"));
|
||||||
|
if (file_name.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile file(file_name);
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
QMessageBox::warning(nullptr, QStringLiteral("导入配置"),
|
||||||
|
QStringLiteral("无法打开配置文件。"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray data = file.readAll();
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
QJsonParseError parse_error {};
|
||||||
|
const QJsonDocument doc = QJsonDocument::fromJson(data, &parse_error);
|
||||||
|
if (doc.isNull() || parse_error.error != QJsonParseError::NoError || !doc.isArray()) {
|
||||||
|
QMessageBox::warning(nullptr, QStringLiteral("导入配置"),
|
||||||
|
QStringLiteral("配置文件格式不正确。"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonArray array = doc.array();
|
||||||
|
std::vector<ConfigProfile> imported_profiles;
|
||||||
|
imported_profiles.reserve(static_cast<std::size_t>(array.size()));
|
||||||
|
for (const auto& value : array) {
|
||||||
|
if (!value.isObject()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ConfigProfile profile {};
|
||||||
|
if (ConfigProfileFromJson(value.toObject(), profile)) {
|
||||||
|
imported_profiles.push_back(profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imported_profiles.empty()) {
|
||||||
|
QMessageBox::warning(nullptr, QStringLiteral("导入配置"),
|
||||||
|
QStringLiteral("配置文件中没有有效的配置。"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& helper = GlobalHelper::instance();
|
||||||
|
|
||||||
|
// 清空现有配置
|
||||||
|
const auto existing = helper.get_all_profile();
|
||||||
|
for (const auto& p : existing) {
|
||||||
|
helper.remove_profile(p.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入新配置到 ini
|
||||||
|
for (const auto& p : imported_profiles) {
|
||||||
|
helper.add_new_profile(p);
|
||||||
|
}
|
||||||
|
helper.reload_profiles();
|
||||||
|
|
||||||
|
if (auto store = g_profiles_store.lock()) {
|
||||||
|
store->set(helper.get_all_profile());
|
||||||
|
}
|
||||||
|
RefreshProfilesForView();
|
||||||
|
if (g_profiles_refresh) {
|
||||||
|
g_profiles_refresh();
|
||||||
|
}
|
||||||
|
}},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
static auto ExportProfileLongItem(creeper::ThemeManager& manager) {
|
static auto ExportProfileLongItem(creeper::ThemeManager& manager) {
|
||||||
@@ -263,7 +392,45 @@ static auto ExportProfileLongItem(creeper::ThemeManager& manager) {
|
|||||||
widget::pro::MinimumHeight {40},
|
widget::pro::MinimumHeight {40},
|
||||||
widget::pro::MinimumWidth {320},
|
widget::pro::MinimumWidth {320},
|
||||||
fbpro::Radius {12},
|
fbpro::Radius {12},
|
||||||
fbpro::Clickable {[]{ qDebug() << "ExportProfileLongItem"; }},
|
fbpro::Clickable {[]{
|
||||||
|
QString file_name = QFileDialog::getSaveFileName(
|
||||||
|
nullptr,
|
||||||
|
QStringLiteral("导出配置"),
|
||||||
|
QString(),
|
||||||
|
QStringLiteral("配置文件 (*.conf);;所有文件 (*.*)"));
|
||||||
|
if (file_name.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_name.endsWith(QStringLiteral(".conf"), Qt::CaseInsensitive)) {
|
||||||
|
file_name.append(QStringLiteral(".conf"));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& helper = GlobalHelper::instance();
|
||||||
|
helper.reload_profiles();
|
||||||
|
const auto& profiles = helper.get_all_profile();
|
||||||
|
|
||||||
|
QJsonArray array;
|
||||||
|
// array.reserve(static_cast<int>(profiles.size()));
|
||||||
|
for (const auto& p : profiles) {
|
||||||
|
array.append(ConfigProfileToJson(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonDocument doc(array);
|
||||||
|
|
||||||
|
QFile file(file_name);
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||||
|
QMessageBox::warning(nullptr, QStringLiteral("导出配置"),
|
||||||
|
QStringLiteral("无法写入配置文件。"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.write(doc.toJson(QJsonDocument::Indented));
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
QMessageBox::information(nullptr, QStringLiteral("导出配置"),
|
||||||
|
QStringLiteral("配置导出完成。"));
|
||||||
|
}},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
static auto ProfileItemComponent(creeper::ThemeManager& manager, ConfigProfile& profile,
|
static auto ProfileItemComponent(creeper::ThemeManager& manager, ConfigProfile& profile,
|
||||||
|
|||||||
@@ -2,61 +2,67 @@
|
|||||||
// Created by Lenn on 2025/10/14.
|
// Created by Lenn on 2025/10/14.
|
||||||
//
|
//
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <array>
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <exception>
|
|
||||||
#include <future>
|
|
||||||
#include <memory>
|
|
||||||
#include <mutex>
|
|
||||||
#include <thread>
|
|
||||||
#include <QString>
|
|
||||||
#include <QObject>
|
|
||||||
#include <QMetaObject>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <QtCore/Qt>
|
|
||||||
#include <QCoreApplication>
|
|
||||||
#include <QDateTime>
|
|
||||||
#include <QDir>
|
|
||||||
#include <QFileDialog>
|
|
||||||
#include <QMessageBox>
|
|
||||||
#include <QStandardPaths>
|
|
||||||
#include <QPair>
|
|
||||||
#include <optional>
|
|
||||||
#include <string>
|
|
||||||
#include <qsize.h>
|
|
||||||
#include <qsizepolicy.h>
|
|
||||||
#include <chrono>
|
|
||||||
#include <QEvent>
|
|
||||||
#include <iomanip>
|
|
||||||
#include <iostream>
|
|
||||||
#include <sstream>
|
|
||||||
#include "component.hh"
|
|
||||||
#include "cpstream_core.hh"
|
|
||||||
#include "base/globalhelper.hh"
|
#include "base/globalhelper.hh"
|
||||||
|
#include "component.hh"
|
||||||
|
#include "components/charts/heatmap.hh"
|
||||||
|
#include "components/charts/line_chart.hh"
|
||||||
|
#include "components/charts/vector_field.hh"
|
||||||
|
#include "components/ffmsep/presist/presist.hh"
|
||||||
|
#include "components/ffmsep/tactile/tacdec.hh"
|
||||||
|
#include "cpstream_core.hh"
|
||||||
#include "creeper-qt/utility/theme/theme.hh"
|
#include "creeper-qt/utility/theme/theme.hh"
|
||||||
#include "creeper-qt/utility/wrapper/layout.hh"
|
#include "creeper-qt/utility/wrapper/layout.hh"
|
||||||
#include "creeper-qt/utility/wrapper/widget.hh"
|
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||||
#include "components/charts/heatmap.hh"
|
#include <QCoreApplication>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QMetaObject>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPair>
|
||||||
|
#include <QPointF>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QtCore/Qt>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
#include <creeper-qt/layout/flow.hh>
|
#include <creeper-qt/layout/flow.hh>
|
||||||
#include <creeper-qt/layout/linear.hh>
|
#include <creeper-qt/layout/linear.hh>
|
||||||
#include <creeper-qt/utility/material-icon.hh>
|
#include <creeper-qt/utility/material-icon.hh>
|
||||||
#include <creeper-qt/utility/wrapper/mutable-value.hh>
|
#include <creeper-qt/utility/wrapper/mutable-value.hh>
|
||||||
#include <creeper-qt/widget/buttons/icon-button.hh>
|
|
||||||
#include <creeper-qt/widget/buttons/filled-button.hh>
|
#include <creeper-qt/widget/buttons/filled-button.hh>
|
||||||
|
#include <creeper-qt/widget/buttons/icon-button.hh>
|
||||||
#include <creeper-qt/widget/cards/filled-card.hh>
|
#include <creeper-qt/widget/cards/filled-card.hh>
|
||||||
#include <creeper-qt/widget/cards/outlined-card.hh>
|
#include <creeper-qt/widget/cards/outlined-card.hh>
|
||||||
|
#include <creeper-qt/widget/dropdown-menu.hh>
|
||||||
#include <creeper-qt/widget/image.hh>
|
#include <creeper-qt/widget/image.hh>
|
||||||
#include <creeper-qt/widget/shape/wave-circle.hh>
|
#include <creeper-qt/widget/shape/wave-circle.hh>
|
||||||
#include <creeper-qt/widget/sliders.hh>
|
#include <creeper-qt/widget/sliders.hh>
|
||||||
#include <creeper-qt/widget/switch.hh>
|
#include <creeper-qt/widget/switch.hh>
|
||||||
#include <creeper-qt/widget/text-fields.hh>
|
#include <creeper-qt/widget/text-fields.hh>
|
||||||
#include <creeper-qt/widget/text.hh>
|
#include <creeper-qt/widget/text.hh>
|
||||||
#include <creeper-qt/widget/dropdown-menu.hh>
|
#include <cstdint>
|
||||||
#include "components/ffmsep/presist/presist.hh"
|
#include <exception>
|
||||||
#include "components/ffmsep/tactile/tacdec.hh"
|
#include <future>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <iostream>
|
||||||
|
#include <limits>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
|
#include <qdebug.h>
|
||||||
|
#include <qsize.h>
|
||||||
|
#include <qsizepolicy.h>
|
||||||
#include <qstringliteral.h>
|
#include <qstringliteral.h>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
|
||||||
#define DEBUG 0
|
#define DEBUG 0
|
||||||
|
|
||||||
@@ -68,10 +74,12 @@ namespace ibpro = icon_button::pro;
|
|||||||
namespace fbpro = filled_button::pro;
|
namespace fbpro = filled_button::pro;
|
||||||
namespace dmpro = dropdown_menu::pro;
|
namespace dmpro = dropdown_menu::pro;
|
||||||
namespace pwpro = plot_widget::pro;
|
namespace pwpro = plot_widget::pro;
|
||||||
|
namespace vfpro = vector_widget::pro;
|
||||||
|
namespace lcpro = line_widget::pro;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr std::array<std::uint8_t, 14> kSlaveRequestCommand{
|
constexpr std::array<std::uint8_t, 14> kSlaveRequestCommandTemplate{
|
||||||
0x55,
|
0x55,
|
||||||
0xAA,
|
0xAA,
|
||||||
0x09,
|
0x09,
|
||||||
@@ -83,11 +91,61 @@ constexpr std::array<std::uint8_t, 14> kSlaveRequestCommand{
|
|||||||
0x1C,
|
0x1C,
|
||||||
0x00,
|
0x00,
|
||||||
0x00,
|
0x00,
|
||||||
0x18,
|
|
||||||
0x00,
|
0x00,
|
||||||
0x7A
|
0x00,
|
||||||
|
0x00
|
||||||
};
|
};
|
||||||
|
|
||||||
|
std::uint8_t compute_slave_request_crc(const std::uint8_t* data,
|
||||||
|
std::size_t size) {
|
||||||
|
constexpr std::uint8_t kPolynomial = 0x07;
|
||||||
|
constexpr std::uint8_t kInitial = 0x00;
|
||||||
|
constexpr std::uint8_t kXorOut =
|
||||||
|
0x55; // CRC-8/ITU params match device expectation
|
||||||
|
|
||||||
|
std::uint8_t reg = kInitial;
|
||||||
|
for (std::size_t idx = 0; idx < size; ++idx) {
|
||||||
|
reg = static_cast<std::uint8_t>(reg ^ data[idx]);
|
||||||
|
for (int bit = 0; bit < 8; ++bit) {
|
||||||
|
if ((reg & 0x80U) != 0U) {
|
||||||
|
reg = static_cast<std::uint8_t>((reg << 1U) ^ kPolynomial);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
reg = static_cast<std::uint8_t>(reg << 1U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return static_cast<std::uint8_t>(reg ^ kXorOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> make_slave_request_command(const QSize& matrix) {
|
||||||
|
auto command = kSlaveRequestCommandTemplate;
|
||||||
|
|
||||||
|
const int width = std::max(matrix.width(), 1);
|
||||||
|
const int height = std::max(matrix.height(), 1);
|
||||||
|
const std::uint32_t value_count =
|
||||||
|
static_cast<std::uint32_t>(width) * static_cast<std::uint32_t>(height);
|
||||||
|
const std::uint32_t byte_count = value_count * 2U; // 2 bytes per cell
|
||||||
|
const std::uint16_t payload_len =
|
||||||
|
static_cast<std::uint16_t>(std::min<std::uint32_t>(
|
||||||
|
byte_count, std::numeric_limits<std::uint16_t>::max()));
|
||||||
|
|
||||||
|
command[11] = static_cast<std::uint8_t>(payload_len & 0xFFU);
|
||||||
|
command[12] = static_cast<std::uint8_t>((payload_len >> 8U) & 0xFFU);
|
||||||
|
|
||||||
|
std::cout << "command11-12 0x" << std::hex << std::uppercase
|
||||||
|
<< std::setfill('0') << std::setw(2)
|
||||||
|
<< (int)(unsigned char)command[11] << ",0x" << std::setw(2)
|
||||||
|
<< (int)(unsigned char)command[12]
|
||||||
|
<< std::dec // 记得切回10进制,避免影响后续输出
|
||||||
|
<< std::endl;
|
||||||
|
|
||||||
|
command.back() =
|
||||||
|
compute_slave_request_crc(command.data(), command.size() - 1U);
|
||||||
|
|
||||||
|
return std::vector<std::uint8_t>(command.begin(), command.end());
|
||||||
|
}
|
||||||
|
|
||||||
QVector<PointData> make_flat_points(const QSize& size, double value = 0.0) {
|
QVector<PointData> make_flat_points(const QSize& size, double value = 0.0) {
|
||||||
const int width = std::max(size.width(), 1);
|
const int width = std::max(size.width(), 1);
|
||||||
const int height = std::max(size.height(), 1);
|
const int height = std::max(size.height(), 1);
|
||||||
@@ -95,10 +153,8 @@ QVector<PointData> make_flat_points(const QSize& size, double value = 0.0) {
|
|||||||
points.reserve(static_cast<int>(width * height));
|
points.reserve(static_cast<int>(width * height));
|
||||||
for (int y = 0; y < height; ++y) {
|
for (int y = 0; y < height; ++y) {
|
||||||
for (int x = 0; x < width; ++x) {
|
for (int x = 0; x < width; ++x) {
|
||||||
points.append(PointData{
|
points.append(
|
||||||
static_cast<double>(x),
|
PointData{ static_cast<double>(x), static_cast<double>(y), value });
|
||||||
static_cast<double>(y),
|
|
||||||
value });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return points;
|
return points;
|
||||||
@@ -111,23 +167,37 @@ std::once_flag& codec_registration_flag() {
|
|||||||
|
|
||||||
class SensorStreamController: public QObject {
|
class SensorStreamController: public QObject {
|
||||||
public:
|
public:
|
||||||
SensorStreamController(std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data,
|
SensorStreamController(
|
||||||
|
std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data,
|
||||||
std::shared_ptr<MutableValue<QSize>> matrix_context,
|
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::shared_ptr<MutableValue<QVector<QPointF>>> line_series,
|
||||||
std::call_once(codec_registration_flag(), [] {
|
int line_capacity = 240,
|
||||||
|
QObject* parent = nullptr): QObject(parent), heatmap_data_(std::move(heatmap_data)),
|
||||||
|
matrix_context_(std::move(matrix_context)),
|
||||||
|
line_series_(std::move(line_series)),
|
||||||
|
line_series_capacity_(std::max(1, line_capacity)) {
|
||||||
|
std::call_once(codec_registration_flag(),
|
||||||
|
[] {
|
||||||
ffmsep::tactile::register_tactile_codec();
|
ffmsep::tactile::register_tactile_codec();
|
||||||
|
ffmsep::tactile::register_tactile_b_codec();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
~SensorStreamController() override {
|
~SensorStreamController() override { reset_core(); }
|
||||||
reset_core();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool start(const QString& requested_port, std::uint32_t baudrate) {
|
bool start(const QString& requested_port,
|
||||||
|
std::uint32_t baudrate,
|
||||||
|
Tactile_TYPE type) {
|
||||||
|
std::cout << "start" << std::endl;
|
||||||
if (is_connected()) {
|
if (is_connected()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sample_counter_ = 0;
|
||||||
|
if (line_series_) {
|
||||||
|
line_series_->set(QVector<QPointF>{});
|
||||||
|
}
|
||||||
|
|
||||||
const auto ports = ffmsep::CPStreamCore::list_available_ports();
|
const auto ports = ffmsep::CPStreamCore::list_available_ports();
|
||||||
std::string port_utf8;
|
std::string port_utf8;
|
||||||
if (!requested_port.isEmpty()) {
|
if (!requested_port.isEmpty()) {
|
||||||
@@ -137,13 +207,15 @@ class SensorStreamController: public QObject {
|
|||||||
if (it == ports.end()) {
|
if (it == ports.end()) {
|
||||||
if (ports.empty()) {
|
if (ports.empty()) {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
std::cerr << "SensorStreamController: requested port '" << port_utf8 << "' not available and no other ports detected.\n";
|
std::cerr << "SensorStreamController: requested port '" << port_utf8
|
||||||
|
<< "' not available and no other ports detected.\n";
|
||||||
#endif
|
#endif
|
||||||
last_error_ = QString::fromUtf8("未检测到串口");
|
last_error_ = QString::fromUtf8("未检测到串口");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
std::cerr << "SensorStreamController: requested port '" << port_utf8 << "' not available, falling back to first detected port.\n";
|
std::cerr << "SensorStreamController: requested port '" << port_utf8
|
||||||
|
<< "' not available, falling back to first detected port.\n";
|
||||||
#endif
|
#endif
|
||||||
port_utf8 = ports.front().port;
|
port_utf8 = ports.front().port;
|
||||||
}
|
}
|
||||||
@@ -164,30 +236,66 @@ class SensorStreamController: public QObject {
|
|||||||
ffmsep::CPStreamConfig cfg;
|
ffmsep::CPStreamConfig cfg;
|
||||||
cfg.port = port_utf8;
|
cfg.port = port_utf8;
|
||||||
cfg.baudrate = baud;
|
cfg.baudrate = baud;
|
||||||
cfg.codec_id = ffmsep::CPCodecID::Tactile;
|
|
||||||
cfg.read_chunk_size = 256;
|
cfg.read_chunk_size = 256;
|
||||||
cfg.packet_queue_capacity = 128;
|
cfg.packet_queue_capacity = 128;
|
||||||
cfg.frame_queue_capacity = 32;
|
cfg.frame_queue_capacity = 32;
|
||||||
cfg.slave_request_command.assign(kSlaveRequestCommand.begin(), kSlaveRequestCommand.end());
|
const auto format_command =
|
||||||
cfg.slave_request_interval = 3ms;
|
[](const std::vector<std::uint8_t>& data) -> std::string {
|
||||||
|
if (data.empty()) {
|
||||||
|
return "[]";
|
||||||
|
}
|
||||||
|
std::ostringstream 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();
|
||||||
|
};
|
||||||
|
if (type == Tactile_TYPE::PiezoresistiveB) {
|
||||||
|
cfg.codec_id = ffmsep::CPCodecID::PiezoresistiveB;
|
||||||
|
cfg.slave_request_command.clear();
|
||||||
|
cfg.slave_request_interval = 0ms;
|
||||||
|
ffmsep::tactile::set_tactile_expected_payload_bytes(
|
||||||
|
ffmsep::tactile::kPiezoresistiveBValueCount * 2U);
|
||||||
|
std::cout << "[Sensor] using PiezoresistiveB codec" << std::endl;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cfg.codec_id = ffmsep::CPCodecID::Tactile;
|
||||||
|
const auto matrix = matrix_context_ ? matrix_context_->get() : QSize{ 3, 4 };
|
||||||
|
const auto request_command = make_slave_request_command(matrix);
|
||||||
|
const auto points = std::max(1, matrix.width()) * std::max(1, matrix.height());
|
||||||
|
ffmsep::tactile::set_tactile_expected_payload_bytes(
|
||||||
|
static_cast<std::size_t>(points) * 2U);
|
||||||
|
std::cout << "[Sensor] request command="
|
||||||
|
<< format_command(request_command) << std::endl;
|
||||||
|
cfg.slave_request_command = request_command;
|
||||||
|
cfg.slave_request_interval = 10ms;
|
||||||
|
}
|
||||||
|
|
||||||
reset_core();
|
reset_core();
|
||||||
core_ = std::make_unique<ffmsep::CPStreamCore>();
|
core_ = std::make_unique<ffmsep::CPStreamCore>();
|
||||||
|
|
||||||
if (!core_->open(cfg)) {
|
if (!core_->open(cfg)) {
|
||||||
last_error_ = QString::fromStdString(core_->last_error());
|
last_error_ = QString::fromStdString(core_->last_error());
|
||||||
std::cerr << "SensorStreamController: open failed - " << core_->last_error() << "\n";
|
std::cerr << "SensorStreamController: open failed - "
|
||||||
|
<< core_->last_error() << "\n";
|
||||||
reset_core();
|
reset_core();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
core_->set_frame_callback([this](std::shared_ptr<ffmsep::DecodedFrame> frame) {
|
core_->set_frame_callback(
|
||||||
|
[this](std::shared_ptr<ffmsep::DecodedFrame> frame) {
|
||||||
handle_frame(frame);
|
handle_frame(frame);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!core_->start()) {
|
if (!core_->start()) {
|
||||||
last_error_ = QString::fromStdString(core_->last_error());
|
last_error_ = QString::fromStdString(core_->last_error());
|
||||||
std::cerr << "SensorStreamController: start failed - " << core_->last_error() << "\n";
|
std::cerr << "SensorStreamController: start failed - "
|
||||||
|
<< core_->last_error() << "\n";
|
||||||
reset_core();
|
reset_core();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -204,6 +312,10 @@ class SensorStreamController: public QObject {
|
|||||||
if (heatmap_data_ && matrix_context_) {
|
if (heatmap_data_ && matrix_context_) {
|
||||||
heatmap_data_->set(make_flat_points(matrix_context_->get()));
|
heatmap_data_->set(make_flat_points(matrix_context_->get()));
|
||||||
}
|
}
|
||||||
|
if (line_series_) {
|
||||||
|
line_series_->set(QVector<QPointF>{});
|
||||||
|
}
|
||||||
|
sample_counter_ = 0;
|
||||||
connected_ = false;
|
connected_ = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -220,26 +332,24 @@ class SensorStreamController: public QObject {
|
|||||||
if (heatmap_data_ && matrix_context_) {
|
if (heatmap_data_ && matrix_context_) {
|
||||||
heatmap_data_->set(make_flat_points(matrix_context_->get()));
|
heatmap_data_->set(make_flat_points(matrix_context_->get()));
|
||||||
}
|
}
|
||||||
|
if (line_series_) {
|
||||||
|
line_series_->set(QVector<QPointF>{});
|
||||||
|
}
|
||||||
|
sample_counter_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool is_running() const noexcept {
|
[[nodiscard]] bool is_running() const noexcept {
|
||||||
return core_ && core_->is_running();
|
return core_ && core_->is_running();
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool is_connected() const noexcept {
|
[[nodiscard]] bool is_connected() const noexcept { return connected_; }
|
||||||
return connected_;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] QString active_port() const {
|
[[nodiscard]] QString active_port() const { return active_port_; }
|
||||||
return active_port_;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] QString last_error() const {
|
[[nodiscard]] QString last_error() const { return last_error_; }
|
||||||
return last_error_;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::future<ffmsep::persist::WriteResult> export_frames(const QString& path,
|
std::future<ffmsep::persist::WriteResult>
|
||||||
bool clear_after_export) {
|
export_frames(const QString& path, bool clear_after_export) {
|
||||||
if (path.isEmpty()) {
|
if (path.isEmpty()) {
|
||||||
return make_failed_future(path, "export path is empty");
|
return make_failed_future(path, "export path is empty");
|
||||||
}
|
}
|
||||||
@@ -250,7 +360,8 @@ class SensorStreamController: public QObject {
|
|||||||
return make_failed_future(path, "no tactile frames recorded");
|
return make_failed_future(path, "no tactile frames recorded");
|
||||||
}
|
}
|
||||||
const auto normalized = QDir::toNativeSeparators(path);
|
const auto normalized = QDir::toNativeSeparators(path);
|
||||||
return core_->export_recorded_frames(normalized.toStdString(), clear_after_export);
|
return core_->export_recorded_frames(normalized.toStdString(),
|
||||||
|
clear_after_export);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -270,31 +381,15 @@ class SensorStreamController: public QObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static QSize to_qsize(const ffmsep::tactile::MatrixSize& m) {
|
static QSize to_qsize(const ffmsep::tactile::MatrixSize& m) {
|
||||||
return QSize{
|
return QSize{ static_cast<int>(m.long_edge), static_cast<int>(m.short_edge) };
|
||||||
static_cast<int>(m.long_edge),
|
|
||||||
static_cast<int>(m.short_edge)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void handle_frame(std::shared_ptr<ffmsep::DecodedFrame> frame) {
|
void handle_frame(std::shared_ptr<ffmsep::DecodedFrame> frame) {
|
||||||
if (!frame->tactile || frame->tactile_pressures.empty()) {
|
if (!frame) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto pressures = frame->tactile_pressures;
|
auto format_raw = [](const std::vector<std::uint8_t>& data) -> std::string {
|
||||||
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),
|
|
||||||
frame_bytes = std::move(frame_bytes),
|
|
||||||
raw_payload = std::move(raw_payload)]() mutable {
|
|
||||||
const auto format_raw = [](const std::vector<std::uint8_t>& data) -> std::string {
|
|
||||||
if (data.empty()) {
|
if (data.empty()) {
|
||||||
return "[]";
|
return "[]";
|
||||||
}
|
}
|
||||||
@@ -305,42 +400,66 @@ class SensorStreamController: public QObject {
|
|||||||
if (idx != 0U) {
|
if (idx != 0U) {
|
||||||
oss << ' ';
|
oss << ' ';
|
||||||
}
|
}
|
||||||
oss << std::setw(2) << std::hex << static_cast<unsigned int>(data[idx]);
|
oss << std::setw(2) << std::hex
|
||||||
|
<< static_cast<unsigned int>(data[idx]);
|
||||||
}
|
}
|
||||||
oss << ']';
|
oss << ']';
|
||||||
return oss.str();
|
return oss.str();
|
||||||
};
|
};
|
||||||
|
|
||||||
std::cout << "[Sensor] frame=" << format_raw(frame_bytes);
|
auto frame_bytes = frame->frame.data;
|
||||||
std::cout << " payload=" << format_raw(raw_payload);
|
|
||||||
std::cout << " received " << pressures.size() << " pressure values";
|
|
||||||
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;
|
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> raw_payload;
|
||||||
|
if (frame->tactile) {
|
||||||
|
raw_payload = frame->tactile->payload;
|
||||||
|
}
|
||||||
|
else if (frame->id == ffmsep::CPCodecID::PiezoresistiveB) {
|
||||||
|
raw_payload =
|
||||||
|
ffmsep::tactile::extract_piezoresistive_b_payload(frame->frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "[Sensor][raw] frame=" << format_raw(frame_bytes);
|
||||||
|
std::cout << " payload=" << format_raw(raw_payload) << std::endl;
|
||||||
|
|
||||||
|
if (frame->tactile_pressures.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto pressures = frame->tactile_pressures;
|
||||||
|
|
||||||
|
QMetaObject::invokeMethod(
|
||||||
|
this,
|
||||||
|
[this, pressures = std::move(pressures)]() mutable {
|
||||||
auto matrix = matrix_context_->get();
|
auto matrix = matrix_context_->get();
|
||||||
const auto cells_exp = static_cast<std::size_t>(std::max(1, matrix.width()) *
|
const auto cells_exp = static_cast<std::size_t>(
|
||||||
std::max(1, matrix.height()));
|
std::max(1, matrix.width()) * std::max(1, matrix.height()));
|
||||||
if (cells_exp == 0) return;
|
if (cells_exp == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
if (pressures.size() > cells_exp) {
|
if (pressures.size() > cells_exp) {
|
||||||
pressures.resize(cells_exp);
|
pressures.resize(cells_exp);
|
||||||
} else if (pressures.size() < cells_exp) {
|
}
|
||||||
|
else if (pressures.size() < cells_exp) {
|
||||||
pressures.resize(cells_exp, 0);
|
pressures.resize(cells_exp, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double total = 0.0;
|
||||||
|
for (const auto value: pressures) {
|
||||||
|
total += static_cast<double>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line_series_) {
|
||||||
|
auto series = line_series_->get();
|
||||||
|
series.append(QPointF(static_cast<double>(sample_counter_), total));
|
||||||
|
if (line_series_capacity_ > 0
|
||||||
|
&& series.size() > line_series_capacity_) {
|
||||||
|
const int start = series.size() - line_series_capacity_;
|
||||||
|
series = series.mid(start);
|
||||||
|
}
|
||||||
|
++sample_counter_;
|
||||||
|
line_series_->set(std::move(series));
|
||||||
|
}
|
||||||
|
|
||||||
QVector<PointData> points;
|
QVector<PointData> points;
|
||||||
points.reserve(matrix.width() * matrix.height());
|
points.reserve(matrix.width() * matrix.height());
|
||||||
for (int y = 0; y < matrix.height(); ++y) {
|
for (int y = 0; y < matrix.height(); ++y) {
|
||||||
@@ -349,9 +468,9 @@ class SensorStreamController: public QObject {
|
|||||||
if (idx >= static_cast<int>(pressures.size())) {
|
if (idx >= static_cast<int>(pressures.size())) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const auto value = static_cast<double>(pressures[static_cast<std::size_t>(idx)]);
|
const auto value =
|
||||||
points.append(PointData{
|
static_cast<double>(pressures[static_cast<std::size_t>(idx)]);
|
||||||
static_cast<double>(x),
|
points.append(PointData{ static_cast<double>(x),
|
||||||
static_cast<double>(y),
|
static_cast<double>(y),
|
||||||
value });
|
value });
|
||||||
}
|
}
|
||||||
@@ -362,12 +481,14 @@ class SensorStreamController: public QObject {
|
|||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] QSize normalize_matrix(QSize candidate, std::size_t value_count) const {
|
[[nodiscard]] QSize normalize_matrix(QSize candidate,
|
||||||
|
std::size_t value_count) const {
|
||||||
if (value_count == 0U) {
|
if (value_count == 0U) {
|
||||||
return QSize{};
|
return QSize{};
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto adapt_from = [value_count](const QSize& hint) -> std::optional<QSize> {
|
const auto adapt_from =
|
||||||
|
[value_count](const QSize& hint) -> std::optional<QSize> {
|
||||||
if (hint.width() <= 0 && hint.height() <= 0) {
|
if (hint.width() <= 0 && hint.height() <= 0) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
@@ -406,7 +527,8 @@ class SensorStreamController: public QObject {
|
|||||||
return *adjusted;
|
return *adjusted;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto root = static_cast<int>(std::sqrt(static_cast<double>(value_count)));
|
const auto root =
|
||||||
|
static_cast<int>(std::sqrt(static_cast<double>(value_count)));
|
||||||
for (int width = root; width >= 1; --width) {
|
for (int width = root; width >= 1; --width) {
|
||||||
const auto divisor = static_cast<std::size_t>(width);
|
const auto divisor = static_cast<std::size_t>(width);
|
||||||
if (divisor == 0U) {
|
if (divisor == 0U) {
|
||||||
@@ -423,21 +545,19 @@ class SensorStreamController: public QObject {
|
|||||||
|
|
||||||
std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data_;
|
std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data_;
|
||||||
std::shared_ptr<MutableValue<QSize>> matrix_context_;
|
std::shared_ptr<MutableValue<QSize>> matrix_context_;
|
||||||
|
std::shared_ptr<MutableValue<QVector<QPointF>>> line_series_;
|
||||||
std::unique_ptr<ffmsep::CPStreamCore> core_;
|
std::unique_ptr<ffmsep::CPStreamCore> core_;
|
||||||
QString active_port_;
|
QString active_port_;
|
||||||
QString last_error_;
|
QString last_error_;
|
||||||
|
std::uint64_t sample_counter_ = 0;
|
||||||
|
int line_series_capacity_ = 240;
|
||||||
bool connected_ = false;
|
bool connected_ = false;
|
||||||
|
|
||||||
static std::future<ffmsep::persist::WriteResult> make_failed_future(
|
static std::future<ffmsep::persist::WriteResult>
|
||||||
const QString& path,
|
make_failed_future(const QString& path, std::string message) {
|
||||||
std::string message) {
|
|
||||||
std::promise<ffmsep::persist::WriteResult> promise;
|
std::promise<ffmsep::persist::WriteResult> promise;
|
||||||
auto future = promise.get_future();
|
auto future = promise.get_future();
|
||||||
ffmsep::persist::WriteResult result{
|
ffmsep::persist::WriteResult result{ false, std::move(message), path.toStdString() };
|
||||||
false,
|
|
||||||
std::move(message),
|
|
||||||
path.toStdString()
|
|
||||||
};
|
|
||||||
promise.set_value(std::move(result));
|
promise.set_value(std::move(result));
|
||||||
return future;
|
return future;
|
||||||
}
|
}
|
||||||
@@ -445,19 +565,26 @@ class SensorStreamController: public QObject {
|
|||||||
|
|
||||||
struct SensorUiState {
|
struct SensorUiState {
|
||||||
std::shared_ptr<MutableValue<QString>> link_icon =
|
std::shared_ptr<MutableValue<QString>> link_icon =
|
||||||
std::make_shared<MutableValue<QString>>(QString::fromLatin1(material::icon::kAddLink));
|
std::make_shared<MutableValue<QString>>(
|
||||||
|
QString::fromLatin1(material::icon::kAddLink));
|
||||||
std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data =
|
std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data =
|
||||||
std::make_shared<MutableValue<QVector<PointData>>>();
|
std::make_shared<MutableValue<QVector<PointData>>>();
|
||||||
std::shared_ptr<MutableValue<QSize>> heatmap_matrix =
|
std::shared_ptr<MutableValue<QSize>> heatmap_matrix =
|
||||||
std::make_shared<MutableValue<QSize>>();
|
std::make_shared<MutableValue<QSize>>();
|
||||||
std::shared_ptr<MutableValue<QPair<int, int>>> heatmap_range =
|
std::shared_ptr<MutableValue<QPair<int, int>>> heatmap_range =
|
||||||
std::make_shared<MutableValue<QPair<int, int>>>(QPair<int, int>{ 0, 300 });
|
std::make_shared<MutableValue<QPair<int, int>>>(QPair<int, int>{ 0, 300 });
|
||||||
|
std::shared_ptr<MutableValue<QVector<QPointF>>> line_series =
|
||||||
|
std::make_shared<MutableValue<QVector<QPointF>>>();
|
||||||
|
int line_series_capacity = 240;
|
||||||
std::shared_ptr<MutableValue<QStringList>> port_items =
|
std::shared_ptr<MutableValue<QStringList>> port_items =
|
||||||
std::make_shared<MutableValue<QStringList>>();
|
std::make_shared<MutableValue<QStringList>>();
|
||||||
std::shared_ptr<MutableValue<QStringList>> profile_items =
|
std::shared_ptr<MutableValue<QStringList>> profile_items =
|
||||||
std::make_shared<MutableValue<QStringList>>();
|
std::make_shared<MutableValue<QStringList>>();
|
||||||
|
dropdown_menu::internal::DropdownMenu* port_dropdown = nullptr;
|
||||||
|
dropdown_menu::internal::DropdownMenu* profile_dropdown = nullptr;
|
||||||
QString selected_port;
|
QString selected_port;
|
||||||
QString selected_profile;
|
QString selected_profile;
|
||||||
|
Tactile_TYPE selected_type = Tactile_TYPE::PiezoresistiveA;
|
||||||
std::uint32_t selected_baud = 115200;
|
std::uint32_t selected_baud = 115200;
|
||||||
std::unique_ptr<SensorStreamController> controller;
|
std::unique_ptr<SensorStreamController> controller;
|
||||||
|
|
||||||
@@ -490,18 +617,21 @@ struct SensorUiState {
|
|||||||
if (!profiles.empty()) {
|
if (!profiles.empty()) {
|
||||||
const auto size = QSize{ std::max(1, profiles.front().matrix_width),
|
const auto size = QSize{ std::max(1, profiles.front().matrix_width),
|
||||||
std::max(1, profiles.front().matrix_height) };
|
std::max(1, profiles.front().matrix_height) };
|
||||||
selected_baud = profiles.front().baud_rate == 0 ? 115200U : static_cast<std::uint32_t>(profiles.front().baud_rate);
|
selected_type = profiles.front().type;
|
||||||
|
selected_baud =
|
||||||
|
profiles.front().baud_rate == 0 ? 115200U : static_cast<std::uint32_t>(profiles.front().baud_rate);
|
||||||
heatmap_matrix->set_silent(size);
|
heatmap_matrix->set_silent(size);
|
||||||
heatmap_data->set_silent(make_flat_points(size));
|
heatmap_data->set_silent(make_flat_points(size));
|
||||||
const int range_min = profiles.front().range_left;
|
const int range_min = profiles.front().range_left;
|
||||||
const int range_max = (profiles.front().range_right == profiles.front().range_left)
|
const int range_max =
|
||||||
? profiles.front().range_left + 1
|
(profiles.front().range_right == profiles.front().range_left) ? profiles.front().range_left + 1 : profiles.front().range_right;
|
||||||
: profiles.front().range_right;
|
|
||||||
heatmap_range->set_silent(QPair<int, int>{ range_min, range_max });
|
heatmap_range->set_silent(QPair<int, int>{ range_min, range_max });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
controller = std::make_unique<SensorStreamController>(heatmap_data, heatmap_matrix);
|
controller =
|
||||||
|
std::make_unique<SensorStreamController>(
|
||||||
|
heatmap_data, heatmap_matrix, line_series, line_series_capacity);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -517,25 +647,18 @@ static void RefreshPortsForView(SensorUiState& sensor) {
|
|||||||
for (const auto& info: ports) {
|
for (const auto& info: ports) {
|
||||||
ports_list.emplace_back(QString::fromStdString(info.port));
|
ports_list.emplace_back(QString::fromStdString(info.port));
|
||||||
}
|
}
|
||||||
if (!sensor.selected_port.isEmpty()) {
|
if (sensor.selected_port.isEmpty()) {
|
||||||
const bool exists = ports_list.contains(sensor.selected_port);
|
if (!ports_list.isEmpty()) {
|
||||||
if (!exists) {
|
|
||||||
sensor.selected_port = ports_list.isEmpty() ? QString{} : ports_list.front();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (!ports_list.isEmpty()) {
|
|
||||||
sensor.selected_port = ports_list.front();
|
sensor.selected_port = ports_list.front();
|
||||||
}
|
}
|
||||||
|
|
||||||
sensor.port_items->set(std::move(ports_list));
|
sensor.port_items->set(std::move(ports_list));
|
||||||
RefreshProfilesForView();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PortHoverRefreshFilter final: public QObject {
|
class PortHoverRefreshFilter final: public QObject {
|
||||||
public:
|
public:
|
||||||
explicit PortHoverRefreshFilter(SensorUiState& sensor, QObject* parent = nullptr)
|
explicit PortHoverRefreshFilter(SensorUiState& sensor,
|
||||||
: QObject(parent)
|
QObject* parent = nullptr): QObject(parent), sensor_(sensor) {}
|
||||||
, sensor_(sensor) { }
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool eventFilter(QObject* watched, QEvent* event) override {
|
bool eventFilter(QObject* watched, QEvent* event) override {
|
||||||
@@ -564,18 +687,22 @@ void RefreshProfilesForView() {
|
|||||||
if (!sensor.selected_profile.isEmpty()) {
|
if (!sensor.selected_profile.isEmpty()) {
|
||||||
const bool exists = profile_list.contains(sensor.selected_profile);
|
const bool exists = profile_list.contains(sensor.selected_profile);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
sensor.selected_profile = profile_list.isEmpty() ? QString{} : profile_list.front();
|
sensor.selected_profile =
|
||||||
|
profile_list.isEmpty() ? QString{} : profile_list.front();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (!profile_list.isEmpty()) {
|
else if (!profile_list.isEmpty()) {
|
||||||
sensor.selected_profile = profile_list.front();
|
sensor.selected_profile = profile_list.front();
|
||||||
}
|
}
|
||||||
if (!sensor.selected_profile.isEmpty()) {
|
if (!sensor.selected_profile.isEmpty()) {
|
||||||
const auto it = std::find_if(profiles.begin(), profiles.end(),
|
const auto it = std::find_if(profiles.begin(), profiles.end(), [&](const ConfigProfile& p) {
|
||||||
[&](const ConfigProfile& p) { return p.name == sensor.selected_profile; });
|
return p.name == sensor.selected_profile;
|
||||||
|
});
|
||||||
if (it != profiles.end()) {
|
if (it != profiles.end()) {
|
||||||
sensor.selected_baud = it->baud_rate == 0 ? 115200U : static_cast<std::uint32_t>(it->baud_rate);
|
sensor.selected_baud = it->baud_rate == 0 ? 115200U : static_cast<std::uint32_t>(it->baud_rate);
|
||||||
const auto size = QSize { std::max(1, it->matrix_width), std::max(1, it->matrix_height) };
|
const auto size =
|
||||||
|
QSize{ std::max(1, it->matrix_width), std::max(1, it->matrix_height) };
|
||||||
|
sensor.selected_type = it->type;
|
||||||
sensor.heatmap_matrix->set(size);
|
sensor.heatmap_matrix->set(size);
|
||||||
sensor.heatmap_data->set(make_flat_points(size));
|
sensor.heatmap_data->set(make_flat_points(size));
|
||||||
const int range_min = it->range_left;
|
const int range_min = it->range_left;
|
||||||
@@ -601,7 +728,8 @@ static auto ComConfigComponent(ThemeManager& manager) {
|
|||||||
const auto row = new Row{
|
const auto row = new Row{
|
||||||
// lnpro::Item<FilledTextField> {
|
// lnpro::Item<FilledTextField> {
|
||||||
// text_field::pro::ThemeManager {manager},
|
// text_field::pro::ThemeManager {manager},
|
||||||
// text_field::pro::LeadingIcon {material::icon::kSearch, material::regular::font},
|
// text_field::pro::LeadingIcon {material::icon::kSearch,
|
||||||
|
// material::regular::font},
|
||||||
// MutableForward {
|
// MutableForward {
|
||||||
// text_field::pro::LabelText {},
|
// text_field::pro::LabelText {},
|
||||||
// slogen_context,
|
// slogen_context,
|
||||||
@@ -609,7 +737,8 @@ static auto ComConfigComponent(ThemeManager& manager) {
|
|||||||
// },
|
// },
|
||||||
lnpro::Item<FilledDropdownMenu>{
|
lnpro::Item<FilledDropdownMenu>{
|
||||||
dmpro::ThemeManager{ manager },
|
dmpro::ThemeManager{ manager },
|
||||||
dmpro::LeadingIcon{ material::icon::kArrowDropDown, material::regular::font },
|
dmpro::LeadingIcon{ material::icon::kArrowDropDown,
|
||||||
|
material::regular::font },
|
||||||
dmpro::TextChanged{ [sensor_ptr = &sensor](QString text) {
|
dmpro::TextChanged{ [sensor_ptr = &sensor](QString text) {
|
||||||
// const auto text = self.currentText();
|
// const auto text = self.currentText();
|
||||||
if (!text.isEmpty()) {
|
if (!text.isEmpty()) {
|
||||||
@@ -622,30 +751,37 @@ static auto ComConfigComponent(ThemeManager& manager) {
|
|||||||
sensor.port_items,
|
sensor.port_items,
|
||||||
},
|
},
|
||||||
dmpro::Apply{ [&sensor](dropdown_menu::internal::DropdownMenu& self) {
|
dmpro::Apply{ [&sensor](dropdown_menu::internal::DropdownMenu& self) {
|
||||||
|
sensor.port_dropdown = &self;
|
||||||
if (!self.property("portHoverRefreshAttached").toBool()) {
|
if (!self.property("portHoverRefreshAttached").toBool()) {
|
||||||
self.installEventFilter(new PortHoverRefreshFilter(sensor, &self));
|
self.installEventFilter(
|
||||||
|
new PortHoverRefreshFilter(sensor, &self));
|
||||||
self.setProperty("portHoverRefreshAttached", true);
|
self.setProperty("portHoverRefreshAttached", true);
|
||||||
}
|
}
|
||||||
} },
|
} },
|
||||||
},
|
},
|
||||||
lnpro::Item<FilledDropdownMenu>{
|
lnpro::Item<FilledDropdownMenu>{
|
||||||
dmpro::ThemeManager{ manager },
|
dmpro::ThemeManager{ manager },
|
||||||
dmpro::LeadingIcon{ material::icon::kArrowDropDown, material::regular::font },
|
dmpro::LeadingIcon{ material::icon::kArrowDropDown,
|
||||||
|
material::regular::font },
|
||||||
dmpro::TextChanged{ [sensor_ptr = &sensor](QString text) {
|
dmpro::TextChanged{ [sensor_ptr = &sensor](QString text) {
|
||||||
if (!text.isEmpty()) {
|
if (!text.isEmpty()) {
|
||||||
sensor_ptr->selected_profile = text;
|
sensor_ptr->selected_profile = text;
|
||||||
const auto& profiles = GlobalHelper::instance().get_all_profile();
|
const auto& profiles = GlobalHelper::instance().get_all_profile();
|
||||||
const auto it = std::find_if(profiles.begin(), profiles.end(),
|
const auto it = std::find_if(
|
||||||
[&text](const ConfigProfile& p) { return p.name == text; });
|
profiles.begin(), profiles.end(), [&text](const ConfigProfile& p) { return p.name == text; });
|
||||||
if (it != profiles.end()) {
|
if (it != profiles.end()) {
|
||||||
const auto baud = it->baud_rate == 0 ? 115200U : static_cast<std::uint32_t>(it->baud_rate);
|
const auto baud =
|
||||||
|
it->baud_rate == 0 ? 115200U : static_cast<std::uint32_t>(it->baud_rate);
|
||||||
sensor_ptr->selected_baud = baud;
|
sensor_ptr->selected_baud = baud;
|
||||||
const auto size = QSize { std::max(1, it->matrix_width), std::max(1, it->matrix_height) };
|
sensor_ptr->selected_type = it->type;
|
||||||
|
const auto size = QSize{ std::max(1, it->matrix_width),
|
||||||
|
std::max(1, it->matrix_height) };
|
||||||
sensor_ptr->heatmap_matrix->set(size);
|
sensor_ptr->heatmap_matrix->set(size);
|
||||||
sensor_ptr->heatmap_data->set(make_flat_points(size));
|
sensor_ptr->heatmap_data->set(make_flat_points(size));
|
||||||
const int range_min = it->range_left;
|
const int range_min = it->range_left;
|
||||||
const int range_max = (it->range_right == it->range_left) ? it->range_left + 1 : it->range_right;
|
const int range_max = (it->range_right == it->range_left) ? it->range_left + 1 : it->range_right;
|
||||||
sensor_ptr->heatmap_range->set(QPair<int, int>{range_min, range_max});
|
sensor_ptr->heatmap_range->set(
|
||||||
|
QPair<int, int>{ range_min, range_max });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} },
|
} },
|
||||||
@@ -654,14 +790,13 @@ static auto ComConfigComponent(ThemeManager& manager) {
|
|||||||
dmpro::Items{},
|
dmpro::Items{},
|
||||||
sensor.profile_items,
|
sensor.profile_items,
|
||||||
},
|
},
|
||||||
|
dmpro::Apply{ [&sensor](dropdown_menu::internal::DropdownMenu& self) {
|
||||||
|
sensor.profile_dropdown = &self;
|
||||||
|
} },
|
||||||
},
|
},
|
||||||
lnpro::SpacingItem{ 20 },
|
lnpro::SpacingItem{ 20 },
|
||||||
lnpro::Item<IconButton>{
|
lnpro::Item<IconButton>{
|
||||||
ibpro::ThemeManager{ manager },
|
ibpro::ThemeManager{ manager }, ibpro::FixedSize{ 40, 40 }, ibpro::Color{ IconButton::Color::TONAL }, ibpro::Font{ material::kRegularExtraSmallFont }, MutableForward{
|
||||||
ibpro::FixedSize{ 40, 40 },
|
|
||||||
ibpro::Color{ IconButton::Color::TONAL },
|
|
||||||
ibpro::Font{ material::kRegularExtraSmallFont },
|
|
||||||
MutableForward{
|
|
||||||
icon_button::pro::FontIcon{},
|
icon_button::pro::FontIcon{},
|
||||||
link_icon_context,
|
link_icon_context,
|
||||||
},
|
},
|
||||||
@@ -672,14 +807,17 @@ static auto ComConfigComponent(ThemeManager& manager) {
|
|||||||
}
|
}
|
||||||
if (sensor.controller->is_connected()) {
|
if (sensor.controller->is_connected()) {
|
||||||
sensor.controller->stop();
|
sensor.controller->stop();
|
||||||
link_icon_context->set(QString::fromLatin1(material::icon::kAddLink));
|
link_icon_context->set(
|
||||||
|
QString::fromLatin1(material::icon::kAddLink));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const auto port = sensor.selected_port;
|
const auto port = sensor.selected_port;
|
||||||
const auto baud = sensor.selected_baud == 0U ? 115200U : sensor.selected_baud;
|
const auto baud =
|
||||||
if (sensor.controller->start(port, baud)) {
|
sensor.selected_baud == 0U ? 115200U : sensor.selected_baud;
|
||||||
|
if (sensor.controller->start(port, baud, sensor.selected_type)) {
|
||||||
sensor.selected_port = sensor.controller->active_port();
|
sensor.selected_port = sensor.controller->active_port();
|
||||||
link_icon_context->set(QString::fromLatin1(material::icon::kLinkOff));
|
link_icon_context->set(
|
||||||
|
QString::fromLatin1(material::icon::kLinkOff));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
std::cerr << "Failed to start sensor stream: "
|
std::cerr << "Failed to start sensor stream: "
|
||||||
@@ -688,32 +826,39 @@ static auto ComConfigComponent(ThemeManager& manager) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} } },
|
} } },
|
||||||
lnpro::Item<FilledButton>{
|
lnpro::Item<IconButton>{ ibpro::ThemeManager{ manager }, ibpro::FixedSize{ 40, 40 }, ibpro::Color{ IconButton::Color::TONAL }, ibpro::Font{ material::kRegularExtraSmallFont }, ibpro::FontIcon{ "cleaning_services" }, ibpro::Clickable{ [&sensor] {
|
||||||
fbpro::ThemeManager{ manager },
|
// Clear current selections (keep items) so next hover triggers a fresh scan.
|
||||||
fbpro::FixedSize{ 40, 40 },
|
sensor.selected_port.clear();
|
||||||
fbpro::Radius{ 8.0 },
|
sensor.selected_profile.clear();
|
||||||
|
if (sensor.port_dropdown) {
|
||||||
|
sensor.port_dropdown->setCurrentIndex(-1);
|
||||||
|
}
|
||||||
|
if (sensor.profile_dropdown) {
|
||||||
|
sensor.profile_dropdown->setCurrentIndex(-1);
|
||||||
|
}
|
||||||
|
} } },
|
||||||
|
lnpro::Item<FilledButton>{ fbpro::ThemeManager{ manager }, fbpro::FixedSize{ 40, 40 }, fbpro::Radius{ 8.0 },
|
||||||
// fbpro::Color { IconButton::Color::TONAL },
|
// fbpro::Color { IconButton::Color::TONAL },
|
||||||
fbpro::Font{ material::kRegularExtraSmallFont },
|
fbpro::Font{ material::kRegularExtraSmallFont },
|
||||||
fbpro::Text{ "drive_file_move" },
|
fbpro::Text{ "drive_file_move" },
|
||||||
fbpro::Clickable{ [&sensor] {
|
fbpro::Clickable{ [&sensor] {
|
||||||
auto* controller = sensor.controller.get();
|
auto* controller = sensor.controller.get();
|
||||||
if (!controller) {
|
if (!controller) {
|
||||||
QMessageBox::warning(nullptr,
|
QMessageBox::warning(nullptr, QStringLiteral("导出失败"), QStringLiteral("当前串流尚未初始化。"));
|
||||||
QStringLiteral("导出失败"),
|
|
||||||
QStringLiteral("当前串流尚未初始化。"));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto documents = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
|
const auto documents = QStandardPaths::writableLocation(
|
||||||
const auto timestamp = QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd_HHmmss"));
|
QStandardPaths::DocumentsLocation);
|
||||||
QString suggested_name = QStringLiteral("touchsensor_%1.json").arg(timestamp);
|
const auto timestamp = QDateTime::currentDateTime().toString(
|
||||||
QString initial_path = documents.isEmpty() ? suggested_name : QDir(documents).filePath(suggested_name);
|
QStringLiteral("yyyyMMdd_HHmmss"));
|
||||||
|
QString suggested_name =
|
||||||
|
QStringLiteral("touchsensor_%1.csv").arg(timestamp);
|
||||||
|
QString initial_path =
|
||||||
|
documents.isEmpty() ? suggested_name : QDir(documents).filePath(suggested_name);
|
||||||
|
|
||||||
const QString chosen_path = QFileDialog::getSaveFileName(
|
const QString chosen_path = QFileDialog::getSaveFileName(
|
||||||
nullptr,
|
nullptr, QStringLiteral("导出触觉帧"), initial_path, QStringLiteral("CSV 文件 (*.csv)"));
|
||||||
QStringLiteral("导出触觉帧"),
|
|
||||||
initial_path,
|
|
||||||
QStringLiteral("JSON 文件 (*.json)"));
|
|
||||||
if (chosen_path.isEmpty()) {
|
if (chosen_path.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -734,24 +879,24 @@ static auto ComConfigComponent(ThemeManager& manager) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (auto* app = QCoreApplication::instance()) {
|
if (auto* app = QCoreApplication::instance()) {
|
||||||
QMetaObject::invokeMethod(app, [res = std::move(result)]() {
|
QMetaObject::invokeMethod(
|
||||||
|
app,
|
||||||
|
[res = std::move(result)]() {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
QMessageBox::information(
|
QMessageBox::information(
|
||||||
nullptr,
|
nullptr, QStringLiteral("导出成功"), QStringLiteral("触觉帧已导出至:\n%1").arg(QString::fromStdString(res.path)));
|
||||||
QStringLiteral("导出成功"),
|
}
|
||||||
QStringLiteral("触觉帧已导出至:\n%1")
|
else {
|
||||||
.arg(QString::fromStdString(res.path)));
|
|
||||||
} else {
|
|
||||||
const auto error = QString::fromStdString(
|
const auto error = QString::fromStdString(
|
||||||
res.error.empty() ? std::string{ "unknown error" } : res.error);
|
res.error.empty() ? std::string{ "unknown error" } : res.error);
|
||||||
const auto target = QString::fromStdString(res.path);
|
const auto target = QString::fromStdString(res.path);
|
||||||
const auto message = target.isEmpty()
|
const auto message =
|
||||||
? QStringLiteral("原因:%1").arg(error)
|
target.isEmpty() ? QStringLiteral("原因:%1").arg(error) : QStringLiteral("原因:%1\n目标:%2").arg(error, target);
|
||||||
: QStringLiteral("原因:%1\n目标:%2").arg(error, target);
|
QMessageBox::warning(
|
||||||
QMessageBox::warning(nullptr,
|
nullptr, QStringLiteral("导出失败"), message);
|
||||||
QStringLiteral("导出失败"),
|
}
|
||||||
message);
|
},
|
||||||
} }, Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
@@ -762,7 +907,8 @@ static auto ComConfigComponent(ThemeManager& manager) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static auto DisplayComponent(ThemeManager& manager, int /*index*/ = 0) noexcept {
|
static auto DisplayComponent(ThemeManager& manager,
|
||||||
|
int /*index*/ = 0) noexcept {
|
||||||
auto& sensor = sensor_state();
|
auto& sensor = sensor_state();
|
||||||
const auto row = new Row{
|
const auto row = new Row{
|
||||||
lnpro::Item<HeatMapPlot>{
|
lnpro::Item<HeatMapPlot>{
|
||||||
@@ -774,17 +920,16 @@ static auto DisplayComponent(ThemeManager& manager, int /*index*/ = 0) noexcept
|
|||||||
plot_widget::pro::PlotData{},
|
plot_widget::pro::PlotData{},
|
||||||
sensor.heatmap_data,
|
sensor.heatmap_data,
|
||||||
},
|
},
|
||||||
pwpro::MatrixSize{
|
pwpro::MatrixSize{ sensor.heatmap_matrix->get() },
|
||||||
sensor.heatmap_matrix->get() },
|
MutableTransform{ [](auto& widget, const QSize& size) {
|
||||||
MutableTransform{
|
|
||||||
[](auto& widget, const QSize& size) {
|
|
||||||
pwpro::MatrixSize{ size }.apply(widget);
|
pwpro::MatrixSize{ size }.apply(widget);
|
||||||
},
|
},
|
||||||
sensor.heatmap_matrix },
|
sensor.heatmap_matrix },
|
||||||
MutableTransform{
|
MutableTransform{ [](auto& widget, const QPair<int, int>& range) {
|
||||||
[](auto& widget, const QPair<int, int>& range) {
|
const double min =
|
||||||
const double min = static_cast<double>(range.first);
|
static_cast<double>(range.first);
|
||||||
const double max = static_cast<double>(range.second);
|
const double max =
|
||||||
|
static_cast<double>(range.second);
|
||||||
widget.set_color_gradient_range(min, max);
|
widget.set_color_gradient_range(min, max);
|
||||||
},
|
},
|
||||||
sensor.heatmap_range },
|
sensor.heatmap_range },
|
||||||
@@ -795,6 +940,50 @@ static auto DisplayComponent(ThemeManager& manager, int /*index*/ = 0) noexcept
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static auto DisplayVectorComponent(ThemeManager& manager) noexcept {
|
||||||
|
auto& sensor = sensor_state();
|
||||||
|
const auto row = new Row{
|
||||||
|
lnpro::Item<VectorFieldPlot>{
|
||||||
|
vfpro::SizePolicy{
|
||||||
|
QSizePolicy::Expanding,
|
||||||
|
},
|
||||||
|
vfpro::ThemeManager{ manager },
|
||||||
|
MutableForward{
|
||||||
|
vfpro::PlotData{},
|
||||||
|
sensor.heatmap_data,
|
||||||
|
},
|
||||||
|
vfpro::MatrixSize{ sensor.heatmap_matrix->get() },
|
||||||
|
MutableTransform{ [](auto& widget, const QSize& size) {
|
||||||
|
vfpro::MatrixSize{ size }.apply(widget);
|
||||||
|
},
|
||||||
|
sensor.heatmap_matrix },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return new Widget{
|
||||||
|
widget::pro::Layout{ row },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto DisplayLineComponent(ThemeManager& manager) noexcept {
|
||||||
|
auto& sensor = sensor_state();
|
||||||
|
const auto row = new Row{
|
||||||
|
lnpro::Item<SumLinePlot>{
|
||||||
|
lcpro::SizePolicy{
|
||||||
|
QSizePolicy::Expanding,
|
||||||
|
},
|
||||||
|
lcpro::ThemeManager{ manager },
|
||||||
|
lcpro::MaxPoints{ sensor.line_series_capacity },
|
||||||
|
MutableForward{
|
||||||
|
lcpro::PlotData{},
|
||||||
|
sensor.line_series,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return new Widget{
|
||||||
|
widget::pro::Layout{ row },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
auto ViewComponent(ViewComponentState& state) noexcept -> raw_pointer<QWidget> {
|
auto ViewComponent(ViewComponentState& state) noexcept -> raw_pointer<QWidget> {
|
||||||
return new FilledCard{
|
return new FilledCard{
|
||||||
capro::ThemeManager{ state.manager },
|
capro::ThemeManager{ state.manager },
|
||||||
@@ -811,8 +1000,13 @@ auto ViewComponent(ViewComponentState& state) noexcept -> raw_pointer<QWidget> {
|
|||||||
lnpro::Item{
|
lnpro::Item{
|
||||||
DisplayComponent(state.manager),
|
DisplayComponent(state.manager),
|
||||||
},
|
},
|
||||||
|
lnpro::Item<Col>{
|
||||||
lnpro::Item{
|
lnpro::Item{
|
||||||
DisplayComponent(state.manager),
|
DisplayVectorComponent(state.manager),
|
||||||
|
},
|
||||||
|
lnpro::Item{
|
||||||
|
DisplayLineComponent(state.manager),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ int main(int argc, char** argv) {
|
|||||||
0x55, 0xAA, 0x09, 0x00, 0x34, 0x00, 0xFB,
|
0x55, 0xAA, 0x09, 0x00, 0x34, 0x00, 0xFB,
|
||||||
0x00, 0x1C, 0x00, 0x00, 0x18, 0x00, 0x7A
|
0x00, 0x1C, 0x00, 0x00, 0x18, 0x00, 0x7A
|
||||||
};
|
};
|
||||||
cfg.slave_request_interval = 3ms;
|
cfg.slave_request_interval = 10ms;
|
||||||
|
|
||||||
ffmsep::CPStreamCore core(cfg);
|
ffmsep::CPStreamCore core(cfg);
|
||||||
if (!core.open()) {
|
if (!core.open()) {
|
||||||
|
|||||||
6
main.cc
6
main.cc
@@ -15,7 +15,7 @@
|
|||||||
#include <creeper-qt/widget/main-window.hh>
|
#include <creeper-qt/widget/main-window.hh>
|
||||||
#include <creeper-qt/layout/stacked.hh>
|
#include <creeper-qt/layout/stacked.hh>
|
||||||
#include <qfontdatabase.h>
|
#include <qfontdatabase.h>
|
||||||
|
#include <iostream>
|
||||||
using namespace creeper;
|
using namespace creeper;
|
||||||
|
|
||||||
namespace lnpro = linear::pro;
|
namespace lnpro = linear::pro;
|
||||||
@@ -29,7 +29,7 @@ auto main(int argc, char *argv[]) -> int {
|
|||||||
app::pro::Attribute {Qt::AA_UseHighDpiPixmaps},
|
app::pro::Attribute {Qt::AA_UseHighDpiPixmaps},
|
||||||
app::pro::Complete {argc, argv},
|
app::pro::Complete {argc, argv},
|
||||||
};
|
};
|
||||||
|
std::cout << "========begin========" << std::endl;
|
||||||
auto stack_index = std::make_shared<MutableValue<int>>();
|
auto stack_index = std::make_shared<MutableValue<int>>();
|
||||||
stack_index->set_silent(0);
|
stack_index->set_silent(0);
|
||||||
|
|
||||||
@@ -38,6 +38,8 @@ auto main(int argc, char *argv[]) -> int {
|
|||||||
auto nav_component_state = NavComponentState {
|
auto nav_component_state = NavComponentState {
|
||||||
.manager = manager,
|
.manager = manager,
|
||||||
.switch_callback = [&](int index, const auto& name) {
|
.switch_callback = [&](int index, const auto& name) {
|
||||||
|
|
||||||
|
|
||||||
qDebug() << "switch_callback index: " << index;
|
qDebug() << "switch_callback index: " << index;
|
||||||
},
|
},
|
||||||
.buttons_context = {
|
.buttons_context = {
|
||||||
|
|||||||
Reference in New Issue
Block a user