Merge branch 'dev'
# Conflicts: # components/ffmsep/cpstream_core.hh # components/view.cc
This commit is contained in:
@@ -5,12 +5,12 @@
|
||||
#ifndef TOUCHSENSOR_HEATMAP_H
|
||||
#define TOUCHSENSOR_HEATMAP_H
|
||||
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
||||
#include "modern-qt/utility/wrapper/property.hh"
|
||||
#include "creeper-qt/utility/theme/theme.hh"
|
||||
#include "creeper-qt/utility/wrapper/common.hh"
|
||||
#include "creeper-qt/utility/wrapper/pimpl.hh"
|
||||
#include "creeper-qt/utility/wrapper/property.hh"
|
||||
#include "qcustomplot/qcustomplot.h"
|
||||
#include "modern-qt/utility/wrapper/widget.hh"
|
||||
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||
#include <concepts>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qvector.h>
|
||||
@@ -82,10 +82,13 @@ namespace plot_widget::pro {
|
||||
}
|
||||
};
|
||||
|
||||
using Data = common::pro::Vector<Token, PointData,
|
||||
[](auto& self, const auto& data) {
|
||||
self.set_data(data);
|
||||
}>;
|
||||
// using Data = common::pro::Vector<Token, PointData,
|
||||
// [](auto& self, const auto& data) {
|
||||
// self.set_data(data);
|
||||
// }>;
|
||||
using Data = DerivedProp<Token, QVector<QString>, [](auto& self, const auto& data) {
|
||||
self.set_data(data);
|
||||
}>;
|
||||
|
||||
struct ColorRange : Token {
|
||||
double min;
|
||||
@@ -107,11 +110,15 @@ namespace plot_widget::pro {
|
||||
template<class PlotWidget>
|
||||
concept trait = std::derived_from<PlotWidget, Token>;
|
||||
|
||||
using PlotData = common::pro::Vector<Token, PointData, [](auto& self, const auto& vec) {
|
||||
self.set_data(vec);
|
||||
}>;
|
||||
// using PlotData = common::pro::Vector<Token, PointData, [](auto& self, const auto& vec) {
|
||||
// self.set_data(vec);
|
||||
// }>;
|
||||
using PlotData = DerivedProp<Token, QVector<PointData>, [](auto& self, const auto& vec){self.set_data(vec);}>;
|
||||
|
||||
using DataRange = common::pro::Array<Token, int, 2, [](auto& self, const auto& arr) {
|
||||
// using DataRange = common::pro::Array<Token, int, 2, [](auto& self, const auto& arr) {
|
||||
// self.set_color_gradient_range(arr[0], arr[1]);
|
||||
// }>;
|
||||
using DataRange = DerivedProp<Token, std::array<int, 2>, [](auto& self, const auto& arr) {
|
||||
self.set_color_gradient_range(arr[0], arr[1]);
|
||||
}>;
|
||||
|
||||
|
||||
@@ -1,177 +1,315 @@
|
||||
//
|
||||
// Created by Lenn on 2025/10/17.
|
||||
//
|
||||
|
||||
#ifndef TOUCHSENSOR_HEATMAP_IMPL_HH
|
||||
#define TOUCHSENSOR_HEATMAP_IMPL_HH
|
||||
|
||||
#include "heatmap.hh"
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/widget/sliders.hh"
|
||||
#include <memory>
|
||||
#include <qcolor.h>
|
||||
#include <qdebug.h>
|
||||
using namespace creeper::plot_widget::internal;
|
||||
|
||||
struct BasicPlot::Impl {
|
||||
explicit Impl(BasicPlot& self) noexcept : self{self}, initialized(false), matrix_size(QSize{3, 4}) {}
|
||||
|
||||
public:
|
||||
auto set_xlabel_text(const QString& text) -> void {
|
||||
xlabel = text;
|
||||
if (initialized) {
|
||||
self.xAxis->setLabel(text);
|
||||
self.replot();
|
||||
}
|
||||
}
|
||||
|
||||
auto set_ylabel_text(const QString& text) -> void {
|
||||
ylabel = text;
|
||||
if (initialized) {
|
||||
self.yAxis->setLabel(text);
|
||||
self.replot();
|
||||
}
|
||||
}
|
||||
|
||||
auto set_matrix_size(const QSize& size) -> void {
|
||||
matrix_size = size;
|
||||
if (initialized) {
|
||||
reset_plot();
|
||||
if (!data_points.isEmpty()) {
|
||||
set_data(data_points);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto load_theme_manager(ThemeManager& mgr) -> void {
|
||||
mgr.append_handler(&self, [this](const ThemeManager& mgr) {
|
||||
if (initialized) {
|
||||
self.replot();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
auto set_color_gradient_range(const double& min, const double& max) -> void {
|
||||
if (initialized && self.plottableCount() > 0) {
|
||||
auto* cpmp = static_cast<QCPColorMap*>(self.plottable(0));
|
||||
cpmp->setDataRange(QCPRange(min, max));
|
||||
self.replot();
|
||||
}
|
||||
color_min = min;
|
||||
color_max = max;
|
||||
}
|
||||
|
||||
auto set_data(const QVector<PointData>& data) -> void {
|
||||
data_points = data;
|
||||
if (initialized) {
|
||||
update_plot_data();
|
||||
}
|
||||
}
|
||||
|
||||
auto initialize_plot() -> void {
|
||||
if (initialized) return;
|
||||
|
||||
QCPColorMap* cpmp = new QCPColorMap(self.xAxis, self.yAxis);
|
||||
cpmp->data()->setSize(matrix_size.width(), matrix_size.height());
|
||||
cpmp->data()->setRange(QCPRange(0.5, matrix_size.width() - 0.5),
|
||||
QCPRange(0.5, matrix_size.height() - 0.5));
|
||||
|
||||
QSharedPointer<QCPAxisTickerText> xticker(new QCPAxisTickerText);
|
||||
QSharedPointer<QCPAxisTickerText> yticker(new QCPAxisTickerText);
|
||||
xticker->setSubTickCount(1);
|
||||
yticker->setSubTickCount(1);
|
||||
self.xAxis->setVisible(false);
|
||||
self.yAxis->setVisible(false);
|
||||
self.xAxis->setTicker(xticker);
|
||||
self.yAxis->setTicker(yticker);
|
||||
|
||||
self.xAxis->grid()->setPen(Qt::NoPen);
|
||||
self.yAxis->grid()->setPen(Qt::NoPen);
|
||||
self.xAxis->grid()->setSubGridVisible(true);
|
||||
self.yAxis->grid()->setSubGridVisible(true);
|
||||
self.xAxis->setSubTicks(true);
|
||||
self.yAxis->setSubTicks(true);
|
||||
self.xAxis->setTickLength(0);
|
||||
self.yAxis->setTickLength(0);
|
||||
self.xAxis->setSubTickLength(6);
|
||||
self.yAxis->setSubTickLength(6);
|
||||
|
||||
|
||||
self.xAxis->setRange(0, matrix_size.width());
|
||||
self.yAxis->setRange(0, matrix_size.height());
|
||||
|
||||
if (!xlabel.isEmpty()) self.xAxis->setLabel(xlabel);
|
||||
if (!ylabel.isEmpty()) self.yAxis->setLabel(ylabel);
|
||||
|
||||
QCPColorScale* color_scale = new QCPColorScale(&self);
|
||||
color_scale->setType(QCPAxis::atBottom);
|
||||
self.plotLayout()->addElement(1, 0, color_scale);
|
||||
cpmp->setColorScale(color_scale);
|
||||
|
||||
QCPColorGradient gradient;
|
||||
gradient.setColorStopAt(0.0, QColor(246, 239, 166)); // F6EFA6
|
||||
gradient.setColorStopAt(1.0, QColor(191, 68, 76)); // BF444C
|
||||
cpmp->setGradient(gradient);
|
||||
|
||||
cpmp->setDataRange(QCPRange(color_min, color_max));
|
||||
cpmp->setInterpolate(false);
|
||||
|
||||
QCPMarginGroup *margin_group = new QCPMarginGroup(&self);
|
||||
self.axisRect()->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
|
||||
color_scale->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
|
||||
|
||||
initialized = true;
|
||||
|
||||
if (!data_points.isEmpty()) {
|
||||
update_plot_data();
|
||||
}
|
||||
}
|
||||
|
||||
auto reset_plot() -> void {
|
||||
// 清除所有绘图元素
|
||||
self.clearPlottables();
|
||||
self.clearGraphs();
|
||||
self.clearItems();
|
||||
self.clearFocus();
|
||||
|
||||
// 重新初始化
|
||||
initialized = false;
|
||||
initialize_plot();
|
||||
}
|
||||
auto update_plot_data() -> void {
|
||||
if (!initialized || self.plottableCount() == 0) return;
|
||||
|
||||
auto* cpmp = static_cast<QCPColorMap*>(self.plottable(0));
|
||||
|
||||
// 设置新数据
|
||||
for (const auto& item : data_points) {
|
||||
if (item.x >= 0 && item.x < matrix_size.width() &&
|
||||
item.y >= 0 && item.y < matrix_size.height()) {
|
||||
cpmp->data()->setCell(item.x, item.y, item.z);
|
||||
}
|
||||
}
|
||||
|
||||
// 重绘
|
||||
self.replot();
|
||||
}
|
||||
|
||||
auto is_plot_initialized() const -> bool {
|
||||
return initialized;
|
||||
}
|
||||
|
||||
auto get_matrix_size() const -> QSize {
|
||||
return matrix_size;
|
||||
}
|
||||
|
||||
private:
|
||||
QString xlabel;
|
||||
QString ylabel;
|
||||
QSize matrix_size;
|
||||
QVector<PointData> data_points;
|
||||
double color_min = 0.0;
|
||||
double color_max = 800.0;
|
||||
bool initialized;
|
||||
BasicPlot& self;
|
||||
};
|
||||
|
||||
#endif // TOUCHSENSOR_HEATMAP_IMPL_HH
|
||||
//
|
||||
// Created by Lenn on 2025/10/17.
|
||||
//
|
||||
|
||||
#ifndef TOUCHSENSOR_HEATMAP_IMPL_HH
|
||||
#define TOUCHSENSOR_HEATMAP_IMPL_HH
|
||||
|
||||
#include "heatmap.hh"
|
||||
#include "creeper-qt/utility/theme/theme.hh"
|
||||
#include "creeper-qt/widget/sliders.hh"
|
||||
#include "qcustomplot/qcustomplot.h"
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <qcolor.h>
|
||||
#include <qdebug.h>
|
||||
#include <qfont.h>
|
||||
#include <vector>
|
||||
using namespace creeper::plot_widget::internal;
|
||||
|
||||
struct BasicPlot::Impl {
|
||||
explicit Impl(BasicPlot& self) noexcept : self{self}, initialized(false), matrix_size(QSize{3, 4}) {}
|
||||
|
||||
public:
|
||||
std::optional<creeper::ColorScheme> scheme;
|
||||
auto set_xlabel_text(const QString& text) -> void {
|
||||
xlabel = text;
|
||||
if (initialized) {
|
||||
self.xAxis->setLabel(text);
|
||||
self.replot();
|
||||
}
|
||||
}
|
||||
|
||||
auto set_ylabel_text(const QString& text) -> void {
|
||||
ylabel = text;
|
||||
if (initialized) {
|
||||
self.yAxis->setLabel(text);
|
||||
self.replot();
|
||||
}
|
||||
}
|
||||
|
||||
auto set_matrix_size(const QSize& size) -> void {
|
||||
matrix_size = size;
|
||||
if (initialized) {
|
||||
reset_plot();
|
||||
if (!data_points.isEmpty()) {
|
||||
set_data(data_points);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto load_theme_manager(ThemeManager& mgr) -> void {
|
||||
mgr.append_handler(&self, [this](const ThemeManager& mgr) {
|
||||
scheme = mgr.color_scheme();
|
||||
apply_color_scheme();
|
||||
if (initialized) {
|
||||
self.replot();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
auto set_color_gradient_range(const double& min, const double& max) -> void {
|
||||
if (initialized && color_map) {
|
||||
color_map->setDataRange(QCPRange(min, max));
|
||||
self.replot();
|
||||
}
|
||||
color_min = min;
|
||||
color_max = max;
|
||||
}
|
||||
|
||||
auto set_data(const QVector<PointData>& data) -> void {
|
||||
data_points = data;
|
||||
if (initialized && color_map) {
|
||||
update_plot_data();
|
||||
}
|
||||
}
|
||||
|
||||
auto initialize_plot() -> void {
|
||||
if (initialized) return;
|
||||
|
||||
color_map = new QCPColorMap(self.xAxis, self.yAxis);
|
||||
auto* cpmp = color_map;
|
||||
cpmp->data()->setSize(matrix_size.width(), matrix_size.height());
|
||||
cpmp->data()->setRange(QCPRange(0.5, matrix_size.width() - 0.5),
|
||||
QCPRange(0.5, matrix_size.height() - 0.5));
|
||||
|
||||
QSharedPointer<QCPAxisTickerText> xticker(new QCPAxisTickerText);
|
||||
QSharedPointer<QCPAxisTickerText> yticker(new QCPAxisTickerText);
|
||||
xticker->setSubTickCount(1);
|
||||
yticker->setSubTickCount(1);
|
||||
self.xAxis->setVisible(false);
|
||||
self.yAxis->setVisible(false);
|
||||
self.xAxis->setTicker(xticker);
|
||||
self.yAxis->setTicker(yticker);
|
||||
|
||||
self.xAxis->grid()->setPen(Qt::NoPen);
|
||||
self.yAxis->grid()->setPen(Qt::NoPen);
|
||||
self.xAxis->grid()->setSubGridVisible(true);
|
||||
self.yAxis->grid()->setSubGridVisible(true);
|
||||
self.xAxis->setSubTicks(true);
|
||||
self.yAxis->setSubTicks(true);
|
||||
self.xAxis->setTickLength(0);
|
||||
self.yAxis->setTickLength(0);
|
||||
self.xAxis->setSubTickLength(6);
|
||||
self.yAxis->setSubTickLength(6);
|
||||
|
||||
|
||||
self.xAxis->setRange(0, matrix_size.width());
|
||||
self.yAxis->setRange(0, matrix_size.height());
|
||||
|
||||
if (!xlabel.isEmpty()) self.xAxis->setLabel(xlabel);
|
||||
if (!ylabel.isEmpty()) self.yAxis->setLabel(ylabel);
|
||||
|
||||
if (!color_scale) {
|
||||
color_scale = new QCPColorScale(&self);
|
||||
color_scale->setType(QCPAxis::atBottom);
|
||||
}
|
||||
if (self.plotLayout()) {
|
||||
auto existing = self.plotLayout()->element(1, 0);
|
||||
if (existing && existing != color_scale) {
|
||||
self.plotLayout()->take(existing);
|
||||
}
|
||||
if (!existing || existing != color_scale) {
|
||||
self.plotLayout()->addElement(1, 0, color_scale);
|
||||
}
|
||||
}
|
||||
cpmp->setColorScale(color_scale);
|
||||
|
||||
QCPColorGradient gradient;
|
||||
gradient.setColorStopAt(0.0, QColor(240, 246, 255)); // 低值淡色
|
||||
gradient.setColorStopAt(0.35, QColor(142, 197, 252));
|
||||
gradient.setColorStopAt(0.7, QColor(56, 128, 199));
|
||||
gradient.setColorStopAt(1.0, QColor(8, 36, 95)); // 高值深色
|
||||
cpmp->setGradient(gradient);
|
||||
|
||||
cpmp->setDataRange(QCPRange(color_min, color_max));
|
||||
cpmp->setInterpolate(false);
|
||||
|
||||
QCPMarginGroup *margin_group = new QCPMarginGroup(&self);
|
||||
self.axisRect()->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
|
||||
color_scale->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
|
||||
|
||||
initialized = true;
|
||||
apply_color_scheme();
|
||||
|
||||
if (!data_points.isEmpty()) {
|
||||
update_plot_data();
|
||||
}
|
||||
}
|
||||
|
||||
auto reset_plot() -> void {
|
||||
// 清除所有绘图元素
|
||||
self.clearPlottables();
|
||||
self.clearGraphs();
|
||||
self.clearItems();
|
||||
self.clearFocus();
|
||||
color_map = nullptr;
|
||||
cell_labels.clear();
|
||||
|
||||
// 重新初始化
|
||||
initialized = false;
|
||||
initialize_plot();
|
||||
}
|
||||
auto update_plot_data() -> void {
|
||||
if (!initialized || !color_map) return;
|
||||
|
||||
ensure_labels();
|
||||
|
||||
const int width = matrix_size.width();
|
||||
const int height = matrix_size.height();
|
||||
const int expected = width * height;
|
||||
std::vector<double> values(static_cast<std::size_t>(expected), 0.0);
|
||||
|
||||
// 设置新数据
|
||||
for (const auto& item : data_points) {
|
||||
if (item.x >= 0 && item.x < matrix_size.width() &&
|
||||
item.y >= 0 && item.y < matrix_size.height()) {
|
||||
color_map->data()->setCell(item.x, item.y, item.z);
|
||||
const int idx = static_cast<int>(item.y) * width + static_cast<int>(item.x);
|
||||
if (idx >= 0 && idx < expected) {
|
||||
values[static_cast<std::size_t>(idx)] = item.z;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update_label_values(values);
|
||||
|
||||
// 重绘
|
||||
self.replot();
|
||||
}
|
||||
|
||||
auto is_plot_initialized() const -> bool {
|
||||
return initialized;
|
||||
}
|
||||
|
||||
auto get_matrix_size() const -> QSize {
|
||||
return matrix_size;
|
||||
}
|
||||
|
||||
private:
|
||||
QString xlabel;
|
||||
QString ylabel;
|
||||
QSize matrix_size;
|
||||
QVector<PointData> data_points;
|
||||
double color_min = 0.0;
|
||||
double color_max = 800.0;
|
||||
bool initialized;
|
||||
BasicPlot& self;
|
||||
QCPColorScale* color_scale = nullptr;
|
||||
QCPColorMap* color_map = nullptr;
|
||||
QVector<QCPItemText*> cell_labels;
|
||||
QColor label_text_color = QColor(0, 0, 0);
|
||||
|
||||
void apply_color_scheme() {
|
||||
QColor text_color = QColor(30, 30, 30);
|
||||
if (scheme.has_value()) {
|
||||
if (scheme->on_surface.isValid()) {
|
||||
text_color = scheme->on_surface;
|
||||
}
|
||||
}
|
||||
label_text_color = QColor(0, 0, 0); // 固定黑色
|
||||
|
||||
const auto pen = QPen(text_color);
|
||||
|
||||
self.xAxis->setTickLabelColor(text_color);
|
||||
self.yAxis->setTickLabelColor(text_color);
|
||||
self.xAxis->setLabelColor(text_color);
|
||||
self.yAxis->setLabelColor(text_color);
|
||||
self.xAxis->setBasePen(pen);
|
||||
self.yAxis->setBasePen(pen);
|
||||
self.xAxis->setTickPen(pen);
|
||||
self.yAxis->setTickPen(pen);
|
||||
if (color_scale && color_scale->axis()) {
|
||||
color_scale->axis()->setTickLabelColor(text_color);
|
||||
color_scale->axis()->setLabelColor(text_color);
|
||||
color_scale->axis()->setBasePen(pen);
|
||||
color_scale->axis()->setTickPen(pen);
|
||||
}
|
||||
|
||||
// 已有标签更新
|
||||
for (auto* label : cell_labels) {
|
||||
if (!label) continue;
|
||||
label->setColor(label_text_color);
|
||||
}
|
||||
}
|
||||
|
||||
void clear_labels() {
|
||||
for (auto* label : cell_labels) {
|
||||
if (label) {
|
||||
self.removeItem(label);
|
||||
}
|
||||
}
|
||||
cell_labels.clear();
|
||||
}
|
||||
|
||||
void ensure_labels() {
|
||||
const int width = matrix_size.width();
|
||||
const int height = matrix_size.height();
|
||||
const int expected = width * height;
|
||||
if (expected <= 0) {
|
||||
clear_labels();
|
||||
return;
|
||||
}
|
||||
if (cell_labels.size() == expected) {
|
||||
return;
|
||||
}
|
||||
|
||||
clear_labels();
|
||||
cell_labels.reserve(expected);
|
||||
for (int y = 0; y < height; ++y) {
|
||||
for (int x = 0; x < width; ++x) {
|
||||
auto* label = new QCPItemText(&self);
|
||||
label->position->setType(QCPItemPosition::ptPlotCoords);
|
||||
label->setClipToAxisRect(true);
|
||||
label->setClipAxisRect(self.axisRect());
|
||||
label->setPositionAlignment(Qt::AlignCenter);
|
||||
label->position->setCoords(x + 0.5, y + 0.5);
|
||||
label->setBrush(Qt::NoBrush);
|
||||
label->setPen(Qt::NoPen);
|
||||
QFont font = label->font();
|
||||
if (font.pointSize() > 0) {
|
||||
font.setPointSize(std::max(font.pointSize() - 1, 6));
|
||||
} else {
|
||||
font.setPointSize(8);
|
||||
}
|
||||
label->setFont(font);
|
||||
label->setColor(label_text_color);
|
||||
label->setSelectable(false);
|
||||
cell_labels.push_back(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void update_label_values(const std::vector<double>& values) {
|
||||
const int width = matrix_size.width();
|
||||
const int height = matrix_size.height();
|
||||
const double range = std::max(color_max - color_min, 1.0);
|
||||
const int expected = width * height;
|
||||
for (int idx = 0; idx < expected && idx < cell_labels.size(); ++idx) {
|
||||
auto* label = cell_labels[idx];
|
||||
if (!label) {
|
||||
continue;
|
||||
}
|
||||
const double value = values.size() > static_cast<std::size_t>(idx)
|
||||
? values[static_cast<std::size_t>(idx)]
|
||||
: 0.0;
|
||||
label->setText(QString::number(value, 'f', 0));
|
||||
const int x = idx % width;
|
||||
const int y = idx / width;
|
||||
label->position->setCoords(x + 0.5, y + 0.5);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif // TOUCHSENSOR_HEATMAP_IMPL_HH
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "components/ffmsep/cpstream_core.hh"
|
||||
|
||||
#include "components/ffmsep/presist/presist.hh"
|
||||
#include "dlog/dlog.hh"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
@@ -7,20 +10,22 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
|
||||
namespace ffmsep {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kReaderIdleSleep = std::chrono::milliseconds(5);
|
||||
constexpr auto kDecoderIdleSleep = std::chrono::milliseconds(1);
|
||||
constexpr auto kReaderIdleSleep = 5ms;
|
||||
constexpr auto kDecoderIdleSleep = 1ms;
|
||||
|
||||
const CPCodec* resolve_requested_codec(const CPStreamConfig& config) {
|
||||
if (!config.codec_name.empty()) {
|
||||
@@ -49,6 +54,7 @@ struct CPStreamCore::Impl {
|
||||
explicit Impl(CPStreamConfig config)
|
||||
: config_(std::move(config)) {
|
||||
normalize_config();
|
||||
frame_writer_ = std::make_unique<persist::JsonWritter>();
|
||||
}
|
||||
|
||||
~Impl() = default;
|
||||
@@ -64,7 +70,7 @@ struct CPStreamCore::Impl {
|
||||
config_.frame_queue_capacity = 1U;
|
||||
}
|
||||
if (config_.slave_request_interval.count() < 0) {
|
||||
config_.slave_request_interval = std::chrono::milliseconds{0};
|
||||
config_.slave_request_interval = 0ms;
|
||||
}
|
||||
frame_queue_capacity_ = config_.frame_queue_capacity;
|
||||
}
|
||||
@@ -116,7 +122,7 @@ struct CPStreamCore::Impl {
|
||||
serial->flush();
|
||||
|
||||
{
|
||||
std::lock_guard lock(serial_mutex_);
|
||||
std::lock_guard<std::mutex> lock(serial_mutex_);
|
||||
serial_ = std::move(serial);
|
||||
}
|
||||
} catch (const serial::IOException& ex) {
|
||||
@@ -140,11 +146,11 @@ struct CPStreamCore::Impl {
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lock(packet_mutex_);
|
||||
std::lock_guard<std::mutex> lock(packet_mutex_);
|
||||
packet_queue_.clear();
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(frame_mutex_);
|
||||
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||
frame_queue_.clear();
|
||||
}
|
||||
pts_counter_.store(0, std::memory_order_relaxed);
|
||||
@@ -165,7 +171,7 @@ struct CPStreamCore::Impl {
|
||||
stop();
|
||||
|
||||
{
|
||||
std::lock_guard lock(serial_mutex_);
|
||||
std::lock_guard<std::mutex> lock(serial_mutex_);
|
||||
if (serial_) {
|
||||
try {
|
||||
if (serial_->isOpen()) {
|
||||
@@ -185,12 +191,13 @@ struct CPStreamCore::Impl {
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lock(packet_mutex_);
|
||||
std::lock_guard<std::mutex> lock(packet_mutex_);
|
||||
packet_queue_.clear();
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(frame_mutex_);
|
||||
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||
frame_queue_.clear();
|
||||
frame_record_queue_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +208,7 @@ struct CPStreamCore::Impl {
|
||||
|
||||
std::shared_ptr<serial::Serial> serial_copy;
|
||||
{
|
||||
std::lock_guard lock(serial_mutex_);
|
||||
std::lock_guard<std::mutex> lock(serial_mutex_);
|
||||
serial_copy = serial_;
|
||||
}
|
||||
if (!serial_copy || !serial_copy->isOpen()) {
|
||||
@@ -250,7 +257,7 @@ struct CPStreamCore::Impl {
|
||||
stop_requested_.store(false, std::memory_order_release);
|
||||
|
||||
{
|
||||
std::lock_guard lock(packet_mutex_);
|
||||
std::lock_guard<std::mutex> lock(packet_mutex_);
|
||||
packet_queue_.clear();
|
||||
}
|
||||
|
||||
@@ -260,7 +267,7 @@ struct CPStreamCore::Impl {
|
||||
}
|
||||
|
||||
bool is_open() const {
|
||||
std::lock_guard lock(serial_mutex_);
|
||||
std::lock_guard<std::mutex> lock(serial_mutex_);
|
||||
return serial_ && serial_->isOpen();
|
||||
}
|
||||
|
||||
@@ -279,7 +286,7 @@ struct CPStreamCore::Impl {
|
||||
|
||||
std::shared_ptr<serial::Serial> serial_copy;
|
||||
{
|
||||
std::lock_guard lock(serial_mutex_);
|
||||
std::lock_guard<std::mutex> lock(serial_mutex_);
|
||||
serial_copy = serial_;
|
||||
}
|
||||
|
||||
@@ -301,17 +308,17 @@ struct CPStreamCore::Impl {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<DecodedFrame> try_pop_frame() {
|
||||
std::lock_guard lock(frame_mutex_);
|
||||
std::optional<std::shared_ptr<DecodedFrame>> try_pop_frame() {
|
||||
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||
if (frame_queue_.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
DecodedFrame frame = std::move(frame_queue_.front());
|
||||
std::shared_ptr<DecodedFrame> frame = std::move(frame_queue_.front());
|
||||
frame_queue_.pop_front();
|
||||
return frame;
|
||||
}
|
||||
|
||||
bool wait_for_frame(DecodedFrame& frame, std::chrono::milliseconds timeout) {
|
||||
bool wait_for_frame(std::shared_ptr<DecodedFrame>& frame, std::chrono::milliseconds timeout) {
|
||||
std::unique_lock lock(frame_mutex_);
|
||||
if (!frame_cv_.wait_for(lock, timeout, [&] {
|
||||
return !frame_queue_.empty();
|
||||
@@ -324,7 +331,7 @@ struct CPStreamCore::Impl {
|
||||
}
|
||||
|
||||
void clear_frames() {
|
||||
std::lock_guard lock(frame_mutex_);
|
||||
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||
frame_queue_.clear();
|
||||
}
|
||||
|
||||
@@ -333,7 +340,7 @@ struct CPStreamCore::Impl {
|
||||
capacity = 1U;
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(frame_mutex_);
|
||||
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||
frame_queue_capacity_ = capacity;
|
||||
config_.frame_queue_capacity = capacity;
|
||||
while (frame_queue_.size() > frame_queue_capacity_) {
|
||||
@@ -342,8 +349,46 @@ struct CPStreamCore::Impl {
|
||||
}
|
||||
}
|
||||
|
||||
void clear_recorded_frames() {
|
||||
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||
frame_record_queue_.clear();
|
||||
}
|
||||
|
||||
std::size_t recorded_frame_count() const {
|
||||
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||
return frame_record_queue_.size();
|
||||
}
|
||||
|
||||
std::future<persist::WriteResult> export_recorded_frames(const std::string& path, bool clear_after_export) {
|
||||
if (!frame_writer_) {
|
||||
frame_writer_ = std::make_unique<persist::JsonWritter>();
|
||||
}
|
||||
|
||||
std::deque<std::shared_ptr<DecodedFrame>> snapshot;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||
snapshot = frame_record_queue_;
|
||||
if (clear_after_export) {
|
||||
frame_record_queue_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (snapshot.empty()) {
|
||||
std::promise<persist::WriteResult> promise;
|
||||
auto future = promise.get_future();
|
||||
promise.set_value(persist::WriteResult{
|
||||
false,
|
||||
"no recorded frames available",
|
||||
path
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
return frame_writer_->enqueue(path, std::move(snapshot));
|
||||
}
|
||||
|
||||
void set_frame_callback(FrameCallback callback) {
|
||||
std::lock_guard lock(callback_mutex_);
|
||||
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||
frame_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
@@ -352,7 +397,7 @@ struct CPStreamCore::Impl {
|
||||
}
|
||||
|
||||
std::string last_error() const {
|
||||
std::lock_guard lock(last_error_mutex_);
|
||||
std::lock_guard<std::mutex> lock(last_error_mutex_);
|
||||
return last_error_;
|
||||
}
|
||||
|
||||
@@ -366,7 +411,7 @@ struct CPStreamCore::Impl {
|
||||
while (!stop_requested_.load(std::memory_order_acquire)) {
|
||||
std::shared_ptr<serial::Serial> serial_copy;
|
||||
{
|
||||
std::lock_guard lock(serial_mutex_);
|
||||
std::lock_guard<std::mutex> lock(serial_mutex_);
|
||||
serial_copy = serial_;
|
||||
}
|
||||
if (!serial_copy || !serial_copy->isOpen()) {
|
||||
@@ -401,7 +446,7 @@ struct CPStreamCore::Impl {
|
||||
packet.pts = pts_counter_.fetch_add(1, std::memory_order_relaxed);
|
||||
|
||||
{
|
||||
std::lock_guard lock(packet_mutex_);
|
||||
std::lock_guard<std::mutex> lock(packet_mutex_);
|
||||
if (packet_queue_.size() >= config_.packet_queue_capacity) {
|
||||
packet_queue_.pop_front();
|
||||
}
|
||||
@@ -415,7 +460,7 @@ struct CPStreamCore::Impl {
|
||||
const auto command = config_.slave_request_command;
|
||||
auto interval = config_.slave_request_interval;
|
||||
if (interval.count() < 0) {
|
||||
interval = std::chrono::milliseconds{0};
|
||||
interval = 0ms;
|
||||
}
|
||||
const bool repeat = interval.count() > 0;
|
||||
|
||||
@@ -485,23 +530,24 @@ struct CPStreamCore::Impl {
|
||||
CPFrame frame;
|
||||
rc = cpcodec_receive_frame(codec_ctx_, &frame);
|
||||
if (rc == CP_SUCCESS) {
|
||||
DecodedFrame decoded;
|
||||
decoded.pts = frame.pts;
|
||||
decoded.received_at = std::chrono::steady_clock::now();
|
||||
decoded.frame = std::move(frame);
|
||||
if (codec_descriptor_ && codec_descriptor_->id == CPCodecID::Tactile) {
|
||||
if (auto parsed = tactile::parse_frame(decoded.frame)) {
|
||||
decoded.tactile = parsed;
|
||||
decoded.tactile_pressures = tactile::parse_pressure_values(*parsed);
|
||||
auto decoded = std::make_shared<DecodedFrame>();
|
||||
decoded->pts = frame.pts;
|
||||
decoded->received_at = std::chrono::steady_clock::now();
|
||||
decoded->frame = std::move(frame);
|
||||
decoded->id = codec_descriptor_ ? codec_descriptor_->id : CPCodecID::Unknow;
|
||||
if (decoded->id == CPCodecID::Tactile) {
|
||||
if (auto parsed = tactile::parse_frame(decoded->frame)) {
|
||||
decoded->tactile = parsed;
|
||||
decoded->tactile_pressures = tactile::parse_pressure_values(*parsed);
|
||||
if (auto matrix = tactile::parse_matrix_size_payload(*parsed)) {
|
||||
decoded.tactile_matrix_size = matrix;
|
||||
decoded->tactile_matrix_size = matrix;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FrameCallback callback_copy;
|
||||
{
|
||||
std::lock_guard lock(callback_mutex_);
|
||||
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||
callback_copy = frame_callback_;
|
||||
}
|
||||
if (callback_copy) {
|
||||
@@ -509,11 +555,14 @@ struct CPStreamCore::Impl {
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lock(frame_mutex_);
|
||||
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||
if (frame_queue_.size() >= frame_queue_capacity_) {
|
||||
frame_queue_.pop_front();
|
||||
}
|
||||
frame_queue_.push_back(std::move(decoded));
|
||||
frame_queue_.push_back(decoded);
|
||||
if (decoded->id == CPCodecID::Tactile) {
|
||||
frame_record_queue_.push_back(decoded);
|
||||
}
|
||||
}
|
||||
frame_cv_.notify_one();
|
||||
} else if (rc == CP_ERROR_EAGAIN) {
|
||||
@@ -537,7 +586,7 @@ struct CPStreamCore::Impl {
|
||||
packet.flush = true;
|
||||
packet.end_of_stream = end_of_stream;
|
||||
{
|
||||
std::lock_guard lock(packet_mutex_);
|
||||
std::lock_guard<std::mutex> lock(packet_mutex_);
|
||||
packet_queue_.push_back(std::move(packet));
|
||||
}
|
||||
packet_cv_.notify_one();
|
||||
@@ -555,7 +604,7 @@ struct CPStreamCore::Impl {
|
||||
}
|
||||
|
||||
void set_last_error(std::string message) {
|
||||
std::lock_guard lock(last_error_mutex_);
|
||||
std::lock_guard<std::mutex> lock(last_error_mutex_);
|
||||
last_error_ = std::move(message);
|
||||
}
|
||||
|
||||
@@ -575,9 +624,12 @@ struct CPStreamCore::Impl {
|
||||
std::condition_variable packet_cv_;
|
||||
std::deque<Packet> packet_queue_;
|
||||
|
||||
std::mutex frame_mutex_;
|
||||
mutable std::mutex frame_mutex_;
|
||||
std::condition_variable frame_cv_;
|
||||
std::deque<DecodedFrame> frame_queue_;
|
||||
// std::deque<DecodedFrame> frame_queue_;
|
||||
// 更新为智能指针,我们需要更长的生命周期😊
|
||||
std::deque<std::shared_ptr<DecodedFrame>> frame_queue_;
|
||||
std::deque<std::shared_ptr<DecodedFrame>> frame_record_queue_;
|
||||
std::size_t frame_queue_capacity_ = 16;
|
||||
|
||||
FrameCallback frame_callback_;
|
||||
@@ -589,6 +641,8 @@ struct CPStreamCore::Impl {
|
||||
|
||||
std::string last_error_;
|
||||
mutable std::mutex last_error_mutex_;
|
||||
|
||||
std::unique_ptr<persist::JsonWritter> frame_writer_;
|
||||
};
|
||||
|
||||
CPStreamCore::CPStreamCore(CPStreamConfig config)
|
||||
@@ -641,11 +695,11 @@ bool CPStreamCore::send(const std::uint8_t* data, std::size_t size) {
|
||||
return impl_->send(data, size);
|
||||
}
|
||||
|
||||
std::optional<DecodedFrame> CPStreamCore::try_pop_frame() {
|
||||
std::optional<std::shared_ptr<DecodedFrame>> CPStreamCore::try_pop_frame() {
|
||||
return impl_->try_pop_frame();
|
||||
}
|
||||
|
||||
bool CPStreamCore::wait_for_frame(DecodedFrame& frame, std::chrono::milliseconds timeout) {
|
||||
bool CPStreamCore::wait_for_frame(std::shared_ptr<DecodedFrame>& frame, std::chrono::milliseconds timeout) {
|
||||
return impl_->wait_for_frame(frame, timeout);
|
||||
}
|
||||
|
||||
@@ -657,6 +711,18 @@ void CPStreamCore::set_frame_queue_capacity(std::size_t capacity) {
|
||||
impl_->set_frame_queue_capacity(capacity);
|
||||
}
|
||||
|
||||
void CPStreamCore::clear_recorded_frames() {
|
||||
impl_->clear_recorded_frames();
|
||||
}
|
||||
|
||||
std::size_t CPStreamCore::recorded_frame_count() const {
|
||||
return impl_->recorded_frame_count();
|
||||
}
|
||||
|
||||
std::future<persist::WriteResult> CPStreamCore::export_recorded_frames(const std::string& path, bool clear_after_export) {
|
||||
return impl_->export_recorded_frames(path, clear_after_export);
|
||||
}
|
||||
|
||||
void CPStreamCore::set_frame_callback(FrameCallback callback) {
|
||||
impl_->set_frame_callback(std::move(callback));
|
||||
}
|
||||
|
||||
@@ -5,16 +5,23 @@
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <serial/serial.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace ffmsep {
|
||||
|
||||
namespace persist {
|
||||
struct WriteResult;
|
||||
} // namespace persist
|
||||
|
||||
struct DecodedFrame {
|
||||
CPFrame frame;
|
||||
CPCodecID id = CPCodecID::Unknow;
|
||||
std::chrono::steady_clock::time_point received_at{};
|
||||
std::int64_t pts = 0;
|
||||
std::optional<tactile::TactileFrame> tactile;
|
||||
@@ -22,7 +29,6 @@ struct DecodedFrame {
|
||||
std::optional<tactile::MatrixSize> tactile_matrix_size;
|
||||
};
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
struct CPStreamConfig {
|
||||
std::string port;
|
||||
std::uint32_t baudrate = 115200;
|
||||
@@ -37,12 +43,12 @@ struct CPStreamConfig {
|
||||
CPCodecID codec_id = CPCodecID::Unknow;
|
||||
std::string codec_name;
|
||||
std::vector<std::uint8_t> slave_request_command{};
|
||||
std::chrono::milliseconds slave_request_interval = 200ms;
|
||||
std::chrono::milliseconds slave_request_interval{200ms};
|
||||
};
|
||||
|
||||
class CPStreamCore {
|
||||
public:
|
||||
using FrameCallback = std::function<void(const DecodedFrame&)>;
|
||||
using FrameCallback = std::function<void(std::shared_ptr<DecodedFrame>)>;
|
||||
|
||||
explicit CPStreamCore(CPStreamConfig config = {});
|
||||
~CPStreamCore();
|
||||
@@ -64,10 +70,14 @@ public:
|
||||
bool send(const std::vector<std::uint8_t>& data);
|
||||
bool send(const std::uint8_t* data, std::size_t size);
|
||||
|
||||
std::optional<DecodedFrame> try_pop_frame();
|
||||
bool wait_for_frame(DecodedFrame& frame, std::chrono::milliseconds timeout);
|
||||
std::optional<std::shared_ptr<DecodedFrame>> try_pop_frame();
|
||||
bool wait_for_frame(std::shared_ptr<DecodedFrame>& frame, std::chrono::milliseconds timeout);
|
||||
void clear_frames();
|
||||
void set_frame_queue_capacity(std::size_t capacity);
|
||||
void clear_recorded_frames();
|
||||
[[nodiscard]] std::size_t recorded_frame_count() const;
|
||||
std::future<persist::WriteResult> export_recorded_frames(const std::string& path,
|
||||
bool clear_after_export = false);
|
||||
|
||||
void set_frame_callback(FrameCallback callback);
|
||||
|
||||
@@ -76,6 +86,8 @@ public:
|
||||
|
||||
static std::vector<serial::PortInfo> list_available_ports();
|
||||
|
||||
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
|
||||
255
components/ffmsep/presist/presist.cc
Normal file
255
components/ffmsep/presist/presist.cc
Normal file
@@ -0,0 +1,255 @@
|
||||
//
|
||||
// Created by Lenn on 2025/10/31.
|
||||
//
|
||||
|
||||
#include "components/ffmsep/presist/presist.hh"
|
||||
|
||||
#include "components/ffmsep/cpstream_core.hh"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <system_error>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace ffmsep::persist {
|
||||
|
||||
namespace {
|
||||
|
||||
using nlohmann::json;
|
||||
|
||||
bool is_simple_array(const json& value) {
|
||||
if (!value.is_array()) {
|
||||
return false;
|
||||
}
|
||||
return std::all_of(value.begin(), value.end(), [](const json& item) {
|
||||
return item.is_primitive();
|
||||
});
|
||||
}
|
||||
|
||||
void dump_compact_json(std::ostream& out,
|
||||
const json& value,
|
||||
int indent = 0,
|
||||
int indent_step = 2) {
|
||||
const auto indent_str = std::string(static_cast<std::size_t>(indent), ' ');
|
||||
const auto child_indent = indent + indent_step;
|
||||
const auto child_indent_str = std::string(static_cast<std::size_t>(child_indent), ' ');
|
||||
|
||||
if (value.is_object()) {
|
||||
out << "{\n";
|
||||
bool first = true;
|
||||
for (auto it = value.begin(); it != value.end(); ++it) {
|
||||
if (!first) {
|
||||
out << ",\n";
|
||||
}
|
||||
first = false;
|
||||
out << child_indent_str << json(it.key()).dump() << ": ";
|
||||
dump_compact_json(out, it.value(), child_indent, indent_step);
|
||||
}
|
||||
out << '\n' << indent_str << '}';
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.is_array()) {
|
||||
if (value.empty()) {
|
||||
out << "[]";
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_simple_array(value)) {
|
||||
out << '[';
|
||||
for (std::size_t idx = 0; idx < value.size(); ++idx) {
|
||||
if (idx != 0U) {
|
||||
out << ", ";
|
||||
}
|
||||
out << value[static_cast<json::size_type>(idx)].dump();
|
||||
}
|
||||
out << ']';
|
||||
return;
|
||||
}
|
||||
|
||||
out << "[\n";
|
||||
bool first = true;
|
||||
for (const auto& item : value) {
|
||||
if (!first) {
|
||||
out << ",\n";
|
||||
}
|
||||
first = false;
|
||||
out << child_indent_str;
|
||||
dump_compact_json(out, item, child_indent, indent_step);
|
||||
}
|
||||
out << '\n' << indent_str << ']';
|
||||
return;
|
||||
}
|
||||
|
||||
out << value.dump();
|
||||
}
|
||||
|
||||
json serialize_tactile_frame(const DecodedFrame& frame) {
|
||||
json result = {
|
||||
{"pts", frame.pts},
|
||||
{"raw_frame", frame.frame.data},
|
||||
{"pressures", frame.tactile_pressures},
|
||||
};
|
||||
|
||||
const auto received = frame.received_at.time_since_epoch();
|
||||
result["received_at_ns"] =
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(received).count();
|
||||
|
||||
if (frame.tactile_matrix_size) {
|
||||
result["matrix"] = {
|
||||
{"long_edge", frame.tactile_matrix_size->long_edge},
|
||||
{"short_edge", frame.tactile_matrix_size->short_edge},
|
||||
};
|
||||
}
|
||||
|
||||
if (frame.tactile) {
|
||||
const auto& tactile = *frame.tactile;
|
||||
result["tactile"] = {
|
||||
{"device_address", tactile.device_address},
|
||||
{"response_function", tactile.response_function},
|
||||
{"function", static_cast<std::uint8_t>(tactile.function)},
|
||||
{"start_address", tactile.start_address},
|
||||
{"return_byte_count", tactile.return_byte_count},
|
||||
{"status", tactile.status},
|
||||
{"payload", tactile.payload},
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool WriteQueue::push(WriteRequest&& req) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (stopped_) {
|
||||
return false;
|
||||
}
|
||||
queue_.push(std::move(req));
|
||||
}
|
||||
cond_.notify_one();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WriteQueue::pop(WriteRequest& out) {
|
||||
std::unique_lock lock(mutex_);
|
||||
cond_.wait(lock, [&] {
|
||||
return stopped_ || !queue_.empty();
|
||||
});
|
||||
|
||||
if (queue_.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
out = std::move(queue_.front());
|
||||
queue_.pop();
|
||||
return true;
|
||||
}
|
||||
|
||||
void WriteQueue::stop() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
stopped_ = true;
|
||||
}
|
||||
cond_.notify_all();
|
||||
}
|
||||
|
||||
JsonWritter::JsonWritter()
|
||||
: write_thread_([this] { run(); }) {}
|
||||
|
||||
JsonWritter::~JsonWritter() {
|
||||
stop();
|
||||
}
|
||||
|
||||
std::future<WriteResult> JsonWritter::enqueue(std::string path,
|
||||
std::deque<std::shared_ptr<DecodedFrame>> frames) {
|
||||
std::promise<WriteResult> promise;
|
||||
auto future = promise.get_future();
|
||||
|
||||
WriteRequest request{std::move(path), std::move(frames), std::move(promise)};
|
||||
if (!write_queue_.push(std::move(request))) {
|
||||
WriteResult result{false, "writer has been stopped", request.path};
|
||||
request.promise.set_value(std::move(result));
|
||||
}
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
void JsonWritter::run() {
|
||||
WriteRequest request;
|
||||
while (write_queue_.pop(request)) {
|
||||
try {
|
||||
auto result = write_once(request.path, std::move(request.frames));
|
||||
request.promise.set_value(std::move(result));
|
||||
} catch (const std::exception& ex) {
|
||||
request.promise.set_value(WriteResult{false, ex.what(), request.path});
|
||||
} catch (...) {
|
||||
request.promise.set_value(WriteResult{false, "unknown error", request.path});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WriteResult JsonWritter::write_once(const std::string& path,
|
||||
std::deque<std::shared_ptr<DecodedFrame>> frames) {
|
||||
if (path.empty()) {
|
||||
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);
|
||||
if (fs_path.has_parent_path()) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(fs_path.parent_path(), ec);
|
||||
if (ec) {
|
||||
return {false, "failed to create export directory: " + ec.message(), path};
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream stream(path, std::ios::binary | std::ios::trunc);
|
||||
if (!stream.is_open()) {
|
||||
return {false, "failed to open export file", path};
|
||||
}
|
||||
|
||||
dump_compact_json(stream, root);
|
||||
stream << '\n';
|
||||
stream.flush();
|
||||
if (!stream.good()) {
|
||||
return {false, "failed to write export file", path};
|
||||
}
|
||||
|
||||
return {true, {}, path};
|
||||
}
|
||||
|
||||
void JsonWritter::stop() {
|
||||
if (!stopped_.exchange(true)) {
|
||||
write_queue_.stop();
|
||||
if (write_thread_.joinable()) {
|
||||
write_thread_.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ffmsep::persist
|
||||
86
components/ffmsep/presist/presist.hh
Normal file
86
components/ffmsep/presist/presist.hh
Normal file
@@ -0,0 +1,86 @@
|
||||
//
|
||||
// Created by Lenn on 2025/10/31.
|
||||
//
|
||||
|
||||
#ifndef TOUCHSENSOR_PRESIST_HH
|
||||
#define TOUCHSENSOR_PRESIST_HH
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace ffmsep {
|
||||
|
||||
struct DecodedFrame;
|
||||
|
||||
namespace persist {
|
||||
|
||||
struct WriteResult {
|
||||
bool ok = false;
|
||||
std::string error;
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct WriteRequest {
|
||||
WriteRequest() = default;
|
||||
WriteRequest(std::string p,
|
||||
std::deque<std::shared_ptr<DecodedFrame>> f,
|
||||
std::promise<WriteResult>&& pr)
|
||||
: path(std::move(p))
|
||||
, frames(std::move(f))
|
||||
, promise(std::move(pr)) {}
|
||||
|
||||
WriteRequest(const WriteRequest&) = delete;
|
||||
WriteRequest& operator=(const WriteRequest&) = delete;
|
||||
WriteRequest(WriteRequest&&) noexcept = default;
|
||||
WriteRequest& operator=(WriteRequest&&) noexcept = default;
|
||||
|
||||
std::string path;
|
||||
std::deque<std::shared_ptr<DecodedFrame>> frames;
|
||||
std::promise<WriteResult> promise;
|
||||
};
|
||||
|
||||
class WriteQueue {
|
||||
public:
|
||||
bool push(WriteRequest&& req);
|
||||
bool pop(WriteRequest& out);
|
||||
void stop();
|
||||
|
||||
private:
|
||||
std::mutex mutex_;
|
||||
std::condition_variable cond_;
|
||||
std::queue<WriteRequest> queue_;
|
||||
bool stopped_ = false;
|
||||
};
|
||||
|
||||
class JsonWritter {
|
||||
public:
|
||||
JsonWritter();
|
||||
~JsonWritter();
|
||||
|
||||
std::future<WriteResult> enqueue(std::string path,
|
||||
std::deque<std::shared_ptr<DecodedFrame>> frames);
|
||||
|
||||
void stop();
|
||||
|
||||
private:
|
||||
void run();
|
||||
WriteResult write_once(const std::string& path,
|
||||
std::deque<std::shared_ptr<DecodedFrame>> frames);
|
||||
|
||||
private:
|
||||
std::thread write_thread_;
|
||||
WriteQueue write_queue_;
|
||||
std::atomic_bool stopped_{false};
|
||||
};
|
||||
|
||||
} // namespace persist
|
||||
} // namespace ffmsep
|
||||
|
||||
#endif // TOUCHSENSOR_PRESIST_HH
|
||||
@@ -1,45 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include "cpdecoder.hh"
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "cpdecoder.hh"
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace ffmsep::tactile {
|
||||
inline constexpr std::uint8_t kStartByteFirst = 0xAA;
|
||||
inline constexpr std::uint8_t kStartByteSecond = 0x55;
|
||||
|
||||
enum class FunctionCode : std::uint8_t {
|
||||
Unknown = 0x00,
|
||||
ReadMatrix = 0x01,
|
||||
ReadSingle = 0x02,
|
||||
ReadTemperature = 0x03,
|
||||
SetDeviceId = 0x51,
|
||||
SetMatrixSize = 0x52,
|
||||
CalibrationMode = 0x53,
|
||||
};
|
||||
|
||||
struct MatrixSize {
|
||||
std::uint8_t long_edge = 0;
|
||||
std::uint8_t short_edge = 0;
|
||||
};
|
||||
|
||||
struct TactileFrame {
|
||||
std::uint8_t device_address = 0;
|
||||
std::uint8_t reserved = 0;
|
||||
std::uint8_t response_function = 0;
|
||||
FunctionCode function = FunctionCode::Unknown;
|
||||
std::uint32_t start_address = 0;
|
||||
std::uint16_t return_byte_count = 0;
|
||||
std::uint8_t status = 0;
|
||||
std::vector<std::uint8_t> payload;
|
||||
Unknown = 0x00,
|
||||
ReadMatrix = 0x01,
|
||||
ReadSingle = 0x02,
|
||||
ReadTemperature = 0x03,
|
||||
SetDeviceId = 0x51,
|
||||
SetMatrixSize = 0x52,
|
||||
CalibrationMode = 0x53,
|
||||
};
|
||||
|
||||
std::optional<TactileFrame> parse_frame(const CPFrame& frame);
|
||||
std::vector<std::uint16_t> parse_pressure_values(const TactileFrame& frame);
|
||||
std::optional<MatrixSize> parse_matrix_size_payload(const TactileFrame& frame);
|
||||
std::optional<MatrixSize> parse_patrix_coordinate_payload(const TactileFrame& frame);
|
||||
|
||||
const CPCodec* tactile_codec();
|
||||
void register_tactile_codec();
|
||||
}
|
||||
struct MatrixSize {
|
||||
std::uint8_t long_edge = 0;
|
||||
std::uint8_t short_edge = 0;
|
||||
};
|
||||
|
||||
struct TactileFrame {
|
||||
std::uint8_t device_address = 0;
|
||||
std::uint8_t reserved = 0;
|
||||
std::uint8_t response_function = 0;
|
||||
FunctionCode function = FunctionCode::Unknown;
|
||||
std::uint32_t start_address = 0;
|
||||
std::uint16_t return_byte_count = 0;
|
||||
std::uint8_t status = 0;
|
||||
std::vector<std::uint8_t> payload;
|
||||
};
|
||||
|
||||
std::optional<TactileFrame> parse_frame(const CPFrame &frame);
|
||||
std::vector<std::uint16_t> parse_pressure_values(const TactileFrame &frame);
|
||||
std::optional<MatrixSize> parse_matrix_size_payload(const TactileFrame &frame);
|
||||
std::optional<MatrixSize>
|
||||
parse_patrix_coordinate_payload(const TactileFrame &frame);
|
||||
|
||||
const CPCodec *tactile_codec();
|
||||
void register_tactile_codec();
|
||||
} // namespace ffmsep::tactile
|
||||
|
||||
44
components/hand-view.cc
Normal file
44
components/hand-view.cc
Normal file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// Created by Lenn on 2025/11/24.
|
||||
//
|
||||
#include "component.hh"
|
||||
#include "base/globalhelper.hh"
|
||||
#include "creeper-qt/utility/theme/theme.hh"
|
||||
#include "creeper-qt/utility/wrapper/layout.hh"
|
||||
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||
#include "creeper-qt/layout/flow.hh"
|
||||
#include "creeper-qt/layout/linear.hh"
|
||||
#include "creeper-qt/widget/cards/basic-card.hh"
|
||||
#include <concepts>
|
||||
#include <creeper-qt/utility/material-icon.hh>
|
||||
#include <creeper-qt/utility/wrapper/mutable-value.hh>
|
||||
#include <creeper-qt/widget/buttons/icon-button.hh>
|
||||
#include <creeper-qt/widget/cards/filled-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/shape/wave-circle.hh>
|
||||
#include <creeper-qt/widget/sliders.hh>
|
||||
#include <creeper-qt/widget/switch.hh>
|
||||
#include <creeper-qt/widget/text-fields.hh>
|
||||
#include <creeper-qt/widget/text.hh>
|
||||
#include <creeper-qt/widget/buttons/icon-button.hh>
|
||||
#include <cstddef>
|
||||
#include <iterator>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qlogging.h>
|
||||
#include <qnamespace.h>
|
||||
#include <ranges>
|
||||
#include <utility>
|
||||
|
||||
using namespace creeper;
|
||||
namespace capro = card::pro;
|
||||
namespace lnpro = linear::pro;
|
||||
namespace ibpro = icon_button::pro;
|
||||
|
||||
auto HandViewComponent(HandViewComponentState& state) noexcept -> raw_pointer<QWidget> {
|
||||
return new FilledCard {
|
||||
capro::ThemeManager{state.manager},
|
||||
capro::SizePolicy {QSizePolicy::Expanding},
|
||||
};
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
#include "component.hh"
|
||||
|
||||
#include "modern-qt/core/application.hh"
|
||||
#include "modern-qt/layout/group.hh"
|
||||
#include "modern-qt/layout/linear.hh"
|
||||
#include "modern-qt/layout/mutual-exclusion-group.hh"
|
||||
#include "modern-qt/utility/material-icon.hh"
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/widget/buttons/icon-button.hh"
|
||||
#include "modern-qt/widget/cards/filled-card.hh"
|
||||
#include "modern-qt/widget/image.hh"
|
||||
#include "creeper-qt/core/application.hh"
|
||||
#include "creeper-qt/layout/group.hh"
|
||||
#include "creeper-qt/layout/linear.hh"
|
||||
#include "creeper-qt/layout/mutual-exclusion-group.hh"
|
||||
#include "creeper-qt/utility/material-icon.hh"
|
||||
#include "creeper-qt/utility/theme/theme.hh"
|
||||
#include "creeper-qt/widget/buttons/icon-button.hh"
|
||||
#include "creeper-qt/widget/cards/filled-card.hh"
|
||||
#include "creeper-qt/widget/image.hh"
|
||||
|
||||
using namespace creeper;
|
||||
namespace fc = filled_card::pro;
|
||||
@@ -26,7 +26,6 @@ auto NavComponent(NavComponentState& state) noexcept -> raw_pointer<QWidget> {
|
||||
im::BorderWidth {3},
|
||||
im::PainterResource {
|
||||
":/images/images/logo.png",
|
||||
// "./images/logo.png",
|
||||
},
|
||||
};
|
||||
state.manager.append_handler(AvatarComponent, [AvatarComponent](const ThemeManager& manager) {
|
||||
@@ -74,7 +73,10 @@ auto NavComponent(NavComponentState& state) noexcept -> raw_pointer<QWidget> {
|
||||
navigation_icons_config,
|
||||
status,
|
||||
ic::FontIcon(icon.data()),
|
||||
ic::Clickable {[=]{state.switch_callback(index, name);}},
|
||||
ic::Clickable {[=] {
|
||||
// state.switch_callback(index, name);
|
||||
state.stacked_callback(index);
|
||||
}},
|
||||
};
|
||||
},
|
||||
Qt::AlignHCenter,
|
||||
|
||||
437
components/setting.cc
Normal file
437
components/setting.cc
Normal file
@@ -0,0 +1,437 @@
|
||||
//
|
||||
// Created by Lenn on 2025/11/21.
|
||||
//
|
||||
#include "component.hh"
|
||||
#include "base/globalhelper.hh"
|
||||
#include "creeper-qt/utility/theme/theme.hh"
|
||||
#include "creeper-qt/utility/wrapper/layout.hh"
|
||||
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||
#include "creeper-qt/layout/flow.hh"
|
||||
#include "creeper-qt/layout/linear.hh"
|
||||
#include "creeper-qt/layout/scroll.hh"
|
||||
#include "creeper-qt/widget/buttons/filled-button.hh"
|
||||
#include "creeper-qt/widget/cards/basic-card.hh"
|
||||
#include <concepts>
|
||||
#include <creeper-qt/utility/material-icon.hh>
|
||||
#include <creeper-qt/utility/wrapper/mutable-value.hh>
|
||||
#include <creeper-qt/widget/buttons/icon-button.hh>
|
||||
#include <creeper-qt/widget/cards/filled-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/shape/wave-circle.hh>
|
||||
#include <creeper-qt/widget/sliders.hh>
|
||||
#include <creeper-qt/widget/switch.hh>
|
||||
#include <creeper-qt/widget/text-fields.hh>
|
||||
#include <creeper-qt/widget/text.hh>
|
||||
#include <creeper-qt/widget/buttons/icon-button.hh>
|
||||
#include <cstddef>
|
||||
#include <iterator>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qlogging.h>
|
||||
#include <qnamespace.h>
|
||||
#include <ranges>
|
||||
#include <utility>
|
||||
#include "globalhelper.hh"
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QSpinBox>
|
||||
#include <QVBoxLayout>
|
||||
#include <sys/stat.h>
|
||||
|
||||
namespace repest_literals {
|
||||
template<class F>
|
||||
concept IndexInvocable = std::invocable<F, std::size_t>;
|
||||
template<IndexInvocable F>
|
||||
void operator*(F&& f, std::size_t n) {
|
||||
std::ranges::for_each(std::views::iota(std::size_t{ 0 }, n), std::forward<F>(f));
|
||||
}
|
||||
template<IndexInvocable F>
|
||||
void operator*(std::size_t n, F&& f) {
|
||||
std::ranges::for_each(std::views::iota(std::size_t{ 0 }, n), std::forward<F>(f));
|
||||
}
|
||||
} // namespace repest_literals
|
||||
|
||||
using namespace creeper;
|
||||
namespace capro = card::pro;
|
||||
namespace lnpro = linear::pro;
|
||||
namespace ibpro = icon_button::pro;
|
||||
namespace fbpro = filled_button::pro;
|
||||
|
||||
static std::weak_ptr<MutableValue<std::vector<ConfigProfile>>> g_profiles_store;
|
||||
static std::function<void()> g_profiles_refresh;
|
||||
|
||||
static void ShowEditProfileDialog(
|
||||
const ConfigProfile& current,
|
||||
const std::shared_ptr<MutableValue<std::vector<ConfigProfile>>>& profiles_store) {
|
||||
QDialog dialog;
|
||||
dialog.setWindowTitle(QStringLiteral("修改配置"));
|
||||
auto* layout = new QVBoxLayout(&dialog);
|
||||
auto* form = new QFormLayout();
|
||||
|
||||
auto* name_edit = new QLineEdit(&dialog);
|
||||
name_edit->setText(current.name);
|
||||
|
||||
auto* type_combo = new QComboBox(&dialog);
|
||||
type_combo->addItem(QStringLiteral("压阻A型"));
|
||||
type_combo->addItem(QStringLiteral("压阻B型"));
|
||||
type_combo->addItem(QStringLiteral("霍尔型"));
|
||||
switch (current.type) {
|
||||
case Tactile_TYPE::PiezoresistiveA:
|
||||
type_combo->setCurrentIndex(0);
|
||||
break;
|
||||
case Tactile_TYPE::PiezoresistiveB:
|
||||
type_combo->setCurrentIndex(1);
|
||||
break;
|
||||
case Tactile_TYPE::Hall:
|
||||
type_combo->setCurrentIndex(2);
|
||||
break;
|
||||
}
|
||||
|
||||
auto* width_spin = new QSpinBox(&dialog);
|
||||
auto* height_spin = new QSpinBox(&dialog);
|
||||
width_spin->setRange(1, 64);
|
||||
height_spin->setRange(1, 64);
|
||||
width_spin->setValue(current.matrix_width);
|
||||
height_spin->setValue(current.matrix_height);
|
||||
|
||||
auto* left_spin = new QSpinBox(&dialog);
|
||||
auto* right_spin = new QSpinBox(&dialog);
|
||||
left_spin->setRange(-100000, 100000);
|
||||
right_spin->setRange(-100000, 100000);
|
||||
left_spin->setValue(current.range_left);
|
||||
right_spin->setValue(current.range_right);
|
||||
|
||||
auto* baud_spin = new QSpinBox(&dialog);
|
||||
baud_spin->setRange(1200, 10000000);
|
||||
baud_spin->setValue(current.baud_rate);
|
||||
|
||||
form->addRow(QStringLiteral("名称"), name_edit);
|
||||
form->addRow(QStringLiteral("类型"), type_combo);
|
||||
form->addRow(QStringLiteral("宽"), width_spin);
|
||||
form->addRow(QStringLiteral("高"), height_spin);
|
||||
form->addRow(QStringLiteral("量程左"), left_spin);
|
||||
form->addRow(QStringLiteral("量程右"), right_spin);
|
||||
form->addRow(QStringLiteral("波特率"), baud_spin);
|
||||
|
||||
layout->addLayout(form);
|
||||
|
||||
auto* buttons =
|
||||
new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &dialog);
|
||||
layout->addWidget(buttons);
|
||||
QObject::connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
|
||||
QObject::connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||||
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
ConfigProfile updated = current;
|
||||
updated.name = name_edit->text();
|
||||
updated.type = [idx = type_combo->currentIndex()] {
|
||||
switch (idx) {
|
||||
case 0:
|
||||
return Tactile_TYPE::PiezoresistiveA;
|
||||
case 1:
|
||||
return Tactile_TYPE::PiezoresistiveB;
|
||||
default:
|
||||
return Tactile_TYPE::Hall;
|
||||
}
|
||||
}();
|
||||
updated.matrix_width = width_spin->value();
|
||||
updated.matrix_height = height_spin->value();
|
||||
updated.range_left = left_spin->value();
|
||||
updated.range_right = right_spin->value();
|
||||
updated.baud_rate = baud_spin->value();
|
||||
|
||||
GlobalHelper::instance().remove_profile(current.name);
|
||||
GlobalHelper::instance().add_new_profile(updated);
|
||||
GlobalHelper::instance().reload_profiles();
|
||||
if (profiles_store) {
|
||||
profiles_store->set(GlobalHelper::instance().get_all_profile());
|
||||
}
|
||||
RefreshProfilesForView();
|
||||
if (g_profiles_refresh) {
|
||||
g_profiles_refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void ShowAddProfileDialog() {
|
||||
QDialog dialog;
|
||||
dialog.setWindowTitle(QStringLiteral("添加配置"));
|
||||
auto* layout = new QVBoxLayout(&dialog);
|
||||
auto* form = new QFormLayout();
|
||||
|
||||
auto* name_edit = new QLineEdit(&dialog);
|
||||
|
||||
auto* type_combo = new QComboBox(&dialog);
|
||||
type_combo->addItem(QStringLiteral("压阻A型"));
|
||||
type_combo->addItem(QStringLiteral("压阻B型"));
|
||||
type_combo->addItem(QStringLiteral("霍尔型"));
|
||||
|
||||
auto* width_spin = new QSpinBox(&dialog);
|
||||
auto* height_spin = new QSpinBox(&dialog);
|
||||
width_spin->setRange(1, 64);
|
||||
height_spin->setRange(1, 64);
|
||||
|
||||
auto* left_spin = new QSpinBox(&dialog);
|
||||
auto* right_spin = new QSpinBox(&dialog);
|
||||
left_spin->setRange(-100000, 100000);
|
||||
right_spin->setRange(-100000, 100000);
|
||||
|
||||
auto* baud_spin = new QSpinBox(&dialog);
|
||||
baud_spin->setRange(1200, 10000000);
|
||||
baud_spin->setValue(115200);
|
||||
|
||||
form->addRow(QStringLiteral("名称"), name_edit);
|
||||
form->addRow(QStringLiteral("类型"), type_combo);
|
||||
form->addRow(QStringLiteral("宽"), width_spin);
|
||||
form->addRow(QStringLiteral("高"), height_spin);
|
||||
form->addRow(QStringLiteral("量程左"), left_spin);
|
||||
form->addRow(QStringLiteral("量程右"), right_spin);
|
||||
form->addRow(QStringLiteral("波特率"), baud_spin);
|
||||
|
||||
layout->addLayout(form);
|
||||
|
||||
auto* buttons =
|
||||
new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &dialog);
|
||||
layout->addWidget(buttons);
|
||||
QObject::connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
|
||||
QObject::connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||||
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
ConfigProfile profile;
|
||||
profile.name = name_edit->text();
|
||||
profile.type = [idx = type_combo->currentIndex()] {
|
||||
switch (idx) {
|
||||
case 0:
|
||||
return Tactile_TYPE::PiezoresistiveA;
|
||||
case 1:
|
||||
return Tactile_TYPE::PiezoresistiveB;
|
||||
default:
|
||||
return Tactile_TYPE::Hall;
|
||||
}
|
||||
}();
|
||||
profile.matrix_width = width_spin->value();
|
||||
profile.matrix_height = height_spin->value();
|
||||
profile.range_left = left_spin->value();
|
||||
profile.range_right = right_spin->value();
|
||||
profile.baud_rate = baud_spin->value();
|
||||
|
||||
GlobalHelper::instance().add_new_profile(profile);
|
||||
GlobalHelper::instance().reload_profiles();
|
||||
if (auto store = g_profiles_store.lock()) {
|
||||
store->set(GlobalHelper::instance().get_all_profile());
|
||||
}
|
||||
RefreshProfilesForView();
|
||||
if (g_profiles_refresh) {
|
||||
g_profiles_refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static auto AddProfileLongItem(creeper::ThemeManager& manager) {
|
||||
return new FilledButton {
|
||||
fbpro::ThemeManager {manager},
|
||||
fbpro::Text {QStringLiteral("添加配置")},
|
||||
widget::pro::SizePolicy {QSizePolicy::Fixed, QSizePolicy::Expanding},
|
||||
widget::pro::MinimumHeight {40},
|
||||
widget::pro::MinimumWidth {320},
|
||||
fbpro::Radius {12},
|
||||
fbpro::Clickable {[]{ ShowAddProfileDialog(); }},
|
||||
};
|
||||
}
|
||||
static auto ProfileItemComponent(creeper::ThemeManager& manager, ConfigProfile& profile,
|
||||
const std::shared_ptr<MutableValue<std::vector<ConfigProfile>>>& profiles_store) {
|
||||
QString matrix_size = "规格:" + QString{ "%1 * %2" }.arg(profile.matrix_width).arg(profile.matrix_height);
|
||||
QString value_range = "量程:" + QString{ "%1 ~ %2" }.arg(profile.range_left).arg(profile.range_right);
|
||||
QString tactile_type = [profile]() -> QString {
|
||||
switch (profile.type) {
|
||||
case Tactile_TYPE::PiezoresistiveA:
|
||||
return "类型:压阻A型";
|
||||
case Tactile_TYPE::PiezoresistiveB:
|
||||
return "类型:压阻B型";
|
||||
case Tactile_TYPE::Hall:
|
||||
return "类型:霍尔型";
|
||||
}
|
||||
return "错误";
|
||||
}();
|
||||
QString baud_rate = "波特率:" + QString::number(profile.baud_rate);
|
||||
return new FilledCard{
|
||||
card::pro::SizePolicy {
|
||||
QSizePolicy::Expanding,
|
||||
},
|
||||
card::pro::MinimumSize {
|
||||
300, 50
|
||||
},
|
||||
// card::pro::FixedSize {
|
||||
// 200, 100
|
||||
// },
|
||||
card::pro::ThemeManager{ manager },
|
||||
card::pro::SizePolicy { QSizePolicy::Expanding },
|
||||
card::pro::LevelLow,
|
||||
card::pro::Layout<Col>{
|
||||
lnpro::Item<Row>{
|
||||
// row::pro::Stretch {1},
|
||||
row::pro::Item<Text>{
|
||||
text::pro::AdjustSize {},
|
||||
text::pro::Text{ QString{ profile.name } },
|
||||
text::pro::Apply{
|
||||
[&manager](Text& self) {
|
||||
manager.append_handler(&self, [&](const ThemeManager& manager) {
|
||||
const auto scheme = manager.color_scheme();
|
||||
self.set_color(scheme.primary);
|
||||
});
|
||||
} },
|
||||
},
|
||||
row::pro::Item<IconButton>{
|
||||
ibpro::ThemeManager{ manager },
|
||||
ibpro::FixedSize{ 30, 30 },
|
||||
ibpro::Font{ material::kRegularExtraSmallFont },
|
||||
icon_button::pro::FontIcon{ material::icon::kBorderColor },
|
||||
ibpro::Clickable{ [profiles_store, current = profile] {
|
||||
ShowEditProfileDialog(current, profiles_store);
|
||||
} },
|
||||
},
|
||||
row::pro::Item<IconButton>{
|
||||
ibpro::ThemeManager{ manager },
|
||||
ibpro::FixedSize{ 30, 30 },
|
||||
ibpro::Font{ material::kRegularExtraSmallFont },
|
||||
icon_button::pro::FontIcon{ material::icon::kDelete },
|
||||
ibpro::Clickable{ [profiles_store, name = profile.name, &manager] {
|
||||
GlobalHelper::instance().remove_profile(name);
|
||||
GlobalHelper::instance().reload_profiles();
|
||||
if (profiles_store) {
|
||||
profiles_store->set(GlobalHelper::instance().get_all_profile());
|
||||
}
|
||||
RefreshProfilesForView();
|
||||
manager.apply_theme();
|
||||
} },
|
||||
}
|
||||
},
|
||||
lnpro::Item<Text>{
|
||||
text::pro::Text{ tactile_type },
|
||||
text::pro::Apply{
|
||||
[&manager](Text& self) {
|
||||
manager.append_handler(&self, [&](const ThemeManager& manager) {
|
||||
const auto scheme = manager.color_scheme();
|
||||
self.set_color(scheme.primary);
|
||||
});
|
||||
} } },
|
||||
lnpro::Item<Text>{
|
||||
text::pro::AdjustSize {},
|
||||
text::pro::Text{ matrix_size },
|
||||
text::pro::Apply{
|
||||
[&manager](Text& self) {
|
||||
manager.append_handler(&self, [&](const ThemeManager& manager) {
|
||||
const auto scheme = manager.color_scheme();
|
||||
self.set_color(scheme.primary);
|
||||
});
|
||||
} }
|
||||
},
|
||||
lnpro::Item<Text>{
|
||||
text::pro::Text{ value_range },
|
||||
text::pro::Apply{
|
||||
[&manager](Text& self) {
|
||||
manager.append_handler(&self, [&](const ThemeManager& manager) {
|
||||
const auto scheme = manager.color_scheme();
|
||||
self.set_color(scheme.primary);
|
||||
});
|
||||
} } },
|
||||
lnpro::Item<Text>{
|
||||
text::pro::Text{ baud_rate },
|
||||
text::pro::Apply{
|
||||
[&manager](Text& self) {
|
||||
manager.append_handler(&self, [&](const ThemeManager& manager) {
|
||||
const auto scheme = manager.color_scheme();
|
||||
self.set_color(scheme.primary);
|
||||
});
|
||||
} }
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static void PopulateProfiles(Flow& flow, creeper::ThemeManager& manager,
|
||||
const std::vector<ConfigProfile>& profiles,
|
||||
const std::shared_ptr<MutableValue<std::vector<ConfigProfile>>>& profiles_store) {
|
||||
while (auto item = flow.takeAt(0)) {
|
||||
if (auto* w = item->widget()) {
|
||||
w->deleteLater();
|
||||
}
|
||||
delete item;
|
||||
}
|
||||
using namespace repest_literals;
|
||||
profiles.size() * [&](auto i) {
|
||||
flow.addWidget(ProfileItemComponent(manager, const_cast<ConfigProfile&>(profiles[i]), profiles_store));
|
||||
};
|
||||
flow.update();
|
||||
}
|
||||
|
||||
static void AttachProfilesObserver(const std::shared_ptr<MutableValue<std::vector<ConfigProfile>>>& store,
|
||||
Flow& flow, creeper::ThemeManager& manager) {
|
||||
struct Functor : creeper::MutableValue<std::vector<ConfigProfile>>::Functor {
|
||||
Flow& flow;
|
||||
creeper::ThemeManager& manager;
|
||||
std::weak_ptr<MutableValue<std::vector<ConfigProfile>>> store_ptr;
|
||||
Functor(Flow& f, creeper::ThemeManager& m,
|
||||
std::weak_ptr<MutableValue<std::vector<ConfigProfile>>> s) noexcept
|
||||
: flow(f)
|
||||
, manager(m)
|
||||
, store_ptr(std::move(s)) { }
|
||||
void update(const std::vector<ConfigProfile>& value) override {
|
||||
PopulateProfiles(flow, manager, value, store_ptr.lock());
|
||||
manager.apply_theme();
|
||||
}
|
||||
};
|
||||
auto functor = std::make_unique<Functor>(flow, manager, store);
|
||||
store->callbacks[&flow] = std::move(functor);
|
||||
auto alive = std::weak_ptr { store->alive };
|
||||
QObject::connect(&flow, &QObject::destroyed, [store, alive](auto* key) {
|
||||
if (alive.lock()) store->callbacks.erase(key);
|
||||
});
|
||||
}
|
||||
|
||||
auto SettingComponent(SettingComponentState& state) noexcept -> raw_pointer<QWidget> {
|
||||
auto profiles_ctx = std::make_shared<MutableValue<std::vector<ConfigProfile>>>(
|
||||
GlobalHelper::instance().get_all_profile()
|
||||
);
|
||||
g_profiles_store = profiles_ctx;
|
||||
g_profiles_refresh = [profiles_ctx]() {
|
||||
if (auto store = profiles_ctx) {
|
||||
store->set(GlobalHelper::instance().get_all_profile());
|
||||
}
|
||||
};
|
||||
|
||||
auto* profiles_flow = new Flow {
|
||||
flow::pro::RowSpacing{ 10 },
|
||||
flow::pro::ColSpacing{ 10 },
|
||||
flow::pro::RowLimit{ 10 },
|
||||
};
|
||||
PopulateProfiles(*profiles_flow, state.manager, profiles_ctx->get(), profiles_ctx);
|
||||
AttachProfilesObserver(profiles_ctx, *profiles_flow, state.manager);
|
||||
|
||||
return new FilledCard {
|
||||
capro::ThemeManager{ state.manager },
|
||||
capro::SizePolicy{ QSizePolicy::Expanding },
|
||||
capro::Layout<Col>{
|
||||
lnpro::Alignment {Qt::AlignTop},
|
||||
lnpro::Margin {10},
|
||||
lnpro::Spacing {10},
|
||||
lnpro::Item{
|
||||
AddProfileLongItem(state.manager)
|
||||
},
|
||||
col::pro::Item<ScrollArea>{
|
||||
scroll::pro::ThemeManager { state.manager },
|
||||
scroll::pro::HorizontalScrollBarPolicy { Qt::ScrollBarAlwaysOff },
|
||||
scroll::pro::Item {
|
||||
profiles_flow
|
||||
},
|
||||
},
|
||||
|
||||
},
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user