diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..d12d8a04 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.md -whitespace diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..e17ef26d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: "gradle" + directory: "/firmware_updater/updater" + schedule: + interval: "weekly" + day: "thursday" + time: "12:53" + ignore: + # Exclude java-intelhex-parser + - dependency-name: "com.github.j123b567:java-intelhex-parser" + versions: [">= 0"] # Prevent updating due to its specific pinned commit \ No newline at end of file diff --git a/firmware_updater/bootloader/inc/decompressor.h b/firmware_updater/bootloader/inc/decompressor.h index f8993670..6e673fd3 100644 --- a/firmware_updater/bootloader/inc/decompressor.h +++ b/firmware_updater/bootloader/inc/decompressor.h @@ -49,12 +49,12 @@ class Decompressor uint8_t cmdBuffer[5] = {0}; int expectedCmdLength = 0; int cmdBufferLength = 0; - uint8_t scratchpad[FLASH_PAGE_SIZE] = {0}; + alignas(FLASH_RAM_BUFFER_ALIGNMENT) uint8_t scratchpad[FLASH_PAGE_SIZE] = {0}; uint8_t oldPages[FLASH_PAGE_SIZE * REMEMBER_OLD_PAGES_COUNT] = {0}; int bytesToFlash = 0; int rawLength = 0; State state = State::EXPECT_COMMAND_BYTE; - __attribute__ ((aligned (FLASH_RAM_BUFFER_ALIGNMENT))) uint8_t * startAddrOfPageToBeFlashed = 0; + alignas(FLASH_RAM_BUFFER_ALIGNMENT) uint8_t * startAddrOfPageToBeFlashed = 0; uint8_t * startAddrOfFlash = 0; public: diff --git a/firmware_updater/bootloader/src/update.cpp b/firmware_updater/bootloader/src/update.cpp index 08e3a9fa..b8fc3bb1 100644 --- a/firmware_updater/bootloader/src/update.cpp +++ b/firmware_updater/bootloader/src/update.cpp @@ -57,7 +57,7 @@ * Maximum extended frame length is 254 bytes - 1 byte for the @ref UPD_Command totaling in 1265 bytes */ constexpr uint16_t bufferSize = 1265; -static uint8_t __attribute__ ((aligned (FLASH_RAM_BUFFER_ALIGNMENT))) ramBuffer[bufferSize]; //!< RAM buffer used for flash operations +alignas(FLASH_RAM_BUFFER_ALIGNMENT) static uint8_t ramBuffer[bufferSize]; //!< RAM buffer used for flash operations static uint8_t * retTelegram = nullptr; //!< pointer to return buffer, as a field for easier access and smaller code size // Try to avoid direct access to these global variables. diff --git a/firmware_updater/updater/source/.classpath b/firmware_updater/updater/.classpath similarity index 100% rename from firmware_updater/updater/source/.classpath rename to firmware_updater/updater/.classpath diff --git a/firmware_updater/updater/.gitignore b/firmware_updater/updater/.gitignore index 92eb2403..3396ecc0 100644 --- a/firmware_updater/updater/.gitignore +++ b/firmware_updater/updater/.gitignore @@ -1 +1,89 @@ +# Selfbus Updater specific +.gradle +build +log +out +bin settings.xml + +# all below is dated 10.08.2024 source: +# https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser diff --git a/firmware_updater/updater/.idea/.name b/firmware_updater/updater/.idea/.name new file mode 100644 index 00000000..42278725 --- /dev/null +++ b/firmware_updater/updater/.idea/.name @@ -0,0 +1 @@ +SB_updater \ No newline at end of file diff --git a/firmware_updater/updater/.idea/Selfbus_Updater.iml b/firmware_updater/updater/.idea/Selfbus_Updater.iml deleted file mode 100644 index d6ebd480..00000000 --- a/firmware_updater/updater/.idea/Selfbus_Updater.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/firmware_updater/updater/.idea/artifacts/SB_updater_main_jar.xml b/firmware_updater/updater/.idea/artifacts/SB_updater_main_jar.xml new file mode 100644 index 00000000..dd763497 --- /dev/null +++ b/firmware_updater/updater/.idea/artifacts/SB_updater_main_jar.xml @@ -0,0 +1,28 @@ + + + $PROJECT_DIR$/out/artifacts/SB_updater_main_jar + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/firmware_updater/updater/.idea/compiler.xml b/firmware_updater/updater/.idea/compiler.xml new file mode 100644 index 00000000..b589d56e --- /dev/null +++ b/firmware_updater/updater/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/firmware_updater/updater/.idea/misc.xml b/firmware_updater/updater/.idea/misc.xml index 9e54aca8..df27bd37 100644 --- a/firmware_updater/updater/.idea/misc.xml +++ b/firmware_updater/updater/.idea/misc.xml @@ -1,4 +1,133 @@ - + + + + + + + + + + + Abstraction issuesJava + + + Android + + + Assignment issuesJava + + + Class metricsJava + + + Code maturityJava + + + Code style issuesJava + + + ComplianceLintAndroid + + + Control flow issuesJava + + + CorrectnessLintAndroid + + + Declaration redundancyJava + + + Error handlingJava + + + GradleMigrationKotlin + + + Groovy + + + Inheritance issuesJava + + + InteroperabilityLintAndroid + + + JUnitJava + + + JVM languages + + + Java + + + Java 5Java language level migration aidsJava + + + Java language level migration aidsJava + + + Kotlin + + + LintAndroid + + + MavenMigrationKotlin + + + Method metricsGroovy + + + Method metricsJava + + + MethodNaming conventionsJava + + + MigrationKotlin + + + Naming conventionsGroovy + + + Naming conventionsJava + + + PerformanceLintAndroid + + + Probable bugsJava + + + Probable bugsKotlin + + + ProductivityLintAndroid + + + Redundant constructsKotlin + + + SecurityJava + + + SecurityLintAndroid + + + Style issuesKotlin + + + + + Android + + + + + + \ No newline at end of file diff --git a/firmware_updater/updater/.idea/modules.xml b/firmware_updater/updater/.idea/modules.xml index 1ea5edac..c81f6bdf 100644 --- a/firmware_updater/updater/.idea/modules.xml +++ b/firmware_updater/updater/.idea/modules.xml @@ -1,11 +1,9 @@ - - - - - - - - - - + + + + + + + + \ No newline at end of file diff --git a/firmware_updater/updater/.idea/modules/SB_updater.main.iml b/firmware_updater/updater/.idea/modules/SB_updater.main.iml new file mode 100644 index 00000000..0f14c0d5 --- /dev/null +++ b/firmware_updater/updater/.idea/modules/SB_updater.main.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/firmware_updater/updater/.idea/modules/SB_updater.test.iml b/firmware_updater/updater/.idea/modules/SB_updater.test.iml new file mode 100644 index 00000000..132577dd --- /dev/null +++ b/firmware_updater/updater/.idea/modules/SB_updater.test.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/firmware_updater/updater/.idea/runConfigurations/_template__of_Application.xml b/firmware_updater/updater/.idea/runConfigurations/_template__of_Application.xml new file mode 100644 index 00000000..7d508e44 --- /dev/null +++ b/firmware_updater/updater/.idea/runConfigurations/_template__of_Application.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/firmware_updater/updater/source/.project b/firmware_updater/updater/.project similarity index 100% rename from firmware_updater/updater/source/.project rename to firmware_updater/updater/.project diff --git a/firmware_updater/updater/README.md b/firmware_updater/updater/README.md index e264f0f2..38850216 100644 --- a/firmware_updater/updater/README.md +++ b/firmware_updater/updater/README.md @@ -1,46 +1,72 @@ -# Selfbus-Updater 1.20 +# Selfbus-Updater + + +* [Selfbus-Updater](#selfbus-updater) + * [Wiki](#wiki) + * [Requirements](#requirements) + * [Build](#build) + * [Usage](#usage) + * [Common use cases:](#common-use-cases) + * [Known Issues](#known-issues) + * [Error *Assertion failed* with KNX USB-Interface under Windows](#error-assertion-failed-with-knx-usb-interface-under-windows) + * [Loxone Miniserver Gen 1](#loxone-miniserver-gen-1) + * [Development](#development) + * [IDEs:](#ides) + * [IntelliJ IDEA Settings for Updater GUI development:](#intellij-idea-settings-for-updater-gui-development) + * [gradle:](#gradle) + + +## Wiki + +The Updater Wiki article can be found [here](https://selfbus.org/wiki/software/tools/7-selfbus-firmware-updater). ## Requirements * JDK 17+ -* gradle >=8.5 +* gradle >=9.2.1 * Selfbus device with flashed [bootloader](../bootloader) version 1.00 or higher ## Build ``` -gradle fatJar +gradle shadowJar ``` *or* ``` -linux: gradlew fatJar -windows: gradlew.bat fatJar +linux: gradlew shadowJar +windows: gradlew.bat shadowJar ``` -*SB_updater-x.xx-all.jar* file is created in [build/libs](source/build/libs) directory. +*SB_updater-x.xx-all.jar* file is created in [build/libs](build/libs) directory. ## Usage ``` -java -jar SB_updater-x.xx-all.jar [-f ] [-m | -s | -t - ] [-d ] [-D ] [-o ] [--priority ] - [-bs <256|512|1024>] [--user ] [--user-pwd ] [--device-pwd ] [-u - ] [-f1] [-H ] [-P ] [-p ] [-t2] [-t1] [-n] [-r] [-h | -v] - [--delay ] [-l ] [--ERASEFLASH] [--DUMPFLASH ] [-f0] - [--statistic] +java -jar SB_updater-x.xx-all.jar [-f ] [-m ] [-s | -t + | --usb ] [-d ] [-D ] [-o ] [--priority + ] [-bs <256|512|1024>] [--user ] [--user-pwd ] + [--device-pwd ] [-u ] [-f1] [-H ] [-P ] [-p ] [-t2 + | -t1 | -r] [-n] [-h | -v] [--delay ] [-l ] [--reconnect ] + [--ip-tunnel-reconnect <#sequence>] [--ERASEFLASH] [--DUMPFLASH ] [-f0] + [--statistic] [--discover] Selfbus KNX-Firmware update tool options: -f,--fileName Filename of hex file to program - -m,--medium KNX medium [tp1|rf] (default tp1) + -m,--medium KNX medium [tp1|rf] (default TP1) -s,--serial use FT1.2 serial communication -t,--tpuart use TPUART serial communication (experimental, needs serialcom or rxtx library in java.library.path) + --usb use USB-Interface. Specify VendorID and ProductID e.g. + 147B:5120 for the Selfbus USB-Interface (experimental) -d,--device KNX device address in normal operating mode (default none) -D,--progDevice KNX device address in bootloader mode (default 15.15.192) - -o,--own own physical KNX address (default 0.0.0) + -o,--own own physical KNX tunnel address (default 0.0.0). + Required for some IP interfaces that also use their own + address as the tunnel address, e.g. Loxone Miniserver + Gen 1. --priority KNX telegram priority (default LOW) -bs,--blocksize <256|512|1024> Block size to program (default 1024 bytes) --user KNX IP Secure tunneling user identifier (1..127) - (default 1) + (default -1) --user-pwd KNX IP Secure tunneling user password (Commissioning password/Inbetriebnahmepasswort), quotation marks (") in password may not work @@ -55,14 +81,19 @@ Selfbus KNX-Firmware update tool options: -p,--port UDP port on (default 3671) -t2,--tunnelingv2 use KNXnet/IP tunneling v2 (TCP) (experimental) -t1,--tunneling use KNXnet/IP tunneling v1 (UDP) + -r,--routing use KNXnet/IP routing/multicast (experimental) -n,--nat enable Network Address Translation (NAT) (only available with tunneling v1) - -r,--routing use KNXnet/IP routing/multicast (experimental) -h,--help show this help message -v,--version show tool/library version --delay delay telegrams during data transmission to reduce bus load, valid 0-500ms, default 0 - -l,--logLevel Logfile logging level [TRACE|DEBUG|INFO] (default DEBUG) + -l,--logLevel Logfile logging level [TRACE|DEBUG|INFO] (default TRACE) + --reconnect pause between a KNX connection reconnect, valid 100 - + 12500ms, default 100 + --ip-tunnel-reconnect <#sequence> Reconnect KNX IP tunnel on sequence number, valid 100 - + 247, default -1. May help with some IP-Interfaces e.g. + for Loxone Miniserver Gen 1. set to 245 --ERASEFLASH 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. @@ -71,6 +102,7 @@ Selfbus KNX-Firmware update tool options: bootloader. -f0,--NO_FLASH for debugging use only, disable flashing firmware! --statistic show more statistic data + --discover List available KNXnet/IP interfaces and USB-Interfaces ``` ## Common use cases: Updater with graphical user interface (**experimental**) @@ -81,23 +113,59 @@ Read UID of the device: ``` java -jar SB_updater-x.xx-all.jar ``` -Recommended for new firmware versions if UID is unknown (requires active programming mode to unlock the device): +Recommended for new firmware versions if UID is unknown: ``` -java -jar SB_updater-x.xx-all.jar -fileName "out8-bcu1.hex" -nat +java -jar SB_updater-x.xx-all.jar --fileName "out8-bcu1_flashstart_*.hex" ``` Recommended for new firmware versions with known UID: ``` -java -jar SB_updater-x.xx-all.jar -fileName "out8-bcu1.hex" -uid 05:B0:01:02:E9:80:AC:AE:E9:07:47:55 -nat +java -jar SB_updater-x.xx-all.jar --fileName "out8-bcu1_flashstart_*.hex" --uid 05:B0:01:02:E9:80:AC:AE:E9:07:47:55 ``` -Manual specification of parameters if the App-Version pointer is not found/integrated in the firmware file: +Important for Loxone Miniserver Gen 1: ``` -java -jar SB_updater-x.xx-all.jar -fileName "in16-bim112.hex" -appVersionPtr 0x3263 -uid 05:B0:01:02:E9:80:AC:AE:E9:07:47:55 -nat +java -jar SB_updater-x.xx-all.jar --fileName "out8-bcu1_flashstart_*.hex" --uid 05:B0:01:02:E9:80:AC:AE:E9:07:47:55 --full --reconnect 500 --ip-tunnel-reconnect 247 --own x.y.z ``` -## Used IDE's: -IntelliJ IDEA Community 2023.3.4 (Build -> Build Artifacts)
-eclipse project is currently not maintained -## gradle: -update [gradle wrapper](source/gradle/wrapper) to the newest version: +**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** + +## Known Issues + +### Error *Assertion failed* with KNX USB-Interface under Windows + +Windows error assertion failed + +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). +Zadig KNX USB Interface selected + +- 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 @@ - - - $PROJECT_DIR$/out/artifacts/SB_updater_main_jar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/firmware_updater/updater/source/README.md b/firmware_updater/updater/source/README.md deleted file mode 100644 index e5d06855..00000000 --- a/firmware_updater/updater/source/README.md +++ /dev/null @@ -1 +0,0 @@ -[see ../README.md](../README.md) \ No newline at end of file diff --git a/firmware_updater/updater/source/Settings for GUI development.txt b/firmware_updater/updater/source/Settings for GUI development.txt deleted file mode 100644 index ce099e4c..00000000 --- a/firmware_updater/updater/source/Settings for GUI development.txt +++ /dev/null @@ -1,7 +0,0 @@ -Intellij IDEA - -File -> Settings -> Editor -> GUI Designer -> Generate GUI into: Java source code -File -> Settings -> Build, Execution, Deployment -> Build Tools -> Gradle -> Build and run using: Intellij IDEA -File -> Settings -> Build, Execution, Deployment -> Build Tools -> Gradle -> Run tests using: Intellij IDEA - -Plugings -> install "Resource Bundle Editor" (start with highlight resources->Resource Bundle and press F4) \ No newline at end of file diff --git a/firmware_updater/updater/source/bin/.gitignore b/firmware_updater/updater/source/bin/.gitignore deleted file mode 100644 index 7ac70cd7..00000000 --- a/firmware_updater/updater/source/bin/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/main/ -/test/ -/default/ diff --git a/firmware_updater/updater/source/build.gradle b/firmware_updater/updater/source/build.gradle deleted file mode 100644 index 686ba0a2..00000000 --- a/firmware_updater/updater/source/build.gradle +++ /dev/null @@ -1,86 +0,0 @@ -plugins { - id 'java' - id 'application' -} - -group 'org.selfbus' -version '1.20' ///\todo also change in ../README.md and ToolInfo.java (String version) - -java { - sourceCompatibility = JavaVersion.VERSION_17 -} - -application { - mainClass = 'org.selfbus.updater.Updater' -} - -jar { - manifest { - attributes 'Main-Class': 'org.selfbus.updater.Updater' - } -} - -task fatJar(type: Jar) { - manifest.from jar.manifest - archiveClassifier.set('all') - duplicatesStrategy = DuplicatesStrategy.INCLUDE - from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } - } { - /* - exclude "META-INF/*.SF" - exclude "META-INF/*.DSA" - exclude "META-INF/*.RSA" - exclude "META-INF/LICENSE" - exclude "META-INF/LICENSE.txt" - exclude "META-INF/NOTICE.txt" - exclude "META-INF/LGPL2.1" - exclude "META-INF/AL2.0" - exclude "META-INF/versions/9/module-info.class" - */ - } - - with jar -} - -artifacts { - archives fatJar -} - -repositories { - mavenCentral() - maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots' } -} - -gradle.projectsEvaluated{ - tasks.withType(JavaCompile){ - options.compilerArgs << "-Xlint:unchecked" - options.compilerArgs << "-Xlint:deprecation" - options.compilerArgs << "-Xlint:all" - } -} - -dependencies { - // calimero knx bus access library - implementation 'com.github.calimero:calimero-core:2.5.1' - - // calimero serial tx/rx lib for ft1.2 and tpuart support - implementation 'com.github.calimero:calimero-rxtx:2.5.1' - - // find specific directories under linux and windows - implementation 'net.harawata:appdirs:1.2.1' - - // For search in byte array - implementation 'com.google.guava:guava:32.0.0-jre' - - // command line option - implementation 'commons-cli:commons-cli:1.5.0' - - // console and file logging - implementation 'ch.qos.logback:logback-classic:1.4.12' - - // unit test - testImplementation group: 'junit', name: 'junit', version: '4.13.2' - - // GUI - implementation files('libs/forms_rt.jar') -} \ No newline at end of file diff --git a/firmware_updater/updater/source/gradle/wrapper/gradle-wrapper.jar b/firmware_updater/updater/source/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index ccebba77..00000000 Binary files a/firmware_updater/updater/source/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/firmware_updater/updater/source/settings.gradle b/firmware_updater/updater/source/settings.gradle deleted file mode 100644 index ec38d8fa..00000000 --- a/firmware_updater/updater/source/settings.gradle +++ /dev/null @@ -1,8 +0,0 @@ -rootProject.name = 'SB_updater' -/* -sourceControl { - gitRepository("https://github.com/calimero-project/calimero-core.git") { - producesModule('com.github.calimero:calimero-core') - } -} -*/ \ No newline at end of file diff --git a/firmware_updater/updater/source/src/META-INF/MANIFEST.MF b/firmware_updater/updater/source/src/META-INF/MANIFEST.MF deleted file mode 100644 index 6c11289e..00000000 --- a/firmware_updater/updater/source/src/META-INF/MANIFEST.MF +++ /dev/null @@ -1,3 +0,0 @@ -Manifest-Version: 1.0 -Main-Class: Updater - diff --git a/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/DataListener.java b/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/DataListener.java deleted file mode 100644 index 70d3cd1e..00000000 --- a/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/DataListener.java +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2015, Jan Breuer All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -package cz.jaybee.intelhex; - -/** - * Listener interface to parser events - * - * @author Jan Breuer - */ -public interface DataListener { - - /** - * Every time new data are read from file, this listener method is called - * with appropriate values. Multiple calls of this function may be done - * inside one memory regions but they will not overlap (if they don't - * overlap in original intelhex). - * - * @param address - * @param data - */ - public void data(long address, byte[] data); - - /** - * After eof is detected in the file, this listener method is called - */ - public void eof(); -} diff --git a/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/IntelHexException.java b/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/IntelHexException.java deleted file mode 100644 index 366db604..00000000 --- a/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/IntelHexException.java +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2015, Jan Breuer All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -package cz.jaybee.intelhex; - -/** - * Custom exception to prevent using general Exception - * - * @author Jan Breuer - */ -@SuppressWarnings("serial") -public class IntelHexException extends Exception { - - public IntelHexException() { - } - - public IntelHexException(String message) { - super(message); - } -} diff --git a/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/MemoryRegions.java b/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/MemoryRegions.java deleted file mode 100644 index 91d9802d..00000000 --- a/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/MemoryRegions.java +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Copyright (c) 2015, Jan Breuer All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -package cz.jaybee.intelhex; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; - -/** - * Class to hold all memory address regions - * - * @author Jan Breuer - * @author riilabs - */ -public class MemoryRegions { - - private final List regions = new ArrayList<>(); - - public void add(long start, long length) { - Region prevRegion; - if (regions.size() > 0) { - prevRegion = regions.get(regions.size() - 1); - long nextAddress = prevRegion.getAddressStart() + prevRegion.getLength(); - if (nextAddress == start) { - prevRegion.incLength(length); - return; - } - } - regions.add(new Region(start, length)); - } - - public void compact() { - Collections.sort(regions); - - Iterator iter = regions.iterator(); - Region prev = null; - while (iter.hasNext()) { - Region curr = iter.next(); - if (prev == null) { - prev = curr; - } else { - // check for chaining - if (curr.getAddressStart() == (prev.getAddressStart() + prev.getLength())) { - prev.incLength(curr.getLength()); - iter.remove(); - } else { - prev = curr; - } - } - } - } - - public void clear() { - regions.clear(); - } - - public int size() { - return regions.size(); - } - - public Region get(int index) { - return regions.get(index); - } - - public Region getFullRangeRegion() { - long start = 0; - long length = 0; - if (!regions.isEmpty()) { - start = regions.get(0).getAddressStart(); - Region last = regions.get(regions.size() - 1); - length = last.getAddressStart() + last.getLength() - start; - } - - return new Region(start, length); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - for (Region r : regions) { - sb.append(r).append("\r\n"); - } - - return sb.toString(); - } -} diff --git a/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/Parser.java b/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/Parser.java deleted file mode 100644 index 7ebe9801..00000000 --- a/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/Parser.java +++ /dev/null @@ -1,233 +0,0 @@ -/** - * Copyright (c) 2015, Jan Breuer All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -package cz.jaybee.intelhex; - -import java.io.*; - -/** - * Main Intel HEX parser class - * - * @author Jan Breuer - * @author Kristian Sloth Lauszus - * @author riilabs - */ -public class Parser { - - private final BufferedReader reader; - private DataListener dataListener = null; - private static final int HEX = 16; - private boolean eof = false; - private int recordIdx = 0; - private long upperAddress = 0; - private long startAddress = 0; - - /** - * Constructor of the parser with reader - * - * @param reader - */ - public Parser(Reader reader) { - this.reader = (reader instanceof BufferedReader) ? (BufferedReader) reader : new BufferedReader(reader); - } - - /** - * Constructor of the parser with input stream - * - * @param stream - */ - public Parser(InputStream stream) { - this.reader = new BufferedReader(new InputStreamReader(stream)); - } - - /** - * Set data listener to parsing events (data and eof) - * - * @param listener - */ - public void setDataListener(DataListener listener) { - this.dataListener = listener; - } - - /** - * Parse one line of Intel HEX file - * - * @param string record - * @return parsed record - * @throws IntelHexException - */ - private Record parseRecord(String record) throws IntelHexException { - Record result = new Record(); - // check, if there wasn an accidential EOF record - if (eof) { - throw new IntelHexException("Data after eof (" + recordIdx + ")"); - } - - // every IntelHEX record must start with ":" - if (!record.startsWith(":")) { - throw new IntelHexException("Invalid Intel HEX record (" + recordIdx + ")"); - } - - int lineLength = record.length(); - byte[] hexRecord = new byte[lineLength / 2]; - - // sum of all bytes modulo 256 (including checksum) should be 0 - int sum = 0; - for (int i = 0; i < hexRecord.length; i++) { - String num = record.substring(2 * i + 1, 2 * i + 3); - hexRecord[i] = (byte) Integer.parseInt(num, HEX); - sum += hexRecord[i] & 0xff; - } - sum &= 0xff; - - if (sum != 0) { - throw new IntelHexException("Invalid checksum (" + recordIdx + ")"); - } - - // if the length field does not correspond with line length - result.length = hexRecord[0] & 0xFF; - if ((result.length + 5) != hexRecord.length) { - throw new IntelHexException("Invalid record length (" + recordIdx + ")"); - } - // length is OK, copy data - result.data = new byte[result.length]; - System.arraycopy(hexRecord, 4, result.data, 0, result.length); - - // build lower part of data address - result.address = ((hexRecord[1] & 0xFF) << 8) + (hexRecord[2] & 0xFF); - - // determine record type - result.type = RecordType.fromInt(hexRecord[3] & 0xFF); - if (result.type == RecordType.UNKNOWN) { - throw new IntelHexException("Unsupported record type " + (hexRecord[3] & 0xFF) + " (" + recordIdx + ")"); - } - - return result; - } - - /** - * Process parsed record, copute correct address, emit events - * - * @param record - * @throws IntelHexException - */ - private void processRecord(Record record) throws IntelHexException { - // build full address - long addr = record.address | upperAddress; - switch (record.type) { - case DATA: - if (dataListener != null) { - dataListener.data(addr, record.data); - } - break; - case EOF: - if (dataListener != null) { - dataListener.eof(); - } - eof = true; - break; - case EXT_LIN: - if (record.length == 2) { - upperAddress = ((record.data[0] & 0xFF) << 8) + (record.data[1] & 0xFF); - upperAddress <<= 16; // ELA is bits 16-31 of the segment base address (SBA), so shift left 16 bits - } else { - throw new IntelHexException("Invalid EXT_LIN record (" + recordIdx + ")"); - } - - break; - case EXT_SEG: - if (record.length == 2) { - upperAddress = ((record.data[0] & 0xFF) << 8) + (record.data[1] & 0xFF); - upperAddress <<= 4; // ESA is bits 4-19 of the segment base address (SBA), so shift left 4 bits - } else { - throw new IntelHexException("Invalid EXT_SEG record (" + recordIdx + ")"); - } - break; - case START_LIN: - if (record.length == 4) { - startAddress = 0; - for (byte c : record.data) { - startAddress = startAddress << 8; - startAddress |= (c & 0xFF); - } - } else { - throw new IntelHexException("Invalid START_LIN record at line #" + recordIdx + " " + record); - } - break; - case START_SEG: - if (record.length == 4) { - startAddress = 0; - for (byte c : record.data) { - startAddress = startAddress << 8; - startAddress |= (c & 0xFF); - } - } else { - throw new IntelHexException("Invalid START_SEG record at line #" + recordIdx + " " + record); - } - break; - case UNKNOWN: - break; - } - - } - - /** - * Return program start address/reset address. May not be at the beggining - * of the data. - * - * @return Start address - */ - public long getStartAddress() { - return startAddress; - } - - /** - * Main public method to start parsing of the input - * - * @throws IntelHexException - * @throws IOException - */ - public void parse() throws IntelHexException, IOException { - eof = false; - recordIdx = 1; - upperAddress = 0; - startAddress = 0; - String recordStr; - - while ((recordStr = reader.readLine()) != null) { - // Ignore if this is a blank line. - if (recordStr.isEmpty()) { - continue; - } - Record record = parseRecord(recordStr); - processRecord(record); - recordIdx++; - } - - if (!eof) { - throw new IntelHexException("No eof at the end of file"); - } - } -} diff --git a/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/Record.java b/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/Record.java deleted file mode 100644 index 6fd0dd42..00000000 --- a/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/Record.java +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) 2015, Jan Breuer All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -package cz.jaybee.intelhex; - -/** - * Class to hold one Intel HEX record - one line in the file - * @author Jan Breuer - */ -public class Record { - - public int length; - public int address; - public RecordType type; - public byte[] data; - - /** - * Convert the record to pretty string - * - * @return - */ - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - sb.append(type); - sb.append(" @"); - sb.append(String.format("0x%04X", address)); - sb.append(" ["); - for (byte c : data) { - sb.append(String.format("0x%02X", c)); - sb.append(" "); - } - sb.setLength(sb.length() - 1); - sb.append("]"); - return sb.toString(); - } -} diff --git a/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/RecordType.java b/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/RecordType.java deleted file mode 100644 index 229b5837..00000000 --- a/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/RecordType.java +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright (c) 2015, Jan Breuer All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -package cz.jaybee.intelhex; - -/** - * Type of one record in Intel HEX file (type of line) - * - * @author Jan Breuer - */ -public enum RecordType { - - DATA(0x00), - EOF(0x01), - EXT_SEG(0x02), - START_SEG(0x03), - EXT_LIN(0x04), - START_LIN(0x05), - UNKNOWN(0xFF); - int id; - - RecordType(int id) { - this.id = id; - } - - /** - * Convert enum value to integer - * - * @return record type integer value - */ - public int toInt() { - return id; - } - - /** - * Convert integer value to enum value - * - * @param id record type integer value - * @return record type enum value - */ - public static RecordType fromInt(int id) { - for (RecordType d : RecordType.values()) { - if (d.id == id) { - return d; - } - } - return RecordType.UNKNOWN; - } -} diff --git a/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/Region.java b/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/Region.java deleted file mode 100644 index 0fa41989..00000000 --- a/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/Region.java +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Copyright (c) 2015, Jan Breuer All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -package cz.jaybee.intelhex; - -/** - * One memory region - * - * @author Jan Breuer - */ -public class Region implements Comparable { - - private long addressStart; - private long addressEnd; - - public Region(long start, long length) { - this.addressStart = start; - this.addressEnd = start + length - 1; - } - - /** - * Get length of the region - * @return length of the region - */ - public long getLength() { - return addressEnd - addressStart + 1; - } - - /** - * Return last address in memory region - * @return last address in memory region - */ - public long getAddressEnd() { - return addressEnd; - } - - /** - * Set end address - * @param addressEnd - */ - public void setAddressEnd(long addressEnd) { - this.addressEnd = addressEnd; - } - - /** - * Get start address of the region - * @return start address of memory region - */ - public long getAddressStart() { - return addressStart; - } - - /** - * Set start address - * @param addressStart - */ - public void setAddressStart(long addressStart) { - this.addressStart = addressStart; - } - - /** - * Increment length of the region by value - * @param value - */ - void incLength(long value) { - addressEnd += value; - } - - @Override - public String toString() { - return String.format("0x%08x:0x%08x (%dB 0x%08X)", addressStart, addressEnd, getLength(), getLength()); - } - - /** - * Compare, if one region is after another region - * - * @param o the object to be compared. - * @return a negative integer, zero, or a positive integer as this object - * is less than, equal to, or greater than the specified object. - */ - @Override - public int compareTo(Region o) { - if (this.addressStart == o.addressStart) { - return Long.compare(this.addressEnd, o.addressEnd); - } else { - return Long.compare(this.addressStart, o.addressStart); - } - } -} diff --git a/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/listeners/BinWriter.java b/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/listeners/BinWriter.java deleted file mode 100644 index 870781dd..00000000 --- a/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/listeners/BinWriter.java +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Copyright (c) 2015, Jan Breuer All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -package cz.jaybee.intelhex.listeners; - -import cz.jaybee.intelhex.DataListener; -import cz.jaybee.intelhex.MemoryRegions; -import cz.jaybee.intelhex.Region; -import java.io.IOException; -import java.io.OutputStream; -import java.util.Arrays; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Binary file writer - * - * @author Jan Breuer - */ -public class BinWriter implements DataListener { - - private final Region outputRegion; - private final OutputStream destination; - private final byte[] buffer; - private final MemoryRegions regions; - private long maxAddress; - private final boolean minimize; - - public BinWriter(Region outputRegion, OutputStream destination, boolean minimize) { - this.outputRegion = outputRegion; - this.destination = destination; - this.minimize = minimize; - this.buffer = new byte[(int) (outputRegion.getLength())]; - Arrays.fill(buffer, (byte) 0xFF); - regions = new MemoryRegions(); - maxAddress = outputRegion.getAddressStart(); - } - - @Override - public void data(long address, byte[] data) { - regions.add(address, data.length); - - if ((address >= outputRegion.getAddressStart()) && (address <= outputRegion.getAddressEnd())) { - int length = data.length; - if ((address + length) > outputRegion.getAddressEnd()) { - length = (int) (outputRegion.getAddressEnd() - address + 1); - } - System.arraycopy(data, 0, buffer, (int) (address - outputRegion.getAddressStart()), length); - - if (maxAddress < (address + data.length -1)) { - maxAddress = address + data.length - 1; - } - } - } - - @Override - public void eof() { - try { - if (!minimize) { - maxAddress = outputRegion.getAddressEnd(); - } - destination.write(buffer, 0, (int)(maxAddress - outputRegion.getAddressStart() + 1)); - } catch (IOException ex) { - Logger.getLogger(BinWriter.class.getName()).log(Level.SEVERE, null, ex); - } - } - - public MemoryRegions getMemoryRegions() { - return regions; - } -} diff --git a/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/listeners/RangeDetector.java b/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/listeners/RangeDetector.java deleted file mode 100644 index 2e4c8d7b..00000000 --- a/firmware_updater/updater/source/src/main/java/cz/jaybee/intelhex/listeners/RangeDetector.java +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright (c) 2015, Jan Breuer All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -package cz.jaybee.intelhex.listeners; - -import cz.jaybee.intelhex.DataListener; -import cz.jaybee.intelhex.MemoryRegions; -import cz.jaybee.intelhex.Region; - -/** - * First pass listener to calculate data address range for further use - * - * @author riilabs - * @author Jan Breuer - */ -public class RangeDetector implements DataListener { - - private final MemoryRegions regions = new MemoryRegions(); - - @Override - public void data(long address, byte[] data) { - regions.add(address, data.length); - } - - @Override - public void eof() { - regions.compact(); - } - - public void reset() { - regions.clear(); - } - - public Region getFullRangeRegion() { - return regions.getFullRangeRegion(); - } - - public MemoryRegions getMemoryRegions() { - return regions; - } -} diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/CliOptions.java b/firmware_updater/updater/source/src/main/java/org/selfbus/updater/CliOptions.java deleted file mode 100644 index 72a51573..00000000 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/CliOptions.java +++ /dev/null @@ -1,734 +0,0 @@ -package org.selfbus.updater; - -import ch.qos.logback.classic.Level; -import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.CommandLineParser; -import org.apache.commons.cli.DefaultParser; -import org.apache.commons.cli.HelpFormatter; -import org.apache.commons.cli.Option; -import org.apache.commons.cli.OptionGroup; -import org.apache.commons.cli.Options; -import org.apache.commons.cli.ParseException; - -import org.selfbus.updater.upd.UPDProtocol; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import tuwien.auto.calimero.IndividualAddress; -import tuwien.auto.calimero.KNXFormatException; -import tuwien.auto.calimero.KNXIllegalArgumentException; -import tuwien.auto.calimero.Priority; -import tuwien.auto.calimero.knxnetip.KNXnetIPConnection; -import tuwien.auto.calimero.link.medium.TPSettings; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.net.InetAddress; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; - -/** - * Parses command line interface (cli) options for the application - *

- * 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 VALID_BLOCKSIZES = Arrays.asList(256, 512, 1024); - - private static final int PRINT_WIDTH = 100; - private final static List VALID_LOG_LEVELS = Arrays.asList("TRACE", "DEBUG", "INFO"); - - private final Options cliOptions = new Options(); - // define parser - CommandLine cmdLine; - HelpFormatter helper = new HelpFormatter(); - - private final String helpHeader; - private final String helpFooter; - private final String helpApplicationName; - - private InetAddress knxInterface; - private String fileName = ""; - private InetAddress localhost; - private int localport = 0; - private int port = KNXnetIPConnection.DEFAULT_PORT; - private boolean nat = false; - private String ft12 = ""; - private String tpuart = ""; - private boolean tunnelingV2 = false; - private boolean tunnelingV1 = false; - private boolean routing = false; - private String medium = "tp1"; - - private int userId = 1; - private String userPassword = ""; - private String devicePassword = ""; - - - private IndividualAddress progDevice; - private IndividualAddress ownAddress; - private IndividualAddress device = null; - private byte[] uid; - private boolean full = false; - private int delay = 0; - - private boolean NO_FLASH = false; - private Level logLevel = Level.DEBUG; - private boolean eraseFullFlash = false; - private long dumpFlashStartAddress = -1; - private long dumpFlashEndAddress = -1; - - private Priority priority = Priority.LOW; - - private boolean logStatistics = false; - private int blockSize = Mcu.UPD_PROGRAM_SIZE; - - private boolean help = false; - private boolean version = false; - - - public CliOptions(final String[] args, String helpApplicationName, String helpHeader, String helpFooter, - IndividualAddress progDevice, IndividualAddress ownAddress) - throws ParseException, KNXFormatException { - this.helpApplicationName = helpApplicationName; - this.helpHeader = helpHeader; - this.helpFooter = helpFooter; - this.progDevice = progDevice; - this.ownAddress = ownAddress; - - Option tunnelingV2 = new Option(OPT_SHORT_TUNNEL_V2, OPT_LONG_TUNNEL_V2, false, "use KNXnet/IP tunneling v2 (TCP) (experimental)"); - Option tunnelingV1 = new Option(OPT_SHORT_TUNNEL_V1, OPT_LONG_TUNNEL_V1, false, "use KNXnet/IP tunneling v1 (UDP)"); - Option nat = new Option(OPT_SHORT_NAT, OPT_LONG_NAT, false, "enable Network Address Translation (NAT) (only available with tunneling v1)"); - Option routing = new Option(OPT_SHORT_ROUTING, OPT_LONG_ROUTING, false, "use KNXnet/IP routing/multicast (experimental)"); - Option full = new Option(OPT_SHORT_FULL, OPT_LONG_FULL, false, "force full upload mode (disables differential mode)"); - Option help = new Option(OPT_SHORT_HELP, OPT_LONG_HELP, false, "show this help message"); - Option version = new Option(OPT_SHORT_VERSION, OPT_LONG_VERSION, false, "show tool/library version"); - Option NO_FLASH = new Option(OPT_SHORT_NO_FLASH, OPT_LONG_NO_FLASH, false, "for debugging use only, disable flashing firmware!"); - Option eraseFlash = new Option(null, OPT_LONG_ERASEFLASH, false, "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."); - - Option dumpFlash = Option.builder(null).longOpt(OPT_LONG_DUMPFLASH) - .argName("start> (default %d)", KNXnetIPConnection.DEFAULT_PORT)).build(); - Option ft12 = Option.builder(OPT_SHORT_FT12).longOpt(OPT_LONG_FT12) - .argName("COM-port") - .hasArg() - .required(false) - .desc("use FT1.2 serial communication").build(); - Option tpuart = Option.builder(OPT_SHORT_TPUART).longOpt(OPT_LONG_TPUART) - .argName("COM-port") - .hasArg() - .required(false) - .desc("use TPUART serial communication (experimental, needs serialcom or rxtx library in java.library.path)").build(); - Option medium = Option.builder(OPT_SHORT_MEDIUM).longOpt(OPT_LONG_MEDIUM) - .argName("tp1|rf") - .hasArg() - .required(false) - .type(TPSettings.class) - .desc(String.format("KNX medium [tp1|rf] (default %s)", this.medium)).build(); ///\todo not all implemented missing [tp0|p110|p132] - Option optProgDevice = Option.builder(OPT_SHORT_PROG_DEVICE).longOpt(OPT_LONG_PROG_DEVICE) - .argName("x.x.x") - .hasArg() - .required(false) - .type(IndividualAddress.class) - .desc(String.format("KNX device address in bootloader mode (default %s)", this.progDevice.toString())).build(); - Option device = Option.builder(OPT_SHORT_DEVICE).longOpt(OPT_LONG_DEVICE) - .argName("x.x.x") - .hasArg() - .required(false) - .type(IndividualAddress.class) - .desc("KNX device address in normal operating mode (default none)").build(); - Option ownPhysicalAddress = Option.builder(OPT_SHORT_OWN_ADDRESS).longOpt(OPT_LONG_OWN_ADDRESS) - .argName("x.x.x") - .hasArg() - .required(false) - .type(IndividualAddress.class) - .desc(String.format("own physical KNX address (default %s)", this.ownAddress.toString())).build(); - Option uid = Option.builder(OPT_SHORT_UID).longOpt(OPT_LONG_UID) - .argName("uid") - .hasArg() - .required(false) - .desc(String.format("send UID to unlock (default: request UID to unlock). Only the first %d bytes of UID are used", UPDProtocol.UID_LENGTH_USED)).build(); - Option delay = Option.builder(null).longOpt(OPT_LONG_DELAY) - .argName("ms") - .hasArg() - .required(false) - .type(Number.class) - .desc(String.format("delay telegrams during data transmission to reduce bus load, valid 0-500ms, default %d", Updater.DELAY_MIN)).build(); - Option logLevel = Option.builder(OPT_SHORT_LOGLEVEL).longOpt(OPT_LONG_LOGLEVEL) - .argName("TRACE|DEBUG|INFO") - .hasArg() - .required(false) - .type(String.class) - .desc(String.format("Logfile logging level [TRACE|DEBUG|INFO] (default %s)", this.logLevel.toString())).build(); - - Option userId = Option.builder(null).longOpt(OPT_LONG_USER_ID) - .argName("id") - .hasArg() - .required(false) - .type(Number.class) - .desc(String.format("KNX IP Secure tunneling user identifier (1..127) (default %d)", this.userId)).build(); - Option userPasswd = Option.builder(null).longOpt(OPT_LONG_USER_PASSWORD) - .argName("password") - .hasArg() - .required(false) - .type(Number.class) - .desc("KNX IP Secure tunneling user password (Commissioning password/Inbetriebnahmepasswort), quotation marks (\") in password may not work").build(); - Option devicePasswd = Option.builder(null).longOpt(OPT_LONG_DEVICE_PASSWORD) - .argName("password") - .hasArg() - .required(false) - .type(Number.class) - .desc("KNX IP Secure device authentication code (Authentication Code/Authentifizierungscode) quotation marks(\") in password may not work").build(); - - Option knxPriority = Option.builder(null).longOpt(OPT_LONG_PRIORITY) - .argName("SYSTEM|URGENT|NORMAL|LOW") - .hasArg() - .required(false) - .type(String.class) - .desc(String.format("KNX telegram priority (default %s)", this.priority.toString().toUpperCase())).build(); - - Option blockSize = Option.builder(OPT_SHORT_BLOCKSIZE).longOpt(OPT_LONG_BLOCKSIZE) - .argName("256|512|1024") - .valueSeparator(' ') - .numberOfArgs(1) - .required(false) - .type(Number.class) - .desc(String.format("Block size to program (default %d bytes)", this.blockSize)).build(); - - Option logStatistic = new Option(null, OPT_LONG_LOGSTATISTIC, false, "show more statistic data"); - - // options will be shown in order as they are added to cliOptions - cliOptions.addOption(fileName); - - // ft12 or medium, not both - OptionGroup grpBusAccess = new OptionGroup(); - grpBusAccess.addOption(medium); - grpBusAccess.addOption(ft12); - grpBusAccess.addOption(tpuart); - - cliOptions.addOptionGroup(grpBusAccess); - - cliOptions.addOption(device); - cliOptions.addOption(optProgDevice); - cliOptions.addOption(ownPhysicalAddress); - cliOptions.addOption(knxPriority); - cliOptions.addOption(blockSize); - - cliOptions.addOption(userId); - cliOptions.addOption(userPasswd); - cliOptions.addOption(devicePasswd); - - cliOptions.addOption(uid); - cliOptions.addOption(full); - cliOptions.addOption(localhost); - cliOptions.addOption(localport); - cliOptions.addOption(port); - cliOptions.addOption(tunnelingV2); - cliOptions.addOption(tunnelingV1); - cliOptions.addOption(nat); - cliOptions.addOption(routing); - - // help or version, not both - OptionGroup grpHelper = new OptionGroup(); - grpHelper.addOption(help); - grpHelper.addOption(version); - cliOptions.addOptionGroup(grpHelper); - - cliOptions.addOption(delay); - cliOptions.addOption(logLevel); - cliOptions.addOption(eraseFlash); - cliOptions.addOption(dumpFlash); - cliOptions.addOption(NO_FLASH); - cliOptions.addOption(logStatistic); - - - parse(args); - } - - private void parse(final String[] args) { - CommandLineParser parser = new DefaultParser(); - try { - cmdLine = parser.parse(cliOptions, args); - // get the log level for log file output - ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); - if (cmdLine.hasOption(OPT_SHORT_LOGLEVEL)) { - String cliLogLevel = cmdLine.getOptionValue(OPT_SHORT_LOGLEVEL).toUpperCase(); - if (VALID_LOG_LEVELS.contains(cliLogLevel)) { - logLevel = Level.toLevel(cliLogLevel); - root.setLevel(logLevel); - } - else { - logger.warn("{}invalid {} {}, using {}{}", ConColors.RED, OPT_LONG_LOGLEVEL, cliLogLevel, root.getLevel().toString(), ConColors.RESET); - } - } - logger.debug("logLevel={}", root.getLevel().toString()); - - if (cmdLine.hasOption(OPT_LONG_PRIORITY)) { - try { - priority = Priority.get(cmdLine.getOptionValue(OPT_LONG_PRIORITY)); - } - catch (KNXIllegalArgumentException e) { - logger.warn("{}invalid {} {}, using {}{}", ConColors.RED, OPT_LONG_PRIORITY, cmdLine.getOptionValue(OPT_LONG_PRIORITY), priority, ConColors.RESET); - } - } - logger.debug("priority={}", priority.toString()); - - if (cmdLine.hasOption(OPT_SHORT_HELP)) { - help = true; - logger.debug("help={}", true); - return; - } - - if (cmdLine.hasOption(OPT_SHORT_VERSION)) { - version = true; - logger.debug("version={}", true); - return; - } - - if (cmdLine.hasOption(OPT_SHORT_NO_FLASH)) { - NO_FLASH = true; - } - logger.debug("NO_FLASH={}", NO_FLASH); - - if (cmdLine.hasOption(OPT_LONG_ERASEFLASH)) { - eraseFullFlash = true; - } - logger.debug("eraseFlash={}", eraseFullFlash); - - if (cmdLine.hasOption(OPT_LONG_DUMPFLASH)) { - String[] optArgs = cmdLine.getOptionValues(OPT_LONG_DUMPFLASH); - dumpFlashStartAddress = Long.decode(optArgs[0]); - dumpFlashEndAddress = Long.decode(optArgs[1]); - } - logger.debug("dumpFlashStartAddress={}", dumpFlashStartAddress); - logger.debug("dumpFlashEndAddress={}", dumpFlashEndAddress); - - if (cmdLine.hasOption(OPT_SHORT_FULL)) { - full = true; - } - logger.debug("full={}", full); - - if (cmdLine.hasOption(OPT_SHORT_TUNNEL_V2)) { - tunnelingV2 = true; - } - logger.debug("tunnelingV2={}", tunnelingV2); - - if (cmdLine.hasOption(OPT_SHORT_TUNNEL_V1)) { - tunnelingV1 = true; - } - logger.debug("tunneling={}", tunnelingV1); - - if (cmdLine.hasOption(OPT_SHORT_ROUTING)) { - routing = true; - } - logger.debug("routing={}", routing); - - if (cmdLine.hasOption(OPT_SHORT_NAT)) { - nat = true; - } - logger.debug("nat={}", nat); - - if (cmdLine.hasOption(OPT_SHORT_FILENAME)) { - fileName = cmdLine.getOptionValue(OPT_SHORT_FILENAME); - } - logger.debug("fileName={}", fileName); - - if (cmdLine.hasOption(OPT_SHORT_LOCALHOST)) { - localhost = Utils.parseHost(cmdLine.getOptionValue(OPT_SHORT_LOCALHOST)); - } - logger.debug("localhost={}", localhost); - - if (cmdLine.hasOption(OPT_SHORT_LOCALPORT)) { - localport = ((Number)cmdLine.getParsedOptionValue(OPT_SHORT_LOCALPORT)).intValue(); - } - logger.debug("localport={}", localport); - - if (cmdLine.hasOption(OPT_SHORT_PORT)) { - port = ((Number)cmdLine.getParsedOptionValue(OPT_SHORT_PORT)).intValue(); - } - logger.debug("port={}", port); - - if (cmdLine.hasOption(OPT_SHORT_MEDIUM)) { - medium = cmdLine.getOptionValue(OPT_SHORT_MEDIUM); - } - logger.debug("medium={}", medium); - - if (cmdLine.hasOption(OPT_LONG_DELAY)) { - delay = ((Number)cmdLine.getParsedOptionValue(OPT_LONG_DELAY)).intValue(); - if ((delay < Updater.DELAY_MIN) || (delay > Updater.DELAY_MAX)) { - logger.warn("{}option --delay {} is invalid (min:{}, max:{}) => setting --delay {}{}", ConColors.RED, delay, Updater.DELAY_MIN, Updater.DELAY_MAX, Updater.DELAY_DEFAULT, ConColors.RESET); - delay = Updater.DELAY_DEFAULT; // set to DELAY_DEFAULT in case of invalid waiting time - } - } - logger.debug("delay={}", delay); - - if (cmdLine.hasOption(OPT_SHORT_BLOCKSIZE)) { - int newBlockSize = ((Number)cmdLine.getParsedOptionValue(OPT_LONG_BLOCKSIZE)).intValue(); - if (VALID_BLOCKSIZES.contains(newBlockSize)) { - blockSize = newBlockSize; - } - else { - logger.info("{}--{} {} is not supported => Set --{} to default {} bytes{}", ConColors.YELLOW, OPT_LONG_BLOCKSIZE, newBlockSize, OPT_LONG_BLOCKSIZE, blockSize, ConColors.RESET); - } - } - logger.debug("{}={}", OPT_LONG_BLOCKSIZE, delay); - - if (cmdLine.hasOption(OPT_SHORT_UID)) { - uid = uidToByteArray(cmdLine.getOptionValue(OPT_SHORT_UID)); - } - logger.debug("uid={}", Utils.byteArrayToHex(uid)); - - if (cmdLine.hasOption(OPT_SHORT_DEVICE)) { - device = new IndividualAddress(cmdLine.getOptionValue(OPT_SHORT_DEVICE)); - } else { - device = null; - } - logger.debug("device={}", device); - - if (cmdLine.hasOption(OPT_SHORT_PROG_DEVICE)) { - progDevice = new IndividualAddress(cmdLine.getOptionValue(OPT_SHORT_PROG_DEVICE)); - } - - logger.debug("progDevice={}", progDevice); - - if (cmdLine.hasOption(OPT_SHORT_OWN_ADDRESS)) { - ownAddress = new IndividualAddress(cmdLine.getOptionValue(OPT_SHORT_OWN_ADDRESS)); - } - logger.debug("ownAddress={}", ownAddress); - - if (cmdLine.hasOption(OPT_SHORT_FT12)) { - ft12 = cmdLine.getOptionValue(OPT_SHORT_FT12); - } - logger.debug("ft12={}", ft12); - - if (cmdLine.hasOption(OPT_SHORT_TPUART)) { - tpuart = cmdLine.getOptionValue(OPT_SHORT_TPUART); - } - logger.debug("tpuart={}", tpuart); - - if ((ft12.isEmpty()) && (tpuart.isEmpty())) { - // no ft12 or tpuart => get the - if (cmdLine.getArgs().length <= 0) { - throw new ParseException("No , ft12 or tpuart specified."); - } else { - knxInterface = Utils.parseHost(cmdLine.getArgs()[0]); - } - } - logger.debug("knxInterface={}", knxInterface); - - if (cmdLine.hasOption(OPT_LONG_USER_ID)) { - logger.debug("KNX IP Secure userId is set"); // don't log knx secure ip specific options - userId = ((Number)cmdLine.getParsedOptionValue(OPT_LONG_USER_ID)).intValue(); - } - - if (cmdLine.hasOption(OPT_LONG_USER_PASSWORD)) { - logger.debug("KNX IP Secure userPassword is set"); // don't log knx secure ip specific options - userPassword = cmdLine.getOptionValue(OPT_LONG_USER_PASSWORD); - } - - if (cmdLine.hasOption(OPT_LONG_DEVICE_PASSWORD)) { - logger.debug("KNX IP Secure devicePassword is set"); // don't log knx secure ip specific options - devicePassword = cmdLine.getOptionValue(OPT_LONG_DEVICE_PASSWORD); - } - - if (cmdLine.hasOption(OPT_LONG_LOGSTATISTIC)) { - logStatistics = true; - } - logger.debug("logStatistics={}", logStatistics); - - // some logical checks for options which exclude each other - // differential mode and eraseflash makes no sense - if (eraseFullFlash() && (!full())) { - full = true; - logger.info("{}--{} is set. --> switching to full flash mode{}", ConColors.RED, OPT_LONG_ERASEFLASH, ConColors.RESET); - } - - if (nat() && (!tunnelingV1())) { - throw new ParseException(String.format("%sOption --%s can only be used together with --%s%s", ConColors.RED, OPT_LONG_NAT, OPT_LONG_TUNNEL_V1, ConColors.RESET)); - } - - int interfacesSet = 0; - if (!(userPassword().isEmpty()) && !(devicePassword().isEmpty())) interfacesSet++; - if (tunnelingV2()) interfacesSet++; - if (tunnelingV1()) interfacesSet++; - if (routing()) interfacesSet++; - if (!ft12().isEmpty()) interfacesSet++; - if (!tpuart().isEmpty()) interfacesSet++; - - if (interfacesSet > 1) { - throw new ParseException(String.format("%sOnly one bus interface can be used.%s", ConColors.RED, ConColors.RESET)); - } - - } catch (ParseException | KNXFormatException e) { - StringBuilder cliParsed = new StringBuilder(); - for (String arg : args) { - cliParsed.append(String.format("%s ", arg)); - } - logger.error("Invalid command line parameters:"); - logger.error("{}", cliParsed); - logger.error("{}{}{}", ConColors.RED, e.getMessage(), ConColors.RESET); - logger.error("For more information about the usage start with --{}", OPT_LONG_HELP); - logger.error("", e); - System.exit(0); - } - } - - public String helpToString() { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - helper.setWidth(PRINT_WIDTH); - helper.setOptionComparator(null); - helper.printHelp(pw, helper.getWidth(), helpApplicationName + " ", - "\n" + helpHeader + ":\n", cliOptions, helper.getLeftPadding(), - helper.getDescPadding(), helpFooter, true); - pw.flush(); - return sw.toString(); - } - - ///\todo move to Utils.java - private byte[] uidToByteArray(String str) { - String[] tokens = str.split(":"); - if (tokens.length != UPDProtocol.UID_LENGTH_USED) { - logger.warn("ignoring --uid {}, wrong size {}, expected {}", str, tokens.length, UPDProtocol.UID_LENGTH_USED); - return null; - } - byte[] uid = new byte[tokens.length]; - for (int n = 0; n < tokens.length; n++) { - uid[n] = (byte) Integer.parseUnsignedInt(tokens[n], 16); - } - return uid; - } - - public InetAddress knxInterface() { - return knxInterface; - } - - public InetAddress host() { - return knxInterface; - } - - public String fileName() { - return fileName; - } - - public InetAddress localhost() { - return localhost; - } - - public int localport() { - return localport; - } - - public int port() { - return port; - } - - public boolean nat() { - return nat; - } - - public String ft12() { - return ft12; - } - - public String tpuart() { - return tpuart; - } - - public boolean tunnelingV2() { return tunnelingV2; } - - public boolean tunnelingV1() { return tunnelingV1; } - - public boolean routing() { - return routing; - } - - public String medium() { - return medium; - } - - public IndividualAddress progDevice() { - return progDevice; - } - - public IndividualAddress device() { - return device; - } - - public IndividualAddress ownAddress() { - return ownAddress; - } - - public byte[] uid() { - return uid; - } - - public boolean full() { - return full; - } - - public int delay() { - return delay; - } - - public boolean NO_FLASH() { - return NO_FLASH; - } - - public boolean eraseFullFlash() { - return eraseFullFlash; - } - - public long dumpFlashStartAddress() { - return dumpFlashStartAddress; - } - - public long dumpFlashEndAddress() { - return dumpFlashEndAddress; - } - - public boolean help() { - return help; - } - - public boolean version() { - return version; - } - - public Level logLevel() { - return logLevel; - } - - public int userId() { - return userId; - } - - public String userPassword() { - return userPassword; - } - - public String devicePassword() { - return devicePassword; - } - - public Priority priority() { - return priority; - } - - public boolean logStatistics() { - return logStatistics; - } - - public String getOptionLongFileName() { - return OPT_LONG_FILENAME; - } - - public int getBlockSize() { - return blockSize; - } -} \ No newline at end of file diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/ConColors.java b/firmware_updater/updater/source/src/main/java/org/selfbus/updater/ConColors.java deleted file mode 100644 index 2ac6d61a..00000000 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/ConColors.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.selfbus.updater; - -/** - * Provides constants for console color output (font color/style and background) - */ -public class ConColors { - // Reset to default colors - public static final String RESET = "\033[0m"; // Reset - - // Regular Colors - public static final String BLACK = "\033[0;30m"; // BLACK - public static final String RED = "\033[0;31m"; // RED - public static final String GREEN = "\033[0;32m"; // GREEN - public static final String YELLOW = "\033[0;33m"; // YELLOW - public static final String BLUE = "\033[0;34m"; // BLUE - public static final String PURPLE = "\033[0;35m"; // PURPLE - public static final String CYAN = "\033[0;36m"; // CYAN - public static final String WHITE = "\033[0;37m"; // WHITE - - // Regular Bold - public static final String BOLD_BLACK = "\033[1;30m"; // BLACK - public static final String BOLD_RED = "\033[1;31m"; // RED - public static final String BOLD_GREEN = "\033[1;32m"; // GREEN - public static final String BOLD_YELLOW = "\033[1;33m"; // YELLOW - public static final String BOLD_BLUE = "\033[1;34m"; // BLUE - public static final String BOLD_PURPLE = "\033[1;35m"; // PURPLE - public static final String BOLD_CYAN = "\033[1;36m"; // CYAN - public static final String BOLD_WHITE = "\033[1;37m"; // WHITE - - // Regular Background - public static final String BG_BLACK = "\033[40m"; // BLACK - public static final String BG_RED = "\033[41m"; // RED - public static final String BG_GREEN = "\033[42m"; // GREEN - public static final String BG_YELLOW = "\033[43m"; // YELLOW - public static final String BG_BLUE = "\033[44m"; // BLUE - public static final String BG_PURPLE = "\033[45m"; // PURPLE - public static final String BG_CYAN = "\033[46m"; // CYAN - public static final String BG_WHITE = "\033[47m"; // WHITE - - // High Intensity - public static final String BRIGHT_BLACK = "\033[0;90m"; // BLACK - public static final String BRIGHT_RED = "\033[0;91m"; // RED - public static final String BRIGHT_GREEN = "\033[0;92m"; // GREEN - public static final String BRIGHT_YELLOW = "\033[0;93m"; // YELLOW - public static final String BRIGHT_BLUE = "\033[0;94m"; // BLUE - public static final String BRIGHT_PURPLE = "\033[0;95m"; // PURPLE - public static final String BRIGHT_CYAN = "\033[0;96m"; // CYAN - public static final String BRIGHT_WHITE = "\033[0;97m"; // WHITE - - // High Intensity Bold - public static final String BRIGHT_BOLD_BLACK = "\033[1;90m"; // BLACK - public static final String BRIGHT_BOLD_RED = "\033[1;91m"; // RED - public static final String BRIGHT_BOLD_GREEN = "\033[1;92m"; // GREEN - public static final String BRIGHT_BOLD_YELLOW = "\033[1;93m"; // YELLOW - public static final String BRIGHT_BOLD_BLUE = "\033[1;94m"; // BLUE - public static final String BRIGHT_BOLD_PURPLE = "\033[1;95m"; // PURPLE - public static final String BRIGHT_BOLD_CYAN = "\033[1;96m"; // CYAN - public static final String BRIGHT_BOLD_WHITE = "\033[1;97m"; // WHITE - - // High Intensity Background - public static final String BRIGHT_BG_BLACK = "\033[0;100m"; // BLACK - public static final String BRIGHT_BG_RED = "\033[0;101m"; // RED - public static final String BRIGHT_BG_GREEN = "\033[0;102m"; // GREEN - public static final String BRIGHT_BG_YELLOW = "\033[0;103m"; // YELLOW - public static final String BRIGHT_BG_BLUE = "\033[0;104m"; // BLUE - public static final String BRIGHT_BG_PURPLE = "\033[0;105m"; // PURPLE - public static final String BRIGHT_BG_CYAN = "\033[0;106m"; // CYAN - public static final String BRIGHT_BG_WHITE = "\033[0;107m"; // WHITE -} - diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/DeviceManagement.java b/firmware_updater/updater/source/src/main/java/org/selfbus/updater/DeviceManagement.java deleted file mode 100644 index 01c81579..00000000 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/DeviceManagement.java +++ /dev/null @@ -1,440 +0,0 @@ -package 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.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.mgmt.Destination; -import tuwien.auto.calimero.mgmt.KNXDisconnectException; -import tuwien.auto.calimero.mgmt.ManagementProcedures; -import tuwien.auto.calimero.mgmt.ManagementProceduresImpl; - -import java.time.Duration; -import java.util.Arrays; - -import static org.selfbus.updater.Mcu.MAX_FLASH_ERASE_TIMEOUT; -import static org.selfbus.updater.upd.UPDProtocol.COMMAND_POSITION; -import static org.selfbus.updater.upd.UPDProtocol.DATA_POSITION; - -/** - * Provides methods to send firmware update telegrams to the bootloader (MCU) - */ -public final class DeviceManagement { - private static final int RESTART_ERASE_CODE = 7; //!< EraseCode for the APCI_MASTER_RESET_PDU (valid from 1..7) - private static final int RESTART_CHANNEL = 255; //!< Channelnumber for the APCI_MASTER_RESET_PDU - - private static final int MAX_UPD_COMMAND_RETRY = 3; //!< default maximum retries a UPD command is sent to the client - - private UDPProtocolVersion protocolVersion; - - private int blockSize; - private int maxPayload; - private int updSendDataOffset; - - private DeviceManagement () { - setProtocolVersion(UDPProtocolVersion.UDP_V1); - } - - private final static Logger logger = LoggerFactory.getLogger(DeviceManagement.class.getName()); - private SBManagementClientImpl mc; //!< calimero device management client - private Destination progDestination; - private KNXNetworkLink link; - - public DeviceManagement(KNXNetworkLink link, IndividualAddress progDevice, Priority priority) - throws KNXLinkClosedException { - this(); - this.link = link; - logger.debug("Creating SBManagementClientImpl"); - this.mc = new SBManagementClientImpl(this.link); - this.mc.setPriority(priority); - this.progDestination = this.mc.createDestination(progDevice, true, false, false); - } - - public void restartProgrammingDevice() - throws KNXTimeoutException, KNXLinkClosedException, InterruptedException { - logger.info("Restarting device {}", progDestination); - mc.restart(progDestination); - } - - private void waitRestartTime(int restartTimeSeconds) { - try { - while (restartTimeSeconds > 0) { - Thread.sleep(1000); - System.out.printf("%s%s%s", ConColors.BRIGHT_GREEN, Utils.PROGRESS_MARKER, ConColors.RESET); - restartTimeSeconds--; - } - } - catch (final InterruptedException e) { - logger.error("InterruptedException ", e); - } - } - - /** - * Restarts the 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 List> getAllInterfaces(){ - - List> interfacesResult = null; - try { - // set true to be aware of Network Address Translation (NAT) during discovery - final boolean useNAT = false; - interfacesResult = Discoverer.udp(useNAT).timeout(Duration.ofSeconds(3)).search().get(); - }catch (InterruptedException | ExecutionException e) { - System.err.println("Error during KNXnet/IP discovery: " + e); - } - - return interfacesResult; - } -} diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/FlashFullMode.java b/firmware_updater/updater/source/src/main/java/org/selfbus/updater/FlashFullMode.java deleted file mode 100644 index d568dadb..00000000 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/FlashFullMode.java +++ /dev/null @@ -1,125 +0,0 @@ -package org.selfbus.updater; - -import org.selfbus.updater.upd.UPDCommand; -import org.selfbus.updater.upd.UPDProtocol; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import tuwien.auto.calimero.KNXRemoteException; -import tuwien.auto.calimero.KNXTimeoutException; -import tuwien.auto.calimero.link.KNXLinkClosedException; -import tuwien.auto.calimero.mgmt.KNXDisconnectException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import static org.selfbus.updater.upd.UDPResult.*; - -/** - * Provides full flash mode for the bootloader (MCU) - */ -public class FlashFullMode { - private final static Logger logger = LoggerFactory.getLogger(FlashFullMode.class.getName()); - - /** - * Normal update routine, sending complete image - */ - public static ResponseResult doFullFlash(DeviceManagement dm, BinImage newFirmware, int dataSendDelay, - boolean eraseFirmwareRange, boolean logStatistics) - throws IOException, KNXDisconnectException, KNXTimeoutException, KNXLinkClosedException, - InterruptedException, UpdaterException, KNXRemoteException { - ResponseResult resultSendData, resultProgramData; - ResponseResult resultTotal = new ResponseResult(); // collect so static data - - long totalLength = newFirmware.length(); - ByteArrayInputStream fis = new ByteArrayInputStream(newFirmware.getBinData()); - - if (eraseFirmwareRange) { - dm.eraseAddressRange(newFirmware.startAddress(), totalLength); // erase affected flash range - } - - byte[] buffer = new byte[dm.getBlockSize()]; - long progAddress = newFirmware.startAddress(); - - String logMessage = String.format("Start sending application data (%d bytes)", totalLength); - if (dataSendDelay > 0) { - logMessage += String.format(" with telegram delay of %dms", dataSendDelay); - } - logger.info(logMessage); - - int nRead = 0; - boolean repeat = false; - while (fis.available() > 0) { - if (!repeat) { - nRead = fis.read(buffer); // Read up to size of buffer - } - else { - repeat = false; - System.out.println(); - } - - byte[] txBuffer = new byte[nRead]; - System.arraycopy(buffer, 0, txBuffer, 0, nRead); - - // send data to the bootloader - long flashTimeStart = System.currentTimeMillis(); // time this run started - resultSendData = dm.doFlash(txBuffer, dm.getMaxUpdCommandRetry(), dataSendDelay); - resultTotal.addCounters(resultSendData); // keep track of static data - - // logging of connection speed - long flashTimeDuration = System.currentTimeMillis() - flashTimeStart; - float bytesPerSecond = (float) resultSendData.written() / (flashTimeDuration / 1000f); - String col; - if (bytesPerSecond >= 50.0) { - col = ConColors.BRIGHT_GREEN; - } else { - col = ConColors.BRIGHT_YELLOW; - } - String percentageDone = String.format("%5.1f", (float) 100 * (resultTotal.written()) / totalLength); - String progressInfo = String.format("%s %s%% %s%6.2f B/s%s", ConColors.BRIGHT_GREEN, percentageDone, col, bytesPerSecond, ConColors.RESET); - // Check if printed Utils.PROGRESS_MARKER and progressInfo would exceed console width - int progressMarkerLength = dm.getBlockSize()/(dm.getMaxPayload() * Utils.PROGRESS_MARKER.length()); - if ((progressMarkerLength + progressInfo.length()) > Utils.CONSOLE_WIDTH) { - System.out.println(); - } - - // flash the previously sent data - int crc32 = Utils.crc32Value(txBuffer); - byte[] progPars = new byte[2 + 4 + 4]; - Utils.shortToStream(progPars, 0, (short)txBuffer.length); - Utils.longToStream(progPars, 2, progAddress); - Utils.longToStream(progPars, 6, crc32); - - String programFlashInfo = String.format("%s 0x%04X-0x%04X", progressInfo, progAddress, progAddress + txBuffer.length - 1); - if (txBuffer.length != dm.getBlockSize()) - { - programFlashInfo = String.format("%s (%d bytes)", programFlashInfo, txBuffer.length); - } - logger.info(programFlashInfo); - logger.trace("with crc32 {}", String.format("crc32 0x%08X", crc32)); - - resultProgramData = dm.sendWithRetry(UPDCommand.PROGRAM, progPars, dm.getMaxUpdCommandRetry()); - - long result = UPDProtocol.checkResult(resultProgramData.data()); - if ((result == BYTECOUNT_RECEIVED_TOO_LOW.id) || (result == BYTECOUNT_RECEIVED_TOO_HIGH.id)) { - repeat = true; - resultTotal.setWritten(resultTotal.written() - txBuffer.length); // do not count failed transfer - } - else if (result == IAP_COMPARE_ERROR.id) { - throw new UpdaterException(String.format("ProgramData update failed. %sTry again with option '--%s 256'%s", - ConColors.RED, CliOptions.OPT_LONG_BLOCKSIZE, ConColors.RESET)); - } - else if (result == IAP_SUCCESS.id) { - progAddress += txBuffer.length; - if (logStatistics) { - dm.requestBootLoaderStatistic(); - } - } - else { - dm.restartProgrammingDevice(); - throw new UpdaterException("ProgramData update failed."); - } - resultTotal.addCounters(resultProgramData); // keep track of statistic data - } - fis.close(); - return resultTotal; - } -} diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/SBKNXLink.java b/firmware_updater/updater/source/src/main/java/org/selfbus/updater/SBKNXLink.java deleted file mode 100644 index e9497a82..00000000 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/SBKNXLink.java +++ /dev/null @@ -1,150 +0,0 @@ -package org.selfbus.updater; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import tuwien.auto.calimero.IndividualAddress; -import tuwien.auto.calimero.KNXException; -import tuwien.auto.calimero.KNXIllegalArgumentException; -import tuwien.auto.calimero.knxnetip.KNXnetIPRouting; -import tuwien.auto.calimero.knxnetip.SecureConnection; -import tuwien.auto.calimero.link.KNXNetworkLink; -import tuwien.auto.calimero.link.KNXNetworkLinkFT12; -import tuwien.auto.calimero.link.KNXNetworkLinkIP; -import tuwien.auto.calimero.link.KNXNetworkLinkTpuart; -import tuwien.auto.calimero.link.medium.KNXMediumSettings; -import tuwien.auto.calimero.link.medium.RFSettings; -import tuwien.auto.calimero.link.medium.TPSettings; - -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.util.Collections; - -public class SBKNXLink { - private static final Logger logger = LoggerFactory.getLogger(CliOptions.class.getName()); - private CliOptions cliOptions; - - public SBKNXLink() { - - } - - private KNXNetworkLink createSecureTunnelingLink(InetSocketAddress local, InetSocketAddress remote, - KNXMediumSettings medium) throws KNXException, InterruptedException { - // KNX IP Secure TCP tunneling v2 connection - logger.info("Connect using KNX IP Secure tunneling..."); - byte[] deviceAuthCode = SecureConnection.hashDeviceAuthenticationPassword(cliOptions.devicePassword().toCharArray()); - byte[] userKey = SecureConnection.hashUserPassword(cliOptions.userPassword().toCharArray()); - final var session = Utils.tcpConnection(local, remote).newSecureSession(cliOptions.userId(), userKey, deviceAuthCode); - return KNXNetworkLinkIP.newSecureTunnelingLink(session, medium); - } - - private KNXNetworkLink createTunnelingLinkV2(InetSocketAddress local, InetSocketAddress remote, - KNXMediumSettings medium) throws KNXException, InterruptedException { - logger.info("Connect using TCP tunneling v2..."); - final var session = Utils.tcpConnection(local, remote); - return KNXNetworkLinkIP.newTunnelingLink(session, medium); - } - - private KNXNetworkLink createTunnelingLinkV1(InetSocketAddress local, InetSocketAddress remote, boolean useNat, - KNXMediumSettings medium) throws KNXException, InterruptedException { - logger.info("{}Connect using UDP tunneling v1 (nat:{})...{}", ConColors.YELLOW, useNat, ConColors.RESET); - return KNXNetworkLinkIP.newTunnelingLink(local, remote, useNat, medium); - } - - private KNXNetworkLink createRoutingLink(InetSocketAddress local, KNXMediumSettings medium) throws KNXException { - logger.info("{}Connect using routing (multicast:{})...{}", ConColors.YELLOW, KNXnetIPRouting.DefaultMulticast, ConColors.RESET); - - return KNXNetworkLinkIP.newRoutingLink(local.getAddress(), KNXnetIPRouting.DefaultMulticast, medium); - } - - private static InetSocketAddress createLocalSocket(final InetAddress host, final Integer port) { - final int p = port != null ? port.intValue() : 0; - return host != null ? new InetSocketAddress(host, p) : new InetSocketAddress(p); - } - - private static KNXMediumSettings getMedium(final String id, IndividualAddress ownAddress) { - if (id.equals("tp1")) { - return new TPSettings(ownAddress); - } else if (id.equals("rf")) { - return new RFSettings(null); - } - //else if (id.equals("tp0")) - // return TPSettings.TP0; - //else if (id.equals("p110")) - // return new PLSettings(false); - //else if (id.equals("p132")) - // return new PLSettings(true); - else - throw new KNXIllegalArgumentException("unknown medium"); - } - - public void setCliOptions(CliOptions cliOptions) { - this.cliOptions = cliOptions; - } - - /** - * Creates the KNX network link to access the network specified in - * options. - *

- * - * @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> responseFilter = (responder, apdu) -> { - if (responder.equals(dst.getAddress())) { - return Optional.of(apdu); - } - else { - return Optional.empty(); - } - }; - 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); - - final byte[] response = this.sendWait(dst, getPriority(), send, apciResponse, 2, MAX_ASDU_LENGTH, responseTimeout()); - return response; - } -} diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/Updater.java b/firmware_updater/updater/source/src/main/java/org/selfbus/updater/Updater.java deleted file mode 100644 index f3d1de50..00000000 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/Updater.java +++ /dev/null @@ -1,419 +0,0 @@ -package org.selfbus.updater; - -import org.selfbus.updater.bootloader.BootloaderStatistic; -import tuwien.auto.calimero.*; -import org.apache.commons.cli.ParseException; -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 tuwien.auto.calimero.link.KNXNetworkLink; - -import java.io.FileNotFoundException; -import java.io.IOException; - -import static org.selfbus.updater.Utils.shortenPath; - -import org.selfbus.updater.gui.GuiMain; - -/** - * A Tool for updating firmware of a Selfbus device in a KNX network. - *

- * Updater is a {@link Runnable} tool implementation allowing a user to update - * KNXduino devices.
- *
- * 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. - *

- * When running this tool from the console, the 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. - *

- * 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, 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:%= 0) && (cliOptions.dumpFlashEndAddress() >= 0)) { - logger.warn("{}Dumping flash content range 0x{}-0x{} to bootloader's serial port.{}", - ConColors.BRIGHT_GREEN, String.format("%04X", cliOptions.dumpFlashStartAddress()), String.format("%04X", cliOptions.dumpFlashEndAddress()), ConColors.RESET); - dm.dumpFlashRange(cliOptions.dumpFlashStartAddress(), cliOptions.dumpFlashEndAddress()); - 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.info("Requesting APP_VERSION..."); - String appVersion = dm.requestAppVersionString(); - if (appVersion != null) { - logger.info(" Current APP_VERSION: {}{}{}", ConColors.BRIGHT_GREEN, appVersion, ConColors.RESET); - } - else { - logger.info("{} failed!{}", ConColors.BRIGHT_RED, ConColors.RESET); - } - - // From here on we need a valid firmware - if (newFirmware == null) { - if (cliOptions.device() != null) { - dm.restartProgrammingDevice(); - } - // to get here `uid == null` must be true, so it's fine to exit with no-error - link.close(); - System.exit(0); - } - - if (cliOptions.eraseFullFlash()) { - logger.warn("{}Deleting the entire flash except from the bootloader itself!{}", ConColors.BRIGHT_RED, ConColors.RESET); - dm.eraseFlash(); - } - - // 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 : {}{}{}", ConColors.BRIGHT_GREEN, newFirmware.getAppVersion(), ConColors.RESET); - - // Check if FW image has correct offset for MCUs bootloader size - if (newFirmware.startAddress() < bootLoaderIdentity.getApplicationFirstAddress()) { - logger.error("{} Error! The specified firmware image would overwrite parts of the bootloader. Check FW offset setting in the linker!{}", ConColors.BRIGHT_RED, ConColors.RESET); - logger.error("{} Firmware needs to start at or beyond 0x{}{}", ConColors.BRIGHT_RED, String.format("%04X", bootLoaderIdentity.getApplicationFirstAddress()), ConColors.RESET); - throw new UpdaterException("Firmware offset not correct!"); - } - else if (newFirmware.startAddress() == bootLoaderIdentity.getApplicationFirstAddress()) { - logger.info(" {}Firmware starts directly beyond bootloader.{}", ConColors.BRIGHT_GREEN, ConColors.RESET); - } - else { - logger.info(" {}Info: There are {} bytes of unused flash between bootloader and firmware.{}", - ConColors.BRIGHT_YELLOW, newFirmware.startAddress() - bootLoaderIdentity.getApplicationFirstAddress(), ConColors.RESET); - } - - boolean diffMode = false; - if (!(cliOptions.full())) { - if (bootDescriptor.valid()) { - diffMode = FlashDiffMode.setupDifferentialMode(bootDescriptor); - } - else { - logger.error("{} BootDescriptor is not valid -> switching to full mode{}", ConColors.BRIGHT_RED, ConColors.RESET); - } - } - - if ((bootLoaderIdentity.getVersionMajor()) <= 1 && (bootLoaderIdentity.getVersionMinor() < 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.{}", ConColors.YELLOW, - cliOptions.getBlockSize(), dm.getBlockSize(), ConColors.RESET); - } - - if (!cliOptions.NO_FLASH()) { // is flashing firmware disabled? for debugging use only! - // Start to flash the new firmware - long flashTimeStart = System.currentTimeMillis(); // time flash process started - ResponseResult resultTotal; - logger.info("{}{}Starting to send new firmware now:{}", ConColors.BLACK, ConColors.BG_GREEN, ConColors.RESET); - if (diffMode && FlashDiffMode.isInitialized()) { - logger.error("{}Differential mode is EXPERIMENTAL -> Use with caution.{}", ConColors.BRIGHT_RED, ConColors.RESET); - resultTotal = FlashDiffMode.doDifferentialFlash(dm, newFirmware.startAddress(), newFirmware.getBinData()); - } - else { - resultTotal = FlashFullMode.doFullFlash(dm, newFirmware, cliOptions.delay(), !cliOptions.eraseFullFlash(), cliOptions.logStatistics()); - } - logger.info("Requesting Bootloader statistic..."); - dm.requestBootLoaderStatistic(); - - String updaterStatisticMsg = " Updater: "; - String colored; - if (resultTotal.dropCount() > BootloaderStatistic.THRESHOLD_DISCONNECT) { - colored = ConColors.BRIGHT_YELLOW; - } else { - colored = ConColors.BRIGHT_GREEN; - } - updaterStatisticMsg += String.format("#Disconnect: %s%2d%s", colored, resultTotal.dropCount(), ConColors.RESET); - if (resultTotal.timeoutCount() > BootloaderStatistic.THRESHOLD_REPEATED) { - colored = ConColors.BRIGHT_YELLOW; - } else { - colored = ConColors.BRIGHT_GREEN; - } - updaterStatisticMsg += String.format(" #Timeout : %s%2d%s", colored, resultTotal.timeoutCount(), ConColors.RESET); - logger.info("{}", updaterStatisticMsg); - printStatisticData(flashTimeStart, resultTotal); - } - else { - logger.warn("--{} => {}only boot description block will be written{}", CliOptions.OPT_LONG_NO_FLASH, ConColors.RED, ConColors.RESET); - } - - BootDescriptor newBootDescriptor = new BootDescriptor(newFirmware.startAddress(), - newFirmware.endAddress(), - (int) newFirmware.crc32(), - newFirmware.startAddress() + newFirmware.getAppVersionAddress()); - logger.info("Preparing boot descriptor with {}", newBootDescriptor); - dm.programBootDescriptor(newBootDescriptor, cliOptions.delay()); - String deviceInfo = cliOptions.progDevice().toString(); - if (cliOptions.device() != null) { - deviceInfo = cliOptions.device().toString(); - } - logger.info("Finished programming {}device {} with {}{}", ConColors.BRIGHT_YELLOW, deviceInfo, shortenPath(cliOptions.fileName(), 1), ConColors.RESET); - logger.info("{}{}Firmware Update done, Restarting device now...{}", ConColors.BLACK, ConColors.BG_GREEN, ConColors.RESET); - dm.restartProgrammingDevice(); - - if (newFirmware.getAppVersion().contains(BootloaderUpdater.BOOTLOADER_UPDATER_ID_STRING)) { - logger.info("{}{}Wait {} second(s) for Bootloader Updater to finish its job...{}", ConColors.BLACK, ConColors.BG_GREEN, - String.format("%.2f", BootloaderUpdater.BOOTLOADER_UPDATER_MAX_RESTART_TIME_MS / 1000.0f), ConColors.RESET); - Thread.sleep(BootloaderUpdater.BOOTLOADER_UPDATER_MAX_RESTART_TIME_MS); - } - } catch (final KNXException | UpdaterException | RuntimeException e) { - thrown = e; - } catch (final InterruptedException e) { - canceled = true; - Thread.currentThread().interrupt(); - } catch (FileNotFoundException e) { - logger.error("FileNotFoundException ", e); - } catch (IOException e) { - logger.error("IOException ", e); - } catch (Throwable e) { - logger.error("Throwable ", e); - } finally { - if (link != null) - link.close(); - onCompletion(thrown, canceled); - } - } - - public String requestUid(){ - KNXNetworkLink link; - try { - link = this.sbKNXLink.openLink(); - DeviceManagement dm = new DeviceManagement(link, cliOptions.progDevice(), cliOptions.priority()); - logger.info("KNX connection: {}", link); - - //for option --device restart the device in bootloader mode - if (cliOptions.device() != 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.device()); - } - - dm.checkDeviceInProgrammingMode(cliOptions.progDevice()); - - byte[] uid = dm.requestUIDFromDevice(); - - if (cliOptions.device() != null) { - dm.restartProgrammingDevice(); - } - link.close(); - return Utils.byteArrayToHex(uid); - - } catch (InterruptedException | UpdaterException | KNXException e) { - throw new RuntimeException(e); - } - } -} diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/bootloader/BootloaderIdentity.java b/firmware_updater/updater/source/src/main/java/org/selfbus/updater/bootloader/BootloaderIdentity.java deleted file mode 100644 index 7a541306..00000000 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/bootloader/BootloaderIdentity.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.selfbus.updater.bootloader; - -import org.selfbus.updater.Utils; - -/** - * Holds Bootloader identity information - * see software-arm-lib/Bus-Updater/src/update.cpp (method updRequestBootloaderIdentity) for more information. - */ -public class BootloaderIdentity { - private final long versionMajor; - private final long versionMinor; - private final long versionSBLibMajor; - private final long versionSBLibMinor; - private final long features; - private final 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 BootloaderIdentity(long versionMajor, long versionMinor, long versionSBLibMajor, long versionSBLibMinor, long features, long applicationFirstAddress) { - this.versionSBLibMajor = versionSBLibMajor; - this.versionSBLibMinor = versionSBLibMinor; - this.features = features; - this.applicationFirstAddress = applicationFirstAddress; - this.versionMajor = versionMajor; - this.versionMinor = versionMinor; - } - - public static BootloaderIdentity fromArray(byte[] parse) { - long vMajor = parse[0] & 0xff; - long vMinor = parse[1] & 0xff; - long features = Utils.streamToShort(parse, 2); - 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 ); - } - - public String toString() { - return String.format("Version: %s, sbLib Version: %s, Features: 0x%04X, App-start: 0x%04X", - getVersion(), getVersionSBLib(), getFeatures(), getApplicationFirstAddress()); - } - - public long getFeatures() - { - return features; - } - - public long getApplicationFirstAddress() - { - return applicationFirstAddress; - } - - public long getVersionMajor() - { - return versionMajor; - } - - public long getVersionMinor() - { - return versionMinor; - } - - public long getVersionSBLibMajor() { - return versionSBLibMajor; - } - - public long getVersionSBLibMinor() { - return versionSBLibMinor; - } - - public String getVersion() { - return String.format("%d.%02d", getVersionMajor(), getVersionMinor()); - } - - public String getVersionSBLib() { - return hexVersionToString(getVersionSBLibMajor(), getVersionSBLibMinor()); - } -} diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/gui/ConColorsToStyledDoc.java b/firmware_updater/updater/source/src/main/java/org/selfbus/updater/gui/ConColorsToStyledDoc.java deleted file mode 100644 index c512881f..00000000 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/gui/ConColorsToStyledDoc.java +++ /dev/null @@ -1,113 +0,0 @@ -package org.selfbus.updater.gui; - -import javax.swing.text.*; -import java.awt.*; -import java.util.Objects; - -public class ConColorsToStyledDoc { - - private static final StyleContext sc = new StyleContext(); - private static final Style stringStyle = sc.getStyle(StyleContext.DEFAULT_STYLE); - - /* - * hier werden die Steuerzeichen für die Farben der Windows Konsole in Java Parameter gewandelt - * z.B. wird ein String "\033[0;31m dieser Text wird rot sein,\033[44m jetzt auf blauem Hintergrund" übergeben - * Dieser String wird entsprechend der Steuerbefehle in ein StyledDocument gefüllt - */ - public static void Convert(String docWithConColors, StyledDocument originDocument) throws BadLocationException { - - if(!docWithConColors.contains("\033")) { - // ohne ConColors im Datenstring - originDocument.insertString(originDocument.getLength(), docWithConColors, null); - return; - } - - String colorCode =""; - int startPart = 0; - int startIndex = docWithConColors.indexOf("\033"); - String[] splitDoc = docWithConColors.split("\033"); - - // wenn der String vor dem ersten ConColor Zeichen hat, werden diese ohne Formatierung angehängt - if(startIndex != 0){ - startPart = 1; - originDocument.insertString(originDocument.getLength(), splitDoc[0], null); - } - - for (int docPartCnt = startPart; docPartCnt < splitDoc.length; docPartCnt++) { - String docPart = splitDoc[docPartCnt]; - if(!docPart.isEmpty()) { - for (int i = 2; i < 7; i++) { - String endChar = docPart.substring(i, i + 1); - if (endChar.equals("m")) { - colorCode = docPart.substring(1, i); - break; - } - } - String cleanedString = docPart.replace("[" + colorCode + "m", ""); - - Style partStyle = convertConColorToStyle(colorCode); - - int oldDocLength = originDocument.getLength(); - - originDocument.insertString(originDocument.getLength(), cleanedString, null); - - originDocument.setCharacterAttributes(oldDocLength, cleanedString.length(), partStyle,false); - } - } - } - - private static Style convertConColorToStyle(String colorCode){ - // ColorCode kommt z.B. 0;30 oder 0;105 oder 41 oder 0 oder ... - - if(colorCode.contains(";")){ - // Regular Colors or High Intensity or High Intensity Background or Regular Bold or High Intensity Bold - String[] colorCodeParts = colorCode.split(";"); - if(Objects.equals(colorCodeParts[0], "0")){ // Regular Colors or High Intensity or High Intensity Background - if(Integer.parseInt(colorCodeParts[1]) < 100){ - // Regular Colors or High Intensity - Color color = ConColorToColor(Integer.parseInt(colorCodeParts[1].substring(1,2))); - StyleConstants.setForeground(stringStyle, color); - }else{ - // High Intensity Background - Color color = ConColorToColor(Integer.parseInt(colorCodeParts[1].substring(2,3))); - StyleConstants.setBackground(stringStyle, color); - } - }else if(Objects.equals(colorCodeParts[0], "1")){ - // Regular Bold or High Intensity Bold - Color color = ConColorToColor(Integer.parseInt(colorCodeParts[1].substring(1,2))); - StyleConstants.setForeground(stringStyle, color); - StyleConstants.setBold(stringStyle,true); - } - }else{ - // Regular Background or Reset - if(colorCode.equals("0")){ - // Reset - StyleConstants.setForeground(stringStyle, Color.white); - StyleConstants.setBackground(stringStyle, Color.black); - StyleConstants.setBold(stringStyle,false); - } - else{ - // Regular Background - Color color = ConColorToColor(Integer.parseInt(colorCode.substring(1,2))); - StyleConstants.setBackground(stringStyle, color); - } - } - return stringStyle; - } - - private static Color ConColorToColor(int colorNumber){ - Color retColor = switch (colorNumber) { - case 0 -> Color.black; - case 1 -> Color.red; - case 2 -> Color.green; - case 3 -> Color.orange; - case 4 -> Color.blue; - case 5 -> Color.pink; - case 6 -> Color.cyan; - case 7 -> Color.white; - default -> null; - }; - - return retColor; - } -} diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/gui/TextAppender.java b/firmware_updater/updater/source/src/main/java/org/selfbus/updater/gui/TextAppender.java deleted file mode 100644 index aa589e37..00000000 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/gui/TextAppender.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.selfbus.updater.gui; - -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.AppenderBase; - -import javax.swing.*; -import javax.swing.text.StyledDocument; -import java.util.ArrayList; - -import static javax.swing.SwingUtilities.invokeLater; - -public class TextAppender extends AppenderBase { - - private static final ArrayList textPanes = new ArrayList<>(); - - // Add the target JTextPane to be populated and updated by the logging information. - public static void addLog4j2TextPaneAppender(final JTextPane textPane){ - TextAppender.textPanes.add(textPane); - } - - @Override - protected void append(ILoggingEvent event) { - String message = event.getFormattedMessage(); - message = message + System.lineSeparator(); // jeden Eintrag als newline enden lassen - - String finalMessage = message; - - // Append formatted message to text area using the Thread. - try - { - invokeLater(() -> - { - for (JTextPane textPane : textPanes) - { - try - { - if (textPane != null) - { - StyledDocument document = (StyledDocument) textPane.getDocument(); - ConColorsToStyledDoc.Convert(finalMessage, document); - - // immer die letzte Zeile zeigen - textPane.setCaretPosition(textPane.getDocument().getLength()); - } - } catch (Throwable throwable) - { - throwable.printStackTrace(); - } - } - }); - } catch (IllegalStateException exception) - { - exception.printStackTrace(); - } - } -} \ No newline at end of file diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/upd/UPDCommand.java b/firmware_updater/updater/source/src/main/java/org/selfbus/updater/upd/UPDCommand.java deleted file mode 100644 index 91bd856f..00000000 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/upd/UPDCommand.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.selfbus.updater.upd; - -import java.util.HashMap; -import java.util.Map; - -/** - * Implementation of the UPD/UDP protocol control commands - * see /Bus-Updater/inc/upd_protocol.h for details - */ -public enum UPDCommand { - // ERASE_SECTOR(0, "ERASE_SECTOR"), //!< Erase flash sector number (data[3]) @note device must be unlocked - //!< @note deprecated, use ERASE_ADDRESSRANGE instead - SEND_DATA((byte)0xef, "SEND_DATA"), //!< Copy ((data[0] & 0x0f)-1) bytes to ramBuffer starting from address data[3] @note device must be unlocked - PROGRAM((byte)0xee, "PROGRAM"), //!< Copy count (data[3-6]) bytes from ramBuffer to address (data[7-10]) in flash buffer, crc in data[11-14] @note device must be unlocked - UPDATE_BOOT_DESC((byte)0xed, "UPDATE_BOOT_DESC"), //!< Flash an application boot descriptor block @note device must be unlocked - SEND_DATA_TO_DECOMPRESS((byte)0xec, "SEND_DATA_TO_DECOMPRESS"), //!< Copy bytes from telegram (data) to ramBuffer with differential method @note device must be unlocked - PROGRAM_DECOMPRESSED_DATA((byte)0xeb, "PROGRAM_DECOMPRESSED_DATA"), //!< Flash bytes from ramBuffer to flash with differential method @note device must be unlocked - ERASE_COMPLETE_FLASH((byte)0xea, "ERASE_COMPLETE_FLASH"), //!< Erase the entire flash area excluding the bootloader itself @note device must be unlocked - ERASE_ADDRESS_RANGE((byte)0xe9, "ERASE_ADDRESS_RANGE"), //!< Erase flash from given start address to end address (start: data[3-6] end: data[7-10]) @note device must be unlocked - REQ_DATA((byte)0xe8, "REQ_DATA"), //!< Return bytes from flash at given address? @note device must be unlocked - //!<@warning Not implemented - DUMP_FLASH((byte)0xe7, "DUMP_FLASH"), //!< DUMP the flash of a given address range (data[0-3] - data[4-7]) to serial port of the mcu, works only with DEBUG version of bootloader - REQUEST_STATISTIC((byte)0xdf, "STATISTIC_REQUEST"), //!< Return some statistic data for the active connection - RESPONSE_STATISTIC((byte)0xde, "STATISTIC_RESPONSE"), //!< Response for @ref UPD_STATISTIC_RESPONSE containing the statistic data - SEND_LAST_ERROR((byte)0xdc, "SEND_LAST_ERROR"), //!< Response containing the last error - UNLOCK_DEVICE((byte)0xbf, "UNLOCK_DEVICE"), //!< Unlock the device for operations, which are only allowed on a unlocked device - REQUEST_UID((byte)0xbe, "REQUEST_UID"), //!< Return the 12 byte shorten UID (GUID) of the mcu @note device must be unlocked - RESPONSE_UID((byte)0xbd, "RESPONSE_UID"), //!< Response for @ref REQUEST_UID containing the first 12 bytes of the UID - APP_VERSION_REQUEST((byte)0xbc, "APP_VERSION_REQUEST"), //!< Return address of AppVersion string - APP_VERSION_RESPONSE((byte)0xbb, "APP_VERSION_RESPONSE"), //!< Response for @ref APP_VERSION_REQUEST containing the application version string - // RESET(35, "RESET"), //!< Reset the device @note device must be unlocked - REQUEST_BOOT_DESC((byte)0xba, "REQUEST_BOOT_DESC"), //!< Return the application boot descriptor block @note device must be unlocked - RESPONSE_BOOT_DESC((byte)0xb9, "RESPONSE_BOOT_DESC"), //!< Response for @ref REQUEST_BOOT_DESC containing the application boot descriptor block - REQUEST_BL_IDENTITY((byte)0xb8, "REQUEST_BL_IDENTITY"), //!< Return the bootloader's identity @note device must be unlocked - RESPONSE_BL_IDENTITY((byte)0xb7, "RESPONSE_BL_IDENTITY"), //!< Response for @ref UPD_REQUEST_BL_IDENTITY containing the identity - RESPONSE_BL_VERSION_MISMATCH((byte)0xb6, "RESPONSE_BL_VERSION_MISMATCH"), //!< Response for @ref UPD_REQUEST_BL_IDENTITY containing the minimum required major and minor version of Selfbus Updater - SET_EMULATION((byte)0x01, "SET_EMULATION"); //!<@warning Not implemented - - private static final Map BY_INDEX = new HashMap<>(); - static { - for (UPDCommand e: values()) { - BY_INDEX.put(e.id, e); - } - } - - public final byte id; - private final String description; - UPDCommand(byte id, String description) { - this.id = id; - this.description = description; - } - - public static UPDCommand valueOfIndex(Integer index) { - return BY_INDEX.get(index); - } - @Override - public String toString() { - return String.format("%s.%s", this.getClass().getSimpleName(), this.description); - } -} diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/upd/UPDProtocol.java b/firmware_updater/updater/source/src/main/java/org/selfbus/updater/upd/UPDProtocol.java deleted file mode 100644 index f8e4ad52..00000000 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/upd/UPDProtocol.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.selfbus.updater.upd; - -import ch.qos.logback.classic.Level; -import org.selfbus.updater.ConColors; -import org.selfbus.updater.Utils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Implementation of the UPD/UDP protocol handling - */ -public final class UPDProtocol { - private static final Logger logger = LoggerFactory.getLogger(UPDProtocol.class.getName()); - - public static final int COMMAND_POSITION = 2; - public static final int DATA_POSITION = 3; - public static final int UID_LENGTH_USED = 12; //!< uid/guid length of the mcu used for unlocking/flashing - public static final int UID_LENGTH_MAX = 16; //!< uid/guid length of the mcu - - private UPDProtocol() {} - - public static long checkResult(byte[] result) { - return checkResult(result, true); - } - - public static long checkResult(byte[] result, boolean verbose) { - if (result[COMMAND_POSITION] != UPDCommand.SEND_LAST_ERROR.id) { - logger.error("checkResult called on other than UPDCommand.SEND_LAST_ERROR.id=0x{}, result[{}]=0x{}", - String.format("%02X", UPDCommand.SEND_LAST_ERROR.id), - COMMAND_POSITION, - String.format("%02X", result[COMMAND_POSITION])); - return 0; - } - - UDPResult udpResult = UDPResult.valueOfIndex(result[DATA_POSITION]); - if (udpResult.isError()) { - logger.error("{}{} resultCode=0x{}{}", ConColors.BRIGHT_RED, udpResult, String.format("%02X", udpResult.id), ConColors.RESET); - } else { - if (verbose) { - ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); - if (root.getLevel() == Level.TRACE) { - // only to display the message on the console - logger.debug("{}done ({}){}", ConColors.BRIGHT_GREEN, udpResult.id, ConColors.RESET); - } - else { - logger.trace("{}done ({}){}", ConColors.BRIGHT_GREEN, udpResult.id, ConColors.RESET); - } - - } else { - System.out.printf("%s%s%s", ConColors.BRIGHT_GREEN, Utils.PROGRESS_MARKER, ConColors.RESET); // Success in green - logger.debug(Utils.PROGRESS_MARKER); - } - } - return udpResult.id; - } -} diff --git a/firmware_updater/updater/source/src/main/resources/GuiTranslation_de.properties b/firmware_updater/updater/source/src/main/resources/GuiTranslation_de.properties deleted file mode 100644 index 6fd5b588..00000000 --- a/firmware_updater/updater/source/src/main/resources/GuiTranslation_de.properties +++ /dev/null @@ -1,68 +0,0 @@ -loadFile=lade Flashdatei -fileName=Dateiname -selectKnxIpGateway=KNX IP Schnittstelle -ipAddress=IP Adresse -scenario=Szenario -uid=UID -startFlash=Starte Flashvorgang -language=Sprache -newDevice=neues Gerät -appDevice=Gerät mit Applikation -allOptions=alle Optionen -newDeviceHint=Es wurde auf ein Selfbus Gerät der Bootloader geflasht
\ -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 (default 3671) -stopFlash=stop flashing -medium=medium -serial=serial -tpuart=tpuart -knxDeviceAddr=device address -knxProgDeviceAddr=bootloader device address -knxOwnAddress=own KNX address -port=port -useNat=NAT -knxMessageDelay=delay [ms] -knxTimeout=timeout -eraseFlash=erase complete flash -noFlash=no flash -requestUid=request UID from device -requestUidHint=The UID can be requested from a selbus device.
\ -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 @@ - - - - - - - - - INFO - - - %message%n - - - - - - INFO - - - - - - - ${application.home:-.}/log/updater-${appStartTimestamp}.html - - - %thread%level%logger%msg - %d{HH:mm:ss.SSS}%thread%level%logger%method%line%message - - - - - - ${application.home:-.}/log/updater-${appStartTimestamp}-warn.html - - WARN - - - - %thread%level%logger%msg - %d{HH:mm:ss.SSS}%thread%level%logger%method%line%message - - - - - - - - - - - - - diff --git a/firmware_updater/updater/source/src/test/java/org/selfbus/updater/ToolInfoTest.java b/firmware_updater/updater/source/src/test/java/org/selfbus/updater/ToolInfoTest.java deleted file mode 100644 index 8746170b..00000000 --- a/firmware_updater/updater/source/src/test/java/org/selfbus/updater/ToolInfoTest.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.selfbus.updater; - -import junit.framework.TestCase; - -public class ToolInfoTest extends TestCase { - - public void testGetVersion() { - } - - public void testGetAuthor() { - } - - public void testGetTool() { - } -} \ No newline at end of file diff --git a/firmware_updater/updater/source/src/test/resources/logback-test.xml b/firmware_updater/updater/source/src/test/resources/logback-test.xml deleted file mode 100644 index 1c418163..00000000 --- a/firmware_updater/updater/source/src/test/resources/logback-test.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - INFO - - - %message%n - - - - - - - - diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/BinImage.java b/firmware_updater/updater/src/org/selfbus/updater/BinImage.java similarity index 85% rename from firmware_updater/updater/source/src/main/java/org/selfbus/updater/BinImage.java rename to firmware_updater/updater/src/org/selfbus/updater/BinImage.java index 5b2df0d8..38bbc898 100644 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/BinImage.java +++ b/firmware_updater/updater/src/org/selfbus/updater/BinImage.java @@ -17,9 +17,12 @@ import java.util.Arrays; import java.util.zip.CRC32; +import static org.fusesource.jansi.Ansi.*; +import static org.selfbus.updater.logging.Color.*; + public class BinImage { - private final static Logger logger = LoggerFactory.getLogger(BinImage.class.getName()); - private byte[] binData; + private final static Logger logger = LoggerFactory.getLogger(BinImage.class); + private final byte[] binData; private final long startAddress; private final long endAddress; private long crc32; @@ -122,37 +125,35 @@ private void calculateCrc32() { this.crc32 = crc32File.getValue(); } - public final boolean writeToBinFile(String fileName) { + public final void writeToBinFile(String fileName) { File binFile = new File(fileName); if (!binFile.getParentFile().exists()) { if (!binFile.getParentFile().mkdirs()) { - logger.warn("{}Could not create bin-file directory {}{}", - ConColors.RED, binFile.getParentFile().toString(), ConColors.RESET); + logger.warn("{}Could not create bin-file directory {}{}", ansi().fgBright(WARN), + binFile.getParentFile().toString(), ansi().reset()); } - return false; + return; } try (FileOutputStream fos = new FileOutputStream(binFile)) { fos.write(binData); - return true; } catch (IOException e) { - logger.warn("{}Could not write bin-file {}{}", - ConColors.RED, binFile.getPath(), ConColors.RESET); + logger.warn("{}Could not write bin-file {}{}", ansi().fgBright(WARN), binFile.getPath(), ansi().reset()); } - return false; } + @Override public final String toString() { - return String.format("0x%04X-0x%04X, %05d byte(s), crc32 0x%08X", - startAddress, endAddress, binData.length, crc32()); + return String.format("0x%04X-0x%04X, %05d byte(s), crc32 0x%08X, APP_VERSION pointer: 0x%04X", + startAddress(), endAddress(), binData.length, crc32(), getAppVersionAddress()); } - public int getAppVersionAddress() { - return Bytes.indexOf(this.getBinData(), Mcu.APP_VER_PTR_MAGIC) + Mcu.APP_VER_PTR_MAGIC.length; + public long getAppVersionAddress() { + return startAddress() + Bytes.indexOf(this.getBinData(), Mcu.APP_VER_PTR_MAGIC) + Mcu.APP_VER_PTR_MAGIC.length; } public String getAppVersion() { - int appVersionAddress = getAppVersionAddress(); + int appVersionAddress = (int)(getAppVersionAddress() - startAddress()); if (appVersionAddress <= Mcu.VECTOR_TABLE_END || appVersionAddress >= (this.length() - Mcu.BL_ID_STRING_LENGTH)) { return ""; } diff --git a/firmware_updater/updater/src/org/selfbus/updater/CliInvalidException.java b/firmware_updater/updater/src/org/selfbus/updater/CliInvalidException.java new file mode 100644 index 00000000..d1d9f9f1 --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/CliInvalidException.java @@ -0,0 +1,13 @@ +package org.selfbus.updater; + +import org.apache.commons.cli.ParseException; + +public class CliInvalidException extends ParseException { + public CliInvalidException(String message) { + super(message); + } + + public CliInvalidException(Throwable e) { + super(e); + } +} diff --git a/firmware_updater/updater/src/org/selfbus/updater/CliOptions.java b/firmware_updater/updater/src/org/selfbus/updater/CliOptions.java new file mode 100644 index 00000000..4c1b3460 --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/CliOptions.java @@ -0,0 +1,1309 @@ +package org.selfbus.updater; + +import ch.qos.logback.classic.Level; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +import org.selfbus.updater.upd.UPDProtocol; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import tuwien.auto.calimero.IndividualAddress; +import tuwien.auto.calimero.KNXFormatException; +import tuwien.auto.calimero.KNXIllegalArgumentException; +import tuwien.auto.calimero.Priority; +import tuwien.auto.calimero.knxnetip.KNXnetIPConnection; +import tuwien.auto.calimero.link.medium.KNXMediumSettings; +import tuwien.auto.calimero.link.medium.TPSettings; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import static org.fusesource.jansi.Ansi.*; +import static org.selfbus.updater.Mcu.FLASH_END_ADDRESS; +import static org.selfbus.updater.Mcu.FLASH_START_ADDRESS; +import static org.selfbus.updater.logging.Color.*; + +/** + * Parses command line interface (cli) options for the application + *

+ * 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 VALID_BLOCKSIZES = Arrays.asList(256, 512, 1024); + + private static final int PRINT_WIDTH = 100; + private final static List VALID_LOG_LEVELS = Arrays.asList("TRACE", "DEBUG", "INFO"); + private final static int INVALID_SECURE_USER_ID = -1; + + private final Options cliOptions = new Options(); + // define parser + CommandLine cmdLine; + + private String helpHeader = ""; + private String helpFooter = ""; + private String helpApplicationName = ""; + + private String knxInterface =""; + private String fileName = ""; + private String localhost = ""; + private int localPort = 0; + private int port = KNXnetIPConnection.DEFAULT_PORT; + private boolean natIsSet = false; + private String ft12SerialPort = ""; + private String tpuartSerialPort = ""; + private String usbVendorIdAndProductId = ""; + private boolean tunnelingV2isSet = false; + private boolean tunnelingV1isSet = false; + private boolean routingIsSet = false; + private String medium = KNXMediumSettings.getMediumString(KNXMediumSettings.MEDIUM_TP1); + + private int knxSecureUserId = INVALID_SECURE_USER_ID; + private String knxSecureUserPassword = ""; + private String knxSecureDevicePassword = ""; + + private IndividualAddress progDevicePhysicalAddress = Updater.PHYS_ADDRESS_BOOTLOADER; + private IndividualAddress ownPhysicalAddress = Updater.PHYS_ADDRESS_OWN; + private IndividualAddress devicePhysicalAddress = null; + private String uid = ""; + private boolean flashingFullModeIsSet = false; + private int delayMs = 0; + private int reconnectMs = RECONNECT_MIN_MS; + private int reconnectSeqNumber = RECONNECT_INVALID_SEQ_NUMBER; + + private boolean noFlashIsSet = false; + private boolean eraseFullFlashIsSet = false; + private long dumpFlashStartAddress = -1; + private long dumpFlashEndAddress = -1; + + private Priority priority = Priority.LOW; + + private boolean logStatisticsIsSet = false; + private int blockSize = Mcu.UPD_PROGRAM_SIZE; + + private final Level defaultLogLevel; + + private boolean discoverIsSet = false; + + private static final int RECONNECT_MIN_MS = 100; + private static final int RECONNECT_MAX_MS = 12500; + + private static final int RECONNECT_MIN_SEQ_NUMBER = 100; + private static final int RECONNECT_MAX_SEQ_NUMBER = 255 - (4*2); // reserve up to 4 repeats on transport layer with up 2 telegrams + private static final int RECONNECT_INVALID_SEQ_NUMBER = -1; + + private CliOptions() { + defaultLogLevel = getLogLevel(); + + Option tunnelingV2 = new Option(OPT_SHORT_TUNNEL_V2, OPT_LONG_TUNNEL_V2, false, "use KNXnet/IP tunneling v2 (TCP) (experimental)"); + Option tunnelingV1 = new Option(OPT_SHORT_TUNNEL_V1, OPT_LONG_TUNNEL_V1, false, "use KNXnet/IP tunneling v1 (UDP)"); + Option nat = new Option(OPT_SHORT_NAT, OPT_LONG_NAT, false, "enable Network Address Translation (NAT) (only available with tunneling v1)"); + Option routing = new Option(OPT_SHORT_ROUTING, OPT_LONG_ROUTING, false, "use KNXnet/IP routing/multicast (experimental)"); + Option full = new Option(OPT_SHORT_FULL, OPT_LONG_FULL, false, "force full upload mode (disables differential mode)"); + Option help = new Option(OPT_SHORT_HELP, OPT_LONG_HELP, false, "show this help message"); + Option version = new Option(OPT_SHORT_VERSION, OPT_LONG_VERSION, false, "show tool/library version"); + Option NO_FLASH = new Option(OPT_SHORT_NO_FLASH, OPT_LONG_NO_FLASH, false, "for debugging use only, disable flashing firmware!"); + Option eraseFlash = new Option(null, OPT_LONG_ERASEFLASH, false, "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."); + Option discover = new Option(null, OPT_LONG_DISCOVER, false, "List available KNXnet/IP interfaces and USB-Interfaces"); + + + Option dumpFlash = Option.builder(null).longOpt(OPT_LONG_DUMPFLASH) + .argName("start> (default %d)", KNXnetIPConnection.DEFAULT_PORT)).get(); + Option ft12 = Option.builder(OPT_SHORT_FT12).longOpt(OPT_LONG_FT12) + .argName("COM-port") + .numberOfArgs(1) + .required(false) + .desc("use FT1.2 serial communication").get(); + Option tpuart = Option.builder(OPT_SHORT_TPUART).longOpt(OPT_LONG_TPUART) + .argName("COM-port") + .numberOfArgs(1) + .required(false) + .desc("use TPUART serial communication (experimental, needs serialcom or rxtx library in java.library.path)").get(); + Option usbInterface = Option.builder(null).longOpt(OPT_LONG_USB) + .argName("vendorId:productId") + .numberOfArgs(1) + .required(false) + .desc("use USB-Interface. Specify VendorID and ProductID e.g. 147B:5120 for the Selfbus USB-Interface (experimental)").get(); + Option medium = Option.builder(OPT_SHORT_MEDIUM).longOpt(OPT_LONG_MEDIUM) + .argName("tp1|rf") + .numberOfArgs(1) + .required(false) + .type(TPSettings.class) + .desc(String.format("KNX medium [tp1|rf] (default %s)", getMedium())).get(); ///\todo not all implemented missing [tp0|p110|p132] + Option optProgDevice = Option.builder(OPT_SHORT_PROG_DEVICE).longOpt(OPT_LONG_PROG_DEVICE) + .argName("x.x.x") + .numberOfArgs(1) + .required(false) + .type(IndividualAddress.class) + .desc(String.format("KNX device address in bootloader mode (default %s)", getProgDevicePhysicalAddress().toString())).get(); + Option device = Option.builder(OPT_SHORT_DEVICE).longOpt(OPT_LONG_DEVICE) + .argName("x.x.x") + .numberOfArgs(1) + .required(false) + .type(IndividualAddress.class) + .desc("KNX device address in normal operating mode (default none)").get(); + Option ownPhysicalAddress = Option.builder(OPT_SHORT_OWN_ADDRESS).longOpt(OPT_LONG_OWN_ADDRESS) + .argName("x.x.x") + .numberOfArgs(1) + .required(false) + .type(IndividualAddress.class) + .desc(String.format("own physical KNX tunnel address (default %s). Required for some IP interfaces that also use their own address as the tunnel address, e.g. Loxone Miniserver Gen 1.", + getOwnPhysicalAddress().toString())).get(); + Option uid = Option.builder(OPT_SHORT_UID).longOpt(OPT_LONG_UID) + .argName("uid") + .numberOfArgs(1) + .required(false) + .desc(String.format("send UID to unlock (default: request UID to unlock). Only the first %d bytes of UID are used", UPDProtocol.UID_LENGTH_USED)).get(); + Option delay = Option.builder(null).longOpt(OPT_LONG_DELAY) + .argName("ms") + .numberOfArgs(1) + .required(false) + .type(Number.class) + .desc(String.format("delay telegrams during data transmission to reduce bus load, valid 0-500ms, default %d", Updater.DELAY_MIN)).get(); + Option reconnect = Option.builder(null).longOpt(OPT_LONG_RECONNECT) + .argName("ms") + .numberOfArgs(1) + .required(false) + .type(Number.class) + .desc(String.format("pause between a KNX connection reconnect, valid %d - %dms, default %d", RECONNECT_MIN_MS, + RECONNECT_MAX_MS, RECONNECT_MIN_MS)).get(); + Option reconnectSeqNumber = Option.builder(null).longOpt(OPT_LONG_RECONNECT_SEQ_NUMBER) + .argName("#sequence") + .numberOfArgs(1) + .required(false) + .type(Number.class) + .desc(String.format("Reconnect KNX IP tunnel on sequence number, valid %d - %d, default %d. May help with some IP-Interfaces e.g. for Loxone Miniserver Gen 1. set to 245", + RECONNECT_MIN_SEQ_NUMBER, RECONNECT_MAX_SEQ_NUMBER, getReconnectSeqNumber())).get(); + Option logLevel = Option.builder(OPT_SHORT_LOGLEVEL).longOpt(OPT_LONG_LOGLEVEL) + .argName("TRACE|DEBUG|INFO") + .numberOfArgs(1) + .required(false) + .type(String.class) + .desc(String.format("Logfile logging level [TRACE|DEBUG|INFO] (default %s)", defaultLogLevel.toString())).get(); + + Option userId = Option.builder(null).longOpt(OPT_LONG_USER_ID) + .argName("id") + .numberOfArgs(1) + .required(false) + .type(Number.class) + .desc(String.format("KNX IP Secure tunneling user identifier (1..127) (default %d)", getKnxSecureUserId())).get(); + Option userPasswd = Option.builder(null).longOpt(OPT_LONG_USER_PASSWORD) + .argName("password") + .numberOfArgs(1) + .required(false) + .type(Number.class) + .desc("KNX IP Secure tunneling user password (Commissioning password/Inbetriebnahmepasswort), quotation marks (\") in password may not work").get(); + Option devicePasswd = Option.builder(null).longOpt(OPT_LONG_DEVICE_PASSWORD) + .argName("password") + .numberOfArgs(1) + .required(false) + .type(Number.class) + .desc("KNX IP Secure device authentication code (Authentication Code/Authentifizierungscode) quotation marks(\") in password may not work").get(); + + Option knxPriority = Option.builder(null).longOpt(OPT_LONG_PRIORITY) + .argName("SYSTEM|URGENT|NORMAL|LOW") + .numberOfArgs(1) + .required(false) + .type(String.class) + .desc(String.format("KNX telegram priority (default %s)", getPriority().toString().toUpperCase())).get(); + + Option blockSizeOption = Option.builder(OPT_SHORT_BLOCKSIZE).longOpt(OPT_LONG_BLOCKSIZE) + .argName("256|512|1024") + .valueSeparator(' ') + .numberOfArgs(1) + .required(false) + .type(Number.class) + .desc(String.format("Block size to program (default %d bytes)", getBlockSize())).get(); + + Option logStatistic = new Option(null, OPT_LONG_LOGSTATISTIC, false, "show more statistic data"); + + // options will be shown in order as they are added to cliOptions + cliOptions.addOption(fileName); + + cliOptions.addOption(medium); + + // usb, ft12 or tpuart + OptionGroup grpBusAccess = new OptionGroup(); + grpBusAccess.addOption(ft12); + grpBusAccess.addOption(tpuart); + grpBusAccess.addOption(usbInterface); + cliOptions.addOptionGroup(grpBusAccess); + + cliOptions.addOption(device); + cliOptions.addOption(optProgDevice); + cliOptions.addOption(ownPhysicalAddress); + cliOptions.addOption(knxPriority); + cliOptions.addOption(blockSizeOption); + + cliOptions.addOption(userId); + cliOptions.addOption(userPasswd); + cliOptions.addOption(devicePasswd); + + cliOptions.addOption(uid); + cliOptions.addOption(full); + cliOptions.addOption(localhost); + cliOptions.addOption(localport); + cliOptions.addOption(port); + + // Tunneling v1, Tunneling v2 or routing + OptionGroup grpTunnelingOrRouting = new OptionGroup(); + grpTunnelingOrRouting.addOption(tunnelingV2); + grpTunnelingOrRouting.addOption(tunnelingV1); + grpTunnelingOrRouting.addOption(routing); + cliOptions.addOptionGroup(grpTunnelingOrRouting); + + cliOptions.addOption(nat); + + // help or version, not both + OptionGroup grpHelper = new OptionGroup(); + grpHelper.addOption(help); + grpHelper.addOption(version); + cliOptions.addOptionGroup(grpHelper); + + cliOptions.addOption(delay); + cliOptions.addOption(logLevel); + cliOptions.addOption(reconnect); + cliOptions.addOption(reconnectSeqNumber); + cliOptions.addOption(eraseFlash); + cliOptions.addOption(dumpFlash); + cliOptions.addOption(NO_FLASH); + cliOptions.addOption(logStatistic); + cliOptions.addOption(discover); + } + + public CliOptions(final String[] args, String helpApplicationName, String helpHeader, String helpFooter) + throws KNXFormatException, ParseException { + this(); + this.helpApplicationName = helpApplicationName; + this.helpHeader = helpHeader; + this.helpFooter = helpFooter; + parse(args); + } + + public CliOptions(final ArrayList argsList) throws KNXFormatException, ParseException { + this(); + this.helpApplicationName = ""; + this.helpHeader = ""; + this.helpFooter = ""; + String[] args = new String[argsList.size()]; + args = argsList.toArray(args); + parse(args); + } + + private void parse(final String[] args) throws ParseException, KNXFormatException { + CommandLineParser parser = new DefaultParser(); + cmdLine = parser.parse(cliOptions, args); + + if (cmdLine.hasOption(OPT_LONG_LOGLEVEL)) + setLogLevel(cmdLine.getOptionValue(OPT_LONG_LOGLEVEL).toUpperCase()); + else + setLogLevel(defaultLogLevel); + + if (cmdLine.hasOption(OPT_LONG_PRIORITY)) + setPriority(cmdLine.getOptionValue(OPT_LONG_PRIORITY)); + else + setPriority("LOW"); + + if (getHelpIsSet()) { + logger.debug("{}={}", OPT_LONG_HELP, getHelpIsSet()); + return; + } + + if (getVersionIsSet()) { + logger.debug("{}={}", OPT_LONG_VERSION, getVersionIsSet()); + return; + } + + setNoFlashIsSet(cmdLine.hasOption(OPT_LONG_NO_FLASH)); + setEraseFullFlashIsSet(cmdLine.hasOption(OPT_LONG_ERASEFLASH)); + + if (cmdLine.hasOption(OPT_LONG_DUMPFLASH)) { + String[] optArgs = cmdLine.getOptionValues(OPT_LONG_DUMPFLASH); + setDumpFlashStartAddress(optArgs[0]); + setDumpFlashEndAddress(optArgs[1]); + } + + setFlashingFullModeIsSet(cmdLine.hasOption(OPT_LONG_FULL)); + setTunnelingV2isSet(cmdLine.hasOption(OPT_LONG_TUNNEL_V2)); + setTunnelingV1isSet(cmdLine.hasOption(OPT_LONG_TUNNEL_V1)); + setRoutingIsSet(cmdLine.hasOption(OPT_LONG_ROUTING)); + setNatIsSet(cmdLine.hasOption(OPT_LONG_NAT)); + setDiscoverIsSet(cmdLine.hasOption(OPT_LONG_DISCOVER)); + + if (cmdLine.hasOption(OPT_LONG_FILENAME)) + setFileName(cmdLine.getOptionValue(OPT_LONG_FILENAME)); + else + setFileName(""); + + + if (cmdLine.hasOption(OPT_LONG_LOCALHOST)) + setLocalhost(cmdLine.getOptionValue(OPT_LONG_LOCALHOST)); + else + setLocalhost(""); + + if (cmdLine.hasOption(OPT_LONG_LOCALPORT)) + setLocalPort(cmdLine.getOptionValue(OPT_LONG_LOCALPORT)); + else + setLocalPort(0); + + if (cmdLine.hasOption(OPT_LONG_PORT)) + setPort(cmdLine.getOptionValue(OPT_LONG_PORT)); + else + setPort(KNXnetIPConnection.DEFAULT_PORT); + + if (cmdLine.hasOption(OPT_LONG_MEDIUM)) + setMedium(cmdLine.getOptionValue(OPT_LONG_MEDIUM)); + else + setMedium(KNXMediumSettings.getMediumString(KNXMediumSettings.MEDIUM_TP1)); + + if (cmdLine.hasOption(OPT_LONG_DELAY)) + setDelayMs(cmdLine.getOptionValue(OPT_LONG_DELAY)); + else + setDelayMs(Updater.DELAY_MIN); + + if (cmdLine.hasOption(OPT_LONG_RECONNECT)) + setReconnectMs(cmdLine.getOptionValue(OPT_LONG_RECONNECT)); + else + setReconnectMs(RECONNECT_MIN_MS); + + if (cmdLine.hasOption(OPT_LONG_RECONNECT_SEQ_NUMBER)) + setReconnectSeqNumber(cmdLine.getOptionValue(OPT_LONG_RECONNECT_SEQ_NUMBER)); + else + setReconnectSeqNumber(RECONNECT_INVALID_SEQ_NUMBER); + + if (cmdLine.hasOption(OPT_LONG_BLOCKSIZE)) + setBlockSize(cmdLine.getOptionValue(OPT_LONG_BLOCKSIZE)); + else + setBlockSize(Mcu.UPD_PROGRAM_SIZE); + + if (cmdLine.hasOption(OPT_LONG_UID)) + setUid(cmdLine.getOptionValue(OPT_LONG_UID)); + else + setUid(""); + + if (cmdLine.hasOption(OPT_LONG_DEVICE)) + setDevicePhysicalAddress(cmdLine.getOptionValue(OPT_LONG_DEVICE)); + else + setDevicePhysicalAddress(null); + + if (cmdLine.hasOption(OPT_LONG_PROG_DEVICE)) + setProgDevicePhysicalAddress(cmdLine.getOptionValue(OPT_LONG_PROG_DEVICE)); + else + setProgDevicePhysicalAddress(Updater.PHYS_ADDRESS_BOOTLOADER.toString()); + + if (cmdLine.hasOption(OPT_LONG_OWN_ADDRESS)) + setOwnPhysicalAddress(cmdLine.getOptionValue(OPT_LONG_OWN_ADDRESS)); + else + setOwnPhysicalAddress(Updater.PHYS_ADDRESS_OWN.toString()); + + if (cmdLine.hasOption(OPT_LONG_FT12)) + setFt12SerialPort(cmdLine.getOptionValue(OPT_LONG_FT12)); + else + setFt12SerialPort(""); + + if (cmdLine.hasOption(OPT_LONG_TPUART)) + setTpuartSerialPort(cmdLine.getOptionValue(OPT_LONG_TPUART)); + else + setTpuartSerialPort(""); + + if (cmdLine.hasOption(OPT_LONG_USB)) + setUsbVendorIdAndProductId(cmdLine.getOptionValue(OPT_LONG_USB)); + else + setUsbVendorIdAndProductId(""); + + if (cmdLine.getArgs().length > 0) + setKnxInterface(cmdLine.getArgs()[0]); + else + setKnxInterface(""); + + if (cmdLine.hasOption(OPT_LONG_USER_ID)) + setKnxSecureUserId(cmdLine.getOptionValue(OPT_LONG_USER_ID)); + else + setKnxSecureUserId(INVALID_SECURE_USER_ID); + + if (cmdLine.hasOption(OPT_LONG_USER_PASSWORD)) + setKnxSecureUserPassword(cmdLine.getOptionValue(OPT_LONG_USER_PASSWORD)); + else + setKnxSecureUserPassword(""); + + if (cmdLine.hasOption(OPT_LONG_DEVICE_PASSWORD)) + setKnxSecureDevicePassword(cmdLine.getOptionValue(OPT_LONG_DEVICE_PASSWORD)); + else + setKnxSecureDevicePassword(""); + + setLogStatisticsIsSet(cmdLine.hasOption(OPT_LONG_LOGSTATISTIC)); + + validateCliParameters(); + } + + private void validateCliParameters() throws CliInvalidException { + // some logical checks for options which exclude each other + // differential mode and eraseflash makes no sense + if (getEraseFullFlashIsSet() && (!getFlashingFullModeIsSet())) { + setFlashingFullModeIsSet(true); + logger.info("{}--{} is set. --> switching to full flash mode{}", ansi().fgBright(WARN), + OPT_LONG_ERASEFLASH, ansi().reset()); + } + + // nat only possible with tunneling v1 + if (getNatIsSet() && (!getTunnelingV1isSet())) { + throw new CliInvalidException(String.format("%sOption --%s can only be used together with --%s%s", + ansi().fgBright(WARN), OPT_LONG_NAT, OPT_LONG_TUNNEL_V1, ansi().reset())); + } + + // nat isn't allowed with tunneling v2 + if (getNatIsSet() && (getTunnelingV2isSet())) { + throw new CliInvalidException(String.format("%sOption --%s can not be used together with --%s%s", + ansi().fgBright(WARN), OPT_LONG_NAT, OPT_LONG_TUNNEL_V2, ansi().reset())); + } + + // check IP-secure configuration + if (!(getKnxSecureUserPassword().isEmpty()) || !(getKnxSecureDevicePassword().isEmpty())) { + if (getKnxInterface().isBlank()) { + throw new CliInvalidException(String.format("%sNo IP-Interface specified for IP-secure%s", + ansi().fgBright(WARN), ansi().reset())); + } + else if (!getUsbVendorIdAndProductId().isBlank()) { + throw new CliInvalidException(String.format("%sIP-secure is not possible with --%s%s", + ansi().fgBright(WARN), OPT_LONG_USB, ansi().reset())); + } + else if (!getFt12SerialPort().isBlank()) { + throw new CliInvalidException(String.format("%sIP-secure is not possible with --%s%s", + ansi().fgBright(WARN), OPT_LONG_FT12, ansi().reset())); + } + else if (!getTpuartSerialPort().isBlank()) { + throw new CliInvalidException(String.format("%sIP-secure is not possible with --%s%s", + ansi().fgBright(WARN), OPT_LONG_TPUART, ansi().reset())); + } + else if (getNatIsSet()) { + throw new CliInvalidException(String.format("%sIP-secure is not possible with --%s%s", + ansi().fgBright(WARN), OPT_LONG_NAT, ansi().reset())); + } + else if (getTunnelingV1isSet()) { + throw new CliInvalidException(String.format("%sIP-secure is not possible with --%s%s", + ansi().fgBright(WARN), OPT_LONG_TUNNEL_V1, ansi().reset())); + } + else if (getTunnelingV2isSet()) { + throw new CliInvalidException(String.format("%sIP-secure is not possible with --%s%s", + ansi().fgBright(WARN), OPT_LONG_TUNNEL_V2, ansi().reset())); + } + + // ensure that all three IP-Secure arguments are set + if ((getKnxSecureUserPassword().isEmpty()) || (getKnxSecureDevicePassword().isEmpty())) { + throw new CliInvalidException(String.format("%sFor IP-secure --%s, --%s and --%s must be set%s", + ansi().fgBright(WARN), OPT_LONG_USER_ID, OPT_LONG_USER_PASSWORD, OPT_LONG_DEVICE_PASSWORD, + ansi().reset())); + } + } + + int interfacesSet = 0; + if (!getKnxInterface().isBlank()) interfacesSet++; + if (getRoutingIsSet()) interfacesSet++; + if (!getFt12SerialPort().isBlank()) interfacesSet++; + if (!getTpuartSerialPort().isBlank()) interfacesSet++; + if (!getUsbVendorIdAndProductId().isBlank()) interfacesSet++; + + if (interfacesSet > 1) { + throw new CliInvalidException("Only one bus interface can be used."); + } + else if ((interfacesSet == 0) && (!getDiscoverIsSet())) { + throw new CliInvalidException("No bus interface specified."); + } + } + + public String reconstructCommandLine() { + String builder = ""; + + if (!getKnxInterface().isBlank()) + builder += getKnxInterface(); + + if (!getFileName().isBlank()) + builder += String.format(" --%s %s", OPT_LONG_FILENAME, getFileName()); + + if (!getLocalhost().isBlank()) + builder += String.format(" --%s %s", OPT_LONG_LOCALHOST, getLocalhost()); + + if (getLocalPort() != 0) + builder += String.format(" --%s %s", OPT_LONG_LOCALPORT, getLocalPort()); + + if (getPort() != KNXnetIPConnection.DEFAULT_PORT) + builder += String.format(" --%s %s", OPT_LONG_PORT, getPort()); + + if (!getFt12SerialPort().isBlank()) + builder += String.format(" --%s %s", OPT_LONG_FT12, getFt12SerialPort()); + + if (!getTpuartSerialPort().isBlank()) + builder += String.format(" --%s %s", OPT_LONG_TPUART, getTpuartSerialPort()); + + if (!getUsbVendorIdAndProductId().isBlank()) + builder += String.format(" --%s %s", OPT_LONG_USB, getUsbVendorIdAndProductId()); + + if (!Objects.equals(getMedium(), KNXMediumSettings.getMediumString(KNXMediumSettings.MEDIUM_TP1)) && (!getMedium().isBlank())) + builder += String.format(" --%s %s", OPT_LONG_MEDIUM, getMedium()); + + if (getKnxSecureUserId() >= 0) + builder += String.format(" --%s ***", OPT_LONG_USER_ID); + + if (!getKnxSecureUserPassword().isBlank()) + builder += String.format(" --%s ***", OPT_LONG_USER_PASSWORD); + + if (!getKnxSecureDevicePassword().isBlank()) + builder += String.format(" --%s ***", OPT_LONG_DEVICE_PASSWORD); + + if (!getProgDevicePhysicalAddress().toString().equals(Updater.PHYS_ADDRESS_BOOTLOADER.toString())) + builder += String.format(" --%s %s", OPT_LONG_PROG_DEVICE, getProgDevicePhysicalAddress()); + + if (getDevicePhysicalAddress() != null) + builder += String.format(" --%s %s", OPT_LONG_DEVICE, getDevicePhysicalAddress()); + + if (!getOwnPhysicalAddress().toString().equals(Updater.PHYS_ADDRESS_OWN.toString())) + builder += String.format(" --%s %s", OPT_LONG_OWN_ADDRESS, getOwnPhysicalAddress()); + + if (!getUid().isBlank()) + builder += String.format(" --%s %s", OPT_LONG_UID, getUid()); + + if (getDelayMs() != Updater.DELAY_MIN) + builder += String.format(" --%s %d", OPT_LONG_DELAY, getDelayMs()); + + if (getReconnectMs() != RECONNECT_MIN_MS) + builder += String.format(" --%s %d", OPT_LONG_RECONNECT, getReconnectMs()); + + if (getTunnelingV2isSet()) + builder += String.format(" --%s", OPT_LONG_TUNNEL_V2); + + if (getTunnelingV1isSet()) + builder += String.format(" --%s", OPT_LONG_TUNNEL_V1); + + if (getNatIsSet()) + builder += String.format(" --%s", OPT_LONG_NAT); + + if (getRoutingIsSet()) + builder += String.format(" --%s", OPT_LONG_ROUTING); + + if (getFlashingFullModeIsSet()) + builder += String.format(" --%s", OPT_LONG_FULL); + + if (getHelpIsSet()) + builder += String.format(" --%s", OPT_LONG_HELP); + + if (getVersionIsSet()) + builder += String.format(" --%s", OPT_LONG_VERSION); + + if (getNoFlashIsSet()) + builder += String.format(" --%s", OPT_LONG_NO_FLASH); + + if (getLogLevel() != defaultLogLevel) + builder += String.format(" --%s %s", OPT_LONG_LOGLEVEL, getLogLevel()); + + if (getPriority() != Priority.LOW) + builder += String.format(" --%s %s", OPT_LONG_PRIORITY, getPriority()); + + if (getEraseFullFlashIsSet()) + builder += String.format(" --%s", OPT_LONG_ERASEFLASH); + + if (getDumpFlashStartAddress() >= 0) + builder += String.format(" --%s 0x%08X 0x%08X", OPT_LONG_DUMPFLASH, + getDumpFlashStartAddress(), getDumpFlashEndAddress()); + + if (getLogStatisticsIsSet()) + builder += String.format(" --%s", OPT_LONG_LOGSTATISTIC); + + if (getBlockSize() != Mcu.UPD_PROGRAM_SIZE) + builder += String.format(" --%s %s", OPT_LONG_BLOCKSIZE, getBlockSize()); + + return builder.trim(); + } + + public String helpToString() { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + +// todo commons-cli 1.11.0 does not yet support setWidth or Printwriter/StringWriter +// import org.apache.commons.cli.help.HelpFormatter +// final HelpFormatter helper = org.apache.commons.cli.help.HelpFormatter.builder() +// //.setWidth(PRINT_WIDTH) +// .setShowSince(false) +// .setComparator((optionA, optionB) -> 0) // do not sort options +// .get(); +// try { +// // printHelp has no pw (PrintWriter) since 1.10.0 +// helper.printHelp(pw, helpApplicationName + " ", +// helpHeader + ":", cliOptions, helpFooter, false); +// } catch (IOException e) { +// throw new RuntimeException(e); +// } + final HelpFormatter helper = new org.apache.commons.cli.HelpFormatter(); + helper.setWidth(PRINT_WIDTH); + helper.setOptionComparator(null); + helper.printHelp(pw, helper.getWidth(), helpApplicationName + " ", + System.lineSeparator() + helpHeader + ":" + System.lineSeparator(), cliOptions, helper.getLeftPadding(), + helper.getDescPadding(), helpFooter, true); + pw.flush(); + return sw.toString(); + } + + private String nonNullString(String s) { + return s != null ? s : ""; + } + + public String getKnxInterface() { + return nonNullString(knxInterface); + } + + private void setKnxInterface(String knxInterface) { + this.knxInterface = knxInterface; + logger.debug("knxInterface={}", getKnxInterface()); + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + logger.debug("{}={}", OPT_LONG_FILENAME, getFileName()); + } + + public String getLocalhost() { + return localhost; + } + + private void setLocalhost(String localhost) { + this.localhost = localhost; + logger.debug("{}={}", OPT_LONG_LOCALHOST, getLocalhost()); + } + + public int getLocalPort() { + return localPort; + } + + private void setLocalPort(int localPort) { + if ((localPort >= 0) && (localPort <= 65535)) { + this.localPort = localPort; + logger.debug("{}={}", OPT_LONG_LOCALPORT, getLocalPort()); + } + else { + this.localPort = 0; + logger.warn("{}option --{} {} is invalid (min:{}, max:{}) => set to {}{}", + ansi().fgBright(WARN), OPT_LONG_LOCALPORT, localPort, 0, 65535, this.localPort, ansi().reset()); + } + } + + private void setLocalPort(String localPort) { + try { + if (localPort == null || localPort.isBlank()) + setLocalPort(KNXnetIPConnection.DEFAULT_PORT); + else + setLocalPort(Integer.parseInt(localPort)); + } + catch (NumberFormatException e) { + setLocalPort(0); + logger.warn("{}option --{} {} is invalid => set to default {}{}", + ansi().fgBright(WARN), OPT_LONG_LOCALPORT, localPort, getLocalPort(), ansi().reset()); + } + } + + public int getPort() { + return port; + } + + private void setPort(int port) { + if ((port >= 1) && (port <= 65535)) { + this.port = port; + logger.debug("{}={}", OPT_LONG_PORT, getPort()); + } + else { + this.port = KNXnetIPConnection.DEFAULT_PORT; + logger.warn("{}option --{} {} is invalid (min:{}, max:{}) => set to {}{}", + ansi().fgBright(WARN), OPT_LONG_PORT, port, 1, 65535, this.port, ansi().reset()); + } + } + + private void setPort(String port) { + try { + if (port == null || port.isBlank()) + setPort(KNXnetIPConnection.DEFAULT_PORT); + else + setPort(Integer.parseInt(port)); + } + catch (NumberFormatException e) { + setPort(KNXnetIPConnection.DEFAULT_PORT); + logger.warn("{}option --{} {} is invalid => set to default {}{}", + ansi().fgBright(WARN), OPT_LONG_PORT, port, getPort(), ansi().reset()); + } + } + + public boolean getNatIsSet() { + return natIsSet; + } + + public void setNatIsSet(boolean natIsSet) { + this.natIsSet = natIsSet; + logger.debug("{}={}", OPT_LONG_NAT, getNatIsSet()); + } + + public String getFt12SerialPort() { + return nonNullString(ft12SerialPort); + } + + private void setFt12SerialPort(String ft12SerialPort) { + this.ft12SerialPort = ft12SerialPort; + logger.debug("{}={}", OPT_LONG_FT12, getFt12SerialPort()); + } + + public String getTpuartSerialPort() { + return nonNullString(tpuartSerialPort); + } + + private void setTpuartSerialPort(String tpuartSerialPort) { + this.tpuartSerialPort = tpuartSerialPort; + logger.debug("{}={}", OPT_LONG_TPUART, getTpuartSerialPort()); + if (!tpuartSerialPort.isBlank()) { + logger.warn("{}TPUART support is experimental!{} Set TPUARTs own address with --{}", + ansi().fgBright(WARN), ansi().reset(), OPT_LONG_OWN_ADDRESS); + } + } + + public boolean getTunnelingV2isSet() { + return tunnelingV2isSet; + } + + public void setTunnelingV2isSet(boolean tunnelingV2isSet) { + this.tunnelingV2isSet = tunnelingV2isSet; + logger.debug("{}={}", OPT_LONG_TUNNEL_V2, getTunnelingV2isSet()); + } + + public boolean getTunnelingV1isSet() { + return tunnelingV1isSet; + } + + public void setTunnelingV1isSet(boolean tunnelingV1isSet) { + this.tunnelingV1isSet = tunnelingV1isSet; + logger.debug("{}={}", OPT_LONG_TUNNEL_V1, getTunnelingV2isSet()); + } + + public boolean getRoutingIsSet() { + return routingIsSet; + } + + private void setRoutingIsSet(boolean routingIsSet) { + this.routingIsSet = routingIsSet; + logger.debug("{}={}", OPT_LONG_ROUTING, getRoutingIsSet()); + } + + public String getMedium() { + return medium; + } + + private void setMedium(String medium) { + this.medium = medium; + logger.debug("{}={}", OPT_LONG_MEDIUM, getMedium()); + } + + public IndividualAddress getProgDevicePhysicalAddress() { + return progDevicePhysicalAddress; + } + + private void setProgDevicePhysicalAddress(String progDevicePhysicalAddress) throws KNXFormatException { + if ((progDevicePhysicalAddress != null) && (!progDevicePhysicalAddress.isBlank())) { + this.progDevicePhysicalAddress = new IndividualAddress(progDevicePhysicalAddress); + } + else { + this.progDevicePhysicalAddress = Updater.PHYS_ADDRESS_BOOTLOADER; + } + logger.debug("{}={}", OPT_LONG_PROG_DEVICE, getProgDevicePhysicalAddress()); + } + + public IndividualAddress getDevicePhysicalAddress() { + return devicePhysicalAddress; + } + + private void setDevicePhysicalAddress(String devicePhysicalAddress) throws KNXFormatException { + if ((devicePhysicalAddress != null) && (!devicePhysicalAddress.isBlank())) { + this.devicePhysicalAddress = new IndividualAddress(devicePhysicalAddress); + } + else { + this.devicePhysicalAddress = null; + } + logger.debug("{}={}", OPT_LONG_DEVICE, getDevicePhysicalAddress()); + } + + public IndividualAddress getOwnPhysicalAddress() { + return ownPhysicalAddress; + } + + private void setOwnPhysicalAddress(String ownPhysicalAddress) throws KNXFormatException { + if ((ownPhysicalAddress != null) && (!ownPhysicalAddress.isBlank())) { + this.ownPhysicalAddress = new IndividualAddress(ownPhysicalAddress); + } + else { + this.ownPhysicalAddress = Updater.PHYS_ADDRESS_OWN; + } + logger.debug("{}={}", OPT_LONG_OWN_ADDRESS, getOwnPhysicalAddress()); + } + + public String getUid() { + return uid; + } + + public void setUid(String uidString) { + this.uid = uidString; + logger.debug("{}={}", OPT_LONG_UID, getUid()); + } + + public boolean getFlashingFullModeIsSet() { + return flashingFullModeIsSet; + } + + private void setFlashingFullModeIsSet(boolean flashingFullModeIsSet) { + this.flashingFullModeIsSet = flashingFullModeIsSet; + logger.debug("{}={}", OPT_LONG_FULL, getFlashingFullModeIsSet()); + } + + public int getDelayMs() { + return delayMs; + } + + private void setDelayMs(int delayMs) { + if ((delayMs < Updater.DELAY_MIN) || (delayMs > Updater.DELAY_MAX)) { + logger.warn("{}option --{} {} is invalid (min:{}, max:{}) => set to {}{}", + ansi().fgBright(WARN), OPT_LONG_DELAY, delayMs, Updater.DELAY_MIN, + Updater.DELAY_MAX, Updater.DELAY_MIN, ansi().reset()); + delayMs = Updater.DELAY_MIN; // set to DELAY_MIN in case of invalid waiting time + } + this.delayMs = delayMs; + logger.debug("{}={}", OPT_LONG_DELAY, getDelayMs()); + } + + private void setDelayMs(String delayMs) { + try { + if (delayMs == null || delayMs.isBlank()) + setDelayMs(Updater.DELAY_MIN); + else + setDelayMs(Integer.parseInt(delayMs)); + } + catch (NumberFormatException e) { + setDelayMs(Updater.DELAY_MIN); + logger.warn("{}option --{} {} is invalid => set to default {}{}", + ansi().fgBright(WARN), OPT_LONG_DELAY, delayMs, getDelayMs(), ansi().reset()); + } + } + + public boolean getNoFlashIsSet() { + return noFlashIsSet; + } + + private void setNoFlashIsSet(boolean noFlashIsSet) { + this.noFlashIsSet = noFlashIsSet; + logger.debug("{}={}", OPT_LONG_NO_FLASH, getNoFlashIsSet()); + } + + public boolean getEraseFullFlashIsSet() { + return eraseFullFlashIsSet; + } + + private void setEraseFullFlashIsSet(boolean eraseFullFlashIsSet) { + this.eraseFullFlashIsSet = eraseFullFlashIsSet; + logger.debug("{}={}", OPT_LONG_ERASEFLASH, getEraseFullFlashIsSet()); + } + + public long getDumpFlashStartAddress() { + return dumpFlashStartAddress; + } + + private void setDumpFlashStartAddress(long dumpFlashStartAddress) { + this.dumpFlashStartAddress = dumpFlashStartAddress; + logger.debug("dumpFlashStartAddress={}", getDumpFlashStartAddress()); + } + + private void setDumpFlashStartAddress(String dumpFlashStartAddress) { + try { + setDumpFlashStartAddress(Long.decode(dumpFlashStartAddress)); + } + catch (NumberFormatException e) { + setDumpFlashStartAddress(FLASH_START_ADDRESS); + logger.warn("{}option --{} start address {} is invalid => set to default 0x{}{}", + ansi().fgBright(WARN), OPT_LONG_DUMPFLASH, dumpFlashStartAddress, + String.format("%08X",getDumpFlashStartAddress()), ansi().reset()); + } + } + + public long getDumpFlashEndAddress() { + return dumpFlashEndAddress; + } + + private void setDumpFlashEndAddress(long dumpFlashEndAddress) { + this.dumpFlashEndAddress = dumpFlashEndAddress; + logger.debug("dumpFlashEndAddress={}", getDumpFlashEndAddress()); + } + + private void setDumpFlashEndAddress(String dumpFlashEndAddress) { + try { + setDumpFlashEndAddress(Long.decode(dumpFlashEndAddress)); + } + catch (NumberFormatException e) { + setDumpFlashEndAddress(FLASH_END_ADDRESS); + logger.warn("{}option --{} end address {} is invalid => set to default 0x{}{}", + ansi().fgBright(WARN), OPT_LONG_DUMPFLASH, dumpFlashEndAddress, + String.format("%08X", getDumpFlashEndAddress()), ansi().reset()); + } + } + + public boolean getHelpIsSet() { + if (cmdLine == null) + return false; + else + return cmdLine.hasOption(OPT_LONG_HELP); + } + + public boolean getVersionIsSet() { + if (cmdLine == null) + return false; + else + return cmdLine.hasOption(OPT_LONG_VERSION); + } + + public Level getLogLevel() { + ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + return root.getLevel(); + } + + private void setLogLevel(Level logLevel) { + ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + root.setLevel(logLevel); + logger.debug("{}={}", OPT_LONG_LOGLEVEL, getLogLevel()); + } + + private void setLogLevel(String newLevel) { + newLevel = newLevel.toUpperCase(); + if (VALID_LOG_LEVELS.contains(newLevel)) { + setLogLevel(Level.toLevel(newLevel)); + } + else { + logger.warn("{}invalid {} {}, using {}{}", ansi().fgBright(WARN), OPT_LONG_LOGLEVEL, newLevel, + getLogLevel().toString(), ansi().reset()); + } + } + + public int getKnxSecureUserId() { + return knxSecureUserId; + } + + private void setKnxSecureUserId(int knxSecureUserId) { + if (knxSecureUserId == INVALID_SECURE_USER_ID) { + this.knxSecureUserId = INVALID_SECURE_USER_ID; + logger.debug("{}=", OPT_LONG_USER_ID); + return; + } + + if (knxSecureUserId >= 1 && knxSecureUserId <= 127) { + logger.debug("{}=***", OPT_LONG_USER_ID); // log only that it's, but not the actual value + this.knxSecureUserId = knxSecureUserId; + } + else { + logger.warn("{}option --{} *** is invalid => set to default{}", + ansi().fgBright(WARN), OPT_LONG_USER_ID, ansi().reset()); + this.knxSecureUserId = INVALID_SECURE_USER_ID; + logger.debug("{}=", OPT_LONG_USER_ID); + } + } + + private void setKnxSecureUserId(String knxSecureUserId) { + try { + if (knxSecureUserId == null || knxSecureUserId.isBlank()) + setKnxSecureUserId(INVALID_SECURE_USER_ID); + else + setKnxSecureUserId(Integer.parseInt(knxSecureUserId)); + } + catch (NumberFormatException e) { + setKnxSecureUserId(INVALID_SECURE_USER_ID); + logger.warn("{}option --{} *** is invalid => set to default{}", + ansi().fgBright(WARN), OPT_LONG_USER_ID, ansi().reset()); + } + } + + public String getKnxSecureUserPassword() { + return knxSecureUserPassword; + } + + private void setKnxSecureUserPassword(String knxSecureUserPassword) { + this.knxSecureUserPassword = knxSecureUserPassword; + if (!nonNullString(this.knxSecureUserPassword).isBlank()) + logger.debug("{}=****", OPT_LONG_USER_PASSWORD); // log only that it's, but not the actual value + else + logger.debug("{}=", OPT_LONG_USER_PASSWORD); + } + + public String getKnxSecureDevicePassword() { + return knxSecureDevicePassword; + } + + private void setKnxSecureDevicePassword(String knxSecureDevicePassword) { + this.knxSecureDevicePassword = knxSecureDevicePassword; + if (!nonNullString(this.knxSecureDevicePassword).isBlank()) + logger.debug("{}=*****", OPT_LONG_DEVICE_PASSWORD); // log only that it's, but not the actual value + else + logger.debug("{}=", OPT_LONG_DEVICE_PASSWORD); + } + + public Priority getPriority() { + return priority; + } + + private void setPriority(String priority) { + try { + this.priority = Priority.get(priority); + logger.debug("{}={}", OPT_LONG_PRIORITY, getPriority().toString()); + } + catch (KNXIllegalArgumentException e) { + logger.warn("{}invalid --{} {}, using {}{}",ansi().fgBright(WARN), OPT_LONG_PRIORITY, + priority, getPriority(), ansi().reset()); + } + } + + public boolean getLogStatisticsIsSet() { + return logStatisticsIsSet; + } + + private void setLogStatisticsIsSet(boolean logStatisticsIsSet) { + this.logStatisticsIsSet = logStatisticsIsSet; + logger.debug("{}={}", OPT_LONG_LOGSTATISTIC , getLogStatisticsIsSet()); + } + + public int getBlockSize() { + return blockSize; + } + + private void setBlockSize(int blockSize) { + if (VALID_BLOCKSIZES.contains(blockSize)) { + this.blockSize = blockSize; + } + else { + this.blockSize = Mcu.UPD_PROGRAM_SIZE; + logger.info("{}--{} {} is not supported => Set --{} to default {} bytes{}", ansi().fgBright(INFO), + OPT_LONG_BLOCKSIZE, blockSize, OPT_LONG_BLOCKSIZE, getBlockSize(), ansi().reset()); + } + logger.debug("{}={}", OPT_LONG_BLOCKSIZE, getBlockSize()); + } + + private void setBlockSize(String blockSize) { + try { + if (blockSize == null || blockSize.isBlank()) + setBlockSize(Mcu.UPD_PROGRAM_SIZE); + else + setBlockSize(Integer.parseInt(blockSize)); + } + catch (NumberFormatException e) { + setBlockSize(Mcu.UPD_PROGRAM_SIZE); + logger.warn("{}option --{} {} is invalid => set to default {}{}", + ansi().fgBright(WARN), OPT_LONG_BLOCKSIZE, blockSize, getBlockSize(), ansi().reset()); + } + } + + public String getUsbVendorIdAndProductId() { + return nonNullString(usbVendorIdAndProductId); + } + + private void setUsbVendorIdAndProductId(String usbVendorIdAndProductId) { + this.usbVendorIdAndProductId = usbVendorIdAndProductId; + logger.debug("{}={}", OPT_LONG_USB, getUsbVendorIdAndProductId()); + } + + public boolean getDiscoverIsSet() { + return discoverIsSet; + } + + public void setDiscoverIsSet(boolean discoverIsSet) { + this.discoverIsSet = discoverIsSet; + logger.debug("{}={}", OPT_LONG_DISCOVER, this.discoverIsSet); + } + + public int getReconnectMs() { + return reconnectMs; + } + + private void setReconnectMs(int reconnectMs) { + if ((reconnectMs < RECONNECT_MIN_MS) || (reconnectMs > RECONNECT_MAX_MS)) { + logger.warn("{}option --{} {} is invalid (min:{}, max:{}) => set to {}{}", + ansi().fgBright(WARN), OPT_LONG_RECONNECT, reconnectMs, RECONNECT_MIN_MS, + RECONNECT_MAX_MS, RECONNECT_MIN_MS, ansi().reset()); + reconnectMs = RECONNECT_MIN_MS; // set to RECONNECT_MIN_MS in case of invalid time + } + this.reconnectMs = reconnectMs; + logger.debug("{}={}", OPT_LONG_RECONNECT, getReconnectMs()); + } + + private void setReconnectMs(String reconnectMs) { + try { + if (reconnectMs == null || reconnectMs.isBlank()) + setReconnectMs(RECONNECT_MIN_MS); + else + setReconnectMs(Integer.parseInt(reconnectMs)); + } + catch (NumberFormatException e) { + setReconnectMs(RECONNECT_MIN_MS); + logger.warn("{}option --{} {} is invalid => set to default {}{}", + ansi().fgBright(WARN), OPT_LONG_RECONNECT, reconnectMs, getReconnectMs(), ansi().reset()); + } + } + + public int getReconnectSeqNumber() { + return reconnectSeqNumber; + } + + private void setReconnectSeqNumber(int reconnectSeqNumber) { + if (((reconnectSeqNumber < RECONNECT_MIN_SEQ_NUMBER) || (reconnectSeqNumber > RECONNECT_MAX_SEQ_NUMBER)) && + (reconnectSeqNumber != RECONNECT_INVALID_SEQ_NUMBER)) { + logger.warn("{}option --{} {} is invalid (min:{}, max:{}) => set to {}{}", + ansi().fgBright(WARN), OPT_LONG_RECONNECT_SEQ_NUMBER, reconnectSeqNumber, RECONNECT_MIN_SEQ_NUMBER, + RECONNECT_MAX_SEQ_NUMBER, RECONNECT_INVALID_SEQ_NUMBER, ansi().reset()); + reconnectSeqNumber = RECONNECT_INVALID_SEQ_NUMBER; // set to invalid + } + this.reconnectSeqNumber = reconnectSeqNumber; + logger.debug("{}={}", OPT_LONG_RECONNECT_SEQ_NUMBER, getReconnectSeqNumber()); + } + + private void setReconnectSeqNumber(String reconnectSeqNumber) { + try { + if (reconnectSeqNumber == null || reconnectSeqNumber.isBlank()) + setReconnectSeqNumber(RECONNECT_INVALID_SEQ_NUMBER); + else + setReconnectSeqNumber(Integer.parseInt(reconnectSeqNumber)); + } + catch (NumberFormatException e) { + setReconnectSeqNumber(RECONNECT_INVALID_SEQ_NUMBER); + logger.warn("{}option --{} {} is invalid => set to default {}{}", + ansi().fgBright(WARN), OPT_LONG_RECONNECT_SEQ_NUMBER, reconnectSeqNumber, getReconnectMs(), ansi().reset()); + } + } + + public boolean isValidReconnectSeqNumber() { + if (getReconnectSeqNumber() == RECONNECT_INVALID_SEQ_NUMBER) { + return false; + } + + return ((getReconnectSeqNumber() >= RECONNECT_MIN_SEQ_NUMBER) && + (getReconnectSeqNumber() <= RECONNECT_MAX_SEQ_NUMBER)); + } +} \ No newline at end of file diff --git a/firmware_updater/updater/src/org/selfbus/updater/Credits.java b/firmware_updater/updater/src/org/selfbus/updater/Credits.java new file mode 100644 index 00000000..35dd6a80 --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/Credits.java @@ -0,0 +1,35 @@ +package org.selfbus.updater; + +public final class Credits { + @SuppressWarnings("unused") + private Credits() {} + + private static final String[] authors = { + "Deti Fliegl", + "Pavel Kriz", + "Dr. Stefan Haller", + "Oliver Stefan", + "et al.", + }; + + @SuppressWarnings("SameReturnValue") + public static String getAsciiLogo() { + return """ + _____ ________ __________ __ _______ __ ______ ____ ___ ________________ + / ___// ____/ / / ____/ __ )/ / / / ___/ / / / / __ \\/ __ \\/ |/_ __/ ____/ __ \\ + \\__ \\/ __/ / / / /_ / __ / / / /\\__ \\ / / / / /_/ / / / / /| | / / / __/ / /_/ / + ___/ / /___/ /___/ __/ / /_/ / /_/ /___/ / / /_/ / ____/ /_/ / ___ |/ / / /___/ _, _/ + /____/_____/_____/_/ /_____/\\____//____/ \\____/_/ /_____/_/ |_/_/ /_____/_/ |_|"""; + } + + public static String getAuthors() { + StringBuilder result = new StringBuilder("by "); + for (int i = 0; i < authors.length; i++) { + result.append(authors[i]); + if (i < authors.length - 1) { + result.append(", "); + } + } + return result.toString(); + } +} diff --git a/firmware_updater/updater/src/org/selfbus/updater/DiscoverKnxInterfaces.java b/firmware_updater/updater/src/org/selfbus/updater/DiscoverKnxInterfaces.java new file mode 100644 index 00000000..612617fd --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/DiscoverKnxInterfaces.java @@ -0,0 +1,98 @@ +package org.selfbus.updater; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tuwien.auto.calimero.KNXException; +import tuwien.auto.calimero.knxnetip.Discoverer; +import tuwien.auto.calimero.knxnetip.servicetype.SearchResponse; +import tuwien.auto.calimero.link.medium.KNXMediumSettings; +import tuwien.auto.calimero.serial.usb.Device; +import tuwien.auto.calimero.serial.usb.UsbConnection; +import tuwien.auto.calimero.serial.usb.UsbConnectionFactory; + +import java.time.Duration; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; + +import static tuwien.auto.calimero.serial.SerialConnectionFactory.portIdentifiers; + +public class DiscoverKnxInterfaces { + private final static Logger logger = LoggerFactory.getLogger(DiscoverKnxInterfaces.class); + + public static List> getAllnetIPInterfaces() throws InterruptedException { + + List> interfacesResult = null; + try { + // set true to be aware of Network Address Translation (NAT) during discovery + final boolean useNAT = false; + interfacesResult = Discoverer.udp(useNAT).timeout(Duration.ofSeconds(3)).search().get(); + } + catch (ExecutionException e) { + logger.warn("Error during KNXnet/IP discovery: {}", e.getMessage()); + } + + return interfacesResult; + } + + public static Set getUsbInterfaces() throws InterruptedException { + Set knxUsbDevices = UsbConnectionFactory.attachedKnxUsbDevices(); + Iterator iterator = knxUsbDevices.iterator(); + while (iterator.hasNext()) { + Device d = iterator.next(); + try { + // check knx medium, defaults to TP1 + try (UsbConnection c = UsbConnectionFactory.open(d)) { + if (c.deviceDescriptor().medium().getMedium() != KNXMediumSettings.MEDIUM_TP1) { + iterator.remove(); + } + } + catch (KNXException e) { + iterator.remove(); + logger.warn("Reading KNX device descriptor of {} failed. ({}: {})", + d, e.getClass().getSimpleName(), e.getMessage()); + logger.debug("", e); + } + + } + catch (final RuntimeException e) { + iterator.remove(); + logger.error("error: {}", e.getMessage()); + } + } + return knxUsbDevices; + } + + public static Set getCOMPorts() { + return portIdentifiers(); + } + + public static void logIPInterfaces(List> ifaceList) { + for (Discoverer.Result r : ifaceList) { + SearchResponse sr = r.response(); + logger.info("Found IP interface: {}", sr.getDevice().getName()); + logger.info("\t{}", sr.toString().replace(", ", "\n\t")); + } + } + + public static void logUSBInterfaces(Set usbInterfaces) { + for (final var d : usbInterfaces) { + final String vp = String.format("%04x:%04x", d.vendorId(), d.productId()); + logger.info("Found USB Interface: {} {} S/N {} VID/PID {}", + d.manufacturer(), d.product(), d.serialNumber(), vp); + } + } + + public static void logCOMPorts(Set comPorts) { + if (comPorts.isEmpty()) { + logger.info("Found 0 COM ports."); + } + else if (comPorts.size() == 1) { + logger.info("Found COM port: {}", comPorts); + } + else { + logger.info("Found {} COM ports: {}", comPorts.size() , comPorts); + } + } +} diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/FlashDiffMode.java b/firmware_updater/updater/src/org/selfbus/updater/FlashDiffMode.java similarity index 72% rename from firmware_updater/updater/source/src/main/java/org/selfbus/updater/FlashDiffMode.java rename to firmware_updater/updater/src/org/selfbus/updater/FlashDiffMode.java index 7178b77f..716963cd 100644 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/FlashDiffMode.java +++ b/firmware_updater/updater/src/org/selfbus/updater/FlashDiffMode.java @@ -2,28 +2,31 @@ import net.harawata.appdirs.AppDirsFactory; import org.selfbus.updater.bootloader.BootDescriptor; -import org.selfbus.updater.tests.flashdiff.FlashDiff; +import org.selfbus.updater.devicemgnt.DeviceManagement; +import org.selfbus.updater.mode.differential.FlashDiff; +import org.selfbus.updater.progress.ProgressInfo; +import org.selfbus.updater.progress.SpinningCursor; 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.KNXRemoteException; import tuwien.auto.calimero.KNXTimeoutException; import tuwien.auto.calimero.link.KNXLinkClosedException; -import tuwien.auto.calimero.mgmt.KNXDisconnectException; import java.io.File; import java.util.Arrays; import java.util.concurrent.atomic.AtomicReference; +import static org.fusesource.jansi.Ansi.*; +import static org.selfbus.updater.logging.Color.*; import static org.selfbus.updater.Mcu.MAX_PAYLOAD; /** * experimental (WIP) Provides differential flash mode for the bootloader (MCU) */ public final class FlashDiffMode { - private final static Logger logger = LoggerFactory.getLogger(FlashDiffMode.class.getName()); + private final static Logger logger = LoggerFactory.getLogger(FlashDiffMode.class); // hexCacheDir will be used as a cache directory for the cached *.hex files used by the differential update mode // windows: C:\Users\[currentUser]\AppData\Local\Selfbus\Selfbus-Updater\Cache\ // linux : /home/[user-home]]/.cache/Selfbus-Updater/ @@ -40,7 +43,7 @@ public final class FlashDiffMode { private FlashDiffMode() {} public static String createCacheFileName(long startAddress, long length, long crc32) { - return String.format("%s%s%s%s0x%s%s%s%s0x%s%s", hexCacheDirectory, File.separator, IMAGE_IDENTIFIER, FILE_NAME_SEP, + return String.format("%s%s%s0x%s%s%s%s0x%s%s", hexCacheDirectory, IMAGE_IDENTIFIER, FILE_NAME_SEP, Long.toHexString(startAddress), FILE_NAME_SEP, length, FILE_NAME_SEP, Integer.toHexString((int)crc32), FILE_EXTENSION); } @@ -58,13 +61,12 @@ public static boolean setupDifferentialMode(BootDescriptor bootDescriptor) { //TODO check that "Current App Version String" and "File App Version String" represent the same application, // if they are not the same app, we should switch to full flash mode if (Utils.fileExists(oldFileName)) { - logger.info(" Found current device firmware in cache {}", Utils.shortenPath(oldFileName, DEFAULT_DISPLAYED_PATH_DEPTH)); - logger.info(" {}--> switching to diff upload mode{}", ConColors.BRIGHT_GREEN, ConColors.RESET); + logger.info("Found current device firmware in cache {}", Utils.shortenPath(oldFileName, DEFAULT_DISPLAYED_PATH_DEPTH)); initialized = true; } else { - logger.warn(" Current device firmware not found in cache {}", Utils.shortenPath(oldFileName, DEFAULT_DISPLAYED_PATH_DEPTH)); - logger.warn(" {}--> switching to FULL upload mode{}", ConColors.BRIGHT_RED, ConColors.RESET); + logger.warn("Current device firmware not found in cache {}", Utils.shortenPath(oldFileName, DEFAULT_DISPLAYED_PATH_DEPTH)); + logger.warn("{} --> switching to FULL upload mode{}", ansi().fgBright(WARN), ansi().reset()); initialized = false; } return initialized; @@ -80,10 +82,8 @@ public static boolean isInitialized() { } // Differential update routine - @SuppressWarnings("unused") public static ResponseResult doDifferentialFlash(DeviceManagement dm, long startAddress, byte[] binData) - throws KNXDisconnectException, KNXTimeoutException, KNXRemoteException, KNXLinkClosedException, InterruptedException, UpdaterException { - ///\todo add connection reset and sending again on failure, like in full flash mode + throws KNXTimeoutException, KNXLinkClosedException, InterruptedException, UpdaterException { if (!isInitialized()) { throw new UpdaterException("Differential mode not initialized!"); } @@ -92,17 +92,24 @@ public static ResponseResult doDifferentialFlash(DeviceManagement dm, long start } File oldImageCacheFile = new File(oldFileName); - - FlashDiff differ = new FlashDiff(); BinImage img2 = BinImage.copyFromArray(binData, startAddress); BinImage img1 = BinImage.readFromBin(oldImageCacheFile.getAbsolutePath(), bootDsc.startAddress()); + //Execute lambda once to get total byte count to transfer + FlashDiff differ = new FlashDiff(); + differ.generateDiff(img1, img2, (outputDiffStream, crc32) -> {}); + long bytesToFlash = differ.getTotalBytesTransferred(); + ProgressInfo progressInfo = new ProgressInfo(bytesToFlash); + SpinningCursor.reset(); + logger.info("Start sending differential data ({} bytes)", bytesToFlash); + dm.startProgressInfo(progressInfo); + AtomicReference result = new AtomicReference<>(); + differ = new FlashDiff(); differ.generateDiff(img1, img2, (outputDiffStream, crc32) -> { // process compressed page - //TODO check why 5 bytes are added to size in FlashDiff.java / generateDiff(...) - logger.info("Sending new firmware ({} diff bytes)", (outputDiffStream.size() - 5)); + logger.debug("Sending new firmware ({} diff bytes)", (outputDiffStream.size())); byte[] buf = new byte[MAX_PAYLOAD]; int i = 0; while (i < outputDiffStream.size()) { @@ -115,23 +122,23 @@ public static ResponseResult doDifferentialFlash(DeviceManagement dm, long start byte[] txBuf = Arrays.copyOf(buf, j); // avoid padded last telegram result.set(dm.sendWithRetry(UPDCommand.SEND_DATA_TO_DECOMPRESS, txBuf, dm.getMaxUpdCommandRetry())); //\todo switch to full flash mode on a NOT_IMPLEMENTED instead of exiting - if (UPDProtocol.checkResult(result.get().data(), false) != UDPResult.IAP_SUCCESS.id) { + if (UPDProtocol.checkResult(result.get().data(), false) != UDPResult.IAP_SUCCESS) { dm.restartProgrammingDevice(); - throw new UpdaterException("Selfbus update failed."); + throw new UpdaterException("DiffMode SEND_DATA_TO_DECOMPRESS failed."); } + dm.updateProgressInfo(progressInfo, txBuf.length); } // diff data of a single page transmitted // flash the page byte[] progPars = new byte[4]; Utils.longToStream(progPars, 0, (int) crc32); - System.out.println(); - logger.info("Program device next page diff, crc32 0x{}", String.format("%08X", crc32)); + logger.debug("Program device next page diff, crc32 0x{}", String.format("%08X", crc32)); result.set(dm.sendWithRetry(UPDCommand.PROGRAM_DECOMPRESSED_DATA, progPars, dm.getMaxUpdCommandRetry())); - if (UPDProtocol.checkResult(result.get().data()) != UDPResult.IAP_SUCCESS.id) { - dm.restartProgrammingDevice(); - throw new UpdaterException("Selfbus update failed."); + if (UPDProtocol.checkResult(result.get().data()) != UDPResult.IAP_SUCCESS) { + throw new UpdaterException("DiffMode PROGRAM_DECOMPRESSED_DATA failed."); } }); + dm.logAndPrintProgressInfo(progressInfo); result.get().setWritten(differ.getTotalBytesTransferred()); return result.get(); } diff --git a/firmware_updater/updater/src/org/selfbus/updater/FlashFullMode.java b/firmware_updater/updater/src/org/selfbus/updater/FlashFullMode.java new file mode 100644 index 00000000..54258902 --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/FlashFullMode.java @@ -0,0 +1,126 @@ +package org.selfbus.updater; + +import org.selfbus.updater.bootloader.BootloaderStatistic; +import org.selfbus.updater.devicemgnt.DeviceManagement; +import org.selfbus.updater.progress.ProgressInfo; +import org.selfbus.updater.progress.ProgressInfoAdvanced; +import org.selfbus.updater.progress.SpinningCursor; +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.KNXTimeoutException; +import tuwien.auto.calimero.link.KNXLinkClosedException; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import static org.fusesource.jansi.Ansi.*; +import static org.selfbus.updater.logging.Color.*; + +/** + * Provides full flash mode for the bootloader (MCU) + */ +public class FlashFullMode { + private final static Logger logger = LoggerFactory.getLogger(FlashFullMode.class); + + /** + * Normal update routine, sending complete image + */ + public static ResponseResult doFullFlash(DeviceManagement dm, BinImage newFirmware, int dataSendDelay, + boolean eraseFirmwareRange, boolean logStatistics) + throws IOException, KNXTimeoutException, KNXLinkClosedException, + InterruptedException, UpdaterException { + ResponseResult resultSendData, resultProgramData; + ResponseResult resultTotal = new ResponseResult(); // collect so static data + + long totalLength = newFirmware.length(); + ByteArrayInputStream fis = new ByteArrayInputStream(newFirmware.getBinData()); + + if (eraseFirmwareRange) { + dm.eraseAddressRange(newFirmware.startAddress(), totalLength); // erase affected flash range + } + + long progAddress = newFirmware.startAddress(); + + String logMessage = String.format("Start sending application data (%d bytes)", totalLength); + if (dataSendDelay > 0) { + logMessage += String.format(" with telegram delay of %dms", dataSendDelay); + } + logger.info(logMessage); + + SpinningCursor.reset(); + ProgressInfo progressInfo; + BootloaderStatistic bootloaderStatistic; + if (logStatistics) { + bootloaderStatistic = dm.requestBootLoaderStatistic(); + progressInfo = new ProgressInfoAdvanced(totalLength, bootloaderStatistic); + } + else { + bootloaderStatistic = null; + progressInfo = new ProgressInfo(totalLength); + } + dm.startProgressInfo(progressInfo); + + int nRead = 0; + byte[] buffer = null; + while ((fis.available() > 0) || (buffer != null)) { + if (buffer == null) { + // Start or buffer was successfully transferred + buffer = new byte[dm.getBlockSize()]; + nRead = fis.read(buffer); // Read up to size of buffer + } + + byte[] txBuffer = new byte[nRead]; + System.arraycopy(buffer, 0, txBuffer, 0, nRead); + + // send data to the bootloader + resultSendData = dm.doFlash(txBuffer, dm.getMaxUpdCommandRetry(), dataSendDelay, progressInfo); + resultTotal.addCounters(resultSendData); // keep track of static data + + // flash the previously sent data + int crc32 = Utils.crc32Value(txBuffer); + byte[] progPars = new byte[2 + 4 + 4]; + Utils.shortToStream(progPars, 0, (short)txBuffer.length); + Utils.longToStream(progPars, 2, progAddress); + Utils.longToStream(progPars, 6, crc32); + + String debugInfo = String.format("%s 0x%04X-0x%04X", progressInfo, progAddress, progAddress + txBuffer.length - 1); + if (txBuffer.length != dm.getBlockSize()) { + debugInfo = String.format("%s (%d bytes)", debugInfo, txBuffer.length); + } + logger.debug("{} with crc32 {}", debugInfo, String.format("0x%08X", crc32)); + + resultProgramData = dm.sendWithRetry(UPDCommand.PROGRAM, progPars, dm.getMaxUpdCommandRetry()); + resultTotal.addCounters(resultProgramData); // keep track of statistic data + + UDPResult result = UPDProtocol.checkResult(resultProgramData.data()); + switch (result) { + case IAP_SUCCESS: + buffer = null; + progAddress += txBuffer.length; + if (bootloaderStatistic != null) { + bootloaderStatistic = dm.requestBootLoaderStatistic(); + } + break; + + case BYTECOUNT_RECEIVED_TOO_LOW: + case BYTECOUNT_RECEIVED_TOO_HIGH: + // do not count failed transfer and send buffer again in next while run + progressInfo.update(-txBuffer.length); + break; + + case IAP_COMPARE_ERROR: + throw new UpdaterException(String.format("ProgramData update failed. %sTry again with option '--%s 256'%s", + ansi().fgBright(WARN), CliOptions.OPT_LONG_BLOCKSIZE, ansi().reset())); + default: + dm.restartProgrammingDevice(); + throw new UpdaterException("ProgramData update failed."); + } + } + fis.close(); + dm.logAndPrintProgressInfo(progressInfo); + resultTotal.setWritten(progressInfo.getBytesDone()); + return resultTotal; + } +} diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/Mcu.java b/firmware_updater/updater/src/org/selfbus/updater/Mcu.java similarity index 75% rename from firmware_updater/updater/source/src/main/java/org/selfbus/updater/Mcu.java rename to firmware_updater/updater/src/org/selfbus/updater/Mcu.java index e7d23233..94a298d1 100644 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/Mcu.java +++ b/firmware_updater/updater/src/org/selfbus/updater/Mcu.java @@ -6,12 +6,19 @@ * Basic information about the bootloader's MCU */ public final class Mcu { - /** Maximum length a asdu can be in a standard frame */ + /** Maximum length an asdu can be in a standard frame */ public static final int MAX_ASDU_LENGTH = 14; /** Maximum data payload one APCI_USERMSG_MANUFACTURER_0/APCI_USERMSG_MANUFACTURER_6 can handle */ public static final int MAX_PAYLOAD = MAX_ASDU_LENGTH - 1; /** Selfbus ARM controller flash page size */ public static final int FLASH_PAGE_SIZE = 256; + /** Selfbus ARM controller flash start address */ + public static final int FLASH_START_ADDRESS = 0; + /** Selfbus ARM controller flash size in bytes*/ + public static final int FLASH_SIZE_BYTES = 0x10000; + /** Selfbus ARM controller flash end address */ + public static final int FLASH_END_ADDRESS = FLASH_START_ADDRESS + FLASH_SIZE_BYTES - 1; + /** Size in bytes to flash with UPD_PROGRAM */ public static final int UPD_PROGRAM_SIZE = 1024; /** Vector table end of the mcu */ diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/ResponseResult.java b/firmware_updater/updater/src/org/selfbus/updater/ResponseResult.java similarity index 95% rename from firmware_updater/updater/source/src/main/java/org/selfbus/updater/ResponseResult.java rename to firmware_updater/updater/src/org/selfbus/updater/ResponseResult.java index 7f313789..0603f233 100644 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/ResponseResult.java +++ b/firmware_updater/updater/src/org/selfbus/updater/ResponseResult.java @@ -8,14 +8,14 @@ public class ResponseResult { private long dropCount; private long written; - ResponseResult() { + public ResponseResult() { this.timeoutCount = 0; this.dropCount = 0; this.data = null; this.written = 0; } - byte[] data() { + public byte[] data() { return data; } diff --git a/firmware_updater/updater/src/org/selfbus/updater/SBKNXLink.java b/firmware_updater/updater/src/org/selfbus/updater/SBKNXLink.java new file mode 100644 index 00000000..de694c3d --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/SBKNXLink.java @@ -0,0 +1,187 @@ +package org.selfbus.updater; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tuwien.auto.calimero.IndividualAddress; +import tuwien.auto.calimero.KNXException; +import tuwien.auto.calimero.knxnetip.KNXnetIPRouting; +import tuwien.auto.calimero.knxnetip.SecureConnection; +import tuwien.auto.calimero.knxnetip.TcpConnection; +import tuwien.auto.calimero.link.KNXNetworkLink; +import tuwien.auto.calimero.link.KNXNetworkLinkFT12; +import tuwien.auto.calimero.link.KNXNetworkLinkIP; +import tuwien.auto.calimero.link.KNXNetworkLinkTpuart; +import tuwien.auto.calimero.link.KNXNetworkLinkUsb; +import tuwien.auto.calimero.link.medium.KNXMediumSettings; +import tuwien.auto.calimero.link.medium.TPSettings; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.Collections; +import java.util.Objects; + +import static org.fusesource.jansi.Ansi.*; +import static org.selfbus.updater.logging.Color.*; + +public class SBKNXLink { + private static final Logger logger = LoggerFactory.getLogger(SBKNXLink.class); + private CliOptions cliOptions; + + @SuppressWarnings("unused") + private SBKNXLink() { + + } + + public SBKNXLink(CliOptions cliOptions) { + this.cliOptions = cliOptions; + } + + private KNXNetworkLink createSecureTunnelingLink(InetSocketAddress local, InetSocketAddress remote, + KNXMediumSettings medium) throws KNXException, InterruptedException { + // KNX IP Secure TCP tunneling v2 connection + logger.info("Connect using KNX IP Secure tunneling"); + byte[] deviceAuthCode = SecureConnection.hashDeviceAuthenticationPassword(cliOptions.getKnxSecureDevicePassword().toCharArray()); + byte[] userKey = SecureConnection.hashUserPassword(cliOptions.getKnxSecureUserPassword().toCharArray()); + final TcpConnection.SecureSession session; + try (TcpConnection tcpConnection = Utils.tcpConnection(local, remote)) { + session = tcpConnection.newSecureSession(cliOptions.getKnxSecureUserId(), userKey, deviceAuthCode); + } + return KNXNetworkLinkIP.newSecureTunnelingLink(session, medium); + } + + private KNXNetworkLink createTunnelingLinkV2(InetSocketAddress local, InetSocketAddress remote, + KNXMediumSettings medium) throws KNXException, InterruptedException { + logger.info("Connect using TCP tunneling v2"); + final var session = Utils.tcpConnection(local, remote); + return KNXNetworkLinkIP.newTunnelingLink(session, medium); + } + + private KNXNetworkLink createTunnelingLinkV1(InetSocketAddress local, InetSocketAddress remote, boolean useNat, + KNXMediumSettings medium) throws KNXException, InterruptedException { + logger.info("{}Connect using UDP tunneling v1 (nat:{}){}", ansi().fgBright(INFO), useNat, ansi().reset()); + return KNXNetworkLinkIP.newTunnelingLink(local, remote, useNat, medium); + } + + private KNXNetworkLink createRoutingLink(InetSocketAddress local, KNXMediumSettings medium) throws KNXException { + logger.info("{}Connect using routing (multicast:{}){}", + ansi().fgBright(INFO), KNXnetIPRouting.DefaultMulticast, ansi().reset()); + + return KNXNetworkLinkIP.newRoutingLink(local.getAddress(), KNXnetIPRouting.DefaultMulticast, medium); + } + + private static InetSocketAddress createLocalSocket(String host, Integer port) { + port = Objects.requireNonNullElse(port, 0); + if (host.isEmpty()) { + return new InetSocketAddress(port); + } + else { + return new InetSocketAddress(host, port); + } + } + + private static KNXMediumSettings getMedium(final String id, IndividualAddress ownAddress) + throws UpdaterException { + if (id.equalsIgnoreCase(KNXMediumSettings.getMediumString(KNXMediumSettings.MEDIUM_TP1))) { + return new TPSettings(ownAddress); + } + else if (id.equalsIgnoreCase(KNXMediumSettings.getMediumString(KNXMediumSettings.MEDIUM_RF))) { + throw new UpdaterException(String.format("medium '%s' not implemented", id)); + //return new RFSettings(ownAddress); + } + //else if (id.equalsIgnoreCase("tp0")) + // return TPSettings.TP0; + //else if (id.equalsIgnoreCase("p110")) + // return new PLSettings(false); + //else if (id.equalsIgnoreCase("p132")) + // return new PLSettings(true); + else + throw new UpdaterException("unknown medium"); + } + + private InetAddress resolveHost(final String host) throws UnknownHostException { + InetAddress res = InetAddress.getByName(host); + logger.debug("Resolved {} with {}", host, res); + return res; + } + + /** + * Creates the KNX network link to access the network specified in + * options. + *

+ * + * @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.
+ *
+ * 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. + *

+ * 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}.
+ */ +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 Map tcpConnectionPool = new HashMap<>(); private static boolean registeredTcpShutdownHook; - static synchronized TcpConnection tcpConnection(final InetSocketAddress local, final InetSocketAddress server) - throws KNXException { + static synchronized TcpConnection tcpConnection(final InetSocketAddress local, final InetSocketAddress server) { if (!registeredTcpShutdownHook) { Runtime.getRuntime().addShutdownHook(new Thread(Utils::closeTcpConnections)); registeredTcpShutdownHook = true; diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/bootloader/BootDescriptor.java b/firmware_updater/updater/src/org/selfbus/updater/bootloader/BootDescriptor.java similarity index 70% rename from firmware_updater/updater/source/src/main/java/org/selfbus/updater/bootloader/BootDescriptor.java rename to firmware_updater/updater/src/org/selfbus/updater/bootloader/BootDescriptor.java index 5cad5434..0448fd1d 100644 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/bootloader/BootDescriptor.java +++ b/firmware_updater/updater/src/org/selfbus/updater/bootloader/BootDescriptor.java @@ -1,12 +1,14 @@ package org.selfbus.updater.bootloader; -import org.selfbus.updater.ConColors; -import org.selfbus.updater.UpdaterException; import org.selfbus.updater.Utils; +import static org.fusesource.jansi.Ansi.*; +import static org.selfbus.updater.logging.Color.*; + /** * Holds Application Boot Block Descriptor the MCU's bootloader - * see software-arm-lib/Bus-Updater/inc/boot_descriptor_block.h for more information + *

+ * 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 device 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 tunnelAddress = settings.assignedAddress(); + if (tunnelAddress.isPresent()) { + linkInfo += String.format(", tunnel %s", tunnelAddress.get()); + } + return linkInfo; + } +} diff --git a/firmware_updater/updater/src/org/selfbus/updater/devicemgnt/DeviceManagementFactory.java b/firmware_updater/updater/src/org/selfbus/updater/devicemgnt/DeviceManagementFactory.java new file mode 100644 index 00000000..7538b0c5 --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/devicemgnt/DeviceManagementFactory.java @@ -0,0 +1,17 @@ +package org.selfbus.updater.devicemgnt; + +import org.selfbus.updater.CliOptions; + +final public class DeviceManagementFactory { + @SuppressWarnings("unused") + private DeviceManagementFactory() {} + + public static DeviceManagement getDeviceManagement(CliOptions cliOptions) { + if (cliOptions.isValidReconnectSeqNumber()) { + return new DeviceManagementReflect(cliOptions); + } + else { + return new DeviceManagement(cliOptions); + } + } +} diff --git a/firmware_updater/updater/src/org/selfbus/updater/devicemgnt/DeviceManagementReflect.java b/firmware_updater/updater/src/org/selfbus/updater/devicemgnt/DeviceManagementReflect.java new file mode 100644 index 00000000..7d25a663 --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/devicemgnt/DeviceManagementReflect.java @@ -0,0 +1,29 @@ +package org.selfbus.updater.devicemgnt; + +import org.selfbus.updater.*; +import org.selfbus.updater.upd.UPDCommand; +import tuwien.auto.calimero.KNXException; + +import java.net.UnknownHostException; + +public class DeviceManagementReflect extends DeviceManagement { + public DeviceManagementReflect(CliOptions cliOptions) { + super(cliOptions); + } + + @Override + public ResponseResult sendWithRetry(final UPDCommand command, final byte[] data, int maxRetry) + throws UpdaterException, InterruptedException { + ReflectKNXnetIPTunnel reflect = new ReflectKNXnetIPTunnel(link); + final int seqNmb = reflect.getSequenceNumberSent(); + + if ((cliOptions.isValidReconnectSeqNumber()) && (seqNmb >= cliOptions.getReconnectSeqNumber())) { + try { + this.reconnect(cliOptions.getReconnectMs()); + } + catch (KNXException | UnknownHostException ignore) { + } + } + return super.sendWithRetry(command, data, maxRetry); + } +} diff --git a/firmware_updater/updater/src/org/selfbus/updater/devicemgnt/ReflectKNXnetIPTunnel.java b/firmware_updater/updater/src/org/selfbus/updater/devicemgnt/ReflectKNXnetIPTunnel.java new file mode 100644 index 00000000..28e91f82 --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/devicemgnt/ReflectKNXnetIPTunnel.java @@ -0,0 +1,79 @@ +package org.selfbus.updater.devicemgnt; + +import com.google.common.collect.Lists; +import tuwien.auto.calimero.knxnetip.KNXnetIPTunnel; + +import java.lang.reflect.Field; +import java.util.List; + +public class ReflectKNXnetIPTunnel { + + private KNXnetIPTunnel tunnel = null; + Field fieldSeqNumb = null; + private final static int SEQUENCE_NUMBER_INVALID = -1; + int sequenceNumberSent = SEQUENCE_NUMBER_INVALID; + + @SuppressWarnings("unused") + private ReflectKNXnetIPTunnel() { + } + + public ReflectKNXnetIPTunnel(Object obj) { + Field fieldConn = getField(obj, "conn"); + if (fieldConn == null) { + return; + } + + fieldConn.setAccessible(true); + try { + Object connValue = fieldConn.get(obj); + if (!(connValue instanceof KNXnetIPTunnel)) { + return; + } + tunnel = (KNXnetIPTunnel) connValue; + fieldSeqNumb = getField(tunnel, "seqSend"); + if (fieldSeqNumb == null) { + return; + } + fieldSeqNumb.setAccessible(true); + } + catch (IllegalAccessException ignored) { + } + } + + public int getSequenceNumberSent() { + if (fieldSeqNumb == null) { + return SEQUENCE_NUMBER_INVALID; + } + + try { + sequenceNumberSent = fieldSeqNumb.getInt(tunnel); + } + catch (IllegalAccessException ignored) { + return SEQUENCE_NUMBER_INVALID; + } + return sequenceNumberSent; + } + + private Field getField(Object obj, String fieldName) { + // Iterate over all declared fields + for (Field field : getFieldsUpTo(obj.getClass(), Object.class)) { + if (field.getName().equals(fieldName)) { + return field; + } + } + return null; + } + + private static Iterable getFieldsUpTo(Class startClass, + Class exclusiveParent) { + List currentClassFields = Lists.newArrayList(startClass.getDeclaredFields()); + Class parentClass = startClass.getSuperclass(); + + if (parentClass != null && (!(parentClass.equals(exclusiveParent)))) { + List parentClassFields = + (List) getFieldsUpTo(parentClass, exclusiveParent); + currentClassFields.addAll(parentClassFields); + } + return currentClassFields; + } +} diff --git a/firmware_updater/updater/src/org/selfbus/updater/gui/CliConverter.java b/firmware_updater/updater/src/org/selfbus/updater/gui/CliConverter.java new file mode 100644 index 00000000..e8723562 --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/gui/CliConverter.java @@ -0,0 +1,41 @@ +package org.selfbus.updater.gui; + +import javax.swing.JTextField; +import javax.swing.JComboBox; +import java.util.Objects; + +public final class CliConverter { + @SuppressWarnings("unused") + private CliConverter() {} + + public static String argument(String cliLongOption, JTextField textField) { + return buildArgument(cliLongOption, textField.getText(), true); + } + + public static String argument(String cliLongOption, JComboBox comboBox) { + return buildArgument(cliLongOption, Objects.requireNonNull(comboBox.getSelectedItem()).toString(), true); + } + + public static String argument(String cliLongOption) { + return buildArgument(cliLongOption, "", false); + } + + private static String buildArgument(String cliLongOption, String argValue, boolean argValueRequired) { + cliLongOption = cliLongOption.trim(); + argValue = argValue.trim(); + + if (argValueRequired && argValue.isEmpty()) { + return ""; // Discard the argument if argValue is required, but not provided + } + + if (argValue.isEmpty()) { + return String.format("--%s", cliLongOption); + } + + if (cliLongOption.isEmpty()) { + return argValue; + } + + return (String.format("--%s=%s", cliLongOption, argValue)); + } +} diff --git a/firmware_updater/updater/src/org/selfbus/updater/gui/ConColorsToStyledDoc.java b/firmware_updater/updater/src/org/selfbus/updater/gui/ConColorsToStyledDoc.java new file mode 100644 index 00000000..ef6f3429 --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/gui/ConColorsToStyledDoc.java @@ -0,0 +1,514 @@ +package org.selfbus.updater.gui; + +import org.selfbus.updater.progress.AnsiCursor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.*; +import javax.swing.text.*; +import java.awt.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.fusesource.jansi.Ansi.ansi; + +public final class ConColorsToStyledDoc { + @SuppressWarnings("unused") + private ConColorsToStyledDoc() {} // avoids instance creation + + private static final Logger logger = LoggerFactory.getLogger(ConColorsToStyledDoc.class); + private static final Style stringStyle = new javax.swing.text.StyleContext().addStyle("testStyle", null); + + public static final java.awt.Color DefaultForegroundColor = java.awt.Color.white; + public static final java.awt.Color DefaultBackgroundColor = java.awt.Color.black; + + private static final double normalScaling = 0.7; + private static final double brightnessScaling = 1; + + private static final String AnsiEscape = "\033"; // ASCII escape character (ESC) + private static final String AnsiBracket = "["; // Ansi second character [ + + /** + * Regular expression for matching ANSI escape sequences. + *

+ * Includes sequences for text styles, colors and some cursor movements. + *

+ *

    + *
  • {@code ([;\\d*)[A-Hmsu]} - Matches a sequence of zero or more digits and semicolons + * followed by a character indicating the type of ANSI control or formatting action. + *
      + *
    • A-H - Cursor movement
    • + *
    • m - Set Graphics Mode (color, bold, underline, etc.)
    • + *
    • s - Save Cursor Position
    • + *
    • u - Restore Cursor Position
    • + *
    + *
  • + *
  • {@code (\\?25[hl])} - Cursor on/off
  • + *
+ *

+ *

+ * Great overview on ANSI escape codes and their usage: + * ANSI escape codes + *

+ */ + @SuppressWarnings("SpellCheckingInspection") + private static final String RegExAnsi = AnsiEscape + "\\" + AnsiBracket + "(([;\\d]*)[A-Hmsu]|(\\?25[hl]))"; + private static final String RegExAnsiCursor = AnsiEscape + "\\" + AnsiBracket + "(([;\\d]*)[A-Hsu])"; + private static final String AnsiCodeSeparator = ";"; + + private static void insertStyledString(String toInsert, String colorCode, JTextPane textPane) throws BadLocationException { + Style newStyle = colorCodeToStyle(colorCode.split(AnsiCodeSeparator)); + StyledDocument document = (StyledDocument) textPane.getDocument(); + document.insertString(document.getLength(), toInsert, newStyle); + textPane.setCaretPosition(document.getLength()); + } + + /** + * Converts ANSI escape sequences in the input string to styled text in a {@link JTextPane}. + *

+ * 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. + *

+ * + * @param input The string containing ANSI escape sequences to be converted. + * @param textPane The {@link JTextPane} where the styled text will be inserted. + * @throws BadLocationException if an error occurs while inserting the styled text. + */ + public static void Convert(String input, JTextPane textPane) throws BadLocationException { + Pattern pattern = Pattern.compile(RegExAnsi); + Matcher matcher = pattern.matcher(input); + + String cleanedString; + String ansiCode = ""; + int index = 0; + while (matcher.find()) { + if (matcher.start() > index) { // possible text before the ANSI escape sequence + cleanedString = input.substring(index, matcher.start()); + insertStyledString(cleanedString, ansiCode, textPane); + } + ansiCode = matcher.group(); + + if (processAnsiCursor(ansiCode, textPane)) { + index = matcher.end(); + ansiCode = ""; + continue; + } + + ansiCode = ansiCode.replace(AnsiEscape + AnsiBracket, ""); + index = matcher.end(); + } + + if (index < input.length()) { // possible remaining text after the last ANSI escape sequence + cleanedString = input.substring(index); + insertStyledString(cleanedString, ansiCode, textPane); + } + + // immer die letzte Zeile zeigen + textPane.setCaretPosition(textPane.getDocument().getLength()); + } + + /** + * Processes ANSI cursor control codes and updates the cursor position in the given {@link 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.

+ * + * @param ansiCode the ANSI code to process + * @param textPane the {@link JTextPane} whose cursor position will be updated + * @return {@code true} if the ANSI code was successfully processed; {@code false} otherwise + * @throws BadLocationException if an error occurs when updating the cursor position + * @throws IllegalStateException if a not implemented or unexpected ANSI code is encountered + */ + private static boolean processAnsiCursor(String ansiCode, JTextPane textPane) throws BadLocationException { + if (ansiCode.equals(AnsiCursor.off()) || ansiCode.equals(AnsiCursor.on())) { + return true; // there is no visible cursor in the JTextPane + } + + if (ansiCode.equals(ansi().restoreCursorPosition().toString()) || + ansiCode.equals(ansi().saveCursorPosition().toString())) { + //todo implement save and restore cursor position + throw new IllegalStateException("Save/Restore cursor position not implemented: " + ansiCode); + } + + Pattern pattern = Pattern.compile(RegExAnsiCursor); + Matcher matcher = pattern.matcher(ansiCode); + while (matcher.find()) { + String cursorMovement = matcher.group(1); + if (cursorMovement.isEmpty()) { + continue; + } + + Point moveCursor = getCursorPosition(textPane); + int cursorMovementCode = cursorMovement.charAt(cursorMovement.length() - 1); + int number; + if (cursorMovement.length() > 1) { + number = Integer.parseInt(cursorMovement.substring(0, cursorMovement.length() - 1)); + } + else { + number = 1; + } + + switch (cursorMovementCode) { + case 65: // A Move up by n rows + moveCursor.y -= number; //todo cursor movement not tested + break; + + case 66: // B Move down by n rows + moveCursor.y += number; //todo cursor movement not tested + break; + + case 67: // C Move right by n columns + moveCursor.x += number; //todo cursor movement not tested + break; + + case 68: // D Move left by n columns + moveCursor.x -= number; //todo cursor movement not tested + break; + + case 72: // H Move cursor to row n and column m + throw new IllegalStateException("Not implemented cursorMovement: " + cursorMovement); + + case 69: // E Move cursor to beginning of line and n lines down + moveCursor.x = 1; + moveCursor.y += number; + break; + + case 70: // F Move cursor to beginning of line and n lines up + moveCursor.x = 1; + moveCursor.y -= number; + break; + + case 71: // G Move cursor to column n + moveCursor.x = number; + break; + + default: + throw new IllegalStateException("Unexpected cursorMovement: " + cursorMovement); + } + setCursorPosition(textPane, moveCursor); + return true; + } + return false; + } + + /** + * Resets the styling attributes of the {@link #stringStyle} + */ + private static void resetStringStyle() { + StyleConstants.setForeground(ConColorsToStyledDoc.stringStyle, DefaultForegroundColor); + StyleConstants.setBackground(ConColorsToStyledDoc.stringStyle, DefaultBackgroundColor); + StyleConstants.setBold(ConColorsToStyledDoc.stringStyle,false); + StyleConstants.setItalic(ConColorsToStyledDoc.stringStyle,false); + StyleConstants.setUnderline(ConColorsToStyledDoc.stringStyle,false); + StyleConstants.setStrikeThrough(ConColorsToStyledDoc.stringStyle,false); + } + + /** + * Converts an array of color codes to a {@link Style} with the corresponding styles applied. + *

+ * 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. + *

+ * + * @param colorCodes an array of ANSI color codes as {@link String} values + * @return the {@link #stringStyle} object with the styles applied + * @throws IllegalStateException if an 256 fore/background color code (38 or 48) or an unexpected code value is encountered + */ + private static Style colorCodeToStyle(String [] colorCodes) { + for (String code : colorCodes) { + if (code.isEmpty() || code.equals("m")) { + resetStringStyle(); + continue; + } + + if (code.charAt(code.length() - 1) == 'm') { + code = code.substring(0, code.length() - 1); // strip of 'm' + } + + switch (Integer.parseInt(code)) { + case 0: // Reset + resetStringStyle(); + break; + case 1: // Bold on + StyleConstants.setBold(stringStyle,true); + break; + case 2: // Bold off + StyleConstants.setBold(stringStyle,false); + break; + case 3: // Italic on + StyleConstants.setItalic(stringStyle,true); + break; + case 4: // Underline on + StyleConstants.setUnderline(stringStyle,true); + break; + // + // 5-8 reserved + // + case 9: // Strike on + StyleConstants.setStrikeThrough(stringStyle,true); + break; + // + // 10-20 reserved + // + case 21: // Bold off (same as 2) + StyleConstants.setBold(stringStyle,false); + break; + // 22 reserved + case 23: // Italic off + StyleConstants.setItalic(stringStyle,false); + break; + case 24: // Underline off + StyleConstants.setUnderline(stringStyle,false); + break; + // 25-28 reserved + case 29: // Strike off + StyleConstants.setStrikeThrough(stringStyle,false); + break; + // + // Foreground colors + // + case 30: // Foreground color black + StyleConstants.setForeground(stringStyle, normalColor(java.awt.Color.black)); + break; + case 31: // Foreground color red + StyleConstants.setForeground(stringStyle, normalColor(java.awt.Color.red)); + break; + case 32: // Foreground color green + StyleConstants.setForeground(stringStyle, normalColor(java.awt.Color.green)); + break; + case 33: // Foreground color yellow + StyleConstants.setForeground(stringStyle, normalColor(java.awt.Color.yellow)); + break; + case 34: // Foreground color blue + StyleConstants.setForeground(stringStyle, normalColor(java.awt.Color.blue)); + break; + case 35: // Foreground color magenta + StyleConstants.setForeground(stringStyle, normalColor(java.awt.Color.magenta)); + break; + case 36: // Foreground color cyan + StyleConstants.setForeground(stringStyle, normalColor(java.awt.Color.cyan)); + break; + case 37: // Foreground color white + StyleConstants.setForeground(stringStyle, normalColor(java.awt.Color.white)); + break; + case 38: + throw new IllegalStateException("256 color foreground not implemented"); + case 39: // Foreground color default + StyleConstants.setForeground(stringStyle, normalColor(DefaultForegroundColor)); + break; + // + // Background colors + // + case 40: // Background color black + StyleConstants.setBackground(stringStyle, normalColor(java.awt.Color.black)); + break; + case 41: // Background color red + StyleConstants.setBackground(stringStyle, normalColor(java.awt.Color.red)); + break; + case 42: // Background color green + StyleConstants.setBackground(stringStyle, normalColor(java.awt.Color.green)); + break; + case 43: // Background color yellow + StyleConstants.setBackground(stringStyle, normalColor(java.awt.Color.yellow)); + break; + case 44: // Background color blue + StyleConstants.setBackground(stringStyle, normalColor(java.awt.Color.blue)); + break; + case 45: // Background color magenta + StyleConstants.setBackground(stringStyle, normalColor(java.awt.Color.magenta)); + break; + case 46: // Background color cyan + StyleConstants.setBackground(stringStyle, normalColor(java.awt.Color.cyan)); + break; + case 47: // Background color white + StyleConstants.setBackground(stringStyle, normalColor(java.awt.Color.white)); + break; + case 48: + throw new IllegalStateException("256 color background not implemented"); + case 49: // Background color default + StyleConstants.setBackground(stringStyle, normalColor(DefaultBackgroundColor)); + break; + // + // 50-89 reserved + // + + // + // Bright foreground colors + // + case 90: // Bright foreground color black + StyleConstants.setForeground(stringStyle, brightColor(java.awt.Color.black)); + break; + case 91: // Bright foreground color red + StyleConstants.setForeground(stringStyle, brightColor(java.awt.Color.red)); + break; + case 92: // Bright foreground color green + StyleConstants.setForeground(stringStyle, brightColor(java.awt.Color.green)); + break; + case 93: // Bright foreground color yellow + StyleConstants.setForeground(stringStyle, brightColor(java.awt.Color.yellow)); + break; + case 94: // Bright foreground color blue + StyleConstants.setForeground(stringStyle, brightColor(java.awt.Color.blue)); + break; + case 95: // Bright foreground color magenta + StyleConstants.setForeground(stringStyle, brightColor(java.awt.Color.magenta)); + break; + case 96: // Bright foreground color cyan + StyleConstants.setForeground(stringStyle, brightColor(java.awt.Color.cyan)); + break; + case 97: // Bright foreground color white + StyleConstants.setForeground(stringStyle, brightColor(java.awt.Color.white)); + break; + // + // 98 reserved + // + case 99: // Bright foreground color default + StyleConstants.setForeground(stringStyle, brightColor(DefaultBackgroundColor)); + break; + + // + // Bright background colors + // + case 100: // Bright background color black + StyleConstants.setBackground(stringStyle, brightColor(java.awt.Color.black)); + break; + case 101: // Bright background color red + StyleConstants.setBackground(stringStyle, brightColor(java.awt.Color.red)); + break; + case 102: // Bright background color green + StyleConstants.setBackground(stringStyle, brightColor(java.awt.Color.green)); + break; + case 103: // Bright background color yellow + StyleConstants.setBackground(stringStyle, brightColor(java.awt.Color.yellow)); + break; + case 104: // Bright background color blue + StyleConstants.setBackground(stringStyle, brightColor(java.awt.Color.blue)); + break; + case 105: // Bright background color magenta + StyleConstants.setBackground(stringStyle, brightColor(java.awt.Color.magenta)); + break; + case 106: // Bright background color cyan + StyleConstants.setBackground(stringStyle, brightColor(java.awt.Color.cyan)); + break; + case 107: // Bright background color white + StyleConstants.setBackground(stringStyle, brightColor(java.awt.Color.white)); + break; + case 109: // Bright background color default + StyleConstants.setBackground(stringStyle, brightColor(DefaultBackgroundColor)); + break; + default: + throw new IllegalStateException("Unexpected code value: " + Integer.parseInt(code)); + } + } + return stringStyle; + } + + /** + * Returns the current virtual cursor position within the given {@link JTextPane} as a {@link Point}. + * The position is represented as (column, row) with 1-based indexing like in consoles. + * + * @param textPane the {@link JTextPane} from which to retrieve the cursor position + * @return a {@link Point} representing the cursor position, where the x-coordinate is the column + * number and the y-coordinate is the row number, both 1-based + */ + private static Point getCursorPosition(JTextPane textPane) { + StyledDocument doc = textPane.getStyledDocument(); + int docLength = doc.getLength(); + // Get the row at the end of the document + Element docRoot = doc.getDefaultRootElement(); + int row = docRoot.getElementIndex(docLength); + + // Get the start offset of the column + Element lineElement = docRoot.getElement(row); + int lineStartOffset = lineElement.getStartOffset(); + int column = docLength - lineStartOffset; + return new Point(column + 1, row + 1); // 1-based indexing for ANSI console + } + + /** + * Sets the virtual cursor position in the specified {@link JTextPane} to the given {@link Point}. + * The {@link Point} specifies the row and column (1-based index). + * + * @param textPane The {@link JTextPane} in which to set the cursor position. + * @param newPosition A {@link Point} where the x-coordinate represents the column + * and the y-coordinate represents the row. Both coordinates are 1-based. + * @throws BadLocationException If the specified position is invalid within the document. + */ + public static void setCursorPosition(JTextPane textPane, Point newPosition) throws BadLocationException { + StyledDocument doc = textPane.getStyledDocument(); + int rowIndex = Math.max(0, newPosition.y - 1); // rowIndex starts from 0 + int columnIndex = Math.max(0, newPosition.x - 1); // columnIndex starts from 0 + + // Get the line start offset + int lineStartOffset = doc.getDefaultRootElement().getElement(rowIndex).getStartOffset(); + + // Calculate the new caret position + int newCaretPosition = lineStartOffset + columnIndex; + logger.trace("lineStartOffset={}", lineStartOffset); + logger.trace("newCaretPosition={}", newCaretPosition); + + if (newCaretPosition < 0) { + logger.trace("newCaretPosition set to 0"); + newCaretPosition = 0; + } + + if (newCaretPosition > doc.getLength()) { + newCaretPosition = doc.getLength(); + logger.trace("newCaretPosition set to {}", newCaretPosition); + } + + int currentLength = doc.getLength(); + // Set the caret position + if (newCaretPosition < currentLength) { + doc.remove(newCaretPosition, currentLength - newCaretPosition); + } + + textPane.setCaretPosition(doc.getLength()); + } + + /** + * Changes the brightness of a given color. + * @param color The original color. + * @param factor Brightness factor (e.g., 1.2 for higher brightness, 0.8 for lower brightness). + * @return A new color with adjusted brightness. + */ + private static Color changeBrightness(Color color, double factor) { + int red = (int) Math.min(255, color.getRed() * factor); + int green = (int) Math.min(255, color.getGreen() * factor); + int blue = (int) Math.min(255, color.getBlue() * factor); + + red = Math.max(0, red); + green = Math.max(0, green); + blue = Math.max(0, blue); + return new Color(red, green, blue); + } + + public static Color brightColor(Color color) { + return changeBrightness(color, brightnessScaling); + } + + public static Color normalColor(Color color) { + return changeBrightness(color, normalScaling); + } + + /** + *

Warning: This method is intended for unit tests only.

+ * It invokes the {@link #colorCodeToStyle} method with the provided color codes. + * + * @param colorCodes an array of color codes to be converted to styles. + */ + public static void testColorCodeToStyle(String [] colorCodes) { + colorCodeToStyle(colorCodes); + } + + /** + *

Warning: This method is intended for unit tests only.

+ * It returns the current {@link #stringStyle}. + * + * @return the current string style. + */ + public static Style testStringStyle() { + return stringStyle; + } +} diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/gui/GuiMain.form b/firmware_updater/updater/src/org/selfbus/updater/gui/GuiMain.form similarity index 87% rename from firmware_updater/updater/source/src/main/java/org/selfbus/updater/gui/GuiMain.form rename to firmware_updater/updater/src/org/selfbus/updater/gui/GuiMain.form index 637361d4..da8e16a3 100644 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/gui/GuiMain.form +++ b/firmware_updater/updater/src/org/selfbus/updater/gui/GuiMain.form @@ -7,6 +7,7 @@ + @@ -16,7 +17,7 @@ - + @@ -52,7 +53,7 @@ - + @@ -60,7 +61,8 @@ - + + @@ -74,7 +76,8 @@ - + + @@ -90,7 +93,7 @@ - + @@ -106,7 +109,8 @@ - + + @@ -114,7 +118,7 @@ - + @@ -122,7 +126,7 @@ - + @@ -138,7 +142,8 @@ - + + @@ -156,7 +161,8 @@ - + + @@ -164,7 +170,8 @@ - + + @@ -173,7 +180,7 @@ - + @@ -189,7 +196,8 @@ - + + @@ -197,7 +205,7 @@ - + @@ -205,7 +213,7 @@ - + @@ -221,7 +229,8 @@ - + + @@ -229,7 +238,7 @@ - + @@ -237,7 +246,7 @@ - + @@ -253,7 +262,8 @@ - + + @@ -261,7 +271,7 @@ - + @@ -269,7 +279,7 @@ - + @@ -277,7 +287,7 @@ - + @@ -285,7 +295,7 @@ - + @@ -293,7 +303,7 @@ - + @@ -301,7 +311,7 @@ - + @@ -309,7 +319,7 @@ - + @@ -318,7 +328,7 @@ - + @@ -326,7 +336,8 @@ - + + @@ -334,7 +345,8 @@ - + + @@ -342,7 +354,8 @@ - + + @@ -366,23 +379,8 @@ - - - - - - - - - - - - - - - - - + + @@ -396,7 +394,8 @@ - + + @@ -404,7 +403,8 @@ - + + @@ -420,7 +420,7 @@ - + @@ -428,7 +428,8 @@ - + + @@ -436,7 +437,7 @@ - + @@ -452,7 +453,8 @@ - + + @@ -468,7 +470,7 @@ - + @@ -476,7 +478,7 @@ - + @@ -538,7 +540,7 @@ - + diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/gui/GuiMain.java b/firmware_updater/updater/src/org/selfbus/updater/gui/GuiMain.java similarity index 62% rename from firmware_updater/updater/source/src/main/java/org/selfbus/updater/gui/GuiMain.java rename to firmware_updater/updater/src/org/selfbus/updater/gui/GuiMain.java index 15314e9b..5baa2afc 100644 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/gui/GuiMain.java +++ b/firmware_updater/updater/src/org/selfbus/updater/gui/GuiMain.java @@ -3,19 +3,20 @@ import com.intellij.uiDesigner.core.GridConstraints; import com.intellij.uiDesigner.core.GridLayoutManager; import com.intellij.uiDesigner.core.Spacer; -import tuwien.auto.calimero.KNXFormatException; -import tuwien.auto.calimero.KNXIllegalArgumentException; +import org.apache.commons.cli.ParseException; +import org.selfbus.updater.*; +import org.selfbus.updater.logging.JTextPaneAppender; +import org.selfbus.updater.logging.LoggingManager; +import tuwien.auto.calimero.*; import tuwien.auto.calimero.knxnetip.Discoverer; import tuwien.auto.calimero.knxnetip.servicetype.SearchResponse; +import tuwien.auto.calimero.link.medium.KNXMediumSettings; +import tuwien.auto.calimero.serial.usb.Device; -import org.apache.commons.cli.ParseException; -import org.selfbus.updater.CliOptions; -import org.selfbus.updater.DiscoverKnxInterfaces; -import org.selfbus.updater.ToolInfo; -import org.selfbus.updater.Updater; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.imageio.ImageIO; import javax.swing.*; import javax.swing.filechooser.FileNameExtensionFilter; import javax.swing.plaf.FontUIResource; @@ -23,17 +24,23 @@ import java.awt.*; import java.awt.event.*; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.lang.reflect.Method; import java.net.InetSocketAddress; +import java.net.UnknownHostException; import java.util.*; import java.util.List; import static java.awt.Font.PLAIN; +import static org.selfbus.updater.logging.Color.*; +import static org.fusesource.jansi.Ansi.ansi; +import static org.selfbus.updater.CliOptions.*; +import static org.selfbus.updater.gui.CliConverter.argument; +import static org.selfbus.updater.gui.ConColorsToStyledDoc.DefaultBackgroundColor; +import static org.selfbus.updater.gui.ConColorsToStyledDoc.DefaultForegroundColor; +import static org.selfbus.updater.logging.LoggingManager.JTEXTPANE_APPENDER_NAME; -@SuppressWarnings("serial") public class GuiMain extends JFrame { private JButton buttonLoadFile; private JTextField textBoxKnxGatewayIpAddr; @@ -57,7 +64,6 @@ public class GuiMain extends JFrame { private JCheckBox natCheckBox; private JTextField textFieldOwnAddress; private JTextField textFieldDelay; - private JTextField textFieldTimeout; private JCheckBox eraseCompleteFlashCheckBox; private JCheckBox noFlashCheckBox; private JLabel labelMedium; @@ -67,7 +73,6 @@ public class GuiMain extends JFrame { private JLabel labelBootloaderDeviceAddr; private JLabel labelOwnAddress; private JLabel labelDelay; - private JLabel labelTimeout; private JLabel labelPortHint; private JLabel labeIIpHint; private JLabel labelFileNameHint; @@ -92,331 +97,361 @@ public class GuiMain extends JFrame { private JScrollPane mainScrollPane; private JButton reloadGatewaysButton; - - private CliOptions cliOptions; private Thread updaterThread; - public static GuiMain guiMainInstance; - private final static Logger logger = LoggerFactory.getLogger(GuiMain.class.getName()); - private static final Properties userProperties = new Properties(); - + private final static Logger logger = LoggerFactory.getLogger(GuiMain.class); + private final GuiSettings guiSettings = new GuiSettings(this); + private static final String FILENAME_SETTINGS = "settings.json"; + private static final String LANGUAGE_RESOURCE_BUNDLE = "language/GuiMain"; - public GuiMain() { - $$$setupUI$$$(); - buttonLoadFile.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - JFileChooser fc = new JFileChooser(); - String filePath = textFieldFileName.getText(); + String getTranslation(String text) { + return $$$getMessageFromBundle$$$(LANGUAGE_RESOURCE_BUNDLE, text); + } - if (filePath != null && !filePath.isEmpty()) { - fc.setCurrentDirectory(new File(filePath).getParentFile()); - } - fc.setFileFilter(new FileNameExtensionFilter("HEX", "hex")); - int result = fc.showOpenDialog(panelMain); + private boolean getUpdaterIsRunning() { + if (updaterThread == null) { + return false; + } + return updaterThread.getState() != Thread.State.TERMINATED; + } - if (result == JFileChooser.APPROVE_OPTION) { - if (fc.getSelectedFile().exists()) { - textFieldFileName.setText(fc.getSelectedFile().toString()); - } - } + private void handleStartStopFlashAction() { + if (getUpdaterIsRunning() && (updaterThread != null)) { + while (updaterThread.getState() != Thread.State.TERMINATED) { + updaterThread.interrupt(); } - }); + updaterThread = null; + updaterFinished(); + return; + } - buttonStartStopFlash.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - - ResourceBundle bundle = ResourceBundle.getBundle("GuiTranslation"); - - if (Objects.equals(buttonStartStopFlash.getText(), bundle.getString("startFlash"))) { - jLoggingPane.setText(""); - - updaterThread = new Thread() { - public void run() { - try { - setCliOptions(); - } catch (KNXFormatException | ParseException ex) { - throw new RuntimeException(ex); - } - - try { - final Updater d = new Updater(cliOptions); - - //final ShutdownHandler sh = new ShutdownHandler().register(); - d.run(); - SwingUtilities.invokeLater(() -> { - guiMainInstance.buttonStartStopFlash.setText(bundle.getString("startFlash")); - }); - //sh.unregister(); - } catch (final Throwable t) { - logger.error("parsing options ", t); - } finally { - logger.info("\n\n"); - logger.debug("main exit"); - } - } - }; - updaterThread.start(); - - buttonStartStopFlash.setText(bundle.getString("stopFlash")); - } else { - updaterThread.interrupt(); //.stop() is deprecated - - buttonStartStopFlash.setText(bundle.getString("startFlash")); - } + jLoggingPane.setText(""); + updaterThread = new Thread(() -> { + try { + Thread.currentThread().setName("guiUpdater"); + CliOptions cliOptions = getCliOptions(); + displayCommandLine(cliOptions); + final Updater updater = new Updater(cliOptions); + updater.run(); + } catch (KNXFormatException | ParseException e) { + logger.error("", e); // todo see logback issue https://github.com/qos-ch/logback/issues/876 + String message = String.format(getTranslation("Exception.handleStartStopFlashAction.Message"), + e.getMessage()); + JOptionPane.showMessageDialog(this, + message, getTranslation("Error"), JOptionPane.ERROR_MESSAGE); + } finally { + SwingUtilities.invokeLater(this::updaterFinished); } - }); - buttonRequestUid.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - - updaterThread = new Thread(() -> { - try { - // für den bestehenden Updater wird zwingend ein Dateiname benötigt - if (textFieldFileName.getText().isEmpty()) { - textFieldFileName.setText("dummy.hex"); - } - setCliOptions(); - if (Objects.equals(textFieldFileName.getText(), "dummy.hex")) { - textFieldFileName.setText(""); - } - } catch (KNXFormatException | ParseException ex) { - throw new RuntimeException(ex); - } - - try { - final Updater upd = new Updater(cliOptions); - - String uid = upd.requestUid(); - - SwingUtilities.invokeLater(() -> { - guiMainInstance.textFieldUid.setText(uid); - }); - - } catch (final Throwable t) { - logger.error("parsing options ", t); - } finally { - logger.info("\n\n"); - logger.debug("main exit"); - } - }); - updaterThread.start(); - } }); + updaterThread.start(); + buttonStartStopFlash.setText(getTranslation("stopFlash")); + } - comboBoxScenario.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - setGuiElementsVisibility(); + private void updaterFinished() { + buttonStartStopFlash.setText(getTranslation("startFlash")); + } + + private void handleLoadFileAction() { + JFileChooser fc = new JFileChooser(); + String fileName = textFieldFileName.getText(); + + File initialDirectory = null; + if (fileName != null && !fileName.isEmpty()) { + // try to set to the directory of the last .hex firmware + initialDirectory = new File(fileName).getParentFile(); + if (!initialDirectory.exists() || !initialDirectory.isDirectory()) { + initialDirectory = null; } - }); + } - advancedSettingsCheckBox.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - setGuiElementsVisibility(); + if (initialDirectory == null) { + // set to current working directory + String currentDirectory = System.getProperty("user.dir"); + initialDirectory = new File(currentDirectory); + } + + fc.setCurrentDirectory(initialDirectory); + + fc.setFileFilter(new FileNameExtensionFilter(getTranslation("loadFile.firmwareFilterDescription"), "hex")); + int result = fc.showOpenDialog(panelMain); + + if (result == JFileChooser.APPROVE_OPTION) { + if (fc.getSelectedFile().exists()) { + textFieldFileName.setText(fc.getSelectedFile().toString()); } - }); - reloadGatewaysButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - new Thread(() -> LoadKnxIpInterfacesAndFillComboBox()).start(); + } + } + + private void handleRequestUidAction() { + jLoggingPane.setText(""); + updaterThread = new Thread(() -> { + Thread.currentThread().setName("uidRequest"); + String uid = ""; + try { + CliOptions cliOptions = getCliOptions(); + cliOptions.setFileName(""); + cliOptions.setUid(""); + displayCommandLine(cliOptions); + final Updater upd = new Updater(cliOptions); + SwingUtilities.invokeLater(this::beforeUidRequest); + uid = upd.requestUid(); + } catch (ParseException | UpdaterException | KNXException | UnknownHostException e) { + logger.error("", e); // todo see logback issue https://github.com/qos-ch/logback/issues/876 + JOptionPane.showMessageDialog(this, + String.format(getTranslation("Exception.requestUidAction.Message"), e), + getTranslation("Error"), JOptionPane.ERROR_MESSAGE); + } finally { + String finalUid = uid; + SwingUtilities.invokeLater(() -> onRequestUidFinished(finalUid)); } }); + updaterThread.start(); + } + + private void setUIDComponentsEnabled(boolean enabled) { + comboBoxScenario.setEnabled(enabled); + buttonRequestUid.setEnabled(enabled); + buttonStartStopFlash.setEnabled(enabled); + textFieldUid.setEnabled(enabled); + } + + private void beforeUidRequest() { + textFieldUid.setText(""); + setUIDComponentsEnabled(false); + } + + private void onRequestUidFinished(String uid) { + textFieldUid.setText(uid); + setUIDComponentsEnabled(true); + } + private void handleReloadGatewaysAction() { + new Thread(this::loadKnxIpInterfacesAndFillComboBox).start(); + } + + public GuiMain() { + //todo load language from FILENAME_SETTINGS and set it here. + // Has to be done before calling $$$setupUI$$$(); + //Locale.setDefault(Locale.ENGLISH); // for tests + //Locale.setDefault(Locale.ROOT); // default language for tests + $$$setupUI$$$(); + + // Set uncaught exception handler to show a dialog + Thread.setDefaultUncaughtExceptionHandler(new GuiUncaughtExceptionHandler(this)); + + buttonLoadFile.addActionListener(actionEvent -> handleLoadFileAction()); + buttonStartStopFlash.addActionListener(actionEvent -> handleStartStopFlashAction()); + buttonRequestUid.addActionListener(actionEvent -> handleRequestUidAction()); + comboBoxScenario.addActionListener(actionEvent -> setGuiElementsVisibility()); + advancedSettingsCheckBox.addActionListener(actionEvent -> setGuiElementsVisibility()); + reloadGatewaysButton.addActionListener(actionEvent -> handleReloadGatewaysAction()); comboBoxIpGateways.addActionListener(comboBoxIpGatewaysActionListener); + startUpdaterGui(); } // externe Definition des ActionListeners, weil dieser vor der Füllung der ComboBox gelöscht und anschließend // wieder eingefügt werden muss - public ActionListener comboBoxIpGatewaysActionListener = new ActionListener() { + public final ActionListener comboBoxIpGatewaysActionListener = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (comboBoxIpGateways.getSelectedItem() == null) return; - if (((CalimeroSearchComboItem) comboBoxIpGateways.getSelectedItem()).value == null) return; - InetSocketAddress addr = ((CalimeroSearchComboItem) comboBoxIpGateways.getSelectedItem()).value.remoteEndpoint(); + Discoverer.Result value = ((CalimeroSearchComboItem) comboBoxIpGateways.getSelectedItem()).value; + if (value == null) return; + InetSocketAddress addr = value.remoteEndpoint(); textBoxKnxGatewayIpAddr.setText(addr.getAddress().getHostAddress()); textFieldPort.setText(String.valueOf(addr.getPort())); } }; - private void saveAllParameters() { - userProperties.setProperty("AdvancedSettings", advancedSettingsCheckBox.isSelected() ? "true" : "false"); - userProperties.setProperty("GatewayIpAddr", textBoxKnxGatewayIpAddr.getText()); - userProperties.setProperty("GatewayPort", textFieldPort.getText()); - userProperties.setProperty("UseNat", natCheckBox.isSelected() ? "true" : "false"); - userProperties.setProperty("TpUart", textFieldTpuart.getText()); - userProperties.setProperty("Serial", textFieldSerial.getText()); - userProperties.setProperty("Medium", Objects.requireNonNull(comboBoxMedium.getSelectedItem()).toString()); - userProperties.setProperty("Scenario", String.valueOf(((ComboItem) Objects.requireNonNull(comboBoxScenario.getSelectedItem())).getValue())); - userProperties.setProperty("FlashFilePath", textFieldFileName.getText()); - userProperties.setProperty("Uid", textFieldUid.getText()); - userProperties.setProperty("DiffFlash", CheckBoxDiffFlash.isSelected() ? "true" : "false"); - userProperties.setProperty("EraseCompleteFlash", eraseCompleteFlashCheckBox.isSelected() ? "true" : "false"); - userProperties.setProperty("NoFlash", noFlashCheckBox.isSelected() ? "true" : "false"); - userProperties.setProperty("BootloaderDeviceAddress", textFieldBootloaderDeviceAddress.getText()); - userProperties.setProperty("DeviceAddress", textFieldDeviceAddress.getText()); - userProperties.setProperty("OwnAddress", textFieldOwnAddress.getText()); - userProperties.setProperty("DelayMs", textFieldDelay.getText()); - userProperties.setProperty("Timeout", textFieldTimeout.getText()); - userProperties.setProperty("TelegramPriority", Objects.requireNonNull(comboBoxKnxTelegramPriority.getSelectedItem()).toString()); - userProperties.setProperty("SecureUser", textFieldKnxSecureUser.getText()); - userProperties.setProperty("SecureUserPassword", textFieldKnxSecureUserPwd.getText()); - userProperties.setProperty("SecureDevicePassword", textFieldKnxSecureDevicePwd.getText()); - userProperties.setProperty("WindowSizeHeight", String.valueOf(this.getSize().height)); - userProperties.setProperty("WindowSizeWidth", String.valueOf(this.getSize().width)); + private void setComponentNames() { + setName("MainFrame"); + comboBoxIpGateways.setName("comboBoxIpGateways"); + advancedSettingsCheckBox.setName("AdvancedSettings"); + textBoxKnxGatewayIpAddr.setName("GatewayIpAddr"); + textFieldPort.setName("GatewayPort"); + natCheckBox.setName("UseNat"); + textFieldTpuart.setName("TpUart"); + textFieldSerial.setName("Serial"); + comboBoxMedium.setName("Medium"); + comboBoxScenario.setName("Scenario"); + textFieldFileName.setName("FlashFilePath"); + textFieldUid.setName("Uid"); + CheckBoxDiffFlash.setName("DiffFlash"); + eraseCompleteFlashCheckBox.setName("EraseCompleteFlash"); + noFlashCheckBox.setName("NoFlash"); + textFieldBootloaderDeviceAddress.setName("BootloaderDeviceAddress"); + textFieldDeviceAddress.setName("DeviceAddress"); + textFieldOwnAddress.setName("OwnAddress"); + textFieldDelay.setName("DelayMs"); + comboBoxKnxTelegramPriority.setName("TelegramPriority"); + textFieldKnxSecureUser.setName("SecureUser"); + textFieldKnxSecureUserPwd.setName("SecureUserPassword"); + textFieldKnxSecureDevicePwd.setName("SecureDevicePassword"); + } + @SuppressWarnings("SameParameterValue") + private void saveAllParameters(String fileName) { try { - userProperties.storeToXML(new FileOutputStream("settings.xml"), ""); - } catch (IOException ex) { - throw new RuntimeException(ex); + guiSettings.writeComponentSettings(fileName); + } catch (IOException e) { + logger.error("", e); // todo see logback issue https://github.com/qos-ch/logback/issues/876 + JOptionPane.showMessageDialog(this, + String.format(getTranslation("IOException.savingSettings.Message"), fileName, e.getMessage()), + this.getTranslation("Warning"), JOptionPane.WARNING_MESSAGE); } } - private void loadAllParameters() { + @SuppressWarnings("SameParameterValue") + private boolean loadAllParameters(String fileName) { + try { + setComponentNames(); + guiSettings.readComponentsSettings(fileName); + } catch (IOException e) { + logger.error("{}:{}", e.getClass().getSimpleName(), e.getMessage()); + JOptionPane.showMessageDialog(this, + String.format(getTranslation("IOException.loadingSettings.Message"), fileName, e.getMessage()), + this.getTranslation("Warning"), JOptionPane.WARNING_MESSAGE); + return false; + } + setGuiElementsVisibility(); + return true; + } - if (new File("settings.xml").exists()) { - try { - userProperties.loadFromXML(new FileInputStream("settings.xml")); - - advancedSettingsCheckBox.setSelected(Boolean.parseBoolean(userProperties.getProperty("AdvancedSettings"))); - textBoxKnxGatewayIpAddr.setText(userProperties.getProperty("GatewayIpAddr")); - textFieldPort.setText(userProperties.getProperty("GatewayPort")); - natCheckBox.setSelected(Boolean.parseBoolean(userProperties.getProperty("UseNat"))); - textFieldTpuart.setText(userProperties.getProperty("TpUart")); - textFieldSerial.setText(userProperties.getProperty("Serial")); - comboBoxMedium.setSelectedItem(userProperties.getProperty("Medium")); - textFieldFileName.setText(userProperties.getProperty("FlashFilePath")); - textFieldUid.setText(userProperties.getProperty("Uid")); - CheckBoxDiffFlash.setSelected(Boolean.parseBoolean(userProperties.getProperty("DiffFlash"))); - eraseCompleteFlashCheckBox.setSelected(Boolean.parseBoolean(userProperties.getProperty("EraseCompleteFlash"))); - noFlashCheckBox.setSelected(Boolean.parseBoolean(userProperties.getProperty("NoFlash"))); - textFieldBootloaderDeviceAddress.setText(userProperties.getProperty("BootloaderDeviceAddress")); - textFieldDeviceAddress.setText(userProperties.getProperty("DeviceAddress")); - textFieldOwnAddress.setText(userProperties.getProperty("OwnAddress")); - textFieldDelay.setText(userProperties.getProperty("DelayMs")); - textFieldTimeout.setText(userProperties.getProperty("Timeout")); - comboBoxKnxTelegramPriority.setSelectedItem(userProperties.getProperty("TelegramPriority")); - textFieldKnxSecureUser.setText(userProperties.getProperty("SecureUser")); - textFieldKnxSecureUserPwd.setText(userProperties.getProperty("SecureUserPassword")); - textFieldKnxSecureDevicePwd.setText(userProperties.getProperty("SecureDevicePassword")); - - this.setSize(Integer.parseInt(userProperties.getProperty("WindowSizeWidth")), Integer.parseInt(userProperties.getProperty("WindowSizeHeight"))); - - for (int i = 0; i < comboBoxScenario.getItemCount(); i++) { - if (Objects.equals(String.valueOf(comboBoxScenario.getItemAt(i).value), userProperties.getProperty("Scenario"))) { - comboBoxScenario.setSelectedItem(comboBoxScenario.getItemAt(i)); - break; - } - } + private CliOptions getCliOptions() throws KNXFormatException, ParseException { + ArrayList argsList = new ArrayList<>(); - setGuiElementsVisibility(); + argsList.add(argument("", textBoxKnxGatewayIpAddr)); + argsList.add(argument(OPT_LONG_FILENAME, textFieldFileName)); + //argsList.add(argument(OPT_LONG_LOCALHOST, textField*)); // todo add gui textfield --localhost + //argsList.add(argument(OPT_LONG_LOCALPORT, textField*)); // todo add gui textfield --localport + argsList.add(argument(OPT_LONG_PORT, textFieldPort)); - } catch (IOException ex) { - throw new RuntimeException(ex); - } - } - } + argsList.add(argument(OPT_LONG_FT12, textFieldSerial)); + argsList.add(argument(OPT_LONG_TPUART, textFieldTpuart)); + //argsList.add(argument(OPT_LONG_USB, textField*)); // todo add gui textfield --usb - private String cliParameterToString(ArrayList list) { - StringBuilder result = new StringBuilder(); - for (String s : list) { - if (!s.isEmpty()) { - result.append(" ").append(s); - } - } - return result.toString().trim(); - } + argsList.add(argument(OPT_LONG_MEDIUM, comboBoxMedium)); - private void setCliOptions() throws KNXFormatException, ParseException { - ArrayList argsList = new ArrayList<>(); + argsList.add(argument(OPT_LONG_USER_ID, textFieldKnxSecureUser)); + argsList.add(argument(OPT_LONG_USER_PASSWORD, textFieldKnxSecureUserPwd)); + argsList.add(argument(OPT_LONG_DEVICE_PASSWORD, textFieldKnxSecureDevicePwd)); - argsList.add(textBoxKnxGatewayIpAddr.getText()); - - argsList.add("-f" + textFieldFileName.getText()); - if (textFieldPort.isVisible() && !Objects.equals(textFieldPort.getText(), "")) - argsList.add("-p" + textFieldPort.getText()); - if (textFieldUid.isVisible() && !Objects.equals(textFieldUid.getText(), "")) - argsList.add("-u" + textFieldUid.getText()); - if (comboBoxMedium.isVisible()) - argsList.add("-m" + comboBoxMedium.getSelectedItem()); - if (textFieldSerial.isVisible() && !Objects.equals(textFieldSerial.getText(), "")) - argsList.add("-s" + textFieldSerial.getText()); - if (textFieldTpuart.isVisible() && !Objects.equals(textFieldTpuart.getText(), "")) - argsList.add("-t" + textFieldTpuart.getText()); - if (textFieldDeviceAddress.isVisible() && !Objects.equals(textFieldDeviceAddress.getText(), "")) - argsList.add("-d" + textFieldDeviceAddress.getText()); - if (textFieldBootloaderDeviceAddress.isVisible() && !Objects.equals(textFieldBootloaderDeviceAddress.getText(), "")) - argsList.add("-D" + textFieldBootloaderDeviceAddress.getText()); - if (textFieldOwnAddress.isVisible() && !Objects.equals(textFieldOwnAddress.getText(), "")) - argsList.add("-o" + textFieldOwnAddress.getText()); - if ((CheckBoxDiffFlash.isVisible() && !CheckBoxDiffFlash.isSelected()) || !CheckBoxDiffFlash.isVisible()) - argsList.add("-f1"); + argsList.add(argument(OPT_LONG_PROG_DEVICE, textFieldBootloaderDeviceAddress)); + + if (textFieldDeviceAddress.isVisible()) + argsList.add(argument(OPT_LONG_DEVICE, textFieldDeviceAddress)); + + if (textFieldOwnAddress.isVisible()) + argsList.add(argument(OPT_LONG_OWN_ADDRESS, textFieldOwnAddress)); + + if (textFieldUid.isVisible()) + argsList.add(argument(OPT_LONG_UID, textFieldUid)); + + argsList.add(argument(OPT_LONG_DELAY, textFieldDelay)); +/* + if (checkBox*.isVisible() && checkBox*.isSelected()) // todo add gui checkbox --tunnelingv2 + argsList.add(argument(OPT_LONG_TUNNEL_V2)); + if (checkBox*.isVisible() && checkBox*.isSelected()) // todo add gui checkbox --tunneling + argsList.add(argument(OPT_LONG_TUNNEL_V1)); +*/ if (natCheckBox.isVisible() && natCheckBox.isSelected()) - argsList.add("-n"); - if (textFieldDelay.isVisible() && !Objects.equals(textFieldDelay.getText(), "")) - argsList.add("--delay" + textFieldDelay.getText()); - if (textFieldTimeout.isVisible() && !Objects.equals(textFieldTimeout.getText(), "")) - argsList.add("--timeout" + textFieldTimeout.getText()); + argsList.add(argument(OPT_LONG_NAT)); + + // todo add gui OPT_LONG_ROUTING (--routing) + + if ((CheckBoxDiffFlash.isVisible() && !CheckBoxDiffFlash.isSelected()) || !CheckBoxDiffFlash.isVisible()) + argsList.add(argument(OPT_LONG_FULL)); + + if (noFlashCheckBox.isVisible() && noFlashCheckBox.isSelected()) + argsList.add(argument(OPT_LONG_NO_FLASH)); + + // argsList.add(argument(OPT_LONG_LOGLEVEL, comboBox*)); // todo add gui combobox --logLevel + + argsList.add(argument(OPT_LONG_PRIORITY, comboBoxKnxTelegramPriority)); + if (eraseCompleteFlashCheckBox.isVisible() && eraseCompleteFlashCheckBox.isSelected()) - argsList.add("--ERASEFLASH"); - if (noFlashCheckBox.isVisible() && noFlashCheckBox.isSelected()) argsList.add("-f0"); + argsList.add(argument(OPT_LONG_ERASEFLASH)); + + // argsList.add(argument(OPT_LONG_DUMPFLASH, textField*)); // todo add gui textfield --DUMPFLASH startaddress endaddress +/* + if (checkBox*.isVisible() && checkBox*.isSelected()) // todo add gui checkbox --statistic + argsList.add(argument(OPT_LONG_LOGSTATISTIC)); +*/ + // argsList.add(argument(OPT_LONG_BLOCKSIZE, comboBox*)); // todo add gui combobox --blocksize 256, 512, 1024 + // argsList.add(argument(OPT_LONG_RECONNECT, textFieldReconnect)); // todo add gui textfield --reconnect 100-12500 + // todo add gui button for OPT_LONG_DISCOVER --discover + // argsList.add(argument(OPT_LONG_RECONNECT_SEQ_NUMBER, textFieldSeqNmbReconn)); todo add textfield --ip-tunnel-reconnect 100-247 + + return new CliOptions(argsList); + } - String updaterFileName = String.format("SB_updater-%s-all.jar", ToolInfo.getVersion()); - String[] args = new String[argsList.size()]; - args = argsList.toArray(args); - logger.info("java -jar {} {}", updaterFileName, cliParameterToString(argsList)); - try { - // read in user-supplied command line options - this.cliOptions = new CliOptions(args, updaterFileName, - "Selfbus KNX-Firmware update tool options", "", Updater.PHYS_ADDRESS_BOOTLOADER, Updater.PHYS_ADDRESS_OWN); - } catch (final KNXIllegalArgumentException | KNXFormatException | ParseException e) { - throw e; - } catch (final RuntimeException e) { - throw new KNXIllegalArgumentException(e.getMessage(), e); - } + private void displayCommandLine(CliOptions cliOptions) { + logger.info("{}java -jar {} {}{}", ansi().fgBright(INFO), ToolInfo.getToolJarName(), + cliOptions.reconstructCommandLine(), ansi().reset()); } public static void startSwingGui() { - SwingUtilities.invokeLater(() -> { - guiMainInstance = new GuiMain(); - guiMainInstance.startUpdaterGui(); - }); + new GuiMain(); } - public void startUpdaterGui() { - this.setContentPane(this.panelMain); - this.setTitle(ToolInfo.getToolAndVersion()); - this.setSize(1000, 800); - this.jLoggingPane.setFont(new Font("Courier New", PLAIN, 12)); - this.jLoggingPane.setBackground(Color.black); - this.jLoggingPane.setForeground(Color.white); - this.setVisible(true); - this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - - new Thread(this::LoadKnxIpInterfacesAndFillComboBox).start(); - - TextAppender.addLog4j2TextPaneAppender(this.jLoggingPane); - - InitGuiElementsVisibility(); + private void startUpdaterGui() { + setContentPane(panelMain); + setTitle(ToolInfo.getToolAndVersion()); + jLoggingPane.setFont(new Font(Font.MONOSPACED, PLAIN, 12)); + jLoggingPane.setBackground(DefaultBackgroundColor); + jLoggingPane.setForeground(DefaultForegroundColor); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + JTextPaneAppender textPaneAppender = (JTextPaneAppender) LoggingManager.getAppender(JTEXTPANE_APPENDER_NAME); + if (textPaneAppender != null) { + textPaneAppender.setTextPane(this.jLoggingPane); + } + initGuiElementsVisibility(); fillScenarios(); fillMediumComboBox(); fillTelegramPriorityComboBox(); - loadAllParameters(); + setMinimumSize(new Dimension(800, 600)); + if (!loadAllParameters(FILENAME_SETTINGS)) { + setLocationRelativeTo(null); + } + int windowExtendedState = getExtendedState(); + if ((windowExtendedState & Frame.ICONIFIED) == Frame.ICONIFIED) { + windowExtendedState &= ~Frame.ICONIFIED; + } + setExtendedState(windowExtendedState); mainScrollPane.getVerticalScrollBar().setUnitIncrement(10); this.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { - saveAllParameters(); + saveAllParameters(FILENAME_SETTINGS); } }); + setVisible(true); + if (!isGatewaySet()) { + new Thread(this::loadKnxIpInterfacesAndFillComboBox).start(); + } } - private void LoadKnxIpInterfacesAndFillComboBox() { + private boolean isGatewaySet() { + if (!textBoxKnxGatewayIpAddr.getText().isEmpty()) { + return true; + } + if (!textFieldSerial.getText().isEmpty()) { + return true; + } + + //todo implement USB check ala + /* + if (!textFieldUSB.getText().isEmpty()) { + return true; + } + */ + return !textFieldTpuart.getText().isEmpty(); + } + + private void loadKnxIpInterfacesAndFillComboBox() { reloadGatewaysButton.setEnabled(false); // ActionListener löschen und nach dem Füllen der ComboBox wieder hinzufügen // damit das Füllen kein Event auslöst @@ -424,12 +459,49 @@ private void LoadKnxIpInterfacesAndFillComboBox() { comboBoxIpGateways.removeAllItems(); - ResourceBundle bundle = ResourceBundle.getBundle("GuiTranslation"); - comboBoxIpGateways.addItem(new CalimeroSearchComboItem(bundle.getString("selectInterface"), null)); + comboBoxIpGateways.addItem(new CalimeroSearchComboItem(getTranslation("selectInterface"), null)); - DiscoverKnxInterfaces.getAllInterfaces().forEach(r -> - comboBoxIpGateways.addItem(new CalimeroSearchComboItem(r.getResponse().getDevice().getName() + - " (" + r.getResponse().getControlEndpoint().getAddress().getHostAddress() + ")", r))); + List> result; + try { + result = DiscoverKnxInterfaces.getAllnetIPInterfaces(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); // Restore interrupt status + logger.info("Interrupted while discovering IP interfaces", e); // todo see logback issue https://github.com/qos-ch/logback/issues/876 + return; + } + + for (Discoverer.Result r : result) { + SearchResponse sr = r.response(); + String ifName = sr.getDevice().getName(); + String ifHostAddress = sr.getControlEndpoint().endpoint().getAddress().getHostAddress(); + IndividualAddress ifPhysAddress = sr.getDevice().getAddress(); + int ifPort = sr.getControlEndpoint().endpoint().getPort(); + int ifTunnelVersion; + if (sr.v2()) + ifTunnelVersion = 2; + else + ifTunnelVersion = 1; + + String ipInterfaceText = String.format("%s (%s:%d) v%d %s", ifName, ifHostAddress, ifPort, ifTunnelVersion, + ifPhysAddress.toString()); + logger.debug("Found IP interface: {}", ipInterfaceText); + CalimeroSearchComboItem comboBoxItem = new CalimeroSearchComboItem(ipInterfaceText, r); + comboBoxIpGateways.addItem(comboBoxItem); + } + + Set usbInterfaces; + try { + usbInterfaces = DiscoverKnxInterfaces.getUsbInterfaces(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); // Restore interrupt status + logger.info("Interrupted while discovering USB interfaces", e); // todo see logback issue https://github.com/qos-ch/logback/issues/876 + return; + } + for (final var d : usbInterfaces) { + final String vp = String.format("%04x:%04x", d.vendorId(), d.productId()); + logger.info("Found USB Interface: {} {} S/N {} VID/PID {}", + d.manufacturer(), d.product(), d.serialNumber(), vp); + } comboBoxIpGateways.addActionListener(comboBoxIpGatewaysActionListener); reloadGatewaysButton.setEnabled(true); @@ -437,39 +509,64 @@ private void LoadKnxIpInterfacesAndFillComboBox() { private void fillScenarios() { if (comboBoxScenario == null) return; - - ResourceBundle bundle = ResourceBundle.getBundle("GuiTranslation"); - - comboBoxScenario.addItem(new ComboItem(bundle.getString(GuiObjsVisOpts.NEWDEV.getVisOption()), GuiObjsVisOpts.NEWDEV)); - comboBoxScenario.addItem(new ComboItem(bundle.getString(GuiObjsVisOpts.APPDEV.getVisOption()), GuiObjsVisOpts.APPDEV)); - comboBoxScenario.addItem(new ComboItem(bundle.getString(GuiObjsVisOpts.REQUID.getVisOption()), GuiObjsVisOpts.REQUID)); + comboBoxScenario.addItem(new ComboItem(getTranslation(GuiObjsVisOpts.NEWDEV.getVisOption()), GuiObjsVisOpts.NEWDEV)); + comboBoxScenario.addItem(new ComboItem(getTranslation(GuiObjsVisOpts.APPDEV.getVisOption()), GuiObjsVisOpts.APPDEV)); + comboBoxScenario.addItem(new ComboItem(getTranslation(GuiObjsVisOpts.REQUID.getVisOption()), GuiObjsVisOpts.REQUID)); } private void fillMediumComboBox() { - comboBoxMedium.addItem("tp1"); - comboBoxMedium.addItem("rf"); + comboBoxMedium.addItem(KNXMediumSettings.getMediumString(KNXMediumSettings.MEDIUM_TP1)); + comboBoxMedium.addItem(KNXMediumSettings.getMediumString(KNXMediumSettings.MEDIUM_RF)); } private void fillTelegramPriorityComboBox() { if (comboBoxKnxTelegramPriority == null) return; - comboBoxKnxTelegramPriority.addItem("LOW"); - comboBoxKnxTelegramPriority.addItem("URGENT"); - comboBoxKnxTelegramPriority.addItem("NORMAL"); - comboBoxKnxTelegramPriority.addItem("SYSTEM"); + comboBoxKnxTelegramPriority.addItem(Priority.LOW.toString()); + comboBoxKnxTelegramPriority.addItem(Priority.NORMAL.toString()); + comboBoxKnxTelegramPriority.addItem(Priority.URGENT.toString()); + comboBoxKnxTelegramPriority.addItem(Priority.SYSTEM.toString()); + } + + private void setFrameImages() { + String resourceTemplate = "/frame_images/selfbus_logo_%sx%s.png"; + String[] resolutions = {"16", "32", "60", "72", "96", "256"}; + List frameImageList = new ArrayList<>(); + + for (String resolution : resolutions) { + String resourceName = String.format(resourceTemplate, resolution, resolution); + InputStream imageStream = this.getClass().getResourceAsStream(resourceName); + if (imageStream == null) { + logger.warn("getResourceAsStream({}) failed", resourceName); + continue; + } + + try { + Image image = ImageIO.read(imageStream); + frameImageList.add(image); + logger.debug("Added {} to frameImageList", resourceName); + } catch (IOException e) { + logger.warn("Could not add {} to frameImageList {}", resourceName, Arrays.toString(e.getStackTrace())); + } + } + + if (!frameImageList.isEmpty()) { + this.setIconImages(frameImageList); + } } private void createUIComponents() { // TODO: place custom component creation code here - comboBoxIpGateways = new JComboBox(); - comboBoxMedium = new JComboBox(); - comboBoxScenario = new JComboBox(); - comboBoxKnxTelegramPriority = new JComboBox(); + comboBoxIpGateways = new JComboBox<>(); + comboBoxMedium = new JComboBox<>(); + comboBoxScenario = new JComboBox<>(); + comboBoxKnxTelegramPriority = new JComboBox<>(); + setFrameImages(); } public static class ComboItem { private final String key; - Object value; + final Object value; public ComboItem(String key, Object value) { this.key = key; @@ -492,7 +589,7 @@ public Object getValue() { public static class CalimeroSearchComboItem { private final String key; - Discoverer.Result value; + final Discoverer.Result value; public CalimeroSearchComboItem(String key, Discoverer.Result value) { this.key = key; @@ -533,7 +630,7 @@ public String getVisOption() { private static final Map> GuiObjectsMap = new HashMap<>(); - private void InitGuiElementsVisibility() { + private void initGuiElementsVisibility() { // hier wird definiert, in welchem Zustand der GUI welche Elemente sichtbar sein sollen GuiObjectsMap.put(buttonLoadFile, Arrays.asList(GuiObjsVisOpts.NEWDEV, GuiObjsVisOpts.APPDEV)); @@ -541,7 +638,7 @@ private void InitGuiElementsVisibility() { GuiObjectsMap.put(labelFileName, Arrays.asList(GuiObjsVisOpts.NEWDEV, GuiObjsVisOpts.APPDEV)); GuiObjectsMap.put(labelFileNameHint, Arrays.asList(GuiObjsVisOpts.NEWDEV, GuiObjsVisOpts.APPDEV)); - GuiObjectsMap.put(buttonRequestUid, Arrays.asList(GuiObjsVisOpts.REQUID)); + GuiObjectsMap.put(buttonRequestUid, List.of(GuiObjsVisOpts.REQUID)); GuiObjectsMap.put(labelUid, Arrays.asList(GuiObjsVisOpts.APPDEV, GuiObjsVisOpts.REQUID)); GuiObjectsMap.put(textFieldUid, Arrays.asList(GuiObjsVisOpts.APPDEV, GuiObjsVisOpts.REQUID)); @@ -556,8 +653,8 @@ private void InitGuiElementsVisibility() { GuiObjectsMap.put(labelTpuart, Arrays.asList(GuiObjsVisOpts.NEWDEV, GuiObjsVisOpts.APPDEV, GuiObjsVisOpts.REQUID, GuiObjsVisOpts.ADVSET)); GuiObjectsMap.put(textFieldTpuart, Arrays.asList(GuiObjsVisOpts.NEWDEV, GuiObjsVisOpts.APPDEV, GuiObjsVisOpts.REQUID, GuiObjsVisOpts.ADVSET)); - GuiObjectsMap.put(labelDeviceAddress, Arrays.asList(GuiObjsVisOpts.APPDEV)); - GuiObjectsMap.put(textFieldDeviceAddress, Arrays.asList(GuiObjsVisOpts.APPDEV)); + GuiObjectsMap.put(labelDeviceAddress, List.of(GuiObjsVisOpts.APPDEV, GuiObjsVisOpts.REQUID)); + GuiObjectsMap.put(textFieldDeviceAddress, List.of(GuiObjsVisOpts.APPDEV, GuiObjsVisOpts.REQUID)); GuiObjectsMap.put(labelBootloaderDeviceAddr, Arrays.asList(GuiObjsVisOpts.NEWDEV, GuiObjsVisOpts.APPDEV, GuiObjsVisOpts.REQUID, GuiObjsVisOpts.ADVSET)); GuiObjectsMap.put(textFieldBootloaderDeviceAddress, Arrays.asList(GuiObjsVisOpts.NEWDEV, GuiObjsVisOpts.APPDEV, GuiObjsVisOpts.REQUID, GuiObjsVisOpts.ADVSET)); @@ -568,9 +665,6 @@ private void InitGuiElementsVisibility() { GuiObjectsMap.put(labelDelay, Arrays.asList(GuiObjsVisOpts.NEWDEV, GuiObjsVisOpts.APPDEV, GuiObjsVisOpts.ADVSET)); GuiObjectsMap.put(textFieldDelay, Arrays.asList(GuiObjsVisOpts.NEWDEV, GuiObjsVisOpts.APPDEV, GuiObjsVisOpts.ADVSET)); - GuiObjectsMap.put(labelTimeout, Arrays.asList(GuiObjsVisOpts.NEWDEV, GuiObjsVisOpts.APPDEV, GuiObjsVisOpts.ADVSET)); - GuiObjectsMap.put(textFieldTimeout, Arrays.asList(GuiObjsVisOpts.NEWDEV, GuiObjsVisOpts.APPDEV, GuiObjsVisOpts.ADVSET)); - GuiObjectsMap.put(CheckBoxDiffFlash, Arrays.asList(GuiObjsVisOpts.NEWDEV, GuiObjsVisOpts.APPDEV, GuiObjsVisOpts.ADVSET)); GuiObjectsMap.put(labelFullFlashHint, Arrays.asList(GuiObjsVisOpts.NEWDEV, GuiObjsVisOpts.APPDEV, GuiObjsVisOpts.ADVSET)); @@ -601,35 +695,27 @@ private void InitGuiElementsVisibility() { } private void setGuiElementsVisibility() { - ResourceBundle bundle = ResourceBundle.getBundle("GuiTranslation"); final GuiObjsVisOpts selectedScenario = (GuiObjsVisOpts) ((ComboItem) Objects.requireNonNull(comboBoxScenario.getSelectedItem())).getValue(); switch (selectedScenario) { case NEWDEV: - labelScenarioHint.setText(bundle.getString("newDeviceHint")); + labelScenarioHint.setText(getTranslation("newDeviceHint")); break; case APPDEV: - labelScenarioHint.setText(bundle.getString("appDeviceHint")); + labelScenarioHint.setText(getTranslation("appDeviceHint")); break; case REQUID: - labelScenarioHint.setText(bundle.getString("requestUidHint")); + labelScenarioHint.setText(getTranslation("requestUidHint")); break; } for (var guiObjectEntry : GuiObjectsMap.entrySet()) { - if (guiObjectEntry.getValue().contains(selectedScenario) && guiObjectEntry.getValue().contains(GuiObjsVisOpts.ADVSET)) { - if (advancedSettingsCheckBox.isSelected()) { - ((JComponent) guiObjectEntry.getKey()).setVisible(true); - } else { - ((JComponent) guiObjectEntry.getKey()).setVisible(false); - } - - } else if (guiObjectEntry.getValue().contains(selectedScenario)) { - ((JComponent) guiObjectEntry.getKey()).setVisible(true); + boolean isInScenario = guiObjectEntry.getValue().contains(selectedScenario); + if (isInScenario && guiObjectEntry.getValue().contains(GuiObjsVisOpts.ADVSET)) { + ((JComponent) guiObjectEntry.getKey()).setVisible(advancedSettingsCheckBox.isSelected()); } else { - - ((JComponent) guiObjectEntry.getKey()).setVisible(false); + ((JComponent) guiObjectEntry.getKey()).setVisible(isInScenario); } } } @@ -647,9 +733,10 @@ private void setGuiElementsVisibility() { panelMain = new JPanel(); panelMain.setLayout(new GridLayoutManager(2, 1, new Insets(0, 0, 0, 0), -1, -1)); panelMain.setAutoscrolls(true); + panelMain.setMinimumSize(new Dimension(400, 300)); advancedSettingsCheckBox = new JCheckBox(); advancedSettingsCheckBox.setSelected(false); - this.$$$loadButtonText$$$(advancedSettingsCheckBox, this.$$$getMessageFromBundle$$$("GuiTranslation", "advancedSettings")); + this.$$$loadButtonText$$$(advancedSettingsCheckBox, this.$$$getMessageFromBundle$$$("language/GuiMain", "advancedSettings")); panelMain.add(advancedSettingsCheckBox, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_EAST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); mainScrollPane = new JScrollPane(); panelMain.add(mainScrollPane, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); @@ -663,154 +750,149 @@ private void setGuiElementsVisibility() { final JLabel label1 = new JLabel(); Font label1Font = this.$$$getFont$$$(null, Font.BOLD, 14, label1.getFont()); if (label1Font != null) label1.setFont(label1Font); - this.$$$loadLabelText$$$(label1, this.$$$getMessageFromBundle$$$("GuiTranslation", "KnxGatewayConnectionSettings")); + this.$$$loadLabelText$$$(label1, this.$$$getMessageFromBundle$$$("language/GuiMain", "KnxGatewayConnectionSettings")); panel1.add(label1, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); labelIpGateway = new JLabel(); - this.$$$loadLabelText$$$(labelIpGateway, this.$$$getMessageFromBundle$$$("GuiTranslation", "selectKnxIpGateway")); + this.$$$loadLabelText$$$(labelIpGateway, this.$$$getMessageFromBundle$$$("language/GuiMain", "selectKnxIpGateway")); panel1.add(labelIpGateway, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); panel1.add(comboBoxIpGateways, new GridConstraints(1, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final JLabel label2 = new JLabel(); - this.$$$loadLabelText$$$(label2, this.$$$getMessageFromBundle$$$("GuiTranslation", "ipAddress")); + this.$$$loadLabelText$$$(label2, this.$$$getMessageFromBundle$$$("language/GuiMain", "ipAddress")); panel1.add(label2, new GridConstraints(2, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); textBoxKnxGatewayIpAddr = new JTextField(); panel1.add(textBoxKnxGatewayIpAddr, new GridConstraints(2, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); labeIIpHint = new JLabel(); - this.$$$loadLabelText$$$(labeIIpHint, this.$$$getMessageFromBundle$$$("GuiTranslation", "ipAddrHint")); + this.$$$loadLabelText$$$(labeIIpHint, this.$$$getMessageFromBundle$$$("language/GuiMain", "ipAddrHint")); panel1.add(labeIIpHint, new GridConstraints(2, 3, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); textFieldPort = new JTextField(); panel1.add(textFieldPort, new GridConstraints(3, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); final JLabel label3 = new JLabel(); - this.$$$loadLabelText$$$(label3, this.$$$getMessageFromBundle$$$("GuiTranslation", "port")); + this.$$$loadLabelText$$$(label3, this.$$$getMessageFromBundle$$$("language/GuiMain", "port")); panel1.add(label3, new GridConstraints(3, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); labelPortHint = new JLabel(); - this.$$$loadLabelText$$$(labelPortHint, this.$$$getMessageFromBundle$$$("GuiTranslation", "portHint")); + this.$$$loadLabelText$$$(labelPortHint, this.$$$getMessageFromBundle$$$("language/GuiMain", "portHint")); panel1.add(labelPortHint, new GridConstraints(3, 3, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); natCheckBox = new JCheckBox(); - this.$$$loadButtonText$$$(natCheckBox, this.$$$getMessageFromBundle$$$("GuiTranslation", "useNat")); + this.$$$loadButtonText$$$(natCheckBox, this.$$$getMessageFromBundle$$$("language/GuiMain", "useNat")); panel1.add(natCheckBox, new GridConstraints(4, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); textFieldTpuart = new JTextField(); panel1.add(textFieldTpuart, new GridConstraints(5, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); labelTpuart = new JLabel(); - this.$$$loadLabelText$$$(labelTpuart, this.$$$getMessageFromBundle$$$("GuiTranslation", "tpuart")); + this.$$$loadLabelText$$$(labelTpuart, this.$$$getMessageFromBundle$$$("language/GuiMain", "tpuart")); panel1.add(labelTpuart, new GridConstraints(5, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); textFieldSerial = new JTextField(); textFieldSerial.setText(""); panel1.add(textFieldSerial, new GridConstraints(6, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); labelSerial = new JLabel(); - this.$$$loadLabelText$$$(labelSerial, this.$$$getMessageFromBundle$$$("GuiTranslation", "serial")); + this.$$$loadLabelText$$$(labelSerial, this.$$$getMessageFromBundle$$$("language/GuiMain", "serial")); panel1.add(labelSerial, new GridConstraints(6, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); labelMedium = new JLabel(); - this.$$$loadLabelText$$$(labelMedium, this.$$$getMessageFromBundle$$$("GuiTranslation", "medium")); + this.$$$loadLabelText$$$(labelMedium, this.$$$getMessageFromBundle$$$("language/GuiMain", "medium")); panel1.add(labelMedium, new GridConstraints(7, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final JLabel label4 = new JLabel(); Font label4Font = this.$$$getFont$$$(null, Font.BOLD, 14, label4.getFont()); if (label4Font != null) label4.setFont(label4Font); - this.$$$loadLabelText$$$(label4, this.$$$getMessageFromBundle$$$("GuiTranslation", "UpdaterSettings")); + this.$$$loadLabelText$$$(label4, this.$$$getMessageFromBundle$$$("language/GuiMain", "UpdaterSettings")); panel1.add(label4, new GridConstraints(9, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final JSeparator separator1 = new JSeparator(); panel1.add(separator1, new GridConstraints(8, 1, 1, 3, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, new Dimension(-1, 1), 0, false)); final JLabel label5 = new JLabel(); - this.$$$loadLabelText$$$(label5, this.$$$getMessageFromBundle$$$("GuiTranslation", "scenario")); + this.$$$loadLabelText$$$(label5, this.$$$getMessageFromBundle$$$("language/GuiMain", "scenario")); panel1.add(label5, new GridConstraints(10, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); labelScenarioHint = new JLabel(); - labelScenarioHint.setText("Label"); + this.$$$loadLabelText$$$(labelScenarioHint, this.$$$getMessageFromBundle$$$("language/GuiMain", "labelScenarioHint")); panel1.add(labelScenarioHint, new GridConstraints(10, 3, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); buttonLoadFile = new JButton(); - this.$$$loadButtonText$$$(buttonLoadFile, this.$$$getMessageFromBundle$$$("GuiTranslation", "loadFile")); + this.$$$loadButtonText$$$(buttonLoadFile, this.$$$getMessageFromBundle$$$("language/GuiMain", "loadFile")); panel1.add(buttonLoadFile, new GridConstraints(11, 2, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); textFieldFileName = new JTextField(); panel1.add(textFieldFileName, new GridConstraints(12, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); labelFileName = new JLabel(); - this.$$$loadLabelText$$$(labelFileName, this.$$$getMessageFromBundle$$$("GuiTranslation", "fileName")); + this.$$$loadLabelText$$$(labelFileName, this.$$$getMessageFromBundle$$$("language/GuiMain", "fileName")); panel1.add(labelFileName, new GridConstraints(12, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); labelFileNameHint = new JLabel(); - this.$$$loadLabelText$$$(labelFileNameHint, this.$$$getMessageFromBundle$$$("GuiTranslation", "fileNameHint")); + this.$$$loadLabelText$$$(labelFileNameHint, this.$$$getMessageFromBundle$$$("language/GuiMain", "fileNameHint")); panel1.add(labelFileNameHint, new GridConstraints(12, 3, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); buttonRequestUid = new JButton(); - this.$$$loadButtonText$$$(buttonRequestUid, this.$$$getMessageFromBundle$$$("GuiTranslation", "requestUid")); + this.$$$loadButtonText$$$(buttonRequestUid, this.$$$getMessageFromBundle$$$("language/GuiMain", "requestUid")); panel1.add(buttonRequestUid, new GridConstraints(13, 2, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); textFieldUid = new JTextField(); panel1.add(textFieldUid, new GridConstraints(14, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); labelUid = new JLabel(); - this.$$$loadLabelText$$$(labelUid, this.$$$getMessageFromBundle$$$("GuiTranslation", "uid")); + this.$$$loadLabelText$$$(labelUid, this.$$$getMessageFromBundle$$$("language/GuiMain", "uid")); panel1.add(labelUid, new GridConstraints(14, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); labelUidHint = new JLabel(); - this.$$$loadLabelText$$$(labelUidHint, this.$$$getMessageFromBundle$$$("GuiTranslation", "uidHint")); + this.$$$loadLabelText$$$(labelUidHint, this.$$$getMessageFromBundle$$$("language/GuiMain", "uidHint")); panel1.add(labelUidHint, new GridConstraints(14, 3, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); CheckBoxDiffFlash = new JCheckBox(); - this.$$$loadButtonText$$$(CheckBoxDiffFlash, this.$$$getMessageFromBundle$$$("GuiTranslation", "diffFlash")); + this.$$$loadButtonText$$$(CheckBoxDiffFlash, this.$$$getMessageFromBundle$$$("language/GuiMain", "diffFlash")); panel1.add(CheckBoxDiffFlash, new GridConstraints(15, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); labelFullFlashHint = new JLabel(); - this.$$$loadLabelText$$$(labelFullFlashHint, this.$$$getMessageFromBundle$$$("GuiTranslation", "diffFlashHint")); + this.$$$loadLabelText$$$(labelFullFlashHint, this.$$$getMessageFromBundle$$$("language/GuiMain", "diffFlashHint")); panel1.add(labelFullFlashHint, new GridConstraints(15, 3, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); eraseCompleteFlashCheckBox = new JCheckBox(); - this.$$$loadButtonText$$$(eraseCompleteFlashCheckBox, this.$$$getMessageFromBundle$$$("GuiTranslation", "eraseFlash")); + this.$$$loadButtonText$$$(eraseCompleteFlashCheckBox, this.$$$getMessageFromBundle$$$("language/GuiMain", "eraseFlash")); panel1.add(eraseCompleteFlashCheckBox, new GridConstraints(16, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); labelEraseCompleteFlash = new JLabel(); - this.$$$loadLabelText$$$(labelEraseCompleteFlash, this.$$$getMessageFromBundle$$$("GuiTranslation", "eraseCompleteFlashHint")); + this.$$$loadLabelText$$$(labelEraseCompleteFlash, this.$$$getMessageFromBundle$$$("language/GuiMain", "eraseCompleteFlashHint")); panel1.add(labelEraseCompleteFlash, new GridConstraints(16, 3, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); noFlashCheckBox = new JCheckBox(); - this.$$$loadButtonText$$$(noFlashCheckBox, this.$$$getMessageFromBundle$$$("GuiTranslation", "noFlash")); + this.$$$loadButtonText$$$(noFlashCheckBox, this.$$$getMessageFromBundle$$$("language/GuiMain", "noFlash")); panel1.add(noFlashCheckBox, new GridConstraints(17, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); labelNoFlashHint = new JLabel(); - this.$$$loadLabelText$$$(labelNoFlashHint, this.$$$getMessageFromBundle$$$("GuiTranslation", "noFlashHint")); + this.$$$loadLabelText$$$(labelNoFlashHint, this.$$$getMessageFromBundle$$$("language/GuiMain", "noFlashHint")); panel1.add(labelNoFlashHint, new GridConstraints(17, 3, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final JLabel label6 = new JLabel(); Font label6Font = this.$$$getFont$$$(null, Font.BOLD, 14, label6.getFont()); if (label6Font != null) label6.setFont(label6Font); - this.$$$loadLabelText$$$(label6, this.$$$getMessageFromBundle$$$("GuiTranslation", "KnxBusSettings")); + this.$$$loadLabelText$$$(label6, this.$$$getMessageFromBundle$$$("language/GuiMain", "KnxBusSettings")); panel1.add(label6, new GridConstraints(19, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); labelBootloaderDeviceAddr = new JLabel(); - this.$$$loadLabelText$$$(labelBootloaderDeviceAddr, this.$$$getMessageFromBundle$$$("GuiTranslation", "knxProgDeviceAddr")); + this.$$$loadLabelText$$$(labelBootloaderDeviceAddr, this.$$$getMessageFromBundle$$$("language/GuiMain", "knxProgDeviceAddr")); panel1.add(labelBootloaderDeviceAddr, new GridConstraints(20, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); labelDeviceAddress = new JLabel(); - this.$$$loadLabelText$$$(labelDeviceAddress, this.$$$getMessageFromBundle$$$("GuiTranslation", "knxDeviceAddr")); + this.$$$loadLabelText$$$(labelDeviceAddress, this.$$$getMessageFromBundle$$$("language/GuiMain", "knxDeviceAddr")); panel1.add(labelDeviceAddress, new GridConstraints(21, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); labelOwnAddress = new JLabel(); - this.$$$loadLabelText$$$(labelOwnAddress, this.$$$getMessageFromBundle$$$("GuiTranslation", "knxOwnAddress")); + this.$$$loadLabelText$$$(labelOwnAddress, this.$$$getMessageFromBundle$$$("language/GuiMain", "knxOwnAddress")); panel1.add(labelOwnAddress, new GridConstraints(22, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); textFieldOwnAddress = new JTextField(); panel1.add(textFieldOwnAddress, new GridConstraints(22, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); textFieldDelay = new JTextField(); panel1.add(textFieldDelay, new GridConstraints(23, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); labelDelay = new JLabel(); - this.$$$loadLabelText$$$(labelDelay, this.$$$getMessageFromBundle$$$("GuiTranslation", "knxMessageDelay")); + this.$$$loadLabelText$$$(labelDelay, this.$$$getMessageFromBundle$$$("language/GuiMain", "knxMessageDelay")); panel1.add(labelDelay, new GridConstraints(23, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); - textFieldTimeout = new JTextField(); - panel1.add(textFieldTimeout, new GridConstraints(24, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); - labelTimeout = new JLabel(); - this.$$$loadLabelText$$$(labelTimeout, this.$$$getMessageFromBundle$$$("GuiTranslation", "knxTimeout")); - panel1.add(labelTimeout, new GridConstraints(24, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); panel1.add(comboBoxKnxTelegramPriority, new GridConstraints(25, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); labelTelegramPriority = new JLabel(); - this.$$$loadLabelText$$$(labelTelegramPriority, this.$$$getMessageFromBundle$$$("GuiTranslation", "messagePriority")); + this.$$$loadLabelText$$$(labelTelegramPriority, this.$$$getMessageFromBundle$$$("language/GuiMain", "messagePriority")); panel1.add(labelTelegramPriority, new GridConstraints(25, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); labelKnxSecureUser = new JLabel(); - this.$$$loadLabelText$$$(labelKnxSecureUser, this.$$$getMessageFromBundle$$$("GuiTranslation", "knxSecureUser")); + this.$$$loadLabelText$$$(labelKnxSecureUser, this.$$$getMessageFromBundle$$$("language/GuiMain", "knxSecureUser")); panel1.add(labelKnxSecureUser, new GridConstraints(26, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); textFieldKnxSecureUser = new JTextField(); panel1.add(textFieldKnxSecureUser, new GridConstraints(26, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); labelKnxSecureUserHint = new JLabel(); - this.$$$loadLabelText$$$(labelKnxSecureUserHint, this.$$$getMessageFromBundle$$$("GuiTranslation", "knxSecureUserHint")); + this.$$$loadLabelText$$$(labelKnxSecureUserHint, this.$$$getMessageFromBundle$$$("language/GuiMain", "knxSecureUserHint")); panel1.add(labelKnxSecureUserHint, new GridConstraints(26, 3, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); labelKnxSecureUserPwd = new JLabel(); - this.$$$loadLabelText$$$(labelKnxSecureUserPwd, this.$$$getMessageFromBundle$$$("GuiTranslation", "knxSecureUserPwd")); + this.$$$loadLabelText$$$(labelKnxSecureUserPwd, this.$$$getMessageFromBundle$$$("language/GuiMain", "knxSecureUserPwd")); panel1.add(labelKnxSecureUserPwd, new GridConstraints(27, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); labelKnxSecureUserPwdHint = new JLabel(); - this.$$$loadLabelText$$$(labelKnxSecureUserPwdHint, this.$$$getMessageFromBundle$$$("GuiTranslation", "knxSecureUserPwdHint")); + this.$$$loadLabelText$$$(labelKnxSecureUserPwdHint, this.$$$getMessageFromBundle$$$("language/GuiMain", "knxSecureUserPwdHint")); panel1.add(labelKnxSecureUserPwdHint, new GridConstraints(27, 3, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); textFieldKnxSecureUserPwd = new JTextField(); panel1.add(textFieldKnxSecureUserPwd, new GridConstraints(27, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); labelKnxSecureDevicePwd = new JLabel(); - this.$$$loadLabelText$$$(labelKnxSecureDevicePwd, this.$$$getMessageFromBundle$$$("GuiTranslation", "knxSecureDevicePwd")); + this.$$$loadLabelText$$$(labelKnxSecureDevicePwd, this.$$$getMessageFromBundle$$$("language/GuiMain", "knxSecureDevicePwd")); panel1.add(labelKnxSecureDevicePwd, new GridConstraints(28, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); textFieldKnxSecureDevicePwd = new JTextField(); panel1.add(textFieldKnxSecureDevicePwd, new GridConstraints(28, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); labelKnxSecureDevicePwdHint = new JLabel(); - this.$$$loadLabelText$$$(labelKnxSecureDevicePwdHint, this.$$$getMessageFromBundle$$$("GuiTranslation", "knxSecureDevicePwdHint")); + this.$$$loadLabelText$$$(labelKnxSecureDevicePwdHint, this.$$$getMessageFromBundle$$$("language/GuiMain", "knxSecureDevicePwdHint")); panel1.add(labelKnxSecureDevicePwdHint, new GridConstraints(28, 3, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); buttonStartStopFlash = new JButton(); - this.$$$loadButtonText$$$(buttonStartStopFlash, this.$$$getMessageFromBundle$$$("GuiTranslation", "startFlash")); + this.$$$loadButtonText$$$(buttonStartStopFlash, this.$$$getMessageFromBundle$$$("language/GuiMain", "startFlash")); panel1.add(buttonStartStopFlash, new GridConstraints(29, 2, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); final JScrollPane scrollPane1 = new JScrollPane(); panel1.add(scrollPane1, new GridConstraints(30, 0, 1, 5, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_WANT_GROW, new Dimension(-1, 200), new Dimension(-1, 200), null, 0, false)); @@ -826,8 +908,25 @@ private void setGuiElementsVisibility() { panel1.add(comboBoxMedium, new GridConstraints(7, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); panel1.add(comboBoxScenario, new GridConstraints(10, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); reloadGatewaysButton = new JButton(); - this.$$$loadButtonText$$$(reloadGatewaysButton, this.$$$getMessageFromBundle$$$("GuiTranslation", "reloadKnxIpGateways")); + this.$$$loadButtonText$$$(reloadGatewaysButton, this.$$$getMessageFromBundle$$$("language/GuiMain", "reloadKnxIpGateways")); panel1.add(reloadGatewaysButton, new GridConstraints(1, 3, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + labelIpGateway.setLabelFor(comboBoxIpGateways); + label2.setLabelFor(textBoxKnxGatewayIpAddr); + label3.setLabelFor(textFieldPort); + labelTpuart.setLabelFor(textFieldTpuart); + labelSerial.setLabelFor(textFieldSerial); + labelMedium.setLabelFor(comboBoxMedium); + label5.setLabelFor(comboBoxScenario); + labelFileName.setLabelFor(textFieldFileName); + labelUid.setLabelFor(textFieldUid); + labelBootloaderDeviceAddr.setLabelFor(textFieldBootloaderDeviceAddress); + labelDeviceAddress.setLabelFor(textFieldDeviceAddress); + labelOwnAddress.setLabelFor(textFieldOwnAddress); + labelDelay.setLabelFor(textFieldDelay); + labelTelegramPriority.setLabelFor(comboBoxKnxTelegramPriority); + labelKnxSecureUser.setLabelFor(textFieldKnxSecureUser); + labelKnxSecureUserPwd.setLabelFor(textFieldKnxSecureUserPwd); + labelKnxSecureDevicePwd.setLabelFor(textFieldKnxSecureDevicePwd); } /** @@ -839,7 +938,7 @@ private void setGuiElementsVisibility() { if (fontName == null) { resultName = currentFont.getName(); } else { - Font testFont = new Font(fontName, PLAIN, 10); + Font testFont = new Font(fontName, Font.PLAIN, 10); if (testFont.canDisplay('a') && testFont.canDisplay('1')) { resultName = fontName; } else { diff --git a/firmware_updater/updater/src/org/selfbus/updater/gui/GuiSettings.java b/firmware_updater/updater/src/org/selfbus/updater/gui/GuiSettings.java new file mode 100644 index 00000000..42290802 --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/gui/GuiSettings.java @@ -0,0 +1,132 @@ +package org.selfbus.updater.gui; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.*; +import java.awt.*; +import java.io.File; +import java.io.IOException; + +public class GuiSettings { + private static final Logger logger = LoggerFactory.getLogger(GuiSettings.class); + private final JFrame parent; + private static final ObjectMapper objectMapper = new ObjectMapper() + .enable(SerializationFeature.INDENT_OUTPUT) + .enable(SerializationFeature.WRAP_ROOT_VALUE); + + @SuppressWarnings("unused") + private GuiSettings() { + this.parent = null; + } + + private ObjectNode getObjectNode() { + return objectMapper.createObjectNode(); + } + + public GuiSettings(JFrame parent) { + this.parent = parent; + logger.debug("parent: {} name: {}", this.parent.getClass().getName(), this.parent.getName()); + } + + public void readComponentsSettings(String fileName) throws IOException { + logger.debug("reading '{}'", fileName); + JsonNode rootNode = objectMapper.readTree(new File(fileName)); + readComponentData(parent, rootNode); + } + + public void writeComponentSettings(String fileName) throws IOException { + logger.debug("saving '{}'", fileName); + JsonNode rootNode = writeComponentsData(parent); + objectMapper.writeValue(new File(fileName), rootNode); + } + + private void readComponentData(Component component, JsonNode node) { + String nodeKey; + // GuiMain.MainFrame is saved with json root key `ObjectNode`s simple class name + // this.getObjectNode() ensures that we set the same nodeKey as in writeComponentsData() + if (component == parent) { + nodeKey = this.getObjectNode().getClass().getSimpleName(); + } + else { + nodeKey = getComponentSettingKey(component); + } + JsonNode componentNode = node.path(nodeKey); + + if (component instanceof JCheckBox checkBox) { + checkBox.setSelected(componentNode.path("selected").asBoolean()); + } + else if (component instanceof JTextField textField) { + textField.setText(componentNode.path("text").asText()); + } + else if (component instanceof JComboBox comboBox) { + int selectionIndex = componentNode.path("selectionIndex").asInt(-1); + if ((selectionIndex >= 0) && (selectionIndex < comboBox.getItemCount())) { + comboBox.setSelectedIndex(selectionIndex); + } + } + else if (component instanceof Container container) { + if (component instanceof JFrame frame) { + readFrame(frame, componentNode); + } + for (Component child : container.getComponents()) { + readComponentData(child, componentNode); + } + } + } + + private JsonNode writeComponentsData(Component component) { + ObjectNode node = getObjectNode(); + if (component instanceof JCheckBox checkBox) { + node.put("selected", checkBox.isSelected()); + } + else if (component instanceof JTextField textField) { + node.put("text", textField.getText()); + } + else if (component instanceof JComboBox comboBox) { + int selectionIndex = comboBox.getSelectedIndex(); + node.put("selectionIndex", selectionIndex); + } + else if (component instanceof Container container) { + if (component instanceof JFrame frame) { + writeFrame(frame, node); + } + for (Component child : container.getComponents()) { + JsonNode childNode = writeComponentsData(child); + if (!childNode.isEmpty(null)) { + node.set(getComponentSettingKey(child), childNode); + } + } + } + return node; + } + + private String getComponentSettingKey(Component component) { + String key = component.getClass().getSimpleName(); + if (component.getName() != null) { + key += "." + component.getName(); + } + else { + key += ".null"; + } + return key; + } + + private void readFrame(JFrame frame, JsonNode node) { + frame.setSize(node.path("width").asInt(), node.path("height").asInt()); + frame.setLocation(node.path("x").asInt(), node.path("y").asInt()); + frame.setExtendedState(node.path("extended_state").asInt()); + } + + private void writeFrame(JFrame frame, ObjectNode node) { + node.put("width", frame.getWidth()); + node.put("height", frame.getHeight()); + node.put("x", frame.getX()); + node.put("y", frame.getY()); + node.put("extended_state", frame.getExtendedState()); + } +} \ No newline at end of file diff --git a/firmware_updater/updater/src/org/selfbus/updater/gui/GuiUncaughtExceptionHandler.java b/firmware_updater/updater/src/org/selfbus/updater/gui/GuiUncaughtExceptionHandler.java new file mode 100644 index 00000000..c2249345 --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/gui/GuiUncaughtExceptionHandler.java @@ -0,0 +1,76 @@ +package org.selfbus.updater.gui; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.*; +import java.io.PrintWriter; +import java.io.StringWriter; + +import static org.selfbus.updater.logging.Color.*; +import static org.fusesource.jansi.Ansi.ansi; + +public class GuiUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { + private final static Logger logger = LoggerFactory.getLogger(GuiUncaughtExceptionHandler.class); + private final JFrame parentFrame; + + public GuiUncaughtExceptionHandler(JFrame parentFrame) { + this.parentFrame = parentFrame; + } + + /** + * Handles uncaught exceptions in threads by logging the error details and displaying + * an error dialog to inform the user. + * + * @param thread The thread in which the uncaught exception occurred. + * @param throwable The uncaught exception or error. + */ + @Override + public void uncaughtException(Thread thread, Throwable throwable) { + logger.error("{}Uncaught exception in thread {} {}{}", + ansi().fgBright(WARN) , thread.getName(), throwable.toString(), ansi().reset(), throwable); + + // todo see logback issue #876 + // https://github.com/qos-ch/logback/issues/876 + // Don´t delete .toString() + // Show exception details in a dialog + SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog( + parentFrame, + "An unexpected error occurred:" + System.lineSeparator() + + throwable.toString() + System.lineSeparator() + + getLimitedStackTrace(throwable, 7), + "Exception", + JOptionPane.ERROR_MESSAGE + )); + } + + /** + * Converts the stack trace to a String and limits its depth. + * + * @param throwable The exception or error + * @param maxDepth The maximum number of stack trace elements to include + * @return A String containing the limited stack trace + */ + private static String getLimitedStackTrace(Throwable throwable, int maxDepth) { + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + + // Get the stack trace elements + StackTraceElement[] stackTrace = throwable.getStackTrace(); + if (maxDepth < 0) + { + maxDepth = stackTrace.length; + } + int limit = Math.min(stackTrace.length, maxDepth); // Ensure we don't exceed the actual trace length + for (int i = 0; i < limit; i++) { + printWriter.println(" at " + stackTrace[i].toString()); + } + + // Indicate if there are more stack trace elements beyond the limit + if (stackTrace.length > maxDepth) { + printWriter.println(" ... (" + (stackTrace.length - maxDepth) + " more elements)"); + } + + return stringWriter.toString(); + } +} diff --git a/firmware_updater/updater/src/org/selfbus/updater/logging/Color.java b/firmware_updater/updater/src/org/selfbus/updater/logging/Color.java new file mode 100644 index 00000000..ce86f395 --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/logging/Color.java @@ -0,0 +1,15 @@ +package org.selfbus.updater.logging; + +import org.fusesource.jansi.Ansi; + +import static org.fusesource.jansi.Ansi.Color.*; + +public final class Color { + @SuppressWarnings("unused") + private Color() {} + + // old style colors + public static final Ansi.Color INFO = YELLOW; + public static final Ansi.Color OK = GREEN; + public static final Ansi.Color WARN = RED; +} \ No newline at end of file diff --git a/firmware_updater/updater/src/org/selfbus/updater/logging/ConsoleEncoder.java b/firmware_updater/updater/src/org/selfbus/updater/logging/ConsoleEncoder.java new file mode 100644 index 00000000..771695ce --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/logging/ConsoleEncoder.java @@ -0,0 +1,54 @@ +package org.selfbus.updater.logging; + +import java.util.Arrays; + +import ch.qos.logback.classic.encoder.PatternLayoutEncoder; +import ch.qos.logback.classic.spi.ILoggingEvent; + +import static org.selfbus.updater.logging.Markers.CONSOLE_GUI_NO_NEWLINE; + +@SuppressWarnings("unused") +public class ConsoleEncoder extends PatternLayoutEncoder { + @Override + public void start() { + super.start(); + if (!LoggingManager.isRunningInIntelliJ()) { + addInfo("IntelliJ not detected => Marker processing enabled."); + } + } + + @Override + public byte[] encode(ILoggingEvent event) { + final byte[] superByteArray = super.encode(event); + // No marker + if (event.getMarkerList() == null) { + return superByteArray; + } + + // Not the marker we are looking for + if (!(event.getMarkerList().contains(CONSOLE_GUI_NO_NEWLINE))) { + return superByteArray; + } + + final byte[] lineSeparator = System.lineSeparator().getBytes(); + // shorter than the line separator + if (superByteArray.length < lineSeparator.length) { + return superByteArray; + } + + byte[] compare = Arrays.copyOfRange(superByteArray, superByteArray.length - lineSeparator.length, superByteArray.length); + if (!Arrays.equals(compare, lineSeparator)) { + return superByteArray; + } + + final byte[] trimmed = Arrays.copyOfRange(superByteArray, 0, superByteArray.length - lineSeparator.length); + if (LoggingManager.isRunningInIntelliJ()) { + // IntelliJs debug console does not support ansi cursor movements + // so we return the original one from the base pattern decoder + return superByteArray; + } + else { + return trimmed; + } + } +} diff --git a/firmware_updater/updater/src/org/selfbus/updater/logging/ExcludeMarkerFilter.java b/firmware_updater/updater/src/org/selfbus/updater/logging/ExcludeMarkerFilter.java new file mode 100644 index 00000000..16c1b525 --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/logging/ExcludeMarkerFilter.java @@ -0,0 +1,38 @@ +package org.selfbus.updater.logging; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.filter.Filter; +import ch.qos.logback.core.spi.FilterReply; +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; + +@SuppressWarnings("unused") +public class ExcludeMarkerFilter extends Filter { + + String marker; + + @Override + public FilterReply decide(ILoggingEvent event) { + Marker markerToMatch = MarkerFactory.getMarker(marker); + if (event.getMarkerList() == null) { + return FilterReply.NEUTRAL; + } + + if (event.getMarkerList().contains(markerToMatch)) { + return FilterReply.DENY; + } + else { + return FilterReply.NEUTRAL; + } + } + + @SuppressWarnings("unused") + public String getMarker() { + return marker; + } + + @SuppressWarnings("unused") + public void setMarker(String marker) { + this.marker = marker; + } +} diff --git a/firmware_updater/updater/src/org/selfbus/updater/logging/JTextPaneAppender.java b/firmware_updater/updater/src/org/selfbus/updater/logging/JTextPaneAppender.java new file mode 100644 index 00000000..70e25d1f --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/logging/JTextPaneAppender.java @@ -0,0 +1,96 @@ +package org.selfbus.updater.logging; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.classic.spi.StackTraceElementProxy; +import ch.qos.logback.core.AppenderBase; +import org.selfbus.updater.gui.ConColorsToStyledDoc; + +import javax.swing.*; +import javax.swing.text.BadLocationException; + +import static org.selfbus.updater.logging.Markers.CONSOLE_GUI_NO_NEWLINE; + +public class JTextPaneAppender extends AppenderBase { + private JTextPane textPane; + + @SuppressWarnings("unused") + public JTextPaneAppender() { + this.textPane = null; + } + + private void updateTextPane(String message) throws BadLocationException { + if (getTextPane() == null) { + return; + } + ConColorsToStyledDoc.Convert(message, getTextPane()); + } + + @Override + protected void append(ILoggingEvent event) { + if (getTextPane() == null) { + return; + } + + StringBuilder eventMessage = new StringBuilder(event.getFormattedMessage()); + + // Check for ThrowableProxy to retrieve the stack trace + IThrowableProxy throwableProxy = event.getThrowableProxy(); + if (throwableProxy != null) { + // Append the stack trace to the log message + eventMessage.append(System.lineSeparator()); + appendStackTrace(throwableProxy, eventMessage); + } + + String logMessage; + if ((event.getMarkerList() != null) && (event.getMarkerList().contains(CONSOLE_GUI_NO_NEWLINE))) { + logMessage = eventMessage.toString(); + } + else { + // Ensure same behavior like ConsoleAppender + // It gets a new line added to every logging message + // by it´s pattern %message%n (notice the n = new line) in logback.xml + logMessage = eventMessage.append(System.lineSeparator()).toString(); + } + + String finalLogMessage = logMessage; + SwingUtilities.invokeLater(() -> { + try { + // Append formatted message to text area using the Thread + updateTextPane(finalLogMessage); + } + catch (BadLocationException e) { + throw new RuntimeException(e); + } + }); + } + + private void appendStackTrace(IThrowableProxy throwableProxy, StringBuilder builder) { + builder.append(throwableProxy.getClassName()) + .append(": ") + .append(throwableProxy.getMessage()) + .append(System.lineSeparator()); + + // Loop through each stack trace element and append it + for (StackTraceElementProxy elementProxy : throwableProxy.getStackTraceElementProxyArray()) { + builder.append("\t ") + .append(elementProxy.toString()) + .append(System.lineSeparator()); + } + + // If there are any nested causes, recursively append them + IThrowableProxy cause = throwableProxy.getCause(); + if (cause != null) { + builder.append("Caused by: "); + appendStackTrace(cause, builder); + } + } + + public JTextPane getTextPane() { + return textPane; + } + + public void setTextPane(JTextPane textPane) { + this.textPane = textPane; + } +} \ No newline at end of file diff --git a/firmware_updater/updater/src/org/selfbus/updater/logging/LoggingManager.java b/firmware_updater/updater/src/org/selfbus/updater/logging/LoggingManager.java new file mode 100644 index 00000000..dc2bad04 --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/logging/LoggingManager.java @@ -0,0 +1,67 @@ +package org.selfbus.updater.logging; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.filter.ThresholdFilter; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.spi.ContextAwareBase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.management.ManagementFactory; + +public final class LoggingManager { + /** + * The name of the console appender used for logging. + * This constant is referenced in the logging configuration defined in + * {@code src/resources/logback/uiAppenders.xml}. + *

+ * Note: If the value of this constant is changed, ensure to update the + * corresponding appender name in the `logback/uiAppenders.xml` file. + */ + public static final String CONSOLE_APPENDER_NAME = "CONSOLE"; + + /** + * The name of the appender used for logging messages to a {@code JTextPane}. + * This constant is referenced in the logging configuration defined in + * {@code src/resources/logback/uiAppenders.xml}. + *

+ * Note: If the value of this constant is changed, ensure to update the + * corresponding appender name in the `logback/uiAppenders.xml` file. + * */ + public static final String JTEXTPANE_APPENDER_NAME = "JTextPane"; + + private final static Logger logger = LoggerFactory.getLogger(LoggingManager.class); + + private static LoggerContext getLoggerContext() { + return (LoggerContext) LoggerFactory.getILoggerFactory(); + } + + public static Appender getAppender(String appenderName) { + return getLoggerContext().getLogger(Logger.ROOT_LOGGER_NAME).getAppender(appenderName); + } + + public static void setThresholdFilterLogLevel(String appenderName, Level newLogLevel) { + Appender appender = getAppender(appenderName); + if (appender == null) { + logger.warn("Appender {} not found", appenderName); + return; + } + + for (ContextAwareBase filter : appender.getCopyOfAttachedFiltersList()) { + if (filter instanceof ThresholdFilter thresholdFilter) { + logger.debug("Appender {} ThresholdFilter log level set to: {}", appender.getName(), newLogLevel); + thresholdFilter.setLevel(newLogLevel.toString()); + return; + } + } + logger.warn("Appender {} has no {}", appenderName, ThresholdFilter.class.getSimpleName()); + } + + public static boolean isRunningInIntelliJ() { + // Check for IntelliJ debugging JVM arguments + return ManagementFactory.getRuntimeMXBean().getInputArguments().stream() + .anyMatch(arg -> arg.contains("javaagent") || arg.contains("agentlib")); + } +} diff --git a/firmware_updater/updater/src/org/selfbus/updater/logging/Markers.java b/firmware_updater/updater/src/org/selfbus/updater/logging/Markers.java new file mode 100644 index 00000000..2666dd7d --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/logging/Markers.java @@ -0,0 +1,22 @@ +package org.selfbus.updater.logging; + +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; + +/** + * The {@code Markers} class provides a set of predefined logging markers + * that can be used to categorize and filter log messages in the application. + */ +public class Markers { + + /** + * Marker used to categorize log messages intended exclusively for the console and GUI. + */ + public static final Marker CONSOLE_GUI_ONLY = MarkerFactory.getMarker("CONSOLE_GUI_ONLY"); + + /** + * Marker used to categorize log messages intended exclusively for the console and GUI, + * specifically to handle cases where newline characters should be omitted. + */ + public static final Marker CONSOLE_GUI_NO_NEWLINE = MarkerFactory.getMarker("CONSOLE_GUI_NO_NEWLINE"); +} diff --git a/firmware_updater/updater/src/org/selfbus/updater/logging/MessageFilter.java b/firmware_updater/updater/src/org/selfbus/updater/logging/MessageFilter.java new file mode 100644 index 00000000..098fa594 --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/logging/MessageFilter.java @@ -0,0 +1,48 @@ +package org.selfbus.updater.logging; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.filter.Filter; +import ch.qos.logback.core.spi.FilterReply; + +import java.util.List; + +/** + * The {@code MessageFilter} class is a custom implementation of {@code Filter}. + * It is used to evaluate logging events and filter out specific log messages based on a + * predefined list of messages. + *

+ * This functionality can be useful for excluding log messages that are deemed + * irrelevant or repetitive during runtime. + */ +public class MessageFilter extends Filter { + + /** + * A list of log messages that should be filtered out. + * If an event's formatted message contains any of the strings in this list, + * the event will be excluded from logging. + */ + public static final List FILTERED_MESSAGES = List.of(); + + /** + * Evaluates a logging event and determines whether the event should + * be accepted or handled neutrally based on its log message content. + * + * @param event the logging event to be evaluated. + * @return {@code FilterReply.DENY} if the event's message contains any of the predefined + * filtered messages, otherwise {@code FilterReply.NEUTRAL}. + */ + @Override + public FilterReply decide(ILoggingEvent event) { + if (!isStarted()) { + return FilterReply.NEUTRAL; + } + + String message = event.getFormattedMessage(); + for (String filteredMessage : FILTERED_MESSAGES) { + if (message.contains(filteredMessage)) { + return FilterReply.DENY; + } + } + return FilterReply.NEUTRAL; + } +} diff --git a/firmware_updater/updater/src/org/selfbus/updater/logging/ThrowableFilter.java b/firmware_updater/updater/src/org/selfbus/updater/logging/ThrowableFilter.java new file mode 100644 index 00000000..b2ce3973 --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/logging/ThrowableFilter.java @@ -0,0 +1,131 @@ +package org.selfbus.updater.logging; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.ThrowableProxy; +import ch.qos.logback.core.filter.Filter; +import ch.qos.logback.core.spi.FilterReply; +import tuwien.auto.calimero.KNXAckTimeoutException; +import tuwien.auto.calimero.KNXTimeoutException; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * The {@code ThrowableFilter} class extends {@code Filter} + * to provide a custom evaluation of logging events that filters out specific exceptions + * with matching messages. + * + *

This evaluator is designed to exclude specific logging events from being processed, + * based on their associated throwable (exception) type and message content. The + * {@code exceptionFilterMap} maintains a mapping of throwable types and corresponding + * messages that should be filtered. + * + *

Example usage: + *

    + *
  • If the throwable is of type {@code KNXAckTimeoutException} and its message matches + * {@code "maximum send attempts, no service acknowledgment received"}, the event will + * be filtered out (ignored).
  • + *
  • If the throwable is of type {@code KNXTimeoutException} and its message matches + * {@code "no confirmation reply received for DM ..."}, the event will also be filtered out.
  • + *
+ * + *

If no throwable is associated with the event or the throwable does not meet the + * filtering criteria, the event is not filtered. + * + *

This can be useful in logging systems where specific repetitive or expected exceptions + * (e.g., timeout exceptions) need to be ignored to reduce log noise. + */ +public class ThrowableFilter extends Filter { + + /** + * The {@code ConditionalFilter} record represents a pair of conditions used to filter + * throwable logging events based on specific log messages and associated exception messages. + * + * @param logMessage The log message as a filtering condition. + * @param exceptionMessage The message of the exception that serves as a filtering condition. + */ + public record ConditionalFilter(String logMessage, String exceptionMessage) {} + + /** + * A customizable map that defines configurable filter criteria for throwable events. + *

+ * Each entry in the map specifies a throwable class (the key) and a corresponding list + * of {@link ConditionalFilter}s (the value). The key represents the type of exception, + * while the value contains specific filtering conditions. A filtering condition is + * defined by a combination of a log message and an exception message. Only events + * containing a throwable that matches the exception type and both of its associated + * filter conditions will be excluded. + *

+ * + * @see ConditionalFilter + */ + public static final Map, List> EXCEPTION_FILTER_MAP = Map.of( + KNXAckTimeoutException.class, List.of( + // Loxone Miniserver Gen1 returns always status 0x04 on KNX tunnel requests with sequence number 255 + new ConditionalFilter( + "close connection - maximum send attempts", + "maximum send attempts, no service acknowledgment received" + ) + ), + KNXTimeoutException.class, List.of( + // Loxone Miniserver Gen1 does not respond to + // tuwien.auto.calimero.link.KNXNetworkLinkIP.configureWithServerSettings(..) which sends + // M_PropRead.req OT=8 (cEMI Server), PID=51 (PID_MEDIUM_TYPE) + new ConditionalFilter( + "response timeout waiting for confirmation", + "no confirmation reply received for DM prop-read.req objtype 8 instance 1 pid 51 start 1 elements 1" + ), + new ConditionalFilter( + "skip link configuration (use defaults)", + "no confirmation reply received for DM prop-read.req objtype 8 instance 1 pid 51 start 1 elements 1" + ), + + // and M_PropRead.req OT=0 (Device), PID=56 (PID_MAX_APDULENGTH) + new ConditionalFilter( + "response timeout waiting for confirmation", + "no confirmation reply received for DM prop-read.req objtype 0 instance 1 pid 56 start 1 elements 1" + ), + new ConditionalFilter( + "skip link configuration (use defaults)", + "no confirmation reply received for DM prop-read.req objtype 0 instance 1 pid 56 start 1 elements 1" + ) + ) + ); + + /** + * Evaluates a logging event to determine whether it should be processed or filtered. + * + * @param event the {@link ILoggingEvent} instance to be evaluated. + * @return {@code FilterReply.NEUTRAL} if the event should be processed; {@code FilterReply.DENY} if the event + * should be filtered out. + */ + @Override + public FilterReply decide(ILoggingEvent event) { + if (!isStarted()) { + return FilterReply.NEUTRAL; + } + + ThrowableProxy throwableProxy = (ThrowableProxy) event.getThrowableProxy(); + if (throwableProxy == null) { + return FilterReply.NEUTRAL; // no stacktrace present + } + + String logMessage = event.getFormattedMessage(); + Throwable throwable = throwableProxy.getThrowable(); + // Check if the current exception is present in the filter map + for (Map.Entry, List> exceptionFilter : EXCEPTION_FILTER_MAP.entrySet()) { + if (exceptionFilter.getKey().isInstance(throwable)) { + // Check if the exception message matches one of the filtered messages + for (ConditionalFilter f : exceptionFilter.getValue()) { + // Check if logMessage and exception message match the filter + if ((Objects.equals(logMessage, f.logMessage())) && + (Objects.equals(throwable.getMessage(), f.exceptionMessage()))) { + return FilterReply.DENY; // Filter this event out + } + } + } + } + return FilterReply.NEUTRAL; + } +} diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/tests/flashdiff/Decompressor.java b/firmware_updater/updater/src/org/selfbus/updater/mode/differential/Decompressor.java similarity index 96% rename from firmware_updater/updater/source/src/main/java/org/selfbus/updater/tests/flashdiff/Decompressor.java rename to firmware_updater/updater/src/org/selfbus/updater/mode/differential/Decompressor.java index 4a442821..b5765960 100644 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/tests/flashdiff/Decompressor.java +++ b/firmware_updater/updater/src/org/selfbus/updater/mode/differential/Decompressor.java @@ -1,129 +1,129 @@ -package org.selfbus.updater.tests.flashdiff; - -import org.selfbus.updater.BinImage; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Arrays; - -/** - * Apply diff stream and produce one page to be flashed - * (based on diff stream, original ROM content, and RAM buffer to store some latest ROM pages already flashed) - */ -public class Decompressor { - private final static Logger logger = LoggerFactory.getLogger(Decompressor.class.getName()); - private enum State { - EXPECT_COMMAND_BYTE, - EXPECT_COMMAND_PARAMS, - EXPECT_RAW_DATA - } - - private final byte[] cmdBuffer = new byte[5]; - private int expectedCmdLength = 0; - private int cmdBufferLength = 0; - private final byte[] scratchpad = new byte[FlashPage.PAGE_SIZE]; - private int scratchpadIndex; - private int rawLength; - private State state = State.EXPECT_COMMAND_BYTE; - private final DecompressorListener listener; - private final BinImage rom; - private final OldWindow oldPagesRam = new OldWindow(); - - public Decompressor(BinImage rom, DecompressorListener listener) { - this.rom = rom; - this.listener = listener; - } - - private int getLength() { - if ((cmdBuffer[0] & FlashDiff.FLAG_LONG) == FlashDiff.FLAG_LONG) { - return ((cmdBuffer[0] & 0b111111) << 8) | (cmdBuffer[1] & 0xff); - } else { - return (cmdBuffer[0] & 0b111111); - } - } - - private boolean isCopyFromRam() { - if ((cmdBuffer[0] & FlashDiff.FLAG_LONG) == FlashDiff.FLAG_LONG) { - return (cmdBuffer[2] & FlashDiff.ADDR_FROM_RAM) == FlashDiff.ADDR_FROM_RAM; - } else { - return (cmdBuffer[1] & FlashDiff.ADDR_FROM_RAM) == FlashDiff.ADDR_FROM_RAM; - } - } - - private int getCopyAddress() { - if ((cmdBuffer[0] & FlashDiff.FLAG_LONG) == FlashDiff.FLAG_LONG) { - return ((cmdBuffer[2] & 0b1111111) << 16) | ((cmdBuffer[3] & 0xff) << 8) | (cmdBuffer[4] & 0xff); - } else { - return ((cmdBuffer[1] & 0b1111111) << 16) | ((cmdBuffer[2] & 0xff) << 8) | (cmdBuffer[3] & 0xff); - } - } - - private void resetStateMachine() { - state = State.EXPECT_COMMAND_BYTE; - } - - public void pageCompleted() { - listener.flashPage(oldPagesRam, new FlashPage(scratchpad, 0)); - scratchpadIndex = 0; - Arrays.fill(scratchpad, (byte)0); // only get the last (incomplete) page padded with 0 for unit tests, otherwise not relevant - } - - public void putByte(byte data) { - logger.trace("Decompressor processing new byte {}, state={}", (data & 0xff), state); - switch (state) { - case EXPECT_COMMAND_BYTE: - cmdBuffer[0] = data; - cmdBufferLength = 1; - expectedCmdLength = 1; - if ((data & FlashDiff.CMD_COPY) == FlashDiff.CMD_COPY) { - expectedCmdLength += 3; // 3 more bytes of source address - } - if ((data & FlashDiff.FLAG_LONG) == FlashDiff.FLAG_LONG) { - expectedCmdLength += 1; // 1 more byte for longer length - } - if (expectedCmdLength > 1) { - state = State.EXPECT_COMMAND_PARAMS; - } else { - state = State.EXPECT_RAW_DATA; - rawLength = 0; - } - break; - case EXPECT_COMMAND_PARAMS: - cmdBuffer[cmdBufferLength++] = data; - if (cmdBufferLength >= expectedCmdLength) { - // we have all params of the command - if ((cmdBuffer[0] & FlashDiff.CMD_COPY) == FlashDiff.CMD_COPY) { - // perform copy - if (isCopyFromRam()) { - logger.trace("COPY FROM RAM index={} length={} from addr={}", scratchpadIndex, getLength(), getCopyAddress()); - System.arraycopy(oldPagesRam.getOldBinData(), getCopyAddress(), scratchpad, scratchpadIndex, getLength()); - } else { - logger.trace("COPY FROM ROM index={} length={} from addr={}", scratchpadIndex, getLength(), getCopyAddress()); - logger.trace("{}", rom.getBinData()[getCopyAddress()] & 0xff); - logger.trace("{}", rom.getBinData()[getCopyAddress()+1] & 0xff); - logger.trace("{}", rom.getBinData()[getCopyAddress()+2] & 0xff); - System.arraycopy(rom.getBinData(), getCopyAddress(), scratchpad, scratchpadIndex, getLength()); - } - scratchpadIndex += getLength(); - // and finish command - resetStateMachine(); - } else { - // next, read raw data - state = State.EXPECT_RAW_DATA; - rawLength = 0; - } - } // else expect more params of the command - break; - case EXPECT_RAW_DATA: - // store data read to scratchpad - scratchpad[scratchpadIndex++] = data; - rawLength++; - if (rawLength >= getLength()) { - // we have all RAW data, reset state machine - resetStateMachine(); - } - } - } - - -} +package org.selfbus.updater.mode.differential; + +import org.selfbus.updater.BinImage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; + +/** + * Apply diff stream and produce one page to be flashed + * (based on diff stream, original ROM content, and RAM buffer to store some latest ROM pages already flashed) + */ +public class Decompressor { + private final static Logger logger = LoggerFactory.getLogger(Decompressor.class); + private enum State { + EXPECT_COMMAND_BYTE, + EXPECT_COMMAND_PARAMS, + EXPECT_RAW_DATA + } + + private final byte[] cmdBuffer = new byte[5]; + private int expectedCmdLength = 0; + private int cmdBufferLength = 0; + private final byte[] scratchpad = new byte[FlashPage.PAGE_SIZE]; + private int scratchpadIndex; + private int rawLength; + private State state = State.EXPECT_COMMAND_BYTE; + private final DecompressorListener listener; + private final BinImage rom; + private final OldWindow oldPagesRam = new OldWindow(); + + public Decompressor(BinImage rom, DecompressorListener listener) { + this.rom = rom; + this.listener = listener; + } + + private int getLength() { + if ((cmdBuffer[0] & FlashDiff.FLAG_LONG) == FlashDiff.FLAG_LONG) { + return ((cmdBuffer[0] & 0b111111) << 8) | (cmdBuffer[1] & 0xff); + } else { + return (cmdBuffer[0] & 0b111111); + } + } + + private boolean isCopyFromRam() { + if ((cmdBuffer[0] & FlashDiff.FLAG_LONG) == FlashDiff.FLAG_LONG) { + return (cmdBuffer[2] & FlashDiff.ADDR_FROM_RAM) == FlashDiff.ADDR_FROM_RAM; + } else { + return (cmdBuffer[1] & FlashDiff.ADDR_FROM_RAM) == FlashDiff.ADDR_FROM_RAM; + } + } + + private int getCopyAddress() { + if ((cmdBuffer[0] & FlashDiff.FLAG_LONG) == FlashDiff.FLAG_LONG) { + return ((cmdBuffer[2] & 0b1111111) << 16) | ((cmdBuffer[3] & 0xff) << 8) | (cmdBuffer[4] & 0xff); + } else { + return ((cmdBuffer[1] & 0b1111111) << 16) | ((cmdBuffer[2] & 0xff) << 8) | (cmdBuffer[3] & 0xff); + } + } + + private void resetStateMachine() { + state = State.EXPECT_COMMAND_BYTE; + } + + public void pageCompleted() { + listener.flashPage(oldPagesRam, new FlashPage(scratchpad, 0)); + scratchpadIndex = 0; + Arrays.fill(scratchpad, (byte)0); // only get the last (incomplete) page padded with 0 for unit tests, otherwise not relevant + } + + public void putByte(byte data) { + logger.trace("Decompressor processing new byte {}, state={}", (data & 0xff), state); + switch (state) { + case EXPECT_COMMAND_BYTE: + cmdBuffer[0] = data; + cmdBufferLength = 1; + expectedCmdLength = 1; + if ((data & FlashDiff.CMD_COPY) == FlashDiff.CMD_COPY) { + expectedCmdLength += 3; // 3 more bytes of source address + } + if ((data & FlashDiff.FLAG_LONG) == FlashDiff.FLAG_LONG) { + expectedCmdLength += 1; // 1 more byte for longer length + } + if (expectedCmdLength > 1) { + state = State.EXPECT_COMMAND_PARAMS; + } else { + state = State.EXPECT_RAW_DATA; + rawLength = 0; + } + break; + case EXPECT_COMMAND_PARAMS: + cmdBuffer[cmdBufferLength++] = data; + if (cmdBufferLength >= expectedCmdLength) { + // we have all params of the command + if ((cmdBuffer[0] & FlashDiff.CMD_COPY) == FlashDiff.CMD_COPY) { + // perform copy + if (isCopyFromRam()) { + logger.trace("COPY FROM RAM index={} length={} from addr={}", scratchpadIndex, getLength(), getCopyAddress()); + System.arraycopy(oldPagesRam.getOldBinData(), getCopyAddress(), scratchpad, scratchpadIndex, getLength()); + } else { + logger.trace("COPY FROM ROM index={} length={} from addr={}", scratchpadIndex, getLength(), getCopyAddress()); + logger.trace("{}", rom.getBinData()[getCopyAddress()] & 0xff); + logger.trace("{}", rom.getBinData()[getCopyAddress()+1] & 0xff); + logger.trace("{}", rom.getBinData()[getCopyAddress()+2] & 0xff); + System.arraycopy(rom.getBinData(), getCopyAddress(), scratchpad, scratchpadIndex, getLength()); + } + scratchpadIndex += getLength(); + // and finish command + resetStateMachine(); + } else { + // next, read raw data + state = State.EXPECT_RAW_DATA; + rawLength = 0; + } + } // else expect more params of the command + break; + case EXPECT_RAW_DATA: + // store data read to scratchpad + scratchpad[scratchpadIndex++] = data; + rawLength++; + if (rawLength >= getLength()) { + // we have all RAW data, reset state machine + resetStateMachine(); + } + } + } + + +} diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/tests/flashdiff/DecompressorListener.java b/firmware_updater/updater/src/org/selfbus/updater/mode/differential/DecompressorListener.java similarity index 66% rename from firmware_updater/updater/source/src/main/java/org/selfbus/updater/tests/flashdiff/DecompressorListener.java rename to firmware_updater/updater/src/org/selfbus/updater/mode/differential/DecompressorListener.java index e0f08685..491d74d0 100644 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/tests/flashdiff/DecompressorListener.java +++ b/firmware_updater/updater/src/org/selfbus/updater/mode/differential/DecompressorListener.java @@ -1,5 +1,5 @@ -package org.selfbus.updater.tests.flashdiff; - -public interface DecompressorListener { - void flashPage(OldWindow oldPagesRam, FlashPage page); -} +package org.selfbus.updater.mode.differential; + +public interface DecompressorListener { + void flashPage(OldWindow oldPagesRam, FlashPage page); +} diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/tests/flashdiff/FlashDiff.java b/firmware_updater/updater/src/org/selfbus/updater/mode/differential/FlashDiff.java similarity index 60% rename from firmware_updater/updater/source/src/main/java/org/selfbus/updater/tests/flashdiff/FlashDiff.java rename to firmware_updater/updater/src/org/selfbus/updater/mode/differential/FlashDiff.java index e0aeef62..2791c6af 100644 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/tests/flashdiff/FlashDiff.java +++ b/firmware_updater/updater/src/org/selfbus/updater/mode/differential/FlashDiff.java @@ -1,251 +1,252 @@ -package org.selfbus.updater.tests.flashdiff; - -import org.selfbus.updater.BinImage; -import org.selfbus.updater.ConColors; -import org.selfbus.updater.UpdaterException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import tuwien.auto.calimero.KNXRemoteException; -import tuwien.auto.calimero.KNXTimeoutException; -import tuwien.auto.calimero.link.KNXLinkClosedException; -import tuwien.auto.calimero.mgmt.KNXDisconnectException; - -import java.util.ArrayList; -import java.util.List; -import java.util.zip.CRC32; - -public class FlashDiff { - private final static Logger logger = LoggerFactory.getLogger(FlashDiff.class.getName()); - private static final int MINIMUM_PATTERN_LENGTH = 6; // less that this is not efficient (metadata would be larger than data) - private static final int MAX_COPY_LENGTH = 2048-1; // 2^12 = 8 bits + 6 bits (remaining in CMD byte). Needs to match flash PAGE_SIZE? - private static final int MAX_LENGTH_SHORT = 64-1; // 2^6 = 6 bits (remaining in CMD byte) - - public static final int CMD_RAW = 0; - public static final int CMD_COPY = 0b10000000; - public static final int FLAG_SHORT = 0; - public static final int FLAG_LONG = 0b01000000; - public static final int ADDR_FROM_ROM = 0; - public static final int ADDR_FROM_RAM = 0b10000000; - - private int totalBytesTransferred = 0; - - enum SourceType { - FORWARD_ROM, - BACKWARD_RAM, - } - - static class SearchResult { - int offset; - int length; - SourceType sourceType = SourceType.FORWARD_ROM; - - public SearchResult(int logestCandidateSrcOffset, int logestCandidateLength) { - this.offset = logestCandidateSrcOffset; - this.length = logestCandidateLength; - } - - @Override - public String toString() { - return "SearchResult{" + - "offset=" + offset + - ", length=" + length + - ", sourceType=" + sourceType + - '}'; - } - } - - public SearchResult letLongestCommonBytes(byte[] ar1, byte[] ar2, int patternOffset, int oldDataMinimumAddr, int maxLength) { - // search as long as possible ar2[beginOffset..n] bytes (pattern) common with ar1[s..t], where n and t are up to length-1 of appropriate arrays and s is unknown - int logestCandidateSrcOffset = 0; - int logestCandidateLength = 0; - for (int i = oldDataMinimumAddr; i < ar1.length; i++) { - int j = 0; - while ((patternOffset + j < ar2.length) - && (i + j < ar1.length) - && (ar1[i + j] == ar2[patternOffset + j]) - && j < maxLength - ) { - j++; - } - if (j > logestCandidateLength) { - // found better (or first) solution - logestCandidateLength = j; - logestCandidateSrcOffset = i; - } - } - if (logestCandidateLength > 0) { - int firstDstPage = patternOffset/FlashPage.PAGE_SIZE; - int lastDstPage = (patternOffset+logestCandidateLength-1)/FlashPage.PAGE_SIZE; - if (lastDstPage != firstDstPage) { - // truncate to a single destination page - logestCandidateLength = (firstDstPage + 1) * FlashPage.PAGE_SIZE - patternOffset; - } - if (logestCandidateLength >= MINIMUM_PATTERN_LENGTH) { - logger.trace("{}", ar1[logestCandidateSrcOffset] & 0xff); - logger.trace("{}", ar1[logestCandidateSrcOffset + 1] & 0xff); - logger.trace("{}", ar1[logestCandidateSrcOffset + 2] & 0xff); - } - return new SearchResult(logestCandidateSrcOffset, logestCandidateLength); - } else { - return new SearchResult(0, 0); - } - } - - private int possiblyFinishRawBuffer(List rawBuffer, List outputDiffStream) { - int outSize; - if (!rawBuffer.isEmpty()) { - byte cmdByte = CMD_RAW; - if (rawBuffer.size() <= MAX_LENGTH_SHORT) { - debug("RAW BUFFER SHORT size=" + rawBuffer.size()); - cmdByte = (byte)(cmdByte | FLAG_SHORT); - cmdByte = (byte)(cmdByte | (rawBuffer.size() & 0b111111)); // command + 6 low bits of the length - outputDiffStream.add(cmdByte); - outputDiffStream.addAll(rawBuffer); - outSize = 1 + rawBuffer.size(); // 1 byte = cmd with short length included - rawBuffer.clear(); - return outSize; - } else { - debug("RAW BUFFER LONG size=" + rawBuffer.size()); - cmdByte = (byte)(cmdByte | FLAG_LONG); - cmdByte = (byte)(cmdByte | ((rawBuffer.size() >> 8) & 0b111111)); // command + 6 high bits of the length - byte lengthLowByte = (byte)(rawBuffer.size() & 0xff); // 8 low bits of the length - outputDiffStream.add(cmdByte); - outputDiffStream.add(lengthLowByte); - outputDiffStream.addAll(rawBuffer); - outSize = 2 + rawBuffer.size(); // 2 bytes = cmd (6bits of length) + length (8bits of length) - rawBuffer.clear(); - return outSize; - } - } else { - return 0; - } - } - - public int getTotalBytesTransferred() { - return totalBytesTransferred; - } - - public void generateDiff(BinImage img1Orig, BinImage img2, FlashProgrammer flashProgrammer) - throws InterruptedException, KNXTimeoutException, KNXLinkClosedException, KNXDisconnectException, KNXRemoteException, UpdaterException { - // TODO check if old image can be truncated for smaller new image, first test was fine - //BinImage img1 = new BinImage(img1Orig); // make copy to keep img1Orig untouched, this fails if arraycopy at 203, when new image is larger than old - BinImage img1 = new BinImage(img1Orig,img2.getBinData().length); // make copy to keep img1Orig untouched, ensure old bin buffer is same size as new bin file - - List outputDiffStream = new ArrayList<>(); - List rawBuffer = new ArrayList<>(); - OldWindow w = new OldWindow(); - CRC32 crc32Block = new CRC32(); - int i = 0; - int size = 0; - int pages = 0; - totalBytesTransferred = 0; - while (i < img2.getBinData().length) { - SearchResult rBackwardRamWindow = letLongestCommonBytes(w.getOldBinData(), img2.getBinData(), i, 0, MAX_COPY_LENGTH); - rBackwardRamWindow.sourceType = SourceType.BACKWARD_RAM; - //SearchResult rBackwardRamWindow = letLongestCommonBytes(img1.getBinData(), img2.getBinData(), i, 0); // in case we would have two flash banks, ie. full old image available - int currentPage = i / FlashPage.PAGE_SIZE; - int firstAddressInThisPage = currentPage * FlashPage.PAGE_SIZE; - SearchResult rForwardOldFlash = letLongestCommonBytes(img1.getBinData(), img2.getBinData(), i, 0, MAX_COPY_LENGTH); - rForwardOldFlash.sourceType = SourceType.FORWARD_ROM; - // which result is better, from FORWARD ROM or BACKWARD RAM? - SearchResult bestResult = (rForwardOldFlash.length > rBackwardRamWindow.length) ? rForwardOldFlash : rBackwardRamWindow; - if (bestResult.length >= MINIMUM_PATTERN_LENGTH) { - size += possiblyFinishRawBuffer(rawBuffer, outputDiffStream); - logger.trace("{} bestResult={}", String.format("%08x ", i), bestResult); - i += bestResult.length; - size += 5; - byte cmdByte = (byte)CMD_COPY; - if (bestResult.length <= MAX_LENGTH_SHORT) { - cmdByte = (byte)(cmdByte | FLAG_SHORT | (bestResult.length & 0b111111)); // command + 6 bits of the length - debug("@ b=%02X i=%d CMD_COPY", (cmdByte & 0xff), i); - outputDiffStream.add(cmdByte); - } else { - cmdByte = (byte)(cmdByte | FLAG_LONG | ((bestResult.length >> 8) & 0b111111)); // command + 6 bits from high byte of the length - debug("@ b=%02X i=%d CMD_COPY FLAG_LONG", (cmdByte & 0xff), i); - byte lengthLowByte = (byte)(bestResult.length & 0xff); // 8 low bits of the length - outputDiffStream.add(cmdByte); - outputDiffStream.add(lengthLowByte); - } - // 3 bytes are enough to address ROM or RAM buffer, the highest bit indicates ROM or RAM source - byte addr1 = (byte)(bestResult.offset & 0xff); // low byte - byte addr2 = (byte)((bestResult.offset >> 8) & 0xff); // middle byte - byte addr3 = (byte)((bestResult.offset >> 16) & 0xff); // high byte - int addrFromFlag = bestResult.sourceType == SourceType.BACKWARD_RAM ? ADDR_FROM_RAM : ADDR_FROM_ROM; - if (bestResult.sourceType == SourceType.BACKWARD_RAM) { - debug(" DO COPY FROM RAM l=%d sa=%08X", bestResult.length, bestResult.offset); - } else { - debug(" DO COPY FROM ROM l=%d sa=%08X", bestResult.length, bestResult.offset); - } - byte[] srcData = bestResult.sourceType == SourceType.BACKWARD_RAM ? w.getOldBinData() : img1.getBinData(); - for (int k = 0; k < bestResult.length; k++) { - if (k % 16 == 0) { - debug("\n "); - } - debug("%02X ", (srcData[bestResult.offset + k] & 0xff)); - } - addr3 = (byte)(addr3 | addrFromFlag); - outputDiffStream.add(addr3); - outputDiffStream.add(addr2); - outputDiffStream.add(addr1); - debug("\n"); - } - else { - logger.trace("{} RAW: {}", String.format("%08x", i), String.format("%02x", img2.getBinData()[i])); - - debug("@ b=%02X i=%d raw", (img2.getBinData()[i] & 0xff), i); - rawBuffer.add(img2.getBinData()[i]); - i++; - debug("\n"); - } - if (i%FlashPage.PAGE_SIZE == 0) { - // passed to new page - size += possiblyFinishRawBuffer(rawBuffer, outputDiffStream); - crc32Block.reset(); - crc32Block.update(img2.getBinData(), i - FlashPage.PAGE_SIZE, FlashPage.PAGE_SIZE); - //p = new FlashPage(img1.getBinData(), i); - - debug("# FLASH PAGE startAddrOfPageToBeFlashed=%08X", i*FlashPage.PAGE_SIZE); - for (int k = pages*FlashPage.PAGE_SIZE; (k < (pages+1)*FlashPage.PAGE_SIZE && k < img2.getBinData().length); k++) { - if (k % 16 == 0) { - debug("\n "); - } - debug(String.format("%02X ", (img2.getBinData()[k] & 0xff))); - } - debug("\n"); - - System.out.print("Page " + pages + ", "); - flashProgrammer.sendCompressedPage(outputDiffStream, crc32Block.getValue()); - outputDiffStream.clear(); - pages++; - // emulate we have loaded the original page from ROM to RAM and written new page to ROM - w.fillNextPage(img1.getBinData(), i - FlashPage.PAGE_SIZE); // backup old data from ROM to RAM - System.arraycopy(img2.getBinData(), i - FlashPage.PAGE_SIZE, img1.getBinData(), i - FlashPage.PAGE_SIZE, FlashPage.PAGE_SIZE); // emulatae write of new data to ROM - } - } - size += possiblyFinishRawBuffer(rawBuffer, outputDiffStream); - if (!outputDiffStream.isEmpty()) { - crc32Block.reset(); - crc32Block.update(img2.getBinData(), img2.getBinData().length - (img2.getBinData().length % FlashPage.PAGE_SIZE), img2.getBinData().length % FlashPage.PAGE_SIZE); - - debug("# FLASH PAGE startAddrOfPageToBeFlashed=%08X", i*FlashPage.PAGE_SIZE); - debug("\n "); - for (int k = pages*FlashPage.PAGE_SIZE; (k < (pages+1)*FlashPage.PAGE_SIZE && k < img2.getBinData().length); k++) { - debug("%02X ", (img2.getBinData()[k] & 0xff)); - if (k % 16 == 0) { - debug("\n "); - } - } - debug("\n"); - - System.out.print("Page " + pages + ", "); - flashProgrammer.sendCompressedPage(outputDiffStream, crc32Block.getValue()); - totalBytesTransferred = size; - logger.debug("OK! Total diff stream length={}{}{} bytes", ConColors.BRIGHT_GREEN, size, ConColors.RESET); - } - //dumpSideBySide(img1, img2); - } - - protected void debug(String format, Object... args) { - //System.out.format(format, args); - } -} +package org.selfbus.updater.mode.differential; + +import org.selfbus.updater.BinImage; +import org.selfbus.updater.UpdaterException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tuwien.auto.calimero.KNXTimeoutException; +import tuwien.auto.calimero.link.KNXLinkClosedException; + +import java.util.ArrayList; +import java.util.List; +import java.util.zip.CRC32; + +public class FlashDiff { + private final static Logger logger = LoggerFactory.getLogger(FlashDiff.class); + private static final int MINIMUM_PATTERN_LENGTH = 6; // less that this is not efficient (metadata would be larger than data) + private static final int MAX_COPY_LENGTH = 2048-1; // 2^12 = 8 bits + 6 bits (remaining in CMD byte). Needs to match flash PAGE_SIZE? + private static final int MAX_LENGTH_SHORT = 64-1; // 2^6 = 6 bits (remaining in CMD byte) + + public static final int CMD_RAW = 0; + public static final int CMD_COPY = 0b10000000; + public static final int FLAG_SHORT = 0; + public static final int FLAG_LONG = 0b01000000; + public static final int ADDR_FROM_ROM = 0; + public static final int ADDR_FROM_RAM = 0b10000000; + + private int totalBytesTransferred = 0; + + enum SourceType { + FORWARD_ROM, + BACKWARD_RAM, + } + + static class SearchResult { + final int offset; + final int length; + SourceType sourceType = SourceType.FORWARD_ROM; + + public SearchResult(int longestCandidateSrcOffset, int longestCandidateLength) { + this.offset = longestCandidateSrcOffset; + this.length = longestCandidateLength; + } + + @Override + public String toString() { + return "SearchResult{" + + "offset=" + offset + + ", length=" + length + + ", sourceType=" + sourceType + + '}'; + } + } + + private SearchResult letLongestCommonBytes(byte[] ar1, byte[] ar2, int patternOffset, int oldDataMinimumAddr, int maxLength) { + // search as long as possible ar2[beginOffset...n] bytes (pattern) common with ar1[s...t], where n and t are up to length-1 of appropriate arrays and s is unknown + int longestCandidateSrcOffset = 0; + int longestCandidateLength = 0; + for (int i = oldDataMinimumAddr; i < ar1.length; i++) { + int j = 0; + while ((patternOffset + j < ar2.length) + && (i + j < ar1.length) + && (ar1[i + j] == ar2[patternOffset + j]) + && j < maxLength + ) { + j++; + } + if (j > longestCandidateLength) { + // found better (or first) solution + longestCandidateLength = j; + longestCandidateSrcOffset = i; + } + } + if (longestCandidateLength > 0) { + int firstDstPage = patternOffset/FlashPage.PAGE_SIZE; + int lastDstPage = (patternOffset+longestCandidateLength-1)/FlashPage.PAGE_SIZE; + if (lastDstPage != firstDstPage) { + // truncate to a single destination page + longestCandidateLength = (firstDstPage + 1) * FlashPage.PAGE_SIZE - patternOffset; + } + if (longestCandidateLength >= MINIMUM_PATTERN_LENGTH) { + logger.trace("{} {} {}", ar1[longestCandidateSrcOffset] & 0xff, ar1[longestCandidateSrcOffset + 1] & 0xff, + ar1[longestCandidateSrcOffset + 2] & 0xff); + } + return new SearchResult(longestCandidateSrcOffset, longestCandidateLength); + } else { + return new SearchResult(0, 0); + } + } + + private int possiblyFinishRawBuffer(List rawBuffer, List outputDiffStream) { + if (rawBuffer.isEmpty()) { + return 0; + } + + int outSize; + byte cmdByte; + if (rawBuffer.size() <= MAX_LENGTH_SHORT) { + logger.trace("RAW BUFFER SHORT size={}", rawBuffer.size()); + cmdByte = (byte)(CMD_RAW | FLAG_SHORT); + cmdByte = (byte)(cmdByte | (rawBuffer.size() & 0b111111)); // command + 6 low bits of the length + outputDiffStream.add(cmdByte); + outSize = 1; // 1 byte = cmd with short length included + } + else { + logger.trace("RAW BUFFER LONG size={}", rawBuffer.size()); + cmdByte = (byte)(CMD_RAW | FLAG_LONG); + cmdByte = (byte)(cmdByte | ((rawBuffer.size() >> 8) & 0b111111)); // command + 6 high bits of the length + byte lengthLowByte = (byte)(rawBuffer.size() & 0xff); // 8 low bits of the length + outputDiffStream.add(cmdByte); + outputDiffStream.add(lengthLowByte); + outSize = 2; // 2 bytes = cmd (6bits of length) + length (8bits of length) + } + outputDiffStream.addAll(rawBuffer); + outSize += rawBuffer.size(); + rawBuffer.clear(); + return outSize; + } + + public int getTotalBytesTransferred() { + return totalBytesTransferred; + } + + public void generateDiff(BinImage img1Orig, BinImage img2, FlashProgrammer flashProgrammer) + throws InterruptedException, KNXTimeoutException, KNXLinkClosedException, UpdaterException { + // TODO check if old image can be truncated for smaller new image, first test was fine + //BinImage img1 = new BinImage(img1Orig); // make copy to keep img1Orig untouched, this fails if arraycopy at 203, when new image is larger than old + BinImage img1 = new BinImage(img1Orig,img2.getBinData().length); // make copy to keep img1Orig untouched, ensure old bin buffer is same size as new bin file + + List outputDiffStream = new ArrayList<>(); + List rawBuffer = new ArrayList<>(); + OldWindow w = new OldWindow(); + CRC32 crc32Block = new CRC32(); + int i = 0; + int size = 0; + int pages = 0; + totalBytesTransferred = 0; + while (i < img2.getBinData().length) { + SearchResult rBackwardRamWindow = letLongestCommonBytes(w.getOldBinData(), img2.getBinData(), i, 0, MAX_COPY_LENGTH); + rBackwardRamWindow.sourceType = SourceType.BACKWARD_RAM; + /* + SearchResult rBackwardRamWindow = letLongestCommonBytes(img1.getBinData(), img2.getBinData(), i, 0); // in case we would have two flash banks, i.e. full old image available + int currentPage = i / FlashPage.PAGE_SIZE; + int firstAddressInThisPage = currentPage * FlashPage.PAGE_SIZE; + */ + SearchResult rForwardOldFlash = letLongestCommonBytes(img1.getBinData(), img2.getBinData(), i, 0, MAX_COPY_LENGTH); + rForwardOldFlash.sourceType = SourceType.FORWARD_ROM; + // which result is better, from FORWARD ROM or BACKWARD RAM? + SearchResult bestResult = (rForwardOldFlash.length > rBackwardRamWindow.length) ? rForwardOldFlash : rBackwardRamWindow; + if (bestResult.length >= MINIMUM_PATTERN_LENGTH) { + size += possiblyFinishRawBuffer(rawBuffer, outputDiffStream); + logger.trace("{} bestResult={}", String.format("%08x ", i), bestResult); + i += bestResult.length; + size += 1; + byte cmdByte = (byte)CMD_COPY; + if (bestResult.length <= MAX_LENGTH_SHORT) { + cmdByte = (byte)(cmdByte | FLAG_SHORT | (bestResult.length & 0b111111)); // command + 6 bits of the length + logger.trace("@ {} CMD_COPY", String.format("b=%02X i=%d", (cmdByte & 0xff), i)); + outputDiffStream.add(cmdByte); + } else { + cmdByte = (byte)(cmdByte | FLAG_LONG | ((bestResult.length >> 8) & 0b111111)); // command + 6 bits from high byte of the length + logger.trace("@ {} CMD_COPY FLAG_LONG", String.format("b=%02X i=%d", (cmdByte & 0xff), i)); + byte lengthLowByte = (byte)(bestResult.length & 0xff); // 8 low bits of the length + outputDiffStream.add(cmdByte); + size += 1; + outputDiffStream.add(lengthLowByte); + } + // 3 bytes are enough to address ROM or RAM buffer, the highest bit indicates ROM or RAM source + byte addr1 = (byte)(bestResult.offset & 0xff); // low byte + byte addr2 = (byte)((bestResult.offset >> 8) & 0xff); // middle byte + byte addr3 = (byte)((bestResult.offset >> 16) & 0xff); // high byte + int addrFromFlag = bestResult.sourceType == SourceType.BACKWARD_RAM ? ADDR_FROM_RAM : ADDR_FROM_ROM; + String location; + if (bestResult.sourceType == SourceType.BACKWARD_RAM) { + location = "RAM"; + } else { + location = "ROM"; + } + logger.trace("DO COPY FROM {} {}", location, String.format("l=%d sa=%08X", bestResult.length, bestResult.offset)); + byte[] srcData = bestResult.sourceType == SourceType.BACKWARD_RAM ? w.getOldBinData() : img1.getBinData(); + StringBuilder traceMessage = new StringBuilder(); + for (int k = 0; k < bestResult.length; k++) { + if (k % 16 == 0) { + logger.trace(traceMessage.toString()); + traceMessage = new StringBuilder(); + } + traceMessage.append(String.format("%02X ", (srcData[bestResult.offset + k] & 0xff))); + } + logger.trace(traceMessage.toString()); + + size += 3; + addr3 = (byte)(addr3 | addrFromFlag); + outputDiffStream.add(addr3); + outputDiffStream.add(addr2); + outputDiffStream.add(addr1); + } + else { + logger.trace("{} raw", String.format("%08x RAW: %02x@ b=%02X i=%d", + i, img2.getBinData()[i], (img2.getBinData()[i] & 0xff), i)); + rawBuffer.add(img2.getBinData()[i]); + i++; + } + if (i%FlashPage.PAGE_SIZE == 0) { + // passed to new page + size += possiblyFinishRawBuffer(rawBuffer, outputDiffStream); + crc32Block.reset(); + crc32Block.update(img2.getBinData(), i - FlashPage.PAGE_SIZE, FlashPage.PAGE_SIZE); + //p = new FlashPage(img1.getBinData(), i); + + logger.trace("# FLASH PAGE startAddrOfPageToBeFlashed={}", + String.format("%08X", i * FlashPage.PAGE_SIZE)); + StringBuilder traceMessage = new StringBuilder(); + for (int k = pages*FlashPage.PAGE_SIZE; (k < (pages+1)*FlashPage.PAGE_SIZE && k < img2.getBinData().length); k++) { + if (k % 16 == 0) { + logger.trace(traceMessage.toString()); + traceMessage = new StringBuilder(); + } + traceMessage.append(String.format("%02X ", (img2.getBinData()[k] & 0xff))); + } + logger.trace(traceMessage.toString()); + + logger.trace("Page {}, ",pages); + flashProgrammer.sendCompressedPage(outputDiffStream, crc32Block.getValue()); + outputDiffStream.clear(); + pages++; + // emulate we have loaded the original page from ROM to RAM and written new page to ROM + w.fillNextPage(img1.getBinData(), i - FlashPage.PAGE_SIZE); // backup old data from ROM to RAM + System.arraycopy(img2.getBinData(), i - FlashPage.PAGE_SIZE, img1.getBinData(), i - FlashPage.PAGE_SIZE, FlashPage.PAGE_SIZE); // emulate write of new data to ROM + } + } + size += possiblyFinishRawBuffer(rawBuffer, outputDiffStream); + if (!outputDiffStream.isEmpty()) { + crc32Block.reset(); + crc32Block.update(img2.getBinData(), img2.getBinData().length - (img2.getBinData().length % FlashPage.PAGE_SIZE), img2.getBinData().length % FlashPage.PAGE_SIZE); + logger.trace("# FLASH PAGE startAddrOfPageToBeFlashed={}", String.format("%08X", i * FlashPage.PAGE_SIZE)); + StringBuilder traceMessage = new StringBuilder(); + for (int k = pages*FlashPage.PAGE_SIZE; (k < (pages+1)*FlashPage.PAGE_SIZE && k < img2.getBinData().length); k++) { + traceMessage.append(String.format("%02X ", (img2.getBinData()[k] & 0xff))); + if (k % 16 == 0) { + logger.trace(traceMessage.toString()); + traceMessage = new StringBuilder(); + } + } + logger.trace(traceMessage.toString()); + + logger.trace("Page {}, ", pages); + flashProgrammer.sendCompressedPage(outputDiffStream, crc32Block.getValue()); + totalBytesTransferred = size; + logger.trace("OK! Total diff stream length={} bytes", size); + } + //dumpSideBySide(img1, img2); + } +} diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/tests/flashdiff/FlashPage.java b/firmware_updater/updater/src/org/selfbus/updater/mode/differential/FlashPage.java similarity index 73% rename from firmware_updater/updater/source/src/main/java/org/selfbus/updater/tests/flashdiff/FlashPage.java rename to firmware_updater/updater/src/org/selfbus/updater/mode/differential/FlashPage.java index ce42a945..87630ddd 100644 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/tests/flashdiff/FlashPage.java +++ b/firmware_updater/updater/src/org/selfbus/updater/mode/differential/FlashPage.java @@ -1,18 +1,18 @@ -package org.selfbus.updater.tests.flashdiff; - -import org.selfbus.updater.Mcu; - -import java.util.Arrays; - -public class FlashPage { - public static final int PAGE_SIZE = Mcu.FLASH_PAGE_SIZE; - private byte[] old; // old content before patching - - public FlashPage(byte[] fromByteArray, int begin) { - old = Arrays.copyOfRange(fromByteArray, begin, begin + PAGE_SIZE); - } - - public byte[] getOldBinData() { - return old; - } -} +package org.selfbus.updater.mode.differential; + +import org.selfbus.updater.Mcu; + +import java.util.Arrays; + +public class FlashPage { + public static final int PAGE_SIZE = Mcu.FLASH_PAGE_SIZE; + private final byte[] old; // old content before patching + + public FlashPage(byte[] fromByteArray, int begin) { + old = Arrays.copyOfRange(fromByteArray, begin, begin + PAGE_SIZE); + } + + public byte[] getOldBinData() { + return old; + } +} diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/tests/flashdiff/FlashProgrammer.java b/firmware_updater/updater/src/org/selfbus/updater/mode/differential/FlashProgrammer.java similarity index 60% rename from firmware_updater/updater/source/src/main/java/org/selfbus/updater/tests/flashdiff/FlashProgrammer.java rename to firmware_updater/updater/src/org/selfbus/updater/mode/differential/FlashProgrammer.java index dc6e4e57..b49b4ed1 100644 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/tests/flashdiff/FlashProgrammer.java +++ b/firmware_updater/updater/src/org/selfbus/updater/mode/differential/FlashProgrammer.java @@ -1,14 +1,12 @@ -package org.selfbus.updater.tests.flashdiff; - -import org.selfbus.updater.UpdaterException; -import tuwien.auto.calimero.KNXRemoteException; -import tuwien.auto.calimero.KNXTimeoutException; -import tuwien.auto.calimero.link.KNXLinkClosedException; -import tuwien.auto.calimero.mgmt.KNXDisconnectException; - -import java.util.List; - -public interface FlashProgrammer { - - void sendCompressedPage(List outputDiffStream, long crc32) throws InterruptedException, KNXTimeoutException, KNXLinkClosedException, KNXRemoteException, KNXDisconnectException, UpdaterException; -} +package org.selfbus.updater.mode.differential; + +import org.selfbus.updater.UpdaterException; +import tuwien.auto.calimero.KNXTimeoutException; +import tuwien.auto.calimero.link.KNXLinkClosedException; + +import java.util.List; + +public interface FlashProgrammer { + + void sendCompressedPage(List outputDiffStream, long crc32) throws InterruptedException, KNXTimeoutException, KNXLinkClosedException, UpdaterException; +} diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/tests/flashdiff/OldWindow.java b/firmware_updater/updater/src/org/selfbus/updater/mode/differential/OldWindow.java similarity index 82% rename from firmware_updater/updater/source/src/main/java/org/selfbus/updater/tests/flashdiff/OldWindow.java rename to firmware_updater/updater/src/org/selfbus/updater/mode/differential/OldWindow.java index eaa2f11a..a3cce585 100644 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/tests/flashdiff/OldWindow.java +++ b/firmware_updater/updater/src/org/selfbus/updater/mode/differential/OldWindow.java @@ -1,20 +1,20 @@ -package org.selfbus.updater.tests.flashdiff; - -public class OldWindow { - public static final int PAGES = 2; - private byte[] old = new byte[PAGES * FlashPage.PAGE_SIZE]; - - public OldWindow() { - } - - public byte[] getOldBinData() { - return old; - } - - public void fillNextPage(byte[] binData, int pageBaseAddress) { - // shift old data one page back, will leave one "empty" page at the end - System.arraycopy( old, FlashPage.PAGE_SIZE, old, 0, old.length - FlashPage.PAGE_SIZE ); - // fill the last (current) page (may not be while page when EOF) at the end - System.arraycopy( binData, pageBaseAddress, old, old.length - FlashPage.PAGE_SIZE , Math.min(FlashPage.PAGE_SIZE, binData.length - pageBaseAddress) ); - } -} +package org.selfbus.updater.mode.differential; + +public class OldWindow { + public static final int PAGES = 2; + private final byte[] old = new byte[PAGES * FlashPage.PAGE_SIZE]; + + public OldWindow() { + } + + public byte[] getOldBinData() { + return old; + } + + public void fillNextPage(byte[] binData, int pageBaseAddress) { + // shift old data one page back, will leave one "empty" page at the end + System.arraycopy( old, FlashPage.PAGE_SIZE, old, 0, old.length - FlashPage.PAGE_SIZE ); + // fill the last (current) page (may not be while page when EOF) at the end + System.arraycopy( binData, pageBaseAddress, old, old.length - FlashPage.PAGE_SIZE , Math.min(FlashPage.PAGE_SIZE, binData.length - pageBaseAddress) ); + } +} diff --git a/firmware_updater/updater/src/org/selfbus/updater/progress/AnsiCursor.java b/firmware_updater/updater/src/org/selfbus/updater/progress/AnsiCursor.java new file mode 100644 index 00000000..5694e0f3 --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/progress/AnsiCursor.java @@ -0,0 +1,18 @@ +package org.selfbus.updater.progress; + +public final class AnsiCursor { + @SuppressWarnings("unused") + private AnsiCursor() {} // avoids instance creation + + @SuppressWarnings("SameReturnValue") + public static String on() { + // cursor on + return "\033[?25h"; + } + + @SuppressWarnings("SameReturnValue") + public static String off() { + // cursor off + return "\033[?25l"; + } +} diff --git a/firmware_updater/updater/src/org/selfbus/updater/progress/ProgressInfo.java b/firmware_updater/updater/src/org/selfbus/updater/progress/ProgressInfo.java new file mode 100644 index 00000000..7da717d3 --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/progress/ProgressInfo.java @@ -0,0 +1,115 @@ +package org.selfbus.updater.progress; + +import org.fusesource.jansi.Ansi; + +import static org.fusesource.jansi.Ansi.ansi; +import static org.selfbus.updater.logging.Color.*; + +public class ProgressInfo { + private long startTimeMs; + private long lastUpdateTimeMs; + private float minBytesPerSecond; + private float maxBytesPerSecond; + private float currentBytesPerSecond; + private long totalBytes; + private long bytesDone; + private float percentageDone; + + @SuppressWarnings("unused") + private ProgressInfo() {} + + public ProgressInfo(long totalByteCount) { + minBytesPerSecond = -1.0f; + maxBytesPerSecond = -1.0f; + currentBytesPerSecond = -1.0f; + totalBytes = totalByteCount; + bytesDone = 0; + startTimeMs = System.currentTimeMillis(); + lastUpdateTimeMs = startTimeMs; + } + + public void update(long byteCount) { + // logging of connection speed + long duration = System.currentTimeMillis() - lastUpdateTimeMs; + currentBytesPerSecond = (float) byteCount / (duration / 1000.0f); + lastUpdateTimeMs = System.currentTimeMillis(); + + if (minBytesPerSecond < 0) { + minBytesPerSecond = currentBytesPerSecond; + } + + if (maxBytesPerSecond < 0) { + maxBytesPerSecond = currentBytesPerSecond; + } + + minBytesPerSecond = Math.min(minBytesPerSecond, currentBytesPerSecond); + maxBytesPerSecond = Math.max(maxBytesPerSecond, currentBytesPerSecond); + bytesDone += byteCount; + percentageDone = 100.0f * (bytesDone) / getTotalBytes(); + } + + private String getSpeed(boolean averageSpeed) { + float bytesPerSecond; + if (averageSpeed) { + long duration = getLastUpdateTimeMs() - getStartTimeMs(); + bytesPerSecond = (float) getBytesDone() / (duration / 1000.0f); + } + else { + bytesPerSecond = getCurrentBytesPerSecond(); + } + Ansi.Color color; + if (bytesPerSecond >= 50.0) { + color = OK; + } + else { + color = INFO; + } + return String.format("%s%5.1f%s", ansi().fgBright(color), bytesPerSecond, ansi().reset()); + } + + public String getHeader() { + return " Done Speed Avg Min Max Time"; + } + + @Override + public String toString() { + return String.format("%5.1f%% %s %s %5.1f %5.1f %tM:% (cursorPool.length - 1)) { + reset(); + } + char nextCursor = cursorPool[index]; + index++; + + return nextCursor; + } + + public static void setBlank() { + index = 0; + } +} diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/upd/UDPProtocolVersion.java b/firmware_updater/updater/src/org/selfbus/updater/upd/UDPProtocolVersion.java similarity index 100% rename from firmware_updater/updater/source/src/main/java/org/selfbus/updater/upd/UDPProtocolVersion.java rename to firmware_updater/updater/src/org/selfbus/updater/upd/UDPProtocolVersion.java diff --git a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/upd/UDPResult.java b/firmware_updater/updater/src/org/selfbus/updater/upd/UDPResult.java similarity index 86% rename from firmware_updater/updater/source/src/main/java/org/selfbus/updater/upd/UDPResult.java rename to firmware_updater/updater/src/org/selfbus/updater/upd/UDPResult.java index d300840f..c0d927ab 100644 --- a/firmware_updater/updater/source/src/main/java/org/selfbus/updater/upd/UDPResult.java +++ b/firmware_updater/updater/src/org/selfbus/updater/upd/UDPResult.java @@ -5,7 +5,8 @@ /** * Implementation of the UPD/UDP protocol result commands - * see /Bus-Updater/inc/upd_protocol.h for details + *

+ * see /firmware_updater/bootloader/inc/upd_protocol.h for details */ public enum UDPResult { /** Flash (IAP) Command is executed successfully */ @@ -34,12 +35,12 @@ public enum UDPResult { IAP_BUSY((byte)0x74, "Flash (IAP) Flash programming hardware interface is busy.", true), /** Unknown IAP_Status */ - UDP_IAP_UNKNOWN((byte)0x73, "Unknown IAP_Status", true), + IAP_UNKNOWN((byte)0x73, "Unknown IAP_Status", true), /** Command is not defined */ UNKNOWN_COMMAND((byte)0x5f, "Command unknown", true), /** CRC calculated on the device and by the updater don't match */ - CRC_ERROR((byte)0x5e, "CRC error, try option -full for a clean and full flash", true), + CRC_ERROR((byte)0x5e, "CRC error, try option --full for a clean and full flash", true), /** Specified address cannot be programmed */ ADDRESS_NOT_ALLOWED_TO_FLASH((byte)0x5d, "Address not allowed to flash", true), /** The specified sector cannot be erased */ @@ -51,7 +52,7 @@ public enum UDPResult { /** The programmed application is not startable */ APPLICATION_NOT_STARTABLE((byte)0x59, "Application not startable", true), /** The device is locked */ - DEVICE_LOCKED((byte)0x58, "Device locked. Programming mode on device must be active!", true), + DEVICE_LOCKED((byte)0x58, "Device locked. Device must first be unlocked with UPDCommand::UNLOCK_DEVICE!", true), /** UID sent to unlock the device is invalid */ UID_MISMATCH((byte)0x57, "UID mismatch", true), /** Flash page erase failed */ @@ -60,7 +61,7 @@ public enum UDPResult { /** Data received is invalid */ INVALID_DATA((byte)0x55, "Data received is invalid", true), /** No data received in telegram */ - UDP_NO_DATA((byte)0x54, "No data received in telegram", true), + NO_DATA((byte)0x54, "No data received in telegram", true), /** Flash page could not be programmed */ FLASH_ERROR((byte)0x53, "Flash page could not be programmed", true), @@ -68,7 +69,7 @@ public enum UDPResult { PAGE_NOT_ALLOWED_TO_ERASE((byte)0x52, "Flash page not allowed to erase", true), /** Address range not allowed to erase */ - UDP_ADDRESS_RANGE_NOT_ALLOWED_TO_ERASE((byte)0x51, "Address range not allowed to erase", true), + ADDRESS_RANGE_NOT_ALLOWED_TO_ERASE((byte)0x51, "Address range not allowed to erase", true), /** Number of bytes received with @ref UPD_SEND_DATA is lower than number of bytes to program with @ref UPD_PROGRAM */ BYTECOUNT_RECEIVED_TOO_LOW((byte)0x50, "Number of bytes received is lower than number of bytes to program", true), @@ -89,17 +90,17 @@ public enum UDPResult { } /** ID of the UDPResult */ - public final byte id; + private final byte id; /** Simple description of the UDPResult */ private final String description; /** True if the UPDResult represents an error */ private final boolean isError; /** - * Create a UDPResult instance from given id, description and error state + * Create a UDPResult from given id, description and error state * @param id ID of the UDPResult * @param description Description of the UDPResult - * @param isError Set to true, if the UPDResult represents a error, otherwise set to false + * @param isError Set to true, if the UPDResult represents an error, otherwise set to false */ UDPResult(byte id, String description, boolean isError) { this.id = id; @@ -108,22 +109,17 @@ public enum UDPResult { } /** - * Create a UPDResult instance from a given ID + * Create a UPDResult from a given ID * @param index ID of the UPDResult * @return Instance of the UPDResult from the given ID */ - public static UDPResult valueOfIndex(byte index) { - for (UDPResult e: values()) { - if (e.id == index) { - return e; - } - } - return INVALID; + public static UDPResult valueOf(byte index) { + return BY_INDEX.getOrDefault(index, UDPResult.INVALID); } @Override public String toString() { - return this.description; + return String.format("%s (%s)", this.description, super.toString()); } /** @@ -134,5 +130,9 @@ public boolean isError() { return this.isError; } + public String getDescription() { + return description; + } + } diff --git a/firmware_updater/updater/src/org/selfbus/updater/upd/UPDCommand.java b/firmware_updater/updater/src/org/selfbus/updater/upd/UPDCommand.java new file mode 100644 index 00000000..a9192974 --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/upd/UPDCommand.java @@ -0,0 +1,182 @@ +package org.selfbus.updater.upd; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; + +/** + * Implementation of the UPD/UDP protocol control commands + *

+ * See {@code /firmware_updater/bootloader/inc/upd_protocol.h} for details + *

+ */ +public enum UPDCommand { + /** + * Marks an invalid {@link UPDCommand} code + */ + INVALID((byte) 0, "INVALID"), + /** + * Copy ((data[0] & 0x0f)-1) bytes to ramBuffer starting from address data[3]. + *

Device must be unlocked.

+ */ + SEND_DATA((byte)0xef, "SEND_DATA"), + /** + * Copy count (data[3-6]) bytes from ramBuffer to address (data[7-10]) in flash buffer, crc in data[11-14]. + *

Device must be unlocked.

+ */ + PROGRAM((byte)0xee, "PROGRAM"), + /** + * Flash an application boot descriptor block. + *

Device must be unlocked.

+ */ + UPDATE_BOOT_DESC((byte)0xed, "UPDATE_BOOT_DESC"), + /** + * Copy bytes from telegram (data) to ramBuffer with differential method. + *

Device must be unlocked.

+ */ + SEND_DATA_TO_DECOMPRESS((byte)0xec, "SEND_DATA_TO_DECOMPRESS"), + /** + * Flash bytes from ramBuffer to flash with differential method. + *

Device must be unlocked.

+ */ + PROGRAM_DECOMPRESSED_DATA((byte)0xeb, "PROGRAM_DECOMPRESSED_DATA"), + /** + * Erase the entire flash area excluding the bootloader itself. + *

Device must be unlocked.

+ */ + ERASE_COMPLETE_FLASH((byte)0xea, "ERASE_COMPLETE_FLASH"), + /** + * Erase flash from given start address to end address (start: data[3-6] end: data[7-10]). + *

Device must be unlocked.

+ */ + ERASE_ADDRESS_RANGE((byte)0xe9, "ERASE_ADDRESS_RANGE"), + /** + *

Warning: Not implemented.

+ * Return bytes from flash at given address. + */ + REQ_DATA((byte)0xe8, "REQ_DATA"), + /** + * DUMP the flash of a given address range (data[0-3] - data[4-7]) to the serial port of the mcu. + *

Note: Works only with DEBUG version of the bootloader.

+ */ + DUMP_FLASH((byte)0xe7, "DUMP_FLASH"), + /** + * Request some statistic data for the active connection. + */ + REQUEST_STATISTIC((byte)0xdf, "STATISTIC_REQUEST"), + /** + * Response for {@link #REQUEST_STATISTIC} containing the statistic data. + */ + RESPONSE_STATISTIC((byte)0xde, "STATISTIC_RESPONSE"), + /** + * Response containing the last error. + */ + SEND_LAST_ERROR((byte)0xdc, "SEND_LAST_ERROR"), + /** + * Unlock the device for operations, which are only allowed on an unlocked device. + */ + UNLOCK_DEVICE((byte)0xbf, "UNLOCK_DEVICE"), + /** + * Request the 12 byte shorten UID (GUID) of the mcu. + *

Device must be unlocked.

+ */ + REQUEST_UID((byte)0xbe, "REQUEST_UID"), + /** + * Response for {@link #REQUEST_UID} containing the first 12 bytes of the UID. + */ + RESPONSE_UID((byte)0xbd, "RESPONSE_UID"), + /** + * Request address of AppVersion string. + */ + APP_VERSION_REQUEST((byte)0xbc, "APP_VERSION_REQUEST"), + /** + * Response for {@link #APP_VERSION_REQUEST} containing the application version string. + */ + APP_VERSION_RESPONSE((byte)0xbb, "APP_VERSION_RESPONSE"), + /* + * Reset the device. + *

Device must be unlocked.

+ */ + //RESET((byte) 35, "RESET"), + /** + * Request the application boot descriptor block. + *

Device must be unlocked.

+ */ + REQUEST_BOOT_DESC((byte)0xba, "REQUEST_BOOT_DESC"), + /** + * Response for {@link #REQUEST_BOOT_DESC} containing the application boot descriptor block. + */ + RESPONSE_BOOT_DESC((byte)0xb9, "RESPONSE_BOOT_DESC"), + /** + * Request the bootloader's identity. + *

Device must be unlocked.

+ */ + REQUEST_BL_IDENTITY((byte)0xb8, "REQUEST_BL_IDENTITY"), + /** + * Response for {@link #REQUEST_BL_IDENTITY} containing the identity. + */ + RESPONSE_BL_IDENTITY((byte)0xb7, "RESPONSE_BL_IDENTITY"), + /** + * Negative response for {@link #REQUEST_BL_IDENTITY} + * containing the minimum required major and minor version of the Selfbus Updater. + */ + RESPONSE_BL_VERSION_MISMATCH((byte)0xb6, "RESPONSE_BL_VERSION_MISMATCH"), + /** + *

Warning: Not implemented.

+ */ + SET_EMULATION((byte)0x01, "SET_EMULATION"); + + private static final Map BY_INDEX = new HashMap<>(); + static { + for (UPDCommand e: values()) { + BY_INDEX.put(e.id, e); + } + } + private static final Logger logger = LoggerFactory.getLogger(UPDCommand.class); + private final byte id; + private final String description; + + UPDCommand(byte id, String description) { + this.id = id; + this.description = description; + } + + public static UPDCommand valueOf(byte index) { + return BY_INDEX.getOrDefault(index, UPDCommand.INVALID); + } + + public byte toByte() { + return id; + } + + //todo get rid of this stupid "helper" method (needs a little bit more refactoring) + public static UPDCommand tryFromByteArray(byte[] bytes) { + try { + return fromByteArray(bytes); + } + catch (UPDProtocolException e) { + logger.error("Failed with {}", e.getMessage()); + logger.error("bytes: {}", bytes); + return UPDCommand.INVALID; + } + } + + public static UPDCommand fromByteArray(byte[] bytes) throws UPDProtocolException { + if (bytes == null) { + throw new UPDProtocolException("bytes==null"); + } + + if (bytes.length < UPDProtocol.getCommandPosition() - 1) { + throw new UPDProtocolException(String.format("Too few bytes. Expected #%d called with #%d", + bytes.length, UPDProtocol.getCommandPosition() - 1)); + } + return UPDCommand.valueOf(bytes[UPDProtocol.getCommandPosition()]); + } + + @Override + public String toString() { + return String.format("%s", this.description); + } +} diff --git a/firmware_updater/updater/src/org/selfbus/updater/upd/UPDProtocol.java b/firmware_updater/updater/src/org/selfbus/updater/upd/UPDProtocol.java new file mode 100644 index 00000000..a0202fd4 --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/upd/UPDProtocol.java @@ -0,0 +1,83 @@ +package org.selfbus.updater.upd; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.fusesource.jansi.Ansi.*; +import static org.selfbus.updater.logging.Color.*; + +/** + * Implementation of the UPD/UDP protocol handling + */ +public final class UPDProtocol { + @SuppressWarnings("unused") + private UPDProtocol() {} + + private static final Logger logger = LoggerFactory.getLogger(UPDProtocol.class); + + private static final int COMMAND_POSITION = 2; + public static final int DATA_POSITION = 3; + /** + * uid/guid length of the mcu used for unlocking/flashing. + */ + public static final int UID_LENGTH_USED = 12; + /** + * uid/guid length of the mcu. + */ + public static final int UID_LENGTH_MAX = 16; + + public static UDPResult checkResult(byte[] result) { + return checkResult(result, true); + } + + public static UDPResult checkResult(byte[] result, boolean verbose) { + UPDCommand command = UPDCommand.valueOf(result[getCommandPosition()]); + if (command != UPDCommand.SEND_LAST_ERROR) { + logger.error("checkResult called on other than {}, ", UPDCommand.SEND_LAST_ERROR); + logger.error(" result[{}]=0x{}", getCommandPosition(), String.format("%02X", result[getCommandPosition()])); + return UDPResult.INVALID; + } + + UDPResult udpResult = UDPResult.valueOf(result[DATA_POSITION]); + if (udpResult.isError()) { + logger.error("{}{}{} ({})", ansi().fgBright(WARN), udpResult.getDescription(), ansi().reset(), udpResult.name()); + } else { + if (verbose) { + logger.trace("done ({})", udpResult.name()); + } + } + return udpResult; + } + + public static byte[] uidToByteArray(String str) { + String[] tokens = str.split(":"); + if (tokens.length != UPDProtocol.UID_LENGTH_USED) { + logger.warn("ignoring --uid {}, wrong size {}, expected {}", str, tokens.length, UPDProtocol.UID_LENGTH_USED); + return null; + } + byte[] uid = new byte[tokens.length]; + for (int n = 0; n < tokens.length; n++) { + uid[n] = (byte) Integer.parseUnsignedInt(tokens[n], 16); + } + return uid; + } + + 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 int getCommandPosition() { + return COMMAND_POSITION; + } +} diff --git a/firmware_updater/updater/src/org/selfbus/updater/upd/UPDProtocolException.java b/firmware_updater/updater/src/org/selfbus/updater/upd/UPDProtocolException.java new file mode 100644 index 00000000..daba2ddd --- /dev/null +++ b/firmware_updater/updater/src/org/selfbus/updater/upd/UPDProtocolException.java @@ -0,0 +1,12 @@ +package org.selfbus.updater.upd; + +public class UPDProtocolException extends Exception { + @SuppressWarnings("unused") + private UPDProtocolException(Throwable e) { + super(e); + } + + public UPDProtocolException(String message) { + super(message); + } +} diff --git a/firmware_updater/updater/src/resources/META-INF/MANIFEST.MF b/firmware_updater/updater/src/resources/META-INF/MANIFEST.MF new file mode 100644 index 00000000..ba051365 --- /dev/null +++ b/firmware_updater/updater/src/resources/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: org.selfbus.updater.Updater + diff --git a/firmware_updater/updater/src/resources/frame_images/selfbus_logo_16x16.png b/firmware_updater/updater/src/resources/frame_images/selfbus_logo_16x16.png new file mode 100644 index 00000000..e379a63b Binary files /dev/null and b/firmware_updater/updater/src/resources/frame_images/selfbus_logo_16x16.png differ diff --git a/firmware_updater/updater/src/resources/frame_images/selfbus_logo_256x256.png b/firmware_updater/updater/src/resources/frame_images/selfbus_logo_256x256.png new file mode 100644 index 00000000..2340777a Binary files /dev/null and b/firmware_updater/updater/src/resources/frame_images/selfbus_logo_256x256.png differ diff --git a/firmware_updater/updater/src/resources/frame_images/selfbus_logo_32x32.png b/firmware_updater/updater/src/resources/frame_images/selfbus_logo_32x32.png new file mode 100644 index 00000000..b9bdb433 Binary files /dev/null and b/firmware_updater/updater/src/resources/frame_images/selfbus_logo_32x32.png differ diff --git a/firmware_updater/updater/src/resources/frame_images/selfbus_logo_60x60.png b/firmware_updater/updater/src/resources/frame_images/selfbus_logo_60x60.png new file mode 100644 index 00000000..3d40194a Binary files /dev/null and b/firmware_updater/updater/src/resources/frame_images/selfbus_logo_60x60.png differ diff --git a/firmware_updater/updater/src/resources/frame_images/selfbus_logo_72x72.png b/firmware_updater/updater/src/resources/frame_images/selfbus_logo_72x72.png new file mode 100644 index 00000000..c604dc53 Binary files /dev/null and b/firmware_updater/updater/src/resources/frame_images/selfbus_logo_72x72.png differ diff --git a/firmware_updater/updater/src/resources/frame_images/selfbus_logo_96x96.png b/firmware_updater/updater/src/resources/frame_images/selfbus_logo_96x96.png new file mode 100644 index 00000000..cef451bf Binary files /dev/null and b/firmware_updater/updater/src/resources/frame_images/selfbus_logo_96x96.png differ diff --git a/firmware_updater/updater/src/resources/javax.usb.properties b/firmware_updater/updater/src/resources/javax.usb.properties new file mode 100644 index 00000000..159fe29a --- /dev/null +++ b/firmware_updater/updater/src/resources/javax.usb.properties @@ -0,0 +1,14 @@ +# suppress inspection "GrazieInspection" for whole file +# suppress inspection "UnusedProperty" for whole file +# Same values like in calimero-core/javax.usb.properties +# tell the USB host manager of javax-usb to use usb4java +javax.usb.services = org.usb4java.javax.Services +# KNX USB communication timeout +org.usb4java.javax.timeout = 1000 +# USB device scan interval (defaults to 500 ms, assigning 0 will scan only once at startup) +org.usb4java.javax.scanInterval = 1000 + +# todo this might be a USB driver solution on windows instead of zadig and WinUSB +# https://stackoverflow.com/questions/57241363/java-usb4java-reading-from-usb-device-on-windows-10-platform +# https://github.com/daynix/UsbDk +#org.usb4java.javax.useUSBDK = true diff --git a/firmware_updater/updater/source/src/main/resources/GuiTranslation.properties b/firmware_updater/updater/src/resources/language/GuiMain.properties similarity index 61% rename from firmware_updater/updater/source/src/main/resources/GuiTranslation.properties rename to firmware_updater/updater/src/resources/language/GuiMain.properties index a6095dc0..5ecee0a8 100644 --- a/firmware_updater/updater/source/src/main/resources/GuiTranslation.properties +++ b/firmware_updater/updater/src/resources/language/GuiMain.properties @@ -4,24 +4,22 @@ ipAddress=IP address scenario=scenario uid=UID startFlash=start flash -language=language newDevice=new device appDevice=device with application -allOptions=all options -loadFile=load file -medium=medium -serial=serial -tpuart=tpuart -knxDeviceAddr=device address -knxProgDeviceAddr=bootloader device address -knxOwnAddress=own KNX address -port=port -useNat=NAT -knxMessageDelay=delay [ms] -knxTimeout=timeout -eraseFlash=erase complete flash +loadFile=load &file +medium=&medium +serial=ft12 +tpuart=&TPUART +knxDeviceAddr=d&evice address +knxProgDeviceAddr=&bootloader device address +knxOwnAddress=own KNX addres&s +port=p&ort +useNat=&NAT +knxMessageDelay=&delay [ms] +eraseFlash=erase &complete flash noFlash=no flash -newDeviceHint=There is a new device with flashed bootloader
\\ and it is in programming mode. +newDeviceHint=There is a new device with flashed bootloader\ + appDeviceHint=A device is flashed with an application over
\ bootloader and now it should be updated portHint=UDP port on (default 3671) @@ -35,30 +33,37 @@ including the physical KNX address and all settings of
\ the device. Only the bootloader is not deleted. stopFlash=stop flashing requestUid=request UID from device -requestUidHint=The UID can be requested from a Selfbus device.
\ -The Selfbus device has to be in programmable
\ -mode into the bootloader +requestUidHint=The UID can be requested from a Selfbus device.\ + KnxGatewayConnectionSettings=KNX gateway connection settings KnxBusSettings=KNX bus settings UpdaterSettings=Updater settings -advancedSettings=Advanced settings -messagePriority=KNX telegram priority -knxSecureUser=KNX Secure User +advancedSettings=&Advanced settings +messagePriority=KN&X telegram priority +knxSecureUser=KNX IP-Secure &User knxSecureUserHint=KNX IP Secure tunneling user identifier
\ (1..127) (default 1) -knxSecureUserPwd=User password +knxSecureUserPwd=User &password knxSecureUserPwdHint=\ KNX IP Secure tunneling user password
\ (Commissioning password),quotation marks
\ (") in password may not work\ -knxSecureDevicePwd=Device password +knxSecureDevicePwd=Device pass&word knxSecureDevicePwdHint=\ KNX IP Secure device authentication code
\ (Authentication Code) quotation marks(")
\ in password may not work\ -reloadKnxIpGateways=reload gateways +reloadKnxIpGateways=&reload gateways selectInterface=select Interface -diffFlash=differential Flash -diffFlashHint=flash only changed parts of software
\\ [experimental] \ No newline at end of file +diffFlash=differential F&lash +diffFlashHint=flash only changed parts of software
\\ [experimental] +labelScenarioHint=Label +loadFile.firmwareFilterDescription=Firmware (*.hex) +Warning=Warning +IOException.loadingSettings.Message=Could not load settings from file %s.
%s +IOException.savingSettings.Message=Could not save settings to file %s.
%s +Exception.requestUidAction.Message=An error occurred while retrieving the UID.
%s +Exception.handleStartStopFlashAction.Message=An error has occurred while updating.
%s +Error=Error \ No newline at end of file diff --git a/firmware_updater/updater/src/resources/language/GuiMain_de.properties b/firmware_updater/updater/src/resources/language/GuiMain_de.properties new file mode 100644 index 00000000..6c6b2d84 --- /dev/null +++ b/firmware_updater/updater/src/resources/language/GuiMain_de.properties @@ -0,0 +1,71 @@ +loadFile=lade &Firmware-Datei +fileName=Dateiname +selectKnxIpGateway=KNX IP Schnittstelle +ipAddress=IP-Adresse +scenario=Szenario +uid=UID +startFlash=Starte Flash-Vorgang +newDevice=neues Gerät +appDevice=Gerät mit Applikation +newDeviceHint=Es wurde auf ein Selfbus Gerät der Bootloader geflasht\ + +appDeviceHint=Das Gerät wurde bereits mit einer Applikation
\ +über den Bootloader geflasht und soll nun aktualisiert werden.\ + +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 Port der KNX Schnittstelle (default 3671) +stopFlash=Stoppe Flash Vorgang +medium=&Medium +serial=ft12 +tpuart=&TPUART +knxDeviceAddr=&Geräteadresse +knxProgDeviceAddr=Geräteadresse im &Bootloader +knxOwnAddress=eigene KNX Adres&se +port=P&ort +useNat=&NAT +knxMessageDelay=&Verzögerung [ms] +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.\ + +UpdaterSettings=Updater +KnxBusSettings=KNX Bus +KnxGatewayConnectionSettings=KNX Schnittstelle +advancedSettings=&Erweiterte Einstellungen +knxSecureUserPwd=Benutzer &Passwort +messagePriority=KN&X Telegramm Priorität +knxSecureUser=KNX IP-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ätepass&wort +knxSecureDevicePwdHint=\ +KNX IP Secure Geräte Authentifizierung Code
\ +(Authentifizierungscode), Anführungszeichen
\ +(") im Passwort könnten nicht funktionieren\ + +reloadKnxIpGateways=Gateways e&rneut laden +selectInterface=wähle Schnittstelle +diffFlash=differenzieller F&lash Vorgang +diffFlashHint=Es werden nur veränderte Teile der Software
\ +übertragen [experimentell] +labelScenarioHint=Label +loadFile.firmwareFilterDescription=Firmware (*.hex) +Warning=Warning +IOException.loadingSettings.Message=Die Einstellungen konnten nicht aus der Datei file %s geladen werden.
%s +IOException.savingSettings.Message=Die Einstellungen konnten nicht in die Datei %s gespeichert werden.
%s +Exception.requestUidAction.Message=Es ist ein Fehler beim Auslesen der UID aufgetreten.
%s +Exception.handleStartStopFlashAction.Message=Es ist ein Fehler beim Flashen aufgetreten.
%s +Error=Fehler \ No newline at end of file diff --git a/firmware_updater/updater/src/resources/logback.xml b/firmware_updater/updater/src/resources/logback.xml new file mode 100644 index 00000000..11a1bc2f --- /dev/null +++ b/firmware_updater/updater/src/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/firmware_updater/updater/src/resources/logback/fileAppenders.xml b/firmware_updater/updater/src/resources/logback/fileAppenders.xml new file mode 100644 index 00000000..80dda59d --- /dev/null +++ b/firmware_updater/updater/src/resources/logback/fileAppenders.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + ${logFile}.log + false + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %level [%logger] %method \(%line\) %${messageNoAnsi}%n + + + + CONSOLE_GUI_ONLY + + + CONSOLE_GUI_NO_NEWLINE + + + + + ${logFile}.html + + + ${htmlPattern} + + + + CONSOLE_GUI_ONLY + + + CONSOLE_GUI_NO_NEWLINE + + + + \ No newline at end of file diff --git a/firmware_updater/updater/src/resources/logback/noAnsi.xml b/firmware_updater/updater/src/resources/logback/noAnsi.xml new file mode 100644 index 00000000..c462635a --- /dev/null +++ b/firmware_updater/updater/src/resources/logback/noAnsi.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/firmware_updater/updater/src/resources/logback/uiAppenders.xml b/firmware_updater/updater/src/resources/logback/uiAppenders.xml new file mode 100644 index 00000000..49996839 --- /dev/null +++ b/firmware_updater/updater/src/resources/logback/uiAppenders.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + true + + INFO + + + + + + + %message%n + + + + + + + INFO + + + + + + \ No newline at end of file diff --git a/firmware_updater/updater/src/resources/logback/vendorLoggers.xml b/firmware_updater/updater/src/resources/logback/vendorLoggers.xml new file mode 100644 index 00000000..fa488c2a --- /dev/null +++ b/firmware_updater/updater/src/resources/logback/vendorLoggers.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/firmware_updater/updater/source/src/test/java/org/selfbus/updater/BootloaderIdentityTest.java b/firmware_updater/updater/test/org/selfbus/updater/BootloaderIdentityTest.java similarity index 96% rename from firmware_updater/updater/source/src/test/java/org/selfbus/updater/BootloaderIdentityTest.java rename to firmware_updater/updater/test/org/selfbus/updater/BootloaderIdentityTest.java index 94a2f674..f5a9eb02 100644 --- a/firmware_updater/updater/source/src/test/java/org/selfbus/updater/BootloaderIdentityTest.java +++ b/firmware_updater/updater/test/org/selfbus/updater/BootloaderIdentityTest.java @@ -1,9 +1,9 @@ package org.selfbus.updater; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.selfbus.updater.bootloader.BootloaderIdentity; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class BootloaderIdentityTest { diff --git a/firmware_updater/updater/source/src/test/java/org/selfbus/updater/BootloaderStatisticTest.java b/firmware_updater/updater/test/org/selfbus/updater/BootloaderStatisticTest.java similarity index 86% rename from firmware_updater/updater/source/src/test/java/org/selfbus/updater/BootloaderStatisticTest.java rename to firmware_updater/updater/test/org/selfbus/updater/BootloaderStatisticTest.java index ce55dca3..aef8535e 100644 --- a/firmware_updater/updater/source/src/test/java/org/selfbus/updater/BootloaderStatisticTest.java +++ b/firmware_updater/updater/test/org/selfbus/updater/BootloaderStatisticTest.java @@ -1,18 +1,21 @@ package org.selfbus.updater; import org.selfbus.updater.bootloader.BootloaderStatistic; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.*; +import static org.fusesource.jansi.Ansi.*; + +import static org.selfbus.updater.logging.Color.*; public class BootloaderStatisticTest { @Test public void testToString() { - String colorOK = ConColors.BRIGHT_GREEN; - String colorWarn = ConColors.BRIGHT_YELLOW; - String colorReset = ConColors.RESET; - String info1 = "#Disconnect: "; - String info2 = " #repeated T_ACK: "; + String colorOK = ansi().fgBright(OK).toString(); + String colorWarn = ansi().fgBright(INFO).toString(); + String colorReset = ansi().reset().toString(); + String info1 = ""; + String info2 = " "; BootloaderStatistic test1 = new BootloaderStatistic(0, 0); BootloaderStatistic test2 = new BootloaderStatistic(BootloaderStatistic.THRESHOLD_DISCONNECT, 0); diff --git a/firmware_updater/updater/test/org/selfbus/updater/SpinningCursorTest.java b/firmware_updater/updater/test/org/selfbus/updater/SpinningCursorTest.java new file mode 100644 index 00000000..e493d624 --- /dev/null +++ b/firmware_updater/updater/test/org/selfbus/updater/SpinningCursorTest.java @@ -0,0 +1,59 @@ +package org.selfbus.updater; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.selfbus.updater.progress.SpinningCursor; + +import static org.junit.jupiter.api.Assertions.*; + +public class SpinningCursorTest { + + @BeforeEach + void setUp() { + SpinningCursor.reset(); + } + + @Test + public void testInitialCursorState() { + // Ensure the initial state is the first character in the cursor array + assertEquals('|', SpinningCursor.getNext()); + } + + @Test + public void testCursorRotation() { + // Simulate rotating through the cursor characters + assertEquals('|', SpinningCursor.getNext()); + assertEquals('/', SpinningCursor.getNext()); + assertEquals('-', SpinningCursor.getNext()); + assertEquals('\\', SpinningCursor.getNext()); + // After the last character, it should reset and start over + assertEquals('|', SpinningCursor.getNext()); + } + + @Test + public void testCursorReset() { + SpinningCursor.getNext(); // Move to the first character + SpinningCursor.reset(); // Reset to initial state + assertEquals('|', SpinningCursor.getNext()); // Should still be at the beginning + } + + @Test + public void testCursorIndexWrapping() { + // Rotate through all characters and check wrapping behavior + for (int i = 0; i < 100; i++) { + SpinningCursor.getNext(); + } + assertEquals('|', SpinningCursor.getNext()); + } + + @Test + public void testCursorSetBlank() { + SpinningCursor.getNext(); + SpinningCursor.setBlank(); + assertEquals(' ', SpinningCursor.getNext()); + assertEquals('|', SpinningCursor.getNext()); + SpinningCursor.reset(); + assertEquals('|', SpinningCursor.getNext()); + } +} + diff --git a/firmware_updater/updater/source/src/test/java/org/selfbus/updater/UtilsTest.java b/firmware_updater/updater/test/org/selfbus/updater/UtilsTest.java similarity index 65% rename from firmware_updater/updater/source/src/test/java/org/selfbus/updater/UtilsTest.java rename to firmware_updater/updater/test/org/selfbus/updater/UtilsTest.java index e968a6d4..e86195aa 100644 --- a/firmware_updater/updater/source/src/test/java/org/selfbus/updater/UtilsTest.java +++ b/firmware_updater/updater/test/org/selfbus/updater/UtilsTest.java @@ -1,23 +1,20 @@ package org.selfbus.updater; -import org.hamcrest.CoreMatchers; -import org.junit.Test; - -import static org.hamcrest.MatcherAssert.assertThat; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; public class UtilsTest { @Test public void testStreamToLong() { - // a full range test takes about 40s, so try to avoid it - // for lower two bytes we only check the signed bit range of 126..129 - ///\todo implement test of offset + // In 2025 this full test runs in under 6s. + // Let´s meet in the "middle" (~1s). byte[] testStream = new byte[]{0, 0, 0, 0}; - int start0 = 126; // min. 0, max. 255 - int end0 = 129; // min. 0, max. 255 + int start0 = 90; // min. 0, max. 255 + int end0 = 150; // min. 0, max. 255 - int start1 = 126; // min. 0, max. 255 - int end1 = 129; // min. 0, max. 255 + int start1 = 30; // min. 0, max. 255 + int end1 = 220; // min. 0, max. 255 int start2 = 0; // min. 0, max. 255 int end2 = 255; // min. 0, max. 255 @@ -36,7 +33,7 @@ public void testStreamToLong() { long expectedLong = l + (k << 8) + (j << 16) + (i << 24); long result; result = Utils.streamToLong(testStream, 0); - assertThat(result, CoreMatchers.is(expectedLong)); + assertEquals(expectedLong, result); } } } @@ -57,25 +54,13 @@ public void testLongToStream() { long expectedLong; byte[] testStream = new byte[]{0, 0, 0, 0}; - for (int i = 0; i < underTest.length; i++) { - Utils.longToStream(testStream, 0, underTest[i]); + for (long l : underTest) { + Utils.longToStream(testStream, 0, l); expectedLong = Utils.streamToLong(testStream, 0); - assertThat(underTest[i], CoreMatchers.is(expectedLong)); + assertEquals(expectedLong, l); } } - @Test - public void testByteArrayToHex() { - } - - @Test - public void testParseHost() { - } - - @Test - public void testFileExists() { - } - @Test public void testShortenPath() { int thresholdToTest = 4; @@ -83,26 +68,26 @@ public void testShortenPath() { // test windows path testFileName = "C:\\Users\\user\\AppData\\Local\\Selfbus\\Selfbus-Updater\\Cache\\0.61\\image-0x7000-34624-0xe8b27ade.bin"; testFileName = Utils.shortenPath(testFileName, thresholdToTest + 1); - assertThat(testFileName, CoreMatchers.is("C:\\...\\Selfbus\\Selfbus-Updater\\Cache\\0.61\\image-0x7000-34624-0xe8b27ade.bin")); + assertEquals("C:\\...\\Selfbus\\Selfbus-Updater\\Cache\\0.61\\image-0x7000-34624-0xe8b27ade.bin", testFileName); // test linux path testFileName = Utils.shortenPath("/home/username/.cache/Selfbus-Updater/0.61/image-0x7000-34624-0xe8b27ade.bin", thresholdToTest); - assertThat(testFileName, CoreMatchers.is("/.../.cache/Selfbus-Updater/0.61/image-0x7000-34624-0xe8b27ade.bin")); + assertEquals("/.../.cache/Selfbus-Updater/0.61/image-0x7000-34624-0xe8b27ade.bin", testFileName); // test UNC path testFileName = Utils.shortenPath("\\\\share\\funstuff\\running\\from\\Selfbus\\Selfbus-Updater\\Cache\\0.61\\image-0x7000-34624-0xe8b27ade.bin", thresholdToTest); - assertThat(testFileName, CoreMatchers.is("\\\\...\\Selfbus-Updater\\Cache\\0.61\\image-0x7000-34624-0xe8b27ade.bin")); + assertEquals("\\\\...\\Selfbus-Updater\\Cache\\0.61\\image-0x7000-34624-0xe8b27ade.bin", testFileName); // test http testFileName = Utils.shortenPath("http://tester.com/a1/b2/c3/d4/image-0x7000-34624-0xe8b27ade.bin", thresholdToTest); - assertThat(testFileName, CoreMatchers.is("http://.../b2/c3/d4/image-0x7000-34624-0xe8b27ade.bin")); + assertEquals("http://.../b2/c3/d4/image-0x7000-34624-0xe8b27ade.bin", testFileName); // test ftp testFileName = Utils.shortenPath("ftp://tester.com/a1/b2/c3/d4/e5/image-0x7000-34624-0xe8b27ade.bin", thresholdToTest); - assertThat(testFileName, CoreMatchers.is("ftp://.../c3/d4/e5/image-0x7000-34624-0xe8b27ade.bin")); + assertEquals("ftp://.../c3/d4/e5/image-0x7000-34624-0xe8b27ade.bin", testFileName); // test nothing to shorten testFileName = Utils.shortenPath("C:\\Local\\Selfbus\\Selfbus-Updater\\Cache\\0.61\\image-0x7000-34624-0xe8b27ade.bin", thresholdToTest); - assertThat(testFileName, CoreMatchers.is(testFileName)); + assertEquals("C:\\...\\Selfbus-Updater\\Cache\\0.61\\image-0x7000-34624-0xe8b27ade.bin", testFileName); } } \ No newline at end of file diff --git a/firmware_updater/updater/test/org/selfbus/updater/gui/ConColorsToStyledDocTests.java b/firmware_updater/updater/test/org/selfbus/updater/gui/ConColorsToStyledDocTests.java new file mode 100644 index 00000000..37cbc697 --- /dev/null +++ b/firmware_updater/updater/test/org/selfbus/updater/gui/ConColorsToStyledDocTests.java @@ -0,0 +1,289 @@ +package org.selfbus.updater.gui; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import static org.selfbus.updater.gui.ConColorsToStyledDoc.*; + +import javax.swing.*; +import javax.swing.text.*; +import java.awt.*; + +public class ConColorsToStyledDocTests { + + @BeforeEach + public void setup() { + shuffleStyle(false); + } + + private void assertStyleForeground(java.awt.Color expectedColor) { + assertEquals(expectedColor, StyleConstants.getForeground(testStringStyle())); + } + + private void assertStyleBackground(java.awt.Color expectedColor) { + assertEquals(expectedColor, StyleConstants.getBackground(testStringStyle())); + } + + private void assertStyleBold(boolean expectedBold) { + assertEquals(expectedBold, StyleConstants.isBold(testStringStyle())); + } + + private void assertStyleItalic(boolean expectedItalic) { + assertEquals(expectedItalic, StyleConstants.isItalic(testStringStyle())); + } + + private void assertStyleUnderline(boolean expectedUnderline) { + assertEquals(expectedUnderline, StyleConstants.isUnderline(testStringStyle())); + } + + private void assertStyleStrikeThrough(boolean expectedStrikeThrough) { + assertEquals(expectedStrikeThrough, StyleConstants.isStrikeThrough(testStringStyle())); + } + + private void shuffleStyle(boolean setAttribute) { + // fore and background intentionally swapped + StyleConstants.setForeground(testStringStyle(), DefaultBackgroundColor); + StyleConstants.setBackground(testStringStyle(), DefaultForegroundColor); + StyleConstants.setBold(testStringStyle(),setAttribute); + StyleConstants.setItalic(testStringStyle(),setAttribute); + StyleConstants.setUnderline(testStringStyle(),setAttribute); + StyleConstants.setStrikeThrough(testStringStyle(),setAttribute); + } + + @Test + public void testJansiReset() { + shuffleStyle(true); + testColorCodeToStyle(new String[]{""}); + assertStyleForeground(DefaultForegroundColor); + assertStyleBackground(DefaultBackgroundColor); + assertStyleBold(false); + assertStyleItalic(false); + assertStyleUnderline(false); + assertStyleStrikeThrough(false); + } + + @Test + public void testStandardReset() { + shuffleStyle(true); + testColorCodeToStyle(new String[]{"0"}); + // Assuming empty string should reset the style + assertStyleForeground(DefaultForegroundColor); + assertStyleBackground(DefaultBackgroundColor); + assertStyleBold(false); + assertStyleItalic(false); + assertStyleUnderline(false); + assertStyleStrikeThrough(false); + } + + @Test + public void testForegroundColors() { + java.awt.Color backgroundColor = StyleConstants.getBackground(testStringStyle()); + testColorCodeToStyle(new String[]{"30"}); + assertStyleForeground(normalColor(Color.black)); + assertStyleBackground(backgroundColor); + + testColorCodeToStyle(new String[]{"1", "31"}); + assertStyleForeground(normalColor(Color.red)); + assertStyleBackground(backgroundColor); + + testColorCodeToStyle(new String[]{"2", "32", "3"}); + assertStyleForeground(normalColor(Color.green)); + assertStyleBackground(backgroundColor); + } + + @Test + public void testBackgroundColors() { + java.awt.Color foregroundColor = StyleConstants.getForeground(testStringStyle()); + testColorCodeToStyle(new String[]{"1", "40"}); + assertStyleBackground(normalColor(java.awt.Color.black)); + assertStyleForeground(foregroundColor); + + testColorCodeToStyle(new String[]{"2", "41"}); + assertStyleBackground(normalColor(java.awt.Color.red)); + assertStyleForeground(foregroundColor); + + testColorCodeToStyle(new String[]{"42", "3"}); + assertStyleBackground(normalColor(java.awt.Color.green)); + assertStyleForeground(foregroundColor); + + testColorCodeToStyle(new String[]{"43"}); + assertStyleBackground(normalColor(java.awt.Color.yellow)); + assertStyleForeground(foregroundColor); + } + + @Test + public void testBoldOn() { + shuffleStyle(false); + testColorCodeToStyle(new String[]{"1"}); + assertStyleBold(true); + } + + @Test + public void testBoldOff() { + shuffleStyle(true); + testColorCodeToStyle(new String[]{"2"}); + assertStyleBold(false); + } + + @Test + public void testItalicOn() { + shuffleStyle(false); + testColorCodeToStyle(new String[]{"3"}); + assertStyleItalic(true); + } + + @Test + public void testItalicOff() { + shuffleStyle(true); + testColorCodeToStyle(new String[]{"23"}); + assertStyleItalic(false); + } + + @Test + public void testUnderlineOn() { + shuffleStyle(false); + testColorCodeToStyle(new String[]{"4"}); + assertStyleUnderline(true); + } + + @Test + public void testUnderlineOff() { + shuffleStyle(false); + testColorCodeToStyle(new String[]{"24"}); + assertStyleUnderline(false); + } + + @Test + public void testStrikeOn() { + shuffleStyle(false); + testColorCodeToStyle(new String[]{"9"}); + assertStyleStrikeThrough(true); + } + + @Test + public void testStrikeOff() { + shuffleStyle(false); + testColorCodeToStyle(new String[]{"29"}); + assertStyleStrikeThrough(false); + } + + @Test + public void testExceptionOnInvalidCode() { + assertThrows(IllegalStateException.class, () -> testColorCodeToStyle(new String[]{"999"})); + } + + @Test + public void testExceptionOnExtendedColors() { + assertThrows(IllegalStateException.class, () -> testColorCodeToStyle(new String[]{"0","38"})); + assertThrows(IllegalStateException.class, () -> testColorCodeToStyle(new String[]{"1", "48"})); + } + + @Test + public void testConvert() throws BadLocationException { + JTextPane textPane = new JTextPane(); + StyledDocument document = (StyledDocument) textPane.getDocument(); + //todo add more testcases + @SuppressWarnings("SpellCheckingInspection") + String[][] testCases = { + {"\" Done Speed Avg Min Max Time\"", "\" Done Speed Avg Min Max Time\""}, // no ansi + {"\033[mreset_jansi\033[m", "reset_jansi"}, // reset like jansi it sends + {"\033[0mreset_standard\033[0m", "reset_standard"}, // Ansi default reset + {"normal_start\033[1;32mboldGreen\033[0mnormal_end", "normal_startboldGreennormal_end"}, + {"Device 1.1.32 reported \033[92m1\033[m second(s) for restarting", "Device 1.1.32 reported 1 second(s) for restarting"} + }; + + for (String[] underTest : testCases) { + document.remove(0, document.getLength()); + ConColorsToStyledDoc.Convert(underTest[0], textPane); + //todo check also the style and not only the text + assertEquals(underTest[1], document.getText(0, document.getLength())); + } + } + + @Test + public void testCursor() throws BadLocationException { + JTextPane textPane = new JTextPane(); + StyledDocument document = (StyledDocument) textPane.getDocument(); + //todo add more testcases + String[][] testCases = { + {"\033[1G\033[92m|\033[m 0,1% \033[92m 86,7\033[m \033[92m 86,7\033[m 86,7 86,7 00:00", "| 0,1% 86,7 86,7 86,7 86,7 00:00"} + }; + for (String[] underTest : testCases) { + document.remove(0, document.getLength()); + ConColorsToStyledDoc.Convert(underTest[0], textPane); + assertEquals(underTest[1], document.getText(0, document.getLength())); + } + } + + @Test + public void testCursorOnOff() throws BadLocationException { + JTextPane textPane = new JTextPane(); + StyledDocument document = (StyledDocument) textPane.getDocument(); + //todo add more testcases + String[][] testCases = { + {"\033[?25h", ""}, // Cursor on + {"\033[?25l", ""}, // Cursor off + }; + for (String[] underTest : testCases) { + document.remove(0, document.getLength()); + ConColorsToStyledDoc.Convert(underTest[0], textPane); + assertEquals(underTest[1], document.getText(0, document.getLength())); + } + } + + @Test + public void testCursorSaveAndRestore() throws BadLocationException { + JTextPane textPane = new JTextPane(); + StyledDocument document = (StyledDocument) textPane.getDocument(); + String[] testCases = { + "\033[s", // Cursor save + "\033[u", // Cursor restore last position + }; + for (String underTest : testCases) { + document.remove(0, document.getLength()); + assertThrows(IllegalStateException.class, () -> ConColorsToStyledDoc.Convert(underTest, textPane)); + } + } + + @Test + public void testCursorMovement() throws BadLocationException { + JTextPane textPane = new JTextPane(); + StyledDocument document = (StyledDocument) textPane.getDocument(); + String[][] testCases = { + { // Cursor one line up (without parameter )and to column 1 + "testLine_1" + System.lineSeparator() + + "testLine_2" + System.lineSeparator() + + "testLine_3" + "\033[F" + + "test_end", + "testLine_1" + System.lineSeparator() + "test_end"}, + + { // Cursor one line up and to column 1 + "testLine_1" + System.lineSeparator() + + "testLine_2" + System.lineSeparator() + + "testLine_3" + "\033[1F" + + "test_end", + "testLine_1" + System.lineSeparator() + "test_end"}, + + { // Cursor two lines up and to column 1 + "testLine_1" + System.lineSeparator() + + "testLine_2" + System.lineSeparator() + + "testLine_3" + "\033[2F" + + "test_end", + "test_end"}, + + // cursor to column 1 + {"test_front\033[1Gtest_back", "test_back"}, + + // cursor to column 11 + {"test_front_erased\033[11G+test_back", "test_front+test_back"}, + + // cursor to column 1 + {"123456789" + System.lineSeparator() + "\033[1G", "123456789" + System.lineSeparator()}, + }; + for (String[] underTest : testCases) { + document.remove(0, document.getLength()); + ConColorsToStyledDoc.Convert(underTest[0], textPane); + assertEquals(underTest[1], document.getText(0, document.getLength())); + } + } +} diff --git a/firmware_updater/updater/test/org/selfbus/updater/logging/MessageFilterTest.java b/firmware_updater/updater/test/org/selfbus/updater/logging/MessageFilterTest.java new file mode 100644 index 00000000..ad469ac2 --- /dev/null +++ b/firmware_updater/updater/test/org/selfbus/updater/logging/MessageFilterTest.java @@ -0,0 +1,46 @@ +package org.selfbus.updater.logging; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.spi.FilterReply; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +import static ch.qos.logback.classic.Level.TRACE; +import static org.junit.jupiter.api.Assertions.*; + +class MessageFilterTest { + + /** + * Tests the evaluate method in the MessageFilter to ensure it correctly evaluates + * logging events against its filter for log messages. + */ + private final static Logger logger = (Logger) LoggerFactory.getLogger(MessageFilterTest.class); + MessageFilter messageFilter = null; + final String fqcn = logger.getClass().getName(); + + @BeforeEach + public void setUp() { + messageFilter = new MessageFilter(); + messageFilter.start(); + } + + private FilterReply callDecide(String message) { + ILoggingEvent event = new LoggingEvent(fqcn, logger, TRACE, message, null, null); + return messageFilter.decide(event); + } + + @Test + void testFilteredMessagesList() { + // Check some "random" log message is NOT filtered out + assertSame(FilterReply.NEUTRAL, callDecide("some log message ")); + for (String filteredMessage : MessageFilter.FILTERED_MESSAGES) { + // Check matching log message is filtered out + assertSame(FilterReply.DENY, callDecide(filteredMessage)); + // Check containing log message is filtered out + assertSame(FilterReply.DENY, callDecide(filteredMessage + "foo")); // test + } + } +} \ No newline at end of file diff --git a/firmware_updater/updater/test/org/selfbus/updater/logging/ThrowableFilterTest.java b/firmware_updater/updater/test/org/selfbus/updater/logging/ThrowableFilterTest.java new file mode 100644 index 00000000..69537c81 --- /dev/null +++ b/firmware_updater/updater/test/org/selfbus/updater/logging/ThrowableFilterTest.java @@ -0,0 +1,81 @@ +package org.selfbus.updater.logging; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.spi.FilterReply; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.lang.reflect.Constructor; +import java.util.Map; + +import static ch.qos.logback.classic.Level.*; +import static org.junit.jupiter.api.Assertions.assertSame; + +public class ThrowableFilterTest { + + /** + * Tests the evaluate method in the ThrowableFilter to ensure it correctly evaluates + * logging events against its filter for exceptions and corresponding messages. + */ + private final static Logger logger = (Logger) LoggerFactory.getLogger(ThrowableFilterTest.class); + ThrowableFilter throwableFilter = null; + final String fqcn = logger.getClass().getName(); + + @BeforeEach + public void setUp() { + throwableFilter = new ThrowableFilter(); + throwableFilter.start(); + } + + private FilterReply callDecide(String message, Throwable throwable) { + ILoggingEvent event = new LoggingEvent(fqcn, logger, TRACE, message, throwable, null); + return throwableFilter.decide(event); + } + + @Test + void testThrowableIsNull() { + assertSame(FilterReply.NEUTRAL, callDecide("testThrowableIsNull", null)); + } + + private Throwable createExceptionInstance(Class exceptionClass, String message) throws Exception { + // Use reflection to find a compatible constructor (one that accepts a String) + Constructor constructor = exceptionClass.getConstructor(String.class); + return constructor.newInstance(message); + } + + @Test + void testExceptionFilterMap() throws Exception { + // Iterate over all exceptions + for (Map.Entry, List> entry : + ThrowableFilter.EXCEPTION_FILTER_MAP.entrySet()) { + Class exceptionClass = entry.getKey(); + + // Iterate over all log messages / exception messages + for (ThrowableFilter.ConditionalFilter filter : entry.getValue()) { + String logMessage = filter.logMessage(); + String exceptionMessage = filter.exceptionMessage(); + + // Check without exception is not filtered + assertSame(FilterReply.NEUTRAL,callDecide(logMessage, null)); + + Throwable exception = createExceptionInstance(exceptionClass, exceptionMessage); + // Check matching to EXCEPTION_FILTER_MAP is filtered out + assertSame(FilterReply.DENY, callDecide(logMessage, exception)); + + // Check not matching log messages to EXCEPTION_FILTER_MAP is NOT filtered + assertSame(FilterReply.NEUTRAL, callDecide(logMessage + "foo", exception)); + + exception = createExceptionInstance(exceptionClass, exceptionMessage + "foo"); + // Check not matching to EXCEPTION_FILTER_MAP is NOT filtered + assertSame(FilterReply.NEUTRAL, callDecide(logMessage, exception)); + + // Check not matching log messages to EXCEPTION_FILTER_MAP is NOT filtered + assertSame(FilterReply.NEUTRAL, callDecide(logMessage + "foo", exception)); + } + } + } +} \ No newline at end of file diff --git a/firmware_updater/updater/source/src/test/java/org/selfbus/updater/tests/FlashDiffTests.java b/firmware_updater/updater/test/org/selfbus/updater/mode/differential/FlashDiffTests.java similarity index 69% rename from firmware_updater/updater/source/src/test/java/org/selfbus/updater/tests/FlashDiffTests.java rename to firmware_updater/updater/test/org/selfbus/updater/mode/differential/FlashDiffTests.java index ef0b5567..a3c99b0b 100644 --- a/firmware_updater/updater/source/src/test/java/org/selfbus/updater/tests/FlashDiffTests.java +++ b/firmware_updater/updater/test/org/selfbus/updater/mode/differential/FlashDiffTests.java @@ -1,63 +1,65 @@ -package org.selfbus.updater.tests; - -import org.junit.Test; -import org.selfbus.updater.BinImage; -import org.selfbus.updater.UpdaterException; -import org.selfbus.updater.tests.flashdiff.Decompressor; -import org.selfbus.updater.tests.flashdiff.FlashDiff; -import org.selfbus.updater.tests.flashdiff.FlashPage; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.concurrent.atomic.AtomicInteger; - -public class FlashDiffTests { - private void performTest(BinImage img1, BinImage img2) { - int pages1 = (int) Math.ceil(img2.getBinData().length / (double) FlashPage.PAGE_SIZE); - int pages2 = (int) Math.ceil(img2.getBinData().length / (double)FlashPage.PAGE_SIZE); - int pagesMax = pages1 > pages2 ? pages1 : pages2; - BinImage rom = new BinImage(img1, pagesMax * FlashPage.PAGE_SIZE); - BinImage img2wholePages = new BinImage(img2, pagesMax * FlashPage.PAGE_SIZE); - FlashDiff differ = new FlashDiff(); - AtomicInteger pageNumber = new AtomicInteger(); - Decompressor decompressor = new Decompressor(rom, (oldPagesRam, page) -> { - // backup page to be flashed to RAM - oldPagesRam.fillNextPage(rom.getBinData(), pageNumber.get() * FlashPage.PAGE_SIZE); - // process decompressed page - System.out.println("Page decompressed"); - // emulate ROM page flash (rom is our ROM in the MCU) - System.arraycopy(page.getOldBinData(), 0, rom.getBinData(), pageNumber.get() * FlashPage.PAGE_SIZE, FlashPage.PAGE_SIZE); - FlashDiffUtils.dumpSideBySide(img2wholePages.getBinData(), rom.getBinData(), pageNumber.get() * FlashPage.PAGE_SIZE); - pageNumber.incrementAndGet(); - //System.exit(1); - }); - // TODO -// differ.generateDiff(img1, img2, outputDiffStream -> { -// // process compressed page -// for (byte b : outputDiffStream) { -// decompressor.putByte(b); -// } -// decompressor.pageCompleted(); -// }); - } - - @Test - public void testDiff() throws URISyntaxException { - // test of upgrade from old version to newer (longer) - ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); - URI uri1 = contextClassLoader.getResource("updater.ino.slto.v1.hex").toURI(); - URI uri2 = contextClassLoader.getResource("updater.ino.slto.v2.hex").toURI(); - BinImage img1 = BinImage.readFromHex(uri1.getPath()); - BinImage img2 = BinImage.readFromHex(uri2.getPath()); - performTest(img1, img2); - } - - @Test - public void testDiff2() throws URISyntaxException, UpdaterException { - // test of new firmware into empty MCU - URI uri2 = Thread.currentThread().getContextClassLoader().getResource("updater.ino.slto.v2.hex").toURI(); - BinImage img2 = BinImage.readFromHex(uri2.getPath()); - BinImage img1 = BinImage.dummyFilled(img2.getBinData().length, 0xff); - performTest(img1, img2); - } -} +package org.selfbus.updater.mode.differential; + +import org.junit.jupiter.api.Test; +import org.selfbus.updater.BinImage; +import org.selfbus.updater.UpdaterException; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +public class FlashDiffTests { + private void performTest(BinImage img1, BinImage img2) { + int pages1 = (int) Math.ceil(img2.getBinData().length / (double) FlashPage.PAGE_SIZE); + int pages2 = (int) Math.ceil(img2.getBinData().length / (double)FlashPage.PAGE_SIZE); + int pagesMax = Math.max(pages1, pages2); + BinImage rom = new BinImage(img1, pagesMax * FlashPage.PAGE_SIZE); + BinImage img2wholePages = new BinImage(img2, pagesMax * FlashPage.PAGE_SIZE); + FlashDiff differ = new FlashDiff(); + AtomicInteger pageNumber = new AtomicInteger(); + Decompressor decompressor = new Decompressor(rom, (oldPagesRam, page) -> { + // backup page to be flashed to RAM + oldPagesRam.fillNextPage(rom.getBinData(), pageNumber.get() * FlashPage.PAGE_SIZE); + // process decompressed page + System.out.println("Page decompressed"); + // emulate ROM page flash (rom is our ROM in the MCU) + System.arraycopy(page.getOldBinData(), 0, rom.getBinData(), pageNumber.get() * FlashPage.PAGE_SIZE, FlashPage.PAGE_SIZE); + FlashDiffUtils.dumpSideBySide(img2wholePages.getBinData(), rom.getBinData(), pageNumber.get() * FlashPage.PAGE_SIZE); + pageNumber.incrementAndGet(); + //System.exit(1); + }); + // TODO + /* + differ.generateDiff(img1, img2, outputDiffStream -> { + // process compressed page + for (byte b : outputDiffStream) { + decompressor.putByte(b); + } + decompressor.pageCompleted(); + }); + */ + } + + @SuppressWarnings("SpellCheckingInspection") + @Test + public void testDiff() throws URISyntaxException { + // test of upgrade from old version to newer (longer) + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + URI uri1 = Objects.requireNonNull(contextClassLoader.getResource("hex/updater.ino.slto.v1.hex")).toURI(); + URI uri2 = Objects.requireNonNull(contextClassLoader.getResource("hex/updater.ino.slto.v2.hex")).toURI(); + BinImage img1 = BinImage.readFromHex(uri1.getPath()); + BinImage img2 = BinImage.readFromHex(uri2.getPath()); + performTest(img1, img2); + } + + @SuppressWarnings("SpellCheckingInspection") + @Test + public void testDiff2() throws URISyntaxException, UpdaterException { + // test of new firmware into empty MCU + URI uri2 = Objects.requireNonNull(Thread.currentThread().getContextClassLoader().getResource("hex/updater.ino.slto.v2.hex")).toURI(); + BinImage img2 = BinImage.readFromHex(uri2.getPath()); + BinImage img1 = BinImage.dummyFilled(img2.getBinData().length, 0xff); + performTest(img1, img2); + } +} diff --git a/firmware_updater/updater/source/src/test/java/org/selfbus/updater/tests/FlashDiffUtils.java b/firmware_updater/updater/test/org/selfbus/updater/mode/differential/FlashDiffUtils.java similarity index 76% rename from firmware_updater/updater/source/src/test/java/org/selfbus/updater/tests/FlashDiffUtils.java rename to firmware_updater/updater/test/org/selfbus/updater/mode/differential/FlashDiffUtils.java index ac6b2f5b..3e1b35d7 100644 --- a/firmware_updater/updater/source/src/test/java/org/selfbus/updater/tests/FlashDiffUtils.java +++ b/firmware_updater/updater/test/org/selfbus/updater/mode/differential/FlashDiffUtils.java @@ -1,37 +1,37 @@ -package org.selfbus.updater.tests; - -import org.junit.Assert; -import org.selfbus.updater.BinImage; - -public class FlashDiffUtils { - - public static boolean isEqual(byte[] a1, byte[] a2, int begin, int size) { - for(int i=begin;i #include #include +#include class BcuBase; diff --git a/sblib/inc/sblib/eib/com_objectsBCU1.h b/sblib/inc/sblib/eib/com_objectsBCU1.h index c91a4a90..e3bae0e3 100644 --- a/sblib/inc/sblib/eib/com_objectsBCU1.h +++ b/sblib/inc/sblib/eib/com_objectsBCU1.h @@ -49,6 +49,7 @@ class ComObjectsBCU1 : public ComObjects ComObjectsBCU1(BcuDefault* bcuInstance) : ComObjects((BcuBase*)bcuInstance) {} ~ComObjectsBCU1() = default; + void printObjectConfigTable(); virtual const ComConfig& objectConfig(int objno) override; virtual int objectSize(int objno) override; virtual byte* objectValuePtr(int objno) override; diff --git a/sblib/inc/sblib/eib/com_objectsBCU2.h b/sblib/inc/sblib/eib/com_objectsBCU2.h index c47d7858..b1ee271d 100644 --- a/sblib/inc/sblib/eib/com_objectsBCU2.h +++ b/sblib/inc/sblib/eib/com_objectsBCU2.h @@ -44,6 +44,7 @@ class ComObjectsBCU2 : public ComObjectsBCU1 ComObjectsBCU2(BcuDefault* bcuInstance) : ComObjectsBCU1(bcuInstance) {} ~ComObjectsBCU2() = default; + void printObjectConfigTable(); protected: virtual byte* objectValuePtr(int objno) override; virtual byte* objectConfigTable() override; diff --git a/sblib/inc/sblib/eib/com_objects_debug.h b/sblib/inc/sblib/eib/com_objects_debug.h new file mode 100644 index 00000000..fbbeb636 --- /dev/null +++ b/sblib/inc/sblib/eib/com_objects_debug.h @@ -0,0 +1,33 @@ +/** + * com_objects_debug.h - KNX Communication objects debugging helper. + * + */ + +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + */ + + +#ifndef SBLIB_EIB_COM_OBJECTS_DEBUG_H_ +#define SBLIB_EIB_COM_OBJECTS_DEBUG_H_ + +#include +#include + +#if defined(DUMP_COM_OBJ) +# include +#endif + +#ifdef DUMP_COM_OBJ +# define d(x) x +#else +# define d(x) +#endif + + +void printComObjectConfig(uint8_t config); +void printComObjectType(uint8_t type); + +#endif /* SBLIB_EIB_COM_OBJECTS_DEBUG_H_ */ diff --git a/sblib/inc/sblib/eib/types.h b/sblib/inc/sblib/eib/types.h index b6b4614e..16de521e 100644 --- a/sblib/inc/sblib/eib/types.h +++ b/sblib/inc/sblib/eib/types.h @@ -86,6 +86,9 @@ struct ComConfig */ enum ComConfigFlag { + /** Com object configuration flag: update enabled */ + COMCONF_UPDATE = 0x80, + /** Com object configuration flag: transmit enabled */ COMCONF_TRANS = 0x40, diff --git a/sblib/src/eib/bcu1.cpp b/sblib/src/eib/bcu1.cpp index 83c3b59b..22001102 100644 --- a/sblib/src/eib/bcu1.cpp +++ b/sblib/src/eib/bcu1.cpp @@ -25,6 +25,7 @@ inline void BCU1::begin(int manufacturer, int deviceType, int version) { BcuDefault::begin(manufacturer, deviceType, version); BcuDefault::_begin(); + comObjects->printObjectConfigTable(); } bool BCU1::applicationRunning() const diff --git a/sblib/src/eib/bcu2.cpp b/sblib/src/eib/bcu2.cpp index a67e9e7e..439c6154 100644 --- a/sblib/src/eib/bcu2.cpp +++ b/sblib/src/eib/bcu2.cpp @@ -74,6 +74,7 @@ inline void BCU2::begin(int manufacturer, int deviceType, int version, word read userEeprom->orderInfo()[userEeprom->orderInfoSize() - 1] = lowByte(SBLIB_VERSION); BcuDefault::_begin(); + static_cast(this->comObjects)->printObjectConfigTable(); } void BCU2::begin(int manufacturer, int deviceType, int version) diff --git a/sblib/src/eib/bcu_default.cpp b/sblib/src/eib/bcu_default.cpp index 41e7f44a..fcbcd946 100644 --- a/sblib/src/eib/bcu_default.cpp +++ b/sblib/src/eib/bcu_default.cpp @@ -209,6 +209,7 @@ bool BcuDefault::processGroupAddressTelegram(ApciCommand apciCmd, uint16_t group DB_COM_OBJ( serial.println(); serial.print("BCU grp addr: 0x", (unsigned int)groupAddress, HEX, 4); + serial.print(" "); ); comObjects->processGroupTelegram(groupAddress, apciCmd & APCI_GROUP_MASK, telegram); diff --git a/sblib/src/eib/com_object_debug.cpp b/sblib/src/eib/com_object_debug.cpp new file mode 100644 index 00000000..5a940420 --- /dev/null +++ b/sblib/src/eib/com_object_debug.cpp @@ -0,0 +1,162 @@ +/** + * com_objects_debug.cpp - KNX Communication objects debugging helper. + * + */ + +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + */ + +#include +#include + +void printComObjectConfig(uint8_t config) +{ + DB_COM_OBJ( + // C - communicate + if ((config & 0b00000100) == (0b00000100)) + { + serial.print("c"); + } + else + { + serial.print(" "); + } + + // R - read + if ((config & 0b00001000) == (0b00001000)) + { + serial.print("r"); + } + else + { + serial.print(" "); + } + + // W - write + if ((config & 0b00010000) == (0b00010000)) + { + serial.print("w"); + } + else + { + serial.print(" "); + } + + // T - transfer + if ((config & 0b01000000) == (0b01000000)) + { + serial.print("t"); + } + else + { + serial.print(" "); + } + + // U - update + if ((config & 0b10000000) == (0b10000000)) + { + serial.print("u"); + } + else + { + serial.print(" "); + } + + // Segment Selector Type + serial.print(" s"); + if ((config & 0b00100000) == (0b00100000)) + { + serial.print("1"); // segment = 0x100 + } + else + { + serial.print("0"); // segment = 0x00 + } + + serial.print(" "); + switch (config & 0b00000011) + { + case 0b11: + serial.print("low "); + break; + case 0b01: + serial.print("high"); + break; + case 0b10: + serial.print("alm "); + break; + case 0b00: + serial.print("sys "); + break; + default: + // this should never happen + serial.print("unknown"); + break; + } + ); +} + +void printComObjectType(uint8_t type) +{ + DB_COM_OBJ( + if ((type >= BIT_1) && (type <= BIT_7)) + { + serial.print(type + 1, DEC, 1); + serial.print(" bit"); + if (type > 0) + { + serial.print("s"); // bits, if more then one + } + } + else + { + switch(type) + { + case BYTE_1: + serial.print("1 byte"); + break; + + case BYTE_2: + serial.print("2 bytes"); + break; + + case BYTE_3: + serial.print("3 bytes"); + break; + + case BYTE_4: + serial.print("4 bytes"); + break; + + case DATA_6: + serial.print("6 bytes"); + break; + + case DATA_8: + serial.print("8 bytes"); + break; + + case DATA_10: + serial.print("10 bytes"); + break; + + case MAXDATA: + serial.print("14 bytes"); + break; + + case VARDATA: + serial.print("1-14 bytes"); + break; + + default: + serial.print("unknown (0x", type, HEX, 2); + serial.print(")"); + break; + } + } + + ); +} diff --git a/sblib/src/eib/com_objects.cpp b/sblib/src/eib/com_objects.cpp index 1afcdfc6..1c8a3369 100644 --- a/sblib/src/eib/com_objects.cpp +++ b/sblib/src/eib/com_objects.cpp @@ -187,16 +187,6 @@ #include #include -#if defined(DUMP_COM_OBJ) -# include -#endif - -#ifdef DUMP_COM_OBJ -# define d(x) x -#else -# define d(x) -#endif - /** The COMFLAG_UPDATE flag, moved to the high nibble */ #define COMFLAG_UPDATE_HIGH (COMFLAG_UPDATE << 4) @@ -233,16 +223,16 @@ void ComObjects::addObjectFlags(int objno, int flags) if (objno & 1) flags <<= 4; - d(serial.print(" addObjFlags in (obj, flags): ");) - d(serial.print(objno, DEC, 2);) - d(serial.print(", ");) + d(serial.print(" addObjFlags(");) + d(serial.print(objno, DEC, 3);) + d(serial.print(", 0x");) d(serial.print(flags, HEX, 2);) - d(serial.print(", is: ");) + d(serial.print(") old: 0x");) d(serial.print(flagsTab[objno >> 1], HEX, 2);) flagsTab[objno >> 1] |= flags; - d(serial.print(", out: ");) + d(serial.print(" new: ");) d(serial.print(flagsTab[objno >> 1], HEX, 2);) d(serial.println();) } @@ -257,9 +247,9 @@ void ComObjects::setObjectFlags(int objno, int flags) flagsPtr += objno >> 1; // "select" high or low nibble according to objno odd or even d( - serial.print(" setObjFlags obj: ", objno, DEC, 2); - serial.print(" is: ", *flagsPtr, HEX, 2); - serial.print(" to: ", flags, HEX, 2); + serial.print(" setObjFlags obj: ", objno, DEC, 3); + serial.print(" is: 0x", *flagsPtr, HEX, 2); + serial.print(" to: 0x", flags, HEX, 2); ) if (objno & 1) @@ -272,7 +262,7 @@ void ComObjects::setObjectFlags(int objno, int flags) *flagsPtr &= 0xf0; *flagsPtr |= flags; } - d(serial.println(" out: ", *flagsPtr, HEX, 2);) + d(serial.println(" out: 0x", *flagsPtr, HEX, 2);) } unsigned int ComObjects::objectRead(int objno) @@ -308,6 +298,11 @@ void ComObjects::_objectWrite(int objno, unsigned int value, int flags) } //addObjectFlags(objno, flags); + d( + serial.print("ComObjects::_objectWrite call setObjectFlags(", objno, DEC, 3); + serial.print(", 0x", flags, HEX, 2); + serial.println(")"); + ) setObjectFlags(objno, flags); //clear any pending ram com object flags and set new flags } diff --git a/sblib/src/eib/com_objectsBCU1.cpp b/sblib/src/eib/com_objectsBCU1.cpp index 5329b3c2..35d34ed9 100644 --- a/sblib/src/eib/com_objectsBCU1.cpp +++ b/sblib/src/eib/com_objectsBCU1.cpp @@ -72,7 +72,7 @@ void ComObjectsBCU1::processGroupTelegram(uint16_t addr, int apci, byte* tel, in */ const byte* assocTab = bcu->addrTables->assocTable(); const int endAssoc = 1 + (*assocTab) * 2; - int objno, objConf; + int objno; DB_COM_OBJ( serial.print("grpAddr ", mainGroup(addr)); @@ -112,23 +112,39 @@ void ComObjectsBCU1::processGroupTelegram(uint16_t addr, int apci, byte* tel, in } //DB_COM_OBJ(serial.println("commsTabAddr: 0x", ((UserEepromBCU1*)((BcuDefault*)bcu)->userEeprom)->commsTabAddr(), HEX);); - objConf = objectConfig(objno).config; - DB_COM_OBJ(serial.println("objConf: 0x", objno, HEX, 2);); + uint8_t objConf = objectConfig(objno).config; + DB_COM_OBJ(serial.println("objConf: 0x", objConf, HEX, 2);); - - - if (apci == APCI_GROUP_VALUE_WRITE_PDU || apci == APCI_GROUP_VALUE_RESPONSE_PDU) + if ((objConf & COMCONF_COMM) != COMCONF_COMM) { - // Check if communication and write are enabled - if ((objConf & COMCONF_WRITE_COMM) == COMCONF_WRITE_COMM) - processGroupWriteTelegram(objno, tel); // set update flag and update value of object + // communication is disabled, just continue and check remaining associations + continue; } - else if (apci == APCI_GROUP_VALUE_READ_PDU) + + switch (apci) { - // Check if communication and read are enabled - if ((objConf & COMCONF_READ_COMM) == COMCONF_READ_COMM) - // we received read-request from bus - so send response back and search for more associations - sendGroupWriteTelegram(objno, addr, true); // send write to the bus and update all associated local objects + case APCI_GROUP_VALUE_WRITE_PDU: + // Check if write is enabled + if ((objConf & COMCONF_WRITE) == COMCONF_WRITE) + processGroupWriteTelegram(objno, tel); // set update flag and update value of object + break; + + case APCI_GROUP_VALUE_RESPONSE_PDU: + // Check if update is enabled + if ((objConf & COMCONF_UPDATE) == COMCONF_UPDATE) + processGroupWriteTelegram(objno, tel); // set update flag and update value of object + break; + + case APCI_GROUP_VALUE_READ_PDU: + // Check if read is enabled + if ((objConf & COMCONF_READ) == COMCONF_READ) + // we received read-request from bus - so send response back and search for more associations + sendGroupWriteTelegram(objno, addr, true); // send write to the bus and update all associated local objects + break; + + default: + // this should never happen + IF_DEBUG(fatalError();); } } } @@ -167,3 +183,50 @@ inline const ComConfigBCU1* ComObjectsBCU1::objectConfigBCU1(int objno) } return (const ComConfigBCU1*) (objConfigTable + 1 + sizeof(ComConfigBCU1::DataPtrType) + objno * sizeof(ComConfigBCU1) ); } + +void ComObjectsBCU1::printObjectConfigTable() +{ +#ifdef DUMP_COM_OBJ + uint16_t comObjTableAddr = ((BcuDefault*)bcu)->userEeprom->commsTabPtr(); + comObjTableAddr += ((BcuDefault*)bcu)->userEeprom->startAddr(); // for BCU1 add 0x100 + serial.println("ObjectConfigTable:"); + serial.println(" address : 0x", (unsigned int)comObjTableAddr, HEX, 4); + if (comObjTableAddr == 0) + { + serial.println("invalid address!"); + return; + } + byte* currentTablePosition = ((BcuDefault*)bcu)->userMemoryPtr(comObjTableAddr); + byte currentSize = *currentTablePosition; + serial.println(" #com objects : ", (unsigned int)currentSize, DEC, 3); + currentTablePosition++; // 1 byte #com objects + uint16_t ramFlagsTablePointer; + ramFlagsTablePointer = *currentTablePosition; + + currentTablePosition++; // 1 byte RAM-Flags-Pointer + serial.println(" RAM-Flags-Ptr: 0x", (unsigned int)ramFlagsTablePointer, HEX, 4); + for (uint8_t i = 0; i < currentSize; i++) + { + uint8_t data; + data = *currentTablePosition; + currentTablePosition++; // 1 byte data pointer + uint8_t config = *currentTablePosition; + if ((config & COMCONF_VALUE_TYPE) == COMCONF_VALUE_TYPE) + { + data += 0x100; // Segment selector KNX Spec. 3.0 3/5/1 4.18.3.1.2.1 + } + + currentTablePosition++; // 1 byte config + uint8_t type = *currentTablePosition; + currentTablePosition++; // 1 byte type + serial.print("#", i, DEC, 3); + serial.print(": data 0x", data, HEX, 4); + serial.print(" config (0x", config, HEX, 2); + serial.print("): "); + printComObjectConfig(config); + serial.print(" type: "); + printComObjectType(type); + serial.println(); + } +#endif +} diff --git a/sblib/src/eib/com_objectsBCU2.cpp b/sblib/src/eib/com_objectsBCU2.cpp index e8c650a0..9bde3d31 100644 --- a/sblib/src/eib/com_objectsBCU2.cpp +++ b/sblib/src/eib/com_objectsBCU2.cpp @@ -61,3 +61,58 @@ const ComConfig& ComObjectsBCU2::objectConfig(int objno) { return objectConfigBCU2(objno)->baseConfig; } + +void ComObjectsBCU2::printObjectConfigTable() +{ +#ifdef DUMP_COM_OBJ + byte * addr = (byte* ) & ((BCU2*)bcu)->userEeprom->commsTabAddr(); + uint16_t comObjTableAddr = makeWord(*(addr + 1), * addr); + serial.println("ObjectConfigTable:"); + serial.println(" address : 0x", (unsigned int)comObjTableAddr, HEX, 4); + if (comObjTableAddr == 0) + { + serial.println("invalid address!"); + return; + } + byte* currentTablePosition = ((BcuDefault*)bcu)->userMemoryPtr(comObjTableAddr); + byte currentSize = *currentTablePosition; + serial.println(" #com objects : ", (unsigned int)currentSize, DEC, 3); + currentTablePosition++; // 1 byte #com objects + uint16_t ramFlagsTablePointer; + if (le_ptr == LITTLE_ENDIAN) + { + ramFlagsTablePointer = makeWord(*(currentTablePosition + 1), *currentTablePosition); + } + else + { + ramFlagsTablePointer = makeWord(*currentTablePosition, *(currentTablePosition + 1)); + } + currentTablePosition += 2; // 2 bytes RAM-Flags-Pointer + serial.println(" RAM-Flags-Ptr: 0x", (unsigned int)ramFlagsTablePointer, HEX, 4); + for (uint8_t i = 0; i < currentSize; i++) + { + uint16_t data; + if (le_ptr == LITTLE_ENDIAN) + { + data = makeWord(*(currentTablePosition + 1), *currentTablePosition); + } + else + { + data = makeWord(*currentTablePosition, *(currentTablePosition + 1)); + } + currentTablePosition += 2; // 2 bytes data pointer + uint8_t config = *currentTablePosition; + currentTablePosition++; // 1 byte config + uint8_t type = *currentTablePosition; + currentTablePosition++; // 1 byte type + serial.print("#", i, DEC, 3); + serial.print(": data 0x", data, HEX, 4); + serial.print(" config (0x", config, HEX, 2); + serial.print("): "); + printComObjectConfig(config); + serial.print(" type: "); + printComObjectType(type); + serial.println(); + } +#endif +} diff --git a/test/lib-test-cases/lib-test-cases.cmake b/test/lib-test-cases/lib-test-cases.cmake index 6d158625..410575d2 100644 --- a/test/lib-test-cases/lib-test-cases.cmake +++ b/test/lib-test-cases/lib-test-cases.cmake @@ -19,4 +19,8 @@ set(SBLIB_LIB_TEST_CASES_SRC src/test_prot_app_program.cpp src/test_prot_tlayer4.cpp src/timeout_test.cpp + src/knx/test_memory.h + src/knx/test_memory.cpp + src/knx/test_user_eeprom.h + src/knx/test_user_eeprom.cpp ) \ No newline at end of file diff --git a/test/lib-test-cases/src/knx/test_memory.cpp b/test/lib-test-cases/src/knx/test_memory.cpp new file mode 100644 index 00000000..3b84a227 --- /dev/null +++ b/test/lib-test-cases/src/knx/test_memory.cpp @@ -0,0 +1,129 @@ +/* + * Tests for the memory.h + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + */ + +#include +#include "test_memory.h" + + +struct TestRange +{ + uint32_t start; + uint32_t size; +}; + +static std::vector testRanges = { + { 0,1 }, // lower limit + { 0, UINT32_MAX }, // upper limit size + { UINT32_MAX - 1, 1 }, // upper limit address + { 0x100, 256 }, // BCU 1 + { 0x100, 1024 }, // BCU 2 + { 0x3f00, 3072 }, // Mask 0x0701, 0x0705 + { 0x3300, 3072 } // System B +}; + +TEST_CASE("Memory Constructor", "[UserEeprom]") +{ + TestMemory* testMemory; + // Test declared ranges + for (const auto& [start, size] : testRanges) + { + testMemory = new TestMemory(start, size); + REQUIRE(testMemory->startAddr() == start); + REQUIRE(testMemory->size() == size); + REQUIRE(testMemory->endAddr() == start + size - 1); + delete testMemory; + } + + // Test some arbitrary ranges + for (uint32_t start = 0; start < 0xffff; start += 131) // 131 is some arbitrary increment + { + for (uint32_t size = 1; size < 0xffff; size += 251) // 251 is some arbitrary increment + { + testMemory = new TestMemory(start, size); + REQUIRE(testMemory->startAddr() == start); + REQUIRE(testMemory->size() == size); + REQUIRE(testMemory->endAddr() == start + size - 1); + delete testMemory; + } + } +} + +TEST_CASE("Memory normalizeAddress", "[UserEeprom]") +{ + // Test declared ranges + for (const auto& [start, size] : testRanges) + { + TestMemory testMemory(start, size); + const uint32_t maxIterations = std::min(testMemory.endAddr(), static_cast(0xffff)); + for (uint32_t i = start; i <= maxIterations; i++) + { + uint32_t addressToTest = i + start; + testMemory.normalizeAddress(&addressToTest); + REQUIRE(addressToTest == i); + } + } +} + +TEST_CASE("Memory inRange(start, end)", "[UserEeprom]") +{ + // Test declared ranges + for (const auto& [start, size] : testRanges) + { + TestMemory testMemory(start, size); + REQUIRE(testMemory.inRange(start - 1, start) == false); + REQUIRE(testMemory.inRange(start, start) == true); + REQUIRE(testMemory.inRange(start, testMemory.endAddr()) == true); + REQUIRE(testMemory.inRange(start, testMemory.endAddr() - 1) == (size > 1)); + REQUIRE(testMemory.inRange(start, testMemory.endAddr()) == true); + const uint32_t maxIterations = std::min(testMemory.endAddr(), static_cast(0xffff)); + for (uint32_t i = start; i <= maxIterations; i++) + { + REQUIRE(testMemory.inRange(i, maxIterations) == true); + } + + for (uint32_t i = 0; i <= maxIterations; i--) + { + REQUIRE(testMemory.inRange(start, testMemory.endAddr() - i) == true); + } + } +} + +TEST_CASE("Memory inRange(address)", "[UserEeprom]") +{ + // Test declared ranges + for (const auto& [start, size] : testRanges) + { + TestMemory testMemory(start, size); + REQUIRE(testMemory.inRange(start - 1) == false); + REQUIRE(testMemory.inRange(testMemory.endAddr() + 1) == false); + + const uint32_t maxIterations = std::min(testMemory.endAddr(), static_cast(0xffff)); + for (uint32_t i = start; i <= maxIterations; i++) + { + REQUIRE(testMemory.inRange(i) == true); + } + + for (uint32_t i = 0; i <= maxIterations; i--) + { + REQUIRE(testMemory.inRange(testMemory.endAddr() - i) == true); + } + } +} + +TEST_CASE("Memory overridden dummys", "[UserEeprom]") +{ + // Test declared ranges + for (const auto& [start, size] : testRanges) + { + TestMemory testMemory(start, size); + // These checks are just too check that the methods are implemented. + REQUIRE(testMemory[start] == 0); + REQUIRE(testMemory.getUInt8(start) == 0); + REQUIRE(testMemory.getUInt16(start) == 0); + } +} \ No newline at end of file diff --git a/test/lib-test-cases/src/knx/test_memory.h b/test/lib-test-cases/src/knx/test_memory.h new file mode 100644 index 00000000..93824552 --- /dev/null +++ b/test/lib-test-cases/src/knx/test_memory.h @@ -0,0 +1,37 @@ +/* + * Test class for the memory.h + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + */ + +#ifndef TEST_SBLIB_KNX_MEMORY_H_ +#define TEST_SBLIB_KNX_MEMORY_H_ + +#include + + +class TestMemory final : public Memory +{ +public: + TestMemory() = delete; + TestMemory(const uint32_t start, const uint32_t size) : + Memory(start, size) {} + + //using Memory::startAddr; + //using Memory::endAddr; + //using Memory::size; + using Memory::normalizeAddress; + //using Memory::inRange; + + // Implement dummy abstract methods + byte& operator[](const uint32_t address) override { return dummyByte; } + [[nodiscard]] uint8_t getUInt8(uint32_t address) const override { return 0;} + [[nodiscard]] uint16_t getUInt16(uint32_t address) const override { return 0;} + +private: + uint8_t dummyByte = 0; +}; + +#endif /* TEST_SBLIB_KNX_MEMORY_H_ */ diff --git a/test/lib-test-cases/src/knx/test_user_eeprom.cpp b/test/lib-test-cases/src/knx/test_user_eeprom.cpp new file mode 100644 index 00000000..56cd60b0 --- /dev/null +++ b/test/lib-test-cases/src/knx/test_user_eeprom.cpp @@ -0,0 +1,62 @@ +/* + * Tests for the userEeprom.h + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + */ + +#include +#include +#include +#include +#include + +#include "test_user_eeprom.h" + + +struct TestSize +{ + uint32_t start; + uint32_t size; + uint32_t flashSize; + uint32_t resultFlashStart; +}; + +static std::vector testSizes = { + { 0, 1, 1, 0xffff }, // lower limit + { 0x100, 256, 256, 0xff00 }, // BCU 1 + { 0x100, 1024, 1024, 0xfc00 }, // BCU 2 + { 0x3f00, 3072, 4096, 0xf000 }, // Mask 0x0701, 0x0705 + { 0x3300, 3072, 4096, 0xf000 } // System B +}; + +TEST_CASE("User EEPROM test", "[userEeprom]") +{ + // Test declared ranges + for (const auto& [start, size, flashSize, resultFlashStart] : testSizes) + { + // Test a new mcu with erased flash + IAP_Init_Flash(0xff); + auto testUserEeprom = new TestUserEeprom(start, size, flashSize); + uint8_t* validPage = testUserEeprom->findValidPage(); + REQUIRE(validPage == nullptr); + delete testUserEeprom; + + // Test a mcu with erased flash + IAP_Init_Flash(0x31); + testUserEeprom = new TestUserEeprom(start, size, flashSize); + validPage = testUserEeprom->findValidPage(); + // uint32_t i = iapFlashSize(); + // while (i > 0) + // { + // if (&FLASH[i] == validPage) + // break; + // i--; + // } + // printf("i: %x\n", i); + REQUIRE(validPage == (FLASH + iapFlashSize() - flashSize)); + REQUIRE(validPage == (&FLASH[resultFlashStart])); + delete testUserEeprom; + } +} diff --git a/test/lib-test-cases/src/knx/test_user_eeprom.h b/test/lib-test-cases/src/knx/test_user_eeprom.h new file mode 100644 index 00000000..3e3daf05 --- /dev/null +++ b/test/lib-test-cases/src/knx/test_user_eeprom.h @@ -0,0 +1,51 @@ +/* + * Test class for the userEeprom.h + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + */ + +#ifndef TEST_SBLIB_KNX_USEREEPROM_H_ +#define TEST_SBLIB_KNX_USEREEPROM_H_ + +#include + + +class TestUserEeprom final : public UserEeprom +{ +public: + TestUserEeprom() = delete; + using UserEeprom::UserEeprom; + //TestUserEeprom(unsigned int start, unsigned int size, unsigned int flashSize); + using UserEeprom::findValidPage; + + [[nodiscard]] uint8_t& optionReg() const override { return userEepromData[0]; } + [[nodiscard]] uint8_t& manuDataH() const override { return userEepromData[1]; } + [[nodiscard]] uint8_t& manuDataL() const override { return userEepromData[2]; } + [[nodiscard]] uint8_t& manufacturerH() const override { return userEepromData[3]; } + [[nodiscard]] uint8_t& manufacturerL() const override { return userEepromData[4]; } + [[nodiscard]] uint8_t& deviceTypeH() const override { return userEepromData[5]; } + [[nodiscard]] uint8_t& deviceTypeL() const override { return userEepromData[6]; } + [[nodiscard]] uint8_t& version() const override { return userEepromData[7]; } + [[nodiscard]] uint8_t& checkLimit() const override { return userEepromData[8]; } + [[nodiscard]] uint8_t& appPeiType() const override { return userEepromData[9]; } + [[nodiscard]] uint8_t& syncRate() const override { return userEepromData[10]; } + [[nodiscard]] uint8_t& portCDDR() const override { return userEepromData[11]; } + [[nodiscard]] uint8_t& portADDR() const override { return userEepromData[12]; } + [[nodiscard]] uint8_t& runError() const override { return userEepromData[13]; } + [[nodiscard]] uint8_t& routeCnt() const override { return userEepromData[14]; } + [[nodiscard]] uint8_t& maxRetransmit() const override { return userEepromData[15]; } + [[nodiscard]] uint8_t& confDesc() const override { return userEepromData[16]; } + [[nodiscard]] uint8_t& assocTabPtr() const override { return userEepromData[17]; } + [[nodiscard]] uint8_t& commsTabPtr() const override { return userEepromData[18]; } + [[nodiscard]] uint8_t& usrInitPtr() const override { return userEepromData[19]; } + [[nodiscard]] uint8_t& usrProgPtr() const override { return userEepromData[20]; } + + [[nodiscard]] uint8_t& addrTabSize() const override { return userEepromData[21]; } + [[nodiscard]] uint8_t* addrTab() const override { return &userEepromData[22]; } +}; + + + +#endif /* TEST_SBLIB_KNX_USEREEPROM_H_ */