Просмотр исходного кода

feat(d2-02-t3): 标定时预览改读引擎OnFrame最新帧缓冲(避免与CalibrationEngine争相机锁),非标定维持GrabStable

- CalibrationManager 加 per-sid 最新帧缓冲(FrameSlot,volatile byte[] 整块原子赋值,无锁) + StoreLatestFrame/IsCalibrating/TryGetLatestFrame
- calibrateOne 闭包补接 engine.OnFrame=存帧(3.2a 漏接,本属推流通路);Stop 时清该 sid 帧缓冲
- StartPreviewStream while 循环:标定中(_calib.IsCalibrating)优先读缓冲帧,无帧则跳过等下一帧、绝不 GrabStable;非标定走原 GrabStable(行为零变化)
- CalibrationManagerTests 补 IsCalibrating/TryGetLatestFrame 非标定安全分支 2 单测

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
huangjie 2 дней назад
Родитель
Сommit
c1d230f7e5

+ 24 - 0
ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/CalibrationManagerTests.cs

@@ -62,5 +62,29 @@ namespace IvfTl.ControlHost.Tests
             Assert.True(calib.Stop("nope").Ok);
             Assert.True(calib.Stop(null).Ok);
         }
+
+        // ── Task3.2b:标定预览帧缓冲的「非标定/无帧」安全分支(推流线程据此决定走 GrabStable) ──
+
+        [Fact]
+        public void IsCalibrating_NoSession_Returns_False()
+        {
+            // 无活跃协作器(未起标定)→ IsCalibrating=false → 推流线程走原 GrabStable 分支(行为零变化)。
+            var (calib, _, _) = New();
+            Assert.False(calib.IsCalibrating("nope"));
+            Assert.False(calib.IsCalibrating(null));
+        }
+
+        [Fact]
+        public void TryGetLatestFrame_NoFrame_Returns_False_And_Empty_Out()
+        {
+            // 未标定/无缓冲帧 → TryGetLatestFrame=false 且 out 全空;
+            // 推流线程标定中遇此会跳过等下一帧、绝不 GrabStable(不争相机锁)。
+            var (calib, _, _) = New();
+            Assert.False(calib.TryGetLatestFrame("nope", out var bgr, out var w, out var h));
+            Assert.Null(bgr);
+            Assert.Equal(0, w);
+            Assert.Equal(0, h);
+            Assert.False(calib.TryGetLatestFrame(null, out _, out _, out _));
+        }
     }
 }

+ 17 - 2
ivf_tl_operate_2.0/control/ivf_tl_ControlHost/ControlHttpServer.cs

@@ -278,9 +278,24 @@ namespace IvfTl.ControlHost
                         if (!_debug.TryGet(session.SessionId, out _)) { _log($"[debug] 推流舱{session.HouseSn} 会话已失效,停"); break; }
                         try
                         {
-                            byte[] bgr = cam.GrabStable();   // 走全进程相机锁,与采集/对焦串行
+                            byte[] bgr; int fw, fh;
+                            // (Task3.2b) 标定进行时:相机被 CalibrationEngine 独占抓帧,这里【绝不 GrabStable】(否则与标定争相机原生锁)。
+                            // 改读引擎 OnFrame 喂的最新缓冲帧;此刻若还没帧(缓冲空),跳过这轮等下一帧,仍不 GrabStable。
+                            if (_calib != null && _calib.IsCalibrating(session.SessionId))
+                            {
+                                if (!_calib.TryGetLatestFrame(session.SessionId, out bgr, out fw, out fh))
+                                {
+                                    Thread.Sleep(66); continue;   // 标定中暂无帧:等下一帧,绝不抢相机
+                                }
+                            }
+                            else
+                            {
+                                // 非标定:维持原 GrabStable 行为(行为零变化)。
+                                bgr = cam.GrabStable();   // 走全进程相机锁,与采集/对焦串行
+                                fw = cam.Width; fh = cam.Height;
+                            }
                             if (bgr == null) { Thread.Sleep(100); continue; }
-                            byte[] jpeg = IvfTl.ControlHost.Debug.MjpegStreamWriter.EncodeJpeg(bgr, cam.Width, cam.Height);
+                            byte[] jpeg = IvfTl.ControlHost.Debug.MjpegStreamWriter.EncodeJpeg(bgr, fw, fh);
                             if (jpeg == null) { Thread.Sleep(100); continue; }
                             byte[] frame = IvfTl.ControlHost.Debug.MjpegStreamWriter.FrameBytes(jpeg);
                             outStream.Write(frame, 0, frame.Length);

+ 51 - 0
ivf_tl_operate_2.0/control/ivf_tl_ControlHost/Debug/CalibrationManager.cs

@@ -28,6 +28,54 @@ namespace IvfTl.ControlHost.Debug
         private readonly ConcurrentDictionary<string, CalibrationCoordinator> _bySid
             = new ConcurrentDictionary<string, CalibrationCoordinator>();
 
+        // (Task3.2b) 标定时预览用的"每 sid 最新一帧 BGR + 宽高"缓冲。
+        // 标定中相机被 CalibrationEngine 独占抓帧,推流线程绝不能再 GrabStable(会和标定争相机原生锁);
+        // 改由引擎 OnFrame 每次抓帧把原始帧塞进这里,推流线程只读这块缓冲、不碰相机。
+        // byte[] 整块引用赋值是原子的,读端拿到的要么是整块旧帧要么是整块新帧、不会撕裂,故无需锁。
+        private sealed class FrameSlot
+        {
+            public volatile byte[] Bgr;
+            public int Width;
+            public int Height;
+        }
+        private readonly ConcurrentDictionary<string, FrameSlot> _lastFrameBySid
+            = new ConcurrentDictionary<string, FrameSlot>();
+
+        // OnFrame 高频触发(标定线程每次 Grab),存帧要轻:只做一次整块引用赋值 + 记宽高。
+        private void StoreLatestFrame(string sid, byte[] bgr, int w, int h)
+        {
+            if (bgr == null) return;
+            var slot = _lastFrameBySid.GetOrAdd(sid, _ => new FrameSlot());
+            slot.Width = w;
+            slot.Height = h;
+            slot.Bgr = bgr;   // 最后赋 volatile 引用,保证宽高先就位再发布该帧
+        }
+
+        /// <summary>
+        /// (Task3.2b) 该 sid 是否正在标定(有活跃协作器且 IsRunning)。推流线程据此决定:
+        /// true→读 OnFrame 缓冲帧(不 GrabStable);false→走原 GrabStable。
+        /// </summary>
+        public bool IsCalibrating(string sid)
+        {
+            return sid != null
+                && _bySid.TryGetValue(sid, out var c)
+                && c.GetProgress().IsRunning;
+        }
+
+        /// <summary>
+        /// (Task3.2b) 取该 sid 最新一帧(引擎 OnFrame 喂的原始 BGR)。无帧返回 false。
+        /// 标定中即使此刻无帧,推流线程也绝不 GrabStable(避免争相机锁),跳过等下一帧即可。
+        /// </summary>
+        public bool TryGetLatestFrame(string sid, out byte[] bgr, out int w, out int h)
+        {
+            bgr = null; w = 0; h = 0;
+            if (sid == null || !_lastFrameBySid.TryGetValue(sid, out var slot)) return false;
+            var buf = slot.Bgr;   // 读 volatile 引用一次
+            if (buf == null) return false;
+            bgr = buf; w = slot.Width; h = slot.Height;
+            return true;
+        }
+
         public CalibrationManager(DebugSessionManager debug, Func<int, HouseBin> houseBinOf, Action<string> log = null)
         {
             _debug = debug ?? throw new ArgumentNullException(nameof(debug));
@@ -71,6 +119,8 @@ namespace IvfTl.ControlHost.Debug
                 var engine = new CalibrationEngine(s.Lease.Serial, s.Lease.Camera)
                 {
                     Log = msg => _log($"[calib][舱{s.HouseSn}][well{well}]{msg}"),
+                    // (Task3.2b) 引擎每次抓帧把原始 BGR 帧喂进 per-sid 缓冲,供推流线程标定时读取(替代 GrabStable,避免争相机锁)。
+                    OnFrame = buf => StoreLatestFrame(sid, buf, s.Lease.Camera.Width, s.Lease.Camera.Height),
                     HFineRange = range.HHalf,       // 水平微调半幅(围绕 HCenter)
                     ZCoarseCenter = vCenter,        // Z 粗扫中心=该 well 清晰位(取代引擎固定 90000)
                     ZCoarseHalf = range.VHalf,      // Z 粗扫半幅
@@ -123,6 +173,7 @@ namespace IvfTl.ControlHost.Debug
             if (sid != null && _bySid.TryRemove(sid, out var c))
             {
                 c.Stop();
+                _lastFrameBySid.TryRemove(sid, out _);   // (Task3.2b) 清该 sid 帧缓冲,避免陈旧帧 + 释放内存
                 _log($"[calib] 停标定 sid={sid}");
             }
             return DebugCommandResult.Okay();