feat:data slove and update heatmap

This commit is contained in:
2025-10-29 14:09:28 +08:00
parent c50b44efe2
commit c6cef3d89d
200 changed files with 100674 additions and 52814 deletions

View File

@@ -1,131 +1,131 @@
//
// Created by Lenn on 2025/10/17.
//
#ifndef TOUCHSENSOR_HEATMAP_H
#define TOUCHSENSOR_HEATMAP_H
#include "modern-qt/utility/theme/theme.hh"
#include "modern-qt/utility/wrapper/common.hh"
#include "modern-qt/utility/wrapper/pimpl.hh"
#include "modern-qt/utility/wrapper/property.hh"
#include "qcustomplot/qcustomplot.h"
#include "modern-qt/utility/wrapper/widget.hh"
#include <concepts>
#include <qcontainerfwd.h>
#include <qvector.h>
struct point_data {
double x;
double y;
double z;
explicit point_data(double x, double y, double z) : x{x}, y{y}, z{z} {}
};
using PointData = struct point_data;
namespace creeper {
class HeatMapPlot;
namespace plot_widget::internal {
class BasicPlot : public QCustomPlot {
CREEPER_PIMPL_DEFINITION(BasicPlot)
friend class HeatMapPlot;
public:
// BasicPlot();
// ~BasicPlot();
// BasicPlot(const BasicPlot&) = delete;
// BasicPlot& operator=(const BasicPlot&) = delete;
void init_plot()const;
void load_theme_manager(ThemeManager&)const;
void set_xlabel_text(const QString&)const;
void set_ylabel_text(const QString&)const;
void set_matrix_size(const QSize&)const;
void set_matrix_size(const int& w, const int& h)const;
void set_data(const QVector<PointData>& data)const;
void set_color_gradient_range(const double& min, const double& max)const;
QSize get_matrix_size() const;
bool is_initialized() const;
public slots:
void update_dynamic_heatmap(const QVector<PointData>& map)const;
void dataChanged(const QVector<PointData>& map)const;
void dataRangeChanged(const double& min, const double& max)const;
protected:
void paintEvent(QPaintEvent*) override;
private:
friend struct Impl;
};
} // namespace plot_widget::internal
namespace plot_widget::pro {
using Token = common::Token<plot_widget::internal::BasicPlot>;
using XLabelText = common::pro::String<Token,
[](auto& self, const auto& string) {
self.set_xlabel_text(string);
}>;
using YLabelText = common::pro::String<Token,
[](auto& self, const auto& string) {
self.set_ylabel_text(string);
}>;
struct MatrixSize : Token {
QSize size;
explicit MatrixSize(const int& w, const int& h) : size{w, h} {}
explicit MatrixSize(const QSize& s) : size{s} {}
void apply(auto& self) const {
self.set_matrix_size(size);
}
};
using Data = common::pro::Vector<Token, PointData,
[](auto& self, const auto& data) {
self.set_data(data);
}>;
struct ColorRange : Token {
double min;
double max;
explicit ColorRange(double min, double max) : min{min}, max{max} {}
void apply(auto& self) const {
self.set_color_gradient_range(min, max);
}
};
template <typename F>
using OnDataChanged =
common::pro::SignalInjection<F, Token, &internal::BasicPlot::dataChanged>;
template <typename F>
using OnDataRangeChanged =
common::pro::SignalInjection<F, Token, &internal::BasicPlot::dataRangeChanged>;
template<class PlotWidget>
concept trait = std::derived_from<PlotWidget, Token>;
using PlotData = common::pro::Vector<Token, PointData, [](auto& self, const auto& vec) {
self.set_data(vec);
}>;
using DataRange = common::pro::Array<Token, int, 2, [](auto& self, const auto& arr) {
self.set_color_gradient_range(arr[0], arr[1]);
}>;
CREEPER_DEFINE_CHECKER(trait);
using namespace widget::pro;
using namespace theme::pro;
}
struct HeatMapPlot
: public Declarative<plot_widget::internal::BasicPlot,
CheckerOr<plot_widget::pro::checker, widget::pro::checker, theme::pro::checker>> {
using Declarative::Declarative;
void paintEvent(QPaintEvent*) override;
};
}
#endif // TOUCHSENSOR_HEATMAP_H
//
// Created by Lenn on 2025/10/17.
//
#ifndef TOUCHSENSOR_HEATMAP_H
#define TOUCHSENSOR_HEATMAP_H
#include "modern-qt/utility/theme/theme.hh"
#include "modern-qt/utility/wrapper/common.hh"
#include "modern-qt/utility/wrapper/pimpl.hh"
#include "modern-qt/utility/wrapper/property.hh"
#include "qcustomplot/qcustomplot.h"
#include "modern-qt/utility/wrapper/widget.hh"
#include <concepts>
#include <qcontainerfwd.h>
#include <qvector.h>
struct point_data {
double x;
double y;
double z;
explicit point_data(double x, double y, double z) : x{x}, y{y}, z{z} {}
};
using PointData = struct point_data;
namespace creeper {
class HeatMapPlot;
namespace plot_widget::internal {
class BasicPlot : public QCustomPlot {
CREEPER_PIMPL_DEFINITION(BasicPlot)
friend class HeatMapPlot;
public:
// BasicPlot();
// ~BasicPlot();
// BasicPlot(const BasicPlot&) = delete;
// BasicPlot& operator=(const BasicPlot&) = delete;
void init_plot()const;
void load_theme_manager(ThemeManager&)const;
void set_xlabel_text(const QString&)const;
void set_ylabel_text(const QString&)const;
void set_matrix_size(const QSize&)const;
void set_matrix_size(const int& w, const int& h)const;
void set_data(const QVector<PointData>& data)const;
void set_color_gradient_range(const double& min, const double& max)const;
QSize get_matrix_size() const;
bool is_initialized() const;
public slots:
void update_dynamic_heatmap(const QVector<PointData>& map)const;
void dataChanged(const QVector<PointData>& map)const;
void dataRangeChanged(const double& min, const double& max)const;
protected:
void paintEvent(QPaintEvent*) override;
private:
friend struct Impl;
};
} // namespace plot_widget::internal
namespace plot_widget::pro {
using Token = common::Token<plot_widget::internal::BasicPlot>;
using XLabelText = common::pro::String<Token,
[](auto& self, const auto& string) {
self.set_xlabel_text(string);
}>;
using YLabelText = common::pro::String<Token,
[](auto& self, const auto& string) {
self.set_ylabel_text(string);
}>;
struct MatrixSize : Token {
QSize size;
explicit MatrixSize(const int& w, const int& h) : size{w, h} {}
explicit MatrixSize(const QSize& s) : size{s} {}
void apply(auto& self) const {
self.set_matrix_size(size);
}
};
using Data = common::pro::Vector<Token, PointData,
[](auto& self, const auto& data) {
self.set_data(data);
}>;
struct ColorRange : Token {
double min;
double max;
explicit ColorRange(double min, double max) : min{min}, max{max} {}
void apply(auto& self) const {
self.set_color_gradient_range(min, max);
}
};
template <typename F>
using OnDataChanged =
common::pro::SignalInjection<F, Token, &internal::BasicPlot::dataChanged>;
template <typename F>
using OnDataRangeChanged =
common::pro::SignalInjection<F, Token, &internal::BasicPlot::dataRangeChanged>;
template<class PlotWidget>
concept trait = std::derived_from<PlotWidget, Token>;
using PlotData = common::pro::Vector<Token, PointData, [](auto& self, const auto& vec) {
self.set_data(vec);
}>;
using DataRange = common::pro::Array<Token, int, 2, [](auto& self, const auto& arr) {
self.set_color_gradient_range(arr[0], arr[1]);
}>;
CREEPER_DEFINE_CHECKER(trait);
using namespace widget::pro;
using namespace theme::pro;
}
struct HeatMapPlot
: public Declarative<plot_widget::internal::BasicPlot,
CheckerOr<plot_widget::pro::checker, widget::pro::checker, theme::pro::checker>> {
using Declarative::Declarative;
void paintEvent(QPaintEvent*) override;
};
}
#endif // TOUCHSENSOR_HEATMAP_H

View File

@@ -1,197 +1,177 @@
//
// Created by Lenn on 2025/10/17.
//
#ifndef TOUCHSENSOR_HEATMAP_IMPL_HH
#define TOUCHSENSOR_HEATMAP_IMPL_HH
#include "heatmap.hh"
#include "modern-qt/utility/theme/theme.hh"
#include "modern-qt/widget/sliders.hh"
#include <memory>
#include <qcolor.h>
#include <qdebug.h>
using namespace creeper::plot_widget::internal;
struct BasicPlot::Impl {
explicit Impl(BasicPlot& self) noexcept : self{self}, initialized(false), matrix_size(QSize{3, 4}) {}
public:
auto set_xlabel_text(const QString& text) -> void {
xlabel = text;
if (initialized) {
self.xAxis->setLabel(text);
self.replot();
}
}
auto set_ylabel_text(const QString& text) -> void {
ylabel = text;
if (initialized) {
self.yAxis->setLabel(text);
self.replot();
}
}
auto set_matrix_size(const QSize& size) -> void {
matrix_size = size;
if (initialized) {
// 重新初始化热力图以适应新的矩阵大小
reset_plot();
if (!data_points.isEmpty()) {
set_data(data_points);
}
}
}
auto load_theme_manager(ThemeManager& mgr) -> void {
mgr.append_handler(&self, [this](const ThemeManager& mgr) {
// 可以根据主题更新颜色渐变等
if (initialized) {
self.replot();
}
});
}
auto set_color_gradient_range(const double& min, const double& max) -> void {
if (initialized && self.plottableCount() > 0) {
auto* cpmp = static_cast<QCPColorMap*>(self.plottable(0));
cpmp->setDataRange(QCPRange(min, max));
self.replot();
}
color_min = min;
color_max = max;
}
auto set_data(const QVector<PointData>& data) -> void {
data_points = data;
if (initialized) {
update_plot_data();
}
}
auto initialize_plot() -> void {
if (initialized) return;
QCPColorMap* cpmp = new QCPColorMap(self.xAxis, self.yAxis);
cpmp->data()->setSize(matrix_size.width(), matrix_size.height());
cpmp->data()->setRange(QCPRange(0.5, matrix_size.width() - 0.5),
QCPRange(0.5, matrix_size.height() - 0.5));
// 配置坐标轴
QSharedPointer<QCPAxisTickerText> xticker(new QCPAxisTickerText);
QSharedPointer<QCPAxisTickerText> yticker(new QCPAxisTickerText);
xticker->setSubTickCount(1);
yticker->setSubTickCount(1);
self.xAxis->setVisible(false);
self.yAxis->setVisible(false);
self.xAxis->setTicker(xticker);
self.yAxis->setTicker(yticker);
// 设置网格
self.xAxis->grid()->setPen(Qt::NoPen);
self.yAxis->grid()->setPen(Qt::NoPen);
self.xAxis->grid()->setSubGridVisible(true);
self.yAxis->grid()->setSubGridVisible(true);
self.xAxis->setSubTicks(true);
self.yAxis->setSubTicks(true);
self.xAxis->setTickLength(0);
self.yAxis->setTickLength(0);
self.xAxis->setSubTickLength(6);
self.yAxis->setSubTickLength(6);
// 设置范围
self.xAxis->setRange(0, matrix_size.width());
self.yAxis->setRange(0, matrix_size.height());
// 设置标签
if (!xlabel.isEmpty()) self.xAxis->setLabel(xlabel);
if (!ylabel.isEmpty()) self.yAxis->setLabel(ylabel);
// 添加/复用颜色刻度(避免重复 addElement 到相同单元格导致告警)
QCPLayoutElement* occupied = self.plotLayout()->element(1, 0);
QCPColorScale* color_scale = occupied ? qobject_cast<QCPColorScale*>(occupied) : nullptr;
if (!color_scale) {
if (occupied) {
// 单元格被其他元素占用,移除并删除后再放入 ColorScale
self.plotLayout()->remove(occupied);
delete occupied;
occupied = nullptr;
}
color_scale = new QCPColorScale(&self);
color_scale->setType(QCPAxis::atBottom);
self.plotLayout()->addElement(1, 0, color_scale);
}
cpmp->setColorScale(color_scale);
// 设置颜色渐变
QCPColorGradient gradient;
gradient.setColorStopAt(0.0, QColor(246, 239, 166)); // F6EFA6
gradient.setColorStopAt(1.0, QColor(191, 68, 76)); // BF444C
cpmp->setGradient(gradient);
// 设置数据范围
cpmp->setDataRange(QCPRange(color_min, color_max));
cpmp->setInterpolate(false);
// 配置边距
QCPMarginGroup *margin_group = new QCPMarginGroup(&self);
self.axisRect()->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
color_scale->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
initialized = true;
// 如果已有数据,更新图表
if (!data_points.isEmpty()) {
update_plot_data();
}
}
auto reset_plot() -> void {
// 清除所有绘图元素
self.clearPlottables();
self.clearGraphs();
self.clearItems();
self.clearFocus();
// 重新初始化
initialized = false;
initialize_plot();
}
auto update_plot_data() -> void {
if (!initialized || self.plottableCount() == 0) return;
auto* cpmp = static_cast<QCPColorMap*>(self.plottable(0));
// 设置新数据
for (const auto& item : data_points) {
if (item.x >= 0 && item.x < matrix_size.width() &&
item.y >= 0 && item.y < matrix_size.height()) {
cpmp->data()->setCell(item.x, item.y, item.z);
}
}
// 重绘
self.replot();
}
auto is_plot_initialized() const -> bool {
return initialized;
}
auto get_matrix_size() const -> QSize {
return matrix_size;
}
private:
QString xlabel;
QString ylabel;
QSize matrix_size;
QVector<PointData> data_points;
double color_min = 0.0;
double color_max = 800.0;
bool initialized;
BasicPlot& self;
};
#endif // TOUCHSENSOR_HEATMAP_IMPL_HH
//
// Created by Lenn on 2025/10/17.
//
#ifndef TOUCHSENSOR_HEATMAP_IMPL_HH
#define TOUCHSENSOR_HEATMAP_IMPL_HH
#include "heatmap.hh"
#include "modern-qt/utility/theme/theme.hh"
#include "modern-qt/widget/sliders.hh"
#include <memory>
#include <qcolor.h>
#include <qdebug.h>
using namespace creeper::plot_widget::internal;
struct BasicPlot::Impl {
explicit Impl(BasicPlot& self) noexcept : self{self}, initialized(false), matrix_size(QSize{3, 4}) {}
public:
auto set_xlabel_text(const QString& text) -> void {
xlabel = text;
if (initialized) {
self.xAxis->setLabel(text);
self.replot();
}
}
auto set_ylabel_text(const QString& text) -> void {
ylabel = text;
if (initialized) {
self.yAxis->setLabel(text);
self.replot();
}
}
auto set_matrix_size(const QSize& size) -> void {
matrix_size = size;
if (initialized) {
reset_plot();
if (!data_points.isEmpty()) {
set_data(data_points);
}
}
}
auto load_theme_manager(ThemeManager& mgr) -> void {
mgr.append_handler(&self, [this](const ThemeManager& mgr) {
if (initialized) {
self.replot();
}
});
}
auto set_color_gradient_range(const double& min, const double& max) -> void {
if (initialized && self.plottableCount() > 0) {
auto* cpmp = static_cast<QCPColorMap*>(self.plottable(0));
cpmp->setDataRange(QCPRange(min, max));
self.replot();
}
color_min = min;
color_max = max;
}
auto set_data(const QVector<PointData>& data) -> void {
data_points = data;
if (initialized) {
update_plot_data();
}
}
auto initialize_plot() -> void {
if (initialized) return;
QCPColorMap* cpmp = new QCPColorMap(self.xAxis, self.yAxis);
cpmp->data()->setSize(matrix_size.width(), matrix_size.height());
cpmp->data()->setRange(QCPRange(0.5, matrix_size.width() - 0.5),
QCPRange(0.5, matrix_size.height() - 0.5));
QSharedPointer<QCPAxisTickerText> xticker(new QCPAxisTickerText);
QSharedPointer<QCPAxisTickerText> yticker(new QCPAxisTickerText);
xticker->setSubTickCount(1);
yticker->setSubTickCount(1);
self.xAxis->setVisible(false);
self.yAxis->setVisible(false);
self.xAxis->setTicker(xticker);
self.yAxis->setTicker(yticker);
self.xAxis->grid()->setPen(Qt::NoPen);
self.yAxis->grid()->setPen(Qt::NoPen);
self.xAxis->grid()->setSubGridVisible(true);
self.yAxis->grid()->setSubGridVisible(true);
self.xAxis->setSubTicks(true);
self.yAxis->setSubTicks(true);
self.xAxis->setTickLength(0);
self.yAxis->setTickLength(0);
self.xAxis->setSubTickLength(6);
self.yAxis->setSubTickLength(6);
self.xAxis->setRange(0, matrix_size.width());
self.yAxis->setRange(0, matrix_size.height());
if (!xlabel.isEmpty()) self.xAxis->setLabel(xlabel);
if (!ylabel.isEmpty()) self.yAxis->setLabel(ylabel);
QCPColorScale* color_scale = new QCPColorScale(&self);
color_scale->setType(QCPAxis::atBottom);
self.plotLayout()->addElement(1, 0, color_scale);
cpmp->setColorScale(color_scale);
QCPColorGradient gradient;
gradient.setColorStopAt(0.0, QColor(246, 239, 166)); // F6EFA6
gradient.setColorStopAt(1.0, QColor(191, 68, 76)); // BF444C
cpmp->setGradient(gradient);
cpmp->setDataRange(QCPRange(color_min, color_max));
cpmp->setInterpolate(false);
QCPMarginGroup *margin_group = new QCPMarginGroup(&self);
self.axisRect()->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
color_scale->setMarginGroup(QCP::msLeft | QCP::msRight, margin_group);
initialized = true;
if (!data_points.isEmpty()) {
update_plot_data();
}
}
auto reset_plot() -> void {
// 清除所有绘图元素
self.clearPlottables();
self.clearGraphs();
self.clearItems();
self.clearFocus();
// 重新初始化
initialized = false;
initialize_plot();
}
auto update_plot_data() -> void {
if (!initialized || self.plottableCount() == 0) return;
auto* cpmp = static_cast<QCPColorMap*>(self.plottable(0));
// 设置新数据
for (const auto& item : data_points) {
if (item.x >= 0 && item.x < matrix_size.width() &&
item.y >= 0 && item.y < matrix_size.height()) {
cpmp->data()->setCell(item.x, item.y, item.z);
}
}
// 重绘
self.replot();
}
auto is_plot_initialized() const -> bool {
return initialized;
}
auto get_matrix_size() const -> QSize {
return matrix_size;
}
private:
QString xlabel;
QString ylabel;
QSize matrix_size;
QVector<PointData> data_points;
double color_min = 0.0;
double color_max = 800.0;
bool initialized;
BasicPlot& self;
};
#endif // TOUCHSENSOR_HEATMAP_IMPL_HH

View File

@@ -1,125 +1,119 @@
#pragma once
#include "components/ffmsep/cpdecoder.hh"
#include <cstdint>
#include <cstddef>
#include <mutex>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include <initializer_list>
namespace ffmsep {
inline constexpr int CP_SUCCESS = 0;
inline constexpr int CP_ERROR_EOF = -1;
inline constexpr int CP_ERROR_EAGAIN = -2;
inline constexpr int CP_ERROR_NOT_OPEN = -3;
inline constexpr int CP_ERROR_INVALID_STATE = -4;
inline constexpr int CP_ERROR_INVALID_ARGUMENT = -5;
enum class CPMediaType : std::uint8_t {
Unknow = 0,
Data,
};
enum class CPCodecID : std::uint32_t {
Unknow = 0,
Tactile = 0x54514354u // 'T','Q','C','T':触觉传感器协议标识 Tactile Quick Codec Type
};
struct CPPacket {
std::vector<std::uint8_t> payload;
std::int64_t pts = 0;
std::int64_t dts = 0;
bool end_of_stream = false;
bool flush = false;
CPPacket() = default;
CPPacket(std::vector<std::uint8_t> data, std::int64_t pts_value = 0, std::int64_t dts_value = 0) noexcept
: payload(std::move(data)), pts(pts_value), dts(dts_value) {}
[[nodiscard]] bool empty() const noexcept {return payload.empty();}
};
struct CPFrame {
std::vector<std::uint8_t> data;
std::int64_t pts = 0;
bool key_frame = false;
bool valid = false;
void reset() noexcept {
data.clear();
key_frame = false;
valid = false;
pts = 0;
}
};
struct CPCodecContext;
struct CPCodec {
using InitFn = int(*)(CPCodecContext*);
using CloseFn = void(*)(CPCodecContext*);
using SendPacketFn = int(*)(CPCodecContext*, const CPPacket&);
using ReceiveFrameFn = int(*)(CPCodecContext*, CPFrame&);
const char* name = nullptr;
const char* long_name = nullptr;
CPMediaType type = CPMediaType::Unknow;
CPCodecID id = CPCodecID::Unknow;
std::size_t priv_data_size = 0;
InitFn init = nullptr;
CloseFn close = nullptr;
SendPacketFn send_packet = nullptr;
ReceiveFrameFn receive_frame = nullptr;
};
struct CPCodecContext {
const CPCodec* codec = nullptr;
void* priv_data = nullptr;
CPMediaType codec_type = CPMediaType::Unknow;
bool is_open = false;
void clear() noexcept {
codec = nullptr;
priv_data = nullptr;
codec_type = CPMediaType::Unknow;
is_open = false;
priv_storage.clear();
}
void* ensure_priv_storage(std::size_t size);
void release_priv_storage() noexcept;
template<typename T>
[[nodiscard]] T* priv_as() noexcept {
return static_cast<T*>(priv_data);
}
template<typename T>
[[nodiscard]] const T* priv_as() const noexcept {
return static_cast<const T*>(priv_data);
}
private:
std::vector<std::uint8_t> priv_storage;
friend CPCodecContext* cpcodec_alloc_context(const CPCodec*);
friend int cpcodec_open(CPCodecContext*, const CPCodec*);
friend int cpcodec_close(CPCodecContext*);
};
void cpcodec_register(const CPCodec* codec);
void cpcodec_register_many(std::initializer_list<const CPCodec*> codecs);
const CPCodec* cpcodec_find_decoder(CPCodecID id);
const CPCodec* cpcodec_find_decoder_by_name(std::string_view name);
std::vector<const CPCodec*> cpcodec_list_codecs();
CPCodecContext* cpcodec_alloc_context(const CPCodec* codec);
int cpcodec_open(CPCodecContext*, const CPCodec*);
int cpcodec_close(CPCodecContext*);
void cpcodec_free_context(CPCodecContext **ctx);
int cpcodec_send_packet(CPCodecContext*, const CPPacket*);
int cpcodec_receive_frame(CPCodecContext*, CPFrame*);
#pragma once
#include <cstdint>
#include <cstddef>
#include <string_view>
#include <vector>
#include <initializer_list>
namespace ffmsep {
inline constexpr int CP_SUCCESS = 0;
inline constexpr int CP_ERROR_EOF = -1;
inline constexpr int CP_ERROR_EAGAIN = -2;
inline constexpr int CP_ERROR_NOT_OPEN = -3;
inline constexpr int CP_ERROR_INVALID_STATE = -4;
inline constexpr int CP_ERROR_INVALID_ARGUMENT = -5;
enum class CPMediaType : std::uint8_t {
Unknow = 0,
Data,
};
enum class CPCodecID : std::uint32_t {
Unknow = 0,
Tactile = 0x54514354u // 'T','Q','C','T':触觉传感器协议标识 Tactile Quick Codec Type
};
struct CPPacket {
std::vector<std::uint8_t> payload;
std::int64_t pts = 0;
std::int64_t dts = 0;
bool end_of_stream = false;
bool flush = false;
CPPacket() = default;
CPPacket(std::vector<std::uint8_t> data, std::int64_t pts_value = 0, std::int64_t dts_value = 0) noexcept
: payload(std::move(data)), pts(pts_value), dts(dts_value) {}
[[nodiscard]] bool empty() const noexcept {return payload.empty();}
};
struct CPFrame {
std::vector<std::uint8_t> data;
std::int64_t pts = 0;
bool key_frame = false;
bool valid = false;
void reset() noexcept {
data.clear();
key_frame = false;
valid = false;
pts = 0;
}
};
struct CPCodecContext;
struct CPCodec {
using InitFn = int(*)(CPCodecContext*);
using CloseFn = void(*)(CPCodecContext*);
using SendPacketFn = int(*)(CPCodecContext*, const CPPacket&);
using ReceiveFrameFn = int(*)(CPCodecContext*, CPFrame&);
const char* name = nullptr;
const char* long_name = nullptr;
CPMediaType type = CPMediaType::Unknow;
CPCodecID id = CPCodecID::Unknow;
std::size_t priv_data_size = 0;
InitFn init = nullptr;
CloseFn close = nullptr;
SendPacketFn send_packet = nullptr;
ReceiveFrameFn receive_frame = nullptr;
};
struct CPCodecContext {
const CPCodec* codec = nullptr;
void* priv_data = nullptr;
CPMediaType codec_type = CPMediaType::Unknow;
bool is_open = false;
void clear() noexcept {
codec = nullptr;
priv_data = nullptr;
codec_type = CPMediaType::Unknow;
is_open = false;
priv_storage.clear();
}
void* ensure_priv_storage(std::size_t size);
void release_priv_storage() noexcept;
template<typename T>
[[nodiscard]] T* priv_as() noexcept {
return static_cast<T*>(priv_data);
}
template<typename T>
[[nodiscard]] const T* priv_as() const noexcept {
return static_cast<const T*>(priv_data);
}
private:
std::vector<std::uint8_t> priv_storage;
friend CPCodecContext* cpcodec_alloc_context(const CPCodec*);
friend int cpcodec_open(CPCodecContext*, const CPCodec*);
friend int cpcodec_close(CPCodecContext*);
};
void cpcodec_register(const CPCodec* codec);
void cpcodec_register_many(std::initializer_list<const CPCodec*> codecs);
const CPCodec* cpcodec_find_decoder(CPCodecID id);
const CPCodec* cpcodec_find_decoder_by_name(std::string_view name);
std::vector<const CPCodec*> cpcodec_list_codecs();
CPCodecContext* cpcodec_alloc_context(const CPCodec* codec);
int cpcodec_open(CPCodecContext*, const CPCodec*);
int cpcodec_close(CPCodecContext*);
void cpcodec_free_context(CPCodecContext **ctx);
int cpcodec_send_packet(CPCodecContext*, const CPPacket*);
int cpcodec_receive_frame(CPCodecContext*, CPFrame*);
}

View File

@@ -62,6 +62,9 @@ struct CPStreamCore::Impl {
if (config_.frame_queue_capacity == 0U) {
config_.frame_queue_capacity = 1U;
}
if (config_.slave_request_interval.count() < 0) {
config_.slave_request_interval = std::chrono::milliseconds{0};
}
frame_queue_capacity_ = config_.frame_queue_capacity;
}
@@ -106,7 +109,9 @@ struct CPStreamCore::Impl {
config_.parity,
config_.stopbits,
config_.flowcontrol);
serial->open();
if (!serial->isOpen()) {
serial->open();
}
serial->flush();
{
@@ -213,6 +218,9 @@ struct CPStreamCore::Impl {
reader_thread_ = std::thread(&Impl::reader_loop, this);
decoder_thread_ = std::thread(&Impl::decoder_loop, this);
if (!config_.slave_request_command.empty()) {
slave_thread_ = std::thread(&Impl::slave_loop, this);
}
return true;
}
@@ -227,6 +235,9 @@ struct CPStreamCore::Impl {
if (reader_thread_.joinable()) {
reader_thread_.join();
}
if (slave_thread_.joinable()) {
slave_thread_.join();
}
signal_decoder_flush(true);
packet_cv_.notify_all();
@@ -399,6 +410,33 @@ struct CPStreamCore::Impl {
}
}
void slave_loop() {
const auto command = config_.slave_request_command;
auto interval = config_.slave_request_interval;
if (interval.count() < 0) {
interval = std::chrono::milliseconds{0};
}
const bool repeat = interval.count() > 0;
while (!stop_requested_.load(std::memory_order_acquire)) {
const bool success = send(command);
if (!success) {
std::this_thread::sleep_for(kReaderIdleSleep);
continue;
}
if (!repeat) {
break;
}
auto remaining = interval;
while (remaining.count() > 0 && !stop_requested_.load(std::memory_order_acquire)) {
const auto step = std::min(remaining, kReaderIdleSleep);
std::this_thread::sleep_for(step);
remaining -= step;
}
}
}
void decoder_loop() {
while (true) {
Packet packet;
@@ -450,6 +488,15 @@ struct CPStreamCore::Impl {
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);
if (auto matrix = tactile::parse_matrix_size_payload(*parsed)) {
decoded.tactile_matrix_size = matrix;
}
}
}
FrameCallback callback_copy;
{
@@ -520,6 +567,7 @@ struct CPStreamCore::Impl {
CPCodecContext* codec_ctx_ = nullptr;
std::thread reader_thread_;
std::thread slave_thread_;
std::thread decoder_thread_;
std::mutex packet_mutex_;

View File

@@ -1,6 +1,7 @@
#pragma once
#include "components/ffmsep/cpdecoder.hh"
#include "components/ffmsep/tactile/tacdec.hh"
#include <chrono>
#include <cstdint>
#include <functional>
@@ -16,6 +17,9 @@ struct DecodedFrame {
CPFrame frame;
std::chrono::steady_clock::time_point received_at{};
std::int64_t pts = 0;
std::optional<tactile::TactileFrame> tactile;
std::vector<std::uint16_t> tactile_pressures;
std::optional<tactile::MatrixSize> tactile_matrix_size;
};
struct CPStreamConfig {
@@ -31,6 +35,8 @@ struct CPStreamConfig {
std::size_t frame_queue_capacity = 16;
CPCodecID codec_id = CPCodecID::Unknow;
std::string codec_name;
std::vector<std::uint8_t> slave_request_command{};
std::chrono::milliseconds slave_request_interval{200};
};
class CPStreamCore {

View File

@@ -1,25 +1,26 @@
#include "tacdec.hh"
#include "components/ffmsep/cpdecoder.hh"
#include "tacdec.hh"
#include "components/ffmsep/cpdecoder.hh"
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <new>
#include <optional>
#include <vector>
#include <array>
#include <cstddef>
#include <cstdint>
#include <new>
#include <optional>
#include <vector>
namespace ffmsep::tactile {
namespace {
constexpr std::size_t kMinimumFrameSize = 1
+ 1
+ 1
+ 1
+ 0
+ 2
+ 2;
constexpr std::uint16_t kCrcInitial = 0xFFFF;
constexpr std::uint16_t kCrcPolynomial = 0xA001;
constexpr std::size_t kHeaderSize = 4U; // start bytes + length field
constexpr std::size_t kFixedSectionSize = 1U + 1U + 1U + 4U + 2U + 1U; // address..status
constexpr std::size_t kMinimumFrameSize = kHeaderSize + kFixedSectionSize + 1U; // + CRC byte
constexpr std::uint8_t kCrcPolynomial = 0x07U;
constexpr std::uint8_t kCrcInitial = 0x00U;
constexpr std::uint8_t kCrcXorOut = 0xA9U;
constexpr std::array<std::uint8_t, 2> kStartSequence{
kStartByteFirst,
kStartByteSecond
};
struct TactileDecoderContext {
std::vector<std::uint8_t> fifo;
@@ -27,77 +28,72 @@ struct TactileDecoderContext {
std::int64_t next_pts = 0;
};
std::size_t frame_length_from_payload(std::uint8_t payload_length) {
return 1U + 1U + 1U + 1U + payload_length + 2U + 2U;
}
const std::uint8_t* buffer_data(const std::vector<std::uint8_t>& buf) {
return buf.empty() ? nullptr : buf.data();
}
std::uint16_t crc16_modbus(const std::uint8_t* data, std::size_t length) {
std::uint16_t crc = kCrcInitial;
std::uint8_t crc8_with_xorout(const std::uint8_t* data, std::size_t length) {
std::uint8_t reg = kCrcInitial;
for (std::size_t i = 0; i < length; ++i) {
crc ^= static_cast<std::uint16_t>(data[i]);
reg ^= data[i];
for (int bit = 0; bit < 8; ++bit) {
if ((crc & 0x0001U) != 0U) {
crc = static_cast<std::uint16_t>((crc >> 1U) ^ kCrcPolynomial);
}
else {
crc = static_cast<std::uint16_t>(crc >> 1U);
if ((reg & 0x80U) != 0U) {
reg = static_cast<std::uint8_t>((reg << 1U) ^ kCrcPolynomial);
} else {
reg = static_cast<std::uint8_t>(reg << 1U);
}
}
}
return crc;
return static_cast<std::uint8_t>(reg ^ kCrcXorOut);
}
TactileDecoderContext* get_priv(CPCodecContext* ctx) {
return ctx ? ctx->priv_as<TactileDecoderContext>() : nullptr;
}
int tactile_init(CPCodecContext* ctx) {
if (!ctx) {
return CP_ERROR_INVALID_ARGUMENT;
}
if (!ctx->priv_data) {
ctx->ensure_priv_storage(sizeof(TactileDecoderContext));
}
auto* storage = static_cast<TactileDecoderContext*>(ctx->priv_data);
new (storage) TactileDecoderContext();
return CP_SUCCESS;
}
void tactile_close(CPCodecContext* ctx) {
if (!ctx || !ctx->priv_data) {
return;
}
if (auto* priv = get_priv(ctx); priv != nullptr) {
priv->~TactileDecoderContext();
}
}
int tactile_send_packet(CPCodecContext* ctx, const CPPacket& packet) {
auto priv = get_priv(ctx);
if (!priv) {
return CP_ERROR_INVALID_STATE;
}
if (packet.flush) {
priv->fifo.clear();
priv->end_of_stream = false;
priv->next_pts = 0;
}
if (!packet.payload.empty()) {
priv->fifo.insert(priv->fifo.end(), packet.payload.begin(), packet.payload.end());
}
if (packet.end_of_stream) {
priv->end_of_stream = true;
}
return CP_SUCCESS;
}
int tactile_init(CPCodecContext* ctx) {
if (!ctx) {
return CP_ERROR_INVALID_ARGUMENT;
}
if (!ctx->priv_data) {
ctx->ensure_priv_storage(sizeof(TactileDecoderContext));
}
auto* storage = static_cast<TactileDecoderContext*>(ctx->priv_data);
new (storage) TactileDecoderContext();
return CP_SUCCESS;
}
void tactile_close(CPCodecContext* ctx) {
if (!ctx || !ctx->priv_data) {
return;
}
if (auto* priv = get_priv(ctx); priv != nullptr) {
priv->~TactileDecoderContext();
}
}
int tactile_send_packet(CPCodecContext* ctx, const CPPacket& packet) {
auto priv = get_priv(ctx);
if (!priv) {
return CP_ERROR_INVALID_STATE;
}
if (packet.flush) {
priv->fifo.clear();
priv->end_of_stream = false;
priv->next_pts = 0;
}
if (!packet.payload.empty()) {
priv->fifo.insert(priv->fifo.end(), packet.payload.begin(), packet.payload.end());
}
if (packet.end_of_stream) {
priv->end_of_stream = true;
}
return CP_SUCCESS;
}
int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
auto* priv = get_priv(ctx);
if (!priv) {
@@ -115,7 +111,8 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
return CP_ERROR_EAGAIN;
}
auto start_it = std::find(buf.begin(), buf.end(), kStartByte);
const auto start_it = std::search(buf.begin(), buf.end(),
kStartSequence.begin(), kStartSequence.end());
if (start_it == buf.end()) {
buf.clear();
if (priv->end_of_stream) {
@@ -129,7 +126,7 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
buf.erase(buf.begin(), start_it);
}
if (buf.size() < kMinimumFrameSize) {
if (buf.size() < kHeaderSize) {
if (priv->end_of_stream) {
buf.clear();
priv->end_of_stream = false;
@@ -139,11 +136,21 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
}
const std::uint8_t* data = buffer_data(buf);
const std::uint8_t address = data[1U];
const FunctionCode function = static_cast<FunctionCode>(data[2U]);
const std::uint8_t payload_length = data[3U];
const std::size_t total_frame_length = frame_length_from_payload(payload_length);
if (!data) {
buf.clear();
continue;
}
const std::uint16_t data_length =
static_cast<std::uint16_t>(data[2]) |
static_cast<std::uint16_t>(static_cast<std::uint16_t>(data[3]) << 8U);
if (data_length < kFixedSectionSize) {
buf.erase(buf.begin());
continue;
}
const std::size_t total_frame_length = kHeaderSize + static_cast<std::size_t>(data_length) + 1U;
if (buf.size() < total_frame_length) {
if (priv->end_of_stream) {
buf.clear();
@@ -153,33 +160,13 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
return CP_ERROR_EAGAIN;
}
const std::size_t payload_offset = 4U;
const std::size_t crc_offset = payload_offset + payload_length;
const std::size_t end_offset = crc_offset + 2U;
const std::uint8_t crc_lo = data[crc_offset];
const std::uint8_t crc_hi = data[crc_offset + 1U];
const std::uint16_t crc_value = static_cast<std::uint16_t>(crc_lo) |
static_cast<std::uint16_t>(crc_hi << 8U);
const std::uint8_t end_first = data[end_offset];
const std::uint8_t end_second = data[end_offset + 1U];
if (end_first != kEndByteFirst || end_second != kEndByteSecond) {
const std::uint8_t computed_crc = crc8_with_xorout(data + kHeaderSize, data_length);
const std::uint8_t frame_crc = data[kHeaderSize + static_cast<std::size_t>(data_length)];
if (computed_crc != frame_crc) {
buf.erase(buf.begin());
continue;
}
const std::size_t crc_region_length = 3U + payload_length;
const std::uint16_t computed_crc = crc16_modbus(data + 1U, crc_region_length);
if (computed_crc != crc_value) {
buf.erase(buf.begin());
continue;
}
(void)address;
(void)function;
frame.data.assign(buf.begin(), buf.begin() + static_cast<std::ptrdiff_t>(total_frame_length));
frame.pts = priv->next_pts++;
frame.key_frame = true;
@@ -189,20 +176,20 @@ int tactile_receive_frame(CPCodecContext* ctx, CPFrame& frame) {
return CP_SUCCESS;
}
}
const CPCodec kTactileCodec {
.name = "tactile_serial",
.long_name = "Framed tactile sensor serial protocol decoder",
.type = CPMediaType::Data,
.id = CPCodecID::Tactile,
.priv_data_size = sizeof(TactileDecoderContext),
.init = &tactile_init,
.close = &tactile_close,
.send_packet = &tactile_send_packet,
.receive_frame = &tactile_receive_frame
};
}
const CPCodec kTactileCodec {
.name = "tactile_serial",
.long_name = "Framed tactile sensor serial protocol decoder",
.type = CPMediaType::Data,
.id = CPCodecID::Tactile,
.priv_data_size = sizeof(TactileDecoderContext),
.init = &tactile_init,
.close = &tactile_close,
.send_packet = &tactile_send_packet,
.receive_frame = &tactile_receive_frame
};
}
std::optional<TactileFrame> parse_frame(const CPFrame& frame) {
if (!frame.valid || frame.data.size() < kMinimumFrameSize) {
return std::nullopt;
@@ -210,64 +197,92 @@ std::optional<TactileFrame> parse_frame(const CPFrame& frame) {
const auto* bytes = frame.data.data();
const std::size_t size = frame.data.size();
if (bytes[0] != kStartByte) {
if (bytes[0] != kStartByteFirst || bytes[1] != kStartByteSecond) {
return std::nullopt;
}
if (bytes[size - 2] != kEndByteFirst || bytes[size - 1] != kEndByteSecond) {
const std::uint16_t data_length =
static_cast<std::uint16_t>(bytes[2]) |
static_cast<std::uint16_t>(static_cast<std::uint16_t>(bytes[3]) << 8U);
if (data_length < kFixedSectionSize) {
return std::nullopt;
}
if (size < 4U) {
const std::size_t expected_size = kHeaderSize + static_cast<std::size_t>(data_length) + 1U;
if (size != expected_size) {
return std::nullopt;
}
const std::uint8_t length = bytes[3];
if (frame_length_from_payload(length) != size) {
const std::uint8_t crc_byte = bytes[size - 1U];
const std::uint8_t computed_crc = crc8_with_xorout(bytes + kHeaderSize, data_length);
if (computed_crc != crc_byte) {
return std::nullopt;
}
const std::uint8_t device_address = bytes[4];
const std::uint8_t reserved = bytes[5];
const std::uint8_t response_function = bytes[6];
const std::uint32_t start_address =
static_cast<std::uint32_t>(bytes[7]) |
(static_cast<std::uint32_t>(bytes[8]) << 8U) |
(static_cast<std::uint32_t>(bytes[9]) << 16U) |
(static_cast<std::uint32_t>(bytes[10]) << 24U);
const std::uint16_t return_byte_count =
static_cast<std::uint16_t>(bytes[11]) |
(static_cast<std::uint16_t>(bytes[12]) << 8U);
const std::uint8_t status = bytes[13];
const std::size_t payload_offset = kHeaderSize + kFixedSectionSize;
const std::size_t payload_length = static_cast<std::size_t>(data_length) - kFixedSectionSize;
if (payload_length != return_byte_count) {
return std::nullopt;
}
const std::uint8_t address = bytes[1];
const FunctionCode function = static_cast<FunctionCode>(bytes[2]);
const std::size_t payload_offset = 4U;
TactileFrame parsed{};
parsed.device_address = address;
parsed.function = function;
parsed.data_length = length;
parsed.payload.assign(bytes + payload_offset, bytes + payload_offset + length);
parsed.device_address = device_address;
parsed.reserved = reserved;
parsed.response_function = response_function;
parsed.function = static_cast<FunctionCode>(response_function & 0x7FU);
parsed.start_address = start_address;
parsed.return_byte_count = return_byte_count;
parsed.status = status;
parsed.payload.assign(bytes + payload_offset, bytes + payload_offset + payload_length);
return parsed;
}
std::vector<std::uint16_t> parse_pressure_values(const TactileFrame& frame) {
if (frame.payload.size() != frame.return_byte_count) {
return {};
}
if (frame.payload.empty() || (frame.payload.size() % 2U != 0U)) {
return {};
}
std::vector<std::uint16_t> values;
values.reserve(frame.payload.size() / 2U);
for (std::size_t idx = 0; idx + 1U < frame.payload.size(); idx += 2U) {
const std::uint16_t value = static_cast<std::uint16_t>(
static_cast<std::uint16_t>(frame.payload[idx]) |
static_cast<std::uint16_t>(frame.payload[idx + 1U] << 8U));
values.push_back(value);
}
return values;
}
std::optional<MatrixSize> parse_matrix_size_payload(const TactileFrame& frame) {
if (frame.payload.size() != 2U) {
return std::nullopt;
}
MatrixSize size{};
size.long_edge = frame.payload[0];
size.short_edge = frame.payload[1];
return size;
}
const CPCodec* tactile_codec() {
return &kTactileCodec;
}
void register_tactile_codec() {
cpcodec_register(&kTactileCodec);
}
}
std::vector<std::uint16_t> values;
values.reserve(frame.payload.size() / 2U);
for (std::size_t idx = 0; idx + 1U < frame.payload.size(); idx += 2U) {
const std::uint16_t value = static_cast<std::uint16_t>(
static_cast<std::uint16_t>(frame.payload[idx]) |
static_cast<std::uint16_t>(frame.payload[idx + 1U] << 8U));
values.push_back(value);
}
return values;
}
std::optional<MatrixSize> parse_matrix_size_payload(const TactileFrame& frame) {
if (frame.payload.size() != 2U) {
return std::nullopt;
}
MatrixSize size{};
size.long_edge = frame.payload[0];
size.short_edge = frame.payload[1];
return size;
}
const CPCodec* tactile_codec() {
return &kTactileCodec;
}
void register_tactile_codec() {
cpcodec_register(&kTactileCodec);
}
}

View File

@@ -1,42 +1,45 @@
#pragma once
#include "cpdecoder.hh"
#include <cstdint>
#include <optional>
#include <vector>
#pragma once
#include "cpdecoder.hh"
#include <cstdint>
#include <optional>
#include <vector>
namespace ffmsep::tactile {
inline constexpr std::uint8_t kStartByte = 0x3A;
inline constexpr std::uint8_t kEndByteFirst = 0x0D;
inline constexpr std::uint8_t kEndByteSecond = 0x0A;
inline constexpr std::uint8_t kStartByteFirst = 0xAA;
inline constexpr std::uint8_t kStartByteSecond = 0x55;
enum class FunctionCode : std::uint8_t {
Unknown = 0x00,
ReadMatrix = 0x01,
ReadSingle = 0x02,
ReadTemperature = 0x03,
SetDeviceId = 0x51,
SetMatrixSize = 0x52,
CalibrationMode = 0x53,
};
struct MatrixSize {
std::uint8_t long_edge = 0;
std::uint8_t short_edge = 0;
};
ReadTemperature = 0x03,
SetDeviceId = 0x51,
SetMatrixSize = 0x52,
CalibrationMode = 0x53,
};
struct MatrixSize {
std::uint8_t long_edge = 0;
std::uint8_t short_edge = 0;
};
struct TactileFrame {
std::uint8_t device_address = 0;
std::uint8_t reserved = 0;
std::uint8_t response_function = 0;
FunctionCode function = FunctionCode::Unknown;
std::uint8_t data_length = 0;
std::uint32_t start_address = 0;
std::uint16_t return_byte_count = 0;
std::uint8_t status = 0;
std::vector<std::uint8_t> payload;
};
std::optional<TactileFrame> parse_frame(const CPFrame& frame);
std::vector<std::uint16_t> parse_pressure_values(const TactileFrame& frame);
std::optional<MatrixSize> parse_matrix_size_payload(const TactileFrame& frame);
std::optional<MatrixSize> parse_patrix_coordinate_payload(const TactileFrame& frame);
const CPCodec* tactile_codec();
void register_tactile_codec();
}
std::optional<MatrixSize> parse_matrix_size_payload(const TactileFrame& frame);
std::optional<MatrixSize> parse_patrix_coordinate_payload(const TactileFrame& frame);
const CPCodec* tactile_codec();
void register_tactile_codec();
}

View File

@@ -1,103 +1,103 @@
#include "component.hh"
#include "modern-qt/core/application.hh"
#include "modern-qt/layout/group.hh"
#include "modern-qt/layout/linear.hh"
#include "modern-qt/layout/mutual-exclusion-group.hh"
#include "modern-qt/utility/material-icon.hh"
#include "modern-qt/utility/theme/theme.hh"
#include "modern-qt/widget/buttons/icon-button.hh"
#include "modern-qt/widget/cards/filled-card.hh"
#include "modern-qt/widget/image.hh"
using namespace creeper;
namespace fc = filled_card::pro;
namespace sg = select_group::pro;
namespace ln = linear::pro;
namespace im = image::pro;
namespace ic = icon_button::pro;
auto NavComponent(NavComponentState& state) noexcept -> raw_pointer<QWidget> {
const auto AvatarComponent = new Image {
im::FixedSize {60, 60},
im::Radius {-1},
im::ContentScale {ContentScale::CROP},
im::BorderWidth {3},
im::PainterResource {
":/images/images/logo.png",
// "./images/logo.png",
},
};
state.manager.append_handler(AvatarComponent, [AvatarComponent](const ThemeManager& manager) {
const auto colorscheme = manager.color_scheme();
const auto colorborder = colorscheme.secondary_container;
AvatarComponent->set_border_color(colorborder);
});
const auto navigation_icons_config = std::tuple {
ic::ThemeManager {state.manager},
ic::ColorStandard,
ic::ShapeRound,
ic::TypesToggleUnselected,
ic::WidthDefault,
ic::Font {material::regular::font_1},
ic::FixedSize {IconButton::kSmallContainerSize},
};
return new FilledCard {
fc::ThemeManager {state.manager},
fc::Radius {0},
fc::Level {CardLevel::HIGHEST},
fc::Layout<Col> {
ln::Spacing {10},
ln::Margin {15},
ln::Item {
{0, Qt::AlignHCenter},
AvatarComponent,
},
ln::SpacingItem {20},
ln::Item<SelectGroup<Col, IconButton>> {
{0, Qt::AlignHCenter},
ln::Margin {0},
ln::SpacingItem {10},
sg::Compose {
state.buttons_context | std::views::enumerate,
[&](int index, const auto& context) {
const auto& [name, icon] = context;
const auto status = (index == 0)
? ic::TypesToggleSelected
: ic::TypesToggleUnselected;
return new IconButton {
navigation_icons_config,
status,
ic::FontIcon(icon.data()),
ic::Clickable {[=]{state.switch_callback(index, name);}},
};
},
Qt::AlignHCenter,
},
sg::SignalInjection{&IconButton::clicked},
},
ln::SpacingItem {40},
ln::Stretch {255},
ln::Item<IconButton> {
{0, Qt::AlignHCenter},
navigation_icons_config,
ic::TypesDefault,
ic::FontIcon {material::icon::kLogout},
ic::Clickable {&app::quit},
},
ln::Item<IconButton> {
{0, Qt::AlignHCenter},
navigation_icons_config,
ic::ColorFilled,
ic::FontIcon {material::icon::kDarkMode},
ic::Clickable{[&]{state.manager.toggle_color_mode();state.manager.apply_theme();}},
}
}
};
}
#include "component.hh"
#include "modern-qt/core/application.hh"
#include "modern-qt/layout/group.hh"
#include "modern-qt/layout/linear.hh"
#include "modern-qt/layout/mutual-exclusion-group.hh"
#include "modern-qt/utility/material-icon.hh"
#include "modern-qt/utility/theme/theme.hh"
#include "modern-qt/widget/buttons/icon-button.hh"
#include "modern-qt/widget/cards/filled-card.hh"
#include "modern-qt/widget/image.hh"
using namespace creeper;
namespace fc = filled_card::pro;
namespace sg = select_group::pro;
namespace ln = linear::pro;
namespace im = image::pro;
namespace ic = icon_button::pro;
auto NavComponent(NavComponentState& state) noexcept -> raw_pointer<QWidget> {
const auto AvatarComponent = new Image {
im::FixedSize {60, 60},
im::Radius {-1},
im::ContentScale {ContentScale::CROP},
im::BorderWidth {3},
im::PainterResource {
":/images/images/logo.png",
// "./images/logo.png",
},
};
state.manager.append_handler(AvatarComponent, [AvatarComponent](const ThemeManager& manager) {
const auto colorscheme = manager.color_scheme();
const auto colorborder = colorscheme.secondary_container;
AvatarComponent->set_border_color(colorborder);
});
const auto navigation_icons_config = std::tuple {
ic::ThemeManager {state.manager},
ic::ColorStandard,
ic::ShapeRound,
ic::TypesToggleUnselected,
ic::WidthDefault,
ic::Font {material::regular::font_1},
ic::FixedSize {IconButton::kSmallContainerSize},
};
return new FilledCard {
fc::ThemeManager {state.manager},
fc::Radius {0},
fc::Level {CardLevel::HIGHEST},
fc::Layout<Col> {
ln::Spacing {10},
ln::Margin {15},
ln::Item {
{0, Qt::AlignHCenter},
AvatarComponent,
},
ln::SpacingItem {20},
ln::Item<SelectGroup<Col, IconButton>> {
{0, Qt::AlignHCenter},
ln::Margin {0},
ln::SpacingItem {10},
sg::Compose {
state.buttons_context | std::views::enumerate,
[&](int index, const auto& context) {
const auto& [name, icon] = context;
const auto status = (index == 0)
? ic::TypesToggleSelected
: ic::TypesToggleUnselected;
return new IconButton {
navigation_icons_config,
status,
ic::FontIcon(icon.data()),
ic::Clickable {[=]{state.switch_callback(index, name);}},
};
},
Qt::AlignHCenter,
},
sg::SignalInjection{&IconButton::clicked},
},
ln::SpacingItem {40},
ln::Stretch {255},
ln::Item<IconButton> {
{0, Qt::AlignHCenter},
navigation_icons_config,
ic::TypesDefault,
ic::FontIcon {material::icon::kLogout},
ic::Clickable {&app::quit},
},
ln::Item<IconButton> {
{0, Qt::AlignHCenter},
navigation_icons_config,
ic::ColorFilled,
ic::FontIcon {material::icon::kDarkMode},
ic::Clickable{[&]{state.manager.toggle_color_mode();state.manager.apply_theme();}},
}
}
};
}

View File

@@ -1,213 +1,589 @@
//
// Created by Lenn on 2025/10/14.
//
//
// Created by Lenn on 2025/10/14.
//
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <memory>
#include <mutex>
#include <QString>
#include <QObject>
#include <QMetaObject>
#include <QStringList>
#include <QtCore/Qt>
#include <optional>
#include <string>
#include <qsize.h>
#include <qsizepolicy.h>
#include <random>
#include <chrono>
#include <iomanip>
#include <iostream>
#include <sstream>
#include "component.hh"
#include "cpstream_core.hh"
#include "modern-qt/utility/theme/theme.hh"
#include "modern-qt/utility/wrapper/layout.hh"
#include "modern-qt/utility/wrapper/widget.hh"
#include "components/charts/heatmap.hh"
#include <modern-qt/layout/flow.hh>
#include <modern-qt/layout/linear.hh>
#include <modern-qt/utility/material-icon.hh>
#include <modern-qt/utility/wrapper/mutable-value.hh>
#include <modern-qt/widget/buttons/icon-button.hh>
#include <modern-qt/widget/cards/filled-card.hh>
#include <modern-qt/widget/cards/outlined-card.hh>
#include <modern-qt/widget/image.hh>
#include <modern-qt/widget/shape/wave-circle.hh>
#include <modern-qt/widget/sliders.hh>
#include <modern-qt/widget/switch.hh>
#include <modern-qt/widget/text-fields.hh>
#include <modern-qt/widget/text.hh>
#include <modern-qt/widget/select.hh>
#include "components/ffmsep/tactile/tacdec.hh"
using namespace creeper;
namespace capro = card::pro;
namespace lnpro = linear::pro;
namespace impro = image::pro;
namespace ibpro = icon_button::pro;
namespace slpro = select_widget::pro;
namespace pwpro = plot_widget::pro;
namespace {
constexpr std::array<std::uint8_t, 14> kSlaveRequestCommand{
0x55, 0xAA, 0x09, 0x00, 0x34, 0x00, 0xFB,
0x00, 0x1C, 0x00, 0x00, 0x18, 0x00, 0x7A
};
QVector<PointData> make_flat_points(const QSize& size, double value = 0.0) {
const int width = std::max(size.width(), 1);
const int height = std::max(size.height(), 1);
QVector<PointData> points;
points.reserve(static_cast<int>(width * height));
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
points.append(PointData{
static_cast<double>(x),
static_cast<double>(y),
value
});
}
}
return points;
}
std::once_flag& codec_registration_flag() {
static std::once_flag flag;
return flag;
}
class SensorStreamController : public QObject {
public:
SensorStreamController(std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data,
std::shared_ptr<MutableValue<QSize>> matrix_context,
QObject* parent = nullptr)
: QObject(parent)
, heatmap_data_(std::move(heatmap_data))
, matrix_context_(std::move(matrix_context)) {
std::call_once(codec_registration_flag(), [] {
ffmsep::tactile::register_tactile_codec();
});
}
~SensorStreamController() override {
reset_core();
}
bool start(const QString& requested_port, std::uint32_t baudrate) {
if (is_connected()) {
return true;
}
const auto ports = ffmsep::CPStreamCore::list_available_ports();
std::string port_utf8;
if (!requested_port.isEmpty()) {
port_utf8 = requested_port.toStdString();
const auto it = std::find_if(
ports.begin(), ports.end(),
[&](const serial::PortInfo& info) { return info.port == port_utf8; });
if (it == ports.end()) {
if (ports.empty()) {
std::cerr << "SensorStreamController: requested port '" << port_utf8 << "' not available and no other ports detected.\n";
last_error_ = QString::fromUtf8("未检测到串口");
return false;
}
std::cerr << "SensorStreamController: requested port '" << port_utf8 << "' not available, falling back to first detected port.\n";
port_utf8 = ports.front().port;
}
} else if (!ports.empty()) {
port_utf8 = ports.front().port;
} else {
std::cerr << "SensorStreamController: no serial ports available\n";
last_error_ = QString::fromUtf8("未检测到串口");
return false;
}
const std::uint32_t baud = baudrate == 0U ? 115200U : baudrate;
ffmsep::CPStreamConfig cfg;
cfg.port = port_utf8;
cfg.baudrate = baud;
cfg.codec_id = ffmsep::CPCodecID::Tactile;
cfg.read_chunk_size = 256;
cfg.packet_queue_capacity = 128;
cfg.frame_queue_capacity = 32;
cfg.slave_request_command.assign(kSlaveRequestCommand.begin(), kSlaveRequestCommand.end());
cfg.slave_request_interval = std::chrono::milliseconds{200};
reset_core();
core_ = std::make_unique<ffmsep::CPStreamCore>();
if (!core_->open(cfg)) {
last_error_ = QString::fromStdString(core_->last_error());
std::cerr << "SensorStreamController: open failed - " << core_->last_error() << "\n";
reset_core();
return false;
}
core_->set_frame_callback([this](const ffmsep::DecodedFrame& frame) {
handle_frame(frame);
});
if (!core_->start()) {
last_error_ = QString::fromStdString(core_->last_error());
std::cerr << "SensorStreamController: start failed - " << core_->last_error() << "\n";
reset_core();
return false;
}
active_port_ = QString::fromStdString(cfg.port);
last_error_.clear();
connected_ = true;
return true;
}
#include "component.hh"
#include "modern-qt/utility/theme/theme.hh"
#include "modern-qt/utility/wrapper/layout.hh"
#include "modern-qt/utility/wrapper/widget.hh"
#include "components/charts/heatmap.hh"
#include <modern-qt/layout/flow.hh>
#include <modern-qt/layout/linear.hh>
#include <modern-qt/utility/material-icon.hh>
#include <modern-qt/utility/wrapper/mutable-value.hh>
#include <modern-qt/widget/buttons/icon-button.hh>
#include <modern-qt/widget/cards/filled-card.hh>
#include <modern-qt/widget/cards/outlined-card.hh>
#include <modern-qt/widget/image.hh>
#include <modern-qt/widget/shape/wave-circle.hh>
#include <modern-qt/widget/sliders.hh>
#include <modern-qt/widget/switch.hh>
#include <modern-qt/widget/text-fields.hh>
#include <modern-qt/widget/text.hh>
#include <modern-qt/widget/select.hh>
void stop() {
reset_core();
active_port_.clear();
if (heatmap_data_ && matrix_context_) {
heatmap_data_->set(make_flat_points(matrix_context_->get()));
}
connected_ = false;
}
using namespace creeper;
namespace capro = card::pro;
namespace lnpro = linear::pro;
namespace impro = image::pro;
namespace ibpro = icon_button::pro;
namespace slpro = select_widget::pro;
namespace pwpro = plot_widget::pro;
[[nodiscard]] bool is_running() const noexcept {
return core_ && core_->is_running();
}
static auto ComConfigComponent(ThemeManager& manager, auto&& callback) {
auto slogen_context = std::make_shared<MutableValue<QString>>();
slogen_context->set_silent("BanG Bream! It's MyGo!!!");
auto select_com_context = std::make_shared<MutableValue<QStringList>>();
select_com_context->set_silent(QStringList {"COM1", "COM2", "COM3", "COM4", "COM5"});
[[nodiscard]] bool is_connected() const noexcept {
return connected_;
}
auto select_baud_context = std::make_shared<MutableValue<QStringList>>();
select_baud_context->set_silent(QStringList {"9600", "115200"});
[[nodiscard]] QString active_port() const {
return active_port_;
}
[[nodiscard]] QString last_error() const {
return last_error_;
}
private:
void reset_core() {
connected_ = false;
if (!core_) {
return;
}
core_->set_frame_callback({});
if (core_->is_running()) {
core_->stop();
}
if (core_->is_open()) {
core_->close();
}
core_.reset();
}
static QSize to_qsize(const ffmsep::tactile::MatrixSize& m) {
return QSize{
static_cast<int>(m.long_edge),
static_cast<int>(m.short_edge)
};
}
void handle_frame(const ffmsep::DecodedFrame& frame) {
if (!frame.tactile || frame.tactile_pressures.empty()) {
return;
}
auto pressures = frame.tactile_pressures;
auto size_hint = frame.tactile_matrix_size;
auto frame_bytes = frame.frame.data;
std::vector<std::uint8_t> raw_payload;
if (frame.tactile) {
raw_payload = frame.tactile->payload;
}
QMetaObject::invokeMethod(
this,
[this,
pressures = std::move(pressures),
size_hint,
frame_bytes = std::move(frame_bytes),
raw_payload = std::move(raw_payload)]() {
const auto format_raw = [](const std::vector<std::uint8_t>& data) -> std::string {
if (data.empty()) {
return "[]";
}
std::ostringstream oss;
oss << '[';
oss << std::uppercase << std::setfill('0');
for (std::size_t idx = 0; idx < data.size(); ++idx) {
if (idx != 0U) {
oss << ' ';
}
oss << std::setw(2) << std::hex << static_cast<unsigned int>(data[idx]);
}
oss << ']';
return oss.str();
};
std::cout << "[Sensor] frame=" << format_raw(frame_bytes);
std::cout << " payload=" << format_raw(raw_payload);
std::cout << " received " << pressures.size() << " pressure values";
if (size_hint) {
std::cout << " matrix=" << int(size_hint->long_edge)
<< "x" << int(size_hint->short_edge);
}
const std::size_t preview = std::min<std::size_t>(pressures.size(), 12);
if (preview > 0) {
std::cout << " values=[";
for (std::size_t idx = 0; idx < preview; ++idx) {
if (idx != 0U) {
std::cout << ", ";
}
std::cout << pressures[idx];
}
if (preview < pressures.size()) {
std::cout << ", ...";
}
std::cout << "]";
}
std::cout << std::endl;
auto matrix = matrix_context_->get();
if (size_hint) {
matrix = to_qsize(*size_hint);
}
matrix = normalize_matrix(matrix, pressures.size());
if (matrix.isEmpty()) {
return;
}
QVector<PointData> points;
points.reserve(matrix.width() * matrix.height());
for (int y = 0; y < matrix.height(); ++y) {
for (int x = 0; x < matrix.width(); ++x) {
const int idx = y * matrix.width() + x;
if (idx >= static_cast<int>(pressures.size())) {
break;
}
const auto value = static_cast<double>(pressures[static_cast<std::size_t>(idx)]);
points.append(PointData{
static_cast<double>(x),
static_cast<double>(y),
value
});
}
}
matrix_context_->set(matrix);
heatmap_data_->set(std::move(points));
},
Qt::QueuedConnection);
}
[[nodiscard]] QSize normalize_matrix(QSize candidate, std::size_t value_count) const {
if (value_count == 0U) {
return QSize{};
}
const auto row = new Row {
// lnpro::Item<FilledTextField> {
// text_field::pro::ThemeManager {manager},
// text_field::pro::LeadingIcon {material::icon::kSearch, material::regular::font},
// MutableForward {
// text_field::pro::LabelText {},
// slogen_context,
// },
// },
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_;
std::shared_ptr<MutableValue<QSize>> matrix_context_;
std::unique_ptr<ffmsep::CPStreamCore> core_;
QString active_port_;
QString last_error_;
bool connected_ = false;
};
struct SensorUiState {
std::shared_ptr<MutableValue<QString>> link_icon =
std::make_shared<MutableValue<QString>>(QString::fromLatin1(material::icon::kAddLink));
std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data =
std::make_shared<MutableValue<QVector<PointData>>>();
std::shared_ptr<MutableValue<QSize>> heatmap_matrix =
std::make_shared<MutableValue<QSize>>();
std::shared_ptr<MutableValue<QStringList>> port_items =
std::make_shared<MutableValue<QStringList>>();
QString selected_port;
std::uint32_t selected_baud = 115200;
std::unique_ptr<SensorStreamController> controller;
SensorUiState() {
const QSize size{3, 4};
heatmap_matrix->set_silent(size);
heatmap_data->set_silent(make_flat_points(size));
// 初始化串口列表
QStringList ports_list;
const auto ports = ffmsep::CPStreamCore::list_available_ports();
ports_list.reserve(static_cast<qsizetype>(ports.size()));
for (const auto& info : ports) {
ports_list.emplace_back(QString::fromStdString(info.port));
}
port_items->set_silent(ports_list);
if (selected_port.isEmpty() && !ports_list.isEmpty()) {
selected_port = ports_list.front();
}
controller = std::make_unique<SensorStreamController>(heatmap_data, heatmap_matrix);
}
};
SensorUiState& sensor_state() {
static SensorUiState state;
return state;
}
} // namespace
static auto ComConfigComponent(ThemeManager& manager) {
auto& sensor = sensor_state();
auto link_icon_context = sensor.link_icon;
// 串口下拉:改为绑定可变数据源,初始值由 SensorUiState 构造时填充
if (sensor.selected_port.isEmpty() && !sensor.port_items->get().isEmpty()) {
sensor.selected_port = sensor.port_items->get().front();
}
const QStringList baud_items{
QString::fromLatin1("9600"),
QString::fromLatin1("115200")
};
if (sensor.selected_baud == 0U) {
sensor.selected_baud = 115200U;
}
const auto row = new Row {
// lnpro::Item<FilledTextField> {
// text_field::pro::ThemeManager {manager},
// text_field::pro::LeadingIcon {material::icon::kSearch, material::regular::font},
// MutableForward {
// text_field::pro::LabelText {},
// slogen_context,
// },
// },
lnpro::Item<MatSelect> {
slpro::ThemeManager {manager},
slpro::LeadingIcon {material::icon::kArrowDropDown, material::regular::font},
slpro::IndexChanged {[&](auto& self){ qDebug() << self.currentIndex();}},
slpro::IndexChanged {[sensor_ptr = &sensor](auto& self){
const auto text = self.currentText();
if (!text.isEmpty()) {
sensor_ptr->selected_port = text;
}
}},
slpro::LeadingText {"COM"},
MutableForward {
slpro::SelectItems {},
select_com_context,
}
},
lnpro::Item<MatSelect> {
slpro::ThemeManager {manager },
slpro::LeadingIcon { material::icon::kArrowDropDown, material::regular::font},
slpro::IndexChanged {[&](auto& self){ qDebug() << self.currentIndex();}},
slpro::LeadingText {"Baud"},
MutableForward {
slpro::SelectItems {},
select_baud_context,
}
},
lnpro::SpacingItem {20},
lnpro::Item<IconButton> {
ibpro::ThemeManager {manager},
ibpro::FixedSize {40, 40},
ibpro::Color { IconButton::Color::TONAL },
ibpro::Font { material::kRoundSmallFont },
ibpro::FontIcon { material::icon::kAddLink },
ibpro::Clickable {[slogen_context] {
constexpr auto random_slogen = [] {
constexpr auto slogens = std::array {
"为什么要演奏《春日影》!",
"我从来不觉得玩乐队开心过。",
"我好想…成为人啊!",
"那你愿意……跟我组一辈子的乐队吗?",
"过去软弱的我…已经死了。",
};
static std::random_device rd;
static std::mt19937 gen(rd());
std::uniform_int_distribution<> dist(0, slogens.size() - 1);
return QString::fromUtf8(slogens[dist(gen)]);
};
*slogen_context = random_slogen();
}},
sensor.port_items,
},
},
lnpro::Item<MatSelect> {
slpro::ThemeManager {manager },
slpro::LeadingIcon { material::icon::kArrowDropDown, material::regular::font},
slpro::IndexChanged {[sensor_ptr = &sensor](auto& self){
bool ok = false;
const auto text = self.currentText();
const auto value = text.toUInt(&ok);
if (ok && value > 0U) {
sensor_ptr->selected_baud = static_cast<std::uint32_t>(value);
}
}},
slpro::LeadingText {"Baud"},
slpro::SelectItems {baud_items},
},
lnpro::SpacingItem {20},
lnpro::Item<IconButton> {
ibpro::ThemeManager {manager},
ibpro::FixedSize {40, 40},
ibpro::Color { IconButton::Color::TONAL },
ibpro::Font { material::kRoundSmallFont },
MutableForward {
icon_button::pro::FontIcon {},
link_icon_context,
},
ibpro::Clickable { [sensor_ptr = &sensor, link_icon_context]{
auto& sensor = *sensor_ptr;
if (!sensor.controller) {
return;
}
if (sensor.controller->is_connected()) {
sensor.controller->stop();
link_icon_context->set(QString::fromLatin1(material::icon::kAddLink));
} else {
const auto port = sensor.selected_port;
const auto baud = sensor.selected_baud == 0U ? 115200U : sensor.selected_baud;
if (sensor.controller->start(port, baud)) {
sensor.selected_port = sensor.controller->active_port();
link_icon_context->set(QString::fromLatin1(material::icon::kLinkOff));
} else {
std::cerr << "Failed to start sensor stream: "
<< sensor.controller->last_error().toStdString()
<< "\n";
}
}
} }
},
lnpro::Item<IconButton> {
ibpro::ThemeManager { manager },
ibpro::FixedSize { 40, 40 },
ibpro::Color { IconButton::Color::TONAL },
ibpro::Font { material::kRoundSmallFont },
ibpro::FontIcon { material::icon::kRefresh },
ibpro::Clickable {[select_baud_context] {
static constexpr auto options_group1 = std::array {
"第一组选项1", "第一组选项2", "第一组选项3"
};
static constexpr auto options_group2 = std::array {
"第二组选项A", "第二组选项B", "第二组选项C", "第二组选项D"
};
static std::random_device rd;
static std::mt19937 gen(rd());
std::uniform_int_distribution<> dist(0, 1);
QStringList new_options;
if (dist(gen) == 0) {
for (const auto& option : options_group1) {
new_options << QString::fromUtf8(option);
}
} else {
for (const auto& option : options_group2) {
new_options << QString::fromUtf8(option);
}
ibpro::Clickable {[&sensor] {
// 刷新串口列表
QStringList ports_list;
const auto ports = ffmsep::CPStreamCore::list_available_ports();
ports_list.reserve(static_cast<qsizetype>(ports.size()));
for (const auto& info : ports) {
ports_list.emplace_back(QString::fromStdString(info.port));
}
*select_baud_context = new_options;
// 保持原选择(若仍然存在)
if (!sensor.selected_port.isEmpty()) {
const bool exists = ports_list.contains(sensor.selected_port);
if (!exists) {
sensor.selected_port = ports_list.isEmpty() ? QString{} : ports_list.front();
}
} else if (!ports_list.isEmpty()) {
sensor.selected_port = ports_list.front();
}
sensor.port_items->set(std::move(ports_list));
}},
},
};
return new Widget {
widget::pro::Layout {row},
};
}
static auto DisplayComponent(ThemeManager& manager, int index = 0) noexcept {
auto heatmap_context = std::make_shared<MutableValue<QVector<PointData>>>();
heatmap_context->set_silent(QVector<PointData>{
PointData{0, 0, 1}, PointData{1, 0, 2}, PointData{2, 0, 3},
PointData{0, 1, 3}, PointData{1, 1, 4}, PointData{2, 1, 5},
PointData{0, 2, 6}, PointData{1, 2, 7}, PointData{2, 2, 8},
PointData{0, 3, 9}, PointData{1, 3, 10}, PointData{2, 3, 11},
});
const auto row = new Row{
lnpro::Item<HeatMapPlot> {
plot_widget::pro::SizePolicy {
QSizePolicy::Expanding,
},
MutableForward {
plot_widget::pro::PlotData {},
heatmap_context,
},
pwpro::MatrixSize {
QSize{3, 4}
},
},
};
return new Widget {
widget::pro::Layout{row},
};
}
auto ViewComponent(ViewComponentState& state) noexcept -> raw_pointer<QWidget> {
const auto texts = std::array {
std::make_shared<MutableValue<QString>>("0.500"),
std::make_shared<MutableValue<QString>>("0.500"),
std::make_shared<MutableValue<QString>>("0.500"),
};
const auto progresses = std::array {
std::make_shared<MutableValue<double>>(0.5),
std::make_shared<MutableValue<double>>(0.5),
std::make_shared<MutableValue<double>>(0.5),
};
return new FilledCard {
capro::ThemeManager { state.manager },
capro::SizePolicy {QSizePolicy::Expanding},
capro::Layout<Col> {
lnpro::Alignment {Qt::AlignTop},
lnpro::Margin {10},
lnpro::Spacing {10},
lnpro::Item {
ComConfigComponent(state.manager,
[texts, progresses] {
constexpr auto random_unit = []() {
static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_real_distribution<double> dist(0.0, 1.0);
return dist(gen);
};
for (auto&& [string, number] : std::views::zip(texts, progresses)) {
auto v = random_unit();
*number = v;
*string = QString::number(v, 'f', 3);
}
}),
},
lnpro::Item<Row> {
lnpro::Item {
DisplayComponent(state.manager),
},
lnpro::Item {
DisplayComponent(state.manager),
},
},
},
};
}
};
return new Widget {
widget::pro::Layout {row},
};
}
static auto DisplayComponent(ThemeManager& /*manager*/, int /*index*/ = 0) noexcept {
auto& sensor = sensor_state();
const auto row = new Row{
lnpro::Item<HeatMapPlot> {
plot_widget::pro::SizePolicy {
QSizePolicy::Expanding,
},
MutableForward {
plot_widget::pro::PlotData {},
sensor.heatmap_data,
},
pwpro::MatrixSize {
sensor.heatmap_matrix->get()
},
MutableTransform {
[](auto& widget, const QSize& size) {
pwpro::MatrixSize{size}.apply(widget);
},
sensor.heatmap_matrix
},
},
};
return new Widget {
widget::pro::Layout{row},
};
}
auto ViewComponent(ViewComponentState& state) noexcept -> raw_pointer<QWidget> {
return new FilledCard {
capro::ThemeManager { state.manager },
capro::SizePolicy {QSizePolicy::Expanding},
capro::Layout<Col> {
lnpro::Alignment {Qt::AlignTop},
lnpro::Margin {10},
lnpro::Spacing {10},
lnpro::Item {
ComConfigComponent(state.manager),
},
lnpro::Item<Row> {
lnpro::Item {
DisplayComponent(state.manager),
},
lnpro::Item {
DisplayComponent(state.manager),
},
},
},
};
}