diff --git a/doc/通讯协议.pdf b/doc/通讯协议.pdf new file mode 100644 index 0000000..b164b25 Binary files /dev/null and b/doc/通讯协议.pdf differ diff --git a/src/main/java/com/dreamworks/boditech/BoditechApplication.java b/src/main/java/com/dreamworks/boditech/BoditechApplication.java index ee8b03a..e501cc1 100644 --- a/src/main/java/com/dreamworks/boditech/BoditechApplication.java +++ b/src/main/java/com/dreamworks/boditech/BoditechApplication.java @@ -1,6 +1,5 @@ package com.dreamworks.boditech; -import com.dreamworks.boditech.utils.BoditechDeviceClient; -import org.java_websocket.client.WebSocketClient; +import com.dreamworks.boditech.driver.connection.ComWebsocketClient; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -11,22 +10,11 @@ import java.net.URISyntaxException; @SpringBootApplication public class BoditechApplication { - @Value("${app.device.wsurl}") - private String deviceWsUrl; - public static void main(String[] args) { SpringApplication.run(BoditechApplication.class, args); } @Bean - public BoditechDeviceClient boditechDeviceClient() throws URISyntaxException { - URI uri = new URI(this.deviceWsUrl); - BoditechDeviceClient client = new BoditechDeviceClient(uri); - client.connect(); - return client; - } - - @Bean public ServerEndpointExporter websocketServerEndpointExporter() { return new ServerEndpointExporter(); } diff --git a/src/main/java/com/dreamworks/boditech/driver/Device.java b/src/main/java/com/dreamworks/boditech/driver/Device.java index d572b20..5b40319 100644 --- a/src/main/java/com/dreamworks/boditech/driver/Device.java +++ b/src/main/java/com/dreamworks/boditech/driver/Device.java @@ -1,6 +1,8 @@ package com.dreamworks.boditech.driver; import com.dreamworks.boditech.driver.actuator.*; +import com.dreamworks.boditech.driver.connection.ClientRequest; import com.dreamworks.boditech.driver.connection.ComSerialPort; +import com.dreamworks.boditech.driver.connection.ComWebsocket; import com.dreamworks.boditech.driver.consumable.*; import com.dreamworks.boditech.driver.entity.DeviceLogEntry; import com.dreamworks.boditech.mapper.DeviceLogMapper; @@ -26,12 +28,15 @@ import java.util.Map; @Component public class Device { private static final Logger LOG = LoggerFactory.getLogger(Device.class); - + @Value("${app.device.connectionType}") + private String connectionType; @Value("${app.device.debug}") private Boolean debug; @Resource private ComSerialPort serialPort; @Resource + private ComWebsocket websocket; + @Resource private RuntimeOptionService runtimeOptionService; @Resource public ProjectService projectService; @@ -115,16 +120,18 @@ public class Device { int mainCmdId = (cmd >> 8) & 0xFFFF; int moduleId = mid; - ByteBuffer request = ByteBuffer.allocate(length); - request.order(ByteOrder.LITTLE_ENDIAN); - request.putShort(this.messageIndex); - request.putShort((short)mainCmdId); // main cmd id - request.put((byte)subCmdId); // sub cmd id - request.put((byte)0); // directive type - request.putShort((short)moduleId); // mid + ClientRequest request = new ClientRequest(); + request.id = this.messageIndex; + request.parameter = ByteBuffer.allocate(length); + request.parameter.order(ByteOrder.LITTLE_ENDIAN); + request.parameter.putShort(this.messageIndex); + request.parameter.putShort((short)mainCmdId); // main cmd id + request.parameter.put((byte)subCmdId); // sub cmd id + request.parameter.put((byte)0); // directive type + request.parameter.putShort((short)moduleId); // mid for ( Object param : params ) { if ( param instanceof Integer ) { - request.putInt((Integer)param); + request.parameter.putInt((Integer)param); } } @@ -133,10 +140,11 @@ public class Device { this.messageIndex = 0; } - if ( this.debug ) { - return this.serialPortCall(request); + if ("SerialPort".equals(this.connectionType)) { + return this.serialPortCall(request.parameter); } - return this.prodCall(cmd, mid, params); + + return this.websocket.call(request); } // call device command in debug mode @@ -152,11 +160,6 @@ public class Device { return response; } - private ByteBuffer prodCall(Integer cmd, Integer mid, Object[] params) { - System.out.println("Device prodCall"); - return null; - } - // log device message public void logMessage( Integer direction, ByteBuffer message ) { String messageHex = MyByteBuffer.toHex(message); diff --git a/src/main/java/com/dreamworks/boditech/driver/connection/ClientRequest.java b/src/main/java/com/dreamworks/boditech/driver/connection/ClientRequest.java new file mode 100644 index 0000000..df0f3ca --- /dev/null +++ b/src/main/java/com/dreamworks/boditech/driver/connection/ClientRequest.java @@ -0,0 +1,12 @@ +package com.dreamworks.boditech.driver.connection; +import java.nio.ByteBuffer; +public class ClientRequest { + // message id + public short id; + // parameter + public ByteBuffer parameter; + // response + public ByteBuffer response; + // error code + public Integer errorCode = 0; +} diff --git a/src/main/java/com/dreamworks/boditech/driver/connection/ComWebsocket.java b/src/main/java/com/dreamworks/boditech/driver/connection/ComWebsocket.java new file mode 100644 index 0000000..ef149bb --- /dev/null +++ b/src/main/java/com/dreamworks/boditech/driver/connection/ComWebsocket.java @@ -0,0 +1,47 @@ +package com.dreamworks.boditech.driver.connection; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; + +@Component +public class ComWebsocket { + @Value("${app.device.connectionType}") + private String connectionType; + @Value("${app.device.wsuri}") + private String deviceWsUrl; + // websocket client + private ComWebsocketClient client; + + @PostConstruct + public void init() { + if ( !this.connectionType.equals("WebSocket") ) { + return ; + } + this.open(); + } + + // open websocket connection + public void open() { + URI uri = null; + try { + uri = new URI(this.deviceWsUrl); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + + this.client = new ComWebsocketClient(uri); + this.client.connect(); + } + + // call device method and wait for response + public ByteBuffer call(ClientRequest request ) { + this.client.call(request); + if ( 0 != request.errorCode ) { + throw new RuntimeException("device error : " + request.errorCode); + } + return request.response; + } +} diff --git a/src/main/java/com/dreamworks/boditech/driver/connection/ComWebsocketClient.java b/src/main/java/com/dreamworks/boditech/driver/connection/ComWebsocketClient.java new file mode 100644 index 0000000..fcd79e2 --- /dev/null +++ b/src/main/java/com/dreamworks/boditech/driver/connection/ComWebsocketClient.java @@ -0,0 +1,124 @@ +package com.dreamworks.boditech.driver.connection; +import com.dreamworks.boditech.utils.MyByteBuffer; +import org.java_websocket.client.WebSocketClient; +import org.java_websocket.handshake.ServerHandshake; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +public class ComWebsocketClient extends WebSocketClient { + // message type : ack + private static final byte MESSAGE_TYPE_ACK = 0x01; + // message type : error ack + private static final byte MESSAGE_TYPE_ERROR_ACK = 0x02; + // message type : event + private static final byte MESSAGE_TYPE_EVENT = 0x03; + // logger + private static final Logger LOG = LoggerFactory.getLogger(ComWebsocketClient.class); + // request list + private final List requestList = new ArrayList<>(); + + // constructor + public ComWebsocketClient(URI uri ) { + super(uri); + } + + /** + * call device method and wait for response + * @param request request + */ + public void call( ClientRequest request ) { + this.requestList.add(request); + int index = this.requestList.indexOf(request); + ClientRequest requestItem = this.requestList.get(index); + synchronized (requestItem) { + String cmd = MyByteBuffer.toHex(request.parameter); + cmd = cmd.replace(" ",""); + LOG.info("device => {}", cmd); + this.send(cmd); + try { + requestItem.wait(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + /** + * event handler for receiving text message from device + * @param text message from device + */ + @Override + public void onMessage(String text) { + LOG.info("device <= {}", text); + ByteBuffer message = MyByteBuffer.fromHex(text); + message.order(ByteOrder.LITTLE_ENDIAN); + byte messageType = message.get(5); + if ( ComWebsocketClient.MESSAGE_TYPE_ACK == messageType ) { + this.onMessageHandleAck(message); + } else if ( ComWebsocketClient.MESSAGE_TYPE_ERROR_ACK == messageType ) { + this.onMessageHandleErrorAck(message); + } else if ( ComWebsocketClient.MESSAGE_TYPE_EVENT == messageType ) { + this.onMessageHandleEvent(message); + } else { + throw new RuntimeException("unknown message type"); + } + } + + // handle ack message + private void onMessageHandleAck(ByteBuffer message) { + ClientRequest request = this.getRequestMessage(message); + synchronized (Objects.requireNonNull(request)) { + request.response = message; + request.notify(); + } + } + + // handle error ack message + private void onMessageHandleErrorAck(ByteBuffer message) { + ClientRequest request = this.getRequestMessage(message); + synchronized (Objects.requireNonNull(request)) { + request.response = null; + request.errorCode = message.getInt(8); + request.notify(); + } + } + + // get request message + private ClientRequest getRequestMessage( ByteBuffer message ) { + short messageId = message.getShort(0); + for ( ClientRequest requestItem : this.requestList ) { + if ( requestItem.id == messageId ) { + return requestItem; + } + } + return null; + } + + // handle event message + private void onMessageHandleEvent( ByteBuffer message ) { + // @TODO : handle event + throw new RuntimeException("not implemented"); + } + + @Override + public void onOpen(ServerHandshake serverHandshake) { + LOG.info("device connected"); + } + + @Override + public void onClose(int i, String s, boolean b) { + LOG.info("close"); + throw new RuntimeException("device connection closed"); + } + + @Override + public void onError(Exception e) { + LOG.error("error", e); + throw new RuntimeException(e); + } +} diff --git a/src/main/java/com/dreamworks/boditech/driver/consumable/CsmSampleTube.java b/src/main/java/com/dreamworks/boditech/driver/consumable/CsmSampleTube.java index d1bcfad..be34b36 100644 --- a/src/main/java/com/dreamworks/boditech/driver/consumable/CsmSampleTube.java +++ b/src/main/java/com/dreamworks/boditech/driver/consumable/CsmSampleTube.java @@ -2,9 +2,15 @@ package com.dreamworks.boditech.driver.consumable; import com.dreamworks.boditech.driver.Device; +import java.util.Objects; + public class CsmSampleTube { // tube type : Epp 0.5 public static final String TYPE_EPP05 = "Epp0.5"; + // tube type : blood tube 3ml + public static final String TYPE_BLOOD_TUBE_3ML = "BloodTube3ml"; + // tube type : blood tube 5ml + public static final String TYPE_BLOOD_TUBE_5ML = "BloodTube5ml"; // task type : regular public static final String TASK_TYPE_REGULAR = "regular"; // task type : emergency @@ -21,6 +27,7 @@ public class CsmSampleTube { // device private final Device device; + // constructor public CsmSampleTube(Device device) { this.device = device; } @@ -35,14 +42,23 @@ public class CsmSampleTube { return this.locationY; } + // check if tube has cap + public Boolean hasCap() { + return Objects.equals(this.type, CsmSampleTube.TYPE_BLOOD_TUBE_3ML) + || Objects.equals(this.type, CsmSampleTube.TYPE_BLOOD_TUBE_5ML); + } + // get location z for pipette to start aspiration. public Integer getLocationZ() { if ( CsmSampleTube.TASK_TYPE_EMERGENCY.equals(this.taskType) ) { return this.device.getLocationByName("emergencyTubeDepth"); + } else if ( CsmSampleTube.TYPE_BLOOD_TUBE_5ML.equals(this.type) ) { + return this.device.getLocationByName("regularBloodTube5mlDepth"); + } else if ( CsmSampleTube.TYPE_BLOOD_TUBE_3ML.equals(this.type) ) { + return this.device.getLocationByName("regularBloodTube3mlDepth"); + } else { + throw new RuntimeException("unknown tube type " + this.type); } - - // @TODO : get location z for regular tube - return 1400; } public Boolean isShakable() { diff --git a/src/main/java/com/dreamworks/boditech/driver/task/step/StepCapTube.java b/src/main/java/com/dreamworks/boditech/driver/task/step/StepCapTube.java new file mode 100644 index 0000000..a7dd1e2 --- /dev/null +++ b/src/main/java/com/dreamworks/boditech/driver/task/step/StepCapTube.java @@ -0,0 +1,28 @@ +package com.dreamworks.boditech.driver.task.step; +import com.dreamworks.boditech.driver.Device; +import com.dreamworks.boditech.driver.actuator.ActMotor; +import com.dreamworks.boditech.driver.actuator.ActuatorModule; +import com.dreamworks.boditech.driver.task.Executor; +import com.dreamworks.boditech.driver.task.Task; +import com.dreamworks.boditech.driver.task.TaskTest; +public class StepCapTube extends StepBase { + @Override + public void execute(Executor executor, Task task) { + TaskTest taskTest = (TaskTest)task; + if ( !taskTest.getSampleTube().hasCap() ) { + return ; + } + + Device device = executor.getDevice(); + ActMotor testTubeMoveMotor = (ActMotor)device.getActuator(ActuatorModule.TEST_TUBE_SHAKING_MOVE_MOTOR); + ActMotor testTubeCapClipMotor = (ActMotor)device.getActuator(ActuatorModule.TEST_TUBE_SHAKING_CAP_CLIP_MOTOR); + ActMotor testTubeClipMotor = (ActMotor)device.getActuator(ActuatorModule.TEST_TUBE_SHAKING_CLIP_MOTOR); + + testTubeMoveMotor.moveTo("shakeTestTubeMoveCapClip"); + testTubeCapClipMotor.moveTo("shakeTestTubeCapClipOpen"); + testTubeMoveMotor.moveTo("shakeTestTubeMoveStandby"); + testTubeCapClipMotor.moveTo("shakeTestTubeCapClipClose"); + testTubeClipMotor.rotate(ActMotor.ROTATE_DIRECTION_COUNTER_CLOCKWISE); + testTubeClipMotor.stop(); + } +} diff --git a/src/main/java/com/dreamworks/boditech/driver/task/step/StepManager.java b/src/main/java/com/dreamworks/boditech/driver/task/step/StepManager.java index 9daba09..fe79def 100644 --- a/src/main/java/com/dreamworks/boditech/driver/task/step/StepManager.java +++ b/src/main/java/com/dreamworks/boditech/driver/task/step/StepManager.java @@ -20,6 +20,7 @@ public class StepManager { StepManager.stepMap.put("sampling", StepSampling.class); StepManager.stepMap.put("analysis", StepAnalysis.class); StepManager.stepMap.put("wait", StepWait.class); + StepManager.stepMap.put("cap-tube", StepCapTube.class); } // build step by given name diff --git a/src/main/java/com/dreamworks/boditech/driver/task/step/StepPretreatment.java b/src/main/java/com/dreamworks/boditech/driver/task/step/StepPretreatment.java index e2cc2d6..a54510d 100644 --- a/src/main/java/com/dreamworks/boditech/driver/task/step/StepPretreatment.java +++ b/src/main/java/com/dreamworks/boditech/driver/task/step/StepPretreatment.java @@ -40,8 +40,6 @@ public class StepPretreatment extends StepBase { if ( this.uncap ) { this.uncap("none"); } - - this.taskTest.postStepPush(this); } // shake diff --git a/src/main/java/com/dreamworks/boditech/utils/BoditechDeviceClient.java b/src/main/java/com/dreamworks/boditech/utils/BoditechDeviceClient.java deleted file mode 100644 index bb39a60..0000000 --- a/src/main/java/com/dreamworks/boditech/utils/BoditechDeviceClient.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.dreamworks.boditech.utils; -import org.java_websocket.client.WebSocketClient; -import org.java_websocket.handshake.ServerHandshake; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.net.URI; -import java.nio.ByteBuffer; -public class BoditechDeviceClient extends WebSocketClient { - public interface OnMessageCallback { - void onCallback(ByteBuffer buffer); - } - - private static final Logger LOG = LoggerFactory.getLogger(BoditechDeviceClient.class); - - private OnMessageCallback onMessageCallback; - - public BoditechDeviceClient(URI uri ) { - super(uri); - } - - public void setOnMessageCallback( OnMessageCallback callback ) { - this.onMessageCallback = callback; - } - - @Override - public void onOpen(ServerHandshake serverHandshake) { - LOG.info("device connected"); - } - - /** - * event handler for receiving binary message from device - * @param buffer message from device - */ - @Override - public void onMessage(ByteBuffer buffer) { - LOG.info("on message (binary) :{}", buffer); - if ( this.onMessageCallback != null ) { - this.onMessageCallback.onCallback(buffer); - } - } - - /** - * event handler for receiving text message from device - * @param text message from device - */ - @Override - public void onMessage(String text) { - LOG.info("on message (text) : {}", text); - if ( this.onMessageCallback != null ) { - this.onMessageCallback.onCallback(ByteBuffer.wrap(text.getBytes())); - } - } - - @Override - public void onClose(int i, String s, boolean b) { - LOG.info("close"); - } - - @Override - public void onError(Exception e) { - LOG.error("error", e); - } -} diff --git a/src/main/java/com/dreamworks/boditech/utils/MyByteBuffer.java b/src/main/java/com/dreamworks/boditech/utils/MyByteBuffer.java index 382cdcf..983d18e 100644 --- a/src/main/java/com/dreamworks/boditech/utils/MyByteBuffer.java +++ b/src/main/java/com/dreamworks/boditech/utils/MyByteBuffer.java @@ -9,4 +9,18 @@ public class MyByteBuffer { } return sb.toString(); } + + // convert hex string to byte buffer + public static ByteBuffer fromHex ( String hex ) { + // remove space + hex = hex.replaceAll(" ", ""); + // convert hex string to byte array + byte[] bytes = new byte[hex.length() / 2]; + for (int i = 0; i < hex.length(); i += 2) { + String sub = hex.substring(i, i + 2); + bytes[i / 2] = (byte) Integer.parseInt(sub, 16); + } + // convert byte array to byte buffer + return ByteBuffer.wrap(bytes); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 57db8c7..704d706 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -8,7 +8,7 @@ spring: app: device: debug : true + connectionType : WebSocket # SerialPort, WebSocket path : COM3 baudrate : 921600 - # wsurl : wss://ws.postman-echo.com/raw - wsurl : ws://127.0.0.1:8899/device \ No newline at end of file + wsuri : ws://192.168.8.10:19005 \ No newline at end of file