feat:export data while running or close
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include "components/ffmsep/cpstream_core.hh"
|
#include "components/ffmsep/cpstream_core.hh"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
@@ -19,6 +20,73 @@ namespace {
|
|||||||
|
|
||||||
using nlohmann::json;
|
using nlohmann::json;
|
||||||
|
|
||||||
|
bool is_simple_array(const json& value) {
|
||||||
|
if (!value.is_array()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return std::all_of(value.begin(), value.end(), [](const json& item) {
|
||||||
|
return item.is_primitive();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void dump_compact_json(std::ostream& out,
|
||||||
|
const json& value,
|
||||||
|
int indent = 0,
|
||||||
|
int indent_step = 2) {
|
||||||
|
const auto indent_str = std::string(static_cast<std::size_t>(indent), ' ');
|
||||||
|
const auto child_indent = indent + indent_step;
|
||||||
|
const auto child_indent_str = std::string(static_cast<std::size_t>(child_indent), ' ');
|
||||||
|
|
||||||
|
if (value.is_object()) {
|
||||||
|
out << "{\n";
|
||||||
|
bool first = true;
|
||||||
|
for (auto it = value.begin(); it != value.end(); ++it) {
|
||||||
|
if (!first) {
|
||||||
|
out << ",\n";
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
out << child_indent_str << json(it.key()).dump() << ": ";
|
||||||
|
dump_compact_json(out, it.value(), child_indent, indent_step);
|
||||||
|
}
|
||||||
|
out << '\n' << indent_str << '}';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.is_array()) {
|
||||||
|
if (value.empty()) {
|
||||||
|
out << "[]";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_simple_array(value)) {
|
||||||
|
out << '[';
|
||||||
|
for (std::size_t idx = 0; idx < value.size(); ++idx) {
|
||||||
|
if (idx != 0U) {
|
||||||
|
out << ", ";
|
||||||
|
}
|
||||||
|
out << value[static_cast<json::size_type>(idx)].dump();
|
||||||
|
}
|
||||||
|
out << ']';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
out << "[\n";
|
||||||
|
bool first = true;
|
||||||
|
for (const auto& item : value) {
|
||||||
|
if (!first) {
|
||||||
|
out << ",\n";
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
out << child_indent_str;
|
||||||
|
dump_compact_json(out, item, child_indent, indent_step);
|
||||||
|
}
|
||||||
|
out << '\n' << indent_str << ']';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
out << value.dump();
|
||||||
|
}
|
||||||
|
|
||||||
json serialize_tactile_frame(const DecodedFrame& frame) {
|
json serialize_tactile_frame(const DecodedFrame& frame) {
|
||||||
json result = {
|
json result = {
|
||||||
{"pts", frame.pts},
|
{"pts", frame.pts},
|
||||||
@@ -165,7 +233,8 @@ WriteResult JsonWritter::write_once(const std::string& path,
|
|||||||
return {false, "failed to open export file", path};
|
return {false, "failed to open export file", path};
|
||||||
}
|
}
|
||||||
|
|
||||||
stream << std::setw(2) << root;
|
dump_compact_json(stream, root);
|
||||||
|
stream << '\n';
|
||||||
stream.flush();
|
stream.flush();
|
||||||
if (!stream.good()) {
|
if (!stream.good()) {
|
||||||
return {false, "failed to write export file", path};
|
return {false, "failed to write export file", path};
|
||||||
|
|||||||
@@ -6,13 +6,22 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <exception>
|
||||||
|
#include <future>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <thread>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QMetaObject>
|
#include <QMetaObject>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QtCore/Qt>
|
#include <QtCore/Qt>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QStandardPaths>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <qsize.h>
|
#include <qsize.h>
|
||||||
@@ -42,6 +51,7 @@
|
|||||||
#include <modern-qt/widget/text-fields.hh>
|
#include <modern-qt/widget/text-fields.hh>
|
||||||
#include <modern-qt/widget/text.hh>
|
#include <modern-qt/widget/text.hh>
|
||||||
#include <modern-qt/widget/select.hh>
|
#include <modern-qt/widget/select.hh>
|
||||||
|
#include "components/ffmsep/presist/presist.hh"
|
||||||
#include "components/ffmsep/tactile/tacdec.hh"
|
#include "components/ffmsep/tactile/tacdec.hh"
|
||||||
|
|
||||||
#define DEBUG 0
|
#define DEBUG 0
|
||||||
@@ -146,7 +156,7 @@ public:
|
|||||||
cfg.packet_queue_capacity = 128;
|
cfg.packet_queue_capacity = 128;
|
||||||
cfg.frame_queue_capacity = 32;
|
cfg.frame_queue_capacity = 32;
|
||||||
cfg.slave_request_command.assign(kSlaveRequestCommand.begin(), kSlaveRequestCommand.end());
|
cfg.slave_request_command.assign(kSlaveRequestCommand.begin(), kSlaveRequestCommand.end());
|
||||||
cfg.slave_request_interval = std::chrono::milliseconds{200};
|
cfg.slave_request_interval = 3ms;
|
||||||
|
|
||||||
reset_core();
|
reset_core();
|
||||||
core_ = std::make_unique<ffmsep::CPStreamCore>();
|
core_ = std::make_unique<ffmsep::CPStreamCore>();
|
||||||
@@ -175,14 +185,29 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
reset_core();
|
if (!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;
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
core_->set_frame_callback({});
|
||||||
|
if (core_->is_running()) {
|
||||||
|
core_->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
core_->clear_frames();
|
||||||
|
|
||||||
|
connected_ = false;
|
||||||
|
active_port_.clear();
|
||||||
|
if (heatmap_data_ && matrix_context_) {
|
||||||
|
heatmap_data_->set(make_flat_points(matrix_context_->get()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool is_running() const noexcept {
|
[[nodiscard]] bool is_running() const noexcept {
|
||||||
return core_ && core_->is_running();
|
return core_ && core_->is_running();
|
||||||
@@ -200,6 +225,21 @@ public:
|
|||||||
return last_error_;
|
return last_error_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::future<ffmsep::persist::WriteResult> export_frames(const QString& path,
|
||||||
|
bool clear_after_export) {
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
return make_failed_future(path, "export path is empty");
|
||||||
|
}
|
||||||
|
if (!core_) {
|
||||||
|
return make_failed_future(path, "stream is not active");
|
||||||
|
}
|
||||||
|
if (core_->recorded_frame_count() == 0U) {
|
||||||
|
return make_failed_future(path, "no tactile frames recorded");
|
||||||
|
}
|
||||||
|
const auto normalized = QDir::toNativeSeparators(path);
|
||||||
|
return core_->export_recorded_frames(normalized.toStdString(), clear_after_export);
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void reset_core() {
|
void reset_core() {
|
||||||
connected_ = false;
|
connected_ = false;
|
||||||
@@ -380,6 +420,20 @@ private:
|
|||||||
QString active_port_;
|
QString active_port_;
|
||||||
QString last_error_;
|
QString last_error_;
|
||||||
bool connected_ = false;
|
bool connected_ = false;
|
||||||
|
|
||||||
|
static std::future<ffmsep::persist::WriteResult> make_failed_future(
|
||||||
|
const QString& path,
|
||||||
|
std::string message) {
|
||||||
|
std::promise<ffmsep::persist::WriteResult> promise;
|
||||||
|
auto future = promise.get_future();
|
||||||
|
ffmsep::persist::WriteResult result{
|
||||||
|
false,
|
||||||
|
std::move(message),
|
||||||
|
path.toStdString()
|
||||||
|
};
|
||||||
|
promise.set_value(std::move(result));
|
||||||
|
return future;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SensorUiState {
|
struct SensorUiState {
|
||||||
@@ -482,7 +536,7 @@ static auto ComConfigComponent(ThemeManager& manager) {
|
|||||||
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::kRegularExtraSmallFont },
|
||||||
MutableForward {
|
MutableForward {
|
||||||
icon_button::pro::FontIcon {},
|
icon_button::pro::FontIcon {},
|
||||||
link_icon_context,
|
link_icon_context,
|
||||||
@@ -513,18 +567,15 @@ static auto ComConfigComponent(ThemeManager& manager) {
|
|||||||
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::kRegularExtraSmallFont },
|
||||||
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) {
|
||||||
@@ -539,15 +590,74 @@ static auto ComConfigComponent(ThemeManager& manager) {
|
|||||||
},
|
},
|
||||||
lnpro::Item<FilledButton> {
|
lnpro::Item<FilledButton> {
|
||||||
fbpro::ThemeManager {manager},
|
fbpro::ThemeManager {manager},
|
||||||
fbpro::FixedSize {40, 40},
|
fbpro::FixedSize {40, 40},
|
||||||
fbpro::Color {FilledButton::Color::TONAL},
|
fbpro::Radius {8.0},
|
||||||
fbpro::Font {material::kRoundSmallFont},
|
// fbpro::Color { IconButton::Color::TONAL },
|
||||||
fbpro::FontIcon {material::icon::kRefresh},
|
fbpro::Font { material::kRegularExtraSmallFont },
|
||||||
fbpro::Clickable {
|
fbpro::Text { "drive_file_move" },
|
||||||
[] {
|
fbpro::Clickable {[&sensor] {
|
||||||
qDebug() << "export";
|
auto* controller = sensor.controller.get();
|
||||||
|
if (!controller) {
|
||||||
|
QMessageBox::warning(nullptr,
|
||||||
|
QStringLiteral("导出失败"),
|
||||||
|
QStringLiteral("当前串流尚未初始化。"));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
const auto documents = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
|
||||||
|
const auto timestamp = QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd_HHmmss"));
|
||||||
|
QString suggested_name = QStringLiteral("touchsensor_%1.json").arg(timestamp);
|
||||||
|
QString initial_path = documents.isEmpty()
|
||||||
|
? suggested_name
|
||||||
|
: QDir(documents).filePath(suggested_name);
|
||||||
|
|
||||||
|
const QString chosen_path = QFileDialog::getSaveFileName(
|
||||||
|
nullptr,
|
||||||
|
QStringLiteral("导出触觉帧"),
|
||||||
|
initial_path,
|
||||||
|
QStringLiteral("JSON 文件 (*.json)"));
|
||||||
|
if (chosen_path.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto future = controller->export_frames(chosen_path, false);
|
||||||
|
std::thread([future = std::move(future)]() mutable {
|
||||||
|
ffmsep::persist::WriteResult result{};
|
||||||
|
try {
|
||||||
|
result = future.get();
|
||||||
|
} catch (const std::exception& ex) {
|
||||||
|
result.ok = false;
|
||||||
|
result.error = ex.what();
|
||||||
|
} catch (...) {
|
||||||
|
result.ok = false;
|
||||||
|
result.error = "unknown export failure";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto* app = QCoreApplication::instance()) {
|
||||||
|
QMetaObject::invokeMethod(app,
|
||||||
|
[res = std::move(result)]() {
|
||||||
|
if (res.ok) {
|
||||||
|
QMessageBox::information(
|
||||||
|
nullptr,
|
||||||
|
QStringLiteral("导出成功"),
|
||||||
|
QStringLiteral("触觉帧已导出至:\n%1")
|
||||||
|
.arg(QString::fromStdString(res.path)));
|
||||||
|
} else {
|
||||||
|
const auto error = QString::fromStdString(
|
||||||
|
res.error.empty() ? std::string{"unknown error"} : res.error);
|
||||||
|
const auto target = QString::fromStdString(res.path);
|
||||||
|
const auto message = target.isEmpty()
|
||||||
|
? QStringLiteral("原因:%1").arg(error)
|
||||||
|
: QStringLiteral("原因:%1\n目标:%2").arg(error, target);
|
||||||
|
QMessageBox::warning(nullptr,
|
||||||
|
QStringLiteral("导出失败"),
|
||||||
|
message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Qt::QueuedConnection);
|
||||||
|
}
|
||||||
|
}).detach();
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return new Widget {
|
return new Widget {
|
||||||
|
|||||||
@@ -96,6 +96,13 @@ namespace material {
|
|||||||
inline const auto kOutlinedLargeFont = outlined::font_3;
|
inline const auto kOutlinedLargeFont = outlined::font_3;
|
||||||
inline const auto kOutlinedExtraLargeFont = outlined::font_4;
|
inline const auto kOutlinedExtraLargeFont = outlined::font_4;
|
||||||
|
|
||||||
|
constexpr auto kRegularFontName = regular::font;
|
||||||
|
inline const auto kRegularExtraSmallFont = regular::font_0;
|
||||||
|
inline const auto kRegularSmallFont = regular::font_1;
|
||||||
|
inline const auto kRegularMediumFont = regular::font_2;
|
||||||
|
inline const auto kRegularLargeFont = regular::font_3;
|
||||||
|
inline const auto kRegularExtraLargeFont = regular::font_4;
|
||||||
|
|
||||||
namespace icon {
|
namespace icon {
|
||||||
|
|
||||||
// Function
|
// Function
|
||||||
@@ -140,6 +147,7 @@ namespace material {
|
|||||||
constexpr auto kLogout = "logout";
|
constexpr auto kLogout = "logout";
|
||||||
constexpr auto kRoutine = "routine";
|
constexpr auto kRoutine = "routine";
|
||||||
constexpr auto kDarkMode = "dark_mode";
|
constexpr auto kDarkMode = "dark_mode";
|
||||||
|
constexpr auto kFileExport = "file_export";
|
||||||
|
|
||||||
// File
|
// File
|
||||||
constexpr auto kFolder = "folder";
|
constexpr auto kFolder = "folder";
|
||||||
|
|||||||
Reference in New Issue
Block a user