DebugSessionManager.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. using System;
  2. using System.Collections.Concurrent;
  3. using IvfTl.Hardware;
  4. using Newtonsoft.Json.Linq;
  5. namespace IvfTl.ControlHost.Debug
  6. {
  7. /// <summary>
  8. /// control 端调试会话后端:会话表 + 借用/归还 + 心跳续约 + 超时自动回收 + 命令分发。
  9. /// 安全地基(spec §5):绝不指望 operate 主动还,SweepExpired 超时回收兜底。
  10. /// </summary>
  11. public sealed class DebugSessionManager
  12. {
  13. private readonly Func<int, IHouseGate> _gateOf;
  14. private readonly Func<DateTime> _clock;
  15. private readonly int _ttlMs;
  16. private readonly Action<string> _log;
  17. private readonly Func<int, (bool cultivating, int embryoCount)> _cultivationOf;
  18. // (Critical 并发修复)"会话即将关闭"回调(参数=sid):在 Release/SweepExpired 回收某会话、
  19. // Dispose lease 之前调用,通知标定先停并等当前孔跑完(Program.cs 装配时接到 calibMgr.StopAndWait)。
  20. // 用可空 Action + 可后置 SetOnSessionClosing,打破"CalibrationManager 依赖 DebugSessionManager / 回调又要调
  21. // CalibrationManager"的循环依赖(先 new debugMgr → new calibMgr(debugMgr) → debugMgr.SetOnSessionClosing(...))。
  22. private volatile Action<string> _onSessionClosing;
  23. private readonly ConcurrentDictionary<string, DebugSession> _sessions = new ConcurrentDictionary<string, DebugSession>();
  24. public DebugSessionManager(Func<int, IHouseGate> gateOf, Func<DateTime> clock, int ttlMs, Action<string> log,
  25. Func<int, (bool, int)> cultivationOf = null, Action<string> onSessionClosing = null)
  26. {
  27. _gateOf = gateOf; _clock = clock; _ttlMs = ttlMs; _log = log ?? (_ => { });
  28. _cultivationOf = cultivationOf;
  29. _onSessionClosing = onSessionClosing;
  30. }
  31. /// <summary>(装配用)后置注入"会话即将关闭"回调,打破与 CalibrationManager 的循环依赖。</summary>
  32. public void SetOnSessionClosing(Action<string> onSessionClosing) => _onSessionClosing = onSessionClosing;
  33. // 会话关闭前钩子:Dispose lease 之前调,先停该 sid 标定并等当前孔跑完(回调内部吞异常,绝不阻断回收/Dispose)。
  34. private void InvokeOnSessionClosing(string sid)
  35. {
  36. var cb = _onSessionClosing;
  37. if (cb == null) return;
  38. try { cb(sid); }
  39. catch (Exception ex) { _log($"[debug] onSessionClosing 回调异常 sid={sid}: {ex.Message}"); }
  40. }
  41. public DebugCommandResult Acquire(int houseSn)
  42. {
  43. var gate = _gateOf(houseSn);
  44. if (gate == null) return DebugCommandResult.Fail("NO_HANDLE", $"舱{houseSn}无闸门");
  45. var lease = gate.Acquire(HardwareUser.OperateDebug);
  46. if (lease == null) return DebugCommandResult.Fail("BUSY", $"舱{houseSn}被占用,借用超时");
  47. string sid = Guid.NewGuid().ToString("N");
  48. _sessions[sid] = new DebugSession(sid, houseSn, lease, _clock());
  49. var res = DebugCommandResult.Okay(sid);
  50. try
  51. {
  52. if (_cultivationOf != null) { var c = _cultivationOf(houseSn); res.Cultivating = c.cultivating; res.EmbryoCount = c.embryoCount; }
  53. }
  54. catch (Exception ex) { _log($"[debug] 取培养态异常 舱{houseSn}: {ex.Message}"); }
  55. _log($"[debug] acquire 舱{houseSn} sid={sid}");
  56. return res;
  57. }
  58. public DebugCommandResult Heartbeat(string sid)
  59. {
  60. if (sid != null && _sessions.TryGetValue(sid, out var s)) { s.LastSeen = _clock(); return DebugCommandResult.Okay(); }
  61. return DebugCommandResult.Fail("SESSION_EXPIRED", "会话不存在或已过期");
  62. }
  63. /// <summary>只读取会话(供推流端点校验 sid)。不刷新 LastSeen、不改状态。</summary>
  64. public bool TryGet(string sid, out DebugSession session)
  65. {
  66. if (sid != null) return _sessions.TryGetValue(sid, out session);
  67. session = null; return false;
  68. }
  69. public DebugCommandResult Release(string sid)
  70. {
  71. if (sid != null && _sessions.TryRemove(sid, out var s))
  72. {
  73. InvokeOnSessionClosing(sid); // (Critical)Dispose 前先停标定并等当前孔跑完,再恢复采集,消除争用
  74. try { s.Lease.Dispose(); } catch (Exception ex) { _log($"[debug] dispose 异常 sid={sid} 舱{s.HouseSn}: {ex.Message}"); }
  75. _log($"[debug] release sid={sid} 舱{s.HouseSn}");
  76. }
  77. return DebugCommandResult.Okay();
  78. }
  79. /// <summary>超时回收:LastSeen + ttl < now 的会话自动归还(spec §5.1)。</summary>
  80. public int SweepExpired()
  81. {
  82. int n = 0; var now = _clock();
  83. foreach (var kv in _sessions)
  84. {
  85. if ((now - kv.Value.LastSeen).TotalMilliseconds > _ttlMs)
  86. {
  87. if (_sessions.TryRemove(kv.Key, out var s))
  88. {
  89. InvokeOnSessionClosing(kv.Key); // (Critical)同 Release:超时回收路径也在 Dispose 前先停标定并等当前孔跑完
  90. try { s.Lease.Dispose(); } catch (Exception ex) { _log($"[debug] dispose 异常 sid={kv.Key} 舱{s.HouseSn}: {ex.Message}"); }
  91. _log($"[debug] 会话超时自动回收 sid={kv.Key} 舱{s.HouseSn}");
  92. n++;
  93. }
  94. }
  95. }
  96. return n;
  97. }
  98. public DebugCommandResult Execute(string sid, string op, JObject args)
  99. {
  100. if (sid == null || !_sessions.TryGetValue(sid, out var s))
  101. return DebugCommandResult.Fail("SESSION_EXPIRED", "会话不存在或已过期");
  102. s.LastSeen = _clock();
  103. var ser = s.Lease.Serial;
  104. if (ser == null) return DebugCommandResult.Fail("NO_HANDLE", "借用串口句柄为空");
  105. try
  106. {
  107. switch (op)
  108. {
  109. // 相机曝光:operate 进程相机是空壳,调试页设曝光改走 control 借用的真相机(Task3.4b)。
  110. case "SetExposure":
  111. {
  112. var cam = s.Lease.Camera;
  113. if (cam == null) return DebugCommandResult.Fail("NO_HANDLE", "借用相机句柄为空");
  114. int e = args?["value"] != null ? args["value"].Value<int>() : 0;
  115. return DebugCommandResult.Okay(cam.SetExposure(e));
  116. }
  117. case "ReadTemp": return DebugCommandResult.Okay(ser.TemperatureWait());
  118. case "ReadPressure": return DebugCommandResult.Okay(ser.PressureWait());
  119. case "ReadDoor": return DebugCommandResult.Okay(ser.DoorStatusWait().ToString());
  120. case "ReadVentTime": return DebugCommandResult.Okay(ser.ReadOpenVentTimeWait());
  121. case "ShakeHands": return DebugCommandResult.Okay(ser.ShakeHandsWait());
  122. case "OpenLed": return DebugCommandResult.Okay(ser.OpenLedWait());
  123. case "CloseLed": return DebugCommandResult.Okay(ser.CloseLedWait());
  124. case "OpenIntake": return DebugCommandResult.Okay(ser.OpenIntakeValveWait());
  125. case "CloseIntake": return DebugCommandResult.Okay(ser.CloseIntakeValveWait());
  126. case "OpenExhaust": return DebugCommandResult.Okay(ser.OpenExhaustValveWait());
  127. case "CloseExhaust": return DebugCommandResult.Okay(ser.CloseExhaustValveWait());
  128. case "HouseAeration": return DebugCommandResult.Okay(ser.HouseAerationWait());
  129. case "HouseVent": return DebugCommandResult.Okay(ser.HouseVentWait());
  130. case "BufferState":
  131. {
  132. var (pressure, t1, t2) = ser.BufferBottleStateWait();
  133. return DebugCommandResult.Okay(new { pressure, t1, t2 });
  134. }
  135. case "BufferAeration": return DebugCommandResult.Okay(ser.BufferBottleAerationWait());
  136. case "ReadLight": return DebugCommandResult.Okay(ser.ReadLightBrightnessWait());
  137. case "WriteLight":
  138. {
  139. int v = args?["value"] != null ? args["value"].Value<int>() : 0;
  140. return DebugCommandResult.Okay(ser.WriteLightBrightnessWait(v));
  141. }
  142. case "WriteOpenIntakeTimeBuffer":
  143. {
  144. int v = args?["value"] != null ? args["value"].Value<int>() : 0;
  145. return DebugCommandResult.Okay(ser.WriteOpenIntakeTimeWait(v, isBuffer: true));
  146. }
  147. default:
  148. return ExecuteMotorOrEeprom(s, ser, op, args);
  149. }
  150. }
  151. catch (Exception ex) { return DebugCommandResult.Fail("HARDWARE_ERROR", ex.Message); }
  152. }
  153. // Task7 先占位:返回 BAD_OP,让"未知 op"测试通过;Task7 替换为真实电机/EEPROM 分发。
  154. private DebugCommandResult ExecuteMotorOrEeprom(DebugSession s, IvfTl.Hardware.ISerialChannel ser, string op, JObject args)
  155. {
  156. int Arg(string k, int def = 0) => args?[k] != null ? args[k].Value<int>() : def;
  157. int delay = Arg("motorDelay", -1);
  158. switch (op)
  159. {
  160. case "VerticalReset": { bool ok = ser.VerticalResetWait(delay); if (ok) s.CurrentVer = 0; return DebugCommandResult.Okay(ok); }
  161. case "VerticalMoveTo":
  162. {
  163. int pos = Arg("pos");
  164. if (!MotorClamp.IsVerticalInRange(pos)) return DebugCommandResult.Fail("OUT_OF_RANGE", $"垂直目标{pos}越界[0,{MotorClamp.VerMax}]");
  165. bool ok = ser.VerticalMoveToWait(pos, delay); if (ok) s.CurrentVer = pos; return DebugCommandResult.Okay(ok);
  166. }
  167. case "VerticalForward":
  168. case "VerticalBackward":
  169. {
  170. // 红线钳位必须基于回读的真实物理位,不信任会话跟踪位(防真机已在高位时相对运动越红线)。
  171. int basePos = ser.ReadVerticalPositionWait();
  172. int delta = Arg("value") * (op == "VerticalBackward" ? -1 : 1);
  173. int target = MotorClamp.RelativeTarget(basePos, delta);
  174. if (!MotorClamp.IsVerticalInRange(target)) return DebugCommandResult.Fail("OUT_OF_RANGE", $"垂直目标{target}越界");
  175. bool ok = op == "VerticalBackward" ? ser.VerticalBackwardWait(Arg("value"), delay) : ser.VerticalForwardWait(Arg("value"), delay);
  176. if (ok) s.CurrentVer = ser.ReadVerticalPositionWait(); return DebugCommandResult.Okay(ok);
  177. }
  178. case "HorizontalReset": { bool ok = ser.HorizontalResetWait(delay); if (ok) s.CurrentHor = 0; return DebugCommandResult.Okay(ok); }
  179. case "HorizontalMoveTo":
  180. {
  181. int pos = Arg("pos");
  182. if (!MotorClamp.IsHorizontalInRange(pos)) return DebugCommandResult.Fail("OUT_OF_RANGE", $"水平目标{pos}越界[0,{MotorClamp.HorMax}]");
  183. bool ok = ser.HorizontalMoveToWait(pos, delay); if (ok) s.CurrentHor = pos; return DebugCommandResult.Okay(ok);
  184. }
  185. case "HorizontalForward":
  186. case "HorizontalBackward":
  187. {
  188. // 红线钳位必须基于回读的真实物理位,不信任会话跟踪位。
  189. int basePos = ser.ReadHorizontalPositionWait();
  190. int delta = Arg("value") * (op == "HorizontalBackward" ? -1 : 1);
  191. int target = MotorClamp.RelativeTarget(basePos, delta);
  192. if (!MotorClamp.IsHorizontalInRange(target)) return DebugCommandResult.Fail("OUT_OF_RANGE", $"水平目标{target}越界");
  193. bool ok = op == "HorizontalBackward" ? ser.HorizontalBackwardWait(Arg("value"), delay) : ser.HorizontalForwardWait(Arg("value"), delay);
  194. if (ok) s.CurrentHor = ser.ReadHorizontalPositionWait(); return DebugCommandResult.Okay(ok);
  195. }
  196. case "WriteScanStep": return DebugCommandResult.Okay(ser.WriteScanStepWait(Arg("value")));
  197. case "WriteOpenIntakeTime": return DebugCommandResult.Okay(ser.WriteOpenIntakeTimeWait(Arg("value")));
  198. case "WriteOpenVentTime": return DebugCommandResult.Okay(ser.WriteOpenVentTimeWait(Arg("value")));
  199. case "WriteWellHorizontalPos": return DebugCommandResult.Okay(ser.WriteWellHorizontalPosWait(Arg("well"), Arg("hor")));
  200. default: return DebugCommandResult.Fail("BAD_OP", $"未知 op: {op}");
  201. }
  202. }
  203. }
  204. }