Initial commit

This commit is contained in:
zhongjin 2019-01-16 11:59:25 +08:00
commit 3a0cf3f14d
72 changed files with 11102 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
/.idea
/node_modules
.git
admin/i18n/flat.txt
admin/i18n/*/flat.txt
test/Ananas64.exe
iob_npm.done
tmp
package-lock.json

12
.npmignore Normal file
View File

@ -0,0 +1,12 @@
gulpfile.js
tasks
node_modules
.idea
.git
/node_modules
test
.travis.yml
appveyor.yml
admin/i18n
iob_npm.done
package-lock.json

24
.travis.yml Normal file
View File

@ -0,0 +1,24 @@
os:
- linux
- osx
language: node_js
node_js:
- '4'
- '6'
- '8'
- '10'
before_install:
- 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then export CC=clang++; export CXX=clang++; export CXXFLAGS=-std=c++11; fi'
- 'if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export CXX=g++-4.8; fi'
before_script:
- export NPMVERSION=$(echo "$($(which npm) -v)"|cut -c1)
- 'if [[ $NPMVERSION == 5 ]]; then npm install -g npm@5; fi'
- npm -v
- npm install winston@2.3.1
- 'npm install https://github.com/yunkong2/yunkong2.js-controller/tarball/master --production'
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015-2018 Bluefox <dogafox@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

304
README.md Normal file
View File

@ -0,0 +1,304 @@
![Logo](admin/modbus.png)
# yunkong2.rsonoff
=====================
[![NPM version](http://img.shields.io/npm/v/yunkong2.rsonoff.svg)](https://www.npmjs.com/package/yunkong2.rsonoff)
[![Downloads](https://img.shields.io/npm/dm/yunkong2.rsonoff.svg)](https://www.npmjs.com/package/yunkong2.rsonoff)
[![NPM](https://nodei.co/npm/yunkong2.rsonoff.png?downloads=true)](https://nodei.co/npm/yunkong2.rsonoff/)
Implementation of ModBus Slave and Master for yunkong2. Following types are supported:
- Modbus RTU over serial (master)
- Modbus RTU over TCP (master)
- Modbus TCP (slave, master)
## Settings
### Partner IP Address
IP address of modbus partner.
### Port
TCP Port of modbus partner if configured as master (client) or own port if configured as slave(server).
### Device ID
Modbus Device ID. Important if TCP/Modbus bridge is used.
### Type
Slave(Server) or Master(Client).
### Use aliases as address
Normally all registers can have address from 0 to 65535. By using of aliases you can define virtual address fields for every type of registers. Normally:
- discrete inputs are from 10001 to 20000
- coils are from 1 to 1000
- input registers are from 30001 to 40000
- holding registers are from 40001 to 60000
Every alias will be mapped internally to address, e.g. 30011 will be mapped to input register 10. and so on.
### Do not align addresses to word
Normally the coils and the discrete inputs addresses are aligned to 16 bit. Like addresses from 3 to 20 will be aligned to 0 up 32.
If this option is active the addresses will not be aligned.
### Round Real to
How many digits after comma for float and doubles.
### Poll delay
Cyclic poll interval (Only relevant for master)
### Reconnect time
Reconnection interval (Only relevant for master)
### Pulse time
if pulse used for coils, this define the interval how long is pulse.
### Max read request length
Maximal length of command READ_MULTIPLE_REGISTERS as number of registers to read.
Some systems require first "write request" to deliver the data on "read request".
You can force this mode by setting of the "Max read request length" to 1.
**Notice:**
Some USB Modbus solutions (e.g. based on socat) can have trouble to work with serialport npm module.
There is a software [**Modbus RTU <-> Modbus RTU over TCP**](http://mbus.sourceforge.net/index.html) gateway to enable using of serial RTU over TCP protocol.
Both solutions **RTU over TCP** and **TCP** works well.
## Data types
- uint16be - Unsigned 16 bit (Big Endian): AABB => AABB
- uint16le - Unsigned 16 bit (Little Endian): AABB => BBAA
- int16be - Signed 16 bit (Big Endian): AABB => AABB
- int16le - Signed 16 bit (Little Endian): AABB => BBAA
- uint32be - Unsigned 32 bit (Big Endian): AABBCCDD => AABBCCDD
- uint32le - Unsigned 32 bit (Little Endian): AABBCCDD => DDCCBBAA
- uint32sw - Unsigned 32 bit (Big Endian Word Swap): AABBCCDD => CCDDAABB
- uint32sb - Unsigned 32 bit (Big Endian Byte Swap): AABBCCDD => DDCCBBAA
- int32be - Signed 32 bit (Big Endian): AABBCCDD => AABBCCDD
- int32le - Signed 32 bit (Little Endian): ABBCCDD => DDCCBBAA
- int32sw - Signed 32 bit (Big Endian Word Swap): AABBCCDD => CCDDAABB
- int32sb - Signed 32 bit (Big Endian Byte Swap): AABBCCDD => DDCCBBAA
- uint64be - Unsigned 64 bit (Big Endian): AABBCCDDEEFFGGHH => AABBCCDDEEFFGGHH
- uint64le - Unsigned 64 bit (Little Endian): AABBCCDDEEFFGGHH => HHGGFFEEDDCCBBAA
- uint8be - Unsigned 8 bit (Big Endian): AA => AA
- uint8le - Unsigned 8 bit (Little Endian): AA => AA
- int8be - Signed 8 bit (Big Endian): AA => AA
- int8le - Signed 8 bit (Little Endian): AA => AA
- floatbe - Float (Big Endian): AABBCCDD => AABBCCDD
- floatle - Float (Little Endian): AABBCCDD => DDCCBBAA
- floatsw - Float (Big Endian Word Swap): AABBCCDD => CCDDAABB
- floatsb - Float (Big Endian Byte Swap): AABBCCDD => DDCCBBAA
- doublebe - Double (Big Endian): AABBCCDDEEFFGGHH => AABBCCDDEEFFGGHH
- doublele - Double (Little Endian): AABBCCDDEEFFGGHH => HHGGFFEEDDCCBBAA
- string - String (Zero-end): ABCDEF\0 => ABCDEF\0
- stringle - String (Little Endian, Zero-end): BADCFE\0 => ABCDEF\0
Following description was copied from [here](http://www.chipkin.com/how-real-floating-point-and-32-bit-data-is-encoded-in-modbus-rtu-messages/)
The point-to-point Modbus protocol is a popular choice for RTU communications if for no other reason that its basic convenience. The protocol itself controls the interactions of each device on a Modbus network, how device establishes a known address, how each device recognizes its messages and how basic information is extracted from the data. In essence, the protocol is the foundation of the entire Modbus network.
Such convenience does not come without some complications however, and Modbus RTU Message protocol is no exception. The protocol itself was designed based on devices with a 16-bit register length. Consequently, special considerations were required when implementing 32-bit data elements. This implementation settled on using two consecutive 16-bit registers to represent 32 bits of data or essentially 4 bytes of data. It is within these 4 bytes of data that single-precision floating point data can be encoded into a Modbus RTU message.
### The Importance of Byte Order
Modbus itself does not define a floating point data type but it is widely accepted that it implements 32-bit floating point data using the IEEE-754 standard. However, the IEEE standard has no clear cut definition of byte order of the data payload. Therefore the most important consideration when dealing with 32-bit data is that data is addressed in the proper order.
For example, the number 123/456.00 as defined in the IEEE 754 standard for single-precision 32-bit floating point numbers appears as follows:
![Image1](img/img1.png)
The affects of various byte orderings are significant. For example, ordering the 4 bytes of data that represent 123456.00 in a “B A D C” sequence in known as a “byte swap”. When interpreted as an IEEE 744 floating point data type, the result is quite different:
![Image2](img/img2.png)
Ordering the same bytes in a “C D A B” sequence is known as a “word swap”. Again, the results differ drastically from the original value of 123456.00:
![Image3](img/img3.png)
Furthermore, both a “byte swap” and a “word swap” would essentially reverse the sequence of the bytes altogether to produce yet another result:
![Image4](img/img4.png)
Clearly, when using network protocols such as Modbus, strict attention must be paid to how bytes of memory are ordered when they are transmitted, also known as the byte order.
### Determining Byte Order
The Modbus protocol itself is declared as a big-Endian protocol, as per the Modbus Application Protocol Specification, V1.1.b:
```Modbus uses a “big-Endian” representation for addresses and data items. This means that when a numerical quantity larger than a single byte is transmitted, the most significant byte is sent first.```
Big-Endian is the most commonly used format for network protocols so common, in fact, that it is also referred to as network order.
Given that the Modbus RTU message protocol is big-Endian, in order to successfully exchange a 32-bit datatype via a Modbus RTU message, the endianness of both the master and the slave must considered. Many RTU master and slave devices allow specific selection of byte order particularly in the case of software-simulated units. One must merely insure that both all units are set to the same byte order.
As a rule of thumb, the family of a devices microprocessor determines its endianness. Typically, the big-Endian style (the high-order byte is stored first, followed by the low-order byte) is generally found in CPUs designed with a Motorola processor. The little-Endian style (the low-order byte is stored first, followed by the high-order byte) is generally found in CPUs using the Intel architecture. It is a matter of personal perspective as to which style is considered backwards.
If, however, byte order and endianness is not a configurable option, you will have to determine the how to interpret the byte. This can be done requesting a known floating-point value from the slave. If an impossible value is returned, i.e. a number with a double-digit exponent or such, the byte ordering will most likely need modification.
### Practical Help
The FieldServer Modbus RTU drivers offer several function moves that handle 32-bit integers and 32-bit float values. More importantly, these function moves consider all different forms of byte sequencing. The following table shows the FieldServer function moves that copy two adjacent 16-bit registers to a 32-bit integer value.
| Function Keyword | Swap Mode | Source Bytes | Target Bytes |
|-------------------|--------------------|-----------------|--------------|
| 2.i16-1.i32 | N/A | [ a b ] [ c d ] | [ a b c d ] |
| 2.i16-1.i32-s | byte and word swap | [ a b ] [ c d ] | [ d c b a ] |
| 2.i16-1.i32-sb | byte swap | [ a b ] [ c d ] | [ b a d c ] |
| 2.i16-1.i32-sw | word swap | [ a b ] [ c d ] | [ c d a b ] |
The following table shows the FieldServer function moves that copy two adjacent 16-bit registers to a 32-bit floating point value:
| Function Keyword | Swap Mode | Source Bytes | Target Bytes |
|-------------------|--------------------|-----------------|--------------|
| 2.i16-1.ifloat | N/A | [ a b ] [ c d ] | [ a b c d ] |
| 2.i16-1.ifloat-s | byte and word swap | [ a b ] [ c d ] | [ d c b a ] |
| 2.i16-1.ifloat-sb | byte swap | [ a b ] [ c d ] | [ b a d c ] |
| 2.i16-1.ifloat-sw | word swap | [ a b ] [ c d ] | [ c d a b ] |
The following table shows the FieldServer function moves that copy a single 32-bit floating point value to two adjacent 16-bit registers:
| Function Keyword | Swap Mode | Source Bytes | Target Bytes |
|------------------|-------------------|-----------------|----------------|
| 1.float-2.i16 |N/A | [ a b ] [ c d ] | [ a b ][ c d ] |
| 1.float-2.i16-s |byte and word swap | [ a b ] [ c d ] | [ d c ][ b a ] |
| 1.float-2.i16-sb |byte swap | [ a b ] [ c d ] | [ b a ][ d c ] |
| 1.float-2.i16-sw |word swap | [ a b ] [ c d ] | [ c d ][ a b ] |
Given the various FieldServer function moves, the correct handling of 32-bit data is dependent on choosing the proper one. Observe the following behavior of these FieldServer function moves on the known single-precision decimal float value of 123456.00:
|16-bit Values | Function Move | Result | Function Move | Result |
|---------------|-------------------|-----------|-------------------|---------------|
|0x2000 0x47F1 | 2.i16-1.float | 123456.00 | 1.float-2.i16 | 0x2000 0x47F1 |
|0xF147 0x0020 | 2.i16-1.float-s | 123456.00 | 1.float-2.i16-s | 0xF147 0X0020 |
|0x0020 0xF147 | 2.i16-1.float-sb | 123456.00 | 1.float-2.i16-sb | 0x0020 0xF147 |
|0x47F1 0x2000 | 2.i16-1.float-sw | 123456.00 | 1.float-2.i16-sw | 0x47F1 0x2000 |
Notice that different byte and word orderings require the use of the appropriate FieldServer function move. Once the proper function move is selected, the data can be converted in both directions.
Of the many hex-to-floating point converters and calculators that are available in the Internet, very few actually allow manipulation of the byte and word orders. One such utility is located at www.61131.com/download.htm where both Linux and Windows versions of the utilities can be downloaded. Once installed, the utility is run as an executable with a single dialog interface. The utility presents the decimal float value of 123456.00 as follows:
![Image5](img/img5.png)
One can then swap bytes and/or words to analyze what potential endianness issues may exist between Modbus RTU master and slave devices.
## Test
There are some programs in folder *test' to test the TCP communication:
- Ananas32/64 is slave simulator (only holding registers and inputs, no coils and digital inputs)
- RMMS is master simulator
- mod_RSsim.exe is slave simulator. It can be that you need [Microsoft Visual C++ 2008 SP1 Redistributable Package](https://www.microsoft.com/en-us/download/details.aspx?id=5582) to start it (because of SideBySide error).
## Changelog
# 2.0.9 (2018-10-11)
* (Bjoern3003) Write registers was corrected
# 2.0.7 (2018-07-02)
* (bluefox) The server mode was fixed
# 2.0.6 (2018-06-26)
* (bluefox) rtu-tcp master mode was fixed
# 2.0.3 (2018-06-16)
* (bluefox) Fixed the rounding of numbers
# 2.0.2 (2018-06-12)
* (bluefox) The error with blocks reading was fixed
* (bluefox) The block reading for discrete values was implemented
# 2.0.1 (2018-05-06)
* (bluefox) Added the support of multiple device IDs
# 1.1.1 (2018-04-15)
* (Apollon77) Optimize reconnect handling
# 1.1.0 (2018-01-23)
* (bluefox) Little endian strings added
* (Apollon77) Upgrade Serialport Library
# 1.0.2 (2018-01-20)
* (bluefox) Fixed read of coils
# 0.5.4 (2017-09-27)
* (Apollon77) Several Fixes
# 0.5.0 (2017-02-11)
* (bluefox) Create all states each after other
# 0.4.10 (2017-02-10)
* (Apollon77) Do not recreate all datapoints on start of adapter
* (ykuendig) Multiple optimization and wording fixes
# 0.4.9 (2016-12-20)
* (bluefox) fix serial RTU
# 0.4.8 (2016-12-15)
* (Apollon77) update serialport library for node 6.x compatibility
# 0.4.7 (2016-11-27)
* (bluefox) Use old version of jsmodbus
# 0.4.6 (2016-11-08)
* (bluefox) backward compatibility with 0.3.x
# 0.4.5 (2016-10-25)
* (bluefox) better buffer handling on tcp and serial
# 0.4.4 (2016-10-21)
* (bluefox) Fix write of holding registers
# 0.4.1 (2016-10-19)
* (bluefox) Support of ModBus RTU over serial and over TCP (only slave)
# 0.3.11 (2016-08-18)
* (Apollon77) Fix wrong byte count in loop
# 0.3.10 (2016-02-01)
* (bluefox) fix lost of history settings.
# 0.3.9 (2015-11-09)
* (bluefox) Use always write_multiple_registers by write of holding registers.
# 0.3.7 (2015-11-02)
* (bluefox) add special read/write mode if "Max read request length" is 1.
# 0.3.6 (2015-11-01)
* (bluefox) add cyclic write for holding registers (fix)
# 0.3.5 (2015-10-31)
* (bluefox) add cyclic write for holding registers
# 0.3.4 (2015-10-28)
* (bluefox) add doubles and fix uint64
# 0.3.3 (2015-10-27)
* (bluefox) fix holding registers
# 0.3.2 (2015-10-27)
* (bluefox) fix import from text file
# 0.3.1 (2015-10-26)
* (bluefox) fix error with length of read block (master)
* (bluefox) support of read blocks and maximal length of read request (master)
* (bluefox) can define fields by import
# 0.3.0 (2015-10-24)
* (bluefox) add round settings
* (bluefox) add deviceID
* (bluefox) slave supports floats, integers and strings
# 0.2.6 (2015-10-22)
* (bluefox) add different types for inputRegisters and for holding registers ONLY FOR MASTER
# 0.2.5 (2015-10-20)
* (bluefox) fix names of objects if aliases used
# 0.2.4 (2015-10-19)
* (bluefox) fix error add new values
# 0.2.3 (2015-10-15)
* (bluefox) fix error with master
# 0.2.2 (2015-10-14)
* (bluefox) implement slave
* (bluefox) change addressing model
# 0.0.1
* (bluefox) initial commit

View File

@ -0,0 +1,74 @@
{
"General": "Allgemein",
"Inputs": "Diskrete Eingänge",
"Do not align addresses to word:": "Die Adressen nicht auf 16 Bits ausrichten:",
"Coils": "Diskrete Ausgänge",
"Input Registers": "Eingangsregister",
"Holding Registers": "Eingangsregister",
"PLC Connection:": "SPS Verbindung:",
"PLC IP Address:": "SPS IP Adresse:",
"PLC Rack:": "SPS Rack:",
"PLC Slot:": "SPS Slot:",
"Round Real to:": "Aufrunden Real auf:",
"Poll delay:": "Poll delay:",
"deviceId": "Slave-ID",
"Reconnect time:": "Reconnect-Zeit:",
"Pulse time:": "Pulsetime:",
"Import symbols file:": "Symboldatei Importieren:",
"Import DB file:": "DB-Datei importieren:",
"Load Symbols": "Lade Symbole",
"Add DB": "DB einfügen",
"Toggle poll": "Poll umschalten",
"Toggle RW": "RW umschalten",
"Toggle WP": "WP umschalten",
"Address": "Adresse",
"Name": "Name",
"Description": "Beschreibung",
"Type": "Typ",
"Unit": "Einheit",
"poll": "poll",
"RW": "RW",
"CW": "CW",
"WP": "WP",
"Role": "Rolle",
"Room": "Raum",
"registers": "Register",
"Device ID:": "Geräte ID:",
"RTU over TCP": "RTU over TCP",
"Data bits:": "Data bits:",
"Stop bits:": "Stop bits:",
"Parity:": "Parity:",
"Read timeout:": "Read timeout:",
"Multi device IDs:": "Mehrere Geräte-IDs:",
"Use aliases as address:": "Aliases benutzen:",
"Max read request length:": "Maximale Lese-Request-Länge:",
"Enable polling of data point": "Zyklische Abfrage vom Datenpunkt",
"Write access allowed": "Schreiben erlaubt",
"Write pulses (true=>false edge)": "Schreibe Pulse (Ja=>Nein Flanke)",
"Connection parameters:": "Verbindungsparameter:",
"Partner IP Address:": "Partner IP Adresse:",
"Port:": "Port:",
"Type:": "Typ:",
"Master": "Master",
"Slave": "Slave",
"Are you sure?": "Sind Sie sicher?",
"Start address:": "Start-Adresse:",
"Text copied to clipboard. Click to close the window": "Text wurde in die Zwischenablage kopiert. Klicke um das Fenster zu schliessen.",
"Export": "Export",
"Import": "Import",
"Close": "Zumachen",
"Export to CSV": "Export in CSV",
"Import from CSV": "Import aus CSV",
"Delete all entries": "Alle Einträge löschen",
"Length": "Länge",
"All entries will be deleted. Are you sure?": "Alle Einträge werden gelöscht. Sind sie sicher?",
"Factor": "Faktor",
"Offset": "Offset",
"Cyclic write": "Zyklisch schreiben",
"TCP/Serial RTU:": "TCP/Serial RTU:",
"TCP": "TCP",
"Serial": "Serial",
"Baud rate:": "Baud rate:",
"Use direct addresses by aliases:": "Direkte Adressen benutzen (bei Aliases):",
"Select port": "Port wählen"
}

View File

@ -0,0 +1,74 @@
{
"General": "General",
"Inputs": "Discrete Inputs",
"Do not align addresses to word:": "Do not align addresses to 16 bits:",
"Coils": "Coils",
"Input Registers": "Input Registers",
"Holding Registers": "Holding Registers",
"PLC Connection:": "PLC Connection:",
"PLC IP Address:": "PLC IP Address:",
"PLC Rack:": "PLC Rack:",
"PLC Slot:": "PLC Slot:",
"Round Real to:": "Round real to:",
"Poll delay:": "Poll delay:",
"deviceId": "Slave ID",
"Reconnect time:": "Reconnect time:",
"Pulse time:": "Pulse time:",
"Import symbols file:": "Import symbols file:",
"Import DB file:": "Import DB file:",
"Load Symbols": "Load symbols",
"Add DB": "Add DB",
"Toggle poll": "Toggle poll",
"Toggle RW": "Toggle RW",
"Toggle WP": "Toggle WP",
"Address": "Address",
"Name": "Name",
"Description": "Description",
"Type": "Type",
"Unit": "Unit",
"poll": "poll",
"RW": "RW",
"CW": "CW",
"WP": "WP",
"Role": "Role",
"Room": "Room",
"registers": "registers",
"Device ID:": "Device ID:",
"RTU over TCP": "RTU over TCP",
"Data bits:": "Data bits:",
"Stop bits:": "Stop bits:",
"Parity:": "Parity:",
"Read timeout:": "Read timeout:",
"Multi device IDs:": "Multi device IDs:",
"Use aliases as address:": "Use aliases:",
"Max read request length:": "Max read request length:",
"Enable polling of data point": "Enable polling of data point",
"Write access allowed": "Write access allowed",
"Write pulses (true=>false edge)": "Write pulses (true=>false edge)",
"Connection parameters:": "Connection parameters:",
"Partner IP Address:": "Partner IP Address:",
"Port:": "Port:",
"Type:": "Type:",
"Master": "Master",
"Slave": "Slave",
"Are you sure?": "Are you sure?",
"Start address:": "Start address:",
"Text copied to clipboard. Click to close the window": "Text copied to clipboard. Click to close the window",
"Export": "Export",
"Import": "Import",
"Close": "Close",
"Export to CSV": "Export to CSV",
"Import from CSV": "Import from CSV",
"Delete all entries": "Delete all entries",
"Length": "Length",
"All entries will be deleted. Are you sure?": "All entries will be deleted. Are you sure?",
"Factor": "Factor",
"Offset": "Offset",
"Cyclic write": "Cyclic write",
"TCP/Serial RTU:": "TCP/Serial RTU:",
"TCP": "TCP",
"Serial": "Serial",
"Baud rate:": "Baud rate:",
"Use direct addresses by aliases:": "Use direct addresses by aliases:",
"Select port": "Select port"
}

View File

@ -0,0 +1,74 @@
{
"General": "General",
"Inputs": "Entradas discretas",
"Do not align addresses to word:": "No alinee las direcciones a 16 bits:",
"Coils": "Bobinas",
"Input Registers": "Registros de entrada",
"Holding Registers": "Registros de mantenimiento",
"PLC Connection:": "Conexión de PLC:",
"PLC IP Address:": "Dirección IP del PLC:",
"PLC Rack:": "Estante del PLC:",
"PLC Slot:": "Ranura de PLC:",
"Round Real to:": "Redondo real para:",
"Poll delay:": "Retraso en la encuesta:",
"deviceId": "Slave ID",
"Reconnect time:": "Tiempo de reconexión:",
"Pulse time:": "Tiempo de pulso:",
"Import symbols file:": "Importar archivo de símbolos:",
"Import DB file:": "Importar archivo DB",
"Load Symbols": "Cargar símbolos",
"Add DB": "Agregar DB",
"Toggle poll": "Alternar encuesta",
"Toggle RW": "Alternar RW",
"Toggle WP": "Alternar WP",
"Address": "Dirección",
"Name": "Nombre",
"Description": "Descripción",
"Type": "Tipo",
"Unit": "Unidad",
"poll": "encuesta",
"RW": "RW",
"CW": "CW",
"WP": "WP",
"Role": "Papel",
"Room": "Habitación",
"registers": "registros",
"Device ID:": "ID del dispositivo:",
"RTU over TCP": "RTU sobre TCP",
"Data bits:": "Bits de datos:",
"Stop bits:": "Bits de parada:",
"Parity:": "Paridad:",
"Read timeout:": "Tiempo de espera de lectura:",
"Multi device IDs:": "ID de dispositivos múltiples:",
"Use aliases as address:": "Usa alias:",
"Max read request length:": "Longitud máxima de solicitud de lectura:",
"Enable polling of data point": "Habilitar el sondeo del punto de datos",
"Write access allowed": "Acceso de escritura permitido",
"Write pulses (true=>false edge)": "Escribir pulsos (verdadero => borde falso)",
"Connection parameters:": "Parámetros de conexión:",
"Partner IP Address:": "Dirección IP del socio:",
"Port:": "Puerto:",
"Type:": "Tipo:",
"Master": "Dominar",
"Slave": "Esclavo",
"Are you sure?": "¿Estás seguro?",
"Start address:": "Dirección de inicio:",
"Text copied to clipboard. Click to close the window": "Texto copiado al portapapeles. Haga clic para cerrar la ventana",
"Export": "Exportar",
"Import": "Importar",
"Close": "Cerca",
"Export to CSV": "Exportar a CSV",
"Import from CSV": "Importar desde CSV",
"Delete all entries": "Eliminar todas las entradas",
"Length": "Longitud",
"All entries will be deleted. Are you sure?": "Todas las entradas serán eliminadas. ¿Estás seguro?",
"Factor": "Factor",
"Offset": "Compensar",
"Cyclic write": "Escritura cíclica",
"TCP/Serial RTU:": "TCP / Serial RTU:",
"TCP": "TCP",
"Serial": "De serie",
"Baud rate:": "Velocidad de baudios:",
"Use direct addresses by aliases:": "Use direcciones directas por alias:",
"Select port": "Seleccionar puerto"
}

View File

@ -0,0 +1,74 @@
{
"General": "Général",
"Inputs": "Entrées discrètes",
"Do not align addresses to word:": "Ne pas aligner les adresses sur 16 bits:",
"Coils": "Bobines",
"Input Registers": "Registres d'entrée",
"Holding Registers": "Tenir des registres",
"PLC Connection:": "Connexion PLC:",
"PLC IP Address:": "Adresse IP de l'API:",
"PLC Rack:": "Rack PLC:",
"PLC Slot:": "Slot PLC:",
"Round Real to:": "Rond réel à:",
"Poll delay:": "Délai d'interrogation:",
"deviceId": "Slave ID",
"Reconnect time:": "Reconnectez le temps:",
"Pulse time:": "Temps d'impulsion:",
"Import symbols file:": "Importer un fichier de symboles:",
"Import DB file:": "Importer un fichier DB:",
"Load Symbols": "Charger des symboles",
"Add DB": "Ajouter une DB",
"Toggle poll": "Basculer le sondage",
"Toggle RW": "Toggle RW",
"Toggle WP": "Toggle WP",
"Address": "Adresse",
"Name": "prénom",
"Description": "La description",
"Type": "Type",
"Unit": "Unité",
"poll": "sondage",
"RW": "RW",
"CW": "CW",
"WP": "WP",
"Role": "Rôle",
"Room": "Chambre",
"registers": "registres",
"Device ID:": "Reference de l'appareil:",
"RTU over TCP": "RTU sur TCP",
"Data bits:": "Bits de données:",
"Stop bits:": "Bits d'arrêt:",
"Parity:": "Parité:",
"Read timeout:": "Lire le délai d'attente:",
"Multi device IDs:": "ID multi-appareils:",
"Use aliases as address:": "Utiliser des alias:",
"Max read request length:": "Max longueur de la requête de lecture:",
"Enable polling of data point": "Activer l'interrogation du point de données",
"Write access allowed": "Accès en écriture autorisé",
"Write pulses (true=>false edge)": "Écrire des impulsions (true => false edge)",
"Connection parameters:": "Paramètres de connexion:",
"Partner IP Address:": "Adresse IP du partenaire:",
"Port:": "Port:",
"Type:": "Type:",
"Master": "Maîtriser",
"Slave": "Esclave",
"Are you sure?": "Êtes-vous sûr?",
"Start address:": "Adresse de départ:",
"Text copied to clipboard. Click to close the window": "Texte copié dans le presse-papier Cliquez pour fermer la fenêtre",
"Export": "Exportation",
"Import": "Importer",
"Close": "Fermer",
"Export to CSV": "Exporter au format CSV",
"Import from CSV": "Importer à partir du fichier CSV",
"Delete all entries": "Supprimer toutes les entrées",
"Length": "Longueur",
"All entries will be deleted. Are you sure?": "Toutes les entrées seront supprimées. Êtes-vous sûr?",
"Factor": "Facteur",
"Offset": "Décalage",
"Cyclic write": "Écriture cyclique",
"TCP/Serial RTU:": "TCP / Sériel RTU:",
"TCP": "TCP",
"Serial": "En série",
"Baud rate:": "Débit en bauds:",
"Use direct addresses by aliases:": "Utilisez des adresses directes par alias:",
"Select port": "Sélectionnez un port"
}

View File

@ -0,0 +1,74 @@
{
"General": "Generale",
"Inputs": "Ingressi discreti",
"Do not align addresses to word:": "Non allineare gli indirizzi a 16 bit:",
"Coils": "bobine",
"Input Registers": "Registri di input",
"Holding Registers": "Holding Registers",
"PLC Connection:": "Connessione PLC:",
"PLC IP Address:": "Indirizzo IP del PLC:",
"PLC Rack:": "Rack PLC:",
"PLC Slot:": "Slot PLC:",
"Round Real to:": "Round reale a:",
"Poll delay:": "Ritardo del sondaggio:",
"deviceId": "ID slave",
"Reconnect time:": "Tempo di riconnessione:",
"Pulse time:": "Tempo di impulso:",
"Import symbols file:": "Importa file simboli:",
"Import DB file:": "Importa file DB:",
"Load Symbols": "Carica simboli",
"Add DB": "Aggiungi DB",
"Toggle poll": "Attiva / disattiva sondaggio",
"Toggle RW": "Attiva / disattiva RW",
"Toggle WP": "Attiva / disattiva WP",
"Address": "Indirizzo",
"Name": "Nome",
"Description": "Descrizione",
"Type": "genere",
"Unit": "Unità",
"poll": "sondaggio",
"RW": "RW",
"CW": "CW",
"WP": "WP",
"Role": "Ruolo",
"Room": "Camera",
"registers": "registri",
"Device ID:": "ID del dispositivo:",
"RTU over TCP": "RTU su TCP",
"Data bits:": "Bit di dati:",
"Stop bits:": "Stop bit:",
"Parity:": "Parità:",
"Read timeout:": "Leggi il timeout:",
"Multi device IDs:": "ID multi dispositivo:",
"Use aliases as address:": "Usa alias:",
"Max read request length:": "Lunghezza massima richiesta di lettura:",
"Enable polling of data point": "Abilita il polling del punto dati",
"Write access allowed": "Accesso in scrittura consentito",
"Write pulses (true=>false edge)": "Scrivi impulsi (true => falso bordo)",
"Connection parameters:": "Parametri di connessione:",
"Partner IP Address:": "Indirizzo IP del partner:",
"Port:": "Porta:",
"Type:": "Genere:",
"Master": "Maestro",
"Slave": "Schiavo",
"Are you sure?": "Sei sicuro?",
"Start address:": "Indirizzo iniziale:",
"Text copied to clipboard. Click to close the window": "Testo copiato negli appunti. Clicca per chiudere la finestra",
"Export": "Esportare",
"Import": "Importare",
"Close": "Vicino",
"Export to CSV": "Esporta in CSV",
"Import from CSV": "Importa da CSV",
"Delete all entries": "Elimina tutte le voci",
"Length": "Lunghezza",
"All entries will be deleted. Are you sure?": "Tutte le voci saranno cancellate. Sei sicuro?",
"Factor": "Fattore",
"Offset": "Compensare",
"Cyclic write": "Scrittura ciclica",
"TCP/Serial RTU:": "RTU TCP / seriale:",
"TCP": "TCP",
"Serial": "Seriale",
"Baud rate:": "Baud rate:",
"Use direct addresses by aliases:": "Usa indirizzi diretti per alias:",
"Select port": "Seleziona porta"
}

View File

@ -0,0 +1,74 @@
{
"General": "Algemeen",
"Inputs": "Discrete ingangen",
"Do not align addresses to word:": "Lijn adressen niet uit met 16 bits:",
"Coils": "coils",
"Input Registers": "Invoegregisters",
"Holding Registers": "Registers houden",
"PLC Connection:": "PLC-verbinding:",
"PLC IP Address:": "PLC IP-adres:",
"PLC Rack:": "PLC Rack:",
"PLC Slot:": "PLC-slot:",
"Round Real to:": "Rond echt naar:",
"Poll delay:": "Poll vertraging:",
"deviceId": "Slave ID",
"Reconnect time:": "Reconnect tijd:",
"Pulse time:": "Pulstijd:",
"Import symbols file:": "Symboolbestand importeren:",
"Import DB file:": "DB-bestand importeren:",
"Load Symbols": "Laad symbolen",
"Add DB": "Voeg DB toe",
"Toggle poll": "Poll wisselen",
"Toggle RW": "RW omschakelen",
"Toggle WP": "Wissel WP",
"Address": "Adres",
"Name": "Naam",
"Description": "Beschrijving",
"Type": "Type",
"Unit": "Eenheid",
"poll": "poll",
"RW": "RW",
"CW": "CW",
"WP": "WP",
"Role": "Rol",
"Room": "Kamer",
"registers": "registers",
"Device ID:": "Apparaat ID:",
"RTU over TCP": "RTU via TCP",
"Data bits:": "Databits:",
"Stop bits:": "Stop bits:",
"Parity:": "Pariteit:",
"Read timeout:": "Lees time-out:",
"Multi device IDs:": "ID's voor meerdere apparaten:",
"Use aliases as address:": "Gebruik aliassen:",
"Max read request length:": "Max. Leesverzoeklengte:",
"Enable polling of data point": "Polling van gegevenspunt inschakelen",
"Write access allowed": "Schrijftoegang toegestaan",
"Write pulses (true=>false edge)": "Schrijf pulsen (true => false edge)",
"Connection parameters:": "Verbindingsparameters:",
"Partner IP Address:": "IP-adres van partner:",
"Port:": "Haven:",
"Type:": "Type:",
"Master": "Meester",
"Slave": "Slaaf",
"Are you sure?": "Weet je het zeker?",
"Start address:": "Start adres:",
"Text copied to clipboard. Click to close the window": "Tekst gekopieerd naar klembord. Klik om het venster te sluiten",
"Export": "Exporteren",
"Import": "Importeren",
"Close": "Dichtbij",
"Export to CSV": "Exporteren naar CSV",
"Import from CSV": "Importeren vanuit CSV",
"Delete all entries": "Verwijder alle vermeldingen",
"Length": "Lengte",
"All entries will be deleted. Are you sure?": "Alle inzendingen worden verwijderd. Weet je het zeker?",
"Factor": "Factor",
"Offset": "compenseren",
"Cyclic write": "Cyclisch schrijven",
"TCP/Serial RTU:": "TCP / Serial RTU:",
"TCP": "TCP",
"Serial": "serie-",
"Baud rate:": "Baudrate:",
"Use direct addresses by aliases:": "Gebruik directe adressen op aliassen:",
"Select port": "Selecteer poort"
}

View File

@ -0,0 +1,74 @@
{
"General": "Generał",
"Inputs": "Wejścia dyskretne",
"Do not align addresses to word:": "Nie wyrównaj adresów do 16 bitów:",
"Coils": "Cewki",
"Input Registers": "Rejestry wejściowe",
"Holding Registers": "Prowadzenie rejestrów",
"PLC Connection:": "Połączenie PLC:",
"PLC IP Address:": "Adres IP sterownika PLC:",
"PLC Rack:": "Rack PLC:",
"PLC Slot:": "Gniazdo PLC:",
"Round Real to:": "Runda prawdziwa do:",
"Poll delay:": "Opóźnienie ankiety:",
"deviceId": "ID Slave",
"Reconnect time:": "Czas ponownego połączenia:",
"Pulse time:": "Czas impulsu:",
"Import symbols file:": "Importuj plik symboli:",
"Import DB file:": "Importuj plik DB:",
"Load Symbols": "Załaduj symbole",
"Add DB": "Dodaj DB",
"Toggle poll": "Przełącz ankietę",
"Toggle RW": "Przełącz RW",
"Toggle WP": "Przełącz WP",
"Address": "Adres",
"Name": "Nazwa",
"Description": "Opis",
"Type": "Rodzaj",
"Unit": "Jednostka",
"poll": "głosowanie",
"RW": "RW",
"CW": "CW",
"WP": "WP",
"Role": "Rola",
"Room": "Pokój",
"registers": "rejestry",
"Device ID:": "Identyfikator urzadzenia:",
"RTU over TCP": "RTU przez TCP",
"Data bits:": "Bity danych:",
"Stop bits:": "Stop bitów:",
"Parity:": "Parytet:",
"Read timeout:": "Odczyt limitu czasu:",
"Multi device IDs:": "Identyfikatory wielu urządzeń:",
"Use aliases as address:": "Użyj aliasów:",
"Max read request length:": "Maksymalna długość żądania odczytu:",
"Enable polling of data point": "Włącz odpytywanie punktu danych",
"Write access allowed": "Dostęp do zapisu dozwolony",
"Write pulses (true=>false edge)": "Pisz impulsy (true => false edge)",
"Connection parameters:": "Parametry połączenia:",
"Partner IP Address:": "Adres IP partnera:",
"Port:": "Port:",
"Type:": "Rodzaj:",
"Master": "Mistrz",
"Slave": "Niewolnik",
"Are you sure?": "Jesteś pewny?",
"Start address:": "Adres początkowy:",
"Text copied to clipboard. Click to close the window": "Tekst skopiowany do schowka. Kliknij, aby zamknąć okno",
"Export": "Eksport",
"Import": "Import",
"Close": "Blisko",
"Export to CSV": "Eksportuj do pliku CSV",
"Import from CSV": "Importuj z CSV",
"Delete all entries": "Usuń wszystkie wpisy",
"Length": "Długość",
"All entries will be deleted. Are you sure?": "Wszystkie wpisy zostaną usunięte. Jesteś pewny?",
"Factor": "Czynnik",
"Offset": "Offsetowy",
"Cyclic write": "Cykliczny zapis",
"TCP/Serial RTU:": "TCP / Serial RTU:",
"TCP": "TCP",
"Serial": "Seryjny",
"Baud rate:": "Szybkość transmisji:",
"Use direct addresses by aliases:": "Użyj bezpośrednich adresów przez aliasy:",
"Select port": "Wybierz port"
}

View File

@ -0,0 +1,74 @@
{
"General": "Geral",
"Inputs": "Entradas discretas",
"Do not align addresses to word:": "Não alinhe endereços para 16 bits:",
"Coils": "Bobinas",
"Input Registers": "Registos de entrada",
"Holding Registers": "Registros de retenção",
"PLC Connection:": "Conexão do PLC:",
"PLC IP Address:": "Endereço IP do PLC:",
"PLC Rack:": "PLC Rack:",
"PLC Slot:": "Slot PLC:",
"Round Real to:": "Round real to:",
"Poll delay:": "Retardo de enquete:",
"deviceId": "Slave ID",
"Reconnect time:": "Reconectar o tempo:",
"Pulse time:": "Tempo de pulso:",
"Import symbols file:": "Arquivo de símbolos de importação:",
"Import DB file:": "Importar arquivo DB:",
"Load Symbols": "Carregar símbolos",
"Add DB": "Adicionar DB",
"Toggle poll": "Alternar pesquisa",
"Toggle RW": "Toggle RW",
"Toggle WP": "Toggle WP",
"Address": "Endereço",
"Name": "Nome",
"Description": "Descrição",
"Type": "Tipo",
"Unit": "Unidade",
"poll": "votação",
"RW": "RW",
"CW": "CW",
"WP": "WP",
"Role": "Função",
"Room": "Quarto",
"registers": "registros",
"Device ID:": "ID de dispositivo:",
"RTU over TCP": "RTU sobre TCP",
"Data bits:": "Bits de dados:",
"Stop bits:": "Parar bits:",
"Parity:": "Paridade:",
"Read timeout:": "Tempo de ler esgotado:",
"Multi device IDs:": "IDs de vários dispositivos:",
"Use aliases as address:": "Use aliases:",
"Max read request length:": "Comprimento máximo do pedido de leitura:",
"Enable polling of data point": "Habilitar a votação do ponto de dados",
"Write access allowed": "Acesso de acesso permitido",
"Write pulses (true=>false edge)": "Escrever pulsos (true => false edge)",
"Connection parameters:": "Parâmetros de conexão:",
"Partner IP Address:": "Endereço IP do parceiro:",
"Port:": "Porta:",
"Type:": "Tipo:",
"Master": "mestre",
"Slave": "Escravo",
"Are you sure?": "Você tem certeza?",
"Start address:": "Endereço de início:",
"Text copied to clipboard. Click to close the window": "Texto copiado para a área de transferência. Clique para fechar a janela",
"Export": "Exportar",
"Import": "Importar",
"Close": "Fechar",
"Export to CSV": "Exportar para CSV",
"Import from CSV": "Importação de CSV",
"Delete all entries": "Eliminar todas as entradas",
"Length": "comprimento",
"All entries will be deleted. Are you sure?": "Todas as entradas serão excluídas. Você tem certeza?",
"Factor": "Fator",
"Offset": "Offset",
"Cyclic write": "Escrita cíclica",
"TCP/Serial RTU:": "TCP / Serial RTU:",
"TCP": "TCP",
"Serial": "Serial",
"Baud rate:": "Taxa de transmissão:",
"Use direct addresses by aliases:": "Use endereços diretos por alias:",
"Select port": "Selecione a porta"
}

View File

@ -0,0 +1,74 @@
{
"General": "Основное",
"Inputs": "Дискретные входы",
"Do not align addresses to word:": "Не выравнивать адреса до 16 бит:",
"Coils": "Регистры флагов",
"Input Registers": "Регистры входа",
"Holding Registers": "Регистры хранения",
"PLC Connection:": "PLC соединение:",
"PLC IP Address:": "PLC IP адрес:",
"PLC Rack:": "PLC Rack:",
"PLC Slot:": "PLC слот:",
"Round Real to:": "Округлять real до:",
"Poll delay:": "Интервал опроса:",
"deviceId": "Slave ID",
"Reconnect time:": "Reconnect time:",
"Pulse time:": "Pulse time:",
"Import symbols file:": "Ипморт символьных файлов:",
"Import DB file:": "Ипморт DB файлов:",
"Load Symbols": "Загрузить символы",
"Add DB": "Добавить DB",
"Toggle poll": "Изменить poll",
"Toggle RW": "Изменить RW",
"Toggle WP": "Изменить WP",
"Address": "Адрес",
"Name": "Имя",
"Description": "Описание",
"Type": "Тип",
"Unit": "Единицы",
"poll": "poll",
"RW": "RW",
"CW": "CW",
"WP": "WP",
"Role": "Роль",
"Room": "Комната",
"registers": "регистров",
"Device ID:": "ID устройства:",
"RTU over TCP": "RTU over TCP",
"Data bits:": "Data bits:",
"Stop bits:": "Stop bits:",
"Parity:": "Parity:",
"Read timeout:": "Таймаут чтения:",
"Multi device IDs:": "Несколько ID:",
"Use aliases as address:": "Использовать alias:",
"Max read request length:": "Макс. кол-во регистров при чтении:",
"Enable polling of data point": "Постоянный опрос переменной в каждом цикле",
"Write access allowed": "Разрешить запись в переменную",
"Write pulses (true=>false edge)": "Генерировать импульсы (1 => 0)",
"Connection parameters:": "Параметры соединения:",
"Partner IP Address:": "IP адрес партнёра:",
"Port:": "Порт:",
"Type:": "Тип:",
"Master": "Master",
"Slave": "Slave",
"Are you sure?": "Вы уверенны?",
"Start address:": "Начальный адрес:",
"Text copied to clipboard. Click to close the window": "Текст скопирован в буфер обмена. Щелкните мышкой здесь, чтобы закрыть окно",
"Export": "Экспорт",
"Import": "Импорт",
"Close": "Закрыть",
"Export to CSV": "Экспорт в CSV",
"Import from CSV": "Импорт из CSV",
"Delete all entries": "Удалить все элементы",
"Length": "Длина",
"All entries will be deleted. Are you sure?": "Все элементы будут удалены. Вы уверенны?",
"Factor": "Множитель",
"Offset": "Сдвиг",
"Cyclic write": "Писать в каждом цикле",
"TCP/Serial RTU:": "TCP/Serial RTU:",
"TCP": "TCP",
"Serial": "Serial",
"Baud rate:": "Скорость порта:",
"Use direct addresses by aliases:": "Использовать прямые адреса при alias:",
"Select port": "Выберите порт"
}

BIN
admin/img/plc_back.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

1372
admin/index.html Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

7
admin/lib/css/jsgrid-theme.min.css vendored Normal file

File diff suppressed because one or more lines are too long

121
admin/lib/css/jsgrid.css Normal file
View File

@ -0,0 +1,121 @@
/*
* jsGrid v1.0.1 (http://js-grid.com)
* (c) 2015 Artem Tabalin
* Licensed under MIT (https://github.com/tabalinas/jsgrid/blob/master/LICENSE)
*/
.jsgrid {
position: relative;
overflow: hidden;
font-size: 1em;
}
.jsgrid, .jsgrid *, .jsgrid *:before, .jsgrid *:after {
box-sizing: border-box;
}
.jsgrid input,
.jsgrid textarea,
.jsgrid select {
font-size: 1em;
}
.jsgrid-grid-header {
overflow-x: hidden;
overflow-y: hidden;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
.jsgrid-grid-body {
overflow-x: hidden;
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
}
.jsgrid-table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
border-spacing: 0;
}
.jsgrid-table td {
padding: 0.5em 0.5em;
}
.jsgrid-table td,
.jsgrid-table th {
box-sizing: border-box;
}
.jsgrid-align-left {
text-align: left;
}
.jsgrid-align-center {
text-align: center;
}
.jsgrid-align-right {
text-align: right;
}
.jsgrid-header-row > th {
padding: .5em .5em;
}
.jsgrid-filter-row input,
.jsgrid-filter-row textarea,
.jsgrid-filter-row select,
.jsgrid-edit-row input,
.jsgrid-edit-row textarea,
.jsgrid-edit-row select,
.jsgrid-insert-row input,
.jsgrid-insert-row textarea,
.jsgrid-insert-row select {
width: 90%;
padding: .3em .5em;
}
.jsgrid-filter-row input[type='checkbox'],
.jsgrid-edit-row input[type='checkbox'],
.jsgrid-insert-row input[type='checkbox'] {
width: auto;
}
.jsgrid-header-row > th,
.jsgrid-filter-row > td,
.jsgrid-insert-row > td,
.jsgrid-edit-row > td {
text-align: center;
}
.jsgrid-selected-row td {
cursor: pointer;
}
.jsgrid-nodata-row td {
padding: .5em 0;
text-align: center;
}
.jsgrid-header-sort {
cursor: pointer;
}
.jsgrid-pager {
padding: .5em 0;
}
.jsgrid-pager-nav-button {
padding: .2em .6em;
}
.jsgrid-pager-page {
padding: .2em .6em;
}

7
admin/lib/css/jsgrid.min.css vendored Normal file
View File

@ -0,0 +1,7 @@
/*
* jsGrid v1.0.1 (http://js-grid.com)
* (c) 2015 Artem Tabalin
* Licensed under MIT (https://github.com/tabalinas/jsgrid/blob/master/LICENSE)
*/
.jsgrid{position:relative;overflow:hidden;font-size:1em}.jsgrid,.jsgrid *,.jsgrid :after,.jsgrid :before{box-sizing:border-box}.jsgrid input,.jsgrid select,.jsgrid textarea{font-size:1em}.jsgrid-grid-header{overflow-x:hidden;overflow-y:hidden;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.jsgrid-grid-body{overflow-x:hidden;overflow-y:scroll;-webkit-overflow-scrolling:touch}.jsgrid-table{width:100%;table-layout:fixed;border-collapse:collapse;border-spacing:0}.jsgrid-table td{padding:.5em}.jsgrid-table td,.jsgrid-table th{box-sizing:border-box}.jsgrid-align-left{text-align:left}.jsgrid-align-center{text-align:center}.jsgrid-align-right{text-align:right}.jsgrid-header-row>th{padding:.5em}.jsgrid-edit-row input,.jsgrid-edit-row select,.jsgrid-edit-row textarea,.jsgrid-filter-row input,.jsgrid-filter-row select,.jsgrid-filter-row textarea,.jsgrid-insert-row input,.jsgrid-insert-row select,.jsgrid-insert-row textarea{width:90%;padding:.3em .5em}.jsgrid-edit-row input[type=checkbox],.jsgrid-filter-row input[type=checkbox],.jsgrid-insert-row input[type=checkbox]{width:auto}.jsgrid-edit-row>td,.jsgrid-filter-row>td,.jsgrid-header-row>th,.jsgrid-insert-row>td{text-align:center}.jsgrid-selected-row td{cursor:pointer}.jsgrid-nodata-row td{padding:.5em 0;text-align:center}.jsgrid-header-sort{cursor:pointer}.jsgrid-pager{padding:.5em 0}.jsgrid-pager-nav-button,.jsgrid-pager-page{padding:.2em .6em}

View File

@ -0,0 +1,10 @@
function translate_de() {
window.jsGrid.Grid.prototype.noDataContent = "Nicht gefunden";
window.jsGrid.Grid.prototype.deleteConfirm = "Sind Sie sicher?";
window.jsGrid.Grid.prototype.pagerFormat = "Seiten = {first} {prev} {pages} {next} {last} &nbsp;&nbsp; {pageIndex} of {pageCount}";
window.jsGrid.Grid.prototype.pagePrevText = "Vorh.";
window.jsGrid.Grid.prototype.pageNextText = "Nexte";
window.jsGrid.Grid.prototype.pageFirstText = "Erste";
window.jsGrid.Grid.prototype.pageLastText = "Letzte";
window.jsGrid.Grid.prototype.loadMessage = "Bitte; warten...";
}

View File

@ -0,0 +1,11 @@
function translate_ru() {
window.jsGrid.Grid.prototype.noDataContent = "Не найдено";
window.jsGrid.Grid.prototype.deleteConfirm = "Вы уверены?";
window.jsGrid.Grid.prototype.pagerFormat = "Страницы = {first} {prev} {pages} {next} {last} &nbsp;&nbsp; {pageIndex} of {pageCount}";
window.jsGrid.Grid.prototype.pagePrevText = "Пред.";
window.jsGrid.Grid.prototype.pageNextText = "След";
window.jsGrid.Grid.prototype.pageFirstText = "Первая";
window.jsGrid.Grid.prototype.pageLastText = "Последняя";
window.jsGrid.Grid.prototype.loadMessage = "Подождите...";
}

1954
admin/lib/js/jsgrid.js Normal file

File diff suppressed because it is too large Load Diff

BIN
admin/modbus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

83
admin/words.js Normal file
View File

@ -0,0 +1,83 @@
// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n
/*global systemDictionary:true */
'use strict';
systemDictionary = {
"General": { "en": "General", "de": "Allgemein", "ru": "Основное", "pt": "Geral", "nl": "Algemeen", "fr": "Général", "it": "Generale", "es": "General", "pl": "Generał"},
"Inputs": { "en": "Discrete Inputs", "de": "Diskrete Eingänge", "ru": "Дискретные входы", "pt": "Entradas discretas", "nl": "Discrete ingangen", "fr": "Entrées discrètes", "it": "Ingressi discreti", "es": "Entradas discretas", "pl": "Wejścia dyskretne"},
"Do not align addresses to word:": { "en": "Do not align addresses to 16 bits:", "de": "Die Adressen nicht auf 16 Bits ausrichten:", "ru": "Не выравнивать адреса до 16 бит:", "pt": "Não alinhe endereços para 16 bits:", "nl": "Lijn adressen niet uit met 16 bits:", "fr": "Ne pas aligner les adresses sur 16 bits:", "it": "Non allineare gli indirizzi a 16 bit:", "es": "No alinee las direcciones a 16 bits:", "pl": "Nie wyrównaj adresów do 16 bitów:"},
"Coils": { "en": "Coils", "de": "Diskrete Ausgänge", "ru": "Регистры флагов", "pt": "Bobinas", "nl": "coils", "fr": "Bobines", "it": "bobine", "es": "Bobinas", "pl": "Cewki"},
"Input Registers": { "en": "Input Registers", "de": "Eingangsregister", "ru": "Регистры входа", "pt": "Registos de entrada", "nl": "Invoegregisters", "fr": "Registres d'entrée", "it": "Registri di input", "es": "Registros de entrada", "pl": "Rejestry wejściowe"},
"Holding Registers": { "en": "Holding Registers", "de": "Eingangsregister", "ru": "Регистры хранения", "pt": "Registros de retenção", "nl": "Registers houden", "fr": "Tenir des registres", "it": "Holding Registers", "es": "Registros de mantenimiento", "pl": "Prowadzenie rejestrów"},
"PLC Connection:": { "en": "PLC Connection:", "de": "SPS Verbindung:", "ru": "PLC соединение:", "pt": "Conexão do PLC:", "nl": "PLC-verbinding:", "fr": "Connexion PLC:", "it": "Connessione PLC:", "es": "Conexión de PLC:", "pl": "Połączenie PLC:"},
"PLC IP Address:": { "en": "PLC IP Address:", "de": "SPS IP Adresse:", "ru": "PLC IP адрес:", "pt": "Endereço IP do PLC:", "nl": "PLC IP-adres:", "fr": "Adresse IP de l'API:", "it": "Indirizzo IP del PLC:", "es": "Dirección IP del PLC:", "pl": "Adres IP sterownika PLC:"},
"PLC Rack:": { "en": "PLC Rack:", "de": "SPS Rack:", "ru": "PLC Rack:", "pt": "PLC Rack:", "nl": "PLC Rack:", "fr": "Rack PLC:", "it": "Rack PLC:", "es": "Estante del PLC:", "pl": "Rack PLC:"},
"PLC Slot:": { "en": "PLC Slot:", "de": "SPS Slot:", "ru": "PLC слот:", "pt": "Slot PLC:", "nl": "PLC-slot:", "fr": "Slot PLC:", "it": "Slot PLC:", "es": "Ranura de PLC:", "pl": "Gniazdo PLC:"},
"Round Real to:": { "en": "Round real to:", "de": "Aufrunden Real auf:", "ru": "Округлять real до:", "pt": "Round real to:", "nl": "Rond echt naar:", "fr": "Rond réel à:", "it": "Round reale a:", "es": "Redondo real para:", "pl": "Runda prawdziwa do:"},
"Poll delay:": { "en": "Poll delay:", "de": "Poll delay:", "ru": "Интервал опроса:", "pt": "Retardo de enquete:", "nl": "Poll vertraging:", "fr": "Délai d'interrogation:", "it": "Ritardo del sondaggio:", "es": "Retraso en la encuesta:", "pl": "Opóźnienie ankiety:"},
"deviceId": { "en": "Slave ID", "de": "Slave-ID", "ru": "Slave ID", "pt": "Slave ID", "nl": "Slave ID", "fr": "Slave ID", "it": "ID slave", "es": "Slave ID", "pl": "ID Slave"},
"Reconnect time:": { "en": "Reconnect time:", "de": "Reconnect-Zeit:", "ru": "Reconnect time:", "pt": "Reconectar o tempo:", "nl": "Reconnect tijd:", "fr": "Reconnectez le temps:", "it": "Tempo di riconnessione:", "es": "Tiempo de reconexión:", "pl": "Czas ponownego połączenia:"},
"Pulse time:": { "en": "Pulse time:", "de": "Pulsetime:", "ru": "Pulse time:", "pt": "Tempo de pulso:", "nl": "Pulstijd:", "fr": "Temps d'impulsion:", "it": "Tempo di impulso:", "es": "Tiempo de pulso:", "pl": "Czas impulsu:"},
"Import symbols file:": { "en": "Import symbols file:", "de": "Symboldatei Importieren:", "ru": "Ипморт символьных файлов:", "pt": "Arquivo de símbolos de importação:", "nl": "Symboolbestand importeren:", "fr": "Importer un fichier de symboles:", "it": "Importa file simboli:", "es": "Importar archivo de símbolos:", "pl": "Importuj plik symboli:"},
"Import DB file:": { "en": "Import DB file:", "de": "DB-Datei importieren:", "ru": "Ипморт DB файлов:", "pt": "Importar arquivo DB:", "nl": "DB-bestand importeren:", "fr": "Importer un fichier DB:", "it": "Importa file DB:", "es": "Importar archivo DB", "pl": "Importuj plik DB:"},
"Load Symbols": { "en": "Load symbols", "de": "Lade Symbole", "ru": "Загрузить символы", "pt": "Carregar símbolos", "nl": "Laad symbolen", "fr": "Charger des symboles", "it": "Carica simboli", "es": "Cargar símbolos", "pl": "Załaduj symbole"},
"Add DB": { "en": "Add DB", "de": "DB einfügen", "ru": "Добавить DB", "pt": "Adicionar DB", "nl": "Voeg DB toe", "fr": "Ajouter une DB", "it": "Aggiungi DB", "es": "Agregar DB", "pl": "Dodaj DB"},
"Toggle poll": { "en": "Toggle poll", "de": "Poll umschalten", "ru": "Изменить poll", "pt": "Alternar pesquisa", "nl": "Poll wisselen", "fr": "Basculer le sondage", "it": "Attiva / disattiva sondaggio", "es": "Alternar encuesta", "pl": "Przełącz ankietę"},
"Toggle RW": { "en": "Toggle RW", "de": "RW umschalten", "ru": "Изменить RW", "pt": "Toggle RW", "nl": "RW omschakelen", "fr": "Toggle RW", "it": "Attiva / disattiva RW", "es": "Alternar RW", "pl": "Przełącz RW"},
"Toggle WP": { "en": "Toggle WP", "de": "WP umschalten", "ru": "Изменить WP", "pt": "Toggle WP", "nl": "Wissel WP", "fr": "Toggle WP", "it": "Attiva / disattiva WP", "es": "Alternar WP", "pl": "Przełącz WP"},
"Address": { "en": "Address", "de": "Adresse", "ru": "Адрес", "pt": "Endereço", "nl": "Adres", "fr": "Adresse", "it": "Indirizzo", "es": "Dirección", "pl": "Adres"},
"Name": { "en": "Name", "de": "Name", "ru": "Имя", "pt": "Nome", "nl": "Naam", "fr": "prénom", "it": "Nome", "es": "Nombre", "pl": "Nazwa"},
"Description": { "en": "Description", "de": "Beschreibung", "ru": "Описание", "pt": "Descrição", "nl": "Beschrijving", "fr": "La description", "it": "Descrizione", "es": "Descripción", "pl": "Opis"},
"Type": { "en": "Type", "de": "Typ", "ru": "Тип", "pt": "Tipo", "nl": "Type", "fr": "Type", "it": "genere", "es": "Tipo", "pl": "Rodzaj"},
"Unit": { "en": "Unit", "de": "Einheit", "ru": "Единицы", "pt": "Unidade", "nl": "Eenheid", "fr": "Unité", "it": "Unità", "es": "Unidad", "pl": "Jednostka"},
"Say \"yes\" for the next minute:": { "en": "Say \"yes\" for the next minute", "de": "Sag \"Ja\" für die nächste Minute", "ru": "Скажите «да» на следующую минуту", "pt": "Diga \"sim\" no próximo minuto", "nl": "Zeg 'ja' voor de volgende minuut", "fr": "Dites \"oui\" pour la minute suivante", "it": "Dì \"sì\" per il minuto successivo", "es": "Di \"sí\" por el próximo minuto", "pl": "Powiedz \"tak\" przez następną minutę"},
"Delete": { "en": "Delete", "de": "Löschen", "ru": "Удалить", "pt": "Excluir", "nl": "Verwijder", "fr": "Effacer", "it": "Elimina", "es": "Borrar", "pl": "Kasować"},
"Delete address": { "en": "Delete address", "de": "Adresse löschen", "ru": "Удалить адрес", "pt": "Excluir endereço", "nl": "Verwijder adres", "fr": "Supprimer l'adresse", "it": "Elimina l'indirizzo", "es": "Eliminar dirección", "pl": "Usuń adres"},
"Cancel": { "en": "Cancel", "de": "Stornieren", "ru": "Отмена", "pt": "Cancelar", "nl": "Annuleer", "fr": "Annuler", "it": "Annulla", "es": "Cancelar", "pl": "Anuluj"},
"poll": { "en": "poll", "de": "poll", "ru": "poll", "pt": "votação", "nl": "poll", "fr": "sondage", "it": "sondaggio", "es": "encuesta", "pl": "głosowanie"},
"RW": { "en": "RW", "de": "RW", "ru": "RW", "pt": "RW", "nl": "RW", "fr": "RW", "it": "RW", "es": "RW", "pl": "RW"},
"CW": { "en": "CW", "de": "CW", "ru": "CW", "pt": "CW", "nl": "CW", "fr": "CW", "it": "CW", "es": "CW", "pl": "CW"},
"WP": { "en": "WP", "de": "WP", "ru": "WP", "pt": "WP", "nl": "WP", "fr": "WP", "it": "WP", "es": "WP", "pl": "WP"},
"Role": { "en": "Role", "de": "Rolle", "ru": "Роль", "pt": "Função", "nl": "Rol", "fr": "Rôle", "it": "Ruolo", "es": "Papel", "pl": "Rola"},
"Room": { "en": "Room", "de": "Raum", "ru": "Комната", "pt": "Quarto", "nl": "Kamer", "fr": "Chambre", "it": "Camera", "es": "Habitación", "pl": "Pokój"},
"registers": { "en": "registers", "de": "Register", "ru": "регистров", "pt": "registros", "nl": "registers", "fr": "registres", "it": "registri", "es": "registros", "pl": "rejestry"},
"Device ID:": { "en": "Device ID:", "de": "Geräte ID:", "ru": "ID устройства:", "pt": "ID de dispositivo:", "nl": "Apparaat ID:", "fr": "Reference de l'appareil:", "it": "ID del dispositivo:", "es": "ID del dispositivo:", "pl": "Identyfikator urzadzenia:"},
"RTU over TCP": { "en": "RTU over TCP", "de": "RTU over TCP", "ru": "RTU over TCP", "pt": "RTU sobre TCP", "nl": "RTU via TCP", "fr": "RTU sur TCP", "it": "RTU su TCP", "es": "RTU sobre TCP", "pl": "RTU przez TCP"},
"Data bits:": { "en": "Data bits:", "de": "Data bits:", "ru": "Data bits:", "pt": "Bits de dados:", "nl": "Databits:", "fr": "Bits de données:", "it": "Bit di dati:", "es": "Bits de datos:", "pl": "Bity danych:"},
"Stop bits:": { "en": "Stop bits:", "de": "Stop bits:", "ru": "Stop bits:", "pt": "Parar bits:", "nl": "Stop bits:", "fr": "Bits d'arrêt:", "it": "Stop bit:", "es": "Bits de parada:", "pl": "Stop bitów:"},
"Parity:": { "en": "Parity:", "de": "Parity:", "ru": "Parity:", "pt": "Paridade:", "nl": "Pariteit:", "fr": "Parité:", "it": "Parità:", "es": "Paridad:", "pl": "Parytet:"},
"Read timeout:": { "en": "Read timeout:", "de": "Read timeout:", "ru": "Таймаут чтения:", "pt": "Tempo de ler esgotado:", "nl": "Lees time-out:", "fr": "Lire le délai d'attente:", "it": "Leggi il timeout:", "es": "Tiempo de espera de lectura:", "pl": "Odczyt limitu czasu:"},
"Multi device IDs:": { "en": "Multi device IDs:", "de": "Mehrere Geräte-IDs:", "ru": "Несколько ID:", "pt": "IDs de vários dispositivos:", "nl": "ID's voor meerdere apparaten:", "fr": "ID multi-appareils:", "it": "ID multi dispositivo:", "es": "ID de dispositivos múltiples:", "pl": "Identyfikatory wielu urządzeń:"},
"Use aliases as address:": { "en": "Use aliases:", "de": "Aliases benutzen:", "ru": "Использовать alias:", "pt": "Use aliases:", "nl": "Gebruik aliassen:", "fr": "Utiliser des alias:", "it": "Usa alias:", "es": "Usa alias:", "pl": "Użyj aliasów:"},
"Max read request length:": { "en": "Max read request length (float)", "de": "Max Leseanforderungslänge (float)", "ru": "Максимальная длина запроса на чтение (float)", "pt": "Comprimento máximo da solicitação de leitura (float)", "nl": "Max. Leesverzoeklengte (float)", "fr": "Max longueur de la requête de lecture (float)", "it": "Lunghezza massima richiesta di lettura (float)", "es": "Longitud de solicitud de lectura máxima (float)", "pl": "Maksymalna długość żądania odczytu (float)"},
"Max read request length (booleans):": { "en": "Max read request length (booleans)", "de": "Max Leseanforderungslänge (booleans)", "ru": "Максимальная длина запроса на чтение (booleans)", "pt": "Comprimento máximo da solicitação de leitura (booleans)", "nl": "Max. Leesverzoeklengte (booleans)", "fr": "Max longueur de la requête de lecture (booleans)", "it": "Lunghezza massima richiesta di lettura (booleans)", "es": "Longitud de solicitud de lectura máxima (booleans)", "pl": "Maksymalna długość żądania odczytu (booleans)"},
"Enable polling of data point": { "en": "Enable polling of data point", "de": "Zyklische Abfrage vom Datenpunkt", "ru": "Постоянный опрос переменной в каждом цикле", "pt": "Habilitar a votação do ponto de dados", "nl": "Polling van gegevenspunt inschakelen", "fr": "Activer l'interrogation du point de données", "it": "Abilita il polling del punto dati", "es": "Habilitar el sondeo del punto de datos", "pl": "Włącz odpytywanie punktu danych"},
"Write access allowed": { "en": "Write access allowed", "de": "Schreiben erlaubt", "ru": "Разрешить запись в переменную", "pt": "Acesso de acesso permitido", "nl": "Schrijftoegang toegestaan", "fr": "Accès en écriture autorisé", "it": "Accesso in scrittura consentito", "es": "Acceso de escritura permitido", "pl": "Dostęp do zapisu dozwolony"},
"Write pulses (true=>false edge)": { "en": "Write pulses (true=>false edge)", "de": "Schreibe Pulse (Ja=>Nein Flanke)", "ru": "Генерировать импульсы (1 => 0)", "pt": "Escrever pulsos (true => false edge)", "nl": "Schrijf pulsen (true => false edge)", "fr": "Écrire des impulsions (true => false edge)", "it": "Scrivi impulsi (true => falso bordo)", "es": "Escribir pulsos (verdadero => borde falso)", "pl": "Pisz impulsy (true => false edge)"},
"Connection parameters:": { "en": "Connection parameters:", "de": "Verbindungsparameter:", "ru": "Параметры соединения:", "pt": "Parâmetros de conexão:", "nl": "Verbindingsparameters:", "fr": "Paramètres de connexion:", "it": "Parametri di connessione:", "es": "Parámetros de conexión:", "pl": "Parametry połączenia:"},
"Partner IP Address:": { "en": "Partner IP Address:", "de": "Partner IP Adresse:", "ru": "IP адрес партнёра:", "pt": "Endereço IP do parceiro:", "nl": "IP-adres van partner:", "fr": "Adresse IP du partenaire:", "it": "Indirizzo IP del partner:", "es": "Dirección IP del socio:", "pl": "Adres IP partnera:"},
"Port:": { "en": "Port:", "de": "Port:", "ru": "Порт:", "pt": "Porta:", "nl": "Haven:", "fr": "Port:", "it": "Porta:", "es": "Puerto:", "pl": "Port:"},
"Type:": { "en": "Type:", "de": "Typ:", "ru": "Тип:", "pt": "Tipo:", "nl": "Type:", "fr": "Type:", "it": "Genere:", "es": "Tipo:", "pl": "Rodzaj:"},
"Master": { "en": "Master", "de": "Master", "ru": "Master", "pt": "mestre", "nl": "Meester", "fr": "Maîtriser", "it": "Maestro", "es": "Dominar", "pl": "Mistrz"},
"Slave": { "en": "Slave", "de": "Slave", "ru": "Slave", "pt": "Escravo", "nl": "Slaaf", "fr": "Esclave", "it": "Schiavo", "es": "Esclavo", "pl": "Niewolnik"},
"Are you sure?": { "en": "Are you sure?", "de": "Sind Sie sicher?", "ru": "Вы уверенны?", "pt": "Você tem certeza?", "nl": "Weet je het zeker?", "fr": "Êtes-vous sûr?", "it": "Sei sicuro?", "es": "¿Estás seguro?", "pl": "Jesteś pewny?"},
"Start address:": { "en": "Start address:", "de": "Start-Adresse:", "ru": "Начальный адрес:", "pt": "Endereço de início:", "nl": "Start adres:", "fr": "Adresse de départ:", "it": "Indirizzo iniziale:", "es": "Dirección de inicio:", "pl": "Adres początkowy:"},
"Text copied to clipboard. Click to close the window": {"en": "Text copied to clipboard. Click to close the window", "de": "Text wurde in die Zwischenablage kopiert. Klicke um das Fenster zu schliessen.", "ru": "Текст скопирован в буфер обмена. Щелкните мышкой здесь, чтобы закрыть окно", "pt": "Texto copiado para a área de transferência. Clique para fechar a janela", "nl": "Tekst gekopieerd naar klembord. Klik om het venster te sluiten", "fr": "Texte copié dans le presse-papier Cliquez pour fermer la fenêtre", "it": "Testo copiato negli appunti. Clicca per chiudere la finestra", "es": "Texto copiado al portapapeles. Haga clic para cerrar la ventana", "pl": "Tekst skopiowany do schowka. Kliknij, aby zamknąć okno"},
"Export": { "en": "Export", "de": "Export", "ru": "Экспорт", "pt": "Exportar", "nl": "Exporteren", "fr": "Exportation", "it": "Esportare", "es": "Exportar", "pl": "Eksport"},
"Import": { "en": "Import", "de": "Import", "ru": "Импорт", "pt": "Importar", "nl": "Importeren", "fr": "Importer", "it": "Importare", "es": "Importar", "pl": "Import"},
"Close": { "en": "Close", "de": "Zumachen", "ru": "Закрыть", "pt": "Fechar", "nl": "Dichtbij", "fr": "Fermer", "it": "Vicino", "es": "Cerca", "pl": "Blisko"},
"Export to CSV": { "en": "Export to CSV", "de": "Export in CSV", "ru": "Экспорт в CSV", "pt": "Exportar para CSV", "nl": "Exporteren naar CSV", "fr": "Exporter au format CSV", "it": "Esporta in CSV", "es": "Exportar a CSV", "pl": "Eksportuj do pliku CSV"},
"Import from CSV": { "en": "Import from CSV", "de": "Import aus CSV", "ru": "Импорт из CSV", "pt": "Importação de CSV", "nl": "Importeren vanuit CSV", "fr": "Importer à partir du fichier CSV", "it": "Importa da CSV", "es": "Importar desde CSV", "pl": "Importuj z CSV"},
"Delete all entries": { "en": "Delete all entries", "de": "Alle Einträge löschen", "ru": "Удалить все элементы", "pt": "Eliminar todas as entradas", "nl": "Verwijder alle vermeldingen", "fr": "Supprimer toutes les entrées", "it": "Elimina tutte le voci", "es": "Eliminar todas las entradas", "pl": "Usuń wszystkie wpisy"},
"Length": { "en": "Length", "de": "Länge", "ru": "Длина", "pt": "comprimento", "nl": "Lengte", "fr": "Longueur", "it": "Lunghezza", "es": "Longitud", "pl": "Długość"},
"All entries will be deleted. Are you sure?": { "en": "All entries will be deleted. Are you sure?", "de": "Alle Einträge werden gelöscht. Sind sie sicher?", "ru": "Все элементы будут удалены. Вы уверенны?", "pt": "Todas as entradas serão excluídas. Você tem certeza?", "nl": "Alle inzendingen worden verwijderd. Weet je het zeker?", "fr": "Toutes les entrées seront supprimées. Êtes-vous sûr?", "it": "Tutte le voci saranno cancellate. Sei sicuro?", "es": "Todas las entradas serán eliminadas. ¿Estás seguro?", "pl": "Wszystkie wpisy zostaną usunięte. Jesteś pewny?"},
"Factor": { "en": "Factor", "de": "Faktor", "ru": "Множитель", "pt": "Fator", "nl": "Factor", "fr": "Facteur", "it": "Fattore", "es": "Factor", "pl": "Czynnik"},
"Offset": { "en": "Offset", "de": "Offset", "ru": "Сдвиг", "pt": "Offset", "nl": "compenseren", "fr": "Décalage", "it": "Compensare", "es": "Compensar", "pl": "Offsetowy"},
"Cyclic write": { "en": "Cyclic write", "de": "Zyklisch schreiben", "ru": "Писать в каждом цикле", "pt": "Escrita cíclica", "nl": "Cyclisch schrijven", "fr": "Écriture cyclique", "it": "Scrittura ciclica", "es": "Escritura cíclica", "pl": "Cykliczny zapis"},
"TCP/Serial RTU:": { "en": "TCP/Serial RTU:", "de": "TCP/Serial RTU:", "ru": "TCP/Serial RTU:", "pt": "TCP / Serial RTU:", "nl": "TCP / Serial RTU:", "fr": "TCP / Sériel RTU:", "it": "RTU TCP / seriale:", "es": "TCP / Serial RTU:", "pl": "TCP / Serial RTU:"},
"TCP": { "en": "TCP", "de": "TCP", "ru": "TCP", "pt": "TCP", "nl": "TCP", "fr": "TCP", "it": "TCP", "es": "TCP", "pl": "TCP"},
"Serial": { "en": "Serial", "de": "Serial", "ru": "Serial", "pt": "Serial", "nl": "serie-", "fr": "En série", "it": "Seriale", "es": "De serie", "pl": "Seryjny"},
"Baud rate:": { "en": "Baud rate:", "de": "Baud rate:", "ru": "Скорость порта:", "pt": "Taxa de transmissão:", "nl": "Baudrate:", "fr": "Débit en bauds:", "it": "Baud rate:", "es": "Velocidad de baudios:", "pl": "Szybkość transmisji:"},
"Use direct addresses by aliases:": { "en": "Use direct addresses by aliases:", "de": "Direkte Adressen benutzen (bei Aliases):", "ru": "Использовать прямые адреса при alias:", "pt": "Use endereços diretos por alias:", "nl": "Gebruik directe adressen op aliassen:", "fr": "Utilisez des adresses directes par alias:", "it": "Usa indirizzi diretti per alias:", "es": "Use direcciones directas por alias:", "pl": "Użyj bezpośrednich adresów przez aliasy:"},
"Select port": { "en": "Select port", "de": "Port wählen", "ru": "Выберите порт", "pt": "Selecione a porta", "nl": "Selecteer poort", "fr": "Sélectionnez un port", "it": "Seleziona porta", "es": "Seleccionar puerto", "pl": "Wybierz port"}
};

25
appveyor.yml Normal file
View File

@ -0,0 +1,25 @@
version: 'test-{build}'
environment:
matrix:
- nodejs_version: '4'
- nodejs_version: '6'
- nodejs_version: '8'
- nodejs_version: '10'
platform:
- x86
- x64
clone_folder: 'c:\projects\%APPVEYOR_PROJECT_NAME%'
install:
- ps: 'Install-Product node $env:nodejs_version $env:platform'
- ps: '$NpmVersion = (npm -v).Substring(0,1)'
- ps: 'if($NpmVersion -eq 5) { npm install -g npm@5 }'
- ps: npm --version
- npm install
- npm install winston@2.3.1
- 'npm install https://github.com/yunkong2/yunkong2.js-controller/tarball/master --production'
test_script:
- echo %cd%
- node --version
- npm --version
- npm test
build: 'off'

401
gulpfile.js Normal file
View File

@ -0,0 +1,401 @@
'use strict';
var gulp = require('gulp');
var fs = require('fs');
var pkg = require('./package.json');
var iopackage = require('./io-package.json');
var version = (pkg && pkg.version) ? pkg.version : iopackage.common.version;
/*var appName = getAppName();
function getAppName() {
var parts = __dirname.replace(/\\/g, '/').split('/');
return parts[parts.length - 1].split('.')[0].toLowerCase();
}
*/
const fileName = 'words.js';
var languages = {
en: {},
de: {},
ru: {},
pt: {},
nl: {},
fr: {},
it: {},
es: {},
pl: {}
};
function lang2data(lang, isFlat) {
var str = isFlat ? '' : '{\n';
var count = 0;
for (var w in lang) {
if (lang.hasOwnProperty(w)) {
count++;
if (isFlat) {
str += (lang[w] === '' ? (isFlat[w] || w) : lang[w]) + '\n';
} else {
var key = ' "' + w.replace(/"/g, '\\"') + '": ';
str += key + '"' + lang[w].replace(/"/g, '\\"') + '",\n';
}
}
}
if (!count) return isFlat ? '' : '{\n}';
if (isFlat) {
return str;
} else {
return str.substring(0, str.length - 2) + '\n}';
}
}
function readWordJs(src) {
try {
var words;
if (fs.existsSync(src + 'js/' + fileName)) {
words = fs.readFileSync(src + 'js/' + fileName).toString();
} else {
words = fs.readFileSync(src + fileName).toString();
}
var lines = words.split(/\r\n|\r|\n/g);
var i = 0;
while (!lines[i].match(/^systemDictionary = {/)) {
i++;
}
lines.splice(0, i);
// remove last empty lines
i = lines.length - 1;
while (!lines[i]) {
i--;
}
if (i < lines.length - 1) {
lines.splice(i + 1);
}
lines[0] = lines[0].replace('systemDictionary = ', '');
lines[lines.length - 1] = lines[lines.length - 1].trim().replace(/};$/, '}');
words = lines.join('\n');
var resultFunc = new Function('return ' + words + ';');
return resultFunc();
} catch (e) {
return null;
}
}
function padRight(text, totalLength) {
return text + (text.length < totalLength ? new Array(totalLength - text.length).join(' ') : '');
}
function writeWordJs(data, src) {
var text = '// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n\n';
text += '/*global systemDictionary:true */\n';
text += '\'use strict\';\n\n';
text += 'systemDictionary = {\n';
for (var word in data) {
if (data.hasOwnProperty(word)) {
text += ' ' + padRight('"' + word.replace(/"/g, '\\"') + '": {', 50);
var line = '';
for (var lang in data[word]) {
if (data[word].hasOwnProperty(lang)) {
line += '"' + lang + '": "' + padRight(data[word][lang].replace(/"/g, '\\"') + '",', 50) + ' ';
}
}
if (line) {
line = line.trim();
line = line.substring(0, line.length - 1);
}
text += line + '},\n';
}
}
text = text.replace(/},\n$/, '}\n');
text += '};';
if (fs.existsSync(src + 'js/' + fileName)) {
fs.writeFileSync(src + 'js/' + fileName, text);
} else {
fs.writeFileSync(src + '' + fileName, text);
}
}
const EMPTY = '';
function words2languages(src) {
var langs = Object.assign({}, languages);
var data = readWordJs(src);
if (data) {
for (var word in data) {
if (data.hasOwnProperty(word)) {
for (var lang in data[word]) {
if (data[word].hasOwnProperty(lang)) {
langs[lang][word] = data[word][lang];
// pre-fill all other languages
for (var j in langs) {
if (langs.hasOwnProperty(j)) {
langs[j][word] = langs[j][word] || EMPTY;
}
}
}
}
}
}
if (!fs.existsSync(src + 'i18n/')) {
fs.mkdirSync(src + 'i18n/');
}
for (var l in langs) {
if (!langs.hasOwnProperty(l)) continue;
var keys = Object.keys(langs[l]);
//keys.sort();
var obj = {};
for (var k = 0; k < keys.length; k++) {
obj[keys[k]] = langs[l][keys[k]];
}
if (!fs.existsSync(src + 'i18n/' + l)) {
fs.mkdirSync(src + 'i18n/' + l);
}
fs.writeFileSync(src + 'i18n/' + l + '/translations.json', lang2data(obj));
}
} else {
console.error('Cannot read or parse ' + fileName);
}
}
function words2languagesFlat(src) {
var langs = Object.assign({}, languages);
var data = readWordJs(src);
if (data) {
for (var word in data) {
if (data.hasOwnProperty(word)) {
for (var lang in data[word]) {
if (data[word].hasOwnProperty(lang)) {
langs[lang][word] = data[word][lang];
// pre-fill all other languages
for (var j in langs) {
if (langs.hasOwnProperty(j)) {
langs[j][word] = langs[j][word] || EMPTY;
}
}
}
}
}
}
var keys = Object.keys(langs.en);
//keys.sort();
for (var l in langs) {
if (!langs.hasOwnProperty(l)) continue;
var obj = {};
for (var k = 0; k < keys.length; k++) {
obj[keys[k]] = langs[l][keys[k]];
}
langs[l] = obj;
}
if (!fs.existsSync(src + 'i18n/')) {
fs.mkdirSync(src + 'i18n/');
}
for (var ll in langs) {
if (!langs.hasOwnProperty(ll)) continue;
if (!fs.existsSync(src + 'i18n/' + ll)) {
fs.mkdirSync(src + 'i18n/' + ll);
}
fs.writeFileSync(src + 'i18n/' + ll + '/flat.txt', lang2data(langs[ll], langs.en));
}
fs.writeFileSync(src + 'i18n/flat.txt', keys.join('\n'));
} else {
console.error('Cannot read or parse ' + fileName);
}
}
function languagesFlat2words(src) {
var dirs = fs.readdirSync(src + 'i18n/');
var langs = {};
var bigOne = {};
var order = Object.keys(languages);
dirs.sort(function (a, b) {
var posA = order.indexOf(a);
var posB = order.indexOf(b);
if (posA === -1 && posB === -1) {
if (a > b) return 1;
if (a < b) return -1;
return 0;
} else if (posA === -1) {
return -1;
} else if (posB === -1) {
return 1;
} else {
if (posA > posB) return 1;
if (posA < posB) return -1;
return 0;
}
});
var keys = fs.readFileSync(src + 'i18n/flat.txt').toString().split('\n');
for (var l = 0; l < dirs.length; l++) {
if (dirs[l] === 'flat.txt') continue;
var lang = dirs[l];
var values = fs.readFileSync(src + 'i18n/' + lang + '/flat.txt').toString().split('\n');
langs[lang] = {};
keys.forEach(function (word, i) {
langs[lang][word] = values[i].replace(/<\/ i>/g, '</i>').replace(/<\/ b>/g, '</b>').replace(/<\/ span>/g, '</span>').replace(/% s/g, ' %s');
});
var words = langs[lang];
for (var word in words) {
if (words.hasOwnProperty(word)) {
bigOne[word] = bigOne[word] || {};
if (words[word] !== EMPTY) {
bigOne[word][lang] = words[word];
}
}
}
}
// read actual words.js
var aWords = readWordJs();
var temporaryIgnore = ['pt', 'fr', 'nl', 'flat.txt'];
if (aWords) {
// Merge words together
for (var w in aWords) {
if (aWords.hasOwnProperty(w)) {
if (!bigOne[w]) {
console.warn('Take from actual words.js: ' + w);
bigOne[w] = aWords[w]
}
dirs.forEach(function (lang) {
if (temporaryIgnore.indexOf(lang) !== -1) return;
if (!bigOne[w][lang]) {
console.warn('Missing "' + lang + '": ' + w);
}
});
}
}
}
writeWordJs(bigOne, src);
}
function languages2words(src) {
var dirs = fs.readdirSync(src + 'i18n/');
var langs = {};
var bigOne = {};
var order = Object.keys(languages);
dirs.sort(function (a, b) {
var posA = order.indexOf(a);
var posB = order.indexOf(b);
if (posA === -1 && posB === -1) {
if (a > b) return 1;
if (a < b) return -1;
return 0;
} else if (posA === -1) {
return -1;
} else if (posB === -1) {
return 1;
} else {
if (posA > posB) return 1;
if (posA < posB) return -1;
return 0;
}
});
for (var l = 0; l < dirs.length; l++) {
if (dirs[l] === 'flat.txt') continue;
var lang = dirs[l];
langs[lang] = fs.readFileSync(src + 'i18n/' + lang + '/translations.json').toString();
langs[lang] = JSON.parse(langs[lang]);
var words = langs[lang];
for (var word in words) {
if (words.hasOwnProperty(word)) {
bigOne[word] = bigOne[word] || {};
if (words[word] !== EMPTY) {
bigOne[word][lang] = words[word];
}
}
}
}
// read actual words.js
var aWords = readWordJs();
var temporaryIgnore = ['pt', 'fr', 'nl', 'it'];
if (aWords) {
// Merge words together
for (var w in aWords) {
if (aWords.hasOwnProperty(w)) {
if (!bigOne[w]) {
console.warn('Take from actual words.js: ' + w);
bigOne[w] = aWords[w]
}
dirs.forEach(function (lang) {
if (temporaryIgnore.indexOf(lang) !== -1) return;
if (!bigOne[w][lang]) {
console.warn('Missing "' + lang + '": ' + w);
}
});
}
}
}
writeWordJs(bigOne, src);
}
gulp.task('adminWords2languages', function (done) {
words2languages('./admin/');
done();
});
gulp.task('adminWords2languagesFlat', function (done) {
words2languagesFlat('./admin/');
done();
});
gulp.task('adminLanguagesFlat2words', function (done) {
languagesFlat2words('./admin/');
done();
});
gulp.task('adminLanguages2words', function (done) {
languages2words('./admin/');
done();
});
gulp.task('updatePackages', function (done) {
iopackage.common.version = pkg.version;
iopackage.common.news = iopackage.common.news || {};
if (!iopackage.common.news[pkg.version]) {
var news = iopackage.common.news;
var newNews = {};
newNews[pkg.version] = {
en: 'news',
de: 'neues',
ru: 'новое'
};
iopackage.common.news = Object.assign(newNews, news);
}
fs.writeFileSync('io-package.json', JSON.stringify(iopackage, null, 4));
done();
});
gulp.task('updateReadme', function (done) {
var readme = fs.readFileSync('README.md').toString();
var pos = readme.indexOf('## Changelog\n');
if (pos !== -1) {
var readmeStart = readme.substring(0, pos + '## Changelog\n'.length);
var readmeEnd = readme.substring(pos + '## Changelog\n'.length);
if (readme.indexOf(version) === -1) {
var timestamp = new Date();
var date = timestamp.getFullYear() + '-' +
('0' + (timestamp.getMonth() + 1).toString(10)).slice(-2) + '-' +
('0' + (timestamp.getDate()).toString(10)).slice(-2);
var news = '';
if (iopackage.common.news && iopackage.common.news[pkg.version]) {
news += '* ' + iopackage.common.news[pkg.version].en;
}
fs.writeFileSync('README.md', readmeStart + '### ' + version + ' (' + date + ')\n' + (news ? news + '\n\n' : '\n') + readmeEnd);
}
}
done();
});
gulp.task('default', ['updatePackages', 'updateReadme']);

BIN
img/img1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
img/img2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
img/img3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
img/img4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
img/img5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

84
io-package.json Normal file
View File

@ -0,0 +1,84 @@
{
"common": {
"name": "rsonoff",
"version": "2.0.9",
"title": "RSonoff",
"desc": {
"en": "RSonoff",
"cn": "串口开关控制器"
},
"license": "MIT",
"platform": "Javascript/Node.js",
"mode": "daemon",
"loglevel": "info",
"messagebox": true,
"icon": "modbus.png",
"keywords": [
"ModBus"
],
"enabled": true,
"extIcon": "https://git.spacen.net/yunkong2/yunkong2.rsonoff/raw/master/admin/modbus.png",
"type": "protocols",
"config": {
"minWidth": 1024,
"width": 1224,
"height": 600
}
},
"native": {
"params": {
"type": "tcp",
"bind": "127.0.0.1",
"port": 502,
"comName": "",
"baudRate": 9600,
"dataBits": 8,
"stopBits": 1,
"parity": "none",
"deviceId": 1,
"timeout": 5000,
"slave": 0,
"poll": 1000,
"recon": 60000,
"maxBlock": 100,
"maxBoolBlock": 128,
"multiDeviceId": false,
"pulsetime": 1000,
"disInputsOffset": 10001,
"coilsOffset": 1,
"inputRegsOffset": 30001,
"holdingRegsOffset": 40001,
"showAliases": true,
"directAddresses": false,
"round": 2,
"doNotRoundAddressToWord": false
},
"disInputs": [],
"coils": [],
"inputRegs": [],
"holdingRegs": []
},
"instanceObjects": [
{
"_id": "info",
"type": "channel",
"common": {
"name": "Information"
},
"native": {}
},
{
"_id": "info.connection",
"type": "state",
"common": {
"role": "indicator.connected",
"name": "If master connected",
"type": "boolean",
"read": true,
"write": false,
"def": false
},
"native": {}
}
]
}

335
lib/common.js Normal file
View File

@ -0,0 +1,335 @@
'use strict';
function extractValue(type, len, buffer, offset) {
let i1;
let i2;
let buf;
switch (type) {
case 'uint8be':
return buffer.readUInt8(offset * 2 + 1);
case 'uint8le':
return buffer.readUInt8(offset * 2);
case 'int8be':
return buffer.readInt8(offset * 2 + 1);
case 'int8le':
return buffer.readInt8(offset * 2);
case 'uint16be':
return buffer.readUInt16BE(offset * 2);
case 'uint16le':
return buffer.readUInt16LE(offset * 2);
case 'int16be':
return buffer.readInt16BE(offset * 2);
case 'int16le':
return buffer.readInt16LE(offset * 2);
case 'uint32be':
return buffer.readUInt32BE(offset * 2);
case 'uint32le':
return buffer.readUInt32LE(offset * 2);
case 'uint32sw':
buf = new Buffer(4);
buf[0] = buffer[offset * 2 + 2];
buf[1] = buffer[offset * 2 + 3];
buf[2] = buffer[offset * 2 + 0];
buf[3] = buffer[offset * 2 + 1];
return buf.readUInt32BE(0);
case 'uint32sb':
buf = new Buffer(4);
buf[0] = buffer[offset * 2 + 1];
buf[1] = buffer[offset * 2 + 0];
buf[2] = buffer[offset * 2 + 3];
buf[3] = buffer[offset * 2 + 2];
return buf.readUInt32BE(0);
case 'int32be':
return buffer.readInt32BE(offset * 2);
case 'int32le':
return buffer.readInt32LE(offset * 2);
case 'int32sw':
buf = new Buffer(4);
buf[0] = buffer[offset * 2 + 2];
buf[1] = buffer[offset * 2 + 3];
buf[2] = buffer[offset * 2 + 0];
buf[3] = buffer[offset * 2 + 1];
return buf.readInt32BE(0);
case 'int32sb':
buf = new Buffer(4);
buf[0] = buffer[offset * 2 + 1];
buf[1] = buffer[offset * 2 + 0];
buf[2] = buffer[offset * 2 + 3];
buf[3] = buffer[offset * 2 + 2];
return buf.readInt32BE(0);
case 'uint64be':
return buffer.readUInt32BE(offset * 2) * 0x100000000 + buffer.readUInt32BE(offset * 2 + 4);
case 'uint64le':
return buffer.readUInt32LE(offset * 2) + buffer.readUInt32LE(offset * 2 + 4) * 0x100000000;
case 'int64be':
i1 = buffer.readInt32BE(offset * 2);
i2 = buffer.readUInt32BE(offset * 2 + 4);
if (i1 >= 0) {
return i1 * 0x100000000 + i2; // <<32 does not work
} else {
return i1 * 0x100000000 - i2; // I have no solution for that !
}
break;
case 'int64le':
i2 = buffer.readUInt32LE(offset * 2);
i1 = buffer.readInt32LE(offset * 2 + 4);
if (i1 >= 0) {
return i1 * 0x100000000 + i2; // <<32 does not work
} else {
return i1 * 0x100000000 - i2; // I have no solution for that !
}
break;
case 'floatbe':
return buffer.readFloatBE(offset * 2);
case 'floatle':
return buffer.readFloatLE(offset * 2);
case 'floatsw':
buf = new Buffer(4);
buf[0] = buffer[offset * 2 + 2];
buf[1] = buffer[offset * 2 + 3];
buf[2] = buffer[offset * 2 + 0];
buf[3] = buffer[offset * 2 + 1];
return buf.readFloatBE(0);
case 'floatsb':
buf = new Buffer(4);
buf[0] = buffer[offset * 2 + 1];
buf[1] = buffer[offset * 2 + 0];
buf[2] = buffer[offset * 2 + 3];
buf[3] = buffer[offset * 2 + 2];
return buf.readFloatBE(0);
case 'doublebe':
return buffer.readDoubleBE(offset * 2);
case 'doublele':
return buffer.readDoubleLE(offset * 2);
case 'string':
// find length
let _len = 0;
while (buffer[offset * 2 + _len] && _len < len * 2) {
_len++;
}
return buffer.toString('ascii', offset * 2, offset * 2 + _len);
case 'stringle':
// find length
let __len = 0;
let str = '';
while (__len < len * 2) {
if (buffer[offset * 2 + __len + 1]) {
str += String.fromCharCode(buffer[offset * 2 + __len + 1]);
if (str += String.fromCharCode(buffer[offset * 2 + __len])) {
str += String.fromCharCode(buffer[offset * 2 + __len]);
} else {
break;
}
} else {
break;
}
__len += 2;
}
return str;
default:
throw new Error('Invalid type: ' + type);
return 0;
}
}
function writeValue(type, value, len) {
let a0;
let a1;
let a2;
let buffer;
switch (type) {
case 'uint8be':
buffer = new Buffer(2);
buffer[0] = 0;
buffer.writeUInt8(value & 0xFF, 1);
break;
case 'uint8le':
buffer = new Buffer(2);
buffer[1] = 0;
buffer.writeUInt8(value & 0xFF, 0);
break;
case 'int8be':
buffer = new Buffer(2);
buffer[0] = 0;
buffer.writeInt8(value & 0xFF, 1);
break;
case 'int8le':
buffer = new Buffer(2);
buffer[1] = 0;
buffer.writeInt8(value & 0xFF, 0);
break;
case 'uint16be':
buffer = new Buffer(2);
buffer.writeUInt16BE(value, 0);
break;
case 'uint16le':
buffer = new Buffer(2);
buffer.writeUInt16LE(value, 0);
break;
case 'int16be':
buffer = new Buffer(2);
buffer.writeInt16BE(value, 0);
break;
case 'int16le':
buffer = new Buffer(2);
buffer.writeInt16LE(value, 0);
break;
case 'uint32be':
buffer = new Buffer(4);
buffer.writeUInt32BE(value, 0);
break;
case 'uint32le':
buffer = new Buffer(4);
buffer.writeUInt32LE(value, 0);
break;
case 'uint32sw':
buffer = new Buffer(4);
buffer.writeUInt32BE(value, 0);
a0 = buffer[0];
a1 = buffer[1];
buffer[0] = buffer[2];
buffer[1] = buffer[3];
buffer[2] = a0;
buffer[3] = a1;
break;
case 'uint32sb':
buffer = new Buffer(4);
buffer.writeUInt32BE(value, 0);
a0 = buffer[0];
a2 = buffer[2];
buffer[0] = buffer[1];
buffer[2] = buffer[3];
buffer[1] = a0;
buffer[3] = a2;
break;
case 'int32be':
buffer = new Buffer(4);
buffer.writeInt32BE(value, 0);
break;
case 'int32le':
buffer = new Buffer(4);
buffer.writeInt32LE(value, 0);
break;
case 'int32sw':
buffer = new Buffer(4);
buffer.writeInt32BE(value, 0);
a0 = buffer[0];
a1 = buffer[1];
buffer[0] = buffer[2];
buffer[1] = buffer[3];
buffer[2] = a0;
buffer[3] = a1;
break;
case 'int32sb':
buffer = new Buffer(4);
buffer.writeInt32BE(value, 0);
a0 = buffer[0];
a2 = buffer[2];
buffer[0] = buffer[1];
buffer[2] = buffer[3];
buffer[1] = a0;
buffer[3] = a2;
break;
case 'uint64be':
buffer = new Buffer(8);
buffer.writeUInt32BE(value >> 32, 0);
buffer.writeUInt32BE(value & 0xFFFFFFFF, 4);
break;
case 'uint64le':
buffer = new Buffer(8);
buffer.writeUInt32LE(value & 0xFFFFFFFF, 0);
buffer.writeUInt32LE(value >> 32, 4);
break;
case 'int64be':
buffer = new Buffer(8);
buffer.writeInt32BE(value >> 32, 0);
buffer.writeUInt32BE(value & 0xFFFFFFFF, 4);
break;
case 'int64le':
buffer = new Buffer(8);
buffer.writeUInt32LE(value & 0xFFFFFFFF, 0);
buffer.writeInt32LE(value >> 32, 4);
break;
case 'floatbe':
buffer = new Buffer(4);
buffer.writeFloatBE(value, 0);
break;
case 'floatle':
buffer = new Buffer(4);
buffer.writeFloatLE(value, 0);
break;
case 'floatsw':
buffer = new Buffer(4);
buffer.writeFloatBE(value, 0);
a0 = buffer[0];
a1 = buffer[1];
buffer[0] = buffer[2];
buffer[1] = buffer[3];
buffer[2] = a0;
buffer[3] = a1;
break;
case 'floatsb':
buffer = new Buffer(4);
buffer.writeFloatBE(value, 0);
a0 = buffer[0];
a2 = buffer[2];
buffer[0] = buffer[1];
buffer[2] = buffer[3];
buffer[1] = a0;
buffer[3] = a2;
break;
case 'doublebe':
buffer = new Buffer(8);
buffer.writeDoubleBE(value, 0);
break;
case 'doublele':
buffer = new Buffer(8);
buffer.writeDoubleLE(value, 0);
break;
case 'string':
if (value === null) value = 'null';
value = value.toString();
let _len = (value.length + 1);
if (_len % 2) _len++;
buffer = new Buffer(_len);
buffer.write(value, 0, value.length > _len ? _len : value.length, 'ascii');
break;
case 'stringle':
if (value === null) value = 'null';
value = value.toString();
let __len = (value.length + 1);
if (__len % 2) __len++;
buffer = new Buffer(__len);
for (let b = 0; b < (__len >> 1); b++) {
buffer.writeInt16LE((value.charCodeAt(b * 2) << 8) | value.charCodeAt(b * 2 + 1));
if (b * 2 + 2 >= buffer.length) {
break;
}
}
break;
default:
throw new Error('Invalid type: ' + type);
buffer = new Buffer(2);
break;
}
return buffer;
}
function getJSModbusPath() {
let path = require.resolve('jsmodbus/package.json');
let parts__ = path.replace(/\\/g, '/').split('/');
parts__.pop();
path = parts__.join('/');
return path;
}
module.exports = {
writeValue,
extractValue,
getJSModbusPath
};

206
lib/jsmodbus/README.md Normal file
View File

@ -0,0 +1,206 @@
A simple an easy to use Modbus TCP client/server implementation.
Modbus
========
Modbus is a simple Modbus TCP Client with a simple API.
Installation
------------
Just type `npm install jsmodbus` and you are ready to go.
Testing
-------
The test files are implemented using [mocha](https://github.com/visionmedia/mocha) and sinon.
Simply `npm install -g mocha` and `npm install -g sinon`. To run the tests type from the projects root folder `mocha test/*`.
Please feel free to fork and add your own tests.
TCP Client example
--------------
```javascript
var modbus = require('jsmodbus');
// create a modbus client
var client = modbus.client.tcp.complete({
'host' : host,
'port' : port,
'autoReconnect' : true,
'reconnectTimeout' : 1000,
'timeout' : 5000,
'unitId' : 0
});
client.connect();
// reconnect with client.reconnect()
client.on('connect', function () {
// make some calls
client.readCoils(0, 13).then(function (resp) {
// resp will look like { fc: 1, byteCount: 20, coils: [ values 0 - 13 ], payload: <Buffer> }
console.log(resp);
}).fail(console.log);
client.readDiscreteInputs(0, 13).then(function (resp) {
// resp will look like { fc: 2, byteCount: 20, coils: [ values 0 - 13 ], payload: <Buffer> }
console.log(resp);
}).fail(console.log);
client.readHoldingRegisters(0, 10).then(function (resp) {
// resp will look like { fc: 3, byteCount: 20, register: [ values 0 - 10 ], payload: <Buffer> }
console.log(resp);
}).fail(console.log);
client.readInputRegisters(0, 10).then(function (resp) {
// resp will look like { fc: 4, byteCount: 20, register: [ values 0 - 10 ], payload: <Buffer> }
console.log(resp);
}).fail(console.log);
client.writeSingleCoil(5, true).then(function (resp) {
// resp will look like { fc: 5, byteCount: 4, outputAddress: 5, outputValue: true }
console.log(resp);
}).fail(console.log);
client.writeSingleCoil(5, new Buffer(0x01)).then(function (resp) {
// resp will look like { fc: 5, byteCount: 4, outputAddress: 5, outputValue: true }
console.log(resp);
}).fail(console.log);
client.writeSingleRegister(13, 42).then(function (resp) {
// resp will look like { fc: 6, byteCount: 4, registerAddress: 13, registerValue: 42 }
console.log(resp);
}).fail(console.log);
client.writeSingleRegister(13, new Buffer([0x00 0x2A])).then(function (resp) {
// resp will look like { fc: 6, byteCount: 4, registerAddress: 13, registerValue: 42 }
console.log(resp);
}).fail(console.log);
client.writeMultipleCoils(3, [1, 0, 1, 0, 1, 1]).then(function (resp) {
// resp will look like { fc: 15, startAddress: 3, quantity: 6 }
console.log(resp);
}).fail(console.log);
client.writeMultipleCoils(3, new Buffer([0x2B]), 6).then(function (resp) {
// resp will look like { fc: 15, startAddress: 3, quantity: 6 }
console.log(resp);
}).fail(console.log);
client.writeMultipleRegisters(4, [1, 2, 3, 4]).then(function (resp) {
// resp will look like { fc : 16, startAddress: 4, quantity: 4 }
console.log(resp);
}).fail(console.log);
client.writeMultipleRegisters(4, new Buffer([0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04]).then(function (resp) {
// resp will look like { fc : 16, startAddress: 4, quantity: 4 }
console.log(resp);
}).fail(console.log);
});
client.on('error', function (err) {
console.log(err);
})
```
Server example
--------------
```javascript
var stampit = require('stampit'),
modbus = require('jsmodbus');
var customServer = stampit()
.refs({
'logEnabled' : true,
'port' : 8888,
'responseDelay' : 10, // so we do not fry anything when someone is polling this server
// specify coils, holding and input register here as buffer or leave it for them to be new Buffer(1024)
coils : new Buffer(1024),
holding : new Buffer(1024),
input : new Buffer(1024)
})
.compose(modbus.server.tcp.complete)
.init(function () {
var init = function () {
// get the coils with this.getCoils() [ Buffer(1024) ]
// get the holding register with this.getHolding() [ Buffer(1024) ]
// get the input register with this.getInput() [ Buffer(1024) ]
// listen to requests
this.on('readCoilsRequest', function (start, quantity) {
// do something, this will be executed in sync before the
// read coils request is executed
});
// the write request have pre and post listener
this.on('[pre][post]WriteSingleCoilRequest', function (address, value) {
});
}.bind(this);
init();
});
customServer();
// you can of course always use a standard server like so
var server = modbus.server.tcp.complete({ port : 8888 });
// and interact with the register via the getCoils(), getHolding() and getInput() calls
server.getHolding().writeUInt16BE(123, 1);
````
## License
Copyright (C) 2016 Stefan Poeter (Stefan.Poeter[at]cloud-automation.de)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,56 @@
'use strict';
const Stampit = require('stampit');
const Put = require('put');
module.exports = Stampit()
.init(function () {
const FC = 1;
let init = () => this.addResponseHandler(FC, onResponse);
let onResponse = (unitId, pdu, cb) => {
let fc = pdu.readUInt8(0);
if (fc !== FC) {
cb(`ReadCoils: Invalid FC ${fc}`);
} else {
let byteCount = pdu.readUInt8(1);
// let bitCount = byteCount * 8;
let resp = {
unitId,
fc: fc,
byteCount: byteCount,
payload: pdu.slice(2),
data: []
};
let counter = 0;
for (let i = 0; i < byteCount; i+=1) {
let h = 1, cur = pdu.readUInt8(2 + i);
for (let j = 0; j < 8; j++) {
resp.data[counter] = (cur & h) > 0 ;
h = h << 1;
counter += 1;
}
}
cb && cb(null, resp);
}
};
this.readCoils = (unitId, start, quantity) => {
return new Promise((resolve, reject) => {
let pdu = Put().word8(FC).word16be(start).word16be(quantity).buffer();
this.queueRequest(unitId, FC, pdu, (err, resp) => {
if (err) {
reject(err);
} else {
resolve(resp);
}
});
});
};
init();
});

View File

@ -0,0 +1,62 @@
'use strict';
const Stampit = require('stampit');
const Put = require('put');
module.exports = Stampit()
.init(function () {
const FC = 2;
let init = () => this.addResponseHandler(FC, onResponse);
let onResponse = (unitId, pdu, cb) => {
let fc = pdu.readUInt8(0);
if (fc !== FC) {
cb(`ReadDiscreteInputs: Invalid FC ${fc}`);
} else {
let byteCount = pdu.readUInt8(1);
let counter = 0;
let resp = {
unitId,
fc: fc,
byteCount: byteCount,
payload: pdu.slice(2),
data: []
};
for (let i = 0; i < byteCount; i++) {
let h = 1, cur = pdu.readUInt8(2 + i);
for (let j = 0; j < 8; j+=1) {
resp.data[counter] = (cur & h) > 0 ;
h = h << 1;
counter += 1;
}
}
cb && cb(null, resp);
}
};
this.readDiscreteInputs = (unitId, start, quantity) => {
return new Promise((resolve, reject) => {
if (quantity > 2000) {
return reject('quantity is too big');
}
let pdu = Put().word8be(FC).word16be(start).word16be(quantity).buffer();
this.queueRequest(unitId, FC, pdu, (err, resp) => {
if (err) {
reject(err);
} else {
resolve(resp);
}
});
});
};
init();
});

View File

@ -0,0 +1,52 @@
'use strict';
const Stampit = require('stampit');
const Put = require('put');
module.exports = Stampit()
.init(function () {
const FC = 3;
let init = () => this.addResponseHandler(FC, onResponse);
let onResponse = (unitId, pdu, cb) => {
let fc = pdu.readUInt8(0);
if (fc !== FC) {
cb(`ReadHoldingRegisters: Invalid FC ${fc}`);
} else {
let byteCount = pdu.readUInt8(1);
let resp = {
unitId,
fc: fc,
byteCount: byteCount,
payload: pdu.slice(2),
register: []
};
const registerCount = byteCount / 2;
for (let i = 0; i < registerCount; i++) {
resp.register.push(pdu.readUInt16BE(2 + (i * 2)));
}
cb && cb(null, resp);
}
};
this.readHoldingRegisters = (unitId, start, quantity) => {
return new Promise((resolve, reject) => {
let pdu = Put().word8be(FC).word16be(start).word16be(quantity).buffer();
this.queueRequest(unitId, FC, pdu, (err, resp) => {
if (err) {
reject(err);
} else {
resolve(resp);
}
});
});
};
init();
});

View File

@ -0,0 +1,52 @@
'use strict';
const Stampit = require('stampit');
const Put = require('put');
module.exports = Stampit()
.init(function () {
const FC = 4;
let init = () => this.addResponseHandler(FC, onResponse);
let onResponse = (unitId, pdu, cb) => {
let fc = pdu.readUInt8(0);
if (fc !== FC) {
cb(`ReadInputRegisters: Invalid FC ${fc}`);
} else {
let byteCount = pdu.readUInt8(1);
let resp = {
unitId,
fc: fc,
byteCount: byteCount,
payload: pdu.slice(2),
register: []
};
const registerCount = byteCount / 2;
for (let i = 0; i < registerCount; i++) {
resp.register.push(pdu.readUInt16BE(2 + (i * 2)));
}
cb && cb(null, resp);
}
};
this.readInputRegisters = (unitId, start, quantity) => {
return new Promise((resolve, reject) => {
let pdu = Put().word8be(FC).word16be(start).word16be(quantity).buffer();
this.queueRequest(unitId, FC, pdu, (err, resp) => {
if (err) {
reject(err);
} else {
resolve(resp);
}
});
});
};
init();
});

View File

@ -0,0 +1,73 @@
'use strict';
const Stampit = require('stampit');
const Put = require('put');
module.exports = Stampit()
.init(function () {
const FC = 15;
let init = () => this.addResponseHandler(FC, onResponse);
let onResponse = (unitId, pdu, cb) => {
const fc = pdu.readUInt8(0);
const startAddress = pdu.readUInt16BE(1);
const quantity = pdu.readUInt16BE(3);
let resp = {
unitId: unitId,
fc: fc,
startAddress: startAddress,
quantity: quantity
};
if (fc !== FC) {
cb(`WriteMultipleCoils: Invalid FC ${fc}`);
} else {
cb(null, resp);
}
};
this.writeMultipleCoils = (unitId, startAddress, data, N) => {
return new Promise((resolve, reject) => {
let pdu = Put().word8(FC).word16be(startAddress);
if (data instanceof Buffer) {
pdu.word16be(N).word8(data.length).put(data);
} else if (data instanceof Array) {
if (data.length > 1968) {
reject('Length is too big');
return;
}
const byteCount = Math.ceil(data.length / 8);
let curByte = 0;
let cntr = 0;
pdu.word16be(data.length).word8(byteCount);
for (let i = 0; i < data.length; i += 1) {
curByte += data[i] ? Math.pow(2, cntr) : 0;
cntr = (cntr + 1) % 8;
if (cntr === 0 || i === coils.length - 1 ) {
pdu.word8(curByte);
curByte = 0;
}
}
}
this.queueRequest(unitId, FC, pdu.buffer(), (err, resp) => {
if (err) {
reject(err);
} else {
resolve(resp);
}
});
});
};
init();
});

View File

@ -0,0 +1,71 @@
'use strict';
const Stampit = require('stampit');
const Put = require('put');
module.exports = Stampit()
.init(function () {
const FC = 16;
let init = () => this.addResponseHandler(FC, onResponse);
let onResponse = (unitId, pdu, cb) => {
const fc = pdu.readUInt8(0);
if (fc !== FC) {
cb(`WriteMultipleRegisters: Invalid FC ${fc}`);
} else {
const startAddress = pdu.readUInt16BE(1);
const quantity = pdu.readUInt16BE(3);
let resp = {
unitId: unitId,
fc: fc,
startAddress: startAddress,
quantity: quantity
};
cb(null, resp);
}
};
this.writeMultipleRegisters = (unitId, startAddress, data) => {
return new Promise((resolve, reject) => {
let pdu = Put().word8(FC).word16be(startAddress);
if (data instanceof Buffer) {
if (data.length / 2 > 0x007b) {
reject('Length is too big');
return;
}
pdu.word16be(data.length / 2).word8(data.length).put(data);
} else if (data instanceof Array) {
if (data.length > 0x007b) {
reject('Length is too big');
return;
}
let byteCount = Math.ceil(data.length * 2);
pdu.word16be(data.length).word8(byteCount);
for (let i = 0; i < data.length; i += 1) {
pdu.word16be(data[i]);
}
} else {
reject('Invalid data');
return;
}
this.queueRequest(unitId, FC, pdu.buffer(), (err, resp) => {
if (err) {
reject(err);
} else {
resolve(resp);
}
});
});
};
init();
});

View File

@ -0,0 +1,47 @@
'use strict';
const Stampit = require('stampit');
const Put = require('put');
module.exports = Stampit()
.init(function () {
const FC = 5;
let init = () => this.addResponseHandler(FC, onResponse);
let onResponse = (unitId, pdu, cb) => {
const fc = pdu.readUInt8(0);
const outputAddress = pdu.readUInt16BE(1);
const outputValue = pdu.readUInt16BE(3);
let resp = {
unitId: unitId,
fc: fc,
outputAddress: outputAddress,
outputValue: outputValue === 0x0000 ? false : (outputValue === 0xFF00 ? true : undefined)
};
if (fc !== FC) {
cb(`WriteSingleCoil: Invalid FC ${fc}`);
} else {
cb(null, resp);
}
};
this.writeSingleCoil = (unitId, address, value) => {
return new Promise((resolve, reject) => {
const payload = (value instanceof Buffer) ? (value.readUInt8(0) > 0) : value;
const pdu = Put().word8be(FC).word16be(address).word16be(payload ? 0xff00 : 0x0000);
this.queueRequest(unitId, FC, pdu.buffer(), (err, resp) => {
if (err) {
reject(err);
} else {
resolve(resp);
}
});
});
};
init();
});

View File

@ -0,0 +1,50 @@
'use strict';
const Stampit = require('stampit');
const Put = require('put');
module.exports = Stampit()
.init(function () {
const FC = 6;
let init = () => this.addResponseHandler(FC, onResponse);
let onResponse = (unitId, pdu, cb) => {
const fc = pdu.readUInt8(0);
const registerAddress = pdu.readUInt16BE(1);
const registerValue = pdu.readUInt16BE(3);
let resp = {
unitId: unitId,
fc: fc,
registerAddress: registerAddress,
registerValue: registerValue,
registerAddressRaw: pdu.slice(1,2),
registerValueRaw: pdu.slice(3,2)
};
if (fc !== FC) {
cb(`WriteSingleRegister: Invalid FC ${fc}`);
} else {
cb(null, resp);
}
};
this.writeSingleRegister = (unitId, address, value) => {
return new Promise((resolve, reject) => {
const payload = (value instanceof Buffer) ? value : Put().word16be(value).buffer();
const pdu = Put().word8be(FC).word16be(address).put(payload);
this.queueRequest(unitId, FC, pdu.buffer(), (err, resp) => {
if (err) {
reject(err);
} else {
resolve(resp);
}
});
});
};
init();
});

View File

@ -0,0 +1,52 @@
'use strict';
const Stampit = require('stampit');
const Put = require('put');
module.exports = Stampit()
.init(function () {
const FC = 1;
let init = () => {
this.log.debug('initiating read coils request handler.');
this.responseDelay = this.responseDelay || 0;
this.setRequestHandler(FC, onRequest);
};
let _onRequest = (pdu, cb) => {
this.log.debug('handling read coils request.');
if (pdu.length !== 5) {
cb(Put().word8(0x81).word8(0x02).buffer());
} else {
const fc = pdu.readUInt8(0);
const address = pdu.readUInt16BE(1);
const byteAddress = address * 2;
const value = pdu.readUInt16BE(3);
this.emit('preWriteSingleRegisterRequest', byteAddress, value);
let mem = this.getHolding();
if (byteAddress + 2 > mem.length) {
cb(Put().word8(0x86).word8(0x02).buffer());
} else {
let response = Put().word8(0x06).word16be(address).word16be(value).buffer();
mem.writeUInt16BE(value, byteAddress);
this.emit('postWriteSingleRegisterRequest', byteAddress, value);
cb(response);
}
}
};
let onRequest = (pdu, cb) => {
if (this.responseDelay) {
setTimeout(_onRequest, this.responseDelay, pdu, cb);
} else {
setImmediate(_onRequest, pdu, cb);
}
};
init();
});

View File

@ -0,0 +1,69 @@
'use strict';
const Stampit = require('stampit');
const Put = require('put');
module.exports = Stampit()
.init(function () {
const FC = 2;
let init = () => {
this.log.debug('initiating read discrete inputs request handler.');
this.responseDelay = this.responseDelay || 0;
this.setRequestHandler(FC, onRequest);
};
let _onRequest = (pdu, cb) => {
this.log.debug('handling read discrete inputs request.');
if (pdu.length !== 5) {
cb(Put().word8(0x82).word8(0x02).buffer());
} else {
const fc = pdu.readUInt8(0);
const start = pdu.readUInt16BE(1);
const quantity = pdu.readUInt16BE( 3);
this.emit('readDiscreteInputsRequest', start, quantity);
let mem = this.getDiscrete();
if (!quantity || start + quantity > mem.length * 8) {
cb(Put().word8(0x82).word8(0x02).buffer());
} else {
let val = 0;
let thisByteBitCount = 0;
let response = Put().word8(0x02).word8(Math.floor(quantity / 8) + (quantity % 8 === 0 ? 0 : 1));
for (let totalBitCount = start; totalBitCount < start + quantity; totalBitCount += 1) {
let buf = mem.readUInt8(Math.floor(totalBitCount / 8));
let mask = 1 << (totalBitCount % 8);
if (buf & mask) {
val += 1 << (thisByteBitCount % 8)
}
thisByteBitCount += 1;
if (thisByteBitCount % 8 === 0 || totalBitCount === (start + quantity) - 1) {
response.word8(val);
val = 0;
}
}
cb(response.buffer());
}
}
};
let onRequest = (pdu, cb) => {
if (this.responseDelay) {
setTimeout(_onRequest, this.responseDelay, pdu, cb);
} else {
setImmediate(_onRequest, pdu, cb);
}
};
init();
});

View File

@ -0,0 +1,55 @@
'use strict';
const Stampit = require('stampit');
const Put = require('put');
module.exports = Stampit()
.init(function () {
const FC = 3;
let init = () => {
this.log.debug('initiating read holding registers request handler.');
this.responseDelay = this.responseDelay || 0;
this.setRequestHandler(FC, onRequest);
};
let _onRequest = (pdu, cb) => {
if (pdu.length !== 5) {
this.log.warn('wrong pdu length.');
cb(Put().word8(0x83).word8(0x02).buffer());
} else {
const fc = pdu.readUInt8(0);
const start = pdu.readUInt16BE(1);
const byteStart = start * 2;
const quantity = pdu.readUInt16BE(3);
this.emit('readHoldingRegistersRequest', byteStart, quantity);
let mem = this.getHolding();
if (!quantity || byteStart + (quantity * 2) > mem.length) {
this.log.debug('request outside register boundaries.');
cb(Put().word8(0x83).word8(0x02).buffer());
return;
}
let response = Put().word8(0x03).word8(quantity * 2);
for (let i = byteStart; i < byteStart + (quantity * 2); i += 2) {
response.word16be(mem.readUInt16BE(i));
}
this.log.debug('finished read holding register request.');
cb(response.buffer());
}
};
let onRequest = (pdu, cb) => {
if (this.responseDelay) {
setTimeout(_onRequest, this.responseDelay, pdu, cb);
} else {
setImmediate(_onRequest, pdu, cb);
}
};
init();
});

View File

@ -0,0 +1,52 @@
'use strict';
const Stampit = require('stampit');
const Put = require('put');
module.exports = Stampit()
.init(function () {
const FC = 4;
let init = () => {
this.log.debug('initiating read input registers request handler.');
this.responseDelay = this.responseDelay || 0;
this.setRequestHandler(FC, onRequest);
};
let _onRequest = (pdu, cb) => {
this.log.debug('handling read input registers request.');
if (pdu.length !== 5) {
cb(Put().word8(0x84).word8(0x02).buffer());
} else {
const fc = pdu.readUInt8(0);
const start = pdu.readUInt16BE(1);
const byteStart = start * 2;
const quantity = pdu.readUInt16BE(3);
this.emit('readInputRegistersRequest', byteStart, quantity);
let mem = this.getInput();
if (!quantity || byteStart + (quantity * 2) > mem.length) {
cb(Put().word8(0x84).word8(0x02).buffer());
} else {
let response = Put().word8(0x04).word8(quantity * 2);
for (let i = byteStart; i < byteStart + (quantity * 2); i += 2) {
response.word16be(mem.readUInt16BE(i));
}
cb(response.buffer());
}
}
};
let onRequest = (pdu, cb) => {
if (this.responseDelay) {
setTimeout(_onRequest, this.responseDelay, pdu, cb);
} else {
setImmediate(_onRequest, pdu, cb);
}
};
init();
});

View File

@ -0,0 +1,77 @@
'use strict';
const Stampit = require('stampit');
const Put = require('put');
module.exports = Stampit()
.init(function () {
const FC = 15;
let init = () => {
this.log.debug('initiating write multiple coils request handler.');
this.responseDelay = this.responseDelay || 0;
this.setRequestHandler(FC, onRequest);
};
let _onRequest = (pdu, cb) => {
this.log.debug('handling write multiple coils request.');
if (pdu.length < 3) {
cb(Put().word8(0x8F).word8(0x02).buffer());
} else {
// const fc = pdu.readUInt8(0);
const start = pdu.readUInt16BE(1);
const quantity = pdu.readUInt16BE(3);
const byteCount = pdu.readUInt8(5);
this.emit('preWriteMultipleCoilsRequest', start, quantity, byteCount);
let mem = this.getCoils();
// error response
if (!quantity || start + quantity > mem.length * 8) {
cb(Put().word8(0x8F).word8(0x02).buffer());
} else {
let response = Put().word8(0x0F).word16be(start).word16be(quantity).buffer();
let oldValue;
let newValue, current = pdu.readUInt8(6);
let j = 0;
for (let i = start; i < start + quantity; i += 1) {
// reading old value from the coils register
oldValue = mem.readUInt8(Math.floor(i / 8));
// apply new value
if ((Math.pow(2, j % 8) & current)) {
newValue = oldValue | Math.pow(2, i % 8);
} else {
newValue = oldValue & ~Math.pow(2, i % 8);
}
// write to buffer
mem.writeUInt8(newValue, Math.floor(i / 8));
// read new value from request pdu
j += 1;
if (j % 8 === 0 && j < quantity) {
current = pdu.readUInt8(6 + Math.floor(j / 8));
}
}
this.emit('postWriteMultipleCoilsRequest', start, quantity, byteCount);
cb(response);
}
}
};
let onRequest = (pdu, cb) => {
if (this.responseDelay) {
setTimeout(_onRequest, this.responseDelay, pdu, cb);
} else {
setImmediate(_onRequest, pdu, cb);
}
};
init();
});

View File

@ -0,0 +1,60 @@
'use strict';
const Stampit = require('stampit');
const Put = require('put');
module.exports = Stampit()
.init(function () {
const FC = 16;
let init = () => {
this.log.debug('initiating write multiple registers request handler.');
this.responseDelay = this.responseDelay || 0;
this.setRequestHandler(FC, onRequest);
};
let _onRequest = (pdu, cb) => {
if (pdu.length < 3) {
cb(Put().word8(0x90).word8(0x02).buffer());
} else {
// const fc = pdu.readUInt8(0);
const start = pdu.readUInt16BE(1);
const byteStart = start * 2;
const quantity = pdu.readUInt16BE(3);
const byteCount = pdu.readUInt8(5);
if (quantity > 0x007b) {
cb(Put().word8(0x90).word8(0x03).buffer());
} else {
this.emit('preWriteMultipleRegistersRequest', byteStart, quantity, byteCount);
let mem = this.getHolding();
if (!quantity || byteStart + (quantity * 2) > mem.length) {
cb(Put().word8(0x90).word8(0x02).buffer());
} else {
let response = Put().word8(0x10).word16be(start).word16be(quantity).buffer();
let j = 0;
for (let i = byteStart; i < byteStart + byteCount; i += 1) {
mem.writeUInt8(pdu.readUInt8(6 + j), i);
j++;
}
this.emit('postWriteMultipleRegistersRequest', byteStart, quantity, byteCount);
cb(response);
}
}
}
};
let onRequest = (pdu, cb) => {
if (this.responseDelay) {
setTimeout(_onRequest, this.responseDelay, pdu, cb);
} else {
setImmediate(_onRequest, pdu, cb);
}
};
init();
});

View File

@ -0,0 +1,62 @@
'use strict';
const Stampit = require('stampit');
const Put = require('put');
module.exports = Stampit()
.init(function () {
const FC = 5;
let init = () => {
this.log.debug('initiating write single coil request handler.');
this.responseDelay = this.responseDelay || 0;
this.setRequestHandler(FC, onRequest);
};
let _onRequest = (pdu, cb) => {
if (pdu.length !== 5) {
cb(Put().word8(0x85).word8(0x02).buffer());
} else {
// const fc = pdu.readUInt8(0);
const address = pdu.readUInt16BE(1);
const value = (pdu.readUInt16BE(3) === 0x0000);
if (pdu.readUInt16BE(3) !== 0x0000 && pdu.readUInt16BE(3) !== 0xFF00) {
cb(Put().word8(0x85).word8(0x03).buffer());
} else {
this.emit('preWriteSingleCoilRequest', address, value);
let mem = this.getCoils();
if (address + 1 > mem.length * 8) {
cb(Put().word8(0x85).word8(0x02).buffer());
} else {
let response = Put().word8(0x05).word16be(address).word16be(value ? 0xFF00 : 0x0000);
let oldValue = mem.readUInt8(Math.floor(address / 8));
let newValue;
if (value) {
newValue = oldValue | Math.pow(2, address % 8);
} else {
newValue = oldValue & ~Math.pow(2, address % 8);
}
mem.writeUInt8(newValue, Math.floor(address / 8));
this.emit('postWriteSingleCoilRequest', address, value);
cb(response.buffer());
}
}
}
};
let onRequest = (pdu, cb) => {
if (this.responseDelay) {
setTimeout(_onRequest, this.responseDelay, pdu, cb);
} else {
setImmediate(_onRequest, pdu, cb);
}
};
init();
});

View File

@ -0,0 +1,50 @@
'use strict';
const Stampit = require('stampit');
const Put = require('put');
module.exports = Stampit()
.init(function () {
const FC = 6;
let init = () => {
this.log.debug('initiating write single register request handler.');
this.responseDelay = this.responseDelay || 0;
this.setRequestHandler(FC, onRequest);
};
let _onRequest = (pdu, cb) => {
this.log.debug('handling write single register request.');
if (pdu.length !== 5) {
cb(Put().word8(0x86).word8(0x02).buffer());
} else {
// const fc = pdu.readUInt8(0);
const address = pdu.readUInt16BE(1);
const byteAddress = address * 2;
const value = pdu.readUInt16BE(3);
this.emit('preWriteSingleRegisterRequest', byteAddress, value);
let mem = this.getHolding();
if (byteAddress + 2 > mem.length) {
cb(Put().word8(0x86).word8(0x02).buffer());
} else {
let response = Put().word8(0x06).word16be(address).word16be(value).buffer();
mem.writeUInt16BE(value, byteAddress);
this.emit('postWriteSingleRegisterRequest', byteAddress, value);
cb(response);
}
}
};
let onRequest = (pdu, cb) => {
if (this.responseDelay) {
setTimeout(_onRequest, this.responseDelay, pdu, cb);
} else {
setImmediate(_onRequest, pdu, cb);
}
};
init();
});

17
lib/jsmodbus/index.js Normal file
View File

@ -0,0 +1,17 @@
'use strict';
const fs = require('fs');
function Modbus(type, transport) {
let core = require(__dirname + `/transports/modbus-${type}-${transport || 'tcp'}.js`);
fs.readdirSync(__dirname + `/handler/${type}`)
.filter(file => file.substr(-3) === '.js').forEach(file => {
const handler = require(__dirname + `/handler/${type}/${file}`);
core = core.compose(handler);
//core.handler[file.substr(0, file.length - 3)] = handler;
});
return core;
}
module.exports = Modbus;

View File

@ -0,0 +1,145 @@
'use strict';
const Stampit = require('stampit');
const StateMachine = require('stampit-state-machine');
const EventBus = require('stampit-event-bus');
const ExceptionMessage = {
0x01: 'ILLEGAL FUNCTION',
0x02: 'ILLEGAL DATA ADDRESS',
0x03: 'ILLEGAL DATA VALUE',
0x04: 'SLAVE DEVICE FAILURE',
0x05: 'ACKNOWLEDGE',
0x06: 'SLAVE DEVICE BUSY',
0x08: 'MEMORY PARITY ERROR',
0x0A: 'GATEWAY PATH UNAVAILABLE',
0x0B: 'GATEWAY TARGET DEVICE FAILED TO RESPOND'
};
module.exports = Stampit()
.compose(StateMachine, EventBus)
.init(function () {
this.log = this.options && this.options.log;
if (!this.log) {
this.log = {
log: () => console.log.apply(console, arguments),
error: () => console.error.apply(console, arguments),
warn: () => console.warn.apply(console, arguments),
debug: () => console.log.apply(console, arguments)
};
}
let responseHandler = {};
let currentRequest = null;
let reqFifo = [];
let init = () => {
this.options.timeout = this.options.timeout || (5 * 1000); // 5s
this.on('data', onData);
this.on('newState_ready', flush);
this.on('newState_closed', onClosed);
};
let flush = () => {
if (reqFifo.length) {
currentRequest = reqFifo.shift();
currentRequest.timeout = setTimeout(() => {
currentRequest.cb && currentRequest.cb({err: 'timeout'});
this.emit('trashCurrentRequest');
this.log.error('Request timed out.');
this.setState('error');
}, this.options.timeout);
this.setState('waiting');
this.emit('send', currentRequest.pdu, currentRequest.unitId);
}
};
let onClosed = () => {
if (currentRequest) {
this.log.debug('Clearing timeout of the current request.');
clearTimeout(currentRequest.timeout);
}
this.log.debug('Cleaning up request fifo.');
reqFifo = [];
};
let handleErrorPDU = (pdu) => {
const errorCode = pdu.readUInt8(0);
// if error code is smaller than 0x80
// ths pdu describes no error
if (errorCode < 0x80) {
return false;
}
// pdu describes an error
const exceptionCode = pdu.readUInt8(1);
const message = ExceptionMessage[exceptionCode];
const err = {
errorCode: errorCode,
exceptionCode: exceptionCode,
message: message
};
// call the desired deferred
currentRequest.cb && currentRequest.cb(err);
return true;
};
/**
* Handle the incoming data, cut out the mbap
* packet and send the pdu to the listener
*/
let onData = (pdu, unitId) => {
if (!currentRequest) {
this.log.debug('No current request.');
return;
}
clearTimeout(currentRequest.timeout);
// check pdu for error
if (handleErrorPDU(pdu)) {
this.log.debug('Received pdu describes an error.');
currentRequest = null;
this.setState('ready');
return;
}
// handle pdu
const handler = responseHandler[currentRequest.fc];
if (!handler) {
this.log.debug(`Found no handler for fc ${currentRequest.fc}`);
throw new Error(`No handler implemented for fc ${currentRequest.fc}`);
}
handler(unitId, pdu, currentRequest.cb);
this.setState('ready');
};
this.addResponseHandler = (fc, handler) => {
responseHandler[fc] = handler;
return this;
};
this.queueRequest = (unitId, fc, pdu, cb) => {
reqFifo.push({unitId, fc, pdu, cb});
if (this.inState('ready')) {
flush();
}
};
init();
});

View File

@ -0,0 +1,91 @@
'use strict';
const Stampit = require('stampit');
const Put = require('put');
const EventBus = require('stampit-event-bus');
const core = Stampit()
.compose(EventBus)
.init(function () {
this.log = this.options && this.options.log;
if (!this.log) {
this.log = {
log: () => console.log.apply(console, arguments),
error: () => console.error.apply(console, arguments),
warn: () => console.warn.apply(console, arguments),
debug: () => console.log.apply(console, arguments)
};
}
let data = {
coils: null,
holding: null,
input: null,
discrete: null,
};
let handler = {};
let init = () => {
if (!this.coils) {
data.coils = new Buffer(1024);
} else {
data.coils = this.coils;
}
if (!this.holding) {
data.holding = new Buffer(1024);
} else {
data.holding = this.holding;
}
if (!this.input) {
data.input = new Buffer(1024);
} else {
data.input = this.input;
}
if (!this.discrete) {
data.discrete = new Buffer(1024);
} else {
data.discrete = this.discrete;
}
};
this.onData = (pdu, callback) => {
// get fc and byteCount in advance
const fc = pdu.readUInt8(0);
// const byteCount = pdu.readUInt8(1);
// get the pdu handler
const reqHandler = handler[fc];
if (!reqHandler) {
// write a error/exception pkt to the
// socket with error code fc + 0x80 and
// exception code 0x01 (Illegal Function)
this.log.debug('no handler for fc', fc);
callback(Put().word8(fc + 0x80).word8(0x01).buffer());
} else {
reqHandler(pdu, response => {
callback(response);
});
}
};
this.setRequestHandler = (fc, callback) => {
this.log.debug('setting request handler', fc);
handler[fc] = callback;
return this;
};
this.getCoils = () => data.coils;
this.getInput = () => data.input;
this.getHolding = () => data.holding;
this.getDiscrete = () => data.discrete;
init();
});
module.exports = core;

52
lib/jsmodbus/package.json Normal file
View File

@ -0,0 +1,52 @@
{
"author": {
"name": "Stefan Poeter",
"email": "stefan.poeter@cloud-automation.de"
},
"bugs": {
"url": "https://github.com/Cloud-Automation/node-modbus/issues"
},
"dependencies": {
"crc": "3.4.0",
"put": "0.0.6",
"q": "1.0.1",
"serialport": "^4.0.1",
"stampit": "^2.1.2",
"stampit-event-bus": "^0.1.1",
"stampit-log": "^0.3.0",
"stampit-state-machine": "^0.2.1"
},
"deprecated": false,
"description": "Implementation for the Serial/TCP Modbus protocol.",
"devDependencies": {
"mocha": "^3.0.2",
"sinon": "1.5.0"
},
"directories": {
"test": "test",
"example": "examples"
},
"engine": {
"node": ">=0.6"
},
"homepage": "https://github.com/Cloud-Automation/node-modbus#readme",
"keywords": [
"client",
"server",
"serial",
"port",
"modbus",
"tcp"
],
"license": "MIT",
"main": "index.js",
"name": "jsmodbus",
"repository": {
"type": "git",
"url": "git+https://github.com/Cloud-Automation/node-modbus.git"
},
"scripts": {
"test": "mocha test/*"
},
"version": "1.2.4"
}

View File

@ -0,0 +1,148 @@
'use strict';
const stampit = require('stampit');
const crc = require('crc');
const Put = require('put');
const ModbusCore = require('../modbus-client-core.js');
module.exports = stampit()
.compose(ModbusCore)
.init(function () {
const SerialPort = require('serialport');//.SerialPort,
let serialport;
let buffer = new Buffer(0);
let init = () => {
this.setState('init');
let serial = this.options.serial;
if (!serial.portName) {
throw new Error('No portname.');
}
serial.baudRate = serial.baudRate || 9600; // the most are working with 9600
serial.dataBits = serial.dataBits || 8;
serial.stopBits = serial.stopBits || 1;
serial.parity = serial.parity || 'none';
// TODO: flowControl - ['xon', 'xoff', 'xany', 'rtscts']
// TODO: settings - ['brk', 'cts', 'dtr', 'dts', 'rts']
this.log.debug('connect to serial ' + serial.portName + ' with ' + serial.baudRate);
serialport = new SerialPort(serial.portName, {
baudRate: serial.baudRate,
parity: serial.parity,
dataBits: serial.dataBits,
stopBits: serial.stopBits
});
serialport.on('open', onOpen);
serialport.on('close', onClose);
serialport.on('data', onData);
serialport.on('error', onError);
this.on('send', onSend);
};
let onOpen = () => {
this.emit('connect');
this.setState('ready');
};
let onClose = () => this.setState('closed');
function toStrArray(buf) {
if (!buf || !buf.length) return '';
let text = '';
for (let i = 0; i < buf.length; i++) {
text += (text ? ',' : '') + buf[i];
}
return text;
}
let onData = data => {
buffer = Buffer.concat([buffer, data]);
while (buffer.length > 4) {
// 1. there is no mbap
// 2. extract pdu
// 0 - device ID
// 1 - Function CODE
// 2 - Bytes length
// 3.. Data
// checksum.(2 bytes
let len;
let pdu;
// if response for write
if (buffer[1] === 5 || buffer[1] === 6 || buffer[1] === 15 || buffer[1] === 16) {
if (buffer.length < 8) {
break;
}
pdu = buffer.slice(0, 8); // 1 byte device ID + 1 byte FC + 2 bytes address + 2 bytes value + 2 bytes CRC
} else if (buffer[1] > 0 && buffer[1] < 5){
len = buffer[2];
if (buffer.length < len + 5) {
break;
}
pdu = buffer.slice(0, len + 5); // 1 byte deviceID + 1 byte FC + 1 byte length + 2 bytes CRC
} else {
// unknown function code
this.log.error('unknown function code: ' + buffer[1]);
// reset buffer and try again
buffer = new Buffer(0);
break;
}
if (crc.crc16modbus(pdu) === 0) { /* PDU is valid if CRC across whole PDU equals 0, else ignore and do nothing */
if (this.options.unitId !== undefined && pdu[0] !== this.options.unitId) {
// answer for wrong device
this.log.debug('received answer for wrong ID ' + buffer[0] + ', expected ' + this.options.unitId);
}
// emit data event and let the
// listener handle the pdu
this.emit('data', pdu.slice(1, pdu.length - 2), pdu[0]);
} else {
this.log.error('Wrong CRC for frame: ' + toStrArray(pdu));
// reset buffer and try again
buffer = new Buffer(0);
break;
}
buffer = buffer.slice(pdu.length, buffer.length);
}
};
let onError = err => this.emit('error', err);
let onSend = (pdu, unitId) => {
let pkt = Put().word8((unitId === undefined ? this.options.unitId : unitId) || 0).put(pdu);
let buf = pkt.buffer();
let crc16 = crc.crc16modbus(buf);
pkt = pkt.word16le(crc16).buffer();
if (!serialport) {
init();
}
serialport.write(pkt, err => err && this.emit('error', err));
};
this.connect = () => {
if (!serialport) {
init();
}
};
this.close = () => {
if (serialport) {
serialport.close();
serialport = null;
}
};
init();
});

View File

@ -0,0 +1,175 @@
'use strict';
const stampit = require('stampit');
const Put = require('put');
const crc = require('crc');
const Net = require('net');
const ModbusCore = require('../modbus-client-core.js');
module.exports = stampit()
.compose(ModbusCore)
.init(function () {
let closedOnPurpose = false;
let reconnect = false;
let buffer = new Buffer(0);
let socket;
let init = () => {
this.setState('init');
let tcp = this.options.tcp;
tcp.protocolVersion = tcp.protocolVersion || 0;
tcp.port = tcp.port || 502;
tcp.host = tcp.host || 'localhost';
tcp.autoReconnect = tcp.autoReconnect || false;
tcp.reconnectTimeout = tcp.reconnectTimeout || 0;
this.on('send', onSend);
this.on('newState_error', onError);
//this.on('stateChanged', this.log.debug);
};
let connect = () => {
this.setState('connect');
if (!socket) {
socket = new Net.Socket();
socket.on('connect', onSocketConnect);
socket.on('close', onSocketClose);
socket.on('error', onSocketError);
socket.on('data', onSocketData);
}
socket.connect(this.options.tcp.port, this.options.tcp.host);
};
let onSocketConnect = () => {
this.emit('connect');
this.setState('ready');
};
let onSocketClose = hadErrors => {
this.log.debug('Socket closed with error', hadErrors);
this.setState('closed');
this.emit('close');
if (!closedOnPurpose && (this.options.tcp.autoReconnect || reconnect)) {
setTimeout(() => {
reconnect = false;
connect();
}, this.options.tcp.reconnectTimeout);
}
};
let onSocketError = err => {
this.log.error('Socket Error', err);
this.setState('error');
this.emit('error', err);
};
function toStrArray(buf) {
if (!buf || !buf.length) return '';
let text = '';
for (let i = 0; i < buf.length; i++) {
text += (text ? ',' : '') + buf[i];
}
return text;
}
let onSocketData = data => {
buffer = Buffer.concat([buffer, data]);
while (buffer.length > 4) {
// 1. there is no mbap
// 2. extract pdu
// 0 - device ID
// 1 - Function CODE
// 2 - Bytes length
// 3.. Data
// checksum.(2 bytes
let len;
let pdu;
// if response for write
if (buffer[1] === 5 || buffer[1] === 6 || buffer[1] === 15 || buffer[1] === 16) {
if (buffer.length < 8) break;
pdu = buffer.slice(0, 8); // 1 byte device ID + 1 byte FC + 2 bytes address + 2 bytes value + 2 bytes CRC
} else if (buffer[1] > 0 && buffer[1] < 5){
len = buffer[2];
if (buffer.length < len + 5) break;
pdu = buffer.slice(0, len + 5); // 1 byte deviceID + 1 byte FC + 1 byte length + 2 bytes CRC
} else {
// unknown function code
this.log.error('unknown function code: ' + buffer[1]);
// reset buffer and try again
buffer = new Buffer(0);
break;
}
if (crc.crc16modbus(pdu) === 0) { /* PDU is valid if CRC across whole PDU equals 0, else ignore and do nothing */
if (this.options.unitId !== undefined && pdu[0] !== this.options.unitId) {
// answer for wrong device
this.log.debug('received answer for wrong ID ' + buffer[0] + ', expected ' + this.options.unitId);
}
// emit data event and let the
// listener handle the pdu
this.emit('data', pdu.slice(1, pdu.length - 2), pdu[0]);
} else {
this.log.error('Wrong CRC for frame: ' + toStrArray(pdu));
// reset buffer and try again
buffer = new Buffer(0);
break;
}
buffer = buffer.slice(pdu.length, buffer.length);
}
};
let onError = () => {
this.log.error('Client in error state.');
socket.destroy();
};
let onSend = (pdu, unitId) => {
this.log.debug('Sending pdu to the socket.');
let pkt = Put()
.word8((unitId === undefined ? this.options.unitId : unitId) || 0) // unit id
.put(pdu); // the actual pdu
let buf = pkt.buffer();
let crc16 = crc.crc16modbus(buf);
pkt = pkt.word16le(crc16).buffer();
socket.write(pkt);
};
this.connect = () => {
this.setState('connect');
connect();
return this;
};
this.reconnect = () => {
if (!this.inState('closed')) {
return this;
}
closedOnPurpose = false;
reconnect = true;
this.log.debug('Reconnecting client.');
socket.end();
return this;
};
this.close = () => {
closedOnPurpose = true;
this.log.debug('Closing client on purpose.');
socket.end();
return this;
};
init();
});

View File

@ -0,0 +1,156 @@
'use strict';
const stampit = require('stampit');
const Put = require('put');
const Net = require('net');
const ModbusCore = require('../modbus-client-core.js');
module.exports = stampit()
.compose(ModbusCore)
.init(function () {
let reqId = 0;
let currentRequestId = reqId;
let closedOnPurpose = false;
let reconnect = false;
let buffer = new Buffer(0);
let trashRequestId;
let socket;
let init = () => {
this.setState('init');
let tcp = this.options.tcp;
tcp.protocolVersion = tcp.protocolVersion || 0;
tcp.port = tcp.port || 502;
tcp.host = tcp.host || 'localhost';
tcp.autoReconnect = tcp.autoReconnect || false;
tcp.reconnectTimeout = tcp.reconnectTimeout || 0;
this.on('send', onSend);
this.on('newState_error', onError);
this.on('trashCurrentRequest', onTrashCurrentRequest);
//this.on('stateChanged', this.log.debug);
};
let connect = () => {
this.setState('connect');
if (!socket) {
socket = new Net.Socket();
socket.on('connect', onSocketConnect);
socket.on('close', onSocketClose);
socket.on('error', onSocketError);
socket.on('data', onSocketData);
}
socket.connect(this.options.tcp.port, this.options.tcp.host);
};
let onSocketConnect = () => {
this.emit('connect');
this.setState('ready');
};
let onSocketClose = hadErrors => {
this.log.debug('Socket closed with error', hadErrors);
this.setState('closed');
this.emit('close');
if (!closedOnPurpose && (this.options.tcp.autoReconnect || reconnect)) {
setTimeout(() => {
reconnect = false;
connect();
}, this.options.tcp.reconnectTimeout);
}
};
let onSocketError = err => {
this.log.error('Socket Error', err);
this.setState('error');
this.emit('error', err);
};
let onSocketData = data => {
buffer = Buffer.concat([buffer, data]);
while (buffer.length > 8) {
// http://www.simplymodbus.ca/TCP.htm
// 1. extract mbap
const id = buffer.readUInt16BE(0);
//const protId = buffer.readUInt16BE(2);
const len = buffer.readUInt16BE(4);
const unitId = buffer.readUInt8(6);
// 2. extract pdu
if (buffer.length < 7 + len - 1) {
break;
}
const pdu = buffer.slice(7, 7 + len - 1);
if (id === trashRequestId) {
this.log.debug('current mbap contains trashed request id.');
} else {
// emit data event and let the
// listener handle the pdu
this.emit('data', pdu, unitId);
}
buffer = buffer.slice(pdu.length + 7, buffer.length);
}
};
let onError = err => {
this.log.error('Client in error state.');
socket.destroy();
};
let onSend = (pdu, unitId) => {
reqId = (reqId + 1) % 0xffff;
let pkt = Put()
.word16be(reqId) // transaction id
.word16be(this.options.tcp.protocolVersion) // protocol version
.word16be(pdu.length + 1) // pdu length
.word8((unitId === undefined ? this.options.unitId : unitId) || 0) // unit id
.put(pdu) // the actual pdu
.buffer();
currentRequestId = reqId;
socket.write(pkt);
};
let onTrashCurrentRequest = () => trashRequestId = currentRequestId;
this.connect = () => {
connect();
return this;
};
this.reconnect = () => {
if (!this.inState('closed')) {
return this;
}
closedOnPurpose = false;
reconnect = true;
this.log.debug('Reconnecting client.');
socket.end();
return this;
};
this.close = () => {
closedOnPurpose = true;
this.log.debug('Closing client on purpose.');
socket.end();
return this;
};
init();
});

View File

@ -0,0 +1,149 @@
'use strict';
const stampit = require('stampit');
const ModbusServerCore = require('../modbus-server-core.js');
const StateMachine = require('stampit-state-machine');
const Put = require('put');
const net = require('net');
module.exports = stampit()
.compose(ModbusServerCore)
.compose(StateMachine)
.init(function () {
let server;
let socketCount = 0;
let fifo = [];
let clients = [];
let buffer = new Buffer(0);
let init = () => {
let tcp = this.options.tcp;
tcp.port = tcp.port || 502;
tcp.hostname = tcp.hostname || '0.0.0.0';
server = net.createServer();
server.on('connection', s => {
this.log.debug('new connection', s.address());
clients.push(s);
initiateSocket(s);
this.emit('connection', s.address());
});
server.on('disconnect', s => {
this.emit('close', s.address());
});
server.listen(tcp.port, tcp.hostname, err => {
if (err) {
this.log.debug('error while listening', err);
this.emit('error', err);
}
});
this.log.debug('server is listening on port', tcp.hostname + ':' + tcp.port);
this.on('newState_ready', flush);
this.setState('ready');
};
let onSocketEnd = (socket, socketId) => {
return () => {
this.emit('close');
this.log.debug('connection closed, socket', socketId);
//clients[socketId-1].destroy();
delete clients[socketId - 1];
};
};
let onSocketData = (socket, socketId) => {
return data => {
buffer = Buffer.concat([buffer, data]);
while (buffer.length > 8) {
// 1. extract mbap
const len = buffer.readUInt16BE(4);
const request = {
transId: buffer.readUInt16BE(0),
protocolVer: buffer.readUInt16BE(2),
untiId: buffer.readUInt8(6)
};
// 2. extract pdu
if (buffer.length < 7 + len - 1) {
break; // wait for next bytes
}
const pdu = buffer.slice(7, 7 + len - 1);
// emit data event and let the
// listener handle the pdu
fifo.push({request: request, pdu: pdu, socket: socket});
flush();
buffer = buffer.slice(pdu.length + 7, buffer.length);
}
};
};
let flush = () => {
if (this.inState('processing')) {
return;
}
if (!fifo.length) {
return;
}
this.setState('processing');
let current = fifo.shift();
this.onData(current.pdu, response => {
this.log.debug('sending tcp data');
let pkt = Put()
.word16be(current.request.transId) // transaction id
.word16be(current.request.protocolVer) // protocol version
.word16be(response.length + 1) // pdu length
.word8(current.request.untiId) // unit id
.put(response) // the actual pdu
.buffer();
current.socket.write(pkt);
this.setState('ready');
});
};
let onSocketError = (socket, socketCount) => {
return e => {
this.emit('error', e);
this.log.error('Socket error', e);
};
};
let initiateSocket = socket => {
socketCount += 1;
socket.on('end', onSocketEnd(socket, socketCount));
socket.on('data', onSocketData(socket, socketCount));
socket.on('error', onSocketError(socket, socketCount));
};
this.close = cb => {
for (let c in clients) {
if (clients.hasOwnProperty(c)) {
clients[c].destroy()
}
}
server.close(() => {
server.unref();
cb && cb();
});
};
this.getClients = () => clients;
init();
});

650
lib/master.js Normal file
View File

@ -0,0 +1,650 @@
'use strict';
const common = require(__dirname + '/common.js');
const Modbus = require('./jsmodbus');
// expected
// let options = {
// config: {
// type: 'tcp',
// recon:
// timeout:
// pulsetime:
// poll:
// defaultDeviceId: 0,
// tcp: { // only if type="tcp" or type="tcprtu"
// bind: '0.0.0.0',
// port: 502,
// },
// serial: { // only if type="serial"
// comName: 'tty',
// parity:
// dataBits:
// stopBits
// baudRate
// },
// maxBlock
// },
// objects: {
// }
// coils: {
// addressLow: 0,
// length: 1000,
// config: []
// blocks: {},
// cyclicWrite // only holdingRegs
// },
// inputRegs: ...,
// disInputs: ...,
// holdingRegs: ...
// };
function Master(options, adapter) {
let modbusClient;
let connected;
let connectTimer;
let nextPoll;
let pollTime;
let errorCount = 0;
let ackObjects = {};
let objects = options.objects;
let isStop = false;
let pulseList = {};
let sendBuffer = {};
let devices = options.devices;
let deviceIds = Object.keys(devices);
let reconnectTimeout = null;
function reconnect(isImmediately) {
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
reconnectTimeout = null;
}
if (nextPoll) {
clearTimeout(nextPoll);
nextPoll = null;
}
try {
if (modbusClient) {
modbusClient.close();
}
} catch (e) {
adapter.log.error('Cannot close master: ' + e);
}
if (connected) {
if (options.config.tcp) {
adapter.log.info('Disconnected from slave ' + options.config.tcp.bind);
} else {
adapter.log.info('Disconnected from slave');
}
connected = false;
adapter.setState('info.connection', false, true);
}
if (!connectTimer) {
connectTimer = setTimeout(function () {
connectTimer = null;
modbusClient.connect();
}, isImmediately ? 1000 : options.config.recon);
}
}
function pollBinariesBlock(device, regType, func, block, callback) {
let regs = device[regType];
if (block >= regs.blocks.length) {
return callback();
}
const regBlock = regs.blocks[block];
if (regBlock.startIndex === regBlock.endIndex) {
regBlock.endIndex++;
}
adapter.log.debug(`Poll ${regType} DevID(${regs.deviceId}) address ${regBlock.start} - ${regBlock.count} bits`);
if (modbusClient) {
modbusClient[func](regs.deviceId, regBlock.start, regBlock.count).then(response => {
if (response.data && response.data.length) {
for (let n = regBlock.startIndex; n < regBlock.endIndex; n++) {
let id = regs.config[n].id;
let val = response.data[regs.config[n].address - regBlock.start];
if (options.config.alwaysUpdate || ackObjects[id] === undefined || ackObjects[id].val !== val) {
ackObjects[id] = {val: val};
adapter.setState(id, !!val, true, err => {
// analyse if the state could be set (because of permissions)
if (err) adapter.log.error('Can not set state ' + id + ': ' + err);
});
}
}
} else {
adapter.log.warn(`Null buffer length ${func} for ${regType} ${regBlock.start}`);
}
setImmediate(() => pollBinariesBlock(device, regType, func, block + 1, callback));
}).catch(err => {
callback(err);
});
} else {
adapter.log.debug(`Poll canceled, because no connection`);
callback('No connection');
}
}
function pollBinariesBlocks(device, regType, func, callback) {
let regs = device[regType];
if (regs.length) {
pollBinariesBlock(device, regType, func, 0, err => {
callback(err);
});
} else {
callback();
}
}
function pollFloatBlock(device, regType, func, block, callback) {
let regs = device[regType];
if (block >= regs.blocks.length) {
return callback();
}
const regBlock = regs.blocks[block];
if (regBlock.startIndex === regBlock.endIndex) {
regBlock.endIndex++;
}
adapter.log.debug(`Poll ${regType} DevID(${regs.deviceId}) address ${regBlock.start} - ${regBlock.count} bytes`);
if (modbusClient) {
modbusClient[func](regs.deviceId, regBlock.start, regBlock.count).then(response => {
if (response.payload && response.payload.length) {
for (let n = regBlock.startIndex; n < regBlock.endIndex; n++) {
let id = regs.config[n].id;
let val = common.extractValue(regs.config[n].type, regs.config[n].len, response.payload, regs.config[n].address - regBlock.start);
if (regs.config[n].type !== 'string') {
val = val * regs.config[n].factor + regs.config[n].offset;
val = Math.round(val * options.config.round) / options.config.round;
}
if (options.config.alwaysUpdate || ackObjects[id] === undefined || ackObjects[id].val !== val) {
ackObjects[id] = {val: val};
adapter.setState(id, val, true, err => {
// analyse if the state could be set (because of permissions)
if (err) adapter.log.error(`Can not set state ${id}: ${err}`);
});
}
}
} else {
adapter.log.warn(`Null buffer length ${func} for ${regType} ${regBlock.start}`);
}
// special case
if (options.config.maxBlock < 2 && regs.config[regBlock.startIndex].cw) {
// write immediately the current value
writeFloatsReg(device, regType, objects[regs.config[regBlock.startIndex].fullId], () => {
pollFloatBlock(device, regType, func, block + 1, callback);
});
} else {
setImmediate(() => pollFloatBlock(device, regType, func, block + 1, callback));
}
}).catch(err => {
callback(err);
});
} else {
adapter.log.debug(`Poll canceled, because no connection`);
callback('No connection');
}
}
function pollFloatsBlocks(device, regType, func, callback) {
let regs = device[regType];
if (regs.length) {
pollFloatBlock(device, regType, func, 0, err => {
if (!err && regs.cyclicWrite && regs.cyclicWrite.length && options.config.maxBlock >= 2) {
writeFloatsRegs(device, regType, 0, callback);
} else {
callback(err);
}
});
} else {
callback();
}
}
function writeFloatsReg(device, regType, obj, callback) {
let regs = device[regType];
if (obj.native.len > 1) {
let buffer = new Buffer(obj.native.len * 2);
for (let b = 0; b < buffer.length; b++) {
buffer[b] = regs.config[(obj.native.address - regs.addressLow) * 2 + b];
}
modbusClient.writeMultipleRegisters(regs.deviceId, obj.native.address, buffer)
.then(response => callback())
.catch(err => {
adapter.log.error('Cannot write: ' + JSON.stringify(err));
callback(err);
});
} else {
callback();
}
}
function writeFloatsRegs(device, regType, i, callback) {
let regs = device[regType];
if (i >= regs.cyclicWrite.length) {
return callback();
}
let id = regs.cyclicWrite[i];
writeFloatsReg(device, regType, objects[id], () => {
writeFloatsRegs(device, regType, i + 1, callback);
});
}
function pollResult(startTime, err, cb) {
if (err) {
errorCount++;
adapter.log.warn(`Poll error count: ${errorCount} code: ${JSON.stringify(err)}`);
adapter.setState('info.connection', false, true);
if (errorCount > 12 * deviceIds.length) { // 2 reconnects did not help, restart adapter
throw new Error('Reconnect did not help, restart adapter');
} else if (errorCount < 6 * deviceIds.length && connected) {
cb && cb();
} else {
cb && cb('disconnect');
}
} else {
let currentPollTime = (new Date()).valueOf() - startTime;
if (pollTime !== null && pollTime !== undefined) {
if (Math.abs(pollTime - currentPollTime) > 100) {
pollTime = currentPollTime;
adapter.setState('info.pollTime', currentPollTime, true);
}
} else {
pollTime = currentPollTime;
adapter.setState('info.pollTime', currentPollTime, true);
}
if (errorCount > 0) {
adapter.setState('info.connection', true, true);
errorCount = 0;
}
cb && cb();
}
}
function pollDevice(device, callback) {
adapter.log.debug(`Poll device ${device.coils.deviceId}`);
let startTime = new Date().valueOf();
let requestTimer = setTimeout(() => {
requestTimer = null;
if (connected && !isStop) {
pollResult(startTime, 'App Timeout', callback);
}
}, options.config.timeout + 200);
// TODO: use promises here
pollBinariesBlocks(device, 'disInputs', 'readDiscreteInputs', err => {
if (err) {
if (requestTimer) {
clearTimeout(requestTimer);
requestTimer = null;
if (connected && !isStop) {
pollResult(startTime, err, callback);
}
}
} else {
pollBinariesBlocks(device, 'coils', 'readCoils', err => {
if (err) {
if (requestTimer) {
clearTimeout(requestTimer);
requestTimer = null;
if (connected && !isStop) {
pollResult(startTime, err, callback);
}
}
} else {
pollFloatsBlocks(device, 'inputRegs', 'readInputRegisters', err => {
if (err) {
if (requestTimer) {
clearTimeout(requestTimer);
requestTimer = null;
if (connected && !isStop) {
pollResult(startTime, err, callback);
}
}
} else {
pollFloatsBlocks(device, 'holdingRegs', 'readHoldingRegisters', err => {
if (requestTimer) {
clearTimeout(requestTimer);
requestTimer = null;
if (connected && !isStop) {
pollResult(startTime, err, callback);
}
}
});
}
});
}
});
}
});
}
function poll(i, cb) {
if (typeof i === 'function') {
cb = i;
i = 0;
}
i = i || 0;
if (i >= deviceIds.length) {
if (deviceIds.find(id => !devices[id].err)) {
nextPoll = setTimeout(function () {
nextPoll = null;
poll();
}, options.config.poll);
} else {
!reconnectTimeout && reconnect();
}
cb && cb();
} else {
pollDevice(devices[deviceIds[i]], err => {
devices[deviceIds[i]].err = err;
setImmediate(poll, i + 1, cb);
});
}
}
function send() {
if (!modbusClient) {
adapter.log.error('Client not connected');
return;
}
let id = Object.keys(sendBuffer)[0];
let type = objects[id].native.regType;
let val = sendBuffer[id];
if (type === 'coils') {
if (val === 'true' || val === true) val = 1;
if (val === 'false' || val === false) val = 0;
val = parseFloat(val);
modbusClient.writeSingleCoil(objects[id].native.deviceId, objects[id].native.address, !!val).then(response => {
adapter.log.debug('Write successfully [' + objects[id].native.address + ']: ' + val);
}).catch(err => {
adapter.log.error('Cannot write [' + objects[id].native.address + ']: ' + JSON.stringify(err));
// still keep on communication
if (!isStop) {
!reconnectTimeout && reconnect(true);
}
});
} else if (type === 'holdingRegs') {
if (objects[id].native.float === undefined) {
objects[id].native.float =
objects[id].native.type === 'floatle' || objects[id].native.type === 'floatbe' || objects[id].native.type === 'floatsw' ||
objects[id].native.type === 'doublele' || objects[id].native.type === 'doublebe' || objects[id].native.type === 'floatsb';
}
if (objects[id].native.type !== 'string') {
val = parseFloat(val);
val = (val - objects[id].native.offset) / objects[id].native.factor;
if (!objects[id].native.float) val = Math.round(val);
}
if (objects[id].native.len > 1) {
let hrBuffer = common.writeValue(objects[id].native.type, val, objects[id].native.len);
modbusClient.writeMultipleRegisters(objects[id].native.deviceId, objects[id].native.address, hrBuffer, function (err, response) {
adapter.log.debug('Write successfully [' + objects[id].native.address + ']: ' + val);
}).catch(err => {
adapter.log.error('Cannot write [' + objects[id].native.address + ']: ' + JSON.stringify(err));
// still keep on communication
if (!isStop) {
!reconnectTimeout && reconnect(true);
}
});
} else {
if (!modbusClient) {
adapter.log.error('Client not connected');
return;
}
let buffer = common.writeValue(objects[id].native.type, val, objects[id].native.len);
modbusClient.writeSingleRegister(objects[id].native.deviceId, objects[id].native.address, buffer).then(function (response) {
adapter.log.debug('Write successfully [' + objects[id].native.address + ': ' + val);
}).catch(err => {
adapter.log.error('Cannot write [' + objects[id].native.address + ']: ' + JSON.stringify(err));
// still keep on communication
if (!isStop) {
!reconnectTimeout && reconnect(true);
}
});
}
}
delete(sendBuffer[id]);
if (Object.keys(sendBuffer).length) {
setTimeout(send, 0);
}
}
function writeHelper(id, state) {
sendBuffer[id] = state.val;
if (Object.keys(sendBuffer).length === 1) {
send();
}
}
this.write = (id, state) => {
if (!objects[id] || !objects[id].native) {
adapter.log.error('Can not set state ' + id + ': unknown object');
return;
}
if (objects[id].native.regType === 'coils' || objects[id].native.regType === 'holdingRegs') {
if (!objects[id].native.wp) {
writeHelper(id, state);
setTimeout(function () {
let _id = id.substring(adapter.namespace.length + 1);
adapter.setState(id, ackObjects[_id] ? ackObjects[_id].val : null, true, function (err) {
// analyse if the state could be set (because of permissions)
if (err) adapter.log.error('Can not set state ' + id + ': ' + err);
});
}, options.config.poll * 1.5);
} else {
if (pulseList[id] === undefined) {
let _id = id.substring(adapter.namespace.length + 1);
pulseList[id] = ackObjects[_id] ? ackObjects[_id].val : !state.val;
setTimeout(function () {
writeHelper(id, {val: pulseList[id]});
setTimeout(function () {
if (ackObjects[_id]) {
adapter.setState(id, ackObjects[_id].val, true, function (err) {
// analyse if the state could be set (because of permissions)
if (err) adapter.log.error('Can not set state ' + id + ': ' + err);
});
}
delete pulseList[id];
}, options.config.poll * 1.5);
}, options.config.pulsetime);
writeHelper(id, state);
}
}
} else {
setImmediate(() => {
let _id = id.substring(adapter.namespace.length + 1);
adapter.setState(id, ackObjects[_id] ? ackObjects[_id].val : null, true, function (err) {
// analyse if the state could be set (because of permissions)
if (err) adapter.log.error('Can not set state ' + id + ': ' + err);
});
});
}
};
this.start = () => {
if (modbusClient && typeof modbusClient.connect === 'function') {
modbusClient.connect();
}
};
this.close = () => {
isStop = true;
if (nextPoll) {
clearTimeout(nextPoll);
nextPoll = null;
}
if (modbusClient) {
try {
modbusClient.close();
} catch (e) {
}
modbusClient = null;
}
};
(function _constructor () {
adapter.setState('info.connection', false, true);
if (options.config.type === 'tcp') {
const tcp = options.config.tcp;
if (!tcp || !tcp.bind || tcp.bind === '0.0.0.0') {
adapter.log.error('IP address is not defined');
return;
}
try {
modbusClient = Modbus('client', 'tcp')({
options: {
tcp: {
host: tcp.bind,
port: parseInt(tcp.port, 10) || 502,
autoReconnect: false,
},
log: adapter.log,
timeout: options.config.timeout,
unitId: options.config.defaultDeviceId
}
});
} catch (e) {
adapter.log.error('Cannot connect to "' + tcp.bind + ':' + (parseInt(tcp.port, 10) || 502) + '": ' + e);
}
} else if (options.config.type === 'tcprtu') {
const tcp = options.config.tcp;
if (!tcp || !tcp.bind || tcp.bind === '0.0.0.0') {
adapter.log.error('IP address is not defined');
return;
}
try {
modbusClient = Modbus('client', 'tcp-rtu')({
options: {
tcp: {
host: tcp.bind,
port: parseInt(tcp.port, 10) || 502,
autoReconnect: false,
},
log: adapter.log,
timeout: options.config.timeout,
unitId: options.config.defaultDeviceId
}
});
} catch (e) {
adapter.log.error('Cannot connect to "' + tcp.bind + ':' + (parseInt(tcp.port, 10) || 502) + '": ' + e);
}
} else if (options.config.type === 'serial') {
const serial = options.config.serial;
if (!serial || !serial.comName) {
adapter.log.error('Serial devicename is not defined');
return;
}
try {
modbusClient = Modbus('client', 'serial')({
options: {
serial: {
portName: serial.comName,
baudRate: parseInt(serial.baudRate, 10) || 9600,
dataBits: parseInt(serial.dataBits, 10) || 8,
stopBits: parseInt(serial.stopBits, 10) || 1,
parity: serial.parity || 'none',
},
log: adapter.log,
timeout: options.config.timeout,
unitId: options.config.defaultDeviceId
}
});
} catch (e) {
adapter.log.error('Cannot open port "' + serial.comName + '" [' + (parseInt(serial.baudRate, 10) || 9600) + ']: ' + e);
}
} else {
adapter.log.error(`Unsupported type ${options.config.type}"`);
return;
}
if (!modbusClient) {
adapter.log.error('Cannot create modbus master!');
return;
}
modbusClient.on('connect', function () {
if (!connected) {
if (options.config.type === 'tcp') {
adapter.log.info('Connected to slave ' + options.config.tcp.bind);
} else {
adapter.log.info('Connected to slave');
}
connected = true;
adapter.setState('info.connection', true, true);
}
if (nextPoll) {
clearTimeout(nextPoll);
nextPoll = null;
}
poll();
}).on('disconnect', () => {
if (isStop) return;
if (!reconnectTimeout) {
reconnectTimeout = setTimeout(reconnect, 1000);
}
});
modbusClient.on('close', () => {
if (isStop) return;
if (!reconnectTimeout) {
reconnectTimeout = setTimeout(reconnect, 1000);
}
});
modbusClient.on('error', err => {
if (isStop) return;
adapter.log.warn('On error: ' + JSON.stringify(err));
if (!reconnectTimeout) {
reconnectTimeout = setTimeout(reconnect, 1000);
}
});
modbusClient.on('trashCurrentRequest', err => {
if (isStop) return;
adapter.log.warn('Error: ' + JSON.stringify(err));
if (!reconnectTimeout) {
reconnectTimeout = setTimeout(reconnect, 1000);
}
});
})();
return this;
}
module.exports = Master;

406
lib/slave.js Normal file
View File

@ -0,0 +1,406 @@
'use strict';
const common = require(__dirname + '/common.js');
const Modbus = require('./jsmodbus');
// expected
// let options = {
// config: {
// round: 1,
// tcp: {
// port: 502
// }
// },
// objects: {
// }
// coils: {
// config: ...
// changed: true,
// addressHigh: 0,
// addressLow: 0,
// values: [],
// mapping: {}
// },
// inputRegs: ...,
// disInputs: ...,
// holdingRegs: ...
// };
function Slave(options, adapter) {
let objects = options.objects;
let device = options.devices[Object.keys(options.devices)[0]];
let delayStart = true;
let modbusServer;
function getListOfClients(clients) {
let list = [];
for(let c in clients) {
if (clients.hasOwnProperty(c)) {
let address = clients[c].address().address;
if (address) list.push(address);
}
}
return list.join(',');
}
this.write = (id, state) => {
if (!objects[id] || !objects[id].native) {
adapter.log.error('Can not set state ' + id + ': unknown object');
return;
}
if (objects[id].native.float === undefined) {
objects[id].native.float =
objects[id].native.type === 'floatle' || objects[id].native.type === 'floatbe' || objects[id].native.type === 'floatsw' ||
objects[id].native.type === 'doublele' || objects[id].native.type === 'doublebe' || objects[id].native.type === 'floatsb';
}
let val;
let buffer;
let b;
let t = typeof state.val;
let type = objects[id].native.regType;
let regs = device[type];
if (!regs) {
adapter.log.error('Invalid type ' + type);
return;
}
regs.changed = true;
if (type === 'disInputs' || type === 'coils') {
if (t === 'boolean' || t === 'number') {
regs.values[objects[id].native.address - regs.addressLow] = state.val ? 1 : 0;
} else {
regs.values[objects[id].native.address - regs.addressLow] = parseInt(state.val, 10) ? 1 : 0;
}
} else if (type === 'inputRegs' || type === 'holdingRegs') {
if (objects[id].native.type !== 'string') {
if (t === 'boolean') {
val = state.val ? 1 : 0;
} else if (t === 'number') {
val = state.val;
} else {
val = parseFloat(state.val);
}
val = (val - objects[id].native.offset) / objects[id].native.factor;
if (!objects[id].native.float) val = Math.round(val);
} else {
val = state.val;
}
buffer = common.writeValue(objects[id].native.type, val, objects[id].native.len);
for (b = 0; b < buffer.length; b++) {
regs.values[(objects[id].native.address - regs.addressLow) * 2 + b] = buffer[b];
}
} else {
adapter.log.error('Unknown state "' + id + '" type: ' + objects[id].native.regType);
}
};
this.start = () => {
if (!delayStart && !modbusServer) {
modbusServer = Modbus('server', 'tcp')({
options: {
log: adapter.log,
tcp: {
port: parseInt(options.config.tcp.port, 10) || 502,
hostname: options.config.tcp.bind || '127.0.0.1',
}
},
responseDelay: 100,
coils: new Buffer((device.coils.addressHigh >> 3) + ((device.coils.addressHigh - 1) % 8 ? 1 : 0)),
discrete: new Buffer((device.disInputs.addressHigh >> 3) + ((device.disInputs.addressHigh - 1) % 8 ? 1 : 0)),
input: new Buffer(device.inputRegs.addressHigh * 2),
holding: new Buffer(device.holdingRegs.addressHigh * 2)
});
modbusServer.on('readCoilsRequest', function (start, quantity) {
let regs = device.coils;
if (regs.changed || (regs.lastStart > start || regs.lastEnd < start + quantity)) {
regs.lastStart = start;
regs.lastEnd = start + quantity;
regs.changed = false;
let resp = new Array(Math.ceil(quantity / 16) * 2);
let i = 0;
let data = this.getCoils();
let j;
for (j = 0; j < resp.length && start + j < data.byteLength; j++) {
resp[j] = data.readUInt8(start + j);
}
for (; j < resp.length; j++) {
resp[j] = 0;
}
while (i < quantity && i + start < regs.addressHigh) {
if (regs.values[i + start - regs.addressLow]) {
resp[Math.floor(i / 8)] |= 1 << (i % 8);
} else {
resp[Math.floor(i / 8)] &= ~(1 << (i % 8));
}
i++;
}
let len = data.length;
for (i = 0; i < resp.length; i++) {
if (start + i >= len) break;
data.writeUInt8(resp[i], start + i);
}
}
});
modbusServer.on('readDiscreteInputsRequest', function (start, quantity) {
let regs = device.disInputs;
if (regs.changed || (regs.lastStart > start || regs.lastEnd < start + quantity)) {
regs.lastStart = start;
regs.lastEnd = start + quantity;
regs.changed = false;
let resp = new Array(Math.ceil(quantity / 16) * 2);
let i = 0;
let data = this.getDiscrete();
let j;
for (j = 0; j < resp.length && start + j < data.byteLength; j++) {
resp[j] = data.readUInt8(start + j);
}
for (; j < resp.length; j++) {
resp[j] = 0;
}
while (i < quantity && i + start < regs.addressHigh) {
if (regs.values[i + start - regs.addressLow]) {
resp[Math.floor(i / 8)] |= 1 << (i % 8);
} else {
resp[Math.floor(i / 8)] &= ~(1 << (i % 8));
}
i++;
}
let len = data.length;
for (i = 0; i < resp.length; i++) {
if (start + i >= len) break;
data.writeUInt8(resp[i], start + i);
}
}
});
modbusServer.on('readInputRegistersRequest', function (start, quantity) {
let regs = device.inputRegs;
if (regs.changed || (regs.lastStart > start || regs.lastEnd < start + quantity)) {
regs.lastStart = start;
regs.lastEnd = start + quantity;
regs.changed = false;
let data = this.getInput();
const end = start + quantity * 2;
const low = regs.addressLow * 2;
const high = regs.addressHigh * 2;
for (let i = start; i < end; i++) {
if (i >= data.length) break;
if (i >= low && i < high) {
data.writeUInt8(regs.values[i - low], i);
} else {
data.writeUInt8(0, i);
}
}
}
});
modbusServer.on('readHoldingRegistersRequest', function (start, quantity) {
let regs = device.holdingRegs;
if (regs.changed || (regs.lastStart > start || regs.lastEnd < start + quantity)) {
regs.lastStart = start;
regs.lastEnd = start + quantity;
regs.changed = false;
let data = this.getHolding();
const end = start + quantity * 2;
const low = regs.addressLow * 2;
const high = regs.addressHigh * 2;
for (let i = start; i < end; i++) {
if (i >= data.length) break;
if (i >= low && i < high) {
data.writeUInt8(regs.values[i - low], i);
} else {
data.writeUInt8(0, i);
}
}
}
});
modbusServer.on('postWriteSingleCoilRequest', function (start, value) {
let regs = device.coils;
let a = start - regs.addressLow;
if (a >= 0 && regs.mapping[a]) {
adapter.setState(regs.mapping[a], value, true, function (err) {
// analyse if the state could be set (because of permissions)
if (err) adapter.log.error('Can not set state: ' + err);
});
regs.values[a] = value;
}
});
const mPow2 = [
0x01,
0x02,
0x04,
0x08,
0x10,
0x20,
0x40,
0x80
];
modbusServer.on('postWriteMultipleCoilsRequest', function (start, length /* , byteLength*/) {
let regs = device.coils;
let i = 0;
let data = this.getCoils();
if (start < regs.addressLow) {
start = regs.addressLow;
}
while (i < length && i + start < regs.addressHigh) {
let a = i + start - regs.addressLow;
if (a >= 0 && regs.mapping[a]) {
let value = data.readUInt8((i + start) >> 3);
value = value & mPow2[(i + start) % 8];
adapter.setState(regs.mapping[a], !!value, true, function (err) {
// analyse if the state could be set (because of permissions)
if (err) adapter.log.error('Can not set state: ' + err);
});
regs.values[a] = !!value;
}
i++;
}
});
modbusServer.on('postWriteSingleRegisterRequest', function (start, value) {
let regs = device.holdingRegs;
start = start >> 1;
let a = start - regs.addressLow;
if (a >= 0 && regs.mapping[a]) {
let native = options.objects[regs.mapping[a]].native;
let buf = new Buffer(2);
buf.writeUInt16BE(value);
let val = common.extractValue(native.type, native.len, buf, 0);
if (native.type !== 'string') {
val = (val - native.offset) / native.factor;
val = Math.round(val * options.config.round) / options.config.round;
}
adapter.setState(regs.mapping[a], val, true, function (err) {
// analyse if the state could be set (because of permissions)
if (err) adapter.log.error('Can not set state: ' + err);
});
regs.values[a] = buf[0];
regs.values[a + 1] = buf[1];
}
});
modbusServer.on('postWriteMultipleRegistersRequest', function (start, length /* , byteLength*/) {
let regs = device.holdingRegs;
let data = this.getHolding();
let i = 0;
start = start >> 1;
if (start < regs.addressLow) {
start = regs.addressLow;
}
while (i < length && i + start < regs.addressHigh) {
let a = i + start - regs.addressLow;
if (a >= 0 && regs.mapping[a]) {
let native = options.objects[regs.mapping[a]].native;
let val = common.extractValue(native.type, native.len, data, i + start);
if (native.type !== 'string') {
val = val * native.factor + native.offset;
val = Math.round(val * options.config.round) / options.config.round;
}
adapter.setState(regs.mapping[a], val, true, function (err) {
// analyse if the state could be set (because of permissions)
if (err) adapter.log.error('Can not set state: ' + err);
});
for (let k = 0; k < native.len * 2; k++) {
regs.values[a * 2 + k] = data.readUInt8(start * 2 + k);
}
i += native.len;
} else {
i++;
}
}
});
modbusServer.on('connection', client => {
let list = getListOfClients(modbusServer.getClients());
adapter.log.debug('+ Clients connected: ' + list);
adapter.setState('info.connection', list, true);
}).on('close', client => {
let list = getListOfClients(modbusServer.getClients());
adapter.log.debug('- Client connected: ' + list);
adapter.setState('info.connection', list, true);
}).on('error', err => {
let list = getListOfClients(modbusServer.getClients());
adapter.log.info('- Clients connected: ' + list);
adapter.setState('info.connection', list, true);
adapter.log.warn('Error on connection: ' + JSON.stringify(err));
});
}
};
this.close = () => {
if (modbusServer) {
try {
modbusServer.close();
} catch (e) {
}
modbusServer = null;
}
};
this._initValues = (states, regs) => {
// build ready arrays
for (let i = 0; regs.fullIds.length > i; i++) {
let id = regs.fullIds[i];
if (states[id] && states[id].val !== undefined) {
this.write(id, states[id]);
} else {
adapter.setState(id, 0, true, err => {
// analyse if the state could be set (because of permissions)
if (err) adapter.log.error('Can not set state ' + id + ': ' + err);
});
}
}
// fill with 0 empty values
for (let i = 0; i < regs.values.length; i++) {
if (regs.values[i] === undefined || regs.values[i] === null) {
regs.values[i] = 0;
} else if (typeof regs.values[i] === 'boolean') {
regs.values[i] = regs.values[i] ? 1 : 0;
} else if (typeof regs.values[i] !== 'number') {
regs.values[i] = parseInt(regs.values[i], 10) ? 1 : 0;
}
}
};
this.initValues = callback => {
// read all states
adapter.getStates('*', (err, states) => {
this._initValues(states, device.disInputs);
this._initValues(states, device.coils);
this._initValues(states, device.inputRegs);
this._initValues(states, device.holdingRegs);
callback();
});
};
(function _constructor() {
adapter.setState('info.connection', 0, true);
this.initValues(() => {
delayStart = false;
adapter.log.debug('Slave ready to start');
this.start();
});
}.bind(this))();
return this;
}
module.exports = Slave;

83
lib/utils.js Normal file
View File

@ -0,0 +1,83 @@
'use strict';
const fs = require('fs');
const path = require('path');
let controllerDir;
let appName;
/**
* returns application name
*
* The name of the application can be different and this function finds it out.
*
* @returns {string}
*/
function getAppName() {
const parts = __dirname.replace(/\\/g, '/').split('/');
return parts[parts.length - 2].split('.')[0];
}
/**
* looks for js-controller home folder
*
* @param {boolean} isInstall
* @returns {string}
*/
function getControllerDir(isInstall) {
// Find the js-controller location
const possibilities = [
'yunkong2.js-controller',
'yunkong2.js-controller',
];
/** @type {string} */
let controllerPath;
for (const pkg of possibilities) {
try {
const possiblePath = require.resolve(pkg);
if (fs.existsSync(possiblePath)) {
controllerPath = possiblePath;
break;
}
} catch (e) { /* not found */ }
}
if (controllerPath == null) {
if (!isInstall) {
console.log('Cannot find js-controller');
process.exit(10);
} else {
process.exit();
}
}
// we found the controller
return path.dirname(controllerPath);
}
/**
* reads controller base settings
*
* @alias getConfig
* @returns {object}
*/
function getConfig() {
let configPath;
if (fs.existsSync(
configPath = path.join(controllerDir, 'conf', appName + '.json')
)) {
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
} else if (fs.existsSync(
configPath = path.join(controllerDir, 'conf', + appName.toLowerCase() + '.json')
)) {
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
} else {
throw new Error('Cannot find ' + controllerDir + '/conf/' + appName + '.json');
}
}
appName = getAppName();
controllerDir = getControllerDir(typeof process !== 'undefined' && process.argv && process.argv.indexOf('--install') !== -1);
const adapter = require(path.join(controllerDir, 'lib/adapter.js'));
exports.controllerDir = controllerDir;
exports.getConfig = getConfig;
exports.Adapter = adapter;
exports.appName = appName;

1211
main.js Normal file

File diff suppressed because it is too large Load Diff

42
package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "yunkong2.rsonoff",
"version": "2.0.9",
"description": "Connect devices oder RSonoff",
"homepage": "https://git.spacen.net/yunkong2/yunkong2.rsonoff",
"license": "MIT",
"keywords": [
"yunkong2",
"modbus",
"Smart Home",
"home automation"
],
"repository": {
"type": "git",
"url": "https://git.spacen.net/yunkong2/yunkong2.rsonoff"
},
"optionalDependencies": {
"serialport": "^6.0.4"
},
"dependencies": {
"stampit": "^2.1.2",
"put": "0.0.6",
"crc": "3.8.0",
"stampit-state-machine": "^0.2.1",
"stampit-event-bus": "^0.1.1"
},
"devDependencies": {
"gulp": "^3.9.1",
"mocha": "^5.2.0",
"chai": "^4.2.0",
"nan": "2.11.1",
"modbus-stack": "^0.2.1"
},
"main": "main.js",
"scripts": {
"test": "node node_modules/mocha/bin/mocha test/testAdapter.js"
},
"bugs": {
"url": "https://git.spacen.net/yunkong2/yunkong2.rsonoff/issues"
},
"readmeFilename": "README.md"
}

BIN
test/Ananas32.zip Normal file

Binary file not shown.

BIN
test/Ananas64.zip Normal file

Binary file not shown.

BIN
test/RMMS.zip Normal file

Binary file not shown.

728
test/lib/setup.js Normal file
View File

@ -0,0 +1,728 @@
/* jshint -W097 */// jshint strict:false
/*jslint node: true */
// check if tmp directory exists
var fs = require('fs');
var path = require('path');
var child_process = require('child_process');
var rootDir = path.normalize(__dirname + '/../../');
var pkg = require(rootDir + 'package.json');
var debug = typeof v8debug === 'object';
pkg.main = pkg.main || 'main.js';
var adapterName = path.normalize(rootDir).replace(/\\/g, '/').split('/');
adapterName = adapterName[adapterName.length - 2];
var adapterStarted = false;
function getAppName() {
var parts = __dirname.replace(/\\/g, '/').split('/');
return parts[parts.length - 3].split('.')[0];
}
var appName = getAppName().toLowerCase();
var objects;
var states;
var pid = null;
function copyFileSync(source, target) {
var targetFile = target;
//if target is a directory a new file with the same name will be created
if (fs.existsSync(target)) {
if ( fs.lstatSync( target ).isDirectory() ) {
targetFile = path.join(target, path.basename(source));
}
}
try {
fs.writeFileSync(targetFile, fs.readFileSync(source));
}
catch (err) {
console.log("file copy error: " +source +" -> " + targetFile + " (error ignored)");
}
}
function copyFolderRecursiveSync(source, target, ignore) {
var files = [];
var base = path.basename(source);
if (base === adapterName) {
base = pkg.name;
}
//check if folder needs to be created or integrated
var targetFolder = path.join(target, base);
if (!fs.existsSync(targetFolder)) {
fs.mkdirSync(targetFolder);
}
//copy
if (fs.lstatSync(source).isDirectory()) {
files = fs.readdirSync(source);
files.forEach(function (file) {
if (ignore && ignore.indexOf(file) !== -1) {
return;
}
var curSource = path.join(source, file);
var curTarget = path.join(targetFolder, file);
if (fs.lstatSync(curSource).isDirectory()) {
// ignore grunt files
if (file.indexOf('grunt') !== -1) return;
if (file === 'chai') return;
if (file === 'mocha') return;
copyFolderRecursiveSync(curSource, targetFolder, ignore);
} else {
copyFileSync(curSource, curTarget);
}
});
}
}
if (!fs.existsSync(rootDir + 'tmp')) {
fs.mkdirSync(rootDir + 'tmp');
}
function storeOriginalFiles() {
console.log('Store original files...');
var dataDir = rootDir + 'tmp/' + appName + '-data/';
var f = fs.readFileSync(dataDir + 'objects.json');
var objects = JSON.parse(f.toString());
if (objects['system.adapter.admin.0'] && objects['system.adapter.admin.0'].common) {
objects['system.adapter.admin.0'].common.enabled = false;
}
if (objects['system.adapter.admin.1'] && objects['system.adapter.admin.1'].common) {
objects['system.adapter.admin.1'].common.enabled = false;
}
fs.writeFileSync(dataDir + 'objects.json.original', JSON.stringify(objects));
try {
f = fs.readFileSync(dataDir + 'states.json');
fs.writeFileSync(dataDir + 'states.json.original', f);
}
catch (err) {
console.log('no states.json found - ignore');
}
}
function restoreOriginalFiles() {
console.log('restoreOriginalFiles...');
var dataDir = rootDir + 'tmp/' + appName + '-data/';
var f = fs.readFileSync(dataDir + 'objects.json.original');
fs.writeFileSync(dataDir + 'objects.json', f);
try {
f = fs.readFileSync(dataDir + 'states.json.original');
fs.writeFileSync(dataDir + 'states.json', f);
}
catch (err) {
console.log('no states.json.original found - ignore');
}
}
function checkIsAdapterInstalled(cb, counter, customName) {
customName = customName || pkg.name.split('.').pop();
counter = counter || 0;
var dataDir = rootDir + 'tmp/' + appName + '-data/';
console.log('checkIsAdapterInstalled...');
try {
var f = fs.readFileSync(dataDir + 'objects.json');
var objects = JSON.parse(f.toString());
if (objects['system.adapter.' + customName + '.0']) {
console.log('checkIsAdapterInstalled: ready!');
setTimeout(function () {
if (cb) cb();
}, 100);
return;
} else {
console.warn('checkIsAdapterInstalled: still not ready');
}
} catch (err) {
}
if (counter > 20) {
console.error('checkIsAdapterInstalled: Cannot install!');
if (cb) cb('Cannot install');
} else {
console.log('checkIsAdapterInstalled: wait...');
setTimeout(function() {
checkIsAdapterInstalled(cb, counter + 1);
}, 1000);
}
}
function checkIsControllerInstalled(cb, counter) {
counter = counter || 0;
var dataDir = rootDir + 'tmp/' + appName + '-data/';
console.log('checkIsControllerInstalled...');
try {
var f = fs.readFileSync(dataDir + 'objects.json');
var objects = JSON.parse(f.toString());
if (objects['system.adapter.admin.0']) {
console.log('checkIsControllerInstalled: installed!');
setTimeout(function () {
if (cb) cb();
}, 100);
return;
}
} catch (err) {
}
if (counter > 20) {
console.log('checkIsControllerInstalled: Cannot install!');
if (cb) cb('Cannot install');
} else {
console.log('checkIsControllerInstalled: wait...');
setTimeout(function() {
checkIsControllerInstalled(cb, counter + 1);
}, 1000);
}
}
function installAdapter(customName, cb) {
if (typeof customName === 'function') {
cb = customName;
customName = null;
}
customName = customName || pkg.name.split('.').pop();
console.log('Install adapter...');
var startFile = 'node_modules/' + appName + '.js-controller/' + appName + '.js';
// make first install
if (debug) {
child_process.execSync('node ' + startFile + ' add ' + customName + ' --enabled false', {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2]
});
checkIsAdapterInstalled(function (error) {
if (error) console.error(error);
console.log('Adapter installed.');
if (cb) cb();
});
} else {
// add controller
var _pid = child_process.fork(startFile, ['add', customName, '--enabled', 'false'], {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2, 'ipc']
});
waitForEnd(_pid, function () {
checkIsAdapterInstalled(function (error) {
if (error) console.error(error);
console.log('Adapter installed.');
if (cb) cb();
});
});
}
}
function waitForEnd(_pid, cb) {
if (!_pid) {
cb(-1, -1);
return;
}
_pid.on('exit', function (code, signal) {
if (_pid) {
_pid = null;
cb(code, signal);
}
});
_pid.on('close', function (code, signal) {
if (_pid) {
_pid = null;
cb(code, signal);
}
});
}
function installJsController(cb) {
console.log('installJsController...');
if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller') ||
!fs.existsSync(rootDir + 'tmp/' + appName + '-data')) {
// try to detect appName.js-controller in node_modules/appName.js-controller
// travis CI installs js-controller into node_modules
if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller')) {
console.log('installJsController: no js-controller => copy it from "' + rootDir + 'node_modules/' + appName + '.js-controller"');
// copy all
// stop controller
console.log('Stop controller if running...');
var _pid;
if (debug) {
// start controller
_pid = child_process.exec('node ' + appName + '.js stop', {
cwd: rootDir + 'node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2]
});
} else {
_pid = child_process.fork(appName + '.js', ['stop'], {
cwd: rootDir + 'node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
waitForEnd(_pid, function () {
// copy all files into
if (!fs.existsSync(rootDir + 'tmp')) fs.mkdirSync(rootDir + 'tmp');
if (!fs.existsSync(rootDir + 'tmp/node_modules')) fs.mkdirSync(rootDir + 'tmp/node_modules');
if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')){
console.log('Copy js-controller...');
copyFolderRecursiveSync(rootDir + 'node_modules/' + appName + '.js-controller', rootDir + 'tmp/node_modules/');
}
console.log('Setup js-controller...');
var __pid;
if (debug) {
// start controller
_pid = child_process.exec('node ' + appName + '.js setup first --console', {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2]
});
} else {
__pid = child_process.fork(appName + '.js', ['setup', 'first', '--console'], {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
waitForEnd(__pid, function () {
checkIsControllerInstalled(function () {
// change ports for object and state DBs
var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json');
config.objects.port = 19001;
config.states.port = 19000;
fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2));
console.log('Setup finished.');
copyAdapterToController();
installAdapter(function () {
storeOriginalFiles();
if (cb) cb(true);
});
});
});
});
} else {
// check if port 9000 is free, else admin adapter will be added to running instance
var client = new require('net').Socket();
client.connect(9000, '127.0.0.1', function() {
console.error('Cannot initiate fisrt run of test, because one instance of application is running on this PC. Stop it and repeat.');
process.exit(0);
});
setTimeout(function () {
client.destroy();
if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')) {
console.log('installJsController: no js-controller => install from git');
child_process.execSync('npm install https://github.com/' + appName + '/' + appName + '.js-controller/tarball/master --prefix ./ --production', {
cwd: rootDir + 'tmp/',
stdio: [0, 1, 2]
});
} else {
console.log('Setup js-controller...');
var __pid;
if (debug) {
// start controller
child_process.exec('node ' + appName + '.js setup first', {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2]
});
} else {
child_process.fork(appName + '.js', ['setup', 'first'], {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
}
// let npm install admin and run setup
checkIsControllerInstalled(function () {
var _pid;
if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller/' + appName + '.js')) {
_pid = child_process.fork(appName + '.js', ['stop'], {
cwd: rootDir + 'node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
waitForEnd(_pid, function () {
// change ports for object and state DBs
var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json');
config.objects.port = 19001;
config.states.port = 19000;
fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2));
copyAdapterToController();
installAdapter(function () {
storeOriginalFiles();
if (cb) cb(true);
});
});
});
}, 1000);
}
} else {
setTimeout(function () {
console.log('installJsController: js-controller installed');
if (cb) cb(false);
}, 0);
}
}
function copyAdapterToController() {
console.log('Copy adapter...');
// Copy adapter to tmp/node_modules/appName.adapter
copyFolderRecursiveSync(rootDir, rootDir + 'tmp/node_modules/', ['.idea', 'test', 'tmp', '.git', appName + '.js-controller']);
console.log('Adapter copied.');
}
function clearControllerLog() {
var dirPath = rootDir + 'tmp/log';
var files;
try {
if (fs.existsSync(dirPath)) {
console.log('Clear controller log...');
files = fs.readdirSync(dirPath);
} else {
console.log('Create controller log directory...');
files = [];
fs.mkdirSync(dirPath);
}
} catch(e) {
console.error('Cannot read "' + dirPath + '"');
return;
}
if (files.length > 0) {
try {
for (var i = 0; i < files.length; i++) {
var filePath = dirPath + '/' + files[i];
fs.unlinkSync(filePath);
}
console.log('Controller log cleared');
} catch (err) {
console.error('cannot clear log: ' + err);
}
}
}
function clearDB() {
var dirPath = rootDir + 'tmp/yunkong2-data/sqlite';
var files;
try {
if (fs.existsSync(dirPath)) {
console.log('Clear sqlite DB...');
files = fs.readdirSync(dirPath);
} else {
console.log('Create controller log directory...');
files = [];
fs.mkdirSync(dirPath);
}
} catch(e) {
console.error('Cannot read "' + dirPath + '"');
return;
}
if (files.length > 0) {
try {
for (var i = 0; i < files.length; i++) {
var filePath = dirPath + '/' + files[i];
fs.unlinkSync(filePath);
}
console.log('Clear sqlite DB');
} catch (err) {
console.error('cannot clear DB: ' + err);
}
}
}
function setupController(cb) {
installJsController(function (isInited) {
clearControllerLog();
clearDB();
if (!isInited) {
restoreOriginalFiles();
copyAdapterToController();
}
// read system.config object
var dataDir = rootDir + 'tmp/' + appName + '-data/';
var objs;
try {
objs = fs.readFileSync(dataDir + 'objects.json');
objs = JSON.parse(objs);
}
catch (e) {
console.log('ERROR reading/parsing system configuration. Ignore');
objs = {'system.config': {}};
}
if (!objs || !objs['system.config']) {
objs = {'system.config': {}};
}
if (cb) cb(objs['system.config']);
});
}
function startAdapter(objects, states, callback) {
if (adapterStarted) {
console.log('Adapter already started ...');
if (callback) callback(objects, states);
return;
}
adapterStarted = true;
console.log('startAdapter...');
if (fs.existsSync(rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main)) {
try {
if (debug) {
// start controller
pid = child_process.exec('node node_modules/' + pkg.name + '/' + pkg.main + ' --console silly', {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2]
});
} else {
// start controller
pid = child_process.fork('node_modules/' + pkg.name + '/' + pkg.main, ['--console', 'silly'], {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2, 'ipc']
});
}
} catch (error) {
console.error(JSON.stringify(error));
}
} else {
console.error('Cannot find: ' + rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main);
}
if (callback) callback(objects, states);
}
function startController(isStartAdapter, onObjectChange, onStateChange, callback) {
if (typeof isStartAdapter === 'function') {
callback = onStateChange;
onStateChange = onObjectChange;
onObjectChange = isStartAdapter;
isStartAdapter = true;
}
if (onStateChange === undefined) {
callback = onObjectChange;
onObjectChange = undefined;
}
if (pid) {
console.error('Controller is already started!');
} else {
console.log('startController...');
adapterStarted = false;
var isObjectConnected;
var isStatesConnected;
var Objects = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/objects/objectsInMemServer');
objects = new Objects({
connection: {
"type" : "file",
"host" : "127.0.0.1",
"port" : 19001,
"user" : "",
"pass" : "",
"noFileCache": false,
"connectTimeout": 2000
},
logger: {
silly: function (msg) {
console.log(msg);
},
debug: function (msg) {
console.log(msg);
},
info: function (msg) {
console.log(msg);
},
warn: function (msg) {
console.warn(msg);
},
error: function (msg) {
console.error(msg);
}
},
connected: function () {
isObjectConnected = true;
if (isStatesConnected) {
console.log('startController: started!');
if (isStartAdapter) {
startAdapter(objects, states, callback);
} else {
if (callback) {
callback(objects, states);
callback = null;
}
}
}
},
change: onObjectChange
});
// Just open in memory DB itself
var States = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/states/statesInMemServer');
states = new States({
connection: {
type: 'file',
host: '127.0.0.1',
port: 19000,
options: {
auth_pass: null,
retry_max_delay: 15000
}
},
logger: {
silly: function (msg) {
console.log(msg);
},
debug: function (msg) {
console.log(msg);
},
info: function (msg) {
console.log(msg);
},
warn: function (msg) {
console.log(msg);
},
error: function (msg) {
console.log(msg);
}
},
connected: function () {
isStatesConnected = true;
if (isObjectConnected) {
console.log('startController: started!!');
if (isStartAdapter) {
startAdapter(objects, states, callback);
} else {
if (callback) {
callback(objects, states);
callback = null;
}
}
}
},
change: onStateChange
});
}
}
function stopAdapter(cb) {
if (!pid) {
console.error('Controller is not running!');
if (cb) {
setTimeout(function () {
cb(false);
}, 0);
}
} else {
adapterStarted = false;
pid.on('exit', function (code, signal) {
if (pid) {
console.log('child process terminated due to receipt of signal ' + signal);
if (cb) cb();
pid = null;
}
});
pid.on('close', function (code, signal) {
if (pid) {
if (cb) cb();
pid = null;
}
});
pid.kill('SIGTERM');
}
}
function _stopController() {
if (objects) {
objects.destroy();
objects = null;
}
if (states) {
states.destroy();
states = null;
}
}
function stopController(cb) {
var timeout;
if (objects) {
console.log('Set system.adapter.' + pkg.name + '.0');
objects.setObject('system.adapter.' + pkg.name + '.0', {
common:{
enabled: false
}
});
}
stopAdapter(function () {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
_stopController();
if (cb) {
cb(true);
cb = null;
}
});
timeout = setTimeout(function () {
timeout = null;
console.log('child process NOT terminated');
_stopController();
if (cb) {
cb(false);
cb = null;
}
pid = null;
}, 5000);
}
// Setup the adapter
function setAdapterConfig(common, native, instance) {
var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString());
var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0);
if (common) objects[id].common = common;
if (native) objects[id].native = native;
fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/objects.json', JSON.stringify(objects));
}
// Read config of the adapter
function getAdapterConfig(instance) {
var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString());
var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0);
return objects[id];
}
if (typeof module !== undefined && module.parent) {
module.exports.getAdapterConfig = getAdapterConfig;
module.exports.setAdapterConfig = setAdapterConfig;
module.exports.startController = startController;
module.exports.stopController = stopController;
module.exports.setupController = setupController;
module.exports.stopAdapter = stopAdapter;
module.exports.startAdapter = startAdapter;
module.exports.installAdapter = installAdapter;
module.exports.appName = appName;
module.exports.adapterName = adapterName;
module.exports.adapterStarted = adapterStarted;
}

BIN
test/mod_RSsim.exe Normal file

Binary file not shown.

20
test/test.js Normal file
View File

@ -0,0 +1,20 @@
// 'RIR' contains the "Function Code" that we are going to invoke on the remote device
var FC = require('modbus-stack').FUNCTION_CODES;
// IP and port of the MODBUS slave, default port is 502
var client = require('modbus-stack/client').createClient(502, 'localhost');
// 'req' is an instance of the low-level `ModbusRequestStack` class
var req = client.request(FC.READ_INPUT_REGISTERS, // Function Code: 4
0, // Start at address 0
50) // Read 50 contiguous registers from 0
.on('error', function (err) {
console.error(err);
});
// 'response' is emitted after the entire contents of the response has been received.
req.on('response', function(registers) {
// An Array of length 50 filled with Numbers of the current registers.
console.log(registers);
client.end();
});

141
test/testAdapter.js Normal file
View File

@ -0,0 +1,141 @@
/* jshint -W097 */// jshint strict:false
/*jslint node: true */
/*jshint expr: true*/
var expect = require('chai').expect;
var setup = require(__dirname + '/lib/setup');
var objects = null;
var states = null;
var onStateChanged = null;
var onObjectChanged = null;
var sendToID = 1;
var adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1);
function checkConnectionOfAdapter(cb, counter) {
counter = counter || 0;
console.log('Try check #' + counter);
if (counter > 30) {
if (cb) cb('Cannot check connection');
return;
}
states.getState('system.adapter.' + adapterShortName + '.0.alive', function (err, state) {
if (err) console.error(err);
if (state && state.val) {
if (cb) cb();
} else {
setTimeout(function () {
checkConnectionOfAdapter(cb, counter + 1);
}, 1000);
}
});
}
function checkValueOfState(id, value, cb, counter) {
counter = counter || 0;
if (counter > 20) {
if (cb) cb('Cannot check value Of State ' + id);
return;
}
states.getState(id, function (err, state) {
if (err) console.error(err);
if (value === null && !state) {
if (cb) cb();
} else
if (state && (value === undefined || state.val === value)) {
if (cb) cb();
} else {
setTimeout(function () {
checkValueOfState(id, value, cb, counter + 1);
}, 500);
}
});
}
function sendTo(target, command, message, callback) {
onStateChanged = function (id, state) {
if (id === 'messagebox.system.adapter.test.0') {
callback(state.message);
}
};
states.pushMessage('system.adapter.' + target, {
command: command,
message: message,
from: 'system.adapter.test.0',
callback: {
message: message,
id: sendToID++,
ack: false,
time: (new Date()).getTime()
}
});
}
describe('Test ' + adapterShortName + ' adapter', function() {
before('Test ' + adapterShortName + ' adapter: Start js-controller', function (_done) {
this.timeout(600000); // because of first install from npm
setup.setupController(function () {
var config = setup.getAdapterConfig();
// enable adapter
config.common.enabled = true;
config.common.loglevel = 'debug';
//config.native.dbtype = 'sqlite';
setup.setAdapterConfig(config.common, config.native);
setup.startController(true, function(id, obj) {}, function (id, state) {
if (onStateChanged) onStateChanged(id, state);
},
function (_objects, _states) {
objects = _objects;
states = _states;
_done();
});
});
});
/*
ENABLE THIS WHEN ADAPTER RUNS IN DEAMON MODE TO CHECK THAT IT HAS STARTED SUCCESSFULLY
*/
it('Test ' + adapterShortName + ' adapter: Check if adapter started', function (done) {
this.timeout(60000);
checkConnectionOfAdapter(function (res) {
if (res) console.log(res);
expect(res).not.to.be.equal('Cannot check connection');
objects.setObject('system.adapter.test.0', {
common: {
},
type: 'instance'
},
function () {
states.subscribeMessage('system.adapter.test.0');
done();
});
});
});
/**/
/*
PUT YOUR OWN TESTS HERE USING
it('Testname', function ( done) {
...
});
You can also use "sendTo" method to send messages to the started adapter
*/
after('Test ' + adapterShortName + ' adapter: Stop js-controller', function (done) {
this.timeout(10000);
setup.stopController(function (normalTerminated) {
console.log('Adapter normal terminated: ' + normalTerminated);
done();
});
});
});

91
test/testPackageFiles.js Normal file
View File

@ -0,0 +1,91 @@
/* jshint -W097 */
/* jshint strict:false */
/* jslint node: true */
/* jshint expr: true */
var expect = require('chai').expect;
var fs = require('fs');
describe('Test package.json and io-package.json', function() {
it('Test package files', function (done) {
console.log();
var fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json', 'utf8');
var ioPackage = JSON.parse(fileContentIOPackage);
var fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json', 'utf8');
var npmPackage = JSON.parse(fileContentNPMPackage);
expect(ioPackage).to.be.an('object');
expect(npmPackage).to.be.an('object');
expect(ioPackage.common.version, 'ERROR: Version number in io-package.json needs to exist').to.exist;
expect(npmPackage.version, 'ERROR: Version number in package.json needs to exist').to.exist;
expect(ioPackage.common.version, 'ERROR: Version numbers in package.json and io-package.json needs to match').to.be.equal(npmPackage.version);
if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) {
console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!');
console.log();
}
expect(npmPackage.author, 'ERROR: Author in package.json needs to exist').to.exist;
expect(ioPackage.common.authors, 'ERROR: Authors in io-package.json needs to exist').to.exist;
if (ioPackage.common.name.indexOf('template') !== 0) {
if (Array.isArray(ioPackage.common.authors)) {
expect(ioPackage.common.authors.length, 'ERROR: Author in io-package.json needs to be set').to.not.be.equal(0);
if (ioPackage.common.authors.length === 1) {
expect(ioPackage.common.authors[0], 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
}
}
else {
expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
}
}
else {
console.log('WARNING: Testing for set authors field in io-package skipped because template adapter');
console.log();
}
expect(fs.existsSync(__dirname + '/../README.md'), 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.').to.be.true;
if (!ioPackage.common.titleLang || typeof ioPackage.common.titleLang !== 'object') {
console.log('WARNING: titleLang is not existing in io-package.json. Please add');
console.log();
}
if (
ioPackage.common.title.indexOf('yunkong2') !== -1 ||
ioPackage.common.title.indexOf('yunkong2') !== -1 ||
ioPackage.common.title.indexOf('adapter') !== -1 ||
ioPackage.common.title.indexOf('Adapter') !== -1
) {
console.log('WARNING: title contains Adapter or yunkong2. It is clear anyway, that it is adapter for yunkong2.');
console.log();
}
if (ioPackage.common.name.indexOf('vis-') !== 0) {
if (!ioPackage.common.materialize || !fs.existsSync(__dirname + '/../admin/index_m.html') || !fs.existsSync(__dirname + '/../gulpfile.js')) {
console.log('WARNING: Admin3 support is missing! Please add it');
console.log();
}
if (ioPackage.common.materialize) {
expect(fs.existsSync(__dirname + '/../admin/index_m.html'), 'Admin3 support is enabled in io-package.json, but index_m.html is missing!').to.be.true;
}
}
var licenseFileExists = fs.existsSync(__dirname + '/../LICENSE');
var fileContentReadme = fs.readFileSync(__dirname + '/../README.md', 'utf8');
if (fileContentReadme.indexOf('## Changelog') === -1) {
console.log('Warning: The README.md should have a section ## Changelog');
console.log();
}
expect((licenseFileExists || fileContentReadme.indexOf('## License') !== -1), 'A LICENSE must exist as LICENSE file or as part of the README.md').to.be.true;
if (!licenseFileExists) {
console.log('Warning: The License should also exist as LICENSE file');
console.log();
}
if (fileContentReadme.indexOf('## License') === -1) {
console.log('Warning: The README.md should also have a section ## License to be shown in Admin3');
console.log();
}
done();
});
});