1. 系统概述
1.2 硬件架构
┌─────────────────────────────────────────────────────┐
│ 医疗检测设备 │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ 泵 │ │ 电磁阀 │ │ 传感器 │ │ LED │ │
│ │(抽气) │ │(开关) │ │(检测) │ │(指示) │ │
│ └────────┘ └────────┘ └────────┘ └────────┘ │
│ ↓ │
│ ┌──────────┐ │
│ │ 主控板 │ │
│ │ (Arduino) │ │
│ └─────┬─────┘ │
│ │ RS232 │
│ ↓ │
└─────────────┼─────────────────────────────────────┘
↓
┌──────────┐
│ 电脑 │
│ (Java) │
└──────────┘
1.3 通信方式
- 接口:RS232 串口(COM1/COM2/COM3...)
- 速率:9600/115200bps
- 协议:自定义二进制
2. 通信协议详解
2.1 协议帧结构
┌─────┬─────┬────────┬────────┬────────┬─────┐
│ 0x5A│ 0xA5│ 长度+5 │设备ID高│设备ID低│命令 │
└─────┴─────┴────────┴────────┴────────┴─────┘
固定头 固定头
+─────┬─────┬─────┬──────┐
│参数1│参数2│参数3│校验和│
└─────┴─────┴─────┴──────┘
示例解析(控制泵速度 = 6)
java
// 十六进制表示
5A A5 08 02 05 B8 06 00 XX
// 字节分解
0x5A → 固定头1
0xA5 → 固定头2
0x08 → 数据长度(8字节) + 5
0x02 → 主控板ID高字节
0x05 → 应用模块ID(5=泵控制模块)
0xB8 → 命令字(控制泵)
0x06 → 泵速度低字节(6)
0x00 → 泵速度高字节(0)
0xXX → 校验和
2.2 设备 ID 定义
| ID | 模块 | 功能 |
|---|---|---|
| 0x02 | 主控板 | 协调各模块 |
| 0x01 | APP1 | 电磁阀控制 |
| 0x03 | APP3 | 电压采集 |
| 0x05 | APP5 | 泵控制 |
| 0x06 | APP6 | 打印控制 |
2.3 创建协议帧
协议帧 = 固定格式的 "数据容器",专门解决 "双方怎么准确传数据" 的问题。
java
// NewProtocol.java 第123-164行
public byte[] createOrder(final Order order) {
byte nCount = (byte) (order.commad_str[1] & 0x0f);
byte[] orders = new byte[nCount + 8];
// 固定头
orders[0] = 0x5a;
orders[1] = (byte) 0xa5;
orders[2] = (byte) ((1 << 7) + nCount + 5); // 长度+标志
// 设备ID
orders[3] = (byte) (order.commid_str[0] & 0x1f);
orders[4] = (byte) (order.commid_str[1] & 0x1f);
// 命令
orders[5] = order.commad_str[0];
orders[6] = (byte) ((order.commad_str[1] & 0xf0) >> 4);
// 参数(1-8个)
switch (nCount) {
case 8: orders[14] = order.part4[1];
case 7: orders[13] = order.part4[0];
case 6: orders[12] = order.part3[1];
case 5: orders[11] = order.part3[0];
case 4: orders[10] = order.part2[1];
case 3: orders[9] = order.part2[0];
case 2: orders[8] = order.part1[1];
case 1: orders[7] = order.part1[0];
}
// 计算校验和
int num = 0;
for (int i = 3; i < (nCount + 7); i++) {
num += orders[i];
}
orders[nCount + 7] = (byte) num;
return orders;
}
3. 硬件控制接口详解
3.1 泵控制 ctrlPump(int nRate)
java
// 第248-266行
public void ctrlPump(int nRate) {
Order order = new Order();
order.commid_str[0] = MASTER_ID; // 0x02 主控板
order.commid_str[1] = APP5_ID; // 0x05 泵模块
order.commad_str[0] = COMMAND_HGKGP_PUMP; // 0xB8 命令
// 速度值(16位)
order.part1[0] = (byte) (nRate & 0xff); // 低字节
order.part1[1] = (byte) ((nRate >> 8) & 0xff); // 高字节
base.sendOrder(new String(createOrder(order), "XXXXXX"));
}
- 速度参数含义:
0= 停止,6= 低速采集,8= 中速清空,10= 快速清空
3.2 电磁阀控制 ctrlValves(int nIndex, boolean bOpen)
java
// 第269-299行
public void ctrlValves(int nSample, boolean bOpen) {
Order order = new Order();
order.commid_str[0] = MASTER_ID; // 0x02 主控板
order.commid_str[1] = APP1_ID; // 0x01 电磁阀模块
order.commad_str[0] = COMMAND_HGKGP_SETTAP; // 0xXX
if (bOpen) {
order.commad_str[1] = (byte) ((COMMAND_SETTAP_OPEN << 4) + 3);
} else {
order.commad_str[1] = (byte) ((COMMAND_SETTAP_CLOSE << 4) + 3);
}
// 根据电磁阀编号选择地址
if (nSample < 6) {
order.part1[0] = ADDR_TAP1_APP1; // 地址块1
order.part2[0] = (byte) (nSample); // 1-5号阀
} else if (nSample >= 6 && nSample < 12) {
order.part1[0] = ADDR_TAP2_APP1; // 地址块2
order.part2[0] = (byte) (nSample - 6); // 6-11号阀
} else if (nSample >= 12) {
order.part1[0] = ADDR_TAP3_APP1; // 地址块3
order.part2[0] = (byte) (nSample - 12); // 12+号阀
}
base.sendOrder(new String(createOrder(order), "XXXXXX"));
}
物理示例:开 1 号阀→抽 1 号瓶气体,开 2 号阀→抽 2 号瓶气体。
3.3 采样控制 sampleValue(int port)
java
// 第381-391行
public void sampleValue(int nSample) {
if (nSample <= 10) {
ctrlValves(1 + nSample, true); // 打开对应电磁阀
} else if ((nSample > 10) && (nSample < 16)) {
ctrlValves(2 + nSample, true);
} else if (nSample == 16) {
ctrlValves(1, true);
}
}
// 示例:sampleValue(1)
// → ctrlValves(2, true)
// → 打开2号电磁阀
// → 从1号采样瓶抽气
调用示例:sampleValue(1) 打开 2 号电磁阀,抽取 1 号采样瓶气体;sampleValue(2) 打开 3 号电磁阀,抽取 2 号采样瓶气体。
3.4 零点控制 zeroValue(boolean bOpen)
java
// 第374-378行
public void zeroValue(boolean bOpen) {
logger.info("send order 5...");
ctrlValves(12, bOpen); // 控制12号电磁阀
}
- 零点阀作用:切换环境空气,建立测量基准;关闭时接通样气。
3.5 循环模式 circlMode(boolean bOpen)
java
// 第343-371行
public void circlMode(boolean bOpen) {
Order order = new Order();
order.commid_str[0] = MASTER_ID;
order.commid_str[1] = APP1_ID;
order.commad_str[0] = COMMAND_HGKGP_SETTAP;
if (bOpen) {
order.commad_str[1] = (byte) ((COMMAND_SETTAP_OPEN << 4) + 4);
} else {
order.commad_str[1] = (byte) ((COMMAND_SETTAP_CLOSE << 4) + 4);
}
// 控制0号电磁阀
ctrlValves(0, bOpen);
}
- 开循环:气体在腔体循环复用,稳定测量;闭循环:气体单向流动,完成采集后清空。
3.6 LED 控制 ctrlLeds(int nIndex, boolean bOpen)
java
// 第302-334行
public void ctrlLeds(int nIndex, boolean bOpen) {
Order order = new Order();
order.commid_str[0] = MASTER_ID;
order.commid_str[1] = APP1_ID;
order.commad_str[0] = COMMAND_HGKGP_SETLED;
order.commad_str[1] = (byte) ((COMMAND_SETLED_FLASH << 4) + 3);
// 根据索引选择LED地址
int nPort;
if (nIndex <= 10) {
nPort = 4 - (nIndex - 1) / 2;
order.part1[0] = ADDR_LED1_APP1; // LED地址块1
} else {
nPort = (nIndex - 1) / 2 - 4;
order.part1[0] = ADDR_LED2_APP1; // LED地址块2
}
if (bOpen) {
order.part1[1] = (byte) ((0x01 << nPort)); // 点亮
} else {
order.part1[1] = (byte) (0x00); // 熄灭
}
base.sendOrder(new String(createOrder(order), "xxxxxxx"));
}
用途:指示当前端口与设备工作状态。
4. 数据采集:硬件→软件
java
// MainFrame.java 第3109-3158行
case NewProtocol.COMMAND_HGKGP_REALV1V:
// 解包数据
int nData = (order.commad_str[1] >> 4) & 0x0f; // 采样次数
// C12电压(32位)
int nV1 = ((order.part2[1]& 0xff)<<24) |
((order.part2[0]&0xff)<<16) |
((order.part1[1]&0xff)<<8) |
(order.part1[0]&0xff);
// C13电压(32位)
int nV2 = ((order.part4[1]& 0xff)<<24) |
((order.part4[0]&0xff)<<16) |
((order.part3[1]&0xff)<<8) |
(order.part3[0]&0xff);
// 计算平均电压
double C12Sum = nV1 * 1.0 / nData;
double C13Sum = nV2 * 1.0 / nData;
// 添加到实时数据数组
RealTimeData.addRealTimeData(C12Sum, C13Sum);
// 界面更新
SwingUtilities.invokeLater(new Runnable() {
public void run() {
votageChart.addVotageSeriesData(C12Sum, C13Sum);
lC12Votage.setText(String.format("12C:%.2f V", C12Sum));
lC13Votage.setText(String.format("13C:%.2f V", C13Sum));
// 计算并显示厚度
gasChart.addThickSeriesData(
ThicknessData.caculateC12ThickChange(C12Sum),
ThicknessData.caculateC13ThickChange(C13Sum)
);
}
});
5. 串口通信实现
5.1 初始化串口
java
private boolean openCom() {
// 1. 查找串口
portId = CommPortIdentifier.getPortIdentifier("COM1");
// 2. 打开串口(超时2秒)
serialPort = (SerialPort) portId.open("Serial Communication", 2000);
// 3. 设置参数
serialPort.setSerialPortParams(
9600, // 波特率
SerialPort.DATABITS_8, // 数据位
SerialPort.STOPBITS_1, // 停止位
SerialPort.PARITY_NONE // 校验位
);
// 4. 获取输入输出流
outputStream = serialPort.getOutputStream(); // 发送
inputStream = serialPort.getInputStream(); // 接收
// 5. 添加监听器(接收数据)
serialPort.addEventListener(new serialPortListener());
serialPort.notifyOnDataAvailable(true);
bIsConnected = true;
return true;
}
5.2 发送数据
java
// SerialComm.java 第252-287行
public boolean sendOrder(String strOrder) {
if (strOrder != null) {
try {
// 检查串口状态
if (b_com_status && outputStream != null && bIsConnected) {
// 转换为字节并发送
outputStream.write(strOrder.getBytes("xxxxxxxx"));
return true;
}
} catch (Exception e) {
logger.error(e);
// 出错时关闭连接
close();
return false;
}
}
return false;
}
5.3 接收数据
java
// SerialComm.java 第65-124行
public class serialPortListener implements SerialPortEventListener {
public void serialEvent(SerialPortEvent event) {
switch (event.getEventType()) {
case SerialPortEvent.DATA_AVAILABLE: // 有数据到达
while (newData != -1) {
newData = inputStream.read(); // 读一个字节
// 解析协议帧
if (receiveData(newData)) {
// 完整帧,通知监听者
for (CommunicationListener listener : listeners) {
listener.dataAvailable(
new String(m_RecBuf, 0, m_RecNum, "xxxxx")
);
}
// 清空缓冲区
m_RecNum = 0;
}
}
break;
case SerialPortEvent.OE: // 溢出错误
logger.info("溢位错误");
break;
// ... 其他错误
}
}
}
5.4 协议帧解析
java
// SerialComm.java 第311-396行
private boolean receiveData(int nData) {
byte uReChar = (byte) nData;
// 步骤1:检测帧头 0x5A
if ((0x5A == uReChar) && (0 == m_RecNum)) {
m_RecBuf[0] = uReChar;
m_RecNum = 1;
return false;
}
// 步骤2:检测第二字节 0xA5
if ((0x5A == m_RecBuf[0]) && (0xA5 == uReChar)) {
m_RecBuf[1] = uReChar;
m_RecNum = 2;
return false;
}
// 步骤3:读取数据长度
if (m_RecNum == 2) {
m_RecBuf[2] = uReChar;
int nRec = uReChar & 0x7f;
if (nRec < 4 || nRec > 21) {
// 长度异常,清空
m_RecNum = 0;
return false;
}
M_RECNUMB = nRec; // 保存数据长度
m_RecNum = 3;
return false;
}
// 步骤4:读取数据部分
if (m_RecNum < M_RECNUMB + 2) {
m_RecBuf[m_RecNum] = uReChar;
m_RecNum++;
return false;
}
// 步骤5:读取校验和并验证
if (m_RecNum == M_RECNUMB + 2) {
m_RecBuf[m_RecNum] = uReChar; // 校验和
// 计算校验和
int nResult1 = 0;
for (int i = 3; i < M_RECNUMB - 1; i++) {
nResult1 += m_RecBuf[i];
}
nResult1 = nResult1 & 0xff;
// 验证
if (nResult1 == nData) {
return true; // 校验通过
} else {
return false; // 校验失败
}
}
return false;
}
6. 数据计算:从电压到 DOB 值
7.1 电压→光强
java
// RealTimeData.java 第187-189行
private static double caculateVotage(double dLight) {
return dLight * 4.096 / 65535; // 16位ADC转换
}
7.2 光强→浓度
java
// ThicknessData.java
public static double caculateC12ThickChange(double light) {
// 光强 → C12浓度
return ...; // 复杂算法
}
public static double caculateC13ThickChange(double light) {
// 光强 → C13浓度
return ...; // 复杂算法
}
7.3 Delta 计算
java
// ThicknessData.java 第111-113行
public static double caculateDelta(double c12, double c13) {
// Delta = ((C13/C12)/10000 - 标准值) * 1000 / 标准值
return ((c13/c12)/10000 - 0.01123686) * 1000 / 0.01123686;
}
Delta 含义:相对标准偏差(千分率)
7.4 DOB 最终计算
java
// MainFrame.java 第3515-3528行
// Delta30分钟
double del30 = -25 + (delta_3 - delNihe_3);
// Delta0分钟(基准)
double del00 = -25 + (delta_0 - delNihe_0);
// DOB = Delta30 - Delta0
double temp = del30 - curDel00;
// DLL修正
temp = DllUtil.INSTANCE.caculate(curThick, c12, temp);
// 保存最终结果
status.setDob(temp);
// 判断结果
if (temp >= 4.0) {
阳性(+) // 检测到幽门螺杆菌
} else {
阴性(-) // 未检测到
}
7. 调试与常见问题
7.1 串口连接问题
java
// 现象:无法打开串口
// 可能原因:COM口被占用、波特率不匹配、驱动问题
// 解决方案
try {
portId = CommPortIdentifier.getPortIdentifier("COM1");
serialPort = (SerialPort) portId.open("Serial Communication", 2000);
} catch (NoSuchPortException e) {
MessageBoxUtil.showMessageBox(null, "串口不存在");
} catch (PortInUseException e) {
MessageBoxUtil.showMessageBox(null, "串口被占用");
}
7.2 数据接收异常
java
// 现象:接收不到数据
// 检查点:
// 1. 串口是否打开
if (!bIsConnected) {
logger.error("串口未连接");
return;
}
// 2. 数据校验是否正确
if (checksum != calculatedChecksum) {
logger.info("数据校验失败");
return;
}
// 3. 监听器是否注册
serialPort.notifyOnDataAvailable(true);
7.3 硬件响应超时
java
// 现象:发送指令后硬件无响应
// 解决方案:增加重试机制
public boolean sendOrderWithRetry(String order, int maxRetries) {
for (int i = 0; i < maxRetries; i++) {
if (sendOrder(order)) {
// 等待响应
Thread.sleep(100);
if (checkResponse()) {
return true;
}
}
}
return false;
}