fix: 修复打砖块游戏碰撞穿透bug,添加渐进提速机制

This commit is contained in:
lenn
2026-04-29 15:43:56 +08:00
parent 26533f6916
commit 326f07ed4f
23 changed files with 786 additions and 376 deletions

View File

@@ -1,6 +1,6 @@
@echo off
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...
python -m grpc_tools.protoc ^
@@ -21,4 +21,4 @@ pyinstaller ^
echo [3/3] Done!
echo Output: dist/je-skin-devkit-server.exe
pause
pause

View File

@@ -5,8 +5,8 @@ a = Analysis(
['sensor_server.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=['grpc', 'openpyxl'],
datas=[('sensor_stream_pb2.py', '.'), ('sensor_stream_pb2_grpc.py', '.')],
hiddenimports=['grpc', 'openpyxl', 'numpy'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],

View File

@@ -234,30 +234,63 @@ class SensorPushServicer(sensor_stream_pb2_grpc.SensorPushServicer):
def __init__(self):
self.frame_count = 0
self.last_report_time = time.time()
self.last_angle = None
def Upload(self, request_iterator, context):
print("[SensorPush] Client connected, waiting for frames...")
reset_baseline()
self.last_angle = None
for frame in request_iterator:
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:
now = time.time()
elapsed = now - self.last_report_time
fps = 100 / elapsed if elapsed > 0 else 0
self.last_report_time = now
angle_text = (
f"{self.last_angle:.2f}"
if self.last_angle is not None
else "n/a"
)
print(
f"[SensorPush] Frame #{frame.seq} | "
f"{frame.rows}x{frame.cols} | "
f"angle={angle_text} | "
f"force={frame.resultant_force:.1f} | "
f"total={self.frame_count} | ~{fps:.1f} fps"
)
print(f"[SensorPush] Stream ended. Total: {self.frame_count}")
return sensor_stream_pb2.UploadResponse(
ok=True,
frames_received=self.frame_count,
message=f"Processed {self.frame_count} frames",
)
class ExportProcessorServicer(sensor_stream_pb2_grpc.ExportProcessorServicer):
@@ -316,8 +349,119 @@ def serve(port: int):
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__":
parser = argparse.ArgumentParser(description="JE-Skin DevKit gRPC Server")
parser.add_argument("--port", type=int, default=50051, help="gRPC listen port (default: 50051)")
args = parser.parse_args()
serve(args.port)
serve(args.port)

View File

@@ -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()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -33,14 +33,14 @@ if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_SENSORFRAME']._serialized_start=39
_globals['_SENSORFRAME']._serialized_end=172
_globals['_UPLOADRESPONSE']._serialized_start=174
_globals['_UPLOADRESPONSE']._serialized_end=244
_globals['_PROCESSREQUEST']._serialized_start=246
_globals['_PROCESSREQUEST']._serialized_end=302
_globals['_PROCESSRESPONSE']._serialized_start=305
_globals['_PROCESSRESPONSE']._serialized_end=471
_globals['_SENSORPUSH']._serialized_start=473
_globals['_SENSORPUSH']._serialized_end=556
_globals['_EXPORTPROCESSOR']._serialized_start=558
_globals['_EXPORTPROCESSOR']._serialized_end=653
_globals['_PZTANGLERESPONSE']._serialized_start=174
_globals['_PZTANGLERESPONSE']._serialized_end=287
_globals['_PROCESSREQUEST']._serialized_start=289
_globals['_PROCESSREQUEST']._serialized_end=345
_globals['_PROCESSRESPONSE']._serialized_start=348
_globals['_PROCESSRESPONSE']._serialized_end=514
_globals['_SENSORPUSH']._serialized_start=516
_globals['_SENSORPUSH']._serialized_end=603
_globals['_EXPORTPROCESSOR']._serialized_start=605
_globals['_EXPORTPROCESSOR']._serialized_end=700
# @@protoc_insertion_point(module_scope)

View File

@@ -26,8 +26,7 @@ if _version_not_supported:
class SensorPushStub(object):
"""传感器数据推送服务 —— Rust 端作为 gRPC client 推送实时帧
"""
"""Missing associated documentation comment in .proto file."""
def __init__(self, channel):
"""Constructor.
@@ -35,16 +34,15 @@ class SensorPushStub(object):
Args:
channel: A grpc.Channel.
"""
self.Upload = channel.stream_unary(
self.Upload = channel.stream_stream(
'/sensor_stream.SensorPush/Upload',
request_serializer=sensor__stream__pb2.SensorFrame.SerializeToString,
response_deserializer=sensor__stream__pb2.UploadResponse.FromString,
response_deserializer=sensor__stream__pb2.PztAngleResponse.FromString,
_registered_method=True)
class SensorPushServicer(object):
"""传感器数据推送服务 —— Rust 端作为 gRPC client 推送实时帧
"""
"""Missing associated documentation comment in .proto file."""
def Upload(self, request_iterator, context):
"""Missing associated documentation comment in .proto file."""
@@ -55,10 +53,10 @@ class SensorPushServicer(object):
def add_SensorPushServicer_to_server(servicer, server):
rpc_method_handlers = {
'Upload': grpc.stream_unary_rpc_method_handler(
'Upload': grpc.stream_stream_rpc_method_handler(
servicer.Upload,
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(
@@ -69,8 +67,7 @@ def add_SensorPushServicer_to_server(servicer, server):
# This class is part of an EXPERIMENTAL API.
class SensorPush(object):
"""传感器数据推送服务 —— Rust 端作为 gRPC client 推送实时帧
"""
"""Missing associated documentation comment in .proto file."""
@staticmethod
def Upload(request_iterator,
@@ -83,12 +80,12 @@ class SensorPush(object):
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.stream_unary(
return grpc.experimental.stream_stream(
request_iterator,
target,
'/sensor_stream.SensorPush/Upload',
sensor__stream__pb2.SensorFrame.SerializeToString,
sensor__stream__pb2.UploadResponse.FromString,
sensor__stream__pb2.PztAngleResponse.FromString,
options,
channel_credentials,
insecure,
@@ -101,8 +98,7 @@ class SensorPush(object):
class ExportProcessorStub(object):
"""导出后处理服务 —— Rust 导出 CSV 后将路径发给 Python 做梯度过滤
"""
"""Missing associated documentation comment in .proto file."""
def __init__(self, channel):
"""Constructor.
@@ -118,8 +114,7 @@ class ExportProcessorStub(object):
class ExportProcessorServicer(object):
"""导出后处理服务 —— Rust 导出 CSV 后将路径发给 Python 做梯度过滤
"""
"""Missing associated documentation comment in .proto file."""
def ProcessFile(self, request, context):
"""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.
class ExportProcessor(object):
"""导出后处理服务 —— Rust 导出 CSV 后将路径发给 Python 做梯度过滤
"""
"""Missing associated documentation comment in .proto file."""
@staticmethod
def ProcessFile(request,