diff --git a/build.gradle.kts b/build.gradle.kts index 70658a4302..f06b7cc096 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -77,6 +77,7 @@ val moduleReleaseBinaries: Configuration by configurations.creating { dependencies { hivemq("com.hivemq:hivemq-edge") // ** module-deps ** // + edgeModule("com.hivemq:hivemq-edge-module-etherip") edgeModule("com.hivemq:hivemq-edge-module-file") edgeModule("com.hivemq:hivemq-edge-module-http") edgeModule("com.hivemq:hivemq-edge-module-plc4x") @@ -102,6 +103,7 @@ val hivemqEdgeZip by tasks.registering(Zip::class) { val edgeProjectsToUpdate = setOf( "hivemq-edge", + "hivemq-edge-module-etherip", "hivemq-edge-module-file", "hivemq-edge-module-http", "hivemq-edge-module-modbus", diff --git a/edge-plugins/build.gradle.kts b/edge-plugins/build.gradle.kts index 58f94b08f4..03dfc00ea6 100644 --- a/edge-plugins/build.gradle.kts +++ b/edge-plugins/build.gradle.kts @@ -6,7 +6,7 @@ group = "com.hivemq" java { toolchain { - languageVersion.set(JavaLanguageVersion.of(8)) + languageVersion.set(JavaLanguageVersion.of(11)) } } diff --git a/modules/hivemq-edge-module-etherip/HEADER b/modules/hivemq-edge-module-etherip/HEADER new file mode 100644 index 0000000000..068849ca5a --- /dev/null +++ b/modules/hivemq-edge-module-etherip/HEADER @@ -0,0 +1,13 @@ +Copyright 2023-present HiveMQ GmbH + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/modules/hivemq-edge-module-etherip/README.md b/modules/hivemq-edge-module-etherip/README.md new file mode 100644 index 0000000000..c43b7c345a --- /dev/null +++ b/modules/hivemq-edge-module-etherip/README.md @@ -0,0 +1,10 @@ +# EtherIP Dependency + +For interacting with Ethernet IP/CIP devices we rely on an open source library which isn't published on Maven-Central: + +[EtherIP](https://github.com/ornl-epics/etherip/) + +We build the library internall and provide the binary in **libs/etherip-1.0.0.jar**. + +In case you want to build the libary yourself simply check out the original project, build the libary and put it into +the libs folder. diff --git a/modules/hivemq-edge-module-etherip/build.gradle.kts b/modules/hivemq-edge-module-etherip/build.gradle.kts new file mode 100644 index 0000000000..40b885a1e1 --- /dev/null +++ b/modules/hivemq-edge-module-etherip/build.gradle.kts @@ -0,0 +1,217 @@ +import nl.javadude.gradle.plugins.license.DownloadLicensesExtension.license +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent.* + + +plugins { + java + alias(libs.plugins.defaults) + alias(libs.plugins.shadow) + alias(libs.plugins.license) + id("com.hivemq.edge-version-updater") +} + +group = "com.hivemq" + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(11)) + } +} + +repositories { + mavenCentral() + exclusiveContent { + forRepository { + maven { + url = uri("https://jitpack.io") + } + } + filter { + includeGroup("com.github.simon622.mqtt-sn") + includeGroup("com.github.simon622") + } + } +} + +dependencies { + compileOnly(libs.hivemq.edge.adapterSdk) + compileOnly(libs.apache.commonsIO) + compileOnly(libs.apache.commonsLang) + implementation(files("libs/etherip-1.0.0.jar")) + implementation(libs.jackson.databind) + implementation(libs.slf4j.api) +} + +dependencies { + testImplementation("com.hivemq:hivemq-edge") + testImplementation(libs.hivemq.edge.adapterSdk) + testImplementation(libs.apache.commonsIO) + testImplementation(libs.mockito.junitJupiter) + testImplementation(libs.junit.jupiter) + testImplementation(libs.assertj) +} + +tasks.test { + useJUnitPlatform{ + excludeTags("requiresVpn") + } + testLogging { + events = setOf(STARTED, PASSED, FAILED, SKIPPED, STANDARD_ERROR) + exceptionFormat = TestExceptionFormat.FULL + } +} + +tasks.register("vpnTests") { + useJUnitPlatform { + includeTags("requiresVpn") + } + shouldRunAfter("test") +} + + +tasks.register("copyAllDependencies") { + shouldRunAfter("assemble") + from(configurations.runtimeClasspath) + into("${buildDir}/deps/libs") +} + +tasks.named("assemble") { finalizedBy("copyAllDependencies") } + +/* ******************** artifacts ******************** */ + +val releaseBinary: Configuration by configurations.creating { + isCanBeConsumed = true + isCanBeResolved = false + attributes { + attribute(Category.CATEGORY_ATTRIBUTE, objects.named("binary")) + attribute(Usage.USAGE_ATTRIBUTE, objects.named("release")) + } +} + +artifacts { + add(releaseBinary.name, tasks.shadowJar) +} + +/* ******************** compliance ******************** */ + +license { + header = file("HEADER") + mapping("java", "SLASHSTAR_STYLE") +} + +downloadLicenses { + aliases = mapOf( + license("Apache License, Version 2.0", "https://opensource.org/licenses/Apache-2.0") to listOf( + "Apache 2", + "Apache 2.0", + "Apache-2.0", + "Apache License 2.0", + "Apache License, 2.0", + "Apache License v2.0", + "Apache License, Version 2", + "Apache License Version 2.0", + "Apache License, Version 2.0", + "Apache License, version 2.0", + "The Apache License, Version 2.0", + "Apache Software License - Version 2.0", + "Apache Software License, version 2.0", + "The Apache Software License, Version 2.0" + ), + license("MIT License", "https://opensource.org/licenses/MIT") to listOf( + "MIT License", + "MIT license", + "The MIT License", + "The MIT License (MIT)" + ), + license("CDDL, Version 1.0", "https://opensource.org/licenses/CDDL-1.0") to listOf( + "CDDL, Version 1.0", + "Common Development and Distribution License 1.0", + "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0", + license("CDDL", "https://glassfish.dev.java.net/public/CDDLv1.0.html") + ), + license("CDDL, Version 1.1", "https://oss.oracle.com/licenses/CDDL+GPL-1.1") to listOf( + "CDDL 1.1", + "CDDL, Version 1.1", + "Common Development And Distribution License 1.1", + "CDDL+GPL License", + "CDDL + GPLv2 with classpath exception", + "Dual license consisting of the CDDL v1.1 and GPL v2", + "CDDL or GPLv2 with exceptions", + "CDDL/GPLv2+CE" + ), + license("LGPL, Version 2.0", "https://opensource.org/licenses/LGPL-2.0") to listOf( + "LGPL, Version 2.0", + "GNU General Public License, version 2" + ), + license("LGPL, Version 2.1", "https://opensource.org/licenses/LGPL-2.1") to listOf( + "LGPL, Version 2.1", + "LGPL, version 2.1", + "GNU Lesser General Public License version 2.1 (LGPLv2.1)", + license("GNU Lesser General Public License", "http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html") + ), + license("LGPL, Version 3.0", "https://opensource.org/licenses/LGPL-3.0") to listOf( + "LGPL, Version 3.0", + "Lesser General Public License, version 3 or greater" + ), + license("EPL, Version 1.0", "https://opensource.org/licenses/EPL-1.0") to listOf( + "EPL, Version 1.0", + "Eclipse Public License - v 1.0", + "Eclipse Public License - Version 1.0", + license("Eclipse Public License", "http://www.eclipse.org/legal/epl-v10.html") + ), + license("EPL, Version 2.0", "https://opensource.org/licenses/EPL-2.0") to listOf( + "EPL 2.0", + "EPL, Version 2.0" + ), + license("EDL, Version 1.0", "https://www.eclipse.org/org/documents/edl-v10.php") to listOf( + "EDL 1.0", + "EDL, Version 1.0", + "Eclipse Distribution License - v 1.0" + ), + license("BSD 3-Clause License", "https://opensource.org/licenses/BSD-3-Clause") to listOf( + "BSD 3-clause", + "BSD-3-Clause", + "BSD 3-Clause License", + "3-Clause BSD License", + "New BSD License", + license("BSD", "http://asm.ow2.org/license.html"), + license("BSD", "http://asm.objectweb.org/license.html"), + license("BSD", "LICENSE.txt") + ), + license("Bouncy Castle License", "https://www.bouncycastle.org/licence.html") to listOf( + "Bouncy Castle Licence" + ), + license("W3C License", "https://opensource.org/licenses/W3C") to listOf( + "W3C License", + "W3C Software Copyright Notice and License", + "The W3C Software License" + ), + license("CC0", "https://creativecommons.org/publicdomain/zero/1.0/") to listOf( + "CC0", + "Public Domain" + ) + ) + + dependencyConfiguration = "runtimeClasspath" +} + +val updateThirdPartyLicenses by tasks.registering { + group = "license" + dependsOn(tasks.downloadLicenses) + doLast { + javaexec { + classpath("gradle/tools/license-third-party-tool-2.0.jar") + args( + "$buildDir/reports/license/dependency-license.xml", + "$projectDir/src/distribution/third-party-licenses/licenses", + "$projectDir/src/distribution/third-party-licenses/licenses.html" + ) + } + } +} + +val javaComponent = components["java"] as AdhocComponentWithVariants +javaComponent.withVariantsFromConfiguration(configurations.shadowRuntimeElements.get()) { + skip() +} diff --git a/modules/hivemq-edge-module-etherip/gradle.properties b/modules/hivemq-edge-module-etherip/gradle.properties new file mode 100644 index 0000000000..c16381e63e --- /dev/null +++ b/modules/hivemq-edge-module-etherip/gradle.properties @@ -0,0 +1 @@ +version=2024.6 diff --git a/modules/hivemq-edge-module-etherip/gradle/dependency-check/suppress.xml b/modules/hivemq-edge-module-etherip/gradle/dependency-check/suppress.xml new file mode 100644 index 0000000000..85bf31aae8 --- /dev/null +++ b/modules/hivemq-edge-module-etherip/gradle/dependency-check/suppress.xml @@ -0,0 +1,18 @@ + + + + + ^org\.jsoup:jsoup:.*$ + CVE-2015-6748 + + + + ^javax\.ws\.rs:javax\.ws\.rs-api:.*$ + CVE-2015-4345 + + \ No newline at end of file diff --git a/modules/hivemq-edge-module-etherip/gradle/libs.versions.toml b/modules/hivemq-edge-module-etherip/gradle/libs.versions.toml new file mode 100644 index 0000000000..410ca9576e --- /dev/null +++ b/modules/hivemq-edge-module-etherip/gradle/libs.versions.toml @@ -0,0 +1,24 @@ +[versions] +apache-commonsIO = "2.16.1" +apache-commonsLang = "3.14.0" +assertj = "3.25.3" +hivemq-edge-adapterSdk = "2024.5" +jackson = "2.17.1" +junit-jupiter = "5.10.3" +mockito = "5.12.0" +slf4j = "2.0.13" + +[libraries] +apache-commonsIO = { module = "commons-io:commons-io", version.ref = "apache-commonsIO" } +apache-commonsLang = { module = "org.apache.commons:commons-lang3", version.ref = "apache-commonsLang" } +assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" } +hivemq-edge-adapterSdk = { module = "com.hivemq:hivemq-edge-adapter-sdk", version.ref = "hivemq-edge-adapterSdk" } +jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" } +mockito-junitJupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" } +slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } + +[plugins] +license = { id = "com.github.hierynomus.license", version = "0.16.1" } +shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1" } +defaults = { id = "io.github.sgtsilvio.gradle.defaults", version = "0.2.0" } diff --git a/modules/hivemq-edge-module-etherip/gradle/tools/javadoc-cleaner-1.0.jar b/modules/hivemq-edge-module-etherip/gradle/tools/javadoc-cleaner-1.0.jar new file mode 100644 index 0000000000..66659877e4 Binary files /dev/null and b/modules/hivemq-edge-module-etherip/gradle/tools/javadoc-cleaner-1.0.jar differ diff --git a/modules/hivemq-edge-module-etherip/gradle/tools/license-third-party-tool-2.0.jar b/modules/hivemq-edge-module-etherip/gradle/tools/license-third-party-tool-2.0.jar new file mode 100644 index 0000000000..0135ec5b40 Binary files /dev/null and b/modules/hivemq-edge-module-etherip/gradle/tools/license-third-party-tool-2.0.jar differ diff --git a/modules/hivemq-edge-module-etherip/gradle/wrapper/gradle-wrapper.jar b/modules/hivemq-edge-module-etherip/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..e0cd9790ae Binary files /dev/null and b/modules/hivemq-edge-module-etherip/gradle/wrapper/gradle-wrapper.jar differ diff --git a/modules/hivemq-edge-module-etherip/gradle/wrapper/gradle-wrapper.properties b/modules/hivemq-edge-module-etherip/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..41dfb87909 --- /dev/null +++ b/modules/hivemq-edge-module-etherip/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/modules/hivemq-edge-module-etherip/gradlew b/modules/hivemq-edge-module-etherip/gradlew new file mode 100755 index 0000000000..a69d9cb6c2 --- /dev/null +++ b/modules/hivemq-edge-module-etherip/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/modules/hivemq-edge-module-etherip/gradlew.bat b/modules/hivemq-edge-module-etherip/gradlew.bat new file mode 100644 index 0000000000..f127cfd49d --- /dev/null +++ b/modules/hivemq-edge-module-etherip/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/modules/hivemq-edge-module-etherip/libs/etherip-1.0.0.jar b/modules/hivemq-edge-module-etherip/libs/etherip-1.0.0.jar new file mode 100644 index 0000000000..f9aae23c8a Binary files /dev/null and b/modules/hivemq-edge-module-etherip/libs/etherip-1.0.0.jar differ diff --git a/modules/hivemq-edge-module-etherip/settings.gradle.kts b/modules/hivemq-edge-module-etherip/settings.gradle.kts new file mode 100644 index 0000000000..aef8966223 --- /dev/null +++ b/modules/hivemq-edge-module-etherip/settings.gradle.kts @@ -0,0 +1,5 @@ +rootProject.name = "hivemq-edge-module-etherip" + +pluginManagement { + includeBuild("../../edge-plugins") +} diff --git a/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/EtherIpPollingProtocolAdapter.java b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/EtherIpPollingProtocolAdapter.java new file mode 100644 index 0000000000..f955e2c895 --- /dev/null +++ b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/EtherIpPollingProtocolAdapter.java @@ -0,0 +1,194 @@ +/* + * Copyright 2023-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hivemq.edge.adapters.etherip; + +import com.hivemq.adapter.sdk.api.ProtocolAdapterInformation; +import com.hivemq.adapter.sdk.api.factories.AdapterFactories; +import com.hivemq.adapter.sdk.api.model.ProtocolAdapterInput; +import com.hivemq.adapter.sdk.api.model.ProtocolAdapterStartInput; +import com.hivemq.adapter.sdk.api.model.ProtocolAdapterStartOutput; +import com.hivemq.adapter.sdk.api.model.ProtocolAdapterStopInput; +import com.hivemq.adapter.sdk.api.model.ProtocolAdapterStopOutput; +import com.hivemq.adapter.sdk.api.polling.PollingInput; +import com.hivemq.adapter.sdk.api.polling.PollingOutput; +import com.hivemq.adapter.sdk.api.polling.PollingProtocolAdapter; +import com.hivemq.adapter.sdk.api.state.ProtocolAdapterState; +import com.hivemq.edge.adapters.etherip.model.EtherIpAdapterConfig; +import com.hivemq.edge.adapters.etherip.model.EtherIpValue; +import com.hivemq.edge.adapters.etherip.model.EtherIpValueFactory; +import etherip.EtherNetIP; +import etherip.data.CipException; +import etherip.types.CIPData; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + + +public class EtherIpPollingProtocolAdapter implements PollingProtocolAdapter { + + private static final @NotNull org.slf4j.Logger LOG = LoggerFactory.getLogger(EtherIpPollingProtocolAdapter.class); + + protected static final String TAG_ADDRESS_TYPE_SEP = ":"; + + private final @NotNull EtherIpAdapterConfig adapterConfig; + private final @NotNull ProtocolAdapterInformation adapterInformation; + private final @NotNull ProtocolAdapterState protocolAdapterState; + protected final @NotNull AdapterFactories adapterFactories; + private volatile @Nullable EtherNetIP etherNetIP; + + private final Map lastSeenValues; + + public EtherIpPollingProtocolAdapter( + final @NotNull ProtocolAdapterInformation adapterInformation, + final @NotNull ProtocolAdapterInput input) { + this.adapterInformation = adapterInformation; + this.adapterConfig = input.getConfig(); + this.protocolAdapterState = input.getProtocolAdapterState(); + this.adapterFactories = input.adapterFactories(); + this.lastSeenValues = new ConcurrentHashMap<>(); + } + + @Override + public @NotNull String getId() { + return adapterConfig.getId(); + } + + @Override + public void start( + final @NotNull ProtocolAdapterStartInput input, final @NotNull ProtocolAdapterStartOutput output) { + // any setup which should be done before the adapter starts polling comes here. + try { + final EtherNetIP etherNetIP = new EtherNetIP(adapterConfig.getHost(), adapterConfig.getSlot()); + etherNetIP.connectTcp(); + this.etherNetIP = etherNetIP; + output.startedSuccessfully(); + protocolAdapterState.setConnectionStatus(ProtocolAdapterState.ConnectionStatus.CONNECTED); + protocolAdapterState.setRuntimeStatus(ProtocolAdapterState.RuntimeStatus.STARTED); + } catch (final Exception e) { + output.failStart(e, null); + } + } + + @Override + public void stop( + final @NotNull ProtocolAdapterStopInput protocolAdapterStopInput, + final @NotNull ProtocolAdapterStopOutput protocolAdapterStopOutput) { + try { + if (etherNetIP != null) { + etherNetIP.close(); + etherNetIP = null; + protocolAdapterStopOutput.stoppedSuccessfully(); + + protocolAdapterState.setRuntimeStatus(ProtocolAdapterState.RuntimeStatus.STOPPED); + LOG.info("Stopped"); + } else { + protocolAdapterStopOutput.stoppedSuccessfully(); + LOG.info("Stopped without an open connection"); + } + } catch (Exception e) { + protocolAdapterStopOutput.failStop(e, "Unable to stop Ethernet IP connection"); + LOG.error("Unable to stop", e); + } + } + + + @Override + public @NotNull ProtocolAdapterInformation getProtocolAdapterInformation() { + return adapterInformation; + } + + @Override + public void poll( + final @NotNull PollingInput pollingInput, + final @NotNull PollingOutput pollingOutput) { + + if (etherNetIP == null) { + return; + } + + final String tagAddress = createTagAddressForSubscription(pollingInput.getPollingContext()); + try { + final CIPData evt = etherNetIP.readTag(pollingInput.getPollingContext().getTagAddress()); + + if(adapterConfig.getPublishChangedDataOnly()) { + handleResult(evt, tagAddress) + .forEach(it -> { + if(!lastSeenValues.containsKey(tagAddress) || !lastSeenValues.get(tagAddress).equals(it)) { + pollingOutput.addDataPoint(tagAddress, it.getValue()); + lastSeenValues.put(tagAddress, it); + } + }); + } else { + handleResult(evt, tagAddress) + .forEach(it -> pollingOutput.addDataPoint(tagAddress, it.getValue())); + } + + + pollingOutput.finish(); + } catch (CipException e) { + if (e.getStatusCode() == 0x04) { + LOG.warn("Tag '{}' doesn't exist on device.", tagAddress, e); + pollingOutput.fail(e, "Tag '" + tagAddress + "' doesn't exist on device"); + } else { + LOG.warn("Problem accessing tag '{}' on device.", tagAddress, e); + pollingOutput.fail(e, "Problem accessing tag '" + tagAddress + "' on device."); + } + } catch (Exception e) { + LOG.warn("An exception occurred while reading tag '{}'.", tagAddress, e); + pollingOutput.fail(e, "An exception occurred while reading tag '" + tagAddress + "'."); + } + } + + private List handleResult(final CIPData evt, final String tagAddress) { + return EtherIpValueFactory + .fromTagAddressAndCipData(tagAddress, evt) + .map(List::of) + .orElseGet( () -> { + LOG.warn("Unable to parse tag {}, type {} not supported", tagAddress, evt.getType()); + return List.of(); + }); + } + + @Override + public @NotNull List getPollingContexts() { + return adapterConfig.getSubscriptions(); + } + + @Override + public int getPollingIntervalMillis() { + return adapterConfig.getPollingIntervalMillis(); + } + + @Override + public int getMaxPollingErrorsBeforeRemoval() { + return adapterConfig.getMaxPollingErrorsBeforeRemoval(); + } + + /** + * Use this hook method to modify the query generated used to read|subscribe to the devices, + * for the most part this is simply the tagAddress field unchanged from the subscription + *

+ * Default: tagAddress:expectedDataType eg. "0%20:BOOL" + */ + protected @NotNull String createTagAddressForSubscription(@NotNull final EtherIpAdapterConfig.PollingContextImpl subscription) { + return String.format("%s%s%s", subscription.getTagAddress(), TAG_ADDRESS_TYPE_SEP, subscription.getDataType()); + } + +} diff --git a/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/EtherIpProtocolAdapterFactory.java b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/EtherIpProtocolAdapterFactory.java new file mode 100644 index 0000000000..f6aa22e38a --- /dev/null +++ b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/EtherIpProtocolAdapterFactory.java @@ -0,0 +1,43 @@ +/* + * Copyright 2023-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hivemq.edge.adapters.etherip; + +import com.hivemq.adapter.sdk.api.ProtocolAdapter; +import com.hivemq.adapter.sdk.api.ProtocolAdapterInformation; +import com.hivemq.adapter.sdk.api.factories.ProtocolAdapterFactory; +import com.hivemq.adapter.sdk.api.model.ProtocolAdapterInput; +import com.hivemq.edge.adapters.etherip.model.EtherIpAdapterConfig; +import org.jetbrains.annotations.NotNull; + +public class EtherIpProtocolAdapterFactory implements ProtocolAdapterFactory { + + @Override + public @NotNull ProtocolAdapterInformation getInformation() { + return EtherIpProtocolAdapterInformation.INSTANCE; + } + + @Override + public @NotNull ProtocolAdapter createAdapter(final @NotNull ProtocolAdapterInformation adapterInformation, @NotNull final ProtocolAdapterInput input) { + return new EtherIpPollingProtocolAdapter(adapterInformation, input); + } + + + @Override + public @NotNull Class getConfigClass() { + return EtherIpAdapterConfig.class; + } + +} diff --git a/modules/hivemq-edge-module-plc4x/src/main/java/com/hivemq/edge/adapters/plc4x/types/eip/EIPProtocolAdapterInformation.java b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/EtherIpProtocolAdapterInformation.java similarity index 77% rename from modules/hivemq-edge-module-plc4x/src/main/java/com/hivemq/edge/adapters/plc4x/types/eip/EIPProtocolAdapterInformation.java rename to modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/EtherIpProtocolAdapterInformation.java index 4f418538b8..79569df4e8 100644 --- a/modules/hivemq-edge-module-plc4x/src/main/java/com/hivemq/edge/adapters/plc4x/types/eip/EIPProtocolAdapterInformation.java +++ b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/EtherIpProtocolAdapterInformation.java @@ -1,19 +1,20 @@ /* * Copyright 2023-present HiveMQ GmbH * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ -package com.hivemq.edge.adapters.plc4x.types.eip; +package com.hivemq.edge.adapters.etherip; + import com.hivemq.adapter.sdk.api.ProtocolAdapterCapability; import com.hivemq.adapter.sdk.api.ProtocolAdapterCategory; @@ -30,15 +31,12 @@ import java.util.EnumSet; import java.util.List; -/** - * @author HiveMQ Adapter Generator - */ -public class EIPProtocolAdapterInformation implements ProtocolAdapterInformation { +public class EtherIpProtocolAdapterInformation implements ProtocolAdapterInformation { - public static final ProtocolAdapterInformation INSTANCE = new EIPProtocolAdapterInformation(); - private static final @NotNull Logger log = LoggerFactory.getLogger(EIPProtocolAdapterInformation.class); + public static final ProtocolAdapterInformation INSTANCE = new EtherIpProtocolAdapterInformation(); + private static final @NotNull Logger log = LoggerFactory.getLogger(EtherIpProtocolAdapterInformation.class); - protected EIPProtocolAdapterInformation() { + protected EtherIpProtocolAdapterInformation() { } @Override diff --git a/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/EtherIpAdapterConfig.java b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/EtherIpAdapterConfig.java new file mode 100644 index 0000000000..4c63925ba4 --- /dev/null +++ b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/EtherIpAdapterConfig.java @@ -0,0 +1,326 @@ +/* + * Copyright 2023-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hivemq.edge.adapters.etherip.model; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.hivemq.adapter.sdk.api.annotations.ModuleConfigField; +import com.hivemq.adapter.sdk.api.config.MessageHandlingOptions; +import com.hivemq.adapter.sdk.api.config.PollingContext; +import com.hivemq.adapter.sdk.api.config.ProtocolAdapterConfig; +import com.hivemq.adapter.sdk.api.config.UserProperty; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class EtherIpAdapterConfig implements ProtocolAdapterConfig { + + private static final @NotNull String ID_REGEX = "^([a-zA-Z_0-9-_])*$"; + private static final int PORT_MIN = 1; + private static final int PORT_MAX = 65535; + + @JsonProperty("port") + @ModuleConfigField(title = "Port", + description = "The port number on the device you wish to connect to", + required = true, + numberMin = PORT_MIN, + numberMax = PORT_MAX, + defaultValue = "44818") + private int port = 44818; + + @JsonProperty("backplane") + @ModuleConfigField(title = "Backplane", + description = "Backplane device value", + defaultValue = "1", + required = false) + private @NotNull Integer backplane; + + @JsonProperty("slot") + @ModuleConfigField(title = "Slot", description = "Slot device value", defaultValue = "0", required = false) + private @NotNull Integer slot; + + @JsonProperty("subscriptions") + @ModuleConfigField(title = "Subscriptions", description = "Map your sensor data to MQTT Topics") + private @NotNull List subscriptions = new ArrayList<>(); + + + @JsonProperty("pollingIntervalMillis") + @JsonAlias(value = "publishingInterval") //-- Ensure we cater for properties created with legacy configuration + @ModuleConfigField(title = "Polling Interval [ms]", + description = "Time in millisecond that this endpoint will be polled", + numberMin = 1, + required = true, + defaultValue = "1000") + private int pollingIntervalMillis = 1000; //1 second + + @JsonProperty("maxPollingErrorsBeforeRemoval") + @ModuleConfigField(title = "Max. Polling Errors", + description = "Max. errors polling the endpoint before the polling daemon is stopped", + defaultValue = "10") + private int maxPollingErrorsBeforeRemoval = 10; + + @JsonProperty("host") + @ModuleConfigField(title = "Host", + description = "IP Address or hostname of the device you wish to connect to", + required = true, + format = ModuleConfigField.FieldType.HOSTNAME) + private @NotNull String host; + + @JsonProperty("publishChangedDataOnly") + @ModuleConfigField(title = "Only publish data items that have changed since last poll", + defaultValue = "true", + format = ModuleConfigField.FieldType.BOOLEAN) + private boolean publishChangedDataOnly = true; + + @JsonProperty(value = "id", required = true) + @ModuleConfigField(title = "Identifier", + description = "Unique identifier for this protocol adapter", + format = ModuleConfigField.FieldType.IDENTIFIER, + required = true, + stringPattern = ID_REGEX, + stringMinLength = 1, + stringMaxLength = 1024) + protected @NotNull String id; + + public enum EIP_DATA_TYPE { + BOOL, + DINT, + INT, + LINT, + LREAL, + LTIME, + REAL, + SINT, + STRING, + TIME, + UDINT, + UINT, + ULINT, + USINT; + } + + @JsonPropertyOrder({"tagName", "tagAddress", "dataType", "destination", "qos"}) + @JsonIgnoreProperties({"dataType"}) + public static class EIPPollingContextImpl extends PollingContextImpl { + @JsonProperty("eipDataType") + @ModuleConfigField(title = "Data Type", description = "The expected data type of the tag", enumDisplayValues = { + "Bool", + "DInt", + "Int", + "LInt", + "LReal", + "LTime", + "Real", + "SInt", + "String", + "Time", + "UDInt", + "UInt", + "ULInt", + "USInt"}, required = true) + private @NotNull EIP_DATA_TYPE eipDataType; + + public EIP_DATA_TYPE getEipDataType() { + return eipDataType; + } + } + + public Integer getBackplane() { + return backplane; + } + + public Integer getSlot() { + return slot; + } + + public EtherIpAdapterConfig() { + } + + public @NotNull String getId() { + return id; + } + + public void setId(final @NotNull String id) { + this.id = id; + } + + public int getPort() { + return port; + } + + public @NotNull String getHost() { + return host; + } + + public boolean getPublishChangedDataOnly() { + return publishChangedDataOnly; + } + + public @NotNull List getSubscriptions() { + return subscriptions; + } + + public int getPollingIntervalMillis() { + return pollingIntervalMillis; + } + + public int getMaxPollingErrorsBeforeRemoval() { + return maxPollingErrorsBeforeRemoval; + } + + @JsonPropertyOrder({"tagName", "tagAddress", "dataType", "destination", "qos"}) + public static class PollingContextImpl implements PollingContext { + + @JsonProperty(value = "tagName", required = true) + @ModuleConfigField(title = "Tag Name", + description = "The name to assign to this address. The tag name must be unique for all subscriptions within this protocol adapter.", + required = true, + format = ModuleConfigField.FieldType.IDENTIFIER) + private @NotNull String tagName; + + @JsonProperty("tagAddress") + @ModuleConfigField(title = "Tag Address", + description = "The well formed address of the tag to read", + required = true) + private @NotNull String tagAddress; + + @JsonProperty("dataType") + @ModuleConfigField(title = "Data Type", + description = "The expected data type of the tag", + enumDisplayValues = {"Null", + "Boolean", + "Byte", + "Word (unit 16)", + "DWord (uint 32)", + "LWord (uint 64)", + "USint (uint 8)", + "Uint (uint 16)", + "UDint (uint 32)", + "ULint (uint 64)", + "Sint (int 8)", + "Int (int 16)", + "Dint (int 32)", + "Lint (int 64)", + "Real (float 32)", + "LReal (double 64)", + "Char (1 byte char)", + "WChar (2 byte char)", + "String", + "WString", + "Timing (Duration ms)", + "Long Timing (Duration ns)", + "Date (DateStamp)", + "Long Date (DateStamp)", + "Time Of Day (TimeStamp)", + "Long Time Of Day (TimeStamp)", + "Date Time (DateTimeStamp)", + "Long Date Time (DateTimeStamp)", + "Raw Byte Array" + }, + required = true) + private @NotNull EtherIpDataTypes.DATA_TYPE dataType; + + @JsonProperty(value = "destination", required = true) + @ModuleConfigField(title = "Destination Topic", + description = "The topic to publish data on", + required = true, + format = ModuleConfigField.FieldType.MQTT_TOPIC) + protected @Nullable String destination; + + @JsonProperty(value = "qos", required = true) + @ModuleConfigField(title = "QoS", + description = "MQTT Quality of Service level", + required = true, + numberMin = 0, + numberMax = 2, + defaultValue = "0") + protected int qos = 0; + + @JsonProperty(value = "messageHandlingOptions") + @ModuleConfigField(title = "Message Handling Options", + description = "This setting defines the format of the resulting MQTT message, either a message per changed tag or a message per subscription that may include multiple data points per sample", + enumDisplayValues = { + "MQTT Message Per Device Tag", + "MQTT Message Per Subscription (Potentially Multiple Data Points Per Sample)"}, + defaultValue = "MQTTMessagePerTag") + protected @NotNull MessageHandlingOptions messageHandlingOptions = MessageHandlingOptions.MQTTMessagePerTag; + + @JsonProperty(value = "includeTimestamp") + @ModuleConfigField(title = "Include Sample Timestamp In Publish?", + description = "Include the unix timestamp of the sample time in the resulting MQTT message", + defaultValue = "true") + protected @NotNull Boolean includeTimestamp = Boolean.TRUE; + + @JsonProperty(value = "includeTagNames") + @ModuleConfigField(title = "Include Tag Names In Publish?", + description = "Include the names of the tags in the resulting MQTT publish", + defaultValue = "false") + protected @NotNull Boolean includeTagNames = Boolean.FALSE; + + @JsonProperty(value = "userProperties") + @ModuleConfigField(title = "User Properties", + description = "Arbitrary properties to associate with the subscription", + arrayMaxItems = 10) + private @NotNull List userProperties = new ArrayList<>(); + + + public @NotNull String getTagName() { + return tagName; + } + + public String getTagAddress() { + return tagAddress; + } + + public EtherIpDataTypes.DATA_TYPE getDataType() { + return dataType; + } + + @Override + public @Nullable String getDestinationMqttTopic() { + return destination; + } + + @Override + public int getQos() { + return qos; + } + + @Override + public @NotNull MessageHandlingOptions getMessageHandlingOptions() { + return messageHandlingOptions; + } + + @Override + public @NotNull Boolean getIncludeTimestamp() { + return includeTimestamp; + } + + @Override + public @NotNull Boolean getIncludeTagNames() { + return includeTagNames; + } + + @Override + public @NotNull List getUserProperties() { + return userProperties; + } + } +} diff --git a/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/EtherIpDataTypes.java b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/EtherIpDataTypes.java new file mode 100644 index 0000000000..adbbb52f89 --- /dev/null +++ b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/EtherIpDataTypes.java @@ -0,0 +1,80 @@ +/* + * Copyright 2023-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hivemq.edge.adapters.etherip.model; + +import java.math.BigInteger; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +/** + * @author Simon L Johnson + */ +public class EtherIpDataTypes { + + //-- A sub-set of the supported core types (TODO fix the object types) + public enum DATA_TYPE { + + NULL((short) 0x00, null), + BOOL((short) 0x01, Boolean.class), + BYTE((short) 0x02, Byte.class), + WORD((short) 0x03, Short.class), + DWORD((short) 0x04, Integer.class), + LWORD((short) 0x05, Long.class), + USINT((short) 0x11, Short.class), + UINT((short) 0x12, Integer.class), + UDINT((short) 0x13, Long.class), + ULINT((short) 0x14, BigInteger.class), + SINT((short) 0x21, Byte.class), + INT((short) 0x22, Short.class), + DINT((short) 0x23, Integer.class), + LINT((short) 0x24, Long.class), + REAL((short) 0x31, Float.class), + LREAL((short) 0x32, Double.class), + CHAR((short) 0x41, Character.class), + WCHAR((short) 0x42, Short.class), + STRING((short) 0x43, String.class), + WSTRING((short) 0x44, String.class), + TIME((short) 0x51, Duration.class), + LTIME((short) 0x52, Duration.class), + DATE((short) 0x53, LocalDate.class), + LDATE((short) 0x54, LocalDate.class), + TIME_OF_DAY((short) 0x55, LocalTime.class), + LTIME_OF_DAY((short) 0x56, LocalTime.class), + DATE_AND_TIME((short) 0x57, LocalDateTime.class), + LDATE_AND_TIME((short) 0x58, LocalDateTime.class), + RAW_BYTE_ARRAY((short) 0x71, Byte.class); + + + DATA_TYPE(short code, Class javaType){ + this.code = code; + this.javaType = javaType; + } + + private short code; + private Class javaType; + + public short getCode() { + return code; + } + + public Class getJavaType() { + return javaType; + } + } + +} diff --git a/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/EtherIpValue.java b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/EtherIpValue.java new file mode 100644 index 0000000000..fd5110711d --- /dev/null +++ b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/EtherIpValue.java @@ -0,0 +1,9 @@ +package com.hivemq.edge.adapters.etherip.model; + +public interface EtherIpValue { + + String getTagAdress(); + + Object getValue(); + +} diff --git a/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/EtherIpValueFactory.java b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/EtherIpValueFactory.java new file mode 100644 index 0000000000..934777dddf --- /dev/null +++ b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/EtherIpValueFactory.java @@ -0,0 +1,53 @@ +package com.hivemq.edge.adapters.etherip.model; + +import com.hivemq.edge.adapters.etherip.model.dataypes.EtherIpBool; +import com.hivemq.edge.adapters.etherip.model.dataypes.EtherIpDouble; +import com.hivemq.edge.adapters.etherip.model.dataypes.EtherIpInt; +import com.hivemq.edge.adapters.etherip.model.dataypes.EtherIpLong; +import com.hivemq.edge.adapters.etherip.model.dataypes.EtherIpString; +import etherip.types.CIPData; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; + +public class EtherIpValueFactory { + private static final Logger log = LoggerFactory.getLogger(EtherIpValue.class); + + public static Optional fromTagAddressAndCipData(@NotNull String tagAddress, @NotNull CIPData cipData) { + CIPData.Type dataType = cipData.getType(); + + try { + if(cipData.isNumeric()) { + if (cipData.getElementCount() > 1) { + log.warn("More than one element returned, only the first one will be used"); + } + Number number = cipData.getNumber(0); + log.debug("Got value {} for type {} for tag address {}", number, dataType, tagAddress); + switch (dataType) { + case BOOL: + return Optional.of(new EtherIpBool(tagAddress, number)); + case SINT: + case INT: + return Optional.of(new EtherIpInt(tagAddress, number)); + case DINT: + return Optional.of(new EtherIpLong(tagAddress, number)); + case REAL: + return Optional.of(new EtherIpDouble(tagAddress, number)); + case BITS: + case STRUCT: + case STRUCT_STRING: + return Optional.empty(); + } + return Optional.empty(); + } else { + log.debug("Got value {} for type {} for tag address {}", cipData.getString(), dataType, tagAddress); + return Optional.of(new EtherIpString(tagAddress, cipData.getString())); + } + } catch (Exception e) { + log.error("Unable to parse data", e); + } + return Optional.empty(); + } +} diff --git a/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/dataypes/EtherIpBool.java b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/dataypes/EtherIpBool.java new file mode 100644 index 0000000000..7f3d7bfb98 --- /dev/null +++ b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/dataypes/EtherIpBool.java @@ -0,0 +1,39 @@ +package com.hivemq.edge.adapters.etherip.model.dataypes; + +import com.hivemq.edge.adapters.etherip.model.EtherIpValue; + +import java.util.Objects; + +public class EtherIpBool implements EtherIpValue { + private final Boolean value; + private final String tagAddress; + + public EtherIpBool(final String tagAddress, final Number value) { + //Values of 0 are false, all other values are treated as true + this.value = value.intValue() != 0; + this.tagAddress = tagAddress; + } + + @Override + public Object getValue() { + return value; + } + + @Override + public String getTagAdress() { + return tagAddress; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EtherIpBool that = (EtherIpBool) o; + return Objects.equals(value, that.value) && Objects.equals(tagAddress, that.tagAddress); + } + + @Override + public int hashCode() { + return Objects.hash(value, tagAddress); + } +} diff --git a/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/dataypes/EtherIpDouble.java b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/dataypes/EtherIpDouble.java new file mode 100644 index 0000000000..48aa2c9596 --- /dev/null +++ b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/dataypes/EtherIpDouble.java @@ -0,0 +1,38 @@ +package com.hivemq.edge.adapters.etherip.model.dataypes; + +import com.hivemq.edge.adapters.etherip.model.EtherIpValue; + +import java.util.Objects; + +public class EtherIpDouble implements EtherIpValue { + private final Double value; + private final String tagAddress; + + public EtherIpDouble(final String tagAddress, final Number value) { + this.value = value.doubleValue(); + this.tagAddress = tagAddress; + } + + @Override + public Object getValue() { + return value; + } + + @Override + public String getTagAdress() { + return tagAddress; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EtherIpDouble that = (EtherIpDouble) o; + return Objects.equals(value, that.value) && Objects.equals(tagAddress, that.tagAddress); + } + + @Override + public int hashCode() { + return Objects.hash(value, tagAddress); + } +} diff --git a/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/dataypes/EtherIpInt.java b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/dataypes/EtherIpInt.java new file mode 100644 index 0000000000..b0b48c6eac --- /dev/null +++ b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/dataypes/EtherIpInt.java @@ -0,0 +1,38 @@ +package com.hivemq.edge.adapters.etherip.model.dataypes; + +import com.hivemq.edge.adapters.etherip.model.EtherIpValue; + +import java.util.Objects; + +public class EtherIpInt implements EtherIpValue { + private final Integer value; + private final String tagAddress; + + public EtherIpInt(final String tagAddress, final Number value) { + this.value = value.intValue(); + this.tagAddress = tagAddress; + } + + @Override + public Object getValue() { + return value; + } + + @Override + public String getTagAdress() { + return tagAddress; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EtherIpInt that = (EtherIpInt) o; + return Objects.equals(value, that.value) && Objects.equals(tagAddress, that.tagAddress); + } + + @Override + public int hashCode() { + return Objects.hash(value, tagAddress); + } +} diff --git a/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/dataypes/EtherIpLong.java b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/dataypes/EtherIpLong.java new file mode 100644 index 0000000000..752e299a31 --- /dev/null +++ b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/dataypes/EtherIpLong.java @@ -0,0 +1,38 @@ +package com.hivemq.edge.adapters.etherip.model.dataypes; + +import com.hivemq.edge.adapters.etherip.model.EtherIpValue; + +import java.util.Objects; + +public class EtherIpLong implements EtherIpValue { + private final Long value; + private final String tagAddress; + + public EtherIpLong(final String tagAddress, final Number value) { + this.value = value.longValue(); + this.tagAddress = tagAddress; + } + + @Override + public Object getValue() { + return value; + } + + @Override + public String getTagAdress() { + return tagAddress; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EtherIpLong that = (EtherIpLong) o; + return Objects.equals(value, that.value) && Objects.equals(tagAddress, that.tagAddress); + } + + @Override + public int hashCode() { + return Objects.hash(value, tagAddress); + } +} diff --git a/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/dataypes/EtherIpString.java b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/dataypes/EtherIpString.java new file mode 100644 index 0000000000..f3a3ed771d --- /dev/null +++ b/modules/hivemq-edge-module-etherip/src/main/java/com/hivemq/edge/adapters/etherip/model/dataypes/EtherIpString.java @@ -0,0 +1,38 @@ +package com.hivemq.edge.adapters.etherip.model.dataypes; + +import com.hivemq.edge.adapters.etherip.model.EtherIpValue; + +import java.util.Objects; + +public class EtherIpString implements EtherIpValue { + private final String value; + private final String tagAddress; + + public EtherIpString(final String tagAddress, final String value) { + this.value = value; + this.tagAddress = tagAddress; + } + + @Override + public Object getValue() { + return value; + } + + @Override + public String getTagAdress() { + return tagAddress; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EtherIpString that = (EtherIpString) o; + return Objects.equals(value, that.value) && Objects.equals(tagAddress, that.tagAddress); + } + + @Override + public int hashCode() { + return Objects.hash(value, tagAddress); + } +} diff --git a/modules/hivemq-edge-module-etherip/src/main/resources/META-INF/services/com.hivemq.adapter.sdk.api.factories.ProtocolAdapterFactory b/modules/hivemq-edge-module-etherip/src/main/resources/META-INF/services/com.hivemq.adapter.sdk.api.factories.ProtocolAdapterFactory new file mode 100644 index 0000000000..682caf218c --- /dev/null +++ b/modules/hivemq-edge-module-etherip/src/main/resources/META-INF/services/com.hivemq.adapter.sdk.api.factories.ProtocolAdapterFactory @@ -0,0 +1,2 @@ +com.hivemq.edge.adapters.etherip.EtherIpProtocolAdapterFactory + diff --git a/modules/hivemq-edge-module-plc4x/src/main/resources/eip-adapter-ui-schema.json b/modules/hivemq-edge-module-etherip/src/main/resources/eip-adapter-ui-schema.json similarity index 100% rename from modules/hivemq-edge-module-plc4x/src/main/resources/eip-adapter-ui-schema.json rename to modules/hivemq-edge-module-etherip/src/main/resources/eip-adapter-ui-schema.json diff --git a/modules/hivemq-edge-module-plc4x/src/main/resources/httpd/images/ab-eth-icon.png b/modules/hivemq-edge-module-etherip/src/main/resources/httpd/images/ab-eth-icon.png similarity index 100% rename from modules/hivemq-edge-module-plc4x/src/main/resources/httpd/images/ab-eth-icon.png rename to modules/hivemq-edge-module-etherip/src/main/resources/httpd/images/ab-eth-icon.png diff --git a/modules/hivemq-edge-module-etherip/src/test/java/com/hivemq/edge/adapters/etherip/Constants.java b/modules/hivemq-edge-module-etherip/src/test/java/com/hivemq/edge/adapters/etherip/Constants.java new file mode 100644 index 0000000000..4b4a2a8d2b --- /dev/null +++ b/modules/hivemq-edge-module-etherip/src/test/java/com/hivemq/edge/adapters/etherip/Constants.java @@ -0,0 +1,20 @@ +/* + * Copyright 2023-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hivemq.edge.adapters.etherip; + +public class Constants { + public static final String TAG_REQUIRES_VPN = "requiresVpn"; +} diff --git a/modules/hivemq-edge-module-etherip/src/test/java/com/hivemq/edge/adapters/etherip/EtherIpPollingProtocolAdapterIT.java b/modules/hivemq-edge-module-etherip/src/test/java/com/hivemq/edge/adapters/etherip/EtherIpPollingProtocolAdapterIT.java new file mode 100644 index 0000000000..40513630b9 --- /dev/null +++ b/modules/hivemq-edge-module-etherip/src/test/java/com/hivemq/edge/adapters/etherip/EtherIpPollingProtocolAdapterIT.java @@ -0,0 +1,165 @@ +package com.hivemq.edge.adapters.etherip; + +import com.hivemq.adapter.sdk.api.model.ProtocolAdapterInput; +import com.hivemq.adapter.sdk.api.model.ProtocolAdapterStartOutput; +import com.hivemq.adapter.sdk.api.polling.PollingInput; +import com.hivemq.adapter.sdk.api.polling.PollingOutput; +import com.hivemq.edge.adapters.etherip.model.EtherIpAdapterConfig; +import com.hivemq.edge.adapters.etherip.model.EtherIpDataTypes; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; + +import java.util.stream.Stream; + +import static com.hivemq.edge.adapters.etherip.Constants.TAG_REQUIRES_VPN; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.withPrecision; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@Tag(TAG_REQUIRES_VPN) +public class EtherIpPollingProtocolAdapterIT { + + private static String TAG_INT = "at_int_tag"; + private static String TAG_BOOL = "at_bool_tag"; + private static String TAG_PROGRAM_BOOL_TRUE = "program:MainProgram.dev_bool_tag_t"; + private static String TAG_PROGRAM_BOOL_FALSE = "program:MainProgram.dev_bool_tag_f"; + private static String TAG_REAL = "at_real_tag"; + private static String TAG_STRING = "at_string_tag"; + + private static final String HOST = "172.16.10.60"; + + public static Stream tagsToExpectedValues() { + return Stream.of( + Arguments.of(TAG_INT, EtherIpDataTypes.DATA_TYPE.INT, TAG_INT + ":INT", 3), + Arguments.of(TAG_BOOL, EtherIpDataTypes.DATA_TYPE.BOOL, TAG_BOOL + ":BOOL", false), + Arguments.of(TAG_PROGRAM_BOOL_TRUE, EtherIpDataTypes.DATA_TYPE.BOOL, TAG_PROGRAM_BOOL_TRUE + ":BOOL", true), + Arguments.of(TAG_PROGRAM_BOOL_FALSE, EtherIpDataTypes.DATA_TYPE.BOOL, TAG_PROGRAM_BOOL_FALSE + ":BOOL", false), + Arguments.of(TAG_STRING, EtherIpDataTypes.DATA_TYPE.STRING, TAG_STRING + ":STRING", "test"), + Arguments.of(TAG_REAL, EtherIpDataTypes.DATA_TYPE.REAL, TAG_REAL + ":REAL", 5.59) + ); + } + + @ParameterizedTest + @MethodSource("tagsToExpectedValues") + public void test_parameterized(String tagAddress, EtherIpDataTypes.DATA_TYPE tagType, String expectedName, Object expectedValue) { + EtherIpAdapterConfig config = mock(EtherIpAdapterConfig.class); + when(config.getHost()).thenReturn(HOST); + when(config.getSlot()).thenReturn(0); + + ProtocolAdapterInput inputMock = mock(ProtocolAdapterInput.class); + when(inputMock.getConfig()).thenReturn(config); + + EtherIpAdapterConfig.EIPPollingContextImpl ctx = mock(EtherIpAdapterConfig.EIPPollingContextImpl.class); + when(ctx.getTagAddress()).thenReturn(tagAddress); + when(ctx.getDataType()).thenReturn(tagType); + + PollingInput input = mock(PollingInput.class); + when(input.getPollingContext()).thenReturn(ctx); + + PollingOutput output = mock(PollingOutput.class); + + EtherIpPollingProtocolAdapter adapter = new EtherIpPollingProtocolAdapter( + new EtherIpProtocolAdapterInformation(), + inputMock); + + adapter.start(null, mock(ProtocolAdapterStartOutput.class)); + + ArgumentCaptor captorName = ArgumentCaptor.forClass(String.class); + ArgumentCaptor captorValue = ArgumentCaptor.forClass(Object.class); + + adapter.poll(input, output); + verify(output).addDataPoint(captorName.capture(), captorValue.capture()); + + assertThat(captorName.getAllValues()).first().isEqualTo(expectedName); + if (expectedValue instanceof Double) { + assertThat(captorValue.getValue()) + .isInstanceOf(Double.class) + .asInstanceOf(InstanceOfAssertFactories.DOUBLE) + .isEqualTo((Double) expectedValue, withPrecision(2d)); + } else { + assertThat(captorValue.getValue()) + .isEqualTo(expectedValue); + } + } + + @Test + public void test_PublishChangedDataOnly_False() { + EtherIpAdapterConfig config = mock(EtherIpAdapterConfig.class); + when(config.getHost()).thenReturn(HOST); + when(config.getSlot()).thenReturn(0); + + ProtocolAdapterInput inputMock = mock(ProtocolAdapterInput.class); + when(inputMock.getConfig()).thenReturn(config); + + EtherIpAdapterConfig.EIPPollingContextImpl ctx = mock(EtherIpAdapterConfig.EIPPollingContextImpl.class); + when(ctx.getTagAddress()).thenReturn(TAG_INT); + when(ctx.getDataType()).thenReturn(EtherIpDataTypes.DATA_TYPE.INT); + + PollingInput input = mock(PollingInput.class); + when(input.getPollingContext()).thenReturn(ctx); + + PollingOutput output = mock(PollingOutput.class); + + EtherIpPollingProtocolAdapter adapter = new EtherIpPollingProtocolAdapter( + new EtherIpProtocolAdapterInformation(), + inputMock); + + adapter.start(null, mock(ProtocolAdapterStartOutput.class)); + + ArgumentCaptor captorName = ArgumentCaptor.forClass(String.class); + ArgumentCaptor captorValue = ArgumentCaptor.forClass(Object.class); + + adapter.poll(input, output); + adapter.poll(input, output); + verify(output, times(2)).addDataPoint(captorName.capture(), captorValue.capture()); + + assertThat(captorName.getAllValues()).allMatch(n -> n.equals(TAG_INT + ":INT")); + assertThat(captorValue.getAllValues()).allMatch(v -> v.equals(3)); + } + + @Test + public void test_PublishChangedDataOnly_True() { + EtherIpAdapterConfig config = mock(EtherIpAdapterConfig.class); + when(config.getHost()).thenReturn(HOST); + when(config.getSlot()).thenReturn(0); + when(config.getPublishChangedDataOnly()).thenReturn(true); + + ProtocolAdapterInput inputMock = mock(ProtocolAdapterInput.class); + when(inputMock.getConfig()).thenReturn(config); + + EtherIpAdapterConfig.EIPPollingContextImpl ctx = mock(EtherIpAdapterConfig.EIPPollingContextImpl.class); + when(ctx.getTagAddress()).thenReturn(TAG_INT); + when(ctx.getDataType()).thenReturn(EtherIpDataTypes.DATA_TYPE.INT); + + PollingInput input = mock(PollingInput.class); + when(input.getPollingContext()).thenReturn(ctx); + + PollingOutput output = mock(PollingOutput.class); + + EtherIpPollingProtocolAdapter adapter = new EtherIpPollingProtocolAdapter( + new EtherIpProtocolAdapterInformation(), + inputMock); + + adapter.start(null, mock(ProtocolAdapterStartOutput.class)); + + ArgumentCaptor captorName = ArgumentCaptor.forClass(String.class); + ArgumentCaptor captorValue = ArgumentCaptor.forClass(Object.class); + + adapter.poll(input, output); + adapter.poll(input, output); + verify(output, times(1)).addDataPoint(captorName.capture(), captorValue.capture()); + + assertThat(captorName.getAllValues()).hasSize(1); + assertThat(captorName.getValue()).isEqualTo(TAG_INT + ":INT"); + assertThat(captorValue.getAllValues()).hasSize(1); + assertThat(captorValue.getValue()).isEqualTo(3); + } +} diff --git a/modules/hivemq-edge-module-etherip/src/test/java/com/hivemq/edge/adapters/etherip/model/EtherIpValueFactoryTest.java b/modules/hivemq-edge-module-etherip/src/test/java/com/hivemq/edge/adapters/etherip/model/EtherIpValueFactoryTest.java new file mode 100644 index 0000000000..d7aa1cf334 --- /dev/null +++ b/modules/hivemq-edge-module-etherip/src/test/java/com/hivemq/edge/adapters/etherip/model/EtherIpValueFactoryTest.java @@ -0,0 +1,91 @@ +package com.hivemq.edge.adapters.etherip.model; + +import com.hivemq.edge.adapters.etherip.model.dataypes.EtherIpBool; +import com.hivemq.edge.adapters.etherip.model.dataypes.EtherIpDouble; +import com.hivemq.edge.adapters.etherip.model.dataypes.EtherIpInt; +import com.hivemq.edge.adapters.etherip.model.dataypes.EtherIpLong; +import etherip.types.CIPData; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.offset; + +public class EtherIpValueFactoryTest { + + @Test + public void test_EtherIpBool_False() throws Exception { + CIPData data = new CIPData(CIPData.Type.BOOL, 1); + data.set(0, 0); + assertThat(EtherIpValueFactory.fromTagAddressAndCipData("wullewu", data)) + .isNotEmpty() + .get() + .isInstanceOf(EtherIpBool.class) + .extracting(EtherIpValue::getTagAdress, EtherIpValue::getValue) + .containsExactly("wullewu", false); + } + + @Test + public void test_EtherIpBool_True() throws Exception { + CIPData data = new CIPData(CIPData.Type.BOOL, 1); + data.set(0, 1); + assertThat(EtherIpValueFactory.fromTagAddressAndCipData("wullewu", data)) + .isNotEmpty() + .get() + .isInstanceOf(EtherIpBool.class) + .extracting(EtherIpValue::getTagAdress, EtherIpValue::getValue) + .containsExactly("wullewu", true); + } + + @Test + public void test_EtherIpDouble() throws Exception { + CIPData data = new CIPData(CIPData.Type.REAL, 1); + data.set(0, 1.12); + assertThat(EtherIpValueFactory.fromTagAddressAndCipData("wullewu", data)) + .isNotEmpty() + .get() + .isInstanceOf(EtherIpDouble.class) + .satisfies(it -> { + assertThat(it.getTagAdress()).isEqualTo("wullewu"); + assertThat((Double)it.getValue()).isEqualTo(1.12, offset(2D)); + }); + } + + @Test + public void test_EtherIpInt() throws Exception { + CIPData data = new CIPData(CIPData.Type.INT, 1); + data.set(0, 17); + assertThat(EtherIpValueFactory.fromTagAddressAndCipData("wullewu", data)) + .isNotEmpty() + .get() + .isInstanceOf(EtherIpInt.class) + .extracting(EtherIpValue::getTagAdress, EtherIpValue::getValue) + .containsExactly("wullewu", 17); + } + + @Test + public void test_EtherIpLong() throws Exception { + CIPData data = new CIPData(CIPData.Type.DINT, 1); + data.set(0, 17L); + assertThat(EtherIpValueFactory.fromTagAddressAndCipData("wullewu", data)) + .isNotEmpty() + .get() + .isInstanceOf(EtherIpLong.class) + .extracting(EtherIpValue::getTagAdress, EtherIpValue::getValue) + .containsExactly("wullewu", 17L); + } + + @Test + @Disabled("CIPData needs to be constructed using the CIPData(Type type, byte[] data) constructor") + //FIXME: enabled test + public void test_EtherIpString() throws Exception { + CIPData data = new CIPData(CIPData.Type.STRUCT, 1); + data.setString("test"); + assertThat(EtherIpValueFactory.fromTagAddressAndCipData("wullewu", data)) + .isNotEmpty() + .get() + .isInstanceOf(EtherIpBool.class) + .extracting(EtherIpValue::getTagAdress, EtherIpValue::getValue) + .containsExactly("wullewu", "test"); + } +} diff --git a/modules/hivemq-edge-module-plc4x/build.gradle.kts b/modules/hivemq-edge-module-plc4x/build.gradle.kts index ff948b9eab..f4073b4450 100644 --- a/modules/hivemq-edge-module-plc4x/build.gradle.kts +++ b/modules/hivemq-edge-module-plc4x/build.gradle.kts @@ -41,8 +41,6 @@ dependencies { implementation(libs.plc4j.s7) implementation(libs.plc4j.ads) implementation(libs.plc4j.api) - implementation(libs.plc4j.eip) - implementation(libs.plc4j.ab.eth) implementation(libs.plc4j.transport.raw.socket) } diff --git a/modules/hivemq-edge-module-plc4x/gradle/libs.versions.toml b/modules/hivemq-edge-module-plc4x/gradle/libs.versions.toml index 43a17c7e7c..5b412fb963 100644 --- a/modules/hivemq-edge-module-plc4x/gradle/libs.versions.toml +++ b/modules/hivemq-edge-module-plc4x/gradle/libs.versions.toml @@ -15,8 +15,6 @@ mockito-junitJupiter = { module = "org.mockito:mockito-junit-jupiter", version.r plc4j-api = { module = "org.apache.plc4x:plc4j-api", version.ref = "apache-plc4x" } plc4j-s7 = { module = "org.apache.plc4x:plc4j-driver-s7", version.ref = "apache-plc4x" } plc4j-ads = { module = "org.apache.plc4x:plc4j-driver-ads", version.ref = "apache-plc4x" } -plc4j-eip = { module = "org.apache.plc4x:plc4j-driver-eip", version.ref = "apache-plc4x" } -plc4j-ab-eth = { module = "org.apache.plc4x:plc4j-driver-ab-eth", version.ref = "apache-plc4x" } plc4j-transport-raw-socket = { module = "org.apache.plc4x:plc4j-transport-raw-socket", version.ref = "apache-plc4x" } slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } diff --git a/modules/hivemq-edge-module-plc4x/src/main/java/com/hivemq/edge/adapters/plc4x/types/eip/EIPAdapterConfig.java b/modules/hivemq-edge-module-plc4x/src/main/java/com/hivemq/edge/adapters/plc4x/types/eip/EIPAdapterConfig.java deleted file mode 100644 index 355e01c6ce..0000000000 --- a/modules/hivemq-edge-module-plc4x/src/main/java/com/hivemq/edge/adapters/plc4x/types/eip/EIPAdapterConfig.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2023-present HiveMQ GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hivemq.edge.adapters.plc4x.types.eip; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.hivemq.adapter.sdk.api.annotations.ModuleConfigField; -import com.hivemq.edge.adapters.plc4x.model.Plc4xAdapterConfig; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.List; - - -public class EIPAdapterConfig extends Plc4xAdapterConfig { - - private static final int PORT_MIN = 1; - private static final int PORT_MAX = 65535; - - @JsonProperty("port") - @ModuleConfigField(title = "Port", - description = "The port number on the device you wish to connect to", - required = true, - numberMin = PORT_MIN, - numberMax = PORT_MAX, - defaultValue = "44818") - private int port = 44818; - - @JsonProperty("backplane") - @ModuleConfigField(title = "Backplane", - description = "Backplane device value", - defaultValue = "1", - required = false) - private @NotNull Integer backplane; - - @JsonProperty("slot") - @ModuleConfigField(title = "Slot", description = "Slot device value", defaultValue = "0", required = false) - private @NotNull Integer slot; - - @JsonProperty("subscriptions") - @ModuleConfigField(title = "Subscriptions", description = "Map your sensor data to MQTT Topics") - private @NotNull List subscriptions = new ArrayList<>(); - - @Override - public int getPort() { - return port; - } - - @NotNull - @Override - public List getSubscriptions() { - return subscriptions; - } - - public enum EIP_DATA_TYPE { - BOOL, - DINT, - INT, - LINT, - LREAL, - LTIME, - REAL, - SINT, - STRING, - TIME, - UDINT, - UINT, - ULINT, - USINT; - } - - @JsonPropertyOrder({"tagName", "tagAddress", "dataType", "destination", "qos"}) - @JsonIgnoreProperties({"dataType"}) - public static class EIPPollingContextImpl extends PollingContextImpl { - @JsonProperty("eipDataType") - @ModuleConfigField(title = "Data Type", description = "The expected data type of the tag", enumDisplayValues = { - "Bool", - "DInt", - "Int", - "LInt", - "LReal", - "LTime", - "Real", - "SInt", - "String", - "Time", - "UDInt", - "UInt", - "ULInt", - "USInt"}, required = true) - private @NotNull EIPAdapterConfig.EIP_DATA_TYPE eipDataType; - - public EIPAdapterConfig.EIP_DATA_TYPE getEipDataType() { - return eipDataType; - } - } - - public Integer getBackplane() { - return backplane; - } - - public Integer getSlot() { - return slot; - } -} diff --git a/modules/hivemq-edge-module-plc4x/src/main/java/com/hivemq/edge/adapters/plc4x/types/eip/EIPProtocolAdapter.java b/modules/hivemq-edge-module-plc4x/src/main/java/com/hivemq/edge/adapters/plc4x/types/eip/EIPProtocolAdapter.java deleted file mode 100644 index 566269882b..0000000000 --- a/modules/hivemq-edge-module-plc4x/src/main/java/com/hivemq/edge/adapters/plc4x/types/eip/EIPProtocolAdapter.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2023-present HiveMQ GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hivemq.edge.adapters.plc4x.types.eip; - -import com.hivemq.adapter.sdk.api.ProtocolAdapterInformation; -import com.hivemq.adapter.sdk.api.config.PollingContext; -import com.hivemq.adapter.sdk.api.data.ProtocolAdapterDataSample; -import com.hivemq.adapter.sdk.api.model.ProtocolAdapterInput; -import com.hivemq.adapter.sdk.api.polling.PollingInput; -import com.hivemq.adapter.sdk.api.polling.PollingOutput; -import com.hivemq.edge.adapters.plc4x.impl.AbstractPlc4xAdapter; -import com.hivemq.edge.adapters.plc4x.model.Plc4xAdapterConfig; -import org.jetbrains.annotations.NotNull; - -import java.util.HashMap; -import java.util.Map; - -/** - * @author HiveMQ Adapter Generator - */ -public class EIPProtocolAdapter extends AbstractPlc4xAdapter { - - static final String SLOT = "slot", BACKPLANE = "backplane"; - - public EIPProtocolAdapter( - final @NotNull ProtocolAdapterInformation adapterInformation, - final @NotNull ProtocolAdapterInput input) { - super(adapterInformation, input); - } - - @Override - protected @NotNull String getProtocolHandler() { - return "eip:tcp"; - } - - @Override - protected @NotNull ReadType getReadType() { - return ReadType.Read; - } - - @Override - protected @NotNull String createTagAddressForSubscription(final Plc4xAdapterConfig.PollingContextImpl subscription) { - return "%" + subscription.getTagAddress(); - } - - @Override - protected @NotNull Map createQueryStringParams(final @NotNull EIPAdapterConfig config) { - Map map = new HashMap<>(); - map.put(BACKPLANE, nullSafe(config.getBackplane())); - map.put(SLOT, nullSafe(config.getSlot())); - map.put("bigEndian", "false"); - return map; - } - - @Override - public void poll( - final @NotNull PollingInput pollingInput, final @NotNull PollingOutput pollingOutput) { - final PollingContext pollingContext = pollingInput.getPollingContext(); - if (!(pollingContext instanceof EIPAdapterConfig.EIPPollingContextImpl)) { - pollingOutput.fail( "Subscription configuration is not of correct type Ethernet/IP"); - return; - } - if (connection.isConnected()) { - connection.read((Plc4xAdapterConfig.PollingContextImpl) pollingContext) - .thenApply(response -> (ProtocolAdapterDataSample) processReadResponse((EIPAdapterConfig.EIPPollingContextImpl) pollingContext, - response)) - .thenApply(data -> captureDataSample(data, pollingContext)) - .whenComplete((sample, t) -> handleDataAndExceptions(sample, t, pollingOutput)); - } else { - pollingOutput.fail( "EIP Adapter is not connected."); - } - } -} diff --git a/modules/hivemq-edge-module-plc4x/src/main/java/com/hivemq/edge/adapters/plc4x/types/eip/EIPProtocolAdapterFactory.java b/modules/hivemq-edge-module-plc4x/src/main/java/com/hivemq/edge/adapters/plc4x/types/eip/EIPProtocolAdapterFactory.java deleted file mode 100644 index bea93cf3c1..0000000000 --- a/modules/hivemq-edge-module-plc4x/src/main/java/com/hivemq/edge/adapters/plc4x/types/eip/EIPProtocolAdapterFactory.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2023-present HiveMQ GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hivemq.edge.adapters.plc4x.types.eip; - -import com.hivemq.adapter.sdk.api.ProtocolAdapter; -import com.hivemq.adapter.sdk.api.ProtocolAdapterInformation; -import com.hivemq.adapter.sdk.api.factories.ProtocolAdapterFactory; -import com.hivemq.adapter.sdk.api.model.ProtocolAdapterInput; -import org.jetbrains.annotations.NotNull; - -/** - * @author HiveMQ Adapter Generator - */ -public class EIPProtocolAdapterFactory implements ProtocolAdapterFactory { - - @Override - public @NotNull ProtocolAdapterInformation getInformation() { - return EIPProtocolAdapterInformation.INSTANCE; - } - - @Override - public @NotNull ProtocolAdapter createAdapter( - @NotNull final ProtocolAdapterInformation adapterInformation, - @NotNull final ProtocolAdapterInput input) { - return new EIPProtocolAdapter(adapterInformation, input); - } - - @Override - public @NotNull Class getConfigClass() { - return EIPAdapterConfig.class; - } - -} diff --git a/modules/hivemq-edge-module-plc4x/src/main/resources/META-INF/services/com.hivemq.adapter.sdk.api.factories.ProtocolAdapterFactory b/modules/hivemq-edge-module-plc4x/src/main/resources/META-INF/services/com.hivemq.adapter.sdk.api.factories.ProtocolAdapterFactory index 2a9c8ec07c..7035893518 100644 --- a/modules/hivemq-edge-module-plc4x/src/main/resources/META-INF/services/com.hivemq.adapter.sdk.api.factories.ProtocolAdapterFactory +++ b/modules/hivemq-edge-module-plc4x/src/main/resources/META-INF/services/com.hivemq.adapter.sdk.api.factories.ProtocolAdapterFactory @@ -1,3 +1,2 @@ com.hivemq.edge.adapters.plc4x.types.siemens.S7ProtocolAdapterFactory com.hivemq.edge.adapters.plc4x.types.ads.ADSProtocolAdapterFactory -com.hivemq.edge.adapters.plc4x.types.eip.EIPProtocolAdapterFactory diff --git a/modules/hivemq-edge-module-plc4x/src/main/resources/META-INF/services/org.apache.plc4x.java.api.PlcDriver b/modules/hivemq-edge-module-plc4x/src/main/resources/META-INF/services/org.apache.plc4x.java.api.PlcDriver index 8c6b2a3643..16dfbdaed7 100644 --- a/modules/hivemq-edge-module-plc4x/src/main/resources/META-INF/services/org.apache.plc4x.java.api.PlcDriver +++ b/modules/hivemq-edge-module-plc4x/src/main/resources/META-INF/services/org.apache.plc4x.java.api.PlcDriver @@ -1,4 +1,2 @@ org.apache.plc4x.java.ads.AdsPlcDriver org.apache.plc4x.java.s7.readwrite.S7Driver -org.apache.plc4x.java.eip.base.EIPDriver -org.apache.plc4x.java.eip.logix.LogixDriver diff --git a/modules/hivemq-edge-module-plc4x/src/main/resources/httpd/images/allenbradley-icon.png b/modules/hivemq-edge-module-plc4x/src/main/resources/httpd/images/allenbradley-icon.png deleted file mode 100644 index 905969f937..0000000000 Binary files a/modules/hivemq-edge-module-plc4x/src/main/resources/httpd/images/allenbradley-icon.png and /dev/null differ diff --git a/modules/hivemq-edge-module-plc4x/src/main/resources/httpd/images/eip-icon.png b/modules/hivemq-edge-module-plc4x/src/main/resources/httpd/images/eip-icon.png deleted file mode 100644 index 0e00b7c1dc..0000000000 Binary files a/modules/hivemq-edge-module-plc4x/src/main/resources/httpd/images/eip-icon.png and /dev/null differ diff --git a/settings.gradle.kts b/settings.gradle.kts index 9ce771f086..882715f3df 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,6 +11,7 @@ includeBuild("./hivemq-edge") // ** module-deps ** // +includeBuild("./modules/hivemq-edge-module-etherip") includeBuild("./modules/hivemq-edge-module-plc4x") includeBuild("./modules/hivemq-edge-module-http") includeBuild("./modules/hivemq-edge-module-modbus")