Skip to content

Commit

Permalink
Added purejavacomm library, uses api
Browse files Browse the repository at this point in the history
  • Loading branch information
retrodaredevil committed May 20, 2020
1 parent 8cbb971 commit 7dea636
Show file tree
Hide file tree
Showing 13 changed files with 202 additions and 44 deletions.
53 changes: 44 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
7 changes: 0 additions & 7 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,6 @@ subprojects {
}
}

project(":jSerialComm") {
apply plugin: 'java'
dependencies {
api project(":core")
}
}


wrapper {
gradleVersion = '6.4.1'
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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() {
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down
13 changes: 11 additions & 2 deletions core/src/main/java/me/retrodaredevil/io/modbus/FunctionCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
package me.retrodaredevil.io.modbus;

/**
* Represents the function code and data of a Modbus message.
* <p>
* 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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ public static ReadHoldingRegisters parseFromRequestData(int[] data) throws Messa
);
}

@Deprecated
public int getRegister() { return getStartingDataAddress(); }
public int getNumberOfRegisters() {
return numberOfRegisters;
}
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
3 changes: 2 additions & 1 deletion jSerialComm/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
10 changes: 10 additions & 0 deletions purejavacomm/build.gradle
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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())) {

}
}
}
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
rootProject.name = 'io-lib'
include 'core'
include 'jSerialComm'
include 'purejavacomm'

0 comments on commit 7dea636

Please sign in to comment.