diff --git a/src/main/java/a8k/app/hardware/driver/PipetteCtrlDriverV2.java b/src/main/java/a8k/app/hardware/driver/PipetteCtrlDriverV2.java index 1653a21..e5ea24a 100644 --- a/src/main/java/a8k/app/hardware/driver/PipetteCtrlDriverV2.java +++ b/src/main/java/a8k/app/hardware/driver/PipetteCtrlDriverV2.java @@ -333,6 +333,7 @@ public class PipetteCtrlDriverV2 { } public void pierceThroughBlock(ContainerCpyId containerCpyId, Integer containerPos) throws AppException { + log.info("pierceThroughBlock: containerCpyId={}, containerPos={}", containerCpyId, containerPos); callcmd(MId.PipetteMod, CmdId.pipette_pump_pierce_through, containerCpyId.toInteger(), containerPos); waitForMod(CmdId.pipette_pump_pierce_through); } diff --git a/src/main/java/a8k/app/service/analyzer/A8kEcodeAnalyzer.java b/src/main/java/a8k/app/service/analyzer/A8kEcodeAnalyzer.java index ab2435e..7dccc45 100644 --- a/src/main/java/a8k/app/service/analyzer/A8kEcodeAnalyzer.java +++ b/src/main/java/a8k/app/service/analyzer/A8kEcodeAnalyzer.java @@ -8,7 +8,9 @@ public class A8kEcodeAnalyzer { if (ecode.index >= A8kEcode.LOW_ERROR_HARDWARE_ERROR_START.index) { //部分底层错误,属于非必须停机的错误 return switch (ecode) { - case LOW_ERROR_PIPETTE_ERROR_TIP_POP_ERROR, LOW_ERROR_PIPETTE_ERROR_LLD_ERROR -> false; + case LOW_ERROR_PIPETTE_ERROR_TIP_POP_ERROR, + LOW_ERROR_PIPETTE_LLD_ERROR_NO_LIQUID_DETECTED, + LOW_ERROR_PIPETTE_ERROR_LLD_ERROR -> false; default -> true; }; } else { diff --git a/src/main/java/a8k/app/service/appsetup/A8kSubModuleRegInitService.java b/src/main/java/a8k/app/service/appsetup/A8kSubModuleRegInitService.java index 8dc46f4..a667477 100644 --- a/src/main/java/a8k/app/service/appsetup/A8kSubModuleRegInitService.java +++ b/src/main/java/a8k/app/service/appsetup/A8kSubModuleRegInitService.java @@ -48,6 +48,10 @@ public class A8kSubModuleRegInitService { private void onAppEvent(AppEvent event) { if (event instanceof A8kCanBusOnConnectEvent) { + if (gStateMgrService.isDeviceInited()) { + log.warn("board param already inited, skip init it again"); + return; + } var initThread = new Thread(this::initModuleRegVal); initThread.setName("initModuleRegVal"); initThread.start(); diff --git a/src/main/java/a8k/app/service/lowerctrl/LiquidOperationCtrService.java b/src/main/java/a8k/app/service/lowerctrl/LiquidOperationCtrService.java index 21eae89..5ec1c6f 100644 --- a/src/main/java/a8k/app/service/lowerctrl/LiquidOperationCtrService.java +++ b/src/main/java/a8k/app/service/lowerctrl/LiquidOperationCtrService.java @@ -111,7 +111,7 @@ public class LiquidOperationCtrService { pipetteCtrlDriverV2.distributeAllBlock(new DistribuAllParam( probeSubstanceContainerPos.z, ContainerCpyId.DetectSubstancesCup, - LiquidConfigCpyIdx.NotSet, + LiquidConfigCpyIdx.Default, true, DistribuType.JET_DIST, 0, @@ -163,7 +163,7 @@ public class LiquidOperationCtrService { ul * 10, sampleContainerPos.z, a8kSamplePosToContainerCpyId(from), - LiquidConfigCpyIdx.NotSet, + LiquidConfigCpyIdx.Default, 0, 1, 0, @@ -191,7 +191,7 @@ public class LiquidOperationCtrService { pircePos = hbotLittleBSPosMgr.getLittleBSPiercePos(pos.group, pos.index); containerCpyId = ContainerCpyId.LittleBufferCup; } - + hbotMoveExCtrlService.moveToXY(pircePos); pipetteCtrlDriverV2.pierceThroughBlock(containerCpyId, pircePos.z); } diff --git a/src/main/java/a8k/app/service/module/SamplePreProcessModule.java b/src/main/java/a8k/app/service/module/SamplePreProcessModule.java index 3ee45d2..8ab4f97 100644 --- a/src/main/java/a8k/app/service/module/SamplePreProcessModule.java +++ b/src/main/java/a8k/app/service/module/SamplePreProcessModule.java @@ -154,17 +154,23 @@ public class SamplePreProcessModule { // 预定孵育盘位置 for (var cxt : tube.getPreProcessContexts()) { //预定孵育盘位置 - cxt.incubatorPos = incubationPlateStateMgr.takeOneIncubationIDLEPos((IncubatorPos pos) -> - incubationPlateStateMgr.syncCxtInfo(pos, cxt.sampleInfo, cxt.projBuildinInfo, cxt.projExtInfoCard, cxt.consumableInfo) + var incubatorPos = incubationPlateStateMgr.takeOneIncubationIDLEPos((IncubatorPos pos) -> + incubationPlateStateMgr.syncCxtInfo(pos, cxt.getSampleInfo(), cxt.getProjBuildinInfo(), cxt.getProjExtInfoCard(), cxt.getConsumableInfo()) ); - //使用耗材 - consumablesMgrService.useReserveConsumable(cxt.consumableInfo); + tubeStateMgrService.setTubeCxtIncubationPos(cxt,incubatorPos); + consumablesMgrService.useReserveConsumable(cxt.getConsumableInfo()); } tubeStateMgrService.changeTubeStateToResourceIsReady(); startTask(); actionTaskPool.waitAllDone(); + if (actionTaskPool.isHasError()) { + sampleProcessRollback(); + samplePrepareRollback(); + } + + AppError samplePrepareAppError = actionTaskPool.getError(TaskLine.SamplePrepare); if (samplePrepareAppError != null && A8kEcodeAnalyzer.isFatalError(samplePrepareAppError)) { throw AppException.of(samplePrepareAppError); @@ -177,8 +183,6 @@ public class SamplePreProcessModule { if (samplePrepareAppError != null || sampleProcessAppError != null) { //发生错误,进行回滚 - sampleProcessRollback(); - samplePrepareRollback(); tubeStateMgrService.changeTubeStateToError(samplePrepareAppError, sampleProcessAppError); } else { // 样本处理完成 @@ -190,6 +194,7 @@ public class SamplePreProcessModule { void startTask() { actionTaskPool.resetState(); + actionTaskPool.pushTask(TaskLine.SamplePrepare, () -> { Tube tube = tubeStateMgrService.getCurProcessingTube(); TubeHolder tubeHolder = tubeStateMgrService.getTubeHolder(); @@ -233,34 +238,38 @@ public class SamplePreProcessModule { List cxts = tube.getPreProcessContexts(); //! 等待样本准备完成 - sampleIsReadyCondition.waitTrue(); for (ProjectPreProcessContext cxt : cxts) { + Assert.isTrue(cxt.getIncubatorPos() != null, "cxt.incubatorPos != null"); + boolean finalCxt = cxt.equals(cxts.getLast()); PreReactionPos preReactionPos = ProjectParamUtils.getPreReactionPos(tubeHolder, tube, cxt); - A8kReactionFlowType reactionFlowType = cxt.projBuildinInfo.reactionFlowType; + A8kReactionFlowType reactionFlowType = cxt.getProjBuildinInfo().reactionFlowType; A8kSamplePos samplePos = ProjectParamUtils.getSamplePos(tubeHolder, tube); Integer sampleVol = ProjectParamUtils.getSampleUl(cxt); - liquidOperationCtrService.setProjContext(cxt.projBuildinInfo, cxt.projExtInfoCard); + liquidOperationCtrService.setProjContext(cxt.getProjBuildinInfo(), cxt.getProjExtInfoCard()); //预处理 if (reactionFlowType.equals(A8kReactionFlowType.SampleAndBSAndProbeSubstance)) { UISender.txInfoMsg(log, "取大瓶缓冲液到探测物质中"); - LargeBufferPos largeBufferPos = LargeBufferPos.of(cxt.consumableInfo.group); - Integer largeBSVolume = cxt.projBuildinInfo.bigBufferSampleUl; + LargeBufferPos largeBufferPos = LargeBufferPos.of(cxt.getConsumableInfo().group); + Integer largeBSVolume = cxt.getProjBuildinInfo().bigBufferSampleUl; Assert.notNull(largeBufferPos, "largeBufferPos != null"); docmd("取大瓶缓冲液", () -> { liquidOperationCtrService.takeLargeBottleBufferLiquidToProbeSubstance(largeBufferPos, preReactionPos, largeBSVolume); }); + } else if (reactionFlowType.equals(A8kReactionFlowType.SampleAndBS)) { + docmd("刺破小瓶缓冲液", () -> liquidOperationCtrService.pirceLittleBuffer(preReactionPos)); } + sampleIsReadyCondition.waitTrue(); + //取样本到小缓冲瓶或者探测物质 if (reactionFlowType.equals(A8kReactionFlowType.SampleAndBS)) { - docmd("刺破小瓶缓冲液", () -> liquidOperationCtrService.pirceLittleBuffer(preReactionPos)); UISender.txInfoMsg(log, "取样"); docmd("取样", () -> liquidOperationCtrService.takeSampleToPreReactionPos(samplePos, preReactionPos, sampleVol)); } else if (reactionFlowType.equals(A8kReactionFlowType.SampleAndBSAndProbeSubstance)) { @@ -274,9 +283,9 @@ public class SamplePreProcessModule { liquidOperationCtrService.takePreReactionLiquid(preReactionPos); //开始孵育 UISender.txInfoMsg(log, "开始孵育"); - Integer incubatedTimeSec = cxt.projBuildinInfo.reactionPlateIncubationTimeMin * 60; + Integer incubatedTimeSec = cxt.getProjBuildinInfo().reactionPlateIncubationTimeMin * 60; incubationPlateCtrlModule.dropLiquidAndStartIncubating( - cxt.incubatorPos, + cxt.getIncubatorPos(), () -> docmd("取反应液到孵育盘", liquidOperationCtrService::dropLiquidToReactionPlate), incubatedTimeSec ); diff --git a/src/main/java/a8k/app/service/statemgr/IncubationPlateStateMgr.java b/src/main/java/a8k/app/service/statemgr/IncubationPlateStateMgr.java index e00224b..119511e 100644 --- a/src/main/java/a8k/app/service/statemgr/IncubationPlateStateMgr.java +++ b/src/main/java/a8k/app/service/statemgr/IncubationPlateStateMgr.java @@ -144,6 +144,7 @@ public class IncubationPlateStateMgr { return subtank.getPos(); } } + log.warn("孵育盘没有空闲位置可用"); return null; } diff --git a/src/main/java/a8k/app/service/statemgr/TubeStateMgr.java b/src/main/java/a8k/app/service/statemgr/TubeStateMgr.java index 614f52a..8611a55 100644 --- a/src/main/java/a8k/app/service/statemgr/TubeStateMgr.java +++ b/src/main/java/a8k/app/service/statemgr/TubeStateMgr.java @@ -4,6 +4,7 @@ import a8k.app.dao.type.db.DeviceStatistic; import a8k.app.service.analyzer.ConsumableStateAnalyzerService; import a8k.app.type.DeviceRunMode; import a8k.app.type.a8k.pos.ConsumableInfo; +import a8k.app.type.a8k.pos.IncubatorPos; import a8k.app.type.a8k.state.*; import a8k.app.type.error.AEConsumeNotEnoughError; import a8k.app.type.a8k.proj.ProjBriefInfo; @@ -238,7 +239,7 @@ public class TubeStateMgr { //try delete the last emergency sample if (tube.getState().isEq(TubeState.TO_BE_PROCESSED)) { for (ProjectPreProcessContext cxt : tube.getPreProcessContexts()) { - consumablesMgrService.bakReserveConsumable(cxt.consumableInfo); + consumablesMgrService.bakReserveConsumable(cxt.getConsumableInfo()); } deleteSampleInfo(tube.getSampleId()); //delete sample info } @@ -279,14 +280,42 @@ public class TubeStateMgr { String sampleId = tube.getSampleId(); if (sampleId != null && !sampleId.isEmpty()) { for (ProjectPreProcessContext cxt : tube.getPreProcessContexts()) { - consumablesMgrService.bakReserveConsumable(cxt.consumableInfo); + consumablesMgrService.bakReserveConsumable(cxt.getConsumableInfo()); } deleteSampleInfo(tube.getSampleId()); //delete sample info } throw e; } + } + synchronized public void setTubeCxtIncubationPos(ProjectPreProcessContext cxt, IncubatorPos pos) { + if (cxt == null || pos == null) { + log.error("设置试管预处理上下文孵育盘位置失败,cxt或pos为空"); + return; + } + if (emergencyTubePos.tube != null) { + for (ProjectPreProcessContext context : emergencyTubePos.tube.getPreProcessContexts()) { + if (context.getCxtId().equals(cxt.getCxtId())) { + context.setIncubatorPos(pos); + log.info("设置急诊试管孵育盘预定位置,pos:{}", pos); + return; + } + } + } + for (Tube tube : tubeHolder.getTubes()) { + if (tube == null) { + continue; + } + for (ProjectPreProcessContext context : tube.getPreProcessContexts()) { + if (context.getCxtId().equals(cxt.getCxtId())) { + context.setIncubatorPos(pos); + log.info("设置试管{}孵育盘预定位置, pos:{}", tube.getPos(), pos); + return; + } + } + } + log.error("设置试管预处理上下文孵育盘位置失败,未找到对应的上下文 cxtId:{}", cxt.getCxtId()); } @@ -380,6 +409,7 @@ public class TubeStateMgr { curProcessingTube.setState(TubeState.RESOURCE_IS_READY); } + synchronized public void changeTubeStateToProcessing() { log.info("试管 状态->处理中 SampleId:{}", curProcessingTube.getSampleId()); curProcessingTube.setState(TubeState.PROCESSING); @@ -471,7 +501,7 @@ public class TubeStateMgr { private void bakReserveConsumable(TubeHolder tubeHolder) { for (Tube tube : tubeHolder.getTubes()) { for (ProjectPreProcessContext cxt : tube.getPreProcessContexts()) { - consumablesMgrService.bakReserveConsumable(cxt.consumableInfo); + consumablesMgrService.bakReserveConsumable(cxt.getConsumableInfo()); } } } diff --git a/src/main/java/a8k/app/type/a8k/pos/IncubatorPos.java b/src/main/java/a8k/app/type/a8k/pos/IncubatorPos.java index 9bf978f..128f57e 100644 --- a/src/main/java/a8k/app/type/a8k/pos/IncubatorPos.java +++ b/src/main/java/a8k/app/type/a8k/pos/IncubatorPos.java @@ -1,5 +1,7 @@ package a8k.app.type.a8k.pos; +import org.springframework.util.Assert; + /** * 孵育盘位置 1->20 */ @@ -37,6 +39,7 @@ public enum IncubatorPos { return pos; } } + Assert.isTrue(off >= 0 && off < 20, "IncubatorPos off must be in [0, 19]"); return null; } } diff --git a/src/main/java/a8k/app/type/a8k/state/ProjectPreProcessContext.java b/src/main/java/a8k/app/type/a8k/state/ProjectPreProcessContext.java index b4799c4..d510c43 100644 --- a/src/main/java/a8k/app/type/a8k/state/ProjectPreProcessContext.java +++ b/src/main/java/a8k/app/type/a8k/state/ProjectPreProcessContext.java @@ -5,31 +5,39 @@ import a8k.app.dao.type.db.ProjExtInfoCard; import a8k.app.type.a8k.pos.ConsumableInfo; import a8k.app.type.a8k.pos.IncubatorPos; import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import lombok.Getter; import java.io.Serializable; +@Data public class ProjectPreProcessContext implements Serializable { - public String cxtId = ""; - public SampleInfo sampleInfo = null; - public ConsumableInfo consumableInfo = null; //耗材信息 - public Integer projId = 0; //项目ID + private String cxtId = ""; + private SampleInfo sampleInfo = null; + private ConsumableInfo consumableInfo = null; //耗材信息 + private Integer projId = 0; //项目ID @JsonIgnore - public ProjBuildInInfo projBuildinInfo; + private ProjBuildInInfo projBuildinInfo; @JsonIgnore - public ProjExtInfoCard projExtInfoCard; - public IncubatorPos incubatorPos = null; //孵育盘位置 + private ProjExtInfoCard projExtInfoCard; + private IncubatorPos incubatorPos = null; //孵育盘位置 + private Integer off = 0; //项目在样本上的偏移量 + + static private long uuid = 0; public ProjectPreProcessContext(SampleInfo sampleInfo, ConsumableInfo consumableInfo, Integer projId, ProjBuildInInfo projBuildinInfo, ProjExtInfoCard projExtInfoCard, Integer off) { - this.cxtId = String.format("{}-{}-{}", sampleInfo.sampleId, projId, off); + this.cxtId = String.format("%s-%s-%s-%d", sampleInfo.sampleId, projId, off, uuid++); this.sampleInfo = sampleInfo; this.consumableInfo = consumableInfo; this.projId = projId; this.projBuildinInfo = projBuildinInfo; this.projExtInfoCard = projExtInfoCard; + this.incubatorPos = null; //孵育盘位置 + this.off = off; } public ProjectPreProcessContext() { diff --git a/src/main/java/a8k/app/utils/ActionTaskPool.java b/src/main/java/a8k/app/utils/ActionTaskPool.java index a53abdc..a0a511f 100644 --- a/src/main/java/a8k/app/utils/ActionTaskPool.java +++ b/src/main/java/a8k/app/utils/ActionTaskPool.java @@ -30,6 +30,8 @@ public class ActionTaskPool { Map, AppError> errorMap = new HashMap<>(); + List boolConditions = new ArrayList<>(); + public ActionTaskPool(Integer threadNum) { executor = new ThreadPoolExecutor(threadNum, threadNum, 0, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(10)); } @@ -51,13 +53,18 @@ public class ActionTaskPool { } public BoolCondition createBoolCondition(String name) { - return new BoolCondition(name, () -> errorFlag); + var boolCondition = new BoolCondition(name, () -> errorFlag); + boolConditions.add(boolCondition); + return boolCondition; } public void resetState() { errorFlag = false; errorMap.clear(); futures.clear(); + for (var boolCondition : boolConditions) { + boolCondition.clear(); + } } public AppError getError(Enum taskLineMaskter) { diff --git a/src/main/java/a8k/app/utils/ProjectParamUtils.java b/src/main/java/a8k/app/utils/ProjectParamUtils.java index 24a04ed..7d85464 100644 --- a/src/main/java/a8k/app/utils/ProjectParamUtils.java +++ b/src/main/java/a8k/app/utils/ProjectParamUtils.java @@ -40,18 +40,18 @@ public class ProjectParamUtils { } static public PreReactionPos getPreReactionPos(TubeHolder tubeHolder, Tube tube, ProjectPreProcessContext cxt) { - A8kReactionFlowType reactionFlowType = cxt.projBuildinInfo.reactionFlowType; + A8kReactionFlowType reactionFlowType = cxt.getProjBuildinInfo().reactionFlowType; PreReactionPos preReactionPos = new PreReactionPos(); switch (reactionFlowType) { case SampleAndBS -> { preReactionPos.type = ConsumableType.SmallBottleBuffer; - preReactionPos.group = cxt.consumableInfo.group; - preReactionPos.index = cxt.consumableInfo.pos; + preReactionPos.group = cxt.getConsumableInfo().group; + preReactionPos.index = cxt.getConsumableInfo().pos; } case SampleAndBSAndProbeSubstance -> { preReactionPos.type = ConsumableType.ProbeSubstance; - preReactionPos.group = cxt.consumableInfo.group; - preReactionPos.index = cxt.consumableInfo.pos; + preReactionPos.group = cxt.getConsumableInfo().group; + preReactionPos.index = cxt.getConsumableInfo().pos; } } return preReactionPos; @@ -59,17 +59,17 @@ public class ProjectParamUtils { static public Integer getSampleUl(ProjectPreProcessContext cxt) { Integer sampleUl = 0; - switch (cxt.sampleInfo.bloodType) { + switch (cxt.getSampleInfo().bloodType) { case WHOLE_BLOOD -> { - sampleUl = cxt.projBuildinInfo.wBloodSampleVolUl; - if (cxt.projExtInfoCard.wBloodSampleVolUl > 0) { - sampleUl = cxt.projExtInfoCard.wBloodSampleVolUl; + sampleUl = cxt.getProjBuildinInfo().wBloodSampleVolUl; + if (cxt.getProjExtInfoCard().wBloodSampleVolUl > 0) { + sampleUl = cxt.getProjExtInfoCard().wBloodSampleVolUl; } } case SERUM_OR_PLASMA -> { - sampleUl = cxt.projBuildinInfo.serumSampleVolUl; - if (cxt.projExtInfoCard.serumSampleVolUl > 0) { - sampleUl = cxt.projExtInfoCard.serumSampleVolUl; + sampleUl = cxt.getProjBuildinInfo().serumSampleVolUl; + if (cxt.getProjExtInfoCard().serumSampleVolUl > 0) { + sampleUl = cxt.getProjExtInfoCard().serumSampleVolUl; } } } ; diff --git a/src/main/java/a8k/extui/page/driver/pipette_module/PipetteGunTestCtrlPage.java b/src/main/java/a8k/extui/page/driver/pipette_module/PipetteGunTestCtrlPage.java index eeaaa43..c87c896 100644 --- a/src/main/java/a8k/extui/page/driver/pipette_module/PipetteGunTestCtrlPage.java +++ b/src/main/java/a8k/extui/page/driver/pipette_module/PipetteGunTestCtrlPage.java @@ -1,5 +1,6 @@ package a8k.extui.page.driver.pipette_module; +import a8k.OS; import a8k.app.hardware.driver.PipetteCtrlDriverV2; import a8k.app.hardware.type.pipette_module.cpyidx.ContainerCpyId; import a8k.app.hardware.type.pipette_module.cpyidx.LiquidConfigCpyIdx; @@ -30,6 +31,7 @@ public class PipetteGunTestCtrlPage { private Integer containerPos = 0; // 默认容器位置 @Setter private ContainerCpyId containerCpyId = ContainerCpyId.Default; // 默认容器配置索引 + private Boolean breakFlag = false; public ExtApiCurve getAspPressureRecord() throws AppException { var datas = pipetteCtrlDriverV2.getAspPressureData(); @@ -48,6 +50,50 @@ public class PipetteGunTestCtrlPage { return getAspPressureRecord(); } + public void breakPipetteTestPumpMoveToX100nlV2() { + breakFlag = true; + } + + public ExtApiCurve pipetteTestPumpMoveToX100nlV2(Integer x100nl, PMVCpyIdx vcpyidx) throws AppException { + pipetteCtrlDriverV2.pipetteTestPumpMoveToX100nl(x100nl, vcpyidx.toInteger()); + pipetteCtrlDriverV2.zMotorEnable(1); + pipetteCtrlDriverV2.zMotorMoveByBlock(-200); + pipetteCtrlDriverV2.zMotorEnable(0); + breakFlag = false; + + List pressures = new ArrayList<>(); + for (int i = 0; i < 1000; i++) { + Integer pressure = pipetteCtrlDriverV2.readPressure(); + pressures.add(pressure); + log.info("read pressure:{}/{}", i, 1000); + OS.forceSleep(10); + if (breakFlag) { + break; + } + } + List aspPressures = pipetteCtrlDriverV2.getAspPressureData(); + + List points = new ArrayList<>(); + int i = 0; + for (Integer pressure : aspPressures) { + points.add(new Object[]{i, pressure}); + log.info("AspPressure: {}", pressure); + i++; + } + points.add(new Object[]{i++, 3000}); + points.add(new Object[]{i++, 3000}); + points.add(new Object[]{i++, 3000}); + + for (Integer pressure : pressures) { + points.add(new Object[]{i, pressure}); + log.info("AfterAspPressure: {}", pressure); + i++; + } + + + return CurveBuilder.buidCurve("压力曲线", "value", "value", 1500, 3100, points); + } + public void pipetteTestLld(LiquidConfigCpyIdx liquidCpyId) throws AppException { pipetteCtrlDriverV2.pipetteTestLld(containerPos, containerCpyId, liquidCpyId); } @@ -97,6 +143,8 @@ public class PipetteGunTestCtrlPage { page.addFunction("设置容器配置索引", this::setContainerCpyId).setParamVal("containerCpyId", () -> containerCpyId); page.newGroup(" 操作"); page.addFunction("单元测试-测试泵机移动到X100nl", this::pipetteTestPumpMoveToX100nl); + page.addFunction("单元测试-测试泵机移动到X100nl-V2", this::pipetteTestPumpMoveToX100nlV2); + page.addFunction("单元测试-中断-泵机移动到X100nl-V2", this::breakPipetteTestPumpMoveToX100nlV2); page.addFunction("单元测试-测试泵机LLD", this::pipetteTestLld); page.addFunction("泵机移动到容器底部", this::pipetteTestMoveToContainerBottom);