| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>时差培养箱 · 系统改造需求 + 业务流程总文档(开发上手必读)</title>
- <style>
- :root{
- --c-control:#2563eb; --c-control-bg:#eff6ff;
- --c-operate:#16a34a; --c-operate-bg:#f0fdf4;
- --c-server:#7c3aed; --c-server-bg:#f5f3ff;
- --c-hw:#0891b2; --c-hw-bg:#ecfeff;
- --c-db:#64748b; --c-db-bg:#f8fafc;
- --c-down:#dc2626; --c-down-bg:#fef2f2;
- --c-new:#ea580c; --c-new-bg:#fff7ed;
- --ink:#1e293b; --muted:#64748b; --line:#e2e8f0; --bg:#eef2f6;
- }
- *{box-sizing:border-box;margin:0;padding:0}
- body{font-family:"Microsoft YaHei","Segoe UI",system-ui,sans-serif;color:var(--ink);background:var(--bg);line-height:1.65;font-size:14px}
- a{color:inherit;text-decoration:none}
- .layout{display:flex;align-items:flex-start}
- nav.toc{position:sticky;top:0;width:268px;height:100vh;overflow-y:auto;background:#0f172a;color:#cbd5e1;padding:16px 13px;flex-shrink:0}
- nav.toc h2{font-size:14px;color:#fff;margin-bottom:8px;padding-bottom:7px;border-bottom:1px solid #334155}
- nav.toc .part{font-size:11px;color:#64748b;text-transform:uppercase;letter-spacing:.5px;margin:13px 0 3px;padding-left:6px;font-weight:700}
- nav.toc a{display:block;padding:4px 8px;border-radius:6px;font-size:12.5px;color:#94a3b8;margin:1px 0}
- nav.toc a:hover{background:#1e293b;color:#fff}
- nav.toc a.sub{padding-left:20px;font-size:11.5px;color:#7c8aa0}
- nav.toc .tag{font-size:9.5px;padding:1px 5px;border-radius:4px;margin-left:3px;color:#fff}
- main{flex:1;max-width:1080px;margin:0 auto;padding:24px 32px 90px}
- header.hero{background:linear-gradient(120deg,#1e3a8a,#0f766e);color:#fff;border-radius:14px;padding:28px 32px;margin-bottom:14px;box-shadow:0 8px 24px rgba(15,23,42,.18)}
- header.hero h1{font-size:24px;margin-bottom:7px}
- header.hero p{opacity:.93;font-size:13px;max-width:820px}
- .legend{display:flex;flex-wrap:wrap;gap:8px;margin-top:15px}
- .legend span{font-size:12px;padding:3px 10px;border-radius:20px;background:rgba(255,255,255,.16);color:#fff}
- .legend .dot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:5px;vertical-align:middle}
- .readme{background:#fff;border:1px solid var(--line);border-left:5px solid #0f766e;border-radius:10px;padding:14px 18px;margin-bottom:20px;font-size:13px}
- .readme b{color:#0f766e}
- .part-banner{font-size:13px;font-weight:700;color:#fff;background:#334155;border-radius:8px;padding:7px 16px;margin:26px 0 12px;letter-spacing:.5px}
- .part-banner.a{background:linear-gradient(90deg,#7c2d12,#9a3412)}
- .part-banner.b{background:linear-gradient(90deg,#1e3a8a,#1d4ed8)}
- .part-banner.c{background:linear-gradient(90deg,#065f46,#047857)}
- .part-banner.d{background:linear-gradient(90deg,#7f1d1d,#b91c1c)}
- .part-banner.e{background:linear-gradient(90deg,#4338ca,#6d28d9)}
- section{background:#fff;border-radius:12px;padding:20px 26px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.06);border:1px solid var(--line)}
- section>h2{font-size:18.5px;margin-bottom:3px;display:flex;align-items:center;gap:9px;cursor:pointer}
- section>h2 .num{background:#0f172a;color:#fff;border-radius:8px;min-width:30px;height:30px;display:inline-flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0;padding:0 4px}
- section>h2 .chev{margin-left:auto;color:var(--muted);font-size:14px;transition:.2s}
- section.collapsed>h2 .chev{transform:rotate(-90deg)}
- section.collapsed>.body{display:none}
- .lead{color:var(--muted);font-size:13px;margin:6px 0 14px;padding-left:39px}
- h3{font-size:15.5px;margin:20px 0 9px;padding-left:11px;border-left:4px solid var(--c-control);color:#0f172a}
- h4{font-size:13.5px;margin:14px 0 6px;color:#334155}
- p{margin:7px 0}
- p.note{font-size:12.5px;color:var(--muted)}
- code{background:#f1f5f9;padding:1px 6px;border-radius:5px;font-family:Consolas,monospace;font-size:12px;color:#0f766e}
- .fl{color:#94a3b8;font-family:Consolas,monospace;font-size:11px}
- .flow{margin:13px 0}
- .step{position:relative;padding:10px 14px 10px 46px;margin:0 0 0 16px;border-left:2px dashed #cbd5e1}
- .step:last-child{border-left:2px solid transparent}
- .step .idx{position:absolute;left:-15px;top:9px;width:30px;height:30px;border-radius:50%;background:var(--c-control);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:13px;box-shadow:0 0 0 4px #fff}
- .step .box{background:var(--c-control-bg);border:1px solid #bfdbfe;border-radius:9px;padding:9px 13px}
- .step .box b{color:#1e3a8a}
- .step.op .idx{background:var(--c-operate)} .step.op .box{background:var(--c-operate-bg);border-color:#bbf7d0} .step.op .box b{color:#14532d}
- .step.sv .idx{background:var(--c-server)} .step.sv .box{background:var(--c-server-bg);border-color:#ddd6fe} .step.sv .box b{color:#5b21b6}
- .step.hw .idx{background:var(--c-hw)} .step.hw .box{background:var(--c-hw-bg);border-color:#a5f3fc} .step.hw .box b{color:#155e75}
- .step.dn .idx{background:var(--c-down)} .step.dn .box{background:var(--c-down-bg);border-color:#fecaca} .step.dn .box b{color:#991b1b}
- .step small{display:block;color:var(--muted);font-size:11.5px;margin-top:3px}
- .chain{display:flex;flex-wrap:wrap;align-items:stretch;gap:0;margin:12px 0}
- .chain .node{background:var(--c-control-bg);border:1px solid #bfdbfe;border-radius:8px;padding:8px 11px;font-size:12px;min-width:78px;display:flex;flex-direction:column;justify-content:center}
- .chain .node b{font-size:12.5px}
- .chain .node.op{background:var(--c-operate-bg);border-color:#bbf7d0}
- .chain .node.sv{background:var(--c-server-bg);border-color:#ddd6fe}
- .chain .node.hw{background:var(--c-hw-bg);border-color:#a5f3fc}
- .chain .node.db{background:var(--c-db-bg);border-color:#cbd5e1}
- .chain .arrow{display:flex;align-items:center;color:#94a3b8;font-size:18px;padding:0 6px}
- table{width:100%;border-collapse:collapse;margin:10px 0;font-size:12.5px}
- th,td{border:1px solid var(--line);padding:6px 9px;text-align:left;vertical-align:top}
- th{background:#f8fafc;color:#334155;font-weight:600}
- tr:nth-child(even) td{background:#fcfdfe}
- td.c{white-space:nowrap}
- .b{display:inline-block;font-size:10.5px;padding:1px 7px;border-radius:5px;color:#fff;font-weight:600;white-space:nowrap}
- .b.control{background:var(--c-control)} .b.operate{background:var(--c-operate)}
- .b.server{background:var(--c-server)} .b.hw{background:var(--c-hw)}
- .b.db{background:var(--c-db)} .b.down{background:var(--c-down)} .b.new{background:var(--c-new)}
- .b.o{background:#fff;border:1px solid currentColor}
- .callout{border-radius:9px;padding:12px 15px;margin:13px 0;font-size:12.8px}
- .callout.warn{background:var(--c-down-bg);border:1px solid #fecaca;color:#7f1d1d}
- .callout.info{background:#eff6ff;border:1px solid #bfdbfe;color:#1e3a8a}
- .callout.new{background:var(--c-new-bg);border:1px solid #fed7aa;color:#9a3412}
- .callout.ok{background:#f0fdf4;border:1px solid #bbf7d0;color:#14532d}
- .grid2{display:grid;grid-template-columns:1fr 1fr;gap:14px}
- .grid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px}
- @media(max-width:980px){.grid2,.grid3{grid-template-columns:1fr}nav.toc{display:none}}
- .vs{display:grid;grid-template-columns:1fr auto 1fr;gap:0;align-items:stretch;margin:14px 0}
- .vs .col{border-radius:10px;padding:13px 16px}
- .vs .before{background:var(--c-down-bg);border:1px solid #fecaca}
- .vs .after{background:var(--c-operate-bg);border:1px solid #bbf7d0}
- .vs .mid{display:flex;align-items:center;color:#94a3b8;font-size:24px;padding:0 12px}
- .vs h4{margin-top:0}
- .vs ul{margin:6px 0 0 16px;font-size:12.3px}
- .vs li{margin:4px 0}
- .pill-row{display:flex;flex-wrap:wrap;gap:6px;margin:8px 0}
- .pill{font-size:11.5px;background:#f1f5f9;border:1px solid var(--line);border-radius:16px;padding:3px 10px}
- .kicker{font-size:11px;font-weight:700;letter-spacing:.5px;color:var(--muted);text-transform:uppercase}
- ol.clean{margin:6px 0 6px 20px}ol.clean li{margin:5px 0}
- ul.clean{margin:6px 0 6px 18px}ul.clean li{margin:4px 0}
- hr.sep{border:none;border-top:1px dashed var(--line);margin:14px 0}
- .tasknum{display:inline-flex;width:22px;height:22px;border-radius:50%;background:#1d4ed8;color:#fff;font-size:11px;align-items:center;justify-content:center;margin-right:6px;font-weight:700}
- </style>
- </head>
- <body>
- <div class="layout">
- <nav class="toc">
- <h2>📖 总文档目录</h2>
- <div class="part">A · 为什么改造</div>
- <a href="#what">0 · 项目是什么</a>
- <a href="#bg">1 · 改造背景与三大问题</a>
- <a href="#goal">2 · 目标·范围·关键决策</a>
- <div class="part">B · 改成什么样</div>
- <a href="#arch">3 · 目标架构(双进程)</a>
- <a href="#http-contract" class="sub">3.1 本地HTTP接口契约</a>
- <a href="#lifecycle" class="sub">3.2 生命周期管理</a>
- <div class="part">C · 现在是什么样(现状流程)</div>
- <a href="#overview">4 · 当前系统拓扑</a>
- <a href="#boot">5 · 启动与登录</a>
- <a href="#loop">6 · 采集主循环 <span class="tag" style="background:#2563eb">control</span></a>
- <a href="#photo">7 · 拍照 / 对焦四步</a>
- <a href="#air">8 · 换气 / 缓冲瓶排队</a>
- <a href="#server">9 · 服务器双向交互</a>
- <a href="#operate">10 · operate 模块逐功能 <span class="tag" style="background:#16a34a">operate</span></a>
- <a href="#hal">11 · 硬件层 / 借串口</a>
- <a href="#data">12 · 数据库与数据流</a>
- <div class="part">D · 接下来改什么</div>
- <a href="#degrade">13 · ★合并降级登记 <span class="tag" style="background:#dc2626">必读</span></a>
- <a href="#roadmap">14 · 三阶段路线图</a>
- <a href="#stage1" class="sub">14.1 阶段1·七个任务</a>
- <a href="#split">15 · 双进程8个改造点</a>
- <div class="part">E · 怎么上手</div>
- <a href="#env">16 · 技术栈 / 环境 / 构建</a>
- <a href="#pitfall">17 · 已知坑</a>
- <a href="#howto">18 · 开工方式</a>
- </nav>
- <main>
- <header class="hero">
- <h1>🧬 时差培养箱 · 系统改造需求 + 业务流程总文档</h1>
- <p>给<b>刚接手、不熟悉代码与需求</b>的开发:读这一篇即可掌握「为什么改造 → 改成什么样 → 现在是什么样 → 接下来改什么 → 怎么上手」。流程细化到每个功能,关键处标 <code>file:line</code>(2026-06-22 源码复核)。</p>
- <div class="legend">
- <span><i class="dot" style="background:#2563eb"></i>control 机器驱动</span>
- <span><i class="dot" style="background:#16a34a"></i>operate 操作界面</span>
- <span><i class="dot" style="background:#7c3aed"></i>服务器/中央</span>
- <span><i class="dot" style="background:#0891b2"></i>硬件/下位机</span>
- <span><i class="dot" style="background:#64748b"></i>数据库</span>
- <span><i class="dot" style="background:#dc2626"></i>合并降级</span>
- <span><i class="dot" style="background:#ea580c"></i>合并新增</span>
- </div>
- </header>
- <div class="readme">
- <b>怎么读这份文档:</b> 全文分 5 部分 —— <b>A 为什么改造</b>(背景,先建立认知)→ <b>B 改成什么样</b>(目标架构,你要交付的)→ <b>C 现在是什么样</b>(现状全量业务流程,你要在它上面动手)→ <b>D 接下来改什么</b>(降级坑 + 三阶段路线图 + 你从哪开始)→ <b>E 怎么上手</b>(环境/构建/已知坑)。每节标题可点击折叠。配色见上方图例。<br>
- <b>权威基线文档(本页是它们的汇总入口):</b> <code>需求文档/control-逻辑与配置全景.md</code>、<code>需求文档/操作端逻辑与配置全景.md</code>、<code>需求文档/specs/...双进程拆分-design.md</code>、<code>开发计划/2026-06-22-阶段1-control独立进程骨架.md</code>。改代码前查对应全景。
- </div>
- <!-- ============================ PART A ============================ -->
- <div class="part-banner a">A · 为什么改造(背景认知)</div>
- <section id="what">
- <h2><span class="num">0</span> 这是什么项目 <span class="chev">▼</span></h2>
- <div class="body">
- <p class="lead"><b>时差培养箱(IVF 胚胎培养)软件系统</b>:培养箱 7×24 自动培养胚胎,定时拍照记录胚胎发育延时影像,数据上传服务器供医生查看打分。</p>
- <table>
- <tr><th>程序</th><th>目录</th><th>角色</th><th>跑在哪</th></tr>
- <tr><td><span class="b operate">operate</span></td><td><code>ivf_tl_operate_2.0/</code></td><td>培养箱<b>本机操作界面</b>(舱室/调试/对焦/配置/监控)</td><td>培养箱机器</td></tr>
- <tr><td><span class="b control">control</span></td><td><code>ivf_tl_operate_2.0/control/</code><br>(现为类库,被 operate 进程内托管)</td><td><b>机器驱动大脑</b>:串口控下位机、拍照、换气补气、自动对焦、图片上传</td><td>培养箱机器</td></tr>
- <tr><td><span class="b" style="background:#0d9488">front</span></td><td><code>aivfo-front-manament-2.0/</code></td><td>管理端/医生工作站:患者管理、胚胎打分、出报告</td><td>医生办公电脑</td></tr>
- <tr><td><span class="b server">Java 微服务</span></td><td>gateway / tl-control / business-manage / data-transmission / oplog 等</td><td>网关/业务/数据传输/操作日志</td><td>服务器(开发时 108+本机)</td></tr>
- </table>
- <div class="callout info"><b>本次改造只涉及 operate 与 control(都在培养箱本机)。front 与 Java 后端完全不动。</b> 技术栈:C# / .NET 6 / WPF / MVVM。</div>
- </div>
- </section>
- <section id="bg">
- <h2><span class="num">1</span> 改造背景与三大问题 <span class="chev">▼</span></h2>
- <div class="body">
- <p class="lead">理解改造,先理解"合并的真相"。</p>
- <h4>1.1 合并的真相</h4>
- <p>operate(本机操作界面)与 control(机器驱动)<b>原本是两个独立进程、独立软件</b>,各自连服务器,本地互不通信。之前做过一次"三项目合并",<b>只为代码仓库/管理方便</b>,把 control 逻辑塞进了 operate 进程内运行(operate <code>MainWindow_Loaded → Task.Run → StartMain.StartRun()</code>)。</p>
- <div class="callout info">📌 <b>"三项目"= operate + control + AutoFocusTool</b>(对焦工具,重构进 <code>control/IvfTl.AutoFocus</code> / <code>IvfTl.Hardware</code>,核心算法逐字保留,见 §7.1)三者合并进 operate;<b>front(医生端)从未被合并</b>,是独立程序,本次也完全不动。原始三项目代码仍在 <code>临时文件/</code> 下(ivf_tl_operate / ivf_tl_control / AutoFocusTool)。</div>
- <div class="callout warn">⚠ 这次合并把<b>两个生命周期相反</b>的东西塞进了一个进程:control 要 7×24 常驻不能停,operate 只是 UI 想关就关。由此引出下面三个必须解决的问题。</div>
- <h4>1.2 合并引入的三个问题(本次改造要解决)</h4>
- <div class="grid3">
- <div class="callout warn" style="margin:0"><b>① 生命周期冲突</b><br>operate 关 UI 后进程退不掉(control 的 LongRunning 前台线程撑着)→ <b>残留无窗口进程</b>(如曾出现 PID 20268);重开 operate 又起一套 control → <b>多实例抢串口</b>。</div>
- <div class="callout warn" style="margin:0"><b>② 监控功能没写全</b><br>operate"服务监控"页本应跨进程看 control 运行状态,合并时只做了"同进程直读内存"(<code>GetMonitorSnapshot</code>),内容也不全。</div>
- <div class="callout warn" style="margin:0"><b>③ 调试串口占用</b><br>control 常驻占着串口,operate 进调试页/对焦页要用串口时与 control 冲突(现有 HouseGate 闸门只是<b>同进程内</b>协调)。</div>
- </div>
- <h4>1.3 改造前 vs 改造后</h4>
- <div class="vs">
- <div class="col before">
- <h4>🔴 现状(合并态·单进程)</h4>
- <ul>
- <li>operate.exe 一个进程,内部 Task.Run 跑 control</li>
- <li>operate 关闭 → control 跟着死 / 或残留退不掉</li>
- <li>重开 operate → 又起一套 control 抢串口</li>
- <li>监控页同进程直读,内容不全</li>
- <li>调试借串口靠同进程内 HouseGate 闸门</li>
- <li>退出语义缺失(App_Exit 空、去掉了 Environment.Exit)</li>
- </ul>
- </div>
- <div class="mid">➡</div>
- <div class="col after">
- <h4>🟢 目标(双进程)</h4>
- <ul>
- <li>control.exe 独立常驻 + operate.exe 可随时开关</li>
- <li><b>operate 关闭 → control 继续驱动机器</b>(采集/换气/拍照/上传不断)</li>
- <li>control 用 Mutex 单实例,永不多开抢串口</li>
- <li>监控页跨进程读 control 的 /status,内容补全</li>
- <li>调试经本地 HTTP 跨进程借/还串口,control 让路调完恢复</li>
- <li><b>用户/装机仍是一个软件</b>:只装只启 operate,control 由它自动拉起</li>
- </ul>
- </div>
- </div>
- <div class="callout warn">⚠ <b>本次是接一个"半成品"</b>:上一个"三项目合并"任务<b>代码完成,但真机验收整体未做、且有 operate 侧功能降级遗留</b>(详见 §13 合并降级登记 / <code>待验证清单.md</code> M-01~M-07)。双进程拆分在其基础上推进。</div>
- </div>
- </section>
- <section id="goal">
- <h2><span class="num">2</span> 目标 · 范围 · 关键决策 <span class="chev">▼</span></h2>
- <div class="body">
- <div class="grid2">
- <div>
- <h4>🎯 目标</h4>
- <ul class="clean">
- <li>运行时回归两个独立进程:control.exe 常驻驱动机器,operate.exe 可随时开关。</li>
- <li>用户/装机仍是一个软件:只装、只启动 operate;control 由 operate 自动管理。</li>
- <li>operate 关闭 → control 继续驱动机器、采集、上传。</li>
- <li>补全监控页(跨进程读 control 真实状态)。</li>
- <li>解决调试串口占用(跨进程借用/归还)。</li>
- </ul>
- </div>
- <div>
- <h4>🚧 范围红线</h4>
- <div class="callout warn" style="margin-top:6px"><b>只动 operate / control,front 完全不动。</b> control 的采集/换气/对焦/上传<b>业务逻辑零改动</b>,只动"进程边界 + 进程间本地通信"(降低回归风险)。</div>
- </div>
- </div>
- <h4>🔑 已确认的关键决策(已与用户逐项确认,勿推翻重议)</h4>
- <table>
- <tr><th>#</th><th>决策</th><th>选定方案</th></tr>
- <tr><td>1</td><td>进程模型</td><td>operate / control <b>两个独立进程</b>;代码仍一个解决方案管理,装机一个软件;front 不动</td></tr>
- <tr><td>2</td><td>进程间通信</td><td><b>control 开本地 HTTP 小服务</b>(<code>127.0.0.1:38080</code>,.NET 自带 HttpListener),operate 调</td></tr>
- <tr><td>3</td><td>谁拉起 control</td><td><b>operate 按需拉起</b>(登录后探活,不在则 Process.Start)+ operate 开机自启 + control <b>Mutex 单实例</b></td></tr>
- <tr><td>4</td><td>调试让串口</td><td>operate 调 <code>/serial/pause</code>+<code>/resume</code>,复用现有 HouseGate 闸门(改跨进程);control 不死、调完恢复</td></tr>
- <tr><td>5</td><td>整体停止 control</td><td>监控页<b>受护栏按钮</b>(二次确认 + 工程师口令 <code>tl13579</code>)→ <code>/shutdown</code> 安全停机</td></tr>
- <tr><td>6</td><td>监控页补全</td><td>补:各舱实时活动、后台线程健康/心跳、串口借用/占用状态</td></tr>
- <tr><td>7</td><td>实现节奏</td><td>分三阶段:①独立进程骨架 ②监控/借串口/停止 ③清理老壳+装机</td></tr>
- </table>
- </div>
- </section>
- <!-- ============================ PART B ============================ -->
- <div class="part-banner b">B · 改成什么样(目标架构 = 你要交付的)</div>
- <section id="arch">
- <h2><span class="num">3</span> 目标架构(双进程) <span class="chev">▼</span></h2>
- <div class="body">
- <p class="lead">两进程业务上仍各自连服务器(维持现状);<b>本地新增的唯一通道是 control 的 HTTP 小服务</b>,只承载"读状态/借串口/停止"三类本地协调。</p>
- <div class="chain" style="flex-direction:column;align-items:stretch;gap:10px">
- <div class="node op" style="border-width:2px"><b>operate.exe(UI,可随时开关,管理员)</b><small>本机操作界面 · 服务监控页轮询 control 的 /status · 启动时确保 control 在跑(不在则拉起)</small></div>
- <div style="text-align:center;color:#94a3b8;font-size:13px">▼ 本地 HTTP(127.0.0.1:38080) GET /status /ping POST /serial/pause|resume /shutdown ▼</div>
- <div class="node" style="border-width:2px"><b>control.exe(无界面常驻,管理员,Mutex 单实例)</b><small>StartMain.StartRun():串口/相机/采集/对焦/换气 · 10×HouseBin 采集循环 + BufferBottleBin + 上报线程 · 内嵌轻量 HttpListener</small></div>
- <div style="text-align:center;color:#94a3b8;font-size:13px">▼ control 连服务器(MQTT指令/Kafka图片/HTTP) operate 连服务器(gateway HTTP/MQTT) ▼</div>
- <div class="node sv" style="border-width:2px"><b>中央服务器</b><small>网关 / 业务 / 数据传输 / 配置下发 / 操作日志</small></div>
- </div>
- <h4>control 独立进程的形态</h4>
- <ul class="clean">
- <li><b>无界面后台常驻</b>(control 本就无功能界面),不弹登录窗(原 Window1 退役)。</li>
- <li>账号由 operate 拉起时<b>命令行参数传入</b>(替代原读配置+透传)。</li>
- <li>启动即 <code>StartMain.StartRun()</code> 跑采集 + 起 HttpListener。</li>
- <li>基于<b>干净的 <code>ivf_tl_Control</code> 类库新建</b>启动器(<code>ivf_tl_ControlHost</code>),<b>不改造脏壳 <code>ivf_tl_ControlTest</code></b>(其唯一正式资产 Window1 已被 operate 复刻,其余是测试/死代码,阶段3 退役删)。</li>
- </ul>
- <h3 id="http-contract">3.1 本地 HTTP 接口契约(control 提供)</h3>
- <p class="note">control 内用 .NET 自带 <code>HttpListener</code>,只监听 <code>127.0.0.1:<端口></code>(默认 38080,可配)。全部仅本机、拒绝外部请求(防外部调停机/借串口)。</p>
- <table>
- <tr><th>方法</th><th>路径</th><th>入参</th><th>返回</th><th>用途</th><th>阶段</th></tr>
- <tr><td>GET</td><td><code>/ping</code></td><td>—</td><td><code>{ok,pid,tlSn}</code></td><td>operate 启动探活,判断要不要拉起</td><td><span class="b control o" style="color:#2563eb">阶段1</span></td></tr>
- <tr><td>GET</td><td><code>/status</code></td><td>—</td><td>JSON 快照(§6 三块)</td><td>监控页轮询(每 2s)</td><td><span class="b control o" style="color:#2563eb">1→2补</span></td></tr>
- <tr><td>POST</td><td><code>/serial/pause</code></td><td><code>{houseSn}</code></td><td><code>{ok}</code></td><td>调试借串口:control 让出该舱串口</td><td><span class="b new">阶段2</span></td></tr>
- <tr><td>POST</td><td><code>/serial/resume</code></td><td><code>{houseSn}</code></td><td><code>{ok}</code></td><td>调试完归还:恢复该舱采集</td><td><span class="b new">阶段2</span></td></tr>
- <tr><td>POST</td><td><code>/shutdown</code></td><td><code>{token}</code></td><td><code>{ok}</code></td><td>受护栏整体停机(token=工程师口令校验)</td><td><span class="b new">阶段2</span></td></tr>
- </table>
- <h3 id="lifecycle">3.2 生命周期管理</h3>
- <div class="grid2">
- <div>
- <h4>拉起(operate → control)</h4>
- <ol class="clean">
- <li>operate 登录成功 → 探 <code>GET /ping</code>。</li>
- <li>通 → control 已在跑,直接"已连接",<b>不拉起</b>。</li>
- <li>不通 → <code>Process.Start</code> 拉起 control.exe,账号/密码/缓存盘<b>命令行传入</b>;管理员静默不弹 UAC。</li>
- <li>轮询 <code>/ping</code> 直到就绪(带超时重试)。</li>
- </ol>
- <h4>单实例(control)</h4>
- <p class="note">启动用命名 Mutex <code>Global\ivf_tl_control_singleton</code> 判重:已存在则立即退出。根治多实例抢串口。</p>
- </div>
- <div>
- <h4>关闭(operate)</h4>
- <p class="note">operate 关 UI <b>只退自己,绝不动 control</b>。control 继续驱动机器。(合并期的"退出语义缺失"在双进程下反而是对的。)</p>
- <h4>停止(control,仅受护栏)</h4>
- <p class="note">仅监控页受护栏按钮(二次确认+工程师口令)→ <code>/shutdown</code> → control 执行<b>安全停机</b>:停采集→停上报线程→<code>HardwareAccessLayer.ShutdownAll()</code>关句柄→释放 Mutex→退出。<b>control 需新增统一 <code>Shutdown()</code> 入口</b>(现状无统一停机)。</p>
- <h4>开机自启</h4>
- <p class="note">装机把 operate 设开机自启 → 开机自动起 operate → 拉起 control → 机器自动被驱动。</p>
- </div>
- </div>
- </div>
- </section>
- <!-- ============================ PART C ============================ -->
- <div class="part-banner c">C · 现在是什么样(现状全量业务流程 · 你要在它上面动手)</div>
- <section id="overview">
- <h2><span class="num">4</span> 当前系统拓扑(合并态) <span class="chev">▼</span></h2>
- <div class="body">
- <p class="lead">当前"一个软件"= operate UI 进程,内部用 <code>Task.Run</code> 把 control 当类库托起来常驻跑;control 经串口驱动下位机。</p>
- <div class="chain">
- <div class="node sv"><b>中央服务器</b><small>MySQL/MQTT/Kafka/网关</small></div>
- <div class="arrow">⇄</div>
- <div class="node op"><b>operate.exe 进程</b><small>登录·UI·调试·监控</small></div>
- <div class="arrow">⊃</div>
- <div class="node"><b>control 线程组</b><small>Task.Run StartRun()</small></div>
- <div class="arrow">→</div>
- <div class="node hw"><b>下位机 ×11</b><small>10 培养舱+1 缓冲瓶</small></div>
- </div>
- <div class="grid2">
- <div>
- <h4>三条服务器通道</h4>
- <table>
- <tr><th>通道</th><th>用途</th></tr>
- <tr><td><span class="b server">MQTT</span></td><td>下行指令(StartDish等);上报每舱实时状态 ~1s</td></tr>
- <tr><td><span class="b server">HTTP</span></td><td>配置下发/历史/报警/心跳/回执;operate 全部业务查询</td></tr>
- <tr><td><span class="b server">Kafka</span></td><td>拍照图片上传(扫盘→上传→成功删盘)</td></tr>
- </table>
- </div>
- <div>
- <h4>合并后常驻线程</h4>
- <table>
- <tr><th>线程</th><th>数量/节奏</th></tr>
- <tr><td>采集主循环</td><td>×10 / while</td></tr>
- <tr><td>运行监测·历史上报</td><td>×10·×10</td></tr>
- <tr><td>缓冲瓶自监测+换气调度</td><td>×1+1</td></tr>
- <tr><td>状态上报/图片上传/心跳</td><td>1s/5s/10min</td></tr>
- <tr><td>operate 详情页报警轮询</td><td>×1 / 10s</td></tr>
- </table>
- </div>
- </div>
- <div class="callout warn">⚠ 这些大多是<b>前台 LongRunning 线程</b> —— operate 关 UI 进程退不掉的根因(合并只接了"启动"没接"停止")。正是 §15 拆分要解决的。</div>
- </div>
- </section>
- <section id="boot">
- <h2><span class="num">5</span> 启动与登录流程 <span class="chev">▼</span></h2>
- <div class="body">
- <p class="lead">单实例(Mutex <code>ivf_tl_Operate</code>)→ 登录窗 → 云端校验 → 后台线程托管 control。</p>
- <div class="flow">
- <div class="step op"><div class="idx">1</div><div class="box"><b>App 启动</b> <span class="fl">App.xaml.cs:33-68</span><br>单实例互斥;注册三类全局异常;<span class="b new">新增</span>凭据迁移(明文口令一次性 DPAPI 加密)、操作日志初始化、全局点击层、语言加载<small>App_Exit 体为空 → 退出语义缺失(§15)</small></div></div>
- <div class="step op"><div class="idx">2</div><div class="box"><b>弹登录窗</b> <span class="fl">MainWindow.xaml.cs:47-61</span> → 校验账号/密码(DPAPI解密)/设备号 → <span class="b server">HTTP</span> 云端校验 → 置 <code>TlSn=NEO-1-{tlNum}</code> → 建 MqttHelper 订阅 → 拉通用设置+受精类型字典 <span class="fl">AppData.cs:114-147</span></div></div>
- <div class="step"><div class="idx">3</div><div class="box"><b>后台线程托管 control</b> <span class="fl">MainWindow.xaml.cs:67-132</span> <span class="b new">合并核心</span><br><code>Task.Run</code>:① control.AppData.Login(同账号透传)→ ② 设缓存盘 → ③ HAL.ScanDevices() 设备发现 → ④ StartMain.StartRun() 阻塞常驻<small>全程异常只记日志不退进程(已去掉 control 原 Environment.Exit)</small></div></div>
- <div class="step"><div class="idx">4</div><div class="box"><b>StartMain.StartRun 四步</b> <span class="fl">control/ivf_tl_Control/StartMain.cs:34</span><br><code>AppData.Instance</code>(读配置/建服务/开SQLite)→ <code>InitTL</code>(枚举相机+扫COM握手+拉配置)→ <code>InitHouse</code>(建1缓冲瓶+10舱逐舱起采集)→ <code>StartAsync</code>(Kafka/MQTT/上报线程)</div></div>
- <div class="step hw"><div class="idx">5</div><div class="box"><b>InitTL 硬件发现</b> <span class="fl">StartMain.cs:66</span><br>并行枚举相机 index0-9 读 CCDSN → 扫 COM(跳过COM1/2)握手得每舱 houseSn、读 EEPROM、配对相机 → 从11号缓冲瓶读 TLNum 组 tlSn<small>期望11模块,不符看 ContinueOnModuleCountMismatch(默认继续)</small></div></div>
- </div>
- <h4>配置下发链(断网也能开机)</h4>
- <div class="chain">
- <div class="node sv"><b>中央 MySQL</b><small>aivfo_tl_setting</small></div><div class="arrow">→</div>
- <div class="node"><b>HTTP拉取</b><small>重试3次</small></div><div class="arrow">→</div>
- <div class="node"><b>运行态对象</b><small>TLSetting/House/Well</small></div><div class="arrow">→</div>
- <div class="node db"><b>本地SQLite</b><small>缓存兜底</small></div>
- </div>
- <p class="note">开机链重试 3 次全失败 → 回退本地 SQLite <code>DbGetTLInfo</code>(断网开机);运行期收 MQTT <code>Update</code> 热刷新。映射在 <code>ivf_tl_UtilHelper/ConvertHelper.cs</code>。</p>
- </div>
- </section>
- <section id="loop">
- <h2><span class="num">6</span> 采集主循环 —— control 核心 <span class="chev">▼</span></h2>
- <div class="body">
- <p class="lead">每舱一个 <code>while(true)</code> 线程(<code>HouseBin.MainThread</code> · HouseBin.cs:614),靠串口往返自然节流。</p>
- <div class="flow">
- <div class="step dn"><div class="idx">1</div><div class="box"><b>让路守卫</b> <span class="fl">:632</span><br><code>IsDebug</code>(MQTT调试)或 <code>CapturePausedByGate</code>(前台借串口)任一为真 → 不发任何串口命令、Sleep(3s)<small>这是 operate 调试页借走串口时 control 让路的机制(协作式)</small></div></div>
- <div class="step"><div class="idx">2</div><div class="box"><b>温压读取+补气/排气</b> <span class="fl">ParamFun :663/1255</span><br>读门/三路温度/压力;<code>IsPai</code>且压力高→排气 PaiQi;压力<code><pressureAlarmMin</code>→补气 AerationNew<small>温度=只读(下位机自控温),气压=control 主动控制</small></div></div>
- <div class="step"><div class="idx">3</div><div class="box"><b>舱门守卫</b> <span class="fl">:667</span> 门没关→只读温压本轮结束<small>开门会立即置 IsStop* 打断进行中的对焦/拍照/换气</small></div></div>
- <div class="step"><div class="idx">4</div><div class="box"><b>按模式分流</b> <code>Dish.id>0</code> 培养皿 / <code>Balance.id>0</code> 平衡</div></div>
- </div>
- <div class="grid2">
- <div>
- <h4>🟦 培养皿模式 <span class="fl">:671-750</span></h4>
- <table>
- <tr><th>触发</th><th>动作</th></tr>
- <tr><td>换气间隔到 :696</td><td>AirSwapFun 换气</td></tr>
- <tr><td>拍照间隔到 :704</td><td>StartCCD+ccdThreadFun(§7)</td></tr>
- <tr><td>FirstClearest 真 :712</td><td>等气稳→对焦→成功后换气拍照</td></tr>
- <tr><td>拍照异常冷却 :722</td><td>CCDState=1 未到等待→continue</td></tr>
- </table>
- </div>
- <div>
- <h4>🟦 平衡模式 <span class="fl">:753-783</span></h4>
- <p class="note"><b>只有换气,无拍照/对焦。</b> 排气走 <code>VentWaitTimeB</code>(培养态 <code>VentWaitTimeD</code>)。</p>
- <div class="callout info">对焦默认降级:<code>localAutofocusEnabled</code> 默认 0 时<b>不做实时对焦</b>,按 scene=0 出厂基准 / eepromClearPosition 回退拍照(:1462);置1才走实时对焦。</div>
- </div>
- </div>
- <h4>关键状态标志</h4>
- <table>
- <tr><th>标志</th><th class="c">行</th><th>含义</th></tr>
- <tr><td><code>IsPai</code></td><td class="c">:153</td><td>排队换气模式,App.config QueuAir 注入</td></tr>
- <tr><td><code>FirstClearest</code></td><td class="c">:277</td><td>本轮是否先对焦;关门/StartDish/HouseAutoFocus 置 true</td></tr>
- <tr><td><code>IsStopClearest/CCD/AirSwap</code></td><td class="c">:287</td><td><b>开门</b>全置 true,立即打断对焦/拍照/换气</td></tr>
- <tr><td><code>CCDState</code></td><td class="c">:369</td><td>连续抓图失败 CCDFailedNumber 次→报警+冷却</td></tr>
- <tr><td><code>CapturePausedByGate</code>/<code>IsDebug</code></td><td class="c">:90</td><td>HAL借用让路 / MQTT调试让路</td></tr>
- </table>
- </div>
- </section>
- <section id="photo">
- <h2><span class="num">7</span> 拍照 / 对焦动作内部 <span class="chev">▼</span></h2>
- <div class="body">
- <p class="lead">拍照 <code>ccdThreadFun</code>(HouseBin.cs:2134):逐 well × 逐层 移电机抓图上传。</p>
- <div class="flow">
- <div class="step hw"><div class="idx">1</div><div class="box"><b>水平电机复位 → 开 LED</b></div></div>
- <div class="step hw"><div class="idx">2</div><div class="box"><b>逐 well</b> 移水平电机到该 well 的 <code>horizontalMotorPosition</code></div></div>
- <div class="step hw"><div class="idx">3</div><div class="box"><b>逐层</b> 移垂直电机到该层 Z(层间距 <code>focusLayerSpacingPulse</code> × 层数 <code>focusLayerCount</code>)</div></div>
- <div class="step hw"><div class="idx">4</div><div class="box"><b>Photograph 抓图+处理+上传</b> 失败重试 <code>CCDFailedNumber</code> 次;门开/欠压有打断与插补</div></div>
- <div class="step hw"><div class="idx">5</div><div class="box"><b>关 LED → 电机归位</b></div></div>
- </div>
- <div class="callout warn">⚠ <b>层链就近优先</b>:well 级(非空)> 设备级 > 报错。层间距缺失抛 <code>FocusConfigMissingException</code> 跳过该 well,<b>不兜底</b>。</div>
- <h3>7.1 自动对焦四步标定(合并自 AutoFocusTool,算法逐字保留)</h3>
- <p class="note">重构进 <code>control/IvfTl.AutoFocus/Calib/CalibrationEngine.cs:189</code>,硬件依赖改走 HAL。</p>
- <div class="chain">
- <div class="node hw"><b>①粗对焦</b><small>中心90000±30000<br>步2000·中央40%ROI</small></div><div class="arrow">→</div>
- <div class="node hw"><b>②水平居中</b><small>EEPROM±2000微调</small></div><div class="arrow">→</div>
- <div class="node hw"><b>③曝光二分</b><small>MeanLo95/Hi150</small></div><div class="arrow">→</div>
- <div class="node hw"><b>④精对焦</b><small>0.95r ROI·抛物线插值<br>PeakRatio>1.2 判合格</small></div>
- </div>
- <p class="note">清晰度评价 Tenengrad/Laplacian÷mean(真机修复);标定真相源 <code>calibration.json</code> → 镜像写库 <code>house_autofocus_calibration</code>(scene0出厂基准永留/scene1日常),经 <code>CalibrationStore</code> 委托回调。</p>
- </div>
- </section>
- <section id="air">
- <h2><span class="num">8</span> 换气 / 补气 / 排气 <span class="chev">▼</span></h2>
- <div class="body">
- <p class="lead">control 主动控制气体:周期换气置换预混气、低压补气、高压排气。</p>
- <table>
- <tr><th>动作</th><th class="c">行</th><th>逻辑</th></tr>
- <tr><td><b>换气</b> AirSwapOldFun <span class="b control o" style="color:#2563eb">唯一启用</span></td><td class="c">:991</td><td>循环 <code>airSwapTime</code> 次:开排气阀→放气 <code>ventilationDelay</code> 秒→关阀→欠压补气。反复排空+回充稀释置换<br><span class="fl">newAirSwap 新版被注释,实质未生效</span></td></tr>
- <tr><td><b>补气</b> AerationNew</td><td class="c">:1307</td><td>循环 <code>houseAerationNum</code> 次,每次 Sleep(<code>aerationDelay</code>) 读压力,达 <code>pressureAlarmMin</code> 停</td></tr>
- <tr><td><b>排气</b> PaiQi</td><td class="c">:1341</td><td>循环 <code>VentNum</code> 次,降到 <code>VentPre</code> 停</td></tr>
- </table>
- <h3>8.1 缓冲瓶排队换气(IsPai=true,8步握手)</h3>
- <p class="note">11号缓冲瓶不是培养舱(无相机/不拍照),是给多舱排队换气供预混气的共享气源。<code>AirSwapQueueFun</code>(:1120)与缓冲瓶 <code>HuanQiThread</code>(:398)经 5 个 bool 握手位协调。</p>
- <div class="flow">
- <div class="step"><div class="idx">1</div><div class="box">舱<b>报名入队</b></div></div>
- <div class="step"><div class="idx">2</div><div class="box">缓冲瓶通知"轮到你"(QueueAir)</div></div>
- <div class="step"><div class="idx">3</div><div class="box">舱开自己进/排气阀(ReadyAir)</div></div>
- <div class="step"><div class="idx">4</div><div class="box"><b>缓冲瓶开自己进气阀</b>(OpenBuffer)</div></div>
- <div class="step"><div class="idx">5</div><div class="box">舱按 TongQi 时间冲刷</div></div>
- <div class="step"><div class="idx">6</div><div class="box">缓冲瓶收尾、队空关阀(EndAir)</div></div>
- <div class="step"><div class="idx">7</div><div class="box">舱关阀补气</div></div>
- <div class="step dn"><div class="idx">8</div><div class="box">StopPai 防中途退出的舱卡住队列</div></div>
- </div>
- <p class="note">缓冲瓶自身:1s/轮读气压,<code><bufferBottlerPressureMin+10</code>→自补气(循环 <code>bufferBottlerAerationNum</code> 次)。只控进气阀+自补气,无排气阀。</p>
- </div>
- </section>
- <section id="server">
- <h2><span class="num">9</span> 服务器双向交互 <span class="chev">▼</span></h2>
- <div class="body">
- <p class="lead">指令走 MQTT、回执走 HTTP、图片走 Kafka。</p>
- <div class="grid2">
- <div>
- <h4>下行指令(服务器→control,MQTT)<span class="fl">AppData.MqttMessage:972</span></h4>
- <table>
- <tr><th>指令</th><th>作用</th></tr>
- <tr><td>StartBalance/EndBalance(1/2)</td><td>起/停平衡</td></tr>
- <tr><td>StartDish/EndDish(3/4)</td><td>起/停培养皿</td></tr>
- <tr><td>EmbryoState(5)</td><td>改胚胎状态</td></tr>
- <tr><td>Update(6)</td><td>重拉配置刷新</td></tr>
- <tr><td>DebugStart(7)</td><td>进调试(置IsDebug,唯一带uuid回执)</td></tr>
- <tr><td>HouseAutoFocus/WellAutoFocus(8/9)</td><td>动电机+拍照对焦整舱/单孔</td></tr>
- </table>
- </div>
- <div>
- <h4>上行(control→服务器)</h4>
- <table>
- <tr><th>类型</th><th>通道/内容</th></tr>
- <tr><td>状态~1s</td><td><span class="b server">MQTT</span> 压力/温度/门/阀/运行态 → TL/House/collecting-data QoS2</td></tr>
- <tr><td>图片5s</td><td><span class="b server">Kafka</span> 扫盘上传,成功才删</td></tr>
- <tr><td>报警/历史/心跳</td><td><span class="b server">HTTP</span> 门/串口/拍照态;曲线;每日维护</td></tr>
- <tr><td>回执</td><td><span class="b server">HTTP</span> MqttResultController /result</td></tr>
- </table>
- </div>
- </div>
- <h4>trace_id 全链路(排障利器)</h4>
- <div class="chain">
- <div class="node op"><b>operate/control</b><small>OperationLogContext.TraceId</small></div><div class="arrow">→</div>
- <div class="node"><b>HTTP请求头 traceId</b><small>HttpHelper:1460</small></div><div class="arrow">→</div>
- <div class="node sv"><b>Java网关/微服务</b><small>同trace_id透传</small></div><div class="arrow">→</div>
- <div class="node db"><b>log.operation_log</b><small>跨端时间线</small></div>
- </div>
- <p class="note">两层日志:<b>动作层</b>(谁/功能/输入/输出/结果/报错/耗时,定位失败靠它)+ <b>点击层</b>(哪页点了哪按钮)。排障第一步:拿 trace_id 拉跨端时间线找 <code>result=失败</code> 那条。</p>
- </div>
- </section>
- <section id="operate">
- <h2><span class="num">10</span> operate 业务模块逐功能 <span class="chev">▼</span></h2>
- <div class="body">
- <p class="lead">每功能:操作→背后动作(串口/HTTP/DB/借用)→file:line。<span class="b operate">绿=正常</span> <span class="b down">红=降级</span> <span class="b new">橙=新增</span></p>
- <h3>10.1 首页/舱室总览 · MainPageViewModel(1023行)</h3>
- <table>
- <tr><th>操作</th><th>背后动作</th><th class="c">行</th></tr>
- <tr><td>看舱室列表/状态</td><td><span class="b server">HTTP</span> GetHouseCultureListApi</td><td class="c">:142</td></tr>
- <tr><td>报警轮询/计数/语音</td><td><span class="b server">HTTP</span> SearchAlarmHistory(Num)Api+TlInfoTimeApi</td><td class="c">:745</td></tr>
- <tr><td>结束培养(EndDish)</td><td><b>仅本地清空 ExDish</b>,无 HTTP 下发</td><td class="c">:347</td></tr>
- <tr><td>(隐性)反写 control 心跳</td><td>直写 <code>ControlAppData.LastHttpOkAt</code></td><td class="c">:768</td></tr>
- </table>
- <h3>10.2 舱室设置 / 对焦调试 · HouseDebugPageViewModel(1592行)<span class="b down">含3处降级</span></h3>
- <p class="note">进调试 <code>ComHouseInit</code>(:244)向 HAL <code>Acquire(OperateDebug)</code> 借采集端同一句柄;退出 <code>ComHouseUnit</code>(:355)只 Dispose 归还,绝不关口。</p>
- <table>
- <tr><th>操作</th><th>背后动作</th><th class="c">行</th><th>状态</th></tr>
- <tr><td>读温/压/门、开关 LED/气阀、电机点动</td><td>Serial.*Wait</td><td class="c">:383~610</td><td><span class="b operate">正常</span></td></tr>
- <tr><td>写进气阀时间/垂直脉冲/存水平位</td><td>Serial.Write*Wait(真下发)</td><td class="c">:520/565/579</td><td><span class="b down">成功语义待验 M-05</span></td></tr>
- <tr><td><b>写舱室排气阀时间</b></td><td>→ <b>return false 不下发</b>,仅本地暂存</td><td class="c">:534</td><td><span class="b down">降级 M-01</span></td></tr>
- <tr><td><b>读舱室排气阀时间</b></td><td>→ <b>return -1</b>,静默保留旧值</td><td class="c">:553</td><td><span class="b down">降级 M-02</span></td></tr>
- <tr><td><b>调试页存图</b></td><td>→ <b>丢弃宽高转 SaveBmpPic</b>,未验证</td><td class="c">:748</td><td><span class="b down">降级 M-04</span></td></tr>
- </table>
- <h3>10.3 缓冲瓶调试 · BufferDebugViewModel(311行)<span class="b down">含1处降级</span></h3>
- <table>
- <tr><th>操作</th><th>背后动作</th><th class="c">行</th><th>状态</th></tr>
- <tr><td>读状态/补气/写进气阀时间/读灯光</td><td>Serial.*Wait(真下发/读)</td><td class="c">:229~285</td><td><span class="b operate">正常</span></td></tr>
- <tr><td><b>写灯光亮度 EEPROM</b></td><td>→ <b>return false 不下发</b>,仅本地+服务器</td><td class="c">:268</td><td><span class="b down">降级 M-03</span></td></tr>
- </table>
- <div class="grid2">
- <div>
- <h3>10.4 其它(纯 HTTP/展示)</h3>
- <table>
- <tr><th>模块</th><th>关键接口</th></tr>
- <tr><td>对焦设置</td><td>FocusSettingApi</td></tr>
- <tr><td>皿管理/加皿</td><td>Add/Update CultureRecord</td></tr>
- <tr><td>胚胎详情(10s报警轮询)</td><td>GetRecordDetail</td></tr>
- <tr><td>胚胎照片/环境曲线</td><td>GetPictureView/GetHouseEnvironmentList</td></tr>
- </table>
- </div>
- <div>
- <h3>10.5 合并新增三页 <span class="b new">新增</span></h3>
- <table>
- <tr><th>页</th><th>职责</th></tr>
- <tr><td>服务监控</td><td>每2s跨读 GetMonitorSnapshot(:77),只读;阈值占位待验</td></tr>
- <tr><td>统一配置</td><td>写本地App.config(凭据加密);SaveAll 返回值恒true占位</td></tr>
- <tr><td>内置软键盘</td><td>替代 osk.exe</td></tr>
- </table>
- </div>
- </div>
- <h3>10.6 operate HTTP 接口全表(经 HttpHelper,超时12s/无重试)</h3>
- <div class="pill-row">
- <span class="pill">登录 auth/login</span><span class="pill">天平 start/stopBalance</span><span class="pill">在培列表 getHouseCultureList</span>
- <span class="pill">皿 add/end/update CultureRecord</span><span class="pill">记录详情 getRecordDetail</span><span class="pill">切层 switchVideoPictureLayers</span>
- <span class="pill">快捷/时间按钮 getButtons</span><span class="pill">系统设置 setting/system(+update)</span><span class="pill">舱室设置 setting/house(+update)</span>
- <span class="pill">调试回写 house/debugging</span><span class="pill">well更新 house/well/update</span><span class="pill">对焦 house/focus/setting</span>
- <span class="pill">立即拍照 house/immediately</span><span class="pill">通用设置 setting/common(+update)</span><span class="pill">光照 lights/update</span>
- <span class="pill">仪器时间 tlInfo/time</span><span class="pill">字典 queryDictionaryByType</span><span class="pill">环境列表 getHouseEnvironmentList</span>
- <span class="pill">标记去向 markEmbryoDestination</span><span class="pill">皿记录 getEmbryoCultureRecord(Num)</span><span class="pill">告警 getAlarmNum/getAlarm</span>
- <span class="pill">图片 getPictures/getSourcePictures</span><span class="pill">静音 muteAlarm</span><span class="pill">裁剪告警 getHouseCropAlarm</span>
- </div>
- <p class="note">统一汇聚 <code>HttpClientSendAsync</code>(:1449):token缓存、trace_id透传、Stopwatch计时、四类分支统一操作日志埋点。接口与基准<b>无增删</b>,仅加埋点。</p>
- </div>
- </section>
- <section id="hal">
- <h2><span class="num">11</span> 硬件层 / 调试借串口(HAL) <span class="chev">▼</span></h2>
- <div class="body">
- <p class="lead">HAL(HardwareAccessLayer 单例)全进程唯一持有每个 COM 口/每台相机(四字典),杜绝同口重复 Open。</p>
- <div class="flow">
- <div class="step op"><div class="idx">1</div><div class="box"><b>前台 Acquire(OperateDebug)</b> <span class="fl">HouseGateImpl.cs:36</span> 先 MarkPause() 让后台让路,再 SemaphoreSlim 抢舱级独占锁</div></div>
- <div class="step"><div class="idx">2</div><div class="box"><b>后台采集让路</b> HouseBin 订阅 OnPauseCapture 置 volatile <code>CapturePausedByGate</code>,主循环顶部读到停发命令(非强杀)</div></div>
- <div class="step op"><div class="idx">3</div><div class="box"><b>前台用 lease.Serial/Camera 调试</b> 复用采集端同一句柄,不 new 第二个口</div></div>
- <div class="step"><div class="idx">4</div><div class="box"><b>Dispose 归还 → OnResumeCapture</b> 绝不 ClosePort/UnInit,归还后采集恢复</div></div>
- </div>
- <div class="callout info">ComBin(串口命令):队列+单发送线程+两级握手。发送级等回包 <b>30s 超时后重发3次</b>;调用级等整条完成(无限等)。一口同时只一条在途。</div>
- <div class="callout warn">⚠ <b>两套并行栈未去重</b>:旧栈(ComEntitys/CameraHelper)InitTL 用;HAL包装栈(IvfTl.Hardware/SerialHelper)借用用 —— 排障要分清当前走哪条。</div>
- </div>
- </section>
- <section id="data">
- <h2><span class="num">12</span> 数据库与数据流 <span class="chev">▼</span></h2>
- <div class="body">
- <p class="lead">本地 SQLite <code>aivfoTL.db</code>(9 表:8 表须预置,<code>house_autofocus_calibration</code> 由 DBService 启动时 CodeFirst.InitTables 自动建)+ 中央 MySQL(7库)。</p>
- <div class="grid2">
- <div>
- <h4>本地 SQLite 表</h4>
- <table>
- <tr><th>表</th><th>用途</th></tr>
- <tr><td>tl_setting/house/house_well_setting</td><td>配置本地缓存(断网兜底)</td></tr>
- <tr><td>dish/embryo/balance</td><td>培养皿/胚胎/平衡(MQTT落库)</td></tr>
- <tr><td>house_well_photo</td><td>电机位置缓存</td></tr>
- <tr><td>picture</td><td>待传图片元数据(传成功即删)</td></tr>
- <tr><td>house_autofocus_calibration</td><td>对焦标定镜像(scene0永留)</td></tr>
- </table>
- </div>
- <div>
- <h4>数据归属</h4>
- <table>
- <tr><th>类别</th><th>例</th></tr>
- <tr><td>中央下发·本地缓存</td><td>配置、培养数据、电机位置</td></tr>
- <tr><td>本地产生·双写镜像</td><td>对焦标定(json↔库)</td></tr>
- <tr><td>本地产生·上报不落库</td><td>实时状态/历史/图片/告警/日志</td></tr>
- </table>
- </div>
- </div>
- <p class="note">中央7库:aivfo-auth/aivfo_services/aivfo-tl(传输·视频)/<b>aivfo_tl_setting(配置下发源)</b>/aivfo_tl(业务)/log(operation_log)/quartz。⚠ <code>aivfo_tl</code>(下划线=业务)≠ <code>aivfo-tl</code>(中划线=传输)。</p>
- </div>
- </section>
- <!-- ============================ PART D ============================ -->
- <div class="part-banner d">D · 接下来改什么(你从这里开始)</div>
- <section id="degrade">
- <h2><span class="num">13</span> ★ 合并降级登记(动手前必读) <span class="chev">▼</span></h2>
- <div class="body">
- <div class="callout warn"><b>合并代码完成 ≠ 业务闭环。</b> 以下相对合并前基准是真实功能缺失,根因统一:control 端 Commander 缺 builder,HAL <code>SerialChannelImpl.cs</code> 返回桩值,VM 据返回值提示。对应 <code>待验证清单.md</code> M-01~M-07,真机已连、均由 Claude 自主真机验证(无需用户在场/配合);仅「水平电机」「垂直 Z 电机」运动范围需守安全区间(参考 <code>临时文件/相关参数.html</code>),其余下位机控制无风险。</div>
- <table>
- <tr><th>编号</th><th>功能</th><th>基准行为</th><th>现状(file:line)</th><th>UI提示</th><th>补法</th></tr>
- <tr><td><b>M-01</b></td><td>排气阀时间<b>写</b></td><td>真下发</td><td>SerialChannelImpl.cs:130 <code>return false</code> 仅本地暂存</td><td>✅有</td><td>补 control <code>CreateWriteEEPROOpenVentTimeCommand</code>+真机核对字节</td></tr>
- <tr><td><b>M-02</b></td><td>排气阀时间<b>读</b></td><td>真读</td><td>:137 <code>return -1</code> 静默保留旧值</td><td>❌无</td><td>补 control 读命令</td></tr>
- <tr><td><b>M-03</b></td><td>缓冲瓶灯光<b>写EEPROM</b></td><td>真写</td><td>:143 <code>return false</code> 仅本地+服务器</td><td>✅有</td><td>补 control <code>CreateWriteEEPROMLightNum</code></td></tr>
- <tr><td><b>M-04</b></td><td>调试页<b>存图</b></td><td>MVCAPI.SavePic(带宽高)</td><td>CameraImpl.cs:148 丢宽高转 SaveBmpPic 未验证</td><td>❌无</td><td>真机核对落盘一致性</td></tr>
- <tr><td><b>M-05</b></td><td>写EEPROM"成功=true"</td><td>—</td><td>阻塞收回复即true,未校验真实成功</td><td>—</td><td>真机核对</td></tr>
- <tr><td><b>M-06</b></td><td>ReadWellFocusZero按well</td><td>按well</td><td>control Z零点整舱单值,well入参被忽略</td><td>—</td><td>真机核对</td></tr>
- <tr><td><b>M-07</b></td><td>Release连内网网关</td><td>—</td><td>AppData.cs:91-111 #if DEBUG 覆写到 test-gateway 外网</td><td>—</td><td>真机/排障用 Release+现场核对 urlIp</td></tr>
- </table>
- <div class="callout ok"><b>正向修复(已在合并里做掉,记录在案):</b> ① 8GB 日志洪流根因修复(ChangeLanguage 精确移除字典 App.xaml.cs:265);② 日志滚动 Date→Composite;③ BaseUrl 取值健壮化(缺键不崩);④ passWord DPAPI 加密治理。</div>
- </div>
- </section>
- <section id="roadmap">
- <h2><span class="num">14</span> 三阶段路线图 <span class="chev">▼</span></h2>
- <div class="body">
- <p class="lead">每阶段可独立编译/验证;真机已连,真机步骤由 Claude 自主跑完(无需用户在场/配合),仅「水平电机」「垂直 Z 电机」运动范围需守安全区间(参考 <code>临时文件/相关参数.html</code>),其余下位机控制无风险。<b>当前进度:阶段1 待开工</b>。</p>
- <div class="flow">
- <div class="step"><div class="idx">1</div><div class="box"><b>阶段1 · control 独立进程骨架(最关键)</b><br>新建无界面 control 启动器 + Mutex单实例 + 命令行接账号 + 内嵌 HttpListener(/ping+/status)+ operate 改为探活拉起<br><small><b>出口</b>:operate 能拉起独立 control.exe、control 驱动机器、<b>operate 关了 control 续命</b>、重开复用、单实例;真机采集闭环</small></div></div>
- <div class="step"><div class="idx">2</div><div class="box"><b>阶段2 · 监控补全 + 调试借串口 + 受护栏停止</b><br>/status 补三块(各舱活动/线程心跳/串口借用);/serial/pause|resume 接 HouseGate 跨进程借;/shutdown 受护栏停止 + control 统一 Shutdown()<br><small><b>出口</b>:监控页完整;调试页跨进程借串口(control让路)调完恢复;停止按钮安全停 control</small></div></div>
- <div class="step"><div class="idx">3</div><div class="box"><b>阶段3 · 清理老壳 + 装机收尾</b><br>退役删 ivf_tl_ControlTest;operate 开机自启;ComBin 两套栈去重;部署文档(两exe+端口+DependFile+开机自启)<br><small><b>出口</b>:全新部署一次到位、文档与代码对齐。<b>顺带补 §13 的 M-01~M-07 真机门控项</b></small></div></div>
- </div>
- <h3 id="stage1">14.1 阶段1 · 七个任务(照着做,详见开发计划文档)</h3>
- <p class="note">新建项目 <code>ivf_tl_operate_2.0/control/ivf_tl_ControlHost/</code>(替代脏壳)。Task1-6 纯编码可独立完成,Task7 真机验证由 Claude 自主跑完(真机已连,无需用户在场/配合)。技术栈:.NET6 / HttpListener / Mutex / xUnit。</p>
- <table>
- <tr><th>任务</th><th>内容</th><th>产出/改动</th><th>验证</th></tr>
- <tr><td><span class="tasknum">1</span>项目骨架</td><td>新建 ivf_tl_ControlHost(WinExe,requireAdmin,引用 ivf_tl_Control)</td><td>csproj/manifest/Program(空壳)</td><td>编译</td></tr>
- <tr><td><span class="tasknum">2</span>参数解析</td><td>HostArgs:account/password/cacheDisk/port,IsValid 守卫</td><td>HostArgs.cs + 单测</td><td>xUnit 4条</td></tr>
- <tr><td><span class="tasknum">3</span>StatusDto</td><td>/ping /status 返回体(ok/pid/tlSn/started)</td><td>StatusDto.cs + 单测</td><td>xUnit 2条</td></tr>
- <tr><td><span class="tasknum">4</span>HttpListener</td><td>内嵌本地小服务,127.0.0.1 only,路由 /ping /status</td><td>ControlHttpServer.cs</td><td>编译</td></tr>
- <tr><td><span class="tasknum">5</span>完整启动序</td><td>Program:Mutex→参数→Login→盘→ScanDevices→StartRun→HTTP驻留<br><span class="fl">复刻 operate MainWindow:78-130,顺序不可变</span></td><td>Program.cs</td><td>编译</td></tr>
- <tr><td><span class="tasknum">6</span>operate 改拉起</td><td>删进程内 StartRun,新增 ControlProcessLauncher(探活→Process.Start→轮询);App.config 加 controlPort/controlExePath</td><td>Launcher + MainWindow:67-134 + App.config</td><td>编译</td></tr>
- <tr><td><span class="tasknum">7</span><span class="b down">真机</span>端到端</td><td>部署→起微服务→启 operate 登录→观察 control 被拉起/续命/复用/单实例/采集闭环</td><td>文档回写</td><td>运行+真机</td></tr>
- </table>
- <div class="callout info"><b>开工方式:</b> 子代理驱动开发(每 Task 派子代理实现+两阶段审查)。<b>先建 feature 分支,勿在 main 直接改代码。</b> 每步按回写协议更新进度文档。</div>
- </div>
- </section>
- <section id="split">
- <h2><span class="num">15</span> 双进程 8 个改造点(现状直连 control 的地方) <span class="chev">▼</span></h2>
- <div class="body">
- <p class="lead">合并后 control 跑在 operate 同进程,这 8 个直连点要全改成跨进程(启动 control.exe + HTTP/IPC)。这是拆分的"拆除清单"。</p>
- <table>
- <tr><th>#</th><th>直连点(file:line)</th><th>现状</th><th>拆后改成</th><th>阶段</th></tr>
- <tr><td>1</td><td>MainWindow.xaml.cs:89</td><td>ControlAppData.Login() 透传</td><td>启动独立进程+透传凭证</td><td>1</td></tr>
- <tr><td>2</td><td>MainWindow.xaml.cs:100</td><td>直写 LogService.Pan</td><td>启动参数</td><td>1</td></tr>
- <tr><td>3</td><td>MainWindow.xaml.cs:103,121</td><td>new StartMain()+StartRun()</td><td>拉起 control.exe 等就绪</td><td>1</td></tr>
- <tr><td>4</td><td>MainWindow.xaml.cs:110,112</td><td>HAL.ScanDevices()</td><td>移入 control 进程</td><td>1</td></tr>
- <tr><td>5</td><td>HouseDebugPageViewModel.cs:256,257</td><td>HAL.Acquire 借串口</td><td>跨进程借用协议 /serial/pause|resume</td><td>2</td></tr>
- <tr><td>6</td><td>BufferDebugViewModel.cs:163,164</td><td>HAL.Acquire 借串口</td><td>同上</td><td>2</td></tr>
- <tr><td>7</td><td>ServiceMonitorViewModel.cs:77</td><td>GetMonitorSnapshot() 直读</td><td>HTTP /status 拉快照</td><td>2</td></tr>
- <tr><td>8</td><td>MainPageViewModel.cs:768</td><td>直写 LastHttpOkAt</td><td>跨进程上报心跳</td><td>2</td></tr>
- </table>
- </div>
- </section>
- <!-- ============================ PART E ============================ -->
- <div class="part-banner e">E · 怎么上手(环境 / 已知坑 / 开工)</div>
- <section id="env">
- <h2><span class="num">16</span> 技术栈 / 环境 / 构建 <span class="chev">▼</span></h2>
- <div class="body">
- <div class="grid2">
- <div>
- <h4>技术栈</h4>
- <ul class="clean">
- <li>桌面端:C# / .NET 6(net6.0-windows)/ WPF / MVVM(CommunityToolkit.Mvvm)</li>
- <li>JSON:Newtonsoft.Json;单测:xUnit(见 control/IvfTl.AutoFocus.Tests)</li>
- <li>解决方案:<code>ivf_tl_operate_2.0/ivf_tl_Operate.sln</code> 与 <code>control/ivf_tl_Control.sln</code></li>
- <li>权限:operate/control 都要求 <code>requireAdministrator</code>(串口/相机/盘符)</li>
- </ul>
- <h4>代码检索(重要)</h4>
- <p class="note">仓库已建 <b>codegraph 索引</b>。理解/定位代码优先用 <code>codegraph_explore</code>/<code>codegraph_node</code> 或 shell <code>codegraph explore "..."</code>,别上来就 grep。增删文件后跑 <code>codegraph sync</code>。</p>
- </div>
- <div>
- <h4>构建 / 运行依赖</h4>
- <ul class="clean">
- <li><code>dotnet build</code>;Java 微服务用 JDK11+Maven3.9.9 @ <code>C:\TLData\tools</code></li>
- <li>control 启动要连本机 gateway(127.0.0.1:10010)登录、连 108 的 MySQL/Kafka/MQTT/Nacos</li>
- <li>起微服务集群:<code>bash 项目文档/开发环境/start-all.sh</code></li>
- <li>control 运行依赖 <code>DependFile</code>(SQLite库/相机原生DLL)+ App.config,部署须随 exe 到位</li>
- </ul>
- <h4>排障(操作日志)</h4>
- <p class="note">拿 trace_id 查 <code>log.operation_log</code>(192.168.0.108:3306,root/root)拉跨端时间线,找 <code>result=失败</code> 读 input+error。需先起 <code>aivfo-oplog</code> 消费端,否则消息只堆 Kafka 不入库。</p>
- </div>
- </div>
- </div>
- </section>
- <section id="pitfall">
- <h2><span class="num">17</span> 已知坑(务必知道) <span class="chev">▼</span></h2>
- <div class="body">
- <ol class="clean">
- <li><b>operate 两个 build 行为不同</b>:Debug 版 <code>AppData.cs:91-111</code> 有 <code>#if DEBUG</code> 把服务器地址覆盖成测试外网(test-gateway)——<b>真机/本机验证必须用 Release</b>(走 App.config 的 127.0.0.1/108)。(=M-07)</li>
- <li><b>管理员进程</b>:operate/control 都 requireAdministrator,非交互 shell RunAs 提权可能弹不出(起不来/杀不掉)。</li>
- <li><b>control 启动依赖运行时文件</b>:DependFile(SQLite/相机DLL)+ App.config,拆独立进程部署时要随 control.exe 到位,否则 StartRun 失败。清单见 control 全景 §八。</li>
- <li><b>两套并行串口/相机栈</b>(旧 ComEntitys + HAL 包装栈),迁移期现状,排障要分清当前走哪条(全景 §六)。</li>
- <li><b>本地 SQLite 多数表须预置</b>:<code>aivfoTL.db</code> 须预置文件;但 <code>house_autofocus_calibration</code> 例外,由 <code>DBService</code> 启动时 <code>CodeFirst.InitTables</code> 自动建(<code>CREATE TABLE IF NOT EXISTS</code>),勿当缺表 bug 排查。</li>
- <li><b>老壳 ivf_tl_ControlTest</b>:混测试代码、命名乱、已被 operate 旁置不引用。<b>别改造它</b>;新启动器基于干净的 ivf_tl_Control 类库新建,老壳阶段3 退役删。</li>
- </ol>
- </div>
- </section>
- <section id="howto">
- <h2><span class="num">18</span> 开工方式 & 续接 <span class="chev">▼</span></h2>
- <div class="body">
- <div class="grid2">
- <div>
- <h4>开工方式(用户已定)</h4>
- <ul class="clean">
- <li>子代理驱动开发:每 Task 派全新子代理实现 + 两阶段审查(spec 合规→代码质量)。</li>
- <li><b>先建 feature 分支</b>(勿在 main 直接改代码)。</li>
- <li>阶段1 Task1-6 纯编码;Task7 真机验证由 Claude 自主跑完(真机已连,无需用户在场/配合)。</li>
- <li>每完成一步按回写协议更新:进度状态.yaml(断点)+ 交接卡.md(追加)+ 工作计划表 + 进度数据.js。<b>提交边界 = 文档已同步</b>。</li>
- </ul>
- </div>
- <div>
- <h4>权威文档导航(深入查)</h4>
- <ul class="clean">
- <li><code>需求文档/specs/...双进程拆分-design.md</code> — 架构设计全文(动手前必读)</li>
- <li><code>开发计划/...阶段1-control独立进程骨架.md</code> — 阶段1 七任务(带完整代码/命令)</li>
- <li><code>需求文档/control-逻辑与配置全景.md</code> — control 现状全图(改 control 前查)</li>
- <li><code>需求文档/操作端逻辑与配置全景.md</code> — operate 现状全图 + §八降级登记</li>
- <li><code>进度/</code> — 进度状态.yaml(断点)/工作计划表/交接卡/待验证清单(M-01~M-07)</li>
- <li><code>CLAUDE.md</code> — 项目铁律(中文/codegraph/回写协议)</li>
- </ul>
- </div>
- </div>
- <p class="note" style="text-align:center;margin-top:18px;color:#94a3b8">— 本总文档基于 2026-06-22 源码复核生成,汇总自上述各权威文档;若代码与本页不符,以源码 + 对应全景文档为准 —</p>
- </div>
- </section>
- </main>
- </div>
- <script>
- document.querySelectorAll('section>h2').forEach(h=>h.addEventListener('click',()=>h.parentElement.classList.toggle('collapsed')));
- const links=[...document.querySelectorAll('nav.toc a')];
- const secs=[...document.querySelectorAll('section')];
- const obs=new IntersectionObserver(es=>{es.forEach(e=>{if(e.isIntersecting){
- links.forEach(l=>l.style.background='');
- const a=links.find(l=>l.getAttribute('href')==='#'+e.target.id);
- if(a)a.style.background='#1e293b';
- }});},{rootMargin:'-8% 0px -82% 0px'});
- secs.forEach(s=>obs.observe(s));
- links.forEach(a=>a.addEventListener('click',ev=>{
- const t=document.querySelector(a.getAttribute('href'));
- if(t){ev.preventDefault();t.classList.remove('collapsed');t.scrollIntoView({behavior:'smooth'});}
- }));
- </script>
- </body>
- </html>
|