Migrate updater LAN and devkit features from old repo
This commit is contained in:
BIN
devkit/__pycache__/sensor_stream_pb2.cpython-310.pyc
Normal file
BIN
devkit/__pycache__/sensor_stream_pb2.cpython-310.pyc
Normal file
Binary file not shown.
BIN
devkit/__pycache__/sensor_stream_pb2_grpc.cpython-310.pyc
Normal file
BIN
devkit/__pycache__/sensor_stream_pb2_grpc.cpython-310.pyc
Normal file
Binary file not shown.
24
devkit/build_server.bat
Normal file
24
devkit/build_server.bat
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
@echo off
|
||||||
|
REM ── JE-Skin DevKit: 打包 Python gRPC server 为 exe ──
|
||||||
|
REM 前提: pip install pyinstaller grpcio grpcio-tools openpyxl
|
||||||
|
|
||||||
|
echo [1/3] Generating gRPC stubs...
|
||||||
|
python -m grpc_tools.protoc ^
|
||||||
|
-I../src-tauri/proto ^
|
||||||
|
--python_out=. ^
|
||||||
|
--grpc_python_out=. ^
|
||||||
|
../src-tauri/proto/sensor_stream.proto
|
||||||
|
|
||||||
|
echo [2/3] Building exe with PyInstaller...
|
||||||
|
pyinstaller ^
|
||||||
|
--onefile ^
|
||||||
|
--name je-skin-devkit-server ^
|
||||||
|
--add-data "sensor_stream_pb2*.py;." ^
|
||||||
|
--hidden-import grpc ^
|
||||||
|
--hidden-import openpyxl ^
|
||||||
|
--noconfirm ^
|
||||||
|
sensor_server.py
|
||||||
|
|
||||||
|
echo [3/3] Done!
|
||||||
|
echo Output: dist/je-skin-devkit-server.exe
|
||||||
|
pause
|
||||||
38
devkit/je-skin-devkit-server.spec
Normal file
38
devkit/je-skin-devkit-server.spec
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['sensor_server.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=[],
|
||||||
|
hiddenimports=['grpc', 'openpyxl'],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='je-skin-devkit-server',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=True,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
)
|
||||||
323
devkit/sensor_server.py
Normal file
323
devkit/sensor_server.py
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
"""
|
||||||
|
JE-Skin DevKit — Python gRPC Sensor Server
|
||||||
|
|
||||||
|
提供两个服务:
|
||||||
|
1. SensorPush (streaming) — 接收实时传感器帧
|
||||||
|
2. ExportProcessor (unary) — 处理导出的 CSV 文件:梯度过滤、xlsx 转换
|
||||||
|
|
||||||
|
安装依赖:
|
||||||
|
pip install grpcio grpcio-tools openpyxl
|
||||||
|
|
||||||
|
生成 gRPC 代码:
|
||||||
|
python -m grpc_tools.protoc -I../src-tauri/proto --python_out=. --grpc_python_out=. ../src-tauri/proto/sensor_stream.proto
|
||||||
|
|
||||||
|
启动:
|
||||||
|
python sensor_server.py [--port 50051]
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import statistics
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from concurrent import futures
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import grpc
|
||||||
|
import sensor_stream_pb2
|
||||||
|
import sensor_stream_pb2_grpc
|
||||||
|
|
||||||
|
# ── 梯度过滤逻辑(来自用户的 main.py) ─────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def load_rows(path: Path) -> list[list[str]]:
|
||||||
|
with path.open("r", encoding="utf-8-sig", newline="") as f:
|
||||||
|
return [row for row in csv.reader(f) if row]
|
||||||
|
|
||||||
|
|
||||||
|
def row_sum(row: list[str]) -> float:
|
||||||
|
return sum(float(v) for v in row[1:] if v.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def find_threshold(sum_values: list[float]) -> float:
|
||||||
|
if len(sum_values) < 2:
|
||||||
|
raise ValueError("At least two rows are required.")
|
||||||
|
sorted_v = sorted(sum_values)
|
||||||
|
idx = max(
|
||||||
|
range(len(sorted_v) - 1),
|
||||||
|
key=lambda i: sorted_v[i + 1] - sorted_v[i],
|
||||||
|
)
|
||||||
|
return (sorted_v[idx] + sorted_v[idx + 1]) / 2.0
|
||||||
|
|
||||||
|
|
||||||
|
def extract_press_groups(
|
||||||
|
rows: list[list[str]], sum_values: list[float], threshold: float
|
||||||
|
) -> tuple[list[list[str]], list[float]]:
|
||||||
|
filtered: list[list[str]] = []
|
||||||
|
group_means: list[float] = []
|
||||||
|
current_group: list[float] = []
|
||||||
|
|
||||||
|
for row, total in zip(rows, sum_values):
|
||||||
|
if total >= threshold:
|
||||||
|
filtered.append(row)
|
||||||
|
current_group.append(total)
|
||||||
|
continue
|
||||||
|
if current_group:
|
||||||
|
group_means.append(statistics.fmean(current_group))
|
||||||
|
current_group = []
|
||||||
|
|
||||||
|
if current_group:
|
||||||
|
group_means.append(statistics.fmean(current_group))
|
||||||
|
|
||||||
|
return filtered, group_means
|
||||||
|
|
||||||
|
|
||||||
|
def write_csv(path: Path, rows: list[list[str]]) -> Path:
|
||||||
|
out = path.with_name(f"{path.stem}_filtered.csv")
|
||||||
|
with out.open("w", encoding="utf-8-sig", newline="") as f:
|
||||||
|
csv.writer(f).writerows(rows)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def write_xlsx(path: Path, rows: list[list[str]], stats: dict) -> Path:
|
||||||
|
"""将过滤后的数据和统计信息写入 xlsx"""
|
||||||
|
try:
|
||||||
|
import openpyxl
|
||||||
|
except ImportError:
|
||||||
|
raise RuntimeError("openpyxl is required for xlsx output. Install it with: pip install openpyxl")
|
||||||
|
|
||||||
|
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||||||
|
|
||||||
|
wb = openpyxl.Workbook()
|
||||||
|
|
||||||
|
# Sheet 1: 过滤后的数据
|
||||||
|
ws_data = wb.active
|
||||||
|
ws_data.title = "Filtered Data"
|
||||||
|
for row in rows:
|
||||||
|
ws_data.append([float(c) if c.strip().replace(".", "").replace("-", "").isdigit() else c for c in row])
|
||||||
|
|
||||||
|
# Sheet 2: 统计信息
|
||||||
|
ws_stats = wb.create_sheet("Statistics")
|
||||||
|
header_font = Font(bold=True, size=11)
|
||||||
|
header_fill = PatternFill(start_color="E0E0E0", end_color="E0E0E0", fill_type="solid")
|
||||||
|
|
||||||
|
ws_stats.append(["Parameter", "Value"])
|
||||||
|
ws_stats["A1"].font = header_font
|
||||||
|
ws_stats["A1"].fill = header_fill
|
||||||
|
ws_stats["B1"].font = header_font
|
||||||
|
ws_stats["B1"].fill = header_fill
|
||||||
|
|
||||||
|
stats_rows = [
|
||||||
|
("Source File", stats.get("source_file", "")),
|
||||||
|
("Total Rows", stats.get("rows_total", 0)),
|
||||||
|
("Filtered Rows", stats.get("rows_kept", 0)),
|
||||||
|
("Groups Used", stats.get("groups_used", 0)),
|
||||||
|
("Mean Value", f"{stats.get('mean_value', 0):.3f}"),
|
||||||
|
("Threshold", f"{stats.get('threshold', 0):.3f}"),
|
||||||
|
("Process Time", stats.get("process_time", "")),
|
||||||
|
]
|
||||||
|
for label, value in stats_rows:
|
||||||
|
ws_stats.append([label, value])
|
||||||
|
|
||||||
|
ws_stats.column_dimensions["A"].width = 18
|
||||||
|
ws_stats.column_dimensions["B"].width = 30
|
||||||
|
|
||||||
|
out = path.with_name(f"{path.stem}_filtered.xlsx")
|
||||||
|
wb.save(str(out))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def process_csv(csv_path: str, save_as_xlsx: bool) -> dict:
|
||||||
|
"""执行梯度过滤,返回结果统计"""
|
||||||
|
path = Path(csv_path)
|
||||||
|
if not path.is_file():
|
||||||
|
raise FileNotFoundError(f"CSV file not found: {csv_path}")
|
||||||
|
|
||||||
|
rows = load_rows(path)
|
||||||
|
if not rows:
|
||||||
|
raise ValueError("CSV file is empty.")
|
||||||
|
|
||||||
|
sum_values = [row_sum(r) for r in rows]
|
||||||
|
threshold = find_threshold(sum_values)
|
||||||
|
filtered_rows, group_means = extract_press_groups(rows, sum_values, threshold)
|
||||||
|
|
||||||
|
if not filtered_rows:
|
||||||
|
raise ValueError("No large press-down data was found.")
|
||||||
|
|
||||||
|
overall_mean = statistics.fmean(group_means)
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
"source_file": path.name,
|
||||||
|
"rows_total": len(rows),
|
||||||
|
"rows_kept": len(filtered_rows),
|
||||||
|
"groups_used": len(group_means),
|
||||||
|
"mean_value": overall_mean,
|
||||||
|
"threshold": threshold,
|
||||||
|
"process_time": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if save_as_xlsx:
|
||||||
|
output_path = write_xlsx(path, filtered_rows, stats)
|
||||||
|
# 删除源 CSV
|
||||||
|
try:
|
||||||
|
path.unlink()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
output_path = write_csv(path, filtered_rows)
|
||||||
|
# 用过滤后的文件替换源文件
|
||||||
|
try:
|
||||||
|
path.unlink()
|
||||||
|
output_path.rename(path)
|
||||||
|
output_path = path
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 追加一行到汇总 xlsx
|
||||||
|
_append_analysis_log(csv_path, stats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"output_path": str(output_path),
|
||||||
|
"groups_used": len(group_means),
|
||||||
|
"mean_value": overall_mean,
|
||||||
|
"threshold": threshold,
|
||||||
|
"rows_total": len(rows),
|
||||||
|
"rows_kept": len(filtered_rows),
|
||||||
|
"message": "OK",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _append_analysis_log(source_csv: str, stats: dict):
|
||||||
|
"""将处理结果追加到 devkit_analysis_results.xlsx"""
|
||||||
|
try:
|
||||||
|
import openpyxl
|
||||||
|
except ImportError:
|
||||||
|
return # openpyxl 不可用时跳过
|
||||||
|
|
||||||
|
log_path = Path(source_csv).parent / "devkit_analysis_results.xlsx"
|
||||||
|
|
||||||
|
if log_path.exists():
|
||||||
|
wb = openpyxl.load_workbook(str(log_path))
|
||||||
|
ws = wb.active
|
||||||
|
else:
|
||||||
|
wb = openpyxl.Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "Analysis Log"
|
||||||
|
ws.append(["Time", "Source File", "Total Rows", "Kept Rows",
|
||||||
|
"Groups", "Mean Value", "Threshold", "Output File"])
|
||||||
|
|
||||||
|
ws.append([
|
||||||
|
stats.get("process_time", ""),
|
||||||
|
stats.get("source_file", ""),
|
||||||
|
stats.get("rows_total", 0),
|
||||||
|
stats.get("rows_kept", 0),
|
||||||
|
stats.get("groups_used", 0),
|
||||||
|
round(stats.get("mean_value", 0), 3),
|
||||||
|
round(stats.get("threshold", 0), 3),
|
||||||
|
f"{Path(stats.get('source_file', '')).stem}_filtered",
|
||||||
|
])
|
||||||
|
|
||||||
|
wb.save(str(log_path))
|
||||||
|
|
||||||
|
|
||||||
|
# ── gRPC 服务实现 ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class SensorPushServicer(sensor_stream_pb2_grpc.SensorPushServicer):
|
||||||
|
"""接收实时传感器帧(streaming)"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.frame_count = 0
|
||||||
|
self.last_report_time = time.time()
|
||||||
|
|
||||||
|
def Upload(self, request_iterator, context):
|
||||||
|
print("[SensorPush] Client connected, waiting for frames...")
|
||||||
|
|
||||||
|
for frame in request_iterator:
|
||||||
|
self.frame_count += 1
|
||||||
|
if self.frame_count % 100 == 0:
|
||||||
|
now = time.time()
|
||||||
|
elapsed = now - self.last_report_time
|
||||||
|
fps = 100 / elapsed if elapsed > 0 else 0
|
||||||
|
self.last_report_time = now
|
||||||
|
print(
|
||||||
|
f"[SensorPush] Frame #{frame.seq} | "
|
||||||
|
f"{frame.rows}x{frame.cols} | "
|
||||||
|
f"force={frame.resultant_force:.1f} | "
|
||||||
|
f"total={self.frame_count} | ~{fps:.1f} fps"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"[SensorPush] Stream ended. Total: {self.frame_count}")
|
||||||
|
return sensor_stream_pb2.UploadResponse(
|
||||||
|
ok=True,
|
||||||
|
frames_received=self.frame_count,
|
||||||
|
message=f"Processed {self.frame_count} frames",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ExportProcessorServicer(sensor_stream_pb2_grpc.ExportProcessorServicer):
|
||||||
|
"""处理导出的 CSV 文件(unary)"""
|
||||||
|
|
||||||
|
def ProcessFile(self, request, context):
|
||||||
|
csv_path = request.csv_path
|
||||||
|
save_as_xlsx = request.save_as_xlsx
|
||||||
|
|
||||||
|
print(f"[ExportProcessor] Processing: {csv_path} (xlsx={save_as_xlsx})")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = process_csv(csv_path, save_as_xlsx)
|
||||||
|
return sensor_stream_pb2.ProcessResponse(
|
||||||
|
ok=result["ok"],
|
||||||
|
output_path=result["output_path"],
|
||||||
|
groups_used=result["groups_used"],
|
||||||
|
mean_value=result["mean_value"],
|
||||||
|
threshold=result["threshold"],
|
||||||
|
rows_total=result["rows_total"],
|
||||||
|
rows_kept=result["rows_kept"],
|
||||||
|
message=result["message"],
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ExportProcessor] Error: {e}")
|
||||||
|
return sensor_stream_pb2.ProcessResponse(
|
||||||
|
ok=False,
|
||||||
|
output_path="",
|
||||||
|
message=str(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── 启动 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def serve(port: int):
|
||||||
|
server = grpc.server(futures.ThreadPoolExecutor(max_workers=4))
|
||||||
|
sensor_stream_pb2_grpc.add_SensorPushServicer_to_server(SensorPushServicer(), server)
|
||||||
|
sensor_stream_pb2_grpc.add_ExportProcessorServicer_to_server(ExportProcessorServicer(), server)
|
||||||
|
|
||||||
|
listen_addr = f"0.0.0.0:{port}"
|
||||||
|
server.add_insecure_port(listen_addr)
|
||||||
|
server.start()
|
||||||
|
|
||||||
|
print(f"[DevKit Server] gRPC listening on {listen_addr}")
|
||||||
|
print(f"[DevKit Server] Services: SensorPush (streaming), ExportProcessor (unary)")
|
||||||
|
|
||||||
|
def shutdown(signum, frame):
|
||||||
|
print("\n[DevKit Server] Shutting down...")
|
||||||
|
server.stop(grace=5)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, shutdown)
|
||||||
|
signal.signal(signal.SIGTERM, shutdown)
|
||||||
|
|
||||||
|
server.wait_for_termination()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="JE-Skin DevKit gRPC Server")
|
||||||
|
parser.add_argument("--port", type=int, default=50051, help="gRPC listen port (default: 50051)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
serve(args.port)
|
||||||
46
devkit/sensor_stream_pb2.py
Normal file
46
devkit/sensor_stream_pb2.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
|
# NO CHECKED-IN PROTOBUF GENCODE
|
||||||
|
# source: sensor_stream.proto
|
||||||
|
# Protobuf Python Version: 6.31.1
|
||||||
|
"""Generated protocol buffer code."""
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||||
|
from google.protobuf import runtime_version as _runtime_version
|
||||||
|
from google.protobuf import symbol_database as _symbol_database
|
||||||
|
from google.protobuf.internal import builder as _builder
|
||||||
|
_runtime_version.ValidateProtobufRuntimeVersion(
|
||||||
|
_runtime_version.Domain.PUBLIC,
|
||||||
|
6,
|
||||||
|
31,
|
||||||
|
1,
|
||||||
|
'',
|
||||||
|
'sensor_stream.proto'
|
||||||
|
)
|
||||||
|
# @@protoc_insertion_point(imports)
|
||||||
|
|
||||||
|
_sym_db = _symbol_database.Default()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13sensor_stream.proto\x12\rsensor_stream\"\x85\x01\n\x0bSensorFrame\x12\x0b\n\x03seq\x18\x01 \x01(\x04\x12\x14\n\x0ctimestamp_ms\x18\x02 \x01(\x04\x12\x0c\n\x04rows\x18\x03 \x01(\r\x12\x0c\n\x04\x63ols\x18\x04 \x01(\r\x12\x0e\n\x06matrix\x18\x05 \x03(\r\x12\x17\n\x0fresultant_force\x18\x06 \x01(\x01\x12\x0e\n\x06\x64ts_ms\x18\x07 \x01(\r\"F\n\x0eUploadResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\x17\n\x0f\x66rames_received\x18\x02 \x01(\x04\x12\x0f\n\x07message\x18\x03 \x01(\t\"8\n\x0eProcessRequest\x12\x10\n\x08\x63sv_path\x18\x01 \x01(\t\x12\x14\n\x0csave_as_xlsx\x18\x02 \x01(\x08\"\xa6\x01\n\x0fProcessResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\x13\n\x0boutput_path\x18\x02 \x01(\t\x12\x13\n\x0bgroups_used\x18\x03 \x01(\r\x12\x12\n\nmean_value\x18\x04 \x01(\x01\x12\x11\n\tthreshold\x18\x05 \x01(\x01\x12\x12\n\nrows_total\x18\x06 \x01(\r\x12\x11\n\trows_kept\x18\x07 \x01(\r\x12\x0f\n\x07message\x18\x08 \x01(\t2S\n\nSensorPush\x12\x45\n\x06Upload\x12\x1a.sensor_stream.SensorFrame\x1a\x1d.sensor_stream.UploadResponse(\x01\x32_\n\x0f\x45xportProcessor\x12L\n\x0bProcessFile\x12\x1d.sensor_stream.ProcessRequest\x1a\x1e.sensor_stream.ProcessResponseb\x06proto3')
|
||||||
|
|
||||||
|
_globals = globals()
|
||||||
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'sensor_stream_pb2', _globals)
|
||||||
|
if not _descriptor._USE_C_DESCRIPTORS:
|
||||||
|
DESCRIPTOR._loaded_options = None
|
||||||
|
_globals['_SENSORFRAME']._serialized_start=39
|
||||||
|
_globals['_SENSORFRAME']._serialized_end=172
|
||||||
|
_globals['_UPLOADRESPONSE']._serialized_start=174
|
||||||
|
_globals['_UPLOADRESPONSE']._serialized_end=244
|
||||||
|
_globals['_PROCESSREQUEST']._serialized_start=246
|
||||||
|
_globals['_PROCESSREQUEST']._serialized_end=302
|
||||||
|
_globals['_PROCESSRESPONSE']._serialized_start=305
|
||||||
|
_globals['_PROCESSRESPONSE']._serialized_end=471
|
||||||
|
_globals['_SENSORPUSH']._serialized_start=473
|
||||||
|
_globals['_SENSORPUSH']._serialized_end=556
|
||||||
|
_globals['_EXPORTPROCESSOR']._serialized_start=558
|
||||||
|
_globals['_EXPORTPROCESSOR']._serialized_end=653
|
||||||
|
# @@protoc_insertion_point(module_scope)
|
||||||
175
devkit/sensor_stream_pb2_grpc.py
Normal file
175
devkit/sensor_stream_pb2_grpc.py
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||||
|
"""Client and server classes corresponding to protobuf-defined services."""
|
||||||
|
import grpc
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
import sensor_stream_pb2 as sensor__stream__pb2
|
||||||
|
|
||||||
|
GRPC_GENERATED_VERSION = '1.80.0'
|
||||||
|
GRPC_VERSION = grpc.__version__
|
||||||
|
_version_not_supported = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
from grpc._utilities import first_version_is_lower
|
||||||
|
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
|
||||||
|
except ImportError:
|
||||||
|
_version_not_supported = True
|
||||||
|
|
||||||
|
if _version_not_supported:
|
||||||
|
raise RuntimeError(
|
||||||
|
f'The grpc package installed is at version {GRPC_VERSION},'
|
||||||
|
+ ' but the generated code in sensor_stream_pb2_grpc.py depends on'
|
||||||
|
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
||||||
|
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
||||||
|
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SensorPushStub(object):
|
||||||
|
"""传感器数据推送服务 —— Rust 端作为 gRPC client 推送实时帧
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, channel):
|
||||||
|
"""Constructor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel: A grpc.Channel.
|
||||||
|
"""
|
||||||
|
self.Upload = channel.stream_unary(
|
||||||
|
'/sensor_stream.SensorPush/Upload',
|
||||||
|
request_serializer=sensor__stream__pb2.SensorFrame.SerializeToString,
|
||||||
|
response_deserializer=sensor__stream__pb2.UploadResponse.FromString,
|
||||||
|
_registered_method=True)
|
||||||
|
|
||||||
|
|
||||||
|
class SensorPushServicer(object):
|
||||||
|
"""传感器数据推送服务 —— Rust 端作为 gRPC client 推送实时帧
|
||||||
|
"""
|
||||||
|
|
||||||
|
def Upload(self, request_iterator, context):
|
||||||
|
"""Missing associated documentation comment in .proto file."""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
|
||||||
|
def add_SensorPushServicer_to_server(servicer, server):
|
||||||
|
rpc_method_handlers = {
|
||||||
|
'Upload': grpc.stream_unary_rpc_method_handler(
|
||||||
|
servicer.Upload,
|
||||||
|
request_deserializer=sensor__stream__pb2.SensorFrame.FromString,
|
||||||
|
response_serializer=sensor__stream__pb2.UploadResponse.SerializeToString,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
generic_handler = grpc.method_handlers_generic_handler(
|
||||||
|
'sensor_stream.SensorPush', rpc_method_handlers)
|
||||||
|
server.add_generic_rpc_handlers((generic_handler,))
|
||||||
|
server.add_registered_method_handlers('sensor_stream.SensorPush', rpc_method_handlers)
|
||||||
|
|
||||||
|
|
||||||
|
# This class is part of an EXPERIMENTAL API.
|
||||||
|
class SensorPush(object):
|
||||||
|
"""传感器数据推送服务 —— Rust 端作为 gRPC client 推送实时帧
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def Upload(request_iterator,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.stream_unary(
|
||||||
|
request_iterator,
|
||||||
|
target,
|
||||||
|
'/sensor_stream.SensorPush/Upload',
|
||||||
|
sensor__stream__pb2.SensorFrame.SerializeToString,
|
||||||
|
sensor__stream__pb2.UploadResponse.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
_registered_method=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ExportProcessorStub(object):
|
||||||
|
"""导出后处理服务 —— Rust 导出 CSV 后将路径发给 Python 做梯度过滤
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, channel):
|
||||||
|
"""Constructor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel: A grpc.Channel.
|
||||||
|
"""
|
||||||
|
self.ProcessFile = channel.unary_unary(
|
||||||
|
'/sensor_stream.ExportProcessor/ProcessFile',
|
||||||
|
request_serializer=sensor__stream__pb2.ProcessRequest.SerializeToString,
|
||||||
|
response_deserializer=sensor__stream__pb2.ProcessResponse.FromString,
|
||||||
|
_registered_method=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ExportProcessorServicer(object):
|
||||||
|
"""导出后处理服务 —— Rust 导出 CSV 后将路径发给 Python 做梯度过滤
|
||||||
|
"""
|
||||||
|
|
||||||
|
def ProcessFile(self, request, context):
|
||||||
|
"""Missing associated documentation comment in .proto file."""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
|
||||||
|
def add_ExportProcessorServicer_to_server(servicer, server):
|
||||||
|
rpc_method_handlers = {
|
||||||
|
'ProcessFile': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ProcessFile,
|
||||||
|
request_deserializer=sensor__stream__pb2.ProcessRequest.FromString,
|
||||||
|
response_serializer=sensor__stream__pb2.ProcessResponse.SerializeToString,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
generic_handler = grpc.method_handlers_generic_handler(
|
||||||
|
'sensor_stream.ExportProcessor', rpc_method_handlers)
|
||||||
|
server.add_generic_rpc_handlers((generic_handler,))
|
||||||
|
server.add_registered_method_handlers('sensor_stream.ExportProcessor', rpc_method_handlers)
|
||||||
|
|
||||||
|
|
||||||
|
# This class is part of an EXPERIMENTAL API.
|
||||||
|
class ExportProcessor(object):
|
||||||
|
"""导出后处理服务 —— Rust 导出 CSV 后将路径发给 Python 做梯度过滤
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def ProcessFile(request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.unary_unary(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
'/sensor_stream.ExportProcessor/ProcessFile',
|
||||||
|
sensor__stream__pb2.ProcessRequest.SerializeToString,
|
||||||
|
sensor__stream__pb2.ProcessResponse.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
_registered_method=True)
|
||||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -11,6 +11,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
|
"@tauri-apps/plugin-process": "^2.3.1",
|
||||||
|
"@tauri-apps/plugin-updater": "^2.10.1",
|
||||||
"three": "^0.183.2"
|
"three": "^0.183.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1280,6 +1282,24 @@
|
|||||||
"@tauri-apps/api": "^2.8.0"
|
"@tauri-apps/api": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-process": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-updater": {
|
||||||
|
"version": "2.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.10.1.tgz",
|
||||||
|
"integrity": "sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.10.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tweenjs/tween.js": {
|
"node_modules/@tweenjs/tween.js": {
|
||||||
"version": "23.1.3",
|
"version": "23.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||||
|
|||||||
@@ -9,12 +9,16 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"tauri": "tauri"
|
"tauri": "tauri",
|
||||||
|
"tauri:devkit": "tauri dev -- --features devkit",
|
||||||
|
"tauri:devkit:build": "tauri build -- --features devkit"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
|
"@tauri-apps/plugin-process": "^2.3.1",
|
||||||
|
"@tauri-apps/plugin-updater": "^2.10.1",
|
||||||
"three": "^0.183.2"
|
"three": "^0.183.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
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"
|
name = "tauri_demo_lib"
|
||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
devkit = ["dep:tonic", "dep:prost", "dep:prost-types", "dep:async-stream", "dep:dirs"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
|
tonic-build = { version = "0.12" }
|
||||||
|
protoc-bin-vendored = "3"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2", features = ["tray-icon"] }
|
tauri = { version = "2", features = ["tray-icon"] }
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
|
tauri-plugin-process = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
anyhow = "1.0.102"
|
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-serial = { version = "5.4.5" }
|
||||||
tokio = { version = "1.50.0", features = ["full"] }
|
tokio = { version = "1.50.0", features = ["full"] }
|
||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
@@ -33,3 +45,12 @@ humantime = "2.3.0"
|
|||||||
csv = "1.4.0"
|
csv = "1.4.0"
|
||||||
chrono = "0.4.44"
|
chrono = "0.4.44"
|
||||||
crc = "3.4.0"
|
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() {
|
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()
|
tauri_build::build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
"core:window:allow-inner-size",
|
"core:window:allow-inner-size",
|
||||||
"core:window:allow-set-size",
|
"core:window:allow-set-size",
|
||||||
"core:window:allow-start-dragging",
|
"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 file_explorer;
|
||||||
pub mod serial;
|
pub mod serial;
|
||||||
pub mod window;
|
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;
|
mod commands;
|
||||||
pub mod serial_core;
|
mod lan_game;
|
||||||
pub mod log;
|
pub mod log;
|
||||||
|
pub mod serial_core;
|
||||||
|
|
||||||
|
#[cfg(feature = "devkit")]
|
||||||
|
pub mod devkit;
|
||||||
|
|
||||||
use commands::serial::SerialConnectionState;
|
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)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
let builder = tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_process::init())
|
||||||
.manage(SerialConnectionState::default())
|
.manage(SerialConnectionState::default())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init());
|
||||||
.invoke_handler(tauri::generate_handler![
|
|
||||||
commands::file_explorer::file_explorer_list,
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
commands::serial::serial_enum,
|
let builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
|
||||||
commands::serial::serial_connect,
|
|
||||||
commands::serial::serial_disconnect,
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||||
commands::serial::serial_export_csv,
|
let builder = builder;
|
||||||
commands::serial::serial_has_record_data,
|
|
||||||
commands::serial::serial_export_csv_to_path,
|
#[cfg(feature = "devkit")]
|
||||||
commands::serial::serial_import_csv,
|
let builder = {
|
||||||
commands::serial::serial_import_csv_from_path,
|
let devkit_state = devkit::DevKitState::default();
|
||||||
commands::window::win_minimize,
|
let devkit_state_clone = devkit_state.clone();
|
||||||
commands::window::win_toggle_maximize,
|
|
||||||
commands::window::win_close
|
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!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,16 @@ use fern::{
|
|||||||
Dispatch,
|
Dispatch,
|
||||||
};
|
};
|
||||||
use log::debug;
|
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() {
|
pub fn setup_logger() {
|
||||||
let colors_line = ColoredLevelConfig::new()
|
let colors_line = ColoredLevelConfig::new()
|
||||||
.error(Color::Red)
|
.error(Color::Red)
|
||||||
@@ -38,7 +47,11 @@ pub fn setup_logger() {
|
|||||||
// .chain(fern::DateBased::new("program.log", "%Y-%m-%d"))
|
// .chain(fern::DateBased::new("program.log", "%Y-%m-%d"))
|
||||||
// .apply()
|
// .apply()
|
||||||
// .unwrap();
|
// .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()
|
let file_config = fern::Dispatch::new()
|
||||||
.format(move |out, message, record| {
|
.format(move |out, message, record| {
|
||||||
out.finish(format_args!(
|
out.finish(format_args!(
|
||||||
@@ -50,7 +63,7 @@ pub fn setup_logger() {
|
|||||||
));
|
));
|
||||||
})
|
})
|
||||||
.level(level)
|
.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()
|
Dispatch::new()
|
||||||
.level(log::LevelFilter::Debug)
|
.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::model::{HudChartState, HudPacket};
|
||||||
use crate::serial_core::record::Recording;
|
use crate::serial_core::record::Recording;
|
||||||
use crate::serial_core::record::{FrameTiming, RecordedFrame};
|
use crate::serial_core::record::{FrameTiming, RecordedFrame};
|
||||||
|
#[cfg(feature = "devkit")]
|
||||||
|
use crate::devkit::{proto::SensorFrame, DevKitState};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::future::pending;
|
use std::future::pending;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
|
#[cfg(feature = "devkit")]
|
||||||
|
use tauri::Manager;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::time::{self, Duration, MissedTickBehavior};
|
use tokio::time::{self, Duration, MissedTickBehavior};
|
||||||
use tokio_serial::SerialStream;
|
use tokio_serial::SerialStream;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
#[cfg(feature = "devkit")]
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
pub enum PollMode<F> {
|
pub enum PollMode<F> {
|
||||||
Disable,
|
Disable,
|
||||||
@@ -271,6 +277,8 @@ where
|
|||||||
let force = raw_to_g1(summary as u32);
|
let force = raw_to_g1(summary as u32);
|
||||||
chart_state.record_summary(force as f32);
|
chart_state.record_summary(force as f32);
|
||||||
chart_state.record_pressure_matrix(vals.as_slice());
|
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])
|
Some(vec![summary])
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -286,6 +294,58 @@ where
|
|||||||
Ok(())
|
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 {
|
fn raw_to_g1(raw: u32) -> f64 {
|
||||||
const X: [u32; 12] = [
|
const X: [u32; 12] = [
|
||||||
0, 84402, 117218, 140176, 159126, 175812, 191484, 208758, 224703, 252448, 302361, 352703,
|
0, 84402, 117218, 140176, 159126, 175812, 191484, 208758, 224703, 252448, 302361, 352703,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
|
"createUpdaterArtifacts": true,
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
"icon": [
|
"icon": [
|
||||||
@@ -31,6 +32,27 @@
|
|||||||
"icons/128x128@2x.png",
|
"icons/128x128@2x.png",
|
||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
362
src/lib/components/DevKitConfigPanel.svelte
Normal file
362
src/lib/components/DevKitConfigPanel.svelte
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
export let running = false;
|
||||||
|
export let port = 50051;
|
||||||
|
export let framesSent = 0;
|
||||||
|
export let filterLiftEnabled = true;
|
||||||
|
export let saveAsXlsx = false;
|
||||||
|
export let locale: "zh-CN" | "en-US" = "zh-CN";
|
||||||
|
export let lastProcessResult: {
|
||||||
|
outputPath: string;
|
||||||
|
groupsUsed: number;
|
||||||
|
meanValue: number;
|
||||||
|
threshold: number;
|
||||||
|
rowsTotal: number;
|
||||||
|
rowsKept: number;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
close: void;
|
||||||
|
togglefilterlift: void;
|
||||||
|
togglexlsx: void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
$: labels = locale === "zh-CN"
|
||||||
|
? {
|
||||||
|
title: "DevKit 配置",
|
||||||
|
status: "状态",
|
||||||
|
connected: "已连接",
|
||||||
|
disconnected: "未连接",
|
||||||
|
port: "端口",
|
||||||
|
framesSent: "已发送帧",
|
||||||
|
filterLift: "导出过滤抬起",
|
||||||
|
filterLiftHint: "导出 CSV 后自动调用 Python 做梯度过滤,过滤掉抬起的小值数据",
|
||||||
|
saveXlsx: "以 xlsx 保存",
|
||||||
|
saveXlsxHint: "Python 处理后输出 xlsx 格式并删除源 CSV 文件",
|
||||||
|
lastResult: "最近一次处理",
|
||||||
|
output: "输出文件",
|
||||||
|
groups: "分组数",
|
||||||
|
mean: "均值",
|
||||||
|
threshold: "阈值",
|
||||||
|
rows: "行数",
|
||||||
|
kept: "保留行数",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
title: "DevKit Config",
|
||||||
|
status: "Status",
|
||||||
|
connected: "Connected",
|
||||||
|
disconnected: "Disconnected",
|
||||||
|
port: "Port",
|
||||||
|
framesSent: "Frames sent",
|
||||||
|
filterLift: "Filter lift on export",
|
||||||
|
filterLiftHint: "After CSV export, automatically call Python to filter out small values",
|
||||||
|
saveXlsx: "Save as xlsx",
|
||||||
|
saveXlsxHint: "Python outputs xlsx format and deletes the source CSV file",
|
||||||
|
lastResult: "Last process",
|
||||||
|
output: "Output",
|
||||||
|
groups: "Groups",
|
||||||
|
mean: "Mean",
|
||||||
|
threshold: "Threshold",
|
||||||
|
rows: "Rows",
|
||||||
|
kept: "Kept rows",
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="dk-panel">
|
||||||
|
<header class="dk-head">
|
||||||
|
<h3 class="dk-title">{labels.title}</h3>
|
||||||
|
<button type="button" class="dk-close" on:click={() => dispatch("close")} aria-label="Close">
|
||||||
|
<span></span><span></span>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="dk-section">
|
||||||
|
<div class="dk-status-row">
|
||||||
|
<span class="dk-dot" class:active={running}></span>
|
||||||
|
<span class="dk-label">{labels.status}</span>
|
||||||
|
<span class="dk-value">{running ? labels.connected : labels.disconnected}</span>
|
||||||
|
</div>
|
||||||
|
{#if running}
|
||||||
|
<div class="dk-info-grid">
|
||||||
|
<div class="dk-info">
|
||||||
|
<span class="dk-info-label">{labels.port}</span>
|
||||||
|
<span class="dk-info-value">:{port}</span>
|
||||||
|
</div>
|
||||||
|
<div class="dk-info">
|
||||||
|
<span class="dk-info-label">{labels.framesSent}</span>
|
||||||
|
<span class="dk-info-value">{framesSent}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="dk-section">
|
||||||
|
<button type="button" class="dk-toggle" class:active={filterLiftEnabled} on:click={() => dispatch("togglefilterlift")}>
|
||||||
|
<span class="dk-toggle-indicator"></span>
|
||||||
|
<div class="dk-toggle-text">
|
||||||
|
<span class="dk-toggle-label">{labels.filterLift}</span>
|
||||||
|
<span class="dk-toggle-hint">{labels.filterLiftHint}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="dk-section">
|
||||||
|
<button type="button" class="dk-toggle" class:active={saveAsXlsx} on:click={() => dispatch("togglexlsx")}>
|
||||||
|
<span class="dk-toggle-indicator"></span>
|
||||||
|
<div class="dk-toggle-text">
|
||||||
|
<span class="dk-toggle-label">{labels.saveXlsx}</span>
|
||||||
|
<span class="dk-toggle-hint">{labels.saveXlsxHint}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{#if lastProcessResult}
|
||||||
|
<section class="dk-section dk-result">
|
||||||
|
<p class="dk-section-title">{labels.lastResult}</p>
|
||||||
|
<div class="dk-result-grid">
|
||||||
|
<div class="dk-result-item">
|
||||||
|
<span class="dk-result-label">{labels.output}</span>
|
||||||
|
<span class="dk-result-value">{lastProcessResult.outputPath}</span>
|
||||||
|
</div>
|
||||||
|
<div class="dk-result-item">
|
||||||
|
<span class="dk-result-label">{labels.groups}</span>
|
||||||
|
<span class="dk-result-value">{lastProcessResult.groupsUsed}</span>
|
||||||
|
</div>
|
||||||
|
<div class="dk-result-item">
|
||||||
|
<span class="dk-result-label">{labels.mean}</span>
|
||||||
|
<span class="dk-result-value">{lastProcessResult.meanValue.toFixed(3)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="dk-result-item">
|
||||||
|
<span class="dk-result-label">{labels.threshold}</span>
|
||||||
|
<span class="dk-result-value">{lastProcessResult.threshold.toFixed(3)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="dk-result-item">
|
||||||
|
<span class="dk-result-label">{labels.rows}</span>
|
||||||
|
<span class="dk-result-value">{lastProcessResult.rowsTotal} → {lastProcessResult.rowsKept}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dk-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.7rem;
|
||||||
|
inline-size: min(22rem, 100%);
|
||||||
|
padding: 0.9rem 0.96rem 1rem;
|
||||||
|
border: 1px solid rgb(var(--hud-border-rgb) / 0.3);
|
||||||
|
border-radius: 0.82rem;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgb(var(--hud-surface-alt-rgb) / 0.92), rgb(var(--hud-surface-deep-rgb) / 0.88)),
|
||||||
|
radial-gradient(circle at 100% 0, rgb(var(--hud-info-rgb) / 0.07), transparent 38%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.08),
|
||||||
|
0 18px 46px rgb(0 0 0 / 0.28);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-title {
|
||||||
|
margin: 0;
|
||||||
|
color: rgb(var(--hud-text-main-rgb) / 0.98);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-close {
|
||||||
|
position: relative;
|
||||||
|
inline-size: 1.8rem;
|
||||||
|
block-size: 1.8rem;
|
||||||
|
border: 1px solid rgb(var(--hud-border-rgb) / 0.32);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgb(var(--hud-surface-deep-rgb) / 0.72);
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-close span {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
inline-size: 0.7rem;
|
||||||
|
block-size: 1px;
|
||||||
|
background: rgb(var(--hud-text-main-rgb) / 0.9);
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-close span:first-child { transform: translate(-50%, -50%) rotate(45deg); }
|
||||||
|
.dk-close span:last-child { transform: translate(-50%, -50%) rotate(-45deg); }
|
||||||
|
|
||||||
|
.dk-section {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.6rem 0.7rem;
|
||||||
|
border: 1px solid rgb(var(--hud-border-rgb) / 0.22);
|
||||||
|
border-radius: 0.68rem;
|
||||||
|
background: linear-gradient(180deg, rgb(var(--hud-surface-rgb) / 0.76), rgb(var(--hud-surface-deep-rgb) / 0.64));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-status-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-dot {
|
||||||
|
inline-size: 0.5rem;
|
||||||
|
block-size: 0.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgb(var(--hud-text-dim-rgb) / 0.8);
|
||||||
|
transition: background 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-dot.active {
|
||||||
|
background: rgb(var(--hud-lime-rgb) / 0.95);
|
||||||
|
box-shadow: 0 0 0 2px rgb(var(--hud-lime-rgb) / 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-label {
|
||||||
|
color: rgb(var(--hud-text-dim-rgb) / 0.8);
|
||||||
|
font-size: 0.62rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-value {
|
||||||
|
color: rgb(var(--hud-text-main-rgb) / 0.96);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-inline-start: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-info-grid {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-info-label {
|
||||||
|
color: rgb(var(--hud-text-dim-rgb) / 0.7);
|
||||||
|
font-size: 0.56rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-info-value {
|
||||||
|
color: rgb(var(--hud-text-main-rgb) / 0.94);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.6rem;
|
||||||
|
border: 1px solid rgb(var(--hud-border-rgb) / 0.24);
|
||||||
|
border-radius: 0.58rem;
|
||||||
|
padding: 0.55rem 0.65rem;
|
||||||
|
background: rgb(var(--hud-surface-rgb) / 0.82);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition: border-color 180ms ease, box-shadow 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-toggle:hover {
|
||||||
|
border-color: rgb(var(--hud-cyan-rgb) / 0.36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-toggle.active {
|
||||||
|
border-color: rgb(var(--hud-lime-rgb) / 0.48);
|
||||||
|
box-shadow: inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.08), 0 0 14px rgb(var(--hud-glow-alt-rgb) / 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-toggle-indicator {
|
||||||
|
inline-size: 0.56rem;
|
||||||
|
block-size: 0.56rem;
|
||||||
|
min-inline-size: 0.56rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-block-start: 0.15rem;
|
||||||
|
background: rgb(var(--hud-text-dim-rgb) / 0.7);
|
||||||
|
transition: background 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-toggle.active .dk-toggle-indicator {
|
||||||
|
background: rgb(var(--hud-lime-rgb) / 0.92);
|
||||||
|
box-shadow: 0 0 0 2px rgb(var(--hud-lime-rgb) / 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-toggle-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-toggle-label {
|
||||||
|
color: rgb(var(--hud-text-main-rgb) / 0.96);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-toggle-hint {
|
||||||
|
color: rgb(var(--hud-text-dim-rgb) / 0.7);
|
||||||
|
font-size: 0.64rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-section-title {
|
||||||
|
margin: 0;
|
||||||
|
color: rgb(var(--hud-text-dim-rgb) / 0.8);
|
||||||
|
font-size: 0.58rem;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-result {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-result-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-result-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
padding: 0.2rem 0;
|
||||||
|
border-bottom: 1px solid rgb(var(--hud-border-rgb) / 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-result-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-result-label {
|
||||||
|
color: rgb(var(--hud-text-dim-rgb) / 0.72);
|
||||||
|
font-size: 0.64rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dk-result-value {
|
||||||
|
color: rgb(var(--hud-text-main-rgb) / 0.94);
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: right;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -41,6 +41,10 @@
|
|||||||
export let importActionLabel = "";
|
export let importActionLabel = "";
|
||||||
export let connectionNotice = "";
|
export let connectionNotice = "";
|
||||||
export let connectionNoticeTone: HudNoticeTone = "info";
|
export let connectionNoticeTone: HudNoticeTone = "info";
|
||||||
|
export let noticeConfirmLabel = "";
|
||||||
|
export let noticeCancelLabel = "";
|
||||||
|
export let noticeShowActions = false;
|
||||||
|
export let noticeActionBusy = false;
|
||||||
export let isRefreshingPorts = false;
|
export let isRefreshingPorts = false;
|
||||||
export let isConnectDisabled = false;
|
export let isConnectDisabled = false;
|
||||||
export let isExporting = false;
|
export let isExporting = false;
|
||||||
@@ -58,6 +62,8 @@
|
|||||||
serialexport: void;
|
serialexport: void;
|
||||||
csvimport: void;
|
csvimport: void;
|
||||||
noticeclear: void;
|
noticeclear: void;
|
||||||
|
noticeconfirm: void;
|
||||||
|
noticecancel: void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const connectionToneByState: Record<ConnectionState, "ok" | "warn" | "idle"> = {
|
const connectionToneByState: Record<ConnectionState, "ok" | "warn" | "idle"> = {
|
||||||
@@ -123,6 +129,14 @@
|
|||||||
function emitNoticeClear(): void {
|
function emitNoticeClear(): void {
|
||||||
dispatch("noticeclear");
|
dispatch("noticeclear");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function emitNoticeConfirm(): void {
|
||||||
|
dispatch("noticeconfirm");
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitNoticeCancel(): void {
|
||||||
|
dispatch("noticecancel");
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="hud-panel" aria-label={controlAreaLabel}>
|
<section class="hud-panel" aria-label={controlAreaLabel}>
|
||||||
@@ -301,14 +315,25 @@
|
|||||||
{#if connectionNotice}
|
{#if connectionNotice}
|
||||||
<div class="connection-notice tone-{connectionNoticeTone}" role={connectionNoticeTone === "warn" ? "alert" : "status"}>
|
<div class="connection-notice tone-{connectionNoticeTone}" role={connectionNoticeTone === "warn" ? "alert" : "status"}>
|
||||||
<p class="connection-notice-text">{connectionNotice}</p>
|
<p class="connection-notice-text">{connectionNotice}</p>
|
||||||
<button
|
{#if noticeShowActions}
|
||||||
type="button"
|
<div class="notice-actions">
|
||||||
class="notice-close-btn"
|
<button type="button" class="notice-action-btn is-primary" disabled={noticeActionBusy} on:click={emitNoticeConfirm}>
|
||||||
aria-label={locale === "zh-CN" ? "关闭提示" : "Dismiss notice"}
|
{noticeActionBusy ? (locale === "zh-CN" ? "处理中..." : "Working...") : noticeConfirmLabel}
|
||||||
on:click={emitNoticeClear}
|
</button>
|
||||||
>
|
<button type="button" class="notice-action-btn" disabled={noticeActionBusy} on:click={emitNoticeCancel}>
|
||||||
×
|
{noticeCancelLabel}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="notice-close-btn"
|
||||||
|
aria-label={locale === "zh-CN" ? "关闭提示" : "Dismiss notice"}
|
||||||
|
on:click={emitNoticeClear}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -921,6 +946,58 @@
|
|||||||
background: rgb(9 16 22 / 0.92);
|
background: rgb(9 16 22 / 0.92);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notice-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.42rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-action-btn {
|
||||||
|
min-block-size: 1.55rem;
|
||||||
|
border: 1px solid rgb(116 151 176 / 0.38);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.24rem 0.72rem;
|
||||||
|
background: rgb(7 12 16 / 0.76);
|
||||||
|
color: rgb(194 225 245 / 0.94);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color 180ms ease,
|
||||||
|
color 180ms ease,
|
||||||
|
background-color 180ms ease,
|
||||||
|
box-shadow 200ms ease,
|
||||||
|
opacity 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-action-btn:hover:not(:disabled) {
|
||||||
|
border-color: rgb(62 232 255 / 0.46);
|
||||||
|
color: rgb(237 250 255 / 0.98);
|
||||||
|
background: rgb(9 16 22 / 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-action-btn.is-primary {
|
||||||
|
border-color: rgb(var(--hud-lime-rgb) / 0.44);
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgb(var(--hud-surface-alt-rgb) / 0.94), rgb(var(--hud-surface-rgb) / 0.88)),
|
||||||
|
radial-gradient(circle at 50% 0, rgb(var(--hud-lime-rgb) / 0.14), transparent 58%);
|
||||||
|
color: rgb(var(--hud-text-main-rgb) / 0.98);
|
||||||
|
box-shadow: 0 0 10px rgb(var(--hud-lime-rgb) / 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-action-btn.is-primary:hover:not(:disabled) {
|
||||||
|
border-color: rgb(var(--hud-lime-rgb) / 0.62);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgb(var(--hud-text-main-rgb) / 0.08),
|
||||||
|
0 0 13px rgb(var(--hud-lime-rgb) / 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-action-btn:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.64;
|
||||||
|
}
|
||||||
|
|
||||||
.connection-notice.tone-warn .notice-close-btn:hover {
|
.connection-notice.tone-warn .notice-close-btn:hover {
|
||||||
border-color: rgb(255 91 63 / 0.6);
|
border-color: rgb(255 91 63 / 0.6);
|
||||||
color: rgb(255 227 220 / 0.98);
|
color: rgb(255 227 220 / 0.98);
|
||||||
@@ -1139,4 +1216,4 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,2 @@
|
|||||||
export const DEFAULT_PRESSURE_RANGE_MIN = 0;
|
export const DEFAULT_PRESSURE_RANGE_MIN = 0;
|
||||||
export const DEFAULT_PRESSURE_RANGE_MAX = 6000;
|
export const DEFAULT_PRESSURE_RANGE_MAX = 7000;
|
||||||
|
|||||||
@@ -3,9 +3,12 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
||||||
import { LogicalSize, currentMonitor, getCurrentWindow } from "@tauri-apps/api/window";
|
import { LogicalSize, currentMonitor, getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
|
import { relaunch } from "@tauri-apps/plugin-process";
|
||||||
|
import { check } from "@tauri-apps/plugin-updater";
|
||||||
import HudPanel from "$lib/components/HudPanel.svelte";
|
import HudPanel from "$lib/components/HudPanel.svelte";
|
||||||
import CenterStage from "$lib/components/CenterStage.svelte";
|
import CenterStage from "$lib/components/CenterStage.svelte";
|
||||||
import FileExplorerModal from "$lib/components/FileExplorerModal.svelte";
|
import FileExplorerModal from "$lib/components/FileExplorerModal.svelte";
|
||||||
|
import DevKitConfigPanel from "$lib/components/DevKitConfigPanel.svelte";
|
||||||
import { DEFAULT_PRESSURE_RANGE_MAX, DEFAULT_PRESSURE_RANGE_MIN } from "$lib/config/pressure-range";
|
import { DEFAULT_PRESSURE_RANGE_MAX, DEFAULT_PRESSURE_RANGE_MIN } from "$lib/config/pressure-range";
|
||||||
import { pressureColorPalettes } from "$lib/config/color-map";
|
import { pressureColorPalettes } from "$lib/config/color-map";
|
||||||
import "$lib/styles/theme.css";
|
import "$lib/styles/theme.css";
|
||||||
@@ -203,6 +206,9 @@
|
|||||||
let isRefreshingPorts = false;
|
let isRefreshingPorts = false;
|
||||||
let connectionNotice = "";
|
let connectionNotice = "";
|
||||||
let connectionNoticeTone: HudNoticeTone = "info";
|
let connectionNoticeTone: HudNoticeTone = "info";
|
||||||
|
let updateNoticeVisible = false;
|
||||||
|
let updateInstallBusy = false;
|
||||||
|
let pendingUpdate: Awaited<ReturnType<typeof check>> | null = null;
|
||||||
let isExporting = false;
|
let isExporting = false;
|
||||||
let deviceValue = "JE-Skin-F";
|
let deviceValue = "JE-Skin-F";
|
||||||
let sampleRateValue = "100Hz";
|
let sampleRateValue = "100Hz";
|
||||||
@@ -238,6 +244,22 @@
|
|||||||
let fileExplorerRoots: FileExplorerRoot[] = [];
|
let fileExplorerRoots: FileExplorerRoot[] = [];
|
||||||
let fileExplorerSelectedPath = "";
|
let fileExplorerSelectedPath = "";
|
||||||
let fileExplorerFileName = "";
|
let fileExplorerFileName = "";
|
||||||
|
let isDevKitConfigOpen = false;
|
||||||
|
let devkitEnabled = false;
|
||||||
|
let devkitRunning = false;
|
||||||
|
let devkitPort = 50051;
|
||||||
|
let devkitFramesSent = 0;
|
||||||
|
let devkitFilterLift = true;
|
||||||
|
let devkitSaveXlsx = false;
|
||||||
|
let devkitLastResult: {
|
||||||
|
outputPath: string;
|
||||||
|
groupsUsed: number;
|
||||||
|
meanValue: number;
|
||||||
|
threshold: number;
|
||||||
|
rowsTotal: number;
|
||||||
|
rowsKept: number;
|
||||||
|
} | null = null;
|
||||||
|
let devkitStatusTimer: number | null = null;
|
||||||
|
|
||||||
$: uiCopy = copyByLocale[locale];
|
$: uiCopy = copyByLocale[locale];
|
||||||
$: configLinks = buildConfigLinks(locale, activeConfigLinkId, isConfigPanelOpen, isPrecisionTestOpen);
|
$: configLinks = buildConfigLinks(locale, activeConfigLinkId, isConfigPanelOpen, isPrecisionTestOpen);
|
||||||
@@ -1012,7 +1034,7 @@
|
|||||||
settings: "Setup"
|
settings: "Setup"
|
||||||
};
|
};
|
||||||
|
|
||||||
return [
|
const links: HudConfigLink[] = [
|
||||||
{
|
{
|
||||||
id: "stream-on",
|
id: "stream-on",
|
||||||
label: labels.streamOn,
|
label: labels.streamOn,
|
||||||
@@ -1044,6 +1066,17 @@
|
|||||||
active: isSettingsOpen
|
active: isSettingsOpen
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (devkitEnabled) {
|
||||||
|
links.push({
|
||||||
|
id: "devkit",
|
||||||
|
label: "DevKit",
|
||||||
|
tone: "cyan",
|
||||||
|
active: isDevKitConfigOpen
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return links;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureDefaultWindowSize(): Promise<void> {
|
async function ensureDefaultWindowSize(): Promise<void> {
|
||||||
@@ -1097,6 +1130,71 @@
|
|||||||
const context = canvas.getContext("webgl2");
|
const context = canvas.getContext("webgl2");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkForAppUpdate(): Promise<void> {
|
||||||
|
if (!isTauriRuntime()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateDismissKey = "je-skin-update-dismissed-version";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const update = await check();
|
||||||
|
if (!update) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.sessionStorage.getItem(updateDismissKey) === update.version) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message =
|
||||||
|
locale === "zh-CN"
|
||||||
|
? `发现新版本 ${update.version},是否现在下载并安装?`
|
||||||
|
: `Version ${update.version} is available. Download and install now?`;
|
||||||
|
|
||||||
|
pendingUpdate = update;
|
||||||
|
updateNoticeVisible = true;
|
||||||
|
updateInstallBusy = false;
|
||||||
|
connectionNotice = message;
|
||||||
|
connectionNoticeTone = "info";
|
||||||
|
} catch (error) {
|
||||||
|
console.error("App update check failed:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUpdateConfirm(): Promise<void> {
|
||||||
|
if (!pendingUpdate || updateInstallBusy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateInstallBusy = true;
|
||||||
|
connectionNotice = locale === "zh-CN" ? "正在下载并安装更新..." : "Downloading and installing update...";
|
||||||
|
connectionNoticeTone = "info";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pendingUpdate.downloadAndInstall();
|
||||||
|
await relaunch();
|
||||||
|
} catch (error) {
|
||||||
|
updateInstallBusy = false;
|
||||||
|
updateNoticeVisible = false;
|
||||||
|
pendingUpdate = null;
|
||||||
|
connectionNotice = locale === "zh-CN" ? "更新安装失败,请稍后重试。" : "Update failed. Please try again later.";
|
||||||
|
connectionNoticeTone = "warn";
|
||||||
|
console.error("App update install failed:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpdateCancel(): void {
|
||||||
|
if (pendingUpdate) {
|
||||||
|
window.sessionStorage.setItem("je-skin-update-dismissed-version", pendingUpdate.version);
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingUpdate = null;
|
||||||
|
updateNoticeVisible = false;
|
||||||
|
updateInstallBusy = false;
|
||||||
|
connectionNotice = "";
|
||||||
|
}
|
||||||
|
|
||||||
function handleLocaleChange(event: CustomEvent<LocaleCode>): void {
|
function handleLocaleChange(event: CustomEvent<LocaleCode>): void {
|
||||||
locale = event.detail;
|
locale = event.detail;
|
||||||
}
|
}
|
||||||
@@ -1318,6 +1416,57 @@
|
|||||||
? await invoke<SerialExportResult>("serial_export_csv_to_path", { filePath })
|
? await invoke<SerialExportResult>("serial_export_csv_to_path", { filePath })
|
||||||
: await invoke<SerialExportResult>("serial_export_csv");
|
: await invoke<SerialExportResult>("serial_export_csv");
|
||||||
|
|
||||||
|
if (devkitEnabled && devkitRunning && devkitFilterLift) {
|
||||||
|
try {
|
||||||
|
const processResult = await invoke<{
|
||||||
|
ok: boolean;
|
||||||
|
outputPath: string;
|
||||||
|
groupsUsed: number;
|
||||||
|
meanValue: number;
|
||||||
|
threshold: number;
|
||||||
|
rowsTotal: number;
|
||||||
|
rowsKept: number;
|
||||||
|
message: string;
|
||||||
|
}>("devkit_process_export", {
|
||||||
|
csvPath: result.path,
|
||||||
|
saveAsXlsx: devkitSaveXlsx
|
||||||
|
});
|
||||||
|
|
||||||
|
if (processResult.ok) {
|
||||||
|
devkitLastResult = {
|
||||||
|
outputPath: processResult.outputPath,
|
||||||
|
groupsUsed: processResult.groupsUsed,
|
||||||
|
meanValue: processResult.meanValue,
|
||||||
|
threshold: processResult.threshold,
|
||||||
|
rowsTotal: processResult.rowsTotal,
|
||||||
|
rowsKept: processResult.rowsKept
|
||||||
|
};
|
||||||
|
|
||||||
|
connectionNotice =
|
||||||
|
locale === "zh-CN"
|
||||||
|
? `CSV 已导出并完成 DevKit 处理(${result.frameCount} 帧):${processResult.outputPath}`
|
||||||
|
: `CSV exported and processed by DevKit (${result.frameCount} frames): ${processResult.outputPath}`;
|
||||||
|
connectionNoticeTone = "ok";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionNotice =
|
||||||
|
locale === "zh-CN"
|
||||||
|
? `CSV 已导出,但 DevKit 处理失败:${processResult.message}`
|
||||||
|
: `CSV exported, but DevKit processing failed: ${processResult.message}`;
|
||||||
|
connectionNoticeTone = "warn";
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
connectionNotice =
|
||||||
|
locale === "zh-CN"
|
||||||
|
? "CSV 已导出,但 DevKit 后处理调用失败。"
|
||||||
|
: "CSV exported, but DevKit post-processing failed.";
|
||||||
|
connectionNoticeTone = "warn";
|
||||||
|
console.error("DevKit export post-process failed:", error);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
connectionNotice =
|
connectionNotice =
|
||||||
locale === "zh-CN"
|
locale === "zh-CN"
|
||||||
? `CSV 导出成功(${result.frameCount} 帧):${result.path}`
|
? `CSV 导出成功(${result.frameCount} 帧):${result.path}`
|
||||||
@@ -1477,17 +1626,27 @@
|
|||||||
if (event.detail === "precision-test") {
|
if (event.detail === "precision-test") {
|
||||||
isPrecisionTestOpen = !isPrecisionTestOpen;
|
isPrecisionTestOpen = !isPrecisionTestOpen;
|
||||||
isConfigPanelOpen = false;
|
isConfigPanelOpen = false;
|
||||||
|
isDevKitConfigOpen = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.detail === "settings") {
|
if (event.detail === "settings") {
|
||||||
isPrecisionTestOpen = false;
|
isPrecisionTestOpen = false;
|
||||||
isConfigPanelOpen = !isConfigPanelOpen;
|
isConfigPanelOpen = !isConfigPanelOpen;
|
||||||
|
isDevKitConfigOpen = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.detail === "devkit") {
|
||||||
|
isPrecisionTestOpen = false;
|
||||||
|
isConfigPanelOpen = false;
|
||||||
|
isDevKitConfigOpen = !isDevKitConfigOpen;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isPrecisionTestOpen = false;
|
isPrecisionTestOpen = false;
|
||||||
isConfigPanelOpen = false;
|
isConfigPanelOpen = false;
|
||||||
|
isDevKitConfigOpen = false;
|
||||||
activeConfigLinkId = event.detail;
|
activeConfigLinkId = event.detail;
|
||||||
console.info("[hud] config link clicked:", event.detail);
|
console.info("[hud] config link clicked:", event.detail);
|
||||||
}
|
}
|
||||||
@@ -1511,6 +1670,55 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── DevKit Functions ────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function pollDevKitStatus(): Promise<void> {
|
||||||
|
if (!isTauriRuntime()) return;
|
||||||
|
try {
|
||||||
|
const status = await invoke<{
|
||||||
|
enabled: boolean;
|
||||||
|
running: boolean;
|
||||||
|
port: number;
|
||||||
|
framesSent: number;
|
||||||
|
config: { filterLiftEnabled: boolean; saveAsXlsx: boolean };
|
||||||
|
}>("devkit_status");
|
||||||
|
devkitEnabled = status.enabled;
|
||||||
|
devkitRunning = status.running;
|
||||||
|
devkitPort = status.port;
|
||||||
|
devkitFramesSent = status.framesSent;
|
||||||
|
devkitFilterLift = status.config.filterLiftEnabled;
|
||||||
|
devkitSaveXlsx = status.config.saveAsXlsx;
|
||||||
|
} catch {
|
||||||
|
devkitEnabled = false;
|
||||||
|
devkitRunning = false;
|
||||||
|
isDevKitConfigOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDevKitToggleFilterLift(): Promise<void> {
|
||||||
|
if (!isTauriRuntime()) return;
|
||||||
|
try {
|
||||||
|
const newConfig = { filterLiftEnabled: !devkitFilterLift, saveAsXlsx: devkitSaveXlsx };
|
||||||
|
const result = await invoke<{ filterLiftEnabled: boolean; saveAsXlsx: boolean }>("devkit_set_config", { config: newConfig });
|
||||||
|
devkitFilterLift = result.filterLiftEnabled;
|
||||||
|
devkitSaveXlsx = result.saveAsXlsx;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("DevKit config update failed:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDevKitToggleXlsx(): Promise<void> {
|
||||||
|
if (!isTauriRuntime()) return;
|
||||||
|
try {
|
||||||
|
const newConfig = { filterLiftEnabled: devkitFilterLift, saveAsXlsx: !devkitSaveXlsx };
|
||||||
|
const result = await invoke<{ filterLiftEnabled: boolean; saveAsXlsx: boolean }>("devkit_set_config", { config: newConfig });
|
||||||
|
devkitFilterLift = result.filterLiftEnabled;
|
||||||
|
devkitSaveXlsx = result.saveAsXlsx;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("DevKit config update failed:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleMatrixDisplayToggle(event: CustomEvent<boolean>): void {
|
function handleMatrixDisplayToggle(event: CustomEvent<boolean>): void {
|
||||||
matrixDisplayMode = event.detail ? "dots" : "numeric";
|
matrixDisplayMode = event.detail ? "dots" : "numeric";
|
||||||
}
|
}
|
||||||
@@ -1526,6 +1734,9 @@
|
|||||||
|
|
||||||
if (isTauriRuntime()) {
|
if (isTauriRuntime()) {
|
||||||
void refreshSerialPorts();
|
void refreshSerialPorts();
|
||||||
|
void checkForAppUpdate();
|
||||||
|
void pollDevKitStatus();
|
||||||
|
devkitStatusTimer = window.setInterval(() => void pollDevKitStatus(), 3000);
|
||||||
void startTauriHudStream(applyPacket)
|
void startTauriHudStream(applyPacket)
|
||||||
.then((unlisten) => {
|
.then((unlisten) => {
|
||||||
if (disposed) {
|
if (disposed) {
|
||||||
@@ -1547,6 +1758,10 @@
|
|||||||
pauseReplayPlayback();
|
pauseReplayPlayback();
|
||||||
stopMockFeed?.();
|
stopMockFeed?.();
|
||||||
unlistenHudStream?.();
|
unlistenHudStream?.();
|
||||||
|
if (devkitStatusTimer != null) {
|
||||||
|
window.clearInterval(devkitStatusTimer);
|
||||||
|
devkitStatusTimer = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -1591,6 +1806,10 @@
|
|||||||
importActionLabel={uiCopy.importActionLabel}
|
importActionLabel={uiCopy.importActionLabel}
|
||||||
{connectionNotice}
|
{connectionNotice}
|
||||||
{connectionNoticeTone}
|
{connectionNoticeTone}
|
||||||
|
noticeConfirmLabel={locale === "zh-CN" ? "确定" : "Confirm"}
|
||||||
|
noticeCancelLabel={locale === "zh-CN" ? "取消" : "Cancel"}
|
||||||
|
noticeShowActions={updateNoticeVisible}
|
||||||
|
noticeActionBusy={updateInstallBusy}
|
||||||
{configLinks}
|
{configLinks}
|
||||||
{isRefreshingPorts}
|
{isRefreshingPorts}
|
||||||
{isExporting}
|
{isExporting}
|
||||||
@@ -1606,7 +1825,12 @@
|
|||||||
on:serialconnect={handleSerialConnect}
|
on:serialconnect={handleSerialConnect}
|
||||||
on:serialexport={handleSerialExportRequest}
|
on:serialexport={handleSerialExportRequest}
|
||||||
on:csvimport={handleReplayImportRequest}
|
on:csvimport={handleReplayImportRequest}
|
||||||
on:noticeclear={() => (connectionNotice = "")}
|
on:noticeclear={() => {
|
||||||
|
connectionNotice = "";
|
||||||
|
updateNoticeVisible = false;
|
||||||
|
}}
|
||||||
|
on:noticeconfirm={handleUpdateConfirm}
|
||||||
|
on:noticecancel={handleUpdateCancel}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CenterStage
|
<CenterStage
|
||||||
@@ -1693,6 +1917,25 @@
|
|||||||
on:navigate={handleFileExplorerNavigate}
|
on:navigate={handleFileExplorerNavigate}
|
||||||
on:confirm={handleFileExplorerConfirm}
|
on:confirm={handleFileExplorerConfirm}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{#if isDevKitConfigOpen && devkitEnabled}
|
||||||
|
<div class="devkit-overlay" role="dialog" aria-label="DevKit Config">
|
||||||
|
<div class="devkit-float">
|
||||||
|
<DevKitConfigPanel
|
||||||
|
running={devkitRunning}
|
||||||
|
port={devkitPort}
|
||||||
|
framesSent={devkitFramesSent}
|
||||||
|
filterLiftEnabled={devkitFilterLift}
|
||||||
|
saveAsXlsx={devkitSaveXlsx}
|
||||||
|
locale={locale}
|
||||||
|
lastProcessResult={devkitLastResult}
|
||||||
|
on:close={() => (isDevKitConfigOpen = false)}
|
||||||
|
on:togglefilterlift={handleDevKitToggleFilterLift}
|
||||||
|
on:togglexlsx={handleDevKitToggleXlsx}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -1871,4 +2114,20 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.devkit-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgb(0 0 0 / 0.5);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.devkit-float {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user