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 ConcurrentDictionary _sessions = new ConcurrentDictionary(); public DebugSessionManager(Func gateOf, Func clock, int ttlMs, Action log) { _gateOf = gateOf; _clock = clock; _ttlMs = ttlMs; _log = log ?? (_ => { }); } 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()); _log($"[debug] acquire 舱{houseSn} sid={sid}"); return DebugCommandResult.Okay(sid); } 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", "会话不存在或已过期"); } public DebugCommandResult Release(string sid) { if (sid != null && _sessions.TryRemove(sid, out var s)) { try { s.Lease.Dispose(); } catch { } _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)) { try { s.Lease.Dispose(); } catch { } _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) { 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()); 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) => DebugCommandResult.Fail("BAD_OP", $"未知 op: {op}"); } }