Migrate updater LAN and devkit features from old repo
This commit is contained in:
1041
src-tauri/Cargo.lock
generated
1041
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -14,14 +14,26 @@ edition = "2021"
|
||||
name = "tauri_demo_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
devkit = ["dep:tonic", "dep:prost", "dep:prost-types", "dep:async-stream", "dep:dirs"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
tonic-build = { version = "0.12" }
|
||||
protoc-bin-vendored = "3"
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["tray-icon"] }
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-process = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
anyhow = "1.0.102"
|
||||
tonic = { version = "0.12", optional = true }
|
||||
prost = { version = "0.13", optional = true }
|
||||
prost-types = { version = "0.13", optional = true }
|
||||
async-stream = { version = "0.3", optional = true }
|
||||
dirs = { version = "6", optional = true }
|
||||
tokio-serial = { version = "5.4.5" }
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
async-trait = "0.1.89"
|
||||
@@ -33,3 +45,12 @@ humantime = "2.3.0"
|
||||
csv = "1.4.0"
|
||||
chrono = "0.4.44"
|
||||
crc = "3.4.0"
|
||||
axum = { version = "0.8", features = ["ws"] }
|
||||
tower-http = { version = "0.6", features = ["cors"] }
|
||||
futures-util = "0.3"
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
rand = "0.8"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-updater = "2"
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
fn main() {
|
||||
if std::env::var("CARGO_FEATURE_DEVKIT").is_ok() {
|
||||
let protoc = protoc_bin_vendored::protoc_bin_path()
|
||||
.unwrap_or_else(|error| panic!("Failed to resolve bundled protoc: {error}"));
|
||||
std::env::set_var("PROTOC", protoc);
|
||||
tonic_build::compile_protos("proto/sensor_stream.proto")
|
||||
.unwrap_or_else(|error| panic!("Failed to compile devkit proto: {error}"));
|
||||
}
|
||||
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"core:window:allow-inner-size",
|
||||
"core:window:allow-set-size",
|
||||
"core:window:allow-start-dragging",
|
||||
"opener:default"
|
||||
"opener:default",
|
||||
"process:default",
|
||||
"updater:default"
|
||||
]
|
||||
}
|
||||
|
||||
49
src-tauri/proto/sensor_stream.proto
Normal file
49
src-tauri/proto/sensor_stream.proto
Normal file
@@ -0,0 +1,49 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package sensor_stream;
|
||||
|
||||
// 传感器数据推送服务 —— Rust 端作为 gRPC client 推送实时帧
|
||||
service SensorPush {
|
||||
rpc Upload (stream SensorFrame) returns (UploadResponse);
|
||||
}
|
||||
|
||||
// 导出后处理服务 —— Rust 导出 CSV 后将路径发给 Python 做梯度过滤
|
||||
service ExportProcessor {
|
||||
rpc ProcessFile (ProcessRequest) returns (ProcessResponse);
|
||||
}
|
||||
|
||||
// 一帧传感器数据
|
||||
message SensorFrame {
|
||||
uint64 seq = 1;
|
||||
uint64 timestamp_ms = 2;
|
||||
uint32 rows = 3;
|
||||
uint32 cols = 4;
|
||||
repeated uint32 matrix = 5;
|
||||
double resultant_force = 6;
|
||||
uint32 dts_ms = 7;
|
||||
}
|
||||
|
||||
// 上传确认响应
|
||||
message UploadResponse {
|
||||
bool ok = 1;
|
||||
uint64 frames_received = 2;
|
||||
string message = 3;
|
||||
}
|
||||
|
||||
// 导出处理请求
|
||||
message ProcessRequest {
|
||||
string csv_path = 1; // 导出的 CSV 文件路径
|
||||
bool save_as_xlsx = 2; // 是否以 xlsx 保存(删除源 CSV)
|
||||
}
|
||||
|
||||
// 导出处理响应
|
||||
message ProcessResponse {
|
||||
bool ok = 1;
|
||||
string output_path = 2; // 输出文件路径
|
||||
uint32 groups_used = 3; // 分组数
|
||||
double mean_value = 4; // 均值
|
||||
double threshold = 5; // 梯度阈值
|
||||
uint32 rows_total = 6; // 原始行数
|
||||
uint32 rows_kept = 7; // 保留行数
|
||||
string message = 8;
|
||||
}
|
||||
BIN
src-tauri/resources/je-skin-devkit-server.exe
Normal file
BIN
src-tauri/resources/je-skin-devkit-server.exe
Normal file
Binary file not shown.
47
src-tauri/src/commands/devkit.rs
Normal file
47
src-tauri/src/commands/devkit.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
//! DevKit Tauri 命令
|
||||
//!
|
||||
//! 仅在 `devkit` feature 启用时编译。
|
||||
|
||||
use tauri::State;
|
||||
|
||||
use crate::devkit::{DevKitConfig, DevKitState, DevKitStatusSnapshot, ExportProcessResult};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn devkit_status(state: State<'_, DevKitState>) -> DevKitStatusSnapshot {
|
||||
state.status()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn devkit_start(state: State<'_, DevKitState>, port: Option<u16>) -> Result<DevKitStatusSnapshot, String> {
|
||||
let target_port = port.unwrap_or(50051);
|
||||
state.start(target_port).await?;
|
||||
Ok(state.status())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn devkit_stop(state: State<'_, DevKitState>) -> Result<DevKitStatusSnapshot, String> {
|
||||
state.stop().await?;
|
||||
Ok(state.status())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn devkit_get_config(state: State<'_, DevKitState>) -> DevKitConfig {
|
||||
state.get_config()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn devkit_set_config(state: State<'_, DevKitState>, config: DevKitConfig) -> Result<DevKitConfig, String> {
|
||||
state.set_config(config)?;
|
||||
Ok(state.get_config())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn devkit_process_export(
|
||||
state: State<'_, DevKitState>,
|
||||
csv_path: String,
|
||||
save_as_xlsx: Option<bool>,
|
||||
) -> Result<ExportProcessResult, String> {
|
||||
let config = state.get_config();
|
||||
let use_xlsx = save_as_xlsx.unwrap_or(config.save_as_xlsx);
|
||||
state.process_export(&csv_path, use_xlsx).await
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
pub mod file_explorer;
|
||||
pub mod serial;
|
||||
pub mod window;
|
||||
|
||||
#[cfg(feature = "devkit")]
|
||||
pub mod devkit;
|
||||
|
||||
268
src-tauri/src/devkit/client.rs
Normal file
268
src-tauri/src/devkit/client.rs
Normal file
@@ -0,0 +1,268 @@
|
||||
//! DevKit gRPC Client
|
||||
//!
|
||||
//! Rust 端作为 gRPC client:
|
||||
//! 1. 以 client-streaming 方式推送实时帧(SensorPush.Upload)
|
||||
//! 2. 以 unary 方式发送导出文件路径做后处理(ExportProcessor.ProcessFile)
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task::JoinHandle;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::proto::sensor_push_client::SensorPushClient;
|
||||
use super::proto::export_processor_client::ExportProcessorClient;
|
||||
use super::proto::{ProcessRequest, SensorFrame};
|
||||
|
||||
// ── DevKit 配置 ────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DevKitConfig {
|
||||
/// 导出过滤抬起:导出 CSV 后自动调用 Python 做梯度过滤
|
||||
pub filter_lift_enabled: bool,
|
||||
/// 以 xlsx 保存:Python 处理后输出 xlsx 并删除源 CSV
|
||||
pub save_as_xlsx: bool,
|
||||
}
|
||||
|
||||
impl Default for DevKitConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
filter_lift_enabled: true,
|
||||
save_as_xlsx: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DevKitConfig {
|
||||
fn config_path() -> PathBuf {
|
||||
let base = dirs::config_dir()
|
||||
.or_else(|| dirs::data_dir())
|
||||
.unwrap_or_else(|| PathBuf::from("."));
|
||||
base.join("JE-Skin").join("devkit_config.json")
|
||||
}
|
||||
|
||||
/// 从文件加载配置,失败则返回默认值
|
||||
pub fn load() -> Self {
|
||||
let path = Self::config_path();
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
|
||||
Err(_) => Self::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存配置到文件
|
||||
pub fn save(&self) -> Result<(), String> {
|
||||
let path = Self::config_path();
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("Failed to create config dir: {e}"))?;
|
||||
}
|
||||
let json = serde_json::to_string_pretty(self)
|
||||
.map_err(|e| format!("Failed to serialize config: {e}"))?;
|
||||
std::fs::write(&path, json)
|
||||
.map_err(|e| format!("Failed to write config: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ── 导出处理结果 ───────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExportProcessResult {
|
||||
pub ok: bool,
|
||||
pub output_path: String,
|
||||
pub groups_used: u32,
|
||||
pub mean_value: f64,
|
||||
pub threshold: f64,
|
||||
pub rows_total: u32,
|
||||
pub rows_kept: u32,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
// ── Tauri 状态 ─────────────────────────────────────────────────────
|
||||
|
||||
/// DevKit 全局状态,由 Tauri manage
|
||||
#[derive(Clone)]
|
||||
pub struct DevKitState {
|
||||
pub running: Arc<AtomicBool>,
|
||||
pub port: Arc<std::sync::Mutex<u16>>,
|
||||
pub frame_count: Arc<AtomicU32>,
|
||||
pub config: Arc<std::sync::Mutex<DevKitConfig>>,
|
||||
frame_tx: Arc<std::sync::Mutex<Option<mpsc::Sender<SensorFrame>>>>,
|
||||
client_handle: Arc<std::sync::Mutex<Option<JoinHandle<()>>>>,
|
||||
}
|
||||
|
||||
impl Default for DevKitState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
running: Arc::new(AtomicBool::new(false)),
|
||||
port: Arc::new(std::sync::Mutex::new(50051)),
|
||||
frame_count: Arc::new(AtomicU32::new(0)),
|
||||
config: Arc::new(std::sync::Mutex::new(DevKitConfig::load())),
|
||||
frame_tx: Arc::new(std::sync::Mutex::new(None)),
|
||||
client_handle: Arc::new(std::sync::Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 前端查询到的状态快照
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DevKitStatusSnapshot {
|
||||
pub enabled: bool,
|
||||
pub running: bool,
|
||||
pub port: u16,
|
||||
pub frames_sent: u32,
|
||||
pub config: DevKitConfig,
|
||||
}
|
||||
|
||||
impl DevKitState {
|
||||
pub fn status(&self) -> DevKitStatusSnapshot {
|
||||
let cfg = self.config.lock().unwrap().clone();
|
||||
DevKitStatusSnapshot {
|
||||
enabled: true,
|
||||
running: self.running.load(Ordering::SeqCst),
|
||||
port: *self.port.lock().unwrap(),
|
||||
frames_sent: self.frame_count.load(Ordering::SeqCst),
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取当前配置
|
||||
pub fn get_config(&self) -> DevKitConfig {
|
||||
self.config.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
/// 更新配置并持久化
|
||||
pub fn set_config(&self, new_config: DevKitConfig) -> Result<(), String> {
|
||||
new_config.save()?;
|
||||
*self.config.lock().unwrap() = new_config;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 启动 gRPC client,连接到 Python server 并开始推送数据
|
||||
pub async fn start(&self, port: u16) -> Result<(), String> {
|
||||
if self.running.load(Ordering::SeqCst) {
|
||||
return Err("AlreadyRunning".into());
|
||||
}
|
||||
|
||||
let addr = format!("http://127.0.0.1:{port}");
|
||||
*self.port.lock().unwrap() = port;
|
||||
self.running.store(true, Ordering::SeqCst);
|
||||
self.frame_count.store(0, Ordering::SeqCst);
|
||||
|
||||
// mpsc channel: 主线程 send 帧 → gRPC task 推送给 Python
|
||||
let (tx, rx) = mpsc::channel::<SensorFrame>(512);
|
||||
*self.frame_tx.lock().unwrap() = Some(tx);
|
||||
|
||||
let running = Arc::clone(&self.running);
|
||||
let frame_count = Arc::clone(&self.frame_count);
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
if let Err(e) = run_grpc_upload(addr, rx, frame_count).await {
|
||||
::log::error!("DevKit gRPC upload error: {e:?}");
|
||||
}
|
||||
running.store(false, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
*self.client_handle.lock().unwrap() = Some(handle);
|
||||
::log::info!("DevKit gRPC client started, connecting to 127.0.0.1:{port}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 停止 gRPC client
|
||||
pub async fn stop(&self) -> Result<(), String> {
|
||||
if !self.running.load(Ordering::SeqCst) {
|
||||
return Err("NotRunning".into());
|
||||
}
|
||||
|
||||
*self.frame_tx.lock().unwrap() = None;
|
||||
|
||||
if let Some(handle) = self.client_handle.lock().unwrap().take() {
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
self.running.store(false, Ordering::SeqCst);
|
||||
::log::info!("DevKit gRPC client stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 推送一帧数据到 gRPC stream(由主线程调用)
|
||||
pub fn push_frame(&self, frame: SensorFrame) {
|
||||
if !self.running.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
if let Some(tx) = self.frame_tx.lock().unwrap().as_ref() {
|
||||
let _ = tx.try_send(frame);
|
||||
}
|
||||
}
|
||||
|
||||
/// 调用 Python ExportProcessor.ProcessFile 做导出后处理(unary)
|
||||
pub async fn process_export(
|
||||
&self,
|
||||
csv_path: &str,
|
||||
save_as_xlsx: bool,
|
||||
) -> Result<ExportProcessResult, String> {
|
||||
let port = *self.port.lock().unwrap();
|
||||
let addr = format!("http://127.0.0.1:{port}");
|
||||
|
||||
let mut client = ExportProcessorClient::connect(addr)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to connect to DevKit server: {e}"))?;
|
||||
|
||||
let request = ProcessRequest {
|
||||
csv_path: csv_path.to_string(),
|
||||
save_as_xlsx,
|
||||
};
|
||||
|
||||
let response = client
|
||||
.process_file(request)
|
||||
.await
|
||||
.map_err(|e| format!("ProcessFile RPC failed: {e}"))?;
|
||||
|
||||
let resp = response.into_inner();
|
||||
Ok(ExportProcessResult {
|
||||
ok: resp.ok,
|
||||
output_path: resp.output_path,
|
||||
groups_used: resp.groups_used,
|
||||
mean_value: resp.mean_value,
|
||||
threshold: resp.threshold,
|
||||
rows_total: resp.rows_total,
|
||||
rows_kept: resp.rows_kept,
|
||||
message: resp.message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ── gRPC Upload Client ─────────────────────────────────────────────
|
||||
|
||||
async fn run_grpc_upload(
|
||||
addr: String,
|
||||
mut rx: mpsc::Receiver<SensorFrame>,
|
||||
frame_count: Arc<AtomicU32>,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut client = SensorPushClient::connect(addr.clone()).await?;
|
||||
|
||||
let stream = async_stream::stream! {
|
||||
while let Some(frame) = rx.recv().await {
|
||||
frame_count.fetch_add(1, Ordering::SeqCst);
|
||||
yield frame;
|
||||
}
|
||||
};
|
||||
|
||||
let response = client.upload(stream).await?;
|
||||
let resp = response.into_inner();
|
||||
|
||||
::log::info!(
|
||||
"DevKit upload complete: ok={}, frames={}, msg={}",
|
||||
resp.ok,
|
||||
resp.frames_received,
|
||||
resp.message
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
13
src-tauri/src/devkit/mod.rs
Normal file
13
src-tauri/src/devkit/mod.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
//! Develop Kit 模块
|
||||
//!
|
||||
//! 仅在 `devkit` feature 启用时编译。
|
||||
//! Rust 端作为 gRPC client,将传感器压力矩阵数据实时推送给 Python gRPC server。
|
||||
|
||||
mod client;
|
||||
|
||||
pub use client::{DevKitConfig, DevKitState, DevKitStatusSnapshot, ExportProcessResult};
|
||||
|
||||
// 导入 tonic 生成的 gRPC 代码
|
||||
pub mod proto {
|
||||
tonic::include_proto!("sensor_stream");
|
||||
}
|
||||
1250
src-tauri/src/lan_game.rs
Normal file
1250
src-tauri/src/lan_game.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,27 +1,147 @@
|
||||
mod commands;
|
||||
pub mod serial_core;
|
||||
mod lan_game;
|
||||
pub mod log;
|
||||
pub mod serial_core;
|
||||
|
||||
#[cfg(feature = "devkit")]
|
||||
pub mod devkit;
|
||||
|
||||
use commands::serial::SerialConnectionState;
|
||||
#[cfg(feature = "devkit")]
|
||||
use tauri::Manager;
|
||||
|
||||
#[cfg(feature = "devkit")]
|
||||
fn start_server_exe(exe_path: &std::path::Path) {
|
||||
let mut command = std::process::Command::new(exe_path);
|
||||
command.arg("--port").arg("50051");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
command.creation_flags(0x08000000);
|
||||
}
|
||||
|
||||
match command.spawn() {
|
||||
Ok(_) => ::log::info!("DevKit Python server started: {}", exe_path.display()),
|
||||
Err(error) => ::log::warn!("Failed to start DevKit Python server: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
let builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.manage(SerialConnectionState::default())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::file_explorer::file_explorer_list,
|
||||
commands::serial::serial_enum,
|
||||
commands::serial::serial_connect,
|
||||
commands::serial::serial_disconnect,
|
||||
commands::serial::serial_export_csv,
|
||||
commands::serial::serial_has_record_data,
|
||||
commands::serial::serial_export_csv_to_path,
|
||||
commands::serial::serial_import_csv,
|
||||
commands::serial::serial_import_csv_from_path,
|
||||
commands::window::win_minimize,
|
||||
commands::window::win_toggle_maximize,
|
||||
commands::window::win_close
|
||||
])
|
||||
.plugin(tauri_plugin_opener::init());
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
let builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
let builder = builder;
|
||||
|
||||
#[cfg(feature = "devkit")]
|
||||
let builder = {
|
||||
let devkit_state = devkit::DevKitState::default();
|
||||
let devkit_state_clone = devkit_state.clone();
|
||||
|
||||
builder.manage(devkit_state).setup(move |app| {
|
||||
tauri::async_runtime::spawn(async {
|
||||
if let Err(error) = lan_game::serve().await {
|
||||
::log::error!("LAN game server failed: {error:?}");
|
||||
}
|
||||
});
|
||||
|
||||
let resource_dir = app
|
||||
.path()
|
||||
.resource_dir()
|
||||
.unwrap_or_else(|_| std::path::PathBuf::from("./resources"));
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
#[cfg(target_os = "windows")]
|
||||
let exe_name = "je-skin-devkit-server.exe";
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let exe_name = "je-skin-devkit-server";
|
||||
|
||||
let bundled_exe = resource_dir.join(exe_name);
|
||||
let fallback_exe = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|path| path.parent().map(|parent| parent.join(exe_name)));
|
||||
|
||||
let server_exe = if bundled_exe.exists() {
|
||||
Some(bundled_exe)
|
||||
} else {
|
||||
fallback_exe.filter(|path| path.exists())
|
||||
};
|
||||
|
||||
if let Some(exe_path) = server_exe {
|
||||
start_server_exe(&exe_path);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1200)).await;
|
||||
} else {
|
||||
::log::info!("DevKit Python server not found, skipping auto-start");
|
||||
}
|
||||
|
||||
if let Err(error) = devkit_state_clone.start(50051).await {
|
||||
::log::warn!("DevKit auto-start failed: {error}");
|
||||
} else {
|
||||
::log::info!("DevKit auto-started on 127.0.0.1:50051");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "devkit"))]
|
||||
let builder = builder.setup(|_app| {
|
||||
tauri::async_runtime::spawn(async {
|
||||
if let Err(error) = lan_game::serve().await {
|
||||
::log::error!("LAN game server failed: {error:?}");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
#[cfg(feature = "devkit")]
|
||||
let builder = builder.invoke_handler(tauri::generate_handler![
|
||||
commands::file_explorer::file_explorer_list,
|
||||
commands::serial::serial_enum,
|
||||
commands::serial::serial_connect,
|
||||
commands::serial::serial_disconnect,
|
||||
commands::serial::serial_export_csv,
|
||||
commands::serial::serial_has_record_data,
|
||||
commands::serial::serial_export_csv_to_path,
|
||||
commands::serial::serial_import_csv,
|
||||
commands::serial::serial_import_csv_from_path,
|
||||
commands::window::win_minimize,
|
||||
commands::window::win_toggle_maximize,
|
||||
commands::window::win_close,
|
||||
commands::devkit::devkit_status,
|
||||
commands::devkit::devkit_start,
|
||||
commands::devkit::devkit_stop,
|
||||
commands::devkit::devkit_get_config,
|
||||
commands::devkit::devkit_set_config,
|
||||
commands::devkit::devkit_process_export
|
||||
]);
|
||||
|
||||
#[cfg(not(feature = "devkit"))]
|
||||
let builder = builder.invoke_handler(tauri::generate_handler![
|
||||
commands::file_explorer::file_explorer_list,
|
||||
commands::serial::serial_enum,
|
||||
commands::serial::serial_connect,
|
||||
commands::serial::serial_disconnect,
|
||||
commands::serial::serial_export_csv,
|
||||
commands::serial::serial_has_record_data,
|
||||
commands::serial::serial_export_csv_to_path,
|
||||
commands::serial::serial_import_csv,
|
||||
commands::serial::serial_import_csv_from_path,
|
||||
commands::window::win_minimize,
|
||||
commands::window::win_toggle_maximize,
|
||||
commands::window::win_close
|
||||
]);
|
||||
|
||||
builder
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
@@ -3,7 +3,16 @@ use fern::{
|
||||
Dispatch,
|
||||
};
|
||||
use log::debug;
|
||||
use std::time::SystemTime;
|
||||
use std::{path::{Path, PathBuf}, time::SystemTime};
|
||||
|
||||
fn log_directory() -> PathBuf {
|
||||
let base_dir = std::env::var_os("LOCALAPPDATA")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".local/share")))
|
||||
.unwrap_or_else(std::env::temp_dir);
|
||||
|
||||
base_dir.join("JE-Skin").join("logs")
|
||||
}
|
||||
pub fn setup_logger() {
|
||||
let colors_line = ColoredLevelConfig::new()
|
||||
.error(Color::Red)
|
||||
@@ -38,7 +47,11 @@ pub fn setup_logger() {
|
||||
// .chain(fern::DateBased::new("program.log", "%Y-%m-%d"))
|
||||
// .apply()
|
||||
// .unwrap();
|
||||
let log_path = std::env::temp_dir().join("program.log");
|
||||
let log_dir = log_directory();
|
||||
if let Err(error) = std::fs::create_dir_all(&log_dir) {
|
||||
eprintln!("failed to create log_directory {}: {error}", log_dir.display());
|
||||
}
|
||||
// let log_path = std::env::temp_dir().join("program.log");
|
||||
let file_config = fern::Dispatch::new()
|
||||
.format(move |out, message, record| {
|
||||
out.finish(format_args!(
|
||||
@@ -50,7 +63,7 @@ pub fn setup_logger() {
|
||||
));
|
||||
})
|
||||
.level(level)
|
||||
.chain(fern::DateBased::new(&log_path, "%Y-%m-%d"));
|
||||
.chain(fern::DateBased::new(log_dir.join("program.log"), "%Y-%m-%d"));
|
||||
|
||||
Dispatch::new()
|
||||
.level(log::LevelFilter::Debug)
|
||||
|
||||
@@ -4,15 +4,21 @@ use crate::serial_core::frame::{FrameHandler, TactileAFrame, TestFrame};
|
||||
use crate::serial_core::model::{HudChartState, HudPacket};
|
||||
use crate::serial_core::record::Recording;
|
||||
use crate::serial_core::record::{FrameTiming, RecordedFrame};
|
||||
#[cfg(feature = "devkit")]
|
||||
use crate::devkit::{proto::SensorFrame, DevKitState};
|
||||
use anyhow::Result;
|
||||
use std::future::pending;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
#[cfg(feature = "devkit")]
|
||||
use tauri::Manager;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::time::{self, Duration, MissedTickBehavior};
|
||||
use tokio_serial::SerialStream;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
#[cfg(feature = "devkit")]
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
pub enum PollMode<F> {
|
||||
Disable,
|
||||
@@ -271,6 +277,8 @@ where
|
||||
let force = raw_to_g1(summary as u32);
|
||||
chart_state.record_summary(force as f32);
|
||||
chart_state.record_pressure_matrix(vals.as_slice());
|
||||
#[cfg(feature = "devkit")]
|
||||
push_devkit_frame(&app, vals.as_slice(), frame.dts_ms(), force);
|
||||
Some(vec![summary])
|
||||
} else {
|
||||
None
|
||||
@@ -286,6 +294,58 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "devkit")]
|
||||
fn push_devkit_frame(app: &AppHandle, values: &[i32], dts_ms: u64, resultant_force: f64) {
|
||||
let devkit_state = app.state::<DevKitState>();
|
||||
if !devkit_state.running.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let (rows, cols) = infer_matrix_shape(values.len());
|
||||
let timestamp_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as u64;
|
||||
|
||||
let seq = timestamp_ms;
|
||||
let matrix = values
|
||||
.iter()
|
||||
.map(|value| (*value).max(0) as u32)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
devkit_state.push_frame(SensorFrame {
|
||||
seq,
|
||||
timestamp_ms,
|
||||
rows,
|
||||
cols,
|
||||
matrix,
|
||||
resultant_force,
|
||||
dts_ms: dts_ms as u32,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "devkit")]
|
||||
fn infer_matrix_shape(len: usize) -> (u32, u32) {
|
||||
if len == 84 {
|
||||
return (12, 7);
|
||||
}
|
||||
|
||||
if len == 0 {
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
let mut best = (len, 1);
|
||||
let mut factor = 1usize;
|
||||
while factor * factor <= len {
|
||||
if len % factor == 0 {
|
||||
best = (len / factor, factor);
|
||||
}
|
||||
factor += 1;
|
||||
}
|
||||
|
||||
(best.0 as u32, best.1 as u32)
|
||||
}
|
||||
|
||||
fn raw_to_g1(raw: u32) -> f64 {
|
||||
const X: [u32; 12] = [
|
||||
0, 84402, 117218, 140176, 159126, 175812, 191484, 208758, 224703, 252448, 302361, 352703,
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"createUpdaterArtifacts": true,
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
@@ -31,6 +32,27 @@
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"windows": {
|
||||
"nsis": {
|
||||
"installMode": "both",
|
||||
"displayLanguageSelector": false,
|
||||
"installerIcon": "icons/icon.ico"
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
"resources/je-skin-devkit-server.exe"
|
||||
]
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"windows": {
|
||||
"installMode": "passive"
|
||||
},
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDkwODM1QkFEODI2NkZENgpSV1RXYnliWXVqVUlDUVRxbjlseDNDNjhQTGpDYis4TEZMeUk2WVhiMEhTRWJhN3hGRnQ3TTJtcwo=",
|
||||
"endpoints": [
|
||||
"https://je-skin.cn-nb1.rains3.com/latest.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user