7 Commits
dev ... main

Author SHA1 Message Date
5b319cb76d 默认暗色模式,添加值显示开关 2025-12-16 19:18:21 +08:00
a1f7f337c2 颜色修改红绿,修复帧错误卡顿bug 2025-12-16 14:25:48 +08:00
c86c24488c setting.cc预留导入导出配置文件 2025-11-27 15:02:39 +08:00
98dcfa1520 修改CMake添加install和deploy文件 2025-11-27 15:02:18 +08:00
aedba5813f Merge branch 'dev'
# Conflicts:
#	components/ffmsep/cpstream_core.hh
#	components/view.cc
2025-11-25 16:20:04 +08:00
221f7303ee update README.md 2025-11-05 09:12:24 +08:00
6ed795a2b6 feat:slave 300Hz 2025-10-31 10:03:43 +08:00
25 changed files with 2761 additions and 1028 deletions

2
.gitignore vendored
View File

@@ -5,6 +5,8 @@
.VSCodeCounter/
mingw-build/
cmake-build-*/
clion-build-*/
build-demo*/
build/
output/
AppDir/

View File

@@ -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,13 +100,21 @@ set(TOUCHSENSOR_HEADERS
qt6_add_resources(APP_RESOURCES resources.qrc)
add_executable(${PROJECT_NAME}
${COMPONENT_SOURCES}
${UTILITY_SOURCES}
${TOUCHSENSOR_HEADERS}
${BASE_SOURCES}
main.cc
)
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}
# ${BASE_SOURCES}
# main.cc
#)
target_sources(${PROJECT_NAME} PRIVATE ${APP_RESOURCES})
target_include_directories(${PROJECT_NAME}
PRIVATE
@@ -136,3 +146,71 @@ if(BUILD_EXAMPLE)
target_link_libraries(cpstream_demo PRIVATE serial)
target_link_libraries(cpstream_demo PRIVATE setupapi)
endif()
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}
)
if(BUILD_EXAMPLE)
install(TARGETS cpstream_demo
RUNTIME DESTINATION .
)
endif()
if(WIN32)
# 利用 QtCore 目标找到 Qt bin 目录
get_target_property(_qt_core_location ${QT_VERSION}::Core LOCATION)
get_filename_component(_qt_bin_dir "${_qt_core_location}" DIRECTORY)
find_program(WINDEPLOYQT_EXECUTABLE
NAMES windeployqt windeployqt.exe
HINTS "${_qt_bin_dir}"
)
if(WINDEPLOYQT_EXECUTABLE)
message(STATUS "Found windeployqt: ${WINDEPLOYQT_EXECUTABLE}")
# 安装完之后,对 deploy/touchsensor.exe 跑 windeployqt
install(CODE
"execute_process(
COMMAND \"${WINDEPLOYQT_EXECUTABLE}\"
--dir \"${CMAKE_INSTALL_PREFIX}\"
--no-translations
\"${CMAKE_INSTALL_PREFIX}/touchsensor.exe\"
RESULT_VARIABLE _windeployqt_result
)
if(NOT _windeployqt_result EQUAL 0)
message(FATAL_ERROR \"windeployqt failed with exit code: \${_windeployqt_result}\")
endif()"
)
else()
message(WARNING "windeployqt not found, Qt 相关 dll 需要你手动处理")
endif()
endif()
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
)
foreach(dll ${MINGW_RUNTIME_DLLS})
if(EXISTS "${MINGW_BIN_DIR}/${dll}")
message(STATUS "Will install MinGW runtime DLL: ${dll}")
install(FILES "${MINGW_BIN_DIR}/${dll}" DESTINATION .)
else()
message(WARNING "MinGW runtime DLL not found: ${MINGW_BIN_DIR}/${dll}")
endif()
endforeach()
endif()

119
README.md
View File

@@ -1,41 +1,49 @@
# TouchSensor 2.0
> Real-time tactile sensor exploration UI powered by Qt 6 and a custom Modern Qt component toolkit.
> 基于 Qt 6 与自研 Modern Qt 组件库打造的实时触觉传感器探索界面。
![Status](https://img.shields.io/badge/status-prototype-orange?style=for-the-badge)
![Qt 6](https://img.shields.io/badge/Qt-6.5%2B-41CD52?style=for-the-badge&logo=qt)
![C++23](https://img.shields.io/badge/C%2B%2B-23-00599C?style=for-the-badge&logo=cplusplus)
![CMake](https://img.shields.io/badge/CMake-3.20%2B-064F8C?style=for-the-badge&logo=cmake)
![UI Toolkit](https://img.shields.io/badge/Modern%20Qt-internal-8A2BE2?style=for-the-badge)
![状态](.assist/status-alpha.svg)
![Qt 版本](.assist/qt-6-5-plus.svg)
![C++ 标准](.assist/cpp-23.svg)
![CMake 版本](.assist/cmake-3-20.svg)
![UI 工具集](.assist/modern-qt.svg)
![TouchSensor Logo](images/logo.png)
## Highlights
- Modern, material-inspired desktop shell built on the in-repo `modern-qt` library.
- Modular component system (`NavComponent`, `ViewComponent`) for quick UI experiments.
- Real-time tactile matrix visualisation via the `HeatMapPlot` wrapper around QCustomPlot.
- Codec infrastructure (`components/ffmsep`) ready for custom tactile packet decoding.
- Serial transport module scaffolded for COM port discovery and streaming.
## 项目亮点
- 基于仓库内置的 `modern-qt` 库构建现代化、贴近 Material 风格的桌面壳层。
- 左侧导航栏搭配卡片式主界面,集中承载串口控制面板与双热力图视图。
- 实时触觉数据流水线:`ffmsep::CPStreamCore` 负责串口 I/O、编解码调度与帧缓存。
- 内置触觉编解码模块(`ffmsep::tactile`)自动解析载荷、推断矩阵尺寸并输出压力量表。
- 两套 `HeatMapPlot` 组件通过响应式数据源实时刷新,并与主题色梯度联动。
## Architecture At A Glance
- **Entry point**: `main.cc` composes the themed window, navigation rail, and card-based layout.
- **Components**: `components/` hosts UI widgets, charts, and the tactile decoder pipeline.
- **Modern Qt toolkit**: `modern-qt/` provides declarative wrappers, theming, and Material icon helpers.
- **Data layer**: `components/ffmsep` implements codec registration, packet decoding, and tactile frame processing.
- **Visualisation**: `components/charts/heatmap.*` exposes a themable heatmap control for sensor grids.
## 当前进展
- UI 内可完成串口的搜索、连接与断开,同时支持周期性从站轮询指令。
- 编解码注册流程已接入触觉解码器,控制台输出原始帧内容与矩阵提示,便于调试。
- 双热力图面板会根据解码结果动态调整矩阵尺寸,停止串流时自动回落至默认状态。
- 同时提供 `cpstream_demo` CLI用于在无界面环境校验串流核心逻辑。
## Getting Started
## 架构速览
- **入口**`main.cc` 负责加载主题、导航栏、遮罩动画以及传感器面板。
- **组件层**`components/` 收纳导航/视图界面、图表组件以及触觉串流管线。
- **Modern Qt 工具集**`modern-qt/` 提供声明式封装、主题系统与 Material 图标工具。
- **串流与编解码**`components/ffmsep` 打包编解码注册、`CPStreamCore` 与触觉帧解析工具。
- **可视化**`components/charts/heatmap.*` 提供可主题化的热力图控件,支持梯度和矩阵重设。
- **示例**`examples/` 下的 `cpstream_demo.cc` 可单独运行验证串流核心。
### Prerequisites
## 快速开始
### 环境要求
- CMake 3.20+
- A C++23-capable compiler (MSVC 19.3x, Clang 16+, or GCC 13+)
- Qt 6 (Widgets, Network, PrintSupport modules)
- 支持 C++23 的编译器(MSVC 19.3xClang 16+ GCC 13+
- Qt 6(至少包含 WidgetsNetworkPrintSupport 模块)
- Eigen3
- `spdlog` (fetched via package manager or provided to CMake)
- `spdlog`(可通过包管理器安装或手动提供给 CMake
- [`serial`](https://github.com/wjwwood/serial)(跨平台串口库,可包管理器安装或自行构建)
Ensure `Qt6_DIR` (or `CMAKE_PREFIX_PATH`) points to the Qt install so CMake can locate the required modules.
确保 `Qt6_DIR` `CMAKE_PREFIX_PATH` 指向 Qt 安装路径,以便 CMake 正确找到依赖模块。
### Configure & Build
### 配置与构建
![](https://picgo-upload.cn-nb1.rains3.com/2025/10/407c29f139a834ed0f79a81347f810f1.png)
```powershell
@@ -52,42 +60,51 @@ cd build && mingw32-make install
cat install_manifest.txt
```
To run from the build directory:
从构建目录直接运行:
```powershell
.\touchsensor.exe
```
On Linux/macOS adjust the Qt path and executable name accordingly.
Linux/macOS 上使用对应的 Qt 安装路径与可执行文件名。
## Project Layout
### 运行传感器界面
- 通过 USB 接入触觉设备,确认系统识别到的串口号。
- 启动 `touchsensor.exe`(或平台对应的可执行文件)。
- 在串口下拉列表中选择端口,可按需刷新;若需更换波特率请同步调整。
- 点击链接图标开始串流,双热力图将实时更新,控制台亦会打印原始载荷。
- 再次点击链接图标即可断开连接,界面会回落到默认示例数据。
`BUILD_EXAMPLE` 选项开启时会同步构建 `cpstream_demo`,可在终端中运行验证串流与编解码流程。
## 项目结构
```text
.
|-- components/
| |-- charts/ # QCustomPlot-based visualisations (heatmaps, etc.)
| |-- ffmsep/ # Codec system and tactile decoder experiments
| |-- view.cc # Main dashboard composition
| `-- ... # Additional UI widgets
|-- modern-qt/ # In-house declarative Qt UI framework
|-- serial/ # Serial communication helper library
|-- images/logo.png # Current app branding
|-- main.cc # Application bootstrap
`-- CMakeLists.txt # Build script (adds Qt, Eigen, Modern Qt, Serial, SPDLOG)
| |-- charts/ # 基于 QCustomPlot 的热力图等可视化组件
| |-- ffmsep/ # 触觉串流核心与编解码实现
| |-- nav.cc # 导航栏与主题切换逻辑
| |-- view.cc # 传感器控制面板与双热力图
| `-- ... # 其他 UI 组件
|-- examples/ # cpstream_demo 命令行示例
|-- modern-qt/ # 自研 Qt 声明式 UI 框架
|-- serial/ # 串口通信辅助库
|-- images/logo.png # 项目标识
|-- main.cc # 程序入口
`-- CMakeLists.txt # 构建脚本,聚合 Qt、Eigen、Modern Qt、Serial、SPDLOG
```
## Roadmap (WIP)
- Flesh out tactile codec implementations and connect them to live serial streams.
- Replace placeholder random data with decoded sensor frames.
- Expand navigation targets beyond the current demo cards.
- Capture screenshots or recordings for documentation.
- Polish theming, animation masks, and landing experience.
## 路线图(进行中)
- **已完成** 串口串流管线(轮询、编解码回调、热力图数据绑定)已连通。
- **已完成** HeatMapPlot 支持矩阵提示与梯度调节,能随解码结果刷新。
- **规划中** 引入内置诊断/历史面板,替换当前的标准输出日志。
- **规划中** 保存串口偏好、提供手动矩阵覆写,并补充文档素材。
- **规划中** 扩展导航目标,完善非传感器场景。
## Contributing
This repository is in active development; feel free to open issues or PRs once guidelines land. Until then, keep discussions in the project chat or issues board.
## Acknowledgements
- [Qt](https://www.qt.io/) for the core UI framework.
- [QCustomPlot](https://www.qcustomplot.com/) powering the heatmap widget.
- [spdlog](https://github.com/gabime/spdlog) for logging (wired via CMake).
- Internal **Modern Qt** toolkit built on top of creeper-qt utilities.
## 致谢
- [Qt](https://www.qt.io/) 提供核心 UI 框架。
- [QCustomPlot](https://www.qcustomplot.com/) 支撑热力图组件。
- [spdlog](https://github.com/gabime/spdlog) 用于日志记录(已在 CMake 中接入)。
- [serial](https://github.com/wjwwood/serial) 提供跨平台串口能力。
- 内部 **Modern Qt** 工具集基于 creeper-qt 生态构建。

View File

@@ -1,8 +1,13 @@
#pragma once
#include <creeper-qt/utility/theme/theme.hh>
#include <creeper-qt/utility/wrapper/mutable-value.hh>
#include <functional>
#include <memory>
#include <qwidget.h>
#include <string_view>
#include <tuple>
#include <vector>
template <typename T>
using raw_pointer = T*;
@@ -12,6 +17,7 @@ struct NavComponentState {
std::function<void(int, const std::string_view&)> switch_callback;
std::vector<std::tuple<std::string_view, std::string_view>> buttons_context;
std::function<void(int)> stacked_callback;
std::shared_ptr<creeper::MutableValue<bool>> heatmap_show_numbers;
};
auto NavComponent(NavComponentState&) noexcept -> raw_pointer<QWidget>;
@@ -33,3 +39,4 @@ auto HandViewComponent(HandViewComponentState&) noexcept -> raw_pointer<QWidget>
// 让其他模块可触发视图层的串口/配置刷新
void RefreshProfilesForView();
std::shared_ptr<creeper::MutableValue<bool>> HeatmapNumberVisibilityContext();

View File

@@ -52,6 +52,14 @@ void BasicPlot::set_data(const QVector<PointData>& data)const {
pimpl->set_data(data);
}
void BasicPlot::set_labels_visible(bool visible) const {
pimpl->set_labels_visible(visible);
}
bool BasicPlot::labels_visible() const {
return pimpl->labels_visible();
}
bool BasicPlot::is_initialized() const {
return pimpl->is_plot_initialized();
}

View File

@@ -43,7 +43,9 @@ public:
void set_matrix_size(const int& w, const int& h)const;
void set_data(const QVector<PointData>& data)const;
void set_color_gradient_range(const double& min, const double& max)const;
void set_labels_visible(bool visible) const;
QSize get_matrix_size() const;
bool labels_visible() const;
bool is_initialized() const;
public slots:

View File

@@ -19,7 +19,13 @@
using namespace creeper::plot_widget::internal;
struct BasicPlot::Impl {
explicit Impl(BasicPlot& self) noexcept : self{self}, initialized(false), matrix_size(QSize{3, 4}) {}
explicit Impl(BasicPlot& self) noexcept
: matrix_size(QSize{3, 4})
, color_min(0.0)
, color_max(800.0)
, show_labels(false)
, initialized(false)
, self{self} { }
public:
std::optional<creeper::ColorScheme> scheme;
@@ -41,10 +47,15 @@ public:
auto set_matrix_size(const QSize& size) -> void {
matrix_size = size;
const int expected = std::max(0, matrix_size.width() * matrix_size.height());
last_values.assign(static_cast<std::size_t>(expected), 0.0);
if (initialized) {
reset_plot();
if (!data_points.isEmpty()) {
set_data(data_points);
} else if (show_labels) {
sync_labels(last_values);
self.replot();
}
}
}
@@ -75,6 +86,18 @@ public:
}
}
auto set_labels_visible(bool visible) -> void {
show_labels = visible;
if (initialized) {
sync_labels(last_values);
self.replot();
}
}
auto labels_visible() const -> bool {
return show_labels;
}
auto initialize_plot() -> void {
if (initialized) return;
@@ -127,10 +150,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));
@@ -150,12 +172,12 @@ public:
auto reset_plot() -> void {
// 清除所有绘图元素
clear_labels();
self.clearPlottables();
self.clearGraphs();
self.clearItems();
self.clearFocus();
color_map = nullptr;
cell_labels.clear();
// 重新初始化
initialized = false;
@@ -164,7 +186,7 @@ public:
auto update_plot_data() -> void {
if (!initialized || !color_map) return;
ensure_labels();
// ensure_labels();
const int width = matrix_size.width();
const int height = matrix_size.height();
@@ -183,7 +205,8 @@ public:
}
}
update_label_values(values);
last_values = values;
sync_labels(last_values);
// 重绘
self.replot();
@@ -202,8 +225,10 @@ private:
QString ylabel;
QSize matrix_size;
QVector<PointData> data_points;
std::vector<double> last_values;
double color_min = 0.0;
double color_max = 800.0;
bool show_labels = false;
bool initialized;
BasicPlot& self;
QCPColorScale* color_scale = nullptr;
@@ -218,7 +243,7 @@ private:
text_color = scheme->on_surface;
}
}
label_text_color = QColor(0, 0, 0); // 固定黑色
label_text_color = text_color;
const auto pen = QPen(text_color);
@@ -294,7 +319,6 @@ private:
void update_label_values(const std::vector<double>& values) {
const int width = matrix_size.width();
const int height = matrix_size.height();
const double range = std::max(color_max - color_min, 1.0);
const int expected = width * height;
for (int idx = 0; idx < expected && idx < cell_labels.size(); ++idx) {
auto* label = cell_labels[idx];
@@ -310,6 +334,18 @@ private:
label->position->setCoords(x + 0.5, y + 0.5);
}
}
void sync_labels(const std::vector<double>& values) {
if (!initialized) {
return;
}
if (show_labels) {
ensure_labels();
update_label_values(values);
} else if (!cell_labels.isEmpty()) {
clear_labels();
}
}
};
#endif // TOUCHSENSOR_HEATMAP_IMPL_HH

View File

@@ -0,0 +1,254 @@
//
// Created by Codex on 2025/12/10.
//
#include "line_chart.hh"
#include <QLinearGradient>
#include <QMargins>
#include <algorithm>
#include <cmath>
#include <limits>
using creeper::line_widget::internal::LinePlot;
LinePlot::LinePlot() {
setBackground(Qt::transparent);
}
LinePlot::~LinePlot() = default;
void LinePlot::load_theme_manager(creeper::ThemeManager& mgr) {
mgr.append_handler(this, [this](const creeper::ThemeManager& manager) {
scheme_ = manager.color_scheme();
apply_theme();
if (initialized_) {
replot();
}
});
}
void LinePlot::set_data(const QVector<QPointF>& points) {
points_ = points;
trim_points();
if (initialized_) {
update_graph();
}
}
void LinePlot::set_max_points(int count) {
const int clamped = std::max(1, count);
if (clamped == max_points_) {
return;
}
max_points_ = clamped;
trim_points();
if (initialized_) {
update_graph();
}
}
void LinePlot::paintEvent(QPaintEvent* event) {
if (!initialized_) {
initialize_plot();
}
QCustomPlot::paintEvent(event);
}
void LinePlot::initialize_plot() {
if (initialized_) {
return;
}
axisRect()->setAutoMargins(QCP::msNone);
axisRect()->setMargins(QMargins(40, 12, 12, 18));
axisRect()->setBackground(QBrush(QColor(246, 249, 255)));
legend->setVisible(false);
xAxis->setVisible(true);
yAxis->setVisible(true);
xAxis->setRange(0.0, std::max(10, max_points_));
yAxis->setRange(0.0, default_y_range_);
xAxis->grid()->setVisible(false);
yAxis->grid()->setVisible(false);
xAxis->setTicks(false);
yAxis->setTicks(true);
xAxis->setSubTicks(false);
yAxis->setSubTicks(false);
xAxis->setTickLength(0);
yAxis->setTickLength(0);
yAxis->setTickLabelPadding(6);
yAxis->setLabelPadding(8);
graph_ = addGraph();
graph_->setLineStyle(QCPGraph::lsLine);
graph_->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, QPen(Qt::NoPen), QBrush(Qt::white), 6));
graph_->setBrush(QColor(16, 54, 128, 42));
graph_->setAntialiased(true);
graph_->setAdaptiveSampling(true);
current_label_ = new QCPItemText(this);
current_label_->position->setType(QCPItemPosition::ptAxisRectRatio);
current_label_->setPositionAlignment(Qt::AlignRight | Qt::AlignTop);
current_label_->position->setCoords(0.98, 0.04);
current_label_->setPadding(QMargins(6, 3, 6, 3));
current_label_->setBrush(QColor(0, 0, 0, 50));
current_label_->setPen(Qt::NoPen);
current_label_->setLayer("overlay");
current_label_->setClipToAxisRect(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 };
QColor label_bg = QColor(0, 0, 0, 50);
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;
}
if (scheme_->surface_container.isValid()) {
label_bg = QColor(scheme_->surface_container.red(),
scheme_->surface_container.green(),
scheme_->surface_container.blue(), 90);
}
}
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);
}
if (current_label_) {
current_label_->setColor(text_color);
current_label_->setBrush(label_bg);
QFont f = current_label_->font();
f.setBold(true);
current_label_->setFont(f);
}
}
void LinePlot::update_graph() {
if (!initialized_) {
return;
}
if (!graph_) {
graph_ = addGraph();
apply_theme();
}
if (points_.isEmpty()) {
graph_->data()->clear();
reset_graph_range();
if (current_label_) {
current_label_->setText(QStringLiteral("--"));
}
replot();
return;
}
QVector<double> keys(points_.size());
QVector<double> values(points_.size());
double min_key = std::numeric_limits<double>::max();
double max_key = std::numeric_limits<double>::lowest();
double min_val = std::numeric_limits<double>::max();
double max_val = std::numeric_limits<double>::lowest();
for (int i = 0; i < points_.size(); ++i) {
const auto& pt = points_[i];
keys[i] = pt.x();
values[i] = pt.y();
min_key = std::min(min_key, pt.x());
max_key = std::max(max_key, pt.x());
min_val = std::min(min_val, pt.y());
max_val = std::max(max_val, pt.y());
}
graph_->setData(keys, values, true);
if (min_key == std::numeric_limits<double>::max()) {
reset_graph_range();
}
else {
const double key_span = std::max(1e-3, max_key - min_key);
xAxis->setRange(min_key, max_key + key_span * 0.02);
double value_span = max_val - min_val;
if (value_span < 1e-3) {
value_span = std::max(std::abs(max_val), 1.0);
min_val = max_val - value_span * 0.5;
}
const double padding = std::max(value_span * 0.25, 1.0);
yAxis->setRange(min_val - padding, max_val + padding);
}
if (current_label_) {
const double last_val = points_.back().y();
current_label_->setText(QString::number(last_val, 'f', 1));
}
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);
}

View File

@@ -0,0 +1,81 @@
//
// Created by Codex on 2025/12/10.
//
#pragma once
#include "creeper-qt/utility/theme/theme.hh"
#include "creeper-qt/utility/wrapper/common.hh"
#include "creeper-qt/utility/wrapper/property.hh"
#include "creeper-qt/utility/wrapper/widget.hh"
#include "qcustomplot/qcustomplot.h"
#include <QPaintEvent>
#include <QPointF>
#include <QString>
#include <concepts>
#include <optional>
#include <qsize.h>
#include <qvector.h>
namespace creeper {
class SumLinePlot;
namespace line_widget::internal {
class LinePlot: public QCustomPlot {
public:
LinePlot();
~LinePlot() override;
void load_theme_manager(ThemeManager& mgr);
void set_data(const QVector<QPointF>& points);
void set_max_points(int count);
protected:
void paintEvent(QPaintEvent* event) override;
private:
void initialize_plot();
void update_graph();
void apply_theme();
void reset_graph_range();
void trim_points();
QVector<QPointF> points_;
bool initialized_ = false;
std::optional<ColorScheme> scheme_;
QCPGraph* graph_ = nullptr;
QCPItemText* current_label_ = nullptr;
int max_points_ = 240;
double default_y_range_ = 100.0;
};
} // namespace line_widget::internal
namespace line_widget::pro {
using Token = common::Token<internal::LinePlot>;
struct MaxPoints: Token {
int count;
explicit MaxPoints(int c): count{ c } { }
void apply(auto& self) const { self.set_max_points(count); }
};
using PlotData = DerivedProp<Token, QVector<QPointF>, [](auto& self, const auto& vec) {
self.set_data(vec);
}>;
template<class PlotWidget>
concept trait = std::derived_from<PlotWidget, Token>;
CREEPER_DEFINE_CHECKER(trait);
using namespace widget::pro;
using namespace theme::pro;
} // namespace line_widget::pro
struct SumLinePlot
: public Declarative<line_widget::internal::LinePlot,
CheckerOr<line_widget::pro::checker, widget::pro::checker, theme::pro::checker>> {
using Declarative::Declarative;
void paintEvent(QPaintEvent* event) override;
};
} // namespace creeper

View File

@@ -0,0 +1,318 @@
//
// Created by Codex on 2025/12/05.
//
#include "vector_field.hh"
#include <algorithm>
#include <cmath>
#include <vector>
#include <QLinearGradient>
#include <QPainter>
#include <QPainterPath>
#include <QPointF>
using creeper::vector_widget::internal::VectorPlot;
VectorPlot::VectorPlot() {
setBackground(Qt::transparent);
}
VectorPlot::~VectorPlot() = default;
void VectorPlot::load_theme_manager(creeper::ThemeManager& mgr) {
mgr.append_handler(this, [this](const creeper::ThemeManager& manager) {
scheme_ = manager.color_scheme();
apply_color_scheme();
if (initialized_) {
replot();
}
});
}
void VectorPlot::set_matrix_size(const QSize& size) {
if (size == matrix_size_) {
return;
}
matrix_size_ = size;
reset_plot();
}
void VectorPlot::set_data(const QVector<PointData>& data) {
data_points_ = data;
if (initialized_) {
update_vectors();
}
}
void VectorPlot::paintEvent(QPaintEvent* event) {
if (!initialized_) {
initialize_plot();
}
QCustomPlot::paintEvent(event);
// Custom glossy arrow rendering
const auto rect = axisRect();
if (!rect) {
return;
}
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
const QRectF plot_rect = rect->rect();
const double side_pad = plot_rect.width() * 0.24;
QRectF area = plot_rect.adjusted(side_pad, 8.0, -side_pad, -8.0);
const double cx_canvas = area.center().x();
const double cy_canvas = area.center().y();
const double scale = 0.72; // overall shrink to make it slimmer
area.setWidth(area.width() * scale);
area.setHeight(area.height() * scale);
area.moveCenter(QPointF(cx_canvas, cy_canvas));
if (area.width() <= 0.0 || area.height() <= 0.0) {
return;
}
const double w = area.width();
const double h = area.height();
const double cx = area.center().x();
const double shaft_w = w * 0.18;
const double shaft_r = shaft_w * 0.25;
const double head_h = h * 0.36;
const double head_w = w * 0.52;
const double shaft_h = h - head_h;
const double shaft_x = cx - shaft_w * 0.5;
const double shaft_y = area.top();
const double head_base_y = shaft_y + shaft_h - shaft_r * 0.4; // overlap slightly to avoid seam
QPainterPath shaft;
shaft.addRoundedRect(QRectF(shaft_x, shaft_y, shaft_w, shaft_h), shaft_r, shaft_r);
QPainterPath head;
head.moveTo(cx - head_w * 0.5, head_base_y);
head.lineTo(cx + head_w * 0.5, head_base_y);
head.lineTo(cx, area.bottom());
head.closeSubpath();
QPainterPath arrow = shaft.united(head);
// Vibrant orange palette (fixed)
QColor base_color(255, 140, 0);
QColor highlight = base_color.lighter(165);
QColor mid = base_color;
QColor shadow = base_color.darker(180);
QLinearGradient body_grad(area.topLeft(), QPointF(area.left(), area.bottom()));
body_grad.setColorAt(0.0, highlight);
body_grad.setColorAt(0.28, mid);
body_grad.setColorAt(0.72, base_color.darker(110));
body_grad.setColorAt(1.0, shadow);
const double angle_deg = std::atan2(arrow_dir_.y(), arrow_dir_.x()) * 180.0 / 3.14159265358979323846 - 90.0;
QTransform transform;
transform.translate(cx, (shaft_y + head_base_y + area.bottom()) / 3.0); // approximate center
transform.rotate(angle_deg);
transform.translate(-cx, -(shaft_y + head_base_y + area.bottom()) / 3.0);
auto draw_with_transform = [&](const QPainterPath& path, const QBrush& brush, const QPen* pen = nullptr) {
const QPainterPath rotated = transform.map(path);
painter.setBrush(brush);
if (pen) {
painter.setPen(*pen);
}
else {
painter.setPen(Qt::NoPen);
}
painter.drawPath(rotated);
};
draw_with_transform(arrow, QBrush(body_grad));
// Gloss highlight
QPainterPath gloss;
const double gloss_w = shaft_w * 0.42;
QRectF gloss_rect(cx - gloss_w * 0.5, shaft_y + h * 0.04, gloss_w, shaft_h * 0.5);
gloss.addRoundedRect(gloss_rect, gloss_w * 0.4, gloss_w * 0.4);
QLinearGradient gloss_grad(gloss_rect.topLeft(), gloss_rect.bottomLeft());
QColor gloss_hi = Qt::white;
gloss_hi.setAlpha(190);
QColor gloss_lo = Qt::white;
gloss_lo.setAlpha(40);
gloss_grad.setColorAt(0.0, gloss_hi);
gloss_grad.setColorAt(1.0, gloss_lo);
draw_with_transform(gloss, QBrush(gloss_grad));
// Head specular highlights
const double spec_w = head_w * 0.18;
QRectF spec_left(cx - head_w * 0.32, head_base_y + head_h * 0.08, spec_w, head_h * 0.2);
QRectF spec_right(cx + head_w * 0.14, head_base_y + head_h * 0.1, spec_w * 0.8, head_h * 0.2);
auto paint_spec = [&](const QRectF& r) {
QPainterPath p;
p.addRoundedRect(r, r.width() * 0.4, r.height() * 0.6);
QLinearGradient g(r.topLeft(), r.bottomLeft());
QColor hi = Qt::white;
hi.setAlpha(180);
QColor lo = Qt::white;
lo.setAlpha(30);
g.setColorAt(0.0, hi);
g.setColorAt(1.0, lo);
draw_with_transform(p, QBrush(g));
};
paint_spec(spec_left);
paint_spec(spec_right);
}
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);
primary_arrow_->setVisible(false); // 使用自绘 3D 箭头
}
}
void VectorPlot::update_vectors() {
if (!initialized_) {
return;
}
ensure_primary_arrow();
const int width = std::max(1, matrix_size_.width());
const int height = std::max(1, matrix_size_.height());
const int expected = width * height;
if (expected <= 0 || !primary_arrow_) {
replot();
return;
}
std::vector<double> values(static_cast<std::size_t>(expected), 0.0);
for (const auto& item: data_points_) {
const int x = static_cast<int>(item.x);
const int y = static_cast<int>(item.y);
if (x >= 0 && x < width && y >= 0 && y < height) {
const int idx = y * width + x;
values[static_cast<std::size_t>(idx)] = item.z;
}
}
auto value_at = [&](int x, int y) -> double {
if (x < 0 || x >= width || y < 0 || y >= height) {
return 0.0;
}
return values[static_cast<std::size_t>(y * width + x)];
};
std::vector<std::pair<double, double>> grads(static_cast<std::size_t>(expected), { 0.0, 0.0 });
double max_mag = 0.0;
double sum_gx = 0.0;
double sum_gy = 0.0;
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
const double gx = 0.5 * (value_at(x + 1, y) - value_at(x - 1, y));
const double gy = 0.5 * (value_at(x, y + 1) - value_at(x, y - 1));
const double mag = std::sqrt(gx * gx + gy * gy);
grads[static_cast<std::size_t>(y * width + x)] = { gx, gy };
max_mag = std::max(max_mag, mag);
sum_gx += gx;
sum_gy += gy;
}
}
const double scale = (max_mag > 1e-6) ? (0.35 / max_mag) : 0.0;
const bool fallback = max_mag <= 1e-6;
const double mid_x = static_cast<double>(width) * 0.5;
const double mid_y = static_cast<double>(height) * 0.5;
double dir_x = sum_gx;
double dir_y = sum_gy;
if (fallback || std::abs(dir_x) + std::abs(dir_y) < 1e-6) {
dir_x = 0.0;
dir_y = -1.0; // 默认向上指,保证可见
}
double dir_len = std::sqrt(dir_x * dir_x + dir_y * dir_y);
if (dir_len < 1e-6) {
dir_x = 0.0;
dir_y = -1.0;
dir_len = 1.0;
}
dir_x /= dir_len;
dir_y /= dir_len;
arrow_dir_ = QPointF(dir_x, dir_y);
const double arrow_len = 0.48 * std::min(width, height); // 稍长的指针
const double tail_ratio = 0.25; // 穿过中心的尾巴略长
const double tail_len = arrow_len * tail_ratio;
const double head_len = arrow_len - tail_len;
const double backoff = std::max(0.5, head_len * 0.12); // 轻微回缩,避免方头顶出
const double head_base = std::max(0.0, head_len - backoff);
const double cx = static_cast<double>(width) * 0.5;
const double cy = static_cast<double>(height) * 0.5;
if (primary_arrow_) {
primary_arrow_->start->setCoords(cx - dir_x * tail_len, cy - dir_y * tail_len);
primary_arrow_->end->setCoords(cx + dir_x * head_base, cy + dir_y * head_base);
primary_arrow_->setVisible(false); // 位置保留以备需要,但当前使用自绘 3D 箭头
}
replot();
}
using namespace creeper;
void VectorFieldPlot::paintEvent(QPaintEvent* event) {
vector_widget::internal::VectorPlot::paintEvent(event);
}

View File

@@ -0,0 +1,84 @@
//
// Created by Codex on 2025/12/05.
//
#pragma once
#include "components/charts/heatmap.hh" // for PointData definition
#include "creeper-qt/utility/theme/theme.hh"
#include "creeper-qt/utility/wrapper/common.hh"
#include "creeper-qt/utility/wrapper/property.hh"
#include "creeper-qt/utility/wrapper/widget.hh"
#include "qcustomplot/qcustomplot.h"
#include <optional>
#include <QPaintEvent>
#include <concepts>
#include <qsize.h>
#include <qpoint.h>
#include <qvector.h>
namespace creeper {
class VectorFieldPlot;
namespace vector_widget::internal {
class VectorPlot : public QCustomPlot {
public:
VectorPlot();
~VectorPlot() override;
void load_theme_manager(ThemeManager& mgr);
void set_matrix_size(const QSize& size);
void set_data(const QVector<PointData>& data);
protected:
void paintEvent(QPaintEvent* event) override;
private:
void initialize_plot();
void reset_plot();
void update_vectors();
void ensure_arrows();
void apply_color_scheme();
QSize matrix_size_{ 3, 4 };
QVector<PointData> data_points_;
bool initialized_ = false;
std::optional<ColorScheme> scheme_;
QColor arrow_color_{ 16, 54, 128 }; // 深蓝色
QPointF arrow_dir_{ 0.0, 1.0 };
QCPItemLine* primary_arrow_ = nullptr;
void ensure_primary_arrow();
};
} // namespace vector_widget::internal
namespace vector_widget::pro {
using Token = common::Token<internal::VectorPlot>;
struct MatrixSize : Token {
QSize size;
explicit MatrixSize(const QSize& s) : size{ s } { }
explicit MatrixSize(int w, int h) : size{ w, h } { }
void apply(auto& self) const { self.set_matrix_size(size); }
};
using PlotData = DerivedProp<Token, QVector<PointData>, [](auto& self, const auto& vec) {
self.set_data(vec);
}>;
template<class PlotWidget>
concept trait = std::derived_from<PlotWidget, Token>;
CREEPER_DEFINE_CHECKER(trait);
using namespace widget::pro;
using namespace theme::pro;
} // namespace vector_widget::pro
struct VectorFieldPlot
: public Declarative<vector_widget::internal::VectorPlot,
CheckerOr<vector_widget::pro::checker, widget::pro::checker, theme::pro::checker>> {
using Declarative::Declarative;
void paintEvent(QPaintEvent* event) override;
};
} // namespace creeper

View File

@@ -1,9 +1,11 @@
#pragma once
#include <cstdint>
#include <cstddef>
#include <future>
#include <string_view>
#include <vector>
#include <initializer_list>
#include <nlohmann/json.hpp>
namespace ffmsep {
@@ -21,7 +23,8 @@ enum class CPMediaType : std::uint8_t {
enum class CPCodecID : std::uint32_t {
Unknow = 0,
Tactile = 0x54514354u // 'T','Q','C','T':触觉传感器协议标识 Tactile Quick Codec Type
Tactile = 0x54514354u, // 'T','Q','C','T':触觉传感器协议标识 Tactile Quick Codec Type
PiezoresistiveB = 0x54514342u // 'T','Q','C','B'压阻B测试协议
};
struct CPPacket {
@@ -59,6 +62,7 @@ struct CPCodec {
using CloseFn = void(*)(CPCodecContext*);
using SendPacketFn = int(*)(CPCodecContext*, const CPPacket&);
using ReceiveFrameFn = int(*)(CPCodecContext*, CPFrame&);
using PresistFrameFn = int(*)(CPCodecContext*);
const char* name = nullptr;
const char* long_name = nullptr;
@@ -69,11 +73,13 @@ struct CPCodec {
CloseFn close = nullptr;
SendPacketFn send_packet = nullptr;
ReceiveFrameFn receive_frame = nullptr;
PresistFrameFn presistend = nullptr;
};
struct CPCodecContext {
const CPCodec* codec = nullptr;
void* priv_data = nullptr;
void* record_data = nullptr;
CPMediaType codec_type = CPMediaType::Unknow;
bool is_open = false;

View File

@@ -2,7 +2,7 @@
#include "components/ffmsep/presist/presist.hh"
#include "dlog/dlog.hh"
#include <iostream>
#include <algorithm>
#include <atomic>
#include <chrono>
@@ -11,19 +11,24 @@
#include <cstdint>
#include <deque>
#include <future>
#include <ios>
#include <memory>
#include <mutex>
#include <optional>
#include <qlogging.h>
#include <thread>
#include <utility>
#include <vector>
#include <qdebug.h>
#include <iostream>
using namespace std::chrono_literals;
namespace ffmsep {
namespace {
constexpr auto kReaderIdleSleep = 5ms;
constexpr auto kReaderIdleSleep = 5ms;
constexpr auto kDecoderIdleSleep = 1ms;
const CPCodec* resolve_requested_codec(const CPStreamConfig& config) {
@@ -45,13 +50,12 @@ const CPCodec* resolve_requested_codec(const CPStreamConfig& config) {
struct CPStreamCore::Impl {
struct Packet {
std::vector<std::uint8_t> 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<persist::JsonWritter>();
}
@@ -108,13 +112,13 @@ struct CPStreamCore::Impl {
try {
auto serial = std::make_shared<serial::Serial>(
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();
}
@@ -124,19 +128,22 @@ struct CPStreamCore::Impl {
std::lock_guard<std::mutex> 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_);
@@ -176,7 +183,8 @@ struct CPStreamCore::Impl {
if (serial_->isOpen()) {
serial_->close();
}
} catch (...) {
}
catch (...) {
// Ignore close errors.
}
serial_.reset();
@@ -223,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);
@@ -297,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;
@@ -340,7 +351,7 @@ struct CPStreamCore::Impl {
}
{
std::lock_guard<std::mutex> 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();
@@ -374,12 +385,11 @@ struct CPStreamCore::Impl {
if (snapshot.empty()) {
std::promise<persist::WriteResult> 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;
}
@@ -408,6 +418,7 @@ struct CPStreamCore::Impl {
std::vector<std::uint8_t> buffer(config_.read_chunk_size);
while (!stop_requested_.load(std::memory_order_acquire)) {
std::shared_ptr<serial::Serial> serial_copy;
{
std::lock_guard<std::mutex> lock(serial_mutex_);
@@ -421,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;
@@ -439,9 +454,26 @@ struct CPStreamCore::Impl {
std::this_thread::sleep_for(kReaderIdleSleep);
continue;
}
const auto format_command =
[](const std::vector<std::uint8_t>& data) -> std::string {
if (data.empty()) {
return "[]";
}
std::ostringstream oss;
oss << '[' << std::uppercase << std::setfill('0');
for (std::size_t idx = 0; idx < data.size(); ++idx) {
if (idx != 0U) {
oss << ' ';
}
oss << std::setw(2) << std::hex << static_cast<unsigned int>(data[idx]);
}
oss << ']';
return oss.str();
};
Packet packet;
packet.payload.assign(buffer.begin(), buffer.begin() + static_cast<std::ptrdiff_t>(bytes_read));
// std::cout << "======payload======" << std::endl;
// std::cout << format_command(packet.payload) << std::endl;
packet.pts = pts_counter_.fetch_add(1, std::memory_order_relaxed);
{
@@ -456,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;
}
@@ -511,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) {
@@ -529,20 +561,24 @@ struct CPStreamCore::Impl {
CPFrame frame;
rc = cpcodec_receive_frame(codec_ctx_, &frame);
if (rc == CP_SUCCESS) {
auto decoded = std::make_shared<DecodedFrame>();
decoded->pts = frame.pts;
auto decoded = std::make_shared<DecodedFrame>();
decoded->pts = frame.pts;
decoded->received_at = std::chrono::steady_clock::now();
decoded->frame = std::move(frame);
decoded->id = codec_descriptor_ ? codec_descriptor_->id : CPCodecID::Unknow;
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;
{
@@ -559,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;
}
@@ -582,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<std::mutex> lock(packet_mutex_);
@@ -611,7 +649,7 @@ struct CPStreamCore::Impl {
const CPCodec* codec_descriptor_ = nullptr;
std::shared_ptr<serial::Serial> serial_;
mutable std::mutex serial_mutex_;
mutable std::mutex serial_mutex_;
CPCodecContext* codec_ctx_ = nullptr;
@@ -619,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> packet_queue_;
std::deque<Packet> packet_queue_;
mutable std::mutex frame_mutex_;
mutable std::mutex frame_mutex_;
std::condition_variable frame_cv_;
// std::deque<DecodedFrame> frame_queue_;
// 更新为智能指针,我们需要更长的生命周期😊
std::deque<std::shared_ptr<DecodedFrame>> frame_queue_;
std::deque<std::shared_ptr<DecodedFrame>> frame_record_queue_;
std::size_t frame_queue_capacity_ = 16;
std::size_t frame_queue_capacity_ = 16;
FrameCallback frame_callback_;
FrameCallback frame_callback_;
mutable std::mutex callback_mutex_;
std::atomic<bool> running_{false};
std::atomic<bool> stop_requested_{false};
std::atomic<std::int64_t> pts_counter_{0};
std::atomic<bool> running_{ false };
std::atomic<bool> stop_requested_{ false };
std::atomic<std::int64_t> pts_counter_{ 0 };
std::string last_error_;
std::string last_error_;
mutable std::mutex last_error_mutex_;
std::unique_ptr<persist::JsonWritter> frame_writer_;
};
CPStreamCore::CPStreamCore(CPStreamConfig config)
: impl_(std::make_unique<Impl>(std::move(config))) {}
CPStreamCore::CPStreamCore(CPStreamConfig config): impl_(std::make_unique<Impl>(std::move(config))) {}
CPStreamCore::~CPStreamCore() {
if (impl_) {

View File

@@ -11,6 +11,8 @@
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <nlohmann/json_fwd.hpp>
#include <sstream>
#include <system_error>
#include <nlohmann/json.hpp>
@@ -20,6 +22,57 @@ 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::string& timestamp,
const std::vector<std::uint8_t>& payload) {
// First column: receive local time (YYYYMMDDHHMMSS). Then payload every 2 bytes -> uint16.
std::ostringstream oss;
oss << timestamp;
bool first = false; // timestamp already placed
for (std::size_t idx = 0; idx + 1U < payload.size(); idx += 2U) {
const auto value =
static_cast<std::uint16_t>(payload[idx]) | static_cast<std::uint16_t>(payload[idx + 1U] << 8U);
if (!first) {
oss << ',';
}
first = false;
oss << value;
}
return oss.str();
}
} // namespace
namespace {
using nlohmann::json;
std::string format_receive_time(const std::chrono::steady_clock::time_point& received) {
// Map steady_clock timestamp to system_clock using the current offset, then format as YYYYMMDDHHMMSS.
const auto now_sys = std::chrono::system_clock::now();
const auto now_steady = std::chrono::steady_clock::now();
const auto steady_delta = received - now_steady;
const auto sys_tp = now_sys + steady_delta;
const auto sys_ms_tp = std::chrono::time_point_cast<std::chrono::milliseconds>(sys_tp);
const auto ms_part = std::chrono::duration_cast<std::chrono::milliseconds>(sys_ms_tp.time_since_epoch()) % 1000;
const auto sys_sec_tp = std::chrono::time_point_cast<std::chrono::seconds>(sys_ms_tp);
const std::time_t tt = std::chrono::system_clock::to_time_t(sys_sec_tp);
std::tm tm{};
#if defined(_WIN32)
localtime_s(&tm, &tt);
#else
localtime_r(&tt, &tm);
#endif
std::ostringstream oss;
oss << std::put_time(&tm, "%Y%m%d%H%M%S")
<< std::setw(3) << std::setfill('0') << ms_part.count();
return oss.str();
}
bool is_simple_array(const json& value) {
if (!value.is_array()) {
return false;
@@ -30,11 +83,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<std::size_t>(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<std::size_t>(indent), ' ');
const auto child_indent = indent + indent_step;
const auto child_indent_str = std::string(static_cast<std::size_t>(child_indent), ' ');
if (value.is_object()) {
@@ -48,7 +101,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 +126,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 +134,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 +144,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<std::chrono::nanoseconds>(received).count();
std::chrono::duration_cast<std::chrono::nanoseconds>(received).count();
if (frame.tactile_matrix_size) {
result["matrix"] = {
{"long_edge", frame.tactile_matrix_size->long_edge},
{"short_edge", frame.tactile_matrix_size->short_edge},
{ "long_edge", frame.tactile_matrix_size->long_edge },
{ "short_edge", frame.tactile_matrix_size->short_edge },
};
}
if (frame.tactile) {
const auto& tactile = *frame.tactile;
result["tactile"] = {
{"device_address", tactile.device_address},
{"response_function", tactile.response_function},
{"function", static_cast<std::uint8_t>(tactile.function)},
{"start_address", tactile.start_address},
{"return_byte_count", tactile.return_byte_count},
{"status", tactile.status},
{"payload", tactile.payload},
result["tactile"] = {
{ "device_address", tactile.device_address },
{ "response_function", tactile.response_function },
{ "function", static_cast<std::uint8_t>(tactile.function) },
{ "start_address", tactile.start_address },
{ "return_byte_count", tactile.return_byte_count },
{ "status", tactile.status },
{ "payload", tactile.payload },
};
}
return result;
}
} // namespace
bool WriteQueue::push(WriteRequest&& req) {
@@ -158,21 +212,20 @@ void WriteQueue::stop() {
cond_.notify_all();
}
JsonWritter::JsonWritter()
: write_thread_([this] { run(); }) {}
JsonWritter::JsonWritter(): write_thread_([this] { run(); }) {}
JsonWritter::~JsonWritter() {
stop();
}
std::future<WriteResult> JsonWritter::enqueue(std::string path,
std::deque<std::shared_ptr<DecodedFrame>> frames) {
std::deque<std::shared_ptr<DecodedFrame>> frames) {
std::promise<WriteResult> 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 +238,68 @@ 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<std::shared_ptr<DecodedFrame>> frames) {
std::deque<std::shared_ptr<DecodedFrame>> 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<std::uint8_t> payload;
if (frame->id == CPCodecID::Tactile && frame->tactile) {
payload = frame->tactile->payload;
}
else if (frame->id == CPCodecID::PiezoresistiveB) {
payload = tactile::extract_piezoresistive_b_payload(frame->frame);
}
if (payload.empty()) {
continue;
}
const auto timestamp = format_receive_time(frame->received_at);
const auto row = payload_to_csv_row(timestamp, 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() {

View File

@@ -2,30 +2,60 @@
#include "components/ffmsep/cpdecoder.hh"
#include <algorithm>
#include <array>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <new>
#include <optional>
#include <qlogging.h>
#include <vector>
#include <qdebug.h>
#include <iostream>
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<std::uint8_t, 2> kStartSequence{
kStartByteFirst,
kStartByteSecond
};
constexpr std::size_t kAbsoluteMaxPayloadBytes = 4096U; // 硬上限,防止异常配置撑爆内存
constexpr std::array<std::uint8_t, 2> kPiezoresistiveBStartSequence{
kPiezoresistiveBStartByteFirst,
kPiezoresistiveBStartByteSecond
};
constexpr std::array<std::uint8_t, 2> kPiezoresistiveBEndSequence{
kPiezoresistiveBEndByteFirst,
kPiezoresistiveBEndByteSecond
};
constexpr std::size_t kPiezoresistiveBPayloadSize =
kPiezoresistiveBValueCount * 2U;
constexpr std::size_t kPiezoresistiveBFrameSize =
kPiezoresistiveBStartSequence.size() + kPiezoresistiveBPayloadSize + kPiezoresistiveBEndSequence.size();
struct TactileDecoderContext {
std::vector<std::uint8_t> 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::size_t>(
std::max<std::size_t>(payload_bytes, 2U),
kAbsoluteMaxPayloadBytes);
max_payload_bytes = clamped_payload;
max_frame_bytes = kHeaderSize + kFixedSectionSize + max_payload_bytes + 1U;
max_fifo_bytes = max_frame_bytes * 2U;
}
};
const std::uint8_t* buffer_data(const std::vector<std::uint8_t>& buf) {
@@ -33,24 +63,78 @@ const std::uint8_t* buffer_data(const std::vector<std::uint8_t>& buf) {
}
std::uint8_t crc8_with_xorout(const std::uint8_t* data, std::size_t length) {
#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<std::uint8_t>((reg << 1U) ^ kCrcPolynomial);
} else {
}
else {
reg = static_cast<std::uint8_t>(reg << 1U);
}
}
}
return static_cast<std::uint8_t>(reg ^ kCrcXorOut);
#endif
constexpr std::uint8_t kPolynomial = 0x07;
constexpr std::uint8_t kInitial = 0x00;
constexpr std::uint8_t kXorOut =
0x55; // CRC-8/ITU params match device expectation
std::uint8_t reg = kInitial;
for (std::size_t idx = 0; idx < length; ++idx) {
reg = static_cast<std::uint8_t>(reg ^ data[idx]);
for (int bit = 0; bit < 8; ++bit) {
if ((reg & 0x80U) != 0U) {
reg = static_cast<std::uint8_t>((reg << 1U) ^ kPolynomial);
}
else {
reg = static_cast<std::uint8_t>(reg << 1U);
}
}
}
return static_cast<std::uint8_t>(reg ^ kXorOut);
}
TactileDecoderContext* get_priv(CPCodecContext* ctx) {
return ctx ? ctx->priv_as<TactileDecoderContext>() : nullptr;
}
template<std::size_t N>
void keep_partial_start_prefix(std::vector<std::uint8_t>& buf, const std::array<std::uint8_t, N>& start_sequence) {
if (buf.empty() || N == 0U) {
return;
}
const std::size_t max_prefix = std::min<std::size_t>(N - 1U, buf.size());
for (std::size_t len = max_prefix; len > 0; --len) {
const auto seq_begin = start_sequence.begin();
const auto seq_end = seq_begin + static_cast<std::ptrdiff_t>(len);
const auto buf_begin =
buf.end() - static_cast<std::ptrdiff_t>(len);
if (std::equal(seq_begin, seq_end, buf_begin)) {
std::vector<std::uint8_t> tail(buf_begin, buf.end());
buf.swap(tail);
return;
}
}
buf.clear();
}
void trim_fifo_if_needed(std::vector<std::uint8_t>& buf, std::size_t max_fifo_bytes) {
if (buf.size() <= max_fifo_bytes) {
return;
}
const auto excess = buf.size() - max_fifo_bytes;
buf.erase(buf.begin(), buf.begin() + static_cast<std::ptrdiff_t>(excess));
}
std::atomic<std::size_t>& expected_payload_bytes_for_tactile() {
static std::atomic<std::size_t> expected{kPiezoresistiveBPayloadSize};
return expected;
}
int tactile_init(CPCodecContext* ctx) {
if (!ctx) {
return CP_ERROR_INVALID_ARGUMENT;
@@ -60,6 +144,14 @@ int tactile_init(CPCodecContext* ctx) {
}
auto* storage = static_cast<TactileDecoderContext*>(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;
}
@@ -80,11 +172,12 @@ int tactile_send_packet(CPCodecContext* ctx, const CPPacket& packet) {
if (packet.flush) {
priv->fifo.clear();
priv->end_of_stream = false;
priv->next_pts = 0;
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) {
@@ -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<std::uint16_t>(data[2]) |
static_cast<std::uint16_t>(static_cast<std::uint16_t>(data[3]) << 8U);
static_cast<std::uint16_t>(data[2]) | static_cast<std::uint16_t>(static_cast<std::uint16_t>(data[3]) << 8U);
if (data_length < kFixedSectionSize) {
buf.erase(buf.begin());
@@ -151,6 +242,10 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
}
const std::size_t total_frame_length = kHeaderSize + static_cast<std::size_t>(data_length) + 1U;
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,109 +255,187 @@ 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<std::size_t>(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<std::ptrdiff_t>(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<std::ptrdiff_t>(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
};
int tactile_b_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
auto* priv = get_priv(ctx);
if (!priv) {
return CP_ERROR_INVALID_STATE;
}
auto& buf = priv->fifo;
while (true) {
if (buf.size() < kPiezoresistiveBStartSequence.size()) {
if (priv->end_of_stream) {
buf.clear();
priv->end_of_stream = false;
return CP_ERROR_EOF;
}
return CP_ERROR_EAGAIN;
}
const auto start_it = std::search(buf.begin(),
buf.end(),
kPiezoresistiveBStartSequence.begin(),
kPiezoresistiveBStartSequence.end());
if (start_it == buf.end()) {
keep_partial_start_prefix(buf, kPiezoresistiveBStartSequence);
if (priv->end_of_stream) {
priv->end_of_stream = false;
return CP_ERROR_EOF;
}
return CP_ERROR_EAGAIN;
}
if (start_it != buf.begin()) {
buf.erase(buf.begin(), start_it);
}
if (buf.size() < kPiezoresistiveBFrameSize) {
if (priv->end_of_stream) {
buf.clear();
priv->end_of_stream = false;
return CP_ERROR_EOF;
}
return CP_ERROR_EAGAIN;
}
const auto end_offset = kPiezoresistiveBFrameSize - kPiezoresistiveBEndSequence.size();
const auto end_it = buf.begin() + static_cast<std::ptrdiff_t>(end_offset);
if (!std::equal(end_it,
end_it + static_cast<std::ptrdiff_t>(kPiezoresistiveBEndSequence.size()),
kPiezoresistiveBEndSequence.begin())) {
buf.erase(buf.begin());
continue;
}
frame.data.assign(buf.begin(),
buf.begin() + static_cast<std::ptrdiff_t>(kPiezoresistiveBFrameSize));
frame.pts = priv->next_pts++;
frame.key_frame = true;
frame.valid = true;
buf.erase(buf.begin(),
buf.begin() + static_cast<std::ptrdiff_t>(kPiezoresistiveBFrameSize));
return CP_SUCCESS;
}
}
const CPCodec kTactileCodec{
.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<TactileFrame> parse_frame(const CPFrame& frame) {
if (!frame.valid || frame.data.size() < kMinimumFrameSize) {
return std::nullopt;
}
const auto* bytes = frame.data.data();
const std::size_t size = frame.data.size();
// 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<std::uint16_t>(bytes[2]) |
static_cast<std::uint16_t>(static_cast<std::uint16_t>(bytes[3]) << 8U);
static_cast<std::uint16_t>(bytes[2]) | static_cast<std::uint16_t>(static_cast<std::uint16_t>(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<std::size_t>(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<std::uint32_t>(bytes[7]) |
(static_cast<std::uint32_t>(bytes[8]) << 8U) |
(static_cast<std::uint32_t>(bytes[9]) << 16U) |
(static_cast<std::uint32_t>(bytes[10]) << 24U);
static_cast<std::uint32_t>(bytes[7]) | (static_cast<std::uint32_t>(bytes[8]) << 8U) | (static_cast<std::uint32_t>(bytes[9]) << 16U) | (static_cast<std::uint32_t>(bytes[10]) << 24U);
const std::uint16_t return_byte_count =
static_cast<std::uint16_t>(bytes[11]) |
(static_cast<std::uint16_t>(bytes[12]) << 8U);
static_cast<std::uint16_t>(bytes[11]) | (static_cast<std::uint16_t>(bytes[12]) << 8U);
const std::uint8_t status = bytes[13];
const std::size_t payload_offset = kHeaderSize + kFixedSectionSize;
const std::size_t payload_length = static_cast<std::size_t>(data_length) - kFixedSectionSize;
if (payload_length != return_byte_count) {
const std::size_t payload_available =
data_length > kFixedSectionSize ? static_cast<std::size_t>(data_length) - kFixedSectionSize : 0U;
const std::size_t requested_payload = static_cast<std::size_t>(return_byte_count);
if (payload_available < requested_payload) {
return std::nullopt;
}
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<FunctionCode>(response_function & 0x7FU);
parsed.start_address = start_address;
parsed.function = static_cast<FunctionCode>(response_function & 0x7FU);
parsed.start_address = start_address;
parsed.return_byte_count = return_byte_count;
parsed.status = status;
parsed.payload.assign(bytes + payload_offset, bytes + payload_offset + payload_length);
parsed.status = status;
parsed.payload.assign(bytes + payload_offset, bytes + payload_offset + requested_payload);
return parsed;
}
std::vector<std::uint16_t> parse_pressure_values(const TactileFrame& frame) {
if (frame.payload.size() != frame.return_byte_count) {
return {};
}
if (frame.payload.empty() || (frame.payload.size() % 2U != 0U)) {
std::cout << "parse_pressure_values" << std::endl;
const auto requested_bytes = static_cast<std::size_t>(frame.return_byte_count);
const auto usable_bytes = std::min(requested_bytes, frame.payload.size());
if (usable_bytes == 0U || (usable_bytes % 2U != 0U)) {
return {};
}
std::vector<std::uint16_t> values;
values.reserve(frame.payload.size() / 2U);
for (std::size_t idx = 0; idx + 1U < frame.payload.size(); idx += 2U) {
values.reserve(usable_bytes / 2U);
for (std::size_t idx = 0; idx + 1U < usable_bytes; idx += 2U) {
const std::uint16_t value = static_cast<std::uint16_t>(
static_cast<std::uint16_t>(frame.payload[idx]) |
static_cast<std::uint16_t>(frame.payload[idx + 1U] << 8U));
static_cast<std::uint16_t>(frame.payload[idx]) | static_cast<std::uint16_t>(frame.payload[idx + 1U] << 8U));
values.push_back(value);
}
return values;
@@ -273,11 +446,76 @@ std::optional<MatrixSize> parse_matrix_size_payload(const TactileFrame& frame) {
return std::nullopt;
}
MatrixSize size{};
size.long_edge = frame.payload[0];
size.long_edge = frame.payload[0];
size.short_edge = frame.payload[1];
return size;
}
std::vector<std::uint16_t> parse_piezoresistive_b_pressures(const CPFrame& frame) {
// if (!frame.valid) {
// return {};
// }
// if (frame.data.size() != kPiezoresistiveBFrameSize) {
// return {};
// }
// if (frame.data.size() < kPiezoresistiveBFrameSize) {
// return {};
// }
// if (frame.data[0] != kPiezoresistiveBStartByteFirst || frame.data[1] != kPiezoresistiveBStartByteSecond) {
// return {};
// }
const auto end_offset = kPiezoresistiveBFrameSize - kPiezoresistiveBEndSequence.size();
// if (frame.data[end_offset] != kPiezoresistiveBEndByteFirst || frame.data[end_offset + 1U] != kPiezoresistiveBEndByteSecond) {
// return {};
// }
std::vector<std::uint16_t> values;
values.reserve(kPiezoresistiveBValueCount);
std::cout << "valuessize:" << values.size() << std::endl;
const auto payload_offset = kPiezoresistiveBStartSequence.size();
for (std::size_t idx = 0; idx < kPiezoresistiveBValueCount; ++idx) {
const auto base = payload_offset + idx * 2U;
if (base + 1U >= frame.data.size()) {
break;
}
const auto hi = static_cast<std::uint16_t>(frame.data[base]);
const auto lo = static_cast<std::uint16_t>(frame.data[base + 1U]);
values.push_back(static_cast<std::uint16_t>((hi << 8U) | lo));
}
return values;
}
std::vector<std::uint8_t> extract_piezoresistive_b_payload(const CPFrame& frame) {
if (!frame.valid) {
return {};
}
if (frame.data.size() != kPiezoresistiveBFrameSize) {
return {};
}
if (frame.data[0] != kPiezoresistiveBStartByteFirst || frame.data[1] != kPiezoresistiveBStartByteSecond) {
return {};
}
const auto payload_offset = kPiezoresistiveBStartSequence.size();
const auto payload_end = payload_offset + kPiezoresistiveBPayloadSize;
if (frame.data.size() < payload_end + kPiezoresistiveBEndSequence.size()) {
return {};
}
if (frame.data[payload_end] != kPiezoresistiveBEndByteFirst || frame.data[payload_end + 1U] != kPiezoresistiveBEndByteSecond) {
return {};
}
return std::vector<std::uint8_t>(
frame.data.begin() + static_cast<std::ptrdiff_t>(payload_offset),
frame.data.begin() + static_cast<std::ptrdiff_t>(payload_end));
}
void set_tactile_expected_payload_bytes(std::size_t bytes) {
const auto clamped = std::min<std::size_t>(
std::max<std::size_t>(bytes, 2U),
kAbsoluteMaxPayloadBytes);
expected_payload_bytes_for_tactile().store(clamped, std::memory_order_relaxed);
}
const CPCodec* tactile_codec() {
return &kTactileCodec;
}
@@ -285,4 +523,12 @@ const CPCodec* tactile_codec() {
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

View File

@@ -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<std::uint16_t> parse_pressure_values(const TactileFrame &frame);
std::optional<MatrixSize> parse_matrix_size_payload(const TactileFrame &frame);
std::optional<MatrixSize>
parse_patrix_coordinate_payload(const TactileFrame &frame);
std::vector<std::uint16_t> parse_piezoresistive_b_pressures(const CPFrame &frame);
std::vector<std::uint8_t> extract_piezoresistive_b_payload(const CPFrame &frame);
// 配置触觉 A 类型预期的 payload 字节数(点数 * 2用于限制解码 FIFO。
void set_tactile_expected_payload_bytes(std::size_t bytes);
const CPCodec *tactile_codec();
void register_tactile_codec();
const CPCodec *tactile_b_codec();
void register_tactile_b_codec();
} // namespace ffmsep::tactile

View File

@@ -92,6 +92,23 @@ auto NavComponent(NavComponentState& state) noexcept -> raw_pointer<QWidget> {
ic::FontIcon {material::icon::kLogout},
ic::Clickable {&app::quit},
},
ln::Item<IconButton> {
{0, Qt::AlignHCenter},
navigation_icons_config,
ic::ColorFilled,
ic::FontIcon {material::icon::k123},
MutableTransform{ [](auto& self, bool show_numbers) {
self.set_types(show_numbers
? icon_button::internal::IconButton::Types::TOGGLE_SELECTED
: icon_button::internal::IconButton::Types::TOGGLE_UNSELECTED);
},
state.heatmap_show_numbers },
ic::Clickable{ [ctx = state.heatmap_show_numbers] {
if (ctx) {
ctx->set(!ctx->get());
}
} },
},
ln::Item<IconButton> {
{0, Qt::AlignHCenter},
navigation_icons_config,
@@ -102,4 +119,3 @@ auto NavComponent(NavComponentState& state) noexcept -> raw_pointer<QWidget> {
}
};
}

View File

@@ -42,6 +42,12 @@
#include <QLineEdit>
#include <QSpinBox>
#include <QVBoxLayout>
#include <QFileDialog>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMessageBox>
#include <sys/stat.h>
namespace repest_literals {
@@ -66,6 +72,61 @@ namespace fbpro = filled_button::pro;
static std::weak_ptr<MutableValue<std::vector<ConfigProfile>>> g_profiles_store;
static std::function<void()> g_profiles_refresh;
static QString TactileTypeToJsonString(Tactile_TYPE type)
{
switch (type) {
case Tactile_TYPE::PiezoresistiveA:
return QStringLiteral("PiezoresistiveA");
case Tactile_TYPE::PiezoresistiveB:
return QStringLiteral("PiezoresistiveB");
case Tactile_TYPE::Hall:
default:
return QStringLiteral("Hall");
}
}
static Tactile_TYPE TactileTypeFromJsonString(const QString& str)
{
if (str == QStringLiteral("PiezoresistiveA")) {
return Tactile_TYPE::PiezoresistiveA;
}
if (str == QStringLiteral("PiezoresistiveB")) {
return Tactile_TYPE::PiezoresistiveB;
}
return Tactile_TYPE::Hall;
}
static QJsonObject ConfigProfileToJson(const ConfigProfile& profile)
{
QJsonObject obj;
obj.insert(QStringLiteral("name"), profile.name);
obj.insert(QStringLiteral("type"), TactileTypeToJsonString(profile.type));
obj.insert(QStringLiteral("matrix_width"), profile.matrix_width);
obj.insert(QStringLiteral("matrix_height"), profile.matrix_height);
obj.insert(QStringLiteral("range_left"), profile.range_left);
obj.insert(QStringLiteral("range_right"), profile.range_right);
obj.insert(QStringLiteral("baud_rate"), profile.baud_rate);
return obj;
}
static bool ConfigProfileFromJson(const QJsonObject& obj, ConfigProfile& out_profile)
{
const auto name = obj.value(QStringLiteral("name")).toString();
if (name.isEmpty()) {
return false;
}
out_profile.name = name;
out_profile.type = TactileTypeFromJsonString(obj.value(QStringLiteral("type")).toString());
out_profile.matrix_width = obj.value(QStringLiteral("matrix_width")).toInt(0);
out_profile.matrix_height = obj.value(QStringLiteral("matrix_height")).toInt(0);
out_profile.range_left = obj.value(QStringLiteral("range_left")).toInt(0);
out_profile.range_right = obj.value(QStringLiteral("range_right")).toInt(0);
out_profile.baud_rate = obj.value(QStringLiteral("baud_rate")).toInt(0);
return true;
}
static void ShowEditProfileDialog(
const ConfigProfile& current,
const std::shared_ptr<MutableValue<std::vector<ConfigProfile>>>& profiles_store) {
@@ -244,6 +305,134 @@ static auto AddProfileLongItem(creeper::ThemeManager& manager) {
fbpro::Clickable {[]{ ShowAddProfileDialog(); }},
};
}
static auto ImportProfileLongItem(creeper::ThemeManager& manager) {
return new FilledButton {
fbpro::ThemeManager {manager},
fbpro::Text {QStringLiteral("导入配置")},
widget::pro::SizePolicy {QSizePolicy::Fixed, QSizePolicy::Expanding},
widget::pro::MinimumHeight {40},
widget::pro::MinimumWidth {320},
fbpro::Radius {12},
fbpro::Clickable {[]{
const QString file_name = QFileDialog::getOpenFileName(
nullptr,
QStringLiteral("导入配置"),
QString(),
QStringLiteral("配置文件 (*.conf);;所有文件 (*.*)"));
if (file_name.isEmpty()) {
return;
}
QFile file(file_name);
if (!file.open(QIODevice::ReadOnly)) {
QMessageBox::warning(nullptr, QStringLiteral("导入配置"),
QStringLiteral("无法打开配置文件。"));
return;
}
const QByteArray data = file.readAll();
file.close();
QJsonParseError parse_error {};
const QJsonDocument doc = QJsonDocument::fromJson(data, &parse_error);
if (doc.isNull() || parse_error.error != QJsonParseError::NoError || !doc.isArray()) {
QMessageBox::warning(nullptr, QStringLiteral("导入配置"),
QStringLiteral("配置文件格式不正确。"));
return;
}
const QJsonArray array = doc.array();
std::vector<ConfigProfile> imported_profiles;
imported_profiles.reserve(static_cast<std::size_t>(array.size()));
for (const auto& value : array) {
if (!value.isObject()) {
continue;
}
ConfigProfile profile {};
if (ConfigProfileFromJson(value.toObject(), profile)) {
imported_profiles.push_back(profile);
}
}
if (imported_profiles.empty()) {
QMessageBox::warning(nullptr, QStringLiteral("导入配置"),
QStringLiteral("配置文件中没有有效的配置。"));
return;
}
auto& helper = GlobalHelper::instance();
// 清空现有配置
const auto existing = helper.get_all_profile();
for (const auto& p : existing) {
helper.remove_profile(p.name);
}
// 写入新配置到 ini
for (const auto& p : imported_profiles) {
helper.add_new_profile(p);
}
helper.reload_profiles();
if (auto store = g_profiles_store.lock()) {
store->set(helper.get_all_profile());
}
RefreshProfilesForView();
if (g_profiles_refresh) {
g_profiles_refresh();
}
}},
};
}
static auto ExportProfileLongItem(creeper::ThemeManager& manager) {
return new FilledButton {
fbpro::ThemeManager {manager},
fbpro::Text {QStringLiteral("导出配置")},
widget::pro::SizePolicy {QSizePolicy::Fixed, QSizePolicy::Expanding},
widget::pro::MinimumHeight {40},
widget::pro::MinimumWidth {320},
fbpro::Radius {12},
fbpro::Clickable {[]{
QString file_name = QFileDialog::getSaveFileName(
nullptr,
QStringLiteral("导出配置"),
QString(),
QStringLiteral("配置文件 (*.conf);;所有文件 (*.*)"));
if (file_name.isEmpty()) {
return;
}
if (!file_name.endsWith(QStringLiteral(".conf"), Qt::CaseInsensitive)) {
file_name.append(QStringLiteral(".conf"));
}
auto& helper = GlobalHelper::instance();
helper.reload_profiles();
const auto& profiles = helper.get_all_profile();
QJsonArray array;
// array.reserve(static_cast<int>(profiles.size()));
for (const auto& p : profiles) {
array.append(ConfigProfileToJson(p));
}
const QJsonDocument doc(array);
QFile file(file_name);
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
QMessageBox::warning(nullptr, QStringLiteral("导出配置"),
QStringLiteral("无法写入配置文件。"));
return;
}
file.write(doc.toJson(QJsonDocument::Indented));
file.close();
QMessageBox::information(nullptr, QStringLiteral("导出配置"),
QStringLiteral("配置导出完成。"));
}},
};
}
static auto ProfileItemComponent(creeper::ThemeManager& manager, ConfigProfile& profile,
const std::shared_ptr<MutableValue<std::vector<ConfigProfile>>>& profiles_store) {
QString matrix_size = "规格:" + QString{ "%1 * %2" }.arg(profile.matrix_width).arg(profile.matrix_height);
@@ -421,9 +610,19 @@ auto SettingComponent(SettingComponentState& state) noexcept -> raw_pointer<QWid
lnpro::Alignment {Qt::AlignTop},
lnpro::Margin {10},
lnpro::Spacing {10},
lnpro::Item{
AddProfileLongItem(state.manager)
lnpro::Item<Row> {
lnpro::Item{
AddProfileLongItem(state.manager)
},
lnpro::Item {
ImportProfileLongItem(state.manager)
},
lnpro::Item {
ExportProfileLongItem(state.manager)
}
},
col::pro::Item<ScrollArea>{
scroll::pro::ThemeManager { state.manager },
scroll::pro::HorizontalScrollBarPolicy { Qt::ScrollBarAlwaysOff },

File diff suppressed because it is too large Load Diff

View File

@@ -146,6 +146,7 @@ namespace material {
constexpr auto kCancel = "cancel";
constexpr auto kOpenInNew = "open_in_new";
constexpr auto kLogout = "logout";
constexpr auto k123 = "123";
constexpr auto kRoutine = "routine";
constexpr auto kDarkMode = "dark_mode";
constexpr auto kFileExport = "file_export";

View File

@@ -1,4 +1,3 @@
/// TODO:
/// 显然的,原生 QComboBox 的下拉列表样式并不符合 Material Design
/// 规范,未来必须切换成自定义的组件,相关参考:
/// - https://m3.material.io/components/menus/guidelines

View File

@@ -96,7 +96,6 @@ public:
auto get_progress() const noexcept -> double;
/// @bug Signals can not be exported on Windows
/// TODO: Fix it
signals:
auto signal_value_change(double) -> void;
auto signal_value_change_finished(double) -> void;

10
deploy.sh Normal file
View File

@@ -0,0 +1,10 @@
mkdir -p deploy
ldd touchsensor.exe \
| awk '/=> \// {print $3}' \
| grep -vi 'windows' \
| sort -u \
| while read -r dll; do
echo "拷贝 $dll"
cp -u "$dll" deploy/
done

View File

@@ -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 = 200ms;
cfg.slave_request_interval = 10ms;
ffmsep::CPStreamCore core(cfg);
if (!core.open()) {

11
main.cc
View File

@@ -15,7 +15,7 @@
#include <creeper-qt/widget/main-window.hh>
#include <creeper-qt/layout/stacked.hh>
#include <qfontdatabase.h>
#include <iostream>
using namespace creeper;
namespace lnpro = linear::pro;
@@ -29,15 +29,17 @@ 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<MutableValue<int>>();
stack_index->set_silent(0);
auto manager = ThemeManager {kBlueMikuThemePack};
auto manager = ThemeManager {kBlueMikuThemePack, ColorMode::DARK};
creeper::material::FontLoader::load_font();
auto nav_component_state = NavComponentState {
.manager = manager,
.switch_callback = [&](int index, const auto& name) {
qDebug() << "switch_callback index: " << index;
},
.buttons_context = {
@@ -49,7 +51,8 @@ auto main(int argc, char *argv[]) -> int {
},
.stacked_callback = [&](int index) {
*stack_index = index;
}
},
.heatmap_show_numbers = HeatmapNumberVisibilityContext(),
};
auto setting_component_state = SettingComponentState {
.manager = manager,