feat:heapmap with value;fix:qcustomplot warning
This commit is contained in:
@@ -71,11 +71,13 @@ file(
|
||||
set(FFMSEP_SOURCES
|
||||
components/ffmsep/cpdecoder.cc
|
||||
components/ffmsep/cpstream_core.cc
|
||||
components/ffmsep/presist/presist.cc
|
||||
components/ffmsep/tactile/tacdec.cc
|
||||
)
|
||||
set(FFMSEP_HEADERS
|
||||
components/ffmsep/cpdecoder.hh
|
||||
components/ffmsep/cpstream_core.hh
|
||||
components/ffmsep/presist/presist.hh
|
||||
components/ffmsep/tactile/tacdec.hh
|
||||
)
|
||||
set(FFMSEP_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/components/ffmsep")
|
||||
|
||||
@@ -8,9 +8,13 @@
|
||||
#include "heatmap.hh"
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/widget/sliders.hh"
|
||||
#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 {
|
||||
@@ -52,9 +56,8 @@ 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));
|
||||
if (initialized && color_map) {
|
||||
color_map->setDataRange(QCPRange(min, max));
|
||||
self.replot();
|
||||
}
|
||||
color_min = min;
|
||||
@@ -63,7 +66,7 @@ public:
|
||||
|
||||
auto set_data(const QVector<PointData>& data) -> void {
|
||||
data_points = data;
|
||||
if (initialized) {
|
||||
if (initialized && color_map) {
|
||||
update_plot_data();
|
||||
}
|
||||
}
|
||||
@@ -71,7 +74,8 @@ public:
|
||||
auto initialize_plot() -> void {
|
||||
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()->setRange(QCPRange(0.5, matrix_size.width() - 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 (!ylabel.isEmpty()) self.yAxis->setLabel(ylabel);
|
||||
|
||||
QCPColorScale* color_scale = new QCPColorScale(&self);
|
||||
color_scale->setType(QCPAxis::atBottom);
|
||||
self.plotLayout()->addElement(1, 0, color_scale);
|
||||
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(246, 239, 166)); // F6EFA6
|
||||
gradient.setColorStopAt(1.0, QColor(191, 68, 76)); // BF444C
|
||||
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));
|
||||
@@ -133,24 +149,37 @@ QCPColorMap* cpmp = new QCPColorMap(self.xAxis, self.yAxis);
|
||||
self.clearGraphs();
|
||||
self.clearItems();
|
||||
self.clearFocus();
|
||||
color_map = nullptr;
|
||||
cell_labels.clear();
|
||||
|
||||
// 重新初始化
|
||||
initialized = false;
|
||||
initialize_plot();
|
||||
}
|
||||
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) {
|
||||
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);
|
||||
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();
|
||||
}
|
||||
@@ -172,6 +201,82 @@ private:
|
||||
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
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -214,17 +221,17 @@ 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;
|
||||
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;
|
||||
if (frame->tactile) {
|
||||
raw_payload = frame->tactile->payload;
|
||||
}
|
||||
|
||||
QMetaObject::invokeMethod(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <csignal>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
@@ -115,40 +116,40 @@ int main(int argc, char** argv) {
|
||||
|
||||
// Also demonstrate polling API (in case users don't want callbacks)
|
||||
while (g_running) {
|
||||
ffmsep::DecodedFrame df;
|
||||
if (core.wait_for_frame(df, 200ms)) {
|
||||
std::cout << "Frame pts=" << df.pts
|
||||
<< " bytes=" << df.frame.data.size();
|
||||
if (df.tactile) {
|
||||
const auto& tf = *df.tactile;
|
||||
std::shared_ptr<ffmsep::DecodedFrame> df;
|
||||
if (core.wait_for_frame(df, 200ms) && df) {
|
||||
std::cout << "Frame pts=" << df->pts
|
||||
<< " bytes=" << df->frame.data.size();
|
||||
if (df->tactile) {
|
||||
const auto& tf = *df->tactile;
|
||||
std::cout << " addr=" << int(tf.device_address)
|
||||
<< " func=0x" << std::hex << std::uppercase << int(tf.response_function)
|
||||
<< std::dec;
|
||||
if (df.tactile_matrix_size) {
|
||||
const auto& ms = *df.tactile_matrix_size;
|
||||
if (df->tactile_matrix_size) {
|
||||
const auto& ms = *df->tactile_matrix_size;
|
||||
std::cout << " matrix=" << int(ms.long_edge)
|
||||
<< "x" << int(ms.short_edge);
|
||||
}
|
||||
if (!df.tactile_pressures.empty()) {
|
||||
std::cout << " pressures=" << df.tactile_pressures.size()
|
||||
if (!df->tactile_pressures.empty()) {
|
||||
std::cout << " pressures=" << df->tactile_pressures.size()
|
||||
<< " 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) {
|
||||
if (idx != 0U) {
|
||||
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 << "\n raw=";
|
||||
print_hex(df.frame.data);
|
||||
print_hex(df->frame.data);
|
||||
} else {
|
||||
std::cout << " raw=";
|
||||
print_hex(df.frame.data);
|
||||
print_hex(df->frame.data);
|
||||
}
|
||||
std::cout << "\n";
|
||||
}
|
||||
|
||||
@@ -27,15 +27,12 @@ namespace material {
|
||||
private:
|
||||
static QString get_font_family(int fontId, const QString& fallback) {
|
||||
if (fontId == -1) {
|
||||
qWarning() << "Failed to load font:" << fallback;
|
||||
return fallback;
|
||||
}
|
||||
QStringList families = QFontDatabase::applicationFontFamilies(fontId);
|
||||
if (families.isEmpty()) {
|
||||
qWarning() << "No families found for font:" << fallback;
|
||||
return fallback;
|
||||
}
|
||||
qDebug() << "families found for font:" << families;
|
||||
return families.first();
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user