using System; using System.Collections.Concurrent; using IvfTl.Hardware; using Newtonsoft.Json.Linq; namespace IvfTl.ControlHost.Debug { /// /// control 端调试会话后端:会话表 + 借用/归还 + 心跳续约 + 超时自动回收 + 命令分发。 /// 安全地基(spec §5):绝不指望 operate 主动还,SweepExpired 超时回收兜底。 /// public sealed class DebugSessionManager { private readonly Func _gateOf; private readonly Func _clock; private readonly int _ttlMs; private readonly Action _log; private readonly Func _cultivationOf; // (Critical 并发修复)"会话即将关闭"回调(参数=sid):在 Release/SweepExpired 回收某会话、 // Dispose lease 之前调用,通知标定先停并等当前孔跑完(Program.cs 装配时接到 calibMgr.StopAndWait)。 // 用可空 Action + 可后置 SetOnSessionClosing,打破"CalibrationManager 依赖 DebugSessionManager / 回调又要调 // CalibrationManager"的循环依赖(先 new debugMgr → new calibMgr(debugMgr) → debugMgr.SetOnSessionClosing(...))。 private volatile Action _onSessionClosing; private readonly ConcurrentDictionary _sessions = new ConcurrentDictionary(); public DebugSessionManager(Func gateOf, Func clock, int ttlMs, Action log, Func cultivationOf = null, Action onSessionClosing = null) { _gateOf = gateOf; _clock = clock; _ttlMs = ttlMs; _log = log ?? (_ => { }); _cultivationOf = cultivationOf; _onSessionClosing = onSessionClosing; } /// (装配用)后置注入"会话即将关闭"回调,打破与 CalibrationManager 的循环依赖。 public void SetOnSessionClosing(Action onSessionClosing) => _onSessionClosing = onSessionClosing; // 会话关闭前钩子:Dispose lease 之前调,先停该 sid 标定并等当前孔跑完(回调内部吞异常,绝不阻断回收/Dispose)。 private void InvokeOnSessionClosing(string sid) { var cb = _onSessionClosing; if (cb == null) return; try { cb(sid); } catch (Exception ex) { _log($"[debug] onSessionClosing 回调异常 sid={sid}: {ex.Message}"); } } public DebugCommandResult Acquire(int houseSn) { var gate = _gateOf(houseSn); if (gate == null) return DebugCommandResult.Fail("NO_HANDLE", $"舱{houseSn}无闸门"); var lease = gate.Acquire(HardwareUser.OperateDebug); if (lease == null) return DebugCommandResult.Fail("BUSY", $"舱{houseSn}被占用,借用超时"); string sid = Guid.NewGuid().ToString("N"); _sessions[sid] = new DebugSession(sid, houseSn, lease, _clock()); var res = DebugCommandResult.Okay(sid); try { if (_cultivationOf != null) { var c = _cultivationOf(houseSn); res.Cultivating = c.cultivating; res.EmbryoCount = c.embryoCount; } } catch (Exception ex) { _log($"[debug] 取培养态异常 舱{houseSn}: {ex.Message}"); } _log($"[debug] acquire 舱{houseSn} sid={sid}"); return res; } public DebugCommandResult Heartbeat(string sid) { if (sid != null && _sessions.TryGetValue(sid, out var s)) { s.LastSeen = _clock(); return DebugCommandResult.Okay(); } return DebugCommandResult.Fail("SESSION_EXPIRED", "会话不存在或已过期"); } /// 只读取会话(供推流端点校验 sid)。不刷新 LastSeen、不改状态。 public bool TryGet(string sid, out DebugSession session) { if (sid != null) return _sessions.TryGetValue(sid, out session); session = null; return false; } public DebugCommandResult Release(string sid) { if (sid != null && _sessions.TryRemove(sid, out var s)) { InvokeOnSessionClosing(sid); // (Critical)Dispose 前先停标定并等当前孔跑完,再恢复采集,消除争用 try { s.Lease.Dispose(); } catch (Exception ex) { _log($"[debug] dispose 异常 sid={sid} 舱{s.HouseSn}: {ex.Message}"); } _log($"[debug] release sid={sid} 舱{s.HouseSn}"); } return DebugCommandResult.Okay(); } /// 超时回收:LastSeen + ttl < now 的会话自动归还(spec §5.1)。 public int SweepExpired() { int n = 0; var now = _clock(); foreach (var kv in _sessions) { if ((now - kv.Value.LastSeen).TotalMilliseconds > _ttlMs) { if (_sessions.TryRemove(kv.Key, out var s)) { InvokeOnSessionClosing(kv.Key); // (Critical)同 Release:超时回收路径也在 Dispose 前先停标定并等当前孔跑完 try { s.Lease.Dispose(); } catch (Exception ex) { _log($"[debug] dispose 异常 sid={kv.Key} 舱{s.HouseSn}: {ex.Message}"); } _log($"[debug] 会话超时自动回收 sid={kv.Key} 舱{s.HouseSn}"); n++; } } } return n; } public DebugCommandResult Execute(string sid, string op, JObject args) { if (sid == null || !_sessions.TryGetValue(sid, out var s)) return DebugCommandResult.Fail("SESSION_EXPIRED", "会话不存在或已过期"); s.LastSeen = _clock(); var ser = s.Lease.Serial; if (ser == null) return DebugCommandResult.Fail("NO_HANDLE", "借用串口句柄为空"); try { switch (op) { // 相机曝光:operate 进程相机是空壳,调试页设曝光改走 control 借用的真相机(Task3.4b)。 case "SetExposure": { var cam = s.Lease.Camera; if (cam == null) return DebugCommandResult.Fail("NO_HANDLE", "借用相机句柄为空"); int e = args?["value"] != null ? args["value"].Value() : 0; return DebugCommandResult.Okay(cam.SetExposure(e)); } case "ReadTemp": return DebugCommandResult.Okay(ser.TemperatureWait()); case "ReadPressure": return DebugCommandResult.Okay(ser.PressureWait()); case "ReadDoor": return DebugCommandResult.Okay(ser.DoorStatusWait().ToString()); case "ReadVentTime": return DebugCommandResult.Okay(ser.ReadOpenVentTimeWait()); case "ShakeHands": return DebugCommandResult.Okay(ser.ShakeHandsWait()); case "OpenLed": return DebugCommandResult.Okay(ser.OpenLedWait()); case "CloseLed": return DebugCommandResult.Okay(ser.CloseLedWait()); case "OpenIntake": return DebugCommandResult.Okay(ser.OpenIntakeValveWait()); case "CloseIntake": return DebugCommandResult.Okay(ser.CloseIntakeValveWait()); case "OpenExhaust": return DebugCommandResult.Okay(ser.OpenExhaustValveWait()); case "CloseExhaust": return DebugCommandResult.Okay(ser.CloseExhaustValveWait()); case "HouseAeration": return DebugCommandResult.Okay(ser.HouseAerationWait()); case "HouseVent": return DebugCommandResult.Okay(ser.HouseVentWait()); case "BufferState": { var (pressure, t1, t2) = ser.BufferBottleStateWait(); return DebugCommandResult.Okay(new { pressure, t1, t2 }); } case "BufferAeration": return DebugCommandResult.Okay(ser.BufferBottleAerationWait()); case "ReadLight": return DebugCommandResult.Okay(ser.ReadLightBrightnessWait()); case "WriteLight": { int v = args?["value"] != null ? args["value"].Value() : 0; return DebugCommandResult.Okay(ser.WriteLightBrightnessWait(v)); } case "WriteOpenIntakeTimeBuffer": { int v = args?["value"] != null ? args["value"].Value() : 0; return DebugCommandResult.Okay(ser.WriteOpenIntakeTimeWait(v, isBuffer: true)); } default: return ExecuteMotorOrEeprom(s, ser, op, args); } } catch (Exception ex) { return DebugCommandResult.Fail("HARDWARE_ERROR", ex.Message); } } // Task7 先占位:返回 BAD_OP,让"未知 op"测试通过;Task7 替换为真实电机/EEPROM 分发。 private DebugCommandResult ExecuteMotorOrEeprom(DebugSession s, IvfTl.Hardware.ISerialChannel ser, string op, JObject args) { int Arg(string k, int def = 0) => args?[k] != null ? args[k].Value() : def; int delay = Arg("motorDelay", -1); switch (op) { case "VerticalReset": { bool ok = ser.VerticalResetWait(delay); if (ok) s.CurrentVer = 0; return DebugCommandResult.Okay(ok); } case "VerticalMoveTo": { int pos = Arg("pos"); if (!MotorClamp.IsVerticalInRange(pos)) return DebugCommandResult.Fail("OUT_OF_RANGE", $"垂直目标{pos}越界[0,{MotorClamp.VerMax}]"); bool ok = ser.VerticalMoveToWait(pos, delay); if (ok) s.CurrentVer = pos; return DebugCommandResult.Okay(ok); } case "VerticalForward": case "VerticalBackward": { // 红线钳位必须基于回读的真实物理位,不信任会话跟踪位(防真机已在高位时相对运动越红线)。 int basePos = ser.ReadVerticalPositionWait(); int delta = Arg("value") * (op == "VerticalBackward" ? -1 : 1); int target = MotorClamp.RelativeTarget(basePos, delta); if (!MotorClamp.IsVerticalInRange(target)) return DebugCommandResult.Fail("OUT_OF_RANGE", $"垂直目标{target}越界"); bool ok = op == "VerticalBackward" ? ser.VerticalBackwardWait(Arg("value"), delay) : ser.VerticalForwardWait(Arg("value"), delay); if (ok) s.CurrentVer = ser.ReadVerticalPositionWait(); return DebugCommandResult.Okay(ok); } case "HorizontalReset": { bool ok = ser.HorizontalResetWait(delay); if (ok) s.CurrentHor = 0; return DebugCommandResult.Okay(ok); } case "HorizontalMoveTo": { int pos = Arg("pos"); if (!MotorClamp.IsHorizontalInRange(pos)) return DebugCommandResult.Fail("OUT_OF_RANGE", $"水平目标{pos}越界[0,{MotorClamp.HorMax}]"); bool ok = ser.HorizontalMoveToWait(pos, delay); if (ok) s.CurrentHor = pos; return DebugCommandResult.Okay(ok); } case "HorizontalForward": case "HorizontalBackward": { // 红线钳位必须基于回读的真实物理位,不信任会话跟踪位。 int basePos = ser.ReadHorizontalPositionWait(); int delta = Arg("value") * (op == "HorizontalBackward" ? -1 : 1); int target = MotorClamp.RelativeTarget(basePos, delta); if (!MotorClamp.IsHorizontalInRange(target)) return DebugCommandResult.Fail("OUT_OF_RANGE", $"水平目标{target}越界"); bool ok = op == "HorizontalBackward" ? ser.HorizontalBackwardWait(Arg("value"), delay) : ser.HorizontalForwardWait(Arg("value"), delay); if (ok) s.CurrentHor = ser.ReadHorizontalPositionWait(); return DebugCommandResult.Okay(ok); } case "WriteScanStep": return DebugCommandResult.Okay(ser.WriteScanStepWait(Arg("value"))); case "WriteOpenIntakeTime": return DebugCommandResult.Okay(ser.WriteOpenIntakeTimeWait(Arg("value"))); case "WriteOpenVentTime": return DebugCommandResult.Okay(ser.WriteOpenVentTimeWait(Arg("value"))); case "WriteWellHorizontalPos": return DebugCommandResult.Okay(ser.WriteWellHorizontalPosWait(Arg("well"), Arg("hor"))); default: return DebugCommandResult.Fail("BAD_OP", $"未知 op: {op}"); } } } }