DebugSessionManager.cs 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  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 ConcurrentDictionary<string, DebugSession> _sessions = new ConcurrentDictionary<string, DebugSession>();
  18. public DebugSessionManager(Func<int, IHouseGate> gateOf, Func<DateTime> clock, int ttlMs, Action<string> log)
  19. {
  20. _gateOf = gateOf; _clock = clock; _ttlMs = ttlMs; _log = log ?? (_ => { });
  21. }
  22. public DebugCommandResult Acquire(int houseSn)
  23. {
  24. var gate = _gateOf(houseSn);
  25. if (gate == null) return DebugCommandResult.Fail("NO_HANDLE", $"舱{houseSn}无闸门");
  26. var lease = gate.Acquire(HardwareUser.OperateDebug);
  27. if (lease == null) return DebugCommandResult.Fail("BUSY", $"舱{houseSn}被占用,借用超时");
  28. string sid = Guid.NewGuid().ToString("N");
  29. _sessions[sid] = new DebugSession(sid, houseSn, lease, _clock());
  30. _log($"[debug] acquire 舱{houseSn} sid={sid}");
  31. return DebugCommandResult.Okay(sid);
  32. }
  33. public DebugCommandResult Heartbeat(string sid)
  34. {
  35. if (sid != null && _sessions.TryGetValue(sid, out var s)) { s.LastSeen = _clock(); return DebugCommandResult.Okay(); }
  36. return DebugCommandResult.Fail("SESSION_EXPIRED", "会话不存在或已过期");
  37. }
  38. public DebugCommandResult Release(string sid)
  39. {
  40. if (sid != null && _sessions.TryRemove(sid, out var s))
  41. {
  42. try { s.Lease.Dispose(); } catch (Exception ex) { _log($"[debug] dispose 异常 sid={sid} 舱{s.HouseSn}: {ex.Message}"); }
  43. _log($"[debug] release sid={sid} 舱{s.HouseSn}");
  44. }
  45. return DebugCommandResult.Okay();
  46. }
  47. /// <summary>超时回收:LastSeen + ttl < now 的会话自动归还(spec §5.1)。</summary>
  48. public int SweepExpired()
  49. {
  50. int n = 0; var now = _clock();
  51. foreach (var kv in _sessions)
  52. {
  53. if ((now - kv.Value.LastSeen).TotalMilliseconds > _ttlMs)
  54. {
  55. if (_sessions.TryRemove(kv.Key, out var s))
  56. {
  57. try { s.Lease.Dispose(); } catch (Exception ex) { _log($"[debug] dispose 异常 sid={kv.Key} 舱{s.HouseSn}: {ex.Message}"); }
  58. _log($"[debug] 会话超时自动回收 sid={kv.Key} 舱{s.HouseSn}");
  59. n++;
  60. }
  61. }
  62. }
  63. return n;
  64. }
  65. public DebugCommandResult Execute(string sid, string op, JObject args)
  66. {
  67. if (sid == null || !_sessions.TryGetValue(sid, out var s))
  68. return DebugCommandResult.Fail("SESSION_EXPIRED", "会话不存在或已过期");
  69. s.LastSeen = _clock();
  70. var ser = s.Lease.Serial;
  71. if (ser == null) return DebugCommandResult.Fail("NO_HANDLE", "借用串口句柄为空");
  72. try
  73. {
  74. switch (op)
  75. {
  76. case "ReadTemp": return DebugCommandResult.Okay(ser.TemperatureWait());
  77. case "ReadPressure": return DebugCommandResult.Okay(ser.PressureWait());
  78. case "ReadDoor": return DebugCommandResult.Okay(ser.DoorStatusWait().ToString());
  79. case "ReadVentTime": return DebugCommandResult.Okay(ser.ReadOpenVentTimeWait());
  80. case "ShakeHands": return DebugCommandResult.Okay(ser.ShakeHandsWait());
  81. case "OpenLed": return DebugCommandResult.Okay(ser.OpenLedWait());
  82. case "CloseLed": return DebugCommandResult.Okay(ser.CloseLedWait());
  83. case "OpenIntake": return DebugCommandResult.Okay(ser.OpenIntakeValveWait());
  84. case "CloseIntake": return DebugCommandResult.Okay(ser.CloseIntakeValveWait());
  85. case "OpenExhaust": return DebugCommandResult.Okay(ser.OpenExhaustValveWait());
  86. case "CloseExhaust": return DebugCommandResult.Okay(ser.CloseExhaustValveWait());
  87. case "HouseAeration": return DebugCommandResult.Okay(ser.HouseAerationWait());
  88. case "HouseVent": return DebugCommandResult.Okay(ser.HouseVentWait());
  89. default:
  90. return ExecuteMotorOrEeprom(s, ser, op, args);
  91. }
  92. }
  93. catch (Exception ex) { return DebugCommandResult.Fail("HARDWARE_ERROR", ex.Message); }
  94. }
  95. // Task7 先占位:返回 BAD_OP,让"未知 op"测试通过;Task7 替换为真实电机/EEPROM 分发。
  96. private DebugCommandResult ExecuteMotorOrEeprom(DebugSession s, IvfTl.Hardware.ISerialChannel ser, string op, JObject args)
  97. {
  98. int Arg(string k, int def = 0) => args?[k] != null ? args[k].Value<int>() : def;
  99. int delay = Arg("motorDelay", -1);
  100. switch (op)
  101. {
  102. case "VerticalReset": { bool ok = ser.VerticalResetWait(delay); if (ok) s.CurrentVer = 0; return DebugCommandResult.Okay(ok); }
  103. case "VerticalMoveTo":
  104. {
  105. int pos = Arg("pos");
  106. if (!MotorClamp.IsVerticalInRange(pos)) return DebugCommandResult.Fail("OUT_OF_RANGE", $"垂直目标{pos}越界[0,{MotorClamp.VerMax}]");
  107. bool ok = ser.VerticalMoveToWait(pos, delay); if (ok) s.CurrentVer = pos; return DebugCommandResult.Okay(ok);
  108. }
  109. case "VerticalForward":
  110. case "VerticalBackward":
  111. {
  112. // 红线钳位必须基于回读的真实物理位,不信任会话跟踪位(防真机已在高位时相对运动越红线)。
  113. int basePos = ser.ReadVerticalPositionWait();
  114. int delta = Arg("value") * (op == "VerticalBackward" ? -1 : 1);
  115. int target = MotorClamp.RelativeTarget(basePos, delta);
  116. if (!MotorClamp.IsVerticalInRange(target)) return DebugCommandResult.Fail("OUT_OF_RANGE", $"垂直目标{target}越界");
  117. bool ok = op == "VerticalBackward" ? ser.VerticalBackwardWait(Arg("value"), delay) : ser.VerticalForwardWait(Arg("value"), delay);
  118. if (ok) s.CurrentVer = ser.ReadVerticalPositionWait(); return DebugCommandResult.Okay(ok);
  119. }
  120. case "HorizontalReset": { bool ok = ser.HorizontalResetWait(delay); if (ok) s.CurrentHor = 0; return DebugCommandResult.Okay(ok); }
  121. case "HorizontalMoveTo":
  122. {
  123. int pos = Arg("pos");
  124. if (!MotorClamp.IsHorizontalInRange(pos)) return DebugCommandResult.Fail("OUT_OF_RANGE", $"水平目标{pos}越界[0,{MotorClamp.HorMax}]");
  125. bool ok = ser.HorizontalMoveToWait(pos, delay); if (ok) s.CurrentHor = pos; return DebugCommandResult.Okay(ok);
  126. }
  127. case "HorizontalForward":
  128. case "HorizontalBackward":
  129. {
  130. // 红线钳位必须基于回读的真实物理位,不信任会话跟踪位。
  131. int basePos = ser.ReadHorizontalPositionWait();
  132. int delta = Arg("value") * (op == "HorizontalBackward" ? -1 : 1);
  133. int target = MotorClamp.RelativeTarget(basePos, delta);
  134. if (!MotorClamp.IsHorizontalInRange(target)) return DebugCommandResult.Fail("OUT_OF_RANGE", $"水平目标{target}越界");
  135. bool ok = op == "HorizontalBackward" ? ser.HorizontalBackwardWait(Arg("value"), delay) : ser.HorizontalForwardWait(Arg("value"), delay);
  136. if (ok) s.CurrentHor = ser.ReadHorizontalPositionWait(); return DebugCommandResult.Okay(ok);
  137. }
  138. case "WriteScanStep": return DebugCommandResult.Okay(ser.WriteScanStepWait(Arg("value")));
  139. case "WriteOpenIntakeTime": return DebugCommandResult.Okay(ser.WriteOpenIntakeTimeWait(Arg("value")));
  140. case "WriteOpenVentTime": return DebugCommandResult.Okay(ser.WriteOpenVentTimeWait(Arg("value")));
  141. case "WriteWellHorizontalPos": return DebugCommandResult.Okay(ser.WriteWellHorizontalPosWait(Arg("well"), Arg("hor")));
  142. default: return DebugCommandResult.Fail("BAD_OP", $"未知 op: {op}");
  143. }
  144. }
  145. }
  146. }