From 7dea636d6f62f316fe6a07c0c287522e81ef5d41 Mon Sep 17 00:00:00 2001 From: Joshua Shannon Date: Wed, 20 May 2020 18:48:11 -0500 Subject: [PATCH] Added purejavacomm library, uses api --- README.md | 53 +++++++++-- build.gradle | 7 -- .../io/modbus/AsciiDataEncoder.java | 32 ++++--- .../io/modbus/FunctionCode.java | 13 ++- .../io/modbus/ModbusMessage.java | 12 ++- .../io/modbus/RtuDataEncoder.java | 4 +- .../modbus/handling/ReadHoldingRegisters.java | 10 +-- .../modbus/parsing/DefaultMessageParser.java | 2 +- jSerialComm/build.gradle | 3 +- purejavacomm/build.gradle | 10 +++ .../io/serial/PureJavaCommIOBundle.java | 90 +++++++++++++++++++ .../io/serial/PureJavaSerialPortExample.java | 9 ++ settings.gradle | 1 + 13 files changed, 202 insertions(+), 44 deletions(-) create mode 100644 purejavacomm/build.gradle create mode 100644 purejavacomm/src/main/java/me/retrodaredevil/io/serial/PureJavaCommIOBundle.java create mode 100644 purejavacomm/src/test/java/me/retrodaredevil/io/serial/PureJavaSerialPortExample.java diff --git a/README.md b/README.md index efad427..e752b47 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,38 @@ from a master device and send a response back. This library aims to provide an open source heavily object oriented approach to modbus mappings. -## Features +## Serial Use +This library has an [IOBundle](core/src/main/java/me/retrodaredevil/io/IOBundle.java) interface which consists of +a getters for an InputStream and OutputStream. By importing the `jSerialComm` module, you can use the JSerialIOBundle class +to create a serial port. Because IOBundle is a simple interface, you can easily create your own implementation + +## Modbus Features +* Simple. Nothing is tightly coupled. +* Modbus logic is not coupled to serial port logic or TCP logic. (Use YOUR own `InputStream` and `OutputStream`) * Create custom function codes by implementing `MessageHandler`. * Total freedom to extend any class and override its behavior. -* Supports Ascii, RTU, and TCP encoding. Also create your own custom encoding by implementing `IODataEncoder`. +* Supports Ascii, RTU, and TCP encoding. Also create your own custom encoding by implementing `IODataEncoder` or creating your own `ModbusSlaveBus` * Supports CRC and LRC checksums. Automatically checks CRC while using RTU and LRC while using Ascii. -* Parse ModbusMessages (allows you to easily respond to a master). -* Modbus logic is not coupled to serial logic. -* Uses common interfaces. This makes it easy to swap out implementations. Decide to switch from Ascii encoding to using +* Parse request ModbusMessages (allows you to easily respond to a master). +* Uses common interfaces. This makes it easy to **swap out implementations**. Decide to switch from Ascii encoding to using TCP? No problem. -## Drawbacks +## Defined Modbus Function Codes + +Hex | Dec | Function +---- | --- | -------- +0x01 | 1 | Read Coils +0x02 | 2 | Read Discrete Inputs +0x03 | 3 | Read Holding Registers +0x05 | 5 | Write Single Coil +0x06 | 6 | Write Single Register +0x0F | 15 | Write Multiple Coils +0x10 | 16 | Write Multiple Registers + +You can also define more functions if you need to by extending `MessageHandler`. If you want to respond to other functions, +you can extend `MessageResponseCreator`, which is a subinterface of `MessageHandler`. + +## Modbus Drawbacks * Not set up for asynchronous requests * Not set up for multiple requests at once for TCP (you must request and wait for response) @@ -67,13 +88,19 @@ public float getBatteryVoltage() { } ``` -## Exceptions +## Modbus Exceptions There are many places in this library where checked exceptions are thrown. Such as `MessageParseException`s, `SerialPortException`s. However, you should also be aware of `ModbusRuntimeException`s. These can pop up in just about any place that deals with Modbus. These are runtime exceptions for convenience. You likely aren't able to deal with them when they first pop up, so you usually handle them later up the call stack. -## Using Asynchronously +## Dependencies +If you just import the `core` module, it doesn't have any dependencies. However, it is recommended to also import the +`jSerialComm` module, which will make it easy to interact with serial ports. + +However, if you only need TCP Modbus, this library has 0 dependencies because you only need to import the `core` module. + +## Using Modbus Asynchronously This library doesn't deal with threads at all. Everything is set up to be synchronous. However, if you want to use this asynchronously, you can set up your own way of executing a request asynchronously. This library won't fight you. Since almost everything in this library is immutable you usually don't have to worry about putting locks on @@ -82,7 +109,7 @@ objects because they don't have mutable state. If you do use this asynchronously, remember that you cannot make two requests to two different devices because they may come back out of order, which the library does not support. -## 8 Bit and 16 Bit +## Modbus 8 Bit and 16 Bit Since this project deals with Modbus, there are times when code is dealing with 8 bit data or 16 bit data. Sometimes it can be difficult to tell which one it is. If we wanted code to be most readable, we would make `byte` represent 8 bit data and `short` represent 16 bit data, right? That would make sense, but it wouldn't be very practical because @@ -103,3 +130,11 @@ If you want to test this library, you can use https://www.modbusdriver.com/diags ## TODO * Implement Modbus exception codes and throw Java Exceptions corresponding to them * Support two byte slave addressing +* Check out these serial librarys + * https://github.com/Gurux/gurux.serial.java + * https://github.com/NeuronRobotics/nrjavaserial + * https://github.com/fy-create/JavaSerialPort + +## References +* http://modbus.org/docs/PI_MBUS_300.pdf +* http://www.simplymodbus.ca/FAQ.htm diff --git a/build.gradle b/build.gradle index 8aeaf55..3dccd0e 100644 --- a/build.gradle +++ b/build.gradle @@ -28,13 +28,6 @@ subprojects { } } -project(":jSerialComm") { - apply plugin: 'java' - dependencies { - api project(":core") - } -} - wrapper { gradleVersion = '6.4.1' diff --git a/core/src/main/java/me/retrodaredevil/io/modbus/AsciiDataEncoder.java b/core/src/main/java/me/retrodaredevil/io/modbus/AsciiDataEncoder.java index b64db1a..ae6ba95 100644 --- a/core/src/main/java/me/retrodaredevil/io/modbus/AsciiDataEncoder.java +++ b/core/src/main/java/me/retrodaredevil/io/modbus/AsciiDataEncoder.java @@ -1,5 +1,6 @@ package me.retrodaredevil.io.modbus; +import me.retrodaredevil.io.modbus.handling.ResponseException; import me.retrodaredevil.io.modbus.handling.ResponseLengthException; import java.io.ByteArrayOutputStream; @@ -11,6 +12,10 @@ import java.util.List; public class AsciiDataEncoder implements IODataEncoder { + /* + * Note that since this is ascii, each byte only ever uses 7 bits, so we (sometimes) don't have to worry about negative numbers here + */ + public AsciiDataEncoder() { } @@ -75,7 +80,7 @@ private static char toChar(int b){ * * @param expectedAddress The expected address in the data * @param bytes The ascii data between the ':' and '\r' Not including ':', '\r', or '\n' - * @return + * @return The parsed modbus message */ public static ModbusMessage fromAscii(int expectedAddress, byte[] bytes){ if (bytes.length < 6) { @@ -100,21 +105,20 @@ public static ModbusMessage fromAscii(int expectedAddress, byte[] bytes){ } return ModbusMessages.createMessage(functionCode, data); } - private static int fromAscii(byte high, byte low){ - int r = 0; - if(high >= 'A'){ -// r += (high - 65 + 10) << 4; - r += ((high & 0xFF) - 55) << 4; - } else { - r += ((high & 0xFF) - 0x30) << 4; + private static int parseDigit(byte asciiValue) { + if (asciiValue > 'F') { + throw new ModbusRuntimeException("Ascii value: " + asciiValue + " is not valid!"); } - if(low >= 'A'){ - r += (low & 0xFF) - 55; - } else { - r += (low & 0xFF) - 0x30; + if (asciiValue >= 'A') { + return asciiValue - 55; } - - return r; + if (asciiValue < 0x30 || asciiValue > 0x39) { + throw new ModbusRuntimeException("Ascii value: " + asciiValue + " is not valid!"); + } + return asciiValue - 0x30; + } + private static int fromAscii(byte high, byte low){ + return (parseDigit(high) << 4) + parseDigit(low); } @Override diff --git a/core/src/main/java/me/retrodaredevil/io/modbus/FunctionCode.java b/core/src/main/java/me/retrodaredevil/io/modbus/FunctionCode.java index a57c208..98c2f7b 100644 --- a/core/src/main/java/me/retrodaredevil/io/modbus/FunctionCode.java +++ b/core/src/main/java/me/retrodaredevil/io/modbus/FunctionCode.java @@ -2,12 +2,21 @@ public final class FunctionCode { private FunctionCode(){ throw new UnsupportedOperationException(); } - + public static final int READ_COIL = 1; public static final int READ_DISCRETE_INPUT = 2; - public static final int READ_REGISTERS = 3; + public static final int READ_HOLDING_REGISTERS = 3; + public static final int READ_INPUT_REGISTERS = 4; public static final int WRITE_SINGLE_COIL = 5; public static final int WRITE_SINGLE_REGISTER = 6; + + public static final int READ_EXCEPTION_STATUS = 7; + public static final int WRITE_MULTIPLE_COILS = 15; public static final int WRITE_MULTIPLE_REGISTERS = 16; + + + public static final int PROGRAM_CONTROLLER = 13; + public static final int POLL_CONTROLLER = 14; + public static final int REPORT_SLAVE_ID = 17; } diff --git a/core/src/main/java/me/retrodaredevil/io/modbus/ModbusMessage.java b/core/src/main/java/me/retrodaredevil/io/modbus/ModbusMessage.java index 67d7d92..646c183 100644 --- a/core/src/main/java/me/retrodaredevil/io/modbus/ModbusMessage.java +++ b/core/src/main/java/me/retrodaredevil/io/modbus/ModbusMessage.java @@ -1,15 +1,25 @@ package me.retrodaredevil.io.modbus; +/** + * Represents the function code and data of a Modbus message. + *

+ * This does not contain the checksum or the slave address, or MBAP header data. + */ public interface ModbusMessage { /** * @return The function code */ int getFunctionCode(); byte getByteFunctionCode(); - + /** + * NOTE: Do not modify the returned array. Doing so may produce undefined results * @return An array where each element represents a single byte (8 bit number). */ int[] getData(); + /** + * NOTE: Do not modify the returned array. Doing so may produce undefined results + * @return An array where each element represents a single byte (8 bit number). + */ byte[] getByteData(); } diff --git a/core/src/main/java/me/retrodaredevil/io/modbus/RtuDataEncoder.java b/core/src/main/java/me/retrodaredevil/io/modbus/RtuDataEncoder.java index 7d0d7d1..b967fc9 100644 --- a/core/src/main/java/me/retrodaredevil/io/modbus/RtuDataEncoder.java +++ b/core/src/main/java/me/retrodaredevil/io/modbus/RtuDataEncoder.java @@ -105,9 +105,7 @@ private byte[] readBytes(InputStream inputStream) throws IOException { throw new AssertionError("We check InputStream#available()! len should not be <= 0! It's: " + len); } lastData = System.currentTimeMillis(); - for(int i = 0; i < len; i++){ - bytes.write(buffer[i]); - } + bytes.write(buffer, 0, len); } else { long currentTime = System.currentTimeMillis(); if(lastData == null){ // not started diff --git a/core/src/main/java/me/retrodaredevil/io/modbus/handling/ReadHoldingRegisters.java b/core/src/main/java/me/retrodaredevil/io/modbus/handling/ReadHoldingRegisters.java index c104a98..4df0a6f 100644 --- a/core/src/main/java/me/retrodaredevil/io/modbus/handling/ReadHoldingRegisters.java +++ b/core/src/main/java/me/retrodaredevil/io/modbus/handling/ReadHoldingRegisters.java @@ -32,8 +32,6 @@ public static ReadHoldingRegisters parseFromRequestData(int[] data) throws Messa ); } - @Deprecated - public int getRegister() { return getStartingDataAddress(); } public int getNumberOfRegisters() { return numberOfRegisters; } @@ -54,14 +52,14 @@ public int hashCode() { @Override public ModbusMessage createRequest() { - return ModbusMessages.createMessage(FunctionCode.READ_REGISTERS, get8BitDataFrom16BitArray(getStartingDataAddress(), numberOfRegisters)); + return ModbusMessages.createMessage(FunctionCode.READ_HOLDING_REGISTERS, get8BitDataFrom16BitArray(getStartingDataAddress(), numberOfRegisters)); } @Override public int[] handleResponse(ModbusMessage response) { int functionCode = response.getFunctionCode(); - if(functionCode != FunctionCode.READ_REGISTERS){ - throw new FunctionCodeException(FunctionCode.READ_REGISTERS, functionCode); + if(functionCode != FunctionCode.READ_HOLDING_REGISTERS){ + throw new FunctionCodeException(FunctionCode.READ_HOLDING_REGISTERS, functionCode); } int[] allData = response.getData(); int expectedLength = numberOfRegisters * 2 + 1; @@ -91,6 +89,6 @@ public ModbusMessage createResponse(int[] data16Bit) { int[] allData = new int[data.length + 1]; allData[0] = byteCount; System.arraycopy(data, 0, allData, 1, data.length); - return ModbusMessages.createMessage(FunctionCode.READ_REGISTERS, allData); + return ModbusMessages.createMessage(FunctionCode.READ_HOLDING_REGISTERS, allData); } } diff --git a/core/src/main/java/me/retrodaredevil/io/modbus/parsing/DefaultMessageParser.java b/core/src/main/java/me/retrodaredevil/io/modbus/parsing/DefaultMessageParser.java index fe2e0d0..b54db8b 100644 --- a/core/src/main/java/me/retrodaredevil/io/modbus/parsing/DefaultMessageParser.java +++ b/core/src/main/java/me/retrodaredevil/io/modbus/parsing/DefaultMessageParser.java @@ -12,7 +12,7 @@ public MessageHandler parseRequestMessage(ModbusMessage message) throws Messa return ReadCoils.parseFromRequestData(message.getData()); case FunctionCode.READ_DISCRETE_INPUT: return ReadDiscreteInputs.parseFromRequestData(message.getData()); - case FunctionCode.READ_REGISTERS: + case FunctionCode.READ_HOLDING_REGISTERS: return ReadHoldingRegisters.parseFromRequestData(message.getData()); case FunctionCode.WRITE_SINGLE_COIL: return WriteSingleCoil.parseFromRequestData(message.getData()); diff --git a/jSerialComm/build.gradle b/jSerialComm/build.gradle index 00084ae..e9708e8 100644 --- a/jSerialComm/build.gradle +++ b/jSerialComm/build.gradle @@ -5,5 +5,6 @@ plugins { version "0.0.1-SNAPSHOT" dependencies { - implementation 'com.fazecast:jSerialComm:2.6.2' + api project(":core") + api 'com.fazecast:jSerialComm:2.6.2' } diff --git a/purejavacomm/build.gradle b/purejavacomm/build.gradle new file mode 100644 index 0000000..1afe887 --- /dev/null +++ b/purejavacomm/build.gradle @@ -0,0 +1,10 @@ +plugins { + id 'java' +} + +version "0.0.1-SNAPSHOT" + +dependencies { + api project(":core") + api "com.github.purejavacomm:purejavacomm:1.0.2.RELEASE" +} diff --git a/purejavacomm/src/main/java/me/retrodaredevil/io/serial/PureJavaCommIOBundle.java b/purejavacomm/src/main/java/me/retrodaredevil/io/serial/PureJavaCommIOBundle.java new file mode 100644 index 0000000..f497482 --- /dev/null +++ b/purejavacomm/src/main/java/me/retrodaredevil/io/serial/PureJavaCommIOBundle.java @@ -0,0 +1,90 @@ +package me.retrodaredevil.io.serial; + +import me.retrodaredevil.io.IOBundle; +import purejavacomm.*; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class PureJavaCommIOBundle implements IOBundle { + private final SerialPort serialPort; + private final InputStream inputStream; + private final OutputStream outputStream; + + public PureJavaCommIOBundle(SerialPort serialPort) throws IOException { + this.serialPort = serialPort; + inputStream = serialPort.getInputStream(); + outputStream = serialPort.getOutputStream(); + } + public static PureJavaCommIOBundle create(String port, SerialConfig serialConfig) throws SerialPortException { + final CommPortIdentifier portIdentifier; + try { + portIdentifier = CommPortIdentifier.getPortIdentifier(port); + } catch (NoSuchPortException e) { + throw new SerialPortException(e); + } + final CommPort commPort; + try { + commPort = portIdentifier.open("io-lib", 0); + } catch (PortInUseException e) { + throw new SerialPortException(e); + } + final SerialPort serialPort = (SerialPort) commPort; // right now all CommPorts should be SerialPorts (maybe this could be changed in a future version) + + try { + serialPort.setSerialPortParams(serialConfig.getBaudRateValue(), serialConfig.getDataBitsValue(), convertStopBits(serialConfig.getStopBits()), convertParity(serialConfig.getParity())); + serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE); + } catch (UnsupportedCommOperationException e) { + throw new SerialPortException(e); + } + try { + if (serialConfig.isRTS()) { + serialPort.setRTS(true); + } + if (serialConfig.isDTR()) { + serialPort.setDTR(true); + } + } catch (PureJavaIllegalStateException e) { + throw new SerialPortException("It's likely that RTS or DTR is not supported", e); + } + try { + return new PureJavaCommIOBundle(serialPort); + } catch (IOException e) { + throw new SerialPortException(e); + } + } + private static int convertStopBits(SerialConfig.StopBits stopBits) { + switch (stopBits) { + case ONE: return SerialPort.STOPBITS_1; + case TWO: return SerialPort.STOPBITS_2; + case ONE_POINT_FIVE: return SerialPort.STOPBITS_1_5; + } + throw new IllegalArgumentException("Unsupported stop bits: " + stopBits); + } + private static int convertParity(SerialConfig.Parity parity) { + switch (parity) { + case NONE: return SerialPort.PARITY_NONE; + case ODD: return SerialPort.PARITY_ODD; + case EVEN: return SerialPort.PARITY_EVEN; + case MARK: return SerialPort.PARITY_MARK; + case SPACE: return SerialPort.PARITY_SPACE; + } + throw new IllegalArgumentException("Unsupported parity: " + parity); + } + + @Override + public InputStream getInputStream() { + return inputStream; + } + + @Override + public OutputStream getOutputStream() { + return outputStream; + } + + @Override + public void close() { + serialPort.close(); + } +} diff --git a/purejavacomm/src/test/java/me/retrodaredevil/io/serial/PureJavaSerialPortExample.java b/purejavacomm/src/test/java/me/retrodaredevil/io/serial/PureJavaSerialPortExample.java new file mode 100644 index 0000000..0bdf02b --- /dev/null +++ b/purejavacomm/src/test/java/me/retrodaredevil/io/serial/PureJavaSerialPortExample.java @@ -0,0 +1,9 @@ +package me.retrodaredevil.io.serial; + +public class PureJavaSerialPortExample { + public static void main(String[] args) throws SerialPortException { + try (PureJavaCommIOBundle ioBundle = PureJavaCommIOBundle.create("/dev/ttyS10", new SerialConfigBuilder(9600).build())) { + + } + } +} diff --git a/settings.gradle b/settings.gradle index e97252e..6dee9ed 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,4 @@ rootProject.name = 'io-lib' include 'core' include 'jSerialComm' +include 'purejavacomm'