+
+For the KNX USB interface to work with the Updater, the interface must use the WinUSB driver under Windows.
+The WinUSB driver can be installed with e.g. [zadig](https://zadig.akeo.ie).
+- Open zadig and click in the menu *Options->List all devices*
+- In the drop-down select the KNX USB-Interface (e.g. KNX-Interface).
+
+
+- Make sure that driver *WinUSB* is selected and click on *Replace Driver*.
+
+Note: After replacing the standard *HID* driver, the ETS no longer works with the KNX USB interface.
+To uninstall the *WinUSB* driver, search for the interface in the Windows Device Manager and uninstall its driver.
+Windows will reinstall the standard *HID* driver after reconnecting the interface.
+
+### Loxone Miniserver Gen 1
+
+For Loxone Miniserver Gen 1 `--ip-tunnel-reconnect 247` and `--own x.y.z` are mandatory.
+`x.y.z` must match the Loxone Miniserver´s own physical KNX address.
+
+## Development
+
+### IDEs:
+- [IntelliJ IDEA (Community Edition)](https://www.jetbrains.com/idea/download)
+- Eclipse project is currently not maintained
+
+### IntelliJ IDEA Settings for Updater GUI development:
+Change these in [**Settings Dialog**](https://www.jetbrains.com/help/idea/settings-preferences-dialog.html) (Menu File->Settings):
+- Editor->[GUI Designer](https://www.jetbrains.com/help/idea/gui-designer.html)->Generate GUI into: Java source code
+- [Plugins](https://www.jetbrains.com/help/idea/plugins-settings.html)->install "Resource Bundle Editor" [(howto)](https://www.jetbrains.com/help/idea/resource-bundle.html#open-bundle-editor)
+- Build, Execution, Deployment->[Build Tools](https://www.jetbrains.com/help/idea/settings-build-tools.html)->Gradle->Build and run using: Intellij IDEA
+- Build, Execution, Deployment->[Build Tools](https://www.jetbrains.com/help/idea/settings-build-tools.html)->Gradle->Run tests using: Intellij IDEA
+
+### gradle:
+update [gradle wrapper](gradle/wrapper) to the newest version:
```
gradlew wrapper
```
diff --git a/firmware_updater/updater/build.gradle b/firmware_updater/updater/build.gradle
new file mode 100644
index 00000000..1b139699
--- /dev/null
+++ b/firmware_updater/updater/build.gradle
@@ -0,0 +1,164 @@
+plugins {
+ id 'java'
+ id 'application'
+ id 'com.gradleup.shadow' version '9.2.2'
+ id 'jvm-test-suite'
+}
+
+group = 'org.selfbus'
+version = '1.27' ///\todo Change also in ToolInfo.java (versionMajor, versionMinor)
+description = 'Selfbus Updater, a tool to update the firmware of Selfbus devices via the KNX Bus.'
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(17))
+ }
+ // withJavadocJar() //todo fails, because there is no real docu
+}
+
+application {
+ mainClass = 'org.selfbus.updater.Updater'
+}
+
+def gitHash = "git rev-parse --verify --short HEAD".execute().text.trim()
+def buildDate = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z")
+ .withZone(ZoneId.of("UTC"))
+ .format(Instant.now())
+jar {
+ manifest {
+ attributes(
+ 'Main-Class': application.mainClass,
+ 'Implementation-Version': project.version,
+ 'Revision': "${gitHash}",
+ 'Build-Date': "${buildDate}"
+ )
+ }
+}
+
+sourceSets {
+ main {
+ java.srcDirs = ['src']
+ resources.srcDirs = ['src/resources']
+ }
+ test {
+ java.srcDirs = ['test']
+ //java.exclude 'dir1/', 'dir2'
+ resources.srcDirs = ['test/resources']
+ }
+}
+
+tasks.named('compileJava') {
+ options.javaModuleVersion = provider { project.version }
+}
+
+// ensure UTF-8 encoding
+compileJava.options.encoding = 'UTF-8'
+compileTestJava.options.encoding = 'UTF-8'
+javadoc.options.encoding = 'UTF-8'
+
+compileJava.options.compilerArgs = [
+ '-Xlint:all', // enable all warnings
+ '-Xlint:-serial', // disable serialisation warning
+]
+
+compileTestJava.options.compilerArgs = [
+ '-Xlint:all', // enable all warnings
+]
+
+tasks.assemble {
+ dependsOn(tasks.shadowJar)
+}
+
+testing {
+ //
+ // comment avoids warning "suites' cannot be applied to '(groovy.lang.closure)'"
+ //
+ //noinspection GroovyAssignabilityCheck
+ suites {
+ test {
+ useJUnitJupiter()
+ }
+
+ integrationTest(JvmTestSuite) {
+ dependencies {
+ implementation project()
+ }
+
+ targets {
+ configureEach {
+ testTask.configure {
+ shouldRunAfter(test)
+ }
+ }
+ }
+ }
+ }
+}
+
+tasks.named('check') {
+ dependsOn(testing.suites.integrationTest)
+}
+
+//
+// Repositories are now configured in settings.gradle, so we can include https://jitpack.io.
+// We need it to add java-intelhex-parser as an dependency, instead of copy/pasting the source.
+//
+dependencies {
+ // Use this implementation for debugging with local calimero-core
+ // Set location of local calimero-core in settings.gradle
+ //implementation 'com.github.calimero:calimero-core'
+
+ // calimero knx bus access library
+ implementation 'com.github.calimero:calimero-core:2.6'
+
+ // calimero serial tx/rx lib for ft1.2 and tpuart support
+ implementation 'com.github.calimero:calimero-rxtx:2.6'
+
+ // calimero usb support
+ implementation 'io.calimero:calimero-usb:2.6'
+
+ // find specific directories under linux and windows
+ implementation 'net.harawata:appdirs:1.5.0'
+
+ // For search in byte array
+ implementation 'com.google.guava:guava:33.5.0-jre'
+
+ // command line option
+ implementation 'commons-cli:commons-cli:1.11.0' // todo On update check todo in CliOptions::helpToString
+
+ // console and file logging
+ implementation 'ch.qos.logback:logback-classic:1.5.21'
+
+ // Redirect System.Logger (used by calimero >=3.0) to slf4J (logback)
+ implementation 'org.slf4j:slf4j-jdk-platform-logging:2.0.17'
+
+ // Console ansi color support
+ implementation 'org.jline:jline-terminal-jansi:3.30.6'
+
+ // GUI
+ implementation files('libs/forms_rt.jar')
+
+ // GUI settings
+ implementation 'com.fasterxml.jackson.core:jackson-databind:2.20.1'
+
+ // Intel hex parser
+ // commit 9dec823 of 2023/02/27
+ // https://github.com/j123b567/java-intelhex-parser/commit/9dec82355697dba028e8aa1832b8b9ee1b66d9e2
+ implementation 'com.github.j123b567:java-intelhex-parser:9dec823'
+ // To revert this, just delete "implementation 'com.github.j123b567:java-intelhex-parser:9dec823'"
+ // and c&p the code of
+ // ../cz/jaybee/intelhex to
+ // ../software-arm-lib/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex
+}
+
+// Just to maintain compatibility with the old fatJar task for a while.
+tasks.register('fatJar') {
+ dependsOn shadowJar
+ group = 'build'
+ description = 'An alias for the shadowJar task that creates a fat JAR'
+ doFirst {
+ def blue = '\u001B[34m'
+ def reset = '\u001B[0m'
+ logger.warn("${blue}The fatJar task is deprecated. Please use the \"shadowJar\" task directly.${reset}")
+ }
+}
\ No newline at end of file
diff --git a/firmware_updater/updater/source/gradle.properties b/firmware_updater/updater/gradle.properties
similarity index 100%
rename from firmware_updater/updater/source/gradle.properties
rename to firmware_updater/updater/gradle.properties
diff --git a/firmware_updater/updater/gradle/wrapper/gradle-wrapper.jar b/firmware_updater/updater/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..f8e1ee31
Binary files /dev/null and b/firmware_updater/updater/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/firmware_updater/updater/source/gradle/wrapper/gradle-wrapper.properties b/firmware_updater/updater/gradle/wrapper/gradle-wrapper.properties
similarity index 82%
rename from firmware_updater/updater/source/gradle/wrapper/gradle-wrapper.properties
rename to firmware_updater/updater/gradle/wrapper/gradle-wrapper.properties
index 3499ded5..23449a2b 100644
--- a/firmware_updater/updater/source/gradle/wrapper/gradle-wrapper.properties
+++ b/firmware_updater/updater/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/firmware_updater/updater/source/gradlew b/firmware_updater/updater/gradlew
similarity index 87%
rename from firmware_updater/updater/source/gradlew
rename to firmware_updater/updater/gradlew
index 79a61d42..adff685a 100755
--- a/firmware_updater/updater/source/gradlew
+++ b/firmware_updater/updater/gradlew
@@ -1,7 +1,7 @@
#!/bin/sh
#
-# Copyright © 2015-2021 the original authors.
+# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+# SPDX-License-Identifier: Apache-2.0
+#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -83,10 +85,8 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -133,10 +132,13 @@ location of your Java installation."
fi
else
JAVACMD=java
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
@@ -144,7 +146,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC3045
+ # shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -152,7 +154,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC3045
+ # shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -169,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
- CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -197,16 +198,19 @@ if "$cygwin" || "$msys" ; then
done
fi
-# Collect all arguments for the java command;
-# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
-# shell script including quotes and variable substitutions, so put them in
-# double quotes to make sure that they get re-expanded; and
-# * put everything else in single quotes, so that it's not re-expanded.
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
- -classpath "$CLASSPATH" \
- org.gradle.wrapper.GradleWrapperMain \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
diff --git a/firmware_updater/updater/source/gradlew.bat b/firmware_updater/updater/gradlew.bat
similarity index 88%
rename from firmware_updater/updater/source/gradlew.bat
rename to firmware_updater/updater/gradlew.bat
index 93e3f59f..c4bdd3ab 100644
--- a/firmware_updater/updater/source/gradlew.bat
+++ b/firmware_updater/updater/gradlew.bat
@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail
@@ -57,22 +59,21 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
diff --git a/firmware_updater/updater/images/libusb4java_assertion_error.png b/firmware_updater/updater/images/libusb4java_assertion_error.png
new file mode 100644
index 00000000..448da552
Binary files /dev/null and b/firmware_updater/updater/images/libusb4java_assertion_error.png differ
diff --git a/firmware_updater/updater/images/zadig_knx_interface_selected.png b/firmware_updater/updater/images/zadig_knx_interface_selected.png
new file mode 100644
index 00000000..ab23aaa1
Binary files /dev/null and b/firmware_updater/updater/images/zadig_knx_interface_selected.png differ
diff --git a/firmware_updater/updater/source/libs/forms_rt.jar b/firmware_updater/updater/libs/forms_rt.jar
similarity index 100%
rename from firmware_updater/updater/source/libs/forms_rt.jar
rename to firmware_updater/updater/libs/forms_rt.jar
diff --git a/firmware_updater/updater/settings.gradle b/firmware_updater/updater/settings.gradle
new file mode 100644
index 00000000..94e79588
--- /dev/null
+++ b/firmware_updater/updater/settings.gradle
@@ -0,0 +1,21 @@
+rootProject.name = 'SB_updater'
+
+// Use this to build with local calimero-core
+// Change also dependency in build.gradle
+//includeBuild('../../../calimero/calimero-core')
+
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ mavenCentral()
+ maven { (url = 'https://s01.oss.sonatype.org/content/repositories/snapshots')}
+ maven { (url = 'https://jitpack.io')} // jitpack builds if needed java-intelhex-parser for us
+ }
+/*
+ sourceControl {
+ gitRepository(uri("https://github.com/calimero-project/calimero-core.git")) {
+ producesModule('com.github.calimero:calimero-core')
+ }
+ }
+*/
+}
diff --git a/firmware_updater/updater/source/.gitignore b/firmware_updater/updater/source/.gitignore
deleted file mode 100644
index 3f874f51..00000000
--- a/firmware_updater/updater/source/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-.idea
-.gradle
-build
-out
diff --git a/firmware_updater/updater/source/.idea/artifacts/SB_updater_main_jar.xml b/firmware_updater/updater/source/.idea/artifacts/SB_updater_main_jar.xml
deleted file mode 100644
index 371f0a8b..00000000
--- a/firmware_updater/updater/source/.idea/artifacts/SB_updater_main_jar.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-
- * Adding a new cli option name:
- * - declare a short and a long option
- * private static final String OPT_SHORT_XXX = "X";
- * private static final String OPT_LONG_XXX = "XXXXX";
- * - in constructor @ref CliOptions(.) create an instance of class Option
- * and add it with cliOptions.addOption(yourOptionInstance)
- * - check with cmdLine.hasOption(OPT_SHORT_XXX) in method parse(.) is it set or not
- */
-public class CliOptions {
- private static final Logger logger = LoggerFactory.getLogger(CliOptions.class.getName());
-
- private static final String OPT_SHORT_FILENAME = "f";
- private static final String OPT_LONG_FILENAME = "fileName";
-
- private static final String OPT_SHORT_LOCALHOST = "H";
- private static final String OPT_LONG_LOCALHOST = "localhost";
- private static final String OPT_SHORT_LOCALPORT = "P";
- private static final String OPT_LONG_LOCALPORT = "localport";
- private static final String OPT_SHORT_PORT = "p";
- private static final String OPT_LONG_PORT = "port";
-
- private static final String OPT_SHORT_FT12 = "s";
- private static final String OPT_LONG_FT12 = "serial";
- private static final String OPT_SHORT_TPUART = "t";
- private static final String OPT_LONG_TPUART = "tpuart";
- private static final String OPT_SHORT_MEDIUM = "m";
- private static final String OPT_LONG_MEDIUM = "medium";
-
- private static final String OPT_LONG_USER_ID = "user";
- private static final String OPT_LONG_USER_PASSWORD = "user-pwd";
- private static final String OPT_LONG_DEVICE_PASSWORD = "device-pwd";
-
- private static final String OPT_SHORT_PROG_DEVICE = "D";
- private static final String OPT_LONG_PROG_DEVICE = "progDevice";
- private static final String OPT_SHORT_DEVICE = "d";
- private static final String OPT_LONG_DEVICE = "device";
- private static final String OPT_SHORT_OWN_ADDRESS = "o";
- private static final String OPT_LONG_OWN_ADDRESS = "own";
-
- private static final String OPT_SHORT_UID = "u";
- private static final String OPT_LONG_UID = "uid";
-
- private static final String OPT_LONG_DELAY = "delay";
-
- private static final String OPT_SHORT_TUNNEL_V2 = "t2";
- private static final String OPT_LONG_TUNNEL_V2 = "tunnelingv2";
- private static final String OPT_SHORT_TUNNEL_V1 = "t1";
- private static final String OPT_LONG_TUNNEL_V1 = "tunneling";
- private static final String OPT_SHORT_NAT = "n";
- private static final String OPT_LONG_NAT = "nat";
- private static final String OPT_SHORT_ROUTING = "r";
- private static final String OPT_LONG_ROUTING = "routing";
-
- private static final String OPT_SHORT_FULL = "f1";
- private static final String OPT_LONG_FULL = "full";
-
- private static final String OPT_SHORT_HELP = "h";
- private static final String OPT_LONG_HELP = "help";
-
- private static final String OPT_SHORT_VERSION = "v";
- private static final String OPT_LONG_VERSION = "version";
-
- private static final String OPT_SHORT_NO_FLASH = "f0";
- public static final String OPT_LONG_NO_FLASH = "NO_FLASH";
-
- private static final String OPT_SHORT_LOGLEVEL = "l";
- private static final String OPT_LONG_LOGLEVEL = "logLevel";
-
- private static final String OPT_LONG_PRIORITY = "priority";
-
- private static final String OPT_LONG_ERASEFLASH = "ERASEFLASH";
-
- private static final String OPT_LONG_DUMPFLASH = "DUMPFLASH";
-
- private static final String OPT_LONG_LOGSTATISTIC = "statistic";
-
- private static final String OPT_SHORT_BLOCKSIZE = "bs";
- public static final String OPT_LONG_BLOCKSIZE = "blocksize";
- private final static List
- *
- * @return the KNX network link
- * @throws KNXException on problems on link creation
- * @throws InterruptedException on interrupted thread
- */
- public KNXNetworkLink openLink() throws KNXException,
- InterruptedException, UpdaterException {
- final KNXMediumSettings medium = getMedium(cliOptions.medium(), cliOptions.ownAddress());
- logger.debug("Creating KNX network link {}...", medium);
- if (!cliOptions.ft12().isEmpty()) {
- // create FT1.2 network link
- try {
- return new KNXNetworkLinkFT12(Integer.parseInt(cliOptions.ft12()), medium);
- } catch (final NumberFormatException e) {
- return new KNXNetworkLinkFT12(cliOptions.ft12(), medium);
- }
- } else if (!cliOptions.tpuart().isEmpty()) {
- // create TPUART network link
- KNXNetworkLinkTpuart linkTpuart = new KNXNetworkLinkTpuart(cliOptions.tpuart(), medium, Collections.emptyList());
- linkTpuart.addAddress(cliOptions.ownAddress()); //\todo check if this is rly needed
- return linkTpuart;
- }
-
- // create local and remote socket address for network link
- InetSocketAddress local = createLocalSocket(cliOptions.localhost(), cliOptions.localport());
-
- final InetSocketAddress remote = new InetSocketAddress(cliOptions.knxInterface(), cliOptions.port());
-
- // Connect using KNX IP Secure
- if ((!cliOptions.devicePassword().isEmpty()) && (!cliOptions.userPassword().isEmpty())) {
- return createSecureTunnelingLink(local, remote, medium);
- }
-
- if (cliOptions.tunnelingV2()) {
- return createTunnelingLinkV2(local, remote, medium);
- }
-
- if (cliOptions.tunnelingV1()) {
- return createTunnelingLinkV1(local, remote, cliOptions.nat(), medium);
- }
-
- if (cliOptions.routing()) {
- return createRoutingLink(local, medium);
- }
-
- // try unsecure TCP tunneling v2 connection
- try {
- return createTunnelingLinkV2(local, remote, medium);
- } catch (final KNXException | InterruptedException e) {
- logger.info("failed with {}", e.toString());
- }
-
- // try unsecure UDP tunneling v1 connection with nat option set on cli
- try {
- return createTunnelingLinkV1(local, remote, cliOptions.nat(), medium);
- } catch (final KNXException | InterruptedException e) {
- logger.info("{}failed with {}{}", ConColors.YELLOW, e, ConColors.RESET);
- }
-
- // last chance try unsecure UDP tunneling v1 connection with INVERTED nat option set on cli
- return createTunnelingLinkV1(local, remote, !cliOptions.nat(), medium);
- }
-}
\ No newline at end of file
diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/SBManagementClientImpl.java b/firmware_updater/updater/source/src/main/java/org/selfbus/updater/SBManagementClientImpl.java
deleted file mode 100644
index 4c834479..00000000
--- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/SBManagementClientImpl.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package org.selfbus.updater;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import tuwien.auto.calimero.DataUnitBuilder;
-import tuwien.auto.calimero.IndividualAddress;
-import tuwien.auto.calimero.KNXInvalidResponseException;
-import tuwien.auto.calimero.KNXTimeoutException;
-import tuwien.auto.calimero.link.KNXLinkClosedException;
-import tuwien.auto.calimero.link.KNXNetworkLink;
-import tuwien.auto.calimero.mgmt.Destination;
-import tuwien.auto.calimero.mgmt.KNXDisconnectException;
-import tuwien.auto.calimero.mgmt.ManagementClientImpl;
-
-import java.util.List;
-import java.util.Optional;
-import java.util.function.BiFunction;
-
-import static org.selfbus.updater.Mcu.MAX_ASDU_LENGTH;
-import static org.selfbus.updater.Mcu.MAX_PAYLOAD;
-
-/**
- * Extends the calimero-core class {@link ManagementClientImpl}
- * to send firmware update using
- * APCI_USERMSG_MANUFACTURER_0 and APCI_USERMSG_MANUFACTURER_6
- * with our custom UPD/UDP protocol.
- */
-public class SBManagementClientImpl extends ManagementClientImpl {
- private static final int USERMSG_MANUFACTURER_0_WRITE = 0x2F8;
- private static final int USERMSG_MANUFACTURER_6_RESPONSE = 0x2FE;
- private static final int apciWrite = USERMSG_MANUFACTURER_0_WRITE;
- private static final int apciResponse = USERMSG_MANUFACTURER_6_RESPONSE;
- private final Logger logger;
-
- public SBManagementClientImpl(KNXNetworkLink link)
- throws KNXLinkClosedException {
- super(link);
- logger = LoggerFactory.getLogger(SBManagementClientImpl.class.getName() + " " + link.getName());
- }
-
- private byte[] prepareAsdu(final int cmd, final byte[] data) {
- byte[] asdu;
- asdu = new byte[data.length + 1];
- asdu[0] = (byte) cmd;
- System.arraycopy(data, 0, asdu, 1, data.length);
- return asdu;
- }
-
- // for Selfbus updater
- public byte[] sendUpdateData(final Destination dst, final int cmd, final byte[] data)
- throws KNXTimeoutException, KNXLinkClosedException, KNXInvalidResponseException, KNXDisconnectException, InterruptedException, UpdaterException {
-
- BiFunction
- * Updater is a {@link Runnable} tool implementation allowing a user to update
- * KNXduino devices.
- * When running this tool from the console, the
- * CAUTION:
- * {@link SBManagementClientImpl} uses java reflections to get access to private field's/method's
- * of calimero-core's ManagementClientImpl
- *
- * @author Deti Fliegl
- * @author Pavel Kriz
- * @author Stefan Haller
- * @author Oliver Stefan
- */
-public class Updater implements Runnable {
- private final static Logger logger = LoggerFactory.getLogger(Updater.class.getName());
- private final CliOptions cliOptions;
- private final SBKNXLink sbKNXLink;
-
- /**
- * Creates a new Updater instance using the supplied options.
- *
- * Mandatory arguments are an IP host/address or a FT1.2 port identifier,
- * depending on the type of connection to the KNX network, and the KNX
- * device individual address ("area.line.device"). See
- * {@link #main(String[])} for the list of options.
- *
- * @param args
- * list with options
- * @throws KNXIllegalArgumentException
- * on unknown/invalid options
- * @throws ParseException
- * on invalid command line parameters
- */
- public Updater(final String[] args) throws ParseException, KNXFormatException {
- logger.debug(ToolInfo.getFullInfo());
- logger.debug(Settings.getLibraryHeader(false));
- logger.info(ConColors.BRIGHT_BOLD_GREEN +
- " _____ ________ __________ __ _______ __ ______ ____ ___ ________________ \n" +
- " / ___// ____/ / / ____/ __ )/ / / / ___/ / / / / __ \\/ __ \\/ |/_ __/ ____/ __ \\\n" +
- " \\__ \\/ __/ / / / /_ / __ / / / /\\__ \\ / / / / /_/ / / / / /| | / / / __/ / /_/ /\n" +
- " ___/ / /___/ /___/ __/ / /_/ / /_/ /___/ / / /_/ / ____/ /_/ / ___ |/ / / /___/ _, _/ \n" +
- "/____/_____/_____/_/ /_____/\\____//____/ \\____/_/ /_____/_/ |_/_/ /_____/_/ |_| \n" +
- "by Dr. Stefan Haller, Oliver Stefan et al. " + ToolInfo.getToolAndVersion() +
- ConColors.RESET);
- try {
- // read in user-supplied command line options
- this.cliOptions = new CliOptions(args, String.format("SB_updater-%s-all.jar", ToolInfo.getVersion()) ,
- "Selfbus KNX-Firmware update tool options", "", PHYS_ADDRESS_BOOTLOADER, PHYS_ADDRESS_OWN);
- this.sbKNXLink = new SBKNXLink();
- this.sbKNXLink.setCliOptions(cliOptions);
- } catch (final KNXIllegalArgumentException | KNXFormatException | ParseException e) {
- throw e;
- } catch (final RuntimeException e) {
- throw new KNXIllegalArgumentException(e.getMessage(), e);
- }
- }
-
- public Updater(CliOptions cliOptions){
- this.cliOptions = cliOptions;
- this.sbKNXLink = new SBKNXLink();
- this.sbKNXLink.setCliOptions(cliOptions);
- }
-
- public static void main(final String[] args) {
- if(args.length == 0) {
- GuiMain.startSwingGui();
- }else {
- try {
- final Updater d = new Updater(args);
- final ShutdownHandler sh = new ShutdownHandler().register();
- d.run();
- sh.unregister();
- } catch (final Throwable t) {
- logger.error("parsing options ", t);
- } finally {
- logger.info("\n\n");
- logger.debug("main exit");
- }
- }
- }
-
- /**
- * Called by this tool on completion.
- *
- * @param thrown
- * the thrown exception if operation completed due to a raised
- * exception,
+ * Adding a new cli option name:
+ * - declare a short and a long option
+ * private static final String OPT_SHORT_XXX = "X";
+ * private static final String OPT_LONG_XXX = "XXXXX";
+ * - in constructor @ref CliOptions(.) create an instance of class Option
+ * and add it with cliOptions.addOption(yourOptionInstance)
+ * - check with cmdLine.hasOption(OPT_SHORT_XXX) in method parse(.) is it set or not
+ */
+public class CliOptions {
+ private static final Logger logger = LoggerFactory.getLogger(CliOptions.class);
+
+ private static final String OPT_SHORT_FILENAME = "f";
+ public static final String OPT_LONG_FILENAME = "fileName";
+
+ private static final String OPT_SHORT_LOCALHOST = "H";
+ public static final String OPT_LONG_LOCALHOST = "localhost";
+ private static final String OPT_SHORT_LOCALPORT = "P";
+ public static final String OPT_LONG_LOCALPORT = "localport";
+ private static final String OPT_SHORT_PORT = "p";
+ public static final String OPT_LONG_PORT = "port";
+
+ private static final String OPT_SHORT_FT12 = "s";
+ public static final String OPT_LONG_FT12 = "serial";
+ private static final String OPT_SHORT_TPUART = "t";
+ public static final String OPT_LONG_TPUART = "tpuart";
+ public static final String OPT_LONG_USB = "usb";
+ private static final String OPT_SHORT_MEDIUM = "m";
+ public static final String OPT_LONG_MEDIUM = "medium";
+
+ public static final String OPT_LONG_USER_ID = "user";
+ public static final String OPT_LONG_USER_PASSWORD = "user-pwd";
+ public static final String OPT_LONG_DEVICE_PASSWORD = "device-pwd";
+
+ private static final String OPT_SHORT_PROG_DEVICE = "D";
+ public static final String OPT_LONG_PROG_DEVICE = "progDevice";
+ private static final String OPT_SHORT_DEVICE = "d";
+ public static final String OPT_LONG_DEVICE = "device";
+ private static final String OPT_SHORT_OWN_ADDRESS = "o";
+ public static final String OPT_LONG_OWN_ADDRESS = "own";
+
+ private static final String OPT_SHORT_UID = "u";
+ public static final String OPT_LONG_UID = "uid";
+
+ public static final String OPT_LONG_DELAY = "delay";
+ public static final String OPT_LONG_RECONNECT = "reconnect";
+ public static final String OPT_LONG_RECONNECT_SEQ_NUMBER = "ip-tunnel-reconnect";
+
+ private static final String OPT_SHORT_TUNNEL_V2 = "t2";
+ public static final String OPT_LONG_TUNNEL_V2 = "tunnelingv2";
+ private static final String OPT_SHORT_TUNNEL_V1 = "t1";
+ public static final String OPT_LONG_TUNNEL_V1 = "tunneling";
+ private static final String OPT_SHORT_NAT = "n";
+ public static final String OPT_LONG_NAT = "nat";
+ private static final String OPT_SHORT_ROUTING = "r";
+ public static final String OPT_LONG_ROUTING = "routing";
+
+ private static final String OPT_SHORT_FULL = "f1";
+ public static final String OPT_LONG_FULL = "full";
+
+ private static final String OPT_SHORT_HELP = "h";
+ public static final String OPT_LONG_HELP = "help";
+
+ private static final String OPT_SHORT_VERSION = "v";
+ public static final String OPT_LONG_VERSION = "version";
+
+ private static final String OPT_SHORT_NO_FLASH = "f0";
+ public static final String OPT_LONG_NO_FLASH = "NO_FLASH";
+
+ private static final String OPT_SHORT_LOGLEVEL = "l";
+ public static final String OPT_LONG_LOGLEVEL = "logLevel";
+
+ public static final String OPT_LONG_PRIORITY = "priority";
+
+ public static final String OPT_LONG_ERASEFLASH = "ERASEFLASH";
+
+ public static final String OPT_LONG_DUMPFLASH = "DUMPFLASH";
+
+ public static final String OPT_LONG_LOGSTATISTIC = "statistic";
+
+ public static final String OPT_SHORT_BLOCKSIZE = "bs";
+ public static final String OPT_LONG_BLOCKSIZE = "blocksize";
+
+ public static final String OPT_LONG_DISCOVER = "discover";
+
+ private final static List
+ *
+ * @return the KNX network link
+ * @throws KNXException on problems on link creation
+ * @throws InterruptedException on interrupted thread
+ */
+ public KNXNetworkLink openLink() throws KNXException, InterruptedException,
+ UnknownHostException, UpdaterException {
+ return doOpenLink();
+ }
+
+ private KNXNetworkLink doOpenLink() throws KNXException,
+ InterruptedException, UnknownHostException, UpdaterException {
+ final KNXMediumSettings medium = getMedium(cliOptions.getMedium(), cliOptions.getOwnPhysicalAddress());
+ logger.debug("Creating KNX network link {}", medium);
+ if (!cliOptions.getFt12SerialPort().isEmpty()) {
+ // create FT1.2 network link
+ return new KNXNetworkLinkFT12(cliOptions.getFt12SerialPort(), medium);
+ } else if (!cliOptions.getTpuartSerialPort().isEmpty()) {
+ // create TPUART network link
+ KNXNetworkLinkTpuart linkTpuart = new KNXNetworkLinkTpuart(cliOptions.getTpuartSerialPort(), medium, Collections.emptyList());
+ linkTpuart.addAddress(cliOptions.getOwnPhysicalAddress());
+ return linkTpuart;
+ } else if (!cliOptions.getUsbVendorIdAndProductId().isEmpty()) {
+ // create USB network link
+ return new KNXNetworkLinkUsb(cliOptions.getUsbVendorIdAndProductId(), medium);
+ }
+
+ // create local and remote socket address for network link
+ InetSocketAddress local = createLocalSocket(cliOptions.getLocalhost(), cliOptions.getLocalPort());
+
+ final InetSocketAddress remote = new InetSocketAddress(resolveHost(cliOptions.getKnxInterface()), cliOptions.getPort());
+
+ // Connect using KNX IP Secure
+ if ((!cliOptions.getKnxSecureDevicePassword().isEmpty()) && (!cliOptions.getKnxSecureUserPassword().isEmpty())) {
+ return createSecureTunnelingLink(local, remote, medium);
+ }
+
+ if (cliOptions.getTunnelingV2isSet()) {
+ return createTunnelingLinkV2(local, remote, medium);
+ }
+
+ if (cliOptions.getTunnelingV1isSet()) {
+ return createTunnelingLinkV1(local, remote, cliOptions.getNatIsSet(), medium);
+ }
+
+ if (cliOptions.getRoutingIsSet()) {
+ return createRoutingLink(local, medium);
+ }
+
+ // try unsecure TCP tunneling v2 connection
+ try {
+ KNXNetworkLink testLink = createTunnelingLinkV2(local, remote, medium);
+ cliOptions.setTunnelingV2isSet(true);
+ return testLink;
+ } catch (final KNXException e) {
+ logger.info("failed with {}: {}", e.getClass().getSimpleName(), e.getMessage());
+ cliOptions.setTunnelingV2isSet(false);
+ }
+
+ // try unsecure UDP tunneling v1 connection with nat option set on cli
+ try {
+ KNXNetworkLink testLink = createTunnelingLinkV1(local, remote, cliOptions.getNatIsSet(), medium);
+ cliOptions.setTunnelingV1isSet(true);
+ return testLink;
+ } catch (final KNXException e) {
+ cliOptions.setTunnelingV1isSet(false);
+ logger.info("{}failed with {}: {}{}", ansi().fgBright(INFO), e.getClass().getSimpleName(), e.getMessage(),
+ ansi().reset());
+ }
+
+ // last chance try unsecure UDP tunneling v1 connection with INVERTED nat option set on cli
+ KNXNetworkLink testLink = createTunnelingLinkV1(local, remote, !cliOptions.getNatIsSet(), medium);
+ cliOptions.setNatIsSet(!cliOptions.getNatIsSet());
+ return testLink;
+ }
+}
\ No newline at end of file
diff --git a/firmware_updater/updater/src/org/selfbus/updater/SBManagementClientImpl.java b/firmware_updater/updater/src/org/selfbus/updater/SBManagementClientImpl.java
new file mode 100644
index 00000000..d7788780
--- /dev/null
+++ b/firmware_updater/updater/src/org/selfbus/updater/SBManagementClientImpl.java
@@ -0,0 +1,247 @@
+package org.selfbus.updater;
+
+import ch.qos.logback.classic.Logger;
+import org.slf4j.LoggerFactory;
+import tuwien.auto.calimero.*;
+import tuwien.auto.calimero.cemi.CEMI;
+import tuwien.auto.calimero.cemi.CEMILData;
+import tuwien.auto.calimero.link.*;
+import tuwien.auto.calimero.mgmt.Destination;
+import tuwien.auto.calimero.mgmt.KNXDisconnectException;
+import tuwien.auto.calimero.mgmt.ManagementClientImpl;
+import tuwien.auto.calimero.mgmt.TransportListener;
+
+import java.time.Duration;
+
+import static org.selfbus.updater.Mcu.MAX_ASDU_LENGTH;
+import static tuwien.auto.calimero.mgmt.Destination.State.OpenIdle;
+
+/**
+ * Extends the calimero-core class {@link ManagementClientImpl}
+ * to send firmware update using
+ * APCI_USERMSG_MANUFACTURER_0 and APCI_USERMSG_MANUFACTURER_6
+ * with our custom UPD/UDP protocol.
+ */
+public class SBManagementClientImpl extends ManagementClientImpl {
+
+ private CEMILData getCEMILData(final FrameEvent e, Logger eventLogger) {
+ final CEMI cemi = e.getFrame();
+ if (!(cemi instanceof final CEMILData linkData)) {
+ eventLogger.debug("not a CEMILData");
+ return null;
+ }
+ return linkData;
+ }
+
+ private class SBLinkListener implements NetworkLinkListener
+ {
+ private final Logger linkLogger;
+
+ SBLinkListener() {
+ this.linkLogger = (Logger) LoggerFactory.getLogger(SBLinkListener.class.getName() + " " + link.getName());
+ }
+
+ @Override
+ public void indication(final FrameEvent e)
+ {
+ CEMILData cemilData = getCEMILData(e, linkLogger);
+ if (cemilData == null) {
+ return;
+ }
+
+ if (cemilData.getPayload().length < 1) {
+ linkLogger.debug("no payload");
+ return;
+ }
+
+ final int tpdu = cemilData.getPayload()[0] & 0xff;
+ if (tpdu == 0x80) {
+ linkLogger.debug("indication {} -> {} T_Connect", cemilData.getSource(), cemilData.getDestination());
+ return;
+ }
+ else if (tpdu == 0x81) {
+ linkLogger.debug("indication {} -> {} T_Disconnect", cemilData.getSource(), cemilData.getDestination());
+ return;
+ }
+
+ final int ctrl = tpdu & 0xc0; // is it connection oriented? (1100 0000)
+ if (ctrl != 0) {
+ final int conControl = tpdu & 0xc3; // clear sequence number bits (1100 0011)
+ final int sequenceNumber = (tpdu & 0x3c) >> 2; // get sequence number and shift right by 2 (0011 1100)
+ switch (conControl) {
+ case 0xC2:
+ linkLogger.debug("indication {} -> {} T_Ack #{}", cemilData.getSource(), cemilData.getDestination(), sequenceNumber);
+ break;
+
+ case 0xC3:
+ linkLogger.debug("indication {} -> {} T_Nack #{}", cemilData.getSource(), cemilData.getDestination(), sequenceNumber);
+ break;
+
+ case 0x40:
+ case 0x41:
+ case 0x42:
+ case 0x43:
+ linkLogger.debug("indication {} -> {} T_Data_Connected #{}", cemilData.getSource(), cemilData.getDestination(), sequenceNumber);
+ break;
+
+ default:
+ // This should never happen
+ linkLogger.warn("indication {} -> {} conControl == {} tpdu == {} #{}", cemilData.getSource(), cemilData.getDestination(),
+ String.format("0x%X", conControl), String.format("0x%X", tpdu), sequenceNumber);
+ break;
+ }
+ }
+ else {
+ final KNXAddress dst = cemilData.getDestination();
+ // check for broadcast or group
+ if ((dst instanceof GroupAddress) && (dst.getRawAddress() == 0)) {
+ linkLogger.debug("broadcast {} -> {}", cemilData.getSource(), dst);
+ }
+ }
+ }
+
+ @Override
+ public void linkClosed(final CloseEvent e)
+ {
+ linkLogger.debug("attached link was closed");
+ }
+ }
+
+ private class SBTransportListener implements TransportListener
+ {
+ private final Logger transportLogger;
+
+ SBTransportListener() {
+ this.transportLogger = (Logger) LoggerFactory.getLogger(SBTransportListener.class.getName() + " " + link.getName());
+ }
+
+ @Override
+ public void broadcast(final FrameEvent e) {}
+
+ @Override
+ public void dataConnected(final FrameEvent e) {}
+
+ @Override
+ public void dataIndividual(final FrameEvent e) {}
+
+ @Override
+ public void disconnected(final Destination d) {
+ transportLogger.debug("disconnected {}", d);
+ }
+
+ @Override
+ public void group(final FrameEvent e) {}
+
+ @Override
+ public void detached(final DetachEvent e) {
+ transportLogger.debug("detached {}", e);
+ }
+
+ @Override
+ public void linkClosed(final CloseEvent e)
+ {
+ transportLogger.debug("linkClosed {}", e);
+ }
+ }
+
+ private static final int USERMSG_MANUFACTURER_0_WRITE = 0x2F8;
+ private static final int USERMSG_MANUFACTURER_6_RESPONSE = 0x2FE;
+ private static final int apciWrite = USERMSG_MANUFACTURER_0_WRITE;
+ private static final int apciResponse = USERMSG_MANUFACTURER_6_RESPONSE;
+ private final Logger logger;
+
+ KNXNetworkLink link;
+
+ public SBManagementClientImpl(KNXNetworkLink link)
+ throws KNXLinkClosedException {
+ super(link);
+ logger = (Logger) LoggerFactory.getLogger(SBManagementClientImpl.class.getName() + " " + link.getName());
+ this.link = link;
+ SBLinkListener linkListener = new SBLinkListener();
+ this.link.addLinkListener(linkListener);
+ SBTransportListener transportListener = new SBTransportListener();
+ this.transportLayer().addTransportListener(transportListener);
+ }
+
+ private byte[] prepareAsdu(final int cmd, final byte[] data) {
+ byte[] asdu;
+ asdu = new byte[data.length + 1];
+ asdu[0] = (byte) cmd;
+ System.arraycopy(data, 0, asdu, 1, data.length);
+ return asdu;
+ }
+
+ // for Selfbus updater
+ public byte[] sendUpdateData(final Destination dst, final int cmd, final byte[] data)
+ throws KNXTimeoutException, KNXLinkClosedException, KNXInvalidResponseException, KNXDisconnectException,
+ InterruptedException, UpdaterException {
+ final byte[] asdu = prepareAsdu(cmd, data);
+
+ byte[] send;
+ if (asdu.length > MAX_ASDU_LENGTH) {
+ // prevent creation of long frames, sblib doesn't support them yet
+ throw new UpdaterException(String.format("asdu.length (%d) exceeds MAX_ASDU_LENGTH (%d)",
+ asdu.length, MAX_ASDU_LENGTH));
+ }
+
+ send = DataUnitBuilder.createAPDU(apciWrite, asdu);
+
+ byte[] response;
+ try {
+ response = this.sendWait(dst, getPriority(), send, apciResponse, 2, MAX_ASDU_LENGTH, responseTimeout());
+ }
+ // We "try catch" here to catch at least once a KNXAckTimeoutException.
+ // It´s thrown on missing/faulty ack at linklayer level, but we never have seen it.
+ // Check public void send(final CEMI frame, final BlockingMode mode) in ConnectionBase.java
+ catch (KNXAckTimeoutException e) {
+ logger.error("Unexpected: never seen before {}", e);
+ // Delete logger.error, if you don´t get in line above IDEA warning "Fewer arguments provided (0) than placeholders specified (1)"
+ logger.error("", e);
+ throw e;
+ }
+ catch (KNXException e) {
+ if (e.getCause() instanceof KNXAckTimeoutException) {
+ logger.error("Unexpected: never seen before e.getCause() {}", e.getCause());
+ // Delete logger.error, if you don´t get in line above IDEA warning "Fewer arguments provided (0) than placeholders specified (1)"
+ logger.error("", e.getCause());
+ }
+ throw e;
+ }
+ return response;
+ }
+
+ @Override
+ protected byte[] sendWait(final Destination d, final Priority p, final byte[] apdu, final int responseServiceType,
+ final int minAsduLen, final int maxAsduLen, final Duration timeout) throws KNXDisconnectException,
+ KNXTimeoutException, KNXInvalidResponseException, KNXLinkClosedException, InterruptedException {
+ byte[] received;
+ try {
+ received = super.sendWait(d, p, apdu, responseServiceType, minAsduLen, maxAsduLen, timeout);
+ }
+ catch (KNXTimeoutException e) {
+ if (d.getState() != OpenIdle)
+ {
+ logger.debug("Critical destination state: {}", d);
+ }
+ throw e;
+ }
+ return received;
+ }
+
+ @Override
+ public String toString() {
+ String status;
+ if (isOpen()) {
+ status = "is open";
+ }
+ else {
+ status = "closed";
+ }
+
+ if (link == null) {
+ return String.format("%s, link==null, %s", status, super.toString());
+ }
+
+ return String.format("%s, link %s, %s", status, link, super.toString());
+ }
+}
diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/ToolInfo.java b/firmware_updater/updater/src/org/selfbus/updater/ToolInfo.java
similarity index 64%
rename from firmware_updater/updater/source/src/main/java/org/selfbus/updater/ToolInfo.java
rename to firmware_updater/updater/src/org/selfbus/updater/ToolInfo.java
index 12af3d77..dae5d2cb 100644
--- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/ToolInfo.java
+++ b/firmware_updater/updater/src/org/selfbus/updater/ToolInfo.java
@@ -6,18 +6,20 @@
import org.slf4j.LoggerFactory;
import tuwien.auto.calimero.Settings;
+import java.io.IOException;
+
/**
* Provides information about the application
*/
public final class ToolInfo
{
- private static final long versionMajor = 1; ///\todo change also in ../README.md and build.gradle
- private static final long versionMinor = 20;
+ private static final long versionMajor = 1; ///\todo change also in build.gradle
+ private static final long versionMinor = 27;
private static final long minMajorVersionBootloader = 1; ///\todo change also in ../README.md
private static final long minMinorVersionBootloader = 0;
- private static final Logger logger = LoggerFactory.getLogger(ToolInfo.class.getName());
+ private static final Logger logger = LoggerFactory.getLogger(ToolInfo.class);
private static final String author = "Selfbus";
private static final String tool = "Selfbus-Updater";
private static final String desc = "A Tool for updating firmware of a Selfbus device in a KNX network.";
@@ -29,8 +31,6 @@ private ToolInfo() {}
/**
* Returns the version as string representation.
- *
- * The returned version
*
* @return version as string
*/
@@ -57,11 +57,12 @@ public static String getTool() {
}
public static String getFullInfo() {
- return getToolAndVersion() + String.format("\n%s\n%s", desc, author);
+ return getToolAndVersion() + String.format("%s%s%s%s", System.lineSeparator(), desc,
+ System.lineSeparator(), author);
}
public static String getToolAndVersion() {
- return String.format("%s %s", getTool(), getVersion());
+ return String.format("%s %s (build %s)", getTool(), getVersion(), getManifestInfo());
}
public static void showVersion() {
@@ -77,4 +78,27 @@ public static String minVersionBootloader() {
minMinorVersionBootloader(),
0, 0, 0, 0).getVersion();
}
+
+ public static String getToolJarName() {
+ return String.format("SB_updater-%s-all.jar", ToolInfo.getVersion());
+ }
+
+ private static String getManifestInfo() {
+ // Only works correctly if built with gradle.
+ // This will fail during debugging, e.g. in IDEA, due to a different content of MANIFEST.MF
+ try (final var metaInf = ToolInfo.class.getResourceAsStream("/META-INF/MANIFEST.MF")) {
+ if (metaInf == null) {
+ return "";
+ }
+
+ final var manifest = new java.util.jar.Manifest(metaInf);
+ final var attributes = manifest.getMainAttributes();
+ return String.format("%s sha %s",
+ attributes.getValue("Build-Date"), attributes.getValue("Revision"));
+ }
+ catch (IOException ignore) {
+
+ }
+ return "";
+ }
}
diff --git a/firmware_updater/updater/src/org/selfbus/updater/Updater.java b/firmware_updater/updater/src/org/selfbus/updater/Updater.java
new file mode 100644
index 00000000..511ecfeb
--- /dev/null
+++ b/firmware_updater/updater/src/org/selfbus/updater/Updater.java
@@ -0,0 +1,452 @@
+package org.selfbus.updater;
+
+import ch.qos.logback.classic.Level;
+import org.apache.commons.cli.ParseException;
+import org.fusesource.jansi.AnsiConsole;
+import org.selfbus.updater.bootloader.BootloaderStatistic;
+import org.selfbus.updater.devicemgnt.DeviceManagement;
+import org.selfbus.updater.devicemgnt.DeviceManagementFactory;
+import org.selfbus.updater.logging.LoggingManager;
+import org.selfbus.updater.progress.AnsiCursor;
+import tuwien.auto.calimero.*;
+import org.selfbus.updater.bootloader.BootDescriptor;
+import org.selfbus.updater.bootloader.BootloaderIdentity;
+import org.selfbus.updater.bootloader.BootloaderUpdater;
+import org.selfbus.updater.upd.UDPProtocolVersion;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.fusesource.jansi.Ansi.*;
+import static org.selfbus.updater.logging.Color.*;
+import static org.selfbus.updater.logging.LoggingManager.CONSOLE_APPENDER_NAME;
+import static org.selfbus.updater.Utils.shortenPath;
+
+import org.selfbus.updater.gui.GuiMain;
+import tuwien.auto.calimero.serial.KNXPortClosedException;
+
+import java.net.UnknownHostException;
+
+/**
+ * A Tool for updating the firmware of a Selfbus device in a KNX network.
+ *
+ * {@link Updater} is a {@link Runnable} tool implementation allowing
+ * a user to update Selfbus KNX devices.
+ * When running this tool from the console, the {@code main} method of this class is invoked,
+ * otherwise use this class in the context appropriate to a {@link Runnable}.
+ * see /firmware_updater/bootloader/inc/boot_descriptor_block.h for more information
*/
public class BootDescriptor {
private static final long INVALID_ADDRESS = 0xFFFFFFFFL;
@@ -16,17 +18,18 @@ public class BootDescriptor {
private long appVersionAddress;
private boolean valid;
+ @SuppressWarnings("unused")
private BootDescriptor() {}
/**
- * Create a @ref BootDescriptor instance from given start/end address, crc32 and application pointer address
+ * Creates a {@link BootDescriptor} instance from the given start and end addresses, CRC32 checksum,
+ * and application version address.
* @param startAddress start address of the application firmware
* @param endAddress end address of the application firmware
* @param crc32 crc32 checksum from start to end address
* @param appVersionAddress AppVersionPointer address
- * @throws UpdaterException Exception in case of invalid start or end address
*/
- public BootDescriptor(long startAddress, long endAddress, int crc32, long appVersionAddress) throws UpdaterException {
+ public BootDescriptor(long startAddress, long endAddress, int crc32, long appVersionAddress) {
this.startAddress = startAddress;
this.endAddress = endAddress;
this.crc32 = crc32;
@@ -36,7 +39,7 @@ public BootDescriptor(long startAddress, long endAddress, int crc32, long appVer
}
- public static BootDescriptor fromArray(byte[] parse) throws UpdaterException {
+ public static BootDescriptor fromArray(byte[] parse) {
long startAddr = (parse[0] & 0xFF) +
((parse[1] & 0xFF) << 8) +
((parse[2] & 0xFF) << 16) +
@@ -75,22 +78,22 @@ public long length() {
return endAddress() - startAddress() + 1;
}
+ //todo get rid of this dirty hack for shorter console output
+ private short long2short(long a) {
+ return (short) (a & 0x0000FFFF);
+ }
+
+ @Override
public String toString() {
String res;
- if (valid) {
- res = String.format(" %svalid%s, 0x%04X-0x%04X, %5d byte(s), crc32 0x%08X",
- ConColors.BRIGHT_GREEN, ConColors.RESET, startAddress, endAddress, length(), crc32);
+ if (valid()) {
+ res = String.format("%s valid%s", ansi().fgBright(OK), ansi().reset());
}
else {
- res = String.format("%sinvalid%s, 0x%04X-0x%04X, %5d byte(s), crc32 0x%08X",
- ConColors.RED, ConColors.RESET, INVALID_ADDRESS & 0x0000FFFF,
- INVALID_ADDRESS & 0x0000FFFF, length(), crc32);
- }
-
- if (appVersionAddress != INVALID_ADDRESS)
- {
- res += String.format(", APP_VERSION pointer: 0x%04X", appVersionAddress);
+ res = String.format("%sinvalid%s", ansi().fgBright(WARN), ansi().reset());
}
+ res = String.format("%s, 0x%04X-0x%04X, %5d byte(s), crc32 0x%08X, APP_VERSION pointer: 0x%04X",
+ res, long2short(startAddress()), long2short(endAddress()), length(), crc32(), long2short(appVersionAddress()));
return res;
}
diff --git a/firmware_updater/updater/src/org/selfbus/updater/bootloader/BootloaderIdentity.java b/firmware_updater/updater/src/org/selfbus/updater/bootloader/BootloaderIdentity.java
new file mode 100644
index 00000000..9fcb5290
--- /dev/null
+++ b/firmware_updater/updater/src/org/selfbus/updater/bootloader/BootloaderIdentity.java
@@ -0,0 +1,63 @@
+package org.selfbus.updater.bootloader;
+
+import org.selfbus.updater.Utils;
+
+/**
+ * Holds Bootloader identity information
+ *
+ * see /firmware_updater/bootloader/src/update.cpp (method updRequestBootloaderIdentity) for more information.
+ */
+public record BootloaderIdentity(long versionMajor,
+ long versionMinor,
+ long versionSBLibMajor,
+ long versionSBLibMinor,
+ long features,
+ long applicationFirstAddress) {
+ private String hexVersionToString(long versionMajor, long versionMinor) {
+ byte high = (byte) (versionMinor >> 4);
+ byte low = (byte) (versionMinor & 0x0f);
+
+ String highPart;
+ String lowPart;
+
+ if (high < 10) {
+ highPart = String.format("%d", high);
+ }
+ else {
+ highPart = "x";
+ }
+
+ if (low < 10) {
+ lowPart = String.format("%d", low);
+ }
+ else {
+ lowPart = "y";
+ }
+
+ return String.format("%d.%s%s", versionMajor, highPart, lowPart);
+ }
+
+ public static BootloaderIdentity fromArray(byte[] parse) {
+ long vMajor = parse[0] & 0xff;
+ long vMinor = parse[1] & 0xff;
+ long features = Utils.streamToShort(parse, 2) & 0xffff;
+ long versionSBLibMajor = parse[4] & 0xff;
+ long versionSBLibMinor = parse[5] & 0xff;
+ long applicationFirstAddress = Utils.streamToLong(parse, 6);
+ return new BootloaderIdentity(vMajor, vMinor, versionSBLibMajor, versionSBLibMinor, features, applicationFirstAddress);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("v%s, sbLib v%s, Features: 0x%04X, App-start: 0x%04X",
+ getVersion(), getVersionSBLib(), features(), applicationFirstAddress());
+ }
+
+ public String getVersion() {
+ return String.format("%d.%02d", versionMajor(), versionMinor());
+ }
+
+ public String getVersionSBLib() {
+ return hexVersionToString(versionSBLibMajor(), versionSBLibMinor());
+ }
+}
diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/bootloader/BootloaderStatistic.java b/firmware_updater/updater/src/org/selfbus/updater/bootloader/BootloaderStatistic.java
similarity index 50%
rename from firmware_updater/updater/source/src/main/java/org/selfbus/updater/bootloader/BootloaderStatistic.java
rename to firmware_updater/updater/src/org/selfbus/updater/bootloader/BootloaderStatistic.java
index 6c7ba350..6eae2064 100644
--- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/bootloader/BootloaderStatistic.java
+++ b/firmware_updater/updater/src/org/selfbus/updater/bootloader/BootloaderStatistic.java
@@ -1,8 +1,10 @@
package org.selfbus.updater.bootloader;
-import org.selfbus.updater.ConColors;
import org.selfbus.updater.Utils;
+import static org.fusesource.jansi.Ansi.*;
+import static org.selfbus.updater.logging.Color.*;
+
public class BootloaderStatistic {
public static final int THRESHOLD_DISCONNECT = 1;
public static final int THRESHOLD_REPEATED = 1;
@@ -21,31 +23,38 @@ public static BootloaderStatistic fromArray(byte[] parse) {
return new BootloaderStatistic(disConnectCount, repeatedT_ACKcount);
}
- public String toString() {
- String result;
- String colored;
- if (getDisconnectCount() > BootloaderStatistic.THRESHOLD_DISCONNECT) {
- colored = ConColors.BRIGHT_YELLOW;
- } else {
- colored = ConColors.BRIGHT_GREEN;
- }
- result = String.format("#Disconnect: %s%2d%s", colored, getDisconnectCount(), ConColors.RESET);
- if (getRepeatedT_ACKcount() > BootloaderStatistic.THRESHOLD_REPEATED) {
- colored = ConColors.BRIGHT_YELLOW;
+ private static String toColoredThreshold(int value, int threshold) {
+ String ansiColor;
+ if (value > threshold) {
+ ansiColor = ansi().fgBright(INFO).toString();
} else {
- colored = ConColors.BRIGHT_GREEN;
+ ansiColor = ansi().fgBright(OK).toString();
}
- result += String.format(" #repeated T_ACK: %s%2d%s", colored, getRepeatedT_ACKcount(), ConColors.RESET);
+ return String.format("%s%2d%s", ansiColor, value, ansi().reset());
+ }
+
+ @Override
+ public String toString() {
+ String result = getDisconnectCountColored();
+ result += " " + getRepeatedT_ACKcountColored();
return result;
}
- public long getDisconnectCount()
+ public int getDisconnectCount()
{
return disconnectCount;
}
- public long getRepeatedT_ACKcount()
+ public int getRepeatedT_ACKcount()
{
return repeatedT_ACKcount;
}
+
+ public String getDisconnectCountColored() {
+ return toColoredThreshold(getDisconnectCount(), BootloaderStatistic.THRESHOLD_DISCONNECT);
+ }
+
+ public String getRepeatedT_ACKcountColored() {
+ return toColoredThreshold(getRepeatedT_ACKcount(), BootloaderStatistic.THRESHOLD_REPEATED);
+ }
}
diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/bootloader/BootloaderUpdater.java b/firmware_updater/updater/src/org/selfbus/updater/bootloader/BootloaderUpdater.java
similarity index 93%
rename from firmware_updater/updater/source/src/main/java/org/selfbus/updater/bootloader/BootloaderUpdater.java
rename to firmware_updater/updater/src/org/selfbus/updater/bootloader/BootloaderUpdater.java
index 11df7bd6..bd5f6e3a 100644
--- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/bootloader/BootloaderUpdater.java
+++ b/firmware_updater/updater/src/org/selfbus/updater/bootloader/BootloaderUpdater.java
@@ -2,6 +2,7 @@
public final class BootloaderUpdater {
/** Bootloader Updater appVersion identity string from app_main.cpp of bootloaderupdater */
+ @SuppressWarnings("SpellCheckingInspection")
public static final String BOOTLOADER_UPDATER_ID_STRING = "SBblu";
/** Maximum restart and processing time in milliseconds to
diff --git a/firmware_updater/updater/src/org/selfbus/updater/devicemgnt/DeviceManagement.java b/firmware_updater/updater/src/org/selfbus/updater/devicemgnt/DeviceManagement.java
new file mode 100644
index 00000000..2f760bcf
--- /dev/null
+++ b/firmware_updater/updater/src/org/selfbus/updater/devicemgnt/DeviceManagement.java
@@ -0,0 +1,583 @@
+package org.selfbus.updater.devicemgnt;
+
+import org.selfbus.updater.*;
+import org.selfbus.updater.bootloader.BootDescriptor;
+import org.selfbus.updater.bootloader.BootloaderIdentity;
+import org.selfbus.updater.bootloader.BootloaderStatistic;
+import org.selfbus.updater.progress.AnsiCursor;
+import org.selfbus.updater.progress.ProgressInfo;
+import org.selfbus.updater.progress.SpinningCursor;
+import org.selfbus.updater.upd.UDPProtocolVersion;
+import org.selfbus.updater.upd.UDPResult;
+import org.selfbus.updater.upd.UPDCommand;
+import org.selfbus.updater.upd.UPDProtocol;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import tuwien.auto.calimero.*;
+import tuwien.auto.calimero.link.KNXLinkClosedException;
+import tuwien.auto.calimero.link.KNXNetworkLink;
+import tuwien.auto.calimero.link.medium.KNXMediumSettings;
+import tuwien.auto.calimero.mgmt.Destination;
+import tuwien.auto.calimero.mgmt.KNXDisconnectException;
+import tuwien.auto.calimero.mgmt.ManagementProcedures;
+import tuwien.auto.calimero.mgmt.ManagementProceduresImpl;
+
+import java.net.UnknownHostException;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Optional;
+
+import static org.fusesource.jansi.Ansi.*;
+import static org.selfbus.updater.logging.Color.*;
+import static org.selfbus.updater.Mcu.MAX_FLASH_ERASE_TIMEOUT;
+import static org.selfbus.updater.logging.Markers.CONSOLE_GUI_ONLY;
+import static org.selfbus.updater.logging.Markers.CONSOLE_GUI_NO_NEWLINE;
+import static org.selfbus.updater.upd.UPDProtocol.DATA_POSITION;
+
+/**
+ * Provides methods to send firmware update telegrams to the bootloader (MCU)
+ */
+public class DeviceManagement implements AutoCloseable {
+ /**
+ * EraseCode for the APCI_MASTER_RESET_PDU (valid from 1..7)
+ */
+ private static final int RESTART_ERASE_CODE = 7;
+ /**
+ * Channelnumber for the APCI_MASTER_RESET_PDU
+ */
+ private static final int RESTART_CHANNEL = 255;
+
+ /**
+ * Default maximum retries a UPD command is sent to the client
+ */
+ private static final int MAX_UPD_COMMAND_RETRY = 3;
+
+ private UDPProtocolVersion protocolVersion;
+
+ private int blockSize;
+ private int maxPayload;
+ private int updSendDataOffset;
+
+ private final static Logger logger = LoggerFactory.getLogger(DeviceManagement.class);
+ /**
+ * Calimero device management client
+ */
+ private SBManagementClientImpl mc;
+ private Destination progDestination;
+ protected KNXNetworkLink link;
+
+ private IndividualAddress progDevice;
+
+ protected CliOptions cliOptions;
+
+ private DeviceManagement () {
+ setProtocolVersion(UDPProtocolVersion.UDP_V1);
+ }
+
+ public DeviceManagement(CliOptions cliOptions) {
+ this();
+ this.cliOptions = cliOptions;
+ }
+
+ public void reconnect(final int waitMs) throws KNXException, UpdaterException, UnknownHostException, InterruptedException {
+ close();
+ if (waitMs > 0) {
+ logger.info("Reconnecting in {}ms", waitMs);
+ Thread.sleep(waitMs);
+ }
+ else {
+ logger.info("Reconnecting now");
+ }
+ open();
+ }
+
+ public void reconnect() throws KNXException, UpdaterException, UnknownHostException, InterruptedException {
+ reconnect(cliOptions.getReconnectMs());
+ }
+
+ public void open() throws KNXException, UpdaterException, UnknownHostException, InterruptedException {
+ close();
+ this.link = new SBKNXLink(this.cliOptions).openLink();
+ this.progDevice = cliOptions.getProgDevicePhysicalAddress();
+ this.mc = new SBManagementClientImpl(this.link);
+ this.mc.setPriority(cliOptions.getPriority());
+ this.progDestination = this.mc.createDestination(progDevice, true, false, false);
+ }
+
+ @Override
+ public void close() {
+ logger.debug("Closing {}", this.getClass().getSimpleName());
+ if (progDestination != null) {
+ logger.debug("Releasing progDestination {}", progDestination);
+ progDestination.close();
+ }
+
+ if (mc != null) {
+ logger.debug("Releasing mc {}", mc);
+ mc.close(); // mc.close calls already mc.detach()
+ }
+ if (link != null) {
+ logger.debug("Releasing link {}", link);
+ link.close();
+ }
+ progDestination = null;
+ progDevice = null;
+ mc = null;
+ link = null;
+ }
+
+ public void restartProgrammingDevice()
+ throws KNXTimeoutException, KNXLinkClosedException, InterruptedException {
+ logger.info("Restarting device {}", progDestination);
+ mc.restart(progDestination);
+ }
+
+ private void waitRestartTime(int restartTimeSeconds) throws InterruptedException {
+ while (restartTimeSeconds > 0) {
+ Thread.sleep(1000);
+ logger.info(CONSOLE_GUI_NO_NEWLINE, String.format("%s.%s", ansi().fgBright(OK), ansi().reset()));
+ restartTimeSeconds--;
+ }
+ logger.info(CONSOLE_GUI_ONLY, ""); // Just a new line
+ }
+
+ /**
+ * Restarts the
+ * Includes sequences for text styles, colors and some cursor movements.
+ *
+ *
+ *
+ * Great overview on ANSI escape codes and their usage:
+ * ANSI escape codes
+ *
+ * The method processes ANSI color codes and applies the corresponding text styles to the content of the JTextPane.
+ * For example, a string like "\033[0;31m this text will be red, \033[44m now with a blue background"
+ * will be parsed and displayed with the appropriate styles in the JTextPane.
+ * Warning: This method is partially implemented. Some cursor movement codes are not yet handled and will throw an exception if encountered. When encountering an unsupported ANSI cursor movement code, an {@link IllegalStateException} is thrown.
+ * This method parses ANSI color codes and applies the styles to the {@link #stringStyle}, such as
+ * setting text to bold, italic, underlined, strikethrough, and changing foreground and background colors.
+ * Warning: This method is intended for unit tests only. Warning: This method is intended for unit tests only.device running in normal into the Bootloader mode
- * @param device
- * the IndividualAddress of the device to restart
- * @return true if successful, otherwise false
- */
- public boolean restartDeviceToBootloader(IndividualAddress device)
- throws KNXLinkClosedException {
- Destination dest = this.mc.createDestination(device, true, false, false);
- int restartProcessTime = Mcu.DEFAULT_RESTART_TIME_SECONDS;
- try {
- logger.info("Restarting device {} into bootloader mode using {}", device, this.link);
- restartProcessTime = this.mc.restart(dest, RESTART_ERASE_CODE, RESTART_CHANNEL);
- logger.info("Device {} reported {}{} second(s){} for restarting", device, ConColors.BRIGHT_GREEN, restartProcessTime, ConColors.RESET);
- waitRestartTime(restartProcessTime);
- System.out.println();
- return true;
- } catch (final KNXException | InterruptedException e) {
- logger.info("{}Restart state of device {} unknown. {}{}", ConColors.BRIGHT_RED, device, e.getMessage(), ConColors.RESET);
- logger.debug("KNXException ", e);
- logger.info("Waiting {}{} seconds{} for device {} to restart", ConColors.BRIGHT_GREEN, restartProcessTime, ConColors.RESET, device);
- waitRestartTime(restartProcessTime);
- } finally {
- dest.close();
- }
- return false;
- }
-
- public byte[] requestUIDFromDevice()
- throws KNXTimeoutException, KNXLinkClosedException, KNXDisconnectException, KNXRemoteException, InterruptedException, UpdaterException {
- logger.info("\nRequesting UID from {}...", progDestination.getAddress());
- byte[] result = sendWithRetry(UPDCommand.REQUEST_UID, new byte[0], getMaxUpdCommandRetry()).data();
- if (result[COMMAND_POSITION] != UPDCommand.RESPONSE_UID.id) {
- UPDProtocol.checkResult(result, true);
- restartProgrammingDevice();
- throw new UpdaterException(String.format("Requesting UID failed! result[%d]=0x%02X", COMMAND_POSITION, result[COMMAND_POSITION]));
- }
-
- byte[] uid;
- if ((result.length >= UPDProtocol.UID_LENGTH_USED) && (result.length <= UPDProtocol.UID_LENGTH_MAX)){
- uid = Arrays.copyOfRange(result, DATA_POSITION, UPDProtocol.UID_LENGTH_USED + DATA_POSITION);
- logger.info(" got: {} length {}", Utils.byteArrayToHex(uid), uid.length);
- return uid;
- } else {
- uid = Arrays.copyOfRange(result, DATA_POSITION, result.length - DATA_POSITION);
- logger.error("Request UID failed {} result.length={}, UID_LENGTH_USED={}, UID_LENGTH_MAX={}",
- Utils.byteArrayToHex(uid),uid.length, UPDProtocol.UID_LENGTH_USED, UPDProtocol.UID_LENGTH_MAX);
- restartProgrammingDevice();
- throw new UpdaterException("Selfbus update failed.");
- }
- }
-
- public BootloaderIdentity requestBootloaderIdentity()
- throws KNXTimeoutException, KNXLinkClosedException, KNXDisconnectException, KNXRemoteException, InterruptedException, UpdaterException {
- logger.info("Requesting Bootloader Identity...");
-
- byte[] telegram = new byte[2];
- telegram[0] = (byte) ToolInfo.versionMajor();
- telegram[1] = (byte) ToolInfo.versionMinor();
-
- byte[] result = sendWithRetry(UPDCommand.REQUEST_BL_IDENTITY, telegram, getMaxUpdCommandRetry()).data();
- if (result[COMMAND_POSITION] != UPDCommand.RESPONSE_BL_IDENTITY.id)
- {
- if (result[COMMAND_POSITION] == UPDCommand.RESPONSE_BL_VERSION_MISMATCH.id) {
- long minMajorVersion = result[DATA_POSITION] & 0xff;
- long minMinorVersion = result[DATA_POSITION + 1] & 0xff;
- logger.error("{}Selfbus Updater version {} is not compatible. Please update to version {}.{} or higher.{}",
- ConColors.RED, ToolInfo.getVersion(), minMajorVersion, minMinorVersion, ConColors.RESET);
- }
- else {
- UPDProtocol.checkResult(result);
- }
- restartProgrammingDevice();
- throw new UpdaterException("Requesting Bootloader Identity failed!");
- }
-
- BootloaderIdentity bl = BootloaderIdentity.fromArray(Arrays.copyOfRange(result, DATA_POSITION, result.length));
- logger.info(" Device Bootloader: {}{}{}", ConColors.BRIGHT_YELLOW, bl, ConColors.RESET);
-
- boolean versionsMatch = (bl.getVersionMajor() > ToolInfo.minMajorVersionBootloader()) ||
- ((bl.getVersionMajor() == ToolInfo.minMajorVersionBootloader()) && (bl.getVersionMinor() >= ToolInfo.minMinorVersionBootloader()));
-
- if (!versionsMatch)
- {
- logger.error("{}Bootloader version {} is not compatible, please update Bootloader to version {} or higher{}",
- ConColors.RED, bl.getVersion(), ToolInfo.minVersionBootloader(), ConColors.RESET);
- throw new UpdaterException("Bootloader version not compatible!");
- }
- return bl;
- }
-
- public BootDescriptor requestBootDescriptor()
- throws KNXTimeoutException, KNXLinkClosedException, KNXDisconnectException, KNXRemoteException, InterruptedException, UpdaterException {
- logger.info("Requesting Boot Descriptor...");
- byte[] result = sendWithRetry(UPDCommand.REQUEST_BOOT_DESC, new byte[0], getMaxUpdCommandRetry()).data();
- if (result[COMMAND_POSITION] != UPDCommand.RESPONSE_BOOT_DESC.id) {
- UPDProtocol.checkResult(result);
- restartProgrammingDevice();
- throw new UpdaterException(String.format("Boot descriptor request failed! result[%d]=0x%02X, result[%d]=0x%02X",
- COMMAND_POSITION, result[COMMAND_POSITION],
- DATA_POSITION, result[DATA_POSITION]));
- }
- BootDescriptor bootDescriptor = BootDescriptor.fromArray(Arrays.copyOfRange(result, DATA_POSITION, result.length));
- logger.info(" Current firmware: {}", bootDescriptor);
- return bootDescriptor;
- }
-
- public String requestAppVersionString()
- throws KNXTimeoutException, KNXLinkClosedException, KNXDisconnectException, KNXRemoteException, InterruptedException, UpdaterException {
- byte[] result = sendWithRetry(UPDCommand.APP_VERSION_REQUEST, new byte[0], getMaxUpdCommandRetry()).data();
- if (result[COMMAND_POSITION] != UPDCommand.APP_VERSION_RESPONSE.id){
- UPDProtocol.checkResult(result);
- return null;
- }
- return new String(result,DATA_POSITION,result.length - DATA_POSITION); // Convert 12 bytes to string starting from result[DATA_POSITION];
- }
-
- public void unlockDeviceWithUID(byte[] uid)
- throws KNXTimeoutException, KNXLinkClosedException, KNXDisconnectException, KNXRemoteException, InterruptedException, UpdaterException {
- logger.info("Unlocking device {} with UID {}...", progDestination.getAddress(), Utils.byteArrayToHex(uid));
- byte[] result = sendWithRetry(UPDCommand.UNLOCK_DEVICE, uid, getMaxUpdCommandRetry()).data();
- if (UPDProtocol.checkResult(result) != UDPResult.IAP_SUCCESS.id) {
- restartProgrammingDevice();
- throw new UpdaterException("Selfbus update failed.");
- }
- }
-
- public void eraseAddressRange(long startAddress, long totalLength)
- throws KNXLinkClosedException, InterruptedException, UpdaterException, KNXTimeoutException {
- long endAddress = startAddress + totalLength - 1;
- byte[] telegram = new byte[8];
- Utils.longToStream(telegram, 0 , startAddress);
- Utils.longToStream(telegram, 4 , endAddress);
- logger.info(String.format("Erasing firmware address range: 0x%04X - 0x%04X...", startAddress, endAddress));
- Duration oldResponseTimeout = mc.responseTimeout();
- Duration newResponseTimeout = MAX_FLASH_ERASE_TIMEOUT.multipliedBy(2);
- if (oldResponseTimeout.compareTo(newResponseTimeout) < 0) {
- mc.responseTimeout(newResponseTimeout); // temporarily increase responseTimeout
- logger.trace("mc.ResponseTimeout temporarily increased to {}", mc.responseTimeout());
- }
-
- byte[] result = sendWithRetry(UPDCommand.ERASE_ADDRESS_RANGE, telegram, getMaxUpdCommandRetry()).data();
-
- mc.responseTimeout(oldResponseTimeout); // restore responseTimeout
- logger.trace("mc.ResponseTimeout restored to {}", mc.responseTimeout());
-
- if (UPDProtocol.checkResult(result) != UDPResult.IAP_SUCCESS.id) {
- restartProgrammingDevice();
- throw new UpdaterException("Erasing firmware address range failed.");
- }
- }
-
- public void eraseFlash()
- throws KNXLinkClosedException, InterruptedException, UpdaterException, KNXTimeoutException {
- byte[] result = sendWithRetry(UPDCommand.ERASE_COMPLETE_FLASH, new byte[0], getMaxUpdCommandRetry()).data();
- if (UPDProtocol.checkResult(result) != UDPResult.IAP_SUCCESS.id) {
- restartProgrammingDevice();
- throw new UpdaterException("Deleting the entire flash failed.");
- }
- }
-
- public void dumpFlashRange(long startAddress, long endAddress)
- throws KNXLinkClosedException, InterruptedException, UpdaterException, KNXTimeoutException {
- byte[] telegram = new byte[8];
- Utils.longToStream(telegram, 0 , startAddress);
- Utils.longToStream(telegram, 4 , endAddress);
- // sendWithRetry will always time out, because the mcu is busy dumping the flash
- byte[] result = sendWithRetry(UPDCommand.DUMP_FLASH, telegram, 0).data();
- if (UPDProtocol.checkResult(result) != UDPResult.IAP_SUCCESS.id) {
- restartProgrammingDevice();
- throw new UpdaterException("Flash dumping failed.");
- }
- }
-
- public ResponseResult doFlash(byte[] data, int maxRetry, int delay)
- throws UpdaterException, KNXLinkClosedException, InterruptedException, KNXTimeoutException {
- int nIndex = 0;
- ResponseResult result = new ResponseResult();
- while (nIndex < data.length)
- {
- byte[] txBuffer;
- if ((data.length + updSendDataOffset - nIndex) >= maxPayload) {
- txBuffer = new byte[maxPayload];
- }
- else {
- txBuffer = new byte[data.length + updSendDataOffset- nIndex];
- }
-
- if (protocolVersion == UDPProtocolVersion.UDP_V0) {
- txBuffer[0] = (byte) nIndex; // First byte contains mcu's ramBuffer start position to copy to
- }
- System.arraycopy(data, nIndex, txBuffer, updSendDataOffset, txBuffer.length - updSendDataOffset);
-
- ResponseResult tmp = sendWithRetry(UPDCommand.SEND_DATA, txBuffer, maxRetry);
- result.addCounters(tmp);
-
- if ((tmp.dropCount() > 0) || (tmp.timeoutCount() > 0)) {
- logger.warn("{}x{}", ConColors.RED, ConColors.RESET);
- continue;
- }
-
- if (UPDProtocol.checkResult(tmp.data(), false) != UDPResult.IAP_SUCCESS.id) {
- restartProgrammingDevice();
- throw new UpdaterException("doFlash failed.");
- }
-
- nIndex += txBuffer.length - updSendDataOffset;
-
- if (delay > 0) {
- Thread.sleep(delay); //Reduce bus load during data upload, without 2:04, 50ms 2:33, 60ms 2:41, 70ms 2:54, 80ms 3:04
- }
- }
- result.addWritten(nIndex);
- return result;
- }
-
- public ResponseResult programBootDescriptor(BootDescriptor bootDescriptor, int delay)
- throws UpdaterException, KNXLinkClosedException, InterruptedException, KNXTimeoutException {
-
- byte[] streamBootDescriptor = bootDescriptor.toStream();
- // send new boot descriptor
- ResponseResult flashResult = doFlash(streamBootDescriptor, getMaxUpdCommandRetry(), delay);
- if (flashResult.written() != streamBootDescriptor.length) {
- throw new UpdaterException(String.format("Sending Boot descriptor (length %d) failed. Wrote %d", streamBootDescriptor.length, flashResult.written()));
- }
- if (delay > 0) {
- Thread.sleep(delay);
- }
- int crc32Value = Utils.crc32Value(streamBootDescriptor);
- byte[] programBootDescriptor = new byte[8];
- Utils.longToStream(programBootDescriptor, 0, streamBootDescriptor.length);
- Utils.longToStream(programBootDescriptor, 4, crc32Value);
- System.out.println();
- logger.info(String.format("Updating boot descriptor with crc32 0x%08X, length %d",
- crc32Value, streamBootDescriptor.length));
- ResponseResult programResult = sendWithRetry(UPDCommand.UPDATE_BOOT_DESC, programBootDescriptor, getMaxUpdCommandRetry());
- if (UPDProtocol.checkResult(programResult.data()) != UDPResult.IAP_SUCCESS.id) {
- restartProgrammingDevice();
- throw new UpdaterException("Updating boot descriptor failed.");
- }
- programResult.addCounters(flashResult);
- if (delay > 0) {
- Thread.sleep(delay);
- }
- return programResult;
- }
-
- public void requestBootLoaderStatistic()
- throws KNXTimeoutException, KNXLinkClosedException, KNXDisconnectException, KNXRemoteException, InterruptedException, UpdaterException {
-
- byte[] result = sendWithRetry(UPDCommand.REQUEST_STATISTIC, new byte[0], getMaxUpdCommandRetry()).data();
- if (result[COMMAND_POSITION] == UPDCommand.RESPONSE_STATISTIC.id)
- {
- BootloaderStatistic blStatistic = BootloaderStatistic.fromArray(Arrays.copyOfRange(result, DATA_POSITION, result.length));
- logger.info(" Bootloader: {}", blStatistic);
- }
- else {
- logger.warn(" {}{}{}", ConColors.RED,
- String.format("Requesting Bootloader statistic failed! result[%d]=0x%02X, result[%d]=0x%02X",
- COMMAND_POSITION, result[COMMAND_POSITION],
- DATA_POSITION, result[DATA_POSITION]), ConColors.RESET);
- }
- }
-
- public ResponseResult sendWithRetry(UPDCommand command, byte[] data, int maxRetry)
- throws UpdaterException {
- ResponseResult result = new ResponseResult();
- while (true) {
- try {
- byte[] data2 = mc.sendUpdateData(progDestination, command.id, data);
- result.copyFromArray(data2);
- return result;
- }
- catch (KNXTimeoutException e) {
- logger.warn("{}{} {} : {}{}", ConColors.RED, command, e.getMessage(), e.getClass().getSimpleName(), ConColors.RESET);
- result.incTimeoutCount();
- }
- catch (KNXDisconnectException | KNXRemoteException e) { ///\todo check causes of KNXRemoteException, if think they are unrecoverable
- logger.warn("{}{} {} : {}{}", ConColors.RED, command, e.getMessage(), e.getClass().getSimpleName(), ConColors.RESET);
- result.incDropCount();
- }
- catch (KNXIllegalArgumentException e) {
- throw new UpdaterException(String.format("%s failed.", command), e);
- }
- catch (Throwable e) {
- throw new UpdaterException(String.format("%s failed.", command), e);
- // logger.error("{}{} Exception {}{}", ConColors.RED, command, e, ConColors.RESET);
- }
-
- if (maxRetry > 0) {
- maxRetry--;
- }
-
- if (maxRetry == 0)
- {
- throw new UpdaterException(String.format("%s failed.", command));
- }
- }
- }
-
- public void checkDeviceInProgrammingMode(IndividualAddress progDeviceAddr) throws UpdaterException {
- try {
- ManagementProcedures mgmt = new ManagementProceduresImpl(link);
- IndividualAddress[] devices = mgmt.readAddress();
- mgmt.detach();
- mgmt.close();
- if ((devices.length == 0) && (progDeviceAddr == null)) { // no device in prog mode
- return;
- }
- else if ((devices.length == 1) && (progDeviceAddr != null) && (progDeviceAddr.equals(devices[0]))) { // correct device in prog mode
- return;
- }
- logger.warn("{}{} Device(s) in bootloader/programming mode: {}{}", ConColors.BRIGHT_RED, devices.length, Arrays.stream(devices).toArray(), ConColors.RESET);
- if (devices.length == 0) {
- throw new UpdaterException("No device in programming mode.");
- }
- else {
- throw new UpdaterException(String.format("%d wrong device(s) %s are already in programming mode.", devices.length, Arrays.toString(devices)));
- }
- } catch (KNXException | InterruptedException e ) {
- throw new UpdaterException(String.format("checkDevicesInProgrammingMode failed. %s", e.getMessage()));
- }
- }
-
- public UDPProtocolVersion getProtocolVersion() {
- return protocolVersion;
- }
-
- public void setProtocolVersion(UDPProtocolVersion protocolVersion) {
- this.protocolVersion = protocolVersion;
- this.maxPayload = Mcu.MAX_PAYLOAD;
- if (this.protocolVersion == UDPProtocolVersion.UDP_V1) {
- this.blockSize = Mcu.UPD_PROGRAM_SIZE;
- this.updSendDataOffset = 0;
- }
- else {
- this.blockSize = Mcu.FLASH_PAGE_SIZE;
- this.updSendDataOffset = 1;
- }
- }
-
- public int getBlockSize() {
- return blockSize;
- }
-
- public boolean setBlockSize(int blockSize) {
- if (this.protocolVersion == UDPProtocolVersion.UDP_V0) {
- return false;
- }
- this.blockSize = blockSize;
- return true;
- }
-
- public int getMaxPayload() {
- return maxPayload;
- }
-
- public int getMaxUpdCommandRetry() {
- return MAX_UPD_COMMAND_RETRY;
- }
-}
diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/DiscoverKnxInterfaces.java b/firmware_updater/updater/source/src/main/java/org/selfbus/updater/DiscoverKnxInterfaces.java
deleted file mode 100644
index 49f229fd..00000000
--- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/DiscoverKnxInterfaces.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package org.selfbus.updater;
-
-import tuwien.auto.calimero.knxnetip.Discoverer;
-import tuwien.auto.calimero.knxnetip.servicetype.SearchResponse;
-
-import java.time.Duration;
-import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-
-public class DiscoverKnxInterfaces {
- public static Listoptions.
- *
- *
- * This tool supports KNX network access using a KNXnet/IP connection FT1.2 or TPUART
- * connection. It uses the {@link SBManagementClientImpl} functionality of the library
- * to read KNX device description, properties, and memory locations. It collects
- * and shows device information similar to the ETS.
- * main- method of
- * this class is invoked, otherwise use this class in the context appropriate to
- * a {@link Runnable}.
- * In console mode, the KNX device information, as well as errors and problems
- * during its execution are written to logback LOGGER.
- * null otherwise
- * @param canceled
- * whether the operation got canceled before its planned end
- */
- protected void onCompletion(final Exception thrown, final boolean canceled) {
- if (canceled)
- logger.info("reading device info canceled");
- if (thrown != null) {
- logger.error("Operation did not finish.", thrown);
- }
- }
-
- private static final class ShutdownHandler extends Thread {
- private final Thread t = Thread.currentThread();
-
- ShutdownHandler register() {
- Runtime.getRuntime().addShutdownHook(this);
- return this;
- }
-
- void unregister() {
- Runtime.getRuntime().removeShutdownHook(this);
- }
-
- public void run() {
- t.interrupt();
- }
- }
-
- private static void printStatisticData(long flashTimeStart, ResponseResult result) {
- // logging of some static data
- long flashTimeDuration = System.currentTimeMillis() - flashTimeStart;
- float bytesPerSecond = (float) result.written() / (flashTimeDuration / 1000f);
- String col;
- if (bytesPerSecond >= 50.0) {
- col = ConColors.BRIGHT_GREEN;
- } else {
- col = ConColors.BRIGHT_RED;
- }
- String infoMsg = String.format("Wrote %d bytes from file to device in %tM:%
\
-und das Gerät wurde in den programmierbaren Modus versetzt.
-appDeviceHint=Das Gerät wurde bereits mit einer Applikation
\
-über den Bootloader geflasht und soll nun aktualisiert werden.
\
-Dazu muss die physikalische Adresse und das Applikationsprogramm
\
-in der ETS geschrieben worden sein.
-eraseCompleteFlashHint=MIT VORSICHT VERWENDEN! Es wird der komplette
\
-Speicher gelöscht, inklusive der physikalischen KNX Adresse
\
-und allen Einstellungen. Nur der Bootloader bleibt bestehen.
-noFlashHint=Flashen der Firmware deaktivieren
-uidHint=die UID des Selfbus Gerätes
-fileNameHint=Der Dateipfad kann manuell bearbeitet werden
\
-(Enter zum bestätigen)
-ipAddrHint=IP Adresse der KNX Schnittstelle
-portHint=TCP/UDP Portnummer der KNX Schnittstelle (default 3671)
-stopFlash=Stoppe Flashvorgang
-medium=Medium
-serial=Seriell
-tpuart=TpUart
-knxDeviceAddr=Geräteadresse
-knxProgDeviceAddr=Geräteadresse im Bootloader
-knxOwnAddress=eigene KNX Adresse
-port=Port
-useNat=NAT
-knxMessageDelay=Verzögerung [ms]
-knxTimeout=Timeout
-eraseFlash=Lösche kompletten Flash
-noFlash=keine Daten übertragen
-requestUid=Erfasse UID von Gerät
-requestUidHint=Die UID wird von einem Selfbus Gerät abgefragt
\
-und angezeigt. Das Selfbus Gerät muss sich
\
-im programmierbaren Modus im Bootloader befinden
-UpdaterSettings=Updater
-KnxBusSettings=KNX Bus
-KnxGatewayConnectionSettings=KNX Schnittstelle
-advancedSettings=Erweiterte Einstellungen
-knxSecureUserPwd=Benutzer Passwort
-messagePriority=KNX Telegramm Priorität
-knxSecureUser=KNX Secure Benutzer
-knxSecureUserHint=KNX IP Secure tunneling Benutzer Identifikator
\
-(1..127) (default 1)
-knxSecureUserPwdHint=\
-KNX IP Secure tunneling Benutzer Passwort
\
-(Inbetriebnahmepasswort), Anführungszeichen
\
-(") im Passwort könnten nicht funktionieren\
-
-knxSecureDevicePwd=Gerätepasswort
-knxSecureDevicePwdHint=\
-KNX IP Secure Geräte Authentifizierung Code
\
-(Authentifizierungscode), Anführungszeichen
\
-(") im Passwort könnten nicht funktionieren\
-
-reloadKnxIpGateways=Gateways erneut laden
-selectInterface=wähle Schnittstelle
-diffFlash=differenzieller Flashvorgang
-diffFlashHint=Es werden nur veränderte Teile der Software
\
-übertragen [experimentell]
\ No newline at end of file
diff --git a/firmware_updater/updater/source/src/main/resources/GuiTranslation_en.properties b/firmware_updater/updater/source/src/main/resources/GuiTranslation_en.properties
deleted file mode 100644
index 12264956..00000000
--- a/firmware_updater/updater/source/src/main/resources/GuiTranslation_en.properties
+++ /dev/null
@@ -1,65 +0,0 @@
-loadFile=load file
-fileName=file name
-selectKnxIpGateway=select KNX IP gateway
-ipAddress=IP address
-scenario=scenario
-uid=UID
-startFlash=start flash
-language=language
-newDevice=new device
-appDevice=device with application
-allOptions=all options
-newDeviceHint=There is a new device with flashed bootloader
\
-and it is in programming mode.
-appDeviceHint=A device is flashed with an application over
\
-bootloader and now it should be updated
-eraseCompleteFlashHint=USE WITH CAUTION! Erases the complete flash memory
\
-including the physical KNX address and all settings of
\
-the device. Only the bootloader is not deleted.
-noFlashHint=disable flashing firmware
-uidHint=the UID of the selfbus device
-fileNameHint=you can modify the file path manually
-ipAddrHint=IP address of the KNX interface
-portHint=UDP port on
\
-The selfbus device has to be in programmable
\
-mode into the bootloader
-UpdaterSettings=Updater settings
-KnxBusSettings=KNX bus settings
-KnxGatewayConnectionSettings=KNX gateway connection settings
-advancedSettings=Advanced settings
-knxSecureUserPwd=User password
-messagePriority=KNX telegram priority
-knxSecureUserHint=KNX IP Secure tunneling user identifier
\
-(1..127) (default 1)
-knxSecureUserPwdHint=\
-KNX IP Secure tunneling user password
\
-(Commissioning password/Inbetriebnahmepasswort),
\
-quotation marks (") in password may not work\
-
-knxSecureDevicePwd=Device password
-knxSecureDevicePwdHint=\
-KNX IP Secure device authentication code
\
-(Authentication Code/Authentifizierungscode)
\
-quotation marks(") in password may not work\
-
-reloadKnxIpGateways=reload gateways
-selectInterface=select Interface
-diffFlash=differential Flash
-diffFlashHint=flash only changed parts of software
\
-[experimental]
\ No newline at end of file
diff --git a/firmware_updater/updater/source/src/main/resources/logback.xml b/firmware_updater/updater/source/src/main/resources/logback.xml
deleted file mode 100644
index f9b4fde9..00000000
--- a/firmware_updater/updater/source/src/main/resources/logback.xml
+++ /dev/null
@@ -1,65 +0,0 @@
-
-
-options.
+ *
+ *
+ * This tool supports KNX network access using a KNXnet/IP, USB, FT1.2 or TPUART
+ * connection. It uses the {@link DeviceManagement} to access the Selfbus KNX device.
+ *
+ */
+public class Updater implements Runnable {
+ @SuppressWarnings("unused")
+ private Updater() {} // disable default constructor
+
+ private final static String UPDATER_README_LINK = "https://github.com/selfbus/software-arm-lib/blob/main/firmware_updater/updater/README.md";
+
+ private final static Logger logger = LoggerFactory.getLogger(Updater.class);
+ private CliOptions cliOptions = null;
+ private static DeviceManagement dm = null;
+
+ /**
+ * Constructs an instance of the {@link #Updater} class.
+ *
+ * @param cliOptions the command-line options to be used
+ */
+ public Updater(CliOptions cliOptions) {
+ logger.debug(ToolInfo.getFullInfo());
+ logger.debug(Settings.getLibraryHeader(false));
+ this.cliOptions = cliOptions;
+ }
+
+ public static void initJansi() {
+ AnsiConsole.systemInstall();
+ }
+
+ public static void finalizeJansi() {
+ System.out.print(AnsiCursor.on()); // make sure we enable the cursor
+ AnsiConsole.systemUninstall();
+ }
+
+ public static void main(final String[] args) {
+ initJansi();
+
+ if (args.length == 0) {
+ if (!LoggingManager.isRunningInIntelliJ()) {
+ logger.info("GUI start => console output set to log level {}", Level.WARN);
+ LoggingManager.setThresholdFilterLogLevel(CONSOLE_APPENDER_NAME, Level.WARN);
+ }
+ GuiMain.startSwingGui();
+ finalizeJansi();
+ return;
+ }
+
+ try {
+ logger.info("{}{}{}", ansi().fgBright(OK).bold(), ToolInfo.getToolAndVersion(), ansi().reset());
+ // read in user-supplied command line options
+ CliOptions options = new CliOptions(args, ToolInfo.getToolJarName() ,
+ "Selfbus KNX-Firmware update tool options", "");
+ if (options.getVersionIsSet()) {
+ logger.info("{}{}{}", ansi().fgBright(OK).bold(), Credits.getAsciiLogo(), ansi().reset());
+ logger.info("{}{}{}", ansi().fgBright(OK).bold(), Credits.getAuthors(), ansi().reset());
+ ToolInfo.showVersion();
+ finalizeJansi();
+ return;
+ }
+
+ if (options.getHelpIsSet()) {
+ logger.info(options.helpToString());
+ finalizeJansi();
+ return;
+ }
+
+ if (options.getDiscoverIsSet()) {
+ logger.info("Discovering KNX network...");
+ DiscoverKnxInterfaces.logIPInterfaces(DiscoverKnxInterfaces.getAllnetIPInterfaces());
+ DiscoverKnxInterfaces.logUSBInterfaces(DiscoverKnxInterfaces.getUsbInterfaces());
+ DiscoverKnxInterfaces.logCOMPorts(DiscoverKnxInterfaces.getCOMPorts());
+ finalizeJansi();
+ return;
+ }
+
+ final Updater updater = new Updater(options);
+ final ShutdownHandler shutdownHandler = new ShutdownHandler().register();
+ updater.run();
+ shutdownHandler.unregister();
+ }
+ catch (KNXFormatException | ParseException | InterruptedException e) {
+ logExceptionUserFriendly(e);
+ }
+ finally {
+ if (dm != null) {
+ dm.close();
+ }
+ finalizeJansi();
+ logger.debug("main exit");
+ }
+ }
+
+ private static final class ShutdownHandler extends Thread {
+ private final Thread t = Thread.currentThread();
+
+ ShutdownHandler register() {
+ Runtime.getRuntime().addShutdownHook(this);
+ logger.trace("ShutdownHandler registered");
+ return this;
+ }
+
+ void unregister() {
+ try {
+ Runtime.getRuntime().removeShutdownHook(this);
+ logger.trace("ShutdownHandler unregistered");
+ }
+ catch (IllegalStateException ignored) {
+ // Shutdown is currently in progress
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+ t.interrupt();
+ logger.trace("ShutdownHandler called");
+ if (dm != null) {
+ dm.close();
+ dm = null;
+ }
+ }
+ catch (Exception e) {
+ logger.error("Error while shutting down", e);
+ }
+ }
+ }
+
+ /**
+ * Minimum delay between two UPDCommand.SEND_DATA telegrams in milliseconds
+ */
+ public static final int DELAY_MIN = 0;
+ /**
+ * Maximum delay between two UPDCommand.SEND_DATA telegrams in milliseconds
+ */
+ public static final int DELAY_MAX = 500;
+ /**
+ * Physical address the bootloader is using
+ */
+ public static final IndividualAddress PHYS_ADDRESS_BOOTLOADER = new IndividualAddress(15, 15,192);
+ /**
+ * Physical address the Selfbus Updater is using
+ */
+ public static final IndividualAddress PHYS_ADDRESS_OWN = new IndividualAddress(0, 0,0);
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Runnable#run()
+ */
+ @Override
+ public void run() {
+ try {
+ final String hexFileName = cliOptions.getFileName();
+ BinImage newFirmware = null;
+
+ if (!hexFileName.isEmpty()) {
+ // check if the firmware file exists
+ if (!Utils.fileExists(hexFileName)) {
+ throw new UpdaterException(String.format("File '%s' does not exist!", cliOptions.getFileName()));
+ }
+ // Load Firmware hex file
+ logger.info("Loading file '{}'", hexFileName);
+ newFirmware = BinImage.readFromHex(hexFileName);
+ // Check for APP_VERSION string in new firmware
+ if (newFirmware.getAppVersion().isEmpty()) {
+ throw new UpdaterException("Missing APP_VERSION string in firmware!");
+ }
+ }
+ else {
+ logger.info("{}No firmware file (*.hex) specified! Specify with --{}{}",
+ ansi().fgBright(WARN), CliOptions.OPT_LONG_FILENAME, ansi().reset());
+ logger.info("{}Reading only device information{}", ansi().fgBright(INFO), ansi().reset());
+ }
+
+ dm = DeviceManagementFactory.getDeviceManagement(cliOptions);
+
+ logger.debug("Telegram priority: {}", cliOptions.getPriority());
+ dm.open();
+ logger.info("KNX connection: {}", dm.getLinkInfo());
+
+ //for option --device restart the device in bootloader mode
+ if (cliOptions.getDevicePhysicalAddress() != null) { // phys. knx address of the device in normal operation
+ dm.checkDeviceInProgrammingMode(null); // check that before no device is in programming mode
+ dm.restartDeviceToBootloader(cliOptions.getDevicePhysicalAddress());
+ }
+
+ dm.checkDeviceInProgrammingMode(cliOptions.getProgDevicePhysicalAddress());
+ String uid = cliOptions.getUid();
+ if (uid.isEmpty()) {
+ uid = dm.requestUIDFromDevice();
+ }
+
+ dm.unlockDeviceWithUID(uid);
+
+ if ((cliOptions.getDumpFlashStartAddress() >= 0) && (cliOptions.getDumpFlashEndAddress() >= 0)) {
+ logger.warn("{}Dumping flash content range {} to bootloader's serial port.{}",
+ ansi().fgBright(OK),
+ String.format("0x%04X-0x%04X", cliOptions.getDumpFlashStartAddress(), cliOptions.getDumpFlashEndAddress()),
+ ansi().reset());
+ dm.dumpFlashRange(cliOptions.getDumpFlashStartAddress(), cliOptions.getDumpFlashEndAddress());
+ return;
+ }
+
+ BootloaderIdentity bootLoaderIdentity = dm.requestBootloaderIdentity();
+
+ // Request current main firmware boot descriptor from device
+ BootDescriptor bootDescriptor = dm.requestBootDescriptor();
+ if (newFirmware != null) {
+ logger.info("New firmware: {}", newFirmware);
+ }
+
+ logger.debug("Requesting APP_VERSION");
+ String appVersion = dm.requestAppVersionString();
+ if (appVersion.isEmpty()) {
+ logger.info("Current APP_VERSION: {}invalid{} ", ansi().fgBright(WARN), ansi().reset());
+ } else {
+ logger.info("Current APP_VERSION: {}{}{}", ansi().fgBright(OK), appVersion, ansi().reset());
+ }
+
+ // From here on we need a valid firmware
+ if (newFirmware == null) {
+ if (cliOptions.getDevicePhysicalAddress() != null) {
+ dm.restartProgrammingDevice();
+ }
+ // to get here `uid == null` must be true, so it's fine to exit with no-error
+ dm.close();
+ return;
+ }
+
+ // store new firmware bin file in cache directory
+ String cacheFileName = FlashDiffMode.createCacheFileName(newFirmware.startAddress(), newFirmware.length(), newFirmware.crc32());
+ BinImage imageCache = BinImage.copyFromArray(newFirmware.getBinData(), newFirmware.startAddress());
+ imageCache.writeToBinFile(cacheFileName);
+
+ logger.info("File APP_VERSION : {}{}{}", ansi().fgBright(OK), newFirmware.getAppVersion(), ansi().reset());
+
+ // Check if FW image has correct offset for MCUs bootloader size
+ if (newFirmware.startAddress() < bootLoaderIdentity.applicationFirstAddress()) {
+ logger.error("{}Error! The specified firmware image would overwrite parts of the bootloader. Check FW offset setting in the linker!{}",
+ ansi().fgBright(WARN), ansi().reset());
+ throw new UpdaterException(String.format("Firmware needs to start at or beyond 0x%04X",
+ bootLoaderIdentity.applicationFirstAddress()));
+ }
+ else if (newFirmware.startAddress() == bootLoaderIdentity.applicationFirstAddress()) {
+ logger.debug("Firmware starts directly beyond bootloader.");
+ }
+ else {
+ logger.debug("Info: There are {} bytes of unused flash between bootloader and firmware.",
+ newFirmware.startAddress() - bootLoaderIdentity.applicationFirstAddress());
+ }
+
+ if (cliOptions.getEraseFullFlashIsSet()) {
+ logger.warn("{}Deleting the entire flash except from the bootloader itself!{}",
+ ansi().fgBright(WARN), ansi().reset());
+ dm.eraseFlash();
+ }
+
+ boolean diffMode = false;
+ if (!(cliOptions.getFlashingFullModeIsSet())) {
+ if (bootDescriptor.valid()) {
+ diffMode = FlashDiffMode.setupDifferentialMode(bootDescriptor);
+ }
+ else {
+ logger.warn("{} BootDescriptor is not valid -> switching to full mode{}",
+ ansi().fgBright(WARN), ansi().reset());
+ }
+ }
+
+ if ((bootLoaderIdentity.versionMajor()) <= 1 && (bootLoaderIdentity.versionMinor() < 20)) {
+ dm.setProtocolVersion(UDPProtocolVersion.UDP_V0);
+ }
+ else {
+ dm.setProtocolVersion(UDPProtocolVersion.UDP_V1);
+ }
+
+ if (!dm.setBlockSize(cliOptions.getBlockSize())) {
+ logger.info("{}Connected bootloader doesn't support block size {}. Using {} bytes.{}",
+ ansi().fgBright(INFO), cliOptions.getBlockSize(), dm.getBlockSize(), ansi().reset());
+ }
+
+ if (!cliOptions.getNoFlashIsSet()) { // is flashing firmware disabled? for debugging use only!
+ // Start to flash the new firmware
+ ResponseResult resultTotal;
+ if (diffMode && FlashDiffMode.isInitialized()) {
+ logger.warn("{}Differential mode is EXPERIMENTAL -> Use with caution.{}",
+ ansi().fgBright(WARN), ansi().reset());
+ resultTotal = FlashDiffMode.doDifferentialFlash(dm, newFirmware.startAddress(), newFirmware.getBinData());
+ }
+ else {
+ logger.debug("Starting FlashFullMode");
+ resultTotal = FlashFullMode.doFullFlash(dm, newFirmware, cliOptions.getDelayMs(), !cliOptions.getEraseFullFlashIsSet(), cliOptions.getLogStatisticsIsSet());
+ }
+ BootloaderStatistic bootloaderStatistic = dm.requestBootLoaderStatistic();
+ if (bootloaderStatistic != null) {
+ logger.info("Bootloader: #Disconnect: {} #repeated T_ACK: {}",
+ bootloaderStatistic.getDisconnectCountColored(), bootloaderStatistic.getRepeatedT_ACKcountColored());
+ }
+ BootloaderStatistic updaterStatistic = new BootloaderStatistic((int)resultTotal.dropCount(),
+ (int)resultTotal.timeoutCount());
+ logger.info("Updater : #Disconnect: {} #repeated T_ACK: {}",
+ updaterStatistic.getDisconnectCountColored(), updaterStatistic.getRepeatedT_ACKcountColored());
+ }
+ else {
+ logger.warn("--{} => {}only boot description block will be written{}", CliOptions.OPT_LONG_NO_FLASH,
+ ansi().fgBright(WARN), ansi().reset());
+ }
+
+ BootDescriptor newBootDescriptor = new BootDescriptor(newFirmware.startAddress(),
+ newFirmware.endAddress(), (int) newFirmware.crc32(), newFirmware.getAppVersionAddress());
+ logger.info("Updating boot descriptor with {}", newBootDescriptor);
+ dm.programBootDescriptor(newBootDescriptor, cliOptions.getDelayMs());
+ String deviceInfo = cliOptions.getProgDevicePhysicalAddress().toString();
+ if (cliOptions.getDevicePhysicalAddress() != null) {
+ deviceInfo = cliOptions.getDevicePhysicalAddress().toString();
+ }
+ logger.info("Finished programming device {}{}{} with '{}{}{}'",
+ ansi().fgBright(INFO), deviceInfo, ansi().reset(),
+ ansi().fgBright(INFO), shortenPath(cliOptions.getFileName(), 1), ansi().reset());
+ dm.restartProgrammingDevice();
+ dm.close();
+
+ if (newFirmware.getAppVersion().contains(BootloaderUpdater.BOOTLOADER_UPDATER_ID_STRING)) {
+ logger.info("{}Wait {} second(s) for Bootloader Updater to finish its job{}",
+ ansi().fgBright(OK),
+ String.format("%.2f", BootloaderUpdater.BOOTLOADER_UPDATER_MAX_RESTART_TIME_MS / 1000.0f),
+ ansi().reset());
+ Thread.sleep(BootloaderUpdater.BOOTLOADER_UPDATER_MAX_RESTART_TIME_MS);
+ }
+
+ logger.info("Update finished successfully.");
+ }
+ catch (final InterruptedException | IllegalStateException e) {
+ Thread.currentThread().interrupt();
+ logger.info("{}Update canceled.", System.lineSeparator());
+ logger.debug("", e); // todo see logback issue https://github.com/qos-ch/logback/issues/876
+ }
+ catch (final KNXPortClosedException e) {
+ logger.error("", e); // todo see logback issue https://github.com/qos-ch/logback/issues/876
+ logger.error("Update failed.");
+ if (e.getMessage().contains("error sending report over USB") &&
+ (!cliOptions.getUsbVendorIdAndProductId().isBlank()) &&
+ System.getProperty("os.name").startsWith("Windows")) {
+ logger.info("{}Make sure the USB Interface {} uses the WinUSB driver.{}",
+ ansi().fgBright(WARN), cliOptions.getUsbVendorIdAndProductId(), ansi().reset() );
+ logger.info("Checkout {}{}{} for more info.",
+ ansi().fgBright(INFO), UPDATER_README_LINK, ansi().reset() );
+ }
+ }
+ catch (final UpdaterException | KNXException e) {
+ logExceptionUserFriendly(e);
+ }
+ catch (final Throwable e) {
+ logger.error("", e); // todo see logback issue https://github.com/qos-ch/logback/issues/876
+ logger.error("Update did not finish.");
+ }
+ finally {
+ if (dm != null) {
+ dm.close();
+ }
+ }
+ }
+
+ public String requestUid() throws KNXException, UpdaterException, UnknownHostException {
+ try {
+ DeviceManagement dm = new DeviceManagement(cliOptions);
+ dm.open();
+
+ //for option --device restart the device in bootloader mode
+ if (cliOptions.getDevicePhysicalAddress() != null) { // phys. knx address of the device in normal operation
+ dm.checkDeviceInProgrammingMode(null); // check that before no device is in programming mode
+ dm.restartDeviceToBootloader(cliOptions.getDevicePhysicalAddress());
+ }
+
+ dm.checkDeviceInProgrammingMode(cliOptions.getProgDevicePhysicalAddress());
+
+ String uid = dm.requestUIDFromDevice();
+
+ dm.unlockDeviceWithUID(uid);
+ dm.requestBootloaderIdentity();
+ dm.requestBootDescriptor();
+ String appVersion = dm.requestAppVersionString();
+ if (appVersion.isEmpty()) {
+ logger.info("APP_VERSION: {}invalid{} ", ansi().fgBright(WARN), ansi().reset());
+ } else {
+ logger.info("APP_VERSION: {}{}{}", ansi().fgBright(OK), appVersion, ansi().reset());
+ }
+
+ if (cliOptions.getDevicePhysicalAddress() != null) {
+ dm.restartProgrammingDevice();
+ }
+ dm.close();
+ return uid;
+ }
+ catch (final InterruptedException e) {
+ logger.info("requestUid canceled.");
+ Thread.currentThread().interrupt();
+ return "";
+ }
+ catch (UpdaterException | KNXException e) {
+ logger.error("{}An error occurred while retrieving the UID. {}{}{}",
+ ansi().fgBright(WARN), System.lineSeparator(), e, ansi().reset());
+ throw e;
+ }
+ }
+
+ private static void logExceptionUserFriendly(Throwable e) {
+ logger.error("{}{}{} ({})", ansi().fgBright(WARN), e.getMessage(), ansi().reset(),
+ e.getClass().getSimpleName());
+ Throwable cause = e.getCause();
+ if (cause != null) {
+ logger.error("{}Caused by: {}{} ({})", ansi().fgBright(WARN), cause.getMessage(), ansi().reset(),
+ cause.getClass().getSimpleName());
+ }
+ logger.debug("", e); // todo see logback issue https://github.com/qos-ch/logback/issues/876
+ }
+}
diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/UpdaterException.java b/firmware_updater/updater/src/org/selfbus/updater/UpdaterException.java
similarity index 96%
rename from firmware_updater/updater/source/src/main/java/org/selfbus/updater/UpdaterException.java
rename to firmware_updater/updater/src/org/selfbus/updater/UpdaterException.java
index 638895e4..6cb6dba5 100644
--- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/UpdaterException.java
+++ b/firmware_updater/updater/src/org/selfbus/updater/UpdaterException.java
@@ -3,7 +3,6 @@
/**
* Basic exception throwable by the application
*/
-@SuppressWarnings("serial")
public class UpdaterException extends Exception {
/**
* Constructs a new UpdaterException without a detail message.
diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/Utils.java b/firmware_updater/updater/src/org/selfbus/updater/Utils.java
similarity index 80%
rename from firmware_updater/updater/source/src/main/java/org/selfbus/updater/Utils.java
rename to firmware_updater/updater/src/org/selfbus/updater/Utils.java
index e390904c..a2158068 100644
--- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/Utils.java
+++ b/firmware_updater/updater/src/org/selfbus/updater/Utils.java
@@ -2,14 +2,10 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import tuwien.auto.calimero.KNXException;
-import tuwien.auto.calimero.KNXIllegalArgumentException;
import tuwien.auto.calimero.knxnetip.TcpConnection;
import java.io.File;
-import java.net.InetAddress;
import java.net.InetSocketAddress;
-import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.CRC32;
@@ -18,9 +14,8 @@
* Basic utilities usable for the application
*/
public class Utils {
- private static final Logger logger = LoggerFactory.getLogger(Utils.class.getName());
- public static final String PROGRESS_MARKER = "."; //!< symbol to print progress to console
- public static final int CONSOLE_WIDTH = 80;
+ @SuppressWarnings("unused")
+ private static final Logger logger = LoggerFactory.getLogger(Utils.class);
public static long streamToLong(byte[] stream, int offset) {
return ((stream[offset] & 0xFF)) |
@@ -43,32 +38,6 @@ public static void shortToStream(byte[] stream, int offset, short val) {
stream[offset] = (byte) (val);
}
- public static String byteArrayToHex(byte[] bytes) {
- if (bytes == null) {
- return "";
- }
-
- StringBuilder txt = new StringBuilder();
- for (int i = 0; i < bytes.length; i++) {
- if (i != 0) {
- txt.append(":");
- }
- txt.append(String.format("%02X", bytes[i]));
- }
- return txt.toString();
- }
-
- public static InetAddress parseHost(final String host) {
- try {
- InetAddress res = InetAddress.getByName(host);
- logger.debug("Resolved {} with {}", host, res);
- return res;
- } catch (final UnknownHostException e) {
- throw new KNXIllegalArgumentException(
- "failed to read host " + host, e);
- }
- }
-
public static boolean fileExists(String fileName) {
File f = new File(fileName);
return (f.exists() && f.isFile());
@@ -156,8 +125,7 @@ public static int crc32Value(byte[] buffer) {
private static final Mapdevice running in normal into the Bootloader mode
+ *
+ * @param device the IndividualAddress of the device to restart
+ */
+ public void restartDeviceToBootloader(IndividualAddress device) throws InterruptedException {
+ int restartProcessTime = Mcu.DEFAULT_RESTART_TIME_SECONDS;
+ try (Destination dest = this.mc.createDestination(device, true, false, false)) {
+ logger.info("Restarting device {}{}{} into bootloader", ansi().fgBright(OK), device, ansi().reset());
+ restartProcessTime = this.mc.restart(dest, RESTART_ERASE_CODE, RESTART_CHANNEL);
+ String timeStr;
+ if (restartProcessTime <= 1) {
+ timeStr = "second";
+ }
+ else {
+ timeStr = "seconds";
+ }
+ logger.info(CONSOLE_GUI_NO_NEWLINE, "Device {} reported {}{}{} {} for restarting",
+ device, ansi().fgBright(OK), restartProcessTime, ansi().reset(), timeStr);
+ } catch (final KNXException e) {
+ logger.info("{}Restart state of device {} unknown. {}{}",
+ ansi().fgBright(WARN), device, e.getMessage(), ansi().reset());
+ logger.debug("", e); // todo see logback issue https://github.com/qos-ch/logback/issues/876
+ logger.info(CONSOLE_GUI_NO_NEWLINE, "Waiting {}{}{} seconds for device {} to restart",
+ ansi().fgBright(OK), restartProcessTime, ansi().reset(), device);
+ }
+ finally {
+ waitRestartTime(restartProcessTime);
+ }
+ }
+
+ public String requestUIDFromDevice()
+ throws KNXTimeoutException, KNXLinkClosedException, InterruptedException, UpdaterException {
+ logger.info("Requesting UID from {}", progDestination.getAddress());
+ byte[] result = sendWithRetry(UPDCommand.REQUEST_UID, new byte[0], getMaxUpdCommandRetry()).data();
+ UPDCommand command = UPDCommand.tryFromByteArray(result);
+ if (command != UPDCommand.RESPONSE_UID) {
+ restartProgrammingDevice();
+ throw new UpdaterException("Requesting UID failed!");
+ }
+
+ byte[] uid;
+ if ((result.length >= UPDProtocol.UID_LENGTH_USED) && (result.length <= UPDProtocol.UID_LENGTH_MAX)){
+ uid = Arrays.copyOfRange(result, DATA_POSITION, UPDProtocol.UID_LENGTH_USED + DATA_POSITION);
+ logger.info(" got: {} length {}", UPDProtocol.byteArrayToHex(uid), uid.length);
+ return UPDProtocol.byteArrayToHex(uid);
+ } else {
+ uid = Arrays.copyOfRange(result, DATA_POSITION, result.length - DATA_POSITION);
+ String errorMsg = String.format("Request UID failed %s result.length=%d, UID_LENGTH_USED=%d, UID_LENGTH_MAX=%d",
+ UPDProtocol.byteArrayToHex(uid), uid.length, UPDProtocol.UID_LENGTH_USED, UPDProtocol.UID_LENGTH_MAX);
+ restartProgrammingDevice();
+ throw new UpdaterException(errorMsg);
+ }
+ }
+
+ public BootloaderIdentity requestBootloaderIdentity()
+ throws KNXTimeoutException, KNXLinkClosedException, InterruptedException, UpdaterException {
+ logger.debug("Requesting Bootloader Identity");
+
+ byte[] telegram = new byte[2];
+ telegram[0] = (byte) ToolInfo.versionMajor();
+ telegram[1] = (byte) ToolInfo.versionMinor();
+
+ byte[] result = sendWithRetry(UPDCommand.REQUEST_BL_IDENTITY, telegram, getMaxUpdCommandRetry()).data();
+ UPDCommand command = UPDCommand.tryFromByteArray(result);
+ if (command != UPDCommand.RESPONSE_BL_IDENTITY)
+ {
+ if (command == UPDCommand.RESPONSE_BL_VERSION_MISMATCH) {
+ long minMajorVersion = result[DATA_POSITION] & 0xff;
+ long minMinorVersion = result[DATA_POSITION + 1] & 0xff;
+ logger.error("{}Selfbus Updater version {} is not compatible. Please update to version {}.{} or higher.{}",
+ ansi().fgBright(WARN), ToolInfo.getVersion(), minMajorVersion, minMinorVersion, ansi().reset());
+ }
+ restartProgrammingDevice();
+ throw new UpdaterException("Requesting Bootloader Identity failed!");
+ }
+
+ BootloaderIdentity bl = BootloaderIdentity.fromArray(Arrays.copyOfRange(result, DATA_POSITION, result.length));
+ logger.info("Device Bootloader: {}{}{}", ansi().fgBright(INFO), bl, ansi().reset());
+
+ boolean versionsMatch = (bl.versionMajor() > ToolInfo.minMajorVersionBootloader()) ||
+ ((bl.versionMajor() == ToolInfo.minMajorVersionBootloader()) && (bl.versionMinor() >= ToolInfo.minMinorVersionBootloader()));
+
+ if (!versionsMatch)
+ {
+ throw new UpdaterException(String.format("Bootloader version %s is not compatible, please update Bootloader to version %s or higher.",
+ bl.getVersion(), ToolInfo.minVersionBootloader()));
+ }
+ return bl;
+ }
+
+ public BootDescriptor requestBootDescriptor()
+ throws KNXTimeoutException, KNXLinkClosedException, InterruptedException, UpdaterException {
+ logger.debug("Requesting Boot Descriptor");
+ byte[] result = sendWithRetry(UPDCommand.REQUEST_BOOT_DESC, new byte[0], getMaxUpdCommandRetry()).data();
+ UPDCommand command = UPDCommand.tryFromByteArray(result);
+ if (command != UPDCommand.RESPONSE_BOOT_DESC) {
+ restartProgrammingDevice();
+ throw new UpdaterException("Boot descriptor request failed!");
+ }
+
+ BootDescriptor bootDescriptor = BootDescriptor.fromArray(Arrays.copyOfRange(result, DATA_POSITION, result.length));
+ logger.info("Current firmware: {}", bootDescriptor);
+ return bootDescriptor;
+ }
+
+ public String requestAppVersionString() throws UpdaterException,
+ InterruptedException {
+ byte[] result = sendWithRetry(UPDCommand.APP_VERSION_REQUEST, new byte[0], getMaxUpdCommandRetry()).data();
+ UPDCommand command = UPDCommand.tryFromByteArray(result);
+ if (command != UPDCommand.APP_VERSION_RESPONSE) {
+ return "";
+ }
+
+ return new String(result,DATA_POSITION,result.length - DATA_POSITION); // Convert 12 bytes to string starting from result[DATA_POSITION];
+ }
+
+ public void unlockDeviceWithUID(String uid)
+ throws KNXTimeoutException, KNXLinkClosedException, InterruptedException, UpdaterException {
+ logger.info("Unlocking device {} with UID {}", progDestination.getAddress(), uid);
+ byte[] result = sendWithRetry(UPDCommand.UNLOCK_DEVICE, UPDProtocol.uidToByteArray(uid), getMaxUpdCommandRetry()).data();
+ if (UPDProtocol.checkResult(result) != UDPResult.IAP_SUCCESS) {
+ restartProgrammingDevice();
+ throw new UpdaterException(String.format("Unlocking device %s failed.", progDestination.getAddress()));
+ }
+ }
+
+ public void eraseAddressRange(long startAddress, long totalLength)
+ throws KNXLinkClosedException, InterruptedException, UpdaterException, KNXTimeoutException {
+ long endAddress = startAddress + totalLength - 1;
+ byte[] telegram = new byte[8];
+ Utils.longToStream(telegram, 0 , startAddress);
+ Utils.longToStream(telegram, 4 , endAddress);
+ logger.info("Erasing firmware address range: {}", String.format("0x%04X-0x%04X", startAddress, endAddress));
+ Duration oldResponseTimeout = mc.responseTimeout();
+ Duration newResponseTimeout = MAX_FLASH_ERASE_TIMEOUT.multipliedBy(2);
+ if (oldResponseTimeout.compareTo(newResponseTimeout) < 0) {
+ mc.responseTimeout(newResponseTimeout); // temporarily increase responseTimeout
+ logger.trace("mc.ResponseTimeout temporarily increased to {}", mc.responseTimeout());
+ }
+
+ byte[] result = sendWithRetry(UPDCommand.ERASE_ADDRESS_RANGE, telegram, getMaxUpdCommandRetry()).data();
+
+ mc.responseTimeout(oldResponseTimeout); // restore responseTimeout
+ logger.trace("mc.ResponseTimeout restored to {}", mc.responseTimeout());
+
+ if (UPDProtocol.checkResult(result) != UDPResult.IAP_SUCCESS) {
+ restartProgrammingDevice();
+ throw new UpdaterException("Erasing firmware address range failed.");
+ }
+ }
+
+ public void eraseFlash()
+ throws KNXLinkClosedException, InterruptedException, UpdaterException, KNXTimeoutException {
+ byte[] result = sendWithRetry(UPDCommand.ERASE_COMPLETE_FLASH, new byte[0], getMaxUpdCommandRetry()).data();
+ if (UPDProtocol.checkResult(result) != UDPResult.IAP_SUCCESS) {
+ restartProgrammingDevice();
+ throw new UpdaterException("Deleting the entire flash failed.");
+ }
+ }
+
+ public void dumpFlashRange(long startAddress, long endAddress)
+ throws KNXLinkClosedException, InterruptedException, UpdaterException, KNXTimeoutException {
+ byte[] telegram = new byte[8];
+ Utils.longToStream(telegram, 0 , startAddress);
+ Utils.longToStream(telegram, 4 , endAddress);
+ // sendWithRetry will always time out, because the mcu is busy dumping the flash
+ byte[] result = sendWithRetry(UPDCommand.DUMP_FLASH, telegram, 0).data();
+ if (UPDProtocol.checkResult(result) != UDPResult.IAP_SUCCESS) {
+ restartProgrammingDevice();
+ throw new UpdaterException("Flash dumping failed.");
+ }
+ }
+
+ public ResponseResult doFlash(byte[] data, int maxRetry, int delay, ProgressInfo progressInfo)
+ throws UpdaterException, InterruptedException {
+ int nIndex = 0;
+ ResponseResult result = new ResponseResult();
+ while (nIndex < data.length)
+ {
+ byte[] txBuffer;
+ if ((data.length + updSendDataOffset - nIndex) >= getMaxPayload()) {
+ txBuffer = new byte[getMaxPayload()];
+ }
+ else {
+ txBuffer = new byte[data.length + updSendDataOffset - nIndex];
+ }
+
+ if (getProtocolVersion() == UDPProtocolVersion.UDP_V0) {
+ txBuffer[0] = (byte) nIndex; // First byte contains mcu's ramBuffer start position to copy to
+ }
+ System.arraycopy(data, nIndex, txBuffer, updSendDataOffset, txBuffer.length - updSendDataOffset);
+
+ ResponseResult tmp = sendWithRetry(UPDCommand.SEND_DATA, txBuffer, maxRetry);
+ result.addCounters(tmp);
+
+ if (UPDProtocol.checkResult(tmp.data(), false) != UDPResult.IAP_SUCCESS) {
+ throw new UpdaterException("doFlash failed.");
+ }
+ updateProgressInfo(progressInfo, txBuffer.length - updSendDataOffset);
+ nIndex += txBuffer.length - updSendDataOffset;
+
+ if (delay > 0) {
+ Thread.sleep(delay); //Reduce bus load during data upload, without 2:04, 50ms 2:33, 60ms 2:41, 70ms 2:54, 80ms 3:04
+ }
+ }
+ result.addWritten(nIndex);
+ return result;
+ }
+
+ public void startProgressInfo(ProgressInfo progressInfo) {
+ logger.info(CONSOLE_GUI_ONLY, progressInfo.getHeader());
+ }
+
+ public void updateProgressInfo(ProgressInfo progressInfo, long bytesDone) {
+ if (progressInfo == null) {
+ return;
+ }
+
+ progressInfo.update(bytesDone);
+ printProgressInfo(progressInfo);
+ }
+
+ private void printProgressInfo(ProgressInfo progressInfo) {
+ // append one space, just in case an exception message may come up
+ String logText = String.format("%s%s%s%s ",
+ AnsiCursor.off(),
+ ansi().cursorToColumn(1).fgBright(OK).a(SpinningCursor.getNext()).reset(),
+ progressInfo,
+ AnsiCursor.on());
+ logger.info(CONSOLE_GUI_NO_NEWLINE, logText);
+ }
+
+ public void logAndPrintProgressInfo(ProgressInfo progressInfo) {
+ logger.debug("{}{}{}", progressInfo.getHeader(), System.lineSeparator(), progressInfo);
+ SpinningCursor.setBlank();
+ printProgressInfo(progressInfo);
+ logger.info(CONSOLE_GUI_ONLY, ""); // Just a new line
+ }
+
+ public void programBootDescriptor(BootDescriptor bootDescriptor, int delay)
+ throws UpdaterException, KNXLinkClosedException, InterruptedException, KNXTimeoutException {
+
+ byte[] streamBootDescriptor = bootDescriptor.toStream();
+ // send new boot descriptor
+ ResponseResult flashResult = doFlash(streamBootDescriptor, getMaxUpdCommandRetry(), delay, null);
+ if (flashResult.written() != streamBootDescriptor.length) {
+ throw new UpdaterException(String.format("Sending Boot descriptor (length %d) failed. Wrote %d", streamBootDescriptor.length, flashResult.written()));
+ }
+ if (delay > 0) {
+ Thread.sleep(delay);
+ }
+ int crc32Value = Utils.crc32Value(streamBootDescriptor);
+ byte[] programBootDescriptor = new byte[8];
+ Utils.longToStream(programBootDescriptor, 0, streamBootDescriptor.length);
+ Utils.longToStream(programBootDescriptor, 4, crc32Value);
+ logger.debug("Updating boot descriptor with crc32 {}",
+ String.format("0x%08X, length %d", crc32Value, streamBootDescriptor.length));
+ ResponseResult programResult = sendWithRetry(UPDCommand.UPDATE_BOOT_DESC, programBootDescriptor, getMaxUpdCommandRetry());
+ if (UPDProtocol.checkResult(programResult.data()) != UDPResult.IAP_SUCCESS) {
+ restartProgrammingDevice();
+ throw new UpdaterException("Updating boot descriptor failed.");
+ }
+ programResult.addCounters(flashResult);
+ if (delay > 0) {
+ Thread.sleep(delay);
+ }
+ }
+
+ public BootloaderStatistic requestBootLoaderStatistic() throws UpdaterException,
+ InterruptedException {
+ logger.debug("Requesting Bootloader statistic");
+ byte[] result = sendWithRetry(UPDCommand.REQUEST_STATISTIC, new byte[0], getMaxUpdCommandRetry()).data();
+ UPDCommand command = UPDCommand.tryFromByteArray(result);
+ if (command != UPDCommand.RESPONSE_STATISTIC) {
+ logger.warn("Requesting Bootloader statistic {}failed!{}", ansi().fgBright(WARN), ansi().reset());
+ return null;
+ }
+ BootloaderStatistic blStatistic = BootloaderStatistic.fromArray(Arrays.copyOfRange(result, DATA_POSITION, result.length));
+ logger.debug("#Disconnect: {} #repeated T_ACK: {}", blStatistic.getDisconnectCountColored(),
+ blStatistic.getRepeatedT_ACKcount());
+ return blStatistic;
+ }
+
+ private boolean isLinkDead() {
+ if (link == null) {
+ return true;
+ }
+ return !link.isOpen();
+ }
+
+ private void handleKNXException(final UPDCommand command, final KNXException e) throws
+ UpdaterException, InterruptedException {
+ logger.warn("{}{}{} ({} {})", ansi().fgBright(WARN), e.getMessage(), ansi().reset(),
+ e.getClass().getSimpleName(), command);
+ try {
+ reconnect();
+ }
+ catch (KNXException | UnknownHostException e2) {
+ throw new UpdaterException(String.format("%s failed.", command), e);
+ }
+ }
+
+ public ResponseResult sendWithRetry(final UPDCommand command, final byte[] data, int maxRetry)
+ throws UpdaterException, InterruptedException {
+ ResponseResult result = new ResponseResult();
+ while (true) {
+ KNXException lastCaughtException;
+ try {
+ byte[] data2 = mc.sendUpdateData(progDestination, command.toByte(), data);
+ result.copyFromArray(data2);
+ return result;
+ }
+ catch (KNXDisconnectException | KNXLinkClosedException e) {
+ lastCaughtException = e;
+ result.incDropCount();
+ handleKNXException(command, e);
+ }
+ catch (KNXTimeoutException e) {
+ // Can happen on a L2 tunnel request ACK timeout or a TL4 ACK timeout
+ lastCaughtException = e;
+ result.incTimeoutCount();
+ handleKNXException(command, e);
+ }
+ catch (KNXInvalidResponseException e) {
+ lastCaughtException = e;
+ handleKNXException(command, e);
+ }
+ finally {
+ if (isLinkDead()) {
+ maxRetry = 0; // exit while
+ }
+ }
+
+ if (maxRetry > 0) {
+ maxRetry--;
+ }
+ else {
+ throw new UpdaterException(String.format("%s failed.", command), lastCaughtException);
+ }
+ }
+ }
+
+ public void checkDeviceInProgrammingMode(IndividualAddress progDeviceAddr) throws UpdaterException,
+ InterruptedException {
+ try {
+ if (isLinkDead()) {
+ reconnect();
+ }
+
+ ManagementProcedures mgmt = new ManagementProceduresImpl(link);
+ IndividualAddress[] devices = mgmt.readAddress();
+ mgmt.close();
+ if ((devices.length == 0) && (progDeviceAddr == null)) { // no device in prog mode
+ return;
+ }
+ else if ((devices.length == 1) && (progDeviceAddr != null) && (progDeviceAddr.equals(devices[0]))) { // correct device in prog mode
+ return;
+ }
+
+ throw new UpdaterException(getExceptionMessage(devices, progDeviceAddr));
+ } catch (KNXException | UnknownHostException e ) {
+ throw new UpdaterException(String.format("checkDevicesInProgrammingMode failed. %s", e.getMessage()), e);
+ }
+ }
+
+ private static String getExceptionMessage(IndividualAddress[] devicesInProgMode, IndividualAddress progDeviceAddr) {
+ String exceptionMessage;
+ if (devicesInProgMode.length == 0) {
+ exceptionMessage = "No device in programming mode.";
+ }
+ else if (devicesInProgMode.length == 1) {
+ exceptionMessage = String.format("Device %s is already in bootloader/programming mode.",
+ Arrays.toString(devicesInProgMode));
+ }
+ else {
+ exceptionMessage = String.format("%d other devices %s are already in bootloader/programming mode.",
+ devicesInProgMode.length, Arrays.toString(devicesInProgMode));
+ }
+ String expectedDeviceAddr;
+ if (progDeviceAddr == null) {
+ expectedDeviceAddr = "none";
+
+ }
+ else {
+ expectedDeviceAddr = progDeviceAddr.toString();
+ }
+ return String.format("%s Expected [%s]", exceptionMessage, expectedDeviceAddr);
+ }
+
+ public UDPProtocolVersion getProtocolVersion() {
+ return protocolVersion;
+ }
+
+ public void setProtocolVersion(UDPProtocolVersion protocolVersion) {
+ this.protocolVersion = protocolVersion;
+ this.maxPayload = Mcu.MAX_PAYLOAD;
+ if (this.protocolVersion == UDPProtocolVersion.UDP_V1) {
+ this.blockSize = Mcu.UPD_PROGRAM_SIZE;
+ this.updSendDataOffset = 0;
+ }
+ else {
+ this.blockSize = Mcu.FLASH_PAGE_SIZE;
+ this.updSendDataOffset = 1;
+ }
+ }
+
+ public int getBlockSize() {
+ return blockSize;
+ }
+
+ public boolean setBlockSize(int blockSize) {
+ if (getProtocolVersion() == UDPProtocolVersion.UDP_V0) {
+ return false;
+ }
+ this.blockSize = blockSize;
+ return true;
+ }
+
+ public int getMaxPayload() {
+ return maxPayload;
+ }
+
+ public int getMaxUpdCommandRetry() {
+ return MAX_UPD_COMMAND_RETRY;
+ }
+
+ public String getLinkInfo() {
+ if (this.link == null) {
+ return "No link available.";
+ }
+ String linkInfo = link.toString();
+ KNXMediumSettings settings = link.getKNXMedium();
+ Optional
+ *
+ *
+ *
+ *