fix: 修复打砖块游戏碰撞穿透bug,添加渐进提速机制
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
@echo off
|
@echo off
|
||||||
REM ── JE-Skin DevKit: 打包 Python gRPC server 为 exe ──
|
REM ── JE-Skin DevKit: 打包 Python gRPC server 为 exe ──
|
||||||
REM 前提: pip install pyinstaller grpcio grpcio-tools openpyxl
|
REM 前提: pip install pyinstaller grpcio grpcio-tools numpy openpyxl
|
||||||
|
|
||||||
echo [1/3] Generating gRPC stubs...
|
echo [1/3] Generating gRPC stubs...
|
||||||
python -m grpc_tools.protoc ^
|
python -m grpc_tools.protoc ^
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ a = Analysis(
|
|||||||
['sensor_server.py'],
|
['sensor_server.py'],
|
||||||
pathex=[],
|
pathex=[],
|
||||||
binaries=[],
|
binaries=[],
|
||||||
datas=[],
|
datas=[('sensor_stream_pb2.py', '.'), ('sensor_stream_pb2_grpc.py', '.')],
|
||||||
hiddenimports=['grpc', 'openpyxl'],
|
hiddenimports=['grpc', 'openpyxl', 'numpy'],
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
hooksconfig={},
|
hooksconfig={},
|
||||||
runtime_hooks=[],
|
runtime_hooks=[],
|
||||||
|
|||||||
@@ -234,30 +234,63 @@ class SensorPushServicer(sensor_stream_pb2_grpc.SensorPushServicer):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.frame_count = 0
|
self.frame_count = 0
|
||||||
self.last_report_time = time.time()
|
self.last_report_time = time.time()
|
||||||
|
self.last_angle = None
|
||||||
|
|
||||||
def Upload(self, request_iterator, context):
|
def Upload(self, request_iterator, context):
|
||||||
print("[SensorPush] Client connected, waiting for frames...")
|
print("[SensorPush] Client connected, waiting for frames...")
|
||||||
|
reset_baseline()
|
||||||
|
self.last_angle = None
|
||||||
|
|
||||||
for frame in request_iterator:
|
for frame in request_iterator:
|
||||||
self.frame_count += 1
|
self.frame_count += 1
|
||||||
|
angle = 0.0
|
||||||
|
ok = True
|
||||||
|
message = "OK"
|
||||||
|
if len(frame.matrix) == SENSOR_ROWS * SENSOR_COLS:
|
||||||
|
try:
|
||||||
|
angle = get_pzt_angle(frame.matrix)
|
||||||
|
self.last_angle = angle
|
||||||
|
if self.frame_count <= 10 or self.frame_count % 30 == 0:
|
||||||
|
print(
|
||||||
|
f"[SensorPush] PZT angle frame #{frame.seq} "
|
||||||
|
f"dts={frame.dts_ms} angle={angle:.2f}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
ok = False
|
||||||
|
message = str(e)
|
||||||
|
print(f"[SensorPush] PZT compute error on frame #{frame.seq}: {e}")
|
||||||
|
else:
|
||||||
|
ok = False
|
||||||
|
message = f"Invalid matrix length: {len(frame.matrix)}"
|
||||||
|
|
||||||
|
yield sensor_stream_pb2.PztAngleResponse(
|
||||||
|
seq=frame.seq,
|
||||||
|
timestamp_ms=frame.timestamp_ms,
|
||||||
|
angle=angle,
|
||||||
|
dts_ms=frame.dts_ms,
|
||||||
|
ok=ok,
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
||||||
if self.frame_count % 100 == 0:
|
if self.frame_count % 100 == 0:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
elapsed = now - self.last_report_time
|
elapsed = now - self.last_report_time
|
||||||
fps = 100 / elapsed if elapsed > 0 else 0
|
fps = 100 / elapsed if elapsed > 0 else 0
|
||||||
self.last_report_time = now
|
self.last_report_time = now
|
||||||
|
angle_text = (
|
||||||
|
f"{self.last_angle:.2f}"
|
||||||
|
if self.last_angle is not None
|
||||||
|
else "n/a"
|
||||||
|
)
|
||||||
print(
|
print(
|
||||||
f"[SensorPush] Frame #{frame.seq} | "
|
f"[SensorPush] Frame #{frame.seq} | "
|
||||||
f"{frame.rows}x{frame.cols} | "
|
f"{frame.rows}x{frame.cols} | "
|
||||||
|
f"angle={angle_text} | "
|
||||||
f"force={frame.resultant_force:.1f} | "
|
f"force={frame.resultant_force:.1f} | "
|
||||||
f"total={self.frame_count} | ~{fps:.1f} fps"
|
f"total={self.frame_count} | ~{fps:.1f} fps"
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"[SensorPush] Stream ended. Total: {self.frame_count}")
|
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):
|
class ExportProcessorServicer(sensor_stream_pb2_grpc.ExportProcessorServicer):
|
||||||
@@ -316,6 +349,117 @@ def serve(port: int):
|
|||||||
server.wait_for_termination()
|
server.wait_for_termination()
|
||||||
|
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import threading
|
||||||
|
|
||||||
|
# ===================== 算法参数=====================
|
||||||
|
TOTAL_PRESSURE_LOW_THRESHOLD = 500
|
||||||
|
COP_STABILITY_FRAMES_REQUIRED = 5
|
||||||
|
SENSOR_ROWS = 12
|
||||||
|
SENSOR_COLS = 7
|
||||||
|
|
||||||
|
# ===================== 线程安全全局状态 =====================
|
||||||
|
first_frame = None
|
||||||
|
first_frame_lock = threading.Lock()
|
||||||
|
|
||||||
|
first_contact_CoP_x = None
|
||||||
|
first_contact_CoP_y = None
|
||||||
|
contact_initialized = False
|
||||||
|
|
||||||
|
total_pressure_low_counter = 0
|
||||||
|
|
||||||
|
# ===================== 基线减除 =====================
|
||||||
|
def subtract_baseline(current_frame):
|
||||||
|
global first_frame
|
||||||
|
current_frame = np.array(current_frame, dtype=np.float32).flatten()
|
||||||
|
|
||||||
|
with first_frame_lock:
|
||||||
|
if first_frame is None:
|
||||||
|
first_frame = current_frame.copy()
|
||||||
|
|
||||||
|
diff = current_frame - first_frame
|
||||||
|
return np.clip(diff, 0, None)
|
||||||
|
|
||||||
|
# ===================== 重置CoP状态 =====================
|
||||||
|
def reset_cop_state():
|
||||||
|
global first_contact_CoP_x, first_contact_CoP_y, contact_initialized
|
||||||
|
global total_pressure_low_counter
|
||||||
|
|
||||||
|
first_contact_CoP_x = None
|
||||||
|
first_contact_CoP_y = None
|
||||||
|
contact_initialized = False
|
||||||
|
total_pressure_low_counter = 0
|
||||||
|
|
||||||
|
# ===================== CoP压力中心计算 =====================
|
||||||
|
def compute_pressure_direction(baseline_subtracted_frame):
|
||||||
|
global first_contact_CoP_x, first_contact_CoP_y, contact_initialized
|
||||||
|
global total_pressure_low_counter
|
||||||
|
|
||||||
|
rows, cols = SENSOR_ROWS, SENSOR_COLS
|
||||||
|
frame_flat = np.asarray(baseline_subtracted_frame, dtype=np.float32).flatten()
|
||||||
|
frame2d = frame_flat.reshape(rows, cols)
|
||||||
|
|
||||||
|
total_pressure = np.sum(frame2d)
|
||||||
|
if total_pressure < TOTAL_PRESSURE_LOW_THRESHOLD:
|
||||||
|
total_pressure_low_counter += 1
|
||||||
|
else:
|
||||||
|
total_pressure_low_counter = 0
|
||||||
|
|
||||||
|
if total_pressure_low_counter >= COP_STABILITY_FRAMES_REQUIRED:
|
||||||
|
reset_cop_state()
|
||||||
|
return 0.0, 0.0
|
||||||
|
|
||||||
|
if total_pressure == 0:
|
||||||
|
return 0.0, 0.0
|
||||||
|
|
||||||
|
x_grid = np.tile(np.arange(cols), (rows, 1))
|
||||||
|
y_grid = np.repeat(np.arange(rows), cols).reshape(rows, cols)
|
||||||
|
cop_x = np.sum(frame2d * x_grid) / total_pressure
|
||||||
|
cop_y = np.sum(frame2d * y_grid) / total_pressure
|
||||||
|
|
||||||
|
delta_CoP_x = 0.0
|
||||||
|
delta_CoP_y = 0.0
|
||||||
|
|
||||||
|
if not contact_initialized:
|
||||||
|
first_contact_CoP_x = cop_x
|
||||||
|
first_contact_CoP_y = cop_y
|
||||||
|
contact_initialized = True
|
||||||
|
else:
|
||||||
|
delta_CoP_x = cop_x - first_contact_CoP_x
|
||||||
|
delta_CoP_y = cop_y - first_contact_CoP_y
|
||||||
|
|
||||||
|
return delta_CoP_x, delta_CoP_y
|
||||||
|
|
||||||
|
# ===================== 角度计算核心 =====================
|
||||||
|
def compute_vector_angle(x: float, y: float) -> tuple[float, float]:
|
||||||
|
epsilon = 1e-8
|
||||||
|
mag = np.hypot(x, y)
|
||||||
|
angle = np.degrees(np.arctan2(y, x + epsilon))
|
||||||
|
if angle < 0:
|
||||||
|
angle += 360
|
||||||
|
return angle, mag
|
||||||
|
|
||||||
|
def compute_PZT_angle(Px: float, Py: float) -> tuple[float, float]:
|
||||||
|
return compute_vector_angle(Px, -Py)
|
||||||
|
|
||||||
|
# ===================== 核心入口函数 =====================
|
||||||
|
def get_pzt_angle(adc_data):
|
||||||
|
if len(adc_data) != 84:
|
||||||
|
raise ValueError("ADC数据长度必须为84")
|
||||||
|
baseline_subtracted = subtract_baseline(adc_data)
|
||||||
|
dx, dy = compute_pressure_direction(baseline_subtracted)
|
||||||
|
pzt_angle, _ = compute_PZT_angle(dx, dy)
|
||||||
|
|
||||||
|
return pzt_angle
|
||||||
|
|
||||||
|
# ===================== 重置基线(校准用) =====================
|
||||||
|
def reset_baseline():
|
||||||
|
global first_frame
|
||||||
|
with first_frame_lock:
|
||||||
|
first_frame = None
|
||||||
|
reset_cop_state()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="JE-Skin DevKit gRPC Server")
|
parser = argparse.ArgumentParser(description="JE-Skin DevKit gRPC Server")
|
||||||
parser.add_argument("--port", type=int, default=50051, help="gRPC listen port (default: 50051)")
|
parser.add_argument("--port", type=int, default=50051, help="gRPC listen port (default: 50051)")
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ _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')
|
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\"q\n\x10PztAngleResponse\x12\x0b\n\x03seq\x18\x01 \x01(\x04\x12\x14\n\x0ctimestamp_ms\x18\x02 \x01(\x04\x12\r\n\x05\x61ngle\x18\x03 \x01(\x02\x12\x0e\n\x06\x64ts_ms\x18\x04 \x01(\r\x12\n\n\x02ok\x18\x05 \x01(\x08\x12\x0f\n\x07message\x18\x06 \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(\t2W\n\nSensorPush\x12I\n\x06Upload\x12\x1a.sensor_stream.SensorFrame\x1a\x1f.sensor_stream.PztAngleResponse(\x01\x30\x01\x32_\n\x0f\x45xportProcessor\x12L\n\x0bProcessFile\x12\x1d.sensor_stream.ProcessRequest\x1a\x1e.sensor_stream.ProcessResponseb\x06proto3')
|
||||||
|
|
||||||
_globals = globals()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
@@ -33,14 +33,14 @@ if not _descriptor._USE_C_DESCRIPTORS:
|
|||||||
DESCRIPTOR._loaded_options = None
|
DESCRIPTOR._loaded_options = None
|
||||||
_globals['_SENSORFRAME']._serialized_start=39
|
_globals['_SENSORFRAME']._serialized_start=39
|
||||||
_globals['_SENSORFRAME']._serialized_end=172
|
_globals['_SENSORFRAME']._serialized_end=172
|
||||||
_globals['_UPLOADRESPONSE']._serialized_start=174
|
_globals['_PZTANGLERESPONSE']._serialized_start=174
|
||||||
_globals['_UPLOADRESPONSE']._serialized_end=244
|
_globals['_PZTANGLERESPONSE']._serialized_end=287
|
||||||
_globals['_PROCESSREQUEST']._serialized_start=246
|
_globals['_PROCESSREQUEST']._serialized_start=289
|
||||||
_globals['_PROCESSREQUEST']._serialized_end=302
|
_globals['_PROCESSREQUEST']._serialized_end=345
|
||||||
_globals['_PROCESSRESPONSE']._serialized_start=305
|
_globals['_PROCESSRESPONSE']._serialized_start=348
|
||||||
_globals['_PROCESSRESPONSE']._serialized_end=471
|
_globals['_PROCESSRESPONSE']._serialized_end=514
|
||||||
_globals['_SENSORPUSH']._serialized_start=473
|
_globals['_SENSORPUSH']._serialized_start=516
|
||||||
_globals['_SENSORPUSH']._serialized_end=556
|
_globals['_SENSORPUSH']._serialized_end=603
|
||||||
_globals['_EXPORTPROCESSOR']._serialized_start=558
|
_globals['_EXPORTPROCESSOR']._serialized_start=605
|
||||||
_globals['_EXPORTPROCESSOR']._serialized_end=653
|
_globals['_EXPORTPROCESSOR']._serialized_end=700
|
||||||
# @@protoc_insertion_point(module_scope)
|
# @@protoc_insertion_point(module_scope)
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ if _version_not_supported:
|
|||||||
|
|
||||||
|
|
||||||
class SensorPushStub(object):
|
class SensorPushStub(object):
|
||||||
"""传感器数据推送服务 —— Rust 端作为 gRPC client 推送实时帧
|
"""Missing associated documentation comment in .proto file."""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, channel):
|
def __init__(self, channel):
|
||||||
"""Constructor.
|
"""Constructor.
|
||||||
@@ -35,16 +34,15 @@ class SensorPushStub(object):
|
|||||||
Args:
|
Args:
|
||||||
channel: A grpc.Channel.
|
channel: A grpc.Channel.
|
||||||
"""
|
"""
|
||||||
self.Upload = channel.stream_unary(
|
self.Upload = channel.stream_stream(
|
||||||
'/sensor_stream.SensorPush/Upload',
|
'/sensor_stream.SensorPush/Upload',
|
||||||
request_serializer=sensor__stream__pb2.SensorFrame.SerializeToString,
|
request_serializer=sensor__stream__pb2.SensorFrame.SerializeToString,
|
||||||
response_deserializer=sensor__stream__pb2.UploadResponse.FromString,
|
response_deserializer=sensor__stream__pb2.PztAngleResponse.FromString,
|
||||||
_registered_method=True)
|
_registered_method=True)
|
||||||
|
|
||||||
|
|
||||||
class SensorPushServicer(object):
|
class SensorPushServicer(object):
|
||||||
"""传感器数据推送服务 —— Rust 端作为 gRPC client 推送实时帧
|
"""Missing associated documentation comment in .proto file."""
|
||||||
"""
|
|
||||||
|
|
||||||
def Upload(self, request_iterator, context):
|
def Upload(self, request_iterator, context):
|
||||||
"""Missing associated documentation comment in .proto file."""
|
"""Missing associated documentation comment in .proto file."""
|
||||||
@@ -55,10 +53,10 @@ class SensorPushServicer(object):
|
|||||||
|
|
||||||
def add_SensorPushServicer_to_server(servicer, server):
|
def add_SensorPushServicer_to_server(servicer, server):
|
||||||
rpc_method_handlers = {
|
rpc_method_handlers = {
|
||||||
'Upload': grpc.stream_unary_rpc_method_handler(
|
'Upload': grpc.stream_stream_rpc_method_handler(
|
||||||
servicer.Upload,
|
servicer.Upload,
|
||||||
request_deserializer=sensor__stream__pb2.SensorFrame.FromString,
|
request_deserializer=sensor__stream__pb2.SensorFrame.FromString,
|
||||||
response_serializer=sensor__stream__pb2.UploadResponse.SerializeToString,
|
response_serializer=sensor__stream__pb2.PztAngleResponse.SerializeToString,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
generic_handler = grpc.method_handlers_generic_handler(
|
generic_handler = grpc.method_handlers_generic_handler(
|
||||||
@@ -69,8 +67,7 @@ def add_SensorPushServicer_to_server(servicer, server):
|
|||||||
|
|
||||||
# This class is part of an EXPERIMENTAL API.
|
# This class is part of an EXPERIMENTAL API.
|
||||||
class SensorPush(object):
|
class SensorPush(object):
|
||||||
"""传感器数据推送服务 —— Rust 端作为 gRPC client 推送实时帧
|
"""Missing associated documentation comment in .proto file."""
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def Upload(request_iterator,
|
def Upload(request_iterator,
|
||||||
@@ -83,12 +80,12 @@ class SensorPush(object):
|
|||||||
wait_for_ready=None,
|
wait_for_ready=None,
|
||||||
timeout=None,
|
timeout=None,
|
||||||
metadata=None):
|
metadata=None):
|
||||||
return grpc.experimental.stream_unary(
|
return grpc.experimental.stream_stream(
|
||||||
request_iterator,
|
request_iterator,
|
||||||
target,
|
target,
|
||||||
'/sensor_stream.SensorPush/Upload',
|
'/sensor_stream.SensorPush/Upload',
|
||||||
sensor__stream__pb2.SensorFrame.SerializeToString,
|
sensor__stream__pb2.SensorFrame.SerializeToString,
|
||||||
sensor__stream__pb2.UploadResponse.FromString,
|
sensor__stream__pb2.PztAngleResponse.FromString,
|
||||||
options,
|
options,
|
||||||
channel_credentials,
|
channel_credentials,
|
||||||
insecure,
|
insecure,
|
||||||
@@ -101,8 +98,7 @@ class SensorPush(object):
|
|||||||
|
|
||||||
|
|
||||||
class ExportProcessorStub(object):
|
class ExportProcessorStub(object):
|
||||||
"""导出后处理服务 —— Rust 导出 CSV 后将路径发给 Python 做梯度过滤
|
"""Missing associated documentation comment in .proto file."""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, channel):
|
def __init__(self, channel):
|
||||||
"""Constructor.
|
"""Constructor.
|
||||||
@@ -118,8 +114,7 @@ class ExportProcessorStub(object):
|
|||||||
|
|
||||||
|
|
||||||
class ExportProcessorServicer(object):
|
class ExportProcessorServicer(object):
|
||||||
"""导出后处理服务 —— Rust 导出 CSV 后将路径发给 Python 做梯度过滤
|
"""Missing associated documentation comment in .proto file."""
|
||||||
"""
|
|
||||||
|
|
||||||
def ProcessFile(self, request, context):
|
def ProcessFile(self, request, context):
|
||||||
"""Missing associated documentation comment in .proto file."""
|
"""Missing associated documentation comment in .proto file."""
|
||||||
@@ -144,8 +139,7 @@ def add_ExportProcessorServicer_to_server(servicer, server):
|
|||||||
|
|
||||||
# This class is part of an EXPERIMENTAL API.
|
# This class is part of an EXPERIMENTAL API.
|
||||||
class ExportProcessor(object):
|
class ExportProcessor(object):
|
||||||
"""导出后处理服务 —— Rust 导出 CSV 后将路径发给 Python 做梯度过滤
|
"""Missing associated documentation comment in .proto file."""
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def ProcessFile(request,
|
def ProcessFile(request,
|
||||||
|
|||||||
48
src-tauri/Cargo.lock
generated
48
src-tauri/Cargo.lock
generated
@@ -18,6 +18,7 @@ dependencies = [
|
|||||||
"futures-util",
|
"futures-util",
|
||||||
"humantime",
|
"humantime",
|
||||||
"log",
|
"log",
|
||||||
|
"ndarray",
|
||||||
"prost",
|
"prost",
|
||||||
"prost-types",
|
"prost-types",
|
||||||
"protoc-bin-vendored",
|
"protoc-bin-vendored",
|
||||||
@@ -2441,6 +2442,16 @@ version = "0.8.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matrixmultiply"
|
||||||
|
version = "0.3.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"rawpointer",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.0"
|
version = "2.8.0"
|
||||||
@@ -2530,6 +2541,19 @@ version = "0.10.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
|
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ndarray"
|
||||||
|
version = "0.15.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32"
|
||||||
|
dependencies = [
|
||||||
|
"matrixmultiply",
|
||||||
|
"num-complex",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
"rawpointer",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ndk"
|
name = "ndk"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -2595,12 +2619,30 @@ version = "0.1.14"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-complex"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-integer"
|
||||||
|
version = "0.1.46"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -3605,6 +3647,12 @@ version = "0.6.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rawpointer"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
|||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
devkit = ["dep:tonic", "dep:prost", "dep:prost-types", "dep:async-stream", "dep:dirs"]
|
devkit = ["dep:tonic", "dep:prost", "dep:prost-types", "dep:async-stream", "dep:dirs"]
|
||||||
|
multi-dim = ["dep:ndarray"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
@@ -51,6 +52,7 @@ futures-util = "0.3"
|
|||||||
uuid = { version = "1", features = ["v4", "serde"] }
|
uuid = { version = "1", features = ["v4", "serde"] }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||||
|
ndarray = { version = "0.15", optional = true }
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-updater = "2"
|
tauri-plugin-updater = "2"
|
||||||
|
|||||||
@@ -2,17 +2,14 @@ syntax = "proto3";
|
|||||||
|
|
||||||
package sensor_stream;
|
package sensor_stream;
|
||||||
|
|
||||||
// 传感器数据推送服务 —— Rust 端作为 gRPC client 推送实时帧
|
|
||||||
service SensorPush {
|
service SensorPush {
|
||||||
rpc Upload (stream SensorFrame) returns (UploadResponse);
|
rpc Upload(stream SensorFrame) returns (stream PztAngleResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出后处理服务 —— Rust 导出 CSV 后将路径发给 Python 做梯度过滤
|
|
||||||
service ExportProcessor {
|
service ExportProcessor {
|
||||||
rpc ProcessFile(ProcessRequest) returns (ProcessResponse);
|
rpc ProcessFile(ProcessRequest) returns (ProcessResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 一帧传感器数据
|
|
||||||
message SensorFrame {
|
message SensorFrame {
|
||||||
uint64 seq = 1;
|
uint64 seq = 1;
|
||||||
uint64 timestamp_ms = 2;
|
uint64 timestamp_ms = 2;
|
||||||
@@ -23,27 +20,27 @@ message SensorFrame {
|
|||||||
uint32 dts_ms = 7;
|
uint32 dts_ms = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上传确认响应
|
message PztAngleResponse {
|
||||||
message UploadResponse {
|
uint64 seq = 1;
|
||||||
bool ok = 1;
|
uint64 timestamp_ms = 2;
|
||||||
uint64 frames_received = 2;
|
float angle = 3;
|
||||||
string message = 3;
|
uint32 dts_ms = 4;
|
||||||
|
bool ok = 5;
|
||||||
|
string message = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出处理请求
|
|
||||||
message ProcessRequest {
|
message ProcessRequest {
|
||||||
string csv_path = 1; // 导出的 CSV 文件路径
|
string csv_path = 1;
|
||||||
bool save_as_xlsx = 2; // 是否以 xlsx 保存(删除源 CSV)
|
bool save_as_xlsx = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出处理响应
|
|
||||||
message ProcessResponse {
|
message ProcessResponse {
|
||||||
bool ok = 1;
|
bool ok = 1;
|
||||||
string output_path = 2; // 输出文件路径
|
string output_path = 2;
|
||||||
uint32 groups_used = 3; // 分组数
|
uint32 groups_used = 3;
|
||||||
double mean_value = 4; // 均值
|
double mean_value = 4;
|
||||||
double threshold = 5; // 梯度阈值
|
double threshold = 5;
|
||||||
uint32 rows_total = 6; // 原始行数
|
uint32 rows_total = 6;
|
||||||
uint32 rows_kept = 7; // 保留行数
|
uint32 rows_kept = 7;
|
||||||
string message = 8;
|
string message = 8;
|
||||||
}
|
}
|
||||||
Binary file not shown.
@@ -3,6 +3,8 @@
|
|||||||
//! 仅在 `devkit` feature 启用时编译。
|
//! 仅在 `devkit` feature 启用时编译。
|
||||||
|
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
#[cfg(feature = "devkit")]
|
||||||
|
use tauri::AppHandle;
|
||||||
|
|
||||||
use crate::devkit::{DevKitConfig, DevKitState, DevKitStatusSnapshot, ExportProcessResult};
|
use crate::devkit::{DevKitConfig, DevKitState, DevKitStatusSnapshot, ExportProcessResult};
|
||||||
|
|
||||||
@@ -12,9 +14,13 @@ pub fn devkit_status(state: State<'_, DevKitState>) -> DevKitStatusSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn devkit_start(state: State<'_, DevKitState>, port: Option<u16>) -> Result<DevKitStatusSnapshot, String> {
|
pub async fn devkit_start(
|
||||||
|
app: AppHandle,
|
||||||
|
state: State<'_, DevKitState>,
|
||||||
|
port: Option<u16>,
|
||||||
|
) -> Result<DevKitStatusSnapshot, String> {
|
||||||
let target_port = port.unwrap_or(50051);
|
let target_port = port.unwrap_or(50051);
|
||||||
state.start(target_port).await?;
|
state.start(app, target_port).await?;
|
||||||
Ok(state.status())
|
Ok(state.status())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,24 @@ use std::path::PathBuf;
|
|||||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tauri::{AppHandle, Emitter};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use super::proto::sensor_push_client::SensorPushClient;
|
use super::proto::sensor_push_client::SensorPushClient;
|
||||||
use super::proto::export_processor_client::ExportProcessorClient;
|
use super::proto::export_processor_client::ExportProcessorClient;
|
||||||
use super::proto::{ProcessRequest, SensorFrame};
|
use super::proto::{ProcessRequest, SensorFrame};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct DevKitPztAngleEvent {
|
||||||
|
seq: u64,
|
||||||
|
timestamp_ms: u64,
|
||||||
|
dts_ms: u32,
|
||||||
|
angle: f32,
|
||||||
|
}
|
||||||
|
|
||||||
// ── DevKit 配置 ────────────────────────────────────────────────────
|
// ── DevKit 配置 ────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -145,7 +155,7 @@ impl DevKitState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 启动 gRPC client,连接到 Python server 并开始推送数据
|
/// 启动 gRPC client,连接到 Python server 并开始推送数据
|
||||||
pub async fn start(&self, port: u16) -> Result<(), String> {
|
pub async fn start(&self, app: AppHandle, port: u16) -> Result<(), String> {
|
||||||
if self.running.load(Ordering::SeqCst) {
|
if self.running.load(Ordering::SeqCst) {
|
||||||
return Err("AlreadyRunning".into());
|
return Err("AlreadyRunning".into());
|
||||||
}
|
}
|
||||||
@@ -161,9 +171,10 @@ impl DevKitState {
|
|||||||
|
|
||||||
let running = Arc::clone(&self.running);
|
let running = Arc::clone(&self.running);
|
||||||
let frame_count = Arc::clone(&self.frame_count);
|
let frame_count = Arc::clone(&self.frame_count);
|
||||||
|
let app_handle = app.clone();
|
||||||
|
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
if let Err(e) = run_grpc_upload(addr, rx, frame_count).await {
|
if let Err(e) = run_grpc_upload(app_handle, addr, rx, frame_count).await {
|
||||||
::log::error!("DevKit gRPC upload error: {e:?}");
|
::log::error!("DevKit gRPC upload error: {e:?}");
|
||||||
}
|
}
|
||||||
running.store(false, Ordering::SeqCst);
|
running.store(false, Ordering::SeqCst);
|
||||||
@@ -241,6 +252,7 @@ impl DevKitState {
|
|||||||
// ── gRPC Upload Client ─────────────────────────────────────────────
|
// ── gRPC Upload Client ─────────────────────────────────────────────
|
||||||
|
|
||||||
async fn run_grpc_upload(
|
async fn run_grpc_upload(
|
||||||
|
app: AppHandle,
|
||||||
addr: String,
|
addr: String,
|
||||||
mut rx: mpsc::Receiver<SensorFrame>,
|
mut rx: mpsc::Receiver<SensorFrame>,
|
||||||
frame_count: Arc<AtomicU32>,
|
frame_count: Arc<AtomicU32>,
|
||||||
@@ -255,14 +267,29 @@ async fn run_grpc_upload(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let response = client.upload(stream).await?;
|
let response = client.upload(stream).await?;
|
||||||
let resp = response.into_inner();
|
let mut inbound = response.into_inner();
|
||||||
|
|
||||||
::log::info!(
|
while let Some(message) = inbound.message().await? {
|
||||||
"DevKit upload complete: ok={}, frames={}, msg={}",
|
if message.ok {
|
||||||
resp.ok,
|
let payload = DevKitPztAngleEvent {
|
||||||
resp.frames_received,
|
seq: message.seq,
|
||||||
resp.message
|
timestamp_ms: message.timestamp_ms,
|
||||||
|
dts_ms: message.dts_ms,
|
||||||
|
angle: message.angle,
|
||||||
|
};
|
||||||
|
::log::debug!(
|
||||||
|
"python pzt angle: seq={} dts_ms={} angle={:.2}",
|
||||||
|
message.seq,
|
||||||
|
message.dts_ms,
|
||||||
|
message.angle
|
||||||
);
|
);
|
||||||
|
app.emit("devkit_pzt_angle", payload)?;
|
||||||
|
} else {
|
||||||
|
::log::warn!("DevKit PZT response error: {}", message.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::log::info!("DevKit upload stream closed");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -22,11 +22,43 @@ fn start_server_exe(exe_path: &std::path::Path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match command.spawn() {
|
match command.spawn() {
|
||||||
Ok(_) => ::log::info!("DevKit Python server started: {}", exe_path.display()),
|
Ok(_) => ::log::info!("DevKit Python server launched: {}", exe_path.display()),
|
||||||
Err(error) => ::log::warn!("Failed to start DevKit Python server: {error}"),
|
Err(error) => ::log::warn!("Failed to start DevKit Python server: {error}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "devkit")]
|
||||||
|
fn is_local_port_open(port: u16) -> bool {
|
||||||
|
use std::net::{SocketAddr, TcpStream};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||||
|
TcpStream::connect_timeout(&addr, Duration::from_millis(250)).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "devkit")]
|
||||||
|
fn find_server_exe(
|
||||||
|
resource_dir: &std::path::Path,
|
||||||
|
exe_name: &str,
|
||||||
|
) -> Option<std::path::PathBuf> {
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
candidates.push(resource_dir.join(exe_name));
|
||||||
|
|
||||||
|
if let Ok(current_exe) = std::env::current_exe() {
|
||||||
|
if let Some(parent) = current_exe.parent() {
|
||||||
|
candidates.push(parent.join(exe_name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(current_dir) = std::env::current_dir() {
|
||||||
|
candidates.push(current_dir.join("src-tauri").join("resources").join(exe_name));
|
||||||
|
candidates.push(current_dir.join("devkit").join("dist").join(exe_name));
|
||||||
|
candidates.push(current_dir.join("resources").join(exe_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates.into_iter().find(|path| path.exists())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
let builder = tauri::Builder::default()
|
let builder = tauri::Builder::default()
|
||||||
@@ -56,23 +88,22 @@ pub fn run() {
|
|||||||
.path()
|
.path()
|
||||||
.resource_dir()
|
.resource_dir()
|
||||||
.unwrap_or_else(|_| std::path::PathBuf::from("./resources"));
|
.unwrap_or_else(|_| std::path::PathBuf::from("./resources"));
|
||||||
|
let app_handle = app.handle().clone();
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let devkit_port = 50051u16;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let exe_name = "je-skin-devkit-server.exe";
|
let exe_name = "je-skin-devkit-server.exe";
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
let exe_name = "je-skin-devkit-server";
|
let exe_name = "je-skin-devkit-server";
|
||||||
|
|
||||||
let bundled_exe = resource_dir.join(exe_name);
|
if is_local_port_open(devkit_port) {
|
||||||
let fallback_exe = std::env::current_exe()
|
::log::info!(
|
||||||
.ok()
|
"DevKit port {} already in use, skipping Python server auto-start",
|
||||||
.and_then(|path| path.parent().map(|parent| parent.join(exe_name)));
|
devkit_port
|
||||||
|
);
|
||||||
let server_exe = if bundled_exe.exists() {
|
|
||||||
Some(bundled_exe)
|
|
||||||
} else {
|
} else {
|
||||||
fallback_exe.filter(|path| path.exists())
|
let server_exe = find_server_exe(&resource_dir, exe_name);
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(exe_path) = server_exe {
|
if let Some(exe_path) = server_exe {
|
||||||
start_server_exe(&exe_path);
|
start_server_exe(&exe_path);
|
||||||
@@ -80,11 +111,12 @@ pub fn run() {
|
|||||||
} else {
|
} else {
|
||||||
::log::info!("DevKit Python server not found, skipping auto-start");
|
::log::info!("DevKit Python server not found, skipping auto-start");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(error) = devkit_state_clone.start(50051).await {
|
if let Err(error) = devkit_state_clone.start(app_handle, devkit_port).await {
|
||||||
::log::warn!("DevKit auto-start failed: {error}");
|
::log::warn!("DevKit auto-start failed: {error}");
|
||||||
} else {
|
} else {
|
||||||
::log::info!("DevKit auto-started on 127.0.0.1:50051");
|
::log::info!("DevKit gRPC client initialized for 127.0.0.1:{devkit_port}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ pub mod model;
|
|||||||
pub mod serial;
|
pub mod serial;
|
||||||
pub mod record;
|
pub mod record;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
#[cfg(feature = "multi-dim")]
|
||||||
|
pub mod multi_dim_force;
|
||||||
|
|
||||||
pub type TestRecording = Recording<TestFrame>;
|
pub type TestRecording = Recording<TestFrame>;
|
||||||
pub type TactileARecording = Recording<TactileAFrame>;
|
pub type TactileARecording = Recording<TactileAFrame>;
|
||||||
|
|||||||
122
src-tauri/src/serial_core/multi_dim_force.rs
Normal file
122
src-tauri/src/serial_core/multi_dim_force.rs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
use ndarray::Array2;
|
||||||
|
|
||||||
|
const TOTAL_PRESSURE_LOW_THRESHOLD: usize = 500;
|
||||||
|
const COP_STABILITY_FRAMES_REQUIRED: usize = 5;
|
||||||
|
const SENSOR_ROWS: usize = 12;
|
||||||
|
const SENSOR_COLS: usize = 7;
|
||||||
|
|
||||||
|
pub struct PztProcessor {
|
||||||
|
first_frame: Option<Vec<f32>>,
|
||||||
|
first_contact_cop_x: Option<f32>,
|
||||||
|
first_contact_cop_y: Option<f32>,
|
||||||
|
contact_initialized: bool,
|
||||||
|
total_pressure_low_counter: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PztProcessor {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
first_frame: None,
|
||||||
|
first_contact_cop_x: None,
|
||||||
|
first_contact_cop_y: None,
|
||||||
|
contact_initialized: false,
|
||||||
|
total_pressure_low_counter: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subtract_baseline(&mut self, current_frame: &[f32]) -> Vec<f32> {
|
||||||
|
if self.first_frame.is_none() {
|
||||||
|
self.first_frame = Some(current_frame.to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseline = self.first_frame.as_ref().unwrap();
|
||||||
|
current_frame
|
||||||
|
.iter()
|
||||||
|
.zip(baseline.iter())
|
||||||
|
.map(|(c, b)| (c - b).max(0.0))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_cop_state(&mut self) {
|
||||||
|
self.first_contact_cop_x = None;
|
||||||
|
self.first_contact_cop_y = None;
|
||||||
|
self.contact_initialized = false;
|
||||||
|
self.total_pressure_low_counter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_pressure_direction(&mut self, frame: &[f32]) -> (f32, f32) {
|
||||||
|
let frame2d = Array2::from_shape_vec((SENSOR_ROWS, SENSOR_COLS), frame.to_vec()).unwrap();
|
||||||
|
let total_pressure: f32 = frame2d.sum();
|
||||||
|
if total_pressure < TOTAL_PRESSURE_LOW_THRESHOLD as f32 {
|
||||||
|
self.total_pressure_low_counter += 1;
|
||||||
|
} else {
|
||||||
|
self.total_pressure_low_counter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.total_pressure_low_counter >= COP_STABILITY_FRAMES_REQUIRED {
|
||||||
|
self.reset_cop_state();
|
||||||
|
return (0.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if total_pressure == 0.0 {
|
||||||
|
return (0.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sum_x = 0.0;
|
||||||
|
let mut sum_y = 0.0;
|
||||||
|
|
||||||
|
for r in 0..SENSOR_ROWS {
|
||||||
|
for c in 0..SENSOR_COLS {
|
||||||
|
let val = frame2d[(r, c)];
|
||||||
|
sum_x += val * c as f32;
|
||||||
|
sum_y += val * r as f32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cop_x = sum_x / total_pressure;
|
||||||
|
let cop_y = sum_y / total_pressure;
|
||||||
|
|
||||||
|
if !self.contact_initialized {
|
||||||
|
self.first_contact_cop_x = Some(cop_x);
|
||||||
|
self.first_contact_cop_y = Some(cop_y);
|
||||||
|
self.contact_initialized = true;
|
||||||
|
return (0.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dx = cop_x - self.first_contact_cop_x.unwrap();
|
||||||
|
let dy = cop_y - self.first_contact_cop_y.unwrap();
|
||||||
|
|
||||||
|
(dx, dy)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_vector_angle(x: f32, y: f32) -> (f32, f32) {
|
||||||
|
let epsilon = 1e-8;
|
||||||
|
let mag = (x * x + y * y).sqrt();
|
||||||
|
let mut angle = (y).atan2(x + epsilon).to_degrees();
|
||||||
|
if angle < 0.0 {
|
||||||
|
angle += 360.0;
|
||||||
|
}
|
||||||
|
(angle, mag)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_pzt_angle(px: f32, py: f32) -> (f32, f32) {
|
||||||
|
Self::compute_vector_angle(px, -py)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_pzt_angle(&mut self, adc_data: &[f32]) -> Result<f32, &'static str> {
|
||||||
|
if adc_data.len() != 84 {
|
||||||
|
return Err("ADC data length must be 84");
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseline = self.subtract_baseline(adc_data);
|
||||||
|
let (dx, dy) = self.compute_pressure_direction(&baseline);
|
||||||
|
let (angle, _) = Self::compute_pzt_angle(dx, dy);
|
||||||
|
|
||||||
|
Ok(angle)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_baseline(&mut self) {
|
||||||
|
self.first_frame = None;
|
||||||
|
self.reset_cop_state();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,17 @@ use crate::serial_core::codec::Codec;
|
|||||||
use crate::serial_core::codecs::tactile_a::TactileACodec;
|
use crate::serial_core::codecs::tactile_a::TactileACodec;
|
||||||
use crate::serial_core::frame::{FrameHandler, TactileAFrame, TestFrame};
|
use crate::serial_core::frame::{FrameHandler, TactileAFrame, TestFrame};
|
||||||
use crate::serial_core::model::{HudChartState, HudPacket};
|
use crate::serial_core::model::{HudChartState, HudPacket};
|
||||||
|
#[cfg(feature = "multi-dim")]
|
||||||
|
use crate::serial_core::multi_dim_force::PztProcessor;
|
||||||
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")]
|
#[cfg(feature = "devkit")]
|
||||||
use crate::devkit::{proto::SensorFrame, DevKitState};
|
use crate::devkit::{proto::SensorFrame, DevKitState};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use log::debug;
|
||||||
use std::future::pending;
|
use std::future::pending;
|
||||||
|
#[cfg(feature = "devkit")]
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
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};
|
||||||
@@ -17,14 +22,19 @@ 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;
|
const AUTO_SUB_INTERVAL: Duration = Duration::from_nanos(16_666_667);
|
||||||
|
|
||||||
pub enum PollMode<F> {
|
pub enum PollMode<F> {
|
||||||
Disable,
|
Disable,
|
||||||
Enabled(Box<dyn PollRequester<F>>),
|
Enabled(Box<dyn PollRequester<F>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct PendingSubFrame<F> {
|
||||||
|
frame: F,
|
||||||
|
values: Vec<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
pub trait SerialFrame: Clone + Send + 'static {
|
pub trait SerialFrame: Clone + Send + 'static {
|
||||||
fn dts_ms(&self) -> u64;
|
fn dts_ms(&self) -> u64;
|
||||||
|
|
||||||
@@ -215,10 +225,15 @@ where
|
|||||||
it.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
it.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||||
it
|
it
|
||||||
});
|
});
|
||||||
|
let mut poll_sub_interval = time::interval(AUTO_SUB_INTERVAL);
|
||||||
|
poll_sub_interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||||
|
|
||||||
let mut chart_state = HudChartState::new();
|
let mut chart_state = HudChartState::new();
|
||||||
let mut buffer = [0u8; 1024];
|
let mut buffer = [0u8; 1024];
|
||||||
let mut prune_interval = time::interval(Duration::from_millis(450));
|
let mut prune_interval = time::interval(Duration::from_millis(450));
|
||||||
|
#[cfg(feature = "multi-dim")]
|
||||||
|
let mut pzt_processor = PztProcessor::new();
|
||||||
|
let mut pending_sub_frame: Option<PendingSubFrame<F>> = None;
|
||||||
prune_interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
|
prune_interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -246,6 +261,21 @@ where
|
|||||||
app.emit("hud_stream", packet)?;
|
app.emit("hud_stream", packet)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ = poll_sub_interval.tick() => {
|
||||||
|
if let Some(pending) = pending_sub_frame.take() {
|
||||||
|
let display_values = build_display_values(
|
||||||
|
&mut chart_state,
|
||||||
|
pending.values.as_slice(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(packet) = pending
|
||||||
|
.frame
|
||||||
|
.to_hud_packet(&mut chart_state, display_values.as_deref())
|
||||||
|
{
|
||||||
|
app.emit("hud_stream", packet)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
read_result = port.read(&mut buffer) => {
|
read_result = port.read(&mut buffer) => {
|
||||||
let n = read_result?;
|
let n = read_result?;
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
@@ -266,25 +296,38 @@ where
|
|||||||
.await?
|
.await?
|
||||||
.map(|vals| vals.into_iter().map(Into::into).collect::<Vec<i32>>());
|
.map(|vals| vals.into_iter().map(Into::into).collect::<Vec<i32>>());
|
||||||
|
|
||||||
let mut record = recording.lock().map_err(|_| anyhow::anyhow!("recording state poisoned"))?;
|
let mut record = recording
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| anyhow::anyhow!("recording state poisoned"))?;
|
||||||
record.push(RecordedFrame {
|
record.push(RecordedFrame {
|
||||||
timing: FrameTiming { pts_ms: None, dts_ms: frame.dts_ms() },
|
timing: FrameTiming {
|
||||||
|
pts_ms: None,
|
||||||
|
dts_ms: frame.dts_ms(),
|
||||||
|
},
|
||||||
frame: frame.clone(),
|
frame: frame.clone(),
|
||||||
});
|
});
|
||||||
|
drop(record);
|
||||||
|
|
||||||
let display_values = if let Some(vals) = decode_res.as_ref() {
|
if let Some(vals) = decode_res {
|
||||||
|
#[cfg(feature = "multi-dim")]
|
||||||
|
{
|
||||||
|
let pzt_values = vals.iter().map(|value| *value as f32).collect::<Vec<f32>>();
|
||||||
|
if let Ok(angle) = pzt_processor.get_pzt_angle(&pzt_values) {
|
||||||
|
// debug!("pzt angle: {:.2}", angle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(feature = "devkit")]
|
||||||
|
{
|
||||||
let summary = vals.iter().copied().sum::<i32>();
|
let summary = vals.iter().copied().sum::<i32>();
|
||||||
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_pressure_matrix(vals.as_slice());
|
|
||||||
#[cfg(feature = "devkit")]
|
|
||||||
push_devkit_frame(&app, vals.as_slice(), frame.dts_ms(), force);
|
push_devkit_frame(&app, vals.as_slice(), frame.dts_ms(), force);
|
||||||
Some(vec![summary])
|
}
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(packet) = frame.to_hud_packet(&mut chart_state, display_values.as_deref()) {
|
pending_sub_frame = Some(PendingSubFrame {
|
||||||
|
frame: frame.clone(),
|
||||||
|
values: vals,
|
||||||
|
});
|
||||||
|
} else if let Some(packet) = frame.to_hud_packet(&mut chart_state, None) {
|
||||||
app.emit("hud_stream", packet)?;
|
app.emit("hud_stream", packet)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,6 +337,14 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_display_values(chart_state: &mut HudChartState, values: &[i32]) -> Option<Vec<i32>> {
|
||||||
|
let summary = values.iter().copied().sum::<i32>();
|
||||||
|
let force = raw_to_g1(summary as u32);
|
||||||
|
chart_state.record_summary(force as f32);
|
||||||
|
chart_state.record_pressure_matrix(values);
|
||||||
|
Some(vec![summary])
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "devkit")]
|
#[cfg(feature = "devkit")]
|
||||||
fn push_devkit_frame(app: &AppHandle, values: &[i32], dts_ms: u64, resultant_force: f64) {
|
fn push_devkit_frame(app: &AppHandle, values: &[i32], dts_ms: u64, resultant_force: f64) {
|
||||||
let devkit_state = app.state::<DevKitState>();
|
let devkit_state = app.state::<DevKitState>();
|
||||||
|
|||||||
@@ -37,7 +37,8 @@
|
|||||||
"nsis": {
|
"nsis": {
|
||||||
"installMode": "both",
|
"installMode": "both",
|
||||||
"displayLanguageSelector": false,
|
"displayLanguageSelector": false,
|
||||||
"installerIcon": "icons/icon.ico"
|
"installerIcon": "icons/icon.ico",
|
||||||
|
"template": "nsis/installer.nsi"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"resources": [
|
"resources": [
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
export let rangeLabel = "";
|
export let rangeLabel = "";
|
||||||
export let rangeMinLabel = "";
|
export let rangeMinLabel = "";
|
||||||
export let rangeMaxLabel = "";
|
export let rangeMaxLabel = "";
|
||||||
export let colorMapLabel = "";
|
|
||||||
export let resetConfigLabel = "";
|
export let resetConfigLabel = "";
|
||||||
export let applyLiveHint = "";
|
export let applyLiveHint = "";
|
||||||
export let matrixRows = 12;
|
export let matrixRows = 12;
|
||||||
@@ -42,7 +41,6 @@
|
|||||||
export let rangeMax = DEFAULT_PRESSURE_RANGE_MAX;
|
export let rangeMax = DEFAULT_PRESSURE_RANGE_MAX;
|
||||||
export let colorMapPreset: PressureColorMapPreset = "emerald";
|
export let colorMapPreset: PressureColorMapPreset = "emerald";
|
||||||
export let matrixDisplayMode: MatrixDisplayMode = "dots";
|
export let matrixDisplayMode: MatrixDisplayMode = "dots";
|
||||||
export let colorMapOptions: HudColorMapOption[] = [];
|
|
||||||
export let replaySectionLabel = "";
|
export let replaySectionLabel = "";
|
||||||
export let replayPlayLabel = "";
|
export let replayPlayLabel = "";
|
||||||
export let replayPauseLabel = "";
|
export let replayPauseLabel = "";
|
||||||
@@ -56,6 +54,7 @@
|
|||||||
export let replayFileName = "";
|
export let replayFileName = "";
|
||||||
export let replayFrameInfo = "";
|
export let replayFrameInfo = "";
|
||||||
export let showPrecisionTestPanel = false;
|
export let showPrecisionTestPanel = false;
|
||||||
|
export let sessionStartedAt: number = Date.now();
|
||||||
|
|
||||||
let stagePlaneEl: HTMLDivElement | undefined;
|
let stagePlaneEl: HTMLDivElement | undefined;
|
||||||
let panelZoneEl: HTMLDivElement | undefined;
|
let panelZoneEl: HTMLDivElement | undefined;
|
||||||
@@ -195,6 +194,7 @@
|
|||||||
{rangeMax}
|
{rangeMax}
|
||||||
{colorMapPreset}
|
{colorMapPreset}
|
||||||
{matrixDisplayMode}
|
{matrixDisplayMode}
|
||||||
|
{locale}
|
||||||
showStatsPanel={true}
|
showStatsPanel={true}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
@@ -225,6 +225,7 @@
|
|||||||
{rangeMax}
|
{rangeMax}
|
||||||
{colorMapPreset}
|
{colorMapPreset}
|
||||||
{matrixDisplayMode}
|
{matrixDisplayMode}
|
||||||
|
{locale}
|
||||||
showStatsPanel={true}
|
showStatsPanel={true}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
@@ -246,9 +247,6 @@
|
|||||||
{rangeLabel}
|
{rangeLabel}
|
||||||
{rangeMinLabel}
|
{rangeMinLabel}
|
||||||
{rangeMaxLabel}
|
{rangeMaxLabel}
|
||||||
{colorMapLabel}
|
|
||||||
bind:colorMapPreset
|
|
||||||
{colorMapOptions}
|
|
||||||
resetLabel={resetConfigLabel}
|
resetLabel={resetConfigLabel}
|
||||||
{applyLiveHint}
|
{applyLiveHint}
|
||||||
on:close={() => dispatch("configclose")}
|
on:close={() => dispatch("configclose")}
|
||||||
@@ -267,7 +265,7 @@
|
|||||||
in:fly={{ x: -180, duration: 340, opacity: 0.08, easing: cubicOut }}
|
in:fly={{ x: -180, duration: 340, opacity: 0.08, easing: cubicOut }}
|
||||||
out:fly={{ x: -180, duration: 280, opacity: 0.06, easing: cubicIn }}
|
out:fly={{ x: -180, duration: 280, opacity: 0.06, easing: cubicIn }}
|
||||||
>
|
>
|
||||||
<SignalChart {panel} panelIndex={index} />
|
<SignalChart {panel} panelIndex={index} {locale} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
@@ -281,6 +279,9 @@
|
|||||||
{summary}
|
{summary}
|
||||||
xValues={summary.xValues ?? null}
|
xValues={summary.xValues ?? null}
|
||||||
yValues={summary.points}
|
yValues={summary.points}
|
||||||
|
{locale}
|
||||||
|
{sessionStartedAt}
|
||||||
|
isRealtime={!replayHasData}
|
||||||
side="left"
|
side="left"
|
||||||
panelIndex={leftPanels.length}
|
panelIndex={leftPanels.length}
|
||||||
/>
|
/>
|
||||||
@@ -298,7 +299,7 @@
|
|||||||
in:fly={{ x: 180, duration: 340, opacity: 0.08, easing: cubicOut }}
|
in:fly={{ x: 180, duration: 340, opacity: 0.08, easing: cubicOut }}
|
||||||
out:fly={{ x: 180, duration: 280, opacity: 0.06, easing: cubicIn }}
|
out:fly={{ x: 180, duration: 280, opacity: 0.06, easing: cubicIn }}
|
||||||
>
|
>
|
||||||
<SignalChart {panel} panelIndex={index} />
|
<SignalChart {panel} panelIndex={index} {locale} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
@@ -312,6 +313,9 @@
|
|||||||
{summary}
|
{summary}
|
||||||
xValues={summary.xValues ?? null}
|
xValues={summary.xValues ?? null}
|
||||||
yValues={summary.points}
|
yValues={summary.points}
|
||||||
|
{locale}
|
||||||
|
{sessionStartedAt}
|
||||||
|
isRealtime={!replayHasData}
|
||||||
side="right"
|
side="right"
|
||||||
panelIndex={rightPanels.length}
|
panelIndex={rightPanels.length}
|
||||||
/>
|
/>
|
||||||
@@ -396,7 +400,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stage-canvas-plane {
|
.stage-canvas-plane {
|
||||||
--rail-width: clamp(17.5rem, 23vw, 21.5rem);
|
--rail-width: clamp(20rem, 27vw, 26rem);
|
||||||
--rail-edge-inset: clamp(0.35rem, 1vw, 0.9rem);
|
--rail-edge-inset: clamp(0.35rem, 1vw, 0.9rem);
|
||||||
--safe-gap: clamp(0.35rem, 0.9vw, 0.85rem);
|
--safe-gap: clamp(0.35rem, 0.9vw, 0.85rem);
|
||||||
--panel-zone-top: clamp(6.4rem, 11.8vh, 8rem);
|
--panel-zone-top: clamp(6.4rem, 11.8vh, 8rem);
|
||||||
@@ -754,7 +758,7 @@
|
|||||||
|
|
||||||
@media (max-width: 1180px) {
|
@media (max-width: 1180px) {
|
||||||
.stage-canvas-plane {
|
.stage-canvas-plane {
|
||||||
--rail-width: clamp(14.2rem, 28vw, 16.4rem);
|
--rail-width: clamp(17rem, 32vw, 22rem);
|
||||||
--rail-edge-inset: clamp(0.25rem, 0.7vw, 0.55rem);
|
--rail-edge-inset: clamp(0.25rem, 0.7vw, 0.55rem);
|
||||||
--safe-gap: clamp(0.2rem, 0.75vw, 0.45rem);
|
--safe-gap: clamp(0.2rem, 0.75vw, 0.45rem);
|
||||||
--panel-zone-top: clamp(6rem, 11.2vh, 7.2rem);
|
--panel-zone-top: clamp(6rem, 11.2vh, 7.2rem);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "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 type { HudColorMapOption, PressureColorMapPreset } from "$lib/types/hud";
|
|
||||||
|
|
||||||
export let title = "";
|
export let title = "";
|
||||||
export let hint = "";
|
export let hint = "";
|
||||||
@@ -11,15 +10,12 @@
|
|||||||
export let rangeLabel = "";
|
export let rangeLabel = "";
|
||||||
export let rangeMinLabel = "";
|
export let rangeMinLabel = "";
|
||||||
export let rangeMaxLabel = "";
|
export let rangeMaxLabel = "";
|
||||||
export let colorMapLabel = "";
|
|
||||||
export let resetLabel = "";
|
export let resetLabel = "";
|
||||||
export let applyLiveHint = "";
|
export let applyLiveHint = "";
|
||||||
export let matrixRows = 12;
|
export let matrixRows = 12;
|
||||||
export let matrixCols = 7;
|
export let matrixCols = 7;
|
||||||
export let rangeMin = DEFAULT_PRESSURE_RANGE_MIN;
|
export let rangeMin = DEFAULT_PRESSURE_RANGE_MIN;
|
||||||
export let rangeMax = DEFAULT_PRESSURE_RANGE_MAX;
|
export let rangeMax = DEFAULT_PRESSURE_RANGE_MAX;
|
||||||
export let colorMapPreset: PressureColorMapPreset = "emerald";
|
|
||||||
export let colorMapOptions: HudColorMapOption[] = [];
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
close: void;
|
close: void;
|
||||||
@@ -78,24 +74,17 @@
|
|||||||
matrixCols = size;
|
matrixCols = size;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyColorMapPreset(id: PressureColorMapPreset): void {
|
|
||||||
colorMapPreset = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetDefaults(): void {
|
function resetDefaults(): void {
|
||||||
matrixRows = 12;
|
matrixRows = 12;
|
||||||
matrixCols = 7;
|
matrixCols = 7;
|
||||||
rangeMin = DEFAULT_PRESSURE_RANGE_MIN;
|
rangeMin = DEFAULT_PRESSURE_RANGE_MIN;
|
||||||
rangeMax = DEFAULT_PRESSURE_RANGE_MAX;
|
rangeMax = DEFAULT_PRESSURE_RANGE_MAX;
|
||||||
colorMapPreset = "emerald";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit(): void {
|
function handleSubmit(): void {
|
||||||
dispatch("close");
|
dispatch("close");
|
||||||
}
|
}
|
||||||
|
|
||||||
$: selectedColorMap = colorMapOptions.find((option) => option.id === colorMapPreset) ?? colorMapOptions[0];
|
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
const nextRows = normalizeGridValue(matrixRows);
|
const nextRows = normalizeGridValue(matrixRows);
|
||||||
if (nextRows !== matrixRows) {
|
if (nextRows !== matrixRows) {
|
||||||
@@ -190,31 +179,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="config-section">
|
|
||||||
<div class="section-head">
|
|
||||||
<p class="section-title">{colorMapLabel}</p>
|
|
||||||
<p class="section-note">{selectedColorMap?.label ?? colorMapPreset}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="palette-row" role="group" aria-label={colorMapLabel}>
|
|
||||||
{#each colorMapOptions as option}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="palette-btn"
|
|
||||||
class:is-active={colorMapPreset === option.id}
|
|
||||||
on:click={() => applyColorMapPreset(option.id)}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="palette-preview"
|
|
||||||
style={`--palette-stop-0: ${option.previewStops[0]}; --palette-stop-1: ${option.previewStops[1]}; --palette-stop-2: ${option.previewStops[2]};`}
|
|
||||||
aria-hidden="true"
|
|
||||||
></span>
|
|
||||||
<span class="palette-name">{option.label}</span>
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="config-foot">
|
<footer class="config-foot">
|
||||||
<p class="live-note">{applyLiveHint}</p>
|
<p class="live-note">{applyLiveHint}</p>
|
||||||
<button type="button" class="reset-btn" on:click={resetDefaults}>{resetLabel}</button>
|
<button type="button" class="reset-btn" on:click={resetDefaults}>{resetLabel}</button>
|
||||||
@@ -327,15 +291,8 @@
|
|||||||
gap: 0.42rem;
|
gap: 0.42rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.palette-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 0.48rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preset-btn,
|
.preset-btn,
|
||||||
.reset-btn,
|
.reset-btn {
|
||||||
.palette-btn {
|
|
||||||
border: 1px solid rgb(var(--hud-border-rgb) / 0.28);
|
border: 1px solid rgb(var(--hud-border-rgb) / 0.28);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 0.38rem 0.72rem;
|
padding: 0.38rem 0.72rem;
|
||||||
@@ -358,48 +315,6 @@
|
|||||||
box-shadow: inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.14), 0 0 16px rgb(var(--hud-glow-alt-rgb) / 0.14);
|
box-shadow: inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.14), 0 0 16px rgb(var(--hud-glow-alt-rgb) / 0.14);
|
||||||
}
|
}
|
||||||
|
|
||||||
.palette-btn {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.34rem;
|
|
||||||
min-height: 4rem;
|
|
||||||
padding: 0.52rem 0.56rem 0.58rem;
|
|
||||||
border-radius: 0.74rem;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.palette-btn.is-active {
|
|
||||||
border-color: rgb(var(--hud-lime-rgb) / 0.48);
|
|
||||||
background: linear-gradient(180deg, rgb(var(--hud-surface-alt-rgb) / 0.96), rgb(var(--hud-surface-rgb) / 0.92));
|
|
||||||
color: rgb(var(--hud-text-main-rgb) / 0.98);
|
|
||||||
box-shadow: inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.14), 0 0 16px rgb(var(--hud-glow-alt-rgb) / 0.14);
|
|
||||||
}
|
|
||||||
|
|
||||||
.palette-preview {
|
|
||||||
display: block;
|
|
||||||
inline-size: 100%;
|
|
||||||
block-size: 0.74rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
border: 1px solid rgb(255 255 255 / 0.08);
|
|
||||||
background:
|
|
||||||
linear-gradient(
|
|
||||||
90deg,
|
|
||||||
var(--palette-stop-0) 0%,
|
|
||||||
var(--palette-stop-1) 52%,
|
|
||||||
var(--palette-stop-2) 100%
|
|
||||||
),
|
|
||||||
linear-gradient(180deg, rgb(255 255 255 / 0.08), transparent 55%);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 1px 0 rgb(255 255 255 / 0.1),
|
|
||||||
0 0 12px rgb(0 0 0 / 0.14);
|
|
||||||
}
|
|
||||||
|
|
||||||
.palette-name {
|
|
||||||
color: inherit;
|
|
||||||
font-size: 0.74rem;
|
|
||||||
line-height: 1.1;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-grid {
|
.field-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -457,9 +372,5 @@
|
|||||||
.field-grid {
|
.field-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.palette-row {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -2,8 +2,6 @@
|
|||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
export let running = false;
|
export let running = false;
|
||||||
export let port = 50051;
|
|
||||||
export let framesSent = 0;
|
|
||||||
export let filterLiftEnabled = true;
|
export let filterLiftEnabled = true;
|
||||||
export let saveAsXlsx = false;
|
export let saveAsXlsx = false;
|
||||||
export let locale: "zh-CN" | "en-US" = "zh-CN";
|
export let locale: "zh-CN" | "en-US" = "zh-CN";
|
||||||
@@ -22,37 +20,37 @@
|
|||||||
togglexlsx: void;
|
togglexlsx: void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
$: labels = locale === "zh-CN"
|
$: labels =
|
||||||
|
locale === "zh-CN"
|
||||||
? {
|
? {
|
||||||
title: "DevKit 配置",
|
title: "开发工具配置",
|
||||||
|
close: "关闭",
|
||||||
status: "状态",
|
status: "状态",
|
||||||
connected: "已连接",
|
connected: "已连接",
|
||||||
disconnected: "未连接",
|
disconnected: "未连接",
|
||||||
port: "端口",
|
filterLift: "导出后过滤抬起",
|
||||||
framesSent: "已发送帧",
|
filterLiftHint: "导出 CSV 时自动过滤掉抬起阶段的小值数据。",
|
||||||
filterLift: "导出过滤抬起",
|
saveXlsx: "保存为 xlsx",
|
||||||
filterLiftHint: "导出 CSV 后自动调用 Python 做梯度过滤,过滤掉抬起的小值数据",
|
saveXlsxHint: "将导出文件转换为 xlsx 格式。",
|
||||||
saveXlsx: "以 xlsx 保存",
|
|
||||||
saveXlsxHint: "Python 处理后输出 xlsx 格式并删除源 CSV 文件",
|
|
||||||
lastResult: "最近一次处理",
|
lastResult: "最近一次处理",
|
||||||
output: "输出文件",
|
output: "输出文件",
|
||||||
groups: "分组数",
|
groups: "分组数",
|
||||||
mean: "均值",
|
mean: "均值",
|
||||||
threshold: "阈值",
|
threshold: "阈值",
|
||||||
rows: "行数",
|
rows: "总行数",
|
||||||
kept: "保留行数",
|
kept: "保留行数",
|
||||||
|
rowsFlow: "行数变化"
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
title: "DevKit Config",
|
title: "DevKit Config",
|
||||||
|
close: "Close",
|
||||||
status: "Status",
|
status: "Status",
|
||||||
connected: "Connected",
|
connected: "Connected",
|
||||||
disconnected: "Disconnected",
|
disconnected: "Disconnected",
|
||||||
port: "Port",
|
|
||||||
framesSent: "Frames sent",
|
|
||||||
filterLift: "Filter lift on export",
|
filterLift: "Filter lift on export",
|
||||||
filterLiftHint: "After CSV export, automatically call Python to filter out small values",
|
filterLiftHint: "Automatically filter out small values from lift-off phases during CSV export.",
|
||||||
saveXlsx: "Save as xlsx",
|
saveXlsx: "Save as xlsx",
|
||||||
saveXlsxHint: "Python outputs xlsx format and deletes the source CSV file",
|
saveXlsxHint: "Convert exported file to xlsx format.",
|
||||||
lastResult: "Last process",
|
lastResult: "Last process",
|
||||||
output: "Output",
|
output: "Output",
|
||||||
groups: "Groups",
|
groups: "Groups",
|
||||||
@@ -60,13 +58,14 @@
|
|||||||
threshold: "Threshold",
|
threshold: "Threshold",
|
||||||
rows: "Rows",
|
rows: "Rows",
|
||||||
kept: "Kept rows",
|
kept: "Kept rows",
|
||||||
|
rowsFlow: "Rows flow"
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="dk-panel">
|
<div class="dk-panel">
|
||||||
<header class="dk-head">
|
<header class="dk-head">
|
||||||
<h3 class="dk-title">{labels.title}</h3>
|
<h3 class="dk-title">{labels.title}</h3>
|
||||||
<button type="button" class="dk-close" on:click={() => dispatch("close")} aria-label="Close">
|
<button type="button" class="dk-close" on:click={() => dispatch("close")} aria-label={labels.close}>
|
||||||
<span></span><span></span>
|
<span></span><span></span>
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
@@ -77,18 +76,6 @@
|
|||||||
<span class="dk-label">{labels.status}</span>
|
<span class="dk-label">{labels.status}</span>
|
||||||
<span class="dk-value">{running ? labels.connected : labels.disconnected}</span>
|
<span class="dk-value">{running ? labels.connected : labels.disconnected}</span>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<section class="dk-section">
|
<section class="dk-section">
|
||||||
@@ -132,8 +119,8 @@
|
|||||||
<span class="dk-result-value">{lastProcessResult.threshold.toFixed(3)}</span>
|
<span class="dk-result-value">{lastProcessResult.threshold.toFixed(3)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="dk-result-item">
|
<div class="dk-result-item">
|
||||||
<span class="dk-result-label">{labels.rows}</span>
|
<span class="dk-result-label">{labels.rowsFlow}</span>
|
||||||
<span class="dk-result-value">{lastProcessResult.rowsTotal} → {lastProcessResult.rowsKept}</span>
|
<span class="dk-result-value">{lastProcessResult.rowsTotal} -> {lastProcessResult.rowsKept}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -236,30 +223,6 @@
|
|||||||
margin-inline-start: auto;
|
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 {
|
.dk-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
export let matrixDisplayMode: MatrixDisplayMode = "dots";
|
export let matrixDisplayMode: MatrixDisplayMode = "dots";
|
||||||
export let summary: HudSummary | null = null;
|
export let summary: HudSummary | null = null;
|
||||||
export let showStatsPanel = true;
|
export let showStatsPanel = true;
|
||||||
|
export let locale: "zh-CN" | "en-US" = "zh-CN";
|
||||||
|
|
||||||
let viewerEl: HTMLDivElement | undefined;
|
let viewerEl: HTMLDivElement | undefined;
|
||||||
let canvasEl: HTMLCanvasElement | undefined;
|
let canvasEl: HTMLCanvasElement | undefined;
|
||||||
@@ -131,8 +132,13 @@
|
|||||||
$: resolvedRangeMin = resolvedRange.min;
|
$: resolvedRangeMin = resolvedRange.min;
|
||||||
$: resolvedRangeMax = resolvedRange.max;
|
$: resolvedRangeMax = resolvedRange.max;
|
||||||
$: matrixLayout = buildMatrixLayout(resolvedMatrixRows, resolvedMatrixCols);
|
$: matrixLayout = buildMatrixLayout(resolvedMatrixRows, resolvedMatrixCols);
|
||||||
$: statsModeLabel = matrixDisplayMode === "dots" ? "dot pulse" : "numeric pulse";
|
$: statsModeLabel = matrixDisplayMode === "dots"
|
||||||
$: statsNote = `${resolvedMatrixRows}x${resolvedMatrixCols} / force range ${resolvedRangeMin}-${resolvedRangeMax} / ${statsModeLabel}`;
|
? (locale === "zh-CN" ? "点阵脉冲" : "dot pulse")
|
||||||
|
: (locale === "zh-CN" ? "数字脉冲" : "numeric pulse");
|
||||||
|
$: statsNote = `${resolvedMatrixRows}x${resolvedMatrixCols} / ${locale === "zh-CN" ? "力量范围" : "force range"} ${resolvedRangeMin}-${resolvedRangeMax} / ${statsModeLabel}`;
|
||||||
|
$: viewerI18n = locale === "zh-CN"
|
||||||
|
? { title: "合力", current: "当前合力", max: "最大合力", min: "最小合力" }
|
||||||
|
: { title: "Resultant Force", current: "Current RF", max: "Max RF", min: "Min RF" };
|
||||||
|
|
||||||
function formatForceStat(value: number | null): string {
|
function formatForceStat(value: number | null): string {
|
||||||
if (value == null || !Number.isFinite(value)) {
|
if (value == null || !Number.isFinite(value)) {
|
||||||
@@ -660,18 +666,18 @@
|
|||||||
{#if showStatsPanel}
|
{#if showStatsPanel}
|
||||||
<div class="viewer-controls">
|
<div class="viewer-controls">
|
||||||
<section class="stats-panel" aria-label="Pressure Summary">
|
<section class="stats-panel" aria-label="Pressure Summary">
|
||||||
<p class="stats-label">Resultant Force</p>
|
<p class="stats-label">{viewerI18n.title}</p>
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
<article class="stats-card stats-card-wide">
|
<article class="stats-card stats-card-wide">
|
||||||
<span class="stats-key">Current RF</span>
|
<span class="stats-key">{viewerI18n.current}</span>
|
||||||
<strong class="stats-value">{formatForceStat(stats.current)}</strong>
|
<strong class="stats-value">{formatForceStat(stats.current)}</strong>
|
||||||
</article>
|
</article>
|
||||||
<article class="stats-card">
|
<article class="stats-card">
|
||||||
<span class="stats-key">Max RF</span>
|
<span class="stats-key">{viewerI18n.max}</span>
|
||||||
<strong class="stats-value">{formatForceStat(stats.max)}</strong>
|
<strong class="stats-value">{formatForceStat(stats.max)}</strong>
|
||||||
</article>
|
</article>
|
||||||
<article class="stats-card">
|
<article class="stats-card">
|
||||||
<span class="stats-key">Min RF</span>
|
<span class="stats-key">{viewerI18n.min}</span>
|
||||||
<strong class="stats-value">{formatForceStat(stats.min)}</strong>
|
<strong class="stats-value">{formatForceStat(stats.min)}</strong>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
|
|
||||||
export let panel: HudSignalPanel;
|
export let panel: HudSignalPanel;
|
||||||
export let panelIndex = 0;
|
export let panelIndex = 0;
|
||||||
|
export let locale: "zh-CN" | "en-US" = "zh-CN";
|
||||||
|
|
||||||
|
$: signalI18n = locale === "zh-CN"
|
||||||
|
? { now: "当前", max: "最大", min: "最小", total: "合计" }
|
||||||
|
: { now: "Now", max: "Max", min: "Min", total: "TOTAL" };
|
||||||
|
|
||||||
const viewportWidth = 100;
|
const viewportWidth = 100;
|
||||||
const viewportHeight = 36;
|
const viewportHeight = 36;
|
||||||
@@ -110,7 +115,7 @@
|
|||||||
|
|
||||||
<div class="icon-layer" aria-hidden="true">
|
<div class="icon-layer" aria-hidden="true">
|
||||||
{#each panel.icons as icon (icon.id)}
|
{#each panel.icons as icon (icon.id)}
|
||||||
<span class="icon-chip tone-{icon.tone}">{icon.label}</span>
|
<span class="icon-chip tone-{icon.tone}">{icon.label === "TOTAL" ? signalI18n.total : icon.label}</span>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -136,17 +141,17 @@
|
|||||||
<footer class="panel-foot">
|
<footer class="panel-foot">
|
||||||
<p class="foot-item">
|
<p class="foot-item">
|
||||||
<span class="dot tone-cyan"></span>
|
<span class="dot tone-cyan"></span>
|
||||||
<span class="metric-label">Now</span>
|
<span class="metric-label">{signalI18n.now}</span>
|
||||||
<span class="value">{latestValue}</span>
|
<span class="value">{latestValue}</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="foot-item">
|
<p class="foot-item">
|
||||||
<span class="dot tone-lime"></span>
|
<span class="dot tone-lime"></span>
|
||||||
<span class="metric-label">Max</span>
|
<span class="metric-label">{signalI18n.max}</span>
|
||||||
<span class="value">{maxValue}</span>
|
<span class="value">{maxValue}</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="foot-item">
|
<p class="foot-item">
|
||||||
<span class="dot tone-orange"></span>
|
<span class="dot tone-orange"></span>
|
||||||
<span class="metric-label">Min</span>
|
<span class="metric-label">{signalI18n.min}</span>
|
||||||
<span class="value">{minValue}</span>
|
<span class="value">{minValue}</span>
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -158,7 +163,7 @@
|
|||||||
--enter-ms: 1800ms;
|
--enter-ms: 1800ms;
|
||||||
--fade-ms: 1000ms;
|
--fade-ms: 1000ms;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
inline-size: min(100%, clamp(16.8rem, 23vw, 22rem));
|
inline-size: min(100%, clamp(19rem, 27vw, 26rem));
|
||||||
aspect-ratio: 1.44 / 1;
|
aspect-ratio: 1.44 / 1;
|
||||||
min-block-size: 11.8rem;
|
min-block-size: 11.8rem;
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
@@ -388,7 +393,7 @@
|
|||||||
|
|
||||||
@media (max-width: 1180px) {
|
@media (max-width: 1180px) {
|
||||||
.signal-panel {
|
.signal-panel {
|
||||||
inline-size: min(100%, clamp(14rem, 30vw, 17rem));
|
inline-size: min(100%, clamp(16rem, 32vw, 21rem));
|
||||||
aspect-ratio: 1.5 / 1;
|
aspect-ratio: 1.5 / 1;
|
||||||
min-block-size: 10.1rem;
|
min-block-size: 10.1rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
import type { HudSummary } from "$lib/types/hud";
|
import type { HudSummary } from "$lib/types/hud";
|
||||||
|
|
||||||
export let summary: HudSummary;
|
export let summary: HudSummary;
|
||||||
@@ -6,6 +7,25 @@
|
|||||||
export let panelIndex = 0;
|
export let panelIndex = 0;
|
||||||
export let xValues: number[] | null = null;
|
export let xValues: number[] | null = null;
|
||||||
export let yValues: number[] | null = null;
|
export let yValues: number[] | null = null;
|
||||||
|
export let locale: "zh-CN" | "en-US" = "zh-CN";
|
||||||
|
export let sessionStartedAt: number = Date.now();
|
||||||
|
export let isRealtime = false;
|
||||||
|
|
||||||
|
let currentTimeSeconds = 0;
|
||||||
|
let timerId: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
timerId = setInterval(() => {
|
||||||
|
currentTimeSeconds = Math.round((Date.now() - sessionStartedAt) / 100) / 10;
|
||||||
|
}, 200);
|
||||||
|
return () => {
|
||||||
|
if (timerId != null) clearInterval(timerId);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
$: i18n = locale === "zh-CN"
|
||||||
|
? { now: "当前", min: "最小", max: "最大", waiting: "等待数据" }
|
||||||
|
: { now: "Now", min: "Min", max: "Max", waiting: "Waiting" };
|
||||||
|
|
||||||
const viewportWidth = 120;
|
const viewportWidth = 120;
|
||||||
const viewportHeight = 48;
|
const viewportHeight = 48;
|
||||||
@@ -50,7 +70,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (axis === "x") {
|
if (axis === "x") {
|
||||||
return String(Math.round(value));
|
if (value < 60) {
|
||||||
|
return `${value.toFixed(1)}s`;
|
||||||
|
}
|
||||||
|
const mins = Math.floor(value / 60);
|
||||||
|
const secs = value - mins * 60;
|
||||||
|
return `${mins}:${secs.toFixed(0).padStart(2, "0")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${Math.round(value)} N`;
|
return `${Math.round(value)} N`;
|
||||||
@@ -104,14 +129,51 @@
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let previousX = 0;
|
||||||
|
|
||||||
return rawYValues.map((rawY, index) => {
|
return rawYValues.map((rawY, index) => {
|
||||||
const x = rawXValues[index];
|
const x = rawXValues[index];
|
||||||
const y = Number.isFinite(rawY) ? Number(rawY) : 0;
|
const y = Number.isFinite(rawY) ? Number(rawY) : 0;
|
||||||
const resolvedX = Number.isFinite(x) ? Number(x) : index + 1;
|
const fallbackX = index === 0 ? 0 : previousX + 1;
|
||||||
return { x: resolvedX, y };
|
const resolvedX = Number.isFinite(x) ? Number(x) : fallbackX;
|
||||||
|
const normalizedX = index === 0 ? resolvedX : Math.max(resolvedX, previousX);
|
||||||
|
previousX = normalizedX;
|
||||||
|
return { x: normalizedX, y };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveXScaleBounds(
|
||||||
|
samples: CurveSample[],
|
||||||
|
currentSeconds: number,
|
||||||
|
realtime: boolean
|
||||||
|
): { min: number; max: number } {
|
||||||
|
if (samples.length === 0) {
|
||||||
|
return { min: 0, max: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = samples.map((sample) => sample.x);
|
||||||
|
const dataBounds = resolveBounds(values);
|
||||||
|
|
||||||
|
if (!realtime) {
|
||||||
|
return dataBounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstX = samples[0].x;
|
||||||
|
const lastX = samples[samples.length - 1].x;
|
||||||
|
const axisMax = Math.max(lastX, currentSeconds);
|
||||||
|
const positiveDiffs = samples
|
||||||
|
.slice(1)
|
||||||
|
.map((sample, index) => sample.x - samples[index].x)
|
||||||
|
.filter((diff) => diff > 0);
|
||||||
|
const averageSpacing =
|
||||||
|
positiveDiffs.length > 0 ? positiveDiffs.reduce((sum, diff) => sum + diff, 0) / positiveDiffs.length : 1;
|
||||||
|
const dataSpan = Math.max(lastX - firstX, 0);
|
||||||
|
const windowSpan = Math.max(dataSpan, averageSpacing * Math.max(samples.length - 1, 1), 1);
|
||||||
|
const axisMin = Math.max(0, axisMax - windowSpan);
|
||||||
|
|
||||||
|
return resolveBounds([axisMin, axisMax]);
|
||||||
|
}
|
||||||
|
|
||||||
function convertPoints(
|
function convertPoints(
|
||||||
samples: CurveSample[],
|
samples: CurveSample[],
|
||||||
xBounds: { min: number; max: number },
|
xBounds: { min: number; max: number },
|
||||||
@@ -146,14 +208,14 @@
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildXAxisTicks(samples: CurveSample[], xScaleBounds: { min: number; max: number }): AxisTick[] {
|
function buildXAxisTicks(xScaleBounds: { min: number; max: number }): AxisTick[] {
|
||||||
if (!samples.length) {
|
if (!Number.isFinite(xScaleBounds.min) || !Number.isFinite(xScaleBounds.max)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const first = samples[0].x;
|
const first = xScaleBounds.min;
|
||||||
const middle = samples[Math.floor((samples.length - 1) / 2)].x;
|
const middle = xScaleBounds.min + (xScaleBounds.max - xScaleBounds.min) / 2;
|
||||||
const last = samples[samples.length - 1].x;
|
const last = xScaleBounds.max;
|
||||||
const tickValues = [first, middle, last];
|
const tickValues = [first, middle, last];
|
||||||
return tickValues.map((value) => ({
|
return tickValues.map((value) => ({
|
||||||
value,
|
value,
|
||||||
@@ -185,9 +247,16 @@
|
|||||||
|
|
||||||
$: sourceYValues = yValues && yValues.length ? yValues : summary.points;
|
$: sourceYValues = yValues && yValues.length ? yValues : summary.points;
|
||||||
$: sourceXValues = xValues && xValues.length ? xValues : summary.xValues ?? [];
|
$: sourceXValues = xValues && xValues.length ? xValues : summary.xValues ?? [];
|
||||||
$: samples = buildSamples(sourceYValues, sourceXValues);
|
$: samples = (() => {
|
||||||
|
const base = buildSamples(sourceYValues, sourceXValues);
|
||||||
|
if (isRealtime && base.length > 0 && currentTimeSeconds > 0) {
|
||||||
|
const lastSample = base[base.length - 1];
|
||||||
|
base[base.length - 1] = { ...lastSample, x: Math.max(lastSample.x, currentTimeSeconds) };
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
})();
|
||||||
$: sampleCount = samples.length;
|
$: sampleCount = samples.length;
|
||||||
$: xScaleBounds = resolveBounds(samples.map((sample) => sample.x));
|
$: xScaleBounds = resolveXScaleBounds(samples, currentTimeSeconds, isRealtime);
|
||||||
$: yScaleBounds = fixedYBounds;
|
$: yScaleBounds = fixedYBounds;
|
||||||
$: xDataBounds = resolveDataBounds(samples.map((sample) => sample.x));
|
$: xDataBounds = resolveDataBounds(samples.map((sample) => sample.x));
|
||||||
$: yDataBounds = resolveDataBounds(samples.map((sample) => sample.y));
|
$: yDataBounds = resolveDataBounds(samples.map((sample) => sample.y));
|
||||||
@@ -196,7 +265,7 @@
|
|||||||
$: areaPath = createAreaPath(plotPoints);
|
$: areaPath = createAreaPath(plotPoints);
|
||||||
$: lastPoint = plotPoints.length > 0 ? plotPoints[plotPoints.length - 1] : null;
|
$: lastPoint = plotPoints.length > 0 ? plotPoints[plotPoints.length - 1] : null;
|
||||||
$: yAxisTicks = sampleCount > 0 ? buildYAxisTicks(yScaleBounds, yDataBounds) : [];
|
$: yAxisTicks = sampleCount > 0 ? buildYAxisTicks(yScaleBounds, yDataBounds) : [];
|
||||||
$: xAxisTicks = sampleCount > 0 ? buildXAxisTicks(samples, xScaleBounds) : [];
|
$: xAxisTicks = sampleCount > 0 ? buildXAxisTicks(xScaleBounds) : [];
|
||||||
$: latestValue = formatValue(summary.latest);
|
$: latestValue = formatValue(summary.latest);
|
||||||
$: minValue = formatValue(summary.min);
|
$: minValue = formatValue(summary.min);
|
||||||
$: maxValue = formatValue(summary.max);
|
$: maxValue = formatValue(summary.max);
|
||||||
@@ -270,7 +339,7 @@
|
|||||||
|
|
||||||
{#if sampleCount === 0}
|
{#if sampleCount === 0}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<span>Waiting</span>
|
<span>{i18n.waiting}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -278,17 +347,17 @@
|
|||||||
<footer class="panel-foot">
|
<footer class="panel-foot">
|
||||||
<p class="foot-item">
|
<p class="foot-item">
|
||||||
<span class="dot tone-cyan"></span>
|
<span class="dot tone-cyan"></span>
|
||||||
<span class="metric-text">Now</span>
|
<span class="metric-text">{i18n.now}</span>
|
||||||
<span class="value">{latestValue}</span>
|
<span class="value">{latestValue}</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="foot-item">
|
<p class="foot-item">
|
||||||
<span class="dot tone-lime"></span>
|
<span class="dot tone-lime"></span>
|
||||||
<span class="metric-text">Min</span>
|
<span class="metric-text">{i18n.min}</span>
|
||||||
<span class="value">{minValue}</span>
|
<span class="value">{minValue}</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="foot-item">
|
<p class="foot-item">
|
||||||
<span class="dot tone-orange"></span>
|
<span class="dot tone-orange"></span>
|
||||||
<span class="metric-text">Max</span>
|
<span class="metric-text">{i18n.max}</span>
|
||||||
<span class="value">{maxValue}</span>
|
<span class="value">{maxValue}</span>
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -300,12 +369,10 @@
|
|||||||
--enter-ms: 1800ms;
|
--enter-ms: 1800ms;
|
||||||
--fade-ms: 1000ms;
|
--fade-ms: 1000ms;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
inline-size: min(100%, clamp(29rem, 38vw, 37rem));
|
inline-size: min(100%, clamp(34rem, 44vw, 44rem));
|
||||||
aspect-ratio: 1.42 / 1;
|
|
||||||
min-block-size: 20.5rem;
|
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto auto auto;
|
grid-template-rows: auto 1fr auto;
|
||||||
gap: 0.68rem;
|
gap: 0.68rem;
|
||||||
padding: 0.88rem 0.96rem 1rem;
|
padding: 0.88rem 0.96rem 1rem;
|
||||||
border: 1px solid rgb(var(--hud-border-strong-rgb) / 0.42);
|
border: 1px solid rgb(var(--hud-border-strong-rgb) / 0.42);
|
||||||
@@ -404,6 +471,7 @@
|
|||||||
.chart-stage {
|
.chart-stage {
|
||||||
position: relative;
|
position: relative;
|
||||||
block-size: clamp(12rem, 15.5vw, 15rem);
|
block-size: clamp(12rem, 15.5vw, 15rem);
|
||||||
|
min-block-size: 5rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid rgb(var(--hud-border-strong-rgb) / 0.32);
|
border: 1px solid rgb(var(--hud-border-strong-rgb) / 0.32);
|
||||||
border-radius: 0.62rem;
|
border-radius: 0.62rem;
|
||||||
@@ -474,25 +542,29 @@
|
|||||||
|
|
||||||
.panel-foot {
|
.panel-foot {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
gap: 0.8rem;
|
gap: 0.8rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.foot-item {
|
.foot-item {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.28rem;
|
gap: 0.22rem;
|
||||||
color: rgb(var(--hud-text-main-rgb) / 0.9);
|
color: rgb(var(--hud-text-main-rgb) / 0.9);
|
||||||
font-size: 0.76rem;
|
font-size: 0.68rem;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.03em;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-text {
|
.metric-text {
|
||||||
color: rgb(var(--hud-text-dim-rgb) / 0.82);
|
color: rgb(var(--hud-text-dim-rgb) / 0.82);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dot {
|
.dot {
|
||||||
@@ -519,16 +591,18 @@
|
|||||||
|
|
||||||
@media (max-width: 1180px) {
|
@media (max-width: 1180px) {
|
||||||
.signal-panel {
|
.signal-panel {
|
||||||
inline-size: min(100%, clamp(24rem, 36vw, 31rem));
|
inline-size: min(100%, clamp(28rem, 40vw, 38rem));
|
||||||
aspect-ratio: 1.48 / 1;
|
}
|
||||||
min-block-size: 17rem;
|
|
||||||
|
.chart-stage {
|
||||||
|
block-size: clamp(10rem, 13vw, 12rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-height: 900px) {
|
@media (max-height: 900px) {
|
||||||
.signal-panel {
|
.signal-panel {
|
||||||
inline-size: min(100%, clamp(24rem, 33vw, 30rem));
|
inline-size: min(100%, clamp(28rem, 38vw, 36rem));
|
||||||
min-block-size: 16.8rem;
|
padding: 0.7rem 0.76rem 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-stage {
|
.chart-stage {
|
||||||
@@ -538,46 +612,31 @@
|
|||||||
|
|
||||||
@media (max-height: 760px) {
|
@media (max-height: 760px) {
|
||||||
.signal-panel {
|
.signal-panel {
|
||||||
inline-size: min(100%, clamp(21rem, 29vw, 26rem));
|
inline-size: min(100%, clamp(24rem, 34vw, 30rem));
|
||||||
min-block-size: 14.4rem;
|
padding: 0.62rem 0.68rem 0.72rem;
|
||||||
padding: 0.7rem 0.76rem 0.8rem;
|
gap: 0.48rem;
|
||||||
}
|
|
||||||
|
|
||||||
.panel-foot {
|
|
||||||
margin-block-start: 0.28rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-stage {
|
.chart-stage {
|
||||||
block-size: clamp(8.3rem, 9.6vw, 9.8rem);
|
block-size: clamp(8rem, 9.5vw, 9.8rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-height: 680px) {
|
@media (max-height: 680px) {
|
||||||
.signal-panel {
|
.signal-panel {
|
||||||
inline-size: min(100%, clamp(18.5rem, 24vw, 22.5rem));
|
inline-size: min(100%, clamp(20rem, 28vw, 26rem));
|
||||||
min-block-size: 12.4rem;
|
padding: 0.52rem 0.58rem 0.6rem;
|
||||||
padding: 0.62rem 0.66rem 0.68rem;
|
gap: 0.36rem;
|
||||||
}
|
|
||||||
|
|
||||||
.panel-head {
|
|
||||||
margin-block-end: 0.26rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-foot {
|
|
||||||
margin-block-start: 0.18rem;
|
|
||||||
gap: 0.56rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-stage {
|
.chart-stage {
|
||||||
block-size: clamp(7rem, 7.8vw, 8rem);
|
block-size: clamp(6.5rem, 8vw, 7.5rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.signal-panel {
|
.signal-panel {
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
aspect-ratio: 1.7 / 1;
|
|
||||||
min-block-size: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -260,9 +260,17 @@
|
|||||||
rowsKept: number;
|
rowsKept: number;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
let devkitStatusTimer: number | null = null;
|
let devkitStatusTimer: number | null = null;
|
||||||
|
let sessionStartedAt: number = Date.now();
|
||||||
|
|
||||||
$: uiCopy = copyByLocale[locale];
|
$: uiCopy = copyByLocale[locale];
|
||||||
$: configLinks = buildConfigLinks(locale, activeConfigLinkId, isConfigPanelOpen, isPrecisionTestOpen);
|
$: configLinks = buildConfigLinks(
|
||||||
|
locale,
|
||||||
|
activeConfigLinkId,
|
||||||
|
isConfigPanelOpen,
|
||||||
|
isPrecisionTestOpen,
|
||||||
|
devkitEnabled,
|
||||||
|
isDevKitConfigOpen
|
||||||
|
);
|
||||||
$: leftSignalPanels = signalPanels.filter((panel) => panel.side === "left");
|
$: leftSignalPanels = signalPanels.filter((panel) => panel.side === "left");
|
||||||
$: rightSignalPanels = signalPanels.filter((panel) => panel.side === "right");
|
$: rightSignalPanels = signalPanels.filter((panel) => panel.side === "right");
|
||||||
$: rangeTicks = buildRangeTicks(rangeMin, rangeMax);
|
$: rangeTicks = buildRangeTicks(rangeMin, rangeMax);
|
||||||
@@ -718,12 +726,12 @@
|
|||||||
const safeIndex = clamp(index, 0, replayFrames.length - 1);
|
const safeIndex = clamp(index, 0, replayFrames.length - 1);
|
||||||
const startIndex = Math.max(0, safeIndex - summaryPointsPerSeries + 1);
|
const startIndex = Math.max(0, safeIndex - summaryPointsPerSeries + 1);
|
||||||
const points: number[] = [];
|
const points: number[] = [];
|
||||||
const frameIds: number[] = [];
|
const xSeconds: number[] = [];
|
||||||
for (let cursor = startIndex; cursor <= safeIndex; cursor += 1) {
|
for (let cursor = startIndex; cursor <= safeIndex; cursor += 1) {
|
||||||
points.push(replayFrameTotal(replayFrames[cursor]));
|
points.push(replayFrameTotal(replayFrames[cursor]));
|
||||||
frameIds.push(cursor + 1);
|
xSeconds.push(replayFrames[cursor].dtsMs / 1000);
|
||||||
}
|
}
|
||||||
return buildSummary(points, frameIds);
|
return buildSummary(points, xSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyReplayFrame(index: number): void {
|
function applyReplayFrame(index: number): void {
|
||||||
@@ -952,10 +960,11 @@
|
|||||||
? summaryValue.points[summaryValue.points.length - 1]
|
? summaryValue.points[summaryValue.points.length - 1]
|
||||||
: randomBetween(280, 1600);
|
: randomBetween(280, 1600);
|
||||||
const next = Math.round(clamp(previous + randomBetween(-160, 160), 120, 2400) * 10) / 10;
|
const next = Math.round(clamp(previous + randomBetween(-160, 160), 120, 2400) * 10) / 10;
|
||||||
|
const nowSeconds = Math.round((Date.now() - sessionStartedAt) / 100) / 10;
|
||||||
const previousXValues =
|
const previousXValues =
|
||||||
summaryValue.xValues && summaryValue.xValues.length === summaryValue.points.length
|
summaryValue.xValues && summaryValue.xValues.length === summaryValue.points.length
|
||||||
? summaryValue.xValues
|
? summaryValue.xValues
|
||||||
: summaryValue.points.map((_, index) => index + 1);
|
: summaryValue.points.map((_, index) => nowSeconds);
|
||||||
const points =
|
const points =
|
||||||
summaryValue.points.length >= summaryPointsPerSeries
|
summaryValue.points.length >= summaryPointsPerSeries
|
||||||
? summaryValue.points.slice(1)
|
? summaryValue.points.slice(1)
|
||||||
@@ -964,7 +973,7 @@
|
|||||||
previousXValues.length >= summaryPointsPerSeries ? previousXValues.slice(1) : previousXValues.slice();
|
previousXValues.length >= summaryPointsPerSeries ? previousXValues.slice(1) : previousXValues.slice();
|
||||||
|
|
||||||
points.push(next);
|
points.push(next);
|
||||||
xValues.push((xValues[xValues.length - 1] ?? 0) + 1);
|
xValues.push(nowSeconds);
|
||||||
return buildSummary(points, xValues);
|
return buildSummary(points, xValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -977,7 +986,17 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
signalPanels = showSignalPanels ? packet.panels : buildInactivePanels();
|
signalPanels = showSignalPanels ? packet.panels : buildInactivePanels();
|
||||||
|
if (packet.summary.points.length > 0) {
|
||||||
|
const nowSeconds = Math.round((Date.now() - sessionStartedAt) / 100) / 10;
|
||||||
|
const pointCount = packet.summary.points.length;
|
||||||
|
const spacing =
|
||||||
|
pointCount > 1 ? Math.min(1.2, nowSeconds / Math.max(pointCount - 1, 1)) : 0;
|
||||||
|
const startX = Math.max(0, nowSeconds - spacing * Math.max(pointCount - 1, 0));
|
||||||
|
const xValues = packet.summary.points.map((_, index) => Math.round((startX + index * spacing) * 10) / 10);
|
||||||
|
summary = { ...packet.summary, xValues };
|
||||||
|
} else {
|
||||||
summary = packet.summary;
|
summary = packet.summary;
|
||||||
|
}
|
||||||
pressureMatrix = packet.pressureMatrix;
|
pressureMatrix = packet.pressureMatrix;
|
||||||
hasSignalData = signalPanels.length > 0 || packet.summary.points.length > 0;
|
hasSignalData = signalPanels.length > 0 || packet.summary.points.length > 0;
|
||||||
}
|
}
|
||||||
@@ -1015,24 +1034,25 @@
|
|||||||
currentLocale: LocaleCode,
|
currentLocale: LocaleCode,
|
||||||
activeId: string,
|
activeId: string,
|
||||||
isSettingsOpen: boolean,
|
isSettingsOpen: boolean,
|
||||||
isPrecisionOpen: boolean
|
isPrecisionOpen: boolean,
|
||||||
|
isDevKitEnabled: boolean,
|
||||||
|
isDevKitOpen: boolean
|
||||||
): HudConfigLink[] {
|
): HudConfigLink[] {
|
||||||
const labels =
|
const labels =
|
||||||
currentLocale === "zh-CN"
|
currentLocale === "zh-CN"
|
||||||
? {
|
? {
|
||||||
streamOn: "打开",
|
streamOn: "打开",
|
||||||
streamOff: "关闭",
|
streamOff: "关闭",
|
||||||
calibrate: "校准",
|
|
||||||
precisionTest: "游戏",
|
precisionTest: "游戏",
|
||||||
settings: "参数"
|
settings: "参数"
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
streamOn: "Open",
|
streamOn: "Open",
|
||||||
streamOff: "Close",
|
streamOff: "Close",
|
||||||
calibrate: "Calib",
|
|
||||||
precisionTest: "Game",
|
precisionTest: "Game",
|
||||||
settings: "Setup"
|
settings: "Setup"
|
||||||
};
|
};
|
||||||
|
const devkitLabel = currentLocale === "zh-CN" ? "开发工具" : "DevKit";
|
||||||
|
|
||||||
const links: HudConfigLink[] = [
|
const links: HudConfigLink[] = [
|
||||||
{
|
{
|
||||||
@@ -1047,12 +1067,6 @@
|
|||||||
tone: "orange",
|
tone: "orange",
|
||||||
active: activeId === "stream-off"
|
active: activeId === "stream-off"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "calibrate",
|
|
||||||
label: labels.calibrate,
|
|
||||||
tone: "cyan",
|
|
||||||
active: activeId === "calibrate"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "precision-test",
|
id: "precision-test",
|
||||||
label: labels.precisionTest,
|
label: labels.precisionTest,
|
||||||
@@ -1067,12 +1081,12 @@
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
if (devkitEnabled) {
|
if (isDevKitEnabled) {
|
||||||
links.push({
|
links.push({
|
||||||
id: "devkit",
|
id: "devkit",
|
||||||
label: "DevKit",
|
label: devkitLabel,
|
||||||
tone: "cyan",
|
tone: "cyan",
|
||||||
active: isDevKitConfigOpen
|
active: isDevKitOpen
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1195,6 +1209,12 @@
|
|||||||
connectionNotice = "";
|
connectionNotice = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: if (updateNoticeVisible && pendingUpdate && !updateInstallBusy) {
|
||||||
|
connectionNotice = locale === "zh-CN"
|
||||||
|
? `发现新版本 ${pendingUpdate.version},是否现在下载并安装?`
|
||||||
|
: `Version ${pendingUpdate.version} is available. Download and install now?`;
|
||||||
|
}
|
||||||
|
|
||||||
function handleLocaleChange(event: CustomEvent<LocaleCode>): void {
|
function handleLocaleChange(event: CustomEvent<LocaleCode>): void {
|
||||||
locale = event.detail;
|
locale = event.detail;
|
||||||
}
|
}
|
||||||
@@ -1726,6 +1746,7 @@
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
let unlistenHudStream: UnlistenFn | null = null;
|
let unlistenHudStream: UnlistenFn | null = null;
|
||||||
|
let unlistenDevkitPztAngle: UnlistenFn | null = null;
|
||||||
let stopMockFeed: (() => void) | null = null;
|
let stopMockFeed: (() => void) | null = null;
|
||||||
|
|
||||||
void ensureDefaultWindowSize();
|
void ensureDefaultWindowSize();
|
||||||
@@ -1749,6 +1770,23 @@
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Failed to listen for hud_stream:", error);
|
console.error("Failed to listen for hud_stream:", error);
|
||||||
});
|
});
|
||||||
|
void listen<{ seq: number; timestampMs: number; dtsMs: number; angle: number }>(
|
||||||
|
"devkit_pzt_angle",
|
||||||
|
(event) => {
|
||||||
|
console.log("[devkit_pzt_angle]", event.payload);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((unlisten) => {
|
||||||
|
if (disposed) {
|
||||||
|
unlisten();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unlistenDevkitPztAngle = unlisten;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Failed to listen for devkit_pzt_angle:", error);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
stopMockFeed = startMockFeed(applyPacket);
|
stopMockFeed = startMockFeed(applyPacket);
|
||||||
}
|
}
|
||||||
@@ -1758,6 +1796,7 @@
|
|||||||
pauseReplayPlayback();
|
pauseReplayPlayback();
|
||||||
stopMockFeed?.();
|
stopMockFeed?.();
|
||||||
unlistenHudStream?.();
|
unlistenHudStream?.();
|
||||||
|
unlistenDevkitPztAngle?.();
|
||||||
if (devkitStatusTimer != null) {
|
if (devkitStatusTimer != null) {
|
||||||
window.clearInterval(devkitStatusTimer);
|
window.clearInterval(devkitStatusTimer);
|
||||||
devkitStatusTimer = null;
|
devkitStatusTimer = null;
|
||||||
@@ -1849,8 +1888,6 @@
|
|||||||
rangeLabel={uiCopy.rangeLabel}
|
rangeLabel={uiCopy.rangeLabel}
|
||||||
rangeMinLabel={uiCopy.rangeMinLabel}
|
rangeMinLabel={uiCopy.rangeMinLabel}
|
||||||
rangeMaxLabel={uiCopy.rangeMaxLabel}
|
rangeMaxLabel={uiCopy.rangeMaxLabel}
|
||||||
colorMapLabel={uiCopy.colorMapLabel}
|
|
||||||
{colorMapOptions}
|
|
||||||
replaySectionLabel={uiCopy.replaySectionLabel}
|
replaySectionLabel={uiCopy.replaySectionLabel}
|
||||||
replayPlayLabel={uiCopy.replayPlayLabel}
|
replayPlayLabel={uiCopy.replayPlayLabel}
|
||||||
replayPauseLabel={uiCopy.replayPauseLabel}
|
replayPauseLabel={uiCopy.replayPauseLabel}
|
||||||
@@ -1863,6 +1900,7 @@
|
|||||||
{replayProgress}
|
{replayProgress}
|
||||||
{replayFileName}
|
{replayFileName}
|
||||||
{replayFrameInfo}
|
{replayFrameInfo}
|
||||||
|
{sessionStartedAt}
|
||||||
resetConfigLabel={uiCopy.resetConfigLabel}
|
resetConfigLabel={uiCopy.resetConfigLabel}
|
||||||
applyLiveHint={uiCopy.applyLiveHint}
|
applyLiveHint={uiCopy.applyLiveHint}
|
||||||
leftPanels={leftSignalPanels}
|
leftPanels={leftSignalPanels}
|
||||||
@@ -1880,7 +1918,7 @@
|
|||||||
>
|
>
|
||||||
{#if !isPrecisionTestOpen}
|
{#if !isPrecisionTestOpen}
|
||||||
<section class="range-scale" aria-label="Signal Range">
|
<section class="range-scale" aria-label="Signal Range">
|
||||||
<p class="range-label">Range</p>
|
<p class="range-label">{locale === "zh-CN" ? "范围" : "Range"}</p>
|
||||||
<div class="range-track">
|
<div class="range-track">
|
||||||
{#each rangeTicks as tick}
|
{#each rangeTicks as tick}
|
||||||
<span class="range-tick">{tick}</span>
|
<span class="range-tick">{tick}</span>
|
||||||
@@ -1919,12 +1957,10 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{#if isDevKitConfigOpen && devkitEnabled}
|
{#if isDevKitConfigOpen && devkitEnabled}
|
||||||
<div class="devkit-overlay" role="dialog" aria-label="DevKit Config">
|
<div class="devkit-overlay" role="dialog" aria-label={locale === "zh-CN" ? "开发工具配置" : "DevKit Config"}>
|
||||||
<div class="devkit-float">
|
<div class="devkit-float">
|
||||||
<DevKitConfigPanel
|
<DevKitConfigPanel
|
||||||
running={devkitRunning}
|
running={devkitRunning}
|
||||||
port={devkitPort}
|
|
||||||
framesSent={devkitFramesSent}
|
|
||||||
filterLiftEnabled={devkitFilterLift}
|
filterLiftEnabled={devkitFilterLift}
|
||||||
saveAsXlsx={devkitSaveXlsx}
|
saveAsXlsx={devkitSaveXlsx}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
|
|||||||
Reference in New Issue
Block a user