commit ac62b554f581f51c4b185a7f720ec1126979075f Author: litoral05 Date: Tue May 19 15:20:57 2026 +0100 Initial Spring Boot backend with Modbus TCP infrastructure diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3b41682 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7ec3c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +data/ +*.db +*.db-journal +*.log + +# Runtime telemetry dumps / future exports +*.csv + +# Tauri frontend later if nested +node_modules/ + +# OS junk +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..5291372 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.15/apache-maven-3.9.15-bin.zip diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..bd8896b --- /dev/null +++ b/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + 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" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..daf2d08 --- /dev/null +++ b/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.14 + + + com.litoralregas + backend + 0.0.1-SNAPSHOT + + + + + + + + + + + + + + + + + 21 + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-websocket + + + org.flywaydb + flyway-core + + + + org.xerial + sqlite-jdbc + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + org.hibernate.orm + hibernate-community-dialects + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/src/main/java/com/litoralregas/backend/BackendApplication.java b/src/main/java/com/litoralregas/backend/BackendApplication.java new file mode 100644 index 0000000..85634f2 --- /dev/null +++ b/src/main/java/com/litoralregas/backend/BackendApplication.java @@ -0,0 +1,15 @@ +package com.litoralregas.backend; + +import com.litoralregas.backend.modbus.ModbusConnectionProperties; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +@SpringBootApplication +@EnableConfigurationProperties(ModbusConnectionProperties.class) +public class BackendApplication { + + public static void main(String[] args) { + SpringApplication.run(BackendApplication.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/common/HealthController.java b/src/main/java/com/litoralregas/backend/common/HealthController.java new file mode 100644 index 0000000..636edbb --- /dev/null +++ b/src/main/java/com/litoralregas/backend/common/HealthController.java @@ -0,0 +1,18 @@ +package com.litoralregas.backend.common; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +public class HealthController { + + @GetMapping("/api/health") + public Map health() { + return Map.of( + "status", "UP", + "service", "backend" + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/common/api/ApiErrorResponse.java b/src/main/java/com/litoralregas/backend/common/api/ApiErrorResponse.java new file mode 100644 index 0000000..71a7ef3 --- /dev/null +++ b/src/main/java/com/litoralregas/backend/common/api/ApiErrorResponse.java @@ -0,0 +1,14 @@ +package com.litoralregas.backend.common.api; + +import java.time.Instant; +import java.util.Map; + +public record ApiErrorResponse( + Instant timestamp, + int status, + String error, + String message, + String path, + Map fieldErrors +) { +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/common/api/GlobalExceptionHandler.java b/src/main/java/com/litoralregas/backend/common/api/GlobalExceptionHandler.java new file mode 100644 index 0000000..4455bd8 --- /dev/null +++ b/src/main/java/com/litoralregas/backend/common/api/GlobalExceptionHandler.java @@ -0,0 +1,88 @@ +package com.litoralregas.backend.common.api; + +import jakarta.persistence.EntityNotFoundException; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiErrorResponse handleIllegalArgument( + IllegalArgumentException exception, + HttpServletRequest request + ) { + return new ApiErrorResponse( + Instant.now(), + HttpStatus.BAD_REQUEST.value(), + HttpStatus.BAD_REQUEST.getReasonPhrase(), + exception.getMessage(), + request.getRequestURI(), + Map.of() + ); + } + + @ExceptionHandler(EntityNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ApiErrorResponse handleEntityNotFound( + EntityNotFoundException exception, + HttpServletRequest request + ) { + return new ApiErrorResponse( + Instant.now(), + HttpStatus.NOT_FOUND.value(), + HttpStatus.NOT_FOUND.getReasonPhrase(), + exception.getMessage(), + request.getRequestURI(), + Map.of() + ); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiErrorResponse handleValidation( + MethodArgumentNotValidException exception, + HttpServletRequest request + ) { + Map fieldErrors = new LinkedHashMap<>(); + + exception.getBindingResult().getFieldErrors().forEach(error -> + fieldErrors.put(error.getField(), error.getDefaultMessage()) + ); + + return new ApiErrorResponse( + Instant.now(), + HttpStatus.BAD_REQUEST.value(), + HttpStatus.BAD_REQUEST.getReasonPhrase(), + "Request validation failed.", + request.getRequestURI(), + fieldErrors + ); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiErrorResponse handleUnreadableJson( + HttpMessageNotReadableException exception, + HttpServletRequest request + ) { + return new ApiErrorResponse( + Instant.now(), + HttpStatus.BAD_REQUEST.value(), + HttpStatus.BAD_REQUEST.getReasonPhrase(), + "Malformed JSON request body.", + request.getRequestURI(), + Map.of() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/modbus/EasyModbusLrClient.java b/src/main/java/com/litoralregas/backend/modbus/EasyModbusLrClient.java new file mode 100644 index 0000000..e99659a --- /dev/null +++ b/src/main/java/com/litoralregas/backend/modbus/EasyModbusLrClient.java @@ -0,0 +1,145 @@ +package com.litoralregas.backend.modbus; + +import de.re.easymodbus.modbusclient.ModbusClient; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.Arrays; + +@Component +public class EasyModbusLrClient implements LrModbusClient { + + private final ModbusConnectionProperties properties; + + public EasyModbusLrClient(ModbusConnectionProperties properties) { + this.properties = properties; + } + + @Override + public ModbusReadResult readInputRegisters(ModbusUnit unit, int startingAddress, int quantity) { + validateRegisterRead(startingAddress, quantity, 125); + + int[] values = executeWithRetry( + unit, + client -> client.ReadInputRegisters(startingAddress, quantity, 1, 0) + ); + + return new ModbusReadResult( + unit, + ModbusRegisterType.INPUT_REGISTER, + startingAddress, + quantity, + Arrays.stream(values).boxed().toList(), + Instant.now() + ); + } + + @Override + public ModbusReadResult readHoldingRegisters(ModbusUnit unit, int startingAddress, int quantity) { + validateRegisterRead(startingAddress, quantity, 125); + + int[] values = executeWithRetry( + unit, + client -> client.ReadHoldingRegisters(startingAddress, quantity, 1, 0) + ); + + return new ModbusReadResult( + unit, + ModbusRegisterType.HOLDING_REGISTER, + startingAddress, + quantity, + Arrays.stream(values).boxed().toList(), + Instant.now() + ); + } + + @Override + public boolean[] readCoils(ModbusUnit unit, int startingAddress, int quantity) { + validateRegisterRead(startingAddress, quantity, 2000); + + return executeWithRetry( + unit, + client -> client.ReadCoils(startingAddress, quantity, 1, 0) + ); + } + + @Override + public void writeSingleCoil(ModbusUnit unit, int startingAddress, boolean value) { + validateAddress(startingAddress); + + executeWithRetry( + unit, + client -> { + client.WriteSingleCoil(startingAddress, value); + return null; + } + ); + } + + private T executeWithRetry(ModbusUnit unit, ModbusOperation operation) { + RuntimeException lastException = null; + + for (int attempt = 1; attempt <= properties.getMaxAttempts(); attempt++) { + ModbusClient client = new ModbusClient( + properties.getHost(), + properties.getPort(), + unit.getUnitId() + ); + + try { + client.setConnectionTimeout(properties.getTimeoutMillis()); + client.Connect(); + + return operation.execute(client); + } catch (Exception exception) { + lastException = new ModbusException( + "Modbus operation failed on attempt " + attempt + " for unit " + unit + ".", + exception + ); + + sleepBeforeRetry(attempt); + } finally { + try { + client.Disconnect(); + } catch (Exception ignored) { + } + } + } + + throw lastException != null + ? lastException + : new ModbusException("Modbus operation failed."); + } + + private void sleepBeforeRetry(int attempt) { + if (attempt >= properties.getMaxAttempts()) { + return; + } + + try { + Thread.sleep(properties.getRetryDelayMillis()); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new ModbusException("Modbus retry interrupted.", exception); + } + } + + private void validateRegisterRead(int startingAddress, int quantity, int maxQuantity) { + validateAddress(startingAddress); + + if (quantity < 1 || quantity > maxQuantity) { + throw new IllegalArgumentException("Quantity must be between 1 and " + maxQuantity + "."); + } + } + + private void validateAddress(int startingAddress) { + if (startingAddress < 0 || startingAddress > 65535) { + throw new IllegalArgumentException("Starting address must be between 0 and 65535."); + } + } + + @FunctionalInterface + private interface ModbusOperation { + T execute(ModbusClient client) throws Exception; + } +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/modbus/LrModbusClient.java b/src/main/java/com/litoralregas/backend/modbus/LrModbusClient.java new file mode 100644 index 0000000..79c1011 --- /dev/null +++ b/src/main/java/com/litoralregas/backend/modbus/LrModbusClient.java @@ -0,0 +1,12 @@ +package com.litoralregas.backend.modbus; + +public interface LrModbusClient { + + ModbusReadResult readInputRegisters(ModbusUnit unit, int startingAddress, int quantity); + + ModbusReadResult readHoldingRegisters(ModbusUnit unit, int startingAddress, int quantity); + + boolean[] readCoils(ModbusUnit unit, int startingAddress, int quantity); + + void writeSingleCoil(ModbusUnit unit, int startingAddress, boolean value); +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/modbus/ModbusConnectionProperties.java b/src/main/java/com/litoralregas/backend/modbus/ModbusConnectionProperties.java new file mode 100644 index 0000000..ab881f8 --- /dev/null +++ b/src/main/java/com/litoralregas/backend/modbus/ModbusConnectionProperties.java @@ -0,0 +1,53 @@ +package com.litoralregas.backend.modbus; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "litoralregas.modbus") +public class ModbusConnectionProperties { + + private String host = "127.0.0.1"; + private int port = 533; + private int timeoutMillis = 500; + private int maxAttempts = 3; + private long retryDelayMillis = 1000; + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public int getTimeoutMillis() { + return timeoutMillis; + } + + public void setTimeoutMillis(int timeoutMillis) { + this.timeoutMillis = timeoutMillis; + } + + public int getMaxAttempts() { + return maxAttempts; + } + + public void setMaxAttempts(int maxAttempts) { + this.maxAttempts = maxAttempts; + } + + public long getRetryDelayMillis() { + return retryDelayMillis; + } + + public void setRetryDelayMillis(long retryDelayMillis) { + this.retryDelayMillis = retryDelayMillis; + } +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/modbus/ModbusException.java b/src/main/java/com/litoralregas/backend/modbus/ModbusException.java new file mode 100644 index 0000000..fd9fe74 --- /dev/null +++ b/src/main/java/com/litoralregas/backend/modbus/ModbusException.java @@ -0,0 +1,12 @@ +package com.litoralregas.backend.modbus; + +public class ModbusException extends RuntimeException { + + public ModbusException(String message) { + super(message); + } + + public ModbusException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/modbus/ModbusReadResult.java b/src/main/java/com/litoralregas/backend/modbus/ModbusReadResult.java new file mode 100644 index 0000000..481d795 --- /dev/null +++ b/src/main/java/com/litoralregas/backend/modbus/ModbusReadResult.java @@ -0,0 +1,14 @@ +package com.litoralregas.backend.modbus; + +import java.time.Instant; +import java.util.List; + +public record ModbusReadResult( + ModbusUnit unit, + ModbusRegisterType registerType, + int startingAddress, + int quantity, + List values, + Instant timestamp +) { +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/modbus/ModbusRegisterType.java b/src/main/java/com/litoralregas/backend/modbus/ModbusRegisterType.java new file mode 100644 index 0000000..6c7dde3 --- /dev/null +++ b/src/main/java/com/litoralregas/backend/modbus/ModbusRegisterType.java @@ -0,0 +1,8 @@ +package com.litoralregas.backend.modbus; + +public enum ModbusRegisterType { + COIL, + DISCRETE_INPUT, + HOLDING_REGISTER, + INPUT_REGISTER +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/modbus/ModbusTestController.java b/src/main/java/com/litoralregas/backend/modbus/ModbusTestController.java new file mode 100644 index 0000000..1a53729 --- /dev/null +++ b/src/main/java/com/litoralregas/backend/modbus/ModbusTestController.java @@ -0,0 +1,56 @@ +package com.litoralregas.backend.modbus; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/modbus") +public class ModbusTestController { + + private final LrModbusClient modbusClient; + + public ModbusTestController(LrModbusClient modbusClient) { + this.modbusClient = modbusClient; + } + + @GetMapping("/input-registers") + public ModbusReadResult readInputRegisters( + @RequestParam(defaultValue = "PC") ModbusUnit unit, + @RequestParam int address, + @RequestParam(defaultValue = "1") int quantity + ) { + return modbusClient.readInputRegisters( + unit, + address, + quantity + ); + } + + @GetMapping("/holding-registers") + public ModbusReadResult readHoldingRegisters( + @RequestParam(defaultValue = "PC") ModbusUnit unit, + @RequestParam int address, + @RequestParam(defaultValue = "1") int quantity + ) { + return modbusClient.readHoldingRegisters( + unit, + address, + quantity + ); + } + + @GetMapping("/coils") + public boolean[] readCoils( + @RequestParam(defaultValue = "PC") ModbusUnit unit, + @RequestParam int address, + @RequestParam(defaultValue = "1") int quantity + ) { + return modbusClient.readCoils( + unit, + address, + quantity + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/modbus/ModbusUnit.java b/src/main/java/com/litoralregas/backend/modbus/ModbusUnit.java new file mode 100644 index 0000000..91ecfb2 --- /dev/null +++ b/src/main/java/com/litoralregas/backend/modbus/ModbusUnit.java @@ -0,0 +1,17 @@ +package com.litoralregas.backend.modbus; + +public enum ModbusUnit { + + GENERAL((byte) 99), + PC((byte) 106); + + private final byte unitId; + + ModbusUnit(byte unitId) { + this.unitId = unitId; + } + + public byte getUnitId() { + return unitId; + } +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/sensor/SensorDefinition.java b/src/main/java/com/litoralregas/backend/sensor/SensorDefinition.java new file mode 100644 index 0000000..8032b62 --- /dev/null +++ b/src/main/java/com/litoralregas/backend/sensor/SensorDefinition.java @@ -0,0 +1,167 @@ +package com.litoralregas.backend.sensor; + +import jakarta.persistence.*; +import java.time.Instant; + +@Entity +@Table(name = "sensor_definition") +public class SensorDefinition { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(nullable = false) + private String name; + + @Column(name = "modbus_address", nullable = false) + private Integer modbusAddress; + + @Column(name = "bit_offset") + private Integer bitOffset; + + @Enumerated(EnumType.STRING) + @Column(name = "value_type", nullable = false) + private SensorValueType valueType; + + private String unit; + + @Column(name = "decimal_places", nullable = false) + private Integer decimalPlaces; + + @Column(nullable = false) + private String category; + + @Enumerated(EnumType.STRING) + @Column(name = "source_type", nullable = false) + private SensorSourceType sourceType; + + @Column(name = "polling_interval_seconds", nullable = false) + private Integer pollingIntervalSeconds; + + @Column(nullable = false) + private Boolean enabled; + + @Column(name = "created_at", nullable = false) + private Instant createdAt; + + protected SensorDefinition() { + } + + public SensorDefinition( + String name, + Integer modbusAddress, + Integer bitOffset, + SensorValueType valueType, + String unit, + Integer decimalPlaces, + String category, + SensorSourceType sourceType, + Integer pollingIntervalSeconds, + Boolean enabled + ) { + this.name = name; + this.modbusAddress = modbusAddress; + this.bitOffset = bitOffset; + this.valueType = valueType; + this.unit = unit; + this.decimalPlaces = decimalPlaces; + this.category = category; + this.sourceType = sourceType; + this.pollingIntervalSeconds = pollingIntervalSeconds; + this.enabled = enabled; + this.createdAt = Instant.now(); + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getModbusAddress() { + return modbusAddress; + } + + public void setModbusAddress(Integer modbusAddress) { + this.modbusAddress = modbusAddress; + } + + public Integer getBitOffset() { + return bitOffset; + } + + public void setBitOffset(Integer bitOffset) { + this.bitOffset = bitOffset; + } + + public SensorValueType getValueType() { + return valueType; + } + + public void setValueType(SensorValueType valueType) { + this.valueType = valueType; + } + + public String getUnit() { + return unit; + } + + public void setUnit(String unit) { + this.unit = unit; + } + + public Integer getDecimalPlaces() { + return decimalPlaces; + } + + public void setDecimalPlaces(Integer decimalPlaces) { + this.decimalPlaces = decimalPlaces; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public SensorSourceType getSourceType() { + return sourceType; + } + + public void setSourceType(SensorSourceType sourceType) { + this.sourceType = sourceType; + } + + public Integer getPollingIntervalSeconds() { + return pollingIntervalSeconds; + } + + public void setPollingIntervalSeconds(Integer pollingIntervalSeconds) { + this.pollingIntervalSeconds = pollingIntervalSeconds; + } + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/sensor/SensorDefinitionController.java b/src/main/java/com/litoralregas/backend/sensor/SensorDefinitionController.java new file mode 100644 index 0000000..d900445 --- /dev/null +++ b/src/main/java/com/litoralregas/backend/sensor/SensorDefinitionController.java @@ -0,0 +1,57 @@ +package com.litoralregas.backend.sensor; + +import com.litoralregas.backend.sensor.dto.SensorDefinitionCreateRequest; +import com.litoralregas.backend.sensor.dto.SensorDefinitionResponse; +import com.litoralregas.backend.sensor.dto.SensorDefinitionUpdateRequest; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/sensor-definitions") +public class SensorDefinitionController { + + private final SensorDefinitionService sensorDefinitionService; + + public SensorDefinitionController(SensorDefinitionService sensorDefinitionService) { + this.sensorDefinitionService = sensorDefinitionService; + } + + @GetMapping + public List findAll( + @RequestParam(name = "enabledOnly", defaultValue = "false") boolean enabledOnly + ) { + if (enabledOnly) { + return sensorDefinitionService.findEnabled(); + } + + return sensorDefinitionService.findAll(); + } + + @GetMapping("/{id}") + public SensorDefinitionResponse findById(@PathVariable Integer id) { + return sensorDefinitionService.findById(id); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public SensorDefinitionResponse create(@Valid @RequestBody SensorDefinitionCreateRequest request) { + return sensorDefinitionService.create(request); + } + + @PatchMapping("/{id}") + public SensorDefinitionResponse update( + @PathVariable Integer id, + @Valid @RequestBody SensorDefinitionUpdateRequest request + ) { + return sensorDefinitionService.update(id, request); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable Integer id) { + sensorDefinitionService.delete(id); + } +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/sensor/SensorDefinitionRepository.java b/src/main/java/com/litoralregas/backend/sensor/SensorDefinitionRepository.java new file mode 100644 index 0000000..cafdf50 --- /dev/null +++ b/src/main/java/com/litoralregas/backend/sensor/SensorDefinitionRepository.java @@ -0,0 +1,14 @@ +package com.litoralregas.backend.sensor; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface SensorDefinitionRepository extends JpaRepository { + + boolean existsByName(String name); + + List findByEnabledTrueOrderByNameAsc(); + + List findAllByOrderByNameAsc(); +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/sensor/SensorDefinitionService.java b/src/main/java/com/litoralregas/backend/sensor/SensorDefinitionService.java new file mode 100644 index 0000000..45d8d03 --- /dev/null +++ b/src/main/java/com/litoralregas/backend/sensor/SensorDefinitionService.java @@ -0,0 +1,156 @@ +package com.litoralregas.backend.sensor; + +import com.litoralregas.backend.sensor.dto.SensorDefinitionCreateRequest; +import com.litoralregas.backend.sensor.dto.SensorDefinitionResponse; +import com.litoralregas.backend.sensor.dto.SensorDefinitionUpdateRequest; +import jakarta.persistence.EntityNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.util.List; + +@Service +public class SensorDefinitionService { + + private final SensorDefinitionRepository sensorDefinitionRepository; + + public SensorDefinitionService(SensorDefinitionRepository sensorDefinitionRepository) { + this.sensorDefinitionRepository = sensorDefinitionRepository; + } + + @Transactional(readOnly = true) + public List findAll() { + return sensorDefinitionRepository.findAllByOrderByNameAsc() + .stream() + .map(SensorDefinitionResponse::fromEntity) + .toList(); + } + + @Transactional(readOnly = true) + public List findEnabled() { + return sensorDefinitionRepository.findByEnabledTrueOrderByNameAsc() + .stream() + .map(SensorDefinitionResponse::fromEntity) + .toList(); + } + + @Transactional(readOnly = true) + public SensorDefinitionResponse findById(Integer id) { + SensorDefinition sensorDefinition = getRequiredSensorDefinition(id); + return SensorDefinitionResponse.fromEntity(sensorDefinition); + } + + @Transactional + public SensorDefinitionResponse create(SensorDefinitionCreateRequest request) { + if (sensorDefinitionRepository.existsByName(request.name())) { + throw new IllegalArgumentException("A sensor definition with this name already exists."); + } + + validateBitOffset(request.valueType(), request.bitOffset()); + + SensorDefinition sensorDefinition = new SensorDefinition(); + sensorDefinition.setName(request.name().trim()); + sensorDefinition.setModbusAddress(request.modbusAddress()); + sensorDefinition.setBitOffset(request.bitOffset()); + sensorDefinition.setValueType(request.valueType()); + sensorDefinition.setUnit(normalizeNullableText(request.unit())); + sensorDefinition.setDecimalPlaces(request.decimalPlaces()); + sensorDefinition.setCategory(request.category().trim()); + sensorDefinition.setSourceType(request.sourceType()); + sensorDefinition.setPollingIntervalSeconds(request.pollingIntervalSeconds()); + sensorDefinition.setEnabled(request.enabled()); + sensorDefinition.setCreatedAt(Instant.now()); + + SensorDefinition saved = sensorDefinitionRepository.save(sensorDefinition); + return SensorDefinitionResponse.fromEntity(saved); + } + + @Transactional + public SensorDefinitionResponse update(Integer id, SensorDefinitionUpdateRequest request) { + SensorDefinition sensorDefinition = getRequiredSensorDefinition(id); + + if (request.name() != null) { + String normalizedName = request.name().trim(); + + if (!normalizedName.equals(sensorDefinition.getName()) + && sensorDefinitionRepository.existsByName(normalizedName)) { + throw new IllegalArgumentException("A sensor definition with this name already exists."); + } + + sensorDefinition.setName(normalizedName); + } + + if (request.modbusAddress() != null) { + sensorDefinition.setModbusAddress(request.modbusAddress()); + } + + if (request.bitOffset() != null || request.valueType() != null) { + SensorValueType effectiveValueType = + request.valueType() != null ? request.valueType() : sensorDefinition.getValueType(); + + Integer effectiveBitOffset = + request.bitOffset() != null ? request.bitOffset() : sensorDefinition.getBitOffset(); + + validateBitOffset(effectiveValueType, effectiveBitOffset); + + sensorDefinition.setValueType(effectiveValueType); + sensorDefinition.setBitOffset(effectiveBitOffset); + } + + if (request.unit() != null) { + sensorDefinition.setUnit(normalizeNullableText(request.unit())); + } + + if (request.decimalPlaces() != null) { + sensorDefinition.setDecimalPlaces(request.decimalPlaces()); + } + + if (request.category() != null) { + sensorDefinition.setCategory(request.category().trim()); + } + + if (request.sourceType() != null) { + sensorDefinition.setSourceType(request.sourceType()); + } + + if (request.pollingIntervalSeconds() != null) { + sensorDefinition.setPollingIntervalSeconds(request.pollingIntervalSeconds()); + } + + if (request.enabled() != null) { + sensorDefinition.setEnabled(request.enabled()); + } + + return SensorDefinitionResponse.fromEntity(sensorDefinition); + } + + @Transactional + public void delete(Integer id) { + SensorDefinition sensorDefinition = getRequiredSensorDefinition(id); + sensorDefinitionRepository.delete(sensorDefinition); + } + + private SensorDefinition getRequiredSensorDefinition(Integer id) { + return sensorDefinitionRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Sensor definition not found: " + id)); + } + + private void validateBitOffset(SensorValueType valueType, Integer bitOffset) { + if (valueType == SensorValueType.BOOLEAN && bitOffset == null) { + throw new IllegalArgumentException("Boolean sensor definitions require bitOffset."); + } + + if (valueType != SensorValueType.BOOLEAN && bitOffset != null) { + throw new IllegalArgumentException("Only boolean sensor definitions may use bitOffset."); + } + } + + private String normalizeNullableText(String value) { + if (value == null || value.isBlank()) { + return null; + } + + return value.trim(); + } +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/sensor/SensorSourceType.java b/src/main/java/com/litoralregas/backend/sensor/SensorSourceType.java new file mode 100644 index 0000000..5cc798b --- /dev/null +++ b/src/main/java/com/litoralregas/backend/sensor/SensorSourceType.java @@ -0,0 +1,7 @@ +package com.litoralregas.backend.sensor; + +public enum SensorSourceType { + MODBUS, + CALCULATED, + MANUAL +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/sensor/SensorValueType.java b/src/main/java/com/litoralregas/backend/sensor/SensorValueType.java new file mode 100644 index 0000000..77c07a7 --- /dev/null +++ b/src/main/java/com/litoralregas/backend/sensor/SensorValueType.java @@ -0,0 +1,7 @@ +package com.litoralregas.backend.sensor; + +public enum SensorValueType { + BOOLEAN, + INTEGER, + DECIMAL +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/sensor/dto/SensorDefinitionCreateRequest.java b/src/main/java/com/litoralregas/backend/sensor/dto/SensorDefinitionCreateRequest.java new file mode 100644 index 0000000..e6a324c --- /dev/null +++ b/src/main/java/com/litoralregas/backend/sensor/dto/SensorDefinitionCreateRequest.java @@ -0,0 +1,50 @@ +package com.litoralregas.backend.sensor.dto; + +import com.litoralregas.backend.sensor.SensorSourceType; +import com.litoralregas.backend.sensor.SensorValueType; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record SensorDefinitionCreateRequest( + + @NotBlank + @Size(max = 255) + String name, + + @NotNull + @Min(0) + Integer modbusAddress, + + @Min(0) + @Max(15) + Integer bitOffset, + + @NotNull + SensorValueType valueType, + + @Size(max = 50) + String unit, + + @NotNull + @Min(0) + @Max(6) + Integer decimalPlaces, + + @NotBlank + @Size(max = 100) + String category, + + @NotNull + SensorSourceType sourceType, + + @NotNull + @Min(1) + Integer pollingIntervalSeconds, + + @NotNull + Boolean enabled +) { +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/sensor/dto/SensorDefinitionResponse.java b/src/main/java/com/litoralregas/backend/sensor/dto/SensorDefinitionResponse.java new file mode 100644 index 0000000..afdceff --- /dev/null +++ b/src/main/java/com/litoralregas/backend/sensor/dto/SensorDefinitionResponse.java @@ -0,0 +1,40 @@ +package com.litoralregas.backend.sensor.dto; + +import com.litoralregas.backend.sensor.SensorDefinition; +import com.litoralregas.backend.sensor.SensorSourceType; +import com.litoralregas.backend.sensor.SensorValueType; + +import java.time.Instant; + +public record SensorDefinitionResponse( + Integer id, + String name, + Integer modbusAddress, + Integer bitOffset, + SensorValueType valueType, + String unit, + Integer decimalPlaces, + String category, + SensorSourceType sourceType, + Integer pollingIntervalSeconds, + Boolean enabled, + Instant createdAt +) { + + public static SensorDefinitionResponse fromEntity(SensorDefinition sensorDefinition) { + return new SensorDefinitionResponse( + sensorDefinition.getId(), + sensorDefinition.getName(), + sensorDefinition.getModbusAddress(), + sensorDefinition.getBitOffset(), + sensorDefinition.getValueType(), + sensorDefinition.getUnit(), + sensorDefinition.getDecimalPlaces(), + sensorDefinition.getCategory(), + sensorDefinition.getSourceType(), + sensorDefinition.getPollingIntervalSeconds(), + sensorDefinition.getEnabled(), + sensorDefinition.getCreatedAt() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/sensor/dto/SensorDefinitionUpdateRequest.java b/src/main/java/com/litoralregas/backend/sensor/dto/SensorDefinitionUpdateRequest.java new file mode 100644 index 0000000..ce97076 --- /dev/null +++ b/src/main/java/com/litoralregas/backend/sensor/dto/SensorDefinitionUpdateRequest.java @@ -0,0 +1,40 @@ +package com.litoralregas.backend.sensor.dto; + +import com.litoralregas.backend.sensor.SensorSourceType; +import com.litoralregas.backend.sensor.SensorValueType; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Size; + +public record SensorDefinitionUpdateRequest( + + @Size(max = 255) + String name, + + @Min(0) + Integer modbusAddress, + + @Min(0) + @Max(15) + Integer bitOffset, + + SensorValueType valueType, + + @Size(max = 50) + String unit, + + @Min(0) + @Max(6) + Integer decimalPlaces, + + @Size(max = 100) + String category, + + SensorSourceType sourceType, + + @Min(1) + Integer pollingIntervalSeconds, + + Boolean enabled +) { +} \ No newline at end of file diff --git a/src/main/java/de/re/easymodbus/exceptions/CRCCheckFailedException.java b/src/main/java/de/re/easymodbus/exceptions/CRCCheckFailedException.java new file mode 100644 index 0000000..e1b6056 --- /dev/null +++ b/src/main/java/de/re/easymodbus/exceptions/CRCCheckFailedException.java @@ -0,0 +1,30 @@ +/* */ package de.re.easymodbus.exceptions; +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ public class CRCCheckFailedException +/* */ extends ModbusException +/* */ { +/* */ public CRCCheckFailedException() {} +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ public CRCCheckFailedException(String s) +/* */ { +/* 22 */ super(s); +/* */ } +/* */ } + + +/* Location: C:\Users\Admin\Desktop\JAVA DEV\EasyModbusJava.jar!\de\re\easymodbus\exceptions\CRCCheckFailedException.class + * Java compiler version: 8 (52.0) + * JD-Core Version: 0.7.1 + */ \ No newline at end of file diff --git a/src/main/java/de/re/easymodbus/exceptions/ConnectionException.java b/src/main/java/de/re/easymodbus/exceptions/ConnectionException.java new file mode 100644 index 0000000..49e6889 --- /dev/null +++ b/src/main/java/de/re/easymodbus/exceptions/ConnectionException.java @@ -0,0 +1,30 @@ +/* */ package de.re.easymodbus.exceptions; +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ public class ConnectionException +/* */ extends ModbusException +/* */ { +/* */ public ConnectionException() {} +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ public ConnectionException(String s) +/* */ { +/* 22 */ super(s); +/* */ } +/* */ } + + +/* Location: C:\Users\Admin\Desktop\JAVA DEV\EasyModbusJava.jar!\de\re\easymodbus\exceptions\ConnectionException.class + * Java compiler version: 8 (52.0) + * JD-Core Version: 0.7.1 + */ \ No newline at end of file diff --git a/src/main/java/de/re/easymodbus/exceptions/FunctionCodeNotSupportedException.java b/src/main/java/de/re/easymodbus/exceptions/FunctionCodeNotSupportedException.java new file mode 100644 index 0000000..bd070db --- /dev/null +++ b/src/main/java/de/re/easymodbus/exceptions/FunctionCodeNotSupportedException.java @@ -0,0 +1,30 @@ +/* */ package de.re.easymodbus.exceptions; +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ public class FunctionCodeNotSupportedException +/* */ extends ModbusException +/* */ { +/* */ public FunctionCodeNotSupportedException() {} +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ public FunctionCodeNotSupportedException(String s) +/* */ { +/* 22 */ super(s); +/* */ } +/* */ } + + +/* Location: C:\Users\Admin\Desktop\JAVA DEV\EasyModbusJava.jar!\de\re\easymodbus\exceptions\FunctionCodeNotSupportedException.class + * Java compiler version: 8 (52.0) + * JD-Core Version: 0.7.1 + */ \ No newline at end of file diff --git a/src/main/java/de/re/easymodbus/exceptions/ModbusException.java b/src/main/java/de/re/easymodbus/exceptions/ModbusException.java new file mode 100644 index 0000000..efc3843 --- /dev/null +++ b/src/main/java/de/re/easymodbus/exceptions/ModbusException.java @@ -0,0 +1,30 @@ +/* */ package de.re.easymodbus.exceptions; +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ public class ModbusException +/* */ extends Exception +/* */ { +/* */ public ModbusException() {} +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ public ModbusException(String s) +/* */ { +/* 22 */ super(s); +/* */ } +/* */ } + + +/* Location: C:\Users\Admin\Desktop\JAVA DEV\EasyModbusJava.jar!\de\re\easymodbus\exceptions\ModbusException.class + * Java compiler version: 8 (52.0) + * JD-Core Version: 0.7.1 + */ \ No newline at end of file diff --git a/src/main/java/de/re/easymodbus/exceptions/QuantityInvalidException.java b/src/main/java/de/re/easymodbus/exceptions/QuantityInvalidException.java new file mode 100644 index 0000000..d244616 --- /dev/null +++ b/src/main/java/de/re/easymodbus/exceptions/QuantityInvalidException.java @@ -0,0 +1,30 @@ +/* */ package de.re.easymodbus.exceptions; +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ public class QuantityInvalidException +/* */ extends ModbusException +/* */ { +/* */ public QuantityInvalidException() {} +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ public QuantityInvalidException(String s) +/* */ { +/* 22 */ super(s); +/* */ } +/* */ } + + +/* Location: C:\Users\Admin\Desktop\JAVA DEV\EasyModbusJava.jar!\de\re\easymodbus\exceptions\QuantityInvalidException.class + * Java compiler version: 8 (52.0) + * JD-Core Version: 0.7.1 + */ \ No newline at end of file diff --git a/src/main/java/de/re/easymodbus/exceptions/StartingAddressInvalidException.java b/src/main/java/de/re/easymodbus/exceptions/StartingAddressInvalidException.java new file mode 100644 index 0000000..2875a18 --- /dev/null +++ b/src/main/java/de/re/easymodbus/exceptions/StartingAddressInvalidException.java @@ -0,0 +1,30 @@ +/* */ package de.re.easymodbus.exceptions; +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ public class StartingAddressInvalidException +/* */ extends ModbusException +/* */ { +/* */ public StartingAddressInvalidException() {} +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ public StartingAddressInvalidException(String s) +/* */ { +/* 22 */ super(s); +/* */ } +/* */ } + + +/* Location: C:\Users\Admin\Desktop\JAVA DEV\EasyModbusJava.jar!\de\re\easymodbus\exceptions\StartingAddressInvalidException.class + * Java compiler version: 8 (52.0) + * JD-Core Version: 0.7.1 + */ \ No newline at end of file diff --git a/src/main/java/de/re/easymodbus/modbusclient/DateTime.java b/src/main/java/de/re/easymodbus/modbusclient/DateTime.java new file mode 100644 index 0000000..f56c6a4 --- /dev/null +++ b/src/main/java/de/re/easymodbus/modbusclient/DateTime.java @@ -0,0 +1,39 @@ +/* */ package de.re.easymodbus.modbusclient; +/* */ +/* */ import java.text.DateFormat; +/* */ import java.text.SimpleDateFormat; +/* */ import java.util.Calendar; +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ public class DateTime +/* */ { +/* */ protected static long getDateTimeTicks() +/* */ { +/* 18 */ long TICKS_AT_EPOCH = 621355968000000000L; +/* 19 */ long tick = System.currentTimeMillis() * 10000L + TICKS_AT_EPOCH; +/* 20 */ return tick; +/* */ } +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ protected static String getDateTimeString() +/* */ { +/* 29 */ DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); +/* 30 */ Calendar cal = Calendar.getInstance(); +/* 31 */ return dateFormat.format(cal.getTime()); +/* */ } +/* */ } + + +/* Location: C:\Users\Admin\Desktop\JAVA DEV\EasyModbusJava.jar!\de\re\easymodbus\modbusclient\DateTime.class + * Java compiler version: 8 (52.0) + * JD-Core Version: 0.7.1 + */ \ No newline at end of file diff --git a/src/main/java/de/re/easymodbus/modbusclient/ModbusClient.java b/src/main/java/de/re/easymodbus/modbusclient/ModbusClient.java new file mode 100644 index 0000000..d3eefa5 --- /dev/null +++ b/src/main/java/de/re/easymodbus/modbusclient/ModbusClient.java @@ -0,0 +1,1058 @@ +package de.re.easymodbus.modbusclient; + +import de.re.easymodbus.exceptions.*; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.*; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +public class ModbusClient { + private enum RegisterOrder { + LowHigh, HighLow; + } + private Socket tcpClientSocket = new Socket(); + private String ipAddress; + private int port; + private byte unitIdentifier; + private boolean udpFlag = false; + private int connectTimeout = 500; + private int NTENTATIVAS = 3; + private InputStream inStream; + private DataOutputStream outStream; + private byte[] receiveData; + private byte[] sendData; + private List receiveDataChangedListener = new ArrayList(); + private List sendDataChangedListener = new ArrayList(); + public ModbusClient(String ipAddress, int port, byte unitIdentifier) { + this.ipAddress = ipAddress; + this.port = port; + this.unitIdentifier = unitIdentifier; + } + + public void Connect() throws IOException { + if (!this.udpFlag) { + this.tcpClientSocket = new Socket(this.ipAddress, this.port); + this.tcpClientSocket.setSoTimeout(this.connectTimeout); + this.outStream = new DataOutputStream(this.tcpClientSocket.getOutputStream()); + this.inStream = this.tcpClientSocket.getInputStream(); + } + } + public static float ConvertRegistersToFloat(int[] registers) throws IllegalArgumentException { + if (registers.length != 2) + throw new IllegalArgumentException("Input Array length invalid"); + int highRegister = registers[1]; + int lowRegister = registers[0]; + byte[] highRegisterBytes = toByteArray(highRegister); + byte[] lowRegisterBytes = toByteArray(lowRegister); + byte[] floatBytes = { + highRegisterBytes[1], + highRegisterBytes[0], + lowRegisterBytes[1], + lowRegisterBytes[0]}; + + return ByteBuffer.wrap(floatBytes).getFloat(); + } + + public static double ConvertRegistersToDoublePrecisionFloat(int[] registers) throws IllegalArgumentException { + if (registers.length != 4) + throw new IllegalArgumentException("Input Array length invalid"); + byte[] highRegisterBytes = toByteArray(registers[3]); + byte[] highLowRegisterBytes = toByteArray(registers[2]); + byte[] lowHighRegisterBytes = toByteArray(registers[1]); + byte[] lowRegisterBytes = toByteArray(registers[0]); + byte[] doubleBytes = { + highRegisterBytes[1], + highRegisterBytes[0], + highLowRegisterBytes[1], + highLowRegisterBytes[0], + lowHighRegisterBytes[1], + lowHighRegisterBytes[0], + lowRegisterBytes[1], + lowRegisterBytes[0]}; + + return ByteBuffer.wrap(doubleBytes).getDouble(); + } + + public static double ConvertRegistersToDoublePrecisionFloat(int[] registers, RegisterOrder registerOrder) throws IllegalArgumentException { + if (registers.length != 4) + throw new IllegalArgumentException("Input Array length invalid"); + int[] swappedRegisters = {registers[0], registers[1], registers[2], registers[3]}; + if (registerOrder == RegisterOrder.HighLow) + swappedRegisters = new int[]{registers[3], registers[2], registers[1], registers[0]}; + return ConvertRegistersToDoublePrecisionFloat(swappedRegisters); + } + + public static float ConvertRegistersToFloat(int[] registers, RegisterOrder registerOrder) throws IllegalArgumentException { + int[] swappedRegisters = {registers[0], registers[1]}; + if (registerOrder == RegisterOrder.HighLow) + swappedRegisters = new int[]{registers[1], registers[0]}; + return ConvertRegistersToFloat(swappedRegisters); + } + + public static long ConvertRegistersToLong(int[] registers) throws IllegalArgumentException { + if (registers.length != 4) + throw new IllegalArgumentException("Input Array length invalid"); + byte[] highRegisterBytes = toByteArray(registers[3]); + byte[] highLowRegisterBytes = toByteArray(registers[2]); + byte[] lowHighRegisterBytes = toByteArray(registers[1]); + byte[] lowRegisterBytes = toByteArray(registers[0]); + byte[] longBytes = { + highRegisterBytes[1], + highRegisterBytes[0], + highLowRegisterBytes[1], + highLowRegisterBytes[0], + lowHighRegisterBytes[1], + lowHighRegisterBytes[0], + lowRegisterBytes[1], + lowRegisterBytes[0]}; + + return ByteBuffer.wrap(longBytes).getLong(); + } + + public static long ConvertRegistersToLong(int[] registers, RegisterOrder registerOrder) throws IllegalArgumentException { + if (registers.length != 4) + throw new IllegalArgumentException("Input Array length invalid"); + int[] swappedRegisters = {registers[0], registers[1], registers[2], registers[3]}; + if (registerOrder == RegisterOrder.HighLow) + swappedRegisters = new int[]{registers[3], registers[2], registers[1], registers[0]}; + return ConvertRegistersToLong(swappedRegisters); + } + + public static int ConvertRegistersToDouble(int[] registers) throws IllegalArgumentException { + if (registers.length != 2) + throw new IllegalArgumentException("Input Array length invalid"); + int highRegister = registers[1]; + int lowRegister = registers[0]; + byte[] highRegisterBytes = toByteArray(highRegister); + byte[] lowRegisterBytes = toByteArray(lowRegister); + byte[] doubleBytes = { + highRegisterBytes[1], + highRegisterBytes[0], + lowRegisterBytes[1], + lowRegisterBytes[0]}; + + return ByteBuffer.wrap(doubleBytes).getInt(); + } + + public static int ConvertRegistersToDouble(int[] registers, RegisterOrder registerOrder) throws IllegalArgumentException { + int[] swappedRegisters = {registers[0], registers[1]}; + if (registerOrder == RegisterOrder.HighLow) + swappedRegisters = new int[]{registers[1], registers[0]}; + return ConvertRegistersToDouble(swappedRegisters); + } + + public static int[] ConvertFloatToTwoRegisters(float floatValue) { + byte[] floatBytes = toByteArray(floatValue); + byte[] highRegisterBytes = {0, 0, floatBytes[0], floatBytes[1]}; + byte[] lowRegisterBytes = {0, 0, floatBytes[2], floatBytes[3]}; + int[] returnValue = {ByteBuffer.wrap(lowRegisterBytes).getInt(), ByteBuffer.wrap(highRegisterBytes).getInt()}; + + return returnValue; + } + + public static int[] ConvertFloatToTwoRegisters(float floatValue, RegisterOrder registerOrder) { + int[] registerValues = ConvertFloatToTwoRegisters(floatValue); + int[] returnValue = registerValues; + if (registerOrder == RegisterOrder.HighLow) + returnValue = new int[]{registerValues[1], registerValues[0]}; + return returnValue; + } + + public static int[] ConvertDoubleToTwoRegisters(int doubleValue) { + byte[] doubleBytes = toByteArrayDouble(doubleValue); + byte[] highRegisterBytes = {0, 0, doubleBytes[0], doubleBytes[1]}; + byte[] lowRegisterBytes = {0, 0, doubleBytes[2], doubleBytes[3]}; + int[] returnValue = {ByteBuffer.wrap(lowRegisterBytes).getInt(), ByteBuffer.wrap(highRegisterBytes).getInt()}; + + return returnValue; + } + + public static int[] ConvertDoubleToTwoRegisters(int doubleValue, RegisterOrder registerOrder) { + int[] registerValues = ConvertFloatToTwoRegisters(doubleValue); + int[] returnValue = registerValues; + if (registerOrder == RegisterOrder.HighLow) + returnValue = new int[]{registerValues[1], registerValues[0]}; + return returnValue; + } + + public static String ConvertRegistersToString(int[] registers, int offset, int stringLength) { + byte[] result = new byte[stringLength]; + byte[] registerResult = new byte[2]; + + for (int i = 0; i < stringLength / 2; i++) { + registerResult = toByteArray(registers[(offset + i)]); + result[(i * 2)] = registerResult[0]; + result[(i * 2 + 1)] = registerResult[1]; + } + return new String(result); + } + + public static int[] ConvertStringToRegisters(String stringToConvert) { + byte[] array = stringToConvert.getBytes(); + int[] returnarray = new int[stringToConvert.length() / 2 + stringToConvert.length() % 2]; + for (int i = 0; i < returnarray.length; i++) { + returnarray[i] = array[(i * 2)]; + if (i * 2 + 1 < array.length) { + returnarray[i] |= array[(i * 2 + 1)] << 8; + } + } + return returnarray; + } + + public boolean[] ReadDiscreteInputs(int startingAddress, int quantity) throws ModbusException, UnknownHostException, SocketException, IOException { + if (this.tcpClientSocket == null) + throw new ConnectionException("connection Error"); + if (((startingAddress > 65535 ? 1 : 0) | (quantity > 2000 ? 1 : 0)) != 0) + throw new IllegalArgumentException("Starting adress must be 0 - 65535; quantity must be 0 - 2000"); + boolean[] response = null; + byte transactionIdentifier[] = toByteArray(1); + byte protocolIdentifier[] = toByteArray(0); + byte length[] = toByteArray(6); + byte functionCode = 2; + byte startingAddressIn[] = toByteArray(startingAddress); + byte quantityIn[] = toByteArray(quantity); + byte crc[] = new byte[2]; + byte[] data = { + transactionIdentifier[1], + transactionIdentifier[0], + protocolIdentifier[1], + protocolIdentifier[0], + length[1], + length[0], + this.unitIdentifier, + functionCode, + startingAddressIn[1], + startingAddressIn[0], + quantityIn[1], + quantityIn[0], + crc[0], + crc[1]}; + + if ((this.tcpClientSocket.isConnected() | this.udpFlag)) { + DatagramPacket sendPacket; + DatagramSocket clientSocket; + if (this.udpFlag) { + InetAddress ipAddress = InetAddress.getByName(this.ipAddress); + sendPacket = new DatagramPacket(data, data.length - 2, ipAddress, this.port); + clientSocket = new DatagramSocket(); + clientSocket.setSoTimeout(500); + clientSocket.send(sendPacket); + data = new byte['࠴']; + DatagramPacket receivePacket = new DatagramPacket(data, data.length); + clientSocket.receive(receivePacket); + clientSocket.close(); + data = receivePacket.getData(); + } else { + this.outStream.write(data, 0, data.length - 2); + if (this.sendDataChangedListener.size() > 0) { + this.sendData = new byte[data.length - 2]; + System.arraycopy(data, 0, this.sendData, 0, data.length - 2); + for (SendDataChangedListener hl : this.sendDataChangedListener) + hl.SendDataChanged(); + } + data = new byte['࠴']; + int numberOfBytes = this.inStream.read(data, 0, data.length); + if (this.receiveDataChangedListener.size() > 0) { + this.receiveData = new byte[numberOfBytes]; + System.arraycopy(data, 0, this.receiveData, 0, numberOfBytes); + for (ReceiveDataChangedListener hl : this.receiveDataChangedListener) + hl.ReceiveDataChanged(); + } + } + } + if ((((data[7] & 0xFF) == 130 ? 1 : 0) & (data[8] == 1 ? 1 : 0)) != 0) + throw new FunctionCodeNotSupportedException("Function code not supported by master"); + if ((((data[7] & 0xFF) == 130 ? 1 : 0) & (data[8] == 2 ? 1 : 0)) != 0) + throw new StartingAddressInvalidException("Starting adress invalid or starting adress + quantity invalid"); + if ((((data[7] & 0xFF) == 130 ? 1 : 0) & (data[8] == 3 ? 1 : 0)) != 0) + throw new QuantityInvalidException("Quantity invalid"); + if ((((data[7] & 0xFF) == 130 ? 1 : 0) & (data[8] == 4 ? 1 : 0)) != 0) + throw new ModbusException("Error reading"); + response = new boolean[quantity]; + for (int i = 0; i < quantity; i++) { + int intData = data[(9 + i / 8)]; + int mask = (int) Math.pow(2.0D, i % 8); + intData = (intData & mask) / mask; + if (intData > 0) { + response[i] = true; + } else { + response[i] = false; + } + } + + return response; + } + + public boolean[] ReadCoils(int startingAddress, int quantity, int nTentativa, int posicao) throws IllegalArgumentException { + if(nTentativa > 1) System.out.println("Tentativa " + nTentativa + ":Pos " + posicao); + if(nTentativa > NTENTATIVAS) { + System.out.println("Maximo de tentativas excedido"); + throw new IllegalArgumentException("Maximo de tentativas excedido"); + } + + if (this.tcpClientSocket == null) throw new IllegalArgumentException("Socket nulo. Conectado?"); + if (NTENTATIVAS < 1 || nTentativa < 1 || startingAddress > 65535 || quantity > 2000) throw new IllegalArgumentException("Parametros invalidos"); + + boolean[] response = new boolean[quantity]; + byte transactionIdentifier[] = toByteArray(1); + byte protocolIdentifier[] = toByteArray(0); + byte length[] = toByteArray(6); + + byte functionCode = 1; + byte startingAddressIn[] = toByteArray(startingAddress); + byte quantityIn[] = toByteArray(quantity); + byte crc[] = new byte[2]; + byte[] data = { + transactionIdentifier[1], + transactionIdentifier[0], + protocolIdentifier[1], + protocolIdentifier[0], + length[1], + length[0], + this.unitIdentifier, + functionCode, + startingAddressIn[1], + startingAddressIn[0], + quantityIn[1], + quantityIn[0], + crc[0], + crc[1]}; + + try { + if ((this.tcpClientSocket.isConnected() | this.udpFlag)) { + DatagramPacket sendPacket; + DatagramSocket clientSocket; + if (this.udpFlag) { + InetAddress ipAddress = InetAddress.getByName(this.ipAddress); + sendPacket = new DatagramPacket(data, data.length, ipAddress, this.port); + clientSocket = new DatagramSocket(); + clientSocket.setSoTimeout(500); + clientSocket.send(sendPacket); + data = new byte['࠴']; + DatagramPacket receivePacket = new DatagramPacket(data, data.length); + clientSocket.receive(receivePacket); + clientSocket.close(); + data = receivePacket.getData(); + } else { + this.outStream.write(data, 0, data.length - 2); + if (this.sendDataChangedListener.size() > 0) { + this.sendData = new byte[data.length - 2]; + System.arraycopy(data, 0, this.sendData, 0, data.length - 2); + for (SendDataChangedListener hl : this.sendDataChangedListener) + hl.SendDataChanged(); + } + data = new byte['࠴']; + int numberOfBytes = this.inStream.read(data, 0, data.length); + if (this.receiveDataChangedListener.size() > 0) { + this.receiveData = new byte[numberOfBytes]; + System.arraycopy(data, 0, this.receiveData, 0, numberOfBytes); + for (ReceiveDataChangedListener hl : this.receiveDataChangedListener) + hl.ReceiveDataChanged(); + } + } + } + if ((((data[7] & 0xFF) == 129 ? 1 : 0) & (data[8] == 1 ? 1 : 0)) != 0) + throw new FunctionCodeNotSupportedException("Function code not supported by master"); + if ((((data[7] & 0xFF) == 129 ? 1 : 0) & (data[8] == 2 ? 1 : 0)) != 0) + throw new StartingAddressInvalidException("Starting adress invalid or starting adress + quantity invalid"); + if ((((data[7] & 0xFF) == 129 ? 1 : 0) & (data[8] == 3 ? 1 : 0)) != 0) + throw new QuantityInvalidException("Quantity invalid"); + if ((((data[7] & 0xFF) == 129 ? 1 : 0) & (data[8] == 4 ? 1 : 0)) != 0) + throw new ModbusException("Error reading"); + } catch (Exception e){ + try { + Thread.sleep(1000); + }catch (Exception ign){} + return ReadCoils(startingAddress, quantity, ++nTentativa, posicao); + } + + for (int i = 0; i < quantity; i++) { + int intData = data[(9 + i / 8)]; + int mask = (int) Math.pow(2.0D, i % 8); + intData = (intData & mask) / mask; + if (intData > 0) { + response[i] = true; + } else { + response[i] = false; + } + } + + if(nTentativa > 1) System.out.println("Tentativa " + nTentativa + " OK:Pos " + posicao); + return response; + } + + public int[] ReadHoldingRegisters(int startingAddress, int quantity, int nTentativa, int posicao) throws IllegalArgumentException { + if(nTentativa > 1) System.out.println("Tentativa " + nTentativa + ":Pos " + posicao); + if(nTentativa > NTENTATIVAS) { + System.out.println("Maximo de tentativas excedido"); + throw new IllegalArgumentException("Maximo de tentativas excedido"); + } + + if (this.tcpClientSocket == null) throw new IllegalArgumentException("Socket nulo. Conectado?"); + if (NTENTATIVAS < 1 || nTentativa < 1 || startingAddress > 65535 || quantity > 125) throw new IllegalArgumentException("Parametros invalidos"); + + int[] response = new int[quantity]; + byte transactionIdentifier[] = toByteArray(1); + byte protocolIdentifier[] = toByteArray(0); + byte length[] = toByteArray(6); + + byte functionCode = 3; + byte startingAddressIn[] = toByteArray(startingAddress); + byte quantityIn[] = toByteArray(quantity); + byte crc[] = new byte[2]; + byte[] data = { + transactionIdentifier[1], + transactionIdentifier[0], + protocolIdentifier[1], + protocolIdentifier[0], + length[1], + length[0], + this.unitIdentifier, + functionCode, + startingAddressIn[1], + startingAddressIn[0], + quantityIn[1], + quantityIn[0], + crc[0], + crc[1]}; + + try { + if ((this.tcpClientSocket.isConnected() | this.udpFlag)) { + DatagramPacket sendPacket; + DatagramSocket clientSocket; + if (this.udpFlag) { + InetAddress ipAddress = InetAddress.getByName(this.ipAddress); + sendPacket = new DatagramPacket(data, data.length, ipAddress, this.port); + clientSocket = new DatagramSocket(); + clientSocket.setSoTimeout(500); + clientSocket.send(sendPacket); + data = new byte['࠴']; + DatagramPacket receivePacket = new DatagramPacket(data, data.length); + clientSocket.receive(receivePacket); + clientSocket.close(); + data = receivePacket.getData(); + } else { + this.outStream.write(data, 0, data.length - 2); + if (this.sendDataChangedListener.size() > 0) { + this.sendData = new byte[data.length - 2]; + System.arraycopy(data, 0, this.sendData, 0, data.length - 2); + for (SendDataChangedListener hl : this.sendDataChangedListener) + hl.SendDataChanged(); + } + data = new byte['࠴']; + int numberOfBytes = this.inStream.read(data, 0, data.length); + if (this.receiveDataChangedListener.size() > 0) { + this.receiveData = new byte[numberOfBytes]; + System.arraycopy(data, 0, this.receiveData, 0, numberOfBytes); + for (ReceiveDataChangedListener hl : this.receiveDataChangedListener) + hl.ReceiveDataChanged(); + } + } + } + if (((data[7] == 131 ? 1 : 0) & (data[8] == 1 ? 1 : 0)) != 0) + throw new FunctionCodeNotSupportedException("Function code not supported by master"); + if (((data[7] == 131 ? 1 : 0) & (data[8] == 2 ? 1 : 0)) != 0) + throw new StartingAddressInvalidException("Starting adress invalid or starting adress + quantity invalid"); + if (((data[7] == 131 ? 1 : 0) & (data[8] == 3 ? 1 : 0)) != 0) + throw new QuantityInvalidException("Quantity invalid"); + if (((data[7] == 131 ? 1 : 0) & (data[8] == 4 ? 1 : 0)) != 0) + throw new ModbusException("Error reading"); + } catch (Exception e){ + try { + Thread.sleep(1000); + } catch (Exception ign){} + + return ReadHoldingRegisters(startingAddress, quantity, ++nTentativa, posicao); + } + + for (int i = 0; i < quantity; i++) { + byte[] bytes = new byte[2]; + bytes[0] = data[(9 + i * 2)]; + bytes[1] = data[(9 + i * 2 + 1)]; + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + + response[i] = byteBuffer.getShort(); + } + + if(nTentativa > 1) System.out.println("Tentativa " + nTentativa + " OK:Pos " + posicao); + return response; + } + + public int[] ReadInputRegisters(int startingAddress, int quantity, int nTentativa, int posicao) throws IllegalArgumentException { + if(nTentativa > 1) System.out.println("Tentativa " + nTentativa + ":Pos " + posicao); + if(nTentativa > NTENTATIVAS) { + System.out.println("Maximo de tentativas excedido"); + throw new IllegalArgumentException("Maximo de tentativas excedido"); + } + + if (this.tcpClientSocket == null) throw new IllegalArgumentException("Socket nulo. Conectado?"); + if (NTENTATIVAS < 1 || nTentativa < 1 || startingAddress > 65535 || quantity > 125) throw new IllegalArgumentException("Parametros invalidos"); + + int[] response = new int[quantity]; + byte transactionIdentifier[] = toByteArray(1); + byte protocolIdentifier[] = toByteArray(0); + byte length[] = toByteArray(6); + + byte functionCode = 4; + byte startingAddressIn[] = toByteArray(startingAddress); + byte quantityIn[] = toByteArray(quantity); + byte crc[] = new byte[2]; + byte[] data = { + transactionIdentifier[1], + transactionIdentifier[0], + protocolIdentifier[1], + protocolIdentifier[0], + length[1], + length[0], + this.unitIdentifier, + functionCode, + startingAddressIn[1], + startingAddressIn[0], + quantityIn[1], + quantityIn[0], + crc[0], + crc[1]}; + + try { + if ((this.tcpClientSocket.isConnected() | this.udpFlag)) { + DatagramPacket sendPacket; + DatagramSocket clientSocket; + if (this.udpFlag) { + InetAddress ipAddress = InetAddress.getByName(this.ipAddress); + sendPacket = new DatagramPacket(data, data.length, ipAddress, this.port); + clientSocket = new DatagramSocket(); + clientSocket.setSoTimeout(500); + clientSocket.send(sendPacket); + data = new byte['࠴']; + DatagramPacket receivePacket = new DatagramPacket(data, data.length); + clientSocket.receive(receivePacket); + clientSocket.close(); + data = receivePacket.getData(); + } else { + this.outStream.write(data, 0, data.length - 2); + if (this.sendDataChangedListener.size() > 0) { + this.sendData = new byte[data.length - 2]; + System.arraycopy(data, 0, this.sendData, 0, data.length - 2); + for (SendDataChangedListener hl : this.sendDataChangedListener) + hl.SendDataChanged(); + } + data = new byte['࠴']; + int numberOfBytes = this.inStream.read(data, 0, data.length); + if (this.receiveDataChangedListener.size() > 0) { + this.receiveData = new byte[numberOfBytes]; + System.arraycopy(data, 0, this.receiveData, 0, numberOfBytes); + for (ReceiveDataChangedListener hl : this.receiveDataChangedListener) + hl.ReceiveDataChanged(); + } + } + if ((((data[7] & 0xFF) == 132 ? 1 : 0) & (data[8] == 1 ? 1 : 0)) != 0) + throw new FunctionCodeNotSupportedException("Function code not supported by master"); + if ((((data[7] & 0xFF) == 132 ? 1 : 0) & (data[8] == 2 ? 1 : 0)) != 0) + throw new StartingAddressInvalidException("Starting adress invalid or starting adress + quantity invalid"); + if ((((data[7] & 0xFF) == 132 ? 1 : 0) & (data[8] == 3 ? 1 : 0)) != 0) + throw new QuantityInvalidException("Quantity invalid"); + if ((((data[7] & 0xFF) == 132 ? 1 : 0) & (data[8] == 4 ? 1 : 0)) != 0) + throw new ModbusException("Error reading"); + } + } catch (Exception e){ + //return ReadInputRegisters(startingAddress, quantity, ++nTentativa, posicao); + return null; + } + + for (int i = 0; i < quantity; i++) { + byte[] bytes = new byte[2]; + bytes[0] = data[(9 + i * 2)]; + bytes[1] = data[(9 + i * 2 + 1)]; + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + response[i] = byteBuffer.getShort(); + } + + if(nTentativa > 1) System.out.println("Tentativa " + nTentativa + " OK:Pos " + posicao); + return response; + } + + public void WriteSingleCoil(int startingAddress, boolean value) throws ModbusException, IOException { + if (((this.tcpClientSocket == null ? 1 : 0) & (this.udpFlag ? 0 : 1)) != 0) + throw new ConnectionException("connection error"); + byte[] coilValue = new byte[2]; + byte transactionIdentifier[] = toByteArray(1); + byte protocolIdentifier[] = toByteArray(0); + byte length[] = toByteArray(6); + + byte functionCode = 5; + byte startingAddressIn[] = toByteArray(startingAddress); + if (value) { + coilValue = toByteArray(65280); + } else { + coilValue = toByteArray(0); + } + byte crc[] = new byte[2]; + byte[] data = { + transactionIdentifier[1], + transactionIdentifier[0], + protocolIdentifier[1], + protocolIdentifier[0], + length[1], + length[0], + this.unitIdentifier, + functionCode, + startingAddressIn[1], + startingAddressIn[0], + coilValue[1], + coilValue[0], + crc[0], + crc[1]}; + + + if ((this.tcpClientSocket.isConnected() | this.udpFlag)) { + DatagramPacket sendPacket; + DatagramSocket clientSocket; + if (this.udpFlag) { + InetAddress ipAddress = InetAddress.getByName(this.ipAddress); + sendPacket = new DatagramPacket(data, data.length, ipAddress, this.port); + clientSocket = new DatagramSocket(); + clientSocket.setSoTimeout(500); + clientSocket.send(sendPacket); + data = new byte['࠴']; + DatagramPacket receivePacket = new DatagramPacket(data, data.length); + clientSocket.receive(receivePacket); + clientSocket.close(); + data = receivePacket.getData(); + } else { + this.outStream.write(data, 0, data.length - 2); + if (this.sendDataChangedListener.size() > 0) { + this.sendData = new byte[data.length - 2]; + System.arraycopy(data, 0, this.sendData, 0, data.length - 2); + for (SendDataChangedListener hl : this.sendDataChangedListener) + hl.SendDataChanged(); + } + data = new byte['࠴']; + int numberOfBytes = this.inStream.read(data, 0, data.length); + if (this.receiveDataChangedListener.size() > 0) { + this.receiveData = new byte[numberOfBytes]; + System.arraycopy(data, 0, this.receiveData, 0, numberOfBytes); + for (ReceiveDataChangedListener hl : this.receiveDataChangedListener) + hl.ReceiveDataChanged(); + } + } + } + if ((((data[7] & 0xFF) == 133 ? 1 : 0) & (data[8] == 1 ? 1 : 0)) != 0) + throw new FunctionCodeNotSupportedException("Function code not supported by master"); + if ((((data[7] & 0xFF) == 133 ? 1 : 0) & (data[8] == 2 ? 1 : 0)) != 0) + throw new StartingAddressInvalidException("Starting address invalid or starting address + quantity invalid"); + if ((((data[7] & 0xFF) == 133 ? 1 : 0) & (data[8] == 3 ? 1 : 0)) != 0) + throw new QuantityInvalidException("quantity invalid"); + if ((((data[7] & 0xFF) == 133 ? 1 : 0) & (data[8] == 4 ? 1 : 0)) != 0) { + throw new ModbusException("error reading"); + } + } + + public void WriteSingleRegister(int startingAddress, int value) throws ModbusException, IOException { + if (((this.tcpClientSocket == null ? 1 : 0) & (this.udpFlag ? 0 : 1)) != 0) + throw new ConnectionException("connection error"); + byte[] registerValue = new byte[2]; + byte transactionIdentifier[] = toByteArray(1); + byte protocolIdentifier[] = toByteArray(0); + byte length[] = toByteArray(6); + byte functionCode = 6; + byte startingAddressIn[] = toByteArray(startingAddress); + registerValue = toByteArray((short) value); + + byte crc[] = new byte[2]; + byte[] data = { + transactionIdentifier[1], + transactionIdentifier[0], + protocolIdentifier[1], + protocolIdentifier[0], + length[1], + length[0], + this.unitIdentifier, + functionCode, + startingAddressIn[1], + startingAddressIn[0], + registerValue[1], + registerValue[0], + crc[0], + crc[1]}; + + if ((this.tcpClientSocket.isConnected() | this.udpFlag)) { + DatagramPacket sendPacket; + DatagramSocket clientSocket; + if (this.udpFlag) { + InetAddress ipAddress = InetAddress.getByName(this.ipAddress); + sendPacket = new DatagramPacket(data, data.length, ipAddress, this.port); + clientSocket = new DatagramSocket(); + clientSocket.setSoTimeout(500); + clientSocket.send(sendPacket); + data = new byte['࠴']; + DatagramPacket receivePacket = new DatagramPacket(data, data.length); + clientSocket.receive(receivePacket); + clientSocket.close(); + data = receivePacket.getData(); + } else { + this.outStream.write(data, 0, data.length - 2); + if (this.sendDataChangedListener.size() > 0) { + this.sendData = new byte[data.length - 2]; + System.arraycopy(data, 0, this.sendData, 0, data.length - 2); + for (SendDataChangedListener hl : this.sendDataChangedListener) + hl.SendDataChanged(); + } + data = new byte['࠴']; + int numberOfBytes = this.inStream.read(data, 0, data.length); + if (this.receiveDataChangedListener.size() > 0) { + this.receiveData = new byte[numberOfBytes]; + System.arraycopy(data, 0, this.receiveData, 0, numberOfBytes); + for (ReceiveDataChangedListener hl : this.receiveDataChangedListener) + hl.ReceiveDataChanged(); + } + } + } + if ((((data[7] & 0xFF) == 134 ? 1 : 0) & (data[8] == 1 ? 1 : 0)) != 0) + throw new FunctionCodeNotSupportedException("Function code not supported by master"); + if ((((data[7] & 0xFF) == 134 ? 1 : 0) & (data[8] == 2 ? 1 : 0)) != 0) + throw new StartingAddressInvalidException("Starting address invalid or starting address + quantity invalid"); + if ((((data[7] & 0xFF) == 134 ? 1 : 0) & (data[8] == 3 ? 1 : 0)) != 0) + throw new QuantityInvalidException("quantity invalid"); + if ((((data[7] & 0xFF) == 134 ? 1 : 0) & (data[8] == 4 ? 1 : 0)) != 0) { + throw new ModbusException("error reading"); + } + } + + public void WriteMultipleCoils(int startingAddress, boolean[] values) throws ModbusException, IOException { + byte byteCount = (byte) (values.length / 8 + 1); + if (values.length % 8 == 0) + byteCount = (byte) (byteCount - 1); + byte[] quantityOfOutputs = toByteArray(values.length); + byte singleCoilValue = 0; + if (((this.tcpClientSocket == null ? 1 : 0) & (this.udpFlag ? 0 : 1)) != 0) + throw new ConnectionException("connection error"); + byte transactionIdentifier[] = toByteArray(1); + byte protocolIdentifier[] = toByteArray(0); + byte length[] = toByteArray(7 + (values.length / 8 + 1)); + byte functionCode = 15; + byte startingAddressIn[] = toByteArray(startingAddress); + + byte[] data = new byte[16 + byteCount - 1]; + data[0] = transactionIdentifier[1]; + data[1] = transactionIdentifier[0]; + data[2] = protocolIdentifier[1]; + data[3] = protocolIdentifier[0]; + data[4] = length[1]; + data[5] = length[0]; + data[6] = this.unitIdentifier; + data[7] = functionCode; + data[8] = startingAddressIn[1]; + data[9] = startingAddressIn[0]; + data[10] = quantityOfOutputs[1]; + data[11] = quantityOfOutputs[0]; + data[12] = byteCount; + for (int i = 0; i < values.length; i++) { + if (i % 8 == 0) + singleCoilValue = 0; + byte CoilValue; + if (values[i] != false) { + CoilValue = 1; + } else { + CoilValue = 0; + } + + singleCoilValue = (byte) (CoilValue << i % 8 | singleCoilValue); + + data[(13 + i / 8)] = singleCoilValue; + } + + if ((this.tcpClientSocket.isConnected() | this.udpFlag)) { + DatagramPacket sendPacket; + DatagramSocket clientSocket; + if (this.udpFlag) { + InetAddress ipAddress = InetAddress.getByName(this.ipAddress); + sendPacket = new DatagramPacket(data, data.length, ipAddress, this.port); + clientSocket = new DatagramSocket(); + clientSocket.setSoTimeout(500); + clientSocket.send(sendPacket); + data = new byte['࠴']; + DatagramPacket receivePacket = new DatagramPacket(data, data.length); + clientSocket.receive(receivePacket); + clientSocket.close(); + data = receivePacket.getData(); + } else { + this.outStream.write(data, 0, data.length - 2); + if (this.sendDataChangedListener.size() > 0) { + this.sendData = new byte[data.length - 2]; + System.arraycopy(data, 0, this.sendData, 0, data.length - 2); + for (SendDataChangedListener hl : this.sendDataChangedListener) + hl.SendDataChanged(); + } + data = new byte['࠴']; + int numberOfBytes = this.inStream.read(data, 0, data.length); + if (this.receiveDataChangedListener.size() > 0) { + this.receiveData = new byte[numberOfBytes]; + System.arraycopy(data, 0, this.receiveData, 0, numberOfBytes); + for (ReceiveDataChangedListener hl : this.receiveDataChangedListener) + hl.ReceiveDataChanged(); + } + } + } + if ((((data[7] & 0xFF) == 143 ? 1 : 0) & (data[8] == 1 ? 1 : 0)) != 0) + throw new FunctionCodeNotSupportedException("Function code not supported by master"); + if ((((data[7] & 0xFF) == 143 ? 1 : 0) & (data[8] == 2 ? 1 : 0)) != 0) + throw new StartingAddressInvalidException("Starting address invalid or starting address + quantity invalid"); + if ((((data[7] & 0xFF) == 143 ? 1 : 0) & (data[8] == 3 ? 1 : 0)) != 0) + throw new QuantityInvalidException("quantity invalid"); + if ((((data[7] & 0xFF) == 143 ? 1 : 0) & (data[8] == 4 ? 1 : 0)) != 0) { + throw new ModbusException("error reading"); + } + } + + public void WriteMultipleRegisters(int startingAddress, int[] values) throws ModbusException, IOException { + byte byteCount = (byte) (values.length * 2); + byte[] quantityOfOutputs = toByteArray(values.length); + if (((this.tcpClientSocket == null ? 1 : 0) & (this.udpFlag ? 0 : 1)) != 0) + throw new ConnectionException("connection error"); + byte transactionIdentifier[] = toByteArray(1); + byte protocolIdentifier[] = toByteArray(0); + byte length[] = toByteArray(7 + values.length * 2); + byte functionCode = 16; + byte startingAddressIn[] = toByteArray(startingAddress); + + byte[] data = new byte[15 + values.length * 2]; + data[0] = transactionIdentifier[1]; + data[1] = transactionIdentifier[0]; + data[2] = protocolIdentifier[1]; + data[3] = protocolIdentifier[0]; + data[4] = length[1]; + data[5] = length[0]; + data[6] = this.unitIdentifier; + data[7] = functionCode; + data[8] = startingAddressIn[1]; + data[9] = startingAddressIn[0]; + data[10] = quantityOfOutputs[1]; + data[11] = quantityOfOutputs[0]; + data[12] = byteCount; + for (int i = 0; i < values.length; i++) { + byte[] singleRegisterValue = toByteArray(values[i]); + data[(13 + i * 2)] = singleRegisterValue[1]; + data[(14 + i * 2)] = singleRegisterValue[0]; + } + + if ((this.tcpClientSocket.isConnected() | this.udpFlag)) { + DatagramPacket sendPacket; + DatagramSocket clientSocket; + if (this.udpFlag) { + InetAddress ipAddress = InetAddress.getByName(this.ipAddress); + sendPacket = new DatagramPacket(data, data.length, ipAddress, this.port); + clientSocket = new DatagramSocket(); + clientSocket.setSoTimeout(500); + clientSocket.send(sendPacket); + data = new byte['࠴']; + DatagramPacket receivePacket = new DatagramPacket(data, data.length); + clientSocket.receive(receivePacket); + clientSocket.close(); + data = receivePacket.getData(); + } else { + this.outStream.write(data, 0, data.length - 2); + if (this.sendDataChangedListener.size() > 0) { + this.sendData = new byte[data.length - 2]; + System.arraycopy(data, 0, this.sendData, 0, data.length - 2); + for (SendDataChangedListener hl : this.sendDataChangedListener) + hl.SendDataChanged(); + } + data = new byte['࠴']; + int numberOfBytes = this.inStream.read(data, 0, data.length); + if (this.receiveDataChangedListener.size() > 0) { + this.receiveData = new byte[numberOfBytes]; + System.arraycopy(data, 0, this.receiveData, 0, numberOfBytes); + for (ReceiveDataChangedListener hl : this.receiveDataChangedListener) + hl.ReceiveDataChanged(); + } + } + } + if ((((data[7] & 0xFF) == 144 ? 1 : 0) & (data[8] == 1 ? 1 : 0)) != 0) + throw new FunctionCodeNotSupportedException("Function code not supported by master"); + if ((((data[7] & 0xFF) == 144 ? 1 : 0) & (data[8] == 2 ? 1 : 0)) != 0) + throw new StartingAddressInvalidException("Starting address invalid or starting address + quantity invalid"); + if ((((data[7] & 0xFF) == 144 ? 1 : 0) & (data[8] == 3 ? 1 : 0)) != 0) + throw new QuantityInvalidException("quantity invalid"); + if ((((data[7] & 0xFF) == 144 ? 1 : 0) & (data[8] == 4 ? 1 : 0)) != 0) { + throw new ModbusException("error reading"); + } + } + + public int[] ReadWriteMultipleRegisters(int startingAddressRead, int quantityRead, int startingAddressWrite, int[] values) throws ModbusException, IOException { + byte[] startingAddressReadLocal = new byte[2]; + byte[] quantityReadLocal = new byte[2]; + byte[] startingAddressWriteLocal = new byte[2]; + byte[] quantityWriteLocal = new byte[2]; + byte writeByteCountLocal = 0; + if (((this.tcpClientSocket == null ? 1 : 0) & (this.udpFlag ? 0 : 1)) != 0) + throw new ConnectionException("connection error"); + if (((startingAddressRead > 65535 ? 1 : 0) | (quantityRead > 125 ? 1 : 0) | (startingAddressWrite > 65535 ? 1 : 0) | (values.length > 121 ? 1 : 0)) != 0) { + throw new IllegalArgumentException("Starting address must be 0 - 65535; quantity must be 0 - 125"); + } + byte transactionIdentifier[] = toByteArray(1); + byte protocolIdentifier[] = toByteArray(0); + byte length[] = toByteArray(6); + byte functionCode = 23; + startingAddressReadLocal = toByteArray(startingAddressRead); + quantityReadLocal = toByteArray(quantityRead); + startingAddressWriteLocal = toByteArray(startingAddressWrite); + quantityWriteLocal = toByteArray(values.length); + writeByteCountLocal = (byte) (values.length * 2); + byte[] data = new byte[19 + values.length * 2]; + data[0] = transactionIdentifier[1]; + data[1] = transactionIdentifier[0]; + data[2] = protocolIdentifier[1]; + data[3] = protocolIdentifier[0]; + data[4] = length[1]; + data[5] = length[0]; + data[6] = this.unitIdentifier; + data[7] = functionCode; + data[8] = startingAddressReadLocal[1]; + data[9] = startingAddressReadLocal[0]; + data[10] = quantityReadLocal[1]; + data[11] = quantityReadLocal[0]; + data[12] = startingAddressWriteLocal[1]; + data[13] = startingAddressWriteLocal[0]; + data[14] = quantityWriteLocal[1]; + data[15] = quantityWriteLocal[0]; + data[16] = writeByteCountLocal; + + for (int i = 0; i < values.length; i++) { + byte[] singleRegisterValue = toByteArray(values[i]); + data[(17 + i * 2)] = singleRegisterValue[1]; + data[(18 + i * 2)] = singleRegisterValue[0]; + } + + if ((this.tcpClientSocket.isConnected() | this.udpFlag)) { + DatagramPacket sendPacket; + DatagramSocket clientSocket; + if (this.udpFlag) { + InetAddress ipAddress = InetAddress.getByName(this.ipAddress); + sendPacket = new DatagramPacket(data, data.length, ipAddress, this.port); + clientSocket = new DatagramSocket(); + clientSocket.setSoTimeout(500); + clientSocket.send(sendPacket); + data = new byte['࠴']; + DatagramPacket receivePacket = new DatagramPacket(data, data.length); + clientSocket.receive(receivePacket); + clientSocket.close(); + data = receivePacket.getData(); + } else { + this.outStream.write(data, 0, data.length - 2); + if (this.sendDataChangedListener.size() > 0) { + this.sendData = new byte[data.length - 2]; + System.arraycopy(data, 0, this.sendData, 0, data.length - 2); + for (SendDataChangedListener hl : this.sendDataChangedListener) + hl.SendDataChanged(); + } + data = new byte['࠴']; + int numberOfBytes = this.inStream.read(data, 0, data.length); + if (this.receiveDataChangedListener.size() > 0) { + this.receiveData = new byte[numberOfBytes]; + System.arraycopy(data, 0, this.receiveData, 0, numberOfBytes); + for (ReceiveDataChangedListener hl : this.receiveDataChangedListener) + hl.ReceiveDataChanged(); + } + } + } + if ((((data[7] & 0xFF) == 151 ? 1 : 0) & (data[8] == 1 ? 1 : 0)) != 0) + throw new FunctionCodeNotSupportedException("Function code not supported by master"); + if ((((data[7] & 0xFF) == 151 ? 1 : 0) & (data[8] == 2 ? 1 : 0)) != 0) + throw new StartingAddressInvalidException("Starting address invalid or starting address + quantity invalid"); + if ((((data[7] & 0xFF) == 151 ? 1 : 0) & (data[8] == 3 ? 1 : 0)) != 0) + throw new QuantityInvalidException("quantity invalid"); + if ((((data[7] & 0xFF) == 151 ? 1 : 0) & (data[8] == 4 ? 1 : 0)) != 0) + throw new ModbusException("error reading"); + int[] response = new int[quantityRead]; + for (int i = 0; i < quantityRead; i++) { + + + byte highByte = data[(9 + i * 2)]; + byte lowByte = data[(9 + i * 2 + 1)]; + + byte[] bytes = {highByte, lowByte}; + + + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + response[i] = byteBuffer.getShort(); + } + return response; + } + + public void Disconnect() throws IOException { + if (this.inStream != null) + this.inStream.close(); + + if (this.outStream != null) + this.outStream.close(); + + if (this.tcpClientSocket != null) + this.tcpClientSocket.close(); + + this.tcpClientSocket = null; + } + + public static byte[] toByteArray(int value) { + byte[] result = new byte[2]; + result[1] = ((byte) (value >> 8)); + result[0] = ((byte) value); + return result; + } + + public static byte[] toByteArrayDouble(int value) { + return ByteBuffer.allocate(4).putInt(value).array(); + } + + public static byte[] toByteArray(float value) { + return ByteBuffer.allocate(4).putFloat(value).array(); + } + + public boolean isConnected() { + return this.tcpClientSocket != null + && this.tcpClientSocket.isConnected(); + } + + public String getipAddress() { + return this.ipAddress; + } + + public void setipAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + + public int getPort() { + return this.port; + } + + public void setPort(int port) { + this.port = port; + } + + public boolean getUDPFlag() { + return this.udpFlag; + } + + public void setUDPFlag(boolean udpFlag) { + this.udpFlag = udpFlag; + } + + public int getConnectionTimeout() { + return this.connectTimeout; + } + + public void setConnectionTimeout(int connectionTimeout) { + this.connectTimeout = connectionTimeout; + } + + public void addReveiveDataChangedListener(ReceiveDataChangedListener toAdd) { + this.receiveDataChangedListener.add(toAdd); + } + + public void addSendDataChangedListener(SendDataChangedListener toAdd) { + this.sendDataChangedListener.add(toAdd); + } +} diff --git a/src/main/java/de/re/easymodbus/modbusclient/ReceiveDataChangedListener.java b/src/main/java/de/re/easymodbus/modbusclient/ReceiveDataChangedListener.java new file mode 100644 index 0000000..e258358 --- /dev/null +++ b/src/main/java/de/re/easymodbus/modbusclient/ReceiveDataChangedListener.java @@ -0,0 +1,12 @@ +package de.re.easymodbus.modbusclient; + +public abstract interface ReceiveDataChangedListener +{ + public abstract void ReceiveDataChanged(); +} + + +/* Location: C:\Users\Admin\Desktop\JAVA DEV\EasyModbusJava.jar!\de\re\easymodbus\modbusclient\ReceiveDataChangedListener.class + * Java compiler version: 8 (52.0) + * JD-Core Version: 0.7.1 + */ \ No newline at end of file diff --git a/src/main/java/de/re/easymodbus/modbusclient/SendDataChangedListener.java b/src/main/java/de/re/easymodbus/modbusclient/SendDataChangedListener.java new file mode 100644 index 0000000..c2c119b --- /dev/null +++ b/src/main/java/de/re/easymodbus/modbusclient/SendDataChangedListener.java @@ -0,0 +1,12 @@ +package de.re.easymodbus.modbusclient; + +public abstract interface SendDataChangedListener +{ + public abstract void SendDataChanged(); +} + + +/* Location: C:\Users\Admin\Desktop\JAVA DEV\EasyModbusJava.jar!\de\re\easymodbus\modbusclient\SendDataChangedListener.class + * Java compiler version: 8 (52.0) + * JD-Core Version: 0.7.1 + */ \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..02cabd0 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,24 @@ +spring: + application: + name: backend + + datasource: + url: jdbc:sqlite:./data/backend.db + driver-class-name: org.sqlite.JDBC + + jpa: + database-platform: org.hibernate.community.dialect.SQLiteDialect + hibernate: + ddl-auto: validate + + flyway: + enabled: true + locations: classpath:db/migration + +litoralregas: + modbus: + host: 198.19.0.176 + port: 533 + timeout-millis: 500 + max-attempts: 3 + retry-delay-millis: 1000 \ No newline at end of file diff --git a/src/main/resources/db/migration/V1__initial_schema.sql b/src/main/resources/db/migration/V1__initial_schema.sql new file mode 100644 index 0000000..829b62f --- /dev/null +++ b/src/main/resources/db/migration/V1__initial_schema.sql @@ -0,0 +1,14 @@ +CREATE TABLE sensor_definition ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(255) NOT NULL, + modbus_address INTEGER NOT NULL, + bit_offset INTEGER, + value_type VARCHAR(50) NOT NULL, + unit VARCHAR(50), + decimal_places INTEGER NOT NULL DEFAULT 0, + category VARCHAR(100) NOT NULL, + source_type VARCHAR(50) NOT NULL, + polling_interval_seconds INTEGER NOT NULL DEFAULT 1, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP NOT NULL +); \ No newline at end of file diff --git a/src/main/resources/db/migration/V2__seed_sensor_definitions.sql b/src/main/resources/db/migration/V2__seed_sensor_definitions.sql new file mode 100644 index 0000000..e9eb4d4 --- /dev/null +++ b/src/main/resources/db/migration/V2__seed_sensor_definitions.sql @@ -0,0 +1,52 @@ +INSERT INTO sensor_definition ( + name, + modbus_address, + bit_offset, + value_type, + unit, + decimal_places, + category, + source_type, + polling_interval_seconds, + enabled, + created_at +) VALUES +( + 'Greenhouse Temperature', + 100, + NULL, + 'DECIMAL', + 'ºC', + 1, + 'CLIMATE', + 'MODBUS', + 2, + TRUE, + CURRENT_TIMESTAMP +), +( + 'Greenhouse Humidity', + 101, + NULL, + 'DECIMAL', + '%', + 1, + 'CLIMATE', + 'MODBUS', + 2, + TRUE, + CURRENT_TIMESTAMP +), +( + 'Irrigation Pump Running', + 200, + 0, + 'BOOLEAN', + NULL, + 0, + 'IRRIGATION', + 'MODBUS', + 1, + TRUE, + CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/src/main/resources/db/migration/V3__add_sensor_definition_constraints.sql b/src/main/resources/db/migration/V3__add_sensor_definition_constraints.sql new file mode 100644 index 0000000..8080fbd --- /dev/null +++ b/src/main/resources/db/migration/V3__add_sensor_definition_constraints.sql @@ -0,0 +1,2 @@ +CREATE UNIQUE INDEX ux_sensor_definition_name +ON sensor_definition(name); \ No newline at end of file diff --git a/src/test/java/com/litoralregas/backend/BackendApplicationTests.java b/src/test/java/com/litoralregas/backend/BackendApplicationTests.java new file mode 100644 index 0000000..b821643 --- /dev/null +++ b/src/test/java/com/litoralregas/backend/BackendApplicationTests.java @@ -0,0 +1,13 @@ +package com.litoralregas.backend; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class BackendApplicationTests { + + @Test + void contextLoads() { + } + +}