Melsec
A java library which implements MELSEC Communication protocol (QnA compatible 3E Binary).
Install / Use
/learn @megahoneybadger/MelsecREADME
General
A java library which implements MELSEC Communication protocol (QnA compatible 3E Binary).
Releases are available via Maven central. To add a dependency to melsec-client, use:
<dependency>
<groupId>io.github.megahoneybadger</groupId>
<artifactId>melsec-client</artifactId>
<version>0.1</version>
</dependency>
Content
Project consists of the following modules:
- client - the main library you need to communicate with PLC device
- eqp - equipment simulator that can be used for tests
- monitor - console application which allows to connect to a device and test read/write commands
- tests - contains unit and integration tests
How to build
Firstly get the sources:
git clone https://github.com/megahoneybadger/melsec.git
cd melsec
Secondly compile the project (you will need maven):
mvn compile
Main concepts
Bindings
To communicate with a remote device you will use so-called bindings: typed values that reflect and interpret memory blocks. In other words we are not working with a raw memory (words or bits). Available bindings are:
- PlcU2 - unsigned short
- PlcI2 - signed short
- PlcU4 - unsigned integer
- PlcI4 - signed integer
- PlcString - ASCII string
- PlcStruct - structure
- PlcBit - boolean
Every binding contains target device code and address. In addition, it may have optional value and id.
var a = new PlcU2( WordDeviceCode.D, 100 );
var b = new PlcI4( WordDeviceCode.W, 0x200, 200 );
// You can specify optional id to differentiate objects
var c = new PlcBit( BitDeviceCode.B, 0, true, "GLASS_EVENT_BIT" );
// Bits and words use different device code families
var d = new PlcBit( BitDeviceCode.M, 0 );
// String must specify a size in BYTES
var e = new PlcString( WordDeviceCode.W, 300, 10, "This is a long string", "GLASS_DESCRIPTION" );
var f = PlcStruct
.builder( WordDeviceCode.W, 0x100, "Glass" )
.u2( 101 )
.u2( 27894 )
.u2( 31254 )
// we may have offsets in WORDS
.offset( 3 )
.i2( ( short )-1456 )
.offset( 1 )
.i2( ( short )5567 )
.string( 4, "helloworld" )
.build();
Equipment client
Primary object responsible for communication is an Equipment Client. To make it work you have to create a proper configuration which sets remote device's IP and port.
If you do not have a physical PLC device you can use simulator.
var config = ClientOptions
.builder()
.address( "127.0.0.1" )
.port( 8000 )
.loggers(
new ConsoleLogger( LogLevel.DEBUG ))
.build();
var client = new EquipmentClient( config );
// This line initiates connection
client.start();
IO Requests
To read and write bindings you will need to pack them into the requests. Every request may contain a chain of IO operations. Then complete you will receive a response with detailed result information for every binding.
Example #1
var config = ClientOptions
.builder()
.address( "127.0.0.1" )
.port( 8000 )
.loggers(
new ConsoleLogger( LogLevel.DEBUG ))
.build();
var client = new EquipmentClient( config );
client.start();
var a = new PlcU2( WordDeviceCode.D, 100 );
var b = new PlcU2( WordDeviceCode.D, 200 );
var request = IORequest
.builder()
.read( a, b, new PlcBit( BitDeviceCode.M, 0 ) )
.complete( x -> x.items().forEach( y -> System.out.println( y ) ) )
.build();
// let's establish a connection
Thread.sleep( 500 );
client.exec( request );
new Scanner( System.in ).nextLine();
Output #1
[melsec][INFO ][10:45:51.051] Client started
[melsec][DEBUG][10:45:51.051] Connection#234 trying to connect to 127.0.0.1:8000
[melsec][INFO ][10:45:51.051] Connection#234 established
[melsec][DEBUG][10:45:52.052] Enqueue mbbr#762 [2w|1b]
[melsec][DEBUG][10:45:52.052] Process mbbr#762 [2w|1b]
[melsec][DEBUG][10:45:52.052] Complete mbbr#762 [2w|1b]
Read [OK] U2 [D100] 125
Read [OK] U2 [D200] 231
Read [OK] bit [M0] 1
Example #2
// omit client creation for brevity...
var request = IORequest
.builder()
.read( new PlcU2( WordDeviceCode.D, -500/*bad address*/ ) )
.complete( x -> x.items().forEach( y -> System.out.println( y ) ) )
.build();
// let's establish a connection
Thread.sleep( 500 );
client.exec( request );
Output #2
[melsec][INFO ][10:53:06.006] Client started
[melsec][DEBUG][10:53:06.006] Connection#816 trying to connect to 127.0.0.1:8000
[melsec][INFO ][10:53:06.006] Connection#816 established
[melsec][DEBUG][10:53:06.006] Enqueue mbbr#50b [1w]
[melsec][DEBUG][10:53:06.006] Process mbbr#50b [1w]
[melsec][ERROR][10:53:06.006] Failed to complete mbbr#50b [1w]. Failed to encode mbbr#50b [1w]. Invalid device address
Read [NG] U2 [D-500] -> Failed to encode mbbr#50b [1w]. Invalid device address
Example #3
// omit client creation for brevity...
var w1 = new PlcString( WordDeviceCode.D, 0, 10, "Hello word" );
var b1 = new PlcBit( BitDeviceCode.B, 500, true );
var w2 = new PlcU2( WordDeviceCode.D, 100, "Pressure" );
var b2 = new PlcBit( BitDeviceCode.B, 501, false, "Reply Bit" );
var request = IORequest
.builder()
// we want the client to stick to
// the order: write -> read -> write
.write( w1, b1 )
.read( w2 )
.write( b2 )
.complete( x -> x.items().forEach( y -> System.out.println( y ) ) )
.build();
// let's establish a connection
Thread.sleep( 500 );
client.exec( request );
Output #3
[melsec][INFO ][10:56:05.005] Client started
[melsec][DEBUG][10:56:05.005] Connection#635 trying to connect to 127.0.0.1:8000
[melsec][INFO ][10:56:05.005] Connection#635 established
[melsec][DEBUG][10:56:06.006] Enqueue mbbw#2d0 [1w], rw#261 [1], mbbr#9d7 [1w], rw#b53 [1]
[melsec][DEBUG][10:56:06.006] Process mbbw#2d0 [1w]
[melsec][DEBUG][10:56:06.006] Complete mbbw#2d0 [1w]
[melsec][DEBUG][10:56:06.006] Process rw#261 [1]
[melsec][DEBUG][10:56:06.006] Complete rw#261 [1]
[melsec][DEBUG][10:56:06.006] Process mbbr#9d7 [1w]
[melsec][DEBUG][10:56:06.006] Complete mbbr#9d7 [1w]
[melsec][DEBUG][10:56:06.006] Process rw#b53 [1]
[melsec][DEBUG][10:56:06.006] Complete rw#b53 [1]
Write [OK] A10 [D0] Hello word
Write [OK] bit [Bx01F4] 1
Read [OK] U2 [D100 Pressure] 2500
Write [OK] bit [Bx01F5 Reply Bit] 0
Example #4
var w1 = new PlcString( WordDeviceCode.D, 0, 2000/*to many points*/, "Hello word" );
var w2 = new PlcU2( WordDeviceCode.D, 100, "Pressure" );
var request = IORequest
.builder()
.write( w1 )
.read( w2 )
.complete( x -> x.items().forEach( y -> System.out.println( y ) ) )
.build();
Thread.sleep( 500 );
client.exec( request );
Output #4
[melsec][INFO ][10:58:59.059] Client started
[melsec][DEBUG][10:58:59.059] Connection#816 trying to connect to 127.0.0.1:8000
[melsec][INFO ][10:58:59.059] Connection#816 established
[melsec][DEBUG][10:58:59.059] Enqueue mbbw#5ca [1w], mbbr#9d9 [1w]
[melsec][DEBUG][10:58:59.059] Process mbbw#5ca [1w]
[melsec][ERROR][10:58:59.059] Failed to complete mbbw#5ca [1w]. Failed to encode mbbw#5ca [1w]. Too many points
[melsec][DEBUG][10:58:59.059] Process mbbr#9d9 [1w]
[melsec][DEBUG][10:58:59.059] Complete mbbr#9d9 [1w]
Write [NG] A2000 [D0] Hello word -> Failed to encode mbbw#5ca [1w]. Too many points
Read [OK] U2 [D100 Pressure]
Example #5
var st = PlcStruct
.builder( WordDeviceCode.W, 0x100, "Employee" )
.u2( "Age" )
.u2( "Weight" )
.u2( "Salary" )
// pay attention to the offset between fields
.offset( 3 )
.string( 20, "Name" )
.build();
var request = IORequest
.builder()
.read( st )
.complete( x -> x.items().forEach( y -> System.out.println( y ) ) )
.build();
Thread.sleep( 500 );
client.exec( request );
Output #5
[melsec][INFO ][11:00:37.037] Client started
[melsec][DEBUG][11:00:38.038] Connection#402 trying to connect to 127.0.0.1:8000
[melsec][INFO ][11:00:38.038] Connection#402 established
[melsec][DEBUG][11:00:38.038] Enqueue mbbr#69b [1w]
[melsec][DEBUG][11:00:38.038] Process mbbr#69b [1w]
[melsec][DEBUG][11:00:38.038] Complete mbbr#69b [1w]
Read [OK] struct [Wx0100 Employee]
U2 [Wx0100 Age] 25
U2 [Wx0101 Weight] 75
U2 [Wx0102 Salary] 1000
A20 [Wx0106] John Smith

Events
You may subscribe to a number of events.
var config = ClientOptions
.builder()
.address( "127.0.0.1" )
.port( 8000 )
.loggers(
new ConsoleLogger( LogLevel.DEBUG ))
.build();
var client = new EquipmentClient( config );
client.events().subscribe( ( IClientStartedEvent ) x -> System.out.println( "client started" ) );
client.events().subscribe( ( IClientStoppedEvent ) x -> System.out.println( "client stopped" ) );
client.events().subscribe( ( IConnectionConnectingEvent ) x -> System.out.println( "connecting" ) );
client.events().subscribe( ( IConnectionEsta
