From a8eaa4c367e855d113ced361afe9e02063984c28 Mon Sep 17 00:00:00 2001 From: lxp <2770281812@qq.com> Date: Fri, 18 Sep 2026 13:37:03 +0800 Subject: [PATCH] =?UTF-8?q?255=E6=BC=82=E7=A7=BB=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E7=A8=B3=E5=AE=9A=E6=97=B6=E9=97=B4=E6=94=B930=E7=A7=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/linkerhand_range_calibration/README.md | 5 +- .../config/station.yaml | 3 +- .../core/engine.py | 27 +- .../linkerhand_range_calibration/profiles.py | 5 +- .../fixtures/group_upper_reference_drift.json | 1667 +++++++++++++++++ .../test/test_core.py | 3 +- .../test/test_observation_evidence.py | 4 +- .../test/test_sampling_diagnostics.py | 99 +- 8 files changed, 1798 insertions(+), 15 deletions(-) create mode 100644 src/linkerhand_range_calibration/test/fixtures/group_upper_reference_drift.json diff --git a/src/linkerhand_range_calibration/README.md b/src/linkerhand_range_calibration/README.md index e61a07b..e1fdcf2 100644 --- a/src/linkerhand_range_calibration/README.md +++ b/src/linkerhand_range_calibration/README.md @@ -205,8 +205,9 @@ SDK 自身的初始化检查和设备保护保持原样。标定仍要求 SDK 先按实际时间戳估计四角点的移动趋势,再用去除趋势后的 MAD 和相邻帧差分估计随机抖动。 全窗口及前、后半窗口分别检查趋势,避免缓慢往返运动在整个窗口中互相抵消;每个半窗口至少4帧。 持续漂移取各段 `max(0, 趋势累计位移 - 抖动裕量)` 的最大值,裕量由随机抖动、帧数和时间分布计算。 -两项门槛独立生效:随机抖动允许到0.5像素,低噪声条件下的持续漂移仍按0.1像素检查。任一项超限就保持当前指令继续等待,超过 `point_timeout` 仍不能稳定则暂停。 -`point_timeout` 默认6秒,给换向后的短时振动留出衰减时间;这是最长等待时间,达到稳定条件就立即采样推进。持续振动仍会暂停,不通过放宽噪声或漂移门槛获得结果。 +两项门槛独立生效:随机抖动允许到0.5像素,低噪声条件下的持续漂移仍按0.1像素检查。任一项超限就保持当前指令继续等待,到达该阶段的等待时限仍不能稳定则暂停。 +端点参考使用独立的 `reference_timeout`,默认30秒,给大幅定位后的缓慢位移留出稳定时间;普通逐点采样、准备和回位仍使用 `point_timeout`,默认6秒。等待从软件轨迹到达目标时开始,达到原有稳定条件就立即推进,不固定等满时限。端点参考超时会明确提示“端点参考未稳定”、保持目标的时长以及具体 Tag 的原因,日志同时保存 `elapsed_seconds` 和 `timeout_seconds`。扫描过程中丢失 Tag 导致轨迹暂停仍使用6秒时限。 +延长参考等待不会放宽噪声或漂移门槛;255的参考观测通过稳定检查后,才开始发送254。若30秒后仍不满足稳定条件,程序继续按超时暂停处理;需结合实机排查手指、Tag 粘贴和相机/手掌固定处。 这是对有限图像的估计,并非已完全静止的保证。高噪声会降低细小运动的可辨别性,实机准确性仍需复测;不根据四指一起变化自动扣除整体位移,以免消除四指真实的同步运动。 旧工位文件的 `stability_px` 仍可加载,仅作为随机抖动上限;新增配置优先,漂移上限默认0.1。建议自定义工位改用上述三个明确字段。 diff --git a/src/linkerhand_range_calibration/config/station.yaml b/src/linkerhand_range_calibration/config/station.yaml index 11d379e..6ae9fa3 100644 --- a/src/linkerhand_range_calibration/config/station.yaml +++ b/src/linkerhand_range_calibration/config/station.yaml @@ -21,7 +21,8 @@ scan: stable_seconds: 0.5 # 新图像必须覆盖连续静止时间,不能只凭几帧局部平稳。 reference_seconds: 1.5 # 每端开始前额外核对前、中、后三段参考位置一致。 motion_frames: 2 # 瞬时运动须超过参考和当前点的抖动包络;不看方向。 - point_timeout: 6 # 最长等待振动衰减时间;稳定后立即推进,不固定等满6秒。 + point_timeout: 6 # 普通逐点采样的最长等待;稳定后立即推进。 + reference_timeout: 30 # 到达端点后允许更长时间稳定;达标立即开始,不固定等满30秒。 freshness: 0.5 feedback_tolerance: 2 repeat_tolerance: 2 diff --git a/src/linkerhand_range_calibration/linkerhand_range_calibration/core/engine.py b/src/linkerhand_range_calibration/linkerhand_range_calibration/core/engine.py index 7d2e820..ab40469 100644 --- a/src/linkerhand_range_calibration/linkerhand_range_calibration/core/engine.py +++ b/src/linkerhand_range_calibration/linkerhand_range_calibration/core/engine.py @@ -232,8 +232,20 @@ class Engine: 'reason': reason}) return issues + @property + def collecting_reference(self): + return self.state == 'SCANNING' and any( + isinstance(window, ReferenceWindow) for window in self.windows.values()) + + @property + def waiting_timeout(self): + return (self.settings.reference_timeout if self.collecting_reference + else self.settings.point_timeout) + def _pause_sampling_timeout(self, now, moving=False): restoring = self.state == 'RESTORING' + limit = self.settings.point_timeout if moving else self.waiting_timeout + elapsed = now - (self.blocked_since if moving else self.arrived) issues = [] if restoring else self._visibility_issues(now) if not moving: indices = self.active_indices if self.state == 'SCANNING' else None @@ -248,8 +260,14 @@ class Engine: view, tag_id = self.keys[name] issues.append({'source': 'tag', 'joint': name, 'view': view, 'tag_id': tag_id, 'window_samples': window.snapshot(), **problem}) - prefix = ('目标 Tag 持续不可见或观测过期' if moving else - '测后回位稳定超时' if restoring else '单点稳定采样超时') + if moving: + prefix = '目标 Tag 持续不可见或观测过期' + elif restoring: + prefix = '测后回位稳定超时' + elif self.collecting_reference: + prefix = f'端点参考未稳定:等待超时(已保持目标 {elapsed:.1f} 秒,上限 {limit:g} 秒)' + else: + prefix = '单点稳定采样超时' details = '\n'.join(describe_issue(issue) for issue in issues) shared_motion = shared_translation(issues) if shared_motion: @@ -258,9 +276,10 @@ class Engine: # Conditions can recover on the tick that crosses the deadline. context = self.restoring_joint if restoring else '、'.join( tag_label(view, tag_id, name) for name, (view, tag_id) in self.keys.items()) - details = f'{context}:条件刚恢复,但已超过 {self.settings.point_timeout:g} 秒等待时限' + details = f'{context}:条件刚恢复,但已超过 {limit:g} 秒等待时限' self._record('sampling_timeout', time=now, task=self.task.name, stage=self.stage, direction=self.direction, repeat=self.repeat, target=list(self.command), + elapsed_seconds=elapsed, timeout_seconds=limit, issues=issues, shared_motion=shared_motion, reason=details) self.pause(prefix + '\n' + details) @@ -308,7 +327,7 @@ class Engine: for window in self.windows.values(): window.clear() return - if now-self.arrived > self.settings.point_timeout: + if now-self.arrived > self.waiting_timeout: self._pause_sampling_timeout(now) return if self.state == 'RESTORING': diff --git a/src/linkerhand_range_calibration/linkerhand_range_calibration/profiles.py b/src/linkerhand_range_calibration/linkerhand_range_calibration/profiles.py index 2a8efd8..4f8eb0c 100644 --- a/src/linkerhand_range_calibration/linkerhand_range_calibration/profiles.py +++ b/src/linkerhand_range_calibration/linkerhand_range_calibration/profiles.py @@ -97,6 +97,7 @@ class Settings: reference_seconds: float = 1.5 # Verify the endpoint across independent time blocks. motion_frames: int = 2 # Consecutive valid frames at this command, independent of direction. point_timeout: float = 6 + reference_timeout: float = 30 # Endpoint positioning can settle slower than a one-unit step. freshness: float = 0.5 feedback_tolerance: float = 2 non_target_tolerance: float = 3 # Legacy configuration only; unrelated motion does not gate scans. @@ -141,8 +142,8 @@ class Settings: raise ValueError('单点超时必须大于最短等待与稳定观察时长之和') if result.reference_seconds < result.stable_seconds: raise ValueError('端点参考观察时长不能短于普通稳定观察时长') - if result.point_timeout <= result.settle_seconds + result.reference_seconds: - raise ValueError('单点超时必须大于最短等待与端点参考观察时长之和') + if result.reference_timeout <= result.settle_seconds + result.reference_seconds: + raise ValueError('端点参考超时必须大于最短等待与端点参考观察时长之和') return result diff --git a/src/linkerhand_range_calibration/test/fixtures/group_upper_reference_drift.json b/src/linkerhand_range_calibration/test/fixtures/group_upper_reference_drift.json new file mode 100644 index 0000000..e45653c --- /dev/null +++ b/src/linkerhand_range_calibration/test/fixtures/group_upper_reference_drift.json @@ -0,0 +1,1667 @@ +{ + "source_session": "20260917_195710_953214", + "description": "Four-finger upper reference remained in smooth drift after holding 255 for six seconds. No frames after the timeout are recorded.", + "held_seconds": 6.00038968201261, + "target": [ + 255, + 255, + 255, + 255 + ], + "issues": [ + { + "joint": "middle_mcp_roll", + "tag_id": 3, + "window_samples": [ + { + "stamp_ns": 1789646741618200400, + "corners": [ + [ + 647.8538818359374, + 598.7337646484375 + ], + [ + 701.4808349609375, + 592.4707641601562 + ], + [ + 694.6845092773438, + 536.0392456054685 + ], + [ + 640.7659301757811, + 543.4821777343749 + ] + ] + }, + { + "stamp_ns": 1789646741684883920, + "corners": [ + [ + 647.8479003906251, + 598.7316894531247 + ], + [ + 701.4734497070312, + 592.4694824218749 + ], + [ + 694.67138671875, + 536.0384521484377 + ], + [ + 640.7500610351562, + 543.4845581054688 + ] + ] + }, + { + "stamp_ns": 1789646741718225680, + "corners": [ + [ + 647.8277587890626, + 598.7236328125002 + ], + [ + 701.4558715820314, + 592.4621582031249 + ], + [ + 694.6532592773438, + 536.0350952148438 + ], + [ + 640.7288818359374, + 543.4732055664064 + ] + ] + }, + { + "stamp_ns": 1789646741751567440, + "corners": [ + [ + 647.8157348632812, + 598.7330322265625 + ], + [ + 701.4384155273438, + 592.46240234375 + ], + [ + 694.6363525390626, + 536.038818359375 + ], + [ + 640.7250366210939, + 543.4793701171875 + ] + ] + }, + { + "stamp_ns": 1789646741784909200, + "corners": [ + [ + 647.812744140625, + 598.7422485351562 + ], + [ + 701.4390869140625, + 592.466552734375 + ], + [ + 694.641845703125, + 536.0386352539062 + ], + [ + 640.7237548828125, + 543.494140625 + ] + ] + }, + { + "stamp_ns": 1789646741818250960, + "corners": [ + [ + 647.8096313476561, + 598.748779296875 + ], + [ + 701.4369506835936, + 592.470703125 + ], + [ + 694.6223144531251, + 536.0488891601562 + ], + [ + 640.7149658203126, + 543.49951171875 + ] + ] + }, + { + "stamp_ns": 1789646741851568573, + "corners": [ + [ + 647.7973022460939, + 598.7518310546876 + ], + [ + 701.4266967773439, + 592.4723510742188 + ], + [ + 694.6168823242189, + 536.0452880859375 + ], + [ + 640.7025756835939, + 543.5059814453126 + ] + ] + }, + { + "stamp_ns": 1789646741884910333, + "corners": [ + [ + 647.7857666015626, + 598.7423706054688 + ], + [ + 701.4183959960938, + 592.4698486328125 + ], + [ + 694.6087646484375, + 536.0390014648436 + ], + [ + 640.6982421875002, + 543.4940185546875 + ] + ] + }, + { + "stamp_ns": 1789646741951593853, + "corners": [ + [ + 647.7604370117185, + 598.7429809570311 + ], + [ + 701.3922119140623, + 592.4627075195312 + ], + [ + 694.5729370117189, + 536.04296875 + ], + [ + 640.6630859375, + 543.4953613281249 + ] + ] + }, + { + "stamp_ns": 1789646741984935613, + "corners": [ + [ + 647.7603149414065, + 598.7517700195314 + ], + [ + 701.388916015625, + 592.4732666015625 + ], + [ + 694.5712890625, + 536.0507202148436 + ], + [ + 640.6608276367188, + 543.5033569335936 + ] + ] + }, + { + "stamp_ns": 1789646742018277373, + "corners": [ + [ + 647.7572021484374, + 598.7670898437499 + ], + [ + 701.3822021484374, + 592.476806640625 + ], + [ + 694.5662841796875, + 536.0554809570314 + ], + [ + 640.6471557617188, + 543.5139770507814 + ] + ] + }, + { + "stamp_ns": 1789646742051619133, + "corners": [ + [ + 647.73583984375, + 598.7660522460938 + ], + [ + 701.3676757812499, + 592.4767456054688 + ], + [ + 694.5477905273439, + 536.0533447265626 + ], + [ + 640.6346435546877, + 543.5115356445312 + ] + ] + }, + { + "stamp_ns": 1789646742084960893, + "corners": [ + [ + 647.7352294921875, + 598.7586059570311 + ], + [ + 701.37255859375, + 592.4710693359375 + ], + [ + 694.5452880859375, + 536.0423583984375 + ], + [ + 640.6389160156251, + 543.5063476562499 + ] + ] + }, + { + "stamp_ns": 1789646742151644413, + "corners": [ + [ + 647.7101440429688, + 598.7485351562499 + ], + [ + 701.3444824218751, + 592.465576171875 + ], + [ + 694.5155639648438, + 536.0355834960939 + ], + [ + 640.5977783203125, + 543.5007934570312 + ] + ] + }, + { + "stamp_ns": 1789646742184986173, + "corners": [ + [ + 647.6968994140626, + 598.7690429687501 + ], + [ + 701.3276977539062, + 592.4685668945312 + ], + [ + 694.5007934570312, + 536.03662109375 + ], + [ + 640.5881347656251, + 543.5161743164062 + ] + ] + }, + { + "stamp_ns": 1789646742218327933, + "corners": [ + [ + 647.7109375, + 598.7700805664062 + ], + [ + 701.3370361328124, + 592.4723510742188 + ], + [ + 694.5056762695312, + 536.0518798828125 + ], + [ + 640.5910034179689, + 543.5218505859376 + ] + ] + }, + { + "stamp_ns": 1789646742251669693, + "corners": [ + [ + 647.7178344726561, + 598.7812499999998 + ], + [ + 701.3456420898438, + 592.4699707031249 + ], + [ + 694.5158691406251, + 536.0552368164064 + ], + [ + 640.5982055664061, + 543.5137939453126 + ] + ] + }, + { + "stamp_ns": 1789646742285011453, + "corners": [ + [ + 647.6891479492186, + 598.773132324219 + ], + [ + 701.3236694335938, + 592.4756469726562 + ], + [ + 694.4868164062501, + 536.0549926757811 + ], + [ + 640.5764770507812, + 543.5132446289062 + ] + ] + }, + { + "stamp_ns": 1789646742318353213, + "corners": [ + [ + 647.6666870117188, + 598.776123046875 + ], + [ + 701.3050537109376, + 592.4749755859375 + ], + [ + 694.4710083007812, + 536.0431518554688 + ], + [ + 640.5514526367186, + 543.5112304687499 + ] + ] + }, + { + "stamp_ns": 1789646742385167154, + "corners": [ + [ + 647.6519165039062, + 598.7715454101562 + ], + [ + 701.29150390625, + 592.4685058593751 + ], + [ + 694.4492797851564, + 536.0447387695314 + ], + [ + 640.5321655273439, + 543.5190429687501 + ] + ] + }, + { + "stamp_ns": 1789646742418508914, + "corners": [ + [ + 647.6458129882814, + 598.77734375 + ], + [ + 701.286865234375, + 592.4713134765625 + ], + [ + 694.444580078125, + 536.0458374023439 + ], + [ + 640.5347290039064, + 543.5260620117188 + ] + ] + }, + { + "stamp_ns": 1789646742451850674, + "corners": [ + [ + 647.6422119140624, + 598.777099609375 + ], + [ + 701.2815551757812, + 592.4683837890625 + ], + [ + 694.4476318359375, + 536.05322265625 + ], + [ + 640.5343017578124, + 543.5184326171875 + ] + ] + }, + { + "stamp_ns": 1789646742485192434, + "corners": [ + [ + 647.6401977539061, + 598.770202636719 + ], + [ + 701.2809448242189, + 592.4740600585939 + ], + [ + 694.4338378906251, + 536.0422973632811 + ], + [ + 640.5207519531249, + 543.5291748046875 + ] + ] + }, + { + "stamp_ns": 1789646742518534194, + "corners": [ + [ + 647.6198120117188, + 598.7784423828125 + ], + [ + 701.2578735351561, + 592.4780273437501 + ], + [ + 694.415283203125, + 536.0504760742188 + ], + [ + 640.4993286132815, + 543.5336303710936 + ] + ] + }, + { + "stamp_ns": 1789646742551875954, + "corners": [ + [ + 647.6058349609375, + 598.7910156250001 + ], + [ + 701.2438354492188, + 592.4748535156251 + ], + [ + 694.401611328125, + 536.0529174804686 + ], + [ + 640.486572265625, + 543.5405883789061 + ] + ] + }, + { + "stamp_ns": 1789646742585217714, + "corners": [ + [ + 647.5998535156249, + 598.7890014648436 + ], + [ + 701.24609375, + 592.4754638671875 + ], + [ + 694.4003295898438, + 536.0548095703125 + ], + [ + 640.4920043945311, + 543.5397338867186 + ] + ] + }, + { + "stamp_ns": 1789646742651901234, + "corners": [ + [ + 647.6033325195311, + 598.7827758789062 + ], + [ + 701.2414550781249, + 592.4616088867188 + ], + [ + 694.3950805664062, + 536.0427856445312 + ], + [ + 640.4771728515627, + 543.526123046875 + ] + ] + }, + { + "stamp_ns": 1789646742685242994, + "corners": [ + [ + 647.5838012695311, + 598.7815551757814 + ], + [ + 701.2233276367189, + 592.4619750976562 + ], + [ + 694.3743896484375, + 536.041564941406 + ], + [ + 640.4652709960935, + 543.5289306640625 + ] + ] + }, + { + "stamp_ns": 1789646742718584754, + "corners": [ + [ + 647.59033203125, + 598.7964477539061 + ], + [ + 701.2161865234376, + 592.4716796875 + ], + [ + 694.3687133789062, + 536.0543823242189 + ], + [ + 640.4604492187499, + 543.5394287109376 + ] + ] + }, + { + "stamp_ns": 1789646742751926514, + "corners": [ + [ + 647.5802001953125, + 598.8054199218752 + ], + [ + 701.2112426757811, + 592.4829711914062 + ], + [ + 694.3599243164062, + 536.063537597656 + ], + [ + 640.458984375, + 543.5560302734375 + ] + ] + }, + { + "stamp_ns": 1789646742785268274, + "corners": [ + [ + 647.571044921875, + 598.8065185546875 + ], + [ + 701.2061767578123, + 592.4774169921875 + ], + [ + 694.3489379882812, + 536.0626220703125 + ], + [ + 640.4490966796878, + 543.5537109375001 + ] + ] + }, + { + "stamp_ns": 1789646742818610034, + "corners": [ + [ + 647.568359375, + 598.797119140625 + ], + [ + 701.2034912109376, + 592.472900390625 + ], + [ + 694.3471069335936, + 536.0496215820314 + ], + [ + 640.4536132812499, + 543.5468139648438 + ] + ] + }, + { + "stamp_ns": 1789646742851951794, + "corners": [ + [ + 647.5726928710939, + 598.7844238281251 + ], + [ + 701.2040405273439, + 592.4630126953125 + ], + [ + 694.3482055664062, + 536.0446166992186 + ], + [ + 640.4478759765623, + 543.5352172851561 + ] + ] + }, + { + "stamp_ns": 1789646742918565931, + "corners": [ + [ + 647.531005859375, + 598.8031005859374 + ], + [ + 701.171569824219, + 592.46923828125 + ], + [ + 694.3016357421875, + 536.0574951171875 + ], + [ + 640.4138793945311, + 543.5494384765625 + ] + ] + }, + { + "stamp_ns": 1789646742951907691, + "corners": [ + [ + 647.5239257812499, + 598.816833496094 + ], + [ + 701.1596679687499, + 592.477783203125 + ], + [ + 694.2971801757814, + 536.0648803710936 + ], + [ + 640.4052124023438, + 543.5670776367189 + ] + ] + }, + { + "stamp_ns": 1789646742985249451, + "corners": [ + [ + 647.5377807617188, + 598.8178710937501 + ], + [ + 701.1697998046876, + 592.4782104492188 + ], + [ + 694.3023071289064, + 536.0637817382812 + ], + [ + 640.4148559570312, + 543.5625610351562 + ] + ] + }, + { + "stamp_ns": 1789646743018591211, + "corners": [ + [ + 647.5380249023438, + 598.80517578125 + ], + [ + 701.1702270507812, + 592.4702758789062 + ], + [ + 694.2982177734375, + 536.0568237304686 + ], + [ + 640.4086303710936, + 543.5582885742186 + ] + ] + }, + { + "stamp_ns": 1789646743051932971, + "corners": [ + [ + 647.517822265625, + 598.8000488281249 + ], + [ + 701.1482543945312, + 592.4654541015625 + ], + [ + 694.2857055664062, + 536.0477294921876 + ], + [ + 640.4000244140625, + 543.5504760742188 + ] + ] + }, + { + "stamp_ns": 1789646743118616491, + "corners": [ + [ + 647.5017089843751, + 598.8192749023438 + ], + [ + 701.127685546875, + 592.4779663085938 + ], + [ + 694.2595825195312, + 536.0574951171876 + ], + [ + 640.3659057617189, + 543.5700073242188 + ] + ] + } + ], + "drift_px": 0.3674605633083937, + "jitter_px": 0.01017392432865831 + }, + { + "joint": "ring_mcp_roll", + "tag_id": 2, + "window_samples": [ + { + "stamp_ns": 1789646741618200400, + "corners": [ + [ + 538.4630126953124, + 620.6002807617188 + ], + [ + 590.4893188476561, + 603.3847656250001 + ], + [ + 572.6082763671875, + 549.7672729492188 + ], + [ + 521.2317504882812, + 567.7176513671874 + ] + ] + }, + { + "stamp_ns": 1789646741684883920, + "corners": [ + [ + 538.4321899414062, + 620.6201171875001 + ], + [ + 590.4549560546875, + 603.392333984375 + ], + [ + 572.5816650390625, + 549.7676391601562 + ], + [ + 521.1964111328125, + 567.7338256835939 + ] + ] + }, + { + "stamp_ns": 1789646741718225680, + "corners": [ + [ + 538.3907470703124, + 620.6243896484375 + ], + [ + 590.4232177734374, + 603.3895263671875 + ], + [ + 572.5504760742188, + 549.7701416015625 + ], + [ + 521.157958984375, + 567.7247924804688 + ] + ] + }, + { + "stamp_ns": 1789646741751567440, + "corners": [ + [ + 538.3607788085938, + 620.6249389648439 + ], + [ + 590.404846191406, + 603.3950805664061 + ], + [ + 572.5297851562501, + 549.7804565429688 + ], + [ + 521.1299438476564, + 567.7362670898439 + ] + ] + }, + { + "stamp_ns": 1789646741784909200, + "corners": [ + [ + 538.3609619140626, + 620.6398315429688 + ], + [ + 590.3890991210939, + 603.4031372070312 + ], + [ + 572.5224609375, + 549.7850341796874 + ], + [ + 521.1193847656248, + 567.7565307617186 + ] + ] + }, + { + "stamp_ns": 1789646741818250960, + "corners": [ + [ + 538.3370361328124, + 620.6514282226562 + ], + [ + 590.3671264648438, + 603.4139404296875 + ], + [ + 572.5036010742188, + 549.7916870117189 + ], + [ + 521.0915527343749, + 567.7744140625 + ] + ] + }, + { + "stamp_ns": 1789646741851568573, + "corners": [ + [ + 538.326904296875, + 620.6693115234375 + ], + [ + 590.3496704101561, + 603.4090576171875 + ], + [ + 572.4901733398438, + 549.802490234375 + ], + [ + 521.0597534179689, + 567.7826538085939 + ] + ] + }, + { + "stamp_ns": 1789646741884910333, + "corners": [ + [ + 538.3151855468752, + 620.6604614257814 + ], + [ + 590.3289794921878, + 603.4155883789064 + ], + [ + 572.4757080078125, + 549.7821655273436 + ], + [ + 521.0264892578124, + 567.7941284179686 + ] + ] + }, + { + "stamp_ns": 1789646741951593853, + "corners": [ + [ + 538.2568359374999, + 620.673095703125 + ], + [ + 590.2908325195311, + 603.412109375 + ], + [ + 572.4241943359375, + 549.7828979492186 + ], + [ + 520.9682006835939, + 567.8023071289061 + ] + ] + }, + { + "stamp_ns": 1789646741984935613, + "corners": [ + [ + 538.2667236328125, + 620.6956787109374 + ], + [ + 590.2788696289062, + 603.4170532226562 + ], + [ + 572.42626953125, + 549.8046264648438 + ], + [ + 520.9501342773438, + 567.8162841796875 + ] + ] + }, + { + "stamp_ns": 1789646742018277373, + "corners": [ + [ + 538.2487182617188, + 620.7019653320314 + ], + [ + 590.2703857421874, + 603.434814453125 + ], + [ + 572.3943481445312, + 549.8060913085938 + ], + [ + 520.9313354492189, + 567.8480224609375 + ] + ] + }, + { + "stamp_ns": 1789646742051619133, + "corners": [ + [ + 538.2186889648438, + 620.7026977539061 + ], + [ + 590.249084472656, + 603.4371948242186 + ], + [ + 572.3839721679688, + 549.8090209960938 + ], + [ + 520.9053344726565, + 567.8472900390625 + ] + ] + }, + { + "stamp_ns": 1789646742084960893, + "corners": [ + [ + 538.2142333984374, + 620.7010498046874 + ], + [ + 590.2475585937501, + 603.4296874999999 + ], + [ + 572.3544921875, + 549.8047485351562 + ], + [ + 520.892639160156, + 567.8499145507812 + ] + ] + }, + { + "stamp_ns": 1789646742151644413, + "corners": [ + [ + 538.1649169921876, + 620.7082519531251 + ], + [ + 590.2106323242186, + 603.4185791015626 + ], + [ + 572.3024291992188, + 549.7983398437499 + ], + [ + 520.8413085937502, + 567.8500366210936 + ] + ] + }, + { + "stamp_ns": 1789646742184986173, + "corners": [ + [ + 538.1607055664061, + 620.7145385742186 + ], + [ + 590.1997680664062, + 603.4291381835938 + ], + [ + 572.2696533203125, + 549.8114013671876 + ], + [ + 520.8154296874999, + 567.8679809570312 + ] + ] + }, + { + "stamp_ns": 1789646742218327933, + "corners": [ + [ + 538.1558837890624, + 620.7233886718751 + ], + [ + 590.1967163085936, + 603.4425048828125 + ], + [ + 572.2694702148439, + 549.8265380859374 + ], + [ + 520.8139038085938, + 567.8820800781249 + ] + ] + }, + { + "stamp_ns": 1789646742251669693, + "corners": [ + [ + 538.1660156249999, + 620.7364501953124 + ], + [ + 590.1956787109375, + 603.4415893554688 + ], + [ + 572.2656860351562, + 549.8324584960938 + ], + [ + 520.8124389648436, + 567.8912963867188 + ] + ] + }, + { + "stamp_ns": 1789646742285011453, + "corners": [ + [ + 538.1240844726562, + 620.7290649414061 + ], + [ + 590.175598144531, + 603.4363403320311 + ], + [ + 572.2361450195312, + 549.8296508789064 + ], + [ + 520.7821044921876, + 567.903564453125 + ] + ] + }, + { + "stamp_ns": 1789646742318353213, + "corners": [ + [ + 538.1010131835939, + 620.7418212890624 + ], + [ + 590.1527099609375, + 603.4234619140625 + ], + [ + 572.2033081054688, + 549.8291015625002 + ], + [ + 520.7543334960939, + 567.9031372070312 + ] + ] + }, + { + "stamp_ns": 1789646742385167154, + "corners": [ + [ + 538.054443359375, + 620.7639770507811 + ], + [ + 590.1304321289062, + 603.4352416992188 + ], + [ + 572.1663818359375, + 549.8424682617189 + ], + [ + 520.7328491210936, + 567.9131469726561 + ] + ] + }, + { + "stamp_ns": 1789646742418508914, + "corners": [ + [ + 538.0391235351564, + 620.7791137695311 + ], + [ + 590.1190795898439, + 603.4359741210938 + ], + [ + 572.1529541015625, + 549.8487548828125 + ], + [ + 520.7265014648436, + 567.9207763671873 + ] + ] + }, + { + "stamp_ns": 1789646742451850674, + "corners": [ + [ + 538.0359497070311, + 620.783935546875 + ], + [ + 590.1143188476562, + 603.4329833984374 + ], + [ + 572.1356811523438, + 549.850341796875 + ], + [ + 520.7125244140625, + 567.9241943359376 + ] + ] + }, + { + "stamp_ns": 1789646742485192434, + "corners": [ + [ + 538.0100097656248, + 620.7898559570314 + ], + [ + 590.0941772460936, + 603.4330444335938 + ], + [ + 572.122802734375, + 549.8509521484373 + ], + [ + 520.7152099609375, + 567.9254760742188 + ] + ] + }, + { + "stamp_ns": 1789646742518534194, + "corners": [ + [ + 537.9857788085936, + 620.7980346679686 + ], + [ + 590.0809936523439, + 603.4461059570314 + ], + [ + 572.0819702148436, + 549.8665161132811 + ], + [ + 520.7006225585935, + 567.9352416992186 + ] + ] + }, + { + "stamp_ns": 1789646742551875954, + "corners": [ + [ + 537.9666137695314, + 620.8140869140624 + ], + [ + 590.0610351562501, + 603.441650390625 + ], + [ + 572.0573120117188, + 549.8693237304689 + ], + [ + 520.6837768554688, + 567.9500122070312 + ] + ] + }, + { + "stamp_ns": 1789646742585217714, + "corners": [ + [ + 537.9557495117186, + 620.8141479492189 + ], + [ + 590.0541381835938, + 603.4451904296875 + ], + [ + 572.0487670898439, + 549.876220703125 + ], + [ + 520.6786499023436, + 567.9495239257814 + ] + ] + }, + { + "stamp_ns": 1789646742651901234, + "corners": [ + [ + 537.925048828125, + 620.8115234375 + ], + [ + 590.0438232421874, + 603.4334716796875 + ], + [ + 572.0200805664062, + 549.8723754882812 + ], + [ + 520.6649169921877, + 567.9414672851564 + ] + ] + }, + { + "stamp_ns": 1789646742685242994, + "corners": [ + [ + 537.9102172851562, + 620.8177490234376 + ], + [ + 590.0243530273438, + 603.4234619140625 + ], + [ + 571.9832153320312, + 549.86767578125 + ], + [ + 520.6503295898439, + 567.9525756835938 + ] + ] + }, + { + "stamp_ns": 1789646742718584754, + "corners": [ + [ + 537.8900146484375, + 620.8362426757814 + ], + [ + 590.0238037109375, + 603.4442749023436 + ], + [ + 571.96240234375, + 549.8878173828125 + ], + [ + 520.6503295898436, + 567.9677124023439 + ] + ] + }, + { + "stamp_ns": 1789646742751926514, + "corners": [ + [ + 537.8774414062501, + 620.8515624999998 + ], + [ + 590.0120239257812, + 603.4572143554688 + ], + [ + 571.9426879882811, + 549.9022827148439 + ], + [ + 520.6417236328125, + 567.9755859375 + ] + ] + }, + { + "stamp_ns": 1789646742785268274, + "corners": [ + [ + 537.8672485351561, + 620.8549804687499 + ], + [ + 590.0003051757814, + 603.4519653320312 + ], + [ + 571.9302978515625, + 549.9068603515625 + ], + [ + 520.6271972656249, + 567.9747924804688 + ] + ] + }, + { + "stamp_ns": 1789646742818610034, + "corners": [ + [ + 537.8571166992186, + 620.8535766601561 + ], + [ + 589.9962158203124, + 603.4442749023438 + ], + [ + 571.9177856445314, + 549.9016723632814 + ], + [ + 520.6268310546876, + 567.9700317382812 + ] + ] + }, + { + "stamp_ns": 1789646742851951794, + "corners": [ + [ + 537.8530883789061, + 620.841003417969 + ], + [ + 589.9899291992188, + 603.4362182617188 + ], + [ + 571.9122314453125, + 549.8972778320311 + ], + [ + 520.6192626953124, + 567.9620361328126 + ] + ] + }, + { + "stamp_ns": 1789646742918565931, + "corners": [ + [ + 537.7893066406249, + 620.8701171875002 + ], + [ + 589.9541015624999, + 603.4492797851562 + ], + [ + 571.8533935546875, + 549.9022216796873 + ], + [ + 520.6043701171875, + 567.9761352539062 + ] + ] + }, + { + "stamp_ns": 1789646742951907691, + "corners": [ + [ + 537.7835083007811, + 620.890869140625 + ], + [ + 589.9420776367186, + 603.4649658203125 + ], + [ + 571.8458862304688, + 549.9193115234376 + ], + [ + 520.6003417968751, + 567.9912109375001 + ] + ] + }, + { + "stamp_ns": 1789646742985249451, + "corners": [ + [ + 537.7901611328125, + 620.8864135742185 + ], + [ + 589.9467163085939, + 603.4684448242186 + ], + [ + 571.8407592773438, + 549.9119873046877 + ], + [ + 520.5936889648438, + 568.0044555664064 + ] + ] + }, + { + "stamp_ns": 1789646743018591211, + "corners": [ + [ + 537.7817993164061, + 620.8883666992189 + ], + [ + 589.944091796875, + 603.4565429687501 + ], + [ + 571.837890625, + 549.9218749999998 + ], + [ + 520.5866088867186, + 567.9862060546874 + ] + ] + }, + { + "stamp_ns": 1789646743051932971, + "corners": [ + [ + 537.7787475585938, + 620.8842163085935 + ], + [ + 589.9172363281249, + 603.4462280273438 + ], + [ + 571.8204956054689, + 549.9067993164065 + ], + [ + 520.5618896484376, + 567.9871215820312 + ] + ] + }, + { + "stamp_ns": 1789646743118616491, + "corners": [ + [ + 537.7418212890625, + 620.9046020507812 + ], + [ + 589.8798217773438, + 603.4660644531251 + ], + [ + 571.7765502929686, + 549.924072265625 + ], + [ + 520.5403442382812, + 568.0194702148436 + ] + ] + } + ], + "drift_px": 0.7282181873036626, + "jitter_px": 0.012890813901842853 + } + ] +} diff --git a/src/linkerhand_range_calibration/test/test_core.py b/src/linkerhand_range_calibration/test/test_core.py index 8d05f8d..19766ab 100644 --- a/src/linkerhand_range_calibration/test/test_core.py +++ b/src/linkerhand_range_calibration/test/test_core.py @@ -618,7 +618,8 @@ def test_pinky_pip_uses_its_own_tag_and_feedback_despite_coupled_motion(scenario for event in commands) if scenario in ('missing_side', 'moving_active_feedback'): assert engine.state == 'PAUSED' - assert ('Tag' if scenario == 'missing_side' else '稳定采样超时') in engine.reason + assert ('侧面 Tag ID5' if scenario == 'missing_side' + else '关节 pinky_pip:反馈未稳定') in engine.reason assert not engine.samples else: assert engine.state == 'COMPLETED', engine.reason diff --git a/src/linkerhand_range_calibration/test/test_observation_evidence.py b/src/linkerhand_range_calibration/test/test_observation_evidence.py index a182922..a7df252 100644 --- a/src/linkerhand_range_calibration/test/test_observation_evidence.py +++ b/src/linkerhand_range_calibration/test/test_observation_evidence.py @@ -208,6 +208,8 @@ def test_one_pass_four_fingers_with_late_settling_noise_and_replay(tmp_path, see def test_reference_duration_and_timeout_configuration_are_validated(): - for change in ({'reference_seconds': .1}, {'point_timeout': 1}): + for change in ({'reference_seconds': .1}, {'reference_timeout': 1}): with pytest.raises(ValueError, match='参考观察时长'): Settings.from_dict(change) + # Each deadline validates only the observation window it actually governs. + assert Settings.from_dict({'point_timeout': 1}).reference_timeout == 30 diff --git a/src/linkerhand_range_calibration/test/test_sampling_diagnostics.py b/src/linkerhand_range_calibration/test/test_sampling_diagnostics.py index 4b5e637..2653c19 100644 --- a/src/linkerhand_range_calibration/test/test_sampling_diagnostics.py +++ b/src/linkerhand_range_calibration/test/test_sampling_diagnostics.py @@ -1,5 +1,7 @@ """Timeout explanations must identify actual blockers before windows are cleared.""" from dataclasses import replace +import json +from pathlib import Path import numpy as np import pytest @@ -14,10 +16,10 @@ from linkerhand_range_calibration.vision.reference import ReferenceWindow class SamplingRun: - def __init__(self, task='thumb_cmc_roll', phase='waiting'): + def __init__(self, task='thumb_cmc_roll', phase='waiting', settings=None): profile = load_profile() - settings = Settings(rate=1000, stable_frames=4, stable_seconds=.15, reference_seconds=.45, - settle_seconds=.05, point_timeout=.8) + settings = settings or Settings(rate=1000, stable_frames=4, stable_seconds=.15, reference_seconds=.45, + settle_seconds=.05, point_timeout=.8, reference_timeout=.8) self.adapter = FakeAdapter(profile) self.events, self.now = [], 1.0 self.engine = Engine(profile, settings, self.adapter, self.events.append) @@ -106,7 +108,7 @@ def test_four_finger_sampling_advances_with_random_jitter_above_old_limit(): @pytest.mark.parametrize('deadline,settles_at', [(3, 4), (6, 4), (6, 0), (6, None)]) def test_waits_for_transient_vibration_without_advancing_or_relaxing_limits(deadline, settles_at): run = SamplingRun('four_finger_roll') - run.engine.settings = replace(run.engine.settings, point_timeout=deadline) + run.engine.settings = replace(run.engine.settings, reference_timeout=deadline) started, sent = run.now, len(run.adapter.sent) def shake(frames, rejected): @@ -136,6 +138,95 @@ def test_waits_for_transient_vibration_without_advancing_or_relaxing_limits(dead assert observation['jitter_px'] <= .5 and observation['drift_px'] <= .1 +def test_recorded_upper_reference_drift_is_still_rejected(): + path = Path(__file__).parent / 'fixtures' / 'group_upper_reference_drift.json' + recorded = json.loads(path.read_text()) + settings = Settings() + assert recorded['target'] == [255] * 4 + assert settings.point_timeout < recorded['held_seconds'] < settings.reference_timeout + assert {issue['tag_id'] for issue in recorded['issues']} == {2, 3} + for issue in recorded['issues']: + window = ReferenceWindow(settings) + for sample in issue['window_samples']: + window.add(Observation(sample['stamp_ns'], 0, sample['corners'])) + problem = window.problem(settings.stability_noise_px, settings.stability_drift_px) + assert problem['drift_px'] == pytest.approx(issue['drift_px']) + assert problem['jitter_px'] < .02 + assert '持续漂移' in problem['reason'] + # The real recording ends at the timeout. Recovery below is synthetic; + # it tests scheduling without claiming the real hardware settled later. + + +@pytest.mark.parametrize('direction', ['up', 'down']) +@pytest.mark.parametrize('settles_at', [0, 8, 18, None]) +def test_endpoint_holds_until_stable_or_reference_deadline(direction, settles_at): + settings = Settings(rate=10000, stable_frames=4, settle_seconds=.05) + run = SamplingRun('four_finger_roll', settings=settings) + for _ in range(2000): + if (run.engine.direction == direction and run.engine.collecting_reference + and run.engine.phase == 'waiting'): + break + run.step() + else: + pytest.fail('未到达端点参考采样阶段') + started = run.engine.arrived + sent, events_before = len(run.adapter.sent), len(run.events) + target = run.engine.command + + def slow_settling(frames, rejected): + elapsed = run.now - started + # Middle/ring drift independently of stable encoder feedback. + travel = elapsed if settles_at is None else min(elapsed, settles_at) + for tag, rate in [(3, .3), (2, .6)]: + frames['front'][tag] = (np.asarray(frames['front'][tag]) + [travel * rate, 0]).tolist() + + for _ in range(round((settings.reference_timeout + 1) / .05)): + run.step(slow_settling) + samples = [event for event in run.events[events_before:] if event['kind'] == 'sample'] + assert len(run.adapter.sent) == sent and run.engine.command == target + if samples or run.engine.state == 'PAUSED': + break + if settles_at is None: + assert not samples and run.engine.state == 'PAUSED' + assert '端点参考未稳定' in run.engine.reason + timeout = run.events[-2] + assert timeout['kind'] == 'sampling_timeout' + assert timeout['timeout_seconds'] == settings.reference_timeout + assert settings.reference_timeout < timeout['elapsed_seconds'] <= settings.reference_timeout + .1 + assert {issue['tag_id'] for issue in timeout['issues']} == {2, 3} + else: + assert len(samples) == 1 and run.engine.state == 'SCANNING' + assert settles_at <= run.now - started < settles_at + 2 + for observation in samples[0]['observations'].values(): + assert observation['drift_px'] <= .1 and observation['jitter_px'] <= .5 + if settles_at: + assert run.now - started > settings.point_timeout + assert all(search.boundary is None for search in run.engine.searches.values()) + + +def test_ordinary_command_does_not_inherit_reference_deadline(): + run = SamplingRun('four_finger_roll', settings=Settings(rate=10000, stable_frames=4, settle_seconds=.05)) + for _ in range(300): + run.step() + if run.engine.stage == 'search' and run.engine.phase == 'waiting': + break + assert run.engine.stage == 'search' and not run.engine.collecting_reference + started, sent = run.engine.arrived, len(run.adapter.sent) + + def creep(frames, rejected): + frames['front'][2] = (np.asarray(frames['front'][2]) + [run.now - started, 0]).tolist() + + for _ in range(130): + run.step(creep) + if run.engine.state == 'PAUSED': + break + assert run.engine.state == 'PAUSED' and '单点稳定采样超时' in run.engine.reason + assert len(run.adapter.sent) == sent + timeout = run.events[-2] + assert timeout['timeout_seconds'] == 6 + assert 6 < timeout['elapsed_seconds'] <= 6.1 + + @pytest.mark.parametrize('phase', ['moving', 'waiting']) @pytest.mark.parametrize('reason', [None, 'Tag 像素尺寸不足']) def test_missing_or_rejected_tag_does_not_blame_other_group_tags(phase, reason):