Browse Source

Refactors to use monotonic clock for timeouts

将 System.currentTimeMillis() 替换为 OS.getMonotonicClockTimestamp() 以实现更可靠的超时计算。

此项变更可避免系统时钟调整对超时持续时间造成影响。

为 BiLisDoubleTrackTcpClient 添加 onLostConnectionListener 监听器,并调整 BiLisDoubleTrackChannel 以处理连接状态。

新增 OFrame、QFrame 和 RFrame

移除 LIS 设置控制器相关 API。

更新应用程序 WebSocket 状态报告上报顺序.
master
zhaohe 2 weeks ago
parent
commit
5f51531b96
  1. 14
      src/main/java/a8k/OS.java
  2. 4
      src/main/java/a8k/app/channel/iflytophald/channel/A8kCanBusConnection.java
  3. 4
      src/main/java/a8k/app/channel/iflytophald/channel/A8kCanBusService.java
  4. 4
      src/main/java/a8k/app/channel/iflytophald/driver/CodeScanerDriver.java
  5. 4
      src/main/java/a8k/app/channel/iflytophald/driver/MiniServoDriver.java
  6. 12
      src/main/java/a8k/app/channel/iflytophald/driver/OptModuleDriver.java
  7. 26
      src/main/java/a8k/app/channel/net/BiLisDoubleTrackTcpClient.java
  8. 62
      src/main/java/a8k/app/controler/api/v1/app/setting/LISSettingController.java
  9. 23
      src/main/java/a8k/app/controler/api/v1/app/ws/AppWebSocketEndpointMgr.java
  10. 70
      src/main/java/a8k/app/factory/BiLisDoubleTrackFrameFactory.java
  11. 125
      src/main/java/a8k/app/service/lis/BiLisDoubleTrackChannel.java
  12. 89
      src/main/java/a8k/app/type/lisprotocol/BiLisDoubleTrackFrame.java
  13. 13
      src/main/java/a8k/app/type/lisprotocol/subframe/OFrame.java
  14. 7
      src/main/java/a8k/app/type/lisprotocol/subframe/QFrame.java
  15. 38
      src/main/java/a8k/app/type/lisprotocol/subframe/RFrame.java
  16. 15
      src/main/java/a8k/app/utils/ThreadUtils.java
  17. 8
      src/main/java/a8k/extui/page/codetest/BiLisDoubleTrackChannelTestPage.java
  18. 19
      src/main/resources/application.yml

14
src/main/java/a8k/OS.java

@ -37,10 +37,10 @@ public class OS {
}
public static void forceSleep(Integer mills) {
Long start = System.currentTimeMillis();
Long start = getMonotonicClockTimestamp();
long end = start + mills;
while (System.currentTimeMillis() < end) {
int left = (int) (end - System.currentTimeMillis());
while (getMonotonicClockTimestamp() < end) {
int left = (int) (end - getMonotonicClockTimestamp());
threadSleep(left);
}
}
@ -134,6 +134,14 @@ public class OS {
return System.currentTimeMillis();
}
public static long getMonotonicClockTimestamp() {
return System.nanoTime() / 1_000_000; // 转换为毫秒
}
public static boolean monotonicClockHasElapsed(long startTimestamp, long durationMillis) {
return (getMonotonicClockTimestamp() - startTimestamp) >= durationMillis;
}
public static void main(String[] args) {
// System.out.println("LOCAL IP: " + OS.getLocalIp());
// System.out.println("LOCAL IP: " + OS.readSN());

4
src/main/java/a8k/app/channel/iflytophald/channel/A8kCanBusConnection.java

@ -50,9 +50,9 @@ public class A8kCanBusConnection extends WebSocketClient {
public A8kPacket getReceipt(long overtime) {
long end = System.currentTimeMillis() + overtime;
long end = OS.getMonotonicClockTimestamp() + overtime;
A8kPacket packet = null;
while (System.currentTimeMillis() < end) {
while (OS.getMonotonicClockTimestamp() < end) {
try {
packet = receiptQueue.poll(2, TimeUnit.MILLISECONDS);
} catch (InterruptedException ignored) {

4
src/main/java/a8k/app/channel/iflytophald/channel/A8kCanBusService.java

@ -115,7 +115,7 @@ public class A8kCanBusService {
}
public void waitForMod(MId mid, Integer acitionOvertime) throws AppException {
long startedAt = System.currentTimeMillis();
long startedAt = OS.getMonotonicClockTimestamp();
CmdId action = connection.getLastTxCmd(mid);
do {
ModuleStatus statu = moduleGetStatus(mid);
@ -125,7 +125,7 @@ public class A8kCanBusService {
log.error("{} waitting for action {} , catch error {}, defail ecode {}", mid, action, moduleGetError(mid), moduleGetDetailError(mid));
throw AppException.of(new AEHardwareError(moduleGetError(mid), mid, action));
}
long now = System.currentTimeMillis();
long now = OS.getMonotonicClockTimestamp();
if (now - startedAt > acitionOvertime) {
log.error("{} waitting for action {} overtime({})", mid, action, acitionOvertime);
moduleStopNoException(mid);

4
src/main/java/a8k/app/channel/iflytophald/driver/CodeScanerDriver.java

@ -70,11 +70,11 @@ public class CodeScanerDriver {
}
private String codeScanerWaittingForResult(MId mid, Integer acitionOvertime) throws AppException {
long startedAt = System.currentTimeMillis();
long startedAt = OS.getMonotonicClockTimestamp();
do {
if (codeScanerResultIsReady(mid))
break;
long now = System.currentTimeMillis();
long now = OS.getMonotonicClockTimestamp();
if (now - startedAt > acitionOvertime) {
codeScanerStopScan(mid);
return null;

4
src/main/java/a8k/app/channel/iflytophald/driver/MiniServoDriver.java

@ -160,13 +160,13 @@ public class MiniServoDriver {
}
public void miniServoWaitIsNotMove(MiniServoMId id, int acitionOvertime) throws AppException {
long startedAt = System.currentTimeMillis();
long startedAt = OS.getMonotonicClockTimestamp();
do {
var isMove = canBus.moduleGetReg(id.mid, RegIndex.kreg_mini_servo_is_move);
if (isMove != 0) {
break;
}
long now = System.currentTimeMillis();
long now = OS.getMonotonicClockTimestamp();
if (now - startedAt > acitionOvertime) {
throw AppException.of(new AEHardwareError(A8kEcode.LOW_ERROR_OVERTIME, id.mid, null));
}

12
src/main/java/a8k/app/channel/iflytophald/driver/OptModuleDriver.java

@ -39,7 +39,7 @@ public class OptModuleDriver {
void waittingForOptFinish(Integer overtime) throws AppException {
long startedAt = System.currentTimeMillis();
long startedAt = OS.getMonotonicClockTimestamp();
do {
var packet = A8kPacketFactory.buildCMDPacket(MId.OptMod.toInt(), CmdId.module_get_status.toInt(), ZList.of());
@ -59,7 +59,7 @@ public class OptModuleDriver {
throw AppException.of(new AEHardwareError(canBus.moduleGetError(MId.OptMod), MId.OptMod, CmdId.a8k_opt_v2_t_start_scan));
}
if (System.currentTimeMillis() - startedAt > overtime) {
if (OS.getMonotonicClockTimestamp() - startedAt > overtime) {
throw AppException.of(new AEHardwareError(A8kEcode.LOW_ERROR_ACTION_OVERTIME, MId.OptMod, CmdId.a8k_opt_v2_t_start_scan));
}
OS.forceSleep(100);
@ -72,20 +72,20 @@ public class OptModuleDriver {
log.info("OptTStartScan scanDirection:{} lasterGain:{} scanGain:{}", scanDirection, lasterGain, scanGain);
canBus.callcmd(MId.OptMod, CmdId.a8k_opt_v2_t_start_scan, scanDirection.getInteger(), lasterGain, scanGain);
//TODO: 2024-10-26 zhaohe,修改底层光学结束上报一条完成事件
long startedAt = System.currentTimeMillis();
long startedAt = OS.getMonotonicClockTimestamp();
OS.forceSleep(100);
waittingForOptFinish(10 * 1000);
log.info("opt t scan use {}s", (System.currentTimeMillis() - startedAt) / 1000.0);
log.info("opt t scan use {}s", (OS.getMonotonicClockTimestamp() - startedAt) / 1000.0);
}
public void optFStartScan(OptScanDirection scanDirection, Integer lasterGain, Integer scanGain) throws AppException {
log.info("OptFStartScan scanDirection:{} lasterGain:{} scanGain:{}", scanDirection, lasterGain, scanGain);
canBus.callcmd(MId.OptMod, CmdId.a8k_opt_v2_f_start_scan, scanDirection.getInteger(), lasterGain, scanGain);
//TODO: 2024-10-26 zhaohe,修改底层光学结束上报一条完成事件
long startedAt = System.currentTimeMillis();
long startedAt = OS.getMonotonicClockTimestamp();
OS.forceSleep(100);
waittingForOptFinish(10 * 1000);
log.info("opt f scan use {}s", (System.currentTimeMillis() - startedAt) / 1000.0);
log.info("opt f scan use {}s", (OS.getMonotonicClockTimestamp() - startedAt) / 1000.0);
}
public Integer[] optReadRaw() throws AppException {

26
src/main/java/a8k/app/channel/net/BiLisDoubleTrackTcpClient.java

@ -28,14 +28,20 @@ public class BiLisDoubleTrackTcpClient extends ChannelInboundHandlerAdapter {
void onDataReceived(byte[] data);
}
private final EventLoopGroup group = NettyUtils.buildEventLoopGroup("BiLisDoubleTrackTcpClient", 1);
private final Bootstrap bootstrap = new Bootstrap();
private String serverIp;
private Integer port;
private volatile Channel channel;
private volatile boolean isShuttingDown = false;
private final ByteBuf rxBuffer = Unpooled.buffer(rxBufferSize); // 接收数据缓冲区初始大小为1024字节
private OnDataReceivedListener onDataReceivedListener;
@FunctionalInterface
public interface OnLostConnectionListener {
void onLostConnection();
}
private final EventLoopGroup group = NettyUtils.buildEventLoopGroup("BiLisDoubleTrackTcpClient", 1);
private final Bootstrap bootstrap = new Bootstrap();
private String serverIp;
private Integer port;
private volatile Channel channel;
private volatile boolean isShuttingDown = false;
private final ByteBuf rxBuffer = Unpooled.buffer(rxBufferSize); // 接收数据缓冲区初始大小为1024字节
private OnDataReceivedListener onDataReceivedListener;
private OnLostConnectionListener onLostConnectionListener;
//
// External API
@ -115,6 +121,10 @@ public class BiLisDoubleTrackTcpClient extends ChannelInboundHandlerAdapter {
this.onDataReceivedListener = listener;
}
public synchronized void regOnLostConnectionListener(OnLostConnectionListener listener) {
this.onLostConnectionListener = listener;
}
public synchronized boolean isConnected() {
return channel != null && channel.isActive();
}

62
src/main/java/a8k/app/controler/api/v1/app/setting/LISSettingController.java

@ -57,66 +57,4 @@ public class LISSettingController {
return ApiRet.success();
}
@Operation(summary = "设置LIS类型")
@PostMapping("/setLISType")
public ApiRet<Void> setLISType(LISDirectionTypeEnum val) {
appSettingsMgrService.setLISType(val);
return ApiRet.success();
}
@Operation(summary = "设置LIS协议")
@PostMapping("/setLISProtocol")
public ApiRet<Void> setLISProtocol(LISProtocolEnum val) {
appSettingsMgrService.setLISProtocol(val);
return ApiRet.success();
}
@Operation(summary = "设置LIS接口")
@PostMapping("/setLIFIf")
public ApiRet<Void> setLIFIf(LISIFType val) {
appSettingsMgrService.setLIFIf(val);
return ApiRet.success();
}
@Operation(summary = "设置LIS是否自动上报报告")
@PostMapping("/setLISAutoExport")
public ApiRet<Void> setLISAutoExport(Boolean val) {
appSettingsMgrService.setLISAutoExport(val);
return ApiRet.success();
}
@Operation(summary = "设置LIS串口波特率")
@PostMapping("/setLISSerialBaudrate")
public ApiRet<Void> setLISSerialBaudrate(LISSerialBaudrateType val) {
appSettingsMgrService.setLISSerialBaudrate(val);
return ApiRet.success();
}
@Operation(summary = "设置LISIP")
@PostMapping("/setLISNetIp")
public ApiRet<Void> setLISNetIp(String val) {
appSettingsMgrService.setLISNetIp(val);
return ApiRet.success();
}
@Operation(summary = "设置LIS端口")
@PostMapping("/setLISNetPort")
public ApiRet<Void> setLISNetPort(Integer val) {
appSettingsMgrService.setLISNetPort(val);
return ApiRet.success();
}
}

23
src/main/java/a8k/app/controler/api/v1/app/ws/AppWebSocketEndpointMgr.java

@ -39,7 +39,7 @@ public class AppWebSocketEndpointMgr {
public String messageType = "Report";
public String dataType;
public Object data;
public Integer timestamp = (int) (System.currentTimeMillis() / 1000);
public Long timestamp = (System.currentTimeMillis());
public Report(String dataType, Object object) {
this.dataType = dataType;
@ -183,32 +183,33 @@ public class AppWebSocketEndpointMgr {
reportState("DeviceContext", deviceContext);
}
@Scheduled(fixedDelay = 100)
@Scheduled(fixedDelay = 50)
public void quickReportStateSchedule() {
quickReportStateSchedule(false);
}
public void quickReportStateSchedule(boolean force) {
reportState("ConsumablesState", consumablesMgrService.getStateVersion(), force,
consumablesMgrService::getState);
reportState("EngineerModeState", engineerModeStateMgrService.getVersion(), force,
engineerModeStateMgrService::getState);
reportState("TubeHolderState", tubeStateMgrService.getVersion(), force, tubeStateMgrService::getTubeHolderState);
reportState("EmergencyPosState", tubeStateMgrService.getVersion(), force, tubeStateMgrService::getEmergencyPosRunState);
reportState("OptScanModuleState", optScanModuleStateMgr.getStateVersion(), force,
optScanModuleStateMgr::getOptScanModule);
reportState("DeviceWorkState", deviceWorkStateMgrService.getVersion(), force,
deviceWorkStateMgrService::getDeviceWorkState);
reportState("OptScanModuleState", optScanModuleStateMgr.getStateVersion(), force,
optScanModuleStateMgr::getOptScanModule);
reportState("TubeHolderSetting", tubeHolderSettingMgrService.getTubeHolderSettingVersion(), force,
tubeHolderSettingMgrService::getTubeHolderSettings);
reportState("AppFlagStateList", appFlagStateMgrService.getStateVersion(), force,
appFlagStateMgrService::getFlagState);
reportState("ConsumablesState", consumablesMgrService.getStateVersion(), force,
consumablesMgrService::getState);
reportState("MessageBoxState", frontEndMessageBoxAndEventMgr.getMessageBoxStateVersion(), force,
frontEndMessageBoxAndEventMgr::getMessageBoxState);
reportState("TubeHolderState", tubeStateMgrService.getVersion(), force, tubeStateMgrService::getTubeHolderState);
reportState("EmergencyPosState", tubeStateMgrService.getVersion(), force, tubeStateMgrService::getEmergencyPosRunState);
reportState("PreReactionPosGroupState-CG1", "PreReactionPosGroupState", preReactionStateMgr.getPreReactionGridGroupVersion(ConsumableGroup.CG1), force,
() -> preReactionStateMgr.getPreReactionGridGroupState(ConsumableGroup.CG1));
reportState("PreReactionPosGroupState-CG2", "PreReactionPosGroupState", preReactionStateMgr.getPreReactionGridGroupVersion(ConsumableGroup.CG2), force,

70
src/main/java/a8k/app/factory/BiLisDoubleTrackFrameFactory.java

@ -6,6 +6,9 @@ import a8k.app.type.lisprotocol.BiLisDoubleTrackFrame;
import a8k.app.type.lisprotocol.BiLisDoubleTrackFrameBuildContext;
import a8k.app.type.lisprotocol.BiLisFrameParseErrorCode;
import a8k.app.type.lisprotocol.BiLisFrameParseException;
import a8k.app.type.lisprotocol.subframe.OFrame;
import a8k.app.type.lisprotocol.subframe.QFrame;
import a8k.app.type.lisprotocol.subframe.RFrame;
import a8k.app.utils.ByteArrayUtils;
import a8k.app.utils.ZDateUtils;
import io.netty.buffer.ByteBuf;
@ -21,8 +24,8 @@ public class BiLisDoubleTrackFrameFactory {
BiLisDoubleTrackFrameBuildContext buildContext = new BiLisDoubleTrackFrameBuildContext();
BiLisDoubleTrackFrame frame = new BiLisDoubleTrackFrame();
BiLisDoubleTrackFrame.RFrame rFrame = new BiLisDoubleTrackFrame.RFrame();
BiLisDoubleTrackFrame frame = new BiLisDoubleTrackFrame();
RFrame rFrame = new RFrame();
Double rawResultVal = reactionRecord.results.get(subProjIndex).result;
var converter = reactionRecord.results.get(subProjIndex).resultConverters.get(0);
@ -35,7 +38,6 @@ public class BiLisDoubleTrackFrameFactory {
case FECES -> "3"; // 尿
};
rFrame.type = "R"; // 信息类型 R-1
rFrame.deviceName = AppConstant.deviceTypeName;
rFrame.sampleId = reactionRecord.sampleUserid;//样本ID,条形码 R-3
@ -81,8 +83,8 @@ public class BiLisDoubleTrackFrameFactory {
static public BiLisDoubleTrackFrameBuildContext createQFrameBytes(String deviceName, String barcode) {
BiLisDoubleTrackFrameBuildContext context = new BiLisDoubleTrackFrameBuildContext();
BiLisDoubleTrackFrame frame = new BiLisDoubleTrackFrame();
BiLisDoubleTrackFrame.QFrame qFrame = new BiLisDoubleTrackFrame.QFrame();
BiLisDoubleTrackFrame frame = new BiLisDoubleTrackFrame();
QFrame qFrame = new QFrame();
frame.frameType = BiLisDoubleTrackFrame.STX;
qFrame.type = "Q"; // 请求类型
qFrame.deviceName = deviceName;
@ -94,37 +96,40 @@ public class BiLisDoubleTrackFrameFactory {
return context;
}
static public int minRxFrameLength() {
return 1/*STX*/ + 1/*FN*/ + 1/*ETX*/ + 2/*C1,C2*/ + 2;
}
static public BiLisDoubleTrackFrame createOFrame(byte[] rxData) throws BiLisFrameParseException {
static public void checkRxFrame(byte[] rxData) throws BiLisFrameParseException {
/*
* Frame Structure:
* STX,FN,Content,ETX,C1,C2,CR,LF
*/
int minLength = 1/*STX*/ + 1/*FN*/ + 1/*ETX*/ + 2/*C1,C2*/ + 2/*CR,LF*/;
if (rxData.length <= minLength) {
if (rxData.length <= minRxFrameLength()) {
log.warn("Received frame is too short: {}", rxData.length);
throw new BiLisFrameParseException(BiLisFrameParseErrorCode.FrameLengthTooShort, "rx packet length is too short: " + rxData.length);
}
byte stx = rxData[0];
char fn = (char) rxData[1];
byte etx = rxData[rxData.length - 5];
byte c1 = rxData[rxData.length - 4];
byte c2 = rxData[rxData.length - 3];
char c1 = (char) rxData[rxData.length - 4];
char c2 = (char) rxData[rxData.length - 3];
byte cr = rxData[rxData.length - 2];
byte lf = rxData[rxData.length - 1];
String cmdContent = new String(rxData, 2, rxData.length - minLength); // 从第3个字节开始到倒数第5个字节之前
String cmdContent = new String(rxData, 2, rxData.length - minRxFrameLength()); // 从第3个字节开始到倒数第5个字节之前
if (stx != BiLisDoubleTrackFrame.STX) {
log.warn("Received frame does not start with STX: {}", stx);
throw new BiLisFrameParseException(BiLisFrameParseErrorCode.NotSupportedCommand, "rx not support packet type: " + stx);
}
if (cr != BiLisDoubleTrackFrame.LF ||
lf != BiLisDoubleTrackFrame.CR) {
if (lf != BiLisDoubleTrackFrame.LF ||
cr != BiLisDoubleTrackFrame.CR) {
log.warn("received frame with invalid end: LF={}, CR={}",
rxData[rxData.length - 1], rxData[rxData.length - 2]);
throw new BiLisFrameParseException(BiLisFrameParseErrorCode.InvalidFrameFormat, "rx not end with LF and CR: " + rxData[rxData.length - 1] + ", " + rxData[rxData.length - 2]);
throw new BiLisFrameParseException(BiLisFrameParseErrorCode.InvalidFrameFormat, String.format(
"rx packet end is invalid: LF=%x CR=%x", rxData[rxData.length - 1], rxData[rxData.length - 2]));
}
if (fn != BiLisDoubleTrackFrame.FN) {
@ -138,8 +143,9 @@ public class BiLisDoubleTrackFrameFactory {
}
char[] checkcode = computeChecksum(cmdContent);
if (c1 != checkcode[0] || c2 != checkcode[1]) {
log.warn("Received frame has invalid checksum: C1={}, C2={}", c1, c2);
if (!isCheckcodeEq(c1,c2,checkcode)) {
log.warn("Received frame has invalid checksum: {}, {} !={} {}", c1, c2, checkcode[0], checkcode[1]);
throw new BiLisFrameParseException(BiLisFrameParseErrorCode.ChecksumError, "rx checksum error: " + c1 + ", " + c2);
}
@ -148,15 +154,16 @@ public class BiLisDoubleTrackFrameFactory {
log.warn("Received frame content is too short: {}", cmdContent.length());
throw new BiLisFrameParseException(BiLisFrameParseErrorCode.FrameLengthTooShort, "rx packet content is too short: " + cmdContent.length());
}
BiLisDoubleTrackFrame frame = new BiLisDoubleTrackFrame();
frame.frameType = BiLisDoubleTrackFrame.STX;
}
if (cmdContent.charAt(0) == 'O') {
BiLisDoubleTrackFrame.OFrame oframe = new BiLisDoubleTrackFrame.OFrame();
frame.frameContent = oframe;
static public OFrame createOFrame(byte[] rxData) throws BiLisFrameParseException {
checkRxFrame(rxData);
String[] parts = cmdContent.split("\\|");
String cmdContent = new String(rxData, 2, rxData.length - minRxFrameLength()); // 从第3个字节开始到倒数第5个字节之前
if (cmdContent.charAt(0) == 'O') {
OFrame oframe = new OFrame();
String[] parts = cmdContent.split("\\|");
oframe.type = strArraySafeGet(parts, 0);//O1
oframe.deviceName = strArraySafeGet(parts, 1);//O2
oframe.sampleId = strArraySafeGet(parts, 2);//O3
@ -169,7 +176,7 @@ public class BiLisDoubleTrackFrameFactory {
}
oframe.sampleType = strArraySafeGet(parts, 5).isEmpty() ? null : Integer.parseInt(parts[5]);//O6
oframe.samplingDate = strArraySafeGet(parts, 7);//O8
return frame;
return oframe;
} else {
throw new BiLisFrameParseException(BiLisFrameParseErrorCode.NotSupportedCommand, "rx not support packet type: " + cmdContent.charAt(0));
}
@ -186,8 +193,8 @@ public class BiLisDoubleTrackFrameFactory {
buildContext.buildSuccess = false;
}
if ((frame.frameContent instanceof BiLisDoubleTrackFrame.QFrame
|| frame.frameContent instanceof BiLisDoubleTrackFrame.RFrame
if ((frame.frameContent instanceof QFrame
|| frame.frameContent instanceof RFrame
)) {
String content = serializeFrameContent(frame.frameContent);
buildContext.frameContentStr = content;
@ -272,6 +279,16 @@ public class BiLisDoubleTrackFrameFactory {
return intToHexChars(checksum);
}
public static boolean isCheckcodeEq(char c1, char c2, char[] computeCheckcode) {
char _c1 = Character.toLowerCase(c1);
char _c2 = Character.toLowerCase(c2);
char _computeCheckcode1 = Character.toLowerCase(computeCheckcode[0]);
char _computeCheckcode2 = Character.toLowerCase(computeCheckcode[1]);
return _c1 == _computeCheckcode1 && _c2 == _computeCheckcode2;
}
public static char[] intToHexChars(int value) {
// 将整数转换为两个十六进制字符 ret (0-9,a-f)
char[] hexChars = new char[2];
@ -286,6 +303,7 @@ public class BiLisDoubleTrackFrameFactory {
return hexChars;
}
static public String strArraySafeGet(String[] list, int index) {
if (list == null || index < 0 || index >= list.length) {
return "";
@ -295,7 +313,7 @@ public class BiLisDoubleTrackFrameFactory {
}
public static void main(String[] args) {
log.info("intToHexChars(0x1A) = {}", new String(intToHexChars(0x1A)));
// log.info("intToHexChars(0x1A) = {}", new String(intToHexChars(0x1A)));
}
}

125
src/main/java/a8k/app/service/lis/BiLisDoubleTrackChannel.java

@ -1,15 +1,18 @@
package a8k.app.service.lis;
import a8k.OS;
import a8k.app.channel.iflytophald.channel.LisUartChannel;
import a8k.app.channel.net.BiLisDoubleTrackTcpClient;
import a8k.app.constant.AppConstant;
import a8k.app.dao.type.db.ReactionRecord;
import a8k.app.factory.BiLisDoubleTrackFrameFactory;
import a8k.app.type.lisprotocol.BiLisDoubleTrackFrame;
import a8k.app.type.lisprotocol.BiLisFrameParseException;
import a8k.app.type.lisprotocol.subframe.OFrame;
import a8k.app.utils.ByteArrayUtils;
import a8k.app.utils.ThreadUtils;
import a8k.app.utils.ZByteRxBuffer;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import a8k.app.utils.ZJsonHelper;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@ -101,7 +104,6 @@ public class BiLisDoubleTrackChannel {
if (!isChannelReady())
return false;
rxBuffer.clear();
txBytes(new byte[]{BiLisDoubleTrackFrame.ENQ});
boolean rxAckOrNak = ThreadUtils.waitingForSomeCondition("BiLisDoubleTrackChannel.rx", 30, 2000,
@ -112,7 +114,7 @@ public class BiLisDoubleTrackChannel {
return rxAckOrNak;
}
synchronized BiLisDoubleTrackFrame requestSampleInfo(String barcode) {
public synchronized OFrame requestSampleInfo(String barcode) {
/*
* 请求样本信息协议
* TX: <STX> FN Q|A5000P|^1111<CR> <ETX> C1 C2 <CR><LF>
@ -124,11 +126,27 @@ public class BiLisDoubleTrackChannel {
var cxt = BiLisDoubleTrackFrameFactory.createQFrameBytes(AppConstant.deviceTypeName, barcode);
txBytes(cxt.frameBytes);
boolean rxAckOrNak = ThreadUtils.waitingForSomeCondition("BiLisDoubleTrackChannel", 30, 2000,
ThreadUtils.waitingForSomeCondition("BiLisDoubleTrackChannel", 30, 2000,
() -> rxBuffer.readableBytes() != 0);
byte[] rxData = rxBuffer.cpy();
log.info("RX: {}", ByteArrayUtils.toByteString(rxData));
if (rxData.length == 1 && rxData[0] == BiLisDoubleTrackFrame.NAK) {
txBytes(new byte[]{BiLisDoubleTrackFrame.EOT});
return null;
} else if (rxData.length == 0) {
txBytes(new byte[]{BiLisDoubleTrackFrame.EOT});
return null;
}
OFrame oFrame;
try {
oFrame = BiLisDoubleTrackFrameFactory.createOFrame(rxData);
} catch (BiLisFrameParseException e) {
log.error("BiLisDoubleTrackChannel: 解析样本信息失败: {}", ByteArrayUtils.toByteString(rxData), e);
txBytes(new byte[]{BiLisDoubleTrackFrame.EOT});
return null;
}
log.info("BiLisDoubleTrackChannel: 收到样本信息: {}", ZJsonHelper.objectToJson(oFrame));
txBytes(new byte[]{BiLisDoubleTrackFrame.ACK});
@ -136,7 +154,61 @@ public class BiLisDoubleTrackChannel {
ThreadUtils.waitingForSomeCondition("BiLisDoubleTrackChannel", 30, 2000,
() -> rxBuffer.readableBytes() != 0);
return null;
return oFrame;
}
public boolean reportReactionReport(ReactionRecord reactionRecord) {
if (reactionRecord == null) {
return false;
}
boolean success = false;
for (int i = 0; i < reactionRecord.getSubProjNum(); i++) {
var cxt = BiLisDoubleTrackFrameFactory.createRFrame(reactionRecord, i);
if (!cxt.buildSuccess) {
log.error("BiLisDoubleTrackChannel: 发送反应报告失败: {}", ZJsonHelper.objectToJson(cxt));
break;
}
success = reportRFrameAutoReSend(cxt.frameBytes);
if (!success) {
break;
}
}
return success;
}
private boolean reportRFrameAutoReSend(byte[] rFrame) {
for (int i = 0; i < 3; i++) {
if (!connectedState) {
log.warn("BiLisDoubleTrackChannel: 连接状态异常,无法发送反应报告");
return false;
}
if (reportRFrame(rFrame)) {
return true;
} else {
log.warn("BiLisDoubleTrackChannel: 发送反应报告失败,尝试重发第{}次", i + 1);
}
}
return false;
}
private boolean reportRFrame(byte[] rFrame) {
boolean transmitSuccess = false;
txBytes(rFrame);
ThreadUtils.waitingForSomeCondition("BiLisDoubleTrackChannel", 30, 2000,
() -> rxBuffer.readableBytes() != 0);
if (rxBuffer.readableBytes() == 1 || rxBuffer.getByte(0) == BiLisDoubleTrackFrame.ACK) {
transmitSuccess = true;
}
txBytes(new byte[]{BiLisDoubleTrackFrame.EOT});
return transmitSuccess;
}
@ -173,20 +245,35 @@ public class BiLisDoubleTrackChannel {
}
private void monitorThread() {
long lastPingTimestamp = OS.getMonotonicClockTimestamp();
while (!isShuttingDown) {
if (connectedState) {
ThreadUtils.trySleep(1000);
} else {
ThreadUtils.trySleep(1000);
}
boolean state = ping();
if (state) {
connectedState = true;
} else {
connectedState = false;
log.warn("BiLisDoubleTrackChannel: Ping失败");
try {
if (connectedState) {
if (channelType.equals(ChannelType.TCP) && !biLisDoubleTrackTcpClient.isConnected()) {
connectedState = false;
continue;
}
if (OS.monotonicClockHasElapsed(lastPingTimestamp, PING_PERIOD)) {
lastPingTimestamp = OS.getMonotonicClockTimestamp();
if (!ping()) {
connectedState = false;
continue;
}
}
} else {
if (channelType.equals(ChannelType.TCP) && !biLisDoubleTrackTcpClient.isConnected()) {
continue;
}
if (ping()) {
connectedState = true;
}
}
} catch (Exception ignored) {
}
ThreadUtils.trySleep(500);
}
}

89
src/main/java/a8k/app/type/lisprotocol/BiLisDoubleTrackFrame.java

@ -21,95 +21,6 @@ public class BiLisDoubleTrackFrame {
public static final Integer ACKOvertimeMS = 2000;
public static class QFrame {
public String type = "Q"; // 请求类型
public String deviceName = "A8000";
public String barcode = "";//格式示例 ^abcdef1234567
}
public static class OFrame {
public String type = "O"; // 请求类型
public String deviceName = "A8000";
public String sampleId = "";//
public List<String> projList;
public Integer sampleType;//全血0,血清血浆1,尿2,粪便3,唾液4
public String samplingDate;//采样时间 YYYYMMDDhhmmss
}
public static class RFrame {
// DEMO
//Example: (每次只发送一条信息)
//R|A5000p|1234567890|^hsCRP^^#|18.57|mg/L||||F||||20211209100114|<CR>
//R|A10|123456789|^CRP^^#|76|mg/L|0.5-200||L_4^_02|F||||20141201144906||0|<CR>
//R|A10||^COVID-19 Ag^^%||||Positive|R_1^_01|F||||20210428153944||4|<CR>
//
//二联卡 (单项-PCT/CRP):
//R|A10|123456789|^PCT/CRP^CRP^#|46|mg/L|0.5-200||L_1^_03|F||||20141201144906||0|<CR>
//R|A10|123456789|^PCT/CRP^PCT^#|15|ng/mL|0.1-100||L_1^_03|F||||20141201144906||0|<CR>
//
//三联卡 (单项-TnI/CK-MB/Myo):
//R|A5000p|Aa123456|^TnI/CK-MB/Myo^TnI^#|3.84|ng/mL||||F||||20211209101158|<CR>
//R|A5000p|Aa123456|^TnI/CK-MB/Myo^CK-MB^#|46.39|ng/mL||||F||||20211209101158|<CR>
//R|A5000p|Aa123456|^TnI/CK-MB/Myo^Myoglobin^#|45.25|ng/mL||||F||||20211209101158|<CR>
//
//COVID-19 Ab:
//R|A10|123456789|^COVID-19 Ab^IgG^@||||Positive|L_1^|F||||20141201144906||1|<CR>
//R|A10|123456789|^COVID-19 Ab^IgM^@||||Negative|L_1^ |F||||20141201144906||1|<CR>
public String type = "R"; // 信息类型 R-1
public String deviceName = "A8000"; //设备的名称 R-2
public String sampleId = "";//样本ID,条形码 R-3
public String projInfo = "";//R-4 项目名+子项目名+结果类型(定性,定量,半定性)
public String resultVal = "";//R-5
public String resultUnit = "";//结果单位 R-6
public String refRange = "";//参考范围,也可以为空 R-7
public String qualitativeResult = "";//定性结果,如果是定量结果,则该值为空 R-8
public String samplePosInfo = "";//通道号+试管号 R9
public String resultType = "F";//always F R10
public String pad0 = "";// 填充位,如果没有填充则为空 R11
public String startIncubationTime = "";//开始孵育时间 R12 YYYYMMDDhhmmsss
public String pad1 = "";// 填充位,如果没有填充则为空 R13
public String endIncubationTime = "";//结束孵育时间 R14 YYYYMMDDhhmmsss
public String pid = "";// 用户输入的ID R15
public String sampleType = "1";//样本类型
}
public byte frameType; //STX,ACK,EOF
public Object frameContent;
// @Override public String serialized() {
// // Q|A5000|^987654321<CR>
// return type + "|" + deviceName + "|" + "^" + barcode + "\r";
// }
//
// @Override public String getType() {
// return type;
// }
//
// @Override
//
// @Override public String getType() {
// return "O";
// }
//
// public static OFrame fromSerialized(String serialized) throws BiLisFrameParseException {
// OFrame frame = new OFrame();
// String[] parts = serialized.split("\\|");
// if (parts.length != 8) {
// throw new BiLisFrameParseException(BiLisFrameParseException.ErrorType.FrameFieldNumberError, "Invalid OFrame : " + serialized);
// }
// frame.type = parts[0];//O1
// frame.deviceName = parts[1];//O2
// frame.sampleId = parts[2];//O3
// var projList = List.of(parts[4].split("\\\\"));//O5
// //|^COVID-19 Ab^^ 去除掉所有^
// for (var projName : projList) {
// projName = projName.replace("^^", "");
// projName = projName.replace("^", "");
// frame.projList.add(projName);
// }
// frame.sampleType = parts[5].isEmpty() ? null : Integer.parseInt(parts[5]);//O6
// frame.samplingDate = parts[7];//O8
// return frame;
// }
}

13
src/main/java/a8k/app/type/lisprotocol/subframe/OFrame.java

@ -0,0 +1,13 @@
package a8k.app.type.lisprotocol.subframe;
import java.util.ArrayList;
import java.util.List;
public class OFrame {
public String type = "O"; // 请求类型
public String deviceName = "A8000";
public String sampleId = "";//
public List<String> projList = new ArrayList<>();
public Integer sampleType;//全血0,血清血浆1,尿2,粪便3,唾液4
public String samplingDate;//采样时间 YYYYMMDDhhmmss
}

7
src/main/java/a8k/app/type/lisprotocol/subframe/QFrame.java

@ -0,0 +1,7 @@
package a8k.app.type.lisprotocol.subframe;
public class QFrame {
public String type = "Q"; // 请求类型
public String deviceName = "A8000";
public String barcode = "";//格式示例 ^abcdef1234567
}

38
src/main/java/a8k/app/type/lisprotocol/subframe/RFrame.java

@ -0,0 +1,38 @@
package a8k.app.type.lisprotocol.subframe;
public class RFrame {
// DEMO
//Example: (每次只发送一条信息)
//R|A5000p|1234567890|^hsCRP^^#|18.57|mg/L||||F||||20211209100114|<CR>
//R|A10|123456789|^CRP^^#|76|mg/L|0.5-200||L_4^_02|F||||20141201144906||0|<CR>
//R|A10||^COVID-19 Ag^^%||||Positive|R_1^_01|F||||20210428153944||4|<CR>
//
//二联卡 (单项-PCT/CRP):
//R|A10|123456789|^PCT/CRP^CRP^#|46|mg/L|0.5-200||L_1^_03|F||||20141201144906||0|<CR>
//R|A10|123456789|^PCT/CRP^PCT^#|15|ng/mL|0.1-100||L_1^_03|F||||20141201144906||0|<CR>
//
//三联卡 (单项-TnI/CK-MB/Myo):
//R|A5000p|Aa123456|^TnI/CK-MB/Myo^TnI^#|3.84|ng/mL||||F||||20211209101158|<CR>
//R|A5000p|Aa123456|^TnI/CK-MB/Myo^CK-MB^#|46.39|ng/mL||||F||||20211209101158|<CR>
//R|A5000p|Aa123456|^TnI/CK-MB/Myo^Myoglobin^#|45.25|ng/mL||||F||||20211209101158|<CR>
//
//COVID-19 Ab:
//R|A10|123456789|^COVID-19 Ab^IgG^@||||Positive|L_1^|F||||20141201144906||1|<CR>
//R|A10|123456789|^COVID-19 Ab^IgM^@||||Negative|L_1^ |F||||20141201144906||1|<CR>
public String type = "R"; // 信息类型 R-1
public String deviceName = "A8000"; //设备的名称 R-2
public String sampleId = "";//样本ID,条形码 R-3
public String projInfo = "";//R-4 项目名+子项目名+结果类型(定性,定量,半定性)
public String resultVal = "";//R-5
public String resultUnit = "";//结果单位 R-6
public String refRange = "";//参考范围,也可以为空 R-7
public String qualitativeResult = "";//定性结果,如果是定量结果,则该值为空 R-8
public String samplePosInfo = "";//通道号+试管号 R9
public String resultType = "F";//always F R10
public String pad0 = "";// 填充位,如果没有填充则为空 R11
public String startIncubationTime = "";//开始孵育时间 R12 YYYYMMDDhhmmsss
public String pad1 = "";// 填充位,如果没有填充则为空 R13
public String endIncubationTime = "";//结束孵育时间 R14 YYYYMMDDhhmmsss
public String pid = "";// 用户输入的ID R15
public String sampleType = "1";//样本类型
}

15
src/main/java/a8k/app/utils/ThreadUtils.java

@ -1,5 +1,6 @@
package a8k.app.utils;
import a8k.OS;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@ -10,6 +11,12 @@ public class ThreadUtils {
boolean isConditionMet();
}
private static long getMonotonicClockTimestamp() {
return System.nanoTime() / 1_000_000; // 转换为毫秒
}
static public void threadForceJoin(Thread thread) {
boolean isInterrupted = false;
while (thread.isAlive()) {
@ -27,9 +34,9 @@ public class ThreadUtils {
static public void forceSleep(long millis) {
boolean isInterrupted = false;
long startTime = System.currentTimeMillis();
long startTime = getMonotonicClockTimestamp();
while (true) {
int remainingTime = (int) (millis - (System.currentTimeMillis() - startTime));
int remainingTime = (int) (millis - (getMonotonicClockTimestamp() - startTime));
if(remainingTime <= 0) {
break;
}
@ -57,8 +64,8 @@ public class ThreadUtils {
}
static public boolean waitingForSomeCondition(String conditionName,long retryIntervalMillis, long timeoutMillis, ConditionListener condition ) {
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < timeoutMillis) {
long startTime =getMonotonicClockTimestamp();
while (getMonotonicClockTimestamp() - startTime < timeoutMillis) {
if (condition.isConditionMet())
return true; // 如果条件满足直接返回
log.debug("Waiting for condition: {}", conditionName);

8
src/main/java/a8k/extui/page/codetest/BiLisDoubleTrackChannelTestPage.java

@ -34,6 +34,12 @@ public class BiLisDoubleTrackChannelTestPage {
biLisDoubleTrackChannel.ping();
}
synchronized public void execRequestSampleInfo(String sampleId) {
biLisDoubleTrackChannel.requestSampleInfo(sampleId);
log.info("请求样本信息: {}", sampleId);
}
@PostConstruct
public void init() {
var page = extApiPageMgr.newPage(this);
@ -42,6 +48,8 @@ public class BiLisDoubleTrackChannelTestPage {
.setParamVal("serverPort", () -> serverPort);
page.addFunction("新建连接", this::newConnect);
page.addFunction("执行Ping", this::execPing);
page.addFunction("请求样本信息", this::execRequestSampleInfo)
.setParamVal("sampleId", () -> "");
extApiPageMgr.addPage(page);
}

19
src/main/resources/application.yml

@ -1,15 +1,16 @@
#WEB虚拟后端
server.port: 80
iflytophald.ip: 127.0.0.1
iflytophald.enable: false
device.runmode: "VirtualStateGenerateMode"
lis.enable: false
#server.port: 80
#iflytophald.ip: 127.0.0.1
#iflytophald.enable: false
#device.runmode: "VirtualStateGenerateMode"
#lis.enable: false
#PC调试
#server.port: 80
#device.runmode: "RealMode"
#iflytophald.ip: 192.168.8.10
#iflytophald.enable: true
server.port: 80
device.runmode: "RealMode"
iflytophald.ip: 192.168.8.10
iflytophald.enable: true
lis.enable: false
#硬件测试
#server.port: 8082

Loading…
Cancel
Save