From a1f7f337c2c961d79b789a7b0c5203ba8f1168d6 Mon Sep 17 00:00:00 2001 From: lenn Date: Tue, 16 Dec 2025 14:25:48 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A2=9C=E8=89=B2=E4=BF=AE=E6=94=B9=E7=BA=A2?= =?UTF-8?q?=E7=BB=BF=EF=BC=8C=E4=BF=AE=E5=A4=8D=E5=B8=A7=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=8D=A1=E9=A1=BFbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 38 +- components/charts/heatmap.impl.hh | 7 +- components/charts/line_chart.cc | 221 ++++ components/charts/line_chart.hh | 79 ++ components/charts/vector_field.cc | 202 ++++ components/charts/vector_field.hh | 82 ++ components/ffmsep/cpdecoder.hh | 11 +- components/ffmsep/cpstream_core.cc | 154 +-- components/ffmsep/presist/presist.cc | 156 +-- components/ffmsep/tactile/tacdec.cc | 540 +++++++--- components/ffmsep/tactile/tacdec.hh | 11 + components/setting.cc | 171 ++- components/view.cc | 1438 +++++++++++++++----------- examples/cpstream_demo.cc | 2 +- main.cc | 6 +- 15 files changed, 2201 insertions(+), 917 deletions(-) create mode 100644 components/charts/line_chart.cc create mode 100644 components/charts/line_chart.hh create mode 100644 components/charts/vector_field.cc create mode 100644 components/charts/vector_field.hh diff --git a/CMakeLists.txt b/CMakeLists.txt index efe1bfd..38a1c1f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,8 @@ set(TOUCHSENSOR_HEADERS component.hh components/charts/heatmap.hh components/charts/heatmap.impl.hh + components/charts/vector_field.hh + components/charts/line_chart.hh dlog/dlog.hh ${FFMSEP_HEADERS} components/setting.cc @@ -98,7 +100,15 @@ set(TOUCHSENSOR_HEADERS 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} ${UTILITY_SOURCES} ${TOUCHSENSOR_HEADERS} @@ -141,17 +151,17 @@ set(CMAKE_INSTALL_PREFIX "${CMAKE_SOURCE_DIR}/deploy" CACHE PATH "" FORCE) include(GNUInstallDirs) install(TARGETS - touchsensor - creeper-qt - qcustomplot - RUNTIME DESTINATION . - LIBRARY DESTINATION . - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + touchsensor + creeper-qt + qcustomplot + RUNTIME DESTINATION . + LIBRARY DESTINATION . + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} ) if(BUILD_EXAMPLE) install(TARGETS cpstream_demo - RUNTIME DESTINATION . + RUNTIME DESTINATION . ) endif() @@ -161,8 +171,8 @@ if(WIN32) get_filename_component(_qt_bin_dir "${_qt_core_location}" DIRECTORY) find_program(WINDEPLOYQT_EXECUTABLE - NAMES windeployqt windeployqt.exe - HINTS "${_qt_bin_dir}" + NAMES windeployqt windeployqt.exe + HINTS "${_qt_bin_dir}" ) if(WINDEPLOYQT_EXECUTABLE) @@ -170,7 +180,7 @@ if(WIN32) # 安装完之后,对 deploy/touchsensor.exe 跑 windeployqt install(CODE - "execute_process( + "execute_process( COMMAND \"${WINDEPLOYQT_EXECUTABLE}\" --dir \"${CMAKE_INSTALL_PREFIX}\" --no-translations @@ -190,9 +200,9 @@ if(MINGW) get_filename_component(MINGW_BIN_DIR "${CMAKE_CXX_COMPILER}" DIRECTORY) set(MINGW_RUNTIME_DLLS - libstdc++-6.dll - libgcc_s_seh-1.dll - libwinpthread-1.dll + libstdc++-6.dll + libgcc_s_seh-1.dll + libwinpthread-1.dll ) foreach(dll ${MINGW_RUNTIME_DLLS}) diff --git a/components/charts/heatmap.impl.hh b/components/charts/heatmap.impl.hh index 8df9e89..dc7a20e 100644 --- a/components/charts/heatmap.impl.hh +++ b/components/charts/heatmap.impl.hh @@ -127,10 +127,9 @@ public: 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)); // 高值深色 + gradient.setColorStopAt(0.0, QColor(0, 176, 80)); + gradient.setColorStopAt(0.5, QColor(255, 214, 10)); + gradient.setColorStopAt(1.0, QColor(204, 0, 0)); cpmp->setGradient(gradient); cpmp->setDataRange(QCPRange(color_min, color_max)); diff --git a/components/charts/line_chart.cc b/components/charts/line_chart.cc new file mode 100644 index 0000000..0ce1a10 --- /dev/null +++ b/components/charts/line_chart.cc @@ -0,0 +1,221 @@ +// +// Created by Codex on 2025/12/10. +// + +#include "line_chart.hh" +#include +#include +#include +#include + +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& 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 keys(points_.size()); + QVector values(points_.size()); + double min_key = std::numeric_limits::max(); + double max_key = std::numeric_limits::lowest(); + double min_val = std::numeric_limits::max(); + double max_val = std::numeric_limits::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::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); +} diff --git a/components/charts/line_chart.hh b/components/charts/line_chart.hh new file mode 100644 index 0000000..d4a678b --- /dev/null +++ b/components/charts/line_chart.hh @@ -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 +#include +#include +#include +#include +#include + +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& 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 points_; + bool initialized_ = false; + std::optional 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; + +struct MaxPoints: Token { + int count; + explicit MaxPoints(int c): count{ c } { } + void apply(auto& self) const { self.set_max_points(count); } +}; + +using PlotData = DerivedProp, [](auto& self, const auto& vec) { + self.set_data(vec); +}>; + +template +concept trait = std::derived_from; + +CREEPER_DEFINE_CHECKER(trait); + +using namespace widget::pro; +using namespace theme::pro; +} // namespace line_widget::pro + +struct SumLinePlot + : public Declarative> { + using Declarative::Declarative; + void paintEvent(QPaintEvent* event) override; +}; +} // namespace creeper diff --git a/components/charts/vector_field.cc b/components/charts/vector_field.cc new file mode 100644 index 0000000..5a770e6 --- /dev/null +++ b/components/charts/vector_field.cc @@ -0,0 +1,202 @@ +// +// Created by Codex on 2025/12/05. +// + +#include "vector_field.hh" +#include +#include +#include + +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& 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 values(static_cast(expected), 0.0); + for (const auto& item: data_points_) { + const int x = static_cast(item.x); + const int y = static_cast(item.y); + if (x >= 0 && x < width && y >= 0 && y < height) { + const int idx = y * width + x; + values[static_cast(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(y * width + x)]; + }; + + std::vector> grads(static_cast(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(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(width) * 0.5; + const double mid_y = static_cast(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(width) * 0.5; + const double cy = static_cast(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); +} diff --git a/components/charts/vector_field.hh b/components/charts/vector_field.hh new file mode 100644 index 0000000..b24726f --- /dev/null +++ b/components/charts/vector_field.hh @@ -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 +#include +#include +#include +#include + +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& 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 data_points_; + bool initialized_ = false; + std::optional 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; + +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, [](auto& self, const auto& vec) { + self.set_data(vec); +}>; + +template +concept trait = std::derived_from; + +CREEPER_DEFINE_CHECKER(trait); + +using namespace widget::pro; +using namespace theme::pro; +} // namespace vector_widget::pro + +struct VectorFieldPlot + : public Declarative> { + using Declarative::Declarative; + void paintEvent(QPaintEvent* event) override; +}; +} // namespace creeper diff --git a/components/ffmsep/cpdecoder.hh b/components/ffmsep/cpdecoder.hh index a407dce..6152766 100644 --- a/components/ffmsep/cpdecoder.hh +++ b/components/ffmsep/cpdecoder.hh @@ -21,10 +21,11 @@ enum class CPMediaType : std::uint8_t { Data, }; -enum class CPCodecID : std::uint32_t { - Unknow = 0, - Tactile = 0x54514354u // 'T','Q','C','T':触觉传感器协议标识 Tactile Quick Codec Type -}; +enum class CPCodecID : std::uint32_t { + Unknow = 0, + Tactile = 0x54514354u, // 'T','Q','C','T':触觉传感器协议标识 Tactile Quick Codec Type + PiezoresistiveB = 0x54514342u // 'T','Q','C','B':压阻B测试协议 +}; struct CPPacket { std::vector payload; @@ -121,4 +122,4 @@ int cpcodec_close(CPCodecContext*); void cpcodec_free_context(CPCodecContext **ctx); int cpcodec_send_packet(CPCodecContext*, const CPPacket*); int cpcodec_receive_frame(CPCodecContext*, CPFrame*); -} \ No newline at end of file +} diff --git a/components/ffmsep/cpstream_core.cc b/components/ffmsep/cpstream_core.cc index dd7fae4..6331b12 100644 --- a/components/ffmsep/cpstream_core.cc +++ b/components/ffmsep/cpstream_core.cc @@ -2,7 +2,7 @@ #include "components/ffmsep/presist/presist.hh" #include "dlog/dlog.hh" - +#include #include #include #include @@ -11,12 +11,16 @@ #include #include #include +#include #include #include #include +#include #include #include #include +#include +#include using namespace std::chrono_literals; @@ -24,7 +28,7 @@ namespace ffmsep { namespace { -constexpr auto kReaderIdleSleep = 5ms; +constexpr auto kReaderIdleSleep = 5ms; constexpr auto kDecoderIdleSleep = 1ms; const CPCodec* resolve_requested_codec(const CPStreamConfig& config) { @@ -46,13 +50,12 @@ const CPCodec* resolve_requested_codec(const CPStreamConfig& config) { struct CPStreamCore::Impl { struct Packet { std::vector payload; - std::int64_t pts = 0; - bool end_of_stream = false; - bool flush = false; + std::int64_t pts = 0; + bool end_of_stream = false; + bool flush = false; }; - explicit Impl(CPStreamConfig config) - : config_(std::move(config)) { + explicit Impl(CPStreamConfig config): config_(std::move(config)) { normalize_config(); frame_writer_ = std::make_unique(); } @@ -109,13 +112,13 @@ struct CPStreamCore::Impl { try { auto serial = std::make_shared( - config_.port, - config_.baudrate, - config_.timeout, - config_.bytesize, - config_.parity, - config_.stopbits, - config_.flowcontrol); + config_.port, + config_.baudrate, + config_.timeout, + config_.bytesize, + config_.parity, + config_.stopbits, + config_.flowcontrol); if (!serial->isOpen()) { serial->open(); } @@ -125,19 +128,22 @@ struct CPStreamCore::Impl { std::lock_guard lock(serial_mutex_); serial_ = std::move(serial); } - } catch (const serial::IOException& ex) { + } + catch (const serial::IOException& ex) { set_last_error(ex.what() ? ex.what() : "serial IO exception"); cpcodec_close(codec_ctx_); cpcodec_free_context(&codec_ctx_); codec_ctx_ = nullptr; return false; - } catch (const serial::SerialException& ex) { + } + catch (const serial::SerialException& ex) { set_last_error(ex.what() ? ex.what() : "serial exception"); cpcodec_close(codec_ctx_); cpcodec_free_context(&codec_ctx_); codec_ctx_ = nullptr; return false; - } catch (const std::exception& ex) { + } + catch (const std::exception& ex) { set_last_error(ex.what()); cpcodec_close(codec_ctx_); cpcodec_free_context(&codec_ctx_); @@ -177,7 +183,8 @@ struct CPStreamCore::Impl { if (serial_->isOpen()) { serial_->close(); } - } catch (...) { + } + catch (...) { // Ignore close errors. } serial_.reset(); @@ -224,7 +231,7 @@ struct CPStreamCore::Impl { stop_requested_.store(false, std::memory_order_release); running_.store(true, std::memory_order_release); - reader_thread_ = std::thread(&Impl::reader_loop, this); + reader_thread_ = std::thread(&Impl::reader_loop, this); decoder_thread_ = std::thread(&Impl::decoder_loop, this); if (!config_.slave_request_command.empty()) { slave_thread_ = std::thread(&Impl::slave_loop, this); @@ -298,11 +305,14 @@ struct CPStreamCore::Impl { try { const auto written = serial_copy->write(data, size); return written == size; - } catch (const serial::IOException& ex) { + } + catch (const serial::IOException& ex) { 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"); - } catch (const std::exception& ex) { + } + catch (const std::exception& ex) { set_last_error(ex.what()); } return false; @@ -341,7 +351,7 @@ struct CPStreamCore::Impl { } { std::lock_guard lock(frame_mutex_); - frame_queue_capacity_ = capacity; + frame_queue_capacity_ = capacity; config_.frame_queue_capacity = capacity; while (frame_queue_.size() > frame_queue_capacity_) { frame_queue_.pop_front(); @@ -375,12 +385,11 @@ struct CPStreamCore::Impl { if (snapshot.empty()) { std::promise promise; - auto future = promise.get_future(); + auto future = promise.get_future(); promise.set_value(persist::WriteResult{ - false, - "no recorded frames available", - path - }); + false, + "no recorded frames available", + path }); return future; } @@ -409,6 +418,7 @@ struct CPStreamCore::Impl { std::vector buffer(config_.read_chunk_size); while (!stop_requested_.load(std::memory_order_acquire)) { + std::shared_ptr serial_copy; { std::lock_guard lock(serial_mutex_); @@ -422,15 +432,19 @@ struct CPStreamCore::Impl { std::size_t bytes_read = 0; try { 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"); std::this_thread::sleep_for(kReaderIdleSleep); continue; - } catch (const serial::SerialException& ex) { + } + catch (const serial::SerialException& ex) { set_last_error(ex.what() ? ex.what() : "serial exception"); std::this_thread::sleep_for(kReaderIdleSleep); continue; - } catch (const std::exception& ex) { + } + catch (const std::exception& ex) { set_last_error(ex.what()); std::this_thread::sleep_for(kReaderIdleSleep); continue; @@ -440,9 +454,26 @@ struct CPStreamCore::Impl { std::this_thread::sleep_for(kReaderIdleSleep); continue; } - + const auto format_command = + [](const std::vector& 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(data[idx]); + } + oss << ']'; + return oss.str(); + }; Packet packet; packet.payload.assign(buffer.begin(), buffer.begin() + static_cast(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); { @@ -457,8 +488,8 @@ struct CPStreamCore::Impl { } void slave_loop() { - const auto command = config_.slave_request_command; - auto interval = config_.slave_request_interval; + const auto command = config_.slave_request_command; + auto interval = config_.slave_request_interval; if (interval.count() < 0) { interval = 0ms; } @@ -512,11 +543,11 @@ struct CPStreamCore::Impl { } CPPacket cp_packet; - cp_packet.payload = std::move(packet.payload); - cp_packet.pts = packet.pts; - cp_packet.dts = packet.pts; + cp_packet.payload = std::move(packet.payload); + cp_packet.pts = packet.pts; + cp_packet.dts = packet.pts; cp_packet.end_of_stream = packet.end_of_stream; - cp_packet.flush = packet.flush; + cp_packet.flush = packet.flush; int rc = cpcodec_send_packet(codec_ctx_, &cp_packet); if (rc < CP_SUCCESS) { @@ -530,20 +561,24 @@ struct CPStreamCore::Impl { CPFrame frame; rc = cpcodec_receive_frame(codec_ctx_, &frame); if (rc == CP_SUCCESS) { - auto decoded = std::make_shared(); - decoded->pts = frame.pts; + auto decoded = std::make_shared(); + 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; + 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 = parsed; decoded->tactile_pressures = tactile::parse_pressure_values(*parsed); if (auto matrix = tactile::parse_matrix_size_payload(*parsed)) { decoded->tactile_matrix_size = matrix; } } } + else if (decoded->id == CPCodecID::PiezoresistiveB) { + decoded->tactile_pressures = + tactile::parse_piezoresistive_b_pressures(decoded->frame); + } FrameCallback callback_copy; { @@ -560,14 +595,16 @@ struct CPStreamCore::Impl { frame_queue_.pop_front(); } 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_cv_.notify_one(); - } else if (rc == CP_ERROR_EAGAIN) { + } + else if (rc == CP_ERROR_EAGAIN) { break; - } else { + } + else { if (rc == CP_ERROR_EOF && packet.end_of_stream) { return; } @@ -583,7 +620,7 @@ struct CPStreamCore::Impl { void signal_decoder_flush(bool end_of_stream) { Packet packet; - packet.flush = true; + packet.flush = true; packet.end_of_stream = end_of_stream; { std::lock_guard lock(packet_mutex_); @@ -612,7 +649,7 @@ struct CPStreamCore::Impl { const CPCodec* codec_descriptor_ = nullptr; std::shared_ptr serial_; - mutable std::mutex serial_mutex_; + mutable std::mutex serial_mutex_; CPCodecContext* codec_ctx_ = nullptr; @@ -620,33 +657,32 @@ struct CPStreamCore::Impl { std::thread slave_thread_; std::thread decoder_thread_; - std::mutex packet_mutex_; + std::mutex packet_mutex_; std::condition_variable packet_cv_; - std::deque packet_queue_; + std::deque packet_queue_; - mutable std::mutex frame_mutex_; + mutable std::mutex frame_mutex_; std::condition_variable frame_cv_; // std::deque frame_queue_; // 更新为智能指针,我们需要更长的生命周期😊 std::deque> frame_queue_; std::deque> frame_record_queue_; - std::size_t frame_queue_capacity_ = 16; + std::size_t frame_queue_capacity_ = 16; - FrameCallback frame_callback_; + FrameCallback frame_callback_; mutable std::mutex callback_mutex_; - std::atomic running_{false}; - std::atomic stop_requested_{false}; - std::atomic pts_counter_{0}; + std::atomic running_{ false }; + std::atomic stop_requested_{ false }; + std::atomic pts_counter_{ 0 }; - std::string last_error_; + std::string last_error_; mutable std::mutex last_error_mutex_; std::unique_ptr frame_writer_; }; -CPStreamCore::CPStreamCore(CPStreamConfig config) - : impl_(std::make_unique(std::move(config))) {} +CPStreamCore::CPStreamCore(CPStreamConfig config): impl_(std::make_unique(std::move(config))) {} CPStreamCore::~CPStreamCore() { if (impl_) { diff --git a/components/ffmsep/presist/presist.cc b/components/ffmsep/presist/presist.cc index 1534910..021b0f8 100644 --- a/components/ffmsep/presist/presist.cc +++ b/components/ffmsep/presist/presist.cc @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include #include @@ -20,6 +22,33 @@ namespace { 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& 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(payload[idx]) | static_cast(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) { if (!value.is_array()) { return false; @@ -30,11 +59,11 @@ bool is_simple_array(const json& value) { } 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(indent), ' '); - const auto child_indent = indent + indent_step; +const json& value, +int indent = 0, +int indent_step = 2) { + const auto indent_str = std::string(static_cast(indent), ' '); + const auto child_indent = indent + indent_step; const auto child_indent_str = std::string(static_cast(child_indent), ' '); if (value.is_object()) { @@ -48,7 +77,8 @@ void dump_compact_json(std::ostream& out, out << child_indent_str << json(it.key()).dump() << ": "; dump_compact_json(out, it.value(), child_indent, indent_step); } - out << '\n' << indent_str << '}'; + out << '\n' + << indent_str << '}'; return; } @@ -72,7 +102,7 @@ void dump_compact_json(std::ostream& out, out << "[\n"; bool first = true; - for (const auto& item : value) { + for (const auto& item: value) { if (!first) { out << ",\n"; } @@ -80,7 +110,8 @@ void dump_compact_json(std::ostream& out, out << child_indent_str; dump_compact_json(out, item, child_indent, indent_step); } - out << '\n' << indent_str << ']'; + out << '\n' + << indent_str << ']'; return; } @@ -89,38 +120,37 @@ void dump_compact_json(std::ostream& out, json serialize_tactile_frame(const DecodedFrame& frame) { json result = { - {"pts", frame.pts}, - {"raw_frame", frame.frame.data}, - {"pressures", frame.tactile_pressures}, + { "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(received).count(); + std::chrono::duration_cast(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}, + { "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(tactile.function)}, - {"start_address", tactile.start_address}, - {"return_byte_count", tactile.return_byte_count}, - {"status", tactile.status}, - {"payload", tactile.payload}, + result["tactile"] = { + { "device_address", tactile.device_address }, + { "response_function", tactile.response_function }, + { "function", static_cast(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) { @@ -158,21 +188,20 @@ void WriteQueue::stop() { cond_.notify_all(); } -JsonWritter::JsonWritter() - : write_thread_([this] { run(); }) {} +JsonWritter::JsonWritter(): write_thread_([this] { run(); }) {} JsonWritter::~JsonWritter() { stop(); } std::future JsonWritter::enqueue(std::string path, - std::deque> frames) { +std::deque> frames) { std::promise promise; - auto future = promise.get_future(); + auto future = promise.get_future(); - WriteRequest request{std::move(path), std::move(frames), std::move(promise)}; + 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}; + WriteResult result{ false, "writer has been stopped", request.path }; request.promise.set_value(std::move(result)); } @@ -185,62 +214,67 @@ void JsonWritter::run() { 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}); + } + 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> frames) { +std::deque> frames) { if (path.empty()) { - 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); 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}; + 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}; + return { false, "failed to open export file", path }; + } + + bool wrote_any = false; + for (const auto& frame: frames) { + if (!frame) { + continue; + } + + std::vector 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; } - dump_compact_json(stream, root); - stream << '\n'; stream.flush(); 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 }; } void JsonWritter::stop() { diff --git a/components/ffmsep/tactile/tacdec.cc b/components/ffmsep/tactile/tacdec.cc index 978f8cf..3e1f76e 100644 --- a/components/ffmsep/tactile/tacdec.cc +++ b/components/ffmsep/tactile/tacdec.cc @@ -1,31 +1,61 @@ -#include "tacdec.hh" -#include "components/ffmsep/cpdecoder.hh" +#include "tacdec.hh" +#include "components/ffmsep/cpdecoder.hh" #include #include -#include -#include -#include -#include -#include - +#include +#include +#include +#include +#include +#include +#include +#include +#include + namespace ffmsep::tactile { namespace { -constexpr std::size_t kHeaderSize = 4U; // start bytes + length field -constexpr std::size_t kFixedSectionSize = 1U + 1U + 1U + 4U + 2U + 1U; // address..status -constexpr std::size_t kMinimumFrameSize = kHeaderSize + kFixedSectionSize + 1U; // + CRC byte -constexpr std::uint8_t kCrcPolynomial = 0x07U; -constexpr std::uint8_t kCrcInitial = 0x00U; -constexpr std::uint8_t kCrcXorOut = 0xA9U; +constexpr std::size_t kHeaderSize = 4U; // start bytes + length field +constexpr std::size_t kFixedSectionSize = 1U + 1U + 1U + 4U + 2U + 1U; // address..status +constexpr std::size_t kMinimumFrameSize = kHeaderSize + kFixedSectionSize + 1U; // + CRC byte +constexpr std::uint8_t kCrcPolynomial = 0x07U; +constexpr std::uint8_t kCrcInitial = 0x00U; +constexpr std::uint8_t kCrcXorOut = 0xA9U; constexpr std::array kStartSequence{ kStartByteFirst, kStartByteSecond }; +constexpr std::size_t kAbsoluteMaxPayloadBytes = 4096U; // 硬上限,防止异常配置撑爆内存 +constexpr std::array kPiezoresistiveBStartSequence{ + kPiezoresistiveBStartByteFirst, + kPiezoresistiveBStartByteSecond +}; +constexpr std::array kPiezoresistiveBEndSequence{ + kPiezoresistiveBEndByteFirst, + kPiezoresistiveBEndByteSecond +}; +constexpr std::size_t kPiezoresistiveBPayloadSize = +kPiezoresistiveBValueCount * 2U; +constexpr std::size_t kPiezoresistiveBFrameSize = +kPiezoresistiveBStartSequence.size() + kPiezoresistiveBPayloadSize + kPiezoresistiveBEndSequence.size(); struct TactileDecoderContext { std::vector fifo; - bool end_of_stream = false; - std::int64_t next_pts = 0; + bool end_of_stream = false; + 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::max(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& buf) { @@ -33,67 +63,130 @@ const std::uint8_t* buffer_data(const std::vector& buf) { } std::uint8_t crc8_with_xorout(const std::uint8_t* data, std::size_t length) { +#if 0 std::uint8_t reg = kCrcInitial; for (std::size_t i = 0; i < length; ++i) { reg ^= data[i]; for (int bit = 0; bit < 8; ++bit) { if ((reg & 0x80U) != 0U) { reg = static_cast((reg << 1U) ^ kCrcPolynomial); - } else { + } + else { reg = static_cast(reg << 1U); } } } return static_cast(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(reg ^ data[idx]); + for (int bit = 0; bit < 8; ++bit) { + if ((reg & 0x80U) != 0U) { + reg = static_cast((reg << 1U) ^ kPolynomial); + } + else { + reg = static_cast(reg << 1U); + } + } + } + return static_cast(reg ^ kXorOut); } TactileDecoderContext* get_priv(CPCodecContext* ctx) { return ctx ? ctx->priv_as() : nullptr; } - -int tactile_init(CPCodecContext* ctx) { - if (!ctx) { - return CP_ERROR_INVALID_ARGUMENT; - } - if (!ctx->priv_data) { - ctx->ensure_priv_storage(sizeof(TactileDecoderContext)); - } - auto* storage = static_cast(ctx->priv_data); - new (storage) TactileDecoderContext(); - return CP_SUCCESS; -} - -void tactile_close(CPCodecContext* ctx) { - if (!ctx || !ctx->priv_data) { - return; - } - if (auto* priv = get_priv(ctx); priv != nullptr) { - priv->~TactileDecoderContext(); - } -} - -int tactile_send_packet(CPCodecContext* ctx, const CPPacket& packet) { - auto priv = get_priv(ctx); - if (!priv) { - return CP_ERROR_INVALID_STATE; - } - if (packet.flush) { - priv->fifo.clear(); - priv->end_of_stream = false; - priv->next_pts = 0; - } - - if (!packet.payload.empty()) { - priv->fifo.insert(priv->fifo.end(), packet.payload.begin(), packet.payload.end()); - } - - if (packet.end_of_stream) { - priv->end_of_stream = true; - } - - return CP_SUCCESS; -} - + +template +void keep_partial_start_prefix(std::vector& buf, const std::array& start_sequence) { + if (buf.empty() || N == 0U) { + return; + } + const std::size_t max_prefix = std::min(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(len); + const auto buf_begin = + buf.end() - static_cast(len); + if (std::equal(seq_begin, seq_end, buf_begin)) { + std::vector tail(buf_begin, buf.end()); + buf.swap(tail); + return; + } + } + buf.clear(); +} + +void trim_fifo_if_needed(std::vector& 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(excess)); +} + +std::atomic& expected_payload_bytes_for_tactile() { + static std::atomic expected{kPiezoresistiveBPayloadSize}; + return expected; +} + +int tactile_init(CPCodecContext* ctx) { + if (!ctx) { + return CP_ERROR_INVALID_ARGUMENT; + } + if (!ctx->priv_data) { + ctx->ensure_priv_storage(sizeof(TactileDecoderContext)); + } + auto* storage = static_cast(ctx->priv_data); + 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; +} + +void tactile_close(CPCodecContext* ctx) { + if (!ctx || !ctx->priv_data) { + return; + } + if (auto* priv = get_priv(ctx); priv != nullptr) { + priv->~TactileDecoderContext(); + } +} + +int tactile_send_packet(CPCodecContext* ctx, const CPPacket& packet) { + auto priv = get_priv(ctx); + if (!priv) { + return CP_ERROR_INVALID_STATE; + } + if (packet.flush) { + priv->fifo.clear(); + priv->end_of_stream = false; + priv->next_pts = 0; + } + + if (!packet.payload.empty()) { + priv->fifo.insert(priv->fifo.end(), packet.payload.begin(), packet.payload.end()); + trim_fifo_if_needed(priv->fifo, priv->max_fifo_bytes); + } + + if (packet.end_of_stream) { + priv->end_of_stream = true; + } + + return CP_SUCCESS; +} + int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) { auto* priv = get_priv(ctx); if (!priv) { @@ -111,10 +204,9 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) { return CP_ERROR_EAGAIN; } - const auto start_it = std::search(buf.begin(), buf.end(), - kStartSequence.begin(), kStartSequence.end()); + const auto start_it = std::search(buf.begin(), buf.end(), kStartSequence.begin(), kStartSequence.end()); if (start_it == buf.end()) { - buf.clear(); + keep_partial_start_prefix(buf, kStartSequence); if (priv->end_of_stream) { priv->end_of_stream = false; return CP_ERROR_EOF; @@ -142,8 +234,7 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) { } const std::uint16_t data_length = - static_cast(data[2]) | - static_cast(static_cast(data[3]) << 8U); + static_cast(data[2]) | static_cast(static_cast(data[3]) << 8U); if (data_length < kFixedSectionSize) { 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(data_length) + 1U; + if (total_frame_length > priv->max_frame_bytes) { + buf.erase(buf.begin()); + continue; + } if (buf.size() < total_frame_length) { if (priv->end_of_stream) { buf.clear(); @@ -160,129 +255,280 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) { return CP_ERROR_EAGAIN; } - const std::uint8_t computed_crc = crc8_with_xorout(data + kHeaderSize, data_length); - const std::uint8_t frame_crc = data[kHeaderSize + static_cast(data_length)]; + const auto crc_offset = total_frame_length - 1U; + 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) { buf.erase(buf.begin()); continue; } frame.data.assign(buf.begin(), buf.begin() + static_cast(total_frame_length)); - frame.pts = priv->next_pts++; + frame.pts = priv->next_pts++; frame.key_frame = true; - frame.valid = true; + frame.valid = true; buf.erase(buf.begin(), buf.begin() + static_cast(total_frame_length)); return CP_SUCCESS; } } - -const CPCodec kTactileCodec { - .name = "tactile_serial", - .long_name = "Framed tactile sensor serial protocol decoder", - .type = CPMediaType::Data, - .id = CPCodecID::Tactile, - .priv_data_size = sizeof(TactileDecoderContext), - .init = &tactile_init, - .close = &tactile_close, - .send_packet = &tactile_send_packet, - .receive_frame = &tactile_receive_frame -}; -} - -std::optional parse_frame(const CPFrame& frame) { - if (!frame.valid || frame.data.size() < kMinimumFrameSize) { - return std::nullopt; + +int tactile_b_receive_frame(CPCodecContext* ctx, CPFrame& frame) { + auto* priv = get_priv(ctx); + if (!priv) { + return CP_ERROR_INVALID_STATE; } - const auto* bytes = frame.data.data(); - const std::size_t size = frame.data.size(); + + 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(end_offset); + if (!std::equal(end_it, + end_it + static_cast(kPiezoresistiveBEndSequence.size()), + kPiezoresistiveBEndSequence.begin())) { + buf.erase(buf.begin()); + continue; + } + + frame.data.assign(buf.begin(), + buf.begin() + static_cast(kPiezoresistiveBFrameSize)); + frame.pts = priv->next_pts++; + frame.key_frame = true; + frame.valid = true; + + buf.erase(buf.begin(), + buf.begin() + static_cast(kPiezoresistiveBFrameSize)); + return CP_SUCCESS; + } +} + +const CPCodec kTactileCodec{ + .name = "tactile_serial", + .long_name = "Framed tactile sensor serial protocol decoder", + .type = CPMediaType::Data, + .id = CPCodecID::Tactile, + .priv_data_size = sizeof(TactileDecoderContext), + .init = &tactile_init, + .close = &tactile_close, + .send_packet = &tactile_send_packet, + .receive_frame = &tactile_receive_frame +}; + +const CPCodec 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 parse_frame(const CPFrame& frame) { + // if (!frame.valid || frame.data.size() < kMinimumFrameSize) { + // return std::nullopt; + // } + std::cout << "frame valid:" << frame.valid << ", frame.data.size:" << frame.data.size() << std::endl; + + const auto* bytes = frame.data.data(); + const std::size_t size = frame.data.size(); if (bytes[0] != kStartByteFirst || bytes[1] != kStartByteSecond) { return std::nullopt; } - + std::cout << "frame valid1:" << frame.valid << ", frame.data.size1:" << frame.data.size() << std::endl; const std::uint16_t data_length = - static_cast(bytes[2]) | - static_cast(static_cast(bytes[3]) << 8U); + static_cast(bytes[2]) | static_cast(static_cast(bytes[3]) << 8U); + qDebug() << "data_length: " << data_length; if (data_length < kFixedSectionSize) { 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(data_length) + 1U; if (size != expected_size) { return std::nullopt; } - - const std::uint8_t crc_byte = bytes[size - 1U]; - const std::uint8_t computed_crc = crc8_with_xorout(bytes + kHeaderSize, data_length); + std::cout << "frame valid3:" << frame.valid << ", frame.data.size1:" << frame.data.size() << std::endl; + const std::uint8_t crc_byte = bytes[expected_size - 1U]; + const std::uint8_t computed_crc = + crc8_with_xorout(bytes, expected_size - 1U); // header..last payload byte if (computed_crc != crc_byte) { return std::nullopt; } - - const std::uint8_t device_address = bytes[4]; - const std::uint8_t reserved = bytes[5]; - const std::uint8_t response_function = bytes[6]; + 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 reserved = bytes[5]; + const std::uint8_t response_function = bytes[6]; const std::uint32_t start_address = - static_cast(bytes[7]) | - (static_cast(bytes[8]) << 8U) | - (static_cast(bytes[9]) << 16U) | - (static_cast(bytes[10]) << 24U); + static_cast(bytes[7]) | (static_cast(bytes[8]) << 8U) | (static_cast(bytes[9]) << 16U) | (static_cast(bytes[10]) << 24U); const std::uint16_t return_byte_count = - static_cast(bytes[11]) | - (static_cast(bytes[12]) << 8U); + static_cast(bytes[11]) | (static_cast(bytes[12]) << 8U); const std::uint8_t status = bytes[13]; const std::size_t payload_offset = kHeaderSize + kFixedSectionSize; - const std::size_t payload_length = static_cast(data_length) - kFixedSectionSize; - if (payload_length != return_byte_count) { + const std::size_t payload_available = + data_length > kFixedSectionSize ? static_cast(data_length) - kFixedSectionSize : 0U; + const std::size_t requested_payload = static_cast(return_byte_count); + if (payload_available < requested_payload) { return std::nullopt; } - + std::cout << "frame valid5:" << frame.valid << ", frame.data.size1:" << frame.data.size() << std::endl; TactileFrame parsed{}; - parsed.device_address = device_address; - parsed.reserved = reserved; + parsed.device_address = device_address; + parsed.reserved = reserved; parsed.response_function = response_function; - parsed.function = static_cast(response_function & 0x7FU); - parsed.start_address = start_address; + parsed.function = static_cast(response_function & 0x7FU); + parsed.start_address = start_address; parsed.return_byte_count = return_byte_count; - parsed.status = status; - parsed.payload.assign(bytes + payload_offset, bytes + payload_offset + payload_length); + parsed.status = status; + parsed.payload.assign(bytes + payload_offset, bytes + payload_offset + requested_payload); return parsed; } - + std::vector parse_pressure_values(const TactileFrame& frame) { - if (frame.payload.size() != frame.return_byte_count) { + std::cout << "parse_pressure_values" << std::endl; + const auto requested_bytes = static_cast(frame.return_byte_count); + const auto usable_bytes = std::min(requested_bytes, frame.payload.size()); + if (usable_bytes == 0U || (usable_bytes % 2U != 0U)) { return {}; } - if (frame.payload.empty() || (frame.payload.size() % 2U != 0U)) { + std::vector values; + values.reserve(usable_bytes / 2U); + for (std::size_t idx = 0; idx + 1U < usable_bytes; idx += 2U) { + const std::uint16_t value = static_cast( + static_cast(frame.payload[idx]) | static_cast(frame.payload[idx + 1U] << 8U)); + values.push_back(value); + } + return values; +} + +std::optional parse_matrix_size_payload(const TactileFrame& frame) { + if (frame.payload.size() != 2U) { + return std::nullopt; + } + MatrixSize size{}; + size.long_edge = frame.payload[0]; + size.short_edge = frame.payload[1]; + return size; +} + +std::vector 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 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(frame.data[base]); + const auto lo = static_cast(frame.data[base + 1U]); + values.push_back(static_cast((hi << 8U) | lo)); + } + return values; +} + +std::vector extract_piezoresistive_b_payload(const CPFrame& frame) { + if (!frame.valid) { return {}; } - std::vector values; - values.reserve(frame.payload.size() / 2U); - for (std::size_t idx = 0; idx + 1U < frame.payload.size(); idx += 2U) { - const std::uint16_t value = static_cast( - static_cast(frame.payload[idx]) | - static_cast(frame.payload[idx + 1U] << 8U)); - values.push_back(value); - } - return values; -} - -std::optional parse_matrix_size_payload(const TactileFrame& frame) { - if (frame.payload.size() != 2U) { - return std::nullopt; - } - MatrixSize size{}; - size.long_edge = frame.payload[0]; - size.short_edge = frame.payload[1]; - return size; -} - -const CPCodec* tactile_codec() { - return &kTactileCodec; -} - -void register_tactile_codec() { - cpcodec_register(&kTactileCodec); -} -} + 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( + frame.data.begin() + static_cast(payload_offset), + frame.data.begin() + static_cast(payload_end)); +} + +void set_tactile_expected_payload_bytes(std::size_t bytes) { + const auto clamped = std::min( + std::max(bytes, 2U), + kAbsoluteMaxPayloadBytes); + expected_payload_bytes_for_tactile().store(clamped, std::memory_order_relaxed); +} + +const CPCodec* tactile_codec() { + return &kTactileCodec; +} + +void register_tactile_codec() { + cpcodec_register(&kTactileCodec); +} + +const CPCodec* tactile_b_codec() { + return &kTactileBCodec; +} + +void register_tactile_b_codec() { + cpcodec_register(&kTactileBCodec); +} +} // namespace ffmsep::tactile diff --git a/components/ffmsep/tactile/tacdec.hh b/components/ffmsep/tactile/tacdec.hh index 4b0e6f0..338b2ed 100644 --- a/components/ffmsep/tactile/tacdec.hh +++ b/components/ffmsep/tactile/tacdec.hh @@ -8,6 +8,11 @@ namespace ffmsep::tactile { inline constexpr std::uint8_t kStartByteFirst = 0xAA; 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 { Unknown = 0x00, @@ -40,7 +45,13 @@ std::vector parse_pressure_values(const TactileFrame &frame); std::optional parse_matrix_size_payload(const TactileFrame &frame); std::optional parse_patrix_coordinate_payload(const TactileFrame &frame); +std::vector parse_piezoresistive_b_pressures(const CPFrame &frame); +std::vector 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(); void register_tactile_codec(); +const CPCodec *tactile_b_codec(); +void register_tactile_b_codec(); } // namespace ffmsep::tactile diff --git a/components/setting.cc b/components/setting.cc index 1441f4b..58b7cca 100644 --- a/components/setting.cc +++ b/components/setting.cc @@ -42,6 +42,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include namespace repest_literals { @@ -66,6 +72,61 @@ namespace fbpro = filled_button::pro; static std::weak_ptr>> g_profiles_store; static std::function 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( const ConfigProfile& current, const std::shared_ptr>>& profiles_store) { @@ -252,7 +313,75 @@ static auto ImportProfileLongItem(creeper::ThemeManager& manager) { widget::pro::MinimumHeight {40}, widget::pro::MinimumWidth {320}, 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 imported_profiles; + imported_profiles.reserve(static_cast(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) { @@ -263,7 +392,45 @@ static auto ExportProfileLongItem(creeper::ThemeManager& manager) { widget::pro::MinimumHeight {40}, widget::pro::MinimumWidth {320}, 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(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, diff --git a/components/view.cc b/components/view.cc index 8c8ede1..5c6f71a 100644 --- a/components/view.cc +++ b/components/view.cc @@ -1,475 +1,602 @@ -// -// Created by Lenn on 2025/10/14. -// - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +// +// Created by Lenn on 2025/10/14. +// + +#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/wrapper/layout.hh" +#include "creeper-qt/utility/wrapper/widget.hh" +#include #include #include +#include #include #include -#include +#include +#include #include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include #include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include "component.hh" -#include "cpstream_core.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 "components/charts/heatmap.hh" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "components/ffmsep/presist/presist.hh" -#include "components/ffmsep/tactile/tacdec.hh" -#include - -#define DEBUG 0 - -using namespace creeper; -namespace capro = card::pro; -namespace lnpro = linear::pro; -namespace impro = image::pro; -namespace ibpro = icon_button::pro; -namespace fbpro = filled_button::pro; -namespace dmpro = dropdown_menu::pro; -namespace pwpro = plot_widget::pro; - -namespace { - -constexpr std::array kSlaveRequestCommand{ - 0x55, - 0xAA, - 0x09, - 0x00, - 0x34, - 0x00, - 0xFB, - 0x00, - 0x1C, - 0x00, - 0x00, - 0x18, - 0x00, - 0x7A -}; - -QVector make_flat_points(const QSize& size, double value = 0.0) { - const int width = std::max(size.width(), 1); - const int height = std::max(size.height(), 1); - QVector points; - points.reserve(static_cast(width * height)); - for (int y = 0; y < height; ++y) { - for (int x = 0; x < width; ++x) { - points.append(PointData{ - static_cast(x), - static_cast(y), - value }); - } - } - return points; -} - -std::once_flag& codec_registration_flag() { - static std::once_flag flag; - return flag; -} - -class SensorStreamController: public QObject { - public: - SensorStreamController(std::shared_ptr>> heatmap_data, - std::shared_ptr> matrix_context, - QObject* parent = nullptr): QObject(parent), heatmap_data_(std::move(heatmap_data)), matrix_context_(std::move(matrix_context)) { - std::call_once(codec_registration_flag(), [] { - ffmsep::tactile::register_tactile_codec(); - }); - } - - ~SensorStreamController() override { - reset_core(); - } - - bool start(const QString& requested_port, std::uint32_t baudrate) { - if (is_connected()) { - return true; - } - - const auto ports = ffmsep::CPStreamCore::list_available_ports(); - std::string port_utf8; - if (!requested_port.isEmpty()) { - port_utf8 = requested_port.toStdString(); - const auto it = std::find_if( - ports.begin(), ports.end(), [&](const serial::PortInfo& info) { return info.port == port_utf8; }); - if (it == ports.end()) { - if (ports.empty()) { -#if DEBUG - std::cerr << "SensorStreamController: requested port '" << port_utf8 << "' not available and no other ports detected.\n"; -#endif - last_error_ = QString::fromUtf8("未检测到串口"); - return false; - } -#if DEBUG - std::cerr << "SensorStreamController: requested port '" << port_utf8 << "' not available, falling back to first detected port.\n"; -#endif - port_utf8 = ports.front().port; - } - } - else if (!ports.empty()) { - port_utf8 = ports.front().port; - } - else { -#if DEBUG - std::cerr << "SensorStreamController: no serial ports available\n"; -#endif - last_error_ = QString::fromUtf8("未检测到串口"); - return false; - } - - const std::uint32_t baud = baudrate == 0U ? 115200U : baudrate; - - ffmsep::CPStreamConfig cfg; - cfg.port = port_utf8; - cfg.baudrate = baud; - cfg.codec_id = ffmsep::CPCodecID::Tactile; - cfg.read_chunk_size = 256; - cfg.packet_queue_capacity = 128; - cfg.frame_queue_capacity = 32; - cfg.slave_request_command.assign(kSlaveRequestCommand.begin(), kSlaveRequestCommand.end()); - cfg.slave_request_interval = 3ms; - - reset_core(); - core_ = std::make_unique(); - - if (!core_->open(cfg)) { - last_error_ = QString::fromStdString(core_->last_error()); - std::cerr << "SensorStreamController: open failed - " << core_->last_error() << "\n"; - reset_core(); - return false; - } - - core_->set_frame_callback([this](std::shared_ptr frame) { - handle_frame(frame); - }); - - if (!core_->start()) { - last_error_ = QString::fromStdString(core_->last_error()); - std::cerr << "SensorStreamController: start failed - " << core_->last_error() << "\n"; - reset_core(); - return false; - } - - active_port_ = QString::fromStdString(cfg.port); - last_error_.clear(); - connected_ = true; - return true; - } - - void stop() { - if (!core_) { - active_port_.clear(); - if (heatmap_data_ && matrix_context_) { - heatmap_data_->set(make_flat_points(matrix_context_->get())); - } - connected_ = false; - return; - } - - core_->set_frame_callback({}); - if (core_->is_running()) { - core_->stop(); - } - - core_->clear_frames(); - - connected_ = false; - active_port_.clear(); - if (heatmap_data_ && matrix_context_) { - heatmap_data_->set(make_flat_points(matrix_context_->get())); - } - } - - [[nodiscard]] bool is_running() const noexcept { - return core_ && core_->is_running(); - } - - [[nodiscard]] bool is_connected() const noexcept { - return connected_; - } - - [[nodiscard]] QString active_port() const { - return active_port_; - } - - [[nodiscard]] QString last_error() const { - return last_error_; - } - - std::future export_frames(const QString& path, - bool clear_after_export) { - if (path.isEmpty()) { - return make_failed_future(path, "export path is empty"); - } - if (!core_) { - return make_failed_future(path, "stream is not active"); - } - if (core_->recorded_frame_count() == 0U) { - return make_failed_future(path, "no tactile frames recorded"); - } - const auto normalized = QDir::toNativeSeparators(path); - return core_->export_recorded_frames(normalized.toStdString(), clear_after_export); - } - - private: - void reset_core() { - connected_ = false; - if (!core_) { - return; - } - core_->set_frame_callback({}); - if (core_->is_running()) { - core_->stop(); - } - if (core_->is_open()) { - core_->close(); - } - core_.reset(); - } - - static QSize to_qsize(const ffmsep::tactile::MatrixSize& m) { - return QSize{ - static_cast(m.long_edge), - static_cast(m.short_edge) - }; - } - - void handle_frame(std::shared_ptr frame) { - if (!frame->tactile || frame->tactile_pressures.empty()) { - return; - } - - auto pressures = frame->tactile_pressures; +#include +#include + + +#define DEBUG 0 + +using namespace creeper; +namespace capro = card::pro; +namespace lnpro = linear::pro; +namespace impro = image::pro; +namespace ibpro = icon_button::pro; +namespace fbpro = filled_button::pro; +namespace dmpro = dropdown_menu::pro; +namespace pwpro = plot_widget::pro; +namespace vfpro = vector_widget::pro; +namespace lcpro = line_widget::pro; + +namespace { + +constexpr std::array kSlaveRequestCommandTemplate{ + 0x55, + 0xAA, + 0x09, + 0x00, + 0x34, + 0x00, + 0xFB, + 0x00, + 0x1C, + 0x00, + 0x00, + 0x00, + 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(reg ^ data[idx]); + for (int bit = 0; bit < 8; ++bit) { + if ((reg & 0x80U) != 0U) { + reg = static_cast((reg << 1U) ^ kPolynomial); + } + else { + reg = static_cast(reg << 1U); + } + } + } + return static_cast(reg ^ kXorOut); +} + +std::vector 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(width) * static_cast(height); + const std::uint32_t byte_count = value_count * 2U; // 2 bytes per cell + const std::uint16_t payload_len = + static_cast(std::min( + byte_count, std::numeric_limits::max())); + + command[11] = static_cast(payload_len & 0xFFU); + command[12] = static_cast((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(command.begin(), command.end()); +} + +QVector make_flat_points(const QSize& size, double value = 0.0) { + const int width = std::max(size.width(), 1); + const int height = std::max(size.height(), 1); + QVector points; + points.reserve(static_cast(width * height)); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + points.append( + PointData{ static_cast(x), static_cast(y), value }); + } + } + return points; +} + +std::once_flag& codec_registration_flag() { + static std::once_flag flag; + return flag; +} + +class SensorStreamController: public QObject { + public: + SensorStreamController( + std::shared_ptr>> heatmap_data, + std::shared_ptr> matrix_context, + std::shared_ptr>> line_series, + 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_b_codec(); + }); + } + + ~SensorStreamController() override { reset_core(); } + + bool start(const QString& requested_port, + std::uint32_t baudrate, + Tactile_TYPE type) { + std::cout << "start" << std::endl; + if (is_connected()) { + return true; + } + + sample_counter_ = 0; + if (line_series_) { + line_series_->set(QVector{}); + } + + const auto ports = ffmsep::CPStreamCore::list_available_ports(); + std::string port_utf8; + if (!requested_port.isEmpty()) { + port_utf8 = requested_port.toStdString(); + const auto it = std::find_if( + ports.begin(), ports.end(), [&](const serial::PortInfo& info) { return info.port == port_utf8; }); + if (it == ports.end()) { + if (ports.empty()) { +#if DEBUG + std::cerr << "SensorStreamController: requested port '" << port_utf8 + << "' not available and no other ports detected.\n"; +#endif + last_error_ = QString::fromUtf8("未检测到串口"); + return false; + } +#if DEBUG + std::cerr << "SensorStreamController: requested port '" << port_utf8 + << "' not available, falling back to first detected port.\n"; +#endif + port_utf8 = ports.front().port; + } + } + else if (!ports.empty()) { + port_utf8 = ports.front().port; + } + else { +#if DEBUG + std::cerr << "SensorStreamController: no serial ports available\n"; +#endif + last_error_ = QString::fromUtf8("未检测到串口"); + return false; + } + + const std::uint32_t baud = baudrate == 0U ? 115200U : baudrate; + + ffmsep::CPStreamConfig cfg; + cfg.port = port_utf8; + cfg.baudrate = baud; + cfg.read_chunk_size = 256; + cfg.packet_queue_capacity = 128; + cfg.frame_queue_capacity = 32; + const auto format_command = + [](const std::vector& 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(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(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(); + core_ = std::make_unique(); + + if (!core_->open(cfg)) { + last_error_ = QString::fromStdString(core_->last_error()); + std::cerr << "SensorStreamController: open failed - " + << core_->last_error() << "\n"; + reset_core(); + return false; + } + + core_->set_frame_callback( + [this](std::shared_ptr frame) { + handle_frame(frame); + }); + + if (!core_->start()) { + last_error_ = QString::fromStdString(core_->last_error()); + std::cerr << "SensorStreamController: start failed - " + << core_->last_error() << "\n"; + reset_core(); + return false; + } + + active_port_ = QString::fromStdString(cfg.port); + last_error_.clear(); + connected_ = true; + return true; + } + + void stop() { + if (!core_) { + active_port_.clear(); + if (heatmap_data_ && matrix_context_) { + heatmap_data_->set(make_flat_points(matrix_context_->get())); + } + if (line_series_) { + line_series_->set(QVector{}); + } + sample_counter_ = 0; + connected_ = false; + return; + } + + core_->set_frame_callback({}); + if (core_->is_running()) { + core_->stop(); + } + + core_->clear_frames(); + + connected_ = false; + active_port_.clear(); + if (heatmap_data_ && matrix_context_) { + heatmap_data_->set(make_flat_points(matrix_context_->get())); + } + if (line_series_) { + line_series_->set(QVector{}); + } + sample_counter_ = 0; + } + + [[nodiscard]] bool is_running() const noexcept { + return core_ && core_->is_running(); + } + + [[nodiscard]] bool is_connected() const noexcept { return connected_; } + + [[nodiscard]] QString active_port() const { return active_port_; } + + [[nodiscard]] QString last_error() const { return last_error_; } + + std::future + export_frames(const QString& path, bool clear_after_export) { + if (path.isEmpty()) { + return make_failed_future(path, "export path is empty"); + } + if (!core_) { + return make_failed_future(path, "stream is not active"); + } + if (core_->recorded_frame_count() == 0U) { + return make_failed_future(path, "no tactile frames recorded"); + } + const auto normalized = QDir::toNativeSeparators(path); + return core_->export_recorded_frames(normalized.toStdString(), + clear_after_export); + } + + private: + void reset_core() { + connected_ = false; + if (!core_) { + return; + } + core_->set_frame_callback({}); + if (core_->is_running()) { + core_->stop(); + } + if (core_->is_open()) { + core_->close(); + } + core_.reset(); + } + + static QSize to_qsize(const ffmsep::tactile::MatrixSize& m) { + return QSize{ static_cast(m.long_edge), static_cast(m.short_edge) }; + } + + void handle_frame(std::shared_ptr frame) { + if (!frame) { + return; + } + + auto format_raw = [](const std::vector& data) -> std::string { + if (data.empty()) { + return "[]"; + } + std::ostringstream oss; + oss << '['; + oss << std::uppercase << std::setfill('0'); + for (std::size_t idx = 0; idx < data.size(); ++idx) { + if (idx != 0U) { + oss << ' '; + } + oss << std::setw(2) << std::hex + << static_cast(data[idx]); + } + oss << ']'; + return oss.str(); + }; + auto frame_bytes = frame->frame.data; + std::vector 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& data) -> std::string { - if (data.empty()) { - return "[]"; - } - std::ostringstream oss; - oss << '['; - oss << std::uppercase << std::setfill('0'); - for (std::size_t idx = 0; idx < data.size(); ++idx) { - if (idx != 0U) { - oss << ' '; - } - oss << std::setw(2) << std::hex << static_cast(data[idx]); - } - oss << ']'; - return oss.str(); - }; - - std::cout << "[Sensor] frame=" << format_raw(frame_bytes); - std::cout << " payload=" << format_raw(raw_payload); - std::cout << " received " << pressures.size() << " pressure values"; - const std::size_t preview = std::min(pressures.size(), 12); - if (preview > 0) { - std::cout << " values=["; - for (std::size_t idx = 0; idx < preview; ++idx) { - if (idx != 0U) { - std::cout << ", "; - } - std::cout << pressures[idx]; - } - if (preview < pressures.size()) { - std::cout << ", ..."; - } - std::cout << "]"; - } - std::cout << std::endl; - - auto matrix = matrix_context_->get(); - const auto cells_exp = static_cast(std::max(1, matrix.width()) * - std::max(1, matrix.height())); - if (cells_exp == 0) return; + 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(); + const auto cells_exp = static_cast( + std::max(1, matrix.width()) * std::max(1, matrix.height())); + if (cells_exp == 0) + return; if (pressures.size() > cells_exp) { pressures.resize(cells_exp); - } else if (pressures.size() < cells_exp) { + } + else if (pressures.size() < cells_exp) { pressures.resize(cells_exp, 0); } + double total = 0.0; + for (const auto value: pressures) { + total += static_cast(value); + } + + if (line_series_) { + auto series = line_series_->get(); + series.append(QPointF(static_cast(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 points; points.reserve(matrix.width() * matrix.height()); for (int y = 0; y < matrix.height(); ++y) { for (int x = 0; x < matrix.width(); ++x) { - const int idx = y * matrix.width() + x; - if (idx >= static_cast(pressures.size())) { - break; - } - const auto value = static_cast(pressures[static_cast(idx)]); - points.append(PointData{ - static_cast(x), - static_cast(y), - value }); - } - } - matrix_context_->set(matrix); - heatmap_data_->set(std::move(points)); - }, - Qt::QueuedConnection); - } - - [[nodiscard]] QSize normalize_matrix(QSize candidate, std::size_t value_count) const { - if (value_count == 0U) { - return QSize{}; - } - - const auto adapt_from = [value_count](const QSize& hint) -> std::optional { - if (hint.width() <= 0 && hint.height() <= 0) { - return std::nullopt; - } - - if (hint.width() > 0 && hint.height() > 0) { - const auto cells = static_cast(hint.width()) * static_cast(hint.height()); - if (cells == value_count) { - return hint; - } - } - - if (hint.width() > 0) { - const auto width = static_cast(hint.width()); - if (width != 0U && (value_count % width) == 0U) { - const auto height = static_cast(value_count / width); - return QSize{ hint.width(), height }; - } - } - - if (hint.height() > 0) { - const auto height = static_cast(hint.height()); - if (height != 0U && (value_count % height) == 0U) { - const auto width = static_cast(value_count / height); - return QSize{ width, hint.height() }; - } - } - - return std::nullopt; - }; - - if (auto adjusted = adapt_from(candidate)) { - return *adjusted; - } - - if (auto adjusted = adapt_from(matrix_context_->get())) { - return *adjusted; - } - - const auto root = static_cast(std::sqrt(static_cast(value_count))); - for (int width = root; width >= 1; --width) { - const auto divisor = static_cast(width); - if (divisor == 0U) { - continue; - } - if ((value_count % divisor) == 0U) { - const auto height = static_cast(value_count / divisor); - return QSize{ width, height }; - } - } - - return QSize{ static_cast(value_count), 1 }; - } - - std::shared_ptr>> heatmap_data_; - std::shared_ptr> matrix_context_; - std::unique_ptr core_; - QString active_port_; - QString last_error_; - bool connected_ = false; - - static std::future make_failed_future( - const QString& path, - std::string message) { - std::promise promise; - auto future = promise.get_future(); - ffmsep::persist::WriteResult result{ - false, - std::move(message), - path.toStdString() - }; - promise.set_value(std::move(result)); - return future; - } -}; - + const int idx = y * matrix.width() + x; + if (idx >= static_cast(pressures.size())) { + break; + } + const auto value = + static_cast(pressures[static_cast(idx)]); + points.append(PointData{ static_cast(x), + static_cast(y), + value }); + } + } + matrix_context_->set(matrix); + heatmap_data_->set(std::move(points)); + }, + Qt::QueuedConnection); + } + + [[nodiscard]] QSize normalize_matrix(QSize candidate, + std::size_t value_count) const { + if (value_count == 0U) { + return QSize{}; + } + + const auto adapt_from = + [value_count](const QSize& hint) -> std::optional { + if (hint.width() <= 0 && hint.height() <= 0) { + return std::nullopt; + } + + if (hint.width() > 0 && hint.height() > 0) { + const auto cells = static_cast(hint.width()) * static_cast(hint.height()); + if (cells == value_count) { + return hint; + } + } + + if (hint.width() > 0) { + const auto width = static_cast(hint.width()); + if (width != 0U && (value_count % width) == 0U) { + const auto height = static_cast(value_count / width); + return QSize{ hint.width(), height }; + } + } + + if (hint.height() > 0) { + const auto height = static_cast(hint.height()); + if (height != 0U && (value_count % height) == 0U) { + const auto width = static_cast(value_count / height); + return QSize{ width, hint.height() }; + } + } + + return std::nullopt; + }; + + if (auto adjusted = adapt_from(candidate)) { + return *adjusted; + } + + if (auto adjusted = adapt_from(matrix_context_->get())) { + return *adjusted; + } + + const auto root = + static_cast(std::sqrt(static_cast(value_count))); + for (int width = root; width >= 1; --width) { + const auto divisor = static_cast(width); + if (divisor == 0U) { + continue; + } + if ((value_count % divisor) == 0U) { + const auto height = static_cast(value_count / divisor); + return QSize{ width, height }; + } + } + + return QSize{ static_cast(value_count), 1 }; + } + + std::shared_ptr>> heatmap_data_; + std::shared_ptr> matrix_context_; + std::shared_ptr>> line_series_; + std::unique_ptr core_; + QString active_port_; + QString last_error_; + std::uint64_t sample_counter_ = 0; + int line_series_capacity_ = 240; + bool connected_ = false; + + static std::future + make_failed_future(const QString& path, std::string message) { + std::promise promise; + auto future = promise.get_future(); + ffmsep::persist::WriteResult result{ false, std::move(message), path.toStdString() }; + promise.set_value(std::move(result)); + return future; + } +}; + struct SensorUiState { std::shared_ptr> link_icon = - std::make_shared>(QString::fromLatin1(material::icon::kAddLink)); + std::make_shared>( + QString::fromLatin1(material::icon::kAddLink)); std::shared_ptr>> heatmap_data = std::make_shared>>(); std::shared_ptr> heatmap_matrix = std::make_shared>(); std::shared_ptr>> heatmap_range = - std::make_shared>>(QPair{0, 300}); + std::make_shared>>(QPair{ 0, 300 }); + std::shared_ptr>> line_series = + std::make_shared>>(); + int line_series_capacity = 240; std::shared_ptr> port_items = std::make_shared>(); std::shared_ptr> profile_items = std::make_shared>(); + dropdown_menu::internal::DropdownMenu* port_dropdown = nullptr; + dropdown_menu::internal::DropdownMenu* profile_dropdown = nullptr; QString selected_port; QString selected_profile; + Tactile_TYPE selected_type = Tactile_TYPE::PiezoresistiveA; std::uint32_t selected_baud = 115200; std::unique_ptr controller; SensorUiState() { const QSize size{ 3, 4 }; - heatmap_matrix->set_silent(size); - heatmap_data->set_silent(make_flat_points(size)); - - // 初始化串口列表 - QStringList ports_list; - const auto ports = ffmsep::CPStreamCore::list_available_ports(); - ports_list.reserve(static_cast(ports.size())); + heatmap_matrix->set_silent(size); + heatmap_data->set_silent(make_flat_points(size)); + + // 初始化串口列表 + QStringList ports_list; + const auto ports = ffmsep::CPStreamCore::list_available_ports(); + ports_list.reserve(static_cast(ports.size())); for (const auto& info: ports) { ports_list.emplace_back(QString::fromStdString(info.port)); } @@ -481,30 +608,33 @@ struct SensorUiState { QStringList profile_list; const auto& profiles = GlobalHelper::instance().get_all_profile(); profile_list.reserve(static_cast(profiles.size())); - for (const auto& p : profiles) { + for (const auto& p: profiles) { profile_list << p.name; } profile_items->set_silent(profile_list); if (selected_profile.isEmpty() && !profile_list.isEmpty()) { selected_profile = profile_list.front(); 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) }; - selected_baud = profiles.front().baud_rate == 0 ? 115200U : static_cast(profiles.front().baud_rate); + selected_type = profiles.front().type; + selected_baud = + profiles.front().baud_rate == 0 ? 115200U : static_cast(profiles.front().baud_rate); heatmap_matrix->set_silent(size); heatmap_data->set_silent(make_flat_points(size)); const int range_min = profiles.front().range_left; - const int range_max = (profiles.front().range_right == profiles.front().range_left) - ? profiles.front().range_left + 1 - : profiles.front().range_right; - heatmap_range->set_silent(QPair{range_min, range_max}); + const int range_max = + (profiles.front().range_right == profiles.front().range_left) ? profiles.front().range_left + 1 : profiles.front().range_right; + heatmap_range->set_silent(QPair{ range_min, range_max }); } } - controller = std::make_unique(heatmap_data, heatmap_matrix); + controller = + std::make_unique( + heatmap_data, heatmap_matrix, line_series, line_series_capacity); } }; - + SensorUiState& sensor_state() { static SensorUiState state; return state; @@ -517,25 +647,18 @@ static void RefreshPortsForView(SensorUiState& sensor) { for (const auto& info: ports) { ports_list.emplace_back(QString::fromStdString(info.port)); } - if (!sensor.selected_port.isEmpty()) { - const bool exists = ports_list.contains(sensor.selected_port); - if (!exists) { - sensor.selected_port = ports_list.isEmpty() ? QString{} : ports_list.front(); + if (sensor.selected_port.isEmpty()) { + if (!ports_list.isEmpty()) { + sensor.selected_port = ports_list.front(); } + sensor.port_items->set(std::move(ports_list)); } - else if (!ports_list.isEmpty()) { - sensor.selected_port = ports_list.front(); - } - - sensor.port_items->set(std::move(ports_list)); - RefreshProfilesForView(); } -class PortHoverRefreshFilter final : public QObject { +class PortHoverRefreshFilter final: public QObject { public: - explicit PortHoverRefreshFilter(SensorUiState& sensor, QObject* parent = nullptr) - : QObject(parent) - , sensor_(sensor) { } + explicit PortHoverRefreshFilter(SensorUiState& sensor, + QObject* parent = nullptr): QObject(parent), sensor_(sensor) {} protected: bool eventFilter(QObject* watched, QEvent* event) override { @@ -558,58 +681,64 @@ void RefreshProfilesForView() { QStringList profile_list; const auto& profiles = GlobalHelper::instance().get_all_profile(); profile_list.reserve(static_cast(profiles.size())); - for (const auto& p : profiles) { + for (const auto& p: profiles) { profile_list << p.name; } - if (!sensor.selected_profile.isEmpty()) { - const bool exists = profile_list.contains(sensor.selected_profile); - if (!exists) { - sensor.selected_profile = profile_list.isEmpty() ? QString{} : profile_list.front(); - } + if (!sensor.selected_profile.isEmpty()) { + const bool exists = profile_list.contains(sensor.selected_profile); + if (!exists) { + sensor.selected_profile = + profile_list.isEmpty() ? QString{} : profile_list.front(); } - else if (!profile_list.isEmpty()) { - sensor.selected_profile = profile_list.front(); - } - if (!sensor.selected_profile.isEmpty()) { - const auto it = std::find_if(profiles.begin(), profiles.end(), - [&](const ConfigProfile& p) { return p.name == sensor.selected_profile; }); - if (it != profiles.end()) { - sensor.selected_baud = it->baud_rate == 0 ? 115200U : static_cast(it->baud_rate); - const auto size = QSize { std::max(1, it->matrix_width), std::max(1, it->matrix_height) }; - sensor.heatmap_matrix->set(size); - sensor.heatmap_data->set(make_flat_points(size)); - const int range_min = it->range_left; - const int range_max = (it->range_right == it->range_left) ? it->range_left + 1 : it->range_right; - sensor.heatmap_range->set(QPair{range_min, range_max}); - } + } + else if (!profile_list.isEmpty()) { + sensor.selected_profile = profile_list.front(); + } + if (!sensor.selected_profile.isEmpty()) { + const auto it = std::find_if(profiles.begin(), profiles.end(), [&](const ConfigProfile& p) { + return p.name == sensor.selected_profile; + }); + if (it != profiles.end()) { + sensor.selected_baud = it->baud_rate == 0 ? 115200U : static_cast(it->baud_rate); + 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_data->set(make_flat_points(size)); + const int range_min = it->range_left; + const int range_max = (it->range_right == it->range_left) ? it->range_left + 1 : it->range_right; + sensor.heatmap_range->set(QPair{ range_min, range_max }); } + } sensor.profile_items->set(std::move(profile_list)); } static auto ComConfigComponent(ThemeManager& manager) { auto& sensor = sensor_state(); auto link_icon_context = sensor.link_icon; - - if (sensor.selected_port.isEmpty() && !sensor.port_items->get().isEmpty()) { - sensor.selected_port = sensor.port_items->get().front(); - } - + + if (sensor.selected_port.isEmpty() && !sensor.port_items->get().isEmpty()) { + sensor.selected_port = sensor.port_items->get().front(); + } + if (sensor.selected_baud == 0U) { sensor.selected_baud = 115200U; } - + const auto row = new Row{ // lnpro::Item { // 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 { - // text_field::pro::LabelText {}, - // slogen_context, - // }, - // }, - lnpro::Item{ - dmpro::ThemeManager{ manager }, - dmpro::LeadingIcon{ material::icon::kArrowDropDown, material::regular::font }, + // text_field::pro::LabelText {}, + // slogen_context, + // }, + // }, + lnpro::Item{ + dmpro::ThemeManager{ manager }, + dmpro::LeadingIcon{ material::icon::kArrowDropDown, + material::regular::font }, dmpro::TextChanged{ [sensor_ptr = &sensor](QString text) { // const auto text = self.currentText(); if (!text.isEmpty()) { @@ -622,30 +751,37 @@ static auto ComConfigComponent(ThemeManager& manager) { sensor.port_items, }, dmpro::Apply{ [&sensor](dropdown_menu::internal::DropdownMenu& self) { + sensor.port_dropdown = &self; if (!self.property("portHoverRefreshAttached").toBool()) { - self.installEventFilter(new PortHoverRefreshFilter(sensor, &self)); + self.installEventFilter( + new PortHoverRefreshFilter(sensor, &self)); self.setProperty("portHoverRefreshAttached", true); } } }, }, lnpro::Item{ 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) { if (!text.isEmpty()) { sensor_ptr->selected_profile = text; const auto& profiles = GlobalHelper::instance().get_all_profile(); - const auto it = std::find_if(profiles.begin(), profiles.end(), - [&text](const ConfigProfile& p) { return p.name == text; }); + const auto it = std::find_if( + profiles.begin(), profiles.end(), [&text](const ConfigProfile& p) { return p.name == text; }); if (it != profiles.end()) { - const auto baud = it->baud_rate == 0 ? 115200U : static_cast(it->baud_rate); + const auto baud = + it->baud_rate == 0 ? 115200U : static_cast(it->baud_rate); 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_data->set(make_flat_points(size)); const int range_min = it->range_left; const int range_max = (it->range_right == it->range_left) ? it->range_left + 1 : it->range_right; - sensor_ptr->heatmap_range->set(QPair{range_min, range_max}); + sensor_ptr->heatmap_range->set( + QPair{ range_min, range_max }); } } } }, @@ -654,115 +790,125 @@ static auto ComConfigComponent(ThemeManager& manager) { dmpro::Items{}, sensor.profile_items, }, + dmpro::Apply{ [&sensor](dropdown_menu::internal::DropdownMenu& self) { + sensor.profile_dropdown = &self; + } }, }, lnpro::SpacingItem{ 20 }, lnpro::Item{ - ibpro::ThemeManager{ manager }, - ibpro::FixedSize{ 40, 40 }, - ibpro::Color{ IconButton::Color::TONAL }, - ibpro::Font{ material::kRegularExtraSmallFont }, - MutableForward{ - icon_button::pro::FontIcon{}, - link_icon_context, - }, + ibpro::ThemeManager{ manager }, ibpro::FixedSize{ 40, 40 }, ibpro::Color{ IconButton::Color::TONAL }, ibpro::Font{ material::kRegularExtraSmallFont }, MutableForward{ + icon_button::pro::FontIcon{}, + link_icon_context, + }, ibpro::Clickable{ [sensor_ptr = &sensor, link_icon_context] { auto& sensor = *sensor_ptr; if (!sensor.controller) { return; } - if (sensor.controller->is_connected()) { - sensor.controller->stop(); - link_icon_context->set(QString::fromLatin1(material::icon::kAddLink)); + if (sensor.controller->is_connected()) { + sensor.controller->stop(); + link_icon_context->set( + QString::fromLatin1(material::icon::kAddLink)); } else { const auto port = sensor.selected_port; - const auto baud = sensor.selected_baud == 0U ? 115200U : sensor.selected_baud; - if (sensor.controller->start(port, baud)) { + const auto 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(); - link_icon_context->set(QString::fromLatin1(material::icon::kLinkOff)); + link_icon_context->set( + QString::fromLatin1(material::icon::kLinkOff)); } else { - std::cerr << "Failed to start sensor stream: " - << sensor.controller->last_error().toStdString() - << "\n"; + std::cerr << "Failed to start sensor stream: " + << sensor.controller->last_error().toStdString() + << "\n"; } } } } }, - lnpro::Item{ - fbpro::ThemeManager{ manager }, - fbpro::FixedSize{ 40, 40 }, - fbpro::Radius{ 8.0 }, + lnpro::Item{ ibpro::ThemeManager{ manager }, ibpro::FixedSize{ 40, 40 }, ibpro::Color{ IconButton::Color::TONAL }, ibpro::Font{ material::kRegularExtraSmallFont }, ibpro::FontIcon{ "cleaning_services" }, ibpro::Clickable{ [&sensor] { + // Clear current selections (keep items) so next hover triggers a fresh scan. + sensor.selected_port.clear(); + sensor.selected_profile.clear(); + if (sensor.port_dropdown) { + sensor.port_dropdown->setCurrentIndex(-1); + } + if (sensor.profile_dropdown) { + sensor.profile_dropdown->setCurrentIndex(-1); + } + } } }, + lnpro::Item{ fbpro::ThemeManager{ manager }, fbpro::FixedSize{ 40, 40 }, fbpro::Radius{ 8.0 }, // fbpro::Color { IconButton::Color::TONAL }, - fbpro::Font{ material::kRegularExtraSmallFont }, - fbpro::Text{ "drive_file_move" }, - fbpro::Clickable{ [&sensor] { - auto* controller = sensor.controller.get(); - if (!controller) { - QMessageBox::warning(nullptr, - QStringLiteral("导出失败"), - QStringLiteral("当前串流尚未初始化。")); - return; - } - - const auto documents = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); - const auto timestamp = QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd_HHmmss")); - QString suggested_name = QStringLiteral("touchsensor_%1.json").arg(timestamp); - QString initial_path = documents.isEmpty() ? suggested_name : QDir(documents).filePath(suggested_name); - - const QString chosen_path = QFileDialog::getSaveFileName( - nullptr, - QStringLiteral("导出触觉帧"), - initial_path, - QStringLiteral("JSON 文件 (*.json)")); - if (chosen_path.isEmpty()) { - return; - } - - auto future = controller->export_frames(chosen_path, false); - std::thread([future = std::move(future)]() mutable { - ffmsep::persist::WriteResult result{}; - try { - result = future.get(); - } - catch (const std::exception& ex) { - result.ok = false; - result.error = ex.what(); - } - catch (...) { - result.ok = false; - result.error = "unknown export failure"; - } - - if (auto* app = QCoreApplication::instance()) { - QMetaObject::invokeMethod(app, [res = std::move(result)]() { - if (res.ok) { - QMessageBox::information( - nullptr, - QStringLiteral("导出成功"), - QStringLiteral("触觉帧已导出至:\n%1") - .arg(QString::fromStdString(res.path))); - } else { - const auto error = QString::fromStdString( - res.error.empty() ? std::string{"unknown error"} : res.error); - const auto target = QString::fromStdString(res.path); - const auto message = target.isEmpty() - ? QStringLiteral("原因:%1").arg(error) - : QStringLiteral("原因:%1\n目标:%2").arg(error, target); - QMessageBox::warning(nullptr, - QStringLiteral("导出失败"), - message); - } }, Qt::QueuedConnection); - } - }) - .detach(); - } } } - }; - return new Widget{ - widget::pro::Layout{ row }, - }; -} - -static auto DisplayComponent(ThemeManager& manager, int /*index*/ = 0) noexcept { + fbpro::Font{ material::kRegularExtraSmallFont }, + fbpro::Text{ "drive_file_move" }, + fbpro::Clickable{ [&sensor] { + auto* controller = sensor.controller.get(); + if (!controller) { + QMessageBox::warning(nullptr, QStringLiteral("导出失败"), QStringLiteral("当前串流尚未初始化。")); + return; + } + + const auto documents = QStandardPaths::writableLocation( + QStandardPaths::DocumentsLocation); + const auto timestamp = QDateTime::currentDateTime().toString( + 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( + nullptr, QStringLiteral("导出触觉帧"), initial_path, QStringLiteral("CSV 文件 (*.csv)")); + if (chosen_path.isEmpty()) { + return; + } + + auto future = controller->export_frames(chosen_path, false); + std::thread([future = std::move(future)]() mutable { + ffmsep::persist::WriteResult result{}; + try { + result = future.get(); + } + catch (const std::exception& ex) { + result.ok = false; + result.error = ex.what(); + } + catch (...) { + result.ok = false; + result.error = "unknown export failure"; + } + + if (auto* app = QCoreApplication::instance()) { + QMetaObject::invokeMethod( + app, + [res = std::move(result)]() { + if (res.ok) { + QMessageBox::information( + nullptr, QStringLiteral("导出成功"), QStringLiteral("触觉帧已导出至:\n%1").arg(QString::fromStdString(res.path))); + } + else { + const auto error = QString::fromStdString( + res.error.empty() ? std::string{ "unknown error" } : res.error); + const auto target = QString::fromStdString(res.path); + const auto message = + target.isEmpty() ? QStringLiteral("原因:%1").arg(error) : QStringLiteral("原因:%1\n目标:%2").arg(error, target); + QMessageBox::warning( + nullptr, QStringLiteral("导出失败"), message); + } + }, + Qt::QueuedConnection); + } + }) + .detach(); + } } } + }; + return new Widget{ + widget::pro::Layout{ row }, + }; +} + +static auto DisplayComponent(ThemeManager& manager, +int /*index*/ = 0) noexcept { auto& sensor = sensor_state(); const auto row = new Row{ lnpro::Item{ @@ -774,19 +920,18 @@ static auto DisplayComponent(ThemeManager& manager, int /*index*/ = 0) noexcept plot_widget::pro::PlotData{}, sensor.heatmap_data, }, - pwpro::MatrixSize{ - sensor.heatmap_matrix->get() }, - MutableTransform{ - [](auto& widget, const QSize& size) { - pwpro::MatrixSize{ size }.apply(widget); - }, + pwpro::MatrixSize{ sensor.heatmap_matrix->get() }, + MutableTransform{ [](auto& widget, const QSize& size) { + pwpro::MatrixSize{ size }.apply(widget); + }, sensor.heatmap_matrix }, - MutableTransform{ - [](auto& widget, const QPair& range) { - const double min = static_cast(range.first); - const double max = static_cast(range.second); - widget.set_color_gradient_range(min, max); - }, + MutableTransform{ [](auto& widget, const QPair& range) { + const double min = + static_cast(range.first); + const double max = + static_cast(range.second); + widget.set_color_gradient_range(min, max); + }, sensor.heatmap_range }, }, }; @@ -794,27 +939,76 @@ static auto DisplayComponent(ThemeManager& manager, int /*index*/ = 0) noexcept widget::pro::Layout{ row }, }; } - -auto ViewComponent(ViewComponentState& state) noexcept -> raw_pointer { - return new FilledCard{ - capro::ThemeManager{ state.manager }, - capro::SizePolicy{ QSizePolicy::Expanding }, - capro::Layout{ - lnpro::Alignment{ Qt::AlignTop }, - // lnpro::Margin {10}, - // lnpro::Spacing {10}, - - lnpro::Item{ - ComConfigComponent(state.manager), - }, - lnpro::Item{ - lnpro::Item{ - DisplayComponent(state.manager), - }, - lnpro::Item{ - DisplayComponent(state.manager), - }, - }, - }, - }; -} + +static auto DisplayVectorComponent(ThemeManager& manager) noexcept { + auto& sensor = sensor_state(); + const auto row = new Row{ + lnpro::Item{ + 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{ + 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 { + return new FilledCard{ + capro::ThemeManager{ state.manager }, + capro::SizePolicy{ QSizePolicy::Expanding }, + capro::Layout{ + lnpro::Alignment{ Qt::AlignTop }, + // lnpro::Margin {10}, + // lnpro::Spacing {10}, + + lnpro::Item{ + ComConfigComponent(state.manager), + }, + lnpro::Item{ + lnpro::Item{ + DisplayComponent(state.manager), + }, + lnpro::Item{ + lnpro::Item{ + DisplayVectorComponent(state.manager), + }, + lnpro::Item{ + DisplayLineComponent(state.manager), + }, + }, + }, + }, + }; +} diff --git a/examples/cpstream_demo.cc b/examples/cpstream_demo.cc index 7633ebe..19966ee 100644 --- a/examples/cpstream_demo.cc +++ b/examples/cpstream_demo.cc @@ -76,7 +76,7 @@ int main(int argc, char** argv) { 0x55, 0xAA, 0x09, 0x00, 0x34, 0x00, 0xFB, 0x00, 0x1C, 0x00, 0x00, 0x18, 0x00, 0x7A }; - cfg.slave_request_interval = 3ms; + cfg.slave_request_interval = 10ms; ffmsep::CPStreamCore core(cfg); if (!core.open()) { diff --git a/main.cc b/main.cc index 7d6acc2..bd8360c 100644 --- a/main.cc +++ b/main.cc @@ -15,7 +15,7 @@ #include #include #include - +#include using namespace creeper; namespace lnpro = linear::pro; @@ -29,7 +29,7 @@ auto main(int argc, char *argv[]) -> int { app::pro::Attribute {Qt::AA_UseHighDpiPixmaps}, app::pro::Complete {argc, argv}, }; - + std::cout << "========begin========" << std::endl; auto stack_index = std::make_shared>(); stack_index->set_silent(0); @@ -38,6 +38,8 @@ auto main(int argc, char *argv[]) -> int { auto nav_component_state = NavComponentState { .manager = manager, .switch_callback = [&](int index, const auto& name) { + + qDebug() << "switch_callback index: " << index; }, .buttons_context = {