|
@@ -0,0 +1,438 @@
|
|
|
|
|
+// ============================================================
|
|
|
|
|
+// 时差培养箱 · 培养全流程 · 节点与连线数据
|
|
|
|
|
+// 设计原则:所有业务分支都真实画进图(不藏在面板文字里)
|
|
|
|
|
+// - 节点 type: start/operate/control/front/branch/error
|
|
|
|
|
+// - 边 type: internal(本端实线) / cross(跨端虚线) / error(异常点线/回流)
|
|
|
|
|
+// ============================================================
|
|
|
|
|
+
|
|
|
|
|
+const NODES = [
|
|
|
|
|
+ // ========== 起点 ==========
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'start', type: 'start', icon: '🥚', title: '放入胚胎·开始',
|
|
|
|
|
+ brief: '医生取卵受精后,把胚胎放进培养皿,准备入箱',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: '这是整个时差培养的起点:医生在实验室完成取卵、受精(IVF/ICSI),得到胚胎后,准备把胚胎放进时差培养箱的某个空舱开始延时培养。',
|
|
|
|
|
+ pre: ['已完成取卵+受精,得到胚胎', '时差培养箱有空闲舱室', 'operate 操作端已登录'],
|
|
|
|
|
+ trigger: '医生在 operate 触摸屏主界面,看到目标舱室是"空闲"状态',
|
|
|
|
|
+ steps: ['确认胚胎数量、对应孔位', '走到培养箱旁的 operate 触摸屏', '点击空闲舱格 → 进入"新建入箱"'],
|
|
|
|
|
+ backend: '无后端动作(纯物理 + 界面准备)',
|
|
|
|
|
+ data: ['暂无数据写入,等"开始培养"才落库'],
|
|
|
|
|
+ cross: null,
|
|
|
|
|
+ code: ['operate MainPageView.xaml.cs:155 CustomHouseInfo_ClickHouseEvent']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ========== operate:主界面 ==========
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'op-main', type: 'operate', icon: '🖥️', title: '主界面·舱室总览',
|
|
|
|
|
+ brief: '显示 10 个舱室状态,点空舱进入入箱',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: 'operate 操作端首页,网格显示 10 个培养舱 + 缓冲瓶的实时状态(空闲/培养中/平衡中),顶栏显示报警数。是所有操作的入口。',
|
|
|
|
|
+ pre: ['operate 已登录', 'control 后台已启动并上报状态'],
|
|
|
|
|
+ trigger: 'operate 启动登录后 → MainWindow → LoadPage(MainPageView)',
|
|
|
|
|
+ steps: ['定时轮询每个舱室状态刷新舱格颜色/患者信息', '点空舱 → 弹"新建入箱"窗', '点有患者舱 → 进看图详情页', '点报警角标 → 报警页'],
|
|
|
|
|
+ backend: '定时轮询 GetHouseStateApi → 更新 10 个舱格 UI',
|
|
|
|
|
+ data: ['内存:MainPageViewModel 维护 10 个 HouseInfo', 'UI:舱格颜色随状态实时变'],
|
|
|
|
|
+ cross: { from: 'operate 显示舱室状态', via: 'HTTP 轮询 GetHouseStateApi', to: ['control 实时上报每舱温度/压力/有无培养皿 → operate 渲染'] },
|
|
|
|
|
+ code: ['operate MainPageView.xaml.cs:155', 'operate MainPageViewModel.cs(轮询)']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ========== operate:新建入箱 ==========
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'op-add', type: 'operate', icon: '📝', title: '新建患者入箱',
|
|
|
|
|
+ brief: '录入患者信息 + 选 16 孔位 + 受精方式',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: '点空舱弹出"新建患者"窗:在 16 孔圆盘上点选放胚胎的孔位,填夫妻姓名/病例号/周期/受精时间/受精方式(IVF/ICSI),可勾"重点关注"。',
|
|
|
|
|
+ pre: ['舱室=空闲', '有操作权限', '该舱硬件正常(串口/相机/电机)'],
|
|
|
|
|
+ trigger: 'operate 主界面点空舱格 → 弹 AddDishWindowView',
|
|
|
|
|
+ steps: ['在 16 孔圆盘点选胚胎孔位(可多选)', '填病例号、周期、夫妻姓名、出生日期、受精时间、受精方式', '可选勾"重点关注"(VIP)', '底部二选一:【平衡】或【保存=开始培养】'],
|
|
|
|
|
+ backend: 'operate 前端校验必填 → 点按钮分两条路(见下方两个分支)',
|
|
|
|
|
+ data: ['此步仅前端表单,未落库(点了"开始培养"才落库)'],
|
|
|
|
|
+ cross: null,
|
|
|
|
|
+ code: ['operate AddDishWindowView.xaml.cs:54 构造', 'operate AddDishWindowView.xaml.cs:451 StartDish_Click']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 分支判断:平衡 还是 直接开始 ==========
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'br-balance-or-start', type: 'branch', icon: '❓', title: '判断:先平衡?还是直接开始?',
|
|
|
|
|
+ brief: '入箱窗底部两个按钮 → 两条路',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: '新建入箱窗底部有两个按钮,对应两条不同的业务路径:点【平衡】先做环境预平衡,点【保存】直接开始培养。',
|
|
|
|
|
+ pre: ['已在新建入箱窗', '表单校验通过'],
|
|
|
|
|
+ trigger: '点底部按钮',
|
|
|
|
|
+ steps: ['【平衡】→ 走平衡流程(换气稳定环境,可反复)', '【保存=开始培养】→ 直接进培养'],
|
|
|
|
|
+ backend: '两个按钮分别触发 StartBalance_Click 或 StartDish_Click',
|
|
|
|
|
+ data: ['取决于走哪条分支'],
|
|
|
|
|
+ cross: null,
|
|
|
|
|
+ code: ['operate AddDishWindowView.xaml.cs:640 StartBalance_Click(平衡)', 'operate AddDishWindowView.xaml.cs:451 StartDish_Click(开始培养)']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 平衡分支 ==========
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'op-balance', type: 'operate', icon: '⚖️', title: '平衡分支:开启平衡',
|
|
|
|
|
+ brief: '换气预平衡,使舱内温压稳定',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: '可选分支。开启平衡后,control 对该舱反复换气,让舱内温度/气压达到稳定,再放胚胎培养,避免胚胎一进箱就遇环境冲击。',
|
|
|
|
|
+ pre: ['在入箱窗点了【平衡】', '舱室=空闲'],
|
|
|
|
|
+ trigger: 'AddDishWindowView 点【平衡】→ StartBalance_Click',
|
|
|
|
|
+ steps: ['调 StartBalance 接口创建 balance 记录', '后端发 MQTT StartBalance 给 control', 'control 收到 → HouseBin.StartBlance → 舱主循环走"平衡换气"', '医生手动点【结束平衡】后,再回入箱窗点【开始培养】'],
|
|
|
|
|
+ backend: 'StartBalance → /balance/startBalance → balance 表插入 → MqttSendRpc 发 MQTT StartBalance',
|
|
|
|
|
+ data: ['DB:balance 表插入一条', 'control 本地 SQLite:AddBalance', '内存:HouseBin.Balance 赋值,FirstAir=true', '舱状态:空闲 → 平衡中'],
|
|
|
|
|
+ cross: { from: 'operate 点【平衡】', via: 'StartBalance 接口 → balance 表 → MQTT StartBalance', to: ['control 收到 → HouseBin.StartBlance → 舱主循环执行平衡换气', 'front 设备管理:该舱显示"平衡中"', 'operate 主界面:该舱显示"平衡中"'] },
|
|
|
|
|
+ code: ['operate AddDishWindowView.xaml.cs:640', 'control AppData.cs:1066 StartBalance', 'control HouseBin.cs:2865 StartBlance', 'DB balance 表']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'op-end-balance', type: 'operate', icon: '⏹️', title: '结束平衡',
|
|
|
|
|
+ brief: '平衡够了,手动结束 → 回入箱',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: '平衡到合适程度,医生手动点【结束平衡】。control 停止平衡换气,舱室回空闲,医生再回入箱窗点【开始培养】正式入箱。',
|
|
|
|
|
+ pre: ['舱室正在平衡中'],
|
|
|
|
|
+ trigger: 'operate 点【结束平衡】→ StopBalance',
|
|
|
|
|
+ steps: ['调 StopBalance 接口结束 balance 记录', '发 MQTT EndBalance', 'control 收到 → HouseBin 停平衡', '回到入箱窗点【开始培养】'],
|
|
|
|
|
+ backend: 'StopBalance → /balance/stopBalance → balance 表更新 → MQTT EndBalance',
|
|
|
|
|
+ data: ['DB:balance 表 endTime', 'control 内存:HouseBin.Balance 置 null', '舱状态:平衡中 → 空闲'],
|
|
|
|
|
+ cross: { from: 'operate 点【结束平衡】', via: 'StopBalance → MQTT EndBalance', to: ['control 停平衡换气', '舱室回空闲,可正式入箱'] },
|
|
|
|
|
+ code: ['operate AddDishWindowViewModel.cs:164 StopBalance', 'control AppData.cs:1084 EndBalance']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ========== operate:开始培养(核心跨端点)==========
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'op-start-dish', type: 'operate', icon: '▶️', title: '开始培养(保存)',
|
|
|
|
|
+ brief: '落库 dish+embryo → 发 MQTT 通知 control',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: '入箱的核心动作。前端校验后调 StartDishApi,后端把培养皿+每个胚胎落库,再发 MQTT StartDish 命令通知 control 开始驱动硬件培养。',
|
|
|
|
|
+ pre: ['表单校验通过(孔位≥1、必填非空、受精时间合法)', '舱室=空闲', 'control 在线'],
|
|
|
|
|
+ trigger: 'operate 入箱窗点【保存=开始培养】→ StartDish_Click',
|
|
|
|
|
+ steps: ['前端校验所有必填项', '调 StartDishApi(POST /dish/start)', '后端落库:dish 表 1 条 + embryo 表 N 条(N=孔位数)', '后端发 MQTT StartDish → control'],
|
|
|
|
|
+ backend: 'StartDish_Click → StartDishApi → dish/embryo 落库 → MqttSendRpc 发 MQTT StartDish(topic=tl/command/{tlSn})',
|
|
|
|
|
+ data: ['DB:dish 表插 1 条(state=培养中),embryo 表插 N 条(state=0 待拍)', '舱状态:空闲 → 培养中'],
|
|
|
|
|
+ cross: { from: 'operate 点【开始培养】', via: 'StartDishApi → dish/embryo 落库 → MQTT StartDish', to: ['control 收 MQTT → AppData.StartDish → HouseBin.StartDish → 启动舱主循环', 'front 设备管理:舱格变"培养中"+患者信息', 'front 培养记录列表:新增一条', 'operate 主界面:舱格变色+显示患者'] },
|
|
|
|
|
+ code: ['operate AddDishWindowView.xaml.cs:451 StartDish_Click', 'control AppData.cs:1102 StartDish', 'control HouseBin.cs:2891 StartDish', 'DB dish/embryo 表']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ========== control:收到 StartDish ==========
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'ctl-recv', type: 'control', icon: '⚙️', title: 'control 收到 StartDish',
|
|
|
|
|
+ brief: 'MQTT 收命令 → 同步本地库 → 启动舱循环',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: 'control 后台通过 MQTT 收到 StartDish 命令,同步本地 SQLite,给 HouseBin 设置培养皿对象,启动该舱的主循环线程。',
|
|
|
|
|
+ pre: ['MQTT 连接正常', 'control 进程运行中', '该舱硬件已扫描登记'],
|
|
|
|
|
+ trigger: 'MQTT topic=tl/command/{tlSn}, type=StartDish',
|
|
|
|
|
+ steps: ['MqttService 收消息 → AppData.MqttMessage 按 type 分发', 'AppData.StartDish → DBService 同步 dish/embryo 到本地 SQLite', 'HouseBin.StartDish → 设 Dish、FirstClearest=true、清对焦历史'],
|
|
|
|
|
+ backend: 'AppData.cs:995 MqttMessage → case StartDish → AppData.cs:1102 StartDish → HouseBin.cs:2891 StartDish',
|
|
|
|
|
+ data: ['本地 SQLite:dish/embryo 同步插入', '内存:HouseBin.Dish 赋值,FirstClearest=true'],
|
|
|
|
|
+ cross: { from: 'control 收到 MQTT StartDish', via: '本地 SQLite 同步 + HouseBin 状态设置', to: ['本舱主循环开始运转(见下)', '不反向通知(operate/front 在落库时已刷新)'] },
|
|
|
|
|
+ code: ['control AppData.cs:995 MqttMessage', 'control AppData.cs:1102 StartDish', 'control HouseBin.cs:2891 StartDish']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ========== control:舱主循环(核心,分出三条路)==========
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'ctl-loop', type: 'control', icon: '🔁', title: '舱主循环 MainThread',
|
|
|
|
|
+ brief: '每舱一线程·看表轮询·分流到换气/对焦/拍照',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: 'control 的核心:每个舱一个独立线程死循环。每轮先读温压、判断舱门,再按"有皿/有平衡/都没有"分流,有皿时依次判断该换气、该对焦、该拍照。',
|
|
|
|
|
+ pre: ['HouseBin.Dish!=null(有皿) 或 Balance!=null(平衡)', '舱门关闭', '非调试/非前台借用'],
|
|
|
|
|
+ trigger: 'HouseBin.MainThread → Task.Factory.StartNew → while(true)',
|
|
|
|
|
+ steps: ['读温压 ParamFun,低压标记待补气', '舱门没关 → 跳过本轮', '有皿:判断换气间隔→换气;判断 FirstClearest→对焦;对焦成功→拍照', '有平衡:执行平衡换气', '都没有:空闲监测'],
|
|
|
|
|
+ backend: 'HouseBin.cs:614 MainThread,按 Dish/Balance 是否非空分三大分支',
|
|
|
|
|
+ data: ['内存:Pressure/Temperature 实时更新', 'FirstClearest:对焦成功后转 false', 'RunState:当前状态字符串'],
|
|
|
|
|
+ cross: { from: 'control 舱主循环持续运转', via: '温压采集 + 对焦/拍照触发', to: ['拍照结果经 Kafka → aivfo-oplog 入库 picture 表 → operate/front 轮询可见', '温压经 MQTT 上报 → operate/front 显示'] },
|
|
|
|
|
+ code: ['control HouseBin.cs:614 MainThread', 'HouseBin.cs:663 ParamFun', 'HouseBin.cs:696/704/726 三分支判断']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ----- 分支1:换气 -----
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'ctl-airswap', type: 'control', icon: '💨', title: '换气(AirSwapFun)',
|
|
|
|
|
+ brief: '到换气间隔 → 开排气阀冲刷 + 低压补气',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: '舱主循环分支之一。到达换气间隔(airSwapFrequency)就执行换气:开排气阀冲刷舱内气体若干轮,中途气压低就向缓冲瓶申请补气。',
|
|
|
|
|
+ pre: ['有皿或平衡中', '换气计时器 ≥ airSwapFrequency'],
|
|
|
|
|
+ trigger: '主循环判断 AirSwapTimeStopwatch ≥ 间隔 → AirSwapFun',
|
|
|
|
|
+ steps: ['开排气阀冲刷 N 轮', '气压低于下限 → AerationNew 补气(找缓冲瓶要气)', '记录换气审计日志', '换气完成,重置计时器'],
|
|
|
|
|
+ backend: 'HouseBin.cs:975 AirSwapFun / AirSwapQueueFun(排队换气经缓冲瓶)',
|
|
|
|
|
+ data: ['硬件:排气阀/进气阀动作', '内存:WorkingType=AirSwapWorking', '气压回升'],
|
|
|
|
|
+ cross: { from: 'control 换气', via: '与缓冲瓶 BufferBottleBin 排队握手', to: ['缓冲瓶供气', '换气状态经 MQTT 上报 operate/front'] },
|
|
|
|
|
+ code: ['control HouseBin.cs:975 AirSwapFun', 'HouseBin.cs:1138 AirSwapQueueFun', 'control BufferBottleBin.cs:136 缓冲瓶']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ----- 分支2:对焦 -----
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'ctl-focus', type: 'control', icon: '🔬', title: '自动对焦(GetClearest)',
|
|
|
|
|
+ brief: '逐孔四步标定,找最清晰 Z 位',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: '舱主循环分支之二。FirstClearest=true 时执行:遍历所有待拍 well,逐个跑四步标定算法找到最清晰焦平面 Z 位,存盘 calibration.json + 入库。',
|
|
|
|
|
+ pre: ['FirstClearest=true(首次或上次对焦失败)', '有 state=0 的 well', '舱门关闭'],
|
|
|
|
|
+ trigger: '主循环 → StartAutoFocus → GetClearest(AutoFocusThread)',
|
|
|
|
|
+ steps: ['筛选待拍 well:Dish.Embryo.Where(state==0)', '逐 well 调 AutoFocusWellAny 四步标定', '算法:粗扫→精扫→峰值判定→得最佳 FocusZ', '存盘 calibration.json + DB house_autofocus_calibration', '成功→FirstClearest=false;失败→保持 true 下轮重试'],
|
|
|
|
|
+ backend: 'HouseBin.cs:729 GetClearest → AutoFocusWellAny → CalibrationStore.SaveAsync',
|
|
|
|
|
+ data: ['calibration.json 追加对焦结果', 'DB house_autofocus_calibration 表', 'FirstClearest→false', 'LastAutoFocusTimeDic 记时间'],
|
|
|
|
|
+ cross: { from: 'control 自动对焦', via: 'calibration.json + DB 入库', to: ['对焦参数可被 operate/front 查询', '对焦成功后进入拍照'] },
|
|
|
|
|
+ code: ['control HouseBin.cs:729 GetClearest', 'control AutofocusTool 四步标定', 'control CalibrationStore', 'DB house_autofocus_calibration']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ----- 分支3:拍照 -----
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'ctl-photo', type: 'control', icon: '📸', title: '拍照(ccdThreadFun)',
|
|
|
|
|
+ brief: '逐孔逐层抓图 → 存盘 → 上传 Kafka',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: '舱主循环分支之三。对焦完成且到拍照间隔时执行:遍历所有待拍 well,逐孔逐焦层移动电机抓图,存盘本地,上传 Kafka。',
|
|
|
|
|
+ pre: ['对焦完成(FirstClearest=false)', '到拍照间隔(≥photoIntervalPerRound)', 'IsCCD=true(有 state=0 的 well)'],
|
|
|
|
|
+ trigger: '主循环 → StartCCD → ccdThreadFun',
|
|
|
|
|
+ steps: ['IsCCD 筛选待拍 well', '逐 well 逐层:移电机→相机抓图→存盘本地', '命名 {tlSn}/{houseSn}/{dishId}/{wellSn}/{时间}_{层}.bmp', '封装 ImageDTO → KafkaService 上传 tl-picture topic'],
|
|
|
|
|
+ backend: 'HouseBin.cs:2171 ccdThreadFun → CCDStart 逐孔逐层 → Camera.TakePicture → KafkaService.cs:115 kafkaProducerAsync',
|
|
|
|
|
+ data: ['本地磁盘:图片文件', 'Kafka:ImageDTO 消息', 'DB picture 表(由 aivfo-oplog 消费 Kafka 写入)'],
|
|
|
|
|
+ cross: { from: 'control 拍照上传 Kafka', via: 'Kafka tl-picture → aivfo-oplog 消费 → picture 表入库 + 文件转 OSS', to: ['operate 看图页:轮询拉取最新延时图', 'front 胚胎详情:轮询拉取延时回放'] },
|
|
|
|
|
+ code: ['control HouseBin.cs:2171 ccdThreadFun', 'control HouseBin.cs:1459 IsCCD', 'control KafkaService.cs:115', 'DB picture 表']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ========== operate:看图页 ==========
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'op-detail', type: 'operate', icon: '🎬', title: '看图页·延时回放',
|
|
|
|
|
+ brief: '16 孔圆盘 + 延时视频 + 胚胎去向操作',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: '点主界面有患者的舱进入看图页:16 孔圆盘选 well,播放该 well 从受精开始的延时缩影片(播放/暂停/倍速/逐帧/切焦层),底部 6 个操作按钮。',
|
|
|
|
|
+ pre: ['舱有培养皿(Dish.id>0)', '已拍至少一张图'],
|
|
|
|
|
+ trigger: 'operate 主界面点有患者舱 → DetailPageView,传 dishId',
|
|
|
|
|
+ steps: ['加载 GetDishPicAndVideoApi(dishId) → 16 well 图片/视频', '圆盘点 well 切换当前胚胎', '播放控制:播放/暂停/倍速/上下帧/拖进度条/切焦层', '底部 6 钮:移植/冷冻/删除/作废/结束培养/图片查看'],
|
|
|
|
|
+ backend: 'DetailPageViewModel.Init → GetDishPicAndVideoApi → mediaElement 播放',
|
|
|
|
|
+ data: ['内存:DishPicAndVideoList 保存 16 well 数据', 'UI:视频播放'],
|
|
|
|
|
+ cross: { from: 'operate 看图页', via: 'HTTP GetDishPicAndVideoApi 拉 picture 表', to: ['control 拍照上传的图经 Kafka→DB→operate 轮询拉到'] },
|
|
|
|
|
+ code: ['operate DetailPageView.xaml.cs:44 Loaded', 'operate DetailPageViewModel.cs Init', 'GetDishPicAndVideoApi']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 分支判断:胚胎去向 ==========
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'br-destination', type: 'branch', icon: '❓', title: '判断:胚胎去向?',
|
|
|
|
|
+ brief: '看图后决定每个胚胎去哪 → 四条路',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: '医生看完延时回放评估胚胎质量后,对每个胚胎决定去向。底部 4 个按钮对应 4 条独立分支:移植/冷冻/删除/作废。标记后 control 不再拍该 well。',
|
|
|
|
|
+ pre: ['在看图页', '当前 well 有胚胎', '胚胎 state=0', '培养未结束'],
|
|
|
|
|
+ trigger: '看图页底部点四选一按钮',
|
|
|
|
|
+ steps: ['【移植】state→1', '【冷冻】state→2', '【删除】state→3', '【作废】state→4'],
|
|
|
|
|
+ backend: '四个按钮共用 EmbryoMark → MarkEmbryoDestinationApi(id, state)',
|
|
|
|
|
+ data: ['取决于点哪个'],
|
|
|
|
|
+ cross: null,
|
|
|
|
|
+ code: ['operate DetailPageView.xaml.cs:294/370/443/517(移/冻/删/废)', 'operate DetailPageView.xaml.cs:795 EmbryoMark']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ----- 四个去向分支 -----
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'op-transplant', type: 'operate', icon: '🌱', title: '移植',
|
|
|
|
|
+ brief: 'state→1,胚胎用于移植',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: '把当前胚胎标记为"移植"。该 well 的 embryo.state 改为 1,control 收 MQTT 后不再对焦拍照该 well。',
|
|
|
|
|
+ pre: ['当前 well 有胚胎', 'state=0'],
|
|
|
|
|
+ trigger: '看图页点【移植】→ EmbryoMove_Click',
|
|
|
|
|
+ steps: ['弹确认窗', '调 MarkEmbryoDestinationApi(id, 1)', 'embryo 表 state→1', '发 MQTT EmbryoState'],
|
|
|
|
|
+ backend: 'EmbryoMove_Click → EmbryoMark(1) → MarkEmbryoDestinationApi → MQTT EmbryoState',
|
|
|
|
|
+ data: ['DB embryo 表:state→1', 'control 本地同步 + 内存更新', '舱主循环 IsCCD 跳过该 well'],
|
|
|
|
|
+ cross: { from: 'operate 点【移植】', via: 'MarkEmbryoDestinationApi → embryo 表 → MQTT EmbryoState', to: ['control HouseBin.ChangeEmbryoState → IsCCD 跳过该 well', 'front 胚胎详情:该 well 显"移植"标签', 'front 标记明细:新增记录', 'operate 看图页:该 well 显移植图标'] },
|
|
|
|
|
+ code: ['operate DetailPageView.xaml.cs:294 EmbryoMove_Click', 'control AppData.cs:1238 EmbryoState', 'DB embryo 表 state=1']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'op-freeze', type: 'operate', icon: '❄️', title: '冷冻',
|
|
|
|
|
+ brief: 'state→2,胚胎冷冻保存',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: '把当前胚胎标记为"冷冻"。embryo.state→2,control 不再拍该 well。',
|
|
|
|
|
+ pre: ['当前 well 有胚胎', 'state=0'],
|
|
|
|
|
+ trigger: '看图页点【冷冻】→ EmbryoFree_Click',
|
|
|
|
|
+ steps: ['弹确认窗', '调 MarkEmbryoDestinationApi(id, 2)', 'embryo 表 state→2', '发 MQTT EmbryoState'],
|
|
|
|
|
+ backend: 'EmbryoFree_Click → EmbryoMark(2) → MarkEmbryoDestinationApi → MQTT EmbryoState',
|
|
|
|
|
+ data: ['DB embryo 表:state→2', 'control 同步', '舱主循环跳过该 well'],
|
|
|
|
|
+ cross: { from: 'operate 点【冷冻】', via: 'MarkEmbryoDestinationApi → MQTT EmbryoState', to: ['control 跳过该 well', 'front/operate 显示"冷冻"标签'] },
|
|
|
|
|
+ code: ['operate DetailPageView.xaml.cs:370 EmbryoFree_Click', 'control AppData.cs:1238 EmbryoState', 'DB embryo 表 state=2']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'op-delete', type: 'operate', icon: '🗑️', title: '删除',
|
|
|
|
|
+ brief: 'state→3,胚胎删除',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: '把当前胚胎标记为"删除"。embryo.state→3,control 不再拍该 well。',
|
|
|
|
|
+ pre: ['当前 well 有胚胎', 'state=0'],
|
|
|
|
|
+ trigger: '看图页点【删除】→ EmbryoDel_Click',
|
|
|
|
|
+ steps: ['弹确认窗', '调 MarkEmbryoDestinationApi(id, 3)', 'embryo 表 state→3', '发 MQTT EmbryoState'],
|
|
|
|
|
+ backend: 'EmbryoDel_Click → EmbryoMark(3) → MarkEmbryoDestinationApi → MQTT EmbryoState',
|
|
|
|
|
+ data: ['DB embryo 表:state→3', 'control 同步', '舱主循环跳过该 well'],
|
|
|
|
|
+ cross: { from: 'operate 点【删除】', via: 'MarkEmbryoDestinationApi → MQTT EmbryoState', to: ['control 跳过该 well', 'front/operate 显示"删除"标签'] },
|
|
|
|
|
+ code: ['operate DetailPageView.xaml.cs:443 EmbryoDel_Click', 'control AppData.cs:1238 EmbryoState', 'DB embryo 表 state=3']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'op-invalid', type: 'operate', icon: '⛔', title: '作废',
|
|
|
|
|
+ brief: 'state→4,胚胎作废',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: '把当前胚胎标记为"作废"。embryo.state→4,control 不再拍该 well。',
|
|
|
|
|
+ pre: ['当前 well 有胚胎', 'state=0'],
|
|
|
|
|
+ trigger: '看图页点【作废】→ EmbryoInv_Click',
|
|
|
|
|
+ steps: ['弹确认窗', '调 MarkEmbryoDestinationApi(id, 4)', 'embryo 表 state→4', '发 MQTT EmbryoState'],
|
|
|
|
|
+ backend: 'EmbryoInv_Click → EmbryoMark(4) → MarkEmbryoDestinationApi → MQTT EmbryoState',
|
|
|
|
|
+ data: ['DB embryo 表:state→4', 'control 同步', '舱主循环跳过该 well'],
|
|
|
|
|
+ cross: { from: 'operate 点【作废】', via: 'MarkEmbryoDestinationApi → MQTT EmbryoState', to: ['control 跳过该 well', 'front/operate 显示"作废"标签'] },
|
|
|
|
|
+ code: ['operate DetailPageView.xaml.cs:517 EmbryoInv_Click', 'control AppData.cs:1238 EmbryoState', 'DB embryo 表 state=4']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ========== operate:结束培养 ==========
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'op-end', type: 'operate', icon: '🏁', title: '结束培养',
|
|
|
|
|
+ brief: '整皿收尾 → MQTT 停 control → 舱回空闲',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: '看图页点"结束培养",结束整皿所有胚胎培养。dish.state→1,control 停止对焦拍照,舱室回空闲,可再放新皿。',
|
|
|
|
|
+ pre: ['在看图页', 'dish.state=0(培养中)'],
|
|
|
|
|
+ trigger: '看图页点【结束培养】→ EmbryoOver_Click',
|
|
|
|
|
+ steps: ['弹确认窗', '调 EndDishApi(dishId)', 'dish 表 state→1,记 endTime', '发 MQTT EndDish'],
|
|
|
|
|
+ backend: 'EmbryoOver_Click → Finish → EndDishApi → dish 表更新 → MQTT EndDish → control HouseBin.StopDish',
|
|
|
|
|
+ data: ['DB dish 表:state→1, endTime', 'control 内存:HouseBin.Dish→null', '舱主循环回空闲监测', '舱状态:培养中→空闲'],
|
|
|
|
|
+ cross: { from: 'operate 点【结束培养】', via: 'EndDishApi → dish 表 → MQTT EndDish', to: ['control HouseBin.StopDish → 停对焦拍照', 'front 设备管理:舱格变空闲', 'front 培养记录:显"已结束"', 'operate 主界面:舱格变灰回空闲'] },
|
|
|
|
|
+ code: ['operate DetailPageView.xaml.cs:591 EmbryoOver_Click', 'control AppData.cs:1120 EndDish', 'control HouseBin.cs:2904 StopDish', 'DB dish 表']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 终点 ==========
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'op-back-main', type: 'start', icon: '🔚', title: '回主界面·舱位释放',
|
|
|
|
|
+ brief: '舱位空出,可放下一个患者',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: '结束培养后自动返回主界面,该舱格变回空闲灰色,整个培养周期结束。这个舱可以重新点击放入下一个患者的胚胎(回到流程起点)。',
|
|
|
|
|
+ pre: ['培养已结束'],
|
|
|
|
|
+ trigger: '结束培养后自动 BackMainPage',
|
|
|
|
|
+ steps: ['看图页自动返回主界面', '该舱格变灰=空闲', '可重新点击进入新一轮入箱'],
|
|
|
|
|
+ backend: 'AppData.BackMainPage',
|
|
|
|
|
+ data: ['UI:舱格回空闲'],
|
|
|
|
|
+ cross: null,
|
|
|
|
|
+ code: ['operate DetailPageView.xaml.cs:265 Return_Click → BackMainPage']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ========== front:管理端(旁路联动)==========
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'front-device', type: 'front', icon: '💻', title: 'front 设备管理',
|
|
|
|
|
+ brief: '医生 PC 端·跨设备查看所有舱',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: 'front 管理端 PC 的设备管理首页,跨设备显示所有时差仪 + 11 舱室状态。operate/control 的任何操作都会改 DB,front 轮询拉取实时反映。',
|
|
|
|
|
+ pre: ['front 已登录', '网络正常'],
|
|
|
|
|
+ trigger: 'front 导航点"设备管理" → DeviceManageView',
|
|
|
|
|
+ steps: ['轮询 GetDeviceStateApi 拉所有设备+舱室状态', '显示设备卡 + 11 舱格', '点舱格进胚胎详情'],
|
|
|
|
|
+ backend: 'DeviceManageViewModel 轮询 → GetDeviceStateApi',
|
|
|
|
|
+ data: ['UI:所有设备舱室状态'],
|
|
|
|
|
+ cross: { from: 'front 设备管理', via: 'HTTP 轮询 GetDeviceStateApi 读 DB', to: ['operate/control 任何操作(入箱/标记/结束)改 DB → front 轮询拉到变化'] },
|
|
|
|
|
+ code: ['front DeviceManageView.xaml.cs', 'GetDeviceStateApi']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'front-detail', type: 'front', icon: '💻', title: 'front 胚胎详情',
|
|
|
|
|
+ brief: '医生 PC 端·延时回放 + AI 报告 + 评分',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: 'front 的看图页,医生在 PC 端查看延时回放、AI 报告、胚胎评分、标记明细。和 operate 看图共用同一份 picture 数据。',
|
|
|
|
|
+ pre: ['从培养记录点进', 'picture 表有图'],
|
|
|
|
|
+ trigger: 'front 培养记录点某条 → DetailView',
|
|
|
|
|
+ steps: ['GetDishPicAndVideoApi(dishId)', '16 孔圆盘 + 延时视频', '查看 AI 报告/评分/标记明细'],
|
|
|
|
|
+ backend: 'DetailViewModel.Init → GetDishPicAndVideoApi',
|
|
|
|
|
+ data: ['UI:视频播放 + AI 报告'],
|
|
|
|
|
+ cross: { from: 'front 看图页', via: 'HTTP GetDishPicAndVideoApi', to: ['control 拍照→Kafka→DB→front 轮询拉到延时图'] },
|
|
|
|
|
+ code: ['front DetailView.xaml.cs', 'GetDishPicAndVideoApi']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 异常:报警上报 ==========
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'err-alarm', type: 'error', icon: '⚠️', title: '异常·报警上报',
|
|
|
|
|
+ brief: '温压/硬件/拍照异常 → 报警 + 短信',
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ desc: 'control 运行中检测到异常(温压超阈值/串口断/相机丢失/拍照连续失败/卡死)就上报报警,入 alarm 表,operate/front 显示,可选短信通知。不中止主流程。',
|
|
|
|
|
+ pre: ['control 运行中', '触发报警条件'],
|
|
|
|
|
+ trigger: 'control 各检测点 → ReportAlarm',
|
|
|
|
|
+ steps: ['检测异常(温压/硬件/拍照失败)', '组装 AlarmEntity', '调 ReportAlarmApi → alarm 表插入', '可选发短信'],
|
|
|
|
|
+ backend: 'control 各模块 → ReportAlarmApi → alarm 表 → 可选短信',
|
|
|
|
|
+ data: ['DB alarm 表:插入报警', 'operate/front 轮询显示'],
|
|
|
|
|
+ cross: { from: 'control 检测异常', via: 'ReportAlarmApi → alarm 表', to: ['operate 主界面:报警角标+数字', 'operate 报警页:报警列表', 'front 系统异常:报警历史', '可选短信通知联系人'] },
|
|
|
|
|
+ code: ['control AlarmService', 'control HouseBin 各检测点', 'ReportAlarmApi', 'DB alarm 表']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+// ============================================================
|
|
|
|
|
+// 连线(边):分支在这里真实分叉
|
|
|
|
|
+// type: internal(本端实线) / cross(跨端虚线绿) / error(异常点线红) / loop(回流点线红)
|
|
|
|
|
+// ============================================================
|
|
|
|
|
+const EDGES = [
|
|
|
|
|
+ // 主流程:起点 → 主界面 → 入箱 → 判断
|
|
|
|
|
+ { from: 'start', to: 'op-main', type: 'internal', label: '走到屏幕' },
|
|
|
|
|
+ { from: 'op-main', to: 'op-add', type: 'internal', label: '点空舱' },
|
|
|
|
|
+ { from: 'op-add', to: 'br-balance-or-start',type: 'internal', label: '填完表单' },
|
|
|
|
|
+
|
|
|
|
|
+ // 分支A:平衡(可反复)→ 结束平衡 → 回到开始培养
|
|
|
|
|
+ { from: 'br-balance-or-start',to: 'op-balance', type: 'internal', label: '①点【平衡】' },
|
|
|
|
|
+ { from: 'op-balance', to: 'op-end-balance', type: 'internal', label: '平衡稳定后' },
|
|
|
|
|
+ { from: 'op-end-balance', to: 'op-start-dish', type: 'internal', label: '回入箱·开始培养' },
|
|
|
|
|
+
|
|
|
|
|
+ // 分支B:直接开始培养
|
|
|
|
|
+ { from: 'br-balance-or-start',to: 'op-start-dish', type: 'internal', label: '②点【开始培养】' },
|
|
|
|
|
+
|
|
|
|
|
+ // 开始培养 → control(跨端)
|
|
|
|
|
+ { from: 'op-start-dish', to: 'ctl-recv', type: 'cross', label: 'MQTT StartDish' },
|
|
|
|
|
+ { from: 'ctl-recv', to: 'ctl-loop', type: 'internal', label: 'HouseBin.StartDish' },
|
|
|
|
|
+
|
|
|
|
|
+ // 舱主循环 → 三条分支(真实分叉)
|
|
|
|
|
+ { from: 'ctl-loop', to: 'ctl-airswap', type: 'internal', label: '①到换气间隔' },
|
|
|
|
|
+ { from: 'ctl-loop', to: 'ctl-focus', type: 'internal', label: '②首次/需对焦' },
|
|
|
|
|
+ { from: 'ctl-focus', to: 'ctl-photo', type: 'internal', label: '对焦成功' },
|
|
|
|
|
+ { from: 'ctl-loop', to: 'ctl-photo', type: 'internal', label: '③已对焦·到拍照间隔' },
|
|
|
|
|
+
|
|
|
|
|
+ // 三分支都回流到主循环(点线回流)
|
|
|
|
|
+ { from: 'ctl-airswap', to: 'ctl-loop', type: 'loop', label: '换气完→下轮' },
|
|
|
|
|
+ { from: 'ctl-photo', to: 'ctl-loop', type: 'loop', label: '拍完→下轮' },
|
|
|
|
|
+
|
|
|
|
|
+ // 拍照 → 看图(跨端:Kafka→DB→轮询)
|
|
|
|
|
+ { from: 'ctl-photo', to: 'op-detail', type: 'cross', label: 'Kafka→DB→轮询' },
|
|
|
|
|
+ { from: 'ctl-photo', to: 'front-detail', type: 'cross', label: 'Kafka→DB→轮询' },
|
|
|
|
|
+
|
|
|
|
|
+ // 看图 → 胚胎去向判断 → 四条分支
|
|
|
|
|
+ { from: 'op-detail', to: 'br-destination', type: 'internal', label: '评估完' },
|
|
|
|
|
+ { from: 'br-destination', to: 'op-transplant', type: 'internal', label: '移植' },
|
|
|
|
|
+ { from: 'br-destination', to: 'op-freeze', type: 'internal', label: '冷冻' },
|
|
|
|
|
+ { from: 'br-destination', to: 'op-delete', type: 'internal', label: '删除' },
|
|
|
|
|
+ { from: 'br-destination', to: 'op-invalid', type: 'internal', label: '作废' },
|
|
|
|
|
+
|
|
|
|
|
+ // 四去向 → 通知 control 跳过该 well(跨端)
|
|
|
|
|
+ { from: 'op-transplant', to: 'ctl-loop', type: 'cross', label: 'MQTT 跳过该well' },
|
|
|
|
|
+ { from: 'op-freeze', to: 'ctl-loop', type: 'cross', label: 'MQTT 跳过该well' },
|
|
|
|
|
+ { from: 'op-delete', to: 'ctl-loop', type: 'cross', label: 'MQTT 跳过该well' },
|
|
|
|
|
+ { from: 'op-invalid', to: 'ctl-loop', type: 'cross', label: 'MQTT 跳过该well' },
|
|
|
|
|
+
|
|
|
|
|
+ // 四去向 → 结束培养
|
|
|
|
|
+ { from: 'op-transplant', to: 'op-end', type: 'internal' },
|
|
|
|
|
+ { from: 'op-freeze', to: 'op-end', type: 'internal' },
|
|
|
|
|
+ { from: 'op-delete', to: 'op-end', type: 'internal' },
|
|
|
|
|
+ { from: 'op-invalid', to: 'op-end', type: 'internal' },
|
|
|
|
|
+
|
|
|
|
|
+ // 结束培养 → 停 control + 回主界面
|
|
|
|
|
+ { from: 'op-end', to: 'ctl-loop', type: 'cross', label: 'MQTT EndDish·停' },
|
|
|
|
|
+ { from: 'op-end', to: 'op-back-main', type: 'internal', label: '返回' },
|
|
|
|
|
+ { from: 'op-back-main', to: 'op-main', type: 'loop', label: '舱位释放·可放下一个' },
|
|
|
|
|
+
|
|
|
|
|
+ // front 旁路联动
|
|
|
|
|
+ { from: 'op-start-dish', to: 'front-device', type: 'cross', label: 'DB→front轮询' },
|
|
|
|
|
+ { from: 'front-device', to: 'front-detail', type: 'internal', label: '点舱格' },
|
|
|
|
|
+
|
|
|
|
|
+ // 异常:从拍照/换气/对焦旁出报警
|
|
|
|
|
+ { from: 'ctl-photo', to: 'err-alarm', type: 'error', label: '拍照失败' },
|
|
|
|
|
+ { from: 'ctl-airswap', to: 'err-alarm', type: 'error', label: '换气/温压异常' },
|
|
|
|
|
+ { from: 'err-alarm', to: 'op-main', type: 'error', label: '报警显示' },
|
|
|
|
|
+ { from: 'err-alarm', to: 'front-device', type: 'error', label: '报警显示' },
|
|
|
|
|
+];
|