feat:data slove and update heatmap
This commit is contained in:
@@ -1,213 +1,589 @@
|
||||
//
|
||||
// Created by Lenn on 2025/10/14.
|
||||
//
|
||||
|
||||
//
|
||||
// Created by Lenn on 2025/10/14.
|
||||
//
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <QString>
|
||||
#include <QObject>
|
||||
#include <QMetaObject>
|
||||
#include <QStringList>
|
||||
#include <QtCore/Qt>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <qsize.h>
|
||||
#include <qsizepolicy.h>
|
||||
#include <random>
|
||||
#include <chrono>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include "component.hh"
|
||||
#include "cpstream_core.hh"
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/utility/wrapper/layout.hh"
|
||||
#include "modern-qt/utility/wrapper/widget.hh"
|
||||
#include "components/charts/heatmap.hh"
|
||||
#include <modern-qt/layout/flow.hh>
|
||||
#include <modern-qt/layout/linear.hh>
|
||||
#include <modern-qt/utility/material-icon.hh>
|
||||
#include <modern-qt/utility/wrapper/mutable-value.hh>
|
||||
#include <modern-qt/widget/buttons/icon-button.hh>
|
||||
#include <modern-qt/widget/cards/filled-card.hh>
|
||||
#include <modern-qt/widget/cards/outlined-card.hh>
|
||||
#include <modern-qt/widget/image.hh>
|
||||
#include <modern-qt/widget/shape/wave-circle.hh>
|
||||
#include <modern-qt/widget/sliders.hh>
|
||||
#include <modern-qt/widget/switch.hh>
|
||||
#include <modern-qt/widget/text-fields.hh>
|
||||
#include <modern-qt/widget/text.hh>
|
||||
#include <modern-qt/widget/select.hh>
|
||||
#include "components/ffmsep/tactile/tacdec.hh"
|
||||
|
||||
|
||||
using namespace creeper;
|
||||
namespace capro = card::pro;
|
||||
namespace lnpro = linear::pro;
|
||||
namespace impro = image::pro;
|
||||
namespace ibpro = icon_button::pro;
|
||||
namespace slpro = select_widget::pro;
|
||||
namespace pwpro = plot_widget::pro;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::array<std::uint8_t, 14> kSlaveRequestCommand{
|
||||
0x55, 0xAA, 0x09, 0x00, 0x34, 0x00, 0xFB,
|
||||
0x00, 0x1C, 0x00, 0x00, 0x18, 0x00, 0x7A
|
||||
};
|
||||
|
||||
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,
|
||||
QObject* parent = nullptr)
|
||||
: QObject(parent)
|
||||
, heatmap_data_(std::move(heatmap_data))
|
||||
, matrix_context_(std::move(matrix_context)) {
|
||||
std::call_once(codec_registration_flag(), [] {
|
||||
ffmsep::tactile::register_tactile_codec();
|
||||
});
|
||||
}
|
||||
|
||||
~SensorStreamController() override {
|
||||
reset_core();
|
||||
}
|
||||
|
||||
bool start(const QString& requested_port, std::uint32_t baudrate) {
|
||||
if (is_connected()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto ports = ffmsep::CPStreamCore::list_available_ports();
|
||||
std::string port_utf8;
|
||||
if (!requested_port.isEmpty()) {
|
||||
port_utf8 = requested_port.toStdString();
|
||||
const auto it = std::find_if(
|
||||
ports.begin(), ports.end(),
|
||||
[&](const serial::PortInfo& info) { return info.port == port_utf8; });
|
||||
if (it == ports.end()) {
|
||||
if (ports.empty()) {
|
||||
std::cerr << "SensorStreamController: requested port '" << port_utf8 << "' not available and no other ports detected.\n";
|
||||
last_error_ = QString::fromUtf8("未检测到串口");
|
||||
return false;
|
||||
}
|
||||
std::cerr << "SensorStreamController: requested port '" << port_utf8 << "' not available, falling back to first detected port.\n";
|
||||
port_utf8 = ports.front().port;
|
||||
}
|
||||
} else if (!ports.empty()) {
|
||||
port_utf8 = ports.front().port;
|
||||
} else {
|
||||
std::cerr << "SensorStreamController: no serial ports available\n";
|
||||
last_error_ = QString::fromUtf8("未检测到串口");
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::uint32_t baud = baudrate == 0U ? 115200U : baudrate;
|
||||
|
||||
ffmsep::CPStreamConfig cfg;
|
||||
cfg.port = port_utf8;
|
||||
cfg.baudrate = baud;
|
||||
cfg.codec_id = ffmsep::CPCodecID::Tactile;
|
||||
cfg.read_chunk_size = 256;
|
||||
cfg.packet_queue_capacity = 128;
|
||||
cfg.frame_queue_capacity = 32;
|
||||
cfg.slave_request_command.assign(kSlaveRequestCommand.begin(), kSlaveRequestCommand.end());
|
||||
cfg.slave_request_interval = std::chrono::milliseconds{200};
|
||||
|
||||
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](const 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;
|
||||
}
|
||||
|
||||
#include "component.hh"
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/utility/wrapper/layout.hh"
|
||||
#include "modern-qt/utility/wrapper/widget.hh"
|
||||
#include "components/charts/heatmap.hh"
|
||||
#include <modern-qt/layout/flow.hh>
|
||||
#include <modern-qt/layout/linear.hh>
|
||||
#include <modern-qt/utility/material-icon.hh>
|
||||
#include <modern-qt/utility/wrapper/mutable-value.hh>
|
||||
#include <modern-qt/widget/buttons/icon-button.hh>
|
||||
#include <modern-qt/widget/cards/filled-card.hh>
|
||||
#include <modern-qt/widget/cards/outlined-card.hh>
|
||||
#include <modern-qt/widget/image.hh>
|
||||
#include <modern-qt/widget/shape/wave-circle.hh>
|
||||
#include <modern-qt/widget/sliders.hh>
|
||||
#include <modern-qt/widget/switch.hh>
|
||||
#include <modern-qt/widget/text-fields.hh>
|
||||
#include <modern-qt/widget/text.hh>
|
||||
#include <modern-qt/widget/select.hh>
|
||||
void stop() {
|
||||
reset_core();
|
||||
active_port_.clear();
|
||||
if (heatmap_data_ && matrix_context_) {
|
||||
heatmap_data_->set(make_flat_points(matrix_context_->get()));
|
||||
}
|
||||
connected_ = false;
|
||||
}
|
||||
|
||||
using namespace creeper;
|
||||
namespace capro = card::pro;
|
||||
namespace lnpro = linear::pro;
|
||||
namespace impro = image::pro;
|
||||
namespace ibpro = icon_button::pro;
|
||||
namespace slpro = select_widget::pro;
|
||||
namespace pwpro = plot_widget::pro;
|
||||
[[nodiscard]] bool is_running() const noexcept {
|
||||
return core_ && core_->is_running();
|
||||
}
|
||||
|
||||
static auto ComConfigComponent(ThemeManager& manager, auto&& callback) {
|
||||
auto slogen_context = std::make_shared<MutableValue<QString>>();
|
||||
slogen_context->set_silent("BanG Bream! It's MyGo!!!");
|
||||
|
||||
auto select_com_context = std::make_shared<MutableValue<QStringList>>();
|
||||
select_com_context->set_silent(QStringList {"COM1", "COM2", "COM3", "COM4", "COM5"});
|
||||
[[nodiscard]] bool is_connected() const noexcept {
|
||||
return connected_;
|
||||
}
|
||||
|
||||
auto select_baud_context = std::make_shared<MutableValue<QStringList>>();
|
||||
select_baud_context->set_silent(QStringList {"9600", "115200"});
|
||||
[[nodiscard]] QString active_port() const {
|
||||
return active_port_;
|
||||
}
|
||||
|
||||
[[nodiscard]] QString last_error() const {
|
||||
return last_error_;
|
||||
}
|
||||
|
||||
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(const ffmsep::DecodedFrame& frame) {
|
||||
if (!frame.tactile || frame.tactile_pressures.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto pressures = frame.tactile_pressures;
|
||||
auto size_hint = frame.tactile_matrix_size;
|
||||
auto frame_bytes = frame.frame.data;
|
||||
std::vector<std::uint8_t> raw_payload;
|
||||
if (frame.tactile) {
|
||||
raw_payload = frame.tactile->payload;
|
||||
}
|
||||
|
||||
QMetaObject::invokeMethod(
|
||||
this,
|
||||
[this,
|
||||
pressures = std::move(pressures),
|
||||
size_hint,
|
||||
frame_bytes = std::move(frame_bytes),
|
||||
raw_payload = std::move(raw_payload)]() {
|
||||
const 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();
|
||||
};
|
||||
|
||||
std::cout << "[Sensor] frame=" << format_raw(frame_bytes);
|
||||
std::cout << " payload=" << format_raw(raw_payload);
|
||||
std::cout << " received " << pressures.size() << " pressure values";
|
||||
if (size_hint) {
|
||||
std::cout << " matrix=" << int(size_hint->long_edge)
|
||||
<< "x" << int(size_hint->short_edge);
|
||||
}
|
||||
const std::size_t preview = std::min<std::size_t>(pressures.size(), 12);
|
||||
if (preview > 0) {
|
||||
std::cout << " values=[";
|
||||
for (std::size_t idx = 0; idx < preview; ++idx) {
|
||||
if (idx != 0U) {
|
||||
std::cout << ", ";
|
||||
}
|
||||
std::cout << pressures[idx];
|
||||
}
|
||||
if (preview < pressures.size()) {
|
||||
std::cout << ", ...";
|
||||
}
|
||||
std::cout << "]";
|
||||
}
|
||||
std::cout << std::endl;
|
||||
|
||||
auto matrix = matrix_context_->get();
|
||||
if (size_hint) {
|
||||
matrix = to_qsize(*size_hint);
|
||||
}
|
||||
matrix = normalize_matrix(matrix, pressures.size());
|
||||
if (matrix.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 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,
|
||||
// },
|
||||
// },
|
||||
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::unique_ptr<ffmsep::CPStreamCore> core_;
|
||||
QString active_port_;
|
||||
QString last_error_;
|
||||
bool connected_ = false;
|
||||
};
|
||||
|
||||
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<QStringList>> port_items =
|
||||
std::make_shared<MutableValue<QStringList>>();
|
||||
QString selected_port;
|
||||
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();
|
||||
}
|
||||
|
||||
controller = std::make_unique<SensorStreamController>(heatmap_data, heatmap_matrix);
|
||||
}
|
||||
};
|
||||
|
||||
SensorUiState& sensor_state() {
|
||||
static SensorUiState state;
|
||||
return state;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static auto ComConfigComponent(ThemeManager& manager) {
|
||||
auto& sensor = sensor_state();
|
||||
auto link_icon_context = sensor.link_icon;
|
||||
|
||||
// 串口下拉:改为绑定可变数据源,初始值由 SensorUiState 构造时填充
|
||||
if (sensor.selected_port.isEmpty() && !sensor.port_items->get().isEmpty()) {
|
||||
sensor.selected_port = sensor.port_items->get().front();
|
||||
}
|
||||
|
||||
const QStringList baud_items{
|
||||
QString::fromLatin1("9600"),
|
||||
QString::fromLatin1("115200")
|
||||
};
|
||||
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<MatSelect> {
|
||||
slpro::ThemeManager {manager},
|
||||
slpro::LeadingIcon {material::icon::kArrowDropDown, material::regular::font},
|
||||
slpro::IndexChanged {[&](auto& self){ qDebug() << self.currentIndex();}},
|
||||
slpro::IndexChanged {[sensor_ptr = &sensor](auto& self){
|
||||
const auto text = self.currentText();
|
||||
if (!text.isEmpty()) {
|
||||
sensor_ptr->selected_port = text;
|
||||
}
|
||||
}},
|
||||
slpro::LeadingText {"COM"},
|
||||
MutableForward {
|
||||
slpro::SelectItems {},
|
||||
select_com_context,
|
||||
}
|
||||
},
|
||||
lnpro::Item<MatSelect> {
|
||||
slpro::ThemeManager {manager },
|
||||
slpro::LeadingIcon { material::icon::kArrowDropDown, material::regular::font},
|
||||
slpro::IndexChanged {[&](auto& self){ qDebug() << self.currentIndex();}},
|
||||
slpro::LeadingText {"Baud"},
|
||||
MutableForward {
|
||||
slpro::SelectItems {},
|
||||
select_baud_context,
|
||||
}
|
||||
},
|
||||
lnpro::SpacingItem {20},
|
||||
lnpro::Item<IconButton> {
|
||||
ibpro::ThemeManager {manager},
|
||||
ibpro::FixedSize {40, 40},
|
||||
ibpro::Color { IconButton::Color::TONAL },
|
||||
ibpro::Font { material::kRoundSmallFont },
|
||||
ibpro::FontIcon { material::icon::kAddLink },
|
||||
ibpro::Clickable {[slogen_context] {
|
||||
constexpr auto random_slogen = [] {
|
||||
constexpr auto slogens = std::array {
|
||||
"为什么要演奏《春日影》!",
|
||||
"我从来不觉得玩乐队开心过。",
|
||||
"我好想…成为人啊!",
|
||||
"那你愿意……跟我组一辈子的乐队吗?",
|
||||
"过去软弱的我…已经死了。",
|
||||
};
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution<> dist(0, slogens.size() - 1);
|
||||
return QString::fromUtf8(slogens[dist(gen)]);
|
||||
};
|
||||
*slogen_context = random_slogen();
|
||||
}},
|
||||
sensor.port_items,
|
||||
},
|
||||
},
|
||||
lnpro::Item<MatSelect> {
|
||||
slpro::ThemeManager {manager },
|
||||
slpro::LeadingIcon { material::icon::kArrowDropDown, material::regular::font},
|
||||
slpro::IndexChanged {[sensor_ptr = &sensor](auto& self){
|
||||
bool ok = false;
|
||||
const auto text = self.currentText();
|
||||
const auto value = text.toUInt(&ok);
|
||||
if (ok && value > 0U) {
|
||||
sensor_ptr->selected_baud = static_cast<std::uint32_t>(value);
|
||||
}
|
||||
}},
|
||||
slpro::LeadingText {"Baud"},
|
||||
slpro::SelectItems {baud_items},
|
||||
},
|
||||
lnpro::SpacingItem {20},
|
||||
lnpro::Item<IconButton> {
|
||||
ibpro::ThemeManager {manager},
|
||||
ibpro::FixedSize {40, 40},
|
||||
ibpro::Color { IconButton::Color::TONAL },
|
||||
ibpro::Font { material::kRoundSmallFont },
|
||||
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_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::kRoundSmallFont },
|
||||
ibpro::FontIcon { material::icon::kRefresh },
|
||||
ibpro::Clickable {[select_baud_context] {
|
||||
|
||||
static constexpr auto options_group1 = std::array {
|
||||
"第一组选项1", "第一组选项2", "第一组选项3"
|
||||
};
|
||||
static constexpr auto options_group2 = std::array {
|
||||
"第二组选项A", "第二组选项B", "第二组选项C", "第二组选项D"
|
||||
};
|
||||
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution<> dist(0, 1);
|
||||
|
||||
QStringList new_options;
|
||||
if (dist(gen) == 0) {
|
||||
for (const auto& option : options_group1) {
|
||||
new_options << QString::fromUtf8(option);
|
||||
}
|
||||
} else {
|
||||
for (const auto& option : options_group2) {
|
||||
new_options << QString::fromUtf8(option);
|
||||
}
|
||||
ibpro::Clickable {[&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));
|
||||
}
|
||||
*select_baud_context = new_options;
|
||||
|
||||
// 保持原选择(若仍然存在)
|
||||
if (!sensor.selected_port.isEmpty()) {
|
||||
const bool exists = ports_list.contains(sensor.selected_port);
|
||||
if (!exists) {
|
||||
sensor.selected_port = ports_list.isEmpty() ? QString{} : ports_list.front();
|
||||
}
|
||||
} else if (!ports_list.isEmpty()) {
|
||||
sensor.selected_port = ports_list.front();
|
||||
}
|
||||
|
||||
sensor.port_items->set(std::move(ports_list));
|
||||
}},
|
||||
},
|
||||
|
||||
};
|
||||
return new Widget {
|
||||
widget::pro::Layout {row},
|
||||
};
|
||||
}
|
||||
|
||||
static auto DisplayComponent(ThemeManager& manager, int index = 0) noexcept {
|
||||
auto heatmap_context = std::make_shared<MutableValue<QVector<PointData>>>();
|
||||
heatmap_context->set_silent(QVector<PointData>{
|
||||
PointData{0, 0, 1}, PointData{1, 0, 2}, PointData{2, 0, 3},
|
||||
PointData{0, 1, 3}, PointData{1, 1, 4}, PointData{2, 1, 5},
|
||||
PointData{0, 2, 6}, PointData{1, 2, 7}, PointData{2, 2, 8},
|
||||
PointData{0, 3, 9}, PointData{1, 3, 10}, PointData{2, 3, 11},
|
||||
});
|
||||
const auto row = new Row{
|
||||
lnpro::Item<HeatMapPlot> {
|
||||
plot_widget::pro::SizePolicy {
|
||||
QSizePolicy::Expanding,
|
||||
},
|
||||
MutableForward {
|
||||
plot_widget::pro::PlotData {},
|
||||
heatmap_context,
|
||||
},
|
||||
pwpro::MatrixSize {
|
||||
QSize{3, 4}
|
||||
},
|
||||
},
|
||||
};
|
||||
return new Widget {
|
||||
widget::pro::Layout{row},
|
||||
};
|
||||
}
|
||||
auto ViewComponent(ViewComponentState& state) noexcept -> raw_pointer<QWidget> {
|
||||
const auto texts = std::array {
|
||||
std::make_shared<MutableValue<QString>>("0.500"),
|
||||
std::make_shared<MutableValue<QString>>("0.500"),
|
||||
std::make_shared<MutableValue<QString>>("0.500"),
|
||||
};
|
||||
const auto progresses = std::array {
|
||||
std::make_shared<MutableValue<double>>(0.5),
|
||||
std::make_shared<MutableValue<double>>(0.5),
|
||||
std::make_shared<MutableValue<double>>(0.5),
|
||||
};
|
||||
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,
|
||||
[texts, progresses] {
|
||||
constexpr auto random_unit = []() {
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
static std::uniform_real_distribution<double> dist(0.0, 1.0);
|
||||
return dist(gen);
|
||||
};
|
||||
for (auto&& [string, number] : std::views::zip(texts, progresses)) {
|
||||
auto v = random_unit();
|
||||
*number = v;
|
||||
*string = QString::number(v, 'f', 3);
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
lnpro::Item<Row> {
|
||||
lnpro::Item {
|
||||
DisplayComponent(state.manager),
|
||||
},
|
||||
lnpro::Item {
|
||||
DisplayComponent(state.manager),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
};
|
||||
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,
|
||||
},
|
||||
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
|
||||
},
|
||||
},
|
||||
};
|
||||
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 {
|
||||
DisplayComponent(state.manager),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user