// // Created by Lenn on 2025/10/14. // #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "component.hh" #include "cpstream_core.hh" #include "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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include "components/ffmsep/tactile/tacdec.hh" using namespace std::chrono_literals; 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 kSlaveRequestCommand{ 0x55, 0xAA, 0x09, 0x00, 0x34, 0x00, 0xFB, 0x00, 0x1C, 0x00, 0x00, 0x18, 0x00, 0x7A }; QVector make_flat_points(const QSize& size, double value = 0.0) { const int width = std::max(size.width(), 1); const int height = std::max(size.height(), 1); QVector points; points.reserve(static_cast(width * height)); for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { points.append(PointData{ static_cast(x), static_cast(y), value }); } } return points; } std::once_flag& codec_registration_flag() { static std::once_flag flag; return flag; } class SensorStreamController : public QObject { public: SensorStreamController(std::shared_ptr>> heatmap_data, std::shared_ptr> matrix_context, QObject* parent = nullptr) : QObject(parent) , heatmap_data_(std::move(heatmap_data)) , matrix_context_(std::move(matrix_context)) { std::call_once(codec_registration_flag(), [] { ffmsep::tactile::register_tactile_codec(); }); } ~SensorStreamController() override { reset_core(); } bool start(const QString& requested_port, std::uint32_t baudrate) { if (is_connected()) { return true; } const auto ports = ffmsep::CPStreamCore::list_available_ports(); std::string port_utf8; if (!requested_port.isEmpty()) { port_utf8 = requested_port.toStdString(); const auto it = std::find_if( ports.begin(), ports.end(), [&](const serial::PortInfo& info) { return info.port == port_utf8; }); if (it == ports.end()) { if (ports.empty()) { 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 = 3ms; reset_core(); core_ = std::make_unique(); if (!core_->open(cfg)) { last_error_ = QString::fromStdString(core_->last_error()); std::cerr << "SensorStreamController: open failed - " << core_->last_error() << "\n"; reset_core(); return false; } core_->set_frame_callback([this](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; } void stop() { reset_core(); active_port_.clear(); if (heatmap_data_ && matrix_context_) { heatmap_data_->set(make_flat_points(matrix_context_->get())); } connected_ = false; } [[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_; } private: void reset_core() { connected_ = false; if (!core_) { return; } core_->set_frame_callback({}); if (core_->is_running()) { core_->stop(); } if (core_->is_open()) { core_->close(); } core_.reset(); } static QSize to_qsize(const ffmsep::tactile::MatrixSize& m) { return QSize{ static_cast(m.long_edge), static_cast(m.short_edge) }; } void handle_frame(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 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& data) -> std::string { if (data.empty()) { return "[]"; } std::ostringstream oss; oss << '['; oss << std::uppercase << std::setfill('0'); for (std::size_t idx = 0; idx < data.size(); ++idx) { if (idx != 0U) { oss << ' '; } oss << std::setw(2) << std::hex << static_cast(data[idx]); } oss << ']'; return oss.str(); }; std::cout << "[Sensor] frame=" << format_raw(frame_bytes); std::cout << " payload=" << format_raw(raw_payload); std::cout << " received " << pressures.size() << " pressure values"; if (size_hint) { std::cout << " matrix=" << int(size_hint->long_edge) << "x" << int(size_hint->short_edge); } const std::size_t preview = std::min(pressures.size(), 12); if (preview > 0) { std::cout << " values=["; for (std::size_t idx = 0; idx < preview; ++idx) { if (idx != 0U) { std::cout << ", "; } std::cout << pressures[idx]; } if (preview < pressures.size()) { std::cout << ", ..."; } std::cout << "]"; } std::cout << std::endl; auto matrix = matrix_context_->get(); if (size_hint) { matrix = to_qsize(*size_hint); } matrix = normalize_matrix(matrix, pressures.size()); if (matrix.isEmpty()) { return; } QVector points; points.reserve(matrix.width() * matrix.height()); for (int y = 0; y < matrix.height(); ++y) { for (int x = 0; x < matrix.width(); ++x) { const int idx = y * matrix.width() + x; if (idx >= static_cast(pressures.size())) { break; } const auto value = static_cast(pressures[static_cast(idx)]); points.append(PointData{ static_cast(x), static_cast(y), value }); } } matrix_context_->set(matrix); heatmap_data_->set(std::move(points)); }, Qt::QueuedConnection); } [[nodiscard]] QSize normalize_matrix(QSize candidate, std::size_t value_count) const { if (value_count == 0U) { return QSize{}; } const auto adapt_from = [value_count](const QSize& hint) -> std::optional { if (hint.width() <= 0 && hint.height() <= 0) { return std::nullopt; } if (hint.width() > 0 && hint.height() > 0) { const auto cells = static_cast(hint.width()) * static_cast(hint.height()); if (cells == value_count) { return hint; } } if (hint.width() > 0) { const auto width = static_cast(hint.width()); if (width != 0U && (value_count % width) == 0U) { const auto height = static_cast(value_count / width); return QSize{hint.width(), height}; } } if (hint.height() > 0) { const auto height = static_cast(hint.height()); if (height != 0U && (value_count % height) == 0U) { const auto width = static_cast(value_count / height); return QSize{width, hint.height()}; } } return std::nullopt; }; if (auto adjusted = adapt_from(candidate)) { return *adjusted; } if (auto adjusted = adapt_from(matrix_context_->get())) { return *adjusted; } const auto root = static_cast(std::sqrt(static_cast(value_count))); for (int width = root; width >= 1; --width) { const auto divisor = static_cast(width); if (divisor == 0U) { continue; } if ((value_count % divisor) == 0U) { const auto height = static_cast(value_count / divisor); return QSize{width, height}; } } return QSize{static_cast(value_count), 1}; } std::shared_ptr>> heatmap_data_; std::shared_ptr> matrix_context_; std::unique_ptr core_; QString active_port_; QString last_error_; bool connected_ = false; }; struct SensorUiState { std::shared_ptr> link_icon = std::make_shared>(QString::fromLatin1(material::icon::kAddLink)); std::shared_ptr>> heatmap_data = std::make_shared>>(); std::shared_ptr> heatmap_matrix = std::make_shared>(); std::shared_ptr> port_items = std::make_shared>(); QString selected_port; std::uint32_t selected_baud = 115200; std::unique_ptr controller; SensorUiState() { const QSize size{3, 4}; heatmap_matrix->set_silent(size); heatmap_data->set_silent(make_flat_points(size)); // 初始化串口列表 QStringList ports_list; const auto ports = ffmsep::CPStreamCore::list_available_ports(); ports_list.reserve(static_cast(ports.size())); 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(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 { // text_field::pro::ThemeManager {manager}, // text_field::pro::LeadingIcon {material::icon::kSearch, material::regular::font}, // MutableForward { // text_field::pro::LabelText {}, // slogen_context, // }, // }, lnpro::Item { slpro::ThemeManager {manager}, slpro::LeadingIcon {material::icon::kArrowDropDown, material::regular::font}, 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 {}, sensor.port_items, }, }, lnpro::Item { 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(value); } }}, slpro::LeadingText {"Baud"}, slpro::SelectItems {baud_items}, }, lnpro::SpacingItem {20}, lnpro::Item { 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 { ibpro::ThemeManager { manager }, ibpro::FixedSize { 40, 40 }, ibpro::Color { IconButton::Color::TONAL }, ibpro::Font { material::kRoundSmallFont }, ibpro::FontIcon { material::icon::kRefresh }, ibpro::Clickable {[&sensor] { // 刷新串口列表 QStringList ports_list; const auto ports = ffmsep::CPStreamCore::list_available_ports(); ports_list.reserve(static_cast(ports.size())); for (const auto& info : ports) { ports_list.emplace_back(QString::fromStdString(info.port)); } // 保持原选择(若仍然存在) 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& sensor = sensor_state(); const auto row = new Row{ lnpro::Item { 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 { return new FilledCard { capro::ThemeManager { state.manager }, capro::SizePolicy {QSizePolicy::Expanding}, capro::Layout { lnpro::Alignment {Qt::AlignTop}, lnpro::Margin {10}, lnpro::Spacing {10}, lnpro::Item { ComConfigComponent(state.manager), }, lnpro::Item { lnpro::Item { DisplayComponent(state.manager), }, lnpro::Item { DisplayComponent(state.manager), }, }, }, }; }