Initial commit
9
.gitignore
vendored
Normal 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
@ -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
@ -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
@ -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.
|
301
README.md
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
![Logo](admin/modbus.png)
|
||||||
|
# yunkong2.modbus
|
||||||
|
=====================
|
||||||
|
|
||||||
|
[![NPM version](http://img.shields.io/npm/v/yunkong2.modbus.svg)](https://www.npmjs.com/package/yunkong2.modbus)
|
||||||
|
[![Downloads](https://img.shields.io/npm/dm/yunkong2.modbus.svg)](https://www.npmjs.com/package/yunkong2.modbus)
|
||||||
|
|
||||||
|
[![NPM](https://nodei.co/npm/yunkong2.modbus.png?downloads=true)](https://nodei.co/npm/yunkong2.modbus/)
|
||||||
|
|
||||||
|
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 it’s 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 device’s 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.6 (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
|
74
admin/i18n/cn/translations.json
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"General": "基本",
|
||||||
|
"Inputs": "离散输入",
|
||||||
|
"Do not align addresses to word:": "Do not align addresses to 16 bits:",
|
||||||
|
"Coils": "线圈",
|
||||||
|
"Input Registers": "输入寄存器",
|
||||||
|
"Holding Registers": "保持寄存器",
|
||||||
|
"PLC Connection:": "PLC 连接:",
|
||||||
|
"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": "切换 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:": "Modbus站 ID:",
|
||||||
|
"RTU over TCP": "RTU over TCP",
|
||||||
|
"Data 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:": "连接参数",
|
||||||
|
"Partner IP Address:": "IP Address:",
|
||||||
|
"Port:": "端口:",
|
||||||
|
"Type:": "类型:",
|
||||||
|
"Master": "主机",
|
||||||
|
"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": "Factor",
|
||||||
|
"Offset": "偏移",
|
||||||
|
"Cyclic write": "Cyclic write",
|
||||||
|
"TCP/Serial RTU:": "TCP/Serial RTU:",
|
||||||
|
"TCP": "TCP",
|
||||||
|
"Serial": "串口",
|
||||||
|
"Baud rate:": "波特率:",
|
||||||
|
"Use direct addresses by aliases:": "Use direct addresses by aliases:",
|
||||||
|
"Select port": "端口"
|
||||||
|
}
|
74
admin/i18n/en/translations.json
Normal 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"
|
||||||
|
}
|
BIN
admin/img/plc_back.png
Normal file
After Width: | Height: | Size: 67 KiB |
1365
admin/index.html
Normal file
205
admin/lib/css/jsgrid-theme.css
Normal file
7
admin/lib/css/jsgrid-theme.min.css
vendored
Normal file
121
admin/lib/css/jsgrid.css
Normal 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
@ -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}
|
10
admin/lib/js/grid.locale-cn.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
function translate_cn() {
|
||||||
|
window.jsGrid.Grid.prototype.noDataContent = "暂无数据";
|
||||||
|
window.jsGrid.Grid.prototype.deleteConfirm = "确认删除吗?";
|
||||||
|
window.jsGrid.Grid.prototype.pagerFormat = "分页 = {first} {prev} {pages} {next} {last} {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
BIN
admin/modbus.png
Normal file
After Width: | Height: | Size: 17 KiB |
78
admin/words.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n
|
||||||
|
/*global systemDictionary:true */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
systemDictionary = {
|
||||||
|
"General": { "cn": "基本", "en": "General"},
|
||||||
|
"Inputs": { "cn": "离散输入", "en": "Discrete Inputs"},
|
||||||
|
"Do not align addresses to word:": { "cn": "Do not align addresses to 16 bits:", "en": "Do not align addresses to 16 bits:"},
|
||||||
|
"Coils": { "cn": "线圈", "en": "Coils"},
|
||||||
|
"Input Registers": { "cn": "输入寄存器", "en": "Input Registers"},
|
||||||
|
"Holding Registers": { "cn": "保持寄存器", "en": "Holding Registers"},
|
||||||
|
"PLC Connection:": { "cn": "PLC 连接:", "en": "PLC Connection:"},
|
||||||
|
"PLC IP Address:": { "cn": "PLC IP Address:", "en": "PLC IP Address:"},
|
||||||
|
"PLC Rack:": { "cn": "PLC Rack:", "en": "PLC Rack:"},
|
||||||
|
"PLC Slot:": { "cn": "PLC Slot:", "en": "PLC Slot:"},
|
||||||
|
"Round Real to:": { "cn": "Round real to:", "en": "Round real to:"},
|
||||||
|
"Poll delay:": { "cn": "Poll delay:", "en": "Poll delay:"},
|
||||||
|
"deviceId": { "cn": "Slave ID", "en": "Slave ID"},
|
||||||
|
"Reconnect time:": { "cn": "Reconnect time:", "en": "Reconnect time:"},
|
||||||
|
"Pulse time:": { "cn": "Pulse time:", "en": "Pulse time:"},
|
||||||
|
"Import symbols file:": { "cn": "Import symbols file:", "en": "Import symbols file:"},
|
||||||
|
"Import DB file:": { "cn": "Import DB file:", "en": "Import DB file:"},
|
||||||
|
"Load Symbols": { "cn": "Load symbols", "en": "Load symbols"},
|
||||||
|
"Add DB": { "cn": "Add DB", "en": "Add DB"},
|
||||||
|
"Toggle poll": { "cn": "切换 poll", "en": "Toggle poll"},
|
||||||
|
"Toggle RW": { "cn": "切换 RW", "en": "Toggle RW"},
|
||||||
|
"Toggle WP": { "cn": "切换 WP", "en": "Toggle WP"},
|
||||||
|
"Address": { "cn": "地址", "en": "Address"},
|
||||||
|
"Name": { "cn": "名称", "en": "Name"},
|
||||||
|
"Description": { "cn": "说明", "en": "Description"},
|
||||||
|
"Type": { "cn": "类型", "en": "Type"},
|
||||||
|
"Unit": { "cn": "单位", "en": "Unit"},
|
||||||
|
"poll": { "cn": "poll", "en": "poll"},
|
||||||
|
"RW": { "cn": "RW", "en": "RW"},
|
||||||
|
"CW": { "cn": "CW", "en": "CW"},
|
||||||
|
"WP": { "cn": "WP", "en": "WP"},
|
||||||
|
"Role": { "cn": "规则", "en": "Role"},
|
||||||
|
"Room": { "cn": "场景", "en": "Room"},
|
||||||
|
"registers": { "cn": "寄存器", "en": "registers"},
|
||||||
|
"Device ID:": { "cn": "Modbus站 ID:", "en": "Device ID:"},
|
||||||
|
"RTU over TCP": { "cn": "RTU over TCP", "en": "RTU over TCP"},
|
||||||
|
"Data bits:": { "cn": "数据位:", "en": "Data bits:"},
|
||||||
|
"Stop bits:": { "cn": "停止位:", "en": "Stop bits:"},
|
||||||
|
"Parity:": { "cn": "Parity:", "en": "Parity:"},
|
||||||
|
"Read timeout:": { "cn": "Read timeout:", "en": "Read timeout:"},
|
||||||
|
"Multi device IDs:": { "cn": "Multi device IDs:", "en": "Multi device IDs:"},
|
||||||
|
"Use aliases as address:": { "cn": "Use aliases:", "en": "Use aliases:"},
|
||||||
|
"Max read request length:": { "cn": "Max read request length:", "en": "Max read request length:"},
|
||||||
|
"Enable polling of data point": { "cn": "Enable polling of data point", "en": "Enable polling of data point"},
|
||||||
|
"Write access allowed": { "cn": "Write access allowed", "en": "Write access allowed"},
|
||||||
|
"Write pulses (true=>false edge)": { "cn": "Write pulses (true=>false edge)", "en": "Write pulses (true=>false edge)"},
|
||||||
|
"Connection parameters:": { "cn": "连接参数", "en": "Connection parameters:"},
|
||||||
|
"Partner IP Address:": { "cn": "IP Address:", "en": "Partner IP Address:"},
|
||||||
|
"Port:": { "cn": "端口:", "en": "Port:"},
|
||||||
|
"Type:": { "cn": "类型:", "en": "Type:"},
|
||||||
|
"Master": { "cn": "主机", "en": "Master"},
|
||||||
|
"Slave": { "cn": "从机", "en": "Slave"},
|
||||||
|
"Are you sure?": { "cn": "你确定吗?", "en": "Are you sure?"},
|
||||||
|
"Start address:": { "cn": "开始地址:", "en": "Start address:"},
|
||||||
|
"Text copied to clipboard. Click to close the window": {"cn": "文本已经复制到剪贴板", "en": "Text copied to clipboard. Click to close the window"},
|
||||||
|
"Export": { "cn": "导出", "en": "Export"},
|
||||||
|
"Import": { "cn": "导入", "en": "Import"},
|
||||||
|
"Close": { "cn": "关闭", "en": "Close"},
|
||||||
|
"Export to CSV": { "cn": "导出成 CSV文件", "en": "Export to CSV"},
|
||||||
|
"Import from CSV": { "cn": "从 CSV文件导入", "en": "Import from CSV"},
|
||||||
|
"Delete all entries": { "cn": "删除所有的点位数据", "en": "Delete all entries"},
|
||||||
|
"Length": { "cn": "长度", "en": "Length"},
|
||||||
|
"All entries will be deleted. Are you sure?": { "cn": "所有的点位数据都将被删除,你确定吗?", "en": "All entries will be deleted. Are you sure?"},
|
||||||
|
"Factor": { "cn": "Factor", "en": "Factor"},
|
||||||
|
"Offset": { "cn": "偏移", "en": "Offset"},
|
||||||
|
"Cyclic write": { "cn": "Cyclic write", "en": "Cyclic write"},
|
||||||
|
"TCP/Serial RTU:": { "cn": "TCP/Serial RTU:", "en": "TCP/Serial RTU:"},
|
||||||
|
"TCP": { "cn": "TCP", "en": "TCP"},
|
||||||
|
"Serial": { "cn": "串口", "en": "Serial"},
|
||||||
|
"Baud rate:": { "cn": "波特率:", "en": "Baud rate:"},
|
||||||
|
"Use direct addresses by aliases:": { "cn": "Use direct addresses by aliases:", "en": "Use direct addresses by aliases:"},
|
||||||
|
"Select port": { "cn": "端口", "en": "Select port"}
|
||||||
|
};
|
401
gulpfile.js
Normal 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
After Width: | Height: | Size: 11 KiB |
BIN
img/img2.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
img/img3.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
img/img4.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
img/img5.png
Normal file
After Width: | Height: | Size: 59 KiB |
85
io-package.json
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"name": "yuanqu",
|
||||||
|
"version": "0.0.7",
|
||||||
|
"title": "Yuanqu",
|
||||||
|
"desc": {
|
||||||
|
"en": "YQ connection Slave or Master",
|
||||||
|
"cn": "智慧园区"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"platform": "Javascript/Node.js",
|
||||||
|
"mode": "daemon",
|
||||||
|
"loglevel": "info",
|
||||||
|
"messagebox": true,
|
||||||
|
"icon": "yuanqu.png",
|
||||||
|
"keywords": [
|
||||||
|
"Yuanqu"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"extIcon": "https://git.spacen.net/yunkong2/yunkong2.yuanqu/raw/master/admin/yuanqu.png",
|
||||||
|
"readme": "https://github.com/yunkong2/yunkong2.yuanqu/blob/master/README.md",
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
33
package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "yunkong2.yuanqu",
|
||||||
|
"version": "0.0.7",
|
||||||
|
"description": "Connect devices oder yuanqu protocol to yunkong2",
|
||||||
|
"homepage": "https://git.spacen.net/yunkong2/yunkong2.yuanqu",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"modbus",
|
||||||
|
"Smart Home",
|
||||||
|
"home automation"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.spacen.net/yunkong2/yunkong2.yuanqu"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"gulp": "^3.9.1",
|
||||||
|
"mocha": "^4.1.0"
|
||||||
|
},
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "node node_modules/mocha/bin/mocha test/testAdapter.js"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/yunkong2/yunkong2.yuanqu/issues"
|
||||||
|
},
|
||||||
|
"readmeFilename": "README.md"
|
||||||
|
}
|