ControlHttpServer.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. using System;
  2. using System.IO;
  3. using System.Net;
  4. using System.Text;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. using Newtonsoft.Json;
  8. using Newtonsoft.Json.Linq;
  9. using IvfTl.ControlHost.Debug;
  10. namespace IvfTl.ControlHost
  11. {
  12. /// <summary>
  13. /// control 进程内的本地 HTTP 小服务,只监听 127.0.0.1:port。
  14. /// 阶段1:/ping、/status。阶段2:/status 补全(rich) + /serial/pause|resume(借串口) + /shutdown(受护栏停止)。
  15. /// </summary>
  16. public class ControlHttpServer
  17. {
  18. private readonly int _port;
  19. private readonly Func<StatusDto> _pingProvider; // /ping 轻量存活
  20. private readonly Func<object> _statusProvider; // /status 完整快照(阶段2 §6 三块)
  21. private readonly Func<string, bool> _shutdownHandler; // /shutdown(token 校验后安全停机)
  22. private readonly Func<int, bool> _serialPauseHandler; // /serial/pause(借串口:control 让路该舱)
  23. private readonly Func<int, bool> _serialResumeHandler; // /serial/resume(归还:恢复采集)
  24. private readonly Action<string> _log;
  25. private readonly DebugSessionManager _debug;
  26. private readonly CalibrationManager _calib;
  27. private HttpListener _listener;
  28. private CancellationTokenSource _cts;
  29. public ControlHttpServer(
  30. int port,
  31. Func<StatusDto> pingProvider,
  32. Func<object> statusProvider,
  33. Func<string, bool> shutdownHandler,
  34. Func<int, bool> serialPauseHandler,
  35. Func<int, bool> serialResumeHandler,
  36. Action<string> log,
  37. DebugSessionManager debug = null,
  38. CalibrationManager calib = null)
  39. {
  40. _port = port;
  41. _pingProvider = pingProvider;
  42. _statusProvider = statusProvider;
  43. _shutdownHandler = shutdownHandler;
  44. _serialPauseHandler = serialPauseHandler;
  45. _serialResumeHandler = serialResumeHandler;
  46. _log = log ?? (_ => { });
  47. _debug = debug;
  48. _calib = calib;
  49. }
  50. public void Start()
  51. {
  52. _listener = new HttpListener();
  53. // 仅本机回环,拒绝外部访问(防外部调停机/借串口)。
  54. _listener.Prefixes.Add($"http://127.0.0.1:{_port}/");
  55. _listener.Start();
  56. _cts = new CancellationTokenSource();
  57. _log($"ControlHttpServer 监听 http://127.0.0.1:{_port}/");
  58. Task.Run(() => Loop(_cts.Token));
  59. }
  60. private async Task Loop(CancellationToken token)
  61. {
  62. while (!token.IsCancellationRequested)
  63. {
  64. HttpListenerContext ctx;
  65. try { ctx = await _listener.GetContextAsync(); }
  66. catch (Exception ex) { if (!token.IsCancellationRequested) _log("HttpListener 异常:" + ex.Message); break; }
  67. try { Handle(ctx); }
  68. catch (Exception ex) { _log("处理请求异常:" + ex.Message); }
  69. }
  70. }
  71. private void Handle(HttpListenerContext ctx)
  72. {
  73. string path = ctx.Request.Url.AbsolutePath.TrimEnd('/').ToLowerInvariant();
  74. string method = ctx.Request.HttpMethod.ToUpperInvariant();
  75. string body;
  76. int code = 200;
  77. switch (path)
  78. {
  79. case "/ping":
  80. body = JsonConvert.SerializeObject(_pingProvider());
  81. break;
  82. case "/status":
  83. body = JsonConvert.SerializeObject(_statusProvider());
  84. break;
  85. case "/shutdown":
  86. if (method != "POST") { code = 405; body = Err("method not allowed"); break; }
  87. {
  88. string token = ReadField(ctx, "token");
  89. bool ok = _shutdownHandler != null && _shutdownHandler(token ?? "");
  90. code = ok ? 200 : 403;
  91. body = "{\"ok\":" + (ok ? "true" : "false") + (ok ? "" : ",\"error\":\"token invalid\"") + "}";
  92. }
  93. break;
  94. case "/serial/pause":
  95. case "/serial/resume":
  96. if (method != "POST") { code = 405; body = Err("method not allowed"); break; }
  97. {
  98. int houseSn = ReadIntField(ctx, "houseSn");
  99. bool isPause = path == "/serial/pause";
  100. var handler = isPause ? _serialPauseHandler : _serialResumeHandler;
  101. bool ok = handler != null && houseSn > 0 && handler(houseSn);
  102. code = ok ? 200 : 400;
  103. body = "{\"ok\":" + (ok ? "true" : "false") + ",\"houseSn\":" + houseSn + (ok ? "" : ",\"error\":\"bad houseSn or handler\"") + "}";
  104. }
  105. break;
  106. case "/debug/acquire":
  107. if (method != "POST") { code = 405; body = Err("method not allowed"); break; }
  108. {
  109. int houseSn = ReadIntField(ctx, "houseSn");
  110. var r = _debug != null ? _debug.Acquire(houseSn) : DebugCommandResult.Fail("NO_HANDLE", "debug 未装配");
  111. code = r.Ok ? 200 : 409; body = JsonConvert.SerializeObject(r);
  112. }
  113. break;
  114. case "/debug/heartbeat":
  115. if (method != "POST") { code = 405; body = Err("method not allowed"); break; }
  116. {
  117. var r = _debug != null ? _debug.Heartbeat(ReadField(ctx, "sessionId")) : DebugCommandResult.Fail("SESSION_EXPIRED", "debug 未装配");
  118. code = r.Ok ? 200 : 410; body = JsonConvert.SerializeObject(r);
  119. }
  120. break;
  121. case "/debug/release":
  122. if (method != "POST") { code = 405; body = Err("method not allowed"); break; }
  123. {
  124. var r = _debug != null ? _debug.Release(ReadField(ctx, "sessionId")) : DebugCommandResult.Okay();
  125. code = 200; body = JsonConvert.SerializeObject(r);
  126. }
  127. break;
  128. case "/debug/command":
  129. if (method != "POST") { code = 405; body = Err("method not allowed"); break; }
  130. {
  131. var jo = ReadBody(ctx);
  132. string sid = jo?["sessionId"]?.ToString();
  133. string op = jo?["op"]?.ToString();
  134. var argsObj = jo?["args"] as Newtonsoft.Json.Linq.JObject;
  135. var r = _debug != null ? _debug.Execute(sid, op, argsObj) : DebugCommandResult.Fail("SESSION_EXPIRED", "debug 未装配");
  136. code = r.Ok ? 200 : (r.Code == "SESSION_EXPIRED" ? 410 : (r.Code == "OUT_OF_RANGE" ? 400 : 200));
  137. body = JsonConvert.SerializeObject(r);
  138. }
  139. break;
  140. case "/debug/calibrate/start":
  141. if (method != "POST") { code = 405; body = Err("method not allowed"); break; }
  142. {
  143. var jo = ReadBody(ctx);
  144. string sid = jo?["sessionId"]?.ToString();
  145. // wells 可空(默认 1..16);给了则解析为 int 列表(忽略非法项)。
  146. System.Collections.Generic.List<int> wells = null;
  147. if (jo?["wells"] is JArray arr)
  148. {
  149. wells = new System.Collections.Generic.List<int>();
  150. foreach (var t in arr) { if (int.TryParse(t?.ToString(), out int w)) wells.Add(w); }
  151. }
  152. var r = _calib != null ? _calib.StartCalibrate(sid, wells) : DebugCommandResult.Fail("SESSION_EXPIRED", "calib 未装配");
  153. code = r.Ok ? 200 : (r.Code == "SESSION_EXPIRED" ? 410 : 200);
  154. body = JsonConvert.SerializeObject(r);
  155. }
  156. break;
  157. case "/debug/calibrate/progress":
  158. if (method != "POST") { code = 405; body = Err("method not allowed"); break; }
  159. {
  160. string sid = ReadField(ctx, "sessionId");
  161. var r = _calib != null ? _calib.GetProgress(sid) : DebugCommandResult.Fail("SESSION_EXPIRED", "calib 未装配");
  162. code = r.Ok ? 200 : (r.Code == "SESSION_EXPIRED" ? 410 : 200);
  163. body = JsonConvert.SerializeObject(r);
  164. }
  165. break;
  166. case "/debug/calibrate/recalibrate":
  167. if (method != "POST") { code = 405; body = Err("method not allowed"); break; }
  168. {
  169. var jo = ReadBody(ctx);
  170. string sid = jo?["sessionId"]?.ToString();
  171. int wellSn = jo?["wellSn"] != null && int.TryParse(jo["wellSn"].ToString(), out int wv) ? wv : -1;
  172. var r = _calib != null ? _calib.Recalibrate(sid, wellSn) : DebugCommandResult.Fail("SESSION_EXPIRED", "calib 未装配");
  173. code = r.Ok ? 200 : (r.Code == "SESSION_EXPIRED" ? 410 : 200);
  174. body = JsonConvert.SerializeObject(r);
  175. }
  176. break;
  177. case "/debug/calibrate/stop":
  178. if (method != "POST") { code = 405; body = Err("method not allowed"); break; }
  179. {
  180. string sid = ReadField(ctx, "sessionId");
  181. var r = _calib != null ? _calib.Stop(sid) : DebugCommandResult.Okay();
  182. code = 200; body = JsonConvert.SerializeObject(r);
  183. }
  184. break;
  185. case "/debug/preview/stream":
  186. if (method != "GET") { code = 405; body = Err("method not allowed"); break; }
  187. {
  188. string sid = ctx.Request.QueryString["sessionId"];
  189. if (_debug == null || sid == null || !_debug.TryGet(sid, out var session))
  190. {
  191. code = 404; body = Err("session not found");
  192. break; // 走统一收尾返回 404
  193. }
  194. // 校验通过:分流起后台推流线程,不走统一收尾(那会 Close 流终止推流)。
  195. StartPreviewStream(ctx, session);
  196. return;
  197. }
  198. default:
  199. code = 404; body = Err("not found");
  200. break;
  201. }
  202. byte[] buf = Encoding.UTF8.GetBytes(body);
  203. ctx.Response.StatusCode = code;
  204. ctx.Response.ContentType = "application/json";
  205. ctx.Response.ContentLength64 = buf.Length;
  206. ctx.Response.OutputStream.Write(buf, 0, buf.Length);
  207. ctx.Response.OutputStream.Close();
  208. }
  209. private static string Err(string msg) => "{\"ok\":false,\"error\":\"" + msg + "\"}";
  210. /// <summary>读 POST JSON body 的某字符串字段(失败返回 null)。</summary>
  211. private string ReadField(HttpListenerContext ctx, string field)
  212. {
  213. try
  214. {
  215. using (var sr = new StreamReader(ctx.Request.InputStream, ctx.Request.ContentEncoding ?? Encoding.UTF8))
  216. {
  217. string raw = sr.ReadToEnd();
  218. if (string.IsNullOrEmpty(raw)) return null;
  219. var jo = JObject.Parse(raw);
  220. return jo[field]?.ToString();
  221. }
  222. }
  223. catch (Exception ex) { _log("解析请求体异常:" + ex.Message); return null; }
  224. }
  225. private int ReadIntField(HttpListenerContext ctx, string field)
  226. {
  227. string s = ReadField(ctx, field);
  228. return int.TryParse(s, out int v) ? v : -1;
  229. }
  230. /// <summary>把 POST body 整体解析为 JObject(失败返回 null)。/debug/command 多字段用。</summary>
  231. private Newtonsoft.Json.Linq.JObject ReadBody(HttpListenerContext ctx)
  232. {
  233. try
  234. {
  235. using (var sr = new StreamReader(ctx.Request.InputStream, ctx.Request.ContentEncoding ?? Encoding.UTF8))
  236. {
  237. string raw = sr.ReadToEnd();
  238. return string.IsNullOrEmpty(raw) ? null : Newtonsoft.Json.Linq.JObject.Parse(raw);
  239. }
  240. }
  241. catch (Exception ex) { _log("解析 body 异常:" + ex.Message); return null; }
  242. }
  243. /// <summary>
  244. /// 推流分支:起专用后台线程,抓帧→JPEG→multipart 持续写。
  245. /// HttpListener 工作线程立即返回(本方法起线程后即返回),不被推流阻塞。
  246. /// 任何退出路径都标记 session.StreamBroken,会话靠心跳 TTL 看门狗最终回收(spec §7)。
  247. /// </summary>
  248. private void StartPreviewStream(HttpListenerContext ctx, IvfTl.ControlHost.Debug.DebugSession session)
  249. {
  250. var resp = ctx.Response;
  251. resp.StatusCode = 200;
  252. resp.ContentType = IvfTl.ControlHost.Debug.MjpegStreamWriter.ContentType;
  253. resp.SendChunked = true; // 流式,长度未知
  254. resp.Headers.Add("Cache-Control", "no-cache");
  255. var t = new Thread(() =>
  256. {
  257. int errCount = 0;
  258. var cam = session.Lease?.Camera;
  259. try
  260. {
  261. if (cam == null) { _log($"[debug] 推流舱{session.HouseSn} 无相机句柄,放弃"); return; }
  262. cam.SetOpMode(1); // 实时模式(0=单帧/1=实时,见 ICamera 注释)
  263. var outStream = resp.OutputStream;
  264. while (true)
  265. {
  266. // 会话已被回收(release/超时)→ 停推流
  267. if (!_debug.TryGet(session.SessionId, out _)) { _log($"[debug] 推流舱{session.HouseSn} 会话已失效,停"); break; }
  268. try
  269. {
  270. byte[] bgr = cam.GrabStable(); // 走全进程相机锁,与采集/对焦串行
  271. if (bgr == null) { Thread.Sleep(100); continue; }
  272. byte[] jpeg = IvfTl.ControlHost.Debug.MjpegStreamWriter.EncodeJpeg(bgr, cam.Width, cam.Height);
  273. if (jpeg == null) { Thread.Sleep(100); continue; }
  274. byte[] frame = IvfTl.ControlHost.Debug.MjpegStreamWriter.FrameBytes(jpeg);
  275. outStream.Write(frame, 0, frame.Length);
  276. outStream.Flush();
  277. errCount = 0;
  278. Thread.Sleep(66); // ~15fps(spec §4.2)
  279. }
  280. catch (IOException) { _log($"[debug] 推流舱{session.HouseSn} 客户端断开"); break; } // operate 关预览/崩溃:正常退出
  281. catch (HttpListenerException) { _log($"[debug] 推流舱{session.HouseSn} 连接断开"); break; }
  282. catch (Exception ex)
  283. {
  284. errCount++;
  285. _log($"[debug] 推流舱{session.HouseSn} 抓帧/编码异常({errCount}/5): {ex.Message}");
  286. if (errCount >= 5) { _log($"[debug] 推流舱{session.HouseSn} 连续错误过多,停"); break; }
  287. Thread.Sleep(500);
  288. }
  289. }
  290. }
  291. catch (Exception ex) { _log($"[debug] 推流舱{session.HouseSn} 线程异常: {ex.Message}"); }
  292. finally
  293. {
  294. session.StreamBroken = true; // 可回收快信号;会话最终由心跳 TTL 看门狗收(不在此 Dispose,避免与命令分发/超时回收争 lease)
  295. try { resp.OutputStream.Close(); } catch { }
  296. try { resp.Close(); } catch { }
  297. _log($"[debug] 推流舱{session.HouseSn} 线程结束");
  298. }
  299. });
  300. t.IsBackground = true;
  301. t.Name = $"MjpegStream-h{session.HouseSn}";
  302. t.Start();
  303. }
  304. public void Stop()
  305. {
  306. try { _cts?.Cancel(); _listener?.Stop(); _listener?.Close(); }
  307. catch (Exception ex) { _log("ControlHttpServer 停止异常:" + ex.Message); }
  308. }
  309. }
  310. }