diff --git a/src/main/java/a8k/app/hardware/driver/PipetteCtrlDriverV2.java b/src/main/java/a8k/app/hardware/driver/PipetteCtrlDriverV2.java index 342687e..1653a21 100644 --- a/src/main/java/a8k/app/hardware/driver/PipetteCtrlDriverV2.java +++ b/src/main/java/a8k/app/hardware/driver/PipetteCtrlDriverV2.java @@ -18,6 +18,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import java.util.ArrayList; +import java.util.List; + @Component @RequiredArgsConstructor @Slf4j @@ -444,6 +447,24 @@ public class PipetteCtrlDriverV2 { return callcmd(MId.PipetteMod, CmdId.pipette_read_capacitance).getContentI32(0); } + + public List getAspPressureData() throws AppException { + List pressuredata = new ArrayList<>(); + for (int i = 0; ; i++) { + A8kPacket packet = callcmd(MId.PipetteMod, CmdId.pipette_get_asp_pressure_data, i); + if (packet.getDataLen() == 0) { + break; + } + var content = packet.getContentI16Array(); + for (Integer val : content) { + pressuredata.add(val); + } + } + return pressuredata; + } + + + // // UTILS // diff --git a/src/main/java/a8k/app/hardware/type/A8kPacket.java b/src/main/java/a8k/app/hardware/type/A8kPacket.java index 42bea3c..4ba2c9f 100644 --- a/src/main/java/a8k/app/hardware/type/A8kPacket.java +++ b/src/main/java/a8k/app/hardware/type/A8kPacket.java @@ -33,7 +33,7 @@ public class A8kPacket { /** * * @WARNING - * 1. 修改这里时,需要注意连同createPacket一起修改 + * 1. 修改这里时,需要注意连同createPacket一起修改 * 2. PACKET_MIN_LEN 比Header多一个字节,是因为还有一个字节的校验位 */ @@ -99,6 +99,15 @@ public class A8kPacket { return cmdcontent; } + public int[] getContentI16Array() { + int[] ret = new int[getDataLen() / 2]; + for (int i = 0; i < ret.length; i++) { + ret[i] = ByteArray.readS16bit(raw, DATA_OFFSET + i * 2); + } + return ret; + } + + public int getContentI32(int index) { return ByteArray.read32bit(raw, DATA_OFFSET + index * 4); } @@ -220,7 +229,7 @@ public class A8kPacket { } public static void main(String[] args) { - var packet = createPacket(41, A8kPacket.PACKET_TYPE_CMD, CmdId.module_stop.index, new Integer[]{}); + var packet = createPacket(41, A8kPacket.PACKET_TYPE_CMD, CmdId.module_stop.index, new Integer[]{}); logger.info("{}", packet.toByteString()); } diff --git a/src/main/java/a8k/app/hardware/type/CmdId.java b/src/main/java/a8k/app/hardware/type/CmdId.java index c879dd5..be877df 100644 --- a/src/main/java/a8k/app/hardware/type/CmdId.java +++ b/src/main/java/a8k/app/hardware/type/CmdId.java @@ -203,6 +203,7 @@ public enum CmdId { pipette_read_capacitance(0x758D, "pipette_read_capacitance"), pipette_pump_distribu_all_set_param(0x758E, "pipette_pump_distribu_all_set_param"), // {paramid,val}, ack:{} pipette_pump_distribu_all(0x758F, "pipette_pump_distribu_all"), // {}, ack:{} + pipette_get_asp_pressure_data(0x7590, "pipette_get_asp_pressure_data"), // {section_off}, ack:{int16_t data[]} pipette_test_pump_move_to_x100nl(0x7600, "pipette_test_pump_move_to_x100nl"), // int32_t x100nl, int32_t vcfgindex diff --git a/src/main/java/a8k/app/hardware/type/pipette_module/cfg/PipetteCommonConfigIndex.java b/src/main/java/a8k/app/hardware/type/pipette_module/cfg/PipetteCommonConfigIndex.java index b404f89..229065a 100644 --- a/src/main/java/a8k/app/hardware/type/pipette_module/cfg/PipetteCommonConfigIndex.java +++ b/src/main/java/a8k/app/hardware/type/pipette_module/cfg/PipetteCommonConfigIndex.java @@ -4,6 +4,7 @@ public enum PipetteCommonConfigIndex { pressureRecordEnable, platformInfoCpyid, eachActionDelayTime, + aspiratePressureReportExtendTime, // 吸液压力采集延长时间,吸液完成后,多采集压力的时间 mark; public Integer toInteger() { diff --git a/src/main/java/a8k/app/hardware/type/pipette_module/cfgbean/PipetteCommonConfig.java b/src/main/java/a8k/app/hardware/type/pipette_module/cfgbean/PipetteCommonConfig.java index 469f700..9b0c1f4 100644 --- a/src/main/java/a8k/app/hardware/type/pipette_module/cfgbean/PipetteCommonConfig.java +++ b/src/main/java/a8k/app/hardware/type/pipette_module/cfgbean/PipetteCommonConfig.java @@ -2,6 +2,7 @@ package a8k.app.hardware.type.pipette_module.cfgbean; import a8k.app.hardware.type.pipette_module.cfg.PipetteCommonConfigIndex; import a8k.app.type.exception.AppException; +import io.swagger.v3.oas.models.security.SecurityScheme; public class PipetteCommonConfig { @@ -23,10 +24,11 @@ public class PipetteCommonConfig { // // lld // - public Integer pressureRecordEnable = 0; // 是否记录压力数据 - public Integer platformInfoCpyid = 0; // 平台信息配置索引,当调用initPumpDevice时,如果传入的参数为-1,则使用platformInfoMCpyid - public Integer eachActionDelayTime = 0; // 每个动作之间的延时,单位ms (动作调试使用,方便观察液体变化情况) - public Integer mark = 0; // 结构体最后一个数值,设置9973,用于保证单片机端和java端均正确更新了枚举 + public Integer pressureRecordEnable = 0; // 是否记录压力数据 + public Integer platformInfoCpyid = 0; // 平台信息配置索引,当调用initPumpDevice时,如果传入的参数为-1,则使用platformInfoMCpyid + public Integer eachActionDelayTime = 0; // 每个动作之间的延时,单位ms (动作调试使用,方便观察液体变化情况) + public Integer aspiratePressureReportExtendTime = 0; // 吸液压力采集延长时间,吸液完成后,多采集压力的时间 + public Integer mark = 0; // 结构体最后一个数值,设置9973,用于保证单片机端和java端均正确更新了枚举 @FunctionalInterface diff --git a/src/main/java/a8k/app/service/module/TurntableAndOptScannerCtrlModule.java b/src/main/java/a8k/app/service/module/TurntableAndOptScannerCtrlModule.java new file mode 100644 index 0000000..ef49920 --- /dev/null +++ b/src/main/java/a8k/app/service/module/TurntableAndOptScannerCtrlModule.java @@ -0,0 +1,84 @@ +package a8k.app.service.module; + +import a8k.app.service.data.ReactionRecordMgrService; +import a8k.app.service.lowerctrl.OptScanModuleCtrlService; +import a8k.app.service.statemgr.IncubationPlateStateMgrService; +import a8k.app.service.statemgr.OptScanModuleStateMgrService; +import a8k.app.teststate.VirtualDevice; +import a8k.app.utils.ZWorkThread; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RequiredArgsConstructor +public class TurntableAndOptScannerCtrlModule { + public enum State { + idle, + starting, + working, + stopping, + } + + // + //状态 + // + private final VirtualDevice virtualDevice; + private final IncubationPlateStateMgrService incubationPlateStateMgrService; + private final OptScanModuleStateMgrService optScanModuleStateMgrService; + // + // 数据库 + // + private final ReactionRecordMgrService reactionRecordMgrService; + // + // 控制 + // + private final OptScanModuleCtrlService optScanModuleCtrlService; + + + private ZWorkThread workThread; + private State state = State.idle; + private final Object stateLock = new Object(); + private final Object turntableLock = new Object(); + + @PostConstruct + public void init() { + state = State.idle; + workThread = new ZWorkThread(); + workThread.init("TurntableAndOptScannerCtrlModule-WorkThread"); + } + + public void workThreadFn() { + // while (!workThread.isStopping()) { + // } + } + + + // + // utils + // + private void setState(State state) { + synchronized (stateLock) { + this.state = state; + log.info("TurntableAndOptScannerCtrlModule state changed to {}", state); + } + } + + private State getState() { + synchronized (stateLock) { + return state; + } + } + + private void waitForState(State state) { + while (!getState().equals(state)) { + try { + Thread.sleep(50); + } catch (InterruptedException ignored) { + } + } + } + +} diff --git a/src/main/java/a8k/app/service/statemgr/ReactionPlateContainerStateMgr.java b/src/main/java/a8k/app/service/statemgr/ReactionPlateContainerStateMgr.java new file mode 100644 index 0000000..24867db --- /dev/null +++ b/src/main/java/a8k/app/service/statemgr/ReactionPlateContainerStateMgr.java @@ -0,0 +1,64 @@ +package a8k.app.service.statemgr; + +import a8k.app.constant.AppConstant; +import a8k.app.type.a8k.ConsumableGroup; +import a8k.app.type.a8k.container.ReactionPlateContainer; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RequiredArgsConstructor +public class ReactionPlateContainerStateMgr { + + public ReactionPlateContainer[] reactionPlateGroup = new ReactionPlateContainer[AppConstant.CONSUMABLE_CHANNEL_NUM]; + + @PostConstruct + void init() { + for (int i = 0; i < reactionPlateGroup.length; i++) { + reactionPlateGroup[i] = new ReactionPlateContainer(); + } + } + + synchronized void installReactionPlateContainer(ConsumableGroup group, Integer projId, String projName, String projShortName, String lotId, String color, Integer num) { + ReactionPlateContainer reactionPlateContainer = reactionPlateGroup[group.off]; + reactionPlateContainer.projId = projId; + reactionPlateContainer.projName = projName; + reactionPlateContainer.projShortName = projShortName; + reactionPlateContainer.lotId = lotId; + reactionPlateContainer.color = color; + reactionPlateContainer.num = num; + reactionPlateContainer.isInstall = true; + reactionPlateContainer.reserveNum = 0; + } + + synchronized void setReactionPlateNum(ConsumableGroup group, Integer num) { + log.debug("setReactionPlateNum: group={}, num={}", group, num); + ReactionPlateContainer reactionPlateContainer = reactionPlateGroup[group.off]; + if (reactionPlateContainer.isInstall) { + reactionPlateContainer.num = num; + } else { + log.warn("setReactionPlateNum: Reaction plate container is not installed for group: {}", group); + } + } + + + synchronized Boolean popOneReactionPlate(ConsumableGroup group) { + log.debug("popOneReactionPlate: group={}", group); + ReactionPlateContainer reactionPlateContainer = reactionPlateGroup[group.off]; + if (reactionPlateContainer.isInstall && reactionPlateContainer.num > 0) { + reactionPlateContainer.num--; + return true; + } else { + return false; + } + } + + synchronized void uninstallReactionPlateContainer(ConsumableGroup group) { + log.debug("uninstallReactionPlateContainer: group={}", group); + ReactionPlateContainer reactionPlateContainer = reactionPlateGroup[group.off]; + reactionPlateContainer.reset(); + } +} diff --git a/src/main/java/a8k/app/utils/ZWorkThread.java b/src/main/java/a8k/app/utils/ZWorkThread.java new file mode 100644 index 0000000..27dd010 --- /dev/null +++ b/src/main/java/a8k/app/utils/ZWorkThread.java @@ -0,0 +1,100 @@ +package a8k.app.utils; + +import org.springframework.util.Assert; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class ZWorkThread { + Thread thread; + Runnable pendingRunnable; + AtomicBoolean workingFlag = new AtomicBoolean(false); + AtomicBoolean stopPendingFlag = new AtomicBoolean(false); + + + public void init(String name) { + thread = new Thread(this::workThread); + thread.setName(name); + thread.start(); + } + + + synchronized public void start(Runnable runnable) { + Assert.isTrue(thread != null, "Thread not initialized. Call init() first."); + + if (workingFlag.get()) { + stop(); + } + pendingRunnable = runnable; + } + + synchronized public void stop() { + Assert.isTrue(thread != null, "Thread not initialized. Call init() first."); + + stopPendingFlag.set(true); + while (workingFlag.get()) { + threadSleep(50); + } + stopPendingFlag.set(false); + } + + public Boolean isStopping() { + return stopPendingFlag.get(); + } + + public Boolean isWorking() { + return workingFlag.get(); + } + + + private void workThread() { + while (true) { + Runnable runnable = null; + if (pendingRunnable != null) { + runnable = pendingRunnable; + pendingRunnable = null; + } + + if (runnable == null) { + threadSleep(50); + continue; + } + + workingFlag.set(true); + runnable.run(); + workingFlag.set(false); + } + } + + + public void threadSleep(int ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException ignored) { + } + } + + public static void main(String[] args) throws InterruptedException { + ZWorkThread workThread = new ZWorkThread(); + workThread.init("TestWorkThread"); + + workThread.start(() -> { + while (!workThread.isStopping()) { + System.out.println("Task 1 is running"); + workThread.threadSleep(100); + } + }); + + Thread.sleep(3000); + workThread.stop(); + + workThread.start(() -> { + while (!workThread.isStopping()) { + System.out.println("Task 2 is running"); + workThread.threadSleep(100); + } + }); + Thread.sleep(3000); + workThread.stop(); + } + +} diff --git a/src/main/java/a8k/extui/page/driver/pipette_module/PipetteGunCommonConfigPage.java b/src/main/java/a8k/extui/page/driver/pipette_module/PipetteGunCommonConfigPage.java index 71b6c36..e38cbc0 100644 --- a/src/main/java/a8k/extui/page/driver/pipette_module/PipetteGunCommonConfigPage.java +++ b/src/main/java/a8k/extui/page/driver/pipette_module/PipetteGunCommonConfigPage.java @@ -43,6 +43,11 @@ public class PipetteGunCommonConfigPage { pipetteCtrlDriverV2.setCommonConfig(PipetteCommonConfigIndex.eachActionDelayTime, timems); } + // aspiratePressureReportExtendTime + public void setAspiratePressureReportExtendTime(Integer timems) throws AppException { + pipetteCtrlDriverV2.setCommonConfig(PipetteCommonConfigIndex.aspiratePressureReportExtendTime, timems); + } + @PostConstruct void init() { var page = extApiPageMgr.newPage(this); @@ -53,6 +58,8 @@ public class PipetteGunCommonConfigPage { .setParamVal("platInfoCpyIdx", () -> PlatInfoCpyIdx.of(getVal(PipetteCommonConfigIndex.platformInfoCpyid))); page.addFunction("设置每次动作延时(ms)", this::setEachActionDelayTime) .setParamVal("timems", () -> getVal(PipetteCommonConfigIndex.eachActionDelayTime)); + page.addFunction("设置吸液压力采集延长时间(ms)", this::setAspiratePressureReportExtendTime) + .setParamVal("timems", () -> getVal(PipetteCommonConfigIndex.aspiratePressureReportExtendTime)); extApiPageMgr.addPage(page); } diff --git a/src/main/java/a8k/extui/page/driver/pipette_module/PipetteGunOperationCtrlPage.java b/src/main/java/a8k/extui/page/driver/pipette_module/PipetteGunOperationCtrlPage.java index 584d7df..b796fb4 100644 --- a/src/main/java/a8k/extui/page/driver/pipette_module/PipetteGunOperationCtrlPage.java +++ b/src/main/java/a8k/extui/page/driver/pipette_module/PipetteGunOperationCtrlPage.java @@ -7,13 +7,18 @@ import a8k.app.hardware.type.pipette_module.cpyidx.PlatInfoCpyIdx; import a8k.app.hardware.type.pipette_module.param.AspirationParam; import a8k.app.hardware.type.pipette_module.param.DistribuAllParam; import a8k.app.type.exception.AppException; +import a8k.extui.factory.CurveBuilder; import a8k.extui.mgr.ExtApiPageMgr; +import a8k.extui.type.ret.ExtApiCurve; import jakarta.annotation.PostConstruct; import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import java.util.ArrayList; +import java.util.List; + @Component @RequiredArgsConstructor @Slf4j @@ -23,7 +28,6 @@ public class PipetteGunOperationCtrlPage { private final PipetteCtrlDriverV2 pipetteCtrlDriverV2; - AspirationParam aspirateParam = new AspirationParam(); DistribuAllParam distributeParam = new DistribuAllParam(); @@ -40,7 +44,7 @@ public class PipetteGunOperationCtrlPage { aspirateParam.containerInfoCpyId = containerInfoCpyId.toInteger(); } - public void setAspirationParamLiquidCfgIndex(PlatInfoCpyIdx liquidCfgIdx) { + public void setAspirationParamLiquidCfgIndex(LiquidConfigCpyIdx liquidCfgIdx) { aspirateParam.liquidCfgIdx = liquidCfgIdx.ordinal(); } @@ -109,9 +113,10 @@ public class PipetteGunOperationCtrlPage { } - public void execAspirate() throws AppException { + public ExtApiCurve execAspirate() throws AppException { pipetteCtrlDriverV2.aspirateSetParam(aspirateParam); pipetteCtrlDriverV2.aspirateBlock(); + return getAspPressureRecord(); } public void execDistribute() throws AppException { @@ -140,6 +145,24 @@ public class PipetteGunOperationCtrlPage { pipetteCtrlDriverV2.pierceThroughBlock(containerCpyId, containerPos); } + public ExtApiCurve getAspPressureRecord() throws AppException { + var datas = pipetteCtrlDriverV2.getAspPressureData(); + Integer i = 0; + List points = new ArrayList<>(); + for (var data : datas) { + points.add(new Object[]{i, data}); + i++; + } + return CurveBuilder.buidCurve("压力曲线", "value", "value", 2000, 3000, points); + } + + public Integer readPressure() throws AppException { + return pipetteCtrlDriverV2.readPressure(); + } + + public Integer readCapacity() throws AppException { + return pipetteCtrlDriverV2.readCapacitance(); + } @PostConstruct void init() { @@ -147,7 +170,7 @@ public class PipetteGunOperationCtrlPage { page.newGroup("配置吸液参数"); page.addFunction("设置吸取体积", this::setAspirationParamVolume).setParamVal("volume", () -> aspirateParam.volume); page.addFunction("设置容器位置", this::setAspirationParamContainerPos).setParamVal("containerPos", () -> aspirateParam.containerPos); - page.addFunction("设置容器信息", this::setAspirationParamContainerInfoIndex).setParamVal("containerInfoCpyId", () ->ContainerCpyId.of(aspirateParam.containerInfoCpyId)); + page.addFunction("设置容器信息", this::setAspirationParamContainerInfoIndex).setParamVal("containerInfoCpyId", () -> ContainerCpyId.of(aspirateParam.containerInfoCpyId)); page.addFunction("设置液体配置", this::setAspirationParamLiquidCfgIndex).setParamVal("liquidCfgIdx", () -> LiquidConfigCpyIdx.of(aspirateParam.liquidCfgIdx)); page.addFunction("设置吸取模式", this::setAspirationParamAspirationMode).setParamVal("aspirationMode", () -> aspirateParam.aspirationMode); page.addFunction("设置LLDEnable", this::setAspirationParamLldEnable).setParamVal("lldEnable", () -> aspirateParam.lldEnable); @@ -177,6 +200,11 @@ public class PipetteGunOperationCtrlPage { page.addFunction("吸取", this::execAspirate); page.addFunction("分配全部", this::execDistribute); + page.newGroup(" 数据"); + page.addFunction("获取吸液压力记录", this::getAspPressureRecord); + page.addFunction("读取压力", this::readPressure); + page.addFunction("读取电容", this::readCapacity); + extApiPageMgr.addPage(page); } 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 93f5443..a6a62f2 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 @@ -6,7 +6,9 @@ import a8k.app.hardware.type.pipette_module.cpyidx.ContainerCpyId; import a8k.app.hardware.type.pipette_module.cpyidx.LiquidConfigCpyIdx; import a8k.app.hardware.type.pipette_module.cpyidx.PMVCpyIdx; import a8k.app.type.exception.AppException; +import a8k.extui.factory.CurveBuilder; import a8k.extui.mgr.ExtApiPageMgr; +import a8k.extui.type.ret.ExtApiCurve; import jakarta.annotation.PostConstruct; import lombok.Data; import lombok.RequiredArgsConstructor; @@ -14,6 +16,9 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import java.util.ArrayList; +import java.util.List; + @Component @RequiredArgsConstructor @Slf4j @@ -27,9 +32,21 @@ public class PipetteGunTestCtrlPage { @Setter private ContainerCpyId containerCpyId = ContainerCpyId.Default; // 默认容器配置索引 + public ExtApiCurve getAspPressureRecord() throws AppException { + var datas = pipetteCtrlDriverV2.getAspPressureData(); + Integer i = 0; + List points = new ArrayList<>(); + for (var data : datas) { + points.add(new Object[]{i, data}); + i++; + } + return CurveBuilder.buidCurve("压力曲线", "value", "value", 2000, 3000, points); + } + - public void pipetteTestPumpMoveToX100nl(Integer x100nl, PMVCpyIdx vcpyidx) throws AppException { + public ExtApiCurve pipetteTestPumpMoveToX100nl(Integer x100nl, PMVCpyIdx vcpyidx) throws AppException { pipetteCtrlDriverV2.pipetteTestPumpMoveToX100nl(x100nl, vcpyidx.toInteger()); + return getAspPressureRecord(); } public void pipetteTestLld(LiquidConfigCpyIdx liquidCpyId) throws AppException { @@ -67,7 +84,8 @@ public class PipetteGunTestCtrlPage { public void pipetteTestMoveToPiercePos() throws AppException { pipetteCtrlDriverV2.pipetteTestMoveToPiercePos(containerPos, containerCpyId); } - public void pipetteTestMoveToLldEndPos() throws AppException { + + public void pipetteTestMoveToLldEndPos() throws AppException { pipetteCtrlDriverV2.pipetteTestMoveToLldEndPos(containerPos, containerCpyId); }