[背景]
化工园区封闭化建设,出入需要称重,所以加装了地磅。现有两个卡口,每个卡口进出分别各有一个地磅,每个卡口配一台主机,两个地磅都直接连接到电脑主机上,仪表232接电脑串口(仪表15芯7、8接电脑九芯2、5)。业务需求是提供一个接口能够读取指定地磅的实时数据,所以就开发一个程序部署在这台主机上读取串口数据并往外提供最新数据。
准备工作
因为本身并没有出差去现场,加上他们不知道应该怎么把地磅和主机接起来,所以远程跟运维沟通了下,现场发来卡口主机接口如下:
我一看,以我贫瘠的硬件知识来说貌似也没太多选择,买俩usb转485线应该就可以了。然后打开京东,搜索485转USB
打开第一家,转发物品链接,让运维去采购好了。(其实我觉得这线绝对贵了很多,不过也不用我掏钱,也懒的再去找了😂)
过了一个周末,线到了。开始配合现场调试数据,听说现场调试地磅的还是一个60多的老前辈,指着串口返回的一堆二进制的00数据跟我说地磅调试正常了。我是一脸懵逼,这和报文协议咋不一样,而且这二进制的00能解析出啥?这个时候我是有点开始怀疑自己了,难道是报文协议看漏了,还是我用的串口工具有问题啊,还没等我安装上另外的串口工具,结果老前辈发消息来说是线没接上🥴。我估摸着有点扯淡的,线没接也打不开串口啊,当然也有可能是其他部件的线没接上导致数据不是正常数据。反正是经过一通折腾,终于有正经的数据返回了。
报文协议
文档的链接为:
https://mf.alllf.com/cloudpic/2024/07/c530a6c79d0d846679f9e3a4aade3a88.pdf
主要核心内容包含如下:
源码说明
因为只会java
所以就用java写的,其实要是用go
的话代码应该会简略很多,想了想以后再学吧,现在用java写写得了。
引入核心串口通讯依赖
1 2 3 4 5
| <dependency> <groupId>com.fazecast</groupId> <artifactId>jSerialComm</artifactId> <version>2.11.0</version> </dependency>
|
串口打开、关闭、发送数据、接收数据的类开发
每个方法的作用详细见方法注释,初始化时会遍历所有可以打开的串口,并将串口储存在map中便于后续的使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
| package cn.allbs.weightscale.config; import cn.allbs.weightscale.exception.BhudyException; import cn.allbs.weightscale.handler.SerialPortListener; import cn.allbs.weightscale.util.SerialPortUtil; import com.fazecast.jSerialComm.SerialPort; import jakarta.annotation.PostConstruct; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.io.ByteArrayOutputStream; import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; import java.util.Optional;
@Slf4j @Component public class SerialPortManager { private final Map<String, SerialPort> serialPorts; private final Map<String, SerialPortListener> listeners; @Resource private RedisTemplate redisTemplate; @Resource private SerialPortConfig serialPortConfig; public SerialPortManager() { serialPorts = new HashMap<>(); listeners = new HashMap<>(); } @PostConstruct public void init() { log.info("\nUsing Library Version v{}", SerialPort.getVersion()); SerialPort[] ports = SerialPort.getCommPorts(); log.info("\nAvailable Ports:\n"); Map<String, String> portMappings = serialPortConfig.getPortMappings(); for (SerialPort port : ports) { log.info("{}: {} - {}", port.getSystemPortName(), port.getDescriptivePortName(), port.getPortDescription()); serialPorts.put(port.getSystemPortName(), port); openPort(port.getSystemPortName()); } } public boolean addPort(String portName) { SerialPort[] ports = SerialPort.getCommPorts(); for (SerialPort port : ports) { if (port.getSystemPortName().equals(portName)) { serialPorts.put(portName, port); log.info("Added Port: {} - {}", port.getSystemPortName(), port.getDescriptivePortName()); return true; } } log.info("Port {} not found!", portName); return false; }
public void openPort(String portName) { SerialPort port = serialPorts.get(portName); if (port == null) { log.info("Port {} is not managed!", portName); return; } if (!port.isOpen()) { log.info("\nPre-setting RTS: {}", port.setRTS() ? "Success" : "Failure"); if (!port.openPort()) { log.info("Open serial port {} error!", portName); return; } log.info("\nOpening {}: {} - {}", port.getSystemPortName(), port.getDescriptivePortName(), port.getPortDescription()); port.setFlowControl(SerialPort.FLOW_CONTROL_DISABLED); port.setComPortParameters(9600, 8, SerialPort.ONE_STOP_BIT, SerialPort.NO_PARITY); port.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING | SerialPort.TIMEOUT_WRITE_BLOCKING, 1000, 1000); String redisKey = Optional.ofNullable(serialPortConfig.getPortMappings()).map(a -> a.get(portName)).orElse("pc:weight:unknown"); SerialPortListener listener = new SerialPortListener(port, portName, redisTemplate, redisKey); listeners.put(portName, listener); Thread listenerThread = new Thread(listener); listenerThread.start(); } }
public boolean isPortOpen(String portName) { SerialPort port = serialPorts.get(portName); return port != null && port.isOpen(); }
public void closePort(String portName) { SerialPort port = serialPorts.get(portName); SerialPortListener listener = listeners.get(portName); if (port != null && port.isOpen()) { if (listener != null) { listener.stop(); listeners.remove(portName); } port.closePort(); log.info("Closed Port: {}", portName); } }
public int write(String portName, byte[] data) { SerialPort port = serialPorts.get(portName); if (port == null || !port.isOpen()) { return 0; } return port.writeBytes(data, data.length); }
public int read(String portName, byte[] data) { SerialPort port = serialPorts.get(portName); if (port == null || !port.isOpen()) { return 0; } return port.readBytes(data, data.length); }
public byte[] writeAndRead(String portName, byte[] bytes) { byte[] resultData = null; try (ByteArrayOutputStream bao = new ByteArrayOutputStream()) { SerialPort port = serialPorts.get(portName); if (port == null || !port.isOpen()) throw new BhudyException("Port not open or not found: " + portName); int numWrite = port.writeBytes(bytes, bytes.length); if (numWrite > 0) { Thread.sleep(100); while (port.bytesAvailable() > 0) { byte[] newData = new byte[port.bytesAvailable()]; int numRead = port.readBytes(newData, newData.length); if (numRead > 0) { bao.write(newData); } } resultData = bao.toByteArray(); } } catch (Exception e) { throw new BhudyException(e.getMessage()); } return resultData; }
public String readWeightOnce(String portName) { try { byte[] readBuffer = new byte[12]; SerialPort port = serialPorts.get(portName); int numRead = port.readBytes(readBuffer, readBuffer.length); if (numRead > 0) { String result = SerialPortUtil.parseWeightData(readBuffer); log.info("{}读取只读取一次串口{}的数据为:{}", LocalDateTime.now(), portName, result); return result; } } catch (Exception e) { throw new BhudyException(e.getMessage()); } return null; }
public void closeAllPorts() { for (String portName : serialPorts.keySet()) { closePort(portName); } } }
|
报文解析工具类
包含指令生成和报文解析方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
| package cn.allbs.weightscale.util; import cn.allbs.weightscale.exception.BhudyException; import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; import java.util.Arrays;
@Slf4j @UtilityClass public class SerialPortUtil {
public static byte[] hexStringToByteArray(String s) { s = s.replaceAll("\\s", ""); int len = s.length(); if (len % 2 != 0) { throw new IllegalArgumentException("Invalid hex string: " + s); } byte[] data = new byte[len / 2]; for (int i = 0; i < len; i += 2) { int high = Character.digit(s.charAt(i), 16); int low = Character.digit(s.charAt(i + 1), 16); if (high == -1 || low == -1) { throw new IllegalArgumentException("Invalid hex character in string: " + s); } data[i / 2] = (byte) ((high << 4) + low); } return data; }
public static String byteArrayToHexString(byte[] byteArray) { StringBuilder hexString = new StringBuilder(); for (byte b : byteArray) { hexString.append(String.format("%02X ", b)); } return hexString.toString().trim(); }
public static String parseWeightData(byte[] data) { if (data.length < 12) { throw new BhudyException("串口未接收到数据或者数据长度不够,返回数据内容为" + Arrays.toString(data)); } if (data[0] != 0x02 || data[data.length - 1] != 0x03) { throw new IllegalArgumentException("Invalid data format"); } byte[] payload = Arrays.copyOfRange(data, 1, data.length - 3); byte xorHigh = data[data.length - 3]; byte xorLow = data[data.length - 2]; byte end = data[data.length - 1]; byte calculatedXor = 0; for (int i = 1; i < data.length - 3; i++) { calculatedXor ^= data[i]; } byte expectedXorHigh = (byte) ((calculatedXor >> 4) & 0x0F); byte expectedXorLow = (byte) (calculatedXor & 0x0F); expectedXorHigh += (byte) ((expectedXorHigh <= 9) ? 0x30 : 0x37); expectedXorLow += (byte) ((expectedXorLow <= 9) ? 0x30 : 0x37); if (expectedXorHigh != xorHigh || expectedXorLow != xorLow) { log.info("校验失败,报文中高四位{},低四位{};主动校验后的高四位:{},低四位{};", xorHigh, xorLow, expectedXorHigh, expectedXorLow); } return parseWeight(payload); }
private static String parseWeight(byte[] payload) { if (payload.length != 8) { throw new IllegalArgumentException("Invalid payload length for weight data"); } char sign = (char) payload[0]; String weightValue = new String(Arrays.copyOfRange(payload, 1, 7)).trim(); int decimalPointPosition = payload[7] - '0'; StringBuilder weight = new StringBuilder(weightValue); if (decimalPointPosition > 0 && decimalPointPosition < weight.length()) { weight.insert(weight.length() - decimalPointPosition, '.'); } String weightString = sign + weight.toString(); double weightNumber = Double.parseDouble(weightString); if (decimalPointPosition == 0) { return String.valueOf((int) weightNumber); } else { return String.valueOf(weightNumber); } }
public static byte calculateXorChecksum(byte[] data) { byte xor = 0; for (byte b : data) { xor ^= b; } return xor; }
public byte[] generateCommand(String address, char command) { byte start = 0x02; byte end = 0x03; byte addressByte = (byte) address.charAt(0); byte commandByte = (byte) command; byte xor = (byte) (addressByte ^ commandByte); byte xorHigh = (byte) ((xor >> 4) & 0x0F); byte xorLow = (byte) (xor & 0x0F); xorHigh += (byte) ((xorHigh <= 9) ? 0x30 : 0x37); xorLow += (byte) ((xorLow <= 9) ? 0x30 : 0x37); return new byte[]{start, addressByte, commandByte, xorHigh, xorLow, end}; } public static void main(String[] args) { byte[] data = { 0x02, 0x2B, 0x30, 0x30, 0x31, 0x35, 0x30, 0x30, 0x30, 0x31, 0x46, 0x03 }; String result = parseWeightData(data); System.out.println(result); } }
|
串口监听器
考虑到串口持续输出数据,所以在串口初始化时,起一个线程专门监听串口数据并解析存储数据。为了减轻cpu压力,设置每100毫秒接收解析一次数据,实际上1秒一次也绝对够了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| package cn.allbs.weightscale.handler; import cn.allbs.weightscale.constants.CommonConstants; import cn.allbs.weightscale.util.SerialPortUtil; import com.fazecast.jSerialComm.SerialPort; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @Slf4j public class SerialPortListener implements Runnable { private final SerialPort serialPort; private final String portName; private final RedisTemplate<String, Object> redisTemplate; private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); private final String redisKey; public SerialPortListener(SerialPort serialPort, String portName, RedisTemplate<String, Object> redisTemplate, String redisKey) { this.serialPort = serialPort; this.portName = portName; this.redisTemplate = redisTemplate; this.redisKey = redisKey; } @Override public void run() { scheduler.scheduleAtFixedRate(this::readFromPort, 0, 100, TimeUnit.MILLISECONDS); } private void readFromPort() { try { if (serialPort.bytesAvailable() > 0) { byte[] readBuffer = new byte[12]; int numRead = serialPort.readBytes(readBuffer, readBuffer.length); if (numRead > 0) { String result = SerialPortUtil.parseWeightData(readBuffer); log.info("{}读取到串口{}的数据为:{}", LocalDateTime.now().format(DateTimeFormatter.ofPattern(CommonConstants.DATETIME_PATTERN)), portName, result); redisTemplate.opsForValue().set(redisKey, result); } } } catch (Exception e) { log.error("Error reading from serial port", e); } } public void stop() { scheduler.shutdown(); } }
|
其他相关说明
因为本来根据业务需求,指令应答方式的形式获取数据肯定是够了,所以一开始就偷了个懒没做连续发送方式的解析代码。但是老师傅在现场一句指令应答做不了给断送了。。。。只能使用连续发送的方式来获取数据,也就是项目中后续加了启动线程监听相关代码的原因。
接口说明
本项目目的是给其他服务提供地磅最新数据,所以加了接口供其他服务调用。接口文档路径为{ip}:{port}/dic.html
,本项目默认设置端口为7878
连续发送方式
顾名思义项目启动后会一直接收串口数据,本项目中考虑到连续发送间隔时间太短,一是无意义数据较多,二是cpu负荷较大,所以额外起了线程,每100毫秒接收并解析一次,根据自己需要设置。
调整方式为修改SerialPortListener
类中的scheduler.scheduleAtFixedRate(this::readFromPort, 0, 100, TimeUnit.MILLISECONDS);
period即可,现在设置的时100
,单位为毫秒
获取该数据有两种方式:
- 一种是获取缓存到redis中的
pc:weight:*
,这个*
代表的是不同地磅缓存的数据,具体定义见application.yml的active和SerialPortConfig
的getPortMappings
方法获取的rediskey值
- 第二种是通过接口,
/weight
,这个接口是获取最新的一次称重数据,如果没有称重数据则返回null
,参数需要传指定的串口名称,比如我当前项目两个串口分别为COM3
,COM4
一个进的地磅一个出的地磅
指令应答方式
根据指令获取具体数据
只能通过接口获取
接口: /scale
传的参数有
address
地址,从A~Z
operationCode
操作方式,A握手,B读毛重,C读皮重,D读净重
portName
串口,比如当前项目的两个串口COM3
,COM4
,其他项目可能是COM1
,COM2
之类的。
项目运行效果
部署
考虑到是在windows中运行, 万一主机关机啥的得弄个开机自启,所以使用的是Windows Service Wrapper
这个工具
下载地址
http://repo.jenkins-ci.org/releases/com/sun/winsw/winsw
使用方式
将下载下来的exe名称修改为jar包相同的名称
然后新增一个和jar相同名称的xml,内置内容如下:
1 2 3 4 5 6 7 8 9 10
| <service> <id>weight-scale</id> <name>Weight Scale Service</name> <description>卡口进出地磅称重数据</description> <executable>java</executable> <arguments>-jar "D:\weight-scale\weight-scale-0.0.1.jar"</arguments> <logmode>none</logmode> <startmode>Automatic</startmode> <stoptimeout>30sec</stoptimeout> </service>
|
需要注意其中的logmod我这边设置的是none
,因为内置了logback,有对应的日志文件在logs中所以不需要额外的logmod。
注册jar为系统服务并启动
1 2 3 4 5 6 7 8
| ./weight-scale-0.0.1.exe install
./weight-scale-0.0.1.exe start
./weight-scale-0.0.1.exe stop
./weight-scale-0.0.1.exe uninstall
|
最后附上项目源码
如果对你有帮助,给个star哦😘
https://github.com/chenqi92/weight-scale.git