feat:export data while running or close
This commit is contained in:
@@ -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};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -96,6 +96,13 @@ namespace material {
|
||||
inline const auto kOutlinedLargeFont = outlined::font_3;
|
||||
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 {
|
||||
|
||||
// Function
|
||||
@@ -140,6 +147,7 @@ namespace material {
|
||||
constexpr auto kLogout = "logout";
|
||||
constexpr auto kRoutine = "routine";
|
||||
constexpr auto kDarkMode = "dark_mode";
|
||||
constexpr auto kFileExport = "file_export";
|
||||
|
||||
// File
|
||||
constexpr auto kFolder = "folder";
|
||||
|
||||
Reference in New Issue
Block a user