Files
ts-qt/components/view.cc

1062 lines
40 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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 <QCoreApplication>
#include <QDateTime>
#include <QDir>
#include <QEvent>
#include <QFileDialog>
#include <QMessageBox>
#include <QMetaObject>
#include <QObject>
#include <QPair>
#include <QPointF>
#include <QStandardPaths>
#include <QString>
#include <QStringList>
#include <QtCore/Qt>
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <creeper-qt/layout/flow.hh>
#include <creeper-qt/layout/linear.hh>
#include <creeper-qt/utility/material-icon.hh>
#include <creeper-qt/utility/wrapper/mutable-value.hh>
#include <creeper-qt/widget/buttons/filled-button.hh>
#include <creeper-qt/widget/buttons/icon-button.hh>
#include <creeper-qt/widget/cards/filled-card.hh>
#include <creeper-qt/widget/cards/outlined-card.hh>
#include <creeper-qt/widget/dropdown-menu.hh>
#include <creeper-qt/widget/image.hh>
#include <creeper-qt/widget/shape/wave-circle.hh>
#include <creeper-qt/widget/sliders.hh>
#include <creeper-qt/widget/switch.hh>
#include <creeper-qt/widget/text-fields.hh>
#include <creeper-qt/widget/text.hh>
#include <cstdint>
#include <exception>
#include <future>
#include <iomanip>
#include <iostream>
#include <limits>
#include <memory>
#include <mutex>
#include <optional>
#include <qdebug.h>
#include <qsize.h>
#include <qsizepolicy.h>
#include <qstringliteral.h>
#include <sstream>
#include <string>
#include <thread>
#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<std::uint8_t, 14> 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<std::uint8_t>(reg ^ data[idx]);
for (int bit = 0; bit < 8; ++bit) {
if ((reg & 0x80U) != 0U) {
reg = static_cast<std::uint8_t>((reg << 1U) ^ kPolynomial);
}
else {
reg = static_cast<std::uint8_t>(reg << 1U);
}
}
}
return static_cast<std::uint8_t>(reg ^ kXorOut);
}
std::vector<std::uint8_t> make_slave_request_command(const QSize& matrix) {
auto command = kSlaveRequestCommandTemplate;
const int width = std::max(matrix.width(), 1);
const int height = std::max(matrix.height(), 1);
const std::uint32_t value_count =
static_cast<std::uint32_t>(width) * static_cast<std::uint32_t>(height);
const std::uint32_t byte_count = value_count * 2U; // 2 bytes per cell
const std::uint16_t payload_len =
static_cast<std::uint16_t>(std::min<std::uint32_t>(
byte_count, std::numeric_limits<std::uint16_t>::max()));
command[11] = static_cast<std::uint8_t>(payload_len & 0xFFU);
command[12] = static_cast<std::uint8_t>((payload_len >> 8U) & 0xFFU);
std::cout << "command11-12 0x" << std::hex << std::uppercase
<< std::setfill('0') << std::setw(2)
<< (int)(unsigned char)command[11] << ",0x" << std::setw(2)
<< (int)(unsigned char)command[12]
<< std::dec // 记得切回10进制避免影响后续输出
<< std::endl;
command.back() =
compute_slave_request_crc(command.data(), command.size() - 1U);
return std::vector<std::uint8_t>(command.begin(), command.end());
}
QVector<PointData> make_flat_points(const QSize& size, double value = 0.0) {
const int width = std::max(size.width(), 1);
const int height = std::max(size.height(), 1);
QVector<PointData> points;
points.reserve(static_cast<int>(width * height));
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
points.append(
PointData{ static_cast<double>(x), static_cast<double>(y), value });
}
}
return points;
}
std::once_flag& codec_registration_flag() {
static std::once_flag flag;
return flag;
}
class SensorStreamController: public QObject {
public:
SensorStreamController(
std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data,
std::shared_ptr<MutableValue<QSize>> matrix_context,
std::shared_ptr<MutableValue<QVector<QPointF>>> line_series,
std::shared_ptr<MutableValue<QVector<QPointF>>> line_series_max,
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_max_(std::move(line_series_max)),
line_series_capacity_(std::max(1, line_capacity)),
line_series_half_capacity_(std::max(1, line_capacity / 2)) {
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<QPointF>{});
}
if (line_series_max_) {
line_series_max_->set(QVector<QPointF>{});
}
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<std::uint8_t>& data) -> std::string {
if (data.empty()) {
return "[]";
}
std::ostringstream oss;
oss << '[' << std::uppercase << std::setfill('0');
for (std::size_t idx = 0; idx < data.size(); ++idx) {
if (idx != 0U) {
oss << ' ';
}
oss << std::setw(2) << std::hex << static_cast<unsigned int>(data[idx]);
}
oss << ']';
return oss.str();
};
if (type == Tactile_TYPE::PiezoresistiveB) {
cfg.codec_id = ffmsep::CPCodecID::PiezoresistiveB;
cfg.slave_request_command.clear();
cfg.slave_request_interval = 0ms;
ffmsep::tactile::set_tactile_expected_payload_bytes(
ffmsep::tactile::kPiezoresistiveBValueCount * 2U);
std::cout << "[Sensor] using PiezoresistiveB codec" << std::endl;
}
else {
cfg.codec_id = ffmsep::CPCodecID::Tactile;
const auto matrix = matrix_context_ ? matrix_context_->get() : QSize{ 3, 4 };
const auto request_command = make_slave_request_command(matrix);
const auto points = std::max(1, matrix.width()) * std::max(1, matrix.height());
ffmsep::tactile::set_tactile_expected_payload_bytes(
static_cast<std::size_t>(points) * 2U);
std::cout << "[Sensor] request command="
<< format_command(request_command) << std::endl;
cfg.slave_request_command = request_command;
cfg.slave_request_interval = 10ms;
}
reset_core();
core_ = std::make_unique<ffmsep::CPStreamCore>();
if (!core_->open(cfg)) {
last_error_ = QString::fromStdString(core_->last_error());
std::cerr << "SensorStreamController: open failed - "
<< core_->last_error() << "\n";
reset_core();
return false;
}
core_->set_frame_callback(
[this](std::shared_ptr<ffmsep::DecodedFrame> frame) {
handle_frame(frame);
});
if (!core_->start()) {
last_error_ = QString::fromStdString(core_->last_error());
std::cerr << "SensorStreamController: start failed - "
<< core_->last_error() << "\n";
reset_core();
return false;
}
active_port_ = QString::fromStdString(cfg.port);
last_error_.clear();
connected_ = true;
return true;
}
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<QPointF>{});
}
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<QPointF>{});
}
if (line_series_max_) {
line_series_max_->set(QVector<QPointF>{});
}
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<ffmsep::persist::WriteResult>
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<int>(m.long_edge), static_cast<int>(m.short_edge) };
}
void handle_frame(std::shared_ptr<ffmsep::DecodedFrame> frame) {
if (!frame) {
return;
}
auto format_raw = [](const std::vector<std::uint8_t>& data) -> std::string {
if (data.empty()) {
return "[]";
}
std::ostringstream oss;
oss << '[';
oss << std::uppercase << std::setfill('0');
for (std::size_t idx = 0; idx < data.size(); ++idx) {
if (idx != 0U) {
oss << ' ';
}
oss << std::setw(2) << std::hex
<< static_cast<unsigned int>(data[idx]);
}
oss << ']';
return oss.str();
};
auto frame_bytes = frame->frame.data;
std::vector<std::uint8_t> raw_payload;
if (frame->tactile) {
raw_payload = frame->tactile->payload;
}
else if (frame->id == ffmsep::CPCodecID::PiezoresistiveB) {
raw_payload =
ffmsep::tactile::extract_piezoresistive_b_payload(frame->frame);
}
std::cout << "[Sensor][raw] frame=" << format_raw(frame_bytes);
std::cout << " payload=" << format_raw(raw_payload) << std::endl;
if (frame->tactile_pressures.empty()) {
return;
}
auto pressures = frame->tactile_pressures;
QMetaObject::invokeMethod(
this,
[this, pressures = std::move(pressures)]() mutable {
auto matrix = matrix_context_->get();
const auto cells_exp = static_cast<std::size_t>(
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) {
pressures.resize(cells_exp, 0);
}
double total = 0.0;
double max_v = 0.0;
for (const auto value: pressures) {
const double v = static_cast<double>(value);
total += v;
if (v > max_v) {
max_v = v;
}
}
if (line_series_) {
auto series = line_series_->get();
series.append(QPointF(static_cast<double>(sample_counter_), total));
if (line_series_capacity_ > 0
&& series.size() > line_series_capacity_) {
const int start = series.size() - line_series_capacity_;
series = series.mid(start);
}
line_series_->set(std::move(series));
}
if (line_series_max_) {
auto series = line_series_max_->get();
series.append(QPointF(static_cast<double>(sample_counter_), max_v));
const int cap = std::max(1, line_series_half_capacity_);
if (series.size() > cap) {
const int start = series.size() - cap;
series = series.mid(start);
}
line_series_max_->set(std::move(series));
}
++sample_counter_;
QVector<PointData> points;
points.reserve(matrix.width() * matrix.height());
for (int y = 0; y < matrix.height(); ++y) {
for (int x = 0; x < matrix.width(); ++x) {
const int idx = y * matrix.width() + x;
if (idx >= static_cast<int>(pressures.size())) {
break;
}
const auto value =
static_cast<double>(pressures[static_cast<std::size_t>(idx)]);
points.append(PointData{ static_cast<double>(x),
static_cast<double>(y),
value });
}
}
matrix_context_->set(matrix);
heatmap_data_->set(std::move(points));
},
Qt::QueuedConnection);
}
[[nodiscard]] QSize normalize_matrix(QSize candidate,
std::size_t value_count) const {
if (value_count == 0U) {
return QSize{};
}
const auto adapt_from =
[value_count](const QSize& hint) -> std::optional<QSize> {
if (hint.width() <= 0 && hint.height() <= 0) {
return std::nullopt;
}
if (hint.width() > 0 && hint.height() > 0) {
const auto cells = static_cast<std::size_t>(hint.width()) * static_cast<std::size_t>(hint.height());
if (cells == value_count) {
return hint;
}
}
if (hint.width() > 0) {
const auto width = static_cast<std::size_t>(hint.width());
if (width != 0U && (value_count % width) == 0U) {
const auto height = static_cast<int>(value_count / width);
return QSize{ hint.width(), height };
}
}
if (hint.height() > 0) {
const auto height = static_cast<std::size_t>(hint.height());
if (height != 0U && (value_count % height) == 0U) {
const auto width = static_cast<int>(value_count / height);
return QSize{ width, hint.height() };
}
}
return std::nullopt;
};
if (auto adjusted = adapt_from(candidate)) {
return *adjusted;
}
if (auto adjusted = adapt_from(matrix_context_->get())) {
return *adjusted;
}
const auto root =
static_cast<int>(std::sqrt(static_cast<double>(value_count)));
for (int width = root; width >= 1; --width) {
const auto divisor = static_cast<std::size_t>(width);
if (divisor == 0U) {
continue;
}
if ((value_count % divisor) == 0U) {
const auto height = static_cast<int>(value_count / divisor);
return QSize{ width, height };
}
}
return QSize{ static_cast<int>(value_count), 1 };
}
std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data_;
std::shared_ptr<MutableValue<QSize>> matrix_context_;
std::shared_ptr<MutableValue<QVector<QPointF>>> line_series_;
std::shared_ptr<MutableValue<QVector<QPointF>>> line_series_max_;
std::unique_ptr<ffmsep::CPStreamCore> core_;
QString active_port_;
QString last_error_;
std::uint64_t sample_counter_ = 0;
int line_series_capacity_ = 240;
int line_series_half_capacity_ = 120;
bool connected_ = false;
static std::future<ffmsep::persist::WriteResult>
make_failed_future(const QString& path, std::string message) {
std::promise<ffmsep::persist::WriteResult> 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<MutableValue<QString>> link_icon =
std::make_shared<MutableValue<QString>>(
QString::fromLatin1(material::icon::kAddLink));
std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data =
std::make_shared<MutableValue<QVector<PointData>>>();
std::shared_ptr<MutableValue<QSize>> heatmap_matrix =
std::make_shared<MutableValue<QSize>>();
std::shared_ptr<MutableValue<QPair<int, int>>> heatmap_range =
std::make_shared<MutableValue<QPair<int, int>>>(QPair<int, int>{ 0, 300 });
std::shared_ptr<MutableValue<bool>> heatmap_show_numbers =
std::make_shared<MutableValue<bool>>(false);
std::shared_ptr<MutableValue<QVector<QPointF>>> line_series =
std::make_shared<MutableValue<QVector<QPointF>>>();
std::shared_ptr<MutableValue<QVector<QPointF>>> line_series_max =
std::make_shared<MutableValue<QVector<QPointF>>>();
int line_series_capacity = 240;
std::shared_ptr<MutableValue<QStringList>> port_items =
std::make_shared<MutableValue<QStringList>>();
std::shared_ptr<MutableValue<QStringList>> profile_items =
std::make_shared<MutableValue<QStringList>>();
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<SensorStreamController> controller;
SensorUiState() {
const QSize size{ 3, 4 };
heatmap_matrix->set_silent(size);
heatmap_data->set_silent(make_flat_points(size));
// 初始化串口列表
QStringList ports_list;
const auto ports = ffmsep::CPStreamCore::list_available_ports();
ports_list.reserve(static_cast<qsizetype>(ports.size()));
for (const auto& info: ports) {
ports_list.emplace_back(QString::fromStdString(info.port));
}
port_items->set_silent(ports_list);
if (selected_port.isEmpty() && !ports_list.isEmpty()) {
selected_port = ports_list.front();
}
GlobalHelper::instance().reload_profiles();
QStringList profile_list;
const auto& profiles = GlobalHelper::instance().get_all_profile();
profile_list.reserve(static_cast<qsizetype>(profiles.size()));
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),
std::max(1, profiles.front().matrix_height) };
selected_type = profiles.front().type;
selected_baud =
profiles.front().baud_rate == 0 ? 115200U : static_cast<std::uint32_t>(profiles.front().baud_rate);
heatmap_matrix->set_silent(size);
heatmap_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<int, int>{ range_min, range_max });
}
}
controller =
std::make_unique<SensorStreamController>(
heatmap_data, heatmap_matrix, line_series, line_series_max, line_series_capacity);
}
};
SensorUiState& sensor_state() {
static SensorUiState state;
return state;
}
static void RefreshPortsForView(SensorUiState& sensor) {
QStringList ports_list;
const auto ports = ffmsep::CPStreamCore::list_available_ports();
ports_list.reserve(static_cast<qsizetype>(ports.size()));
for (const auto& info: ports) {
ports_list.emplace_back(QString::fromStdString(info.port));
}
if (sensor.selected_port.isEmpty()) {
if (!ports_list.isEmpty()) {
sensor.selected_port = ports_list.front();
}
sensor.port_items->set(std::move(ports_list));
}
}
class PortHoverRefreshFilter final: public QObject {
public:
explicit PortHoverRefreshFilter(SensorUiState& sensor,
QObject* parent = nullptr): QObject(parent), sensor_(sensor) {}
protected:
bool eventFilter(QObject* watched, QEvent* event) override {
if (event && event->type() == QEvent::Enter) {
RefreshPortsForView(sensor_);
}
return QObject::eventFilter(watched, event);
}
private:
SensorUiState& sensor_;
};
} // namespace
std::shared_ptr<MutableValue<bool>> HeatmapNumberVisibilityContext() {
return sensor_state().heatmap_show_numbers;
}
void RefreshProfilesForView() {
auto& sensor = sensor_state();
GlobalHelper::instance().reload_profiles();
QStringList profile_list;
const auto& profiles = GlobalHelper::instance().get_all_profile();
profile_list.reserve(static_cast<qsizetype>(profiles.size()));
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();
}
}
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<std::uint32_t>(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<int, int>{ 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_baud == 0U) {
sensor.selected_baud = 115200U;
}
const auto row = new Row{
// lnpro::Item<FilledTextField> {
// text_field::pro::ThemeManager {manager},
// text_field::pro::LeadingIcon {material::icon::kSearch,
// material::regular::font},
// MutableForward {
// text_field::pro::LabelText {},
// slogen_context,
// },
// },
lnpro::Item<FilledDropdownMenu>{
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()) {
sensor_ptr->selected_port = text;
}
} },
dmpro::LabelText{ "COM" },
MutableForward{
dmpro::Items{},
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.setProperty("portHoverRefreshAttached", true);
}
} },
},
lnpro::Item<FilledDropdownMenu>{
dmpro::ThemeManager{ manager },
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; });
if (it != profiles.end()) {
const auto baud =
it->baud_rate == 0 ? 115200U : static_cast<std::uint32_t>(it->baud_rate);
sensor_ptr->selected_baud = baud;
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<int, int>{ range_min, range_max });
}
}
} },
dmpro::LabelText{ "Profile" },
MutableForward{
dmpro::Items{},
sensor.profile_items,
},
dmpro::Apply{ [&sensor](dropdown_menu::internal::DropdownMenu& self) {
sensor.profile_dropdown = &self;
} },
},
lnpro::SpacingItem{ 20 },
lnpro::Item<IconButton>{
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));
}
else {
const auto port = sensor.selected_port;
const auto baud =
sensor.selected_baud == 0U ? 115200U : sensor.selected_baud;
if (sensor.controller->start(port, baud, sensor.selected_type)) {
sensor.selected_port = sensor.controller->active_port();
link_icon_context->set(
QString::fromLatin1(material::icon::kLinkOff));
}
else {
std::cerr << "Failed to start sensor stream: "
<< sensor.controller->last_error().toStdString()
<< "\n";
}
}
} } },
lnpro::Item<IconButton>{ ibpro::ThemeManager{ manager }, ibpro::FixedSize{ 40, 40 }, ibpro::Color{ IconButton::Color::TONAL }, ibpro::Font{ material::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<FilledButton>{ 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.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<HeatMapPlot>{
widget::pro::FixedSize {600, 600},
// plot_widget::pro::SizePolicy{
// // QSizePolicy::Fixed,
// },
plot_widget::pro::ThemeManager{ manager },
MutableForward{
plot_widget::pro::PlotData{},
sensor.heatmap_data,
},
pwpro::MatrixSize{ sensor.heatmap_matrix->get() },
MutableTransform{ [](auto& widget, const QSize& size) {
pwpro::MatrixSize{ size }.apply(widget);
},
sensor.heatmap_matrix },
MutableTransform{ [](auto& widget, const QPair<int, int>& range) {
const double min =
static_cast<double>(range.first);
const double max =
static_cast<double>(range.second);
widget.set_color_gradient_range(min, max);
},
sensor.heatmap_range },
MutableTransform{ [](auto& widget, bool show_numbers) {
widget.set_labels_visible(show_numbers);
},
sensor.heatmap_show_numbers },
},
};
return new Widget{
widget::pro::Layout{ row },
};
}
static auto DisplayVectorComponent(ThemeManager& manager) noexcept {
auto& sensor = sensor_state();
const auto row = new Row{
lnpro::Item<VectorFieldPlot>{
vfpro::SizePolicy{
QSizePolicy::Expanding,
},
vfpro::ThemeManager{ manager },
MutableForward{
vfpro::PlotData{},
sensor.heatmap_data,
},
vfpro::MatrixSize{ sensor.heatmap_matrix->get() },
MutableTransform{ [](auto& widget, const QSize& size) {
vfpro::MatrixSize{ size }.apply(widget);
},
sensor.heatmap_matrix },
},
};
return new Widget{
widget::pro::Layout{ row },
};
}
static auto DisplayLineComponent(ThemeManager& manager) noexcept {
auto& sensor = sensor_state();
const int half_cap = std::max(1, sensor.line_series_capacity / 2);
const auto col = new Col{
lnpro::Item<SumLinePlot>{
lcpro::SizePolicy{ QSizePolicy::Expanding },
lcpro::ThemeManager{ manager },
lcpro::MaxPoints{ half_cap },
MutableForward{
lcpro::PlotData{},
sensor.line_series,
},
},
lnpro::Item<SumLinePlot>{
lcpro::SizePolicy{ QSizePolicy::Expanding },
lcpro::ThemeManager{ manager },
lcpro::MaxPoints{ half_cap },
MutableForward{
lcpro::PlotData{},
sensor.line_series_max,
},
},
};
return new Widget{
widget::pro::Layout{ col },
};
}
auto ViewComponent(ViewComponentState& state) noexcept -> raw_pointer<QWidget> {
return new FilledCard{
capro::ThemeManager{ state.manager },
capro::SizePolicy{ QSizePolicy::Expanding },
capro::Layout<Col>{
lnpro::Alignment{ Qt::AlignTop },
// lnpro::Margin {10},
// lnpro::Spacing {10},
lnpro::Item{
ComConfigComponent(state.manager),
},
lnpro::Item<Row>{
lnpro::Item{
DisplayComponent(state.manager),
},
lnpro::Item<Col>{
lnpro::Item{
DisplayVectorComponent(state.manager),
},
lnpro::Item{
DisplayLineComponent(state.manager),
},
},
},
},
};
}