feat:heapmap with value;fix:qcustomplot warning

This commit is contained in:
2025-11-04 10:47:41 +08:00
parent f411ab21cb
commit a07ff7d6b7
9 changed files with 852 additions and 389 deletions

View File

@@ -71,11 +71,13 @@ file(
set(FFMSEP_SOURCES set(FFMSEP_SOURCES
components/ffmsep/cpdecoder.cc components/ffmsep/cpdecoder.cc
components/ffmsep/cpstream_core.cc components/ffmsep/cpstream_core.cc
components/ffmsep/presist/presist.cc
components/ffmsep/tactile/tacdec.cc components/ffmsep/tactile/tacdec.cc
) )
set(FFMSEP_HEADERS set(FFMSEP_HEADERS
components/ffmsep/cpdecoder.hh components/ffmsep/cpdecoder.hh
components/ffmsep/cpstream_core.hh components/ffmsep/cpstream_core.hh
components/ffmsep/presist/presist.hh
components/ffmsep/tactile/tacdec.hh components/ffmsep/tactile/tacdec.hh
) )
set(FFMSEP_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/components/ffmsep") set(FFMSEP_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/components/ffmsep")

View File

@@ -8,9 +8,13 @@
#include "heatmap.hh" #include "heatmap.hh"
#include "modern-qt/utility/theme/theme.hh" #include "modern-qt/utility/theme/theme.hh"
#include "modern-qt/widget/sliders.hh" #include "modern-qt/widget/sliders.hh"
#include "qcustomplot/qcustomplot.h"
#include <algorithm>
#include <memory> #include <memory>
#include <qcolor.h> #include <qcolor.h>
#include <qdebug.h> #include <qdebug.h>
#include <qfont.h>
#include <vector>
using namespace creeper::plot_widget::internal; using namespace creeper::plot_widget::internal;
struct BasicPlot::Impl { struct BasicPlot::Impl {
@@ -52,9 +56,8 @@ public:
} }
auto set_color_gradient_range(const double& min, const double& max) -> void { auto set_color_gradient_range(const double& min, const double& max) -> void {
if (initialized && self.plottableCount() > 0) { if (initialized && color_map) {
auto* cpmp = static_cast<QCPColorMap*>(self.plottable(0)); color_map->setDataRange(QCPRange(min, max));
cpmp->setDataRange(QCPRange(min, max));
self.replot(); self.replot();
} }
color_min = min; color_min = min;
@@ -63,7 +66,7 @@ public:
auto set_data(const QVector<PointData>& data) -> void { auto set_data(const QVector<PointData>& data) -> void {
data_points = data; data_points = data;
if (initialized) { if (initialized && color_map) {
update_plot_data(); update_plot_data();
} }
} }
@@ -71,7 +74,8 @@ public:
auto initialize_plot() -> void { auto initialize_plot() -> void {
if (initialized) return; if (initialized) return;
QCPColorMap* cpmp = new QCPColorMap(self.xAxis, self.yAxis); color_map = new QCPColorMap(self.xAxis, self.yAxis);
auto* cpmp = color_map;
cpmp->data()->setSize(matrix_size.width(), matrix_size.height()); cpmp->data()->setSize(matrix_size.width(), matrix_size.height());
cpmp->data()->setRange(QCPRange(0.5, matrix_size.width() - 0.5), cpmp->data()->setRange(QCPRange(0.5, matrix_size.width() - 0.5),
QCPRange(0.5, matrix_size.height() - 0.5)); QCPRange(0.5, matrix_size.height() - 0.5));
@@ -103,14 +107,26 @@ QCPColorMap* cpmp = new QCPColorMap(self.xAxis, self.yAxis);
if (!xlabel.isEmpty()) self.xAxis->setLabel(xlabel); if (!xlabel.isEmpty()) self.xAxis->setLabel(xlabel);
if (!ylabel.isEmpty()) self.yAxis->setLabel(ylabel); if (!ylabel.isEmpty()) self.yAxis->setLabel(ylabel);
QCPColorScale* color_scale = new QCPColorScale(&self); if (!color_scale) {
color_scale = new QCPColorScale(&self);
color_scale->setType(QCPAxis::atBottom); 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); self.plotLayout()->addElement(1, 0, color_scale);
}
}
cpmp->setColorScale(color_scale); cpmp->setColorScale(color_scale);
QCPColorGradient gradient; QCPColorGradient gradient;
gradient.setColorStopAt(0.0, QColor(246, 239, 166)); // F6EFA6 gradient.setColorStopAt(0.0, QColor(240, 246, 255)); // 低值淡色
gradient.setColorStopAt(1.0, QColor(191, 68, 76)); // BF444C 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->setGradient(gradient);
cpmp->setDataRange(QCPRange(color_min, color_max)); cpmp->setDataRange(QCPRange(color_min, color_max));
@@ -133,23 +149,36 @@ QCPColorMap* cpmp = new QCPColorMap(self.xAxis, self.yAxis);
self.clearGraphs(); self.clearGraphs();
self.clearItems(); self.clearItems();
self.clearFocus(); self.clearFocus();
color_map = nullptr;
cell_labels.clear();
// 重新初始化 // 重新初始化
initialized = false; initialized = false;
initialize_plot(); initialize_plot();
} }
auto update_plot_data() -> void { auto update_plot_data() -> void {
if (!initialized || self.plottableCount() == 0) return; if (!initialized || !color_map) return;
auto* cpmp = static_cast<QCPColorMap*>(self.plottable(0)); 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) { for (const auto& item : data_points) {
if (item.x >= 0 && item.x < matrix_size.width() && if (item.x >= 0 && item.x < matrix_size.width() &&
item.y >= 0 && item.y < matrix_size.height()) { item.y >= 0 && item.y < matrix_size.height()) {
cpmp->data()->setCell(item.x, item.y, item.z); 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(); self.replot();
@@ -172,6 +201,82 @@ private:
double color_max = 800.0; double color_max = 800.0;
bool initialized; bool initialized;
BasicPlot& self; 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 #endif // TOUCHSENSOR_HEATMAP_IMPL_HH

View File

@@ -1,5 +1,8 @@
#include "components/ffmsep/cpstream_core.hh" #include "components/ffmsep/cpstream_core.hh"
#include "components/ffmsep/presist/presist.hh"
#include "dlog/dlog.hh"
#include <algorithm> #include <algorithm>
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
@@ -7,19 +10,21 @@
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <deque> #include <deque>
#include <future>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <optional> #include <optional>
#include <thread> #include <thread>
#include <utility> #include <utility>
#include <vector> #include <vector>
using namespace std::chrono_literals;
namespace ffmsep { namespace ffmsep {
namespace { namespace {
constexpr auto kReaderIdleSleep = std::chrono::milliseconds(5); constexpr auto kReaderIdleSleep = 5ms;
constexpr auto kDecoderIdleSleep = std::chrono::milliseconds(1); constexpr auto kDecoderIdleSleep = 1ms;
const CPCodec* resolve_requested_codec(const CPStreamConfig& config) { const CPCodec* resolve_requested_codec(const CPStreamConfig& config) {
if (!config.codec_name.empty()) { if (!config.codec_name.empty()) {
@@ -48,6 +53,7 @@ struct CPStreamCore::Impl {
explicit Impl(CPStreamConfig config) explicit Impl(CPStreamConfig config)
: config_(std::move(config)) { : config_(std::move(config)) {
normalize_config(); normalize_config();
frame_writer_ = std::make_unique<persist::JsonWritter>();
} }
~Impl() = default; ~Impl() = default;
@@ -63,7 +69,7 @@ struct CPStreamCore::Impl {
config_.frame_queue_capacity = 1U; config_.frame_queue_capacity = 1U;
} }
if (config_.slave_request_interval.count() < 0) { 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; frame_queue_capacity_ = config_.frame_queue_capacity;
} }
@@ -115,7 +121,7 @@ struct CPStreamCore::Impl {
serial->flush(); serial->flush();
{ {
std::lock_guard lock(serial_mutex_); std::lock_guard<std::mutex> lock(serial_mutex_);
serial_ = std::move(serial); serial_ = std::move(serial);
} }
} catch (const serial::IOException& ex) { } 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(); packet_queue_.clear();
} }
{ {
std::lock_guard lock(frame_mutex_); std::lock_guard<std::mutex> lock(frame_mutex_);
frame_queue_.clear(); frame_queue_.clear();
} }
pts_counter_.store(0, std::memory_order_relaxed); pts_counter_.store(0, std::memory_order_relaxed);
@@ -164,7 +170,7 @@ struct CPStreamCore::Impl {
stop(); stop();
{ {
std::lock_guard lock(serial_mutex_); std::lock_guard<std::mutex> lock(serial_mutex_);
if (serial_) { if (serial_) {
try { try {
if (serial_->isOpen()) { 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(); packet_queue_.clear();
} }
{ {
std::lock_guard lock(frame_mutex_); std::lock_guard<std::mutex> lock(frame_mutex_);
frame_queue_.clear(); frame_queue_.clear();
frame_record_queue_.clear();
} }
} }
@@ -200,7 +207,7 @@ struct CPStreamCore::Impl {
std::shared_ptr<serial::Serial> serial_copy; std::shared_ptr<serial::Serial> serial_copy;
{ {
std::lock_guard lock(serial_mutex_); std::lock_guard<std::mutex> lock(serial_mutex_);
serial_copy = serial_; serial_copy = serial_;
} }
if (!serial_copy || !serial_copy->isOpen()) { if (!serial_copy || !serial_copy->isOpen()) {
@@ -249,7 +256,7 @@ struct CPStreamCore::Impl {
stop_requested_.store(false, std::memory_order_release); 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(); packet_queue_.clear();
} }
@@ -259,7 +266,7 @@ struct CPStreamCore::Impl {
} }
bool is_open() const { bool is_open() const {
std::lock_guard lock(serial_mutex_); std::lock_guard<std::mutex> lock(serial_mutex_);
return serial_ && serial_->isOpen(); return serial_ && serial_->isOpen();
} }
@@ -278,7 +285,7 @@ struct CPStreamCore::Impl {
std::shared_ptr<serial::Serial> serial_copy; std::shared_ptr<serial::Serial> serial_copy;
{ {
std::lock_guard lock(serial_mutex_); std::lock_guard<std::mutex> lock(serial_mutex_);
serial_copy = serial_; serial_copy = serial_;
} }
@@ -300,17 +307,17 @@ struct CPStreamCore::Impl {
return false; return false;
} }
std::optional<DecodedFrame> try_pop_frame() { std::optional<std::shared_ptr<DecodedFrame>> try_pop_frame() {
std::lock_guard lock(frame_mutex_); std::lock_guard<std::mutex> lock(frame_mutex_);
if (frame_queue_.empty()) { if (frame_queue_.empty()) {
return std::nullopt; return std::nullopt;
} }
DecodedFrame frame = std::move(frame_queue_.front()); std::shared_ptr<DecodedFrame> frame = std::move(frame_queue_.front());
frame_queue_.pop_front(); frame_queue_.pop_front();
return frame; 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_); std::unique_lock lock(frame_mutex_);
if (!frame_cv_.wait_for(lock, timeout, [&] { if (!frame_cv_.wait_for(lock, timeout, [&] {
return !frame_queue_.empty(); return !frame_queue_.empty();
@@ -323,7 +330,7 @@ struct CPStreamCore::Impl {
} }
void clear_frames() { void clear_frames() {
std::lock_guard lock(frame_mutex_); std::lock_guard<std::mutex> lock(frame_mutex_);
frame_queue_.clear(); frame_queue_.clear();
} }
@@ -332,7 +339,7 @@ struct CPStreamCore::Impl {
capacity = 1U; capacity = 1U;
} }
{ {
std::lock_guard lock(frame_mutex_); std::lock_guard<std::mutex> lock(frame_mutex_);
frame_queue_capacity_ = capacity; frame_queue_capacity_ = capacity;
config_.frame_queue_capacity = capacity; config_.frame_queue_capacity = capacity;
while (frame_queue_.size() > frame_queue_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) { 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); frame_callback_ = std::move(callback);
} }
@@ -351,7 +396,7 @@ struct CPStreamCore::Impl {
} }
std::string last_error() const { std::string last_error() const {
std::lock_guard lock(last_error_mutex_); std::lock_guard<std::mutex> lock(last_error_mutex_);
return last_error_; return last_error_;
} }
@@ -365,7 +410,7 @@ struct CPStreamCore::Impl {
while (!stop_requested_.load(std::memory_order_acquire)) { while (!stop_requested_.load(std::memory_order_acquire)) {
std::shared_ptr<serial::Serial> serial_copy; std::shared_ptr<serial::Serial> serial_copy;
{ {
std::lock_guard lock(serial_mutex_); std::lock_guard<std::mutex> lock(serial_mutex_);
serial_copy = serial_; serial_copy = serial_;
} }
if (!serial_copy || !serial_copy->isOpen()) { if (!serial_copy || !serial_copy->isOpen()) {
@@ -400,7 +445,7 @@ struct CPStreamCore::Impl {
packet.pts = pts_counter_.fetch_add(1, std::memory_order_relaxed); 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) { if (packet_queue_.size() >= config_.packet_queue_capacity) {
packet_queue_.pop_front(); packet_queue_.pop_front();
} }
@@ -414,7 +459,7 @@ struct CPStreamCore::Impl {
const auto command = config_.slave_request_command; const auto command = config_.slave_request_command;
auto interval = config_.slave_request_interval; auto interval = config_.slave_request_interval;
if (interval.count() < 0) { if (interval.count() < 0) {
interval = std::chrono::milliseconds{0}; interval = 0ms;
} }
const bool repeat = interval.count() > 0; const bool repeat = interval.count() > 0;
@@ -484,23 +529,24 @@ struct CPStreamCore::Impl {
CPFrame frame; CPFrame frame;
rc = cpcodec_receive_frame(codec_ctx_, &frame); rc = cpcodec_receive_frame(codec_ctx_, &frame);
if (rc == CP_SUCCESS) { if (rc == CP_SUCCESS) {
DecodedFrame decoded; auto decoded = std::make_shared<DecodedFrame>();
decoded.pts = frame.pts; decoded->pts = frame.pts;
decoded.received_at = std::chrono::steady_clock::now(); decoded->received_at = std::chrono::steady_clock::now();
decoded.frame = std::move(frame); decoded->frame = std::move(frame);
if (codec_descriptor_ && codec_descriptor_->id == CPCodecID::Tactile) { decoded->id = codec_descriptor_ ? codec_descriptor_->id : CPCodecID::Unknow;
if (auto parsed = tactile::parse_frame(decoded.frame)) { if (decoded->id == CPCodecID::Tactile) {
decoded.tactile = parsed; if (auto parsed = tactile::parse_frame(decoded->frame)) {
decoded.tactile_pressures = tactile::parse_pressure_values(*parsed); decoded->tactile = parsed;
decoded->tactile_pressures = tactile::parse_pressure_values(*parsed);
if (auto matrix = tactile::parse_matrix_size_payload(*parsed)) { if (auto matrix = tactile::parse_matrix_size_payload(*parsed)) {
decoded.tactile_matrix_size = matrix; decoded->tactile_matrix_size = matrix;
} }
} }
} }
FrameCallback callback_copy; FrameCallback callback_copy;
{ {
std::lock_guard lock(callback_mutex_); std::lock_guard<std::mutex> lock(callback_mutex_);
callback_copy = frame_callback_; callback_copy = frame_callback_;
} }
if (callback_copy) { 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_) { if (frame_queue_.size() >= frame_queue_capacity_) {
frame_queue_.pop_front(); 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(); frame_cv_.notify_one();
} else if (rc == CP_ERROR_EAGAIN) { } else if (rc == CP_ERROR_EAGAIN) {
@@ -536,7 +585,7 @@ struct CPStreamCore::Impl {
packet.flush = true; packet.flush = true;
packet.end_of_stream = end_of_stream; 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_queue_.push_back(std::move(packet));
} }
packet_cv_.notify_one(); packet_cv_.notify_one();
@@ -554,7 +603,7 @@ struct CPStreamCore::Impl {
} }
void set_last_error(std::string message) { 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); last_error_ = std::move(message);
} }
@@ -574,9 +623,12 @@ struct CPStreamCore::Impl {
std::condition_variable packet_cv_; std::condition_variable packet_cv_;
std::deque<Packet> packet_queue_; std::deque<Packet> packet_queue_;
std::mutex frame_mutex_; mutable std::mutex frame_mutex_;
std::condition_variable frame_cv_; 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; std::size_t frame_queue_capacity_ = 16;
FrameCallback frame_callback_; FrameCallback frame_callback_;
@@ -588,6 +640,8 @@ struct CPStreamCore::Impl {
std::string last_error_; std::string last_error_;
mutable std::mutex last_error_mutex_; mutable std::mutex last_error_mutex_;
std::unique_ptr<persist::JsonWritter> frame_writer_;
}; };
CPStreamCore::CPStreamCore(CPStreamConfig config) 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); 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(); 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); 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); 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) { void CPStreamCore::set_frame_callback(FrameCallback callback) {
impl_->set_frame_callback(std::move(callback)); impl_->set_frame_callback(std::move(callback));
} }

View File

@@ -5,16 +5,23 @@
#include <chrono> #include <chrono>
#include <cstdint> #include <cstdint>
#include <functional> #include <functional>
#include <future>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <serial/serial.h> #include <serial/serial.h>
#include <string> #include <string>
#include <vector> #include <vector>
using namespace std::chrono_literals;
namespace ffmsep { namespace ffmsep {
namespace persist {
struct WriteResult;
} // namespace persist
struct DecodedFrame { struct DecodedFrame {
CPFrame frame; CPFrame frame;
CPCodecID id = CPCodecID::Unknow;
std::chrono::steady_clock::time_point received_at{}; std::chrono::steady_clock::time_point received_at{};
std::int64_t pts = 0; std::int64_t pts = 0;
std::optional<tactile::TactileFrame> tactile; std::optional<tactile::TactileFrame> tactile;
@@ -36,12 +43,12 @@ struct CPStreamConfig {
CPCodecID codec_id = CPCodecID::Unknow; CPCodecID codec_id = CPCodecID::Unknow;
std::string codec_name; std::string codec_name;
std::vector<std::uint8_t> slave_request_command{}; std::vector<std::uint8_t> slave_request_command{};
std::chrono::milliseconds slave_request_interval{200}; std::chrono::milliseconds slave_request_interval{200ms};
}; };
class CPStreamCore { class CPStreamCore {
public: public:
using FrameCallback = std::function<void(const DecodedFrame&)>; using FrameCallback = std::function<void(std::shared_ptr<DecodedFrame>)>;
explicit CPStreamCore(CPStreamConfig config = {}); explicit CPStreamCore(CPStreamConfig config = {});
~CPStreamCore(); ~CPStreamCore();
@@ -63,10 +70,14 @@ public:
bool send(const std::vector<std::uint8_t>& data); bool send(const std::vector<std::uint8_t>& data);
bool send(const std::uint8_t* data, std::size_t size); bool send(const std::uint8_t* data, std::size_t size);
std::optional<DecodedFrame> try_pop_frame(); std::optional<std::shared_ptr<DecodedFrame>> try_pop_frame();
bool wait_for_frame(DecodedFrame& frame, std::chrono::milliseconds timeout); bool wait_for_frame(std::shared_ptr<DecodedFrame>& frame, std::chrono::milliseconds timeout);
void clear_frames(); void clear_frames();
void set_frame_queue_capacity(std::size_t capacity); 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); void set_frame_callback(FrameCallback callback);
@@ -75,6 +86,8 @@ public:
static std::vector<serial::PortInfo> list_available_ports(); static std::vector<serial::PortInfo> list_available_ports();
private: private:
struct Impl; struct Impl;
std::unique_ptr<Impl> impl_; std::unique_ptr<Impl> impl_;

View 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

View 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

View File

@@ -43,6 +43,7 @@
#include <modern-qt/widget/select.hh> #include <modern-qt/widget/select.hh>
#include "components/ffmsep/tactile/tacdec.hh" #include "components/ffmsep/tactile/tacdec.hh"
#define DEBUG 0
using namespace creeper; using namespace creeper;
namespace capro = card::pro; namespace capro = card::pro;
@@ -112,17 +113,23 @@ public:
[&](const serial::PortInfo& info) { return info.port == port_utf8; }); [&](const serial::PortInfo& info) { return info.port == port_utf8; });
if (it == ports.end()) { if (it == ports.end()) {
if (ports.empty()) { if (ports.empty()) {
#if DEBUG
std::cerr << "SensorStreamController: requested port '" << port_utf8 << "' not available and no other ports detected.\n"; std::cerr << "SensorStreamController: requested port '" << port_utf8 << "' not available and no other ports detected.\n";
#endif
last_error_ = QString::fromUtf8("未检测到串口"); last_error_ = QString::fromUtf8("未检测到串口");
return false; return false;
} }
#if DEBUG
std::cerr << "SensorStreamController: requested port '" << port_utf8 << "' not available, falling back to first detected port.\n"; std::cerr << "SensorStreamController: requested port '" << port_utf8 << "' not available, falling back to first detected port.\n";
#endif
port_utf8 = ports.front().port; port_utf8 = ports.front().port;
} }
} else if (!ports.empty()) { } else if (!ports.empty()) {
port_utf8 = ports.front().port; port_utf8 = ports.front().port;
} else { } else {
#if DEBUG
std::cerr << "SensorStreamController: no serial ports available\n"; std::cerr << "SensorStreamController: no serial ports available\n";
#endif
last_error_ = QString::fromUtf8("未检测到串口"); last_error_ = QString::fromUtf8("未检测到串口");
return false; return false;
} }
@@ -149,7 +156,7 @@ public:
return false; 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); handle_frame(frame);
}); });
@@ -214,17 +221,17 @@ private:
}; };
} }
void handle_frame(const ffmsep::DecodedFrame& frame) { void handle_frame(std::shared_ptr<ffmsep::DecodedFrame> frame) {
if (!frame.tactile || frame.tactile_pressures.empty()) { if (!frame->tactile || frame->tactile_pressures.empty()) {
return; return;
} }
auto pressures = frame.tactile_pressures; auto pressures = frame->tactile_pressures;
auto size_hint = frame.tactile_matrix_size; auto size_hint = frame->tactile_matrix_size;
auto frame_bytes = frame.frame.data; auto frame_bytes = frame->frame.data;
std::vector<std::uint8_t> raw_payload; std::vector<std::uint8_t> raw_payload;
if (frame.tactile) { if (frame->tactile) {
raw_payload = frame.tactile->payload; raw_payload = frame->tactile->payload;
} }
QMetaObject::invokeMethod( QMetaObject::invokeMethod(

View File

@@ -3,6 +3,7 @@
#include <csignal> #include <csignal>
#include <iomanip> #include <iomanip>
#include <iostream> #include <iostream>
#include <memory>
#include <optional> #include <optional>
#include <string> #include <string>
#include <thread> #include <thread>
@@ -115,40 +116,40 @@ int main(int argc, char** argv) {
// Also demonstrate polling API (in case users don't want callbacks) // Also demonstrate polling API (in case users don't want callbacks)
while (g_running) { while (g_running) {
ffmsep::DecodedFrame df; std::shared_ptr<ffmsep::DecodedFrame> df;
if (core.wait_for_frame(df, 200ms)) { if (core.wait_for_frame(df, 200ms) && df) {
std::cout << "Frame pts=" << df.pts std::cout << "Frame pts=" << df->pts
<< " bytes=" << df.frame.data.size(); << " bytes=" << df->frame.data.size();
if (df.tactile) { if (df->tactile) {
const auto& tf = *df.tactile; const auto& tf = *df->tactile;
std::cout << " addr=" << int(tf.device_address) std::cout << " addr=" << int(tf.device_address)
<< " func=0x" << std::hex << std::uppercase << int(tf.response_function) << " func=0x" << std::hex << std::uppercase << int(tf.response_function)
<< std::dec; << std::dec;
if (df.tactile_matrix_size) { if (df->tactile_matrix_size) {
const auto& ms = *df.tactile_matrix_size; const auto& ms = *df->tactile_matrix_size;
std::cout << " matrix=" << int(ms.long_edge) std::cout << " matrix=" << int(ms.long_edge)
<< "x" << int(ms.short_edge); << "x" << int(ms.short_edge);
} }
if (!df.tactile_pressures.empty()) { if (!df->tactile_pressures.empty()) {
std::cout << " pressures=" << df.tactile_pressures.size() std::cout << " pressures=" << df->tactile_pressures.size()
<< " values=["; << " values=[";
const std::size_t preview = std::min<std::size_t>(df.tactile_pressures.size(), 8); const std::size_t preview = std::min<std::size_t>(df->tactile_pressures.size(), 8);
for (std::size_t idx = 0; idx < preview; ++idx) { for (std::size_t idx = 0; idx < preview; ++idx) {
if (idx != 0U) { if (idx != 0U) {
std::cout << ", "; std::cout << ", ";
} }
std::cout << df.tactile_pressures[idx]; std::cout << df->tactile_pressures[idx];
} }
if (preview < df.tactile_pressures.size()) { if (preview < df->tactile_pressures.size()) {
std::cout << ", ..."; std::cout << ", ...";
} }
std::cout << "]"; std::cout << "]";
} }
std::cout << "\n raw="; std::cout << "\n raw=";
print_hex(df.frame.data); print_hex(df->frame.data);
} else { } else {
std::cout << " raw="; std::cout << " raw=";
print_hex(df.frame.data); print_hex(df->frame.data);
} }
std::cout << "\n"; std::cout << "\n";
} }

View File

@@ -27,15 +27,12 @@ namespace material {
private: private:
static QString get_font_family(int fontId, const QString& fallback) { static QString get_font_family(int fontId, const QString& fallback) {
if (fontId == -1) { if (fontId == -1) {
qWarning() << "Failed to load font:" << fallback;
return fallback; return fallback;
} }
QStringList families = QFontDatabase::applicationFontFamilies(fontId); QStringList families = QFontDatabase::applicationFontFamilies(fontId);
if (families.isEmpty()) { if (families.isEmpty()) {
qWarning() << "No families found for font:" << fallback;
return fallback; return fallback;
} }
qDebug() << "families found for font:" << families;
return families.first(); return families.first();
} }
}; };