feat:export data while running or close

This commit is contained in:
2025-11-05 10:42:43 +08:00
parent 7517f79c07
commit b2350a3b35
3 changed files with 210 additions and 23 deletions

View File

@@ -6,6 +6,7 @@
#include "components/ffmsep/cpstream_core.hh"
#include <algorithm>
#include <chrono>
#include <filesystem>
#include <fstream>
@@ -19,6 +20,73 @@ namespace {
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 result = {
{"pts", frame.pts},
@@ -165,7 +233,8 @@ WriteResult JsonWritter::write_once(const std::string& path,
return {false, "failed to open export file", path};
}
stream << std::setw(2) << root;
dump_compact_json(stream, root);
stream << '\n';
stream.flush();
if (!stream.good()) {
return {false, "failed to write export file", path};

View File

@@ -6,13 +6,22 @@
#include <array>
#include <cmath>
#include <cstdint>
#include <exception>
#include <future>
#include <memory>
#include <mutex>
#include <thread>
#include <QString>
#include <QObject>
#include <QMetaObject>
#include <QStringList>
#include <QtCore/Qt>
#include <QCoreApplication>
#include <QDateTime>
#include <QDir>
#include <QFileDialog>
#include <QMessageBox>
#include <QStandardPaths>
#include <optional>
#include <string>
#include <qsize.h>
@@ -42,6 +51,7 @@
#include <modern-qt/widget/text-fields.hh>
#include <modern-qt/widget/text.hh>
#include <modern-qt/widget/select.hh>
#include "components/ffmsep/presist/presist.hh"
#include "components/ffmsep/tactile/tacdec.hh"
#define DEBUG 0
@@ -146,7 +156,7 @@ public:
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};
cfg.slave_request_interval = 3ms;
reset_core();
core_ = std::make_unique<ffmsep::CPStreamCore>();
@@ -175,14 +185,29 @@ public:
return true;
}
void stop() {
reset_core();
active_port_.clear();
if (heatmap_data_ && matrix_context_) {
heatmap_data_->set(make_flat_points(matrix_context_->get()));
}
connected_ = false;
}
void stop() {
if (!core_) {
active_port_.clear();
if (heatmap_data_ && matrix_context_) {
heatmap_data_->set(make_flat_points(matrix_context_->get()));
}
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 {
return core_ && core_->is_running();
@@ -200,6 +225,21 @@ public:
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:
void reset_core() {
connected_ = false;
@@ -380,6 +420,20 @@ private:
QString active_port_;
QString last_error_;
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 {
@@ -482,7 +536,7 @@ static auto ComConfigComponent(ThemeManager& manager) {
ibpro::ThemeManager {manager},
ibpro::FixedSize {40, 40},
ibpro::Color { IconButton::Color::TONAL },
ibpro::Font { material::kRoundSmallFont },
ibpro::Font { material::kRegularExtraSmallFont },
MutableForward {
icon_button::pro::FontIcon {},
link_icon_context,
@@ -513,18 +567,15 @@ static auto ComConfigComponent(ThemeManager& manager) {
ibpro::ThemeManager { manager },
ibpro::FixedSize { 40, 40 },
ibpro::Color { IconButton::Color::TONAL },
ibpro::Font { material::kRoundSmallFont },
ibpro::Font { material::kRegularExtraSmallFont },
ibpro::FontIcon { material::icon::kRefresh },
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));
}
// 保持原选择(若仍然存在)
if (!sensor.selected_port.isEmpty()) {
const bool exists = ports_list.contains(sensor.selected_port);
if (!exists) {
@@ -539,15 +590,74 @@ static auto ComConfigComponent(ThemeManager& manager) {
},
lnpro::Item<FilledButton> {
fbpro::ThemeManager {manager},
fbpro::FixedSize {40, 40},
fbpro::Color {FilledButton::Color::TONAL},
fbpro::Font {material::kRoundSmallFont},
fbpro::FontIcon {material::icon::kRefresh},
fbpro::Clickable {
[] {
qDebug() << "export";
fbpro::FixedSize {40, 40},
fbpro::Radius {8.0},
// fbpro::Color { IconButton::Color::TONAL },
fbpro::Font { material::kRegularExtraSmallFont },
fbpro::Text { "drive_file_move" },
fbpro::Clickable {[&sensor] {
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 {