diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9e08c84
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+*.class
+
+.mtj.tmp/
+
+*.war
+*.ear
+
+hs_err_pid*
+log/
+target/
+
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/artifacts/Thermocycler_jar.xml b/.idea/artifacts/Thermocycler_jar.xml
new file mode 100644
index 0000000..7086dbf
--- /dev/null
+++ b/.idea/artifacts/Thermocycler_jar.xml
@@ -0,0 +1,15 @@
+
+
+ $PROJECT_DIR$/out/artifacts/Thermocycler_jar
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..391abe8
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..aa00ffa
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/git_toolbox_prj.xml b/.idea/git_toolbox_prj.xml
new file mode 100644
index 0000000..02b915b
--- /dev/null
+++ b/.idea/git_toolbox_prj.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 0000000..cd6af77
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jpa-buddy.xml b/.idea/jpa-buddy.xml
new file mode 100644
index 0000000..966d5f5
--- /dev/null
+++ b/.idea/jpa-buddy.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..2d59d7b
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..c16cc57
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/sonarlint/issuestore/index.pb b/.idea/sonarlint/issuestore/index.pb
new file mode 100644
index 0000000..e69de29
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.project b/.project
new file mode 100644
index 0000000..b51ffba
--- /dev/null
+++ b/.project
@@ -0,0 +1,23 @@
+
+
+ Thermocycler
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..99f26c0
--- /dev/null
+++ b/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/=UTF-8
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..6e80039
--- /dev/null
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,8 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/.settings/org.eclipse.jdt.ui.prefs b/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 0000000..c29fe85
--- /dev/null
+++ b/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,59 @@
+cleanup.add_default_serial_version_id=true
+cleanup.add_generated_serial_version_id=false
+cleanup.add_missing_annotations=true
+cleanup.add_missing_deprecated_annotations=true
+cleanup.add_missing_methods=false
+cleanup.add_missing_nls_tags=false
+cleanup.add_missing_override_annotations=true
+cleanup.add_missing_override_annotations_interface_methods=true
+cleanup.add_serial_version_id=false
+cleanup.always_use_blocks=true
+cleanup.always_use_parentheses_in_expressions=false
+cleanup.always_use_this_for_non_static_field_access=true
+cleanup.always_use_this_for_non_static_method_access=false
+cleanup.convert_functional_interfaces=false
+cleanup.convert_to_enhanced_for_loop=false
+cleanup.correct_indentation=true
+cleanup.format_source_code=true
+cleanup.format_source_code_changes_only=false
+cleanup.insert_inferred_type_arguments=false
+cleanup.make_local_variable_final=true
+cleanup.make_parameters_final=false
+cleanup.make_private_fields_final=true
+cleanup.make_type_abstract_if_missing_method=false
+cleanup.make_variable_declarations_final=false
+cleanup.never_use_blocks=false
+cleanup.never_use_parentheses_in_expressions=true
+cleanup.organize_imports=true
+cleanup.qualify_static_field_accesses_with_declaring_class=false
+cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+cleanup.qualify_static_member_accesses_with_declaring_class=true
+cleanup.qualify_static_method_accesses_with_declaring_class=false
+cleanup.remove_private_constructors=true
+cleanup.remove_redundant_type_arguments=false
+cleanup.remove_trailing_whitespaces=true
+cleanup.remove_trailing_whitespaces_all=true
+cleanup.remove_trailing_whitespaces_ignore_empty=false
+cleanup.remove_unnecessary_casts=true
+cleanup.remove_unnecessary_nls_tags=true
+cleanup.remove_unused_imports=true
+cleanup.remove_unused_local_variables=false
+cleanup.remove_unused_private_fields=true
+cleanup.remove_unused_private_members=false
+cleanup.remove_unused_private_methods=true
+cleanup.remove_unused_private_types=true
+cleanup.sort_members=true
+cleanup.sort_members_all=false
+cleanup.use_anonymous_class_creation=false
+cleanup.use_blocks=false
+cleanup.use_blocks_only_for_return_and_throw=false
+cleanup.use_lambda=true
+cleanup.use_parentheses_in_expressions=false
+cleanup.use_this_for_non_static_field_access=true
+cleanup.use_this_for_non_static_field_access_only_if_necessary=false
+cleanup.use_this_for_non_static_method_access=false
+cleanup.use_this_for_non_static_method_access_only_if_necessary=true
+cleanup_profile=_DejanFullCleanup
+cleanup_settings_version=2
+eclipse.preferences.version=1
diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 0000000..f897a7f
--- /dev/null
+++ b/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/META-INF/MANIFEST.MF b/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..2f15928
--- /dev/null
+++ b/META-INF/MANIFEST.MF
@@ -0,0 +1,3 @@
+Manifest-Version: 1.0
+Main-Class: com.codlex.thermocycler.view.ThermocyclerGUI
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..66568c5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# raspberrypi-thermocycler
+After working for couple of months on arduino-thermocycler, we turned to Raspberry Pi to have more powerful machine and easier development in Java.
diff --git a/Thermocycler.iml b/Thermocycler.iml
new file mode 100644
index 0000000..43cdacd
--- /dev/null
+++ b/Thermocycler.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/builds/1.0/ThermocyclerState.bin b/builds/1.0/ThermocyclerState.bin
new file mode 100644
index 0000000..b393a52
--- /dev/null
+++ b/builds/1.0/ThermocyclerState.bin
@@ -0,0 +1,6 @@
+55
+30
+5
+30
+24000
+15478
diff --git a/builds/1.0/deploy.sh b/builds/1.0/deploy.sh
new file mode 100755
index 0000000..62a1b2e
--- /dev/null
+++ b/builds/1.0/deploy.sh
@@ -0,0 +1,5 @@
+./killRemote.sh
+scp -r tc.jar pi@pi:~/tc/
+scp *.properties pi@pi:~/tc/
+scp *.sh pi@pi:~/tc/
+./startRemote.sh
diff --git a/builds/1.0/killRemote.sh b/builds/1.0/killRemote.sh
new file mode 100755
index 0000000..5cbfd92
--- /dev/null
+++ b/builds/1.0/killRemote.sh
@@ -0,0 +1,2 @@
+ssh pi@pi "sudo killall java"
+
diff --git a/builds/1.0/log4j.properties b/builds/1.0/log4j.properties
new file mode 100644
index 0000000..5f6b4ef
--- /dev/null
+++ b/builds/1.0/log4j.properties
@@ -0,0 +1,17 @@
+log4j.rootLogger=DEBUG, ConsoleAppender, DebugFileAppender, ErrorFileAppender
+
+log4j.appender.ConsoleAppender=org.apache.log4j.ConsoleAppender
+log4j.appender.ConsoleAppender.layout=org.apache.log4j.PatternLayout
+log4j.appender.ConsoleAppender.layout.ConversionPattern=%-7p %d [%t] %c %x - %m%n
+
+log4j.appender.DebugFileAppender=org.apache.log4j.FileAppender
+log4j.appender.ErrorFileAppender.Threshold=DEBUG
+log4j.appender.DebugFileAppender.File=log/debug.log
+log4j.appender.DebugFileAppender.layout=org.apache.log4j.PatternLayout
+log4j.appender.DebugFileAppender.layout.ConversionPattern=%-7p %d [%t] %c %x - %m%n
+
+log4j.appender.ErrorFileAppender=org.apache.log4j.FileAppender
+log4j.appender.ErrorFileAppender.Threshold=ERROR
+log4j.appender.ErrorFileAppender.File=log/error.log
+log4j.appender.ErrorFileAppender.layout=org.apache.log4j.PatternLayout
+log4j.appender.ErrorFileAppender.layout.ConversionPattern=%-7p %d [%t] %c %x - %m%n
\ No newline at end of file
diff --git a/builds/1.0/settings.properties b/builds/1.0/settings.properties
new file mode 100644
index 0000000..22eea85
--- /dev/null
+++ b/builds/1.0/settings.properties
@@ -0,0 +1,44 @@
+Production = true
+FullScreen = false
+TranslationTimeMillis = 1500
+DistanceRefreshMillis = 1000
+TemperatureRefreshMillis = 1000
+
+# bath
+BathMinimumLevel = 60
+BathDepth = 20
+LevelEpsilon = 1
+TemperatureEpsilon = 1
+
+# hot bath
+HotBathLevelEchoPin = 4
+HotBathLevelTriggerPin = 3
+HotBathHeaterPin = 29
+HotBathWaterPump = 27
+HotBathCirculationWaterPump = 28
+
+# cold bath
+ColdBathLevelEchoPin = 1
+ColdBathLevelTriggerPin = 0
+ColdBathCoolerPin = 21
+ColdBathWaterPump = 25
+ColdBathEpsOn = 2
+ColdBathEpsOff = 5
+
+# translator
+TranslatorPowerPin = 24
+ # indices for one wire
+TranslatorPulsePin = 22
+TranslatorToColdDirection = 26
+
+# validation
+ValidationCyclesRange = 1, 999999
+ValidationHotTemperatureRange = 20, 100
+ValidationColdTemperatureRange = -6, 20
+ValidationTimeRange = 1, 300
+
+# safety
+Safety = true
+SafetyHotBathTemperatureMax = 120
+SafetyLevelMin = 30
+SafetyLevelMax = 120
diff --git a/builds/1.0/start.sh b/builds/1.0/start.sh
new file mode 100755
index 0000000..a05dbea
--- /dev/null
+++ b/builds/1.0/start.sh
@@ -0,0 +1,3 @@
+sudo service lightdm stop &
+# sudo java -jar -Dcom.sun.javafx.touch=true -Djavafx.platform=eglfb -Dcom.sun.javafx.isEmbedded=false Test.jar &
+sudo java -jar tc.jar &> log/startup.log &
\ No newline at end of file
diff --git a/builds/1.0/start2.sh b/builds/1.0/start2.sh
new file mode 100755
index 0000000..22ee603
--- /dev/null
+++ b/builds/1.0/start2.sh
@@ -0,0 +1,3 @@
+sudo service lightdm stop &
+# sudo java -jar -Dcom.sun.javafx.touch=true -Djavafx.platform=eglfb -Dcom.sun.javafx.isEmbedded=false Test.jar &
+sudo java -jar test.jar &> log/startup.log &
diff --git a/builds/1.0/startRemote.sh b/builds/1.0/startRemote.sh
new file mode 100755
index 0000000..4d374b6
--- /dev/null
+++ b/builds/1.0/startRemote.sh
@@ -0,0 +1 @@
+ssh pi@pi "cd tc && ./start.sh"
diff --git a/builds/1.0/tc.jar b/builds/1.0/tc.jar
new file mode 100755
index 0000000..516194d
Binary files /dev/null and b/builds/1.0/tc.jar differ
diff --git a/builds/1.0/test.jar b/builds/1.0/test.jar
new file mode 100644
index 0000000..51c296b
Binary files /dev/null and b/builds/1.0/test.jar differ
diff --git a/builds/3.0/Thermocycler.jar b/builds/3.0/Thermocycler.jar
new file mode 100755
index 0000000..6c96e9f
Binary files /dev/null and b/builds/3.0/Thermocycler.jar differ
diff --git a/builds/3.0/deploy.sh b/builds/3.0/deploy.sh
new file mode 100755
index 0000000..847ec60
--- /dev/null
+++ b/builds/3.0/deploy.sh
@@ -0,0 +1,5 @@
+./killRemote.sh
+scp *.jar pi@pi:~/tc3/
+scp *.properties pi@pi:~/tc3/
+scp *.sh pi@pi:~/tc3/
+./startRemote.sh
diff --git a/builds/3.0/killRemote.sh b/builds/3.0/killRemote.sh
new file mode 100755
index 0000000..5cbfd92
--- /dev/null
+++ b/builds/3.0/killRemote.sh
@@ -0,0 +1,2 @@
+ssh pi@pi "sudo killall java"
+
diff --git a/builds/3.0/log4j.properties b/builds/3.0/log4j.properties
new file mode 100644
index 0000000..5f6b4ef
--- /dev/null
+++ b/builds/3.0/log4j.properties
@@ -0,0 +1,17 @@
+log4j.rootLogger=DEBUG, ConsoleAppender, DebugFileAppender, ErrorFileAppender
+
+log4j.appender.ConsoleAppender=org.apache.log4j.ConsoleAppender
+log4j.appender.ConsoleAppender.layout=org.apache.log4j.PatternLayout
+log4j.appender.ConsoleAppender.layout.ConversionPattern=%-7p %d [%t] %c %x - %m%n
+
+log4j.appender.DebugFileAppender=org.apache.log4j.FileAppender
+log4j.appender.ErrorFileAppender.Threshold=DEBUG
+log4j.appender.DebugFileAppender.File=log/debug.log
+log4j.appender.DebugFileAppender.layout=org.apache.log4j.PatternLayout
+log4j.appender.DebugFileAppender.layout.ConversionPattern=%-7p %d [%t] %c %x - %m%n
+
+log4j.appender.ErrorFileAppender=org.apache.log4j.FileAppender
+log4j.appender.ErrorFileAppender.Threshold=ERROR
+log4j.appender.ErrorFileAppender.File=log/error.log
+log4j.appender.ErrorFileAppender.layout=org.apache.log4j.PatternLayout
+log4j.appender.ErrorFileAppender.layout.ConversionPattern=%-7p %d [%t] %c %x - %m%n
\ No newline at end of file
diff --git a/builds/3.0/settings.properties b/builds/3.0/settings.properties
new file mode 100644
index 0000000..ab22ae5
--- /dev/null
+++ b/builds/3.0/settings.properties
@@ -0,0 +1,63 @@
+Production = true
+FullScreen = false
+TrackingOn = true
+
+TranslationTimeMillis = 1500
+DistanceRefreshMillis = 1000
+TemperatureRefreshMillis = 1000
+
+# bath
+BathMinimumLevel = 60
+BathDepth = 20
+LevelEpsilon = 1
+TemperatureEpsilon = 1
+
+# hot bath
+HotBathLevelEchoPin = 4
+HotBathLevelTriggerPin = 3
+HotBathHeaterPin = 28
+HotBathWaterPump = 27
+HotBathCirculationWaterPump = 29
+CirculationPumpOn = false
+FromHotBathPause = 0
+FromHotBathMiddlePause = 0
+MiddlePausePulseDurationFromHot = 0
+
+# cold bath
+ColdBathLevelEchoPin = 1
+ColdBathLevelTriggerPin = 0
+ColdBathCoolerPin = 21
+ColdBathWaterPump = 25
+ColdBathEpsOn = 2
+ColdBathEpsOff = 5
+FromColdBathPause = 0
+FromColdBathMiddlePause = 0
+MiddlePausePulseDurationFromCold = 0
+
+# translator
+TranslatorPowerPin = 24
+ # indices for one wire
+TranslatorPulsePin = 22
+TranslatorToColdDirection = 26
+
+# validation
+ValidationCyclesRange = 1, 999999
+ValidationHotTemperatureRange = 10, 100
+ValidationColdTemperatureRange = -6, 30
+ValidationTimeRange = 1, 1800
+
+# safety
+Safety = true
+SafetyHotBathTemperatureMax = 120
+SafetyLevelMin = 30
+SafetyLevelMax = 120
+
+KeepLevelOn = true
+
+# sensors
+HotBathTemperatureSensor1 = 28-0417838278ff
+HotBathTemperatureSensor2 = 28-051790ae46ff
+HotBathTemperatureSensor3 = 28-0417838287ff
+ColdBathTemperatureSensor1 = 28-04178380a8ff
+ColdBathTemperatureSensor2 = 28-0517907c6dff
+ColdBathTemperatureSensorAntifriz = 28-0417837701ff
\ No newline at end of file
diff --git a/builds/3.0/start.sh b/builds/3.0/start.sh
new file mode 100755
index 0000000..b929dc2
--- /dev/null
+++ b/builds/3.0/start.sh
@@ -0,0 +1,3 @@
+sudo service lightdm stop &
+# sudo java -jar -Dcom.sun.javafx.touch=true -Djavafx.platform=eglfb -Dcom.sun.javafx.isEmbedded=false Test.jar &
+sudo java -jar Thermocycler.jar &> log/startup.log &
diff --git a/builds/3.0/startRemote.sh b/builds/3.0/startRemote.sh
new file mode 100755
index 0000000..2086958
--- /dev/null
+++ b/builds/3.0/startRemote.sh
@@ -0,0 +1 @@
+ssh pi@pi "cd tc3 && ./start.sh"
diff --git a/builds/3.0/update.sh b/builds/3.0/update.sh
new file mode 100755
index 0000000..124404b
--- /dev/null
+++ b/builds/3.0/update.sh
@@ -0,0 +1,26 @@
+
+echo "Updating jar..."
+rm -f Thermocycler.jar
+wget https://raw.githubusercontent.com/Codlex/raspberrypi-thermocycler/master/builds/3.0/Thermocycler.jar
+chmod +x Thermocycler.jar
+echo "Jar updated!"
+
+echo "Updating settings..."
+rm -f settings.properties
+wget https://raw.githubusercontent.com/Codlex/raspberrypi-thermocycler/master/builds/3.0/settings.properties
+echo "Settings updated!"
+
+echo "Updating start script..."
+rm -f start.sh
+wget https://raw.githubusercontent.com/Codlex/raspberrypi-thermocycler/master/builds/3.0/start.sh
+chmod +x start.sh
+echo "Start script updated!"
+
+echo "Updating log4..."
+rm -f log4j.properties
+wget https://raw.githubusercontent.com/Codlex/raspberrypi-thermocycler/master/builds/3.0/log4j.properties
+echo "Log4 updated!"
+
+mkdir log
+
+echo "Thermocycler update complete!"
diff --git a/deploy.sh b/deploy.sh
new file mode 100755
index 0000000..62a1b2e
--- /dev/null
+++ b/deploy.sh
@@ -0,0 +1,5 @@
+./killRemote.sh
+scp -r tc.jar pi@pi:~/tc/
+scp *.properties pi@pi:~/tc/
+scp *.sh pi@pi:~/tc/
+./startRemote.sh
diff --git a/killRemote.sh b/killRemote.sh
new file mode 100755
index 0000000..5cbfd92
--- /dev/null
+++ b/killRemote.sh
@@ -0,0 +1,2 @@
+ssh pi@pi "sudo killall java"
+
diff --git a/log4j.properties b/log4j.properties
new file mode 100644
index 0000000..5f6b4ef
--- /dev/null
+++ b/log4j.properties
@@ -0,0 +1,17 @@
+log4j.rootLogger=DEBUG, ConsoleAppender, DebugFileAppender, ErrorFileAppender
+
+log4j.appender.ConsoleAppender=org.apache.log4j.ConsoleAppender
+log4j.appender.ConsoleAppender.layout=org.apache.log4j.PatternLayout
+log4j.appender.ConsoleAppender.layout.ConversionPattern=%-7p %d [%t] %c %x - %m%n
+
+log4j.appender.DebugFileAppender=org.apache.log4j.FileAppender
+log4j.appender.ErrorFileAppender.Threshold=DEBUG
+log4j.appender.DebugFileAppender.File=log/debug.log
+log4j.appender.DebugFileAppender.layout=org.apache.log4j.PatternLayout
+log4j.appender.DebugFileAppender.layout.ConversionPattern=%-7p %d [%t] %c %x - %m%n
+
+log4j.appender.ErrorFileAppender=org.apache.log4j.FileAppender
+log4j.appender.ErrorFileAppender.Threshold=ERROR
+log4j.appender.ErrorFileAppender.File=log/error.log
+log4j.appender.ErrorFileAppender.layout=org.apache.log4j.PatternLayout
+log4j.appender.ErrorFileAppender.layout.ConversionPattern=%-7p %d [%t] %c %x - %m%n
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..ae36431
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,60 @@
+
+ 4.0.0
+
+ com.codlex
+ Thermocycler
+ 0.0.1-SNAPSHOT
+ jar
+
+ Thermocycler
+ http://maven.apache.org
+
+
+ UTF-8
+ 8
+ 8
+
+
+
+
+ junit
+ junit
+ 3.8.1
+ test
+
+
+
+ com.pi4j
+ pi4j-core
+ 1.1
+
+
+
+ log4j
+ log4j
+ 1.2.17
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.24
+
+
+
+ com.google.guava
+ guava
+ 19.0
+
+
+
+
+
+
+ projectlombok.org
+ http://projectlombok.org/mavenrepo
+
+
+
+
diff --git a/settings.properties b/settings.properties
new file mode 100644
index 0000000..a263acc
--- /dev/null
+++ b/settings.properties
@@ -0,0 +1,63 @@
+Production = false
+FullScreen = false
+TrackingOn = false
+
+TranslationTimeMillis = 1500
+DistanceRefreshMillis = 1000
+TemperatureRefreshMillis = 1000
+
+# bath
+BathMinimumLevel = 60
+BathDepth = 20
+LevelEpsilon = 1
+TemperatureEpsilon = 1
+
+# hot bath
+HotBathLevelEchoPin = 4
+HotBathLevelTriggerPin = 3
+HotBathHeaterPin = 28
+HotBathWaterPump = 27
+HotBathCirculationWaterPump = 29
+CirculationPumpOn = false
+FromHotBathPause = 30000
+FromHotBathMiddlePause = 11000
+MiddlePausePulseDurationFromHot = 200
+
+# cold bath
+ColdBathLevelEchoPin = 1
+ColdBathLevelTriggerPin = 0
+ColdBathCoolerPin = 21
+ColdBathWaterPump = 25
+ColdBathEpsOn = 2
+ColdBathEpsOff = 5
+FromColdBathPause = 30000
+FromColdBathMiddlePause = 10000
+MiddlePausePulseDurationFromCold = 200
+
+# translator
+TranslatorPowerPin = 24
+ # indices for one wire
+TranslatorPulsePin = 22
+TranslatorToColdDirection = 26
+
+# validation
+ValidationCyclesRange = 1, 999999
+ValidationHotTemperatureRange = 10, 100
+ValidationColdTemperatureRange = -6, 30
+ValidationTimeRange = 1, 1800
+
+# safety
+Safety = true
+SafetyHotBathTemperatureMax = 120
+SafetyLevelMin = 30
+SafetyLevelMax = 120
+
+KeepLevelOn = true
+
+# sensors
+HotBathTemperatureSensor1 = 28-0417838278ff
+HotBathTemperatureSensor2 = 28-051790ae46ff
+HotBathTemperatureSensor3 = 28-0417838287ff
+ColdBathTemperatureSensor1 = 28-04178380a8ff
+ColdBathTemperatureSensor2 = 28-0517907c6dff
+ColdBathTemperatureSensorAntifriz = 28-0417837701ff
\ No newline at end of file
diff --git a/src/com/codlex/thermocycler/hardware/AbstractSwitch.java b/src/com/codlex/thermocycler/hardware/AbstractSwitch.java
new file mode 100644
index 0000000..123e08e
--- /dev/null
+++ b/src/com/codlex/thermocycler/hardware/AbstractSwitch.java
@@ -0,0 +1,5 @@
+package com.codlex.thermocycler.hardware;
+
+public class AbstractSwitch {
+
+}
diff --git a/src/com/codlex/thermocycler/hardware/DallasTemperatureSensor.java b/src/com/codlex/thermocycler/hardware/DallasTemperatureSensor.java
new file mode 100644
index 0000000..68ba863
--- /dev/null
+++ b/src/com/codlex/thermocycler/hardware/DallasTemperatureSensor.java
@@ -0,0 +1,66 @@
+package com.codlex.thermocycler.hardware;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.time.Duration;
+
+import com.codlex.thermocycler.logic.Settings;
+
+public class DallasTemperatureSensor extends RefreshedSensor {
+
+ private static File deriveValueFile(File sensorFile) {
+ return new File(sensorFile, "w1_slave");
+ }
+
+ private final File sensorFile;
+
+ private final File valueFile;
+
+ DallasTemperatureSensor(File sensorFile) {
+ super(Duration.ofMillis(Settings.get().getTemperatureRefreshMillis()));
+ this.sensorFile = sensorFile;
+ this.valueFile = deriveValueFile(sensorFile);
+ }
+
+ @Override
+ protected Float getDefaultValue() {
+ return 20.0f;
+ }
+
+ @Override
+ public String getID() {
+ return this.sensorFile.getName();
+ }
+
+ @Override
+ protected Float recalculateValue()
+ throws FileNotFoundException, IOException {
+ try (BufferedReader reader = new BufferedReader(
+ new FileReader(this.valueFile))) {
+ String tmp = reader.readLine();
+ int index = -1;
+ while (tmp != null) {
+ index = tmp.indexOf("t=");
+ if (index >= 0) {
+ break;
+ }
+ tmp = reader.readLine();
+ }
+
+ if (index < 0) {
+ throw new IOException("Could not read sensor " + getID());
+ }
+
+ return Integer.parseInt(tmp.substring(index + 2)) / 1000f;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Sensor ID: %s, Temperature: %2.3fC", getID(),
+ getValue());
+ }
+}
diff --git a/src/com/codlex/thermocycler/hardware/DistanceSensor.java b/src/com/codlex/thermocycler/hardware/DistanceSensor.java
new file mode 100644
index 0000000..475734f
--- /dev/null
+++ b/src/com/codlex/thermocycler/hardware/DistanceSensor.java
@@ -0,0 +1,141 @@
+package com.codlex.thermocycler.hardware;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+import com.codlex.thermocycler.logic.Settings;
+import com.google.common.base.Stopwatch;
+
+/**
+ * Class to monitor distance measured by an HC-SR04 distance sensor on a
+ * Raspberry Pi.
+ *
+ * The main method assumes the trig pin is connected to the pin # 7 and the echo
+ * pin is connected to pin # 11. Output of the program are comma separated lines
+ * where the first value is the number of milliseconds since unix epoch, and the
+ * second value is the measured distance in centimeters.
+ */
+
+import com.pi4j.io.gpio.GpioController;
+import com.pi4j.io.gpio.GpioFactory;
+import com.pi4j.io.gpio.GpioPinDigitalInput;
+import com.pi4j.io.gpio.GpioPinDigitalOutput;
+import com.pi4j.io.gpio.Pin;
+
+import javafx.util.Pair;
+import lombok.extern.log4j.Log4j;
+
+@Log4j
+public class DistanceSensor extends RefreshedSensor {
+
+ /**
+ * Exception thrown when timeout occurs
+ */
+ public static class TimeoutException extends Exception {
+
+ private final String reason;
+
+ public TimeoutException(String reason) {
+ this.reason = reason;
+ }
+
+ @Override
+ public String toString() {
+ return this.reason;
+ }
+ }
+
+ private final static float SOUND_SPEED = 343.2f; // speed of sound in m/s
+ private final static int TRIG_DURATION_IN_MICROS = 10; // trigger duration
+ // of 10 micro s
+ private final static int TIMEOUT = 2100;
+
+ private final static GpioController gpio = GpioFactory.getInstance();
+
+ private final GpioPinDigitalInput echoPin;
+
+ private final GpioPinDigitalOutput trigPin;
+
+ DistanceSensor(Pin echoPin, Pin trigPin) {
+ super(Duration.ofMillis(Settings.get().getDistanceRefreshMillis()));
+ this.echoPin = gpio.provisionDigitalInputPin(echoPin);
+ this.trigPin = gpio.provisionDigitalOutputPin(trigPin);
+ this.trigPin.low();
+ }
+
+ @Override
+ protected Float getDefaultValue() {
+ return 0.0f;
+ }
+
+ @Override
+ public String getID() {
+ // TODO: this is dummy implementations
+ return new Pair(this.echoPin.getPin(), this.trigPin.getPin()).toString();
+ }
+
+ /**
+ * @return the duration of the signal in micro seconds
+ * @throws DistanceSensor.TimeoutException
+ * if no low appears in time
+ */
+ private long measureSignal() throws TimeoutException {
+ int countdown = TIMEOUT;
+ long start = System.nanoTime();
+ while (this.echoPin.isHigh() && countdown > 0) {
+ countdown--;
+ }
+ long end = System.nanoTime();
+
+ if (countdown <= 0) {
+ throw new TimeoutException("Timeout waiting for signal end");
+ }
+
+ return (long) Math.ceil((end - start) / 1000.0); // Return micro seconds
+ }
+
+ @Override
+ public Float recalculateValue() throws TimeoutException {
+ this.triggerSensor();
+ this.waitForSignal();
+ long duration = this.measureSignal();
+ float seconds = (float) (1e-6 * duration);
+ float meters = (float) ((seconds * SOUND_SPEED) / 2.0);
+ float distanceInCm = meters * 100;
+ return distanceInCm;
+ }
+
+ /**
+ * Put a high on the trig pin for TRIG_DURATION_IN_MICROS
+ */
+ private void triggerSensor() {
+ this.trigPin.high();
+ Stopwatch stopwatch = Stopwatch.createStarted();
+ while (stopwatch.elapsed(TimeUnit.MICROSECONDS) < TRIG_DURATION_IN_MICROS) {
+ // wait
+ }
+ this.trigPin.low();
+ }
+
+ /**
+ * Wait for a high on the echo pin
+ *
+ * @throws DistanceSensor.TimeoutException
+ * if no high appears in time
+ */
+ private void waitForSignal() throws TimeoutException {
+ int countdown = TIMEOUT;
+ while (this.echoPin.isLow() && countdown > 0) {
+ countdown--;
+ }
+
+ if (countdown <= 0) {
+ throw new TimeoutException("Timeout waiting for signal start");
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "(pins = " + this.echoPin.getName() + ", " + this.trigPin.getName() + ")";
+ }
+}
\ No newline at end of file
diff --git a/src/com/codlex/thermocycler/hardware/HardwareProvider.java b/src/com/codlex/thermocycler/hardware/HardwareProvider.java
new file mode 100644
index 0000000..7a7a26b
--- /dev/null
+++ b/src/com/codlex/thermocycler/hardware/HardwareProvider.java
@@ -0,0 +1,46 @@
+package com.codlex.thermocycler.hardware;
+
+import com.codlex.thermocycler.logic.Settings;
+import com.pi4j.io.gpio.Pin;
+
+public class HardwareProvider {
+
+ private static final HardwareProvider INSTANCE = new HardwareProvider();
+
+ public static HardwareProvider get() {
+ return INSTANCE;
+ }
+
+ public Sensor getDistanceSensorForPins(Pin echo, Pin trigger) {
+ if (Settings.get().getProduction()) {
+ return new DistanceSensor(echo, trigger);
+ } else {
+ return VirtualSensors.get().getDistanceSensor(echo, trigger);
+ }
+ }
+
+ public Switch getSwitch(Pin pin, String name) {
+ if (Settings.get().getProduction()) {
+ return new SimpleSwitch(pin, name);
+ } else {
+ return VirtualSwitches.get().getSwitch(pin);
+ }
+ }
+
+ public Switch getSwitch(Pin pin, String name, boolean inverse) {
+ if (Settings.get().getProduction()) {
+ return new SimpleSwitch(pin, name, inverse);
+ } else {
+ return VirtualSwitches.get().getSwitch(pin);
+ }
+ }
+
+ public Sensor getTemperatureSensor(String sensorId) {
+ if (Settings.get().getProduction()) {
+ return Sensors.getSensorById(sensorId).get();
+ } else {
+ return VirtualSensors.get().getTemperatureSensorById(sensorId);
+ }
+ }
+
+}
diff --git a/src/com/codlex/thermocycler/hardware/OnOffDevice.java b/src/com/codlex/thermocycler/hardware/OnOffDevice.java
new file mode 100644
index 0000000..f846c56
--- /dev/null
+++ b/src/com/codlex/thermocycler/hardware/OnOffDevice.java
@@ -0,0 +1,23 @@
+package com.codlex.thermocycler.hardware;
+
+import com.pi4j.io.gpio.Pin;
+
+import lombok.Getter;
+
+public class OnOffDevice {
+
+ @Getter
+ private Switch controller;
+
+ public OnOffDevice(Pin pin, String name) {
+ this.controller = HardwareProvider.get().getSwitch(pin, name);
+ }
+
+ public void turnOff() {
+ this.controller.turnOff();
+ }
+
+ public void turnOn() {
+ this.controller.turnOn();
+ }
+}
diff --git a/src/com/codlex/thermocycler/hardware/RefreshedSensor.java b/src/com/codlex/thermocycler/hardware/RefreshedSensor.java
new file mode 100644
index 0000000..6d83868
--- /dev/null
+++ b/src/com/codlex/thermocycler/hardware/RefreshedSensor.java
@@ -0,0 +1,79 @@
+package com.codlex.thermocycler.hardware;
+
+import java.time.Duration;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import com.google.common.base.Stopwatch;
+
+import lombok.extern.log4j.Log4j;
+
+/**
+ * Refreshed sensor refreshes itself periodically on it's own Thread not to
+ * block main execution of logic.
+ */
+@Log4j
+abstract class RefreshedSensor implements Sensor {
+
+ private final ScheduledExecutorService worker = Executors
+ .newSingleThreadScheduledExecutor();
+ private final AtomicReference valueContainer = new AtomicReference<>();
+ private final Duration refreshInterval;
+ private ScheduledFuture> measuringTask;
+
+ protected RefreshedSensor(final Duration refreshInterval) {
+ this.refreshInterval = refreshInterval;
+ }
+
+ protected abstract Value getDefaultValue();
+
+ @Override
+ public final Value getValue() {
+ if (this.valueContainer.get() == null) {
+ log.error("Not initialized sensor: " + getClass().getName());
+ return getDefaultValue();
+ }
+ return this.valueContainer.get();
+ }
+
+ protected abstract Value recalculateValue() throws Exception;
+
+ private final void refreshValue() {
+ try {
+ Stopwatch stopwatch = Stopwatch.createStarted();
+ this.valueContainer.set(recalculateValue());
+ final int capForLog = 1000;
+ if (stopwatch.elapsed(TimeUnit.MILLISECONDS) > capForLog) {
+ log.warn(getClass().getSimpleName() + " took "
+ + stopwatch.elapsed(TimeUnit.MILLISECONDS)
+ + " to measure value.");
+ }
+
+ } catch (Exception e) {
+ log.error(this
+ + " failed to recalculate value: ", e);
+ }
+ }
+
+ @Override
+ public final void startMeasuring() {
+ this.measuringTask = this.worker.scheduleAtFixedRate(this::refreshValue,
+ 0, this.refreshInterval.toMillis(), TimeUnit.MILLISECONDS);
+ }
+
+ public final void stopMeasuring() {
+ if (this.measuringTask == null) {
+ throw new RuntimeException(
+ "Tried to stop measuring but didn't start.");
+ }
+
+ boolean success = this.measuringTask.cancel(false);
+
+ if (!success) {
+ log.error("Failed to cancel task of RefreshedSensor.");
+ }
+ }
+}
diff --git a/src/com/codlex/thermocycler/hardware/Sensor.java b/src/com/codlex/thermocycler/hardware/Sensor.java
new file mode 100644
index 0000000..102817f
--- /dev/null
+++ b/src/com/codlex/thermocycler/hardware/Sensor.java
@@ -0,0 +1,10 @@
+package com.codlex.thermocycler.hardware;
+
+public interface Sensor {
+
+ String getID();
+
+ Value getValue();
+
+ void startMeasuring();
+}
diff --git a/src/com/codlex/thermocycler/hardware/Sensors.java b/src/com/codlex/thermocycler/hardware/Sensors.java
new file mode 100644
index 0000000..cea7702
--- /dev/null
+++ b/src/com/codlex/thermocycler/hardware/Sensors.java
@@ -0,0 +1,55 @@
+package com.codlex.thermocycler.hardware;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+
+import lombok.extern.log4j.Log4j;
+
+/**
+ * Main API entry point. Usage: {@link Sensors#getSensors()}.
+ *
+ * @author Marcus Hirt
+ */
+@Log4j
+class Sensors {
+ private static void addDallasSensors(Set> sensors)
+ throws IOException {
+ File sensorFolder = new File("/sys/bus/w1/devices");
+ if (!sensorFolder.exists()) {
+ System.err.println(
+ "Modules w1-gpio do not seem to be enabled. If you want to use DS18[B|S]20 sensors, please enable them.");
+ return;
+ }
+ if (sensorFolder.list().length == 0) {
+ System.err.println(
+ "The w1-gpio module seem to be set up, but no DS18[B|S]20 devices were found. If no such devices are connected, this is fine. Otherwise please check to see that w1-therm is loaded, that you have added dtoverlay=w1-gpio to your /boot/config.txt, and that the devices are properly connected.");
+ return;
+ }
+ for (File f : sensorFolder.listFiles()) {
+ if (f.getName().startsWith("w1_bus_master")) {
+ continue;
+ }
+ DallasTemperatureSensor sensor = new DallasTemperatureSensor(f);
+ sensors.add(sensor);
+ }
+ }
+
+ public static Optional> getSensorById(final String id) {
+ try {
+ return getSensors().stream()
+ .filter((sensor) -> id.equals(sensor.getID())).findFirst();
+ } catch (IOException e) {
+ log.error("Sensor not found", e);
+ return Optional.empty();
+ }
+ }
+
+ public static Set> getSensors() throws IOException {
+ Set> sensors = new HashSet>();
+ addDallasSensors(sensors);
+ return sensors;
+ }
+}
diff --git a/src/com/codlex/thermocycler/hardware/SimpleSwitch.java b/src/com/codlex/thermocycler/hardware/SimpleSwitch.java
new file mode 100644
index 0000000..f55385f
--- /dev/null
+++ b/src/com/codlex/thermocycler/hardware/SimpleSwitch.java
@@ -0,0 +1,69 @@
+package com.codlex.thermocycler.hardware;
+
+import com.codlex.thermocycler.tracker.Tracker;
+import com.pi4j.io.gpio.GpioController;
+import com.pi4j.io.gpio.GpioFactory;
+import com.pi4j.io.gpio.GpioPinDigitalOutput;
+import com.pi4j.io.gpio.Pin;
+import com.pi4j.io.gpio.PinState;
+
+import lombok.extern.log4j.Log4j;
+
+@Log4j
+public class SimpleSwitch implements Switch {
+
+ private enum SwitchState {
+ On, Off
+ };
+
+ static final PinState ON_CURRENT = PinState.HIGH;
+ static final PinState OFF_CURRENT = PinState.LOW;
+
+ private SwitchState state = SwitchState.Off;
+ private GpioPinDigitalOutput pin;
+ private PinState onCurrent;
+ private PinState offCurrent;
+ private final String name;
+
+ SimpleSwitch(Pin pinNumber, String name) {
+ this(pinNumber, name, true);
+ }
+
+ public SimpleSwitch(Pin pinNumber, String name, boolean inverse) {
+ final GpioController gpio = GpioFactory.getInstance();
+
+ if (inverse) {
+ this.onCurrent = OFF_CURRENT;
+ this.offCurrent = ON_CURRENT;
+ } else {
+ this.onCurrent = ON_CURRENT;
+ this.offCurrent = OFF_CURRENT;
+ }
+
+ this.pin = gpio.provisionDigitalOutputPin(pinNumber,
+ getClass().getSimpleName(), this.offCurrent);
+
+ // default name
+ this.name = name;
+ }
+
+ @Override
+ public void turnOff() {
+ if (this.state != SwitchState.Off) {
+ this.state = SwitchState.Off;
+ this.pin.setState(this.offCurrent);
+ Tracker.track("switch." + this.name, 0);
+ log.debug(this.name + " turned off.");
+ }
+ }
+
+ @Override
+ public void turnOn() {
+ if (this.state != SwitchState.On) {
+ this.state = SwitchState.On;
+ this.pin.setState(this.onCurrent);
+ Tracker.track("switch." + this.name, 100);
+ log.debug(this.name + " turned on.");
+ }
+ }
+}
diff --git a/src/com/codlex/thermocycler/hardware/Switch.java b/src/com/codlex/thermocycler/hardware/Switch.java
new file mode 100644
index 0000000..3060111
--- /dev/null
+++ b/src/com/codlex/thermocycler/hardware/Switch.java
@@ -0,0 +1,6 @@
+package com.codlex.thermocycler.hardware;
+
+public interface Switch {
+ void turnOff();
+ void turnOn();
+}
diff --git a/src/com/codlex/thermocycler/hardware/VirtualSensors.java b/src/com/codlex/thermocycler/hardware/VirtualSensors.java
new file mode 100644
index 0000000..d77a881
--- /dev/null
+++ b/src/com/codlex/thermocycler/hardware/VirtualSensors.java
@@ -0,0 +1,112 @@
+package com.codlex.thermocycler.hardware;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.codlex.thermocycler.logic.Settings;
+import com.pi4j.io.gpio.Pin;
+
+import javafx.beans.property.FloatProperty;
+import javafx.beans.property.SimpleFloatProperty;
+import javafx.util.Pair;
+
+public class VirtualSensors {
+
+ private static final VirtualSensors INSTANCE = new VirtualSensors();
+
+ public static VirtualSensors get() {
+ return INSTANCE;
+ }
+
+ public FloatProperty coldBathTemperature1 = new SimpleFloatProperty(10);
+ public FloatProperty coldBathTemperature2 = new SimpleFloatProperty(10);
+ public FloatProperty coldBathTemperatureAntifriz = new SimpleFloatProperty(10);
+
+ public FloatProperty coldBathDistance = new SimpleFloatProperty(3);
+ public FloatProperty hotBathTemperature1 = new SimpleFloatProperty(10);
+ public FloatProperty hotBathTemperature2 = new SimpleFloatProperty(10);
+ public FloatProperty hotBathTemperatureSafety = new SimpleFloatProperty(10);
+
+ public FloatProperty hotBathDistance = new SimpleFloatProperty(3);
+
+ private final Map> temperatureSensors = new HashMap<>();
+
+ private final Map, Sensor> distanceMonitors = new HashMap<>();
+
+ private VirtualSensors() {
+
+ addTemperatureSensor(Settings.get().getColdBathTemperatureSensor1(), this.coldBathTemperature1);
+ addTemperatureSensor(Settings.get().getColdBathTemperatureSensor2(), this.coldBathTemperature2);
+ addTemperatureSensor(Settings.get().getColdBathTemperatureSensorAntifriz(), this.coldBathTemperatureAntifriz);
+
+ addTemperatureSensor(Settings.get().getHotBathTemperatureSensor1(), this.hotBathTemperature1);
+ addTemperatureSensor(Settings.get().getHotBathTemperatureSensor2(), this.hotBathTemperature2);
+ addTemperatureSensor(Settings.get().getHotBathTemperatureSensor3(), this.hotBathTemperatureSafety);
+
+ addDistanceMonitor(Settings.get().getColdBathLevelEchoPin(), Settings.get().getColdBathLevelTriggerPin(),
+ this.coldBathDistance);
+ addDistanceMonitor(Settings.get().getHotBathLevelEchoPin(), Settings.get().getHotBathLevelTriggerPin(),
+ this.hotBathDistance);
+
+ }
+
+ private void addDistanceMonitor(Pin echo, Pin trigger, FloatProperty distance) {
+ final Pair id = new Pair<>(echo, trigger);
+ this.distanceMonitors.put(id,
+ new RefreshedSensor(Duration.ofMillis(Settings.get().getDistanceRefreshMillis())) {
+
+ @Override
+ protected Float getDefaultValue() {
+ return 0f;
+ }
+
+ @Override
+ public String getID() {
+ return id.toString();
+ }
+
+ @Override
+ protected Float recalculateValue() throws Exception {
+ // simulate duration of measuring
+ // Thread.sleep(ThreadLocalRandom.current().nextLong(1500));
+ return distance.get();
+ }
+
+ });
+ }
+
+ private void addTemperatureSensor(final String id, final FloatProperty property) {
+
+ this.temperatureSensors.put(id,
+ new RefreshedSensor(Duration.ofMillis(Settings.get().getTemperatureRefreshMillis())) {
+
+ @Override
+ protected Float getDefaultValue() {
+ return 20f;
+ }
+
+ @Override
+ public String getID() {
+ return id;
+ }
+
+ @Override
+ protected Float recalculateValue() throws Exception {
+ // simulate duration of measuring
+ // Thread.sleep(ThreadLocalRandom.current().nextLong(1500));
+ return property.get();
+ }
+ });
+
+ }
+
+ public Sensor getDistanceSensor(Pin echo, Pin trigger) {
+ return this.distanceMonitors.get(new Pair(echo, trigger));
+ }
+
+ public Sensor getTemperatureSensorById(String id) {
+ return this.temperatureSensors.get(id);
+ }
+
+}
diff --git a/src/com/codlex/thermocycler/hardware/VirtualSwitches.java b/src/com/codlex/thermocycler/hardware/VirtualSwitches.java
new file mode 100644
index 0000000..2e8c3e6
--- /dev/null
+++ b/src/com/codlex/thermocycler/hardware/VirtualSwitches.java
@@ -0,0 +1,71 @@
+package com.codlex.thermocycler.hardware;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.codlex.thermocycler.logic.Settings;
+import com.codlex.thermocycler.tracker.Tracker;
+import com.pi4j.io.gpio.Pin;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import lombok.extern.log4j.Log4j;
+import lombok.extern.slf4j.Slf4j;
+
+@Log4j
+public class VirtualSwitches {
+ private static final VirtualSwitches INSTANCE = new VirtualSwitches();
+
+ public static final VirtualSwitches get() {
+ return INSTANCE;
+ }
+ public final BooleanProperty coldBathCooler = new SimpleBooleanProperty();
+ public final BooleanProperty coldBathWaterPump = new SimpleBooleanProperty();
+ public final BooleanProperty hotBathCirculationWaterPump = new SimpleBooleanProperty();
+ public final BooleanProperty hotBathHeater = new SimpleBooleanProperty();
+ public final BooleanProperty hotBathWaterPump = new SimpleBooleanProperty();
+ public final BooleanProperty translatorPower = new SimpleBooleanProperty();
+ public final BooleanProperty translatorPulse = new SimpleBooleanProperty();
+
+ private final BooleanProperty translatorToCold = new SimpleBooleanProperty();
+
+ private final Map switches = new HashMap<>();
+
+ private VirtualSwitches() {
+ addSwitch(Settings.get().getColdBathCoolerPin(), this.coldBathCooler, "Cooler");
+ addSwitch(Settings.get().getColdBathWaterPump(), this.coldBathWaterPump, "ColdBathWaterPump");
+ addSwitch(Settings.get().getHotBathCirculationWaterPump(),
+ this.hotBathCirculationWaterPump, "HotBathCircularPump");
+ addSwitch(Settings.get().getHotBathHeaterPin(), this.hotBathHeater, "Heater");
+ addSwitch(Settings.get().getHotBathWaterPump(), this.hotBathWaterPump, "HotBathWaterPump");
+ addSwitch(Settings.get().getTranslatorPowerPin(), this.translatorPower, "TranslatorPower");
+ addSwitch(Settings.get().getTranslatorPulsePin(), this.translatorPulse, "TranslatorPulse");
+ addSwitch(Settings.get().getTranslatorToColdDirection(), this.translatorToCold, "TranslatorToCold");
+ }
+
+ private void addSwitch(Pin pin, BooleanProperty property, String name) {
+ this.switches.put(pin, new Switch() {
+ @Override
+ public void turnOff() {
+ if (property.get()) {
+ log.debug(name + " turned off.");
+ Tracker.track("switch." + name, 0);
+ }
+ property.set(false);
+ }
+
+ @Override
+ public void turnOn() {
+ if (!property.get()) {
+ log.debug(name + " turned on.");
+ Tracker.track("switch." + name, 1);
+ }
+ property.set(true);
+ }
+ });
+ }
+
+ public Switch getSwitch(Pin pin) {
+ return this.switches.get(pin);
+ }
+}
diff --git a/src/com/codlex/thermocycler/logic/RaspiPinsExtened.java b/src/com/codlex/thermocycler/logic/RaspiPinsExtened.java
new file mode 100644
index 0000000..c07155c
--- /dev/null
+++ b/src/com/codlex/thermocycler/logic/RaspiPinsExtened.java
@@ -0,0 +1,8 @@
+package com.codlex.thermocycler.logic;
+
+import com.pi4j.io.gpio.Pin;
+import com.pi4j.io.gpio.RaspiPin;
+
+public class RaspiPinsExtened extends RaspiPin {
+ public static Pin pin = createDigitalPin(40, "GPIO 40");
+}
diff --git a/src/com/codlex/thermocycler/logic/Settings.java b/src/com/codlex/thermocycler/logic/Settings.java
new file mode 100644
index 0000000..7482d9e
--- /dev/null
+++ b/src/com/codlex/thermocycler/logic/Settings.java
@@ -0,0 +1,278 @@
+package com.codlex.thermocycler.logic;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Properties;
+
+import com.google.common.collect.Range;
+import com.pi4j.io.gpio.Pin;
+import com.pi4j.io.gpio.RaspiPin;
+
+import lombok.extern.log4j.Log4j;
+
+@Log4j
+public class Settings {
+
+ private static final Settings INSTANCE = new Settings();
+
+ public String getHotBathTemperatureSensor1() {
+ return getValue(String.class);
+ }
+
+ public String getHotBathTemperatureSensor2() {
+ return getValue(String.class);
+ }
+
+ public String getHotBathTemperatureSensor3() {
+ return getValue(String.class);
+ }
+
+ public String getColdBathTemperatureSensor1() {
+ return getValue(String.class);
+ }
+
+ public String getColdBathTemperatureSensor2() {
+ return getValue(String.class);
+ }
+
+ public String getColdBathTemperatureSensorAntifriz() {
+ return getValue(String.class);
+ }
+
+ public boolean getTrackingOn() {
+ return getValue(Boolean.class);
+ }
+
+ public long getTranslationTimeMillis() {
+ return getValue(Long.class);
+ }
+
+ public int getDistanceRefreshMillis() {
+ return getValue(Integer.class);
+ }
+
+ public int getTemperatureRefreshMillis() {
+ return getValue(Integer.class);
+ }
+
+ /**
+ * Bath.
+ */
+ public int getBathMinimumLevel() {
+ return getValue(Integer.class);
+ }
+
+ public int getBathDepth() {
+ return getValue(Integer.class);
+ }
+
+ public int getLevelEpsilon() {
+ return getValue(Integer.class);
+ }
+
+ public int getTemperatureEpsilon() {
+ return getValue(Integer.class);
+ }
+
+ /**
+ * Hot Bath.
+ */
+
+ public Pin getHotBathLevelEchoPin() {
+ return getValue(Pin.class);
+ }
+
+ public Pin getHotBathLevelTriggerPin() {
+ return getValue(Pin.class);
+ }
+
+ public Pin getHotBathHeaterPin() {
+ return getValue(Pin.class);
+ }
+
+ public Pin getHotBathWaterPump() {
+ return getValue(Pin.class);
+ }
+
+ public Pin getHotBathCirculationWaterPump() {
+ return getValue(Pin.class);
+ }
+
+ public long getFromHotBathPause() {
+ return getValue(Long.class);
+ }
+
+ /**
+ * ColdBath.
+ */
+ public Pin getColdBathLevelEchoPin() {
+ return getValue(Pin.class);
+ }
+
+ public Pin getColdBathLevelTriggerPin() {
+ return getValue(Pin.class);
+ }
+
+ public Pin getColdBathCoolerPin() {
+ return getValue(Pin.class);
+ }
+
+ public Pin getColdBathWaterPump() {
+ return getValue(Pin.class);
+ }
+
+ public long getFromColdBathPause() {
+ return getValue(Long.class);
+ }
+
+ /**
+ * Translator
+ */
+ public Pin getTranslatorPowerPin() {
+ return getValue(Pin.class);
+ }
+
+ public Pin getTranslatorPulsePin() {
+ return getValue(Pin.class);
+ }
+
+ public Pin getTranslatorToColdDirection() {
+ return getValue(Pin.class);
+ }
+
+ /**
+ * Validation
+ */
+ @SuppressWarnings("unchecked")
+ public Range getValidationCyclesRange() {
+ return getValue(Range.class);
+ }
+
+ @SuppressWarnings("unchecked")
+ public Range getValidationHotTemperatureRange() {
+ return getValue(Range.class);
+ }
+
+ @SuppressWarnings("unchecked")
+ public Range getValidationColdTemperatureRange() {
+ return getValue(Range.class);
+ }
+
+ @SuppressWarnings("unchecked")
+ public Range getValidationTimeRange() {
+ return getValue(Range.class);
+ }
+
+ /**
+ * Safety
+ */
+
+ public boolean getSafety() {
+ return getValue(Boolean.class);
+ }
+
+ public float getSafetyHotBathTemperatureMax() {
+ return getValue(Float.class);
+ }
+
+ public int getSafetyLevelMin() {
+ return getValue(Integer.class);
+ }
+
+ public int getSafetyLevelMax() {
+ return getValue(Integer.class);
+ }
+
+ public static final Settings get() {
+ return INSTANCE;
+ }
+
+ public final Properties properties = new Properties();
+
+ private Settings() {
+ try {
+ this.properties.load(new FileInputStream("settings.properties"));
+ } catch (IOException e) {
+ log.error(e);
+ }
+ }
+
+ public boolean getFullScreen() {
+ return getValue(Boolean.class);
+ }
+
+ private String getMethodName() {
+ final int depth = 3;
+ return Thread.currentThread().getStackTrace()[depth].getMethodName().replace("get", ""); // delete
+ // get
+ // part
+ }
+
+ public boolean getProduction() {
+ return getValue(Boolean.class);
+ }
+
+ /**
+ * This method takes name of calling method and by it gets value from
+ * properties file.
+ */
+ private T getValue(Class expectedClass) {
+ final String key = getMethodName();
+ final String textValue = this.properties.getProperty(key);
+
+ // casting
+ switch (expectedClass.getSimpleName()) {
+ case "Integer":
+ return expectedClass.cast(Integer.parseInt(textValue));
+ case "Boolean":
+ return expectedClass.cast(Boolean.parseBoolean(textValue));
+ case "Float":
+ return expectedClass.cast(Float.parseFloat(textValue));
+ case "Long":
+ return expectedClass.cast(Long.parseLong(textValue));
+ case "Pin":
+ return expectedClass.cast(RaspiPin.getPinByAddress(Integer.parseInt(textValue)));
+ case "Range":
+ String[] splitted = textValue.split(",");
+ return expectedClass.cast(Range.closed(Integer.parseInt(splitted[0].trim()), Integer.parseInt(splitted[1].trim())));
+ case "String":
+ return expectedClass.cast(textValue);
+ }
+
+ return null;
+ }
+
+ public float getColdBathEpsOn() {
+ return getValue(Float.class);
+ }
+
+ public float getColdBathEpsOff() {
+ return getValue(Float.class);
+ }
+
+ public boolean getCirculationPumpOn() {
+ return getValue(Boolean.class);
+ }
+
+ public boolean getKeepLevelOn() {
+ return getValue(Boolean.class);
+ }
+
+ public long getMiddlePausePulseDurationFromHot() {
+ return getValue(Long.class);
+ }
+
+ public long getMiddlePausePulseDurationFromCold() {
+ return getValue(Long.class);
+ }
+
+ public long getFromColdBathMiddlePause() {
+ return getValue(Long.class);
+ }
+
+ public long getFromHotBathMiddlePause() {
+ return getValue(Long.class);
+ }
+
+
+}
diff --git a/src/com/codlex/thermocycler/logic/State.java b/src/com/codlex/thermocycler/logic/State.java
new file mode 100644
index 0000000..35262b7
--- /dev/null
+++ b/src/com/codlex/thermocycler/logic/State.java
@@ -0,0 +1,6 @@
+package com.codlex.thermocycler.logic;
+
+public enum State {
+ NotStarted, NotReady, HotBath, ColdBath, ToColdBathPause, ToHotBathPause, ToColdBathMiddlePause,
+ ToHotBathMiddlePause, Finished, UnexpectedShutdown;
+}
diff --git a/src/com/codlex/thermocycler/logic/StateLogic.java b/src/com/codlex/thermocycler/logic/StateLogic.java
new file mode 100644
index 0000000..b79c7d6
--- /dev/null
+++ b/src/com/codlex/thermocycler/logic/StateLogic.java
@@ -0,0 +1,321 @@
+package com.codlex.thermocycler.logic;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import javafx.application.Platform;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import lombok.Getter;
+import lombok.extern.log4j.Log4j;
+
+@Log4j
+public class StateLogic {
+
+ Thermocycler thermocycler;
+ State currentState = State.NotStarted;
+ long currentTime = 0;
+ long stateStartTime = 0;
+ long hotBathImmersionCount = 0;
+ long coldBathImmersionCount = 0;
+
+ @Getter
+ ObjectProperty stateProperty = new SimpleObjectProperty<>(this.currentState);
+
+ StateLogic(Thermocycler thermocycler) {
+ this.thermocycler = thermocycler;
+ if (!this.thermocycler.getPersister().lastFinishedSuccessfully()) {
+ changeState(State.UnexpectedShutdown);
+ } else {
+ reset();
+ }
+ }
+
+ public long calculateTimeInState() {
+ long translationChange = 0;
+
+ if (this.currentState == State.ColdBath || this.currentState == State.HotBath) {
+ translationChange = Settings.get().getTranslationTimeMillis(); // time of translation
+ }
+
+ if (this.currentState == State.ToColdBathPause || this.currentState == State.ToHotBathPause) {
+ translationChange = 450; // time of errection
+ }
+
+ if (this.currentState == State.ToHotBathMiddlePause) {
+ translationChange = Settings.get().getMiddlePausePulseDurationFromCold();
+ }
+
+ if (this.currentState == State.ToColdBathMiddlePause) {
+ translationChange = Settings.get().getMiddlePausePulseDurationFromHot();
+ }
+
+ return Math.max(this.currentTime - this.stateStartTime - translationChange, 0);
+ }
+
+ void changeState(final State state) {
+ log.debug("##### [" + this.currentState + "->" + state + "] State(hot=" + this.hotBathImmersionCount + ", cold="
+ + this.coldBathImmersionCount + ", total=" + this.thermocycler.cycles.get() + ") ####");
+
+ this.thermocycler.onStateChange(state);
+
+ this.currentState = state;
+
+ // reset start time
+ this.stateStartTime = this.currentTime;
+
+ Platform.runLater(() -> {
+ this.stateProperty.set(state);
+ });
+
+ switch (state) {
+ case HotBath:
+ this.hotBathImmersionCount++;
+ break;
+ case ColdBath:
+ this.coldBathImmersionCount++;
+ break;
+ case ToColdBathPause:
+ case ToHotBathPause:
+ log.debug("Pausing...");
+ break;
+
+ case ToColdBathMiddlePause:
+ case ToHotBathMiddlePause:
+ log.debug("Pausing in middle...");
+ break;
+
+ case UnexpectedShutdown:
+ log.debug("Thermocycler had unexpected shutdown last time.");
+ break;
+ case NotStarted:
+ log.debug("Ignored last settings reseting thermocycler.");
+ break;
+ case Finished:
+ break;
+
+ default:
+ log.error("Unexpected state " + state);
+ }
+ }
+
+ void doErrect() {
+ if (this.currentState == State.ColdBath) {
+ changeState(State.ToHotBathPause);
+ } else {
+ changeState(State.ToColdBathPause);
+ }
+ }
+
+ void doCycle() {
+ if (this.currentState == State.ColdBath) {
+ changeState(State.HotBath);
+ } else {
+ changeState(State.ColdBath);
+ }
+ }
+
+ public int getCurrenCycle() {
+ return (int) this.hotBathImmersionCount;
+ }
+
+ State getCurrentState() {
+ return this.currentState;
+ }
+
+ public long getCyclesLeft() {
+ return this.thermocycler.cycles.get() - this.hotBathImmersionCount;
+ }
+
+ public long getFullTimeLeftMillis() {
+
+ int hotCyclesLeft = (int) (this.thermocycler.cycles.get() - this.hotBathImmersionCount);
+ long hotTimeLeft = TimeUnit.SECONDS.toMillis(hotCyclesLeft * this.thermocycler.getHotBath().time.get());
+
+ int coldCyclesLeft = (int) (this.thermocycler.cycles.get() - this.coldBathImmersionCount);
+ long coldTimeLeft = TimeUnit.SECONDS.toMillis(coldCyclesLeft * this.thermocycler.getColdBath().time.get());
+
+ long translatingTime = 2 * (hotCyclesLeft * Settings.get().getTranslationTimeMillis());
+
+ long pauseTime = Settings.get().getFromColdBathPause() * coldCyclesLeft
+ + Settings.get().getFromHotBathPause() * hotCyclesLeft;
+
+ long pauseTimeMid = Settings.get().getFromColdBathMiddlePause() * coldCyclesLeft
+ + Settings.get().getFromHotBathMiddlePause() * hotCyclesLeft;
+
+ return hotTimeLeft + coldTimeLeft + translatingTime + pauseTime + pauseTimeMid - this.calculateTimeInState();
+ }
+
+ long getTargetTimeInState() {
+ // ASSERT state in (ColdBath, HotBath)
+ if (this.currentState == State.ColdBath) {
+ return TimeUnit.SECONDS.toMillis(this.thermocycler.getColdBath().time.get());
+ } else if (this.currentState == State.HotBath) { // this.currentState == HotBath
+ return TimeUnit.SECONDS.toMillis(this.thermocycler.getHotBath().time.get());
+ } else if (this.currentState == State.ToHotBathPause || this.currentState == State.ToColdBathPause
+ || this.currentState == State.ToColdBathMiddlePause
+ || this.currentState == State.ToHotBathMiddlePause) {
+ return getPauseTime();
+ }
+
+// log.error("This state doesn't have time associated with it: " + this.currentState);
+ return 0;
+ }
+
+ boolean isLastCycle() {
+ int targetCycles = this.thermocycler.cycles.get();
+ boolean hotFinished = this.hotBathImmersionCount >= targetCycles;
+ boolean coldFinished = this.coldBathImmersionCount >= targetCycles;
+ return hotFinished && coldFinished;
+ }
+
+ void processPauseTo(State to) {
+ // ASSERT state in (ColdBath, HotBath)
+ long currentStateTime = calculateTimeInState();
+ if (currentStateTime > getPauseTime()) {
+ if (!isLastCycle()) {
+ // reset start time
+ this.stateStartTime = this.currentTime;
+
+ if (this.currentState == State.ToColdBathPause) {
+ changeState(State.ToColdBathMiddlePause);
+ } else {
+ changeState(State.ToHotBathMiddlePause);
+ }
+
+ } else {
+ changeState(State.Finished);
+ }
+ }
+ }
+
+ void processMiddlePauseTo(State to) {
+ // ASSERT state in (ColdBath, HotBath)
+ long currentStateTime = calculateTimeInState();
+ if (currentStateTime > getPauseTime()) {
+ if (!isLastCycle()) {
+ // reset start time
+ this.stateStartTime = this.currentTime;
+
+ if (this.currentState == State.ToColdBathMiddlePause) {
+ changeState(State.ColdBath);
+ } else {
+ changeState(State.HotBath);
+ }
+
+ } else {
+ changeState(State.Finished);
+ }
+ }
+ }
+
+ private long getPauseTime() {
+ if (this.currentState == State.HotBath || this.currentState == State.ToColdBathPause) {
+ return Settings.get().getFromHotBathPause();
+ }
+
+ if (this.currentState == State.ColdBath || this.currentState == State.ToHotBathPause) {
+ return Settings.get().getFromColdBathPause();
+ }
+
+ if (this.currentState == State.ToColdBathMiddlePause) {
+ return Settings.get().getFromHotBathMiddlePause();
+ }
+
+ if (this.currentState == State.ToHotBathMiddlePause) {
+ return Settings.get().getFromColdBathMiddlePause();
+ }
+
+ throw new RuntimeException("Getting pause time when not in valid state: " + this.currentState);
+ }
+
+ void processCycling() {
+ // ASSERT state in (ColdBath, HotBath)
+ long currentImmersionTime = calculateTimeInState();
+ if (currentImmersionTime > getTargetTimeInState()) {
+ if (!isLastCycle()) {
+ // invert state
+ if (getPauseTime() != 0) {
+ doErrect();
+ } else {
+ doCycle();
+ }
+
+ } else {
+ changeState(State.Finished);
+ }
+ }
+ }
+
+ void processNotReady() {
+ boolean hotBathReady = this.thermocycler.getHotBath().isReady();
+ boolean coldBathReady = this.thermocycler.getColdBath().isReady();
+ if (hotBathReady && coldBathReady) {
+ this.stateStartTime = this.currentTime;
+ changeState(State.HotBath);
+ }
+ }
+
+ private void processNotStarted() {
+ if (this.thermocycler.isStarted.get()) {
+ changeState(State.NotReady);
+ }
+ }
+
+ public void reset() {
+ this.currentTime = 0;
+ this.stateStartTime = 0;
+ changeState(State.NotStarted);
+ this.hotBathImmersionCount = 0;
+ this.coldBathImmersionCount = 0;
+ }
+
+ void update(long delta) {
+ this.currentTime += delta;
+
+ switch (this.currentState) {
+ case NotStarted:
+ processNotStarted();
+ break;
+ case NotReady:
+ if (this.currentState != State.NotReady) {
+ log.debug("Thermocycler is warming up.");
+ }
+ processNotReady();
+ break;
+ case HotBath:
+ processCycling();
+ break;
+
+ case ColdBath:
+ processCycling();
+ break;
+
+ case ToColdBathPause:
+ processPauseTo(State.ColdBath);
+ break;
+
+ case ToHotBathPause:
+ processPauseTo(State.HotBath);
+ break;
+
+ case ToColdBathMiddlePause:
+ processMiddlePauseTo(State.ColdBath);
+ break;
+
+ case ToHotBathMiddlePause:
+ processMiddlePauseTo(State.HotBath);
+ break;
+
+ case Finished:
+ break;
+ case UnexpectedShutdown:
+ processNotStarted();
+ break;
+ default:
+ log.error("Unknown state.");
+ break;
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/codlex/thermocycler/logic/Thermocycler.java b/src/com/codlex/thermocycler/logic/Thermocycler.java
new file mode 100644
index 0000000..b20dcea
--- /dev/null
+++ b/src/com/codlex/thermocycler/logic/Thermocycler.java
@@ -0,0 +1,200 @@
+package com.codlex.thermocycler.logic;
+
+import java.util.Date;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.log4j.LogManager;
+
+import com.codlex.thermocycler.logic.bath.Bath;
+import com.codlex.thermocycler.logic.bath.cold.ColdBath;
+import com.codlex.thermocycler.logic.bath.hot.HotBath;
+import com.codlex.thermocycler.tracker.Tracker;
+import com.google.common.collect.ImmutableSet;
+
+import javafx.application.Platform;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import lombok.Getter;
+import lombok.extern.log4j.Log4j;
+
+@Log4j
+public class Thermocycler {
+
+ @Getter
+ private Bath hotBath;
+
+ @Getter
+ private Bath coldBath;
+
+ @Getter
+ private StateLogic stateLogic;
+
+ @Getter
+ private Translator translator;
+
+ @Getter
+ final IntegerProperty cycles = new SimpleIntegerProperty();
+
+ public AtomicBoolean isStarted = new AtomicBoolean(false);
+
+ @Getter
+ private ThermocyclerPersister persister = new ThermocyclerPersister();
+
+ public Thermocycler() {
+ this.stateLogic = new StateLogic(this);
+ this.coldBath = new ColdBath(this);
+ this.hotBath = new HotBath(this);
+ this.translator = new Translator();
+
+ if (!this.persister.lastFinishedSuccessfully()) {
+ boolean succesfullyLoaded = this.persister.load(this);
+ if (!succesfullyLoaded) {
+ reset();
+ }
+ } else {
+ reset();
+ }
+ }
+
+ private void clear() {
+ log.debug("Clearning thermocycler.");
+ this.translator.clear();
+ this.hotBath.clear();
+ this.coldBath.clear();
+ }
+
+ public Date getFinishTime() {
+ return new Date(System.currentTimeMillis() + this.stateLogic.getFullTimeLeftMillis());
+ }
+
+ public int getTimeLeft() {
+ return (int) TimeUnit.MILLISECONDS
+ .toSeconds(this.stateLogic.getTargetTimeInState() - this.stateLogic.calculateTimeInState());
+ }
+
+ void init() {
+ clear();
+ try {
+ log.debug("Checking cold bath...");
+ Thread.sleep(1500);
+ this.translator.goToCold();
+ log.debug("Checking hot bath...");
+ Thread.sleep(2000);
+ this.translator.goToHot();
+ Thread.sleep(1500);
+ log.debug("Errecting...");
+ this.translator.errectFrom(State.HotBath);
+ } catch (InterruptedException e1) {
+ log.error(e1);
+ }
+ }
+
+ public boolean lastFinishedSuccessfully() {
+ return this.persister.lastFinishedSuccessfully();
+ }
+
+ void logStatus() {
+ Tracker.track("cyclesLeft", this.stateLogic.getCyclesLeft());
+ Tracker.track("timeLeft", this.stateLogic.getFullTimeLeftMillis() / 1000);
+
+ log.debug("ThermocyclerStatus(state=" + this.stateLogic.getCurrentState() + ", timeInState="
+ + this.stateLogic.calculateTimeInState() + " ms, targetTimeInState="
+ + this.stateLogic.getTargetTimeInState() + " ms)");
+
+ this.hotBath.logStatus();
+ this.coldBath.logStatus();
+ }
+
+ public void lowerTranslator() {
+ this.translator.goToHot();
+ }
+
+ public void onStateChange(State state) {
+ Set saveStates = ImmutableSet.of(State.NotReady, State.HotBath, State.ColdBath);
+ if (saveStates.contains(state)) {
+ this.persister.save(this);
+ }
+
+ Set deleteStates = ImmutableSet.of(State.Finished);
+ if (deleteStates.contains(state)) {
+ this.persister.delete();
+ }
+
+ }
+
+ public void performSafetyChecks() {
+ boolean success = this.hotBath.performSafetyChecks();
+ success &= this.coldBath.performSafetyChecks();
+
+ if (!success) {
+ log.error("Thermocycler safety check failed, shutting down.");
+ shutdown();
+ }
+ }
+
+ public void reset() {
+ Platform.runLater(() -> {
+ this.cycles.set(1);
+ });
+ this.stateLogic.reset();
+ this.coldBath.reset();
+ this.hotBath.reset();
+
+ this.persister.delete();
+ }
+
+ public void setCurrentCycle(int lastCycle) {
+ this.stateLogic.coldBathImmersionCount = lastCycle;
+ this.stateLogic.hotBathImmersionCount = lastCycle;
+ }
+
+ private void shutdown() {
+ LogManager.shutdown();
+ clear();
+ System.exit(1);
+ }
+
+ public void start() {
+ if (validate()) {
+ this.isStarted.set(true);
+ } else {
+ log.error("Starting of thermocycler failed because of bad input.");
+ shutdown();
+ }
+ }
+
+ public void update(long deltaT) {
+ if (this.isStarted.get()) {
+ this.stateLogic.update(deltaT);
+ this.hotBath.update(deltaT);
+ this.coldBath.update(deltaT);
+ this.translator.update(this.stateLogic.getCurrentState());
+
+ if (this.stateLogic.getCurrentState() == State.Finished) {
+ this.translator.errectFrom(State.ColdBath);
+ this.isStarted.set(false);
+ clear();
+ log.debug("############################## CYCLING_FINISHED ##############################");
+ }
+ }
+
+ // touch to recalculate
+ this.hotBath.getCurrentTemperature();
+ this.hotBath.getLevelSensor().getPercentageFilled();
+
+ this.coldBath.getCurrentTemperature();
+ this.coldBath.getLevelSensor().getPercentageFilled();
+
+ logStatus();
+
+ }
+
+ private boolean validate() {
+ boolean isValid = Settings.get().getValidationCyclesRange().contains(this.cycles.get());
+ isValid &= this.hotBath.isValid();
+ isValid &= this.coldBath.isValid();
+ return isValid;
+ }
+}
\ No newline at end of file
diff --git a/src/com/codlex/thermocycler/logic/ThermocyclerPersister.java b/src/com/codlex/thermocycler/logic/ThermocyclerPersister.java
new file mode 100644
index 0000000..fff36db
--- /dev/null
+++ b/src/com/codlex/thermocycler/logic/ThermocyclerPersister.java
@@ -0,0 +1,111 @@
+package com.codlex.thermocycler.logic;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+
+import lombok.extern.log4j.Log4j;
+
+@Log4j
+public class ThermocyclerPersister {
+
+ private static final Object SEPARATOR = "\n";
+
+ final File temp = new File("ThermocyclerState.bin.temp");
+ final File finalFile = new File("ThermocyclerState.bin");
+
+ public void delete() {
+ if (!this.finalFile.exists()) {
+ return;
+ }
+
+ if (!this.finalFile.delete()) {
+ throw new RuntimeException("Can't delete persistance file.");
+ }
+ }
+
+ public boolean lastFinishedSuccessfully() {
+ return !this.finalFile.exists();
+ }
+
+ public boolean load(Thermocycler thermocycler) {
+ try {
+ List parameters = Files.readLines(this.finalFile,
+ Charsets.UTF_8);
+
+ int i = 0;
+
+ thermocycler.getHotBath().getTemperatureProperty()
+ .set(Integer.parseInt(parameters.get(i)));
+ i++;
+
+ thermocycler.getHotBath().getTimeProperty()
+ .set(Integer.parseInt(parameters.get(i)));
+ i++;
+
+ thermocycler.getColdBath().getTemperatureProperty()
+ .set(Integer.parseInt(parameters.get(i)));
+ i++;
+
+ thermocycler.getColdBath().getTimeProperty()
+ .set(Integer.parseInt(parameters.get(i)));
+ i++;
+
+ thermocycler.getCycles().set(Integer.parseInt(parameters.get(i)));
+ i++;
+
+ thermocycler.setCurrentCycle(Integer.parseInt(parameters.get(i)));
+ i++;
+
+ thermocycler.getHotBath().setLogicOn(Boolean.parseBoolean(parameters.get(i)));
+ i++;
+
+ thermocycler.getColdBath().setLogicOn(Boolean.parseBoolean(parameters.get(i)));
+ i++;
+
+
+
+ } catch (Exception e) {
+ log.error("Couldn't load state of thermocycler: ", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ public void save(Thermocycler thermocycler) {
+ StringBuilder builder = new StringBuilder();
+
+ builder.append(thermocycler.getHotBath().getTemperatureProperty().get())
+ .append(SEPARATOR); // hot bath temperature
+ builder.append(thermocycler.getHotBath().getTimeProperty().get())
+ .append(SEPARATOR); // hot bath time
+
+ builder.append(
+ thermocycler.getColdBath().getTemperatureProperty().get())
+ .append(SEPARATOR); // cold bath temperature
+ builder.append(thermocycler.getColdBath().getTimeProperty().get())
+ .append(SEPARATOR); // cold bath time
+
+ builder.append(thermocycler.cycles.get()).append(SEPARATOR); // total
+ // cycles
+
+ int lastCycle = thermocycler.getStateLogic().getCurrenCycle();
+ builder.append(lastCycle).append(SEPARATOR); // last cycle
+
+ builder.append(thermocycler.getHotBath().getIsLogicOn()).append(SEPARATOR);
+ builder.append(thermocycler.getColdBath().getIsLogicOn()).append(SEPARATOR);
+
+ try {
+ Files.write(builder.toString(), this.temp, Charsets.UTF_8);
+ Files.move(this.temp, this.finalFile);
+ } catch (IOException e) {
+ log.error("Couldn't persist state of thermocycler: ", e);
+ }
+
+ }
+
+}
diff --git a/src/com/codlex/thermocycler/logic/ThermocyclerWorker.java b/src/com/codlex/thermocycler/logic/ThermocyclerWorker.java
new file mode 100644
index 0000000..6a02cfb
--- /dev/null
+++ b/src/com/codlex/thermocycler/logic/ThermocyclerWorker.java
@@ -0,0 +1,66 @@
+package com.codlex.thermocycler.logic;
+
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Stopwatch;
+
+import lombok.extern.log4j.Log4j;
+
+@Log4j
+public class ThermocyclerWorker implements Runnable {
+
+ public static void start(final Thermocycler thermocycler) {
+ Thread worker = new Thread(new ThermocyclerWorker(thermocycler));
+ worker.start();
+ }
+
+ private final Thermocycler thermocycler;
+
+ private long lastLoopStart;
+
+ public ThermocyclerWorker(final Thermocycler thermocycler) {
+ this.thermocycler = thermocycler;
+ }
+
+ private long pollDeltaT() {
+ long currentMillis = System.currentTimeMillis();
+ long deltaT = currentMillis - this.lastLoopStart;
+ this.lastLoopStart = System.currentTimeMillis();
+ return deltaT;
+ }
+
+ @Override
+ public void run() {
+
+ Stopwatch logicInitialization = Stopwatch.createStarted();
+ log.debug("Logic initialization...");
+ this.thermocycler.init();
+ log.debug("######## Logic initialized in "
+ + logicInitialization.elapsed(TimeUnit.MILLISECONDS) + " ms ########");
+
+ this.lastLoopStart = System.currentTimeMillis();
+ while (true) {
+ try {
+ final Stopwatch stopwatch = Stopwatch.createStarted();
+
+ this.thermocycler.update(pollDeltaT());
+
+ long processingTime = stopwatch.elapsed(TimeUnit.MILLISECONDS);
+ if (processingTime > 10) {
+ log.error("Processing done in " + processingTime + " ms");
+ }
+
+ Thread.sleep(500);
+
+ } catch (Exception e) {
+ log.error("Exception happened in themrocycler logic: ", e);
+ } finally {
+ // disabled
+ if (Settings.get().getSafety()) {
+ this.thermocycler.performSafetyChecks();
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/com/codlex/thermocycler/logic/Translator.java b/src/com/codlex/thermocycler/logic/Translator.java
new file mode 100644
index 0000000..4042a9c
--- /dev/null
+++ b/src/com/codlex/thermocycler/logic/Translator.java
@@ -0,0 +1,126 @@
+package com.codlex.thermocycler.logic;
+
+import com.codlex.thermocycler.hardware.HardwareProvider;
+import com.codlex.thermocycler.hardware.Switch;
+
+public class Translator {
+
+ Switch power = HardwareProvider.get().getSwitch(Settings.get().getTranslatorPowerPin(), "TranslatorPower",
+ false);
+ Switch toCold = HardwareProvider.get()
+ .getSwitch(Settings.get().getTranslatorToColdDirection(), "TranslatorToColdSwitch");
+ Switch pulse = HardwareProvider.get()
+ .getSwitch(Settings.get().getTranslatorPulsePin(), "TranslatorPulse");
+
+ State currentState = State.NotReady;
+
+ public Translator() {
+ this.power.turnOn();
+ }
+
+ void changeState(State state) {
+ this.currentState = state;
+
+ switch (this.currentState) {
+ case ColdBath :
+ goToCold();
+ break;
+ case HotBath :
+ goToHot();
+ break;
+ case ToHotBathPause:
+ errectFrom(State.ColdBath);
+ break;
+ case ToColdBathPause:
+ errectFrom(State.HotBath);
+ break;
+ case ToHotBathMiddlePause:
+ errectToMiddleFrom(State.ColdBath);
+ break;
+ case ToColdBathMiddlePause:
+ errectToMiddleFrom(State.HotBath);
+ break;
+ }
+ }
+
+ public void clear() {
+ this.power.turnOff();
+ this.toCold.turnOff();
+ this.pulse.turnOff();
+ }
+
+ public void errectToMiddleFrom(State from) {
+ this.power.turnOn();
+ if (State.HotBath.equals(from)) {
+ this.toCold.turnOn();
+ } else {
+ this.toCold.turnOff();
+ }
+ this.pulse.turnOn();
+ try {
+ if (State.HotBath.equals(from)) {
+ Thread.sleep(Settings.get().getMiddlePausePulseDurationFromHot());
+ } else {
+ Thread.sleep(Settings.get().getMiddlePausePulseDurationFromCold());
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ this.pulse.turnOff();
+ this.power.turnOff();
+ }
+
+ public void errectFrom(State from) {
+ this.power.turnOn();
+ if (State.HotBath.equals(from)) {
+ this.toCold.turnOn();
+ } else {
+ this.toCold.turnOff();
+ }
+ this.pulse.turnOn();
+ try {
+ Thread.sleep(450);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ this.pulse.turnOff();
+ this.power.turnOff();
+ }
+
+ void goToCold() {
+ this.power.turnOn();
+ this.toCold.turnOn();
+ this.pulse.turnOn();
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ this.pulse.turnOff();
+ }
+
+ void goToHot() {
+ this.power.turnOn();
+ this.toCold.turnOff();
+ this.pulse.turnOn();
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ this.pulse.turnOff();
+ }
+
+ void update(State state) {
+
+ // not_ready . hot_bath
+ // hot_bath . cold_bath if there is pause, it will go to to_cold_bath_pause
+ // cold_bath . hot_bath if there is pause, it will go to to_hot_bath_pause
+ // cold_bath . finished
+ if (state != this.currentState) {
+ changeState(state);
+ }
+ }
+}
diff --git a/src/com/codlex/thermocycler/logic/bath/Bath.java b/src/com/codlex/thermocycler/logic/bath/Bath.java
new file mode 100644
index 0000000..289266f
--- /dev/null
+++ b/src/com/codlex/thermocycler/logic/bath/Bath.java
@@ -0,0 +1,172 @@
+package com.codlex.thermocycler.logic.bath;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import com.codlex.thermocycler.logic.Settings;
+import com.codlex.thermocycler.logic.Thermocycler;
+import com.codlex.thermocycler.logic.bath.sensors.LevelSensor;
+import com.codlex.thermocycler.logic.bath.sensors.TemperatureSensor;
+import com.pi4j.io.gpio.Pin;
+
+import javafx.application.Platform;
+import javafx.beans.Observable;
+import javafx.beans.property.FloatProperty;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.SimpleFloatProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import lombok.Getter;
+import lombok.extern.log4j.Log4j;
+
+@Log4j
+public abstract class Bath {
+
+ @Getter
+ protected FloatProperty currentTemperatureProperty = new SimpleFloatProperty();
+ protected TemperatureSensor temperatureSensor1;
+ protected TemperatureSensor temperatureSensor2;
+
+ private WaterPump pump;
+ protected LevelSensor level;
+
+ protected IntegerProperty temperature = new SimpleIntegerProperty();
+ public IntegerProperty time = new SimpleIntegerProperty();
+ private final Thermocycler thermocycler;
+ private AtomicBoolean logicOn = new AtomicBoolean(true);
+
+
+ public boolean getIsLogicOn() {
+ return logicOn.get();
+ }
+
+ public Bath(Thermocycler thermocycler, String temperatureSensorIndex1, String temperatureSensorIndex2,
+ Pin levelEchoPin, Pin levelTriggerPin, Pin waterPumpPin) {
+ this.thermocycler = thermocycler;
+ this.temperatureSensor1 = new TemperatureSensor(temperatureSensorIndex1);
+ this.temperatureSensor2 = new TemperatureSensor(temperatureSensorIndex2);
+ this.level = new LevelSensor(levelEchoPin, levelTriggerPin, Settings.get().getBathDepth());
+ this.pump = new WaterPump(waterPumpPin);
+ }
+
+ public void clear() {
+ this.pump.turnOff();
+ }
+
+ public synchronized float getCurrentTemperature() {
+ float sum = this.temperatureSensor1.getTemperature();
+ sum += this.temperatureSensor2.getTemperature();
+ float averageTemp = sum / 2;
+
+ Platform.runLater(() -> {
+ this.currentTemperatureProperty.setValue(averageTemp);
+ });
+
+ return averageTemp;
+ }
+
+ public LevelSensor getLevelSensor() {
+ return this.level;
+ }
+
+ public IntegerProperty getTemperatureProperty() {
+ return this.temperature;
+ }
+
+ public IntegerProperty getTimeProperty() {
+ return this.time;
+ }
+
+ public boolean isLevelOK() {
+ if (!Settings.get().getKeepLevelOn()) {
+ return true;
+ }
+
+ int minimumLevel = Settings.get().getBathMinimumLevel() - Settings.get().getLevelEpsilon();
+ return this.level.getPercentageFilled() > minimumLevel;
+ }
+
+ public boolean isReady() {
+ return isTemperatureOK() && isLevelOK();
+ }
+
+ public boolean isTemperatureOK() {
+ if (!this.logicOn.get()) {
+ return true;
+ }
+
+ float minTemperature = this.temperature.get() - Settings.get().getTemperatureEpsilon();
+ float maxTemperature = this.temperature.get() + Settings.get().getTemperatureEpsilon();
+ float currentTemperature = getCurrentTemperature();
+ return minTemperature <= currentTemperature && currentTemperature <= maxTemperature;
+ }
+
+ public boolean isValid() {
+ return this.isLevelOK() && Settings.get().getValidationTimeRange().contains(this.time.get());
+ }
+
+ public void keepLevel() {
+ if (!Settings.get().getKeepLevelOn()) {
+ return;
+ }
+
+ int precetage = this.level.getPercentageFilled();
+ if (precetage < Settings.get().getBathMinimumLevel()) {
+ this.pump.turnOn();
+ } else {
+ this.pump.turnOff();
+ }
+ }
+
+ public abstract void keepTemperature();
+
+ public abstract void logStatus();
+
+ private boolean performLevelSafetyChecks() {
+
+ int level = this.level.getPercentageFilled();
+ int minLevel = Settings.get().getSafetyLevelMin();
+ int maxLevel = Settings.get().getSafetyLevelMax();
+
+ boolean isOk = level > minLevel;
+ isOk &= level < maxLevel;
+
+ if (!isOk) {
+ log.error(getClass().getSimpleName() + " safety check failed: level of liquid dangerous! " + minLevel
+ + " < " + level + " < " + maxLevel);
+ }
+ return isOk;
+ }
+
+ public boolean performSafetyChecks() {
+ boolean isOk = true;
+ if (this.thermocycler.isStarted.get()) {
+ if (Settings.get().getKeepLevelOn()) {
+ isOk &= performLevelSafetyChecks();
+ }
+ }
+ return isOk;
+ }
+
+ public void reset() {
+ Platform.runLater(() -> {
+ this.temperature.set(getInitialTemperature());
+ this.time.set(10);
+ });
+ }
+
+ private int getInitialTemperature() {
+ return (int) getCurrentTemperature();
+ }
+
+ public void update(long deltaT) {
+ // TODO: fix this, this is to initialize values
+ getCurrentTemperature();
+ if (this.logicOn.get()) {
+ keepTemperature();
+ }
+ keepLevel();
+ }
+
+ public void setLogicOn(boolean newValue) {
+ this.logicOn.set(newValue);
+ }
+}
diff --git a/src/com/codlex/thermocycler/logic/bath/WaterPump.java b/src/com/codlex/thermocycler/logic/bath/WaterPump.java
new file mode 100644
index 0000000..02fa3d1
--- /dev/null
+++ b/src/com/codlex/thermocycler/logic/bath/WaterPump.java
@@ -0,0 +1,14 @@
+package com.codlex.thermocycler.logic.bath;
+
+import com.codlex.thermocycler.hardware.OnOffDevice;
+import com.pi4j.io.gpio.Pin;
+
+import lombok.extern.log4j.Log4j;
+
+@Log4j
+public class WaterPump extends OnOffDevice {
+
+ public WaterPump(Pin pin) {
+ super(pin, WaterPump.class.getSimpleName());
+ }
+}
\ No newline at end of file
diff --git a/src/com/codlex/thermocycler/logic/bath/cold/ColdBath.java b/src/com/codlex/thermocycler/logic/bath/cold/ColdBath.java
new file mode 100644
index 0000000..3dbec36
--- /dev/null
+++ b/src/com/codlex/thermocycler/logic/bath/cold/ColdBath.java
@@ -0,0 +1,91 @@
+package com.codlex.thermocycler.logic.bath.cold;
+
+import com.codlex.thermocycler.logic.Settings;
+import com.codlex.thermocycler.logic.Thermocycler;
+import com.codlex.thermocycler.logic.bath.Bath;
+import com.codlex.thermocycler.logic.bath.sensors.TemperatureSensor;
+import com.codlex.thermocycler.tracker.Tracker;
+
+import lombok.extern.log4j.Log4j;
+
+@Log4j
+public class ColdBath extends Bath {
+
+ private Cooler cooler;
+ private TemperatureSensor antifrizTemperature;
+
+ public ColdBath(Thermocycler thermocycler) {
+ super(thermocycler, Settings.get().getColdBathTemperatureSensor1(), Settings.get().getColdBathTemperatureSensor2(),
+ Settings.get().getColdBathLevelEchoPin(), Settings.get().getColdBathLevelTriggerPin(),
+ Settings.get().getColdBathWaterPump());
+ this.cooler = new Cooler(Settings.get().getColdBathCoolerPin());
+ this.antifrizTemperature = new TemperatureSensor(Settings.get().getColdBathTemperatureSensorAntifriz());
+
+ this.time.set(1);
+ }
+
+ @Override
+ public void clear() {
+ log.debug("Clearning ColdBath.");
+ super.clear();
+ this.cooler.turnOff();
+ }
+
+ @Override
+ public boolean isValid() {
+ boolean isValid = super.isValid();
+ isValid &= Settings.get().getValidationColdTemperatureRange()
+ .contains(this.temperature.get());
+ return isValid;
+ }
+
+ @Override
+ public void keepTemperature() {
+
+ // cooler turned on when antifriz temperature goes over
+ // (wanted_temperature - epsOn)
+ float epsOn = Settings.get().getColdBathEpsOn();
+ boolean turnOn = this.antifrizTemperature
+ .getTemperature() > this.temperature.get() - epsOn
+ || getCurrentTemperature() > this.temperature.get() - epsOn;
+ if (turnOn) {
+ this.cooler.turnOn();
+ }
+
+ // cooler turned off when temperature in bath reaches wanted temperature
+ // OR antifriz temperature goes below (wanted_temperature - epsOff)
+ float epsOff = Settings.get().getColdBathEpsOff();
+ boolean turnOff = this.antifrizTemperature
+ .getTemperature() < this.temperature.get() - epsOff;
+
+ if (turnOff) {
+ this.cooler.turnOff();
+ }
+
+// if (isTemperatureOK()) {
+// this.cooler.turnOff();
+// return;
+// }
+
+ }
+
+ @Override
+ public void logStatus() {
+ Tracker.track("cold_bath.temperature1",
+ this.temperatureSensor1.getTemperature());
+ Tracker.track("cold_bath.temperature2",
+ this.temperatureSensor2.getTemperature());
+ Tracker.track("cold_bath.antifrizTemperature",
+ this.antifrizTemperature.getTemperature());
+ Tracker.track("cold_bath.percentageFilled",
+ this.level.getPercentageFilled());
+ Tracker.track("cold_bath.average", getCurrentTemperature());
+
+ log.debug("ColdBathStatus(temp1="
+ + this.temperatureSensor1.getTemperature() + ", temp2="
+ + this.temperatureSensor2.getTemperature() + ", tempA="
+ + this.antifrizTemperature.getTemperature() + ", level="
+ + this.level.getPercentageFilled() + ")");
+ }
+
+};
diff --git a/src/com/codlex/thermocycler/logic/bath/cold/Cooler.java b/src/com/codlex/thermocycler/logic/bath/cold/Cooler.java
new file mode 100644
index 0000000..8fe450c
--- /dev/null
+++ b/src/com/codlex/thermocycler/logic/bath/cold/Cooler.java
@@ -0,0 +1,14 @@
+package com.codlex.thermocycler.logic.bath.cold;
+
+import com.codlex.thermocycler.hardware.OnOffDevice;
+import com.pi4j.io.gpio.Pin;
+
+import lombok.extern.log4j.Log4j;
+
+@Log4j
+public class Cooler extends OnOffDevice {
+
+ public Cooler(Pin pin) {
+ super(pin, Cooler.class.getSimpleName());
+ }
+}
diff --git a/src/com/codlex/thermocycler/logic/bath/hot/Heater.java b/src/com/codlex/thermocycler/logic/bath/hot/Heater.java
new file mode 100644
index 0000000..f6636b5
--- /dev/null
+++ b/src/com/codlex/thermocycler/logic/bath/hot/Heater.java
@@ -0,0 +1,13 @@
+package com.codlex.thermocycler.logic.bath.hot;
+
+import com.codlex.thermocycler.hardware.OnOffDevice;
+import com.pi4j.io.gpio.Pin;
+
+import lombok.extern.log4j.Log4j;
+
+@Log4j
+public class Heater extends OnOffDevice {
+ public Heater(Pin pin) {
+ super(pin, Heater.class.getSimpleName());
+ }
+}
diff --git a/src/com/codlex/thermocycler/logic/bath/hot/HotBath.java b/src/com/codlex/thermocycler/logic/bath/hot/HotBath.java
new file mode 100644
index 0000000..3441a6e
--- /dev/null
+++ b/src/com/codlex/thermocycler/logic/bath/hot/HotBath.java
@@ -0,0 +1,107 @@
+package com.codlex.thermocycler.logic.bath.hot;
+
+import com.codlex.thermocycler.hardware.HardwareProvider;
+import com.codlex.thermocycler.hardware.Switch;
+import com.codlex.thermocycler.logic.Settings;
+import com.codlex.thermocycler.logic.Thermocycler;
+import com.codlex.thermocycler.logic.bath.Bath;
+import com.codlex.thermocycler.logic.bath.sensors.TemperatureSensor;
+import com.codlex.thermocycler.tracker.Tracker;
+
+import javafx.application.Platform;
+import lombok.extern.log4j.Log4j;
+
+@Log4j
+public class HotBath extends Bath {
+
+ private final Heater heater;
+ private final Switch circulationWaterPump;
+ private final TemperatureSensor temperatureSensor3;
+
+ public HotBath(Thermocycler thermocycler) {
+ super(thermocycler, Settings.get().getHotBathTemperatureSensor1(), Settings.get().getHotBathTemperatureSensor2(),
+ Settings.get().getHotBathLevelEchoPin(), Settings.get().getHotBathLevelTriggerPin(),
+ Settings.get().getHotBathWaterPump());
+ this.heater = new Heater(Settings.get().getHotBathHeaterPin());
+ this.time.set(1);
+ this.circulationWaterPump = HardwareProvider.get()
+ .getSwitch(Settings.get().getHotBathCirculationWaterPump(), "HotBathCirculationWaterPump");
+ this.temperatureSensor3 = new TemperatureSensor(Settings.get().getHotBathTemperatureSensor3());
+ }
+
+ @Override
+ public void clear() {
+ log.debug("Clearning HotBath.");
+ super.clear();
+ this.heater.turnOff();
+ this.circulationWaterPump.turnOff();
+ }
+
+ @Override
+ public boolean isValid() {
+ boolean isValid = super.isValid();
+ isValid &= Settings.get().getValidationHotTemperatureRange()
+ .contains(this.temperature.get());
+ return isValid;
+ }
+
+ @Override
+ public void keepTemperature() {
+
+ if (Settings.get().getCirculationPumpOn()) {
+ this.circulationWaterPump.turnOn();
+ } else {
+ this.circulationWaterPump.turnOff();
+ }
+
+ if (getCurrentTemperature() < this.temperature.get()) {
+ this.heater.turnOn();
+ } else {
+ this.heater.turnOff();
+ }
+ }
+
+ @Override
+ public void logStatus() {
+ Tracker.track("hot_bath.temperature1",
+ this.temperatureSensor1.getTemperature());
+ Tracker.track("hot_bath.temperature2",
+ this.temperatureSensor2.getTemperature());
+ Tracker.track("hot_bath.temperature3",
+ this.temperatureSensor3.getTemperature());
+ Tracker.track("hot_bath.percentageFilled",
+ this.level.getPercentageFilled());
+ Tracker.track("hot_bath.average", getCurrentTemperature());
+
+ log.debug("HotBathStatus(temp1="
+ + this.temperatureSensor1.getTemperature() + ", temp2="
+ + this.temperatureSensor2.getTemperature() + ", level="
+ + this.level.getPercentageFilled() + ")");
+ }
+
+ @Override
+ public boolean performSafetyChecks() {
+ boolean isOk = super.performSafetyChecks();
+ if (this.temperatureSensor3
+ .getTemperature() > Settings.get().getSafetyHotBathTemperatureMax()) {
+ log.error(
+ "Safety check failed: maximum safe temperature exceeded, t = "
+ + this.temperatureSensor3.getTemperature());
+ isOk &= false;
+ }
+
+ return isOk;
+ }
+
+ @Override
+ public synchronized float getCurrentTemperature() {
+ float sum = this.temperatureSensor1.getTemperature();
+ float averageTemp = sum;
+
+ Platform.runLater(() -> {
+ this.currentTemperatureProperty.setValue(averageTemp);
+ });
+
+ return averageTemp;
+ }
+}
\ No newline at end of file
diff --git a/src/com/codlex/thermocycler/logic/bath/sensors/LevelSensor.java b/src/com/codlex/thermocycler/logic/bath/sensors/LevelSensor.java
new file mode 100644
index 0000000..b915f6e
--- /dev/null
+++ b/src/com/codlex/thermocycler/logic/bath/sensors/LevelSensor.java
@@ -0,0 +1,63 @@
+package com.codlex.thermocycler.logic.bath.sensors;
+
+import com.codlex.thermocycler.hardware.HardwareProvider;
+import com.codlex.thermocycler.hardware.Sensor;
+import com.pi4j.io.gpio.Pin;
+
+import javafx.application.Platform;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import lombok.extern.log4j.Log4j;
+
+@Log4j
+public class LevelSensor {
+
+ private int emptyDistance;
+ private final Sensor distanceMonitor;
+
+ private IntegerProperty property = new SimpleIntegerProperty();
+ private DoubleProperty doubleProperty = new SimpleDoubleProperty();
+ private boolean isFirstTime = true;
+
+ public LevelSensor(Pin echoPin, Pin triggerPin, int emptyDistance) {
+ this.distanceMonitor = HardwareProvider.get()
+ .getDistanceSensorForPins(echoPin, triggerPin);
+ this.distanceMonitor.startMeasuring();
+ this.emptyDistance = emptyDistance;
+ }
+
+ public DoubleProperty getDoubleProperty() {
+ getPercentageFilled(); // dummy way to refresh
+ return this.doubleProperty;
+ }
+
+ public int getPercentageFilled() {
+ float distanceFromWater = this.distanceMonitor.getValue().floatValue();
+
+ float filledCM = this.emptyDistance - distanceFromWater;
+ int integerValue = (int) ((filledCM / this.emptyDistance) * 100);
+
+ int diff = this.isFirstTime ? 0 : Math.abs(this.property.get() - integerValue);
+
+// if (0 <= integerValue && integerValue <= 100 && diff < 10) {
+ Platform.runLater(() -> {
+ this.property.set(integerValue);
+ this.doubleProperty.set(integerValue / 100.0);
+ });
+// } else {
+// log.error("Ignoring value: " + integerValue);
+// }
+
+ this.isFirstTime = false;
+
+ return this.property.get();
+ }
+
+ public IntegerProperty getProperty() {
+ getPercentageFilled(); // dummy way to refresh
+ return this.property;
+ }
+
+}
\ No newline at end of file
diff --git a/src/com/codlex/thermocycler/logic/bath/sensors/TemperatureSensor.java b/src/com/codlex/thermocycler/logic/bath/sensors/TemperatureSensor.java
new file mode 100644
index 0000000..affe5c7
--- /dev/null
+++ b/src/com/codlex/thermocycler/logic/bath/sensors/TemperatureSensor.java
@@ -0,0 +1,23 @@
+package com.codlex.thermocycler.logic.bath.sensors;
+
+import com.codlex.thermocycler.hardware.HardwareProvider;
+import com.codlex.thermocycler.hardware.Sensor;
+
+import lombok.extern.log4j.Log4j;
+
+@Log4j
+public class TemperatureSensor {
+
+ private final Sensor sensor;
+
+ public TemperatureSensor(String sensorAddress) {
+ log.debug("Created sensor with address: " + sensorAddress);
+ this.sensor = HardwareProvider.get()
+ .getTemperatureSensor(sensorAddress);
+ this.sensor.startMeasuring();
+ }
+
+ public float getTemperature() {
+ return this.sensor.getValue();
+ }
+}
diff --git a/src/com/codlex/thermocycler/tracker/Tracker.java b/src/com/codlex/thermocycler/tracker/Tracker.java
new file mode 100644
index 0000000..0709046
--- /dev/null
+++ b/src/com/codlex/thermocycler/tracker/Tracker.java
@@ -0,0 +1,54 @@
+package com.codlex.thermocycler.tracker;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.net.Socket;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import com.codlex.thermocycler.logic.Settings;
+
+public class Tracker {
+
+ final static ExecutorService TRACK_EXECUTOR = Executors
+ .newSingleThreadExecutor();
+
+ final static int port = 2003;
+ final static String address = "pi-dash";
+
+ public static void track(String path, Number value) {
+ if (!Settings.get().getTrackingOn()) {
+ return;
+ }
+
+ TRACK_EXECUTOR.submit(() -> {
+ Socket socket = null;
+ OutputStreamWriter writer = null;
+ try {
+ socket = new Socket(address, port);
+ writer = new OutputStreamWriter(socket.getOutputStream());
+ writer.write(path + " " + value + " "
+ + (System.currentTimeMillis() / 1000) + "\n");
+ writer.flush();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ if (socket != null) {
+ try {
+ socket.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ });
+ }
+
+}
diff --git a/src/com/codlex/thermocycler/view/RootController.java b/src/com/codlex/thermocycler/view/RootController.java
new file mode 100644
index 0000000..1cfa19a
--- /dev/null
+++ b/src/com/codlex/thermocycler/view/RootController.java
@@ -0,0 +1,57 @@
+package com.codlex.thermocycler.view;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Button;
+import lombok.extern.log4j.Log4j;
+
+@Log4j
+public class RootController extends ThermocyclerController {
+
+ @FXML
+ protected Button nextButton;
+
+ @FXML
+ protected Button backButton;
+
+ private ThermocyclerController getController() {
+ return this.gui.getCurrentScene().getController();
+ }
+
+ @Override
+ protected boolean isRoot() {
+ return true;
+ }
+
+ @Override
+ protected void onBackClick() {
+ getController().onBackClick();
+ }
+
+ @Override
+ protected void onNextClick() {
+ getController().onNextClick();
+ }
+
+ @Override
+ protected void onUpdateUI() {
+ final String VALID_BUTTON_STYLE = "-fx-background-color: green; -fx-text-fill: white";
+ final String INVALID_BUTTON_STYLE = "-fx-background-color: gray; -fx-text-fill: white";
+
+ final ThermocyclerController controller = getController();
+
+ boolean isNextValid = controller.validation();
+ this.nextButton.setStyle(
+ isNextValid ? VALID_BUTTON_STYLE : INVALID_BUTTON_STYLE);
+ this.nextButton.setDisable(!isNextValid);
+ this.nextButton.setText(controller.getNextLabel());
+
+ boolean isBackValid = controller.backValidation();
+ this.backButton.setStyle(
+ isNextValid ? VALID_BUTTON_STYLE : INVALID_BUTTON_STYLE);
+ this.backButton.setDisable(!isBackValid);
+ this.backButton.setText(controller.getBackLabel());
+
+ controller.onUpdateUI();
+
+ }
+}
diff --git a/src/com/codlex/thermocycler/view/RootScene.fxml b/src/com/codlex/thermocycler/view/RootScene.fxml
new file mode 100644
index 0000000..2496420
--- /dev/null
+++ b/src/com/codlex/thermocycler/view/RootScene.fxml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/com/codlex/thermocycler/view/ScreenAlarmClock.java b/src/com/codlex/thermocycler/view/ScreenAlarmClock.java
new file mode 100644
index 0000000..3179e52
--- /dev/null
+++ b/src/com/codlex/thermocycler/view/ScreenAlarmClock.java
@@ -0,0 +1,36 @@
+package com.codlex.thermocycler.view;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicLong;
+
+import lombok.extern.log4j.Log4j;
+
+@Log4j
+public class ScreenAlarmClock {
+
+ private static final ExecutorService RINGER = Executors
+ .newSingleThreadExecutor();
+ private static final long WAKE_UP_TIME = Duration.ofMinutes(9).toMillis();
+
+ private AtomicLong lastTouchEventTime = new AtomicLong(System.currentTimeMillis());
+
+ public void onTouchEvent() {
+ final long now = System.currentTimeMillis();
+ final long diff = now - this.lastTouchEventTime.getAndSet(now);
+
+ if (diff > WAKE_UP_TIME) {
+ RINGER.submit(() -> {
+ log.debug("Waking up screen.");
+ final String wakeUpCommand = "/usr/bin/xinit";
+ try {
+ Runtime.getRuntime().exec(wakeUpCommand);
+ } catch (IOException e) {
+ log.error("Something went wrong with waking up.", e);
+ }
+ });
+ }
+ }
+}
diff --git a/src/com/codlex/thermocycler/view/ThermocyclerController.java b/src/com/codlex/thermocycler/view/ThermocyclerController.java
new file mode 100644
index 0000000..0b77d93
--- /dev/null
+++ b/src/com/codlex/thermocycler/view/ThermocyclerController.java
@@ -0,0 +1,71 @@
+package com.codlex.thermocycler.view;
+
+import com.codlex.thermocycler.logic.Thermocycler;
+
+import javafx.fxml.FXML;
+import lombok.extern.log4j.Log4j;
+
+@Log4j
+public abstract class ThermocyclerController {
+
+ protected Thermocycler thermocycler;
+ protected ThermocyclerGUI gui;
+
+ protected boolean backValidation() {
+ return true;
+ }
+
+ protected String getBackLabel() {
+ return "Back";
+ }
+
+ protected String getNextLabel() {
+ return "Next";
+ }
+
+ protected boolean isRoot() {
+ return false;
+ }
+
+ @FXML
+ protected void onBackClick() {
+ this.gui.previousScene();
+ }
+
+ protected void onModelInitialized() {
+
+ }
+
+ @FXML
+ protected void onNextClick() {
+ this.gui.nextScene();
+ }
+
+ protected void onUpdateUI() {
+
+ }
+
+ public final void setGui(ThermocyclerGUI gui) {
+ this.gui = gui;
+ }
+
+ public final void setModel(Thermocycler thermocycler) {
+ if (!isRoot()) {
+ if (this.thermocycler == null) {
+ this.thermocycler = thermocycler;
+ onModelInitialized();
+ }
+
+ updateUI();
+ }
+ }
+
+ protected final void updateUI() {
+ this.gui.updateUI();
+ }
+
+ protected boolean validation() {
+ return true;
+ }
+
+}
diff --git a/src/com/codlex/thermocycler/view/ThermocyclerGUI.java b/src/com/codlex/thermocycler/view/ThermocyclerGUI.java
new file mode 100644
index 0000000..a02bd71
--- /dev/null
+++ b/src/com/codlex/thermocycler/view/ThermocyclerGUI.java
@@ -0,0 +1,131 @@
+package com.codlex.thermocycler.view;
+
+import java.io.IOException;
+
+import org.apache.log4j.BasicConfigurator;
+import org.apache.log4j.PropertyConfigurator;
+
+import com.codlex.thermocycler.logic.Settings;
+import com.codlex.thermocycler.logic.Thermocycler;
+import com.codlex.thermocycler.logic.ThermocyclerWorker;
+
+import javafx.application.Application;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Scene;
+import javafx.scene.input.TouchEvent;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.Pane;
+import javafx.stage.Stage;
+import lombok.Getter;
+import lombok.extern.log4j.Log4j;
+
+@Log4j
+public class ThermocyclerGUI extends Application {
+
+ public static void main(String[] args) {
+ PropertyConfigurator.configure("log4j.properties");
+ log.debug("################ Thermocycler is starting ################");
+ launch(new String[0]);
+ }
+
+ private Stage primaryStage;
+
+ private BorderPane rootLayout;
+
+ private Thermocycler thermocycler;
+
+ @Getter
+ private ThermocyclerScene currentScene = ThermocyclerScene.FillInBaths;
+
+ private ThermocyclerController controller;
+
+ private ScreenAlarmClock screenAlarmClock = new ScreenAlarmClock();
+
+ /**
+ * Returns the main stage.
+ *
+ * @return
+ */
+ public Stage getPrimaryStage() {
+ return this.primaryStage;
+ }
+
+ /**
+ * Initializes the root layout.
+ */
+ public void initRootLayout() {
+ try {
+ // Load root layout from fxml file.
+ FXMLLoader loader = new FXMLLoader();
+ loader.setLocation(ThermocyclerGUI.class.getResource("RootScene.fxml"));
+ this.rootLayout = loader.load();
+
+ // Show the scene containing the root layout.
+ Scene scene = new Scene(this.rootLayout);
+ this.primaryStage.setFullScreen(Settings.get().getFullScreen());
+ this.primaryStage.setScene(scene);
+ this.primaryStage.show();
+
+ this.primaryStage.addEventFilter(TouchEvent.ANY, new EventHandler() {
+ @Override
+ public void handle(Event event) {
+ ThermocyclerGUI.this.screenAlarmClock.onTouchEvent();
+ };
+ });
+
+ this.controller = loader.getController();
+ this.controller.setGui(this);
+ this.controller.setModel(this.thermocycler);
+
+ if (this.thermocycler.lastFinishedSuccessfully()) {
+ setScene(ThermocyclerScene.FillInBaths);
+ } else {
+ setScene(ThermocyclerScene.ThermocyclerOverview);
+ }
+
+ if (!Settings.get().getProduction()) {
+ this.rootLayout.setRight(ThermocyclerScene.MockSensors.load(this.thermocycler, this));
+ }
+
+ } catch (IOException e) {
+ log.error(e);
+ }
+ }
+
+ public void nextScene() {
+ setScene(this.currentScene.nextScene());
+ }
+
+ public void previousScene() {
+ setScene(this.currentScene.previousScene());
+ }
+
+ public void setScene(ThermocyclerScene scene) {
+ this.currentScene = scene;
+ Pane pane = scene.load(this.thermocycler, this);
+ if (pane == null) {
+ throw new RuntimeException(scene.name() + " couldn't be loaded!");
+ }
+ this.rootLayout.setCenter(pane);
+ }
+
+ @Override
+ public void start(Stage primaryStage) {
+ this.thermocycler = new Thermocycler();
+ ThermocyclerWorker.start(this.thermocycler);
+
+ log.debug("Initializing GUI start");
+ this.primaryStage = primaryStage;
+ this.primaryStage.setTitle("Thermocycler");
+ initRootLayout();
+ log.debug("Initializing GUI end");
+ }
+
+ public void updateUI() {
+ this.controller.onUpdateUI();
+
+ }
+
+}
diff --git a/src/com/codlex/thermocycler/view/ThermocyclerScene.java b/src/com/codlex/thermocycler/view/ThermocyclerScene.java
new file mode 100644
index 0000000..6610c1d
--- /dev/null
+++ b/src/com/codlex/thermocycler/view/ThermocyclerScene.java
@@ -0,0 +1,65 @@
+package com.codlex.thermocycler.view;
+
+import java.io.IOException;
+
+import com.codlex.thermocycler.logic.Thermocycler;
+
+import javafx.fxml.FXMLLoader;
+import javafx.scene.layout.Pane;
+import lombok.extern.log4j.Log4j;
+
+@Log4j
+public enum ThermocyclerScene {
+ MockSensors, FillInBaths, FillRefillTanks, PutSpecimen, HotBathConfiguration, ColdBathConfiguration, CyclesConfiguration, ThermocyclerOverview, Shutdown;
+
+ private final FXMLLoader loader;
+ private Pane pane;
+ private ThermocyclerController controller;
+
+ private ThermocyclerScene() {
+ this.loader = new FXMLLoader();
+ this.loader.setLocation(ThermocyclerScene.class
+ .getResource("scenes/" + name() + ".fxml"));
+ try {
+ this.pane = this.loader.load();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ this.controller = this.loader.getController();
+ }
+
+ @SuppressWarnings("unchecked")
+ public T getController() {
+ return (T) this.loader.getController();
+ }
+
+ public boolean hasNextScene() {
+ return ordinal() != values().length - 1;
+ }
+
+ private boolean hasPreviousScene() {
+ return ordinal() != 0;
+ }
+
+ public Pane load(Thermocycler thermocycler, ThermocyclerGUI gui) {
+ this.controller.setGui(gui);
+ this.controller.setModel(thermocycler);
+ return this.pane;
+ }
+
+ public ThermocyclerScene nextScene() {
+ if (!hasNextScene()) {
+ throw new RuntimeException(name() + " doesn't have next scene!");
+ }
+ return values()[ordinal() + 1];
+ }
+
+ public ThermocyclerScene previousScene() {
+ if (!hasPreviousScene()) {
+ throw new RuntimeException(
+ name() + " doesn't have previous scene!");
+ }
+
+ return values()[ordinal() - 1];
+ }
+}
diff --git a/src/com/codlex/thermocycler/view/scenes/AbstractBathController.java b/src/com/codlex/thermocycler/view/scenes/AbstractBathController.java
new file mode 100644
index 0000000..96a930c
--- /dev/null
+++ b/src/com/codlex/thermocycler/view/scenes/AbstractBathController.java
@@ -0,0 +1,132 @@
+package com.codlex.thermocycler.view.scenes;
+
+import com.codlex.thermocycler.logic.bath.Bath;
+import com.codlex.thermocycler.view.ThermocyclerController;
+
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.fxml.FXML;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.Slider;
+import javafx.scene.control.ToggleButton;
+import lombok.extern.log4j.Log4j;
+
+@Log4j
+public abstract class AbstractBathController extends ThermocyclerController {
+
+ private static void decrement(IntegerProperty property) {
+ property.set(property.get() - 1);
+ }
+
+ private static void increment(IntegerProperty property) {
+ property.set(property.get() + 1);
+ }
+
+ @FXML
+ private ToggleButton logicOn;
+
+ @FXML
+ private Slider temperatureSlider;
+
+ @FXML
+ private Label temperatureLabel;
+
+ @FXML
+ private Button minusOneCelzius;
+
+ @FXML
+ private Button plusOneCelzius;
+
+ @FXML
+ private Label timeLabel;
+
+ @FXML
+ private Slider timeSlider;
+
+ @FXML
+ private Button minusOneSecond;
+
+ @FXML
+ private Button plusOneSecond;
+
+ private Bath bath;
+
+ protected abstract Bath getBath();
+
+ @FXML
+ private void onMinusOneCelziusClick() {
+ decrement(this.bath.getTemperatureProperty());
+ }
+
+ @FXML
+ private void onMinusOneSecondClick() {
+ decrement(this.bath.getTimeProperty());
+ }
+
+ @Override
+ protected void onModelInitialized() {
+ this.bath = getBath();
+ this.temperatureSlider.valueProperty()
+ .bindBidirectional(this.bath.getTemperatureProperty());
+ this.bath.getTemperatureProperty().addListener((newValue) -> {
+
+ IntegerProperty property = (IntegerProperty) newValue;
+ Integer value = property.get();
+ updateTemperature(value);
+ updateUI();
+ });
+ updateTemperature(this.bath.getTemperatureProperty().get());
+
+ this.timeSlider.valueProperty()
+ .bindBidirectional(this.bath.getTimeProperty());
+ this.bath.getTimeProperty().addListener((newValue) -> {
+ IntegerProperty property = (IntegerProperty) newValue;
+ Integer value = property.get();
+ updateTime(value);
+ updateUI();
+ });
+ updateTime(this.bath.getTimeProperty().get());
+ this.logicOn.selectedProperty().set(true);
+ this.logicOn.selectedProperty().addListener(new ChangeListener() {
+ @Override
+ public void changed(ObservableValue extends Boolean> observable, Boolean oldValue, Boolean newValue) {
+ AbstractBathController.this.bath.setLogicOn(newValue);
+ }
+ });
+ }
+
+ @FXML
+ private void onPlusOneCelziusClick() {
+ increment(this.bath.getTemperatureProperty());
+ }
+
+ @FXML
+ private void onPlusOneSecondClick() {
+ increment(this.bath.getTimeProperty());
+ }
+
+ @Override
+ protected void onUpdateUI() {
+ this.minusOneCelzius.setDisable(this.bath.getTemperatureProperty()
+ .get() < (int) this.temperatureSlider.getMin() + 1);
+ this.plusOneCelzius.setDisable(this.bath.getTemperatureProperty()
+ .get() > (int) this.temperatureSlider.getMax() - 1);
+
+ this.minusOneSecond.setDisable(this.bath.getTimeProperty()
+ .get() < (int) this.timeSlider.getMin() + 1);
+ this.plusOneSecond.setDisable(this.bath.getTimeProperty()
+ .get() > (int) this.timeSlider.getMax() - 1);
+
+ }
+
+ private void updateTemperature(Integer value) {
+ this.temperatureLabel.textProperty().set(String.format("%d°C", value));
+ }
+
+ private void updateTime(Integer value) {
+ this.timeLabel.textProperty()
+ .set(String.format("%d:%02d", value / 60, value % 60));
+ }
+}
diff --git a/src/com/codlex/thermocycler/view/scenes/ColdBathConfiguration.fxml b/src/com/codlex/thermocycler/view/scenes/ColdBathConfiguration.fxml
new file mode 100644
index 0000000..f790b91
--- /dev/null
+++ b/src/com/codlex/thermocycler/view/scenes/ColdBathConfiguration.fxml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/com/codlex/thermocycler/view/scenes/ColdBathConfigurationController.java b/src/com/codlex/thermocycler/view/scenes/ColdBathConfigurationController.java
new file mode 100644
index 0000000..44ff90d
--- /dev/null
+++ b/src/com/codlex/thermocycler/view/scenes/ColdBathConfigurationController.java
@@ -0,0 +1,12 @@
+package com.codlex.thermocycler.view.scenes;
+
+import com.codlex.thermocycler.logic.bath.Bath;
+
+public class ColdBathConfigurationController extends AbstractBathController {
+
+ @Override
+ protected Bath getBath() {
+ return this.thermocycler.getColdBath();
+ }
+
+}
diff --git a/src/com/codlex/thermocycler/view/scenes/CyclesConfiguration.fxml b/src/com/codlex/thermocycler/view/scenes/CyclesConfiguration.fxml
new file mode 100644
index 0000000..24d24be
--- /dev/null
+++ b/src/com/codlex/thermocycler/view/scenes/CyclesConfiguration.fxml
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/com/codlex/thermocycler/view/scenes/CyclesConfiguration.java b/src/com/codlex/thermocycler/view/scenes/CyclesConfiguration.java
new file mode 100644
index 0000000..18f8cad
--- /dev/null
+++ b/src/com/codlex/thermocycler/view/scenes/CyclesConfiguration.java
@@ -0,0 +1,156 @@
+package com.codlex.thermocycler.view.scenes;
+
+import java.util.function.Consumer;
+
+import com.codlex.thermocycler.view.ThermocyclerController;
+import com.google.common.collect.ImmutableList;
+
+import javafx.beans.property.IntegerProperty;
+import javafx.fxml.FXML;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.input.MouseEvent;
+
+public class CyclesConfiguration extends ThermocyclerController {
+
+ @FXML
+ private Button increment1;
+
+ @FXML
+ private Button increment10;
+
+ @FXML
+ private Button increment100;
+
+ @FXML
+ private Button increment1000;
+
+ @FXML
+ private Button increment10000;
+
+ @FXML
+ private Button increment100000;
+
+ @FXML
+ private Button decrement1;
+
+ @FXML
+ private Button decrement10;
+
+ @FXML
+ private Button decrement100;
+
+ @FXML
+ private Button decrement1000;
+
+ @FXML
+ private Button decrement10000;
+
+ @FXML
+ private Button decrement100000;
+
+ @FXML
+ private Label digit1;
+
+ @FXML
+ private Label digit10;
+
+ @FXML
+ private Label digit100;
+
+ @FXML
+ private Label digit1000;
+
+ @FXML
+ private Label digit10000;
+
+ @FXML
+ private Label digit100000;
+
+ private ImmutableList