├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── ModbusRTU.d.ts ├── README.md ├── ServerSerial.d.ts ├── ServerTCP.d.ts ├── TestPort.d.ts ├── apis ├── connection.js ├── promise.js └── worker.js ├── examples ├── async_server.js ├── buffer.js ├── buffered_server.js ├── buffertcp.js ├── debug.js ├── device-identification.js ├── logger.js ├── logger_complete.js ├── polling_RTU.js ├── polling_TCP.js ├── polling_UDP.js ├── server.js ├── server_enron.js ├── server_enron_serial.js ├── simple.js ├── simple_ble.js ├── simple_enron.js ├── simple_enron_serial.js ├── tcp_client_abort_signal.js ├── write.js └── write_complete.js ├── gulpfile.js ├── index.d.ts ├── index.js ├── npm-update.sh ├── npm-upgrade.sh ├── package.json ├── ports ├── asciiport.js ├── bleport.js ├── c701port.js ├── rtubufferedport.js ├── tcpport.js ├── tcprtubufferedport.js ├── telnetport.js ├── testport.js └── udpport.js ├── servers ├── serverserial.js ├── serverserial_pipe_handler.js ├── servertcp.js └── servertcp_handler.js ├── test ├── Lint │ └── test.js ├── apis │ └── promise.js ├── mocks │ ├── SerialPortMock.js │ ├── dgramMock.js │ └── netMock.js ├── ports │ ├── asciiport.test.js │ ├── bleport.test.js │ ├── c701port.test.js │ ├── rtubufferedport.test.js │ ├── tcpport.test.js │ ├── tcpportrtubuffered.test.js │ ├── telnetport.test.js │ └── udpport.test.js ├── servers │ ├── serverserial.test.js │ ├── servertcp.enron.test.js │ ├── servertcp.test.js │ ├── servertcpCallback.test.js │ └── servertcpPromise.test.js ├── test.js └── utils │ ├── crc16.test.js │ └── lrc.test.js ├── utils ├── buffer_bit.js ├── crc16.js └── lrc.js └── worker ├── README.md └── index.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "parserOptions": { 7 | "ecmaVersion": 2020 8 | }, 9 | "extends": "eslint:recommended", 10 | "rules": { 11 | "accessor-pairs": "error", 12 | "array-bracket-spacing": "off", 13 | "array-callback-return": "error", 14 | "arrow-body-style": "error", 15 | "arrow-parens": "error", 16 | "arrow-spacing": "error", 17 | "block-scoped-var": "off", 18 | "block-spacing": "off", 19 | "brace-style": "off", 20 | "callback-return": "off", 21 | "camelcase": "off", 22 | "capitalized-comments": "off", 23 | "class-methods-use-this": "error", 24 | "comma-dangle": "error", 25 | "comma-spacing": [ 26 | "error", 27 | { 28 | "before": false, 29 | "after": true 30 | } 31 | ], 32 | "comma-style": [ 33 | "error", 34 | "last" 35 | ], 36 | "complexity": "off", 37 | "computed-property-spacing": [ 38 | "error", 39 | "never" 40 | ], 41 | "consistent-return": "off", 42 | "consistent-this": "off", 43 | "curly": "off", 44 | "default-case": "off", 45 | "dot-location": [ 46 | "error", 47 | "property" 48 | ], 49 | "dot-notation": [ 50 | "error", 51 | { 52 | "allowKeywords": true 53 | } 54 | ], 55 | "eol-last": "error", 56 | "eqeqeq": "error", 57 | "func-call-spacing": "error", 58 | "func-name-matching": "error", 59 | "func-names": "off", 60 | "func-style": "off", 61 | "for-direction": "error", 62 | "generator-star-spacing": "error", 63 | "global-require": "off", 64 | "guard-for-in": "error", 65 | "handle-callback-err": "error", 66 | "id-blacklist": "error", 67 | "id-length": "off", 68 | "id-match": "error", 69 | "indent": [ 70 | "error", 71 | 4, 72 | { 73 | "SwitchCase": 1, 74 | "MemberExpression": 1 75 | } 76 | ], 77 | "init-declarations": "off", 78 | "jsx-quotes": "error", 79 | "key-spacing": "error", 80 | "keyword-spacing": "off", 81 | "line-comment-position": "off", 82 | "linebreak-style": [ 83 | "off", 84 | "unix" 85 | ], 86 | "lines-around-comment": "off", 87 | "lines-around-directive": "off", 88 | "max-depth": "error", 89 | "max-len": "off", 90 | "max-lines": "off", 91 | "max-nested-callbacks": "error", 92 | "max-params": "off", 93 | "max-statements": "off", 94 | "max-statements-per-line": "off", 95 | "multiline-ternary": "error", 96 | "new-cap": "error", 97 | "new-parens": "error", 98 | "newline-after-var": "off", 99 | "newline-before-return": "off", 100 | "newline-per-chained-call": "error", 101 | "no-alert": "error", 102 | "no-array-constructor": "error", 103 | "no-await-in-loop": "error", 104 | "no-bitwise": "off", 105 | "no-caller": "error", 106 | "no-catch-shadow": "error", 107 | "no-confusing-arrow": "error", 108 | "no-continue": "off", 109 | "no-div-regex": "error", 110 | "no-duplicate-imports": "error", 111 | "no-else-return": "error", 112 | "no-empty": [ 113 | "error", 114 | { 115 | "allowEmptyCatch": true 116 | } 117 | ], 118 | "no-empty-function": "error", 119 | "no-eq-null": "error", 120 | "no-eval": "error", 121 | "no-extend-native": "error", 122 | "no-extra-bind": "error", 123 | "no-extra-label": "error", 124 | "no-extra-parens": "off", 125 | "no-floating-decimal": "error", 126 | "no-implicit-coercion": "error", 127 | "no-implicit-globals": "off", 128 | "no-implied-eval": "error", 129 | "no-inline-comments": "off", 130 | "no-inner-declarations": [ 131 | "error", 132 | "functions" 133 | ], 134 | "no-invalid-this": "off", 135 | "no-iterator": "error", 136 | "no-label-var": "error", 137 | "no-labels": "error", 138 | "no-lone-blocks": "error", 139 | "no-lonely-if": "error", 140 | "no-loop-func": "error", 141 | "no-magic-numbers": "off", 142 | "no-mixed-operators": "off", 143 | "no-mixed-requires": "error", 144 | "no-multi-assign": "error", 145 | "no-multi-spaces": "off", 146 | "no-multi-str": "off", 147 | "no-multiple-empty-lines": "off", 148 | "no-native-reassign": "error", 149 | "no-negated-condition": "error", 150 | "no-negated-in-lhs": "error", 151 | "no-nested-ternary": "error", 152 | "no-new": "error", 153 | "no-new-func": "error", 154 | "no-new-object": "error", 155 | "no-new-require": "error", 156 | "no-new-wrappers": "error", 157 | "no-octal-escape": "error", 158 | "no-param-reassign": "off", 159 | "no-path-concat": "error", 160 | "no-plusplus": [ 161 | "error", 162 | { 163 | "allowForLoopAfterthoughts": true 164 | } 165 | ], 166 | "no-process-env": "error", 167 | "no-process-exit": "error", 168 | "no-proto": "error", 169 | "no-prototype-builtins": "error", 170 | "no-restricted-globals": "error", 171 | "no-restricted-imports": "error", 172 | "no-restricted-modules": "error", 173 | "no-restricted-properties": "error", 174 | "no-restricted-syntax": "error", 175 | "no-return-assign": "error", 176 | "no-return-await": "error", 177 | "no-script-url": "error", 178 | "no-self-compare": "error", 179 | "no-sequences": "error", 180 | "no-shadow": "off", 181 | "no-shadow-restricted-names": "error", 182 | "no-spaced-func": "error", 183 | "no-sync": "error", 184 | "no-tabs": "error", 185 | "no-template-curly-in-string": "error", 186 | "no-ternary": "error", 187 | "no-throw-literal": "error", 188 | "no-trailing-spaces": "error", 189 | "no-undef-init": "error", 190 | "no-undefined": "off", 191 | "no-underscore-dangle": "off", 192 | "no-unmodified-loop-condition": "error", 193 | "no-unneeded-ternary": "error", 194 | "no-unused-expressions": "off", 195 | "no-use-before-define": "off", 196 | "no-useless-call": "error", 197 | "no-useless-computed-key": "error", 198 | "no-useless-concat": "error", 199 | "no-useless-constructor": "error", 200 | "no-useless-escape": "error", 201 | "no-useless-rename": "error", 202 | "no-useless-return": "off", 203 | "no-var": "error", 204 | "no-void": "error", 205 | "no-warning-comments": "off", 206 | "no-whitespace-before-property": "error", 207 | "no-with": "error", 208 | "object-curly-newline": "error", 209 | "object-curly-spacing": [ 210 | "error", 211 | "always" 212 | ], 213 | "object-property-newline": [ 214 | "error", 215 | { 216 | "allowMultiplePropertiesPerLine": true 217 | } 218 | ], 219 | "object-shorthand": "off", 220 | "one-var": "off", 221 | "one-var-declaration-per-line": "error", 222 | "operator-assignment": "off", 223 | "operator-linebreak": "error", 224 | "padded-blocks": "off", 225 | "prefer-arrow-callback": "off", 226 | "prefer-const": "error", 227 | "prefer-destructuring": [ 228 | "error", 229 | { 230 | "array": false, 231 | "object": false 232 | } 233 | ], 234 | "prefer-numeric-literals": "error", 235 | "prefer-promise-reject-errors": "error", 236 | "prefer-reflect": "off", 237 | "prefer-rest-params": "error", 238 | "prefer-spread": "error", 239 | "prefer-template": "off", 240 | "quote-props": "off", 241 | "quotes": "error", 242 | "radix": [ 243 | "error", 244 | "as-needed" 245 | ], 246 | "require-await": "error", 247 | "require-jsdoc": "off", 248 | "rest-spread-spacing": "error", 249 | "semi": "error", 250 | "semi-spacing": "error", 251 | "sort-imports": "error", 252 | "sort-keys": "off", 253 | "sort-vars": "error", 254 | "space-before-blocks": "error", 255 | "space-before-function-paren": [ 256 | "error", 257 | "never" 258 | ], 259 | "space-in-parens": "error", 260 | "space-infix-ops": [ 261 | "error", 262 | { 263 | "int32Hint": true 264 | } 265 | ], 266 | "space-unary-ops": [ 267 | "error", 268 | { 269 | "nonwords": false, 270 | "words": false 271 | } 272 | ], 273 | "spaced-comment": "error", 274 | "strict": "off", 275 | "symbol-description": "error", 276 | "template-curly-spacing": "error", 277 | "template-tag-spacing": "error", 278 | "unicode-bom": [ 279 | "error", 280 | "never" 281 | ], 282 | "valid-jsdoc": "off", 283 | "vars-on-top": "off", 284 | "wrap-iife": "error", 285 | "wrap-regex": "error", 286 | "yield-star-spacing": "error", 287 | "yoda": [ 288 | "error", 289 | "never" 290 | ] 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: yaacovzamir 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18.x, 20.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Use Node.js 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | 25 | - name: Install 26 | run: | 27 | npm install 28 | 29 | - name: Run tests 30 | run: | 31 | npm run test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # webstorm ide 2 | 3 | .idea/ 4 | 5 | # Logs 6 | logs 7 | *.log 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directory 31 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 32 | node_modules 33 | modbus-serial 34 | docs 35 | 36 | package-lock\.json 37 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Debug Current File", 9 | "type": "node", 10 | "request": "launch", 11 | "program": "${file}" 12 | }, 13 | { 14 | "type": "node", 15 | "request": "launch", 16 | "name": "Mocha single test", 17 | "program": "${workspaceFolder}/node_modules/mocha/bin/mocha.js", 18 | // current file 19 | "args": ["${file}"], 20 | "console": "internalConsole", 21 | "cwd": "${workspaceRoot}", 22 | "runtimeVersion": "16", 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2017, Yaacov Zamir 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /ModbusRTU.d.ts: -------------------------------------------------------------------------------- 1 | import { Socket, SocketConstructorOpts, TcpSocketConnectOpts } from 'net'; 2 | import { TestPort } from "./TestPort"; 3 | import { PortInfo } from '@serialport/bindings-cpp'; 4 | 5 | export class ModbusRTU { 6 | constructor(port?: any); 7 | static TestPort: typeof TestPort 8 | 9 | static getPorts(): Promise 10 | 11 | open(callback?: Function): void; 12 | close(callback?: Function): void; 13 | destroy(callback?: Function): void; 14 | 15 | writeFC1(address: number, dataAddress: number, length: number, next: NodeStyleCallback): void; 16 | writeFC2(address: number, dataAddress: number, length: number, next: NodeStyleCallback): void; 17 | writeFC3(address: number, dataAddress: number, length: number, next: NodeStyleCallback): void; 18 | writeFC4(address: number, dataAddress: number, length: number, next: NodeStyleCallback): void; 19 | writeFC5(address: number, dataAddress: number, state: boolean, next: NodeStyleCallback): void; 20 | writeFC6(address: number, dataAddress: number, value: number, next: NodeStyleCallback): void; 21 | 22 | writeFC15(address: number, dataAddress: number, states: Array, next: NodeStyleCallback): void; 23 | writeFC16(address: number, dataAddress: number, values: Array, next: NodeStyleCallback): void; 24 | writeFC22(address: number, dataAddress: number, andMask: number, orMask: number, next: NodeStyleCallback): void; 25 | 26 | // Connection shorthand API 27 | connectRTU(path: string, options: SerialPortOptions, next: Function): void; 28 | connectRTU(path: string, options: SerialPortOptions): Promise; 29 | connectTCP(ip: string, options: TcpPortOptions, next: Function): void; 30 | connectTCP(ip: string, options: TcpPortOptions): Promise; 31 | connectUDP(ip: string, options: UdpPortOptions, next: Function): void; 32 | connectUDP(ip: string, options: UdpPortOptions): Promise; 33 | connectTcpRTUBuffered(ip: string, options: TcpRTUPortOptions, next: Function): void; 34 | connectTcpRTUBuffered(ip: string, options: TcpRTUPortOptions): Promise; 35 | connectTelnet(ip: string, options: TelnetPortOptions, next: Function): void; 36 | connectTelnet(ip: string, options: TelnetPortOptions): Promise; 37 | connectC701(ip: string, options: C701PortOptions, next: Function): void; 38 | connectC701(ip: string, options: C701PortOptions): Promise; 39 | connectRTUBuffered(path: string, options: SerialPortOptions, next: Function): void; 40 | connectRTUBuffered(path: string, options: SerialPortOptions): Promise; 41 | connectAsciiSerial(path: string, options: SerialPortOptions, next: Function): void; 42 | connectAsciiSerial(path: string, options: SerialPortOptions): Promise; 43 | linkTCP(socket: Socket, options: TcpPortOptions, next: Function): void; 44 | linkTCP(socket: Socket, options: TcpPortOptions): Promise; 45 | linkTcpRTUBuffered(socket: Socket, options: TcpRTUPortOptions, next: Function): void; 46 | linkTcpRTUBuffered(socket: Socket, options: TcpRTUPortOptions): Promise; 47 | linkTelnet(socket: Socket, options: TelnetPortOptions, next: Function): void; 48 | linkTelnet(socket: Socket, options: TelnetPortOptions): Promise; 49 | connectRTUSocket(socket: Socket, next: Function): void; 50 | connectRTUSocket(socket: Socket): Promise; 51 | 52 | // Promise API 53 | setID(id: number): void; 54 | getID(): number; 55 | setTimeout(duration: number): void; 56 | getTimeout(): number; 57 | 58 | readCoils(dataAddress: number, length: number): Promise; 59 | readDiscreteInputs(dataAddress: number, length: number): Promise; 60 | readHoldingRegisters(dataAddress: number, length: number): Promise; 61 | readRegistersEnron(dataAddress: number, length: number): Promise; 62 | readInputRegisters(dataAddress: number, length: number): Promise; 63 | writeCoil(dataAddress: number, state: boolean): Promise; 64 | writeCoils(dataAddress: number, states: Array): Promise; 65 | writeRegister(dataAddress: number, value: number): Promise; 66 | writeRegisterEnron(dataAddress: number, value: number): Promise; 67 | writeRegisters(dataAddress: number, values: Array | Buffer): Promise; // 16 68 | maskWriteRegister(dataAddress: number, andMask: number, orMask: number): Promise; 69 | 70 | on(event: 'close', listener: () => unknown): this; 71 | on(event: 'error', listener: (error: unknown) => unknown): this; 72 | readDeviceIdentification(deviceIdCode: number, objectId: number): Promise; 73 | reportServerID(deviceIdCode: number): Promise; 74 | 75 | isOpen: boolean; 76 | } 77 | 78 | export interface NodeStyleCallback { 79 | (err: NodeJS.ErrnoException, param: T): void; 80 | } 81 | 82 | export interface ReadCoilResult { 83 | data: Array; 84 | buffer: Buffer; 85 | } 86 | 87 | export interface ReadRegisterResult { 88 | data: Array; 89 | buffer: Buffer; 90 | } 91 | 92 | export interface WriteCoilResult { 93 | address: number; 94 | state: boolean; 95 | } 96 | 97 | export interface WriteRegisterResult { 98 | address: number; 99 | value: number; 100 | } 101 | 102 | export interface WriteMultipleResult { 103 | address: number; 104 | length: number; 105 | } 106 | 107 | export interface WriteMaskRegisterResult { 108 | address: number; 109 | andMask: number; 110 | orMask: number; 111 | } 112 | 113 | export interface ReadDeviceIdentificationResult { 114 | data: string[]; 115 | conformityLevel: number; 116 | } 117 | 118 | export interface ReportServerIDResult { 119 | serverId: number; 120 | running: boolean; 121 | additionalData: Buffer; 122 | } 123 | 124 | export interface SerialPortOptions { 125 | baudRate?: number; 126 | dataBits?: number; 127 | stopBits?: number; 128 | parity?: 'none' | 'even' | 'mark' | 'odd' | 'space'; 129 | rtscts?: boolean; 130 | xon?: boolean; 131 | xoff?: boolean; 132 | xany?: boolean; 133 | flowControl?: boolean | Array; 134 | bufferSize?: number; 135 | parser?: any; 136 | platformOptions?: SerialPortUnixPlatformOptions; 137 | } 138 | 139 | export interface SerialPortUnixPlatformOptions { 140 | vmin?: number; 141 | vtime?: number; 142 | } 143 | 144 | export interface TcpPortOptions extends TcpSocketConnectOpts { 145 | port: number; 146 | localAddress?: string; 147 | family?: number; 148 | ip?: string; 149 | timeout?: number; 150 | socket?: Socket; 151 | socketOpts?: SocketConstructorOpts; 152 | } 153 | 154 | export interface UdpPortOptions { 155 | port?: number; 156 | localAddress?: string; 157 | family?: number; 158 | } 159 | 160 | export interface TcpRTUPortOptions { 161 | port?: number; 162 | localAddress?: string; 163 | family?: number; 164 | } 165 | 166 | export interface TelnetPortOptions { 167 | port?: number; 168 | timeout?: number; 169 | socket?: Socket; 170 | } 171 | 172 | export interface C701PortOptions { 173 | port?: number; 174 | } 175 | -------------------------------------------------------------------------------- /ServerSerial.d.ts: -------------------------------------------------------------------------------- 1 | import * as events from 'events'; 2 | import * as stream from 'stream'; 3 | import { SerialPortOpenOptions } from 'serialport'; 4 | import { FCallback, IServiceVector } from './ServerTCP'; 5 | import { ErrorCallback } from '@serialport/stream'; 6 | 7 | export class ServerSerial extends events.EventEmitter { 8 | socks: Map; 9 | constructor(vector: IServiceVector, options: IServerSerialOptions); 10 | close(cb: FCallback): void; 11 | } 12 | 13 | type IServerSerialOptions = SerialPortOpenOptions & { 14 | debug?: boolean, 15 | unitID?: number, 16 | openCallback?: ErrorCallback 17 | } 18 | 19 | export declare interface ServerSerial { 20 | on(event: 'socketError', listener: FCallback): this; 21 | on(event: 'error', listener: FCallback): this; 22 | on(event: 'initialized', listener: FCallback): this; 23 | } 24 | -------------------------------------------------------------------------------- /ServerTCP.d.ts: -------------------------------------------------------------------------------- 1 | import * as events from 'events'; 2 | import * as net from 'net'; 3 | 4 | export class ServerTCP extends events.EventEmitter { 5 | socks: Map 6 | constructor(vector: IServiceVector, options: IServerOptions); 7 | close(cb: FCallback): void; 8 | } 9 | 10 | export interface IServiceVector { 11 | getCoil?: 12 | ((addr: number, unitID: number, cb: FCallbackVal) => void) | 13 | ((addr: number, unitID: number) => Promise) | 14 | ((addr: number, unitID: number) => boolean); 15 | getDiscreteInput?: 16 | ((addr: number, unitID: number, cb: FCallbackVal) => void) | 17 | ((addr: number, unitID: number) => Promise) | 18 | ((addr: number, unitID: number) => boolean); 19 | getInputRegister?: 20 | ((addr: number, unitID: number, cb: FCallbackVal) => void) | 21 | ((addr: number, unitID: number) => Promise) | 22 | ((addr: number, unitID: number) => number); 23 | getHoldingRegister?: 24 | ((addr: number, unitID: number, cb: FCallbackVal) => void) | 25 | ((addr: number, unitID: number) => Promise) | 26 | ((addr: number, unitID: number) => number); 27 | getMultipleInputRegisters?: 28 | ((addr: number, length: number, unitID: number, cb: FCallbackVal) => void) | 29 | ((addr: number, length: number, unitID: number) => Promise) | 30 | ((addr: number, length: number, unitID: number) => number[]); 31 | getMultipleHoldingRegisters?: 32 | ((addr: number, length: number, unitID: number, cb: FCallbackVal) => void) | 33 | ((addr: number, length: number, unitID: number) => Promise) | 34 | ((addr: number, length: number, unitID: number) => number[]); 35 | setCoil?: 36 | ((addr: number, value: boolean, unitID: number, cb: FCallback) => void) | 37 | ((addr: number, value: boolean, unitID: number) => Promise) | 38 | ((addr: number, value: boolean, unitID: number) => void) 39 | setRegister?: 40 | ((addr: number, value: number, unitID: number, cb: FCallback) => void) | 41 | ((addr: number, value: number, unitID: number) => Promise) | 42 | ((addr: number, value: number, unitID: number) => void) 43 | setRegisterArray?: 44 | ((addr: number, value: number[], unitID: number, cb: FCallback) => void) | 45 | ((addr: number, value: number[], unitID: number) => Promise) | 46 | ((addr: number, value: number[], unitID: number) => void); 47 | } 48 | 49 | interface IServerOptions { 50 | host?: string, 51 | port?: number, 52 | debug?: boolean, 53 | unitID?: number, 54 | } 55 | 56 | export declare interface ServerTCP { 57 | on(event: 'socketError', listener: FCallback): this; 58 | on(event: 'serverError', listener: FCallback): this; 59 | on(event: 'error', listener: FCallback): this; 60 | on(event: 'initialized', listener: FCallback): this; 61 | } 62 | 63 | export type FCallbackVal = (err: Error | null, value: T) => void; 64 | export type FCallback = (err: Error | null) => void; 65 | -------------------------------------------------------------------------------- /TestPort.d.ts: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from "events"; 2 | export class TestPort extends EventEmitter { 3 | isOpen: () => boolean; 4 | open: (callback: (...args: any[]) => any) => void; 5 | close: (callback: (...args: any[]) => any) => void; 6 | write:(data:Buffer)=>void; 7 | } 8 | -------------------------------------------------------------------------------- /apis/promise.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Copyright (c) 2015, Yaacov Zamir 4 | * 5 | * Permission to use, copy, modify, and/or distribute this software for any 6 | * purpose with or without fee is hereby granted, provided that the above 7 | * copyright notice and this permission notice appear in all copies. 8 | * 9 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | */ 17 | 18 | /** 19 | * Take a modbus serial function and convert it to use promises. 20 | * 21 | * @param {Function} f the function to convert 22 | * @return a function that calls function "f" and return a promise. 23 | * @private 24 | */ 25 | const _convert = function(f) { 26 | const converted = function(...args) { 27 | const client = this; 28 | const id = this._unitID; 29 | 30 | // The last argument might be the callback (next) 31 | const next = args[args.length - 1]; 32 | 33 | // Determine if the last argument is actually a callback 34 | const hasCallback = typeof next === "function"; 35 | 36 | if (hasCallback) { 37 | // If there is a callback, call the function with the appropriate arguments 38 | if (args.length === 1) { 39 | // This case is used for client close method 40 | f.bind(client)(next); 41 | } else { 42 | // This case is used for client writeFC methods 43 | f.bind(client)(id, ...args); 44 | } 45 | } else { 46 | // Otherwise, use a promise 47 | return new Promise(function(resolve, reject) { 48 | function cb(err, data) { 49 | if (err) { 50 | reject(err); 51 | } else { 52 | resolve(data); 53 | } 54 | } 55 | 56 | if (args.length === 0) { 57 | // This case is used for client close method 58 | f.bind(client)(cb); 59 | } else { 60 | // This case is used for client writeFC methods 61 | f.bind(client)(id, ...args, cb); 62 | } 63 | }); 64 | } 65 | }; 66 | 67 | return converted; 68 | }; 69 | 70 | /** 71 | * Adds promise API to a Modbus object. 72 | * 73 | * @param {ModbusRTU} Modbus the ModbusRTU object. 74 | */ 75 | const addPromiseAPI = function(Modbus) { 76 | 77 | const cl = Modbus.prototype; 78 | 79 | // set/get unitID 80 | cl.setID = function(id) {this._unitID = Number(id);}; 81 | cl.getID = function() {return this._unitID;}; 82 | 83 | // set/get timeout 84 | cl.setTimeout = function(timeout) {this._timeout = timeout;}; 85 | cl.getTimeout = function() {return this._timeout;}; 86 | 87 | // convert functions to return promises 88 | cl.close = _convert(cl.close); 89 | cl.readCoils = _convert(cl.writeFC1); 90 | cl.readDiscreteInputs = _convert(cl.writeFC2); 91 | cl.readHoldingRegisters = _convert(cl.writeFC3); 92 | cl.readRegistersEnron = _convert(cl.writeFC3); 93 | cl.readInputRegisters = _convert(cl.writeFC4); 94 | cl.writeCoil = _convert(cl.writeFC5); 95 | cl.writeRegister = _convert(cl.writeFC6); 96 | cl.writeRegisterEnron = _convert(cl.writeFC6); 97 | cl.writeCoils = _convert(cl.writeFC15); 98 | cl.writeRegisters = _convert(cl.writeFC16); 99 | cl.reportServerID = _convert(cl.writeFC17); 100 | cl.readFileRecords = _convert(cl.writeFC20); 101 | cl.maskWriteRegister = _convert(cl.writeFC22); 102 | cl.readDeviceIdentification = _convert(cl.writeFC43); 103 | }; 104 | 105 | /** 106 | * Promise API Modbus library. 107 | * 108 | * @type {addPromiseAPI} 109 | */ 110 | module.exports = addPromiseAPI; 111 | -------------------------------------------------------------------------------- /apis/worker.js: -------------------------------------------------------------------------------- 1 | const Worker = require("../worker/index"); 2 | 3 | module.exports = function(Modbus) { 4 | const cl = Modbus.prototype; 5 | 6 | 7 | cl.setWorkerOptions = function(options) { 8 | if (this._worker) { 9 | this._worker.setOptions(options); 10 | } else { 11 | this._worker = new Worker(this, options); 12 | } 13 | }; 14 | 15 | cl.send = function(request) { 16 | if(!this._worker) { 17 | this._worker = new Worker(this); 18 | } 19 | 20 | return this._worker.send(request); 21 | }; 22 | 23 | cl.poll = function(options) { 24 | if(!this._worker) { 25 | this._worker = new Worker(this); 26 | } 27 | 28 | return this._worker.poll(options); 29 | }; 30 | 31 | }; 32 | -------------------------------------------------------------------------------- /examples/async_server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, no-unused-vars, spaced-comment */ 2 | 3 | // create an empty modbus client 4 | //let ModbusRTU = require("modbus-serial"); 5 | const ModbusRTU = require("../index"); 6 | const vector = { 7 | getInputRegister: function(addr, unitID) { 8 | // Synchronous handling 9 | return addr; 10 | }, 11 | getHoldingRegister: function(addr, unitID, callback) { 12 | // Asynchronous handling (with callback) 13 | setTimeout(function() { 14 | // callback = function(err, value) 15 | callback(null, addr + 8000); 16 | }, 10); 17 | }, 18 | getCoil: function(addr, unitID) { 19 | // Asynchronous handling (with Promises, async/await supported) 20 | return new Promise(function(resolve) { 21 | setTimeout(function() { 22 | resolve(addr % 2 === 0); 23 | }, 10); 24 | }); 25 | }, 26 | getDiscreteInput: function(addr, unitID) { 27 | // Asynchronous handling (with Promises, async/await supported) 28 | return new Promise(function(resolve) { 29 | setTimeout(function() { 30 | resolve(addr % 2 === 0); 31 | }, 10); 32 | }); 33 | }, 34 | setRegister: function(addr, value, unitID) { 35 | // Asynchronous handling supported also here 36 | console.log("set register", addr, value, unitID); 37 | return; 38 | }, 39 | setCoil: function(addr, value, unitID) { 40 | // Asynchronous handling supported also here 41 | console.log("set coil", addr, value, unitID); 42 | return; 43 | }, 44 | readDeviceIdentification: function(unitID) { 45 | return new Promise(function(resolve) { 46 | setTimeout(function() { 47 | resolve({ 48 | 0x00: "MyVendorName", 49 | 0x01: "MyProductCode", 50 | 0x02: "MyMajorMinorRevision", 51 | 0x05: "MyModelName", 52 | 0x97: "MyExtendedObject1", 53 | 0xAB: "MyExtendedObject2" 54 | }); 55 | }, 10); 56 | }); 57 | } 58 | }; 59 | 60 | // set the server to answer for modbus requests 61 | console.log("ModbusTCP listening on modbus://0.0.0.0:8502"); 62 | const serverTCP = new ModbusRTU.ServerTCP(vector, { 63 | host: "0.0.0.0", 64 | port: 8502, 65 | debug: true, 66 | unitID: 1 67 | }); 68 | 69 | serverTCP.on("socketError", function(err) { 70 | // Handle socket error if needed, can be ignored 71 | console.error(err); 72 | }); 73 | -------------------------------------------------------------------------------- /examples/buffer.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, spaced-comment */ 2 | 3 | // create an empty modbus client 4 | //let ModbusRTU = require("modbus-serial"); 5 | const ModbusRTU = require("../index"); 6 | const client = new ModbusRTU(); 7 | 8 | // open connection to a serial port 9 | //client.connectRTUBuffered("/dev/ttyUSB0", {baudRate: 9600}) 10 | client.connectTCP("127.0.0.1", { port: 8502 }) 11 | .then(setClient) 12 | .then(function() { 13 | console.log("Connected"); }) 14 | .catch(function(e) { 15 | console.log(e.message); }); 16 | 17 | function setClient() { 18 | // set the client's unit id 19 | // set a timout for requests default is null (no timeout) 20 | client.setID(1); 21 | client.setTimeout(1000); 22 | 23 | // run program 24 | run(); 25 | } 26 | 27 | function run() { 28 | client.readInputRegisters(4, 12) 29 | .then(function(d) { 30 | const floatA = d.buffer.readFloatBE(0); 31 | const floatB = d.buffer.readFloatBE(4); 32 | const floatC = d.buffer.readFloatBE(8); 33 | console.log("Receive:", floatA, floatB, floatC); }) 34 | .catch(function(e) { 35 | console.log(e.message); }) 36 | .then(close); 37 | } 38 | 39 | function close() { 40 | client.close(); 41 | } 42 | -------------------------------------------------------------------------------- /examples/buffered_server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, no-unused-vars, spaced-comment */ 2 | 3 | // create an empty modbus client 4 | //let ModbusRTU = require("modbus-serial"); 5 | const ModbusRTU = require("../index"); 6 | 7 | const coils = Buffer.alloc(160008, 0); // coils and discrete inputs 8 | const regsiters = Buffer.alloc(160008, 0); // input and holding registers 9 | 10 | const unitId = 1; 11 | const minAddress = 0; 12 | const maxInputAddress = 10001; 13 | const maxAddress = 20001; 14 | const bufferFactor = 8; 15 | 16 | // 1...10000* address - 1 Coils (outputs) 0 Read/Write 17 | // 10001...20000* address - 10001 Discrete Inputs 01 Read 18 | // 30001...40000* address - 30001 Input Registers 04 Read 19 | // 40001...50000* address - 40001 Holding Registers 03 Read/Write 20 | 21 | const vector = { 22 | getCoil: function(addr, unitID) { if (unitID === unitId && addr >= minAddress && addr < maxAddress) { return coils.readUInt8(addr * bufferFactor); } }, 23 | getInputRegister: function(addr, unitID) { if (unitID === unitId && addr >= minAddress && addr < maxInputAddress) { return regsiters.readUInt16BE(addr * bufferFactor); } }, 24 | getHoldingRegister: function(addr, unitID) { if (unitID === unitId && addr >= maxInputAddress && addr < maxAddress) { return regsiters.readUInt16BE(addr * bufferFactor); } }, 25 | 26 | setCoil: function(addr, value, unitID) { if (unitID === unitId && addr >= minAddress && addr < maxAddress) { coils.writeUInt8(value, addr * bufferFactor); } }, 27 | setRegister: function(addr, value, unitID) { if (unitID === unitId && addr >= minAddress && addr < maxAddress) { regsiters.writeUInt16BE(value, addr * bufferFactor); } } 28 | }; 29 | 30 | // set the server to answer for modbus requests 31 | console.log("ModbusTCP listening on modbus://0.0.0.0:8502"); 32 | const serverTCP = new ModbusRTU.ServerTCP(vector, { host: "0.0.0.0", port: 8502, debug: true, unitID: 1 }); 33 | 34 | serverTCP.on("socketError", function(err) { 35 | console.error(err); 36 | serverTCP.close(closed); 37 | }); 38 | 39 | function closed() { 40 | console.log("server closed"); 41 | } 42 | -------------------------------------------------------------------------------- /examples/buffertcp.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, spaced-comment */ 2 | 3 | // create an empty modbus client 4 | //let ModbusRTU = require("modbus-serial"); 5 | const ModbusRTU = require("../index"); 6 | const client = new ModbusRTU(); 7 | 8 | // NPort Gateway 801D NetPort 9 | client.connectTcpRTUBuffered("127.0.0.1", { port: 8502 }) 10 | .then(setClient) 11 | .then(function() { 12 | console.log("Connected"); 13 | }) 14 | .catch(function(e) { 15 | console.log(e.message); 16 | }); 17 | 18 | function setClient() { 19 | // set the client's unit id 20 | // set a timout for requests default is null (no timeout) 21 | client.setID(1); 22 | client.setTimeout(2000); 23 | 24 | // run program 25 | run(); 26 | } 27 | 28 | function run() { 29 | // read the 4 registers starting at address 5 30 | client.readDiscreteInputs(0, 22) 31 | .then(function(d) { 32 | console.log("Receive:", d.data); 33 | }) 34 | .catch(function(e) { 35 | console.log(e.message); 36 | }) 37 | .then(close); 38 | } 39 | 40 | function close() { 41 | client.close(); 42 | } 43 | -------------------------------------------------------------------------------- /examples/debug.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, spaced-comment */ 2 | 3 | /* DEBUG environment variable is used to enable debug logging 4 | 5 | Set the DEBUG variable to be "modbus-serial" before running this example: 6 | export DEBUG=modbus-serial 7 | 8 | To stop debugging, unset the DEBUG variable: 9 | unset DEBUG 10 | */ 11 | 12 | // create an empty modbus client 13 | //let ModbusRTU = require("modbus-serial"); 14 | const ModbusRTU = require("../index"); 15 | const client = new ModbusRTU(); 16 | 17 | // open connection to a serial port 18 | //client.connectRTUBuffered("/dev/ttyUSB0", {baudRate: 9600}) 19 | client.connectTCP("127.0.0.1", { port: 8502 }) 20 | .then(setClient) 21 | .then(function() { 22 | console.log("Connected"); }) 23 | .catch(function(e) { 24 | console.log(e.message); }); 25 | 26 | function setClient() { 27 | // set the client's unit id 28 | // set a timout for requests default is null (no timeout) 29 | client.setID(1); 30 | client.setTimeout(1000); 31 | 32 | // run program 33 | run(); 34 | } 35 | 36 | function run() { 37 | // read the 4 registers starting at address 5 38 | client.readHoldingRegisters(5, 4) 39 | .then(function(d) { 40 | console.log("Receive:", d.data); }) 41 | .catch(function(e) { 42 | console.log(e.message); }) 43 | .then(close); 44 | } 45 | 46 | function close() { 47 | client.close(); 48 | } 49 | -------------------------------------------------------------------------------- /examples/device-identification.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, spaced-comment */ 2 | 3 | // create an empty modbus client 4 | //let ModbusRTU = require("modbus-serial"); 5 | const ModbusRTU = require("../index"); 6 | const client = new ModbusRTU(); 7 | 8 | const networkErrors = ["ESOCKETTIMEDOUT", "ETIMEDOUT", "ECONNRESET", "ECONNREFUSED", "EHOSTUNREACH"]; 9 | 10 | // open connection to a serial port 11 | //client.connectRTUBuffered("/dev/ttyUSB0", { hupcl: false, dsrdtr: false }) 12 | client.connectTCP("127.0.0.1", { port: 8502 }) 13 | .then(setClient) 14 | .then(function() { 15 | console.log("Connected"); }) 16 | .catch(function(e) { 17 | if(e.errno) { 18 | if(networkErrors.includes(e.errno)) { 19 | console.log("we have to reconnect"); 20 | } 21 | } 22 | console.log(e.message); }); 23 | 24 | function setClient() { 25 | // set the client's unit id 26 | // set a timout for requests default is null (no timeout) 27 | client.setID(1); 28 | client.setTimeout(1000); 29 | 30 | // run program 31 | readDeviceIdentification(); 32 | } 33 | 34 | function readDeviceIdentification() { 35 | // read the 4 registers starting at address 5 36 | client.readDeviceIdentification(1, 0) 37 | .then(function(d) { 38 | console.log("Receive:", d.data); }) 39 | .catch(function(e) { 40 | console.log(e.message); }) 41 | .then(close); 42 | } 43 | 44 | function close() { 45 | client.close(); 46 | } 47 | -------------------------------------------------------------------------------- /examples/logger.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, spaced-comment */ 2 | 3 | // create an empty modbus client 4 | //let ModbusRTU = require("modbus-serial"); 5 | const ModbusRTU = require("../index"); 6 | let client = new ModbusRTU(); 7 | let timeoutRunRef = null; 8 | let timeoutConnectRef = null; 9 | 10 | const networkErrors = [ 11 | "ESOCKETTIMEDOUT", 12 | "ETIMEDOUT", 13 | "ECONNRESET", 14 | "ECONNREFUSED", 15 | "EHOSTUNREACH", 16 | "ENETRESET", 17 | "ECONNABORTED", 18 | "ENETUNREACH", 19 | "ENOTCONN", 20 | "ESHUTDOWN", 21 | "EHOSTDOWN", 22 | "ENETDOWN", 23 | "EWOULDBLOCK", 24 | "EAGAIN" 25 | ]; 26 | 27 | 28 | // check error, and reconnect if needed 29 | function checkError(e) { 30 | if(e.errno && networkErrors.includes(e.errno)) { 31 | console.log("we have to reconnect"); 32 | 33 | // close port 34 | client.close(); 35 | 36 | // re open client 37 | client = new ModbusRTU(); 38 | timeoutConnectRef = setTimeout(connect, 1000); 39 | } 40 | } 41 | 42 | // open connection to a serial port 43 | function connect() { 44 | // clear pending timeouts 45 | clearTimeout(timeoutConnectRef); 46 | 47 | // if client already open, just run 48 | if (client.isOpen) { 49 | run(); 50 | } 51 | 52 | // if client closed, open a new connection 53 | client.connectTCP("127.0.0.1", { port: 8502 }) 54 | .then(setClient) 55 | .then(function() { 56 | console.log("Connected"); }) 57 | .catch(function(e) { 58 | checkError(e); 59 | console.log(e.message); }); 60 | } 61 | 62 | function setClient() { 63 | // set the client's unit id 64 | // set a timout for requests default is null (no timeout) 65 | client.setID(1); 66 | client.setTimeout(1000); 67 | 68 | // run program 69 | run(); 70 | } 71 | 72 | function run() { 73 | // clear pending timeouts 74 | clearTimeout(timeoutRunRef); 75 | 76 | // read the 4 registers starting at address 5 77 | client.readHoldingRegisters(5, 4) 78 | .then(function(d) { 79 | console.log("Receive:", d.data); }) 80 | .then(function() { 81 | timeoutRunRef = setTimeout(run, 1000); }) 82 | .catch(function(e) { 83 | checkError(e); 84 | console.log(e.message); }); 85 | } 86 | 87 | // connect and start logging 88 | connect(); 89 | -------------------------------------------------------------------------------- /examples/logger_complete.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, spaced-comment */ 2 | 3 | // create an empty modbus client 4 | //let ModbusRTU = require("modbus-serial"); 5 | const ModbusRTU = require("../index"); 6 | let client = new ModbusRTU(); 7 | let timeoutRunRefCoils = null; 8 | let timeoutRunRefDiscreteInputs = null; 9 | let timeoutRunRefInputs = null; 10 | let timeoutRunRefHoldings = null; 11 | let timeoutConnectRef = null; 12 | 13 | const networkErrors = [ 14 | "ESOCKETTIMEDOUT", 15 | "ETIMEDOUT", 16 | "ECONNRESET", 17 | "ECONNREFUSED", 18 | "EHOSTUNREACH", 19 | "ENETRESET", 20 | "ECONNABORTED", 21 | "ENETUNREACH", 22 | "ENOTCONN", 23 | "ESHUTDOWN", 24 | "EHOSTDOWN", 25 | "ENETDOWN", 26 | "EWOULDBLOCK", 27 | "EAGAIN" 28 | ]; 29 | 30 | 31 | // check error, and reconnect if needed 32 | function checkError(e) { 33 | if(e.errno && networkErrors.includes(e.errno)) { 34 | console.log("we have to reconnect"); 35 | 36 | // close port 37 | client.close(); 38 | 39 | // re open client 40 | client = new ModbusRTU(); 41 | timeoutConnectRef = setTimeout(connect, 1000); 42 | } 43 | } 44 | 45 | // open connection to a serial port 46 | function connect() { 47 | // clear pending timeouts 48 | clearTimeout(timeoutConnectRef); 49 | 50 | // if client already open, just run 51 | if (client.isOpen) { 52 | run(); 53 | } 54 | 55 | // if client closed, open a new connection 56 | client.connectTCP("127.0.0.1", { port: 8502 }) 57 | .then(setClient) 58 | .then(function() { 59 | console.log("Connected"); }) 60 | .catch(function(e) { 61 | checkError(e); 62 | console.log(e.message); }); 63 | } 64 | 65 | function setClient() { 66 | // set the client's unit id 67 | // set a timout for requests default is null (no timeout) 68 | client.setID(1); 69 | client.setTimeout(3000); 70 | 71 | // run program 72 | run(); 73 | } 74 | 75 | function run() { 76 | // clear pending timeouts 77 | clearTimeout(timeoutRunRefCoils); 78 | 79 | client.readCoils(1, 5) 80 | .then(function(d) { 81 | console.log("Receive Coils:", d.data); }) 82 | .then(function() { 83 | timeoutRunRefCoils = setTimeout(readDiscreteInputs, 1000); 84 | }) 85 | .catch(function(e) { 86 | checkError(e); 87 | console.log(e.message); }); 88 | } 89 | 90 | function readDiscreteInputs() { 91 | clearTimeout(timeoutRunRefDiscreteInputs); 92 | 93 | client.readDiscreteInputs(10001, 5) 94 | .then(function(d) { 95 | console.log("Receive Discrete Inputs:", d.data); }) 96 | .then(function() { 97 | timeoutRunRefDiscreteInputs = setTimeout(readInputRegisters, 1000); }) 98 | .catch(function(e) { 99 | checkError(e); 100 | console.log(e.message); }); 101 | } 102 | 103 | function readInputRegisters() { 104 | clearTimeout(timeoutRunRefInputs); 105 | 106 | client.readInputRegisters(1, 5) 107 | .then(function(d) { 108 | console.log("Receive Inputs:", d.data); }) 109 | .then(function() { 110 | timeoutRunRefInputs = setTimeout(readHoldingRegisters, 1000); }) 111 | .catch(function(e) { 112 | checkError(e); 113 | console.log(e.message); }); 114 | } 115 | 116 | function readHoldingRegisters() { 117 | clearTimeout(timeoutRunRefHoldings); 118 | 119 | client.readHoldingRegisters(10001, 5) 120 | .then(function(d) { 121 | console.log("Receive Holding Registers:", d.data); }) 122 | .then(function() { 123 | timeoutRunRefHoldings = setTimeout(run, 1000); }) 124 | .catch(function(e) { 125 | checkError(e); 126 | console.log(e.message); 127 | }); 128 | } 129 | 130 | // connect and start logging 131 | connect(); 132 | -------------------------------------------------------------------------------- /examples/polling_RTU.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, spaced-comment, func-call-spacing, no-spaced-func */ 2 | 3 | //============================================================== 4 | // This is an example of polling (reading) Holding Registers 5 | // on a regular scan interval with timeouts enabled. 6 | // For robust behaviour, the next action is not activated 7 | // until the previous action is completed (callback served). 8 | //============================================================== 9 | 10 | "use strict"; 11 | 12 | //============================================================== 13 | // create an empty modbus client 14 | const ModbusRTU = require ("modbus-serial"); 15 | const client = new ModbusRTU(); 16 | 17 | let mbsStatus = "Initializing..."; // holds a status of Modbus 18 | 19 | // Modbus 'state' constants 20 | const MBS_STATE_INIT = "State init"; 21 | const MBS_STATE_IDLE = "State idle"; 22 | const MBS_STATE_NEXT = "State next"; 23 | const MBS_STATE_GOOD_READ = "State good (read)"; 24 | const MBS_STATE_FAIL_READ = "State fail (read)"; 25 | const MBS_STATE_GOOD_CONNECT = "State good (port)"; 26 | const MBS_STATE_FAIL_CONNECT = "State fail (port)"; 27 | 28 | // Modbus configuration values 29 | const mbsId = 1; 30 | const mbsScan = 1000; 31 | const mbsTimeout = 5000; 32 | let mbsState = MBS_STATE_INIT; 33 | 34 | // Upon SerialPort error 35 | client.on("error", function(error) { 36 | console.log("SerialPort Error: ", error); 37 | }); 38 | 39 | 40 | //============================================================== 41 | const connectClient = function() 42 | { 43 | // set requests parameters 44 | client.setID (mbsId); 45 | client.setTimeout (mbsTimeout); 46 | 47 | // try to connect 48 | client.connectRTUBuffered ("COM9", { baudRate: 9600, parity: "even", dataBits: 8, stopBits: 1 }) 49 | .then(function() 50 | { 51 | mbsState = MBS_STATE_GOOD_CONNECT; 52 | mbsStatus = "Connected, wait for reading..."; 53 | console.log(mbsStatus); 54 | }) 55 | .catch(function(e) 56 | { 57 | mbsState = MBS_STATE_FAIL_CONNECT; 58 | mbsStatus = e.message; 59 | console.log(e); 60 | }); 61 | }; 62 | 63 | 64 | //============================================================== 65 | const readModbusData = function() 66 | { 67 | // try to read data 68 | client.readHoldingRegisters (5, 1) 69 | .then(function(data) 70 | { 71 | mbsState = MBS_STATE_GOOD_READ; 72 | mbsStatus = "success"; 73 | console.log(data.buffer); 74 | }) 75 | .catch(function(e) 76 | { 77 | mbsState = MBS_STATE_FAIL_READ; 78 | mbsStatus = e.message; 79 | console.log(e); 80 | }); 81 | }; 82 | 83 | 84 | //============================================================== 85 | const runModbus = function() 86 | { 87 | let nextAction; 88 | 89 | switch (mbsState) 90 | { 91 | case MBS_STATE_INIT: 92 | nextAction = connectClient; 93 | break; 94 | 95 | case MBS_STATE_NEXT: 96 | nextAction = readModbusData; 97 | break; 98 | 99 | case MBS_STATE_GOOD_CONNECT: 100 | nextAction = readModbusData; 101 | break; 102 | 103 | case MBS_STATE_FAIL_CONNECT: 104 | nextAction = connectClient; 105 | break; 106 | 107 | case MBS_STATE_GOOD_READ: 108 | nextAction = readModbusData; 109 | break; 110 | 111 | case MBS_STATE_FAIL_READ: 112 | if (client.isOpen) { mbsState = MBS_STATE_NEXT; } 113 | else { nextAction = connectClient; } 114 | break; 115 | 116 | default: 117 | // nothing to do, keep scanning until actionable case 118 | } 119 | 120 | console.log(); 121 | console.log(nextAction); 122 | 123 | // execute "next action" function if defined 124 | if (nextAction !== undefined) 125 | { 126 | nextAction(); 127 | mbsState = MBS_STATE_IDLE; 128 | } 129 | 130 | // set for next run 131 | setTimeout (runModbus, mbsScan); 132 | }; 133 | 134 | //============================================================== 135 | runModbus(); 136 | -------------------------------------------------------------------------------- /examples/polling_TCP.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, spaced-comment, func-call-spacing, no-spaced-func */ 2 | 3 | //============================================================== 4 | // This is an example of polling (reading) Holding Registers 5 | // on a regular scan interval with timeouts enabled. 6 | // For robust behaviour, the next action is not activated 7 | // until the previous action is completed (callback served). 8 | //============================================================== 9 | 10 | "use strict"; 11 | 12 | //============================================================== 13 | // create an empty modbus client 14 | const ModbusRTU = require ("modbus-serial"); 15 | const client = new ModbusRTU(); 16 | 17 | let mbsStatus = "Initializing..."; // holds a status of Modbus 18 | 19 | // Modbus 'state' constants 20 | const MBS_STATE_INIT = "State init"; 21 | const MBS_STATE_IDLE = "State idle"; 22 | const MBS_STATE_NEXT = "State next"; 23 | const MBS_STATE_GOOD_READ = "State good (read)"; 24 | const MBS_STATE_FAIL_READ = "State fail (read)"; 25 | const MBS_STATE_GOOD_CONNECT = "State good (port)"; 26 | const MBS_STATE_FAIL_CONNECT = "State fail (port)"; 27 | 28 | // Modbus TCP configuration values 29 | const mbsId = 1; 30 | const mbsPort = 502; 31 | const mbsHost = "192.168.20.2"; 32 | const mbsScan = 1000; 33 | const mbsTimeout = 5000; 34 | let mbsState = MBS_STATE_INIT; 35 | 36 | 37 | //============================================================== 38 | const connectClient = function() 39 | { 40 | // close port (NOTE: important in order not to create multiple connections) 41 | client.close(); 42 | 43 | // set requests parameters 44 | client.setID (mbsId); 45 | client.setTimeout (mbsTimeout); 46 | 47 | // try to connect 48 | client.connectTCP (mbsHost, { port: mbsPort }) 49 | .then(function() 50 | { 51 | mbsState = MBS_STATE_GOOD_CONNECT; 52 | mbsStatus = "Connected, wait for reading..."; 53 | console.log(mbsStatus); 54 | }) 55 | .catch(function(e) 56 | { 57 | mbsState = MBS_STATE_FAIL_CONNECT; 58 | mbsStatus = e.message; 59 | console.log(e); 60 | }); 61 | 62 | }; 63 | 64 | 65 | //============================================================== 66 | const readModbusData = function() 67 | { 68 | // try to read data 69 | client.readHoldingRegisters (0, 18) 70 | .then(function(data) 71 | { 72 | mbsState = MBS_STATE_GOOD_READ; 73 | mbsStatus = "success"; 74 | console.log(data.buffer); 75 | }) 76 | .catch(function(e) 77 | { 78 | mbsState = MBS_STATE_FAIL_READ; 79 | mbsStatus = e.message; 80 | console.log(e); 81 | }); 82 | }; 83 | 84 | 85 | //============================================================== 86 | const runModbus = function() 87 | { 88 | let nextAction; 89 | 90 | switch (mbsState) 91 | { 92 | case MBS_STATE_INIT: 93 | nextAction = connectClient; 94 | break; 95 | 96 | case MBS_STATE_NEXT: 97 | nextAction = readModbusData; 98 | break; 99 | 100 | case MBS_STATE_GOOD_CONNECT: 101 | nextAction = readModbusData; 102 | break; 103 | 104 | case MBS_STATE_FAIL_CONNECT: 105 | nextAction = connectClient; 106 | break; 107 | 108 | case MBS_STATE_GOOD_READ: 109 | nextAction = readModbusData; 110 | break; 111 | 112 | case MBS_STATE_FAIL_READ: 113 | if (client.isOpen) { mbsState = MBS_STATE_NEXT; } 114 | else { nextAction = connectClient; } 115 | break; 116 | 117 | default: 118 | // nothing to do, keep scanning until actionable case 119 | } 120 | 121 | console.log(); 122 | console.log(nextAction); 123 | 124 | // execute "next action" function if defined 125 | if (nextAction !== undefined) 126 | { 127 | nextAction(); 128 | mbsState = MBS_STATE_IDLE; 129 | } 130 | 131 | // set for next run 132 | setTimeout (runModbus, mbsScan); 133 | }; 134 | 135 | //============================================================== 136 | runModbus(); 137 | -------------------------------------------------------------------------------- /examples/polling_UDP.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, spaced-comment, func-call-spacing, no-spaced-func */ 2 | 3 | //============================================================== 4 | // This is an example of polling (reading) Holding Registers 5 | // on a regular scan interval with timeouts enabled. 6 | // For robust behaviour, the next action is not activated 7 | // until the previous action is completed (callback served). 8 | //============================================================== 9 | 10 | "use strict"; 11 | 12 | //============================================================== 13 | // create an empty modbus client 14 | const ModbusRTU = require ("modbus-serial"); 15 | const client = new ModbusRTU(); 16 | 17 | let mbsStatus = "Initializing..."; // holds a status of Modbus 18 | 19 | // Modbus 'state' constants 20 | const MBS_STATE_INIT = "State init"; 21 | const MBS_STATE_IDLE = "State idle"; 22 | const MBS_STATE_NEXT = "State next"; 23 | const MBS_STATE_GOOD_READ = "State good (read)"; 24 | const MBS_STATE_FAIL_READ = "State fail (read)"; 25 | const MBS_STATE_GOOD_CONNECT = "State good (port)"; 26 | const MBS_STATE_FAIL_CONNECT = "State fail (port)"; 27 | 28 | // Modbus UDP configuration values 29 | const mbsId = 1; 30 | const mbsPort = 502; 31 | const mbsHost = "192.168.1.11"; 32 | const mbsScan = 1000; 33 | const mbsTimeout = 5000; 34 | let mbsState = MBS_STATE_INIT; 35 | 36 | 37 | //============================================================== 38 | const connectClient = function() 39 | { 40 | // close port (NOTE: important in order not to create multiple connections) 41 | client.close(() => { 42 | console.log("Client closed"); 43 | }); 44 | 45 | // set requests parameters 46 | client.setID (mbsId); 47 | client.setTimeout (mbsTimeout); 48 | 49 | // try to connect 50 | client.connectUDP (mbsHost, { port: mbsPort }) 51 | .then(function() 52 | { 53 | mbsState = MBS_STATE_GOOD_CONNECT; 54 | mbsStatus = "Connected, wait for reading..."; 55 | console.log(mbsStatus); 56 | }) 57 | .catch(function(e) 58 | { 59 | mbsState = MBS_STATE_FAIL_CONNECT; 60 | mbsStatus = e.message; 61 | console.log(e); 62 | }); 63 | 64 | }; 65 | 66 | 67 | //============================================================== 68 | const readModbusData = function() 69 | { 70 | // try to read data 71 | client.readHoldingRegisters (0, 18) 72 | .then(function(data) 73 | { 74 | mbsState = MBS_STATE_GOOD_READ; 75 | mbsStatus = "success"; 76 | console.log(data.buffer); 77 | }) 78 | .catch(function(e) 79 | { 80 | mbsState = MBS_STATE_FAIL_READ; 81 | mbsStatus = e.message; 82 | console.log(e); 83 | }); 84 | }; 85 | 86 | 87 | //============================================================== 88 | const runModbus = function() 89 | { 90 | let nextAction; 91 | 92 | switch (mbsState) 93 | { 94 | case MBS_STATE_INIT: 95 | nextAction = connectClient; 96 | break; 97 | 98 | case MBS_STATE_NEXT: 99 | nextAction = readModbusData; 100 | break; 101 | 102 | case MBS_STATE_GOOD_CONNECT: 103 | nextAction = readModbusData; 104 | break; 105 | 106 | case MBS_STATE_FAIL_CONNECT: 107 | nextAction = connectClient; 108 | break; 109 | 110 | case MBS_STATE_GOOD_READ: 111 | nextAction = readModbusData; 112 | break; 113 | 114 | case MBS_STATE_FAIL_READ: 115 | if (client.isOpen) { mbsState = MBS_STATE_NEXT; } 116 | else { nextAction = connectClient; } 117 | break; 118 | 119 | default: 120 | // nothing to do, keep scanning until actionable case 121 | } 122 | 123 | console.log(); 124 | console.log(nextAction); 125 | 126 | // execute "next action" function if defined 127 | if (nextAction !== undefined) 128 | { 129 | nextAction(); 130 | mbsState = MBS_STATE_IDLE; 131 | } 132 | 133 | // set for next run 134 | setTimeout (runModbus, mbsScan); 135 | }; 136 | 137 | //============================================================== 138 | runModbus(); 139 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, no-unused-vars, spaced-comment */ 2 | 3 | // create an empty modbus client 4 | //let ModbusRTU = require("modbus-serial"); 5 | const ModbusRTU = require("../index"); 6 | const vector = { 7 | getInputRegister: function(addr) { return addr; }, 8 | getHoldingRegister: function(addr) { return addr + 8000; }, 9 | getMultipleInputRegisters: function(startAddr, length) { 10 | const values = []; 11 | for (let i = 0; i < length; i++) { 12 | values[i] = startAddr + i; 13 | } 14 | return values; 15 | }, 16 | getMultipleHoldingRegisters: function(startAddr, length) { 17 | const values = []; 18 | for (let i = 0; i < length; i++) { 19 | values[i] = startAddr + i + 8000; 20 | } 21 | return values; 22 | }, 23 | getCoil: function(addr) { return (addr % 2) === 0; }, 24 | setRegister: function(addr, value) { console.log("set register", addr, value); return; }, 25 | setCoil: function(addr, value) { console.log("set coil", addr, value); return; }, 26 | readDeviceIdentification: function(addr) { 27 | return { 28 | 0x00: "MyVendorName", 29 | 0x01: "MyProductCode", 30 | 0x02: "MyMajorMinorRevision", 31 | 0x05: "MyModelName", 32 | 0x97: "MyExtendedObject1", 33 | 0xAB: "MyExtendedObject2" 34 | }; 35 | } 36 | }; 37 | 38 | // set the server to answer for modbus requests 39 | console.log("ModbusTCP listening on modbus://0.0.0.0:8502"); 40 | const serverTCP = new ModbusRTU.ServerTCP(vector, { host: "0.0.0.0", port: 8502, debug: true, unitID: 1 }); 41 | 42 | serverTCP.on("initialized", function() { 43 | console.log("initialized"); 44 | }); 45 | 46 | serverTCP.on("socketError", function(err) { 47 | console.error(err); 48 | serverTCP.close(closed); 49 | }); 50 | 51 | function closed() { 52 | console.log("server closed"); 53 | } 54 | -------------------------------------------------------------------------------- /examples/server_enron.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, no-unused-vars, spaced-comment */ 2 | 3 | const ModbusRTU = require("../index"); 4 | const holdingRegisters = {}; 5 | const coils = {}; 6 | const inputRegisters = {}; 7 | const discreteInputs = {}; 8 | 9 | const vector = { 10 | getInputRegister: function(addr) { 11 | return inputRegisters[addr]; 12 | }, 13 | getMultipleInputRegisters: function(startAddr, length) { 14 | const values = []; 15 | for (let i = 0; i < length; i++) { 16 | values[i] = inputRegisters[startAddr + i]; 17 | } 18 | return values; 19 | }, 20 | getDiscreteInput: function(addr) { 21 | return discreteInputs[addr]; 22 | }, 23 | getHoldingRegister: function(addr) { 24 | return holdingRegisters[addr]; 25 | }, 26 | setRegister: function(addr, value) { 27 | holdingRegisters[addr] = value; 28 | return; 29 | }, 30 | getMultipleHoldingRegisters: function(startAddr, length) { 31 | const values = []; 32 | for (let i = 0; i < length; i++) { 33 | values[i] = holdingRegisters[startAddr + i]; 34 | } 35 | return values; 36 | }, 37 | getCoil: function(addr) { 38 | return coils[addr]; 39 | }, 40 | setCoil: function(addr, value) { 41 | coils[addr] = value; 42 | return coils[addr]; 43 | }, 44 | readDeviceIdentification: function() { 45 | return { 46 | 0x00: "MyVendorName", 47 | 0x01: "MyProductCode", 48 | 0x02: "MyMajorMinorRevision", 49 | 0x05: "MyModelName", 50 | 0x97: "MyExtendedObject1", 51 | 0xab: "MyExtendedObject2" 52 | }; 53 | } 54 | }; 55 | 56 | // set the server to answer for modbus requests 57 | console.log("ModbusTCP listening on modbus://0.0.0.0:8502"); 58 | // set the server to answer for modbus requests 59 | const serverTCP = new ModbusRTU.ServerTCP( 60 | vector, 61 | { 62 | host: "0.0.0.0", 63 | port: 8502, 64 | debug: true, 65 | unitID: 1, 66 | enron: true, 67 | enronTables: { 68 | booleanRange: [1001, 1999], 69 | shortRange: [3001, 3999], 70 | longRange: [5001, 5999], 71 | floatRange: [7001, 7999] 72 | } 73 | } 74 | ); 75 | 76 | serverTCP.on("error", function(err) { 77 | // Handle socket error if needed, can be ignored 78 | console.error(err); 79 | }); 80 | 81 | serverTCP.on("initialized", function() { 82 | console.log("initialized"); 83 | }); 84 | 85 | serverTCP.on("socketError", function(err) { 86 | console.error(err); 87 | serverTCP.close(closed); 88 | }); 89 | 90 | function closed() { 91 | console.log("server closed"); 92 | } 93 | -------------------------------------------------------------------------------- /examples/server_enron_serial.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, no-unused-vars, spaced-comment */ 2 | 3 | const ModbusRTU = require("../index"); 4 | const holdingRegisters = {}; 5 | const coils = {}; 6 | const inputRegisters = {}; 7 | const discreteInputs = {}; 8 | 9 | const vector = { 10 | getInputRegister: function(addr) { 11 | return inputRegisters[addr]; 12 | }, 13 | getMultipleInputRegisters: function(startAddr, length) { 14 | const values = []; 15 | for (let i = 0; i < length; i++) { 16 | values[i] = inputRegisters[startAddr + i]; 17 | } 18 | return values; 19 | }, 20 | getDiscreteInput: function(addr) { 21 | return discreteInputs[addr]; 22 | }, 23 | getHoldingRegister: function(addr) { 24 | return holdingRegisters[addr]; 25 | }, 26 | setRegister: function(addr, value) { 27 | holdingRegisters[addr] = value; 28 | return; 29 | }, 30 | getMultipleHoldingRegisters: function(startAddr, length) { 31 | const values = []; 32 | for (let i = 0; i < length; i++) { 33 | values[i] = holdingRegisters[startAddr + i]; 34 | } 35 | return values; 36 | }, 37 | getCoil: function(addr) { 38 | return coils[addr]; 39 | }, 40 | setCoil: function(addr, value) { 41 | coils[addr] = value; 42 | return coils[addr]; 43 | }, 44 | readDeviceIdentification: function() { 45 | return { 46 | 0x00: "MyVendorName", 47 | 0x01: "MyProductCode", 48 | 0x02: "MyMajorMinorRevision", 49 | 0x05: "MyModelName", 50 | 0x97: "MyExtendedObject1", 51 | 0xab: "MyExtendedObject2" 52 | }; 53 | } 54 | }; 55 | 56 | // set the server to answer for modbus requests 57 | const serverSerial = new ModbusRTU.ServerSerial( 58 | vector, 59 | { 60 | port: "/tmp/ttyp0", 61 | baudRate: 9600, 62 | debug: true, 63 | unitID: 1, 64 | enron: true, 65 | enronTables: { 66 | booleanRange: [1001, 1999], 67 | shortRange: [3001, 3999], 68 | longRange: [5001, 5999], 69 | floatRange: [7001, 7999] 70 | } 71 | } 72 | ); 73 | 74 | serverSerial.on("error", function(err) { 75 | // Handle socket error if needed, can be ignored 76 | console.error(err); 77 | }); 78 | 79 | serverSerial.on("initialized", function() { 80 | console.log("initialized"); 81 | }); 82 | 83 | serverSerial.on("socketError", function(err) { 84 | console.error(err); 85 | serverSerial.close(closed); 86 | }); 87 | 88 | function closed() { 89 | console.log("server closed"); 90 | } 91 | -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, spaced-comment */ 2 | 3 | // create an empty modbus client 4 | //let ModbusRTU = require("modbus-serial"); 5 | const ModbusRTU = require("../index"); 6 | const client = new ModbusRTU(); 7 | 8 | const networkErrors = ["ESOCKETTIMEDOUT", "ETIMEDOUT", "ECONNRESET", "ECONNREFUSED", "EHOSTUNREACH"]; 9 | 10 | // open connection to a serial port 11 | //client.connectRTUBuffered("/dev/ttyUSB0", { hupcl: false, dsrdtr: false }) 12 | client.connectTCP("127.0.0.1", { port: 8502 }) 13 | .then(setClient) 14 | .then(function() { 15 | console.log("Connected"); }) 16 | .catch(function(e) { 17 | if(e.errno) { 18 | if(networkErrors.includes(e.errno)) { 19 | console.log("we have to reconnect"); 20 | } 21 | } 22 | console.log(e.message); }); 23 | 24 | function setClient() { 25 | // set the client's unit id 26 | // set a timout for requests default is null (no timeout) 27 | client.setID(1); 28 | client.setTimeout(1000); 29 | 30 | // run program 31 | readRegisters(); 32 | } 33 | 34 | function readRegisters() { 35 | // read the 4 registers starting at address 5 36 | client.readHoldingRegisters(5, 4) 37 | .then(function(d) { 38 | console.log("Receive:", d.data); }) 39 | .catch(function(e) { 40 | console.log(e.message); }) 41 | .then(readCoils); 42 | } 43 | 44 | function readCoils() { 45 | // read the 4 registers starting at address 5 46 | client.readCoils(1, 20) 47 | .then(function(d) { 48 | console.log("Receive:", d.data); }) 49 | .catch(function(e) { 50 | console.log(e.message); }) 51 | .then(close); 52 | } 53 | 54 | function close() { 55 | client.close(); 56 | } 57 | -------------------------------------------------------------------------------- /examples/simple_ble.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const { bluetooth } = require("webbluetooth"); 4 | const ModbusRTU = require("../index"); 5 | 6 | async function run() { 7 | const client = new ModbusRTU(); 8 | 9 | await client.connectBle({ 10 | bluetooth, 11 | txService: "0000ffd0-0000-1000-8000-00805f9b34fb", 12 | txCharacteristic: "0000ffd1-0000-1000-8000-00805f9b34fb", 13 | rxService: "0000fff0-0000-1000-8000-00805f9b34fb", 14 | rxCharacteristic: "0000fff1-0000-1000-8000-00805f9b34fb" 15 | }); 16 | 17 | // set the client's unit id 18 | client.setID(255); 19 | 20 | // set a timout for requests default is null (no timeout) 21 | client.setTimeout(1000); 22 | 23 | // read the 7 registers starting at address 256 24 | const data = await client.readHoldingRegisters(256, 7); 25 | console.log("Receive:", data.data); 26 | 27 | client.close(); 28 | } 29 | 30 | run().catch((error) => console.error(error)); 31 | -------------------------------------------------------------------------------- /examples/simple_enron.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, spaced-comment */ 2 | 3 | const ModbusRTU = require("../index"); 4 | const client = new ModbusRTU(); 5 | 6 | const networkErrors = ["ESOCKETTIMEDOUT", "ETIMEDOUT", "ECONNRESET", "ECONNREFUSED", "EHOSTUNREACH"]; 7 | 8 | function write() { 9 | console.log("write"); 10 | 11 | // write the values 1234567890 to register 5001 12 | client 13 | .writeRegisterEnron(5001, 0x499602d2) 14 | .then(read) 15 | .catch(function(e) { 16 | console.log(e.message); 17 | }); 18 | } 19 | 20 | function read() { 21 | console.log("read"); 22 | 23 | // read the 2 registers starting at address 5001 24 | // on device number 1. 25 | client.readRegistersEnron(5001, 2) 26 | .then(console.log) 27 | .catch(function(e) { 28 | console.log(e.message); 29 | }) 30 | .then(close); 31 | } 32 | 33 | client.connectTCP( 34 | "127.0.0.1", 35 | { 36 | port: 8502, 37 | enron: true, 38 | enronTables: { 39 | booleanRange: [1001, 1999], 40 | shortRange: [3001, 3999], 41 | longRange: [5001, 5999], 42 | floatRange: [7001, 7999] 43 | } 44 | }) 45 | .then(setClient) 46 | .then(function() { 47 | console.log("Connected"); }) 48 | .catch(function(e) { 49 | if(e.errno) { 50 | if(networkErrors.includes(e.errno)) { 51 | console.log("we have to reconnect"); 52 | } 53 | } 54 | console.log(e.message); 55 | }); 56 | 57 | function setClient() { 58 | // set the client's unit id 59 | // set a timout for requests default is null (no timeout) 60 | client.setID(1); 61 | client.setTimeout(1000); 62 | 63 | // run program 64 | write(); 65 | } 66 | 67 | function close() { 68 | client.close(); 69 | } 70 | -------------------------------------------------------------------------------- /examples/simple_enron_serial.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, spaced-comment */ 2 | 3 | // create an empty modbus client 4 | const ModbusRTU = require("../index"); 5 | const client = new ModbusRTU(); 6 | 7 | // open connection to a serial port 8 | client.connectRTU( 9 | "/tmp/ptyp0", 10 | { 11 | baudRate: 9600, 12 | debug: true, 13 | enron: true, 14 | enronTables: { 15 | booleanRange: [1001, 1999], 16 | shortRange: [3001, 3999], 17 | longRange: [5001, 5999], 18 | floatRange: [7001, 7999] 19 | } 20 | } 21 | ) 22 | .then(setClient) 23 | .then(function() { 24 | console.log("Connected"); }) 25 | .catch(function(e) { 26 | console.log(e.message); }); 27 | 28 | function setClient() { 29 | // set the client's unit id 30 | // set a timout for requests default is null (no timeout) 31 | client.setID(1); 32 | client.setTimeout(1000); 33 | 34 | // run program 35 | writeRegisters(); 36 | } 37 | 38 | function writeRegisters() { 39 | // write to register 5001 40 | client.writeRegisterEnron(5001, 1234567890) 41 | .then(function(d) { 42 | console.log("Write:", d); 43 | }) 44 | .catch(function(e) { 45 | console.log(e.message); }) 46 | .then(readRegisters); 47 | } 48 | 49 | function readRegisters() { 50 | // read the 1 registers starting at address 5001 51 | client.readRegistersEnron(5001, 1) 52 | .then(function(d) { 53 | console.log("Receive:", d.data); }) 54 | .catch(function(e) { 55 | console.log(e.message); }) 56 | .then(writeCoils); 57 | } 58 | 59 | function writeCoils() { 60 | // write true to coil #1 61 | client.writeCoil(1, true) 62 | .then(function(d) { 63 | console.log("Write:", d); 64 | }) 65 | .catch(function(e) { 66 | console.log(e.message); }) 67 | .then(readCoils); 68 | } 69 | 70 | function readCoils() { 71 | // read the 20 coils 72 | client.readCoils(1, 20) 73 | .then(function(d) { 74 | console.log("Receive:", d.data); }) 75 | .catch(function(e) { 76 | console.log(e.message); }) 77 | .then(close); 78 | } 79 | 80 | function close() { 81 | client.close(); 82 | } 83 | -------------------------------------------------------------------------------- /examples/tcp_client_abort_signal.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // create an empty modbus client 4 | // let ModbusRTU = require("modbus-serial"); 5 | const ModbusRTU = require("../index"); 6 | const client = new ModbusRTU(); 7 | 8 | const abortController = new AbortController(); 9 | const { signal } = abortController; 10 | signal.addEventListener("abort", () => { 11 | console.log("Abort signal received by the abort controller"); 12 | }); 13 | 14 | async function connect() { 15 | await client.connectTCP("127.0.0.1", { 16 | port: 8502, 17 | socketOpts: { 18 | signal: signal 19 | } 20 | }); 21 | client.setID(1); 22 | client.setTimeout(2000); 23 | } 24 | 25 | async function readRegisters() { 26 | const data = await client.readHoldingRegisters(5, 4); 27 | console.log("Received:", data.data); 28 | } 29 | 30 | async function runner() { 31 | await connect(); 32 | 33 | setTimeout(() => { 34 | abortController.abort("Aborting request"); 35 | }, 1000); 36 | 37 | await readRegisters(); 38 | } 39 | 40 | runner() 41 | .then(() => { 42 | if (signal.aborted) { 43 | if (signal.reason) { 44 | console.log(`Request aborted with reason: ${signal.reason}`); 45 | } else { 46 | console.log("Request aborted but no reason was given."); 47 | } 48 | } else { 49 | console.log("Request not aborted"); 50 | } 51 | }) 52 | .catch((error) => { 53 | console.error(error); 54 | }) 55 | .finally(async() => { 56 | console.log("Close client"); 57 | await client.close(); 58 | }); 59 | -------------------------------------------------------------------------------- /examples/write.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, spaced-comment */ 2 | 3 | // create an empty modbus client 4 | //let ModbusRTU = require("modbus-serial"); 5 | const ModbusRTU = require("../index"); 6 | const client = new ModbusRTU(); 7 | 8 | // open connection to a serial port 9 | //client.connectRTUBuffered("/dev/ttyUSB0", {baudRate: 9600}) 10 | client.connectTCP("127.0.0.1", { port: 8502 }) 11 | .then(setClient) 12 | .then(function() { 13 | console.log("Connected"); }) 14 | .catch(function(e) { 15 | console.log(e.message); }); 16 | 17 | function setClient() { 18 | // set the client's unit id 19 | // set a timout for requests default is null (no timeout) 20 | client.setID(1); 21 | client.setTimeout(1000); 22 | 23 | // run program 24 | run(); 25 | } 26 | 27 | function run() { 28 | // write to coil 29 | client.writeCoil(5, true) 30 | .then(function(d) { 31 | console.log("Write true to coil 5", d); }) 32 | .catch(function(e) { 33 | console.log(e.message); }) 34 | .then(writeRegisters); 35 | } 36 | 37 | function writeRegisters() { 38 | // write 3 registers statrting at register 101 39 | // negative values (< 0) have to add 65535 for Modbus registers 40 | client.writeRegisters(101, [10, 9, 8, -20 + 65535, -10 + 65535]) 41 | .then(function(d) { 42 | console.log("Write 10, 9, 8, -20, -10 to registers 101 to 105", d); }) 43 | .catch(function(e) { 44 | console.log(e.message); }) 45 | .then(close); 46 | } 47 | 48 | function close() { 49 | client.close(); 50 | } 51 | -------------------------------------------------------------------------------- /examples/write_complete.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, spaced-comment */ 2 | 3 | // create an empty modbus client 4 | // let ModbusRTU = require("modbus-serial") 5 | const ModbusRTU = require("../index"); 6 | const client = new ModbusRTU(); 7 | 8 | // open connection to a serial port 9 | // client.connectRTUBuffered("/dev/ttyUSB0", {baudRate: 9600}) 10 | client.connectTCP("127.0.0.1", { port: 8502 }) 11 | .then(setClient) 12 | .then(function() { 13 | console.log("Connected"); }) 14 | .catch(function(e) { 15 | console.log(e.message); }); 16 | 17 | function setClient() { 18 | // set the client's unit id 19 | // set a timout for requests default is null (no timeout) 20 | client.setID(1); 21 | client.setTimeout(2000); 22 | 23 | // run program 24 | run(); 25 | } 26 | 27 | function run() { 28 | // write to coil 29 | client.writeCoils(1, [true, false, true, false, true, true, false, true]) 30 | .then(function(d) { 31 | console.log("Write to coils", d); }) 32 | .catch(function(e) { 33 | console.log(e.message); }) 34 | .then(writeCoil); 35 | } 36 | 37 | function writeCoil() { 38 | // write to coil 39 | client.writeCoil(2, true) 40 | .then(function(d) { 41 | console.log("Write true to coil 2", d); }) 42 | .catch(function(e) { 43 | console.log(e.message); }) 44 | .then(writeDiscreteCoil); 45 | } 46 | 47 | function writeDiscreteCoil() { 48 | // write to coil 49 | client.writeCoil(10001, true) 50 | .then(function(d) { 51 | console.log("Write true to discrete input", d); }) 52 | .catch(function(e) { 53 | console.log(e.message); }) 54 | .then(writeDiscreteCoils); 55 | } 56 | 57 | function writeDiscreteCoils() { 58 | // write to coil 59 | client.writeCoils(10002, [true, true, true, true]) 60 | .then(function(d) { 61 | console.log("Write true to discrete inputs", d); }) 62 | .catch(function(e) { 63 | console.log(e.message); }) 64 | .then(writeRegisters); 65 | } 66 | 67 | function writeRegisters() { 68 | // write 5 registers statrting at input registers 69 | client.writeRegisters(1, [100, 90, 80, -200 + 65535, -100 + 65535]) 70 | .then(function(d) { 71 | console.log("Write 100, 90, 80, -200, -100 to input registers", d); }) 72 | .catch(function(e) { 73 | console.log(e.message); }) 74 | .then(writeHoldingRegsiters); 75 | } 76 | 77 | function writeHoldingRegsiters() { 78 | // write 5 registers statrting at holding registers 79 | // negative values (< 0) have to add 65535 for Modbus registers 80 | client.writeRegisters(10001, [10, 9, 8, -20 + 65535, -10 + 65535]) 81 | .then(function(d) { 82 | console.log("Write 10, 9, 8, -20, -10 to holding registers", d); }) 83 | .catch(function(e) { 84 | console.log(e.message); }) 85 | .then(close); 86 | } 87 | 88 | function close() { 89 | client.close(); 90 | } 91 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const gulp = require("gulp"); 4 | const pump = require("pump"); 5 | const jsdoc = require("gulp-jsdoc3"); 6 | const clean = require("gulp-clean"); 7 | 8 | gulp.task("default", function() { 9 | // place code for your default task here 10 | }); 11 | 12 | gulp.task("docs", ["doc", "docExamples"]); 13 | gulp.task("build", ["apis", "ports", "servers", "utils"]); 14 | gulp.task("publish", ["build", "docs"]); 15 | 16 | gulp.task("clean", function() { 17 | return gulp.src(["modbus-serial", "docs/gen"]) 18 | .pipe(clean({ force: true })); 19 | }); 20 | 21 | gulp.task("doc", function(cb) { 22 | gulp.src(["README.md", "apis/**/*.js", "ports/**/*.js", "servers/**/*.js", "utils/**/*.js"], { read: false }) 23 | .pipe(jsdoc(cb)); 24 | }); 25 | 26 | gulp.task("docExamples", function() { 27 | return gulp.src("examples/**/*").pipe(gulp.dest("docs/gen/examples")); 28 | }); 29 | 30 | gulp.task("apis", function(cb) { 31 | pump([ 32 | gulp.src("apis/**/*.js"), 33 | gulp.dest("modbus-serial/apis") 34 | ], 35 | cb 36 | ); 37 | }); 38 | 39 | gulp.task("ports", function(cb) { 40 | pump([ 41 | gulp.src("ports/**/*.js"), 42 | gulp.dest("modbus-serial/ports") 43 | ], 44 | cb 45 | ); 46 | }); 47 | 48 | gulp.task("servers", function(cb) { 49 | pump([ 50 | gulp.src("servers/**/*.js"), 51 | gulp.dest("modbus-serial/servers") 52 | ], 53 | cb 54 | ); 55 | }); 56 | 57 | gulp.task("utils", function(cb) { 58 | pump([ 59 | gulp.src("utils/**/*.js"), 60 | gulp.dest("modbus-serial/utils") 61 | ], 62 | cb 63 | ); 64 | }); 65 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import {ModbusRTU} from "./ModbusRTU"; 2 | export * from "./ServerTCP"; 3 | export * from "./ServerSerial"; 4 | 5 | export default ModbusRTU; -------------------------------------------------------------------------------- /npm-update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | npm outdated 4 | 5 | npm install 6 | -------------------------------------------------------------------------------- /npm-upgrade.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | npm outdated 4 | 5 | ncu -u --upgradeAll 6 | 7 | npm install -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modbus-serial", 3 | "version": "8.0.21", 4 | "description": "A pure JavaScript implemetation of MODBUS-RTU (Serial and TCP) for NodeJS.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "nyc --reporter=lcov --reporter=text mocha --recursive" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/yaacov/node-modbus-serial.git" 12 | }, 13 | "keywords": [ 14 | "modbus", 15 | "rtu", 16 | "serial", 17 | "port", 18 | "com", 19 | "arduino" 20 | ], 21 | "author": "Yaacov Zamir ", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/yaacov/node-modbus-serial/issues" 25 | }, 26 | "homepage": "https://github.com/yaacov/node-modbus-serial#readme", 27 | "devDependencies": { 28 | "chai": "^4.2.0", 29 | "chai-as-promised": "^7.1.1", 30 | "eslint": "^8.12.0", 31 | "gulp": "^4.0.2", 32 | "gulp-clean": "^0.4.0", 33 | "gulp-jsdoc3": "^3.0.0", 34 | "mocha": "^10.2.0", 35 | "mocha-eslint": "^7.0.0", 36 | "mockery": "^2.1.0", 37 | "nyc": "^15.1.0", 38 | "pump": "^3.0.0", 39 | "sinon": "^15.2.0", 40 | "web-bluetooth-mock": "^1.2.0", 41 | "webbluetooth": "^3.1.0" 42 | }, 43 | "dependencies": { 44 | "debug": "^4.3.1", 45 | "serialport": "^12.0.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ports/asciiport.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable no-ternary */ 3 | 4 | const events = require("events"); 5 | const EventEmitter = events.EventEmitter || events; 6 | const SerialPort = require("serialport").SerialPort; 7 | const modbusSerialDebug = require("debug")("modbus-serial"); 8 | 9 | const crc16 = require("../utils/crc16"); 10 | const calculateLrc = require("./../utils/lrc"); 11 | 12 | /* TODO: const should be set once, maybe */ 13 | const MIN_DATA_LENGTH = 6; 14 | 15 | /** 16 | * Ascii encode a 'request' buffer and return it. This includes removing 17 | * the CRC bytes and replacing them with an LRC. 18 | * 19 | * @param {Buffer} buf the data buffer to encode. 20 | * @return {Buffer} the ascii encoded buffer 21 | * @private 22 | */ 23 | function _asciiEncodeRequestBuffer(buf) { 24 | 25 | // replace the 2 byte crc16 with a single byte lrc 26 | buf.writeUInt8(calculateLrc(buf.slice(0, -2)), buf.length - 2); 27 | 28 | // create a new buffer of the correct size 29 | const bufAscii = Buffer.alloc(buf.length * 2 + 1); // 1 byte start delimit + x2 data as ascii encoded + 2 lrc + 2 end delimit 30 | 31 | // create the ascii payload 32 | 33 | // start with the single start delimiter 34 | bufAscii.write(":", 0); 35 | // encode the data, with the new single byte lrc 36 | bufAscii.write(buf.toString("hex", 0, buf.length - 1).toUpperCase(), 1); 37 | // end with the two end delimiters 38 | bufAscii.write("\r", bufAscii.length - 2); 39 | bufAscii.write("\n", bufAscii.length - 1); 40 | 41 | return bufAscii; 42 | } 43 | 44 | /** 45 | * Ascii decode a 'response' buffer and return it. 46 | * 47 | * @param {Buffer} bufAscii the ascii data buffer to decode. 48 | * @return {Buffer} the decoded buffer, or null if decode error 49 | * @private 50 | */ 51 | function _asciiDecodeResponseBuffer(bufAscii) { 52 | 53 | // create a new buffer of the correct size (based on ascii encoded buffer length) 54 | const bufDecoded = Buffer.alloc((bufAscii.length - 1) / 2); 55 | 56 | // decode into new buffer (removing delimiters at start and end) 57 | for (let i = 0; i < (bufAscii.length - 3) / 2; i++) { 58 | bufDecoded.write(String.fromCharCode(bufAscii.readUInt8(i * 2 + 1), bufAscii.readUInt8(i * 2 + 2)), i, 1, "hex"); 59 | } 60 | 61 | // check the lrc is true 62 | const lrcIn = bufDecoded.readUInt8(bufDecoded.length - 2); 63 | if(calculateLrc(bufDecoded.slice(0, -2)) !== lrcIn) { 64 | // return null if lrc error 65 | const calcLrc = calculateLrc(bufDecoded.slice(0, -2)); 66 | 67 | modbusSerialDebug({ action: "LRC error", LRC: lrcIn.toString(16), calcLRC: calcLrc.toString(16) }); 68 | return null; 69 | } 70 | 71 | // replace the 1 byte lrc with a two byte crc16 72 | bufDecoded.writeUInt16LE(crc16(bufDecoded.slice(0, -2)), bufDecoded.length - 2); 73 | 74 | return bufDecoded; 75 | } 76 | 77 | /** 78 | * check if a buffer chunk can be a modbus answer 79 | * or modbus exception 80 | * 81 | * @param {AsciiPort} modbus 82 | * @param {Buffer} buf the buffer to check. 83 | * @return {boolean} if the buffer can be an answer 84 | * @private 85 | */ 86 | function _checkData(modbus, buf) { 87 | // check buffer size 88 | if (buf.length !== modbus._length && buf.length !== 5) { 89 | modbusSerialDebug({ action: "length error", recive: buf.length, expected: modbus._length }); 90 | 91 | return false; 92 | } 93 | 94 | // check buffer unit-id and command 95 | return (buf[0] === modbus._id && 96 | (0x7f & buf[1]) === modbus._cmd); 97 | } 98 | 99 | class AsciiPort extends EventEmitter { 100 | /** 101 | * Simulate a modbus-ascii port using serial connection. 102 | * 103 | * @param path 104 | * @param options 105 | * @constructor 106 | */ 107 | constructor(path, options) { 108 | super(); 109 | 110 | const modbus = this; 111 | 112 | // options 113 | options = options || {}; 114 | 115 | // select char for start of slave frame (usually :) 116 | this._startOfSlaveFrameChar = 117 | (options.startOfSlaveFrameChar === undefined) 118 | ? 0x3A 119 | : options.startOfSlaveFrameChar; 120 | 121 | // disable auto open, as we handle the open 122 | options.autoOpen = false; 123 | 124 | // internal buffer 125 | this._buffer = Buffer.from(""); 126 | this._id = 0; 127 | this._cmd = 0; 128 | this._length = 0; 129 | 130 | // create the SerialPort 131 | this._client = new SerialPort(Object.assign({}, { path }, options)); 132 | 133 | // register the port data event 134 | this._client.on("data", function(data) { 135 | 136 | // add new data to buffer 137 | modbus._buffer = Buffer.concat([modbus._buffer, data]); 138 | 139 | modbusSerialDebug({ action: "receive serial ascii port", data: data, buffer: modbus._buffer }); 140 | modbusSerialDebug(JSON.stringify({ action: "receive serial ascii port strings", data: data, buffer: modbus._buffer })); 141 | 142 | // check buffer for start delimiter 143 | const sdIndex = modbus._buffer.indexOf(modbus._startOfSlaveFrameChar); 144 | if(sdIndex === -1) { 145 | // if not there, reset the buffer and return 146 | modbus._buffer = Buffer.from(""); 147 | return; 148 | } 149 | // if there is data before the start delimiter, remove it 150 | if(sdIndex > 0) { 151 | modbus._buffer = modbus._buffer.slice(sdIndex); 152 | } 153 | // do we have the complete message (i.e. are the end delimiters there) 154 | if(modbus._buffer.includes("\r\n", 1, "ascii") === true) { 155 | // check there is no excess data after end delimiters 156 | const edIndex = modbus._buffer.indexOf(0x0A); // ascii for '\n' 157 | if(edIndex !== modbus._buffer.length - 1) { 158 | // if there is, remove it 159 | modbus._buffer = modbus._buffer.slice(0, edIndex + 1); 160 | } 161 | 162 | // we have what looks like a complete ascii encoded response message, so decode 163 | const _data = _asciiDecodeResponseBuffer(modbus._buffer); 164 | modbusSerialDebug({ action: "got EOM", data: _data, buffer: modbus._buffer }); 165 | if(_data !== null) { 166 | 167 | // check if this is the data we are waiting for 168 | if (_checkData(modbus, _data)) { 169 | modbusSerialDebug({ action: "emit data serial ascii port", data: data, buffer: modbus._buffer }); 170 | modbusSerialDebug(JSON.stringify({ action: "emit data serial ascii port strings", data: data, buffer: modbus._buffer })); 171 | // emit a data signal 172 | modbus.emit("data", _data); 173 | } 174 | } 175 | // reset the buffer now its been used 176 | modbus._buffer = Buffer.from(""); 177 | } else { 178 | // otherwise just wait for more data to arrive 179 | } 180 | }); 181 | } 182 | 183 | /** 184 | * Check if port is open. 185 | * 186 | * @returns {boolean} 187 | */ 188 | get isOpen() { 189 | return this._client.isOpen; 190 | } 191 | 192 | /** 193 | * Simulate successful port open. 194 | * 195 | * @param callback 196 | */ 197 | open(callback) { 198 | this._client.open(callback); 199 | } 200 | 201 | /** 202 | * Simulate successful close port. 203 | * 204 | * @param callback 205 | */ 206 | close(callback) { 207 | this._client.close(callback); 208 | this.removeAllListeners(); 209 | } 210 | 211 | /** 212 | * Send data to a modbus slave. 213 | * 214 | * @param data 215 | */ 216 | write(data) { 217 | if(data.length < MIN_DATA_LENGTH) { 218 | modbusSerialDebug("expected length of data is to small - minimum is " + MIN_DATA_LENGTH); 219 | return; 220 | } 221 | 222 | let length = null; 223 | 224 | // remember current unit and command 225 | this._id = data[0]; 226 | this._cmd = data[1]; 227 | 228 | // calculate expected answer length (this is checked after ascii decoding) 229 | switch (this._cmd) { 230 | case 1: 231 | case 2: 232 | length = data.readUInt16BE(4); 233 | this._length = 3 + parseInt((length - 1) / 8 + 1) + 2; 234 | break; 235 | case 3: 236 | case 4: 237 | length = data.readUInt16BE(4); 238 | this._length = 3 + 2 * length + 2; 239 | break; 240 | case 5: 241 | case 6: 242 | case 15: 243 | case 16: 244 | this._length = 6 + 2; 245 | break; 246 | default: 247 | // raise and error ? 248 | modbusSerialDebug({ action: "unknown command", id: this._id.toString(16), command: this._cmd.toString(16) }); 249 | this._length = 0; 250 | break; 251 | } 252 | 253 | // ascii encode buffer 254 | const _encodedData = _asciiEncodeRequestBuffer(data); 255 | 256 | // send buffer to slave 257 | this._client.write(_encodedData); 258 | 259 | modbusSerialDebug({ 260 | action: "send serial ascii port", 261 | data: _encodedData, 262 | unitid: this._id, 263 | functionCode: this._cmd 264 | }); 265 | 266 | modbusSerialDebug(JSON.stringify({ 267 | action: "send serial ascii port", 268 | data: _encodedData, 269 | unitid: this._id, 270 | functionCode: this._cmd 271 | })); 272 | } 273 | } 274 | 275 | /** 276 | * ASCII port for Modbus. 277 | * 278 | * @type {AsciiPort} 279 | */ 280 | module.exports = AsciiPort; 281 | -------------------------------------------------------------------------------- /ports/bleport.js: -------------------------------------------------------------------------------- 1 | /* globals navigator */ 2 | 3 | "use strict"; 4 | 5 | const { EventEmitter } = require("events"); 6 | const debug = require("debug")("modbus-serial"); 7 | 8 | /** 9 | * Bluetooth Low Energy port for Modbus. 10 | */ 11 | class BlePort extends EventEmitter { 12 | constructor(options) { 13 | super(); 14 | 15 | if (typeof(options) === "undefined") options = {}; 16 | 17 | this._bluetooth = options.bluetooth || navigator.bluetooth; 18 | this._txServiceUuid = options.txService; 19 | this._txCharacteristicUuid = options.txCharacteristic; 20 | this._rxServiceUuid = options.rxService; 21 | this._rxCharacteristicUuid = options.rxCharacteristic; 22 | 23 | this._boundHandleDisconnection = this._handleDisconnection.bind(this); 24 | this._boundHandleCharacteristicValueChanged = this._handleCharacteristicValueChanged.bind(this); 25 | } 26 | 27 | get isOpen() { 28 | return Boolean(this._device) && this._device.gatt.connected; 29 | } 30 | 31 | async open(callback) { 32 | let error; 33 | try { 34 | const options = { 35 | filters: [{ services: [this._txServiceUuid] }], 36 | optionalServices: [this._txServiceUuid, this._rxServiceUuid] 37 | }; 38 | debug({ action: "requesting BLE device", options }); 39 | this._device = await this._bluetooth.requestDevice(options); 40 | debug({ action: "BLE device connected", name: this._device.name, id: this._device.id }); 41 | 42 | this._device.addEventListener("gattserverdisconnected", this._boundHandleDisconnection); 43 | 44 | debug({ action: "Connecting to GATT server" }); 45 | this._server = await this._device.gatt.connect(); 46 | debug({ action: "GATT server connected" }); 47 | 48 | debug({ action: "Getting TX service", uuid: this._txServiceUuid }); 49 | this._txService = await this._server.getPrimaryService(this._txServiceUuid); 50 | debug({ action: "TX service found" }); 51 | 52 | debug({ action: "Getting TX characteristic", uuid: this._txCharacteristicUuid }); 53 | this._txCharacteristic = await this._txService.getCharacteristic(this._txCharacteristicUuid); 54 | debug({ action: "TX characteristic found" }); 55 | 56 | debug({ action: "Getting RX service", uuid: this._rxServiceUuid }); 57 | this._rxService = await this._server.getPrimaryService(this._rxServiceUuid); 58 | debug({ action: "RX service found" }); 59 | 60 | debug({ action: "Getting RX characteristic", uuid: this._rxCharacteristicUuid }); 61 | this._rxCharacteristic = await this._rxService.getCharacteristic(this._rxCharacteristicUuid); 62 | debug({ action: "RX characteristic found" }); 63 | 64 | debug({ action: "Starting RX notifications" }); 65 | await this._rxCharacteristic.startNotifications(); 66 | debug({ action: "RX notifications started" }); 67 | 68 | this._rxCharacteristic.addEventListener("characteristicvaluechanged", this._boundHandleCharacteristicValueChanged); 69 | } catch (_error) { 70 | error = _error; 71 | } 72 | 73 | if (callback) { 74 | callback(error); 75 | } 76 | } 77 | 78 | async close(callback) { 79 | let error; 80 | try { 81 | if (this._rxCharacteristic) { 82 | debug({ action: "Stopping RX notifications" }); 83 | await this._rxCharacteristic.stopNotifications(); 84 | debug({ action: "RX notifications stopped" }); 85 | 86 | this._rxCharacteristic.removeEventListener("characteristicvaluechanged", this._boundHandleCharacteristicValueChanged); 87 | } 88 | 89 | if (this._device) { 90 | debug({ action: "Disconnecting from GATT server" }); 91 | 92 | this._device.removeEventListener("gattserverdisconnected", this._boundHandleDisconnection); 93 | 94 | if (this._device.gatt.connected) { 95 | this._device.gatt.disconnect(); 96 | debug({ action: "GATT server disconnected" }); 97 | } else { 98 | debug({ action: "GATT server is already disconnected" }); 99 | } 100 | } 101 | } catch (_error) { 102 | error = _error; 103 | } 104 | 105 | if (callback) { 106 | callback(error); 107 | } 108 | } 109 | 110 | /** 111 | * Writes raw data to the TX characteristic. 112 | * @param {Buffer} data 113 | * @returns {Promise} 114 | */ 115 | async write(data) { 116 | debug({ action: "Writing to TX characteristic", data }); 117 | await this._txCharacteristic.writeValue(BlePort._bufferToArrayBuffer(data)); 118 | } 119 | 120 | _handleDisconnection() { 121 | debug({ action: "GATT server disconnected" }); 122 | this.emit("close"); 123 | } 124 | 125 | /** 126 | * Handles a received GATT value change event. 127 | * @param event 128 | * @private 129 | */ 130 | _handleCharacteristicValueChanged(event) { 131 | const dataView = event.target.value; 132 | const buffer = Buffer.from(dataView.buffer, dataView.byteOffset, dataView.byteLength); 133 | debug({ action: "RX characteristic changed", buffer }); 134 | this.emit("data", buffer); 135 | } 136 | 137 | /** 138 | * Converts a Node.js `Buffer` to an `ArrayBuffer`. 139 | * @param {Buffer} buffer 140 | * @returns {ArrayBuffer} 141 | * @private 142 | */ 143 | static _bufferToArrayBuffer(buffer) { 144 | return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); 145 | } 146 | } 147 | 148 | module.exports = BlePort; 149 | -------------------------------------------------------------------------------- /ports/c701port.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const events = require("events"); 3 | const EventEmitter = events.EventEmitter || events; 4 | const dgram = require("dgram"); 5 | const modbusSerialDebug = require("debug")("modbus-serial"); 6 | 7 | const crc16 = require("../utils/crc16"); 8 | 9 | /* TODO: const should be set once, maybe */ 10 | const MIN_DATA_LENGTH = 6; 11 | 12 | const C701_PORT = 0x7002; 13 | 14 | /** 15 | * Check if a buffer chunk can be a Modbus answer or modbus exception. 16 | * 17 | * @param {UdpPort} modbus 18 | * @param {Buffer} buf the buffer to check. 19 | * @return {boolean} if the buffer can be an answer 20 | * @private 21 | */ 22 | function _checkData(modbus, buf) { 23 | // check buffer size 24 | if (buf.length !== modbus._length && buf.length !== 5) return false; 25 | 26 | // calculate crc16 27 | const crcIn = buf.readUInt16LE(buf.length - 2); 28 | 29 | // check buffer unit-id, command and crc 30 | return (buf[0] === modbus._id && 31 | (0x7f & buf[1]) === modbus._cmd && 32 | crcIn === crc16(buf.slice(0, -2))); 33 | } 34 | 35 | class UdpPort extends EventEmitter { 36 | /** 37 | * Simulate a modbus-RTU port using C701 UDP-to-Serial bridge. 38 | * 39 | * @param ip 40 | * @param options 41 | * @constructor 42 | */ 43 | constructor(ip, options) { 44 | super(); 45 | 46 | const modbus = this; 47 | this.ip = ip; 48 | this.openFlag = false; 49 | 50 | // options 51 | if (typeof(options) === "undefined") options = {}; 52 | this.port = options.port || C701_PORT; // C701 port 53 | 54 | // create a socket 55 | this._client = dgram.createSocket("udp4"); 56 | 57 | // wait for answer 58 | this._client.on("message", function(data) { 59 | let buffer = null; 60 | 61 | // check expected length 62 | if (modbus.length < 6) return; 63 | 64 | // check message length 65 | if (data.length < (116 + 5)) return; 66 | 67 | // check the C701 packet magic 68 | if (data.readUInt16LE(2) !== 602) return; 69 | 70 | // check for modbus valid answer 71 | // get the serial data from the C701 packet 72 | buffer = data.slice(data.length - modbus._length); 73 | 74 | modbusSerialDebug({ action: "receive c701 upd port", data: data, buffer: buffer }); 75 | modbusSerialDebug(JSON.stringify({ action: "receive c701 upd port strings", data: data, buffer: buffer })); 76 | 77 | // check the serial data 78 | if (_checkData(modbus, buffer)) { 79 | modbusSerialDebug({ action: "emit data serial rtu buffered port", buffer: buffer }); 80 | modbusSerialDebug(JSON.stringify({ action: "emit data serial rtu buffered port strings", buffer: buffer })); 81 | 82 | modbus.emit("data", buffer); 83 | } else { 84 | // check for modbus exception 85 | // get the serial data from the C701 packet 86 | buffer = data.slice(data.length - 5); 87 | 88 | // check the serial data 89 | if (_checkData(modbus, buffer)) { 90 | modbusSerialDebug({ action: "emit data serial rtu buffered port", buffer: buffer }); 91 | modbusSerialDebug(JSON.stringify({ 92 | action: "emit data serial rtu buffered port strings", 93 | buffer: buffer 94 | })); 95 | 96 | modbus.emit("data", buffer); 97 | } 98 | } 99 | }); 100 | 101 | this._client.on("listening", function() { 102 | modbus.openFlag = true; 103 | }); 104 | 105 | this._client.on("close", function() { 106 | modbus.openFlag = false; 107 | }); 108 | } 109 | 110 | /** 111 | * Check if port is open. 112 | * 113 | * @returns {boolean} 114 | */ 115 | get isOpen() { 116 | return this.openFlag; 117 | } 118 | 119 | /** 120 | * Simulate successful port open. 121 | * 122 | * @param callback 123 | */ 124 | // eslint-disable-next-line class-methods-use-this 125 | open(callback) { 126 | if (callback) 127 | callback(null); 128 | } 129 | 130 | /** 131 | * Simulate successful close port. 132 | * 133 | * @param callback 134 | */ 135 | close(callback) { 136 | this._client.close(); 137 | if (callback) 138 | callback(null); 139 | } 140 | 141 | /** 142 | * Send data to a modbus-tcp slave. 143 | * 144 | * @param data 145 | */ 146 | write(data) { 147 | if(data.length < MIN_DATA_LENGTH) { 148 | modbusSerialDebug("expected length of data is to small - minimum is " + MIN_DATA_LENGTH); 149 | return; 150 | } 151 | 152 | let length = null; 153 | 154 | // remember current unit and command 155 | this._id = data[0]; 156 | this._cmd = data[1]; 157 | 158 | // calculate expected answer length 159 | switch (this._cmd) { 160 | case 1: 161 | case 2: 162 | length = data.readUInt16BE(4); 163 | this._length = 3 + parseInt((length - 1) / 8 + 1) + 2; 164 | break; 165 | case 3: 166 | case 4: 167 | length = data.readUInt16BE(4); 168 | this._length = 3 + 2 * length + 2; 169 | break; 170 | case 5: 171 | case 6: 172 | case 15: 173 | case 16: 174 | this._length = 6 + 2; 175 | break; 176 | default: 177 | // raise and error ? 178 | this._length = 0; 179 | break; 180 | } 181 | 182 | // build C701 header 183 | const buffer = Buffer.alloc(data.length + 116); 184 | buffer.fill(0); 185 | buffer.writeUInt16LE(600, 2); // C701 magic for serial bridge 186 | buffer.writeUInt16LE(0, 36); // C701 RS485 connector (0..2) 187 | buffer.writeUInt16LE(this._length, 38); // expected serial answer length 188 | buffer.writeUInt16LE(1, 102); // C7011 RS481 hub (1..2) 189 | buffer.writeUInt16LE(data.length, 104); // serial data length 190 | 191 | // add serial line data 192 | data.copy(buffer, 116); 193 | 194 | // send buffer to C701 UDP to serial bridge 195 | this._client.send(buffer, 0, buffer.length, this.port, this.ip); 196 | 197 | modbusSerialDebug({ 198 | action: "send c701 upd port", 199 | data: data, 200 | buffer: buffer, 201 | unitid: this._id, 202 | functionCode: this._cmd 203 | }); 204 | 205 | modbusSerialDebug(JSON.stringify({ 206 | action: "send c701 upd port strings", 207 | data: data, 208 | buffer: buffer, 209 | unitid: this._id, 210 | functionCode: this._cmd 211 | })); 212 | } 213 | } 214 | 215 | /** 216 | * UDP port for Modbus. 217 | * 218 | * @type {UdpPort} 219 | */ 220 | module.exports = UdpPort; 221 | -------------------------------------------------------------------------------- /ports/rtubufferedport.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const events = require("events"); 3 | const EventEmitter = events.EventEmitter || events; 4 | const SerialPort = require("serialport").SerialPort; 5 | const modbusSerialDebug = require("debug")("modbus-serial"); 6 | 7 | /* TODO: const should be set once, maybe */ 8 | const EXCEPTION_LENGTH = 5; 9 | const MIN_DATA_LENGTH = 6; 10 | const MIN_WRITE_DATA_LENGTH = 4; 11 | const MAX_BUFFER_LENGTH = 256; 12 | const CRC_LENGTH = 2; 13 | const READ_DEVICE_IDENTIFICATION_FUNCTION_CODE = 43; 14 | const REPORT_SERVER_ID_FUNCTION_CODE = 17; 15 | const LENGTH_UNKNOWN = "unknown"; 16 | const BITS_TO_NUM_OF_OBJECTS = 7; 17 | 18 | // Helper function -> Bool 19 | // BIT | TYPE 20 | // 8 | OBJECTID 21 | // 9 | length of OBJECTID 22 | // 10 -> n | the object 23 | // 10 + n + 1 | new object id 24 | const calculateFC43Length = function(buffer, numObjects, i, bufferLength) { 25 | const result = { hasAllData: true }; 26 | let currentByte = 8 + i; // current byte starts at object id. 27 | if (numObjects > 0) { 28 | for (let j = 0; j < numObjects; j++) { 29 | if (bufferLength < currentByte) { 30 | result.hasAllData = false; 31 | break; 32 | } 33 | const objLength = buffer[currentByte + 1]; 34 | if (!objLength) { 35 | result.hasAllData = false; 36 | break; 37 | } 38 | currentByte += 2 + objLength; 39 | } 40 | } 41 | if (currentByte + CRC_LENGTH > bufferLength) { 42 | // still waiting on the CRC! 43 | result.hasAllData = false; 44 | } 45 | if (result.hasAllData) { 46 | result.bufLength = currentByte + CRC_LENGTH; 47 | } 48 | return result; 49 | }; 50 | 51 | class RTUBufferedPort extends EventEmitter { 52 | /** 53 | * Simulate a modbus-RTU port using buffered serial connection. 54 | * 55 | * @param path 56 | * @param options 57 | * @constructor 58 | */ 59 | constructor(path, options) { 60 | super(); 61 | 62 | const self = this; 63 | 64 | // options 65 | if (typeof(options) === "undefined") options = {}; 66 | 67 | // disable auto open, as we handle the open 68 | options.autoOpen = false; 69 | 70 | // internal buffer 71 | this._buffer = Buffer.alloc(0); 72 | this._id = 0; 73 | this._cmd = 0; 74 | this._length = 0; 75 | 76 | // create the SerialPort 77 | this._client = new SerialPort(Object.assign({}, { path }, options)); 78 | 79 | // attach an error listner on the SerialPort object 80 | this._client.on("error", function(error) { 81 | self.emit("error", error); 82 | }); 83 | 84 | // attach a close listner on the SerialPort object 85 | this._client.on("close", function() { 86 | self.emit("close"); 87 | }); 88 | 89 | // register the port data event 90 | this._client.on("data", function onData(data) { 91 | // add data to buffer 92 | self._buffer = Buffer.concat([self._buffer, data]); 93 | 94 | modbusSerialDebug({ action: "receive serial rtu buffered port", data: data, buffer: self._buffer }); 95 | 96 | // check if buffer include a complete modbus answer 97 | const expectedLength = self._length; 98 | let bufferLength = self._buffer.length; 99 | 100 | 101 | // check data length 102 | if (expectedLength !== LENGTH_UNKNOWN && 103 | expectedLength < MIN_DATA_LENGTH || 104 | bufferLength < EXCEPTION_LENGTH 105 | ) { return; } 106 | 107 | // check buffer size for MAX_BUFFER_SIZE 108 | if (bufferLength > MAX_BUFFER_LENGTH) { 109 | self._buffer = self._buffer.slice(-MAX_BUFFER_LENGTH); 110 | bufferLength = MAX_BUFFER_LENGTH; 111 | } 112 | 113 | // loop and check length-sized buffer chunks 114 | const maxOffset = bufferLength - EXCEPTION_LENGTH; 115 | 116 | for (let i = 0; i <= maxOffset; i++) { 117 | const unitId = self._buffer[i]; 118 | const functionCode = self._buffer[i + 1]; 119 | 120 | if (unitId !== self._id) continue; 121 | 122 | if (functionCode === self._cmd && functionCode === READ_DEVICE_IDENTIFICATION_FUNCTION_CODE) { 123 | if (bufferLength <= BITS_TO_NUM_OF_OBJECTS + i) { 124 | return; 125 | } 126 | const numObjects = self._buffer[7 + i]; 127 | const result = calculateFC43Length(self._buffer, numObjects, i, bufferLength); 128 | if (result.hasAllData) { 129 | self._emitData(i, result.bufLength); 130 | return; 131 | } 132 | } else if (functionCode === self._cmd && functionCode === REPORT_SERVER_ID_FUNCTION_CODE) { 133 | const contentLength = self._buffer[i + 2]; 134 | self._emitData(i, contentLength + 5); // length + serverID + status + contentLength + CRC 135 | return; 136 | } else { 137 | if (functionCode === self._cmd && i + expectedLength <= bufferLength) { 138 | self._emitData(i, expectedLength); 139 | return; 140 | } 141 | if (functionCode === (0x80 | self._cmd) && i + EXCEPTION_LENGTH <= bufferLength) { 142 | self._emitData(i, EXCEPTION_LENGTH); 143 | return; 144 | } 145 | } 146 | 147 | // frame header matches, but still missing bytes pending 148 | if (functionCode === (0x7f & self._cmd)) break; 149 | } 150 | }); 151 | } 152 | 153 | /** 154 | * Check if port is open. 155 | * 156 | * @returns {boolean} 157 | */ 158 | get isOpen() { 159 | return this._client.isOpen; 160 | } 161 | 162 | /** 163 | * Emit the received response, cut the buffer and reset the internal vars. 164 | * 165 | * @param {number} start The start index of the response within the buffer. 166 | * @param {number} length The length of the response. 167 | * @private 168 | */ 169 | _emitData(start, length) { 170 | const buffer = this._buffer.slice(start, start + length); 171 | modbusSerialDebug({ action: "emit data serial rtu buffered port", buffer: buffer }); 172 | this.emit("data", buffer); 173 | this._buffer = this._buffer.slice(start + length); 174 | } 175 | 176 | /** 177 | * Simulate successful port open. 178 | * 179 | * @param callback 180 | */ 181 | open(callback) { 182 | this._client.open(callback); 183 | } 184 | 185 | /** 186 | * Simulate successful close port. 187 | * 188 | * @param callback 189 | */ 190 | close(callback) { 191 | this._client.close(callback); 192 | this.removeAllListeners("data"); 193 | } 194 | 195 | /** 196 | * Send data to a modbus slave. 197 | * 198 | * @param {Buffer} data 199 | */ 200 | write(data) { 201 | if(data.length < MIN_WRITE_DATA_LENGTH) { 202 | modbusSerialDebug("expected length of data is to small - minimum is " + MIN_WRITE_DATA_LENGTH); 203 | return; 204 | } 205 | 206 | let length = null; 207 | 208 | // remember current unit and command 209 | this._id = data[0]; 210 | this._cmd = data[1]; 211 | 212 | // calculate expected answer length 213 | switch (this._cmd) { 214 | case 1: 215 | case 2: 216 | length = data.readUInt16BE(4); 217 | this._length = 3 + parseInt((length - 1) / 8 + 1) + 2; 218 | break; 219 | case 3: 220 | case 4: 221 | length = data.readUInt16BE(4); 222 | this._length = 3 + 2 * length + 2; 223 | break; 224 | case 5: 225 | case 6: 226 | case 15: 227 | case 16: 228 | this._length = 6 + 2; 229 | break; 230 | case 17: 231 | // response is device specific 232 | this._length = LENGTH_UNKNOWN; 233 | break; 234 | case 43: 235 | // this function is super special 236 | // you know the format of the code response 237 | // and you need to continuously check that all of the data has arrived before emitting 238 | // see onData for more info. 239 | this._length = LENGTH_UNKNOWN; 240 | break; 241 | default: 242 | // raise and error ? 243 | this._length = 0; 244 | break; 245 | } 246 | 247 | // send buffer to slave 248 | this._client.write(data); 249 | 250 | modbusSerialDebug({ 251 | action: "send serial rtu buffered", 252 | data: data, 253 | unitid: this._id, 254 | functionCode: this._cmd, 255 | length: this._length 256 | }); 257 | } 258 | } 259 | 260 | /** 261 | * RTU buffered port for Modbus. 262 | * 263 | * @type {RTUBufferedPort} 264 | */ 265 | module.exports = RTUBufferedPort; 266 | -------------------------------------------------------------------------------- /ports/telnetport.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const events = require("events"); 3 | const EventEmitter = events.EventEmitter || events; 4 | const net = require("net"); 5 | const modbusSerialDebug = require("debug")("modbus-serial"); 6 | 7 | /* TODO: const should be set once, maybe */ 8 | const EXCEPTION_LENGTH = 5; 9 | const MIN_DATA_LENGTH = 6; 10 | 11 | const TELNET_PORT = 2217; 12 | 13 | class TelnetPort extends EventEmitter { 14 | /** 15 | * Simulate a modbus-RTU port using Telent connection. 16 | * 17 | * @param ip 18 | * @param options 19 | * @constructor 20 | */ 21 | constructor(ip, options) { 22 | super(); 23 | 24 | const self = this; 25 | this.ip = ip; 26 | this.openFlag = false; 27 | this.callback = null; 28 | this._externalSocket = null; 29 | 30 | // options 31 | if(typeof ip === "object") { 32 | options = ip; 33 | this.ip = options.ip; 34 | } 35 | if (typeof options === "undefined") options = {}; 36 | this.port = options.port || TELNET_PORT; // telnet server port 37 | 38 | // internal buffer 39 | this._buffer = Buffer.alloc(0); 40 | this._id = 0; 41 | this._cmd = 0; 42 | this._length = 0; 43 | 44 | // handle callback - call a callback function only once, for the first event 45 | // it will triger 46 | const handleCallback = function(had_error) { 47 | if (self.callback) { 48 | self.callback(had_error); 49 | self.callback = null; 50 | } 51 | }; 52 | 53 | if(options.socket) { 54 | if(options.socket instanceof net.Socket) { 55 | this._externalSocket = options.socket; 56 | this.openFlag = this._externalSocket.readyState === "opening" || this._externalSocket.readyState === "open"; 57 | } else { 58 | throw new Error("invalid socket provided"); 59 | } 60 | } 61 | 62 | // create a socket 63 | this._client = this._externalSocket || new net.Socket(); 64 | if (options.timeout) this._client.setTimeout(options.timeout); 65 | 66 | // register the port data event 67 | this._client.on("data", function onData(data) { 68 | // add data to buffer 69 | self._buffer = Buffer.concat([self._buffer, data]); 70 | 71 | // check if buffer include a complete modbus answer 72 | const expectedLength = self._length; 73 | const bufferLength = self._buffer.length; 74 | modbusSerialDebug( 75 | "on data expected length:" + 76 | expectedLength + 77 | " buffer length:" + 78 | bufferLength 79 | ); 80 | 81 | modbusSerialDebug({ 82 | action: "receive tcp telnet port", 83 | data: data, 84 | buffer: self._buffer 85 | }); 86 | modbusSerialDebug( 87 | JSON.stringify({ 88 | action: "receive tcp telnet port strings", 89 | data: data, 90 | buffer: self._buffer 91 | }) 92 | ); 93 | 94 | // check data length 95 | if (expectedLength < 6 || bufferLength < EXCEPTION_LENGTH) return; 96 | 97 | // loop and check length-sized buffer chunks 98 | const maxOffset = bufferLength - EXCEPTION_LENGTH; 99 | for (let i = 0; i <= maxOffset; i++) { 100 | const unitId = self._buffer[i]; 101 | const functionCode = self._buffer[i + 1]; 102 | 103 | if (unitId !== self._id) continue; 104 | 105 | if ( 106 | functionCode === self._cmd && 107 | i + expectedLength <= bufferLength 108 | ) { 109 | self._emitData(i, expectedLength); 110 | return; 111 | } 112 | if ( 113 | functionCode === (0x80 | self._cmd) && 114 | i + EXCEPTION_LENGTH <= bufferLength 115 | ) { 116 | self._emitData(i, EXCEPTION_LENGTH); 117 | return; 118 | } 119 | 120 | // frame header matches, but still missing bytes pending 121 | if (functionCode === (0x7f & self._cmd)) break; 122 | } 123 | }); 124 | 125 | this._client.on("connect", function() { 126 | self.openFlag = true; 127 | handleCallback(); 128 | }); 129 | 130 | this._client.on("close", function(had_error) { 131 | self.openFlag = false; 132 | handleCallback(had_error); 133 | self.emit("close"); 134 | }); 135 | 136 | this._client.on("error", function(had_error) { 137 | self.openFlag = false; 138 | handleCallback(had_error); 139 | }); 140 | 141 | this._client.on("timeout", function() { 142 | // modbus.openFlag is left in its current state as it reflects two types of timeouts, 143 | // i.e. 'false' for "TCP connection timeout" and 'true' for "Modbus response timeout" 144 | // (this allows to continue Modbus request re-tries without reconnecting TCP). 145 | modbusSerialDebug("TelnetPort port: TimedOut"); 146 | handleCallback(new Error("TelnetPort Connection Timed Out.")); 147 | }); 148 | } 149 | 150 | /** 151 | * Check if port is open. 152 | * 153 | * @returns {boolean} 154 | */ 155 | get isOpen() { 156 | return this.openFlag; 157 | } 158 | 159 | /** 160 | * Emit the received response, cut the buffer and reset the internal vars. 161 | * 162 | * @param {number} start the start index of the response within the buffer 163 | * @param {number} length the length of the response 164 | * @private 165 | */ 166 | _emitData(start, length) { 167 | this.emit("data", this._buffer.slice(start, start + length)); 168 | this._buffer = this._buffer.slice(start + length); 169 | 170 | // reset internal vars 171 | this._id = 0; 172 | this._cmd = 0; 173 | this._length = 0; 174 | } 175 | 176 | /** 177 | * Simulate successful port open. 178 | * 179 | * @param callback 180 | */ 181 | open(callback) { 182 | if(this._externalSocket === null) { 183 | this.callback = callback; 184 | this._client.connect(this.port, this.ip); 185 | } else if(this.openFlag) { 186 | modbusSerialDebug("telnet port: external socket is opened"); 187 | callback(); // go ahead to setup existing socket 188 | } else { 189 | callback(new Error("telnet port: external socket is not opened")); 190 | } 191 | } 192 | 193 | /** 194 | * Simulate successful close port. 195 | * 196 | * @param callback 197 | */ 198 | close(callback) { 199 | this.callback = callback; 200 | this._client.end(); 201 | this.removeAllListeners(); 202 | } 203 | 204 | /** 205 | * Simulate successful destroy port. 206 | * 207 | * @param callback 208 | */ 209 | destroy(callback) { 210 | this.callback = callback; 211 | if (!this._client.destroyed) { 212 | this._client.destroy(); 213 | } 214 | } 215 | 216 | /** 217 | * Send data to a modbus slave via telnet server. 218 | * 219 | * @param {Buffer} data 220 | */ 221 | write(data) { 222 | if (data.length < MIN_DATA_LENGTH) { 223 | modbusSerialDebug( 224 | "expected length of data is to small - minimum is " + 225 | MIN_DATA_LENGTH 226 | ); 227 | return; 228 | } 229 | 230 | let length = null; 231 | 232 | // remember current unit and command 233 | this._id = data[0]; 234 | this._cmd = data[1]; 235 | 236 | // calculate expected answer length 237 | switch (this._cmd) { 238 | case 1: 239 | case 2: 240 | length = data.readUInt16BE(4); 241 | this._length = 3 + parseInt((length - 1) / 8 + 1) + 2; 242 | break; 243 | case 3: 244 | case 4: 245 | length = data.readUInt16BE(4); 246 | this._length = 3 + 2 * length + 2; 247 | break; 248 | case 5: 249 | case 6: 250 | case 15: 251 | case 16: 252 | this._length = 6 + 2; 253 | break; 254 | default: 255 | // raise and error ? 256 | this._length = 0; 257 | break; 258 | } 259 | 260 | // send buffer to slave 261 | this._client.write(data); 262 | 263 | modbusSerialDebug({ 264 | action: "send tcp telnet port", 265 | data: data, 266 | unitid: this._id, 267 | functionCode: this._cmd 268 | }); 269 | 270 | modbusSerialDebug( 271 | JSON.stringify({ 272 | action: "send tcp telnet port strings", 273 | data: data, 274 | unitid: this._id, 275 | functionCode: this._cmd 276 | }) 277 | ); 278 | } 279 | } 280 | 281 | /** 282 | * Telnet port for Modbus. 283 | * 284 | * @type {TelnetPort} 285 | */ 286 | module.exports = TelnetPort; 287 | -------------------------------------------------------------------------------- /ports/udpport.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const events = require("events"); 3 | const EventEmitter = events.EventEmitter || events; 4 | const dgram = require("dgram"); 5 | const modbusSerialDebug = require("debug")("modbus-serial"); 6 | 7 | const crc16 = require("../utils/crc16"); 8 | 9 | /* TODO: const should be set once, maybe */ 10 | const MODBUS_PORT = 502; // modbus port 11 | const MAX_TRANSACTIONS = 256; // maximum transaction to wait for 12 | const MIN_DATA_LENGTH = 6; 13 | const MIN_MBAP_LENGTH = 6; 14 | const CRC_LENGTH = 2; 15 | 16 | class ModbusUdpPort extends EventEmitter { 17 | /** 18 | * Simulate a modbus-RTU port using modbus-udp. 19 | * 20 | * @param ip 21 | * @param options 22 | * @constructor 23 | */ 24 | constructor(ip, options) { 25 | super(); 26 | 27 | const modbus = this; 28 | this.ip = ip; 29 | this.openFlag = false; 30 | this._transactionIdWrite = 1; 31 | this.port = options.port || MODBUS_PORT; 32 | 33 | // options 34 | if (typeof(options) === "undefined") options = {}; 35 | 36 | // create a socket 37 | this._client = dgram.createSocket("udp4"); 38 | 39 | // Bind to the same port as we're sending to 40 | this._client.bind(); 41 | 42 | // wait for answer 43 | const self = this; 44 | this._client.on("message", function(data, rinfo) { 45 | let buffer; 46 | let crc; 47 | let length; 48 | 49 | // Filter stuff not intended for us 50 | if(rinfo.address !== self.ip || rinfo.port !== self.port) 51 | { 52 | return; 53 | } 54 | 55 | // data received 56 | modbusSerialDebug({ action: "receive udp port strings", data: data }); 57 | 58 | // check data length 59 | while (data.length > MIN_MBAP_LENGTH) { 60 | // parse udp header length 61 | length = data.readUInt16BE(4); 62 | 63 | // cut 6 bytes of mbap and copy pdu 64 | buffer = Buffer.alloc(length + CRC_LENGTH); 65 | data.copy(buffer, 0, MIN_MBAP_LENGTH); 66 | 67 | // add crc to message 68 | crc = crc16(buffer.slice(0, -CRC_LENGTH)); 69 | buffer.writeUInt16LE(crc, buffer.length - CRC_LENGTH); 70 | 71 | // update transaction id and emit data 72 | modbus._transactionIdRead = data.readUInt16BE(0); 73 | modbus.emit("data", buffer); 74 | 75 | // debug 76 | modbusSerialDebug({ action: "parsed udp port", buffer: buffer, transactionId: modbus._transactionIdRead }); 77 | 78 | // reset data 79 | data = data.slice(length + MIN_MBAP_LENGTH); 80 | } 81 | }); 82 | 83 | this._client.on("listening", function() { 84 | modbus.openFlag = true; 85 | }); 86 | 87 | this._client.on("close", function() { 88 | modbus.openFlag = false; 89 | }); 90 | } 91 | 92 | /** 93 | * Check if port is open. 94 | * 95 | * @returns {boolean} 96 | */ 97 | get isOpen() { 98 | return this.openFlag; 99 | } 100 | 101 | /** 102 | * Simulate successful port open. 103 | * 104 | * @param callback 105 | */ 106 | // eslint-disable-next-line class-methods-use-this 107 | open(callback) { 108 | if (callback) 109 | callback(null); 110 | } 111 | 112 | /** 113 | * Simulate successful close port. 114 | * 115 | * @param callback 116 | */ 117 | close(callback) { 118 | this._client.close(); 119 | if (callback) 120 | callback(null); 121 | } 122 | 123 | /** 124 | * Send data to a modbus-udp slave. 125 | * 126 | * @param data 127 | */ 128 | write(data) { 129 | if(data.length < MIN_DATA_LENGTH) { 130 | modbusSerialDebug("expected length of data is too small - minimum is " + MIN_DATA_LENGTH); 131 | return; 132 | } 133 | 134 | // remember current unit and command 135 | this._id = data[0]; 136 | this._cmd = data[1]; 137 | 138 | // remove crc and add mbap 139 | const buffer = Buffer.alloc(data.length + MIN_MBAP_LENGTH - CRC_LENGTH); 140 | buffer.writeUInt16BE(this._transactionIdWrite, 0); 141 | buffer.writeUInt16BE(0, 2); 142 | buffer.writeUInt16BE(data.length - CRC_LENGTH, 4); 143 | data.copy(buffer, MIN_MBAP_LENGTH); 144 | 145 | 146 | modbusSerialDebug({ 147 | action: "send modbus udp port", 148 | data: data, 149 | buffer: buffer, 150 | unitid: this._id, 151 | functionCode: this._cmd 152 | }); 153 | 154 | // send buffer via udp 155 | this._client.send(buffer, 0, buffer.length, this.port, this.ip); 156 | 157 | // set next transaction id 158 | this._transactionIdWrite = (this._transactionIdWrite + 1) % MAX_TRANSACTIONS; 159 | 160 | } 161 | } 162 | 163 | /** 164 | * UDP port for Modbus. 165 | * 166 | * @type {ModbusUdpPort} 167 | */ 168 | module.exports = ModbusUdpPort; 169 | -------------------------------------------------------------------------------- /servers/serverserial_pipe_handler.js: -------------------------------------------------------------------------------- 1 | const stream = require("stream"); 2 | 3 | class ServerSerialPipeHandler extends stream.Transform { 4 | constructor({ maxBufferSize = 65536, interval, transformOptions }) { 5 | super(transformOptions); 6 | if (!interval) { 7 | throw new TypeError("\"interval\" is required"); 8 | } 9 | if (typeof interval !== "number" || Number.isNaN(interval)) { 10 | throw new TypeError("\"interval\" is not a number"); 11 | } 12 | if (interval < 1) { 13 | throw new TypeError("\"interval\" is not greater than 0"); 14 | } 15 | if (typeof maxBufferSize !== "number" || Number.isNaN(maxBufferSize)) { 16 | throw new TypeError("\"maxBufferSize\" is not a number"); 17 | } 18 | if (maxBufferSize < 1) { 19 | throw new TypeError("\"maxBufferSize\" is not greater than 0"); 20 | } 21 | this.maxBufferSize = maxBufferSize; 22 | this.currentPacket = Buffer.from([]); 23 | this.interval = interval; 24 | } 25 | 26 | _transform(chunk, encoding, cb) { 27 | if (this.intervalID) { 28 | clearTimeout(this.intervalID); 29 | } 30 | 31 | let offset = 0; 32 | while ((this.currentPacket.length + chunk.length) >= this.maxBufferSize) { 33 | this.currentPacket = Buffer.concat([this.currentPacket, chunk.slice(offset, this.maxBufferSize - this.currentPacket.length)]); 34 | offset = offset + this.maxBufferSize; 35 | chunk = chunk.slice(offset); 36 | this.emitPacket(); 37 | } 38 | this.currentPacket = Buffer.concat([this.currentPacket, chunk]); 39 | this.intervalID = setTimeout(this.emitPacket.bind(this), this.interval); 40 | cb(); 41 | } 42 | emitPacket() { 43 | if (this.intervalID) { 44 | clearTimeout(this.intervalID); 45 | } 46 | if (this.currentPacket.length > 0) { 47 | this.push(this.currentPacket); 48 | } 49 | this.currentPacket = Buffer.from([]); 50 | } 51 | _flush(cb) { 52 | this.emitPacket(); 53 | cb(); 54 | } 55 | } 56 | 57 | module.exports = ServerSerialPipeHandler; 58 | -------------------------------------------------------------------------------- /test/Lint/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable no-undef */ 3 | 4 | const lint = require("mocha-eslint"); 5 | 6 | const paths = [ 7 | "index.js", 8 | "ports/**/*.js", 9 | "servers/**/*.js", 10 | "apis/**/*.js", 11 | "utils/**/*.js", 12 | "examples/**/*.js", 13 | "test/**/*.js" 14 | ]; 15 | 16 | const options = { 17 | // Specify style of output 18 | formatter: "compact", // Defaults to `stylish` 19 | timeout: 10000 20 | }; 21 | 22 | // Run the tests 23 | lint(paths, options); 24 | -------------------------------------------------------------------------------- /test/apis/promise.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable no-undef */ 3 | 4 | const ModbusRTU = require("../../index"); 5 | const TestPort = ModbusRTU.TestPort; 6 | const testPort = new TestPort(); 7 | const modbusRTU = new ModbusRTU(testPort); 8 | 9 | const expect = require("chai").expect; 10 | 11 | describe("Promise", function() { 12 | 13 | describe("Functions", function() { 14 | it("should bind promise functions on modbusRTU", function() { 15 | const address = 1; 16 | const arg = 1; 17 | const value = 1; 18 | const deviceIdCode = 1; 19 | const objectId = 2; 20 | 21 | modbusRTU.open(); 22 | expect(modbusRTU.readCoils(address, arg)).to.be.instanceOf(Promise); 23 | expect(modbusRTU.readDiscreteInputs(address, arg)).to.be.instanceOf(Promise); 24 | expect(modbusRTU.readHoldingRegisters(address, arg)).to.be.instanceOf(Promise); 25 | expect(modbusRTU.readInputRegisters(address, arg)).to.be.instanceOf(Promise); 26 | expect(modbusRTU.writeCoil(address, value)).to.be.instanceOf(Promise); 27 | expect(modbusRTU.writeRegister(address, value)).to.be.instanceOf(Promise); 28 | expect(modbusRTU.writeRegisters(address, [value])).to.be.instanceOf(Promise); 29 | expect(modbusRTU.readDeviceIdentification(deviceIdCode, objectId)).to.be.instanceOf(Promise); 30 | expect(modbusRTU.maskWriteRegister(address, arg, arg)).to.be.instanceOf(Promise); 31 | }); 32 | }); 33 | 34 | describe("#setID", function() { 35 | it("should set a unit id on modubusRtu instance", function() { 36 | const someId = 1; 37 | modbusRTU.setID(someId); 38 | expect(modbusRTU._unitID).to.be.equal(someId); 39 | }); 40 | }); 41 | 42 | describe("#getID", function() { 43 | it("should return the unit id of modubusRtu instance", function() { 44 | const someId = 1; 45 | modbusRTU._unitID = someId; 46 | expect(modbusRTU.getID()).to.be.equal(someId); 47 | }); 48 | }); 49 | 50 | describe("#setTimeout", function() { 51 | it("should set a timeout on modubusRtu instance", function() { 52 | const timeout = 1000; 53 | modbusRTU.setTimeout(timeout); 54 | expect(modbusRTU._timeout).to.be.equal(timeout); 55 | }); 56 | }); 57 | 58 | describe("#getTimeout", function() { 59 | it("should return the timeout of modubusRtu instance", function() { 60 | modbusRTU._timeout = 1000; 61 | expect(modbusRTU.getTimeout()).to.be.equal(modbusRTU._timeout); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/mocks/SerialPortMock.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const events = require("events"); 3 | const EventEmitter = events.EventEmitter || events; 4 | 5 | class SerialPortMock extends EventEmitter { 6 | /** 7 | * Mock for SerialPort 8 | */ 9 | constructor(options, callback) { 10 | super(); 11 | 12 | this._openFlag = false; 13 | if (callback) { 14 | callback(null); 15 | } 16 | } 17 | 18 | get isOpen() { 19 | return this._openFlag; 20 | } 21 | 22 | open(callback) { 23 | this._openFlag = true; 24 | if (callback) { 25 | callback(null); 26 | } 27 | this.emit("open"); 28 | } 29 | 30 | write(buffer, callback) { 31 | this._data = buffer; 32 | if (callback) { 33 | callback(null); 34 | } 35 | } 36 | 37 | close(callback) { 38 | this._openFlag = false; 39 | if (callback) { 40 | callback(null); 41 | } 42 | this.emit("close"); 43 | } 44 | 45 | receive(buffer) { 46 | this.emit("data", buffer); 47 | } 48 | } 49 | 50 | module.exports = { SerialPort: SerialPortMock }; 51 | -------------------------------------------------------------------------------- /test/mocks/dgramMock.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const events = require("events"); 3 | const EventEmitter = events.EventEmitter || events; 4 | 5 | class Socket extends EventEmitter { 6 | connect(port, host, connectListener) { 7 | this.emit("connect"); 8 | if (connectListener) { 9 | connectListener(null); 10 | } 11 | } 12 | 13 | close(callback) { 14 | this.emit("close", false); 15 | if (callback) { 16 | callback(); 17 | } 18 | } 19 | 20 | send(buffer, offset, length, port, address, callback) { 21 | this._data = buffer; 22 | this._offset = offset; 23 | this._length = length; 24 | this._port = port; 25 | this._address = address; 26 | if (callback) { 27 | callback(null); 28 | } 29 | } 30 | 31 | bind() { 32 | this.emit("listening"); 33 | } 34 | 35 | // Obsolete? It doesn't reflect the dgram interface 36 | listen() { 37 | this.emit("listening"); 38 | } 39 | 40 | receive(buffer) { 41 | this.emit("message", buffer, { address: this._address, port: this._port, size: this._data.length }); 42 | } 43 | } 44 | 45 | exports.Socket = Socket; 46 | 47 | exports.createSocket = function(type, listener) { 48 | return new Socket(type, listener); 49 | }; 50 | -------------------------------------------------------------------------------- /test/mocks/netMock.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const events = require("events"); 3 | const EventEmitter = events.EventEmitter || events; 4 | 5 | class Socket extends EventEmitter { 6 | constructor() { 7 | super(); 8 | this.destroyed = false; 9 | } 10 | 11 | connect(port, host, connectListener) { 12 | this.emit("connect"); 13 | if (connectListener) { 14 | connectListener(null); 15 | } 16 | } 17 | 18 | setNoDelay() { 19 | return this; 20 | } 21 | 22 | end() { 23 | this.emit("close", false); 24 | } 25 | 26 | write(data) { 27 | this._data = data; 28 | return true; 29 | } 30 | 31 | receive(buffer) { 32 | this.emit("data", buffer); 33 | } 34 | 35 | destroy() { 36 | this.emit("close", true); 37 | this.destroyed = true; 38 | } 39 | } 40 | 41 | exports.Socket = Socket; 42 | 43 | class Server extends EventEmitter { 44 | connect(socket) { 45 | this.emit("connection", socket); 46 | } 47 | 48 | listen() { 49 | this.emit("listening"); 50 | } 51 | 52 | end() { 53 | this.emit("close", false); 54 | } 55 | 56 | receive(buffer) { 57 | this.emit("data", buffer); 58 | } 59 | } 60 | 61 | exports.Server = Server; 62 | 63 | exports.createServer = function(options, connectionListener) { 64 | return new Server(options, connectionListener); 65 | }; 66 | -------------------------------------------------------------------------------- /test/ports/bleport.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable no-undef */ 3 | 4 | const expect = require("chai").expect; 5 | const BlePort = require("./../../ports/bleport"); 6 | const { DeviceMock, WebBluetoothMock } = require("web-bluetooth-mock"); 7 | 8 | const TX_SERVICE_UUID = "0000ffd0-0000-1000-8000-00805f9b34fb"; 9 | const TX_CHARACTERISTIC_UUID = "0000ffd1-0000-1000-8000-00805f9b34fb"; 10 | const RX_SERVICE_UUID = "0000fff0-0000-1000-8000-00805f9b34fb"; 11 | const RX_CHARACTERISTIC_UUID = "0000fff1-0000-1000-8000-00805f9b34fb"; 12 | 13 | describe("Bluetooth LE port", function() { 14 | function prepare() { 15 | const deviceMock = new DeviceMock("Test Device", [TX_SERVICE_UUID, RX_SERVICE_UUID]); 16 | const txServiceMock = deviceMock.getServiceMock(TX_SERVICE_UUID); 17 | const txCharacteristicMock = txServiceMock.getCharacteristicMock(TX_CHARACTERISTIC_UUID); 18 | const rxServiceMock = deviceMock.getServiceMock(RX_SERVICE_UUID); 19 | const rxCharacteristicMock = txServiceMock.getCharacteristicMock(RX_CHARACTERISTIC_UUID); 20 | 21 | const bluetooth = new WebBluetoothMock([deviceMock]); 22 | 23 | const port = new BlePort({ 24 | bluetooth, 25 | txService: TX_SERVICE_UUID, 26 | txCharacteristic: TX_CHARACTERISTIC_UUID, 27 | rxService: RX_SERVICE_UUID, 28 | rxCharacteristic: RX_CHARACTERISTIC_UUID 29 | }); 30 | return { bluetooth, deviceMock, port, rxCharacteristicMock, rxServiceMock, txCharacteristicMock, txServiceMock }; 31 | } 32 | 33 | describe("#isOpen", function() { 34 | it("should not be open before #open", function() { 35 | const { port } = prepare(); 36 | expect(port.isOpen).to.be.false; 37 | }); 38 | 39 | it("should be open after #open", function(done) { 40 | const { port } = prepare(); 41 | port.open(function(error) { 42 | expect(error).to.be.undefined; 43 | expect(port.isOpen).to.be.true; 44 | done(); 45 | }); 46 | }); 47 | 48 | it("should not be open after #close", function(done) { 49 | const { port } = prepare(); 50 | port.open(function(error) { 51 | expect(error).to.be.undefined; 52 | 53 | port.close(function(error) { 54 | expect(error).to.be.undefined; 55 | expect(port.isOpen).to.be.false; 56 | done(); 57 | }); 58 | }); 59 | }); 60 | }); 61 | 62 | describe("data handler", function() { 63 | it("should return a valid Modbus RTU message", function(done) { 64 | const { port } = prepare(); 65 | 66 | port.once("data", function(data) { 67 | expect(data.toString("hex")).to.equal("110306ae415652434049ad"); 68 | done(); 69 | }); 70 | 71 | 72 | port.open(function(error) { 73 | expect(error).to.be.undefined; 74 | 75 | port.write(Buffer.from("1103006B00037687", "hex")); 76 | 77 | setTimeout(function() { 78 | const buffer = Buffer.from("110306ae415652434049ad", "hex"); 79 | port._handleCharacteristicValueChanged(bufferToEvent(buffer)); 80 | }); 81 | }); 82 | }); 83 | }); 84 | 85 | describe("#write", function() { 86 | it("should write a valid RTU message to the port", function(done) { 87 | const { port, txCharacteristicMock } = prepare(); 88 | 89 | txCharacteristicMock.writeValue = (buffer) => { 90 | expect(Buffer.from(buffer).toString("hex")).to.equal("1103006b00037687"); 91 | done(); 92 | }; 93 | 94 | port.open(function(error) { 95 | expect(error).to.be.undefined; 96 | 97 | port.write(Buffer.from("1103006B00037687", "hex")); 98 | }); 99 | }); 100 | }); 101 | }); 102 | 103 | function bufferToEvent(buffer) { 104 | const arrayBuffer = BlePort._bufferToArrayBuffer(buffer); 105 | const dataView = new DataView(arrayBuffer); 106 | return { target: { value: dataView } }; 107 | } 108 | -------------------------------------------------------------------------------- /test/ports/c701port.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable no-undef */ 3 | 4 | const expect = require("chai").expect; 5 | const mockery = require("mockery"); 6 | 7 | describe("Modbus UDP port", function() { 8 | let port; 9 | 10 | before(function() { 11 | const mock = require("./../mocks/dgramMock"); 12 | mockery.resetCache(); 13 | mockery.enable({ warnOnReplace: false, useCleanCache: true, warnOnUnregistered: false }); 14 | mockery.registerMock("dgram", mock); 15 | const UdpPort = require("./../../ports/c701port"); 16 | port = new UdpPort("127.0.0.1", { port: 9999 }); 17 | }); 18 | 19 | after(function() { 20 | mockery.disable(); 21 | }); 22 | 23 | afterEach(function() { 24 | port.close(); 25 | }); 26 | 27 | describe("#isOpen", function() { 28 | it("should not be open before #open", function() { 29 | expect(port.isOpen).to.be.false; 30 | }); 31 | 32 | it("should be open after onListening", function(done) { 33 | port._client.listen(); 34 | setTimeout(function() { 35 | expect(port.isOpen).to.be.true; 36 | done(); 37 | }); 38 | }); 39 | 40 | it("should not be open after #close", function(done) { 41 | port._client.listen(); 42 | setTimeout(function() { 43 | port.close(function() { 44 | expect(port.isOpen).to.be.false; 45 | done(); 46 | }); 47 | }); 48 | }); 49 | }); 50 | 51 | describe("data handler", function() { 52 | it("should return a valid Modbus RTU message", function(done) { 53 | port.once("data", function(data) { 54 | expect(data.toString("hex")).to.equal("110306ae415652434049ad"); 55 | done(); 56 | }); 57 | port.open(function() { 58 | port.write(Buffer.from("1103006B00037687", "hex")); 59 | 60 | if (port._client._data.slice(-8).equals(Buffer.from("1103006B00037687", "hex"))) { 61 | const buffer = Buffer.alloc(116 + 11).fill(0); 62 | buffer.writeUInt16LE(602, 2); // C701 magic for serial bridge 63 | buffer.writeUInt16LE(0, 36); // C701 RS485 connector (0..2) 64 | buffer.writeUInt16LE(0, 38); // expected serial answer length 65 | buffer.writeUInt16LE(1, 102); // C7011 RS481 hub (1..2) 66 | buffer.writeUInt16LE(11, 104); // serial data length 67 | 68 | // add serial line data 69 | Buffer.from("110306ae415652434049ad", "hex").copy(buffer, 116); 70 | port._client.receive(buffer); 71 | } 72 | }); 73 | }); 74 | 75 | it("should return a valid Modbus RTU exception", function(done) { 76 | port.once("data", function(data) { 77 | expect(data.toString("hex")).to.equal("1183044136"); 78 | done(); 79 | }); 80 | port.open(function() { 81 | port.write(Buffer.from("1103006B00037687", "hex")); 82 | 83 | if (port._client._data.slice(-8).equals(Buffer.from("1103006B00037687", "hex"))) { 84 | const buffer = Buffer.alloc(116 + 5).fill(0); 85 | buffer.writeUInt16LE(602, 2); // C701 magic for serial bridge 86 | buffer.writeUInt16LE(0, 36); // C701 RS485 connector (0..2) 87 | buffer.writeUInt16LE(0, 38); // expected serial answer length 88 | buffer.writeUInt16LE(1, 102); // C7011 RS481 hub (1..2) 89 | buffer.writeUInt16LE(11, 104); // serial data length 90 | 91 | // add serial line data 92 | Buffer.from("1183044136", "hex").copy(buffer, 116); 93 | port._client.receive(buffer); 94 | } 95 | }); 96 | }); 97 | }); 98 | 99 | describe("#write", function() { 100 | it("should write a valid message to the port", function() { 101 | port.write(Buffer.from("1103006B00037687", "hex")); 102 | expect(port._client._data.length).to.equal(116 + 8); 103 | expect(port._client._data.slice(-8).toString("hex")).to.equal("1103006b00037687"); 104 | expect(port._client._offset).to.equal(0); 105 | expect(port._client._length).to.equal(124); 106 | expect(port._client._port).to.equal(9999); 107 | expect(port._client._address).to.equal("127.0.0.1"); 108 | }); 109 | }); 110 | 111 | }); 112 | -------------------------------------------------------------------------------- /test/ports/rtubufferedport.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable no-undef */ 3 | 4 | const expect = require("chai").expect; 5 | const mockery = require("mockery"); 6 | 7 | const LONG_MSG = "010380018301830183018301830183018301830183018301830183018301830\ 8 | 1830183018301830183018301830183018301830183018301830183018301830183018301830183\ 9 | 0183018301830183018301830183018301830183018301830183018301830183018301830183018\ 10 | 3018301830183018301830183018301830183018346e0"; 11 | 12 | describe("Modbus RTU buffered port", function() { 13 | let port; 14 | 15 | before(function() { 16 | const mock = require("./../mocks/SerialPortMock"); 17 | mockery.resetCache(); 18 | mockery.enable({ warnOnReplace: false, useCleanCache: true, warnOnUnregistered: false }); 19 | mockery.registerMock("serialport", mock); 20 | const RTUBufferedPort = require("./../../ports/rtubufferedport"); 21 | port = new RTUBufferedPort("/dev/null", {}); 22 | }); 23 | 24 | after(function() { 25 | mockery.disable(); 26 | }); 27 | 28 | afterEach(function() { 29 | port.close(); 30 | }); 31 | 32 | describe("#isOpen", function() { 33 | it("should not be open before #open", function() { 34 | expect(port.isOpen).to.be.false; 35 | }); 36 | 37 | it("should be open after #open", function(done) { 38 | port.open(function() { 39 | expect(port.isOpen).to.be.true; 40 | done(); 41 | }); 42 | }); 43 | 44 | it("should not be open after #close", function(done) { 45 | port.open(function() { 46 | port.close(function() { 47 | expect(port.isOpen).to.be.false; 48 | done(); 49 | }); 50 | }); 51 | }); 52 | }); 53 | 54 | describe("data handler", function() { 55 | it("should return a valid Modbus RTU message", function(done) { 56 | port.once("data", function(data) { 57 | expect(data.toString("hex")).to.equal("110306ae415652434049ad"); 58 | done(); 59 | }); 60 | port.open(function() { 61 | port.write(Buffer.from("1103006B00037687", "hex")); 62 | setTimeout(function() { 63 | port._client.receive(Buffer.from("11", "hex")); 64 | port._client.receive(Buffer.from("03", "hex")); 65 | port._client.receive(Buffer.from("06", "hex")); 66 | port._client.receive(Buffer.from("ae", "hex")); 67 | port._client.receive(Buffer.from("41", "hex")); 68 | port._client.receive(Buffer.from("56", "hex")); 69 | port._client.receive(Buffer.from("52", "hex")); 70 | port._client.receive(Buffer.from("43", "hex")); 71 | port._client.receive(Buffer.from("40", "hex")); 72 | port._client.receive(Buffer.from("49", "hex")); 73 | port._client.receive(Buffer.from("ad", "hex")); 74 | }); 75 | }); 76 | }); 77 | 78 | it("should return a valid Modbus RTU exception", function(done) { 79 | port.once("data", function(data) { 80 | expect(data.toString("hex")).to.equal("1183044136"); 81 | done(); 82 | }); 83 | port.open(function() { 84 | port.write(Buffer.from("1103006B00037687", "hex")); 85 | setTimeout(function() { 86 | port._client.receive(Buffer.from("11", "hex")); 87 | port._client.receive(Buffer.from("83", "hex")); 88 | port._client.receive(Buffer.from("04", "hex")); 89 | port._client.receive(Buffer.from("41", "hex")); 90 | port._client.receive(Buffer.from("36", "hex")); 91 | }); 92 | }); 93 | }); 94 | 95 | it("Special data package, should return a valid Modbus RTU message", function(done) { 96 | port.once("data", function(data) { 97 | expect(data.toString("hex")).to.equal(LONG_MSG); 98 | done(); 99 | }); 100 | port.open(function() { 101 | port.write(Buffer.from("010300000040443A", "hex")); 102 | setTimeout(function() { 103 | for (let i = 0; i < LONG_MSG.length; i += 2) { 104 | port._client.receive(Buffer.from(LONG_MSG.slice(i, i + 2), "hex")); 105 | } 106 | }); 107 | }); 108 | }); 109 | 110 | it("Illegal start chars, should synchronize to valid Modbus RTU message", function(done) { 111 | port.once("data", function(data) { 112 | expect(data.toString("hex")).to.equal("110306ae415652434049ad"); 113 | done(); 114 | }); 115 | port.open(function() { 116 | port.write(Buffer.from("1103006B00037687", "hex")); 117 | setTimeout(function() { 118 | port._client.receive(Buffer.from("20", "hex")); // illegal char 119 | port._client.receive(Buffer.from("54", "hex")); // illegal char 120 | port._client.receive(Buffer.from("54", "hex")); // illegal char 121 | port._client.receive(Buffer.from("ff", "hex")); // illegal char 122 | port._client.receive(Buffer.from("11", "hex")); 123 | port._client.receive(Buffer.from("03", "hex")); 124 | port._client.receive(Buffer.from("06", "hex")); 125 | port._client.receive(Buffer.from("ae", "hex")); 126 | port._client.receive(Buffer.from("41", "hex")); 127 | port._client.receive(Buffer.from("56", "hex")); 128 | port._client.receive(Buffer.from("52", "hex")); 129 | port._client.receive(Buffer.from("43", "hex")); 130 | port._client.receive(Buffer.from("40", "hex")); 131 | port._client.receive(Buffer.from("49", "hex")); 132 | port._client.receive(Buffer.from("ad", "hex")); 133 | }); 134 | }); 135 | }); 136 | 137 | it("Illegal end chars, should return a valid Modbus RTU message", function(done) { 138 | port.once("data", function(data) { 139 | expect(data.toString("hex")).to.equal("110306ae415652434049ad"); 140 | done(); 141 | }); 142 | port.open(function() { 143 | port.write(Buffer.from("1103006B00037687", "hex")); 144 | setTimeout(function() { 145 | port._client.receive(Buffer.from("11", "hex")); 146 | port._client.receive(Buffer.from("03", "hex")); 147 | port._client.receive(Buffer.from("06", "hex")); 148 | port._client.receive(Buffer.from("ae", "hex")); 149 | port._client.receive(Buffer.from("41", "hex")); 150 | port._client.receive(Buffer.from("56", "hex")); 151 | port._client.receive(Buffer.from("52", "hex")); 152 | port._client.receive(Buffer.from("43", "hex")); 153 | port._client.receive(Buffer.from("40", "hex")); 154 | port._client.receive(Buffer.from("49", "hex")); 155 | port._client.receive(Buffer.from("ad", "hex")); 156 | port._client.receive(Buffer.from("20", "hex")); // illegal char 157 | port._client.receive(Buffer.from("54", "hex")); // illegal char 158 | port._client.receive(Buffer.from("54", "hex")); // illegal char 159 | port._client.receive(Buffer.from("ff", "hex")); // illegal char 160 | }); 161 | }); 162 | }); 163 | 164 | it("should return a valid Modbus RTU message on illegal chars", function(done) { 165 | port.once("data", function(data) { 166 | expect(data.toString("hex")).to.equal("110306ae415652434049ad"); 167 | done(); 168 | }); 169 | port.open(function() { 170 | port.write(Buffer.from("1103006B00037687", "hex")); 171 | setTimeout(function() { 172 | port._client.receive(Buffer.from("11", "hex")); 173 | port._client.receive(Buffer.from("03", "hex")); 174 | port._client.receive(Buffer.from("06", "hex")); 175 | port._client.receive(Buffer.from("ae", "hex")); 176 | port._client.receive(Buffer.from("41", "hex")); 177 | port._client.receive(Buffer.from("56", "hex")); 178 | port._client.receive(Buffer.from("52", "hex")); 179 | port._client.receive(Buffer.from("43", "hex")); 180 | port._client.receive(Buffer.from("40", "hex")); 181 | port._client.receive(Buffer.from("49", "hex")); 182 | port._client.receive(Buffer.from("ad", "hex")); 183 | }); 184 | }); 185 | }); 186 | }); 187 | 188 | describe("#write", function() { 189 | it("should write a valid RTU message to the port", function() { 190 | port.write(Buffer.from("1103006B00037687", "hex")); 191 | expect(port._client._data.toString("hex")).to.equal("1103006b00037687"); 192 | }); 193 | }); 194 | }); 195 | -------------------------------------------------------------------------------- /test/ports/tcpport.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable no-undef */ 3 | 4 | const expect = require("chai").expect; 5 | const mockery = require("mockery"); 6 | 7 | describe("Modbus TCP port methods", function() { 8 | let port; 9 | 10 | before(function() { 11 | const mock = require("./../mocks/netMock"); 12 | mockery.resetCache(); 13 | mockery.enable({ warnOnReplace: false, useCleanCache: true, warnOnUnregistered: false }); 14 | mockery.registerMock("net", mock); 15 | const TcpPort = require("./../../ports/tcpport"); 16 | port = new TcpPort("127.0.0.1", { port: 9999 }); 17 | }); 18 | 19 | after(function() { 20 | mockery.disable(); 21 | }); 22 | 23 | afterEach(function() { 24 | port.close(); 25 | }); 26 | 27 | describe("Modbus TCP port constructor", function() { 28 | const TcpPort = require("./../../ports/tcpport"); 29 | it("with ip as string", function() { 30 | const port = new TcpPort("localhost"); 31 | 32 | expect(port).to.be.an.instanceOf(TcpPort); 33 | expect(port.connectOptions.host).to.equal("localhost"); 34 | expect(port.connectOptions.port).to.equal(502); 35 | }); 36 | 37 | it("with ip as object", function() { 38 | const port = new TcpPort({ ip: "localhost" }); 39 | 40 | expect(port).to.be.an.instanceOf(TcpPort); 41 | expect(port.connectOptions.host).to.equal("localhost"); 42 | expect(port.connectOptions.port).to.equal(502); 43 | }); 44 | 45 | it("with ip as object and port as number", function() { 46 | const port = new TcpPort({ ip: "localhost", port: 9999 }); 47 | 48 | expect(port).to.be.an.instanceOf(TcpPort); 49 | expect(port.connectOptions.host).to.equal("localhost"); 50 | expect(port.connectOptions.port).to.equal(9999); 51 | }); 52 | 53 | it("with ip as string and options as object", function() { 54 | const port = new TcpPort("localhost", { port: 9999 }); 55 | 56 | expect(port).to.be.an.instanceOf(TcpPort); 57 | expect(port.connectOptions.host).to.equal("localhost"); 58 | expect(port.connectOptions.port).to.equal(9999); 59 | }); 60 | 61 | it("with socket creation options", function() { 62 | const controller = new AbortController(); 63 | const port = new TcpPort("localhost", { port: 9999, 64 | socketOpts: { 65 | allowHalfOpen: true, 66 | readable: true, 67 | writable: true, 68 | signal: controller.signal 69 | } }); 70 | 71 | expect(port).to.be.an.instanceOf(TcpPort); 72 | expect(port.connectOptions.host).to.equal("localhost"); 73 | expect(port.connectOptions.port).to.equal(9999); 74 | expect(port.socketOpts).to.deep.equal({ 75 | allowHalfOpen: true, 76 | readable: true, 77 | writable: true, 78 | signal: controller.signal 79 | }); 80 | }); 81 | }); 82 | 83 | describe("#isOpen", function() { 84 | it("should not be open before #open", function() { 85 | expect(port.isOpen).to.be.false; 86 | }); 87 | 88 | it("should be open after #open", function(done) { 89 | port.open(function() { 90 | expect(port.isOpen).to.be.true; 91 | done(); 92 | }); 93 | }); 94 | 95 | it("should not be open after #close", function(done) { 96 | port.open(function() { 97 | port.close(function() { 98 | setTimeout(function() { 99 | expect(port.isOpen).to.be.false; 100 | done(); 101 | }); 102 | }); 103 | }); 104 | }); 105 | it("should be able to be destroyed after opening", function(done) { 106 | port.open(function() { 107 | port.destroy(function() { 108 | setTimeout(function() { 109 | expect(port.isOpen).to.be.false; 110 | done(); 111 | }); 112 | }); 113 | }); 114 | }); 115 | }); 116 | 117 | describe("data handler", function() { 118 | it("should return a valid Modbus TCP message", function(done) { 119 | port.once("data", function(data) { 120 | expect(data.toString("hex")).to.equal("1103667788994fa2"); 121 | done(); 122 | }); 123 | port.open(function() { 124 | port.write(Buffer.from("1103006B00037687", "hex")); 125 | port._writeCompleted.then(function() { 126 | if (port._client._data.equals(Buffer.from("0001000000061103006B0003", "hex"))) { 127 | port._client.receive(Buffer.from("000100000006110366778899", "hex")); 128 | } 129 | }); 130 | }); 131 | }); 132 | 133 | it("should return a valid Modbus TCP exception", function(done) { 134 | port.once("data", function(data) { 135 | expect(data.toString("hex")).to.equal("1183044136"); 136 | done(); 137 | }); 138 | port.open(function() { 139 | port.write(Buffer.from("1103006B00037687", "hex")); 140 | port._writeCompleted.then(function() { 141 | if (port._client._data.equals(Buffer.from("0002000000061103006B0003", "hex"))) { 142 | port._client.receive(Buffer.from("000200000003118304", "hex")); 143 | } 144 | }); 145 | }); 146 | }); 147 | }); 148 | 149 | describe("#write", function() { 150 | it("should write a valid TCP message to the port", function(done) { 151 | port.write(Buffer.from("1103006B00037687", "hex")); 152 | port._writeCompleted.then(function() { 153 | expect(port._client._data.toString("hex")).to.equal("0003000000061103006b0003"); 154 | done(); 155 | }); 156 | }); 157 | }); 158 | 159 | }); 160 | -------------------------------------------------------------------------------- /test/ports/tcpportrtubuffered.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable no-undef */ 3 | 4 | const expect = require("chai").expect; 5 | const mockery = require("mockery"); 6 | 7 | describe("Modbus TCP RTU buffered port", function() { 8 | let port; 9 | 10 | function send(buffers) { 11 | port.open(function() { 12 | setTimeout(function() { 13 | buffers.forEach(function(buffer) { port._client.receive(buffer); }); 14 | port.close(); 15 | }); 16 | }); 17 | } 18 | 19 | before(function() { 20 | const mock = require("./../mocks/netMock"); 21 | mockery.resetCache(); 22 | mockery.enable({ warnOnReplace: false, useCleanCache: true, warnOnUnregistered: false }); 23 | mockery.registerMock("net", mock); 24 | const TcpRTUBufferedPort = require("./../../ports/tcprtubufferedport"); 25 | port = new TcpRTUBufferedPort("127.0.0.1", { port: 9999 }); 26 | }); 27 | 28 | after(function() { 29 | mockery.disable(); 30 | }); 31 | 32 | describe("#isOpen", function() { 33 | it("should not be open before #open", function() { 34 | expect(port.isOpen).to.be.false; 35 | }); 36 | 37 | it("should be open after #open", function(done) { 38 | port.open(function() { 39 | expect(port.isOpen).to.be.true; 40 | done(); 41 | }); 42 | }); 43 | 44 | it("should not be open after #close", function(done) { 45 | port.open(function() { 46 | port.close(function() { 47 | expect(port.isOpen).to.be.false; 48 | done(); 49 | }); 50 | }); 51 | }); 52 | it("should be able to be destroyed after opening", function(done) { 53 | port.open(function() { 54 | port.destroy(function() { 55 | setTimeout(function() { 56 | expect(port.isOpen).to.be.false; 57 | done(); 58 | }); 59 | }); 60 | }); 61 | }); 62 | it("should not be closed on timeout", function(done) { 63 | port.open(function() { 64 | expect(port.isOpen).to.be.true; 65 | port.emit("timeout"); 66 | expect(port.isOpen).to.be.true; 67 | done(); 68 | }); 69 | }); 70 | }); 71 | 72 | describe("data handler", function() { 73 | it("should return a valid Modbus TCP message at once", function(done) { 74 | port.once("data", function(data) { 75 | expect(data.toString("hex")).to.equal("1103667788994fa2"); 76 | done(); 77 | }); 78 | send([Buffer.from("000100000006110366778899", "hex")]); 79 | }); 80 | 81 | it("should return a valid Modbus TCP RTU message", function(done) { 82 | port.once("data", function(data) { 83 | expect(data.toString("hex")).to.equal("1103667788994fa2"); 84 | done(); 85 | }); 86 | send([ 87 | Buffer.from("0002000000", "hex"), 88 | Buffer.from("0611036677", "hex"), 89 | Buffer.from("8899", "hex") 90 | ]); 91 | }); 92 | 93 | it("should return a valid Modbus TCP exception", function(done) { 94 | port.once("data", function(data) { 95 | expect(data.toString("hex")).to.equal("1183044136"); 96 | done(); 97 | }); 98 | send([Buffer.from("000300000003118304", "hex")]); 99 | }); 100 | 101 | it("Illegal start chars, should synchronize to valid Modbus TCP RTU message", function(done) { 102 | port.once("data", function(data) { 103 | expect(data.toString("hex")).to.equal("1103667788994fa2"); 104 | done(); 105 | }); 106 | send([Buffer.from("3456000400000006110366778899", "hex")]); 107 | }); 108 | 109 | it("Illegal end chars, should return a valid Modbus TCP RTU message", function(done) { 110 | port.once("data", function(data) { 111 | expect(data.toString("hex")).to.equal("1103667788994fa2"); 112 | done(); 113 | }); 114 | send([Buffer.from("0005000000061103667788993456", "hex")]); 115 | }); 116 | }); 117 | 118 | describe("#write", function() { 119 | it("should write a valid TCP RTU message to the port", function() { 120 | port.write(Buffer.from("0103000500045408", "hex")); 121 | expect(port._client._data.toString("hex")).to.equal("000100000006010300050004"); 122 | }); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /test/ports/telnetport.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable no-undef */ 3 | 4 | const expect = require("chai").expect; 5 | const mockery = require("mockery"); 6 | 7 | const LONG_MSG = "010380018301830183018301830183018301830183018301830183018301830\ 8 | 1830183018301830183018301830183018301830183018301830183018301830183018301830183\ 9 | 0183018301830183018301830183018301830183018301830183018301830183018301830183018\ 10 | 3018301830183018301830183018301830183018346e0"; 11 | 12 | describe("Modbus Telnet port", function() { 13 | let port; 14 | 15 | before(function() { 16 | const mock = require("./../mocks/netMock"); 17 | mockery.resetCache(); 18 | mockery.enable({ warnOnReplace: false, useCleanCache: true, warnOnUnregistered: false }); 19 | mockery.registerMock("net", mock); 20 | const TelnetPort = require("./../../ports/telnetport"); 21 | port = new TelnetPort("127.0.0.1", { port: 9999 }); 22 | }); 23 | 24 | after(function() { 25 | mockery.disable(); 26 | }); 27 | 28 | afterEach(function() { 29 | port.close(); 30 | }); 31 | 32 | describe("#isOpen", function() { 33 | it("should not be open before #open", function() { 34 | expect(port.isOpen).to.be.false; 35 | }); 36 | 37 | it("should be open after #open", function(done) { 38 | port.open(function() { 39 | expect(port.isOpen).to.be.true; 40 | done(); 41 | }); 42 | }); 43 | 44 | it("should not be open after #close", function(done) { 45 | port.open(function() { 46 | port.close(function() { 47 | expect(port.isOpen).to.be.false; 48 | done(); 49 | }); 50 | }); 51 | }); 52 | it("should be able to be destroyed after opening", function(done) { 53 | port.open(function() { 54 | port.destroy(function() { 55 | setTimeout(function() { 56 | expect(port.isOpen).to.be.false; 57 | done(); 58 | }); 59 | }); 60 | }); 61 | }); 62 | it("should not be closed on timeout", function(done) { 63 | port.open(function() { 64 | expect(port.isOpen).to.be.true; 65 | port.emit("timeout"); 66 | expect(port.isOpen).to.be.true; 67 | done(); 68 | }); 69 | }); 70 | }); 71 | 72 | describe("data handler", function() { 73 | it("should return a valid Modbus RTU message", function(done) { 74 | port.once("data", function(data) { 75 | expect(data.toString("hex")).to.equal("110306ae415652434049ad"); 76 | done(); 77 | }); 78 | port.open(function() { 79 | port.write(Buffer.from("1103006B00037687", "hex")); 80 | setTimeout(function() { 81 | port._client.receive(Buffer.from("11", "hex")); 82 | port._client.receive(Buffer.from("03", "hex")); 83 | port._client.receive(Buffer.from("06", "hex")); 84 | port._client.receive(Buffer.from("ae", "hex")); 85 | port._client.receive(Buffer.from("41", "hex")); 86 | port._client.receive(Buffer.from("56", "hex")); 87 | port._client.receive(Buffer.from("52", "hex")); 88 | port._client.receive(Buffer.from("43", "hex")); 89 | port._client.receive(Buffer.from("40", "hex")); 90 | port._client.receive(Buffer.from("49", "hex")); 91 | port._client.receive(Buffer.from("ad", "hex")); 92 | }); 93 | }); 94 | }); 95 | 96 | it("should return a valid Modbus RTU exception", function(done) { 97 | port.once("data", function(data) { 98 | expect(data.toString("hex")).to.equal("1183044136"); 99 | done(); 100 | }); 101 | port.open(function() { 102 | port.write(Buffer.from("1103006B00037687", "hex")); 103 | setTimeout(function() { 104 | port._client.receive(Buffer.from("11", "hex")); 105 | port._client.receive(Buffer.from("83", "hex")); 106 | port._client.receive(Buffer.from("04", "hex")); 107 | port._client.receive(Buffer.from("41", "hex")); 108 | port._client.receive(Buffer.from("36", "hex")); 109 | }); 110 | }); 111 | }); 112 | 113 | it("Special data package, should return a valid Modbus RTU message", function(done) { 114 | port.once("data", function(data) { 115 | expect(data.toString("hex")).to.equal(LONG_MSG); 116 | done(); 117 | }); 118 | port.open(function() { 119 | port.write(Buffer.from("010300000040443A", "hex")); 120 | setTimeout(function() { 121 | for (let i = 0; i < LONG_MSG.length; i += 2) { 122 | port._client.receive(Buffer.from(LONG_MSG.slice(i, i + 2), "hex")); 123 | } 124 | }); 125 | }); 126 | }); 127 | 128 | it("Illegal start chars, should synchronize to valid Modbus RTU message", function(done) { 129 | port.once("data", function(data) { 130 | expect(data.toString("hex")).to.equal("110306ae415652434049ad"); 131 | done(); 132 | }); 133 | port.open(function() { 134 | port.write(Buffer.from("1103006B00037687", "hex")); 135 | setTimeout(function() { 136 | port._client.receive(Buffer.from("20", "hex")); // illegal char 137 | port._client.receive(Buffer.from("54", "hex")); // illegal char 138 | port._client.receive(Buffer.from("54", "hex")); // illegal char 139 | port._client.receive(Buffer.from("ff", "hex")); // illegal char 140 | port._client.receive(Buffer.from("11", "hex")); 141 | port._client.receive(Buffer.from("03", "hex")); 142 | port._client.receive(Buffer.from("06", "hex")); 143 | port._client.receive(Buffer.from("ae", "hex")); 144 | port._client.receive(Buffer.from("41", "hex")); 145 | port._client.receive(Buffer.from("56", "hex")); 146 | port._client.receive(Buffer.from("52", "hex")); 147 | port._client.receive(Buffer.from("43", "hex")); 148 | port._client.receive(Buffer.from("40", "hex")); 149 | port._client.receive(Buffer.from("49", "hex")); 150 | port._client.receive(Buffer.from("ad", "hex")); 151 | }); 152 | }); 153 | }); 154 | it("Illegal end chars, should return a valid Modbus RTU message", function(done) { 155 | port.once("data", function(data) { 156 | expect(data.toString("hex")).to.equal("110306ae415652434049ad"); 157 | done(); 158 | }); 159 | port.open(function() { 160 | port.write(Buffer.from("1103006B00037687", "hex")); 161 | setTimeout(function() { 162 | port._client.receive(Buffer.from("11", "hex")); 163 | port._client.receive(Buffer.from("03", "hex")); 164 | port._client.receive(Buffer.from("06", "hex")); 165 | port._client.receive(Buffer.from("ae", "hex")); 166 | port._client.receive(Buffer.from("41", "hex")); 167 | port._client.receive(Buffer.from("56", "hex")); 168 | port._client.receive(Buffer.from("52", "hex")); 169 | port._client.receive(Buffer.from("43", "hex")); 170 | port._client.receive(Buffer.from("40", "hex")); 171 | port._client.receive(Buffer.from("49", "hex")); 172 | port._client.receive(Buffer.from("ad", "hex")); 173 | port._client.receive(Buffer.from("20", "hex")); // illegal char 174 | port._client.receive(Buffer.from("54", "hex")); // illegal char 175 | port._client.receive(Buffer.from("54", "hex")); // illegal char 176 | port._client.receive(Buffer.from("ff", "hex")); // illegal char 177 | }); 178 | }); 179 | }); 180 | 181 | it("should return a valid Modbus RTU message on illegal chars", function(done) { 182 | port.once("data", function(data) { 183 | expect(data.toString("hex")).to.equal("110306ae415652434049ad"); 184 | done(); 185 | }); 186 | port.open(function() { 187 | port.write(Buffer.from("1103006B00037687", "hex")); 188 | setTimeout(function() { 189 | port._client.receive(Buffer.from("11", "hex")); 190 | port._client.receive(Buffer.from("03", "hex")); 191 | port._client.receive(Buffer.from("06", "hex")); 192 | port._client.receive(Buffer.from("ae", "hex")); 193 | port._client.receive(Buffer.from("41", "hex")); 194 | port._client.receive(Buffer.from("56", "hex")); 195 | port._client.receive(Buffer.from("52", "hex")); 196 | port._client.receive(Buffer.from("43", "hex")); 197 | port._client.receive(Buffer.from("40", "hex")); 198 | port._client.receive(Buffer.from("49", "hex")); 199 | port._client.receive(Buffer.from("ad", "hex")); 200 | }); 201 | }); 202 | }); 203 | }); 204 | 205 | describe("#write", function() { 206 | it("should write a valid RTU message to the port", function() { 207 | port.write(Buffer.from("1103006B00037687", "hex")); 208 | expect(port._client._data.toString("hex")).to.equal("1103006b00037687"); 209 | }); 210 | }); 211 | }); 212 | -------------------------------------------------------------------------------- /test/ports/udpport.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable no-undef */ 3 | 4 | const expect = require("chai").expect; 5 | const mockery = require("mockery"); 6 | 7 | describe("Modbus UDP port", function() { 8 | let port; 9 | 10 | before(function() { 11 | const mock = require("../mocks/dgramMock"); 12 | mockery.resetCache(); 13 | mockery.enable({ warnOnReplace: false, useCleanCache: true, warnOnUnregistered: false }); 14 | mockery.registerMock("dgram", mock); 15 | const UdpPort = require("../../ports/udpport"); 16 | port = new UdpPort("127.0.0.1", { port: 9999 }); 17 | }); 18 | 19 | after(function() { 20 | mockery.disable(); 21 | }); 22 | 23 | afterEach(function() { 24 | port.close(); 25 | }); 26 | 27 | describe("#isOpen", function() { 28 | it("should not be open before #open", function() { 29 | expect(port.isOpen).to.be.false; 30 | }); 31 | 32 | it("should be open after onListening", function(done) { 33 | port._client.listen(); 34 | setTimeout(function() { 35 | expect(port.isOpen).to.be.true; 36 | done(); 37 | }); 38 | }); 39 | 40 | it("should not be open after #close", function(done) { 41 | port._client.listen(); 42 | setTimeout(function() { 43 | port.close(function() { 44 | expect(port.isOpen).to.be.false; 45 | done(); 46 | }); 47 | }); 48 | }); 49 | }); 50 | 51 | describe("data handler", function() { 52 | it("should return a valid Modbus RTU message", function(done) { 53 | port.once("data", function(data) { 54 | expect(data.toString("hex")).to.equal("1103667788994fa2"); 55 | done(); 56 | }); 57 | port.open(function() { 58 | port.write(Buffer.from("1103006B00037687", "hex")); 59 | 60 | if (port._client._data.equals(Buffer.from("0001000000061103006B0003", "hex"))) { 61 | port._client.receive(Buffer.from("000100000006110366778899", "hex")); 62 | } 63 | }); 64 | }); 65 | 66 | it("should return a valid Modbus RTU exception", function(done) { 67 | port.once("data", function(data) { 68 | expect(data.toString("hex")).to.equal("1183044136"); 69 | done(); 70 | }); 71 | port.open(function() { 72 | port.write(Buffer.from("1103006B00037687", "hex")); 73 | 74 | if (port._client._data.equals(Buffer.from("0002000000061103006B0003", "hex"))) { 75 | port._client.receive(Buffer.from("000200000003118304", "hex")); 76 | } 77 | }); 78 | }); 79 | }); 80 | 81 | describe("#write", function() { 82 | it("should write a valid message to the port", function() { 83 | port.write(Buffer.from("1103006B00037687", "hex")); 84 | expect(port._client._data.toString("hex")).to.equal("0003000000061103006b0003"); 85 | }); 86 | }); 87 | 88 | }); 89 | -------------------------------------------------------------------------------- /test/servers/servertcp.enron.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable no-undef, no-console */ 3 | 4 | const chai = require("chai"); 5 | const chaiAsPromised = require("chai-as-promised"); 6 | // Load chai-as-promised support 7 | chai.use(chaiAsPromised); 8 | // Initialise should API (attaches as a property on Object) 9 | chai.should(); 10 | 11 | const net = require("net"); 12 | const TcpServer = require("../../servers/servertcp"); 13 | const Modbus = require("../../"); 14 | 15 | const holdingRegisters = {}; 16 | 17 | describe("Modbus TCP Server (Enron)", function() { 18 | let serverTCP; // eslint-disable-line no-unused-vars 19 | let modbusClient; 20 | 21 | before(function() { 22 | const vector = { 23 | getHoldingRegister: function(addr) { 24 | return holdingRegisters[addr]; 25 | }, 26 | setRegister: function(addr, value) { 27 | holdingRegisters[addr] = value; 28 | return; 29 | }, 30 | getMultipleHoldingRegisters: function(startAddr, length) { 31 | const values = []; 32 | for (let i = 0; i < length; i++) { 33 | values[i] = holdingRegisters[startAddr + i]; 34 | } 35 | return values; 36 | } 37 | }; 38 | serverTCP = new TcpServer(vector, { 39 | host: "0.0.0.0", 40 | unitId: 1, 41 | port: 8512, 42 | debug: true, 43 | enron: true, 44 | enronTables: { 45 | booleanRange: [1001, 1999], 46 | shortRange: [3001, 3999], 47 | longRange: [5001, 5999], 48 | floatRange: [7001, 7999] 49 | } 50 | }); 51 | modbusClient = new Modbus(); 52 | }); 53 | 54 | after(function() { 55 | serverTCP.close(); 56 | modbusClient.close(); 57 | }); 58 | 59 | describe("function code handler", function() { 60 | it("should receive a valid Modbus TCP message", function(done) { 61 | const client = net.connect({ host: "0.0.0.0", port: 8512 }, function() { 62 | // FC06 - write single register 5, to 0x0000ff00 63 | client.write(Buffer.from("000100000008010600050000ff00", "hex")); 64 | }); 65 | 66 | client.once("data", function(data) { 67 | // FC06 - valid responce 68 | chai.expect(data.toString("hex")).to.equal("000100000008010600050000ff00"); 69 | 70 | client.end(); 71 | done(); 72 | }); 73 | }); 74 | }); 75 | 76 | describe("function code handler", function() { 77 | it("should write and receive a valid 32bit integer Modbus (Enron) TCP message", function(done) { 78 | const address = 5001; 79 | const value = 305419896; 80 | 81 | function verify(msg) { 82 | // FC06 - valid responce 83 | chai.expect(msg.data[0]).to.equal(value); 84 | done(); 85 | } 86 | 87 | function read() { 88 | modbusClient.readRegistersEnron(address, 1).then(verify); 89 | } 90 | 91 | function write() { 92 | modbusClient 93 | .writeRegisterEnron(address, value) 94 | .then(read); 95 | } 96 | 97 | modbusClient.connectTCP( 98 | "0.0.0.0", 99 | { 100 | port: 8512, 101 | enron: true, 102 | enronTables: { 103 | booleanRange: [1001, 1999], 104 | shortRange: [3001, 3999], 105 | longRange: [5001, 5999], 106 | floatRange: [7001, 7999] 107 | } 108 | }, 109 | write); 110 | modbusClient.setID(1); 111 | }); 112 | }); 113 | 114 | describe("function code handler", function() { 115 | it("should receive a range error when trying to write a 32bit integer to a 16bit table", function(done) { 116 | const address = 3001; 117 | const value = 305419896; 118 | 119 | async function write() { 120 | await chai.expect(modbusClient.writeRegisterEnron(address, value)) 121 | .to.eventually 122 | .be.rejectedWith(RangeError); 123 | done(); 124 | } 125 | 126 | modbusClient.connectTCP( 127 | "0.0.0.0", 128 | { 129 | port: 8512, 130 | enron: true, 131 | enronTables: { 132 | booleanRange: [1001, 1999], 133 | shortRange: [3001, 3999], 134 | longRange: [5001, 5999], 135 | floatRange: [7001, 7999] 136 | } 137 | }, 138 | write); 139 | modbusClient.setID(1); 140 | }); 141 | }); 142 | 143 | describe("function code handler", function() { 144 | it("should receive exception when enron is chosen but no table is defined in options", function(done) { 145 | const address = 5001; 146 | const value = 305419896; 147 | 148 | async function write() { 149 | await chai.expect(modbusClient.writeRegisterEnron(address, value)) 150 | .to.eventually 151 | .be.rejectedWith(Error); 152 | done(); 153 | } 154 | 155 | modbusClient.connectTCP( 156 | "0.0.0.0", 157 | { 158 | port: 8512, 159 | enron: true 160 | }, 161 | write); 162 | modbusClient.setID(1); 163 | }); 164 | }); 165 | 166 | describe("function code handler", function() { 167 | it("should receive exception when enron is chosen but no valid table-shortRange is defined in options", function(done) { 168 | const address = 5001; 169 | const value = 305419896; 170 | 171 | async function write() { 172 | await chai.expect(modbusClient.writeRegisterEnron(address, value)) 173 | .to.eventually 174 | .be.rejectedWith(Error); 175 | done(); 176 | } 177 | 178 | modbusClient.connectTCP( 179 | "0.0.0.0", 180 | { 181 | port: 8512, 182 | enron: true, 183 | enronTables: { 184 | shortRange: [4, 3] // Should be [start, end], not [end, start] 185 | } 186 | }, 187 | write); 188 | modbusClient.setID(1); 189 | }); 190 | }); 191 | }); 192 | -------------------------------------------------------------------------------- /test/servers/servertcp.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable no-undef, no-console */ 3 | 4 | const expect = require("chai").expect; 5 | const net = require("net"); 6 | const TcpServer = require("./../../servers/servertcp"); 7 | 8 | describe("Modbus TCP Server (no serverID)", function() { 9 | let serverTCP; // eslint-disable-line no-unused-vars 10 | 11 | before(function() { 12 | const vector = { 13 | getInputRegister: function(addr) { 14 | return addr; 15 | }, 16 | getHoldingRegister: function(addr) { 17 | if (addr === 62) 18 | throw new Error(); 19 | 20 | console.log("\tHolding register: ", addr); 21 | 22 | return addr + 8000; 23 | }, 24 | getCoil: function(addr) { 25 | console.log("\tHolding register: ", addr); 26 | return (addr % 2) === 0; 27 | }, 28 | setRegister: function(addr, value) { 29 | console.log("\tset register", addr, value); 30 | return; 31 | }, 32 | setCoil: function(addr, value) { 33 | console.log("\tset coil", addr, value); 34 | return; 35 | } 36 | }; 37 | serverTCP = new TcpServer(vector, { host: "0.0.0.0", port: 8512, debug: true }); 38 | }); 39 | 40 | after(function() { 41 | serverTCP.close(); 42 | }); 43 | 44 | describe("function code handler", function() { 45 | it("should receive a valid Modbus TCP message", function(done) { 46 | const client = net.connect({ host: "0.0.0.0", port: 8512 }, function() { 47 | // FC05 - force single coil, to on 0xff00 48 | client.write(Buffer.from("00010000000601050005ff00", "hex")); 49 | }); 50 | 51 | client.once("data", function(data) { 52 | // FC05 - valid responce 53 | expect(data.toString("hex")).to.equal("00010000000601050005ff00"); 54 | 55 | client.end(); 56 | done(); 57 | }); 58 | }); 59 | 60 | // TODO: FC1 - FCX tests 61 | }); 62 | 63 | describe("modbus exception handler", function() { 64 | it("should receive a valid unhandled function Modbus TCP message", function(done) { 65 | const client = net.connect({ host: "0.0.0.0", port: 8512 }, function() { 66 | // FC07 - unhandled function 67 | client.write(Buffer.from("000100000006010700000000", "hex")); 68 | }); 69 | 70 | client.once("data", function(data) { 71 | // A valid error message, code 0x01 - Illegal fanction 72 | expect(data.toString("hex")).to.equal("000100000003018701"); 73 | 74 | client.end(); 75 | done(); 76 | }); 77 | }); 78 | 79 | it("should receive a valid slave failure Modbus TCP message", function(done) { 80 | const client = net.connect({ host: "0.0.0.0", port: 8512 }, function() { 81 | // FC03 to error triggering address 82 | client.write(Buffer.from("0001000000060103003E0001", "hex")); 83 | }); 84 | 85 | client.once("data", function(data) { 86 | // A valid error message, code 0x04 - Slave failure 87 | expect(data.toString("hex")).to.equal("000100000003018304"); 88 | 89 | client.end(); 90 | done(); 91 | }); 92 | }); 93 | 94 | // TODO: exceptions 95 | }); 96 | 97 | describe("socket connection error", function() { 98 | it("should receive an error event on socket closed by client", function(done) { 99 | const client = net.connect({ host: "0.0.0.0", port: 8512 }, function() { 100 | client.destroy(); 101 | 102 | serverTCP.emit("socketError"); 103 | }); 104 | 105 | serverTCP.on("socketError", function() { 106 | // Error handled correctly 107 | client.end(); 108 | done(); 109 | }); 110 | }); 111 | 112 | // TODO: exceptions 113 | }); 114 | 115 | describe("large client request", function() { 116 | it("should handle a large request without crash successfully (FC1)", function(done) { 117 | const client = net.connect({ host: "0.0.0.0", port: 8512 }, function() { 118 | // request 65535 registers at once 119 | client.write(Buffer.from("0001000000060101003EFFFF", "hex")); 120 | }); 121 | 122 | client.once("data", function(data) { 123 | // A valid error message, code 0x04 - Slave failure 124 | expect(data.toString("hex")).to.equal("000100000003018104"); 125 | 126 | client.end(); 127 | done(); 128 | }); 129 | }); 130 | 131 | it("should handle a large request without crash successfully (FC3)", function(done) { 132 | const client = net.connect({ host: "0.0.0.0", port: 8512 }, function() { 133 | // request 65535 registers at once 134 | client.write(Buffer.from("0001000000060103003EFFFF", "hex")); 135 | }); 136 | 137 | client.once("data", function(data) { 138 | // A valid error message, code 0x04 - Slave failure 139 | expect(data.toString("hex")).to.equal("000100000003018304"); 140 | 141 | client.end(); 142 | done(); 143 | }); 144 | }); 145 | 146 | it("should handle a large request without crash successfully (FC4)", function(done) { 147 | const client = net.connect({ host: "0.0.0.0", port: 8512 }, function() { 148 | // request 65535 registers at once 149 | client.write(Buffer.from("0001000000060104003EFFFF", "hex")); 150 | }); 151 | 152 | client.once("data", function(data) { 153 | // A valid error message, code 0x04 - Slave failure 154 | expect(data.toString("hex")).to.equal("000100000003018404"); 155 | 156 | client.end(); 157 | done(); 158 | }); 159 | }); 160 | 161 | // TODO: exceptions 162 | }); 163 | }); 164 | 165 | describe("Modbus TCP Server (serverID = requestID)", function() { 166 | let serverTCP; // eslint-disable-line no-unused-vars 167 | 168 | before(function() { 169 | const vector = { 170 | setCoil: function(addr, value) { 171 | console.log("\tset coil", addr, value); 172 | return; 173 | } 174 | }; 175 | serverTCP = new TcpServer(vector, { host: "0.0.0.0", port: 8512, debug: true, unitID: 0x04 }); 176 | }); 177 | 178 | after(function() { 179 | serverTCP.close(); 180 | }); 181 | 182 | describe("function code handler", function() { 183 | it("should receive a valid Modbus TCP message", function(done) { 184 | const client = net.connect({ host: "0.0.0.0", port: 8512 }, function() { 185 | // FC05 - force single coil, to on 0xff00 186 | client.write(Buffer.from("00010000000604050005ff00", "hex")); 187 | }); 188 | 189 | client.once("data", function(data) { 190 | // FC05 - valid responce 191 | expect(data.toString("hex")).to.equal("00010000000604050005ff00"); 192 | 193 | client.end(); 194 | done(); 195 | }); 196 | }); 197 | }); 198 | }); 199 | 200 | describe("Modbus TCP Server (serverID != requestID)", function() { 201 | let serverTCP; // eslint-disable-line no-unused-vars 202 | 203 | before(function() { 204 | const vector = { 205 | setCoil: function(addr, value) { 206 | console.log("\tset coil", addr, value); 207 | return; 208 | } 209 | }; 210 | serverTCP = new TcpServer(vector, { host: "0.0.0.0", port: 8512, debug: true, unitID: 0x04 }); 211 | }); 212 | 213 | after(function() { 214 | serverTCP.close(); 215 | }); 216 | 217 | describe("function code handler", function() { 218 | it("should receive a no Modbus TCP message for wrong unitID", function(done) { 219 | let timeout; 220 | this.timeout(1000 + 100); 221 | 222 | const client = net.connect({ host: "0.0.0.0", port: 8512 }, function() { 223 | // FC05 - force single coil, to on 0xff00 224 | client.write(Buffer.from("00010000000603050005ff00", "hex")); 225 | timeout = setTimeout(done, 1000); 226 | }); 227 | 228 | client.once("data", function(data) { 229 | clearTimeout(timeout); 230 | 231 | // FC05 - we expect no data for wrong unitID 232 | expect(data.toString("hex")).to.equal("NO DATA"); 233 | 234 | client.end(); 235 | done(); 236 | }); 237 | }); 238 | }); 239 | }); 240 | -------------------------------------------------------------------------------- /test/servers/servertcpCallback.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable no-undef, no-console */ 3 | 4 | const expect = require("chai").expect; 5 | const net = require("net"); 6 | const TcpServer = require("./../../servers/servertcp"); 7 | 8 | describe("Modbus TCP Server Callback", function() { 9 | let serverTCP; // eslint-disable-line no-unused-vars 10 | 11 | before(function() { 12 | const vector = { 13 | getInputRegister: function(addr, unit, callback) { 14 | setTimeout(function() { 15 | callback(null, addr); 16 | }, 50); 17 | }, 18 | getHoldingRegister: function(addr, unit, callback) { 19 | setTimeout(function() { 20 | if (addr === 0x003E) { 21 | callback(new Error()); 22 | return; 23 | } 24 | 25 | callback(null, addr + 8000); 26 | }, 50); 27 | }, 28 | getCoil: function(addr, unit, callback) { 29 | setTimeout(function() { 30 | callback(null, (addr % 2) === 0); 31 | }, 50); 32 | }, 33 | setRegister: function(addr, value, unit, callback) { 34 | setTimeout(function() { 35 | console.log("set register", addr, value); 36 | callback(null); 37 | }, 50); 38 | }, 39 | setCoil: function(addr, value, unit, callback) { 40 | setTimeout(function() {console.log("\tset coil", addr, value); 41 | callback(null); 42 | }, 50); 43 | } 44 | }; 45 | serverTCP = new TcpServer(vector, { host: "0.0.0.0", port: 8513, debug: true, unitID: 1 }); 46 | }); 47 | 48 | after(function() { 49 | serverTCP.close(); 50 | }); 51 | 52 | describe("function code handler", function() { 53 | it("should receive a valid Modbus TCP message", function(done) { 54 | const client = net.connect({ host: "0.0.0.0", port: 8513 }, function() { 55 | // FC05 - force single coil, to on 0xff00 56 | client.write(Buffer.from("00010000000601050005ff00", "hex")); 57 | }); 58 | 59 | client.once("data", function(data) { 60 | // FC05 - valid responce 61 | expect(data.toString("hex")).to.equal("00010000000601050005ff00"); 62 | done(); 63 | }); 64 | }); 65 | 66 | // TODO: FC1 - FCX tests 67 | }); 68 | 69 | describe("modbus exception handler", function() { 70 | it("should receive a valid Modbus TCP message", function(done) { 71 | const client = net.connect({ host: "0.0.0.0", port: 8513 }, function() { 72 | // FC07 - unhandled function 73 | client.write(Buffer.from("000100000006010700000000", "hex")); 74 | }); 75 | 76 | client.once("data", function(data) { 77 | // A valid error message, code 0x01 - Illegal fanction 78 | expect(data.toString("hex")).to.equal("000100000003018701"); 79 | done(); 80 | }); 81 | }); 82 | 83 | it("should receive a valid slave failure Modbus TCP message", function(done) { 84 | const client = net.connect({ host: "0.0.0.0", port: 8513 }, function() { 85 | // FC03 to error triggering address 86 | client.write(Buffer.from("0001000000060103003E0001", "hex")); 87 | }); 88 | 89 | client.once("data", function(data) { 90 | // A valid error message, code 0x04 - Slave failure 91 | expect(data.toString("hex")).to.equal("000100000003018304"); 92 | done(); 93 | }); 94 | }); 95 | 96 | // TODO: exceptions 97 | }); 98 | 99 | describe("socket connection error", function() { 100 | it("should receive an error event on socket closed by client", function(done) { 101 | const client = net.connect({ host: "0.0.0.0", port: 8513 }, function() { 102 | client.destroy(); 103 | 104 | serverTCP.emit("socketError"); 105 | }); 106 | 107 | serverTCP.on("socketError", function() { 108 | // Error handled correctly 109 | done(); 110 | }); 111 | }); 112 | 113 | // TODO: exceptions 114 | }); 115 | 116 | describe("large client request", function() { 117 | it("should handle a large request without crash successfully (FC1)", function(done) { 118 | const client = net.connect({ host: "0.0.0.0", port: 8513 }, function() { 119 | // request 65535 registers at once 120 | client.write(Buffer.from("0001000000060101003EFFFF", "hex")); 121 | }); 122 | 123 | client.once("data", function(data) { 124 | // A valid error message, code 0x04 - Slave failure 125 | expect(data.toString("hex")).to.equal("000100000003018104"); 126 | done(); 127 | }); 128 | }); 129 | 130 | it("should handle a large request without crash successfully (FC3)", function(done) { 131 | const client = net.connect({ host: "0.0.0.0", port: 8513 }, function() { 132 | // request 65535 registers at once 133 | client.write(Buffer.from("0001000000060103003EFFFF", "hex")); 134 | }); 135 | 136 | client.once("data", function(data) { 137 | // A valid error message, code 0x04 - Slave failure 138 | expect(data.toString("hex")).to.equal("000100000003018304"); 139 | done(); 140 | }); 141 | }); 142 | 143 | it("should handle a large request without crash successfully (FC4)", function(done) { 144 | const client = net.connect({ host: "0.0.0.0", port: 8513 }, function() { 145 | // request 65535 registers at once 146 | client.write(Buffer.from("0001000000060104003EFFFF", "hex")); 147 | }); 148 | 149 | client.once("data", function(data) { 150 | // A valid error message, code 0x04 - Slave failure 151 | expect(data.toString("hex")).to.equal("000100000003018404"); 152 | done(); 153 | }); 154 | }); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /test/servers/servertcpPromise.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable no-undef, no-console */ 3 | 4 | const expect = require("chai").expect; 5 | const net = require("net"); 6 | const TcpServer = require("./../../servers/servertcp"); 7 | 8 | describe("Modbus TCP Server Promise", function() { 9 | let serverTCP; // eslint-disable-line no-unused-vars 10 | 11 | before(function() { 12 | const vector = { 13 | getInputRegister: function(addr) { 14 | return new Promise(function(resolve) { 15 | setTimeout(function() { 16 | resolve(addr); 17 | }, 50); 18 | }); 19 | }, 20 | getHoldingRegister: function(addr) { 21 | return new Promise(function(resolve, reject) { 22 | setTimeout(function() { 23 | if (addr === 62) 24 | return reject(new Error()); 25 | 26 | resolve(addr + 8000); 27 | }, 50); 28 | }); 29 | }, 30 | getCoil: function(addr) { 31 | return new Promise(function(resolve) { 32 | setTimeout(function() { 33 | resolve((addr % 2) === 0); 34 | }, 50); 35 | }); 36 | }, 37 | setRegister: function(addr, value) { 38 | return new Promise(function(resolve) { 39 | setTimeout(function() { 40 | console.log("\tset register", addr, value); 41 | resolve(); 42 | }, 50); 43 | }); 44 | }, 45 | setCoil: function(addr, value) { 46 | return new Promise(function(resolve) { 47 | setTimeout(function() { 48 | console.log("\tset coil", addr, value); 49 | resolve(); 50 | }, 50); 51 | }); 52 | } 53 | }; 54 | serverTCP = new TcpServer(vector, { host: "0.0.0.0", port: 8514, debug: true, unitID: 1 }); 55 | }); 56 | 57 | after(function() { 58 | serverTCP.close(); 59 | }); 60 | 61 | describe("function code handler", function() { 62 | it("should receive a valid Modbus TCP message", function(done) { 63 | const client = net.connect({ host: "0.0.0.0", port: 8514 }, function() { 64 | // FC05 - force single coil, to on 0xff00 65 | client.write(Buffer.from("00010000000601050005ff00", "hex")); 66 | }); 67 | 68 | client.once("data", function(data) { 69 | // FC05 - valid responce 70 | expect(data.toString("hex")).to.equal("00010000000601050005ff00"); 71 | done(); 72 | }); 73 | }); 74 | 75 | // TODO: FC1 - FCX tests 76 | }); 77 | 78 | describe("modbus exception handler", function() { 79 | it("should receive a valid Modbus TCP message", function(done) { 80 | const client = net.connect({ host: "0.0.0.0", port: 8514 }, function() { 81 | // FC07 - unhandled function 82 | client.write(Buffer.from("000100000006010700000000", "hex")); 83 | }); 84 | 85 | client.once("data", function(data) { 86 | // A valid error message, code 0x01 - Illegal fanction 87 | expect(data.toString("hex")).to.equal("000100000003018701"); 88 | done(); 89 | }); 90 | }); 91 | 92 | it("should receive a valid slave failure Modbus TCP message", function(done) { 93 | const client = net.connect({ host: "0.0.0.0", port: 8514 }, function() { 94 | // FC03 to error triggering address 95 | client.write(Buffer.from("0001000000060103003E0001", "hex")); 96 | }); 97 | 98 | client.once("data", function(data) { 99 | // A valid error message, code 0x04 - Slave failure 100 | expect(data.toString("hex")).to.equal("000100000003018304"); 101 | done(); 102 | }); 103 | }); 104 | 105 | // TODO: exceptions 106 | }); 107 | 108 | describe("socket connection error", function() { 109 | it("should receive an error event on socket closed by client", function(done) { 110 | const client = net.connect({ host: "0.0.0.0", port: 8514 }, function() { 111 | client.destroy(); 112 | 113 | serverTCP.emit("socketError"); 114 | }); 115 | 116 | serverTCP.on("socketError", function() { 117 | // Error handled correctly 118 | done(); 119 | }); 120 | }); 121 | 122 | // TODO: exceptions 123 | }); 124 | 125 | describe("large client request", function() { 126 | it("should handle a large request without crash successfully (FC1)", function(done) { 127 | const client = net.connect({ host: "0.0.0.0", port: 8514 }, function() { 128 | // request 65535 registers at once 129 | client.write(Buffer.from("0001000000060101003EFFFF", "hex")); 130 | }); 131 | 132 | client.once("data", function(data) { 133 | // A valid error message, code 0x04 - Slave failure 134 | expect(data.toString("hex")).to.equal("000100000003018104"); 135 | done(); 136 | }); 137 | }); 138 | 139 | it("should handle a large request without crash successfully (FC3)", function(done) { 140 | const client = net.connect({ host: "0.0.0.0", port: 8514 }, function() { 141 | // request 65535 registers at once 142 | client.write(Buffer.from("0001000000060103003EFFFF", "hex")); 143 | }); 144 | 145 | client.once("data", function(data) { 146 | // A valid error message, code 0x04 - Slave failure 147 | expect(data.toString("hex")).to.equal("000100000003018304"); 148 | done(); 149 | }); 150 | }); 151 | 152 | it("should handle a large request without crash successfully (FC4)", function(done) { 153 | const client = net.connect({ host: "0.0.0.0", port: 8514 }, function() { 154 | // request 65535 registers at once 155 | client.write(Buffer.from("0001000000060104003EFFFF", "hex")); 156 | }); 157 | 158 | client.once("data", function(data) { 159 | // A valid error message, code 0x04 - Slave failure 160 | expect(data.toString("hex")).to.equal("000100000003018404"); 161 | done(); 162 | }); 163 | }); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /test/utils/crc16.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable no-undef */ 3 | 4 | const expect = require("chai").expect; 5 | 6 | const crc16 = require("../../utils/crc16"); 7 | 8 | describe("Modbus CRC16", function() { 9 | 10 | describe("crc16() - calculate checksum", function() { 11 | it("should calculate a valid checksum", function(done) { 12 | const buffer = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]); 13 | const crc = crc16(buffer); 14 | expect(crc).to.equal(50227); 15 | done(); 16 | }); 17 | 18 | it("should calculate a valid checksum", function(done) { 19 | const buffer = Buffer.from("110100130025", "hex"); 20 | const crc = crc16(buffer); 21 | expect(crc).to.equal(33806); 22 | done(); 23 | }); 24 | }); 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /test/utils/lrc.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable no-undef */ 3 | 4 | const expect = require("chai").expect; 5 | 6 | const calculateLrc = require("./../../utils/lrc"); 7 | 8 | describe("Modbus LRC", function() { 9 | 10 | describe("lrc() - calculate checksum", function() { 11 | it("should calculate a valid checksum", function(done) { 12 | const buffer = Buffer.from("1103006B0003", "hex"); 13 | const lrc = calculateLrc(buffer); 14 | expect(lrc).to.equal(126); 15 | done(); 16 | }); 17 | }); 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /utils/buffer_bit.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Copyright (c) 2015, Yaacov Zamir 4 | * 5 | * Permission to use, copy, modify, and/or distribute this software for any 6 | * purpose with or without fee is hereby granted, provided that the above 7 | * copyright notice and this permission notice appear in all copies. 8 | * 9 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | */ 17 | 18 | /** 19 | * Adds Bit Operations to Buffer 20 | */ 21 | const addBufferBitOp = function() { 22 | 23 | /** 24 | * Add set one bit in a Buffer prototype. 25 | * 26 | * @param {boolean} value, new state of bit. 27 | * @param {number} bit, The bit offset. 28 | * @param {number} offset, the byte offset. 29 | */ 30 | Buffer.prototype.writeBit = function(value, bit, offset) { 31 | const byteOffset = parseInt(bit / 8 + offset); 32 | const bitOffset = bit % 8; 33 | const bitMask = 0x1 << bitOffset; 34 | 35 | // get byte from buffer 36 | let byte = this.readUInt8(byteOffset); 37 | 38 | // set bit on / off 39 | if (value) { 40 | byte |= bitMask; 41 | } else { 42 | byte &= ~bitMask; 43 | } 44 | 45 | // set byte to buffer 46 | this.writeUInt8(byte, byteOffset); 47 | }; 48 | 49 | /** 50 | * Add get one bit in a Buffer prototype. 51 | * 52 | * @param {boolean} bit, The bit offset. 53 | * @param {number} offset, the byte offset. 54 | * 55 | * @return {boolean} the state of the bit. 56 | */ 57 | Buffer.prototype.readBit = function(bit, offset) { 58 | const byteOffset = parseInt(bit / 8 + offset); 59 | const bitOffset = bit % 8; 60 | const bitMask = 0x1 << bitOffset; 61 | 62 | // get byte from buffer 63 | const byte = this.readUInt8(byteOffset); 64 | 65 | // check bit state 66 | return (byte & bitMask) === bitMask; 67 | }; 68 | }; 69 | 70 | /** 71 | * Buffer Bit operations. 72 | * 73 | * @type {addBufferBitOp} 74 | */ 75 | module.exports = addBufferBitOp; 76 | -------------------------------------------------------------------------------- /utils/crc16.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Calculates the buffers CRC16. 4 | * 5 | * @param {Buffer} buffer the data buffer. 6 | * @return {number} the calculated CRC16. 7 | */ 8 | module.exports = function crc16(buffer) { 9 | let crc = 0xFFFF; 10 | let odd; 11 | 12 | for (let i = 0; i < buffer.length; i++) { 13 | crc = crc ^ buffer[i]; 14 | 15 | for (let j = 0; j < 8; j++) { 16 | odd = crc & 0x0001; 17 | crc = crc >> 1; 18 | if (odd) { 19 | crc = crc ^ 0xA001; 20 | } 21 | } 22 | } 23 | 24 | return crc; 25 | }; 26 | -------------------------------------------------------------------------------- /utils/lrc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Calculates the buffers LRC. 4 | * 5 | * @param {Buffer} buffer the data buffer. 6 | * @return {number} the calculated LRC. 7 | */ 8 | module.exports = function lrc(buffer) { 9 | let lrc = 0; 10 | for (let i = 0; i < buffer.length; i++) { 11 | lrc += buffer[i] & 0xFF; 12 | } 13 | 14 | return ((lrc ^ 0xFF) + 1) & 0xFF; 15 | }; 16 | -------------------------------------------------------------------------------- /worker/README.md: -------------------------------------------------------------------------------- 1 | #### See also modbusdb 2 | 3 | The worker class is a simple class that makes it easy to grab some data from device, for a more complete module see modbusdb https://github.com/yarosdev/modbusdb 4 | 5 | ------------ 6 | 7 | #### What can I do with this module ? 8 | 9 | This class makes it easy to grab some data from device. 10 | Also, it handles typecast to int32, uint32, float etc. 11 | Type 'double' is not supported by nodejs... 12 | 13 | #### Examples 14 | 15 | ###### Setup client and worker 16 | ```` javascript 17 | const client = new ModbusRTU() 18 | 19 | client.connect.......... 20 | 21 | client.setWorkerOptions({ 22 | maxConcurrentRequests: 10, // it will send 10 requests or less at a time if any 23 | debug: true 24 | }) 25 | ```` 26 | ###### Read some data 27 | ``` javascript 28 | // Read 4 values starting from 10009 register. 29 | // Under the hood it will read 8 registers due to int32 type 30 | const response = await client.send({ 31 | unit: 1, 32 | fc: 3, 33 | address: 10009, 34 | quantity: 4, 35 | type: 'int32', 36 | }); 37 | ```` 38 | ###### Write some data 39 | ``` javascript 40 | // Write 2 values to address: 10009 and 10011 41 | const response = await client.send({ 42 | unit: 1, 43 | fc: 16, 44 | address: 10009, 45 | value: [10999, 10888], 46 | type: 'int32', 47 | }); 48 | ```` 49 | ###### Poll some data 50 | ``` javascript 51 | // It will build all READ requests for you in optimal way 52 | const response = await client.poll({ 53 | unit: 1, 54 | map: [ 55 | { fc: 3, address: [10011, 10013, 10018], type: "int32" }, 56 | { fc: 3, address: 10003, type: "int32" }, 57 | { fc: 3, address: 10005, type: "int32" }, 58 | { fc: 3, address: 10007, type: "int32" }, 59 | { fc: 3, address: 10009, type: "int32" }, 60 | { fc: 2, address: [1,2,3]}, 61 | { fc: 1, address: [1,2,3]}, 62 | { fc: 1, address: 4}, 63 | { fc: 1, address: 5}, 64 | { fc: 1, address: 6}, 65 | { fc: 3, address: [10001]}, // default type is int16 66 | { fc: 3, address: [10020, 10023, 10026], type: "float"}, 67 | { fc: 3, address: [10030, 10034], type: "double"} 68 | ], 69 | onProgress: (progress, data) => { 70 | console.log( 71 | progress, // Poll progress from 0...1 where 1 means 100% 72 | data, // Data from the current request 73 | ); 74 | }, 75 | maxChunkSize: 32, // max registers per request 76 | skipErrors: false, // if false it will stop poll and return PARTIAL result 77 | }) 78 | ``` 79 | --------------------------------------------------------------------------------