feat:heapmap with value;fix:qcustomplot warning
This commit is contained in:
@@ -8,13 +8,17 @@
|
||||
#include "heatmap.hh"
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/widget/sliders.hh"
|
||||
#include <memory>
|
||||
#include <qcolor.h>
|
||||
#include <qdebug.h>
|
||||
using namespace creeper::plot_widget::internal;
|
||||
|
||||
struct BasicPlot::Impl {
|
||||
explicit Impl(BasicPlot& self) noexcept : self{self}, initialized(false), matrix_size(QSize{3, 4}) {}
|
||||
#include "qcustomplot/qcustomplot.h"
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <qcolor.h>
|
||||
#include <qdebug.h>
|
||||
#include <qfont.h>
|
||||
#include <vector>
|
||||
using namespace creeper::plot_widget::internal;
|
||||
|
||||
struct BasicPlot::Impl {
|
||||
explicit Impl(BasicPlot& self) noexcept : self{self}, initialized(false), matrix_size(QSize{3, 4}) {}
|
||||
|
||||
public:
|
||||
auto set_xlabel_text(const QString& text) -> void {
|
||||
@@ -51,31 +55,31 @@ public:
|
||||
});
|
||||
}
|
||||
|
||||
auto set_color_gradient_range(const double& min, const double& max) -> void {
|
||||
if (initialized && self.plottableCount() > 0) {
|
||||
auto* cpmp = static_cast<QCPColorMap*>(self.plottable(0));
|
||||
cpmp->setDataRange(QCPRange(min, max));
|
||||
self.replot();
|
||||
}
|
||||
color_min = min;
|
||||
color_max = max;
|
||||
auto set_color_gradient_range(const double& min, const double& max) -> void {
|
||||
if (initialized && color_map) {
|
||||
color_map->setDataRange(QCPRange(min, max));
|
||||
self.replot();
|
||||
}
|
||||
color_min = min;
|
||||
color_max = max;
|
||||
}
|
||||
|
||||
auto set_data(const QVector<PointData>& data) -> void {
|
||||
data_points = data;
|
||||
if (initialized) {
|
||||
update_plot_data();
|
||||
}
|
||||
}
|
||||
|
||||
auto initialize_plot() -> void {
|
||||
if (initialized) return;
|
||||
|
||||
QCPColorMap* cpmp = new QCPColorMap(self.xAxis, self.yAxis);
|
||||
cpmp->data()->setSize(matrix_size.width(), matrix_size.height());
|
||||
cpmp->data()->setRange(QCPRange(0.5, matrix_size.width() - 0.5),
|
||||
QCPRange(0.5, matrix_size.height() - 0.5));
|
||||
|
||||
auto set_data(const QVector<PointData>& data) -> void {
|
||||
data_points = data;
|
||||
if (initialized && color_map) {
|
||||
update_plot_data();
|
||||
}
|
||||
}
|
||||
|
||||
auto initialize_plot() -> void {
|
||||
if (initialized) return;
|
||||
|
||||
color_map = new QCPColorMap(self.xAxis, self.yAxis);
|
||||
auto* cpmp = color_map;
|
||||
cpmp->data()->setSize(matrix_size.width(), matrix_size.height());
|
||||
cpmp->data()->setRange(QCPRange(0.5, matrix_size.width() - 0.5),
|
||||
QCPRange(0.5, matrix_size.height() - 0.5));
|
||||
|
||||
QSharedPointer<QCPAxisTickerText> xticker(new QCPAxisTickerText);
|
||||
QSharedPointer<QCPAxisTickerText> yticker(new QCPAxisTickerText);
|
||||
xticker->setSubTickCount(1);
|
||||
@@ -103,23 +107,35 @@ QCPColorMap* cpmp = new QCPColorMap(self.xAxis, self.yAxis);
|
||||
if (!xlabel.isEmpty()) self.xAxis->setLabel(xlabel);
|
||||
if (!ylabel.isEmpty()) self.yAxis->setLabel(ylabel);
|
||||
|
||||
QCPColorScale* color_scale = new QCPColorScale(&self);
|
||||
color_scale->setType(QCPAxis::atBottom);
|
||||
self.plotLayout()->addElement(1, 0, color_scale);
|
||||
cpmp->setColorScale(color_scale);
|
||||
|
||||
QCPColorGradient gradient;
|
||||
gradient.setColorStopAt(0.0, QColor(246, 239, 166)); // F6EFA6
|
||||
gradient.setColorStopAt(1.0, QColor(191, 68, 76)); // BF444C
|
||||
if (!color_scale) {
|
||||
color_scale = new QCPColorScale(&self);
|
||||
color_scale->setType(QCPAxis::atBottom);
|
||||
}
|
||||
if (self.plotLayout()) {
|
||||
auto existing = self.plotLayout()->element(1, 0);
|
||||
if (existing && existing != color_scale) {
|
||||
self.plotLayout()->take(existing);
|
||||
}
|
||||
if (!existing || existing != color_scale) {
|
||||
self.plotLayout()->addElement(1, 0, color_scale);
|
||||
}
|
||||
}
|
||||
cpmp->setColorScale(color_scale);
|
||||
|
||||
QCPColorGradient gradient;
|
||||
gradient.setColorStopAt(0.0, QColor(240, 246, 255)); // 低值淡色
|
||||
gradient.setColorStopAt(0.35, QColor(142, 197, 252));
|
||||
gradient.setColorStopAt(0.7, QColor(56, 128, 199));
|
||||
gradient.setColorStopAt(1.0, QColor(8, 36, 95)); // 高值深色
|
||||
cpmp->setGradient(gradient);
|
||||
|
||||
cpmp->setDataRange(QCPRange(color_min, color_max));
|
||||
cpmp->setInterpolate(false);
|
||||
|
||||
QCPMarginGroup *margin_group = new QCPMarginGroup(&self);
|
||||
self.axisRect()->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
|
||||
color_scale->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
|
||||
|
||||
|
||||
QCPMarginGroup *margin_group = new QCPMarginGroup(&self);
|
||||
self.axisRect()->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
|
||||
color_scale->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
|
||||
|
||||
initialized = true;
|
||||
|
||||
if (!data_points.isEmpty()) {
|
||||
@@ -127,33 +143,46 @@ QCPColorMap* cpmp = new QCPColorMap(self.xAxis, self.yAxis);
|
||||
}
|
||||
}
|
||||
|
||||
auto reset_plot() -> void {
|
||||
// 清除所有绘图元素
|
||||
self.clearPlottables();
|
||||
self.clearGraphs();
|
||||
self.clearItems();
|
||||
self.clearFocus();
|
||||
|
||||
// 重新初始化
|
||||
initialized = false;
|
||||
initialize_plot();
|
||||
}
|
||||
auto update_plot_data() -> void {
|
||||
if (!initialized || self.plottableCount() == 0) return;
|
||||
|
||||
auto* cpmp = static_cast<QCPColorMap*>(self.plottable(0));
|
||||
|
||||
// 设置新数据
|
||||
for (const auto& item : data_points) {
|
||||
if (item.x >= 0 && item.x < matrix_size.width() &&
|
||||
item.y >= 0 && item.y < matrix_size.height()) {
|
||||
cpmp->data()->setCell(item.x, item.y, item.z);
|
||||
}
|
||||
}
|
||||
|
||||
// 重绘
|
||||
self.replot();
|
||||
}
|
||||
auto reset_plot() -> void {
|
||||
// 清除所有绘图元素
|
||||
self.clearPlottables();
|
||||
self.clearGraphs();
|
||||
self.clearItems();
|
||||
self.clearFocus();
|
||||
color_map = nullptr;
|
||||
cell_labels.clear();
|
||||
|
||||
// 重新初始化
|
||||
initialized = false;
|
||||
initialize_plot();
|
||||
}
|
||||
auto update_plot_data() -> void {
|
||||
if (!initialized || !color_map) return;
|
||||
|
||||
ensure_labels();
|
||||
|
||||
const int width = matrix_size.width();
|
||||
const int height = matrix_size.height();
|
||||
const int expected = width * height;
|
||||
std::vector<double> values(static_cast<std::size_t>(expected), 0.0);
|
||||
|
||||
// 设置新数据
|
||||
for (const auto& item : data_points) {
|
||||
if (item.x >= 0 && item.x < matrix_size.width() &&
|
||||
item.y >= 0 && item.y < matrix_size.height()) {
|
||||
color_map->data()->setCell(item.x, item.y, item.z);
|
||||
const int idx = static_cast<int>(item.y) * width + static_cast<int>(item.x);
|
||||
if (idx >= 0 && idx < expected) {
|
||||
values[static_cast<std::size_t>(idx)] = item.z;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update_label_values(values);
|
||||
|
||||
// 重绘
|
||||
self.replot();
|
||||
}
|
||||
|
||||
auto is_plot_initialized() const -> bool {
|
||||
return initialized;
|
||||
@@ -167,11 +196,87 @@ private:
|
||||
QString xlabel;
|
||||
QString ylabel;
|
||||
QSize matrix_size;
|
||||
QVector<PointData> data_points;
|
||||
double color_min = 0.0;
|
||||
double color_max = 800.0;
|
||||
bool initialized;
|
||||
BasicPlot& self;
|
||||
};
|
||||
|
||||
#endif // TOUCHSENSOR_HEATMAP_IMPL_HH
|
||||
QVector<PointData> data_points;
|
||||
double color_min = 0.0;
|
||||
double color_max = 800.0;
|
||||
bool initialized;
|
||||
BasicPlot& self;
|
||||
QCPColorScale* color_scale = nullptr;
|
||||
QCPColorMap* color_map = nullptr;
|
||||
QVector<QCPItemText*> cell_labels;
|
||||
|
||||
void clear_labels() {
|
||||
for (auto* label : cell_labels) {
|
||||
if (label) {
|
||||
self.removeItem(label);
|
||||
}
|
||||
}
|
||||
cell_labels.clear();
|
||||
}
|
||||
|
||||
void ensure_labels() {
|
||||
const int width = matrix_size.width();
|
||||
const int height = matrix_size.height();
|
||||
const int expected = width * height;
|
||||
if (expected <= 0) {
|
||||
clear_labels();
|
||||
return;
|
||||
}
|
||||
if (cell_labels.size() == expected) {
|
||||
return;
|
||||
}
|
||||
|
||||
clear_labels();
|
||||
cell_labels.reserve(expected);
|
||||
for (int y = 0; y < height; ++y) {
|
||||
for (int x = 0; x < width; ++x) {
|
||||
auto* label = new QCPItemText(&self);
|
||||
label->position->setType(QCPItemPosition::ptPlotCoords);
|
||||
label->setClipToAxisRect(true);
|
||||
label->setClipAxisRect(self.axisRect());
|
||||
label->setPositionAlignment(Qt::AlignCenter);
|
||||
label->position->setCoords(x + 0.5, y + 0.5);
|
||||
label->setBrush(Qt::NoBrush);
|
||||
label->setPen(Qt::NoPen);
|
||||
QFont font = label->font();
|
||||
if (font.pointSize() > 0) {
|
||||
font.setPointSize(std::max(font.pointSize() - 1, 6));
|
||||
} else {
|
||||
font.setPointSize(8);
|
||||
}
|
||||
label->setFont(font);
|
||||
label->setColor(Qt::black);
|
||||
label->setSelectable(false);
|
||||
cell_labels.push_back(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void update_label_values(const std::vector<double>& values) {
|
||||
const int width = matrix_size.width();
|
||||
const int height = matrix_size.height();
|
||||
const double range = std::max(color_max - color_min, 1.0);
|
||||
const int expected = width * height;
|
||||
for (int idx = 0; idx < expected && idx < cell_labels.size(); ++idx) {
|
||||
auto* label = cell_labels[idx];
|
||||
if (!label) {
|
||||
continue;
|
||||
}
|
||||
const double value = values.size() > static_cast<std::size_t>(idx)
|
||||
? values[static_cast<std::size_t>(idx)]
|
||||
: 0.0;
|
||||
label->setText(QString::number(value, 'f', 0));
|
||||
const double normalized = std::clamp((value - color_min) / range, 0.0, 1.0);
|
||||
if (normalized > 0.55) {
|
||||
label->setColor(Qt::white);
|
||||
} else {
|
||||
label->setColor(QColor(25, 25, 25));
|
||||
}
|
||||
const int x = idx % width;
|
||||
const int y = idx / width;
|
||||
label->position->setCoords(x + 0.5, y + 0.5);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif // TOUCHSENSOR_HEATMAP_IMPL_HH
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "components/ffmsep/cpstream_core.hh"
|
||||
|
||||
#include "components/ffmsep/presist/presist.hh"
|
||||
#include "dlog/dlog.hh"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
@@ -7,19 +10,21 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace ffmsep {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kReaderIdleSleep = std::chrono::milliseconds(5);
|
||||
constexpr auto kDecoderIdleSleep = std::chrono::milliseconds(1);
|
||||
constexpr auto kReaderIdleSleep = 5ms;
|
||||
constexpr auto kDecoderIdleSleep = 1ms;
|
||||
|
||||
const CPCodec* resolve_requested_codec(const CPStreamConfig& config) {
|
||||
if (!config.codec_name.empty()) {
|
||||
@@ -48,6 +53,7 @@ struct CPStreamCore::Impl {
|
||||
explicit Impl(CPStreamConfig config)
|
||||
: config_(std::move(config)) {
|
||||
normalize_config();
|
||||
frame_writer_ = std::make_unique<persist::JsonWritter>();
|
||||
}
|
||||
|
||||
~Impl() = default;
|
||||
@@ -63,7 +69,7 @@ struct CPStreamCore::Impl {
|
||||
config_.frame_queue_capacity = 1U;
|
||||
}
|
||||
if (config_.slave_request_interval.count() < 0) {
|
||||
config_.slave_request_interval = std::chrono::milliseconds{0};
|
||||
config_.slave_request_interval = 0ms;
|
||||
}
|
||||
frame_queue_capacity_ = config_.frame_queue_capacity;
|
||||
}
|
||||
@@ -115,7 +121,7 @@ struct CPStreamCore::Impl {
|
||||
serial->flush();
|
||||
|
||||
{
|
||||
std::lock_guard lock(serial_mutex_);
|
||||
std::lock_guard<std::mutex> lock(serial_mutex_);
|
||||
serial_ = std::move(serial);
|
||||
}
|
||||
} catch (const serial::IOException& ex) {
|
||||
@@ -139,11 +145,11 @@ struct CPStreamCore::Impl {
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lock(packet_mutex_);
|
||||
std::lock_guard<std::mutex> lock(packet_mutex_);
|
||||
packet_queue_.clear();
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(frame_mutex_);
|
||||
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||
frame_queue_.clear();
|
||||
}
|
||||
pts_counter_.store(0, std::memory_order_relaxed);
|
||||
@@ -164,7 +170,7 @@ struct CPStreamCore::Impl {
|
||||
stop();
|
||||
|
||||
{
|
||||
std::lock_guard lock(serial_mutex_);
|
||||
std::lock_guard<std::mutex> lock(serial_mutex_);
|
||||
if (serial_) {
|
||||
try {
|
||||
if (serial_->isOpen()) {
|
||||
@@ -184,12 +190,13 @@ struct CPStreamCore::Impl {
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lock(packet_mutex_);
|
||||
std::lock_guard<std::mutex> lock(packet_mutex_);
|
||||
packet_queue_.clear();
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(frame_mutex_);
|
||||
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||
frame_queue_.clear();
|
||||
frame_record_queue_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +207,7 @@ struct CPStreamCore::Impl {
|
||||
|
||||
std::shared_ptr<serial::Serial> serial_copy;
|
||||
{
|
||||
std::lock_guard lock(serial_mutex_);
|
||||
std::lock_guard<std::mutex> lock(serial_mutex_);
|
||||
serial_copy = serial_;
|
||||
}
|
||||
if (!serial_copy || !serial_copy->isOpen()) {
|
||||
@@ -249,7 +256,7 @@ struct CPStreamCore::Impl {
|
||||
stop_requested_.store(false, std::memory_order_release);
|
||||
|
||||
{
|
||||
std::lock_guard lock(packet_mutex_);
|
||||
std::lock_guard<std::mutex> lock(packet_mutex_);
|
||||
packet_queue_.clear();
|
||||
}
|
||||
|
||||
@@ -259,7 +266,7 @@ struct CPStreamCore::Impl {
|
||||
}
|
||||
|
||||
bool is_open() const {
|
||||
std::lock_guard lock(serial_mutex_);
|
||||
std::lock_guard<std::mutex> lock(serial_mutex_);
|
||||
return serial_ && serial_->isOpen();
|
||||
}
|
||||
|
||||
@@ -278,7 +285,7 @@ struct CPStreamCore::Impl {
|
||||
|
||||
std::shared_ptr<serial::Serial> serial_copy;
|
||||
{
|
||||
std::lock_guard lock(serial_mutex_);
|
||||
std::lock_guard<std::mutex> lock(serial_mutex_);
|
||||
serial_copy = serial_;
|
||||
}
|
||||
|
||||
@@ -300,17 +307,17 @@ struct CPStreamCore::Impl {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<DecodedFrame> try_pop_frame() {
|
||||
std::lock_guard lock(frame_mutex_);
|
||||
std::optional<std::shared_ptr<DecodedFrame>> try_pop_frame() {
|
||||
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||
if (frame_queue_.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
DecodedFrame frame = std::move(frame_queue_.front());
|
||||
std::shared_ptr<DecodedFrame> frame = std::move(frame_queue_.front());
|
||||
frame_queue_.pop_front();
|
||||
return frame;
|
||||
}
|
||||
|
||||
bool wait_for_frame(DecodedFrame& frame, std::chrono::milliseconds timeout) {
|
||||
bool wait_for_frame(std::shared_ptr<DecodedFrame>& frame, std::chrono::milliseconds timeout) {
|
||||
std::unique_lock lock(frame_mutex_);
|
||||
if (!frame_cv_.wait_for(lock, timeout, [&] {
|
||||
return !frame_queue_.empty();
|
||||
@@ -323,7 +330,7 @@ struct CPStreamCore::Impl {
|
||||
}
|
||||
|
||||
void clear_frames() {
|
||||
std::lock_guard lock(frame_mutex_);
|
||||
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||
frame_queue_.clear();
|
||||
}
|
||||
|
||||
@@ -332,7 +339,7 @@ struct CPStreamCore::Impl {
|
||||
capacity = 1U;
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(frame_mutex_);
|
||||
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||
frame_queue_capacity_ = capacity;
|
||||
config_.frame_queue_capacity = capacity;
|
||||
while (frame_queue_.size() > frame_queue_capacity_) {
|
||||
@@ -341,8 +348,46 @@ struct CPStreamCore::Impl {
|
||||
}
|
||||
}
|
||||
|
||||
void clear_recorded_frames() {
|
||||
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||
frame_record_queue_.clear();
|
||||
}
|
||||
|
||||
std::size_t recorded_frame_count() const {
|
||||
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||
return frame_record_queue_.size();
|
||||
}
|
||||
|
||||
std::future<persist::WriteResult> export_recorded_frames(const std::string& path, bool clear_after_export) {
|
||||
if (!frame_writer_) {
|
||||
frame_writer_ = std::make_unique<persist::JsonWritter>();
|
||||
}
|
||||
|
||||
std::deque<std::shared_ptr<DecodedFrame>> snapshot;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||
snapshot = frame_record_queue_;
|
||||
if (clear_after_export) {
|
||||
frame_record_queue_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (snapshot.empty()) {
|
||||
std::promise<persist::WriteResult> promise;
|
||||
auto future = promise.get_future();
|
||||
promise.set_value(persist::WriteResult{
|
||||
false,
|
||||
"no recorded frames available",
|
||||
path
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
return frame_writer_->enqueue(path, std::move(snapshot));
|
||||
}
|
||||
|
||||
void set_frame_callback(FrameCallback callback) {
|
||||
std::lock_guard lock(callback_mutex_);
|
||||
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||
frame_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
@@ -351,7 +396,7 @@ struct CPStreamCore::Impl {
|
||||
}
|
||||
|
||||
std::string last_error() const {
|
||||
std::lock_guard lock(last_error_mutex_);
|
||||
std::lock_guard<std::mutex> lock(last_error_mutex_);
|
||||
return last_error_;
|
||||
}
|
||||
|
||||
@@ -365,7 +410,7 @@ struct CPStreamCore::Impl {
|
||||
while (!stop_requested_.load(std::memory_order_acquire)) {
|
||||
std::shared_ptr<serial::Serial> serial_copy;
|
||||
{
|
||||
std::lock_guard lock(serial_mutex_);
|
||||
std::lock_guard<std::mutex> lock(serial_mutex_);
|
||||
serial_copy = serial_;
|
||||
}
|
||||
if (!serial_copy || !serial_copy->isOpen()) {
|
||||
@@ -400,7 +445,7 @@ struct CPStreamCore::Impl {
|
||||
packet.pts = pts_counter_.fetch_add(1, std::memory_order_relaxed);
|
||||
|
||||
{
|
||||
std::lock_guard lock(packet_mutex_);
|
||||
std::lock_guard<std::mutex> lock(packet_mutex_);
|
||||
if (packet_queue_.size() >= config_.packet_queue_capacity) {
|
||||
packet_queue_.pop_front();
|
||||
}
|
||||
@@ -414,7 +459,7 @@ struct CPStreamCore::Impl {
|
||||
const auto command = config_.slave_request_command;
|
||||
auto interval = config_.slave_request_interval;
|
||||
if (interval.count() < 0) {
|
||||
interval = std::chrono::milliseconds{0};
|
||||
interval = 0ms;
|
||||
}
|
||||
const bool repeat = interval.count() > 0;
|
||||
|
||||
@@ -484,23 +529,24 @@ struct CPStreamCore::Impl {
|
||||
CPFrame frame;
|
||||
rc = cpcodec_receive_frame(codec_ctx_, &frame);
|
||||
if (rc == CP_SUCCESS) {
|
||||
DecodedFrame decoded;
|
||||
decoded.pts = frame.pts;
|
||||
decoded.received_at = std::chrono::steady_clock::now();
|
||||
decoded.frame = std::move(frame);
|
||||
if (codec_descriptor_ && codec_descriptor_->id == CPCodecID::Tactile) {
|
||||
if (auto parsed = tactile::parse_frame(decoded.frame)) {
|
||||
decoded.tactile = parsed;
|
||||
decoded.tactile_pressures = tactile::parse_pressure_values(*parsed);
|
||||
auto decoded = std::make_shared<DecodedFrame>();
|
||||
decoded->pts = frame.pts;
|
||||
decoded->received_at = std::chrono::steady_clock::now();
|
||||
decoded->frame = std::move(frame);
|
||||
decoded->id = codec_descriptor_ ? codec_descriptor_->id : CPCodecID::Unknow;
|
||||
if (decoded->id == CPCodecID::Tactile) {
|
||||
if (auto parsed = tactile::parse_frame(decoded->frame)) {
|
||||
decoded->tactile = parsed;
|
||||
decoded->tactile_pressures = tactile::parse_pressure_values(*parsed);
|
||||
if (auto matrix = tactile::parse_matrix_size_payload(*parsed)) {
|
||||
decoded.tactile_matrix_size = matrix;
|
||||
decoded->tactile_matrix_size = matrix;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FrameCallback callback_copy;
|
||||
{
|
||||
std::lock_guard lock(callback_mutex_);
|
||||
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||
callback_copy = frame_callback_;
|
||||
}
|
||||
if (callback_copy) {
|
||||
@@ -508,11 +554,14 @@ struct CPStreamCore::Impl {
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lock(frame_mutex_);
|
||||
std::lock_guard<std::mutex> lock(frame_mutex_);
|
||||
if (frame_queue_.size() >= frame_queue_capacity_) {
|
||||
frame_queue_.pop_front();
|
||||
}
|
||||
frame_queue_.push_back(std::move(decoded));
|
||||
frame_queue_.push_back(decoded);
|
||||
if (decoded->id == CPCodecID::Tactile) {
|
||||
frame_record_queue_.push_back(decoded);
|
||||
}
|
||||
}
|
||||
frame_cv_.notify_one();
|
||||
} else if (rc == CP_ERROR_EAGAIN) {
|
||||
@@ -536,7 +585,7 @@ struct CPStreamCore::Impl {
|
||||
packet.flush = true;
|
||||
packet.end_of_stream = end_of_stream;
|
||||
{
|
||||
std::lock_guard lock(packet_mutex_);
|
||||
std::lock_guard<std::mutex> lock(packet_mutex_);
|
||||
packet_queue_.push_back(std::move(packet));
|
||||
}
|
||||
packet_cv_.notify_one();
|
||||
@@ -554,7 +603,7 @@ struct CPStreamCore::Impl {
|
||||
}
|
||||
|
||||
void set_last_error(std::string message) {
|
||||
std::lock_guard lock(last_error_mutex_);
|
||||
std::lock_guard<std::mutex> lock(last_error_mutex_);
|
||||
last_error_ = std::move(message);
|
||||
}
|
||||
|
||||
@@ -574,9 +623,12 @@ struct CPStreamCore::Impl {
|
||||
std::condition_variable packet_cv_;
|
||||
std::deque<Packet> packet_queue_;
|
||||
|
||||
std::mutex frame_mutex_;
|
||||
mutable std::mutex frame_mutex_;
|
||||
std::condition_variable frame_cv_;
|
||||
std::deque<DecodedFrame> frame_queue_;
|
||||
// std::deque<DecodedFrame> frame_queue_;
|
||||
// 更新为智能指针,我们需要更长的生命周期😊
|
||||
std::deque<std::shared_ptr<DecodedFrame>> frame_queue_;
|
||||
std::deque<std::shared_ptr<DecodedFrame>> frame_record_queue_;
|
||||
std::size_t frame_queue_capacity_ = 16;
|
||||
|
||||
FrameCallback frame_callback_;
|
||||
@@ -588,6 +640,8 @@ struct CPStreamCore::Impl {
|
||||
|
||||
std::string last_error_;
|
||||
mutable std::mutex last_error_mutex_;
|
||||
|
||||
std::unique_ptr<persist::JsonWritter> frame_writer_;
|
||||
};
|
||||
|
||||
CPStreamCore::CPStreamCore(CPStreamConfig config)
|
||||
@@ -640,11 +694,11 @@ bool CPStreamCore::send(const std::uint8_t* data, std::size_t size) {
|
||||
return impl_->send(data, size);
|
||||
}
|
||||
|
||||
std::optional<DecodedFrame> CPStreamCore::try_pop_frame() {
|
||||
std::optional<std::shared_ptr<DecodedFrame>> CPStreamCore::try_pop_frame() {
|
||||
return impl_->try_pop_frame();
|
||||
}
|
||||
|
||||
bool CPStreamCore::wait_for_frame(DecodedFrame& frame, std::chrono::milliseconds timeout) {
|
||||
bool CPStreamCore::wait_for_frame(std::shared_ptr<DecodedFrame>& frame, std::chrono::milliseconds timeout) {
|
||||
return impl_->wait_for_frame(frame, timeout);
|
||||
}
|
||||
|
||||
@@ -656,6 +710,18 @@ void CPStreamCore::set_frame_queue_capacity(std::size_t capacity) {
|
||||
impl_->set_frame_queue_capacity(capacity);
|
||||
}
|
||||
|
||||
void CPStreamCore::clear_recorded_frames() {
|
||||
impl_->clear_recorded_frames();
|
||||
}
|
||||
|
||||
std::size_t CPStreamCore::recorded_frame_count() const {
|
||||
return impl_->recorded_frame_count();
|
||||
}
|
||||
|
||||
std::future<persist::WriteResult> CPStreamCore::export_recorded_frames(const std::string& path, bool clear_after_export) {
|
||||
return impl_->export_recorded_frames(path, clear_after_export);
|
||||
}
|
||||
|
||||
void CPStreamCore::set_frame_callback(FrameCallback callback) {
|
||||
impl_->set_frame_callback(std::move(callback));
|
||||
}
|
||||
|
||||
@@ -5,16 +5,23 @@
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <serial/serial.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace ffmsep {
|
||||
|
||||
namespace persist {
|
||||
struct WriteResult;
|
||||
} // namespace persist
|
||||
|
||||
struct DecodedFrame {
|
||||
CPFrame frame;
|
||||
CPCodecID id = CPCodecID::Unknow;
|
||||
std::chrono::steady_clock::time_point received_at{};
|
||||
std::int64_t pts = 0;
|
||||
std::optional<tactile::TactileFrame> tactile;
|
||||
@@ -36,12 +43,12 @@ struct CPStreamConfig {
|
||||
CPCodecID codec_id = CPCodecID::Unknow;
|
||||
std::string codec_name;
|
||||
std::vector<std::uint8_t> slave_request_command{};
|
||||
std::chrono::milliseconds slave_request_interval{200};
|
||||
std::chrono::milliseconds slave_request_interval{200ms};
|
||||
};
|
||||
|
||||
class CPStreamCore {
|
||||
public:
|
||||
using FrameCallback = std::function<void(const DecodedFrame&)>;
|
||||
using FrameCallback = std::function<void(std::shared_ptr<DecodedFrame>)>;
|
||||
|
||||
explicit CPStreamCore(CPStreamConfig config = {});
|
||||
~CPStreamCore();
|
||||
@@ -63,10 +70,14 @@ public:
|
||||
bool send(const std::vector<std::uint8_t>& data);
|
||||
bool send(const std::uint8_t* data, std::size_t size);
|
||||
|
||||
std::optional<DecodedFrame> try_pop_frame();
|
||||
bool wait_for_frame(DecodedFrame& frame, std::chrono::milliseconds timeout);
|
||||
std::optional<std::shared_ptr<DecodedFrame>> try_pop_frame();
|
||||
bool wait_for_frame(std::shared_ptr<DecodedFrame>& frame, std::chrono::milliseconds timeout);
|
||||
void clear_frames();
|
||||
void set_frame_queue_capacity(std::size_t capacity);
|
||||
void clear_recorded_frames();
|
||||
[[nodiscard]] std::size_t recorded_frame_count() const;
|
||||
std::future<persist::WriteResult> export_recorded_frames(const std::string& path,
|
||||
bool clear_after_export = false);
|
||||
|
||||
void set_frame_callback(FrameCallback callback);
|
||||
|
||||
@@ -75,6 +86,8 @@ public:
|
||||
|
||||
static std::vector<serial::PortInfo> list_available_ports();
|
||||
|
||||
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
|
||||
186
components/ffmsep/presist/presist.cc
Normal file
186
components/ffmsep/presist/presist.cc
Normal file
@@ -0,0 +1,186 @@
|
||||
//
|
||||
// Created by Lenn on 2025/10/31.
|
||||
//
|
||||
|
||||
#include "components/ffmsep/presist/presist.hh"
|
||||
|
||||
#include "components/ffmsep/cpstream_core.hh"
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <system_error>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace ffmsep::persist {
|
||||
|
||||
namespace {
|
||||
|
||||
using nlohmann::json;
|
||||
|
||||
json serialize_tactile_frame(const DecodedFrame& frame) {
|
||||
json result = {
|
||||
{"pts", frame.pts},
|
||||
{"raw_frame", frame.frame.data},
|
||||
{"pressures", frame.tactile_pressures},
|
||||
};
|
||||
|
||||
const auto received = frame.received_at.time_since_epoch();
|
||||
result["received_at_ns"] =
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(received).count();
|
||||
|
||||
if (frame.tactile_matrix_size) {
|
||||
result["matrix"] = {
|
||||
{"long_edge", frame.tactile_matrix_size->long_edge},
|
||||
{"short_edge", frame.tactile_matrix_size->short_edge},
|
||||
};
|
||||
}
|
||||
|
||||
if (frame.tactile) {
|
||||
const auto& tactile = *frame.tactile;
|
||||
result["tactile"] = {
|
||||
{"device_address", tactile.device_address},
|
||||
{"response_function", tactile.response_function},
|
||||
{"function", static_cast<std::uint8_t>(tactile.function)},
|
||||
{"start_address", tactile.start_address},
|
||||
{"return_byte_count", tactile.return_byte_count},
|
||||
{"status", tactile.status},
|
||||
{"payload", tactile.payload},
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool WriteQueue::push(WriteRequest&& req) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (stopped_) {
|
||||
return false;
|
||||
}
|
||||
queue_.push(std::move(req));
|
||||
}
|
||||
cond_.notify_one();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WriteQueue::pop(WriteRequest& out) {
|
||||
std::unique_lock lock(mutex_);
|
||||
cond_.wait(lock, [&] {
|
||||
return stopped_ || !queue_.empty();
|
||||
});
|
||||
|
||||
if (queue_.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
out = std::move(queue_.front());
|
||||
queue_.pop();
|
||||
return true;
|
||||
}
|
||||
|
||||
void WriteQueue::stop() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
stopped_ = true;
|
||||
}
|
||||
cond_.notify_all();
|
||||
}
|
||||
|
||||
JsonWritter::JsonWritter()
|
||||
: write_thread_([this] { run(); }) {}
|
||||
|
||||
JsonWritter::~JsonWritter() {
|
||||
stop();
|
||||
}
|
||||
|
||||
std::future<WriteResult> JsonWritter::enqueue(std::string path,
|
||||
std::deque<std::shared_ptr<DecodedFrame>> frames) {
|
||||
std::promise<WriteResult> promise;
|
||||
auto future = promise.get_future();
|
||||
|
||||
WriteRequest request{std::move(path), std::move(frames), std::move(promise)};
|
||||
if (!write_queue_.push(std::move(request))) {
|
||||
WriteResult result{false, "writer has been stopped", request.path};
|
||||
request.promise.set_value(std::move(result));
|
||||
}
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
void JsonWritter::run() {
|
||||
WriteRequest request;
|
||||
while (write_queue_.pop(request)) {
|
||||
try {
|
||||
auto result = write_once(request.path, std::move(request.frames));
|
||||
request.promise.set_value(std::move(result));
|
||||
} catch (const std::exception& ex) {
|
||||
request.promise.set_value(WriteResult{false, ex.what(), request.path});
|
||||
} catch (...) {
|
||||
request.promise.set_value(WriteResult{false, "unknown error", request.path});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WriteResult JsonWritter::write_once(const std::string& path,
|
||||
std::deque<std::shared_ptr<DecodedFrame>> frames) {
|
||||
if (path.empty()) {
|
||||
return {false, "export path is empty", path};
|
||||
}
|
||||
|
||||
json tactile_frames = json::array();
|
||||
|
||||
for (const auto& frame : frames) {
|
||||
if (!frame) {
|
||||
continue;
|
||||
}
|
||||
if (frame->id != CPCodecID::Tactile || !frame->tactile) {
|
||||
continue;
|
||||
}
|
||||
tactile_frames.push_back(serialize_tactile_frame(*frame));
|
||||
}
|
||||
|
||||
if (tactile_frames.empty()) {
|
||||
return {false, "no tactile frames available for export", path};
|
||||
}
|
||||
|
||||
json root;
|
||||
root["codec"] = "tactile";
|
||||
root["frames"] = std::move(tactile_frames);
|
||||
|
||||
std::filesystem::path fs_path(path);
|
||||
if (fs_path.has_parent_path()) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(fs_path.parent_path(), ec);
|
||||
if (ec) {
|
||||
return {false, "failed to create export directory: " + ec.message(), path};
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream stream(path, std::ios::binary | std::ios::trunc);
|
||||
if (!stream.is_open()) {
|
||||
return {false, "failed to open export file", path};
|
||||
}
|
||||
|
||||
stream << std::setw(2) << root;
|
||||
stream.flush();
|
||||
if (!stream.good()) {
|
||||
return {false, "failed to write export file", path};
|
||||
}
|
||||
|
||||
return {true, {}, path};
|
||||
}
|
||||
|
||||
void JsonWritter::stop() {
|
||||
if (!stopped_.exchange(true)) {
|
||||
write_queue_.stop();
|
||||
if (write_thread_.joinable()) {
|
||||
write_thread_.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ffmsep::persist
|
||||
86
components/ffmsep/presist/presist.hh
Normal file
86
components/ffmsep/presist/presist.hh
Normal file
@@ -0,0 +1,86 @@
|
||||
//
|
||||
// Created by Lenn on 2025/10/31.
|
||||
//
|
||||
|
||||
#ifndef TOUCHSENSOR_PRESIST_HH
|
||||
#define TOUCHSENSOR_PRESIST_HH
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace ffmsep {
|
||||
|
||||
struct DecodedFrame;
|
||||
|
||||
namespace persist {
|
||||
|
||||
struct WriteResult {
|
||||
bool ok = false;
|
||||
std::string error;
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct WriteRequest {
|
||||
WriteRequest() = default;
|
||||
WriteRequest(std::string p,
|
||||
std::deque<std::shared_ptr<DecodedFrame>> f,
|
||||
std::promise<WriteResult>&& pr)
|
||||
: path(std::move(p))
|
||||
, frames(std::move(f))
|
||||
, promise(std::move(pr)) {}
|
||||
|
||||
WriteRequest(const WriteRequest&) = delete;
|
||||
WriteRequest& operator=(const WriteRequest&) = delete;
|
||||
WriteRequest(WriteRequest&&) noexcept = default;
|
||||
WriteRequest& operator=(WriteRequest&&) noexcept = default;
|
||||
|
||||
std::string path;
|
||||
std::deque<std::shared_ptr<DecodedFrame>> frames;
|
||||
std::promise<WriteResult> promise;
|
||||
};
|
||||
|
||||
class WriteQueue {
|
||||
public:
|
||||
bool push(WriteRequest&& req);
|
||||
bool pop(WriteRequest& out);
|
||||
void stop();
|
||||
|
||||
private:
|
||||
std::mutex mutex_;
|
||||
std::condition_variable cond_;
|
||||
std::queue<WriteRequest> queue_;
|
||||
bool stopped_ = false;
|
||||
};
|
||||
|
||||
class JsonWritter {
|
||||
public:
|
||||
JsonWritter();
|
||||
~JsonWritter();
|
||||
|
||||
std::future<WriteResult> enqueue(std::string path,
|
||||
std::deque<std::shared_ptr<DecodedFrame>> frames);
|
||||
|
||||
void stop();
|
||||
|
||||
private:
|
||||
void run();
|
||||
WriteResult write_once(const std::string& path,
|
||||
std::deque<std::shared_ptr<DecodedFrame>> frames);
|
||||
|
||||
private:
|
||||
std::thread write_thread_;
|
||||
WriteQueue write_queue_;
|
||||
std::atomic_bool stopped_{false};
|
||||
};
|
||||
|
||||
} // namespace persist
|
||||
} // namespace ffmsep
|
||||
|
||||
#endif // TOUCHSENSOR_PRESIST_HH
|
||||
@@ -2,25 +2,25 @@
|
||||
// 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 <chrono>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#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 <chrono>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include "component.hh"
|
||||
#include "cpstream_core.hh"
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
@@ -43,6 +43,7 @@
|
||||
#include <modern-qt/widget/select.hh>
|
||||
#include "components/ffmsep/tactile/tacdec.hh"
|
||||
|
||||
#define DEBUG 0
|
||||
|
||||
using namespace creeper;
|
||||
namespace capro = card::pro;
|
||||
@@ -98,10 +99,10 @@ public:
|
||||
reset_core();
|
||||
}
|
||||
|
||||
bool start(const QString& requested_port, std::uint32_t baudrate) {
|
||||
if (is_connected()) {
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
@@ -112,17 +113,23 @@ public:
|
||||
[&](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;
|
||||
}
|
||||
@@ -149,7 +156,7 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
core_->set_frame_callback([this](const ffmsep::DecodedFrame& frame) {
|
||||
core_->set_frame_callback([this](std::shared_ptr<ffmsep::DecodedFrame> frame) {
|
||||
handle_frame(frame);
|
||||
});
|
||||
|
||||
@@ -161,44 +168,44 @@ public:
|
||||
}
|
||||
|
||||
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_;
|
||||
}
|
||||
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()) {
|
||||
void reset_core() {
|
||||
connected_ = false;
|
||||
if (!core_) {
|
||||
return;
|
||||
}
|
||||
core_->set_frame_callback({});
|
||||
if (core_->is_running()) {
|
||||
core_->stop();
|
||||
}
|
||||
if (core_->is_open()) {
|
||||
@@ -214,51 +221,51 @@ private:
|
||||
};
|
||||
}
|
||||
|
||||
void handle_frame(const ffmsep::DecodedFrame& frame) {
|
||||
if (!frame.tactile || frame.tactile_pressures.empty()) {
|
||||
void handle_frame(std::shared_ptr<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);
|
||||
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) {
|
||||
@@ -305,107 +312,107 @@ private:
|
||||
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};
|
||||
}
|
||||
[[nodiscard]] QSize normalize_matrix(QSize candidate, std::size_t value_count) const {
|
||||
if (value_count == 0U) {
|
||||
return QSize{};
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
const auto adapt_from = [value_count](const QSize& hint) -> std::optional<QSize> {
|
||||
if (hint.width() <= 0 && hint.height() <= 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
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;
|
||||
@@ -418,10 +425,10 @@ 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();
|
||||
}
|
||||
// 串口下拉:改为绑定可变数据源,初始值由 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"),
|
||||
@@ -440,21 +447,21 @@ static auto ComConfigComponent(ThemeManager& manager) {
|
||||
// slogen_context,
|
||||
// },
|
||||
// },
|
||||
lnpro::Item<MatSelect> {
|
||||
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<MatSelect> {
|
||||
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<MatSelect> {
|
||||
slpro::ThemeManager {manager },
|
||||
slpro::LeadingIcon { material::icon::kArrowDropDown, material::regular::font},
|
||||
@@ -484,10 +491,10 @@ static auto ComConfigComponent(ThemeManager& manager) {
|
||||
if (!sensor.controller) {
|
||||
return;
|
||||
}
|
||||
if (sensor.controller->is_connected()) {
|
||||
sensor.controller->stop();
|
||||
link_icon_context->set(QString::fromLatin1(material::icon::kAddLink));
|
||||
} else {
|
||||
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)) {
|
||||
@@ -501,34 +508,34 @@ static auto ComConfigComponent(ThemeManager& manager) {
|
||||
}
|
||||
} }
|
||||
},
|
||||
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 {[&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()) {
|
||||
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));
|
||||
}},
|
||||
},
|
||||
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 {[&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()) {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user