19 changed files with 502 additions and 1 deletions
-
BINapp.db
-
2pom.xml
-
53src/main/java/com/iflytop/a800/ResourceLockManager.java
-
5src/main/java/com/iflytop/a800/Task.java
-
36src/main/java/com/iflytop/a800/TaskBase.java
-
28src/main/java/com/iflytop/a800/TaskManager.java
-
25src/main/java/com/iflytop/a800/controller/DemoController.java
-
22src/main/java/com/iflytop/a800/controller/TaskController.java
-
14src/main/java/com/iflytop/a800/device/Device.java
-
42src/main/java/com/iflytop/a800/device/Feeder.java
-
4src/main/java/com/iflytop/a800/device/Incubator.java
-
117src/main/java/com/iflytop/a800/device/Pipette.java
-
4src/main/java/com/iflytop/a800/device/Scanner.java
-
4src/main/java/com/iflytop/a800/device/Shaker.java
-
4src/main/java/com/iflytop/a800/device/TestCardWarehouse.java
-
10src/main/java/com/iflytop/a800/resource/TestTube.java
-
8src/main/java/com/iflytop/a800/resource/TestTubeRack.java
-
73src/main/java/com/iflytop/a800/task/TubeRackTask.java
-
52src/main/java/com/iflytop/a800/task/TubeTestTask.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<String, List<Object>> 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(); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,5 @@ |
|||||
|
package com.iflytop.a800; |
||||
|
public interface Task { |
||||
|
// start this task |
||||
|
void start(); |
||||
|
} |
@ -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<Object> args ); |
||||
|
} |
||||
|
|
||||
|
// 事件回调列表 |
||||
|
public Map<String,List<EventCallback>> 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<Object> args ) { |
||||
|
if ( !eventCallbacks.containsKey(name) ) { |
||||
|
return; |
||||
|
} |
||||
|
for ( var callback : eventCallbacks.get(name) ) { |
||||
|
callback.callback(this, args); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
public void lockRes() {} |
||||
|
public void unlockRes() {} |
||||
|
} |
@ -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<Task> tasks = new ArrayList<Task>(); |
||||
|
// 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(); |
||||
|
} |
||||
|
} |
@ -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(); |
||||
|
} |
||||
|
} |
@ -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(); |
||||
|
} |
||||
|
} |
@ -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; |
||||
|
} |
||||
|
} |
@ -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"); |
||||
|
} |
||||
|
} |
@ -0,0 +1,4 @@ |
|||||
|
package com.iflytop.a800.device; |
||||
|
|
||||
|
public class Incubator { |
||||
|
} |
@ -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<Integer> 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<String,Object> 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; |
||||
|
} |
||||
|
} |
@ -0,0 +1,4 @@ |
|||||
|
package com.iflytop.a800.device; |
||||
|
|
||||
|
public class Scanner { |
||||
|
} |
@ -0,0 +1,4 @@ |
|||||
|
package com.iflytop.a800.device; |
||||
|
|
||||
|
public class Shaker { |
||||
|
} |
@ -0,0 +1,4 @@ |
|||||
|
package com.iflytop.a800.device; |
||||
|
|
||||
|
public class TestCardWarehouse { |
||||
|
} |
@ -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; |
||||
|
} |
@ -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<TestTube> tubes; |
||||
|
} |
@ -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<TestTube> 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<Object> 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(); |
||||
|
} |
||||
|
} |
@ -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"); |
||||
|
} |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue