├── .eslintignore ├── .eslintrc ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── docs └── index.html ├── example ├── PLC │ └── PRG_AdsServerExample.xml └── example.js ├── package-lock.json ├── package.json ├── src ├── ads-commons.ts ├── ads-server-core.ts ├── ads-server-router.ts ├── ads-server-standalone.ts ├── ads-server.ts └── types │ ├── ads-server.ts │ └── ads-types.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | example -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "rules": { 13 | "no-async-promise-executor": "off" 14 | } 15 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | # Ignore files with sensitive environment variables 3 | .env 4 | .env.test 5 | # Next.js output 6 | .next/ 7 | # Parcel cache 8 | .cache/ 9 | # Ignore IDE configuration files 10 | .idea/ 11 | .vscode/ 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | # Ignore built ts files 19 | dist/**/* -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [1.1.2] - 13.09.2021 8 | - Bug fix: Connecting to local router failed with ECONNREFUSED error on Node.js version 17 and newer 9 | - See [https://github.com/nodejs/node/issues/40702](https://github.com/nodejs/node/issues/40702) 10 | - Fixed by using `127.0.0.1` instead of `localhost` 11 | 12 | ## [1.1.0] - 25.11.2021 13 | ### Added 14 | - New `StandAloneServer` class 15 | - A new ADS server for systems without TwinCAT installation / AMS router 16 | - AdsRouterConsole or similar no more needed 17 | - Example: Raspberry Pi, Linux, etc.. 18 | - Listens for incoming connections at TCP port (default 48898) so no router needed 19 | - Can listen and respond to requests to any ADS port 20 | 21 | ### Changed 22 | - Old code divided into `Server` and `ServerCore` (internal) classes 23 | - `Server` class connecting, disconnecting and reconnecting redesigned 24 | - `Server` class reconnecting after connection loss redesigned 25 | - All request callbacks (like `onReadReq`) now have 4th parameter: `adsPort` 26 | - Contains the ADS port where the command was sent to 27 | - Required with `StandAloneServer` as it listens to all ADS ports 28 | - Better typings for `onAddNotification` 29 | - Lots of small improvements 30 | 31 | ## [1.0.0] - 20.11.2021 32 | ### Changed 33 | - Bugfix: `_registerAdsPort` caused unhandled exception if using manually given AmsNetID. 34 | - NOTE: Updated version to 1.0.0 but everything is backwards compatible 35 | 36 | ## [0.2.2] - 16.01.2021 37 | ### Changed 38 | - Minor changes to console warning messages 39 | 40 | ## [0.2.0] - 07.01.2021 41 | ### Changed 42 | - Updated README 43 | - Updated `sendDeviceNotification()` 44 | - Small fixes 45 | - Added ./example 46 | 47 | ## [0.1.0] - 05.01.2021 48 | ### Added 49 | - First public release 50 | - Everything should work somehow 51 | - `sendDeviceNotification()` is not ready, but should work for testing -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jussi Isotalo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ads-server 2 | 3 | 4 | [![npm version](https://img.shields.io/npm/v/ads-server)](https://www.npmjs.org/package/ads-server) 5 | [![GitHub](https://img.shields.io/badge/View%20on-GitHub-brightgreen)](https://github.com/jisotalo/ads-server) 6 | [![License](https://img.shields.io/github/license/jisotalo/ads-server)](https://choosealicense.com/licenses/mit/) 7 | 8 | TwinCAT ADS server for Node.js (unofficial). Listens for incoming ADS protocol commands and responds. 9 | 10 | **Example use cases:** 11 | - Creating a server that can be connected from any TwinCAT PLC for reading and writing using ADS commands 12 | - No need to write own protocols or to buy separate licenses 13 | - Creating a "fake PLC" to test your system that uses [ads-client](https://github.com/jisotalo/ads-client) under the hood 14 | - Using ADS protocol to communicate for your own systems 15 | 16 | 17 | If you need an ADS client for reading/writing PLC values, see my other project [ads-client](https://github.com/jisotalo/ads-client). 18 | 19 | 20 | # Table of contents 21 | - [Installing](#installing) 22 | - [Selecting correct server class to use](#selecting-correct-server-class-to-use) 23 | - [Configuration](#configuration) 24 | * [`Server` class](#server-class) 25 | * [`StandAloneServer` class](#standaloneserver-class) 26 | - [Available ADS commands](#available-ads-commands) 27 | * [Read request](#read-request) 28 | * [Write request](#write-request) 29 | * [ReadWrite requests](#readwrite-requests) 30 | * [ReadDeviceInfo requests](#readdeviceinfo-requests) 31 | * [ReadState requests](#readstate-requests) 32 | * [WriteControl requests](#writecontrol-requests) 33 | * [AddNotification requests](#addnotification-requests) 34 | * [DeleteNotification requests](#deletenotification-requests) 35 | * [Sending a notification](#sending-a-notification) 36 | - [NOTE: Difference when using `StandAloneServer`](#note-difference-when-using-standaloneserver) 37 | - [Handling IEC-61131 data types](#handling-iec-61131-data-types) 38 | * [Responding with a IEC data type](#responding-with-a-iec-data-type) 39 | * [Responding with a STRUCT](#responding-with-a-struct) 40 | - [Example: All example codes as a working version](#example-all-example-codes-as-a-working-version) 41 | - [Example: How to display full ADS packet from (debug etc.)](#example-how-to-display-full-ads-packet-from-debug-etc) 42 | - [Example: Handling device notifications with ads-client](#example-handling-device-notifications-with-ads-client) 43 | - [Example: Creating a fake PLC](#example-creating-a-fake-plc) 44 | * [Base code for fake PLC system with `Server`](#base-code-for-fake-plc-system-with-server) 45 | * [Base code for fake PLC system with `StandAloneServer`](#base-code-for-fake-plc-system-with-standaloneserver) 46 | - [Debugging](#debugging) 47 | * [Enabling debug from code](#enabling-debug-from-code) 48 | * [Enabling debugging from terminal](#enabling-debugging-from-terminal) 49 | - [License](#license) 50 | 51 | # Installing 52 | 53 | Install the package from NPM using command: 54 | ```bash 55 | npm i ads-server 56 | ``` 57 | Or if you like, you can clone the git repository and then build using command: 58 | ```bash 59 | npm run build 60 | ``` 61 | After that, compiled sources are located under `./dist/` 62 | 63 | # Selecting correct server class to use 64 | 65 | There are two servers available (since version 1.1.0) 66 | - `Server` for using with TwinCAT installation or separate AMS router like [AdsRouterConsole](https://www.nuget.org/packages/Beckhoff.TwinCAT.Ads.AdsRouterConsole/) 67 | - For TwinCAT PLCs and Windows PCs with TwinCAT installation 68 | - For Raspberry Pi, Linux, etc. wth [AdsRouterConsole](https://www.nuget.org/packages/Beckhoff.TwinCAT.Ads.AdsRouterConsole/) 69 | - `StandAloneServer` for using without TwinCAT installation 70 | - For Raspberry Pi, Linux, etc. (nothing else is needed) 71 | - Use this if you don't have TC installed (unless you need `AdsRouterConsole` for some reason) 72 | 73 | --- 74 | **TL;DR:** Use `Server` if you have TwinCAT installed. 75 | 76 | --- 77 | **Differences:** 78 | - `Server` connects to the AMS router and then waits for incoming packets 79 | - The `StandAloneServer` starts its own TCP server (port 48898) and listens for any incoming packets 80 | - `Server` listens for commands to only one ADS port (however multiple instances can be created) 81 | - The `StandAloneServer` listens to all ADS ports (only single instance possible) 82 | 83 | The following examples are for `Server`, however they work 1:1 with `StandAloneServer`. Please see chapter [NOTE: Difference when using `StandAloneServer`](#note-difference-when-using-standaloneserver) for specific notes. 84 | 85 | # Configuration 86 | 87 | ## `Server` class 88 | 89 | The `localAdsPort` can be any non-reserved ADS port. See `ADS_RESERVED_PORTS` type at [src/ads-commons.ts](https://github.com/jisotalo/ads-server/blob/7a74d0ebcb51d1836c49c1364da0be748731ce18/src/ads-commons.ts#L86). A port number over 20000 should be OK. 90 | 91 | The `localAdsPort` should **always be provided** to ensure a static ADS port. Otherwise, the router provides next free one which always changes -> PLC/client code needs to be changed. 92 | 93 | The setting are provided when creating the `Server` object and they are all optional. As default, the `Server` connects to the local AMS router running at localhost but it can be changed from settings. 94 | 95 | ```js 96 | const { Server } = require('ads-server') 97 | //import { Server } from 'ads-server' //Typescript 98 | 99 | //Creating a new server instance at ADS port 30012 100 | const server = new Server({ 101 | localAdsPort: 30012 102 | }) 103 | 104 | //Connect to the local AMS router 105 | server.connect() 106 | .then(async conn => { 107 | console.log('Connected:', conn) 108 | 109 | //To disconnect: 110 | //await server.disconnect() 111 | }) 112 | .catch(err => { 113 | console.log('Connecting failed:', err) 114 | }) 115 | ``` 116 | 117 | Available settings for `Server`: 118 | 119 | ```ts 120 | { 121 | /** Optional: Local ADS port to use (default: automatic/router provides) */ 122 | localAdsPort: number, 123 | /** Optional: Local AmsNetId to use (default: automatic) */ 124 | localAmsNetId: string, 125 | /** Optional: If true, no warnings are written to console (= nothing is ever written to console) (default: false) */ 126 | hideConsoleWarnings: boolean, 127 | /** Optional: Target ADS router TCP port (default: 48898) */ 128 | routerTcpPort: number, 129 | /** Optional: Target ADS router IP address/hostname (default: 'localhost') */ 130 | routerAddress: string, 131 | /** Optional: Local IP address to use, use this to change used network interface if required (default: '' = automatic) */ 132 | localAddress: string, 133 | /** Optional: Local TCP port to use for outgoing connections (default: 0 = automatic) */ 134 | localTcpPort: number, 135 | /** Optional: Local AmsNetId to use (default: automatic) */ 136 | localAmsNetId: string, 137 | /** Optional: Time (milliseconds) after connecting to the router or waiting for command response is canceled to timeout (default: 2000) */ 138 | timeoutDelay: number, 139 | /** Optional: If true and connection to the router is lost, the server tries to reconnect automatically (default: true) */ 140 | autoReconnect: boolean, 141 | /** Optional: Time (milliseconds) how often the lost connection is tried to re-establish (default: 2000) */ 142 | reconnectInterval: number, 143 | } 144 | ``` 145 | 146 | ## `StandAloneServer` class 147 | 148 | The only required setting for `StandAloneServer` is `localAmsNetId`. Unlike `Server`, it listens for all ADS ports for incoming commands. 149 | 150 | The `localAmsNetId` can be decided freely. Only requirement is that it's not in use by any other system. It is needed when creating a static route from another system. 151 | 152 | ```js 153 | const { StandAloneServer } = require('ads-server') 154 | //import { StandAloneServer } from 'ads-server' //Typescript 155 | 156 | const server = new StandAloneServer({ 157 | localAmsNetId: '192.168.5.10.1.1' //You can decide whatever you like (needs to be free) 158 | }) 159 | 160 | server.listen() 161 | .then(async conn => { 162 | console.log('Listening:', conn) 163 | 164 | //To stop listening: 165 | //await server.close() 166 | }) 167 | .catch(err => { 168 | console.log('Listening failed:', err) 169 | }) 170 | ``` 171 | 172 | 173 | Available settings for `StandAloneServer`: 174 | 175 | ```ts 176 | { 177 | /** Local AmsNetId to use */ 178 | localAmsNetId: string, 179 | /** Optional: Local IP address to use, use this to change used network interface if required (default: '' = automatic) */ 180 | listeningAddress: string, 181 | /** Optional: Local TCP port to listen for incoming connections (default: 48898) */ 182 | listeningTcpPort: number 183 | /** Optional: If true, no warnings are written to console (= nothing is ever written to console) (default: false) */ 184 | hideConsoleWarnings: boolean, 185 | } 186 | ``` 187 | 188 | For configuring the route, see this [ads-client README](https://github.com/jisotalo/ads-client/#setup-3---connecting-from-any-nodejs-supported-system-to-the-plc). 189 | 190 | 191 | # Available ADS commands 192 | 193 | In this chapter each available feature is explained shortly. Also a correspoding client-side PLC code is shown. 194 | 195 | ## Read request 196 | 197 | **Client reads a value from the server.** 198 | 199 | Node.js (server): 200 | ```ts 201 | server.onReadReq(async (req, res) => { 202 | console.log('Read request received:', req) 203 | 204 | //Create an INT value of 4455 205 | const data = Buffer.alloc(2) 206 | data.writeInt16LE(4455) 207 | 208 | //Respond with data 209 | await res({ data }) 210 | .catch(err => console.log('Responding failed:', err)) 211 | 212 | /* Or to respond with an error: 213 | await res({ 214 | error: 1793 //ADS error code, for example 1793 = Service is not supported by server 215 | }).catch(err => console.log('Responding failed:', err)) */ 216 | }) 217 | ``` 218 | 219 | TwinCAT (client): 220 | ```Pascal 221 | VAR 222 | AdsRead : Tc2_System.ADSREADEX; 223 | ReadValue : INT; 224 | ReadCmd : BOOL; 225 | END_VAR 226 | 227 | //When ReadCmd is TRUE, a value is read from localhost and ADS port 30012 228 | AdsRead( 229 | NETID := , 230 | PORT := 30012, 231 | IDXGRP := 10, 232 | IDXOFFS := 100, 233 | LEN := SIZEOF(ReadValue), 234 | DESTADDR:= ADR(ReadValue), 235 | READ := ReadCmd, 236 | ); 237 | 238 | IF NOT AdsRead.BUSY THEN 239 | //Now ReadValue should be 4455 or AdsReader.ERR is true and AdsReader.ERRID is the error code 240 | ReadCmd := FALSE; 241 | END_IF 242 | ``` 243 | 244 | ## Write request 245 | 246 | **Client writes a value to the server.** 247 | 248 | Node.js (server): 249 | ```ts 250 | server.onWriteReq(async (req, res) => { 251 | console.log('Write request received:', req) 252 | 253 | //Do something with the given data 254 | console.log('Writing', req.data.byteLength, 'bytes of data') 255 | 256 | //Respond OK 257 | await res({}) 258 | .catch(err => console.log('Responding failed:', err)) 259 | 260 | /* Or to respond with an error: 261 | await res({ 262 | error: 1793 //ADS error code, for example 1793 = Service is not supported by server 263 | }).catch(err => console.log('Responding failed:', err)) */ 264 | }) 265 | ``` 266 | 267 | 268 | TwinCAT (client): 269 | ```Pascal 270 | VAR 271 | AdsWrite : Tc2_System.ADSWRITE; 272 | WriteValue : INT := 5555; 273 | WriteCmd : BOOL; 274 | END_VAR 275 | 276 | //When WriteCmd is TRUE, a value is written to localhost and ADS port 30012 277 | AdsWriter( 278 | NETID := , 279 | PORT := 30012, 280 | IDXGRP := 10, 281 | IDXOFFS := 100, 282 | LEN := SIZEOF(WriteValue), 283 | SRCADDR := ADR(WriteValue), 284 | WRITE := WriteCmd 285 | ); 286 | 287 | IF NOT AdsWrit.BUSY THEN 288 | WriteCmd := FALSE; 289 | END_IF 290 | ``` 291 | 292 | ## ReadWrite requests 293 | 294 | **Client writes a value to the server and waits for response data.** 295 | 296 | Node.js (server): 297 | ```ts 298 | server.onReadWriteReq(async (req, res) => { 299 | console.log('ReadWrite request received:', req) 300 | 301 | //Do something with the given data 302 | const requestedValue = server.trimPlcString(req.data.toString('ascii')) 303 | console.log('Requested value: ', requestedValue) 304 | 305 | //This example does not care about index group and index offset 306 | //Instead we just check the received data 307 | if (requestedValue === 'Temperature 1') { 308 | 309 | //Create an REAL value of 27.678 310 | const data = Buffer.alloc(4) 311 | data.writeFloatLE(27.678) 312 | 313 | //Respond with data 314 | await res({ data }).catch(err => console.log('Responding failed:', err)) 315 | 316 | } else { 317 | 318 | await res({ 319 | error: 1793 //ADS error code, for example 1793 = Service is not supported by server 320 | }).catch(err => console.log('Responding failed:', err)) 321 | } 322 | }) 323 | ``` 324 | 325 | 326 | TwinCAT (client): 327 | ```Pascal 328 | VAR 329 | AdsReadWriter : Tc2_System.ADSRDWRTEX; 330 | RW_WriteValue : STRING := 'Temperature 1'; 331 | RW_ReadValue : REAL; 332 | ReadWriteCmd : BOOL; 333 | END_VAR 334 | 335 | //When ReadWriteCmd is TRUE, a value is written to localhost and ADS port 30012 and result is read 336 | AdsReadWriter( 337 | NETID := , 338 | PORT := 30012, 339 | IDXGRP := 10, 340 | IDXOFFS := 100, 341 | WRITELEN:= SIZEOF(RW_WriteValue), 342 | READLEN := SIZEOF(RW_ReadValue), 343 | SRCADDR := ADR(RW_WriteValue), 344 | DESTADDR:= ADR(RW_ReadValue), 345 | WRTRD := ReadWriteCmd 346 | ); 347 | 348 | IF NOT AdsReadWriter.BUSY THEN 349 | ReadWriteCmd := FALSE; 350 | END_IF 351 | ``` 352 | 353 | ## ReadDeviceInfo requests 354 | 355 | **Client reads device (server) info.** 356 | 357 | Node.js (server): 358 | ```ts 359 | server.onReadDeviceInfo(async (req, res) => { 360 | console.log('ReadDeviceInfo request received') 361 | 362 | //Respond with data 363 | res({ 364 | deviceName: 'Server example', 365 | majorVersion: 5, 366 | minorVersion: 123, 367 | versionBuild: 998 368 | }) 369 | 370 | /* Or to respond with an error: 371 | await res({ 372 | error: 1793 //ADS error code, for example 1793 = Service is not supported by server 373 | }).catch(err => console.log('Responding failed:', err)) */ 374 | }) 375 | ``` 376 | 377 | TwinCAT (client): 378 | ```Pascal 379 | VAR 380 | AdsReadDevInfo : Tc2_System.ADSRDDEVINFO; 381 | ReadDevInfoCmd : BOOL; 382 | END_VAR 383 | 384 | //When ReadDevInfoCmd is TRUE, device info is read from localhost and ADS port 30012 385 | AdsReadDevInfo( 386 | NETID := , 387 | PORT := 30012, 388 | RDINFO := ReadDevInfoCmd 389 | ); 390 | 391 | IF NOT AdsReadDevInfo.BUSY THEN 392 | //NOTE: ADS protocol has major version, minor version and build version unlike the ADS PLC block 393 | // -> version is corrupted 394 | ReadDevInfoCmd := FALSE; 395 | END_IF 396 | ``` 397 | ## ReadState requests 398 | 399 | **Client reads device (server) state.** 400 | 401 | You can use the `ADS_STATE` constant values from exported `ADS` object if you like. 402 | 403 | Node.js (server): 404 | ```ts 405 | server.onReadState(async (req, res) => { 406 | console.log('ReadState request received') 407 | 408 | //Respond with data 409 | await res({ 410 | adsState: ADS.ADS_STATE.Config, //Or just any number 411 | deviceState: 123 412 | }).catch(err => console.log('Responding failed:', err)) 413 | 414 | /* Or to respond with an error: 415 | await res({ 416 | error: 1793 //ADS error code, for example 1793 = Service is not supported by server 417 | }).catch(err => console.log('Responding failed:', err)) */ 418 | }) 419 | ``` 420 | 421 | TwinCAT (client): 422 | ```Pascal 423 | VAR 424 | AdsReadDevState : Tc2_System.ADSRDSTATE; 425 | ReadDevStateCmd : BOOL; 426 | END_VAR 427 | 428 | //When AdsReadDevState is TRUE, device state is read from localhost and ADS port 30012 429 | AdsReadDevState( 430 | NETID := , 431 | PORT := 30012, 432 | RDSTATE := ReadDevStateCmd 433 | ); 434 | 435 | IF NOT AdsReadDevState.BUSY THEN 436 | ReadDevStateCmd := FALSE; 437 | END_IF 438 | ``` 439 | 440 | ## WriteControl requests 441 | 442 | **Client commands the device (server) to a given state. Also additional data can be provided.** 443 | 444 | Node.js (server): 445 | ```ts 446 | server.onWriteControl(async (req, res) => { 447 | console.log('WriteControl request received:', req) 448 | 449 | //Do something with req 450 | const dataStr = server.trimPlcString(req.data.toString('ascii')) 451 | console.log('Requested ADS state:', req.adsStateStr, ', provided data:', dataStr) 452 | 453 | //Respond OK 454 | res({}).catch(err => console.log('Responding failed:', err)) 455 | 456 | /* Or to respond with an error: 457 | await res({ 458 | error: 1793 //ADS error code, for example 1793 = Service is not supported by server 459 | }).catch(err => console.log('Responding failed:', err)) */ 460 | }) 461 | ``` 462 | 463 | TwinCAT (client): 464 | ```Pascal 465 | VAR 466 | AdsWriteCtrl : Tc2_System.ADSWRTCTL; 467 | WriteCtrlData : STRING := 'Some test data to write control'; 468 | WriteCtrlCmd : BOOL; 469 | END_VAR 470 | 471 | //When WriteCtrlCmd is TRUE, values are written to localhost and ADS port 30012 472 | AdsWriteCtrl( 473 | NETID := , 474 | PORT := 30012, 475 | ADSSTATE:= ADSSTATE_RUN, 476 | DEVSTATE:= 123, 477 | LEN := SIZEOF(WriteCtrlData), 478 | SRCADDR := ADR(WriteCtrlData), 479 | WRITE := WriteCtrlCmd 480 | ); 481 | 482 | IF NOT AdsWriteCtrl.BUSY THEN 483 | WriteCtrlCmd := FALSE; 484 | END_IF 485 | ``` 486 | 487 | ## AddNotification requests 488 | 489 | **Client requests to have notifications based on given settings (subscribes).** 490 | 491 | Not available with a PLC as a client, see an example with `ads-client` later. 492 | 493 | Node.js (server): 494 | ```ts 495 | server.onAddNotification(async (req, res) => { 496 | console.log('AddNotification request received:', req) 497 | 498 | //Do something with the given req and create an unique notification handle 499 | const notificationHandle = 1 500 | 501 | //Respond with data 502 | res({ notificationHandle }) 503 | .catch(err => console.log('Responding failed:', err)) 504 | 505 | /* Or to respond with an error: 506 | await res({ 507 | error: 1793 //ADS error code, for example 1793 = Service is not supported by server 508 | }).catch(err => console.log('Responding failed:', err)) */ 509 | }) 510 | ``` 511 | 512 | ## DeleteNotification requests 513 | 514 | Client requests to delete an existing notifications by given handle (unsubscribes). Previously create with AddNotification command. 515 | 516 | Not available with a PLC as a client, see an example with `ads-client` later. 517 | 518 | Node.js (server): 519 | ```ts 520 | server.onDeleteNotification(async (req, res) => { 521 | console.log('DeleteNotification request received:', req) 522 | 523 | //Delete existing notification by given req 524 | console.log('Removing handle ', req.notificationHandle) 525 | 526 | //Respond OK 527 | res({}) 528 | .catch(err => console.log('Responding failed:', err)) 529 | 530 | /* Or to respond with an error: 531 | await res({ 532 | error: 1793 //ADS error code, for example 1793 = Service is not supported by server 533 | }).catch(err => console.log('Responding failed:', err)) */ 534 | }) 535 | ``` 536 | 537 | ## Sending a notification 538 | 539 | **Server sends data based on previously created notification to the client.** 540 | 541 | Not available with a PLC as a client, see an example with `ads-client` later. 542 | 543 | 544 | When using `Server`: 545 | ```ts 546 | const data = Buffer.alloc(81) 547 | data.write('Sending some string as notification', 'ascii') 548 | 549 | await server.sendDeviceNotification({ 550 | notificationHandle: 1, //Previously saved 551 | targetAdsPort: 851, //Previously saved 552 | targetAmsNetId: '192.168.1.2.1.1' //Previously saved 553 | }, data) 554 | ``` 555 | 556 | When using `StandAloneServer`: 557 | ```ts 558 | const data = Buffer.alloc(81) 559 | data.write('Sending some string as notification', 'ascii') 560 | 561 | await server.sendDeviceNotification({ 562 | notificationHandle: 1, //Previously saved 563 | targetAdsPort: 851, //Previously saved 564 | targetAmsNetId: '192.168.1.2.1.1', //Previously saved 565 | sourceAdsPort: 851, //Previously saved 566 | socket: socket //Previously saved 567 | }, data) 568 | ``` 569 | 570 | In practise, you can save the `packet.ads.notificationTarget` object, assign a handle to it and then send notifications using it: 571 | 572 | ```ts 573 | //Simplified example 574 | let target = undefined 575 | 576 | server.onAddNotification(async (req, res, packet, adsPort) => { 577 | target = packet.ads.notificationTarget 578 | target.notificationHandle = 1 579 | 580 | res({ 581 | notificationHandle: target.notificationHandle 582 | }).catch(err => console.log('Responding failed:', err)) 583 | 584 | }) 585 | 586 | //Later... 587 | if(target) { 588 | const data = Buffer.alloc(81) 589 | data.write('Sending some string as notification', 'ascii') 590 | 591 | await server.sendDeviceNotification(target, data) 592 | } 593 | ``` 594 | 595 | # NOTE: Difference when using `StandAloneServer` 596 | 597 | The examples work also for `StandAloneServer`, however there is one **major** difference. 598 | 599 | The received command can be sent to **any ADS port**. So the target ADS port needs to be checked using 4th parameter `adsPort` or `packet.ams.targetAdsPort` of the callback function. 600 | 601 | ```ts 602 | //Note: adsPort 603 | server.onReadReq(async (req, res, packet, adsPort) => { 604 | console.log('Read request', req, 'received to ADS port', adsPort) 605 | 606 | const data = Buffer.alloc(2) 607 | 608 | switch (adsPort) { //adsPort or packet.ams.targetAdsPort 609 | case 30012: 610 | //Request for ADS port 30012 -> respond 5555 611 | data.writeInt16LE(5555) 612 | 613 | await res({ data }) 614 | .catch(err => console.log('Responding failed:', err)) 615 | break 616 | 617 | case 30013: 618 | //Request for ADS port 30013 -> respond 888 619 | data.writeInt16LE(888) 620 | 621 | await res({ data }) 622 | .catch(err => console.log('Responding failed:', err)) 623 | 624 | break 625 | 626 | default: 627 | await res({ 628 | error: 6 //ADS error code, "Target port not found" 629 | }).catch(err => console.log('Responding failed:', err)) 630 | } 631 | }) 632 | ``` 633 | If you have a need for only one ADS port, you might do something as simple as: 634 | 635 | ```ts 636 | server.onReadReq(async (req, res, packet, adsPort) => { 637 | console.log('Read request', req, 'received to ADS port', adsPort) 638 | 639 | if (adsPort !== 30012) { //adsPort or packet.ams.targetAdsPort 640 | await res({ 641 | error: 6 //ADS error code, "Target port not found" 642 | }).catch(err => console.log('Responding failed:', err)) 643 | 644 | return 645 | } 646 | 647 | //Then do the magic here.. 648 | }) 649 | ``` 650 | 651 | # Handling IEC-61131 data types 652 | 653 | It's not too easy to work with byte buffers if you want to respond to a PLC request. 654 | 655 | Instead, you can use the [iec-61131-3](https://github.com/jisotalo/iec-61131-3) library to work with IEC data types. 656 | 657 | ## Responding with a IEC data type 658 | 659 | Some examples how to respond with different data types: 660 | 661 | ```js 662 | const iec = require('iec-61131-3') 663 | 664 | //... 665 | 666 | //DINT 667 | await res({ 668 | data: iec.DINT.convertToBuffer(12345) 669 | }) 670 | 671 | //STRING 672 | await res({ 673 | data: iec.STRING(81).convertToBuffer('Convert this!') 674 | }) 675 | 676 | //ARRAY[0..2] OF INT 677 | await res({ 678 | data: iec.ARRAY(iec.INT, 3).convertToBuffer([1, 2, 3]) 679 | }) 680 | 681 | //DT 682 | await res({ 683 | data: iec.DT.convertToBuffer(new Date().getTime() / 1000) 684 | }) 685 | ``` 686 | 687 | The same library can be used with `ads-client`. For example: 688 | 689 | ```js 690 | const iec = require('iec-61131-3') 691 | 692 | //... 693 | 694 | //Reading the ARRAY[0..2] OF INT defined above (indexGroup and indexOffset are just examples) 695 | const data = await client.readRaw(1, 2, iec.ARRAY(iec.INT, 3).byteLength) 696 | const arr = iec.ARRAY(iec.INT, 3).convertFromBuffer(data) 697 | console.log(arr) //"[ 1, 2, 3 ]" 698 | 699 | //Reading the STRING defined above (indexGroup and indexOffset are just examples) 700 | const data = await client.readRaw(1, 2, iec.STRING(81).byteLength) 701 | const str = iec.STRING(81).convertFromBuffer(data) 702 | console.log(str) //"Convert this!" 703 | ``` 704 | 705 | ## Responding with a STRUCT 706 | 707 | ```js 708 | const iec = require('iec-61131-3') 709 | 710 | //... 711 | 712 | const ST_Struct = iec.fromString(` 713 | {attribute 'pack_mode' := '1'} 714 | TYPE ST_Struct: 715 | STRUCT 716 | variable1: INT; 717 | variable2: REAL; 718 | END_STRUCT 719 | END_TYPE 720 | `) 721 | 722 | const data = ST_Struct.convertToBuffer({ 723 | variable1: 123, 724 | variable2: 3.14 725 | }) 726 | 727 | await res({ data }) 728 | 729 | ``` 730 | 731 | # Example: All example codes as a working version 732 | 733 | Please see `./example` directory in the repository for working example (both PLC and server side). 734 | 735 | **To run the example:** 736 | 1. Navigate to `./example` and Initialize a new package 737 | ```js 738 | npm init -y 739 | ``` 740 | 2. Install `ads-server` 741 | ```js 742 | npm i ads-server 743 | ``` 744 | 745 | 3. Run the code 746 | ```js 747 | node example.js 748 | ``` 749 | 750 | 751 | and then import the PLC code to your project and call the `PRG_AdsServerExample` program. Force commands manually to test each ADS command. 752 | 753 | The PLC code is exported as PlcOpenXML format. 754 | 755 | # Example: How to display full ADS packet from (debug etc.) 756 | 757 | In the examples above, a callback of type `(req, res)` is provided for each function. It is also possible to provide 3rd parameter `(req, res, packet)` for debugging purposes. 758 | 759 | ```ts 760 | server.onReadReq(async (req, res, packet) => { 761 | console.log('Full packet is:', packet) 762 | //... 763 | ``` 764 | 765 | Example console output: 766 | ```ts 767 | Full packet is: { 768 | amsTcp: { command: 0, commandStr: 'Ads command', dataLength: 44 }, 769 | ams: { 770 | targetAmsNetId: '192.168.5.131.1.1', 771 | targetAdsPort: 30012, 772 | sourceAmsNetId: '192.168.5.131.1.1', 773 | sourceAdsPort: 350, 774 | adsCommand: 2, 775 | adsCommandStr: 'Read', 776 | stateFlags: 4, 777 | stateFlagsStr: 'AdsCommand, Tcp, Request', 778 | dataLength: 12, 779 | errorCode: 0, 780 | invokeId: 4278255622, 781 | error: false, 782 | errorStr: '' 783 | }, 784 | ads: { indexGroup: 10, indexOffset: 100, readLength: 2 } 785 | } 786 | ``` 787 | As of version 1.1.0, there is also 4th parameter `adsPort`, which is the same as `packet.ams.targetAdsPort`. 788 | 789 | # Example: Handling device notifications with ads-client 790 | 791 | The PLC seems not to have any functionality to register notifications. So it's only available for 3rd party clients. For example using `subscribe()` in [ads-client](https://github.com/jisotalo/ads-client) library. 792 | 793 | The following example listens for certain AddNotification requests (`indexGroup = 10, indexOffset = 100, dataLength = 2`) and saves them. Then notifications are sent, until cancelled with DeleteNotification request. 794 | 795 | **Server side:** 796 | ```js 797 | const { Server } = require('ads-server') 798 | //For Typescript: import { Server } from 'ads-server' 799 | //For Typescript: import { AdsNotificationTarget } from 'ads-server/dist/types/ads-server' 800 | 801 | const server = new Server({ 802 | localAdsPort: 30012 803 | }) 804 | 805 | 806 | let freeHandle = 0 807 | let subscribers = [] 808 | //For Typescript: let subscribers: Array = [] 809 | 810 | 811 | server.connect() 812 | .then(async conn => { 813 | console.log(`Connected: ${JSON.stringify(conn)}`) 814 | 815 | //------------------------------------- 816 | // Timer that sends the notifications 817 | //------------------------------------- 818 | setInterval(async () => { 819 | //Our data is an INT of current seconds 820 | const data = Buffer.alloc(2) 821 | data.writeInt16LE(new Date().getSeconds()) 822 | 823 | //Loop each subscribers and check if enough time has passed 824 | for (const sub of subscribers) { 825 | 826 | if (new Date().getTime() - sub.lastSendTime > sub.cycleTime || sub.cycleTime === 0) { 827 | //Time to send, this should actually be done in parallel 828 | await server.sendDeviceNotification(sub, data) 829 | .then(() => sub.lastSendTime = new Date().getTime()) 830 | .catch(err => console.log('Sending notification failed:', err)) 831 | } 832 | } 833 | }, 100) 834 | 835 | 836 | //------------------------------------- 837 | // Listening for new subscribers 838 | //------------------------------------- 839 | server.onAddNotification(async (req, res) => { 840 | console.log('AddNotification request received:', req) 841 | 842 | //Is the request valid? 843 | if (req.indexGroup === 10 && req.indexOffset === 100 && req.dataLength === 2) { 844 | 845 | const notificationHandle = freeHandle++ 846 | 847 | req.notificationTarget.notificationHandle = notificationHandle 848 | 849 | subscribers.push({ 850 | ...req.notificationTarget, 851 | cycleTime: req.cycleTime, 852 | lastSendTime: 0 853 | }) 854 | 855 | await res({ notificationHandle }) 856 | .then(() => console.log('New subscriber registered with handle', notificationHandle)) 857 | .catch(err => console.log('Responding failed:', err)) 858 | 859 | } else { 860 | //Unknown request 861 | res({ error: 1808 }) //1808 = symbol not found 862 | .catch(err => console.log('Responding failed:', err)) 863 | } 864 | }) 865 | 866 | 867 | //------------------------------------- 868 | // Listening for unsubscribe requests 869 | //------------------------------------- 870 | server.onDeleteNotification(async (req, res) => { 871 | console.log('DeleteNotification request received:', req) 872 | 873 | //Delete existing notification 874 | if (subscribers.find(sub => sub.notificationHandle === req.notificationHandle)) { 875 | //Found, remove it 876 | subscribers = subscribers.filter(sub => sub.notificationHandle !== req.notificationHandle) 877 | 878 | //Respond OK 879 | await res({}) 880 | .then(() => console.log('Subscriber with handle', req.notificationHandle, 'removed')) 881 | .catch(err => console.log('Responding failed:', err)) 882 | 883 | } else { 884 | //Unknown handle 885 | await res({ error: 1812 }) //1812 = Notification handle is invalid 886 | .catch(err => console.log('Responding failed:', err)) 887 | } 888 | }) 889 | }) 890 | ``` 891 | 892 | **Client side (uses ads-client library)** 893 | ```js 894 | const { Client } = require('ads-client') 895 | 896 | const client = new Client({ 897 | targetAmsNetId: 'localhost', 898 | targetAdsPort: 30012, 899 | allowHalfOpen: true //IMPORTANT: We don't have PLC as it's our own server 900 | }) 901 | 902 | client.connect() 903 | .then(async () => { 904 | console.log('Connected') 905 | 906 | try { 907 | //Subscribe with 1s cycle time 908 | const sub = await client.subscribeRaw(10, 100, 2, async data => { 909 | console.log('Notification received:', data, '- value as INT:', data.value.readInt16LE()) 910 | }, 1000) 911 | 912 | console.log('Subscribed') 913 | 914 | //Unsubscribing after 10 seconds 915 | setTimeout(async () => { 916 | await sub.unsubscribe() 917 | console.log('Unsubscribed') 918 | }, 10000) 919 | 920 | } catch (err) { 921 | console.log('Something went wrong:', err) 922 | } 923 | }) 924 | .catch(err => console.log('Failed to connect:', err)) 925 | ``` 926 | 927 | # Example: Creating a fake PLC 928 | The following needs to be provided by the ADS server in order the `ads-client` sees it as a normal PLC: 929 | - System manager state 930 | - PLC runtime state 931 | - PLC runtime state changes (notifications) 932 | - Device info 933 | - Upload info 934 | - *Optional: Symbol version* 935 | - *Optional: Symbol version changes (notifications)* 936 | - *Optional: Symbols* 937 | - *Optional: Data types* 938 | 939 | In this example the optional parts are skipped. In order the `ads-client` to work without them, following settings need to be provided to `ads-client`: 940 | 941 | ```js 942 | const client = new ads.Client({ 943 | //Unrelevant settings not shown 944 | disableSymbolVersionMonitoring: true, 945 | readAndCacheSymbols: false, 946 | readAndCacheDataTypes: false, 947 | }) 948 | ``` 949 | 950 | ## Base code for fake PLC system with `Server` 951 | 952 | The following can be used as a base to fake a PLC system. 953 | 954 | The system manager is handled by TwinCAT router or some other router. 955 | 956 | ```js 957 | const { Server, ADS } = require('./ads-server/dist/ads-server') 958 | 959 | const server = new Server({ 960 | localAdsPort: ADS.ADS_RESERVED_PORTS.Tc3_Plc1 //NOTE: Local PLC can't be running at the same time 961 | }) 962 | 963 | server.onReadState(async (req, res, packet, adsPort) => { 964 | if (adsPort === ADS.ADS_RESERVED_PORTS.Tc3_Plc1) { 965 | //TC3 PLC runtime 1 (port 851) 966 | await res({ 967 | adsState: ADS.ADS_STATE.Run, 968 | deviceState: 0 969 | }).catch(err => console.log('Responding failed:', err)) 970 | 971 | } else { 972 | //Unknown port 973 | await res({ 974 | error: 6 //"Target port not found" 975 | }).catch(err => console.log('Responding failed:', err)) 976 | } 977 | }) 978 | 979 | server.onReadDeviceInfo(async (req, res, packet, adsPort) => { 980 | if (adsPort === ADS.ADS_RESERVED_PORTS.Tc3_Plc1) { 981 | //TC3 PLC runtime 1 (port 851) 982 | await res({ 983 | deviceName: 'Fake PLC runtime 1', 984 | majorVersion: 1, 985 | minorVersion: 0, 986 | versionBuild: 1 987 | }).catch(err => console.log('Responding failed:', err)) 988 | 989 | } else { 990 | //Unknown port 991 | await res({ 992 | error: 6 //"Target port not found" 993 | }).catch(err => console.log('Responding failed:', err)) 994 | } 995 | }) 996 | 997 | server.onAddNotification(async (req, res, packet, adsPort) => { 998 | if (adsPort === ADS.ADS_RESERVED_PORTS.Tc3_Plc1) { 999 | //TC3 PLC runtime 1 (port 851) 1000 | if (req.indexGroup === ADS.ADS_RESERVED_INDEX_GROUPS.DeviceData) { 1001 | //Runtime state changes 1002 | await res({ 1003 | notificationHandle: 1 //This isn't correct way, see example "Handling device notifications with ads-client" 1004 | }).catch(err => console.log('Responding failed:', err)) 1005 | 1006 | } else { 1007 | //Your custom notification handles should be here 1008 | //For now, just answer with error 1009 | await res({ 1010 | error: 1794 //"Invalid index group" 1011 | }).catch(err => console.log('Responding failed:', err)) 1012 | } 1013 | 1014 | } else { 1015 | //Unknown port 1016 | await res({ 1017 | error: 6 //"Target port not found" 1018 | }).catch(err => console.log('Responding failed:', err)) 1019 | } 1020 | }) 1021 | 1022 | server.onReadReq(async (req, res, packet, adsPort) => { 1023 | if (adsPort === ADS.ADS_RESERVED_PORTS.Tc3_Plc1) { 1024 | if (req.indexGroup === ADS.ADS_RESERVED_INDEX_GROUPS.SymbolUploadInfo2) { 1025 | //Upload info, responding 0 to all for now 1026 | const data = Buffer.alloc(24) 1027 | let pos = 0 1028 | 1029 | //0..3 Symbol count 1030 | data.writeUInt32LE(0, pos) 1031 | pos += 4 1032 | 1033 | //4..7 Symbol length 1034 | data.writeUInt32LE(0, pos) 1035 | pos += 4 1036 | 1037 | //8..11 Data type count 1038 | data.writeUInt32LE(0, pos) 1039 | pos += 4 1040 | 1041 | //12..15 Data type length 1042 | data.writeUInt32LE(0, pos) 1043 | pos += 4 1044 | 1045 | //16..19 Extra count 1046 | data.writeUInt32LE(0, pos) 1047 | pos += 4 1048 | 1049 | //20..23 Extra length 1050 | data.writeUInt32LE(0, pos) 1051 | pos += 4 1052 | 1053 | await res({ 1054 | data 1055 | }).catch(err => console.log('Responding failed:', err)) 1056 | 1057 | } else { 1058 | //Your custom notification handles should be here 1059 | //For now, just answer with error 1060 | await res({ 1061 | error: 1794 //"Invalid index group" 1062 | }).catch(err => console.log('Responding failed:', err)) 1063 | } 1064 | } else { 1065 | //Unknown port 1066 | await res({ 1067 | error: 6 //"Target port not found" 1068 | }).catch(err => console.log('Responding failed:', err)) 1069 | } 1070 | }) 1071 | 1072 | server.onDeleteNotification(async (req, res, packet, adsPort) => { 1073 | if (adsPort === ADS.ADS_RESERVED_PORTS.Tc3_Plc1) { 1074 | //TC3 PLC runtime 1 (port 851) 1075 | if (req.notificationHandle === 1) { //This isn't correct way, see example "Handling device notifications with ads-client" 1076 | await res({ }).catch(err => console.log('Responding failed:', err)) 1077 | 1078 | } else { 1079 | //Your custom notification handle deletion should be here 1080 | //For now, just answer with error 1081 | await res({ 1082 | error: 1794 //"Invalid index group" 1083 | }).catch(err => console.log('Responding failed:', err)) 1084 | } 1085 | 1086 | } else { 1087 | //Unknown port 1088 | await res({ 1089 | error: 6 //"Target port not found" 1090 | }).catch(err => console.log('Responding failed:', err)) 1091 | } 1092 | }) 1093 | 1094 | 1095 | server.connect() 1096 | .then(res => { 1097 | console.log('Connected:', res) 1098 | }) 1099 | .catch(err => { 1100 | console.log('Error starting:', err) 1101 | }) 1102 | 1103 | ``` 1104 | 1105 | ## Base code for fake PLC system with `StandAloneServer` 1106 | 1107 | The following can be used as a base to fake a PLC system. It also handles system manager at port 10000. 1108 | 1109 | Won't work if there is a local router. 1110 | ```js 1111 | const { StandAloneServer, ADS } = require('./ads-server/dist/ads-server') 1112 | 1113 | const server = new StandAloneServer({ 1114 | localAmsNetId: '192.168.5.1.1.1' 1115 | }) 1116 | 1117 | server.onReadState(async (req, res, packet, adsPort) => { 1118 | if (adsPort === ADS.ADS_RESERVED_PORTS.SystemService) { 1119 | //System manager / system service (port 10000) 1120 | await res({ 1121 | adsState: ADS.ADS_STATE.Run, 1122 | deviceState: 0 1123 | }).catch(err => console.log('Responding failed:', err)) 1124 | 1125 | } else if (adsPort === ADS.ADS_RESERVED_PORTS.Tc3_Plc1) { 1126 | //TC3 PLC runtime 1 (port 851) 1127 | await res({ 1128 | adsState: ADS.ADS_STATE.Run, 1129 | deviceState: 0 1130 | }).catch(err => console.log('Responding failed:', err)) 1131 | 1132 | } else { 1133 | //Unknown port 1134 | await res({ 1135 | error: 6 //"Target port not found" 1136 | }).catch(err => console.log('Responding failed:', err)) 1137 | } 1138 | }) 1139 | 1140 | server.onReadDeviceInfo(async (req, res, packet, adsPort) => { 1141 | if (adsPort === ADS.ADS_RESERVED_PORTS.SystemService) { 1142 | //System manager / system service (port 10000) 1143 | await res({ 1144 | deviceName: 'Fake PLC', 1145 | majorVersion: 1, 1146 | minorVersion: 0, 1147 | versionBuild: 1 1148 | }).catch(err => console.log('Responding failed:', err)) 1149 | 1150 | } else if (adsPort === ADS.ADS_RESERVED_PORTS.Tc3_Plc1) { 1151 | //TC3 PLC runtime 1 (port 851) 1152 | await res({ 1153 | deviceName: 'Fake PLC runtime 1', 1154 | majorVersion: 1, 1155 | minorVersion: 0, 1156 | versionBuild: 1 1157 | }).catch(err => console.log('Responding failed:', err)) 1158 | 1159 | } else { 1160 | //Unknown port 1161 | await res({ 1162 | error: 6 //"Target port not found" 1163 | }).catch(err => console.log('Responding failed:', err)) 1164 | } 1165 | }) 1166 | 1167 | server.onAddNotification(async (req, res, packet, adsPort) => { 1168 | if (adsPort === ADS.ADS_RESERVED_PORTS.SystemService) { 1169 | //System manager / system service (port 10000) 1170 | await res({ 1171 | error: 1793 //"Service is not supported by server" 1172 | }).catch(err => console.log('Responding failed:', err)) 1173 | 1174 | } else if (adsPort === ADS.ADS_RESERVED_PORTS.Tc3_Plc1) { 1175 | //TC3 PLC runtime 1 (port 851) 1176 | if (req.indexGroup === ADS.ADS_RESERVED_INDEX_GROUPS.DeviceData) { 1177 | //Runtime state changes 1178 | await res({ 1179 | notificationHandle: 1 //This isn't correct way, see example "Handling device notifications with ads-client" 1180 | }).catch(err => console.log('Responding failed:', err)) 1181 | 1182 | } else { 1183 | //Your custom notification handles should be here 1184 | //For now, just answer with error 1185 | await res({ 1186 | error: 1794 //"Invalid index group" 1187 | }).catch(err => console.log('Responding failed:', err)) 1188 | } 1189 | 1190 | } else { 1191 | //Unknown port 1192 | await res({ 1193 | error: 6 //"Target port not found" 1194 | }).catch(err => console.log('Responding failed:', err)) 1195 | } 1196 | }) 1197 | 1198 | server.onReadReq(async (req, res, packet, adsPort) => { 1199 | if (adsPort === ADS.ADS_RESERVED_PORTS.SystemService) { 1200 | //System manager / system service (port 10000) 1201 | await res({ 1202 | error: 1793 //"Service is not supported by server" 1203 | }).catch(err => console.log('Responding failed:', err)) 1204 | 1205 | } else if (adsPort === ADS.ADS_RESERVED_PORTS.Tc3_Plc1) { 1206 | if (req.indexGroup === ADS.ADS_RESERVED_INDEX_GROUPS.SymbolUploadInfo2) { 1207 | //Upload info, responding 0 to all for now 1208 | const data = Buffer.alloc(24) 1209 | let pos = 0 1210 | 1211 | //0..3 Symbol count 1212 | data.writeUInt32LE(0, pos) 1213 | pos += 4 1214 | 1215 | //4..7 Symbol length 1216 | data.writeUInt32LE(0, pos) 1217 | pos += 4 1218 | 1219 | //8..11 Data type count 1220 | data.writeUInt32LE(0, pos) 1221 | pos += 4 1222 | 1223 | //12..15 Data type length 1224 | data.writeUInt32LE(0, pos) 1225 | pos += 4 1226 | 1227 | //16..19 Extra count 1228 | data.writeUInt32LE(0, pos) 1229 | pos += 4 1230 | 1231 | //20..23 Extra length 1232 | data.writeUInt32LE(0, pos) 1233 | pos += 4 1234 | 1235 | await res({ 1236 | data 1237 | }).catch(err => console.log('Responding failed:', err)) 1238 | 1239 | } else { 1240 | //Your custom notification handles should be here 1241 | //For now, just answer with error 1242 | await res({ 1243 | error: 1794 //"Invalid index group" 1244 | }).catch(err => console.log('Responding failed:', err)) 1245 | } 1246 | } else { 1247 | //Unknown port 1248 | await res({ 1249 | error: 6 //"Target port not found" 1250 | }).catch(err => console.log('Responding failed:', err)) 1251 | } 1252 | }) 1253 | 1254 | server.onDeleteNotification(async (req, res, packet, adsPort) => { 1255 | if (adsPort === ADS.ADS_RESERVED_PORTS.SystemService) { 1256 | //System manager / system service (port 10000) 1257 | await res({ 1258 | error: 1793 //"Service is not supported by server" 1259 | }).catch(err => console.log('Responding failed:', err)) 1260 | 1261 | } else if (adsPort === ADS.ADS_RESERVED_PORTS.Tc3_Plc1) { 1262 | //TC3 PLC runtime 1 (port 851) 1263 | if (req.notificationHandle === 1) { //This isn't correct way, see example "Handling device notifications with ads-client" 1264 | await res({ }).catch(err => console.log('Responding failed:', err)) 1265 | 1266 | } else { 1267 | //Your custom notification handle deletion should be here 1268 | //For now, just answer with error 1269 | await res({ 1270 | error: 1794 //"Invalid index group" 1271 | }).catch(err => console.log('Responding failed:', err)) 1272 | } 1273 | 1274 | } else { 1275 | //Unknown port 1276 | await res({ 1277 | error: 6 //"Target port not found" 1278 | }).catch(err => console.log('Responding failed:', err)) 1279 | } 1280 | }) 1281 | 1282 | 1283 | server.listen() 1284 | .then(res => { 1285 | console.log('Listening:', res) 1286 | }) 1287 | .catch(err => { 1288 | console.log('Error starting to listen:', err) 1289 | }) 1290 | 1291 | ``` 1292 | 1293 | # Debugging 1294 | 1295 | To debug each received packet, see: [Example: How to display full ADS packet from (debug etc.)](#example--how-to-display-full-ads-packet-from--debug-etc-) 1296 | 1297 | If you have problems or you are interested, you can enabled debug output to console. The ads-server uses `debug` package for debugging. 1298 | 1299 | Debugging can be enabled from terminal or from code. 1300 | 1301 | ## Enabling debug from code 1302 | 1303 | You can change the debug level with method `setDebugging(level)`: 1304 | ```js 1305 | server.setDebugging(2) 1306 | ``` 1307 | Different debug levels explained: 1308 | - 0: No debugging (default) 1309 | - 1: Errors have full stack traces, no debug printing 1310 | - 2: Basic debug printing (same as `DEBUG='ads-server'`) 1311 | - 3: Detailed debug printing (same as `DEBUG='ads-server,ads-server:details'`) 1312 | - 4: Detailed debug printing and raw I/O data (same as `DEBUG='ads-server*'`) 1313 | 1314 | ## Enabling debugging from terminal 1315 | See the [debug package](https://www.npmjs.com/package/debug) for instructions. 1316 | 1317 | Example for Visual Studio Code (PowerShell): 1318 | ```bash 1319 | $env:DEBUG='ads-server,ads-server:details' 1320 | ``` 1321 | 1322 | Different debug levels explained: 1323 | - Basic debug printing `DEBUG='ads-server'` 1324 | - Basic and detailed debug printing `DEBUG='ads-server,ads-server:details'` 1325 | - Basic, detailed and raw I/O data: `DEBUG='ads-server*'` 1326 | 1327 | 1328 | # License 1329 | 1330 | Licensed under [MIT License](http://www.opensource.org/licenses/MIT) so commercial use is possible. Please respect the license, linking to this page is also much appreciated. 1331 | 1332 | Copyright (c) 2021 Jussi Isotalo <> 1333 | 1334 | Permission is hereby granted, free of charge, to any person obtaining a copy 1335 | of this software and associated documentation files (the "Software"), to deal 1336 | in the Software without restriction, including without limitation the rights 1337 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 1338 | copies of the Software, and to permit persons to whom the Software is 1339 | furnished to do so, subject to the following conditions: 1340 | 1341 | The above copyright notice and this permission notice shall be included in all 1342 | copies or substantial portions of the Software. 1343 | 1344 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 1345 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 1346 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 1347 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 1348 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 1349 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 1350 | SOFTWARE. 1351 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 |

ads-server

2 |

3 | TwinCAT ADS server for Node.js (unofficial). Listens for incoming ADS protocol commands and responds. 4 |

5 |

6 | Documentation coming up later. 7 |

8 |

9 | See GitHub repository 10 |

-------------------------------------------------------------------------------- /example/PLC/PRG_AdsServerExample.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | https://github.com/jisotalo/ads-server 126 | PRG_AdsServerExample 127 | 128 | Copyright (c) 2021 Jussi Isotalo <j.isotalo91@gmail.com> 129 | 130 | Permission is hereby granted, free of charge, to any person obtaining a copy 131 | of this software and associated documentation files (the "Software"), to deal 132 | in the Software without restriction, including without limitation the rights 133 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 134 | copies of the Software, and to permit persons to whom the Software is 135 | furnished to do so, subject to the following conditions: 136 | 137 | The above copyright notice and this permission notice shall be included in all 138 | copies or substantial portions of the Software. 139 | 140 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 141 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 142 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 143 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 144 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 145 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 146 | SOFTWARE. 147 | 148 | 149 | 150 | 151 | 152 | //When ReadCmd is TRUE, a value is read from localhost and ADS port 30012 153 | AdsRead( 154 | NETID := , 155 | PORT := 30012, 156 | IDXGRP := 10, 157 | IDXOFFS := 100, 158 | LEN := SIZEOF(ReadValue), 159 | DESTADDR:= ADR(ReadValue), 160 | READ := ReadCmd, 161 | ); 162 | 163 | IF NOT AdsRead.BUSY THEN 164 | //Now ReadValue should be 4455 or AdsReader.ERR is true and AdsReader.ERRID is the error code 165 | ReadCmd := FALSE; 166 | END_IF 167 | 168 | //When WriteCmd is TRUE, a value is written to localhost and ADS port 30012 169 | AdsWrite( 170 | NETID := , 171 | PORT := 30012, 172 | IDXGRP := 10, 173 | IDXOFFS := 100, 174 | LEN := SIZEOF(WriteValue), 175 | SRCADDR := ADR(WriteValue), 176 | WRITE := WriteCmd 177 | ); 178 | 179 | IF NOT AdsWrite.BUSY THEN 180 | WriteCmd := FALSE; 181 | END_IF 182 | 183 | 184 | //When ReadWriteCmd is TRUE, a value is written to localhost and ADS port 30012 and result is read 185 | AdsReadWriter( 186 | NETID := , 187 | PORT := 30012, 188 | IDXGRP := 10, 189 | IDXOFFS := 100, 190 | WRITELEN:= SIZEOF(RW_WriteValue), 191 | READLEN := SIZEOF(RW_ReadValue), 192 | SRCADDR := ADR(RW_WriteValue), 193 | DESTADDR:= ADR(RW_ReadValue), 194 | WRTRD := ReadWriteCmd 195 | ); 196 | 197 | IF NOT AdsReadWriter.BUSY THEN 198 | ReadWriteCmd := FALSE; 199 | END_IF 200 | 201 | 202 | //When ReadDevInfoCmd is TRUE, device info is read from localhost and ADS port 30012 203 | AdsReadDevInfo( 204 | NETID := , 205 | PORT := 30012, 206 | RDINFO := ReadDevInfoCmd 207 | ); 208 | 209 | IF NOT AdsReadDevInfo.BUSY THEN 210 | //NOTE: ADS protocol has major version, minor version and build version unlike the ADS PLC block 211 | // -> version is corrupted 212 | ReadDevInfoCmd := FALSE; 213 | END_IF 214 | 215 | 216 | //When AdsReadDevState is TRUE, device state is read from localhost and ADS port 30012 217 | AdsReadDevState( 218 | NETID := , 219 | PORT := 30012, 220 | RDSTATE := ReadDevStateCmd 221 | ); 222 | 223 | IF NOT AdsReadDevState.BUSY THEN 224 | ReadDevStateCmd := FALSE; 225 | END_IF 226 | 227 | //When WriteCtrlCmd is TRUE, values are written to localhost and ADS port 30012 228 | AdsWriteCtrl( 229 | NETID := , 230 | PORT := 30012, 231 | ADSSTATE:= ADSSTATE_RUN, 232 | DEVSTATE:= 123, 233 | LEN := SIZEOF(WriteCtrlData), 234 | SRCADDR := ADR(WriteCtrlData), 235 | WRITE := WriteCtrlCmd 236 | ); 237 | 238 | IF NOT AdsWriteCtrl.BUSY THEN 239 | WriteCtrlCmd := FALSE; 240 | END_IF 241 | 242 | 243 | 244 | 245 | 246 | (* 247 | https://github.com/jisotalo/ads-server 248 | PRG_AdsServerExample 249 | 250 | Copyright (c) 2021 Jussi Isotalo <j.isotalo91@gmail.com> 251 | 252 | Permission is hereby granted, free of charge, to any person obtaining a copy 253 | of this software and associated documentation files (the "Software"), to deal 254 | in the Software without restriction, including without limitation the rights 255 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 256 | copies of the Software, and to permit persons to whom the Software is 257 | furnished to do so, subject to the following conditions: 258 | 259 | The above copyright notice and this permission notice shall be included in all 260 | copies or substantial portions of the Software. 261 | 262 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 263 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 264 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 265 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 266 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 267 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 268 | SOFTWARE. 269 | *) 270 | PROGRAM PRG_AdsServerExample 271 | VAR 272 | AdsRead : Tc2_System.ADSREADEX; 273 | ReadValue : INT; 274 | ReadCmd : BOOL; 275 | 276 | AdsWrite : Tc2_System.ADSWRITE; 277 | WriteValue : INT := 5555; 278 | WriteCmd : BOOL; 279 | 280 | AdsReadWriter : Tc2_System.ADSRDWRTEX; 281 | RW_WriteValue : STRING := 'Temperature 1'; 282 | RW_ReadValue : REAL; 283 | ReadWriteCmd : BOOL; 284 | 285 | AdsReadDevInfo : Tc2_System.ADSRDDEVINFO; 286 | ReadDevInfoCmd : BOOL; 287 | 288 | AdsReadDevState : Tc2_System.ADSRDSTATE; 289 | ReadDevStateCmd : BOOL; 290 | 291 | AdsWriteCtrl : Tc2_System.ADSWRTCTL; 292 | WriteCtrlData : STRING := 'Some test data to write control'; 293 | WriteCtrlCmd : BOOL; 294 | END_VAR 295 | 296 | 297 | 298 | 299 | 14fa673f-cd77-428c-aea4-bd1a94d931cb 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | /* 2 | https://github.com/jisotalo/ads-server 3 | example/example.js 4 | 5 | Copyright (c) 2021 Jussi Isotalo 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | */ 25 | 26 | const { ADS, Server } = require('ads-server') 27 | //For Typescript: import { ADS, Server } from 'ads-server' 28 | 29 | const server = new Server({ 30 | localAdsPort: 30012 31 | }) 32 | 33 | server.connect() 34 | .then(async conn => { 35 | console.log('Connected:', conn) 36 | 37 | //-------------------------- 38 | // Listen for Read requests 39 | //-------------------------- 40 | server.onReadReq(async (req, res, packet) => { 41 | console.log('Read request received:', req) 42 | 43 | //Create an INT value of 4455 44 | const data = Buffer.alloc(2) 45 | data.writeInt16LE(4455) 46 | 47 | //Respond with data 48 | await res({ data }) 49 | .catch(err => console.log('Responding failed:', err)) 50 | 51 | /* Or to respond with an error: 52 | await res({ 53 | error: 1793 //ADS error code, for example 1793 = Service is not supported by server 54 | }).catch(err => console.log('Responding failed:', err)) */ 55 | }) 56 | 57 | 58 | //-------------------------- 59 | // Listen for Write requests 60 | //-------------------------- 61 | server.onWriteReq(async (req, res) => { 62 | console.log('Write request received:', req) 63 | 64 | //Do something with the given data 65 | console.log('Writing', req.data.byteLength, 'bytes of data') 66 | 67 | //Respond OK 68 | await res({}) 69 | .catch(err => console.log('Responding failed:', err)) 70 | 71 | /* Or to respond with an error: 72 | await res({ 73 | error: 1793 //ADS error code, for example 1793 = Service is not supported by server 74 | }).catch(err => console.log('Responding failed:', err)) */ 75 | }) 76 | 77 | 78 | //-------------------------- 79 | // Listen for ReadWrite requests 80 | //-------------------------- 81 | server.onReadWriteReq(async (req, res) => { 82 | console.log('ReadWrite request received:', req) 83 | 84 | //Do something with the given data 85 | const requestedValue = server.trimPlcString(req.data.toString('ascii')) 86 | console.log('Requested value: ', requestedValue) 87 | 88 | //This example does not care about index group and index offset 89 | //Instead we just check the received data 90 | if (requestedValue === 'Temperature 1') { 91 | 92 | //Create an REAL value of 27.678 93 | const data = Buffer.alloc(4) 94 | data.writeFloatLE(27.678) 95 | 96 | //Respond with data 97 | await res({ data }).catch(err => console.log('Responding failed:', err)) 98 | 99 | } else { 100 | 101 | await res({ 102 | error: 1793 //ADS error code, for example 1793 = Service is not supported by server 103 | }).catch(err => console.log('Responding failed:', err)) 104 | } 105 | }) 106 | 107 | 108 | 109 | //-------------------------- 110 | // Listen for ReadDeviceInfo requests 111 | //-------------------------- 112 | server.onReadDeviceInfo(async (req, res) => { 113 | console.log('ReadDeviceInfo request received') 114 | 115 | //Respond with data 116 | res({ 117 | deviceName: 'Server example', 118 | majorVersion: 5, 119 | minorVersion: 123, 120 | versionBuild: 998 121 | }) 122 | 123 | /* Or to respond with an error: 124 | await res({ 125 | error: 1793 //ADS error code, for example 1793 = Service is not supported by server 126 | }).catch(err => console.log('Responding failed:', err)) */ 127 | }) 128 | 129 | 130 | 131 | //-------------------------- 132 | // Listen for ReadState requests 133 | //-------------------------- 134 | server.onReadState(async (req, res) => { 135 | console.log('ReadState request received') 136 | 137 | //Respond with data 138 | await res({ 139 | adsState: ADS.ADS_STATE.Config, //Or just any number (ADS can be imported from ads-server package) 140 | deviceState: 123 141 | }).catch(err => console.log('Responding failed:', err)) 142 | 143 | /* Or to respond with an error: 144 | await res({ 145 | error: 1793 //ADS error code, for example 1793 = Service is not supported by server 146 | }).catch(err => console.log('Responding failed:', err)) */ 147 | }) 148 | 149 | 150 | 151 | //-------------------------- 152 | // Listen for WriteControl requests 153 | //-------------------------- 154 | server.onWriteControl(async (req, res) => { 155 | console.log('WriteControl request received:', req) 156 | 157 | //Do something with req 158 | const dataStr = server.trimPlcString(req.data.toString('ascii')) 159 | console.log('Requested ADS state:', req.adsStateStr, ', provided data:', dataStr) 160 | 161 | //Respond OK 162 | res({}).catch(err => console.log('Responding failed:', err)) 163 | 164 | /* Or to respond with an error: 165 | await res({ 166 | error: 1793 //ADS error code, for example 1793 = Service is not supported by server 167 | }).catch(err => console.log('Responding failed:', err)) */ 168 | }) 169 | 170 | }) 171 | .catch(err => { 172 | console.log('Connecting failed:', err) 173 | }) 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ads-server", 3 | "version": "1.1.2", 4 | "description": "TwinCAT ADS server for Node.js (unofficial). Listens for incoming ADS protocol commands and responds.", 5 | "main": "./dist/ads-server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tsc", 9 | "watch": "tsc --watch", 10 | "lint": "eslint . --ext .ts" 11 | }, 12 | "keywords": [ 13 | "ADS", 14 | "twincat", 15 | "beckhoff", 16 | "plc", 17 | "iec-61131-3", 18 | "61131-3", 19 | "twincat 2", 20 | "twincat 2.11", 21 | "twincat 3", 22 | "twincat 3.1", 23 | "twincat ads", 24 | "codesys", 25 | "client", 26 | "server" 27 | ], 28 | "author": "Jussi Isotalo (https://github.com/jisotalo)", 29 | "license": "MIT", 30 | "homepage": "https://github.com/jisotalo/ads-server/", 31 | "bugs": { 32 | "url": "https://github.com/jisotalo/ads-server/issues" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/jisotalo/ads-server.git" 37 | }, 38 | "dependencies": { 39 | "debug": "^4.1.1", 40 | "iconv-lite": "^0.5.1", 41 | "long": "^4.0.0" 42 | }, 43 | "devDependencies": { 44 | "@types/debug": "^4.1.5", 45 | "@types/long": "^4.0.1", 46 | "@types/node": "^14.14.14", 47 | "@typescript-eslint/eslint-plugin": "^4.11.1", 48 | "@typescript-eslint/parser": "^4.11.1", 49 | "docdash": "^1.2.0", 50 | "eslint": "^7.17.0", 51 | "jsdoc": "^3.6.4", 52 | "typescript": "^4.1.3" 53 | }, 54 | "files": [ 55 | "dist/", 56 | "CHANGELOG.md", 57 | "README.md" 58 | ], 59 | "types": "./dist/ads-server.d.ts" 60 | } 61 | -------------------------------------------------------------------------------- /src/ads-commons.ts: -------------------------------------------------------------------------------- 1 | /* 2 | https://github.com/jisotalo/ads-server 3 | 4 | Copyright (c) 2021 Jussi Isotalo 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | 26 | /** 27 | * AMS/TCP header length 28 | */ 29 | export const AMS_TCP_HEADER_LENGTH = 6 30 | 31 | /** 32 | * AMS header length 33 | */ 34 | export const AMS_HEADER_LENGTH = 32 35 | 36 | /** 37 | * AmsNetId length 38 | */ 39 | export const AMS_NET_ID_LENGTH = 6 40 | 41 | /** 42 | * ADS index offset length 43 | */ 44 | export const ADS_INDEX_OFFSET_LENGTH = 4 45 | 46 | /** 47 | * ADS index group length 48 | */ 49 | export const ADS_INDEX_GROUP_LENGTH = 4 50 | 51 | /** 52 | * ADS invoke ID maximum value (32bit unsigned integer) 53 | */ 54 | export const ADS_INVOKE_ID_MAX_VALUE = 4294967295 55 | 56 | /** 57 | * Default ADS server TCP port for incoming connections 58 | */ 59 | export const ADS_DEFAULT_TCP_PORT = 48898 60 | 61 | /** 62 | * Loopback (localhost) AmsNetId 63 | */ 64 | export const LOOPBACK_AMS_NET_ID = '127.0.0.1.1.1' 65 | 66 | /** 67 | * AMS header flag (AMS command) 68 | */ 69 | export const AMS_HEADER_FLAG = { 70 | /** 0x0000 - Used for ADS commands */ 71 | AMS_TCP_PORT_AMS_CMD: 0, 72 | /** 0x0001 - Port close command*/ 73 | AMS_TCP_PORT_CLOSE: 1, 74 | /** 0x1000 - Port connect command */ 75 | AMS_TCP_PORT_CONNECT: 4096, 76 | /** 0x1001 - Router notification */ 77 | AMS_TCP_PORT_ROUTER_NOTE: 4097, 78 | /** 0x1002 - Requests local AmsNetId */ 79 | GET_LOCAL_NETID: 4098, 80 | 81 | /** Returns the corresponding key as string by given value (number) */ 82 | toString: function (value: number): string { 83 | const val = Object.keys(this).find((key) => this[key as keyof typeof AMS_HEADER_FLAG] == value) 84 | 85 | return (val !== undefined ? val : 'UNKNOWN') 86 | } 87 | } 88 | 89 | 90 | /** 91 | * Reserved/known ADS ports 92 | * 93 | * Source: TwinCAT.Ads.dll By Beckhoff 94 | */ 95 | export const ADS_RESERVED_PORTS = { 96 | None: 0, 97 | /** AMS Router (Port 1) */ 98 | Router: 1, 99 | /** AMS Debugger (Port 2) */ 100 | Debugger: 2, 101 | /** The TCom Server. Dpc or passive level. */ 102 | R0_TComServer: 10, // 0x0000000A 103 | /** TCom Server Task. RT context. */ 104 | R0_TComServerTask: 11, // 0x0000000B 105 | /** TCom Serve Task. Passive level. */ 106 | R0_TComServer_PL: 12, // 0x0000000C 107 | /** TwinCAT Debugger */ 108 | R0_TcDebugger: 20, // 0x00000014 109 | /** TwinCAT Debugger Task */ 110 | R0_TcDebuggerTask: 21, // 0x00000015 111 | /** The License Server (Port 30) */ 112 | R0_LicenseServer: 30, // 0x0000001E 113 | /** Logger (Port 100) */ 114 | Logger: 100, // 0x00000064 115 | /** Event Logger (Port 110) */ 116 | EventLog: 110, // 0x0000006E 117 | /** application for coupler (EK), gateway (EL), etc. */ 118 | DeviceApplication: 120, // 0x00000078 119 | /** Event Logger UM */ 120 | EventLog_UM: 130, // 0x00000082 121 | /** Event Logger RT */ 122 | EventLog_RT: 131, // 0x00000083 123 | /** Event Logger Publisher */ 124 | EventLogPublisher: 132, // 0x00000084 125 | /** R0 Realtime (Port 200) */ 126 | R0_Realtime: 200, // 0x000000C8 127 | /** R0 Trace (Port 290) */ 128 | R0_Trace: 290, // 0x00000122 129 | /** R0 IO (Port 300) */ 130 | R0_IO: 300, // 0x0000012C 131 | /** NC (R0) (Port 500) */ 132 | R0_NC: 500, // 0x000001F4 133 | /** R0 Satzausführung (Port 501) */ 134 | R0_NCSAF: 501, // 0x000001F5 135 | /** R0 Satzvorbereitung (Port 511) */ 136 | R0_NCSVB: 511, // 0x000001FF 137 | /** Preconfigured Nc2-Nc3-Instance */ 138 | R0_NCINSTANCE: 520, // 0x00000208 139 | /** R0 ISG (Port 550) */ 140 | R0_ISG: 550, // 0x00000226 141 | /** R0 CNC (Port 600) */ 142 | R0_CNC: 600, // 0x00000258 143 | /** R0 Line (Port 700) */ 144 | R0_LINE: 700, // 0x000002BC 145 | /** R0 PLC (Port 800) */ 146 | R0_PLC: 800, // 0x00000320 147 | /** Tc2 PLC RuntimeSystem 1 (Port 801) */ 148 | Tc2_Plc1: 801, // 0x00000321 149 | /** Tc2 PLC RuntimeSystem 2 (Port 811) */ 150 | Tc2_Plc2: 811, // 0x0000032B 151 | /** Tc2 PLC RuntimeSystem 3 (Port 821) */ 152 | Tc2_Plc3: 821, // 0x00000335 153 | /** Tc2 PLC RuntimeSystem 4 (Port 831) */ 154 | Tc2_Plc4: 831, // 0x0000033F 155 | /** R0 RTS (Port 850) */ 156 | R0_RTS: 850, // 0x00000352 157 | /** Tc3 PLC RuntimeSystem 1 (Port 851) */ 158 | Tc3_Plc1: 851, 159 | /** Tc3 PLC RuntimeSystem 2 (Port 852) */ 160 | Tc3_Plc2: 852, 161 | /** Tc3 PLC RuntimeSystem 3 (Port 853) */ 162 | Tc3_Plc3: 853, 163 | /** Tc3 PLC RuntimeSystem 4 (Port 854) */ 164 | Tc3_Plc4: 854, 165 | /** Tc3 PLC RuntimeSystem 5 (Port 855) */ 166 | Tc3_Plc5: 855, 167 | /** Camshaft Controller (R0) (Port 900) */ 168 | CamshaftController: 900, // 0x00000384 169 | /** R0 CAM Tool (Port 950) */ 170 | R0_CAMTOOL: 950, // 0x000003B6 171 | /** R0 User (Port 2000) */ 172 | R0_USER: 2000, // 0x000007D0 173 | /** (Port 10000) */ 174 | R3_CTRLPROG: 10000, // 0x00002710 175 | /** System Service (AMSPORT_R3_SYSSERV, 10000) */ 176 | SystemService: 10000, // 0x00002710 177 | /** (Port 10001) */ 178 | R3_SYSCTRL: 10001, // 0x00002711 179 | /** Port 10100 */ 180 | R3_SYSSAMPLER: 10100, // 0x00002774 181 | /** Port 10200 */ 182 | R3_TCPRAWCONN: 10200, // 0x000027D8 183 | /** Port 10201 */ 184 | R3_TCPIPSERVER: 10201, // 0x000027D9 185 | /** Port 10300 */ 186 | R3_SYSMANAGER: 10300, // 0x0000283C 187 | /** Port 10400 */ 188 | R3_SMSSERVER: 10400, // 0x000028A0 189 | /** Port 10500 */ 190 | R3_MODBUSSERVER: 10500, // 0x00002904 191 | /** Port 10502 */ 192 | R3_AMSLOGGER: 10502, // 0x00002906 193 | /** Port 10600 */ 194 | R3_XMLDATASERVER: 10600, // 0x00002968 195 | /** Port 10700 */ 196 | R3_AUTOCONFIG: 10700, // 0x000029CC 197 | /** Port 10800 */ 198 | R3_PLCCONTROL: 10800, // 0x00002A30 199 | /** Port 10900 */ 200 | R3_FTPCLIENT: 10900, // 0x00002A94 201 | /** Port 11000 */ 202 | R3_NCCTRL: 11000, // 0x00002AF8 203 | /** Port 11500 */ 204 | R3_NCINTERPRETER: 11500, // 0x00002CEC 205 | /** Port 11600 */ 206 | R3_GSTINTERPRETER: 11600, // 0x00002D50 207 | /** Port 12000 */ 208 | R3_STRECKECTRL: 12000, // 0x00002EE0 209 | /** Port 13000 */ 210 | R3_CAMCTRL: 13000, // 0x000032C8 211 | /** Port 14000 */ 212 | R3_SCOPE: 14000, // 0x000036B0 213 | /** Port 14100 */ 214 | R3_CONDITIONMON: 14100, // 0x00003714 215 | /** Port 15000 */ 216 | R3_SINECH1: 15000, // 0x00003A98 217 | /** Port 16000 */ 218 | R3_CONTROLNET: 16000, // 0x00003E80 219 | /** Port 17000 */ 220 | R3_OPCSERVER: 17000, // 0x00004268 221 | /** Port 17500 */ 222 | R3_OPCCLIENT: 17500, // 0x0000445C 223 | /** Port 18000 */ 224 | R3_MAILSERVER: 18000, // 0x00004650 225 | /** Port 19000 */ 226 | R3_EL60XX: 19000, // 0x00004A38 227 | /** Port 19100 */ 228 | R3_MANAGEMENT: 19100, // 0x00004A9C 229 | /** Port 19200 */ 230 | R3_MIELEHOME: 19200, // 0x00004B00 231 | /** Port 19300 */ 232 | R3_CPLINK3: 19300, // 0x00004B64 233 | /** Port 19500 */ 234 | R3_VNSERVICE: 19500, // 0x00004C2C 235 | /** Multiuser (Port 19600) */ 236 | R3_MULTIUSER: 19600, // 0x00004C90 237 | /** Default (AMS router assigns) */ 238 | USEDEFAULT: 65535, // 0x0000FFFF 239 | } 240 | 241 | 242 | 243 | 244 | /** 245 | * ADS command 246 | * 247 | * Source: TwinCAT.Ads.dll By Beckhoff 248 | */ 249 | export const ADS_COMMAND = { 250 | /** Invalid */ 251 | Invalid: 0, 252 | /** None / Uninitialized */ 253 | None: 0, 254 | /** ReadDeviceInfo command */ 255 | ReadDeviceInfo: 1, 256 | /** Read Command */ 257 | Read: 2, 258 | /** Write Command */ 259 | Write: 3, 260 | /** ReadState Command */ 261 | ReadState: 4, 262 | /** WriteControl Command */ 263 | WriteControl: 5, 264 | /** AddNotification Command */ 265 | AddNotification: 6, 266 | /** DeleteNotification Command */ 267 | DeleteNotification: 7, 268 | /** Notification event. */ 269 | Notification: 8, 270 | /** ReadWrite Command */ 271 | ReadWrite: 9, 272 | 273 | /** Returns the corresponding key as string by given value (number) */ 274 | toString: function (value: number): string { 275 | const val = Object.keys(this).find((key) => this[key as keyof typeof ADS_COMMAND] == value) 276 | 277 | return (val !== undefined ? val : 'UNKNOWN') 278 | } 279 | } 280 | 281 | 282 | 283 | /** 284 | * ADS state flags 285 | * 286 | * Source: TwinCAT.Ads.dll By Beckhoff 287 | */ 288 | export const ADS_STATE_FLAGS = { 289 | //The response (AMSCMDSF_RESPONSE) 290 | Response: 1, 291 | /** (AMSCMDSF_NORETURN) */ 292 | NoReturn: 2, 293 | /** AdsCommand */ 294 | AdsCommand: 4, 295 | /** Internal generated cmds (AMSCMDSF_SYSCMD) */ 296 | SysCommand: 8, 297 | /** High Priority (R0 to R0 checked at task begin, AMSCMDSF_HIGHPRIO) */ 298 | HighPriority: 16, // 0x0010 299 | /** (cbData um 8 Byte vergrößert, AMSCMDSF_TIMESTAMPADDED) */ 300 | TimeStampAdded: 32, // 0x0020 301 | /** (UDP instead of TCP, AMSCMDSF_UDP) */ 302 | Udp: 64, // 0x0040 303 | /** (command during init phase of TwinCAT, AMSCMDSF_INITCMD) */ 304 | InitCmd: 128, // 0x0080 305 | /** (AMSCMDSF_BROADCAST) */ 306 | Broadcast: 32768, // 0x8000 307 | 308 | /** Returns the flags as comma separated list by given flag value (number) */ 309 | toString: function (value: number): string { 310 | const flags: Array = [] 311 | 312 | for (const key of Object.keys(this)) { 313 | const typedKey = key as keyof typeof ADS_STATE_FLAGS 314 | 315 | if (typeof this[typedKey] !== 'number') 316 | continue 317 | 318 | if ((value & this[typedKey] as number) === this[typedKey]) 319 | flags.push(key) 320 | } 321 | 322 | //Specials: For helping debugging 323 | if (!flags.includes('Udp')) 324 | flags.push('Tcp') 325 | 326 | if (!flags.includes('Response')) 327 | flags.push('Request') 328 | 329 | return flags.join(', ') 330 | } 331 | } 332 | 333 | 334 | /** 335 | * ADS error code 336 | * 337 | * Source: Beckhoff InfoSys 338 | */ 339 | export const ADS_ERROR = { 340 | 0: 'No error', 341 | 1: 'Internal error', 342 | 2: 'No Rtime', 343 | 3: 'Allocation locked memory error', 344 | 4: 'Insert mailbox error', 345 | 5: 'Wrong receive HMSG', 346 | 6: 'Target port not found', 347 | 7: 'Target machine not found', 348 | 8: 'Unknown command ID', 349 | 9: 'Bad task ID', 350 | 10: 'No IO', 351 | 11: 'Unknown ADS command', 352 | 12: 'Win 32 error', 353 | 13: 'Port not connected', 354 | 14: 'Invalid ADS length', 355 | 15: 'Invalid AMS Net ID', 356 | 16: 'Low Installation level', 357 | 17: 'No debug available', 358 | 18: 'Port disabled', 359 | 19: 'Port already connected', 360 | 20: 'ADS Sync Win32 error', 361 | 21: 'ADS Sync Timeout', 362 | 22: 'ADS Sync AMS error', 363 | 23: 'ADS Sync no index map', 364 | 24: 'Invalid ADS port', 365 | 25: 'No memory', 366 | 26: 'TCP send error', 367 | 27: 'Host unreachable', 368 | 28: 'Invalid AMS fragment', 369 | 1280: 'No locked memory can be allocated', 370 | 1281: 'The size of the router memory could not be changed', 371 | 1282: 'The mailbox has reached the maximum number of possible messages. The current sent message was rejected', 372 | 1283: 'The mailbox has reached the maximum number of possible messages.', 373 | 1284: 'Unknown port type', 374 | 1285: 'Router is not initialized', 375 | 1286: 'The desired port number is already assigned', 376 | 1287: 'Port not registered', 377 | 1288: 'The maximum number of Ports reached', 378 | 1289: 'Invalid port', 379 | 1290: 'TwinCAT Router not active', 380 | 1792: 'General device error', 381 | 1793: 'Service is not supported by server', 382 | 1794: 'Invalid index group', 383 | 1795: 'Invalid index offset', 384 | 1796: 'Reading/writing not permitted', 385 | 1797: 'Parameter size not correct', 386 | 1798: 'Invalid parameter value(s)', 387 | 1799: 'Device is not in a ready state', 388 | 1800: 'Device is busy', 389 | 1801: 'Invalid context (must be in Windows)', 390 | 1802: 'Out of memory', 391 | 1803: 'Invalid parameter value(s)', 392 | 1804: 'Not found (files, ...)', 393 | 1805: 'Syntax error in command or file', 394 | 1806: 'Objects do not match', 395 | 1807: 'Object already exists', 396 | 1808: 'Symbol not found', 397 | 1809: 'Symbol version invalid', 398 | 1810: 'Server is in invalid state', 399 | 1811: 'AdsTransMode not supported', 400 | 1812: 'Notification handle is invalid', 401 | 1813: 'Notification client not registered', 402 | 1814: 'No more notification handles', 403 | 1815: 'Size for watch too big', 404 | 1816: 'Device not initialized', 405 | 1817: 'Device has a timeout', 406 | 1818: 'Query interface failed', 407 | 1819: 'Wrong interface required', 408 | 1820: 'Class ID is invalid', 409 | 1821: 'Object ID is invalid', 410 | 1822: 'Request is pending', 411 | 1823: 'Request is aborted', 412 | 1824: 'Signal warning', 413 | 1825: 'Invalid array index', 414 | 1826: 'Symbol not active', 415 | 1827: 'Access denied', 416 | 1828: 'Missing license', 417 | 1829: 'License expired', 418 | 1830: 'License exceeded', 419 | 1831: 'License invalid', 420 | 1832: 'License invalid system id', 421 | 1833: 'License not time limited', 422 | 1834: 'License issue time in the future', 423 | 1835: 'License time period to long', 424 | 1836: 'Exception occured during system start', 425 | 1837: 'License file read twice', 426 | 1838: 'Invalid signature', 427 | 1839: 'Public key certificate', 428 | 1856: 'Error class ', 429 | 1857: 'Invalid parameter at service', 430 | 1858: 'Polling list is empty', 431 | 1859: 'Var connection already in use', 432 | 1860: 'Invoke ID in use', 433 | 1861: 'Timeout elapsed', 434 | 1862: 'Error in win32 subsystem', 435 | 1863: 'Invalid client timeout value', 436 | 1864: 'Ads-port not opened', 437 | 1872: 'Internal error in ads sync', 438 | 1873: 'Hash table overflow', 439 | 1874: 'Key not found in hash', 440 | 1875: 'No more symbols in cache', 441 | 1876: 'Invalid response received', 442 | 1877: 'Sync port is locked', 443 | 4096: 'Internal fatal error in the TwinCAT real-time system', 444 | 4097: 'Timer value not vaild', 445 | 4098: 'Task pointer has the invalid value ZERO', 446 | 4099: 'Task stack pointer has the invalid value ZERO', 447 | 4100: 'The demand task priority is already assigned', 448 | 4101: 'No more free TCB (Task Control Block) available. Maximum number of TCBs is 64', 449 | 4102: 'No more free semaphores available. Maximum number of semaphores is 64', 450 | 4103: 'No more free queue available. Maximum number of queue is 64', 451 | 4109: 'An external synchronization interrupt is already applied', 452 | 4110: 'No external synchronization interrupt applied', 453 | 4111: 'The apply of the external synchronization interrupt failed', 454 | 4112: 'Call of a service function in the wrong context', 455 | 4119: 'Intel VT-x extension is not supported', 456 | 4120: 'Intel VT-x extension is not enabled in system BIOS', 457 | 4121: 'Missing function in Intel VT-x extension', 458 | 4122: 'Enabling Intel VT-x fails', 459 | } 460 | 461 | /** 462 | * ADS notification transmission mode 463 | * 464 | * Source: TwinCAT.Ads.dll By Beckhoff 465 | */ 466 | export const ADS_TRANS_MODE = { 467 | None: 0, 468 | ClientCycle: 1, 469 | ClientOnChange: 2, 470 | Cyclic: 3, 471 | OnChange: 4, 472 | CyclicInContext: 5, 473 | OnChangeInContext: 6, 474 | 475 | /** Returns the corresponding key as string by given value (number) */ 476 | toString: function (value: number): string { 477 | const val = Object.keys(this).find((key) => this[key as keyof typeof ADS_TRANS_MODE] == value) 478 | 479 | return (val !== undefined ? val : 'UNKNOWN') 480 | } 481 | } 482 | 483 | /** 484 | * ADS state 485 | * 486 | * Source: TwinCAT.Ads.dll By Beckhoff 487 | */ 488 | export const ADS_STATE = { 489 | Invalid: 0, 490 | Idle: 1, 491 | Reset: 2, 492 | Initialize: 3, 493 | Start: 4, 494 | Run: 5, 495 | Stop: 6, 496 | SaveConfig: 7, 497 | LoadConfig: 8, 498 | PowerFailure: 9, 499 | PowerGood: 10, 500 | Error: 11, 501 | Shutdown: 12, 502 | Susped: 13, 503 | Resume: 14, 504 | Config: 15, 505 | Reconfig: 16, 506 | Stopping: 17, 507 | Incompatible: 18, 508 | Exception: 19, 509 | 510 | /** Returns the corresponding key as string by given value (number) */ 511 | toString: function (value: number): string { 512 | const val = Object.keys(this).find((key) => this[key as keyof typeof ADS_STATE] == value) 513 | 514 | return (val !== undefined ? val : 'UNKNOWN') 515 | } 516 | } 517 | 518 | 519 | /** 520 | * Reserved ADS index groups 521 | * 522 | * Source: TwinCAT.Ads.dll By Beckhoff 523 | */ 524 | export const ADS_RESERVED_INDEX_GROUPS = { 525 | 526 | /** PlcRWIB (0x4000, 16384) */ 527 | PlcRWIB: 16384, // 0x00004000 528 | /** PlcRWOB (0x4010, 16400) */ 529 | PlcRWOB: 16400, // 0x00004010 530 | /** PlcRWMB (0x4020, 16416) */ 531 | PlcRWMB: 16416, // 0x00004020 532 | /** PlcRWRB (0x4030, 16432) */ 533 | PlcRWRB: 16432, // 0x00004030 534 | /** PlcRWDB (0x4040,16448) */ 535 | PlcRWDB: 16448, // 0x00004040 536 | /** SymbolTable (0xF000, 61440) */ 537 | SymbolTable: 61440, // 0x0000F000 538 | /** SymbolName (0xF001, 61441) */ 539 | SymbolName: 61441, // 0x0000F001 540 | /** SymbolValue (0xF002, 61442) */ 541 | SymbolValue: 61442, // 0x0000F002 542 | /** SymbolHandleByName (0xF003, 61443) */ 543 | SymbolHandleByName: 61443, // 0x0000F003 544 | /** SymbolValueByName (0xF004, 61444) */ 545 | SymbolValueByName: 61444, // 0x0000F004 546 | /** SymbolValueByHandle (0xF005, 61445) */ 547 | SymbolValueByHandle: 61445, // 0x0000F005 548 | /** SymbolReleaseHandle (0xF006, 61446) */ 549 | SymbolReleaseHandle: 61446, // 0x0000F006 550 | /** SymbolInfoByName (0xF007, 61447) */ 551 | SymbolInfoByName: 61447, // 0x0000F007 552 | /** SymbolVersion (0xF008, 61448) */ 553 | SymbolVersion: 61448, // 0x0000F008 554 | /** SymbolInfoByNameEx (0xF009, 61449) */ 555 | SymbolInfoByNameEx: 61449, // 0x0000F009 556 | /** SymbolDownload (F00A, 61450) */ 557 | SymbolDownload: 61450, // 0x0000F00A 558 | /** SymbolUpload (F00B, 61451) */ 559 | SymbolUpload: 61451, // 0x0000F00B 560 | /** SymbolUploadInfo (0xF00C, 61452) */ 561 | SymbolUploadInfo: 61452, // 0x0000F00C 562 | /** SymbolDownload2 */ 563 | SymbolDownload2: 0xF00D, //Added, not from .dll 564 | /** SymbolDataTypeUpload */ 565 | SymbolDataTypeUpload: 0xF00E, //Added, not from .dll 566 | /** SymbolUploadInfo2 */ 567 | SymbolUploadInfo2: 0xF00F, //Added, not from .dll - 24 bytes of info, uploadinfo3 would contain 64 bytes 568 | /** Notification of named handle (0xF010, 61456) */ 569 | SymbolNote: 61456, // 0x0000F010 570 | /** DataDataTypeInfoByNameEx */ 571 | DataDataTypeInfoByNameEx: 0xF011, //Added, not from .dll 572 | /** read/write input byte(s) (0xF020, 61472) */ 573 | IOImageRWIB: 61472, // 0x0000F020 574 | /** read/write input bit (0xF021, 61473) */ 575 | IOImageRWIX: 61473, // 0x0000F021 576 | /** read/write output byte(s) (0xF030, 61488) */ 577 | IOImageRWOB: 61488, // 0x0000F030 578 | /** read/write output bit (0xF031, 61489) */ 579 | IOImageRWOX: 61489, // 0x0000F031 580 | /** write inputs to null (0xF040, 61504) */ 581 | IOImageClearI: 61504, // 0x0000F040 582 | /** write outputs to null (0xF050, 61520) */ 583 | IOImageClearO: 61520, // 0x0000F050 584 | /** ADS Sum Read Command (ADSIGRP_SUMUP_READ, 0xF080, 61568) */ 585 | SumCommandRead: 61568, // 0x0000F080 586 | /** ADS Sum Write Command (ADSIGRP_SUMUP_WRITE, 0xF081, 61569) */ 587 | SumCommandWrite: 61569, // 0x0000F081 588 | /** ADS sum Read/Write command (ADSIGRP_SUMUP_READWRITE, 0xF082, 61570) */ 589 | SumCommandReadWrite: 61570, // 0x0000F082 590 | /** ADS sum ReadEx command (ADSIGRP_SUMUP_READEX, 0xF083, 61571) */ 591 | /** AdsRW IOffs list size */ 592 | /** W: {list of IGrp, IOffs, Length} */ 593 | /** R: {list of results, Length} followed by {list of data (expepted lengths)} */ 594 | SumCommandReadEx: 61571, // 0x0000F083 595 | /** ADS sum ReadEx2 command (ADSIGRP_SUMUP_READEX2, 0xF084, 61572) */ 596 | /** AdsRW IOffs list size */ 597 | /** W: {list of IGrp, IOffs, Length} */ 598 | /** R: {list of results, Length} followed by {list of data (returned lengths)} */ 599 | SumCommandReadEx2: 61572, // 0x0000F084 600 | /** ADS sum AddDevNote command (ADSIGRP_SUMUP_ADDDEVNOTE, 0xF085, 61573) */ 601 | /** AdsRW IOffs list size */ 602 | /** W: {list of IGrp, IOffs, Attrib} */ 603 | /** R: {list of results, handles} */ 604 | SumCommandAddDevNote: 61573, // 0x0000F085 605 | /** ADS sum DelDevNot command (ADSIGRP_SUMUP_DELDEVNOTE, 0xF086, 61574) */ 606 | /** AdsRW IOffs list size */ 607 | /** W: {list of handles} */ 608 | /** R: {list of results} */ 609 | SumCommandDelDevNote: 61574, // 0x0000F086 610 | /** DeviceData (0xF100,61696) */ 611 | DeviceData: 61696, // 0x0000F100 612 | 613 | 614 | /** Returns the corresponding key as string by given value (number) */ 615 | toString: function (value: number): string { 616 | const val = Object.keys(this).find((key) => this[key as keyof typeof ADS_RESERVED_INDEX_GROUPS] == value) 617 | 618 | return (val !== undefined ? val : 'UNKNOWN') 619 | } 620 | } 621 | 622 | 623 | 624 | 625 | 626 | /** 627 | * ADS symbol flags 628 | * 629 | * Source: TwinCAT.Ads.dll By Beckhoff 630 | */ 631 | export const ADS_SYMBOL_FLAGS = { 632 | //None 633 | None: 0, 634 | /** ADSSYMBOLFLAG_PERSISTENT */ 635 | Persistent: 1, 636 | /** ADSSYMBOLFLAG_BITVALUE */ 637 | BitValue: 2, 638 | /** ADSSYMBOLFLAG_REFERENCETO */ 639 | ReferenceTo: 4, 640 | /** ADSSYMBOLFLAG_TYPEGUID */ 641 | TypeGuid: 8, 642 | /** ADSSYMBOLFLAG_TCCOMIFACEPTR */ 643 | TComInterfacePtr: 16, // 0x0010 644 | /** ADSSYMBOLFLAG_READONLY */ 645 | ReadOnly: 32, // 0x0020 646 | /** ADSSYMBOLFLAG_ITFMETHODACCESS */ 647 | ItfMethodAccess: 64, // 0x0040 648 | /** ADSSYMBOLFLAG_METHODDEREF */ 649 | MethodDeref: 128, // 0x0080 650 | /** ADSSYMBOLFLAG_CONTEXTMASK (4 Bit) */ 651 | ContextMask: 3840, // 0x0F00 652 | /** ADSSYMBOLFLAG_ATTRIBUTES */ 653 | Attributes: 4096, // 0x1000 654 | /** Symbol is static (ADSSYMBOLFLAG_STATIC,0x2000) */ 655 | Static: 8192, // 0x2000 656 | /** Persistent data will not restored after reset (cold, ADSSYMBOLFLAG_INITONRESET 0x4000) */ 657 | InitOnReset: 16384, // 0x4000 658 | /** Extended Flags in symbol (ADSSYMBOLFLAG_EXTENDEDFLAGS,0x8000) */ 659 | ExtendedFlags: 32768, // 0x8000 660 | 661 | /** Return given flag value as string array */ 662 | toStringArray: function (flags: number): string[] { 663 | const flagsArr: Array = [] 664 | 665 | for (const key of Object.keys(this)) { 666 | const typedKey = key as keyof typeof ADS_SYMBOL_FLAGS 667 | 668 | if (typeof this[typedKey] !== 'number') 669 | continue 670 | 671 | //Check if flag is available 672 | if ((flags & this[typedKey] as number) === this[typedKey]) { 673 | if (flags === 0 || this[typedKey] !== 0) flagsArr.push(key) 674 | } 675 | } 676 | 677 | return flagsArr 678 | } 679 | } 680 | 681 | 682 | 683 | /** 684 | * ADS data type flags 685 | * 686 | * Source: TwinCAT.Ads.dll By Beckhoff 687 | */ 688 | export const ADS_DATA_TYPE_FLAGS = { 689 | /** ADSDATATYPEFLAG_DATATYPE */ 690 | DataType: 1, 691 | /** ADSDATATYPEFLAG_DATAITEM */ 692 | DataItem: 2, 693 | /** ADSDATATYPEFLAG_REFERENCETO */ 694 | ReferenceTo: 4, 695 | /** ADSDATATYPEFLAG_METHODDEREF */ 696 | MethodDeref: 8, 697 | /** ADSDATATYPEFLAG_OVERSAMPLE */ 698 | Oversample: 16, // 0x00000010 699 | /** ADSDATATYPEFLAG_BITVALUES */ 700 | BitValues: 32, // 0x00000020 701 | /** ADSDATATYPEFLAG_PROPITEM */ 702 | PropItem: 64, // 0x00000040 703 | /** ADSDATATYPEFLAG_TYPEGUID */ 704 | TypeGuid: 128, // 0x00000080 705 | /** ADSDATATYPEFLAG_PERSISTENT */ 706 | Persistent: 256, // 0x00000100 707 | /** ADSDATATYPEFLAG_COPYMASK */ 708 | CopyMask: 512, // 0x00000200 709 | /** ADSDATATYPEFLAG_TCCOMIFACEPTR */ 710 | TComInterfacePtr: 1024, // 0x00000400 711 | /** ADSDATATYPEFLAG_METHODINFOS */ 712 | MethodInfos: 2048, // 0x00000800 713 | /** ADSDATATYPEFLAG_ATTRIBUTES */ 714 | Attributes: 4096, // 0x00001000 715 | /** ADSDATATYPEFLAG_ENUMINFOS */ 716 | EnumInfos: 8192, // 0x00002000 717 | /** this flag is set if the datatype is aligned (ADSDATATYPEFLAG_ALIGNED) */ 718 | Aligned: 65536, // 0x00010000 719 | /** data item is static - do not use offs (ADSDATATYPEFLAG_STATIC) */ 720 | Static: 131072, // 0x00020000 721 | /** means "ContainSpLevelss" for DATATYPES and "HasSpLevels" for DATAITEMS (ADSDATATYPEFLAG_SPLEVELS) */ 722 | SpLevels: 262144, // 0x00040000 723 | /** do not restore persistent data (ADSDATATYPEFLAG_IGNOREPERSIST) */ 724 | IgnorePersist: 524288, // 0x00080000 725 | /** Any size array (ADSDATATYPEFLAG_ANYSIZEARRAY) */ 726 | AnySizeArray: 1048576, // 0x00100000 727 | /** data type used for persistent variables -> should be saved with persistent data (ADSDATATYPEFLAG_PERSIST_DT,0x00200000) */ 728 | PersistantDatatype: 2097152, // 0x00200000 729 | /** Persistent data will not restored after reset (cold) (ADSDATATYPEFLAG_INITONRESET,0x00400000) */ 730 | InitOnResult: 4194304, // 0x00400000 731 | /** None / No Flag set */ 732 | None: 0, 733 | 734 | /** Return given flag value as string array */ 735 | toStringArray: function (flags: number): string[] { 736 | const flagsArr: Array = [] 737 | 738 | for (const key of Object.keys(this)) { 739 | const typedKey = key as keyof typeof ADS_DATA_TYPE_FLAGS 740 | 741 | if (typeof this[typedKey] !== 'number') 742 | continue 743 | 744 | //Check if flag is available 745 | if ((flags & this[typedKey] as number) === this[typedKey]) { 746 | if (flags === 0 || this[typedKey] !== 0) flagsArr.push(key) 747 | } 748 | } 749 | 750 | return flagsArr 751 | } 752 | } 753 | 754 | 755 | /** 756 | * ADS data types 757 | * 758 | * Source: TwinCAT.Ads.dll By Beckhoff 759 | */ 760 | export const ADS_DATA_TYPES = { 761 | /** Empty Type*/ 762 | ADST_VOID: 0, 763 | /**Integer 16 Bit*/ 764 | ADST_INT16: 2, 765 | /**Integer 32 Bit*/ 766 | ADST_INT32: 3, 767 | /**Real (32 Bit)*/ 768 | ADST_REAL32: 4, 769 | /**Real 64 Bit*/ 770 | ADST_REAL64: 5, 771 | /**Integer 8 Bit*/ 772 | ADST_INT8: 16, // 0x00000010 773 | /**Unsigned integer 8 Bit*/ 774 | ADST_UINT8: 17, // 0x00000011 775 | /**Unsigned integer 16 Bit*/ 776 | ADST_UINT16: 18, // 0x00000012 777 | /**Unsigned Integer 32 Bit*/ 778 | ADST_UINT32: 19, // 0x00000013 779 | /**LONG Integer 64 Bit*/ 780 | ADST_INT64: 20, // 0x00000014 781 | /**Unsigned Long integer 64 Bit*/ 782 | ADST_UINT64: 21, // 0x00000015 783 | /**STRING*/ 784 | ADST_STRING: 30, // 0x0000001E 785 | /**WSTRING*/ 786 | ADST_WSTRING: 31, // 0x0000001F 787 | /**ADS REAL80*/ 788 | ADST_REAL80: 32, // 0x00000020 789 | /**ADS BIT*/ 790 | ADST_BIT: 33, // 0x00000021 791 | /**Internal Only*/ 792 | ADST_MAXTYPES: 34, // 0x00000022 793 | /**Blob*/ 794 | ADST_BIGTYPE: 65, // 0x00000041 795 | 796 | /** Returns the corresponding key as string by given value (number) */ 797 | toString: function (value: number): string { 798 | const val = Object.keys(this).find((key) => this[key as keyof typeof ADS_DATA_TYPES] == value) 799 | 800 | return (val !== undefined ? val : 'UNKNOWN') 801 | } 802 | } 803 | 804 | 805 | 806 | /** 807 | * ADS RCP method parameter flags 808 | * 809 | * Source: TwinCAT.Ads.dll By Beckhoff 810 | */ 811 | export const RCP_METHOD_PARAM_FLAGS = { 812 | /** Input Parameter (ADSMETHODPARAFLAG_IN) */ 813 | In: 1, 814 | /** Output Parameter (ADSMETHODPARAFLAG_OUT) */ 815 | Out: 2, 816 | /** By reference Parameter (ADSMETHODPARAFLAG_BYREFERENCE) */ 817 | ByReference: 4, 818 | /** Mask for In parameters. */ 819 | MaskIn: 5, 820 | /** Mask for Out parameters. */ 821 | MaskOut: 6, 822 | 823 | /** Return given flag value as string array */ 824 | toStringArray: function (flags: number): string[] { 825 | const flagsArr: Array = [] 826 | 827 | for (const key of Object.keys(this)) { 828 | const typedKey = key as keyof typeof RCP_METHOD_PARAM_FLAGS 829 | 830 | if (typeof this[typedKey] !== 'number') 831 | continue 832 | 833 | //Check if flag is available 834 | if ((flags & this[typedKey] as number) === this[typedKey]) { 835 | if (flags === 0 || this[typedKey] !== 0) flagsArr.push(key) 836 | } 837 | } 838 | 839 | return flagsArr 840 | } 841 | } 842 | 843 | 844 | 845 | /** 846 | * AMS router state 847 | */ 848 | export const AMS_ROUTER_STATE = { 849 | /** Router is stopped */ 850 | STOP: 0, 851 | /** Router is started */ 852 | START: 1, 853 | /** Router is remove (unavailable?) */ 854 | REMOVED: 2, 855 | 856 | /** Returns the corresponding key as string by given value (number) */ 857 | toString: function (value: number): string { 858 | const val = Object.keys(this).find((key) => this[key as keyof typeof AMS_ROUTER_STATE] == value) 859 | 860 | return (val !== undefined ? val : 'UNKNOWN') 861 | } 862 | } -------------------------------------------------------------------------------- /src/ads-server-core.ts: -------------------------------------------------------------------------------- 1 | /* 2 | https://github.com/jisotalo/ads-server 3 | 4 | Copyright (c) 2021 Jussi Isotalo 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | import { 26 | EventEmitter 27 | 28 | } from 'events' 29 | import { 30 | Socket 31 | } from 'net' 32 | 33 | import long from 'long' 34 | import Debug from 'debug' 35 | import iconv from 'iconv-lite' 36 | 37 | import { 38 | AddNotificationReq, 39 | AddNotificationReqCallback, 40 | AddNotificationReqResponse, 41 | AdsNotificationTarget, 42 | AdsRequest, 43 | BaseResponse, 44 | DeleteNotificationReq, 45 | DeleteNotificationReqCallback, 46 | GenericReqCallback, 47 | ReadDeviceInfoReqCallback, 48 | ReadDeviceInfoReqResponse, 49 | ReadReq, 50 | ReadReqCallback, 51 | ReadReqResponse, 52 | ReadStateReqCallback, 53 | ReadStateReqResponse, 54 | ReadWriteReq, 55 | ReadWriteReqCallback, 56 | ReadWriteReqResponse, 57 | ServerConnection, 58 | ServerCoreSettings, 59 | StandAloneAdsNotificationTarget, 60 | UnknownAdsRequest, 61 | WriteControlReq, 62 | WriteControlReqCallback, 63 | WriteReq, 64 | WriteReqCallback 65 | } from './types/ads-server' 66 | 67 | import { 68 | AdsCommandToSend, 69 | AmsHeader, 70 | AmsTcpHeader, 71 | AmsTcpPacket 72 | } from './types/ads-types' 73 | 74 | import * as ADS from './ads-commons' 75 | 76 | /** 77 | * Base abstract class for ADS server 78 | */ 79 | export abstract class ServerCore extends EventEmitter { 80 | 81 | protected debug = Debug('ads-server') 82 | protected debugD = Debug(`ads-server:details`) 83 | protected debugIO = Debug(`ads-server:raw-data`) 84 | 85 | /** 86 | * Active settings 87 | */ 88 | public settings: ServerCoreSettings = { 89 | localAmsNetId: '', 90 | hideConsoleWarnings: false, 91 | } 92 | 93 | /** 94 | * Active connection information 95 | */ 96 | public connection: ServerConnection = { 97 | connected: false, 98 | localAmsNetId: '' 99 | } 100 | 101 | /** 102 | * Active debug level 103 | * - 0 = no debugging 104 | * - 1 = Extended exception stack trace 105 | * - 2 = basic debugging (same as $env:DEBUG='ads-server') 106 | * - 3 = detailed debugging (same as $env:DEBUG='ads-server,ads-server:details') 107 | * - 4 = full debugging (same as $env:DEBUG='ads-server,ads-server:details,ads-server:raw-data') 108 | */ 109 | public debugLevel = 0 110 | 111 | /** 112 | * Next invoke ID to use (for notifications) 113 | */ 114 | protected nextInvokeId = 0 115 | 116 | /** 117 | * Callback used for ams/tcp commands (like port register) 118 | */ 119 | protected amsTcpCallback?: ((packet: AmsTcpPacket) => void) 120 | 121 | /** 122 | * Callback for AMS router state change 123 | * Used only with ads-server-router 124 | */ 125 | protected routerStateChangedCallback?: (data: AmsTcpPacket) => void 126 | 127 | /** 128 | * Callbacks for ADS command requests 129 | */ 130 | protected requestCallbacks: { 131 | [key: string]: GenericReqCallback 132 | } = {} 133 | 134 | /** 135 | * Settings to use are provided as parameter 136 | */ 137 | constructor(settings: Partial) { 138 | super() 139 | 140 | //Taking the default settings and then updating the provided ones 141 | this.settings = { 142 | ...this.settings, 143 | ...settings 144 | } 145 | } 146 | 147 | /** 148 | * Sets debugging using debug package on/off. 149 | * Another way for environment variable DEBUG: 150 | * - 0 = no debugging 151 | * - 1 = Extended exception stack trace 152 | * - 2 = basic debugging (same as $env:DEBUG='ads-server') 153 | * - 3 = detailed debugging (same as $env:DEBUG='ads-server,ads-server:details') 154 | * - 4 = full debugging (same as $env:DEBUG='ads-server,ads-server:details,ads-server:raw-data') 155 | * 156 | * @param level 0 = none, 1 = extended stack traces, 2 = basic, 3 = detailed, 4 = detailed + raw data 157 | */ 158 | setDebugging(level: number): void { 159 | this.debug(`setDebugging(): Setting debug level to ${level}`) 160 | 161 | this.debugLevel = level 162 | 163 | this.debug.enabled = level >= 2 164 | this.debugD.enabled = level >= 3 165 | this.debugIO.enabled = level >= 4 166 | 167 | this.debug(`setDebugging(): Debug level set to ${level}`) 168 | } 169 | 170 | /** 171 | * Checks received data buffer for full AMS packets. If full packet is found, it is parsed and handled. 172 | * Calls itself recursively if multiple packets available. Added also setImmediate calls to prevent event loop from blocking 173 | * 174 | * @param buffer Data buffer to check 175 | * @param socket Socket connection to use for responding 176 | * @param setNewBufferCallback Callback that is called to update current data buffer contents 177 | */ 178 | protected handleReceivedData(buffer: Buffer, socket: Socket, setNewBufferCallback: (newBuffer: Buffer) => void): void { 179 | //If we haven't enough data to determine packet size, quit 180 | if (buffer.byteLength < ADS.AMS_TCP_HEADER_LENGTH) 181 | return 182 | 183 | //There should be an AMS packet, so the packet size is available in the bytes 2..5 184 | const packetLength = buffer.readUInt32LE(2) + ADS.AMS_TCP_HEADER_LENGTH 185 | 186 | //Not enough data yet? quit 187 | if (buffer.byteLength < packetLength) 188 | return 189 | 190 | const data = Buffer.from(buffer.slice(0, packetLength)) 191 | buffer = buffer.slice(data.byteLength) 192 | 193 | setNewBufferCallback(buffer) 194 | 195 | //Parse the packet, but allow time for the event loop 196 | setImmediate(this.parseAmsTcpPacket.bind(this, data, socket)) 197 | 198 | //If there is more, call recursively but allow time for the event loop 199 | if (buffer.byteLength >= ADS.AMS_TCP_HEADER_LENGTH) { 200 | setImmediate(this.handleReceivedData.bind(this, buffer, socket, setNewBufferCallback)) 201 | } 202 | } 203 | 204 | /** 205 | * Parses an AMS/TCP packet from given buffer and then handles it 206 | * 207 | * @param data Buffer that contains data for a single full AMS/TCP packet 208 | * @param socket Socket connection to use for responding 209 | */ 210 | protected async parseAmsTcpPacket(data: Buffer, socket: Socket): Promise { 211 | const packet = {} as AmsTcpPacket 212 | 213 | //1. Parse AMS/TCP header 214 | const parsedAmsTcpHeader = this.parseAmsTcpHeader(data) 215 | packet.amsTcp = parsedAmsTcpHeader.amsTcp 216 | data = parsedAmsTcpHeader.data 217 | 218 | //2. Parse AMS header (if exists) 219 | const parsedAmsHeader = this.parseAmsHeader(data) 220 | packet.ams = parsedAmsHeader.ams 221 | data = parsedAmsHeader.data 222 | 223 | //3. Parse ADS data (if exists) 224 | packet.ads = (packet.ams.error ? { rawData: Buffer.alloc(0) } : this.parseAdsData(packet, data)) 225 | 226 | //4. Handle the parsed packet 227 | this.onAmsTcpPacketReceived(packet, socket) 228 | } 229 | 230 | /** 231 | * Parses an AMS/TCP header from given buffer 232 | * 233 | * @param data Buffer that contains data for a single full AMS/TCP packet 234 | * @returns Object `{amsTcp, data}`, where amsTcp is the parsed header and data is rest of the data 235 | */ 236 | protected parseAmsTcpHeader(data: Buffer): { amsTcp: AmsTcpHeader, data: Buffer } { 237 | this.debugD(`parseAmsTcpHeader(): Starting to parse AMS/TCP header`) 238 | 239 | let pos = 0 240 | const amsTcp = {} as AmsTcpHeader 241 | 242 | //0..1 AMS command (header flag) 243 | amsTcp.command = data.readUInt16LE(pos) 244 | amsTcp.commandStr = ADS.AMS_HEADER_FLAG.toString(amsTcp.command) 245 | pos += 2 246 | 247 | //2..5 Data length 248 | amsTcp.dataLength = data.readUInt32LE(pos) 249 | pos += 4 250 | 251 | //Remove AMS/TCP header from data 252 | data = data.slice(ADS.AMS_TCP_HEADER_LENGTH) 253 | 254 | //If data length is less than AMS_HEADER_LENGTH, 255 | //we know that this packet has no AMS headers -> it's only a AMS/TCP command 256 | if (data.byteLength < ADS.AMS_HEADER_LENGTH) { 257 | amsTcp.data = data 258 | 259 | //Remove data (basically creates an empty buffer..) 260 | data = data.slice(data.byteLength) 261 | } 262 | 263 | this.debugD(`parseAmsTcpHeader(): AMS/TCP header parsed: %o`, amsTcp) 264 | 265 | return { amsTcp, data } 266 | } 267 | 268 | /** 269 | * Parses an AMS header from given buffer 270 | * 271 | * @param data Buffer that contains data for a single AMS packet (without AMS/TCP header) 272 | * @returns Object `{ams, data}`, where ams is the parsed AMS header and data is rest of the data 273 | */ 274 | protected parseAmsHeader(data: Buffer): { ams: AmsHeader, data: Buffer } { 275 | this.debugD(`parseAmsHeader(): Starting to parse AMS header`) 276 | 277 | let pos = 0 278 | const ams = {} as AmsHeader 279 | 280 | if (data.byteLength < ADS.AMS_HEADER_LENGTH) { 281 | this.debugD(`parseAmsHeader(): No AMS header found`) 282 | return { ams, data } 283 | } 284 | 285 | //0..5 Target AMSNetId 286 | ams.targetAmsNetId = this.byteArrayToAmsNedIdStr(data.slice(pos, pos + ADS.AMS_NET_ID_LENGTH)) 287 | pos += ADS.AMS_NET_ID_LENGTH 288 | 289 | //6..8 Target ads port 290 | ams.targetAdsPort = data.readUInt16LE(pos) 291 | pos += 2 292 | 293 | //8..13 Source AMSNetId 294 | ams.sourceAmsNetId = this.byteArrayToAmsNedIdStr(data.slice(pos, pos + ADS.AMS_NET_ID_LENGTH)) 295 | pos += ADS.AMS_NET_ID_LENGTH 296 | 297 | //14..15 Source ads port 298 | ams.sourceAdsPort = data.readUInt16LE(pos) 299 | pos += 2 300 | 301 | //16..17 ADS command 302 | ams.adsCommand = data.readUInt16LE(pos) 303 | ams.adsCommandStr = ADS.ADS_COMMAND.toString(ams.adsCommand) 304 | pos += 2 305 | 306 | //18..19 State flags 307 | ams.stateFlags = data.readUInt16LE(pos) 308 | ams.stateFlagsStr = ADS.ADS_STATE_FLAGS.toString(ams.stateFlags) 309 | pos += 2 310 | 311 | //20..23 Data length 312 | ams.dataLength = data.readUInt32LE(pos) 313 | pos += 4 314 | 315 | //24..27 Error code 316 | ams.errorCode = data.readUInt32LE(pos) 317 | pos += 4 318 | 319 | //28..31 Invoke ID 320 | ams.invokeId = data.readUInt32LE(pos) 321 | pos += 4 322 | 323 | //Remove AMS header from data 324 | data = data.slice(ADS.AMS_HEADER_LENGTH) 325 | 326 | //ADS error 327 | ams.error = (ams.errorCode !== null ? ams.errorCode > 0 : false) 328 | ams.errorStr = '' 329 | if (ams.error) { 330 | ams.errorStr = ADS.ADS_ERROR[ams.errorCode as keyof typeof ADS.ADS_ERROR] 331 | } 332 | 333 | this.debugD(`parseAmsHeader(): AMS header parsed: %o`, ams) 334 | 335 | return { ams, data } 336 | } 337 | 338 | /** 339 | * Parses ADS data from given buffer. Uses `packet.ams` to determine the ADS command. 340 | * 341 | * @param data Buffer that contains data for a single ADS packet (without AMS/TCP header and AMS header) 342 | * @returns Object that contains the parsed ADS data 343 | */ 344 | protected parseAdsData(packet: AmsTcpPacket, data: Buffer): AdsRequest { 345 | this.debugD(`parseAdsData(): Starting to parse ADS data`) 346 | 347 | let pos = 0 348 | 349 | if (data.byteLength === 0) { 350 | this.debugD(`parseAdsData(): No ADS data found`) 351 | return { 352 | //TODO 353 | } as AdsRequest 354 | } 355 | 356 | let ads 357 | 358 | switch (packet.ams.adsCommand) { 359 | //-------------- Read Write --------------- 360 | case ADS.ADS_COMMAND.ReadWrite: 361 | ads = {} as ReadWriteReq 362 | 363 | //0..3 364 | ads.indexGroup = data.readUInt32LE(pos) 365 | pos += 4 366 | 367 | //4..7 368 | ads.indexOffset = data.readUInt32LE(pos) 369 | pos += 4 370 | 371 | //8..11 372 | ads.readLength = data.readUInt32LE(pos) 373 | pos += 4 374 | 375 | //8..9 376 | ads.writeLength = data.readUInt32LE(pos) 377 | pos += 4 378 | 379 | //..n Data 380 | ads.data = Buffer.alloc(ads.writeLength) 381 | data.copy(ads.data, 0, pos) 382 | 383 | break 384 | 385 | 386 | 387 | case ADS.ADS_COMMAND.Read: 388 | ads = {} as ReadReq 389 | 390 | //0..3 391 | ads.indexGroup = data.readUInt32LE(pos) 392 | pos += 4 393 | 394 | //4..7 395 | ads.indexOffset = data.readUInt32LE(pos) 396 | pos += 4 397 | 398 | //8..11 399 | ads.readLength = data.readUInt32LE(pos) 400 | pos += 4 401 | break 402 | 403 | 404 | //-------------- Write --------------- 405 | case ADS.ADS_COMMAND.Write: 406 | ads = {} as WriteReq 407 | 408 | //0..3 409 | ads.indexGroup = data.readUInt32LE(pos) 410 | pos += 4 411 | 412 | //4..7 413 | ads.indexOffset = data.readUInt32LE(pos) 414 | pos += 4 415 | 416 | //8..9 417 | ads.writeLength = data.readUInt32LE(pos) 418 | pos += 4 419 | 420 | //..n Data 421 | ads.data = Buffer.alloc(ads.writeLength) 422 | data.copy(ads.data, 0, pos) 423 | break 424 | 425 | 426 | 427 | //-------------- Device info --------------- 428 | case ADS.ADS_COMMAND.ReadDeviceInfo: 429 | //No request payload 430 | ads = {} 431 | 432 | break 433 | 434 | 435 | 436 | 437 | 438 | //-------------- Device status --------------- 439 | case ADS.ADS_COMMAND.ReadState: 440 | //No request payload 441 | ads = {} 442 | 443 | break 444 | 445 | 446 | 447 | 448 | //-------------- Add notification --------------- 449 | case ADS.ADS_COMMAND.AddNotification: 450 | ads = {} as AddNotificationReq 451 | 452 | //0..3 IndexGroup 453 | ads.indexGroup = data.readUInt32LE(pos) 454 | pos += 4 455 | 456 | //4..7 IndexOffset 457 | ads.indexOffset = data.readUInt32LE(pos) 458 | pos += 4 459 | 460 | //8..11 Data length 461 | ads.dataLength = data.readUInt32LE(pos) 462 | pos += 4 463 | 464 | //12..15 Transmission mode 465 | ads.transmissionMode = data.readUInt32LE(pos) 466 | ads.transmissionModeStr = ADS.ADS_TRANS_MODE.toString(ads.transmissionMode) 467 | pos += 4 468 | 469 | //16..19 Maximum delay (ms) - When subscribing, a notification is sent after this time even if no changes 470 | ads.maximumDelay = data.readUInt32LE(pos) / 10000 471 | pos += 4 472 | 473 | //20..23 Cycle time (ms) - How often the PLC checks for value changes (minimum value: Task 0 cycle time) 474 | ads.cycleTime = data.readUInt32LE(pos) / 10000 475 | pos += 4 476 | 477 | //24..40 reserved 478 | ads.reserved = data.slice(pos) 479 | 480 | break 481 | 482 | 483 | 484 | 485 | //-------------- Delete notification --------------- 486 | case ADS.ADS_COMMAND.DeleteNotification: 487 | ads = {} as DeleteNotificationReq 488 | 489 | //0..3 Notification handle 490 | ads.notificationHandle = data.readUInt32LE(pos) 491 | pos += 4 492 | 493 | break 494 | 495 | 496 | 497 | //-------------- Notification --------------- 498 | case ADS.ADS_COMMAND.Notification: 499 | 500 | //Server shouldn't receive this 501 | 502 | break 503 | 504 | 505 | 506 | //-------------- WriteControl --------------- 507 | case ADS.ADS_COMMAND.WriteControl: { 508 | ads = {} as WriteControlReq 509 | 510 | //0..1 ADS state 511 | ads.adsState = data.readUInt16LE(pos) 512 | ads.adsStateStr = ADS.ADS_STATE.toString(ads.adsState) 513 | pos += 2 514 | 515 | //2..3 Device state 516 | ads.deviceState = data.readUInt16LE(pos) 517 | pos += 2 518 | 519 | //4..7 Data length 520 | const dataLen = data.readUInt32LE(pos) 521 | pos += 4 522 | 523 | //7..n Data 524 | ads.data = Buffer.alloc(dataLen) 525 | data.copy(ads.data, 0, pos) 526 | 527 | break 528 | } 529 | } 530 | 531 | this.debugD(`parseAdsData(): ADS data parsed: %o`, ads) 532 | 533 | if (ads) { 534 | return ads 535 | } 536 | 537 | this.debug(`parseAdsData(): Unknown ads command received: ${packet.ams.adsCommand}`) 538 | return { 539 | error: true, 540 | errorStr: `Unknown ADS command for parser: ${packet.ams.adsCommand} (${packet.ams.adsCommandStr})`, 541 | errorCode: -1 542 | } as UnknownAdsRequest 543 | 544 | } 545 | 546 | /** 547 | * Handles the parsed AMS/TCP packet and actions/callbacks etc. related to it. 548 | * 549 | * @param packet Fully parsed AMS/TCP packet, includes AMS/TCP header and if available, also AMS header and ADS data 550 | */ 551 | protected onAmsTcpPacketReceived(packet: AmsTcpPacket, socket: Socket): void { 552 | this.debugD(`onAmsTcpPacketReceived(): A parsed AMS packet received with command ${packet.amsTcp.command}`) 553 | 554 | switch (packet.amsTcp.command) { 555 | //-------------- ADS command --------------- 556 | case ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_AMS_CMD: 557 | packet.amsTcp.commandStr = 'Ads command' 558 | 559 | if (packet.ams.targetAmsNetId === this.connection.localAmsNetId 560 | || packet.ams.targetAmsNetId === ADS.LOOPBACK_AMS_NET_ID) { 561 | 562 | this.onAdsCommandReceived(packet, socket) 563 | } else { 564 | this.debug(`Received ADS command but it's not for us (target: ${packet.ams.targetAmsNetId}:${packet.ams.targetAdsPort})`) 565 | } 566 | 567 | break 568 | 569 | //-------------- AMS/TCP port unregister --------------- 570 | case ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_CLOSE: 571 | packet.amsTcp.commandStr = 'Port unregister' 572 | //TODO: No action at the moment 573 | break 574 | 575 | //-------------- AMS/TCP port register --------------- 576 | case ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_CONNECT: 577 | packet.amsTcp.commandStr = 'Port register' 578 | 579 | //Parse data 580 | if (packet.amsTcp.data instanceof Buffer) { 581 | const data = packet.amsTcp.data as Buffer 582 | 583 | packet.amsTcp.data = { 584 | //0..5 Own AmsNetId 585 | localAmsNetId: this.byteArrayToAmsNedIdStr(data.slice(0, ADS.AMS_NET_ID_LENGTH)), 586 | //5..6 Own assigned ADS port 587 | localAdsPort: data.readUInt16LE(ADS.AMS_NET_ID_LENGTH) 588 | } 589 | 590 | if (this.amsTcpCallback) { 591 | this.amsTcpCallback(packet) 592 | } else { 593 | this.debugD(`onAmsTcpPacketReceived(): Port register response received but no callback was assigned (${packet.amsTcp.commandStr})`) 594 | } 595 | } else { 596 | this.debugD(`onAmsTcpPacketReceived(): amsTcp data is unknown type`) 597 | } 598 | break 599 | 600 | //-------------- AMS router note --------------- 601 | case ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_ROUTER_NOTE: 602 | packet.amsTcp.commandStr = 'Port router note' 603 | 604 | //Parse data 605 | if (packet.amsTcp.data instanceof Buffer) { 606 | const data = packet.amsTcp.data as Buffer 607 | 608 | packet.amsTcp.data = { 609 | //0..3 Router state 610 | routerState: data.readUInt32LE(0) 611 | } 612 | 613 | if (this.routerStateChangedCallback) { 614 | this.routerStateChangedCallback(packet) 615 | } 616 | 617 | } else { 618 | this.debugD(`onAmsTcpPacketReceived(): amsTcp data is unknown type`) 619 | } 620 | break 621 | 622 | //-------------- Get local ams net id response --------------- 623 | case ADS.AMS_HEADER_FLAG.GET_LOCAL_NETID: 624 | packet.amsTcp.commandStr = 'Get local net id' 625 | //TODO: No action at the moment 626 | break 627 | 628 | default: 629 | packet.amsTcp.commandStr = `Unknown AMS/TCP command ${packet.amsTcp.command}` 630 | this.debug(`onAmsTcpPacketReceived(): Unknown AMS/TCP command received: "${packet.amsTcp.command}" - Doing nothing`) 631 | //TODO: No action at the moment 632 | break 633 | } 634 | } 635 | 636 | /** 637 | * Handles received ADS command 638 | * 639 | * @param packet Fully parsed AMS/TCP packet, includes AMS/TCP header, AMS header and ADS data 640 | * @param socket Socket connection to use for responding 641 | */ 642 | protected onAdsCommandReceived(packet: AmsTcpPacket, socket: Socket): void { 643 | this.debugD(`onAdsCommandReceived(): A parsed ADS command received with command ${packet.ams.adsCommand}`) 644 | 645 | //Get callback by ads command 646 | const callback = this.requestCallbacks[packet.ams.adsCommandStr] 647 | 648 | if (!callback) { 649 | //Command received but no callback 650 | this.consoleWrite(`NOTE: ${packet.ams.adsCommandStr} request received from ${packet.ams.sourceAmsNetId}:${packet.ams.sourceAdsPort} to ADS port ${packet.ams.targetAdsPort} but no callback assigned`) 651 | return; 652 | } 653 | 654 | switch (packet.ams.adsCommand) { 655 | //ReadWrite and Read requests 656 | case ADS.ADS_COMMAND.ReadWrite: 657 | case ADS.ADS_COMMAND.Read: 658 | 659 | callback( 660 | packet.ads, 661 | async (response: (ReadReqResponse | ReadWriteReqResponse) = {}) => { 662 | 663 | let buffer = null, pos = 0 664 | 665 | if (response.data != null && Buffer.isBuffer(response.data)) { 666 | buffer = Buffer.alloc(8 + response.data.byteLength) 667 | 668 | //0..3 ADS error 669 | buffer.writeUInt32LE(response.error != null ? response.error : 0, pos) 670 | pos += 4 671 | 672 | //4..7 Data length 673 | buffer.writeUInt32LE(response.data.byteLength, pos) 674 | pos += 4 675 | 676 | //8..n Data 677 | response.data.copy(buffer, pos) 678 | 679 | } 680 | else { 681 | buffer = Buffer.alloc(8) 682 | 683 | //0..3 ADS error 684 | buffer.writeUInt32LE(response.error != null ? response.error : 0, pos) 685 | pos += 4 686 | 687 | //4..7 Data length 688 | buffer.writeUInt32LE(0, pos) 689 | pos += 4 690 | } 691 | 692 | 693 | //Sending the response 694 | await this.sendAdsCommand({ 695 | adsCommand: packet.ams.adsCommand, 696 | targetAmsNetId: packet.ams.sourceAmsNetId, 697 | targetAdsPort: packet.ams.sourceAdsPort, 698 | sourceAmsNetId: packet.ams.targetAmsNetId, 699 | sourceAdsPort: packet.ams.targetAdsPort, 700 | invokeId: packet.ams.invokeId, 701 | rawData: buffer 702 | }, socket) 703 | }, 704 | packet, 705 | packet.ams.targetAdsPort 706 | ) 707 | 708 | break 709 | 710 | //Write request 711 | case ADS.ADS_COMMAND.Write: 712 | 713 | callback( 714 | packet.ads, 715 | async (response: ReadWriteReqResponse = {}) => { 716 | 717 | const buffer = Buffer.alloc(4) 718 | 719 | //0..3 ADS error 720 | buffer.writeUInt32LE(response !== undefined && response.error != null ? response.error : 0, 0) 721 | 722 | //Sending the response 723 | await this.sendAdsCommand({ 724 | adsCommand: packet.ams.adsCommand, 725 | targetAmsNetId: packet.ams.sourceAmsNetId, 726 | targetAdsPort: packet.ams.sourceAdsPort, 727 | sourceAmsNetId: packet.ams.targetAmsNetId, 728 | sourceAdsPort: packet.ams.targetAdsPort, 729 | invokeId: packet.ams.invokeId, 730 | rawData: buffer 731 | }, socket) 732 | }, 733 | packet, 734 | packet.ams.targetAdsPort 735 | ) 736 | 737 | break 738 | 739 | //Device info request 740 | case ADS.ADS_COMMAND.ReadDeviceInfo: 741 | 742 | callback( 743 | packet.ads, 744 | async (response: ReadDeviceInfoReqResponse = {}) => { 745 | 746 | const buffer = Buffer.alloc(24) 747 | let pos = 0 748 | 749 | //0..3 ADS error 750 | buffer.writeUInt32LE(response.error != null ? response.error : 0, pos) 751 | pos += 4 752 | 753 | //4 Major version 754 | buffer.writeUInt8(response.majorVersion != null ? response.majorVersion : 0, pos) 755 | pos += 1 756 | 757 | //5 Minor version 758 | buffer.writeUInt8(response.minorVersion != null ? response.minorVersion : 0, pos) 759 | pos += 1 760 | 761 | //6..7 Version build 762 | buffer.writeUInt16LE(response.versionBuild != null ? response.versionBuild : 0, pos) 763 | pos += 2 764 | 765 | //8..24 Device name 766 | iconv.encode(response.deviceName != null ? response.deviceName : '', 'cp1252').copy(buffer, pos) 767 | 768 | //Sending the response 769 | await this.sendAdsCommand({ 770 | adsCommand: packet.ams.adsCommand, 771 | targetAmsNetId: packet.ams.sourceAmsNetId, 772 | targetAdsPort: packet.ams.sourceAdsPort, 773 | sourceAmsNetId: packet.ams.targetAmsNetId, 774 | sourceAdsPort: packet.ams.targetAdsPort, 775 | invokeId: packet.ams.invokeId, 776 | rawData: buffer 777 | }, socket) 778 | }, 779 | packet, 780 | packet.ams.targetAdsPort 781 | ) 782 | 783 | break 784 | 785 | //Read state request 786 | case ADS.ADS_COMMAND.ReadState: 787 | 788 | callback( 789 | packet.ads, 790 | async (response: ReadStateReqResponse = {}) => { 791 | 792 | const buffer = Buffer.alloc(8) 793 | let pos = 0 794 | 795 | //0..3 ADS error 796 | buffer.writeUInt32LE(response.error != null ? response.error : 0, pos) 797 | pos += 4 798 | 799 | //4..5 ADS state (ADS.ADS_STATE.Invalid = 0) 800 | buffer.writeUInt16LE(response.adsState != null ? response.adsState : ADS.ADS_STATE.Invalid, pos) 801 | pos += 2 802 | 803 | //6..7 Device state 804 | buffer.writeUInt16LE(response.deviceState != null ? response.deviceState : 0, pos) 805 | pos += 2 806 | 807 | //Sending the response 808 | await this.sendAdsCommand({ 809 | adsCommand: packet.ams.adsCommand, 810 | targetAmsNetId: packet.ams.sourceAmsNetId, 811 | targetAdsPort: packet.ams.sourceAdsPort, 812 | sourceAmsNetId: packet.ams.targetAmsNetId, 813 | sourceAdsPort: packet.ams.targetAdsPort, 814 | invokeId: packet.ams.invokeId, 815 | rawData: buffer 816 | }, socket) 817 | }, 818 | packet, 819 | packet.ams.targetAdsPort 820 | ) 821 | 822 | break 823 | 824 | //Add notification request 825 | case ADS.ADS_COMMAND.AddNotification: 826 | 827 | //Let's add a helper object for sending notifications 828 | packet.ads.notificationTarget = { 829 | targetAmsNetId: packet.ams.targetAmsNetId, 830 | targetAdsPort: packet.ams.sourceAdsPort, 831 | sourceAdsPort: packet.ams.targetAdsPort, 832 | socket 833 | } as StandAloneAdsNotificationTarget 834 | 835 | callback( 836 | packet.ads, 837 | async (response: AddNotificationReqResponse = {}) => { 838 | 839 | const buffer = Buffer.alloc(8) 840 | let pos = 0 841 | 842 | //0..3 ADS error 843 | buffer.writeUInt32LE(response.error != null ? response.error : 0, pos) 844 | pos += 4 845 | 846 | //4..7 Notification handle 847 | buffer.writeUInt32LE(response.notificationHandle != null ? response.notificationHandle : 0, pos) 848 | pos += 2 849 | 850 | //Sending the response 851 | await this.sendAdsCommand({ 852 | adsCommand: packet.ams.adsCommand, 853 | targetAmsNetId: packet.ams.sourceAmsNetId, 854 | targetAdsPort: packet.ams.sourceAdsPort, 855 | sourceAmsNetId: packet.ams.targetAmsNetId, 856 | sourceAdsPort: packet.ams.targetAdsPort, 857 | invokeId: packet.ams.invokeId, 858 | rawData: buffer 859 | }, socket) 860 | }, 861 | packet, 862 | packet.ams.targetAdsPort 863 | ) 864 | break 865 | 866 | //Delete notification request 867 | case ADS.ADS_COMMAND.DeleteNotification: 868 | 869 | callback( 870 | packet.ads, 871 | async (response: BaseResponse = {}) => { 872 | 873 | const buffer = Buffer.alloc(4) 874 | let pos = 0 875 | 876 | //0..3 ADS error 877 | buffer.writeUInt32LE(response.error != null ? response.error : 0, pos) 878 | pos += 4 879 | 880 | //Sending the response 881 | await this.sendAdsCommand({ 882 | adsCommand: packet.ams.adsCommand, 883 | targetAmsNetId: packet.ams.sourceAmsNetId, 884 | targetAdsPort: packet.ams.sourceAdsPort, 885 | sourceAmsNetId: packet.ams.targetAmsNetId, 886 | sourceAdsPort: packet.ams.targetAdsPort, 887 | invokeId: packet.ams.invokeId, 888 | rawData: buffer 889 | }, socket) 890 | }, 891 | packet, 892 | packet.ams.targetAdsPort 893 | ) 894 | break 895 | 896 | //Notification received 897 | case ADS.ADS_COMMAND.Notification: 898 | 899 | //No need for this at the moment? 900 | break 901 | 902 | //WriteControl request 903 | case ADS.ADS_COMMAND.WriteControl: 904 | 905 | callback( 906 | packet.ads, 907 | async (response: BaseResponse) => { 908 | 909 | const buffer = Buffer.alloc(4) 910 | 911 | //0..3 ADS error 912 | buffer.writeUInt32LE(response.error ? response.error : 0, 0) 913 | 914 | //Sending the response 915 | await this.sendAdsCommand({ 916 | adsCommand: packet.ams.adsCommand, 917 | targetAmsNetId: packet.ams.sourceAmsNetId, 918 | targetAdsPort: packet.ams.sourceAdsPort, 919 | sourceAmsNetId: packet.ams.targetAmsNetId, 920 | sourceAdsPort: packet.ams.targetAdsPort, 921 | invokeId: packet.ams.invokeId, 922 | rawData: buffer 923 | }, socket) 924 | }, 925 | packet, 926 | packet.ams.targetAdsPort 927 | ) 928 | 929 | break 930 | 931 | default: 932 | //Unknown command 933 | this.debug(`onAdsCommandReceived: Unknown ads command: ${packet.ams.adsCommand}`) 934 | this.consoleWrite(`WARNING: Unknown ADS command ${packet.ams.adsCommand} received from ${packet.ams.sourceAmsNetId}:${packet.ams.sourceAdsPort} to ADS port ${packet.ams.targetAdsPort}`) 935 | break 936 | } 937 | } 938 | 939 | /** 940 | * 941 | * @param data ADS command to send 942 | * @param socket Socket connection to use for responding 943 | */ 944 | protected sendAdsCommand(data: AdsCommandToSend, socket: Socket): Promise { 945 | return new Promise(async (resolve, reject): Promise => { 946 | 947 | //Creating the data packet object 948 | const packet: AmsTcpPacket = { 949 | amsTcp: { 950 | command: ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_AMS_CMD, 951 | commandStr: ADS.AMS_HEADER_FLAG.toString(ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_AMS_CMD), 952 | dataLength: 0, 953 | data: null 954 | }, 955 | ams: { 956 | targetAmsNetId: data.targetAmsNetId, 957 | targetAdsPort: data.targetAdsPort, 958 | sourceAmsNetId: data.sourceAmsNetId, 959 | sourceAdsPort: data.sourceAdsPort, 960 | adsCommand: data.adsCommand, 961 | adsCommandStr: ADS.ADS_COMMAND.toString(data.adsCommand), 962 | stateFlags: ADS.ADS_STATE_FLAGS.Response | ADS.ADS_STATE_FLAGS.AdsCommand, 963 | stateFlagsStr: '', 964 | dataLength: data.rawData.byteLength, 965 | errorCode: 0, 966 | invokeId: data.invokeId, 967 | error: false, 968 | errorStr: '' 969 | }, 970 | ads: { 971 | rawData: data.rawData 972 | } 973 | } 974 | packet.ams.stateFlagsStr = ADS.ADS_STATE_FLAGS.toString(packet.ams.stateFlags) 975 | 976 | this.debugD(`sendAdsCommand(): Sending an ads command ${packet.ams.adsCommandStr} (${data.rawData.byteLength} bytes): %o`, packet) 977 | 978 | //Creating a full AMS/TCP request 979 | let request = {} as Buffer 980 | 981 | try { 982 | request = this.createAmsTcpRequest(packet) 983 | } catch (err) { 984 | return reject(new ServerException(this, 'sendAdsCommand()', err as Error)) 985 | } 986 | 987 | //Write the data 988 | try { 989 | await this.socketWrite(request, socket) 990 | return resolve() 991 | } catch (err) { 992 | return reject(new ServerException(this, 'sendAdsCommand()', `Error - Socket is not available`, err as Error)) 993 | } 994 | }) 995 | } 996 | 997 | /** 998 | * Creates an AMS/TCP request from given packet 999 | * 1000 | * @param packet Object containing the full AMS/TCP packet 1001 | * @returns Full created AMS/TCP request as buffer 1002 | */ 1003 | protected createAmsTcpRequest(packet: AmsTcpPacket): Buffer { 1004 | //1. Create ADS data 1005 | const adsData = packet.ads.rawData 1006 | 1007 | //2. Create AMS header 1008 | const amsHeader = this.createAmsHeader(packet) 1009 | 1010 | //3. Create AMS/TCP header 1011 | const amsTcpHeader = this.createAmsTcpHeader(packet, amsHeader) 1012 | 1013 | //4. Create full AMS/TCP packet 1014 | const amsTcpRequest = Buffer.concat([amsTcpHeader, amsHeader, adsData ? adsData : Buffer.alloc(0)]) 1015 | 1016 | this.debugD(`createAmsTcpRequest(): AMS/TCP request created (${amsTcpRequest.byteLength} bytes)`) 1017 | 1018 | return amsTcpRequest 1019 | } 1020 | 1021 | /** 1022 | * Creates an AMS header from given packet 1023 | * 1024 | * @param packet Object containing the full AMS/TCP packet 1025 | * @returns Created AMS header as buffer 1026 | */ 1027 | protected createAmsHeader(packet: AmsTcpPacket): Buffer { 1028 | //Allocating bytes for AMS header 1029 | const header = Buffer.alloc(ADS.AMS_HEADER_LENGTH) 1030 | let pos = 0 1031 | 1032 | //0..5 Target AMSNetId 1033 | Buffer.from(this.amsNetIdStrToByteArray(packet.ams.targetAmsNetId)).copy(header, 0) 1034 | pos += ADS.AMS_NET_ID_LENGTH 1035 | 1036 | //6..8 Target ads port 1037 | header.writeUInt16LE(packet.ams.targetAdsPort, pos) 1038 | pos += 2 1039 | 1040 | //8..13 Source ads port 1041 | Buffer.from(this.amsNetIdStrToByteArray(packet.ams.sourceAmsNetId)).copy(header, pos) 1042 | pos += ADS.AMS_NET_ID_LENGTH 1043 | 1044 | //14..15 Source ads port 1045 | header.writeUInt16LE(packet.ams.sourceAdsPort, pos) 1046 | pos += 2 1047 | 1048 | //16..17 ADS command 1049 | header.writeUInt16LE(packet.ams.adsCommand, pos) 1050 | pos += 2 1051 | 1052 | //18..19 State flags 1053 | header.writeUInt16LE(packet.ams.stateFlags, pos) 1054 | pos += 2 1055 | 1056 | //20..23 Data length 1057 | header.writeUInt32LE(packet.ams.dataLength, pos) 1058 | pos += 4 1059 | 1060 | //24..27 Error code 1061 | header.writeUInt32LE(packet.ams.errorCode, pos) 1062 | pos += 4 1063 | 1064 | //28..31 Invoke ID 1065 | header.writeUInt32LE(packet.ams.invokeId, pos) 1066 | pos += 4 1067 | 1068 | this.debugD(`createAmsHeader(): AMS header created (${header.byteLength} bytes)`) 1069 | 1070 | if (this.debugIO.enabled) { 1071 | this.debugIO(`createAmsHeader(): AMS header created: %o`, header.toString('hex')) 1072 | } 1073 | 1074 | return header 1075 | } 1076 | 1077 | /** 1078 | * Creates an AMS/TCP header from given packet and AMS header 1079 | * 1080 | * @param packet Object containing the full AMS/TCP packet 1081 | * @param amsHeader Buffer containing the previously created AMS header 1082 | * @returns Created AMS/TCP header as buffer 1083 | */ 1084 | protected createAmsTcpHeader(packet: AmsTcpPacket, amsHeader: Buffer): Buffer { 1085 | //Allocating bytes for AMS/TCP header 1086 | const header = Buffer.alloc(ADS.AMS_TCP_HEADER_LENGTH) 1087 | let pos = 0 1088 | 1089 | //0..1 AMS command (header flag) 1090 | header.writeUInt16LE(packet.amsTcp.command, pos) 1091 | pos += 2 1092 | 1093 | //2..5 Data length 1094 | header.writeUInt32LE(amsHeader.byteLength + packet.ams.dataLength, pos) 1095 | pos += 4 1096 | 1097 | this.debugD(`_createAmsTcpHeader(): AMS/TCP header created (${header.byteLength} bytes)`) 1098 | 1099 | if (this.debugIO.enabled) { 1100 | this.debugIO(`_createAmsTcpHeader(): AMS/TCP header created: %o`, header.toString('hex')) 1101 | } 1102 | 1103 | return header 1104 | } 1105 | 1106 | /** 1107 | * Sends a device notification to target. 1108 | * @param notification Contains notification target info 1109 | * @param sourceAdsPort ADS port where notification is sent from 1110 | * @param socket Socket where to send data to 1111 | * @param data Data what to send 1112 | */ 1113 | protected sendDeviceNotificationToSocket(notification: AdsNotificationTarget, sourceAdsPort: number, socket: Socket, data: Buffer): Promise { 1114 | return new Promise(async (resolve, reject) => { 1115 | 1116 | if (notification.notificationHandle === undefined) 1117 | return reject(new ServerException(this, 'sendDeviceNotificationToSocket()', `notificationHandle is missing from parameter object "notification".`)) 1118 | 1119 | this.debug(`sendDeviceNotificationToSocket(): Sending device notification to ${notification.targetAmsNetId}:${notification.targetAdsPort} with handle ${notification.notificationHandle}`) 1120 | 1121 | //Sample 1122 | const sample = Buffer.alloc(8 + data.byteLength) 1123 | let pos = 0 1124 | 1125 | //0..3 Notification handle 1126 | sample.writeUInt32LE(notification.notificationHandle, pos) 1127 | pos += 4 1128 | 1129 | //4..7 Data length 1130 | sample.writeUInt32LE(data.byteLength, pos) 1131 | pos += 4 1132 | 1133 | //8..n Data 1134 | data.copy(sample, pos) 1135 | pos += data.byteLength 1136 | 1137 | //Stamp 1138 | const stamp = Buffer.alloc(12) 1139 | pos = 0 1140 | 1141 | //0..7 Timestamp (Converting to Windows FILETIME) 1142 | const ts = long.fromNumber(new Date().getTime()).add(11644473600000).mul(10000) 1143 | stamp.writeUInt32LE(ts.getLowBitsUnsigned(), pos) 1144 | pos += 4 1145 | 1146 | stamp.writeUInt32LE(ts.getHighBitsUnsigned(), pos) 1147 | pos += 4 1148 | 1149 | //8..11 Number of samples 1150 | stamp.writeUInt32LE(1, pos) 1151 | pos += 4 1152 | 1153 | //Notification 1154 | const packet = Buffer.alloc(8) 1155 | pos = 0 1156 | 1157 | //0..3 Data length 1158 | packet.writeUInt32LE(sample.byteLength + stamp.byteLength + packet.byteLength) 1159 | pos += 4 1160 | 1161 | //4..7 Stamp count 1162 | packet.writeUInt32LE(1, pos) 1163 | pos += 4 1164 | 1165 | //Check that next free invoke ID is below 32 bit integer maximum 1166 | if (this.nextInvokeId >= ADS.ADS_INVOKE_ID_MAX_VALUE) 1167 | this.nextInvokeId = 0 1168 | 1169 | //Sending the packet 1170 | this.sendAdsCommand({ 1171 | adsCommand: ADS.ADS_COMMAND.Notification, 1172 | targetAmsNetId: notification.targetAmsNetId, 1173 | targetAdsPort: notification.targetAdsPort, 1174 | sourceAmsNetId: this.connection.localAmsNetId, 1175 | sourceAdsPort: sourceAdsPort, 1176 | invokeId: this.nextInvokeId++, 1177 | rawData: Buffer.concat([packet, stamp, sample]) 1178 | }, socket) 1179 | .then(() => { 1180 | this.debug(`sendDeviceNotificationToSocket(): Device notification sent to ${notification.targetAmsNetId}:${notification.targetAdsPort} with handle ${notification.notificationHandle}`) 1181 | resolve() 1182 | }) 1183 | .catch(res => { 1184 | reject(new ServerException(this, 'sendDeviceNotificationToSocket()', `Sending notification to ${notification.targetAmsNetId}:${notification.targetAdsPort} with handle ${notification.notificationHandle} failed`, res)) 1185 | }) 1186 | }) 1187 | } 1188 | 1189 | /** 1190 | * Sets request callback for given ADS command 1191 | * 1192 | * @param request ADS command as number 1193 | * @param callback Callback function 1194 | */ 1195 | protected setRequestCallback(request: number, callback: GenericReqCallback): void { 1196 | //Allowing null so a callback can be removed 1197 | if (typeof callback !== 'function' && callback != null) { 1198 | throw new TypeError(`Given callback was not a function, it was ${typeof callback} instead`) 1199 | } 1200 | 1201 | this.requestCallbacks[ADS.ADS_COMMAND.toString(request)] = callback 1202 | } 1203 | 1204 | /** 1205 | * Writes given message to console if `settings.hideConsoleWarnings` is false 1206 | * 1207 | * @param str String to write to console 1208 | */ 1209 | protected consoleWrite(str: string): void { 1210 | if (this.settings.hideConsoleWarnings !== true) 1211 | console.log(`ads-server: ${str}`) 1212 | } 1213 | 1214 | /** 1215 | * Sets callback function to be called when ADS Read request is received 1216 | * 1217 | * @param callback Callback that is called when request received 1218 | * ```js 1219 | * onReadReq(async (req, res) => { 1220 | * //do something with req object and then respond 1221 | * await res({..}) 1222 | * }) 1223 | * ``` 1224 | */ 1225 | onReadReq(callback: ReadReqCallback): void { 1226 | this.setRequestCallback(ADS.ADS_COMMAND.Read, callback) 1227 | } 1228 | 1229 | /** 1230 | * Sets callback function to be called when ADS ReadWrite request is received 1231 | * 1232 | * @param callback Callback that is called when request received 1233 | * ```js 1234 | * onReadWriteReq(async (req, res) => { 1235 | * //do something with req object and then respond 1236 | * await res({..}) 1237 | * }) 1238 | * ``` 1239 | */ 1240 | onReadWriteReq(callback: ReadWriteReqCallback): void { 1241 | this.setRequestCallback(ADS.ADS_COMMAND.ReadWrite, callback) 1242 | } 1243 | 1244 | /** 1245 | * Sets callback function to be called when ADS Write request is received 1246 | * 1247 | * @param callback Callback that is called when request received 1248 | * ```js 1249 | * onWriteReq(async (req, res) => { 1250 | * //do something with req object and then respond 1251 | * await res({..}) 1252 | * }) 1253 | * ``` 1254 | */ 1255 | onWriteReq(callback: WriteReqCallback): void { 1256 | this.setRequestCallback(ADS.ADS_COMMAND.Write, callback) 1257 | } 1258 | 1259 | /** 1260 | * Sets callback function to be called when ADS ReadDeviceInfo request is received 1261 | * 1262 | * @param callback Callback that is called when request received 1263 | * ```js 1264 | * onReadDeviceInfo(async (req, res) => { 1265 | * //do something with req object and then respond 1266 | * await res({..}) 1267 | * }) 1268 | * ``` 1269 | */ 1270 | onReadDeviceInfo(callback: ReadDeviceInfoReqCallback): void { 1271 | this.setRequestCallback(ADS.ADS_COMMAND.ReadDeviceInfo, callback) 1272 | } 1273 | 1274 | /** 1275 | * Sets callback function to be called when ADS ReadState request is received 1276 | * 1277 | * @param callback Callback that is called when request received 1278 | * ```js 1279 | * onReadState(async (req, res) => { 1280 | * //do something with req object and then respond 1281 | * await res({..}) 1282 | * }) 1283 | * ``` 1284 | */ 1285 | onReadState(callback: ReadStateReqCallback): void { 1286 | this.setRequestCallback(ADS.ADS_COMMAND.ReadState, callback) 1287 | } 1288 | 1289 | /** 1290 | * Sets callback function to be called when ADS AddNotification request is received 1291 | * 1292 | * @param callback Callback that is called when request received 1293 | * ```js 1294 | * onAddNotification(async (req, res) => { 1295 | * //do something with req object and then respond 1296 | * await res({..}) 1297 | * }) 1298 | * ``` 1299 | */ 1300 | onAddNotification(callback: AddNotificationReqCallback): void { 1301 | this.setRequestCallback(ADS.ADS_COMMAND.AddNotification, callback as GenericReqCallback) 1302 | } 1303 | 1304 | /** 1305 | * Sets callback function to be called when ADS DeleteNotification request is received 1306 | * 1307 | * @param callback Callback that is called when request received 1308 | * ```js 1309 | * onDeleteNotification(async (req, res) => { 1310 | * //do something with req object and then respond 1311 | * await res({..}) 1312 | * }) 1313 | * ``` 1314 | */ 1315 | onDeleteNotification(callback: DeleteNotificationReqCallback): void { 1316 | this.setRequestCallback(ADS.ADS_COMMAND.DeleteNotification, callback) 1317 | } 1318 | 1319 | /** 1320 | * Sets callback function to be called when ADS WriteControl request is received 1321 | * 1322 | * @param callback Callback that is called when request received 1323 | * ```js 1324 | * onWriteControl(async (req, res) => { 1325 | * //do something with req object and then respond 1326 | * await res({..}) 1327 | * }) 1328 | * ``` 1329 | */ 1330 | onWriteControl(callback: WriteControlReqCallback): void { 1331 | this.setRequestCallback(ADS.ADS_COMMAND.WriteControl, callback) 1332 | } 1333 | 1334 | /** 1335 | * Writes data to the socket 1336 | * 1337 | * @param data Data to write 1338 | * @param socket Socket to write to 1339 | */ 1340 | protected socketWrite(data: Buffer, socket: Socket): Promise { 1341 | return new Promise(async (resolve, reject) => { 1342 | 1343 | if (this.debugIO.enabled) { 1344 | this.debugIO(`IO out ------> ${data.byteLength} bytes : ${data.toString('hex')}`) 1345 | } else { 1346 | this.debugD(`IO out ------> ${data.byteLength} bytes`) 1347 | } 1348 | 1349 | socket.write(data, err => { 1350 | if (err) { 1351 | reject(err) 1352 | } else { 1353 | resolve() 1354 | } 1355 | }) 1356 | }) 1357 | } 1358 | 1359 | /** 1360 | * Converts array of bytes (or Buffer) to AmsNetId string 1361 | * 1362 | * @param byteArray 1363 | * @returns AmsNetId as string (like 192.168.1.10.1.1) 1364 | */ 1365 | protected byteArrayToAmsNedIdStr(byteArray: Buffer | number[]): string { 1366 | return byteArray.join('.') 1367 | } 1368 | 1369 | /** 1370 | * Converts AmsNetId string to array of bytes 1371 | * 1372 | * @param str AmsNetId as string 1373 | * @returns AmsNetId as array of bytes 1374 | */ 1375 | protected amsNetIdStrToByteArray(str: string): number[] { 1376 | return str.split('.').map(x => parseInt(x)) 1377 | } 1378 | } 1379 | 1380 | 1381 | 1382 | /** 1383 | * Own exception class used for Server errors 1384 | * 1385 | * Derived from Error but added innerException and ADS error information 1386 | * 1387 | */ 1388 | export class ServerException extends Error { 1389 | sender: string 1390 | adsError: boolean 1391 | adsErrorInfo: Record | null = null 1392 | metaData: unknown | null = null 1393 | errorTrace: Array = [] 1394 | getInnerException: () => (Error | ServerException | null) 1395 | stack: string | undefined 1396 | 1397 | constructor(server: ServerCore, sender: string, messageOrError: string | Error | ServerException, ...errData: unknown[]) { 1398 | 1399 | //The 2nd parameter can be either message or another Error or ServerException 1400 | super((messageOrError as Error).message ? (messageOrError as Error).message : messageOrError as string) 1401 | 1402 | if (messageOrError instanceof ServerException) { 1403 | //Add to errData, so will be handled later 1404 | errData.push(messageOrError) 1405 | 1406 | } else if (messageOrError instanceof Error) { 1407 | //Add to errData, so will be handled later 1408 | errData.push(messageOrError) 1409 | } 1410 | 1411 | //Stack trace 1412 | if (typeof Error.captureStackTrace === 'function') { 1413 | Error.captureStackTrace(this, this.constructor) 1414 | } else { 1415 | this.stack = (new Error(this.message)).stack 1416 | } 1417 | 1418 | this.name = this.constructor.name 1419 | this.sender = sender 1420 | this.adsError = false 1421 | this.adsErrorInfo = null 1422 | this.metaData = null 1423 | this.errorTrace = [] 1424 | this.getInnerException = () => null 1425 | 1426 | 1427 | //Loop through given additional data 1428 | errData.forEach(data => { 1429 | 1430 | if (data instanceof ServerException && this.getInnerException == null) { 1431 | //Another ServerException error 1432 | this.getInnerException = () => data 1433 | 1434 | //Add it to our own tracing array 1435 | this.errorTrace.push(`${(this.getInnerException() as ServerException).sender}: ${this.getInnerException()?.message}`); 1436 | 1437 | //Add also all traces from the inner exception 1438 | (this.getInnerException() as ServerException).errorTrace.forEach((s: string) => this.errorTrace.push(s)) 1439 | 1440 | //Modifying the stack trace so it contains all previous ones too 1441 | //Source: Matt @ https://stackoverflow.com/a/42755876/8140625 1442 | 1443 | if (server.debugLevel > 0) { 1444 | const message_lines = (this.message.match(/\n/g) || []).length + 1 1445 | this.stack = this.stack ? this.stack.split('\n').slice(0, message_lines + 1).join('\n') + '\n' : "" + 1446 | this.getInnerException()?.stack 1447 | } 1448 | 1449 | } else if (data instanceof Error && this.getInnerException == null) { 1450 | 1451 | //Error -> Add it's message to our message 1452 | this.message += ` (${data.message})` 1453 | this.getInnerException = () => data 1454 | 1455 | //Modifying the stack trace so it contains all previous ones too 1456 | //Source: Matt @ https://stackoverflow.com/a/42755876/8140625 1457 | if (server.debugLevel > 0) { 1458 | const message_lines = (this.message.match(/\n/g) || []).length + 1 1459 | this.stack = this.stack ? this.stack.split('\n').slice(0, message_lines + 1).join('\n') + '\n' : "" + 1460 | this.getInnerException()?.stack 1461 | } 1462 | 1463 | } else if ((data as AmsTcpPacket).ams && (data as AmsTcpPacket).ams.error) { 1464 | //AMS reponse with error code 1465 | this.adsError = true 1466 | this.adsErrorInfo = { 1467 | adsErrorType: 'AMS error', 1468 | adsErrorCode: (data as AmsTcpPacket).ams.errorCode, 1469 | adsErrorStr: (data as AmsTcpPacket).ams.errorStr 1470 | } 1471 | 1472 | } else if ((data as AmsTcpPacket).ads && (data as AmsTcpPacket).ads.error) { 1473 | //ADS response with error code 1474 | this.adsError = true 1475 | this.adsErrorInfo = { 1476 | adsErrorType: 'ADS error', 1477 | adsErrorCode: (data as AmsTcpPacket).ads.errorCode, 1478 | adsErrorStr: (data as AmsTcpPacket).ads.errorStr 1479 | } 1480 | 1481 | } else if (this.metaData == null) { 1482 | //If something else is provided, save it 1483 | this.metaData = data 1484 | } 1485 | }) 1486 | 1487 | //If this particular exception has no ADS error, check if the inner exception has 1488 | //It should always be passed upwards to the end-user 1489 | if (!this.adsError && this.getInnerException() != null) { 1490 | const inner = this.getInnerException() as ServerException 1491 | 1492 | if (inner.adsError != null && inner.adsError === true) { 1493 | this.adsError = true 1494 | this.adsErrorInfo = inner.adsErrorInfo 1495 | } 1496 | } 1497 | } 1498 | } 1499 | -------------------------------------------------------------------------------- /src/ads-server-router.ts: -------------------------------------------------------------------------------- 1 | /* 2 | https://github.com/jisotalo/ads-server 3 | 4 | Copyright (c) 2021 Jussi Isotalo 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | import { 26 | Socket, 27 | SocketConnectOpts 28 | } from 'net' 29 | 30 | import { 31 | ServerCore, 32 | ServerException 33 | } from './ads-server-core' 34 | 35 | import { 36 | AmsRouterState, 37 | RouterServerSettings, 38 | TimerObject, 39 | ServerConnection, 40 | AdsNotificationTarget 41 | } from './types/ads-server' 42 | 43 | import { 44 | AmsPortRegisteredData, 45 | AmsRouterStateData, 46 | AmsTcpPacket 47 | } from './types/ads-types' 48 | 49 | import * as ADS from './ads-commons' 50 | 51 | 52 | /** 53 | * TwinCAT ADS server 54 | * 55 | * This ADS server class connects to the AMS/ADS router 56 | * and listens for incoming ADS commands **(ONLY)** for the ADS port provided in settings. 57 | * 58 | * **Requires TwinCAT installation / AMS router for operation.** Without router, see `StandAloneServer` class. 59 | */ 60 | export class RouterServer extends ServerCore { 61 | 62 | /** 63 | * Active settings 64 | */ 65 | public settings: RouterServerSettings = { 66 | routerTcpPort: 48898, 67 | routerAddress: '127.0.0.1', 68 | localAddress: '', 69 | localTcpPort: 0, 70 | localAmsNetId: '', 71 | localAdsPort: 0, 72 | timeoutDelay: 2000, 73 | autoReconnect: true, 74 | reconnectInterval: 2000, 75 | } 76 | 77 | /** 78 | * Local router state (if known) 79 | */ 80 | public routerState?: AmsRouterState = undefined 81 | 82 | /** 83 | * Received data buffer 84 | */ 85 | private receiveBuffer = Buffer.alloc(0) 86 | 87 | /** 88 | * Socket instance 89 | */ 90 | private socket?: Socket = undefined 91 | 92 | /** 93 | * Handler for socket error event 94 | */ 95 | private socketErrorHandler?: (err: Error) => void 96 | 97 | /** 98 | * Handler for socket close event 99 | */ 100 | private socketConnectionLostHandler?: (hadError: boolean) => void 101 | 102 | /** 103 | * Timer ID and handle of reconnection timer 104 | */ 105 | private reconnectionTimer: TimerObject = { id: 0 } 106 | 107 | /** 108 | * Timer handle for port register timeout 109 | */ 110 | private portRegisterTimeoutTimer?: NodeJS.Timeout = undefined 111 | 112 | 113 | /** 114 | * Creates a new ADS server instance. 115 | * 116 | * Settings are provided as parameter 117 | */ 118 | public constructor(settings: Partial) { 119 | super(settings) 120 | 121 | //ServerRouter as router state callback -> set it so core can call it 122 | this.routerStateChangedCallback = this.onRouterStateChanged 123 | 124 | //Taking the default settings and then updating the provided ones 125 | this.settings = { 126 | ...this.settings, 127 | ...settings 128 | } 129 | } 130 | 131 | /** 132 | * Connects to the AMS router and registers ADS port. 133 | * Starts listening for incoming ADS commands 134 | * 135 | * @returns {ServerConnection} Connection info 136 | */ 137 | public connect(): Promise { 138 | return this.connectToTarget() 139 | } 140 | 141 | /** 142 | * Disconnects from target router and unregisters ADS port. 143 | * Stops listening for incoming ADS commands 144 | */ 145 | public disconnect(): Promise { 146 | return this.disconnectFromTarget() 147 | } 148 | 149 | /** 150 | * Reconnects to the AMS router. 151 | * First disconnects and then connects again. 152 | * 153 | * @returns {ServerConnection} Connection info 154 | */ 155 | public reconnect(): Promise { 156 | return this.reconnectToTarget() 157 | } 158 | 159 | /** 160 | * Sends a given data as notification using given 161 | * notificationHandle and target info. 162 | */ 163 | public sendDeviceNotification(notification: AdsNotificationTarget, data: Buffer): Promise { 164 | if (!this.connection.connected || !this.socket) 165 | throw new ServerException(this, 'sendDeviceNotification()', `Server is not connected. Use connect() to connect first.`) 166 | 167 | return this.sendDeviceNotificationToSocket( 168 | notification, 169 | this.connection.localAdsPort as number, 170 | this.socket, 171 | data 172 | ) 173 | } 174 | 175 | /** 176 | * Connects to the AMS router and registers ADS port. 177 | * 178 | * @param isReconnecting Not used at the moment (true = reconnecting in progress) 179 | */ 180 | private connectToTarget(isReconnecting = false): Promise { 181 | return new Promise(async (resolve, reject) => { 182 | 183 | if (this.socket) { 184 | this.debug(`connectToTarget(): Socket already assigned`) 185 | return reject(new ServerException(this, 'connectToTarget()', 'Connection is already opened. Close the connection first using disconnect()')) 186 | } 187 | 188 | this.debug(`connectToTarget(): Starting to connect ${this.settings.routerAddress}:${this.settings.routerTcpPort} (reconnect: ${isReconnecting})`) 189 | 190 | //Creating a socket and setting it up 191 | const socket = new Socket() 192 | socket.setNoDelay(true) //Sends data without delay 193 | 194 | //----- Connecting error events ----- 195 | //Listening error event during connection 196 | socket.once('error', (err: NodeJS.ErrnoException) => { 197 | this.debug('connectToTarget(): Socket connect failed: %O', err) 198 | 199 | //Remove all events from socket 200 | socket.removeAllListeners() 201 | this.socket = undefined 202 | 203 | //Reset connection flag 204 | this.connection.connected = false 205 | 206 | reject(new ServerException(this, 'connectToTarget()', `Connection to ${this.settings.routerAddress}:${this.settings.routerTcpPort} failed (socket error ${err.errno})`, err)) 207 | }) 208 | 209 | 210 | 211 | //Listening close event during connection 212 | socket.once('close', hadError => { 213 | this.debug(`connectToTarget(): Socket closed by remote, connection failed`) 214 | 215 | //Remove all events from socket 216 | socket.removeAllListeners() 217 | this.socket = undefined 218 | 219 | //Reset connection flag 220 | this.connection.connected = false 221 | 222 | reject(new ServerException(this, 'connectToTarget()', `Connection to ${this.settings.routerAddress}:${this.settings.routerTcpPort} failed - socket closed by remote (hadError = ${hadError})`)) 223 | }) 224 | 225 | 226 | //Listening end event during connection 227 | socket.once('end', () => { 228 | this.debug(`connectToTarget(): Socket connection ended by remote, connection failed.`) 229 | 230 | //Remove all events from socket 231 | socket.removeAllListeners() 232 | this.socket = undefined 233 | 234 | //Reset connection flag 235 | this.connection.connected = false 236 | 237 | if (this.settings.localAdsPort != null) 238 | reject(new ServerException(this, 'connectToTarget()', `Connection to ${this.settings.routerAddress}:${this.settings.routerTcpPort} failed - socket ended by remote (is the given local ADS port ${this.settings.localAdsPort} already in use?)`)) 239 | else 240 | reject(new ServerException(this, 'connectToTarget()', `Connection to ${this.settings.routerAddress}:${this.settings.routerTcpPort} failed - socket ended by remote`)) 241 | }) 242 | 243 | //Listening timeout event during connection 244 | socket.once('timeout', () => { 245 | this.debug(`connectToTarget(): Socket timeout`) 246 | 247 | //No more timeout needed 248 | socket.setTimeout(0); 249 | socket.destroy() 250 | 251 | //Remove all events from socket 252 | socket.removeAllListeners() 253 | this.socket = undefined 254 | 255 | //Reset connection flag 256 | this.connection.connected = false 257 | 258 | reject(new ServerException(this, 'connectToTarget()', `Connection to ${this.settings.routerAddress}:${this.settings.routerTcpPort} failed (timeout) - No response from router in ${this.settings.timeoutDelay} ms`)) 259 | }) 260 | 261 | //----- Connecting error events end ----- 262 | 263 | 264 | //Listening for connect event 265 | socket.once('connect', async () => { 266 | this.debug(`connectToTarget(): Socket connection established to ${this.settings.routerAddress}:${this.settings.routerTcpPort}`) 267 | 268 | //No more timeout needed 269 | socket.setTimeout(0); 270 | 271 | this.socket = socket 272 | 273 | //Try to register an ADS port 274 | try { 275 | const res = await this.registerAdsPort() 276 | const amsPortData = res.amsTcp.data as AmsPortRegisteredData 277 | 278 | this.connection.connected = true 279 | this.connection.localAmsNetId = amsPortData.localAmsNetId 280 | this.connection.localAdsPort = amsPortData.localAdsPort 281 | 282 | this.debug(`connectToTarget(): ADS port registered from router. We are ${this.connection.localAmsNetId}:${this.connection.localAdsPort}`) 283 | } catch (err) { 284 | 285 | if (socket) { 286 | socket.destroy() 287 | //Remove all events from socket 288 | socket.removeAllListeners() 289 | } 290 | 291 | this.socket = undefined 292 | this.connection.connected = false 293 | 294 | return reject(new ServerException(this, 'connectToTarget()', `Registering ADS port from router failed`, err)) 295 | } 296 | 297 | //Remove the socket events that were used only during connectToTarget() 298 | socket.removeAllListeners('error') 299 | socket.removeAllListeners('close') 300 | socket.removeAllListeners('end') 301 | 302 | //When socket errors from now on, we will close the connection 303 | this.socketErrorHandler = this.onSocketError.bind(this) 304 | socket.on('error', this.socketErrorHandler) 305 | 306 | //Listening connection lost events 307 | this.socketConnectionLostHandler = this.onConnectionLost.bind(this, true) 308 | socket.on('close', this.socketConnectionLostHandler as (hadError: boolean) => void) 309 | 310 | //We are connected to the target 311 | this.emit('connect', this.connection) 312 | 313 | resolve(this.connection) 314 | }) 315 | 316 | //Listening data event 317 | socket.on('data', data => { 318 | if (this.debugIO.enabled) { 319 | this.debugIO(`IO in <------ ${data.byteLength} bytes from ${socket.remoteAddress}: ${data.toString('hex')}`) 320 | } else if (this.debugD.enabled) { 321 | this.debugD(`IO in <------ ${data.byteLength} bytes from ${socket.remoteAddress}`) 322 | } 323 | 324 | //Adding received data to connection buffer and checking the data 325 | this.receiveBuffer = Buffer.concat([this.receiveBuffer, data]) 326 | 327 | this.handleReceivedData( 328 | this.receiveBuffer, 329 | socket, 330 | (newBuffer: Buffer) => this.receiveBuffer = newBuffer 331 | ) 332 | }) 333 | 334 | //Timeout only during connecting, other timeouts are handled elsewhere 335 | socket.setTimeout(this.settings.timeoutDelay); 336 | 337 | //Finally, connect 338 | try { 339 | socket.connect({ 340 | port: this.settings.routerTcpPort, 341 | host: this.settings.routerAddress, 342 | localPort: this.settings.localTcpPort, 343 | localAddress: this.settings.localAddress 344 | } as SocketConnectOpts) 345 | 346 | } catch (err) { 347 | this.connection.connected = false 348 | 349 | reject(new ServerException(this, 'connectToTarget()', `Opening socket connection to ${this.settings.routerAddress}:${this.settings.routerTcpPort} failed`, err)) 350 | } 351 | }) 352 | } 353 | 354 | 355 | /** 356 | * Unregisters ADS port from router (if it was registered) 357 | * and disconnects target system and ADS router 358 | * 359 | * @param [forceDisconnect] - If true, the connection is dropped immediately (default = false) 360 | * @param [isReconnecting] - If true, call is made during reconnecting 361 | */ 362 | private disconnectFromTarget(forceDisconnect = false, isReconnecting = false): Promise { 363 | return new Promise(async (resolve, reject) => { 364 | 365 | this.debug(`disconnectFromTarget(): Starting to close connection (force: ${forceDisconnect})`) 366 | 367 | try { 368 | if (this.socketConnectionLostHandler) { 369 | this.socket?.off('close', this.socketConnectionLostHandler) 370 | } 371 | 372 | } catch (err) { 373 | //We probably have no socket anymore. Just quit. 374 | forceDisconnect = true 375 | } 376 | 377 | //Clear reconnection timer only when not reconnecting 378 | if (!isReconnecting) { 379 | this.clearTimer(this.reconnectionTimer) 380 | } 381 | 382 | //Clear other timers 383 | if (this.portRegisterTimeoutTimer) 384 | clearTimeout(this.portRegisterTimeoutTimer) 385 | 386 | //If forced, then just destroy the socket 387 | if (forceDisconnect) { 388 | 389 | 390 | this.connection.connected = false 391 | this.connection.localAdsPort = undefined 392 | this.connection.localAmsNetId = '' 393 | 394 | this.socket?.removeAllListeners() 395 | this.socket?.destroy() 396 | this.socket = undefined 397 | 398 | this.emit('disconnect') 399 | 400 | return resolve() 401 | } 402 | 403 | 404 | try { 405 | await this.unregisterAdsPort() 406 | 407 | //Done 408 | this.connection.connected = false 409 | this.connection.localAdsPort = undefined 410 | this.connection.localAmsNetId = '' 411 | 412 | this.socket?.removeAllListeners() 413 | this.socket?.destroy() 414 | this.socket = undefined 415 | 416 | this.debug(`disconnectFromTarget(): Connection closed successfully`) 417 | this.emit('disconnect') 418 | 419 | return resolve() 420 | 421 | } catch (err) { 422 | //Force socket close 423 | this.socket?.removeAllListeners() 424 | this.socket?.destroy() 425 | this.socket = undefined 426 | 427 | this.connection.connected = false 428 | this.connection.localAdsPort = undefined 429 | this.connection.localAmsNetId = '' 430 | 431 | this.debug(`disconnectFromTarget(): Connection closing failed, connection forced to close`) 432 | this.emit('disconnect') 433 | 434 | return reject(new ServerException(this, 'disconnect()', `Disconnected but something failed: ${(err as Error).message}`)) 435 | } 436 | }) 437 | } 438 | 439 | 440 | 441 | /** 442 | * Disconnects and reconnects again 443 | * 444 | * @param [forceDisconnect] - If true, the connection is dropped immediately (default = false) 445 | * @param [isReconnecting] - If true, call is made during reconnecting 446 | */ 447 | private reconnectToTarget(forceDisconnect = false, isReconnecting = false): Promise { 448 | return new Promise(async (resolve, reject) => { 449 | 450 | if (this.socket) { 451 | try { 452 | this.debug(`_reconnect(): Trying to disconnect`) 453 | 454 | await this.disconnectFromTarget(forceDisconnect, isReconnecting) 455 | 456 | } catch (err) { 457 | //debug(`_reconnect(): Disconnecting failed: %o`, err) 458 | } 459 | } 460 | 461 | this.debug(`_reconnect(): Trying to connect`) 462 | 463 | return this.connectToTarget(true) 464 | .then(res => { 465 | this.debug(`_reconnect(): Connected!`) 466 | 467 | this.emit('reconnect') 468 | 469 | resolve(res) 470 | }) 471 | .catch(err => { 472 | this.debug(`_reconnect(): Connecting failed`) 473 | reject(err) 474 | }) 475 | }) 476 | } 477 | 478 | 479 | 480 | 481 | /** 482 | * Registers a new ADS port from AMS router 483 | */ 484 | private registerAdsPort(): Promise { 485 | return new Promise(async (resolve, reject) => { 486 | this.debugD(`registerAdsPort(): Registering an ADS port from ADS router ${this.settings.routerAddress}:${this.settings.routerTcpPort}`) 487 | 488 | //If a manual AmsNetId and ADS port values are used, we should resolve immediately 489 | //This is used for example if connecting to a remote PLC from non-ads device 490 | if (this.settings.localAmsNetId && this.settings.localAdsPort) { 491 | this.debug(`registerAdsPort(): Local AmsNetId and ADS port manually given so using ${this.settings.localAmsNetId}:${this.settings.localAdsPort}`) 492 | 493 | const res = { 494 | amsTcp: { 495 | data: { 496 | localAmsNetId: this.settings.localAmsNetId, 497 | localAdsPort: this.settings.localAdsPort 498 | } 499 | } 500 | } as AmsTcpPacket 501 | 502 | return resolve(res) 503 | } 504 | 505 | const packet = Buffer.alloc(8) 506 | let pos = 0 507 | 508 | //0..1 Ams command (header flag) 509 | packet.writeUInt16LE(ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_CONNECT) 510 | pos += 2 511 | 512 | //2..5 Data length 513 | packet.writeUInt32LE(2, pos) 514 | pos += 4 515 | 516 | //6..7 Data: Requested ads port (0 = let the server decide) 517 | packet.writeUInt16LE((this.settings.localAdsPort ? this.settings.localAdsPort : 0), pos) 518 | 519 | //Setup callback to call when responded 520 | this.amsTcpCallback = (res: AmsTcpPacket) => { 521 | this.amsTcpCallback = undefined 522 | 523 | this.socket?.off('error', errorHandler) 524 | if (this.portRegisterTimeoutTimer) 525 | clearTimeout(this.portRegisterTimeoutTimer) 526 | 527 | this.debugD(`registerAdsPort(): ADS port registered, assigned AMS address is ${(res.amsTcp.data as AmsPortRegisteredData).localAmsNetId}:${(res.amsTcp.data as AmsPortRegisteredData).localAdsPort}`) 528 | 529 | resolve(res) 530 | } 531 | 532 | //Timeout (if no answer from router) 533 | this.portRegisterTimeoutTimer = setTimeout(() => { 534 | //Callback is no longer needed, delete it 535 | this.amsTcpCallback = undefined 536 | 537 | //Create a custom "ads error" so that the info is passed onwards 538 | const adsError = { 539 | ads: { 540 | error: true, 541 | errorCode: -1, 542 | errorStr: `Timeout - no response in ${this.settings.timeoutDelay} ms` 543 | } 544 | } 545 | this.debug(`registerAdsPort(): Failed to register ADS port - Timeout - no response in ${this.settings.timeoutDelay} ms`) 546 | 547 | return reject(new ServerException(this, 'registerAdsPort()', `Timeout - no response in ${this.settings.timeoutDelay} ms`, adsError)) 548 | }, this.settings.timeoutDelay) 549 | 550 | const errorHandler = () => { 551 | if (this.portRegisterTimeoutTimer) 552 | clearTimeout(this.portRegisterTimeoutTimer) 553 | 554 | this.debugD(`registerAdsPort(): Socket connection errored.`) 555 | reject(new ServerException(this, 'registerAdsPort()', `Socket connection error`)) 556 | } 557 | 558 | this.socket?.once('error', errorHandler) 559 | 560 | try { 561 | if (this.socket) { 562 | await this.socketWrite(packet, this.socket) 563 | } else { 564 | throw new ServerException(this, 'registerAdsPort()', `Error - Writing to socket failed, socket is not available`) 565 | } 566 | 567 | } catch (err) { 568 | this.socket?.off('error', errorHandler) 569 | if (this.portRegisterTimeoutTimer) 570 | clearTimeout(this.portRegisterTimeoutTimer) 571 | 572 | return reject(new ServerException(this, 'registerAdsPort()', `Error - Writing to socket failed`, err)) 573 | } 574 | }) 575 | } 576 | 577 | 578 | 579 | 580 | /** 581 | * Unregisters previously registered ADS port from AMS router. 582 | * Connection is usually also closed by remote during unregisterin. 583 | */ 584 | private unregisterAdsPort(): Promise { 585 | return new Promise(async (resolve, reject) => { 586 | this.debugD(`unregisterAdsPort(): Unregister ads port ${this.connection.localAdsPort} from ${this.settings.routerAddress}:${this.settings.routerTcpPort}`) 587 | 588 | if (this.settings.localAmsNetId && this.settings.localAdsPort) { 589 | this.debug(`unregisterAdsPort(): Local AmsNetId and ADS port manually given so no need to unregister`) 590 | 591 | this.socket?.end(() => { 592 | this.debugD(`unregisterAdsPort(): Socket closed`) 593 | this.socket?.destroy() 594 | this.debugD(`unregisterAdsPort(): Socket destroyed`) 595 | }) 596 | return resolve() 597 | } 598 | 599 | if (!this.socket) { 600 | return resolve() 601 | } 602 | 603 | const buffer = Buffer.alloc(8) 604 | let pos = 0 605 | 606 | //0..1 AMS command (header flag) 607 | buffer.writeUInt16LE(ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_CLOSE) 608 | pos += 2 609 | 610 | //2..5 Data length 611 | buffer.writeUInt32LE(2, pos) 612 | pos += 4 613 | 614 | //6..9 Data: port to unregister 615 | buffer.writeUInt16LE(this.connection.localAdsPort as number, pos) 616 | 617 | this.socket.once('timeout', () => { 618 | this.debugD(`unregisterAdsPort(): Timeout happened during port unregister. Closing connection anyways.`) 619 | 620 | this.socket?.end(() => { 621 | this.debugD(`unregisterAdsPort(): Socket closed after timeout`) 622 | 623 | this.socket?.destroy() 624 | 625 | this.debugD(`unregisterAdsPort(): Socket destroyed after timeout`) 626 | }) 627 | }) 628 | 629 | //When socket emits close event, the ads port is unregistered and connection closed 630 | this.socket.once('close', (hadError: boolean) => { 631 | this.debugD(`unregisterAdsPort(): Ads port unregistered and socket connection closed (hadError: ${hadError}).`) 632 | resolve() 633 | }) 634 | 635 | //Sometimes close event is not received, so resolve already here 636 | this.socket.once('end', () => { 637 | this.debugD(`unregisterAdsPort(): Socket connection ended. Connection closed.`) 638 | 639 | this.socket?.destroy() 640 | 641 | resolve() 642 | }) 643 | 644 | try { 645 | await this.socketWrite(buffer, this.socket) 646 | } catch (err) { 647 | reject(err) 648 | } 649 | }) 650 | } 651 | 652 | /** 653 | * Event listener for socket errors 654 | */ 655 | private async onSocketError(err: Error) { 656 | this.consoleWrite(`WARNING: Socket connection had an error, closing connection: ${JSON.stringify(err)}`) 657 | 658 | this.onConnectionLost(true) 659 | } 660 | 661 | /** 662 | * Called when connection to the remote is lost 663 | * 664 | * @param socketFailure - If true, connection was lost due socket/tcp problem -> Just destroy the socket 665 | * 666 | */ 667 | private async onConnectionLost(socketFailure = false) { 668 | this.debug(`onConnectionLost(): Connection was lost. Socket failure: ${socketFailure}`) 669 | 670 | this.connection.connected = false 671 | this.emit('connectionLost') 672 | 673 | if (this.settings.autoReconnect !== true) { 674 | this.consoleWrite.call(this, 'WARNING: Connection was lost and setting autoReconnect=false. Quiting.') 675 | try { 676 | await this.disconnectFromTarget(true) 677 | } catch { 678 | //failed 679 | } 680 | 681 | return 682 | } 683 | 684 | if (this.socketConnectionLostHandler) 685 | this.socket?.off('close', this.socketConnectionLostHandler) 686 | 687 | this.consoleWrite('WARNING: Connection was lost. Trying to reconnect...') 688 | 689 | const tryToReconnect = async (firstTime: boolean, timerId: number) => { 690 | 691 | //If the timer has changed, quit here 692 | if (this.reconnectionTimer.id !== timerId) { 693 | return 694 | } 695 | 696 | //Try to reconnect 697 | this.reconnectToTarget(socketFailure, true) 698 | .then(res => { 699 | this.debug(`Reconnected successfully as ${res.localAmsNetId}`) 700 | 701 | //Success -> remove timer 702 | this.clearTimer(this.reconnectionTimer) 703 | 704 | }) 705 | .catch(err => { 706 | //Reconnecting failed 707 | if (firstTime) { 708 | this.debug(`Reconnecting failed, keeping trying in the background (${(err as Error).message}`) 709 | this.consoleWrite(`WARNING: Reconnecting failed. Keeping trying in the background every ${this.settings.reconnectInterval} ms...`) 710 | } 711 | 712 | //If this is still a valid timer, start over again 713 | if (this.reconnectionTimer.id === timerId) { 714 | //Creating a new timer with the same id 715 | this.reconnectionTimer.timer = setTimeout( 716 | () => tryToReconnect(false, timerId), 717 | this.settings.reconnectInterval 718 | ) 719 | } else { 720 | this.debugD(`onConnectionLost(): Timer is no more valid, quiting here`) 721 | } 722 | }) 723 | } 724 | 725 | //Clearing old timer if there is one + increasing timer id 726 | this.clearTimer(this.reconnectionTimer) 727 | 728 | //Starting poller timer 729 | this.reconnectionTimer.timer = setTimeout( 730 | () => tryToReconnect(true, this.reconnectionTimer.id), 731 | this.settings.reconnectInterval 732 | ) 733 | } 734 | 735 | /** 736 | * Called when local AMS router status has changed (Router notification received) 737 | * For example router state changes when local TwinCAT changes from Config to Run state and vice-versa 738 | * 739 | * @param data Packet that contains the new router state 740 | */ 741 | protected onRouterStateChanged(data: AmsTcpPacket): void { 742 | const routerStateData = data.amsTcp.data as AmsRouterStateData 743 | 744 | const state = routerStateData.routerState 745 | 746 | this.debug(`onRouterStateChanged(): Local AMS router state has changed${(this.routerState?.stateStr ? ` from ${this.routerState?.stateStr}` : '')} to ${ADS.AMS_ROUTER_STATE.toString(state)} (${state})`) 747 | 748 | this.routerState = { 749 | state: state, 750 | stateStr: ADS.AMS_ROUTER_STATE.toString(state) 751 | } 752 | 753 | this.emit('routerStateChange', this.routerState) 754 | 755 | this.debug(`onRouterStateChanged(): Local loopback connection active, monitoring router state`) 756 | 757 | if (this.routerState.state === ADS.AMS_ROUTER_STATE.START) { 758 | this.consoleWrite(`WARNING: Local AMS router state has changed to ${ADS.AMS_ROUTER_STATE.toString(state)}. Reconnecting...`) 759 | this.onConnectionLost() 760 | 761 | } else { 762 | //Nothing to do, just wait until router has started again.. 763 | this.consoleWrite(`WARNING: Local AMS router state has changed to ${ADS.AMS_ROUTER_STATE.toString(state)}. Connection might have been lost.`) 764 | } 765 | } 766 | 767 | /** 768 | * Clears given timer if it's available and increases the id 769 | * @param timerObject Timer object {id, timer} 770 | */ 771 | private clearTimer(timerObject: TimerObject) { 772 | //Clearing timer 773 | if(timerObject.timer) 774 | clearTimeout(timerObject.timer) 775 | timerObject.timer = undefined 776 | 777 | //Increasing timer id 778 | timerObject.id = timerObject.id < Number.MAX_SAFE_INTEGER ? timerObject.id + 1 : 0; 779 | } 780 | } -------------------------------------------------------------------------------- /src/ads-server-standalone.ts: -------------------------------------------------------------------------------- 1 | /* 2 | https://github.com/jisotalo/ads-server 3 | 4 | Copyright (c) 2021 Jussi Isotalo 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | import { 26 | Socket, 27 | Server, 28 | createServer, 29 | AddressInfo 30 | } from 'net' 31 | 32 | import { 33 | ServerCore, 34 | ServerException 35 | } from './ads-server-core' 36 | 37 | import { 38 | AddNotificationReqCallback, 39 | GenericReqCallback, 40 | ServerConnection, 41 | StandAloneAdsNotificationTarget, 42 | StandAloneServerConnection, 43 | StandAloneServerSettings 44 | } from './types/ads-server' 45 | 46 | import * as ADS from './ads-commons' 47 | 48 | 49 | /** 50 | * TwinCAT ADS server 51 | * 52 | * This ADS server starts up a server at provided TCP port (default: ADS.ADS_DEFAULT_TCP_PORT = 48898). 53 | * 54 | * Then it listens for incoming ADS commands for **any ADS port** on this AmsNetId. 55 | * 56 | * **Does not require TwinCAT installation/AMS router (for example Raspberry Pi, Linux, etc...).** 57 | * With router, see `Server` class. 58 | */ 59 | export class StandAloneServer extends ServerCore { 60 | 61 | /** 62 | * Active settings 63 | */ 64 | public settings: StandAloneServerSettings = { 65 | listeningTcpPort: ADS.ADS_DEFAULT_TCP_PORT, 66 | localAmsNetId: '' 67 | } 68 | 69 | /** 70 | * Next free available connection ID 71 | */ 72 | private nextConnectionid = 0 73 | 74 | /** 75 | * Object containing all active connections 76 | */ 77 | private connections: Record = {} 78 | 79 | /** 80 | * Socket server instance 81 | */ 82 | private server?: Server = undefined 83 | 84 | /** 85 | * Settings to use are provided as parameter 86 | */ 87 | public constructor(settings: StandAloneServerSettings) { 88 | super(settings) 89 | 90 | //Taking the default settings and then updating the provided ones 91 | this.settings = { 92 | ...this.settings, 93 | ...settings 94 | } 95 | } 96 | 97 | /** 98 | * Setups a listening server and then starts listening for incoming connections 99 | * and ADS commands. 100 | */ 101 | listen(): Promise { 102 | return new Promise(async (resolve, reject) => { 103 | try { 104 | this.debug(`listen(): Creating socket server`) 105 | this.server = createServer(this.onServerConnection.bind(this)) 106 | 107 | //Error during startup 108 | const handleErrorAtStart = (err: Error) => { 109 | this.debug(`listen(): Creating socket server failed: ${err.message}`) 110 | reject(err) 111 | } 112 | this.server.on('error', handleErrorAtStart) 113 | 114 | //Starting to listening 115 | this.server.listen( 116 | this.settings.listeningTcpPort, 117 | this.settings.listeningAddress, () => { 118 | this.debug(`listen(): Listening on ${(this.server?.address() as AddressInfo).port}`) 119 | 120 | this.server?.off('error', handleErrorAtStart) 121 | this.server?.on('error', this.onServerError.bind(this)) 122 | 123 | this.connection = { 124 | connected: true, 125 | localAmsNetId: this.settings.localAmsNetId 126 | } 127 | 128 | resolve(this.connection) 129 | }) 130 | 131 | } catch (err) { 132 | this.connection.connected = false 133 | reject(err) 134 | } 135 | }) 136 | } 137 | 138 | /** 139 | * Stops listening for incoming connections and closes the server. 140 | * If closing fails, throws an error but the server instance is destroyed anyways. 141 | * So the connection is always closed after calling close() 142 | */ 143 | close(): Promise { 144 | return new Promise(async (resolve, reject) => { 145 | this.debug(`close(): Closing socket server`) 146 | 147 | this.connection.connected = false 148 | 149 | if (!this.server) { 150 | return resolve() 151 | } 152 | 153 | try { 154 | this.server.close(err => { 155 | if (err) { 156 | this.debug(`close(): Error during closing server but server is destroyed: ${err.message}`) 157 | this.server = undefined 158 | return reject(err) 159 | } 160 | 161 | this.server?.removeAllListeners() 162 | this.server = undefined 163 | 164 | resolve(err) 165 | }) 166 | 167 | } catch (err) { 168 | reject(err) 169 | this.server = undefined 170 | } 171 | }) 172 | } 173 | 174 | /** 175 | * Sends a given data as notification using given 176 | * notificationHandle and target info. 177 | */ 178 | public sendDeviceNotification(notification: StandAloneAdsNotificationTarget, data: Buffer): Promise { 179 | if (!this.connection.connected) 180 | throw new ServerException(this, 'sendDeviceNotification()', `Server is not active. Use listen() first.`) 181 | 182 | if (!notification.socket) 183 | throw new ServerException(this, 'sendDeviceNotification()', `Required notification.socket is missing.`) 184 | 185 | return this.sendDeviceNotificationToSocket( 186 | notification, 187 | notification.sourceAdsPort, 188 | notification.socket, 189 | data 190 | ) 191 | } 192 | 193 | /** 194 | * Sets callback function to be called when ADS AddNotification request is received 195 | * 196 | * @param callback Callback that is called when request received 197 | * ```js 198 | * onAddNotification(async (req, res) => { 199 | * //do something with req object and then respond 200 | * await res({..}) 201 | * }) 202 | * ``` 203 | */ 204 | public onAddNotification(callback: AddNotificationReqCallback): void { 205 | this.setRequestCallback(ADS.ADS_COMMAND.AddNotification, callback as GenericReqCallback) 206 | } 207 | 208 | /** 209 | * Called when error is thrown at server instance 210 | * 211 | * @param err Error object from this.server 212 | */ 213 | private onServerError(err: Error) { 214 | this.debug(`onServerError(): Socket server error, disconnecting: ${err.message}`) 215 | 216 | this.emit("server-error", err) 217 | 218 | //Closing connection 219 | this.close() 220 | .catch() 221 | } 222 | 223 | /** 224 | * Called on a new connection at server instance 225 | * 226 | * @param socket The new socket connection that was created 227 | */ 228 | private onServerConnection(socket: Socket) { 229 | this.debug(`onServerConnection(): New connection from ${socket.remoteAddress}`) 230 | 231 | //Creating own data object for this connection 232 | const id = this.nextConnectionid++ 233 | 234 | const connection: StandAloneServerConnection = { 235 | id, 236 | socket, 237 | buffer: Buffer.alloc(0) 238 | } 239 | 240 | //Handling incoming data 241 | socket.on('data', (data) => { 242 | if (this.debugIO.enabled) { 243 | this.debugIO(`IO in <------ ${data.byteLength} bytes from ${socket.remoteAddress}: ${data.toString('hex')}`) 244 | } else if (this.debugD.enabled) { 245 | this.debugD(`IO in <------ ${data.byteLength} bytes from ${socket.remoteAddress}`) 246 | } 247 | 248 | //Adding received data to connection buffer and checking the data 249 | connection.buffer = Buffer.concat([connection.buffer, data]) 250 | 251 | this.handleReceivedData( 252 | connection.buffer, 253 | connection.socket, 254 | (newBuffer: Buffer) => connection.buffer = newBuffer 255 | ) 256 | }) 257 | 258 | //Handling socket closing 259 | socket.on("close", (hadError) => { 260 | this.debug(`onServerConnection(): Connection from ${socket?.remoteAddress} was closed (error: ${hadError})`) 261 | 262 | //Removing the connection 263 | socket?.removeAllListeners() 264 | delete this.connections[id] 265 | }) 266 | 267 | socket.on("end", () => { 268 | this.debug(`onServerConnection(): Connection from ${socket?.remoteAddress} was ended`) 269 | 270 | //Removing the connection 271 | socket?.removeAllListeners() 272 | delete this.connections[id] 273 | }) 274 | 275 | 276 | socket.on('error', (err) => { 277 | this.debug(`onServerConnection(): Connection from ${socket.remoteAddress} had error: ${err}`) 278 | }) 279 | 280 | this.connections[id] = connection 281 | } 282 | } -------------------------------------------------------------------------------- /src/ads-server.ts: -------------------------------------------------------------------------------- 1 | /* 2 | https://github.com/jisotalo/ads-server 3 | 4 | Copyright (c) 2021 Jussi Isotalo 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | //Exporting RouterServer as Server for backwards compatibility 26 | export { RouterServer as Server } from './ads-server-router' 27 | export * from './ads-server-standalone' 28 | export * as ADS from './ads-commons' 29 | -------------------------------------------------------------------------------- /src/types/ads-server.ts: -------------------------------------------------------------------------------- 1 | /* 2 | https://github.com/jisotalo/ads-server 3 | 4 | Copyright (c) 2021 Jussi Isotalo 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | import type { Socket } from 'net' 26 | import type { AdsData, AmsTcpPacket } from './ads-types' 27 | 28 | /** AMS packet that has device notification helper object */ 29 | export interface AddNotificationAmsTcpPacket extends AmsTcpPacket { 30 | /** ADS data */ 31 | ads: AddNotificationAdsData 32 | } 33 | 34 | /** ADS data that has device notification helper object */ 35 | export interface AddNotificationAdsData extends AdsData { 36 | /** Device notification target information (helper object) */ 37 | notificationTarget: T 38 | } 39 | 40 | export interface ServerCoreSettings { 41 | /** Optional: Local AmsNetId to use (default: automatic) */ 42 | localAmsNetId?: string, 43 | /** Optional: If true, no warnings are written to console (= nothing is ever written to console) (default: false) */ 44 | hideConsoleWarnings?: boolean, 45 | } 46 | 47 | export interface StandAloneServerSettings extends ServerCoreSettings { 48 | /** Local AmsNetId to use */ 49 | localAmsNetId: string, 50 | /** Optional: Local IP address to use, use this to change used network interface if required (default: '' = automatic) */ 51 | listeningAddress?: string, 52 | /** Optional: Local TCP port to listen for incoming connections (default: 48898) */ 53 | listeningTcpPort?: number 54 | } 55 | 56 | export interface RouterServerSettings extends ServerCoreSettings { 57 | /** Optional: Target ADS router TCP port (default: 48898) */ 58 | routerTcpPort: number, 59 | /** Optional: Target ADS router IP address/hostname (default: '127.0.0.1') */ 60 | routerAddress: string, 61 | /** Optional: Local IP address to use, use this to change used network interface if required (default: '' = automatic) */ 62 | localAddress: string, 63 | /** Optional: Local TCP port to use for outgoing connections (default: 0 = automatic) */ 64 | localTcpPort: number, 65 | /** Optional: Local AmsNetId to use (default: automatic) */ 66 | localAmsNetId: string, 67 | /** Optional: Local ADS port to use (default: automatic/router provides) */ 68 | localAdsPort: number, 69 | /** Optional: Time (milliseconds) after connecting to the router or waiting for command response is canceled to timeout (default: 2000) */ 70 | timeoutDelay: number, 71 | /** Optional: If true and connection to the router is lost, the server tries to reconnect automatically (default: true) */ 72 | autoReconnect: boolean, 73 | /** Optional: Time (milliseconds) how often the lost connection is tried to re-establish (default: 2000) */ 74 | reconnectInterval: number, 75 | } 76 | 77 | export interface StandAloneServerConnection { 78 | /** Connection ID */ 79 | id: number, 80 | /** Connection socket */ 81 | socket: Socket, 82 | /** Connection receive data buffer */ 83 | buffer: Buffer 84 | } 85 | 86 | export interface TimerObject { 87 | /** Timer ID */ 88 | id: number, 89 | /** Timer handle */ 90 | timer?: NodeJS.Timeout 91 | } 92 | 93 | /** 94 | * Generic request callback 95 | * Just tells that we have req, res and packet properties 96 | */ 97 | export type GenericReqCallback = ( 98 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 99 | req: any, 100 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 101 | res: any, 102 | packet?: AmsTcpPacket | AddNotificationAmsTcpPacket | AddNotificationAmsTcpPacket, 103 | adsPort?: number 104 | ) => void 105 | 106 | export interface AmsRouterState { 107 | /** Router state */ 108 | state: number, 109 | /** Router state as string */ 110 | stateStr: string 111 | } 112 | 113 | /** 114 | * Connection info 115 | */ 116 | export interface ServerConnection { 117 | /** Is the server connected to the AMS router (`Server`) 118 | * or is the server listening for incoming connections (`StandAloneServer`)*/ 119 | connected: boolean, 120 | /** Local AmsNetId of the server */ 121 | localAmsNetId: string, 122 | /** Local ADS port of the server (only with `Server`) */ 123 | localAdsPort?: number 124 | } 125 | 126 | /** 127 | * ADS notification target parameters 128 | */ 129 | export interface AdsNotificationTarget { 130 | /** Notification handle (unique for each registered notification) */ 131 | notificationHandle?: number, 132 | /** Target system AmsNetId (that subscribed to notifications) */ 133 | targetAmsNetId: string, 134 | /** Target system ADS port (that subscribed to notifications) */ 135 | targetAdsPort: number, 136 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 137 | [key: string]: any 138 | } 139 | 140 | /** 141 | * ADS notification target parameters for StandAloneServer 142 | */ 143 | export interface StandAloneAdsNotificationTarget extends AdsNotificationTarget { 144 | /** Socket to use for sending data */ 145 | socket: Socket, 146 | /** Source system ADS port */ 147 | sourceAdsPort: number 148 | } 149 | 150 | /** 151 | * Read request callback 152 | */ 153 | export type ReadReqCallback = ( 154 | /** Request data */ 155 | req: ReadReq, 156 | /** Response callback function (async) */ 157 | res: ReadReqResponseCallback, 158 | /** AmsTcp full packet */ 159 | packet?: AmsTcpPacket, 160 | /** ADS port where the request was received */ 161 | adsPort?: number 162 | ) => void 163 | 164 | /** 165 | * ReadWrite request callback 166 | */ 167 | export type ReadWriteReqCallback = ( 168 | /** Request data */ 169 | req: ReadWriteReq, 170 | /** Response callback function (async) */ 171 | res: ReadWriteReqResponseCallback, 172 | /** AmsTcp full packet */ 173 | packet?: AmsTcpPacket, 174 | /** ADS port where the request was received */ 175 | adsPort?: number 176 | ) => void 177 | 178 | /** 179 | * Write request callback 180 | */ 181 | export type WriteReqCallback = ( 182 | /** Request data */ 183 | req: WriteReq, 184 | /** Response callback function (async) */ 185 | res: WriteReqResponseCallback, 186 | /** AmsTcp full packet */ 187 | packet?: AmsTcpPacket, 188 | /** ADS port where the request was received */ 189 | adsPort?: number 190 | ) => void 191 | 192 | /** 193 | * ReadDevice request callback 194 | */ 195 | export type ReadDeviceInfoReqCallback = ( 196 | /** Request data (empty object) */ 197 | req: Record, 198 | /** Response callback function (async) */ 199 | res: ReadDeviceInfoReqResponseCallback, 200 | /** AmsTcp full packet */ 201 | packet?: AmsTcpPacket, 202 | /** ADS port where the request was received */ 203 | adsPort?: number 204 | ) => void 205 | 206 | /** 207 | * ReadState request callback 208 | */ 209 | export type ReadStateReqCallback = ( 210 | /** Request data (empty object) */ 211 | req: Record, 212 | /** Response callback function (async) */ 213 | res: ReadStateReqResponseCallback, 214 | /** AmsTcp full packet */ 215 | packet?: AmsTcpPacket, 216 | /** ADS port where the request was received */ 217 | adsPort?: number 218 | ) => void 219 | 220 | /** 221 | * AddNotification request callback 222 | */ 223 | export type AddNotificationReqCallback = ( 224 | /** Request data */ 225 | req: AddNotificationReq, 226 | /** Response callback function (async) */ 227 | res: AddNotificationReqResponseCallback, 228 | /** AmsTcp full packet */ 229 | packet?: AddNotificationAmsTcpPacket, 230 | /** ADS port where the request was received */ 231 | adsPort?: number 232 | ) => void 233 | 234 | /** 235 | * DeleteNotification request callback 236 | */ 237 | export type DeleteNotificationReqCallback = ( 238 | /** Request data */ 239 | req: DeleteNotificationReq, 240 | /** Response callback function (async) */ 241 | res: DeleteNotificationReqResponseCallback, 242 | /** AmsTcp full packet */ 243 | packet?: AmsTcpPacket, 244 | /** ADS port where the request was received */ 245 | adsPort?: number 246 | ) => void 247 | 248 | /** 249 | * WriteControl request callback 250 | */ 251 | export type WriteControlReqCallback = ( 252 | /** Request data */ 253 | req: WriteControlReq, 254 | /** Response callback function (async) */ 255 | res: WriteControlReqResponseCallback, 256 | /** AmsTcp full packet */ 257 | packet?: AmsTcpPacket, 258 | /** ADS port where the request was received */ 259 | adsPort?: number 260 | ) => void 261 | 262 | /** ADS request type (any of these) */ 263 | export type AdsRequest = 264 | | EmptyReq 265 | | UnknownAdsRequest 266 | | ReadReq 267 | | ReadWriteReq 268 | | WriteReq 269 | | AddNotificationReq 270 | | DeleteNotificationReq 271 | | WriteControlReq 272 | 273 | /** 274 | * Unknown ads request 275 | */ 276 | export interface UnknownAdsRequest { 277 | error: boolean, 278 | errorStr: string, 279 | errorCode: number 280 | } 281 | 282 | /** 283 | * Empty ads request (no payload) 284 | */ 285 | export type EmptyReq = { 286 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 287 | [K in any]: never //allow only empty object 288 | } 289 | 290 | /** 291 | * Read request data 292 | */ 293 | export interface ReadReq { 294 | /** Index group the read command is targeted to*/ 295 | indexGroup: number, 296 | /** Index offset the read command is targeted to*/ 297 | indexOffset: number, 298 | /** Requested read data length (bytes)*/ 299 | readLength: number 300 | } 301 | 302 | /** 303 | * ReadWrite request data 304 | */ 305 | export interface ReadWriteReq { 306 | /** Index group the read command is targeted to*/ 307 | indexGroup: number, 308 | /** Index offset the read command is targeted to*/ 309 | indexOffset: number, 310 | /** Requested read data length (bytes)*/ 311 | readLength: number, 312 | /** Write data length (bytes), should be same as data.byteLength*/ 313 | writeLength: number, 314 | /** Data to write (Buffer)*/ 315 | data: Buffer 316 | } 317 | 318 | export interface WriteReq { 319 | /** Index group the write command is targeted to*/ 320 | indexGroup: number, 321 | /** Index offset the write command is targeted to*/ 322 | indexOffset: number, 323 | /** Write data length (bytes), should be same as data.byteLength*/ 324 | writeLength: number, 325 | /** Data to write (Buffer)*/ 326 | data: Buffer 327 | } 328 | 329 | export interface AddNotificationReq { 330 | /** Index group the notification request is targeted to*/ 331 | indexGroup: number, 332 | /** Index offset the notification request is targeted to*/ 333 | indexOffset: number, 334 | /** Data length (bytes) - how much data is wanted to get every notification*/ 335 | dataLength: number, 336 | /** ADS notification transmission mode */ 337 | transmissionMode: number, 338 | /** ADS notification transmission mode as string */ 339 | transmissionModeStr: string, 340 | /** Maximum delay (ms) */ 341 | maximumDelay: number, 342 | /** How often the value is checked or sent, depends on the transmissionMode (ms) */ 343 | cycleTime: number, 344 | /** Helper object that can be used to send notifications - NOTE: notificationHandle is empty*/ 345 | notificationTarget: AdsNotificationTarget 346 | /** Reserved for future use */ 347 | reserved?: Buffer 348 | } 349 | 350 | export interface DeleteNotificationReq { 351 | /** Notification unique handle */ 352 | notificationHandle: number 353 | } 354 | 355 | export interface WriteControlReq { 356 | /** ADS state requested */ 357 | adsState: number, 358 | /** ADS state requested as string */ 359 | adsStateStr: string, 360 | /** Device state requested */ 361 | deviceState: number, 362 | /** Length of the data (should be same as data.byteLength) */ 363 | dataLen: number, 364 | /** Data (Buffer)*/ 365 | data: Buffer 366 | } 367 | 368 | /** 369 | * Response callback function 370 | */ 371 | export type ReadReqResponseCallback = ( 372 | /** Data to be responsed */ 373 | response: ReadReqResponse | BaseResponse 374 | ) => Promise 375 | 376 | /** 377 | * Response callback function 378 | */ 379 | export type ReadWriteReqResponseCallback = ( 380 | /** Data to be responsed */ 381 | response: ReadWriteReqResponse | BaseResponse 382 | ) => Promise 383 | 384 | /** 385 | * Response callback function 386 | */ 387 | export type WriteReqResponseCallback = ( 388 | /** Data to be responsed */ 389 | response: BaseResponse 390 | ) => Promise 391 | 392 | /** 393 | * Response callback function 394 | */ 395 | export type ReadDeviceInfoReqResponseCallback = ( 396 | /** Data to be responsed */ 397 | response: ReadDeviceInfoReqResponse | BaseResponse 398 | ) => Promise 399 | 400 | /** 401 | * Response callback function 402 | */ 403 | export type ReadStateReqResponseCallback = ( 404 | /** Data to be responsed */ 405 | response: ReadStateReqResponse | BaseResponse 406 | ) => Promise 407 | 408 | /** 409 | * Response callback function 410 | */ 411 | export type AddNotificationReqResponseCallback = ( 412 | /** Data to be responsed */ 413 | response: AddNotificationReqResponse | BaseResponse 414 | ) => Promise 415 | 416 | /** 417 | * Response callback function 418 | */ 419 | export type DeleteNotificationReqResponseCallback = ( 420 | /** Data to be responsed */ 421 | response: BaseResponse 422 | ) => Promise 423 | 424 | /** 425 | * Response callback function 426 | */ 427 | export type WriteControlReqResponseCallback = ( 428 | /** Data to be responsed */ 429 | response: BaseResponse 430 | ) => Promise 431 | 432 | /** 433 | * Base response, every response has this 434 | */ 435 | export interface BaseResponse { 436 | /** ADS/custom error code (if any), can be omitted if no error (default is 0 = no error) */ 437 | error?: number 438 | } 439 | 440 | /** 441 | * Read request response 442 | */ 443 | export interface ReadReqResponse extends BaseResponse { 444 | /** Data to be responded (Buffer) - can be omitted if nothing to respond */ 445 | data?: Buffer 446 | } 447 | 448 | /** 449 | * ReadWrite request response 450 | */ 451 | export interface ReadWriteReqResponse extends BaseResponse { 452 | /** Data to be responded (Buffer) - can be omitted if nothing to respond */ 453 | data?: Buffer 454 | } 455 | 456 | /** 457 | * ReadDeviceInfo request response 458 | */ 459 | export interface ReadDeviceInfoReqResponse extends BaseResponse { 460 | /** Major version number */ 461 | majorVersion?: number, 462 | /** Minor version number */ 463 | minorVersion?: number, 464 | /** Build version */ 465 | versionBuild?: number, 466 | /** Device name */ 467 | deviceName?: string 468 | } 469 | 470 | /** 471 | * ReadState request response 472 | */ 473 | export interface ReadStateReqResponse extends BaseResponse { 474 | /** ADS state */ 475 | adsState?: number, 476 | /** Device state */ 477 | deviceState?: number 478 | } 479 | 480 | /** 481 | * AddNotification request response 482 | */ 483 | export interface AddNotificationReqResponse extends BaseResponse { 484 | /** Notification unique handle */ 485 | notificationHandle?: number 486 | } -------------------------------------------------------------------------------- /src/types/ads-types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | https://github.com/jisotalo/ads-server 3 | 4 | Copyright (c) 2021 Jussi Isotalo 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | /** AMS packet */ 26 | export interface AmsTcpPacket { 27 | /** AMS TCP header */ 28 | amsTcp: AmsTcpHeader, 29 | /** AMS header */ 30 | ams: AmsHeader, 31 | /** ADS data */ 32 | ads: AdsData 33 | } 34 | 35 | /** AMS TCP header */ 36 | export interface AmsTcpHeader { 37 | /** AMS command as number */ 38 | command: number 39 | /** AMS command as enumerated string */ 40 | commandStr: string, 41 | /** AMS data length (bytes) */ 42 | dataLength: number, 43 | /** AMS data (if available - only in certain commands) */ 44 | data: null | Buffer | AmsRouterStateData | AmsPortRegisteredData 45 | } 46 | 47 | /** AMS header */ 48 | export interface AmsHeader { 49 | /** Target AmsNetId (receiver) */ 50 | targetAmsNetId: string, 51 | /** Target ADS port (receiver) */ 52 | targetAdsPort: number, 53 | /** Source AmsNetId (sender) */ 54 | sourceAmsNetId: string, 55 | /** Source ADS port (sender) */ 56 | sourceAdsPort: number, 57 | /** ADS command as number */ 58 | adsCommand: number, 59 | /** ADS command as enumerated string */ 60 | adsCommandStr: string, 61 | /** ADS state flags as number (bits) */ 62 | stateFlags: number, 63 | /** ADS state flags as comma separated string */ 64 | stateFlagsStr: string, 65 | /** ADS data length */ 66 | dataLength: number, 67 | /** ADS error code */ 68 | errorCode: number, 69 | /** Command invoke ID */ 70 | invokeId: number, 71 | /** True if error */ 72 | error: boolean, 73 | /** Error message as string */ 74 | errorStr: string 75 | } 76 | 77 | /** ADS data */ 78 | export interface AdsData { 79 | /** Raw ADS data as Buffer */ 80 | rawData?: Buffer, 81 | /** Any other value, custom for each command. TODO: Perhaps custom types? */ 82 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 83 | [key: string]: any 84 | } 85 | 86 | /** ADS command that is sent to the target (whole packet is built from this) */ 87 | export interface AdsCommandToSend { 88 | /** Ads command as number */ 89 | adsCommand: number, 90 | /** Target AmsNetId (receiver) */ 91 | targetAmsNetId: string, 92 | /** Target ADS port (receiver) */ 93 | targetAdsPort: number, 94 | /** Source AmsNetId (sender) */ 95 | sourceAmsNetId: string, 96 | /** Source ADS port (sender) */ 97 | sourceAdsPort: number, 98 | /** Invoke ID to use */ 99 | invokeId: number, 100 | /** Raw data to be sent as Buffer */ 101 | rawData: Buffer 102 | } 103 | 104 | /** Data that is received when AMS router state changes */ 105 | export interface AmsRouterStateData { 106 | /** New router state as number */ 107 | routerState: number 108 | } 109 | 110 | /** Data that is received when AMS port is registered to router */ 111 | export interface AmsPortRegisteredData { 112 | /** Local registered AmsNetId */ 113 | localAmsNetId: string, 114 | /** Local registered ADS port */ 115 | localAdsPort: number, 116 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | //"allowJs": false, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | //"sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist", /* Redirect output structure to the directory. */ 18 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | 44 | /* Module Resolution Options */ 45 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | 66 | /* Advanced Options */ 67 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 68 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 69 | } 70 | } 71 | --------------------------------------------------------------------------------