调整 Autocal 标定记录格式与目录命名
将人工参考载荷按档位区间保存,重排帧字段并明确界面操作。已通过相关测试、代码和文档检查。
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user