调整 Autocal 标定记录格式与目录命名

将人工参考载荷按档位区间保存,重排帧字段并明确界面操作。已通过相关测试、代码和文档检查。
This commit is contained in:
ykcheungab
2026-10-11 16:21:49 +08:00
parent dc7e290489
commit 69c1322616
9 changed files with 233 additions and 46 deletions
+115 -5
View File
@@ -6,8 +6,10 @@ module owns only the site, planned steps and operator-confirmed event order.
from __future__ import annotations
import csv
import hashlib
import json
import re
import time
from dataclasses import dataclass
from pathlib import Path
@@ -16,6 +18,26 @@ import numpy as np
from orisys_app_shared.record import ExperimentRecorder, RecordConfig
REFERENCE_HEADER = (
"step_index",
"branch",
"target_load_n",
"t_start_host_mono_ns",
"t_end_host_mono_ns",
"F_ref_normal_N",
"F_ref_x_N",
"F_ref_y_N",
"F_ref_z_N",
"force_frame",
"source",
"stability_verified",
)
def _folder_part(value: str) -> str:
return re.sub(r"[^\w-]+", "-", value.strip()).strip("-_ ")[:48]
@dataclass(frozen=True)
class Site:
u_px: int
@@ -84,6 +106,10 @@ class ManualTrial:
self.aborted = False
self.reference_alignment_possible = False
self._last_confirm_ns: int | None = None
self._reference_file = None
self._reference_writer = None
self._active_reference: tuple[int, str, float, float, int] | None = None
self._reference_error = ""
def start(
self,
@@ -92,6 +118,7 @@ class ManualTrial:
site: Site,
levels: list[float],
hold_s: float,
sensor_model: str,
sensor_sn: str,
bundle_path: str,
source: dict,
@@ -104,6 +131,15 @@ class ManualTrial:
raise RuntimeError("start the finger input and wait for its first frame")
if not sensor_sn.strip():
raise ValueError("sensor SN is required")
if not sensor_model.strip():
raise ValueError("sensor model is required")
model_part = _folder_part(sensor_model)
sn_part = _folder_part(sensor_sn)
if not model_part or not sn_part:
raise ValueError(
"sensor model and SN need letters or numbers for the folder"
)
label = f"{model_part}_{sn_part}"
if not float(fps) > 0.0:
raise ValueError("input frame rate must be positive")
self.steps = load_path(levels)
@@ -122,6 +158,7 @@ class ManualTrial:
metadata = {
"purpose": "autocal_finger_manual_trial",
"sensor_sn": sensor_sn.strip(),
"sensor_model": sensor_model.strip(),
"site": {
"uv_xy_px": [site.u_px, site.v_px],
"position_sensor_mm": list(site.position_sensor_mm),
@@ -138,15 +175,36 @@ class ManualTrial:
"time_basis": packet.time_basis,
"reference_alignment_possible": self.reference_alignment_possible,
"reference_source": "manual_operator_confirmation",
"reference_file": "reference.csv",
}
recorder = ExperimentRecorder(
RecordConfig(fps=float(fps), segment_seconds=24 * 3600.0)
)
directory = recorder.start(
root=Path(root), session_id=packet.session_id, metadata=metadata
root=Path(root),
session_id=packet.session_id,
metadata=metadata,
directory_label=label,
)
reference_file = None
try:
reference_file = (directory / "reference.csv").open(
"w", newline="", encoding="utf-8"
)
reference_writer = csv.writer(reference_file)
reference_writer.writerow(REFERENCE_HEADER)
reference_file.flush()
except OSError:
if reference_file is not None:
reference_file.close()
recorder.stop(wait=True)
raise
self.recorder = recorder
self.directory = directory
self._reference_file = reference_file
self._reference_writer = reference_writer
self._active_reference = None
self._reference_error = ""
self.step_index = 0
self.aborted = False
self._last_confirm_ns = None
@@ -161,11 +219,11 @@ class ManualTrial:
self.steps[self.step_index] if self.step_index < len(self.steps) else None
)
def _event(self, kind: str, detail: dict) -> None:
def _event(self, kind: str, detail: dict, *, now_ns: int | None = None) -> None:
recorder = self.recorder
if recorder is None:
return
now_mono = time.perf_counter_ns()
now_mono = now_ns if now_ns is not None else time.perf_counter_ns()
packet = self.worker.latest()
if packet is not None and packet.capture_mono_ns is not None:
event_mono_t = (
@@ -185,6 +243,33 @@ class ManualTrial:
frame_id=frame_id,
)
def _end_reference(self, end_ns: int) -> None:
if self._active_reference is None:
return
step_index, branch, target, reference, start_ns = self._active_reference
try:
self._reference_writer.writerow(
(
step_index,
branch,
target,
start_ns,
end_ns,
reference,
"",
"",
"",
"",
"manual_operator_confirmation",
0,
)
)
self._reference_file.flush()
except OSError as exc:
self._reference_error = str(exc)
finally:
self._active_reference = None
def confirm(self, reference_n: float) -> tuple[str, float]:
if self.status != "recording" or self.next_step is None:
raise RuntimeError("no load step is waiting for confirmation")
@@ -194,12 +279,20 @@ class ManualTrial:
raise ValueError("reference load must be between 0 and 30 N")
now_ns = time.perf_counter_ns()
if self._last_confirm_ns is not None:
self._end_reference(now_ns)
if self._reference_error:
self.finish(
aborted=True,
reason=f"reference write failed: {self._reference_error}",
)
raise OSError(self._reference_error)
self._event(
"step_ended",
{
"step_index": self.step_index - 1,
"held_s": (now_ns - self._last_confirm_ns) / 1e9,
},
now_ns=now_ns,
)
self._event(
"step_reached",
@@ -210,7 +303,9 @@ class ManualTrial:
"reference_load_n": reference,
"reference_kind": "manual",
},
now_ns=now_ns,
)
self._active_reference = (self.step_index, branch, target, reference, now_ns)
self.step_index += 1
self._last_confirm_ns = now_ns
return branch, target
@@ -226,13 +321,28 @@ class ManualTrial:
raise RuntimeError("no active trial")
if not aborted and self.next_step is not None:
raise RuntimeError("complete the load/unload path before finishing")
now_ns = time.perf_counter_ns()
if self._last_confirm_ns is not None:
self._end_reference(now_ns)
self._event(
"step_ended",
{"step_index": self.step_index - 1, "held_s": self.held_s},
{
"step_index": self.step_index - 1,
"held_s": (now_ns - self._last_confirm_ns) / 1e9,
},
now_ns=now_ns,
)
try:
self._reference_file.close()
except OSError as exc:
self._reference_error = str(exc)
if self._reference_error:
aborted = True
reason = f"reference write failed: {self._reference_error}"
self._event(
"trial_aborted" if aborted else "trial_finished", {"reason": reason}
"trial_aborted" if aborted else "trial_finished",
{"reason": reason},
now_ns=now_ns,
)
self.aborted = aborted
self.worker.remove_sink(self.recorder.on_packet)