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 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/com/codlex/thermocycler/view/scenes/HotBathConfigurationController.java b/src/com/codlex/thermocycler/view/scenes/HotBathConfigurationController.java new file mode 100644 index 0000000..2c7a68e --- /dev/null +++ b/src/com/codlex/thermocycler/view/scenes/HotBathConfigurationController.java @@ -0,0 +1,12 @@ +package com.codlex.thermocycler.view.scenes; + +import com.codlex.thermocycler.logic.bath.Bath; + +public class HotBathConfigurationController extends AbstractBathController { + + @Override + protected Bath getBath() { + return this.thermocycler.getHotBath(); + } + +} diff --git a/src/com/codlex/thermocycler/view/scenes/MockSensors.fxml b/src/com/codlex/thermocycler/view/scenes/MockSensors.fxml new file mode 100644 index 0000000..9a255dd --- /dev/null +++ b/src/com/codlex/thermocycler/view/scenes/MockSensors.fxml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/com/codlex/thermocycler/view/scenes/MockSensorsController.java b/src/com/codlex/thermocycler/view/scenes/MockSensorsController.java new file mode 100644 index 0000000..e936cb5 --- /dev/null +++ b/src/com/codlex/thermocycler/view/scenes/MockSensorsController.java @@ -0,0 +1,111 @@ +package com.codlex.thermocycler.view.scenes; + +import com.codlex.thermocycler.hardware.VirtualSensors; +import com.codlex.thermocycler.hardware.VirtualSwitches; +import com.codlex.thermocycler.view.ThermocyclerController; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.FloatProperty; +import javafx.fxml.FXML; +import javafx.scene.control.Slider; +import javafx.scene.control.TextField; +import javafx.scene.control.ToggleButton; +import javafx.util.converter.NumberStringConverter; + +public class MockSensorsController extends ThermocyclerController { + + private static void bind(FloatProperty sensor, Slider slider, + TextField field) { + slider.valueProperty().bindBidirectional(sensor); + field.textProperty().bindBidirectional(sensor, + new NumberStringConverter()); + } + + @FXML + private Slider hotBathTemperature1Slider; + + @FXML + private TextField hotBathTemperature1Field; + + @FXML + private Slider hotBathTemperature2Slider; + + @FXML + private TextField hotBathTemperature2Field; + + @FXML + private Slider hotBathTemperature3Slider; + + @FXML + private TextField hotBathTemperature3Field; + + @FXML + private ToggleButton hotBathHeater; + + @FXML + private Slider coldBathTemperature1Slider; + + @FXML + private TextField coldBathTemperature1Field; + + @FXML + private Slider coldBathTemperature2Slider; + + @FXML + private TextField coldBathTemperature2Field; + + @FXML + private Slider coldBathTemperatureAntifrizSlider; + + @FXML + private TextField coldBathTemperatureAntifrizField; + + @FXML + private ToggleButton coldBathCooler; + + @FXML + private Slider hotBathLevelSlider; + + @FXML + private Slider coldBathLevelSlider; + + @FXML + private TextField cycles; + + public void bind() { + bind(VirtualSensors.get().hotBathTemperature1, + this.hotBathTemperature1Slider, this.hotBathTemperature1Field); + bind(VirtualSensors.get().hotBathTemperature2, + this.hotBathTemperature2Slider, this.hotBathTemperature2Field); + bind(VirtualSensors.get().hotBathTemperatureSafety, + this.hotBathTemperature3Slider, this.hotBathTemperature3Field); + + bind(VirtualSwitches.get().hotBathHeater, this.hotBathHeater); + VirtualSensors.get().hotBathDistance + .bindBidirectional(this.hotBathLevelSlider.valueProperty()); + + bind(VirtualSensors.get().coldBathTemperature1, + this.coldBathTemperature1Slider, + this.coldBathTemperature1Field); + bind(VirtualSensors.get().coldBathTemperature2, + this.coldBathTemperature2Slider, + this.coldBathTemperature2Field); + bind(VirtualSensors.get().coldBathTemperatureAntifriz, + this.coldBathTemperatureAntifrizSlider, + this.coldBathTemperatureAntifrizField); + bind(VirtualSwitches.get().coldBathCooler, this.coldBathCooler); + VirtualSensors.get().coldBathDistance + .bindBidirectional(this.coldBathLevelSlider.valueProperty()); + + } + + private void bind(BooleanProperty sensor, ToggleButton button) { + button.selectedProperty().bindBidirectional(sensor); + } + + @Override + protected void onModelInitialized() { + bind(); + } + +} diff --git a/src/com/codlex/thermocycler/view/scenes/PutSpecimen.fxml b/src/com/codlex/thermocycler/view/scenes/PutSpecimen.fxml new file mode 100644 index 0000000..c827094 --- /dev/null +++ b/src/com/codlex/thermocycler/view/scenes/PutSpecimen.fxml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/src/com/codlex/thermocycler/view/scenes/PutSpecimen.java b/src/com/codlex/thermocycler/view/scenes/PutSpecimen.java new file mode 100644 index 0000000..eca3cf8 --- /dev/null +++ b/src/com/codlex/thermocycler/view/scenes/PutSpecimen.java @@ -0,0 +1,7 @@ +package com.codlex.thermocycler.view.scenes; + +import com.codlex.thermocycler.view.ThermocyclerController; + +public class PutSpecimen extends ThermocyclerController { + +} diff --git a/src/com/codlex/thermocycler/view/scenes/Shutdown.fxml b/src/com/codlex/thermocycler/view/scenes/Shutdown.fxml new file mode 100644 index 0000000..aadcad3 --- /dev/null +++ b/src/com/codlex/thermocycler/view/scenes/Shutdown.fxml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/src/com/codlex/thermocycler/view/scenes/ShutdownController.java b/src/com/codlex/thermocycler/view/scenes/ShutdownController.java new file mode 100644 index 0000000..bf2ab31 --- /dev/null +++ b/src/com/codlex/thermocycler/view/scenes/ShutdownController.java @@ -0,0 +1,43 @@ +package com.codlex.thermocycler.view.scenes; + +import com.codlex.thermocycler.view.ThermocyclerController; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; + +public class ShutdownController extends ThermocyclerController { + + @FXML + private Label message; + private boolean alreadyClicked; + + @Override + protected boolean backValidation() { + return false; + } + + @Override + protected String getNextLabel() { + if (this.alreadyClicked) { + return "Shutdown"; + } else { + return "Next"; + } + } + + @Override + protected void onNextClick() { + if (this.alreadyClicked) { + System.exit(0); + } + this.thermocycler.lowerTranslator(); + this.message.setText("You may now shutdown Thermocycler."); + this.alreadyClicked = true; + updateUI(); + } + + @Override + protected boolean validation() { + return true; + } +} diff --git a/src/com/codlex/thermocycler/view/scenes/ThermocyclerOverview.fxml b/src/com/codlex/thermocycler/view/scenes/ThermocyclerOverview.fxml new file mode 100644 index 0000000..fd80ac0 --- /dev/null +++ b/src/com/codlex/thermocycler/view/scenes/ThermocyclerOverview.fxml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/com/codlex/thermocycler/view/scenes/ThermocyclerOverviewController.java b/src/com/codlex/thermocycler/view/scenes/ThermocyclerOverviewController.java new file mode 100644 index 0000000..504641f --- /dev/null +++ b/src/com/codlex/thermocycler/view/scenes/ThermocyclerOverviewController.java @@ -0,0 +1,297 @@ +package com.codlex.thermocycler.view.scenes; + +import com.codlex.thermocycler.logic.State; +import com.codlex.thermocycler.logic.bath.Bath; +import com.codlex.thermocycler.view.ThermocyclerController; +import com.codlex.thermocycler.view.ThermocyclerScene; + +import javafx.animation.PauseTransition; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.StringProperty; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.util.Duration; +import lombok.extern.log4j.Log4j; + +@Log4j +public class ThermocyclerOverviewController extends ThermocyclerController { + + private static final String temperatureSensorFormat = "%.0f°C / %s"; + + private static final String temperatureFormat = "%d°C"; + + private static final String timeFormat = "%d:%02d"; + + @FXML + private Label title; + + @FXML + private Label hotBathTitle; + + @FXML + private Label coldBathTitle; + + @FXML + private Label hotBathTemperature; + + @FXML + private Label coldBathTemperature; + + @FXML + private Label hotBathTime; + + @FXML + private Label coldBathTime; + + @FXML + private Label cycles; + + @FXML + private Label estimateTime; + + private State currentState; + + private String okStyle = "-fx-text-fill: green"; + + private String notOkStyle = "-fx-text-fill: red"; + private String neutralStyle = "-fx-text-fill: black"; + @Override + protected boolean backValidation() { + return this.currentState == State.NotStarted + || this.currentState == State.UnexpectedShutdown; + } + + private void everySeconds() { + // TODO: make this better + PauseTransition wait = new PauseTransition(Duration.seconds(1)); + wait.setOnFinished((e) -> { + if (this.currentState == State.ColdBath + || this.currentState == State.HotBath + || this.currentState == State.ToColdBathPause + || this.currentState == State.ToHotBathPause + || this.currentState == State.ToHotBathMiddlePause + || this.currentState == State.ToColdBathMiddlePause) { + // because of immersion time calculation + updateUI(); + + } + if (this.currentState != State.Finished) { + wait.playFromStart(); + } + }); + wait.play(); + } + + @Override + protected String getBackLabel() { + if (this.currentState == State.UnexpectedShutdown) { + return "Start over"; + } else { + return super.getBackLabel(); + } + } + + @Override + protected String getNextLabel() { + switch (this.currentState) { + case UnexpectedShutdown : + return "Continue"; + case NotStarted : + return "Start"; + case Finished : + return "Finish"; + default : + return super.getNextLabel(); + } + } + + @Override + protected void onBackClick() { + if (this.currentState == State.UnexpectedShutdown) { + this.thermocycler.reset(); + this.gui.setScene(ThermocyclerScene.FillInBaths); + } else { + super.onBackClick(); + } + } + + @Override + protected void onModelInitialized() { + this.thermocycler.getColdBath().getCurrentTemperatureProperty() + .addListener((a) -> { + updateUI(); + }); + + this.thermocycler.getHotBath().getCurrentTemperatureProperty() + .addListener((a) -> { + updateUI(); + }); + + this.thermocycler.getStateLogic().getStateProperty() + .addListener((property) -> { + ObjectProperty stateProperty = (ObjectProperty) property; + this.currentState = stateProperty.get(); + updateUI(); + }); + + this.currentState = this.thermocycler.getStateLogic().getStateProperty() + .get(); + + everySeconds(); + } + + @Override + protected void onNextClick() { + + switch (this.currentState) { + case NotStarted : + this.thermocycler.start(); + break; + case NotReady : + log.error( + "Tried to click next when it should have been disabled."); + break; + case ColdBath : + log.error( + "Tried to click next when it should have been disabled."); + break; + case HotBath : + log.error( + "Tried to click next when it should have been disabled."); + break; + case Finished : + this.gui.nextScene(); + break; + case UnexpectedShutdown : + this.thermocycler.start(); + break; + } + + updateUI(); + } + + private void onNotReadyUpdateUI(Bath coldBath, Bath hotBath) { + StringProperty coldBathTemperatureProperty = this.coldBathTemperature + .textProperty(); + float currentColdBathTemperature = this.thermocycler.getColdBath() + .getCurrentTemperatureProperty().get(); + coldBathTemperatureProperty.set(String.format(temperatureSensorFormat, + currentColdBathTemperature, coldBathTemperatureProperty.get())); + this.coldBathTemperature.setStyle( + coldBath.isTemperatureOK() ? this.okStyle : this.notOkStyle); + + StringProperty hotBathTemperatureProperty = this.hotBathTemperature + .textProperty(); + float currenthotBathTemperature = this.thermocycler.getHotBath() + .getCurrentTemperatureProperty().get(); + hotBathTemperatureProperty.set(String.format(temperatureSensorFormat, + currenthotBathTemperature, hotBathTemperatureProperty.get())); + this.hotBathTemperature.setStyle( + hotBath.isTemperatureOK() ? this.okStyle : this.notOkStyle); + } + + @Override + protected void onUpdateUI() { + Bath hotBath = this.thermocycler.getHotBath(); + this.hotBathTemperature.textProperty().set(String.format( + temperatureFormat, hotBath.getTemperatureProperty().get())); + int hotTime = hotBath.getTimeProperty().get(); + this.hotBathTime.textProperty() + .set(String.format(timeFormat, hotTime / 60, hotTime % 60)); + + Bath coldBath = this.thermocycler.getColdBath(); + this.coldBathTemperature.textProperty().set(String.format( + temperatureFormat, coldBath.getTemperatureProperty().get())); + int coldTime = coldBath.getTimeProperty().get(); + this.coldBathTime.textProperty() + .set(String.format(timeFormat, coldTime / 60, coldTime % 60)); + this.cycles.textProperty() + .set(Integer.toString(this.thermocycler.getCycles().get())); + int minutes = this.thermocycler.getTimeLeft() / 60; + int seconds = this.thermocycler.getTimeLeft() % 60; + + this.estimateTime.textProperty() + .set(this.thermocycler.getFinishTime().toString()); + + switch (this.currentState) { + case NotStarted : + this.title.setText("Thermocycler overview"); + break; + case NotReady : + this.title.setText("Warming up..."); + onNotReadyUpdateUI(coldBath, hotBath); + break; + case ColdBath : + this.title.setText("Cooling specimen..."); + this.coldBathTitle.setStyle(this.okStyle); + this.hotBathTitle.setStyle(this.neutralStyle); + this.coldBathTime + .setText(String.format(timeFormat, minutes, seconds)); + this.cycles.setText( + this.thermocycler.getStateLogic().getCurrenCycle() + + " / " + this.cycles.getText()); + break; + + case ToHotBathPause: + this.coldBathTitle.setStyle(this.neutralStyle); + this.hotBathTitle.setStyle(this.neutralStyle); + this.title.setText("Pausing for: " + String.format(timeFormat, minutes, seconds)); + break; + + case ToColdBathPause: + this.coldBathTitle.setStyle(this.neutralStyle); + this.hotBathTitle.setStyle(this.neutralStyle); + this.title.setText("Pausing for: " + String.format(timeFormat, minutes, seconds)); + break; + + case ToHotBathMiddlePause: + this.coldBathTitle.setStyle(this.neutralStyle); + this.hotBathTitle.setStyle(this.neutralStyle); + this.title.setText("Pausing (m) for: " + String.format(timeFormat, minutes, seconds)); + break; + + case ToColdBathMiddlePause: + this.coldBathTitle.setStyle(this.neutralStyle); + this.hotBathTitle.setStyle(this.neutralStyle); + this.title.setText("Pausing (m) for: " + String.format(timeFormat, minutes, seconds)); + break; + + case HotBath : + this.title.setText("Heating specimen..."); + this.coldBathTitle.setStyle(this.neutralStyle); + this.hotBathTitle.setStyle(this.okStyle); + this.hotBathTime + .setText(String.format(timeFormat, minutes, seconds)); + this.cycles.setText( + this.thermocycler.getStateLogic().getCurrenCycle() + + " / " + this.cycles.getText()); + break; + + case Finished : + this.title.setText("Cycling completed click finish."); + break; + case UnexpectedShutdown : + this.title.setText( + "Shutdown unepectedly do you wish to continue?"); + this.cycles.setText( + this.thermocycler.getStateLogic().getCurrenCycle() + + " / " + this.cycles.getText()); + break; + } + + } + + @Override + protected boolean validation() { + switch (this.currentState) { + case NotStarted : + return true; + case Finished : + return true; + case UnexpectedShutdown : + return true; + default : + return false; + } + } +} diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..a05dbea --- /dev/null +++ b/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/startRemote.sh b/startRemote.sh new file mode 100755 index 0000000..4d374b6 --- /dev/null +++ b/startRemote.sh @@ -0,0 +1 @@ +ssh pi@pi "cd tc && ./start.sh" diff --git a/target/classes/com/codlex/thermocycler/logic/RaspiPinsExtened.class b/target/classes/com/codlex/thermocycler/logic/RaspiPinsExtened.class new file mode 100644 index 0000000..b81840a Binary files /dev/null and b/target/classes/com/codlex/thermocycler/logic/RaspiPinsExtened.class differ diff --git a/target/classes/com/codlex/thermocycler/logic/Settings.class b/target/classes/com/codlex/thermocycler/logic/Settings.class new file mode 100644 index 0000000..0a1a206 Binary files /dev/null and b/target/classes/com/codlex/thermocycler/logic/Settings.class differ diff --git a/target/classes/com/codlex/thermocycler/logic/State.class b/target/classes/com/codlex/thermocycler/logic/State.class new file mode 100644 index 0000000..bb87d9e Binary files /dev/null and b/target/classes/com/codlex/thermocycler/logic/State.class differ diff --git a/target/classes/com/codlex/thermocycler/logic/StateLogic.class b/target/classes/com/codlex/thermocycler/logic/StateLogic.class new file mode 100644 index 0000000..3e1ef2c Binary files /dev/null and b/target/classes/com/codlex/thermocycler/logic/StateLogic.class differ diff --git a/target/classes/com/codlex/thermocycler/logic/Switch$SwitchState.class b/target/classes/com/codlex/thermocycler/logic/Switch$SwitchState.class new file mode 100644 index 0000000..9090e99 Binary files /dev/null and b/target/classes/com/codlex/thermocycler/logic/Switch$SwitchState.class differ diff --git a/target/classes/com/codlex/thermocycler/logic/Switch.class b/target/classes/com/codlex/thermocycler/logic/Switch.class new file mode 100644 index 0000000..f1c3132 Binary files /dev/null and b/target/classes/com/codlex/thermocycler/logic/Switch.class differ diff --git a/target/classes/com/codlex/thermocycler/logic/Thermocycler.class b/target/classes/com/codlex/thermocycler/logic/Thermocycler.class new file mode 100644 index 0000000..22dc23b Binary files /dev/null and b/target/classes/com/codlex/thermocycler/logic/Thermocycler.class differ diff --git a/target/classes/com/codlex/thermocycler/logic/ThermocyclerWorker.class b/target/classes/com/codlex/thermocycler/logic/ThermocyclerWorker.class new file mode 100644 index 0000000..2d33c3d Binary files /dev/null and b/target/classes/com/codlex/thermocycler/logic/ThermocyclerWorker.class differ diff --git a/target/classes/com/codlex/thermocycler/logic/Translator.class b/target/classes/com/codlex/thermocycler/logic/Translator.class new file mode 100644 index 0000000..553e52a Binary files /dev/null and b/target/classes/com/codlex/thermocycler/logic/Translator.class differ diff --git a/target/classes/com/codlex/thermocycler/logic/bath/Bath.class b/target/classes/com/codlex/thermocycler/logic/bath/Bath.class new file mode 100644 index 0000000..28b454e Binary files /dev/null and b/target/classes/com/codlex/thermocycler/logic/bath/Bath.class differ diff --git a/target/classes/com/codlex/thermocycler/logic/bath/BathFactory.class b/target/classes/com/codlex/thermocycler/logic/bath/BathFactory.class new file mode 100644 index 0000000..701d125 Binary files /dev/null and b/target/classes/com/codlex/thermocycler/logic/bath/BathFactory.class differ diff --git a/target/classes/com/codlex/thermocycler/logic/bath/WaterPump.class b/target/classes/com/codlex/thermocycler/logic/bath/WaterPump.class new file mode 100644 index 0000000..55fe4ed Binary files /dev/null and b/target/classes/com/codlex/thermocycler/logic/bath/WaterPump.class differ diff --git a/target/classes/com/codlex/thermocycler/logic/bath/cold/ColdBath.class b/target/classes/com/codlex/thermocycler/logic/bath/cold/ColdBath.class new file mode 100644 index 0000000..f9732a5 Binary files /dev/null and b/target/classes/com/codlex/thermocycler/logic/bath/cold/ColdBath.class differ diff --git a/target/classes/com/codlex/thermocycler/logic/bath/cold/Cooler.class b/target/classes/com/codlex/thermocycler/logic/bath/cold/Cooler.class new file mode 100644 index 0000000..8bcff37 Binary files /dev/null and b/target/classes/com/codlex/thermocycler/logic/bath/cold/Cooler.class differ diff --git a/target/classes/com/codlex/thermocycler/logic/bath/hot/Heater.class b/target/classes/com/codlex/thermocycler/logic/bath/hot/Heater.class new file mode 100644 index 0000000..afa240c Binary files /dev/null and b/target/classes/com/codlex/thermocycler/logic/bath/hot/Heater.class differ diff --git a/target/classes/com/codlex/thermocycler/logic/bath/hot/HotBath.class b/target/classes/com/codlex/thermocycler/logic/bath/hot/HotBath.class new file mode 100644 index 0000000..ff158d4 Binary files /dev/null and b/target/classes/com/codlex/thermocycler/logic/bath/hot/HotBath.class differ diff --git a/target/classes/com/codlex/thermocycler/logic/bath/sensors/LevelSensor.class b/target/classes/com/codlex/thermocycler/logic/bath/sensors/LevelSensor.class new file mode 100644 index 0000000..fa6a208 Binary files /dev/null and b/target/classes/com/codlex/thermocycler/logic/bath/sensors/LevelSensor.class differ diff --git a/target/classes/com/codlex/thermocycler/logic/bath/sensors/TemperatureSensor.class b/target/classes/com/codlex/thermocycler/logic/bath/sensors/TemperatureSensor.class new file mode 100644 index 0000000..8d40226 Binary files /dev/null and b/target/classes/com/codlex/thermocycler/logic/bath/sensors/TemperatureSensor.class differ diff --git a/target/classes/com/codlex/thermocycler/view/TestController.class b/target/classes/com/codlex/thermocycler/view/TestController.class new file mode 100644 index 0000000..f121c33 Binary files /dev/null and b/target/classes/com/codlex/thermocycler/view/TestController.class differ diff --git a/target/classes/com/codlex/thermocycler/view/TestScene.fxml b/target/classes/com/codlex/thermocycler/view/TestScene.fxml new file mode 100644 index 0000000..abc7619 --- /dev/null +++ b/target/classes/com/codlex/thermocycler/view/TestScene.fxml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +