Modus協議是由MODICON(現為施耐德電氣公司的一個品牌)在1979年開發的,是全球第一個真正用於工業現場的匯流排協議,應用非常廣泛,可謂大名鼎鼎。
理論性的東西就不多介紹了,推薦一本書《Modbus軟體開發實戰指南》,楊更更著,寫得非常好,從理論到實戰,手把手教你玩轉Modbus,不過程式碼實戰部分使用的是C#,筆者沒練過這項武功,還是看一下Java中怎麼應用吧,網上資料多用Modbus4J,就選它了。
Modbus4J原始碼:https://github.com/infiniteautomation/modbus4j
Modbus4J沒有提供底層串列埠驅動,因此需要先掌握一些Java串列埠程式設計的能力,快速入門可以參考筆者之前寫的《Java串列埠程式設計例子》這篇文章,本文就是在此基礎上進行的,也應用了其中的串列埠程式設計工具類程式碼。
建立專案
新建modbus4j專案,如下圖所示:
原始碼地址:https://github.com/wu-boy/modbus4j.git
測試步驟
實現底層串列埠驅動
Modbus4J沒有提供底層串列埠驅動,因此使用串列埠工具類SerialPortUtils來開啟和關閉串列埠,程式碼如下:
public class SerialPortUtils {
private static Logger log = LoggerFactory.getLogger(SerialPortUtils.class);
/**
* 打卡串列埠
* @param portName 串列埠名
* @param baudRate 波特率
* @param dataBits 資料位
* @param stopBits 停止位
* @param parity 校驗位
* @return 串列埠物件
*/
public static SerialPort open(String portName, Integer baudRate, Integer dataBits,
Integer stopBits, Integer parity) {
SerialPort result = null;
try {
// 通過埠名識別埠
CommPortIdentifier identifier = CommPortIdentifier.getPortIdentifier(portName);
// 開啟埠,並給埠名字和一個timeout(開啟操作的超時時間)
CommPort commPort = identifier.open(portName, 2000);
// 判斷是不是串列埠
if (commPort instanceof SerialPort) {
result = (SerialPort) commPort;
// 設定一下串列埠的波特率等引數
result.setSerialPortParams(baudRate, dataBits, stopBits, parity);
log.info("開啟串列埠{}成功", portName);
}else{
log.info("{}不是串列埠", portName);
}
} catch (Exception e) {
log.error("開啟串列埠{}錯誤", portName, e);
}
return result;
}
/**
* 關閉串列埠
* @param serialPort
*/
public static void close(SerialPort serialPort) {
if (serialPort != null) {
serialPort.close();
log.warn("串列埠{}關閉", serialPort.getName());
}
}
}
實現Modbus4J串列埠包裝器介面
Modbus4J提供了串列埠包裝器介面,但是沒有提供實現,因此自己新建一個實現類SerialPortWrapperImpl,作用是為Modbus4J提供串列埠物件SerialPort和操作串列埠的方法,例如開啟/關閉串列埠,獲取串列埠輸入/輸出流等,核心程式碼如下:
模擬從站裝置
RtuSlaveTest類模擬了一個地址為1的從站裝置,使用串列埠“COM2“(請提前使用虛擬串列埠軟體Virtual Serial Port Driver模擬出來COM1和COM2串列埠),通過ModbusFactory建立RtuSlave,然後模擬線圈狀態、離散輸入狀態、保持暫存器和輸入暫存器的資料,程式碼中有詳細註釋,程式碼如下:
public class RtuSlaveTest {
public static void main(String[] args) {
createRtuSlave();
}
public static void createRtuSlave(){
// 設定串列埠引數,串列埠是COM2,波特率是9600
SerialPortWrapperImpl wrapper = new SerialPortWrapperImpl("COM2", 9600,
SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE, 0, 0);
// Modbus工廠,可以建立RTU、TCP等不同型別的Master和Slave
ModbusFactory modbusFactory = new ModbusFactory();
final ModbusSlaveSet slave = modbusFactory.createRtuSlave(wrapper);
// 這玩意網上有人叫做過程影像區,其實就是暫存器
// 暫存器裡可以設定線圈狀態、離散輸入狀態、保持暫存器和輸入暫存器
// 這裡設定了從站裝置ID是1
BasicProcessImage processImage = new BasicProcessImage(1);
processImage.setInvalidAddressValue(Short.MIN_VALUE);
slave.addProcessImage(processImage);
// 新增監聽器,監聽slave線圈狀態和保持暫存器的寫入
processImage.addListener(new MyProcessImageListener());
setCoil(processImage);
setInput(processImage);
setHoldingRegister(processImage);
setInputRegister(processImage);
// 開啟執行緒啟動從站裝置
new Thread(() -> {
try {
slave.start();
}
catch (ModbusInitException e) {
e.printStackTrace();
}
}).start();
/*new Timer().schedule(new TimerTask() {
@Override
public void run() {
// 間隔1秒修改從站裝置1的保持暫存器資料
updateHoldingRegister(slave.getProcessImage(1));
}
}, 1000, 1000);*/
}
private static void setCoil(ProcessImage processImage){
// 模擬線圈狀態
processImage.setCoil(0, true);
processImage.setCoil(1, false);
processImage.setCoil(2, true);
}
private static void setInput(ProcessImage processImage){
// 模擬離散輸入狀態
processImage.setInput(0, false);
processImage.setInput(1, true);
processImage.setInput(2, false);
}
private static void setHoldingRegister(ProcessImage processImage){
// 模擬保持暫存器的值
processImage.setHoldingRegister(0,(short) 11);
processImage.setHoldingRegister(1,(short) 22);
processImage.setHoldingRegister(2,(short) 33);
}
private static void updateHoldingRegister(ProcessImage processImage){
// 模擬修改保持暫存器的值
processImage.setHoldingRegister(0, (short) RandomUtil.randomInt(0, 100));
processImage.setHoldingRegister(1,(short) RandomUtil.randomInt(0, 100));
processImage.setHoldingRegister(2,(short) RandomUtil.randomInt(0, 100));
}
private static void setInputRegister(ProcessImage processImage){
// 模擬輸入暫存器的值
processImage.setInputRegister(0,(short) 44);
processImage.setInputRegister(1,(short) 55);
processImage.setInputRegister(2,(short) 66);
}
}
使用Modbus Poll測試模擬的從站裝置
Modbus Poll和Modbus Slave分別是主站裝置模擬工具和從站裝置模擬工具,是Modbus開發最常用的兩個測試軟體,下載地址:https://www.modbustools.com/
網上最近出現了一個國產軟體Mthings,能夠同時支援模擬主從機功能,據說功能強大還有使用手冊,免安裝免費使用!筆者由於參考了《Modbus軟體開發實戰指南》這本書,就沒使用Mthings,有興趣的同學可以試用。
- 設定連線引數
下載安裝後,開啟連線引數進行設定,如下圖所示:
RtuSlaveTest類使用了串列埠COM2來模擬從站裝置,因此這裡選擇COM1,選擇RTU模式,點選OK。
- 定義讀寫規則
選擇選單【Setup】->【Read/Write Definition…】,如下圖所示:
設定從站裝置地址為1,功能碼03是讀取保持暫存器資料,暫存器地址為0,數量為3,因為RtuSlaveTest程式中模擬了3個資料,點選OK,如下圖所示:
可以看到讀取到了RtuSlaveTest程式中模擬的3個暫存器的資料,注意別忘了先啟動RtuSlaveTest程式!
選擇不同的功能碼就可以讀取不同的資料,01讀取線圈狀態,02讀取離散輸入狀態,03讀取保持暫存器,04讀取輸入暫存器。
模擬主站裝置
實際開發中可能更多的是開發主站裝置程式,RtuMasterTest程式碼如下:
public class RtuMasterTest {
public static void main(String[] args) throws Exception{
createRtuMaster();
}
private static void createRtuMaster() throws Exception{
// 設定串列埠引數,串列埠是COM1,波特率是9600
SerialPortWrapperImpl wrapper = new SerialPortWrapperImpl("COM1", 9600,
SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE, 0, 0);
ModbusFactory modbusFactory = new ModbusFactory();
ModbusMaster master = modbusFactory.createRtuMaster(wrapper);
master.init();
// 從站裝置ID是1
int slaveId = 1;
// 讀取保持暫存器
readHoldingRegisters(master, slaveId, 0, 3);
// 將地址為0的保持暫存器資料修改為0
writeRegister(master, slaveId, 0, 0);
// 再讀取保持暫存器
readHoldingRegisters(master, slaveId, 0, 3);
}
private static void readHoldingRegisters(ModbusMaster master, int slaveId, int start, int len) throws Exception{
ReadHoldingRegistersRequest request = new ReadHoldingRegistersRequest(slaveId, start, len);
ReadHoldingRegistersResponse response = (ReadHoldingRegistersResponse) master.send(request);
if (response.isException()){
System.out.println("讀取保持暫存器錯誤,錯誤資訊是" + response.getExceptionMessage());
}else {
System.out.println("讀取保持暫存器=" + Arrays.toString(response.getShortData()));
}
}
private static void writeRegister(ModbusMaster master, int slaveId, int offset, int value) throws Exception{
WriteRegisterRequest request = new WriteRegisterRequest(slaveId, offset, value);
WriteRegisterResponse response = (WriteRegisterResponse) master.send(request);
if (response.isException()){
System.out.println("寫保持暫存器錯誤,錯誤資訊是" + response.getExceptionMessage());
}else{
System.out.println("寫保持暫存器成功");
}
}
}
先啟動RtuSlaveTest從站裝置模擬程式,再啟動RtuMasterTest主站裝置模擬程式,可以看到雙方控制檯均有預期輸出,RtuMasterTest能夠讀寫RtuSlaveTest中的資料。
參考資料
1、初探ModBus4j簡單使用指南
2、使用java的modbus4j的Rtu方式獲取監測資料
3、Modbus java slave app