1015 lines
38 KiB
C++
1015 lines
38 KiB
C++
//
|
||
// 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,
|
||
int line_capacity = 240,
|
||
QObject* parent = nullptr): QObject(parent), heatmap_data_(std::move(heatmap_data)),
|
||
matrix_context_(std::move(matrix_context)),
|
||
line_series_(std::move(line_series)),
|
||
line_series_capacity_(std::max(1, line_capacity)) {
|
||
std::call_once(codec_registration_flag(),
|
||
[] {
|
||
ffmsep::tactile::register_tactile_codec();
|
||
ffmsep::tactile::register_tactile_b_codec();
|
||
});
|
||
}
|
||
|
||
~SensorStreamController() override { reset_core(); }
|
||
|
||
bool start(const QString& requested_port,
|
||
std::uint32_t baudrate,
|
||
Tactile_TYPE type) {
|
||
std::cout << "start" << std::endl;
|
||
if (is_connected()) {
|
||
return true;
|
||
}
|
||
|
||
sample_counter_ = 0;
|
||
if (line_series_) {
|
||
line_series_->set(QVector<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>{});
|
||
}
|
||
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;
|
||
for (const auto value: pressures) {
|
||
total += static_cast<double>(value);
|
||
}
|
||
|
||
if (line_series_) {
|
||
auto series = line_series_->get();
|
||
series.append(QPointF(static_cast<double>(sample_counter_), total));
|
||
if (line_series_capacity_ > 0
|
||
&& series.size() > line_series_capacity_) {
|
||
const int start = series.size() - line_series_capacity_;
|
||
series = series.mid(start);
|
||
}
|
||
++sample_counter_;
|
||
line_series_->set(std::move(series));
|
||
}
|
||
|
||
QVector<PointData> points;
|
||
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::unique_ptr<ffmsep::CPStreamCore> core_;
|
||
QString active_port_;
|
||
QString last_error_;
|
||
std::uint64_t sample_counter_ = 0;
|
||
int line_series_capacity_ = 240;
|
||
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<QVector<QPointF>>> line_series =
|
||
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_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
|
||
|
||
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>{
|
||
plot_widget::pro::SizePolicy{
|
||
QSizePolicy::Expanding,
|
||
},
|
||
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 },
|
||
},
|
||
};
|
||
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 auto row = new Row{
|
||
lnpro::Item<SumLinePlot>{
|
||
lcpro::SizePolicy{
|
||
QSizePolicy::Expanding,
|
||
},
|
||
lcpro::ThemeManager{ manager },
|
||
lcpro::MaxPoints{ sensor.line_series_capacity },
|
||
MutableForward{
|
||
lcpro::PlotData{},
|
||
sensor.line_series,
|
||
},
|
||
},
|
||
};
|
||
return new Widget{
|
||
widget::pro::Layout{ row },
|
||
};
|
||
}
|
||
|
||
auto ViewComponent(ViewComponentState& state) noexcept -> raw_pointer<QWidget> {
|
||
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),
|
||
},
|
||
},
|
||
},
|
||
},
|
||
};
|
||
}
|