diff --git a/app.db b/app.db index e9185c0..fce7551 100644 Binary files a/app.db and b/app.db differ diff --git a/pom.xml b/pom.xml index 260d851..a46f33b 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ com.iflytop uf - 0.0.12 + 0.0.19 diff --git a/src/main/java/com/iflytop/a800/ResourceLockManager.java b/src/main/java/com/iflytop/a800/ResourceLockManager.java new file mode 100644 index 0000000..85ce74d --- /dev/null +++ b/src/main/java/com/iflytop/a800/ResourceLockManager.java @@ -0,0 +1,53 @@ +package com.iflytop.a800; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +public class ResourceLockManager { + // lock map + private final Map> locks = new HashMap<>(); + // singleton + private static final ResourceLockManager instance = new ResourceLockManager(); + + // get instance + public static ResourceLockManager getInstance() { + return instance; + } + + // lock + public Object lock( String resName ) { + Object lock = new Object(); + synchronized ( this.locks ) { + var locks = this.locks.computeIfAbsent(resName, k -> new ArrayList<>()); + if ( locks.isEmpty() ) { + locks.add(lock); + return lock; + } + locks.add(lock); + } + synchronized ( lock ) { + try { + lock.wait(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + return lock; + } + + // unlock + public void unlock( String resName, Object lock ) { + Object nextLock = null; + synchronized ( this.locks ) { + var locks = this.locks.get(resName); + if ( null == locks ) { + return ; + } + locks.remove(lock); + nextLock = locks.isEmpty() ? null : locks.get(0); + } + synchronized ( nextLock ) { + nextLock.notify(); + } + } +} diff --git a/src/main/java/com/iflytop/a800/Task.java b/src/main/java/com/iflytop/a800/Task.java new file mode 100644 index 0000000..ed56ef3 --- /dev/null +++ b/src/main/java/com/iflytop/a800/Task.java @@ -0,0 +1,5 @@ +package com.iflytop.a800; +public interface Task { + // start this task + void start(); +} diff --git a/src/main/java/com/iflytop/a800/TaskBase.java b/src/main/java/com/iflytop/a800/TaskBase.java new file mode 100644 index 0000000..0be7bd0 --- /dev/null +++ b/src/main/java/com/iflytop/a800/TaskBase.java @@ -0,0 +1,36 @@ +package com.iflytop.a800; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +abstract public class TaskBase extends Thread implements Task { + // 事件回调 + public interface EventCallback { + void callback( Task task, List args ); + } + + // 事件回调列表 + public Map> eventCallbacks = new HashMap<>(); + + // 追加事件回调 + public void on( String name, EventCallback callback ) { + if ( !eventCallbacks.containsKey(name) ) { + eventCallbacks.put(name, new ArrayList<>()); + } + eventCallbacks.get(name).add(callback); + } + + // 触发事件 + public void emit( String name, List args ) { + if ( !eventCallbacks.containsKey(name) ) { + return; + } + for ( var callback : eventCallbacks.get(name) ) { + callback.callback(this, args); + } + } + + + public void lockRes() {} + public void unlockRes() {} +} diff --git a/src/main/java/com/iflytop/a800/TaskManager.java b/src/main/java/com/iflytop/a800/TaskManager.java new file mode 100644 index 0000000..2f233ee --- /dev/null +++ b/src/main/java/com/iflytop/a800/TaskManager.java @@ -0,0 +1,28 @@ +package com.iflytop.a800; +import jakarta.annotation.PostConstruct; +import org.springframework.stereotype.Component; +import java.util.ArrayList; +import java.util.List; +@Component +public class TaskManager { + // list of tasks + private final List tasks = new ArrayList(); + // singleton instance + private static TaskManager instance; + + // get instance + public static TaskManager getInstance() { + return instance; + } + + @PostConstruct + public void init() { + instance = this; + } + + // append task + public void append( Task task ) { + tasks.add(task); + task.start(); + } +} diff --git a/src/main/java/com/iflytop/a800/controller/DemoController.java b/src/main/java/com/iflytop/a800/controller/DemoController.java new file mode 100644 index 0000000..919480c --- /dev/null +++ b/src/main/java/com/iflytop/a800/controller/DemoController.java @@ -0,0 +1,25 @@ +package com.iflytop.a800.controller; +import com.iflytop.a800.device.Device; +import com.iflytop.uf.controller.UfApiControllerBase; +import com.iflytop.uf.controller.UfApiResponse; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.ResponseBody; +@Controller +public class DemoController extends UfApiControllerBase { + @PostMapping("/api/demo/pipette-tip-pick-up") + @ResponseBody + public UfApiResponse pipetteTipPickUp() { + var pipette = Device.getInstance().pipette; + pipette.tipPickUp(); + return this.success(); + } + + @PostMapping("/api/demo/pipette-tip-drop") + @ResponseBody + public UfApiResponse pipetteTipDrop() { + var pipette = Device.getInstance().pipette; + pipette.tipDrop(); + return this.success(); + } +} diff --git a/src/main/java/com/iflytop/a800/controller/TaskController.java b/src/main/java/com/iflytop/a800/controller/TaskController.java new file mode 100644 index 0000000..287c41d --- /dev/null +++ b/src/main/java/com/iflytop/a800/controller/TaskController.java @@ -0,0 +1,22 @@ +package com.iflytop.a800.controller; +import com.iflytop.a800.TaskManager; +import com.iflytop.a800.task.TubeRackTask; +import com.iflytop.uf.controller.UfApiControllerBase; +import com.iflytop.uf.controller.UfApiResponse; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.ResponseBody; +@Controller +public class TaskController extends UfApiControllerBase { + @Resource + private TaskManager taskManager; + + @PostMapping("/api/task/tube-rack-append") + @ResponseBody + public UfApiResponse tubeRackAppend() { + var task = new TubeRackTask(); + this.taskManager.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 new file mode 100644 index 0000000..2e6a099 --- /dev/null +++ b/src/main/java/com/iflytop/a800/device/Device.java @@ -0,0 +1,14 @@ +package com.iflytop.a800.device; +public class Device { + // singleton instance + private static final Device instance = new Device(); + // feeder + public final Feeder feeder = new Feeder(); + // pipette + public final Pipette pipette = new Pipette(); + + // get instance + public static Device getInstance() { + return instance; + } +} diff --git a/src/main/java/com/iflytop/a800/device/Feeder.java b/src/main/java/com/iflytop/a800/device/Feeder.java new file mode 100644 index 0000000..8e7b4b2 --- /dev/null +++ b/src/main/java/com/iflytop/a800/device/Feeder.java @@ -0,0 +1,42 @@ +package com.iflytop.a800.device; +import com.iflytop.uf.UfCmdSnippetExecutor; +public class Feeder { + // 试管架进料 + public void feed() { + UfCmdSnippetExecutor.execute("FeedTubeRackFeed"); + } + + // 试管架出料 + public void exit() { + UfCmdSnippetExecutor.execute("FeedTubeRackExit"); + } + + // 读取试管架类型 + public Number readTubeRackType() { + UfCmdSnippetExecutor.execute("FeedTubeRackTypeReadPrepare"); + return 1; + } + + // 读取试管是否存在 + public Boolean readIsTestTubeExisted(int i) { + UfCmdSnippetExecutor.execute("FeedTubeRackTubeExistsCheckPrepare"); + return false; + } + + // 读取是否为5ml管 + public Boolean readIsWb5ml(int i) { + UfCmdSnippetExecutor.execute("FeedTubeRackTubeIsWb5mlCheckPrepare"); + return true; + } + + // 读取条码 + public String readBarCode(int i) { + UfCmdSnippetExecutor.execute("FeedTubeRackTubeBarCodeScan"); + return "1222"; + } + + // 准备 + public void prepareForTesting() { + UfCmdSnippetExecutor.execute("FeedTubeRackPrepareForTesting"); + } +} diff --git a/src/main/java/com/iflytop/a800/device/Incubator.java b/src/main/java/com/iflytop/a800/device/Incubator.java new file mode 100644 index 0000000..323c007 --- /dev/null +++ b/src/main/java/com/iflytop/a800/device/Incubator.java @@ -0,0 +1,4 @@ +package com.iflytop.a800.device; + +public class Incubator { +} diff --git a/src/main/java/com/iflytop/a800/device/Pipette.java b/src/main/java/com/iflytop/a800/device/Pipette.java new file mode 100644 index 0000000..6a7c9ed --- /dev/null +++ b/src/main/java/com/iflytop/a800/device/Pipette.java @@ -0,0 +1,117 @@ +package com.iflytop.a800.device; +import com.iflytop.uf.UfActuatorCmdExecutor; +import com.iflytop.uf.UfCmdSnippetExecutor; +import com.iflytop.uf.model.UfMdbOption; +import com.iflytop.uf.util.UfCommon; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +public class Pipette { + // logger + public static final Logger LOG = LoggerFactory.getLogger(Pipette.class); + // if pipette has tip + private Boolean hasTip = false; + // tip amount list + private final List tipAmountList = new ArrayList<>(); + + // + public Pipette() { + // @TODO : 删除下面三行,测试用 + this.tipAmountList.add(0); + this.tipAmountList.add(120-29); + this.tipAmountList.add(120); + } + + // set tip amount + public void setTipAmount( Integer index, Integer amount ) { + this.tipAmountList.set(index, amount); + } + + // pick up tip + public void tipPickUp() { + if ( this.hasTip ) { + return ; + } + + int zoneIndex = -1; + int tipIndex = -1; + for ( int i = 0; i < this.tipAmountList.size(); i++ ) { + if ( this.tipAmountList.get(i) > 0 ) { + zoneIndex = i; + tipIndex = 120 - this.tipAmountList.get(i); + break; + } + } + if ( -1 == zoneIndex ) { + throw new RuntimeException("No tip available"); + } + this.tipAmountList.set(zoneIndex, 120 - tipIndex -1); + + Integer zoneStartX = UfMdbOption.getInteger(String.format("PipetteTipZoneStartX.%d", zoneIndex), 0); + Integer zoneStartY = UfMdbOption.getInteger(String.format("PipetteTipZoneStartY.%d", zoneIndex), 0); + Integer zoneZ = UfMdbOption.getInteger(String.format("PipetteTipZoneZ.%d", zoneIndex), 0); + Integer distanceX = UfMdbOption.getInteger("PipetteTipDistanceX", 0); + Integer distanceY = UfMdbOption.getInteger("PipetteTipDistanceY", 0); + Integer indexX = tipIndex % 12; + Integer indexY = tipIndex / 12; + Integer x = zoneStartX + indexX * distanceX; + Integer y = zoneStartY + indexY * distanceY; + + 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)) { + break ; + } + + } + + String tipState = UfActuatorCmdExecutor.execute("Pipette", "read_pipette_tip_state"); + if ("0".equals(tipState)) { + this.tipPickUp(); + return ; // 如果拾取失败,则跳过这个TIP取下一个 + } + this.hasTip = true; + } + + // 丢弃枪头 + public void tipDrop() { + if ( !this.hasTip ) { + return ; + } + + LOG.info("[Pipette] Drop tip"); + UfCmdSnippetExecutor.execute("PipetteTipDrop"); + for ( int i=0; i<10; i++ ) { + if ( !this.isTipExistOnPipette() ) { + break; + } + UfCommon.delay(500); + UfActuatorCmdExecutor.execute("Pipette", "pipette_ctrl_put_tip"); + LOG.info("[Pipette] Drop tip : retry {}", i); + } + if ( this.isTipExistOnPipette() ) { + throw new RuntimeException("Failed to drop tip"); + } + UfCmdSnippetExecutor.execute("ArmReset"); + this.hasTip = false; + } + + // check if tip exists on pipette by reading tip state + private Boolean isTipExistOnPipette() { + int existsCount = 0; + for ( int i=0; i<3; i++ ) { + String tipState = UfActuatorCmdExecutor.execute("Pipette", "read_pipette_tip_state"); + if ("1".equals(tipState)) { + existsCount++; + } + UfCommon.delay(100); + } + return existsCount > 1; + } +} diff --git a/src/main/java/com/iflytop/a800/device/Scanner.java b/src/main/java/com/iflytop/a800/device/Scanner.java new file mode 100644 index 0000000..6ea0070 --- /dev/null +++ b/src/main/java/com/iflytop/a800/device/Scanner.java @@ -0,0 +1,4 @@ +package com.iflytop.a800.device; + +public class Scanner { +} diff --git a/src/main/java/com/iflytop/a800/device/Shaker.java b/src/main/java/com/iflytop/a800/device/Shaker.java new file mode 100644 index 0000000..12bca42 --- /dev/null +++ b/src/main/java/com/iflytop/a800/device/Shaker.java @@ -0,0 +1,4 @@ +package com.iflytop.a800.device; + +public class Shaker { +} diff --git a/src/main/java/com/iflytop/a800/device/TestCardWarehouse.java b/src/main/java/com/iflytop/a800/device/TestCardWarehouse.java new file mode 100644 index 0000000..1484206 --- /dev/null +++ b/src/main/java/com/iflytop/a800/device/TestCardWarehouse.java @@ -0,0 +1,4 @@ +package com.iflytop.a800.device; + +public class TestCardWarehouse { +} diff --git a/src/main/java/com/iflytop/a800/resource/TestTube.java b/src/main/java/com/iflytop/a800/resource/TestTube.java new file mode 100644 index 0000000..3fe02df --- /dev/null +++ b/src/main/java/com/iflytop/a800/resource/TestTube.java @@ -0,0 +1,10 @@ +package com.iflytop.a800.resource; +import com.iflytop.a800.task.TubeTestTask; +public class TestTube { + // is tube existed + public Boolean isExisted = false; + // tube type + public Number type; + // tube bar code + public String barCode; +} diff --git a/src/main/java/com/iflytop/a800/resource/TestTubeRack.java b/src/main/java/com/iflytop/a800/resource/TestTubeRack.java new file mode 100644 index 0000000..10c5aa9 --- /dev/null +++ b/src/main/java/com/iflytop/a800/resource/TestTubeRack.java @@ -0,0 +1,8 @@ +package com.iflytop.a800.resource; +import java.util.List; +public class TestTubeRack { + // tube rack type + public Number 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 new file mode 100644 index 0000000..bfecdcb --- /dev/null +++ b/src/main/java/com/iflytop/a800/task/TubeRackTask.java @@ -0,0 +1,73 @@ +package com.iflytop.a800.task; +import com.iflytop.a800.Task; +import com.iflytop.a800.TaskBase; +import com.iflytop.a800.TaskManager; +import com.iflytop.a800.device.Device; +import com.iflytop.a800.resource.TestTube; +import com.iflytop.a800.resource.TestTubeRack; +import java.util.ArrayList; +import java.util.List; +public class TubeRackTask extends TaskBase { + // sampling tubes + private List samplingTubes; + + @Override + public void run() { + var device = Device.getInstance(); + var feeder = device.feeder; + var taskMan = TaskManager.getInstance(); + + feeder.feed(); + this.samplingTubes = new ArrayList<>(); + // test tube rack + TestTubeRack tubeRack = new TestTubeRack(); + tubeRack.type = feeder.readTubeRackType(); + tubeRack.tubes = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + var tube = new TestTube(); + tubeRack.tubes.add(tube); + + tube.isExisted = feeder.readIsTestTubeExisted(i); + if ( !tube.isExisted ) { + continue ; + } + + this.samplingTubes.add(tube); + tube.type = tubeRack.type; + if ( !tube.type.equals(1) ) { + continue; + } + + Boolean isWb5ml = feeder.readIsWb5ml(i); + if ( isWb5ml ) { + tube.type = 2; + } + + tube.barCode = feeder.readBarCode(i); + + var testTask = new TubeTestTask(); + testTask.tubeRack = tubeRack; + testTask.tube = tube; + testTask.on("StartIncubating", this::onTubeTaskStartIncubating); + taskMan.append(testTask); + } + + feeder.prepareForTesting(); + } + + // tube task finish callback + private void onTubeTaskStartIncubating(Task task, List args ) { + TubeTestTask tubeTask = null; + if ( !(task instanceof TubeTestTask) ) { + return ; + } + + tubeTask = (TubeTestTask)task; + var tube = tubeTask.tube; + this.samplingTubes.remove(tube); + if ( !this.samplingTubes.isEmpty() ) { + return ; + } + Device.getInstance().feeder.exit(); + } +} diff --git a/src/main/java/com/iflytop/a800/task/TubeTestTask.java b/src/main/java/com/iflytop/a800/task/TubeTestTask.java new file mode 100644 index 0000000..f1c37df --- /dev/null +++ b/src/main/java/com/iflytop/a800/task/TubeTestTask.java @@ -0,0 +1,52 @@ +package com.iflytop.a800.task; +import com.iflytop.a800.TaskBase; +import com.iflytop.a800.resource.TestTube; +import com.iflytop.a800.resource.TestTubeRack; +import com.iflytop.uf.UfCmdSnippetExecutor; +public class TubeTestTask extends TaskBase { + public TestTubeRack tubeRack; + public TestTube tube; + public String status; + + @Override + public void run() { + this.shake(); + this.uncap(); + this.sampling(); + this.cap(); + this.incubate(); + this.scan(); + } + + // 摇匀 + private void shake() { + UfCmdSnippetExecutor.execute("SampleTestShake"); + } + + // 取盖 + private void uncap() { + UfCmdSnippetExecutor.execute("SampleTestUnCap"); + } + + // 取样 + private void sampling() { + UfCmdSnippetExecutor.execute("SampleTestSamplingFromWb5ml"); + UfCmdSnippetExecutor.execute("SampleTestSamplingFromBufferTube"); + UfCmdSnippetExecutor.execute("SampleTestSamplingFromBufferTube"); + } + + // 盖回 + private void cap() { + UfCmdSnippetExecutor.execute("SampleTestCap"); + } + + // 孵育 + private void incubate() { + UfCmdSnippetExecutor.execute("SampleTestIncubate"); + } + + // 扫描 + private void scan() { + UfCmdSnippetExecutor.execute("SampleTestScanResult"); + } +}