├── .eslintignore ├── .prettierignore ├── .npmignore ├── tests ├── .DS_Store ├── Client.test.ts ├── tsconfig.json ├── setup.ts ├── uasc │ ├── SymmetricSecurityHeader.test.ts │ ├── SequenceHeader.test.ts │ ├── SecureConversationMessageHeader.test.ts │ ├── AsymmetricSecurityHeader.test.ts │ └── Message.test.ts ├── utils │ └── date.test.ts ├── ua │ ├── AnonymousIdentityToken.test.ts │ ├── SignatureData.test.ts │ ├── X509IdentityToken.test.ts │ ├── ChannelSecurityToken.test.ts │ ├── QualifiedName.test.ts │ ├── ReadValueId.test.ts │ ├── SignedSoftwareCertificate.test.ts │ ├── IssuedIdentityToken.test.ts │ ├── UserNameIdentityToken.test.ts │ ├── Guid.test.ts │ ├── UserTokenPolicy.test.ts │ ├── run.ts │ ├── CloseSessionResponse.test.ts │ ├── CancelResponse.test.ts │ ├── CloseSecureChannelResponse.test.ts │ ├── OpenSecureChannelRequest.test.ts │ ├── ActivateSessionResponse.test.ts │ ├── OpenSecureChannelResponse.test.ts │ ├── RequestHeader.test.ts │ ├── CreateSubscriptionResponse.test.ts │ ├── ExtensionObject.test.ts │ ├── LocalizedText.test.ts │ ├── CancelRequest.test.ts │ ├── CloseSessionRequest.test.ts │ ├── CloseSecureChannelRequest.test.ts │ ├── ExpandedNodeId.test.ts │ ├── WriteValue.test.ts │ ├── ActivateSessionRequest.test.ts │ ├── FindServersOnNetworkRequest.test.ts │ ├── ReadResponse.test.ts │ ├── ApplicationDescription.test.ts │ ├── ResponseHeader.test.ts │ ├── ServerOnNetwork.test.ts │ ├── GetEndpointsRequest.test.ts │ ├── FindServersRequest.test.ts │ ├── CreateSubscriptionRequest.test.ts │ ├── ReadRequest.test.ts │ ├── WriteResponse.test.ts │ ├── WriteRequest.test.ts │ ├── CreateSessionRequest.test.ts │ ├── NodeId.test.ts │ ├── FindServersResponse.test.ts │ ├── EndpointDescription.test.ts │ ├── DiagnosticInfo.test.ts │ ├── FindServersOnNetworkResponse.test.ts │ ├── Variant.test.ts │ ├── encode.test.ts │ └── Bucket.test.ts └── uacp │ ├── MessageHeader.test.ts │ ├── AcknowledgeMessage.test.ts │ └── HelloMessage.test.ts ├── Makefile ├── prettier.config.js ├── .gitignore ├── src ├── uacp │ ├── README.md │ ├── ConnectionProtocolMessageHeader.ts │ ├── WebSocket.ts │ ├── AcknowledgeMessage.ts │ ├── HelloMessage.ts │ └── Connection.ts ├── uasc │ ├── README.md │ ├── ChunkType.ts │ ├── MessageType.ts │ ├── SymmetricSecurityHeader.ts │ ├── SequenceHeader.ts │ ├── AsymmetricSecurityHeader.ts │ ├── SecureConversationMessageHeader.ts │ └── Message.ts ├── ua │ ├── utils.ts │ ├── SecurityTokenRequestType.ts │ ├── QualifiedName.ts │ ├── service.ts │ ├── guards.ts │ ├── LocalizedText.ts │ ├── ExtensionObject.ts │ ├── Guid.ts │ ├── enums.ts │ ├── ExpandedNodeId.ts │ ├── factory.ts │ ├── encode.ts │ ├── DiagnosticInfo.ts │ ├── DataValue.ts │ └── decode.ts ├── tsconfig.json ├── utils │ └── date.ts ├── types.ts ├── Subscription.ts └── Client.ts ├── tsconfig.json ├── demo ├── tsconfig.json ├── README.md ├── package.json ├── webpack.config.js └── src │ ├── context.tsx │ ├── index.html │ ├── references.tsx │ ├── style.scss │ ├── Breadcrumb.tsx │ ├── Finder.tsx │ ├── attributes.tsx │ ├── index.tsx │ ├── ListGroup.tsx │ └── icons.tsx ├── .eslintrc.js ├── schema └── update.sh ├── .github └── workflows │ └── workflow.yml ├── LICENSE.md ├── cmd ├── id │ └── main.go └── status │ └── main.go ├── package.json ├── README.md └── tsconfig.base.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | compiled 3 | demo 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | compiled 3 | coverage 4 | src/id/id.ts -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | cmd 2 | demo 3 | doc 4 | .github 5 | coverage 6 | src 7 | -------------------------------------------------------------------------------- /tests/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HBM/opcua/HEAD/tests/.DS_Store -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | .PHONY: proxy 3 | proxy: 4 | websockify localhost:1234 localhost:4840 5 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true, 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .vscode/ 3 | server 4 | dist/ 5 | compiled/ 6 | node_modules/ 7 | coverage 8 | -------------------------------------------------------------------------------- /src/uacp/README.md: -------------------------------------------------------------------------------- 1 | 2 | # OPC UA Connection Protocol 3 | 4 | https://reference.opcfoundation.org/v104/Core/docs/Part6/7.1.1/ -------------------------------------------------------------------------------- /src/uasc/README.md: -------------------------------------------------------------------------------- 1 | 2 | # OPC UA Secure Conversation 3 | 4 | https://reference.opcfoundation.org/v104/Core/docs/Part6/6.7.1/ -------------------------------------------------------------------------------- /src/uasc/ChunkType.ts: -------------------------------------------------------------------------------- 1 | export const ChunkTypeIntermediate = 'C' 2 | export const ChunkTypeFinal = 'F' 3 | export const ChunkTypeError = 'A' 4 | -------------------------------------------------------------------------------- /src/uasc/MessageType.ts: -------------------------------------------------------------------------------- 1 | export const MessageTypeMessage = 'MSG' 2 | export const MessageTypeOpenSecureChannel = 'OPN' 3 | export const MessageTypeCloseSecureChannel = 'CLO' 4 | -------------------------------------------------------------------------------- /src/ua/utils.ts: -------------------------------------------------------------------------------- 1 | export const isPrimitiveType = (name: string): boolean => 2 | name === 'string' || 3 | name === 'uint8' || 4 | name === 'uint16' || 5 | name === 'uint32' 6 | -------------------------------------------------------------------------------- /tests/Client.test.ts: -------------------------------------------------------------------------------- 1 | // import Client from './Client' 2 | 3 | describe('Client', () => { 4 | test('constructor', () => { 5 | // new Client('ws://foo:1234') 6 | expect(1).toBe(1) 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../compiled", 5 | "rootDir": "./" 6 | }, 7 | "references": [{ "path": "../src" }] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./src" 6 | }, 7 | { 8 | "path": "./tests" 9 | }, 10 | { 11 | "path": "./demo" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "react", 5 | "outDir": "./dist", 6 | "rootDir": "./" 7 | }, 8 | "references": [{ "path": "../src" }] 9 | } 10 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | // "declaration": true, 5 | // "declarationMap": true, 6 | "outDir": "../dist", 7 | "rootDir": "./" 8 | // "composite": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/setup.ts: -------------------------------------------------------------------------------- 1 | import { TextEncoder, TextDecoder } from 'util' 2 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 3 | ;(global as any).TextEncoder = TextEncoder 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | ;(global as any).TextDecoder = TextDecoder 6 | -------------------------------------------------------------------------------- /src/ua/SecurityTokenRequestType.ts: -------------------------------------------------------------------------------- 1 | import { SecurityTokenRequestType } from '../types' 2 | 3 | // https://reference.opcfoundation.org/v104/Core/DataTypes/SecurityTokenRequestType/ 4 | export const SecurityTokenRequestTypeIssue: SecurityTokenRequestType = 0x0 5 | export const SecurityTokenRequestTypeRenew: SecurityTokenRequestType = 0x1 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | }, 5 | // root: true, 6 | parser: '@typescript-eslint/parser', 7 | plugins: ['@typescript-eslint'], 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:@typescript-eslint/eslint-recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | 'prettier/@typescript-eslint', 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/date.ts: -------------------------------------------------------------------------------- 1 | export const getDateTime = (dv: DataView, offset: number): Date => { 2 | const value = dv.getBigInt64(offset, true) 3 | const d = new Date(Number(value - BigInt(116444736000000000)) / 1e4) 4 | return new Date( 5 | d.getUTCFullYear(), 6 | d.getUTCMonth(), 7 | d.getUTCDate(), 8 | d.getUTCHours(), 9 | d.getUTCMinutes(), 10 | d.getUTCSeconds(), 11 | d.getUTCMilliseconds() 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /tests/uasc/SymmetricSecurityHeader.test.ts: -------------------------------------------------------------------------------- 1 | import run from '../ua/run' 2 | import SymmetricSecurityHeader from '../../dist/uasc/SymmetricSecurityHeader' 3 | 4 | describe('SymmetricSecurityHeader', () => { 5 | run([ 6 | { 7 | name: 'normal', 8 | instance: new SymmetricSecurityHeader({ 9 | TokenId: 0x11223344, 10 | }), 11 | bytes: new Uint8Array([0x44, 0x33, 0x22, 0x11]), 12 | }, 13 | ]) 14 | }) 15 | -------------------------------------------------------------------------------- /schema/update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | curl https://raw.githubusercontent.com/OPCFoundation/UA-Nodeset/master/Schema/NodeIds.csv --output NodeIds.csv 8 | curl https://raw.githubusercontent.com/OPCFoundation/UA-Nodeset/master/Schema/StatusCode.csv --output StatusCode.csv 9 | curl https://raw.githubusercontent.com/OPCFoundation/UA-Nodeset/master/Schema/Opc.Ua.Types.bsd --output Opc.Ua.Types.bsd -------------------------------------------------------------------------------- /src/ua/QualifiedName.ts: -------------------------------------------------------------------------------- 1 | import { Type } from './generated' 2 | import { uint16 } from '../types' 3 | 4 | export default class QualifiedName { 5 | @Type('uint16') 6 | public NamespaceIndex: uint16 7 | 8 | @Type('string') 9 | public Name: string 10 | 11 | constructor(options?: { NamespaceIndex?: uint16; Name?: string }) { 12 | this.NamespaceIndex = options?.NamespaceIndex ?? 0 13 | this.Name = options?.Name ?? '' 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/utils/date.test.ts: -------------------------------------------------------------------------------- 1 | import { getDateTime } from '../../dist/utils/date' 2 | // import { getDateTime } from './date' 3 | 4 | describe('date', () => { 5 | test('decode', () => { 6 | // prettier-ignore 7 | const expected = new Uint8Array([ 8 | 0x00, 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01 9 | ]) 10 | const dv = new DataView(expected.buffer) 11 | const date = getDateTime(dv, 0) 12 | expect(date).toEqual(new Date(2018, 7, 10, 23, 0, 0, 0)) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /tests/uasc/SequenceHeader.test.ts: -------------------------------------------------------------------------------- 1 | import run from '../ua/run' 2 | import SequenceHeader from '../../dist/uasc/SequenceHeader' 3 | 4 | describe('SequenceHeader', () => { 5 | run([ 6 | { 7 | name: 'normal', 8 | instance: new SequenceHeader({ 9 | SequenceNumber: 51, 10 | RequestId: 1, 11 | }), 12 | // prettier-ignore 13 | bytes: new Uint8Array([ 14 | 0x33, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00 15 | ]) 16 | }, 17 | ]) 18 | }) 19 | -------------------------------------------------------------------------------- /tests/ua/AnonymousIdentityToken.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { AnonymousIdentityToken } from '../../dist/ua/generated' 3 | 4 | describe('AnonymousIdentityToken', () => { 5 | run([ 6 | { 7 | name: 'normal', 8 | instance: new AnonymousIdentityToken({ 9 | PolicyId: 'anonymous', 10 | }), 11 | // prettier-ignore 12 | bytes: new Uint8Array([ 13 | 0x09, 0x00, 0x00, 0x00, 0x61, 0x6e, 0x6f, 0x6e, 14 | 0x79, 0x6d, 0x6f, 0x75, 0x73, 15 | ]) 16 | }, 17 | ]) 18 | }) 19 | -------------------------------------------------------------------------------- /tests/ua/SignatureData.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { SignatureData } from '../../dist/ua/generated' 3 | 4 | describe('SignatureData', () => { 5 | run([ 6 | { 7 | name: 'normal', 8 | instance: new SignatureData({ 9 | Algorithm: 'alg', 10 | Signature: new Uint8Array([0xde, 0xad, 0xbe, 0xef]), 11 | }), 12 | // prettier-ignore 13 | bytes: new Uint8Array([ 14 | 0x03, 0x00, 0x00, 0x00, 0x61, 0x6c, 0x67, 0x04, 15 | 0x00, 0x00, 0x00, 0xde, 0xad, 0xbe, 0xef, 16 | ]) 17 | }, 18 | ]) 19 | }) 20 | -------------------------------------------------------------------------------- /tests/uacp/MessageHeader.test.ts: -------------------------------------------------------------------------------- 1 | import run from '../ua/run' 2 | import ConnectionProtocolMessageHeader from '../../dist/uacp/ConnectionProtocolMessageHeader' 3 | 4 | describe('MessageHeader', () => { 5 | run([ 6 | { 7 | name: 'normal', 8 | instance: new ConnectionProtocolMessageHeader({ 9 | MessageType: 'ACK', 10 | ChunkType: 'F', 11 | MessageSize: 28, 12 | }), 13 | // prettier-ignore 14 | bytes: new Uint8Array([ 15 | 0x41, 0x43, 0x4b, 0x46, 0x1c, 0x00, 0x00, 0x00, 16 | ]) 17 | }, 18 | ]) 19 | }) 20 | -------------------------------------------------------------------------------- /tests/uasc/SecureConversationMessageHeader.test.ts: -------------------------------------------------------------------------------- 1 | import run from '../ua/run' 2 | import SecureConversationMessageHeader from '../../dist/uasc/SecureConversationMessageHeader' 3 | 4 | describe('Header', () => { 5 | run([ 6 | { 7 | name: 'normal', 8 | instance: new SecureConversationMessageHeader({ 9 | MessageType: 'OPN', 10 | IsFinal: 'F', 11 | MessageSize: 132, 12 | SecureChannelId: 0, 13 | }), 14 | // prettier-ignore 15 | bytes: new Uint8Array([ 16 | 0x4f, 0x50, 0x4e, 0x46, 0x84, 0x00, 0x00, 0x00, 17 | 0x00, 0x00, 0x00, 0x00 18 | ]) 19 | }, 20 | ]) 21 | }) 22 | -------------------------------------------------------------------------------- /tests/ua/X509IdentityToken.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { X509IdentityToken } from '../../dist/ua/generated' 3 | 4 | describe('X509IdentityToken', () => { 5 | const encoder = new TextEncoder() 6 | run([ 7 | { 8 | name: '', 9 | instance: new X509IdentityToken({ 10 | PolicyId: 'x509', 11 | CertificateData: encoder.encode('certificate'), 12 | }), 13 | // prettier-ignore 14 | bytes: new Uint8Array([ 15 | 0x04, 0x00, 0x00, 0x00, 0x78, 0x35, 0x30, 0x39, 16 | 0x0b, 0x00, 0x00, 0x00, 0x63, 0x65, 0x72, 0x74, 17 | 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 18 | ]) 19 | }, 20 | ]) 21 | }) 22 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | 2 | # demo 3 | 4 | The demo client uses https://github.com/open62541/open62541 as the server. 5 | 6 | ## Usage 7 | 8 | 1. Copy `server.c` into `open62541/examples` 9 | 1. Add `add_example(server server.c)` to `examples/CMakeLists.txt` 10 | 1. Create a build directory for the examples `mkdir build` 11 | 1. Step into the build directory `cd build` 12 | 1. Run `cmake ..` 13 | 1. Compile `make -j` 14 | 1. Start the server `./bin/examples/server` 15 | 16 | ## What's included? 17 | 18 | - Hello 19 | - Open Secure Channel 20 | - Create Session 21 | - Activate Session 22 | - Browse 23 | - Create Subscription 24 | - Create Monitored Item 25 | - Call Method 26 | - Events (coming soon) 27 | -------------------------------------------------------------------------------- /tests/ua/ChannelSecurityToken.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { ChannelSecurityToken } from '../../dist/ua/generated' 3 | 4 | describe('ChannelSecurityToken', () => { 5 | run([ 6 | { 7 | name: 'normal', 8 | instance: new ChannelSecurityToken({ 9 | ChannelId: 1, 10 | TokenId: 2, 11 | CreatedAt: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 12 | RevisedLifetime: 6000000, 13 | }), 14 | // prettier-ignore 15 | bytes: new Uint8Array([ 16 | 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 17 | 0x00, 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 18 | 0x80, 0x8d, 0x5b, 0x00, 19 | ]) 20 | }, 21 | ]) 22 | }) 23 | -------------------------------------------------------------------------------- /src/uasc/SymmetricSecurityHeader.ts: -------------------------------------------------------------------------------- 1 | import Bucket from '../ua/Bucket' 2 | import { uint32 } from '../types' 3 | 4 | interface Options { 5 | TokenId?: uint32 6 | } 7 | 8 | export default class SymmetricSecurityHeader { 9 | public TokenId: uint32 10 | 11 | constructor(options?: Options) { 12 | this.TokenId = options?.TokenId ?? 0 13 | } 14 | 15 | public encode(): ArrayBuffer { 16 | const bucket = new Bucket() 17 | bucket.writeUint32(this.TokenId) 18 | return bucket.bytes 19 | } 20 | 21 | public decode(b: ArrayBuffer, position?: number): number { 22 | const bucket = new Bucket(b, position) 23 | this.TokenId = bucket.readUint32() 24 | return bucket.position 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/uacp/AcknowledgeMessage.test.ts: -------------------------------------------------------------------------------- 1 | import run from '../ua/run' 2 | import AcknowledgeMessage from '../../dist/uacp/AcknowledgeMessage' 3 | 4 | describe('AcknowledgeMessage', () => { 5 | run([ 6 | { 7 | name: 'normal', 8 | instance: new AcknowledgeMessage({ 9 | ProtocolVersion: 0, 10 | ReceiveBufferSize: 65535, 11 | SendBufferSize: 65535, 12 | MaxMessageSize: 0, 13 | MaxChunkCount: 0, 14 | }), 15 | // prettier-ignore 16 | bytes: new Uint8Array([ 17 | 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 18 | 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 19 | 0x00, 0x00, 0x00, 0x00 20 | ]) 21 | }, 22 | ]) 23 | }) 24 | -------------------------------------------------------------------------------- /tests/ua/QualifiedName.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import QualifiedName from '../../dist/ua/QualifiedName' 3 | 4 | describe('QualifiedName', () => { 5 | run([ 6 | { 7 | name: 'normal', 8 | instance: new QualifiedName({ 9 | NamespaceIndex: 1, 10 | Name: 'foobar', 11 | }), 12 | // prettier-ignore 13 | bytes: new Uint8Array([ 14 | 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x66, 0x6f, 15 | 0x6f, 0x62, 0x61, 0x72, 16 | ]) 17 | }, 18 | { 19 | name: 'empty', 20 | instance: new QualifiedName({ 21 | NamespaceIndex: 1, 22 | }), 23 | // prettier-ignore 24 | bytes: new Uint8Array([ 25 | 0x01, 0x00, 0xff, 0xff, 0xff, 0xff, 26 | ]) 27 | }, 28 | ]) 29 | }) 30 | -------------------------------------------------------------------------------- /tests/ua/ReadValueId.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { ReadValueId } from '../../dist/ua/generated' 3 | import { NewFourByteNodeId } from '../../dist/ua/NodeId' 4 | import { AttributeId } from '../../dist/ua/enums' 5 | import QualifiedName from '../../dist/ua/QualifiedName' 6 | 7 | describe('ReadValueId', () => { 8 | run([ 9 | { 10 | name: 'normal', 11 | instance: new ReadValueId({ 12 | NodeId: NewFourByteNodeId(0, 2256), 13 | AttributeId: AttributeId.Value, 14 | DataEncoding: new QualifiedName(), 15 | }), 16 | // prettier-ignore 17 | bytes: new Uint8Array([ 18 | 0x01, 0x00, 0xd0, 0x08, 0x0d, 0x00, 0x00, 0x00, 19 | 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 20 | 0xff, 0xff, 21 | ]) 22 | }, 23 | ]) 24 | }) 25 | -------------------------------------------------------------------------------- /tests/ua/SignedSoftwareCertificate.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { SignedSoftwareCertificate } from '../../dist/ua/generated' 3 | 4 | describe('SignedSoftwareCertificate', () => { 5 | run([ 6 | { 7 | name: 'empty', 8 | instance: new SignedSoftwareCertificate(), 9 | bytes: new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), 10 | }, 11 | { 12 | name: 'dummy', 13 | instance: new SignedSoftwareCertificate({ 14 | CertificateData: new Uint8Array([0xca, 0xfe]), 15 | Signature: new Uint8Array([0xde, 0xad, 0xbe, 0xef]), 16 | }), 17 | // prettier-ignore 18 | bytes: new Uint8Array([ 19 | 0x02, 0x00, 0x00, 0x00, 0xca, 0xfe, 0x04, 0x00, 20 | 0x00, 0x00, 0xde, 0xad, 0xbe, 0xef, 21 | ]) 22 | }, 23 | ]) 24 | }) 25 | -------------------------------------------------------------------------------- /tests/ua/IssuedIdentityToken.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { IssuedIdentityToken } from '../../dist/ua/generated' 3 | 4 | describe('IssuedIdentityToken', () => { 5 | run([ 6 | { 7 | name: 'normal', 8 | instance: new IssuedIdentityToken({ 9 | PolicyId: 'issued', 10 | // prettier-ignore 11 | TokenData: new Uint8Array([ 12 | 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64 13 | ]), 14 | EncryptionAlgorithm: 'plain', 15 | }), 16 | // prettier-ignore 17 | bytes: new Uint8Array([ 18 | 0x06, 0x00, 0x00, 0x00, 0x69, 0x73, 0x73, 0x75, 19 | 0x65, 0x64, 0x08, 0x00, 0x00, 0x00, 0x70, 0x61, 20 | 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x05, 0x00, 21 | 0x00, 0x00, 0x70, 0x6c, 0x61, 0x69, 0x6e, 22 | ]) 23 | }, 24 | ]) 25 | }) 26 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: '13.8' 15 | - run: npm ci 16 | - run: npm run tsc 17 | - run: npm run prettier:ci 18 | - run: npm run eslint 19 | - run: npm test 20 | 21 | - name: post coverage 22 | env: 23 | SERIESCI_TOKEN: ${{ secrets.SERIESCI_TOKEN }} 24 | run: | 25 | npm t -- --coverage --coverageReporters="text-summary" | grep Statements | awk '{print $3}' | xargs -I {} curl \ 26 | --header "Authorization: Token ${SERIESCI_TOKEN}" \ 27 | --data-urlencode value="{}" \ 28 | --data sha="$(git rev-parse HEAD)" \ 29 | https://seriesci.com/api/HBM/opcua/coverage/one 30 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type int8 = number 2 | export type uint8 = number 3 | export type int16 = number 4 | export type uint16 = number 5 | export type int32 = number 6 | export type uint32 = number 7 | export type int64 = bigint 8 | export type uint64 = bigint 9 | export type float32 = number 10 | export type float64 = number 11 | export type ByteString = Uint8Array 12 | 13 | export type TypedArray = Uint8Array | Uint16Array | Uint32Array | Float64Array 14 | 15 | // export type StatusCode = uint32 16 | export type SecurityTokenRequestType = uint32 17 | export type MessageSecurityMode = uint32 18 | export type ApplicationType = uint32 19 | 20 | export interface Encoder { 21 | encode(): ArrayBuffer 22 | } 23 | 24 | export interface Decoder { 25 | decode(b: ArrayBuffer, position?: number): number 26 | } 27 | 28 | export interface EnDecoder { 29 | encode(): ArrayBuffer 30 | decode(b: ArrayBuffer, position?: number): number 31 | } 32 | -------------------------------------------------------------------------------- /tests/uacp/HelloMessage.test.ts: -------------------------------------------------------------------------------- 1 | import run from '../ua/run' 2 | import HelloMessage from '../../dist/uacp/HelloMessage' 3 | 4 | describe('HelloMessage', () => { 5 | run([ 6 | { 7 | name: 'normal', 8 | instance: new HelloMessage({ 9 | ProtocolVersion: 0, 10 | ReceiveBufferSize: 147456, 11 | SendBufferSize: 147456, 12 | MaxMessageSize: 4194240, 13 | MaxChunkCount: 65535, 14 | EndpointUrl: 'opc.tcp://localhost:4840', 15 | }), 16 | // prettier-ignore 17 | bytes: new Uint8Array([ 18 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x02, 0x00, 19 | 0x00, 0x40, 0x02, 0x00, 0xc0, 0xff, 0x3f, 0x00, 20 | 0xff, 0xff, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 21 | 0x6f, 0x70, 0x63, 0x2e, 0x74, 0x63, 0x70, 0x3a, 22 | 0x2f, 0x2f, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x68, 23 | 0x6f, 0x73, 0x74, 0x3a, 0x34, 0x38, 0x34, 0x30 24 | ]) 25 | }, 26 | ]) 27 | }) 28 | -------------------------------------------------------------------------------- /tests/ua/UserNameIdentityToken.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { UserNameIdentityToken } from '../../dist/ua/generated' 3 | 4 | describe('UserNameIdentityToken', () => { 5 | run([ 6 | { 7 | name: '', 8 | instance: new UserNameIdentityToken({ 9 | PolicyId: 'username', 10 | UserName: 'user', 11 | // prettier-ignore 12 | Password: new Uint8Array([ 13 | 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64 14 | ]), 15 | EncryptionAlgorithm: 'plain', 16 | }), 17 | // prettier-ignore 18 | bytes: new Uint8Array([ 19 | 0x08, 0x00, 0x00, 0x00, 0x75, 0x73, 0x65, 0x72, 20 | 0x6e, 0x61, 0x6d, 0x65, 0x04, 0x00, 0x00, 0x00, 21 | 0x75, 0x73, 0x65, 0x72, 0x08, 0x00, 0x00, 0x00, 22 | 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 23 | 0x05, 0x00, 0x00, 0x00, 0x70, 0x6c, 0x61, 0x69, 24 | 0x6e 25 | ]) 26 | }, 27 | ]) 28 | }) 29 | -------------------------------------------------------------------------------- /src/Subscription.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreateMonitoredItemsRequest, 3 | CreateMonitoredItemsResponse, 4 | PublishRequest, 5 | PublishResponse, 6 | } from './ua/generated' 7 | import Client from './Client' 8 | import { uint32 } from './types' 9 | 10 | export default class Subscription extends EventTarget { 11 | public client: Client 12 | public id: uint32 13 | 14 | constructor(client: Client, id: uint32) { 15 | super() 16 | this.client = client 17 | this.id = id 18 | } 19 | 20 | public monitor( 21 | req: CreateMonitoredItemsRequest 22 | ): Promise { 23 | return new Promise((resolve) => { 24 | req.SubscriptionId = this.id 25 | this.client.secureChannel.send(req, resolve) 26 | }) 27 | } 28 | 29 | public publish(req: PublishRequest): Promise { 30 | return new Promise((resolve) => { 31 | this.client.secureChannel.send(req, resolve) 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/ua/Guid.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import Guid from '../../dist/ua/Guid' 3 | 4 | describe('Guid', () => { 5 | run([ 6 | { 7 | name: 'spec', 8 | instance: new Guid('72962B91-FA75-4AE6-8D28-B404DC7DAF63'), 9 | // prettier-ignore 10 | bytes: new Uint8Array([ 11 | 0x91, 0x2b, 0x96, 0x72, 0x75, 0xfa, 0xe6, 0x4a, 12 | 0x8d, 0x28, 0xb4, 0x04, 0xdc, 0x7d, 0xaf, 0x63 13 | ]) 14 | }, 15 | { 16 | name: 'open62541', 17 | instance: new Guid('a123456c-0abc-1a2b-815f-687212aaee1b'), 18 | // prettier-ignore 19 | bytes: new Uint8Array([ 20 | 0x6c, 0x45, 0x23, 0xa1, 0xbc, 0x0a, 0x2b, 0x1a, 21 | 0x81, 0x5f, 0x68, 0x72, 0x12, 0xaa, 0xee, 0x1b 22 | ]) 23 | }, 24 | ]) 25 | 26 | it('toString()', () => { 27 | const guid = new Guid('72962B91-FA75-4AE6-8D28-B404DC7DAF63') 28 | expect(guid.toString()).toBe('72962B91-FA75-4AE6-8D28-B404DC7DAF63') 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /tests/ua/UserTokenPolicy.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { UserTokenType, UserTokenPolicy } from '../../dist/ua/generated' 3 | 4 | describe('UserTokenPolicy', () => { 5 | run([ 6 | { 7 | name: 'normal', 8 | instance: new UserTokenPolicy({ 9 | PolicyId: '1', 10 | TokenType: UserTokenType.Anonymous, 11 | IssuedTokenType: 'issued-token', 12 | IssuerEndpointUrl: 'issuer-uri', 13 | SecurityPolicyUri: 'sec-uri', 14 | }), 15 | // prettier-ignore 16 | bytes: new Uint8Array([ 17 | 0x01, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 18 | 0x00, 0x0c, 0x00, 0x00, 0x00, 0x69, 0x73, 0x73, 19 | 0x75, 0x65, 0x64, 0x2d, 0x74, 0x6f, 0x6b, 0x65, 20 | 0x6e, 0x0a, 0x00, 0x00, 0x00, 0x69, 0x73, 0x73, 21 | 0x75, 0x65, 0x72, 0x2d, 0x75, 0x72, 0x69, 0x07, 22 | 0x00, 0x00, 0x00, 0x73, 0x65, 0x63, 0x2d, 0x75, 23 | 0x72, 0x69, 24 | ]) 25 | }, 26 | ]) 27 | }) 28 | -------------------------------------------------------------------------------- /tests/ua/run.ts: -------------------------------------------------------------------------------- 1 | import { encode } from '../../dist/ua/encode' 2 | import decode from '../../dist/ua/decode' 3 | import factory from '../../dist/ua/factory' 4 | 5 | interface Case { 6 | name: string 7 | instance: unknown 8 | bytes: Uint8Array 9 | } 10 | 11 | const run = (cases: Case[]): void => { 12 | for (const c of cases) { 13 | describe(c.name, () => { 14 | test('encode', () => { 15 | const result = new Uint8Array(encode({ instance: c.instance })) 16 | for (let index = 0; index < c.bytes.byteLength; index++) { 17 | expect(result[index]).toBe(c.bytes[index]) 18 | } 19 | }) 20 | 21 | test('decode', () => { 22 | const name = (c.instance as object).constructor.name 23 | const instance = factory(name) 24 | decode({ 25 | bytes: c.bytes.buffer, 26 | instance, 27 | }) 28 | expect(instance).toEqual(c.instance) 29 | }) 30 | }) 31 | } 32 | } 33 | 34 | export default run 35 | -------------------------------------------------------------------------------- /src/uasc/SequenceHeader.ts: -------------------------------------------------------------------------------- 1 | import Bucket from '../ua/Bucket' 2 | import { uint32 } from '../types' 3 | 4 | interface Options { 5 | SequenceNumber?: uint32 6 | RequestId?: uint32 7 | } 8 | 9 | // https://reference.opcfoundation.org/v104/Core/docs/Part6/6.7.2/#6.7.2.4 10 | export default class SequenceHeader { 11 | public SequenceNumber: uint32 12 | public RequestId: uint32 13 | 14 | constructor(options?: Options) { 15 | this.SequenceNumber = options?.SequenceNumber ?? 0 16 | this.RequestId = options?.RequestId ?? 0 17 | } 18 | 19 | public encode(): ArrayBuffer { 20 | const bucket = new Bucket() 21 | bucket.writeUint32(this.SequenceNumber) 22 | bucket.writeUint32(this.RequestId) 23 | return bucket.bytes 24 | } 25 | 26 | public decode(b: ArrayBuffer, position?: number): number { 27 | const bucket = new Bucket(b, position) 28 | this.SequenceNumber = bucket.readUint32() 29 | this.RequestId = bucket.readUint32() 30 | return bucket.position 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/ua/CloseSessionResponse.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { CloseSessionResponse, ResponseHeader } from '../../dist/ua/generated' 3 | import DiagnosticInfo from '../../dist/ua/DiagnosticInfo' 4 | import ExtensionObject from '../../dist/ua/ExtensionObject' 5 | 6 | describe('CloseSessionResponse', () => { 7 | run([ 8 | { 9 | name: 'normal', 10 | instance: new CloseSessionResponse({ 11 | ResponseHeader: new ResponseHeader({ 12 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 13 | RequestHandle: 1, 14 | ServiceDiagnostics: new DiagnosticInfo(), 15 | StringTable: [], 16 | AdditionalHeader: new ExtensionObject(), 17 | }), 18 | }), 19 | // prettier-ignore 20 | bytes: new Uint8Array([ 21 | 0x00, 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 22 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 23 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 24 | ]) 25 | }, 26 | ]) 27 | }) 28 | -------------------------------------------------------------------------------- /src/ua/service.ts: -------------------------------------------------------------------------------- 1 | import * as spec from './generated' 2 | import * as id from '../id/id' 3 | import ExpandedNodeId from './ExpandedNodeId' 4 | import decode from './decode' 5 | 6 | const registry = new Map() 7 | 8 | registry 9 | 10 | interface Service { 11 | typeId: ExpandedNodeId 12 | service: unknown 13 | } 14 | 15 | export const decodeService = (b: ArrayBuffer, position?: number): Service => { 16 | const typeId = new ExpandedNodeId() 17 | position = typeId.decode(b, position) 18 | 19 | // b = b.subarray(n) 20 | 21 | // create new instance from given type id 22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 23 | const name = (id as any)[typeId.NodeId.Type] as string 24 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 25 | const service = new (spec as any)[name]() 26 | 27 | // decode(b, service, position) 28 | decode({ 29 | bytes: b, 30 | instance: service, 31 | position, 32 | }) 33 | 34 | return { 35 | typeId, 36 | service, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "description": "The demo client uses https://github.com/open62541/open62541 as the server.", 5 | "main": "index.tsx", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "webpack": "webpack --config webpack.config.js", 9 | "dev": "webpack-dev-server" 10 | }, 11 | "author": "Mirco Zeiss", 12 | "license": "MIT", 13 | "dependencies": { 14 | "react": "^16.13.1", 15 | "react-dom": "^16.13.1", 16 | "react-router-dom": "^5.1.2" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^16.9.25", 20 | "@types/react-dom": "^16.9.5", 21 | "@types/react-router-dom": "^5.1.3", 22 | "css-loader": "^3.4.2", 23 | "html-webpack-plugin": "^3.2.0", 24 | "node-sass": "^4.13.1", 25 | "sass-loader": "^8.0.2", 26 | "style-loader": "^1.1.3", 27 | "ts-loader": "^6.2.1", 28 | "webpack": "^4.42.0", 29 | "webpack-cli": "^3.3.11", 30 | "webpack-dev-server": "^3.10.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: path.join(__dirname, 'src', 'index.tsx'), 7 | output: { 8 | filename: 'bundle.js', 9 | publicPath: '/', 10 | }, 11 | devtool: 'source-map', 12 | resolve: { 13 | extensions: ['.ts', '.tsx', '.js'], 14 | }, 15 | module: { 16 | rules: [ 17 | { test: /\.tsx?$/, loader: 'ts-loader' }, 18 | { 19 | test: /\.s[ac]ss$/i, 20 | use: [ 21 | // Creates `style` nodes from JS strings 22 | 'style-loader', 23 | // Translates CSS into CommonJS 24 | 'css-loader', 25 | // Compiles Sass to CSS 26 | 'sass-loader', 27 | ], 28 | }, 29 | ], 30 | }, 31 | devServer: { 32 | historyApiFallback: true, 33 | }, 34 | plugins: [ 35 | new HtmlWebpackPlugin({ 36 | template: path.join(__dirname, 'src', 'index.html'), 37 | }), 38 | ], 39 | } 40 | -------------------------------------------------------------------------------- /tests/uasc/AsymmetricSecurityHeader.test.ts: -------------------------------------------------------------------------------- 1 | import run from '../ua/run' 2 | import AsymmetricSecurityHeader from '../../dist/uasc/AsymmetricSecurityHeader' 3 | 4 | describe('AsymmetricSecurityHeader', () => { 5 | run([ 6 | { 7 | name: 'normal', 8 | instance: new AsymmetricSecurityHeader({ 9 | SecurityPolicyUri: 'http://opcfoundation.org/UA/SecurityPolicy#None', 10 | SenderCertificate: '', 11 | ReceiverCertificateThumbprint: '', 12 | }), 13 | // prettier-ignore 14 | bytes: new Uint8Array([ 15 | 0x2f, 0x00, 0x00, 0x00, 0x68, 0x74, 0x74, 0x70, 16 | 0x3a, 0x2f, 0x2f, 0x6f, 0x70, 0x63, 0x66, 0x6f, 17 | 0x75, 0x6e, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 18 | 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x55, 0x41, 0x2f, 19 | 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 20 | 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x23, 0x4e, 21 | 0x6f, 0x6e, 0x65, 0xff, 0xff, 0xff, 0xff, 0xff, 22 | 0xff, 0xff, 0xff 23 | ]) 24 | }, 25 | ]) 26 | }) 27 | -------------------------------------------------------------------------------- /demo/src/context.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import Client from '../../dist/Client' 3 | 4 | interface Context { 5 | client: Client 6 | } 7 | 8 | export const OPCUAContext = React.createContext({} as Context) 9 | 10 | export const OPCUAProvider: React.FunctionComponent = (props) => { 11 | const [client, setClient] = useState(null) 12 | 13 | const connect = async () => { 14 | const client = new Client('ws://localhost:1234') 15 | await client.open() 16 | await client.hello() 17 | await client.openSecureChannel() 18 | await client.createSession() 19 | await client.activateSession() 20 | setClient(client) 21 | } 22 | 23 | useEffect(() => { 24 | connect() 25 | }, []) 26 | 27 | if (client === null) { 28 | return
loading ...
29 | } 30 | 31 | return ( 32 | 37 | {props.children} 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /tests/ua/CancelResponse.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { CancelResponse, ResponseHeader } from '../../dist/ua/generated' 3 | import DiagnosticInfo from '../../dist/ua/DiagnosticInfo' 4 | import ExtensionObject from '../../dist/ua/ExtensionObject' 5 | 6 | describe('CancelResponse', () => { 7 | run([ 8 | { 9 | name: 'normal', 10 | instance: new CancelResponse({ 11 | ResponseHeader: new ResponseHeader({ 12 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 13 | RequestHandle: 1, 14 | ServiceDiagnostics: new DiagnosticInfo(), 15 | StringTable: [], 16 | AdditionalHeader: new ExtensionObject(), 17 | }), 18 | CancelCount: 1, 19 | }), 20 | // prettier-ignore 21 | bytes: new Uint8Array([ 22 | 0x00, 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 23 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 24 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 25 | 0x01, 0x00, 0x00, 0x00, 26 | ]) 27 | }, 28 | ]) 29 | }) 30 | -------------------------------------------------------------------------------- /tests/ua/CloseSecureChannelResponse.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { 3 | CloseSecureChannelResponse, 4 | ResponseHeader, 5 | } from '../../dist/ua/generated' 6 | import DiagnosticInfo from '../../dist/ua/DiagnosticInfo' 7 | import ExtensionObject from '../../dist/ua/ExtensionObject' 8 | 9 | describe('CloseSecureChannelResponse', () => { 10 | run([ 11 | { 12 | name: 'normal', 13 | instance: new CloseSecureChannelResponse({ 14 | ResponseHeader: new ResponseHeader({ 15 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 16 | RequestHandle: 1, 17 | ServiceDiagnostics: new DiagnosticInfo(), 18 | StringTable: [], 19 | AdditionalHeader: new ExtensionObject(), 20 | }), 21 | }), 22 | // prettier-ignore 23 | bytes: new Uint8Array([ 24 | 0x00, 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 25 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 26 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 27 | ]) 28 | }, 29 | ]) 30 | }) 31 | -------------------------------------------------------------------------------- /tests/ua/OpenSecureChannelRequest.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { 3 | OpenSecureChannelRequest, 4 | RequestHeader, 5 | MessageSecurityMode, 6 | } from '../../dist/ua/generated' 7 | 8 | describe('OpenSecureChannelRequest', () => { 9 | run([ 10 | { 11 | name: 'normal', 12 | instance: new OpenSecureChannelRequest({ 13 | RequestHeader: new RequestHeader({ 14 | Timestamp: new Date(Date.UTC(1601, 0, 1, 0, 0, 0, 0)), 15 | }), 16 | SecurityMode: MessageSecurityMode.None, 17 | RequestedLifetime: 600000, 18 | }), 19 | // prettier-ignore 20 | bytes: new Uint8Array([ 21 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 22 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 23 | 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 24 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 25 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 26 | 0x00, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x27, 0x09, 27 | 0x00 28 | ]) 29 | }, 30 | ]) 31 | }) 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Hottinger Baldwin Messtechnik GmbH 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. -------------------------------------------------------------------------------- /src/ua/guards.ts: -------------------------------------------------------------------------------- 1 | import { Encoder, Decoder, TypedArray } from '../types' 2 | 3 | export const isEncoder = (instance: unknown): instance is Encoder => 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | instance !== null && typeof (instance as any)['encode'] === 'function' 6 | 7 | export const isDecoder = (instance: unknown): instance is Decoder => 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | instance !== null && typeof (instance as any)['decode'] === 'function' 10 | 11 | export const isTypedArray = (instance: unknown): instance is TypedArray => 12 | instance instanceof Uint8Array || 13 | instance instanceof Uint16Array || 14 | instance instanceof Uint32Array 15 | 16 | export const isNotNullObject = (instance: unknown): instance is object => 17 | typeof instance === 'object' && instance !== null 18 | 19 | export const keyInObject = (v: object, key: string): key is keyof typeof v => 20 | key in v 21 | 22 | // export const isIndexable = (instance: unknown, value: string): value is keyof typeof instance => { 23 | // return typeof instance === 'object' && instance !== null && value in instance 24 | // } 25 | -------------------------------------------------------------------------------- /tests/ua/ActivateSessionResponse.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { 3 | NewNullResponseHeader, 4 | NullResponseHeaderBytes, 5 | } from './ResponseHeader.test' 6 | import { ActivateSessionResponse } from '../../dist/ua/generated' 7 | 8 | describe('ActivateSessionResponse', () => { 9 | run([ 10 | { 11 | name: 'nothing', 12 | instance: new ActivateSessionResponse({ 13 | ResponseHeader: NewNullResponseHeader(), 14 | }), 15 | // prettier-ignore 16 | bytes: new Uint8Array([ 17 | ...NullResponseHeaderBytes, 18 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 19 | 0xff, 0xff, 0xff, 0xff, 20 | ]) 21 | }, 22 | { 23 | name: 'with nonce', 24 | instance: new ActivateSessionResponse({ 25 | ResponseHeader: NewNullResponseHeader(), 26 | ServerNonce: new Uint8Array([0xde, 0xad, 0xbe, 0xef]), 27 | }), 28 | // prettier-ignore 29 | bytes: new Uint8Array([ 30 | ...NullResponseHeaderBytes, 31 | 0x04, 0x00, 0x00, 0x00, 0xde, 0xad, 0xbe, 0xef, 32 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 33 | ]) 34 | }, 35 | ]) 36 | }) 37 | -------------------------------------------------------------------------------- /cmd/id/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/csv" 6 | "html/template" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | ) 14 | 15 | func main() { 16 | f, err := os.Open(filepath.Join("..", "..", "schema", "NodeIds.csv")) 17 | if err != nil { 18 | panic(err) 19 | } 20 | defer f.Close() 21 | 22 | rows := make([][]string, 0) 23 | reader := csv.NewReader(f) 24 | 25 | for { 26 | record, err := reader.Read() 27 | if err == io.EOF { 28 | break 29 | } 30 | if err != nil { 31 | panic(err) 32 | } 33 | record[0] = strings.ReplaceAll(record[0], "_", "") 34 | rows = append(rows, record) 35 | } 36 | 37 | var b bytes.Buffer 38 | if err := tmpl.Execute(&b, rows); err != nil { 39 | panic(err) 40 | } 41 | 42 | out := filepath.Join("..", "..", "src", "id", "id.ts") 43 | if err := ioutil.WriteFile(out, b.Bytes(), 0644); err != nil { 44 | panic(err) 45 | } 46 | 47 | log.Printf("Wrote %s", out) 48 | } 49 | 50 | var tmpl = template.Must(template.New("").Parse( 51 | `// Code generated by cmd/id. DO NOT EDIT! 52 | 53 | export enum Id { 54 | {{ range . }}{{ index . 0 }} = {{ index . 1 }}, 55 | {{ end }} 56 | }`)) 57 | -------------------------------------------------------------------------------- /src/uacp/ConnectionProtocolMessageHeader.ts: -------------------------------------------------------------------------------- 1 | import Bucket from '../ua/Bucket' 2 | import { uint32 } from '../types' 3 | 4 | interface Options { 5 | MessageType?: string 6 | ChunkType?: string 7 | MessageSize?: uint32 8 | } 9 | 10 | // https://reference.opcfoundation.org/v104/Core/docs/Part6/7.1.2/#7.1.2.2 11 | export default class ConnectionProtocolMessageHeader { 12 | public MessageType: string 13 | public ChunkType: string 14 | public MessageSize: uint32 15 | 16 | constructor(options?: Options) { 17 | this.MessageType = options?.MessageType ?? '' 18 | this.ChunkType = options?.ChunkType ?? '' 19 | this.MessageSize = options?.MessageSize ?? 0 20 | } 21 | 22 | public encode(): ArrayBuffer { 23 | const bucket = new Bucket() 24 | bucket.writeStringBytes(this.MessageType) 25 | bucket.writeStringBytes(this.ChunkType) 26 | bucket.writeUint32(this.MessageSize) 27 | return bucket.bytes 28 | } 29 | 30 | public decode(b: ArrayBuffer, position?: number): number { 31 | const bucket = new Bucket(b, position) 32 | this.MessageType = bucket.readStringBytes(3) 33 | this.ChunkType = bucket.readStringBytes(1) 34 | this.MessageSize = bucket.readUint32() 35 | return bucket.position 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cmd/status/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/csv" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "text/template" 12 | ) 13 | 14 | func main() { 15 | in := filepath.Join("..", "..", "schema", "StatusCode.csv") 16 | out := filepath.Join("..", "..", "src", "ua", "StatusCode.ts") 17 | 18 | f, err := os.Open(in) 19 | if err != nil { 20 | panic(err) 21 | } 22 | defer f.Close() 23 | 24 | rows, err := csv.NewReader(f).ReadAll() 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | for i := range rows { 30 | // make eslint happy 31 | rows[i][0] = strings.ReplaceAll(rows[i][0], "_", "") 32 | } 33 | 34 | // prepend fixed values 35 | rows = append([][]string{ 36 | {"OK", "0x0", ""}, 37 | {"Uncertain", "0x40000000", ""}, 38 | {"Bad", "0x80000000", ""}, 39 | }, rows...) 40 | 41 | var b bytes.Buffer 42 | if err := tmpl.Execute(&b, rows); err != nil { 43 | panic(err) 44 | } 45 | 46 | if err := ioutil.WriteFile(out, b.Bytes(), 0644); err != nil { 47 | panic(err) 48 | } 49 | log.Printf("Wrote %s", out) 50 | } 51 | 52 | var tmpl = template.Must(template.New("").Parse(` 53 | import { uint32 } from "../types" 54 | 55 | export enum StatusCode { 56 | {{range .}}{{index . 0}} = {{index . 1}} as uint32, 57 | {{end}} 58 | }`)) 59 | -------------------------------------------------------------------------------- /tests/ua/OpenSecureChannelResponse.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { 3 | OpenSecureChannelResponse, 4 | ChannelSecurityToken, 5 | ResponseHeader, 6 | } from '../../dist/ua/generated' 7 | 8 | describe('OpenSecureChannelResponse', () => { 9 | run([ 10 | { 11 | name: 'normal', 12 | instance: new OpenSecureChannelResponse({ 13 | ResponseHeader: new ResponseHeader({ 14 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 15 | RequestHandle: 1, 16 | }), 17 | SecurityToken: new ChannelSecurityToken({ 18 | ChannelId: 1, 19 | TokenId: 2, 20 | CreatedAt: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 21 | RevisedLifetime: 6000000, 22 | }), 23 | ServerNonce: new Uint8Array([0xff]), 24 | }), 25 | // prettier-ignore 26 | bytes: new Uint8Array([ 27 | 0x00, 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 28 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 29 | 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 30 | 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 31 | 0x02, 0x00, 0x00, 0x00, 0x00, 0x98, 0x67, 0xdd, 32 | 0xfd, 0x30, 0xd4, 0x01, 0x80, 0x8d, 0x5b, 0x00, 33 | 0x01, 0x00, 0x00, 0x00, 0xff 34 | ]) 35 | }, 36 | ]) 37 | }) 38 | -------------------------------------------------------------------------------- /tests/ua/RequestHeader.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { RequestHeader } from '../../dist/ua/generated' 3 | import { NewTwoByteNodeId } from '../../dist/ua/NodeId' 4 | 5 | export const NewNullRequestHeader = (): RequestHeader => 6 | new RequestHeader({ 7 | AuthenticationToken: NewTwoByteNodeId(0), 8 | Timestamp: new Date(Date.UTC(1970, 0, 1, 0, 0, 0, 0)), 9 | }) 10 | 11 | // prettier-ignore 12 | export const NullRequestHeaderBytes = new Uint8Array([ 13 | 0x00, 0x00, 0x00, 0x80, 0x3e, 0xd5, 0xde, 0xb1, 14 | 0x9d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 15 | 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 16 | 0x00, 0x00, 0x00, 0x00, 0x00 17 | ]) 18 | 19 | describe('RequestHeader', () => { 20 | run([ 21 | { 22 | name: 'normal', 23 | instance: new RequestHeader({ 24 | Timestamp: new Date(Date.UTC(1601, 0, 1, 0, 0, 0, 0)), 25 | }), 26 | // prettier-ignore 27 | bytes: new Uint8Array([ 28 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 29 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 30 | 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 31 | 0x00, 0x00, 0x00, 0x00, 0x00 32 | ]) 33 | }, 34 | { 35 | name: 'null', 36 | instance: NewNullRequestHeader(), 37 | bytes: NullRequestHeaderBytes, 38 | }, 39 | ]) 40 | }) 41 | -------------------------------------------------------------------------------- /tests/ua/CreateSubscriptionResponse.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { 3 | CreateSubscriptionResponse, 4 | ResponseHeader, 5 | } from '../../dist/ua/generated' 6 | import DiagnosticInfo from '../../dist/ua/DiagnosticInfo' 7 | import ExtensionObject from '../../dist/ua/ExtensionObject' 8 | 9 | describe('CreateSubscriptionResponse', () => { 10 | run([ 11 | { 12 | name: 'normal', 13 | instance: new CreateSubscriptionResponse({ 14 | ResponseHeader: new ResponseHeader({ 15 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 16 | RequestHandle: 1, 17 | ServiceDiagnostics: new DiagnosticInfo(), 18 | StringTable: [], 19 | AdditionalHeader: new ExtensionObject(), 20 | }), 21 | SubscriptionId: 1, 22 | RevisedPublishingInterval: 1000, 23 | RevisedLifetimeCount: 60, 24 | RevisedMaxKeepAliveCount: 20, 25 | }), 26 | // prettier-ignore 27 | bytes: new Uint8Array([ 28 | 0x00, 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 29 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 30 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 31 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 32 | 0x00, 0x40, 0x8f, 0x40, 0x3c, 0x00, 0x00, 0x00, 33 | 0x14, 0x00, 0x00, 0x00, 34 | ]) 35 | }, 36 | ]) 37 | }) 38 | -------------------------------------------------------------------------------- /tests/ua/ExtensionObject.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import ExtensionObject, { 3 | ExtensionObjectEmpty, 4 | ExtensionObjectBinary, 5 | } from '../../dist/ua/ExtensionObject' 6 | import { 7 | NewTwoByteExpandedNodeId, 8 | NewFourByteExpandedNodeId, 9 | } from '../../dist/ua/ExpandedNodeId' 10 | import { Id } from '../../dist/id/id' 11 | import { AnonymousIdentityToken } from '../../dist/ua/generated' 12 | 13 | describe('ExtensionObject', () => { 14 | run([ 15 | { 16 | name: 'empty', 17 | instance: new ExtensionObject({ 18 | TypeId: NewTwoByteExpandedNodeId(0), 19 | Encoding: ExtensionObjectEmpty, 20 | }), 21 | bytes: new Uint8Array([0x00, 0x00, 0x00]), 22 | }, 23 | { 24 | name: 'anonymous', 25 | instance: new ExtensionObject({ 26 | TypeId: NewFourByteExpandedNodeId( 27 | 0, 28 | Id.AnonymousIdentityTokenEncodingDefaultBinary 29 | ), 30 | Encoding: ExtensionObjectBinary, 31 | Value: new AnonymousIdentityToken({ 32 | PolicyId: 'anonymous', 33 | }), 34 | }), 35 | // prettier-ignore 36 | bytes: new Uint8Array([ 37 | 0x01, 0x00, 0x41, 0x01, 0x01, 0x0d, 0x00, 0x00, 38 | 0x00, 0x09, 0x00, 0x00, 0x00, 0x61, 0x6e, 0x6f, 39 | 0x6e, 0x79, 0x6d, 0x6f, 0x75, 0x73, 40 | ]) 41 | }, 42 | ]) 43 | }) 44 | -------------------------------------------------------------------------------- /demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | OPC UA 8 | 14 | 15 | 16 |
17 | 18 | 23 | 28 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/ua/LocalizedText.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import LocalizedText, { 3 | LocalizedTextLocale, 4 | LocalizedTextText, 5 | } from '../../dist/ua/LocalizedText' 6 | 7 | describe('LocalizedText', () => { 8 | run([ 9 | { 10 | name: 'empty', 11 | instance: new LocalizedText(), 12 | bytes: new Uint8Array([0x00]), 13 | }, 14 | { 15 | name: 'locale', 16 | instance: new LocalizedText({ 17 | EncodingMask: LocalizedTextLocale, 18 | Locale: 'foo', 19 | }), 20 | // prettier-ignore 21 | bytes: new Uint8Array([ 22 | 0x01, 0x03, 0x00, 0x00, 0x00, 0x66, 0x6f, 0x6f 23 | ]) 24 | }, 25 | { 26 | name: 'text', 27 | instance: new LocalizedText({ 28 | EncodingMask: LocalizedTextText, 29 | Text: 'bar', 30 | }), 31 | // prettier-ignore 32 | bytes: new Uint8Array([ 33 | 0x02, 0x03, 0x00, 0x00, 0x00, 0x62, 0x61, 0x72 34 | ]) 35 | }, 36 | { 37 | name: 'both', 38 | instance: new LocalizedText({ 39 | EncodingMask: LocalizedTextLocale | LocalizedTextText, 40 | Locale: 'foo', 41 | Text: 'bar', 42 | }), 43 | // prettier-ignore 44 | bytes: new Uint8Array([ 45 | 0x03, 0x03, 0x00, 0x00, 0x00, 0x66, 0x6f, 0x6f, 46 | 0x03, 0x00, 0x00, 0x00, 0x62, 0x61, 0x72 47 | ]) 48 | }, 49 | ]) 50 | }) 51 | -------------------------------------------------------------------------------- /tests/ua/CancelRequest.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { RequestHeader, CancelRequest } from '../../dist/ua/generated' 3 | import ExtensionObject from '../../dist/ua/ExtensionObject' 4 | import { NewByteStringNodeId } from '../../dist/ua/NodeId' 5 | 6 | describe('CancelRequest', () => { 7 | run([ 8 | { 9 | name: 'normal', 10 | instance: new CancelRequest({ 11 | RequestHeader: new RequestHeader({ 12 | // prettier-ignore 13 | AuthenticationToken: NewByteStringNodeId(0x00, new Uint8Array([ 14 | 0x08, 0x22, 0x87, 0x62, 0xba, 0x81, 0xe1, 0x11, 15 | 0xa6, 0x43, 0xf8, 0x77, 0x7b, 0xc6, 0x2f, 0xc8, 16 | ])), 17 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 18 | RequestHandle: 1, 19 | AdditionalHeader: new ExtensionObject(), 20 | }), 21 | RequestHandle: 1, 22 | }), 23 | // prettier-ignore 24 | bytes: new Uint8Array([ 25 | 0x05, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08, 26 | 0x22, 0x87, 0x62, 0xba, 0x81, 0xe1, 0x11, 0xa6, 27 | 0x43, 0xf8, 0x77, 0x7b, 0xc6, 0x2f, 0xc8, 0x00, 28 | 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 0x01, 29 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 30 | 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 31 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 32 | ]) 33 | }, 34 | ]) 35 | }) 36 | -------------------------------------------------------------------------------- /tests/ua/CloseSessionRequest.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { CloseSessionRequest, RequestHeader } from '../../dist/ua/generated' 3 | import { NewByteStringNodeId } from '../../dist/ua/NodeId' 4 | import ExtensionObject from '../../dist/ua/ExtensionObject' 5 | 6 | describe('CloseSessionRequest', () => { 7 | run([ 8 | { 9 | name: 'normal', 10 | instance: new CloseSessionRequest({ 11 | RequestHeader: new RequestHeader({ 12 | // prettier-ignore 13 | AuthenticationToken: NewByteStringNodeId(0x00, new Uint8Array([ 14 | 0x08, 0x22, 0x87, 0x62, 0xba, 0x81, 0xe1, 0x11, 15 | 0xa6, 0x43, 0xf8, 0x77, 0x7b, 0xc6, 0x2f, 0xc8, 16 | ])), 17 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 18 | RequestHandle: 1, 19 | AdditionalHeader: new ExtensionObject(), 20 | }), 21 | DeleteSubscriptions: true, 22 | }), 23 | // prettier-ignore 24 | bytes: new Uint8Array([ 25 | 0x05, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08, 26 | 0x22, 0x87, 0x62, 0xba, 0x81, 0xe1, 0x11, 0xa6, 27 | 0x43, 0xf8, 0x77, 0x7b, 0xc6, 0x2f, 0xc8, 0x00, 28 | 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 0x01, 29 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 30 | 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 31 | 0x00, 0x00, 0x01, 32 | ]) 33 | }, 34 | ]) 35 | }) 36 | -------------------------------------------------------------------------------- /tests/ua/CloseSecureChannelRequest.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { 3 | CloseSecureChannelRequest, 4 | RequestHeader, 5 | } from '../../dist/ua/generated' 6 | import { NewByteStringNodeId } from '../../dist/ua/NodeId' 7 | import ExtensionObject from '../../dist/ua/ExtensionObject' 8 | 9 | describe('CloseSecureChannelRequest', () => { 10 | run([ 11 | { 12 | name: 'normal', 13 | instance: new CloseSecureChannelRequest({ 14 | RequestHeader: new RequestHeader({ 15 | // prettier-ignore 16 | AuthenticationToken: NewByteStringNodeId(0x00, new Uint8Array([ 17 | 0x08, 0x22, 0x87, 0x62, 0xba, 0x81, 0xe1, 0x11, 18 | 0xa6, 0x43, 0xf8, 0x77, 0x7b, 0xc6, 0x2f, 0xc8, 19 | ])), 20 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 21 | RequestHandle: 1, 22 | AdditionalHeader: new ExtensionObject(), 23 | }), 24 | }), 25 | // prettier-ignore 26 | bytes: new Uint8Array([ 27 | 0x05, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08, 28 | 0x22, 0x87, 0x62, 0xba, 0x81, 0xe1, 0x11, 0xa6, 29 | 0x43, 0xf8, 0x77, 0x7b, 0xc6, 0x2f, 0xc8, 0x00, 30 | 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 0x01, 31 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 32 | 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 33 | 0x00, 0x00, 34 | ]) 35 | }, 36 | ]) 37 | }) 38 | -------------------------------------------------------------------------------- /src/uasc/AsymmetricSecurityHeader.ts: -------------------------------------------------------------------------------- 1 | import Bucket from '../ua/Bucket' 2 | 3 | interface Options { 4 | SecurityPolicyUri?: string 5 | SenderCertificate?: string 6 | ReceiverCertificateThumbprint?: string 7 | } 8 | 9 | // https://reference.opcfoundation.org/v104/Core/docs/Part6/6.7.2/#6.7.2.3 10 | export default class AsymmetricSecurityHeader { 11 | public SecurityPolicyUri: string 12 | public SenderCertificate: string 13 | public ReceiverCertificateThumbprint: string 14 | 15 | constructor(options?: Options) { 16 | this.SecurityPolicyUri = 17 | options?.SecurityPolicyUri ?? 18 | 'http://opcfoundation.org/UA/SecurityPolicy#None' 19 | this.SenderCertificate = options?.SenderCertificate ?? '' 20 | this.ReceiverCertificateThumbprint = 21 | options?.ReceiverCertificateThumbprint ?? '' 22 | } 23 | 24 | public encode(): ArrayBuffer { 25 | const bucket = new Bucket() 26 | bucket.writeString(this.SecurityPolicyUri) 27 | bucket.writeString(this.SenderCertificate) 28 | bucket.writeString(this.ReceiverCertificateThumbprint) 29 | return bucket.bytes 30 | } 31 | 32 | public decode(b: ArrayBuffer, position?: number): number { 33 | const bucket = new Bucket(b, position) 34 | this.SecurityPolicyUri = bucket.readString() 35 | this.SenderCertificate = bucket.readString() 36 | this.ReceiverCertificateThumbprint = bucket.readString() 37 | return bucket.position 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/uasc/SecureConversationMessageHeader.ts: -------------------------------------------------------------------------------- 1 | import Bucket from '../ua/Bucket' 2 | import { uint32 } from '../types' 3 | 4 | interface Options { 5 | MessageType?: string 6 | IsFinal?: string 7 | MessageSize?: number 8 | SecureChannelId?: number 9 | } 10 | 11 | // https://reference.opcfoundation.org/v104/Core/docs/Part6/6.7.2/#6.7.2.2 12 | export default class SecureConversationMessageHeader { 13 | public MessageType: string 14 | public IsFinal: string 15 | public MessageSize: uint32 16 | public SecureChannelId: uint32 17 | 18 | constructor(options?: Options) { 19 | this.MessageType = options?.MessageType ?? '' 20 | this.IsFinal = options?.IsFinal ?? '' 21 | this.MessageSize = options?.MessageSize ?? 0 22 | this.SecureChannelId = options?.SecureChannelId ?? 0 23 | } 24 | 25 | public encode(): ArrayBuffer { 26 | const bucket = new Bucket() 27 | bucket.writeStringBytes(this.MessageType) 28 | bucket.writeStringBytes(this.IsFinal) 29 | bucket.writeUint32(this.MessageSize) 30 | bucket.writeUint32(this.SecureChannelId) 31 | return bucket.bytes 32 | } 33 | 34 | public decode(b: ArrayBuffer, position?: number): number { 35 | const bucket = new Bucket(b, position) 36 | this.MessageType = bucket.readStringBytes(3) 37 | this.IsFinal = bucket.readStringBytes(1) 38 | this.MessageSize = bucket.readUint32() 39 | this.SecureChannelId = bucket.readUint32() 40 | return bucket.position 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/ua/ExpandedNodeId.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import ExpandedNodeId from '../../dist/ua/ExpandedNodeId' 3 | import { NewTwoByteNodeId } from '../../dist/ua/NodeId' 4 | 5 | describe('ExpandedNodeId', () => { 6 | run([ 7 | { 8 | name: 'without optional fields', 9 | instance: new ExpandedNodeId({ 10 | NodeId: NewTwoByteNodeId(0xff), 11 | }), 12 | bytes: new Uint8Array([0x00, 0xff]), 13 | }, 14 | { 15 | name: 'with NamespaceUri', 16 | instance: new ExpandedNodeId({ 17 | NodeId: NewTwoByteNodeId(0xff), 18 | NamespaceUri: 'foobar', 19 | }), 20 | // prettier-ignore 21 | bytes: new Uint8Array([ 22 | 0x80, 0xff, 0x06, 0x00, 0x00, 0x00, 0x66, 0x6f, 23 | 0x6f, 0x62, 0x61, 0x72, 24 | ]) 25 | }, 26 | { 27 | name: 'with ServerIndex', 28 | instance: new ExpandedNodeId({ 29 | NodeId: NewTwoByteNodeId(0xff), 30 | ServerIndex: 32768, 31 | }), 32 | bytes: new Uint8Array([0x40, 0xff, 0x00, 0x80, 0x00, 0x00]), 33 | }, 34 | { 35 | name: 'with NamespaceUri and ServerIndex', 36 | instance: new ExpandedNodeId({ 37 | NodeId: NewTwoByteNodeId(0xff), 38 | NamespaceUri: 'foobar', 39 | ServerIndex: 32768, 40 | }), 41 | // prettier-ignore 42 | bytes: new Uint8Array([ 43 | 0xc0, 0xff, 0x06, 0x00, 0x00, 0x00, 0x66, 0x6f, 44 | 0x6f, 0x62, 0x61, 0x72, 0x00, 0x80, 0x00, 0x00, 45 | ]) 46 | }, 47 | ]) 48 | }) 49 | -------------------------------------------------------------------------------- /tests/ua/WriteValue.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { WriteValue } from '../../dist/ua/generated' 3 | import { NewFourByteNodeId } from '../../dist/ua/NodeId' 4 | import { AttributeId, TypeId } from '../../dist/ua/enums' 5 | import DataValue, { 6 | DataValueValue, 7 | DataValueSourceTimestamp, 8 | DataValueServerTimestamp, 9 | } from '../../dist/ua/DataValue' 10 | import Variant from '../../dist/ua/Variant' 11 | 12 | describe('WriteValue', () => { 13 | run([ 14 | { 15 | name: 'normal', 16 | instance: new WriteValue({ 17 | NodeId: NewFourByteNodeId(0, 2256), 18 | AttributeId: AttributeId.Value, 19 | Value: new DataValue({ 20 | EncodingMask: 21 | DataValueValue | 22 | DataValueSourceTimestamp | 23 | DataValueServerTimestamp, 24 | Value: new Variant({ 25 | EncodingMask: TypeId.Float, 26 | Value: 2.5001699924468994, 27 | }), 28 | SourceTimestamp: new Date(Date.UTC(2018, 8, 17, 14, 28, 29, 112)), 29 | ServerTimestamp: new Date(Date.UTC(2018, 8, 17, 14, 28, 29, 112)), 30 | }), 31 | }), 32 | // prettier-ignore 33 | bytes: new Uint8Array([ 34 | 0x01, 0x00, 0xd0, 0x08, 0x0d, 0x00, 0x00, 0x00, 35 | 0xff, 0xff, 0xff, 0xff, 0x0d, 0x0a, 0xc9, 0x02, 36 | 0x20, 0x40, 0x80, 0x3b, 0xe8, 0xb3, 0x92, 0x4e, 37 | 0xd4, 0x01, 0x80, 0x3b, 0xe8, 0xb3, 0x92, 0x4e, 38 | 0xd4, 0x01, 39 | ]) 40 | }, 41 | ]) 42 | }) 43 | -------------------------------------------------------------------------------- /src/uacp/WebSocket.ts: -------------------------------------------------------------------------------- 1 | interface Callback { 2 | resolve: (value: MessageEvent) => void 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | reject: (reason: any) => void 5 | } 6 | 7 | export default class AsyncWebSocket { 8 | private socket: WebSocket 9 | private queue: MessageEvent[] 10 | private callbacks: Callback[] 11 | 12 | constructor(url: string) { 13 | this.socket = new WebSocket(url) 14 | this.socket.binaryType = 'arraybuffer' 15 | this.queue = [] 16 | this.callbacks = [] 17 | } 18 | 19 | public connect(): Promise { 20 | return new Promise((resolve, reject) => { 21 | this.socket.onerror = reject 22 | 23 | this.socket.onopen = (): void => { 24 | this.socket.onmessage = (event: MessageEvent): void => { 25 | if (this.callbacks.length !== 0) { 26 | const shifted = this.callbacks.shift() 27 | if (shifted) { 28 | shifted.resolve(event) 29 | return 30 | } 31 | } 32 | this.queue.push(event) 33 | } 34 | resolve() 35 | } 36 | }) 37 | } 38 | 39 | public write(data: string | ArrayBufferLike | Blob | ArrayBufferView): void { 40 | this.socket.send(data) 41 | } 42 | 43 | public read(): Promise { 44 | if (this.queue.length !== 0) { 45 | return Promise.resolve(this.queue.shift() as MessageEvent) 46 | } 47 | 48 | return new Promise((resolve, reject) => { 49 | this.callbacks.push({ resolve, reject }) 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/ua/ActivateSessionRequest.test.ts: -------------------------------------------------------------------------------- 1 | import ExtensionObject, { 2 | ExtensionObjectBinary, 3 | } from '../../dist/ua/ExtensionObject' 4 | import { 5 | ActivateSessionRequest, 6 | AnonymousIdentityToken, 7 | } from '../../dist/ua/generated' 8 | import { 9 | NewNullRequestHeader, 10 | NullRequestHeaderBytes, 11 | } from './RequestHeader.test' 12 | import { NewFourByteExpandedNodeId } from '../../dist/ua/ExpandedNodeId' 13 | import { Id } from '../../dist/id/id' 14 | import run from './run' 15 | 16 | describe('ActivateSessionRequest', () => { 17 | run([ 18 | { 19 | name: 'normal', 20 | instance: new ActivateSessionRequest({ 21 | RequestHeader: NewNullRequestHeader(), 22 | UserIdentityToken: new ExtensionObject({ 23 | TypeId: NewFourByteExpandedNodeId( 24 | 0, 25 | Id.AnonymousIdentityTokenEncodingDefaultBinary 26 | ), 27 | Encoding: ExtensionObjectBinary, 28 | Value: new AnonymousIdentityToken({ 29 | PolicyId: 'anonymous', 30 | }), 31 | }), 32 | }), 33 | // prettier-ignore 34 | bytes: new Uint8Array([ 35 | ...NullRequestHeaderBytes, 36 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 37 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 38 | 0x01, 0x00, 0x41, 0x01, 0x01, 0x0d, 0x00, 0x00, 39 | 0x00, 0x09, 0x00, 0x00, 0x00, 0x61, 0x6e, 0x6f, 40 | 0x6e, 0x79, 0x6d, 0x6f, 0x75, 0x73, 0xff, 0xff, 41 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 42 | ]) 43 | }, 44 | ]) 45 | }) 46 | -------------------------------------------------------------------------------- /tests/ua/FindServersOnNetworkRequest.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { 3 | FindServersOnNetworkRequest, 4 | RequestHeader, 5 | } from '../../dist/ua/generated' 6 | import { NewByteStringNodeId } from '../../dist/ua/NodeId' 7 | import ExtensionObject from '../../dist/ua/ExtensionObject' 8 | 9 | describe('FindServersOnNetworkRequest', () => { 10 | run([ 11 | { 12 | name: 'normal', 13 | instance: new FindServersOnNetworkRequest({ 14 | RequestHeader: new RequestHeader({ 15 | // prettier-ignore 16 | AuthenticationToken: NewByteStringNodeId(0x00, new Uint8Array([ 17 | 0x08, 0x22, 0x87, 0x62, 0xba, 0x81, 0xe1, 0x11, 18 | 0xa6, 0x43, 0xf8, 0x77, 0x7b, 0xc6, 0x2f, 0xc8, 19 | ])), 20 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 21 | RequestHandle: 1, 22 | AdditionalHeader: new ExtensionObject(), 23 | }), 24 | StartingRecordId: 1000, 25 | MaxRecordsToReturn: 0, 26 | }), 27 | // prettier-ignore 28 | bytes: new Uint8Array([ 29 | 0x05, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08, 30 | 0x22, 0x87, 0x62, 0xba, 0x81, 0xe1, 0x11, 0xa6, 31 | 0x43, 0xf8, 0x77, 0x7b, 0xc6, 0x2f, 0xc8, 0x00, 32 | 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 0x01, 33 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 34 | 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 35 | 0x00, 0x00, 0xe8, 0x03, 0x00, 0x00, 0x00, 0x00, 36 | 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 37 | ]) 38 | }, 39 | ]) 40 | }) 41 | -------------------------------------------------------------------------------- /tests/ua/ReadResponse.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { ReadResponse, ResponseHeader } from '../../dist/ua/generated' 3 | import DiagnosticInfo from '../../dist/ua/DiagnosticInfo' 4 | import ExtensionObject from '../../dist/ua/ExtensionObject' 5 | import DataValue, { DataValueValue } from '../../dist/ua/DataValue' 6 | import Variant from '../../dist/ua/Variant' 7 | import { TypeId } from '../../dist/ua/enums' 8 | 9 | describe('ReadRequest', () => { 10 | run([ 11 | { 12 | name: 'normal', 13 | instance: new ReadResponse({ 14 | ResponseHeader: new ResponseHeader({ 15 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 16 | RequestHandle: 1, 17 | ServiceDiagnostics: new DiagnosticInfo(), 18 | StringTable: [], 19 | AdditionalHeader: new ExtensionObject(), 20 | }), 21 | Results: [ 22 | new DataValue({ 23 | EncodingMask: DataValueValue, 24 | Value: new Variant({ 25 | Value: 2.5001559257507324, 26 | EncodingMask: TypeId.Float, 27 | }), 28 | }), 29 | ], 30 | DiagnosticInfos: [new DiagnosticInfo()], 31 | }), 32 | // prettier-ignore 33 | bytes: new Uint8Array([ 34 | 0x00, 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 35 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 36 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 37 | 0x01, 0x00, 0x00, 0x00, 0x01, 0x0a, 0x8e, 0x02, 38 | 0x20, 0x40, 0x01, 0x00, 0x00, 0x00, 0x00, 39 | ]) 40 | }, 41 | ]) 42 | }) 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "opcua", 3 | "version": "1.0.0", 4 | "description": "TypeScript / JavaScript OPC UA client for the browser", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "clear": "rm -fr dist coverage", 9 | "test:cov": "npm run clear && tsc --sourceMap && jest --coverage", 10 | "test": "rm -rf ./compiled && tsc --project ./tests/tsconfig.json && jest", 11 | "tsc": "tsc --project ./src/tsconfig.json --skipLibCheck", 12 | "watch": "tsc --project ./src/tsconfig.json -w --skipLibCheck", 13 | "prettier": "prettier --write \"./**/*.{js,ts,tsx,json,html,yml,scss}\"", 14 | "prettier:ci": "prettier --list-different \"./**/*.{js,ts,tsx,json,html,yml,scss}\"", 15 | "eslint": "eslint \"./**/*.{js,ts,tsx}\"" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/hbm/opcua.git" 20 | }, 21 | "author": "Mirco Zeiss", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/hbm/opcua/issues" 25 | }, 26 | "homepage": "https://github.com/hbm/opcua#readme", 27 | "devDependencies": { 28 | "@types/classnames": "^2.2.10", 29 | "@types/jest": "^25.1.4", 30 | "@types/node": "^13.9.2", 31 | "@typescript-eslint/eslint-plugin": "^2.25.0", 32 | "@typescript-eslint/parser": "^2.25.0", 33 | "eslint": "^6.8.0", 34 | "eslint-config-prettier": "^6.10.1", 35 | "jest": "^25.2.3", 36 | "prettier": "^2.0.2", 37 | "reflect-metadata": "^0.1.13", 38 | "typescript": "^3.8.3" 39 | }, 40 | "dependencies": { 41 | "classnames": "^2.2.6" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/ua/LocalizedText.ts: -------------------------------------------------------------------------------- 1 | import Bucket from './Bucket' 2 | import { uint8, EnDecoder } from '../types' 3 | 4 | interface Options { 5 | EncodingMask?: uint8 6 | Locale?: string 7 | Text?: string 8 | } 9 | 10 | export const LocalizedTextLocale: uint8 = 0x01 11 | export const LocalizedTextText: uint8 = 0x02 12 | 13 | // https://reference.opcfoundation.org/v104/Core/docs/Part6/5.2.2/#5.2.2.14 14 | export default class LocalizedText implements EnDecoder { 15 | public EncodingMask: uint8 16 | public Locale: string 17 | public Text: string 18 | 19 | constructor(options?: Options) { 20 | this.EncodingMask = options?.EncodingMask ?? 0 21 | this.Locale = options?.Locale ?? '' 22 | this.Text = options?.Text ?? '' 23 | } 24 | 25 | public encode(): ArrayBuffer { 26 | const bucket = new Bucket() 27 | bucket.writeUint8(this.EncodingMask) 28 | 29 | if (this.has(LocalizedTextLocale)) { 30 | bucket.writeString(this.Locale) 31 | } 32 | 33 | if (this.has(LocalizedTextText)) { 34 | bucket.writeString(this.Text) 35 | } 36 | 37 | return bucket.bytes 38 | } 39 | 40 | public decode(b: ArrayBuffer, position?: number): number { 41 | const bucket = new Bucket(b, position) 42 | this.EncodingMask = bucket.readUint8() 43 | if (this.has(LocalizedTextLocale)) { 44 | this.Locale = bucket.readString() 45 | } 46 | if (this.has(LocalizedTextText)) { 47 | this.Text = bucket.readString() 48 | } 49 | return bucket.position 50 | } 51 | 52 | public has(mask: uint8): boolean { 53 | return (this.EncodingMask & mask) === mask 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/uasc/Message.test.ts: -------------------------------------------------------------------------------- 1 | // import run from '../ua/run' 2 | // import OpenSecureChannelRequest from '../ua/OpenSecureChannelRequest' 3 | // import RequestHeader from '../ua/RequestHeader' 4 | // import { SecurityTokenRequestTypeIssue } from '../ua/SecurityTokenRequestType' 5 | // import { MessageSecurityModeNone } from '../ua/MessageSecurityMode' 6 | // import { NewTwoByteNodeId } from '../ua/NodeId' 7 | 8 | describe('Message', () => { 9 | test('foo', () => { 10 | expect(1).toBe(1) 11 | }) 12 | // const request = new OpenSecureChannelRequest({ 13 | // RequestHeader: new RequestHeader({ 14 | // AuthenticationToken: NewTwoByteNodeId(0), 15 | // Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 16 | // RequestHandle: 1, 17 | // ReturnDiagnostics: 0x03ff 18 | // // AdditionalHeader: NewExtensionObject(nil), 19 | // }), 20 | // ClientProtocolVersion: 0, 21 | // RequestType: SecurityTokenRequestTypeIssue, 22 | // SecurityMode: MessageSecurityModeNone, 23 | // RequestedLifetime: 6000000 24 | // }) 25 | 26 | // run([ 27 | // { 28 | // name: 'OPN', 29 | // instance: (function() { 30 | // // const s = &SecureChannel{ 31 | // // cfg: &Config{ 32 | // // SecurityPolicyURI: "http://gopcua.example/OPCUA/SecurityPolicy#Foo", 33 | // // }, 34 | // // requestID: 1, 35 | // // sequenceNumber: 1, 36 | // // securityTokenID: 0, 37 | // // } 38 | // })(), 39 | // bytes: new Uint8Array([]) 40 | // } 41 | // ]) 42 | }) 43 | -------------------------------------------------------------------------------- /tests/ua/ApplicationDescription.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { 3 | ApplicationDescription, 4 | ApplicationType, 5 | } from '../../dist/ua/generated' 6 | import LocalizedText, { LocalizedTextText } from '../../dist/ua/LocalizedText' 7 | 8 | describe('ApplicationDescription', () => { 9 | run([ 10 | { 11 | name: 'normal', 12 | instance: new ApplicationDescription({ 13 | ApplicationUri: 'app-uri', 14 | ProductUri: 'prod-uri', 15 | ApplicationName: new LocalizedText({ 16 | EncodingMask: LocalizedTextText, 17 | Text: 'app-name', 18 | }), 19 | ApplicationType: ApplicationType.Server, 20 | GatewayServerUri: 'gw-uri', 21 | DiscoveryProfileUri: 'prof-uri', 22 | DiscoveryUrls: ['discov-uri-1', 'discov-uri-2'], 23 | }), 24 | // prettier-ignore 25 | bytes: new Uint8Array([ 26 | 0x07, 0x00, 0x00, 0x00, 0x61, 0x70, 0x70, 0x2d, 27 | 0x75, 0x72, 0x69, 0x08, 0x00, 0x00, 0x00, 0x70, 28 | 0x72, 0x6f, 0x64, 0x2d, 0x75, 0x72, 0x69, 0x02, 29 | 0x08, 0x00, 0x00, 0x00, 0x61, 0x70, 0x70, 0x2d, 30 | 0x6e, 0x61, 0x6d, 0x65, 0x00, 0x00, 0x00, 0x00, 31 | 0x06, 0x00, 0x00, 0x00, 0x67, 0x77, 0x2d, 0x75, 32 | 0x72, 0x69, 0x08, 0x00, 0x00, 0x00, 0x70, 0x72, 33 | 0x6f, 0x66, 0x2d, 0x75, 0x72, 0x69, 0x02, 0x00, 34 | 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x64, 0x69, 35 | 0x73, 0x63, 0x6f, 0x76, 0x2d, 0x75, 0x72, 0x69, 36 | 0x2d, 0x31, 0x0c, 0x00, 0x00, 0x00, 0x64, 0x69, 37 | 0x73, 0x63, 0x6f, 0x76, 0x2d, 0x75, 0x72, 0x69, 38 | 0x2d, 0x32 39 | ]) 40 | }, 41 | ]) 42 | }) 43 | -------------------------------------------------------------------------------- /src/uacp/AcknowledgeMessage.ts: -------------------------------------------------------------------------------- 1 | import Bucket from '../ua/Bucket' 2 | import { uint32 } from '../types' 3 | 4 | interface Options { 5 | ProtocolVersion?: uint32 6 | ReceiveBufferSize?: uint32 7 | SendBufferSize?: uint32 8 | MaxMessageSize?: uint32 9 | MaxChunkCount?: uint32 10 | } 11 | 12 | // https://reference.opcfoundation.org/v104/Core/docs/Part6/7.1.2/#7.1.2.4 13 | export default class AcknowledgeMessage { 14 | public ProtocolVersion: uint32 15 | public ReceiveBufferSize: uint32 16 | public SendBufferSize: uint32 17 | public MaxMessageSize: uint32 18 | public MaxChunkCount: uint32 19 | 20 | constructor(options?: Options) { 21 | this.ProtocolVersion = options?.ProtocolVersion ?? 0 22 | this.ReceiveBufferSize = options?.ReceiveBufferSize ?? 0 23 | this.SendBufferSize = options?.SendBufferSize ?? 0 24 | this.MaxMessageSize = options?.MaxMessageSize ?? 0 25 | this.MaxChunkCount = options?.MaxChunkCount ?? 0 26 | } 27 | 28 | public encode(): ArrayBuffer { 29 | const bucket = new Bucket() 30 | bucket.writeUint32(this.ProtocolVersion) 31 | bucket.writeUint32(this.ReceiveBufferSize) 32 | bucket.writeUint32(this.SendBufferSize) 33 | bucket.writeUint32(this.MaxMessageSize) 34 | bucket.writeUint32(this.MaxChunkCount) 35 | return bucket.bytes 36 | } 37 | 38 | public decode(b: ArrayBuffer, position?: number): number { 39 | const bucket = new Bucket(b, position) 40 | this.ProtocolVersion = bucket.readUint32() 41 | this.ReceiveBufferSize = bucket.readUint32() 42 | this.SendBufferSize = bucket.readUint32() 43 | this.MaxMessageSize = bucket.readUint32() 44 | this.MaxChunkCount = bucket.readUint32() 45 | return bucket.position 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/ua/ResponseHeader.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { ResponseHeader } from '../../dist/ua/generated' 3 | import DiagnosticInfo from '../../dist/ua/DiagnosticInfo' 4 | import ExtensionObject from '../../dist/ua/ExtensionObject' 5 | import { NewTwoByteExpandedNodeId } from '../../dist/ua/ExpandedNodeId' 6 | 7 | export const NewNullResponseHeader = (): ResponseHeader => 8 | new ResponseHeader({ 9 | Timestamp: new Date(Date.UTC(1970, 0, 1, 0, 0, 0, 0)), 10 | ServiceDiagnostics: new DiagnosticInfo(), 11 | AdditionalHeader: new ExtensionObject(), 12 | }) 13 | 14 | // prettier-ignore 15 | export const NullResponseHeaderBytes = new Uint8Array([ 16 | 0x00, 0x80, 0x3e, 0xd5, 0xde, 0xb1, 0x9d, 0x01, 17 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 18 | 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00 19 | ]) 20 | 21 | describe('ResponseHeader', () => { 22 | run([ 23 | { 24 | name: 'null', 25 | instance: NewNullResponseHeader(), 26 | bytes: NullResponseHeaderBytes, 27 | }, 28 | { 29 | name: 'normal', 30 | instance: new ResponseHeader({ 31 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 32 | RequestHandle: 1, 33 | StringTable: ['foo', 'bar'], 34 | AdditionalHeader: new ExtensionObject({ 35 | TypeId: NewTwoByteExpandedNodeId(255), 36 | }), 37 | }), 38 | // prettier-ignore 39 | bytes: new Uint8Array([ 40 | 0x00, 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 41 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 42 | 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 43 | 0x00, 0x66, 0x6f, 0x6f, 0x03, 0x00, 0x00, 0x00, 44 | 0x62, 0x61, 0x72, 0x00, 0xff, 0x00, 45 | ]) 46 | }, 47 | ]) 48 | }) 49 | -------------------------------------------------------------------------------- /tests/ua/ServerOnNetwork.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { ServerOnNetwork } from '../../dist/ua/generated' 3 | 4 | describe('ServerOnNetwork', () => { 5 | run([ 6 | { 7 | name: 'normal', 8 | instance: new ServerOnNetwork({ 9 | RecordId: 1, 10 | ServerName: 'server-name', 11 | DiscoveryUrl: 'discov-uri', 12 | ServerCapabilities: ['server-cap-1'], 13 | }), 14 | // prettier-ignore 15 | bytes: new Uint8Array([ 16 | 0x01, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 17 | 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2d, 0x6e, 18 | 0x61, 0x6d, 0x65, 0x0a, 0x00, 0x00, 0x00, 0x64, 19 | 0x69, 0x73, 0x63, 0x6f, 0x76, 0x2d, 0x75, 0x72, 20 | 0x69, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 21 | 0x00, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2d, 22 | 0x63, 0x61, 0x70, 0x2d, 0x31, 23 | ]) 24 | }, 25 | { 26 | name: 'multiple', 27 | instance: new ServerOnNetwork({ 28 | RecordId: 1, 29 | ServerName: 'server-name', 30 | DiscoveryUrl: 'discov-uri', 31 | ServerCapabilities: ['server-cap-1', 'server-cap-2'], 32 | }), 33 | // prettier-ignore 34 | bytes: new Uint8Array([ 35 | 0x01, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 36 | 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2d, 0x6e, 37 | 0x61, 0x6d, 0x65, 0x0a, 0x00, 0x00, 0x00, 0x64, 38 | 0x69, 0x73, 0x63, 0x6f, 0x76, 0x2d, 0x75, 0x72, 39 | 0x69, 0x02, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 40 | 0x00, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2d, 41 | 0x63, 0x61, 0x70, 0x2d, 0x31, 0x0c, 0x00, 0x00, 42 | 0x00, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2d, 43 | 0x63, 0x61, 0x70, 0x2d, 0x32, 44 | ]) 45 | }, 46 | ]) 47 | }) 48 | -------------------------------------------------------------------------------- /tests/ua/GetEndpointsRequest.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { GetEndpointsRequest, RequestHeader } from '../../dist/ua/generated' 3 | import { NewByteStringNodeId } from '../../dist/ua/NodeId' 4 | import ExtensionObject from '../../dist/ua/ExtensionObject' 5 | 6 | describe('GetEndpointsRequest', () => { 7 | run([ 8 | { 9 | name: 'normal', 10 | instance: new GetEndpointsRequest({ 11 | RequestHeader: new RequestHeader({ 12 | // prettier-ignore 13 | AuthenticationToken: NewByteStringNodeId(0x00, new Uint8Array([ 14 | 0x08, 0x22, 0x87, 0x62, 0xba, 0x81, 0xe1, 0x11, 15 | 0xa6, 0x43, 0xf8, 0x77, 0x7b, 0xc6, 0x2f, 0xc8, 16 | ])), 17 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 18 | RequestHandle: 1, 19 | AdditionalHeader: new ExtensionObject(), 20 | }), 21 | EndpointUrl: 'opc.tcp://wow.its.easy:11111/UA/Server', 22 | }), 23 | // prettier-ignore 24 | bytes: new Uint8Array([ 25 | 0x05, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08, 26 | 0x22, 0x87, 0x62, 0xba, 0x81, 0xe1, 0x11, 0xa6, 27 | 0x43, 0xf8, 0x77, 0x7b, 0xc6, 0x2f, 0xc8, 0x00, 28 | 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 0x01, 29 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 30 | 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 31 | 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x6f, 0x70, 32 | 0x63, 0x2e, 0x74, 0x63, 0x70, 0x3a, 0x2f, 0x2f, 33 | 0x77, 0x6f, 0x77, 0x2e, 0x69, 0x74, 0x73, 0x2e, 34 | 0x65, 0x61, 0x73, 0x79, 0x3a, 0x31, 0x31, 0x31, 35 | 0x31, 0x31, 0x2f, 0x55, 0x41, 0x2f, 0x53, 0x65, 36 | 0x72, 0x76, 0x65, 0x72, 0xff, 0xff, 0xff, 0xff, 37 | 0xff, 0xff, 0xff, 0xff, 38 | ]) 39 | }, 40 | ]) 41 | }) 42 | -------------------------------------------------------------------------------- /tests/ua/FindServersRequest.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { FindServersRequest, RequestHeader } from '../../dist/ua/generated' 3 | import { NewByteStringNodeId } from '../../dist/ua/NodeId' 4 | import ExtensionObject from '../../dist/ua/ExtensionObject' 5 | 6 | describe('FindServersRequest', () => { 7 | run([ 8 | { 9 | name: 'normal', 10 | instance: new FindServersRequest({ 11 | RequestHeader: new RequestHeader({ 12 | // prettier-ignore 13 | AuthenticationToken: NewByteStringNodeId(0x00, new Uint8Array([ 14 | 0x08, 0x22, 0x87, 0x62, 0xba, 0x81, 0xe1, 0x11, 15 | 0xa6, 0x43, 0xf8, 0x77, 0x7b, 0xc6, 0x2f, 0xc8, 16 | ])), 17 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 18 | RequestHandle: 1, 19 | AdditionalHeader: new ExtensionObject(), 20 | }), 21 | EndpointUrl: 'opc.tcp://wow.its.easy:11111/UA/Server', 22 | }), 23 | // prettier-ignore 24 | bytes: new Uint8Array([ 25 | 0x05, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08, 26 | 0x22, 0x87, 0x62, 0xba, 0x81, 0xe1, 0x11, 0xa6, 27 | 0x43, 0xf8, 0x77, 0x7b, 0xc6, 0x2f, 0xc8, 0x00, 28 | 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 0x01, 29 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 30 | 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 31 | 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x6f, 0x70, 32 | 0x63, 0x2e, 0x74, 0x63, 0x70, 0x3a, 0x2f, 0x2f, 33 | 0x77, 0x6f, 0x77, 0x2e, 0x69, 0x74, 0x73, 0x2e, 34 | 0x65, 0x61, 0x73, 0x79, 0x3a, 0x31, 0x31, 0x31, 35 | 0x31, 0x31, 0x2f, 0x55, 0x41, 0x2f, 0x53, 0x65, 36 | 0x72, 0x76, 0x65, 0x72, 0xff, 0xff, 0xff, 0xff, 37 | 0xff, 0xff, 0xff, 0xff, 38 | ]) 39 | }, 40 | ]) 41 | }) 42 | -------------------------------------------------------------------------------- /src/uacp/HelloMessage.ts: -------------------------------------------------------------------------------- 1 | import Bucket from '../ua/Bucket' 2 | import { uint32 } from '../types' 3 | 4 | interface Options { 5 | ProtocolVersion?: number 6 | ReceiveBufferSize?: number 7 | SendBufferSize?: number 8 | MaxMessageSize?: number 9 | MaxChunkCount?: number 10 | EndpointUrl?: string 11 | } 12 | 13 | // https://reference.opcfoundation.org/v104/Core/docs/Part6/7.1.2/#7.1.2.3 14 | export default class HelloMessage { 15 | public ProtocolVersion: uint32 16 | public ReceiveBufferSize: uint32 17 | public SendBufferSize: uint32 18 | public MaxMessageSize: uint32 19 | public MaxChunkCount: uint32 20 | public EndpointUrl: string 21 | 22 | constructor(options?: Options) { 23 | this.ProtocolVersion = options?.ProtocolVersion ?? 0 24 | this.ReceiveBufferSize = options?.ReceiveBufferSize ?? 0 25 | this.SendBufferSize = options?.SendBufferSize ?? 0 26 | this.MaxMessageSize = options?.MaxMessageSize ?? 0 27 | this.MaxChunkCount = options?.MaxChunkCount ?? 0 28 | this.EndpointUrl = options?.EndpointUrl ?? '' 29 | } 30 | 31 | public encode(): ArrayBuffer { 32 | const bucket = new Bucket() 33 | bucket.writeUint32(this.ProtocolVersion) 34 | bucket.writeUint32(this.ReceiveBufferSize) 35 | bucket.writeUint32(this.SendBufferSize) 36 | bucket.writeUint32(this.MaxMessageSize) 37 | bucket.writeUint32(this.MaxChunkCount) 38 | bucket.writeString(this.EndpointUrl) 39 | return bucket.bytes 40 | } 41 | 42 | public decode(b: ArrayBuffer, position?: number): number { 43 | const bucket = new Bucket(b, position) 44 | this.ProtocolVersion = bucket.readUint32() 45 | this.ReceiveBufferSize = bucket.readUint32() 46 | this.SendBufferSize = bucket.readUint32() 47 | this.MaxMessageSize = bucket.readUint32() 48 | this.MaxChunkCount = bucket.readUint32() 49 | this.EndpointUrl = bucket.readString() 50 | return bucket.position 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/ua/CreateSubscriptionRequest.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { 3 | CreateSubscriptionRequest, 4 | RequestHeader, 5 | } from '../../dist/ua/generated' 6 | import { NewByteStringNodeId } from '../../dist/ua/NodeId' 7 | import ExtensionObject from '../../dist/ua/ExtensionObject' 8 | import { NewTwoByteExpandedNodeId } from '../../dist/ua/ExpandedNodeId' 9 | 10 | describe('CreateSubscriptionRequest', () => { 11 | run([ 12 | { 13 | name: 'normal', 14 | instance: new CreateSubscriptionRequest({ 15 | RequestHeader: new RequestHeader({ 16 | // prettier-ignore 17 | AuthenticationToken: NewByteStringNodeId(0, new Uint8Array([ 18 | 0xfe, 0x8d, 0x87, 0x79, 0xf7, 0x03, 0x27, 0x77, 19 | 0xc5, 0x03, 0xa1, 0x09, 0x50, 0x29, 0x27, 0x60, 20 | ])), 21 | AuditEntryId: '', 22 | RequestHandle: 1003429, 23 | TimeoutHint: 10000, 24 | AdditionalHeader: new ExtensionObject({ 25 | TypeId: NewTwoByteExpandedNodeId(0), 26 | }), 27 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 28 | }), 29 | RequestedPublishingInterval: 500, 30 | RequestedLifetimeCount: 2400, 31 | RequestedMaxKeepAliveCount: 10, 32 | MaxNotificationsPerPublish: 65536, 33 | PublishingEnabled: true, 34 | Priority: 0, 35 | }), 36 | // prettier-ignore 37 | bytes: new Uint8Array([ 38 | 0x05, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0xfe, 39 | 0x8d, 0x87, 0x79, 0xf7, 0x03, 0x27, 0x77, 0xc5, 40 | 0x03, 0xa1, 0x09, 0x50, 0x29, 0x27, 0x60, 0x00, 41 | 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 0xa5, 42 | 0x4f, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 43 | 0xff, 0xff, 0xff, 0x10, 0x27, 0x00, 0x00, 0x00, 44 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 45 | 0x7f, 0x40, 0x60, 0x09, 0x00, 0x00, 0x0a, 0x00, 46 | 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 47 | ]) 48 | }, 49 | ]) 50 | }) 51 | -------------------------------------------------------------------------------- /src/ua/ExtensionObject.ts: -------------------------------------------------------------------------------- 1 | import Bucket from './Bucket' 2 | import factory from './factory' 3 | import { Id } from '../id/id' 4 | import ExpandedNodeId, { NewTwoByteExpandedNodeId } from './ExpandedNodeId' 5 | import { uint8, EnDecoder } from '../types' 6 | 7 | export const ExtensionObjectEmpty = 0 8 | export const ExtensionObjectBinary = 1 9 | 10 | interface Options { 11 | TypeId?: ExpandedNodeId 12 | Encoding?: uint8 13 | Value?: unknown 14 | } 15 | 16 | // https://reference.opcfoundation.org/v104/Core/docs/Part6/5.2.2/#5.2.2.15 17 | export default class ExtensionObject implements EnDecoder { 18 | public TypeId: ExpandedNodeId 19 | public Encoding: uint8 20 | public Value: unknown 21 | 22 | constructor(options?: Options) { 23 | this.TypeId = options?.TypeId ?? NewTwoByteExpandedNodeId(0) 24 | this.Encoding = options?.Encoding ?? 0 25 | this.Value = options?.Value ?? null 26 | } 27 | 28 | public encode(): ArrayBuffer { 29 | const bucket = new Bucket() 30 | bucket.writeStruct(this.TypeId) 31 | bucket.writeUint8(this.Encoding) 32 | if (this.Encoding == ExtensionObjectEmpty) { 33 | return bucket.bytes 34 | } 35 | 36 | const body = new Bucket() 37 | body.writeStruct(this.Value) 38 | 39 | bucket.writeUint32(body.bytes.byteLength) 40 | bucket.write(body.bytes) 41 | 42 | return bucket.bytes 43 | } 44 | 45 | public decode(b: ArrayBuffer, position?: number): number { 46 | const bucket = new Bucket(b, position) 47 | 48 | this.TypeId = new ExpandedNodeId() 49 | bucket.readStruct(this.TypeId) 50 | 51 | this.Encoding = bucket.readUint8() 52 | 53 | if (this.Encoding === ExtensionObjectEmpty) { 54 | return bucket.position 55 | } 56 | 57 | const length = bucket.readUint32() 58 | if (length === 0 || length === 0xffffffff) { 59 | return bucket.position 60 | } 61 | 62 | const name = Id[this.TypeId.NodeId.Identifier as number] 63 | this.Value = factory(name) 64 | bucket.readStruct(this.Value) 65 | 66 | return bucket.position 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/ua/Guid.ts: -------------------------------------------------------------------------------- 1 | import Bucket from './Bucket' 2 | import { EnDecoder, uint32, uint16 } from '../types' 3 | 4 | const parse = (s: string): number[] => { 5 | const out: number[] = [] 6 | for (let i = 0; i < s.length; i += 2) { 7 | out.push(parseInt(s.substr(i, 2), 16)) 8 | } 9 | return out 10 | } 11 | 12 | // https://reference.opcfoundation.org/v104/Core/docs/Part6/5.2.2/#5.2.2.6 13 | // https://reference.opcfoundation.org/v104/Core/docs/Part6/5.1.3/ 14 | export default class Guid implements EnDecoder { 15 | public Data1: uint32 16 | public Data2: uint16 17 | public Data3: uint16 18 | public Data4: Uint8Array 19 | 20 | constructor(s?: string) { 21 | s = s ?? '00000000-0000-0000-0000-000000000000' 22 | const h = parse(s.replace(/-/g, '')) 23 | const data = new Uint8Array(h) 24 | const dv = new DataView(data.buffer) 25 | // attention! big endian 26 | this.Data1 = dv.getUint32(0) 27 | this.Data2 = dv.getUint16(4) 28 | this.Data3 = dv.getUint16(6) 29 | this.Data4 = data.subarray(8, 16) 30 | } 31 | 32 | public encode(): ArrayBuffer { 33 | const bucket = new Bucket() 34 | bucket.writeUint32(this.Data1) 35 | bucket.writeUint16(this.Data2) 36 | bucket.writeUint16(this.Data3) 37 | bucket.write(this.Data4) 38 | return bucket.bytes 39 | } 40 | 41 | public decode(b: ArrayBuffer, position?: number): number { 42 | const bucket = new Bucket(b, position) 43 | this.Data1 = bucket.readUint32() 44 | this.Data2 = bucket.readUint16() 45 | this.Data3 = bucket.readUint16() 46 | this.Data4 = bucket.readN(8) 47 | return bucket.position 48 | } 49 | 50 | public toString(): string { 51 | const a = Array.from(this.Data4.slice(0, 2)) 52 | .map((b) => b.toString(16).padStart(2, '0')) 53 | .join('') 54 | 55 | const b = Array.from(this.Data4.slice(2)) 56 | .map((b) => b.toString(16).padStart(2, '0')) 57 | .join('') 58 | 59 | return `${this.Data1.toString(16)}-${this.Data2.toString( 60 | 16 61 | )}-${this.Data3.toString(16)}-${a}-${b}`.toUpperCase() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/ua/enums.ts: -------------------------------------------------------------------------------- 1 | import { uint32, uint8 } from '../types' 2 | 3 | // https://reference.opcfoundation.org/v104/Core/docs/Part6/A.1/ 4 | export enum AttributeId { 5 | Invalid = 0 as uint32, 6 | NodeId = 1 as uint32, 7 | NodeClass = 2 as uint32, 8 | BrowseName = 3 as uint32, 9 | DisplayName = 4 as uint32, 10 | Description = 5 as uint32, 11 | WriteMask = 6 as uint32, 12 | UserWriteMask = 7 as uint32, 13 | IsAbstract = 8 as uint32, 14 | Symmetric = 9 as uint32, 15 | InverseName = 10 as uint32, 16 | ContainsNoLoops = 11 as uint32, 17 | EventNotifier = 12 as uint32, 18 | Value = 13 as uint32, 19 | DataType = 14 as uint32, 20 | ValueRank = 15 as uint32, 21 | ArrayDimensions = 16 as uint32, 22 | AccessLevel = 17 as uint32, 23 | UserAccessLevel = 18 as uint32, 24 | MinimumSamplingInterval = 19 as uint32, 25 | Historizing = 20 as uint32, 26 | Executable = 21 as uint32, 27 | UserExecutable = 22 as uint32, 28 | DataTypeDefinition = 23 as uint32, 29 | RolePermissions = 24 as uint32, 30 | UserRolePermissions = 25 as uint32, 31 | AccessRestrictions = 26 as uint32, 32 | AccessLevelEx = 27 as uint32, 33 | } 34 | 35 | // https://reference.opcfoundation.org/v104/Core/docs/Part6/5.1.2/ 36 | export enum TypeId { 37 | Null = 0 as uint8, // not part of specification but some servers (e.g. Prosys) return it anyway 38 | Boolean = 1 as uint8, 39 | SByte = 2 as uint8, 40 | Byte = 3 as uint8, 41 | Int16 = 4 as uint8, 42 | Uint16 = 5 as uint8, 43 | Int32 = 6 as uint8, 44 | Uint32 = 7 as uint8, 45 | Int64 = 8 as uint8, 46 | Uint64 = 9 as uint8, 47 | Float = 10 as uint8, 48 | Double = 11 as uint8, 49 | String = 12 as uint8, 50 | DateTime = 13 as uint8, 51 | GUID = 14 as uint8, 52 | ByteString = 15 as uint8, 53 | XMLElement = 16 as uint8, 54 | NodeID = 17 as uint8, 55 | ExpandedNodeID = 18 as uint8, 56 | StatusCode = 19 as uint8, 57 | QualifiedName = 20 as uint8, 58 | LocalizedText = 21 as uint8, 59 | ExtensionObject = 22 as uint8, 60 | DataValue = 23 as uint8, 61 | Variant = 24 as uint8, 62 | DiagnosticInfo = 25 as uint8, 63 | } 64 | -------------------------------------------------------------------------------- /tests/ua/ReadRequest.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { 3 | ReadRequest, 4 | RequestHeader, 5 | TimestampsToReturn, 6 | ReadValueId, 7 | } from '../../dist/ua/generated' 8 | import { NewByteStringNodeId, NewFourByteNodeId } from '../../dist/ua/NodeId' 9 | import ExtensionObject from '../../dist/ua/ExtensionObject' 10 | import { AttributeId } from '../../dist/ua/enums' 11 | import QualifiedName from '../../dist/ua/QualifiedName' 12 | 13 | describe('ReadRequest', () => { 14 | run([ 15 | { 16 | name: 'normal', 17 | instance: new ReadRequest({ 18 | RequestHeader: new RequestHeader({ 19 | // prettier-ignore 20 | AuthenticationToken: NewByteStringNodeId(0x00, new Uint8Array([ 21 | 0x08, 0x22, 0x87, 0x62, 0xba, 0x81, 0xe1, 0x11, 22 | 0xa6, 0x43, 0xf8, 0x77, 0x7b, 0xc6, 0x2f, 0xc8, 23 | ])), 24 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 25 | RequestHandle: 1, 26 | AdditionalHeader: new ExtensionObject(), 27 | }), 28 | MaxAge: 0, 29 | TimestampsToReturn: TimestampsToReturn.Both, 30 | NodesToRead: [ 31 | new ReadValueId({ 32 | NodeId: NewFourByteNodeId(0, 2256), 33 | AttributeId: AttributeId.Value, 34 | DataEncoding: new QualifiedName(), 35 | }), 36 | ], 37 | }), 38 | // prettier-ignore 39 | bytes: new Uint8Array([ 40 | 0x05, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08, 41 | 0x22, 0x87, 0x62, 0xba, 0x81, 0xe1, 0x11, 0xa6, 42 | 0x43, 0xf8, 0x77, 0x7b, 0xc6, 0x2f, 0xc8, 0x00, 43 | 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 0x01, 44 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 45 | 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 46 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 47 | 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 48 | 0x00, 0x00, 0x01, 0x00, 0xd0, 0x08, 0x0d, 0x00, 49 | 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 50 | 0xff, 0xff, 0xff, 0xff, 51 | ]) 52 | }, 53 | ]) 54 | }) 55 | -------------------------------------------------------------------------------- /tests/ua/WriteResponse.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { WriteResponse, ResponseHeader } from '../../dist/ua/generated' 3 | import DiagnosticInfo from '../../dist/ua/DiagnosticInfo' 4 | import ExtensionObject from '../../dist/ua/ExtensionObject' 5 | import { StatusCode } from '../../dist/ua/StatusCode' 6 | 7 | describe('WriteResponse', () => { 8 | run([ 9 | { 10 | name: 'single', 11 | instance: new WriteResponse({ 12 | ResponseHeader: new ResponseHeader({ 13 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 14 | RequestHandle: 1, 15 | ServiceDiagnostics: new DiagnosticInfo(), 16 | StringTable: [], 17 | AdditionalHeader: new ExtensionObject(), 18 | }), 19 | Results: new Uint32Array([StatusCode.OK]), 20 | }), 21 | // prettier-ignore 22 | bytes: new Uint8Array([ 23 | 0x00, 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 24 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 25 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 26 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 27 | 0xff, 0xff, 0xff, 0xff, 28 | ]) 29 | }, 30 | { 31 | name: 'single', 32 | instance: new WriteResponse({ 33 | ResponseHeader: new ResponseHeader({ 34 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 35 | RequestHandle: 1, 36 | ServiceDiagnostics: new DiagnosticInfo(), 37 | StringTable: [], 38 | AdditionalHeader: new ExtensionObject(), 39 | }), 40 | Results: new Uint32Array([ 41 | StatusCode.OK, 42 | StatusCode.BadUserAccessDenied, 43 | ]), 44 | }), 45 | // prettier-ignore 46 | bytes: new Uint8Array([ 47 | 0x00, 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 48 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 49 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 50 | 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 51 | 0x00, 0x00, 0x1f, 0x80, 0xff, 0xff, 0xff, 0xff, 52 | ]) 53 | }, 54 | ]) 55 | }) 56 | -------------------------------------------------------------------------------- /src/ua/ExpandedNodeId.ts: -------------------------------------------------------------------------------- 1 | import NodeId, { NewTwoByteNodeId, NewFourByteNodeId } from './NodeId' 2 | import Bucket from './Bucket' 3 | import { uint8, uint16 } from '../types' 4 | 5 | interface Options { 6 | NodeId?: NodeId 7 | NamespaceUri?: string 8 | ServerIndex?: number 9 | } 10 | 11 | // https://reference.opcfoundation.org/v104/Core/docs/Part6/5.2.2/#5.2.2.10 12 | export default class ExpandedNodeId { 13 | public NodeId: NodeId 14 | public NamespaceUri: string 15 | public ServerIndex: number 16 | 17 | constructor(options?: Options) { 18 | this.NodeId = options?.NodeId ?? NewTwoByteNodeId(0) 19 | this.NamespaceUri = options?.NamespaceUri ?? '' 20 | this.ServerIndex = options?.ServerIndex ?? 0 21 | 22 | if (options?.NamespaceUri) { 23 | this.NodeId.setNamespaceUriFlag() 24 | } 25 | 26 | if (options?.ServerIndex) { 27 | this.NodeId.setServerIndexFlag() 28 | } 29 | } 30 | 31 | public encode(): ArrayBuffer { 32 | const bucket = new Bucket() 33 | bucket.writeStruct(this.NodeId) 34 | if (this.hasNamespaceUri()) { 35 | bucket.writeString(this.NamespaceUri) 36 | } 37 | if (this.hasServerIndex()) { 38 | bucket.writeUint32(this.ServerIndex) 39 | } 40 | return bucket.bytes 41 | } 42 | 43 | public decode(b: ArrayBuffer, position?: number): number { 44 | const bucket = new Bucket(b, position) 45 | bucket.readStruct(this.NodeId) 46 | if (this.hasNamespaceUri()) { 47 | this.NamespaceUri = bucket.readString() 48 | } 49 | if (this.hasServerIndex()) { 50 | this.ServerIndex = bucket.readUint32() 51 | } 52 | return bucket.position 53 | } 54 | 55 | public hasNamespaceUri(): boolean { 56 | return ((this.NodeId.Type >> 7) & 0x1) === 1 57 | } 58 | 59 | public hasServerIndex(): boolean { 60 | return ((this.NodeId.Type >> 6) & 0x1) === 1 61 | } 62 | } 63 | 64 | export const NewTwoByteExpandedNodeId = (id: uint8): ExpandedNodeId => 65 | new ExpandedNodeId({ 66 | NodeId: NewTwoByteNodeId(id), 67 | }) 68 | 69 | export const NewFourByteExpandedNodeId = ( 70 | ns: uint8, 71 | id: uint16 72 | ): ExpandedNodeId => 73 | new ExpandedNodeId({ 74 | NodeId: NewFourByteNodeId(ns, id), 75 | }) 76 | -------------------------------------------------------------------------------- /demo/src/references.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, useEffect } from 'react' 2 | import { OPCUAContext } from './context' 3 | import { useParams } from 'react-router-dom' 4 | import { ParseNodeId, NewTwoByteNodeId } from '../../dist/ua/NodeId' 5 | import { 6 | ReferenceDescription, 7 | BrowseRequest, 8 | BrowseDirection, 9 | BrowseResultMask, 10 | BrowseDescription, 11 | } from '../../dist/ua/generated' 12 | import { Id } from '../../dist/id/id' 13 | 14 | const References = () => { 15 | const ctx = useContext(OPCUAContext) 16 | const { id } = useParams() 17 | const NodeId = ParseNodeId(id as string) 18 | 19 | const [references, setReferences] = useState([]) 20 | 21 | const read = async () => { 22 | const response = await ctx.client.browse( 23 | new BrowseRequest({ 24 | NodesToBrowse: [ 25 | new BrowseDescription({ 26 | NodeId, 27 | BrowseDirection: BrowseDirection.Forward, 28 | ReferenceTypeId: NewTwoByteNodeId(Id.References), 29 | IncludeSubtypes: true, 30 | ResultMask: BrowseResultMask.All, 31 | }), 32 | ], 33 | }) 34 | ) 35 | 36 | if (response.Results) { 37 | setReferences(response.Results[0].References as ReferenceDescription[]) 38 | } 39 | } 40 | 41 | useEffect(() => { 42 | read() 43 | }, [id]) 44 | 45 | return ( 46 |
47 |
References
48 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | {references.map((ref, i) => { 62 | return ( 63 | 64 | 68 | 69 | 70 | ) 71 | })} 72 | 73 |
TypeTarget
65 | {Id[ref.ReferenceTypeId.Identifier as number]} ( 66 | {ref.ReferenceTypeId.Identifier}) 67 | {ref.DisplayName.Text}
74 |
75 | ) 76 | } 77 | 78 | export default References 79 | -------------------------------------------------------------------------------- /src/Client.ts: -------------------------------------------------------------------------------- 1 | import SecureChannel from './uasc/SecureChannel' 2 | import { 3 | BrowseRequest, 4 | BrowseResponse, 5 | CreateSubscriptionRequest, 6 | CreateSubscriptionResponse, 7 | ReadRequest, 8 | ReadResponse, 9 | WriteRequest, 10 | CallRequest, 11 | CallResponse, 12 | OpenSecureChannelResponse, 13 | CreateSessionResponse, 14 | ActivateSessionResponse, 15 | } from './ua/generated' 16 | import Subscription from './Subscription' 17 | import AcknowledgeMessage from './uacp/AcknowledgeMessage' 18 | import { uint32 } from './types' 19 | 20 | export default class Client { 21 | public endpointUrl: string 22 | public secureChannel: SecureChannel 23 | public subscriptions: Map 24 | 25 | constructor(endpointUrl: string) { 26 | this.endpointUrl = endpointUrl 27 | this.secureChannel = new SecureChannel(endpointUrl) 28 | this.subscriptions = new Map() 29 | } 30 | 31 | public open(): Promise { 32 | return this.secureChannel.open() 33 | } 34 | 35 | public hello(): Promise { 36 | return this.secureChannel.hello() 37 | } 38 | 39 | public openSecureChannel(): Promise { 40 | return this.secureChannel.openSecureChannel() 41 | } 42 | 43 | public createSession(): Promise { 44 | return this.secureChannel.createSession() 45 | } 46 | 47 | public activateSession(): Promise { 48 | return this.secureChannel.activateSession() 49 | } 50 | 51 | public browse(req: BrowseRequest): Promise { 52 | return new Promise((resolve) => { 53 | this.secureChannel.send(req, resolve) 54 | }) 55 | } 56 | 57 | public read(req: ReadRequest): Promise { 58 | return new Promise((resolve) => { 59 | this.secureChannel.send(req, resolve) 60 | }) 61 | } 62 | 63 | public write(req: WriteRequest): Promise { 64 | return new Promise((resolve) => { 65 | this.secureChannel.send(req, resolve) 66 | }) 67 | } 68 | 69 | public call(req: CallRequest): Promise { 70 | return new Promise((resolve) => { 71 | this.secureChannel.send(req, resolve) 72 | }) 73 | } 74 | 75 | public subscribe( 76 | req: CreateSubscriptionRequest 77 | ): Promise { 78 | return new Promise((resolve) => { 79 | this.secureChannel.send(req, resolve) 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/ua/WriteRequest.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { 3 | WriteRequest, 4 | RequestHeader, 5 | WriteValue, 6 | } from '../../dist/ua/generated' 7 | import { NewByteStringNodeId, NewFourByteNodeId } from '../../dist/ua/NodeId' 8 | import ExtensionObject from '../../dist/ua/ExtensionObject' 9 | import { AttributeId, TypeId } from '../../dist/ua/enums' 10 | import DataValue, { 11 | DataValueValue, 12 | DataValueSourceTimestamp, 13 | DataValueServerTimestamp, 14 | } from '../../dist/ua/DataValue' 15 | import Variant from '../../dist/ua/Variant' 16 | 17 | describe('WriteRequest', () => { 18 | run([ 19 | { 20 | name: 'normal', 21 | instance: new WriteRequest({ 22 | RequestHeader: new RequestHeader({ 23 | // prettier-ignore 24 | AuthenticationToken: NewByteStringNodeId(0x00, new Uint8Array([ 25 | 0x08, 0x22, 0x87, 0x62, 0xba, 0x81, 0xe1, 0x11, 26 | 0xa6, 0x43, 0xf8, 0x77, 0x7b, 0xc6, 0x2f, 0xc8, 27 | ])), 28 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 29 | RequestHandle: 1, 30 | AdditionalHeader: new ExtensionObject(), 31 | }), 32 | NodesToWrite: [ 33 | new WriteValue({ 34 | NodeId: NewFourByteNodeId(0, 2256), 35 | AttributeId: AttributeId.Value, 36 | Value: new DataValue({ 37 | EncodingMask: 38 | DataValueValue | 39 | DataValueSourceTimestamp | 40 | DataValueServerTimestamp, 41 | Value: new Variant({ 42 | EncodingMask: TypeId.Float, 43 | Value: 2.5001699924468994, 44 | }), 45 | SourceTimestamp: new Date(Date.UTC(2018, 8, 17, 14, 28, 29, 112)), 46 | ServerTimestamp: new Date(Date.UTC(2018, 8, 17, 14, 28, 29, 112)), 47 | }), 48 | }), 49 | ], 50 | }), 51 | // prettier-ignore 52 | bytes: new Uint8Array([ 53 | 0x05, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08, 54 | 0x22, 0x87, 0x62, 0xba, 0x81, 0xe1, 0x11, 0xa6, 55 | 0x43, 0xf8, 0x77, 0x7b, 0xc6, 0x2f, 0xc8, 0x00, 56 | 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 0x01, 57 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 58 | 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 59 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 60 | 0xd0, 0x08, 0x0d, 0x00, 0x00, 0x00, 0xff, 0xff, 61 | 0xff, 0xff, 0x0d, 0x0a, 0xc9, 0x02, 0x20, 0x40, 62 | 0x80, 0x3b, 0xe8, 0xb3, 0x92, 0x4e, 0xd4, 0x01, 63 | 0x80, 0x3b, 0xe8, 0xb3, 0x92, 0x4e, 0xd4, 0x01, 64 | ]) 65 | }, 66 | ]) 67 | }) 68 | -------------------------------------------------------------------------------- /src/ua/factory.ts: -------------------------------------------------------------------------------- 1 | import * as generated from './generated' 2 | import Variant from './Variant' 3 | import DiagnosticInfo from './DiagnosticInfo' 4 | import ExpandedNodeId from './ExpandedNodeId' 5 | import ExtensionObject from './ExtensionObject' 6 | import LocalizedText from './LocalizedText' 7 | import Guid from './Guid' 8 | import NodeId from './NodeId' 9 | import ConnectionProtocolMessageHeader from '../uacp/ConnectionProtocolMessageHeader' 10 | import AcknowledgeMessage from '../uacp/AcknowledgeMessage' 11 | import HelloMessage from '../uacp/HelloMessage' 12 | import SequenceHeader from '../uasc/SequenceHeader' 13 | import SymmetricSecurityHeader from '../uasc/SymmetricSecurityHeader' 14 | import AsymmetricSecurityHeader from '../uasc/AsymmetricSecurityHeader' 15 | import SecureConversationMessageHeader from '../uasc/SecureConversationMessageHeader' 16 | import QualifiedName from './QualifiedName' 17 | import DataValue from './DataValue' 18 | 19 | const factory = (name: string): unknown => { 20 | // remove 'EncodingDefaultBinary' when name was generated by id 21 | name = name.replace(/EncodingDefaultBinary/gi, '') 22 | 23 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 24 | if ((generated as any)[name]) { 25 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 26 | return new (generated as any)[name]() 27 | } 28 | switch (name) { 29 | case 'string': 30 | return String() 31 | case 'uint32': 32 | return Number() 33 | case 'Variant': 34 | return new Variant() 35 | case 'DiagnosticInfo': 36 | return new DiagnosticInfo() 37 | case 'ExpandedNodeId': 38 | return new ExpandedNodeId() 39 | case 'ExtensionObject': 40 | return new ExtensionObject() 41 | case 'Guid': 42 | return new Guid() 43 | case 'LocalizedText': 44 | return new LocalizedText() 45 | case 'NodeId': 46 | return new NodeId() 47 | case 'ConnectionProtocolMessageHeader': 48 | return new ConnectionProtocolMessageHeader() 49 | case 'AcknowledgeMessage': 50 | return new AcknowledgeMessage() 51 | case 'HelloMessage': 52 | return new HelloMessage() 53 | case 'SequenceHeader': 54 | return new SequenceHeader() 55 | case 'SymmetricSecurityHeader': 56 | return new SymmetricSecurityHeader() 57 | case 'AsymmetricSecurityHeader': 58 | return new AsymmetricSecurityHeader() 59 | case 'SecureConversationMessageHeader': 60 | return new SecureConversationMessageHeader() 61 | case 'QualifiedName': 62 | return new QualifiedName() 63 | case 'DataValue': 64 | return new DataValue() 65 | default: 66 | throw new Error(`unsupported class name: ${name}`) 67 | } 68 | } 69 | 70 | export default factory 71 | -------------------------------------------------------------------------------- /demo/src/style.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 0.75rem; 3 | } 4 | 5 | .left { 6 | grid-area: left; 7 | // border-right: 1px solid rgba(0,0,0,.1); 8 | } 9 | 10 | .right { 11 | grid-area: right; 12 | } 13 | 14 | .content { 15 | grid-area: content; 16 | } 17 | 18 | .footer { 19 | grid-area: footer; 20 | } 21 | 22 | header { 23 | grid-area: header; 24 | } 25 | 26 | .wrapper { 27 | display: grid; 28 | grid-gap: 20px; 29 | grid-template-columns: 360px auto 240px; 30 | grid-template-areas: 31 | 'header header header' 32 | 'left content right' 33 | 'footer footer footer'; 34 | } 35 | 36 | .list-group > .list-group > .list-group-item { 37 | // border-radius: 0; 38 | padding-left: 2rem; 39 | } 40 | 41 | .list-group + .list-group-item { 42 | border-top-width: 0; 43 | } 44 | 45 | .list-group > .list-group > .list-group-item:first-child { 46 | border-radius: 0; 47 | border-top-width: 0; 48 | } 49 | 50 | .list-group > .list-group > .list-group-item:last-child { 51 | border-radius: 0; 52 | } 53 | 54 | .list-group > .list-group > .list-group > .list-group-item { 55 | padding-left: 2.75rem; 56 | } 57 | 58 | .list-group > .list-group > .list-group > .list-group > .list-group-item { 59 | padding-left: 3.5rem; 60 | } 61 | 62 | .list-group 63 | > .list-group 64 | > .list-group 65 | > .list-group 66 | > .list-group 67 | > .list-group-item { 68 | padding-left: 4.25rem; 69 | } 70 | 71 | .list-group 72 | > .list-group 73 | > .list-group 74 | > .list-group 75 | > .list-group 76 | > .list-group 77 | > .list-group-item { 78 | padding-left: 5rem; 79 | } 80 | 81 | .list-group 82 | > .list-group 83 | > .list-group 84 | > .list-group 85 | > .list-group 86 | > .list-group 87 | > .list-group 88 | > .list-group-item { 89 | padding-left: 5.75rem; 90 | } 91 | 92 | .list-group 93 | > .list-group 94 | > .list-group 95 | > .list-group 96 | > .list-group 97 | > .list-group 98 | > .list-group 99 | > .list-group 100 | > .list-group-item { 101 | padding-left: 6.5rem; 102 | } 103 | 104 | .list-group 105 | > .list-group 106 | > .list-group 107 | > .list-group 108 | > .list-group 109 | > .list-group 110 | > .list-group 111 | > .list-group 112 | > .list-group 113 | > .list-group-item { 114 | padding-left: 7.25rem; 115 | } 116 | 117 | .list-group 118 | > .list-group 119 | > .list-group 120 | > .list-group 121 | > .list-group 122 | > .list-group 123 | > .list-group 124 | > .list-group 125 | > .list-group 126 | > .list-group 127 | > .list-group-item { 128 | padding-left: 8rem; 129 | } 130 | 131 | .list-group 132 | > .list-group 133 | > .list-group 134 | > .list-group 135 | > .list-group 136 | > .list-group 137 | > .list-group 138 | > .list-group 139 | > .list-group 140 | > .list-group 141 | > .list-group 142 | > .list-group-item { 143 | padding-left: 8.75rem; 144 | } 145 | 146 | .list-group-item.active a { 147 | color: white; 148 | } 149 | -------------------------------------------------------------------------------- /demo/src/Breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext, useState } from 'react' 2 | import { OPCUAContext } from './context' 3 | import { 4 | ReferenceDescription, 5 | BrowseRequest, 6 | BrowseDescription, 7 | BrowseDirection, 8 | BrowseResultMask, 9 | ReadRequest, 10 | ReadValueId, 11 | } from '../../dist/ua/generated' 12 | import { useParams, Link } from 'react-router-dom' 13 | import NodeId, { ParseNodeId } from '../../dist/ua/NodeId' 14 | import { AttributeId } from '../../dist/ua/enums' 15 | import LocalizedText from '../../dist/ua/LocalizedText' 16 | import { Id } from '../../dist/id/id' 17 | 18 | const Breadcrumb = () => { 19 | const ctx = useContext(OPCUAContext) 20 | const [references, setReferences] = useState([]) 21 | const { id } = useParams() 22 | const [name, setName] = useState('') 23 | 24 | let refs: ReferenceDescription[] = [] 25 | 26 | // browse parent references 27 | const browse = async (NodeId: NodeId) => { 28 | // stop browsing at the root node and update the breadcrumb 29 | if (NodeId.Identifier == Id.RootFolder) { 30 | setReferences([...refs]) 31 | return 32 | } 33 | 34 | // browse recursively 35 | const req = new BrowseRequest({ 36 | NodesToBrowse: [ 37 | new BrowseDescription({ 38 | NodeId, 39 | BrowseDirection: BrowseDirection.Inverse, 40 | IncludeSubtypes: true, 41 | ResultMask: BrowseResultMask.All, 42 | }), 43 | ], 44 | }) 45 | 46 | const res = await ctx.client.browse(req) 47 | if (res.Results) { 48 | if (res.Results[0].References) { 49 | refs = [...res.Results[0].References, ...refs] 50 | browse(res.Results[0].References[0].NodeId.NodeId) 51 | } 52 | } 53 | } 54 | 55 | // read current node 56 | const read = async (NodeId: NodeId) => { 57 | const response = await ctx.client.read( 58 | new ReadRequest({ 59 | NodesToRead: [ 60 | new ReadValueId({ 61 | NodeId, 62 | AttributeId: AttributeId.DisplayName, 63 | }), 64 | ], 65 | }) 66 | ) 67 | 68 | const results = response.Results 69 | if (results) { 70 | setName((results[0].Value?.Value as LocalizedText).Text) 71 | } 72 | } 73 | 74 | useEffect(() => { 75 | const NodeId = ParseNodeId(id as string) 76 | browse(NodeId) 77 | read(NodeId) 78 | }, [id]) 79 | 80 | return ( 81 | 97 | ) 98 | } 99 | 100 | export default Breadcrumb 101 | -------------------------------------------------------------------------------- /tests/ua/CreateSessionRequest.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { 3 | CreateSessionRequest, 4 | RequestHeader, 5 | ApplicationDescription, 6 | ApplicationType, 7 | } from '../../dist/ua/generated' 8 | import { NewByteStringNodeId } from '../../dist/ua/NodeId' 9 | import LocalizedText, { LocalizedTextText } from '../../dist/ua/LocalizedText' 10 | 11 | describe('CreateSessionRequest', () => { 12 | run([ 13 | { 14 | name: 'normal', 15 | instance: new CreateSessionRequest({ 16 | RequestHeader: new RequestHeader({ 17 | AuthenticationToken: NewByteStringNodeId( 18 | 0x00, 19 | // prettier-ignore 20 | new Uint8Array([ 21 | 0x08, 0x22, 0x87, 0x62, 0xba, 0x81, 0xe1, 0x11, 22 | 0xa6, 0x43, 0xf8, 0x77, 0x7b, 0xc6, 0x2f, 0xc8 23 | ]) 24 | ), 25 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 26 | RequestHandle: 1, 27 | }), 28 | ClientDescription: new ApplicationDescription({ 29 | ApplicationUri: 'app-uri', 30 | ProductUri: 'prod-uri', 31 | ApplicationName: new LocalizedText({ 32 | EncodingMask: LocalizedTextText, 33 | Text: 'app-name', 34 | }), 35 | ApplicationType: ApplicationType.Client, 36 | GatewayServerUri: 'gw-uri', 37 | DiscoveryProfileUri: 'profile-uri', 38 | DiscoveryUrls: ['1', '2'], 39 | }), 40 | ServerUri: 'server-uri', 41 | EndpointUrl: 'endpoint-url', 42 | SessionName: 'session-name', 43 | RequestedSessionTimeout: 6000000, 44 | MaxResponseMessageSize: 65534, 45 | }), 46 | // prettier-ignore 47 | bytes: new Uint8Array([ 48 | 0x05, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08, 49 | 0x22, 0x87, 0x62, 0xba, 0x81, 0xe1, 0x11, 0xa6, 50 | 0x43, 0xf8, 0x77, 0x7b, 0xc6, 0x2f, 0xc8, 0x00, 51 | 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 0x01, 52 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 53 | 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 54 | 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x61, 0x70, 55 | 0x70, 0x2d, 0x75, 0x72, 0x69, 0x08, 0x00, 0x00, 56 | 0x00, 0x70, 0x72, 0x6f, 0x64, 0x2d, 0x75, 0x72, 57 | 0x69, 0x02, 0x08, 0x00, 0x00, 0x00, 0x61, 0x70, 58 | 0x70, 0x2d, 0x6e, 0x61, 0x6d, 0x65, 0x01, 0x00, 59 | 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x67, 0x77, 60 | 0x2d, 0x75, 0x72, 0x69, 0x0b, 0x00, 0x00, 0x00, 61 | 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x2d, 62 | 0x75, 0x72, 0x69, 0x02, 0x00, 0x00, 0x00, 0x01, 63 | 0x00, 0x00, 0x00, 0x31, 0x01, 0x00, 0x00, 0x00, 64 | 0x32, 0x0a, 0x00, 0x00, 0x00, 0x73, 0x65, 0x72, 65 | 0x76, 0x65, 0x72, 0x2d, 0x75, 0x72, 0x69, 0x0c, 66 | 0x00, 0x00, 0x00, 0x65, 0x6e, 0x64, 0x70, 0x6f, 67 | 0x69, 0x6e, 0x74, 0x2d, 0x75, 0x72, 0x6c, 0x0c, 68 | 0x00, 0x00, 0x00, 0x73, 0x65, 0x73, 0x73, 0x69, 69 | 0x6f, 0x6e, 0x2d, 0x6e, 0x61, 0x6d, 0x65, 0xff, 70 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 71 | 0x00, 0x00, 0x00, 0x60, 0xe3, 0x56, 0x41, 0xfe, 72 | 0xff, 0x00, 0x00, 73 | ]) 74 | }, 75 | ]) 76 | }) 77 | -------------------------------------------------------------------------------- /src/ua/encode.ts: -------------------------------------------------------------------------------- 1 | import Bucket from './Bucket' 2 | import { isEncoder, isTypedArray, keyInObject, isNotNullObject } from './guards' 3 | 4 | interface Arguments { 5 | instance: unknown 6 | type?: string 7 | subtype?: string 8 | } 9 | 10 | export const encode = (args: Arguments): ArrayBuffer => { 11 | const { instance } = args 12 | const type = args.type ?? typeof instance 13 | const subtype = args.subtype ?? '' 14 | const bucket = new Bucket() 15 | if (isEncoder(instance)) { 16 | return instance.encode() 17 | } 18 | switch (type) { 19 | case 'boolean': { 20 | bucket.writeBoolean(instance as boolean) 21 | break 22 | } 23 | 24 | case 'int8': { 25 | bucket.writeInt8(instance as number) 26 | break 27 | } 28 | 29 | case 'uint8': { 30 | bucket.writeUint8(instance as number) 31 | break 32 | } 33 | 34 | case 'int16': { 35 | bucket.writeInt16(instance as number) 36 | break 37 | } 38 | 39 | case 'uint16': { 40 | bucket.writeUint16(instance as number) 41 | break 42 | } 43 | 44 | case 'int32': { 45 | bucket.writeInt32(instance as number) 46 | break 47 | } 48 | 49 | case 'uint32': { 50 | bucket.writeUint32(instance as number) 51 | break 52 | } 53 | 54 | case 'int64': { 55 | bucket.writeInt64(BigInt(instance)) 56 | break 57 | } 58 | 59 | case 'uint64': { 60 | bucket.writeUint64(BigInt(instance)) 61 | break 62 | } 63 | 64 | case 'float32': { 65 | bucket.writeFloat32(instance as number) 66 | break 67 | } 68 | 69 | case 'float64': { 70 | bucket.writeFloat64(instance as number) 71 | break 72 | } 73 | 74 | case 'string': { 75 | bucket.writeString(instance as string) 76 | break 77 | } 78 | 79 | case 'Date': { 80 | bucket.writeDate(instance as Date) 81 | break 82 | } 83 | 84 | case 'ByteString': { 85 | bucket.writeByteString(instance as Uint8Array) 86 | break 87 | } 88 | 89 | case 'Array': { 90 | if (instance === null) { 91 | bucket.writeInt32(-1) 92 | return bucket.bytes 93 | } 94 | if (isTypedArray(instance) || Array.isArray(instance)) { 95 | bucket.writeUint32(instance.length) 96 | for (const item of instance) { 97 | const b = encode({ 98 | instance: item, 99 | type: subtype, 100 | }) 101 | bucket.write(b) 102 | } 103 | } 104 | break 105 | } 106 | 107 | // all complex objects, structs and classes 108 | default: { 109 | for (const name of Object.getOwnPropertyNames(instance)) { 110 | if (isNotNullObject(instance) && keyInObject(instance, name)) { 111 | const type = Reflect.getMetadata('design:type', instance, name) 112 | const subtype = Reflect.getMetadata('design:subtype', instance, name) 113 | const b = encode({ 114 | instance: instance[name], 115 | type, 116 | subtype, 117 | }) 118 | bucket.write(b) 119 | } 120 | } 121 | } 122 | } 123 | return bucket.bytes 124 | } 125 | -------------------------------------------------------------------------------- /src/uasc/Message.ts: -------------------------------------------------------------------------------- 1 | import SecureConversationMessageHeader from './SecureConversationMessageHeader' 2 | import AsymmetricSecurityHeader from './AsymmetricSecurityHeader' 3 | import SymmetricSecurityHeader from './SymmetricSecurityHeader' 4 | import SequenceHeader from './SequenceHeader' 5 | import Bucket from '../ua/Bucket' 6 | import ExpandedNodeId from '../ua/ExpandedNodeId' 7 | import { decodeService } from '../ua/service' 8 | import { 9 | MessageTypeMessage, 10 | MessageTypeCloseSecureChannel, 11 | MessageTypeOpenSecureChannel, 12 | } from './MessageType' 13 | 14 | export class ChunkHeader { 15 | public Header: SecureConversationMessageHeader 16 | public SecurityHeader: AsymmetricSecurityHeader | SymmetricSecurityHeader 17 | public SequenceHeader: SequenceHeader 18 | 19 | constructor(options?: { 20 | Header?: SecureConversationMessageHeader 21 | SecurityHeader?: AsymmetricSecurityHeader | SymmetricSecurityHeader 22 | SequenceHeader?: SequenceHeader 23 | }) { 24 | this.Header = options?.Header ?? new SecureConversationMessageHeader() 25 | this.SecurityHeader = 26 | options?.SecurityHeader ?? new AsymmetricSecurityHeader() 27 | this.SequenceHeader = options?.SequenceHeader ?? new SequenceHeader() 28 | } 29 | 30 | public decode(b: ArrayBuffer, position?: number): number { 31 | const bucket = new Bucket(b, position) 32 | 33 | bucket.readStruct(this.Header) 34 | 35 | switch (this.Header.MessageType) { 36 | case MessageTypeOpenSecureChannel: { 37 | this.SecurityHeader = new AsymmetricSecurityHeader() 38 | bucket.readStruct(this.SecurityHeader) 39 | break 40 | } 41 | 42 | case MessageTypeMessage: 43 | case MessageTypeCloseSecureChannel: { 44 | this.SecurityHeader = new SymmetricSecurityHeader() 45 | bucket.readStruct(this.SecurityHeader) 46 | break 47 | } 48 | 49 | default: 50 | break 51 | } 52 | 53 | bucket.readStruct(this.SequenceHeader) 54 | 55 | return bucket.position 56 | } 57 | } 58 | 59 | export class Message { 60 | public ChunkHeader: ChunkHeader 61 | public TypeId: ExpandedNodeId 62 | public Service: unknown 63 | 64 | constructor(options?: { 65 | ChunkHeader?: ChunkHeader 66 | TypeId?: ExpandedNodeId 67 | Service?: unknown 68 | }) { 69 | this.ChunkHeader = options?.ChunkHeader ?? new ChunkHeader() 70 | this.TypeId = options?.TypeId ?? new ExpandedNodeId() 71 | this.Service = options?.Service 72 | } 73 | 74 | public decode(b: ArrayBuffer, position?: number): number { 75 | position = this.ChunkHeader.decode(b) 76 | position = this.ChunkHeader.SequenceHeader.decode(b, position) 77 | 78 | const { typeId, service } = decodeService(b, position) 79 | this.TypeId = typeId 80 | this.Service = service 81 | 82 | return b.byteLength 83 | } 84 | 85 | public encode(): ArrayBuffer { 86 | const body = new Bucket() 87 | body.writeStruct(this.ChunkHeader.SecurityHeader) 88 | body.writeStruct(this.ChunkHeader.SequenceHeader) 89 | body.writeStruct(this.TypeId) 90 | body.writeStruct(this.Service) 91 | 92 | this.ChunkHeader.Header.MessageSize = 12 + body.bytes.byteLength 93 | const bucket = new Bucket() 94 | bucket.writeStruct(this.ChunkHeader.Header) 95 | bucket.write(body.bytes) 96 | return bucket.bytes 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/ua/NodeId.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import NodeId, { 3 | NewTwoByteNodeId, 4 | NewFourByteNodeId, 5 | NewStringNodeId, 6 | NewByteStringNodeId, 7 | ParseNodeId, 8 | NewNumericNodeId, 9 | NewGuidNodeId, 10 | } from '../../dist/ua/NodeId' 11 | 12 | interface Case { 13 | s: string 14 | n: NodeId 15 | } 16 | 17 | describe('NodeId', () => { 18 | run([ 19 | { 20 | name: 'TwoByte', 21 | instance: NewTwoByteNodeId(72), 22 | bytes: new Uint8Array([0x00, 0x48]), 23 | }, 24 | { 25 | name: 'FourByte', 26 | instance: NewFourByteNodeId(5, 1025), 27 | bytes: new Uint8Array([0x01, 0x05, 0x01, 0x04]), 28 | }, 29 | { 30 | name: 'String', 31 | instance: NewStringNodeId(1, 'Hot水'), 32 | // prettier-ignore 33 | bytes: new Uint8Array([ 34 | 0x03, 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x48, 35 | 0x6F, 0x74, 0xe6, 0xb0, 0xb4 36 | ]) 37 | }, 38 | { 39 | name: 'Numeric', 40 | instance: NewNumericNodeId(10, 0xdeadbeef), 41 | // prettier-ignore 42 | bytes: new Uint8Array([ 43 | 0x02, 0x0a, 0x00, 0xef, 0xbe, 0xad, 0xde 44 | ]) 45 | }, 46 | { 47 | name: 'String', 48 | instance: NewStringNodeId(255, 'foobar'), 49 | // prettier-ignore 50 | bytes: new Uint8Array([ 51 | 0x03, 0xff, 0x00, 0x06, 0x00, 0x00, 0x00, 0x66, 52 | 0x6f, 0x6f, 0x62, 0x61, 0x72, 53 | ]) 54 | }, 55 | { 56 | name: 'Guid', 57 | instance: NewGuidNodeId(4660, '72962B91-FA75-4AE6-8D28-B404DC7DAF63'), 58 | // prettier-ignore 59 | bytes: new Uint8Array([ 60 | 0x04, 0x34, 0x12, 0x91, 0x2b, 0x96, 0x72, 0x75, 61 | 0xfa, 0xe6, 0x4a, 0x8d, 0x28, 0xb4, 0x04, 0xdc, 62 | 0x7d, 0xaf, 0x63, 63 | ]) 64 | }, 65 | { 66 | name: 'ByteString', 67 | instance: NewByteStringNodeId( 68 | 32768, 69 | new Uint8Array([0xde, 0xad, 0xbe, 0xef]) 70 | ), 71 | // prettier-ignore 72 | bytes: new Uint8Array([ 73 | 0x05, 0x00, 0x80, 0x04, 0x00, 0x00, 0x00, 0xde, 74 | 0xad, 0xbe, 0xef, 75 | ]) 76 | }, 77 | ]) 78 | 79 | describe('parse', () => { 80 | const cases: Case[] = [ 81 | { s: '', n: NewTwoByteNodeId(0) }, 82 | { s: 'ns=0;i=1', n: NewTwoByteNodeId(1) }, 83 | { s: 'i=1', n: NewTwoByteNodeId(1) }, 84 | { s: 'i=2253', n: NewFourByteNodeId(0, 2253) }, 85 | { s: 'ns=1;i=2', n: NewFourByteNodeId(1, 2) }, 86 | { s: 'ns=256;i=2', n: NewNumericNodeId(256, 2) }, 87 | { s: 'ns=1;i=65536', n: NewNumericNodeId(1, 65536) }, 88 | { s: 'ns=65535;i=65536', n: NewNumericNodeId(65535, 65536) }, 89 | { 90 | s: 'ns=1;g=5eac051c-c313-43d7-b790-24aa2c3cfd37', 91 | n: NewGuidNodeId(1, '5eac051c-c313-43d7-b790-24aa2c3cfd37'), 92 | }, 93 | { 94 | s: 'ns=1;b=YWJj', 95 | n: NewByteStringNodeId(1, new Uint8Array([0x61, 0x62, 0x63])), 96 | }, 97 | { s: 'ns=1;s=a', n: NewStringNodeId(1, 'a') }, 98 | { s: 'ns=1;a', n: NewStringNodeId(1, 'a') }, 99 | ] 100 | for (const c of cases) { 101 | it(`string: ${c.s}`, () => { 102 | const result = ParseNodeId(c.s) 103 | expect(result).toEqual(c.n) 104 | }) 105 | } 106 | }) 107 | 108 | // start testing node id 109 | }) 110 | -------------------------------------------------------------------------------- /tests/ua/FindServersResponse.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { 3 | FindServersResponse, 4 | ResponseHeader, 5 | ApplicationDescription, 6 | ApplicationType, 7 | } from '../../dist/ua/generated' 8 | import DiagnosticInfo from '../../dist/ua/DiagnosticInfo' 9 | import ExtensionObject from '../../dist/ua/ExtensionObject' 10 | import LocalizedText, { LocalizedTextText } from '../../dist/ua/LocalizedText' 11 | 12 | describe('FindServersResponse', () => { 13 | run([ 14 | { 15 | name: 'normal', 16 | instance: new FindServersResponse({ 17 | ResponseHeader: new ResponseHeader({ 18 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 19 | RequestHandle: 1, 20 | ServiceDiagnostics: new DiagnosticInfo(), 21 | StringTable: [], 22 | AdditionalHeader: new ExtensionObject(), 23 | }), 24 | Servers: [ 25 | new ApplicationDescription({ 26 | ApplicationUri: 'app-uri', 27 | ProductUri: 'prod-uri', 28 | ApplicationName: new LocalizedText({ 29 | EncodingMask: LocalizedTextText, 30 | Text: 'app-name', 31 | }), 32 | ApplicationType: ApplicationType.Server, 33 | GatewayServerUri: 'gw-uri', 34 | DiscoveryProfileUri: 'prof-uri', 35 | DiscoveryUrls: ['discov-uri-1', 'discov-uri-2'], 36 | }), 37 | new ApplicationDescription({ 38 | ApplicationUri: 'app-uri', 39 | ProductUri: 'prod-uri', 40 | ApplicationName: new LocalizedText({ 41 | EncodingMask: LocalizedTextText, 42 | Text: 'app-name', 43 | }), 44 | ApplicationType: ApplicationType.Server, 45 | GatewayServerUri: 'gw-uri', 46 | DiscoveryProfileUri: 'prof-uri', 47 | DiscoveryUrls: ['discov-uri-1', 'discov-uri-2'], 48 | }), 49 | ], 50 | }), 51 | // prettier-ignore 52 | bytes: new Uint8Array([ 53 | 0x00, 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 54 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 55 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 56 | 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 57 | 0x61, 0x70, 0x70, 0x2d, 0x75, 0x72, 0x69, 0x08, 58 | 0x00, 0x00, 0x00, 0x70, 0x72, 0x6f, 0x64, 0x2d, 59 | 0x75, 0x72, 0x69, 0x02, 0x08, 0x00, 0x00, 0x00, 60 | 0x61, 0x70, 0x70, 0x2d, 0x6e, 0x61, 0x6d, 0x65, 61 | 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 62 | 0x67, 0x77, 0x2d, 0x75, 0x72, 0x69, 0x08, 0x00, 63 | 0x00, 0x00, 0x70, 0x72, 0x6f, 0x66, 0x2d, 0x75, 64 | 0x72, 0x69, 0x02, 0x00, 0x00, 0x00, 0x0c, 0x00, 65 | 0x00, 0x00, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 66 | 0x2d, 0x75, 0x72, 0x69, 0x2d, 0x31, 0x0c, 0x00, 67 | 0x00, 0x00, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 68 | 0x2d, 0x75, 0x72, 0x69, 0x2d, 0x32, 0x07, 0x00, 69 | 0x00, 0x00, 0x61, 0x70, 0x70, 0x2d, 0x75, 0x72, 70 | 0x69, 0x08, 0x00, 0x00, 0x00, 0x70, 0x72, 0x6f, 71 | 0x64, 0x2d, 0x75, 0x72, 0x69, 0x02, 0x08, 0x00, 72 | 0x00, 0x00, 0x61, 0x70, 0x70, 0x2d, 0x6e, 0x61, 73 | 0x6d, 0x65, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 74 | 0x00, 0x00, 0x67, 0x77, 0x2d, 0x75, 0x72, 0x69, 75 | 0x08, 0x00, 0x00, 0x00, 0x70, 0x72, 0x6f, 0x66, 76 | 0x2d, 0x75, 0x72, 0x69, 0x02, 0x00, 0x00, 0x00, 77 | 0x0c, 0x00, 0x00, 0x00, 0x64, 0x69, 0x73, 0x63, 78 | 0x6f, 0x76, 0x2d, 0x75, 0x72, 0x69, 0x2d, 0x31, 79 | 0x0c, 0x00, 0x00, 0x00, 0x64, 0x69, 0x73, 0x63, 80 | 0x6f, 0x76, 0x2d, 0x75, 0x72, 0x69, 0x2d, 0x32, 81 | ]) 82 | }, 83 | ]) 84 | }) 85 | -------------------------------------------------------------------------------- /src/ua/DiagnosticInfo.ts: -------------------------------------------------------------------------------- 1 | import Bucket from './Bucket' 2 | import { StatusCode } from './StatusCode' 3 | import { uint8, int32, EnDecoder } from '../types' 4 | 5 | interface Options { 6 | EncodingMask?: uint8 7 | SymbolicId?: int32 8 | NamespaceUri?: int32 9 | Locale?: int32 10 | LocalizedText?: int32 11 | AdditionalInfo?: string 12 | InnerStatusCode?: StatusCode 13 | InnerDiagnosticInfo?: null | DiagnosticInfo 14 | } 15 | 16 | export const SymbolicId = 0x1 17 | export const NamespaceUri = 0x2 18 | export const LocalizedText = 0x4 19 | export const Locale = 0x8 20 | export const AdditionalInfo = 0x10 21 | export const InnerStatusCode = 0x20 22 | export const InnerDiagnosticInfo = 0x40 23 | 24 | // https://reference.opcfoundation.org/v104/Core/docs/Part6/5.2.2/#5.2.2.12 25 | export default class DiagnosticInfo implements EnDecoder { 26 | public EncodingMask: uint8 27 | public SymbolicId: int32 28 | public NamespaceUri: int32 29 | public Locale: int32 30 | public LocalizedText: int32 31 | public AdditionalInfo: string 32 | public InnerStatusCode: StatusCode 33 | public InnerDiagnosticInfo: DiagnosticInfo | null 34 | 35 | constructor(options?: Options) { 36 | this.EncodingMask = options?.EncodingMask ?? 0x00 37 | this.SymbolicId = options?.SymbolicId ?? 0 38 | this.NamespaceUri = options?.NamespaceUri ?? 0 39 | this.Locale = options?.Locale ?? 0 40 | this.LocalizedText = options?.LocalizedText ?? 0 41 | this.AdditionalInfo = options?.AdditionalInfo ?? '' 42 | this.InnerStatusCode = options?.InnerStatusCode ?? StatusCode.OK 43 | this.InnerDiagnosticInfo = options?.InnerDiagnosticInfo ?? null 44 | } 45 | 46 | public decode(b: ArrayBuffer, position?: number): number { 47 | const bucket = new Bucket(b, position) 48 | this.EncodingMask = bucket.readUint8() 49 | 50 | if (this.has(SymbolicId)) { 51 | this.SymbolicId = bucket.readInt32() 52 | } 53 | 54 | if (this.has(NamespaceUri)) { 55 | this.NamespaceUri = bucket.readInt32() 56 | } 57 | 58 | if (this.has(Locale)) { 59 | this.Locale = bucket.readInt32() 60 | } 61 | 62 | if (this.has(LocalizedText)) { 63 | this.LocalizedText = bucket.readInt32() 64 | } 65 | 66 | if (this.has(AdditionalInfo)) { 67 | this.AdditionalInfo = bucket.readString() 68 | } 69 | 70 | if (this.has(InnerStatusCode)) { 71 | this.InnerStatusCode = bucket.readUint32() 72 | } 73 | 74 | if (this.has(InnerDiagnosticInfo)) { 75 | this.InnerDiagnosticInfo = new DiagnosticInfo() 76 | bucket.readStruct(this.InnerDiagnosticInfo) 77 | } 78 | 79 | return bucket.position 80 | } 81 | 82 | public encode(): ArrayBuffer { 83 | const bucket = new Bucket() 84 | bucket.writeUint8(this.EncodingMask) 85 | 86 | if (this.has(SymbolicId)) { 87 | bucket.writeInt32(this.SymbolicId) 88 | } 89 | 90 | if (this.has(NamespaceUri)) { 91 | bucket.writeInt32(this.NamespaceUri) 92 | } 93 | 94 | if (this.has(Locale)) { 95 | bucket.writeInt32(this.Locale) 96 | } 97 | 98 | if (this.has(LocalizedText)) { 99 | bucket.writeInt32(this.LocalizedText) 100 | } 101 | 102 | if (this.has(AdditionalInfo)) { 103 | bucket.writeString(this.AdditionalInfo) 104 | } 105 | 106 | if (this.has(InnerStatusCode)) { 107 | bucket.writeUint32(this.InnerStatusCode) 108 | } 109 | 110 | if (this.has(InnerDiagnosticInfo)) { 111 | bucket.writeStruct(this.InnerDiagnosticInfo) 112 | } 113 | 114 | return bucket.bytes 115 | } 116 | 117 | public has(mask: uint8): boolean { 118 | return (this.EncodingMask & mask) === mask 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /tests/ua/EndpointDescription.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { 3 | EndpointDescription, 4 | ApplicationDescription, 5 | ApplicationType, 6 | MessageSecurityMode, 7 | UserTokenPolicy, 8 | UserTokenType, 9 | } from '../../dist/ua/generated' 10 | import LocalizedText, { LocalizedTextText } from '../../dist/ua/LocalizedText' 11 | 12 | describe('EndpointDescription', () => { 13 | run([ 14 | { 15 | name: 'normal', 16 | instance: new EndpointDescription({ 17 | EndpointUrl: 'ep-url', 18 | Server: new ApplicationDescription({ 19 | ApplicationUri: 'app-uri', 20 | ProductUri: 'prod-uri', 21 | ApplicationName: new LocalizedText({ 22 | EncodingMask: LocalizedTextText, 23 | Text: 'app-name', 24 | }), 25 | ApplicationType: ApplicationType.Server, 26 | GatewayServerUri: 'gw-uri', 27 | DiscoveryProfileUri: 'prof-uri', 28 | DiscoveryUrls: ['discov-uri-1', 'discov-uri-2'], 29 | }), 30 | // ServerCertificate: nil, 31 | SecurityMode: MessageSecurityMode.None, 32 | SecurityPolicyUri: 'sec-uri', 33 | UserIdentityTokens: [ 34 | new UserTokenPolicy({ 35 | PolicyId: '1', 36 | TokenType: UserTokenType.Anonymous, 37 | IssuedTokenType: 'issued-token', 38 | IssuerEndpointUrl: 'issuer-uri', 39 | SecurityPolicyUri: 'sec-uri', 40 | }), 41 | new UserTokenPolicy({ 42 | PolicyId: '1', 43 | TokenType: UserTokenType.Anonymous, 44 | IssuedTokenType: 'issued-token', 45 | IssuerEndpointUrl: 'issuer-uri', 46 | SecurityPolicyUri: 'sec-uri', 47 | }), 48 | ], 49 | TransportProfileUri: 'trans-uri', 50 | }), 51 | // prettier-ignore 52 | bytes: new Uint8Array([ 53 | 0x06, 0x00, 0x00, 0x00, 0x65, 0x70, 0x2d, 54 | 0x75, 0x72, 0x6c, 0x07, 0x00, 0x00, 0x00, 55 | 0x61, 0x70, 0x70, 0x2d, 0x75, 0x72, 0x69, 56 | 0x08, 0x00, 0x00, 0x00, 0x70, 0x72, 0x6f, 57 | 0x64, 0x2d, 0x75, 0x72, 0x69, 0x02, 0x08, 58 | 0x00, 0x00, 0x00, 0x61, 0x70, 0x70, 0x2d, 59 | 0x6e, 0x61, 0x6d, 0x65, 0x00, 0x00, 0x00, 60 | 0x00, 0x06, 0x00, 0x00, 0x00, 0x67, 0x77, 61 | 0x2d, 0x75, 0x72, 0x69, 0x08, 0x00, 0x00, 62 | 0x00, 0x70, 0x72, 0x6f, 0x66, 0x2d, 0x75, 63 | 0x72, 0x69, 0x02, 0x00, 0x00, 0x00, 0x0c, 64 | 0x00, 0x00, 0x00, 0x64, 0x69, 0x73, 0x63, 65 | 0x6f, 0x76, 0x2d, 0x75, 0x72, 0x69, 0x2d, 66 | 0x31, 0x0c, 0x00, 0x00, 0x00, 0x64, 0x69, 67 | 0x73, 0x63, 0x6f, 0x76, 0x2d, 0x75, 0x72, 68 | 0x69, 0x2d, 0x32, 0xff, 0xff, 0xff, 0xff, 69 | 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 70 | 0x00, 0x73, 0x65, 0x63, 0x2d, 0x75, 0x72, 71 | 0x69, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 72 | 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x00, 73 | 0x0c, 0x00, 0x00, 0x00, 0x69, 0x73, 0x73, 74 | 0x75, 0x65, 0x64, 0x2d, 0x74, 0x6f, 0x6b, 75 | 0x65, 0x6e, 0x0a, 0x00, 0x00, 0x00, 0x69, 76 | 0x73, 0x73, 0x75, 0x65, 0x72, 0x2d, 0x75, 77 | 0x72, 0x69, 0x07, 0x00, 0x00, 0x00, 0x73, 78 | 0x65, 0x63, 0x2d, 0x75, 0x72, 0x69, 0x01, 79 | 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 80 | 0x00, 0x0c, 0x00, 0x00, 0x00, 0x69, 0x73, 81 | 0x73, 0x75, 0x65, 0x64, 0x2d, 0x74, 0x6f, 82 | 0x6b, 0x65, 0x6e, 0x0a, 0x00, 0x00, 0x00, 83 | 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x2d, 84 | 0x75, 0x72, 0x69, 0x07, 0x00, 0x00, 0x00, 85 | 0x73, 0x65, 0x63, 0x2d, 0x75, 0x72, 0x69, 86 | 0x09, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 87 | 0x6e, 0x73, 0x2d, 0x75, 0x72, 0x69, 0x00, 88 | ]) 89 | }, 90 | ]) 91 | }) 92 | -------------------------------------------------------------------------------- /src/uacp/Connection.ts: -------------------------------------------------------------------------------- 1 | import AcknowledgeMessage from './AcknowledgeMessage' 2 | import HelloMessage from './HelloMessage' 3 | import { encode } from '../ua/encode' 4 | import ConnectionProtocolMessageHeader from './ConnectionProtocolMessageHeader' 5 | 6 | const KB = 1024 7 | const MB = 1024 * KB 8 | const DefaultMaxMessageSize = 2 * MB 9 | 10 | const DefaultReceiveBufferSize = 0xffff 11 | const DefaultSendBufferSize = 0xffff 12 | const DefaultMaxChunkCount = 512 13 | 14 | export default class Connection { 15 | public endpointUrl: string 16 | public socket: WebSocket | null 17 | private resolvers: Map 18 | 19 | constructor(endpointUrl: string) { 20 | this.endpointUrl = endpointUrl 21 | this.socket = null 22 | this.resolvers = new Map() 23 | } 24 | 25 | public onerror = (event: Event): void => { 26 | console.log('onerror', event) 27 | } 28 | 29 | public onmessage = (event: MessageEvent): void => { 30 | const header = new ConnectionProtocolMessageHeader() 31 | const offset = header.decode(event.data) 32 | 33 | switch (header.MessageType) { 34 | case 'ACK': { 35 | const acknowledge = new AcknowledgeMessage() 36 | acknowledge.decode(event.data, offset) 37 | 38 | if (acknowledge.ProtocolVersion !== 0) { 39 | throw new Error('invalid version') 40 | } 41 | 42 | if (acknowledge.MaxChunkCount === 0) { 43 | acknowledge.MaxChunkCount = DefaultMaxChunkCount 44 | } 45 | 46 | if (acknowledge.MaxMessageSize === 0) { 47 | acknowledge.MaxMessageSize = DefaultMaxMessageSize 48 | } 49 | 50 | const resolve = this.resolvers.get('hello') 51 | if (resolve) { 52 | resolve(acknowledge) 53 | this.resolvers.delete('hello') 54 | } 55 | 56 | break 57 | } 58 | 59 | default: 60 | break 61 | } 62 | } 63 | 64 | public open(): Promise { 65 | return new Promise((resolve) => { 66 | this.socket = new WebSocket(this.endpointUrl) 67 | this.socket.binaryType = 'arraybuffer' 68 | this.resolvers.set('open', resolve) 69 | 70 | this.socket.addEventListener('message', this.onmessage) 71 | this.socket.addEventListener('open', this.onopen) 72 | this.socket.addEventListener('error', this.onerror) 73 | }) 74 | } 75 | 76 | public hello(): Promise { 77 | return new Promise((resolve) => { 78 | this.resolvers.set('hello', resolve) 79 | 80 | const hello = new HelloMessage({ 81 | ProtocolVersion: 0, 82 | ReceiveBufferSize: DefaultReceiveBufferSize, 83 | SendBufferSize: DefaultSendBufferSize, 84 | MaxMessageSize: 0, 85 | MaxChunkCount: 0, 86 | EndpointUrl: this.endpointUrl, 87 | }) 88 | this.send('HEL', 'F', hello) 89 | }) 90 | } 91 | 92 | public onopen = (): void => { 93 | const resolve = this.resolvers.get('open') 94 | if (resolve) { 95 | resolve() 96 | this.resolvers.delete('open') 97 | } 98 | } 99 | 100 | public send = ( 101 | MessageType: string, 102 | ChunkType: string, 103 | message: unknown 104 | ): void => { 105 | const body = encode({ instance: message }) 106 | 107 | // 8 bytes for header length 108 | const header = new ConnectionProtocolMessageHeader({ 109 | MessageType, 110 | ChunkType, 111 | MessageSize: body.byteLength + 8, 112 | }).encode() 113 | 114 | const b = new Uint8Array(header.byteLength + body.byteLength) 115 | b.set(new Uint8Array(header)) 116 | b.set(new Uint8Array(body), header.byteLength) 117 | 118 | if (this.socket) { 119 | this.socket.send(b) 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/ua/DataValue.ts: -------------------------------------------------------------------------------- 1 | import Variant from './Variant' 2 | import Bucket from './Bucket' 3 | import { uint8, uint16, uint32 } from '../types' 4 | import { StatusCode } from './StatusCode' 5 | 6 | export const DataValueValue = 0x1 7 | export const DataValueStatusCode = 0x2 8 | export const DataValueSourceTimestamp = 0x4 9 | export const DataValueServerTimestamp = 0x8 10 | export const DataValueSourcePicoseconds = 0x10 11 | export const DataValueServerPicoseconds = 0x20 12 | 13 | interface Options { 14 | EncodingMask?: uint8 15 | Value?: Variant 16 | Status?: StatusCode 17 | SourceTimestamp?: Date 18 | SourcePicoSeconds?: uint16 19 | ServerTimestamp?: Date 20 | ServerPicoSeconds?: uint16 21 | } 22 | 23 | export default class DataValue { 24 | public EncodingMask: uint8 25 | public Value: Variant | null 26 | public Status: StatusCode | null 27 | public SourceTimestamp: Date | null 28 | public SourcePicoSeconds: uint16 | null 29 | public ServerTimestamp: Date | null 30 | public ServerPicoSeconds: uint16 | null 31 | 32 | constructor(options?: Options) { 33 | this.EncodingMask = options?.EncodingMask ?? 0 34 | this.Value = this.has(DataValueValue) ? (options?.Value as Variant) : null 35 | this.Status = this.has(DataValueStatusCode) 36 | ? (options?.Status as StatusCode) 37 | : null 38 | this.SourceTimestamp = this.has(DataValueSourceTimestamp) 39 | ? (options?.SourceTimestamp as Date) 40 | : null 41 | this.SourcePicoSeconds = this.has(DataValueSourcePicoseconds) 42 | ? (options?.SourcePicoSeconds as uint16) 43 | : null 44 | this.ServerTimestamp = this.has(DataValueServerTimestamp) 45 | ? (options?.ServerTimestamp as Date) 46 | : null 47 | this.ServerPicoSeconds = this.has(DataValueServerPicoseconds) 48 | ? (options?.ServerPicoSeconds as uint16) 49 | : null 50 | } 51 | 52 | public encode(): ArrayBuffer { 53 | const bucket = new Bucket() 54 | bucket.writeUint8(this.EncodingMask) 55 | 56 | if (this.has(DataValueValue)) { 57 | bucket.writeStruct(this.Value) 58 | } 59 | 60 | if (this.has(DataValueStatusCode)) { 61 | bucket.writeUint32(this.Status as uint32) 62 | } 63 | 64 | if (this.has(DataValueSourceTimestamp)) { 65 | bucket.writeDate(this.SourceTimestamp as Date) 66 | } 67 | 68 | if (this.has(DataValueSourcePicoseconds)) { 69 | bucket.writeUint16(this.SourcePicoSeconds as uint16) 70 | } 71 | 72 | if (this.has(DataValueServerTimestamp)) { 73 | bucket.writeDate(this.ServerTimestamp as Date) 74 | } 75 | 76 | if (this.has(DataValueServerPicoseconds)) { 77 | bucket.writeUint16(this.ServerPicoSeconds as uint16) 78 | } 79 | 80 | return bucket.bytes 81 | } 82 | 83 | public decode(b: ArrayBuffer, position?: number): number { 84 | const bucket = new Bucket(b, position) 85 | this.EncodingMask = bucket.readUint8() 86 | 87 | if (this.has(DataValueValue)) { 88 | this.Value = new Variant() 89 | bucket.readStruct(this.Value) 90 | } 91 | 92 | if (this.has(DataValueStatusCode)) { 93 | this.Status = bucket.readUint32() 94 | } 95 | 96 | if (this.has(DataValueSourceTimestamp)) { 97 | this.SourceTimestamp = bucket.readDate() 98 | } 99 | 100 | if (this.has(DataValueSourcePicoseconds)) { 101 | this.SourcePicoSeconds = bucket.readUint16() 102 | } 103 | 104 | if (this.has(DataValueServerTimestamp)) { 105 | this.ServerTimestamp = bucket.readDate() 106 | } 107 | 108 | if (this.has(DataValueServerPicoseconds)) { 109 | this.ServerPicoSeconds = bucket.readUint16() 110 | } 111 | 112 | return bucket.position 113 | } 114 | 115 | public has(mask: uint8): boolean { 116 | return (this.EncodingMask & mask) === mask 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/ua/DiagnosticInfo.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import DiagnosticInfo, { 3 | SymbolicId, 4 | NamespaceUri, 5 | LocalizedText, 6 | Locale, 7 | AdditionalInfo, 8 | InnerStatusCode, 9 | InnerDiagnosticInfo, 10 | } from '../../dist/ua/DiagnosticInfo' 11 | 12 | describe('DiagnosticInfo', () => { 13 | run([ 14 | { 15 | name: 'nothing', 16 | instance: new DiagnosticInfo(), 17 | bytes: new Uint8Array([0x00]), 18 | }, 19 | { 20 | name: 'has symbolicID', 21 | instance: new DiagnosticInfo({ 22 | EncodingMask: SymbolicId, 23 | SymbolicId: 1, 24 | }), 25 | // prettier-ignore 26 | bytes: new Uint8Array([ 27 | 0x01, 0x01, 0x00, 0x00, 0x00 28 | ]) 29 | }, 30 | { 31 | name: 'has namespaceURI', 32 | instance: new DiagnosticInfo({ 33 | EncodingMask: NamespaceUri, 34 | NamespaceUri: 2, 35 | }), 36 | // prettier-ignore 37 | bytes: new Uint8Array([ 38 | 0x02, 0x02, 0x00, 0x00, 0x00 39 | ]) 40 | }, 41 | { 42 | name: 'has localizedText', 43 | instance: new DiagnosticInfo({ 44 | EncodingMask: LocalizedText, 45 | LocalizedText: 3, 46 | }), 47 | // prettier-ignore 48 | bytes: new Uint8Array([ 49 | 0x04, 0x03, 0x00, 0x00, 0x00 50 | ]) 51 | }, 52 | { 53 | name: 'has locale', 54 | instance: new DiagnosticInfo({ 55 | EncodingMask: Locale, 56 | Locale: 4, 57 | }), 58 | // prettier-ignore 59 | bytes: new Uint8Array([ 60 | 0x08, 0x04, 0x00, 0x00, 0x00 61 | ]) 62 | }, 63 | { 64 | name: 'has additionalInfo', 65 | instance: new DiagnosticInfo({ 66 | EncodingMask: AdditionalInfo, 67 | AdditionalInfo: 'foobar', 68 | }), 69 | // prettier-ignore 70 | bytes: new Uint8Array([ 71 | 0x10, 0x06, 0x00, 0x00, 0x00, 0x66, 0x6f, 0x6f, 72 | 0x62, 0x61, 0x72 73 | ]) 74 | }, 75 | { 76 | name: 'has innerStatusCode', 77 | instance: new DiagnosticInfo({ 78 | EncodingMask: InnerStatusCode, 79 | InnerStatusCode: 6, 80 | }), 81 | // prettier-ignore 82 | bytes: new Uint8Array([ 83 | 0x20, 0x06, 0x00, 0x00, 0x00 84 | ]) 85 | }, 86 | { 87 | name: 'has innerDiagnosticInfo', 88 | instance: new DiagnosticInfo({ 89 | EncodingMask: InnerDiagnosticInfo, 90 | InnerDiagnosticInfo: new DiagnosticInfo({ 91 | EncodingMask: SymbolicId, 92 | SymbolicId: 7, 93 | }), 94 | }), 95 | // prettier-ignore 96 | bytes: new Uint8Array([ 97 | 0x40, 0x01, 0x07, 0x00, 0x00, 0x00 98 | ]) 99 | }, 100 | { 101 | name: 'has all', 102 | instance: new DiagnosticInfo({ 103 | EncodingMask: 104 | SymbolicId | 105 | NamespaceUri | 106 | LocalizedText | 107 | Locale | 108 | AdditionalInfo | 109 | InnerStatusCode | 110 | InnerDiagnosticInfo, 111 | SymbolicId: 1, 112 | NamespaceUri: 2, 113 | Locale: 3, 114 | LocalizedText: 4, 115 | AdditionalInfo: 'foobar', 116 | InnerStatusCode: 6, 117 | InnerDiagnosticInfo: new DiagnosticInfo({ 118 | EncodingMask: SymbolicId, 119 | SymbolicId: 7, 120 | }), 121 | }), 122 | // prettier-ignore 123 | bytes: new Uint8Array([ 124 | 0x7f, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 125 | 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 126 | 0x00, 0x06, 0x00, 0x00, 0x00, 0x66, 0x6f, 0x6f, 127 | 0x62, 0x61, 0x72, 0x06, 0x00, 0x00, 0x00, 0x01, 128 | 0x07, 0x00, 0x00, 0x00 129 | ]) 130 | }, 131 | ]) 132 | }) 133 | -------------------------------------------------------------------------------- /demo/src/Finder.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext, useState } from 'react' 2 | import { OPCUAContext } from './context' 3 | import { 4 | ReferenceDescription, 5 | BrowseRequest, 6 | BrowseDescription, 7 | BrowseDirection, 8 | BrowseResultMask, 9 | NodeClass, 10 | } from '../../dist/ua/generated' 11 | import { useParams, Link } from 'react-router-dom' 12 | import { ParseNodeId } from '../../dist/ua/NodeId' 13 | import { Folder, Play, FileText, BoxArrowInRight, Layers } from './icons' 14 | 15 | interface IconProps { 16 | nodeClass: NodeClass 17 | } 18 | 19 | const Icon = (props: IconProps) => { 20 | if (props.nodeClass === NodeClass.Object) { 21 | return 22 | } 23 | if (props.nodeClass === NodeClass.Method) { 24 | return 25 | } 26 | if (props.nodeClass === NodeClass.Variable) { 27 | return 28 | } 29 | if (props.nodeClass === NodeClass.ReferenceType) { 30 | return 31 | } 32 | if (props.nodeClass === NodeClass.DataType) { 33 | return 34 | } 35 | return null 36 | } 37 | 38 | const Finder = () => { 39 | const ctx = useContext(OPCUAContext) 40 | const [references, setReferences] = useState([]) 41 | const { id } = useParams() 42 | const NodeId = ParseNodeId(id as string) 43 | 44 | const browse = async () => { 45 | const req = new BrowseRequest({ 46 | NodesToBrowse: [ 47 | new BrowseDescription({ 48 | NodeId, 49 | BrowseDirection: BrowseDirection.Both, 50 | IncludeSubtypes: true, 51 | ResultMask: BrowseResultMask.All, 52 | }), 53 | ], 54 | }) 55 | 56 | const res = await ctx.client.browse(req) 57 | if (res.Results) { 58 | if (res.Results[0].References) { 59 | setReferences(res.Results[0].References) 60 | } 61 | } 62 | } 63 | 64 | useEffect(() => { 65 | browse() 66 | }, [id]) 67 | 68 | return ( 69 | 70 | 71 | {references 72 | .filter( 73 | (ref) => 74 | !( 75 | ref.NodeClass === NodeClass.Unspecified || 76 | ref.NodeClass === NodeClass.ObjectType || 77 | ref.NodeClass === NodeClass.VariableType 78 | ) 79 | ) 80 | .sort((a, b) => a.NodeClass - b.NodeClass) 81 | .map((ref, i) => { 82 | if (!ref.IsForward) { 83 | return ( 84 | 85 | 100 | 101 | ) 102 | } 103 | return ( 104 | 105 | 115 | 116 | 117 | ) 118 | })} 119 | 120 |
86 |
87 | 92 | 96 | .. 97 | 98 |
99 |
106 |
107 | 108 | 109 | 110 | {ref.DisplayName.Text} 111 | 112 | 113 |
114 |
121 | ) 122 | } 123 | 124 | export default Finder 125 | -------------------------------------------------------------------------------- /tests/ua/FindServersOnNetworkResponse.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import { 3 | FindServersOnNetworkResponse, 4 | ResponseHeader, 5 | ServerOnNetwork, 6 | } from '../../dist/ua/generated' 7 | import DiagnosticInfo from '../../dist/ua/DiagnosticInfo' 8 | import ExtensionObject from '../../dist/ua/ExtensionObject' 9 | 10 | describe('FindServersOnNetworkResponse', () => { 11 | run([ 12 | { 13 | name: 'single', 14 | instance: new FindServersOnNetworkResponse({ 15 | ResponseHeader: new ResponseHeader({ 16 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 17 | RequestHandle: 1, 18 | ServiceDiagnostics: new DiagnosticInfo(), 19 | StringTable: [], 20 | AdditionalHeader: new ExtensionObject(), 21 | }), 22 | LastCounterResetTime: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 23 | Servers: [ 24 | new ServerOnNetwork({ 25 | RecordId: 1, 26 | ServerName: 'server-name', 27 | DiscoveryUrl: 'discov-uri', 28 | ServerCapabilities: ['server-cap-1'], 29 | }), 30 | ], 31 | }), 32 | // prettier-ignore 33 | bytes: new Uint8Array([ 34 | 0x00, 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 35 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 36 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 37 | 0x00, 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 38 | 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 39 | 0x0b, 0x00, 0x00, 0x00, 0x73, 0x65, 0x72, 0x76, 40 | 0x65, 0x72, 0x2d, 0x6e, 0x61, 0x6d, 0x65, 0x0a, 41 | 0x00, 0x00, 0x00, 0x64, 0x69, 0x73, 0x63, 0x6f, 42 | 0x76, 0x2d, 0x75, 0x72, 0x69, 0x01, 0x00, 0x00, 43 | 0x00, 0x0c, 0x00, 0x00, 0x00, 0x73, 0x65, 0x72, 44 | 0x76, 0x65, 0x72, 0x2d, 0x63, 0x61, 0x70, 0x2d, 45 | 0x31, 46 | ]) 47 | }, 48 | { 49 | name: 'multiple', 50 | instance: new FindServersOnNetworkResponse({ 51 | ResponseHeader: new ResponseHeader({ 52 | Timestamp: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 53 | RequestHandle: 1, 54 | ServiceDiagnostics: new DiagnosticInfo(), 55 | StringTable: [], 56 | AdditionalHeader: new ExtensionObject(), 57 | }), 58 | LastCounterResetTime: new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)), 59 | Servers: [ 60 | new ServerOnNetwork({ 61 | RecordId: 1, 62 | ServerName: 'server-name', 63 | DiscoveryUrl: 'discov-uri', 64 | ServerCapabilities: ['server-cap-1'], 65 | }), 66 | new ServerOnNetwork({ 67 | RecordId: 1, 68 | ServerName: 'server-name', 69 | DiscoveryUrl: 'discov-uri', 70 | ServerCapabilities: ['server-cap-1', 'server-cap-2'], 71 | }), 72 | ], 73 | }), 74 | // prettier-ignore 75 | bytes: new Uint8Array([ 76 | 0x00, 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 77 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 78 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 79 | 0x00, 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01, 80 | 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 81 | 0x0b, 0x00, 0x00, 0x00, 0x73, 0x65, 0x72, 0x76, 82 | 0x65, 0x72, 0x2d, 0x6e, 0x61, 0x6d, 0x65, 0x0a, 83 | 0x00, 0x00, 0x00, 0x64, 0x69, 0x73, 0x63, 0x6f, 84 | 0x76, 0x2d, 0x75, 0x72, 0x69, 0x01, 0x00, 0x00, 85 | 0x00, 0x0c, 0x00, 0x00, 0x00, 0x73, 0x65, 0x72, 86 | 0x76, 0x65, 0x72, 0x2d, 0x63, 0x61, 0x70, 0x2d, 87 | 0x31, 0x01, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 88 | 0x00, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2d, 89 | 0x6e, 0x61, 0x6d, 0x65, 0x0a, 0x00, 0x00, 0x00, 90 | 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x2d, 0x75, 91 | 0x72, 0x69, 0x02, 0x00, 0x00, 0x00, 0x0c, 0x00, 92 | 0x00, 0x00, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 93 | 0x2d, 0x63, 0x61, 0x70, 0x2d, 0x31, 0x0c, 0x00, 94 | 0x00, 0x00, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 95 | 0x2d, 0x63, 0x61, 0x70, 0x2d, 0x32, 96 | ]) 97 | }, 98 | ]) 99 | }) 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # opcua 3 | 4 | TypeScript / JavaScript OPC UA client for the browser. 5 | 6 | [![github actions](https://github.com/hbm/opcua/workflows/ci/badge.svg)](https://github.com/hbm/opcua/actions?query=workflow%3Aci) 7 | [![seriesci coverage](https://seriesci.com/HBM/opcua/series/master/coverage.svg)](https://seriesci.com/HBM/opcua/series/master/coverage) 8 | 9 | ... work in progress ... 10 | 11 | ## Features 12 | 13 | - OPC UA binary data encoding ([Part 6: Mappings - 5.2.1](https://reference.opcfoundation.org/v104/Core/docs/Part6/5.2.1/)) 14 | - WebSockets transport protocol ([Part 6: Mappings - 7.5.1](https://reference.opcfoundation.org/v104/Core/docs/Part6/7.5.1/)) 15 | 16 | ## Usage 17 | 18 | ```bash 19 | npm i opcua 20 | ``` 21 | 22 | ```ts 23 | import Client from 'opcua' 24 | 25 | const client = new Client('ws://localhost:1234') 26 | 27 | // open socket connection 28 | await client.open() 29 | 30 | // send hello and wait for acknowledge 31 | const ack = await client.hello() 32 | 33 | // open secure channel 34 | const openSecureChannelResponse = await client.openSecureChannel() 35 | 36 | // create session 37 | const createSessionResponse = await client.createSession() 38 | 39 | // activate session 40 | const activateSessionResponse = await client.activateSession() 41 | 42 | // browse root folder 43 | const req = new BrowseRequest({ 44 | NodesToBrowse: [ 45 | new BrowseDescription({ 46 | NodeId: NewTwoByteNodeId(IdRootFolder), 47 | BrowseDirection: BrowseDirectionBoth, 48 | IncludeSubtypes: true, 49 | ResultMask: BrowseResultMaskAll 50 | }) 51 | ] 52 | }) 53 | 54 | const res = await client.browse(req) 55 | for (const result of res.Results as BrowseResult[]) { 56 | for (const ref of result.References as ReferenceDescription[]) { 57 | console.log(ref.DisplayName.Text) 58 | } 59 | } 60 | ``` 61 | 62 | ## Subscriptions 63 | 64 | ... work in progress ... it's pretty complex so we need some diagrams 65 | 66 | ### Queue overflow handling 67 | 68 | https://reference.opcfoundation.org/v104/Core/docs/Part4/5.12.1/#5.12.1.5 69 | 70 | ![queue](doc/queue.svg) 71 | 72 | ## Development 73 | 74 | The source code is written in TypeScript. We use the TypeScript compiler to create the JavaScript files for the browser. 75 | 76 | Some source code files are autogenerated. 77 | 78 | - `src/ua/generated.ts` by `cmd/service/main.go` 79 | - `src/id/id.ts` by `cmd/id/main.go` 80 | - `src/ua/StatusCode.ts` by `cmd/status/main.go` 81 | 82 | Do not edit them by hand. You need [Go](https://golang.org/) on your machine to execute those generators. The schema definition files are located at `./schema`. 83 | 84 | We rely on [reflection and decorators](https://www.typescriptlang.org/docs/handbook/decorators.html) to get types (e.g. `uint32`, `CreateSessionRequest` or arrays of certain types `string[]`) during runtime. In JavaScript a number is always [double-precision 64-bit](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number). OPC UA has much more number types like `int8`, `uint8`, `int16`, `uint16` and so on. In order to get the binary encoding / decoding right we must know exactly how many bits represent a number. 85 | 86 | The client architecture consists of multiple layers. They closely follow the official OPC UA specification. Read the following diagram from bottom to top. On the right side you find the responsibilities for each layer. 87 | 88 | ![layers](doc/layers.svg) 89 | 90 | The OPC UA handshake is quite complex and several steps are necessary to get an active session. Those steps are 91 | 92 | 1. Hello / Acknowledge 93 | 1. Open Secure Channel 94 | 1. Create Session 95 | 1. Activate Session 96 | 97 | The following diagram shows this sequence and highlights response parameters that the client has to store internally (e.g. channel id, token id, authentication token, sequence number, request id). 98 | 99 | ![handshake](doc/handshake.svg) 100 | 101 | You need an OPC UA server implementation that supports WebSockets to test the client. Two options exist: 102 | 103 | 1. Use [open62541](https://github.com/open62541/open62541) with WebSockets enabled 104 | 1. Use [websockify](https://github.com/novnc/websockify) for servers that do not support WebSockets (that only support TCP connections) 105 | -------------------------------------------------------------------------------- /tests/ua/Variant.test.ts: -------------------------------------------------------------------------------- 1 | import run from './run' 2 | import Variant from '../../dist/ua/Variant' 3 | import { TypeId } from '../../dist/ua/enums' 4 | import Guid from '../../dist/ua/Guid' 5 | 6 | describe('Variant', () => { 7 | run([ 8 | { 9 | name: 'boolean', 10 | instance: new Variant({ 11 | EncodingMask: TypeId.Boolean, 12 | Value: false, 13 | }), 14 | bytes: new Uint8Array([0x01, 0x00]), 15 | }, 16 | { 17 | name: 'int8', 18 | instance: new Variant({ 19 | EncodingMask: TypeId.SByte, 20 | Value: -5, 21 | }), 22 | bytes: new Uint8Array([0x02, 0xfb]), 23 | }, 24 | { 25 | name: 'uint8', 26 | instance: new Variant({ 27 | EncodingMask: TypeId.Byte, 28 | Value: 5, 29 | }), 30 | bytes: new Uint8Array([0x03, 0x05]), 31 | }, 32 | { 33 | name: 'int16', 34 | instance: new Variant({ 35 | EncodingMask: TypeId.Int16, 36 | Value: -5, 37 | }), 38 | bytes: new Uint8Array([0x04, 0xfb, 0xff]), 39 | }, 40 | { 41 | name: 'uint16', 42 | instance: new Variant({ 43 | EncodingMask: TypeId.Uint16, 44 | Value: 5, 45 | }), 46 | bytes: new Uint8Array([0x05, 0x05, 0x00]), 47 | }, 48 | { 49 | name: 'int32', 50 | instance: new Variant({ 51 | EncodingMask: TypeId.Int32, 52 | Value: -5, 53 | }), 54 | bytes: new Uint8Array([0x06, 0xfb, 0xff, 0xff, 0xff]), 55 | }, 56 | { 57 | name: 'uint32', 58 | instance: new Variant({ 59 | EncodingMask: TypeId.Uint32, 60 | Value: 5, 61 | }), 62 | bytes: new Uint8Array([0x07, 0x05, 0x00, 0x00, 0x00]), 63 | }, 64 | { 65 | name: 'int64', 66 | instance: new Variant({ 67 | EncodingMask: TypeId.Int64, 68 | Value: BigInt(-5), 69 | }), 70 | // prettier-ignore 71 | bytes: new Uint8Array([ 72 | 0x08, 0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 73 | 0xff 74 | ]) 75 | }, 76 | { 77 | name: 'uint64', 78 | instance: new Variant({ 79 | EncodingMask: TypeId.Uint64, 80 | Value: BigInt(5), 81 | }), 82 | // prettier-ignore 83 | bytes: new Uint8Array([ 84 | 0x09, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 85 | 0x00 86 | ]) 87 | }, 88 | { 89 | name: 'float32', 90 | instance: new Variant({ 91 | EncodingMask: TypeId.Float, 92 | Value: 4.000669956207275, 93 | }), 94 | bytes: new Uint8Array([0x0a, 0x7d, 0x05, 0x80, 0x40]), 95 | }, 96 | { 97 | name: 'float64', 98 | instance: new Variant({ 99 | EncodingMask: TypeId.Double, 100 | Value: 4.00067, 101 | }), 102 | // prettier-ignore 103 | bytes: new Uint8Array([ 104 | 0x0b, 0x71, 0x5a, 0xf0, 0xa2, 0xaf, 0x0, 0x10, 105 | 0x40 106 | ]) 107 | }, 108 | { 109 | name: 'string', 110 | instance: new Variant({ 111 | EncodingMask: TypeId.String, 112 | Value: 'abc', 113 | }), 114 | bytes: new Uint8Array([0x0c, 0x03, 0x00, 0x00, 0x00, 0x61, 0x62, 0x63]), 115 | }, 116 | { 117 | name: 'DateTime', 118 | instance: new Variant({ 119 | EncodingMask: TypeId.DateTime, 120 | Value: new Date(Date.UTC(2018, 8, 17, 14, 28, 29, 112)), 121 | }), 122 | // prettier-ignore 123 | bytes: new Uint8Array([ 124 | 0x0d, 0x80, 0x3b, 0xe8, 0xb3, 0x92, 0x4e, 0xd4, 125 | 0x01 126 | ]) 127 | }, 128 | { 129 | name: 'Guid', 130 | instance: new Variant({ 131 | EncodingMask: TypeId.GUID, 132 | Value: new Guid('72962B91-FA75-4AE6-8D28-B404DC7DAF63'), 133 | }), 134 | // prettier-ignore 135 | bytes: new Uint8Array([ 136 | 0x0e, 0x91, 0x2b, 0x96, 0x72, 0x75, 0xfa, 0xe6, 137 | 0x4a, 0x8d, 0x28, 0xb4, 0x04, 0xdc, 0x7d, 0xaf, 138 | 0x63 139 | ]) 140 | }, 141 | { 142 | name: 'ByteString', 143 | instance: new Variant({ 144 | EncodingMask: TypeId.ByteString, 145 | Value: new Uint8Array([0x01, 0x02, 0x03]), 146 | }), 147 | bytes: new Uint8Array([0x0f, 0x03, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03]), 148 | }, 149 | ]) 150 | }) 151 | -------------------------------------------------------------------------------- /src/ua/decode.ts: -------------------------------------------------------------------------------- 1 | import Bucket from './Bucket' 2 | import { isDecoder, keyInObject, isNotNullObject } from './guards' 3 | import factory from './factory' 4 | import { isPrimitiveType } from './utils' 5 | import { TypedArray } from '../types' 6 | 7 | const NewArray = ( 8 | subtype: string, 9 | length: number 10 | ): TypedArray | Array => { 11 | if (subtype === 'uint8') { 12 | return new Uint8Array(length) 13 | } 14 | if (subtype === 'uint16') { 15 | return new Uint16Array(length) 16 | } 17 | if (subtype === 'uint32') { 18 | return new Uint32Array(length) 19 | } 20 | if (subtype === 'float64') { 21 | return new Float64Array(length) 22 | } 23 | return new Array(length) 24 | } 25 | 26 | interface Arguments { 27 | bytes: ArrayBuffer 28 | instance: unknown 29 | key?: PropertyKey 30 | type?: string 31 | position?: number 32 | } 33 | 34 | const decode = (args: Arguments): number => { 35 | const { bytes, instance, position } = args 36 | const key = args.key ?? '' 37 | const type = args.type ?? typeof instance 38 | 39 | const bucket = new Bucket(bytes, position) 40 | 41 | if (isDecoder(instance)) { 42 | return instance.decode(bytes, position) 43 | } 44 | 45 | switch (type) { 46 | case 'boolean': { 47 | Reflect.set(instance as object, key, bucket.readBoolean()) 48 | break 49 | } 50 | 51 | case 'int8': { 52 | Reflect.set(instance as object, key, bucket.readInt8()) 53 | break 54 | } 55 | 56 | case 'uint8': { 57 | Reflect.set(instance as object, key, bucket.readUint8()) 58 | break 59 | } 60 | 61 | case 'int16': { 62 | Reflect.set(instance as object, key, bucket.readInt16()) 63 | break 64 | } 65 | 66 | case 'uint16': { 67 | Reflect.set(instance as object, key, bucket.readUint16()) 68 | break 69 | } 70 | 71 | case 'int32': { 72 | Reflect.set(instance as object, key, bucket.readInt32()) 73 | break 74 | } 75 | 76 | case 'uint32': { 77 | Reflect.set(instance as object, key, bucket.readUint32()) 78 | break 79 | } 80 | 81 | case 'int64': { 82 | Reflect.set(instance as object, key, bucket.readInt64()) 83 | break 84 | } 85 | 86 | case 'uint64': { 87 | Reflect.set(instance as object, key, bucket.readUint64()) 88 | break 89 | } 90 | 91 | case 'float32': { 92 | Reflect.set(instance as object, key, bucket.readFloat32()) 93 | break 94 | } 95 | 96 | case 'float64': { 97 | Reflect.set(instance as object, key, bucket.readFloat64()) 98 | break 99 | } 100 | 101 | case 'string': { 102 | Reflect.set(instance as object, key, bucket.readString()) 103 | break 104 | } 105 | 106 | case 'Date': { 107 | Reflect.set(instance as object, key, bucket.readDate()) 108 | break 109 | } 110 | 111 | case 'ByteString': { 112 | Reflect.set(instance as object, key, bucket.readByteString()) 113 | break 114 | } 115 | 116 | case 'Array': { 117 | const n = bucket.readInt32() 118 | if (n === -1) { 119 | return bucket.position 120 | } 121 | 122 | // create array by name with length 123 | const subtype = Reflect.getMetadata( 124 | 'design:subtype', 125 | instance as object, 126 | key as string 127 | ) 128 | const a = NewArray(subtype, n) 129 | 130 | for (let i = 0; i < n; i++) { 131 | // create new instance of given type and add it to the array 132 | a[i] = factory(subtype) 133 | // decode instance 134 | bucket.position = decode({ 135 | bytes: bucket.bytes, 136 | instance: isPrimitiveType(subtype) ? a : a[i], 137 | key: i, 138 | type: subtype, 139 | position: bucket.position, 140 | }) 141 | } 142 | Reflect.set(instance as object, key, a) 143 | break 144 | } 145 | 146 | default: 147 | for (const name of Object.getOwnPropertyNames(instance)) { 148 | if (isNotNullObject(instance) && keyInObject(instance, name)) { 149 | const type = Reflect.getMetadata('design:type', instance, name) 150 | bucket.position = decode({ 151 | bytes: bytes, 152 | instance: type === 'object' ? instance[name] : instance, 153 | key: name, 154 | type, 155 | position: bucket.position, 156 | }) 157 | } 158 | } 159 | } 160 | return bucket.position 161 | } 162 | 163 | export default decode 164 | -------------------------------------------------------------------------------- /demo/src/attributes.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, useEffect } from 'react' 2 | import { OPCUAContext } from './context' 3 | import { useParams } from 'react-router-dom' 4 | import { ParseNodeId } from '../../dist/ua/NodeId' 5 | import { 6 | ReadRequest, 7 | ReadValueId, 8 | AttributeWriteMask, 9 | EventNotifierType, 10 | NodeClass, 11 | NodeIdType, 12 | } from '../../dist/ua/generated' 13 | import { AttributeId } from '../../dist/ua/enums' 14 | import QualifiedName from '../../dist/ua/QualifiedName' 15 | import LocalizedText from '../../dist/ua/LocalizedText' 16 | 17 | const Attributes = () => { 18 | const ctx = useContext(OPCUAContext) 19 | const { id } = useParams() 20 | const NodeId = ParseNodeId(id as string) 21 | 22 | const [nodeClass, setNodeClass] = useState(0) 23 | const [browseName, setBrowseName] = useState('') 24 | const [displayName, setDisplayName] = useState('') 25 | const [description, setDescription] = useState('') 26 | const [writeMask, setWriteMask] = useState(0) 27 | const [userWriteMask, setUserWriteMask] = useState(0) 28 | const [eventNotifier, setEventNotifier] = useState(0) 29 | 30 | const read = async () => { 31 | const response = await ctx.client.read( 32 | new ReadRequest({ 33 | NodesToRead: [ 34 | new ReadValueId({ 35 | NodeId, 36 | AttributeId: AttributeId.NodeClass, 37 | }), 38 | new ReadValueId({ 39 | NodeId, 40 | AttributeId: AttributeId.BrowseName, 41 | }), 42 | new ReadValueId({ 43 | NodeId, 44 | AttributeId: AttributeId.DisplayName, 45 | }), 46 | new ReadValueId({ 47 | NodeId, 48 | AttributeId: AttributeId.Description, 49 | }), 50 | new ReadValueId({ 51 | NodeId, 52 | AttributeId: AttributeId.WriteMask, 53 | }), 54 | new ReadValueId({ 55 | NodeId, 56 | AttributeId: AttributeId.UserWriteMask, 57 | }), 58 | new ReadValueId({ 59 | NodeId, 60 | AttributeId: AttributeId.EventNotifier, 61 | }), 62 | ], 63 | }) 64 | ) 65 | 66 | const results = response.Results 67 | if (results) { 68 | setNodeClass(results[0].Value?.Value as number) 69 | setBrowseName((response.Results![1].Value?.Value as QualifiedName).Name) 70 | setDisplayName((response.Results![2].Value?.Value as LocalizedText).Text) 71 | setDescription((response.Results![3].Value?.Value as LocalizedText).Text) 72 | setWriteMask(response.Results![4].Value?.Value as AttributeWriteMask) 73 | setUserWriteMask(response.Results![5].Value?.Value as AttributeWriteMask) 74 | setEventNotifier(response.Results![6].Value?.Value as EventNotifierType) 75 | } 76 | } 77 | 78 | useEffect(() => { 79 | read() 80 | }, [id]) 81 | 82 | return ( 83 |
84 |
Attributes
85 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 139 | 140 | 141 | 142 | 145 | 146 | 147 | 148 | 151 | 152 | 153 |
NameValue
Node Id{id}
Namespace Index{NodeId.Namespace}
Identifier Type 109 | {NodeIdType[NodeId.Type]} ({NodeId.Type}) 110 |
Identifier{NodeId.Identifier}
Node Class 119 | {NodeClass[nodeClass]} ({nodeClass}) 120 |
Browse Name{browseName}
Display Name{displayName}
Description{description}
Write Mask 137 | {AttributeWriteMask[writeMask]} ({writeMask}) 138 |
User Write Mask 143 | {AttributeWriteMask[userWriteMask]} ({userWriteMask}) 144 |
Event Notifier 149 | {EventNotifierType[eventNotifier]} ({eventNotifier}) 150 |
154 |
155 | ) 156 | } 157 | 158 | export default Attributes 159 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "incremental": true /* Enable incremental compilation */, 5 | "target": "ES2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true /* Generates corresponding '.d.ts' file. */, 12 | // "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */, 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./dist/opcua.js", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./dist" /* Redirect output structure to the directory. */, 16 | // "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 17 | "composite": true /* Enable project compilation */, 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 28 | "strictNullChecks": true /* Enable strict null checks. */, 29 | "strictFunctionTypes": true /* Enable strict checking of function types. */, 30 | "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */, 31 | "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 32 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 33 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 34 | 35 | /* Additional Checks */ 36 | "noUnusedLocals": true /* Report errors on unused locals. */, 37 | "noUnusedParameters": true /* Report errors on unused parameters. */, 38 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 39 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, 61 | // "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */, 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/ua/encode.test.ts: -------------------------------------------------------------------------------- 1 | import { Type, TypeArray } from '../../dist/ua/generated' 2 | import { encode } from '../../dist/ua/encode' 3 | import { 4 | int16, 5 | uint8, 6 | int8, 7 | uint16, 8 | int32, 9 | uint32, 10 | int64, 11 | uint64, 12 | float32, 13 | float64, 14 | ByteString, 15 | } from '../../dist/types' 16 | 17 | describe('encoding', () => { 18 | test('true', () => { 19 | class Wrapper { 20 | @Type('boolean') 21 | value = true 22 | } 23 | expect(encode({ instance: new Wrapper() })).toEqual( 24 | new Uint8Array([0x01]).buffer 25 | ) 26 | }) 27 | 28 | test('false', () => { 29 | class Wrapper { 30 | @Type('boolean') 31 | value = false 32 | } 33 | expect(encode({ instance: new Wrapper() })).toEqual( 34 | new Uint8Array([0x00]).buffer 35 | ) 36 | }) 37 | 38 | test('int8', () => { 39 | class Wrapper { 40 | @Type('int8') 41 | value: int8 = -5 42 | } 43 | expect(encode({ instance: new Wrapper() })).toEqual( 44 | new Uint8Array([0xfb]).buffer 45 | ) 46 | }) 47 | 48 | test('uint8', () => { 49 | class Wrapper { 50 | @Type('uint8') 51 | value: uint8 = 5 52 | } 53 | expect(encode({ instance: new Wrapper() })).toEqual( 54 | new Uint8Array([0x05]).buffer 55 | ) 56 | }) 57 | 58 | test('int16', () => { 59 | class Wrapper { 60 | @Type('int16') 61 | value: int16 = -5 62 | } 63 | expect(encode({ instance: new Wrapper() })).toEqual( 64 | new Uint8Array([0xfb, 0xff]).buffer 65 | ) 66 | }) 67 | 68 | test('uint16', () => { 69 | class Wrapper { 70 | @Type('uint16') 71 | value: uint16 = 0x1234 72 | } 73 | expect(encode({ instance: new Wrapper() })).toEqual( 74 | new Uint8Array([0x34, 0x12]).buffer 75 | ) 76 | }) 77 | 78 | test('int32', () => { 79 | class Wrapper { 80 | @Type('int32') 81 | value: int32 = -5 82 | } 83 | expect(encode({ instance: new Wrapper() })).toEqual( 84 | new Uint8Array([0xfb, 0xff, 0xff, 0xff]).buffer 85 | ) 86 | }) 87 | 88 | test('uint32', () => { 89 | class Wrapper { 90 | @Type('uint32') 91 | value: uint32 = 0x12345678 92 | } 93 | expect(encode({ instance: new Wrapper() })).toEqual( 94 | new Uint8Array([0x78, 0x56, 0x34, 0x12]).buffer 95 | ) 96 | }) 97 | 98 | test('int64', () => { 99 | class Wrapper { 100 | @Type('int64') 101 | value: int64 = BigInt(-5) 102 | } 103 | expect(encode({ instance: new Wrapper() })).toEqual( 104 | new Uint8Array([0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]).buffer 105 | ) 106 | }) 107 | 108 | test('uint64', () => { 109 | class Wrapper { 110 | @Type('uint64') 111 | value: uint64 = BigInt('0x1234567890ABCDEF') 112 | } 113 | expect(encode({ instance: new Wrapper() })).toEqual( 114 | new Uint8Array([0xef, 0xcd, 0xab, 0x90, 0x78, 0x56, 0x34, 0x12]).buffer 115 | ) 116 | }) 117 | 118 | test('float32', () => { 119 | class Wrapper { 120 | @Type('float32') 121 | value: float32 = 1.234 122 | } 123 | expect(encode({ instance: new Wrapper() })).toEqual( 124 | new Uint8Array([0xb6, 0xf3, 0x9d, 0x3f]).buffer 125 | ) 126 | }) 127 | 128 | test('float64', () => { 129 | class Wrapper { 130 | @Type('float64') 131 | value: float64 = -1.234 132 | } 133 | expect(encode({ instance: new Wrapper() })).toEqual( 134 | new Uint8Array([0x58, 0x39, 0xb4, 0xc8, 0x76, 0xbe, 0xf3, 0xbf]).buffer 135 | ) 136 | }) 137 | 138 | test('Uint32Array', () => { 139 | class Wrapper { 140 | @TypeArray('uint32') 141 | value: Uint32Array = new Uint32Array([0x1234, 0x4567]) 142 | } 143 | expect(encode({ instance: new Wrapper() })).toEqual( 144 | // prettier-ignore 145 | new Uint8Array([ 146 | 0x02, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 147 | 0x67, 0x45, 0x00, 0x00 148 | ]).buffer 149 | ) 150 | }) 151 | 152 | test('null Uint32Array', () => { 153 | class Wrapper { 154 | @TypeArray('uint32') 155 | value: Uint32Array | null = null 156 | } 157 | expect(encode({ instance: new Wrapper() })).toEqual( 158 | new Uint8Array([0xff, 0xff, 0xff, 0xff]).buffer 159 | ) 160 | }) 161 | 162 | test('empty Uint32Array', () => { 163 | class Wrapper { 164 | @TypeArray('uint32') 165 | value: Uint32Array = new Uint32Array() 166 | } 167 | expect(encode({ instance: new Wrapper() })).toEqual( 168 | new Uint8Array([0x00, 0x00, 0x00, 0x00]).buffer 169 | ) 170 | }) 171 | 172 | test('string', () => { 173 | class Wrapper { 174 | @Type('string') 175 | value = 'abc' 176 | } 177 | expect(encode({ instance: new Wrapper() })).toEqual( 178 | new Uint8Array([0x03, 0x00, 0x00, 0x00, 0x61, 0x62, 0x63]).buffer 179 | ) 180 | }) 181 | 182 | test('empty string', () => { 183 | class Wrapper { 184 | @Type('string') 185 | value = '' 186 | } 187 | expect(encode({ instance: new Wrapper() })).toEqual( 188 | new Uint8Array([0xff, 0xff, 0xff, 0xff]).buffer 189 | ) 190 | }) 191 | 192 | test('ByteString', () => { 193 | class Wrapper { 194 | @Type('ByteString') 195 | value: ByteString = new Uint8Array([0x01, 0x02, 0x03]) 196 | } 197 | expect(encode({ instance: new Wrapper() })).toEqual( 198 | new Uint8Array([0x03, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03]).buffer 199 | ) 200 | }) 201 | 202 | test('Date', () => { 203 | class Wrapper { 204 | @Type('Date') 205 | value: Date = new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)) 206 | } 207 | expect(encode({ instance: new Wrapper() })).toEqual( 208 | new Uint8Array([0x00, 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01]).buffer 209 | ) 210 | }) 211 | }) 212 | -------------------------------------------------------------------------------- /demo/src/index.tsx: -------------------------------------------------------------------------------- 1 | // import Client from '../../dist/Client' 2 | // import Subscription from '../../dist/Subscription' 3 | // import { TypeIdString, AttributeIdEventNotifier } from '../../dist/ua/enums' 4 | // import Variant from '../../dist/ua/Variant' 5 | 6 | import React from 'react' 7 | import { render } from 'react-dom' 8 | import { BrowserRouter as Router, Switch, Route } from 'react-router-dom' 9 | // ;(async function() { 10 | // const client = new Client('ws://localhost:1234') 11 | 12 | // // open socket connection 13 | // await client.open() 14 | // console.log('open') 15 | 16 | // // send hello and wait for acknowledge 17 | // const ack = await client.hello() 18 | // console.log('ack', ack) 19 | 20 | // // open secure channel 21 | // const openSecureChannelResponse = await client.openSecureChannel() 22 | // console.log('open secure channel', openSecureChannelResponse) 23 | 24 | // // create session 25 | // const createSessionResponse = await client.createSession() 26 | // console.log('create session response', createSessionResponse) 27 | 28 | // // activate session 29 | // const activateSessionResponse = await client.activateSession() 30 | // console.log('activate session response', activateSessionResponse) 31 | 32 | // // // browse root folder 33 | // // const req = new BrowseRequest({ 34 | // // NodesToBrowse: [ 35 | // // new BrowseDescription({ 36 | // // NodeId: NewTwoByteNodeId(IdRootFolder), 37 | // // BrowseDirection: BrowseDirectionBoth, 38 | // // IncludeSubtypes: true, 39 | // // ResultMask: BrowseResultMaskAll 40 | // // }) 41 | // // ] 42 | // // }) 43 | 44 | // // const res = await client.browse(req) 45 | // // for (const result of res.Results as BrowseResult[]) { 46 | // // for (const ref of result.References as ReferenceDescription[]) { 47 | // // console.log(ref.DisplayName.Text) 48 | // // } 49 | // // } 50 | 51 | // // create subscription 52 | // const createSubscriptionRequest = new CreateSubscriptionRequest({ 53 | // RequestedPublishingInterval: 1000, 54 | // RequestedLifetimeCount: 60, 55 | // RequestedMaxKeepAliveCount: 20, 56 | // MaxNotificationsPerPublish: 0, 57 | // PublishingEnabled: true 58 | // }) 59 | // const createSubscriptionResponse = await client.subscribe( 60 | // createSubscriptionRequest 61 | // ) 62 | // console.log(createSubscriptionResponse) 63 | 64 | // const sub = new Subscription( 65 | // client, 66 | // createSubscriptionResponse.SubscriptionId 67 | // ) 68 | 69 | // // send some publish requests so we get some notifications 70 | // // but do not wait for the response 71 | // sub.publish(new PublishRequest()) 72 | // sub.publish(new PublishRequest()) 73 | 74 | // // create monitored items 75 | // const createMonitoredItemsRequest = new CreateMonitoredItemsRequest({ 76 | // ItemsToCreate: [ 77 | // new MonitoredItemCreateRequest({ 78 | // ItemToMonitor: new ReadValueId({ 79 | // NodeId: NewFourByteNodeId(0, 2253), 80 | // AttributeId: AttributeIdEventNotifier 81 | // }), 82 | // MonitoringMode: MonitoringModeReporting 83 | // }) 84 | // ] 85 | // }) 86 | 87 | // const createMonitoredItemsResponse = await sub.monitor( 88 | // createMonitoredItemsRequest 89 | // ) 90 | // console.log(createMonitoredItemsResponse) 91 | 92 | // // call a method on an object 93 | // const callRequest = new CallRequest({ 94 | // MethodsToCall: [ 95 | // new CallMethodRequest({ 96 | // ObjectId: NewTwoByteNodeId(IdObjectsFolder), 97 | // MethodId: NewFourByteNodeId(1, 62542), 98 | // InputArguments: [ 99 | // new Variant({ 100 | // EncodingMask: TypeIdString, 101 | // Value: '<- please prepend hello' 102 | // }) 103 | // ] 104 | // }) 105 | // ] 106 | // }) 107 | 108 | // const callResponse = await client.call(callRequest) 109 | // console.log(callResponse) 110 | 111 | // // show the method call result 112 | // for (const result of callResponse.Results as CallMethodResult[]) { 113 | // for (const args of result.OutputArguments as Variant[]) { 114 | // console.log(args.Value) 115 | // } 116 | // } 117 | 118 | // // call a method to trigger an event 119 | // const trigger = new CallRequest({ 120 | // MethodsToCall: [ 121 | // new CallMethodRequest({ 122 | // ObjectId: NewTwoByteNodeId(IdObjectsFolder), 123 | // MethodId: NewFourByteNodeId(1, 62541) 124 | // }) 125 | // ] 126 | // }) 127 | 128 | // const triggerResponse = await client.call(trigger) 129 | // console.log(triggerResponse) 130 | // })() 131 | 132 | import './style.scss' 133 | import { OPCUAProvider } from './context' 134 | import Attributes from './attributes' 135 | import References from './references' 136 | import ListGroup from './ListGroup' 137 | import { Id } from '../../dist/id/id' 138 | import { ReferenceDescription } from '../../dist/ua/generated' 139 | import { NewTwoByteExpandedNodeId } from '../../dist/ua/ExpandedNodeId' 140 | import Finder from './Finder' 141 | import Breadcrumb from './Breadcrumb' 142 | 143 | const App = () => { 144 | return ( 145 | 146 | 147 | 148 | 149 | 150 | ) 151 | } 152 | 153 | const Index = () => { 154 | return ( 155 |
156 |
157 | 162 |
163 |
164 | 171 |
172 |
173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 |
182 |
right
183 |
some footer
184 |
185 | ) 186 | } 187 | 188 | render(, document.getElementById('app')) 189 | // NewTwoByteNodeId(Id.RootFolder) 190 | -------------------------------------------------------------------------------- /demo/src/ListGroup.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, useEffect } from 'react' 2 | import { OPCUAContext } from './context' 3 | import { 4 | ReferenceDescription, 5 | BrowseRequest, 6 | BrowseDescription, 7 | BrowseDirection, 8 | BrowseResultMask, 9 | ReadValueId, 10 | ReadRequest, 11 | NodeClass, 12 | } from '../../dist/ua/generated' 13 | import { Id } from '../../dist/id/id' 14 | import { ChevronDown, ChevronRight, Play, Folder, Server } from './icons' 15 | import { Link, matchPath, useLocation } from 'react-router-dom' 16 | import { AttributeId } from '../../dist/ua/enums' 17 | import LocalizedText from '../../dist/ua/LocalizedText' 18 | import classnames from 'classnames' 19 | 20 | interface Properties { 21 | referenceDescription: ReferenceDescription 22 | } 23 | 24 | // const foo = new Map() 25 | 26 | const Icon = (props: Properties) => { 27 | if (props.referenceDescription.NodeId.NodeId.Identifier === Id.Server) { 28 | return 29 | } 30 | if (props.referenceDescription.NodeClass === NodeClass.Method) { 31 | return 32 | } 33 | return 34 | } 35 | 36 | const ListGroup = (props: Properties) => { 37 | const location = useLocation() 38 | const ctx = useContext(OPCUAContext) 39 | const [isOpen, setIsOpen] = useState(false) 40 | const [name, setName] = useState('') 41 | const [references, setReferences] = useState([]) 42 | 43 | const onClick = () => { 44 | setIsOpen(!isOpen) 45 | browse() 46 | } 47 | 48 | const read = async () => { 49 | const response = await ctx.client.read( 50 | new ReadRequest({ 51 | NodesToRead: [ 52 | new ReadValueId({ 53 | NodeId: props.referenceDescription.NodeId.NodeId, 54 | AttributeId: AttributeId.DisplayName, 55 | }), 56 | ], 57 | }) 58 | ) 59 | if (response.Results) { 60 | setName((response.Results[0].Value?.Value as LocalizedText).Text) 61 | } 62 | } 63 | 64 | const browse = async () => { 65 | const req = new BrowseRequest({ 66 | NodesToBrowse: [ 67 | new BrowseDescription({ 68 | NodeId: props.referenceDescription.NodeId.NodeId, 69 | BrowseDirection: BrowseDirection.Forward, 70 | IncludeSubtypes: true, 71 | ResultMask: BrowseResultMask.All, 72 | }), 73 | ], 74 | }) 75 | 76 | const res = await ctx.client.browse(req) 77 | if (res.Results) { 78 | if (res.Results[0].References) { 79 | setReferences(res.Results[0].References) 80 | } 81 | } 82 | } 83 | 84 | useEffect(() => { 85 | read() 86 | }, []) 87 | 88 | const foo = matchPath<{ id: string }>(location.pathname, { 89 | path: '/id/:id', 90 | }) 91 | 92 | return ( 93 |
    94 |
  • 101 |
    102 | {isOpen ? : } 103 | 104 | 105 | 106 | 107 | 108 | 109 | 112 | {name} 113 | 114 | 115 |
    116 |
  • 117 |
    122 | {references 123 | // .filter(ref => ref.ReferenceTypeId.Identifier === Id.Organizes) 124 | .map((ref, i) => { 125 | if (ref.NodeId.NodeId.Identifier === Id.FolderType) { 126 | return null 127 | } 128 | return 129 | // return ( 130 | // 131 | 132 | //
  • 133 | //
    134 | // 135 | // 136 | // 137 | 138 | // 142 | // {ref.DisplayName.Text} 143 | // 144 | //
    145 | 146 | //
  • 147 | // {ref.ReferenceTypeId.Identifier === Id.Organizes ? ( 148 | // 149 | // ) : ( null 150 | // )} 151 | //
    152 | // ) 153 | })} 154 |
    155 |
156 | ) 157 | } 158 | 159 | // const filter = (ref: ReferenceDescription) => { 160 | // return ( 161 | // ref.IsForward && ref.ReferenceTypeId.Identifier !== Id.HasTypeDefinition 162 | // ) 163 | // } 164 | 165 | // const Objects = () => { 166 | // const ctx = useContext(OPCUAContext) 167 | // const [references, setReferences] = useState([]) 168 | 169 | // const browse = async () => { 170 | // // browse all objects 171 | // const objects = await ctx.client.browse( 172 | // new BrowseRequest({ 173 | // NodesToBrowse: [ 174 | // new BrowseDescription({ 175 | // NodeId: NewTwoByteNodeId(Id.ObjectsFolder), 176 | // BrowseDirection: BrowseDirection.Both, 177 | // IncludeSubtypes: true, 178 | // ResultMask: BrowseResultMask.All 179 | // }) 180 | // ] 181 | // }) 182 | // ) 183 | 184 | // if (objects.Results) { 185 | // if (objects.Results[0].References) { 186 | // setReferences(objects.Results[0].References) 187 | // } 188 | // } 189 | // } 190 | 191 | // useEffect(() => { 192 | // browse() 193 | // }, []) 194 | 195 | // return ( 196 | //
197 | // {references.filter(filter).map((ref, i) => { 198 | // return ( 199 | // 200 | // 204 | // {ref.DisplayName.Text} 205 | // 206 | // 207 | // ) 208 | // })} 209 | //
210 | // ) 211 | // } 212 | 213 | export default ListGroup 214 | -------------------------------------------------------------------------------- /tests/ua/Bucket.test.ts: -------------------------------------------------------------------------------- 1 | import Bucket from '../../dist/ua/Bucket' 2 | import NodeId, { NewTwoByteNodeId } from '../../dist/ua/NodeId' 3 | 4 | describe('Bucket', () => { 5 | test('readInt8', () => { 6 | const bucket = new Bucket(new Uint8Array([0x01]).buffer) 7 | expect(bucket.readInt8()).toEqual(1) 8 | }) 9 | 10 | test('writeInt8', () => { 11 | const bucket = new Bucket() 12 | bucket.writeInt8(1) 13 | const expected = new Uint8Array([0x01]) 14 | expect(bucket.bytes).toEqual(expected.buffer) 15 | }) 16 | 17 | test('readUint8', () => { 18 | const bucket = new Bucket(new Uint8Array([0x01]).buffer) 19 | expect(bucket.readUint8()).toEqual(1) 20 | }) 21 | 22 | test('writeUint8', () => { 23 | const bucket = new Bucket() 24 | bucket.writeUint8(1) 25 | const expected = new Uint8Array([0x01]) 26 | expect(bucket.bytes).toEqual(expected.buffer) 27 | }) 28 | 29 | test('readInt16', () => { 30 | const bucket = new Bucket(new Uint8Array([0x01, 0x00]).buffer) 31 | expect(bucket.readInt16()).toEqual(1) 32 | }) 33 | 34 | test('writeInt16', () => { 35 | const bucket = new Bucket() 36 | bucket.writeInt16(1) 37 | const expected = new Uint8Array([0x01, 0x00]) 38 | expect(bucket.bytes).toEqual(expected.buffer) 39 | }) 40 | 41 | test('readUint16', () => { 42 | const bucket = new Bucket(new Uint8Array([0x01, 0x00]).buffer) 43 | expect(bucket.readUint16()).toEqual(1) 44 | }) 45 | 46 | test('writeUint16', () => { 47 | const bucket = new Bucket() 48 | bucket.writeUint16(1) 49 | const expected = new Uint8Array([0x01, 0x00]) 50 | expect(bucket.bytes).toEqual(expected.buffer) 51 | }) 52 | 53 | test('readInt32', () => { 54 | const bucket = new Bucket(new Uint8Array([0x01, 0x00, 0x00, 0x00]).buffer) 55 | expect(bucket.readInt32()).toEqual(1) 56 | }) 57 | 58 | test('writeInt32', () => { 59 | const bucket = new Bucket() 60 | bucket.writeInt32(1) 61 | const expected = new Uint8Array([0x01, 0x00, 0x00, 0x00]) 62 | expect(bucket.bytes).toEqual(expected.buffer) 63 | }) 64 | 65 | test('readUint32', () => { 66 | const bucket = new Bucket(new Uint8Array([0x01, 0x00, 0x00, 0x00]).buffer) 67 | expect(bucket.readUint32()).toEqual(1) 68 | }) 69 | 70 | test('writeUint32', () => { 71 | const bucket = new Bucket() 72 | bucket.writeUint32(1) 73 | const expected = new Uint8Array([0x01, 0x00, 0x00, 0x00]) 74 | expect(bucket.bytes).toEqual(expected.buffer) 75 | }) 76 | 77 | test('readFloat32', () => { 78 | const bucket = new Bucket(new Uint8Array([0xb6, 0xf3, 0x9d, 0x3f]).buffer) 79 | expect(bucket.readFloat32()).toBeCloseTo(1.234) 80 | }) 81 | 82 | test('writeFloat32', () => { 83 | const bucket = new Bucket() 84 | bucket.writeFloat32(1.234) 85 | const expected = new Uint8Array([0xb6, 0xf3, 0x9d, 0x3f]) 86 | expect(bucket.bytes).toEqual(expected.buffer) 87 | }) 88 | 89 | test('readInt64', () => { 90 | const bucket = new Bucket( 91 | new Uint8Array([0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]).buffer 92 | ) 93 | expect(bucket.readInt64()).toEqual(BigInt(-5)) 94 | }) 95 | 96 | test('writeInt64', () => { 97 | const bucket = new Bucket() 98 | bucket.writeInt64(BigInt(-5)) 99 | // prettier-ignore 100 | const expected = new Uint8Array([ 101 | 0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff 102 | ]) 103 | expect(bucket.bytes).toEqual(expected.buffer) 104 | }) 105 | 106 | test('readUint64', () => { 107 | const bucket = new Bucket( 108 | new Uint8Array([0xef, 0xcd, 0xab, 0x90, 0x78, 0x56, 0x34, 0x12]).buffer 109 | ) 110 | expect(bucket.readUint64()).toEqual(BigInt('0x1234567890ABCDEF')) 111 | }) 112 | 113 | test('writeUint64', () => { 114 | const bucket = new Bucket() 115 | bucket.writeUint64(BigInt('0x1234567890ABCDEF')) 116 | // prettier-ignore 117 | const expected = new Uint8Array([ 118 | 0xef, 0xcd, 0xab, 0x90, 0x78, 0x56, 0x34, 0x12 119 | ]) 120 | expect(bucket.bytes).toEqual(expected.buffer) 121 | }) 122 | 123 | test('readFloat64', () => { 124 | const bucket = new Bucket( 125 | new Uint8Array([0x58, 0x39, 0xb4, 0xc8, 0x76, 0xbe, 0xf3, 0xbf]).buffer 126 | ) 127 | expect(bucket.readFloat64()).toBeCloseTo(-1.234) 128 | }) 129 | 130 | test('writeFloat64', () => { 131 | const bucket = new Bucket() 132 | bucket.writeFloat64(-1.234) 133 | // prettier-ignore 134 | const expected = new Uint8Array([ 135 | 0x58, 0x39, 0xb4, 0xc8, 0x76, 0xbe, 0xf3, 0xbf 136 | ]) 137 | expect(bucket.bytes).toEqual(expected.buffer) 138 | }) 139 | 140 | test('readString', () => { 141 | // prettier-ignore 142 | const bucket = new Bucket(new Uint8Array([ 143 | 0x06, 0x00, 0x00, 0x00, 0x48, 0x6F, 0x74, 0xe6, 144 | 0xb0, 0xb4 145 | ]).buffer) 146 | expect(bucket.readString()).toEqual('Hot水') 147 | }) 148 | 149 | test('writeString', () => { 150 | const bucket = new Bucket() 151 | bucket.writeString('Hot水') 152 | // prettier-ignore 153 | const expected = new Uint8Array([ 154 | 0x06, 0x00, 0x00, 0x00, 0x48, 0x6F, 0x74, 0xe6, 155 | 0xb0, 0xb4 156 | ]) 157 | expect(bucket.bytes).toEqual(expected.buffer) 158 | }) 159 | 160 | test('readStringBytes', () => { 161 | const bucket = new Bucket(new Uint8Array([0x41, 0x43, 0x4b]).buffer) 162 | expect(bucket.readStringBytes(3)).toEqual('ACK') 163 | }) 164 | 165 | test('writeStringBytes', () => { 166 | const bucket = new Bucket() 167 | bucket.writeStringBytes('ACK') 168 | // prettier-ignore 169 | const expected = new Uint8Array([ 170 | 0x41, 0x43, 0x4b 171 | ]) 172 | expect(bucket.bytes).toEqual(expected.buffer) 173 | }) 174 | 175 | test('readStruct', () => { 176 | const bucket = new Bucket(new Uint8Array([0x00, 0x00]).buffer) 177 | const v = new NodeId() 178 | bucket.readStruct(v) 179 | expect(JSON.stringify(v)).toEqual(JSON.stringify(NewTwoByteNodeId(0))) 180 | }) 181 | 182 | test('writeStruct', () => { 183 | const bucket = new Bucket() 184 | bucket.writeStruct(NewTwoByteNodeId(0)) 185 | // prettier-ignore 186 | const expected = new Uint8Array([ 187 | 0x00, 0x00 188 | ]) 189 | expect(bucket.bytes).toEqual(expected.buffer) 190 | }) 191 | 192 | test('readDate', () => { 193 | const bucket = new Bucket( 194 | new Uint8Array([0x00, 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01]).buffer 195 | ) 196 | expect(bucket.readDate()).toEqual( 197 | new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0)) 198 | ) 199 | }) 200 | 201 | test('writeDate', () => { 202 | const bucket = new Bucket() 203 | bucket.writeDate(new Date(Date.UTC(2018, 7, 10, 23, 0, 0, 0))) 204 | // prettier-ignore 205 | const expected = new Uint8Array([ 206 | 0x00, 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01 207 | ]) 208 | expect(bucket.bytes).toEqual(expected.buffer) 209 | }) 210 | }) 211 | -------------------------------------------------------------------------------- /demo/src/icons.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Folder = () => ( 4 | 11 | 12 | 17 | 18 | ) 19 | 20 | export const ChevronRight = () => ( 21 | 28 | 33 | 34 | ) 35 | 36 | export const ChevronDown = () => ( 37 | 44 | 49 | 50 | ) 51 | 52 | export const FolderPlus = () => ( 53 | 60 | 65 | 70 | 75 | 76 | ) 77 | 78 | export const FolderMinus = () => ( 79 | 86 | 91 | 96 | 97 | ) 98 | 99 | export const Play = () => ( 100 | 107 | 112 | 113 | ) 114 | 115 | export const Server = () => ( 116 | 123 | 124 | 125 | 126 | 127 | 128 | ) 129 | 130 | export const FileText = () => ( 131 | 138 | 143 | 148 | 149 | ) 150 | 151 | export const BoxArrowInRight = () => ( 152 | 159 | 164 | 169 | 174 | 175 | ) 176 | 177 | export const Layers = () => ( 178 | 185 | 190 | 195 | 196 | ) 197 | --------------------------------------------------------------------------------