From 0f1d6e78e837364a34a9c7e064c28c74a302b5d6 Mon Sep 17 00:00:00 2001 From: sige Date: Mon, 13 May 2024 21:11:32 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=93=E6=9E=9C=E6=89=AB=E6=8F=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.db | Bin 155648 -> 172032 bytes pom.xml | 2 +- .../iflytop/a800/controller/DemoController.java | 18 ++ src/main/java/com/iflytop/a800/device/Device.java | 7 + src/main/java/com/iflytop/a800/device/Feeder.java | 4 +- .../java/com/iflytop/a800/device/Incubator.java | 15 +- src/main/java/com/iflytop/a800/device/Pipette.java | 90 ++++++- src/main/java/com/iflytop/a800/device/Scanner.java | 55 ++++ .../java/com/iflytop/a800/model/MdbIdChip.java | 8 + .../java/com/iflytop/a800/model/MdbProject.java | 15 ++ .../java/com/iflytop/a800/resource/TestCard.java | 4 + .../com/iflytop/a800/resource/TestCardManager.java | 8 + .../java/com/iflytop/a800/resource/TestTube.java | 4 +- .../com/iflytop/a800/resource/TestTubeRack.java | 2 +- .../java/com/iflytop/a800/task/TubeRackTask.java | 4 +- .../java/com/iflytop/a800/task/TubeTestTask.java | 287 +++++++++++++++++++-- .../iflytop/a800/utils/ScanResultAnalysisAlgo.java | 29 +++ 17 files changed, 520 insertions(+), 32 deletions(-) create mode 100644 src/main/java/com/iflytop/a800/model/MdbIdChip.java create mode 100644 src/main/java/com/iflytop/a800/model/MdbProject.java create mode 100644 src/main/java/com/iflytop/a800/resource/TestCard.java create mode 100644 src/main/java/com/iflytop/a800/resource/TestCardManager.java create mode 100644 src/main/java/com/iflytop/a800/utils/ScanResultAnalysisAlgo.java diff --git a/app.db b/app.db index 5abc8008f5a7680cb944d9f420d4b3dcb5626328..bcc0fa2ea3e66cde0f23d9efb9f9e9dc90ec3e2d 100644 GIT binary patch delta 7340 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 delta 1144 zcma)5TTB#J7@l*Motd54nX?oqsUT1U$~KJ5eP<-aBB-q`DNWp@fNeUn7pum&7;IYa zCMYphSlr+gFfG&vLq(x%>A^Ne)U+?{i!Vw^8i5pSD`}|)`k<*DVtntp{10Eg^PT@c z|96f~Fh?hR$-GRNAc!(|KO2Fr3dJiEYu`HbRkVLkHbqPk=qfruOtHYXne6aP5@)&w z_NUJzs{IgN!jFRREJ)8D%mS$cM^2~diO1-CMsg$o(ejGo8uSP)qraKmIQvyFiFOvR zE0#nTr&q3|E8IZkGHxtpItYH8!CViz%q9H$5rej|PyFYZ4)!)P?SGq};R?`ke>qb4 zSNKkDGu!6>iM#J#;^S;HpUqxkW$p&L!}`#jXEU7{*yVGcG=ty~f`HW_DPn7?W(rYF zR|Hjw=z^tKqM*j3s;KCS5!0=5mzbl4WLcEWm=Llpml#nD!M0UZh()BB842l{VcRbe z1Xz&Ns3pZsL(nWEB&enm7mT>A2xin0Wy_Q!dQ5Vw1UF0Lie^V`F(T-)W~?w}!O|5| zQ1!TEN}4Xos#%IVn!tyeho|ZtDhXD*=?5uX?*L2#-tqQ>LP-7Mf&~<=y8t#p$o_*r z83el^&8fr(;oLR3&aENPh~F6kgPu}!g5bGcg1>^^HW zz%96HtsL&X3Kl@nZ9&kG3`MbRO|T8w7F1D_1Y2?wHce5nbt|e`s_enT*T5J^M{8y? z@TMQY#0%?^0JZQx^L5-P7xIsT5x)qE*nFnlx8!SKzosA1?@_mjBvnlglDXb<-qjf= zGk|9=oPry>1{!er4A{bRIaO>pTv(eUWoBj3&PWQDIM=7a`=lg_?s8iG0^K0yP>Z0F zfpf3{i~@#;bq$o_e;2_q$UeXO>G7&eMen9hZd>E|fVKEiJ>pZiGiI%j(GJp0^cq`=lTsf-K<-)OJ-Kjv!Kk%+an#|Fb| zcxN&E9pL34>|6QyJrJ7${1K7>MF|v5XBIUf99<9l1D;~% CEK&IY diff --git a/pom.xml b/pom.xml index 71aa56c..eb7d419 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ com.iflytop uf - 0.0.21 + 0.0.24 diff --git a/src/main/java/com/iflytop/a800/controller/DemoController.java b/src/main/java/com/iflytop/a800/controller/DemoController.java index 65c9acf..c52bbef 100644 --- a/src/main/java/com/iflytop/a800/controller/DemoController.java +++ b/src/main/java/com/iflytop/a800/controller/DemoController.java @@ -1,6 +1,11 @@ package com.iflytop.a800.controller; +import com.iflytop.a800.TaskManager; import com.iflytop.a800.device.Device; +import com.iflytop.a800.model.MdbProject; import com.iflytop.a800.resource.BufferTube; +import com.iflytop.a800.resource.TestTube; +import com.iflytop.a800.task.TubeTestTask; +import com.iflytop.uf.UfActiveRecord; import com.iflytop.uf.UfActuatorCmdExecutor; import com.iflytop.uf.controller.UfApiControllerBase; import com.iflytop.uf.controller.UfApiResponse; @@ -75,4 +80,17 @@ public class DemoController extends UfApiControllerBase { } return this.success(); } + + @PostMapping("/api/demo/tube-test-task-start") + @ResponseBody + public UfApiResponse tubeTestTaskStart() { + var task = new TubeTestTask(); + task.project = UfActiveRecord.findOne(MdbProject.class, "1"); + task.tube = new TestTube(); + task.tube.type = "EmergencyTube"; + task.tube.index = 0; + var taskMan = TaskManager.getInstance(); + taskMan.append(task); + return this.success(); + } } diff --git a/src/main/java/com/iflytop/a800/device/Device.java b/src/main/java/com/iflytop/a800/device/Device.java index 276a79e..c043fbc 100644 --- a/src/main/java/com/iflytop/a800/device/Device.java +++ b/src/main/java/com/iflytop/a800/device/Device.java @@ -2,6 +2,7 @@ package com.iflytop.a800.device; import com.iflytop.a800.resource.BufferTubeManager; import com.iflytop.a800.resource.LargeBufferTubeManager; +import com.iflytop.a800.resource.TestCardManager; public class Device { // singleton instance @@ -10,10 +11,16 @@ public class Device { public final Feeder feeder = new Feeder(); // pipette public final Pipette pipette = new Pipette(); + // incubator + public final Incubator incubator = new Incubator(); + // scanner + public final Scanner scanner = new Scanner(); // buffer tube manager public final BufferTubeManager bufferTube = new BufferTubeManager(); // large buffer tube manager public final LargeBufferTubeManager largeBufferTube = new LargeBufferTubeManager(); + // test card manager + public final TestCardManager testCard = new TestCardManager(); // get instance public static Device getInstance() { diff --git a/src/main/java/com/iflytop/a800/device/Feeder.java b/src/main/java/com/iflytop/a800/device/Feeder.java index 8e7b4b2..9dbdcae 100644 --- a/src/main/java/com/iflytop/a800/device/Feeder.java +++ b/src/main/java/com/iflytop/a800/device/Feeder.java @@ -12,9 +12,9 @@ public class Feeder { } // 读取试管架类型 - public Number readTubeRackType() { + public String readTubeRackType() { UfCmdSnippetExecutor.execute("FeedTubeRackTypeReadPrepare"); - return 1; + return "wb"; } // 读取试管是否存在 diff --git a/src/main/java/com/iflytop/a800/device/Incubator.java b/src/main/java/com/iflytop/a800/device/Incubator.java index 323c007..9ecfb4f 100644 --- a/src/main/java/com/iflytop/a800/device/Incubator.java +++ b/src/main/java/com/iflytop/a800/device/Incubator.java @@ -1,4 +1,17 @@ package com.iflytop.a800.device; - +import com.iflytop.uf.UfCmdSnippetExecutor; +import com.iflytop.uf.model.UfMdbOption; +import java.util.Map; public class Incubator { + // 推送新卡片 + public void pushNewCard( Integer boxIndex ) { + Integer boxPos = UfMdbOption.getInteger(String.format("TestCardWarehouseBox.%d", boxIndex)); + Map params = Map.of("box", boxPos); + UfCmdSnippetExecutor.execute("IncubatorTestCardPushIn", params); + } + + // 退出到扫描 + public void exitToScanner() { + UfCmdSnippetExecutor.execute("IncubatorTestCardExitToScanner"); + } } diff --git a/src/main/java/com/iflytop/a800/device/Pipette.java b/src/main/java/com/iflytop/a800/device/Pipette.java index 9efc0f0..0c7136d 100644 --- a/src/main/java/com/iflytop/a800/device/Pipette.java +++ b/src/main/java/com/iflytop/a800/device/Pipette.java @@ -60,20 +60,20 @@ public class Pipette { Integer x = zoneStartX + indexX * distanceX; Integer y = zoneStartY + indexY * distanceY; + Boolean tipStatusCheckEnable = UfMdbOption.getBoolean("PipetteTipStatusCheckEnable"); Map pickUpParams = Map.of("tipX", x, "tipY", y, "tipZ", zoneZ); // 尝试三次拾取枪头 for ( int i=0; i<3; i++ ) { LOG.info("[Pipette] Pick up tip at [{},{}](x={}, y={}, z={})", indexX, indexY, x, y, zoneZ); UfCmdSnippetExecutor.execute("PipetteTipPickUp", pickUpParams); String tipState = UfActuatorCmdExecutor.execute("Pipette", "read_pipette_tip_state"); - if ("1".equals(tipState)) { + if ( "1".equals(tipState) || !tipStatusCheckEnable ) { break ; } - } String tipState = UfActuatorCmdExecutor.execute("Pipette", "read_pipette_tip_state"); - if ("0".equals(tipState)) { + if ("0".equals(tipState) && tipStatusCheckEnable ) { this.tipPickUp(); return ; // 如果拾取失败,则跳过这个TIP取下一个 } @@ -139,6 +139,75 @@ public class Pipette { UfCmdSnippetExecutor.execute(snippetKey, dispenseParams); } + // 混匀 + public void mix( BufferTube tube, Integer count, Integer volume ) { + if ( 0 == count ) { + return ; + } + + Integer startX = UfMdbOption.getInteger(String.format("%sZoneStartX.%d", tube.type, tube.zoneIndex)); + Integer startY = UfMdbOption.getInteger(String.format("%sZoneStartY.%d", tube.type, tube.zoneIndex)); + Integer distanceX = UfMdbOption.getInteger(String.format("%sDistanceX", tube.type)); + Integer distanceY = UfMdbOption.getInteger(String.format("%sDistanceY", tube.type)); + Integer indexX = tube.tubeIndex % 5; + Integer indexY = tube.tubeIndex / 5; + Integer tubeX = startX + indexX * distanceX; + Integer tubeY = startY + indexY * distanceY; + Map dispenseParams = Map.of("tubeX", tubeX, "tubeY", tubeY); + + String snippetKey = "SampleMixingAtDetectionTubePrepare"; + if ( "BufferTube".equals(tube.type) ) { + snippetKey = "SampleMixingAtBufferTubePrepare"; + } + UfCmdSnippetExecutor.execute(snippetKey, dispenseParams); + + String volumeStr = Integer.toString(volume); + for ( int i=0; i dispenseParams = Map.of("tubeX", tubeX, "tubeY", tubeY); + + String snippetKey = "PunctureAtDetectionTube"; + if ( "BufferTube".equals(tube.type) ) { + snippetKey = "PunctureAtBufferTube"; + } + UfCmdSnippetExecutor.execute(snippetKey, dispenseParams); + } + + // 从缓冲液管吸液并吐液到检测板 + public void aspirateFromBufferTubeAndDispenseToTestCard( BufferTube tube, Integer volume ) { + Integer startX = UfMdbOption.getInteger(String.format("%sZoneStartX.%d", tube.type, tube.zoneIndex)); + Integer startY = UfMdbOption.getInteger(String.format("%sZoneStartY.%d", tube.type, tube.zoneIndex)); + Integer distanceX = UfMdbOption.getInteger(String.format("%sDistanceX", tube.type)); + Integer distanceY = UfMdbOption.getInteger(String.format("%sDistanceY", tube.type)); + Integer indexX = tube.tubeIndex % 5; + Integer indexY = tube.tubeIndex / 5; + Integer tubeX = startX + indexX * distanceX; + Integer tubeY = startY + indexY * distanceY; + Map dispenseParams = Map.of("tubeX", tubeX, "tubeY", tubeY, "volume", volume); + + String snippetKey = "AspirateFromDetectionTubeAndDispenseToTestCard"; + if ( "BufferTube".equals(tube.type) ) { + snippetKey = "AspirateFromBufferTubeAndDispenseToTestCard"; + } + UfCmdSnippetExecutor.execute(snippetKey, dispenseParams); + } + // 从大缓冲液管吸液 public void aspirateFromLargeBufferTube(LargeBufferTube tube, Integer volume) { Integer startX = UfMdbOption.getInteger("LargeBufferTubeZoneStartX"); @@ -208,8 +277,23 @@ public class Pipette { this.aspirateWithLiquidLevelFollow(volume, depth); } + // 从全血5ml吸液 + public void aspirateFromWholeBlood5ml(Integer index, Integer volume) { + throw new RuntimeException("摇匀没接,暂时无法测试"); + } + + // 从全血3ml吸液 + public void aspirateFromWholeBlood3ml(Integer index, Integer volume) { + throw new RuntimeException("摇匀没接,暂时无法测试"); + } + // 移动到液面 private void moveToLiquidLevel( Integer maxDepth, Integer threshold ) { + Boolean liquidLevelDetectEnable = UfMdbOption.getBoolean("PipetteLiquidLevelDetectEnable"); + if ( !liquidLevelDetectEnable ) { + return ; + } + int stepDepth = 10; int depth = 0; do { diff --git a/src/main/java/com/iflytop/a800/device/Scanner.java b/src/main/java/com/iflytop/a800/device/Scanner.java index 6ea0070..47fa1b5 100644 --- a/src/main/java/com/iflytop/a800/device/Scanner.java +++ b/src/main/java/com/iflytop/a800/device/Scanner.java @@ -1,4 +1,59 @@ package com.iflytop.a800.device; +import com.iflytop.uf.UfActuatorCmdExecutor; +import com.iflytop.uf.UfCmdSnippetExecutor; +import com.iflytop.uf.util.UfCommon; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; public class Scanner { + // 扫描类型F + public void scanTypeF() { + UfCmdSnippetExecutor.execute("SampleResultAnalysisTypeF"); + UfCommon.delay(20 * 1000); + } + + // 扫描类型T + public void scanTypeT() { + UfCmdSnippetExecutor.execute("SampleResultAnalysisTypeT"); + UfCommon.delay(20 * 1000); + } + + // 读取结果 + public float[] readResult() { + String sizeStr = UfActuatorCmdExecutor.execute("Scanner","module_get_reg", "10"); + String countStr = UfActuatorCmdExecutor.execute("Scanner","module_get_reg", "11"); + int size = Integer.parseInt(sizeStr); + int count = Integer.parseInt(countStr); + List data = new ArrayList<>(); + for ( int i = 0; i < count; i++ ) { + String readParam = Integer.toString(i); + String response = UfActuatorCmdExecutor.execute("Scanner","module_read_raw", readParam); + Base64.Decoder decoder = Base64.getDecoder(); + byte[] bytes = decoder.decode(response); + for (byte aByte : bytes) { + data.add(aByte); + } + UfCommon.delay(100); + } + + byte[] result = new byte[data.size()]; + for ( int i = 0; i < data.size(); i++ ) { + result[i] = data.get(i); + } + var buffer = ByteBuffer.wrap(result); + buffer.order(ByteOrder.LITTLE_ENDIAN); + float[] scanRawData = new float[buffer.capacity() / 2]; + for (int i = 0; i < scanRawData.length; i++) { + scanRawData[i] = buffer.getShort(); + } + return scanRawData; + } + + // 丢弃卡片 + public void dropCard() { + UfCmdSnippetExecutor.execute("SampleResultAnalysisDropCard"); + } } diff --git a/src/main/java/com/iflytop/a800/model/MdbIdChip.java b/src/main/java/com/iflytop/a800/model/MdbIdChip.java new file mode 100644 index 0000000..bcae114 --- /dev/null +++ b/src/main/java/com/iflytop/a800/model/MdbIdChip.java @@ -0,0 +1,8 @@ +package com.iflytop.a800.model; +public class MdbIdChip { + public Integer peakCount; + public Integer itemCount; + public String items; + public Integer scanPeakCount; + public Integer scanType; +} diff --git a/src/main/java/com/iflytop/a800/model/MdbProject.java b/src/main/java/com/iflytop/a800/model/MdbProject.java new file mode 100644 index 0000000..1fa7e92 --- /dev/null +++ b/src/main/java/com/iflytop/a800/model/MdbProject.java @@ -0,0 +1,15 @@ +package com.iflytop.a800.model; +import com.iflytop.uf.UfActiveRecord; +import com.iflytop.uf.UfActiveRecordField; +public class MdbProject extends UfActiveRecord { + @UfActiveRecordField + public String name; + + @UfActiveRecordField + public String steps; + + // get table name + public static String getTableName() { + return "app_projects"; + } +} diff --git a/src/main/java/com/iflytop/a800/resource/TestCard.java b/src/main/java/com/iflytop/a800/resource/TestCard.java new file mode 100644 index 0000000..3ed7164 --- /dev/null +++ b/src/main/java/com/iflytop/a800/resource/TestCard.java @@ -0,0 +1,4 @@ +package com.iflytop.a800.resource; +public class TestCard { + public Integer boxIndex; +} diff --git a/src/main/java/com/iflytop/a800/resource/TestCardManager.java b/src/main/java/com/iflytop/a800/resource/TestCardManager.java new file mode 100644 index 0000000..14828ca --- /dev/null +++ b/src/main/java/com/iflytop/a800/resource/TestCardManager.java @@ -0,0 +1,8 @@ +package com.iflytop.a800.resource; +public class TestCardManager { + public TestCard alloc() { + var card = new TestCard(); + card.boxIndex = 0; + return card; + } +} diff --git a/src/main/java/com/iflytop/a800/resource/TestTube.java b/src/main/java/com/iflytop/a800/resource/TestTube.java index 3fe02df..44f2cab 100644 --- a/src/main/java/com/iflytop/a800/resource/TestTube.java +++ b/src/main/java/com/iflytop/a800/resource/TestTube.java @@ -4,7 +4,9 @@ public class TestTube { // is tube existed public Boolean isExisted = false; // tube type - public Number type; + public String type; // tube bar code public String barCode; + // tube index + public Integer index; } diff --git a/src/main/java/com/iflytop/a800/resource/TestTubeRack.java b/src/main/java/com/iflytop/a800/resource/TestTubeRack.java index 10c5aa9..281062d 100644 --- a/src/main/java/com/iflytop/a800/resource/TestTubeRack.java +++ b/src/main/java/com/iflytop/a800/resource/TestTubeRack.java @@ -2,7 +2,7 @@ package com.iflytop.a800.resource; import java.util.List; public class TestTubeRack { // tube rack type - public Number type; + public String type; // list of test tubes public List tubes; } diff --git a/src/main/java/com/iflytop/a800/task/TubeRackTask.java b/src/main/java/com/iflytop/a800/task/TubeRackTask.java index bfecdcb..7e52fe2 100644 --- a/src/main/java/com/iflytop/a800/task/TubeRackTask.java +++ b/src/main/java/com/iflytop/a800/task/TubeRackTask.java @@ -34,13 +34,13 @@ public class TubeRackTask extends TaskBase { this.samplingTubes.add(tube); tube.type = tubeRack.type; - if ( !tube.type.equals(1) ) { + if ( !tube.type.equals("wb") ) { continue; } Boolean isWb5ml = feeder.readIsWb5ml(i); if ( isWb5ml ) { - tube.type = 2; + tube.type = "wb5ml"; } tube.barCode = feeder.readBarCode(i); diff --git a/src/main/java/com/iflytop/a800/task/TubeTestTask.java b/src/main/java/com/iflytop/a800/task/TubeTestTask.java index f1c37df..a31a801 100644 --- a/src/main/java/com/iflytop/a800/task/TubeTestTask.java +++ b/src/main/java/com/iflytop/a800/task/TubeTestTask.java @@ -1,25 +1,89 @@ package com.iflytop.a800.task; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.iflytop.a800.TaskBase; -import com.iflytop.a800.resource.TestTube; -import com.iflytop.a800.resource.TestTubeRack; +import com.iflytop.a800.device.Device; +import com.iflytop.a800.model.MdbIdChip; +import com.iflytop.a800.model.MdbProject; +import com.iflytop.a800.resource.*; +import com.iflytop.a800.utils.ScanResultAnalysisAlgo; import com.iflytop.uf.UfCmdSnippetExecutor; +import com.iflytop.uf.util.UfCommon; +import com.iflytop.uf.util.UfJsonHelper; + +import java.util.*; + public class TubeTestTask extends TaskBase { + // 测试项目 + public MdbProject project; public TestTubeRack tubeRack; public TestTube tube; public String status; + // 缓冲液试管 + private BufferTube bufferTube; + // 大容量缓冲液试管 + private LargeBufferTube largeBufferTube; + // 测试卡 + private TestCard testCard; + // id chip + private MdbIdChip idChip; + + // 任务准备 + public void prepare() { + var device = Device.getInstance(); + this.bufferTube = device.bufferTube.alloc(); + this.largeBufferTube = device.largeBufferTube.getTube(); + this.testCard = device.testCard.alloc(); + this.idChip = new MdbIdChip(); + this.idChip.peakCount = 3; + this.idChip.itemCount = 1; + this.idChip.scanPeakCount = 3; + this.idChip.items = UfJsonHelper.objectToJson(List.of(Map.of( + "item", "hsCRP", + "isPiecewiseFunction", false, + "nonPiecewiseFunction", Map.of( + "xValueSource", 1, + "serum", Map.of("a", 1.0, "b", 2.0, "c", 3.0, "d", 4.0), + "wb", Map.of("a", 1.0, "b", 2.0, "c", 3.0, "d", 4.0) + ) + ))); + } + @Override public void run() { - this.shake(); - this.uncap(); - this.sampling(); - this.cap(); - this.incubate(); - this.scan(); + this.prepare(); + + ObjectMapper jsonMapper = new ObjectMapper(); + JsonNode stepsJsonTree = null; + try { + stepsJsonTree = jsonMapper.readTree(this.project.steps); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + for ( JsonNode stepNode : stepsJsonTree ) { + var stepEnable = stepNode.get("enable").asBoolean(); + if ( !stepEnable ) { + continue; + } + + var stepAction = stepNode.get("action").asText(); + switch ( stepAction ) { +// case "shaking" : this.shake(stepNode); break; + case "puncture" : this.executeStepPuncture(stepNode); break; + case "aspirate" : this.executeStepAspirate(stepNode); break; + case "mixing" : this.executeStepMixing(stepNode); break; + case "drop-tip" : this.executeStepDropTip(stepNode); break; + case "incubate" : this.executeStepIncubate(stepNode); break; + case "analysis" : this.executeStepAnalysis(stepNode); break; + } + } } // 摇匀 - private void shake() { + private void shake( JsonNode stepNode ) { UfCmdSnippetExecutor.execute("SampleTestShake"); } @@ -28,25 +92,206 @@ public class TubeTestTask extends TaskBase { UfCmdSnippetExecutor.execute("SampleTestUnCap"); } - // 取样 - private void sampling() { - UfCmdSnippetExecutor.execute("SampleTestSamplingFromWb5ml"); - UfCmdSnippetExecutor.execute("SampleTestSamplingFromBufferTube"); - UfCmdSnippetExecutor.execute("SampleTestSamplingFromBufferTube"); + // 穿孔 + private void executeStepPuncture( JsonNode stepNode ) { + Device device = Device.getInstance(); + var pipette = device.pipette; + pipette.tipPickUp(); + pipette.puncture(this.bufferTube); + } + + // 吸液 + private void executeStepAspirate( JsonNode stepNode ) { + Device device = Device.getInstance(); + var pipette = device.pipette; + pipette.tipPickUp(); + + String source = stepNode.get("source").asText(); + Integer volume = stepNode.get("volume").asInt(); + if ( "LargeBufferTube".equals(source) ) { + pipette.aspirateFromLargeBufferTube(this.largeBufferTube, volume); + } else if ( "Sample".equals(source) && "EmergencyTube".equals(this.tube.type) ) { + pipette.aspirateFromEmergencyTube(this.tube.index, volume); + } else if ( "Sample".equals(source) && "SampleEpp0_5Tube".equals(this.tube.type) ) { + pipette.aspirateFromSampleEpp0_5(this.tube.index, volume); + } else if ( "Sample".equals(source) && "SampleEpp1_5Tube".equals(this.tube.type) ) { + pipette.aspirateFromSampleEpp1_5(this.tube.index, volume); + } else if ( "Sample".equals(source) && "SampleWholeBlood5mlTube".equals(this.tube.type) ) { + pipette.aspirateFromWholeBlood5ml(this.tube.index, volume); + } else if ( "Sample".equals(source) && "SampleWholeBlood3mlTube".equals(this.tube.type) ) { + pipette.aspirateFromWholeBlood3ml(this.tube.index, volume); + } else { + throw new RuntimeException(String.format("无效的取样源 : %s/%s",source,this.tube.type)); + } + pipette.dispense(this.bufferTube); + } + + // 混匀 + private void executeStepMixing( JsonNode stepNode ) { + Device device = Device.getInstance(); + var pipette = device.pipette; + var mixCount = stepNode.get("count").asInt(); + var mixVolume = stepNode.get("volume").asInt(); + pipette.mix(this.bufferTube, mixCount, mixVolume); } - // 盖回 - private void cap() { - UfCmdSnippetExecutor.execute("SampleTestCap"); + // 丢弃吸头 + private void executeStepDropTip( JsonNode stepNode ) { + Device device = Device.getInstance(); + var pipette = device.pipette; + pipette.tipDrop(); } // 孵育 - private void incubate() { - UfCmdSnippetExecutor.execute("SampleTestIncubate"); + private void executeStepIncubate( JsonNode stepNode ) { + Device device = Device.getInstance(); + device.incubator.pushNewCard(this.testCard.boxIndex); + + var pipette = device.pipette; + pipette.tipPickUp(); + + Integer volume = stepNode.get("volume").asInt(); + pipette.aspirateFromBufferTubeAndDispenseToTestCard(this.bufferTube, volume); + + pipette.tipDrop(); + + Integer duration = stepNode.get("duration").asInt(); + UfCommon.delay(duration); } // 扫描 - private void scan() { - UfCmdSnippetExecutor.execute("SampleTestScanResult"); + private void executeStepAnalysis( JsonNode stepNode ) { + // 扫描 + Device device = Device.getInstance(); +// device.incubator.exitToScanner(); + var scanner = device.scanner; + scanner.scanTypeF(); + var scanResult = scanner.readResult(); + scanner.dropCard(); + + // 计算 + var algo = new ScanResultAnalysisAlgo(); + var algoResult = algo.calculate(scanResult, this.idChip.peakCount); + + // 处理 + String sampleType = "WB"; // @TODO : 这里要从样本中取 ~~~~ + List> results = new ArrayList<>(); + var projects = UfJsonHelper.jsonToNode(idChip.items); + for ( var i=0; i< idChip.itemCount; i++ ) { + double valueX = 0; + JsonNode funcInfo = null; + + var project = projects.get(i); + var itemName = project.get("item").asText(); + var isPiecewiseFunction = project.get("isPiecewiseFunction").asBoolean(); + if ( !isPiecewiseFunction ) { // 非分段函数 + var func = project.get("nonPiecewiseFunction"); + var xValueSource = func.get("xValueSource").asInt(); + valueX = this.calculateRatio(algoResult, xValueSource, itemName); + funcInfo = func.get("serum"); + if ("WB".equals(sampleType)) { + funcInfo = func.get("wb"); + } + } else { // 分段函数 + var func = project.get("piecewiseFunction"); + var piecewiseValueSrc = func.get("piecewiseValueSrc").asInt(); + var piecewiseValueSrcValue = this.calculateRatio(algoResult, piecewiseValueSrc, itemName); + var piecewiseValue = func.get("piecewiseValue").asDouble(); + if ( piecewiseValueSrcValue > piecewiseValue ) { // 高浓度 + var highXValueSource = func.get("highXValueSource").asInt(); + valueX = this.calculateRatio(algoResult, highXValueSource, itemName); + funcInfo = func.get("serumHigh"); + if ("WB".equals(sampleType)) { + funcInfo = func.get("wbHigh"); + } + } else { // 低浓度 + var lowXValueSource = func.get("lowXValueSource").asInt(); + valueX = this.calculateRatio(algoResult, lowXValueSource, itemName); + funcInfo = func.get("serumLow"); + if ("WB".equals(sampleType)) { + funcInfo = func.get("wbLow"); + } + } + } + + double valueD = funcInfo.get("d").asDouble(); + double valueB = funcInfo.get("b").asDouble(); + double valueC = funcInfo.get("c").asDouble(); + double valueA = funcInfo.get("a").asDouble(); + double value = valueA * Math.pow(valueX,3) + valueB * Math.pow(valueX,2) + valueC * valueX + valueD; + + // @TODO : 单位也要从配置中读取 + results.add(Map.of("name",itemName,"value",value,"unit","xx/xx")); + } + + String testResult = UfJsonHelper.objectToJson(results); + System.out.println(testResult); + } + + /** + * @link https://iflytop1.feishu.cn/docx/UnRtdSG4qouMTaxzRb2cjiTTnZe + * @link https://iflytop1.feishu.cn/docx/A0CHdQL6OoTTCSx8MB7cQKEjnSc + * @param result - algo result + * @param xSource - x source + * @return double - value of ratio + */ + private double calculateRatio(ScanResultAnalysisAlgo.AlgoResult result, int xSource, String itenName) { + // 计算 ratio + Map ratios = new HashMap<>(); + if ( 5 == idChip.scanPeakCount ) { + var peaks = result.peakInfos; + ratios.put("ratio", peaks[3].area / peaks[4].area); // T/C + ratios.put("antiRatio", peaks[3].area / peaks[4].area); // H/C + ratios.put("antiTestRatio", peaks[3].area / peaks[2].area); // T/H + ratios.put("rfRatio", peaks[1].area / peaks[4].area); // R/C + ratios.put("rtRatio", peaks[3].area / peaks[1].area); // T/R + ratios.put("t4Ratio", peaks[0].area / peaks[4].area); // T4/C + ratios.put("t4t3Ratio", peaks[1].area / peaks[0].area); // R/T4 + } else if ( 4 == idChip.scanPeakCount && Objects.equals(idChip.scanType, 1) ) { // F 光学 + var peaks = result.peakInfos; // H T C + ratios.put("ratio", peaks[1].area / peaks[2].area); // T/C + ratios.put("antiRatio", peaks[0].area / peaks[2].area); // H/C + ratios.put("antiTestRatio", peaks[1].area / peaks[0].area); // T/H + ratios.put("rfRatio", peaks[0].area / peaks[2].area); // R/C @TODO : 待定, 三联卡F光学没有R + ratios.put("rtRatio", peaks[1].area / peaks[0].area); // T/R @TODO : 待定, 三联卡F光学没有R + } else if ( 4 == idChip.scanPeakCount && Objects.equals(idChip.scanType, 2) ) { // T 光学 + var peaks = result.peakInfos; // R H T C + ratios.put("ratio", peaks[2].area / peaks[3].area); // T/C + ratios.put("antiRatio", peaks[1].area / peaks[3].area); // H/C + ratios.put("antiTestRatio", peaks[2].area / peaks[1].area); // T/H + ratios.put("rfRatio", peaks[0].area / peaks[3].area); // R/C + ratios.put("rtRatio", peaks[2].area / peaks[0].area); // T/R + } else if ( 3 == idChip.scanPeakCount ) { + var peaks = result.peakInfos; // H T C + ratios.put("ratio", peaks[1].area / peaks[2].area); // T/C + ratios.put("antiRatio", peaks[0].area / peaks[2].area); // H/C + ratios.put("antiTestRatio", peaks[1].area / peaks[0].area); // T/H + } else if ("PCT".equals(itenName)) { + var peaks = result.peakInfos; // C T + ratios.put("ratio", peaks[1].area / peaks[0].area); // T/C + } else if ( 2 == idChip.scanPeakCount ) { + var peaks = result.peakInfos; // T C + ratios.put("ratio", peaks[0].area / peaks[1].area); // T/C + } else { + throw new RuntimeException("unknown scan peak count" + idChip.scanPeakCount); + } + + // calculate ratio by x source + if ( 1 == xSource ) { + return ratios.get("ratio"); + } else if ( 2 == xSource ) { + return ratios.get("antiTestRatio"); + } else if ( 3 == xSource ) { + return ratios.get("antiRatio"); + } else if ( 4 == xSource ) { + return ratios.get("ratio") + ratios.get("antiTestRatio"); + } else if ( 5 == xSource ) { + // @TODO : 这里 R-ratio 不知道是啥 + throw new RuntimeException("不知道 R-Ratio 是啥"); + } else if ( 6 == xSource ) { + return ratios.get("t4Ratio"); + } else { + throw new RuntimeException("unknown x source" + xSource); + } } } diff --git a/src/main/java/com/iflytop/a800/utils/ScanResultAnalysisAlgo.java b/src/main/java/com/iflytop/a800/utils/ScanResultAnalysisAlgo.java new file mode 100644 index 0000000..2282e5d --- /dev/null +++ b/src/main/java/com/iflytop/a800/utils/ScanResultAnalysisAlgo.java @@ -0,0 +1,29 @@ +package com.iflytop.a800.utils; +public class ScanResultAnalysisAlgo { + public static class AlgoResult { + public static class PeakInfo { + public boolean findPeak; + public float peakFullArea; + public float peakBaseLineArea; + public float area; + public int peakPos; + public int peakStartPos; + public int peakEndPos; + } + + public int peakNum; + public float[] lineAvg250; + public PeakInfo[] peakInfos; + } + + static { + String osName = System.getProperty("os.name").toLowerCase(); + if ( osName.contains("win") ) { + System.load("D:/Sige5193/boditech-a800/lib-algo/x64/Debug/boditech-opt-algo-java-lib.dll"); + } else { + System.load("/app-java/algo/libalgo.so"); + } + } + + public native AlgoResult calculate(float[] data, int peakCount); +}