diff --git a/.ci-local/install-msvc-deps.py b/.ci-local/install-msvc-deps.py index 8944f12c..e4bac6fd 100644 --- a/.ci-local/install-msvc-deps.py +++ b/.ci-local/install-msvc-deps.py @@ -11,7 +11,7 @@ import re gendef = 'C:/msys64/mingw64/bin/gendef' -lib = 'C:/Program Files (x86)/Microsoft Visual Studio/2019/Enterprise/VC/Tools/MSVC/14.29.30133/bin/Hostx64/x64/lib.exe' +lib = 'C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Tools/MSVC/14.44.35207/bin/HostX64/x64/lib.exe' curdir = os.getcwd() diff --git a/.ci-local/install-open62541.py b/.ci-local/install-open62541.py index 58510f26..94fb3167 100644 --- a/.ci-local/install-open62541.py +++ b/.ci-local/install-open62541.py @@ -79,24 +79,25 @@ if cue.ci['os'] == 'windows': if cue.ci['compiler'] == 'gcc': generator = 'MinGW Makefiles' - elif cue.ci['compiler'] == 'vs2019': - generator = 'Visual Studio 16 2019' + elif cue.ci['compiler'] == 'vs2022': + generator = 'Visual Studio 17 2022' build_shared = 'ON' if cue.ci['static']: build_shared = 'OFF' - if ver[0] == '1' and ver[1] == '3': - sp.check_call(['patch', '-p1', '-i', os.path.join(curdir, '.ci-local', 'open62541-1.3.patch')], cwd=sdkdir) - - sp.check_call(['cmake', '..', - '-G', generator, - '-DBUILD_SHARED_LIBS={0}'.format(build_shared), - '-DCMAKE_BUILD_TYPE=RelWithDebInfo', - '-DUA_ENABLE_ENCRYPTION=OPENSSL', - '-DUA_ENABLE_ENCRYPTION_OPENSSL=ON', - '-DCMAKE_INSTALL_PREFIX={0}'.format(installdir)], - cwd=builddir) + cmake_args = ['cmake', '..', + '-G', generator, + '-DBUILD_SHARED_LIBS={0}'.format(build_shared), + '-DCMAKE_BUILD_TYPE=RelWithDebInfo', + '-DUA_ENABLE_ENCRYPTION=OPENSSL', + '-DUA_ENABLE_ENCRYPTION_OPENSSL=ON', + '-DCMAKE_INSTALL_PREFIX={0}'.format(installdir)] + + if cue.ci['os'] == 'windows' and cue.ci['compiler'] == 'gcc': + cmake_args.append('-DCMAKE_C_FLAGS=-Wno-error=jump-misses-init') + + sp.check_call(cmake_args, cwd=builddir) sp.check_call(['cmake', '--build', '.', '--config', 'RelWithDebInfo'], cwd=builddir) sp.check_call(['cmake', '--install', '.', '--config', 'RelWithDebInfo'], cwd=builddir) diff --git a/.ci-local/install-sdk.py b/.ci-local/install-sdk.py index 38b4f937..d19e80d8 100644 --- a/.ci-local/install-sdk.py +++ b/.ci-local/install-sdk.py @@ -10,6 +10,8 @@ import re curdir = os.getcwd() +openssl_debs = ['libssl1.1_1.1.1f-1ubuntu2.24_amd64.deb', 'libssl-dev_1.1.1f-1ubuntu2.24_amd64.deb'] +openssl_url = 'http://nz2.archive.ubuntu.com/ubuntu/pool/main/o/openssl' sys.path.append(os.path.join(curdir, '.ci')) import cue @@ -74,3 +76,11 @@ os.remove(os.path.join(cue.toolsdir, enc_name)) os.remove(os.path.join(cue.toolsdir, tar_name)) + + # Downgrade libssl to 1.1.1 on GHA's Ubuntu 22.04 and up + if cue.ci['service'] == 'github-actions' and cue.ci['os'] == 'linux': + for deb in openssl_debs: + sp.check_call(['curl', '-fsSLO', '--retry', '3', '{0}/{1}'.format(openssl_url, deb)], + cwd=cue.toolsdir) + sp.check_call(['sudo', 'dpkg', '-i', deb], + cwd=cue.toolsdir) diff --git a/.ci-local/open62541-1.3.patch b/.ci-local/open62541-1.3.patch deleted file mode 100644 index 0c24cd7c..00000000 --- a/.ci-local/open62541-1.3.patch +++ /dev/null @@ -1,24 +0,0 @@ -diff -uir open62541.orig/include/open62541/types.h open62541/include/open62541/types.h ---- open62541.orig/include/open62541/types.h 2024-08-09 11:27:52.000000000 +0200 -+++ open62541/include/open62541/types.h 2024-09-03 10:58:38.813382735 +0200 -@@ -1067,7 +1067,7 @@ - * If the member is an array, the offset points to the (size_t) length field. - * (The array pointer comes after the length field without any padding.) */ - #ifdef UA_ENABLE_TYPEDESCRIPTION --UA_Boolean -+UA_Boolean UA_EXPORT - UA_DataType_getStructMember(const UA_DataType *type, - const char *memberName, - size_t *outOffset, -diff -uir open62541.orig/src/ua_types.c open62541/src/ua_types.c ---- open62541.orig/src/ua_types.c 2024-09-03 11:00:06.897318665 +0200 -+++ open62541/src/ua_types.c 2024-09-02 13:27:49.688344016 +0200 -@@ -1882,7 +1882,7 @@ - } - - #ifdef UA_ENABLE_TYPEDESCRIPTION --UA_Boolean -+UA_Boolean UA_EXPORT - UA_DataType_getStructMember(const UA_DataType *type, const char *memberName, - size_t *outOffset, const UA_DataType **outMemberType, - UA_Boolean *outIsArray) { diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index e829145d..4b2a5c7a 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -1,31 +1,30 @@ --- name: Bug Report about: Help improve the module +title: "[BUG]" +labels: '' +assignees: '' +type: Bug --- -**Describe the bug** +## Description + A clear and concise description of what the bug is. -**To Reproduce** +## To Reproduce + Steps to reproduce the behavior: -1. Setup server as '...' -2. Create IOC database that '....' -3. Start IOC, do '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Setup (please complete the following information if applicable):** -- OPCUA Support: \[e.g. v0.1\] -- Platform: \[e.g. RHEL7.5\] -- EPICS Base: \[e.g. 3.15.6\] -- Client library: \[e.g. UA SDK 1.5.5\] -- Server: \[e.g. S7-1500\] - -**Additional context** -Add any other context about the problem here. + +1. Setup server as '...' +2. Create IOC database that '....' +3. Start IOC, do '....' +4. See error + +## Setup / Environment: + +- OPCUA Support version: \[e.g. v0.1\] +- Platform/OS: \[e.g. RHEL7.5\] +- EPICS Base version: \[e.g. 7.0.9\] +- Low-Level Client version: \[e.g. UA SDK 1.7.4\] +- Server: \[e.g. S7-1500\] diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index a76908c1..37ff9ee9 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -1,17 +1,27 @@ --- name: Feature Request about: Suggest an idea for the module +title: '' +labels: '' +assignees: '' +type: Feature --- -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. E.g. I'm always frustrated when \[...\] +## Is your feature request related to a problem? Please describe. + +A clear and concise description of what the problem is. +E.g. I'm always frustrated when \[...\] + +## Describe the solution you'd like -**Describe the solution you'd like** A clear and concise description of what you want to happen. -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. +## Describe alternatives you've considered + +A clear and concise description +of any alternative solutions or features you've considered. + +## Additional context -**Additional context** Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/ci-build-open62541-linux.yml b/.github/workflows/ci-build-open62541-linux.yml index 5d98781d..5b85b17c 100644 --- a/.github/workflows/ci-build-open62541-linux.yml +++ b/.github/workflows/ci-build-open62541-linux.yml @@ -19,6 +19,7 @@ on: - '**/*.md' - 'devOpcuaSup/UaSdk/*' pull_request: + workflow_dispatch: env: SETUP_PATH: .ci-local:.ci @@ -43,35 +44,42 @@ jobs: matrix: include: - # Oldest supported Base 7.x + # Base 7.0 - os: ubuntu-22.04 cmp: gcc configuration: default - base: "R7.0.4" - open62541: "1.3.12" - name: "7.0.4 Ub-22 v1.3.12" + base: "7.0" + open62541: "1.4.17" + name: "7.0 Ub-22 v1.4.17" - os: ubuntu-22.04 cmp: gcc configuration: default base: "7.0" - open62541: "1.3.12" - name: "7.0 Ub-22 v1.3.12" + open62541: "1.3.18" + name: "7.0 Ub-22 v1.3.18" - # Oldest supported Base 3.15.x - os: ubuntu-22.04 cmp: gcc configuration: default - base: "R3.15.9" - open62541: "1.3.12" - name: "3.15.7 Ub-22 v1.3.12" + base: "7.0" + open62541: "1.2.10" + name: "7.0 Ub-22 v1.2.10" + + # Base 3.15 + - os: ubuntu-22.04 + cmp: gcc + configuration: default + base: "3.15" + open62541: "1.4.17" + name: "3.15 Ub-22 v1.4.17" - os: ubuntu-22.04 cmp: gcc configuration: default base: "3.15" - open62541: "1.3.12" - name: "3.15 Ub-22 v1.3.12" + open62541: "1.3.18" + name: "3.15 Ub-22 v1.3.18" - os: ubuntu-22.04 cmp: gcc @@ -80,15 +88,24 @@ jobs: open62541: "1.2.10" name: "3.15 Ub-22 v1.2.10" + # Oldest supported Base 7.x - os: ubuntu-22.04 cmp: gcc configuration: default - base: "7.0" - open62541: "1.2.10" - name: "7.0 Ub-22 v1.2.10" + base: "R7.0.4" + open62541: "1.4.17" + name: "7.0.4 Ub-22 v1.4.17" + + # Oldest supported Base 3.15.x + - os: ubuntu-22.04 + cmp: gcc + configuration: default + base: "R3.15.9" + open62541: "1.4.17" + name: "3.15.9 Ub-22 v1.4.17" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - name: Download and compile open62541 client diff --git a/.github/workflows/ci-build-open62541-win.yml b/.github/workflows/ci-build-open62541-win.yml index d104b39a..968cd67c 100644 --- a/.github/workflows/ci-build-open62541-win.yml +++ b/.github/workflows/ci-build-open62541-win.yml @@ -19,6 +19,7 @@ on: - '**/*.md' - 'devOpcuaSup/UaSdk/*' pull_request: + workflow_dispatch: env: SETUP_PATH: .ci-local:.ci @@ -38,49 +39,71 @@ jobs: BCFG: ${{ matrix.configuration }} BASE: ${{ matrix.base }} OPEN62541: ${{ matrix.open62541 }} + CHOCO: openssl strategy: fail-fast: false matrix: include: - - os: windows-2019 - cmp: vs2019 + - os: windows-2022 + cmp: vs2022 configuration: default base: "3.15" - open62541: "1.3.12" - name: "3.15 Win2019 v1.3.12 VS2019 DLL" + open62541: "1.3.18" + name: "3.15 Win2022 v1.3.18 VS2022 DLL" - - os: windows-2019 - cmp: vs2019 + - os: windows-2022 + cmp: vs2022 configuration: default base: "7.0" - open62541: "1.3.12" - name: "7.0 Win2019 v1.3.12 VS2019 DLL" + open62541: "1.3.18" + name: "7.0 Win2022 v1.3.18 VS2022 DLL" + + - os: windows-2022 + cmp: vs2022 + configuration: default + base: "3.15" + open62541: "1.4.17" + name: "3.15 Win2022 v1.4.17 VS2022 DLL" + + - os: windows-2022 + cmp: vs2022 + configuration: default + base: "7.0" + open62541: "1.4.17" + name: "7.0 Win2022 v1.4.17 VS2022 DLL" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true + # Install gendef.exe + - uses: msys2/setup-msys2@v2 + with: + release: false + msystem: mingw64 + install: >- + mingw-w64-x86_64-tools-git - name: Download and compile open62541 client run: | - python .ci-local/install-open62541.py + python3 .ci-local/install-open62541.py - name: Prepare build dependencies run: | - python .ci/cue.py prepare + python3 .ci/cue.py prepare - name: Install MSVC dependencies run: | - python .ci-local/install-msvc-deps.py + python3 .ci-local/install-msvc-deps.py - name: Add DLL locations to PATH run: | "C:/Users/runneradmin/.cache/open62541/bin" | Out-File -FilePath $env:GITHUB_PATH -Append "C:/Users/runneradmin/.cache/libxml2-2.9.3/bin" | Out-File -FilePath $env:GITHUB_PATH -Append "C:/Users/runneradmin/.cache/iconv-1.14/bin" | Out-File -FilePath $env:GITHUB_PATH -Append - name: Build main module - run: python .ci/cue.py build + run: python3 .ci/cue.py build - name: Run main module tests - run: python .ci/cue.py test + run: python3 .ci/cue.py test - name: Collect and show test results - run: python .ci/cue.py test-results + run: python3 .ci/cue.py test-results build-msys2: name: ${{ matrix.name }} @@ -100,17 +123,30 @@ jobs: matrix: include: - - os: windows-2019 + - os: windows-2022 configuration: default base: "3.15" - open62541: "1.3.12" - name: "3.15 Win2019 v1.3.12 MinGW DLL" + open62541: "1.3.18" + name: "3.15 Win2022 v1.3.18 MinGW DLL" - - os: windows-2019 + - os: windows-2022 configuration: default base: "7.0" - open62541: "1.3.12" - name: "7.0 Win2019 v1.3.12 MinGW DLL" + open62541: "1.3.18" + name: "7.0 Win2022 v1.3.18 MinGW DLL" + + - os: windows-2022 + configuration: default + base: "3.15" + open62541: "1.4.17" + name: "3.15 Win2022 v1.3.18 MinGW DLL" + + - os: windows-2022 + configuration: default + base: "7.0" + open62541: "1.4.17" + name: "7.0 Win2022 v1.4.17 MinGW DLL" + steps: - uses: msys2/setup-msys2@v2 @@ -119,23 +155,28 @@ jobs: msystem: mingw64 install: >- git + make + patch + mingw-w64-x86_64-toolchain + mingw-w64-x86_64-cmake + mingw-w64-x86_64-python pacboy: openssl:p - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - name: Download and compile open62541 client run: | - python .ci-local/install-open62541.py + python3 .ci-local/install-open62541.py - name: Prepare build dependencies run: | - python .ci/cue.py prepare + python3 .ci/cue.py prepare - name: Build main module run: | export PATH=$PATH:/c/Users/runneradmin/.cache/open62541/bin - python .ci/cue.py build + python3 .ci/cue.py build - name: Run main module tests run: | export PATH=$PATH:/c/Users/runneradmin/.cache/open62541/bin - python .ci/cue.py test + python3 .ci/cue.py test - name: Collect and show test results - run: python .ci/cue.py test-results + run: python3 .ci/cue.py test-results diff --git a/.github/workflows/ci-build-uasdk-linux.yml b/.github/workflows/ci-build-uasdk-linux.yml index ff2e5b12..8b45364d 100644 --- a/.github/workflows/ci-build-uasdk-linux.yml +++ b/.github/workflows/ci-build-uasdk-linux.yml @@ -19,6 +19,7 @@ on: - '**/*.md' - 'devOpcuaSup/open62541/*' pull_request: + workflow_dispatch: env: SETUP_PATH: .ci-local:.ci @@ -44,65 +45,65 @@ jobs: include: # Oldest supported Base 7.x - - os: ubuntu-20.04 + - os: ubuntu-22.04 cmp: gcc configuration: default base: "R7.0.4" uasdk: "1.6.3" - name: "7.0.4 Ub-20 v1.6.3" + name: "7.0.4 Ub-22 v1.6.3" - - os: ubuntu-20.04 + - os: ubuntu-22.04 cmp: gcc configuration: default base: "7.0" uasdk: "1.6.3" - name: "7.0 Ub-20 v1.6.3" + name: "7.0 Ub-22 v1.6.3" # Oldest supported Base 3.15.x - - os: ubuntu-20.04 + - os: ubuntu-22.04 cmp: gcc configuration: default - base: "R3.15.7" + base: "R3.15.9" uasdk: "1.6.3" - name: "3.15.7 Ub-20 v1.6.3" + name: "3.15.9 Ub-22 v1.6.3" - - os: ubuntu-20.04 + - os: ubuntu-22.04 cmp: gcc configuration: default base: "3.15" uasdk: "1.6.3" - name: "3.15 Ub-20 v1.6.3" + name: "3.15 Ub-22 v1.6.3" - - os: ubuntu-20.04 + - os: ubuntu-22.04 cmp: gcc configuration: default base: "R7.0.4" uasdk: "1.7.2" - name: "7.0.4 Ub-20 v1.7.2" + name: "7.0.4 Ub-22 v1.7.2" - - os: ubuntu-20.04 + - os: ubuntu-22.04 cmp: gcc configuration: default base: "7.0" uasdk: "1.7.2" - name: "7.0 Ub-20 v1.7.2" + name: "7.0 Ub-22 v1.7.2" - - os: ubuntu-20.04 + - os: ubuntu-22.04 cmp: gcc configuration: default - base: "R3.15.7" + base: "R3.15.9" uasdk: "1.7.2" - name: "3.15.7 Ub-20 v1.7.2" + name: "3.15.9 Ub-22 v1.7.2" - - os: ubuntu-20.04 + - os: ubuntu-22.04 cmp: gcc configuration: default base: "3.15" uasdk: "1.7.2" - name: "3.15 Ub-20 v1.7.2" + name: "3.15 Ub-22 v1.7.2" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - name: Download and install UASDK client diff --git a/.github/workflows/ci-build-uasdk-win.yml b/.github/workflows/ci-build-uasdk-win.yml index e967e2c6..0a403d34 100644 --- a/.github/workflows/ci-build-uasdk-win.yml +++ b/.github/workflows/ci-build-uasdk-win.yml @@ -19,6 +19,7 @@ on: - '**/*.md' - 'devOpcuaSup/open62541/*' pull_request: + workflow_dispatch: env: SETUP_PATH: .ci-local:.ci @@ -43,38 +44,38 @@ jobs: matrix: include: - - os: windows-2019 - cmp: vs2019 + - os: windows-2022 + cmp: vs2022 configuration: default base: "3.15" uasdk: "1.6.3" - name: "3.15 Win2019 v1.6.3 VS2019 DLL" + name: "3.15 Win2022 v1.6.3 VS2022 DLL" - - os: windows-2019 - cmp: vs2019 + - os: windows-2022 + cmp: vs2022 configuration: default base: "7.0" uasdk: "1.6.3" - name: "7.0 Win2019 v1.6.3 VS2019 DLL" + name: "7.0 Win2022 v1.6.3 VS2022 DLL" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - name: Download and install UASDK client run: | - python .ci-local/install-sdk.py + python3 .ci-local/install-sdk.py - name: Prepare build dependencies run: | - python .ci/cue.py prepare + python3 .ci/cue.py prepare - name: Add DLL locations to PATH run: | "C:/Users/runneradmin/.source/sdk/bin" | Out-File -FilePath $env:GITHUB_PATH -Append "C:/Users/runneradmin/.source/sdk/third-party/win64/vs2015/libxml2/out32dll" | Out-File -FilePath $env:GITHUB_PATH -Append "C:/Users/runneradmin/.source/sdk/third-party/win64/vs2015/openssl/out32dll" | Out-File -FilePath $env:GITHUB_PATH -Append - name: Build main module - run: python .ci/cue.py build + run: python3 .ci/cue.py build - name: Run main module tests - run: python .ci/cue.py test + run: python3 .ci/cue.py test - name: Collect and show test results - run: python .ci/cue.py test-results + run: python3 .ci/cue.py test-results diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..bdc9b4fb --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,64 @@ +name: Build and Deploy Documentation + +on: + push: + branches: + - master + paths: + - 'documentation/**' + - '.github/workflows/docs.yml' + pull_request: + paths: + - 'documentation/**' + - '.github/workflows/docs.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + sudo apt-get update && sudo apt-get install -y doxygen + python -m pip install --upgrade pip + pip install "sphinx<8" "docutils<0.21" sphinx-rtd-theme myst-parser breathe + + - name: Run Doxygen + run: doxygen documentation/Doxyfile + + - name: Build HTML + run: sphinx-build -b html documentation documentation/build/html + + - name: Upload artifact + uses: actions/upload-pages-artifact@v5 + with: + path: 'documentation/build/html' + + deploy: + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.gitignore b/.gitignore index 0ec6b757..18dfd0e8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ /include/ /templates/ /configure/*.local +/documentation/build/ envPaths dllPath.bat @@ -23,11 +24,13 @@ dllPath.bat /*Top/configure/*.local O.*/ +.iocsh_history /QtC-* /.qtc_* .idea/ *.orig *.log +*.autosave /devOpcuaSup/.vs/devOpcuaSup/v16 /devOpcuaSup/x64 *.sln diff --git a/README.md b/README.md index f64fdf94..0ac44589 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![open62541][badge.open62541-win]](https://github.com/epics-modules/opcua/actions/workflows/ci-build-open62541-win.yml) [![UA SDK][badge.uasdk-win]](https://github.com/epics-modules/opcua/actions/workflows/ci-build-uasdk-win.yml) -# opcua - EPICS Device Support for OPC UA +# EPICS Device Support for OPC UA [EPICS](https://epics-controls.org) Device Support module for interfacing to the OPC UA protocol. @@ -16,147 +16,22 @@ of the low level client library. Linux and Windows native builds are supported. -## Status - -The module is under development, but releases are stable and robust. - -There are two choices for the low-level OPC UA client library: - -1. The commercially available - [Unified Automation C++ Based OPC UA Client SDK][unified.sdk]. \ - This is the original implementation. - -2. The open source client of the [open62541 project SDK][open62541.sdk]. \ - This integration is relatively new (2024) - and hasn't seen much in-production use yet. - -## Prerequisites - -* A C++ compiler that supports the C++11 standard. \ - Microsoft Visual C++ needs to be from Visual Studio 2015 or newer. - g++ needs to be 4.6 or above. - -* [EPICS Base](https://epics-controls.org/resources-and-support/base/) - 3.15 (>= 3.15.7) or EPICS 7 (>= 7.0.4). - -* The [gtest module](https://github.com/epics-modules/gtest) if you want - to compile and run the unit tests that are based on Google Test. - -### Using the Unified Automation Client SDK - -* Unified Automation C++ Based [OPC UA Client SDK][unified.sdk] - (1.5/1.6/1.7 are supported, as well as their evaluation bundles; - 1.8 is having trouble). - -* For OPC UA security support (authentication/encryption), you need - openssl/libcrypto on your system - both when compiling the SDK and when - generating any binaries (IOCs). - -* In `CONFIG_SITE.local`, set `UASDK` to the path of your SDK installation. - -* For more details, refer to the `README.md` in the - [`devOpcuaSup/UaSdk`][uasdk.dir] directory. - -### Using the open62541 SDK - -* The [open62541 SDK][open62541.sdk] \ - Choose a recent release (1.2 and 1.3 are supported). - Download the sources using GitHub releases, not through the project website. - -* For OPC UA security support (authentication/encryption), you need - openssl/libcrypto on your system - both when compiling the SDK and when - generating any binaries (IOCs). - -* In `CONFIG_SITE.local`, set `OPEN62541` to the path of your SDK installation. - -* For more details, refer to the `README.md` in the - [`devOpcuaSup/open62541`][open62541.dir] directory. - -## Building the module - -This is a standard EPICS module. - -Inside the `configure` subdirectory or one level above the TOP location -(TOP is where this README file resides), create a file `RELEASE.local` -that sets `EPICS_BASE` and `GTEST` to the absolute paths inside your EPICS -installation. The `GTEST` module is needed to compile and run the tests. -Not defining it produces a clean build, but without any tests. - -Configure the compiler on Linux to use the C++11 standard by adding - -```makefile -USR_CXXFLAGS_Linux += -std=c++11 -``` - -to the `CONFIG_SITE` file (or one of the host/target specific site -configuration files). \ -It is preferable to set this option globally in EPICS Base. - -The configuration necessary when building against a specific client library -is documented in the `README.md` file inside the respective subdirectory of -`devOpcuaSup`. - -## Using the module - -IOC applications that use the module need to - -* add an entry to the Device Support module in their `RELEASE.local` file -* include `opcua.dbd` when building the IOC's DBD file -* include `opcua` in the support libraries for the IOC binary. - -## Documentation - -Sparse, but getting better. - -The documentation folder of the Device Support module contains the -[Requirements Specification (SRS)][requirements.pdf] giving an introduction -and the list of requirements that should convey a good idea of the planned -features. - -The [Cheat Sheet][cheatsheet.pdf] explains the configuration in the startup -script and the database links. - -## Binary Distribution - -Please look at the "Assets" sections of specific releases -on the [release page](https://github.com/epics-modules/opcua/releases) -for the binary distribution tars. -These tars contain an EPICS module -with a binary Linux shared library (`libopcua.so.`) -that contains (embeds) the Unified Automation low-level client. - -In your build setup, the module from the binary distribution -can be used like a support module built from source. -The binary device support is fully functional -and can be used without limitations or any fees. - -You need to download the binary distribution tar that matches -your Linux distribution and the exact EPICS Base version, -else you will not be able to create IOCs. - -## Feedback / Reporting issues - -Please use the GitHub project's -[issue tracker](https://github.com/epics-modules/opcua/issues). - -## Credits - -This module is based on extensive -[prototype work](https://github.com/bkuner/opcUaUnifiedAutomation) -by Bernhard Kuner (HZB/BESSY) and uses ideas and code snippets from -Michael Davidsaver (Osprey DCS). - -Support for the open62541 client library -and many fixes and new features were added by Dirk Zimoch (PSI) -with additional help from Carsten Winkler (HZB/BESSY). - -The end-to-end test suite is a reduced clone of the test application -that has been developed at the ESS by Ross Elliot and Karl Vestin. +- [**Documentation**](https://epics-modules.github.io/opcua) - + the things you should know +- [Source Code](https://github.com/epics-modules/opcua) - + everything you never wanted to know +- [OPC UA Room on EPICS Chat](https://matrix.to/#/#opcua:epics-controls.org) - + new quick help and discussion thing +- [Usage / FAQ (Wiki)](https://github.com/epics-modules/opcua/wiki) - + specific things to watch out for, usually server-related +- [Issue Tracker](https://github.com/epics-modules/opcua/issues) - + when things don't work ## License -This module is distributed subject to a Software License Agreement found -in the file `LICENSE` that is included with this distribution. +This module is distributed subject to a Software License Agreement +found in the file `LICENSE` +that is included with this distribution and shown above. [badge.version]: https://img.shields.io/github/v/release/epics-modules/opcua?sort=semver @@ -166,11 +41,3 @@ in the file `LICENSE` that is included with this distribution. [badge.uasdk-linux]: https://github.com/epics-modules/opcua/actions/workflows/ci-build-uasdk-linux.yml/badge.svg [badge.open62541-win]: https://github.com/epics-modules/opcua/actions/workflows/ci-build-open62541-win.yml/badge.svg [badge.open62541-linux]: https://github.com/epics-modules/opcua/actions/workflows/ci-build-open62541-linux.yml/badge.svg - -[unified.sdk]: https://www.unified-automation.com/products/client-sdk/c-ua-client-sdk.html -[open62541.sdk]: https://open62541.org/ - -[uasdk.dir]: https://github.com/epics-modules/opcua/tree/master/devOpcuaSup/UaSdk -[open62541.dir]: https://github.com/epics-modules/opcua/tree/master/devOpcuaSup/open62541 -[requirements.pdf]: https://docs.google.com/viewer?url=https://raw.githubusercontent.com/epics-modules/opcua/master/documentation/EPICS%20Support%20for%20OPC%20UA%20-%20SRS.pdf -[cheatsheet.pdf]: https://docs.google.com/viewer?url=https://raw.githubusercontent.com/epics-modules/opcua/master/documentation/EPICS%20Support%20for%20OPC%20UA%20-%20Cheat%20Sheet.pdf diff --git a/Using-Security.md b/Using-Security.md deleted file mode 100644 index 21287816..00000000 --- a/Using-Security.md +++ /dev/null @@ -1,201 +0,0 @@ -# Using OPC UA Security - -## Concepts - -The security features of OPC UA are based on the widely used X.509 certificates and the concepts of *Public Key Infrastructure* (PKI). - -Certificates are filed in a *Certificate Store*, containing trusted and own certificates as well as certificates from certificate authorities (CAs). On Linux, a file based store is used, defined by four specific locations for certificates and revocation lists. - -In the basic, explicit form, trusting a certificate usually means that the certificate (file) is moved into a specific folder in the certificate store. - -When using certificate authorities (CAs), a certificate is trusted if it is signed by a trusted CA. This approach is preferable for a larger installation, as it does not require updating all servers and clients when new peers with new certificates are added. - -A good analogy to certificates are passports: In a world with only a handful people, fabricating your passport yourself is reasonable. In reality, with millions of passport holders, it helps a lot that passports are fabricated by government agencies and this fact can be verified by checking holograms, watermarks etc. - -### Self-signed Certificates - -Servers often come with preloaded self-signed server certificates and a setting that will generously trust any valid self-signed client certificate. Generic clients typically offer an easy way to create a self-signed client certificate. - -These settings are intended to get users started with security easily and create minimal frustration during development. These "simple" self-signed certificates are good enough for securing the communication, but they are not very practical for authentication: every self-signed certificate must be trusted explicitly - or be accepted by default. - -### Communication Security - -Each OPC UA application (server and client) needs an *Application Instance Certificate* and a related public/private key pair to identify itself to its communication peers. The public key is contained in the certificate; the private key is secret and used for signing and encryption of messages. - -For a secure communication to be established, both peers must trust the certificate of the other. - -OPC UA defines three *Message Security Modes*: - -| Message Security Mode | | -| --------------------- | ------------------------------------------- | -| None | No security is applied. | -| Sign | All messages are signed, but not encrypted. | -| SignAndEncrypt | All messages are signed and encrypted. | - -As security algorithms are getting obsolete, an increasing number of *Security Policies* (sets of security algorithms and key lengths) are defined, by their unique URIs: - -| Security Policy | URI | -| -------------------------- | ------------------------------------------------------------ | -| None | http://opcfoundation.org/UA/SecurityPolicy#None | -| Basic128Rsa15 *(obsolete)* | http://opcfoundation.org/UA/SecurityPolicy#Basic128Rsa15 | -| Basic256 | http://opcfoundation.org/UA/SecurityPolicy#Basic256 | -| Basic256Sha256 | http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256 | -| Aes128_Sha256_RsaOaep | http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep | -| Aes256_Sha256_RsaPss | http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss | - -### Client Authentication (Identity) - -For client authentication related to managing the authorization of access to a server, OPC UA defines four *User Token Types* (methods of user authentication): - -| User Token Type | | -| ------------------------ | ------------------------------------------ | -| Anonymous Identity Token | No user information available. | -| Username Identity Token | User identified by name and password. | -| X.509 Identity Token | User identified by an X.509v3 certificate. | -| Issued Identity Token | User identified by a WS-SecurityToken. | - -### Endpoint Discovery - -In order to establish a secure connection, the client has to find out which security features the server provides. This process is called *Discovery*. - -In its answer to a discovery request, a server provides a list of *Endpoints* for the client to connect to, with different sets of security features. - -As a debugging tool, the iocShell command `opcuaShowSecurity` runs the discovery service and displays the available endpoints with their features and their *security level* (used by the IOC to find the endpoint providing the best security). - -## IOC Configuration - -An IOC using the OPC UA Device Support is considered to be a production system. As such, it has to be fully configured and supplied with the certificates needed to connect to the configured OPC UA servers. Other than general purpose and example clients, the IOC cannot and should not interactively create self-signed certificates or change the trust for a certificate that a server presents. - -To support setting up the certificate store, the `opcuaSaveRejected` iocShell command will configure the IOC to save rejected (untrusted) server certificates in a specified location (default: `/tmp/@`), using DER format. Copying such a certificate file to the IOC's certificate store (under `trusted/certs`) will explicitly trust the certificate and allow secure connections to the server. - -When using the UA SDK low level client, the trusted server and CA certificates (under `trusted/certs`) must use DER format. ([See below](#managing-certificates) for how to convert between formats.) - -Alternatively, you can use a general purpose client (e.g., the `UaExpert` tool) to connect to the server, trust its certificate, and use the certificate file from that client's certificate store. (You could also import it into your certificate management tool, [see below]((#managing-certificates)).) - -*Important Note:* During the process of endpoint discovery and saving the untrusted server certificate, a man-in-the-middle could send its own certificate to the IOC. Only add a server certificate to the trusted certificate store if you are sure it actually originates from the server (e.g., on a trusted network, after verifying its thumb print). This act of trusting is the crucial and weakest point when using self-signed certificates. - -### Certificate Store Setup - -The iocShell command `opcuaSetupPKI` sets the location(s) for the certificate store. The IOC needs to have read access to all files in the PKI store, further access should not be granted as this might compromise security. - -In its simple form, the single argument defines the location of the PKI certificate store, using default subdirectories for trusted peer and CA certificates (`trusted/certs`), CA certificate revocation lists (`trusted/crl`), intermediate issuer certificates (`issuers/certs`) and their certificate revocation lists (`issuers/crl`). - -In the fully detailed form (using four arguments), the four locations are specified separately. - -### Client Certificate - -The iocShell command `opcuaClientCertificate` sets the locations for the client certificate (PEM or DER format) and the matching private key (PEM format). - -### Session Security Setting - -Two security-related session options are used to configure the security features for a given OPC UA session, by calling `opcuaSetOption` in the iocShell. - -Setting `sec-mode` selects the specified message security mode for the connection. The special keyword "best" (default) will have the IOC choose the best mode, based on the server-supplied *security level*. Setting `sec-policy` (to the short name, not the full URI) selects a specific policy. If multiple endpoints match the option settings, the IOC will always choose the best available security. - -*Note:* To connect without security, you have to explicitly set `sec-mode` to `None`. - -If no matching endpoint is discovered or the server certificate is untrusted, the IOC will not connect. - -### Identity (Client Authentication) - -Without configuration, an Anonymous Identity Token will be used. - -To use a Username Identity or a Certificate Identity Token, prepare an identity file. This file should only be readable by the IOC. Configure the filename through the session option `sec-id`. - -In the identity file, lines starting with `#` are considered comments and ignored. - -To use a Username Identity Token, set `user=` and `pass=`, each on a separate line. - -To use a Certificate Identity Token, set `cert=` and `key=`, each on a separate line, to identify the DER or PEM certificate and PEM private key to use. If the private key is password protected, add another line setting `pass=`. Normally, the server will use the Common Name (CN) property of the certificate to determine the user name for authorization. - -## Managing Certificates - -The underlying OpenSSL library provides a command line utility. - -For creating and managing a larger number of certificates, -[Xca](https://hohnstaedt.de/xca/) is a powerful and popular GUI -for X.509 certificate and key handling, -including certificate authority (CA) functionality, -which is the most efficient way -to manage the certificates for a larger installation. - -The `openssl` command line utility can be used to convert certificates (and keys) between formats. To convert certificate `` from DER to PEM format, use: - -```bash -openssl x509 -inform der -in .der -out .pem -``` - -To convert in the other direction, use: - -```bash -openssl x509 -in .pem -outform der -out .der -``` - -The UA SDK client and the underlying OpenSSL library are picky in terms of file name extensions. DER format requires certificates to carry a `.der` extension and revocation lists to be named `.crl`, while PEM format works only if files have a `.pem` extension. Otherwise you may experience unspecific `Bad` status errors. - -### Creating Application Instance Certificates - -Creating a self-signed certificate for OPC UA use is pretty straight-forward. Follow the documented procedure, giving your certificate/key pair the following properties: - -- Choose the issuer information to match your situation. - -- Sign using `SHA 256` (i.e., `sha256WithRSAEncryption`). - -- X509v3 Basic Constraints: **critical**, `CA:FALSE`. - -- X509v3 Key Usage: **critical**, `Digital Signature`, `Non Repudiation`, `Key Encipherment`, `Data Encipherment`, `Certificate Sign`. - -- X509v3 Extended Key Usage: **critical**, `TLS Web Server Authentication`, `TLS Web Client Authentication`. - -- X509v3 Subject Alternative Name: `URI:urn:@:EPICS:IOC`, `DNS:` - with `` being the IOC name,`` being the hostname (i.e., the result of a `gethostname()` call) of the machine that runs the IOC. The URI tag *must* match what the Device Support module sets as its application URI. - Alternatively, a numerical `IP Address` tag can be used instead of `DNS`. - -The source tree contains an Xca certificate template that can be imported and modified to fit your needs, which will simplify generating new IOC client certificates. - -For the IOC, save the certificate in DER or PEM format, the private key as PEM. The server may need different formats - refer to the documentation of your server for more details. - -A simple client certificate/key pair can also be created using the `openssl` command line utility, e.g.: - -```bash -openssl req -x509 -newkey rsa:2048 -keyout private_key.pem -out cert.pem -sha256 -days 365 -nodes -addext "subjectAltName=URI:urn:@:EPICS:IOC,IP:" -``` - -### Certificates and Network / DNS Setup - -The `URI:`, `DNS:` and `IP:` entries in the Subject Alternative Name section require the network and DNS to be set up correctly, otherwise the certificates will not work. - -This applies to both the IOC and the server. If any of the two uses a host name in its certificate that doesn't match the host name that the other side gets when doing a reverse lookup, the connection is likely to fail. - -The IOC uses the `gethostname()` result in the `URI:` entry, which might differ from its DNS host name that has to appear in the `DNS:` entry. Depending on your DNS setup, the host names in the `DNS:` entry need to be simple or fully qualified. Finding out the right way to set up your certificates may be frustrating and time consuming. - -### Creating Identity Token Certificates - -Create a self-signed certificate, following the documented procedure and giving your certificate/key pair the following properties: - -- Choose the issuer information to match your situation. - The Common Name (CN) property of the certificate will normally be used by the server to determine the user name for authorization. - -- Sign using `SHA 256` (i.e., `sha256WithRSAEncryption`). - -- X509v3 Basic Constraints: **critical**, `CA:FALSE`. - -*Note:* The Unified Automation example server accepts any trusted certificate, without further verification and without using certificate properties for authorization. - -### Using a Certificate Authority - -A complete discussion of this concept and the design and implementation of an appropriate public key infrastructure for a specific installation is outside the scope of this README. The [Xca online documentation](https://hohnstaedt.de/xca/index.php/documentation/stepbystep) has more details and step-by-step guides. - -The basic steps are: - -- Create a Certificate Authority. - In the simplest form this means creating a Root CA certificate/key that is configured to sign other certificates. - The CA certificate needs to be placed in DER format inside the `trusted/certs` directory of the IOC's PKI store. - -- Create a Certificate Revocation List. - Even if you do not plan to use the Certificate Revocation List (CRL) mechanism to deal with invalidation of certificates, you need to create an empty CRL for your CA certificate and place it in DER format inside the `trusted/crl` directory of the IOC's PKI store. Otherwise your CA certificate will not work. - -- Create Application Instance and Identity Token Certificates signed by your CA. - When creating new certificates for your servers and clients, instead of selecting "Create a self-signed certificate" under the "Source" tab, choose a different certificate (your CA certificate) that will be used to sign the newly created certificate. - -Using Xca templates can simplify certificate creation and greatly improve consistency. diff --git a/configure/CONFIG_OPCUA_VERSION b/configure/CONFIG_OPCUA_VERSION index 3fb8cb21..c488f981 100644 --- a/configure/CONFIG_OPCUA_VERSION +++ b/configure/CONFIG_OPCUA_VERSION @@ -1,5 +1,5 @@ # Version between releases: next number plus flag EPICS_OPCUA_MAJOR_VERSION = 0 -EPICS_OPCUA_MINOR_VERSION = 11 +EPICS_OPCUA_MINOR_VERSION = 12 EPICS_OPCUA_MAINTENANCE_VERSION = 0 -EPICS_OPCUA_DEVELOPMENT_FLAG = 0 +EPICS_OPCUA_DEVELOPMENT_FLAG = 1 diff --git a/devOpcuaSup/DataElement.h b/devOpcuaSup/DataElement.h index e09055db..9018fdd7 100644 --- a/devOpcuaSup/DataElement.h +++ b/devOpcuaSup/DataElement.h @@ -13,9 +13,7 @@ #ifndef DEVOPCUA_DATAELEMENT_H #define DEVOPCUA_DATAELEMENT_H -#include -#include -#include +#include #include #include @@ -24,6 +22,8 @@ namespace DevOpcua { +typedef std::map EnumChoices; + class RecordConnector; /** @@ -60,13 +60,6 @@ class DataElement RecordConnector *pconnector, const std::list &elementPath); - /** - * @brief Get the type of element (inside a structure). - * - * @return true if element is a leaf (has no child elements) - */ - bool isLeaf() const { return isleaf; } - /** * @brief Setter to create a (bidirectional) link to a RecordConnector. * @@ -201,18 +194,20 @@ class DataElement * the OPC UA status of the item related to this DataElement. * * @param[out] value target string buffer - * @param[in] num length of target string buffer (including null byte) + * @param[in] len length of target string buffer (including null byte) * @param[in] prec pointer to EPICS record * @param[out] nextReason set to ProcessReason for the next update, `none` if last element + * @param[out] lenRead actual number of chars read (may be NULL) * @param[out] statusCode set to the OPC UA status code of the update * @param[out] statusText set to the OPC UA status text of the update (will be null terminated) * @param[in] statusTextLen length of the statusText buffer * * @return status 0 = ok, 1 = error */ - virtual long readScalar(char *value, const size_t num, + virtual long readScalar(char *value, const epicsUInt32 len, dbCommon *prec, ProcessReason *nextReason = nullptr, + epicsUInt32* lenRead = nullptr, epicsUInt32 *statusCode = nullptr, char *statusText = nullptr, const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) = 0; @@ -591,13 +586,13 @@ class DataElement * @brief Write outgoing classic C string (char[]) data. * * @param value pointer to null terminated source string - * @param num source string size + * @param len source string length * @param prec pointer to EPICS record * * @return status 0 = ok, 1 = error */ virtual long writeScalar(const char *value, - const epicsUInt32 num, + const epicsUInt32 len, dbCommon *prec) = 0; /** @@ -794,34 +789,10 @@ class DataElement */ virtual void requestRecordProcessing(const ProcessReason reason) const = 0; - const std::string name; /**< element name */ + const EnumChoices* enumChoices = nullptr; /**< enum definition if this element is an enum */ protected: - /** - * @brief Constructor for node DataElement, to be used by implementations. - * - * @param name structure element name (empty otherwise) - */ - DataElement(const std::string &name = "") - : name(name) - , pconnector(nullptr) - , isleaf(false) - {} - - /** - * @brief Constructor for leaf DataElement, to be used by implementations. - * - * @param name structure element name (empty otherwise) - * @param connector - */ - DataElement(RecordConnector *pconnector, const std::string &name = "") - : name(name) - , pconnector(pconnector) - , isleaf(true) - {} - - RecordConnector *pconnector; /**< pointer to connector (if leaf) */ - const bool isleaf; /**< flag for leaf property */ + RecordConnector *pconnector = nullptr; }; } // namespace DevOpcua diff --git a/devOpcuaSup/ElementTree.h b/devOpcuaSup/ElementTree.h index a80a7ffd..def4a0b8 100644 --- a/devOpcuaSup/ElementTree.h +++ b/devOpcuaSup/ElementTree.h @@ -1,5 +1,5 @@ /*************************************************************************\ -* Copyright (c) 2020-2021 ITER Organization. +* Copyright (c) 2020-2025 ITER Organization. * This module is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. \*************************************************************************/ @@ -41,35 +41,28 @@ namespace DevOpcua { * The template types used (all specific to the implementation) and their * constraints: * - * E is the Element class - * with a constructor E::E(const std::string &name, I *item) - * and methods void E::setParent(std::shared_ptr parent); - * void E::addChild(std::weak_ptr child); - * std::shared_ptr findChild(const std::string &name) - * bool E::isLeaf(); - * and a public const std::string E::name; + * NE is the Node Element class + * with a constructor NE::NE(const std::string &name, I *item) + * and methods void NE::setParent(std::shared_ptr parent); + * void NE::addChild(std::weak_ptr child); + * std::shared_ptr findChild(const std::string &name) * - * I is the Item class. + * E is the common base class of Node Element (NE) and Leaf Element + * with the method void E::setParent(std::shared_ptr parent); + * + * I is the Item class */ -template +template class ElementTree { public: - ElementTree(I *i) - : item(i) - {} - /** * @brief Return root element * * @return weak_ptr to root element */ - std::weak_ptr - root() const - { - return rootElement; - } + std::weak_ptr root() const { return rootElement; } /* Allow testing as 'if (tree) ...' */ explicit operator bool() const { return !rootElement.expired(); } @@ -82,8 +75,7 @@ class ElementTree * * @return shared_ptr to the nearest existing node in the tree, shared_ptr<>() if no overlap */ - std::shared_ptr - nearestNode(std::list &path) + std::shared_ptr nearestNode(std::list &path) { bool found; @@ -97,7 +89,7 @@ class ElementTree do { auto part = path.begin(); found = false; - if (elem) { + if (elem && !elem->isLeaf()) { auto nextelem = elem->findChild(*part); if (nextelem) { found = true; @@ -107,10 +99,7 @@ class ElementTree } } while (found && !path.empty()); - if (elem) - return elem; - else - return std::shared_ptr(); + return elem; } /** @@ -120,17 +109,17 @@ class ElementTree * @param path full path of the leaf * * @throws runtime_error when trying to add elements to a leaf node + * when adding with empty path but [ROOT] exists */ - void - addLeaf(std::shared_ptr leaf, const std::list &fullpath) + void addLeaf (std::shared_ptr leaf, const std::list &fullpath, I *item) { std::shared_ptr elem(leaf); std::list path(fullpath); auto branch = nearestNode(path); + if (branch && branch->isLeaf()) - throw std::runtime_error(SB() - << "can't add leaf to existing leaf " << branch->name); + throw std::runtime_error(SB() << "can't add leaf to existing leaf " << branch->name); if (path.empty()) { if (rootElement.lock()) throw std::runtime_error(SB() << "root node does already exist"); @@ -138,7 +127,7 @@ class ElementTree } else { path.pop_back(); // remove the leaf name for (auto rit = path.crbegin(); rit != path.crend(); ++rit) { - auto node = std::make_shared(*rit, item); + auto node = std::make_shared(*rit, item); node->addChild(elem); elem->setParent(node); elem = std::move(node); @@ -147,7 +136,7 @@ class ElementTree branch->addChild(elem); elem->setParent(branch); } else { - auto node = std::make_shared("[ROOT]", item); + auto node = std::make_shared("[ROOT]", item); node->addChild(elem); elem->setParent(node); rootElement = std::move(node); @@ -155,9 +144,11 @@ class ElementTree } } +public: + void setRoot(std::shared_ptr root) { rootElement = root; } + private: std::weak_ptr rootElement; - I *item; }; } // namespace DevOpcua diff --git a/devOpcuaSup/Makefile b/devOpcuaSup/Makefile index d49e3fce..c1a02752 100644 --- a/devOpcuaSup/Makefile +++ b/devOpcuaSup/Makefile @@ -24,7 +24,7 @@ endif # Windows builds define NOMINMAX (macros min, max clash with numeric_limits<>) # and CALLBACK (clashes with EPICS callback.h) USR_CXXFLAGS_WIN32 += -DNOMINMAX -USR_CXXFLAGS += -DEPICS_NO_CALLBACK +USR_CXXFLAGS += -DEPICS_NO_CALLBACK -DNO_EPICS_CALLBACK OPCUA = $(TOP)/devOpcuaSup SRC_DIRS += $(OPCUA) diff --git a/devOpcuaSup/Registry.h b/devOpcuaSup/OpcuaRegistry.h similarity index 97% rename from devOpcuaSup/Registry.h rename to devOpcuaSup/OpcuaRegistry.h index eda87146..1e805c46 100644 --- a/devOpcuaSup/Registry.h +++ b/devOpcuaSup/OpcuaRegistry.h @@ -8,8 +8,8 @@ * Author: Ralph Lange */ -#ifndef DEVOPCUA_REGISTRY_H -#define DEVOPCUA_REGISTRY_H +#ifndef DEVOPCUA_OPCUAREGISTRY_H +#define DEVOPCUA_OPCUAREGISTRY_H #include #include @@ -158,4 +158,4 @@ class Registry } // namespace DevOpcua -#endif // DEVOPCUA_REGISTRY_H +#endif // DEVOPCUA_OPCUAREGISTRY_H diff --git a/devOpcuaSup/RecordConnector.cpp b/devOpcuaSup/RecordConnector.cpp index e83a5ce2..bb06415f 100644 --- a/devOpcuaSup/RecordConnector.cpp +++ b/devOpcuaSup/RecordConnector.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #define epicsExportSharedSymbols #include "RecordConnector.h" @@ -76,6 +77,16 @@ long reProcess (dbCommon *prec) return status; } +#if EPICS_VERSION_INT >= VERSION_INT(3,16,0,1) +#define SAVE_FLNK(prec) struct lset *lset = prec->flnk.lset +#define DISABLE_FLNK(prec) prec->flnk.lset = NULL +#define RESTORE_FLNK(prec) prec->flnk.lset = lset +#else +#define SAVE_FLNK(prec) short type = prec->flnk.type +#define DISABLE_FLNK(prec) prec->flnk.type = 0 +#define RESTORE_FLNK(prec) prec->flnk.type = type +#endif + void processCallback (epicsCallback *pcallback, const ProcessReason reason) { void *pUsr; @@ -89,10 +100,17 @@ void processCallback (epicsCallback *pcallback, const ProcessReason reason) dbScanLock(prec); ProcessReason oldreason = pvt->reason; pvt->reason = reason; + + // Do not process FLNK on updates if not "I/O Intr" + SAVE_FLNK(prec); + if (reason == incomingData && prec->scan != menuScanI_O_Intr) + DISABLE_FLNK(prec); if (prec->pact) reProcess(prec); else dbProcess(prec); + RESTORE_FLNK(prec); + pvt->reason = oldreason; dbScanUnlock(prec); } diff --git a/devOpcuaSup/RecordConnector.h b/devOpcuaSup/RecordConnector.h index 8c822086..c195b87e 100644 --- a/devOpcuaSup/RecordConnector.h +++ b/devOpcuaSup/RecordConnector.h @@ -45,9 +45,9 @@ class RecordConnector return pdataelement->readScalar(val, prec, nextReason); } - long int readScalar(char *val, const epicsUInt32 num, ProcessReason *nextReason = nullptr) + long int readScalar(char *val, const epicsUInt32 len, ProcessReason *nextReason = nullptr, epicsUInt32 *lenRead = nullptr) { - return pdataelement->readScalar(val, num, prec, nextReason); + return pdataelement->readScalar(val, len, prec, nextReason, lenRead); } template @@ -56,9 +56,9 @@ class RecordConnector return pdataelement->writeScalar(val, prec); } - long int writeScalar(const char* val, const epicsUInt32 num) + long int writeScalar(const char* val, const epicsUInt32 len) { - return pdataelement->writeScalar(val, num, prec); + return pdataelement->writeScalar(val, len, prec); } template diff --git a/devOpcuaSup/Session.h b/devOpcuaSup/Session.h index d1b2283c..4acffe83 100644 --- a/devOpcuaSup/Session.h +++ b/devOpcuaSup/Session.h @@ -355,6 +355,8 @@ class epicsShareClass Session bool autoConnect; /**< auto (re)connect flag */ std::string securityIdentityFile; /**< full path to file with Identity token settings */ std::string securityUserName; /**< user name set in Username token */ + + const int no_of_properties_read = 2; /* always read type and value */ }; diff --git a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp deleted file mode 100644 index e7cbce2f..00000000 --- a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp +++ /dev/null @@ -1,1217 +0,0 @@ -/*************************************************************************\ -* Copyright (c) 2018-2020 ITER Organization. -* This module is distributed subject to a Software License Agreement found -* in file LICENSE that is included with this distribution. -\*************************************************************************/ - -/* - * Author: Ralph Lange - * - * based on prototype work by Bernhard Kuner - * and example code from the Unified Automation C++ Based OPC UA Client SDK - */ - -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "ItemUaSdk.h" -#include "DataElementUaSdk.h" -#include "UpdateQueue.h" -#include "RecordConnector.h" - -namespace DevOpcua { - -/* Specific implementation of DataElement's "factory" method */ -void -DataElement::addElementToTree(Item *item, - RecordConnector *pconnector, - const std::list &elementPath) -{ - DataElementUaSdk::addElementToTree(static_cast(item), pconnector, elementPath); -} - -DataElementUaSdk::DataElementUaSdk (const std::string &name, - ItemUaSdk *item, - RecordConnector *pconnector) - : DataElement(pconnector, name) - , pitem(item) - , timesrc(-1) - , mapped(false) - , incomingQueue(pconnector->plinkinfo->clientQueueSize, pconnector->plinkinfo->discardOldest) - , outgoingLock(pitem->dataTreeWriteLock) - , isdirty(false) -{} - -DataElementUaSdk::DataElementUaSdk (const std::string &name, - ItemUaSdk *item) - : DataElement(name) - , pitem(item) - , timesrc(-1) - , mapped(false) - , incomingQueue(0ul) - , outgoingLock(pitem->dataTreeWriteLock) - , isdirty(false) -{} - -void -DataElementUaSdk::addElementToTree(ItemUaSdk *item, - RecordConnector *pconnector, - const std::list &elementPath) -{ - std::string name("[ROOT]"); - if (elementPath.size()) - name = elementPath.back(); - - auto leaf = std::make_shared(name, item, pconnector); - item->dataTree.addLeaf(leaf, elementPath); - // reference from connector after adding to the tree worked - pconnector->setDataElement(leaf); -} - -void -DataElementUaSdk::show (const int level, const unsigned int indent) const -{ - std::string ind(indent*2, ' '); - std::cout << ind; - if (isLeaf()) { - std::cout << "leaf=" << name << " record(" << pconnector->getRecordType() << ")=" - << pconnector->getRecordName() - << " type=" << variantTypeString(incomingData.type()) - << " timestamp=" << linkOptionTimestampString(pconnector->plinkinfo->timestamp); - if (pconnector->plinkinfo->timestamp == LinkOptionTimestamp::data) - std::cout << "@" << pitem->linkinfo.timestampElement; - std::cout << " bini=" << linkOptionBiniString(pconnector->plinkinfo->bini) - << " monitor=" << (pconnector->plinkinfo->monitor ? "y" : "n") << "\n"; - } else { - std::cout << "node=" << name << " children=" << elements.size() - << " mapped=" << (mapped ? "y" : "n") << "\n"; - for (auto it : elements) { - if (auto pelem = it.lock()) { - pelem->show(level, indent + 1); - } - } - } -} - -// Getting the timestamp and status information from the Item assumes that only one thread -// is pushing data into the Item's DataElement structure at any time. -void -DataElementUaSdk::setIncomingData(const UaVariant &value, - ProcessReason reason, - const std::string *timefrom) -{ - // Make a copy of this element and cache it - incomingData = value; - - if (isLeaf()) { - if ((pconnector->state() == ConnectionStatus::initialRead - && (reason == ProcessReason::readComplete || reason == ProcessReason::readFailure)) - || (pconnector->state() == ConnectionStatus::up)) { - Guard(pconnector->lock); - bool wasFirst = false; - // Make a copy of the value for this element and put it on the queue - UpdateUaSdk *u(new UpdateUaSdk(getIncomingTimeStamp(), reason, value, getIncomingReadStatus())); - incomingQueue.pushUpdate(std::shared_ptr(u), &wasFirst); - if (debug() >= 5) - std::cout << "Element " << name << " set data (" - << processReasonString(reason) - << ") for record " << pconnector->getRecordName() - << " (queue use " << incomingQueue.size() - << "/" << incomingQueue.capacity() << ")" << std::endl; - if (wasFirst) - pconnector->requestRecordProcessing(reason); - } - } else { - if (debug() >= 5) - std::cout << "Element " << name << " splitting structured data to " - << elements.size() << " child elements" << std::endl; - - if (value.type() == OpcUaType_ExtensionObject) { - UaExtensionObject extensionObject; - value.toExtensionObject(extensionObject); - - // Try to get the structure definition from the dictionary - UaStructureDefinition definition = pitem->structureDefinition(extensionObject.encodingTypeId()); - if (!definition.isNull()) { - if (!definition.isUnion()) { - // ExtensionObject is a structure - // Decode the ExtensionObject to a UaGenericValue to provide access to the structure fields - if (extensionObject.encoding() == UaExtensionObject::EncodeableObject) - extensionObject.changeEncoding(UaExtensionObject::Binary); - UaGenericStructureValue genericValue; - genericValue.setGenericValue(extensionObject, definition); - - if (!mapped) { - if (debug() >= 5) - std::cout << " ** creating index-to-element map for child elements" << std::endl; - if (timefrom) { - for (int i = 0; i < definition.childrenCount(); i++) { - if (*timefrom == definition.child(i).name().toUtf8()) { - timesrc = i; - pitem->tsData = epicsTimeFromUaVariant(genericValue.value(i)); - } - } - OpcUa_BuiltInType t = genericValue.value(timesrc).type(); - if (timesrc == -1) { - errlogPrintf( - "%s: timestamp element %s not found - using source timestamp\n", - pitem->recConnector->getRecordName(), - timefrom->c_str()); - } else if (t != OpcUaType_DateTime) { - errlogPrintf("%s: timestamp element %s has invalid type %s - using " - "source timestamp\n", - pitem->recConnector->getRecordName(), - timefrom->c_str(), - variantTypeString(t)); - timesrc = -1; - } - } - for (auto &it : elements) { - auto pelem = it.lock(); - for (int i = 0; i < definition.childrenCount(); i++) { - if (pelem->name == definition.child(i).name().toUtf8()) { - elementMap.insert({i, it}); - pelem->setIncomingData(genericValue.value(i), reason); - } - } - } - if (debug() >= 5) - std::cout << " ** " << elementMap.size() << "/" << elements.size() - << " child elements mapped to a " - << "structure of " << definition.childrenCount() << " elements" << std::endl; - mapped = true; - } else { - if (timefrom) { - if (timesrc >= 0) - pitem->tsData = epicsTimeFromUaVariant(genericValue.value(timesrc)); - else - pitem->tsData = pitem->tsSource; - } - for (auto &it : elementMap) { - auto pelem = it.second.lock(); - pelem->setIncomingData(genericValue.value(it.first), reason); - } - } - } - - } else - errlogPrintf( - "Cannot get a structure definition for item %s element %s (dataTypeId %s " - "encodingTypeId %s) - check access to type dictionary\n", - pitem->nodeid->toString().toUtf8(), - name.c_str(), - extensionObject.dataTypeId().toString().toUtf8(), - extensionObject.encodingTypeId().toString().toUtf8()); - } - } -} - -void -DataElementUaSdk::setIncomingEvent (ProcessReason reason) -{ - if (isLeaf()) { - Guard(pconnector->lock); - bool wasFirst = false; - // Put the event on the queue - UpdateUaSdk *u(new UpdateUaSdk(getIncomingTimeStamp(), reason)); - incomingQueue.pushUpdate(std::shared_ptr(u), &wasFirst); - if (debug() >= 5) - std::cout << "Element " << name << " set event (" - << processReasonString(reason) - << ") for record " << pconnector->getRecordName() - << " (queue use " << incomingQueue.size() - << "/" << incomingQueue.capacity() << ")" << std::endl; - if (wasFirst) - pconnector->requestRecordProcessing(reason); - } else { - for (auto &it : elements) { - auto pelem = it.lock(); - pelem->setIncomingEvent(reason); - } - if (reason == ProcessReason::connectionLoss) { - elementMap.clear(); - timesrc = -1; - mapped = false; - } - } -} - -void -DataElementUaSdk::setState(const ConnectionStatus state) -{ - if (isLeaf()) { - Guard(pconnector->lock); - pconnector->setState(state); - } else { - for (auto &it : elements) { - auto pelem = it.lock(); - pelem->setState(state); - } - } -} - -// Helper to update one data structure element from pointer to child -bool -DataElementUaSdk::updateDataInGenericValue (UaGenericStructureValue &value, - const int index, - std::shared_ptr pelem) -{ - bool updated = false; - { // Scope of Guard G - Guard G(pelem->outgoingLock); - if (pelem->isDirty()) { - value.setField(index, pelem->getOutgoingData()); - pelem->isdirty = false; - updated = true; - } - } - if (debug() >= 4) { - if (updated) { - std::cout << "Data from child element " << pelem->name - << " inserted into data structure" << std::endl; - } else { - std::cout << "Data from child element " << pelem->name - << " ignored (not dirty)" << std::endl; - } - } - return updated; -} - -const UaVariant & -DataElementUaSdk::getOutgoingData () -{ - if (!isLeaf()) { - if (debug() >= 4) - std::cout << "Element " << name << " updating structured data from " - << elements.size() << " child elements" << std::endl; - - outgoingData = incomingData; - isdirty = false; - if (outgoingData.type() == OpcUaType_ExtensionObject) { - UaExtensionObject extensionObject; - outgoingData.toExtensionObject(extensionObject); - if (extensionObject.encoding() == UaExtensionObject::EncodeableObject) - extensionObject.changeEncoding(UaExtensionObject::Binary); - - // Try to get the structure definition from the dictionary - UaStructureDefinition definition = pitem->structureDefinition(extensionObject.encodingTypeId()); - if (!definition.isNull()) { - if (!definition.isUnion()) { - // ExtensionObject is a structure - // Decode the ExtensionObject to a UaGenericValue to provide access to the structure fields - UaGenericStructureValue genericValue; - genericValue.setGenericValue(extensionObject, definition); - - if (!mapped) { - if (debug() >= 5) - std::cout << " ** creating index-to-element map for child elements" << std::endl; - for (auto &it : elements) { - auto pelem = it.lock(); - for (int i = 0; i < definition.childrenCount(); i++) { - if (pelem->name == definition.child(i).name().toUtf8()) { - elementMap.insert({i, it}); - if (updateDataInGenericValue(genericValue, i, pelem)) - isdirty = true; - } - } - } - if (debug() >= 5) - std::cout << " ** " << elementMap.size() << "/" << elements.size() - << " child elements mapped to a " - << "structure of " << definition.childrenCount() << " elements" << std::endl; - mapped = true; - } else { - for (auto &it : elementMap) { - auto pelem = it.second.lock(); - if (updateDataInGenericValue(genericValue, it.first, pelem)) - isdirty = true; - } - } - if (isdirty) { - if (debug() >= 4) - std::cout << "Encoding changed data structure to outgoingData of element " << name - << std::endl; - genericValue.toExtensionObject(extensionObject); - outgoingData.setExtensionObject(extensionObject, OpcUa_True); - } else { - if (debug() >= 4) - std::cout << "Returning unchanged outgoingData of element " << name - << std::endl; - } - } - - } else - errlogPrintf( - "Cannot get a structure definition for extensionObject with dataTypeID %s " - "/ encodingTypeID %s - check access to type dictionary\n", - extensionObject.dataTypeId().toString().toUtf8(), - extensionObject.encodingTypeId().toString().toUtf8()); - } - } - return outgoingData; -} - -void -DataElementUaSdk::dbgReadScalar (const UpdateUaSdk *upd, - const std::string &targetTypeName, - const size_t targetSize) const -{ - if (isLeaf() && debug()) { - char time_buf[40]; - upd->getTimeStamp().strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S.%09f"); - ProcessReason reason = upd->getType(); - - std::cout << pconnector->getRecordName() << ": "; - if (reason == ProcessReason::incomingData || reason == ProcessReason::readComplete) { - std::cout << "(" << linkOptionTimestampString(pconnector->plinkinfo->timestamp); - if (pconnector->plinkinfo->timestamp == LinkOptionTimestamp::data) - std::cout << "(@" << pconnector->plinkinfo->timestampElement << ")"; - std::cout << " time " << time_buf << ") read " << processReasonString(reason) << " (" - << UaStatus(upd->getStatus()).toString().toUtf8() << ") "; - UaVariant &data = upd->getData(); - if (data.type() == OpcUaType_String) - std::cout << "'" << data.toString().toUtf8() << "'"; - else - std::cout << data.toString().toUtf8(); - std::cout << " (" << variantTypeString(data.type()) << ")" - << " as " << targetTypeName; - if (targetSize) - std::cout << "[" << targetSize << "]"; - } else { - std::cout << "(client time "<< time_buf << ") " << processReasonString(reason); - } - std::cout << " --- remaining queue " << incomingQueue.size() - << "/" << incomingQueue.capacity() << std::endl; - } -} - -long -DataElementUaSdk::readScalar (epicsInt32 *value, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readScalar (epicsInt64 *value, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readScalar (epicsUInt32 *value, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readScalar (epicsFloat64 *value, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); -} - -// CString type needs specialization -long -DataElementUaSdk::readScalar (char *value, const size_t num, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - long ret = 0; - - if (incomingQueue.empty()) { - errlogPrintf("%s: incoming data queue empty\n", prec->name); - if (nextReason) - *nextReason = ProcessReason::none; - return 1; - } - - ProcessReason nReason; - std::shared_ptr upd = incomingQueue.popUpdate(&nReason); - dbgReadScalar(upd.get(), "CString", num); - - switch (upd->getType()) { - case ProcessReason::readFailure: - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::connectionLoss: - (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::incomingData: - case ProcessReason::readComplete: - { - if (num && value) { - OpcUa_StatusCode stat = upd->getStatus(); - if (OpcUa_IsNotGood(stat)) { - // No valid OPC UA value - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - // Valid OPC UA value, so copy over - if (OpcUa_IsUncertain(stat)) { - (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); - } - strncpy(value, upd->getData().toString().toUtf8(), num); - value[num-1] = '\0'; - prec->udf = false; - } - if (statusCode) *statusCode = stat; - if (statusText) { - strncpy(statusText, UaStatus(stat).toString().toUtf8(), statusTextLen); - statusText[statusTextLen-1] = '\0'; - } - } - break; - } - default: - break; - } - - prec->time = upd->getTimeStamp(); - if (nextReason) *nextReason = nReason; - return ret; -} - -void -DataElementUaSdk::dbgReadArray (const UpdateUaSdk *upd, - const epicsUInt32 targetSize, - const std::string &targetTypeName) const -{ - if (isLeaf() && debug()) { - char time_buf[40]; - upd->getTimeStamp().strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S.%09f"); - ProcessReason reason = upd->getType(); - - std::cout << pconnector->getRecordName() << ": "; - if (reason == ProcessReason::incomingData || reason == ProcessReason::readComplete) { - std::cout << "(" << linkOptionTimestampString(pconnector->plinkinfo->timestamp); - if (pconnector->plinkinfo->timestamp == LinkOptionTimestamp::data) - std::cout << "@" << pconnector->plinkinfo->timestampElement; - std::cout << " time " << time_buf << ") read " << processReasonString(reason) << " (" - << UaStatus(upd->getStatus()).toString().toUtf8() << ") "; - UaVariant &data = upd->getData(); - std::cout << " array of " << variantTypeString(data.type()) - << "[" << upd->getData().arraySize() << "]" - << " into " << targetTypeName << "[" << targetSize << "]"; - } else { - std::cout << "(client time "<< time_buf << ") " << processReasonString(reason); - } - std::cout << " --- remaining queue " << incomingQueue.size() - << "/" << incomingQueue.capacity() << std::endl; - } -} - -// Read array for EPICS String / OpcUa_String -long int -DataElementUaSdk::readArray (char *value, const epicsUInt32 len, - const epicsUInt32 num, - epicsUInt32 *numRead, - OpcUa_BuiltInType expectedType, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - long ret = 0; - epicsUInt32 elemsWritten = 0; - - if (incomingQueue.empty()) { - errlogPrintf("%s : incoming data queue empty\n", prec->name); - *numRead = 0; - return 1; - } - - ProcessReason nReason; - std::shared_ptr upd = incomingQueue.popUpdate(&nReason); - dbgReadArray(upd.get(), num, epicsTypeString(value)); - - switch (upd->getType()) { - case ProcessReason::readFailure: - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::connectionLoss: - (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::incomingData: - case ProcessReason::readComplete: - { - if (num && value) { - OpcUa_StatusCode stat = upd->getStatus(); - if (OpcUa_IsNotGood(stat)) { - // No valid OPC UA value - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - // Valid OPC UA value, so try to convert - UaVariant &data = upd->getData(); - if (!data.isArray()) { - errlogPrintf("%s : incoming data is not an array\n", prec->name); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else if (data.type() != expectedType) { - errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", - prec->name, variantTypeString(data.type()), epicsTypeString(value)); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - if (OpcUa_IsUncertain(stat)) { - (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); - } - UaStringArray arr; - UaVariant_to(upd->getData(), arr); - elemsWritten = num < arr.length() ? num : arr.length(); - for (epicsUInt32 i = 0; i < elemsWritten; i++) { - strncpy(value + i * len, UaString(arr[i]).toUtf8(), len); - (value + i * len)[len - 1] = '\0'; - } - prec->udf = false; - } - } - if (statusCode) *statusCode = stat; - if (statusText) { - strncpy(statusText, UaStatus(stat).toString().toUtf8(), statusTextLen); - statusText[statusTextLen-1] = '\0'; - } - } - break; - } - default: - break; - } - - prec->time = upd->getTimeStamp(); - if (nextReason) *nextReason = nReason; - if (num && value) - *numRead = elemsWritten; - return ret; -} - -// Specialization for epicsUInt8 / OpcUa_Byte -// (needed because UaByteArray API is different from all other UaXxxArray classes) -// CAVEAT: changes in the template (in DataElementUaSdk.h) must be reflected here -template<> -long -DataElementUaSdk::readArray (epicsUInt8 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - OpcUa_BuiltInType expectedType, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - long ret = 0; - epicsUInt32 elemsWritten = 0; - - if (incomingQueue.empty()) { - errlogPrintf("%s : incoming data queue empty\n", prec->name); - *numRead = 0; - return 1; - } - - ProcessReason nReason; - std::shared_ptr upd = incomingQueue.popUpdate(&nReason); - dbgReadArray(upd.get(), num, epicsTypeString(*value)); - - switch (upd->getType()) { - case ProcessReason::readFailure: - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::connectionLoss: - (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::incomingData: - case ProcessReason::readComplete: - { - if (num && value) { - OpcUa_StatusCode stat = upd->getStatus(); - if (OpcUa_IsNotGood(stat)) { - // No valid OPC UA value - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - // Valid OPC UA value, so try to convert - UaVariant &data = upd->getData(); - if (!data.isArray()) { - errlogPrintf("%s : incoming data is not an array\n", prec->name); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else if (data.type() != expectedType) { - errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", - prec->name, variantTypeString(data.type()), epicsTypeString(*value)); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - if (OpcUa_IsUncertain(stat)) { - (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); - } - UaByteArray arr; - UaVariant_to(upd->getData(), arr); - elemsWritten = static_cast(arr.size()); - if (num < elemsWritten) elemsWritten = num; - memcpy(value, arr.data(), sizeof(epicsUInt8) * elemsWritten); - prec->udf = false; - } - } - if (statusCode) *statusCode = stat; - if (statusText) { - strncpy(statusText, UaStatus(stat).toString().toUtf8(), statusTextLen); - statusText[statusTextLen-1] = '\0'; - } - } - break; - } - default: - break; - } - - prec->time = upd->getTimeStamp(); - if (nextReason) *nextReason = nReason; - if (num) - *numRead = elemsWritten; - return ret; -} - -long -DataElementUaSdk::readArray (epicsInt8 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, OpcUaType_SByte, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readArray (epicsUInt8 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, OpcUaType_Byte, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readArray (epicsInt16 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, OpcUaType_Int16, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readArray (epicsUInt16 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, OpcUaType_UInt16, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readArray (epicsInt32 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, OpcUaType_Int32, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readArray (epicsUInt32 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, OpcUaType_UInt32, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readArray (epicsInt64 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, OpcUaType_Int64, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readArray (epicsUInt64 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, OpcUaType_UInt64, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readArray (epicsFloat32 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, OpcUaType_Float, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readArray (epicsFloat64 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, OpcUaType_Double, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readArray (char *value, const epicsUInt32 len, - const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, len, num, numRead, OpcUaType_String, prec, nextReason, statusCode, statusText, statusTextLen); -} - -inline -void -DataElementUaSdk::dbgWriteScalar () const -{ - if (isLeaf() && debug()) { - std::cout << pconnector->getRecordName() << ": set outgoing data (" - << variantTypeString(outgoingData.type()) << ") to value "; - if (outgoingData.type() == OpcUaType_String) - std::cout << "'" << outgoingData.toString().toUtf8() << "'"; - else - std::cout << outgoingData.toString().toUtf8(); - std::cout << std::endl; - } -} - -long -DataElementUaSdk::writeScalar (const epicsInt32 &value, dbCommon *prec) -{ - return writeScalar(value, prec); -} - -long -DataElementUaSdk::writeScalar (const epicsUInt32 &value, dbCommon *prec) -{ - return writeScalar(value, prec); -} - -long -DataElementUaSdk::writeScalar (const epicsInt64 &value, dbCommon *prec) -{ - return writeScalar(value, prec); -} - -long -DataElementUaSdk::writeScalar (const epicsFloat64 &value, dbCommon *prec) -{ - return writeScalar(value, prec); -} - -long -DataElementUaSdk::writeScalar (const char *value, const epicsUInt32 len, dbCommon *prec) -{ - long ret = 0; - long l; - unsigned long ul; - double d; - - switch (incomingData.type()) { - case OpcUaType_String: - { // Scope of Guard G - Guard G(outgoingLock); - outgoingData.setString(static_cast(value)); - markAsDirty(); - break; - } - case OpcUaType_LocalizedText: - { // Scope of Guard G - Guard G(outgoingLock); - UaLocalizedText localizedText; - incomingData.toLocalizedText(localizedText); - localizedText.setText(static_cast(value)); // keep the Locale - outgoingData.setLocalizedText(localizedText); - markAsDirty(); - break; - } - case OpcUaType_Boolean: - { // Scope of Guard G - Guard G(outgoingLock); - if (strchr("YyTt1", *value)) - outgoingData.setBoolean(true); - else - outgoingData.setBoolean(false); - markAsDirty(); - break; - } - case OpcUaType_Byte: - ul = strtoul(value, nullptr, 0); - if (isWithinRange(ul)) { - Guard G(outgoingLock); - outgoingData.setByte(static_cast(ul)); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case OpcUaType_SByte: - l = strtol(value, nullptr, 0); - if (isWithinRange(l)) { - Guard G(outgoingLock); - outgoingData.setSByte(static_cast(l)); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case OpcUaType_UInt16: - ul = strtoul(value, nullptr, 0); - if (isWithinRange(ul)) { - Guard G(outgoingLock); - outgoingData.setUInt16(static_cast(ul)); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case OpcUaType_Int16: - l = strtol(value, nullptr, 0); - if (isWithinRange(l)) { - Guard G(outgoingLock); - outgoingData.setInt16(static_cast(l)); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case OpcUaType_UInt32: - ul = strtoul(value, nullptr, 0); - if (isWithinRange(ul)) { - Guard G(outgoingLock); - outgoingData.setUInt32(static_cast(ul)); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case OpcUaType_Int32: - l = strtol(value, nullptr, 0); - if (isWithinRange(l)) { - Guard G(outgoingLock); - outgoingData.setInt32(static_cast(l)); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case OpcUaType_UInt64: - ul = strtoul(value, nullptr, 0); - if (isWithinRange(ul)) { - Guard G(outgoingLock); - outgoingData.setUInt64(static_cast(ul)); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case OpcUaType_Int64: - l = strtol(value, nullptr, 0); - if (isWithinRange(l)) { - Guard G(outgoingLock); - outgoingData.setInt64(static_cast(l)); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case OpcUaType_Float: - d = strtod(value, nullptr); - if (isWithinRange(d)) { - Guard G(outgoingLock); - outgoingData.setFloat(static_cast(d)); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case OpcUaType_Double: - { - d = strtod(value, nullptr); - Guard G(outgoingLock); - outgoingData.setDouble(static_cast(d)); - markAsDirty(); - break; - } - default: - errlogPrintf("%s : unsupported conversion for outgoing data\n", - prec->name); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - } - - dbgWriteScalar(); - return ret; -} - -inline -void -DataElementUaSdk::dbgWriteArray (const epicsUInt32 targetSize, const std::string &targetTypeName) const -{ - if (isLeaf() && debug()) { - std::cout << pconnector->getRecordName() << ": writing array of " - << targetTypeName << "[" << targetSize << "] as " - << variantTypeString(outgoingData.type()) << "["<< outgoingData.arraySize() << "]" - << std::endl; - } -} - -// Write array for EPICS String / OpcUa_String -long -DataElementUaSdk::writeArray (const char **value, const epicsUInt32 len, - const epicsUInt32 num, - OpcUa_BuiltInType targetType, - dbCommon *prec) -{ - long ret = 0; - - if (!incomingData.isArray()) { - errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } else if (incomingData.type() != targetType) { - errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", - prec->name, - variantTypeString(incomingData.type()), - variantTypeString(targetType), - epicsTypeString(*value)); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } else { - UaStringArray arr; - arr.create(static_cast(num)); - for (epicsUInt32 i = 0; i < num; i++) { - char *val = nullptr; - const char *pval; - // add zero termination if necessary - if (memchr(value[i], '\0', len) == nullptr) { - val = new char[len+1]; - strncpy(val, value[i], len); - val[len] = '\0'; - pval = val; - } else { - pval = value[i]; - } - UaString(pval).copyTo(&arr[i]); - delete[] val; - } - { // Scope of Guard G - Guard G(outgoingLock); - UaVariant_set(outgoingData, arr); - markAsDirty(); - } - - dbgWriteArray(num, epicsTypeString(*value)); - } - return ret; -} - -// Specialization for epicsUInt8 / OpcUa_Byte -// (needed because UaByteArray API is different from all other UaXxxArray classes) -// CAVEAT: changes in the template (in DataElementUaSdk.h) must be reflected here -template<> -long -DataElementUaSdk::writeArray (const epicsUInt8 *value, const epicsUInt32 num, - OpcUa_BuiltInType targetType, - dbCommon *prec) -{ - long ret = 0; - - if (!incomingData.isArray()) { - errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } else if (incomingData.type() != targetType) { - errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", - prec->name, - variantTypeString(incomingData.type()), - variantTypeString(targetType), - epicsTypeString(*value)); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } else { - UaByteArray arr(reinterpret_cast(value), static_cast(num)); - { // Scope of Guard G - Guard G(outgoingLock); - UaVariant_set(outgoingData, arr); - markAsDirty(); - } - - dbgWriteArray(num, epicsTypeString(*value)); - } - return ret; -} - -long -DataElementUaSdk::writeArray (const epicsInt8 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, OpcUaType_SByte, prec); -} - -long -DataElementUaSdk::writeArray (const epicsUInt8 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, OpcUaType_Byte, prec); -} - -long -DataElementUaSdk::writeArray (const epicsInt16 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, OpcUaType_Int16, prec); -} - -long -DataElementUaSdk::writeArray (const epicsUInt16 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, OpcUaType_UInt16, prec); -} - -long -DataElementUaSdk::writeArray (const epicsInt32 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, OpcUaType_Int32, prec); -} - -long -DataElementUaSdk::writeArray (const epicsUInt32 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, OpcUaType_UInt32, prec); -} - -long -DataElementUaSdk::writeArray (const epicsInt64 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, OpcUaType_Int64, prec); -} - -long -DataElementUaSdk::writeArray (const epicsUInt64 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, OpcUaType_UInt64, prec); -} - -long -DataElementUaSdk::writeArray (const epicsFloat32 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, OpcUaType_Float, prec); -} - -long -DataElementUaSdk::writeArray (const epicsFloat64 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, OpcUaType_Double, prec); -} - -long -DataElementUaSdk::writeArray (const char *value, const epicsUInt32 len, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(&value, len, num, OpcUaType_String, prec); -} - -void -DataElementUaSdk::requestRecordProcessing (const ProcessReason reason) const -{ - if (isLeaf()) { - pconnector->requestRecordProcessing(reason); - } else { - for (auto &it : elementMap) { - auto pelem = it.second.lock(); - pelem->requestRecordProcessing(reason); - } - } -} - -} // namespace DevOpcua diff --git a/devOpcuaSup/UaSdk/DataElementUaSdk.h b/devOpcuaSup/UaSdk/DataElementUaSdk.h index b9ad542c..44d9316e 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdk.h +++ b/devOpcuaSup/UaSdk/DataElementUaSdk.h @@ -1,5 +1,5 @@ /*************************************************************************\ -* Copyright (c) 2018-2020 ITER Organization. +* Copyright (c) 2018-2025 ITER Organization. * This module is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. \*************************************************************************/ @@ -14,220 +14,87 @@ #ifndef DEVOPCUA_DATAELEMENTUASDK_H #define DEVOPCUA_DATAELEMENTUASDK_H -#include -#include #include #include -#include -#include -#include #include +#include #include -#include "DataElement.h" #include "devOpcua.h" -#include "RecordConnector.h" -#include "Update.h" -#include "UpdateQueue.h" #include "ItemUaSdk.h" namespace DevOpcua { -class ItemUaSdk; - -typedef Update UpdateUaSdk; - -inline const char *epicsTypeString (const epicsInt8 &) { return "epicsInt8"; } -inline const char *epicsTypeString (const epicsUInt8 &) { return "epicsUInt8"; } -inline const char *epicsTypeString (const epicsInt16 &) { return "epicsInt16"; } -inline const char *epicsTypeString (const epicsUInt16 &) { return "epicsUInt16"; } -inline const char *epicsTypeString (const epicsInt32 &) { return "epicsInt32"; } -inline const char *epicsTypeString (const epicsUInt32 &) { return "epicsUInt32"; } -inline const char *epicsTypeString (const epicsInt64 &) { return "epicsInt64"; } -inline const char *epicsTypeString (const epicsUInt64 &) { return "epicsUInt64"; } -inline const char *epicsTypeString (const epicsFloat32 &) { return "epicsFloat32"; } -inline const char *epicsTypeString (const epicsFloat64 &) { return "epicsFloat64"; } -inline const char *epicsTypeString (const char*) { return "epicsString"; } +class DataElementUaSdkNode; inline const char * variantTypeString (const OpcUa_BuiltInType type) { switch(type) { - case OpcUaType_Null: return "OpcUa_Null"; - case OpcUaType_Boolean: return "OpcUa_Boolean"; - case OpcUaType_SByte: return "OpcUa_SByte"; - case OpcUaType_Byte: return "OpcUa_Byte"; - case OpcUaType_Int16: return "OpcUa_Int16"; - case OpcUaType_UInt16: return "OpcUa_UInt16"; - case OpcUaType_Int32: return "OpcUa_Int32"; - case OpcUaType_UInt32: return "OpcUa_UInt32"; - case OpcUaType_Int64: return "OpcUa_Int64"; - case OpcUaType_UInt64: return "OpcUa_UInt64"; - case OpcUaType_Float: return "OpcUa_Float"; - case OpcUaType_Double: return "OpcUa_Double"; - case OpcUaType_String: return "OpcUa_String"; - case OpcUaType_DateTime: return "OpcUa_DateTime"; - case OpcUaType_Guid: return "OpcUa_Guid"; - case OpcUaType_ByteString: return "OpcUa_ByteString"; - case OpcUaType_XmlElement: return "OpcUa_XmlElement"; - case OpcUaType_NodeId: return "OpcUa_NodeId"; - case OpcUaType_ExpandedNodeId: return "OpcUa_ExpandedNodeId"; - case OpcUaType_StatusCode: return "OpcUa_StatusCode"; - case OpcUaType_QualifiedName: return "OpcUa_QualifiedName"; - case OpcUaType_LocalizedText: return "OpcUa_LocalizedText"; - case OpcUaType_ExtensionObject: return "OpcUa_ExtensionObject"; - case OpcUaType_DataValue: return "OpcUa_DataValue"; - case OpcUaType_Variant: return "OpcUa_Variant"; - case OpcUaType_DiagnosticInfo: return "OpcUa_DiagnosticInfo"; + case OpcUaType_Null: return "OpcUa_Null"; + case OpcUaType_Boolean: return "OpcUa_Boolean"; + case OpcUaType_SByte: return "OpcUa_SByte"; + case OpcUaType_Byte: return "OpcUa_Byte"; + case OpcUaType_Int16: return "OpcUa_Int16"; + case OpcUaType_UInt16: return "OpcUa_UInt16"; + case OpcUaType_Int32: return "OpcUa_Int32"; + case OpcUaType_UInt32: return "OpcUa_UInt32"; + case OpcUaType_Int64: return "OpcUa_Int64"; + case OpcUaType_UInt64: return "OpcUa_UInt64"; + case OpcUaType_Float: return "OpcUa_Float"; + case OpcUaType_Double: return "OpcUa_Double"; + case OpcUaType_String: return "OpcUa_String"; + case OpcUaType_DateTime: return "OpcUa_DateTime"; + case OpcUaType_Guid: return "OpcUa_Guid"; + case OpcUaType_ByteString: return "OpcUa_ByteString"; + case OpcUaType_XmlElement: return "OpcUa_XmlElement"; + case OpcUaType_NodeId: return "OpcUa_NodeId"; + case OpcUaType_ExpandedNodeId: return "OpcUa_ExpandedNodeId"; + case OpcUaType_StatusCode: return "OpcUa_StatusCode"; + case OpcUaType_QualifiedName: return "OpcUa_QualifiedName"; + case OpcUaType_LocalizedText: return "OpcUa_LocalizedText"; + case OpcUaType_ExtensionObject: return "OpcUa_ExtensionObject"; + case OpcUaType_DataValue: return "OpcUa_DataValue"; + case OpcUaType_Variant: return "OpcUa_Variant"; + case OpcUaType_DiagnosticInfo: return "OpcUa_DiagnosticInfo"; } return "Illegal Value"; } -// Template for range check when writing -template -inline bool isWithinRange (const FROM &value) { - return !(value < std::numeric_limits::lowest() || value > std::numeric_limits::max()); -} - -// Specializations for unsigned to signed to avoid compiler warnings -template<> -inline bool isWithinRange (const epicsUInt32 &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const epicsUInt32 &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const epicsUInt32 &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const epicsUInt64 &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const epicsUInt64 &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const epicsUInt64 &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const epicsUInt64 &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -// Simple-check specializations for converting signed to unsigned of same or wider type -template<> inline bool isWithinRange (const epicsInt8 &value) { return !(value < 0); } -template<> inline bool isWithinRange (const epicsInt16 &value) { return !(value < 0); } -template<> inline bool isWithinRange (const epicsInt32 &value) { return !(value < 0); } -template<> inline bool isWithinRange (const epicsInt8 &value) { return !(value < 0); } -template<> inline bool isWithinRange (const epicsInt16 &value) { return !(value < 0); } -template<> inline bool isWithinRange (const epicsInt32 &value) { return !(value < 0); } -template<> inline bool isWithinRange (const epicsInt64 &value) { return !(value < 0); } - -// No-check-needed specializations for converting to same or wider type -template<> inline bool isWithinRange (const epicsInt32 &) { return true; } -template<> inline bool isWithinRange (const epicsInt32 &) { return true; } -template<> inline bool isWithinRange (const epicsInt32 &) { return true; } -template<> inline bool isWithinRange (const epicsInt32 &) { return true; } - -template<> inline bool isWithinRange (const epicsUInt32 &) { return true; } -template<> inline bool isWithinRange (const epicsUInt32 &) { return true; } -template<> inline bool isWithinRange (const epicsUInt32 &) { return true; } -template<> inline bool isWithinRange (const epicsUInt32 &) { return true; } -template<> inline bool isWithinRange (const epicsUInt32 &) { return true; } - -template<> inline bool isWithinRange (const epicsInt64 &) { return true; } -template<> inline bool isWithinRange (const epicsInt64 &) { return true; } -template<> inline bool isWithinRange (const epicsInt64 &) { return true; } - -template<> inline bool isWithinRange (const epicsFloat64 &) { return true; } /** * @brief The DataElementUaSdk implementation of a single piece of data. * + * This class implements the common parts of a Node and a Leaf element. * See DevOpcua::DataElement + * */ -class DataElementUaSdk : public DataElement +class DataElementUaSdk { -public: - /** - * @brief Constructor for DataElement from record connector. - * - * Creates the final (leaf) element of the data structure. The record connector - * holds a shared pointer to its leaf, while the data element has a weak pointer - * to the record connector. - * - * @param name name of the element (empty for root element) - * @param pitem pointer to corresponding ItemUaSdk - * @param pconnector pointer to record connector to link to - */ - DataElementUaSdk(const std::string &name, - ItemUaSdk *pitem, - RecordConnector *pconnector); - - /** - * @brief Constructor for DataElement from child element. - * - * Creates an intermediate (node) element of the data structure. The child holds - * a shared pointer, while the parent has a weak pointer in its list/map of child - * nodes, to facilitate traversing the structure when data updates come in. - * - * @param name name of the element - * @param item pointer to corresponding ItemUaSdk - */ - DataElementUaSdk(const std::string &name, - ItemUaSdk *item); - - /** - * @brief Create a DataElement and add it to the item's dataTree. - * - * @param item item to add the element to - * @param pconnector pointer to the leaf's record connector - * @param elementPath full path to the element - */ - static void addElementToTree(ItemUaSdk *item, - RecordConnector *pconnector, - const std::list &elementPath); + friend class DataElementUaSdkNode; + friend class DataElementUaSdkLeaf; - /* ElementTree node interface methods */ - void - addChild(std::weak_ptr elem) - { - elements.push_back(elem); - } +public: + DataElementUaSdk (const std::string &name, class ItemUaSdk *pitem) + : name(name) + , pitem(pitem) + , outgoingLock(pitem->dataTreeWriteLock) + {} + virtual ~DataElementUaSdk(); - std::shared_ptr - findChild(const std::string &name) - { - for (auto it : elements) - if (auto pit = it.lock()) - if (pit->name == name) - return pit; - return std::shared_ptr(); - } - - void - setParent(std::shared_ptr elem) - { - parent = elem; - } + /* ElementTree interface */ + void setParent(std::shared_ptr elem) { parent = elem; } + virtual bool isLeaf() const = 0; + virtual void addChild(std::weak_ptr elem) = 0; + virtual std::shared_ptr findChild(const std::string &name) const = 0; /** - * @brief Print configuration and status. See DevOpcua::DataElement::show + * @brief Print configuration and status. + * See DevOpcua::DataElement::show */ - void show(const int level, const unsigned int indent) const override; + virtual void show(const int level, const unsigned int indent) const = 0; /** * @brief Push an incoming data value into the DataElement. @@ -238,10 +105,13 @@ class DataElementUaSdk : public DataElement * @param value new value for this data element * @param reason reason for this value update * @param timefrom name of element to read item timestamp from + * @param typeId data type of the data element */ - void setIncomingData(const UaVariant &value, - ProcessReason reason, - const std::string *timefrom = nullptr); + virtual void setIncomingData(const UaVariant &value, + ProcessReason reason, + const std::string *timefrom = nullptr, + const UaNodeId *typeId = nullptr) + = 0; /** * @brief Push an incoming event into the DataElement. @@ -251,12 +121,12 @@ class DataElementUaSdk : public DataElement * * @param reason reason for this value update */ - void setIncomingEvent(ProcessReason reason); + virtual void setIncomingEvent(ProcessReason reason) = 0; /** * @brief Set the EPICS-related state of the element and all sub-elements. */ - void setState(const ConnectionStatus state); + virtual void setState(const ConnectionStatus state) = 0; /** * @brief Get the outgoing data value from the DataElement. @@ -266,384 +136,7 @@ class DataElementUaSdk : public DataElement * * @return reference to outgoing data */ - const UaVariant &getOutgoingData(); - - /** - * @brief Read incoming data as a scalar epicsInt32. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readScalar(epicsInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readScalar(epicsInt32 *value, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as a scalar epicsInt64. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readScalar(epicsInt64*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readScalar(epicsInt64 *value, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as a scalar epicsUInt32. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readScalar(epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readScalar(epicsUInt32 *value, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as a scalar epicsFloat64. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readScalar(epicsFloat64*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readScalar(epicsFloat64 *value, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as classic C string (char[]). - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readScalar(char*,const size_t,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - - virtual long int readScalar(char *value, const size_t num, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsInt8. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsInt8*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsInt8 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsUInt8. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsUInt8*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsUInt8 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsInt16. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsInt16*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsInt16 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsUInt16. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsUInt16*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsUInt16 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsInt32. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsInt32*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsInt32 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsUInt32. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsUInt32*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsUInt32 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsInt64. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsInt64*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsInt64 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsUInt64. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsUInt64*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsUInt64 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsFloat32. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsFloat32*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsFloat32 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsFloat64. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsFloat64*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsFloat64 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of EPICS String (char[40]). - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(char*,const epicsUInt32,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(char *value, const epicsUInt32 len, - const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Write outgoing scalar epicsInt32 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeScalar(const epicsInt32&,dbCommon*) - */ - virtual long int writeScalar(const epicsInt32 &value, - dbCommon *prec) override; - - /** - * @brief Write outgoing scalar epicsInt64 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeScalar(const epicsInt64&,dbCommon*) - */ - virtual long int writeScalar(const epicsInt64 &value, - dbCommon *prec) override; - - /** - * @brief Write outgoing scalar epicsUInt32 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeScalar(const epicsUInt32&,dbCommon*) - */ - virtual long int writeScalar(const epicsUInt32 &value, - dbCommon *prec) override; - - /** - * @brief Write outgoing scalar epicsFloat64 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeScalar(const epicsFloat64&,dbCommon*) - */ - virtual long int writeScalar(const epicsFloat64 &value, - dbCommon *prec) override; - - /** - * @brief Write outgoing classic C string (char[]) data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeScalar(const char*,const epicsUInt32,dbCommon*) - */ - virtual long int writeScalar(const char *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsInt8 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsInt8*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsInt8 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsUInt8 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsUInt8*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsUInt8 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsInt16 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsInt16*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsInt16 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsUInt16 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsUInt16*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsUInt16 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsInt32 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsInt32*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsInt32 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsUInt32 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsUInt32*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsUInt32 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsInt64 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsInt64*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsInt64 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsUInt64 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsUInt64*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsUInt64 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsFloat32 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsFloat32*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsFloat32 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsFloat64 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsFloat64*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsFloat64 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of EPICS String (char[MAX_STRING_SIZE]) data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const char*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const char *value, const epicsUInt32 len, - const epicsUInt32 num, - dbCommon *prec) override; + virtual const UaVariant &getOutgoingData() = 0; /** * @brief Clear (discard) the current outgoing data. @@ -655,473 +148,34 @@ class DataElementUaSdk : public DataElement * oldest element from the queue, allowing access to the next element * with the next send. */ - virtual void clearOutgoingData() { outgoingData.clear(); } + virtual void clearOutgoingData() = 0; /** * @brief Create processing requests for record(s) attached to this element. * See DevOpcua::DataElement::requestRecordProcessing */ - virtual void requestRecordProcessing(const ProcessReason reason) const override; + virtual void requestRecordProcessing(const ProcessReason reason) const = 0; /** - * @brief Get debug level from record (via RecordConnector). + * @brief Get debug level from item or record (via RecordConnector). * @return debug level */ - int debug() const { return (isLeaf() ? pconnector->debug() : pitem->debug()); } - -private: - void dbgWriteScalar () const; - void dbgReadScalar(const UpdateUaSdk *upd, - const std::string &targetTypeName, - const size_t targetSize = 0) const; - void dbgReadArray(const UpdateUaSdk *upd, - const epicsUInt32 targetSize, - const std::string &targetTypeName) const; - void checkWriteArray(OpcUa_BuiltInType expectedType, const std::string &targetTypeName) const; - void dbgWriteArray(const epicsUInt32 targetSize, const std::string &targetTypeName) const; - bool updateDataInGenericValue(UaGenericStructureValue &value, - const int index, - std::shared_ptr pelem); - // Structure always returns true to ensure full traversal - bool isDirty() const { return isdirty || !isleaf; } - void - markAsDirty() - { - isdirty = true; - pitem->markAsDirty(); - } - - // Convert the time stamp from a data element - epicsTime - epicsTimeFromUaVariant(const UaVariant &data) const - { - UaDateTime dt; - UaStatus s = data.toDateTime(dt); - if (s.isGood()) { - return ItemUaSdk::uaToEpicsTime(dt, 0); - } - return pitem->tsSource; - } - - // Get the time stamp from the incoming object - const epicsTime &getIncomingTimeStamp() const { - ProcessReason reason = pitem->getReason(); - if ((reason == ProcessReason::incomingData || reason == ProcessReason::readComplete) - && isLeaf()) - switch (pconnector->plinkinfo->timestamp) { - case LinkOptionTimestamp::server: - return pitem->tsServer; - case LinkOptionTimestamp::source: - return pitem->tsSource; - case LinkOptionTimestamp::data: - return pitem->tsData; - } - return pitem->tsClient; - } - - // Get the read status from the incoming object - OpcUa_StatusCode getIncomingReadStatus() const { return pitem->getLastStatus().code(); } + virtual int debug() const { return pitem->debug(); } - // Overloaded helper functions that wrap the UaVariant::toXxx() and UaVariant::setXxx methods - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, OpcUa_Int32 &value) { return variant.toInt32(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, OpcUa_UInt32 &value) { return variant.toUInt32(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, OpcUa_Int64 &value) { return variant.toInt64(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, OpcUa_Double &value) { return variant.toDouble(value); } + const std::string name; /**< element name */ - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaSByteArray &value) { return variant.toSByteArray(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaByteArray &value) { return variant.toByteArray(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaInt16Array &value) { return variant.toInt16Array(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaUInt16Array &value) { return variant.toUInt16Array(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaInt32Array &value) { return variant.toInt32Array(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaUInt32Array &value) { return variant.toUInt32Array(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaInt64Array &value) { return variant.toInt64Array(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaUInt64Array &value) { return variant.toUInt64Array(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaFloatArray &value) { return variant.toFloatArray(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaDoubleArray &value) { return variant.toDoubleArray(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaStringArray &value) { return variant.toStringArray(value); } - - void UaVariant_set(UaVariant &variant, UaSByteArray &value) { variant.setSByteArray(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaByteArray &value) { variant.setByteArray(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaInt16Array &value) { variant.setInt16Array(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaUInt16Array &value) { variant.setUInt16Array(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaInt32Array &value) { variant.setInt32Array(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaUInt32Array &value) { variant.setUInt32Array(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaInt64Array &value) { variant.setInt64Array(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaUInt64Array &value) { variant.setUInt64Array(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaFloatArray &value) { variant.setFloatArray(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaDoubleArray &value) { variant.setDoubleArray(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaStringArray &value) { variant.setStringArray(value, OpcUa_True); } - - // Read scalar value as templated function on EPICS type and OPC UA type - // value == nullptr is allowed and leads to the value being dropped (ignored), - // including the extended status - template - long - readScalar (ET *value, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) - { - long ret = 0; - - if (incomingQueue.empty()) { - errlogPrintf("%s: incoming data queue empty\n", prec->name); - if (nextReason) - *nextReason = ProcessReason::none; - return 1; - } - - ProcessReason nReason; - std::shared_ptr upd = incomingQueue.popUpdate(&nReason); - dbgReadScalar(upd.get(), epicsTypeString(*value)); - - switch (upd->getType()) { - case ProcessReason::readFailure: - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::connectionLoss: - (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::incomingData: - case ProcessReason::readComplete: - { - if (value) { - OpcUa_StatusCode stat = upd->getStatus(); - if (OpcUa_IsNotGood(stat)) { - // No valid OPC UA value - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - // Valid OPC UA value, so try to convert - OT v; - if (OpcUa_IsNotGood(UaVariant_to(upd->getData(), v))) { - errlogPrintf("%s : incoming data (%s) out-of-bounds\n", - prec->name, - upd->getData().toString().toUtf8()); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - } else { - if (OpcUa_IsUncertain(stat)) { - (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); - } - *value = v; - prec->udf = false; - } - } - if (statusCode) *statusCode = stat; - if (statusText) { - strncpy(statusText, UaStatus(stat).toString().toUtf8(), statusTextLen); - statusText[statusTextLen-1] = '\0'; - } - } - break; - } - default: - break; - } - - prec->time = upd->getTimeStamp(); - if (nextReason) *nextReason = nReason; - return ret; - } - - // Read array value as templated function on EPICS type and OPC UA type - // (latter *must match* OPC UA type enum argument) - // CAVEAT: changes must also be reflected in specializations (in DataElementUaSdk.cpp) - template - long - readArray (ET *value, const epicsUInt32 num, - epicsUInt32 *numRead, - OpcUa_BuiltInType expectedType, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) - { - long ret = 0; - epicsUInt32 elemsWritten = 0; - - if (incomingQueue.empty()) { - errlogPrintf("%s : incoming data queue empty\n", prec->name); - *numRead = 0; - return 1; - } - - ProcessReason nReason; - std::shared_ptr upd = incomingQueue.popUpdate(&nReason); - dbgReadArray(upd.get(), num, epicsTypeString(*value)); - - switch (upd->getType()) { - case ProcessReason::readFailure: - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::connectionLoss: - (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::incomingData: - case ProcessReason::readComplete: - { - if (num && value) { - OpcUa_StatusCode stat = upd->getStatus(); - if (OpcUa_IsNotGood(stat)) { - // No valid OPC UA value - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - // Valid OPC UA value, so try to convert - UaVariant &data = upd->getData(); - if (!data.isArray()) { - errlogPrintf("%s : incoming data is not an array\n", prec->name); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else if (data.type() != expectedType) { - errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", - prec->name, variantTypeString(data.type()), epicsTypeString(*value)); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - if (OpcUa_IsUncertain(stat)) { - (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); - } - OT arr; - UaVariant_to(upd->getData(), arr); - elemsWritten = num < arr.length() ? num : arr.length(); - memcpy(value, arr.rawData(), sizeof(ET) * elemsWritten); - prec->udf = false; - } - } - if (statusCode) *statusCode = stat; - if (statusText) { - strncpy(statusText, UaStatus(stat).toString().toUtf8(), statusTextLen); - statusText[statusTextLen-1] = '\0'; - } - } - break; - } - default: - break; - } - - prec->time = upd->getTimeStamp(); - if (nextReason) *nextReason = nReason; - if (num && value) - *numRead = elemsWritten; - return ret; - } - - // Read array value for EPICS String / OpcUa_String - long - readArray (char *value, const epicsUInt32 len, - const epicsUInt32 num, - epicsUInt32 *numRead, - OpcUa_BuiltInType expectedType, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen); - - // Write scalar value as templated function on EPICS type - template - long - writeScalar (const ET &value, - dbCommon *prec) - { - long ret = 0; - - switch (incomingData.type()) { - case OpcUaType_Boolean: - { // Scope of Guard G - Guard G(outgoingLock); - if (value == 0) - outgoingData.setBoolean(false); - else - outgoingData.setBoolean(true); - markAsDirty(); - break; - } - case OpcUaType_Byte: - if (isWithinRange(value)) { - Guard G(outgoingLock); - outgoingData.setByte(static_cast(value)); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case OpcUaType_SByte: - if (isWithinRange(value)) { - Guard G(outgoingLock); - outgoingData.setSByte(static_cast(value)); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case OpcUaType_UInt16: - if (isWithinRange(value)) { - Guard G(outgoingLock); - outgoingData.setUInt16(static_cast(value)); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case OpcUaType_Int16: - if (isWithinRange(value)) { - Guard G(outgoingLock); - outgoingData.setInt16(static_cast(value)); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case OpcUaType_UInt32: - if (isWithinRange(value)) { - Guard G(outgoingLock); - outgoingData.setUInt32(static_cast(value)); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case OpcUaType_Int32: - if (isWithinRange(value)) { - Guard G(outgoingLock); - outgoingData.setInt32(static_cast(value)); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case OpcUaType_UInt64: - if (isWithinRange(value)) { - Guard G(outgoingLock); - outgoingData.setUInt64(static_cast(value)); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case OpcUaType_Int64: - if (isWithinRange(value)) { - Guard G(outgoingLock); - outgoingData.setInt64(static_cast(value)); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case OpcUaType_Float: - if (isWithinRange(value)) { - Guard G(outgoingLock); - outgoingData.setFloat(static_cast(value)); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case OpcUaType_Double: - if (isWithinRange(value)) { - Guard G(outgoingLock); - outgoingData.setDouble(static_cast(value)); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case OpcUaType_String: - { // Scope of Guard G - Guard G(outgoingLock); - outgoingData.setString(static_cast(std::to_string(value).c_str())); - markAsDirty(); - break; - } - default: - errlogPrintf("%s : unsupported conversion for outgoing data\n", - prec->name); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - } - - dbgWriteScalar(); - return ret; - } - - // Write array value for EPICS String / OpcUa_String - long - writeArray (const char **value, const epicsUInt32 len, - const epicsUInt32 num, - OpcUa_BuiltInType targetType, - dbCommon *prec); - - // Write array value as templated function on EPICS type, OPC UA container and simple (element) types - // (latter *must match* OPC UA type enum argument) - // CAVEAT: changes must also be reflected in the specialization (in DataElementUaSdk.cpp) - template - long - writeArray (const ET *value, const epicsUInt32 num, - OpcUa_BuiltInType targetType, - dbCommon *prec) - { - long ret = 0; - - if (!incomingData.isArray()) { - errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } else if (incomingData.type() != targetType) { - errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", - prec->name, - variantTypeString(incomingData.type()), - variantTypeString(targetType), - epicsTypeString(*value)); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } else { - // The array methods must cast away the constness of their value argument - // as the UA SDK API uses non-const parameters - ST *val = const_cast(reinterpret_cast(value)); - CT arr(static_cast(num), val); - { // Scope of Guard G - Guard G(outgoingLock); - UaVariant_set(outgoingData, arr); - markAsDirty(); - } - - dbgWriteArray(num, epicsTypeString(*value)); - } - return ret; - } - - ItemUaSdk *pitem; /**< corresponding item */ - std::vector> elements; /**< children (if node) */ - std::shared_ptr parent; /**< parent */ +private: + virtual bool isDirty() const = 0; + virtual void markAsDirty() = 0; - std::unordered_map> elementMap; - int timesrc; +protected: + ItemUaSdk *pitem; /**< corresponding item */ + std::shared_ptr parent; /**< parent */ - bool mapped; /**< child name to index mapping done */ - UpdateQueue incomingQueue; /**< queue of incoming values */ - UaVariant incomingData; /**< cache of latest incoming value */ - epicsMutex &outgoingLock; /**< data lock for outgoing value */ - UaVariant outgoingData; /**< cache of latest outgoing value */ - bool isdirty; /**< outgoing value has been (or needs to be) updated */ + UaVariant incomingData; /**< cache of latest incoming value */ + epicsMutex &outgoingLock; /**< data lock for outgoing value */ + UaVariant outgoingData; /**< cache of latest outgoing value */ + bool isdirty; /**< outgoing value has been (or needs to be) updated */ }; } // namespace DevOpcua diff --git a/devOpcuaSup/UaSdk/DataElementUaSdkLeaf.cpp b/devOpcuaSup/UaSdk/DataElementUaSdkLeaf.cpp new file mode 100644 index 00000000..db0e8c2b --- /dev/null +++ b/devOpcuaSup/UaSdk/DataElementUaSdkLeaf.cpp @@ -0,0 +1,1514 @@ +/*************************************************************************\ +* Copyright (c) 2018-2026 ITER Organization. +* This module is distributed subject to a Software License Agreement found +* in file LICENSE that is included with this distribution. +\*************************************************************************/ + +/* + * Author: Ralph Lange + * + * based on prototype work by Bernhard Kuner + * and example code from the Unified Automation C++ Based OPC UA Client SDK + */ + +#include "DataElementUaSdkLeaf.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "DataElementUaSdk.h" +#include "DataElementUaSdkNode.h" +#include "ItemUaSdk.h" +#include "RecordConnector.h" + +namespace DevOpcua +{ + +DataElementUaSdkLeaf::DataElementUaSdkLeaf (const std::string &name, + class ItemUaSdk *item, + class RecordConnector *pconnector) + : DataElementUaSdk(name, item) + , incomingQueue(pconnector->plinkinfo->clientQueueSize, pconnector->plinkinfo->discardOldest) +{ + item->dataTreeNoOfLeafs++; +} + +DataElementUaSdkLeaf::~DataElementUaSdkLeaf() +{ + delete enumChoices; + pitem->dataTreeNoOfLeafs--; +} + +/* Explicitly implement the destructor here (allows the compiler to place the vtable) */ +DataElementUaSdk::~DataElementUaSdk() = default; + +/* Specific implementation of DataElement's "factory" method */ +void +DataElement::addElementToTree(Item *item, RecordConnector *pconnector, const std::list &elementPath) +{ + DataElementUaSdkLeaf::addElementToTree(static_cast(item), pconnector, elementPath); +} + +void +DataElementUaSdkLeaf::addElementToTree(ItemUaSdk *item, + RecordConnector *pconnector, + const std::list &elementPath) +{ + std::string name("[ROOT]"); + if (elementPath.size()) + name = elementPath.back(); + + auto leaf = std::make_shared(name, item, pconnector); + item->dataTree.addLeaf(leaf, elementPath, item); + // reference from connector after adding to the tree worked + pconnector->setDataElement(leaf); + leaf->pconnector = pconnector; +} + +void +DataElementUaSdkLeaf::show(const int, const unsigned int indent) const +{ + std::string ind(indent * 2, ' '); + std::cout << ind; + std::cout << "leaf=" << name << " record(" << pconnector->getRecordType() << ")=" << pconnector->getRecordName() + << " type=" << variantTypeString(incomingData.type()) + << " timestamp=" << linkOptionTimestampString(pconnector->plinkinfo->timestamp); + if (pconnector->plinkinfo->timestamp == LinkOptionTimestamp::data) + std::cout << "@" << pitem->linkinfo.timestampElement; + std::cout << " bini=" << linkOptionBiniString(pconnector->plinkinfo->bini) + << " monitor=" << (pconnector->plinkinfo->monitor ? "y" : "n") << "\n"; +} + +void +DataElementUaSdkLeaf::setIncomingData (const UaVariant &value, + ProcessReason reason, + const std::string *, + const UaNodeId *typeId) +{ + incomingData = value; + + if (typeId && pconnector->state() == ConnectionStatus::initialRead) { + delete enumChoices; + enumChoices = pitem->session->getEnumChoices(typeId); + } + if ((pconnector->state() == ConnectionStatus::initialRead + && (reason == ProcessReason::readComplete || reason == ProcessReason::readFailure)) + || (pconnector->state() == ConnectionStatus::up)) { + Guard(pconnector->lock); + bool wasFirst = false; + // Make a copy of the value for this element and put it on the queue + UpdateUaSdk *u(new UpdateUaSdk(getIncomingTimeStamp(), reason, value, getIncomingReadStatus())); + incomingQueue.pushUpdate(std::shared_ptr(u), &wasFirst); + if (debug() >= 5) + std::cout << "Element " << name << " set data (" << processReasonString(reason) << ") for record " + << pconnector->getRecordName() << " (queue use " << incomingQueue.size() << "/" + << incomingQueue.capacity() << ")" << std::endl; + if (wasFirst) + pconnector->requestRecordProcessing(reason); + } +} + +void +DataElementUaSdkLeaf::setIncomingEvent (ProcessReason reason) +{ + Guard(pconnector->lock); + bool wasFirst = false; + UpdateUaSdk *u(new UpdateUaSdk(getIncomingTimeStamp(), reason)); + incomingQueue.pushUpdate(std::shared_ptr(u), &wasFirst); + if (debug() >= 5) + std::cout << "Element " << name << " set event (" << processReasonString(reason) << ") for record " + << pconnector->getRecordName() << " (queue use " << incomingQueue.size() << "/" + << incomingQueue.capacity() << ")" << std::endl; + if (wasFirst) + pconnector->requestRecordProcessing(reason); +} + +void +DataElementUaSdkLeaf::setState(const ConnectionStatus state) +{ + Guard(pconnector->lock); + pconnector->setState(state); +} + +const UaVariant & +DataElementUaSdkLeaf::getOutgoingData() +{ + return outgoingData; +} + +void +DataElementUaSdkLeaf::clearOutgoingData() +{ + outgoingData.clear(); +} + +void +DataElementUaSdkLeaf::requestRecordProcessing(const ProcessReason reason) const +{ + pconnector->requestRecordProcessing(reason); +} + +int +DataElementUaSdkLeaf::debug() const +{ + return pconnector->debug(); +} + +const epicsTime & +DataElementUaSdkLeaf::getIncomingTimeStamp() const +{ + ProcessReason reason = pitem->getReason(); + if ((reason == ProcessReason::incomingData || reason == ProcessReason::readComplete)) + switch (pconnector->plinkinfo->timestamp) { + case LinkOptionTimestamp::server: + return pitem->tsServer; + case LinkOptionTimestamp::source: + return pitem->tsSource; + case LinkOptionTimestamp::data: + return pitem->tsData; + } + return pitem->tsClient; +} + +OpcUa_StatusCode +DataElementUaSdkLeaf::getIncomingReadStatus() const +{ + return pitem->getLastStatus().code(); +} + +OpcUa_StatusCode +DataElementUaSdkLeaf::UaVariant_to(const UaVariant &variant, OpcUa_Int32 &value) +{ + return variant.toInt32(value); +} +OpcUa_StatusCode +DataElementUaSdkLeaf::UaVariant_to(const UaVariant &variant, OpcUa_UInt32 &value) +{ + return variant.toUInt32(value); +} +OpcUa_StatusCode +DataElementUaSdkLeaf::UaVariant_to(const UaVariant &variant, OpcUa_Int64 &value) +{ + return variant.toInt64(value); +} +OpcUa_StatusCode +DataElementUaSdkLeaf::UaVariant_to(const UaVariant &variant, OpcUa_Double &value) +{ + return variant.toDouble(value); +} + +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaBooleanArray &value) +{ + variant.setBoolArray(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaSByteArray &value) +{ + variant.setSByteArray(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaByteArray &value) +{ + variant.setByteArray(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaInt16Array &value) +{ + variant.setInt16Array(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaUInt16Array &value) +{ + variant.setUInt16Array(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaInt32Array &value) +{ + variant.setInt32Array(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaUInt32Array &value) +{ + variant.setUInt32Array(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaInt64Array &value) +{ + variant.setInt64Array(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaUInt64Array &value) +{ + variant.setUInt64Array(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaFloatArray &value) +{ + variant.setFloatArray(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaDoubleArray &value) +{ + variant.setDoubleArray(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaStringArray &value) +{ + variant.setStringArray(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaXmlElementArray &value) +{ + variant.setXmlElementArray(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaLocalizedTextArray &value) +{ + variant.setLocalizedTextArray(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaQualifiedNameArray &value) +{ + variant.setQualifiedNameArray(value, OpcUa_True); +} + +void +DataElementUaSdkLeaf::dbgReadScalar(const UpdateUaSdk *upd, + const std::string &targetTypeName, + const size_t targetSize) const +{ + if (isLeaf() && debug()) { + char time_buf[40]; + upd->getTimeStamp().strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S.%09f"); + ProcessReason reason = upd->getType(); + + std::cout << pconnector->getRecordName() << ": "; + if (reason == ProcessReason::incomingData || reason == ProcessReason::readComplete) { + std::cout << "(" << linkOptionTimestampString(pconnector->plinkinfo->timestamp); + if (pconnector->plinkinfo->timestamp == LinkOptionTimestamp::data) + std::cout << "(@" << pconnector->plinkinfo->timestampElement << ")"; + std::cout << " time " << time_buf << ") read " << processReasonString(reason) << " (" + << UaStatus(upd->getStatus()).toString().toUtf8() << ") "; + UaVariant &data = upd->getData(); + if (data.type() == OpcUaType_String) + std::cout << "'" << data.toString().toUtf8() << "'"; + else + std::cout << data.toString().toUtf8(); + std::cout << " (" << variantTypeString(data.type()) << ")" + << " as " << targetTypeName; + if (targetSize) + std::cout << "[" << targetSize << "]"; + } else { + std::cout << "(client time " << time_buf << ") " << processReasonString(reason); + } + std::cout << " --- remaining queue " << incomingQueue.size() << "/" << incomingQueue.capacity() << std::endl; + } +} + +long +DataElementUaSdkLeaf::readScalar(epicsInt32 *value, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readScalar(epicsInt64 *value, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readScalar(epicsUInt32 *value, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readScalar(epicsFloat64 *value, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); +} + +// Helper functions to convert between OpcUa_ByteString and hex encoded C string +// We use our own functions instead of UaByteString methods toHex/fromHex because +// they work on UaString, not C string, which would require additional copy. +static int +printByteString(const OpcUa_ByteString &byteString, char *encoded, int len) +{ + static const char hexdigits[] = "0123456789ABCDEF"; + int i, l = 0; + for (i = 0; i < byteString.Length && l < len - 3; i++) { + encoded[l++] = hexdigits[byteString.Data[i] >> 4]; + encoded[l++] = hexdigits[byteString.Data[i] & 0xf]; + } + encoded[l] = 0; // terminate + return l; +} + +// parseByteString accepts hex strings with optional spaces separating units of 1 or multiple of 2 hex digits +static int +parseByteString(OpcUa_ByteString &byteString, const char *encoded, int len) +{ + OpcUa_ByteString_Clear(&byteString); + byteString.Data + = static_cast(OpcUa_Alloc((len + 1) / 2)); // can never be longer, shorter does not hurt + if (!byteString.Data) + return -1; + int l = 0; + bool firstDigit = true; + while (len--) { + OpcUa_Byte c, b; + c = *encoded++; + if (!c) + break; + if (isblank(c)) { + firstDigit = true; + continue; + } + if (!isxdigit(c)) { + // invalid character + OpcUa_ByteString_Clear(&byteString); + return -1; + } + b = c - (isdigit(c) ? '0' : isupper(c) ? 'A' - 10 : 'a' - 10); + if (len && isxdigit(c = *encoded)) { + firstDigit = false; + encoded++; + len--; + b <<= 4; + b |= c - (isdigit(c) ? '0' : isupper(c) ? 'A' - 10 : 'a' - 10); + } else { + if (!firstDigit || (c && !isblank(c) && !isxdigit(c))) { + // 1 is the only odd number of digits allowed + // because otherwise where is the byte border? 12|3 or 1|23 ? + OpcUa_ByteString_Clear(&byteString); + return -1; + } + } + byteString.Data[l++] = b; + } + byteString.Length = l; + return l; +} + +// CString type needs specialization +long +DataElementUaSdkLeaf::readScalar(char *value, + const epicsUInt32 len, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *lenRead, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + long ret = 0; + + if (incomingQueue.empty()) { + errlogPrintf("%s: incoming data queue empty\n", prec->name); + if (nextReason) + *nextReason = ProcessReason::none; + return 1; + } + + ProcessReason nReason; + std::shared_ptr upd = incomingQueue.popUpdate(&nReason); + dbgReadScalar(upd.get(), "CString", len); + + switch (upd->getType()) { + case ProcessReason::readFailure: + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::connectionLoss: + (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::incomingData: + case ProcessReason::readComplete: { + if (len && value) { + OpcUa_StatusCode stat = upd->getStatus(); + if (OpcUa_IsNotGood(stat)) { + // No valid OPC UA value + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + // Valid OPC UA value, so copy over + if (OpcUa_IsUncertain(stat)) { + (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); + } + UaVariant &data = upd->getData(); + UaString str; + const void *s = nullptr; + OpcUa_Int32 n = 0; + if (data.type() == OpcUaType_ExtensionObject) { + UaExtensionObject extensionObject; + data.toExtensionObject(extensionObject); + UaStructureDefinition definition = pitem->structureDefinition(extensionObject.encodingTypeId()); + if (definition.isUnion()) { + UaGenericUnionValue genericValue(extensionObject, definition); + int switchValue = genericValue.switchValue(); + if (switchValue > 0) { + n = snprintf(value, + len, + "%s:%s", + definition.child(switchValue - 1).name().toUtf8(), + genericValue.value().toString().toUtf8()); + } else { + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } + } + } else { + const OpcUa_Variant *variant = data; + switch (variant->Datatype) { + case OpcUaType_Byte: + case OpcUaType_SByte: + if (variant->ArrayType == OpcUa_VariantArrayType_Array) { + n = variant->Value.Array.Length; + s = variant->Value.Array.Value.Array; + } else if (variant->ArrayType == OpcUa_VariantArrayType_Matrix) { + OpcUa_VariantMatrix_GetElementCount(&variant->Value.Matrix, &n); + s = variant->Value.Matrix.Value.Array; + } + if (static_cast(n) > len) + n = len; + break; + case OpcUaType_ByteString: + n = printByteString(variant->Value.ByteString, value, len); + } + if (enumChoices) { + OpcUa_Int32 enumIndex; + data.toInt32(enumIndex); + const auto &it = enumChoices->find(enumIndex); + if (it != enumChoices->end() && !it->second.empty()) { + s = it->second.c_str(); + n = static_cast(it->second.length()); + if (static_cast(n) >= len) + n = len - 1; + } + } + if (!n) { + str = data.toString(); + s = str.toUtf8(); + n = str.length(); + if (static_cast(n) >= len) + n = len - 1; + } + if (s) + memcpy(value, s, n); + } + memset(value + n, 0, len - n); + prec->udf = false; + if (lenRead) + *lenRead = n; + } + if (statusCode) + *statusCode = stat; + if (statusText) { + strncpy(statusText, UaStatus(stat).toString().toUtf8(), statusTextLen); + statusText[statusTextLen - 1] = '\0'; + } + } + break; + } + default: + break; + } + + prec->time = upd->getTimeStamp(); + if (nextReason) + *nextReason = nReason; + return ret; +} + +void +DataElementUaSdkLeaf::dbgReadArray(const UpdateUaSdk *upd, + const epicsUInt32 targetSize, + const std::string &targetTypeName) const +{ + if (isLeaf() && debug()) { + char time_buf[40]; + upd->getTimeStamp().strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S.%09f"); + ProcessReason reason = upd->getType(); + + std::cout << pconnector->getRecordName() << ": "; + if (reason == ProcessReason::incomingData || reason == ProcessReason::readComplete) { + std::cout << "(" << linkOptionTimestampString(pconnector->plinkinfo->timestamp); + if (pconnector->plinkinfo->timestamp == LinkOptionTimestamp::data) + std::cout << "@" << pconnector->plinkinfo->timestampElement; + std::cout << " time " << time_buf << ") read " << processReasonString(reason) << " (" + << UaStatus(upd->getStatus()).toString().toUtf8() << ") "; + UaVariant &data = upd->getData(); + std::cout << " array of " << variantTypeString(data.type()) << "[" << upd->getData().arraySize() << "]" + << " into " << targetTypeName << "[" << targetSize << "]"; + } else { + std::cout << "(client time " << time_buf << ") " << processReasonString(reason); + } + std::cout << " --- remaining queue " << incomingQueue.size() << "/" << incomingQueue.capacity() << std::endl; + } +} + +// Read array for EPICS String / OpcUa_String +long int +DataElementUaSdkLeaf::readArray(char *value, + epicsUInt32 len, + const epicsUInt32 num, + epicsUInt32 *numRead, + OpcUa_BuiltInType expectedType, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + long ret = 0; + epicsUInt32 elemsWritten = 0; + + // clear *old* array content + memset(value, 0, *numRead * len); + + if (incomingQueue.empty()) { + errlogPrintf("%s : incoming data queue empty\n", prec->name); + *numRead = 0; + return 1; + } + + ProcessReason nReason; + std::shared_ptr upd = incomingQueue.popUpdate(&nReason); + dbgReadArray(upd.get(), num, epicsTypeString(value)); + + switch (upd->getType()) { + case ProcessReason::readFailure: + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::connectionLoss: + (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::incomingData: + case ProcessReason::readComplete: { + if (num && value) { + OpcUa_StatusCode stat = upd->getStatus(); + if (OpcUa_IsNotGood(stat)) { + // No valid OPC UA value + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + // Valid OPC UA value, so try to convert + UaVariant &variant = upd->getData(); + if (!variant.isArray()) { + errlogPrintf("%s : incoming data is not an array\n", prec->name); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + if (OpcUa_IsUncertain(stat)) { + (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); + } + elemsWritten = variant.arraySize(); + if (elemsWritten > num) + elemsWritten = num; + const OpcUa_VariantArrayUnion &arrayData + = static_cast(variant)->Value.Array.Value; + size_t l; + switch (variant.type()) { + case OpcUaType_String: { + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + l = OpcUa_String_StrSize(&arrayData.StringArray[i]); + if (l >= len) + l = len - 1; + memcpy(value + i * len, OpcUa_String_GetRawString(&arrayData.StringArray[i]), l); + } + break; + } + case OpcUaType_XmlElement: { + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + l = arrayData.XmlElementArray[i].Length; + if (l >= len) + l = len - 1; + memcpy(value + i * len, arrayData.XmlElementArray[i].Data, l); + } + break; + } + case OpcUaType_LocalizedText: { + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + l = OpcUa_String_StrSize(&arrayData.LocalizedTextArray[i].Text); + if (l >= len) + l = len - 1; + memcpy( + value + i * len, OpcUa_String_GetRawString(&arrayData.LocalizedTextArray[i].Text), l); + } + break; + } + case OpcUaType_QualifiedName: { + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + l = OpcUa_String_StrSize(&arrayData.QualifiedNameArray[i].Name); + if (l >= len) + l = len - 1; + memcpy( + value + i * len, OpcUa_String_GetRawString(&arrayData.QualifiedNameArray[i].Name), l); + } + break; + } + case OpcUaType_ByteString: { + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + printByteString(arrayData.ByteStringArray[i], value + i * len, len); + } + break; + } + default: + errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", + prec->name, + variantTypeString(variant.type()), + epicsTypeString(value)); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } + prec->udf = false; + } + } + if (statusCode) + *statusCode = stat; + if (statusText) { + strncpy(statusText, UaStatus(stat).toString().toUtf8(), statusTextLen); + statusText[statusTextLen - 1] = '\0'; + } + } + break; + } + default: + break; + } + + prec->time = upd->getTimeStamp(); + if (nextReason) + *nextReason = nReason; + if (num && value) + *numRead = elemsWritten; + return ret; +} + +// Specialization for epicsUInt8 / OpcUa_Byte +// needed because UaByteArray API is different from all other UaXxxArray classes +// and because epicsUInt8 is also used for arrays of OpcUa_Boolean +// CAVEAT: changes in the template (in DataElementUaSdk.h) must be reflected here +template<> +long +DataElementUaSdkLeaf::readArray(epicsUInt8 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + OpcUa_BuiltInType expectedType, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + long ret = 0; + epicsUInt32 elemsWritten = 0; + + if (incomingQueue.empty()) { + errlogPrintf("%s : incoming data queue empty\n", prec->name); + *numRead = 0; + return 1; + } + + ProcessReason nReason; + std::shared_ptr upd = incomingQueue.popUpdate(&nReason); + dbgReadArray(upd.get(), num, epicsTypeString(*value)); + + switch (upd->getType()) { + case ProcessReason::readFailure: + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::connectionLoss: + (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::incomingData: + case ProcessReason::readComplete: { + if (num && value) { + OpcUa_StatusCode stat = upd->getStatus(); + if (OpcUa_IsNotGood(stat)) { + // No valid OPC UA value + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + // Valid OPC UA value, so try to convert + UaVariant &variant = upd->getData(); + if (!variant.isArray() && variant.type() == OpcUaType_ByteString) { + const OpcUa_ByteString &bs = static_cast(variant)->Value.ByteString; + elemsWritten = bs.Length; + if (elemsWritten > num) + elemsWritten = num; + memcpy(value, bs.Data, elemsWritten); + } else if (!variant.isArray()) { + errlogPrintf("%s : incoming data is not an array\n", prec->name); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else if (variant.type() != expectedType && variant.type() != OpcUaType_Boolean) { + errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", + prec->name, + variantTypeString(variant.type()), + epicsTypeString(*value)); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + if (OpcUa_IsUncertain(stat)) { + (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); + } + elemsWritten = variant.arraySize(); + if (elemsWritten > num) + elemsWritten = num; + memcpy(value, static_cast(variant)->Value.Array.Value.Array, elemsWritten); + } + } + if (statusCode) + *statusCode = stat; + if (statusText) { + strncpy(statusText, UaStatus(stat).toString().toUtf8(), statusTextLen); + statusText[statusTextLen - 1] = '\0'; + } + } + break; + } + default: + break; + } + + prec->time = upd->getTimeStamp(); + if (nextReason) + *nextReason = nReason; + if (num) + *numRead = elemsWritten; + return ret; +} + +long +DataElementUaSdkLeaf::readArray(epicsInt8 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, num, numRead, OpcUaType_SByte, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readArray(epicsUInt8 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, num, numRead, OpcUaType_Byte, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readArray(epicsInt16 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, num, numRead, OpcUaType_Int16, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readArray(epicsUInt16 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, num, numRead, OpcUaType_UInt16, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readArray(epicsInt32 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, num, numRead, OpcUaType_Int32, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readArray(epicsUInt32 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, num, numRead, OpcUaType_UInt32, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readArray(epicsInt64 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, num, numRead, OpcUaType_Int64, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readArray(epicsUInt64 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, num, numRead, OpcUaType_UInt64, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readArray(epicsFloat32 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, num, numRead, OpcUaType_Float, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readArray(epicsFloat64 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, num, numRead, OpcUaType_Double, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readArray(char *value, + epicsUInt32 len, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, len, num, numRead, OpcUaType_String, prec, nextReason, statusCode, statusText, statusTextLen); +} + +inline void +DataElementUaSdkLeaf::dbgWriteScalar() const +{ + if (isLeaf() && debug()) { + std::cout << pconnector->getRecordName() << ": set outgoing data (" << variantTypeString(outgoingData.type()) + << ") to value "; + if (outgoingData.type() == OpcUaType_String) + std::cout << "'" << outgoingData.toString().toUtf8() << "'"; + else + std::cout << outgoingData.toString().toUtf8(); + std::cout << std::endl; + } +} + +long +DataElementUaSdkLeaf::writeScalar(const epicsInt32 &value, dbCommon *prec) +{ + return writeScalar(value, prec); +} + +long +DataElementUaSdkLeaf::writeScalar(const epicsUInt32 &value, dbCommon *prec) +{ + return writeScalar(value, prec); +} + +long +DataElementUaSdkLeaf::writeScalar(const epicsInt64 &value, dbCommon *prec) +{ + return writeScalar(value, prec); +} + +long +DataElementUaSdkLeaf::writeScalar(const epicsFloat64 &value, dbCommon *prec) +{ + return writeScalar(value, prec); +} + +long +DataElementUaSdkLeaf::writeScalar(const char *value, epicsUInt32 len, dbCommon *prec) +{ + long ret = 1; + long l; + unsigned long ul; + double d; + char *end = nullptr; + OpcUa_BuiltInType type = incomingData.type(); + + if (type == OpcUaType_ExtensionObject) { + UaExtensionObject extensionObject; + incomingData.toExtensionObject(extensionObject); + UaStructureDefinition definition = pitem->structureDefinition(extensionObject.encodingTypeId()); + if (definition.isUnion()) { + if (value[0] == '\0') { + } else + for (int i = 0; i < definition.childrenCount(); i++) { + UaGenericUnionValue genericValue(extensionObject, definition); + const UaString &memberName = definition.child(i).name(); + epicsUInt32 namelen = static_cast(memberName.length()); + if ((strncmp(value, memberName.toUtf8(), namelen) == 0) && value[namelen] == ':') { + // temporarily set incomingData to selected union member type + UaVariant saveValue = incomingData; + OpcUa_Variant fakeValue; + OpcUa_Variant_Initialize(&fakeValue); + fakeValue.Datatype = definition.child(i).valueType(); + incomingData = fakeValue; + const UaNodeId &typeId = definition.child(i).typeId(); + enumChoices = pitem->session->getEnumChoices(&typeId); + // recurse to set union member + ret = writeScalar(value + namelen + 1, len - (namelen + 1), prec); + // restore incomingData type to union + delete enumChoices; + enumChoices = nullptr; + incomingData = saveValue; + if (ret == 0) { + Guard G(outgoingLock); + genericValue.setValue(i + 1, outgoingData); + genericValue.toExtensionObject(extensionObject); + outgoingData.setExtensionObject(extensionObject, true); + } + return ret; + } + } + } + } + + switch (type) { + case OpcUaType_String: { // Scope of Guard G + Guard G(outgoingLock); + outgoingData.setString(UaByteString(len, (OpcUa_Byte *) value)); // UaByteString supports len + markAsDirty(); + ret = 0; + break; + } + case OpcUaType_XmlElement: { // Scope of Guard G + Guard G(outgoingLock); + outgoingData.setXmlElement(UaByteString(len, (OpcUa_Byte *) value)); + markAsDirty(); + ret = 0; + break; + } + case OpcUaType_LocalizedText: { // Scope of Guard G + Guard G(outgoingLock); + UaLocalizedText localizedText; + const char *sep = static_cast(memchr(value, '|', len)); + if (sep) { + localizedText.setLocale(UaByteString(static_cast(sep - value), (OpcUa_Byte *) value)); + value = sep + 1; + len -= static_cast(sep + 1 - value); + } else { // keep the locale + incomingData.toLocalizedText(localizedText); + } + localizedText.setText(UaByteString(len, (OpcUa_Byte *) value)); + outgoingData.setLocalizedText(localizedText); + markAsDirty(); + ret = 0; + break; + } + case OpcUaType_QualifiedName: { // Scope of Guard G + Guard G(outgoingLock); + UaQualifiedName qualifiedName; + const char *sep = static_cast(memchr(value, '|', len)); + if (sep) { + qualifiedName.setNamespaceIndex(atoi(value)); + value = sep + 1; + len -= static_cast(sep + 1 - value); + } else { // keep the namespace + incomingData.toQualifiedName(qualifiedName); + } + qualifiedName.setQualifiedName(UaByteString(len, (OpcUa_Byte *) value), qualifiedName.namespaceIndex()); + outgoingData.setQualifiedName(qualifiedName); + markAsDirty(); + ret = 0; + break; + } + case OpcUaType_ByteString: { // Scope of Guard G + OpcUa_ByteString bs; + OpcUa_ByteString_Initialize(&bs); + if (parseByteString(bs, value, len) >= 0) { + UaByteString byteString; + byteString.attach(&bs); // wrap, no copy + Guard G(outgoingLock); + outgoingData.setByteString(byteString, true); // move, no copy + markAsDirty(); + ret = 0; + } + break; + } + case OpcUaType_Boolean: { // Scope of Guard G + Guard G(outgoingLock); + if (strchr("YyTt1", *value)) + outgoingData.setBoolean(true); + else + outgoingData.setBoolean(false); + markAsDirty(); + ret = 0; + break; + } + case OpcUaType_Byte: + ul = strtoul(value, &end, 0); + if (end != value && isWithinRange(ul)) { + Guard G(outgoingLock); + outgoingData.setByte(static_cast(ul)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_SByte: + l = strtol(value, &end, 0); + if (end != value && isWithinRange(l)) { + Guard G(outgoingLock); + outgoingData.setSByte(static_cast(l)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_UInt16: + ul = strtoul(value, &end, 0); + if (end != value && isWithinRange(ul)) { + Guard G(outgoingLock); + outgoingData.setUInt16(static_cast(ul)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_Int16: + l = strtol(value, &end, 0); + if (end != value && isWithinRange(l)) { + Guard G(outgoingLock); + outgoingData.setInt16(static_cast(l)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_UInt32: + ul = strtoul(value, &end, 0); + if (end != value && isWithinRange(ul)) { + Guard G(outgoingLock); + outgoingData.setUInt32(static_cast(ul)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_Int32: + l = strtol(value, &end, 0); + if (enumChoices) { + // first test enum strings then numeric values + // in case a string starts with a number but means a different value + for (const auto &it : *enumChoices) + if (it.second == value) { + l = static_cast(it.first); + ret = 0; + end = nullptr; + break; + } + if (ret != 0 && end != value) + for (const auto &it : *enumChoices) + if (l == it.first) { + ret = 0; + break; + } + if (ret != 0) + break; + } + if (end != value && isWithinRange(l)) { + Guard G(outgoingLock); + outgoingData.setInt32(static_cast(l)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_UInt64: + ul = strtoul(value, &end, 0); + if (end != value && isWithinRange(ul)) { + Guard G(outgoingLock); + outgoingData.setUInt64(static_cast(ul)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_Int64: + l = strtol(value, &end, 0); + if (end != value && isWithinRange(l)) { + Guard G(outgoingLock); + outgoingData.setInt64(static_cast(l)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_Float: + d = strtod(value, &end); + if (end != value && isWithinRange(d)) { + Guard G(outgoingLock); + outgoingData.setFloat(static_cast(d)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_Double: { + d = strtod(value, &end); + if (end != value) { + Guard G(outgoingLock); + outgoingData.setDouble(static_cast(d)); + markAsDirty(); + ret = 0; + } + break; + } + default: + errlogPrintf( + "%s : unsupported conversion from string to %s for outgoing data\n", prec->name, variantTypeString(type)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + return -1; + } + + if (ret != 0) { + errlogPrintf("%s : value \"%s\" out of range\n", prec->name, value); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + } + if (ret == 0) + dbgWriteScalar(); + return ret; +} + +inline void +DataElementUaSdkLeaf::dbgWriteArray(const epicsUInt32 targetSize, const std::string &targetTypeName) const +{ + if (isLeaf() && debug()) { + std::cout << pconnector->getRecordName() << ": writing array of " << targetTypeName << "[" << targetSize + << "] as " << variantTypeString(outgoingData.type()) << "[" << outgoingData.arraySize() << "]" + << std::endl; + } +} + +// Write array for EPICS String / OpcUa_String +long +DataElementUaSdkLeaf::writeArray( + const char *value, epicsUInt32 len, const epicsUInt32 num, OpcUa_BuiltInType targetType, dbCommon *prec) +{ + long ret = 0; + + if (!incomingData.isArray()) { + errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else { + switch (incomingData.type()) { + case OpcUaType_String: { + UaStringArray arr; + arr.create(num); + for (epicsUInt32 i = 0; i < num; i++) { + UaString(UaByteString(len, (OpcUa_Byte *) value + i * len)).copyTo(&arr[i]); + } + { // Scope of Guard G + Guard G(outgoingLock); + UaVariant_set(outgoingData, arr); + markAsDirty(); + } + break; + } + case OpcUaType_XmlElement: { + UaXmlElementArray arr; + arr.create(num); + for (epicsUInt32 i = 0; i < num; i++) { + UaByteString(len, (OpcUa_Byte *) value + i * len).copyTo(&arr[i]); + } + { // Scope of Guard G + Guard G(outgoingLock); + UaVariant_set(outgoingData, arr); + markAsDirty(); + } + break; + } + case OpcUaType_LocalizedText: { + UaLocalizedTextArray arr; + OpcUa_UInt32 arraySize = incomingData.arraySize(); + const OpcUa_LocalizedText *incoming + = static_cast(incomingData)->Value.Array.Value.LocalizedTextArray; + + arr.create(num); + for (epicsUInt32 i = 0; i < num; i++) { + const char *sep = static_cast(memchr(value, '|', len)); + if (sep) { + OpcUa_String_AttachToString(const_cast(value), + static_cast(sep - value), + 0, + OpcUa_True, + OpcUa_False, + &arr[i].Locale); + } else if (i < arraySize) { + OpcUa_String_StrnCpy(&arr[i].Locale, &incoming[i].Locale, OPCUA_STRING_LENDONTCARE); + } else if (i > 0) { + OpcUa_String_StrnCpy(&arr[i].Locale, &arr[i - 1].Locale, OPCUA_STRING_LENDONTCARE); + } + OpcUa_String_AttachToString(const_cast(sep ? sep + 1 : value), + OPCUA_STRINGLENZEROTERMINATED, + 0, + OpcUa_True, + OpcUa_False, + &arr[i].Text); + value += len; + } + { // Scope of Guard G + Guard G(outgoingLock); + UaVariant_set(outgoingData, arr); + markAsDirty(); + } + break; + } + case OpcUaType_QualifiedName: { + UaQualifiedNameArray arr; + OpcUa_UInt32 arraySize = incomingData.arraySize(); + const OpcUa_QualifiedName *incoming + = static_cast(incomingData)->Value.Array.Value.QualifiedNameArray; + + arr.create(num); + for (epicsUInt32 i = 0; i < num; i++) { + const char *sep = static_cast(memchr(value, '|', len)); + if (sep) { + arr[i].NamespaceIndex = atoi(value); + } else if (i < arraySize) { + arr[i].NamespaceIndex = incoming[i].NamespaceIndex; + } else if (i > 0) { + arr[i].NamespaceIndex = arr[i - 1].NamespaceIndex; + } + OpcUa_String_AttachToString(const_cast(sep ? sep + 1 : value), + OPCUA_STRINGLENZEROTERMINATED, + 0, + OpcUa_True, + OpcUa_False, + &arr[i].Name); + value += len; + } + { // Scope of Guard G + Guard G(outgoingLock); + UaVariant_set(outgoingData, arr); + markAsDirty(); + } + break; + } + default: + errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", + prec->name, + variantTypeString(incomingData.type()), + variantTypeString(targetType), + epicsTypeString(value)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + dbgWriteArray(num, epicsTypeString(value)); + } + return ret; +} + +// Specialization for epicsUInt8 / OpcUa_Byte +// needed because UaByteArray API is different from all other UaXxxArray classes +// and because epicsUInt8 is also used for arrays of OpcUa_Boolean +// CAVEAT: changes in the template (in DataElementUaSdk.h) must be reflected here +template<> +long +DataElementUaSdkLeaf::writeArray(const epicsUInt8 *value, + const epicsUInt32 num, + OpcUa_BuiltInType targetType, + dbCommon *prec) +{ + long ret = 0; + + if (!incomingData.isArray() && incomingData.type() == OpcUaType_ByteString) { + UaByteString bs(num, reinterpret_cast(const_cast(value))); + { // Scope of Guard G + Guard G(outgoingLock); + outgoingData.setByteString(bs, true); // move, no copy + markAsDirty(); + } + + dbgWriteScalar(); + } else if (!incomingData.isArray()) { + errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else if (incomingData.type() != OpcUaType_Byte && incomingData.type() != OpcUaType_Boolean) { + errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", + prec->name, + variantTypeString(incomingData.type()), + variantTypeString(targetType), + epicsTypeString(*value)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else { + if (incomingData.type() == OpcUaType_Byte) { + UaByteArray arr(reinterpret_cast(value), static_cast(num)); + { // Scope of Guard G + Guard G(outgoingLock); + UaVariant_set(outgoingData, arr); + markAsDirty(); + } + } else { + UaBooleanArray arr(static_cast(num), const_cast(value)); + { // Scope of Guard G + Guard G(outgoingLock); + UaVariant_set(outgoingData, arr); + markAsDirty(); + } + } + dbgWriteArray(num, epicsTypeString(*value)); + } + return ret; +} + +long +DataElementUaSdkLeaf::writeArray(const epicsInt8 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, OpcUaType_SByte, prec); +} + +long +DataElementUaSdkLeaf::writeArray(const epicsUInt8 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, OpcUaType_Byte, prec); +} + +long +DataElementUaSdkLeaf::writeArray(const epicsInt16 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, OpcUaType_Int16, prec); +} + +long +DataElementUaSdkLeaf::writeArray(const epicsUInt16 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, OpcUaType_UInt16, prec); +} + +long +DataElementUaSdkLeaf::writeArray(const epicsInt32 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, OpcUaType_Int32, prec); +} + +long +DataElementUaSdkLeaf::writeArray(const epicsUInt32 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, OpcUaType_UInt32, prec); +} + +long +DataElementUaSdkLeaf::writeArray(const epicsInt64 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, OpcUaType_Int64, prec); +} + +long +DataElementUaSdkLeaf::writeArray(const epicsUInt64 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, OpcUaType_UInt64, prec); +} + +long +DataElementUaSdkLeaf::writeArray(const epicsFloat32 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, OpcUaType_Float, prec); +} + +long +DataElementUaSdkLeaf::writeArray(const epicsFloat64 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, OpcUaType_Double, prec); +} + +long +DataElementUaSdkLeaf::writeArray(const char *value, epicsUInt32 len, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, len, num, OpcUaType_String, prec); +} + +} // namespace DevOpcua diff --git a/devOpcuaSup/UaSdk/DataElementUaSdkLeaf.h b/devOpcuaSup/UaSdk/DataElementUaSdkLeaf.h new file mode 100644 index 00000000..962e8134 --- /dev/null +++ b/devOpcuaSup/UaSdk/DataElementUaSdkLeaf.h @@ -0,0 +1,1030 @@ +/*************************************************************************\ +* Copyright (c) 2018-2026 ITER Organization. +* This module is distributed subject to a Software License Agreement found +* in file LICENSE that is included with this distribution. +\*************************************************************************/ + +/* + * Author: Ralph Lange + * + * based on prototype work by Bernhard Kuner + * and example code from the Unified Automation C++ Based OPC UA Client SDK + */ + +#ifndef DEVOPCUA_DATAELEMENTUASDKLEAF_H +#define DEVOPCUA_DATAELEMENTUASDKLEAF_H + +#include + +#include "DataElementUaSdk.h" +#include "RecordConnector.h" +#include "Update.h" +#include "UpdateQueue.h" + +namespace DevOpcua { + +class ItemUaSdk; + +typedef Update UpdateUaSdk; + +inline const char *epicsTypeString (const epicsInt8 &) { return "epicsInt8"; } +inline const char *epicsTypeString (const epicsUInt8 &) { return "epicsUInt8"; } +inline const char *epicsTypeString (const epicsInt16 &) { return "epicsInt16"; } +inline const char *epicsTypeString (const epicsUInt16 &) { return "epicsUInt16"; } +inline const char *epicsTypeString (const epicsInt32 &) { return "epicsInt32"; } +inline const char *epicsTypeString (const epicsUInt32 &) { return "epicsUInt32"; } +inline const char *epicsTypeString (const epicsInt64 &) { return "epicsInt64"; } +inline const char *epicsTypeString (const epicsUInt64 &) { return "epicsUInt64"; } +inline const char *epicsTypeString (const epicsFloat32 &) { return "epicsFloat32"; } +inline const char *epicsTypeString (const epicsFloat64 &) { return "epicsFloat64"; } +inline const char *epicsTypeString (const char *) { return "epicsString"; } + +// Template for range check when writing +template +inline bool +isWithinRange (const FROM &value) +{ + return !(value < std::numeric_limits::lowest() || value > std::numeric_limits::max()); +} + +// Specializations for unsigned to signed to avoid compiler warnings +template<> +inline bool +isWithinRange(const epicsUInt32 &value) +{ + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool +isWithinRange(const epicsUInt32 &value) +{ + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool +isWithinRange(const epicsUInt32 &value) +{ + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool +isWithinRange(const epicsUInt64 &value) +{ + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool +isWithinRange(const epicsUInt64 &value) +{ + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool +isWithinRange(const epicsUInt64 &value) +{ + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool +isWithinRange(const epicsUInt64 &value) +{ + return !(value > static_cast(std::numeric_limits::max())); +} + +// Simple-check specializations for converting signed to unsigned of same or wider type +template<> +inline bool +isWithinRange(const epicsInt8 &value) +{ + return !(value < 0); +} +template<> +inline bool +isWithinRange(const epicsInt16 &value) +{ + return !(value < 0); +} +template<> +inline bool +isWithinRange(const epicsInt32 &value) +{ + return !(value < 0); +} +template<> +inline bool +isWithinRange(const epicsInt8 &value) +{ + return !(value < 0); +} +template<> +inline bool +isWithinRange(const epicsInt16 &value) +{ + return !(value < 0); +} +template<> +inline bool +isWithinRange(const epicsInt32 &value) +{ + return !(value < 0); +} +template<> +inline bool +isWithinRange(const epicsInt64 &value) +{ + return !(value < 0); +} + +// No-check-needed specializations for converting to same or wider type +template<> +inline bool +isWithinRange(const epicsInt32 &) +{ + return true; +} +template<> +inline bool +isWithinRange(const epicsInt32 &) +{ + return true; +} +template<> +inline bool +isWithinRange(const epicsInt32 &) +{ + return true; +} +template<> +inline bool +isWithinRange(const epicsInt32 &) +{ + return true; +} + +template<> +inline bool +isWithinRange(const epicsUInt32 &) +{ + return true; +} +template<> +inline bool +isWithinRange(const epicsUInt32 &) +{ + return true; +} +template<> +inline bool +isWithinRange(const epicsUInt32 &) +{ + return true; +} +template<> +inline bool +isWithinRange(const epicsUInt32 &) +{ + return true; +} +template<> +inline bool +isWithinRange(const epicsUInt32 &) +{ + return true; +} + +template<> +inline bool +isWithinRange(const epicsInt64 &) +{ + return true; +} +template<> +inline bool +isWithinRange(const epicsInt64 &) +{ + return true; +} +template<> +inline bool +isWithinRange(const epicsInt64 &) +{ + return true; +} + +template<> +inline bool +isWithinRange(const epicsFloat64 &) +{ + return true; +} + +class DataElementUaSdkLeaf + : public DataElement + , public DataElementUaSdk +{ +public: + /** + * @brief Constructor for DataElement (leaf) from record connector. + * + * Creates a final leaf element of the data structure. + * To avoid a circular dependency, + * the record connector holds a shared pointer to its leaf, + * while the data element has a weak pointer to the record connector. + * + * @param name name of the element (empty for root element) + * @param pitem pointer to corresponding ItemUaSdk + * @param pconnector pointer to record connector to link to + */ + DataElementUaSdkLeaf(const std::string &name, ItemUaSdk *item, RecordConnector *pconnector); + virtual ~DataElementUaSdkLeaf () override; + + /* ElementTree node interface methods */ + virtual bool isLeaf () const override { return true; } + virtual void addChild (std::weak_ptr elem) override {} + virtual std::shared_ptr findChild (const std::string &name) const override + { + return std::shared_ptr(); + } + + /** + * @brief Create a DataElement (leaf) and add it to the item's dataTree. + * + * @param item item to add the element to + * @param pconnector pointer to the leaf's record connector + * @param elementPath full path to the element + */ + static void + addElementToTree(ItemUaSdk *item, RecordConnector *pconnector, const std::list &elementPath); + + virtual void show(const int level, const unsigned int indent) const override; + + virtual void setIncomingData(const UaVariant &value, + ProcessReason reason, + const std::string *timefrom = nullptr, + const UaNodeId *typeId = nullptr) override; + virtual void setIncomingEvent(ProcessReason reason) override; + virtual void setState(const ConnectionStatus state) override; + virtual const UaVariant &getOutgoingData() override; + virtual void clearOutgoingData() override; + virtual void requestRecordProcessing(const ProcessReason reason) const override; + virtual int debug() const override; + + /** + * @brief Read incoming data as a scalar epicsInt32. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readScalar(epicsInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readScalar(epicsInt32 *value, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as a scalar epicsInt64. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readScalar(epicsInt64*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readScalar(epicsInt64 *value, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as a scalar epicsUInt32. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readScalar(epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readScalar(epicsUInt32 *value, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as a scalar epicsFloat64. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readScalar(epicsFloat64*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readScalar(epicsFloat64 *value, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as classic C string (char[]). + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readScalar(char*,const size_t,dbCommon*,ProcessReason*,epicsUInt32*,char*,const + * epicsUInt32) + */ + + virtual long int readScalar(char *value, + const epicsUInt32 len, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *lenRead = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of epicsInt8. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsInt8*,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsInt8 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of epicsUInt8. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsUInt8*,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsUInt8 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of epicsInt16. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsInt16*,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsInt16 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of epicsUInt16. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsUInt16*,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsUInt16 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of epicsInt32. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsInt32*,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsInt32 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of epicsUInt32. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsUInt32*,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsUInt32 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of epicsInt64. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsInt64*,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsInt64 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of epicsUInt64. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsUInt64*,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsUInt64 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of epicsFloat32. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsFloat32*,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsFloat32 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of epicsFloat64. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsFloat64*,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsFloat64 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of EPICS String (char[40]). + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(char*,const epicsUInt32,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(char *value, + epicsUInt32 len, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Write outgoing scalar epicsInt32 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeScalar(const epicsInt32&,dbCommon*) + */ + virtual long int writeScalar(const epicsInt32 &value, dbCommon *prec) override; + + /** + * @brief Write outgoing scalar epicsInt64 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeScalar(const epicsInt64&,dbCommon*) + */ + virtual long int writeScalar(const epicsInt64 &value, dbCommon *prec) override; + + /** + * @brief Write outgoing scalar epicsUInt32 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeScalar(const epicsUInt32&,dbCommon*) + */ + virtual long int writeScalar(const epicsUInt32 &value, dbCommon *prec) override; + + /** + * @brief Write outgoing scalar epicsFloat64 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeScalar(const epicsFloat64&,dbCommon*) + */ + virtual long int writeScalar(const epicsFloat64 &value, dbCommon *prec) override; + + /** + * @brief Write outgoing classic C string (char[]) data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeScalar(const char*,const epicsUInt32,dbCommon*) + */ + virtual long int writeScalar(const char *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsInt8 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsInt8*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsInt8 *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsUInt8 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsUInt8*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsUInt8 *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsInt16 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsInt16*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsInt16 *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsUInt16 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsUInt16*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsUInt16 *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsInt32 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsInt32*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsInt32 *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsUInt32 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsUInt32*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsUInt32 *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsInt64 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsInt64*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsInt64 *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsUInt64 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsUInt64*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsUInt64 *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsFloat32 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsFloat32*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsFloat32 *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsFloat64 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsFloat64*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsFloat64 *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of EPICS String (char[MAX_STRING_SIZE]) data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const char*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const char *value, epicsUInt32 len, const epicsUInt32 num, dbCommon *prec) override; + +private: + virtual bool isDirty () const override { return isdirty; } + virtual void markAsDirty () override + { + isdirty = true; + pitem->markAsDirty(); + } + + void dbgWriteScalar() const; + void dbgReadScalar(const UpdateUaSdk *upd, const std::string &targetTypeName, const size_t targetSize = 0) const; + void dbgReadArray(const UpdateUaSdk *upd, const epicsUInt32 targetSize, const std::string &targetTypeName) const; + void checkWriteArray(OpcUa_BuiltInType expectedType, const std::string &targetTypeName) const; + void dbgWriteArray(const epicsUInt32 targetSize, const std::string &targetTypeName) const; + + const epicsTime &getIncomingTimeStamp() const; + OpcUa_StatusCode getIncomingReadStatus() const; + + OpcUa_StatusCode UaVariant_to(const UaVariant &variant, OpcUa_Int32 &value); + OpcUa_StatusCode UaVariant_to(const UaVariant &variant, OpcUa_UInt32 &value); + OpcUa_StatusCode UaVariant_to(const UaVariant &variant, OpcUa_Int64 &value); + OpcUa_StatusCode UaVariant_to(const UaVariant &variant, OpcUa_Double &value); + + void UaVariant_set(UaVariant &variant, UaBooleanArray &value); + void UaVariant_set(UaVariant &variant, UaSByteArray &value); + void UaVariant_set(UaVariant &variant, UaByteArray &value); + void UaVariant_set(UaVariant &variant, UaInt16Array &value); + void UaVariant_set(UaVariant &variant, UaUInt16Array &value); + void UaVariant_set(UaVariant &variant, UaInt32Array &value); + void UaVariant_set(UaVariant &variant, UaUInt32Array &value); + void UaVariant_set(UaVariant &variant, UaInt64Array &value); + void UaVariant_set(UaVariant &variant, UaUInt64Array &value); + void UaVariant_set(UaVariant &variant, UaFloatArray &value); + void UaVariant_set(UaVariant &variant, UaDoubleArray &value); + void UaVariant_set(UaVariant &variant, UaStringArray &value); + void UaVariant_set(UaVariant &variant, UaXmlElementArray &value); + void UaVariant_set(UaVariant &variant, UaLocalizedTextArray &value); + void UaVariant_set(UaVariant &variant, UaQualifiedNameArray &value); + + // Read scalar value as templated function on EPICS type and OPC UA type + // value == nullptr is allowed and leads to the value being dropped (ignored), + // including the extended status + template + long readScalar (ET *value, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) + { + long ret = 0; + + if (incomingQueue.empty()) { + errlogPrintf("%s: incoming data queue empty\n", prec->name); + if (nextReason) + *nextReason = ProcessReason::none; + return 1; + } + + ProcessReason nReason; + std::shared_ptr upd = incomingQueue.popUpdate(&nReason); + dbgReadScalar(upd.get(), epicsTypeString(*value)); + + switch (upd->getType()) { + case ProcessReason::readFailure: + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::connectionLoss: + (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::incomingData: + case ProcessReason::readComplete: { + if (value) { + OpcUa_StatusCode stat = upd->getStatus(); + if (OpcUa_IsNotGood(stat)) { + // No valid OPC UA value + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + // Valid OPC UA value, so try to convert + OT v; + if (OpcUa_IsNotGood(UaVariant_to(upd->getData(), v))) { + errlogPrintf( + "%s : incoming data (%s) out-of-bounds\n", prec->name, upd->getData().toString().toUtf8()); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + } else { + if (OpcUa_IsUncertain(stat)) { + (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); + } + *value = v; + prec->udf = false; + } + } + if (statusCode) + *statusCode = stat; + if (statusText) { + strncpy(statusText, UaStatus(stat).toString().toUtf8(), statusTextLen); + statusText[statusTextLen - 1] = '\0'; + } + } + break; + } + default: + break; + } + + prec->time = upd->getTimeStamp(); + if (nextReason) + *nextReason = nReason; + return ret; + } + + // Read array value as templated function on EPICS type and OPC UA type + // (latter *must match* OPC UA type enum argument) + // CAVEAT: changes must also be reflected in specializations (in DataElementUaSdk.cpp) + template + long readArray (ET *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + OpcUa_BuiltInType expectedType, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) + { + long ret = 0; + epicsUInt32 elemsWritten = 0; + + if (incomingQueue.empty()) { + errlogPrintf("%s : incoming data queue empty\n", prec->name); + *numRead = 0; + return 1; + } + + ProcessReason nReason; + std::shared_ptr upd = incomingQueue.popUpdate(&nReason); + dbgReadArray(upd.get(), num, epicsTypeString(*value)); + + switch (upd->getType()) { + case ProcessReason::readFailure: + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::connectionLoss: + (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::incomingData: + case ProcessReason::readComplete: { + if (num && value) { + OpcUa_StatusCode stat = upd->getStatus(); + if (OpcUa_IsNotGood(stat)) { + // No valid OPC UA value + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + // Valid OPC UA value, so try to convert + UaVariant &variant = upd->getData(); + if (!variant.isArray()) { + errlogPrintf("%s : incoming data is not an array\n", prec->name); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else if (variant.type() != expectedType) { + errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", + prec->name, + variantTypeString(variant.type()), + epicsTypeString(*value)); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + if (OpcUa_IsUncertain(stat)) { + (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); + } + elemsWritten = variant.arraySize(); + if (elemsWritten > num) + elemsWritten = num; + memcpy(value, + static_cast(variant)->Value.Array.Value.Array, + sizeof(ET) * elemsWritten); + } + } + if (statusCode) + *statusCode = stat; + if (statusText) { + strncpy(statusText, UaStatus(stat).toString().toUtf8(), statusTextLen); + statusText[statusTextLen - 1] = '\0'; + } + } + break; + } + default: + break; + } + + prec->time = upd->getTimeStamp(); + if (nextReason) + *nextReason = nReason; + if (num && value) + *numRead = elemsWritten; + return ret; + } + + // Read array value for EPICS String / OpcUa_String + long readArray(char *value, + epicsUInt32 len, + const epicsUInt32 num, + epicsUInt32 *numRead, + OpcUa_BuiltInType expectedType, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen); + + // Write scalar value as templated function on EPICS type + template long writeScalar (const ET &value, dbCommon *prec) + { + long ret = 1; + + switch (incomingData.type()) { + case OpcUaType_Boolean: { // Scope of Guard G + Guard G(outgoingLock); + outgoingData.setBoolean(value != 0); + markAsDirty(); + ret = 0; + break; + } + case OpcUaType_Byte: + if (isWithinRange(value)) { + Guard G(outgoingLock); + outgoingData.setByte(static_cast(value)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_SByte: + if (isWithinRange(value)) { + Guard G(outgoingLock); + outgoingData.setSByte(static_cast(value)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_UInt16: + if (isWithinRange(value)) { + Guard G(outgoingLock); + outgoingData.setUInt16(static_cast(value)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_Int16: + if (isWithinRange(value)) { + Guard G(outgoingLock); + outgoingData.setInt16(static_cast(value)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_UInt32: + if (isWithinRange(value)) { + Guard G(outgoingLock); + outgoingData.setUInt32(static_cast(value)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_Int32: // may be an enum + if (isWithinRange(value) + && (!enumChoices || enumChoices->find(static_cast(value)) != enumChoices->end())) { + Guard G(outgoingLock); + outgoingData.setInt32(static_cast(value)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_UInt64: + if (isWithinRange(value)) { + Guard G(outgoingLock); + outgoingData.setUInt64(static_cast(value)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_Int64: + if (isWithinRange(value)) { + Guard G(outgoingLock); + outgoingData.setInt64(static_cast(value)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_Float: + if (isWithinRange(value)) { + Guard G(outgoingLock); + outgoingData.setFloat(static_cast(value)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_Double: + if (isWithinRange(value)) { + Guard G(outgoingLock); + outgoingData.setDouble(static_cast(value)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_String: { // Scope of Guard G + Guard G(outgoingLock); + outgoingData.setString(static_cast(std::to_string(value).c_str())); + markAsDirty(); + break; + } + default: + errlogPrintf("%s : unsupported conversion from %s to %s for outgoing data\n", + prec->name, + epicsTypeString(value), + variantTypeString(incomingData.type())); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + } + if (ret != 0) { + errlogPrintf("%s : value out of range\n", prec->name); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + } + + if (ret == 0) + dbgWriteScalar(); + return ret; + } + + // Write array value for EPICS String / OpcUa_String + long + writeArray(const char *value, epicsUInt32 len, const epicsUInt32 num, OpcUa_BuiltInType targetType, dbCommon *prec); + + // Write array value as templated function on EPICS type, OPC UA container and simple (element) types + // (latter *must match* OPC UA type enum argument) + // CAVEAT: changes must also be reflected in the specialization (in DataElementUaSdk.cpp) + template + long writeArray (const ET *value, const epicsUInt32 num, OpcUa_BuiltInType targetType, dbCommon *prec) + { + long ret = 0; + + if (!incomingData.isArray()) { + errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else if (incomingData.type() != targetType) { + errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", + prec->name, + variantTypeString(incomingData.type()), + variantTypeString(targetType), + epicsTypeString(*value)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else { + // The array methods must cast away the constness of their value argument + // as the UA SDK API uses non-const parameters + ST *val = const_cast(reinterpret_cast(value)); + CT arr(static_cast(num), val); + { // Scope of Guard G + Guard G(outgoingLock); + UaVariant_set(outgoingData, arr); + markAsDirty(); + } + + dbgWriteArray(num, epicsTypeString(*value)); + } + return ret; + } + + UpdateQueue incomingQueue; /**< queue of incoming values */ +}; + +} // namespace DevOpcua + +#endif // DEVOPCUA_DATAELEMENTUASDKLEAF_H diff --git a/devOpcuaSup/UaSdk/DataElementUaSdkNode.cpp b/devOpcuaSup/UaSdk/DataElementUaSdkNode.cpp new file mode 100644 index 00000000..67ff3fb1 --- /dev/null +++ b/devOpcuaSup/UaSdk/DataElementUaSdkNode.cpp @@ -0,0 +1,496 @@ +/*************************************************************************\ +* Copyright (c) 2018-2026 ITER Organization. +* This module is distributed subject to a Software License Agreement found +* in file LICENSE that is included with this distribution. +\*************************************************************************/ + +/* + * Author: Ralph Lange + * + * based on prototype work by Bernhard Kuner + * and example code from the Unified Automation C++ Based OPC UA Client SDK + */ + +#include "DataElementUaSdkNode.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "ItemUaSdk.h" +#include "RecordConnector.h" + +namespace DevOpcua { + +DataElementUaSdkNode::DataElementUaSdkNode (const std::string &name, class ItemUaSdk *pitem) + : DataElementUaSdk(name, pitem) + , timesrc(-1) + , mapped(false) +{ + pitem->dataTreeNoOfNodes++; +} + +DataElementUaSdkNode::~DataElementUaSdkNode() +{ + pitem->dataTreeNoOfNodes--; +} + +void +DataElementUaSdkNode::addChild (std::weak_ptr elem) +{ + elements.push_back(elem); +} + +std::shared_ptr +DataElementUaSdkNode::findChild (const std::string &name) const +{ + for (const auto &it : elements) + if (auto pit = it.lock()) + if (pit->name == name) + return pit; + return std::shared_ptr(); +} + +void +DataElementUaSdkNode::show (const int level, const unsigned int indent) const +{ + std::string ind(indent * 2, ' '); + std::cout << ind; + std::cout << "node=" << name << " children=" << elements.size() << " mapped=" << (mapped ? "y" : "n") << "\n"; + for (const auto &it : elements) { + if (auto pelem = it.lock()) { + pelem->show(level, indent + 1); + } + } +} + +void +DataElementUaSdkNode::createMap (const UaStructureDefinition &definition, const std::string *timefrom) +{ + if (debug() >= 5) + std::cout << " ** creating index-to-element map for child elements" << std::endl; + + if (timefrom) { + for (int i = 0; i < definition.childrenCount(); i++) { + if (*timefrom == definition.child(i).name().toUtf8()) { + timesrc = i; + } + } + OpcUa_BuiltInType t = definition.child(timesrc).valueType(); + if (timesrc == -1) { + errlogPrintf("%s: timestamp element %s not found - using source timestamp\n", + pitem->recConnector->getRecordName(), + timefrom->c_str()); + } else if (t != OpcUaType_DateTime) { + errlogPrintf("%s: timestamp element %s has invalid type %s - using " + "source timestamp\n", + pitem->recConnector->getRecordName(), + timefrom->c_str(), + variantTypeString(t)); + timesrc = -1; + } + } + for (const auto &it : elements) { + auto pelem = it.lock(); + for (int i = 0; i < definition.childrenCount(); i++) { + if (pelem->name == definition.child(i).name().toUtf8()) + elementMap.insert({i, it}); + } + } + if (debug() >= 5) + std::cout << " ** " << elementMap.size() << "/" << elements.size() << " child elements mapped to a " + << "structure of " << definition.childrenCount() << " elements" << std::endl; + mapped = true; +} + +void +DataElementUaSdkNode::createMap (const UaLocalizedText &) +{ + for (const auto &it : elements) { + auto pelem = it.lock(); + if (pelem->name == "locale") + elementMap.insert({0, it}); + else if (pelem->name == "text") + elementMap.insert({1, it}); + else + errlogPrintf("Item %s %s element %s not found\n", + pitem->nodeid->toString().toUtf8(), + name.c_str(), + pelem->name.c_str()); + } + mapped = true; +} + +void +DataElementUaSdkNode::createMap (const UaQualifiedName &) +{ + for (const auto &it : elements) { + auto pelem = it.lock(); + if (pelem->name == "namespaceIndex") + elementMap.insert({0, it}); + else if (pelem->name == "name") + elementMap.insert({1, it}); + else + errlogPrintf("Item %s %s element %s not found\n", + pitem->nodeid->toString().toUtf8(), + name.c_str(), + pelem->name.c_str()); + } + mapped = true; +} + +void +DataElementUaSdkNode::setIncomingData (const UaVariant &value, + ProcessReason reason, + const std::string *timefrom, + const UaNodeId *typeId) +{ + incomingData = value; + + if (debug() >= 5) + std::cout << "Element " << name << " splitting structured data to " << elements.size() << " child elements" + << std::endl; + + switch (value.type()) { + case OpcUaType_ExtensionObject: { + UaExtensionObject extensionObject; + value.toExtensionObject(extensionObject); + if (extensionObject.encoding() == UaExtensionObject::EncodeableObject) + extensionObject.changeEncoding(UaExtensionObject::Binary); + + UaStructureDefinition definition = pitem->structureDefinition(extensionObject.encodingTypeId()); + if (definition.isNull()) { + errlogPrintf("Cannot get a structure definition for item %s element %s (dataTypeId %s " + "encodingTypeId %s) - check " + "access to type " + "dictionary\n", + pitem->nodeid->toString().toUtf8(), + name.c_str(), + extensionObject.dataTypeId().toString().toUtf8(), + extensionObject.encodingTypeId().toString().toUtf8()); + return; + } + + if (!mapped) + createMap(definition, timefrom); + + if (timefrom) { + if (timesrc >= 0) + pitem->tsData + = epicsTimeFromUaVariant(UaGenericStructureValue(extensionObject, definition).value(timesrc)); + else + pitem->tsData = pitem->tsSource; + } + + for (const auto &it : elementMap) { + auto pelem = it.second.lock(); + OpcUa_StatusCode stat; + if (definition.isUnion()) { + UaGenericUnionValue genericValue(extensionObject, definition); + int index = genericValue.switchValue() - 1; + if (it.first == index) { + pelem->setIncomingData(genericValue.value(), reason); + stat = OpcUa_Good; + } else { + stat = OpcUa_BadNoData; + } + } else { + const UaVariant memberValue + = UaGenericStructureValue(extensionObject, definition).value(it.first, &stat); + if (stat == OpcUa_Good) { + pelem->setIncomingData(memberValue, reason); + } + } + if (stat == OpcUa_BadNoData) { + const UaStructureField &field = definition.child(it.first); + OpcUa_Variant fakeValue; + OpcUa_Variant_Initialize(&fakeValue); + fakeValue.Datatype = field.valueType(); + fakeValue.ArrayType = field.valueRank() != 0; + if (debug()) + std::cerr << pitem->recConnector->getRecordName() << " element " << pelem->name + << (definition.isUnion() ? " not taken choice " : " absent optional ") + << variantTypeString((OpcUa_BuiltInType) fakeValue.Datatype) + << (fakeValue.ArrayType ? " array" : " scalar") << std::endl; + pelem->setIncomingData(fakeValue, ProcessReason::readFailure); + } + } + break; + } + case OpcUaType_LocalizedText: { + UaLocalizedText localizedText; + value.toLocalizedText(localizedText); + if (!mapped) + createMap(localizedText); + + for (const auto &it : elementMap) { + auto pelem = it.second.lock(); + UaVariant memberValue; + switch (it.first) { + case 0: + memberValue.setString(localizedText.locale()); + break; + case 1: + memberValue.setString(localizedText.text()); + break; + } + pelem->setIncomingData(memberValue, reason); + } + break; + } + case OpcUaType_QualifiedName: { + UaQualifiedName qualifiedName; + value.toQualifiedName(qualifiedName); + if (!mapped) + createMap(qualifiedName); + + for (const auto &it : elementMap) { + auto pelem = it.second.lock(); + UaVariant memberValue; + switch (it.first) { + case 0: + memberValue.setUInt16(qualifiedName.namespaceIndex()); + break; + case 1: + memberValue.setString(qualifiedName.name()); + break; + } + pelem->setIncomingData(memberValue, reason); + } + break; + } + default: + errlogPrintf("%s: %s is no structured data but a %s\n", + pitem->recConnector->getRecordName(), + name.c_str(), + variantTypeString(value.type())); + } +} + +void +DataElementUaSdkNode::setIncomingEvent (ProcessReason reason) +{ + for (const auto &it : elements) { + auto pelem = it.lock(); + pelem->setIncomingEvent(reason); + } + if (reason == ProcessReason::connectionLoss) { + elementMap.clear(); + timesrc = -1; + mapped = false; + } +} + +void +DataElementUaSdkNode::setState (const ConnectionStatus state) +{ + for (const auto &it : elements) { + auto pelem = it.lock(); + pelem->setState(state); + } +} + +const UaVariant & +DataElementUaSdkNode::getOutgoingData () +{ + if (debug() >= 4) + std::cout << "Element " << name << " updating structured data from " << elements.size() << " child elements" + << std::endl; + + outgoingData = incomingData; + bool isdirty = false; + switch (outgoingData.type()) { + case OpcUaType_ExtensionObject: { + UaExtensionObject extensionObject; + outgoingData.toExtensionObject(extensionObject); + if (extensionObject.encoding() == UaExtensionObject::EncodeableObject) + extensionObject.changeEncoding(UaExtensionObject::Binary); + + UaStructureDefinition definition = pitem->structureDefinition(extensionObject.encodingTypeId()); + if (definition.isNull()) { + errlogPrintf("Cannot get a structure definition for extensionObject with dataTypeID %s " + "/ encodingTypeID %s - " + "check access to type " + "dictionary\n", + extensionObject.dataTypeId().toString().toUtf8(), + extensionObject.encodingTypeId().toString().toUtf8()); + return outgoingData; + } + + if (!mapped) + createMap(definition); + + if (definition.isUnion()) { + UaGenericUnionValue genericUnion(extensionObject, definition); + for (const auto &it : elementMap) { + auto pelem = it.second.lock(); + bool updated = false; + { + Guard G(pelem->outgoingLock); + if (pelem->isDirty()) { + genericUnion.setValue(it.first + 1, pelem->getOutgoingData()); + pelem->isdirty = false; + isdirty = true; + updated = true; + } + } + if (debug() >= 4) + std::cout << "Data from child element " << pelem->name + << (updated ? " inserted into union" : " ignored (not dirty)") << std::endl; + } + if (isdirty) + genericUnion.toExtensionObject(extensionObject); + + } else { + UaGenericStructureValue genericStruct(extensionObject, definition); + for (const auto &it : elementMap) { + auto pelem = it.second.lock(); + bool updated = false; + { + Guard G(pelem->outgoingLock); + if (pelem->isDirty()) { + genericStruct.setField(it.first, pelem->getOutgoingData()); + pelem->isdirty = false; + isdirty = true; + updated = true; + } + } + if (debug() >= 4) + std::cout << "Data from child element " << pelem->name + << (updated ? " inserted into structure" : " ignored (not dirty)") << std::endl; + } + if (isdirty) + genericStruct.toExtensionObject(extensionObject); + } + + if (isdirty) + outgoingData.setExtensionObject(extensionObject, OpcUa_True); + break; + } + case OpcUaType_LocalizedText: { + UaLocalizedText localizedText; + outgoingData.toLocalizedText(localizedText); + if (!mapped) + createMap(localizedText); + + for (const auto &it : elementMap) { + auto pelem = it.second.lock(); + bool updated = false; + { + Guard G(pelem->outgoingLock); + if (pelem->isDirty()) { + switch (it.first) { + case 0: + localizedText.setLocale(pelem->getOutgoingData().toString()); + break; + case 1: + localizedText.setText(pelem->getOutgoingData().toString()); + break; + } + pelem->isdirty = false; + isdirty = true; + updated = true; + } + if (debug() >= 4) + std::cout << "Data from child element " << pelem->name + << (updated ? " inserted into LocalizedText" : " ignored (not dirty)") << std::endl; + } + } + if (isdirty) + outgoingData.setLocalizedText(localizedText); + break; + } + case OpcUaType_QualifiedName: { + UaQualifiedName qualifiedName; + outgoingData.toQualifiedName(qualifiedName); + if (!mapped) + createMap(qualifiedName); + + for (const auto &it : elementMap) { + auto pelem = it.second.lock(); + bool updated = false; + { + Guard G(pelem->outgoingLock); + OpcUa_UInt16 ns; + if (pelem->isDirty()) { + switch (it.first) { + case 0: + if (pelem->getOutgoingData().toUInt16(ns) == OpcUa_Good) + qualifiedName.setNamespaceIndex(ns); + break; + case 1: + qualifiedName.setQualifiedName(pelem->getOutgoingData().toString(), + qualifiedName.namespaceIndex()); + break; + } + pelem->isdirty = false; + isdirty = true; + updated = true; + } + if (debug() >= 4) + std::cout << "Data from child element " << pelem->name + << (updated ? " inserted into QualifiedName" : " ignored (not dirty)") << std::endl; + } + } + if (isdirty) + outgoingData.setQualifiedName(qualifiedName); + break; + } + default: + errlogPrintf("%s: %s is no structured data but a %s\n", + pitem->recConnector->getRecordName(), + name.c_str(), + variantTypeString(outgoingData.type())); + return outgoingData; + } + if (debug() >= 4) { + if (isdirty) + std::cout << "Encoding changed data structure to outgoingData of element " << name << std::endl; + else + std::cout << "Returning unchanged outgoingData of element " << name << std::endl; + } + return outgoingData; +} + +void +DataElementUaSdkNode::clearOutgoingData () +{ + outgoingData.clear(); +} + +void +DataElementUaSdkNode::requestRecordProcessing (const ProcessReason reason) const +{ + for (const auto &it : elementMap) { + auto pelem = it.second.lock(); + pelem->requestRecordProcessing(reason); + } +} + +epicsTime +DataElementUaSdkNode::epicsTimeFromUaVariant (const UaVariant &data) const +{ + UaDateTime dt; + UaStatus s = data.toDateTime(dt); + if (s.isGood()) { + return ItemUaSdk::uaToEpicsTime(dt, 0); + } + return pitem->tsSource; +} + +#define LOG_ERROR_AND_RETURN(method) \ + errlogPrintf("DataElementUaSdkNode::%s called on a node element. This should not happen.\n", #method); \ + return 1; +} // namespace DevOpcua diff --git a/devOpcuaSup/UaSdk/DataElementUaSdkNode.h b/devOpcuaSup/UaSdk/DataElementUaSdkNode.h new file mode 100644 index 00000000..25eb12d4 --- /dev/null +++ b/devOpcuaSup/UaSdk/DataElementUaSdkNode.h @@ -0,0 +1,68 @@ +/*************************************************************************\ +* Copyright (c) 2018-2026 ITER Organization. +* This module is distributed subject to a Software License Agreement found +* in file LICENSE that is included with this distribution. +\*************************************************************************/ + +/* + * Author: Ralph Lange + * + * based on prototype work by Bernhard Kuner + * and example code from the Unified Automation C++ Based OPC UA Client SDK + */ + +#ifndef DEVOPCUA_DATAELEMENTUASDKNODE_H +#define DEVOPCUA_DATAELEMENTUASDKNODE_H + +#include +#include +#include + +#include + +#include "DataElementUaSdk.h" + +namespace DevOpcua { + +class DataElementUaSdkNode : public DataElementUaSdk +{ +public: + DataElementUaSdkNode(const std::string &name, class ItemUaSdk *pitem); + virtual ~DataElementUaSdkNode () override; + + /* ElementTree node interface methods */ + virtual bool isLeaf() const override { return false; } + virtual void addChild(std::weak_ptr elem) override; + virtual std::shared_ptr findChild(const std::string &name) const override; + + virtual void show(const int level, const unsigned int indent) const override; + + virtual void setIncomingData(const UaVariant &value, + ProcessReason reason, + const std::string *timefrom = nullptr, + const UaNodeId *typeId = nullptr) override; + virtual void setIncomingEvent(ProcessReason reason) override; + virtual void setState(const ConnectionStatus state) override; + virtual const UaVariant &getOutgoingData() override; + virtual void clearOutgoingData() override; + virtual void requestRecordProcessing(const ProcessReason reason) const override; + +private: + virtual bool isDirty() const override { return true; } + virtual void markAsDirty() override {} + + void createMap(const UaStructureDefinition& definition, const std::string *timefrom = nullptr); + void createMap(const UaLocalizedText&); + void createMap(const UaQualifiedName&); + + epicsTime epicsTimeFromUaVariant(const UaVariant &data) const; + + std::vector> elements; + std::unordered_map> elementMap; + int timesrc; + bool mapped; +}; + +} + +#endif // DEVOPCUA_DATAELEMENTUASDKNODE_H diff --git a/devOpcuaSup/UaSdk/ItemUaSdk.cpp b/devOpcuaSup/UaSdk/ItemUaSdk.cpp index a029fcb4..f3538c5d 100644 --- a/devOpcuaSup/UaSdk/ItemUaSdk.cpp +++ b/devOpcuaSup/UaSdk/ItemUaSdk.cpp @@ -1,5 +1,5 @@ /*************************************************************************\ -* Copyright (c) 2018-2021 ITER Organization. +* Copyright (c) 2018-2026 ITER Organization. * This module is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. \*************************************************************************/ @@ -11,41 +11,44 @@ * and example code from the Unified Automation C++ Based OPC UA Client SDK */ +#include "ItemUaSdk.h" + #include #include +#include #include #include -#include -#include "devOpcua.h" +#include "DataElementUaSdk.h" #include "RecordConnector.h" -#include "opcuaItemRecord.h" -#include "ItemUaSdk.h" -#include "SubscriptionUaSdk.h" #include "SessionUaSdk.h" -#include "DataElementUaSdk.h" +#include "SubscriptionUaSdk.h" +#include "devOpcua.h" +#include "opcuaItemRecord.h" -namespace DevOpcua { +namespace DevOpcua +{ using namespace UaClientSdk; /* Specific implementation of Item's factory method */ Item * -Item::newItem(const linkInfo &info) +Item::newItem (const linkInfo &info) { return new ItemUaSdk(info); } -ItemUaSdk::ItemUaSdk(const linkInfo &info) +ItemUaSdk::ItemUaSdk (const linkInfo &info) : Item(info) , subscription(nullptr) , session(nullptr) , registered(false) , revisedSamplingInterval(0.0) , revisedQueueSize(0) - , dataTree(this) , dataTreeDirty(false) + , dataTreeNoOfNodes(0) + , dataTreeNoOfLeafs(0) , lastStatus(OpcUa_BadServerNotConnected) , lastReason(ProcessReason::connectionLoss) { @@ -78,7 +81,7 @@ ItemUaSdk::rebuildNodeId () } void -ItemUaSdk::requestWriteIfDirty() +ItemUaSdk::requestWriteIfDirty () { Guard G(dataTreeWriteLock); if (dataTreeDirty) @@ -100,21 +103,21 @@ ItemUaSdk::show (int level) const std::cout << ";s=" << linkinfo.identifierString; std::cout << " record=" << recConnector->getRecordName() << " state=" << connectionStatusString(recConnector->state()) - << " status=" << UaStatus(lastStatus).toString().toUtf8() - << " dataDirty=" << (dataTreeDirty ? "y" : "n") + << " status=" << UaStatus(lastStatus).toString().toUtf8() << " dataDirty=" << (dataTreeDirty ? "y" : "n") << " context=" << linkinfo.subscription << "@" << session->getName() << " sampling=" << revisedSamplingInterval << "(" << linkinfo.samplingInterval << ")" - << " qsize=" << revisedQueueSize << "(" << linkinfo.queueSize << ")" - << " cqsize=" << linkinfo.clientQueueSize - << " discard=" << (linkinfo.discardOldest ? "old" : "new") + << " deadband=" << linkinfo.deadband << " qsize=" << revisedQueueSize << "(" << linkinfo.queueSize << ")" + << " cqsize=" << linkinfo.clientQueueSize << " discard=" << (linkinfo.discardOldest ? "old" : "new") << " timestamp=" << linkOptionTimestampString(linkinfo.timestamp); if (linkinfo.timestamp == LinkOptionTimestamp::data) std::cout << "@" << linkinfo.timestampElement; - std::cout << " bini=" << linkOptionBiniString(linkinfo.bini) - << " output=" << (linkinfo.isOutput ? "y" : "n") + std::cout << " bini=" << linkOptionBiniString(linkinfo.bini) << " output=" << (linkinfo.isOutput ? "y" : "n") << " monitor=" << (linkinfo.monitor ? "y" : "n") << " registered=" << (registered ? nodeid->toString().toUtf8() : "-") << "(" - << (linkinfo.registerNode ? "y" : "n") << ")" << std::endl; + << (linkinfo.registerNode ? "y" : "n") << ")"; + if (!(dataTreeNoOfNodes == 0 && dataTreeNoOfLeafs == 1)) + std::cout << " dataNodes=" << dataTreeNoOfNodes << " dataLeafs=" << dataTreeNoOfLeafs; + std::cout << std::endl; if (level >= 1) { if (auto re = dataTree.root().lock()) { @@ -124,13 +127,14 @@ ItemUaSdk::show (int level) const } } -int ItemUaSdk::debug() const +int +ItemUaSdk::debug () const { return recConnector->debug(); } void -ItemUaSdk::copyAndClearOutgoingData(_OpcUa_WriteValue &wvalue) +ItemUaSdk::copyAndClearOutgoingData (_OpcUa_WriteValue &wvalue) { Guard G(dataTreeWriteLock); if (auto pd = dataTree.root().lock()) { @@ -145,12 +149,12 @@ ItemUaSdk::uaToEpicsTime (const UaDateTime &dt, const OpcUa_UInt16 pico10) { epicsTimeStamp ts; ts.secPastEpoch = static_cast(dt.toTime_t()) - POSIX_TIME_AT_EPICS_EPOCH; - ts.nsec = static_cast(static_cast(dt) % UA_SECS_TO_100NS) * 100 + pico10 / 100; + ts.nsec = static_cast(static_cast(dt) % UA_SECS_TO_100NS) * 100 + pico10 / 100; return epicsTime(ts); } void -ItemUaSdk::setIncomingData(const OpcUa_DataValue &value, ProcessReason reason) +ItemUaSdk::setIncomingData (const OpcUa_DataValue &value, ProcessReason reason, const UaNodeId *typeId) { tsClient = epicsTime::getCurrent(); if (OpcUa_IsNotBad(value.StatusCode)) { @@ -177,13 +181,12 @@ ItemUaSdk::setIncomingData(const OpcUa_DataValue &value, ProcessReason reason) const std::string *timefrom = nullptr; if (linkinfo.timestamp == LinkOptionTimestamp::data && linkinfo.timestampElement.length()) timefrom = &linkinfo.timestampElement; - pd->setIncomingData(value.Value, reason, timefrom); + pd->setIncomingData(value.Value, reason, timefrom, typeId); } if (linkinfo.isItemRecord) { - if (recConnector->state() == ConnectionStatus::initialRead - && reason == ProcessReason::readComplete - && recConnector->bini() == LinkOptionBini::write) { + if (recConnector->state() == ConnectionStatus::initialRead && reason == ProcessReason::readComplete + && recConnector->bini() == LinkOptionBini::write) { recConnector->setState(ConnectionStatus::initialWrite); recConnector->requestRecordProcessing(ProcessReason::writeRequest); } else { @@ -193,7 +196,7 @@ ItemUaSdk::setIncomingData(const OpcUa_DataValue &value, ProcessReason reason) } void -ItemUaSdk::setIncomingEvent(const ProcessReason reason) +ItemUaSdk::setIncomingEvent (const ProcessReason reason) { tsClient = epicsTime::getCurrent(); setReason(reason); @@ -214,15 +217,17 @@ ItemUaSdk::setIncomingEvent(const ProcessReason reason) } void -ItemUaSdk::setState(const ConnectionStatus state) +ItemUaSdk::setState (const ConnectionStatus state) { if (auto pd = dataTree.root().lock()) { pd->setState(state); } + if (linkinfo.isItemRecord) + recConnector->setState(state); } void -ItemUaSdk::markAsDirty() +ItemUaSdk::markAsDirty () { if (recConnector->plinkinfo->isItemRecord) { Guard G(dataTreeWriteLock); @@ -235,12 +240,12 @@ ItemUaSdk::markAsDirty() } void -ItemUaSdk::getStatus(epicsUInt32 *code, char *text, const epicsUInt32 len, epicsTimeStamp *ts) +ItemUaSdk::getStatus (epicsUInt32 *code, char *text, const epicsUInt32 len, epicsTimeStamp *ts) { *code = lastStatus.code(); if (text && len) { strncpy(text, lastStatus.toString().toUtf8(), len); - text[len-1] = '\0'; + text[len - 1] = '\0'; } if (ts && recConnector) { diff --git a/devOpcuaSup/UaSdk/ItemUaSdk.h b/devOpcuaSup/UaSdk/ItemUaSdk.h index 47a7a75b..f7eeb206 100644 --- a/devOpcuaSup/UaSdk/ItemUaSdk.h +++ b/devOpcuaSup/UaSdk/ItemUaSdk.h @@ -24,7 +24,6 @@ #include #include "Item.h" -#include "opcuaItemRecord.h" #include "devOpcua.h" #include "ElementTree.h" #include "SessionUaSdk.h" @@ -35,6 +34,7 @@ using namespace UaClientSdk; class SubscriptionUaSdk; class DataElementUaSdk; +class DataElementUaSdkNode; struct linkInfo; /** @@ -45,6 +45,8 @@ struct linkInfo; class ItemUaSdk : public Item { friend class DataElementUaSdk; + friend class DataElementUaSdkNode; + friend class DataElementUaSdkLeaf; public: /** @@ -168,8 +170,9 @@ class ItemUaSdk : public Item * * @param value new value for this data element * @param reason reason for this value update + * @param typeId data type id of the item */ - void setIncomingData(const OpcUa_DataValue &value, ProcessReason reason); + void setIncomingData(const OpcUa_DataValue &value, ProcessReason reason, const UaNodeId *typeId = nullptr); /** * @brief Push an incoming event down the root element. @@ -221,9 +224,11 @@ class ItemUaSdk : public Item bool registered; /**< flag for registration status */ OpcUa_Double revisedSamplingInterval; /**< server-revised sampling interval */ OpcUa_UInt32 revisedQueueSize; /**< server-revised queue size */ - ElementTree dataTree; /**< data element tree */ - epicsMutex dataTreeWriteLock; /**< lock for dirty flag */ + ElementTree dataTree; /**< data element tree */ + epicsMutex dataTreeWriteLock; /**< lock for dirty flag */ bool dataTreeDirty; /**< true if any element has been modified */ + unsigned int dataTreeNoOfNodes; /**< number of nodes */ + unsigned int dataTreeNoOfLeafs; /**< number of leafs */ UaStatusCode lastStatus; /**< status code of most recent service */ ProcessReason lastReason; /**< most recent processing reason */ epicsTime tsClient; /**< client (local) time stamp */ diff --git a/devOpcuaSup/UaSdk/Makefile.config b/devOpcuaSup/UaSdk/Makefile.config index 5573499d..36b0743d 100644 --- a/devOpcuaSup/UaSdk/Makefile.config +++ b/devOpcuaSup/UaSdk/Makefile.config @@ -27,7 +27,8 @@ opcua_SRCS += SessionUaSdk.cpp opcua_SRCS += Subscription.cpp opcua_SRCS += SubscriptionUaSdk.cpp opcua_SRCS += ItemUaSdk.cpp -opcua_SRCS += DataElementUaSdk.cpp +opcua_SRCS += DataElementUaSdkNode.cpp +opcua_SRCS += DataElementUaSdkLeaf.cpp DBD_INSTALLS += opcua.dbd diff --git a/devOpcuaSup/UaSdk/README.md b/devOpcuaSup/UaSdk/README.md index 671691c7..9b72baa0 100644 --- a/devOpcuaSup/UaSdk/README.md +++ b/devOpcuaSup/UaSdk/README.md @@ -1,141 +1,6 @@ -# Unified Automation C++ Based OPC UA Client SDK +# Using the Unified Automation C++ Based OPC UA Client SDK -## Status - -:warning: -This module is stable, with some expected development to be happening. -Please contact the author [Ralph Lange](mailto:ralph.lange@gmx.de) for details. -:warning: - -## Prerequisites - -* Unified Automation C++ Based [OPC UA Client SDK][unified.sdk] - (1.5/1.6/1.7 are supported, as well as their evaluation bundles). - -* Windows and Linux builds are supported. - -* Using the evaluation binary bundles by Unified Automation (free download) is supported. - However, the EVAL bundle contains a shared library (DLL), namely the stack component, see below, so only shared builds of this Device Support will work. - -### Linux - -* For OPC UA security support (authentication/encryption), you need `libcrypto` on your system - both when compiling the SDK and when generating any binaries (IOCs). - The name of the package you have to install depends on the Linux distribution: `openssl-devel` on RedHat/CentOS/Fedora, `libssl-dev` on Debian/Ubuntu. - Use the `CONFIG_SITE.local` file in the module where the binary is created to set this option. - -* The OPC UA Client SDK sets `BUILD_SHARED_LIBS=OFF` as default. - To create shared SDK libraries, build the SDK using - - ```Shell - ./buildSdk.sh -s ON - ``` - In many versions of the SDK, the `buildSdk.sh` build script does not apply the `-s ON` setting the the stack component. To fix this and create a complete shared library set of the SDK, apply the following patch (shown for 1.5.5): - - ```Diff - --- buildSdk.sh - +++ buildSdk.sh - @@ -95,7 +95,7 @@ - cd $UASDKDIR/build$config || { echo "cd $UASDKDIR/build$config failed."; exit 1; } - # create the Makefile using CMake - # Just create only the SDK Makefiles - - cmake "$TOOLCHAIN" "$OPTION" -DBUILD_EXAMPLES=off -DBUILD_UACLIENTCPP_APP=off -DBUILD_UASERVERCPP_APP=off -DENABLE_GCC_FORTIFY_SOURCE=off -DCMAKE_BUILD_TYPE=$config -DBUILD_SHARED_LIBS=$BUILD_SHARED_LIBS -DCMAKE_INSTALL_PREFIX=$CMAKE_INSTALL_PREFIX $UASDKDIR - + cmake "$TOOLCHAIN" "$OPTION" -DBUILD_EXAMPLES=off -DBUILD_UACLIENTCPP_APP=off -DBUILD_UASERVERCPP_APP=off -DENABLE_GCC_FORTIFY_SOURCE=off -DCMAKE_BUILD_TYPE=$config -DBUILD_SHARED_LIBS=$BUILD_SHARED_LIBS -DBUILD_SHARED_STACK=$BUILD_SHARED_LIBS -DCMAKE_INSTALL_PREFIX=$CMAKE_INSTALL_PREFIX $UASDKDIR - # build - make "$JOBS" || { echo "make failed."; exit 1; } - # install - ``` - -### Windows - -* Binary libraries (DLLs and headers) of matching versions for openssl and libxml2 are provided under the `third-party` directory of the SDK bundle. - -## Building the device support module - -Inside the `configure` subdirectory or one level above the TOP location, create a file `RELEASE.local` that sets `EPICS_BASE` to the absolute path of your EPICS installation. - -Inside the `configure` subdirectory or one level above the TOP location, create a file `CONFIG_SITE.local` that sets the absolute path of your SDK installation as well as the SDK build and deploy features if necessary. - -```Makefile -# Path to the Unified Automation OPC UA C++ SDK -UASDK = /usr/local/opcua/uasdkcppclient-v1.5.3-346/sdk - -# How the Unified Automation SDK shared libraries are deployed -# SYSTEM = shared libs are in a system location -# PROVIDED = shared libs are in $(UASDK_SHRLIB_DIR) -# INSTALL = shared libs are installed (copied) into this module -# EMBED = link SDK code statically into libopcua, -# SDK libraries are not required on target system -UASDK_DEPLOY_MODE = PROVIDED -UASDK_LIB_DIR = $(UASDK)/lib -UASDK_SHRLIB_DIR = $(UASDK_LIB_DIR) -# How the Unified Automation SDK libraries were built -UASDK_USE_CRYPTO = YES -UASDK_USE_XMLPARSER = YES -``` - -The SDK related configuration only has to be done in this module, which creates a `CONFIG_OPCUA` file that is automatically included by all downstream modules, so that the configuration is always consistent. - -When using the evaluation bundle of the SDK, only regular (shared) builds are supported. -Do not set `STATIC_BUILD = YES` in your configuration. -Also, the evaluation bundle only contains binary libraries to be used with the MSVC compilers. MinGW can not be used with the libraries from the bundle (C++ name mangling is different.) - -### Windows Specifics - -Windows builds should use `UASDK_DEPLOY_MODE = PROVIDED` and set `UASDK` to the location of the Unified Automation bundle. - -All paths in EPICS build configuration files must use Windows "short names" where needed, e.g. -```Makefile -UASDK = C:/PROGRA~2/UnifiedAutomation/UaSdkCppBundleEval -``` - -In your `CONFIG_SITE.local`, you also need to define the locations of the supplied third-party dependencies. - -```makefile -# Prerequisites: libxml2 openssl -LIBXML2 = $(UASDK)/third-party/win64/vs2015/libxml2 -OPENSSL = $(UASDK)/third-party/win64/vs2015/openssl -``` - -Different versions of the Unified Automation bundle have these libraries in different folders, under different names. Check `configure/CONFIG_SITE.Common.win32-x86` and adapt the settings to your situation. - -If you do not have Administrator rights or if you want to unpack multiple versions of the UA SDK bundle in parallel, [UniExtract2][uniextract2] is your friend. - -*Note:* -As always on Windows, the build environment as well as the runtime environment (for any IOC) need to have the locations of all DLLs in the PATH to be able to run binaries (which is part of the build for the unit tests). You will need to add the appropriate locations for EPICS Base, the opcua module, the UASDK library and all other dependencies you installed. -Alternatively, you can put copies of all needed DLLs into the location of the binaries. (Remember that `make distclean` will remove those copies.) - -#### DLL Hell - -Be aware that over time, Windows systems will have a large number of possibly different DLLs with the same names and/or versions being used concurrently on the system - a situation that is hard to maintain and known as [DLL Hell](https://en.wikipedia.org/wiki/DLL_Hell). - -If you want your IOC binaries to be deployable without depending on specific DLLs being present on the target system, consider linking your IOCs statically. (As stated above, static builds are not available when -using the evaluation bundles.) - -On newer Windows systems, the Windows system version of the openssl DLLs are not compatible with the UA SDK bundled versions (see below). The unit tests Makefile copies the vendor-supplied DLLs next to the test binaries to avoid a mismatch. - -## Feedback / Reporting issues - -Please use the GitHub project's [issue tracker](https://github.com/ralphlange/opcua/issues). - -## Known issues - -Reported by Shi Li (ASIPP): -The C++ SDK version 1.7.1 has a bug that shows as monitored items not getting updates anymore after the server has disconnected and reconnected. A reboot of the IOC restores good behaviour until the server disconnects again. Updating the C++ SDK to version 1.7.2 resolves the issue. - -Reported by Carsten Winkler (HZB/BESSY): -On Windows 10, you may experience a version mismatch between the openssl DLLs that are part of the Windows system and a different version of the same DLLs that are part of the UA SDK bundle. -In that case, copying the DLLs from the UA SDK into the same directory as the unit tests (`...\unitTestApp\src\O.win...`) and your executable (IOC binary) seems to be the only reasonable workaround. - -## Credits - -This module is based on extensive [prototype work](https://github.com/bkuner/opcUaUnifiedAutomation) by Bernhard Kuner (HZB/BESSY) and uses ideas and code snippets from Michael Davidsaver (Osprey DCS). -Support for Windows builds with help from Carsten Winkler (HZB/BESSY). - -## License - -This module is distributed subject to a Software License Agreement found in file LICENSE that is included with this distribution. - - - -[unified.sdk]: https://www.unified-automation.com/products/client-sdk/c-ua-client-sdk.html -[uniextract2]: https://github.com/Bioruebe/UniExtract2 +Please refer to the +[**documentation**](https://ralphlange.github.io/opcua/tutorials/20_build_client_lib.html#option-b-unified-automation-c-client-sdk) +for instructions how to build the module +using the Unified Automation SDK low-level client. diff --git a/devOpcuaSup/UaSdk/Session.cpp b/devOpcuaSup/UaSdk/Session.cpp index 3e6a60cd..446242fb 100644 --- a/devOpcuaSup/UaSdk/Session.cpp +++ b/devOpcuaSup/UaSdk/Session.cpp @@ -25,9 +25,9 @@ #include #define epicsExportSharedSymbols +#include "OpcuaRegistry.h" #include "Session.h" #include "SessionUaSdk.h" -#include "Registry.h" #define st(s) #s #define str(s) st(s) diff --git a/devOpcuaSup/UaSdk/SessionUaSdk.cpp b/devOpcuaSup/UaSdk/SessionUaSdk.cpp index 6f8b42ee..447c58df 100644 --- a/devOpcuaSup/UaSdk/SessionUaSdk.cpp +++ b/devOpcuaSup/UaSdk/SessionUaSdk.cpp @@ -13,13 +13,11 @@ #include #include -#include #include #include #include #include #include -#include #include #include @@ -33,6 +31,7 @@ #include #include #include +#include #ifdef HAS_SECURITY #include @@ -52,7 +51,6 @@ #include "RequestQueueBatcher.h" #include "SessionUaSdk.h" #include "SubscriptionUaSdk.h" -#include "DataElementUaSdk.h" #include "ItemUaSdk.h" namespace DevOpcua { @@ -405,9 +403,12 @@ SessionUaSdk::processRequests(std::vector> &batch) ServiceSettings serviceSettings; OpcUa_UInt32 id = getTransactionId(); - nodesToRead.create(static_cast(batch.size())); + nodesToRead.create(static_cast(batch.size() * no_of_properties_read)); OpcUa_UInt32 i = 0; for (auto c : batch) { + c->item->getNodeId().copyTo(&nodesToRead[i].NodeId); + nodesToRead[i].AttributeId = OpcUa_Attributes_DataType; + i++; c->item->getNodeId().copyTo(&nodesToRead[i].NodeId); nodesToRead[i].AttributeId = OpcUa_Attributes_Value; itemsToRead->push_back(c->item); @@ -1173,7 +1174,6 @@ void SessionUaSdk::connectionStatusChanged ( rebuildNodeIds(); registerNodes(); createAllSubscriptions(); - addAllMonitoredItems(); } if (serverConnectionStatus != UaClient::ConnectionWarningWatchdogTimeout) { if (debug) { @@ -1192,6 +1192,12 @@ void SessionUaSdk::connectionStatusChanged ( // status needs to be updated before requests are being issued serverConnectionStatus = serverStatus; reader.pushRequest(cargo, menuPriorityHIGH); + // Wait for initial read to finish + while (!reader.empty(menuPriorityHIGH)) { + epicsThreadSleep(.1); + } + epicsThreadSleep(.1); + addAllMonitoredItems(); } break; @@ -1210,6 +1216,19 @@ void SessionUaSdk::connectionStatusChanged ( serverConnectionStatus = serverStatus; } +const EnumChoices* +SessionUaSdk::getEnumChoices(const UaEnumDefinition& enumDefinition) const +{ + if (!enumDefinition.childrenCount()) + return nullptr; + auto enumChoices = new EnumChoices; + for (int i = 0; i < enumDefinition.childrenCount(); i++) { + const UaEnumValue& choice = enumDefinition.child(i); + enumChoices->emplace(choice.value(), choice.name().toUtf8()); + } + return enumChoices; +} + void SessionUaSdk::readComplete (OpcUa_UInt32 transactionId, const UaStatus &result, @@ -1228,7 +1247,7 @@ SessionUaSdk::readComplete (OpcUa_UInt32 transactionId, << ": (readComplete) getting data for read service" << " (transaction id " << transactionId << "; data for " << values.length() << " items)" << std::endl; - if ((*it->second).size() != values.length()) + if ((*it->second).size() * no_of_properties_read != values.length()) errlogPrintf("OPC UA session %s: (readComplete) received a callback " "with %u values for a request containing %lu items\n", name.c_str(), values.length(), (*it->second).size()); @@ -1237,6 +1256,11 @@ SessionUaSdk::readComplete (OpcUa_UInt32 transactionId, if (i >= values.length()) { item->setIncomingEvent(ProcessReason::readFailure); } else { + UaNodeId typeId; + if (OpcUa_IsGood(values[i].StatusCode)) { + UaVariant(values[i].Value).toNodeId(typeId); + } + i++; if (debug >= 5) { std::cout << "** Session " << name.c_str() << ": (readComplete) getting data for item " @@ -1245,9 +1269,9 @@ SessionUaSdk::readComplete (OpcUa_UInt32 transactionId, ProcessReason reason = ProcessReason::readComplete; if (OpcUa_IsNotGood(values[i].StatusCode)) reason = ProcessReason::readFailure; - item->setIncomingData(values[i], reason); + item->setIncomingData(values[i], reason, &typeId); + i++; } - i++; } outstandingOps.erase(it); } else { diff --git a/devOpcuaSup/UaSdk/SessionUaSdk.h b/devOpcuaSup/UaSdk/SessionUaSdk.h index efee6622..fac201cf 100644 --- a/devOpcuaSup/UaSdk/SessionUaSdk.h +++ b/devOpcuaSup/UaSdk/SessionUaSdk.h @@ -23,15 +23,17 @@ #include #include #include +#include #include #include #include #include +#include "DataElement.h" +#include "OpcuaRegistry.h" #include "RequestQueueBatcher.h" #include "Session.h" -#include "Registry.h" namespace DevOpcua { @@ -115,6 +117,14 @@ class SessionUaSdk */ virtual const std::string & getName() const override; + /** + * @brief Get pointer to enumChoices if argument refers to enum type, else nullptr + */ + const EnumChoices* getEnumChoices(const UaEnumDefinition& enumDefinition) const; + + const EnumChoices* getEnumChoices(const UaNodeId* typeId) const + { return typeId ? getEnumChoices(puasession->enumDefinition(*typeId)) : nullptr; } + /** * @brief Get a structure definition from the session dictionary. * @param dataTypeId data type of the extension object diff --git a/devOpcuaSup/UaSdk/Subscription.cpp b/devOpcuaSup/UaSdk/Subscription.cpp index 1a64d9e4..43b04bfc 100644 --- a/devOpcuaSup/UaSdk/Subscription.cpp +++ b/devOpcuaSup/UaSdk/Subscription.cpp @@ -16,9 +16,7 @@ #include #define epicsExportSharedSymbols -#include "Session.h" #include "Subscription.h" -#include "SessionUaSdk.h" #include "SubscriptionUaSdk.h" namespace DevOpcua { diff --git a/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp b/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp index c83ab8be..2695e151 100644 --- a/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp +++ b/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp @@ -23,16 +23,17 @@ #include #include +#include #include #include #include #define epicsExportSharedSymbols -#include "SubscriptionUaSdk.h" -#include "ItemUaSdk.h" #include "DataElementUaSdk.h" -#include "Registry.h" +#include "OpcuaRegistry.h" +#include "RecordConnector.h" +#include "SubscriptionUaSdk.h" #include "devOpcua.h" namespace DevOpcua { @@ -178,6 +179,26 @@ SubscriptionUaSdk::addMonitoredItems () monitoredItemCreateRequests[i].RequestedParameters.SamplingInterval = it->linkinfo.samplingInterval; monitoredItemCreateRequests[i].RequestedParameters.QueueSize = it->linkinfo.queueSize; monitoredItemCreateRequests[i].RequestedParameters.DiscardOldest = it->linkinfo.discardOldest; + if (it->linkinfo.deadband > 0.0) { +#ifdef _WIN32 + errlogPrintf("OPC UA subscription %s@%s: deadband not implemented for UA SDK on Windows\n", + name.c_str(), psessionuasdk->getName().c_str()); +#else + OpcUa_DataChangeFilter* pDataChangeFilter = NULL; + status = OpcUa_EncodeableObject_CreateExtension( + &OpcUa_DataChangeFilter_EncodeableType, + &monitoredItemCreateRequests[i].RequestedParameters.Filter, + (OpcUa_Void**)&pDataChangeFilter); + if (status.isBad()) { + errlogPrintf("OPC UA subscription %s@%s: cannot create deadband filter: %s\n", + name.c_str(), psessionuasdk->getName().c_str(), status.toString().toUtf8()); + } else { + pDataChangeFilter->DeadbandType = OpcUa_DeadbandType_Absolute; + pDataChangeFilter->DeadbandValue = it->linkinfo.deadband; + pDataChangeFilter->Trigger = OpcUa_DataChangeTrigger_StatusValue; + } +#endif + } i++; } @@ -199,20 +220,21 @@ SubscriptionUaSdk::addMonitoredItems () std::cout << "Subscription " << name << "@" << psessionuasdk->getName() << ": created " << items.size() << " monitored items (" << status.toString().toUtf8() << ")" << std::endl; - if (debug >= 5) { - for (i = 0; i < items.size(); i++) { - UaNodeId node(monitoredItemCreateRequests[i].ItemToMonitor.NodeId); - if (OpcUa_IsGood(monitoredItemCreateResults[i].StatusCode)) - std::cout << "** Monitored item " << node.toXmlString().toUtf8() + for (i = 0; i < items.size(); i++) { + if (OpcUa_IsGood(monitoredItemCreateResults[i].StatusCode)) { + if (debug >= 5) + std::cout << "** OPC UA record " << items[i]->recConnector->getRecordName() + << " monitored item " << UaNodeId(monitoredItemCreateRequests[i].ItemToMonitor.NodeId).toXmlString().toUtf8() << " succeeded with id " << monitoredItemCreateResults[i].MonitoredItemId << " revised sampling interval " << monitoredItemCreateResults[i].RevisedSamplingInterval << " revised queue size " << monitoredItemCreateResults[i].RevisedQueueSize << std::endl; - else - std::cout << "** Monitored item " << node.toXmlString().toUtf8() - << " failed with error " - << UaStatus(monitoredItemCreateResults[i].StatusCode).toString().toUtf8() - << std::endl; + } else { + errlogPrintf("OPC UA record %s monitored item %s failed with error %s\n", + items[i]->recConnector->getRecordName(), + UaNodeId(monitoredItemCreateRequests[i].ItemToMonitor.NodeId).toXmlString().toUtf8(), + UaStatus(monitoredItemCreateResults[i].StatusCode).toString().toUtf8()); + items[i]->setIncomingEvent(ProcessReason::connectionLoss); } } } diff --git a/devOpcuaSup/UaSdk/SubscriptionUaSdk.h b/devOpcuaSup/UaSdk/SubscriptionUaSdk.h index 9bde8840..c444b741 100644 --- a/devOpcuaSup/UaSdk/SubscriptionUaSdk.h +++ b/devOpcuaSup/UaSdk/SubscriptionUaSdk.h @@ -25,9 +25,9 @@ #include #include +#include "OpcuaRegistry.h" #include "SessionUaSdk.h" #include "Subscription.h" -#include "Registry.h" namespace DevOpcua { diff --git a/devOpcuaSup/devOpcua.cpp b/devOpcuaSup/devOpcua.cpp index db2028fe..99ea106f 100644 --- a/devOpcuaSup/devOpcua.cpp +++ b/devOpcuaSup/devOpcua.cpp @@ -699,7 +699,83 @@ opcua_write_analog (REC *prec) return ret; } -// enum output +// enum + +#define NUMBER_OF_ENUM_CHOICES 16 +#define ENUMS_BY_OPCUA 0x10 +#define ENUM_VALUES_CHECKED 0x20 +#define ENUM_VALUES_DEFINED 0x40 + +template +inline void updateEnumInfos (REC *prec, RecordConnector *pcon) +{ + if (pcon->pdataelement->enumChoices && + pcon->state() == ConnectionStatus::initialRead && + (!prec->sdef || prec->sdef & ENUMS_BY_OPCUA)) + { // no strings/value defined by user (but maybe by OPC-UA server) + prec->sdef |= ENUMS_BY_OPCUA | ENUM_VALUES_CHECKED | ENUM_VALUES_DEFINED; + const EnumChoices &enumChoices = *pcon->pdataelement->enumChoices; + auto pvl = &prec->zrvl; + auto pst = &prec->zrst; + size_t i = 0; + for (auto it: enumChoices) { + pvl[i] = it.first; + strncpy(pst[i], it.second.c_str(), sizeof(pst[i])-1); + if (++i >= NUMBER_OF_ENUM_CHOICES) break; + } + while (i < NUMBER_OF_ENUM_CHOICES) { + // clear possible other old strings/values + pvl[i] = ~0; // as invalid as possible + memset(pst[i], 0, sizeof(pst[i])); + i++; + } + db_post_events(prec, &prec->val, DBE_PROPERTY); + } else if (prec->sdef && !(prec->sdef & ENUM_VALUES_CHECKED)) { + // check if user defined strings but no values + prec->sdef |= ENUM_VALUES_CHECKED; + auto pvl = &prec->zrvl; + for (int i = 0; i < NUMBER_OF_ENUM_CHOICES; i++) { + if (*pvl++) { + prec->sdef |= ENUM_VALUES_DEFINED; + break; + } + } + } +} + + +template +long +opcua_read_enum (REC *prec) +{ + long ret = 0; + TRY { + Guard G(pcon->lock); + ProcessReason nextReason = ProcessReason::none; + + if (pcon->reason == ProcessReason::none || pcon->reason == ProcessReason::readRequest) { + if (pcon->state() == ConnectionStatus::up) { + prec->pact = true; + pcon->requestOpcuaRead(); + } else { + (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); + } + } else { + epicsUInt32 *value = useReadValue(pcon) ? &prec->rval : nullptr; + ret = pcon->readScalar(value, &nextReason); + updateEnumInfos (prec, pcon); + if (ret == 0 && value && !(prec->sdef & ENUM_VALUES_DEFINED)) { + prec->val = *value; + ret = 2; + } + traceReadPrint(pdbc, pcon, ret, !!value, prec->rval, "RVAL"); + manageStateAndBiniProcessing(pcon); + } + if (nextReason) + pcon->requestRecordProcessing(nextReason); + } CATCH() + return ret; +} template long @@ -714,8 +790,13 @@ opcua_write_enum (REC *prec) if (pcon->state() == ConnectionStatus::down) { (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); } else if (pcon->state() != ConnectionStatus::initialRead) { - traceWritePrint(pdbc, pcon, prec->rval, "RVAL"); - pcon->writeScalar(prec->rval); + if (prec->sdef & ENUM_VALUES_DEFINED) { + traceWritePrint(pdbc, pcon, prec->rval, "RVAL"); + pcon->writeScalar(prec->rval); + } else { + traceWritePrint(pdbc, pcon, prec->val, "VAL"); + pcon->writeScalar(prec->val); + } if (pcon->plinkinfo->linkedToItem && (pcon->state() == ConnectionStatus::up || pcon->state() == ConnectionStatus::initialWrite)) { @@ -726,14 +807,16 @@ opcua_write_enum (REC *prec) } else { epicsUInt32 *rval = useReadValue(pcon) ? &prec->rval : nullptr; ret = pcon->readScalar(rval, &nextReason); + updateEnumInfos (prec, pcon); if (ret == 0 && rval) { + recGblResetAlarms(prec); // get mbbo out of SOFT alarm after val was out of range *rval &= prec->mask; if (prec->shft) *rval >>= prec->shft; - if (prec->sdef) { + if (prec->sdef & ENUM_VALUES_DEFINED) { epicsUInt32 *pstate_values = &prec->zrvl; prec->val = 65535; // unknown state - for (epicsUInt16 i = 0; i < 16; i++) { + for (epicsUInt16 i = 0; i < NUMBER_OF_ENUM_CHOICES; i++) { if (*pstate_values == *rval) { prec->val = i; break; @@ -741,8 +824,8 @@ opcua_write_enum (REC *prec) pstate_values++; } } else { - // no defined states prec->val = static_cast(*rval); + ret = 2; } } traceReadPrint(pdbc, pcon, ret, !!rval, prec->val, prec->rval); @@ -866,8 +949,8 @@ opcua_read_string_val (REC *prec) } } else { char *value = useReadValue(pcon) ? prec->val : nullptr; - unsigned short num = value ? MAX_STRING_SIZE : 0; - ret = pcon->readScalar(value, num, &nextReason); + unsigned short len = value ? MAX_STRING_SIZE : 0; + ret = pcon->readScalar(value, len, &nextReason); traceReadPrint(pdbc, pcon, ret, !!value, prec->val); manageStateAndBiniProcessing(pcon); } @@ -932,9 +1015,7 @@ opcua_read_lstring_val (REC *prec) } } else { char *value = useReadValue(pcon) ? prec->val : nullptr; - ret = pcon->readScalar(value, prec->sizv, &nextReason); - if (value) - prec->len = static_cast(strlen(prec->val) + 1); + ret = pcon->readScalar(value, prec->sizv, &nextReason, &prec->len); traceReadPrint(pdbc, pcon, ret, !!value, prec->val); manageStateAndBiniProcessing(pcon); } @@ -968,9 +1049,7 @@ opcua_write_lstring_val (REC *prec) } } else { char *value = useReadValue(pcon) ? prec->val : nullptr; - ret = pcon->readScalar(value, prec->sizv, &nextReason); - if (value) - prec->len = static_cast(strlen(prec->val) + 1); + ret = pcon->readScalar(value, prec->sizv, &nextReason, &prec->len); traceReadPrint(pdbc, pcon, ret, !!value, prec->val); manageStateAndBiniProcessing(pcon); } @@ -1002,14 +1081,14 @@ opcua_read_array (REC *prec) } else { void *bptr = useReadValue(pcon) ? prec->bptr : nullptr; switch (prec->ftvl) { + case menuFtypeCHAR: + /* Allow to handle CHAR arrays as long strings */ + ret = pcon->readScalar(static_cast(bptr), prec->nelm, &nextReason, &prec->nord); + break; case menuFtypeSTRING: ret = pcon->readArray(static_cast(bptr), MAX_STRING_SIZE, prec->nelm, &prec->nord, &nextReason); break; - case menuFtypeCHAR: - ret = pcon->readArray(static_cast(bptr), prec->nelm, - &prec->nord, &nextReason); - break; case menuFtypeUCHAR: ret = pcon->readArray(static_cast(bptr), prec->nelm, &prec->nord, &nextReason); @@ -1054,7 +1133,7 @@ opcua_read_array (REC *prec) break; } if (nord != prec->nord) - db_post_events(prec, &prec->nord, DBE_VALUE | DBE_LOG); + db_post_events(prec, &prec->nord, DBE_VALUE | DBE_LOG); traceReadArrayPrint(pdbc, pcon, ret, !!bptr, prec->nord); manageStateAndBiniProcessing(pcon); } @@ -1067,7 +1146,7 @@ opcua_read_array (REC *prec) template long opcua_write_array (REC *prec) -{ +{ long ret = 0; TRY { Guard G(pcon->lock); @@ -1080,12 +1159,13 @@ opcua_write_array (REC *prec) } else if (pcon->state() != ConnectionStatus::initialRead) { traceWriteArrayPrint(pdbc, pcon, prec->nord); switch (prec->ftvl) { + case menuFtypeCHAR: + /* Allow to handle CHAR arrays as long strings */ + ret = pcon->writeScalar(static_cast(prec->bptr), prec->nord); + break; case menuFtypeSTRING: ret = pcon->writeArray(static_cast(prec->bptr), MAX_STRING_SIZE, prec->nord); break; - case menuFtypeCHAR: - ret = pcon->writeArray(static_cast(prec->bptr), prec->nord); - break; case menuFtypeUCHAR: ret = pcon->writeArray(static_cast(prec->bptr), prec->nord); break; @@ -1129,14 +1209,14 @@ opcua_write_array (REC *prec) } else { void *bptr = useReadValue(pcon) ? prec->bptr : nullptr; switch (prec->ftvl) { + case menuFtypeCHAR: + /* Allow to handle CHAR arrays as long strings */ + ret = pcon->readScalar(static_cast(bptr), prec->nelm, &nextReason, &prec->nord); + break; case menuFtypeSTRING: ret = pcon->readArray(static_cast(bptr), MAX_STRING_SIZE, prec->nelm, &prec->nord, &nextReason); break; - case menuFtypeCHAR: - ret = pcon->readArray(static_cast(bptr), prec->nelm, - &prec->nord, &nextReason); - break; case menuFtypeUCHAR: ret = pcon->readArray(static_cast(bptr), prec->nelm, &prec->nord, &nextReason); @@ -1271,7 +1351,7 @@ SUP (devLiOpcua, longin, int32_val, read) SUP (devLoOpcua, longout, int32_val, write) SUP (devBiOpcua, bi, uint32_rval, read) SUP (devBoOpcua, bo, bo, write) -SUPM(devMbbiOpcua, mbbi, uint32_rval, read) +SUPM(devMbbiOpcua, mbbi, enum, read) SUPM(devMbboOpcua, mbbo, enum, write) SUPM(devMbbiDirectOpcua, mbbiDirect, uint32_rval, read) SUPM(devMbboDirectOpcua, mbboDirect, mbbod, write) diff --git a/devOpcuaSup/devOpcua.h b/devOpcuaSup/devOpcua.h index 7c1901fa..3b8453a2 100644 --- a/devOpcuaSup/devOpcua.h +++ b/devOpcuaSup/devOpcua.h @@ -101,6 +101,7 @@ typedef struct linkInfo { epicsUInt32 queueSize; epicsUInt32 clientQueueSize; bool discardOldest = true; + double deadband = 0; std::string element; std::list elementPath; diff --git a/devOpcuaSup/iocshIntegration.cpp b/devOpcuaSup/iocshIntegration.cpp index 1d305929..1635f874 100644 --- a/devOpcuaSup/iocshIntegration.cpp +++ b/devOpcuaSup/iocshIntegration.cpp @@ -20,20 +20,25 @@ #include #include #include +// cannot use : to separate command line arguments as it matches C: in file paths +#define ARGSEP ';' +#else +#define ARGSEP ':' #endif #include #include #include -#include // defines epicsExportSharedSymbols +#include // defines epicsExportSharedSymbols + +#include "OpcuaRegistry.h" +#include "RecordConnector.h" +#include "Session.h" +#include "Subscription.h" #include "devOpcua.h" #include "iocshVariables.h" #include "linkParser.h" -#include "Session.h" -#include "Subscription.h" -#include "Registry.h" -#include "RecordConnector.h" namespace DevOpcua { @@ -147,7 +152,7 @@ opcuaSessionCallFunc(const iocshArgBuf *args) std::list> setopts; for (int i = 1; i < args[2].aval.ac; i++) { - auto options = splitString(args[2].aval.av[i], ':'); + auto options = splitString(args[2].aval.av[i], ARGSEP); for (auto &opt : options) { if (opt.empty()) continue; auto keyval = splitString(opt, '='); @@ -252,7 +257,7 @@ static std::list> setopts; for (int i = 1; i < args[3].aval.ac; i++) { - auto options = splitString(args[3].aval.av[i], ':'); + auto options = splitString(args[3].aval.av[i], ARGSEP); for (auto &opt : options) { if (opt.empty()) continue; auto keyval = splitString(opt, '='); @@ -311,7 +316,6 @@ opcuaOptionsCallFunc(const iocshArgBuf *args) } else if (strcmp(args[0].sval, "help") == 0) { std::cout << opcuaOptionsUsage.c_str() << std::endl; } else { - std::cerr << args[1].aval.ac << " options: " << args[1].aval.av[0] << std::endl; if (args[1].aval.ac <= 1) { errlogPrintf("missing argument #2 (options)\n"); } else { @@ -320,7 +324,7 @@ opcuaOptionsCallFunc(const iocshArgBuf *args) if (sessions.size()) { foundSomething = true; for (int i = 1; i < args[1].aval.ac; i++) { - auto options = splitString(args[1].aval.av[i], ':'); + auto options = splitString(args[1].aval.av[i], ARGSEP); for (auto &opt : options) { if (opt.empty()) continue; auto keyval = splitString(opt, '='); @@ -339,7 +343,7 @@ opcuaOptionsCallFunc(const iocshArgBuf *args) if (subscriptions.size()) { foundSomething = true; for (int i = 1; i < args[1].aval.ac; i++) { - auto options = splitString(args[1].aval.av[i], ':'); + auto options = splitString(args[1].aval.av[i], ARGSEP); for (auto &opt : options) { if (opt.empty()) continue; auto keyval = splitString(opt, '='); @@ -1107,8 +1111,6 @@ void opcuaIocshRegister () iocshRegister(&opcuaShowDataFuncDef, opcuaShowDataCallFunc); } -extern "C" { -epicsExportRegistrar(opcuaIocshRegister); -} - } // namespace + +extern "C" { epicsExportRegistrar(opcuaIocshRegister); } diff --git a/devOpcuaSup/linkParser.cpp b/devOpcuaSup/linkParser.cpp index 88706f90..4ce86249 100644 --- a/devOpcuaSup/linkParser.cpp +++ b/devOpcuaSup/linkParser.cpp @@ -275,6 +275,9 @@ parseLink (dbCommon *prec, const DBEntry &ent) } else if (pinfo->linkedToItem && optname == "sampling") { if (epicsParseDouble(optval.c_str(), &pinfo->samplingInterval, nullptr)) throw std::runtime_error(SB() << "error converting '" << optval << "' to Double"); + } else if (pinfo->linkedToItem && optname == "deadband") { + if (epicsParseDouble(optval.c_str(), &pinfo->deadband, nullptr)) + throw std::runtime_error(SB() << "error converting '" << optval << "' to Double"); } else if (pinfo->linkedToItem && optname == "qsize") { if (epicsParseUInt32(optval.c_str(), &pinfo->queueSize, 0, nullptr)) throw std::runtime_error(SB() << "error converting '" << optval << "' to UInt32"); @@ -352,6 +355,7 @@ parseLink (dbCommon *prec, const DBEntry &ent) else std::cout << " id(s)=" << pinfo->identifierString; std::cout << " sampling=" << pinfo->samplingInterval + << " deadband=" << pinfo->deadband << " qsize=" << pinfo->queueSize << " cqsize=" << pinfo->clientQueueSize << " discard=" << (pinfo->discardOldest ? "old" : "new") diff --git a/devOpcuaSup/opcuaItemRecord.cpp b/devOpcuaSup/opcuaItemRecord.cpp index 8a45fa90..ab0f89b4 100644 --- a/devOpcuaSup/opcuaItemRecord.cpp +++ b/devOpcuaSup/opcuaItemRecord.cpp @@ -203,6 +203,7 @@ rset opcuaItemRSET = { get_control_double, get_alarm_double }; -extern "C" { epicsExportAddress(rset, opcuaItemRSET); } } // namespace + +extern "C" { epicsExportAddress(rset, opcuaItemRSET); } diff --git a/devOpcuaSup/open62541/DataElementOpen62541.cpp b/devOpcuaSup/open62541/DataElementOpen62541.cpp deleted file mode 100644 index dcacf46f..00000000 --- a/devOpcuaSup/open62541/DataElementOpen62541.cpp +++ /dev/null @@ -1,1234 +0,0 @@ -/*************************************************************************\ -* Copyright (c) 2018-2023 ITER Organization. -* This module is distributed subject to a Software License Agreement found -* in file LICENSE that is included with this distribution. -\*************************************************************************/ - -/* - * Author: Dirk Zimoch - * - * based on the UaSdk implementation by Ralph Lange - */ - -#define epicsExportSharedSymbols -#include "DataElementOpen62541.h" -#include "ItemOpen62541.h" - -#include -#include - -#include - -#include -#include -#include -#include - -namespace DevOpcua { - -/* Specific implementation of DataElement's "factory" method */ -void -DataElement::addElementToTree(Item *item, - RecordConnector *pconnector, - const std::list &elementPath) -{ - DataElementOpen62541::addElementToTree(static_cast(item), pconnector, elementPath); -} - -DataElementOpen62541::DataElementOpen62541 (const std::string &name, - ItemOpen62541 *item, - RecordConnector *pconnector) - : DataElement(pconnector, name) - , pitem(item) - , timesrc(-1) - , mapped(false) - , incomingQueue(pconnector->plinkinfo->clientQueueSize, pconnector->plinkinfo->discardOldest) - , outgoingLock(pitem->dataTreeWriteLock) - , isdirty(false) -{ - UA_Variant_init(&incomingData); - UA_Variant_init(&outgoingData); -} - -DataElementOpen62541::DataElementOpen62541 (const std::string &name, - ItemOpen62541 *item) - : DataElement(name) - , pitem(item) - , timesrc(-1) - , mapped(false) - , incomingQueue(0ul) - , outgoingLock(pitem->dataTreeWriteLock) - , isdirty(false) -{ - UA_Variant_init(&incomingData); - UA_Variant_init(&outgoingData); -} - -void -DataElementOpen62541::addElementToTree(ItemOpen62541 *item, - RecordConnector *pconnector, - const std::list &elementPath) -{ - std::string name("[ROOT]"); - if (elementPath.size()) - name = elementPath.back(); - - auto leaf = std::make_shared(name, item, pconnector); - item->dataTree.addLeaf(leaf, elementPath); - // reference from connector after adding to the tree worked - pconnector->setDataElement(leaf); -} - -void -DataElementOpen62541::show (const int level, const unsigned int indent) const -{ - std::string ind(static_cast(indent)*2, ' '); // static_cast to avoid warning C26451 - std::cout << ind; - if (isLeaf()) { - std::cout << "leaf=" << name << " record(" << pconnector->getRecordType() << ")=" - << pconnector->getRecordName() - << " type=" << variantTypeString(incomingData) - << " timestamp=" << linkOptionTimestampString(pconnector->plinkinfo->timestamp) - << " bini=" << linkOptionBiniString(pconnector->plinkinfo->bini) - << " monitor=" << (pconnector->plinkinfo->monitor ? "y" : "n") << "\n"; - } else { - std::cout << "node=" << name << " children=" << elements.size() - << " mapped=" << (mapped ? "y" : "n") << "\n"; - for (auto it : elements) { - if (auto pelem = it.lock()) { - pelem->show(level, indent + 1); - } - } - } -} - -#ifndef UA_ENABLE_TYPEDESCRIPTION -// Without type description, we cannot get struct members -#error Set UA_ENABLE_TYPEDESCRIPTION in open62541 -#endif - -#ifndef UA_DATATYPES_USE_POINTER -// Older open62541 version 1.2 has no UA_DataType_getStructMember -// Code from open62541 version 1.3.7 modified for compatibility with version 1.2 -UA_Boolean -UA_DataType_getStructMember(const UA_DataType *type, const char *memberName, - size_t *outOffset, const UA_DataType **outMemberType, - UA_Boolean *outIsArray) { - if(type->typeKind != UA_DATATYPEKIND_STRUCTURE && - type->typeKind != UA_DATATYPEKIND_OPTSTRUCT) - return false; - - size_t offset = 0; - const UA_DataType *typelists[2] = { UA_TYPES, &type[-type->typeIndex] }; - for(size_t i = 0; i < type->membersSize; ++i) { - const UA_DataTypeMember *m = &type->members[i]; - const UA_DataType *mt = &typelists[!m->namespaceZero][m->memberTypeIndex]; - offset += m->padding; - - if(strcmp(memberName, m->memberName) == 0) { - *outOffset = offset; - *outMemberType = mt; - *outIsArray = m->isArray; - return true; - } - - if(!m->isOptional) { - if(!m->isArray) { - offset += mt->memSize; - } else { - offset += sizeof(size_t); - offset += sizeof(void*); - } - } else { /* field is optional */ - if(!m->isArray) { - offset += sizeof(void *); - } else { - offset += sizeof(size_t); - offset += sizeof(void *); - } - } - } - - return false; -} -#endif - -bool -DataElementOpen62541::createMap (const UA_DataType *type, - const std::string *timefrom) -{ - switch (typeKindOf(type)) { - case UA_DATATYPEKIND_STRUCTURE: - { - if (debug() >= 5) - std::cout << " ** creating index-to-element map for child elements" << std::endl; - - if (timefrom) { - const UA_DataType *timeMemberType; - UA_Boolean timeIsArray; - size_t timeOffset; - - if (UA_DataType_getStructMember(type, timefrom->c_str(), - &timeOffset, - &timeMemberType, - &timeIsArray)) { - if (typeKindOf(timeMemberType) != UA_TYPES_DATETIME || timeIsArray) { - errlogPrintf("%s: timestamp element %s has invalid type %s%s - using " - "source timestamp\n", - pitem->recConnector->getRecordName(), - timefrom->c_str(), - typeKindName(typeKindOf(timeMemberType)), - timeIsArray ? "[]" : ""); - } - timesrc = timeOffset; - } else { - errlogPrintf( - "%s: timestamp element %s not found - using source timestamp\n", - pitem->recConnector->getRecordName(), - timefrom->c_str()); - } - } - - for (auto &it : elements) { - auto pelem = it.lock(); - if (UA_DataType_getStructMember(type, pelem->name.c_str(), - &pelem->offset, - &pelem->memberType, - &pelem->isArray)) { - } else { - std::cerr << "Item " << pitem - << ": element " << pelem->name - << " not found in " << variantTypeString(type) - << std::endl; - } - } - if (debug() >= 5) - std::cout << " ** " << elements.size() - << " child elements mapped to a " - << "structure of " << type->membersSize << " elements" << std::endl; - break; - } - default: - std::cerr << "Item " << pitem - << " has unimplemented type " << variantTypeString(type) - << std::endl; - return false; - } - mapped = true; - return true; -} - -// Getting the timestamp and status information from the Item assumes that only one thread -// is pushing data into the Item's DataElement structure at any time. -void -DataElementOpen62541::setIncomingData (const UA_Variant &value, - ProcessReason reason, - const std::string *timefrom) -{ - // Make a copy of this element and cache it - UA_Variant_clear(&incomingData); - UA_Variant_copy(&value, &incomingData); - - if (isLeaf()) { - if ((pconnector->state() == ConnectionStatus::initialRead - && (reason == ProcessReason::readComplete || reason == ProcessReason::readFailure)) - || (pconnector->state() == ConnectionStatus::up)) { - - Guard(pconnector->lock); - bool wasFirst = false; - // Make a copy of the value for this element and put it on the queue - UA_Variant *valuecopy (new UA_Variant); - UA_Variant_copy(&value, valuecopy); // As a non-C++ object, UA_Variant has no copy constructor - UpdateOpen62541 *u(new UpdateOpen62541(getIncomingTimeStamp(), reason, std::unique_ptr(valuecopy), getIncomingReadStatus())); - incomingQueue.pushUpdate(std::shared_ptr(u), &wasFirst); - if (debug() >= 5) - std::cout << "Item " << pitem - << " element " << name - << " set data (" << processReasonString(reason) - << ") for record " << pconnector->getRecordName() - << " (queue use " << incomingQueue.size() - << "/" << incomingQueue.capacity() << ")" - << std::endl; - if (wasFirst) - pconnector->requestRecordProcessing(reason); - } - } else { - if (UA_Variant_isEmpty(&value)) - return; - - if (debug() >= 5) - std::cout << "Item " << pitem - << " element " << name - << " splitting structured data to " - << elements.size() << " child elements" - << std::endl; - - const UA_DataType *type = value.type; - char* container = static_cast(value.data); - if (type->typeKind == UA_DATATYPEKIND_EXTENSIONOBJECT) { - UA_ExtensionObject &extensionObject = *reinterpret_cast(container); - if (extensionObject.encoding >= UA_EXTENSIONOBJECT_DECODED) { - // Access content of decoded extension objects - type = extensionObject.content.decoded.type; - container = static_cast(extensionObject.content.decoded.data); - } else { - std::cerr << "Cannot get a structure definition for item " << pitem - << " because binaryEncodingId " << extensionObject.content.encoded.typeId - << " is not in the type dictionary." << std::endl; - return; - } - } - - if (!mapped) - createMap(type, timefrom); - - if (timefrom) { - if (timesrc >= 0) - pitem->tsData = ItemOpen62541::uaToEpicsTime(*reinterpret_cast(container + timesrc), 0); - else - pitem->tsData = pitem->tsSource; - } - - for (auto &it : elements) { - auto pelem = it.lock(); - char* memberData = container + pelem->offset; - const UA_DataType* memberType = pelem->memberType; - UA_Variant memberValue; - if (pelem->isArray) - { - size_t arrayLength = *reinterpret_cast(memberData); - memberData = *reinterpret_cast(memberData + sizeof(size_t)); - if (arrayLength == 0) memberData = reinterpret_cast(UA_EMPTY_ARRAY_SENTINEL); - UA_Variant_setArray(&memberValue, memberData, arrayLength, memberType); - } else { - UA_Variant_setScalar(&memberValue, memberData, memberType); - } - pelem->setIncomingData(memberValue, reason); - } - } -} - -void -DataElementOpen62541::setIncomingEvent (ProcessReason reason) -{ - if (isLeaf()) { - Guard(pconnector->lock); - bool wasFirst = false; - // Put the event on the queue - UpdateOpen62541 *u(new UpdateOpen62541(getIncomingTimeStamp(), reason)); - incomingQueue.pushUpdate(std::shared_ptr(u), &wasFirst); - if (debug() >= 5) - std::cout << "Element " << name << " set event (" - << processReasonString(reason) - << ") for record " << pconnector->getRecordName() - << " (queue use " << incomingQueue.size() - << "/" << incomingQueue.capacity() << ")" - << std::endl; - if (wasFirst) - pconnector->requestRecordProcessing(reason); - } else { - for (auto &it : elements) { - auto pelem = it.lock(); - pelem->setIncomingEvent(reason); - } - } -} - -void -DataElementOpen62541::setState(const ConnectionStatus state) -{ - if (isLeaf()) { - Guard(pconnector->lock); - pconnector->setState(state); - } else { - for (auto &it : elements) { - auto pelem = it.lock(); - pelem->setState(state); - } - } -} - -// Helper to update one data structure element from pointer to child -bool -DataElementOpen62541::updateDataInStruct (void* container, - std::shared_ptr pelem) -{ - bool updated = false; - { // Scope of Guard G - Guard G(pelem->outgoingLock); - if (pelem->isDirty()) { - void* memberData = static_cast(container) + pelem->offset; - const UA_Variant& data = pelem->getOutgoingData(); - const UA_DataType* memberType = pelem->memberType; - assert(memberType == data.type); - if (pelem->isArray) - { - size_t& arrayLength = *reinterpret_cast(memberData); - void*& arrayData = *reinterpret_cast(static_cast(memberData) + sizeof(size_t)); - UA_Array_delete(arrayData, arrayLength, memberType); - UA_StatusCode status = UA_Array_copy(data.data, data.arrayLength, &arrayData, memberType); - if (status == UA_STATUSCODE_GOOD) { - arrayLength = data.arrayLength; - } else { - arrayLength = 0; - std::cerr << "Item " << pitem - << ": inserting data from from child element" << pelem->name - << " failed (" << UA_StatusCode_name(status) << ')' - << std::endl; - return false; - } - } else { - UA_copy(data.data, memberData, memberType); - } - pelem->isdirty = false; - updated = true; - } - } - if (debug() >= 4) { - if (updated) { - std::cout << "Data from child element " << pelem->name - << " inserted into data structure" << std::endl; - } else { - std::cout << "Data from child element " << pelem->name - << " ignored (not dirty)" << std::endl; - } - } - return updated; -} - - -const UA_Variant & -DataElementOpen62541::getOutgoingData () -{ - if (!isLeaf()) { - if (debug() >= 4) - std::cout << "Item " << pitem - << " element " << name - << " updating structured data from " - << elements.size() << " child elements" - << std::endl; - - UA_Variant_clear(&outgoingData); - UA_Variant_copy(&incomingData, &outgoingData); - isdirty = false; - const UA_DataType *type = outgoingData.type; - void* container = outgoingData.data; - if (type && type->typeKind == UA_DATATYPEKIND_EXTENSIONOBJECT) { - UA_ExtensionObject &extensionObject = *reinterpret_cast(container); - if (extensionObject.encoding >= UA_EXTENSIONOBJECT_DECODED) { - // Access content decoded extension objects - type = extensionObject.content.decoded.type; - container = extensionObject.content.decoded.data; - } else { - std::cerr << "Cannot get a structure definition for item " << pitem - << " because binaryEncodingId " << extensionObject.content.encoded.typeId - << " is not in the type dictionary." << std::endl; - return outgoingData; - } - } - - if (!mapped) - createMap(type, nullptr); - - for (auto &it : elements) { - auto pelem = it.lock(); - if (updateDataInStruct(container, pelem)) - isdirty = true; - } - if (isdirty) { - if (debug() >= 4) - std::cout << "Encoding changed data structure to outgoingData of element " << name - << std::endl; - } else { - if (debug() >= 4) - std::cout << "Returning unchanged outgoingData of element " << name - << std::endl; - } - } - return outgoingData; -} - -void -DataElementOpen62541::dbgReadScalar (const UpdateOpen62541 *upd, - const std::string &targetTypeName, - const size_t targetSize) const -{ - if (isLeaf() && debug()) { - char time_buf[40]; - upd->getTimeStamp().strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S.%09f"); - ProcessReason reason = upd->getType(); - - std::cout << pconnector->getRecordName() << ": "; - if (reason == ProcessReason::incomingData || reason == ProcessReason::readComplete) { - UA_Variant &data = upd->getData(); - std::cout << "(" << linkOptionTimestampString(pconnector->plinkinfo->timestamp); - if (pconnector->plinkinfo->timestamp == LinkOptionTimestamp::data) - std::cout << "(@" << pconnector->plinkinfo->timestampElement << ")"; - std::cout << " time " << time_buf << ") read " << processReasonString(reason) << " (" - << UA_StatusCode_name(upd->getStatus()) << ") " - << data - << " as " << targetTypeName; - if (targetSize) - std::cout << "[" << targetSize << "]"; - } else { - std::cout << "(client time "<< time_buf << ") " << processReasonString(reason); - } - std::cout << " --- remaining queue " << incomingQueue.size() - << "/" << incomingQueue.capacity() << std::endl; - } -} - -long -DataElementOpen62541::readScalar (epicsInt32 *value, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementOpen62541::readScalar (epicsInt64 *value, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementOpen62541::readScalar (epicsUInt32 *value, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementOpen62541::readScalar (epicsFloat64 *value, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); -} - -// CString type needs specialization -long -DataElementOpen62541::readScalar (char *value, const size_t num, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - long ret = 0; - - if (incomingQueue.empty()) { - errlogPrintf("%s: incoming data queue empty\n", prec->name); - if (nextReason) - *nextReason = ProcessReason::none; - return 1; - } - - ProcessReason nReason; - std::shared_ptr upd = incomingQueue.popUpdate(&nReason); - dbgReadScalar(upd.get(), "CString", num); - - switch (upd->getType()) { - case ProcessReason::readFailure: - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::connectionLoss: - (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::incomingData: - case ProcessReason::readComplete: - { - if (num && value) { - UA_StatusCode stat = upd->getStatus(); - if (UA_STATUS_IS_BAD(stat)) { - // No valid OPC UA value - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - // Valid OPC UA value, so copy over - if (UA_STATUS_IS_UNCERTAIN(stat)) { - (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); - } - UA_Variant &data = upd->getData(); - size_t n = num-1; - UA_String buffer = UA_STRING_NULL; - UA_String *datastring = &buffer; - switch (typeKindOf(data)) { - case UA_DATATYPEKIND_STRING: - { - datastring = static_cast(data.data); - break; - } - case UA_DATATYPEKIND_LOCALIZEDTEXT: - { - datastring = &static_cast(data.data)->text; - break; - } - case UA_DATATYPEKIND_DATETIME: - { - // UA_print does not correct printed time for time zone - UA_Int64 tOffset = UA_DateTime_localTimeUtcOffset(); - UA_DateTime dt = *static_cast(data.data); - dt += tOffset; - UA_print(&dt, data.type, &buffer); - break; - } - default: - if (data.type) - UA_print(data.data, data.type, &buffer); - } - if (n > datastring->length) - n = datastring->length; - strncpy(value, reinterpret_cast(datastring->data), n); - value[n] = '\0'; - UA_String_clear(&buffer); - prec->udf = false; - UA_Variant_clear(&data); - } - if (statusCode) *statusCode = stat; - if (statusText) { - strncpy(statusText, UA_StatusCode_name(stat), statusTextLen); - statusText[statusTextLen-1] = '\0'; - } - } - break; - } - default: - break; - } - - prec->time = upd->getTimeStamp(); - if (nextReason) *nextReason = nReason; - return ret; -} - -void -DataElementOpen62541::dbgReadArray (const UpdateOpen62541 *upd, - const epicsUInt32 targetSize, - const std::string &targetTypeName) const -{ - if (isLeaf() && debug()) { - char time_buf[40]; - upd->getTimeStamp().strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S.%09f"); - ProcessReason reason = upd->getType(); - - std::cout << pconnector->getRecordName() << ": "; - if (reason == ProcessReason::incomingData || reason == ProcessReason::readComplete) { - std::cout << "(" << linkOptionTimestampString(pconnector->plinkinfo->timestamp); - if (pconnector->plinkinfo->timestamp == LinkOptionTimestamp::data) - std::cout << "@" << pconnector->plinkinfo->timestampElement; - std::cout << " time " << time_buf << ") read " << processReasonString(reason) << " (" - << UA_StatusCode_name(upd->getStatus()) << ") "; - UA_Variant &data = upd->getData(); - std::cout << " array of " << variantTypeString(data) - << "[" << upd->getData().arrayLength << "]" - << " into " << targetTypeName << "[" << targetSize << "]"; - } else { - std::cout << "(client time "<< time_buf << ") " << processReasonString(reason); - } - std::cout << " --- remaining queue " << incomingQueue.size() - << "/" << incomingQueue.capacity() << std::endl; - } -} - -// Read array for EPICS String / UA_String -long int -DataElementOpen62541::readArray (char *value, const epicsUInt32 len, - const epicsUInt32 num, - epicsUInt32 *numRead, - const UA_DataType *expectedType, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - long ret = 0; - epicsUInt32 elemsWritten = 0; - - if (incomingQueue.empty()) { - errlogPrintf("%s : incoming data queue empty\n", prec->name); - *numRead = 0; - return 1; - } - - ProcessReason nReason; - std::shared_ptr upd = incomingQueue.popUpdate(&nReason); - dbgReadArray(upd.get(), num, epicsTypeString(value)); - - switch (upd->getType()) { - case ProcessReason::readFailure: - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::connectionLoss: - (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::incomingData: - case ProcessReason::readComplete: - { - if (num && value) { - UA_StatusCode stat = upd->getStatus(); - if (UA_STATUS_IS_BAD(stat)) { - // No valid OPC UA value - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - // Valid OPC UA value, so try to convert - UA_Variant &data = upd->getData(); - if (UA_Variant_isScalar(&data)) { - errlogPrintf("%s : incoming data is not an array\n", prec->name); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else if (data.type != expectedType) { - errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", - prec->name, variantTypeString(data), epicsTypeString(value)); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - if (UA_STATUS_IS_UNCERTAIN(stat)) { - (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); - } - elemsWritten = static_cast(num < data.arrayLength ? num : data.arrayLength); - for (epicsUInt32 i = 0; i < elemsWritten; i++) { - strncpy(value + i * len, reinterpret_cast(static_cast(data.data)[i].data), len); - (value + i * len)[len - 1] = '\0'; - } - prec->udf = false; - } - UA_Variant_clear(&data); - } - if (statusCode) *statusCode = stat; - if (statusText) { - strncpy(statusText, UA_StatusCode_name(stat), statusTextLen); - statusText[statusTextLen-1] = '\0'; - } - } - break; - } - default: - break; - } - - prec->time = upd->getTimeStamp(); - if (nextReason) *nextReason = nReason; - if (num && value) - *numRead = elemsWritten; - return ret; -} - -long -DataElementOpen62541::readArray (epicsInt8 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, &UA_TYPES[UA_TYPES_SBYTE], prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementOpen62541::readArray (epicsUInt8 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, &UA_TYPES[UA_TYPES_BYTE], prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementOpen62541::readArray (epicsInt16 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, &UA_TYPES[UA_TYPES_INT16], prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementOpen62541::readArray (epicsUInt16 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, &UA_TYPES[UA_TYPES_UINT16], prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementOpen62541::readArray (epicsInt32 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, &UA_TYPES[UA_TYPES_INT32], prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementOpen62541::readArray (epicsUInt32 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, &UA_TYPES[UA_TYPES_UINT32], prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementOpen62541::readArray (epicsInt64 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, &UA_TYPES[UA_TYPES_INT64], prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementOpen62541::readArray (epicsUInt64 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, &UA_TYPES[UA_TYPES_UINT64], prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementOpen62541::readArray (epicsFloat32 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, &UA_TYPES[UA_TYPES_FLOAT], prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementOpen62541::readArray (epicsFloat64 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, &UA_TYPES[UA_TYPES_DOUBLE], prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementOpen62541::readArray (char *value, const epicsUInt32 len, - const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, len, num, numRead, &UA_TYPES[UA_TYPES_STRING], prec, nextReason, statusCode, statusText, statusTextLen); -} - -inline -void -DataElementOpen62541::dbgWriteScalar () const -{ - if (isLeaf() && debug()) { - std::cout << pconnector->getRecordName() - << ": set outgoing data to value " - << outgoingData << std::endl; - } -} - -long -DataElementOpen62541::writeScalar (const epicsInt32 &value, dbCommon *prec) -{ - return writeScalar(value, prec); -} - -long -DataElementOpen62541::writeScalar (const epicsUInt32 &value, dbCommon *prec) -{ - return writeScalar(value, prec); -} - -long -DataElementOpen62541::writeScalar (const epicsInt64 &value, dbCommon *prec) -{ - return writeScalar(value, prec); -} - -long -DataElementOpen62541::writeScalar (const epicsFloat64 &value, dbCommon *prec) -{ - return writeScalar(value, prec); -} - -long -DataElementOpen62541::writeScalar (const char *value, const epicsUInt32 len, dbCommon *prec) -{ - long ret = 0; - UA_StatusCode status = UA_STATUSCODE_BADUNEXPECTEDERROR; - long l; - unsigned long ul; - double d; - - switch (typeKindOf(incomingData)) { - case UA_DATATYPEKIND_STRING: - { - UA_String val; - val.length = strnlen(value, len); - val.data = const_cast(reinterpret_cast(value)); - { // Scope of Guard G - Guard G(outgoingLock); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_STRING]); - markAsDirty(); - } - break; - } - case UA_DATATYPEKIND_LOCALIZEDTEXT: - { - UA_LocalizedText val; - val.locale = reinterpret_cast(incomingData.data)->locale; - val.text.length = strnlen(value, len); - val.text.data = const_cast(reinterpret_cast(value)); - { // Scope of Guard G - Guard G(outgoingLock); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]); - markAsDirty(); - } - break; - } - case UA_DATATYPEKIND_BOOLEAN: - { // Scope of Guard G - Guard G(outgoingLock); - UA_Boolean val = strchr("YyTt1", *value) != NULL; - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BOOLEAN]); - markAsDirty(); - break; - } - case UA_DATATYPEKIND_BYTE: - ul = strtoul(value, nullptr, 0); - if (isWithinRange(ul)) { - Guard G(outgoingLock); - UA_Byte val = static_cast(ul); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BYTE]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_SBYTE: - l = strtol(value, nullptr, 0); - if (isWithinRange(l)) { - Guard G(outgoingLock); - UA_SByte val = static_cast(l); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_SBYTE]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_UINT16: - ul = strtoul(value, nullptr, 0); - if (isWithinRange(ul)) { - Guard G(outgoingLock); - UA_UInt16 val = static_cast(ul); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT16]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_INT16: - l = strtol(value, nullptr, 0); - if (isWithinRange(l)) { - Guard G(outgoingLock); - UA_Int16 val = static_cast(l); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT16]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_UINT32: - ul = strtoul(value, nullptr, 0); - if (isWithinRange(ul)) { - Guard G(outgoingLock); - UA_UInt32 val = static_cast(ul); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT32]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_INT32: - l = strtol(value, nullptr, 0); - if (isWithinRange(l)) { - Guard G(outgoingLock); - UA_Int32 val = static_cast(l); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT32]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_UINT64: - ul = strtoul(value, nullptr, 0); - if (isWithinRange(ul)) { - Guard G(outgoingLock); - UA_UInt64 val = static_cast(ul); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT64]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_INT64: - l = strtol(value, nullptr, 0); - if (isWithinRange(l)) { - Guard G(outgoingLock); - UA_Int64 val = static_cast(l); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT64]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_FLOAT: - d = strtod(value, nullptr); - if (isWithinRange(d)) { - Guard G(outgoingLock); - UA_Float val = static_cast(d); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_FLOAT]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_DOUBLE: - { - d = strtod(value, nullptr); - Guard G(outgoingLock); - UA_Double val = static_cast(d); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_DOUBLE]); - markAsDirty(); - break; - } - default: - errlogPrintf("%s : unsupported conversion for outgoing data\n", - prec->name); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - } - if (ret == 0 && UA_STATUS_IS_BAD(status)) { - errlogPrintf("%s : scalar copy failed: %s\n", - prec->name, UA_StatusCode_name(status)); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - dbgWriteScalar(); - return ret; -} - -inline -void -DataElementOpen62541::dbgWriteArray (const epicsUInt32 targetSize, const std::string &targetTypeName) const -{ - if (isLeaf() && debug()) { - std::cout << pconnector->getRecordName() << ": writing array of " - << targetTypeName << "[" << targetSize << "] as " - << variantTypeString(outgoingData) << "["<< outgoingData.arrayLength << "]" - << std::endl; - } -} - -// Write array for EPICS String / UA_String -long -DataElementOpen62541::writeArray (const char **value, const epicsUInt32 len, - const epicsUInt32 num, - const UA_DataType *targetType, - dbCommon *prec) -{ - long ret = 0; - - if (UA_Variant_isScalar(&incomingData)) { - errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } else if (incomingData.type != targetType) { - errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", - prec->name, - variantTypeString(incomingData), - variantTypeString(targetType), - epicsTypeString(*value)); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } else { - UA_String *arr = static_cast(UA_Array_new(num, &UA_TYPES[UA_TYPES_STRING])); - if (!arr) { - errlogPrintf("%s : out of memory\n", prec->name); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - } else { - for (epicsUInt32 i = 0; i < num; i++) { - char *val = nullptr; - const char *pval; - // add zero termination if necessary - if (memchr(value[i], '\0', len) == nullptr) { - val = new char[static_cast(len)+1]; // static_cast to avoid warning C26451 - strncpy(val, value[i], len); - val[len] = '\0'; - pval = val; - } else { - pval = value[i]; - } - arr[i] = UA_STRING_ALLOC(pval); - delete[] val; - } - UA_StatusCode status; - { // Scope of Guard G - Guard G(outgoingLock); - status = UA_Variant_setArrayCopy(&outgoingData, arr, num, targetType); - markAsDirty(); - } - if (UA_STATUS_IS_BAD(status)) { - errlogPrintf("%s : array copy failed: %s\n", - prec->name, UA_StatusCode_name(status)); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } else { - dbgWriteArray(num, epicsTypeString(*value)); - } - } - } - return ret; -} - -long -DataElementOpen62541::writeArray (const epicsInt8 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, &UA_TYPES[UA_TYPES_SBYTE], prec); -} - -long -DataElementOpen62541::writeArray (const epicsUInt8 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, &UA_TYPES[UA_TYPES_SBYTE], prec); -} - -long -DataElementOpen62541::writeArray (const epicsInt16 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, &UA_TYPES[UA_TYPES_INT16], prec); -} - -long -DataElementOpen62541::writeArray (const epicsUInt16 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, &UA_TYPES[UA_TYPES_UINT16], prec); -} - -long -DataElementOpen62541::writeArray (const epicsInt32 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, &UA_TYPES[UA_TYPES_INT32], prec); -} - -long -DataElementOpen62541::writeArray (const epicsUInt32 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, &UA_TYPES[UA_TYPES_UINT32], prec); -} - -long -DataElementOpen62541::writeArray (const epicsInt64 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, &UA_TYPES[UA_TYPES_INT64], prec); -} - -long -DataElementOpen62541::writeArray (const epicsUInt64 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, &UA_TYPES[UA_TYPES_UINT64], prec); -} - -long -DataElementOpen62541::writeArray (const epicsFloat32 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, &UA_TYPES[UA_TYPES_FLOAT], prec); -} - -long -DataElementOpen62541::writeArray (const epicsFloat64 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, &UA_TYPES[UA_TYPES_DOUBLE], prec); -} - -long -DataElementOpen62541::writeArray (const char *value, const epicsUInt32 len, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(&value, len, num, &UA_TYPES[UA_TYPES_STRING], prec); -} - -void -DataElementOpen62541::requestRecordProcessing (const ProcessReason reason) const -{ - if (isLeaf()) { - pconnector->requestRecordProcessing(reason); - } else { - for (auto &it : elementMap) { - auto pelem = it.second.lock(); - pelem->requestRecordProcessing(reason); - } - } -} - -} // namespace DevOpcua diff --git a/devOpcuaSup/open62541/DataElementOpen62541.h b/devOpcuaSup/open62541/DataElementOpen62541.h index 979f1068..900b39db 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.h +++ b/devOpcuaSup/open62541/DataElementOpen62541.h @@ -13,17 +13,9 @@ #ifndef DEVOPCUA_DATAELEMENTOPEN62541_H #define DEVOPCUA_DATAELEMENTOPEN62541_H -#include "DataElement.h" -#include "RecordConnector.h" #include "Update.h" -#include "UpdateQueue.h" #include "ItemOpen62541.h" -#include -#include -#include -#include - #include #ifndef UA_STATUSCODE_BAD // Not yet defined in open62541 version 1.2 #define UA_STATUSCODE_BAD 0x80000000 @@ -36,26 +28,11 @@ #define UA_STATUS_IS_UNCERTAIN(status) (((status)&UA_STATUSCODE_UNCERTAIN)!=0) #include -#include -#include -#include namespace DevOpcua { typedef Update UpdateOpen62541; -inline const char *epicsTypeString (const epicsInt8 &) { return "epicsInt8"; } -inline const char *epicsTypeString (const epicsUInt8 &) { return "epicsUInt8"; } -inline const char *epicsTypeString (const epicsInt16 &) { return "epicsInt16"; } -inline const char *epicsTypeString (const epicsUInt16 &) { return "epicsUInt16"; } -inline const char *epicsTypeString (const epicsInt32 &) { return "epicsInt32"; } -inline const char *epicsTypeString (const epicsUInt32 &) { return "epicsUInt32"; } -inline const char *epicsTypeString (const epicsInt64 &) { return "epicsInt64"; } -inline const char *epicsTypeString (const epicsUInt64 &) { return "epicsUInt64"; } -inline const char *epicsTypeString (const epicsFloat32 &) { return "epicsFloat32"; } -inline const char *epicsTypeString (const epicsFloat64 &) { return "epicsFloat64"; } -inline const char *epicsTypeString (const char*) { return "epicsString"; } - inline const char * variantTypeString (const UA_DataType *type) { @@ -77,222 +54,35 @@ inline int typeKindOf(const UA_Variant& v) return typeKindOf(v.type); } -// Template for range check when writing -template -inline bool isWithinRange (const FROM &value) { - return !(value < std::numeric_limits::lowest() || value > std::numeric_limits::max()); -} - -// Specializations for unsigned to signed to avoid compiler warnings -template<> -inline bool isWithinRange (const unsigned int &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const unsigned int &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const unsigned int &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const unsigned long &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const unsigned long &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const unsigned long &value) { - return !(value > std::numeric_limits::max()); -} - -template<> -inline bool isWithinRange (const unsigned long long &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const unsigned long long &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const unsigned long long &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const unsigned long long &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -// Simple-check specializations for converting signed to unsigned of same or wider type -template<> inline bool isWithinRange (const char &value) { return !(value < 0); } -template<> inline bool isWithinRange (const signed char &value) { return !(value < 0); } -template<> inline bool isWithinRange (const short &value) { return !(value < 0); } -template<> inline bool isWithinRange (const int &value) { return !(value < 0); } -template<> inline bool isWithinRange (const char &value) { return !(value < 0); } -template<> inline bool isWithinRange (const short &value) { return !(value < 0); } -template<> inline bool isWithinRange (const int &value) { return !(value < 0); } -template<> inline bool isWithinRange (const long &value) { return !(value < 0); } -template<> inline bool isWithinRange (const char &value) { return !(value < 0); } -template<> inline bool isWithinRange (const signed char &value) { return !(value < 0); } -template<> inline bool isWithinRange (const short &value) { return !(value < 0); } -template<> inline bool isWithinRange (const int &value) { return !(value < 0); } -template<> inline bool isWithinRange (const long &value) { return !(value < 0); } -template<> inline bool isWithinRange (const long long &value) { return !(value < 0); } - -// No-check-needed specializations for converting to same or wider type -template<> inline bool isWithinRange (const int &) { return true; } -template<> inline bool isWithinRange (const int &) { return true; } -template<> inline bool isWithinRange (const int &) { return true; } -template<> inline bool isWithinRange (const int &) { return true; } -template<> inline bool isWithinRange (const int &) { return true; } - -template<> inline bool isWithinRange (const unsigned int &) { return true; } -template<> inline bool isWithinRange (const unsigned int &) { return true; } -template<> inline bool isWithinRange (const unsigned int &) { return true; } -template<> inline bool isWithinRange (const unsigned int &) { return true; } -template<> inline bool isWithinRange (const unsigned int &) { return true; } -template<> inline bool isWithinRange (const unsigned int &) { return true; } - -template<> inline bool isWithinRange (const long &) { return true; } -template<> inline bool isWithinRange (const long &) { return true; } - -template<> inline bool isWithinRange (const long long &) { return true; } -template<> inline bool isWithinRange (const long long &) { return true; } -template<> inline bool isWithinRange (const long long &) { return true; } - -template<> inline bool isWithinRange (const double &) { return true; } - -// long long may or may not be the same size as long -template<> -inline bool isWithinRange (const long long &value) { - return sizeof(long long) > sizeof(long) ? !(value < 0 || value > static_cast(std::numeric_limits::max())) : !(value < 0); -} - -// Helper function to convert strings to numeric types - -inline bool string_to(const std::string& s, epicsInt32& value) { - try { - value = std::stol(s, 0, 0); - return true; - } catch (...) { - return false; - } -} - -inline bool string_to(const std::string& s, epicsInt64& value) { - try { - value = std::stoll(s, 0, 0); - return true; - } catch (...) { - return false; - } -} - -inline bool string_to(const std::string& s, epicsUInt32& value) { - try { - long long v = std::stoll(s, 0, 0); - if (!isWithinRange(v)) return false; - value = static_cast(v); - return true; - } catch (...) { - return false; - } -} - -inline bool string_to(const std::string& s, epicsFloat64& value) { - try { - value = std::stod(s, 0); - return true; - } catch (...) { - return false; - } -} - /** * @brief The DataElementOpen62541 implementation of a single piece of data. * * See DevOpcua::DataElement */ -class DataElementOpen62541 : public DataElement +class DataElementOpen62541 { -public: - /** - * @brief Constructor for DataElement from record connector. - * - * Creates the final (leaf) element of the data structure. The record connector - * holds a shared pointer to its leaf, while the data element has a weak pointer - * to the record connector. - * - * @param name name of the element (empty for root element) - * @param pitem pointer to corresponding ItemOpen62541 - * @param pconnector pointer to record connector to link to - */ - DataElementOpen62541(const std::string &name, - ItemOpen62541 *pitem, - RecordConnector *pconnector); + friend class DataElementOpen62541Node; + friend class DataElementOpen62541Leaf; - /** - * @brief Constructor for DataElement from child element. - * - * Creates an intermediate (node) element of the data structure. The child holds - * a shared pointer, while the parent has a weak pointer in its list/map of child - * nodes, to facilitate traversing the structure when data updates come in. - * - * @param name name of the element - * @param item pointer to corresponding ItemOpen62541 - */ - DataElementOpen62541(const std::string &name, - ItemOpen62541 *item); - - /** - * @brief Create a DataElement and add it to the item's dataTree. - * - * @param item item to add the element to - * @param pconnector pointer to the leaf's record connector - * @param elementPath full path to the element - */ - static void addElementToTree(ItemOpen62541 *item, - RecordConnector *pconnector, - const std::list &elementPath); +public: + DataElementOpen62541 (const std::string &name, class ItemOpen62541 *pitem) + : name(name) + , pitem(pitem) + , outgoingLock(pitem->dataTreeWriteLock) + {} + virtual ~DataElementOpen62541(); /* ElementTree node interface methods */ - void - addChild(std::weak_ptr elem) - { - elements.push_back(elem); - } - - std::shared_ptr - findChild(const std::string &name) - { - for (auto it : elements) - if (auto pit = it.lock()) - if (pit->name == name) - return pit; - return std::shared_ptr(); - } - - void - setParent(std::shared_ptr elem) - { - parent = elem; - } + void setParent (std::shared_ptr elem) { parent = elem; } + virtual bool isLeaf() const = 0; + virtual void addChild(std::weak_ptr elem) = 0; + virtual std::shared_ptr findChild(const std::string &name) const = 0; /** - * @brief Print configuration and status. See DevOpcua::DataElement::show + * @brief Print configuration and status. + * See DevOpcua::DataElement::show */ - void show(const int level, const unsigned int indent) const override; + virtual void show(const int level, const unsigned int indent) const = 0; /** * @brief Push an incoming data value into the DataElement. @@ -304,9 +94,8 @@ class DataElementOpen62541 : public DataElement * @param reason reason for this value update * @param timefrom name of element to read item timestamp from */ - void setIncomingData(const UA_Variant &value, - ProcessReason reason, - const std::string *timefrom = nullptr); + virtual void setIncomingData(const UA_Variant &value, ProcessReason reason, const std::string *timefrom = nullptr) + = 0; /** * @brief Push an incoming event into the DataElement. @@ -316,12 +105,12 @@ class DataElementOpen62541 : public DataElement * * @param reason reason for this value update */ - void setIncomingEvent(ProcessReason reason); + virtual void setIncomingEvent(ProcessReason reason) = 0; /** * @brief Set the EPICS-related state of the element and all sub-elements. */ - void setState(const ConnectionStatus state); + virtual void setState(const ConnectionStatus state) = 0; /** * @brief Get the outgoing data value from the DataElement. @@ -331,384 +120,8 @@ class DataElementOpen62541 : public DataElement * * @return reference to outgoing data */ - const UA_Variant &getOutgoingData(); - - /** - * @brief Read incoming data as a scalar epicsInt32. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readScalar(epicsInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readScalar(epicsInt32 *value, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as a scalar epicsInt64. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readScalar(epicsInt64*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readScalar(epicsInt64 *value, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as a scalar epicsUInt32. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readScalar(epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readScalar(epicsUInt32 *value, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as a scalar epicsFloat64. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readScalar(epicsFloat64*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readScalar(epicsFloat64 *value, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as classic C string (char[]). - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readScalar(char*,const size_t,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - - virtual long int readScalar(char *value, const size_t num, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsInt8. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsInt8*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsInt8 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsUInt8. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsUInt8*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsUInt8 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsInt16. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsInt16*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsInt16 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsUInt16. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsUInt16*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsUInt16 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsInt32. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsInt32*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsInt32 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsUInt32. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsUInt32*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsUInt32 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsInt64. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsInt64*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsInt64 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsUInt64. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsUInt64*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsUInt64 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsFloat32. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsFloat32*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsFloat32 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsFloat64. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsFloat64*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsFloat64 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of EPICS String (char[40]). - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(char*,const epicsUInt32,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(char *value, const epicsUInt32 len, - const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + virtual const UA_Variant &getOutgoingData() = 0; - /** - * @brief Write outgoing scalar epicsInt32 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeScalar(const epicsInt32&,dbCommon*) - */ - virtual long int writeScalar(const epicsInt32 &value, - dbCommon *prec) override; - - /** - * @brief Write outgoing scalar epicsInt64 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeScalar(const epicsInt64&,dbCommon*) - */ - virtual long int writeScalar(const epicsInt64 &value, - dbCommon *prec) override; - - /** - * @brief Write outgoing scalar epicsUInt32 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeScalar(const epicsUInt32&,dbCommon*) - */ - virtual long int writeScalar(const epicsUInt32 &value, - dbCommon *prec) override; - - /** - * @brief Write outgoing scalar epicsFloat64 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeScalar(const epicsFloat64&,dbCommon*) - */ - virtual long int writeScalar(const epicsFloat64 &value, - dbCommon *prec) override; - - /** - * @brief Write outgoing classic C string (char[]) data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeScalar(const char*,const epicsUInt32,dbCommon*) - */ - virtual long int writeScalar(const char *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsInt8 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsInt8*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsInt8 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsUInt8 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsUInt8*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsUInt8 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsInt16 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsInt16*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsInt16 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsUInt16 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsUInt16*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsUInt16 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsInt32 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsInt32*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsInt32 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsUInt32 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsUInt32*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsUInt32 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsInt64 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsInt64*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsInt64 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsUInt64 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsUInt64*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsUInt64 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsFloat32 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsFloat32*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsFloat32 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsFloat64 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsFloat64*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsFloat64 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of EPICS String (char[MAX_STRING_SIZE]) data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const char*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const char *value, const epicsUInt32 len, - const epicsUInt32 num, - dbCommon *prec) override; /** * @brief Clear (discard) the current outgoing data. @@ -720,561 +133,79 @@ class DataElementOpen62541 : public DataElement * oldest element from the queue, allowing access to the next element * with the next send. */ - virtual void clearOutgoingData() { UA_Variant_clear(&outgoingData); } + virtual void clearOutgoingData() = 0; + + /** + * @brief Move the contents of the current outgoing data. + * + * Avoids a deep copy by moving the contents out of the outgoing data + * and clearing it afterwards. + * + * Call holding outgoingLock! + */ + void *moveOutgoingData () + { + void* data = outgoingData.data; + outgoingData.data = nullptr; + UA_Variant_clear(&outgoingData); + return data; + } /** * @brief Create processing requests for record(s) attached to this element. * See DevOpcua::DataElement::requestRecordProcessing */ - virtual void requestRecordProcessing(const ProcessReason reason) const override; + virtual void requestRecordProcessing(const ProcessReason reason) const = 0; /** * @brief Get debug level from record (via RecordConnector). * @return debug level */ - int debug() const { return (isLeaf() ? pconnector->debug() : pitem->debug()); } - -private: - void dbgWriteScalar () const; - void dbgReadScalar(const UpdateOpen62541 *upd, - const std::string &targetTypeName, - const size_t targetSize = 0) const; - void dbgReadArray(const UpdateOpen62541 *upd, - const epicsUInt32 targetSize, - const std::string &targetTypeName) const; - void checkWriteArray(const UA_DataType *expectedType, const std::string &targetTypeName) const; - void dbgWriteArray(const epicsUInt32 targetSize, const std::string &targetTypeName) const; - bool updateDataInStruct(void* container, std::shared_ptr pelem); - - bool createMap(const UA_DataType *type, const std::string* timefrom); - - // Structure always returns true to ensure full traversal - bool isDirty() const { return isdirty || !isleaf; } - void - markAsDirty() - { - isdirty = true; - pitem->markAsDirty(); - } - - // Get the time stamp from the incoming object - const epicsTime &getIncomingTimeStamp() const { - ProcessReason reason = pitem->getReason(); - if ((reason == ProcessReason::incomingData || reason == ProcessReason::readComplete) - && isLeaf()) - switch (pconnector->plinkinfo->timestamp) { - case LinkOptionTimestamp::server: - return pitem->tsServer; - case LinkOptionTimestamp::source: - return pitem->tsSource; - case LinkOptionTimestamp::data: - return pitem->tsData; - } - return pitem->tsClient; - } - - // Get the read status from the incoming object - UA_StatusCode getIncomingReadStatus() const { return pitem->getLastStatus(); } - - // Read scalar value as templated function on EPICS type - // value == nullptr is allowed and leads to the value being dropped (ignored), - // including the extended status - template - long - readScalar (ET *value, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) - { - long ret = 0; - - if (incomingQueue.empty()) { - errlogPrintf("%s: incoming data queue empty\n", prec->name); - if (nextReason) - *nextReason = ProcessReason::none; - return 1; - } - - ProcessReason nReason; - std::shared_ptr upd = incomingQueue.popUpdate(&nReason); - dbgReadScalar(upd.get(), epicsTypeString(*value)); - - switch (upd->getType()) { - case ProcessReason::readFailure: - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::connectionLoss: - (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::incomingData: - case ProcessReason::readComplete: - { - if (value) { - UA_StatusCode stat = upd->getStatus(); - if (UA_STATUS_IS_BAD(stat)) { - // No valid OPC UA value - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - // Valid OPC UA value, so try to convert - UA_Variant &data = upd->getData(); - switch(typeKindOf(data)) { - case UA_DATATYPEKIND_BOOLEAN: - *value = (*static_cast(data.data) != 0); - break; - case UA_DATATYPEKIND_BYTE: - if (isWithinRange(*static_cast(data.data))) - *value = *static_cast(data.data); - else - ret = 1; - break; - case UA_DATATYPEKIND_SBYTE: - if (isWithinRange(*static_cast(data.data))) - *value = *static_cast(data.data); - else - ret = 1; - break; - case UA_DATATYPEKIND_INT16: - if (isWithinRange(*static_cast(data.data))) - *value = static_cast(*static_cast(data.data)); - else - ret = 1; - break; - case UA_DATATYPEKIND_UINT16: - if (isWithinRange(*static_cast(data.data))) - *value = static_cast(*static_cast(data.data)); - else - ret = 1; - break; - case UA_DATATYPEKIND_INT32: - if (isWithinRange(*static_cast(data.data))) - *value = static_cast(*static_cast(data.data)); - else - ret = 1; - break; - case UA_DATATYPEKIND_UINT32: - if (isWithinRange(*static_cast(data.data))) - *value = static_cast(*static_cast(data.data)); - else - ret = 1; - break; - case UA_DATATYPEKIND_INT64: - if (isWithinRange(*static_cast(data.data))) - *value = static_cast(*static_cast(data.data)); - else - ret = 1; - break; - case UA_DATATYPEKIND_UINT64: - if (isWithinRange(*static_cast(data.data))) - *value = static_cast(*static_cast(data.data)); - else - ret = 1; - break; - case UA_DATATYPEKIND_FLOAT: - if (isWithinRange(*static_cast(data.data))) - *value = static_cast(*static_cast(data.data)); - else - ret = 1; - break; - case UA_DATATYPEKIND_DOUBLE: - if (isWithinRange(*static_cast(data.data))) - *value = static_cast(*static_cast(data.data)); - else - ret = 1; - break; - case UA_DATATYPEKIND_STRING: - { - UA_String* s = static_cast(data.data); - if (!string_to(std::string(reinterpret_cast(s->data), s->length), *value)) - ret = 1; - break; - } - case UA_DATATYPEKIND_LOCALIZEDTEXT: - { - UA_LocalizedText* lt = static_cast(data.data); - if (!string_to(std::string(reinterpret_cast(lt->text.data), lt->text.length), *value)) - ret = 1; - break; - } - default: - ret = 1; - } - if (ret == 1) { - UA_String datastring = UA_STRING_NULL; - if (data.type) - UA_print(data.data, data.type, &datastring); - errlogPrintf("%s : incoming data (%s %.*s) out-of-bounds for %s\n", - prec->name, - variantTypeString(data), - static_cast(datastring.length), datastring.data, - epicsTypeString(*value)); - UA_String_clear(&datastring); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - } else { - if (UA_STATUS_IS_UNCERTAIN(stat)) { - (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); - } - prec->udf = false; - } - UA_Variant_clear(&data); - } - if (statusCode) *statusCode = stat; - if (statusText) { - strncpy(statusText, UA_StatusCode_name(stat), statusTextLen); - statusText[statusTextLen-1] = '\0'; - } - } - break; - } - default: - break; - } - - prec->time = upd->getTimeStamp(); - if (nextReason) *nextReason = nReason; - return ret; - } - - // Read array value as templated function on EPICS type - // CAVEAT: changes must also be reflected in specializations (in DataElementOpen62541.cpp) - template - long - readArray (ET *value, const epicsUInt32 num, - epicsUInt32 *numRead, - const UA_DataType *expectedType, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) - { - long ret = 0; - epicsUInt32 elemsWritten = 0; - - if (incomingQueue.empty()) { - errlogPrintf("%s : incoming data queue empty\n", prec->name); - *numRead = 0; - return 1; - } - - ProcessReason nReason; - std::shared_ptr upd = incomingQueue.popUpdate(&nReason); - dbgReadArray(upd.get(), num, epicsTypeString(*value)); - - switch (upd->getType()) { - case ProcessReason::readFailure: - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::connectionLoss: - (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::incomingData: - case ProcessReason::readComplete: - { - if (num && value) { - UA_StatusCode stat = upd->getStatus(); - if (UA_STATUS_IS_BAD(stat)) { - // No valid OPC UA value - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - // Valid OPC UA value, so try to convert - UA_Variant &data = upd->getData(); - if (UA_Variant_isScalar(&data)) { - errlogPrintf("%s : incoming data is not an array\n", prec->name); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else if (data.type != expectedType) { - errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", - prec->name, variantTypeString(data), epicsTypeString(*value)); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - if (UA_STATUS_IS_UNCERTAIN(stat)) { - (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); - } - elemsWritten = static_cast(num) < data.arrayLength ? num : static_cast(data.arrayLength); - memcpy(value, data.data, sizeof(ET) * elemsWritten); - prec->udf = false; - } - UA_Variant_clear(&data); - } - if (statusCode) *statusCode = stat; - if (statusText) { - strncpy(statusText, UA_StatusCode_name(stat), statusTextLen); - statusText[statusTextLen-1] = '\0'; - } - } - break; - } - default: - break; - } - - prec->time = upd->getTimeStamp(); - if (nextReason) *nextReason = nReason; - if (num && value) - *numRead = elemsWritten; - return ret; - } - - // Read array value for EPICS String / UA_String - long - readArray (char *value, const epicsUInt32 len, - const epicsUInt32 num, - epicsUInt32 *numRead, - const UA_DataType *expectedType, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen); - - // Write scalar value as templated function on EPICS type - template - long - writeScalar (const ET &value, - dbCommon *prec) - { - long ret = 0; - UA_StatusCode status = UA_STATUSCODE_BADUNEXPECTEDERROR; - - switch (typeKindOf(incomingData)) { - case UA_DATATYPEKIND_BOOLEAN: - { // Scope of Guard G - Guard G(outgoingLock); - UA_Boolean val = (value != 0); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BOOLEAN]); - markAsDirty(); - break; - } - case UA_DATATYPEKIND_BYTE: - if (isWithinRange(value)) { - Guard G(outgoingLock); - UA_Byte val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BYTE]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_SBYTE: - if (isWithinRange(value)) { - Guard G(outgoingLock); - UA_SByte val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_SBYTE]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_UINT16: - if (isWithinRange(value)) { - Guard G(outgoingLock); - UA_UInt16 val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT16]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_INT16: - if (isWithinRange(value)) { - Guard G(outgoingLock); - UA_Int16 val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT16]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_UINT32: - if (isWithinRange(value)) { - Guard G(outgoingLock); - UA_UInt32 val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT32]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_INT32: - if (isWithinRange(value)) { - Guard G(outgoingLock); - UA_Int32 val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT32]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_UINT64: - if (isWithinRange(value)) { - Guard G(outgoingLock); - UA_UInt64 val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT64]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_INT64: - if (isWithinRange(value)) { - Guard G(outgoingLock); - UA_Int64 val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT64]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_FLOAT: - if (isWithinRange(value)) { - Guard G(outgoingLock); - UA_Float val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_FLOAT]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_DOUBLE: - if (isWithinRange(value)) { - Guard G(outgoingLock); - UA_Double val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_DOUBLE]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_STRING: - { - std::string strval = std::to_string(value); - UA_String val; - val.length = strval.length(); - val.data = const_cast(reinterpret_cast(strval.c_str())); - { // Scope of Guard G - Guard G(outgoingLock); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_STRING]); - markAsDirty(); - } - break; - } - case UA_DATATYPEKIND_LOCALIZEDTEXT: - { - std::string strval = std::to_string(value); - UA_LocalizedText val; - val.locale = reinterpret_cast(incomingData.data)->locale; - val.text.length = strval.length(); - val.text.data = const_cast(reinterpret_cast(strval.c_str())); - { // Scope of Guard G - Guard G(outgoingLock); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]); - markAsDirty(); - } - break; - } - default: - ret = 1; - errlogPrintf("%s : unsupported conversion for outgoing data\n", - prec->name); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - } - if (ret == 0 && UA_STATUS_IS_BAD(status)) { - errlogPrintf("%s : scalar copy failed: %s\n", - prec->name, UA_StatusCode_name(status)); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - - } - dbgWriteScalar(); - return ret; - } + virtual int debug() const { return pitem->debug(); } - // Write array value for EPICS String / UA_String - long - writeArray (const char **value, const epicsUInt32 len, - const epicsUInt32 num, - const UA_DataType *targetType, - dbCommon *prec); + const std::string name; - // Write array value as templated function on EPICS type, OPC UA container and simple (element) types - // (latter *must match* OPC UA type enum argument) - // CAVEAT: changes must also be reflected in the specialization (in DataElementOpen62541.cpp) - template - long - writeArray (const ET *value, const epicsUInt32 num, - const UA_DataType *targetType, - dbCommon *prec) - { - long ret = 0; - - if (UA_Variant_isScalar(&incomingData)) { - errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } else if (incomingData.type != targetType) { - errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", - prec->name, - variantTypeString(incomingData), - variantTypeString(targetType), - epicsTypeString(*value)); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } else { - UA_StatusCode status; - { // Scope of Guard G - Guard G(outgoingLock); - status = UA_Variant_setArrayCopy(&outgoingData, value, num, targetType); - markAsDirty(); - } - if (UA_STATUS_IS_BAD(status)) { - errlogPrintf("%s : array copy failed: %s\n", - prec->name, UA_StatusCode_name(status)); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } else { - dbgWriteArray(num, epicsTypeString(*value)); - } - } - return ret; - } +private: + virtual bool isDirty() const = 0; + virtual void markAsDirty() = 0; +protected: ItemOpen62541 *pitem; /**< corresponding item */ - std::vector> elements; /**< children (if node) */ std::shared_ptr parent; /**< parent */ - std::unordered_map> elementMap; - ptrdiff_t timesrc; - - const UA_DataType *memberType; /**< type of this element */ - UA_Boolean isArray; /**< is this element an array? */ - size_t offset; /**< data offset of this element in parent structure */ - bool mapped; /**< child name to index mapping done */ - UpdateQueue incomingQueue; /**< queue of incoming values */ UA_Variant incomingData; /**< cache of latest incoming value */ epicsMutex &outgoingLock; /**< data lock for outgoing value */ UA_Variant outgoingData; /**< cache of latest outgoing value */ bool isdirty; /**< outgoing value has been (or needs to be) updated */ + + const UA_DataType *memberType = nullptr; /**< type of this element */ + UA_Boolean isArray = false; /**< is this element an array? */ + UA_Boolean isOptional = false; /**< is this element optional? */ + size_t offset = 0; /**< data offset of this element in parent structure */ + UA_UInt32 index = 0; /**< element index (for unions) */ + + friend std::ostream& operator << (std::ostream& os, const DataElementOpen62541& element); }; +// print the full element name, e.g.: item elem.array[index].elem +inline std::ostream& operator << (std::ostream& os, const DataElementOpen62541& element) +{ + // Skip the [ROOT] element in printing and print the item name instead + if (!element.parent) + return os << element.pitem; + os << element.parent; + if (!element.parent->parent) + os << " element "; + else if (element.name[0] != '[') + os << '.'; + return os << element.name; +} + +inline std::ostream& operator << (std::ostream& os, const DataElementOpen62541* pelement) +{ + return os << *pelement; +} + } // namespace DevOpcua #endif // DEVOPCUA_DATAELEMENTOPEN62541_H diff --git a/devOpcuaSup/open62541/DataElementOpen62541Leaf.cpp b/devOpcuaSup/open62541/DataElementOpen62541Leaf.cpp new file mode 100644 index 00000000..1b223c01 --- /dev/null +++ b/devOpcuaSup/open62541/DataElementOpen62541Leaf.cpp @@ -0,0 +1,1352 @@ +/*************************************************************************\ +* Copyright (c) 2018-2026 ITER Organization. +* This module is distributed subject to a Software License Agreement found +* in file LICENSE that is included with this distribution. +\*************************************************************************/ + +/* + * Author: Dirk Zimoch + * + * based on the UaSdk implementation by Ralph Lange + */ + +#define epicsExportSharedSymbols +#include "DataElementOpen62541Leaf.h" +#include "DataElementOpen62541Node.h" +#include "ItemOpen62541.h" +#include "RecordConnector.h" + +#include +#include + +#include + +#include +#include +#include +#include + +namespace DevOpcua { + +DataElementOpen62541Leaf::DataElementOpen62541Leaf (const std::string &name, + ItemOpen62541 *item, + RecordConnector *pconnector) + : DataElementOpen62541(name, item) + , incomingQueue(pconnector->plinkinfo->clientQueueSize, pconnector->plinkinfo->discardOldest) +{ + UA_Variant_init(&incomingData); + UA_Variant_init(&outgoingData); + item->dataTreeNoOfLeafs++; +} + +DataElementOpen62541Leaf::~DataElementOpen62541Leaf() +{ + delete enumChoices; + pitem->dataTreeNoOfLeafs--; +} + +/* Explicitly implement the destructor here (allows the compiler to place the vtable) */ +DataElementOpen62541::~DataElementOpen62541() = default; + +/* Specific implementation of DataElement's "factory" method */ +void +DataElement::addElementToTree (Item *item, RecordConnector *pconnector, const std::list &elementPath) +{ + DataElementOpen62541Leaf::addElementToTree(static_cast(item), pconnector, elementPath); +} + +void +DataElementOpen62541Leaf::addElementToTree (ItemOpen62541 *item, + RecordConnector *pconnector, + const std::list &elementPath) +{ + std::string name("[ROOT]"); + if (elementPath.size()) + name = elementPath.back(); + + auto leaf = std::make_shared(name, item, pconnector); + item->dataTree.addLeaf(leaf, elementPath, item); + // reference from connector after adding to the tree worked + pconnector->setDataElement(leaf); + leaf->pconnector = pconnector; +} + +void +DataElementOpen62541Leaf::show (const int level, const unsigned int indent) const +{ + std::string ind(static_cast(indent) * 2, ' '); // static_cast to avoid warning C26451 + std::cout << ind; + std::cout << "leaf=" << name << " record(" << pconnector->getRecordType() << ")=" << pconnector->getRecordName() + << " type=" << variantTypeString(incomingData) + << " timestamp=" << linkOptionTimestampString(pconnector->plinkinfo->timestamp) + << " bini=" << linkOptionBiniString(pconnector->plinkinfo->bini) + << " monitor=" << (pconnector->plinkinfo->monitor ? "y" : "n") << "\n"; +} + +#ifndef UA_ENABLE_TYPEDESCRIPTION +// Without type description, we cannot get struct members +#error Set UA_ENABLE_TYPEDESCRIPTION in open62541 +#endif + +// Older open62541 version 1.2 has no UA_DataType_getStructMember +// Code from open62541 version 1.3.7 modified for compatibility with version 1.2 +// and extended to return isOptional flag and index for union + +inline const UA_DataType* +memberTypeOf(const UA_DataType *type, const UA_DataTypeMember *m) { +#ifdef UA_DATATYPES_USE_POINTER + return m->memberType; +#else + const UA_DataType *typelists[2] = { UA_TYPES, &type[-type->typeIndex] }; + return &typelists[!m->namespaceZero][m->memberTypeIndex]; +#endif +} + +// Getting the timestamp and status information from the Item assumes that only one thread +// is pushing data into the Item's DataElement structure at any time. +void +DataElementOpen62541Leaf::setIncomingData (const UA_Variant &value, ProcessReason reason, const std::string *timefrom) +{ + // Cache this element. We can make a shallow copy because + // ItemOpen62541::setIncomingData marks the original response data as "ours". + // Member data is owned by the [ROOT] element. + UA_Variant_clear(&incomingData); + incomingData = value; + + if (pconnector->state() == ConnectionStatus::initialRead && typeKindOf(value) == UA_DATATYPEKIND_ENUM) { + enumChoices = pitem->session->getEnumChoices(&value.type->typeId); + } + if ((pconnector->state() == ConnectionStatus::initialRead + && (reason == ProcessReason::readComplete || reason == ProcessReason::readFailure)) + || (pconnector->state() == ConnectionStatus::up)) { + Guard(pconnector->lock); + bool wasFirst = false; + // Make a copy of the value for this element and put it on the queue + UA_Variant *valuecopy(new UA_Variant); + UA_Variant_copy(&value, valuecopy); // As a non-C++ object, UA_Variant has no copy constructor + UpdateOpen62541 *u(new UpdateOpen62541( + getIncomingTimeStamp(), reason, std::unique_ptr(valuecopy), getIncomingReadStatus())); + incomingQueue.pushUpdate(std::shared_ptr(u), &wasFirst); + if (debug() >= 5) + std::cout << "Item " << pitem << " element " << name << " set data (" << processReasonString(reason) + << ") for record " << pconnector->getRecordName() << " (queue use " << incomingQueue.size() << "/" + << incomingQueue.capacity() << ")" << std::endl; + if (wasFirst) + pconnector->requestRecordProcessing(reason); + } +} + +void +DataElementOpen62541Leaf::setIncomingEvent (ProcessReason reason) +{ + Guard(pconnector->lock); + if (reason == ProcessReason::connectionLoss && enumChoices) + enumChoices = nullptr; + bool wasFirst = false; + // Put the event on the queue + UpdateOpen62541 *u(new UpdateOpen62541(getIncomingTimeStamp(), reason)); + incomingQueue.pushUpdate(std::shared_ptr(u), &wasFirst); + if (debug() >= 5) + std::cout << "Element " << name << " set event (" << processReasonString(reason) << ") for record " + << pconnector->getRecordName() << " (queue use " << incomingQueue.size() << "/" + << incomingQueue.capacity() << ")" << std::endl; + if (wasFirst) + pconnector->requestRecordProcessing(reason); +} + +void +DataElementOpen62541Leaf::setState (const ConnectionStatus state) +{ + Guard(pconnector->lock); + pconnector->setState(state); +} + +const UA_Variant & +DataElementOpen62541Leaf::getOutgoingData () +{ + return outgoingData; +} + +void +DataElementOpen62541Leaf::dbgReadScalar (const UpdateOpen62541 *upd, + const std::string &targetTypeName, + const size_t targetSize) const +{ + if (isLeaf() && debug()) { + char time_buf[40]; + upd->getTimeStamp().strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S.%09f"); + ProcessReason reason = upd->getType(); + + std::cout << pconnector->getRecordName() << ": "; + if (reason == ProcessReason::incomingData || reason == ProcessReason::readComplete) { + UA_Variant &data = upd->getData(); + std::cout << "(" << linkOptionTimestampString(pconnector->plinkinfo->timestamp); + if (pconnector->plinkinfo->timestamp == LinkOptionTimestamp::data) + std::cout << "(@" << pconnector->plinkinfo->timestampElement << ")"; + std::cout << " time " << time_buf << ") read " << processReasonString(reason) << " (" + << UA_StatusCode_name(upd->getStatus()) << ") " + << data + << " as " << targetTypeName; + if (targetSize) + std::cout << "[" << targetSize << "]"; + } else { + std::cout << "(client time "<< time_buf << ") " << processReasonString(reason); + } + std::cout << " --- remaining queue " << incomingQueue.size() + << "/" << incomingQueue.capacity() << std::endl; + } +} + +long +DataElementOpen62541Leaf::readScalar (epicsInt32 *value, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementOpen62541Leaf::readScalar (epicsInt64 *value, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementOpen62541Leaf::readScalar (epicsUInt32 *value, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementOpen62541Leaf::readScalar (epicsFloat64 *value, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); +} + +// Helper functions to convert between UA_ByteString and hex encoded C string +static size_t printByteString(const UA_ByteString& byteString, char* encoded, size_t len) +{ + static const char hexdigits[] = "0123456789ABCDEF"; + size_t i, l = 0; + for (i = 0; i < byteString.length && l < len-3; i++) { + encoded[l++] = hexdigits[byteString.data[i] >> 4]; + encoded[l++] = hexdigits[byteString.data[i] & 0xf]; + } + encoded[l] = 0; + return l; +} + +static int parseByteString(UA_ByteString& byteString, const char* encoded, int len) +{ + UA_ByteString_clear(&byteString); + byteString.data = static_cast(UA_malloc(len+1/2)); // can never be longer + if (!byteString.data) return -1; + int l = 0; + bool firstDigit = true; + while (len--) { + UA_Byte c, b; + c = *encoded++; + if (!c) break; + if (isblank(c)) { + firstDigit = true; + continue; + } + if (!isxdigit(c)) { + // invalid character + UA_ByteString_clear(&byteString); + return -1; + } + b = c - (isdigit(c) ? '0' : isupper(c) ? 'A'-10 : 'a'-10); + if (len && isxdigit(c = *encoded)) { + firstDigit = false; + encoded++; + len--; + b <<= 4; + b |= c - (isdigit(c) ? '0' : isupper(c) ? 'A'-10 : 'a'-10); + } else { + if (!firstDigit || (c && !isblank(c) && !isxdigit(c))) { + // 1 is the only odd number of digits allowed + // because otherwise where is the byte border? 12|3 or 1|23 ? + UA_ByteString_clear(&byteString); + return -1; + } + } + byteString.data[l++] = b; + } + byteString.length = l; + return l; +} + +// CString type needs specialization +long +DataElementOpen62541Leaf::readScalar (char *value, const epicsUInt32 len, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *lenRead, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + long ret = 0; + + if (incomingQueue.empty()) { + errlogPrintf("%s: incoming data queue empty\n", prec->name); + if (nextReason) + *nextReason = ProcessReason::none; + return 1; + } + + ProcessReason nReason; + std::shared_ptr upd = incomingQueue.popUpdate(&nReason); + dbgReadScalar(upd.get(), "CString", len); + + switch (upd->getType()) { + case ProcessReason::readFailure: + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::connectionLoss: + (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::incomingData: + case ProcessReason::readComplete: + { + if (len && value) { + UA_StatusCode stat = upd->getStatus(); + if (UA_STATUS_IS_BAD(stat)) { + // No valid OPC UA value + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + // Valid OPC UA value, so copy over + if (UA_STATUS_IS_UNCERTAIN(stat)) { + (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); + } + + UA_String buffer = UA_STRING_NULL; + UA_String *datastring = &buffer; + size_t n = len-1; + + UA_Variant &variant = upd->getData(); + void* payload = variant.data; + const UA_DataType* type = variant.type; + + if (type->typeKind == UA_DATATYPEKIND_UNION) + { + UA_UInt32 switchfield = *static_cast(payload)-1; + if (switchfield >= type->membersSize) { + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + break; + } + const UA_DataTypeMember *member = &type->members[switchfield]; + payload = static_cast(payload) + member->padding; + type = memberTypeOf(type, member); + + // prefix value string with switch choice name + size_t l = snprintf(value, n, "%s:", member->memberName); + value += l; + n -= l; + } + + switch (type->typeKind) { + case UA_DATATYPEKIND_STRING: + case UA_DATATYPEKIND_XMLELEMENT: + { + datastring = static_cast(payload); + break; + } + case UA_DATATYPEKIND_LOCALIZEDTEXT: + { + datastring = &static_cast(payload)->text; + break; + } + case UA_DATATYPEKIND_QUALIFIEDNAME: + { + datastring = &static_cast(payload)->name; + break; + } + case UA_DATATYPEKIND_BYTESTRING: + { + n = printByteString(*static_cast(payload), value, len); + datastring = nullptr; + break; + } + case UA_DATATYPEKIND_DATETIME: + { + // UA_print does not correct printed time for time zone + UA_Int64 tOffset = UA_DateTime_localTimeUtcOffset(); + UA_DateTime dt = *static_cast(payload); + dt += tOffset; + UA_print(&dt, type, &buffer); + break; + } + case UA_DATATYPEKIND_BYTE: + case UA_DATATYPEKIND_SBYTE: + { + buffer.data = static_cast(payload); + buffer.length = UA_Variant_isScalar(&variant) ? 1 : variant.arrayLength; + variant.storageType = UA_VARIANT_DATA_NODELETE; // we have moved ownership + n++; + break; + } + case UA_DATATYPEKIND_ENUM: + case UA_DATATYPEKIND_INT32: + { + if (enumChoices) { + auto it = enumChoices->find(*static_cast(payload)); + if (it != enumChoices->end()) { + buffer = UA_String_fromChars(it->second.c_str()); + break; + } + } + // no enum or index not found: fall through + } + default: + if (type) + UA_print(payload, type, &buffer); + }; + if (datastring) { + if (n > datastring->length) + n = datastring->length; + memcpy(value, reinterpret_cast(datastring->data), n); + } + memset(value+n, 0, len-n); + if (lenRead) + *lenRead = static_cast(n); + UA_String_clear(&buffer); + prec->udf = false; + UA_Variant_clear(&variant); + } + if (statusCode) *statusCode = stat; + if (statusText) { + strncpy(statusText, UA_StatusCode_name(stat), statusTextLen); + statusText[statusTextLen-1] = '\0'; + } + } + break; + } + default: + break; + } + + prec->time = upd->getTimeStamp(); + if (nextReason) *nextReason = nReason; + return ret; +} + +void +DataElementOpen62541Leaf::dbgReadArray (const UpdateOpen62541 *upd, + const epicsUInt32 targetSize, + const std::string &targetTypeName) const +{ + if (isLeaf() && debug()) { + char time_buf[40]; + upd->getTimeStamp().strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S.%09f"); + ProcessReason reason = upd->getType(); + + std::cout << pconnector->getRecordName() << ": "; + if (reason == ProcessReason::incomingData || reason == ProcessReason::readComplete) { + std::cout << "(" << linkOptionTimestampString(pconnector->plinkinfo->timestamp); + if (pconnector->plinkinfo->timestamp == LinkOptionTimestamp::data) + std::cout << "@" << pconnector->plinkinfo->timestampElement; + std::cout << " time " << time_buf << ") read " << processReasonString(reason) << " (" + << UA_StatusCode_name(upd->getStatus()) << ") "; + UA_Variant &data = upd->getData(); + std::cout << " array of " << variantTypeString(data) + << "[" << upd->getData().arrayLength << "]" + << " into " << targetTypeName << "[" << targetSize << "]"; + } else { + std::cout << "(client time "<< time_buf << ") " << processReasonString(reason); + } + std::cout << " --- remaining queue " << incomingQueue.size() + << "/" << incomingQueue.capacity() << std::endl; + } +} + +// Read array for EPICS String / UA_String +long int +DataElementOpen62541Leaf::readArray (char *value, epicsUInt32 len, + const epicsUInt32 num, + epicsUInt32 *numRead, + const UA_DataType *expectedType, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + long ret = 0; + epicsUInt32 elemsWritten = 0; + + // clear *old* array content + if (value) + memset(value, 0, *numRead * len); + + if (incomingQueue.empty()) { + errlogPrintf("%s : incoming data queue empty\n", prec->name); + *numRead = 0; + return 1; + } + + ProcessReason nReason; + std::shared_ptr upd = incomingQueue.popUpdate(&nReason); + dbgReadArray(upd.get(), num, epicsTypeString(value)); + + switch (upd->getType()) { + case ProcessReason::readFailure: + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::connectionLoss: + (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::incomingData: + case ProcessReason::readComplete: + { + if (num && value) { + UA_StatusCode stat = upd->getStatus(); + if (UA_STATUS_IS_BAD(stat)) { + // No valid OPC UA value + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + // Valid OPC UA value, so try to convert + UA_Variant &variant = upd->getData(); + if (UA_Variant_isScalar(&variant)) { + errlogPrintf("%s : incoming data is not an array\n", prec->name); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + if (UA_STATUS_IS_UNCERTAIN(stat)) { + (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); + } + elemsWritten = static_cast(num < variant.arrayLength ? num : variant.arrayLength); + switch (typeKindOf(variant.type)) { + case UA_DATATYPEKIND_STRING: + case UA_DATATYPEKIND_XMLELEMENT: + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + const UA_String& str = static_cast(variant.data)[i]; + size_t l = str.length; + if (l >= len) l = len-1; + memcpy(value + i * len, str.data, l); + } + break; + case UA_DATATYPEKIND_LOCALIZEDTEXT: + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + const UA_String& str = static_cast(variant.data)[i].text; + size_t l = str.length; + if (l >= len) l = len-1; + memcpy(value + i * len, str.data, l); + } + break; + case UA_DATATYPEKIND_QUALIFIEDNAME: + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + const UA_String& str = static_cast(variant.data)[i].name; + size_t l = str.length; + if (l >= len) l = len-1; + memcpy(value + i * len, str.data, l); + } + break; + case UA_DATATYPEKIND_BYTESTRING: + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + printByteString(static_cast(variant.data)[i], value + i * len, len); + } + break; + default: + errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", + prec->name, variantTypeString(variant), epicsTypeString(value)); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } + prec->udf = false; + } + UA_Variant_clear(&variant); + } + if (statusCode) *statusCode = stat; + if (statusText) { + strncpy(statusText, UA_StatusCode_name(stat), statusTextLen); + statusText[statusTextLen-1] = '\0'; + } + } + break; + } + default: + break; + } + + prec->time = upd->getTimeStamp(); + if (nextReason) *nextReason = nReason; + if (num && value) + *numRead = elemsWritten; + return ret; +} + +// Specialization for epicsUInt8 +// needed because epicsUInt8 is used for arrays of UA_BYTE and UA_BOOLEAN +// CAVEAT: changes in the template (in DataElementOpen62541.h) must be reflected here +template<> +long +DataElementOpen62541Leaf::readArray (epicsUInt8 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + const UA_DataType *expectedType, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + long ret = 0; + epicsUInt32 elemsWritten = 0; + + if (incomingQueue.empty()) { + errlogPrintf("%s : incoming data queue empty\n", prec->name); + *numRead = 0; + return 1; + } + + ProcessReason nReason; + std::shared_ptr upd = incomingQueue.popUpdate(&nReason); + dbgReadArray(upd.get(), num, epicsTypeString(*value)); + + switch (upd->getType()) { + case ProcessReason::readFailure: + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::connectionLoss: + (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::incomingData: + case ProcessReason::readComplete: + { + if (num && value) { + UA_StatusCode stat = upd->getStatus(); + if (UA_STATUS_IS_BAD(stat)) { + // No valid OPC UA value + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + // Valid OPC UA value, so try to convert + UA_Variant &variant = upd->getData(); + if (UA_Variant_isScalar(&variant) && variant.type == &UA_TYPES[UA_TYPES_BYTESTRING]) { + const UA_ByteString& bs = *static_cast(variant.data); + elemsWritten = static_cast(bs.length); + if (elemsWritten > num) + elemsWritten = num; + memcpy(value, bs.data, elemsWritten); + } else if (UA_Variant_isScalar(&variant)) { + errlogPrintf("%s : incoming data is not an array\n", prec->name); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else if (typeKindOf(variant) != UA_DATATYPEKIND_BYTE && typeKindOf(variant) != UA_DATATYPEKIND_BOOLEAN) { + errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", + prec->name, variantTypeString(variant), epicsTypeString(*value)); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + if (UA_STATUS_IS_UNCERTAIN(stat)) { + (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); + } + elemsWritten = static_cast(variant.arrayLength); + if (elemsWritten > num) + elemsWritten = num; + memcpy(value, variant.data, elemsWritten); + prec->udf = false; + } + UA_Variant_clear(&variant); + } + if (statusCode) *statusCode = stat; + if (statusText) { + strncpy(statusText, UA_StatusCode_name(stat), statusTextLen); + statusText[statusTextLen-1] = '\0'; + } + } + break; + } + default: + break; + } + + prec->time = upd->getTimeStamp(); + if (nextReason) *nextReason = nReason; + if (num && value) + *numRead = elemsWritten; + return ret; +} + +long +DataElementOpen62541Leaf::readArray (epicsInt8 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray(value, num, numRead, &UA_TYPES[UA_TYPES_SBYTE], prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementOpen62541Leaf::readArray (epicsUInt8 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray(value, num, numRead, &UA_TYPES[UA_TYPES_BYTE], prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementOpen62541Leaf::readArray (epicsInt16 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray(value, num, numRead, &UA_TYPES[UA_TYPES_INT16], prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementOpen62541Leaf::readArray (epicsUInt16 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray(value, num, numRead, &UA_TYPES[UA_TYPES_UINT16], prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementOpen62541Leaf::readArray (epicsInt32 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray(value, num, numRead, &UA_TYPES[UA_TYPES_INT32], prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementOpen62541Leaf::readArray (epicsUInt32 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray(value, num, numRead, &UA_TYPES[UA_TYPES_UINT32], prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementOpen62541Leaf::readArray (epicsInt64 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray(value, num, numRead, &UA_TYPES[UA_TYPES_INT64], prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementOpen62541Leaf::readArray (epicsUInt64 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray(value, num, numRead, &UA_TYPES[UA_TYPES_UINT64], prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementOpen62541Leaf::readArray (epicsFloat32 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray(value, num, numRead, &UA_TYPES[UA_TYPES_FLOAT], prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementOpen62541Leaf::readArray (epicsFloat64 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray(value, num, numRead, &UA_TYPES[UA_TYPES_DOUBLE], prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementOpen62541Leaf::readArray (char *value, epicsUInt32 len, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray(value, len, num, numRead, &UA_TYPES[UA_TYPES_STRING], prec, nextReason, statusCode, statusText, statusTextLen); +} + +inline +void +DataElementOpen62541Leaf::dbgWriteScalar () const +{ + if (isLeaf() && debug()) { + std::cout << pconnector->getRecordName() + << ": set outgoing data to value " + << outgoingData << std::endl; + } +} + +long +DataElementOpen62541Leaf::writeScalar (const epicsInt32 &value, dbCommon *prec) +{ + return writeScalar(value, prec); +} + +long +DataElementOpen62541Leaf::writeScalar (const epicsUInt32 &value, dbCommon *prec) +{ + return writeScalar(value, prec); +} + +long +DataElementOpen62541Leaf::writeScalar (const epicsInt64 &value, dbCommon *prec) +{ + return writeScalar(value, prec); +} + +long +DataElementOpen62541Leaf::writeScalar (const epicsFloat64 &value, dbCommon *prec) +{ + return writeScalar(value, prec); +} + +long +DataElementOpen62541Leaf::writeScalar (const char *value, epicsUInt32 len, dbCommon *prec) +{ + long ret = 1; + UA_StatusCode status = UA_STATUSCODE_BADUNEXPECTEDERROR; + long l; + unsigned long ul; + double d; + char* end = nullptr; + + { // Scope of Guard G + Guard G(outgoingLock); + UA_Variant_clear(&outgoingData); // unlikely but we may still have unsent old data to discard + const UA_DataType* type = incomingData.type; + + int switchfield = -1; + if (typeKindOf(type) == UA_DATATYPEKIND_UNION) { + if (value[0] == '\0') { + switchfield = 0; + } else for (UA_UInt32 i = 0; i < type->membersSize; i++) { + epicsUInt32 namelen = static_cast(strlen(type->members[i].memberName)); + if (strncmp(value, type->members[i].memberName, namelen) == 0 + && value[namelen] == ':') { + value += namelen+1; + len -= namelen+1; + switchfield = i+1; + type = memberTypeOf(type, &type->members[i]); + } + } + } + + switch (typeKindOf(type)) { + case UA_DATATYPEKIND_STRING: + case UA_DATATYPEKIND_XMLELEMENT: + { + UA_String val; + val.length = strnlen(value, len); + val.data = const_cast(reinterpret_cast(value)); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); + markAsDirty(); + ret = 0; + break; + } + case UA_DATATYPEKIND_LOCALIZEDTEXT: + { + UA_LocalizedText val; + const char* sep = static_cast(memchr(value, '|', len)); + if (sep) { + val.locale.length = sep - value; + val.locale.data = const_cast(reinterpret_cast(value)); + len -= static_cast(sep + 1 - value); + value = sep + 1; + } else { // keep the locale + val.locale = reinterpret_cast(incomingData.data)->locale; + } + val.text.length = strnlen(value, len); + val.text.data = const_cast(reinterpret_cast(value)); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); + markAsDirty(); + ret = 0; + break; + } + case UA_DATATYPEKIND_QUALIFIEDNAME: + { + UA_QualifiedName val; + const char* sep = static_cast(memchr(value, '|', len)); + if (sep) { + val.namespaceIndex = atoi(value); + len -= static_cast(sep + 1 - value); + value = sep + 1; + } else { // keep the namespace + val.namespaceIndex = reinterpret_cast(incomingData.data)->namespaceIndex; + } + val.name.length = strnlen(value, len); + val.name.data = const_cast(reinterpret_cast(value)); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); + markAsDirty(); + ret = 0; + break; + } + case UA_DATATYPEKIND_BYTESTRING: + { + UA_ByteString val; + UA_ByteString_init(&val); + if (parseByteString(val, value, len) >= 0) { + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); + markAsDirty(); + ret = 0; + } + break; + } + case UA_DATATYPEKIND_BOOLEAN: + { + UA_Boolean val = strchr("YyTt1", *value) != NULL; + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); + markAsDirty(); + ret = 0; + break; + } + case UA_DATATYPEKIND_BYTE: + ul = strtoul(value, &end, 0); + if (end != value && isWithinRange(ul)) { + UA_Byte val = static_cast(ul); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_SBYTE: + l = strtol(value, &end, 0); + if (end != value && isWithinRange(l)) { + UA_SByte val = static_cast(l); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_UINT16: + ul = strtoul(value, &end, 0); + if (end != value && isWithinRange(ul)) { + UA_UInt16 val = static_cast(ul); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_INT16: + l = strtol(value, &end, 0); + if (end != value && isWithinRange(l)) { + UA_Int16 val = static_cast(l); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_UINT32: + ul = strtoul(value, &end, 0); + if (end != value && isWithinRange(ul)) { + UA_UInt32 val = static_cast(ul); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_ENUM: + case UA_DATATYPEKIND_INT32: + l = strtol(value, &end, 0); + if (enumChoices) { + // first test enum strings then numeric values + // in case a string starts with a number but means a different value + for (const auto &it: *enumChoices) + if (it.second == value) { + l = static_cast(it.first); + ret = 0; + end = nullptr; + break; + } + if (ret != 0 && end != value) + for (const auto &it: *enumChoices) + if (l == it.first) { + ret = 0; + break; + } + if (ret != 0) + break; + } + if (end != value && isWithinRange(l)) { + UA_Int32 val = static_cast(l); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT32]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_UINT64: + ul = strtoul(value, &end, 0); + if (end != value && isWithinRange(ul)) { + UA_UInt64 val = static_cast(ul); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_INT64: + l = strtol(value, &end, 0); + if (end != value && isWithinRange(l)) { + UA_Int64 val = static_cast(l); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_FLOAT: + d = strtod(value, &end); + if (end != value && isWithinRange(d)) { + UA_Float val = static_cast(d); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_DOUBLE: + { + d = strtod(value, &end); + if (end != value) { + UA_Double val = static_cast(d); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); + } + break; + } + default: + errlogPrintf("%s : unsupported conversion from string to %s for outgoing data\n", + prec->name, variantTypeString(incomingData)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + } + if (switchfield >= 0) { + // manually wrap value from outgoingData into union + void *p = UA_new(type); + if (p) { + *static_cast(p) = switchfield; + if (switchfield > 0) { + memcpy(static_cast(p) + type->members[switchfield-1].padding, + outgoingData.data, outgoingData.type->memSize); + UA_free(outgoingData.data); + } + UA_Variant_setScalar(&outgoingData, p, type); + status = UA_STATUSCODE_GOOD; + markAsDirty(); + ret = 0; + } + } + } // Scope of Guard G + if (ret != 0) { + errlogPrintf("%s : value \"%s\" out of range\n", + prec->name, value); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + } + if (ret == 0 && UA_STATUS_IS_BAD(status)) { + errlogPrintf("%s : scalar copy failed: %s\n", + prec->name, UA_StatusCode_name(status)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + if (ret == 0) + dbgWriteScalar(); + return ret; +} + +inline +void +DataElementOpen62541Leaf::dbgWriteArray (const epicsUInt32 targetSize, const std::string &targetTypeName) const +{ + if (isLeaf() && debug()) { + std::cout << pconnector->getRecordName() << ": writing array of " + << targetTypeName << "[" << targetSize << "] as " + << variantTypeString(outgoingData) << "["<< outgoingData.arrayLength << "]" + << std::endl; + } +} + +static inline +UA_String UA_StringNCopy(const char *src, size_t maxlength) +{ + UA_String s; + s.length = src ? strnlen(src, maxlength) : 0; + s.data = s.length ? static_cast(UA_malloc(s.length)) : nullptr; + if (s.data) memcpy(s.data, src, s.length); + return s; +} + +// Write array for EPICS String / UA_String +long +DataElementOpen62541Leaf::writeArray (const char *value, epicsUInt32 len, + const epicsUInt32 num, + const UA_DataType *targetType, + dbCommon *prec) +{ + long ret = 0; + + if (UA_Variant_isScalar(&incomingData)) { + errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else { + const UA_DataType* type = incomingData.type; + void* data = UA_Array_new(num, type); + if (!data) { + errlogPrintf("%s : out of memory\n", prec->name); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else { + switch(typeKindOf(type)) { + case UA_DATATYPEKIND_STRING: + case UA_DATATYPEKIND_XMLELEMENT: + case UA_DATATYPEKIND_BYTESTRING: { + UA_String *arr = static_cast(data); + for (epicsUInt32 i = 0; i < num; i++) { + arr[i] = UA_StringNCopy(value, len); + value += len; + } + break; + } + case UA_DATATYPEKIND_LOCALIZEDTEXT: { + UA_LocalizedText *arr = static_cast(data); + for (epicsUInt32 i = 0; i < num; i++) { + const char* sep = static_cast(memchr(value, '|', len)); + if (sep) { + arr[i].locale = UA_StringNCopy(value, sep - value); + } else if (i < incomingData.arrayLength) { + UA_copy(&reinterpret_cast( + incomingData.data)[i].locale, + &arr[i].locale, &UA_TYPES[UA_TYPES_STRING]); + } else if (i > 0) { + UA_copy(&arr[i-1].locale, + &arr[i].locale, &UA_TYPES[UA_TYPES_STRING]); + } + arr[i].text = UA_StringNCopy(sep ? sep+1 : value, sep ? len - (sep+1-value) : len); + value += len; + } + break; + } + case UA_DATATYPEKIND_QUALIFIEDNAME: { + UA_QualifiedName *arr = static_cast(data); + for (epicsUInt32 i = 0; i < num; i++) { + const char* sep = static_cast(memchr(value, '|', len)); + if (sep) { + arr[i].namespaceIndex = atoi(value); + } else if (i < incomingData.arrayLength) { + arr[i].namespaceIndex = reinterpret_cast( + incomingData.data)[i].namespaceIndex; + } else if (i > 0) { + arr[i].namespaceIndex = arr[i-1].namespaceIndex; + } + arr[i].name = UA_StringNCopy(sep ? sep+1 : value, sep ? len - (sep+1-value) : len); + value += len; + } + break; + } + default: + errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", + prec->name, + variantTypeString(incomingData), + variantTypeString(targetType), + epicsTypeString(value)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + UA_Array_delete(data, num, type); + ret = 1; + } + { // Scope of Guard G + Guard G(outgoingLock); + UA_Variant_clear(&outgoingData); // unlikely but we may still have unsent old data to discard + UA_Variant_setArray(&outgoingData, data, num, type); // no copy but move content + markAsDirty(); + } + + dbgWriteArray(num, epicsTypeString(value)); + } + } + return ret; +} + +// Specialization for epicsUInt8 +// needed because epicsUInt8 is used for arrays of UA_BYTE and UA_BOOLEAN +// CAVEAT: changes in the template (in DataElementOpen62541.h) must be reflected here +template<> +long +DataElementOpen62541Leaf::writeArray(const epicsUInt8 *value, + const epicsUInt32 num, + const UA_DataType *targetType, + dbCommon *prec) +{ + long ret = 0; + + if (UA_Variant_isScalar(&incomingData) && incomingData.type == &UA_TYPES[UA_TYPES_BYTESTRING]) { + UA_ByteString bs; + bs.length = num; + bs.data = static_cast(const_cast(value)); + { // Scope of Guard G + Guard G(outgoingLock); + UA_Variant_setScalarCopy(&outgoingData, &bs, incomingData.type); + markAsDirty(); + } + + dbgWriteScalar(); + } else if (UA_Variant_isScalar(&incomingData)) { + errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else if (typeKindOf(incomingData) != UA_DATATYPEKIND_BYTE && typeKindOf(incomingData) != UA_DATATYPEKIND_BOOLEAN) { + errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", + prec->name, + variantTypeString(incomingData), + variantTypeString(targetType), + epicsTypeString(*value)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else { + UA_StatusCode status; + { // Scope of Guard G + Guard G(outgoingLock); + status = UA_Variant_setArrayCopy(&outgoingData, value, num, incomingData.type); + markAsDirty(); + } + if (UA_STATUS_IS_BAD(status)) { + errlogPrintf("%s : array copy failed: %s\n", + prec->name, UA_StatusCode_name(status)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else { + dbgWriteArray(num, epicsTypeString(*value)); + } + } + return ret; +} + +long +DataElementOpen62541Leaf::writeArray (const epicsInt8 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, &UA_TYPES[UA_TYPES_SBYTE], prec); +} + +long +DataElementOpen62541Leaf::writeArray (const epicsUInt8 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, &UA_TYPES[UA_TYPES_BYTE], prec); +} + +long +DataElementOpen62541Leaf::writeArray (const epicsInt16 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, &UA_TYPES[UA_TYPES_INT16], prec); +} + +long +DataElementOpen62541Leaf::writeArray (const epicsUInt16 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, &UA_TYPES[UA_TYPES_UINT16], prec); +} + +long +DataElementOpen62541Leaf::writeArray (const epicsInt32 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, &UA_TYPES[UA_TYPES_INT32], prec); +} + +long +DataElementOpen62541Leaf::writeArray (const epicsUInt32 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, &UA_TYPES[UA_TYPES_UINT32], prec); +} + +long +DataElementOpen62541Leaf::writeArray (const epicsInt64 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, &UA_TYPES[UA_TYPES_INT64], prec); +} + +long +DataElementOpen62541Leaf::writeArray (const epicsUInt64 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, &UA_TYPES[UA_TYPES_UINT64], prec); +} + +long +DataElementOpen62541Leaf::writeArray (const epicsFloat32 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, &UA_TYPES[UA_TYPES_FLOAT], prec); +} + +long +DataElementOpen62541Leaf::writeArray (const epicsFloat64 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, &UA_TYPES[UA_TYPES_DOUBLE], prec); +} + +long +DataElementOpen62541Leaf::writeArray (const char *value, epicsUInt32 len, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, len, num, &UA_TYPES[UA_TYPES_STRING], prec); +} + +void +DataElementOpen62541Leaf::requestRecordProcessing (const ProcessReason reason) const +{ + pconnector->requestRecordProcessing(reason); +} + +} // namespace DevOpcua diff --git a/devOpcuaSup/open62541/DataElementOpen62541Leaf.h b/devOpcuaSup/open62541/DataElementOpen62541Leaf.h new file mode 100644 index 00000000..d3eb3d51 --- /dev/null +++ b/devOpcuaSup/open62541/DataElementOpen62541Leaf.h @@ -0,0 +1,1165 @@ +/*************************************************************************\ +* Copyright (c) 2018-2026 ITER Organization. +* This module is distributed subject to a Software License Agreement found +* in file LICENSE that is included with this distribution. +\*************************************************************************/ + +/* + * Author: Dirk Zimoch + * + * based on the UaSdk implementation by Ralph Lange + */ + +#ifndef DEVOPCUA_DATAELEMENTOPEN62541LEAF_H +#define DEVOPCUA_DATAELEMENTOPEN62541LEAF_H + +#include "DataElementOpen62541.h" +#include "RecordConnector.h" +#include "Update.h" +#include "UpdateQueue.h" + +#include +#include +#include +#include + +#include +#include +#include + +namespace DevOpcua { + +typedef Update UpdateOpen62541; + +inline const char *epicsTypeString (const epicsInt8 &) { return "epicsInt8"; } +inline const char *epicsTypeString (const epicsUInt8 &) { return "epicsUInt8"; } +inline const char *epicsTypeString (const epicsInt16 &) { return "epicsInt16"; } +inline const char *epicsTypeString (const epicsUInt16 &) { return "epicsUInt16"; } +inline const char *epicsTypeString (const epicsInt32 &) { return "epicsInt32"; } +inline const char *epicsTypeString (const epicsUInt32 &) { return "epicsUInt32"; } +inline const char *epicsTypeString (const epicsInt64 &) { return "epicsInt64"; } +inline const char *epicsTypeString (const epicsUInt64 &) { return "epicsUInt64"; } +inline const char *epicsTypeString (const epicsFloat32 &) { return "epicsFloat32"; } +inline const char *epicsTypeString (const epicsFloat64 &) { return "epicsFloat64"; } +inline const char *epicsTypeString (const char*) { return "epicsString"; } + +// Template for range check when writing +template +inline bool isWithinRange (const FROM &value) { + return !(value < std::numeric_limits::lowest() || value > std::numeric_limits::max()); +} + +// Specializations for unsigned to signed to avoid compiler warnings +template<> +inline bool isWithinRange (const unsigned int &value) { + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool isWithinRange (const unsigned int &value) { + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool isWithinRange (const unsigned int &value) { + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool isWithinRange (const unsigned long &value) { + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool isWithinRange (const unsigned long &value) { + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool isWithinRange (const unsigned long &value) { + return !(value > std::numeric_limits::max()); +} + +template<> +inline bool isWithinRange (const unsigned long long &value) { + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool isWithinRange (const unsigned long long &value) { + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool isWithinRange (const unsigned long long &value) { + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool isWithinRange (const unsigned long long &value) { + return !(value > static_cast(std::numeric_limits::max())); +} + +// Simple-check specializations for converting signed to unsigned of same or wider type +template<> inline bool isWithinRange (const char &value) { return !(value < 0); } +template<> inline bool isWithinRange (const signed char &value) { return !(value < 0); } +template<> inline bool isWithinRange (const short &value) { return !(value < 0); } +template<> inline bool isWithinRange (const int &value) { return !(value < 0); } +template<> inline bool isWithinRange (const char &value) { return !(value < 0); } +template<> inline bool isWithinRange (const short &value) { return !(value < 0); } +template<> inline bool isWithinRange (const int &value) { return !(value < 0); } +template<> inline bool isWithinRange (const long &value) { return !(value < 0); } +template<> inline bool isWithinRange (const char &value) { return !(value < 0); } +template<> inline bool isWithinRange (const signed char &value) { return !(value < 0); } +template<> inline bool isWithinRange (const short &value) { return !(value < 0); } +template<> inline bool isWithinRange (const int &value) { return !(value < 0); } +template<> inline bool isWithinRange (const long &value) { return !(value < 0); } +template<> inline bool isWithinRange (const long long &value) { return !(value < 0); } + +// No-check-needed specializations for converting to same or wider type +template<> inline bool isWithinRange (const int &) { return true; } +template<> inline bool isWithinRange (const int &) { return true; } +template<> inline bool isWithinRange (const int &) { return true; } +template<> inline bool isWithinRange (const int &) { return true; } +template<> inline bool isWithinRange (const int &) { return true; } + +template<> inline bool isWithinRange (const unsigned int &) { return true; } +template<> inline bool isWithinRange (const unsigned int &) { return true; } +template<> inline bool isWithinRange (const unsigned int &) { return true; } +template<> inline bool isWithinRange (const unsigned int &) { return true; } +template<> inline bool isWithinRange (const unsigned int &) { return true; } +template<> inline bool isWithinRange (const unsigned int &) { return true; } + +template<> inline bool isWithinRange (const long &) { return true; } +template<> inline bool isWithinRange (const long &) { return true; } + +template<> inline bool isWithinRange (const long long &) { return true; } +template<> inline bool isWithinRange (const long long &) { return true; } +template<> inline bool isWithinRange (const long long &) { return true; } + +template<> inline bool isWithinRange (const double &) { return true; } + +// long long may or may not be the same size as long +template<> +inline bool isWithinRange (const long long &value) { + return sizeof(long long) > sizeof(long) ? !(value < 0 || value > static_cast(std::numeric_limits::max())) : !(value < 0); +} + +// Helper function to convert strings to numeric types + +inline bool string_to(const std::string& s, epicsInt32& value) { + try { + value = std::stol(s, 0, 0); + return true; + } catch (...) { + return false; + } +} + +inline bool string_to(const std::string& s, epicsInt64& value) { + try { + value = std::stoll(s, 0, 0); + return true; + } catch (...) { + return false; + } +} + +inline bool string_to(const std::string& s, epicsUInt32& value) { + try { + long long v = std::stoll(s, 0, 0); + if (!isWithinRange(v)) return false; + value = static_cast(v); + return true; + } catch (...) { + return false; + } +} + +inline bool string_to(const std::string& s, epicsFloat64& value) { + try { + value = std::stod(s, 0); + return true; + } catch (...) { + return false; + } +} + +/** + * @brief The DataElementOpen62541 implementation of a single piece of data. + * + * See DevOpcua::DataElement + */ +class DataElementOpen62541Leaf + : public DataElement + , public DataElementOpen62541 +{ +public: + /** + * @brief Constructor for DataElement (leaf) from record connector. + * + * Creates the final (leaf) element of the data structure. The record connector + * holds a shared pointer to its leaf, while the data element has a weak pointer + * to the record connector. + * + * @param name name of the element (empty for root element) + * @param pitem pointer to corresponding ItemOpen62541 + * @param pconnector pointer to record connector to link to + */ + DataElementOpen62541Leaf(const std::string &name, ItemOpen62541 *pitem, RecordConnector *pconnector); + virtual ~DataElementOpen62541Leaf() override; + + /* ElementTree node interface methods */ + virtual bool isLeaf () const override { return true; } + virtual void addChild (std::weak_ptr elem) override {} + virtual std::shared_ptr findChild (const std::string &name) const override + { + return std::shared_ptr(); + } + + /** + * @brief Create a DataElement and add it to the item's dataTree. + * + * @param item item to add the element to + * @param pconnector pointer to the leaf's record connector + * @param elementPath full path to the element + */ + static void + addElementToTree(ItemOpen62541 *item, RecordConnector *pconnector, const std::list &elementPath); + + virtual void show(const int level, const unsigned int indent) const override; + + + virtual void + setIncomingData(const UA_Variant &value, ProcessReason reason, const std::string *timefrom = nullptr) override; + + virtual void setIncomingEvent(ProcessReason reason) override; + + /** + * @brief Set the EPICS-related state of the element and all sub-elements. + */ + void setState(const ConnectionStatus state) override; + + /** + * @brief Get the outgoing data value from the DataElement. + * + * Called from the OPC UA client worker thread when data is being + * assembled in OPC UA session for sending. + * + * @return reference to outgoing data + */ + const UA_Variant &getOutgoingData() override; + + /** + * @brief Read incoming data as a scalar epicsInt32. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readScalar(epicsInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readScalar(epicsInt32 *value, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as a scalar epicsInt64. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readScalar(epicsInt64*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readScalar(epicsInt64 *value, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as a scalar epicsUInt32. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readScalar(epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readScalar(epicsUInt32 *value, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as a scalar epicsFloat64. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readScalar(epicsFloat64*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readScalar(epicsFloat64 *value, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as classic C string (char[]). + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readScalar(char*,const epicsUInt32,dbCommon*,ProcessReason*,epicsUInt32*,epicsUInt32*,char*,const epicsUInt32) + */ + + virtual long int readScalar(char *value, const epicsUInt32 len, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32* lenRead = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of epicsInt8. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsInt8*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsInt8 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of epicsUInt8. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsUInt8*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsUInt8 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of epicsInt16. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsInt16*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsInt16 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of epicsUInt16. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsUInt16*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsUInt16 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of epicsInt32. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsInt32*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsInt32 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of epicsUInt32. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsUInt32*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsUInt32 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of epicsInt64. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsInt64*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsInt64 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of epicsUInt64. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsUInt64*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsUInt64 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of epicsFloat32. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsFloat32*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsFloat32 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of epicsFloat64. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsFloat64*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsFloat64 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of EPICS String (char[40]). + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(char*,const epicsUInt32,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(char *value, epicsUInt32 len, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Write outgoing scalar epicsInt32 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeScalar(const epicsInt32&,dbCommon*) + */ + virtual long int writeScalar(const epicsInt32 &value, + dbCommon *prec) override; + + /** + * @brief Write outgoing scalar epicsInt64 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeScalar(const epicsInt64&,dbCommon*) + */ + virtual long int writeScalar(const epicsInt64 &value, + dbCommon *prec) override; + + /** + * @brief Write outgoing scalar epicsUInt32 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeScalar(const epicsUInt32&,dbCommon*) + */ + virtual long int writeScalar(const epicsUInt32 &value, + dbCommon *prec) override; + + /** + * @brief Write outgoing scalar epicsFloat64 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeScalar(const epicsFloat64&,dbCommon*) + */ + virtual long int writeScalar(const epicsFloat64 &value, + dbCommon *prec) override; + + /** + * @brief Write outgoing classic C string (char[]) data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeScalar(const char*,const epicsUInt32,dbCommon*) + */ + virtual long int writeScalar(const char *value, + const epicsUInt32 len, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsInt8 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsInt8*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsInt8 *value, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsUInt8 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsUInt8*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsUInt8 *value, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsInt16 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsInt16*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsInt16 *value, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsUInt16 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsUInt16*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsUInt16 *value, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsInt32 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsInt32*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsInt32 *value, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsUInt32 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsUInt32*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsUInt32 *value, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsInt64 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsInt64*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsInt64 *value, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsUInt64 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsUInt64*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsUInt64 *value, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsFloat32 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsFloat32*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsFloat32 *value, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsFloat64 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsFloat64*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsFloat64 *value, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of EPICS String (char[MAX_STRING_SIZE]) data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const char*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const char *value, const epicsUInt32 len, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Clear (discard) the current outgoing data. + * + * Called by the low level connection (OPC UA session) + * after it is done accessing the data in the context of sending. + * + * In case an implementation uses a queue, this should remove the + * oldest element from the queue, allowing access to the next element + * with the next send. + */ + virtual void clearOutgoingData() { UA_Variant_clear(&outgoingData); } + + /** + * @brief Create processing requests for record(s) attached to this element. + * See DevOpcua::DataElement::requestRecordProcessing + */ + virtual void requestRecordProcessing(const ProcessReason reason) const override; + + /** + * @brief Get debug level from record (via RecordConnector). + * @return debug level + */ + int debug() const { return (isLeaf() ? pconnector->debug() : pitem->debug()); } + +private: + virtual bool isDirty () const override { return isdirty; } + virtual void markAsDirty () override + { + isdirty = true; + pitem->markAsDirty(); + } + + void dbgWriteScalar () const; + void dbgReadScalar(const UpdateOpen62541 *upd, + const std::string &targetTypeName, + const size_t targetSize = 0) const; + void dbgReadArray(const UpdateOpen62541 *upd, + const epicsUInt32 targetSize, + const std::string &targetTypeName) const; + void checkWriteArray(const UA_DataType *expectedType, const std::string &targetTypeName) const; + void dbgWriteArray(const epicsUInt32 targetSize, const std::string &targetTypeName) const; + bool updateDataInStruct(void* container, std::shared_ptr pelem); + void createMap(const UA_DataType *type, const std::string* timefrom = nullptr); + + // Get the time stamp from the incoming object + const epicsTime &getIncomingTimeStamp() const { + ProcessReason reason = pitem->getReason(); + if ((reason == ProcessReason::incomingData || reason == ProcessReason::readComplete) + && isLeaf()) + switch (pconnector->plinkinfo->timestamp) { + case LinkOptionTimestamp::server: + return pitem->tsServer; + case LinkOptionTimestamp::source: + return pitem->tsSource; + case LinkOptionTimestamp::data: + return pitem->tsData; + } + return pitem->tsClient; + } + + // Get the read status from the incoming object + UA_StatusCode getIncomingReadStatus() const { return pitem->getLastStatus(); } + + // Read scalar value as templated function on EPICS type + // value == nullptr is allowed and leads to the value being dropped (ignored), + // including the extended status + template + long + readScalar (ET *value, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) + { + long ret = 1; + + if (incomingQueue.empty()) { + errlogPrintf("%s: incoming data queue empty\n", prec->name); + if (nextReason) + *nextReason = ProcessReason::none; + return 1; + } + + ProcessReason nReason; + std::shared_ptr upd = incomingQueue.popUpdate(&nReason); + dbgReadScalar(upd.get(), epicsTypeString(*value)); + prec->udf = false; + + switch (upd->getType()) { + case ProcessReason::readFailure: + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + break; + case ProcessReason::connectionLoss: + (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); + break; + case ProcessReason::incomingData: + case ProcessReason::readComplete: + { + if (value) { + UA_StatusCode stat = upd->getStatus(); + if (UA_STATUS_IS_BAD(stat)) { + // No valid OPC UA value + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + } else { + // Valid OPC UA value, so try to convert + UA_Variant &data = upd->getData(); + switch(typeKindOf(data)) { + case UA_DATATYPEKIND_BOOLEAN: + *value = (*static_cast(data.data) != 0); + ret = 0; + break; + case UA_DATATYPEKIND_BYTE: + if (isWithinRange(*static_cast(data.data))) { + *value = *static_cast(data.data); + ret = 0; + } + break; + case UA_DATATYPEKIND_SBYTE: + if (isWithinRange(*static_cast(data.data))) { + *value = *static_cast(data.data); + ret = 0; + } + break; + case UA_DATATYPEKIND_INT16: + if (isWithinRange(*static_cast(data.data))) { + *value = static_cast(*static_cast(data.data)); + ret = 0; + } + break; + case UA_DATATYPEKIND_UINT16: + if (isWithinRange(*static_cast(data.data))) { + *value = static_cast(*static_cast(data.data)); + ret = 0; + } + break; + case UA_DATATYPEKIND_INT32: + if (isWithinRange(*static_cast(data.data))) { + *value = static_cast(*static_cast(data.data)); + ret = 0; + } + break; + case UA_DATATYPEKIND_UINT32: + if (isWithinRange(*static_cast(data.data))) { + *value = static_cast(*static_cast(data.data)); + ret = 0; + } + break; + case UA_DATATYPEKIND_INT64: + if (isWithinRange(*static_cast(data.data))) { + *value = static_cast(*static_cast(data.data)); + ret = 0; + } + break; + case UA_DATATYPEKIND_UINT64: + if (isWithinRange(*static_cast(data.data))) { + *value = static_cast(*static_cast(data.data)); + ret = 0; + } + break; + case UA_DATATYPEKIND_FLOAT: + if (isWithinRange(*static_cast(data.data))) { + *value = static_cast(*static_cast(data.data)); + ret = 0; + } + break; + case UA_DATATYPEKIND_DOUBLE: + if (isWithinRange(*static_cast(data.data))) { + *value = static_cast(*static_cast(data.data)); + ret = 0; + } + break; + case UA_DATATYPEKIND_ENUM: + if (isWithinRange(*static_cast(data.data))) { + *value = static_cast(*static_cast(data.data)); + if (!enumChoices || enumChoices->find(static_cast(*value)) != enumChoices->end()) + ret = 0; + } + break; + case UA_DATATYPEKIND_STRING: + { + UA_String* s = static_cast(data.data); + if (string_to(std::string(reinterpret_cast(s->data), s->length), *value)) + ret = 0; + break; + } + case UA_DATATYPEKIND_LOCALIZEDTEXT: + { + UA_LocalizedText* lt = static_cast(data.data); + if (string_to(std::string(reinterpret_cast(lt->text.data), lt->text.length), *value)) + ret = 0; + break; + } + default: + errlogPrintf("%s : unsupported type kind %s for incoming data\n", + prec->name, typeKindName(typeKindOf(data))); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + } + if (ret == 1) { + UA_String datastring = UA_STRING_NULL; + if (data.type) + UA_print(data.data, data.type, &datastring); + errlogPrintf("%s : incoming data (%s %.*s) out-of-bounds for %s\n", + prec->name, + variantTypeString(data), + static_cast(datastring.length), datastring.data, + epicsTypeString(*value)); + UA_String_clear(&datastring); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + } else { + if (UA_STATUS_IS_UNCERTAIN(stat)) { + (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); + } + } + UA_Variant_clear(&data); + } + if (statusCode) *statusCode = stat; + if (statusText) { + strncpy(statusText, UA_StatusCode_name(stat), statusTextLen); + statusText[statusTextLen-1] = '\0'; + } + } + break; + } + default: + break; + } + + prec->time = upd->getTimeStamp(); + if (nextReason) *nextReason = nReason; + return ret; + } + + // Read array value as templated function on EPICS type + // CAVEAT: changes must also be reflected in specializations (in DataElementOpen62541.cpp) + template + long + readArray (ET *value, const epicsUInt32 num, + epicsUInt32 *numRead, + const UA_DataType *expectedType, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) + { + long ret = 0; + epicsUInt32 elemsWritten = 0; + + if (incomingQueue.empty()) { + errlogPrintf("%s : incoming data queue empty\n", prec->name); + *numRead = 0; + return 1; + } + + ProcessReason nReason; + std::shared_ptr upd = incomingQueue.popUpdate(&nReason); + dbgReadArray(upd.get(), num, epicsTypeString(*value)); + + switch (upd->getType()) { + case ProcessReason::readFailure: + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::connectionLoss: + (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::incomingData: + case ProcessReason::readComplete: + { + if (num && value) { + UA_StatusCode stat = upd->getStatus(); + if (UA_STATUS_IS_BAD(stat)) { + // No valid OPC UA value + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + // Valid OPC UA value, so try to convert + UA_Variant &variant = upd->getData(); + if (UA_Variant_isScalar(&variant)) { + errlogPrintf("%s : incoming data is not an array\n", prec->name); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else if (variant.type != expectedType) { + errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", + prec->name, variantTypeString(variant), epicsTypeString(*value)); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + if (UA_STATUS_IS_UNCERTAIN(stat)) { + (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); + } + elemsWritten = static_cast(num) < variant.arrayLength ? num : static_cast(variant.arrayLength); + memcpy(value, variant.data, sizeof(ET) * elemsWritten); + } + UA_Variant_clear(&variant); + } + if (statusCode) *statusCode = stat; + if (statusText) { + strncpy(statusText, UA_StatusCode_name(stat), statusTextLen); + statusText[statusTextLen-1] = '\0'; + } + } + break; + } + default: + break; + } + + prec->time = upd->getTimeStamp(); + if (nextReason) *nextReason = nReason; + if (num && value) + *numRead = elemsWritten; + return ret; + } + + // Read array value for EPICS String / UA_String + long + readArray (char *value, const epicsUInt32 len, + const epicsUInt32 num, + epicsUInt32 *numRead, + const UA_DataType *expectedType, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen); + + // Write scalar value as templated function on EPICS type + template + long + writeScalar (const ET &value, + dbCommon *prec) + { + long ret = 1; + UA_StatusCode status = UA_STATUSCODE_BADUNEXPECTEDERROR; + + { // Scope of Guard G + Guard G(outgoingLock); + UA_Variant_clear(&outgoingData); // unlikely but we may still have unsent old data to discard + switch (typeKindOf(incomingData)) { + case UA_DATATYPEKIND_BOOLEAN: + { + UA_Boolean val = (value != 0); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BOOLEAN]); + markAsDirty(); + ret = 0; + break; + } + case UA_DATATYPEKIND_BYTE: + if (isWithinRange(value)) { + UA_Byte val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BYTE]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_SBYTE: + if (isWithinRange(value)) { + UA_SByte val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_SBYTE]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_UINT16: + if (isWithinRange(value)) { + UA_UInt16 val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT16]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_INT16: + if (isWithinRange(value)) { + UA_Int16 val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT16]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_UINT32: + if (isWithinRange(value)) { + UA_UInt32 val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT32]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_ENUM: + case UA_DATATYPEKIND_INT32: + if (isWithinRange(value) && + (!enumChoices || + enumChoices->find(static_cast(value)) != enumChoices->end())) { + UA_Int32 val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT32]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_UINT64: + if (isWithinRange(value)) { + UA_UInt64 val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT64]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_INT64: + if (isWithinRange(value)) { + UA_Int64 val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT64]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_FLOAT: + if (isWithinRange(value)) { + UA_Float val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_FLOAT]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_DOUBLE: + if (isWithinRange(value)) { + UA_Double val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_DOUBLE]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_STRING: + { + std::string strval = std::to_string(value); + UA_String val; + val.length = strval.length(); + val.data = const_cast(reinterpret_cast(strval.c_str())); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_STRING]); + markAsDirty(); + ret = 0; + break; + } + case UA_DATATYPEKIND_LOCALIZEDTEXT: + { + std::string strval = std::to_string(value); + UA_LocalizedText val; + val.locale = reinterpret_cast(incomingData.data)->locale; + val.text.length = strval.length(); + val.text.data = const_cast(reinterpret_cast(strval.c_str())); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]); + markAsDirty(); + ret = 0; + break; + } + default: + errlogPrintf("%s : unsupported conversion from %s to %s for outgoing data\n", + prec->name, epicsTypeString(value), variantTypeString(incomingData)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + } + } + if (ret != 0) { + errlogPrintf("%s : value out of range\n", + prec->name); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + } + if (ret == 0 && UA_STATUS_IS_BAD(status)) { + errlogPrintf("%s : scalar copy failed: %s\n", + prec->name, UA_StatusCode_name(status)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + + } + if (ret == 0) + dbgWriteScalar(); + return ret; + } + + // Write array value for EPICS String / UA_String + long + writeArray (const char *value, const epicsUInt32 len, + const epicsUInt32 num, + const UA_DataType *targetType, + dbCommon *prec); + + // Write array value as templated function on EPICS type, OPC UA container and simple (element) types + // (latter *must match* OPC UA type enum argument) + // CAVEAT: changes must also be reflected in the specialization (in DataElementOpen62541.cpp) + template + long + writeArray (const ET *value, const epicsUInt32 num, + const UA_DataType *targetType, + dbCommon *prec) + { + long ret = 0; + + if (UA_Variant_isScalar(&incomingData)) { + errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else if (incomingData.type != targetType) { + errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", + prec->name, + variantTypeString(incomingData), + variantTypeString(targetType), + epicsTypeString(*value)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else { + UA_StatusCode status; + { // Scope of Guard G + Guard G(outgoingLock); + UA_Variant_clear(&outgoingData); // unlikely but we may still have unsent old data to discard + status = UA_Variant_setArrayCopy(&outgoingData, value, num, targetType); + markAsDirty(); + } + if (UA_STATUS_IS_BAD(status)) { + errlogPrintf("%s : array copy failed: %s\n", + prec->name, UA_StatusCode_name(status)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else { + dbgWriteArray(num, epicsTypeString(*value)); + } + } + return ret; + } + + UpdateQueue incomingQueue; /**< queue of incoming values */ +}; + +} // namespace DevOpcua + +#endif // DEVOPCUA_DATAELEMENTOPEN62541LEAF_H diff --git a/devOpcuaSup/open62541/DataElementOpen62541Node.cpp b/devOpcuaSup/open62541/DataElementOpen62541Node.cpp new file mode 100644 index 00000000..58ea09e0 --- /dev/null +++ b/devOpcuaSup/open62541/DataElementOpen62541Node.cpp @@ -0,0 +1,418 @@ +/*************************************************************************\ +* Copyright (c) 2018-2026 ITER Organization. +* This module is distributed subject to a Software License Agreement found +* in file LICENSE that is included with this distribution. +\*************************************************************************/ + +/* + * Author: Dirk Zimoch + * + * based on the UaSdk implementation by Ralph Lange + */ + +#define epicsExportSharedSymbols +#include "DataElementOpen62541Node.h" +#include "ItemOpen62541.h" +#include "RecordConnector.h" + +#include +#include + +#include + +#include +#include +#include + +namespace DevOpcua { + +#ifndef UA_ENABLE_TYPEDESCRIPTION + // Without type description, we cannot get struct members + #error Set UA_ENABLE_TYPEDESCRIPTION in open62541 +#endif + +// Older open62541 version 1.2 has no UA_DataType_getStructMember +// Code from open62541 version 1.3.7 modified for compatibility with version 1.2 +// and extended to return isOptional flag and index for union + +inline const UA_DataType* +memberTypeOf(const UA_DataType *type, const UA_DataTypeMember *m) { +#ifdef UA_DATATYPES_USE_POINTER + return m->memberType; +#else + const UA_DataType *typelists[2] = { UA_TYPES, &type[-type->typeIndex] }; + return &typelists[!m->namespaceZero][m->memberTypeIndex]; +#endif +} + +UA_UInt32 +UA_DataType_getStructMemberExt(const UA_DataType *type, const char *memberName, + size_t *outOffset, const UA_DataType **outMemberType, + UA_Boolean *outIsArray, UA_Boolean *outIsOptional) { + + size_t offset = 0; + switch(type->typeKind) { + case UA_DATATYPEKIND_STRUCTURE: + case UA_DATATYPEKIND_OPTSTRUCT: + case UA_DATATYPEKIND_UNION: + for(UA_UInt32 i = 0; i < type->membersSize; ++i) { + const UA_DataTypeMember *m = &type->members[i]; + const UA_DataType *mt = memberTypeOf(type, m); + offset += m->padding; + + if(strcmp(memberName, m->memberName) == 0) { + *outOffset = offset; + *outMemberType = mt; + *outIsArray = m->isArray; + *outIsOptional = m->isOptional; + return i+1; + } + + if (type->typeKind == UA_DATATYPEKIND_UNION) { + offset = 0; + } else if(!m->isOptional) { + if(!m->isArray) { + offset += mt->memSize; + } else { + offset += sizeof(size_t); + offset += sizeof(void*); + } + } else { /* field is optional */ + if(!m->isArray) { + offset += sizeof(void *); + } else { + offset += sizeof(size_t); + offset += sizeof(void *); + } + } + } + break; + case UA_DATATYPEKIND_LOCALIZEDTEXT: + *outMemberType = &UA_TYPES[UA_TYPES_STRING]; + *outIsArray = 0; + *outIsOptional = 0; + if (strcmp(memberName, "locale") == 0) { + *outOffset = offsetof(UA_LocalizedText, locale); + return 1; + } + if (strcmp(memberName, "text") == 0) { + *outOffset = offsetof(UA_LocalizedText, text); + return 2; + } + break; + case UA_DATATYPEKIND_QUALIFIEDNAME: + *outIsArray = 0; + *outIsOptional = 0; + if (strcmp(memberName, "namespaceIndex") == 0) { + *outMemberType = &UA_TYPES[UA_DATATYPEKIND_UINT16]; + *outOffset = offsetof(UA_QualifiedName, namespaceIndex); + return 1; + } + if (strcmp(memberName, "name") == 0) { + *outMemberType = &UA_TYPES[UA_TYPES_STRING]; + *outOffset = offsetof(UA_QualifiedName, name); + return 2; + } + break; + } + return 0; +} + +DataElementOpen62541Node::DataElementOpen62541Node (const std::string &name, ItemOpen62541 *item) + : DataElementOpen62541(name, item) + , timesrc(-1) + , mapped(false) +{ + UA_Variant_init(&incomingData); + UA_Variant_init(&outgoingData); + item->dataTreeNoOfNodes++; +} + +DataElementOpen62541Node::~DataElementOpen62541Node() +{ + pitem->dataTreeNoOfNodes--; +} + +void +DataElementOpen62541Node::createMap (const UA_DataType *type, const std::string *timefrom) +{ + if (debug() >= 5) + std::cout << " ** creating index-to-element map for child elements" << std::endl; + + switch (typeKindOf(type)) { + case UA_DATATYPEKIND_STRUCTURE: + case UA_DATATYPEKIND_OPTSTRUCT: + case UA_DATATYPEKIND_UNION: + case UA_DATATYPEKIND_LOCALIZEDTEXT: + case UA_DATATYPEKIND_QUALIFIEDNAME: + if (timefrom) { + const UA_DataType *timeMemberType; + UA_Boolean timeIsArray; + UA_Boolean timeIsOptional; + size_t timeOffset; + + if (UA_DataType_getStructMemberExt( + type, timefrom->c_str(), &timeOffset, &timeMemberType, &timeIsArray, &timeIsOptional)) { + if (typeKindOf(timeMemberType) != UA_TYPES_DATETIME || timeIsArray) { + errlogPrintf("%s: timestamp element %s has invalid type %s%s - using " + "source timestamp\n", + pitem->recConnector->getRecordName(), + timefrom->c_str(), + typeKindName(typeKindOf(timeMemberType)), + timeIsArray ? "[]" : ""); + } else + timesrc = timeOffset; + } else { + errlogPrintf("%s: timestamp element %s not found - using source timestamp\n", + pitem->recConnector->getRecordName(), + timefrom->c_str()); + } + } + + for (const auto &it : elements) { + auto pelem = it.lock(); + if ((pelem->index = UA_DataType_getStructMemberExt(type, + pelem->name.c_str(), + &pelem->offset, + &pelem->memberType, + &pelem->isArray, + &pelem->isOptional))) { + if (debug() >= 5) + std::cout << typeKindName(typeKindOf(type)) << " " << pelem << " index=" << pelem->index + << " offset=" << pelem->offset << " type=" << variantTypeString(pelem->memberType) + << (pelem->isArray ? "[]" : "") << (pelem->isOptional ? " optional" : "") << std::endl; + } else { + std::cerr << "Item " << pitem << ": element " << pelem->name << " not found in " + << variantTypeString(type) << std::endl; + } + } + if (debug() >= 5) + std::cout << " ** " << elements.size() << " child elements mapped to " << variantTypeString(type) << " of " + << type->membersSize << " elements" << std::endl; + break; + default: + std::cerr << "Error: " << this << " is no structured data but a " << typeKindName(typeKindOf(type)) + << std::endl; + } + mapped = true; +} + +void +DataElementOpen62541Node::show (const int level, const unsigned int indent) const +{ + std::string ind(indent * 2, ' '); + std::cout << ind; + std::cout << "node=" << name << " children=" << elements.size() << " mapped=" << (mapped ? "y" : "n") << "\n"; + for (const auto &it : elements) { + if (auto pelem = it.lock()) { + pelem->show(level, indent + 1); + } + } +} + +// Getting the timestamp and status information from the Item assumes that only one thread +// is pushing data into the Item's DataElement structure at any time. +void +DataElementOpen62541Node::setIncomingData (const UA_Variant &value, ProcessReason reason, const std::string *timefrom) +{ + // Cache this element. We can make a shallow copy because + // ItemOpen62541::setIncomingData marks the original response data as "ours". + // Member data is owned by the [ROOT] element. + UA_Variant_clear(&incomingData); + incomingData = value; + + if (UA_Variant_isEmpty(&value)) + return; + + if (debug() >= 5) + std::cout << "Item " << pitem << " element " << name << " splitting structured data to " << elements.size() + << " child elements" << std::endl; + + const UA_DataType *type = value.type; + char *container = static_cast(value.data); + if (typeKindOf(type) == UA_DATATYPEKIND_EXTENSIONOBJECT) { + UA_ExtensionObject &extensionObject = *reinterpret_cast(container); + if (extensionObject.encoding >= UA_EXTENSIONOBJECT_DECODED) { + // Access content of decoded extension objects + type = extensionObject.content.decoded.type; + container = static_cast(extensionObject.content.decoded.data); + } else { + std::cerr << "Cannot get a structure definition for item " << pitem << " because binaryEncodingId " + << extensionObject.content.encoded.typeId << " is not in the type dictionary." << std::endl; + return; + } + } + + if (!mapped) + createMap(type, timefrom); + + if (timefrom) { + if (timesrc >= 0) + pitem->tsData = ItemOpen62541::uaToEpicsTime(*reinterpret_cast(container + timesrc), 0); + else + pitem->tsData = pitem->tsSource; + } + + for (const auto &it : elements) { + auto pelem = it.lock(); + const UA_DataType *memberType = pelem->memberType; + char *memberData = container + pelem->offset; + UA_Variant memberValue; + size_t arrayLength = 0; // default to scalar + if (pelem->isArray) { + arrayLength = *reinterpret_cast(memberData); + memberData = *reinterpret_cast(memberData + sizeof(size_t)); + } else if (pelem->isOptional) { + /* optional scalar stored through pointer like an array */ + memberData = *reinterpret_cast(memberData); + } + if (type->typeKind == UA_DATATYPEKIND_UNION && pelem->index != *reinterpret_cast(container)) { + // union option not taken + memberData = nullptr; + } + UA_Variant_setArray(&memberValue, memberData, arrayLength, memberType); + memberValue.storageType = UA_VARIANT_DATA_NODELETE; // Keep ownership of data + if (debug() && !memberData) { + std::cerr << pitem->recConnector->getRecordName() << " " << pelem + << (type->typeKind == UA_DATATYPEKIND_UNION ? " not taken choice " : " absent optional ") + << variantTypeString(memberType) << (pelem->isArray ? " array" : " scalar") << std::endl; + } + pelem->setIncomingData(memberValue, memberData ? reason : ProcessReason::readFailure); + } +} + +void +DataElementOpen62541Node::setIncomingEvent (ProcessReason reason) +{ + for (const auto &it : elements) { + auto pelem = it.lock(); + pelem->setIncomingEvent(reason); + } + if (reason == ProcessReason::connectionLoss) { + elementMap.clear(); + timesrc = -1; + mapped = false; + } +} + +void +DataElementOpen62541Node::setState (const ConnectionStatus state) +{ + for (const auto &it : elements) { + auto pelem = it.lock(); + pelem->setState(state); + } +} + +// Helper to update one data structure element from pointer to child +bool +DataElementOpen62541Node::updateDataInStruct (void *container, std::shared_ptr pelem) +{ + bool updated = false; + { // Scope of Guard G + Guard G(pelem->outgoingLock); + if (pelem->isDirty()) { + char *memberData = static_cast(container) + pelem->offset; + const UA_Variant &elementData = pelem->getOutgoingData(); + const UA_DataType *memberType = pelem->memberType; + assert(memberType == elementData.type + || (typeKindOf(memberType) == UA_DATATYPEKIND_ENUM + && typeKindOf(elementData.type) == UA_DATATYPEKIND_INT32)); + if (!pelem->isArray && !pelem->isOptional) { + // mandatory scalar: shallow copy + UA_clear(memberData, memberType); + void *data = pelem->moveOutgoingData(); + if (typeKindOf(outgoingData) == UA_DATATYPEKIND_UNION) { + *reinterpret_cast(container) = pelem->index; + } + memcpy(memberData, data, memberType->memSize); + UA_free(data); + } else { + // array or optional scalar: move content + void **memberDataPtr; + if (pelem->isArray) /* mandatory or optional array stored as length and pointer */ { + size_t &arrayLength = *reinterpret_cast(memberData); + memberDataPtr = reinterpret_cast(memberData + sizeof(size_t)); + UA_Array_delete(*memberDataPtr, arrayLength, memberType); + arrayLength = elementData.arrayLength; + } else /* optional scalar stored through pointer */ { + memberDataPtr = reinterpret_cast(memberData); + if (*memberDataPtr) /* absent optional has nullptr here */ + UA_Array_delete(*memberDataPtr, 1, memberType); + } + *memberDataPtr = pelem->moveOutgoingData(); + } + pelem->isdirty = false; + updated = true; + } + } + if (debug() >= 4) { + if (updated) { + std::cout << "Data from child element " << pelem->name << " inserted into data structure" << std::endl; + } else { + std::cout << "Data from child element " << pelem->name << " ignored (not dirty)" << std::endl; + } + } + return updated; +} + +const UA_Variant & +DataElementOpen62541Node::getOutgoingData () +{ + if (debug() >= 4) + std::cout << "Item " << pitem << " element " << name << " updating structured data from " << elements.size() + << " child elements" << std::endl; + + UA_Variant_clear(&outgoingData); + UA_Variant_copy(&incomingData, &outgoingData); + isdirty = false; + const UA_DataType *type = outgoingData.type; + void *container = outgoingData.data; + + if (typeKindOf(type) == UA_DATATYPEKIND_EXTENSIONOBJECT) { + UA_ExtensionObject &extensionObject = *reinterpret_cast(container); + if (extensionObject.encoding >= UA_EXTENSIONOBJECT_DECODED) { + // Access content decoded extension objects + type = extensionObject.content.decoded.type; + container = extensionObject.content.decoded.data; + } else { + std::cerr << "Cannot get a structure definition for item " << pitem << " because binaryEncodingId " + << extensionObject.content.encoded.typeId << " is not in the type dictionary." << std::endl; + return outgoingData; + } + } + + if (!mapped) + createMap(type); + + for (const auto &it : elements) { + auto pelem = it.lock(); + if (updateDataInStruct(container, pelem)) + isdirty = true; + } + if (isdirty) { + if (debug() >= 4) + std::cout << "Encoding changed data structure to outgoingData of element " << name << std::endl; + } else { + if (debug() >= 4) + std::cout << "Returning unchanged outgoingData of element " << name << std::endl; + } + return outgoingData; +} + +void +DataElementOpen62541Node::clearOutgoingData () +{ + UA_Variant_clear(&outgoingData); +} + +void +DataElementOpen62541Node::requestRecordProcessing (const ProcessReason reason) const +{ + for (auto &it : elementMap) { + auto pelem = it.second.lock(); + pelem->requestRecordProcessing(reason); + } +} + +} // namespace DevOpcua diff --git a/devOpcuaSup/open62541/DataElementOpen62541Node.h b/devOpcuaSup/open62541/DataElementOpen62541Node.h new file mode 100644 index 00000000..efb912d3 --- /dev/null +++ b/devOpcuaSup/open62541/DataElementOpen62541Node.h @@ -0,0 +1,68 @@ +/*************************************************************************\ +* Copyright (c) 2018-2026 ITER Organization. +* This module is distributed subject to a Software License Agreement found +* in file LICENSE that is included with this distribution. +\*************************************************************************/ + +/* + * Author: Dirk Zimoch + * + * based on the UaSdk implementation by Ralph Lange + */ + +#ifndef DEVOPCUA_DATAELEMENTOPEN62541NODE_H +#define DEVOPCUA_DATAELEMENTOPEN62541NODE_H + +#include "DataElementOpen62541.h" + +namespace DevOpcua { + +class DataElementOpen62541Node : public DataElementOpen62541 +{ +public: + DataElementOpen62541Node(const std::string &name, + ItemOpen62541 *item); + virtual ~DataElementOpen62541Node() override; + + /* ElementTree node interface methods */ + virtual bool isLeaf() const override { return false; } + virtual void addChild(std::weak_ptr elem) override + { + elements.push_back(elem); + } + virtual std::shared_ptr findChild(const std::string &name) const override + { + for (const auto &it : elements) + if (auto pit = it.lock()) + if (pit->name == name) + return pit; + return std::shared_ptr(); + } + + virtual void show(const int level, const unsigned int indent) const override; + + virtual void setIncomingData(const UA_Variant &value, + ProcessReason reason, + const std::string *timefrom = nullptr) override; + virtual void setIncomingEvent(ProcessReason reason) override; + virtual void setState(const ConnectionStatus state) override; + virtual const UA_Variant &getOutgoingData() override; + virtual void clearOutgoingData() override; + virtual void requestRecordProcessing(const ProcessReason reason) const override; + +private: + virtual bool isDirty() const override { return true; } + virtual void markAsDirty() override {} + + bool updateDataInStruct(void* container, std::shared_ptr pelem); + void createMap(const UA_DataType *type, const std::string* timefrom = nullptr); + + std::vector> elements; /**< children (if node) */ + std::unordered_map> elementMap; + ptrdiff_t timesrc; + bool mapped; /**< child name to index mapping done */ +}; + +} // namespace DevOpcua + +#endif // DEVOPCUA_DATAELEMENTOPEN62541NODE_H diff --git a/devOpcuaSup/open62541/ItemOpen62541.cpp b/devOpcuaSup/open62541/ItemOpen62541.cpp index 930e5809..89fc4475 100644 --- a/devOpcuaSup/open62541/ItemOpen62541.cpp +++ b/devOpcuaSup/open62541/ItemOpen62541.cpp @@ -1,5 +1,5 @@ /*************************************************************************\ -* Copyright (c) 2018-2023 ITER Organization. +* Copyright (c) 2018-2026 ITER Organization. * This module is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. \*************************************************************************/ @@ -12,6 +12,7 @@ #define epicsExportSharedSymbols #include "ItemOpen62541.h" +#include "RecordConnector.h" #include "SubscriptionOpen62541.h" #include "SessionOpen62541.h" #include "DataElementOpen62541.h" @@ -41,12 +42,13 @@ ItemOpen62541::ItemOpen62541(const linkInfo &info) , registered(false) , revisedSamplingInterval(0.0) , revisedQueueSize(0) - , dataTree(this) , dataTreeDirty(false) + , dataTreeNoOfNodes(0) + , dataTreeNoOfLeafs(0) , lastStatus(UA_STATUSCODE_BADSERVERNOTCONNECTED) , lastReason(ProcessReason::connectionLoss) { - UA_NodeId_init(&nodeid); + UA_NodeId_init(&nodeId); if (linkinfo.subscription != "" && linkinfo.monitor) { subscription = SubscriptionOpen62541::find(linkinfo.subscription); subscription->addItemOpen62541(this); @@ -61,18 +63,18 @@ ItemOpen62541::~ItemOpen62541 () { subscription->removeItemOpen62541(this); session->removeItemOpen62541(this); - UA_NodeId_clear(&nodeid); + UA_NodeId_clear(&nodeId); } void ItemOpen62541::rebuildNodeId () { UA_UInt16 ns = session->mapNamespaceIndex(linkinfo.namespaceIndex); - UA_NodeId_clear(&nodeid); + UA_NodeId_clear(&nodeId); if (linkinfo.identifierIsNumeric) { - nodeid = UA_NODEID_NUMERIC(ns, linkinfo.identifierNumber); + nodeId = UA_NODEID_NUMERIC(ns, linkinfo.identifierNumber); } else { - nodeid = UA_NODEID_STRING_ALLOC(ns, linkinfo.identifierString.c_str()); + nodeId = UA_NODEID_STRING_ALLOC(ns, linkinfo.identifierString.c_str()); } registered = false; } @@ -90,8 +92,8 @@ ItemOpen62541::show (int level) const { std::cout << "item" << " ns="; - if (nodeid.namespaceIndex != linkinfo.namespaceIndex) - std::cout << nodeid.namespaceIndex << "(" << linkinfo.namespaceIndex << ")"; + if (nodeId.namespaceIndex != linkinfo.namespaceIndex) + std::cout << nodeId.namespaceIndex << "(" << linkinfo.namespaceIndex << ")"; else std::cout << linkinfo.namespaceIndex; if (linkinfo.identifierIsNumeric) @@ -104,6 +106,7 @@ ItemOpen62541::show (int level) const << " dataDirty=" << (dataTreeDirty ? "y" : "n") << " context=" << linkinfo.subscription << "@" << session->getName() << " sampling=" << revisedSamplingInterval << "(" << linkinfo.samplingInterval << ")" + << " deadband=" << linkinfo.deadband << " qsize=" << revisedQueueSize << "(" << linkinfo.queueSize << ")" << " cqsize=" << linkinfo.clientQueueSize << " discard=" << (linkinfo.discardOldest ? "old" : "new") @@ -115,10 +118,13 @@ ItemOpen62541::show (int level) const << " monitor=" << (linkinfo.monitor ? "y" : "n") << " registered="; if (registered) - std::cout << nodeid; - else std::cout << "-"; - std::cout << "(" << (linkinfo.registerNode ? "y" : "n") << ")" - << std::endl; + std::cout << nodeId; + else + std::cout << "-"; + std::cout << "(" << (linkinfo.registerNode ? "y" : "n") << ")"; + if (!(dataTreeNoOfNodes == 0 && dataTreeNoOfLeafs == 1)) + std::cout << " dataNodes=" << dataTreeNoOfNodes << " dataLeafs=" << dataTreeNoOfLeafs; + std::cout << std::endl; if (level >= 1) { if (auto re = dataTree.root().lock()) { @@ -154,7 +160,7 @@ ItemOpen62541::uaToEpicsTime (const UA_DateTime &dt, const UA_UInt16 pico10) } void -ItemOpen62541::setIncomingData(const UA_DataValue &value, ProcessReason reason) +ItemOpen62541::setIncomingData(UA_DataValue &value, ProcessReason reason) { tsClient = epicsTime::getCurrent(); if (!UA_STATUS_IS_BAD(value.status)) { @@ -182,6 +188,7 @@ ItemOpen62541::setIncomingData(const UA_DataValue &value, ProcessReason reason) if (linkinfo.timestamp == LinkOptionTimestamp::data && linkinfo.timestampElement.length()) timefrom = &linkinfo.timestampElement; pd->setIncomingData(value.value, reason, timefrom); + value.value.storageType = UA_VARIANT_DATA_NODELETE; // take ownership of data } if (linkinfo.isItemRecord) { diff --git a/devOpcuaSup/open62541/ItemOpen62541.h b/devOpcuaSup/open62541/ItemOpen62541.h index 146de8bf..ce8d3197 100644 --- a/devOpcuaSup/open62541/ItemOpen62541.h +++ b/devOpcuaSup/open62541/ItemOpen62541.h @@ -1,5 +1,5 @@ /*************************************************************************\ -* Copyright (c) 2018-2023 ITER Organization. +* Copyright (c) 2018-2026 ITER Organization. * This module is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. \*************************************************************************/ @@ -17,9 +17,13 @@ #include "ElementTree.h" #include "SessionOpen62541.h" +#include + namespace DevOpcua { +class SubscriptionOpen62541; class DataElementOpen62541; +class DataElementOpen62541Node; /** * @brief The ItemOpen62541 inplementation of an OPC UA item. @@ -29,6 +33,8 @@ class DataElementOpen62541; class ItemOpen62541 : public Item { friend class DataElementOpen62541; + friend class DataElementOpen62541Node; + friend class DataElementOpen62541Leaf; public: /** @@ -94,13 +100,13 @@ class ItemOpen62541 : public Item * @brief Setter for the node id of this item. * @return node id */ - void setRegisteredNodeId(const UA_NodeId &id) { UA_NodeId_copy(&id, &nodeid); registered = true; } + void setRegisteredNodeId(const UA_NodeId &id) { UA_NodeId_copy(&id, &nodeId); registered = true; } /** * @brief Getter that returns the node id of this item. * @return node id */ - const UA_NodeId &getNodeId() const { return nodeid; } + const UA_NodeId &getNodeId() const { return nodeId; } /** * @brief Setter for the status of a read operation. @@ -154,7 +160,7 @@ class ItemOpen62541 : public Item * @param value new value for this data element * @param reason reason for this value update */ - void setIncomingData(const UA_DataValue &value, ProcessReason reason); + void setIncomingData(UA_DataValue &value, ProcessReason reason); /** * @brief Push an incoming event down the root element. @@ -202,13 +208,15 @@ class ItemOpen62541 : public Item private: SubscriptionOpen62541 *subscription; /**< raw pointer to subscription (if monitored) */ SessionOpen62541 *session; /**< raw pointer to session */ - UA_NodeId nodeid; /**< node id of this item */ + UA_NodeId nodeId; /**< node id of this item */ bool registered; /**< flag for registration status */ UA_Double revisedSamplingInterval; /**< server-revised sampling interval */ UA_UInt32 revisedQueueSize; /**< server-revised queue size */ - ElementTree dataTree; /**< data element tree */ + ElementTree dataTree; /**< data element tree */ epicsMutex dataTreeWriteLock; /**< lock for dirty flag */ bool dataTreeDirty; /**< true if any element has been modified */ + unsigned int dataTreeNoOfNodes; /**< number of nodes */ + unsigned int dataTreeNoOfLeafs; /**< number of leafs */ UA_StatusCode lastStatus; /**< status code of most recent service */ ProcessReason lastReason; /**< most recent processing reason */ epicsTime tsClient; /**< client (local) time stamp */ diff --git a/devOpcuaSup/open62541/Makefile.config b/devOpcuaSup/open62541/Makefile.config index d1b82cb7..2c34a3ec 100644 --- a/devOpcuaSup/open62541/Makefile.config +++ b/devOpcuaSup/open62541/Makefile.config @@ -21,19 +21,23 @@ USR_DBDFLAGS += -I $(OPCUA)/open62541 ifeq ($(OPEN62541_USE_CRYPTO),YES) USR_CXXFLAGS += -DHAS_SECURITY endif + +ifeq ($(OPEN62541_USE_XMLPARSER),YES) USR_CXXFLAGS += -DHAS_XMLPARSER ifneq ($(filter -D_MINGW,$(OP_SYS_CPPFLAGS)),) USR_INCLUDES += -I$(MSYSTEM_PREFIX)/include/libxml2 else USR_INCLUDES += -I/usr/include/libxml2 endif +endif opcua_SRCS += Session.cpp opcua_SRCS += SessionOpen62541.cpp opcua_SRCS += Subscription.cpp opcua_SRCS += SubscriptionOpen62541.cpp opcua_SRCS += ItemOpen62541.cpp -opcua_SRCS += DataElementOpen62541.cpp +opcua_SRCS += DataElementOpen62541Node.cpp +opcua_SRCS += DataElementOpen62541Leaf.cpp DBD_INSTALLS += opcua.dbd diff --git a/devOpcuaSup/open62541/README.md b/devOpcuaSup/open62541/README.md index 4d57898c..10a5b247 100644 --- a/devOpcuaSup/open62541/README.md +++ b/devOpcuaSup/open62541/README.md @@ -1,237 +1,6 @@ -# Open62541 Client C Library +# Using the open62541 Client C Library -## Status - -:warning: -This module is under development and still experimental. - -**Structured OPC UA data types are not supported at this time.** - -:warning: -Please contact the authors -[Dirk Zimoch](mailto:dirk.zimoch@psi.ch) and -[Ralph Lange](mailto:ralph.lange@gmx.de) for details. - -## Prerequisites - -* [Open62541](https://www.open62541.org/) SDK / C library - (v1.2 and v1.3 series have been tested; their system packages should also work. - open62541 v1.4 changed the header file structure and does not work yet.) - -* Cmake (3.x) if you're building Open62541 from sources. - -* For OPC UA security support (authentication/encryption), we suggest using the openssl plugin for Open62541. For that to work, you need openssl on your system - both when compiling the client library and when generating any binaries (IOCs). - On Linux, the name of the package you have to install depends on the distribution: development packages are called `openssl-devel` on RedHat/CentOS/Fedora and `libssl-dev` on Debian/Ubuntu, runtime packages are called `openssl` on RedHat/CentOS/Fedora and `libssl` on Debian/Ubuntu, respectively. - See below for Windows instructions. - In your `CONFIG_SITE.local` file (see below), enable the OPC UA Security option. - -## Building Open62541 - -Note: -The Open62541 project is focused on the server implementation of OPC UA. -The client functionality is fully supported, complete and usable, -but it does not get the attention that the server parts get. - -Do *not* use the download link on the open62541 web site. -Use their GitHub Release Page instead. - -### Bugfix for Shared Build - -The 1.3 release series need the following fix to be applied -when building shared libraries: - -```Diff ---- src/ua_types.c.orig 2024-09-02 11:31:15.514006029 +0200 -+++ src/ua_types.c 2024-09-02 11:31:29.499010032 +0200 -@@ -1882,7 +1882,7 @@ - } - - #ifdef UA_ENABLE_TYPEDESCRIPTION --UA_Boolean -+UA_Boolean UA_EXPORT - UA_DataType_getStructMember(const UA_DataType *type, const char *memberName, - size_t *outOffset, const UA_DataType **outMemberType, - UA_Boolean *outIsArray) { -``` - -### On Linux - -* Unpack the open62541 distribution. Create a build directory on the top level and `cd` into it. We'll use the usual convention of calling it `build` . - -* The cmake build of Open62541 creates a static library by default. This type of library is needed for the EMBED type of Device Support build (see below). - - To create shared libraries, build the Open62541 library setting the cmake option `BUILD_SHARED_LIBS=ON`. - - ```Shell - cmake .. -DBUILD_SHARED_LIBS=ON [...] - ``` - - You can create both types by running two builds with different configurations in the same build directory. Remove the CMakeCache in between to get clean configurations. - -* Open62541 supports multiple low-level libraries to implement OPC UA Security. The most widely used option (that also keeps things simple by staying in line with the UA SDK client) is based on openssl. We suggest to select that by setting the cmake option `UA_ENABLE_ENCRYPTION=OPENSSL`. - -* Given all the above, for a Linux build, a reasonable option setting would be: - - ```shell - cmake .. -DBUILD_SHARED_LIBS=ON \ - -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DUA_ENABLE_ENCRYPTION=OPENSSL - ``` - - to create a shared library, and - - ```shell - cmake .. -DBUILD_SHARED_LIBS=OFF \ - -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF \ - -DUA_ENABLE_ENCRYPTION=OPENSSL - ``` - - to create the static variant. - - There are a lot of other options available, but most of them affect only the server part and are not relevant for use of the client. - - For Open62541 version 1.2, set `UA_ENABLE_ENCRYPTION_OPENSSL=ON` instead of the OPENSSL setting shown above. You may also have to explicitly set the Open62541 version string in file `CMakeLists.txt`, as the auto-detection doesn't work when you're not working under git. - -* The default installation location is below `/usr/local`. Normally, this is a system location, so that the deploy mode setting (see below) would be `SYSTEM`. - If you want to install into another location, set the option `CMAKE_INSTALL_PREFIX=/other/location`. In that case, the deploy mode setting (see below) would be `PROVIDED` and the library location needs to be set. - The same is true - if you want to use the Open62541 library - in its build location (not recommended). - The library location (set in `CONFIG_SITE.local`) - will be `$(OPEN62541)/build/bin` or `$(OPEN62541)/build/bin/Release` - or something different, - depending on your installation, OS and choice of build directory name. - -* After running `cmake`, run `make` followed by `make install`. Installation into a system location will need root permission. - -### On Windows - -See the [Windows Installation How-To][windows-howto] for an overview and introduction to the different compilation options under Windows. - -#### Using MSYS2/MinGW - -* Install the necessary tools and dependencies using the `pacman` system package manager. You will need `mingw-w64-x86_64-cmake`, `mingw-w64-x86_64-libxml2` and `mingw-w64-x86_64-openssl`. - -* Unpack the open62541 distribution. Create a build directory on the top level and `cd` into it. We'll use the usual convention of calling it `build` . - -* A reasonable option setting for running `cmake` would look similar to the Linux variant: - - ```shell - cmake .. -G "MinGW Makefiles" \ - -DBUILD_SHARED_LIBS=ON \ - -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DUA_ENABLE_ENCRYPTION=OPENSSL - ``` - -* Run `mingw32-make` followed by `mingw32-make install`. - -#### Using MSVC - -* Get the necessary prerequisites and install them: - Python version 3 can be found [here][python]. - `libml2` and `iconv` binaries for Windows can be found [here][libxml2-mhils] or [here][libxml2-xmlsoft]. Install them in an appropriate location. - You might have to rename `.dll.a` DLL export stubs to `.lib` to work properly with the EPICS build system. - `openssl` can be found [here][openssl.slpro]. The installer makes sure that MSVC finds it (without explicit configuration). - -* Unpack the open62541 distribution. Create a build directory on the top level and `cd` into it. We'll use the usual convention of calling it `build` . - -* A reasonable option setting for running `cmake` would look like this: - - ```shell - cmake .. -DBUILD_SHARED_LIBS=ON \ - -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DUA_ENABLE_ENCRYPTION=OPENSSL - ``` - - plus `-DCMAKE_INSTALL_PREFIX=/other/location` if you want to select where things are installed. - - The cmake configuration seems to have a bug (seen on 1.3.6) that always selects `Release` for the path of the library to install. The easiest fix is to replace that string with the appropriate path (`Debug`) in two places in the block between lines 35 and 55 of the file `cmake_install.cmake`. - -* run `cmake --build .` followed by `cmake --install .` to build and install the library. - -## Building the device support module - -Inside the `configure` subdirectory or one level above the TOP location, create a file `RELEASE.local` that sets `EPICS_BASE` to the absolute path of your EPICS installation. - -Inside the `configure` subdirectory or one level above the TOP location, create a file `CONFIG_SITE.local` that sets the absolute path of your Open62541 installation as well as its build and deploy features if necessary. -You also need to configure the locations of the other dependencies that you installed. - -```Makefile -# Path to the Open62541 C++ installation -OPEN62541 = /other/location - -# How the Open62541 shared libraries are deployed -# SYSTEM = shared libs are in a system location -# PROVIDED = shared libs are in $(OPEN62541_SHRLIB_DIR) -# INSTALL = shared libs are installed (copied) into this module -# EMBED = link Open62541 code statically into libopcua, -# the Open62541 libraries are not required on target system -OPEN62541_DEPLOY_MODE = PROVIDED -OPEN62541_LIB_DIR = $(OPEN62541)/lib -OPEN62541_SHRLIB_DIR = $(OPEN62541_LIB_DIR) -# How the Open62541 libraries were built -OPEN62541_USE_CRYPTO = YES -``` - -The Open62541 related configuration only has to be done in this module, which creates a `CONFIG_OPCUA` file that is automatically included by all downstream modules (those creating IOCs), so that the configuration is always consistent. - -### On Windows - -All paths in EPICS build configuration files must use Windows "short names" where needed, e.g. - -```Makefile -OPEN62541 = C:/PROGRA~2/open62541-1.3.6-MinGW/ -``` - -If you're using MSYS2/MinGW64, you need to set the location of the MinGW64 installation in a CONFIG_SITE file, e.g. - -```makefile -# Location of MSYS2/MinGW64 -MSYSTEM_PREFIX = C:/msys64/mingw64 -``` - -When using MSVC you need to add the settings for the dependencies that you installed in non-system locations: - -```makefile -# Prerequisites: libxml2 iconv -LIBXML2 = C:/dependency/location/libxml2-2.9.3 -ICONV = C:/dependency/location/iconv-1.14 -``` - -*Note:* -As always on Windows, the build environment as well as the runtime environment (for any IOC) need to have the locations of all DLLs in the PATH to be able to run binaries (which is part of the build for the unit tests). You will need to add the appropriate locations for EPICS Base, the opcua module, the open62541 library and all other dependencies you installed. -Alternatively, you can put copies of all needed DLLs into the location of the binaries. (Remember that `make distclean` will remove those copies.) - -#### DLL Hell - -Be aware that over time, Windows systems will have a large number of possibly different DLLs with the same names and/or versions being used concurrently on the system - a situation that is hard to maintain and known as [DLL Hell](https://en.wikipedia.org/wiki/DLL_Hell). - -If you want your IOC binaries to be deployable -without depending on specific DLLs being present on the target system, -consider linking your IOCs statically. - -## Feedback / Reporting issues - -Please use the GitHub project's [issue tracker](https://github.com/ralphlange/opcua/issues). - -## Known issues - -n/a - -## Credits - -The adaption of the Open62541 client library has been done by Dirk Zimoch (PSI) with help from Carsten Winkler (HZB/BESSY II). Additional EPICS build system magic by Ralph Lange (ITER). - -## License - -This module is distributed subject to a Software License Agreement found -in file LICENSE that is included with this distribution. - - - -[windows-howto]: https://docs.epics-controls.org/projects/how-tos/en/latest/getting-started/installation-windows.html -[python]: https://www.python.org/downloads/windows/ -[libxml2-mhils]: https://github.com/mhils/libxml2-win-binaries/releases -[libxml2-xmlsoft]: http://xmlsoft.org/sources/win32/64bit/ -[openssl.slpro]: https://slproweb.com/products/Win32OpenSSL.html +Please refer to the +[**documentation**](https://epics-modules.github.io/opcua/tutorials/20_build_client_lib.html#option-a-open62541-client-sdk) +for instructions how to build the module +using the open62541 low-level client. diff --git a/devOpcuaSup/open62541/Session.cpp b/devOpcuaSup/open62541/Session.cpp index 25e46348..864a113a 100644 --- a/devOpcuaSup/open62541/Session.cpp +++ b/devOpcuaSup/open62541/Session.cpp @@ -11,9 +11,9 @@ */ #define epicsExportSharedSymbols -#include "SessionOpen62541.h" #include "Session.h" -#include "Registry.h" +#include "OpcuaRegistry.h" +#include "SessionOpen62541.h" #include "devOpcua.h" #include diff --git a/devOpcuaSup/open62541/SessionOpen62541.cpp b/devOpcuaSup/open62541/SessionOpen62541.cpp index 3694422b..ffe03e40 100644 --- a/devOpcuaSup/open62541/SessionOpen62541.cpp +++ b/devOpcuaSup/open62541/SessionOpen62541.cpp @@ -15,7 +15,6 @@ #include "SessionOpen62541.h" #include "SubscriptionOpen62541.h" #include "DataElementOpen62541.h" -#include "ItemOpen62541.h" #include "linkParser.h" #ifdef HAS_XMLPARSER @@ -46,9 +45,8 @@ #include #include #include -#include -#include #include +#include #include /* loadFile helper from open62541 examples */ @@ -107,7 +105,7 @@ operator << (std::ostream& os, const UA_Variant &ua_variant) if (ua_variant.type == nullptr) return os << "NO_TYPE"; UA_String s = UA_STRING_NULL; if (UA_Variant_isScalar(&ua_variant)) { - if (ua_variant.type == &UA_TYPES[UA_TYPES_DATETIME]) { + if (ua_variant.type->typeKind == UA_DATATYPEKIND_DATETIME) { // UA_print does not correct printed time for time zone UA_Int64 tOffset = UA_DateTime_localTimeUtcOffset(); UA_DateTime dt = *static_cast(ua_variant.data); @@ -116,7 +114,16 @@ operator << (std::ostream& os, const UA_Variant &ua_variant) } else { UA_print(ua_variant.data, ua_variant.type, &s); } - os << s << " (" << ua_variant.type->typeName << ')'; + os << s << " ("; + switch (ua_variant.type->typeKind) { + case UA_DATATYPEKIND_ENUM: + case UA_DATATYPEKIND_UNION: + case UA_DATATYPEKIND_STRUCTURE: + case UA_DATATYPEKIND_OPTSTRUCT: + os << typeKindName(ua_variant.type->typeKind) << ' '; + break; + } + os << ua_variant.type->typeName << ')'; } else { os << s << '{'; char* data = static_cast(ua_variant.data); @@ -127,7 +134,16 @@ operator << (std::ostream& os, const UA_Variant &ua_variant) os << s; UA_String_clear(&s); } - os << s << "} (" << ua_variant.type->typeName + os << s << "} ("; + switch (ua_variant.type->typeKind) { + case UA_DATATYPEKIND_ENUM: + case UA_DATATYPEKIND_UNION: + case UA_DATATYPEKIND_STRUCTURE: + case UA_DATATYPEKIND_OPTSTRUCT: + os << typeKindName(ua_variant.type->typeKind) << ' '; + break; + } + os << ua_variant.type->typeName << '[' << ua_variant.arrayLength << "])"; } UA_String_clear(&s); @@ -138,8 +154,9 @@ inline static std::ostream& operator << (std::ostream& os, UA_SecureChannelState channelState) { switch (channelState) { +#if UA_OPEN62541_VER_MAJOR*100+UA_OPEN62541_VER_MINOR < 104 case UA_SECURECHANNELSTATE_FRESH: return os << "Fresh"; -#if UA_OPEN62541_VER_MAJOR*100+UA_OPEN62541_VER_MINOR >= 104 +#else case UA_SECURECHANNELSTATE_REVERSE_LISTENING: return os << "ReverseListening"; case UA_SECURECHANNELSTATE_CONNECTING: return os << "Connecting"; case UA_SECURECHANNELSTATE_CONNECTED: return os << "Connected"; @@ -220,15 +237,16 @@ operator != (const std::string& str, const UA_String& ua_string) return !(str == ua_string); } -inline std::string& -operator += (std::string& str, const UA_String& ua_string) +inline std::string +to_string (const UA_String& ua_string) { - return str.append(reinterpret_cast(ua_string.data), ua_string.length); + return std::string(reinterpret_cast(ua_string.data), ua_string.length); } -const char* typeKindName(UA_UInt32 typeKind) +const char* typeKindName(int typeKind) { static const char* typeKindNames[] = { + "None", "Boolean", "SByte", "Byte", @@ -262,8 +280,8 @@ const char* typeKindName(UA_UInt32 typeKind) "BitfieldCluster", "???" }; - if (typeKind > 31) typeKind = 31; - return typeKindNames[typeKind]; + if (typeKind < -1 || typeKind > 31) typeKind = 31; + return typeKindNames[typeKind+1]; } @@ -327,6 +345,9 @@ SessionOpen62541::SessionOpen62541 (const std::string &name, , channelState(UA_SECURECHANNELSTATE_CLOSED) , sessionState(UA_SESSIONSTATE_CLOSED) , connectStatus(UA_STATUSCODE_BADINVALIDSTATE) + , needsInit(false) + , MaxNodesPerRead(0) + , MaxNodesPerWrite(0) , workerThread(nullptr) { sessions.insert({name, this}); @@ -391,12 +412,18 @@ SessionOpen62541::setOption (const std::string &name, const std::string &value) debug = ul; UA_ClientConfig *config = UA_Client_getConfig(client); if (config) { - // Loglevels: 0:trace, 1:debug, 2:info, 3:warning, 4:error, 5:fatal (and higher) - // Our debug=0 shall only print fatal errors. - // After that, the higher debug the lower UA_LogLevel, down to 0. + // Starting from v1.4, the UA_LogLevel enum has changed numerical values + UA_LogLevel loglevel = static_cast(std::max( + UA_LOGLEVEL_TRACE, + static_cast(UA_LOGLEVEL_FATAL - (UA_LOGLEVEL_DEBUG - UA_LOGLEVEL_TRACE) * debug))); +#if UA_OPEN62541_VER_MAJOR * 100 + UA_OPEN62541_VER_MINOR < 104 if (config->logger.clear) - config->logger.clear(config->logger.context); // Use context as opaque handle only! - config->logger = UA_Log_Stdout_withLevel(static_cast(std::max(0, 5-debug))); + config->logger.clear(config->logger.context); + config->logger = UA_Log_Stdout_withLevel(loglevel); +#else + if (config->logging) + *(config->logging) = UA_Log_Stdout_withLevel(loglevel); +#endif } } else if (name == "batch-nodes") { errlogPrintf("DEPRECATED: option 'batch-nodes'; use 'nodes-max' instead\n"); @@ -461,6 +488,7 @@ SessionOpen62541::setOption (const std::string &name, const std::string &value) long SessionOpen62541::connect (bool manual) { + Guard G(clientlock); if (isConnected()) { if (debug || manual) std::cerr << "Session " << name @@ -470,9 +498,6 @@ SessionOpen62541::connect (bool manual) return 0; } - if (client) - disconnect(); // Do a proper disconnection before attempting to reconnect - setupClientSecurityInfo(securityInfo, &name, debug); if (!client) { @@ -484,19 +509,29 @@ SessionOpen62541::connect (bool manual) return -1; } } + // client is guaranteed to be non-NULL at this point, same for config UA_ClientConfig *config = UA_Client_getConfig(client); - if (debug < 5) { - if (config->logger.clear) - config->logger.clear(config->logger.context); - config->logger = UA_Log_Stdout_withLevel(static_cast(std::max(0, 5-debug))); - } + + // Starting from v1.4, the UA_LogLevel enum has changed numerical values + UA_LogLevel loglevel = static_cast( + std::max(UA_LOGLEVEL_TRACE, + static_cast(UA_LOGLEVEL_FATAL - (UA_LOGLEVEL_DEBUG - UA_LOGLEVEL_TRACE) * debug))); +#if UA_OPEN62541_VER_MAJOR * 100 + UA_OPEN62541_VER_MINOR < 104 + if (config->logger.clear) + config->logger.clear(config->logger.context); + config->logger = UA_Log_Stdout_withLevel(loglevel); +#else + if (config->logging) + *(config->logging) = UA_Log_Stdout_withLevel(loglevel); +#endif + #ifdef HAS_SECURITY // We need the client certificate before UA_ClientConfig_setDefaultEncryption UA_ClientConfig_setDefaultEncryption(config, securityInfo.clientCertificate, securityInfo.privateKey, NULL, 0, NULL, 0); -#ifdef __linux__ /* UA_CertificateVerification_CertFolders supported only for Linux so far */ + #ifdef __linux__ /* UA_CertificateVerification_CertFolders supported only for Linux so far */ if (securityCertificateTrustListDir.length() || securityIssuersCertificatesDir.length()) { if (debug) { @@ -513,10 +548,11 @@ SessionOpen62541::connect (bool manual) errlogPrintf("OPC UA session %s: setting up PKI context failed with status %s\n", name.c_str(), UA_StatusCode_name(status)); } -#endif // #ifdef __linux__ + #endif // #ifdef __linux__ #else // #ifdef HAS_SECURITY UA_ClientConfig_setDefault(config); #endif + config->clientDescription.applicationType = UA_APPLICATIONTYPE_CLIENT; config->clientDescription.applicationName = UA_LOCALIZEDTEXT_ALLOC("en-US", "EPICS IOC"); config->clientDescription.productUri = UA_STRING_ALLOC("urn:EPICS:IOC"); @@ -587,26 +623,30 @@ SessionOpen62541::connect (bool manual) long SessionOpen62541::disconnect () { - if (!client) { - if (debug) - std::cerr << "Session " << name - << " already disconnected" - << std::endl; - return 0; - } { Guard G(clientlock); - if(!client) return 0; - clearCustomTypeDictionaries(); - UA_Client_delete(client); // This also deletes all open62541 subscriptions - client = nullptr; + if (client) { + clearCustomTypeDictionaries(); + UA_Client_disconnect(client); + UA_Client_delete(client); // This also deletes all open62541 subscriptions + client = nullptr; + } + sessionState = UA_SESSIONSTATE_CLOSED; + channelState = UA_SECURECHANNELSTATE_CLOSED; + markConnectionLoss(); } + // Worker thread terminates when client was destroyed if (workerThread) { - workerThread->exitWait(); - delete workerThread; - workerThread = nullptr; + if (epicsThreadGetIdSelf() != workerThread->getId()) { + workerThread->exitWait(); + delete workerThread; + workerThread = nullptr; + } else { + // Called from worker thread itself: it will exit by itself + } } + return 0; } @@ -639,16 +679,20 @@ SessionOpen62541::processRequests (std::vector> &ba UA_ReadRequest_init(&request); request.maxAge = 0; request.timestampsToReturn = UA_TIMESTAMPSTORETURN_BOTH; - request.nodesToReadSize = batch.size(); - request.nodesToRead = static_cast(UA_Array_new(batch.size(), &UA_TYPES[UA_TYPES_READVALUEID])); + request.nodesToRead = static_cast( + UA_Array_new(batch.size() * no_of_properties_read, &UA_TYPES[UA_TYPES_READVALUEID])); UA_UInt32 i = 0; for (auto c : batch) { + UA_NodeId_copy(&c->item->getNodeId(), &request.nodesToRead[i].nodeId); + request.nodesToRead[i].attributeId = UA_ATTRIBUTEID_DATATYPE; + i++; UA_NodeId_copy(&c->item->getNodeId(), &request.nodesToRead[i].nodeId); request.nodesToRead[i].attributeId = UA_ATTRIBUTEID_VALUE; itemsToRead->push_back(c->item); i++; } + request.nodesToReadSize = i; { Guard G(clientlock); @@ -864,8 +908,7 @@ SessionOpen62541::updateNamespaceMap(const UA_String *nsArray, UA_UInt16 nsCount if (namespaceMap.size()) { nsIndexMap.clear(); for (UA_UInt16 i = 0; i < nsCount; i++) { - std::string ns; - ns += nsArray[i]; + std::string ns = to_string(nsArray[i]); auto it = namespaceMap.find(ns); if (it != namespaceMap.end()) nsIndexMap.insert({it->second, i}); @@ -945,8 +988,7 @@ SessionOpen62541::showSecurity () } for (size_t k = 0; k < endpointDescriptionsLength; k++) { - if (std::string(reinterpret_cast(endpointDescriptions[k].endpointUrl.data), - endpointDescriptions[k].endpointUrl.length).compare(0, 7, "opc.tcp") == 0) { + if (to_string(endpointDescriptions[k].endpointUrl).compare(0, 7, "opc.tcp") == 0) { char dash = '-'; std::string marker; if (isConnected() @@ -1042,8 +1084,7 @@ SessionOpen62541::setupSecurity () int selectedSecurityLevel = -1; int selectedEndpoint = -1; for (size_t k = 0; k < endpointDescriptionsLength; k++) { - if (std::string(reinterpret_cast(endpointDescriptions[k].endpointUrl.data), - endpointDescriptions[k].endpointUrl.length).compare(0, 7, "opc.tcp") == 0) { + if (to_string(endpointDescriptions[k].endpointUrl).compare(0, 7, "opc.tcp") == 0) { if (reqSecurityMode == RequestedSecurityMode::Best || OpcUaSecurityMode(reqSecurityMode) == endpointDescriptions[k].securityMode) { if (reqSecurityPolicyUri.find("#None") != std::string::npos || @@ -1391,7 +1432,12 @@ SessionOpen62541::setupIdentity() name.c_str()); } else { UA_StatusCode status = config->certificateVerification.verifyCertificate( - config->certificateVerification.context, &cert); +#if UA_OPEN62541_VER_MAJOR*100+UA_OPEN62541_VER_MINOR < 104 + config->certificateVerification.context, +#else + &config->certificateVerification, +#endif + &cert); if (UA_STATUS_IS_BAD(connectStatus)) { errlogPrintf("OPC UA session %s: identity certificate is not valid: %s\n", name.c_str(), UA_StatusCode_name(status)); @@ -1459,6 +1505,10 @@ SessionOpen62541::run () return; } status = UA_Client_run_iterate(client, 1); + if (needsInit) { + initializeSession(); + needsInit = false; + } { UnGuard U(G); epicsThreadSleep(0.01); // give other threads a chance to execute @@ -1526,6 +1576,18 @@ SessionOpen62541::getTypeIndexByName(UA_UInt16 nsIndex, const char* typeName) return UnknownType; } +// Wrapper to use lambda with capture in C callback +struct LambdaHolder { + std::function func; +}; + +static UA_StatusCode iteratorCallbackAdapter(UA_NodeId childId, UA_Boolean isInverse, + UA_NodeId referenceTypeId, void *handle) +{ + LambdaHolder *holder = static_cast(handle); + return holder->func(childId, isInverse, referenceTypeId, handle); +} + void SessionOpen62541::readCustomTypeDictionaries() { @@ -1533,6 +1595,16 @@ SessionOpen62541::readCustomTypeDictionaries() UA_ClientConfig *config = UA_Client_getConfig(client); if (debug) std::cout << "Session " << name + << ": reading Enums" + << std::endl; + UA_Client_forEachChildNodeCall(client, UA_NODEID_NUMERIC(0, UA_NS0ID_ENUMERATION), + [] (UA_NodeId childNodeId, UA_Boolean isInverse, UA_NodeId referenceTypeId, void *handle) + { + return static_cast(handle)-> + enumIteratorCallback(childNodeId, referenceTypeId); + }, this); + if (debug) + std::cout << "\nSession " << name << ": reading type dictionaries" << std::endl; UA_Client_forEachChildNodeCall(client, UA_NODEID_NUMERIC(0, UA_NS0ID_OPCBINARYSCHEMA_TYPESYSTEM), @@ -1636,31 +1708,124 @@ SessionOpen62541::clearCustomTypeDictionaries() enumTypes.clear(); } +UA_StatusCode +SessionOpen62541::enumIteratorCallback(const UA_NodeId& childId, const UA_NodeId& referenceTypeId) +{ + UA_StatusCode status = UA_STATUSCODE_GOOD; + UA_QualifiedName typeName; + UA_QualifiedName_init(&typeName); + UA_Client_readBrowseNameAttribute(client, childId, &typeName); + UA_NodeId binaryEncodingId = UA_NODEID_NULL; + UA_NodeId_copy(&childId, &binaryEncodingId); + if (debug >= 4) + std::cout << "\nEnum " << typeName + << " { # binaryEncodingId: " << binaryEncodingId + << std::endl; + EnumChoices enumChoices; + LambdaHolder holder; + holder.func = [&enumChoices, this] (UA_NodeId childNodeId, UA_Boolean isInverse, UA_NodeId referenceTypeId, void *handle) + { + return this->enumChoiceIteratorCallback(childNodeId, referenceTypeId, enumChoices); + }; + status = UA_Client_forEachChildNodeCall(client, childId, iteratorCallbackAdapter, &holder); + if (debug >= 4) + std::cout << "};" << std::endl; + if (enumChoices.size() > 0) { + if (debug >= 5) + std::cout << "# adding enum " << typeName << " to known types" << std::endl; + enumTypes.emplace(binaryEncodingId, std::move(enumChoices)); + binaryTypeIds.emplace(to_string(typeName.name), binaryEncodingId); + + UA_DataType customDataType = {}; + customDataType.memSize = sizeof(UA_UInt32); + customDataType.typeKind = UA_DATATYPEKIND_ENUM; + customDataType.pointerFree = true; + customDataType.overlayable = UA_BINARY_OVERLAYABLE_INTEGER; +#ifndef UA_DATATYPES_USE_POINTER + customDataType.typeIndex = static_cast(customTypes.size()); +#endif + UA_NodeId_copy(&binaryEncodingId, &customDataType.binaryEncodingId); + UA_NodeId_copy(&binaryEncodingId, &customDataType.typeId); + char* ctypeName = static_cast(calloc(typeName.name.length+1, 1)); + if (!customDataType.typeName) { + status = UA_STATUSCODE_BADOUTOFMEMORY; + } else { + strncpy(ctypeName, reinterpret_cast(typeName.name.data), typeName.name.length); + customDataType.typeName = ctypeName; + customTypes.push_back(customDataType); + } + } + + UA_QualifiedName_clear(&typeName); + return status; +} + +UA_StatusCode +SessionOpen62541::enumChoiceIteratorCallback(const UA_NodeId& childId, const UA_NodeId& referenceTypeId, EnumChoices& enumChoices) +{ + UA_StatusCode status = UA_STATUSCODE_GOOD; + const UA_NodeId hasProperty = UA_NODEID_NUMERIC(0, UA_NS0ID_HASPROPERTY); + + if (UA_NodeId_equal(&referenceTypeId, &hasProperty)) + { + UA_Variant value; + UA_Variant_init(&value); + UA_Client_readValueAttribute(client, childId, &value); + if (UA_Variant_hasArrayType(&value, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT])) + { + UA_LocalizedText* choices = static_cast(value.data); + for (size_t i = 0; i < value.arrayLength; i++) { + if (debug >= 4) + std::cout << " " << i << " = " << choices[i].text << ';' << std::endl; + enumChoices.emplace(static_cast(i), to_string(choices[i].text)); + } + } + else if (UA_Variant_hasArrayType(&value, &UA_TYPES[UA_TYPES_EXTENSIONOBJECT])) + { + UA_ExtensionObject* choices = static_cast(value.data); + for (size_t i = 0; i < value.arrayLength; i++) { + if (choices[i].encoding == UA_EXTENSIONOBJECT_DECODED && + choices[i].content.decoded.type == &UA_TYPES[UA_TYPES_ENUMVALUETYPE]) + { + UA_EnumValueType *enumValue = static_cast(choices[i].content.decoded.data); + if (debug >= 4) + std::cout << " " << enumValue->value << " = " << enumValue->displayName.text << ';' << std::endl; + enumChoices.emplace(static_cast(enumValue->value), + to_string(enumValue->displayName.text)); + } + } + } + UA_Variant_clear(&value); + } + return status; +} + UA_StatusCode SessionOpen62541::typeSystemIteratorCallback(const UA_NodeId& dictNodeId) { UA_QualifiedName dictName; UA_QualifiedName_init(&dictName); + if (debug) { + // Show the dict name in debug messages + UA_Client_readBrowseNameAttribute(client, dictNodeId, &dictName); + } if (dictNodeId.namespaceIndex == 0) { // custom type dictionaries only if (debug) { - UA_Client_readBrowseNameAttribute(client, dictNodeId, &dictName); std::cout << "Session " << name - << ": ignoring types of system dict " << dictNodeId - << " " << dictName - << std::endl; + << ": ignoring system dict " << dictName + << std::endl; + UA_QualifiedName_clear(&dictName); } return UA_STATUSCODE_GOOD; } if (debug) { - UA_Client_readBrowseNameAttribute(client, dictNodeId, &dictName); std::cout << "Session " << name - << ": browsing types of custom dict " << dictNodeId - << " " << dictName + << ": browsing custom dict " << dictName + << " for binary encoding IDs" << std::endl; } - UA_QualifiedName_clear(&dictName); // Browse the dictionary for binaryTypeIds UA_Client_forEachChildNodeCall(client, dictNodeId, @@ -1676,6 +1841,11 @@ SessionOpen62541::typeSystemIteratorCallback(const UA_NodeId& dictNodeId) UA_Client_readValueAttribute(client, dictNodeId, &xmldata); if (UA_Variant_hasScalarType(&xmldata, &UA_TYPES[UA_TYPES_BYTESTRING])) { UA_ByteString* xmlstring = static_cast(xmldata.data); + if (debug >= 5) + std::cout << "\nSession " << name + << ": Data type XML of dict " << dictName + << '\n' << *xmlstring + << std::endl; xmlDocPtr xmldoc = xmlReadMemory(reinterpret_cast(xmlstring->data), static_cast(xmlstring->length), NULL, NULL, 0); if (xmldoc) { @@ -1684,21 +1854,10 @@ SessionOpen62541::typeSystemIteratorCallback(const UA_NodeId& dictNodeId) } } UA_Variant_clear(&xmldata); + UA_QualifiedName_clear(&dictName); return UA_STATUSCODE_GOOD; } -// Wrapper to use lambda with capture in C callback -struct LambdaHolder { - std::function func; -}; - -static UA_StatusCode iteratorCallbackAdapter(UA_NodeId childId, UA_Boolean isInverse, - UA_NodeId referenceTypeId, void *handle) -{ - LambdaHolder *holder = static_cast(handle); - return holder->func(childId, isInverse, referenceTypeId, handle); -} - UA_StatusCode SessionOpen62541::dictIteratorCallback(const UA_NodeId& childId, const UA_NodeId& referenceTypeId) { @@ -1710,6 +1869,10 @@ SessionOpen62541::dictIteratorCallback(const UA_NodeId& childId, const UA_NodeId UA_QualifiedName typeName; UA_QualifiedName_init(&typeName); UA_Client_readBrowseNameAttribute(client, childId, &typeName); + if (debug >= 5) + std::cout << "Session " << name + << ": type " << childId << " = " << typeName + << std::endl; LambdaHolder holder; holder.func = [&typeName, this] (UA_NodeId childNodeId, UA_Boolean isInverse, UA_NodeId referenceTypeId, void *handle) { @@ -1757,9 +1920,7 @@ SessionOpen62541::typeIteratorCallback(const UA_NodeId& childId, const UA_NodeId // Need copy because content of non-numeric (e.g. string) childId is freed after this function returns UA_NodeId binaryEncodingId = UA_NODEID_NULL; UA_NodeId_copy(&childId, &binaryEncodingId); - binaryTypeIds.emplace( - std::string(reinterpret_cast(typeName.name.data), typeName.name.length), - binaryEncodingId); + binaryTypeIds.emplace(to_string(typeName.name), binaryEncodingId); } return UA_STATUSCODE_GOOD; } @@ -1810,20 +1971,20 @@ SessionOpen62541::parseCustomDataTypes(xmlNode* node, UA_UInt16 nsIndex) break; } + auto binaryType = binaryTypeIds.find(typeName); + if (binaryType == binaryTypeIds.end()) { + if (debug) + std::cerr << "Session " << name + << ": Ignoring type " << typeName + << " which has no binaryEncodingId" << std::endl; + continue; + } + UA_NodeId_copy(&binaryType->second, &customDataType.binaryEncodingId); + if (strcmp(nodeKind, "StructuredType") == 0) { UA_UInt32 structureAlignment = 0; UA_UInt32 memberSize; - auto binaryType = binaryTypeIds.find(typeName); - if (binaryType == binaryTypeIds.end()) { - if (debug) - std::cerr << "Session " << name - << ": Ignoring type " << typeName - << " which has no binaryEncodingId" << std::endl; - continue; - } - UA_NodeId_copy(&binaryType->second, &customDataType.binaryEncodingId); - const char* baseType = getProp(node, "BaseType"); if (baseType && strcmp(baseType, "ua:Union") == 0) customDataType.typeKind = UA_DATATYPEKIND_UNION; @@ -2133,9 +2294,10 @@ SessionOpen62541::parseCustomDataTypes(xmlNode* node, UA_UInt16 nsIndex) customDataType.overlayable = UA_BINARY_OVERLAYABLE_INTEGER; if (debug >= 4) - std::cout << "\nenum " << typeName << " {" + std::cout << "\nEnum " << typeName + << " { # binaryEncodingId: " << customDataType.binaryEncodingId << std::endl; - std::vector> choices; + EnumChoices enumChoices; for (xmlNode* choice = node->children; choice; choice = choice->next) { if (choice->type != XML_ELEMENT_NODE) continue; @@ -2154,10 +2316,10 @@ SessionOpen62541::parseCustomDataTypes(xmlNode* node, UA_UInt16 nsIndex) const char* choiceName = getProp(choice, "Name"); const char* choiceValue = getProp(choice, "Value"); if (debug >= 4) - std::cout << " " << choiceName << " = " << choiceValue << ';' << std::endl; - choices.emplace_back(static_cast(atoll(choiceValue)), choiceName); + std::cout << " " << choiceValue << " = " << choiceName << ';' << std::endl; + enumChoices.emplace(atol(choiceValue), choiceName); } - enumTypes.emplace(typeName, std::move(choices)); + enumTypes.emplace(customDataType.binaryEncodingId, std::move(enumChoices)); if (debug >= 4) std::cout << "};" << std::endl; } @@ -2166,13 +2328,29 @@ SessionOpen62541::parseCustomDataTypes(xmlNode* node, UA_UInt16 nsIndex) if (typeName) { if (debug >= 5) std::cout << "# adding type " << typeName << " to known types" << std::endl; - customDataType.typeId = UA_NODEID_STRING(nsIndex, const_cast(typeName)); + UA_NodeId_copy(&customDataType.binaryEncodingId, &customDataType.typeId); customDataType.typeName = strdup(typeName); customTypes.push_back(customDataType); } } } +const EnumChoices* +SessionOpen62541::getEnumChoices(const UA_NodeId* typeId) +{ + Guard G(clientlock); + if (client && typeId) { + const UA_DataType* type = UA_Client_findDataType(client, typeId); + if (type && type->typeKind == UA_DATATYPEKIND_ENUM) + { + auto enumChoices = enumTypes.find(*typeId); + if (enumChoices != enumTypes.end()) + return &enumChoices->second; + } + } + return nullptr; +} + void SessionOpen62541::showCustomDataTypes(int level) const { @@ -2184,12 +2362,10 @@ SessionOpen62541::showCustomDataTypes(int level) const std::cout << " {"; if (type.typeKind == UA_DATATYPEKIND_ENUM) { - for (auto choice: enumTypes.find(type.typeName)->second) - { - std::cout << "\n " << choice.second << " = " << choice.first; - } + for (auto it: enumTypes.at(type.typeId)) + std::cout << "\n " << it.first << " = " << it.second; } else - for (size_t i = 0; i < type.membersSize; i++) { + for (UA_UInt32 i = 0; i < type.membersSize; i++) { UA_DataTypeMember member = type.members[i]; const UA_DataType& memberType = #ifdef UA_DATATYPES_USE_POINTER @@ -2250,13 +2426,9 @@ SessionOpen62541::connectionStatusChanged ( // Deactivated by user or server shut down markConnectionLoss(); registeredItemsNo = 0; - break; - case UA_SECURECHANNELSTATE_FRESH: - if (sessionState == UA_SESSIONSTATE_CREATED) { - // The server has shut down - if (autoConnect) - autoConnector.start(); - } + // The server has shut down + if (autoConnect) + autoConnector.start(); break; case UA_SECURECHANNELSTATE_OPEN: { // Connection to server has been established @@ -2279,98 +2451,21 @@ SessionOpen62541::connectionStatusChanged ( case UA_SESSIONSTATE_ACTIVATED: { - UA_ClientConfig *config = UA_Client_getConfig(client); - config->connectivityCheckInterval = 1000; // 1 sec - - std::string token; - auto type = config->userIdentityToken.content.decoded.type; - if (type == &UA_TYPES[UA_TYPES_USERNAMEIDENTITYTOKEN]) - token = " (username token)"; - if (type == &UA_TYPES[UA_TYPES_X509IDENTITYTOKEN]) - token = " (certificate token)"; - std::ostringstream buf; - buf << "OPC UA session " << name << ": connected as '" << securityUserName << "'" - << token << " with security level " << securityLevel - << " (mode=" << config->securityMode - << "; policy=" << securityPolicyString(config->securityPolicyUri) << ")" - << std::endl; - errlogPrintf("%s", buf.str().c_str()); - if (config->securityMode == UA_MESSAGESECURITYMODE_NONE) { - errlogPrintf("OPC UA session %s: WARNING - this session uses *** NO SECURITY ***\n", - name.c_str()); - } + needsInit = true; + break; + } - // read some settings from server - UA_Variant value; - UA_StatusCode status; - unsigned int max; - - UA_Variant_init(&value); - - // max nodes per read request - status = UA_Client_readValueAttribute(client, - UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXNODESPERREAD) - , &value); - if (status == UA_STATUSCODE_GOOD && UA_Variant_hasScalarType(&value, &UA_TYPES[UA_TYPES_UINT32])) - MaxNodesPerRead = *static_cast(value.data); - UA_Variant_clear(&value); - if (MaxNodesPerRead > 0 && readNodesMax > 0) - max = std::min(MaxNodesPerRead, readNodesMax); - else - max = MaxNodesPerRead + readNodesMax; - if (max != readNodesMax) - reader.setParams(max, readTimeoutMin, readTimeoutMax); - - // max nodes per write request - status = UA_Client_readValueAttribute(client, - UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXNODESPERWRITE) - , &value); - if (status == UA_STATUSCODE_GOOD && UA_Variant_hasScalarType(&value, &UA_TYPES[UA_TYPES_UINT32])) - MaxNodesPerWrite = *static_cast(value.data); - UA_Variant_clear(&value); - if (MaxNodesPerWrite > 0 && writeNodesMax > 0) - max = std::min(MaxNodesPerWrite, writeNodesMax); - else - max = MaxNodesPerWrite + writeNodesMax; - if (max != writeNodesMax) - writer.setParams(max, writeTimeoutMin, writeTimeoutMax); - - // namespaces - status = UA_Client_readValueAttribute(client, - UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_NAMESPACEARRAY) - , &value); - if (status == UA_STATUSCODE_GOOD && UA_Variant_hasArrayType(&value, &UA_TYPES[UA_TYPES_STRING])) - updateNamespaceMap(static_cast(value.data), static_cast(value.arrayLength)); - UA_Variant_clear(&value); - - readCustomTypeDictionaries(); - rebuildNodeIds(); - registerNodes(); - createAllSubscriptions(); - addAllMonitoredItems(); - if (debug) { - std::cout << "Session " << name - << ": triggering initial read for all " - << items.size() << " items" - << std::endl; - } - auto cargo = std::vector>(items.size()); - unsigned int i = 0; - for (auto it : items) { - it->setState(ConnectionStatus::initialRead); - cargo[i] = std::make_shared(); - cargo[i]->item = it; - i++; - } - // status needs to be updated before requests are being issued - sessionState = newSessionState; - reader.pushRequest(cargo, menuPriorityHIGH); + case UA_SESSIONSTATE_CLOSED: + case UA_SESSIONSTATE_CLOSING: + { + needsInit = false; break; } case UA_SESSIONSTATE_CREATED: { if (sessionState == UA_SESSIONSTATE_ACTIVATED) errlogPrintf("OPC UA session %s: disconnected\n", name.c_str()); + needsInit = false; clearCustomTypeDictionaries(); break; } @@ -2397,7 +2492,7 @@ SessionOpen62541::readComplete (UA_UInt32 transactionId, << " (transaction id " << transactionId << "; data for " << response->resultsSize << " items)" << std::endl; - if ((*it->second).size() != response->resultsSize) + if ((*it->second).size() * no_of_properties_read != response->resultsSize) errlogPrintf("OPC UA session %s: (readComplete) received a callback " "with %llu values for a request containing %llu items\n", name.c_str(), @@ -2408,11 +2503,21 @@ SessionOpen62541::readComplete (UA_UInt32 transactionId, if (i >= response->resultsSize) { item->setIncomingEvent(ProcessReason::readFailure); } else { + const UA_DataType* type = nullptr; + if (!UA_STATUS_IS_BAD(response->results[i].status)) { + type = UA_Client_findDataType(client, static_cast(response->results[i].value.data)); + } + i++; + if (typeKindOf(type) == UA_DATATYPEKIND_ENUM && + typeKindOf(response->results[i].value.type) == UA_DATATYPEKIND_INT32) { + // Enums arrive as INT32. Tweak the type to what we find in structs for better diagnosics. + response->results[i].value.type = type; + } if (debug >= 5) { std::cout << "** Session " << name << ": (readComplete) getting data for item " << item - << " = " << response->results[i].value + << "\" = " << response->results[i].value << ' ' << UA_StatusCode_name(response->results[i].status) << std::endl; } @@ -2420,8 +2525,8 @@ SessionOpen62541::readComplete (UA_UInt32 transactionId, if (UA_STATUS_IS_BAD(response->results[i].status)) reason = ProcessReason::readFailure; item->setIncomingData(response->results[i], reason); + i++; } - i++; } outstandingOps.erase(it); } else { @@ -2557,10 +2662,97 @@ SessionOpen62541::atExit (void *) errlogPrintf("OPC UA: Disconnecting sessions\n"); for (auto &it : sessions) { it.second->disconnect(); - SessionOpen62541 *session = it.second; - if (session->isConnected()) - session->disconnect(); } } +void +SessionOpen62541::initializeSession () +{ + if (!client) return; + UA_ClientConfig *config = UA_Client_getConfig(client); + config->connectivityCheckInterval = 1000; // 1 sec + + std::string token; + auto type = config->userIdentityToken.content.decoded.type; + if (type == &UA_TYPES[UA_TYPES_USERNAMEIDENTITYTOKEN]) + token = " (username token)"; + if (type == &UA_TYPES[UA_TYPES_X509IDENTITYTOKEN]) + token = " (certificate token)"; + std::ostringstream buf; + buf << "OPC UA session " << name << ": connected as '" << securityUserName << "'" + << token << " with security level " << securityLevel + << " (mode=" << config->securityMode + << "; policy=" << securityPolicyString(config->securityPolicyUri) << ")" + << std::endl; + errlogPrintf("%s", buf.str().c_str()); + if (config->securityMode == UA_MESSAGESECURITYMODE_NONE) { + errlogPrintf("OPC UA session %s: WARNING - this session uses *** NO SECURITY ***\n", + name.c_str()); + } + + // read some settings from server + UA_Variant value; + UA_StatusCode status; + unsigned int max; + + UA_Variant_init(&value); + + // max nodes per read request + status = UA_Client_readValueAttribute(client, + UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXNODESPERREAD) + , &value); + if (status == UA_STATUSCODE_GOOD && UA_Variant_hasScalarType(&value, &UA_TYPES[UA_TYPES_UINT32])) + MaxNodesPerRead = *static_cast(value.data); + UA_Variant_clear(&value); + if (MaxNodesPerRead > 0 && readNodesMax > 0) + max = std::min(MaxNodesPerRead, readNodesMax); + else + max = MaxNodesPerRead + readNodesMax; + if (max != readNodesMax) + reader.setParams(max, readTimeoutMin, readTimeoutMax); + + // max nodes per write request + status = UA_Client_readValueAttribute(client, + UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXNODESPERWRITE) + , &value); + if (status == UA_STATUSCODE_GOOD && UA_Variant_hasScalarType(&value, &UA_TYPES[UA_TYPES_UINT32])) + MaxNodesPerWrite = *static_cast(value.data); + UA_Variant_clear(&value); + if (MaxNodesPerWrite > 0 && writeNodesMax > 0) + max = std::min(MaxNodesPerWrite, writeNodesMax); + else + max = MaxNodesPerWrite + writeNodesMax; + if (max != writeNodesMax) + writer.setParams(max, writeTimeoutMin, writeTimeoutMax); + + // namespaces + status = UA_Client_readValueAttribute(client, + UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_NAMESPACEARRAY) + , &value); + if (status == UA_STATUSCODE_GOOD && UA_Variant_hasArrayType(&value, &UA_TYPES[UA_TYPES_STRING])) + updateNamespaceMap(static_cast(value.data), static_cast(value.arrayLength)); + UA_Variant_clear(&value); + + readCustomTypeDictionaries(); + rebuildNodeIds(); + registerNodes(); + createAllSubscriptions(); + if (debug) { + std::cout << "Session " << name + << ": triggering initial read for all " + << items.size() << " items" + << std::endl; + } + auto cargo = std::vector>(items.size()); + unsigned int i = 0; + for (auto it : items) { + it->setState(ConnectionStatus::initialRead); + cargo[i] = std::make_shared(); + cargo[i]->item = it; + i++; + } + reader.pushRequest(cargo, menuPriorityHIGH); + addAllMonitoredItems(); +} + } // namespace DevOpcua diff --git a/devOpcuaSup/open62541/SessionOpen62541.h b/devOpcuaSup/open62541/SessionOpen62541.h index 868a6bf0..d69a55e1 100644 --- a/devOpcuaSup/open62541/SessionOpen62541.h +++ b/devOpcuaSup/open62541/SessionOpen62541.h @@ -13,9 +13,9 @@ #ifndef DEVOPCUA_SESSIONOPEN62541_H #define DEVOPCUA_SESSIONOPEN62541_H -#include "Session.h" -#include "Registry.h" +#include "OpcuaRegistry.h" #include "RequestQueueBatcher.h" +#include "Session.h" #include #include @@ -23,6 +23,8 @@ #include #include +#include +#include #ifndef UA_BUILTIN_TYPES_COUNT // Newer open62541 since version 1.3 uses type pointer @@ -39,8 +41,32 @@ #include #include #include +#include #include +namespace std { + +/* Allow UA_NodeId as key */ +template<> +struct hash +{ + inline std::size_t operator()(const UA_NodeId& nodeId) const noexcept + { + return UA_NodeId_hash(&nodeId); + } +}; + +template<> +struct equal_to +{ + inline bool operator()(const UA_NodeId& nodeId1, const UA_NodeId& nodeId2) const noexcept + { + return UA_NodeId_equal(&nodeId1, &nodeId2); + } +}; + +} + namespace DevOpcua { class SubscriptionOpen62541; @@ -78,7 +104,7 @@ std::ostream& operator << (std::ostream& os, const UA_NodeId& ua_nodeId); std::ostream& operator << (std::ostream& os, const UA_Variant &ua_variant); -const char* typeKindName(UA_UInt32 typeKind); +const char* typeKindName(int typeKind); // Open62541 has no ClientSecurityInfo structure // Make our own for convenience @@ -178,12 +204,13 @@ class SessionOpen62541 virtual const std::string & getName() const override; /** - * @brief Get a structure definition from the session dictionary. - * @param dataTypeId data type of the extension object - * @return structure definition + * @brief Get pointer to enumChoices if typeId refers to enum type, else nullptr */ -// UA_StructureDefinition structureDefinition(const UaNodeId &dataTypeId) -// { return puasession->structureDefinition(dataTypeId); } +#ifdef HAS_XMLPARSER + const EnumChoices* getEnumChoices(const UA_NodeId* typeId); +#else + const EnumChoices* getEnumChoices(const UA_NodeId* typeId) { return nullptr; } +#endif /** * @brief Request a beginRead service for an item @@ -371,6 +398,11 @@ class SessionOpen62541 */ virtual void run() override; + /** + * @brief Initialize session after successful activation. + */ + void initializeSession(); + // Wrapper for Session::securityPolicyString to match argument type static std::string securityPolicyString(const UA_String& policy) { @@ -411,6 +443,7 @@ class SessionOpen62541 UA_SecureChannelState channelState; /**< status for this session */ UA_SessionState sessionState; /**< status for this session */ UA_StatusCode connectStatus; /**< status for this session */ + bool needsInit; /**< initialization needed after activation */ unsigned int MaxNodesPerRead; /**< server max number of nodes per write request */ unsigned int MaxNodesPerWrite; /**< server max number of nodes per write request */ epicsThread *workerThread; /**< Asynchronous worker thread */ @@ -419,12 +452,14 @@ class SessionOpen62541 /** open62541 type dictionary handling */ std::vector customTypes; /**< descriptions of custom (non-standard) OPC-UA types */ std::map binaryTypeIds; /**< server defined binary ids of custom types */ - std::map>> enumTypes; + std::unordered_map enumTypes; /**< all the enum definitions from the server */ void readCustomTypeDictionaries(); /**< read custom types from the server */ void clearCustomTypeDictionaries(); /**< clear old custom types */ void parseCustomDataTypes(xmlNode* node, UA_UInt16 nsIndex); /**< parse XML representation of custom types */ size_t getTypeIndexByName(UA_UInt16 nsIndex, const char* typeName); UA_StatusCode typeSystemIteratorCallback(const UA_NodeId& dictNodeId); + UA_StatusCode enumIteratorCallback(const UA_NodeId& childId, const UA_NodeId& referenceTypeId); + UA_StatusCode enumChoiceIteratorCallback(const UA_NodeId& childId, const UA_NodeId& referenceTypeId, EnumChoices& enumChoices); UA_StatusCode dictIteratorCallback(const UA_NodeId& childId, const UA_NodeId& referenceTypeId); UA_StatusCode typeIteratorCallback(const UA_NodeId& childId, const UA_NodeId& referenceTypeId, const UA_QualifiedName& typeName); void showCustomDataTypes(int level) const; diff --git a/devOpcuaSup/open62541/Subscription.cpp b/devOpcuaSup/open62541/Subscription.cpp index 412d8ee8..b66c680b 100644 --- a/devOpcuaSup/open62541/Subscription.cpp +++ b/devOpcuaSup/open62541/Subscription.cpp @@ -16,7 +16,6 @@ #include #define epicsExportSharedSymbols -#include "Session.h" #include "Subscription.h" #include "SessionOpen62541.h" #include "SubscriptionOpen62541.h" diff --git a/devOpcuaSup/open62541/SubscriptionOpen62541.cpp b/devOpcuaSup/open62541/SubscriptionOpen62541.cpp index 624cb76a..0fdb8604 100644 --- a/devOpcuaSup/open62541/SubscriptionOpen62541.cpp +++ b/devOpcuaSup/open62541/SubscriptionOpen62541.cpp @@ -12,9 +12,9 @@ #define epicsExportSharedSymbols #include "SubscriptionOpen62541.h" -#include "DataElementOpen62541.h" #include "ItemOpen62541.h" -#include "Registry.h" +#include "OpcuaRegistry.h" +#include "RecordConnector.h" #include "devOpcua.h" #include @@ -124,6 +124,13 @@ SubscriptionOpen62541::getSessionOpen62541 () const void SubscriptionOpen62541::create () { + if (debug) { + std::cerr << "Subscription " << name + << ": creating with interval=" << requestedSettings.requestedPublishingInterval + << " lifetime=" << requestedSettings.requestedLifetimeCount + << " keepalive=" << requestedSettings.requestedMaxKeepAliveCount + << std::endl; + } subscriptionSettings = UA_Client_Subscriptions_create(session.client, requestedSettings, this, [] (UA_Client *client, UA_UInt32 subscriptionId, void *context, UA_StatusChangeNotification *notification) { @@ -148,6 +155,7 @@ SubscriptionOpen62541::addMonitoredItems () UA_UInt32 i; UA_MonitoredItemCreateRequest monitoredItemCreateRequest; UA_MonitoredItemCreateResult monitoredItemCreateResult; + UA_DataChangeFilter dataChangeFilter; if (items.size()) { monitoredItemCreateResult.statusCode = UA_STATUSCODE_GOOD; // suppress compiler warning @@ -161,29 +169,40 @@ SubscriptionOpen62541::addMonitoredItems () monitoredItemCreateRequest.requestedParameters.samplingInterval = it->linkinfo.samplingInterval; monitoredItemCreateRequest.requestedParameters.queueSize = it->linkinfo.queueSize; monitoredItemCreateRequest.requestedParameters.discardOldest = it->linkinfo.discardOldest; + if (it->linkinfo.deadband > 0.0) { + UA_DataChangeFilter_init(&dataChangeFilter); + dataChangeFilter.deadbandType = UA_DEADBANDTYPE_ABSOLUTE; + dataChangeFilter.deadbandValue = it->linkinfo.deadband; + dataChangeFilter.trigger = UA_DATACHANGETRIGGER_STATUSVALUE; + UA_ExtensionObject *filter = &monitoredItemCreateRequest.requestedParameters.filter; + filter->content.decoded.data = &dataChangeFilter; + filter->content.decoded.type = &UA_TYPES[UA_TYPES_DATACHANGEFILTER]; + filter->encoding = UA_EXTENSIONOBJECT_DECODED; + } monitoredItemCreateResult = UA_Client_MonitoredItems_createDataChange( session.client, subscriptionSettings.subscriptionId, UA_TIMESTAMPSTORETURN_BOTH, - monitoredItemCreateRequest, items[i], [] (UA_Client *client, UA_UInt32 subId, void *subContext, + monitoredItemCreateRequest, it, [] (UA_Client *client, UA_UInt32 subId, void *subContext, UA_UInt32 monId, void *monContext, UA_DataValue *value) { static_cast(subContext)-> dataChange(monId, *static_cast(monContext), value); }, nullptr /* deleteCallback */); if (monitoredItemCreateResult.statusCode == UA_STATUSCODE_GOOD) { - items[i]->setRevisedSamplingInterval(monitoredItemCreateResult.revisedSamplingInterval); - items[i]->setRevisedQueueSize(monitoredItemCreateResult.revisedQueueSize); - } - if (debug >= 5) { - if (monitoredItemCreateResult.statusCode == UA_STATUSCODE_GOOD) - std::cout << "** Monitored item " << monitoredItemCreateRequest.itemToMonitor.nodeId + it->setRevisedSamplingInterval(monitoredItemCreateResult.revisedSamplingInterval); + it->setRevisedQueueSize(monitoredItemCreateResult.revisedQueueSize); + if (debug >= 5) { + std::cout << "** OPC UA record " << it->recConnector->getRecordName() + << " monitored item " << monitoredItemCreateRequest.itemToMonitor.nodeId << " succeeded with id " << monitoredItemCreateResult.monitoredItemId << " revised sampling interval " << monitoredItemCreateResult.revisedSamplingInterval << " revised queue size " << monitoredItemCreateResult.revisedQueueSize << std::endl; - else - std::cout << "** Monitored item " << monitoredItemCreateRequest.itemToMonitor.nodeId - << " failed with error " - << UA_StatusCode_name(monitoredItemCreateResult.statusCode) - << std::endl; + } + } else { + std::cerr << "OPC UA record " << it->recConnector->getRecordName() + << " monitored item " << monitoredItemCreateRequest.itemToMonitor.nodeId + << " failed with error " << UA_StatusCode_name(monitoredItemCreateResult.statusCode) + << std::endl; + it->setIncomingEvent(ProcessReason::connectionLoss); } i++; } diff --git a/devOpcuaSup/open62541/SubscriptionOpen62541.h b/devOpcuaSup/open62541/SubscriptionOpen62541.h index 75530260..948e2ae3 100644 --- a/devOpcuaSup/open62541/SubscriptionOpen62541.h +++ b/devOpcuaSup/open62541/SubscriptionOpen62541.h @@ -13,8 +13,8 @@ #ifndef DEVOPCUA_SUBSCRIPTIONOPEN62541_H #define DEVOPCUA_SUBSCRIPTIONOPEN62541_H +#include "OpcuaRegistry.h" #include "Subscription.h" -#include "Registry.h" #include diff --git a/documentation/Building from source with open62541 - Debian-12.txt b/documentation/Building from source with open62541 - Debian-12.txt new file mode 100644 index 00000000..6e448af7 --- /dev/null +++ b/documentation/Building from source with open62541 - Debian-12.txt @@ -0,0 +1,86 @@ +# Container debian:12 + +# Install general dependencies + +apt update +apt install -y build-essential curl + +# Install EPICS dependencies +apt install -y libreadline-dev + +# Install open62541 dependencies +apt install -y cmake python3 libssl-dev libxml2-dev + +# Create installation dir cd into it +mkdir /opt/epics +cd /opt/epics + +# Download EPICS Base, OPCUA Device Support, open62541 +curl -L https://github.com/epics-base/epics-base/releases/download/R7.0.8.1/base-7.0.8.1.tar.gz | tar xzf - +curl -L https://github.com/epics-modules/opcua/archive/refs/tags/v0.10.0.tar.gz | tar xzf - +curl -L https://github.com/open62541/open62541/archive/refs/tags/v1.3.15.tar.gz | tar xzf - + +# Configure EPICS Base (set C++ standard to C++11) +cat >> base-7.0.8.1/configure/os/CONFIG_SITE.Common.linuxCommon < RELEASE.local < opcua-0.10.0/configure/CONFIG_SITE.local <<\EOF +# Path to the Open62541 installation +OPEN62541 = /usr/local + +# How the Open62541 SDK shared libraries are deployed +# SYSTEM = shared libs are in a system location +# PROVIDED = shared libs are in $(OPEN62541_SHRLIB_DIR) +# INSTALL = shared libs are installed (copied) into this module +# EMBED = link SDK code statically into libopcua, +# SDK libraries are not required on target system +OPEN62541_DEPLOY_MODE = PROVIDED +OPEN62541_LIB_DIR = $(OPEN62541)/lib +OPEN62541_SHRLIB_DIR = $(OPEN62541_LIB_DIR) +# How the Open62541 libraries were built +OPEN62541_USE_CRYPTO = YES +OPEN62541_USE_XMLPARSER = YES +EOF + +# Build the OPCUA Device Support +make -j4 -C opcua-0.10.0 + +### Done ### + +# Now, e.g., run the example IOC +( cd opcua-0.10.0/exampleTop/iocBoot/iocS7-1500/; ../../bin/linux-x86_64/opcuaIoc st.cmd ) \ No newline at end of file diff --git a/documentation/Doxyfile b/documentation/Doxyfile new file mode 100644 index 00000000..8f633344 --- /dev/null +++ b/documentation/Doxyfile @@ -0,0 +1,9 @@ +PROJECT_NAME = "EPICS OPC UA Device Support" +OUTPUT_DIRECTORY = documentation/build +INPUT = devOpcuaSup +RECURSIVE = YES +GENERATE_XML = YES +XML_OUTPUT = xml +GENERATE_HTML = NO +GENERATE_LATEX = NO +QUIET = YES diff --git a/documentation/EPICS Support for OPC UA - Cheat Sheet.pdf b/documentation/EPICS Support for OPC UA - Cheat Sheet.pdf deleted file mode 100644 index a91a7f9d..00000000 Binary files a/documentation/EPICS Support for OPC UA - Cheat Sheet.pdf and /dev/null differ diff --git a/documentation/_static/EPICS Support for OPC UA - Cheat Sheet.pdf b/documentation/_static/EPICS Support for OPC UA - Cheat Sheet.pdf new file mode 100644 index 00000000..e199473b Binary files /dev/null and b/documentation/_static/EPICS Support for OPC UA - Cheat Sheet.pdf differ diff --git a/documentation/EPICS Support for OPC UA - SRS.pdf b/documentation/_static/EPICS Support for OPC UA - SRS.pdf similarity index 100% rename from documentation/EPICS Support for OPC UA - SRS.pdf rename to documentation/_static/EPICS Support for OPC UA - SRS.pdf diff --git a/documentation/conf.py b/documentation/conf.py new file mode 100644 index 00000000..7f834d79 --- /dev/null +++ b/documentation/conf.py @@ -0,0 +1,35 @@ +# Configuration file for the Sphinx documentation builder. + +project = 'EPICS OPC UA Device Support' +project_copyright = '2025, Ralph Lange' +author = 'Ralph Lange' + +# -- General configuration --------------------------------------------------- + +extensions = [ + 'myst_parser', + 'sphinx.ext.napoleon', + 'sphinx_rtd_theme', + 'breathe', +] + +breathe_projects = { + "opcua": "build/xml" +} +breathe_default_project = "opcua" + +templates_path = ['_templates'] +exclude_patterns = ['build'] + +# -- Options for HTML output ------------------------------------------------- + +html_theme = 'sphinx_rtd_theme' +html_static_path = ['_static'] + +# -- MyST Parser configuration ----------------------------------------------- + +myst_enable_extensions = [ + "colon_fence", + "deflist", + "substitution", +] diff --git a/documentation/credits.md b/documentation/credits.md new file mode 100644 index 00000000..fb06512e --- /dev/null +++ b/documentation/credits.md @@ -0,0 +1,30 @@ +# Credits + +## Authors + +__Bernhard Kuner__ +evaluated the available OPC UA client libraries +and implemented a Device Support prototype +based on the Unified Automation client library example code. + +__Ralph Lange__ +added support for the Unified Automation SDK +and is a main author of the OPC UA Device Support implementation. + +__Dirk Zimoch__ +added support for the open62541 SDK client library +and is a main author of the OPC UA Device Support implementation. + +__Carsten Winkler__ +contributed to the open62541 support and helped with the Windows builds. + +__Karl Vestin__ +and __Ross Elliot__ (ESS) +developed the end-to-end test suite. + +## Documentation + +__Mohor Gartner__ +and __Mikel Rojo__ +wrote the OPC UA User Manual for ITER's CODAC Core System, +which was used as a base for parts of this documentation. diff --git a/documentation/explanation/opcua_concepts.md b/documentation/explanation/opcua_concepts.md new file mode 100644 index 00000000..4e2df8da --- /dev/null +++ b/documentation/explanation/opcua_concepts.md @@ -0,0 +1,191 @@ +# Basics, Terms and Concepts + +## Basics + +### What is EPICS? + +[https://docs.epics-controls.org/en/latest/guides/EPICS_Intro.html][epics-intro] + +[https://epics-controls.org/][epics-web] + +### What is OPC UA? + +OPC Unified Architecture (OPC UA) is a cross-platform, +open-source, +IEC62541 standard for data exchange from sensors to cloud applications +developed by the [OPC Foundation][opc-foundation]. + +The OPC UA protocol has become increasingly popular +with vendors of controllers (like PLCs) and sensors +as a very powerful integration tool. +It allows connecting almost any device to almost any SCADA client, +across vendors, with a certification mechanism that ensures compatibility. + +OPC UA integrates all functionality of the individual specifications +of the former OPC Classic protocol into one extensible framework, +providing: + +* _Functional equivalence_: + all COM OPC Classic specifications are mapped to UA + +* _Platform independence_: + from embedded to cloud-based infrastructure + +* _Security_: + encryption, authentication, and auditing + +* _Extensibility_: + ability to add new features without affecting existing applications + +* _Information modelling_: + for defining complex information + +OPC UA is designed for exchanging information in an object-oriented manner, +rather than as isolated data points. +This increases the accessibility of plant floor data +by allowing to re-use information stored in a common object. + +The [Wikipedia page on OPC UA][opcua-wiki] has a good overview +of its characteristics, history and specification. + +The complete [OPC UA specification][opcua-spec] is freely available +on the OPC Foundation web site. + +## OPC UA Terms and Concepts + +### Node + +A _Node_ is a uniquely identified piece of information on the server. +The OPC information model is a mesh network based on nodes. +These nodes are objects holding process data or any kind of meta information, +such as attributes, references, methods, types, ... + +### NodeID + +A _NodeID_ is an identifier for a node, e.g., for a particular OPC UA item. +A Node ID consists of: + +* a namespace index number +* an identifier type +* an identifier. + +The identifier can be either a string identifier, consisting of a name string, +or a numerical identifier, consisting of an integer. +Whenever a client requests to either read or write data to an OPC UA item, +it identifies the node by means of its NodeID. + +### Item + +Within the Data Access part of OPC UA, +an _Item_ is any piece of accessible process data on a server. +It can represent a single data point or a block of structured data. +Elements of structured data can be simple data points +or other structured items. + +### Data Element + +In the context of this Device Support only +(i.e., not part of the OPC UA specification), +a _Data Element_ is a unique data point belonging to a server-side item. +If that item is of basic or array type, the data element corresponds +to the value of the item. +If the item is of a structured type, the data element corresponds +to one element of the item's structure. + +### Namespace + +A _Namespace_ is one of multiple server-side scopes for node identifiers, +which the server uses to avoid name clashes. + +A namespace is uniquely defined by an URI. +To improve performance, the server keeps a table of the URIs +and the index in that table is used between client and server. +Some namespaces (and their index numbers) +are reserved by the OPC UA specification for the protocol and the server. + +### Channel + +In OPC UA, a _Channel_ is the low-level transport connection +between a client and a server. +(The OPC UA Device Support module only supports the TCP transport.) +A channel can be secured by running it through SSL to be encrypted, +and/or by signing all messages that are sent through it. + +### Session + +A _Session_ is a named connection +between an OPC UA client and an OPC UA server, +using the transport provided by the channel. +Typically, there is one session between each client and each server. +While it is possible to have multiple sessions +between a single client/server pair, +this increases resource usage on both ends and is generally not recommended +unless there is a particular justifying reason +(such as making use of parallel processing). + +### Subscription + +A _Subscription_ is a mechanism within a session +that the client uses to retrieve value updates from a server +without the need to poll regularly. +A client establishes a subscription within a session and adds items to it, +requesting that the server notify the client +whenever data of the _Monitored Items_ within that subscription changes. + +When defining a subscription, +the client can specify a _publishing interval_ for the server, +telling it how often it would like to receive updated data. +For each monitored item that is part of a subscription, +the client can also specify its _sampling interval_, +which defines the rate at which the server checks the data source for changes. +If the OPC UA server supports server-side queueing of data samples, +the client can also specify for each monitored item the size of that queue. + +## The OPC UA Device Support + +EPICS applications run processes called IOCs, +which interface device data to the EPICS Process Database, +which provides it to the control system through the EPICS network protocols, +Channel Access and pvAccess. + +The Device Support layer enables records in the EPICS Process Database +to communicate with different device types. +The OPC UA Device Support is a client of the OPC UA protocol, +which can read, write and monitor variables residing on an OPC UA server. +(The relevant part of the OPC UA specification is called "Data Access".) + +If your IOC is built correctly (see [Building an IOC](building-an-ioc)), +the OPC UA Device Support is ready to be used. +You can set up OPC UA sessions and subscriptions from the IOC startup script +through commands in the IOCshell +and link EPICS database records to remote OPC UA variables +through the configuration of your EPICS records. + +To support commissioning and troubleshooting, additional IOCshell commands +allow introspection of the Device Support's internal data structures. +The regular EPICS trace and debug modes can be used to investigate +the OPC UA operations at run time. + +### Addressing Basic/Array and Structured Types + +If an OPC UA item is of basic or array type, +a single EPICS data record connects to it. +That record is configured with the session or subscription to be used, +the nodeID of the item and options that apply to the item level. + +If an OPC UA item is of a structured type, a single instance +of the special opcuaItemRecord type represents the complete structure. +Many EPICS data records each connect to a single structure element. +The opcuaItemRecord is configured with all item level configuration. +The single element data records refer to the opcuaItemRecord +and specify the address of the strcuture element they connect to +as well as data element level options. + +% external links + +[epics-intro]: https://docs.epics-controls.org/en/latest/guides/EPICS_Intro.html +[epics-web]: https://epics-controls.org/ + +[opc-foundation]: https://opcfoundation.org/ +[opcua-wiki]: https://en.wikipedia.org/wiki/OPC_Unified_Architecture +[opcua-spec]: https://opcfoundation.org/developer-tools/documents/?type=Specification diff --git a/documentation/explanation/security_concepts.md b/documentation/explanation/security_concepts.md new file mode 100644 index 00000000..a36aad85 --- /dev/null +++ b/documentation/explanation/security_concepts.md @@ -0,0 +1,112 @@ +# OPC UA Security Concepts + +The security features of OPC UA +are based on the widely used X.509 certificates +and the concepts of *Public Key Infrastructure* (PKI). + +Certificates are filed in a *Certificate Store*, +containing trusted and own certificates +as well as certificates from certificate authorities (CAs). +On Linux, a file based store is used, +defined by four specific locations for certificates and revocation lists. + +In the basic, explicit form, +trusting a certificate usually means +that the certificate (file) is moved into a specific folder +in the certificate store. + +When using certificate authorities (CAs), +a certificate is trusted if it is signed by a trusted CA. +This approach is preferable for a larger installation, +as it does not require updating all servers and clients +when new peers with new certificates are added. + +A good analogy to certificates are passports: +In a world with only a handful people, +fabricating your passport yourself is reasonable. +In reality, with millions of passport holders, +it helps a lot that passports are fabricated by government agencies +and this fact can be verified by checking holograms, watermarks etc. + +## Self-signed Certificates + +Servers often come with preloaded self-signed server certificates +and a setting that will generously trust +any valid self-signed client certificate. +Generic clients typically offer an easy way +to create a self-signed client certificate. + +These settings are intended to get users started with security easily +and create minimal frustration during development. +Such "simple" self-signed certificates +are good enough for securing the communication, +but they are not very practical for authentication: +every self-signed certificate must be trusted explicitly - +or be accepted by default. + +## Communication Security + +Each OPC UA application (server and client) needs an *Application Instance Certificate* and a related public/private key pair to identify itself to its communication peers. The public key is contained in the certificate; the private key is secret and used for signing and encryption of messages. + +For a secure communication to be established, both peers must trust the certificate of the other. + +OPC UA defines three *Message Security Modes*: + +| Message Security Mode | | +| --------------------- | ------------------------------------------- | +| None | No security is applied. | +| Sign | All messages are signed, but not encrypted. | +| SignAndEncrypt | All messages are signed and encrypted. | + +As security algorithms are getting obsolete, +an increasing number of *Security Policies* +(sets of security algorithms and key lengths) are defined, +identified their unique URIs: + +:::{list-table} +:header-rows: 1 + +* - Security Policy + - URI +* - None + - `http://opcfoundation.org/UA/SecurityPolicy#None` +* - Basic128Rsa15 *(obsolete)* + - `http://opcfoundation.org/UA/SecurityPolicy#Basic128Rsa15` +* - Basic256 + - `http://opcfoundation.org/UA/SecurityPolicy#Basic256` +* - Basic256Sha256 + - `http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256` +* - Aes128_Sha256_RsaOaep + - `http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep` +* - Aes256_Sha256_RsaPss + - `http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss` +::: + +## Client Authentication (Identity) + +For client authentication +related to managing the authorization of access to a server, +OPC UA defines four *User Token Types* (methods of user authentication): + +| User Token Type | | +| ------------------------ | ------------------------------------------ | +| Anonymous Identity Token | No user information available. | +| Username Identity Token | User identified by name and password. | +| X.509 Identity Token | User identified by an X.509v3 certificate. | +| Issued Identity Token | User identified by a WS-SecurityToken. | + +## Endpoint Discovery + +In order to establish a secure connection, +the client has to find out which security features the server provides. +This process is called *Discovery*. + +In its answer to a discovery request, +a server provides a list of *Endpoints* for the client to connect to, +with different sets of security features. + +As a debugging tool, +the iocShell command `opcuaShowSecurity` runs the discovery service +and displays the available endpoints +with their features and their *security level* +(used by the IOC to find the endpoint providing the best security). diff --git a/documentation/how-to/debugging.md b/documentation/how-to/debugging.md new file mode 100644 index 00000000..b0d3c39f --- /dev/null +++ b/documentation/how-to/debugging.md @@ -0,0 +1,77 @@ +# Debugging and Tracing + +The OPC UA Device Support module provides +several tools for debugging and tracing. + +## IOC Shell Commands + +Several commands are available for inspecting the state of the module: + +* `opcuaShow`: + Prints information about + sessions, subscriptions, items, and data elements. + Supports glob patterns. +* `opcuaShowSecurity`: + Prints information about the IOCś security setup + and discovered endpoints on the remote server. +* `opcuaConnect` / `opcuaDisconnect`: + Manually manage session connections. + +Example: + +``` +opcuaShow OPC* 1 +``` + +## Session/Subscription Debugging + +Sessions and Subscriptions have a `debug` option. +Setting it to `>= 1` (verbosity level) +will enable printing debug information +when the client handles things related to it. + +Example: + +``` +opcuaOption OPC1 debug=5 +``` + +This option can be changed at runtime. +To see debug messages from initial connection handling, +enable it before `iocInit`. + +## EPICS Record Debugging + +You can enable tracing and debugging for specific EPICS records +by setting the `TPRO` field to a value `>= 1`. + +* In `.db` file: + `field(TPRO, "1")` +* On the IOC: + `dbpf .TPRO 1` +* In a shell: + `caput .TPRO 1` + +## Adaptive Concurrency Monitoring + +:::{versionadded} 0.12 +Adaptive concurrency automatically throttles requests +based on server response times. +This allows the IOC to gracefully handle slow and fast servers, +adapting automatically to the available bandwidth. + +If adaptive concurrency is enabled, +you can monitor its state using `opcuaShow`. +It will display current limits and performance metrics. +::: + +## Using UaExpert (External Tool) + +UaExpert is a powerful free graphical OPC UA client tool +from Unified Automation (it uses the same UA SDK) +that can be used to: + +1. Test connection to the OPC UA server. +2. Browse the address space. +3. Identify `NamespaceIndex` and `Identifier` (NodeID) + for EPICS record configuration. diff --git a/documentation/how-to/plc_configuration.md b/documentation/how-to/plc_configuration.md new file mode 100644 index 00000000..beea0699 --- /dev/null +++ b/documentation/how-to/plc_configuration.md @@ -0,0 +1,127 @@ +# Configuring the OPC UA Server on a Siemens S7-1500 Series PLC + +This guide details the steps to configure a Siemens S7-1500 PLC +as an OPC UA server using TIA Portal. + +## Activate OPC UA Server + +1. Open your project in TIA Portal. +2. Navigate to your PLC's properties (e.g., `PLC_1 [CPU 1518-4 PN/DP]`). +3. Go to `General` > `OPC UA` > `Server` > `General`. +4. Check the `Activate OPC UA server` box. +5. Ensure that one of the `Server addresses` (`opc.tcp://...`) + matches the IP address of your PLC on the network. + The default OPC UA port is 4840. + +### Using OPC UA with a CP Module + +OPC UA works using a CP module +when respecting the following minimal requirements: + +| Feature | Minimum Requirement | +| ---------------------- | ------------------- | +| Virtual interface (W1) | CPU FW **V2.8** | +| Configuration in TIA | TIA Portal **V16** | +| OPC UA via CP | Supported via W1 | +| Compatible CP | CP 1543‑1 FW ≥ **V2.2** (recommended ≥ V3.0) | + +## Configure OPC UA Server Options + +1. In the PLC properties, + navigate to `General` > `OPC UA` > `Server` > `Options`. +2. **Port:** + Adjust the `Port` number + if a different port than the default 4840 is required. +3. **Minimum sampling interval:** + Defines the minimum rate (in ms) + at which the server can check data sources for changes. +4. **Minimum publishing interval:** + Defines the minimum rate (in ms) + at which the server can send updated data to clients. + + :::{hint} + For S7-1500 PLCs, + especially small or mid-size ones (e.g., S7-1516), + starting with 250ms intervals + and grouping variables into structures + is a good practice for improving performance. + ::: + +## Configure OPC UA Server Security + +1. In the PLC properties, + navigate to `General` > `OPC UA` > `Server` > `Security`. +2. **Security Policies:** + Select the desired security policy. + For basic testing and development, + you might select `No security`. + For production environments, + stronger options like `Basic128Rsa15-Sign & Encrypt` are recommended. +3. **Guest Authentication:** + If no specific user authentication is required, + check `Enable guest authentication`. + + :::{attention} + On the IOC, OPC UA Security is enabled by default. + To connect to any OPC UA server *without* security, + you must set the option `sec-mode=None` + for the concerned session(s) in your EPICS IOC configuration. + ::: + +## Configure OPC UA Runtime License + +1. In the PLC properties, + navigate to `General` > `Runtime licenses` > `OPC UA`. +2. Ensure the `Type of purchased license` + is at least the same level as the `Type of required license` + (e.g., `SIMATIC OPC UA S7-1500 large`). + +## Define Variables in PLC Data Structures + +Organize your PLC variables into data blocks +that will be exposed as OPC UA nodes. + +1. **Create Data Blocks:** + In TIA Portal, + create new data blocks + (e.g., `DB1`, `DB2`, `DB3`, `DB4`). +2. **Define Variables:** + Inside each data block, + define the variables you want to expose. + You can use structured user-defined data types, + which will appear as structured data on OPC UA. + Structured data is very efficient when using subscriptions. + For structures, the server on the S7 always creates + a node for the structure + and separate nodes for each of its elements. + +### Example PLC Data Types and EPICS Record Type Compatibility + +:::{list-table} +:header-rows: 1 + +* - PLC data type + - EPICS Record type +* - Real, LReal + - ai/ao +* - Bool + - bi/bo +* - Byte/Word/DWord, most *Int + - longin/longout, mbbi/mbbo, mbbiDirect/mbboDirect, bi/bo, ai/ao (RVAL) +* - LInt/ULInt + - int64in/int64out +* - String + - stringin/stringout, lsi/lso +* - Array[m..n] of * + - aai/aao/waveform (with compatible FTVL) +* - Struct (UDT) + - opcuaItem +::: + +Arrays that start at a non-zero index on the PLC +are shifted to start from zero over OPC UA. + +:::{note} +Structured items can *only* be handled +using the `opcuaItem` record type. +::: diff --git a/documentation/how-to/record_configuration_scalar.md b/documentation/how-to/record_configuration_scalar.md new file mode 100644 index 00000000..77d69aeb --- /dev/null +++ b/documentation/how-to/record_configuration_scalar.md @@ -0,0 +1,210 @@ +# EPICS Records for Single Scalar Variables + +This guide shows how to configure EPICS records +for accessing individual single scalar variables on the OPC UA server. + +## General INP/OUT Link Field Format + +`@ ns=;= +[