feat:heapmap with value;fix:qcustomplot warning
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
@@ -8,13 +8,17 @@
|
|||||||
#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 <memory>
|
#include "qcustomplot/qcustomplot.h"
|
||||||
#include <qcolor.h>
|
#include <algorithm>
|
||||||
#include <qdebug.h>
|
#include <memory>
|
||||||
using namespace creeper::plot_widget::internal;
|
#include <qcolor.h>
|
||||||
|
#include <qdebug.h>
|
||||||
struct BasicPlot::Impl {
|
#include <qfont.h>
|
||||||
explicit Impl(BasicPlot& self) noexcept : self{self}, initialized(false), matrix_size(QSize{3, 4}) {}
|
#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:
|
public:
|
||||||
auto set_xlabel_text(const QString& text) -> void {
|
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 {
|
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;
|
color_max = max;
|
||||||
color_max = max;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
cpmp->data()->setSize(matrix_size.width(), matrix_size.height());
|
auto* cpmp = color_map;
|
||||||
cpmp->data()->setRange(QCPRange(0.5, matrix_size.width() - 0.5),
|
cpmp->data()->setSize(matrix_size.width(), matrix_size.height());
|
||||||
QCPRange(0.5, matrix_size.height() - 0.5));
|
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> xticker(new QCPAxisTickerText);
|
||||||
QSharedPointer<QCPAxisTickerText> yticker(new QCPAxisTickerText);
|
QSharedPointer<QCPAxisTickerText> yticker(new QCPAxisTickerText);
|
||||||
xticker->setSubTickCount(1);
|
xticker->setSubTickCount(1);
|
||||||
@@ -103,23 +107,35 @@ 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->setType(QCPAxis::atBottom);
|
color_scale = new QCPColorScale(&self);
|
||||||
self.plotLayout()->addElement(1, 0, color_scale);
|
color_scale->setType(QCPAxis::atBottom);
|
||||||
cpmp->setColorScale(color_scale);
|
}
|
||||||
|
if (self.plotLayout()) {
|
||||||
QCPColorGradient gradient;
|
auto existing = self.plotLayout()->element(1, 0);
|
||||||
gradient.setColorStopAt(0.0, QColor(246, 239, 166)); // F6EFA6
|
if (existing && existing != color_scale) {
|
||||||
gradient.setColorStopAt(1.0, QColor(191, 68, 76)); // BF444C
|
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->setGradient(gradient);
|
||||||
|
|
||||||
cpmp->setDataRange(QCPRange(color_min, color_max));
|
cpmp->setDataRange(QCPRange(color_min, color_max));
|
||||||
cpmp->setInterpolate(false);
|
cpmp->setInterpolate(false);
|
||||||
|
|
||||||
QCPMarginGroup *margin_group = new QCPMarginGroup(&self);
|
QCPMarginGroup *margin_group = new QCPMarginGroup(&self);
|
||||||
self.axisRect()->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
|
self.axisRect()->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
|
||||||
color_scale->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
|
color_scale->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
|
||||||
|
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
if (!data_points.isEmpty()) {
|
if (!data_points.isEmpty()) {
|
||||||
@@ -127,33 +143,46 @@ QCPColorMap* cpmp = new QCPColorMap(self.xAxis, self.yAxis);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto reset_plot() -> void {
|
auto reset_plot() -> void {
|
||||||
// 清除所有绘图元素
|
// 清除所有绘图元素
|
||||||
self.clearPlottables();
|
self.clearPlottables();
|
||||||
self.clearGraphs();
|
self.clearGraphs();
|
||||||
self.clearItems();
|
self.clearItems();
|
||||||
self.clearFocus();
|
self.clearFocus();
|
||||||
|
color_map = nullptr;
|
||||||
// 重新初始化
|
cell_labels.clear();
|
||||||
initialized = false;
|
|
||||||
initialize_plot();
|
// 重新初始化
|
||||||
}
|
initialized = false;
|
||||||
auto update_plot_data() -> void {
|
initialize_plot();
|
||||||
if (!initialized || self.plottableCount() == 0) return;
|
}
|
||||||
|
auto update_plot_data() -> void {
|
||||||
auto* cpmp = static_cast<QCPColorMap*>(self.plottable(0));
|
if (!initialized || !color_map) return;
|
||||||
|
|
||||||
// 设置新数据
|
ensure_labels();
|
||||||
for (const auto& item : data_points) {
|
|
||||||
if (item.x >= 0 && item.x < matrix_size.width() &&
|
const int width = matrix_size.width();
|
||||||
item.y >= 0 && item.y < matrix_size.height()) {
|
const int height = matrix_size.height();
|
||||||
cpmp->data()->setCell(item.x, item.y, item.z);
|
const int expected = width * height;
|
||||||
}
|
std::vector<double> values(static_cast<std::size_t>(expected), 0.0);
|
||||||
}
|
|
||||||
|
// 设置新数据
|
||||||
// 重绘
|
for (const auto& item : data_points) {
|
||||||
self.replot();
|
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 {
|
auto is_plot_initialized() const -> bool {
|
||||||
return initialized;
|
return initialized;
|
||||||
@@ -167,11 +196,87 @@ private:
|
|||||||
QString xlabel;
|
QString xlabel;
|
||||||
QString ylabel;
|
QString ylabel;
|
||||||
QSize matrix_size;
|
QSize matrix_size;
|
||||||
QVector<PointData> data_points;
|
QVector<PointData> data_points;
|
||||||
double color_min = 0.0;
|
double color_min = 0.0;
|
||||||
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;
|
||||||
#endif // TOUCHSENSOR_HEATMAP_IMPL_HH
|
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/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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_;
|
||||||
|
|||||||
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.
|
// Created by Lenn on 2025/10/14.
|
||||||
//
|
//
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QMetaObject>
|
#include <QMetaObject>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QtCore/Qt>
|
#include <QtCore/Qt>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <qsize.h>
|
#include <qsize.h>
|
||||||
#include <qsizepolicy.h>
|
#include <qsizepolicy.h>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include "component.hh"
|
#include "component.hh"
|
||||||
#include "cpstream_core.hh"
|
#include "cpstream_core.hh"
|
||||||
#include "modern-qt/utility/theme/theme.hh"
|
#include "modern-qt/utility/theme/theme.hh"
|
||||||
@@ -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;
|
||||||
@@ -98,10 +99,10 @@ public:
|
|||||||
reset_core();
|
reset_core();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool start(const QString& requested_port, std::uint32_t baudrate) {
|
bool start(const QString& requested_port, std::uint32_t baudrate) {
|
||||||
if (is_connected()) {
|
if (is_connected()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto ports = ffmsep::CPStreamCore::list_available_ports();
|
const auto ports = ffmsep::CPStreamCore::list_available_ports();
|
||||||
std::string port_utf8;
|
std::string port_utf8;
|
||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -161,44 +168,44 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
active_port_ = QString::fromStdString(cfg.port);
|
active_port_ = QString::fromStdString(cfg.port);
|
||||||
last_error_.clear();
|
last_error_.clear();
|
||||||
connected_ = true;
|
connected_ = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
reset_core();
|
reset_core();
|
||||||
active_port_.clear();
|
active_port_.clear();
|
||||||
if (heatmap_data_ && matrix_context_) {
|
if (heatmap_data_ && matrix_context_) {
|
||||||
heatmap_data_->set(make_flat_points(matrix_context_->get()));
|
heatmap_data_->set(make_flat_points(matrix_context_->get()));
|
||||||
}
|
}
|
||||||
connected_ = false;
|
connected_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool is_running() const noexcept {
|
[[nodiscard]] bool is_running() const noexcept {
|
||||||
return core_ && core_->is_running();
|
return core_ && core_->is_running();
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool is_connected() const noexcept {
|
[[nodiscard]] bool is_connected() const noexcept {
|
||||||
return connected_;
|
return connected_;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] QString active_port() const {
|
[[nodiscard]] QString active_port() const {
|
||||||
return active_port_;
|
return active_port_;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] QString last_error() const {
|
[[nodiscard]] QString last_error() const {
|
||||||
return last_error_;
|
return last_error_;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void reset_core() {
|
void reset_core() {
|
||||||
connected_ = false;
|
connected_ = false;
|
||||||
if (!core_) {
|
if (!core_) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
core_->set_frame_callback({});
|
core_->set_frame_callback({});
|
||||||
if (core_->is_running()) {
|
if (core_->is_running()) {
|
||||||
core_->stop();
|
core_->stop();
|
||||||
}
|
}
|
||||||
if (core_->is_open()) {
|
if (core_->is_open()) {
|
||||||
@@ -214,51 +221,51 @@ 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(
|
||||||
this,
|
this,
|
||||||
[this,
|
[this,
|
||||||
pressures = std::move(pressures),
|
pressures = std::move(pressures),
|
||||||
size_hint,
|
size_hint,
|
||||||
frame_bytes = std::move(frame_bytes),
|
frame_bytes = std::move(frame_bytes),
|
||||||
raw_payload = std::move(raw_payload)]() {
|
raw_payload = std::move(raw_payload)]() {
|
||||||
const auto format_raw = [](const std::vector<std::uint8_t>& data) -> std::string {
|
const auto format_raw = [](const std::vector<std::uint8_t>& data) -> std::string {
|
||||||
if (data.empty()) {
|
if (data.empty()) {
|
||||||
return "[]";
|
return "[]";
|
||||||
}
|
}
|
||||||
std::ostringstream oss;
|
std::ostringstream oss;
|
||||||
oss << '[';
|
oss << '[';
|
||||||
oss << std::uppercase << std::setfill('0');
|
oss << std::uppercase << std::setfill('0');
|
||||||
for (std::size_t idx = 0; idx < data.size(); ++idx) {
|
for (std::size_t idx = 0; idx < data.size(); ++idx) {
|
||||||
if (idx != 0U) {
|
if (idx != 0U) {
|
||||||
oss << ' ';
|
oss << ' ';
|
||||||
}
|
}
|
||||||
oss << std::setw(2) << std::hex << static_cast<unsigned int>(data[idx]);
|
oss << std::setw(2) << std::hex << static_cast<unsigned int>(data[idx]);
|
||||||
}
|
}
|
||||||
oss << ']';
|
oss << ']';
|
||||||
return oss.str();
|
return oss.str();
|
||||||
};
|
};
|
||||||
|
|
||||||
std::cout << "[Sensor] frame=" << format_raw(frame_bytes);
|
std::cout << "[Sensor] frame=" << format_raw(frame_bytes);
|
||||||
std::cout << " payload=" << format_raw(raw_payload);
|
std::cout << " payload=" << format_raw(raw_payload);
|
||||||
std::cout << " received " << pressures.size() << " pressure values";
|
std::cout << " received " << pressures.size() << " pressure values";
|
||||||
if (size_hint) {
|
if (size_hint) {
|
||||||
std::cout << " matrix=" << int(size_hint->long_edge)
|
std::cout << " matrix=" << int(size_hint->long_edge)
|
||||||
<< "x" << int(size_hint->short_edge);
|
<< "x" << int(size_hint->short_edge);
|
||||||
}
|
}
|
||||||
const std::size_t preview = std::min<std::size_t>(pressures.size(), 12);
|
const std::size_t preview = std::min<std::size_t>(pressures.size(), 12);
|
||||||
if (preview > 0) {
|
if (preview > 0) {
|
||||||
std::cout << " values=[";
|
std::cout << " values=[";
|
||||||
for (std::size_t idx = 0; idx < preview; ++idx) {
|
for (std::size_t idx = 0; idx < preview; ++idx) {
|
||||||
@@ -305,107 +312,107 @@ private:
|
|||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] QSize normalize_matrix(QSize candidate, std::size_t value_count) const {
|
[[nodiscard]] QSize normalize_matrix(QSize candidate, std::size_t value_count) const {
|
||||||
if (value_count == 0U) {
|
if (value_count == 0U) {
|
||||||
return QSize{};
|
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};
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data_;
|
const auto adapt_from = [value_count](const QSize& hint) -> std::optional<QSize> {
|
||||||
std::shared_ptr<MutableValue<QSize>> matrix_context_;
|
if (hint.width() <= 0 && hint.height() <= 0) {
|
||||||
std::unique_ptr<ffmsep::CPStreamCore> core_;
|
return std::nullopt;
|
||||||
QString active_port_;
|
}
|
||||||
QString last_error_;
|
|
||||||
bool connected_ = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SensorUiState {
|
if (hint.width() > 0 && hint.height() > 0) {
|
||||||
std::shared_ptr<MutableValue<QString>> link_icon =
|
const auto cells = static_cast<std::size_t>(hint.width()) *
|
||||||
std::make_shared<MutableValue<QString>>(QString::fromLatin1(material::icon::kAddLink));
|
static_cast<std::size_t>(hint.height());
|
||||||
std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data =
|
if (cells == value_count) {
|
||||||
std::make_shared<MutableValue<QVector<PointData>>>();
|
return hint;
|
||||||
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() {
|
if (hint.width() > 0) {
|
||||||
const QSize size{3, 4};
|
const auto width = static_cast<std::size_t>(hint.width());
|
||||||
heatmap_matrix->set_silent(size);
|
if (width != 0U && (value_count % width) == 0U) {
|
||||||
heatmap_data->set_silent(make_flat_points(size));
|
const auto height = static_cast<int>(value_count / width);
|
||||||
|
return QSize{hint.width(), height};
|
||||||
// 初始化串口列表
|
}
|
||||||
QStringList ports_list;
|
}
|
||||||
const auto ports = ffmsep::CPStreamCore::list_available_ports();
|
|
||||||
ports_list.reserve(static_cast<qsizetype>(ports.size()));
|
if (hint.height() > 0) {
|
||||||
for (const auto& info : ports) {
|
const auto height = static_cast<std::size_t>(hint.height());
|
||||||
ports_list.emplace_back(QString::fromStdString(info.port));
|
if (height != 0U && (value_count % height) == 0U) {
|
||||||
}
|
const auto width = static_cast<int>(value_count / height);
|
||||||
port_items->set_silent(ports_list);
|
return QSize{width, hint.height()};
|
||||||
if (selected_port.isEmpty() && !ports_list.isEmpty()) {
|
}
|
||||||
selected_port = ports_list.front();
|
}
|
||||||
}
|
|
||||||
|
return std::nullopt;
|
||||||
controller = std::make_unique<SensorStreamController>(heatmap_data, heatmap_matrix);
|
};
|
||||||
}
|
|
||||||
};
|
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() {
|
SensorUiState& sensor_state() {
|
||||||
static SensorUiState state;
|
static SensorUiState state;
|
||||||
@@ -418,10 +425,10 @@ static auto ComConfigComponent(ThemeManager& manager) {
|
|||||||
auto& sensor = sensor_state();
|
auto& sensor = sensor_state();
|
||||||
auto link_icon_context = sensor.link_icon;
|
auto link_icon_context = sensor.link_icon;
|
||||||
|
|
||||||
// 串口下拉:改为绑定可变数据源,初始值由 SensorUiState 构造时填充
|
// 串口下拉:改为绑定可变数据源,初始值由 SensorUiState 构造时填充
|
||||||
if (sensor.selected_port.isEmpty() && !sensor.port_items->get().isEmpty()) {
|
if (sensor.selected_port.isEmpty() && !sensor.port_items->get().isEmpty()) {
|
||||||
sensor.selected_port = sensor.port_items->get().front();
|
sensor.selected_port = sensor.port_items->get().front();
|
||||||
}
|
}
|
||||||
|
|
||||||
const QStringList baud_items{
|
const QStringList baud_items{
|
||||||
QString::fromLatin1("9600"),
|
QString::fromLatin1("9600"),
|
||||||
@@ -440,21 +447,21 @@ static auto ComConfigComponent(ThemeManager& manager) {
|
|||||||
// slogen_context,
|
// slogen_context,
|
||||||
// },
|
// },
|
||||||
// },
|
// },
|
||||||
lnpro::Item<MatSelect> {
|
lnpro::Item<MatSelect> {
|
||||||
slpro::ThemeManager {manager},
|
slpro::ThemeManager {manager},
|
||||||
slpro::LeadingIcon {material::icon::kArrowDropDown, material::regular::font},
|
slpro::LeadingIcon {material::icon::kArrowDropDown, material::regular::font},
|
||||||
slpro::IndexChanged {[sensor_ptr = &sensor](auto& self){
|
slpro::IndexChanged {[sensor_ptr = &sensor](auto& self){
|
||||||
const auto text = self.currentText();
|
const auto text = self.currentText();
|
||||||
if (!text.isEmpty()) {
|
if (!text.isEmpty()) {
|
||||||
sensor_ptr->selected_port = text;
|
sensor_ptr->selected_port = text;
|
||||||
}
|
}
|
||||||
}},
|
}},
|
||||||
slpro::LeadingText {"COM"},
|
slpro::LeadingText {"COM"},
|
||||||
MutableForward {
|
MutableForward {
|
||||||
slpro::SelectItems {},
|
slpro::SelectItems {},
|
||||||
sensor.port_items,
|
sensor.port_items,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
lnpro::Item<MatSelect> {
|
lnpro::Item<MatSelect> {
|
||||||
slpro::ThemeManager {manager },
|
slpro::ThemeManager {manager },
|
||||||
slpro::LeadingIcon { material::icon::kArrowDropDown, material::regular::font},
|
slpro::LeadingIcon { material::icon::kArrowDropDown, material::regular::font},
|
||||||
@@ -484,10 +491,10 @@ static auto ComConfigComponent(ThemeManager& manager) {
|
|||||||
if (!sensor.controller) {
|
if (!sensor.controller) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (sensor.controller->is_connected()) {
|
if (sensor.controller->is_connected()) {
|
||||||
sensor.controller->stop();
|
sensor.controller->stop();
|
||||||
link_icon_context->set(QString::fromLatin1(material::icon::kAddLink));
|
link_icon_context->set(QString::fromLatin1(material::icon::kAddLink));
|
||||||
} else {
|
} else {
|
||||||
const auto port = sensor.selected_port;
|
const auto port = sensor.selected_port;
|
||||||
const auto baud = sensor.selected_baud == 0U ? 115200U : sensor.selected_baud;
|
const auto baud = sensor.selected_baud == 0U ? 115200U : sensor.selected_baud;
|
||||||
if (sensor.controller->start(port, baud)) {
|
if (sensor.controller->start(port, baud)) {
|
||||||
@@ -501,34 +508,34 @@ static auto ComConfigComponent(ThemeManager& manager) {
|
|||||||
}
|
}
|
||||||
} }
|
} }
|
||||||
},
|
},
|
||||||
lnpro::Item<IconButton> {
|
lnpro::Item<IconButton> {
|
||||||
ibpro::ThemeManager { manager },
|
ibpro::ThemeManager { manager },
|
||||||
ibpro::FixedSize { 40, 40 },
|
ibpro::FixedSize { 40, 40 },
|
||||||
ibpro::Color { IconButton::Color::TONAL },
|
ibpro::Color { IconButton::Color::TONAL },
|
||||||
ibpro::Font { material::kRoundSmallFont },
|
ibpro::Font { material::kRoundSmallFont },
|
||||||
ibpro::FontIcon { material::icon::kRefresh },
|
ibpro::FontIcon { material::icon::kRefresh },
|
||||||
ibpro::Clickable {[&sensor] {
|
ibpro::Clickable {[&sensor] {
|
||||||
// 刷新串口列表
|
// 刷新串口列表
|
||||||
QStringList ports_list;
|
QStringList ports_list;
|
||||||
const auto ports = ffmsep::CPStreamCore::list_available_ports();
|
const auto ports = ffmsep::CPStreamCore::list_available_ports();
|
||||||
ports_list.reserve(static_cast<qsizetype>(ports.size()));
|
ports_list.reserve(static_cast<qsizetype>(ports.size()));
|
||||||
for (const auto& info : ports) {
|
for (const auto& info : ports) {
|
||||||
ports_list.emplace_back(QString::fromStdString(info.port));
|
ports_list.emplace_back(QString::fromStdString(info.port));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保持原选择(若仍然存在)
|
// 保持原选择(若仍然存在)
|
||||||
if (!sensor.selected_port.isEmpty()) {
|
if (!sensor.selected_port.isEmpty()) {
|
||||||
const bool exists = ports_list.contains(sensor.selected_port);
|
const bool exists = ports_list.contains(sensor.selected_port);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
sensor.selected_port = ports_list.isEmpty() ? QString{} : ports_list.front();
|
sensor.selected_port = ports_list.isEmpty() ? QString{} : ports_list.front();
|
||||||
}
|
}
|
||||||
} else if (!ports_list.isEmpty()) {
|
} else if (!ports_list.isEmpty()) {
|
||||||
sensor.selected_port = ports_list.front();
|
sensor.selected_port = ports_list.front();
|
||||||
}
|
}
|
||||||
|
|
||||||
sensor.port_items->set(std::move(ports_list));
|
sensor.port_items->set(std::move(ports_list));
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
return new Widget {
|
return new Widget {
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user